From 1d7374067efa079a32b4ac083f099c6bd14060d9 Mon Sep 17 00:00:00 2001 From: musistudio Date: Wed, 30 Jul 2025 15:39:44 +0800 Subject: [PATCH] fix: improve error handling and config validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add fallback mechanism for service startup with default config - Implement config file backup before saving - Add robust validation for config data in UI components - Improve error handling and user feedback in UI - Fix potential null/undefined access in provider and router components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/cli.ts | 50 ++++++++- src/server.ts | 8 ++ src/utils/index.ts | 14 +++ ui/index.html | 1 - ui/src/App.tsx | 139 +++++++++++++++----------- ui/src/components/ConfigProvider.tsx | 27 ++++- ui/src/components/ProviderList.tsx | 83 +++++++++++---- ui/src/components/Providers.tsx | 65 +++++++++--- ui/src/components/Router.tsx | 59 ++++++++--- ui/src/components/TransformerList.tsx | 72 ++++++++++--- ui/src/components/Transformers.tsx | 62 +++++++----- 11 files changed, 429 insertions(+), 151 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 6ae11bb..b66a406 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -137,10 +137,52 @@ async function main() { startProcess.unref(); if (!(await waitForService())) { - console.error( - "Service startup timeout, please manually run `ccr start` to start the service" - ); - process.exit(1); + // If service startup fails, try to start with default config + console.log("Service startup timeout, trying to start with default configuration..."); + const { initDir, writeConfigFile, backupConfigFile } = require("./utils"); + + try { + // Initialize directories + await initDir(); + + // Backup existing config file if it exists + const backupPath = await backupConfigFile(); + if (backupPath) { + console.log(`Backed up existing configuration file to ${backupPath}`); + } + + // Create a minimal default config file + await writeConfigFile({ + "PORT": 3456, + "Providers": [], + "Router": {} + }); + console.log("Created minimal default configuration file at ~/.claude-code-router/config.json"); + console.log("Please edit this file with your actual configuration."); + + // Try starting the service again + const restartProcess = spawn("node", [cliPath, "start"], { + detached: true, + stdio: "ignore", + }); + + restartProcess.on("error", (error) => { + console.error("Failed to start service with default config:", error.message); + process.exit(1); + }); + + restartProcess.unref(); + + if (!(await waitForService(15000))) { // Wait a bit longer for the first start + console.error( + "Service startup still failing. Please manually run `ccr start` to start the service and check the logs." + ); + process.exit(1); + } + } catch (error: any) { + console.error("Failed to create default configuration:", error.message); + process.exit(1); + } } } diff --git a/src/server.ts b/src/server.ts index bcb65da..6d5f68c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -28,6 +28,14 @@ export const createServer = (config: any): Server => { // Add endpoint to save config.json server.app.post("/api/config", async (req) => { const newConfig = req.body; + + // Backup existing config file if it exists + const { backupConfigFile } = await import("./utils"); + const backupPath = await backupConfigFile(); + if (backupPath) { + console.log(`Backed up existing configuration file to ${backupPath}`); + } + await writeConfigFile(newConfig); return { success: true, message: "Config saved successfully" }; }); diff --git a/src/utils/index.ts b/src/utils/index.ts index a14d5bf..ed9dd4f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -85,6 +85,20 @@ export const readConfigFile = async () => { } }; +export const backupConfigFile = async () => { + try { + if (await fs.access(CONFIG_FILE).then(() => true).catch(() => false)) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = `${CONFIG_FILE}.${timestamp}.bak`; + await fs.copyFile(CONFIG_FILE, backupPath); + return backupPath; + } + } catch (error) { + console.error("Failed to backup config file:", error); + } + return null; +}; + export const writeConfigFile = async (config: any) => { await ensureDir(HOME_DIR); const configWithComment = `${JSON.stringify(config, null, 2)}`; diff --git a/ui/index.html b/ui/index.html index 305150c..3f9f39e 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2,7 +2,6 @@ - CCR UI diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 71ce57e..9a96f86 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -75,87 +75,108 @@ function App() { }, [config, navigate]); const saveConfig = async () => { - if (config) { - try { - // Save to API - const response = await api.updateConfig(config); + // Handle case where config might be null or undefined + if (!config) { + setToast({ message: t('app.config_missing'), type: 'error' }); + return; + } + + try { + // Save to API + const response = await api.updateConfig(config); + // Show success message or handle as needed + console.log('Config saved successfully'); + + // 根据响应信息进行提示 + if (response && typeof response === 'object' && 'success' in response) { + const apiResponse = response as { success: boolean; message?: string }; + if (apiResponse.success) { + setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' }); + } else { + setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' }); + } + } else { + // 默认成功提示 + setToast({ message: t('app.config_saved_success'), type: 'success' }); + } + } catch (error) { + console.error('Failed to save config:', error); + // Handle error appropriately + setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' }); + } + }; + + const saveConfigAndRestart = async () => { + // Handle case where config might be null or undefined + if (!config) { + setToast({ message: t('app.config_missing'), type: 'error' }); + return; + } + + try { + // Save to API + const response = await api.updateConfig(config); + + // Check if save was successful before restarting + let saveSuccessful = true; + if (response && typeof response === 'object' && 'success' in response) { + const apiResponse = response as { success: boolean; message?: string }; + if (!apiResponse.success) { + saveSuccessful = false; + setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' }); + } + } + + // Only restart if save was successful + if (saveSuccessful) { + // Restart service + const response = await api.restartService(); + // Show success message or handle as needed - console.log('Config saved successfully'); + console.log('Config saved and service restarted successfully'); // 根据响应信息进行提示 if (response && typeof response === 'object' && 'success' in response) { const apiResponse = response as { success: boolean; message?: string }; if (apiResponse.success) { - setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' }); - } else { - setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' }); + setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' }); } } else { // 默认成功提示 - setToast({ message: t('app.config_saved_success'), type: 'success' }); + setToast({ message: t('app.config_saved_restart_success'), type: 'success' }); } - } catch (error) { - console.error('Failed to save config:', error); - // Handle error appropriately - setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' }); - } - } - }; - - const saveConfigAndRestart = async () => { - if (config) { - try { - // Save to API - const response = await api.updateConfig(config); - - // Check if save was successful before restarting - let saveSuccessful = true; - if (response && typeof response === 'object' && 'success' in response) { - const apiResponse = response as { success: boolean; message?: string }; - if (!apiResponse.success) { - saveSuccessful = false; - setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' }); - } - } - - // Only restart if save was successful - if (saveSuccessful) { - // Restart service - const response = await api.restartService(); - - // Show success message or handle as needed - console.log('Config saved and service restarted successfully'); - - // 根据响应信息进行提示 - if (response && typeof response === 'object' && 'success' in response) { - const apiResponse = response as { success: boolean; message?: string }; - if (apiResponse.success) { - setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' }); - } - } else { - // 默认成功提示 - setToast({ message: t('app.config_saved_restart_success'), type: 'success' }); - } - } - } catch (error) { - console.error('Failed to save config and restart:', error); - // Handle error appropriately - setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' }); } + } catch (error) { + console.error('Failed to save config and restart:', error); + // Handle error appropriately + setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' }); } }; if (isCheckingAuth) { - return
Loading...
; + return ( +
+
Loading application...
+
+ ); } if (error) { - return
Error: {error.message}
; + return ( +
+
Error: {error.message}
+
+ ); } + // Handle case where config is null or undefined if (!config) { - return
Loading...
; + return ( +
+
Loading configuration...
+
+ ); } return ( diff --git a/ui/src/components/ConfigProvider.tsx b/ui/src/components/ConfigProvider.tsx index 0eb10d3..05f954d 100644 --- a/ui/src/components/ConfigProvider.tsx +++ b/ui/src/components/ConfigProvider.tsx @@ -108,7 +108,32 @@ export function ConfigProvider({ children }: ConfigProviderProps) { try { // Try to fetch config regardless of API key presence const data = await api.getConfig(); - setConfig(data); + + // Validate the received data to ensure it has the expected structure + const validConfig = { + LOG: typeof data.LOG === 'boolean' ? data.LOG : false, + CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '', + HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1', + PORT: typeof data.PORT === 'number' ? data.PORT : 3456, + APIKEY: typeof data.APIKEY === 'string' ? data.APIKEY : '', + transformers: Array.isArray(data.transformers) ? data.transformers : [], + Providers: Array.isArray(data.Providers) ? data.Providers : [], + Router: data.Router && typeof data.Router === 'object' ? { + default: typeof data.Router.default === 'string' ? data.Router.default : '', + background: typeof data.Router.background === 'string' ? data.Router.background : '', + think: typeof data.Router.think === 'string' ? data.Router.think : '', + longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '', + webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : '' + } : { + default: '', + background: '', + think: '', + longContext: '', + webSearch: '' + } + }; + + setConfig(validConfig); } catch (err) { console.error('Failed to fetch config:', err); // If we get a 401, the API client will redirect to login diff --git a/ui/src/components/ProviderList.tsx b/ui/src/components/ProviderList.tsx index 8403eda..5553658 100644 --- a/ui/src/components/ProviderList.tsx +++ b/ui/src/components/ProviderList.tsx @@ -10,29 +10,74 @@ interface ProviderListProps { } export function ProviderList({ providers, onEdit, onRemove }: ProviderListProps) { + // Handle case where providers might be null or undefined + if (!providers || !Array.isArray(providers)) { + return ( +
+
+ No providers configured +
+
+ ); + } + return (
- {providers.map((provider, index) => ( -
-
-

{provider.name}

-

{provider.api_base_url}

-
- {provider.models.map((model) => ( - {model} - ))} + {providers.map((provider, index) => { + // Handle case where individual provider might be null or undefined + if (!provider) { + return ( +
+
+

Invalid Provider

+

Provider data is missing

+
+
+ + +
+
+ ); + } + + // Handle case where provider.name might be null or undefined + const providerName = provider.name || "Unnamed Provider"; + + // Handle case where provider.api_base_url might be null or undefined + const apiBaseUrl = provider.api_base_url || "No API URL"; + + // Handle case where provider.models might be null or undefined + const models = Array.isArray(provider.models) ? provider.models : []; + + return ( +
+
+

{providerName}

+

{apiBaseUrl}

+
+ {models.map((model, modelIndex) => ( + // Handle case where model might be null or undefined + + {model || "Unnamed Model"} + + ))} +
+
+
+ +
-
- - -
-
- ))} + ); + })}
); } \ No newline at end of file diff --git a/ui/src/components/Providers.tsx b/ui/src/components/Providers.tsx index 469bc86..c82a2f5 100644 --- a/ui/src/components/Providers.tsx +++ b/ui/src/components/Providers.tsx @@ -46,10 +46,23 @@ export function Providers() { fetchTransformers(); }, []); + // Handle case where config is null or undefined if (!config) { - return null; + return ( + + + {t("providers.title")} + + +
Loading providers configuration...
+
+
+ ); } + // Validate config.Providers to ensure it's an array + const validProviders = Array.isArray(config.Providers) ? config.Providers : []; + const handleAddProvider = () => { const newProviders = [...config.Providers, { name: "", api_base_url: "", api_key: "", models: [] }]; @@ -307,8 +320,16 @@ export function Providers() { const handleAddModel = (index: number, model: string) => { if (!model.trim()) return; + // Handle case where config.Providers might be null or undefined + if (!config || !Array.isArray(config.Providers)) return; + + // Handle case where the provider at the given index might be null or undefined + if (!config.Providers[index]) return; + const newProviders = [...config.Providers]; - const models = [...newProviders[index].models]; + + // Handle case where provider.models might be null or undefined + const models = Array.isArray(newProviders[index].models) ? [...newProviders[index].models] : []; // Check if model already exists if (!models.includes(model.trim())) { @@ -319,24 +340,36 @@ export function Providers() { }; const handleRemoveModel = (providerIndex: number, modelIndex: number) => { + // Handle case where config.Providers might be null or undefined + if (!config || !Array.isArray(config.Providers)) return; + + // Handle case where the provider at the given index might be null or undefined + if (!config.Providers[providerIndex]) return; + const newProviders = [...config.Providers]; - const models = [...newProviders[providerIndex].models]; - models.splice(modelIndex, 1); - newProviders[providerIndex].models = models; - setConfig({ ...config, Providers: newProviders }); + + // Handle case where provider.models might be null or undefined + const models = Array.isArray(newProviders[providerIndex].models) ? [...newProviders[providerIndex].models] : []; + + // Handle case where modelIndex might be out of bounds + if (modelIndex >= 0 && modelIndex < models.length) { + models.splice(modelIndex, 1); + newProviders[providerIndex].models = models; + setConfig({ ...config, Providers: newProviders }); + } }; - const editingProvider = editingProviderIndex !== null ? config.Providers[editingProviderIndex] : null; + const editingProvider = editingProviderIndex !== null ? validProviders[editingProviderIndex] : null; return ( - {t("providers.title")} ({config.Providers.length}) + {t("providers.title")} ({validProviders.length}) @@ -356,15 +389,15 @@ export function Providers() {
- handleProviderChange(editingProviderIndex, 'name', e.target.value)} /> + handleProviderChange(editingProviderIndex, 'name', e.target.value)} />
- handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} /> + handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} />
- handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} /> + handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} />
@@ -374,7 +407,7 @@ export function Providers() { {hasFetchedModels[editingProviderIndex] ? ( ({ label: model, value: model }))} + options={(editingProvider.models || []).map(model => ({ label: model, value: model }))} value="" onChange={(_) => { // 只更新输入值,不添加模型 @@ -431,7 +464,7 @@ export function Providers() { */}
- {editingProvider.models.map((model, modelIndex) => ( + {(editingProvider.models || []).map((model, modelIndex) => ( {model} + +
+
+ ); + } + + // Handle case where transformer.path might be null or undefined + const transformerPath = transformer.path || "Unnamed Transformer"; + + // Handle case where transformer.options might be null or undefined + const options = transformer.options || {}; + + // Handle case where options.project might be null or undefined + const project = options.project || "No Project"; + + return ( +
+
+

{transformerPath}

+

{project}

+
+
+ + +
-
- - -
-
- ))} + ); + })}
); } diff --git a/ui/src/components/Transformers.tsx b/ui/src/components/Transformers.tsx index 1a7cc43..c39b9a7 100644 --- a/ui/src/components/Transformers.tsx +++ b/ui/src/components/Transformers.tsx @@ -23,27 +23,40 @@ export function Transformers() { const [deletingTransformerIndex, setDeletingTransformerIndex] = useState(null); const [newTransformer, setNewTransformer] = useState<{ path: string; options: { [key: string]: string } } | null>(null); + // Handle case where config is null or undefined if (!config) { - return null; + return ( + + + {t("transformers.title")} + + +
Loading transformers configuration...
+
+
+ ); } + // Validate config.transformers to ensure it's an array + const validTransformers = Array.isArray(config.transformers) ? config.transformers : []; + const handleAddTransformer = () => { const newTransformer = { path: "", options: {} }; setNewTransformer(newTransformer); - setEditingTransformerIndex(config.transformers.length); // Use the length as index for the new item + setEditingTransformerIndex(validTransformers.length); // Use the length as index for the new item }; const handleRemoveTransformer = (index: number) => { - const newTransformers = [...config.transformers]; + const newTransformers = [...validTransformers]; newTransformers.splice(index, 1); setConfig({ ...config, transformers: newTransformers }); setDeletingTransformerIndex(null); }; const handleTransformerChange = (index: number, field: string, value: string, optionKey?: string) => { - if (index < config.transformers.length) { + if (index < validTransformers.length) { // Editing an existing transformer - const newTransformers = [...config.transformers]; + const newTransformers = [...validTransformers]; if (optionKey !== undefined) { newTransformers[index].options[optionKey] = value; } else { @@ -65,15 +78,15 @@ export function Transformers() { }; const editingTransformer = editingTransformerIndex !== null ? - (editingTransformerIndex < config.transformers.length ? - config.transformers[editingTransformerIndex] : + (editingTransformerIndex < validTransformers.length ? + validTransformers[editingTransformerIndex] : newTransformer) : null; const handleSaveTransformer = () => { - if (newTransformer && editingTransformerIndex === config.transformers.length) { + if (newTransformer && editingTransformerIndex === validTransformers.length) { // Saving a new transformer - const newTransformers = [...config.transformers, newTransformer]; + const newTransformers = [...validTransformers, newTransformer]; setConfig({ ...config, transformers: newTransformers }); } // Close the dialog @@ -90,12 +103,12 @@ export function Transformers() { return ( - {t("transformers.title")} ({config.transformers.length}) + {t("transformers.title")} ({validTransformers.length}) @@ -113,7 +126,7 @@ export function Transformers() { handleTransformerChange(editingTransformerIndex, "path", e.target.value)} /> @@ -124,11 +137,12 @@ export function Transformers() { variant="outline" size="sm" onClick={() => { - const newKey = `param${Object.keys(editingTransformer.options).length + 1}`; + const options = editingTransformer.options || {}; + const newKey = `param${Object.keys(options).length + 1}`; if (editingTransformerIndex !== null) { - const newOptions = { ...editingTransformer.options, [newKey]: "" }; - if (editingTransformerIndex < config.transformers.length) { - const newTransformers = [...config.transformers]; + const newOptions = { ...options, [newKey]: "" }; + if (editingTransformerIndex < validTransformers.length) { + const newTransformers = [...validTransformers]; newTransformers[editingTransformerIndex].options = newOptions; setConfig({ ...config, transformers: newTransformers }); } else if (newTransformer) { @@ -140,17 +154,18 @@ export function Transformers() { - {Object.entries(editingTransformer.options).map(([key, value]) => ( + {Object.entries(editingTransformer.options || {}).map(([key, value]) => (
{ - const newOptions = { ...editingTransformer.options }; + const options = editingTransformer.options || {}; + const newOptions = { ...options }; delete newOptions[key]; newOptions[e.target.value] = value; if (editingTransformerIndex !== null) { - if (editingTransformerIndex < config.transformers.length) { - const newTransformers = [...config.transformers]; + if (editingTransformerIndex < validTransformers.length) { + const newTransformers = [...validTransformers]; newTransformers[editingTransformerIndex].options = newOptions; setConfig({ ...config, transformers: newTransformers }); } else if (newTransformer) { @@ -174,10 +189,11 @@ export function Transformers() { size="icon" onClick={() => { if (editingTransformerIndex !== null) { - const newOptions = { ...editingTransformer.options }; + const options = editingTransformer.options || {}; + const newOptions = { ...options }; delete newOptions[key]; - if (editingTransformerIndex < config.transformers.length) { - const newTransformers = [...config.transformers]; + if (editingTransformerIndex < validTransformers.length) { + const newTransformers = [...validTransformers]; newTransformers[editingTransformerIndex].options = newOptions; setConfig({ ...config, transformers: newTransformers }); } else if (newTransformer) {