From f112d4b9a2440b60fada154ca51adc0520007d92 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 24 Mar 2026 17:24:42 +0800 Subject: [PATCH] refactor: redesign cli settings export/import API with endpoint-based schema Replace nested settings structure with flat endpoints array for export/import. Add conflict strategy options (skip/overwrite/merge) and skipInvalid/disableImported flags. Co-Authored-By: Claude Opus 4.6 --- ccw/frontend/src/lib/api.ts | 21 +- ccw/frontend/src/pages/SettingsPage.tsx | 2 +- ccw/src/core/routes/cli-settings-routes.ts | 292 +++++++++++---------- 3 files changed, 154 insertions(+), 161 deletions(-) diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index bf7d24fd..95596080 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -6159,28 +6159,17 @@ export async function upgradeCcwInstallation( */ export interface ExportedSettings { version: string; - exportedAt: string; - settings: { - cliTools?: Record; - chineseResponse?: { - claudeEnabled: boolean; - codexEnabled: boolean; - }; - windowsPlatform?: { - enabled: boolean; - }; - codexCliEnhancement?: { - enabled: boolean; - }; - }; + timestamp: string; + endpoints: Array>; } /** * Import options for settings import */ export interface ImportOptions { - overwrite?: boolean; - dryRun?: boolean; + conflictStrategy?: 'skip' | 'overwrite' | 'merge'; + skipInvalid?: boolean; + disableImported?: boolean; } /** diff --git a/ccw/frontend/src/pages/SettingsPage.tsx b/ccw/frontend/src/pages/SettingsPage.tsx index 63e4b19f..3c8261be 100644 --- a/ccw/frontend/src/pages/SettingsPage.tsx +++ b/ccw/frontend/src/pages/SettingsPage.tsx @@ -619,7 +619,7 @@ function ResponseLanguageSection() { const data = JSON.parse(text) as ExportedSettings; // Validate basic structure - if (!data.version || !data.settings) { + if (!data.version || !data.endpoints) { toast.error(formatMessage({ id: 'settings.responseLanguage.importInvalidStructure' })); return; } diff --git a/ccw/src/core/routes/cli-settings-routes.ts b/ccw/src/core/routes/cli-settings-routes.ts index daed5dc3..336badeb 100644 --- a/ccw/src/core/routes/cli-settings-routes.ts +++ b/ccw/src/core/routes/cli-settings-routes.ts @@ -213,150 +213,7 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise { - try { - const request = body as Partial; - - // Check if just toggling enabled status - if (Object.keys(request).length === 1 && 'enabled' in request) { - const result = toggleEndpointEnabled(endpointId, request.enabled as boolean); - - if (result.success) { - broadcastToClients({ - type: 'CLI_SETTINGS_TOGGLED', - payload: { - endpointId, - enabled: request.enabled, - timestamp: new Date().toISOString() - } - }); - } - return result; - } - - // Full update - const existing = loadEndpointSettings(endpointId); - if (!existing) { - return { error: 'Endpoint not found', status: 404 }; - } - - const updateRequest: SaveEndpointRequest = { - id: endpointId, - name: request.name || existing.name, - description: request.description ?? existing.description, - settings: request.settings || existing.settings, - enabled: request.enabled ?? existing.enabled - }; - - const result = saveEndpointSettings(updateRequest); - - if (result.success) { - broadcastToClients({ - type: 'CLI_SETTINGS_UPDATED', - payload: { - endpoint: result.endpoint, - filePath: result.filePath, - timestamp: new Date().toISOString() - } - }); - } - - return result; - } catch (err) { - return { error: (err as Error).message, status: 500 }; - } - }); - return true; - } - - // ========== DELETE SETTINGS ========== - // DELETE /api/cli/settings/:id - const deleteMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/); - if (deleteMatch && req.method === 'DELETE') { - const endpointId = sanitizeEndpointId(deleteMatch[1]); - try { - const result = deleteEndpointSettings(endpointId); - - if (result.success) { - broadcastToClients({ - type: 'CLI_SETTINGS_DELETED', - payload: { - endpointId, - timestamp: new Date().toISOString() - } - }); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(result)); - } else { - res.writeHead(404, { '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 SETTINGS FILE PATH ========== - // GET /api/cli/settings/:id/path - const pathMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)\/path$/); - if (pathMatch && req.method === 'GET') { - const endpointId = sanitizeEndpointId(pathMatch[1]); - try { - const endpoint = loadEndpointSettings(endpointId); - - if (!endpoint) { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Endpoint not found' })); - return true; - } - - const filePath = getSettingsFilePath(endpointId); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - endpointId, - filePath, - enabled: endpoint.enabled - })); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: (err as Error).message })); - } - return true; - } + // ========== NAMED SUB-ROUTES (must come before :id routes) ========== // ========== SYNC BUILTIN TOOLS AVAILABILITY ========== // POST /api/cli/settings/sync-tools @@ -505,5 +362,152 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise { + try { + const request = body as Partial; + + // Check if just toggling enabled status + if (Object.keys(request).length === 1 && 'enabled' in request) { + const result = toggleEndpointEnabled(endpointId, request.enabled as boolean); + + if (result.success) { + broadcastToClients({ + type: 'CLI_SETTINGS_TOGGLED', + payload: { + endpointId, + enabled: request.enabled, + timestamp: new Date().toISOString() + } + }); + } + return result; + } + + // Full update + const existing = loadEndpointSettings(endpointId); + if (!existing) { + return { error: 'Endpoint not found', status: 404 }; + } + + const updateRequest: SaveEndpointRequest = { + id: endpointId, + name: request.name || existing.name, + description: request.description ?? existing.description, + settings: request.settings || existing.settings, + enabled: request.enabled ?? existing.enabled + }; + + const result = saveEndpointSettings(updateRequest); + + if (result.success) { + broadcastToClients({ + type: 'CLI_SETTINGS_UPDATED', + payload: { + endpoint: result.endpoint, + filePath: result.filePath, + timestamp: new Date().toISOString() + } + }); + } + + return result; + } catch (err) { + return { error: (err as Error).message, status: 500 }; + } + }); + return true; + } + + // ========== DELETE SETTINGS ========== + // DELETE /api/cli/settings/:id + const deleteMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/); + if (deleteMatch && req.method === 'DELETE') { + const endpointId = sanitizeEndpointId(deleteMatch[1]); + try { + const result = deleteEndpointSettings(endpointId); + + if (result.success) { + broadcastToClients({ + type: 'CLI_SETTINGS_DELETED', + payload: { + endpointId, + timestamp: new Date().toISOString() + } + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } else { + res.writeHead(404, { '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 SETTINGS FILE PATH ========== + // GET /api/cli/settings/:id/path + const pathMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)\/path$/); + if (pathMatch && req.method === 'GET') { + const endpointId = sanitizeEndpointId(pathMatch[1]); + try { + const endpoint = loadEndpointSettings(endpointId); + + if (!endpoint) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Endpoint not found' })); + return true; + } + + const filePath = getSettingsFilePath(endpointId); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + endpointId, + filePath, + enabled: endpoint.enabled + })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (err as Error).message })); + } + return true; + } + return false; }