mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
feat: Enhance CLI tools and settings management
- Added auto-initialization of CSRF token for state-changing requests in cli-manager.js. - Refactored Claude CLI Tools configuration to separate tools and settings into cli-tools.json and cli-settings.json respectively. - Introduced new interfaces for Claude CLI Tools and Settings, including support for tags and primary models. - Implemented loading and saving functions for CLI settings, ensuring backward compatibility with legacy combined config. - Updated functions to synchronize tags between CLI tools and configuration manager. - Added error handling and logging for loading and saving configurations. - Created initial cli-settings.json with default settings.
This commit is contained in:
@@ -34,6 +34,8 @@ import {
|
||||
import {
|
||||
loadClaudeCliTools,
|
||||
saveClaudeCliTools,
|
||||
loadClaudeCliSettings,
|
||||
saveClaudeCliSettings,
|
||||
updateClaudeToolEnabled,
|
||||
updateClaudeCacheSettings,
|
||||
getClaudeCliToolsInfo,
|
||||
@@ -704,11 +706,13 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global)
|
||||
if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
|
||||
try {
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
const toolsConfig = loadClaudeCliTools(initialPath);
|
||||
const settingsConfig = loadClaudeCliSettings(initialPath);
|
||||
const info = getClaudeCliToolsInfo(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
...config,
|
||||
tools: toolsConfig,
|
||||
settings: settingsConfig,
|
||||
_configInfo: info
|
||||
}));
|
||||
} catch (err) {
|
||||
@@ -722,32 +726,55 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
if (pathname === '/api/cli/tools-config' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const updates = body as Partial<any>;
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
const updates = body as { tools?: any; settings?: any };
|
||||
|
||||
// Merge updates
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
...updates,
|
||||
tools: { ...config.tools, ...(updates.tools || {}) },
|
||||
settings: {
|
||||
...config.settings,
|
||||
...(updates.settings || {}),
|
||||
// Update tools config if provided
|
||||
if (updates.tools) {
|
||||
const currentTools = loadClaudeCliTools(initialPath);
|
||||
const updatedTools = {
|
||||
...currentTools,
|
||||
tools: { ...currentTools.tools, ...(updates.tools.tools || {}) },
|
||||
customEndpoints: updates.tools.customEndpoints || currentTools.customEndpoints
|
||||
};
|
||||
saveClaudeCliTools(initialPath, updatedTools);
|
||||
}
|
||||
|
||||
// Update settings config if provided
|
||||
if (updates.settings) {
|
||||
const currentSettings = loadClaudeCliSettings(initialPath);
|
||||
const s = updates.settings;
|
||||
|
||||
// Deep merge: only update fields that are explicitly provided
|
||||
const updatedSettings = {
|
||||
...currentSettings,
|
||||
// Scalar fields: only update if explicitly provided
|
||||
...(s.defaultTool !== undefined && { defaultTool: s.defaultTool }),
|
||||
...(s.promptFormat !== undefined && { promptFormat: s.promptFormat }),
|
||||
...(s.nativeResume !== undefined && { nativeResume: s.nativeResume }),
|
||||
...(s.recursiveQuery !== undefined && { recursiveQuery: s.recursiveQuery }),
|
||||
...(s.codeIndexMcp !== undefined && { codeIndexMcp: s.codeIndexMcp }),
|
||||
// Nested objects: deep merge
|
||||
smartContext: {
|
||||
...currentSettings.smartContext,
|
||||
...(s.smartContext || {})
|
||||
},
|
||||
cache: {
|
||||
...config.settings.cache,
|
||||
...(updates.settings?.cache || {})
|
||||
...currentSettings.cache,
|
||||
...(s.cache || {})
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
saveClaudeCliSettings(initialPath, updatedSettings);
|
||||
}
|
||||
|
||||
saveClaudeCliTools(initialPath, updatedConfig);
|
||||
const toolsConfig = loadClaudeCliTools(initialPath);
|
||||
const settingsConfig = loadClaudeCliSettings(initialPath);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_TOOLS_CONFIG_UPDATED',
|
||||
payload: { config: updatedConfig, timestamp: new Date().toISOString() }
|
||||
payload: { tools: toolsConfig, settings: settingsConfig, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config: updatedConfig };
|
||||
return { success: true, tools: toolsConfig, settings: settingsConfig };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
@@ -782,14 +809,14 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const cacheSettings = body as { injectionMode?: string; defaultPrefix?: string; defaultSuffix?: string };
|
||||
const config = updateClaudeCacheSettings(initialPath, cacheSettings as any);
|
||||
const settings = updateClaudeCacheSettings(initialPath, cacheSettings as any);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_CACHE_SETTINGS_UPDATED',
|
||||
payload: { cache: config.settings.cache, timestamp: new Date().toISOString() }
|
||||
payload: { cache: settings.cache, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config };
|
||||
return { success: true, settings };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
|
||||
@@ -4,9 +4,35 @@
|
||||
*/
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join as pathJoin } from 'path';
|
||||
import { z } from 'zod';
|
||||
import { getSystemPython } from '../../utils/python-utils.js';
|
||||
import type { RouteContext } from './types.js';
|
||||
|
||||
// ========== Input Validation Schemas ==========
|
||||
|
||||
/**
|
||||
* Validation schema for ModelPoolConfig
|
||||
* Used to validate incoming API requests for model pool operations
|
||||
*/
|
||||
const ModelPoolConfigSchema = z.object({
|
||||
modelType: z.enum(['embedding', 'llm', 'reranker']),
|
||||
enabled: z.boolean(),
|
||||
targetModel: z.string().min(1, 'Target model is required'),
|
||||
strategy: z.enum(['round_robin', 'latency_aware', 'weighted_random']),
|
||||
autoDiscover: z.boolean(),
|
||||
excludedProviderIds: z.array(z.string()).optional().default([]),
|
||||
defaultCooldown: z.number().int().min(0).default(60),
|
||||
defaultMaxConcurrentPerKey: z.number().int().min(1).default(4),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Partial schema for updating ModelPoolConfig
|
||||
* All fields are optional for PATCH-like updates
|
||||
*/
|
||||
const ModelPoolConfigUpdateSchema = ModelPoolConfigSchema.partial();
|
||||
|
||||
// Get current module path for package-relative lookups
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -39,6 +65,12 @@ import {
|
||||
getEmbeddingPoolConfig,
|
||||
updateEmbeddingPoolConfig,
|
||||
discoverProvidersForModel,
|
||||
getModelPools,
|
||||
getModelPool,
|
||||
addModelPool,
|
||||
updateModelPool,
|
||||
deleteModelPool,
|
||||
getAvailableModelsForType,
|
||||
type ProviderCredential,
|
||||
type CustomEndpoint,
|
||||
type ProviderType,
|
||||
@@ -856,6 +888,186 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== Multi-Model Pool Management ==========
|
||||
|
||||
// GET /api/litellm-api/model-pools - Get all model pool configurations
|
||||
if (pathname === '/api/litellm-api/model-pools' && req.method === 'GET') {
|
||||
try {
|
||||
const pools = getModelPools(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ pools }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/model-pools/:id - Get specific pool configuration
|
||||
const poolGetMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/([^/]+)$/);
|
||||
if (poolGetMatch && req.method === 'GET') {
|
||||
const poolId = decodeURIComponent(poolGetMatch[1]);
|
||||
|
||||
try {
|
||||
const pool = getModelPool(initialPath, poolId);
|
||||
|
||||
if (!pool) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Pool not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ pool }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/model-pools - Create new model pool
|
||||
if (pathname === '/api/litellm-api/model-pools' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
// Validate input using zod schema
|
||||
const validationResult = ModelPoolConfigSchema.safeParse(body);
|
||||
if (!validationResult.success) {
|
||||
return {
|
||||
error: 'Invalid request body',
|
||||
details: validationResult.error.issues.map(e => ({
|
||||
field: String(e.path.join('.')),
|
||||
message: e.message
|
||||
})),
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const poolConfig = validationResult.data;
|
||||
const result = addModelPool(initialPath, poolConfig);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'MODEL_POOL_CREATED',
|
||||
payload: { poolId: result.poolId, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, ...result };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/model-pools/:id - Update model pool
|
||||
const poolPutMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/([^/]+)$/);
|
||||
if (poolPutMatch && req.method === 'PUT') {
|
||||
const poolId = decodeURIComponent(poolPutMatch[1]);
|
||||
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
// Validate input using partial schema (all fields optional for updates)
|
||||
const validationResult = ModelPoolConfigUpdateSchema.safeParse(body);
|
||||
if (!validationResult.success) {
|
||||
return {
|
||||
error: 'Invalid request body',
|
||||
details: validationResult.error.issues.map(e => ({
|
||||
field: String(e.path.join('.')),
|
||||
message: e.message
|
||||
})),
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const updates = validationResult.data;
|
||||
const result = updateModelPool(initialPath, poolId, updates);
|
||||
|
||||
if (!result.success) {
|
||||
return { error: 'Pool not found', status: 404 };
|
||||
}
|
||||
|
||||
broadcastToClients({
|
||||
type: 'MODEL_POOL_UPDATED',
|
||||
payload: { poolId, syncResult: result.syncResult, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/litellm-api/model-pools/:id - Delete model pool
|
||||
const poolDeleteMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/([^/]+)$/);
|
||||
if (poolDeleteMatch && req.method === 'DELETE') {
|
||||
const poolId = decodeURIComponent(poolDeleteMatch[1]);
|
||||
|
||||
try {
|
||||
const result = deleteModelPool(initialPath, poolId);
|
||||
|
||||
if (!result.success) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Pool not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastToClients({
|
||||
type: 'MODEL_POOL_DELETED',
|
||||
payload: { poolId, syncResult: result.syncResult, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/model-pools/available-models/:modelType - Get available models for type
|
||||
const availableModelsMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/available-models\/([^/]+)$/);
|
||||
if (availableModelsMatch && req.method === 'GET') {
|
||||
const modelType = decodeURIComponent(availableModelsMatch[1]) as import('../../types/litellm-api-config.js').ModelPoolType;
|
||||
|
||||
try {
|
||||
const availableModels = getAvailableModelsForType(initialPath, modelType);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ availableModels, modelType }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/model-pools/discover/:modelType/:model - Discover providers for model
|
||||
const discoverPoolMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/discover\/([^/]+)\/([^/]+)$/);
|
||||
if (discoverPoolMatch && req.method === 'GET') {
|
||||
const modelType = decodeURIComponent(discoverPoolMatch[1]);
|
||||
const targetModel = decodeURIComponent(discoverPoolMatch[2]);
|
||||
|
||||
try {
|
||||
const discovered = discoverProvidersForModel(initialPath, targetModel);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
modelType,
|
||||
targetModel,
|
||||
discovered,
|
||||
count: discovered.length,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/ccw-litellm/install - Install ccw-litellm package
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
|
||||
@@ -550,15 +550,15 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
|
||||
// Serve dashboard HTML
|
||||
if (pathname === '/' || pathname === '/index.html') {
|
||||
if (isLocalhostRequest(req)) {
|
||||
const tokenResult = tokenManager.getOrCreateAuthToken();
|
||||
setAuthCookie(res, tokenResult.token, tokenResult.expiresAt);
|
||||
// Set session cookie and CSRF token for all requests
|
||||
const tokenResult = tokenManager.getOrCreateAuthToken();
|
||||
setAuthCookie(res, tokenResult.token, tokenResult.expiresAt);
|
||||
|
||||
const sessionId = getOrCreateSessionId(req, res);
|
||||
const csrfToken = getCsrfTokenManager().generateToken(sessionId);
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
|
||||
const sessionId = getOrCreateSessionId(req, res);
|
||||
const csrfToken = getCsrfTokenManager().generateToken(sessionId);
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
}
|
||||
const html = generateServerDashboard(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
|
||||
Reference in New Issue
Block a user