From 90a1321aac1a8e5c7142e59dcfd860f63b656047 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 12 Jan 2026 17:34:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=A4=A7=E5=B0=8F=E8=AE=A1=E7=AE=97=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B5=8C=E5=85=A5=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/routes/codexlens/config-handlers.ts | 120 ++++++++++++++++-- .../core/routes/codexlens/index-handlers.ts | 29 ++++- .../routes/codexlens/semantic-handlers.ts | 116 +++++++++++++++-- .../dashboard-js/views/codexlens-manager.js | 36 ++++-- .../src/codexlens/cli/embedding_manager.py | 91 ++++++++++++- codex-lens/src/codexlens/config.py | 105 ++++++++++----- 6 files changed, 425 insertions(+), 72 deletions(-) diff --git a/ccw/src/core/routes/codexlens/config-handlers.ts b/ccw/src/core/routes/codexlens/config-handlers.ts index cc775339..e45eb9aa 100644 --- a/ccw/src/core/routes/codexlens/config-handlers.ts +++ b/ccw/src/core/routes/codexlens/config-handlers.ts @@ -93,6 +93,96 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise 0 ? Math.round((indexedFiles / totalFiles) * 100) : 0; + + // Vector percentage (files with embeddings) + const vectorPercent = totalFiles > 0 ? Math.round((filesWithEmbeddings / totalFiles) * 1000) / 10 : 0; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + hasIndex: true, + path: initialPath, + fts: { + percent: ftsPercent, + indexedFiles, + totalFiles + }, + vector: { + percent: vectorPercent, + filesWithEmbeddings, + totalFiles, + totalChunks + } + })); + } catch (err: unknown) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) })); + } + return true; + } + // API: CodexLens Bootstrap (Install) if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') { handlePostRequest(req, res, async () => { @@ -164,9 +254,10 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise { + if (project.source_root && ( + project.source_root.includes('\\Temp\\') || + project.source_root.includes('/tmp/') || + project.total_files === 0 + )) { + return false; + } + return true; + }); + responseData.index_count = validProjects.length; } } catch (e: unknown) { - console.error('[CodexLens] Failed to parse status:', e instanceof Error ? e.message : String(e)); - console.error('[CodexLens] Status output:', (statusResult.output ?? '').substring(0, 200)); + console.error('[CodexLens] Failed to parse projects list:', e instanceof Error ? e.message : String(e)); + console.error('[CodexLens] Projects output:', (projectsResult.output ?? '').substring(0, 200)); } } diff --git a/ccw/src/core/routes/codexlens/index-handlers.ts b/ccw/src/core/routes/codexlens/index-handlers.ts index 36e5b341..90f9be55 100644 --- a/ccw/src/core/routes/codexlens/index-handlers.ts +++ b/ccw/src/core/routes/codexlens/index-handlers.ts @@ -5,6 +5,7 @@ import { cancelIndexing, checkVenvStatus, + checkSemanticStatus, ensureLiteLLMEmbedderReady, executeCodexLens, isIndexingInProgress, @@ -230,11 +231,29 @@ export async function handleCodexLensIndexRoutes(ctx: RouteContext): Promise 0 ? embeddingBackend : 'fastembed'; const resolvedMaxWorkers = typeof maxWorkers === 'number' ? maxWorkers : Number(maxWorkers); - // Ensure LiteLLM backend dependencies are installed before running the CLI - if (resolvedIndexType !== 'normal' && resolvedEmbeddingBackend === 'litellm') { - const installResult = await ensureLiteLLMEmbedderReady(); - if (!installResult.success) { - return { success: false, error: installResult.error || 'Failed to prepare LiteLLM embedder', status: 500 }; + // Pre-check: Verify embedding backend availability before proceeding with vector indexing + // This prevents silent degradation where vector indexing is skipped without error + if (resolvedIndexType !== 'normal') { + if (resolvedEmbeddingBackend === 'litellm') { + // For litellm backend, ensure ccw-litellm is installed + const installResult = await ensureLiteLLMEmbedderReady(); + if (!installResult.success) { + return { + success: false, + error: installResult.error || 'LiteLLM embedding backend is not available. Please install ccw-litellm first.', + status: 500 + }; + } + } else { + // For fastembed backend (default), check semantic dependencies + const semanticStatus = await checkSemanticStatus(); + if (!semanticStatus.available) { + return { + success: false, + error: semanticStatus.error || 'FastEmbed semantic backend is not available. Please install semantic dependencies first (CodeLens Settings → Install Semantic).', + status: 500 + }; + } } } diff --git a/ccw/src/core/routes/codexlens/semantic-handlers.ts b/ccw/src/core/routes/codexlens/semantic-handlers.ts index e969afc9..bd1f7852 100644 --- a/ccw/src/core/routes/codexlens/semantic-handlers.ts +++ b/ccw/src/core/routes/codexlens/semantic-handlers.ts @@ -9,7 +9,7 @@ import { installSemantic, } from '../../../tools/codex-lens.js'; import type { GpuMode } from '../../../tools/codex-lens.js'; -import { loadLiteLLMApiConfig } from '../../../config/litellm-api-config-manager.js'; +import { loadLiteLLMApiConfig, getAvailableModelsForType, getProvider, getAllProviders } from '../../../config/litellm-api-config-manager.js'; import { isUvAvailable, createCodexLensUvManager, @@ -317,16 +317,21 @@ export async function handleCodexLensSemanticRoutes(ctx: RouteContext): Promise< config_source: 'default' }; - // Load LiteLLM endpoints for dropdown + // Load LiteLLM reranker models for dropdown (from litellm-api-config providers) try { - const litellmConfig = loadLiteLLMApiConfig(initialPath); - if (litellmConfig.endpoints && Array.isArray(litellmConfig.endpoints)) { - rerankerConfig.litellm_endpoints = litellmConfig.endpoints.map( - (ep: any) => ep.alias || ep.name || ep.baseUrl - ).filter(Boolean); + const availableRerankerModels = getAvailableModelsForType(initialPath, 'reranker'); + if (availableRerankerModels && Array.isArray(availableRerankerModels)) { + // Return full model info for frontend to use + (rerankerConfig as any).litellm_models = availableRerankerModels.map((m: any) => ({ + modelId: m.modelId, + modelName: m.modelName, + providers: m.providers + })); + // Keep litellm_endpoints for backward compatibility (just model IDs) + rerankerConfig.litellm_endpoints = availableRerankerModels.map((m: any) => m.modelId); } } catch { - // LiteLLM config not available, continue with empty endpoints + // LiteLLM config not available, continue with empty models } // If CodexLens is installed, try to get actual config @@ -407,6 +412,97 @@ export async function handleCodexLensSemanticRoutes(ctx: RouteContext): Promise< try { const updates: string[] = []; + // Special handling for litellm backend - auto-configure from litellm-api-config + if (resolvedBackend === 'litellm' && (resolvedModelName || resolvedLiteLLMEndpoint)) { + const selectedModel = resolvedModelName || resolvedLiteLLMEndpoint; + + // Find the provider that has this model + const providers = getAllProviders(initialPath); + let providerWithModel: any = null; + let foundModel: any = null; + + for (const provider of providers) { + if (!provider.enabled || !provider.rerankerModels) continue; + const model = provider.rerankerModels.find((m: any) => m.id === selectedModel && m.enabled); + if (model) { + providerWithModel = provider; + foundModel = model; + break; + } + } + + if (providerWithModel) { + // Set backend to litellm + const backendResult = await executeCodexLens(['config', 'set', 'reranker_backend', 'litellm', '--json']); + if (backendResult.success) updates.push('backend'); + + // Set model + const modelResult = await executeCodexLens(['config', 'set', 'reranker_model', selectedModel, '--json']); + if (modelResult.success) updates.push('model_name'); + + // Auto-configure API credentials from provider + // Write to CodexLens .env file for persistence + const { writeFileSync, existsSync, readFileSync } = await import('fs'); + const { join } = await import('path'); + const { homedir } = await import('os'); + + const codexlensDir = join(homedir(), '.codexlens'); + const envFile = join(codexlensDir, '.env'); + + // Read existing .env content + let envContent = ''; + if (existsSync(envFile)) { + envContent = readFileSync(envFile, 'utf-8'); + } + + // Update or add RERANKER_API_KEY and RERANKER_API_BASE + const apiKey = providerWithModel.apiKey; + const apiBase = providerWithModel.apiBase; + + // Helper to update env var in content + const updateEnvVar = (content: string, key: string, value: string): string => { + const regex = new RegExp(`^${key}=.*$`, 'm'); + const newLine = `${key}="${value}"`; + if (regex.test(content)) { + return content.replace(regex, newLine); + } else { + return content.trim() + '\n' + newLine; + } + }; + + if (apiKey) { + envContent = updateEnvVar(envContent, 'RERANKER_API_KEY', apiKey); + envContent = updateEnvVar(envContent, 'CODEXLENS_RERANKER_API_KEY', apiKey); + process.env.RERANKER_API_KEY = apiKey; + updates.push('api_key (auto-configured)'); + } + if (apiBase) { + envContent = updateEnvVar(envContent, 'RERANKER_API_BASE', apiBase); + envContent = updateEnvVar(envContent, 'CODEXLENS_RERANKER_API_BASE', apiBase); + process.env.RERANKER_API_BASE = apiBase; + updates.push('api_base (auto-configured)'); + } + + // Write updated .env + writeFileSync(envFile, envContent.trim() + '\n', 'utf-8'); + + return { + success: true, + message: `LiteLLM backend configured with model: ${selectedModel}`, + updated_fields: updates, + provider: providerWithModel.name, + auto_configured: true + }; + } else { + return { + success: false, + error: `Model "${selectedModel}" not found in any enabled LiteLLM provider. Please configure it in API Settings first.`, + status: 400 + }; + } + } + + // Standard handling for non-litellm backends // Set backend if (resolvedBackend) { const result = await executeCodexLens(['config', 'set', 'reranker_backend', resolvedBackend, '--json']); @@ -425,8 +521,8 @@ export async function handleCodexLensSemanticRoutes(ctx: RouteContext): Promise< if (result.success) updates.push('api_provider'); } - // Set LiteLLM endpoint - if (resolvedLiteLLMEndpoint) { + // Set LiteLLM endpoint (for backward compatibility) + if (resolvedLiteLLMEndpoint && resolvedBackend !== 'litellm') { const result = await executeCodexLens([ 'config', 'set', diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 6a548e6c..89a4149c 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -6057,6 +6057,7 @@ function buildRerankerConfigContent(config) { const availableBackends = config.available_backends || ['onnx', 'api', 'litellm', 'legacy']; const apiProviders = config.api_providers || ['siliconflow', 'cohere', 'jina']; const litellmEndpoints = config.litellm_endpoints || []; + const litellmModels = config.litellm_models || []; // Rich model info with providers // ONNX models const onnxModels = [ @@ -6067,11 +6068,12 @@ function buildRerankerConfigContent(config) { ]; // Build backend options + const hasLitellmModels = litellmModels.length > 0 || litellmEndpoints.length > 0; const backendOptions = availableBackends.map(function(b) { const labels = { 'onnx': 'ONNX (Local, Optimum)', - 'api': 'API (SiliconFlow/Cohere/Jina)', - 'litellm': 'LiteLLM (Custom Endpoint)', + 'api': 'API (Manual Config)', + 'litellm': hasLitellmModels ? 'LiteLLM (Auto-configured)' : 'LiteLLM (Not configured)', 'legacy': 'Legacy (SentenceTransformers)' }; return ''; @@ -6087,12 +6089,21 @@ function buildRerankerConfigContent(config) { return ''; }).join(''); - // Build LiteLLM endpoint options - const litellmOptions = litellmEndpoints.length > 0 - ? litellmEndpoints.map(function(ep) { - return ''; + // Build LiteLLM model options (use rich model data if available) + const litellmOptions = litellmModels.length > 0 + ? litellmModels.map(function(m) { + // Display: "ModelName (Provider)" for better UX + const providerNames = m.providers && m.providers.length > 0 + ? m.providers.join(', ') + : 'Unknown'; + const displayName = m.modelName + ' (' + providerNames + ')'; + return ''; }).join('') - : ''; + : (litellmEndpoints.length > 0 + ? litellmEndpoints.map(function(ep) { + return ''; + }).join('') + : ''); return '