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}