diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 2dc054c..220cb46 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -60,17 +60,20 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R let config_path = get_codex_config_path(); if let Some(parent) = auth_path.parent() { - std::fs::create_dir_all(parent).map_err(|e| format!("创建 Codex 目录失败: {}", e))?; + std::fs::create_dir_all(parent) + .map_err(|e| format!("创建 Codex 目录失败: {}: {}", parent.display(), e))?; } // 读取旧内容用于回滚 let old_auth = if auth_path.exists() { - Some(fs::read(&auth_path).map_err(|e| format!("读取旧 auth.json 失败: {}", e))?) + Some(fs::read(&auth_path) + .map_err(|e| format!("读取旧 auth.json 失败: {}: {}", auth_path.display(), e))?) } else { None }; let _old_config = if config_path.exists() { - Some(fs::read(&config_path).map_err(|e| format!("读取旧 config.toml 失败: {}", e))?) + Some(fs::read(&config_path) + .map_err(|e| format!("读取旧 config.toml 失败: {}: {}", config_path.display(), e))?) } else { None }; @@ -81,8 +84,13 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R None => String::new(), }; if !cfg_text.trim().is_empty() { - toml::from_str::(&cfg_text) - .map_err(|e| format!("config.toml 格式错误: {}", e))?; + toml::from_str::(&cfg_text).map_err(|e| { + format!( + "config.toml 语法错误: {} (路径: {})", + e, + config_path.display() + ) + })?; } // 第一步:写 auth.json diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 04a9aa4..c5ac6e1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -336,8 +336,13 @@ pub async fn switch_provider( if auth_path.exists() { let auth: Value = crate::config::read_json_file(&auth_path)?; let config_str = if config_path.exists() { - std::fs::read_to_string(&config_path) - .map_err(|e| format!("读取 config.toml 失败: {}", e))? + std::fs::read_to_string(&config_path).map_err(|e| { + format!( + "读取 config.toml 失败: {}: {}", + config_path.display(), + e + ) + })? } else { String::new() }; diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index cf9350c..1b9e896 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -118,16 +118,19 @@ pub fn read_json_file Deserialize<'a>>(path: &Path) -> Result(path: &Path, data: &T) -> Result<(), String> { // 确保目录存在 if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; + fs::create_dir_all(parent) + .map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?; } let json = @@ -139,7 +142,8 @@ pub fn write_json_file(path: &Path, data: &T) -> Result<(), String /// 原子写入文本文件(用于 TOML/纯文本) pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; + fs::create_dir_all(parent) + .map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?; } atomic_write(path, data.as_bytes()) } @@ -147,7 +151,8 @@ pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> { /// 原子写入:写入临时文件后 rename 替换,避免半写状态 pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; + fs::create_dir_all(parent) + .map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?; } let parent = path.parent().ok_or_else(|| "无效的路径".to_string())?; @@ -164,10 +169,12 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> { tmp.push(format!("{}.tmp.{}", file_name, ts)); { - let mut f = fs::File::create(&tmp).map_err(|e| format!("创建临时文件失败: {}", e))?; + let mut f = fs::File::create(&tmp) + .map_err(|e| format!("创建临时文件失败: {}: {}", tmp.display(), e))?; f.write_all(data) - .map_err(|e| format!("写入临时文件失败: {}", e))?; - f.flush().map_err(|e| format!("刷新临时文件失败: {}", e))?; + .map_err(|e| format!("写入临时文件失败: {}: {}", tmp.display(), e))?; + f.flush() + .map_err(|e| format!("刷新临时文件失败: {}: {}", tmp.display(), e))?; } #[cfg(unix)] @@ -185,12 +192,14 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> { if path.exists() { let _ = fs::remove_file(path); } - fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?; + fs::rename(&tmp, path) + .map_err(|e| format!("原子替换失败: {} -> {}: {}", tmp.display(), path.display(), e))?; } #[cfg(not(windows))] { - fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?; + fs::rename(&tmp, path) + .map_err(|e| format!("原子替换失败: {} -> {}: {}", tmp.display(), path.display(), e))?; } Ok(()) } diff --git a/src/App.tsx b/src/App.tsx index 8d5f4a6..09b8214 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ function App() { const [currentProviderId, setCurrentProviderId] = useState(""); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [editingProviderId, setEditingProviderId] = useState( - null + null, ); const [notification, setNotification] = useState<{ message: string; @@ -42,7 +42,7 @@ function App() { const showNotification = ( message: string, type: "success" | "error", - duration = 3000 + duration = 3000, ) => { // 清除之前的定时器 if (timeoutRef.current) { @@ -208,24 +208,33 @@ function App() { }; const handleSwitchProvider = async (id: string) => { - const success = await window.api.switchProvider(id, activeApp); - if (success) { - setCurrentProviderId(id); - // 显示重启提示 - const appName = t(`apps.${activeApp}`); - showNotification( - t("notifications.switchSuccess", { appName }), - "success", - 2000 - ); - // 更新托盘菜单 - await window.api.updateTrayMenu(); + try { + const success = await window.api.switchProvider(id, activeApp); + if (success) { + setCurrentProviderId(id); + // 显示重启提示 + const appName = t(`apps.${activeApp}`); + showNotification( + t("notifications.switchSuccess", { appName }), + "success", + 2000, + ); + // 更新托盘菜单 + await window.api.updateTrayMenu(); - if (activeApp === "claude") { - await syncClaudePlugin(id, true); + if (activeApp === "claude") { + await syncClaudePlugin(id, true); + } + } else { + showNotification(t("notifications.switchFailed"), "error"); } - } else { - showNotification(t("notifications.switchFailed"), "error"); + } catch (error) { + const detail = extractErrorMessage(error); + const msg = detail + ? `${t("notifications.switchFailed")}: ${detail}` + : t("notifications.switchFailed"); + // 详细错误展示稍长时间,便于用户阅读 + showNotification(msg, "error", detail ? 6000 : 3000); } }; @@ -297,6 +306,15 @@ function App() {
+ + @@ -212,7 +214,7 @@ const ProviderList: React.FC = ({ "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center", claudeApplied ? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20" - : "border border-gray-300 text-gray-700 hover:border-green-300 hover:text-green-600 hover:bg-green-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-green-700 dark:hover:text-green-400 dark:hover:bg-green-900/20" + : "border border-gray-300 text-gray-700 hover:border-green-300 hover:text-green-600 hover:bg-green-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-green-700 dark:hover:text-green-400 dark:hover:bg-green-900/20", )} title={ claudeApplied @@ -234,7 +236,7 @@ const ProviderList: React.FC = ({ "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[90px] justify-center whitespace-nowrap", isCurrent ? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed" - : "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700" + : "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700", )} > {isCurrent ? : } @@ -256,7 +258,7 @@ const ProviderList: React.FC = ({ buttonStyles.icon, isCurrent ? "text-gray-400 cursor-not-allowed" - : "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10" + : "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10", )} title={t("provider.deleteProvider")} > diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index dd963e2..6f851b2 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -26,7 +26,10 @@ interface SettingsModalProps { onImportSuccess?: () => void | Promise; } -export default function SettingsModal({ onClose, onImportSuccess }: SettingsModalProps) { +export default function SettingsModal({ + onClose, + onImportSuccess, +}: SettingsModalProps) { const { t, i18n } = useTranslation(); const normalizeLanguage = (lang?: string | null): "zh" | "en" => @@ -67,10 +70,12 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa // 导入/导出相关状态 const [isImporting, setIsImporting] = useState(false); - const [importStatus, setImportStatus] = useState<'idle' | 'importing' | 'success' | 'error'>('idle'); + const [importStatus, setImportStatus] = useState< + "idle" | "importing" | "success" | "error" + >("idle"); const [importError, setImportError] = useState(""); const [importBackupId, setImportBackupId] = useState(""); - const [selectedImportFile, setSelectedImportFile] = useState(''); + const [selectedImportFile, setSelectedImportFile] = useState(""); useEffect(() => { loadSettings(); @@ -340,7 +345,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa // 如果未知或为空,回退到 releases 首页 if (!targetVersion || targetVersion === unknownLabel) { await window.api.openExternal( - "https://github.com/farion1231/cc-switch/releases" + "https://github.com/farion1231/cc-switch/releases", ); return; } @@ -348,7 +353,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa ? targetVersion : `v${targetVersion}`; await window.api.openExternal( - `https://github.com/farion1231/cc-switch/releases/tag/${tag}` + `https://github.com/farion1231/cc-switch/releases/tag/${tag}`, ); } catch (error) { console.error(t("console.openReleaseNotesFailed"), error); @@ -358,7 +363,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa // 导出配置处理函数 const handleExportConfig = async () => { try { - const defaultName = `cc-switch-config-${new Date().toISOString().split('T')[0]}.json`; + const defaultName = `cc-switch-config-${new Date().toISOString().split("T")[0]}.json`; const filePath = await window.api.saveFileDialog(defaultName); if (!filePath) return; // 用户取消了 @@ -380,8 +385,8 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa const filePath = await window.api.openFileDialog(); if (filePath) { setSelectedImportFile(filePath); - setImportStatus('idle'); // 重置状态 - setImportError(''); + setImportStatus("idle"); // 重置状态 + setImportError(""); } } catch (error) { console.error(t("settings.selectFileFailed") + ":", error); @@ -394,22 +399,22 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa if (!selectedImportFile || isImporting) return; setIsImporting(true); - setImportStatus('importing'); + setImportStatus("importing"); try { const result = await window.api.importConfigFromFile(selectedImportFile); if (result.success) { - setImportBackupId(result.backupId || ''); - setImportStatus('success'); + setImportBackupId(result.backupId || ""); + setImportStatus("success"); // ImportProgressModal 会在2秒后触发数据刷新回调 } else { setImportError(result.message || t("settings.configCorrupted")); - setImportStatus('error'); + setImportStatus("error"); } } catch (error) { setImportError(String(error)); - setImportStatus('error'); + setImportStatus("error"); } finally { setIsImporting(false); } @@ -642,18 +647,22 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa disabled={!selectedImportFile || isImporting} className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors text-white ${ !selectedImportFile || isImporting - ? 'bg-gray-400 cursor-not-allowed' - : 'bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700' + ? "bg-gray-400 cursor-not-allowed" + : "bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700" }`} > - {isImporting ? t("settings.importing") : t("settings.import")} + {isImporting + ? t("settings.importing") + : t("settings.import")}
{/* 显示选择的文件 */} {selectedImportFile && (
- {selectedImportFile.split('/').pop() || selectedImportFile.split('\\').pop() || selectedImportFile} + {selectedImportFile.split("/").pop() || + selectedImportFile.split("\\").pop() || + selectedImportFile}
)} @@ -757,15 +766,15 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa {/* Import Progress Modal */} - {importStatus !== 'idle' && ( + {importStatus !== "idle" && ( { - setImportStatus('idle'); - setImportError(''); - setSelectedImportFile(''); + setImportStatus("idle"); + setImportError(""); + setSelectedImportFile(""); }} onSuccess={() => { if (onImportSuccess) { @@ -773,7 +782,12 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa } void window.api .updateTrayMenu() - .catch((error) => console.error("[SettingsModal] Failed to refresh tray menu", error)); + .catch((error) => + console.error( + "[SettingsModal] Failed to refresh tray menu", + error, + ), + ); }} /> )} diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index c2dd514..1703198 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -32,7 +32,7 @@ export function generateThirdPartyAuth(apiKey: string): Record { export function generateThirdPartyConfig( providerName: string, baseUrl: string, - modelName = "gpt-5-codex" + modelName = "gpt-5-codex", ): string { // 清理供应商名称,确保符合TOML键名规范 const cleanProviderName = @@ -71,7 +71,7 @@ export const codexProviderPresets: CodexProviderPreset[] = [ config: generateThirdPartyConfig( "packycode", "https://codex-api.packycode.com/v1", - "gpt-5-codex" + "gpt-5-codex", ), // Codex 请求地址候选(用于地址管理/测速) endpointCandidates: [ diff --git a/src/config/providerPresets.ts b/src/config/providerPresets.ts index a994ced..1edd9a5 100644 --- a/src/config/providerPresets.ts +++ b/src/config/providerPresets.ts @@ -110,7 +110,8 @@ export const providerPresets: ProviderPreset[] = [ apiKeyUrl: "https://console.streamlake.ai/console/wanqing/api-key", settingsConfig: { env: { - ANTHROPIC_BASE_URL: "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy", + ANTHROPIC_BASE_URL: + "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy", ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_MODEL: "KAT-Coder", ANTHROPIC_SMALL_FAST_MODEL: "KAT-Coder", @@ -146,5 +147,4 @@ export const providerPresets: ProviderPreset[] = [ ], category: "third_party", }, - ]; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 5526e1e..5002f0c 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -20,7 +20,8 @@ const getInitialLanguage = (): "zh" | "en" => { const navigatorLang = typeof navigator !== "undefined" - ? navigator.language?.toLowerCase() ?? navigator.languages?.[0]?.toLowerCase() + ? (navigator.language?.toLowerCase() ?? + navigator.languages?.[0]?.toLowerCase()) : undefined; if (navigatorLang?.startsWith("zh")) { diff --git a/src/lib/platform.ts b/src/lib/platform.ts index adcc402..ed5b61a 100644 --- a/src/lib/platform.ts +++ b/src/lib/platform.ts @@ -22,9 +22,10 @@ export const isLinux = (): boolean => { try { const ua = navigator.userAgent || ""; // WebKitGTK/Chromium 在 Linux/Wayland/X11 下 UA 通常包含 Linux 或 X11 - return /linux|x11/i.test(ua) && !/android/i.test(ua) && !isMac() && !isWindows(); + return ( + /linux|x11/i.test(ua) && !/android/i.test(ua) && !isMac() && !isWindows() + ); } catch { return false; } }; - diff --git a/src/lib/tauri-api.ts b/src/lib/tauri-api.ts index 70c2fe9..c040cfc 100644 --- a/src/lib/tauri-api.ts +++ b/src/lib/tauri-api.ts @@ -150,7 +150,9 @@ export const tauriAPI = { }, // 选择配置目录(可选默认路径) - selectConfigDirectory: async (defaultPath?: string): Promise => { + selectConfigDirectory: async ( + defaultPath?: string, + ): Promise => { try { // 后端参数为 snake_case:default_path return await invoke("pick_directory", { default_path: defaultPath }); @@ -384,7 +386,9 @@ export const tauriAPI = { // theirs: 导入导出与文件对话框 // 导出配置到文件 - exportConfigToFile: async (filePath: string): Promise<{ + exportConfigToFile: async ( + filePath: string, + ): Promise<{ success: boolean; message: string; filePath: string; @@ -398,7 +402,9 @@ export const tauriAPI = { }, // 从文件导入配置 - importConfigFromFile: async (filePath: string): Promise<{ + importConfigFromFile: async ( + filePath: string, + ): Promise<{ success: boolean; message: string; backupId?: string; @@ -415,7 +421,9 @@ export const tauriAPI = { saveFileDialog: async (defaultName: string): Promise => { try { // 后端参数为 snake_case:default_name - const result = await invoke("save_file_dialog", { default_name: defaultName }); + const result = await invoke("save_file_dialog", { + default_name: defaultName, + }); return result; } catch (error) { console.error("打开保存对话框失败:", error); diff --git a/src/main.tsx b/src/main.tsx index 8bdc674..f2d9adb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -25,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - + , ); diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index e62c5c6..979ac9d 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -178,16 +178,16 @@ export const getApiKeyFromConfig = (jsonString: string): string => { // 模板变量替换 export const applyTemplateValues = ( config: any, - templateValues: Record | undefined + templateValues: Record | undefined, ): any => { const resolvedValues = Object.fromEntries( Object.entries(templateValues ?? {}).map(([key, value]) => { const resolvedValue = value.editorValue !== undefined ? value.editorValue - : value.defaultValue ?? ""; + : (value.defaultValue ?? ""); return [key, resolvedValue]; - }) + }), ); const replaceInString = (str: string): string => { @@ -384,6 +384,7 @@ export const setCodexBaseUrl = ( return configText.replace(pattern, replacementLine); } - const prefix = configText && !configText.endsWith("\n") ? `${configText}\n` : configText; + const prefix = + configText && !configText.endsWith("\n") ? `${configText}\n` : configText; return `${prefix}${replacementLine}\n`; }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 44a0148..2a8f5a3 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -64,31 +64,33 @@ declare global { testApiEndpoints: ( urls: string[], options?: { timeoutSecs?: number }, - ) => Promise>; + ) => Promise< + Array<{ + url: string; + latency: number | null; + status?: number; + error?: string; + }> + >; // 自定义端点管理 getCustomEndpoints: ( appType: AppType, - providerId: string + providerId: string, ) => Promise; addCustomEndpoint: ( appType: AppType, providerId: string, - url: string + url: string, ) => Promise; removeCustomEndpoint: ( appType: AppType, providerId: string, - url: string + url: string, ) => Promise; updateEndpointLastUsed: ( appType: AppType, providerId: string, - url: string + url: string, ) => Promise; }; platform: {