mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
feat: add useApiSettings hook for managing API settings, including providers, endpoints, cache, and model pools
- Implemented hooks for CRUD operations on providers and endpoints. - Added cache management hooks for cache stats and settings. - Introduced model pool management hooks for high availability and load balancing. - Created localization files for English and Chinese translations of API settings.
This commit is contained in:
@@ -7,6 +7,7 @@ import { listTools } from '../../tools/index.js';
|
||||
import { loadProjectOverview } from '../data-aggregator.js';
|
||||
import { resolvePath } from '../../utils/path-resolver.js';
|
||||
import { join } from 'path';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import type { RouteContext } from './types.js';
|
||||
|
||||
/**
|
||||
@@ -45,6 +46,77 @@ export async function handleCcwRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get Project Guidelines
|
||||
if (pathname === '/api/ccw/guidelines' && req.method === 'GET') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const resolvedPath = resolvePath(projectPath);
|
||||
const guidelinesFile = join(resolvedPath, '.workflow', 'project-guidelines.json');
|
||||
|
||||
if (!existsSync(guidelinesFile)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ guidelines: null }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(guidelinesFile, 'utf-8');
|
||||
const guidelines = JSON.parse(content);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ guidelines }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Failed to read guidelines file' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update Project Guidelines
|
||||
if (pathname === '/api/ccw/guidelines' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const resolvedPath = resolvePath(projectPath);
|
||||
const guidelinesFile = join(resolvedPath, '.workflow', 'project-guidelines.json');
|
||||
|
||||
try {
|
||||
const data = body as Record<string, unknown>;
|
||||
|
||||
// Read existing file to preserve _metadata.created_at
|
||||
let existingMetadata: Record<string, unknown> = {};
|
||||
if (existsSync(guidelinesFile)) {
|
||||
try {
|
||||
const existing = JSON.parse(readFileSync(guidelinesFile, 'utf-8'));
|
||||
existingMetadata = existing._metadata || {};
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
// Build the guidelines object
|
||||
const guidelines = {
|
||||
conventions: data.conventions || { coding_style: [], naming_patterns: [], file_structure: [], documentation: [] },
|
||||
constraints: data.constraints || { architecture: [], tech_stack: [], performance: [], security: [] },
|
||||
quality_rules: data.quality_rules || [],
|
||||
learnings: data.learnings || [],
|
||||
_metadata: {
|
||||
created_at: (existingMetadata.created_at as string) || new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
version: (existingMetadata.version as string) || '1.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
writeFileSync(guidelinesFile, JSON.stringify(guidelines, null, 2), 'utf-8');
|
||||
|
||||
broadcastToClients({
|
||||
type: 'PROJECT_GUIDELINES_UPDATED',
|
||||
payload: { timestamp: new Date().toISOString() },
|
||||
});
|
||||
|
||||
return { success: true, guidelines };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CCW Upgrade
|
||||
if (pathname === '/api/ccw/upgrade' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
|
||||
@@ -58,13 +58,24 @@ interface ActiveExecution {
|
||||
mode: string;
|
||||
prompt: string;
|
||||
startTime: number;
|
||||
output: string;
|
||||
output: string[]; // Array-based buffer to limit memory usage
|
||||
status: 'running' | 'completed' | 'error';
|
||||
completedTimestamp?: number; // When execution completed (for 5-minute retention)
|
||||
}
|
||||
|
||||
// API response type with output as string (for backward compatibility)
|
||||
type ActiveExecutionDto = Omit<ActiveExecution, 'output'> & { output: string };
|
||||
|
||||
const activeExecutions = new Map<string, ActiveExecution>();
|
||||
const EXECUTION_RETENTION_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const CLEANUP_INTERVAL_MS = 60 * 1000; // 1 minute - periodic cleanup interval
|
||||
const MAX_OUTPUT_BUFFER_LINES = 1000; // Max lines to keep in memory per execution
|
||||
const MAX_ACTIVE_EXECUTIONS = 200; // Max concurrent executions in memory
|
||||
|
||||
// Enable periodic cleanup to prevent memory buildup
|
||||
setInterval(() => {
|
||||
cleanupStaleExecutions();
|
||||
}, CLEANUP_INTERVAL_MS);
|
||||
|
||||
/**
|
||||
* Cleanup stale completed executions older than retention period
|
||||
@@ -93,9 +104,13 @@ export function cleanupStaleExecutions(): void {
|
||||
/**
|
||||
* Get all active CLI executions
|
||||
* Used by frontend to restore state when view is opened during execution
|
||||
* Note: Converts output array back to string for API compatibility
|
||||
*/
|
||||
export function getActiveExecutions(): ActiveExecution[] {
|
||||
return Array.from(activeExecutions.values());
|
||||
export function getActiveExecutions(): ActiveExecutionDto[] {
|
||||
return Array.from(activeExecutions.values()).map(exec => ({
|
||||
...exec,
|
||||
output: exec.output.join('') // Convert array buffer to string for API
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,21 +137,30 @@ export function updateActiveExecution(event: {
|
||||
}
|
||||
|
||||
if (type === 'started') {
|
||||
// Create new active execution
|
||||
// Check map size limit before creating new execution
|
||||
if (activeExecutions.size >= MAX_ACTIVE_EXECUTIONS) {
|
||||
console.warn(`[ActiveExec] Max executions limit reached (${MAX_ACTIVE_EXECUTIONS}), cleanup may be needed`);
|
||||
}
|
||||
|
||||
// Create new active execution with array-based output buffer
|
||||
activeExecutions.set(executionId, {
|
||||
id: executionId,
|
||||
tool: tool || 'unknown',
|
||||
mode: mode || 'analysis',
|
||||
prompt: (prompt || '').substring(0, 500),
|
||||
startTime: Date.now(),
|
||||
output: '',
|
||||
output: [], // Initialize as empty array instead of empty string
|
||||
status: 'running'
|
||||
});
|
||||
} else if (type === 'output') {
|
||||
// Append output to existing execution
|
||||
// Append output to existing execution using array with size limit
|
||||
const activeExec = activeExecutions.get(executionId);
|
||||
if (activeExec && output) {
|
||||
activeExec.output += output;
|
||||
activeExec.output.push(output);
|
||||
// Keep buffer size under limit by shifting old entries
|
||||
if (activeExec.output.length > MAX_OUTPUT_BUFFER_LINES) {
|
||||
activeExec.output.shift(); // Remove oldest entry
|
||||
}
|
||||
}
|
||||
} else if (type === 'completed') {
|
||||
// Mark as completed with timestamp for retention-based cleanup
|
||||
@@ -487,6 +511,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const activeExec = activeExecutions.get(executionId);
|
||||
if (activeExec) {
|
||||
// Return active execution data as conversation record format
|
||||
// Note: Convert output array buffer back to string for API compatibility
|
||||
const activeConversation = {
|
||||
id: activeExec.id,
|
||||
tool: activeExec.tool,
|
||||
@@ -497,7 +522,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
turn: 1,
|
||||
timestamp: new Date(activeExec.startTime).toISOString(),
|
||||
prompt: activeExec.prompt,
|
||||
output: { stdout: activeExec.output, stderr: '' },
|
||||
output: { stdout: activeExec.output.join(''), stderr: '' }, // Convert array to string
|
||||
duration_ms: activeExec.completedTimestamp
|
||||
? activeExec.completedTimestamp - activeExec.startTime
|
||||
: Date.now() - activeExec.startTime
|
||||
@@ -662,13 +687,17 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const executionId = `${Date.now()}-${tool}`;
|
||||
|
||||
// Store active execution for state recovery
|
||||
// Check map size limit before creating new execution
|
||||
if (activeExecutions.size >= MAX_ACTIVE_EXECUTIONS) {
|
||||
console.warn(`[ActiveExec] Max executions limit reached (${MAX_ACTIVE_EXECUTIONS}), cleanup may be needed`);
|
||||
}
|
||||
activeExecutions.set(executionId, {
|
||||
id: executionId,
|
||||
tool,
|
||||
mode: mode || 'analysis',
|
||||
prompt: prompt.substring(0, 500), // Truncate for display
|
||||
startTime: Date.now(),
|
||||
output: '',
|
||||
output: [], // Initialize as empty array for memory-efficient buffering
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
@@ -701,10 +730,14 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null)
|
||||
const content = SmartContentFormatter.format(unit.content, unit.type);
|
||||
|
||||
// Append to active execution buffer
|
||||
// Append to active execution buffer using array with size limit
|
||||
const activeExec = activeExecutions.get(executionId);
|
||||
if (activeExec) {
|
||||
activeExec.output += content || '';
|
||||
activeExec.output.push(content || '');
|
||||
// Keep buffer size under limit by shifting old entries
|
||||
if (activeExec.output.length > MAX_OUTPUT_BUFFER_LINES) {
|
||||
activeExec.output.shift(); // Remove oldest entry
|
||||
}
|
||||
}
|
||||
|
||||
broadcastToClients({
|
||||
@@ -753,7 +786,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
execution: result.execution
|
||||
execution: result.execution,
|
||||
parsedOutput: result.parsedOutput, // Filtered output (excludes metadata/progress)
|
||||
finalOutput: result.finalOutput // Agent message only (for --final flag)
|
||||
};
|
||||
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -341,6 +341,108 @@ export async function handleCodexLensIndexRoutes(ctx: RouteContext): Promise<boo
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Update (Incremental index update)
|
||||
if (pathname === '/api/codexlens/update' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code', embeddingBackend = 'fastembed', maxWorkers = 1 } = body as {
|
||||
path?: unknown;
|
||||
indexType?: unknown;
|
||||
embeddingModel?: unknown;
|
||||
embeddingBackend?: unknown;
|
||||
maxWorkers?: unknown;
|
||||
};
|
||||
const targetPath = typeof projectPath === 'string' && projectPath.trim().length > 0 ? projectPath : initialPath;
|
||||
const resolvedIndexType = indexType === 'normal' ? 'normal' : 'vector';
|
||||
const resolvedEmbeddingModel = typeof embeddingModel === 'string' && embeddingModel.trim().length > 0 ? embeddingModel : 'code';
|
||||
const resolvedEmbeddingBackend = typeof embeddingBackend === 'string' && embeddingBackend.trim().length > 0 ? embeddingBackend : 'fastembed';
|
||||
const resolvedMaxWorkers = typeof maxWorkers === 'number' ? maxWorkers : Number(maxWorkers);
|
||||
|
||||
// Pre-check: Verify embedding backend availability before proceeding with vector indexing
|
||||
if (resolvedIndexType !== 'normal') {
|
||||
if (resolvedEmbeddingBackend === 'litellm') {
|
||||
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 {
|
||||
const semanticStatus = await checkSemanticStatus();
|
||||
if (!semanticStatus.available) {
|
||||
return {
|
||||
success: false,
|
||||
error: semanticStatus.error || 'FastEmbed semantic backend is not available. Please install semantic dependencies first.',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build CLI arguments for incremental update using 'index update' subcommand
|
||||
const args = ['index', 'update', targetPath, '--json'];
|
||||
if (resolvedIndexType === 'normal') {
|
||||
args.push('--no-embeddings');
|
||||
} else {
|
||||
args.push('--model', resolvedEmbeddingModel);
|
||||
if (resolvedEmbeddingBackend && resolvedEmbeddingBackend !== 'fastembed') {
|
||||
args.push('--backend', resolvedEmbeddingBackend);
|
||||
}
|
||||
if (!Number.isNaN(resolvedMaxWorkers) && resolvedMaxWorkers > 1) {
|
||||
args.push('--max-workers', String(resolvedMaxWorkers));
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast start event
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||
payload: { stage: 'start', message: 'Starting incremental index update...', percent: 0, path: targetPath, indexType: resolvedIndexType }
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens(args, {
|
||||
cwd: targetPath,
|
||||
timeout: 1800000,
|
||||
onProgress: (progress: ProgressInfo) => {
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||
payload: { ...progress, path: targetPath }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||
payload: { stage: 'complete', message: 'Incremental update complete', percent: 100, path: targetPath }
|
||||
});
|
||||
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
return { success: true, result: parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output ?? '' };
|
||||
}
|
||||
} else {
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||
payload: { stage: 'error', message: result.error || 'Unknown error', percent: 0, path: targetPath }
|
||||
});
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_INDEX_PROGRESS',
|
||||
payload: { stage: 'error', message, percent: 0, path: targetPath }
|
||||
});
|
||||
return { success: false, error: message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Check if indexing is in progress
|
||||
if (pathname === '/api/codexlens/indexing-status') {
|
||||
const inProgress = isIndexingInProgress();
|
||||
|
||||
@@ -174,8 +174,11 @@ type Params = z.infer<typeof ParamsSchema>;
|
||||
|
||||
interface ReadyStatus {
|
||||
ready: boolean;
|
||||
installed: boolean;
|
||||
error?: string;
|
||||
version?: string;
|
||||
pythonVersion?: string;
|
||||
venvPath?: string;
|
||||
}
|
||||
|
||||
interface SemanticStatus {
|
||||
@@ -246,28 +249,32 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
return venvStatusCache.status;
|
||||
}
|
||||
|
||||
const venvPath = getCodexLensVenvDir();
|
||||
|
||||
// Check venv exists
|
||||
if (!existsSync(getCodexLensVenvDir())) {
|
||||
const result = { ready: false, error: 'Venv not found' };
|
||||
if (!existsSync(venvPath)) {
|
||||
const result = { ready: false, installed: false, error: 'Venv not found', venvPath };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
console.log(`[PERF][CodexLens] checkVenvStatus (no venv): ${Date.now() - funcStart}ms`);
|
||||
return result;
|
||||
}
|
||||
|
||||
const pythonPath = getCodexLensPython();
|
||||
|
||||
// Check python executable exists
|
||||
if (!existsSync(getCodexLensPython())) {
|
||||
const result = { ready: false, error: 'Python executable not found in venv' };
|
||||
if (!existsSync(pythonPath)) {
|
||||
const result = { ready: false, installed: false, error: 'Python executable not found in venv', venvPath };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
console.log(`[PERF][CodexLens] checkVenvStatus (no python): ${Date.now() - funcStart}ms`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check codexlens and core dependencies are importable
|
||||
// Check codexlens and core dependencies are importable, and get Python version
|
||||
const spawnStart = Date.now();
|
||||
console.log('[PERF][CodexLens] checkVenvStatus spawning Python...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(getCodexLensPython(), ['-c', 'import codexlens; import watchdog; print(codexlens.__version__)'], {
|
||||
const child = spawn(pythonPath, ['-c', 'import sys; import codexlens; import watchdog; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"); print(codexlens.__version__)'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
});
|
||||
@@ -285,9 +292,18 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
child.on('close', (code) => {
|
||||
let result: ReadyStatus;
|
||||
if (code === 0) {
|
||||
result = { ready: true, version: stdout.trim() };
|
||||
const lines = stdout.trim().split('\n');
|
||||
const pythonVersion = lines[0]?.trim() || '';
|
||||
const codexlensVersion = lines[1]?.trim() || '';
|
||||
result = {
|
||||
ready: true,
|
||||
installed: true,
|
||||
version: codexlensVersion,
|
||||
pythonVersion,
|
||||
venvPath
|
||||
};
|
||||
} else {
|
||||
result = { ready: false, error: `CodexLens not installed: ${stderr}` };
|
||||
result = { ready: false, installed: false, error: `CodexLens not installed: ${stderr}`, venvPath };
|
||||
}
|
||||
// Cache the result
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
@@ -296,7 +312,7 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
const result = { ready: false, error: `Failed to check venv: ${err.message}` };
|
||||
const result = { ready: false, installed: false, error: `Failed to check venv: ${err.message}`, venvPath };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
console.log(`[PERF][CodexLens] checkVenvStatus ERROR: ${Date.now() - funcStart}ms`);
|
||||
resolve(result);
|
||||
|
||||
Reference in New Issue
Block a user