Refactor code structure for improved readability and maintainability

This commit is contained in:
catlog22
2026-02-16 13:09:47 +08:00
parent 111b0f6809
commit 02250bd4dc
22 changed files with 6330 additions and 216 deletions

View File

@@ -157,6 +157,7 @@ const mockMessages: Record<Locale, Record<string, string>> = {
'codexlens.envGroup.reranker': 'Reranker',
'codexlens.envGroup.concurrency': 'Concurrency',
'codexlens.envGroup.cascade': 'Cascade Search',
'codexlens.envGroup.indexing': 'Indexing',
'codexlens.envGroup.chunking': 'Chunking',
'codexlens.envField.backend': 'Backend',
'codexlens.envField.model': 'Model',
@@ -175,6 +176,9 @@ const mockMessages: Record<Locale, Record<string, string>> = {
'codexlens.envField.searchStrategy': 'Search Strategy',
'codexlens.envField.coarseK': 'Coarse K',
'codexlens.envField.fineK': 'Fine K',
'codexlens.envField.useAstGrep': 'Use ast-grep',
'codexlens.envField.staticGraphEnabled': 'Static Graph',
'codexlens.envField.staticGraphRelationshipTypes': 'Relationship Types',
'codexlens.envField.stripComments': 'Strip Comments',
'codexlens.envField.stripDocstrings': 'Strip Docstrings',
'codexlens.envField.testFilePenalty': 'Test File Penalty',
@@ -421,6 +425,7 @@ const mockMessages: Record<Locale, Record<string, string>> = {
'codexlens.envGroup.reranker': '重排序',
'codexlens.envGroup.concurrency': '并发',
'codexlens.envGroup.cascade': '级联搜索',
'codexlens.envGroup.indexing': '索引与解析',
'codexlens.envGroup.chunking': '分块',
'codexlens.envField.backend': '后端',
'codexlens.envField.model': '模型',
@@ -439,6 +444,9 @@ const mockMessages: Record<Locale, Record<string, string>> = {
'codexlens.envField.searchStrategy': '搜索策略',
'codexlens.envField.coarseK': '粗筛 K 值',
'codexlens.envField.fineK': '精筛 K 值',
'codexlens.envField.useAstGrep': '使用 ast-grep',
'codexlens.envField.staticGraphEnabled': '启用静态图',
'codexlens.envField.staticGraphRelationshipTypes': '关系类型',
'codexlens.envField.stripComments': '去除注释',
'codexlens.envField.stripDocstrings': '去除文档字符串',
'codexlens.envField.testFilePenalty': '测试文件惩罚',

View File

@@ -915,6 +915,21 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
settingsDefaults['CODEXLENS_LLM_BATCH_SIZE'] = String(settings.llm.batch_size);
}
// Parsing / indexing settings
if (settings.parsing?.use_astgrep !== undefined) {
settingsDefaults['CODEXLENS_USE_ASTGREP'] = String(settings.parsing.use_astgrep);
}
if (settings.indexing?.static_graph_enabled !== undefined) {
settingsDefaults['CODEXLENS_STATIC_GRAPH_ENABLED'] = String(settings.indexing.static_graph_enabled);
}
if (settings.indexing?.static_graph_relationship_types !== undefined) {
if (Array.isArray(settings.indexing.static_graph_relationship_types)) {
settingsDefaults['CODEXLENS_STATIC_GRAPH_RELATIONSHIP_TYPES'] = settings.indexing.static_graph_relationship_types.join(',');
} else if (typeof settings.indexing.static_graph_relationship_types === 'string') {
settingsDefaults['CODEXLENS_STATIC_GRAPH_RELATIONSHIP_TYPES'] = settings.indexing.static_graph_relationship_types;
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
@@ -1080,7 +1095,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
settings = JSON.parse(settingsContent);
} catch {
// File doesn't exist, create default structure
settings = { embedding: {}, reranker: {}, api: {}, cascade: {}, llm: {} };
settings = { embedding: {}, reranker: {}, api: {}, cascade: {}, llm: {}, parsing: {}, indexing: {} };
}
// Map env vars to settings.json structure
@@ -1105,6 +1120,15 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
'CODEXLENS_CASCADE_FINE_K': { path: ['cascade', 'fine_k'], transform: v => parseInt(v, 10) },
'CODEXLENS_LLM_ENABLED': { path: ['llm', 'enabled'], transform: v => v === 'true' },
'CODEXLENS_LLM_BATCH_SIZE': { path: ['llm', 'batch_size'], transform: v => parseInt(v, 10) },
'CODEXLENS_USE_ASTGREP': { path: ['parsing', 'use_astgrep'], transform: v => v === 'true' },
'CODEXLENS_STATIC_GRAPH_ENABLED': { path: ['indexing', 'static_graph_enabled'], transform: v => v === 'true' },
'CODEXLENS_STATIC_GRAPH_RELATIONSHIP_TYPES': {
path: ['indexing', 'static_graph_relationship_types'],
transform: v => v
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0),
},
'LITELLM_EMBEDDING_MODEL': { path: ['embedding', 'model'] },
'LITELLM_RERANKER_MODEL': { path: ['reranker', 'model'] }
};

View File

@@ -102,10 +102,206 @@ interface UpdateClaudeMdResult {
path?: string;
}
// ========================================
// Explorer API Types (matching frontend)
// ========================================
interface FileSystemNode {
name: string;
path: string;
type: 'file' | 'directory';
children?: FileSystemNode[];
hasClaudeMd?: boolean;
size?: number;
modifiedTime?: string;
extension?: string;
language?: string;
}
interface FileTreeResponse {
rootNodes: FileSystemNode[];
fileCount: number;
directoryCount: number;
totalSize: number;
buildTime: number;
}
interface RootDirectory {
path: string;
name: string;
isWorkspace: boolean;
isGitRoot: boolean;
}
// ========================================
// Helper Functions
// ========================================
/**
* Build recursive file tree
* @param {string} dirPath - Directory path to build tree from
* @param {number} maxDepth - Maximum depth (0 = unlimited)
* @param {boolean} includeHidden - Include hidden files
* @param {number} currentDepth - Current recursion depth
* @returns {Promise<{ node: FileSystemNode, fileCount: number, directoryCount: number, totalSize: number }>}
*/
async function buildFileTree(
dirPath: string,
maxDepth: number = 6,
includeHidden: boolean = false,
currentDepth: number = 0
): Promise<{ node: FileSystemNode | null; fileCount: number; directoryCount: number; totalSize: number }> {
const result = { node: null as FileSystemNode | null, fileCount: 0, directoryCount: 0, totalSize: 0 };
try {
// Normalize path
let normalizedPath = dirPath.replace(/\\/g, '/');
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
}
if (!existsSync(normalizedPath) || !statSync(normalizedPath).isDirectory()) {
return result;
}
const dirName = normalizedPath.split('/').pop() || normalizedPath;
const node: FileSystemNode = {
name: dirName,
path: normalizedPath,
type: 'directory',
children: []
};
// Check for CLAUDE.md
const claudeMdPath = join(normalizedPath, 'CLAUDE.md');
node.hasClaudeMd = existsSync(claudeMdPath);
// Parse .gitignore patterns
const gitignorePath = join(normalizedPath, '.gitignore');
const gitignorePatterns = parseGitignore(gitignorePath);
// Read directory entries
const entries = readdirSync(normalizedPath, { withFileTypes: true });
for (const entry of entries) {
const isDirectory = entry.isDirectory();
// Check if should be ignored
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) {
// Allow hidden files if includeHidden is true and it's .claude or .workflow
if (!includeHidden || (!entry.name.startsWith('.claude') && !entry.name.startsWith('.workflow'))) {
continue;
}
}
const entryPath = join(normalizedPath, entry.name);
if (isDirectory) {
// Recursively build tree for directories
if (maxDepth === 0 || currentDepth < maxDepth - 1) {
const childResult = await buildFileTree(entryPath, maxDepth, includeHidden, currentDepth + 1);
if (childResult.node) {
node.children!.push(childResult.node);
result.fileCount += childResult.fileCount;
result.directoryCount += childResult.directoryCount + 1;
result.totalSize += childResult.totalSize;
}
} else {
// At max depth, just add directory without children
const childNode: FileSystemNode = {
name: entry.name,
path: entryPath.replace(/\\/g, '/'),
type: 'directory'
};
const childClaudeMdPath = join(entryPath, 'CLAUDE.md');
childNode.hasClaudeMd = existsSync(childClaudeMdPath);
node.children!.push(childNode);
result.directoryCount += 1;
}
} else {
// Add file node
const stats = statSync(entryPath);
const ext = entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() : '';
const language = ext && Object.prototype.hasOwnProperty.call(EXT_TO_LANGUAGE, `.${ext}`)
? EXT_TO_LANGUAGE[`.${ext}` as keyof typeof EXT_TO_LANGUAGE]
: undefined;
const fileNode: FileSystemNode = {
name: entry.name,
path: entryPath.replace(/\\/g, '/'),
type: 'file',
size: stats.size,
modifiedTime: stats.mtime.toISOString(),
extension: ext,
language
};
node.children!.push(fileNode);
result.fileCount += 1;
result.totalSize += stats.size;
}
}
// Sort: directories first, then alphabetically
node.children!.sort((a, b) => {
if (a.type === 'directory' && b.type !== 'directory') return -1;
if (a.type !== 'directory' && b.type === 'directory') return 1;
return a.name.localeCompare(b.name);
});
result.node = node;
return result;
} catch (error: unknown) {
console.error('Error building file tree:', error);
return result;
}
}
/**
* Get available root directories
* @param {string} workspacePath - Current workspace path
* @returns {Promise<RootDirectory[]>}
*/
async function getRootDirectories(workspacePath: string): Promise<RootDirectory[]> {
const roots: RootDirectory[] = [];
// Add workspace root
let normalizedWorkspace = workspacePath.replace(/\\/g, '/');
if (normalizedWorkspace.match(/^\/[a-zA-Z]\//)) {
normalizedWorkspace = normalizedWorkspace.charAt(1).toUpperCase() + ':' + normalizedWorkspace.slice(2);
}
const workspaceName = normalizedWorkspace.split('/').pop() || 'Workspace';
const isGitRoot = existsSync(join(normalizedWorkspace, '.git'));
roots.push({
path: normalizedWorkspace,
name: workspaceName,
isWorkspace: true,
isGitRoot
});
// On Windows, also add drive roots
if (process.platform === 'win32') {
// Get the drive letter from workspace path
const driveMatch = normalizedWorkspace.match(/^([A-Z]):/);
if (driveMatch) {
const driveLetter = driveMatch[1];
// Add drive root if not already the workspace
const driveRoot = `${driveLetter}:/`;
if (driveRoot !== normalizedWorkspace) {
roots.push({
path: driveRoot,
name: `${driveLetter}: Drive`,
isWorkspace: false,
isGitRoot: false
});
}
}
}
return roots;
}
/**
* Parse .gitignore file and return patterns
* @param {string} gitignorePath - Path to .gitignore file
@@ -420,6 +616,166 @@ async function triggerUpdateClaudeMd(targetPath: string, tool: string, strategy:
export async function handleFilesRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
// ========================================
// Explorer API Routes (/api/explorer/*)
// ========================================
// API: Get file tree (Explorer view)
if (pathname === '/api/explorer/tree') {
const rootPath = url.searchParams.get('rootPath') || initialPath;
const maxDepth = parseInt(url.searchParams.get('maxDepth') || '6', 10);
const includeHidden = url.searchParams.get('includeHidden') === 'true';
const startTime = Date.now();
try {
const validatedPath = await validateAllowedPath(rootPath, { mustExist: true, allowedDirectories: [initialPath] });
const treeResult = await buildFileTree(validatedPath, maxDepth, includeHidden);
const response: FileTreeResponse = {
rootNodes: treeResult.node ? [treeResult.node] : [],
fileCount: treeResult.fileCount,
directoryCount: treeResult.directoryCount,
totalSize: treeResult.totalSize,
buildTime: Date.now() - startTime
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const status = message.includes('Access denied') ? 403 : 400;
console.error(`[Explorer] Tree path validation failed: ${message}`);
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
rootNodes: [],
fileCount: 0,
directoryCount: 0,
totalSize: 0,
buildTime: 0,
error: status === 403 ? 'Access denied' : 'Invalid path'
}));
}
return true;
}
// API: Get root directories (Explorer view)
if (pathname === '/api/explorer/roots') {
try {
const roots = await getRootDirectories(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(roots));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Explorer] Failed to get roots: ${message}`);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to get root directories' }));
}
return true;
}
// API: Get file content (Explorer view)
if (pathname === '/api/explorer/file') {
const filePath = url.searchParams.get('path');
if (!filePath) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'File path is required' }));
return true;
}
try {
const validatedFile = await validateAllowedPath(filePath, { mustExist: true, allowedDirectories: [initialPath] });
const fileData = await getFileContent(validatedFile);
if (fileData.error) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(fileData));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(fileData));
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const status = message.includes('Access denied') ? 403 : 400;
console.error(`[Explorer] File path validation failed: ${message}`);
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: status === 403 ? 'Access denied' : 'Invalid path' }));
}
return true;
}
// API: Search files (Explorer view)
if (pathname === '/api/explorer/search' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
if (typeof body !== 'object' || body === null) {
return { error: 'Invalid request body', status: 400 };
}
const { query, rootPath, maxResults = 50 } = body as { query?: unknown; rootPath?: unknown; maxResults?: unknown };
if (typeof query !== 'string' || query.trim().length === 0) {
return { error: 'query is required', status: 400 };
}
try {
const validatedPath = await validateAllowedPath(
typeof rootPath === 'string' ? rootPath : initialPath,
{ mustExist: true, allowedDirectories: [initialPath] }
);
// Simple file search - walk directory tree and match by name
const results: Array<{ path: string; name: string; type: 'file' | 'directory' }> = [];
const searchQuery = query.toLowerCase();
const searchDir = (dirPath: string, depth: number = 0) => {
if (depth > 10 || results.length >= (typeof maxResults === 'number' ? maxResults : 50)) return;
try {
const entries = readdirSync(dirPath, { withFileTypes: true });
const gitignorePath = join(dirPath, '.gitignore');
const gitignorePatterns = parseGitignore(gitignorePath);
for (const entry of entries) {
if (results.length >= (typeof maxResults === 'number' ? maxResults : 50)) break;
if (shouldIgnore(entry.name, gitignorePatterns, entry.isDirectory())) continue;
const entryPath = join(dirPath, entry.name);
if (entry.name.toLowerCase().includes(searchQuery)) {
results.push({
path: entryPath.replace(/\\/g, '/'),
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file'
});
}
if (entry.isDirectory()) {
searchDir(entryPath, depth + 1);
}
}
} catch {
// Skip directories we can't read
}
};
searchDir(validatedPath);
return { results, total: results.length };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const status = message.includes('Access denied') ? 403 : 400;
console.error(`[Explorer] Search path validation failed: ${message}`);
return { error: status === 403 ? 'Access denied' : 'Invalid path', status };
}
});
return true;
}
// ========================================
// Legacy Files API Routes (/api/files/*)
// ========================================
// API: List directory files with .gitignore filtering (Explorer view)
if (pathname === '/api/files') {
const dirPath = url.searchParams.get('path') || initialPath;