fix: improve CodexLens env defaults, self-exclusion, and route handling

- Adjust env defaults (embed batch 64, workers 2) and add HNSW/chunking params
- Exclude .codexlens directory from indexing and file watching
- Expand codexlens-routes with improved validation and error handling
- Enhance integration tests for broader route coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
catlog22
2026-03-19 10:34:18 +08:00
parent 683b85228f
commit 00672ec8e5
11 changed files with 478 additions and 191 deletions

View File

@@ -85,7 +85,7 @@ const API_ONLY_KEYS = new Set([
const FIELD_DEFAULTS: Record<string, string> = {
CODEXLENS_EMBED_API_MODEL: 'text-embedding-3-small',
CODEXLENS_EMBED_DIM: '1536',
CODEXLENS_EMBED_BATCH_SIZE: '512',
CODEXLENS_EMBED_BATCH_SIZE: '64',
CODEXLENS_EMBED_API_CONCURRENCY: '4',
CODEXLENS_BINARY_TOP_K: '200',
CODEXLENS_ANN_TOP_K: '50',
@@ -93,7 +93,11 @@ const FIELD_DEFAULTS: Record<string, string> = {
CODEXLENS_FUSION_K: '60',
CODEXLENS_RERANKER_TOP_K: '20',
CODEXLENS_RERANKER_BATCH_SIZE: '32',
CODEXLENS_INDEX_WORKERS: '4',
CODEXLENS_INDEX_WORKERS: '2',
CODEXLENS_CODE_AWARE_CHUNKING: 'true',
CODEXLENS_MAX_FILE_SIZE: '1000000',
CODEXLENS_HNSW_EF: '150',
CODEXLENS_HNSW_M: '32',
};
// Collect all keys
@@ -103,6 +107,15 @@ function buildEmptyEnv(): Record<string, string> {
return Object.fromEntries(ALL_KEYS.map((k) => [k, '']));
}
function buildEffectiveEnv(
values: Record<string, string>,
defaults: Record<string, string>,
): Record<string, string> {
return Object.fromEntries(
ALL_KEYS.map((key) => [key, values[key] ?? defaults[key] ?? '']),
);
}
// ========================================
// Sensitive field with show/hide toggle
// ========================================
@@ -142,30 +155,28 @@ function SensitiveInput({ value, onChange, id }: SensitiveInputProps) {
export function EnvSettingsTab() {
const { formatMessage } = useIntl();
const { data: serverEnv, isLoading } = useCodexLensEnv();
const { data: envData, isLoading } = useCodexLensEnv();
const { saveEnv, isSaving } = useSaveCodexLensEnv();
const [embedMode, setEmbedMode] = useState<EmbedMode>('local');
const [localEnv, setLocalEnv] = useState<Record<string, string>>(buildEmptyEnv);
const serverValues = envData?.values ?? {};
const serverDefaults = { ...FIELD_DEFAULTS, ...(envData?.defaults ?? {}) };
const serverRecord = buildEffectiveEnv(serverValues, serverDefaults);
// Sync server state into local when loaded and detect embed mode
useEffect(() => {
if (serverEnv) {
setLocalEnv((prev) => {
const next = { ...prev };
ALL_KEYS.forEach((k) => {
next[k] = serverEnv[k] ?? '';
});
return next;
});
if (envData) {
const nextDefaults = { ...FIELD_DEFAULTS, ...(envData.defaults ?? {}) };
const nextValues = envData.values ?? {};
setLocalEnv(buildEffectiveEnv(nextValues, nextDefaults));
// Auto-detect mode from saved env
if (serverEnv.CODEXLENS_EMBED_API_URL) {
if (nextValues.CODEXLENS_EMBED_API_URL) {
setEmbedMode('api');
}
}
}, [serverEnv]);
const serverRecord = serverEnv ?? {};
}, [envData]);
const isDirty = ALL_KEYS.some((k) => localEnv[k] !== (serverRecord[k] ?? ''));
@@ -174,7 +185,17 @@ export function EnvSettingsTab() {
};
const handleSave = async () => {
await saveEnv(localEnv);
const payload = Object.fromEntries(
Object.entries(localEnv).flatMap(([key, value]) => {
const trimmed = value.trim();
const defaultValue = serverDefaults[key] ?? '';
if (!trimmed || trimmed === defaultValue) {
return [];
}
return [[key, trimmed]];
}),
);
await saveEnv(payload);
};
const handleReset = () => {
@@ -252,7 +273,7 @@ export function EnvSettingsTab() {
<Input
id={field.key}
value={localEnv[field.key] ?? ''}
placeholder={FIELD_DEFAULTS[field.key] ?? ''}
placeholder={serverDefaults[field.key] ?? ''}
onChange={(e) => handleChange(field.key, e.target.value)}
/>
)}

View File

@@ -10,7 +10,7 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { useCodexLensMcpConfig, useCodexLensEnv } from '@/hooks/useCodexLens';
import { installMcpTemplate } from '@/lib/api';
import { addGlobalMcpServer, copyMcpServerToProject } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
export function McpConfigTab() {
@@ -26,16 +26,34 @@ export function McpConfigTab() {
setInstalling(true);
setInstallResult(null);
try {
const res = await installMcpTemplate({
templateName: 'codexlens',
scope,
projectPath: scope === 'project' ? projectPath : undefined,
});
const mcpServers = mcpConfig?.['mcpServers'];
const serverConfig = mcpServers && typeof mcpServers === 'object'
? (mcpServers as Record<string, unknown>).codexlens
: undefined;
if (!serverConfig || typeof serverConfig !== 'object') {
throw new Error(formatMessage({ id: 'codexlens.mcp.noConfig' }));
}
const typedConfig = serverConfig as {
command: string;
args?: string[];
env?: Record<string, string>;
type?: string;
};
if (scope === 'project') {
if (!projectPath) {
throw new Error(formatMessage({ id: 'codexlens.mcp.installError' }));
}
await copyMcpServerToProject('codexlens', typedConfig, projectPath);
} else {
await addGlobalMcpServer('codexlens', typedConfig);
}
setInstallResult({
ok: !!res.success,
msg: res.success
? formatMessage({ id: 'codexlens.mcp.installSuccess' })
: (res.error ?? formatMessage({ id: 'codexlens.mcp.installError' })),
ok: true,
msg: formatMessage({ id: 'codexlens.mcp.installSuccess' }),
});
} catch (err) {
setInstallResult({ ok: false, msg: (err as Error).message });
@@ -44,7 +62,7 @@ export function McpConfigTab() {
}
};
const hasApiUrl = !!(envData?.CODEXLENS_EMBED_API_URL);
const hasApiUrl = !!(envData?.values.CODEXLENS_EMBED_API_URL);
const embedMode = hasApiUrl ? 'API' : 'Local fastembed';
const configJson = mcpConfig ? JSON.stringify(mcpConfig, null, 2) : '';

View File

@@ -23,7 +23,7 @@ export function ModelManagerTab() {
const { downloadModel, isDownloading } = useDownloadModel();
const { deleteModel, isDeleting } = useDeleteModel();
const hasApiUrl = !!(envData?.CODEXLENS_EMBED_API_URL);
const hasApiUrl = !!(envData?.values.CODEXLENS_EMBED_API_URL);
const embedMode = hasApiUrl ? 'API' : 'Local fastembed';
const models: ModelEntry[] = modelsData ?? [];

View File

@@ -4,6 +4,7 @@
// TanStack Query hooks for CodexLens v2 API management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchApi } from '@/lib/api';
// ========================================
// Domain types (exported for component use)
@@ -25,11 +26,19 @@ export interface IndexStatusData {
}
export type McpConfigData = Record<string, unknown>;
export interface CodexLensEnvData {
values: Record<string, string>;
defaults: Record<string, string>;
}
// Internal API response wrappers
interface ModelsResponse { success: boolean; models: ModelEntry[] }
interface IndexStatusResponse { success: boolean; status: IndexStatusData }
interface EnvResponse { success: boolean; env: Record<string, string> }
interface EnvResponse {
success: boolean;
env: Record<string, string>;
defaults?: Record<string, string>;
}
interface McpConfigResponse { success: boolean; config: McpConfigData }
// ========================================
@@ -44,27 +53,6 @@ export const codexLensKeys = {
mcpConfig: () => [...codexLensKeys.all, 'mcpConfig'] as const,
};
// ========================================
// Internal fetch helper (mirrors fetchApi pattern from api.ts)
// ========================================
async function fetchApi<T>(url: string, options: RequestInit = {}): Promise<T> {
const headers = new Headers(options.headers);
if (options.body && typeof options.body === 'string') {
headers.set('Content-Type', 'application/json');
}
const response = await fetch(url, {
...options,
headers,
credentials: 'same-origin',
});
if (!response.ok) {
const text = await response.text().catch(() => response.statusText);
throw new Error(text || `Request failed: ${response.status}`);
}
return response.json() as Promise<T>;
}
// ========================================
// Models Hooks
// ========================================
@@ -174,7 +162,11 @@ export function useRebuildIndex() {
export function useCodexLensEnv() {
return useQuery({
queryKey: codexLensKeys.env(),
queryFn: () => fetchApi<EnvResponse>('/api/codexlens/env').then(r => r.env),
queryFn: () =>
fetchApi<EnvResponse>('/api/codexlens/env').then((r): CodexLensEnvData => ({
values: r.env ?? {},
defaults: r.defaults ?? {},
})),
staleTime: 60_000,
});
}

View File

@@ -237,7 +237,7 @@ export async function initializeCsrfToken(): Promise<void> {
/**
* Base fetch wrapper with CSRF token and error handling
*/
async function fetchApi<T>(
export async function fetchApi<T>(
url: string,
options: RequestInit = {}
): Promise<T> {