feat: 添加工作空间索引状态接口,增强 CodexLens 状态检查功能,支持前端显示索引信息

This commit is contained in:
catlog22
2026-01-07 11:36:06 +08:00
parent 1bd3d9c9bf
commit 6aa79c6dc9
5 changed files with 300 additions and 7 deletions

View File

@@ -2366,5 +2366,121 @@ except Exception as e:
return true;
}
// API: Get workspace index status (FTS and Vector coverage percentages)
if (pathname === '/api/codexlens/workspace-status') {
try {
const projectPath = url.searchParams.get('path') || initialPath;
// Check if CodexLens is installed first
const venvStatus = await checkVenvStatus();
if (!venvStatus.ready) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
hasIndex: false,
fts: { indexed: false, percent: 0 },
vector: { indexed: false, percent: 0 },
message: 'CodexLens not installed'
}));
return true;
}
let ftsStatus = { indexed: false, percent: 0, totalFiles: 0, indexedFiles: 0 };
let vectorStatus = { indexed: false, percent: 0, totalFiles: 0, filesWithEmbeddings: 0, totalChunks: 0 };
let hasIndex = false;
let indexRoot = '';
// First, get project info to check if index exists
const projectsResult = await executeCodexLens(['projects', 'show', projectPath, '--json']);
if (projectsResult.success && projectsResult.output) {
try {
const projectData = extractJSON(projectsResult.output);
if (projectData.success && projectData.result) {
const project = projectData.result;
hasIndex = true;
indexRoot = project.index_root || '';
// FTS is always 100% when index exists
ftsStatus = {
indexed: true,
percent: 100,
totalFiles: project.total_files || 0,
indexedFiles: project.total_files || 0
};
// Now get embeddings status for this specific project
const statusResult = await executeCodexLens(['index', 'status', projectPath, '--json']);
if (statusResult.success && statusResult.output) {
try {
const status = extractJSON(statusResult.output);
if (status.success && status.result && status.result.embeddings) {
const embeddings = status.result.embeddings;
// Find the project-specific embedding info from indexes array
const indexes = embeddings.indexes || [];
let projectEmbedding = null;
// Look for matching project by path or name
const { basename, resolve } = await import('path');
const normalizedPath = resolve(projectPath).toLowerCase();
const projectName = basename(projectPath);
for (const idx of indexes) {
const idxPath = (idx.path || '').toLowerCase();
const idxProject = (idx.project || '').toLowerCase();
if (idxPath.includes(normalizedPath.replace(/\\/g, '/')) ||
idxPath.includes(normalizedPath) ||
idxProject === projectName.toLowerCase()) {
projectEmbedding = idx;
break;
}
}
if (projectEmbedding) {
vectorStatus = {
indexed: projectEmbedding.has_embeddings || false,
percent: projectEmbedding.coverage_percent || 0,
totalFiles: projectEmbedding.total_files || project.total_files || 0,
filesWithEmbeddings: Math.round((projectEmbedding.coverage_percent || 0) * (projectEmbedding.total_files || 0) / 100),
totalChunks: projectEmbedding.total_chunks || 0
};
} else {
// No specific project found, use aggregated stats
vectorStatus = {
indexed: embeddings.indexes_with_embeddings > 0,
percent: 0,
totalFiles: project.total_files || 0,
filesWithEmbeddings: 0,
totalChunks: 0
};
}
}
} catch (e) {
console.error('[CodexLens] Failed to parse index status:', e.message);
}
}
}
} catch (e) {
console.error('[CodexLens] Failed to parse project data:', e.message);
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
hasIndex,
indexRoot,
path: projectPath,
fts: ftsStatus,
vector: vectorStatus
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
return true;
}
return false;
}

View File

@@ -375,6 +375,14 @@ const i18n = {
'codexlens.cleanFailed': 'Failed to clean indexes',
'codexlens.loadingConfig': 'Loading configuration...',
// Workspace Index Status
'codexlens.workspaceStatus': 'Workspace Index Status',
'codexlens.noIndexFound': 'No index found for current workspace',
'codexlens.filesIndexed': 'files indexed',
'codexlens.filesWithEmbeddings': 'files with embeddings',
'codexlens.vectorSearchEnabled': 'Vector search enabled',
'codexlens.vectorSearchPartial': 'Vector search requires ≥50% coverage',
// Model Management
'codexlens.semanticDeps': 'Semantic Dependencies',
'codexlens.checkingDeps': 'Checking dependencies...',
@@ -2384,6 +2392,14 @@ const i18n = {
'codexlens.cleanFailed': '清理索引失败',
'codexlens.loadingConfig': '加载配置中...',
// 工作空间索引状态
'codexlens.workspaceStatus': '工作空间索引状态',
'codexlens.noIndexFound': '当前工作空间未找到索引',
'codexlens.filesIndexed': '个文件已索引',
'codexlens.filesWithEmbeddings': '个文件已嵌入',
'codexlens.vectorSearchEnabled': '向量搜索已启用',
'codexlens.vectorSearchPartial': '向量搜索需要≥50%覆盖率',
// 模型管理
'codexlens.semanticDeps': '语义搜索依赖',
'codexlens.checkingDeps': '检查依赖中...',

View File

@@ -18,6 +18,117 @@ function escapeHtml(str) {
.replace(/'/g, ''');
}
// ============================================================
// WORKSPACE INDEX STATUS
// ============================================================
/**
* Refresh workspace index status (FTS and Vector coverage)
*/
async function refreshWorkspaceIndexStatus() {
var container = document.getElementById('workspaceIndexStatusContent');
if (!container) return;
// Show loading state
container.innerHTML = '<div class="text-xs text-muted-foreground text-center py-2">' +
'<i data-lucide="loader-2" class="w-4 h-4 animate-spin inline mr-1"></i> ' + (t('common.loading') || 'Loading...') +
'</div>';
if (window.lucide) lucide.createIcons();
try {
var response = await fetch('/api/codexlens/workspace-status');
var result = await response.json();
if (result.success) {
var html = '';
if (!result.hasIndex) {
// No index for current workspace
html = '<div class="text-center py-3">' +
'<div class="text-sm text-muted-foreground mb-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
'</div>' +
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
(t('codexlens.createIndex') || 'Create Index') +
'</button>' +
'</div>';
} else {
// FTS Status
var ftsPercent = result.fts.percent || 0;
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
html += '<div class="space-y-1">' +
'<div class="flex items-center justify-between text-xs">' +
'<span class="flex items-center gap-1.5">' +
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
'</span>' +
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
'</div>' +
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
'</div>' +
'<div class="text-xs text-muted-foreground">' +
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
'</div>' +
'</div>';
// Vector Status
var vectorPercent = result.vector.percent || 0;
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
html += '<div class="space-y-1 mt-3">' +
'<div class="flex items-center justify-between text-xs">' +
'<span class="flex items-center gap-1.5">' +
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
'</span>' +
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
'</div>' +
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
'</div>' +
'<div class="text-xs text-muted-foreground">' +
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
'</div>' +
'</div>';
// Vector search availability indicator
if (vectorPercent >= 50) {
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
'</div>';
} else if (vectorPercent > 0) {
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
'</div>';
}
}
container.innerHTML = html;
} else {
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(result.error || t('common.error') || 'Error loading status') +
'</div>';
}
} catch (err) {
console.error('[CodexLens] Failed to load workspace status:', err);
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
(t('common.error') || 'Error') + ': ' + err.message +
'</div>';
}
if (window.lucide) lucide.createIcons();
}
// ============================================================
// CODEXLENS CONFIGURATION MODAL
// ============================================================
@@ -29,9 +140,23 @@ async function showCodexLensConfigModal() {
try {
showRefreshToast(t('codexlens.loadingConfig'), 'info');
// Fetch current config
const response = await fetch('/api/codexlens/config');
const config = await response.json();
// Fetch current config and status in parallel
const [configResponse, statusResponse] = await Promise.all([
fetch('/api/codexlens/config'),
fetch('/api/codexlens/status')
]);
const config = await configResponse.json();
const status = await statusResponse.json();
// Update window.cliToolsStatus to ensure isInstalled is correct
if (!window.cliToolsStatus) {
window.cliToolsStatus = {};
}
window.cliToolsStatus.codexlens = {
...(window.cliToolsStatus.codexlens || {}),
installed: status.ready || false,
version: status.version || null
};
const modalHtml = buildCodexLensConfigContent(config);
@@ -147,6 +272,25 @@ function buildCodexLensConfigContent(config) {
'</div>' +
'</div>' +
// Workspace Index Status (only if installed)
(isInstalled
? '<div class="rounded-lg border border-border p-4 mb-4 bg-card" id="workspaceIndexStatus">' +
'<div class="flex items-center justify-between mb-3">' +
'<h4 class="text-sm font-medium flex items-center gap-2">' +
'<i data-lucide="hard-drive" class="w-4 h-4"></i> ' + (t('codexlens.workspaceStatus') || 'Workspace Index Status') +
'</h4>' +
'<button onclick="refreshWorkspaceIndexStatus()" class="text-xs text-primary hover:underline flex items-center gap-1" title="Refresh status">' +
'<i data-lucide="refresh-cw" class="w-3 h-3"></i> ' + (t('common.refresh') || 'Refresh') +
'</button>' +
'</div>' +
'<div id="workspaceIndexStatusContent" class="space-y-3">' +
'<div class="text-xs text-muted-foreground text-center py-2">' +
'<i data-lucide="loader-2" class="w-4 h-4 animate-spin inline mr-1"></i> Loading...' +
'</div>' +
'</div>' +
'</div>'
: '') +
// Index Operations - 4 buttons grid
'<div class="space-y-2">' +
'<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">' + (t('codexlens.indexOperations') || 'Index Operations') + '</h4>' +
@@ -547,6 +691,9 @@ function initCodexLensConfigEvents(currentConfig) {
// Load model lists (embedding and reranker)
loadModelList();
loadRerankerModelList();
// Load workspace index status
refreshWorkspaceIndexStatus();
}
// ============================================================

View File

@@ -87,7 +87,7 @@ async function renderHookManager() {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${renderWizardCard('memory-update')}
${renderWizardCard('memory-setup')}
${renderWizardCard('danger-protection')}
${renderWizardCard('skill-context')}
</div>
</div>
@@ -216,10 +216,12 @@ function renderWizardCard(wizardId) {
// Get translated wizard name and description
const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') :
wizardId === 'memory-setup' ? t('hook.wizard.memorySetup') :
wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name;
wizardId === 'skill-context' ? t('hook.wizard.skillContext') :
wizardId === 'danger-protection' ? t('hook.wizard.dangerProtection') : wizard.name;
const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') :
wizardId === 'memory-setup' ? t('hook.wizard.memorySetupDesc') :
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description;
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') :
wizardId === 'danger-protection' ? t('hook.wizard.dangerProtectionDesc') : wizard.description;
// Translate options
const getOptionName = (wizardId, optId) => {
@@ -237,6 +239,12 @@ function renderWizardCard(wizardId) {
if (optId === 'keyword') return t('hook.wizard.keywordMatching');
if (optId === 'auto') return t('hook.wizard.autoDetection');
}
if (wizardId === 'danger-protection') {
if (optId === 'bash-confirm') return t('hook.wizard.dangerBashConfirm');
if (optId === 'file-protection') return t('hook.wizard.dangerFileProtection');
if (optId === 'git-destructive') return t('hook.wizard.dangerGitDestructive');
if (optId === 'network-confirm') return t('hook.wizard.dangerNetworkConfirm');
}
return wizard.options.find(o => o.id === optId)?.name || '';
};
@@ -255,6 +263,12 @@ function renderWizardCard(wizardId) {
if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc');
if (optId === 'auto') return t('hook.wizard.autoDetectionDesc');
}
if (wizardId === 'danger-protection') {
if (optId === 'bash-confirm') return t('hook.wizard.dangerBashConfirmDesc');
if (optId === 'file-protection') return t('hook.wizard.dangerFileProtectionDesc');
if (optId === 'git-destructive') return t('hook.wizard.dangerGitDestructiveDesc');
if (optId === 'network-confirm') return t('hook.wizard.dangerNetworkConfirmDesc');
}
return wizard.options.find(o => o.id === optId)?.description || '';
};

View File

@@ -903,7 +903,7 @@ def status(
schema_version = store._get_schema_version(conn)
# Check if dual FTS tables exist
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('search_fts_exact', 'search_fts_fuzzy')"
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('files_fts_exact', 'files_fts_fuzzy')"
)
fts_tables = [row[0] for row in cursor.fetchall()]
has_dual_fts = len(fts_tables) == 2