feat: 添加忽略模式配置接口和前端支持,允许用户自定义索引排除项

This commit is contained in:
catlog22
2026-01-07 23:33:40 +08:00
parent 178d45e232
commit d2d6cce5f4
9 changed files with 676 additions and 14 deletions

View File

@@ -590,6 +590,33 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
// Execute CLI tool
const syncId = `claude-sync-${level}-${Date.now()}`;
// Broadcast CLI_EXECUTION_STARTED event
broadcastToClients({
type: 'CLI_EXECUTION_STARTED',
payload: {
executionId: syncId,
tool: tool === 'qwen' ? 'qwen' : 'gemini',
mode: 'analysis',
category: 'internal',
context: 'claude-sync',
level
}
});
// Create onOutput callback for real-time streaming
const onOutput = (chunk: { type: string; data: string }) => {
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId: syncId,
chunkType: chunk.type,
data: chunk.data
}
});
};
const startTime = Date.now();
const result = await executeCliTool({
tool: tool === 'qwen' ? 'qwen' : 'gemini',
prompt: cliPrompt,
@@ -600,6 +627,17 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
stream: false,
category: 'internal',
id: syncId
}, onOutput);
// Broadcast CLI_EXECUTION_COMPLETED event
broadcastToClients({
type: 'CLI_EXECUTION_COMPLETED',
payload: {
executionId: syncId,
success: result.success,
status: result.execution?.status || (result.success ? 'success' : 'error'),
duration_ms: Date.now() - startTime
}
});
if (!result.success) {

View File

@@ -909,5 +909,139 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
return true;
}
// ============================================================
// IGNORE PATTERNS CONFIGURATION ENDPOINTS
// ============================================================
// API: Get ignore patterns configuration
if (pathname === '/api/codexlens/ignore-patterns' && req.method === 'GET') {
try {
const { homedir } = await import('os');
const { join } = await import('path');
const { readFile } = await import('fs/promises');
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
let settings: Record<string, any> = {};
try {
const content = await readFile(settingsPath, 'utf-8');
settings = JSON.parse(content);
} catch {
// File doesn't exist
}
// Default ignore patterns (matching WatcherConfig defaults in events.py)
const defaultPatterns = [
// Version control
'.git', '.svn', '.hg',
// Python environments & cache
'.venv', 'venv', 'env', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache',
// Node.js
'node_modules', 'bower_components', '.npm', '.yarn',
// Build artifacts
'dist', 'build', 'out', 'target', 'bin', 'obj', '_build', 'coverage', 'htmlcov',
// IDE & Editor
'.idea', '.vscode', '.vs', '.eclipse',
// CodexLens internal
'.codexlens',
// Package manager caches
'.cache', '.parcel-cache', '.turbo', '.next', '.nuxt',
// Logs & temp
'logs', 'tmp', 'temp',
];
// Default extension filters for embeddings (files skipped for vector index)
const defaultExtensionFilters = [
// Lock files (large, repetitive)
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.lock', 'Gemfile.lock', 'poetry.lock',
// Generated/minified
'*.min.js', '*.min.css', '*.bundle.js',
// Binary-like text
'*.svg', '*.map',
];
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
patterns: settings.ignore_patterns || defaultPatterns,
extensionFilters: settings.extension_filters || defaultExtensionFilters,
defaults: {
patterns: defaultPatterns,
extensionFilters: defaultExtensionFilters
}
}));
} 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: Save ignore patterns configuration
if (pathname === '/api/codexlens/ignore-patterns' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { patterns, extensionFilters } = body as {
patterns?: string[];
extensionFilters?: string[];
};
try {
const { homedir } = await import('os');
const { join, dirname } = await import('path');
const { writeFile, mkdir, readFile } = await import('fs/promises');
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
await mkdir(dirname(settingsPath), { recursive: true });
// Read existing settings
let settings: Record<string, any> = {};
try {
const content = await readFile(settingsPath, 'utf-8');
settings = JSON.parse(content);
} catch {
// File doesn't exist, start fresh
}
// Validate patterns (alphanumeric, dots, underscores, dashes, asterisks)
const validPatternRegex = /^[\w.*\-/]+$/;
if (patterns) {
const invalidPatterns = patterns.filter(p => !validPatternRegex.test(p));
if (invalidPatterns.length > 0) {
return {
success: false,
error: `Invalid patterns: ${invalidPatterns.join(', ')}`,
status: 400
};
}
settings.ignore_patterns = patterns;
}
if (extensionFilters) {
const invalidFilters = extensionFilters.filter(p => !validPatternRegex.test(p));
if (invalidFilters.length > 0) {
return {
success: false,
error: `Invalid extension filters: ${invalidFilters.join(', ')}`,
status: 400
};
}
settings.extension_filters = extensionFilters;
}
// Write updated settings
await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
return {
success: true,
message: 'Ignore patterns saved successfully',
patterns: settings.ignore_patterns,
extensionFilters: settings.extension_filters
};
} catch (err: unknown) {
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
}
});
return true;
}
return false;
}

View File

@@ -970,6 +970,33 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
// Try to execute CLI using CCW's built-in executor
try {
const syncId = `active-memory-${Date.now()}`;
// Broadcast CLI_EXECUTION_STARTED event
broadcastToClients({
type: 'CLI_EXECUTION_STARTED',
payload: {
executionId: syncId,
tool: tool === 'qwen' ? 'qwen' : 'gemini',
mode: 'analysis',
category: 'internal',
context: 'active-memory-sync',
fileCount: hotFiles.length
}
});
// Create onOutput callback for real-time streaming
const onOutput = (chunk: { type: string; data: string }) => {
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId: syncId,
chunkType: chunk.type,
data: chunk.data
}
});
};
const startTime = Date.now();
const result = await executeCliTool({
tool: tool === 'qwen' ? 'qwen' : 'gemini',
prompt: cliPrompt,
@@ -980,6 +1007,17 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
stream: false,
category: 'internal',
id: syncId
}, onOutput);
// Broadcast CLI_EXECUTION_COMPLETED event
broadcastToClients({
type: 'CLI_EXECUTION_COMPLETED',
payload: {
executionId: syncId,
success: result.success,
status: result.execution?.status || (result.success ? 'success' : 'error'),
duration_ms: Date.now() - startTime
}
});
if (result.success) {

View File

@@ -46,6 +46,8 @@ interface RuleGenerateParams {
location: string;
subdirectory: string;
projectPath: string;
enableReview?: boolean;
broadcastToClients?: (data: unknown) => void;
}
/**
@@ -351,7 +353,7 @@ function buildStructuredRulePrompt(params: {
const { description, fileName, subdirectory, location, context, enableReview } = params;
// Build category-specific guidance
const categoryGuidance = {
const categoryGuidance: Record<string, string> = {
coding: 'Focus on code style, naming conventions, and formatting rules. Include specific examples of correct and incorrect patterns.',
testing: 'Emphasize test structure, coverage expectations, mocking strategies, and assertion patterns.',
security: 'Highlight security best practices, input validation, authentication requirements, and sensitive data handling.',
@@ -583,6 +585,9 @@ RULES: $(cat ~/.claude/workflows/cli-templates/prompts/universal/00-universal-ri
* @returns {Object}
*/
async function generateRuleViaCLI(params: RuleGenerateParams): Promise<Record<string, unknown>> {
// Generate unique execution ID for tracking
const executionId = `rule-gen-${params.fileName.replace('.md', '')}-${Date.now()}`;
try {
const {
generationType,
@@ -594,7 +599,8 @@ async function generateRuleViaCLI(params: RuleGenerateParams): Promise<Record<st
location,
subdirectory,
projectPath,
enableReview
enableReview,
broadcastToClients
} = params;
let prompt = '';
@@ -608,7 +614,7 @@ async function generateRuleViaCLI(params: RuleGenerateParams): Promise<Record<st
if (generationType === 'description') {
mode = 'write';
prompt = buildStructuredRulePrompt({
description,
description: description || '',
fileName,
subdirectory: subdirectory || '',
location,
@@ -638,15 +644,59 @@ FILE NAME: ${fileName}`;
return { error: `Unknown generation type: ${generationType}` };
}
// Broadcast CLI_EXECUTION_STARTED event
if (broadcastToClients) {
broadcastToClients({
type: 'CLI_EXECUTION_STARTED',
payload: {
executionId,
tool: 'claude',
mode,
category: 'internal',
context: 'rule-generation',
fileName
}
});
}
// Create onOutput callback for real-time streaming
const onOutput = broadcastToClients
? (chunk: { type: string; data: string }) => {
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId,
chunkType: chunk.type,
data: chunk.data
}
});
}
: undefined;
// Execute CLI tool (Claude) with at least 10 minutes timeout
const startTime = Date.now();
const result = await executeCliTool({
tool: 'claude',
prompt,
mode,
cd: workingDir,
timeout: 600000, // 10 minutes
category: 'internal'
});
category: 'internal',
id: executionId
}, onOutput);
// Broadcast CLI_EXECUTION_COMPLETED event
if (broadcastToClients) {
broadcastToClients({
type: 'CLI_EXECUTION_COMPLETED',
payload: {
executionId,
success: result.success,
status: result.execution?.status || (result.success ? 'success' : 'error'),
duration_ms: Date.now() - startTime
}
});
}
if (!result.success) {
return {
@@ -677,15 +727,60 @@ FILE NAME: ${fileName}`;
let reviewResult = null;
if (enableReview) {
const reviewPrompt = buildReviewPrompt(generatedContent, fileName, context);
const reviewExecutionId = `${executionId}-review`;
// Broadcast review CLI_EXECUTION_STARTED event
if (broadcastToClients) {
broadcastToClients({
type: 'CLI_EXECUTION_STARTED',
payload: {
executionId: reviewExecutionId,
tool: 'claude',
mode: 'write',
category: 'internal',
context: 'rule-review',
fileName
}
});
}
// Create onOutput callback for review step
const reviewOnOutput = broadcastToClients
? (chunk: { type: string; data: string }) => {
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId: reviewExecutionId,
chunkType: chunk.type,
data: chunk.data
}
});
}
: undefined;
const reviewStartTime = Date.now();
const reviewExecution = await executeCliTool({
tool: 'claude',
prompt: reviewPrompt,
mode: 'write',
cd: workingDir,
timeout: 300000, // 5 minutes for review
category: 'internal'
});
category: 'internal',
id: reviewExecutionId
}, reviewOnOutput);
// Broadcast review CLI_EXECUTION_COMPLETED event
if (broadcastToClients) {
broadcastToClients({
type: 'CLI_EXECUTION_COMPLETED',
payload: {
executionId: reviewExecutionId,
success: reviewExecution.success,
status: reviewExecution.execution?.status || (reviewExecution.success ? 'success' : 'error'),
duration_ms: Date.now() - reviewStartTime
}
});
}
if (reviewExecution.success) {
let reviewedContent = (reviewExecution.parsedOutput || reviewExecution.stdout || '').trim();
@@ -801,7 +896,7 @@ paths: [${paths.join(', ')}]
* @returns true if route was handled, false otherwise
*/
export async function handleRulesRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
// API: Get all rules
if (pathname === '/api/rules') {
@@ -920,7 +1015,8 @@ export async function handleRulesRoutes(ctx: RouteContext): Promise<boolean> {
fileName: resolvedFileName,
location: resolvedLocation,
subdirectory: resolvedSubdirectory || '',
projectPath
projectPath,
broadcastToClients
});
}

View File

@@ -55,6 +55,7 @@ interface GenerationParams {
skillName: string;
location: SkillLocation;
projectPath: string;
broadcastToClients?: (data: unknown) => void;
}
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -488,9 +489,13 @@ async function importSkill(sourcePath: string, location: SkillLocation, projectP
* @param {string} params.skillName - Name for the skill
* @param {string} params.location - 'project' or 'user'
* @param {string} params.projectPath - Project root path
* @param {Function} params.broadcastToClients - WebSocket broadcast function
* @returns {Object}
*/
async function generateSkillViaCLI({ generationType, description, skillName, location, projectPath }: GenerationParams) {
async function generateSkillViaCLI({ generationType, description, skillName, location, projectPath, broadcastToClients }: GenerationParams) {
// Generate unique execution ID for tracking
const executionId = `skill-gen-${skillName}-${Date.now()}`;
try {
// Validate inputs
if (!skillName) {
@@ -557,15 +562,59 @@ Create a new Claude Code skill with the following specifications:
4. Follow Claude Code skill design patterns and best practices
5. Output all files to: ${targetPath}`;
// Broadcast CLI_EXECUTION_STARTED event
if (broadcastToClients) {
broadcastToClients({
type: 'CLI_EXECUTION_STARTED',
payload: {
executionId,
tool: 'claude',
mode: 'write',
category: 'internal',
context: 'skill-generation',
skillName
}
});
}
// Create onOutput callback for real-time streaming
const onOutput = broadcastToClients
? (chunk: { type: string; data: string }) => {
broadcastToClients({
type: 'CLI_OUTPUT',
payload: {
executionId,
chunkType: chunk.type,
data: chunk.data
}
});
}
: undefined;
// Execute CLI tool (Claude) with write mode
const startTime = Date.now();
const result = await executeCliTool({
tool: 'claude',
prompt,
mode: 'write',
cd: baseDir,
timeout: 600000, // 10 minutes
category: 'internal'
});
category: 'internal',
id: executionId
}, onOutput);
// Broadcast CLI_EXECUTION_COMPLETED event
if (broadcastToClients) {
broadcastToClients({
type: 'CLI_EXECUTION_COMPLETED',
payload: {
executionId,
success: result.success,
status: result.execution?.status || (result.success ? 'success' : 'error'),
duration_ms: Date.now() - startTime
}
});
}
// Check if execution was successful
if (!result.success) {
@@ -606,7 +655,7 @@ Create a new Claude Code skill with the following specifications:
* @returns true if route was handled, false otherwise
*/
export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
// API: Get all skills (project and user)
if (pathname === '/api/skills') {
@@ -991,7 +1040,8 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
description,
skillName,
location,
projectPath: validatedProjectPath
projectPath: validatedProjectPath,
broadcastToClients
});
} else {
return { error: 'Invalid mode. Must be "import" or "cli-generate"' };