From b2b20dadd76398e77ca2ec5a5c91e8d30df4d432 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 27 Feb 2026 11:50:38 +0800 Subject: [PATCH 01/34] fix: add import button for OpenCode/OpenClaw empty state and remove auto-import on startup Previously OpenCode and OpenClaw auto-imported providers from live config on app startup, which could confuse users. Now they follow the same pattern as Claude/Codex/Gemini: manual import via the empty state button. --- src-tauri/src/lib.rs | 24 +---------------------- src/components/providers/ProviderList.tsx | 18 +++++++++++------ src/lib/api/providers.ts | 8 ++++++++ 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1ba9229da..b0330ec5c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -448,18 +448,7 @@ pub fn run() { Err(e) => log::warn!("✗ Failed to read skills migration flag: {e}"), } - // 2. OpenCode 供应商导入(累加式模式,需特殊处理) - // OpenCode 与其他应用不同:配置文件中可同时存在多个供应商 - // 需要遍历 provider 字段下的每个供应商并导入 - match crate::services::provider::import_opencode_providers_from_live(&app_state) { - Ok(count) if count > 0 => { - log::info!("✓ Imported {count} OpenCode provider(s) from live config"); - } - Ok(_) => log::debug!("○ No OpenCode providers found to import"), - Err(e) => log::warn!("○ Failed to import OpenCode providers: {e}"), - } - - // 2.2 OMO 配置导入(当数据库中无 OMO provider 时,从本地文件导入) + // 2. OMO 配置导入(当数据库中无 OMO provider 时,从本地文件导入) { let has_omo = app_state .db @@ -510,17 +499,6 @@ pub fn run() { } } - // 2.4 OpenClaw 供应商导入(累加式模式,需特殊处理) - // OpenClaw 与 OpenCode 类似:配置文件中可同时存在多个供应商 - // 需要遍历 models.providers 字段下的每个供应商并导入 - match crate::services::provider::import_openclaw_providers_from_live(&app_state) { - Ok(count) if count > 0 => { - log::info!("✓ Imported {count} OpenClaw provider(s) from live config"); - } - Ok(_) => log::debug!("○ No OpenClaw providers found to import"), - Err(e) => log::warn!("○ Failed to import OpenClaw providers: {e}"), - } - // 3. 导入 MCP 服务器配置(表空时触发) if app_state.db.is_mcp_table_empty().unwrap_or(false) { log::info!("MCP table empty, importing from live configurations..."); diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 259120142..2063fd595 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -179,7 +179,17 @@ export function ProviderList({ // Import current live config as default provider const queryClient = useQueryClient(); const importMutation = useMutation({ - mutationFn: () => providersApi.importDefault(appId), + mutationFn: async (): Promise => { + if (appId === "opencode") { + const count = await providersApi.importOpenCodeFromLive(); + return count > 0; + } + if (appId === "openclaw") { + const count = await providersApi.importOpenClawFromLive(); + return count > 0; + } + return providersApi.importDefault(appId); + }, onSuccess: (imported) => { if (imported) { queryClient.invalidateQueries({ queryKey: ["providers", appId] }); @@ -245,15 +255,11 @@ export function ProviderList({ ); } - // Only show import button for standard apps (not additive-mode apps like OpenCode/OpenClaw) - const showImportButton = - appId === "claude" || appId === "codex" || appId === "gemini"; - if (sortedProviders.length === 0) { return ( importMutation.mutate() : undefined} + onImport={() => importMutation.mutate()} /> ); } diff --git a/src/lib/api/providers.ts b/src/lib/api/providers.ts index b0a51a9e5..47a950630 100644 --- a/src/lib/api/providers.ts +++ b/src/lib/api/providers.ts @@ -110,6 +110,14 @@ export const providersApi = { async getOpenClawLiveProviderIds(): Promise { return await invoke("get_openclaw_live_provider_ids"); }, + + /** + * 从 OpenClaw live 配置导入供应商到数据库 + * OpenClaw 特有功能:由于累加模式,用户可能已在 openclaw.json 中配置供应商 + */ + async importOpenClawFromLive(): Promise { + return await invoke("import_openclaw_providers_from_live"); + }, }; // ============================================================================ From 859f413756839c8acbe63089f4dc5e10409a0fbd Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 27 Feb 2026 23:12:34 +0800 Subject: [PATCH 02/34] revert: restore full config overwrite + Common Config Snippet (revert 992dda5c) Revert the partial key-field merging refactoring introduced in 992dda5c, along with two dependent commits (24fa8a18, 87604b18) that referenced the now-removed ClaudeQuickToggles component. The whitelist-based partial merge approach had critical issues: - Non-whitelisted custom fields were lost during provider switching - Backfill permanently stripped non-key fields from the database - Whitelist required constant maintenance to track upstream changes This restores the proven "full config overwrite + Common Config Snippet" architecture where each provider stores its complete configuration and shared settings are managed via a separate snippet mechanism. Reverted commits: - 24fa8a18: context-aware JSON editor hint + hide quick toggles - 87604b18: hide ClaudeQuickToggles when creating - 992dda5c: partial key-field merging refactoring Restored: - Full config snapshot write (write_live_snapshot) for Claude/Codex/Gemini - Full config backfill (settings_config = live_config) - Common Config Snippet UI and backend commands - 6 frontend components/hooks for common config editing - configApi barrel export and DB snippet methods Removed: - ClaudeQuickToggles component - write_live_partial / backfill_key_fields / patch_claude_live - All KEY_FIELDS constants --- src-tauri/src/app_config.rs | 60 +++ src-tauri/src/commands/config.rs | 136 +++++ src-tauri/src/commands/provider.rs | 28 +- src-tauri/src/database/dao/settings.rs | 25 + src-tauri/src/database/migration.rs | 33 ++ src-tauri/src/database/tests.rs | 4 + src-tauri/src/lib.rs | 6 +- src-tauri/src/provider.rs | 5 - src-tauri/src/services/config.rs | 150 ++++++ src-tauri/src/services/provider/live.rs | 435 +--------------- src-tauri/src/services/provider/mod.rs | 286 ++++++++++- src-tauri/src/services/proxy.rs | 4 +- src-tauri/tests/import_export_sync.rs | 272 +++++++++- src-tauri/tests/provider_commands.rs | 35 +- src-tauri/tests/provider_service.rs | 26 +- .../providers/EditProviderDialog.tsx | 1 - .../providers/forms/ClaudeFormFields.tsx | 47 +- .../providers/forms/ClaudeQuickToggles.tsx | 134 ----- .../forms/CodexCommonConfigModal.tsx | 107 ++++ .../providers/forms/CodexConfigEditor.tsx | 50 +- .../providers/forms/CodexConfigSections.tsx | 48 +- .../providers/forms/CommonConfigEditor.tsx | 280 +++++++++++ .../forms/GeminiCommonConfigModal.tsx | 106 ++++ .../providers/forms/GeminiConfigEditor.tsx | 41 +- .../providers/forms/GeminiConfigSections.tsx | 52 +- .../providers/forms/ProviderForm.tsx | 202 ++++---- src/components/providers/forms/hooks/index.ts | 3 + .../providers/forms/hooks/useApiKeyState.ts | 14 +- .../forms/hooks/useCodexCommonConfig.ts | 312 ++++++++++++ .../forms/hooks/useCommonConfigSnippet.ts | 335 +++++++++++++ .../forms/hooks/useGeminiCommonConfig.ts | 469 ++++++++++++++++++ src/config/claudeProviderPresets.ts | 4 +- src/i18n/locales/en.json | 37 +- src/i18n/locales/ja.json | 37 +- src/i18n/locales/zh.json | 37 +- src/lib/api/config.ts | 77 +++ src/lib/api/index.ts | 1 + src/types.ts | 9 - src/utils/providerConfigUtils.ts | 234 ++++++++- 39 files changed, 3285 insertions(+), 857 deletions(-) delete mode 100644 src/components/providers/forms/ClaudeQuickToggles.tsx create mode 100644 src/components/providers/forms/CodexCommonConfigModal.tsx create mode 100644 src/components/providers/forms/CommonConfigEditor.tsx create mode 100644 src/components/providers/forms/GeminiCommonConfigModal.tsx create mode 100644 src/components/providers/forms/hooks/useCodexCommonConfig.ts create mode 100644 src/components/providers/forms/hooks/useCommonConfigSnippet.ts create mode 100644 src/components/providers/forms/hooks/useGeminiCommonConfig.ts create mode 100644 src/lib/api/config.ts diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index f2bb99eef..18e3fbec8 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -352,6 +352,49 @@ impl FromStr for AppType { } } +/// 通用配置片段(按应用分治) +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CommonConfigSnippets { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub claude: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub codex: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gemini: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub opencode: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub openclaw: Option, +} + +impl CommonConfigSnippets { + /// 获取指定应用的通用配置片段 + pub fn get(&self, app: &AppType) -> Option<&String> { + match app { + AppType::Claude => self.claude.as_ref(), + AppType::Codex => self.codex.as_ref(), + AppType::Gemini => self.gemini.as_ref(), + AppType::OpenCode => self.opencode.as_ref(), + AppType::OpenClaw => self.openclaw.as_ref(), + } + } + + /// 设置指定应用的通用配置片段 + pub fn set(&mut self, app: &AppType, snippet: Option) { + match app { + AppType::Claude => self.claude = snippet, + AppType::Codex => self.codex = snippet, + AppType::Gemini => self.gemini = snippet, + AppType::OpenCode => self.opencode = snippet, + AppType::OpenClaw => self.openclaw = snippet, + } + } +} + /// 多应用配置结构(向后兼容) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MultiAppConfig { @@ -369,6 +412,12 @@ pub struct MultiAppConfig { /// Claude Skills 配置 #[serde(default)] pub skills: SkillStore, + /// 通用配置片段(按应用分治) + #[serde(default)] + pub common_config_snippets: CommonConfigSnippets, + /// Claude 通用配置片段(旧字段,用于向后兼容迁移) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub claude_common_config_snippet: Option, } fn default_version() -> u32 { @@ -390,6 +439,8 @@ impl Default for MultiAppConfig { mcp: McpRoot::default(), prompts: PromptRoot::default(), skills: SkillStore::default(), + common_config_snippets: CommonConfigSnippets::default(), + claude_common_config_snippet: None, } } } @@ -483,6 +534,15 @@ impl MultiAppConfig { updated = true; } + // 迁移通用配置片段:claude_common_config_snippet → common_config_snippets.claude + if let Some(old_claude_snippet) = config.claude_common_config_snippet.take() { + log::info!( + "迁移通用配置:claude_common_config_snippet → common_config_snippets.claude" + ); + config.common_config_snippets.claude = Some(old_claude_snippet); + updated = true; + } + if updated { log::info!("配置结构已更新(包括 MCP 迁移或 Prompt 自动导入),保存配置..."); config.save()?; diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 54c996695..8edf15617 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -7,6 +7,7 @@ use tauri_plugin_opener::OpenerExt; use crate::app_config::AppType; use crate::codex_config; use crate::config::{self, get_claude_settings_path, ConfigStatus}; +use crate::settings; #[tauri::command] pub async fn get_claude_config_status() -> Result { @@ -15,6 +16,18 @@ pub async fn get_claude_config_status() -> Result { use std::str::FromStr; +fn invalid_json_format_error(error: serde_json::Error) -> String { + let lang = settings::get_settings() + .language + .unwrap_or_else(|| "zh".to_string()); + + match lang.as_str() { + "en" => format!("Invalid JSON format: {error}"), + "ja" => format!("JSON形式が無効です: {error}"), + _ => format!("无效的 JSON 格式: {error}"), + } +} + #[tauri::command] pub async fn get_config_status(app: String) -> Result { match AppType::from_str(&app).map_err(|e| e.to_string())? { @@ -150,3 +163,126 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result { Ok(true) } + +#[tauri::command] +pub async fn get_claude_common_config_snippet( + state: tauri::State<'_, crate::store::AppState>, +) -> Result, String> { + state + .db + .get_config_snippet("claude") + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_claude_common_config_snippet( + snippet: String, + state: tauri::State<'_, crate::store::AppState>, +) -> Result<(), String> { + if !snippet.trim().is_empty() { + serde_json::from_str::(&snippet).map_err(invalid_json_format_error)?; + } + + let value = if snippet.trim().is_empty() { + None + } else { + Some(snippet) + }; + + state + .db + .set_config_snippet("claude", value) + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn get_common_config_snippet( + app_type: String, + state: tauri::State<'_, crate::store::AppState>, +) -> Result, String> { + state + .db + .get_config_snippet(&app_type) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_common_config_snippet( + app_type: String, + snippet: String, + state: tauri::State<'_, crate::store::AppState>, +) -> Result<(), String> { + if !snippet.trim().is_empty() { + match app_type.as_str() { + "claude" | "gemini" | "omo" | "omo-slim" => { + serde_json::from_str::(&snippet) + .map_err(invalid_json_format_error)?; + } + "codex" => {} + _ => {} + } + } + + let value = if snippet.trim().is_empty() { + None + } else { + Some(snippet) + }; + + state + .db + .set_config_snippet(&app_type, value) + .map_err(|e| e.to_string())?; + + if app_type == "omo" + && state + .db + .get_current_omo_provider("opencode", "omo") + .map_err(|e| e.to_string())? + .is_some() + { + crate::services::OmoService::write_config_to_file( + state.inner(), + &crate::services::omo::STANDARD, + ) + .map_err(|e| e.to_string())?; + } + if app_type == "omo-slim" + && state + .db + .get_current_omo_provider("opencode", "omo-slim") + .map_err(|e| e.to_string())? + .is_some() + { + crate::services::OmoService::write_config_to_file( + state.inner(), + &crate::services::omo::SLIM, + ) + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +pub async fn extract_common_config_snippet( + appType: String, + settingsConfig: Option, + state: tauri::State<'_, crate::store::AppState>, +) -> Result { + let app = AppType::from_str(&appType).map_err(|e| e.to_string())?; + + if let Some(settings_config) = settingsConfig.filter(|s| !s.trim().is_empty()) { + let settings: serde_json::Value = + serde_json::from_str(&settings_config).map_err(invalid_json_format_error)?; + + return crate::services::provider::ProviderService::extract_common_config_snippet_from_settings( + app, + &settings, + ) + .map_err(|e| e.to_string()); + } + + crate::services::provider::ProviderService::extract_common_config_snippet(&state, app) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 590acd7c7..ef9c1bb0e 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -97,7 +97,27 @@ pub fn switch_provider( } fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result { - let imported = ProviderService::import_default_config(state, app_type)?; + let imported = ProviderService::import_default_config(state, app_type.clone())?; + + if imported { + // Extract common config snippet (mirrors old startup logic in lib.rs) + if state + .db + .get_config_snippet(app_type.as_str()) + .ok() + .flatten() + .is_none() + { + match ProviderService::extract_common_config_snippet(state, app_type.clone()) { + Ok(snippet) if !snippet.is_empty() && snippet != "{}" => { + let _ = state + .db + .set_config_snippet(app_type.as_str(), Some(snippet)); + } + _ => {} + } + } + } Ok(imported) } @@ -167,12 +187,6 @@ pub fn read_live_provider_settings(app: String) -> Result Result { - ProviderService::patch_claude_live(patch).map_err(|e| e.to_string())?; - Ok(true) -} - #[tauri::command] pub async fn test_api_endpoints( urls: Vec, diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index 0b51a62e7..e39aa7b31 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -38,6 +38,31 @@ impl Database { Ok(()) } + // --- 通用配置片段 (Common Config Snippet) --- + + /// 获取通用配置片段 + pub fn get_config_snippet(&self, app_type: &str) -> Result, AppError> { + self.get_setting(&format!("common_config_{app_type}")) + } + + /// 设置通用配置片段 + pub fn set_config_snippet( + &self, + app_type: &str, + snippet: Option, + ) -> Result<(), AppError> { + let key = format!("common_config_{app_type}"); + if let Some(value) = snippet { + self.set_setting(&key, &value) + } else { + // 如果为 None 则删除 + let conn = lock_conn!(self.conn); + conn.execute("DELETE FROM settings WHERE key = ?1", params![key]) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) + } + } + // --- 全局出站代理 --- /// 全局代理 URL 的存储键名 diff --git a/src-tauri/src/database/migration.rs b/src-tauri/src/database/migration.rs index f42431587..3c52e7127 100644 --- a/src-tauri/src/database/migration.rs +++ b/src-tauri/src/database/migration.rs @@ -58,6 +58,9 @@ impl Database { // 4. 迁移 Skills Self::migrate_skills(tx, config)?; + // 5. 迁移 Common Config + Self::migrate_common_config(tx, config)?; + Ok(()) } @@ -209,4 +212,34 @@ impl Database { Ok(()) } + + /// 迁移通用配置片段 + fn migrate_common_config( + tx: &rusqlite::Transaction<'_>, + config: &MultiAppConfig, + ) -> Result<(), AppError> { + if let Some(snippet) = &config.common_config_snippets.claude { + tx.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", + params!["common_config_claude", snippet], + ) + .map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?; + } + if let Some(snippet) = &config.common_config_snippets.codex { + tx.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", + params!["common_config_codex", snippet], + ) + .map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?; + } + if let Some(snippet) = &config.common_config_snippets.gemini { + tx.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", + params!["common_config_gemini", snippet], + ) + .map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?; + } + + Ok(()) + } } diff --git a/src-tauri/src/database/tests.rs b/src-tauri/src/database/tests.rs index e76dd7d6b..8e37b3d60 100644 --- a/src-tauri/src/database/tests.rs +++ b/src-tauri/src/database/tests.rs @@ -513,6 +513,8 @@ fn schema_dry_run_does_not_write_to_disk() { mcp: Default::default(), prompts: Default::default(), skills: Default::default(), + common_config_snippets: Default::default(), + claude_common_config_snippet: None, }; // Dry-run should succeed without any file I/O errors @@ -561,6 +563,8 @@ fn dry_run_validates_schema_compatibility() { mcp: Default::default(), prompts: Default::default(), skills: Default::default(), + common_config_snippets: Default::default(), + claude_common_config_snippet: None, }; // Dry-run should validate the full migration path diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b0330ec5c..cea4be170 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -823,8 +823,12 @@ pub fn run() { commands::get_skills_migration_result, commands::get_app_config_path, commands::open_app_config_folder, + commands::get_claude_common_config_snippet, + commands::set_claude_common_config_snippet, + commands::get_common_config_snippet, + commands::set_common_config_snippet, + commands::extract_common_config_snippet, commands::read_live_provider_settings, - commands::patch_claude_live_settings, commands::get_settings, commands::save_settings, commands::get_rectifier_config, diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 0581e7461..cbeee21d0 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -235,11 +235,6 @@ pub struct ProviderMeta { /// - "openai_chat": OpenAI Chat Completions 格式,需要转换 #[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")] pub api_format: Option, - /// Claude 认证字段名(仅 Claude 供应商使用) - /// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商 - /// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key - #[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")] - pub api_key_field: Option, } impl ProviderManager { diff --git a/src-tauri/src/services/config.rs b/src-tauri/src/services/config.rs index d2c8da0c5..d354cccb1 100644 --- a/src-tauri/src/services/config.rs +++ b/src-tauri/src/services/config.rs @@ -1,5 +1,9 @@ +use super::provider::{sanitize_claude_settings_for_live, ProviderService}; +use crate::app_config::{AppType, MultiAppConfig}; use crate::error::AppError; +use crate::provider::Provider; use chrono::Utc; +use serde_json::Value; use std::fs; use std::path::Path; @@ -78,4 +82,150 @@ impl ConfigService { Ok(()) } + + /// 同步当前供应商到对应的 live 配置。 + pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> { + Self::sync_current_provider_for_app(config, &AppType::Claude)?; + Self::sync_current_provider_for_app(config, &AppType::Codex)?; + Self::sync_current_provider_for_app(config, &AppType::Gemini)?; + Ok(()) + } + + fn sync_current_provider_for_app( + config: &mut MultiAppConfig, + app_type: &AppType, + ) -> Result<(), AppError> { + let (current_id, provider) = { + let manager = match config.get_manager(app_type) { + Some(manager) => manager, + None => return Ok(()), + }; + + if manager.current.is_empty() { + return Ok(()); + } + + let current_id = manager.current.clone(); + let provider = match manager.providers.get(¤t_id) { + Some(provider) => provider.clone(), + None => { + log::warn!( + "当前应用 {app_type:?} 的供应商 {current_id} 不存在,跳过 live 同步" + ); + return Ok(()); + } + }; + (current_id, provider) + }; + + match app_type { + AppType::Codex => Self::sync_codex_live(config, ¤t_id, &provider)?, + AppType::Claude => Self::sync_claude_live(config, ¤t_id, &provider)?, + AppType::Gemini => Self::sync_gemini_live(config, ¤t_id, &provider)?, + AppType::OpenCode => { + // OpenCode uses additive mode, no live sync needed + // OpenCode providers are managed directly in the config file + } + AppType::OpenClaw => { + // OpenClaw uses additive mode, no live sync needed + // OpenClaw providers are managed directly in the config file + } + } + + Ok(()) + } + + fn sync_codex_live( + config: &mut MultiAppConfig, + provider_id: &str, + provider: &Provider, + ) -> Result<(), AppError> { + let settings = provider.settings_config.as_object().ok_or_else(|| { + AppError::Config(format!("供应商 {provider_id} 的 Codex 配置必须是对象")) + })?; + let auth = settings.get("auth").ok_or_else(|| { + AppError::Config(format!("供应商 {provider_id} 的 Codex 配置缺少 auth 字段")) + })?; + if !auth.is_object() { + return Err(AppError::Config(format!( + "供应商 {provider_id} 的 Codex auth 配置必须是 JSON 对象" + ))); + } + let cfg_text = settings.get("config").and_then(Value::as_str); + + crate::codex_config::write_codex_live_atomic(auth, cfg_text)?; + // 注意:MCP 同步在 v3.7.0 中已通过 McpService 进行,不再在此调用 + // sync_enabled_to_codex 使用旧的 config.mcp.codex 结构,在新架构中为空 + // MCP 的启用/禁用应通过 McpService::toggle_app 进行 + + let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?; + if let Some(manager) = config.get_manager_mut(&AppType::Codex) { + if let Some(target) = manager.providers.get_mut(provider_id) { + if let Some(obj) = target.settings_config.as_object_mut() { + obj.insert( + "config".to_string(), + serde_json::Value::String(cfg_text_after), + ); + } + } + } + + Ok(()) + } + + fn sync_claude_live( + config: &mut MultiAppConfig, + provider_id: &str, + provider: &Provider, + ) -> Result<(), AppError> { + use crate::config::{read_json_file, write_json_file}; + + let settings_path = crate::config::get_claude_settings_path(); + if let Some(parent) = settings_path.parent() { + fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; + } + + let settings = sanitize_claude_settings_for_live(&provider.settings_config); + write_json_file(&settings_path, &settings)?; + + let live_after = read_json_file::(&settings_path)?; + if let Some(manager) = config.get_manager_mut(&AppType::Claude) { + if let Some(target) = manager.providers.get_mut(provider_id) { + target.settings_config = live_after; + } + } + + Ok(()) + } + + fn sync_gemini_live( + config: &mut MultiAppConfig, + provider_id: &str, + provider: &Provider, + ) -> Result<(), AppError> { + use crate::gemini_config::{env_to_json, read_gemini_env}; + + ProviderService::write_gemini_live(provider)?; + + // 读回实际写入的内容并更新到配置中(包含 settings.json) + let live_after_env = read_gemini_env()?; + let settings_path = crate::gemini_config::get_gemini_settings_path(); + let live_after_config = if settings_path.exists() { + crate::config::read_json_file(&settings_path)? + } else { + serde_json::json!({}) + }; + let mut live_after = env_to_json(&live_after_env); + if let Some(obj) = live_after.as_object_mut() { + obj.insert("config".to_string(), live_after_config); + } + + if let Some(manager) = config.get_manager_mut(&AppType::Gemini) { + if let Some(target) = manager.providers.get_mut(provider_id) { + target.settings_config = live_after; + } + } + + Ok(()) + } } diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 0f815c963..da4e15fa6 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -237,439 +237,6 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re Ok(()) } -// ============================================================================ -// Key fields definitions for partial merge -// ============================================================================ - -/// Claude env-level key fields that belong to the provider. -/// When adding a new field here, also update backfill_claude_key_fields(). -const CLAUDE_KEY_ENV_FIELDS: &[&str] = &[ - // --- API auth & endpoint --- - "ANTHROPIC_BASE_URL", - "ANTHROPIC_AUTH_TOKEN", - "ANTHROPIC_API_KEY", - // --- Model selection --- - "ANTHROPIC_MODEL", - "ANTHROPIC_REASONING_MODEL", - "ANTHROPIC_SMALL_FAST_MODEL", - "ANTHROPIC_DEFAULT_HAIKU_MODEL", - "ANTHROPIC_DEFAULT_SONNET_MODEL", - "ANTHROPIC_DEFAULT_OPUS_MODEL", - "CLAUDE_CODE_SUBAGENT_MODEL", - // --- AWS Bedrock --- - "CLAUDE_CODE_USE_BEDROCK", - "AWS_ACCESS_KEY_ID", - "AWS_SECRET_ACCESS_KEY", - "AWS_SESSION_TOKEN", - "AWS_REGION", - "AWS_PROFILE", - "ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION", - // --- Google Vertex AI --- - "CLAUDE_CODE_USE_VERTEX", - "ANTHROPIC_VERTEX_PROJECT_ID", - "CLOUD_ML_REGION", - // --- Microsoft Foundry --- - "CLAUDE_CODE_USE_FOUNDRY", - // --- Provider behavior --- - "CLAUDE_CODE_MAX_OUTPUT_TOKENS", - "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", - "API_TIMEOUT_MS", - "DISABLE_PROMPT_CACHING", -]; - -/// Claude top-level key fields (legacy + modern format). -/// When adding a new field here, also update backfill_claude_key_fields(). -const CLAUDE_KEY_TOP_LEVEL: &[&str] = &[ - "apiBaseUrl", // legacy - "primaryModel", // legacy - "smallFastModel", // legacy - "model", // modern - "apiKey", // Bedrock API Key auth -]; - -/// Codex TOML key fields. -/// When adding a new field here, also update backfill_codex_key_fields(). -const CODEX_KEY_TOP_LEVEL: &[&str] = &[ - "model_provider", - "model", - "model_reasoning_effort", - "review_model", - "plan_mode_reasoning_effort", -]; - -/// Gemini env-level key fields. -/// When adding a new field here, also update backfill_gemini_key_fields(). -const GEMINI_KEY_ENV_FIELDS: &[&str] = &[ - "GOOGLE_GEMINI_BASE_URL", - "GEMINI_API_KEY", - "GEMINI_MODEL", - "GOOGLE_API_KEY", -]; - -// ============================================================================ -// Partial merge: write only key fields to live config -// ============================================================================ - -/// Write only provider-specific key fields to live configuration, -/// preserving all other user settings in the live file. -/// -/// Used for switch-mode apps (Claude, Codex, Gemini) during: -/// - `switch_normal()` — switching providers -/// - `sync_current_to_live()` — startup sync -/// - `add()` / `update()` when the provider is current -pub(crate) fn write_live_partial(app_type: &AppType, provider: &Provider) -> Result<(), AppError> { - match app_type { - AppType::Claude => write_claude_live_partial(provider), - AppType::Codex => write_codex_live_partial(provider), - AppType::Gemini => write_gemini_live_partial(provider), - // Additive mode apps still use full snapshot - AppType::OpenCode | AppType::OpenClaw => write_live_snapshot(app_type, provider), - } -} - -/// Apply a JSON merge patch (RFC 7396) directly to Claude live settings.json. -/// Used for user-level preferences (attribution, thinking, etc.) that are -/// independent of the active provider. -pub fn patch_claude_live(patch: Value) -> Result<(), AppError> { - let path = get_claude_settings_path(); - let mut live = if path.exists() { - read_json_file(&path).unwrap_or_else(|_| json!({})) - } else { - json!({}) - }; - json_merge_patch(&mut live, &patch); - let settings = sanitize_claude_settings_for_live(&live); - write_json_file(&path, &settings)?; - Ok(()) -} - -/// RFC 7396 JSON Merge Patch: null deletes, objects merge recursively, rest overwrites. -fn json_merge_patch(target: &mut Value, patch: &Value) { - if let Some(patch_obj) = patch.as_object() { - if !target.is_object() { - *target = json!({}); - } - let target_obj = target.as_object_mut().unwrap(); - for (key, value) in patch_obj { - if value.is_null() { - target_obj.remove(key); - } else if value.is_object() { - let entry = target_obj.entry(key.clone()).or_insert(json!({})); - json_merge_patch(entry, value); - // Clean up empty container objects - if entry.as_object().is_some_and(|o| o.is_empty()) { - target_obj.remove(key); - } - } else { - target_obj.insert(key.clone(), value.clone()); - } - } - } -} - -/// Claude: merge only key env and top-level fields into live settings.json -fn write_claude_live_partial(provider: &Provider) -> Result<(), AppError> { - let path = get_claude_settings_path(); - - // 1. Read existing live config (start from empty if file doesn't exist) - let mut live = if path.exists() { - read_json_file(&path).unwrap_or_else(|_| json!({})) - } else { - json!({}) - }; - - // 2. Ensure live.env exists as an object - if !live.get("env").is_some_and(|v| v.is_object()) { - live.as_object_mut() - .unwrap() - .insert("env".into(), json!({})); - } - - // 3. Clear key env fields from live, then write from provider - let live_env = live.get_mut("env").unwrap().as_object_mut().unwrap(); - for key in CLAUDE_KEY_ENV_FIELDS { - live_env.remove(*key); - } - - if let Some(provider_env) = provider - .settings_config - .get("env") - .and_then(|v| v.as_object()) - { - for key in CLAUDE_KEY_ENV_FIELDS { - if let Some(value) = provider_env.get(*key) { - live_env.insert(key.to_string(), value.clone()); - } - } - } - - // 4. Handle top-level legacy key fields - let live_obj = live.as_object_mut().unwrap(); - for key in CLAUDE_KEY_TOP_LEVEL { - live_obj.remove(*key); - } - if let Some(provider_obj) = provider.settings_config.as_object() { - for key in CLAUDE_KEY_TOP_LEVEL { - if let Some(value) = provider_obj.get(*key) { - live_obj.insert(key.to_string(), value.clone()); - } - } - } - - // 5. Sanitize and write - let settings = sanitize_claude_settings_for_live(&live); - write_json_file(&path, &settings)?; - Ok(()) -} - -/// Codex: replace auth.json entirely, partially merge config.toml key fields -fn write_codex_live_partial(provider: &Provider) -> Result<(), AppError> { - let obj = provider - .settings_config - .as_object() - .ok_or_else(|| AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string()))?; - - // auth.json is entirely provider-specific, replace it wholesale - let auth = obj - .get("auth") - .ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?; - - let provider_config_str = obj.get("config").and_then(|v| v.as_str()).unwrap_or(""); - - // Read existing config.toml (or start from empty) - let config_path = get_codex_config_path(); - let existing_toml = if config_path.exists() { - std::fs::read_to_string(&config_path).unwrap_or_default() - } else { - String::new() - }; - - // Parse both existing and provider TOML - let mut live_doc = existing_toml - .parse::() - .unwrap_or_else(|_| toml_edit::DocumentMut::new()); - - // Remove key fields from live doc - let live_root = live_doc.as_table_mut(); - for key in CODEX_KEY_TOP_LEVEL { - live_root.remove(key); - } - live_root.remove("model_providers"); - - // Parse provider TOML and extract key fields - if !provider_config_str.is_empty() { - if let Ok(provider_doc) = provider_config_str.parse::() { - let provider_root = provider_doc.as_table(); - - // Copy key top-level fields from provider - for key in CODEX_KEY_TOP_LEVEL { - if let Some(item) = provider_root.get(key) { - live_root.insert(key, item.clone()); - } - } - - // Copy model_providers table from provider - if let Some(mp) = provider_root.get("model_providers") { - live_root.insert("model_providers", mp.clone()); - } - } - } - - // Write using atomic write - crate::codex_config::write_codex_live_atomic(auth, Some(&live_doc.to_string()))?; - Ok(()) -} - -/// Gemini: merge only key env fields, preserve settings.json (MCP etc.) -fn write_gemini_live_partial(provider: &Provider) -> Result<(), AppError> { - use crate::gemini_config::{get_gemini_env_path, read_gemini_env, write_gemini_env_atomic}; - - let auth_type = detect_gemini_auth_type(provider); - - // 1. Read existing env from live .env file - let mut env_map = if get_gemini_env_path().exists() { - read_gemini_env().unwrap_or_default() - } else { - HashMap::new() - }; - - // 2. Remove key fields from existing env - for key in GEMINI_KEY_ENV_FIELDS { - env_map.remove(*key); - } - - // 3. Extract key fields from provider and merge - if let Some(provider_env) = provider - .settings_config - .get("env") - .and_then(|v| v.as_object()) - { - for key in GEMINI_KEY_ENV_FIELDS { - if let Some(value) = provider_env.get(*key).and_then(|v| v.as_str()) { - if !value.is_empty() { - env_map.insert(key.to_string(), value.to_string()); - } - } - } - } - - // 4. Handle auth type specific behavior - match auth_type { - GeminiAuthType::GoogleOfficial => { - // Google official uses OAuth, clear all env - env_map.clear(); - write_gemini_env_atomic(&env_map)?; - } - GeminiAuthType::Packycode | GeminiAuthType::Generic => { - // Validate and write env - crate::gemini_config::validate_gemini_settings_strict(&provider.settings_config)?; - write_gemini_env_atomic(&env_map)?; - } - } - - // 5. Handle settings.json (same as write_gemini_live — preserve existing MCP etc.) - use crate::gemini_config::get_gemini_settings_path; - let settings_path = get_gemini_settings_path(); - - if let Some(config_value) = provider.settings_config.get("config") { - if config_value.is_object() { - let mut merged = if settings_path.exists() { - read_json_file::(&settings_path).unwrap_or_else(|_| json!({})) - } else { - json!({}) - }; - if let (Some(merged_obj), Some(config_obj)) = - (merged.as_object_mut(), config_value.as_object()) - { - for (k, v) in config_obj { - merged_obj.insert(k.clone(), v.clone()); - } - } - write_json_file(&settings_path, &merged)?; - } else if !config_value.is_null() { - return Err(AppError::localized( - "gemini.validation.invalid_config", - "Gemini 配置格式错误: config 必须是对象或 null", - "Gemini config invalid: config must be an object or null", - )); - } - } - - // 6. Set security flag based on auth type - match auth_type { - GeminiAuthType::GoogleOfficial => ensure_google_oauth_security_flag(provider)?, - GeminiAuthType::Packycode | GeminiAuthType::Generic => { - crate::gemini_config::write_packycode_settings()?; - } - } - - Ok(()) -} - -// ============================================================================ -// Backfill: extract only key fields from live config -// ============================================================================ - -/// Extract only provider-specific key fields from a live config value. -/// -/// Used during backfill to ensure the provider's `settings_config` converges -/// to containing only key fields over time. -pub(crate) fn backfill_key_fields(app_type: &AppType, live_config: &Value) -> Value { - match app_type { - AppType::Claude => backfill_claude_key_fields(live_config), - AppType::Codex => backfill_codex_key_fields(live_config), - AppType::Gemini => backfill_gemini_key_fields(live_config), - // Additive mode: return full config (no backfill needed) - _ => live_config.clone(), - } -} - -fn backfill_claude_key_fields(live: &Value) -> Value { - let mut result = json!({}); - let result_obj = result.as_object_mut().unwrap(); - - // Extract key env fields - if let Some(live_env) = live.get("env").and_then(|v| v.as_object()) { - let mut env_obj = serde_json::Map::new(); - for key in CLAUDE_KEY_ENV_FIELDS { - if let Some(value) = live_env.get(*key) { - env_obj.insert(key.to_string(), value.clone()); - } - } - if !env_obj.is_empty() { - result_obj.insert("env".to_string(), Value::Object(env_obj)); - } - } - - // Extract key top-level fields - if let Some(live_obj) = live.as_object() { - for key in CLAUDE_KEY_TOP_LEVEL { - if let Some(value) = live_obj.get(*key) { - result_obj.insert(key.to_string(), value.clone()); - } - } - } - - result -} - -fn backfill_codex_key_fields(live: &Value) -> Value { - let mut result = json!({}); - let result_obj = result.as_object_mut().unwrap(); - - // auth is entirely provider-specific — keep it as-is - if let Some(auth) = live.get("auth") { - result_obj.insert("auth".to_string(), auth.clone()); - } - - // Extract key TOML fields from config string - if let Some(config_str) = live.get("config").and_then(|v| v.as_str()) { - if let Ok(doc) = config_str.parse::() { - let mut new_doc = toml_edit::DocumentMut::new(); - let new_root = new_doc.as_table_mut(); - - // Copy key top-level fields - for key in CODEX_KEY_TOP_LEVEL { - if let Some(item) = doc.as_table().get(key) { - new_root.insert(key, item.clone()); - } - } - - // Copy model_providers table - if let Some(mp) = doc.as_table().get("model_providers") { - new_root.insert("model_providers", mp.clone()); - } - - let toml_str = new_doc.to_string(); - if !toml_str.trim().is_empty() { - result_obj.insert("config".to_string(), Value::String(toml_str)); - } - } - } - - result -} - -fn backfill_gemini_key_fields(live: &Value) -> Value { - let mut result = json!({}); - let result_obj = result.as_object_mut().unwrap(); - - // Extract key env fields - if let Some(live_env) = live.get("env").and_then(|v| v.as_object()) { - let mut env_obj = serde_json::Map::new(); - for key in GEMINI_KEY_ENV_FIELDS { - if let Some(value) = live_env.get(*key) { - env_obj.insert(key.to_string(), value.clone()); - } - } - if !env_obj.is_empty() { - result_obj.insert("env".to_string(), Value::Object(env_obj)); - } - } - - result -} - /// Sync all providers to live configuration (for additive mode apps) /// /// Writes all providers from the database to the live configuration file. @@ -719,7 +286,7 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> { let providers = state.db.get_all_providers(app_type.as_str())?; if let Some(provider) = providers.get(¤t_id) { - write_live_partial(&app_type, provider)?; + write_live_snapshot(&app_type, provider)?; } // Note: get_effective_current_provider already validates existence, // so providers.get() should always succeed here diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 59f4b8536..d3931f82d 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -27,12 +27,11 @@ pub use live::{ // Internal re-exports (pub(crate)) pub(crate) use live::sanitize_claude_settings_for_live; -pub(crate) use live::write_live_partial; +pub(crate) use live::write_live_snapshot; // Internal re-exports use live::{ - backfill_key_fields, remove_openclaw_provider_from_live, remove_opencode_provider_from_live, - write_live_snapshot, + remove_openclaw_provider_from_live, remove_opencode_provider_from_live, write_gemini_live, }; use usage::validate_usage_script; @@ -85,6 +84,47 @@ mod tests { assert_eq!(api_key, "token"); assert_eq!(base_url, "https://claude.example"); } + + #[test] + fn extract_codex_common_config_preserves_mcp_servers_base_url() { + let config_toml = r#"model_provider = "azure" +model = "gpt-4" +disable_response_storage = true + +[model_providers.azure] +name = "Azure OpenAI" +base_url = "https://azure.example/v1" +wire_api = "responses" + +[mcp_servers.my_server] +base_url = "http://localhost:8080" +"#; + + let settings = json!({ "config": config_toml }); + let extracted = ProviderService::extract_codex_common_config(&settings) + .expect("extract_codex_common_config should succeed"); + + assert!( + !extracted + .lines() + .any(|line| line.trim_start().starts_with("model_provider")), + "should remove top-level model_provider" + ); + assert!( + !extracted + .lines() + .any(|line| line.trim_start().starts_with("model =")), + "should remove top-level model" + ); + assert!( + !extracted.contains("[model_providers"), + "should remove entire model_providers table" + ); + assert!( + extracted.contains("http://localhost:8080"), + "should keep mcp_servers.* base_url" + ); + } } impl ProviderService { @@ -152,7 +192,7 @@ impl ProviderService { state .db .set_current_provider(app_type.as_str(), &provider.id)?; - write_live_partial(&app_type, &provider)?; + write_live_snapshot(&app_type, &provider)?; } Ok(true) @@ -233,7 +273,7 @@ impl ProviderService { ) .map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?; } else { - write_live_partial(&app_type, &provider)?; + write_live_snapshot(&app_type, &provider)?; // Sync MCP McpService::sync_all_enabled(state)?; } @@ -520,9 +560,7 @@ impl ProviderService { // Only backfill when switching to a different provider if let Ok(live_config) = read_live_settings(app_type.clone()) { if let Some(mut current_provider) = providers.get(¤t_id).cloned() { - // Only extract key fields from live config for backfill - current_provider.settings_config = - backfill_key_fields(&app_type, &live_config); + current_provider.settings_config = live_config; if let Err(e) = state.db.save_provider(app_type.as_str(), ¤t_provider) { @@ -546,8 +584,11 @@ impl ProviderService { state.db.set_current_provider(app_type.as_str(), id)?; } - // Sync to live (partial merge: only key fields, preserving user settings) - write_live_partial(&app_type, provider)?; + // Sync to live (write_gemini_live handles security flag internally for Gemini) + write_live_snapshot(&app_type, provider)?; + + // Sync MCP + McpService::sync_all_enabled(state)?; Ok(result) } @@ -557,6 +598,222 @@ impl ProviderService { sync_current_to_live(state) } + /// Extract common config snippet from current provider + /// + /// Extracts the current provider's configuration and removes provider-specific fields + /// (API keys, model settings, endpoints) to create a reusable common config snippet. + pub fn extract_common_config_snippet( + state: &AppState, + app_type: AppType, + ) -> Result { + // Get current provider + let current_id = Self::current(state, app_type.clone())?; + if current_id.is_empty() { + return Err(AppError::Message("No current provider".to_string())); + } + + let providers = state.db.get_all_providers(app_type.as_str())?; + let provider = providers + .get(¤t_id) + .ok_or_else(|| AppError::Message(format!("Provider {current_id} not found")))?; + + match app_type { + AppType::Claude => Self::extract_claude_common_config(&provider.settings_config), + AppType::Codex => Self::extract_codex_common_config(&provider.settings_config), + AppType::Gemini => Self::extract_gemini_common_config(&provider.settings_config), + AppType::OpenCode => Self::extract_opencode_common_config(&provider.settings_config), + AppType::OpenClaw => Self::extract_openclaw_common_config(&provider.settings_config), + } + } + + /// Extract common config snippet from a config value (e.g. editor content). + pub fn extract_common_config_snippet_from_settings( + app_type: AppType, + settings_config: &Value, + ) -> Result { + match app_type { + AppType::Claude => Self::extract_claude_common_config(settings_config), + AppType::Codex => Self::extract_codex_common_config(settings_config), + AppType::Gemini => Self::extract_gemini_common_config(settings_config), + AppType::OpenCode => Self::extract_opencode_common_config(settings_config), + AppType::OpenClaw => Self::extract_openclaw_common_config(settings_config), + } + } + + /// Extract common config for Claude (JSON format) + fn extract_claude_common_config(settings: &Value) -> Result { + let mut config = settings.clone(); + + // Fields to exclude from common config + const ENV_EXCLUDES: &[&str] = &[ + // Auth + "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", + // Models (5 fields) + "ANTHROPIC_MODEL", + "ANTHROPIC_REASONING_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + // Endpoint + "ANTHROPIC_BASE_URL", + ]; + + const TOP_LEVEL_EXCLUDES: &[&str] = &[ + "apiBaseUrl", + // Legacy model fields + "primaryModel", + "smallFastModel", + ]; + + // Remove env fields + if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) { + for key in ENV_EXCLUDES { + env.remove(*key); + } + // If env is empty after removal, remove the env object itself + if env.is_empty() { + config.as_object_mut().map(|obj| obj.remove("env")); + } + } + + // Remove top-level fields + if let Some(obj) = config.as_object_mut() { + for key in TOP_LEVEL_EXCLUDES { + obj.remove(*key); + } + } + + // Check if result is empty + if config.as_object().is_none_or(|obj| obj.is_empty()) { + return Ok("{}".to_string()); + } + + serde_json::to_string_pretty(&config) + .map_err(|e| AppError::Message(format!("Serialization failed: {e}"))) + } + + /// Extract common config for Codex (TOML format) + fn extract_codex_common_config(settings: &Value) -> Result { + // Codex config is stored as { "auth": {...}, "config": "toml string" } + let config_toml = settings + .get("config") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if config_toml.is_empty() { + return Ok(String::new()); + } + + let mut doc = config_toml + .parse::() + .map_err(|e| AppError::Message(format!("TOML parse error: {e}")))?; + + // Remove provider-specific fields. + let root = doc.as_table_mut(); + root.remove("model"); + root.remove("model_provider"); + // Legacy/alt formats might use a top-level base_url. + root.remove("base_url"); + + // Remove entire model_providers table (provider-specific configuration) + root.remove("model_providers"); + + // Clean up multiple empty lines (keep at most one blank line). + let mut cleaned = String::new(); + let mut blank_run = 0usize; + for line in doc.to_string().lines() { + if line.trim().is_empty() { + blank_run += 1; + if blank_run <= 1 { + cleaned.push('\n'); + } + continue; + } + blank_run = 0; + cleaned.push_str(line); + cleaned.push('\n'); + } + + Ok(cleaned.trim().to_string()) + } + + /// Extract common config for Gemini (JSON format) + /// + /// Extracts `.env` values while excluding provider-specific credentials: + /// - GOOGLE_GEMINI_BASE_URL + /// - GEMINI_API_KEY + fn extract_gemini_common_config(settings: &Value) -> Result { + let env = settings.get("env").and_then(|v| v.as_object()); + + let mut snippet = serde_json::Map::new(); + if let Some(env) = env { + for (key, value) in env { + if key == "GOOGLE_GEMINI_BASE_URL" || key == "GEMINI_API_KEY" { + continue; + } + let Value::String(v) = value else { + continue; + }; + let trimmed = v.trim(); + if !trimmed.is_empty() { + snippet.insert(key.to_string(), Value::String(trimmed.to_string())); + } + } + } + + if snippet.is_empty() { + return Ok("{}".to_string()); + } + + serde_json::to_string_pretty(&Value::Object(snippet)) + .map_err(|e| AppError::Message(format!("Serialization failed: {e}"))) + } + + /// Extract common config for OpenCode (JSON format) + fn extract_opencode_common_config(settings: &Value) -> Result { + // OpenCode uses a different config structure with npm, options, models + // For common config, we exclude provider-specific fields like apiKey + let mut config = settings.clone(); + + // Remove provider-specific fields + if let Some(obj) = config.as_object_mut() { + if let Some(options) = obj.get_mut("options").and_then(|v| v.as_object_mut()) { + options.remove("apiKey"); + options.remove("baseURL"); + } + // Keep npm and models as they might be common + } + + if config.is_null() || (config.is_object() && config.as_object().unwrap().is_empty()) { + return Ok("{}".to_string()); + } + + serde_json::to_string_pretty(&config) + .map_err(|e| AppError::Message(format!("Serialization failed: {e}"))) + } + + /// Extract common config for OpenClaw (JSON format) + fn extract_openclaw_common_config(settings: &Value) -> Result { + // OpenClaw uses a different config structure with baseUrl, apiKey, api, models + // For common config, we exclude provider-specific fields like apiKey + let mut config = settings.clone(); + + // Remove provider-specific fields + if let Some(obj) = config.as_object_mut() { + obj.remove("apiKey"); + obj.remove("baseUrl"); + // Keep api and models as they might be common + } + + if config.is_null() || (config.is_object() && config.as_object().unwrap().is_empty()) { + return Ok("{}".to_string()); + } + + serde_json::to_string_pretty(&config) + .map_err(|e| AppError::Message(format!("Serialization failed: {e}"))) + } + /// Import default configuration from live files (re-export) /// /// Returns `Ok(true)` if imported, `Ok(false)` if skipped. @@ -569,11 +826,6 @@ impl ProviderService { read_live_settings(app_type) } - /// Patch Claude live settings directly (user-level preferences) - pub fn patch_claude_live(patch: Value) -> Result<(), AppError> { - live::patch_claude_live(patch) - } - /// Get custom endpoints list (re-export) pub fn get_custom_endpoints( state: &AppState, @@ -669,6 +921,10 @@ impl ProviderService { .await } + pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> { + write_gemini_live(provider) + } + fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> { match app_type { AppType::Claude => { diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 3dc0412dd..5325f4af1 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -8,7 +8,7 @@ use crate::database::Database; use crate::provider::Provider; use crate::proxy::server::ProxyServer; use crate::proxy::types::*; -use crate::services::provider::write_live_partial; +use crate::services::provider::write_live_snapshot; use serde_json::{json, Value}; use std::str::FromStr; use std::sync::Arc; @@ -1266,7 +1266,7 @@ impl ProxyService { return Ok(false); }; - write_live_partial(app_type, provider) + write_live_snapshot(app_type, provider) .map_err(|e| format!("写入 {app_type:?} Live 配置失败: {e}"))?; Ok(true) diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index c9738c7f8..24a1667fb 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -2,7 +2,10 @@ use serde_json::json; use std::fs; use std::path::PathBuf; -use cc_switch_lib::{AppError, AppType, ConfigService, MultiAppConfig, Provider}; +use cc_switch_lib::{ + get_claude_settings_path, read_json_file, AppError, AppType, ConfigService, MultiAppConfig, + Provider, ProviderMeta, +}; #[path = "support.rs"] mod support; @@ -10,6 +13,132 @@ use support::{ create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex, }; +#[test] +fn sync_claude_provider_writes_live_settings() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + let mut config = MultiAppConfig::default(); + let provider_config = json!({ + "env": { + "ANTHROPIC_AUTH_TOKEN": "test-key", + "ANTHROPIC_BASE_URL": "https://api.test" + }, + "ui": { + "displayName": "Test Provider" + } + }); + + let provider = Provider::with_id( + "prov-1".to_string(), + "Test Claude".to_string(), + provider_config.clone(), + None, + ); + + let manager = config + .get_manager_mut(&AppType::Claude) + .expect("claude manager"); + manager.providers.insert("prov-1".to_string(), provider); + manager.current = "prov-1".to_string(); + + ConfigService::sync_current_providers_to_live(&mut config).expect("sync live settings"); + + let settings_path = get_claude_settings_path(); + assert!( + settings_path.exists(), + "live settings should be written to {}", + settings_path.display() + ); + + let live_value: serde_json::Value = read_json_file(&settings_path).expect("read live file"); + assert_eq!(live_value, provider_config); + + // 确认 SSOT 中的供应商也同步了最新内容 + let updated = config + .get_manager(&AppType::Claude) + .and_then(|m| m.providers.get("prov-1")) + .expect("provider in config"); + assert_eq!(updated.settings_config, provider_config); + + // 额外确认写入位置位于测试 HOME 下 + assert!( + settings_path.starts_with(home), + "settings path {settings_path:?} should reside under test HOME {home:?}" + ); +} + +#[test] +fn sync_codex_provider_writes_auth_and_config() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + + let mut config = MultiAppConfig::default(); + + // 注意:v3.7.0 后 MCP 同步由 McpService 独立处理,不再通过 provider 切换触发 + // 此测试仅验证 auth.json 和 config.toml 基础配置的写入 + + let provider_config = json!({ + "auth": { + "OPENAI_API_KEY": "codex-key" + }, + "config": r#"base_url = "https://codex.test""# + }); + + let provider = Provider::with_id( + "codex-1".to_string(), + "Codex Test".to_string(), + provider_config.clone(), + None, + ); + + let manager = config + .get_manager_mut(&AppType::Codex) + .expect("codex manager"); + manager.providers.insert("codex-1".to_string(), provider); + manager.current = "codex-1".to_string(); + + ConfigService::sync_current_providers_to_live(&mut config).expect("sync codex live"); + + let auth_path = cc_switch_lib::get_codex_auth_path(); + let config_path = cc_switch_lib::get_codex_config_path(); + + assert!( + auth_path.exists(), + "auth.json should exist at {}", + auth_path.display() + ); + assert!( + config_path.exists(), + "config.toml should exist at {}", + config_path.display() + ); + + let auth_value: serde_json::Value = read_json_file(&auth_path).expect("read auth"); + assert_eq!( + auth_value, + provider_config.get("auth").cloned().expect("auth object") + ); + + let toml_text = fs::read_to_string(&config_path).expect("read config.toml"); + // 验证基础配置正确写入 + assert!( + toml_text.contains("base_url"), + "config.toml should contain base_url from provider config" + ); + + // 当前供应商应同步最新 config 文本 + let manager = config.get_manager(&AppType::Codex).expect("codex manager"); + let synced = manager.providers.get("codex-1").expect("codex provider"); + let synced_cfg = synced + .settings_config + .get("config") + .and_then(|v| v.as_str()) + .expect("config string"); + assert_eq!(synced_cfg, toml_text); +} + #[test] fn sync_enabled_to_codex_writes_enabled_servers() { let _guard = test_mutex().lock().expect("acquire test mutex"); @@ -209,6 +338,46 @@ fn sync_enabled_to_codex_returns_error_on_invalid_toml() { } } +#[test] +fn sync_codex_provider_missing_auth_returns_error() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + + let mut config = MultiAppConfig::default(); + let provider = Provider::with_id( + "codex-missing-auth".to_string(), + "No Auth".to_string(), + json!({ + "config": "model = \"test\"" + }), + None, + ); + let manager = config + .get_manager_mut(&AppType::Codex) + .expect("codex manager"); + manager.providers.insert(provider.id.clone(), provider); + manager.current = "codex-missing-auth".to_string(); + + let err = ConfigService::sync_current_providers_to_live(&mut config) + .expect_err("sync should fail when auth missing"); + match err { + cc_switch_lib::AppError::Config(msg) => { + assert!(msg.contains("auth"), "error message should mention auth"); + } + other => panic!("unexpected error variant: {other:?}"), + } + + // 确认未产生任何 live 配置文件 + assert!( + !cc_switch_lib::get_codex_auth_path().exists(), + "auth.json should not be created on failure" + ); + assert!( + !cc_switch_lib::get_codex_config_path().exists(), + "config.toml should not be created on failure" + ); +} + #[test] fn write_codex_live_atomic_persists_auth_and_config() { let _guard = test_mutex().lock().expect("acquire test mutex"); @@ -647,6 +816,107 @@ fn create_backup_retains_only_latest_entries() { ); } +#[test] +fn sync_gemini_packycode_sets_security_selected_type() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + let mut config = MultiAppConfig::default(); + { + let manager = config + .get_manager_mut(&AppType::Gemini) + .expect("gemini manager"); + manager.current = "packy-1".to_string(); + manager.providers.insert( + "packy-1".to_string(), + Provider::with_id( + "packy-1".to_string(), + "PackyCode".to_string(), + json!({ + "env": { + "GEMINI_API_KEY": "pk-key", + "GOOGLE_GEMINI_BASE_URL": "https://api-slb.packyapi.com" + } + }), + Some("https://www.packyapi.com".to_string()), + ), + ); + } + + ConfigService::sync_current_providers_to_live(&mut config) + .expect("syncing gemini live should succeed"); + + // security field is written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json + let gemini_settings = home.join(".gemini").join("settings.json"); + assert!( + gemini_settings.exists(), + "Gemini settings.json should exist at {}", + gemini_settings.display() + ); + + let raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings.json"); + let value: serde_json::Value = serde_json::from_str(&raw).expect("parse gemini settings.json"); + assert_eq!( + value + .pointer("/security/auth/selectedType") + .and_then(|v| v.as_str()), + Some("gemini-api-key"), + "syncing PackyCode Gemini should enforce security.auth.selectedType in Gemini settings" + ); +} + +#[test] +fn sync_gemini_google_official_sets_oauth_security() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + let mut config = MultiAppConfig::default(); + { + let manager = config + .get_manager_mut(&AppType::Gemini) + .expect("gemini manager"); + manager.current = "google-official".to_string(); + let mut provider = Provider::with_id( + "google-official".to_string(), + "Google".to_string(), + json!({ + "env": {} + }), + Some("https://ai.google.dev".to_string()), + ); + provider.meta = Some(ProviderMeta { + partner_promotion_key: Some("google-official".to_string()), + ..ProviderMeta::default() + }); + manager + .providers + .insert("google-official".to_string(), provider); + } + + ConfigService::sync_current_providers_to_live(&mut config) + .expect("syncing google official gemini should succeed"); + + // security field is written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json + let gemini_settings = home.join(".gemini").join("settings.json"); + assert!( + gemini_settings.exists(), + "Gemini settings should exist at {}", + gemini_settings.display() + ); + let gemini_raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings"); + let gemini_value: serde_json::Value = + serde_json::from_str(&gemini_raw).expect("parse gemini settings json"); + assert_eq!( + gemini_value + .pointer("/security/auth/selectedType") + .and_then(|v| v.as_str()), + Some("oauth-personal"), + "Gemini settings should record oauth-personal for Google Official" + ); +} + #[test] fn export_sql_writes_to_target_path() { let _guard = test_mutex().lock().expect("acquire test mutex"); diff --git a/src-tauri/tests/provider_commands.rs b/src-tauri/tests/provider_commands.rs index 148c00db6..328848926 100644 --- a/src-tauri/tests/provider_commands.rs +++ b/src-tauri/tests/provider_commands.rs @@ -100,12 +100,9 @@ command = "say" ); let config_text = std::fs::read_to_string(get_codex_config_path()).expect("read config.toml"); - // With partial merge, only key fields (model, provider, model_providers) are - // merged into config.toml. The existing MCP section should be preserved. - // MCP sync from DB is handled separately (at startup or explicit sync). assert!( - config_text.contains("mcp_servers.legacy"), - "config.toml should preserve existing MCP servers after partial merge" + config_text.contains("mcp_servers.echo-server"), + "config.toml should contain synced MCP servers" ); let current_id = app_state @@ -129,9 +126,12 @@ command = "say" .get("config") .and_then(|v| v.as_str()) .unwrap_or_default(); - // With partial merge, only key fields (model_provider, model, model_providers) - // are written to the live file. MCP servers are synced separately. - // The provider's stored config should still contain mcp_servers.latest. + // 供应商配置应该包含在 live 文件中 + // 注意:live 文件还会包含 MCP 同步后的内容 + assert!( + config_text.contains("mcp_servers.latest"), + "live file should contain provider's original config" + ); assert!( new_config_text.contains("mcp_servers.latest"), "provider snapshot should contain provider's original config" @@ -268,22 +268,11 @@ fn switch_provider_updates_claude_live_and_state() { let legacy_provider = providers .get("old-provider") .expect("legacy provider still exists"); - // Backfill mechanism: before switching, the live config's key fields are - // backfilled to the current provider. With partial merge, only key fields - // (auth, model, endpoint) are extracted — non-key fields like workspace - // are NOT included in the backfill. + // 回填机制:切换前会将 live 配置回填到当前供应商 + // 这保护了用户在 live 文件中的手动修改 assert_eq!( - legacy_provider - .settings_config - .get("env") - .and_then(|env| env.get("ANTHROPIC_API_KEY")) - .and_then(|key| key.as_str()), - Some("legacy-key"), - "previous provider should be backfilled with live auth key" - ); - assert!( - legacy_provider.settings_config.get("workspace").is_none(), - "backfill should NOT include non-key fields like workspace" + legacy_provider.settings_config, legacy_live, + "previous provider should be backfilled with live config" ); let new_provider = providers.get("new-provider").expect("new provider exists"); diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index ad664253b..6288bddd0 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -112,12 +112,9 @@ command = "say" let config_text = std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config.toml"); - // With partial merge, only key fields (model, provider, model_providers) are - // merged into config.toml. The existing MCP section should be preserved. - // MCP sync from DB is handled separately (at startup or explicit sync). assert!( - config_text.contains("mcp_servers.legacy"), - "config.toml should preserve existing MCP servers after partial merge" + config_text.contains("mcp_servers.echo-server"), + "config.toml should contain synced MCP servers" ); let current_id = state @@ -146,6 +143,11 @@ command = "say" new_config_text.contains("mcp_servers.latest"), "provider config should contain original MCP servers" ); + // live 文件额外包含同步的 MCP 服务器 + assert!( + config_text.contains("mcp_servers.echo-server"), + "live config should include synced MCP servers" + ); let legacy = providers .get("old-provider") @@ -412,19 +414,9 @@ fn provider_service_switch_claude_updates_live_and_state() { let legacy_provider = providers .get("old-provider") .expect("legacy provider still exists"); - // With partial merge backfill, only key fields are extracted from live config assert_eq!( - legacy_provider - .settings_config - .get("env") - .and_then(|env| env.get("ANTHROPIC_API_KEY")) - .and_then(|key| key.as_str()), - Some("legacy-key"), - "previous provider should receive backfilled auth key" - ); - assert!( - legacy_provider.settings_config.get("workspace").is_none(), - "backfill should NOT include non-key fields like workspace" + legacy_provider.settings_config, legacy_live, + "previous provider should receive backfilled live config" ); } diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx index bf7853235..672848134 100644 --- a/src/components/providers/EditProviderDialog.tsx +++ b/src/components/providers/EditProviderDialog.tsx @@ -192,7 +192,6 @@ export function EditProviderDialog({ onCancel={() => onOpenChange(false)} initialData={initialData} showButtons={false} - isCurrent={liveSettings !== null} /> ); diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index f9ac424c3..21149c919 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -10,11 +10,7 @@ import { } from "@/components/ui/select"; import EndpointSpeedTest from "./EndpointSpeedTest"; import { ApiKeySection, EndpointField } from "./shared"; -import type { - ProviderCategory, - ClaudeApiFormat, - ClaudeApiKeyField, -} from "@/types"; +import type { ProviderCategory, ClaudeApiFormat } from "@/types"; import type { TemplateValueConfig } from "@/config/claudeProviderPresets"; interface EndpointCandidate { @@ -72,10 +68,6 @@ interface ClaudeFormFieldsProps { // API Format (for third-party providers that use OpenAI Chat Completions format) apiFormat: ClaudeApiFormat; onApiFormatChange: (format: ClaudeApiFormat) => void; - - // Auth Key Field (ANTHROPIC_AUTH_TOKEN vs ANTHROPIC_API_KEY) - apiKeyField: ClaudeApiKeyField; - onApiKeyFieldChange: (field: ClaudeApiKeyField) => void; } export function ClaudeFormFields({ @@ -110,8 +102,6 @@ export function ClaudeFormFields({ speedTestEndpoints, apiFormat, onApiFormatChange, - apiKeyField, - onApiKeyFieldChange, }: ClaudeFormFieldsProps) { const { t } = useTranslation(); @@ -229,41 +219,6 @@ export function ClaudeFormFields({ )} - {/* 认证字段选择(仅非官方供应商显示) */} - {shouldShowModelSelector && ( -
- - {t("providerForm.authField", { defaultValue: "认证字段" })} - - -

- {t("providerForm.authFieldHint", { - defaultValue: - "大多数第三方供应商使用 Auth Token;少数供应商需要 API Key", - })} -

-
- )} - {/* 模型选择器 */} {shouldShowModelSelector && (
diff --git a/src/components/providers/forms/ClaudeQuickToggles.tsx b/src/components/providers/forms/ClaudeQuickToggles.tsx deleted file mode 100644 index 87a0d2423..000000000 --- a/src/components/providers/forms/ClaudeQuickToggles.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { invoke } from "@tauri-apps/api/core"; -import { Checkbox } from "@/components/ui/checkbox"; - -type ToggleKey = "hideAttribution" | "alwaysThinking" | "enableTeammates"; - -interface ClaudeQuickTogglesProps { - /** Called after a patch is applied to the live file, so the caller can mirror it in the JSON editor. */ - onPatchApplied?: (patch: Record) => void; -} - -const defaultStates: Record = { - hideAttribution: false, - alwaysThinking: false, - enableTeammates: false, -}; - -function deriveStates( - cfg: Record, -): Record { - const env = cfg?.env as Record | undefined; - const attr = cfg?.attribution as Record | undefined; - return { - hideAttribution: attr?.commit === "" && attr?.pr === "", - alwaysThinking: cfg?.alwaysThinkingEnabled === true, - enableTeammates: env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1", - }; -} - -/** Apply RFC 7396 JSON Merge Patch in-place: null = delete, object = recurse, else overwrite. */ -function jsonMergePatch( - target: Record, - patch: Record, -) { - for (const [key, value] of Object.entries(patch)) { - if (value === null || value === undefined) { - delete target[key]; - } else if (typeof value === "object" && !Array.isArray(value)) { - if ( - typeof target[key] !== "object" || - target[key] === null || - Array.isArray(target[key]) - ) { - target[key] = {}; - } - jsonMergePatch( - target[key] as Record, - value as Record, - ); - if (Object.keys(target[key] as Record).length === 0) { - delete target[key]; - } - } else { - target[key] = value; - } - } -} - -export { jsonMergePatch }; - -export function ClaudeQuickToggles({ - onPatchApplied, -}: ClaudeQuickTogglesProps) { - const { t } = useTranslation(); - const [states, setStates] = useState(defaultStates); - - const readLive = useCallback(async () => { - try { - const cfg = await invoke>( - "read_live_provider_settings", - { app: "claude" }, - ); - setStates(deriveStates(cfg)); - } catch { - // Live file missing or unreadable — show all unchecked - } - }, []); - - useEffect(() => { - readLive(); - }, [readLive]); - - const toggle = useCallback( - async (key: ToggleKey) => { - let patch: Record; - if (key === "hideAttribution") { - patch = states.hideAttribution - ? { attribution: null } - : { attribution: { commit: "", pr: "" } }; - } else if (key === "alwaysThinking") { - patch = states.alwaysThinking - ? { alwaysThinkingEnabled: null } - : { alwaysThinkingEnabled: true }; - } else { - patch = states.enableTeammates - ? { env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: null } } - : { env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" } }; - } - - // Optimistic update - setStates((prev) => ({ ...prev, [key]: !prev[key] })); - - try { - await invoke("patch_claude_live_settings", { patch }); - onPatchApplied?.(patch); - } catch { - // Revert on failure - readLive(); - } - }, - [states, readLive, onPatchApplied], - ); - - return ( -
- {( - [ - ["hideAttribution", "claudeConfig.hideAttribution"], - ["alwaysThinking", "claudeConfig.alwaysThinking"], - ["enableTeammates", "claudeConfig.enableTeammates"], - ] as const - ).map(([key, i18nKey]) => ( - - ))} -
- ); -} diff --git a/src/components/providers/forms/CodexCommonConfigModal.tsx b/src/components/providers/forms/CodexCommonConfigModal.tsx new file mode 100644 index 000000000..e8ddb8664 --- /dev/null +++ b/src/components/providers/forms/CodexCommonConfigModal.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import { Save, Download, Loader2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { FullScreenPanel } from "@/components/common/FullScreenPanel"; +import { Button } from "@/components/ui/button"; +import JsonEditor from "@/components/JsonEditor"; + +interface CodexCommonConfigModalProps { + isOpen: boolean; + onClose: () => void; + value: string; + onChange: (value: string) => void; + error?: string; + onExtract?: () => void; + isExtracting?: boolean; +} + +/** + * CodexCommonConfigModal - Common Codex configuration editor modal + * Allows editing of common TOML configuration shared across providers + */ +export const CodexCommonConfigModal: React.FC = ({ + isOpen, + onClose, + value, + onChange, + error, + onExtract, + isExtracting, +}) => { + const { t } = useTranslation(); + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + + const observer = new MutationObserver(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + return ( + + {onExtract && ( + + )} + + + + } + > +
+

+ {t("codexConfig.commonConfigHint")} +

+ + + + {error && ( +

{error}

+ )} +
+
+ ); +}; diff --git a/src/components/providers/forms/CodexConfigEditor.tsx b/src/components/providers/forms/CodexConfigEditor.tsx index cd65d8d01..42ba3cae3 100644 --- a/src/components/providers/forms/CodexConfigEditor.tsx +++ b/src/components/providers/forms/CodexConfigEditor.tsx @@ -1,5 +1,6 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections"; +import { CodexCommonConfigModal } from "./CodexCommonConfigModal"; interface CodexConfigEditorProps { authValue: string; @@ -12,9 +13,23 @@ interface CodexConfigEditorProps { onAuthBlur?: () => void; + useCommonConfig: boolean; + + onCommonConfigToggle: (checked: boolean) => void; + + commonConfigSnippet: string; + + onCommonConfigSnippetChange: (value: string) => void; + + commonConfigError: string; + authError: string; - configError: string; + configError: string; // config.toml 错误提示 + + onExtract?: () => void; + + isExtracting?: boolean; } const CodexConfigEditor: React.FC = ({ @@ -23,9 +38,25 @@ const CodexConfigEditor: React.FC = ({ onAuthChange, onConfigChange, onAuthBlur, + useCommonConfig, + onCommonConfigToggle, + commonConfigSnippet, + onCommonConfigSnippetChange, + commonConfigError, authError, configError, + onExtract, + isExtracting, }) => { + const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + + // Auto-open common config modal if there's an error + useEffect(() => { + if (commonConfigError && !isCommonConfigModalOpen) { + setIsCommonConfigModalOpen(true); + } + }, [commonConfigError, isCommonConfigModalOpen]); + return (
{/* Auth JSON Section */} @@ -40,8 +71,23 @@ const CodexConfigEditor: React.FC = ({ setIsCommonConfigModalOpen(true)} + commonConfigError={commonConfigError} configError={configError} /> + + {/* Common Config Modal */} + setIsCommonConfigModalOpen(false)} + value={commonConfigSnippet} + onChange={onCommonConfigSnippetChange} + error={commonConfigError} + onExtract={onExtract} + isExtracting={isExtracting} + />
); }; diff --git a/src/components/providers/forms/CodexConfigSections.tsx b/src/components/providers/forms/CodexConfigSections.tsx index 83971f277..99ada4273 100644 --- a/src/components/providers/forms/CodexConfigSections.tsx +++ b/src/components/providers/forms/CodexConfigSections.tsx @@ -78,6 +78,10 @@ export const CodexAuthSection: React.FC = ({ interface CodexConfigSectionProps { value: string; onChange: (value: string) => void; + useCommonConfig: boolean; + onCommonConfigToggle: (checked: boolean) => void; + onEditCommonConfig: () => void; + commonConfigError?: string; configError?: string; } @@ -87,6 +91,10 @@ interface CodexConfigSectionProps { export const CodexConfigSection: React.FC = ({ value, onChange, + useCommonConfig, + onCommonConfigToggle, + onEditCommonConfig, + commonConfigError, configError, }) => { const { t } = useTranslation(); @@ -109,12 +117,40 @@ export const CodexConfigSection: React.FC = ({ return (
- +
+ + + +
+ +
+ +
+ + {commonConfigError && ( +

+ {commonConfigError} +

+ )} void; + useCommonConfig: boolean; + onCommonConfigToggle: (checked: boolean) => void; + commonConfigSnippet: string; + onCommonConfigSnippetChange: (value: string) => void; + commonConfigError: string; + onEditClick: () => void; + isModalOpen: boolean; + onModalClose: () => void; + onExtract?: () => void; + isExtracting?: boolean; +} + +export function CommonConfigEditor({ + value, + onChange, + useCommonConfig, + onCommonConfigToggle, + commonConfigSnippet, + onCommonConfigSnippetChange, + commonConfigError, + onEditClick, + isModalOpen, + onModalClose, + onExtract, + isExtracting, +}: CommonConfigEditorProps) { + const { t } = useTranslation(); + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + + const observer = new MutationObserver(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + // Mirror value prop to local state so checkbox toggles and JsonEditor stay in sync + // (parent uses form.getValues which doesn't trigger re-renders) + const [localValue, setLocalValue] = useState(value); + + useEffect(() => { + setLocalValue(value); + }, [value]); + + const handleLocalChange = useCallback( + (newValue: string) => { + setLocalValue(newValue); + onChange(newValue); + }, + [onChange], + ); + + const toggleStates = useMemo(() => { + try { + const config = JSON.parse(localValue); + return { + hideAttribution: + config?.attribution?.commit === "" && config?.attribution?.pr === "", + alwaysThinking: config?.alwaysThinkingEnabled === true, + teammates: + config?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1" || + config?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === 1, + }; + } catch { + return { + hideAttribution: false, + alwaysThinking: false, + teammates: false, + }; + } + }, [localValue]); + + const handleToggle = useCallback( + (toggleKey: string, checked: boolean) => { + try { + const config = JSON.parse(localValue || "{}"); + + switch (toggleKey) { + case "hideAttribution": + if (checked) { + config.attribution = { commit: "", pr: "" }; + } else { + delete config.attribution; + } + break; + case "alwaysThinking": + if (checked) { + config.alwaysThinkingEnabled = true; + } else { + delete config.alwaysThinkingEnabled; + } + break; + case "teammates": + if (!config.env) config.env = {}; + if (checked) { + config.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"; + } else { + delete config.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS; + if (Object.keys(config.env).length === 0) delete config.env; + } + break; + } + + handleLocalChange(JSON.stringify(config, null, 2)); + } catch { + // Don't modify if JSON is invalid + } + }, + [localValue, handleLocalChange], + ); + + return ( + <> +
+
+ +
+ +
+
+
+ +
+ {commonConfigError && !isModalOpen && ( +

+ {commonConfigError} +

+ )} +
+ + + +
+ +
+ + + {onExtract && ( + + )} + + + + } + > +
+

+ {t("claudeConfig.commonConfigHint", { + defaultValue: "通用配置片段将合并到所有启用它的供应商配置中", + })} +

+ + {commonConfigError && ( +

+ {commonConfigError} +

+ )} +
+
+ + ); +} diff --git a/src/components/providers/forms/GeminiCommonConfigModal.tsx b/src/components/providers/forms/GeminiCommonConfigModal.tsx new file mode 100644 index 000000000..c7413217c --- /dev/null +++ b/src/components/providers/forms/GeminiCommonConfigModal.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from "react"; +import { Save, Download, Loader2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { FullScreenPanel } from "@/components/common/FullScreenPanel"; +import { Button } from "@/components/ui/button"; +import JsonEditor from "@/components/JsonEditor"; + +interface GeminiCommonConfigModalProps { + isOpen: boolean; + onClose: () => void; + value: string; + onChange: (value: string) => void; + error?: string; + onExtract?: () => void; + isExtracting?: boolean; +} + +/** + * GeminiCommonConfigModal - Common Gemini configuration editor modal + * Allows editing of common env snippet shared across Gemini providers + */ +export const GeminiCommonConfigModal: React.FC< + GeminiCommonConfigModalProps +> = ({ isOpen, onClose, value, onChange, error, onExtract, isExtracting }) => { + const { t } = useTranslation(); + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + + const observer = new MutationObserver(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + return ( + + {onExtract && ( + + )} + + + + } + > +
+

+ {t("geminiConfig.commonConfigHint", { + defaultValue: + "该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY)", + })} +

+ + + + {error && ( +

{error}

+ )} +
+
+ ); +}; diff --git a/src/components/providers/forms/GeminiConfigEditor.tsx b/src/components/providers/forms/GeminiConfigEditor.tsx index 985570ac1..2518b051f 100644 --- a/src/components/providers/forms/GeminiConfigEditor.tsx +++ b/src/components/providers/forms/GeminiConfigEditor.tsx @@ -1,5 +1,6 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { GeminiEnvSection, GeminiConfigSection } from "./GeminiConfigSections"; +import { GeminiCommonConfigModal } from "./GeminiCommonConfigModal"; interface GeminiConfigEditorProps { envValue: string; @@ -7,8 +8,15 @@ interface GeminiConfigEditorProps { onEnvChange: (value: string) => void; onConfigChange: (value: string) => void; onEnvBlur?: () => void; + useCommonConfig: boolean; + onCommonConfigToggle: (checked: boolean) => void; + commonConfigSnippet: string; + onCommonConfigSnippetChange: (value: string) => void; + commonConfigError: string; envError: string; configError: string; + onExtract?: () => void; + isExtracting?: boolean; } const GeminiConfigEditor: React.FC = ({ @@ -17,9 +25,25 @@ const GeminiConfigEditor: React.FC = ({ onEnvChange, onConfigChange, onEnvBlur, + useCommonConfig, + onCommonConfigToggle, + commonConfigSnippet, + onCommonConfigSnippetChange, + commonConfigError, envError, configError, + onExtract, + isExtracting, }) => { + const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + + // Auto-open common config modal if there's an error + useEffect(() => { + if (commonConfigError && !isCommonConfigModalOpen) { + setIsCommonConfigModalOpen(true); + } + }, [commonConfigError, isCommonConfigModalOpen]); + return (
{/* Env Section */} @@ -28,6 +52,10 @@ const GeminiConfigEditor: React.FC = ({ onChange={onEnvChange} onBlur={onEnvBlur} error={envError} + useCommonConfig={useCommonConfig} + onCommonConfigToggle={onCommonConfigToggle} + onEditCommonConfig={() => setIsCommonConfigModalOpen(true)} + commonConfigError={commonConfigError} /> {/* Config JSON Section */} @@ -36,6 +64,17 @@ const GeminiConfigEditor: React.FC = ({ onChange={onConfigChange} configError={configError} /> + + {/* Common Config Modal */} + setIsCommonConfigModalOpen(false)} + value={commonConfigSnippet} + onChange={onCommonConfigSnippetChange} + error={commonConfigError} + onExtract={onExtract} + isExtracting={isExtracting} + />
); }; diff --git a/src/components/providers/forms/GeminiConfigSections.tsx b/src/components/providers/forms/GeminiConfigSections.tsx index 2999fc309..999f986dc 100644 --- a/src/components/providers/forms/GeminiConfigSections.tsx +++ b/src/components/providers/forms/GeminiConfigSections.tsx @@ -7,6 +7,10 @@ interface GeminiEnvSectionProps { onChange: (value: string) => void; onBlur?: () => void; error?: string; + useCommonConfig: boolean; + onCommonConfigToggle: (checked: boolean) => void; + onEditCommonConfig: () => void; + commonConfigError?: string; } /** @@ -17,6 +21,10 @@ export const GeminiEnvSection: React.FC = ({ onChange, onBlur, error, + useCommonConfig, + onCommonConfigToggle, + onEditCommonConfig, + commonConfigError, }) => { const { t } = useTranslation(); const [isDarkMode, setIsDarkMode] = useState(false); @@ -45,12 +53,44 @@ export const GeminiEnvSection: React.FC = ({ return (
- +
+ + + +
+ +
+ +
+ + {commonConfigError && ( +

+ {commonConfigError} +

+ )} (() => { - if (appId !== "claude") return "anthropic"; - return initialData?.meta?.apiFormat ?? "anthropic"; - }); - - const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { - setLocalApiFormat(format); - }, []); - - const [localApiKeyField, setLocalApiKeyField] = useState( - () => { - if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN"; - if (initialData?.meta?.apiKeyField) return initialData.meta.apiKeyField; - try { - const config = initialData?.settingsConfig; - if ( - config?.env && - (config.env as Record).ANTHROPIC_API_KEY !== - undefined - ) - return "ANTHROPIC_API_KEY"; - } catch {} - return "ANTHROPIC_AUTH_TOKEN"; - }, - ); - - const handleApiKeyFieldChange = useCallback( - (field: ClaudeApiKeyField) => { - setLocalApiKeyField(field); - try { - const config = JSON.parse(form.getValues("settingsConfig") || "{}") as { - env?: Record; - }; - const env = (config.env ?? {}) as Record; - const oldField = - field === "ANTHROPIC_API_KEY" - ? "ANTHROPIC_AUTH_TOKEN" - : "ANTHROPIC_API_KEY"; - if (oldField in env) { - env[field] = env[oldField]; - delete env[oldField]; - config.env = env; - form.setValue("settingsConfig", JSON.stringify(config, null, 2)); - } - } catch {} - }, - [form], - ); - const { apiKey, handleApiKeyChange, @@ -296,7 +247,6 @@ export function ProviderForm({ selectedPresetId, category, appType: appId, - apiKeyField: appId === "claude" ? localApiKeyField : undefined, }); const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ @@ -320,6 +270,15 @@ export function ProviderForm({ onConfigChange: (config) => form.setValue("settingsConfig", config), }); + const [localApiFormat, setLocalApiFormat] = useState(() => { + if (appId !== "claude") return "anthropic"; + return initialData?.meta?.apiFormat ?? "anthropic"; + }); + + const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { + setLocalApiFormat(format); + }, []); + const { codexAuth, codexConfig, @@ -417,6 +376,37 @@ export function ProviderForm({ onConfigChange: (config) => form.setValue("settingsConfig", config), }); + const { + useCommonConfig, + commonConfigSnippet, + commonConfigError, + handleCommonConfigToggle, + handleCommonConfigSnippetChange, + isExtracting: isClaudeExtracting, + handleExtract: handleClaudeExtract, + } = useCommonConfigSnippet({ + settingsConfig: form.getValues("settingsConfig"), + onConfigChange: (config) => form.setValue("settingsConfig", config), + initialData: appId === "claude" ? initialData : undefined, + selectedPresetId: selectedPresetId ?? undefined, + enabled: appId === "claude", + }); + + const { + useCommonConfig: useCodexCommonConfigFlag, + commonConfigSnippet: codexCommonConfigSnippet, + commonConfigError: codexCommonConfigError, + handleCommonConfigToggle: handleCodexCommonConfigToggle, + handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange, + isExtracting: isCodexExtracting, + handleExtract: handleCodexExtract, + } = useCodexCommonConfig({ + codexConfig, + onConfigChange: handleCodexConfigChange, + initialData: appId === "codex" ? initialData : undefined, + selectedPresetId: selectedPresetId ?? undefined, + }); + const { geminiEnv, geminiConfig, @@ -432,6 +422,7 @@ export function ProviderForm({ handleGeminiConfigChange, resetGeminiConfig, envStringToObj, + envObjToString, } = useGeminiConfigState({ initialData: appId === "gemini" ? initialData : undefined, }); @@ -482,6 +473,23 @@ export function ProviderForm({ [originalHandleGeminiModelChange, updateGeminiEnvField], ); + const { + useCommonConfig: useGeminiCommonConfigFlag, + commonConfigSnippet: geminiCommonConfigSnippet, + commonConfigError: geminiCommonConfigError, + handleCommonConfigToggle: handleGeminiCommonConfigToggle, + handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange, + isExtracting: isGeminiExtracting, + handleExtract: handleGeminiExtract, + } = useGeminiCommonConfig({ + envValue: geminiEnv, + onEnvChange: handleGeminiEnvChange, + envStringToObj, + envObjToString, + initialData: appId === "gemini" ? initialData : undefined, + selectedPresetId: selectedPresetId ?? undefined, + }); + // ── Extracted hooks: OpenCode / OMO / OpenClaw ───────────────────── const { @@ -520,6 +528,8 @@ export function ProviderForm({ getSettingsConfig: () => form.getValues("settingsConfig"), }); + const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + const handleSubmit = (values: ProviderFormData) => { if (appId === "claude" && templateValueEntries.length > 0) { const validation = validateTemplateValues(); @@ -813,10 +823,6 @@ export function ProviderForm({ appId === "claude" && category !== "official" ? localApiFormat : undefined, - apiKeyField: - appId === "claude" && category !== "official" - ? localApiKeyField - : undefined, }; onSubmit(payload); @@ -1055,12 +1061,6 @@ export function ProviderForm({ setLocalApiFormat("anthropic"); } - if (preset.apiKeyField) { - setLocalApiKeyField(preset.apiKeyField); - } else { - setLocalApiKeyField("ANTHROPIC_AUTH_TOKEN"); - } - form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", @@ -1266,8 +1266,6 @@ export function ProviderForm({ speedTestEndpoints={speedTestEndpoints} apiFormat={localApiFormat} onApiFormatChange={handleApiFormatChange} - apiKeyField={localApiKeyField} - onApiKeyFieldChange={handleApiKeyFieldChange} /> )} @@ -1394,8 +1392,15 @@ export function ProviderForm({ configValue={codexConfig} onAuthChange={setCodexAuth} onConfigChange={handleCodexConfigChange} + useCommonConfig={useCodexCommonConfigFlag} + onCommonConfigToggle={handleCodexCommonConfigToggle} + commonConfigSnippet={codexCommonConfigSnippet} + onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange} + commonConfigError={codexCommonConfigError} authError={codexAuthError} configError={codexConfigError} + onExtract={handleCodexExtract} + isExtracting={isCodexExtracting} /> {settingsConfigErrorField} @@ -1406,8 +1411,17 @@ export function ProviderForm({ configValue={geminiConfig} onEnvChange={handleGeminiEnvChange} onConfigChange={handleGeminiConfigChange} + useCommonConfig={useGeminiCommonConfigFlag} + onCommonConfigToggle={handleGeminiCommonConfigToggle} + commonConfigSnippet={geminiCommonConfigSnippet} + onCommonConfigSnippetChange={ + handleGeminiCommonConfigSnippetChange + } + commonConfigError={geminiCommonConfigError} envError={envError} configError={geminiConfigError} + onExtract={handleGeminiExtract} + isExtracting={isGeminiExtracting} /> {settingsConfigErrorField} @@ -1477,48 +1491,20 @@ export function ProviderForm({ ) : ( <> -
- - {isEditMode && isCurrent && ( - { - try { - const cfg = JSON.parse( - form.getValues("settingsConfig") || "{}", - ); - jsonMergePatch(cfg, patch); - form.setValue( - "settingsConfig", - JSON.stringify(cfg, null, 2), - ); - } catch { - // invalid JSON in editor — skip mirror - } - }} - /> - )} - form.setValue("settingsConfig", value)} - placeholder={`{ - "env": { - "ANTHROPIC_AUTH_TOKEN": "your-api-key-here" - } -}`} - rows={14} - showValidation={true} - language="json" - /> -

- {t( - isCurrent - ? "claudeConfig.fullSettingsHint" - : "claudeConfig.fragmentSettingsHint", - )} -

-
+ form.setValue("settingsConfig", value)} + useCommonConfig={useCommonConfig} + onCommonConfigToggle={handleCommonConfigToggle} + commonConfigSnippet={commonConfigSnippet} + onCommonConfigSnippetChange={handleCommonConfigSnippetChange} + commonConfigError={commonConfigError} + onEditClick={() => setIsCommonConfigModalOpen(true)} + isModalOpen={isCommonConfigModalOpen} + onModalClose={() => setIsCommonConfigModalOpen(false)} + onExtract={handleClaudeExtract} + isExtracting={isClaudeExtracting} + /> {settingsConfigErrorField} )} diff --git a/src/components/providers/forms/hooks/index.ts b/src/components/providers/forms/hooks/index.ts index 10f374106..760904339 100644 --- a/src/components/providers/forms/hooks/index.ts +++ b/src/components/providers/forms/hooks/index.ts @@ -6,9 +6,12 @@ export { useCodexConfigState } from "./useCodexConfigState"; export { useApiKeyLink } from "./useApiKeyLink"; export { useCustomEndpoints } from "./useCustomEndpoints"; export { useTemplateValues } from "./useTemplateValues"; +export { useCommonConfigSnippet } from "./useCommonConfigSnippet"; +export { useCodexCommonConfig } from "./useCodexCommonConfig"; export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints"; export { useCodexTomlValidation } from "./useCodexTomlValidation"; export { useGeminiConfigState } from "./useGeminiConfigState"; +export { useGeminiCommonConfig } from "./useGeminiCommonConfig"; export { useOmoModelSource } from "./useOmoModelSource"; export { useOpencodeFormState } from "./useOpencodeFormState"; export { useOmoDraftState } from "./useOmoDraftState"; diff --git a/src/components/providers/forms/hooks/useApiKeyState.ts b/src/components/providers/forms/hooks/useApiKeyState.ts index 8b4c87626..9101d2e35 100644 --- a/src/components/providers/forms/hooks/useApiKeyState.ts +++ b/src/components/providers/forms/hooks/useApiKeyState.ts @@ -12,7 +12,6 @@ interface UseApiKeyStateProps { selectedPresetId: string | null; category?: ProviderCategory; appType?: string; - apiKeyField?: string; } /** @@ -25,7 +24,6 @@ export function useApiKeyState({ selectedPresetId, category, appType, - apiKeyField, }: UseApiKeyStateProps) { const [apiKey, setApiKey] = useState(() => { if (initialConfig) { @@ -60,7 +58,7 @@ export function useApiKeyState({ initialConfig || "{}", key.trim(), { - // 最佳实践:仅在"新增模式"且"非官方类别"时补齐缺失字段 + // 最佳实践:仅在“新增模式”且“非官方类别”时补齐缺失字段 // - 新增模式:selectedPresetId !== null // - 非官方类别:category !== undefined && category !== "official" // - 官方类别:不创建字段(UI 也会禁用输入框) @@ -70,20 +68,12 @@ export function useApiKeyState({ category !== undefined && category !== "official", appType, - apiKeyField, }, ); onConfigChange(configString); }, - [ - initialConfig, - selectedPresetId, - category, - appType, - apiKeyField, - onConfigChange, - ], + [initialConfig, selectedPresetId, category, appType, onConfigChange], ); const showApiKey = useCallback( diff --git a/src/components/providers/forms/hooks/useCodexCommonConfig.ts b/src/components/providers/forms/hooks/useCodexCommonConfig.ts new file mode 100644 index 000000000..849908c7d --- /dev/null +++ b/src/components/providers/forms/hooks/useCodexCommonConfig.ts @@ -0,0 +1,312 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + updateTomlCommonConfigSnippet, + hasTomlCommonConfigSnippet, +} from "@/utils/providerConfigUtils"; +import { configApi } from "@/lib/api"; + +const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet"; +const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config +# Add your common TOML configuration here`; + +interface UseCodexCommonConfigProps { + codexConfig: string; + onConfigChange: (config: string) => void; + initialData?: { + settingsConfig?: Record; + }; + selectedPresetId?: string; +} + +/** + * 管理 Codex 通用配置片段 (TOML 格式) + * 从 config.json 读取和保存,支持从 localStorage 平滑迁移 + */ +export function useCodexCommonConfig({ + codexConfig, + onConfigChange, + initialData, + selectedPresetId, +}: UseCodexCommonConfigProps) { + const { t } = useTranslation(); + const [useCommonConfig, setUseCommonConfig] = useState(false); + const [commonConfigSnippet, setCommonConfigSnippetState] = useState( + DEFAULT_CODEX_COMMON_CONFIG_SNIPPET, + ); + const [commonConfigError, setCommonConfigError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [isExtracting, setIsExtracting] = useState(false); + + // 用于跟踪是否正在通过通用配置更新 + const isUpdatingFromCommonConfig = useRef(false); + // 用于跟踪新建模式是否已初始化默认勾选 + const hasInitializedNewMode = useRef(false); + + // 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑 + useEffect(() => { + hasInitializedNewMode.current = false; + }, [selectedPresetId]); + + // 初始化:从 config.json 加载,支持从 localStorage 迁移 + useEffect(() => { + let mounted = true; + + const loadSnippet = async () => { + try { + // 使用统一 API 加载 + const snippet = await configApi.getCommonConfigSnippet("codex"); + + if (snippet && snippet.trim()) { + if (mounted) { + setCommonConfigSnippetState(snippet); + } + } else { + // 如果 config.json 中没有,尝试从 localStorage 迁移 + if (typeof window !== "undefined") { + try { + const legacySnippet = + window.localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacySnippet && legacySnippet.trim()) { + // 迁移到 config.json + await configApi.setCommonConfigSnippet("codex", legacySnippet); + if (mounted) { + setCommonConfigSnippetState(legacySnippet); + } + // 清理 localStorage + window.localStorage.removeItem(LEGACY_STORAGE_KEY); + console.log( + "[迁移] Codex 通用配置已从 localStorage 迁移到 config.json", + ); + } + } catch (e) { + console.warn("[迁移] 从 localStorage 迁移失败:", e); + } + } + } + } catch (error) { + console.error("加载 Codex 通用配置失败:", error); + } finally { + if (mounted) { + setIsLoading(false); + } + } + }; + + loadSnippet(); + + return () => { + mounted = false; + }; + }, []); + + // 初始化时检查通用配置片段(编辑模式) + useEffect(() => { + if (initialData?.settingsConfig && !isLoading) { + const config = + typeof initialData.settingsConfig.config === "string" + ? initialData.settingsConfig.config + : ""; + const hasCommon = hasTomlCommonConfigSnippet(config, commonConfigSnippet); + setUseCommonConfig(hasCommon); + } + }, [initialData, commonConfigSnippet, isLoading]); + + // 新建模式:如果通用配置片段存在且有效,默认启用 + useEffect(() => { + // 仅新建模式、加载完成、尚未初始化过 + if (!initialData && !isLoading && !hasInitializedNewMode.current) { + hasInitializedNewMode.current = true; + + // 检查 TOML 片段是否有实质内容(不只是注释和空行) + const lines = commonConfigSnippet.split("\n"); + const hasContent = lines.some((line) => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith("#"); + }); + + if (hasContent) { + setUseCommonConfig(true); + // 合并通用配置到当前配置 + const { updatedConfig, error } = updateTomlCommonConfigSnippet( + codexConfig, + commonConfigSnippet, + true, + ); + if (!error) { + isUpdatingFromCommonConfig.current = true; + onConfigChange(updatedConfig); + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + } + } + }, [ + initialData, + commonConfigSnippet, + isLoading, + codexConfig, + onConfigChange, + ]); + + // 处理通用配置开关 + const handleCommonConfigToggle = useCallback( + (checked: boolean) => { + const { updatedConfig, error: snippetError } = + updateTomlCommonConfigSnippet( + codexConfig, + commonConfigSnippet, + checked, + ); + + if (snippetError) { + setCommonConfigError(snippetError); + setUseCommonConfig(false); + return; + } + + setCommonConfigError(""); + setUseCommonConfig(checked); + // 标记正在通过通用配置更新 + isUpdatingFromCommonConfig.current = true; + onConfigChange(updatedConfig); + // 在下一个事件循环中重置标记 + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + }, + [codexConfig, commonConfigSnippet, onConfigChange], + ); + + // 处理通用配置片段变化 + const handleCommonConfigSnippetChange = useCallback( + (value: string) => { + const previousSnippet = commonConfigSnippet; + setCommonConfigSnippetState(value); + + if (!value.trim()) { + setCommonConfigError(""); + // 保存到 config.json(清空) + configApi + .setCommonConfigSnippet("codex", "") + .catch((error: unknown) => { + console.error("保存 Codex 通用配置失败:", error); + setCommonConfigError( + t("codexConfig.saveFailed", { error: String(error) }), + ); + }); + + if (useCommonConfig) { + const { updatedConfig } = updateTomlCommonConfigSnippet( + codexConfig, + previousSnippet, + false, + ); + onConfigChange(updatedConfig); + setUseCommonConfig(false); + } + return; + } + + // TOML 格式校验较为复杂,暂时不做校验,直接清空错误 + setCommonConfigError(""); + // 保存到 config.json + configApi + .setCommonConfigSnippet("codex", value) + .catch((error: unknown) => { + console.error("保存 Codex 通用配置失败:", error); + setCommonConfigError( + t("codexConfig.saveFailed", { error: String(error) }), + ); + }); + + // 若当前启用通用配置,需要替换为最新片段 + if (useCommonConfig) { + const removeResult = updateTomlCommonConfigSnippet( + codexConfig, + previousSnippet, + false, + ); + if (removeResult.error) { + setCommonConfigError(removeResult.error); + return; + } + const addResult = updateTomlCommonConfigSnippet( + removeResult.updatedConfig, + value, + true, + ); + + if (addResult.error) { + setCommonConfigError(addResult.error); + return; + } + + // 标记正在通过通用配置更新,避免触发状态检查 + isUpdatingFromCommonConfig.current = true; + onConfigChange(addResult.updatedConfig); + // 在下一个事件循环中重置标记 + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + }, + [commonConfigSnippet, codexConfig, useCommonConfig, onConfigChange], + ); + + // 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) + useEffect(() => { + if (isUpdatingFromCommonConfig.current || isLoading) { + return; + } + const hasCommon = hasTomlCommonConfigSnippet( + codexConfig, + commonConfigSnippet, + ); + setUseCommonConfig(hasCommon); + }, [codexConfig, commonConfigSnippet, isLoading]); + + // 从编辑器当前内容提取通用配置片段 + const handleExtract = useCallback(async () => { + setIsExtracting(true); + setCommonConfigError(""); + + try { + const extracted = await configApi.extractCommonConfigSnippet("codex", { + settingsConfig: JSON.stringify({ + config: codexConfig ?? "", + }), + }); + + if (!extracted || !extracted.trim()) { + setCommonConfigError(t("codexConfig.extractNoCommonConfig")); + return; + } + + // 更新片段状态 + setCommonConfigSnippetState(extracted); + + // 保存到后端 + await configApi.setCommonConfigSnippet("codex", extracted); + } catch (error) { + console.error("提取 Codex 通用配置失败:", error); + setCommonConfigError( + t("codexConfig.extractFailed", { error: String(error) }), + ); + } finally { + setIsExtracting(false); + } + }, [codexConfig, t]); + + return { + useCommonConfig, + commonConfigSnippet, + commonConfigError, + isLoading, + isExtracting, + handleCommonConfigToggle, + handleCommonConfigSnippetChange, + handleExtract, + }; +} diff --git a/src/components/providers/forms/hooks/useCommonConfigSnippet.ts b/src/components/providers/forms/hooks/useCommonConfigSnippet.ts new file mode 100644 index 000000000..80bf37ff1 --- /dev/null +++ b/src/components/providers/forms/hooks/useCommonConfigSnippet.ts @@ -0,0 +1,335 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + updateCommonConfigSnippet, + hasCommonConfigSnippet, + validateJsonConfig, +} from "@/utils/providerConfigUtils"; +import { configApi } from "@/lib/api"; + +const LEGACY_STORAGE_KEY = "cc-switch:common-config-snippet"; +const DEFAULT_COMMON_CONFIG_SNIPPET = `{ + "includeCoAuthoredBy": false +}`; + +interface UseCommonConfigSnippetProps { + settingsConfig: string; + onConfigChange: (config: string) => void; + initialData?: { + settingsConfig?: Record; + }; + selectedPresetId?: string; + /** When false, the hook skips all logic and returns disabled state. Default: true */ + enabled?: boolean; +} + +/** + * 管理 Claude 通用配置片段 + * 从 config.json 读取和保存,支持从 localStorage 平滑迁移 + */ +export function useCommonConfigSnippet({ + settingsConfig, + onConfigChange, + initialData, + selectedPresetId, + enabled = true, +}: UseCommonConfigSnippetProps) { + const { t } = useTranslation(); + const [useCommonConfig, setUseCommonConfig] = useState(false); + const [commonConfigSnippet, setCommonConfigSnippetState] = useState( + DEFAULT_COMMON_CONFIG_SNIPPET, + ); + const [commonConfigError, setCommonConfigError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [isExtracting, setIsExtracting] = useState(false); + + // 用于跟踪是否正在通过通用配置更新 + const isUpdatingFromCommonConfig = useRef(false); + // 用于跟踪新建模式是否已初始化默认勾选 + const hasInitializedNewMode = useRef(false); + + // 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑 + useEffect(() => { + if (!enabled) return; + hasInitializedNewMode.current = false; + }, [selectedPresetId, enabled]); + + // 初始化:从 config.json 加载,支持从 localStorage 迁移 + useEffect(() => { + if (!enabled) { + setIsLoading(false); + return; + } + let mounted = true; + + const loadSnippet = async () => { + try { + // 使用统一 API 加载 + const snippet = await configApi.getCommonConfigSnippet("claude"); + + if (snippet && snippet.trim()) { + if (mounted) { + setCommonConfigSnippetState(snippet); + } + } else { + // 如果 config.json 中没有,尝试从 localStorage 迁移 + if (typeof window !== "undefined") { + try { + const legacySnippet = + window.localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacySnippet && legacySnippet.trim()) { + // 迁移到 config.json + await configApi.setCommonConfigSnippet("claude", legacySnippet); + if (mounted) { + setCommonConfigSnippetState(legacySnippet); + } + // 清理 localStorage + window.localStorage.removeItem(LEGACY_STORAGE_KEY); + console.log( + "[迁移] Claude 通用配置已从 localStorage 迁移到 config.json", + ); + } + } catch (e) { + console.warn("[迁移] 从 localStorage 迁移失败:", e); + } + } + } + } catch (error) { + console.error("加载通用配置失败:", error); + } finally { + if (mounted) { + setIsLoading(false); + } + } + }; + + loadSnippet(); + + return () => { + mounted = false; + }; + }, [enabled]); + + // 初始化时检查通用配置片段(编辑模式) + useEffect(() => { + if (!enabled) return; + if (initialData && !isLoading) { + const configString = JSON.stringify(initialData.settingsConfig, null, 2); + const hasCommon = hasCommonConfigSnippet( + configString, + commonConfigSnippet, + ); + setUseCommonConfig(hasCommon); + } + }, [enabled, initialData, commonConfigSnippet, isLoading]); + + // 新建模式:如果通用配置片段存在且有效,默认启用 + useEffect(() => { + if (!enabled) return; + // 仅新建模式、加载完成、尚未初始化过 + if (!initialData && !isLoading && !hasInitializedNewMode.current) { + hasInitializedNewMode.current = true; + + // 检查片段是否有实质内容 + try { + const snippetObj = JSON.parse(commonConfigSnippet); + const hasContent = Object.keys(snippetObj).length > 0; + if (hasContent) { + setUseCommonConfig(true); + // 合并通用配置到当前配置 + const { updatedConfig, error } = updateCommonConfigSnippet( + settingsConfig, + commonConfigSnippet, + true, + ); + if (!error) { + isUpdatingFromCommonConfig.current = true; + onConfigChange(updatedConfig); + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + } + } catch { + // ignore parse error + } + } + }, [ + enabled, + initialData, + commonConfigSnippet, + isLoading, + settingsConfig, + onConfigChange, + ]); + + // 处理通用配置开关 + const handleCommonConfigToggle = useCallback( + (checked: boolean) => { + const { updatedConfig, error: snippetError } = updateCommonConfigSnippet( + settingsConfig, + commonConfigSnippet, + checked, + ); + + if (snippetError) { + setCommonConfigError(snippetError); + setUseCommonConfig(false); + return; + } + + setCommonConfigError(""); + setUseCommonConfig(checked); + // 标记正在通过通用配置更新 + isUpdatingFromCommonConfig.current = true; + onConfigChange(updatedConfig); + // 在下一个事件循环中重置标记 + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + }, + [settingsConfig, commonConfigSnippet, onConfigChange], + ); + + // 处理通用配置片段变化 + const handleCommonConfigSnippetChange = useCallback( + (value: string) => { + const previousSnippet = commonConfigSnippet; + setCommonConfigSnippetState(value); + + if (!value.trim()) { + setCommonConfigError(""); + // 保存到 config.json(清空) + configApi + .setCommonConfigSnippet("claude", "") + .catch((error: unknown) => { + console.error("保存通用配置失败:", error); + setCommonConfigError( + t("claudeConfig.saveFailed", { error: String(error) }), + ); + }); + + if (useCommonConfig) { + const { updatedConfig } = updateCommonConfigSnippet( + settingsConfig, + previousSnippet, + false, + ); + onConfigChange(updatedConfig); + setUseCommonConfig(false); + } + return; + } + + // 验证JSON格式 + const validationError = validateJsonConfig(value, "通用配置片段"); + if (validationError) { + setCommonConfigError(validationError); + } else { + setCommonConfigError(""); + // 保存到 config.json + configApi + .setCommonConfigSnippet("claude", value) + .catch((error: unknown) => { + console.error("保存通用配置失败:", error); + setCommonConfigError( + t("claudeConfig.saveFailed", { error: String(error) }), + ); + }); + } + + // 若当前启用通用配置且格式正确,需要替换为最新片段 + if (useCommonConfig && !validationError) { + const removeResult = updateCommonConfigSnippet( + settingsConfig, + previousSnippet, + false, + ); + if (removeResult.error) { + setCommonConfigError(removeResult.error); + return; + } + const addResult = updateCommonConfigSnippet( + removeResult.updatedConfig, + value, + true, + ); + + if (addResult.error) { + setCommonConfigError(addResult.error); + return; + } + + // 标记正在通过通用配置更新,避免触发状态检查 + isUpdatingFromCommonConfig.current = true; + onConfigChange(addResult.updatedConfig); + // 在下一个事件循环中重置标记 + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + }, + [commonConfigSnippet, settingsConfig, useCommonConfig, onConfigChange], + ); + + // 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) + useEffect(() => { + if (!enabled) return; + if (isUpdatingFromCommonConfig.current || isLoading) { + return; + } + const hasCommon = hasCommonConfigSnippet( + settingsConfig, + commonConfigSnippet, + ); + setUseCommonConfig(hasCommon); + }, [enabled, settingsConfig, commonConfigSnippet, isLoading]); + + // 从编辑器当前内容提取通用配置片段 + const handleExtract = useCallback(async () => { + setIsExtracting(true); + setCommonConfigError(""); + + try { + const extracted = await configApi.extractCommonConfigSnippet("claude", { + settingsConfig, + }); + + if (!extracted || extracted === "{}") { + setCommonConfigError(t("claudeConfig.extractNoCommonConfig")); + return; + } + + // 验证 JSON 格式 + const validationError = validateJsonConfig(extracted, "提取的配置"); + if (validationError) { + setCommonConfigError(validationError); + return; + } + + // 更新片段状态 + setCommonConfigSnippetState(extracted); + + // 保存到后端 + await configApi.setCommonConfigSnippet("claude", extracted); + } catch (error) { + console.error("提取通用配置失败:", error); + setCommonConfigError( + t("claudeConfig.extractFailed", { error: String(error) }), + ); + } finally { + setIsExtracting(false); + } + }, [settingsConfig, t]); + + return { + useCommonConfig, + commonConfigSnippet, + commonConfigError, + isLoading, + isExtracting, + handleCommonConfigToggle, + handleCommonConfigSnippetChange, + handleExtract, + }; +} diff --git a/src/components/providers/forms/hooks/useGeminiCommonConfig.ts b/src/components/providers/forms/hooks/useGeminiCommonConfig.ts new file mode 100644 index 000000000..71f761250 --- /dev/null +++ b/src/components/providers/forms/hooks/useGeminiCommonConfig.ts @@ -0,0 +1,469 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { configApi } from "@/lib/api"; + +const LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet"; +const DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET = "{}"; + +const GEMINI_COMMON_ENV_FORBIDDEN_KEYS = [ + "GOOGLE_GEMINI_BASE_URL", + "GEMINI_API_KEY", +] as const; +type GeminiForbiddenEnvKey = (typeof GEMINI_COMMON_ENV_FORBIDDEN_KEYS)[number]; + +interface UseGeminiCommonConfigProps { + envValue: string; + onEnvChange: (env: string) => void; + envStringToObj: (envString: string) => Record; + envObjToString: (envObj: Record) => string; + initialData?: { + settingsConfig?: Record; + }; + selectedPresetId?: string; +} + +function isPlainObject(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.prototype.toString.call(value) === "[object Object]" + ); +} + +/** + * 管理 Gemini 通用配置片段 (JSON 格式) + * 写入 Gemini 的 .env,但会排除以下敏感字段: + * - GOOGLE_GEMINI_BASE_URL + * - GEMINI_API_KEY + */ +export function useGeminiCommonConfig({ + envValue, + onEnvChange, + envStringToObj, + envObjToString, + initialData, + selectedPresetId, +}: UseGeminiCommonConfigProps) { + const { t } = useTranslation(); + const [useCommonConfig, setUseCommonConfig] = useState(false); + const [commonConfigSnippet, setCommonConfigSnippetState] = useState( + DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET, + ); + const [commonConfigError, setCommonConfigError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [isExtracting, setIsExtracting] = useState(false); + + // 用于跟踪是否正在通过通用配置更新 + const isUpdatingFromCommonConfig = useRef(false); + // 用于跟踪新建模式是否已初始化默认勾选 + const hasInitializedNewMode = useRef(false); + + // 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑 + useEffect(() => { + hasInitializedNewMode.current = false; + }, [selectedPresetId]); + + const parseSnippetEnv = useCallback( + ( + snippetString: string, + ): { env: Record; error?: string } => { + const trimmed = snippetString.trim(); + if (!trimmed) { + return { env: {} }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch { + return { env: {}, error: t("geminiConfig.invalidJsonFormat") }; + } + + if (!isPlainObject(parsed)) { + return { env: {}, error: t("geminiConfig.invalidJsonFormat") }; + } + + const keys = Object.keys(parsed); + const forbiddenKeys = keys.filter((key) => + GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(key as GeminiForbiddenEnvKey), + ); + if (forbiddenKeys.length > 0) { + return { + env: {}, + error: t("geminiConfig.commonConfigInvalidKeys", { + keys: forbiddenKeys.join(", "), + }), + }; + } + + const env: Record = {}; + for (const [key, value] of Object.entries(parsed)) { + if (typeof value !== "string") { + return { + env: {}, + error: t("geminiConfig.commonConfigInvalidValues"), + }; + } + const normalized = value.trim(); + if (!normalized) continue; + env[key] = normalized; + } + + return { env }; + }, + [t], + ); + + const hasEnvCommonConfigSnippet = useCallback( + (envObj: Record, snippetEnv: Record) => { + const entries = Object.entries(snippetEnv); + if (entries.length === 0) return false; + return entries.every(([key, value]) => envObj[key] === value); + }, + [], + ); + + const applySnippetToEnv = useCallback( + (envObj: Record, snippetEnv: Record) => { + const updated = { ...envObj }; + for (const [key, value] of Object.entries(snippetEnv)) { + if (typeof value === "string") { + updated[key] = value; + } + } + return updated; + }, + [], + ); + + const removeSnippetFromEnv = useCallback( + (envObj: Record, snippetEnv: Record) => { + const updated = { ...envObj }; + for (const [key, value] of Object.entries(snippetEnv)) { + if (typeof value === "string" && updated[key] === value) { + delete updated[key]; + } + } + return updated; + }, + [], + ); + + // 初始化:从 config.json 加载,支持从 localStorage 迁移 + useEffect(() => { + let mounted = true; + + const loadSnippet = async () => { + try { + // 使用统一 API 加载 + const snippet = await configApi.getCommonConfigSnippet("gemini"); + + if (snippet && snippet.trim()) { + if (mounted) { + setCommonConfigSnippetState(snippet); + } + } else { + // 如果 config.json 中没有,尝试从 localStorage 迁移 + if (typeof window !== "undefined") { + try { + const legacySnippet = + window.localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacySnippet && legacySnippet.trim()) { + const parsed = parseSnippetEnv(legacySnippet); + if (parsed.error) { + console.warn( + "[迁移] legacy Gemini 通用配置片段格式不符合当前规则,跳过迁移", + ); + return; + } + // 迁移到 config.json + await configApi.setCommonConfigSnippet("gemini", legacySnippet); + if (mounted) { + setCommonConfigSnippetState(legacySnippet); + } + // 清理 localStorage + window.localStorage.removeItem(LEGACY_STORAGE_KEY); + console.log( + "[迁移] Gemini 通用配置已从 localStorage 迁移到 config.json", + ); + } + } catch (e) { + console.warn("[迁移] 从 localStorage 迁移失败:", e); + } + } + } + } catch (error) { + console.error("加载 Gemini 通用配置失败:", error); + } finally { + if (mounted) { + setIsLoading(false); + } + } + }; + + loadSnippet(); + + return () => { + mounted = false; + }; + }, [parseSnippetEnv]); + + // 初始化时检查通用配置片段(编辑模式) + useEffect(() => { + if (initialData?.settingsConfig && !isLoading) { + try { + const env = + isPlainObject(initialData.settingsConfig.env) && + Object.keys(initialData.settingsConfig.env).length > 0 + ? (initialData.settingsConfig.env as Record) + : {}; + const parsed = parseSnippetEnv(commonConfigSnippet); + if (parsed.error) return; + const hasCommon = hasEnvCommonConfigSnippet( + env, + parsed.env as Record, + ); + setUseCommonConfig(hasCommon); + } catch { + // ignore parse error + } + } + }, [ + commonConfigSnippet, + hasEnvCommonConfigSnippet, + initialData, + isLoading, + parseSnippetEnv, + ]); + + // 新建模式:如果通用配置片段存在且有效,默认启用 + useEffect(() => { + // 仅新建模式、加载完成、尚未初始化过 + if (!initialData && !isLoading && !hasInitializedNewMode.current) { + hasInitializedNewMode.current = true; + + const parsed = parseSnippetEnv(commonConfigSnippet); + if (parsed.error) return; + const hasContent = Object.keys(parsed.env).length > 0; + if (!hasContent) return; + + setUseCommonConfig(true); + const currentEnv = envStringToObj(envValue); + const merged = applySnippetToEnv(currentEnv, parsed.env); + const nextEnvString = envObjToString(merged); + + isUpdatingFromCommonConfig.current = true; + onEnvChange(nextEnvString); + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + }, [ + initialData, + isLoading, + commonConfigSnippet, + envValue, + envStringToObj, + envObjToString, + applySnippetToEnv, + onEnvChange, + parseSnippetEnv, + ]); + + // 处理通用配置开关 + const handleCommonConfigToggle = useCallback( + (checked: boolean) => { + const parsed = parseSnippetEnv(commonConfigSnippet); + if (parsed.error) { + setCommonConfigError(parsed.error); + setUseCommonConfig(false); + return; + } + if (Object.keys(parsed.env).length === 0) { + setCommonConfigError(t("geminiConfig.noCommonConfigToApply")); + setUseCommonConfig(false); + return; + } + + const currentEnv = envStringToObj(envValue); + const updatedEnvObj = checked + ? applySnippetToEnv(currentEnv, parsed.env) + : removeSnippetFromEnv(currentEnv, parsed.env); + + setCommonConfigError(""); + setUseCommonConfig(checked); + + isUpdatingFromCommonConfig.current = true; + onEnvChange(envObjToString(updatedEnvObj)); + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + }, + [ + applySnippetToEnv, + commonConfigSnippet, + envObjToString, + envStringToObj, + envValue, + onEnvChange, + parseSnippetEnv, + removeSnippetFromEnv, + t, + ], + ); + + // 处理通用配置片段变化 + const handleCommonConfigSnippetChange = useCallback( + (value: string) => { + const previousSnippet = commonConfigSnippet; + setCommonConfigSnippetState(value); + + if (!value.trim()) { + setCommonConfigError(""); + // 保存到 config.json(清空) + configApi + .setCommonConfigSnippet("gemini", "") + .catch((error: unknown) => { + console.error("保存 Gemini 通用配置失败:", error); + setCommonConfigError( + t("geminiConfig.saveFailed", { error: String(error) }), + ); + }); + + if (useCommonConfig) { + const parsed = parseSnippetEnv(previousSnippet); + if (!parsed.error && Object.keys(parsed.env).length > 0) { + const currentEnv = envStringToObj(envValue); + const updatedEnv = removeSnippetFromEnv(currentEnv, parsed.env); + onEnvChange(envObjToString(updatedEnv)); + } + setUseCommonConfig(false); + } + return; + } + + // 校验 JSON 格式 + const parsed = parseSnippetEnv(value); + if (parsed.error) { + setCommonConfigError(parsed.error); + return; + } + + setCommonConfigError(""); + configApi + .setCommonConfigSnippet("gemini", value) + .catch((error: unknown) => { + console.error("保存 Gemini 通用配置失败:", error); + setCommonConfigError( + t("geminiConfig.saveFailed", { error: String(error) }), + ); + }); + + // 若当前启用通用配置,需要替换为最新片段 + if (useCommonConfig) { + const prevParsed = parseSnippetEnv(previousSnippet); + const prevEnv = prevParsed.error ? {} : prevParsed.env; + const nextEnv = parsed.env; + const currentEnv = envStringToObj(envValue); + + const withoutOld = + Object.keys(prevEnv).length > 0 + ? removeSnippetFromEnv(currentEnv, prevEnv) + : currentEnv; + const withNew = + Object.keys(nextEnv).length > 0 + ? applySnippetToEnv(withoutOld, nextEnv) + : withoutOld; + + isUpdatingFromCommonConfig.current = true; + onEnvChange(envObjToString(withNew)); + setTimeout(() => { + isUpdatingFromCommonConfig.current = false; + }, 0); + } + }, + [ + applySnippetToEnv, + commonConfigSnippet, + envObjToString, + envStringToObj, + envValue, + onEnvChange, + parseSnippetEnv, + removeSnippetFromEnv, + t, + useCommonConfig, + ], + ); + + // 当 env 变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) + useEffect(() => { + if (isUpdatingFromCommonConfig.current || isLoading) { + return; + } + const parsed = parseSnippetEnv(commonConfigSnippet); + if (parsed.error) return; + const envObj = envStringToObj(envValue); + setUseCommonConfig( + hasEnvCommonConfigSnippet(envObj, parsed.env as Record), + ); + }, [ + envValue, + commonConfigSnippet, + envStringToObj, + hasEnvCommonConfigSnippet, + isLoading, + parseSnippetEnv, + ]); + + // 从编辑器当前内容提取通用配置片段 + const handleExtract = useCallback(async () => { + setIsExtracting(true); + setCommonConfigError(""); + + try { + const extracted = await configApi.extractCommonConfigSnippet("gemini", { + settingsConfig: JSON.stringify({ + env: envStringToObj(envValue), + }), + }); + + if (!extracted || extracted === "{}") { + setCommonConfigError(t("geminiConfig.extractNoCommonConfig")); + return; + } + + // 验证 JSON 格式 + const parsed = parseSnippetEnv(extracted); + if (parsed.error) { + setCommonConfigError(t("geminiConfig.extractedConfigInvalid")); + return; + } + + // 更新片段状态 + setCommonConfigSnippetState(extracted); + + // 保存到后端 + await configApi.setCommonConfigSnippet("gemini", extracted); + } catch (error) { + console.error("提取 Gemini 通用配置失败:", error); + setCommonConfigError( + t("geminiConfig.extractFailed", { error: String(error) }), + ); + } finally { + setIsExtracting(false); + } + }, [envStringToObj, envValue, parseSnippetEnv, t]); + + return { + useCommonConfig, + commonConfigSnippet, + commonConfigError, + isLoading, + isExtracting, + handleCommonConfigToggle, + handleCommonConfigSnippetChange, + handleExtract, + }; +} diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 569b6085a..0fdf6698c 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -316,10 +316,12 @@ export const providerPresets: ProviderPreset[] = [ name: "AiHubMix", websiteUrl: "https://aihubmix.com", apiKeyUrl: "https://aihubmix.com", + // 说明:该供应商使用 ANTHROPIC_API_KEY(而非 ANTHROPIC_AUTH_TOKEN) + apiKeyField: "ANTHROPIC_API_KEY", settingsConfig: { env: { ANTHROPIC_BASE_URL: "https://aihubmix.com", - ANTHROPIC_AUTH_TOKEN: "", + ANTHROPIC_API_KEY: "", }, }, // 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖 diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8984d9f9c..6ed487f3d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -51,8 +51,15 @@ }, "claudeConfig": { "configLabel": "Claude Code settings.json (JSON) *", + "writeCommonConfig": "Write Common Config", + "editCommonConfig": "Edit Common Config", + "editCommonConfigTitle": "Edit Common Config Snippet", + "commonConfigHint": "This snippet will be merged into settings.json when 'Write Common Config' is checked", "fullSettingsHint": "Full Claude Code settings.json content", - "fragmentSettingsHint": "Config snippet for this provider; will be written to settings.json when activated", + "extractFromCurrent": "Extract from Editor", + "extractNoCommonConfig": "No common config available to extract from editor", + "extractFailed": "Extract failed: {{error}}", + "saveFailed": "Save failed: {{error}}", "hideAttribution": "Hide AI Attribution", "alwaysThinking": "Extended Thinking", "enableTeammates": "Teammates Mode" @@ -118,7 +125,11 @@ "notes": "Notes", "notesPlaceholder": "e.g., Company dedicated account", "configJson": "Config JSON", + "writeCommonConfig": "Write common config", + "editCommonConfigButton": "Edit common config", "configJsonHint": "Please fill in complete Claude Code configuration", + "editCommonConfigTitle": "Edit common config snippet", + "editCommonConfigHint": "Common config snippet will be merged into all providers that enable it", "addProvider": "Add Provider", "sortUpdated": "Sort order updated", "usageSaved": "Usage query configuration saved", @@ -669,10 +680,6 @@ "apiFormatHint": "Select the input format for the provider's API", "apiFormatAnthropic": "Anthropic Messages (Native)", "apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)", - "authField": "Auth Field", - "authFieldAuthToken": "Auth Token (Default)", - "authFieldApiKey": "API Key", - "authFieldHint": "Most third-party providers use Auth Token; a few require API Key", "anthropicDefaultHaikuModel": "Default Haiku Model", "anthropicDefaultSonnetModel": "Default Sonnet Model", "anthropicDefaultOpusModel": "Default Opus Model", @@ -744,15 +751,33 @@ "authJsonHint": "Codex auth.json configuration content", "configToml": "config.toml (TOML)", "configTomlHint": "Codex config.toml configuration content", - "apiUrlLabel": "API Request URL" + "writeCommonConfig": "Write Common Config", + "editCommonConfig": "Edit Common Config", + "editCommonConfigTitle": "Edit Codex Common Config Snippet", + "commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked", + "apiUrlLabel": "API Request URL", + "extractFromCurrent": "Extract from Editor", + "extractNoCommonConfig": "No common config available to extract from editor", + "extractFailed": "Extract failed: {{error}}", + "saveFailed": "Save failed: {{error}}" }, "geminiConfig": { "envFile": "Environment Variables (.env)", "envFileHint": "Configure Gemini environment variables in .env format", "configJson": "Configuration File (config.json)", "configJsonHint": "Configure Gemini extended parameters in JSON format (optional)", + "writeCommonConfig": "Write Common Config", + "editCommonConfig": "Edit Common Config", + "editCommonConfigTitle": "Edit Gemini Common Config Snippet", + "commonConfigHint": "This snippet writes to Gemini .env (GOOGLE_GEMINI_BASE_URL and GEMINI_API_KEY are not allowed)", + "extractFromCurrent": "Extract from Editor", + "extractNoCommonConfig": "No common config available to extract from editor", + "extractFailed": "Extract failed: {{error}}", + "saveFailed": "Save failed: {{error}}", "extractedConfigInvalid": "Extracted config format is invalid", "invalidJsonFormat": "Common config snippet format error (must be valid JSON)", + "commonConfigInvalidKeys": "Common config snippet must not include GOOGLE_GEMINI_BASE_URL or GEMINI_API_KEY (found: {{keys}})", + "commonConfigInvalidValues": "Common config snippet values must be strings", "noCommonConfigToApply": "Common config snippet is empty or has no applicable entries", "configMergeFailed": "Config merge failed: {{error}}", "configReplaceFailed": "Config replace failed: {{error}}" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 7da0b0e87..0b985a535 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -51,8 +51,15 @@ }, "claudeConfig": { "configLabel": "Claude Code settings.json (JSON) *", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfig": "共通設定を編集", + "editCommonConfigTitle": "共通設定スニペットを編集", + "commonConfigHint": "「共通設定を書き込む」がオンのとき settings.json にマージされます", "fullSettingsHint": "Claude Code の settings.json 全文", - "fragmentSettingsHint": "このプロバイダーの設定スニペット。有効化時に settings.json に書き込まれます", + "extractFromCurrent": "編集内容から抽出", + "extractNoCommonConfig": "編集内容から抽出できる共通設定がありません", + "extractFailed": "抽出に失敗しました: {{error}}", + "saveFailed": "保存に失敗しました: {{error}}", "hideAttribution": "AI署名を非表示", "alwaysThinking": "拡張思考", "enableTeammates": "Teammates モード" @@ -118,7 +125,11 @@ "notes": "メモ", "notesPlaceholder": "例: 会社用アカウント", "configJson": "Config JSON", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfigButton": "共通設定を編集", "configJsonHint": "Claude Code の設定をすべて入力してください", + "editCommonConfigTitle": "共通設定スニペットを編集", + "editCommonConfigHint": "共通設定スニペットは、この機能をオンにしたすべてのプロバイダーへマージされます", "addProvider": "プロバイダーを追加", "sortUpdated": "並び順を更新しました", "usageSaved": "利用状況の設定を保存しました", @@ -669,10 +680,6 @@ "apiFormatHint": "プロバイダー API の入力フォーマットを選択", "apiFormatAnthropic": "Anthropic Messages(ネイティブ)", "apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)", - "authField": "認証フィールド", - "authFieldAuthToken": "Auth Token(デフォルト)", - "authFieldApiKey": "API Key", - "authFieldHint": "ほとんどのサードパーティプロバイダーは Auth Token を使用します。一部は API Key が必要です", "anthropicDefaultHaikuModel": "既定 Haiku モデル", "anthropicDefaultSonnetModel": "既定 Sonnet モデル", "anthropicDefaultOpusModel": "既定 Opus モデル", @@ -744,15 +751,33 @@ "authJsonHint": "Codex の auth.json 設定内容", "configToml": "config.toml (TOML)", "configTomlHint": "Codex の config.toml 設定内容", - "apiUrlLabel": "API リクエスト URL" + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfig": "共通設定を編集", + "editCommonConfigTitle": "Codex 共通設定スニペットを編集", + "commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます", + "apiUrlLabel": "API リクエスト URL", + "extractFromCurrent": "編集内容から抽出", + "extractNoCommonConfig": "編集内容から抽出できる共通設定がありません", + "extractFailed": "抽出に失敗しました: {{error}}", + "saveFailed": "保存に失敗しました: {{error}}" }, "geminiConfig": { "envFile": "環境変数 (.env)", "envFileHint": ".env 形式で Gemini の環境変数を設定", "configJson": "設定ファイル (config.json)", "configJsonHint": "Gemini 拡張パラメーターを JSON 形式で設定(任意)", + "writeCommonConfig": "共通設定を書き込む", + "editCommonConfig": "共通設定を編集", + "editCommonConfigTitle": "Gemini 共通設定スニペットを編集", + "commonConfigHint": "このスニペットは Gemini の .env に書き込みます(GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY は使用できません)", + "extractFromCurrent": "編集内容から抽出", + "extractNoCommonConfig": "編集内容から抽出できる共通設定がありません", + "extractFailed": "抽出に失敗しました: {{error}}", + "saveFailed": "保存に失敗しました: {{error}}", "extractedConfigInvalid": "抽出した設定のフォーマットが不正です", "invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)", + "commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}})", + "commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります", "noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません", "configMergeFailed": "設定のマージに失敗しました: {{error}}", "configReplaceFailed": "設定の置換に失敗しました: {{error}}" diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 0f58f920a..d4351bdf6 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -51,8 +51,15 @@ }, "claudeConfig": { "configLabel": "Claude Code 配置 (JSON) *", + "writeCommonConfig": "写入通用配置", + "editCommonConfig": "编辑通用配置", + "editCommonConfigTitle": "编辑通用配置片段", + "commonConfigHint": "该片段会在勾选\"写入通用配置\"时合并到 settings.json 中", "fullSettingsHint": "完整的 Claude Code settings.json 配置内容", - "fragmentSettingsHint": "此供应商的配置片段,激活后将写入 settings.json", + "extractFromCurrent": "从编辑内容提取", + "extractNoCommonConfig": "当前编辑内容没有可提取的通用配置", + "extractFailed": "提取失败: {{error}}", + "saveFailed": "保存失败: {{error}}", "hideAttribution": "隐藏 AI 署名", "alwaysThinking": "扩展思考", "enableTeammates": "Teammates 模式" @@ -118,7 +125,11 @@ "notes": "备注", "notesPlaceholder": "例如:公司专用账号", "configJson": "配置 JSON", + "writeCommonConfig": "写入通用配置", + "editCommonConfigButton": "编辑通用配置", "configJsonHint": "请填写完整的 Claude Code 配置", + "editCommonConfigTitle": "编辑通用配置片段", + "editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中", "addProvider": "添加供应商", "sortUpdated": "排序已更新", "usageSaved": "用量查询配置已保存", @@ -669,10 +680,6 @@ "apiFormatHint": "选择供应商 API 的输入格式", "apiFormatAnthropic": "Anthropic Messages (原生)", "apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)", - "authField": "认证字段", - "authFieldAuthToken": "Auth Token (默认)", - "authFieldApiKey": "API Key", - "authFieldHint": "大多数第三方供应商使用 Auth Token;少数供应商需要 API Key", "anthropicDefaultHaikuModel": "Haiku 默认模型", "anthropicDefaultSonnetModel": "Sonnet 默认模型", "anthropicDefaultOpusModel": "Opus 默认模型", @@ -744,15 +751,33 @@ "authJsonHint": "Codex auth.json 配置内容", "configToml": "config.toml (TOML)", "configTomlHint": "Codex config.toml 配置内容", - "apiUrlLabel": "API 请求地址" + "writeCommonConfig": "写入通用配置", + "editCommonConfig": "编辑通用配置", + "editCommonConfigTitle": "编辑 Codex 通用配置片段", + "commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾", + "apiUrlLabel": "API 请求地址", + "extractFromCurrent": "从编辑内容提取", + "extractNoCommonConfig": "当前编辑内容没有可提取的通用配置", + "extractFailed": "提取失败: {{error}}", + "saveFailed": "保存失败: {{error}}" }, "geminiConfig": { "envFile": "环境变量 (.env)", "envFileHint": "使用 .env 格式配置 Gemini 环境变量", "configJson": "配置文件 (config.json)", "configJsonHint": "使用 JSON 格式配置 Gemini 扩展参数(可选)", + "writeCommonConfig": "写入通用配置", + "editCommonConfig": "编辑通用配置", + "editCommonConfigTitle": "编辑 Gemini 通用配置片段", + "commonConfigHint": "该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY)", + "extractFromCurrent": "从编辑内容提取", + "extractNoCommonConfig": "当前编辑内容没有可提取的通用配置", + "extractFailed": "提取失败: {{error}}", + "saveFailed": "保存失败: {{error}}", "extractedConfigInvalid": "提取的配置格式错误", "invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)", + "commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}})", + "commonConfigInvalidValues": "通用配置片段的值必须是字符串", "noCommonConfigToApply": "通用配置片段为空或没有可写入的内容", "configMergeFailed": "配置合并失败: {{error}}", "configReplaceFailed": "配置替换失败: {{error}}" diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts new file mode 100644 index 000000000..1fbbb30cf --- /dev/null +++ b/src/lib/api/config.ts @@ -0,0 +1,77 @@ +// 配置相关 API +import { invoke } from "@tauri-apps/api/core"; + +export type AppType = "claude" | "codex" | "gemini" | "omo" | "omo_slim"; + +/** + * 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet) + * @returns 通用配置片段(JSON 字符串),如果不存在则返回 null + * @deprecated 使用 getCommonConfigSnippet('claude') 替代 + */ +export async function getClaudeCommonConfigSnippet(): Promise { + return invoke("get_claude_common_config_snippet"); +} + +/** + * 设置 Claude 通用配置片段(已废弃,使用 setCommonConfigSnippet) + * @param snippet - 通用配置片段(JSON 字符串) + * @throws 如果 JSON 格式无效 + * @deprecated 使用 setCommonConfigSnippet('claude', snippet) 替代 + */ +export async function setClaudeCommonConfigSnippet( + snippet: string, +): Promise { + return invoke("set_claude_common_config_snippet", { snippet }); +} + +/** + * 获取通用配置片段(统一接口) + * @param appType - 应用类型(claude/codex/gemini) + * @returns 通用配置片段(原始字符串),如果不存在则返回 null + */ +export async function getCommonConfigSnippet( + appType: AppType, +): Promise { + return invoke("get_common_config_snippet", { appType }); +} + +/** + * 设置通用配置片段(统一接口) + * @param appType - 应用类型(claude/codex/gemini) + * @param snippet - 通用配置片段(原始字符串) + * @throws 如果格式无效(Claude/Gemini 验证 JSON,Codex 暂不验证) + */ +export async function setCommonConfigSnippet( + appType: AppType, + snippet: string, +): Promise { + return invoke("set_common_config_snippet", { appType, snippet }); +} + +/** + * 提取通用配置片段 + * + * 默认读取当前激活供应商的配置;若传入 `options.settingsConfig`,则从编辑器当前内容提取。 + * 会自动排除差异化字段(API Key、模型配置、端点等),返回可复用的通用配置片段。 + * + * @param appType - 应用类型(claude/codex/gemini) + * @param options - 可选:提取来源 + * @returns 提取的通用配置片段(JSON/TOML 字符串) + */ +export type ExtractCommonConfigSnippetOptions = { + settingsConfig?: string; +}; + +export async function extractCommonConfigSnippet( + appType: Exclude, + options?: ExtractCommonConfigSnippetOptions, +): Promise { + const args: Record = { appType }; + const settingsConfig = options?.settingsConfig; + + if (typeof settingsConfig === "string" && settingsConfig.trim()) { + args.settingsConfig = settingsConfig; + } + + return invoke("extract_common_config_snippet", args); +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 1087b293f..bf00f1617 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -11,5 +11,6 @@ export { proxyApi } from "./proxy"; export { openclawApi } from "./openclaw"; export { sessionsApi } from "./sessions"; export { workspaceApi } from "./workspace"; +export * as configApi from "./config"; export type { ProviderSwitchEvent } from "./providers"; export type { Prompt } from "./prompts"; diff --git a/src/types.ts b/src/types.ts index a6c769e61..eda850887 100644 --- a/src/types.ts +++ b/src/types.ts @@ -146,10 +146,6 @@ export interface ProviderMeta { // - "anthropic": 原生 Anthropic Messages API 格式,直接透传 // - "openai_chat": OpenAI Chat Completions 格式,需要格式转换 apiFormat?: "anthropic" | "openai_chat"; - // Claude 认证字段名(仅 Claude 供应商使用) - // - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商 - // - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key - apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY"; } // Skill 同步方式 @@ -160,11 +156,6 @@ export type SkillSyncMethod = "auto" | "symlink" | "copy"; // - "openai_chat": OpenAI Chat Completions 格式,需要格式转换 export type ClaudeApiFormat = "anthropic" | "openai_chat"; -// Claude 认证字段类型 -// - "ANTHROPIC_AUTH_TOKEN": 大多数第三方/聚合供应商使用(默认) -// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key -export type ClaudeApiKeyField = "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY"; - // 主页面显示的应用配置 export interface VisibleApps { claude: boolean; diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index 7d56d4025..059140888 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -3,6 +3,86 @@ import type { TemplateValueConfig } from "../config/claudeProviderPresets"; import { normalizeQuotes } from "@/utils/textNormalization"; +const isPlainObject = (value: unknown): value is Record => { + return Object.prototype.toString.call(value) === "[object Object]"; +}; + +const deepMerge = ( + target: Record, + source: Record, +): Record => { + Object.entries(source).forEach(([key, value]) => { + if (isPlainObject(value)) { + if (!isPlainObject(target[key])) { + target[key] = {}; + } + deepMerge(target[key], value); + } else { + // 直接覆盖非对象字段(数组/基础类型) + target[key] = value; + } + }); + return target; +}; + +const deepRemove = ( + target: Record, + source: Record, +) => { + Object.entries(source).forEach(([key, value]) => { + if (!(key in target)) return; + + if (isPlainObject(value) && isPlainObject(target[key])) { + // 只移除完全匹配的嵌套属性 + deepRemove(target[key], value); + if (Object.keys(target[key]).length === 0) { + delete target[key]; + } + } else if (isSubset(target[key], value)) { + // 只有当值完全匹配时才删除 + delete target[key]; + } + }); +}; + +const isSubset = (target: any, source: any): boolean => { + if (isPlainObject(source)) { + if (!isPlainObject(target)) return false; + return Object.entries(source).every(([key, value]) => + isSubset(target[key], value), + ); + } + + if (Array.isArray(source)) { + if (!Array.isArray(target) || target.length !== source.length) return false; + return source.every((item, index) => isSubset(target[index], item)); + } + + return target === source; +}; + +// 深拷贝函数 +const deepClone = (obj: T): T => { + if (obj === null || typeof obj !== "object") return obj; + if (obj instanceof Date) return new Date(obj.getTime()) as T; + if (obj instanceof Array) return obj.map((item) => deepClone(item)) as T; + if (obj instanceof Object) { + const clonedObj = {} as T; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + clonedObj[key] = deepClone(obj[key]); + } + } + return clonedObj; + } + return obj; +}; + +export interface UpdateCommonConfigResult { + updatedConfig: string; + error?: string; +} + // 验证JSON配置格式 export const validateJsonConfig = ( value: string, @@ -22,6 +102,69 @@ export const validateJsonConfig = ( } }; +// 将通用配置片段写入/移除 settingsConfig +export const updateCommonConfigSnippet = ( + jsonString: string, + snippetString: string, + enabled: boolean, +): UpdateCommonConfigResult => { + let config: Record; + try { + config = jsonString ? JSON.parse(jsonString) : {}; + } catch (err) { + return { + updatedConfig: jsonString, + error: "配置 JSON 解析失败,无法写入通用配置", + }; + } + + if (!snippetString.trim()) { + return { + updatedConfig: JSON.stringify(config, null, 2), + }; + } + + // 使用统一的验证函数 + const snippetError = validateJsonConfig(snippetString, "通用配置片段"); + if (snippetError) { + return { + updatedConfig: JSON.stringify(config, null, 2), + error: snippetError, + }; + } + + const snippet = JSON.parse(snippetString) as Record; + + if (enabled) { + const merged = deepMerge(deepClone(config), snippet); + return { + updatedConfig: JSON.stringify(merged, null, 2), + }; + } + + const cloned = deepClone(config); + deepRemove(cloned, snippet); + return { + updatedConfig: JSON.stringify(cloned, null, 2), + }; +}; + +// 检查当前配置是否已包含通用配置片段 +export const hasCommonConfigSnippet = ( + jsonString: string, + snippetString: string, +): boolean => { + try { + if (!snippetString.trim()) return false; + const config = jsonString ? JSON.parse(jsonString) : {}; + const snippet = JSON.parse(snippetString); + if (!isPlainObject(snippet)) return false; + return isSubset(config, snippet); + } catch (err) { + return false; + } +}; + // 读取配置中的 API Key(支持 Claude, Codex, Gemini) export const getApiKeyFromConfig = ( jsonString: string, @@ -151,13 +294,9 @@ export const hasApiKeyField = ( export const setApiKeyInConfig = ( jsonString: string, apiKey: string, - options: { - createIfMissing?: boolean; - appType?: string; - apiKeyField?: string; - } = {}, + options: { createIfMissing?: boolean; appType?: string } = {}, ): string => { - const { createIfMissing = false, appType, apiKeyField } = options; + const { createIfMissing = false, appType } = options; try { const config = JSON.parse(jsonString); @@ -197,13 +336,13 @@ export const setApiKeyInConfig = ( return JSON.stringify(config, null, 2); } - // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 指定的字段名) + // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段) if ("ANTHROPIC_AUTH_TOKEN" in env) { env.ANTHROPIC_AUTH_TOKEN = apiKey; } else if ("ANTHROPIC_API_KEY" in env) { env.ANTHROPIC_API_KEY = apiKey; } else if (createIfMissing) { - env[apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"] = apiKey; + env.ANTHROPIC_AUTH_TOKEN = apiKey; } else { return jsonString; } @@ -213,6 +352,85 @@ export const setApiKeyInConfig = ( } }; +// ========== TOML Config Utilities ========== + +export interface UpdateTomlCommonConfigResult { + updatedConfig: string; + error?: string; +} + +// 保存之前的通用配置片段,用于替换操作 +let previousCommonSnippet = ""; + +// 将通用配置片段写入/移除 TOML 配置 +export const updateTomlCommonConfigSnippet = ( + tomlString: string, + snippetString: string, + enabled: boolean, +): UpdateTomlCommonConfigResult => { + if (!snippetString.trim()) { + // 如果片段为空,直接返回原始配置 + return { + updatedConfig: tomlString, + }; + } + + if (enabled) { + // 添加通用配置 + // 先移除旧的通用配置(如果有) + let updatedConfig = tomlString; + if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) { + updatedConfig = tomlString.replace(previousCommonSnippet, ""); + } + + // 在文件末尾添加新的通用配置 + // 确保有适当的换行 + const needsNewline = updatedConfig && !updatedConfig.endsWith("\n"); + updatedConfig = + updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString; + + // 保存当前通用配置片段 + previousCommonSnippet = snippetString; + + return { + updatedConfig: updatedConfig.trim() + "\n", + }; + } else { + // 移除通用配置 + if (tomlString.includes(snippetString)) { + const updatedConfig = tomlString.replace(snippetString, ""); + // 清理多余的空行 + const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim(); + + // 清空保存的状态 + previousCommonSnippet = ""; + + return { + updatedConfig: cleaned ? cleaned + "\n" : "", + }; + } + return { + updatedConfig: tomlString, + }; + } +}; + +// 检查 TOML 配置是否已包含通用配置片段 +export const hasTomlCommonConfigSnippet = ( + tomlString: string, + snippetString: string, +): boolean => { + if (!snippetString.trim()) return false; + + // 简单检查配置是否包含片段内容 + // 去除空白字符后比较,避免格式差异影响 + const normalizeWhitespace = (str: string) => str.replace(/\s+/g, " ").trim(); + + return normalizeWhitespace(tomlString).includes( + normalizeWhitespace(snippetString), + ); +}; + // ========== Codex base_url utils ========== // 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号) From f8c1f1736ec42f33c6c3181152054e09007d1af0 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 00:12:12 +0800 Subject: [PATCH 03/34] fix: disable env check and one-click install on Windows to prevent protocol handler side effects --- src-tauri/src/commands/misc.rs | 51 ++-- src/components/settings/AboutSection.tsx | 313 ++++++++++++----------- 2 files changed, 193 insertions(+), 171 deletions(-) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index b4dd12d93..3d78534f1 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -133,27 +133,40 @@ pub async fn get_tool_versions( tools: Option>, wsl_shell_by_tool: Option>, ) -> Result, String> { - let requested: Vec<&str> = if let Some(tools) = tools.as_ref() { - let set: std::collections::HashSet<&str> = tools.iter().map(|s| s.as_str()).collect(); - VALID_TOOLS - .iter() - .copied() - .filter(|t| set.contains(t)) - .collect() - } else { - VALID_TOOLS.to_vec() - }; - let mut results = Vec::new(); - - for tool in requested { - let pref = wsl_shell_by_tool.as_ref().and_then(|m| m.get(tool)); - let tool_wsl_shell = pref.and_then(|p| p.wsl_shell.as_deref()); - let tool_wsl_shell_flag = pref.and_then(|p| p.wsl_shell_flag.as_deref()); - - results.push(get_single_tool_version_impl(tool, tool_wsl_shell, tool_wsl_shell_flag).await); + // Windows: completely disable tool version detection to prevent + // accidentally launching apps (e.g. Claude Code) via protocol handlers. + #[cfg(target_os = "windows")] + { + let _ = (tools, wsl_shell_by_tool); + return Ok(Vec::new()); } - Ok(results) + #[cfg(not(target_os = "windows"))] + { + let requested: Vec<&str> = if let Some(tools) = tools.as_ref() { + let set: std::collections::HashSet<&str> = tools.iter().map(|s| s.as_str()).collect(); + VALID_TOOLS + .iter() + .copied() + .filter(|t| set.contains(t)) + .collect() + } else { + VALID_TOOLS.to_vec() + }; + let mut results = Vec::new(); + + for tool in requested { + let pref = wsl_shell_by_tool.as_ref().and_then(|m| m.get(tool)); + let tool_wsl_shell = pref.and_then(|p| p.wsl_shell.as_deref()); + let tool_wsl_shell_flag = pref.and_then(|p| p.wsl_shell_flag.as_deref()); + + results.push( + get_single_tool_version_impl(tool, tool_wsl_shell, tool_wsl_shell_flag).await, + ); + } + + Ok(results) + } } /// 获取单个工具的版本信息(内部实现) diff --git a/src/components/settings/AboutSection.tsx b/src/components/settings/AboutSection.tsx index 343ba52e6..ac0c9ffb2 100644 --- a/src/components/settings/AboutSection.tsx +++ b/src/components/settings/AboutSection.tsx @@ -27,6 +27,7 @@ import { relaunchApp } from "@/lib/updater"; import { Badge } from "@/components/ui/badge"; import { motion } from "framer-motion"; import appIcon from "@/assets/icons/app-icon.png"; +import { isWindows } from "@/lib/platform"; interface AboutSectionProps { isPortable: boolean; @@ -199,7 +200,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) { try { const [appVersion] = await Promise.all([ getVersion(), - loadAllToolVersions(), + ...(isWindows() ? [] : [loadAllToolVersions()]), ]); if (active) { @@ -423,165 +424,173 @@ export function AboutSection({ isPortable }: AboutSectionProps) { )} -
-
-

{t("settings.localEnvCheck")}

- -
- -
- {TOOL_NAMES.map((toolName, index) => { - const tool = toolVersions.find((item) => item.name === toolName); - // Special case for OpenCode (capital C), others use capitalize - const displayName = - toolName === "opencode" - ? "OpenCode" - : toolName.charAt(0).toUpperCase() + toolName.slice(1); - const title = tool?.version || tool?.error || t("common.unknown"); - - return ( - -
-
- - {displayName} - {/* Environment Badge */} - {tool?.env_type && ENV_BADGE_CONFIG[tool.env_type] && ( - - {t(ENV_BADGE_CONFIG[tool.env_type].labelKey)} - - )} - {/* WSL Shell Selector */} - {tool?.env_type === "wsl" && ( - - )} - {/* WSL Shell Flag Selector */} - {tool?.env_type === "wsl" && ( - - )} -
- {isLoadingTools || loadingTools[toolName] ? ( - - ) : tool?.version ? ( - tool.latest_version && - tool.version !== tool.latest_version ? ( - - {tool.latest_version} - - ) : ( - - ) - ) : ( - - )} -
-
- {isLoadingTools - ? t("common.loading") - : tool?.version - ? tool.version - : tool?.error || t("common.notInstalled")} -
-
- ); - })} -
-
- - -

- {t("settings.oneClickInstall")} -

-
-
-

- {t("settings.oneClickInstallHint")} -

+ {!isWindows() && ( +
+
+

+ {t("settings.localEnvCheck")} +

-
-            {ONE_CLICK_INSTALL_COMMANDS}
-          
+ +
+ {TOOL_NAMES.map((toolName, index) => { + const tool = toolVersions.find((item) => item.name === toolName); + // Special case for OpenCode (capital C), others use capitalize + const displayName = + toolName === "opencode" + ? "OpenCode" + : toolName.charAt(0).toUpperCase() + toolName.slice(1); + const title = tool?.version || tool?.error || t("common.unknown"); + + return ( + +
+
+ + {displayName} + {/* Environment Badge */} + {tool?.env_type && ENV_BADGE_CONFIG[tool.env_type] && ( + + {t(ENV_BADGE_CONFIG[tool.env_type].labelKey)} + + )} + {/* WSL Shell Selector */} + {tool?.env_type === "wsl" && ( + + )} + {/* WSL Shell Flag Selector */} + {tool?.env_type === "wsl" && ( + + )} +
+ {isLoadingTools || loadingTools[toolName] ? ( + + ) : tool?.version ? ( + tool.latest_version && + tool.version !== tool.latest_version ? ( + + {tool.latest_version} + + ) : ( + + ) + ) : ( + + )} +
+
+ {isLoadingTools + ? t("common.loading") + : tool?.version + ? tool.version + : tool?.error || t("common.notInstalled")} +
+
+ ); + })} +
- + )} + + {!isWindows() && ( + +

+ {t("settings.oneClickInstall")} +

+
+
+

+ {t("settings.oneClickInstallHint")} +

+ +
+
+              {ONE_CLICK_INSTALL_COMMANDS}
+            
+
+
+ )} ); } From d5e4e8d13330927ad0069074cc4899b909c136dc Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 09:13:32 +0800 Subject: [PATCH 04/34] refactor: move proxy toggle into panel and surface app takeover options Move the proxy on/off switch from the accordion header into the panel content area, placing it right above the app takeover section. This ensures users see the takeover options immediately after enabling the proxy, preventing the common pitfall of running the proxy without actually taking over any app. - Simplify accordion trigger to standard style with Badge only - Add AnimatePresence animation for takeover section reveal - Remove duplicate takeover switches from running info card - Update stoppedDescription i18n to reference "above toggle" - Add proxy.takeover.hint key in zh/en/ja --- src/components/proxy/ProxyPanel.tsx | 157 ++++++++++++++------ src/components/settings/ProxyTabContent.tsx | 58 +++----- src/i18n/locales/en.json | 8 +- src/i18n/locales/ja.json | 8 +- src/i18n/locales/zh.json | 8 +- 5 files changed, 153 insertions(+), 86 deletions(-) diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index e874e0cdb..1052f2ece 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -7,11 +7,14 @@ import { ListOrdered, Save, Loader2, + Zap, + Power, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; +import { ToggleRow } from "@/components/ui/toggle-row"; import { useProxyStatus } from "@/hooks/useProxyStatus"; import { toast } from "sonner"; import { useFailoverQueue } from "@/lib/query/failover"; @@ -25,8 +28,21 @@ import { } from "@/lib/query/proxy"; import type { ProxyStatus } from "@/types/proxy"; import { useTranslation } from "react-i18next"; +import { AnimatePresence, motion } from "framer-motion"; -export function ProxyPanel() { +interface ProxyPanelProps { + enableLocalProxy: boolean; + onEnableLocalProxyChange: (checked: boolean) => void; + onToggleProxy: (checked: boolean) => Promise; + isProxyPending: boolean; +} + +export function ProxyPanel({ + enableLocalProxy, + onEnableLocalProxyChange, + onToggleProxy, + isProxyPending, +}: ProxyPanelProps) { const { t } = useTranslation(); const { status, isRunning } = useProxyStatus(); @@ -183,9 +199,98 @@ export function ProxyPanel() { return ( <> -
+
+ {/* [1] Enable proxy button on main page — always visible */} + } + title={t("settings.advanced.proxy.enableFeature")} + description={t("settings.advanced.proxy.enableFeatureDescription")} + checked={enableLocalProxy} + onCheckedChange={onEnableLocalProxyChange} + /> + + {/* [2] Proxy service toggle — always visible */} +
+
+
+ +
+
+

+ {t("proxyConfig.proxyEnabled", { + defaultValue: "代理服务", + })} +

+

+ {isRunning + ? t("settings.advanced.proxy.running") + : t("settings.advanced.proxy.stopped")} +

+
+
+ +
+ + {/* [3] App takeover switches — animated, visible only when proxy is running */} + + {isRunning && ( + +
+

+ {t("proxyConfig.appTakeover", { + defaultValue: "应用接管", + })} +

+
+ {(["claude", "codex", "gemini"] as const).map((appType) => { + const isEnabled = + takeoverStatus?.[ + appType as keyof typeof takeoverStatus + ] ?? false; + return ( +
+ + {appType} + + + handleTakeoverChange(appType, checked) + } + disabled={setTakeoverForApp.isPending} + /> +
+ ); + })} +
+

+ {t("proxy.takeover.hint", { + defaultValue: + "选择要接管的应用,启用后该应用的请求将通过本地代理转发", + })} +

+
+
+ )} +
+ + {/* Running state: service info + stats */} {isRunning && status ? (
+ {/* [4] Running info: address + current provider */}

@@ -263,41 +368,7 @@ export function ProxyPanel() { )}

- {/* 应用接管开关 */} -
-

- {t("proxyConfig.appTakeover", { - defaultValue: "应用接管", - })} -

-
- {(["claude", "codex", "gemini"] as const).map((appType) => { - const isEnabled = - takeoverStatus?.[ - appType as keyof typeof takeoverStatus - ] ?? false; - return ( -
- - {appType} - - - handleTakeoverChange(appType, checked) - } - disabled={setTakeoverForApp.isPending} - /> -
- ); - })} -
-
- - {/* 日志记录开关 */} + {/* [5] Logging toggle */}
@@ -320,7 +391,7 @@ export function ProxyPanel() {
- {/* 供应商队列 - 按应用类型分组展示 */} + {/* [6] Provider queues */} {(claudeQueue.length > 0 || codexQueue.length > 0 || geminiQueue.length > 0) && ( @@ -332,7 +403,6 @@ export function ProxyPanel() {

- {/* Claude 队列 */} {claudeQueue.length > 0 && ( )} - {/* Codex 队列 */} {codexQueue.length > 0 && ( )} - {/* Gemini 队列 */} {geminiQueue.length > 0 && ( + {/* [7] Stats cards */}
} @@ -408,7 +477,7 @@ export function ProxyPanel() {
) : (
- {/* 基础设置 - 监听地址/端口 */} + {/* [8] Basic settings — address/port (only when stopped) */}

@@ -496,7 +565,7 @@ export function ProxyPanel() {

- {/* 代理服务已停止提示 */} + {/* Stopped hint */}
@@ -508,7 +577,7 @@ export function ProxyPanel() {

{t("proxy.panel.stoppedDescription", { - defaultValue: "使用右上角开关即可启动服务", + defaultValue: "使用上方开关即可启动服务", })}

diff --git a/src/components/settings/ProxyTabContent.tsx b/src/components/settings/ProxyTabContent.tsx index 825f34b3e..3d99aae35 100644 --- a/src/components/settings/ProxyTabContent.tsx +++ b/src/components/settings/ProxyTabContent.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { Server, Activity, ChevronDown, Zap, Globe } from "lucide-react"; +import { Server, Activity, Zap, Globe } from "lucide-react"; import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import { @@ -10,8 +9,6 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Switch } from "@/components/ui/switch"; -import { ToggleRow } from "@/components/ui/toggle-row"; import { Badge } from "@/components/ui/badge"; import { ProxyPanel } from "@/components/proxy"; import { AutoFailoverConfigPanel } from "@/components/proxy/AutoFailoverConfigPanel"; @@ -76,28 +73,22 @@ export function ProxyTabContent({ {/* Local Proxy */} - - -
- -
-

- {t("settings.advanced.proxy.title")} -

-

- {t("settings.advanced.proxy.description")} -

-
+ +
+ +
+

+ {t("settings.advanced.proxy.title")} +

+

+ {t("settings.advanced.proxy.description")} +

- - - -
-
- + - } - title={t("settings.advanced.proxy.enableFeature")} - description={t( - "settings.advanced.proxy.enableFeatureDescription", - )} - checked={settings?.enableLocalProxy ?? false} - onCheckedChange={(checked) => + onAutoSave({ enableLocalProxy: checked }) } + onToggleProxy={handleToggleProxy} + isProxyPending={isProxyPending} /> -
- -
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6ed487f3d..fccbaa456 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1582,7 +1582,7 @@ "currentProvider": "Current Provider:", "waitingFirstRequest": "Current Provider: Waiting for first request...", "stoppedTitle": "Proxy Service Stopped", - "stoppedDescription": "Use the toggle in the top right to start the service", + "stoppedDescription": "Use the toggle above to start the service", "openSettings": "Configure Proxy Service", "stats": { "activeConnections": "Active Connections", @@ -1673,6 +1673,12 @@ "restartRequired": "Restart proxy service for address or port changes to take effect" }, "switchFailed": "Switch failed: {{error}}", + "takeover": { + "hint": "Select apps to take over — once enabled, requests from that app will be routed through the local proxy", + "enabled": "{{app}} takeover enabled", + "disabled": "{{app}} takeover disabled", + "failed": "Failed to toggle takeover" + }, "failover": { "proxyRequired": "Proxy service must be started to configure failover", "autoSwitch": "Auto Failover", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0b985a535..f6b5508d6 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1582,7 +1582,7 @@ "currentProvider": "現在のプロバイダー:", "waitingFirstRequest": "現在のプロバイダー: 最初のリクエスト待ち...", "stoppedTitle": "プロキシサービス停止中", - "stoppedDescription": "右上のトグルでサービスを開始できます", + "stoppedDescription": "上のトグルでサービスを開始できます", "openSettings": "プロキシサービスを設定", "stats": { "activeConnections": "アクティブ接続", @@ -1673,6 +1673,12 @@ "restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です" }, "switchFailed": "切り替えに失敗しました: {{error}}", + "takeover": { + "hint": "テイクオーバーするアプリを選択します。有効にすると、そのアプリのリクエストはローカルプロキシ経由で転送されます", + "enabled": "{{app}} テイクオーバー有効", + "disabled": "{{app}} テイクオーバー無効", + "failed": "テイクオーバーの切り替えに失敗しました" + }, "failover": { "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります", "autoSwitch": "自動フェイルオーバー", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d4351bdf6..0fe3901c3 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1582,7 +1582,7 @@ "currentProvider": "当前 Provider:", "waitingFirstRequest": "当前 Provider:等待首次请求…", "stoppedTitle": "代理服务已停止", - "stoppedDescription": "使用右上角开关即可启动服务", + "stoppedDescription": "使用上方开关即可启动服务", "openSettings": "配置代理服务", "stats": { "activeConnections": "活跃连接", @@ -1673,6 +1673,12 @@ "restartRequired": "修改地址或端口后需要重启代理服务才能生效" }, "switchFailed": "切换失败: {{error}}", + "takeover": { + "hint": "选择要接管的应用,启用后该应用的请求将通过本地代理转发", + "enabled": "{{app}} 接管已启用", + "disabled": "{{app}} 接管已关闭", + "failed": "切换接管状态失败" + }, "failover": { "proxyRequired": "需要先启动代理服务才能配置故障转移", "autoSwitch": "自动故障转移", From fd836ce70dbb80f769703a11d57c20533adc1ab5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 14:50:51 +0800 Subject: [PATCH 05/34] fix: let "follow system" theme auto-update by delegating to Tauri's native theme tracking Pass "system" to set_window_theme instead of explicitly detecting dark/light, so Tauri uses window.set_theme(None) and the WebView's prefers-color-scheme media query stays in sync with the real OS theme. --- src/components/theme-provider.tsx | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 3a32f46ad..f730241e5 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -113,33 +113,17 @@ export function ThemeProvider({ } }; - // Determine current effective theme + // When "system", pass "system" so Tauri uses None (follows OS theme natively). + // This keeps the WebView's prefers-color-scheme in sync with the real OS theme, + // allowing effect #3's media query listener to fire on system theme changes. if (theme === "system") { - const isDark = - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches; - updateNativeTheme(isDark ? "dark" : "light"); + updateNativeTheme("system"); } else { updateNativeTheme(theme); } - // Listen to system theme changes for native window when in "system" mode - const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - const handleChange = () => { - if (theme === "system" && !isCancelled) { - updateNativeTheme(mediaQuery.matches ? "dark" : "light"); - } - }; - - if (theme === "system") { - mediaQuery.addEventListener("change", handleChange); - } - return () => { isCancelled = true; - if (theme === "system") { - mediaQuery.removeEventListener("change", handleChange); - } }; }, [theme]); From 35a4a1589829529fd798aa8350d5529ea4660630 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 15:33:15 +0800 Subject: [PATCH 06/34] fix: restore flex-1 on toolbarRef to fix compact mode exit After moving ProxyToggle/FailoverToggle outside toolbarRef, the flex-1 class was accidentally left only on the outer wrapper. Without flex-1, toolbarRef.clientWidth reflects content width instead of available space, causing useAutoCompact's exit condition to never trigger. --- src/App.tsx | 479 ++++++++++++++++++++++++++-------------------------- 1 file changed, 244 insertions(+), 235 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f18164b76..8207a9cce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -964,252 +964,261 @@ function App() { )}
-
-
- {currentView === "prompts" && ( - - )} - {currentView === "mcp" && ( - <> - + +
+
+ )} +
+
+ {currentView === "prompts" && ( - - )} - {currentView === "skills" && ( - <> - - - - - )} - {currentView === "skillsDiscovery" && ( - <> - - - - )} - {currentView === "providers" && ( - <> - {activeApp !== "opencode" && - activeApp !== "openclaw" && - settingsData?.enableLocalProxy && ( - <> - -
+ + + + )} + {currentView === "skills" && ( + <> + + + + + )} + {currentView === "skillsDiscovery" && ( + <> + + + + )} + {currentView === "providers" && ( + <> + + +
+ + - -
- - )} + {activeApp === "openclaw" ? ( + <> + + + + + + + ) : ( + <> + + + + + + )} + + +
- - -
- - - {activeApp === "openclaw" ? ( - <> - - - - - - - ) : ( - <> - - - - - - )} - - -
- - - - )} + + + )} +
From c75311e14e578cbc01b9c4565a7f64a56d420b5c Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 15:47:08 +0800 Subject: [PATCH 07/34] fix: pass app interpolation param to proxy takeover toast messages The i18next t() calls for proxy.takeover.enabled/disabled were missing the `app` interpolation parameter, causing {{app}} placeholders in translation strings to render literally instead of showing the app name. --- src/hooks/useProxyStatus.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useProxyStatus.ts b/src/hooks/useProxyStatus.ts index d2faa2a8f..e2cf3c367 100644 --- a/src/hooks/useProxyStatus.ts +++ b/src/hooks/useProxyStatus.ts @@ -108,9 +108,11 @@ export function useProxyStatus() { toast.success( variables.enabled ? t("proxy.takeover.enabled", { + app: appLabel, defaultValue: `已接管 ${appLabel} 配置(请求将走本地代理)`, }) : t("proxy.takeover.disabled", { + app: appLabel, defaultValue: `已恢复 ${appLabel} 配置`, }), { closeButton: true }, From 83fe3402c217862a9e14ed52ba19ddafadb0cbc4 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 16:12:50 +0800 Subject: [PATCH 08/34] chore: bump version to v3.11.1 and add release notes --- CHANGELOG.md | 28 +++++++- README.md | 4 +- docs/release-note-v3.11.1-en.md | 122 ++++++++++++++++++++++++++++++++ docs/release-note-v3.11.1-ja.md | 122 ++++++++++++++++++++++++++++++++ docs/release-note-v3.11.1-zh.md | 122 ++++++++++++++++++++++++++++++++ docs/user-manual/README.md | 6 +- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 9 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 docs/release-note-v3.11.1-en.md create mode 100644 docs/release-note-v3.11.1-ja.md create mode 100644 docs/release-note-v3.11.1-zh.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d25292165..f93cc35d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [3.11.1] - 2026-02-28 + +### Hotfix Release + +This release reverts the Partial Key-Field Merging architecture introduced in v3.11.0, restoring the proven "full config overwrite + Common Config Snippet" mechanism, and fixes several UI and platform compatibility issues. + +**Stats**: 8 commits | 52 files changed | +3,948 insertions | -1,411 deletions + +### Reverted + +- **Restore Full Config Overwrite + Common Config Snippet** (revert 992dda5c): Reverted the partial key-field merging refactoring from v3.11.0 due to critical issues — non-whitelisted custom fields were lost during provider switching, backfill permanently stripped non-key fields from the database, and the whitelist required constant maintenance. Restores full config snapshot write, Common Config Snippet UI and backend commands, and 6 frontend components/hooks + +### Changed + +- **Proxy Panel Layout**: Moved proxy on/off toggle from accordion header into panel content area, placed directly above app takeover options, ensuring users see takeover configuration immediately after enabling the proxy +- **Manual Import for OpenCode/OpenClaw**: Removed auto-import on startup; empty state now shows an "Import Current Config" button, consistent with Claude/Codex/Gemini behavior + +### Fixed + +- **"Follow System" Theme Not Auto-Updating**: Delegated to Tauri's native theme tracking (`set_window_theme(None)`) so the WebView's `prefers-color-scheme` media query stays in sync with OS theme changes +- **Compact Mode Cannot Exit**: Restored `flex-1` on `toolbarRef` so `useAutoCompact`'s exit condition triggers correctly based on available width instead of content width +- **Proxy Takeover Toast Shows {{app}}**: Added missing `app` interpolation parameter to i18next `t()` calls for proxy takeover enabled/disabled messages +- **Windows Protocol Handler Side Effects**: Disabled environment check and one-click install on Windows to prevent unintended protocol handler registration + +--- + ## [3.11.0] - 2026-02-26 ### Feature Release @@ -89,7 +115,7 @@ This release introduces **OpenClaw** as the fifth supported application, a full #### Architecture -- **Partial Key-Field Merging (⚠️ Breaking)**: Provider switching now uses partial key-field merging instead of full config overwrite, preserving user's non-provider settings (plugins, MCP, permissions). The "Common Config Snippet" feature has been removed as it is no longer needed. Removes 6 frontend files and ~150 lines of backend dead code (#1098) +- **Partial Key-Field Merging (⚠️ Breaking, reverted in v3.11.1)**: Provider switching now uses partial key-field merging instead of full config overwrite, preserving user's non-provider settings (plugins, MCP, permissions). The "Common Config Snippet" feature has been removed as it is no longer needed. Removes 6 frontend files and ~150 lines of backend dead code (#1098) - **Manual Import**: Replaced auto-import on startup with manual “Import Current Config” button in empty state, reducing ~47 lines of startup code - **OMO Variant Parameterization**: Eliminated ~250 lines of OMO/OMO Slim code duplication via `OmoVariant` struct with STANDARD/SLIM constants - **OMO Common Config Removal**: Removed the two-layer merge system for OMO common config (-1,733 lines across 21 files) diff --git a/README.md b/README.md index 44f4c54d4..e89785a4f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # All-in-One Assistant for Claude Code, Codex & Gemini CLI -[![Version](https://img.shields.io/badge/version-3.10.2-blue.svg)](https://github.com/farion1231/cc-switch/releases) +[![Version](https://img.shields.io/badge/version-3.11.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest) @@ -80,7 +80,7 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric ## Features -### Current Version: v3.10.2 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.9.0-en.md) +### Current Version: v3.11.1 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.11.1-en.md) **v3.8.0 Major Update (2025-11-28)** diff --git a/docs/release-note-v3.11.1-en.md b/docs/release-note-v3.11.1-en.md new file mode 100644 index 000000000..48ff18156 --- /dev/null +++ b/docs/release-note-v3.11.1-en.md @@ -0,0 +1,122 @@ +# CC Switch v3.11.1 + +> Revert Partial Key-Field Merging, Restore Common Config Snippet & Bug Fixes + +**[中文版 →](release-note-v3.11.1-zh.md) | [日本語版 →](release-note-v3.11.1-ja.md)** + +--- + +## Overview + +CC Switch v3.11.1 is a hotfix release that reverts the **Partial Key-Field Merging** architecture introduced in v3.11.0, restoring the proven "**full config overwrite + Common Config Snippet**" mechanism. It also includes several UI and platform compatibility fixes. + +**Release Date**: 2026-02-28 + +**Update Scale**: 8 commits | 52 files changed | +3,948 / -1,411 lines + +--- + +## Highlights + +- **Restore Full Config Overwrite + Common Config Snippet**: Reverted partial key-field merging due to critical data loss issues; restores full config snapshot write and Common Config Snippet UI +- **Proxy Panel Improvements**: Proxy toggle moved into panel body for better discoverability of takeover options +- **Theme & Compact Mode Fixes**: "Follow System" theme now auto-updates; compact mode exit works correctly +- **Windows Compatibility**: Disabled env check and one-click install to prevent protocol handler side effects + +--- + +## Reverted + +### Restore Full Config Overwrite + Common Config Snippet + +Reverted the partial key-field merging refactoring introduced in v3.11.0 (revert 992dda5c). + +**Why reverted**: The partial key-field merging approach had three critical issues: +1. **Data loss on switch**: Non-whitelisted custom fields were silently dropped during provider switching +2. **Permanent backfill stripping**: Backfill permanently removed non-key fields from the database, causing irreversible data loss +3. **Maintenance burden**: The whitelist of "key fields" required constant maintenance as new config keys were added + +**What's restored**: +- Full config snapshot write on provider switch (predictable, complete overwrite) +- Common Config Snippet UI and backend commands +- 6 frontend components/hooks (3 components + 3 hooks) + +**Migration**: +- If you upgraded to v3.11.0 and your providers lost custom fields, re-import your config or manually re-add the missing fields +- Common Config Snippet is available again — use it to define shared config that should persist across provider switches + +--- + +## Changed + +- **Proxy Panel Layout**: Moved proxy on/off toggle from accordion header into panel content area, placed directly above app takeover options. This ensures users see takeover configuration immediately after enabling the proxy, avoiding the common mistake of enabling the proxy without configuring takeover +- **Manual Import for OpenCode/OpenClaw**: Removed auto-import on startup; empty state now shows an "Import Current Config" button, consistent with Claude/Codex/Gemini behavior + +--- + +## Fixed + +- **"Follow System" Theme Not Auto-Updating**: Delegated to Tauri's native theme tracking (`set_window_theme(None)`) so the WebView's `prefers-color-scheme` media query stays in sync with OS theme changes +- **Compact Mode Cannot Exit**: Restored `flex-1` on `toolbarRef` so `useAutoCompact`'s exit condition triggers correctly based on available width instead of content width +- **Proxy Takeover Toast Shows {{app}}**: Added missing `app` interpolation parameter to i18next `t()` calls for proxy takeover enabled/disabled messages +- **Windows Protocol Handler Side Effects**: Disabled environment check and one-click install on Windows to prevent unintended protocol handler registration + +--- + +## Notes & Considerations + +- **Common Config Snippet is back**: If you relied on this feature in v3.10.x and earlier, it works the same way again. Define shared config that should persist across all provider switches. +- **v3.11.0 Partial Key-Field Merging users**: If you noticed missing config fields after switching providers in v3.11.0, re-import your config to restore them. + +--- + +## Download & Installation + +Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the appropriate version. + +### System Requirements + +| System | Minimum Version | Architecture | +| ------- | ------------------------------- | ----------------------------------- | +| Windows | Windows 10 or later | x64 | +| macOS | macOS 10.15 (Catalina) or later | Intel (x64) / Apple Silicon (arm64) | +| Linux | See table below | x64 | + +### Windows + +| File | Description | +| ---------------------------------------- | ---------------------------------------------------- | +| `CC-Switch-v3.11.1-Windows.msi` | **Recommended** - MSI installer with auto-update | +| `CC-Switch-v3.11.1-Windows-Portable.zip` | Portable version, extract and run, no registry write | + +### macOS + +| File | Description | +| -------------------------------- | -------------------------------------------------------------------- | +| `CC-Switch-v3.11.1-macOS.zip` | **Recommended** - Extract and drag to Applications, Universal Binary | +| `CC-Switch-v3.11.1-macOS.tar.gz` | For Homebrew installation and auto-update | + +> **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it, then go to "System Settings" → "Privacy & Security" → click "Open Anyway", and it will open normally afterwards. + +### Homebrew (macOS) + +```bash +brew tap farion1231/ccswitch +brew install --cask cc-switch +``` + +Update: + +```bash +brew upgrade --cask cc-switch +``` + +### Linux + +| Distribution | Recommended Format | Installation Method | +| --------------------------------------- | ------------------ | ---------------------------------------------------------------------- | +| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` or `sudo apt install ./CC-Switch-*.deb` | +| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` or `sudo dnf install ./CC-Switch-*.rpm` | +| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` | +| Arch Linux / Manjaro | `.AppImage` | Add execute permission and run directly, or use AUR | +| Other distributions / Unsure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` | diff --git a/docs/release-note-v3.11.1-ja.md b/docs/release-note-v3.11.1-ja.md new file mode 100644 index 000000000..3d4aa862e --- /dev/null +++ b/docs/release-note-v3.11.1-ja.md @@ -0,0 +1,122 @@ +# CC Switch v3.11.1 + +> 部分キーフィールドマージの撤回、共通設定スニペットの復元とバグ修正 + +**[中文版 →](release-note-v3.11.1-zh.md) | [English →](release-note-v3.11.1-en.md)** + +--- + +## 概要 + +CC Switch v3.11.1 は修正リリースです。v3.11.0 で導入された**部分キーフィールドマージ**アーキテクチャを撤回し、実績のある「**完全設定上書き + 共通設定スニペット**」メカニズムを復元しました。また、複数の UI とプラットフォーム互換性の問題を修正しています。 + +**リリース日**: 2026-02-28 + +**更新規模**: 8 commits | 52 files changed | +3,948 / -1,411 lines + +--- + +## ハイライト + +- **完全設定上書き + 共通設定スニペットの復元**: 重大なデータ損失問題のため部分キーフィールドマージを撤回、完全設定スナップショット書き込みと共通設定スニペット UI を復元 +- **プロキシパネルの改善**: プロキシトグルをパネル本体に移動し、テイクオーバーオプションの発見性を向上 +- **テーマとコンパクトモードの修正**: 「システムに従う」テーマが正しく自動更新、コンパクトモードの終了が正常に動作 +- **Windows 互換性**: プロトコルハンドラーの副作用を防ぐため、環境チェックとワンクリックインストールを無効化 + +--- + +## 撤回 + +### 完全設定上書き + 共通設定スニペットの復元 + +v3.11.0 で導入された部分キーフィールドマージリファクタリングを撤回しました(revert 992dda5c)。 + +**撤回理由**: 部分キーフィールドマージのアプローチには3つの重大な問題がありました: +1. **切り替え時のデータ損失**: ホワイトリストにないカスタムフィールドがプロバイダー切り替え時にサイレントに破棄された +2. **バックフィルによる永続的な剥離**: バックフィル操作がデータベースから非キーフィールドを永続的に削除し、不可逆なデータ損失を引き起こした +3. **メンテナンス負担**: 「キーフィールド」のホワイトリストは新しい設定キーが追加されるたびに継続的なメンテナンスが必要 + +**復元された内容**: +- プロバイダー切り替え時の完全設定スナップショット書き込み(予測可能な完全上書き) +- 共通設定スニペット UI およびバックエンドコマンド +- 6つのフロントエンドファイル(コンポーネント 3つ + hooks 3つ) + +**移行ガイド**: +- v3.11.0 にアップグレードしてプロバイダーのカスタムフィールドが失われた場合は、設定を再インポートするか、欠落したフィールドを手動で追加してください +- 共通設定スニペット機能が再び利用可能です — プロバイダー切り替え時に保持すべき共有設定を定義するために使用してください + +--- + +## 変更 + +- **プロキシパネルレイアウト**: プロキシのオン/オフトグルをアコーディオンヘッダーからパネルのコンテンツエリアに移動し、アプリテイクオーバーオプションの直上に配置。プロキシを有効にした後すぐにテイクオーバー設定が見えるようになり、「プロキシだけ有効にしてテイクオーバーを設定しない」というよくある誤操作を防止 +- **OpenCode/OpenClaw の手動インポート**: 起動時の自動インポートを削除。空の状態ページに「現在の設定をインポート」ボタンを表示し、Claude/Codex/Gemini と同じ動作に統一 + +--- + +## 修正 + +- **「システムに従う」テーマが自動更新されない**: Tauri のネイティブテーマ追跡(`set_window_theme(None)`)に委譲し、WebView の `prefers-color-scheme` メディアクエリが OS テーマの変更に同期するように修正 +- **コンパクトモードを終了できない**: `toolbarRef` の `flex-1` を復元し、`useAutoCompact` の終了条件がコンテンツ幅ではなく利用可能な幅に基づいて正しくトリガーされるように修正 +- **プロキシテイクオーバー Toast に {{app}} が表示される**: プロキシテイクオーバーの有効/無効メッセージの i18next `t()` 呼び出しに欠落していた `app` 補間パラメータを追加 +- **Windows プロトコルハンドラーの副作用**: 意図しないプロトコルハンドラー登録を防ぐため、Windows で環境チェックとワンクリックインストールを無効化 + +--- + +## 注意事項 + +- **共通設定スニペットが復活しました**: v3.10.x 以前でこの機能を使用していた場合、同じ方法で動作します。プロバイダー切り替え時に保持すべき共有設定を定義するために使用してください。 +- **v3.11.0 部分キーフィールドマージユーザーの方へ**: v3.11.0 でプロバイダー切り替え後に設定フィールドが欠落していた場合は、設定を再インポートして復元してください。 + +--- + +## ダウンロードとインストール + +[Releases](https://github.com/farion1231/cc-switch/releases/latest) から適切なバージョンをダウンロードしてください。 + +### システム要件 + +| システム | 最小バージョン | アーキテクチャ | +| -------- | -------------------------------- | ----------------------------------- | +| Windows | Windows 10 以降 | x64 | +| macOS | macOS 10.15 (Catalina) 以降 | Intel (x64) / Apple Silicon (arm64) | +| Linux | 下表参照 | x64 | + +### Windows + +| ファイル | 説明 | +| ---------------------------------------- | ---------------------------------------------------- | +| `CC-Switch-v3.11.1-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 | +| `CC-Switch-v3.11.1-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ書き込みなし | + +### macOS + +| ファイル | 説明 | +| -------------------------------- | ----------------------------------------------------------------- | +| `CC-Switch-v3.11.1-macOS.zip` | **推奨** - 解凍して Applications にドラッグ、Universal Binary | +| `CC-Switch-v3.11.1-macOS.tar.gz` | Homebrew インストールと自動更新用 | + +> **注意**: 作者が Apple Developer アカウントを持っていないため、初回起動時に「開発元を確認できません」という警告が表示される場合があります。一度閉じてから、「システム設定」→「プライバシーとセキュリティ」→「このまま開く」をクリックすると、その後は正常に開けます。 + +### Homebrew (macOS) + +```bash +brew tap farion1231/ccswitch +brew install --cask cc-switch +``` + +更新: + +```bash +brew upgrade --cask cc-switch +``` + +### Linux + +| ディストリビューション | 推奨形式 | インストール方法 | +| --------------------------------------- | ----------- | ---------------------------------------------------------------------- | +| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` または `sudo apt install ./CC-Switch-*.deb` | +| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` または `sudo dnf install ./CC-Switch-*.rpm` | +| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` | +| Arch Linux / Manjaro | `.AppImage` | 実行権限を追加して直接実行、または AUR を使用 | +| その他のディストリビューション / 不明 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` | diff --git a/docs/release-note-v3.11.1-zh.md b/docs/release-note-v3.11.1-zh.md new file mode 100644 index 000000000..9c2dc1590 --- /dev/null +++ b/docs/release-note-v3.11.1-zh.md @@ -0,0 +1,122 @@ +# CC Switch v3.11.1 + +> 回退部分键值合并、恢复通用配置片段与多项修复 + +**[English →](release-note-v3.11.1-en.md) | [日本語版 →](release-note-v3.11.1-ja.md)** + +--- + +## 概览 + +CC Switch v3.11.1 是一个修复版本,回退了 v3.11.0 中引入的**部分键值合并**架构,恢复经过验证的「**全量配置覆写 + 通用配置片段**」机制,同时修复了多个 UI 和平台兼容性问题。 + +**发布日期**:2026-02-28 + +**更新规模**:8 commits | 52 files changed | +3,948 / -1,411 lines + +--- + +## 重点内容 + +- **恢复全量配置覆写 + 通用配置片段**:因关键数据丢失问题回退部分键值合并,恢复完整配置快照写入和通用配置片段 UI +- **代理面板交互优化**:代理开关移入面板内部,接管选项一目了然 +- **主题与紧凑模式修复**:「跟随系统」主题现可正确自动更新,紧凑模式退出恢复正常 +- **Windows 兼容性**:禁用环境检查和一键安装,防止协议处理程序副作用 + +--- + +## 回退 + +### 恢复全量配置覆写 + 通用配置片段 + +回退了 v3.11.0 中引入的部分键值合并重构(revert 992dda5c)。 + +**回退原因**:部分键值合并方案存在三个关键缺陷: +1. **切换时数据丢失**:非白名单的自定义字段在供应商切换时被静默丢弃 +2. **回填永久剥离**:回填操作永久移除数据库中的非键字段,造成不可逆的数据丢失 +3. **维护成本高**:「键字段」白名单需要随新配置项不断维护,容易遗漏 + +**恢复的内容**: +- 供应商切换时的完整配置快照写入(可预测的全量覆写) +- 通用配置片段 UI 及后端命令 +- 6 个前端文件(3 个组件 + 3 个 hooks) + +**迁移说明**: +- 如果你在 v3.11.0 中切换供应商后丢失了自定义字段,请重新导入配置或手动补回缺失的字段 +- 通用配置片段功能已恢复——用它来定义切换供应商时需要保留的共享配置 + +--- + +## 变更 + +- **代理面板交互优化**:将代理开关从折叠面板标题移入面板内部,紧邻应用接管选项。确保用户启用代理后能立即看到接管配置,避免「只开代理不接管」的常见误操作 +- **OpenCode/OpenClaw 手动导入**:移除启动时自动导入供应商配置的行为,改为在空状态页显示「导入当前配置」按钮,与 Claude/Codex/Gemini 保持一致 + +--- + +## 修复 + +- **「跟随系统」主题不自动更新**:改用 Tauri 原生主题追踪(`set_window_theme(None)`),使 WebView 的 `prefers-color-scheme` 媒体查询能正确响应 OS 主题切换 +- **紧凑模式无法退出**:恢复 `toolbarRef` 上的 `flex-1` class,修复 `useAutoCompact` 的退出条件因宽度计算错误而永远不触发的问题 +- **代理接管 Toast 显示 {{app}}**:为 proxy takeover 的 i18next `t()` 调用补充缺失的 `app` 插值参数 +- **Windows 协议处理副作用**:在 Windows 上禁用环境检查和一键安装功能,防止协议处理程序注册引发的意外副作用 + +--- + +## 说明与注意事项 + +- **通用配置片段已恢复**:如果你在 v3.10.x 及更早版本中使用了此功能,它的工作方式与之前完全一致。用它来定义切换供应商时需要保留的共享配置。 +- **v3.11.0 部分键值合并用户**:如果你在 v3.11.0 中切换供应商后发现配置字段丢失,请重新导入配置以恢复。 + +--- + +## 下载与安装 + +访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。 + +### 系统要求 + +| 系统 | 最低版本 | 架构 | +| ------- | ----------------------------- | ----------------------------------- | +| Windows | Windows 10 及以上 | x64 | +| macOS | macOS 10.15 (Catalina) 及以上 | Intel (x64) / Apple Silicon (arm64) | +| Linux | 见下表 | x64 | + +### Windows + +| 文件 | 说明 | +| ---------------------------------------- | ----------------------------------- | +| `CC-Switch-v3.11.1-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 | +| `CC-Switch-v3.11.1-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 | + +### macOS + +| 文件 | 说明 | +| -------------------------------- | --------------------------------------------------------- | +| `CC-Switch-v3.11.1-macOS.zip` | **推荐** - 解压后拖入 Applications 即可,Universal Binary | +| `CC-Switch-v3.11.1-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 | + +> **注意**:由于作者没有苹果开发者账号,首次打开可能出现「未知开发者」警告,请先关闭,然后前往「系统设置」→「隐私与安全性」→ 点击「仍要打开」,之后便可以正常打开 + +### Homebrew(macOS) + +```bash +brew tap farion1231/ccswitch +brew install --cask cc-switch +``` + +更新: + +```bash +brew upgrade --cask cc-switch +``` + +### Linux + +| 发行版 | 推荐格式 | 安装方式 | +| --------------------------------------- | ----------- | ---------------------------------------------------------------------- | +| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` 或 `sudo apt install ./CC-Switch-*.deb` | +| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` 或 `sudo dnf install ./CC-Switch-*.rpm` | +| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` | +| Arch Linux / Manjaro | `.AppImage` | 添加执行权限后直接运行,或使用 AUR | +| 其他发行版 / 不确定 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` | diff --git a/docs/user-manual/README.md b/docs/user-manual/README.md index 01ce206ae..041d5d76b 100644 --- a/docs/user-manual/README.md +++ b/docs/user-manual/README.md @@ -99,9 +99,9 @@ ## 版本信息 -- 文档版本:v3.11.0 -- 最后更新:2026-02-26 -- 适用于 CC Switch v3.11.0+ +- 文档版本:v3.11.1 +- 最后更新:2026-02-28 +- 适用于 CC Switch v3.11.1+ ## 贡献 diff --git a/package.json b/package.json index 5502c8db3..778081b86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cc-switch", - "version": "3.11.0", + "version": "3.11.1", "description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b5ce8ee7a..3d708ca03 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cc-switch" -version = "3.11.0" +version = "3.11.1" description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI" authors = ["Jason Young"] license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2ce7aadd7..d647eb648 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CC Switch", - "version": "3.11.0", + "version": "3.11.1", "identifier": "com.ccswitch.desktop", "build": { "frontendDist": "../dist", From 4b0f14e2e6232a7953852d9b2da98d84a2898195 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 2 Mar 2026 11:30:04 +0800 Subject: [PATCH 09/34] docs: sync README features across EN/ZH/JA Add format conversion, per-provider proxy granularity, and symlink/file-copy support to EN and JA to match ZH updates. --- README.md | 364 +++++++++++++++++++++--------------------------- README_JA.md | 374 +++++++++++++++++++++---------------------------- README_ZH.md | 382 ++++++++++++++++++++++----------------------------- 3 files changed, 480 insertions(+), 640 deletions(-) diff --git a/README.md b/README.md index e89785a4f..a2edeeebb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@
-# All-in-One Assistant for Claude Code, Codex & Gemini CLI +# CC Switch + +### The All-in-One Manager for Claude Code, Codex, Gemini CLI, OpenCode & OpenClaw [![Version](https://img.shields.io/badge/version-3.11.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) @@ -15,6 +17,9 @@ English | [中文](README_ZH.md) | [日本語](README_JA.md) | [Changelog](CHANG ## ❤️Sponsor +
+Click to collapse + [![MiniMax](assets/partners/banners/minimax-en.jpeg)](https://platform.minimax.io/subscribe/coding-plan?code=ClLhgxr2je&source=link) MiniMax-M2.5 is a SOTA large language model designed for real-world productivity. Trained in a diverse range of complex real-world digital working environments, M2.5 builds upon the coding expertise of M2.1 to extend into general office work, reaching fluency in generating and operating Word, Excel, and Powerpoint files, context switching between diverse software environments, and working across different agent and human teams. Scoring 80.2% on SWE-Bench Verified, 51.3% on Multi-SWE-Bench, and 76.3% on BrowseComp, M2.5 is also more token efficient than previous generations, having been trained to optimize its actions and output through planning. @@ -72,6 +77,22 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric +
+ +## Why CC Switch? + +Modern AI-powered coding relies on CLI tools like Claude Code, Codex, Gemini CLI, OpenCode, and OpenClaw — but each has its own configuration format. Switching API providers means manually editing JSON, TOML, or `.env` files, and there is no unified way to manage MCP and Skills across multiple tools. + +**CC Switch** gives you a single desktop app to manage all five CLI tools. Instead of editing config files by hand, you get a visual interface to import providers with one click, switch between them instantly, with 50+ built-in provider presets, unified MCP and Skills management, and system tray quick switching — all backed by a reliable SQLite database with atomic writes that protect your configs from corruption. + +- **One App, Five CLI Tools** — Manage Claude Code, Codex, Gemini CLI, OpenCode, and OpenClaw from a single interface +- **No More Manual Editing** — 50+ provider presets including AWS Bedrock, NVIDIA NIM, and community relays; just pick and switch +- **Unified MCP & Skills Management** — One panel to manage MCP servers and Skills across four apps with bidirectional sync +- **System Tray Quick Switch** — Switch providers instantly from the tray menu, no need to open the full app +- **Cloud Sync** — Sync provider data across devices via Dropbox, OneDrive, iCloud, or WebDAV servers +- **Cross-Platform** — Native desktop app for Windows, macOS, and Linux, built with Tauri 2 +- **Built-in Utilities** — Includes various utilities for first-launch login confirmation, signature bypass, plugin extension sync, and more + ## Screenshots | Main Interface | Add Provider | @@ -80,102 +101,113 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric ## Features -### Current Version: v3.11.1 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.11.1-en.md) +[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.11.1-en.md) -**v3.8.0 Major Update (2025-11-28)** +### Provider Management -**Persistence Architecture Upgrade & Brand New UI** +- **5 CLI tools, 50+ presets** — Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw; copy your key and import with one click +- **Universal providers** — One config syncs to multiple apps (OpenCode, OpenClaw) +- One-click switching, system tray quick access, drag-and-drop sorting, import/export -- **SQLite + JSON Dual-layer Architecture** - - Migrated from JSON file storage to SQLite + JSON dual-layer structure - - Syncable data (providers, MCP, Prompts, Skills) stored in SQLite - - Device-level data (window state, local paths) stored in JSON - - Lays the foundation for future cloud sync functionality - - Schema version management for database migrations +### Proxy & Failover -- **Brand New User Interface** - - Completely redesigned interface layout - - Unified component styles and smoother animations - - Optimized visual hierarchy - - Tailwind CSS downgraded from v4 to v3.4 for better browser compatibility +- **Local proxy with hot-switching** — Format conversion, auto-failover, circuit breaker, provider health monitoring, and request rectifier +- **App-level takeover** — Independently proxy Claude, Codex, or Gemini, down to individual providers -- **Japanese Language Support** - - Added Japanese interface support (now supports Chinese/English/Japanese) +### MCP, Prompts & Skills -- **Auto Launch on Startup** - - One-click enable/disable in settings - - Platform-native APIs (Registry/LaunchAgent/XDG autostart) +- **Unified MCP panel** — Manage MCP servers across 4 apps with bidirectional sync and Deep Link import +- **Prompts** — Markdown editor with cross-app sync (CLAUDE.md / AGENTS.md / GEMINI.md) and backfill protection +- **Skills** — One-click install from GitHub repos or ZIP files, custom repository management, with symlink and file copy support -- **Skills Recursive Scanning** - - Support for multi-level directory structures - - Allow same-named skills from different repositories +### Usage & Cost Tracking -- **Critical Bug Fixes** - - Fixed custom endpoints lost when updating providers - - Fixed Gemini configuration write issues - - Fixed Linux WebKitGTK rendering issues +- **Usage dashboard** — Track spending, requests, and tokens with trend charts, detailed request logs, and custom per-model pricing -**v3.7.0 Highlights** +### Session Manager & Workspace -**Six Core Features, 18,000+ Lines of New Code** +- Browse, search, and restore conversation history across all apps +- **Workspace editor** (OpenClaw) — Edit agent files (AGENTS.md, SOUL.md, etc.) with Markdown preview -- **Gemini CLI Integration** - - Third supported AI CLI (Claude Code / Codex / Gemini) - - Dual-file configuration support (`.env` + `settings.json`) - - Complete MCP server management - - Presets: Google Official (OAuth) / PackyCode / Custom +### System & Platform -- **Claude Skills Management System** - - Auto-scan skills from GitHub repositories (3 pre-configured curated repos) - - One-click install/uninstall to `~/.claude/skills/` - - Custom repository support + subdirectory scanning - - Complete lifecycle management (discover/install/update) +- **Cloud sync** — Custom config directory (Dropbox, OneDrive, iCloud, NAS) and WebDAV server sync +- **Deep Link** (`ccswitch://`) — Import providers, MCP servers, prompts, and skills via URL +- Dark / Light / System theme, auto-launch, auto-updater, atomic writes, auto-backups, i18n (zh/en/ja) -- **Prompts Management System** - - Multi-preset system prompt management (unlimited presets, quick switching) - - Cross-app support (Claude: `CLAUDE.md` / Codex: `AGENTS.md` / Gemini: `GEMINI.md`) - - Markdown editor (CodeMirror 6 + real-time preview) - - Smart backfill protection, preserves manual modifications +## FAQ -- **MCP v3.7.0 Unified Architecture** - - Single panel manages MCP servers across three applications - - New SSE (Server-Sent Events) transport type - - Smart JSON parser + Codex TOML format auto-correction - - Unified import/export + bidirectional sync +
+Which AI CLI tools does CC Switch support? -- **Deep Link Protocol** - - `ccswitch://` protocol registration (all platforms) - - One-click import provider configs via shared links - - Security validation + lifecycle integration +CC Switch supports five tools: **Claude Code**, **Codex**, **Gemini CLI**, **OpenCode**, and **OpenClaw**. Each tool has dedicated provider presets and configuration management. -- **Environment Variable Conflict Detection** - - Auto-detect cross-app configuration conflicts (Claude/Codex/Gemini/MCP) - - Visual conflict indicators + resolution suggestions - - Override warnings + backup before changes +
-**Core Capabilities** +
+Do I need to restart the terminal after switching providers? -- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations -- **AWS Bedrock Support**: Built-in AWS Bedrock provider presets with AKSK and API Key authentication, cross-region inference support (global/us/eu/apac), covering Claude Code and OpenCode -- **Speed Testing**: Measure API endpoint latency with visual quality indicators -- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent) -- **i18n Support**: Complete Chinese/English localization (UI, errors, tray) -- **Claude Plugin Sync**: One-click apply/restore Claude plugin configurations +For most tools, yes — restart your terminal or the CLI tool for changes to take effect. The exception is **Claude Code**, which currently supports hot-switching of provider data without a restart. -**v3.6 Highlights** +
-- Provider duplication & drag-and-drop sorting -- Multi-endpoint management & custom config directory (cloud sync ready) -- Granular model configuration (4-tier: Haiku/Sonnet/Opus/Custom) -- WSL environment support with auto-sync on directory change -- 100% hooks test coverage & complete architecture refactoring +
+My plugin configuration disappeared after switching providers — what happened? -**System Features** +CC Switch provides a "Shared Config Snippet" feature to pass common data (beyond API keys and endpoints) between providers. Go to "Edit Provider" → "Shared Config Panel" → click "Extract from Current Provider" to save all common data. When creating a new provider, check "Write Shared Config" (enabled by default) to include plugin data in the new provider. All your configuration items are preserved in the default provider imported when you first launched the app. -- System tray with quick switching -- Single instance daemon -- Built-in auto-updater -- Atomic writes with rollback protection +
+ +
+macOS shows "unidentified developer" warning — how do I fix it? + +The author doesn't have an Apple Developer account yet (registration in progress). Close the warning, then go to **System Settings → Privacy & Security → Open Anyway**. After that, the app will open normally. + +
+ +
+Why can't I delete the currently active provider? + +CC Switch follows a "minimal intrusion" design principle — even if you uninstall the app, your CLI tools will continue to work normally. The system always keeps one active configuration, because deleting all configurations would make the corresponding CLI tool unusable. If you rarely use a specific CLI tool, you can hide it in Settings. To switch back to official login, see the next question. + +
+ +
+How do I switch back to official login? + +Add an official provider from the preset list. After switching to it, run the Log out / Log in flow, and then you can freely switch between the official provider and third-party providers. Codex supports switching between different official providers, making it easy to switch between multiple Plus or Team accounts. + +
+ +
+Where is my data stored? + +- **Database**: `~/.cc-switch/cc-switch.db` (SQLite — providers, MCP, prompts, skills) +- **Local settings**: `~/.cc-switch/settings.json` (device-level UI preferences) +- **Backups**: `~/.cc-switch/backups/` (auto-rotated, keeps 10 most recent) +- **Skills**: `~/.cc-switch/skills/` (symlinked to corresponding apps by default) + +
+ +## Quick Start + +### Basic Usage + +1. **Add Provider**: Click "Add Provider" → Choose a preset or create custom configuration +2. **Switch Provider**: + - Main UI: Select provider → Click "Enable" + - System Tray: Click provider name directly (instant effect) +3. **Takes Effect**: Restart your terminal or the corresponding CLI tool to apply changes (Claude Code does not require a restart) +4. **Back to Official**: Add an "Official Login" preset, restart the CLI tool, then follow its login/OAuth flow + +### MCP, Prompts, Skills & Sessions + +- **MCP**: Click the "MCP" button → Add servers via templates or custom config → Toggle per-app sync +- **Prompts**: Click "Prompts" → Create presets with Markdown editor → Activate to sync to live files +- **Skills**: Click "Skills" → Browse GitHub repos → One-click install to all apps +- **Sessions**: Click "Sessions" → Browse, search, and restore conversation history across all apps + +> **Note**: On first launch, you can manually import existing CLI tool configs as the default provider. ## Download & Installation @@ -234,89 +266,8 @@ flatpak install --user ./CC-Switch-v{version}-Linux.flatpak flatpak run com.ccswitch.desktop ``` -## Quick Start - -### Basic Usage - -1. **Add Provider**: Click "Add Provider" → Choose preset or create custom configuration -2. **Switch Provider**: - - Main UI: Select provider → Click "Enable" - - System Tray: Click provider name directly (instant effect) -3. **Takes Effect**: Restart your terminal or Claude Code / Codex / Gemini clients to apply changes -4. **Back to Official**: Select the "Official Login" preset (Claude/Codex) or "Google Official" preset (Gemini), restart the corresponding client, then follow its login/OAuth flow - -### MCP Management - -- **Location**: Click "MCP" button in top-right corner -- **Add Server**: - - Use built-in templates (mcp-fetch, mcp-filesystem, etc.) - - Support stdio / http / sse transport types - - Configure independent MCP servers for different apps -- **Enable/Disable**: Toggle switches to control which servers sync to live config -- **Sync**: Enabled servers auto-sync to each app's live files -- **Import/Export**: Import existing MCP servers from Claude/Codex/Gemini config files - -### Skills Management (v3.7.0 New) - -- **Location**: Click "Skills" button in top-right corner -- **Discover Skills**: - - Auto-scan pre-configured GitHub repositories (Anthropic official, ComposioHQ, community, etc.) - - Add custom repositories (supports subdirectory scanning) -- **Install Skills**: Click "Install" to one-click install to `~/.claude/skills/` -- **Uninstall Skills**: Click "Uninstall" to safely remove and clean up state -- **Manage Repositories**: Add/remove custom GitHub repositories - -### Prompts Management (v3.7.0 New) - -- **Location**: Click "Prompts" button in top-right corner -- **Create Presets**: - - Create unlimited system prompt presets - - Use Markdown editor to write prompts (syntax highlighting + real-time preview) -- **Switch Presets**: Select preset → Click "Activate" to apply immediately -- **Sync Mechanism**: - - Claude: `~/.claude/CLAUDE.md` - - Codex: `~/.codex/AGENTS.md` - - Gemini: `~/.gemini/GEMINI.md` -- **Protection Mechanism**: Auto-save current prompt content before switching, preserves manual modifications - -### Configuration Files - -**Claude Code** - -- Live config: `~/.claude/settings.json` (or `claude.json`) -- API key field: `env.ANTHROPIC_AUTH_TOKEN` or `env.ANTHROPIC_API_KEY` -- MCP servers: `~/.claude.json` → `mcpServers` - -**Codex** - -- Live config: `~/.codex/auth.json` (required) + `config.toml` (optional) -- API key field: `OPENAI_API_KEY` in `auth.json` -- MCP servers: `~/.codex/config.toml` → `[mcp_servers]` tables - -**Gemini** - -- Live config: `~/.gemini/.env` (API key) + `~/.gemini/settings.json` (auth mode) -- API key field: `GEMINI_API_KEY` or `GOOGLE_GEMINI_API_KEY` in `.env` -- Environment variables: Support `GOOGLE_GEMINI_BASE_URL`, `GEMINI_MODEL`, etc. -- MCP servers: `~/.gemini/settings.json` → `mcpServers` -- Tray quick switch: Each provider switch rewrites `~/.gemini/.env`, no need to restart Gemini CLI - -**CC Switch Storage (v3.8.0 New Architecture)** - -- Database (SSOT): `~/.cc-switch/cc-switch.db` (SQLite, stores providers, MCP, Prompts, Skills) -- Local settings: `~/.cc-switch/settings.json` (device-level settings) -- Backups: `~/.cc-switch/backups/` (auto-rotate, keep 10) - -### Cloud Sync Setup - -1. Go to Settings → "Custom Configuration Directory" -2. Choose your cloud sync folder (Dropbox, OneDrive, iCloud, etc.) -3. Restart app to apply -4. Repeat on other devices to enable cross-device sync - -> **Note**: First launch auto-imports existing Claude/Codex configs as default provider. - -## Architecture Overview +
+Architecture Overview ### Design Principles @@ -351,16 +302,15 @@ flatpak run com.ccswitch.desktop - **ProviderService**: Provider CRUD, switching, backfill, sorting - **McpService**: MCP server management, import/export, live file sync +- **ProxyService**: Local proxy mode with hot-switching and format conversion +- **SessionManager**: Claude Code conversation history browsing - **ConfigService**: Config import/export, backup rotation - **SpeedtestService**: API endpoint latency measurement -**v3.6 Refactoring** +
-- Backend: 5-phase refactoring (error handling → command split → tests → services → concurrency) -- Frontend: 4-stage refactoring (test infra → hooks → components → cleanup) -- Testing: 100% hooks coverage + integration tests (vitest + MSW) - -## Development +
+Development Guide ### Environment Requirements @@ -421,7 +371,7 @@ cargo test test_name cargo test --features test-hooks ``` -### Testing Guide (v3.6 New) +### Testing Guide **Frontend Testing**: @@ -429,18 +379,6 @@ cargo test --features test-hooks - Uses **MSW (Mock Service Worker)** to mock Tauri API calls - Uses **@testing-library/react** for component testing -**Test Coverage**: - -- Hooks unit tests (100% coverage) - - `useProviderActions` - Provider operations - - `useMcpActions` - MCP management - - `useSettings` series - Settings management - - `useImportExport` - Import/export -- Integration tests - - App main application flow - - SettingsDialog complete interaction - - MCP panel functionality - **Running Tests**: ```bash @@ -454,49 +392,56 @@ pnpm test:unit:watch pnpm test:unit --coverage ``` -## Tech Stack +### Tech Stack -**Frontend**: React 18 · TypeScript · Vite · TailwindCSS 4 · TanStack Query v5 · react-i18next · react-hook-form · zod · shadcn/ui · @dnd-kit +**Frontend**: React 18 · TypeScript · Vite · TailwindCSS 3.4 · TanStack Query v5 · react-i18next · react-hook-form · zod · shadcn/ui · @dnd-kit **Backend**: Tauri 2.8 · Rust · serde · tokio · thiserror · tauri-plugin-updater/process/dialog/store/log **Testing**: vitest · MSW · @testing-library/react -## Project Structure +
+ +
+Project Structure ``` -├── src/ # Frontend (React + TypeScript) -│ ├── components/ # UI components (providers/settings/mcp/ui) -│ ├── hooks/ # Custom hooks (business logic) +├── src/ # Frontend (React + TypeScript) +│ ├── components/ +│ │ ├── providers/ # Provider management +│ │ ├── mcp/ # MCP panel +│ │ ├── prompts/ # Prompts management +│ │ ├── skills/ # Skills management +│ │ ├── sessions/ # Session Manager +│ │ ├── proxy/ # Proxy mode panel +│ │ ├── openclaw/ # OpenClaw config panels +│ │ ├── settings/ # Settings (Terminal/Backup/About) +│ │ ├── deeplink/ # Deep Link import +│ │ ├── env/ # Environment variable management +│ │ ├── universal/ # Cross-app configuration +│ │ ├── usage/ # Usage statistics +│ │ └── ui/ # shadcn/ui component library +│ ├── hooks/ # Custom hooks (business logic) │ ├── lib/ -│ │ ├── api/ # Tauri API wrapper (type-safe) -│ │ └── query/ # TanStack Query config -│ ├── i18n/locales/ # Translations (zh/en) -│ ├── config/ # Presets (providers/mcp) -│ └── types/ # TypeScript definitions -├── src-tauri/ # Backend (Rust) +│ │ ├── api/ # Tauri API wrapper (type-safe) +│ │ └── query/ # TanStack Query config +│ ├── locales/ # Translations (zh/en/ja) +│ ├── config/ # Presets (providers/mcp) +│ └── types/ # TypeScript definitions +├── src-tauri/ # Backend (Rust) │ └── src/ -│ ├── commands/ # Tauri command layer (by domain) -│ ├── services/ # Business logic layer -│ ├── app_config.rs # Config data models -│ ├── provider.rs # Provider domain models -│ ├── mcp.rs # MCP sync & validation -│ └── lib.rs # App entry & tray menu -├── tests/ # Frontend tests -│ ├── hooks/ # Unit tests -│ └── components/ # Integration tests -└── assets/ # Screenshots & partner resources +│ ├── commands/ # Tauri command layer (by domain) +│ ├── services/ # Business logic layer +│ ├── database/ # SQLite DAO layer +│ ├── proxy/ # Proxy module +│ ├── session_manager/ # Session management +│ ├── deeplink/ # Deep Link handling +│ └── mcp/ # MCP sync module +├── tests/ # Frontend tests +└── assets/ # Screenshots & partner resources ``` -## Changelog - -See [CHANGELOG.md](CHANGELOG.md) for version update details. - -## Legacy Electron Version - -[Releases](../../releases) retains v2.0.3 legacy Electron version - -If you need legacy Electron code, you can pull the electron-legacy branch +
## Contributing @@ -507,7 +452,8 @@ Before submitting PRs, please ensure: - Pass type check: `pnpm typecheck` - Pass format check: `pnpm format:check` - Pass unit tests: `pnpm test:unit` -- 💡 For new features, please open an issue for discussion before submitting a PR + +For new features, please open an issue for discussion before submitting a PR. PRs for features that are not a good fit for the project may be closed. ## Star History diff --git a/README_JA.md b/README_JA.md index 2b69c1cef..b683ac222 100644 --- a/README_JA.md +++ b/README_JA.md @@ -1,20 +1,25 @@
-# Claude Code / Codex / Gemini CLI オールインワン・アシスタント +# CC Switch -[![Version](https://img.shields.io/badge/version-3.10.2-blue.svg)](https://github.com/farion1231/cc-switch/releases) +### Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw のオールインワン管理ツール + +[![Version](https://img.shields.io/badge/version-3.11.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest) farion1231%2Fcc-switch | Trendshift -[English](README.md) | [中文](README_ZH.md) | 日本語 | [Changelog](CHANGELOG.md) | [v3.9.0 リリースノート](docs/release-note-v3.9.0-ja.md) +[English](README.md) | [中文](README_ZH.md) | 日本語 | [Changelog](CHANGELOG.md)
## ❤️スポンサー +
+クリックで折りたたむ + [![MiniMax](assets/partners/banners/minimax-en.jpeg)](https://platform.minimax.io/subscribe/coding-plan?code=ClLhgxr2je&source=link) MiniMax-M2.5 は、実際の生産性向上のために設計された最先端の大規模言語モデルです。多様で複雑な実環境のデジタルワークスペースでトレーニングされた M2.5 は、M2.1 のコーディング能力をベースに一般的なオフィス業務へと拡張し、Word・Excel・PowerPoint ファイルの生成と操作、多様なソフトウェア環境間のコンテキスト切り替え、異なるエージェントや人間チーム間での協働を流暢にこなします。SWE-Bench Verified で 80.2%、Multi-SWE-Bench で 51.3%、BrowseComp で 76.3% を達成し、計画的な行動と出力の最適化トレーニングにより、前世代よりもトークン効率に優れています。 @@ -72,6 +77,22 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / +
+ +## CC Switch を選ぶ理由 + +最新の AI コーディングは Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw などの CLI ツールに依存していますが、各ツールの設定形式はバラバラです。API プロバイダを切り替えるたびに JSON、TOML、`.env` ファイルを手動で編集する必要があり、複数ツール間で MCP や Skills を統一的に管理する手段もありません。 + +**CC Switch** は、5 つの CLI ツールを 1 つのデスクトップアプリで一元管理できます。設定ファイルを手作業で編集する代わりに、ワンクリックでプロバイダをインポートし、瞬時に切り替えられるビジュアルインターフェースを提供します。50 以上の組み込みプリセット、統一 MCP・Skills 管理、システムトレイからの即時切り替え機能を搭載。すべてはアトミック書き込みによる信頼性の高い SQLite データベースに支えられており、設定の破損を防ぎます。 + +- **1 つのアプリで 5 つの CLI ツール** -- Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw を単一インターフェースで管理 +- **手動編集は不要** -- AWS Bedrock、NVIDIA NIM、コミュニティリレーなど 50 以上のプロバイダプリセットを内蔵。選んで切り替えるだけ +- **統一 MCP・Skills 管理** -- 1 つのパネルで 4 つのアプリの MCP サーバーと Skills を双方向同期で管理 +- **システムトレイでクイック切り替え** -- トレイメニューから即座にプロバイダを切り替え。アプリを開く必要なし +- **クラウド同期** -- Dropbox、OneDrive、iCloud、または WebDAV サーバー経由でデバイス間のプロバイダデータを同期 +- **クロスプラットフォーム** -- Tauri 2 で構築された Windows、macOS、Linux 対応のネイティブデスクトップアプリ +- **便利ツール内蔵** -- 初回起動時のログイン確認、署名バイパス、プラグイン拡張の同期など、さまざまなユーティリティを搭載 + ## スクリーンショット | メイン画面 | プロバイダ追加 | @@ -80,102 +101,113 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / ## 特長 -### 現在のバージョン:v3.10.2 | [完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.9.0-ja.md) +[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.11.1-ja.md) -**v3.8.0 メジャーアップデート (2025-11-28)** +### プロバイダ管理 -**永続化アーキテクチャ刷新 & 新 UI** +- **5 つの CLI ツール、50 以上のプリセット** -- Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw。キーをコピーしてワンクリックでインポート +- **ユニバーサルプロバイダ** -- 1 つの設定を複数アプリに同期(OpenCode、OpenClaw) +- ワンクリック切り替え、システムトレイクイックアクセス、ドラッグ&ドロップ並び替え、インポート/エクスポート -- **SQLite + JSON 二層構造** - - JSON 単独保存から SQLite + JSON の二層構造へ移行 - - 同期対象データ(プロバイダ、MCP、Prompts、Skills)は SQLite に保存 - - デバイス固有データ(ウィンドウ状態、ローカルパス)は JSON に保存 - - 将来のクラウド同期の土台を用意 - - DB マイグレーション用にスキーマバージョンを管理 +### プロキシ & フェイルオーバー -- **新しいユーザーインターフェース** - - レイアウトを全面再設計 - - コンポーネントスタイルとアニメーションを統一 - - 視覚的な階層を最適化 - - ブラウザ互換性向上のため Tailwind CSS を v4 から v3.4 にダウングレード +- **ローカルプロキシのホットスイッチ** -- フォーマット変換、自動フェイルオーバー、サーキットブレーカー、プロバイダヘルスモニタリング、リクエストレクティファイア +- **アプリレベルのテイクオーバー** -- Claude、Codex、Gemini を個別にプロキシ経由でルーティング、プロバイダ単位で設定可能 -- **日本語対応** - - UI が中国語/英語/日本語の 3 言語対応に +### MCP、Prompts & Skills -- **自動起動** - - 設定画面でワンクリック ON/OFF - - プラットフォームネイティブ API(Registry/LaunchAgent/XDG autostart)を使用 +- **統一 MCP パネル** -- 4 つのアプリの MCP サーバーを管理、双方向同期、Deep Link インポート対応 +- **Prompts** -- Markdown エディタ、クロスアプリ同期(CLAUDE.md / AGENTS.md / GEMINI.md)、バックフィル保護 +- **Skills** -- GitHub リポジトリまたは ZIP ファイルからワンクリックインストール、カスタムリポジトリ管理、シンボリックリンクとファイルコピーに対応 -- **Skills 再帰スキャン** - - 多階層ディレクトリをサポート - - リポジトリが異なる同名スキルを許可 +### 使用量 & コストトラッキング -- **重要なバグ修正** - - プロバイダ更新時にカスタムエンドポイントが失われる問題を修正 - - Gemini 設定の書き込み問題を修正 - - Linux WebKitGTK の描画問題を修正 +- **使用量ダッシュボード** -- プロバイダ横断で支出・リクエスト数・トークン使用量を追跡、トレンドチャート、詳細リクエストログ、カスタムモデル価格設定 -**v3.7.0 ハイライト** +### Session Manager & ワークスペース -**6 つのコア機能、18,000 行超の新コード** +- すべてのアプリの会話履歴を閲覧・検索・復元 +- **ワークスペースエディタ**(OpenClaw)-- エージェントファイル(AGENTS.md、SOUL.md など)を Markdown プレビュー付きで編集 -- **Gemini CLI 統合** - - Claude Code / Codex / Gemini の 3 番目のサポート AI CLI - - 2 つの設定ファイル(`.env` + `settings.json`)に対応 - - MCP サーバー管理を完備 - - プリセット:Google 公式(OAuth)/ PackyCode / カスタム +### システム & プラットフォーム -- **Claude Skills 管理システム** - - GitHub リポジトリを自動スキャン(3 つのキュレーション済みリポジトリを同梱) - - `~/.claude/skills/` へワンクリックでインストール/アンインストール - - カスタムリポジトリ + サブディレクトリスキャンをサポート - - ライフサイクル管理(検出/インストール/更新)を完備 +- **クラウド同期** -- カスタム設定ディレクトリ(Dropbox、OneDrive、iCloud、NAS)および WebDAV サーバー同期 +- **Deep Link** (`ccswitch://`) -- URL 経由でプロバイダ、MCP サーバー、Prompts、Skills をワンクリックインポート +- ダーク / ライト / システムテーマ、自動起動、自動アップデーター、アトミック書き込み、自動バックアップ、多言語対応(中/英/日) -- **Prompts 管理システム** - - 無制限のシステムプロンプトプリセットを作成 - - Markdown エディタ(CodeMirror 6 + リアルタイムプレビュー)付き - - スマートなバックフィル保護で手動変更を保持 - - 複数アプリに同時対応(Claude: `CLAUDE.md` / Codex: `AGENTS.md` / Gemini: `GEMINI.md`) +## よくある質問 -- **MCP v3.7.0 統合アーキテクチャ** - - 1 つのパネルで 3 アプリの MCP を管理 - - 新たに SSE(Server-Sent Events)トランスポートを追加 - - スマート JSON パーサー + Codex TOML 自動修正 - - 双方向のインポート/エクスポート + 双方向同期 +
+CC Switch はどの AI CLI ツールに対応していますか? -- **ディープリンクプロトコル** - - `ccswitch://` を全プラットフォームで登録 - - 共有リンクからプロバイダ設定をワンクリックでインポート - - セキュリティ検証 + ライフサイクル統合 +CC Switch は **Claude Code**、**Codex**、**Gemini CLI**、**OpenCode**、**OpenClaw** の 5 つのツールに対応しています。各ツールに専用のプロバイダプリセットと設定管理が用意されています。 -- **環境変数の競合検知** - - Claude/Codex/Gemini/MCP 間の設定競合を自動検出 - - 競合表示 + 解決ガイド - - 上書き前の警告 + バックアップ +
-**コア機能** +
+プロバイダを切り替えた後、ターミナルの再起動は必要ですか? -- **プロバイダ管理**:Claude Code、Codex、Gemini の API 設定をワンクリックで切り替え -- **AWS Bedrock 対応**:AWS Bedrock プロバイダプリセットを内蔵、AKSK および API Key 認証に対応、クロスリージョン推論(global/us/eu/apac)をサポート、Claude Code と OpenCode に対応 -- **速度テスト**:エンドポイント遅延を計測し、品質を可視化 -- **インポート/エクスポート**:設定をバックアップ・復元(最新 10 件を自動ローテーション) -- **多言語対応**:UI/エラー/トレイを含む中国語・英語・日本語ローカライズ -- **Claude プラグイン同期**:Claude プラグイン設定をワンクリックで適用/復元 +ほとんどのツールでは、はい。変更を反映するにはターミナルまたは CLI ツールを再起動してください。ただし **Claude Code** は例外で、現在プロバイダデータのホットスイッチに対応しており、再起動は不要です。 -**v3.6 ハイライト** +
-- プロバイダの複製とドラッグ&ドロップ並び替え -- 複数エンドポイント管理とカスタム設定ディレクトリ(クラウド同期準備済み) -- 4 階層のモデル設定(Haiku/Sonnet/Opus/Custom) -- WSL 環境をサポートし、ディレクトリ変更時に自動同期 -- Hooks テスト 100% カバレッジ + アーキテクチャ全面リファクタ +
+プロバイダを切り替えた後、プラグイン設定が消えてしまいました。どうすればよいですか? -**システム機能** +CC Switch には「共有設定スニペット」機能があり、APIキーやエンドポイント以外の共通データをプロバイダ間で引き継ぐことができます。「プロバイダ編集」→「共有設定パネル」→「現在のプロバイダから抽出」をクリックして、すべての共通データを保存してください。新しいプロバイダを作成する際に「共有設定を書き込む」にチェック(デフォルトで有効)を入れれば、プラグインなどのデータが新しいプロバイダ設定に含まれます。すべての設定項目は、アプリ初回起動時にインポートされたデフォルトプロバイダに保存されており、失われることはありません。 -- クイックスイッチ付きシステムトレイ -- シングルインスタンス常駐 -- ビルトイン自動アップデータ -- ロールバック保護付きのアトミック書き込み +
+ +
+macOS で「開発元を確認できません」と表示されます。どうすればよいですか? + +開発者が Apple Developer アカウントをまだ取得していないためです(登録手続き中)。警告を閉じてから、**システム設定 → プライバシーとセキュリティ → このまま開く**をクリックしてください。以降は通常通り起動できます。 + +
+ +
+現在アクティブなプロバイダを削除できないのはなぜですか? + +CC Switch は「最小限の介入」という設計原則に従っています。アプリをアンインストールしても、CLI ツールは正常に動作し続けます。すべての設定を削除すると対応する CLI ツールが使用できなくなるため、システムは常にアクティブな設定を 1 つ保持します。特定の CLI ツールをあまり使用しない場合は、設定で非表示にできます。公式ログインに戻す方法は、次の質問をご覧ください。 + +
+ +
+公式ログインに戻すにはどうすればよいですか? + +プリセットリストから公式プロバイダを追加してください。切り替え後、ログアウト/ログインのフローを実行すれば、以降は公式プロバイダとサードパーティプロバイダを自由に切り替えられます。Codex では異なる公式プロバイダ間の切り替えに対応しており、複数の Plus アカウントや Team アカウントの切り替えに便利です。 + +
+ +
+データはどこに保存されますか? + +- **データベース**: `~/.cc-switch/cc-switch.db`(SQLite -- プロバイダ、MCP、Prompts、Skills) +- **ローカル設定**: `~/.cc-switch/settings.json`(デバイスレベルの UI 設定) +- **バックアップ**: `~/.cc-switch/backups/`(自動ローテーション、最新 10 件を保持) +- **Skills**: `~/.cc-switch/skills/`(デフォルトでシンボリックリンクにより対応アプリに接続) + +
+ +## クイックスタート + +### 基本的な使い方 + +1. **プロバイダ追加**: 「Add Provider」をクリック → プリセットを選ぶかカスタム設定を作成 +2. **プロバイダ切り替え**: + - メイン UI: プロバイダを選択 → 「Enable」をクリック + - システムトレイ: プロバイダ名をクリック(即時反映) +3. **反映**: ターミナルまたは対応する CLI ツールを再起動して適用(Claude Code は再起動不要) +4. **公式設定に戻す**: 「Official Login」プリセットを追加し、CLI ツールを再起動してログイン/OAuth フローを実行 + +### MCP、Prompts、Skills & Sessions + +- **MCP**: 「MCP」ボタンをクリック → テンプレートまたはカスタム設定でサーバーを追加 → アプリごとの同期をトグルで切り替え +- **Prompts**: 「Prompts」をクリック → Markdown エディタでプリセットを作成 → 有効化してライブファイルに同期 +- **Skills**: 「Skills」をクリック → GitHub リポジトリを閲覧 → ワンクリックですべてのアプリにインストール +- **Sessions**: 「Sessions」をクリック → すべてのアプリの会話履歴を閲覧・検索・復元 + +> **補足**: 初回起動時に、既存の CLI ツール設定を手動でインポートしてデフォルトプロバイダとして使用できます。 ## ダウンロード & インストール @@ -210,7 +242,7 @@ brew upgrade --cask cc-switch > **注意**: 開発者アカウント未登録のため、初回起動時に「開発元を確認できません」と表示される場合があります。一度閉じてから「システム設定」→「プライバシーとセキュリティ」→「このまま開く」をクリックしてください。以降は通常通り起動できます。 -### ArchLinux ユーザー +### Arch Linux ユーザー **paru でインストール(推奨)** @@ -234,89 +266,8 @@ flatpak install --user ./CC-Switch-v{version}-Linux.flatpak flatpak run com.ccswitch.desktop ``` -## クイックスタート - -### 基本的な使い方 - -1. **プロバイダ追加**:「Add Provider」をクリック → プリセットを選ぶかカスタム設定を作成 -2. **プロバイダ切り替え**: - - メイン UI: プロバイダを選択 → 「Enable」をクリック - - システムトレイ: プロバイダ名をクリック(即時反映) -3. **反映**: ターミナルや Claude Code / Codex / Gemini クライアントを再起動して適用 -4. **公式設定に戻す**: 「Official Login」プリセット(Claude/Codex)または「Google Official」プリセット(Gemini)を選び、対応クライアントを再起動してログイン/OAuth を実行 - -### MCP 管理 - -- **入口**: 右上の「MCP」ボタンをクリック -- **サーバー追加**: - - 組み込みテンプレート(mcp-fetch、mcp-filesystem など)を使用 - - stdio / http / sse の各トランスポートをサポート - - アプリごとに独立した MCP を設定可能 -- **有効/無効**: トグルでライブ設定への同期を切り替え -- **同期**: 有効なサーバーは各アプリのライブファイルへ自動同期 -- **インポート/エクスポート**: Claude/Codex/Gemini の設定ファイルから既存 MCP を取り込み - -### Skills 管理 (v3.7.0 新機能) - -- **入口**: 右上の「Skills」ボタンをクリック -- **スキル探索**: - - 事前設定済みの GitHub リポジトリを自動スキャン(Anthropic 公式、ComposioHQ、コミュニティなど) - - カスタムリポジトリを追加(サブディレクトリスキャン対応) -- **インストール**: 「Install」を押すだけで `~/.claude/skills/` に配置 -- **アンインストール**: 「Uninstall」で安全に削除と状態クリーンアップ -- **リポジトリ管理**: カスタム GitHub リポジトリを追加/削除 - -### Prompts 管理 (v3.7.0 新機能) - -- **入口**: 右上の「Prompts」ボタンをクリック -- **プリセット作成**: - - 無制限のシステムプロンプトプリセットを作成 - - Markdown エディタで記述(シンタックスハイライト + リアルタイムプレビュー) -- **プリセット切り替え**: プリセットを選択 → 「Activate」で即適用 -- **同期先**: - - Claude: `~/.claude/CLAUDE.md` - - Codex: `~/.codex/AGENTS.md` - - Gemini: `~/.gemini/GEMINI.md` -- **保護機構**: 切り替え前に現在の内容を自動保存し、手動変更を保持 - -### 設定ファイルパス - -**Claude Code** - -- ライブ設定: `~/.claude/settings.json`(または `claude.json`) -- API キーフィールド: `env.ANTHROPIC_AUTH_TOKEN` または `env.ANTHROPIC_API_KEY` -- MCP サーバー: `~/.claude.json` → `mcpServers` - -**Codex** - -- ライブ設定: `~/.codex/auth.json`(必須)+ `config.toml`(任意) -- API キーフィールド: `auth.json` 内の `OPENAI_API_KEY` -- MCP サーバー: `~/.codex/config.toml` → `[mcp_servers]` テーブル - -**Gemini** - -- ライブ設定: `~/.gemini/.env`(API キー)+ `~/.gemini/settings.json`(認証モード) -- API キーフィールド: `.env` 内の `GEMINI_API_KEY` または `GOOGLE_GEMINI_API_KEY` -- 環境変数: `GOOGLE_GEMINI_BASE_URL`、`GEMINI_MODEL` などをサポート -- MCP サーバー: `~/.gemini/settings.json` → `mcpServers` -- トレイでのクイックスイッチ: プロバイダ切り替えごとに `~/.gemini/.env` を書き換えるため Gemini CLI の再起動は不要 - -**CC Switch 保存先 (v3.8.0 新アーキテクチャ)** - -- データベース (SSOT): `~/.cc-switch/cc-switch.db`(SQLite。プロバイダ、MCP、Prompts、Skills を保存) -- ローカル設定: `~/.cc-switch/settings.json`(デバイスレベル設定) -- バックアップ: `~/.cc-switch/backups/`(自動ローテーション、最新 10 件を保持) - -### クラウド同期の設定 - -1. 設定 → 「Custom Configuration Directory」へ進む -2. クラウド同期フォルダ(Dropbox、OneDrive、iCloud など)を選択 -3. アプリを再起動して反映 -4. 他のデバイスでも同じフォルダを指定すればクロスデバイス同期が有効に - -> **補足**: 初回起動時に既存の Claude/Codex 設定をデフォルトプロバイダとして自動インポートします。 - -## アーキテクチャ概要 +
+アーキテクチャ概要 ### 設計原則 @@ -344,23 +295,22 @@ flatpak run com.ccswitch.desktop - **二層ストレージ**: 同期データは SQLite、デバイスデータは JSON - **双方向同期**: 切り替え時はライブファイルへ書き込み、編集時はアクティブプロバイダから逆同期 - **アトミック書き込み**: 一時ファイル + rename パターンで設定破損を防止 -- **並行安全**: Mutex で保護された DB 接続でレースを防ぐ +- **並行安全**: Mutex で保護された DB 接続でレースコンディションを防止 - **レイヤードアーキテクチャ**: Commands → Services → DAO → Database を明確に分離 **主要コンポーネント** - **ProviderService**: プロバイダの CRUD、切り替え、バックフィル、ソート - **McpService**: MCP サーバー管理、インポート/エクスポート、ライブファイル同期 +- **ProxyService**: ローカル Proxy モードのホットスイッチとフォーマット変換 +- **SessionManager**: Claude Code の会話履歴閲覧 - **ConfigService**: 設定のインポート/エクスポート、バックアップローテーション - **SpeedtestService**: API エンドポイントの遅延計測 -**v3.6 リファクタリング** +
-- バックエンド: エラーハンドリング → コマンド分割 → テスト → サービス層 → 並行性の 5 フェーズ -- フロントエンド: テスト基盤 → hooks → コンポーネント → クリーンアップの 4 ステージ -- テスト: hooks 100% カバレッジ + 統合テスト(vitest + MSW) - -## 開発 +
+開発ガイド ### 開発環境 @@ -421,7 +371,7 @@ cargo test test_name cargo test --features test-hooks ``` -### テストガイド (v3.6) +### テストガイド **フロントエンドテスト**: @@ -429,18 +379,6 @@ cargo test --features test-hooks - **MSW (Mock Service Worker)** で Tauri API 呼び出しをモック - コンポーネントテストに **@testing-library/react** を採用 -**テストカバレッジ**: - -- Hooks の単体テスト(100% カバレッジ) - - `useProviderActions` - プロバイダ操作 - - `useMcpActions` - MCP 管理 - - `useSettings` 系 - 設定管理 - - `useImportExport` - インポート/エクスポート -- 統合テスト - - アプリのメインフロー - - SettingsDialog の一連操作 - - MCP パネルの機能 - **テスト実行**: ```bash @@ -454,60 +392,68 @@ pnpm test:unit:watch pnpm test:unit --coverage ``` -## 技術スタック +### 技術スタック -**フロントエンド**: React 18 · TypeScript · Vite · TailwindCSS 4 · TanStack Query v5 · react-i18next · react-hook-form · zod · shadcn/ui · @dnd-kit +**フロントエンド**: React 18 · TypeScript · Vite · TailwindCSS 3.4 · TanStack Query v5 · react-i18next · react-hook-form · zod · shadcn/ui · @dnd-kit **バックエンド**: Tauri 2.8 · Rust · serde · tokio · thiserror · tauri-plugin-updater/process/dialog/store/log **テスト**: vitest · MSW · @testing-library/react -## プロジェクト構成 +
+ +
+プロジェクト構成 ``` -├── src/ # フロントエンド (React + TypeScript) -│ ├── components/ # UI コンポーネント (providers/settings/mcp/ui) -│ ├── hooks/ # ビジネスロジック用カスタムフック +├── src/ # フロントエンド (React + TypeScript) +│ ├── components/ +│ │ ├── providers/ # プロバイダ管理 +│ │ ├── mcp/ # MCP パネル +│ │ ├── prompts/ # Prompts 管理 +│ │ ├── skills/ # Skills 管理 +│ │ ├── sessions/ # Session Manager +│ │ ├── proxy/ # Proxy モードパネル +│ │ ├── openclaw/ # OpenClaw 設定パネル +│ │ ├── settings/ # 設定 (Terminal/Backup/About) +│ │ ├── deeplink/ # Deep Link インポート +│ │ ├── env/ # 環境変数管理 +│ │ ├── universal/ # クロスアプリ設定 +│ │ ├── usage/ # 使用量統計 +│ │ └── ui/ # shadcn/ui コンポーネントライブラリ +│ ├── hooks/ # カスタムフック(ビジネスロジック) │ ├── lib/ -│ │ ├── api/ # Tauri API ラッパー (型安全) -│ │ └── query/ # TanStack Query 設定 -│ ├── i18n/locales/ # 翻訳 (zh/en) -│ ├── config/ # プリセット (providers/mcp) -│ └── types/ # TypeScript 型定義 -├── src-tauri/ # バックエンド (Rust) +│ │ ├── api/ # Tauri API ラッパー(型安全) +│ │ └── query/ # TanStack Query 設定 +│ ├── locales/ # 翻訳 (zh/en/ja) +│ ├── config/ # プリセット (providers/mcp) +│ └── types/ # TypeScript 型定義 +├── src-tauri/ # バックエンド (Rust) │ └── src/ -│ ├── commands/ # Tauri コマンド層 (ドメイン別) -│ ├── services/ # ビジネスロジック層 -│ ├── app_config.rs # 設定モデル -│ ├── provider.rs # プロバイダドメインモデル -│ ├── mcp.rs # MCP 同期 & 検証 -│ └── lib.rs # アプリエントリ & トレイメニュー -├── tests/ # フロントエンドテスト -│ ├── hooks/ # 単体テスト -│ └── components/ # 統合テスト -└── assets/ # スクリーンショット & スポンサーリソース +│ ├── commands/ # Tauri コマンド層(ドメイン別) +│ ├── services/ # ビジネスロジック層 +│ ├── database/ # SQLite DAO 層 +│ ├── proxy/ # Proxy モジュール +│ ├── session_manager/ # セッション管理 +│ ├── deeplink/ # Deep Link 処理 +│ └── mcp/ # MCP 同期モジュール +├── tests/ # フロントエンドテスト +└── assets/ # スクリーンショット & パートナーリソース ``` -## 更新履歴 - -詳細は [CHANGELOG.md](CHANGELOG.md) をご覧ください。 - -## 旧 Electron 版 - -[Releases](../../releases) に v2.0.3 の Electron 旧版を残しています。 - -旧版コードが必要な場合は `electron-legacy` ブランチを取得してください。 +
## 貢献 Issue や提案を歓迎します! -PR を送る前に以下をご確認ください: +PR を送る前に以下をご確認ください: - 型チェック: `pnpm typecheck` - フォーマットチェック: `pnpm format:check` - 単体テスト: `pnpm test:unit` -- 💡 新機能の場合は、事前に Issue でディスカッションしていただけると助かります + +新機能の場合は、PR を送る前に Issue でディスカッションしてください。プロジェクトに合わない機能の PR はクローズされる場合があります。 ## Star History diff --git a/README_ZH.md b/README_ZH.md index f7beadb3e..aab319cab 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -1,20 +1,25 @@
-# Claude Code / Codex / Gemini CLI 全方位辅助工具 +# CC Switch -[![Version](https://img.shields.io/badge/version-3.10.2-blue.svg)](https://github.com/farion1231/cc-switch/releases) +### Claude Code、Codex、Gemini CLI、OpenCode 和 OpenClaw 的全方位管理工具 + +[![Version](https://img.shields.io/badge/version-3.11.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest) farion1231%2Fcc-switch | Trendshift -[English](README.md) | 中文 | [日本語](README_JA.md) | [更新日志](CHANGELOG.md) | [v3.9.0 发布说明](docs/release-note-v3.9.0-zh.md) +[English](README.md) | 中文 | [日本語](README_JA.md) | [更新日志](CHANGELOG.md)
## ❤️赞助商 +
+点击折叠 + [![MiniMax](assets/partners/banners/minimax-zh.jpeg)](https://platform.minimaxi.com/subscribe/coding-plan?code=7kYF2VoaCn&source=link) MiniMax M2.5 在编程、工具调用与搜索、办公等核心生产力场景均达到或刷新行业 SOTA,拥有架构师级代码能力与高效任务拆解能力,推理速度较上一代提升 37%、token 消耗更优;100 token/s 连续工作一小时仅需 1 美金,让复杂 Agent 规模化部署经济可行,已在企业多职能场景深度落地,加速全民 Agent 时代到来。 @@ -73,6 +78,22 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更 +
+ +## 为什么选择 CC Switch? + +现代 AI 编程依赖于 Claude Code、Codex、Gemini CLI、OpenCode 和 OpenClaw 等 CLI 工具——但每个工具都有自己的配置格式。切换 API 供应商意味着手动编辑 JSON、TOML 或 `.env` 文件,而在多个工具之间缺乏一个统一管理 MCP, SKILLS 的方式。 + +**CC Switch** 为你提供一个桌面应用来管理所有五个 CLI 工具。无需手动编辑配置文件,你将获得一个可视化界面,一键将供应商导入应用,一键在不同的供应商之间进行切换,内置 50+ 供应商预设、统一的 MCP, SKILLS 管理以及系统托盘即时切换功能——所有操作都基于可靠的 SQLite 数据库和原子写入机制,保护你的配置不被损坏。 + +- **一个应用,五个 CLI 工具** — 在单一界面中管理 Claude Code、Codex、Gemini CLI、OpenCode 和 OpenClaw +- **告别手动编辑** — 50+ 供应商预设,包括 AWS Bedrock、NVIDIA NIM 和社区中转服务;一键即可切换 +- **统一 MCP, SKILLS 管理** — 一个面板管理四个应用的 MCP, SKILLS, 支持双向同步 +- **系统托盘快速切换** — 从托盘菜单即时切换供应商,无需打开完整应用 +- **云同步** — 通过 Dropbox、OneDrive、iCloud 或 WebDAV 服务器在不同设备之间同步供应商数据 +- **跨平台** — 基于 Tauri 2 构建的原生桌面应用,支持 Windows、macOS 和 Linux +- **小工具** - 内置了多种小工具来解决首次安装登录确认、禁止签名、插件拓展同步等多种功能 + ## 界面预览 | 主界面 | 添加供应商 | @@ -81,114 +102,127 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更 ## 功能特性 -### 当前版本:v3.10.2 | [完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.9.0-zh.md) +[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.11.1-zh.md) -**v3.8.0 重大更新(2025-11-28)** +### 供应商管理 -**持久化架构升级 & 全新用户界面** +- **5 个 CLI 工具,50+ 预设** — Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw;复制 key 即可一键导入 +- **通用供应商** — 一份配置同步到多个应用(OpenCode、OpenClaw) +- 一键切换、系统托盘快速访问、拖拽排序、导入导出 -- **SQLite + JSON 双层架构** - - 从 JSON 文件存储迁移到 SQLite + JSON 双层结构 - - 可同步数据(供应商、MCP、Prompts、Skills)存入 SQLite - - 设备级数据(窗口状态、本地路径)保留在 JSON - - 为未来云同步功能奠定基础 - - Schema 版本管理支持数据库迁移 +### 代理与故障转移 -- **全新用户界面** - - 完全重新设计的界面布局 - - 统一的组件样式和更流畅的动画 - - 优化的视觉层次 - - Tailwind CSS 从 v4 降级到 v3.4 以提升浏览器兼容性 +- **本地代理热切换** — 格式转换、自动故障转移、熔断器、供应商健康监控和整流器 +- **应用级代理接管** — 独立为 Claude、Codex 或 Gemini 配置代理,具体到单个供应商 -- **日语支持** - - 新增日语界面支持(现支持中文/英文/日语) +### MCP、Prompts 与 Skills -- **开机自启** - - 在设置中一键开启/关闭 - - 使用平台原生 API(注册表/LaunchAgent/XDG autostart) +- **统一 MCP 面板** — 管理 4 个应用的 MCP 服务器,双向同步,支持 Deep Link 导入 +- **Prompts** — Markdown 编辑器,跨应用同步(CLAUDE.md / AGENTS.md / GEMINI.md),回填保护 +- **Skills** — 从 GitHub 仓库或 ZIP 文件一键安装,自定义仓库管理,支持软连接和文件复制 -- **Skills 递归扫描** - - 支持多层目录结构 - - 允许不同仓库的同名技能 +### 用量与成本追踪 -- **关键 Bug 修复** - - 修复更新供应商时自定义端点丢失问题 - - 修复 Gemini 配置写入问题 - - 修复 Linux WebKitGTK 渲染问题 +- **用量仪表盘** — 跨供应商追踪支出、请求数和 Token 用量,趋势图表、详细请求日志和自定义模型定价 -**v3.7.0 亮点** +### 会话管理器与工作区 -**六大核心功能,18,000+ 行新增代码** +- 浏览、搜索和恢复全部应用对话历史 +- **工作区编辑器**(OpenClaw)— 编辑 Agent 文件(AGENTS.md、SOUL.md 等),支持 Markdown 预览 -- **Gemini CLI 集成** - - 第三个支持的 AI CLI(Claude Code / Codex / Gemini) - - 双文件配置支持(`.env` + `settings.json`) - - 完整 MCP 服务器管理 - - 预设:Google Official (OAuth) / PackyCode / 自定义 +### 系统与平台 -- **Claude Skills 管理系统** - - 从 GitHub 仓库自动扫描技能(预配置 3 个精选仓库) - - 一键安装/卸载到 `~/.claude/skills/` - - 自定义仓库支持 + 子目录扫描 - - 完整生命周期管理(发现/安装/更新) +- **云同步** — 自定义配置目录(Dropbox、OneDrive、iCloud、坚果云、NAS)及 WebDAV 服务器同步 +- **Deep Link** (`ccswitch://`) — 通过 URL 一键导入供应商、MCP 服务器、提示词和技能 +- 深色 / 浅色 / 跟随系统主题、开机自启、自动更新、原子写入、自动备份、国际化(中/英/日) -- **Prompts 管理系统** - - 多预设系统提示词管理(无限数量,快速切换) - - 跨应用支持(Claude: `CLAUDE.md` / Codex: `AGENTS.md` / Gemini: `GEMINI.md`) - - Markdown 编辑器(CodeMirror 6 + 实时预览) - - 智能回填保护,保留手动修改 +## 常见问题 -- **MCP v3.7.0 统一架构** - - 单一面板管理三个应用的 MCP 服务器 - - 新增 SSE (Server-Sent Events) 传输类型 - - 智能 JSON 解析器 + Codex TOML 格式自动修正 - - 统一导入/导出 + 双向同步 +
+CC Switch 支持哪些 AI CLI 工具? -- **深度链接协议** - - `ccswitch://` 协议注册(全平台) - - 通过共享链接一键导入供应商配置 - - 安全验证 + 生命周期集成 +CC Switch 支持五个工具:**Claude Code**、**Codex**、**Gemini CLI**、**OpenCode** 和 **OpenClaw**。每个工具都有专属的供应商预设和配置管理。 -- **环境变量冲突检测** - - 自动检测跨应用配置冲突(Claude/Codex/Gemini/MCP) - - 可视化冲突指示器 + 解决建议 - - 覆盖警告 + 更改前备份 +
-**核心功能** +
+切换供应商后需要重启终端吗? -- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置 -- **AWS Bedrock 支持**:内置 AWS Bedrock 供应商预设,支持 AKSK 和 API Key 两种认证方式,支持跨区域推理(global/us/eu/apac),覆盖 Claude Code 和 OpenCode -- **速度测试**:测量 API 端点延迟,可视化连接质量指示器 -- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个) -- **国际化支持**:完整的中英文本地化(UI、错误、托盘) -- **Claude 插件同步**:一键应用或恢复 Claude 插件配置 +大多数工具需要重启终端或 CLI 工具才能使更改生效。例外的是 **Claude Code**,它目前支持供应商数据的热切换,无需重启。 -**v3.6 亮点** +
-- 供应商复制 & 拖拽排序 -- 多端点管理 & 自定义配置目录(支持云同步) -- 细粒度模型配置(四层:Haiku/Sonnet/Opus/自定义) -- WSL 环境支持,配置目录切换自动同步 -- 100% hooks 测试覆盖 & 完整架构重构 +
+切换供应商之后我的插件配置怎么不见了? -**系统功能** +CC Switch 使用“通用配置片段”功能,在不同的供应商之间传递 Key 和请求地址之外的通用数据,您可以在“编辑供应商”菜单的“通用配置面板”里,点击“从当前供应商提取”,把所有的通用数据提取到通用配置中,之后在新建“供应商”的时候,只要勾选“写入通用配置”(默认勾选),就会把插件等数据写入到新的供应商配置中。您的所有配置项都会保存在运行本软件的时候,第一次导入的默认供应商里面,不会丢失。 -- 系统托盘快速切换 -- 单实例守护 -- 内置自动更新器 -- 原子写入与回滚保护 +
+ +
+macOS 提示"未知开发者"警告 — 如何解决? + +这是由于作者没有苹果开发者账号(正在注册中)。关闭警告后,前往**系统设置 → 隐私与安全性 → 仍要打开**。之后应用即可正常打开。 + +
+ +
+为什么总有一个正在激活中的供应商无法删除? + +本软件的设计原则是“最小侵入性”,即使卸载本软件,也不会影响应用的正常使用。 + +所以系统总会保留一个正在激活中的配置,因为如果将所有配置全部删除,该应用将无法正常使用。如果你不经常使用某个对应的应用,可以在设置中关掉该应用的显示。如果你想切换回官方登录,可以参考下条。 + +
+ +
+如何切换回官方登录? + +可以在预设供应商里面添加一个官方供应商。切换过去之后,执行一遍 Log out / Log in 流程,之后便可以在官方供应商和第三方供应商之间随意切换。CodeX 可以在不同官方供应商之间进行切换,方便多个 Plus 或者 Team 账号之间切换。 + +
+ +
+我的数据存储在哪里? + +- **数据库**:`~/.cc-switch/cc-switch.db`(SQLite — 供应商、MCP、提示词、技能) +- **本地设置**:`~/.cc-switch/settings.json`(设备级 UI 偏好设置) +- **备份**:`~/.cc-switch/backups/`(自动轮换,保留最近 10 个) +- **SKILLS**:`~/.cc-switch/skills/`(默认通过软链接连接到对应应用) + +
+ +## 快速开始 + +### 基本使用 + +1. **添加供应商**:点击"添加供应商" → 选择预设或创建自定义配置 +2. **切换供应商**: + - 主界面:选择供应商 → 点击"启用" + - 系统托盘:直接点击供应商名称(立即生效) +3. **生效方式**:重启终端或对应的 CLI 工具以应用更改(CLaude Code 无需重启) +4. **恢复官方登录**:添加"官方登录"预设,重启 CLI 工具后按照其登录/OAuth 流程操作 + +### MCP、Prompts、Skills 与会话 + +- **MCP**:点击"MCP"按钮 → 通过模板或自定义配置添加服务器 → 切换各应用同步开关 +- **Prompts**:点击"Prompts" → 使用 Markdown 编辑器创建预设 → 激活后同步到 live 文件 +- **Skills**:点击"Skills" → 浏览 GitHub 仓库 → 一键安装到全部应用 +- **会话**:点击"Sessions" → 浏览和搜索和恢复全部应用对话历史 + +> **注意**:首次启动可以手动导入现有 CLI 工具配置作为默认供应商。 ## 下载安装 ### 系统要求 -- **Windows**: Windows 10 及以上 -- **macOS**: macOS 10.15 (Catalina) 及以上 -- **Linux**: Ubuntu 22.04+ / Debian 11+ / Fedora 34+ 等主流发行版 +- **Windows**:Windows 10 及以上 +- **macOS**:macOS 10.15 (Catalina) 及以上 +- **Linux**:Ubuntu 22.04+ / Debian 11+ / Fedora 34+ 等主流发行版 ### Windows 用户 -从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Windows.msi` 安装包或者 `CC-Switch-v{版本号}-Windows-Portable.zip` 绿色版。 +从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Windows.msi` 安装包或 `CC-Switch-v{版本号}-Windows-Portable.zip` 绿色版。 ### macOS 用户 @@ -209,9 +243,9 @@ brew upgrade --cask cc-switch 从 [Releases](../../releases) 页面下载 `CC-Switch-v{版本号}-macOS.zip` 解压使用。 -> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开 +> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开。 -### ArchLinux 用户 +### Arch Linux 用户 **通过 paru 安装(推荐)** @@ -235,89 +269,8 @@ flatpak install --user ./CC-Switch-v{版本号}-Linux.flatpak flatpak run com.ccswitch.desktop ``` -## 快速开始 - -### 基本使用 - -1. **添加供应商**:点击"添加供应商" → 选择预设或创建自定义配置 -2. **切换供应商**: - - 主界面:选择供应商 → 点击"启用" - - 系统托盘:直接点击供应商名称(立即生效) -3. **生效方式**:重启终端或 Claude Code / Codex / Gemini 客户端以应用更改 -4. **恢复官方登录**:选择"官方登录"预设(Claude/Codex)或"Google 官方"预设(Gemini),重启对应客户端后按照其登录/OAuth 流程操作 - -### MCP 管理 - -- **位置**:点击右上角"MCP"按钮 -- **添加服务器**: - - 使用内置模板(mcp-fetch、mcp-filesystem 等) - - 支持 stdio / http / sse 三种传输类型 - - 为不同应用配置独立的 MCP 服务器 -- **启用/禁用**:切换开关以控制哪些服务器同步到 live 配置 -- **同步**:启用的服务器自动同步到各应用的 live 文件 -- **导入/导出**:支持从 Claude/Codex/Gemini 配置文件导入现有 MCP 服务器 - -### Skills 管理(v3.7.0 新增) - -- **位置**:点击右上角"Skills"按钮 -- **发现技能**: - - 自动扫描预配置的 GitHub 仓库(Anthropic 官方、ComposioHQ、社区等) - - 添加自定义仓库(支持子目录扫描) -- **安装技能**:点击"安装"一键安装到 `~/.claude/skills/` -- **卸载技能**:点击"卸载"安全移除并清理状态 -- **管理仓库**:添加/删除自定义 GitHub 仓库 - -### Prompts 管理(v3.7.0 新增) - -- **位置**:点击右上角"Prompts"按钮 -- **创建预设**: - - 创建无限数量的系统提示词预设 - - 使用 Markdown 编辑器编写提示词(语法高亮 + 实时预览) -- **切换预设**:选择预设 → 点击"激活"立即应用 -- **同步机制**: - - Claude: `~/.claude/CLAUDE.md` - - Codex: `~/.codex/AGENTS.md` - - Gemini: `~/.gemini/GEMINI.md` -- **保护机制**:切换前自动保存当前提示词内容,保留手动修改 - -### 配置文件 - -**Claude Code** - -- Live 配置:`~/.claude/settings.json`(或 `claude.json`) -- API key 字段:`env.ANTHROPIC_AUTH_TOKEN` 或 `env.ANTHROPIC_API_KEY` -- MCP 服务器:`~/.claude.json` → `mcpServers` - -**Codex** - -- Live 配置:`~/.codex/auth.json`(必需)+ `config.toml`(可选) -- API key 字段:`auth.json` 中的 `OPENAI_API_KEY` -- MCP 服务器:`~/.codex/config.toml` → `[mcp_servers]` 表 - -**Gemini** - -- Live 配置:`~/.gemini/.env`(API Key)+ `~/.gemini/settings.json`(保存认证模式) -- API key 字段:`.env` 文件中的 `GEMINI_API_KEY` 或 `GOOGLE_GEMINI_API_KEY` -- 环境变量:支持 `GOOGLE_GEMINI_BASE_URL`、`GEMINI_MODEL` 等自定义变量 -- MCP 服务器:`~/.gemini/settings.json` → `mcpServers` -- 托盘快速切换:每次切换供应商都会重写 `~/.gemini/.env`,无需重启 Gemini CLI 即可生效 - -**CC Switch 存储(v3.8.0 新架构)** - -- 数据库(SSOT):`~/.cc-switch/cc-switch.db`(SQLite,存储供应商、MCP、Prompts、Skills) -- 本地设置:`~/.cc-switch/settings.json`(设备级设置) -- 备份:`~/.cc-switch/backups/`(自动轮换,保留 10 个) - -### 云同步设置 - -1. 前往设置 → "自定义配置目录" -2. 选择您的云同步文件夹(Dropbox、OneDrive、iCloud、坚果云等) -3. 重启应用以应用 -4. 在其他设备上重复操作以启用跨设备同步 - -> **注意**:首次启动会自动导入现有 Claude/Codex 配置作为默认供应商。 - -## 架构总览 +
+架构总览 ### 设计原则 @@ -352,16 +305,15 @@ flatpak run com.ccswitch.desktop - **ProviderService**:供应商增删改查、切换、回填、排序 - **McpService**:MCP 服务器管理、导入导出、live 文件同步 +- **ProxyService**:本地 Proxy 模式,支持热切换和格式转换 +- **SessionManager**:Claude Code 对话历史浏览 - **ConfigService**:配置导入导出、备份轮换 - **SpeedtestService**:API 端点延迟测量 -**v3.6 重构** +
-- 后端:5 阶段重构(错误处理 → 命令拆分 → 测试 → 服务 → 并发) -- 前端:4 阶段重构(测试基础 → hooks → 组件 → 清理) -- 测试:100% hooks 覆盖 + 集成测试(vitest + MSW) - -## 开发 +
+开发指南 ### 环境要求 @@ -422,7 +374,7 @@ cargo test test_name cargo test --features test-hooks ``` -### 测试说明(v3.6 新增) +### 测试说明 **前端测试**: @@ -430,18 +382,6 @@ cargo test --features test-hooks - 使用 **MSW (Mock Service Worker)** 模拟 Tauri API 调用 - 使用 **@testing-library/react** 进行组件测试 -**测试覆盖**: - -- Hooks 单元测试(100% 覆盖) - - `useProviderActions` - 供应商操作 - - `useMcpActions` - MCP 管理 - - `useSettings` 系列 - 设置管理 - - `useImportExport` - 导入导出 -- 集成测试 - - App 主应用流程 - - SettingsDialog 完整交互 - - MCP 面板功能 - **运行测试**: ```bash @@ -455,49 +395,56 @@ pnpm test:unit:watch pnpm test:unit --coverage ``` -## 技术栈 +### 技术栈 -**前端**:React 18 · TypeScript · Vite · TailwindCSS 4 · TanStack Query v5 · react-i18next · react-hook-form · zod · shadcn/ui · @dnd-kit +**前端**:React 18 · TypeScript · Vite · TailwindCSS 3.4 · TanStack Query v5 · react-i18next · react-hook-form · zod · shadcn/ui · @dnd-kit **后端**:Tauri 2.8 · Rust · serde · tokio · thiserror · tauri-plugin-updater/process/dialog/store/log **测试**:vitest · MSW · @testing-library/react -## 项目结构 +
+ +
+项目结构 ``` -├── src/ # 前端 (React + TypeScript) -│ ├── components/ # UI 组件 (providers/settings/mcp/ui) -│ ├── hooks/ # 自定义 hooks (业务逻辑) +├── src/ # 前端 (React + TypeScript) +│ ├── components/ +│ │ ├── providers/ # 供应商管理 +│ │ ├── mcp/ # MCP 面板 +│ │ ├── prompts/ # Prompts 管理 +│ │ ├── skills/ # Skills 管理 +│ │ ├── sessions/ # 会话管理器 +│ │ ├── proxy/ # Proxy 模式面板 +│ │ ├── openclaw/ # OpenClaw 配置面板 +│ │ ├── settings/ # 设置(终端/备份/关于) +│ │ ├── deeplink/ # Deep Link 导入 +│ │ ├── env/ # 环境变量管理 +│ │ ├── universal/ # 跨应用配置 +│ │ ├── usage/ # 用量统计 +│ │ └── ui/ # shadcn/ui 组件库 +│ ├── hooks/ # 自定义 hooks(业务逻辑) │ ├── lib/ -│ │ ├── api/ # Tauri API 封装(类型安全) -│ │ └── query/ # TanStack Query 配置 -│ ├── i18n/locales/ # 翻译 (zh/en) -│ ├── config/ # 预设 (providers/mcp) -│ └── types/ # TypeScript 类型定义 -├── src-tauri/ # 后端 (Rust) +│ │ ├── api/ # Tauri API 封装(类型安全) +│ │ └── query/ # TanStack Query 配置 +│ ├── locales/ # 翻译 (zh/en/ja) +│ ├── config/ # 预设 (providers/mcp) +│ └── types/ # TypeScript 类型定义 +├── src-tauri/ # 后端 (Rust) │ └── src/ -│ ├── commands/ # Tauri 命令层(按领域) -│ ├── services/ # 业务逻辑层 -│ ├── app_config.rs # 配置数据模型 -│ ├── provider.rs # 供应商领域模型 -│ ├── mcp.rs # MCP 同步与校验 -│ └── lib.rs # 应用入口 & 托盘菜单 -├── tests/ # 前端测试 -│ ├── hooks/ # 单元测试 -│ └── components/ # 集成测试 -└── assets/ # 截图 & 合作商资源 +│ ├── commands/ # Tauri 命令层(按领域) +│ ├── services/ # 业务逻辑层 +│ ├── database/ # SQLite DAO 层 +│ ├── proxy/ # Proxy 模块 +│ ├── session_manager/ # 会话管理 +│ ├── deeplink/ # Deep Link 处理 +│ └── mcp/ # MCP 同步模块 +├── tests/ # 前端测试 +└── assets/ # 截图 & 合作商资源 ``` -## 更新日志 - -查看 [CHANGELOG.md](CHANGELOG.md) 了解版本更新详情。 - -## Electron 旧版 - -[Releases](../../releases) 里保留 v2.0.3 Electron 旧版 - -如果需要旧版 Electron 代码,可以拉取 electron-legacy 分支 +
## 贡献 @@ -508,7 +455,8 @@ pnpm test:unit --coverage - 通过类型检查:`pnpm typecheck` - 通过格式检查:`pnpm format:check` - 通过单元测试:`pnpm test:unit` -- 💡 新功能开发前,欢迎先开 issue 讨论实现方案 + +新功能开发前,欢迎先开 Issue 讨论实现方案,不适合项目的功能性 PR 有可能会被关闭。 ## Star History From 2eca90e43a574df2e2e4641235768f356c8cdc80 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 2 Mar 2026 11:47:36 +0800 Subject: [PATCH 10/34] feat: auto-extract common config snippets from live files on first run During app startup, iterate all app types and extract non-provider-specific config fields from live configuration files into the database. This runs only when no snippet exists yet for a given app type, enabling incremental extraction as new apps are configured. --- src-tauri/Cargo.lock | 2 +- src-tauri/src/lib.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7d204de25..311bd6469 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "cc-switch" -version = "3.11.0" +version = "3.11.1" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cea4be170..d38a12f7c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -560,6 +560,59 @@ pub fn run() { } } + // 5. Auto-extract common config snippets from live files (when snippet is missing) + for app_type in crate::app_config::AppType::all() { + // Skip if snippet already exists + if app_state + .db + .get_config_snippet(app_type.as_str()) + .ok() + .flatten() + .is_some() + { + continue; + } + + // Try to read the live config file for this app type + let settings = + match crate::services::provider::ProviderService::read_live_settings( + app_type.clone(), + ) { + Ok(s) => s, + Err(_) => continue, // No live config file, skip silently + }; + + // Extract common config (strip provider-specific fields) + match crate::services::provider::ProviderService::extract_common_config_snippet_from_settings( + app_type.clone(), + &settings, + ) { + Ok(snippet) if !snippet.is_empty() && snippet != "{}" => { + match app_state + .db + .set_config_snippet(app_type.as_str(), Some(snippet)) + { + Ok(()) => log::info!( + "✓ Auto-extracted common config snippet for {}", + app_type.as_str() + ), + Err(e) => log::warn!( + "✗ Failed to save config snippet for {}: {e}", + app_type.as_str() + ), + } + } + Ok(_) => log::debug!( + "○ Live config for {} has no extractable common fields", + app_type.as_str() + ), + Err(e) => log::warn!( + "✗ Failed to extract config snippet for {}: {e}", + app_type.as_str() + ), + } + } + // 迁移旧的 app_config_dir 配置到 Store if let Err(e) = app_store::migrate_app_config_dir_from_settings(app.handle()) { log::warn!("迁移 app_config_dir 失败: {e}"); From ce9c23833ad9b0684bebbd988932e5ffcb052f73 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 2 Mar 2026 23:16:58 +0800 Subject: [PATCH 11/34] docs: add OpenClaw coverage and complete settings docs for user manual - Add OpenClaw as the 5th supported app across all doc chapters (1-3, 5) - Add OpenClaw provider presets table to 2.1-add.md (30 presets) - Add OpenClaw config section to 5.1-config-files.md (JSON5 format) - Complete 1.5-settings.md with missing sections: app visibility, skill sync method, terminal settings, proxy tab, WebDAV cloud sync, backup/restore, and log configuration - Fix deeplink parser.rs to accept 'opencode' and 'openclaw' app types - Update 5.3-deeplink.md with new app type parameters - Remove incorrect OpenCode references from proxy docs (4.1-4.4) --- .../1-getting-started/1.1-introduction.md | 5 +- .../1-getting-started/1.3-interface.md | 5 +- .../1-getting-started/1.5-settings.md | 163 +++++++++++++++--- docs/user-manual/2-providers/2.1-add.md | 41 ++++- docs/user-manual/3-extensions/3.1-mcp.md | 2 + docs/user-manual/3-extensions/3.2-prompts.md | 2 + docs/user-manual/3-extensions/3.3-skills.md | 42 +++-- docs/user-manual/4-proxy/4.1-service.md | 2 +- docs/user-manual/4-proxy/4.2-takeover.md | 3 +- docs/user-manual/4-proxy/4.3-failover.md | 3 +- docs/user-manual/4-proxy/4.4-usage.md | 2 +- docs/user-manual/5-faq/5.1-config-files.md | 64 ++++++- docs/user-manual/5-faq/5.3-deeplink.md | 4 +- docs/user-manual/README.md | 2 +- src-tauri/src/deeplink/parser.rs | 21 ++- 15 files changed, 300 insertions(+), 61 deletions(-) diff --git a/docs/user-manual/1-getting-started/1.1-introduction.md b/docs/user-manual/1-getting-started/1.1-introduction.md index 536662c30..efcd3cefe 100644 --- a/docs/user-manual/1-getting-started/1.1-introduction.md +++ b/docs/user-manual/1-getting-started/1.1-introduction.md @@ -2,14 +2,14 @@ ## 什么是 CC Switch -CC Switch 是一款跨平台桌面应用,专为使用 AI 编程工具的开发者设计。它帮助你统一管理 **Claude Code**、**Codex**、**Gemini CLI**、**OpenCode** 四大 AI 编程工具的配置。 +CC Switch 是一款跨平台桌面应用,专为使用 AI 编程工具的开发者设计。它帮助你统一管理 **Claude Code**、**Codex**、**Gemini CLI**、**OpenCode** 和 **OpenClaw** 五大 AI 编程工具的配置。 ## 解决什么问题 在日常开发中,你可能会遇到这些痛点: - **多供应商切换麻烦**:使用不同的 API 供应商(官方、中转服务商),需要手动修改配置文件 -- **配置分散难管理**:Claude、Codex、Gemini、OpenCode 各有独立的配置文件,格式不同 +- **配置分散难管理**:Claude、Codex、Gemini、OpenCode、OpenClaw 各有独立的配置文件,格式不同 - **无法监控用量**:不知道 API 调用了多少次,花了多少钱 - **服务不稳定**:单一供应商出问题时,整个工作流中断 @@ -43,6 +43,7 @@ CC Switch 通过统一的界面解决这些问题。 | **Codex** | OpenAI 的代码生成工具 | | **Gemini CLI** | Google 的 AI 命令行工具 | | **OpenCode** | 开源 AI 编程终端工具 | +| **OpenClaw** | 开源 AI 编程助手(多供应商网关) | ## 支持的平台 diff --git a/docs/user-manual/1-getting-started/1.3-interface.md b/docs/user-manual/1-getting-started/1.3-interface.md index e88afb96e..358221e97 100644 --- a/docs/user-manual/1-getting-started/1.3-interface.md +++ b/docs/user-manual/1-getting-started/1.3-interface.md @@ -11,7 +11,7 @@ | ① | Logo | 点击访问 GitHub 项目页 | | ② | 设置按钮 | 打开设置页面(快捷键 `Cmd/Ctrl + ,`) | | ③ | 代理开关 | 启动/停止本地代理服务 | -| ④ | 应用切换器 | 切换 Claude / Codex / Gemini / OpenCode | +| ④ | 应用切换器 | 切换 Claude / Codex / Gemini / OpenCode / OpenClaw | | ⑤ | 功能区 | Skills / Prompts / MCP 入口 | | ⑥ | 添加按钮 | 添加新供应商 | @@ -23,6 +23,7 @@ - **Codex** - 管理 Codex 配置 - **Gemini** - 管理 Gemini CLI 配置 - **OpenCode** - 管理 OpenCode 配置 +- **OpenClaw** - 管理 OpenClaw 配置 切换后,供应商列表会显示对应应用的配置。 @@ -99,7 +100,7 @@ CC Switch 在系统托盘显示图标,提供快速操作入口。 | 菜单项 | 功能 | |--------|------| | 打开主界面 | 显示主窗口并聚焦 | -| 应用分组 | 按 Claude/Codex/Gemini/OpenCode 分组显示供应商 | +| 应用分组 | 按 Claude/Codex/Gemini/OpenCode/OpenClaw 分组显示供应商 | | 供应商列表 | 点击切换,当前启用的显示勾选标记 | | 退出 | 完全退出应用 | diff --git a/docs/user-manual/1-getting-started/1.5-settings.md b/docs/user-manual/1-getting-started/1.5-settings.md index 9c73e8aca..3f9a591cb 100644 --- a/docs/user-manual/1-getting-started/1.5-settings.md +++ b/docs/user-manual/1-getting-started/1.5-settings.md @@ -11,21 +11,21 @@ CC Switch 支持三种语言: -| 语言 | 说明 | -|------|------| +| 语言 | 说明 | +| -------- | -------- | | 简体中文 | 默认语言 | -| English | 英文界面 | -| 日本語 | 日文界面 | +| English | 英文界面 | +| 日本語 | 日文界面 | 切换语言后立即生效,无需重启。 ## 主题设置 -| 选项 | 说明 | -|------|------| +| 选项 | 说明 | +| -------- | --------------------------- | | 跟随系统 | 自动匹配系统的深色/浅色模式 | -| 浅色 | 始终使用浅色主题 | -| 深色 | 始终使用深色主题 | +| 浅色 | 始终使用浅色主题 | +| 深色 | 始终使用深色主题 | ## 窗口行为 @@ -39,10 +39,10 @@ CC Switch 支持三种语言: ### 关闭行为 -| 选项 | 说明 | -|------|------| +| 选项 | 说明 | +| ------------ | ---------------------------- | | 最小化到托盘 | 点击关闭按钮时隐藏到系统托盘 | -| 直接退出 | 点击关闭按钮时完全退出应用 | +| 直接退出 | 点击关闭按钮时完全退出应用 | 推荐使用「最小化到托盘」,方便通过托盘快速切换供应商。 @@ -58,6 +58,37 @@ CC Switch 支持三种语言: > ⚠️ **注意**:此选项会写入 `~/.claude/settings.json` 的 `skipIntroduction` 字段。 +### 应用可见性 + +选择在应用切换器中显示哪些应用。每个应用可以独立开关,但至少保留一个。 + +可配置的应用:Claude、Codex、Gemini、OpenCode、OpenClaw。 + +> 💡 **使用场景**:如果你只使用 Claude Code 和 Codex CLI,可以隐藏其他应用,保持界面简洁。 + +### Skills 同步方式 + +设置技能安装到各应用目录时的同步方式: + +| 方式 | 说明 | +| ----------------- | ---------------------------------------------------- | +| 软链接(Symlink) | 创建符号链接指向技能源文件,占用空间小,更新实时同步 | +| 复制(Copy) | 将技能文件完整复制到目标目录 | + +> 💡 **推荐**:默认使用软链接方式。如果遇到权限问题,可切换为复制方式。 + +### 终端设置 + +选择 CC Switch 打开终端时使用的终端应用程序。 + +支持的终端(按平台): + +| 平台 | 终端选项 | +| ------- | ------------------------------------------------------------------ | +| macOS | Terminal、iTerm2、Alacritty、Kitty、Ghostty、WezTerm | +| Windows | CMD、PowerShell、Windows Terminal | +| Linux | GNOME Terminal、Konsole、Xfce4 Terminal、Alacritty、Kitty、Ghostty | + ## 目录配置 ### 应用配置目录 @@ -68,11 +99,13 @@ CC Switch 自身数据的存储位置,默认为 `~/.cc-switch/`。 可以自定义各 CLI 工具的配置目录: -| 配置 | 默认值 | 说明 | -|------|--------|------| -| Claude 目录 | `~/.claude/` | Claude Code 配置目录 | -| Codex 目录 | `~/.codex/` | Codex 配置目录 | -| Gemini 目录 | `~/.gemini/` | Gemini CLI 配置目录 | +| 配置 | 默认值 | 说明 | +| ------------- | -------------- | -------------------- | +| Claude 目录 | `~/.claude/` | Claude Code 配置目录 | +| Codex 目录 | `~/.codex/` | Codex 配置目录 | +| Gemini 目录 | `~/.gemini/` | Gemini CLI 配置目录 | +| OpenCode 目录 | `~/.opencode/` | OpenCode 配置目录 | +| OpenClaw 目录 | `~/.openclaw/` | OpenClaw 配置目录 | > ⚠️ **注意**:修改目录后需要重启应用,且对应的 CLI 工具也需要配置相同的目录。 @@ -98,6 +131,89 @@ CC Switch 自身数据的存储位置,默认为 `~/.cc-switch/`。 > ⚠️ **注意**:导入会覆盖现有配置,建议先导出当前配置作为备份。 +## 代理设置 + +设置 → 代理 Tab + +代理 Tab 集中管理所有代理相关功能: + +### 本地代理 + +启动/停止本地代理服务,配置监听地址和端口。详见 [4.1 代理服务](../4-proxy/4.1-service.md)。 + +### 故障转移 + +按应用(Claude/Codex/Gemini)配置故障转移队列和自动切换策略。详见 [4.3 故障转移](../4-proxy/4.3-failover.md)。 + +### 定价矫正器 + +配置模型定价矫正规则,用于代理计费统计的校准。 + +### 全局出站代理 + +配置 CC Switch 的出站 HTTP/HTTPS 代理,适用于需要通过代理访问外部 API 的场景。 + +## 高级设置 + +设置 → 高级 Tab + +### 配置目录 + +自定义各应用的配置文件目录。详见下方「目录配置」章节。 + +### 数据管理 + +导入/导出配置备份。详见下方「数据管理」章节。 + +### 备份与恢复 + +管理自动备份: + +| 配置 | 说明 | +| -------- | -------------------------- | +| 备份间隔 | 自动备份的时间间隔(小时) | +| 保留数量 | 保留的备份份数 | + +支持查看备份列表和从备份恢复。 + +### 云同步(WebDAV) + +通过 WebDAV 协议在多台设备间同步配置。 + +| 配置项 | 说明 | +| -------- | ------------------------------------- | +| 服务预设 | 坚果云 / Nextcloud / 群晖 / 自定义 | +| 服务地址 | WebDAV 服务器 URL | +| 用户名 | 登录用户名 | +| 密码 | 登录密码(应用专用密码) | +| 远程目录 | 远程存储路径(默认 `cc-switch-sync`) | +| 配置名称 | 设备配置文件名(默认 `default`) | +| 自动同步 | 开启后自动上传变更 | + +操作: + +- **测试连接**:验证 WebDAV 配置是否正确 +- **保存**:保存配置并自动测试 +- **上传**:将本地数据上传到远程 +- **下载**:从远程下载数据到本地 + +> ⚠️ **注意**:上传会覆盖远程数据,下载会覆盖本地数据。操作前请确认。 + +### 日志配置 + +| 配置项 | 说明 | +| -------- | ----------------------------------- | +| 启用日志 | 开启/关闭应用日志记录 | +| 日志级别 | error / warn / info / debug / trace | + +日志级别说明: + +- **error** - 仅记录错误 +- **warn** - 记录警告和错误 +- **info** - 记录一般信息(推荐) +- **debug** - 记录调试信息 +- **trace** - 记录所有详细信息 + ## 关于页面 设置 → 关于 Tab @@ -105,6 +221,7 @@ CC Switch 自身数据的存储位置,默认为 `~/.cc-switch/`。 ### 版本信息 显示当前 CC Switch 版本号,支持: + - 查看发布说明 - 检查更新 - 下载并安装新版本 @@ -113,11 +230,13 @@ CC Switch 自身数据的存储位置,默认为 `~/.cc-switch/`。 自动检测已安装的 CLI 工具版本: -| 工具 | 检测内容 | -|------|----------| -| Claude | 当前版本、最新版本 | -| Codex | 当前版本、最新版本 | -| Gemini | 当前版本、最新版本 | +| 工具 | 检测内容 | +| -------- | ------------------ | +| Claude | 当前版本、最新版本 | +| Codex | 当前版本、最新版本 | +| Gemini | 当前版本、最新版本 | +| OpenCode | 当前版本、最新版本 | +| OpenClaw | 当前版本、最新版本 | 点击「刷新」按钮可重新检测。 @@ -129,6 +248,8 @@ CC Switch 自身数据的存储位置,默认为 `~/.cc-switch/`。 npm i -g @anthropic-ai/claude-code@latest npm i -g @openai/codex@latest npm i -g @google/gemini-cli@latest +npm i -g opencode@latest +npm i -g openclaw@latest ``` 点击「复制」按钮可复制到剪贴板。 diff --git a/docs/user-manual/2-providers/2.1-add.md b/docs/user-manual/2-providers/2.1-add.md index 12425a4d8..812761005 100644 --- a/docs/user-manual/2-providers/2.1-add.md +++ b/docs/user-manual/2-providers/2.1-add.md @@ -5,7 +5,7 @@ 点击主界面右上角的 **+** 按钮,打开添加供应商面板。 面板分为两个 Tab: -- **应用专属供应商**:仅用于当前选中的应用(Claude/Codex/Gemini/OpenCode) +- **应用专属供应商**:仅用于当前选中的应用(Claude/Codex/Gemini/OpenCode/OpenClaw) - **统一供应商**:跨应用共享的配置 ## 使用预设添加 @@ -114,6 +114,41 @@ > 💡 预设列表持续更新中,以应用内实际显示为准。 +#### OpenClaw 预设 + +| 预设名称 | 说明 | +|----------|------| +| DeepSeek | DeepSeek 模型 | +| 智谱 GLM | 智谱 AI 的 GLM 模型 | +| 智谱 GLM en | 智谱 AI(英文版) | +| Qwen Coder | 通义千问编码模型 | +| Kimi k2.5 | Moonshot Kimi-k2.5 模型 | +| Kimi For Coding | Kimi 编程专用模型 | +| MiniMax | MiniMax 模型 | +| MiniMax en | MiniMax(英文版) | +| KAT-Coder | KAT-Coder 模型 | +| Longcat | Longcat AI | +| DouBaoSeed | 豆包 Seed 模型 | +| BaiLing | 百灵 AI | +| Xiaomi MiMo | 小米 MiMo 模型 | +| AiHubMix | AiHubMix 聚合服务 | +| DMXAPI | DMXAPI 中转服务 | +| OpenRouter | 聚合路由服务 | +| ModelScope | 魔搭社区 | +| SiliconFlow | 硅基流动 | +| SiliconFlow en | 硅基流动(英文版) | +| Nvidia | Nvidia AI 服务 | +| PackyCode | PackyCode 中转服务 | +| Cubence | Cubence 服务 | +| AIGoCode | AIGoCode 服务 | +| RightCode | RightCode 服务 | +| AICodeMirror | AICodeMirror 服务 | +| AICoding | AICoding 服务 | +| CrazyRouter | CrazyRouter 服务 | +| SSSAiCode | SSSAiCode 服务 | +| AWS Bedrock | AWS Bedrock 服务 | +| OpenAI Compatible | OpenAI 兼容接口 | + ## 自定义配置 选择「自定义」预设后,需要手动编辑 JSON 配置。 @@ -204,7 +239,7 @@ requires_openai_auth = true ## 统一供应商 -统一供应商可以跨 Claude/Codex/Gemini/OpenCode 共享配置,适用于支持多种 API 格式的中转服务。 +统一供应商可以跨 Claude/Codex/Gemini/OpenCode/OpenClaw 共享配置,适用于支持多种 API 格式的中转服务。 ### 创建统一供应商 @@ -214,7 +249,7 @@ requires_openai_auth = true - 名称 - API Key - 端点地址 -4. 勾选要同步的应用(Claude/Codex/Gemini/OpenCode) +4. 勾选要同步的应用(Claude/Codex/Gemini/OpenCode/OpenClaw) 5. 保存 ### 同步机制 diff --git a/docs/user-manual/3-extensions/3.1-mcp.md b/docs/user-manual/3-extensions/3.1-mcp.md index 8ee7c3cb0..d3eca8648 100644 --- a/docs/user-manual/3-extensions/3.1-mcp.md +++ b/docs/user-manual/3-extensions/3.1-mcp.md @@ -105,6 +105,8 @@ MCP (Model Context Protocol) 是一种协议,允许 AI 工具访问外部数 | Gemini | 同步到 Gemini CLI | `~/.gemini/settings.json` 的 `mcpServers` | | OpenCode | 同步到 OpenCode | `~/.opencode/config.json` 的 `mcpServers` | +> ⚠️ **注意**:OpenClaw 暂不支持 MCP 服务器管理。MCP 功能目前仅支持 Claude、Codex、Gemini 和 OpenCode 四个应用。 + ### 开关实现机制 当开启某个应用的开关时,CC Switch 会: diff --git a/docs/user-manual/3-extensions/3.2-prompts.md b/docs/user-manual/3-extensions/3.2-prompts.md index 641822906..c5a0ce15d 100644 --- a/docs/user-manual/3-extensions/3.2-prompts.md +++ b/docs/user-manual/3-extensions/3.2-prompts.md @@ -82,6 +82,7 @@ Prompts 功能用于管理系统提示词预设。系统提示词会影响 AI | Codex | `~/.codex/AGENTS.md` | | Gemini | `~/.gemini/GEMINI.md` | | OpenCode | `~/.opencode/AGENTS.md` | +| OpenClaw | `~/.openclaw/AGENTS.md` | ## 编辑预设 @@ -140,6 +141,7 @@ Prompts 是按应用分开管理的: - 切换到 Codex 时,显示 Codex 的预设 - 切换到 Gemini 时,显示 Gemini 的预设 - 切换到 OpenCode 时,显示 OpenCode 的预设 +- 切换到 OpenClaw 时,显示 OpenClaw 的预设 如需在多个应用使用相同的提示词,需要分别创建。 diff --git a/docs/user-manual/3-extensions/3.3-skills.md b/docs/user-manual/3-extensions/3.3-skills.md index 4223bc87b..30d8e5124 100644 --- a/docs/user-manual/3-extensions/3.3-skills.md +++ b/docs/user-manual/3-extensions/3.3-skills.md @@ -5,6 +5,7 @@ Skills 是可复用的能力扩展,让 AI 工具获得特定领域的专业能力。 技能以文件夹形式存在,包含: + - 提示词模板 - 工具定义 - 示例代码 @@ -12,6 +13,7 @@ Skills 是可复用的能力扩展,让 AI 工具获得特定领域的专业能 ## 支持的应用 Skills 功能支持所有四种应用: + - **Claude Code** - **Codex** - **Gemini CLI** @@ -33,11 +35,11 @@ Skills 功能支持所有四种应用: CC Switch 预配置了以下 GitHub 仓库: -| 仓库 | 说明 | -|------|------| +| 仓库 | 说明 | +| -------------- | ------------------------ | | Anthropic 官方 | Anthropic 提供的官方技能 | -| ComposioHQ | 社区维护的技能集合 | -| 社区精选 | 精选的高质量技能 | +| ComposioHQ | 社区维护的技能集合 | +| 社区精选 | 精选的高质量技能 | ![image-20260108010308060](../assets/image-20260108010308060.png) @@ -56,9 +58,9 @@ CC Switch 提供强大的搜索和过滤功能: 使用下拉菜单按安装状态过滤: -| 选项 | 说明 | -|------|------| -| 全部 | 显示所有技能 | +| 选项 | 说明 | +| ------ | ------------------ | +| 全部 | 显示所有技能 | | 已安装 | 仅显示已安装的技能 | | 未安装 | 仅显示未安装的技能 | @@ -67,6 +69,7 @@ CC Switch 提供强大的搜索和过滤功能: #### 组合使用 搜索和过滤可以组合使用: + - 先选择「已安装」过滤 - 再输入关键词搜索 - 结果显示匹配数量 @@ -85,11 +88,11 @@ CC Switch 提供强大的搜索和过滤功能: ### 安装位置 -| 应用 | 安装目录 | -|------|----------| -| Claude | `~/.claude/skills/` | -| Codex | `~/.codex/skills/` | -| Gemini | `~/.gemini/skills/` | +| 应用 | 安装目录 | +| -------- | --------------------- | +| Claude | `~/.claude/skills/` | +| Codex | `~/.codex/skills/` | +| Gemini | `~/.gemini/skills/` | | OpenCode | `~/.opencode/skills/` | ### 安装内容 @@ -141,6 +144,7 @@ https://github.com/{owner}/{name}/tree/{branch}/{subdirectory} ``` 示例: + ``` Owner: anthropics Name: claude-skills @@ -160,11 +164,11 @@ Subdirectory: skills 每个技能卡片显示: -| 信息 | 说明 | -|------|------| -| 名称 | 技能名称 | -| 描述 | 功能说明 | -| 来源 | 所属仓库 | +| 信息 | 说明 | +| ---- | --------------- | +| 名称 | 技能名称 | +| 描述 | 功能说明 | +| 来源 | 所属仓库 | | 状态 | 已安装 / 未安装 | ## 技能更新 @@ -178,10 +182,12 @@ Subdirectory: skills ### 技能列表为空 可能原因: + - 网络问题,无法访问 GitHub - 仓库配置错误 解决方法: + - 检查网络连接 - 点击「刷新」重试 - 检查仓库配置 @@ -189,11 +195,13 @@ Subdirectory: skills ### 安装失败 可能原因: + - 网络问题 - 磁盘空间不足 - 权限问题 解决方法: + - 检查网络连接 - 检查磁盘空间 - 检查目录权限 diff --git a/docs/user-manual/4-proxy/4.1-service.md b/docs/user-manual/4-proxy/4.1-service.md index 49666f21d..4f0fc3b39 100644 --- a/docs/user-manual/4-proxy/4.1-service.md +++ b/docs/user-manual/4-proxy/4.1-service.md @@ -181,7 +181,7 @@ GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721 | 字段 | 说明 | |------|------| | 时间 | 请求时间 | -| 应用 | Claude/Codex/Gemini/OpenCode | +| 应用 | Claude / Codex / Gemini | | 供应商 | 使用的供应商 | | 模型 | 请求的模型 | | Token | 输入/输出 token 数 | diff --git a/docs/user-manual/4-proxy/4.2-takeover.md b/docs/user-manual/4-proxy/4.2-takeover.md index 67e8afd89..9054dcd44 100644 --- a/docs/user-manual/4-proxy/4.2-takeover.md +++ b/docs/user-manual/4-proxy/4.2-takeover.md @@ -32,7 +32,6 @@ | Claude 接管 | 接管 Claude Code 的请求 | | Codex 接管 | 接管 Codex 的请求 | | Gemini 接管 | 接管 Gemini CLI 的请求 | -| OpenCode 接管 | 接管 OpenCode 的请求 | 可以同时开启多个应用的接管。 @@ -84,7 +83,7 @@ GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721 代理收到请求后: -1. 识别请求来源(Claude/Codex/Gemini/OpenCode) +1. 识别请求来源(Claude/Codex/Gemini) 2. 查找该应用当前启用的供应商 3. 将请求转发到供应商的实际端点 4. 记录请求日志 diff --git a/docs/user-manual/4-proxy/4.3-failover.md b/docs/user-manual/4-proxy/4.3-failover.md index 484dc24e0..d8195a92a 100644 --- a/docs/user-manual/4-proxy/4.3-failover.md +++ b/docs/user-manual/4-proxy/4.3-failover.md @@ -26,11 +26,10 @@ ### 选择应用 -页面顶部有四个 Tab: +页面顶部有三个 Tab: - Claude - Codex - Gemini -- OpenCode 选择要配置的应用。 diff --git a/docs/user-manual/4-proxy/4.4-usage.md b/docs/user-manual/4-proxy/4.4-usage.md index acf8c095a..7fea1c9fe 100644 --- a/docs/user-manual/4-proxy/4.4-usage.md +++ b/docs/user-manual/4-proxy/4.4-usage.md @@ -123,7 +123,7 @@ | 筛选项 | 选项 | |--------|------| -| 应用类型 | 全部 / Claude / Codex / Gemini / OpenCode | +| 应用类型 | 全部 / Claude / Codex / Gemini | | 状态码 | 全部 / 200 / 400 / 401 / 429 / 500 | | 供应商 | 文本搜索 | | 模型 | 文本搜索 | diff --git a/docs/user-manual/5-faq/5.1-config-files.md b/docs/user-manual/5-faq/5.1-config-files.md index 4049a67a6..83ec2e743 100644 --- a/docs/user-manual/5-faq/5.1-config-files.md +++ b/docs/user-manual/5-faq/5.1-config-files.md @@ -51,7 +51,8 @@ "claudeConfigDir": null, "codexConfigDir": null, "geminiConfigDir": null, - "opencodeConfigDir": null + "opencodeConfigDir": null, + "openclawConfigDir": null } ``` @@ -208,6 +209,67 @@ GEMINI_MODEL=gemini-pro └── ... ``` +## OpenClaw 配置 + +### 配置目录 + +默认:`~/.openclaw/` + +### 主要文件 + +``` +~/.openclaw/ +├── openclaw.json # 主配置文件(JSON5 格式) +├── AGENTS.md # 系统提示词 +└── skills/ # 技能目录 + └── ... +``` + +### openclaw.json + +OpenClaw 使用 JSON5 格式配置文件,主要包含以下部分: + +```json5 +{ + // 模型供应商配置 + models: { + mode: "merge", + providers: { + "custom-provider": { + baseUrl: "https://api.example.com/v1", + apiKey: "your-api-key", + api: "openai-completions", + models: [{ id: "model-id", name: "Model Name" }] + } + } + }, + // 环境变量 + env: { + ANTHROPIC_API_KEY: "sk-..." + }, + // Agent 默认模型配置 + agents: { + defaults: { + model: { + primary: "provider/model" + } + } + }, + // 工具配置 + tools: {}, + // 工作区文件配置 + workspace: {} +} +``` + +| 字段 | 说明 | +|------|------| +| `models.providers` | 供应商配置(映射为 CC Switch 的"供应商") | +| `env` | 环境变量配置 | +| `agents.defaults` | Agent 默认模型设置 | +| `tools` | 工具配置 | +| `workspace` | 工作区文件管理 | + ## 配置优先级 CC Switch 修改配置时的优先级: diff --git a/docs/user-manual/5-faq/5.3-deeplink.md b/docs/user-manual/5-faq/5.3-deeplink.md index 32057e17a..82ddebb6c 100644 --- a/docs/user-manual/5-faq/5.3-deeplink.md +++ b/docs/user-manual/5-faq/5.3-deeplink.md @@ -39,7 +39,7 @@ ccswitch://v1/import?resource={type}&app={app}&name={name}&... | 参数 | 必填 | 说明 | |------|------|------| | `resource` | 是 | 资源类型:`provider` / `mcp` / `prompt` / `skill` | -| `app` | 是 | 应用类型:`claude` / `codex` / `gemini` / `opencode` | +| `app` | 是 | 应用类型:`claude` / `codex` / `gemini` / `opencode` / `openclaw` | | `name` | 是 | 名称 | **供应商参数**(resource=provider): @@ -79,7 +79,7 @@ ccswitch://v1/import?resource={type}&app={app}&name={name}&... | 参数 | 必填 | 说明 | |------|------|------| -| `apps` | 是 | 应用列表(逗号分隔,如 `claude,codex,gemini`) | +| `apps` | 是 | 应用列表(逗号分隔,如 `claude,codex,gemini,opencode`) | | `config` | 是 | MCP 服务器配置(JSON 格式) | | `enabled` | 否 | 是否启用(布尔值) | diff --git a/docs/user-manual/README.md b/docs/user-manual/README.md index 041d5d76b..5a3380635 100644 --- a/docs/user-manual/README.md +++ b/docs/user-manual/README.md @@ -1,6 +1,6 @@ # CC Switch 用户手册 -> Claude Code / Codex / Gemini CLI / OpenCode 全方位辅助工具 +> Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw 全方位辅助工具 ## 目录结构 diff --git a/src-tauri/src/deeplink/parser.rs b/src-tauri/src/deeplink/parser.rs index 58553d442..290e30318 100644 --- a/src-tauri/src/deeplink/parser.rs +++ b/src-tauri/src/deeplink/parser.rs @@ -79,9 +79,12 @@ fn parse_provider_deeplink( .clone(); // Validate app type - if app != "claude" && app != "codex" && app != "gemini" { + if !matches!( + app.as_str(), + "claude" | "codex" | "gemini" | "opencode" | "openclaw" + ) { return Err(AppError::InvalidInput(format!( - "Invalid app type: must be 'claude', 'codex', or 'gemini', got '{app}'" + "Invalid app type: must be 'claude', 'codex', 'gemini', 'opencode', or 'openclaw', got '{app}'" ))); } @@ -185,9 +188,12 @@ fn parse_prompt_deeplink( .clone(); // Validate app type - if app != "claude" && app != "codex" && app != "gemini" { + if !matches!( + app.as_str(), + "claude" | "codex" | "gemini" | "opencode" | "openclaw" + ) { return Err(AppError::InvalidInput(format!( - "Invalid app type: must be 'claude', 'codex', or 'gemini', got '{app}'" + "Invalid app type: must be 'claude', 'codex', 'gemini', 'opencode', or 'openclaw', got '{app}'" ))); } @@ -254,9 +260,12 @@ fn parse_mcp_deeplink( // Validate apps format for app in apps.split(',') { let trimmed = app.trim(); - if trimmed != "claude" && trimmed != "codex" && trimmed != "gemini" { + if !matches!( + trimmed, + "claude" | "codex" | "gemini" | "opencode" | "openclaw" + ) { return Err(AppError::InvalidInput(format!( - "Invalid app in 'apps': must be 'claude', 'codex', or 'gemini', got '{trimmed}'" + "Invalid app in 'apps': must be 'claude', 'codex', 'gemini', 'opencode', or 'openclaw', got '{trimmed}'" ))); } } From bbed2a1fe16937a6f54daadbf7a2e5c0ddeca0db Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 08:40:52 +0800 Subject: [PATCH 12/34] docs: restructure user manual for i18n and add EN/JA translations Reorganize docs/user-manual/ from flat structure to language subdirectories (zh/, en/, ja/) with shared assets/. Move existing Chinese docs into zh/, fix image paths, add multilingual navigation README, and translate all 23 markdown files (~4500 lines each) to English and Japanese. --- docs/user-manual/README.md | 115 +----- .../en/1-getting-started/1.1-introduction.md | 65 ++++ .../en/1-getting-started/1.2-installation.md | 229 +++++++++++ .../en/1-getting-started/1.3-interface.md | 170 +++++++++ .../en/1-getting-started/1.4-quickstart.md | 92 +++++ .../en/1-getting-started/1.5-settings.md | 255 +++++++++++++ docs/user-manual/en/2-providers/2.1-add.md | 354 ++++++++++++++++++ docs/user-manual/en/2-providers/2.2-switch.md | 111 ++++++ docs/user-manual/en/2-providers/2.3-edit.md | 145 +++++++ .../en/2-providers/2.4-sort-duplicate.md | 76 ++++ .../en/2-providers/2.5-usage-query.md | 181 +++++++++ docs/user-manual/en/3-extensions/3.1-mcp.md | 209 +++++++++++ .../en/3-extensions/3.2-prompts.md | 160 ++++++++ .../user-manual/en/3-extensions/3.3-skills.md | 207 ++++++++++ docs/user-manual/en/4-proxy/4.1-service.md | 222 +++++++++++ docs/user-manual/en/4-proxy/4.2-takeover.md | 195 ++++++++++ docs/user-manual/en/4-proxy/4.3-failover.md | 232 ++++++++++++ docs/user-manual/en/4-proxy/4.4-usage.md | 291 ++++++++++++++ docs/user-manual/en/4-proxy/4.5-model-test.md | 156 ++++++++ docs/user-manual/en/5-faq/5.1-config-files.md | 340 +++++++++++++++++ docs/user-manual/en/5-faq/5.2-questions.md | 220 +++++++++++ docs/user-manual/en/5-faq/5.3-deeplink.md | 256 +++++++++++++ docs/user-manual/en/5-faq/5.4-env-conflict.md | 108 ++++++ docs/user-manual/en/README.md | 111 ++++++ .../ja/1-getting-started/1.1-introduction.md | 65 ++++ .../ja/1-getting-started/1.2-installation.md | 229 +++++++++++ .../ja/1-getting-started/1.3-interface.md | 170 +++++++++ .../ja/1-getting-started/1.4-quickstart.md | 92 +++++ .../ja/1-getting-started/1.5-settings.md | 255 +++++++++++++ docs/user-manual/ja/2-providers/2.1-add.md | 354 ++++++++++++++++++ docs/user-manual/ja/2-providers/2.2-switch.md | 111 ++++++ docs/user-manual/ja/2-providers/2.3-edit.md | 145 +++++++ .../ja/2-providers/2.4-sort-duplicate.md | 76 ++++ .../ja/2-providers/2.5-usage-query.md | 181 +++++++++ docs/user-manual/ja/3-extensions/3.1-mcp.md | 209 +++++++++++ .../ja/3-extensions/3.2-prompts.md | 160 ++++++++ .../user-manual/ja/3-extensions/3.3-skills.md | 207 ++++++++++ docs/user-manual/ja/4-proxy/4.1-service.md | 222 +++++++++++ docs/user-manual/ja/4-proxy/4.2-takeover.md | 195 ++++++++++ docs/user-manual/ja/4-proxy/4.3-failover.md | 232 ++++++++++++ docs/user-manual/ja/4-proxy/4.4-usage.md | 291 ++++++++++++++ docs/user-manual/ja/4-proxy/4.5-model-test.md | 156 ++++++++ docs/user-manual/ja/5-faq/5.1-config-files.md | 340 +++++++++++++++++ docs/user-manual/ja/5-faq/5.2-questions.md | 220 +++++++++++ docs/user-manual/ja/5-faq/5.3-deeplink.md | 256 +++++++++++++ docs/user-manual/ja/5-faq/5.4-env-conflict.md | 108 ++++++ docs/user-manual/ja/README.md | 111 ++++++ .../1-getting-started/1.1-introduction.md | 0 .../1-getting-started/1.2-installation.md | 0 .../1-getting-started/1.3-interface.md | 4 +- .../1-getting-started/1.4-quickstart.md | 4 +- .../1-getting-started/1.5-settings.md | 0 .../{ => zh}/2-providers/2.1-add.md | 2 +- .../{ => zh}/2-providers/2.2-switch.md | 2 +- .../{ => zh}/2-providers/2.3-edit.md | 2 +- .../2-providers/2.4-sort-duplicate.md | 2 +- .../{ => zh}/2-providers/2.5-usage-query.md | 0 .../{ => zh}/3-extensions/3.1-mcp.md | 4 +- .../{ => zh}/3-extensions/3.2-prompts.md | 2 +- .../{ => zh}/3-extensions/3.3-skills.md | 6 +- .../{ => zh}/4-proxy/4.1-service.md | 4 +- .../{ => zh}/4-proxy/4.2-takeover.md | 0 .../{ => zh}/4-proxy/4.3-failover.md | 0 .../user-manual/{ => zh}/4-proxy/4.4-usage.md | 12 +- .../{ => zh}/4-proxy/4.5-model-test.md | 0 .../{ => zh}/5-faq/5.1-config-files.md | 0 .../{ => zh}/5-faq/5.2-questions.md | 0 .../{ => zh}/5-faq/5.3-deeplink.md | 0 .../{ => zh}/5-faq/5.4-env-conflict.md | 0 docs/user-manual/zh/README.md | 111 ++++++ 70 files changed, 8916 insertions(+), 124 deletions(-) create mode 100644 docs/user-manual/en/1-getting-started/1.1-introduction.md create mode 100644 docs/user-manual/en/1-getting-started/1.2-installation.md create mode 100644 docs/user-manual/en/1-getting-started/1.3-interface.md create mode 100644 docs/user-manual/en/1-getting-started/1.4-quickstart.md create mode 100644 docs/user-manual/en/1-getting-started/1.5-settings.md create mode 100644 docs/user-manual/en/2-providers/2.1-add.md create mode 100644 docs/user-manual/en/2-providers/2.2-switch.md create mode 100644 docs/user-manual/en/2-providers/2.3-edit.md create mode 100644 docs/user-manual/en/2-providers/2.4-sort-duplicate.md create mode 100644 docs/user-manual/en/2-providers/2.5-usage-query.md create mode 100644 docs/user-manual/en/3-extensions/3.1-mcp.md create mode 100644 docs/user-manual/en/3-extensions/3.2-prompts.md create mode 100644 docs/user-manual/en/3-extensions/3.3-skills.md create mode 100644 docs/user-manual/en/4-proxy/4.1-service.md create mode 100644 docs/user-manual/en/4-proxy/4.2-takeover.md create mode 100644 docs/user-manual/en/4-proxy/4.3-failover.md create mode 100644 docs/user-manual/en/4-proxy/4.4-usage.md create mode 100644 docs/user-manual/en/4-proxy/4.5-model-test.md create mode 100644 docs/user-manual/en/5-faq/5.1-config-files.md create mode 100644 docs/user-manual/en/5-faq/5.2-questions.md create mode 100644 docs/user-manual/en/5-faq/5.3-deeplink.md create mode 100644 docs/user-manual/en/5-faq/5.4-env-conflict.md create mode 100644 docs/user-manual/en/README.md create mode 100644 docs/user-manual/ja/1-getting-started/1.1-introduction.md create mode 100644 docs/user-manual/ja/1-getting-started/1.2-installation.md create mode 100644 docs/user-manual/ja/1-getting-started/1.3-interface.md create mode 100644 docs/user-manual/ja/1-getting-started/1.4-quickstart.md create mode 100644 docs/user-manual/ja/1-getting-started/1.5-settings.md create mode 100644 docs/user-manual/ja/2-providers/2.1-add.md create mode 100644 docs/user-manual/ja/2-providers/2.2-switch.md create mode 100644 docs/user-manual/ja/2-providers/2.3-edit.md create mode 100644 docs/user-manual/ja/2-providers/2.4-sort-duplicate.md create mode 100644 docs/user-manual/ja/2-providers/2.5-usage-query.md create mode 100644 docs/user-manual/ja/3-extensions/3.1-mcp.md create mode 100644 docs/user-manual/ja/3-extensions/3.2-prompts.md create mode 100644 docs/user-manual/ja/3-extensions/3.3-skills.md create mode 100644 docs/user-manual/ja/4-proxy/4.1-service.md create mode 100644 docs/user-manual/ja/4-proxy/4.2-takeover.md create mode 100644 docs/user-manual/ja/4-proxy/4.3-failover.md create mode 100644 docs/user-manual/ja/4-proxy/4.4-usage.md create mode 100644 docs/user-manual/ja/4-proxy/4.5-model-test.md create mode 100644 docs/user-manual/ja/5-faq/5.1-config-files.md create mode 100644 docs/user-manual/ja/5-faq/5.2-questions.md create mode 100644 docs/user-manual/ja/5-faq/5.3-deeplink.md create mode 100644 docs/user-manual/ja/5-faq/5.4-env-conflict.md create mode 100644 docs/user-manual/ja/README.md rename docs/user-manual/{ => zh}/1-getting-started/1.1-introduction.md (100%) rename docs/user-manual/{ => zh}/1-getting-started/1.2-installation.md (100%) rename docs/user-manual/{ => zh}/1-getting-started/1.3-interface.md (97%) rename docs/user-manual/{ => zh}/1-getting-started/1.4-quickstart.md (95%) rename docs/user-manual/{ => zh}/1-getting-started/1.5-settings.md (100%) rename docs/user-manual/{ => zh}/2-providers/2.1-add.md (99%) rename docs/user-manual/{ => zh}/2-providers/2.2-switch.md (96%) rename docs/user-manual/{ => zh}/2-providers/2.3-edit.md (97%) rename docs/user-manual/{ => zh}/2-providers/2.4-sort-duplicate.md (96%) rename docs/user-manual/{ => zh}/2-providers/2.5-usage-query.md (100%) rename docs/user-manual/{ => zh}/3-extensions/3.1-mcp.md (97%) rename docs/user-manual/{ => zh}/3-extensions/3.2-prompts.md (98%) rename docs/user-manual/{ => zh}/3-extensions/3.3-skills.md (94%) rename docs/user-manual/{ => zh}/4-proxy/4.1-service.md (96%) rename docs/user-manual/{ => zh}/4-proxy/4.2-takeover.md (100%) rename docs/user-manual/{ => zh}/4-proxy/4.3-failover.md (100%) rename docs/user-manual/{ => zh}/4-proxy/4.4-usage.md (94%) rename docs/user-manual/{ => zh}/4-proxy/4.5-model-test.md (100%) rename docs/user-manual/{ => zh}/5-faq/5.1-config-files.md (100%) rename docs/user-manual/{ => zh}/5-faq/5.2-questions.md (100%) rename docs/user-manual/{ => zh}/5-faq/5.3-deeplink.md (100%) rename docs/user-manual/{ => zh}/5-faq/5.4-env-conflict.md (100%) create mode 100644 docs/user-manual/zh/README.md diff --git a/docs/user-manual/README.md b/docs/user-manual/README.md index 5a3380635..095b799f5 100644 --- a/docs/user-manual/README.md +++ b/docs/user-manual/README.md @@ -1,111 +1,22 @@ -# CC Switch 用户手册 +# CC Switch User Manual / 用户手册 / ユーザーマニュアル -> Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw 全方位辅助工具 +> Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw -## 目录结构 +## Language / 语言 / 言語 -``` -📚 CC Switch 用户手册 -│ -├── 1. 快速入门 -│ ├── 1.1 软件介绍 -│ ├── 1.2 安装指南 -│ ├── 1.3 界面概览 -│ ├── 1.4 快速上手 -│ └── 1.5 个性化配置 -│ -├── 2. 供应商管理 -│ ├── 2.1 添加供应商 -│ ├── 2.2 切换供应商 -│ ├── 2.3 编辑供应商 -│ ├── 2.4 排序与复制 -│ └── 2.5 用量查询 -│ -├── 3. 扩展功能 -│ ├── 3.1 MCP 服务器管理 -│ ├── 3.2 Prompts 提示词管理 -│ └── 3.3 Skills 技能管理 -│ -├── 4. 代理与高可用 -│ ├── 4.1 代理服务 -│ ├── 4.2 应用接管 -│ ├── 4.3 故障转移 -│ ├── 4.4 用量统计 -│ └── 4.5 模型检查 -│ -└── 5. 常见问题 - ├── 5.1 配置文件说明 - ├── 5.2 FAQ - ├── 5.3 深度链接协议 - └── 5.4 环境变量冲突 -``` +| Language | Link | +|----------|------| +| [中文](./zh/README.md) | 简体中文用户手册 | +| [English](./en/README.md) | English User Manual | +| [日本語](./ja/README.md) | 日本語ユーザーマニュアル | -## 文件列表 +## Version / 版本 / バージョン -### 1. 快速入门 +- Documentation version: v3.11.1 +- Last updated: 2026-03-02 +- Compatible with CC Switch v3.11.1+ -| 文件 | 内容 | -|------|------| -| [1.1-introduction.md](./1-getting-started/1.1-introduction.md) | 软件介绍、核心功能、支持平台 | -| [1.2-installation.md](./1-getting-started/1.2-installation.md) | Windows/macOS/Linux 安装指南 | -| [1.3-interface.md](./1-getting-started/1.3-interface.md) | 界面布局、导航栏、供应商卡片说明 | -| [1.4-quickstart.md](./1-getting-started/1.4-quickstart.md) | 5 分钟快速上手教程 | -| [1.5-settings.md](./1-getting-started/1.5-settings.md) | 语言、主题、目录、云同步配置 | - -### 2. 供应商管理 - -| 文件 | 内容 | -|------|------| -| [2.1-add.md](./2-providers/2.1-add.md) | 使用预设、自定义配置、统一供应商 | -| [2.2-switch.md](./2-providers/2.2-switch.md) | 主界面切换、托盘切换、生效方式 | -| [2.3-edit.md](./2-providers/2.3-edit.md) | 编辑配置、修改 API Key、回填机制 | -| [2.4-sort-duplicate.md](./2-providers/2.4-sort-duplicate.md) | 拖拽排序、复制供应商、删除 | -| [2.5-usage-query.md](./2-providers/2.5-usage-query.md) | 用量查询、剩余额度、多套餐显示 | - -### 3. 扩展功能 - -| 文件 | 内容 | -|------|------| -| [3.1-mcp.md](./3-extensions/3.1-mcp.md) | MCP 协议、添加服务器、应用绑定 | -| [3.2-prompts.md](./3-extensions/3.2-prompts.md) | 创建预设、激活切换、智能回填 | -| [3.3-skills.md](./3-extensions/3.3-skills.md) | 发现技能、安装卸载、仓库管理 | - -### 4. 代理与高可用 - -| 文件 | 内容 | -|------|------| -| [4.1-service.md](./4-proxy/4.1-service.md) | 启动代理、配置项、运行状态 | -| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | 应用接管、配置修改、状态指示 | -| [4.3-failover.md](./4-proxy/4.3-failover.md) | 故障转移队列、熔断器、健康状态 | -| [4.4-usage.md](./4-proxy/4.4-usage.md) | 用量统计、趋势图表、定价配置 | -| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | 模型检查、健康检测、延迟测试 | - -### 5. 常见问题 - -| 文件 | 内容 | -|------|------| -| [5.1-config-files.md](./5-faq/5.1-config-files.md) | CC Switch 存储、CLI 配置文件格式 | -| [5.2-questions.md](./5-faq/5.2-questions.md) | 常见问题解答 | -| [5.3-deeplink.md](./5-faq/5.3-deeplink.md) | 深度链接协议、生成和使用方法 | -| [5.4-env-conflict.md](./5-faq/5.4-env-conflict.md) | 环境变量冲突检测与处理 | - -## 快速链接 - -- **新用户**:从 [1.1 软件介绍](./1-getting-started/1.1-introduction.md) 开始 -- **安装问题**:查看 [1.2 安装指南](./1-getting-started/1.2-installation.md) -- **配置供应商**:查看 [2.1 添加供应商](./2-providers/2.1-add.md) -- **使用代理**:查看 [4.1 代理服务](./4-proxy/4.1-service.md) -- **遇到问题**:查看 [5.2 FAQ](./5-faq/5.2-questions.md) - -## 版本信息 - -- 文档版本:v3.11.1 -- 最后更新:2026-02-28 -- 适用于 CC Switch v3.11.1+ - -## 贡献 - -欢迎提交 Issue 或 PR 改进文档: +## Links - [GitHub Issues](https://github.com/farion1231/cc-switch/issues) - [GitHub Repository](https://github.com/farion1231/cc-switch) diff --git a/docs/user-manual/en/1-getting-started/1.1-introduction.md b/docs/user-manual/en/1-getting-started/1.1-introduction.md new file mode 100644 index 000000000..ba96e598a --- /dev/null +++ b/docs/user-manual/en/1-getting-started/1.1-introduction.md @@ -0,0 +1,65 @@ +# 1.1 Introduction + +## What is CC Switch + +CC Switch is a cross-platform desktop application designed for developers who use AI coding tools. It helps you centrally manage configurations for five major AI coding tools: **Claude Code**, **Codex**, **Gemini CLI**, **OpenCode**, and **OpenClaw**. + +## What Problems Does It Solve + +In your daily development workflow, you may encounter these pain points: + +- **Tedious multi-provider switching**: Using different API providers (official, proxy services) requires manually editing configuration files +- **Scattered configurations**: Claude, Codex, Gemini, OpenCode, and OpenClaw each have independent configuration files in different formats +- **No usage monitoring**: No visibility into how many API calls were made or how much they cost +- **Service instability**: When a single provider goes down, your entire workflow is interrupted + +CC Switch solves these problems through a unified interface. + +## Core Features + +### Provider Management +- One-click switching between multiple API provider configurations +- Preset templates for quickly adding common providers +- Universal provider feature for sharing configurations across apps +- Usage query and balance display +- Endpoint speed testing + +### Extensions +- **MCP Servers**: Manage Model Context Protocol servers to extend AI capabilities +- **Prompts**: Manage system prompt presets for quick scenario switching +- **Skills**: Install and manage skill extensions + +### Proxy & High Availability +- Local proxy service for request logging and usage statistics +- Automatic failover that switches to a backup provider when the primary one fails +- Circuit breaker mechanism to prevent repeated retries against failing providers +- Detailed token usage tracking and cost estimation + +## Supported Applications + +| Application | Description | +|-------------|-------------| +| **Claude Code** | Anthropic's official AI coding assistant | +| **Codex** | OpenAI's code generation tool | +| **Gemini CLI** | Google's AI command-line tool | +| **OpenCode** | Open-source AI coding terminal tool | +| **OpenClaw** | Open-source AI coding assistant (multi-provider gateway) | + +## Supported Platforms + +- **Windows** 10 and above +- **macOS** 10.15 (Catalina) and above +- **Linux** Ubuntu 22.04+ / Debian 11+ / Fedora 34+ + +## Technical Architecture + +CC Switch is built with a modern technology stack: + +- **Frontend**: React 18 + TypeScript + Tailwind CSS +- **Backend**: Tauri 2 + Rust +- **Data Storage**: SQLite (providers, MCP, Prompts) + JSON (device settings) + +This architecture ensures: +- Consistent cross-platform experience +- Native-level performance +- Secure local data storage diff --git a/docs/user-manual/en/1-getting-started/1.2-installation.md b/docs/user-manual/en/1-getting-started/1.2-installation.md new file mode 100644 index 000000000..65b948617 --- /dev/null +++ b/docs/user-manual/en/1-getting-started/1.2-installation.md @@ -0,0 +1,229 @@ +# 1.2 Installation Guide + +## Prerequisites + +### Install Node.js + +The CLI tools managed by CC Switch (Claude Code, Codex, Gemini CLI) require a Node.js environment. + +**Recommended version**: Node.js 18 LTS or higher + +#### Windows + +1. Visit the [Node.js official website](https://nodejs.org/) + +2. Download the LTS version installer + +3. Run the installer and follow the prompts + +4. Verify installation: + + ```bash + node --version + npm --version + ``` + +#### macOS + +```bash +# Install with Homebrew +brew install node + +# Or use nvm (recommended) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +nvm install --lts +``` + +#### Linux + +```bash +# Ubuntu/Debian +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Or use nvm +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +nvm install --lts +``` + +### Install CLI Tools + +#### Claude Code + +**Option 1: Homebrew (recommended for macOS)** + +```bash +brew install claude-code +``` + +**Option 2: npm** + +```bash +npm install -g @anthropic-ai/claude-code +``` + +#### Codex + +**Option 1: Homebrew (recommended for macOS)** + +```bash +brew install codex +``` + +**Option 2: npm** + +```bash +npm install -g @openai/codex +``` + +#### Gemini CLI + +**Option 1: Homebrew (recommended for macOS)** + +```bash +brew install gemini-cli +``` + +**Option 2: npm** + +```bash +npm install -g @google/gemini-cli +``` + +--- + +## Windows + +### Installer + +1. Visit the [Releases page](https://github.com/farion1231/cc-switch/releases) +2. Download `CC-Switch-v{version}-Windows.msi` +3. Double-click to run the installer +4. Follow the prompts to complete installation + +### Portable Version (No Installation Required) + +1. Download `CC-Switch-v{version}-Windows-Portable.zip` +2. Extract to any directory +3. Run `CC-Switch.exe` + +## macOS + +### Option 1: Homebrew (Recommended) + +```bash +# Add tap +brew tap farion1231/ccswitch + +# Install +brew install --cask cc-switch +``` + +Update to the latest version: + +```bash +brew upgrade --cask cc-switch +``` + +### Option 2: Manual Download + +1. Download `CC-Switch-v{version}-macOS.zip` +2. Extract to get `CC Switch.app` +3. Drag it to the Applications folder + +### First Launch Warning + +Since the developer does not have an Apple Developer account, a "developer cannot be verified" warning may appear on first launch: + +**Recommended solution**: +Open Terminal and run the following command: +```bash +sudo xattr -dr com.apple.quarantine /Applications/CC\ Switch.app/ +``` + +**Alternative solution (via System Settings)**: +1. Close the warning dialog +2. Open "System Settings" > "Privacy & Security" +3. Find the CC Switch prompt and click "Open Anyway" +4. Reopen the app to use it normally + +## Linux + +### ArchLinux + +Install using an AUR helper: + +```bash +# Using paru +paru -S cc-switch-bin + +# Or using yay +yay -S cc-switch-bin +``` + +### Debian / Ubuntu + +1. Download `CC-Switch-v{version}-Linux.deb` +2. Install: + +```bash +sudo dpkg -i CC-Switch-v{version}-Linux.deb + +# If there are dependency issues +sudo apt-get install -f +``` + +### AppImage (Universal) + +1. Download `CC-Switch-v{version}-Linux.AppImage` +2. Add execute permission: + +```bash +chmod +x CC-Switch-v{version}-Linux.AppImage +``` + +3. Run: + +```bash +./CC-Switch-v{version}-Linux.AppImage +``` + +## Verify Installation + +After installation, launch CC Switch: + +1. The app window displays correctly +2. A CC Switch icon appears in the system tray +3. You can switch between Claude / Codex / Gemini apps + +## Auto Update + +CC Switch includes built-in auto-update functionality: + +- Automatically checks for updates on startup +- Displays an update prompt in the UI when a new version is available +- Click to download and install + +You can also manually check for updates in "Settings > About". + +## Uninstall + +### Windows + +- Uninstall via "Settings > Apps" +- Or run the uninstaller in the installation directory + +### macOS + +- Move `CC Switch.app` to Trash +- Optional: Delete the configuration directory `~/.cc-switch/` + +### Linux + +```bash +# Debian/Ubuntu +sudo apt remove cc-switch + +# ArchLinux +paru -R cc-switch-bin +``` diff --git a/docs/user-manual/en/1-getting-started/1.3-interface.md b/docs/user-manual/en/1-getting-started/1.3-interface.md new file mode 100644 index 000000000..df78b84be --- /dev/null +++ b/docs/user-manual/en/1-getting-started/1.3-interface.md @@ -0,0 +1,170 @@ +# 1.3 Interface Overview + +## Main Interface Layout + +![image-20260108001629138](../../assets/image-20260108001629138.png) + +## Top Navigation Bar + +| # | Element | Description | +|---|---------|-------------| +| 1 | Logo | Click to visit the GitHub project page | +| 2 | Settings Button | Open the settings page (shortcut `Cmd/Ctrl + ,`) | +| 3 | Proxy Toggle | Start/stop the local proxy service | +| 4 | App Switcher | Switch between Claude / Codex / Gemini / OpenCode / OpenClaw | +| 5 | Feature Area | Skills / Prompts / MCP entry points | +| 6 | Add Button | Add a new provider | + +### App Switcher + +Click the dropdown menu to switch the currently managed application: + +- **Claude** - Manage Claude Code configuration +- **Codex** - Manage Codex configuration +- **Gemini** - Manage Gemini CLI configuration +- **OpenCode** - Manage OpenCode configuration +- **OpenClaw** - Manage OpenClaw configuration + +After switching, the provider list displays the configurations for the selected application. + +### Feature Area Buttons + +| Button | Function | Visibility | +|--------|----------|------------| +| Skills | Skill extension management | Always visible | +| Prompts | System prompt management | Always visible | +| MCP | MCP server management | Always visible | + +## Provider Cards + +Each provider is displayed as a card, containing the following elements from left to right: + +### Card Elements (Left to Right) + +| # | Element | Icon | Description | +|---|---------|------|-------------| +| 1 | Drag Handle | ≡ | Hold and drag up/down to reorder providers | +| 2 | Provider Icon | - | Displays provider brand icon with customizable color | +| 3 | Provider Info | - | Name, notes/endpoint URL (clickable to open website) | +| 4 | Usage Info | - | Shows remaining balance; displays plan count for multi-plan | +| 5 | Enable Button | - | Switch to this provider | +| 6 | Edit Button | - | Edit provider configuration | +| 7 | Duplicate Button | - | Duplicate provider (create a copy) | +| 8 | Speed Test Button | - | Test model availability and response speed | +| 9 | Usage Query | - | Configure usage query script | +| 10 | Delete Button | - | Delete provider (disabled when currently active) | + +> **Tip**: The action buttons area (5-10) appears on hover and is hidden by default to keep the interface clean. + +### Button Details + +| Button | State Changes | Notes | +|--------|---------------|-------| +| **Enable** | Shows checkmark and disables when active | Changes to "Join/Joined" in failover mode | +| **Edit** | Always available | Opens edit panel to modify configuration | +| **Duplicate** | Always available | Creates a copy with `copy` suffix | +| **Speed Test** | Shows loading animation during test | Only available when proxy service is running | +| **Usage Query** | Always available | Configure custom usage query script | +| **Delete** | Semi-transparent/disabled when active | Must switch to another provider first | + +### Card States + +| State | Border Color | Description | +|-------|--------------|-------------| +| **Currently Active** | Blue border | Current provider in normal mode | +| **Proxy Active** | Green border | Provider actually in use during proxy takeover mode | +| **Normal** | Default border | Inactive provider | +| **In Failover** | Shows priority badge | e.g., P1, P2 indicates failover priority | + +### Health Status Badges + +In proxy mode, providers in the failover queue display health status: + +| Badge | Color | Description | +|-------|-------|-------------| +| Healthy | Green | 0 consecutive failures | +| Warning | Yellow | 1-2 consecutive failures | +| Unhealthy | Red | 3+ consecutive failures, may trigger circuit breaker | + + +## System Tray + +CC Switch displays an icon in the system tray, providing quick access to operations. + +### Tray Menu Structure + +![image-20260108002153668](../../assets/image-20260108002153668.png) + +### Menu Functions + +| Menu Item | Function | +|-----------|----------| +| Open Main Window | Show and focus the main window | +| App Groups | Providers grouped by Claude/Codex/Gemini/OpenCode/OpenClaw | +| Provider List | Click to switch; currently active one shows a checkmark | +| Quit | Fully exit the application | + +### Multi-language Support + +The tray menu supports three languages, automatically switching based on settings: + +| Language | Open Main Window | Quit | +|----------|-----------------|------| +| Chinese | Open Main Window | Quit | +| English | Open main window | Quit | +| Japanese | Open main window | Quit | + +### Use Cases + +Switching providers via the tray menu doesn't require opening the main window, suitable for: + +- Frequently switching providers +- Quick operations when the main window is minimized +- Managing configurations while running in the background + +## Settings Page + +The settings page is divided into multiple tabs: + +### General Tab + +- Language settings (Chinese/English/Japanese) +- Theme settings (System/Light/Dark) +- Window behavior (launch on startup, close behavior) + +### Advanced Tab + +- Configuration directory settings +- Proxy service configuration +- Failover settings +- Pricing configuration +- Data import/export + +### Usage Tab + +- Request statistics overview +- Trend charts +- Request logs +- Provider/model statistics + +### About Tab + +- Version information +- Update check +- Open source license + +## Keyboard Shortcuts + +| Shortcut | Function | +|----------|----------| +| `Cmd/Ctrl + ,` | Open Settings | +| `Cmd/Ctrl + F` | Search providers | +| `Esc` | Close dialog/search | + +## Search + +Press `Cmd/Ctrl + F` to open the search bar: + +- Search by name, notes, or URL +- Real-time provider list filtering +- Press `Esc` to close search diff --git a/docs/user-manual/en/1-getting-started/1.4-quickstart.md b/docs/user-manual/en/1-getting-started/1.4-quickstart.md new file mode 100644 index 000000000..37ab305f4 --- /dev/null +++ b/docs/user-manual/en/1-getting-started/1.4-quickstart.md @@ -0,0 +1,92 @@ +# 1.4 Quick Start + +This section helps you complete the initial setup in 5 minutes. + +## Step 1: Add a Provider + +1. Click the **+** button in the top-right corner of the main interface +2. Select your provider from the "Preset" dropdown + - Common presets: Zhipu GLM, MiniMax, DeepSeek, Kimi, PackyCode + - Or select "Custom" for manual configuration +3. Enter your **API Key** +4. Click "Add" + +![image-20260108002807657](../../assets/image-20260108002807657.png) + +> **Tip**: Presets auto-fill the endpoint URL, so you only need to enter your API Key. + +## Step 2: Switch Provider + +After adding, the provider appears in the list. + +**Option 1: Switch from the main interface** +- Click the "Enable" button on the provider card + +**Option 2: Quick switch via system tray** +- Right-click the CC Switch icon in the system tray +- Click the provider name directly + +## Step 3: Activation + +After switching providers, each CLI tool activates differently: + +| Application | Activation Method | +|-------------|-------------------| +| Claude Code | Instant effect (supports hot reload) | +| Codex | Requires closing and reopening the terminal | +| Gemini | Instant effect (re-reads config on each request) | + +### Claude Code First Launch Prompt + +If Claude Code prompts you to **log in** or shows an onboarding wizard on first launch, enable the "Skip Claude Code first-run confirmation" option in CC Switch: + +1. Open CC Switch "Settings > General" +2. Enable the "Skip Claude Code first-run confirmation" toggle +3. Restart Claude Code + +![image-20260108002626389](../../assets/image-20260108002626389.png) + +> **Note**: This option writes the `skipIntroduction` field to `~/.claude/settings.json`, skipping the official onboarding flow. + +## Verify Configuration + +After restarting, launch the corresponding CLI tool and enter a simple question to test: + +```bash +# Claude Code - enter a test question after launching +claude +> Hello, please briefly introduce yourself + +# Codex - enter a test question after launching +codex +> Hello, please briefly introduce yourself + +# Gemini - enter a test question after launching +gemini +> Hello, please briefly introduce yourself +``` + +If the AI responds normally, the configuration is successful. + +## Next Steps + +Congratulations! You have completed the basic configuration. Next, you can: + +- [Add more providers](../2-providers/2.1-add.md) - Configure multiple providers for easy switching +- [Configure MCP servers](../3-extensions/3.1-mcp.md) - Extend AI tool capabilities +- [Set up system prompts](../3-extensions/3.2-prompts.md) - Customize AI behavior +- [Enable proxy service](../4-proxy/4.1-service.md) - Monitor usage and enable automatic failover + +## Common Issues + +### Not taking effect after switching? + +Make sure you restarted the terminal or CLI tool. The configuration file is updated at switch time, but running programs do not automatically reload it. + +### Can't find a preset? + +If your provider is not in the preset list, select "Custom" for manual configuration. See [Add Provider](../2-providers/2.1-add.md) for configuration format details. + +### How to restore official login? + +Select the "Official Login" preset (Claude/Codex) or "Google Official" preset (Gemini), restart the client, and follow the login flow. diff --git a/docs/user-manual/en/1-getting-started/1.5-settings.md b/docs/user-manual/en/1-getting-started/1.5-settings.md new file mode 100644 index 000000000..3d29bf3c7 --- /dev/null +++ b/docs/user-manual/en/1-getting-started/1.5-settings.md @@ -0,0 +1,255 @@ +# 1.5 Personalization + +This section describes how to configure CC Switch according to your preferences. + +## Open Settings + +- Click the **gear** button in the top-left corner +- Or use the shortcut `Cmd/Ctrl + ,` + +## Language Settings + +CC Switch supports three languages: + +| Language | Description | +|----------|-------------| +| Simplified Chinese | Default language | +| English | English interface | +| Japanese | Japanese interface | + +Language changes take effect immediately without restarting. + +## Theme Settings + +| Option | Description | +|--------|-------------| +| System | Automatically matches the system's dark/light mode | +| Light | Always use the light theme | +| Dark | Always use the dark theme | + +## Window Behavior + +### Launch on Startup + +When enabled, CC Switch automatically runs when the system starts. + +- **Windows**: Implemented via the registry +- **macOS**: Implemented via LaunchAgent +- **Linux**: Implemented via XDG autostart + +### Close Behavior + +| Option | Description | +|--------|-------------| +| Minimize to tray | Clicking the close button hides to the system tray | +| Exit directly | Clicking the close button fully exits the app | + +"Minimize to tray" is recommended for convenient provider switching via the tray. + +### Claude Plugin Integration + +When enabled, CC Switch automatically syncs the configuration to the VS Code Claude Code extension (writes `primaryApiKey` to `~/.claude/config.json`) when switching providers. + +> **Use case**: If you use both Claude Code CLI and the VS Code extension, enable this option to keep both configurations in sync. + +### Skip Claude Onboarding + +When enabled, skips the Claude Code onboarding flow, suitable for users already familiar with Claude Code. + +> **Note**: This option writes the `skipIntroduction` field to `~/.claude/settings.json`. + +### App Visibility + +Choose which applications to display in the app switcher. Each app can be toggled independently, but at least one must remain visible. + +Configurable apps: Claude, Codex, Gemini, OpenCode, OpenClaw. + +> **Use case**: If you only use Claude Code and Codex CLI, you can hide the other apps to keep the interface clean. + +### Skill Sync Method + +Set the sync method when installing skills to each app's directory: + +| Method | Description | +|--------|-------------| +| Symlink | Creates symbolic links pointing to skill source files; saves space, syncs in real-time | +| Copy | Copies skill files entirely to the target directory | + +> **Recommended**: Symlink is the default method. Switch to Copy if you encounter permission issues. + +### Terminal Settings + +Choose the terminal application that CC Switch uses when opening a terminal. + +Supported terminals (by platform): + +| Platform | Terminal Options | +|----------|-----------------| +| macOS | Terminal, iTerm2, Alacritty, Kitty, Ghostty, WezTerm | +| Windows | CMD, PowerShell, Windows Terminal | +| Linux | GNOME Terminal, Konsole, Xfce4 Terminal, Alacritty, Kitty, Ghostty | + +## Directory Configuration + +### App Configuration Directory + +The storage location for CC Switch's own data, defaulting to `~/.cc-switch/`. + +### CLI Tool Directories + +You can customize each CLI tool's configuration directory: + +| Setting | Default | Description | +|---------|---------|-------------| +| Claude Directory | `~/.claude/` | Claude Code configuration directory | +| Codex Directory | `~/.codex/` | Codex configuration directory | +| Gemini Directory | `~/.gemini/` | Gemini CLI configuration directory | +| OpenCode Directory | `~/.opencode/` | OpenCode configuration directory | +| OpenClaw Directory | `~/.openclaw/` | OpenClaw configuration directory | + +> **Note**: After changing directories, the app must be restarted, and the corresponding CLI tools must also be configured to use the same directory. + +## Data Management + +### Export Configuration + +Click the "Export" button to save a backup file containing: + +- All provider configurations +- MCP server configurations +- Prompt presets +- App settings + +The backup file is in JSON format and can be viewed with a text editor. + +### Import Configuration + +1. Click "Select File" +2. Select a previously exported backup file +3. Click "Import" +4. Confirm to overwrite existing configuration + +> **Note**: Importing will overwrite existing configuration. It is recommended to export your current configuration as a backup first. + +## Proxy Settings + +Settings > Proxy Tab + +The Proxy tab centralizes all proxy-related features: + +### Local Proxy + +Start/stop the local proxy service, configure the listen address and port. See [4.1 Proxy Service](../4-proxy/4.1-service.md) for details. + +### Failover + +Configure failover queues and automatic switching strategies by app (Claude/Codex/Gemini). See [4.3 Failover](../4-proxy/4.3-failover.md) for details. + +### Pricing Rectifier + +Configure model pricing correction rules for proxy billing statistics calibration. + +### Global Outbound Proxy + +Configure CC Switch's outbound HTTP/HTTPS proxy, applicable for scenarios where external API access requires a proxy. + +## Advanced Settings + +Settings > Advanced Tab + +### Configuration Directories + +Customize configuration file directories for each app. See the "Directory Configuration" section above for details. + +### Data Management + +Import/export configuration backups. See the "Data Management" section above for details. + +### Backup & Restore + +Manage automatic backups: + +| Setting | Description | +|---------|-------------| +| Backup Interval | Time interval for automatic backups (hours) | +| Retention Count | Number of backups to retain | + +Supports viewing the backup list and restoring from backups. + +### Cloud Sync (WebDAV) + +Sync configurations across multiple devices via the WebDAV protocol. + +| Setting | Description | +|---------|-------------| +| Service Preset | Jianguoyun / Nextcloud / Synology / Custom | +| Server URL | WebDAV server URL | +| Username | Login username | +| Password | Login password (app-specific password) | +| Remote Directory | Remote storage path (default: `cc-switch-sync`) | +| Profile Name | Device profile name (default: `default`) | +| Auto Sync | Automatically upload changes when enabled | + +Operations: + +- **Test Connection**: Verify WebDAV configuration is correct +- **Save**: Save configuration and auto-test +- **Upload**: Upload local data to the remote server +- **Download**: Download data from the remote server to local + +> **Note**: Upload will overwrite remote data, and download will overwrite local data. Please confirm before proceeding. + +### Log Configuration + +| Setting | Description | +|---------|-------------| +| Enable Logging | Enable/disable application logging | +| Log Level | error / warn / info / debug / trace | + +Log level descriptions: + +- **error** - Critical errors only +- **warn** - Warnings and errors +- **info** - General information (recommended) +- **debug** - Detailed debugging information +- **trace** - All verbose information + +## About Page + +Settings > About Tab + +### Version Information + +Displays the current CC Switch version number, with support for: + +- Viewing release notes +- Checking for updates +- Downloading and installing new versions + +### Local Environment Check + +Automatically detects installed CLI tool versions: + +| Tool | Detection Contents | +|------|-------------------| +| Claude | Current version, latest version | +| Codex | Current version, latest version | +| Gemini | Current version, latest version | +| OpenCode | Current version, latest version | +| OpenClaw | Current version, latest version | + +Click the "Refresh" button to re-detect. + +### One-click Install Commands + +Provides quick commands to install/update CLI tools: + +```bash +npm i -g @anthropic-ai/claude-code@latest +npm i -g @openai/codex@latest +npm i -g @google/gemini-cli@latest +npm i -g opencode@latest +npm i -g openclaw@latest +``` + +Click the "Copy" button to copy to clipboard. diff --git a/docs/user-manual/en/2-providers/2.1-add.md b/docs/user-manual/en/2-providers/2.1-add.md new file mode 100644 index 000000000..3ba472ff1 --- /dev/null +++ b/docs/user-manual/en/2-providers/2.1-add.md @@ -0,0 +1,354 @@ +# 2.1 Add Provider + +## Open the Add Panel + +Click the **+** button in the top-right corner of the main interface to open the Add Provider panel. + +The panel has two tabs: +- **App-specific Provider**: Only for the currently selected app (Claude/Codex/Gemini/OpenCode/OpenClaw) +- **Universal Provider**: Shared configuration across apps + +## Add Using Presets + +Presets are pre-configured provider templates that only require an API Key to use. + +### Steps + +1. Select a provider from the "Preset" dropdown +2. Name and endpoint are auto-filled +3. Enter your **API Key** +4. (Optional) Add notes +5. Click "Add" + +### Common Presets + +#### Claude Presets + +| Preset Name | Description | +|-------------|-------------| +| Claude Official | Log in with an Anthropic official account | +| DeepSeek | DeepSeek model | +| Zhipu GLM | Zhipu AI GLM model | +| Zhipu GLM en | Zhipu AI (English version) | +| Bailian | Alibaba Cloud Bailian (Qwen) | +| Kimi | Moonshot Kimi model | +| Kimi For Coding | Kimi coding-specific model | +| ModelScope | ModelScope community | +| KAT-Coder | KAT-Coder model | +| Longcat | Longcat AI | +| MiniMax | MiniMax model | +| MiniMax en | MiniMax (English version) | +| DouBaoSeed | DouBao Seed model | +| BaiLing | BaiLing AI | +| AiHubMix | AiHubMix aggregation service | +| SiliconFlow | SiliconFlow | +| SiliconFlow en | SiliconFlow (English version) | +| DMXAPI | DMXAPI proxy service | +| PackyCode | PackyCode proxy service | +| Cubence | Cubence service | +| AIGoCode | AIGoCode service | +| RightCode | RightCode service | +| AICodeMirror | AICodeMirror service | +| OpenRouter | Aggregation routing service | +| Nvidia | Nvidia AI service | +| Xiaomi MiMo | Xiaomi MiMo model | + +> The preset list may be updated with new versions. Refer to the actual list shown in the app. + +#### Codex Presets + +| Preset Name | Description | +|-------------|-------------| +| OpenAI Official | Log in with an OpenAI official account | +| Azure OpenAI | Azure OpenAI service | +| AiHubMix | AiHubMix aggregation service | +| DMXAPI | DMXAPI proxy service | +| PackyCode | PackyCode proxy service | +| Cubence | Cubence service | +| AIGoCode | AIGoCode service | +| RightCode | RightCode service | +| AICodeMirror | AICodeMirror service | +| OpenRouter | Aggregation routing service | + +#### Gemini Presets + +| Preset Name | Description | +|-------------|-------------| +| Google Official | Log in with Google OAuth | +| PackyCode | PackyCode proxy service | +| Cubence | Cubence service | +| AIGoCode | AIGoCode service | +| AICodeMirror | AICodeMirror service | +| OpenRouter | Aggregation routing service | +| Custom | Manually configure all parameters | + +#### OpenCode Presets + +| Preset Name | Description | +|-------------|-------------| +| DeepSeek | DeepSeek model | +| Zhipu GLM | Zhipu AI GLM model | +| Zhipu GLM en | Zhipu AI (English version) | +| Bailian | Alibaba Cloud Bailian | +| Kimi k2.5 | Moonshot Kimi-k2.5 model | +| Kimi For Coding | Kimi coding-specific model | +| ModelScope | ModelScope community | +| KAT-Coder | KAT-Coder model | +| Longcat | Longcat AI | +| MiniMax | MiniMax model | +| MiniMax en | MiniMax (English version) | +| DouBaoSeed | DouBao Seed model | +| BaiLing | BaiLing AI | +| Xiaomi MiMo | Xiaomi MiMo model | +| AiHubMix | AiHubMix aggregation service | +| DMXAPI | DMXAPI proxy service | +| OpenRouter | Aggregation routing service | +| Nvidia | Nvidia AI service | +| PackyCode | PackyCode proxy service | +| Cubence | Cubence service | +| AIGoCode | AIGoCode service | +| RightCode | RightCode service | +| AICodeMirror | AICodeMirror service | +| OpenAI Compatible | OpenAI-compatible interface | +| Oh My OpenCode | Oh My OpenCode service | + +> The preset list is continuously updated. Refer to the actual list shown in the app. + +#### OpenClaw Presets + +| Preset Name | Description | +|-------------|-------------| +| DeepSeek | DeepSeek model | +| Zhipu GLM | Zhipu AI GLM model | +| Zhipu GLM en | Zhipu AI (English version) | +| Qwen Coder | Qwen coding model | +| Kimi k2.5 | Moonshot Kimi-k2.5 model | +| Kimi For Coding | Kimi coding-specific model | +| MiniMax | MiniMax model | +| MiniMax en | MiniMax (English version) | +| KAT-Coder | KAT-Coder model | +| Longcat | Longcat AI | +| DouBaoSeed | DouBao Seed model | +| BaiLing | BaiLing AI | +| Xiaomi MiMo | Xiaomi MiMo model | +| AiHubMix | AiHubMix aggregation service | +| DMXAPI | DMXAPI proxy service | +| OpenRouter | Aggregation routing service | +| ModelScope | ModelScope community | +| SiliconFlow | SiliconFlow | +| SiliconFlow en | SiliconFlow (English version) | +| Nvidia | Nvidia AI service | +| PackyCode | PackyCode proxy service | +| Cubence | Cubence service | +| AIGoCode | AIGoCode service | +| RightCode | RightCode service | +| AICodeMirror | AICodeMirror service | +| AICoding | AICoding service | +| CrazyRouter | CrazyRouter service | +| SSSAiCode | SSSAiCode service | +| AWS Bedrock | AWS Bedrock service | +| OpenAI Compatible | OpenAI-compatible interface | + +## Custom Configuration + +After selecting the "Custom" preset, you need to manually edit the JSON configuration. + +### Claude Configuration Format + +```json +{ + "env": { + "ANTHROPIC_API_KEY": "your-api-key", + "ANTHROPIC_BASE_URL": "https://api.example.com" + } +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `ANTHROPIC_API_KEY` | Yes | API key | +| `ANTHROPIC_BASE_URL` | No | Custom endpoint URL | +| `ANTHROPIC_AUTH_TOKEN` | No | Alternative authentication method to API_KEY | + +### Codex Configuration Format + +Codex uses two configuration files: + +**1. auth.json** (`~/.codex/auth.json`) - Stores API key: + +```json +{ + "OPENAI_API_KEY": "your-api-key" +} +``` + +**2. config.toml** (`~/.codex/config.toml`) - Stores model and endpoint configuration: + +```toml +# Basic configuration +model_provider = "custom" +model = "gpt-5.2" +model_reasoning_effort = "high" +disable_response_storage = true + +# Custom provider configuration +[model_providers.custom] +name = "custom" +base_url = "https://api.example.com/v1" +wire_api = "responses" +requires_openai_auth = true +``` + +**auth.json field descriptions**: + +| Field | Required | Description | +|-------|----------|-------------| +| `OPENAI_API_KEY` | Yes | API key | + +**config.toml field descriptions**: + +| Field | Required | Description | +|-------|----------|-------------| +| `model_provider` | Yes | Model provider name (must match `[model_providers.xxx]`) | +| `model` | Yes | Model to use (e.g., `gpt-5.2`, `gpt-4o`) | +| `model_reasoning_effort` | No | Reasoning effort: `low` / `medium` / `high` | +| `disable_response_storage` | No | Whether to disable response storage | +| `base_url` | Yes | API endpoint URL | +| `wire_api` | No | API protocol type (usually `responses`) | +| `requires_openai_auth` | No | Whether to use OpenAI authentication | + + +### Gemini Configuration Format + +```json +{ + "env": { + "GEMINI_API_KEY": "your-api-key", + "GOOGLE_GEMINI_BASE_URL": "https://api.example.com" + } +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `GEMINI_API_KEY` | Yes | API key | +| `GOOGLE_GEMINI_BASE_URL` | No | Custom endpoint URL | +| `GEMINI_MODEL` | No | Specify model | + +> Authentication type is automatically detected by CC Switch (PackyCode API proxy / Google OAuth / generic API Key), no manual configuration needed. + +## Universal Provider + +Universal providers can share configurations across Claude/Codex/Gemini/OpenCode/OpenClaw, suitable for proxy services that support multiple API formats. + +### Create a Universal Provider + +1. Switch to the "Universal Provider" tab +2. Click "Add Universal Provider" +3. Fill in the common configuration: + - Name + - API Key + - Endpoint URL +4. Check the apps to sync to (Claude/Codex/Gemini/OpenCode/OpenClaw) +5. Save + +### Sync Mechanism + +Universal providers automatically sync to the selected apps: + +- After modifying a universal provider, all linked app configurations are updated +- After deleting a universal provider, linked app configurations are also deleted + +### Save and Sync + +When editing a universal provider, you can choose: + +| Action | Description | +|--------|-------------| +| Save | Save configuration only, without immediate sync | +| Save and Sync | Save configuration and immediately sync to all enabled apps | + +### Manual Sync + +If you need to manually trigger a sync: + +1. Click the "Sync" button on the universal provider card +2. Confirm the sync operation +3. Configuration will overwrite the linked provider in each app + +## Import Providers + +CC Switch supports two ways to import provider configurations: + +### Option 1: Deep Link Import + +One-click import via `ccswitch://` protocol links: + +1. Click or visit the deep link +2. CC Switch opens automatically and shows the import confirmation +3. Preview the configuration information +4. Click "Confirm Import" + +**Getting deep links**: +- Obtain from shared links by others +- Create using the [online generator tool](https://farion1231.github.io/cc-switch/deplink.html) + +### Option 2: Database Backup Import + +Batch import from SQL backup files: + +1. Open "Settings > Advanced > Data Management" +2. Click "Select File" +3. Select a previously exported `.sql` backup file +4. Click "Import" +5. Confirm to overwrite existing configuration + +**Imported contents**: +- All provider configurations +- MCP server configurations +- Prompt presets +- Usage logs + +> **Note**: Importing will overwrite the existing database. It is recommended to export your current configuration as a backup first. The exported file name format is `cc-switch-export-{timestamp}.sql`. + +## Advanced Options + +### Custom Icon + +Click the icon area to the left of the name to: + +- Select a preset icon +- Customize icon color + +### Website Link + +Enter the provider's website or console URL for quick access: + +- Click the link icon on the provider card to open directly +- Useful for checking balance, obtaining API keys, etc. + +### Notes + +Add notes such as: + +- Account purpose (personal/work) +- Plan information +- Expiration date + +Notes are displayed on the provider card and are searchable. + +### Endpoint Speed Test + +After adding a provider, you can speed-test API endpoints: + +1. Click the "Speed Test" button on the provider card +2. Add multiple endpoint URLs in the speed test panel +3. Click "Test" to run the test +4. Select the endpoint with the lowest latency + +**Test results**: +- Green: Latency < 500ms (Excellent) +- Yellow: Latency 500-1000ms (Fair) +- Red: Latency > 1000ms (Slow) + +![image-20260108005327817](../../assets/image-20260108005327817.png) diff --git a/docs/user-manual/en/2-providers/2.2-switch.md b/docs/user-manual/en/2-providers/2.2-switch.md new file mode 100644 index 000000000..7dced2215 --- /dev/null +++ b/docs/user-manual/en/2-providers/2.2-switch.md @@ -0,0 +1,111 @@ +# 2.2 Switch Provider + +## Switch from Main Interface + +In the provider list, click the "Enable" button on the target provider card. + +### Switching Flow + +1. Click the "Enable" button +2. CC Switch updates the configuration file +3. The card status changes to "Currently Active" +4. Claude/Gemini take effect immediately, Codex requires a terminal restart + +### Status Indicators + +| Status | Display | Description | +|--------|---------|-------------| +| Currently Active | Blue border + label | Current provider in the configuration file | +| Proxy Active | Green border | Provider actually in use during proxy mode | +| Normal | Default style | Inactive provider | + +## Quick Switch via System Tray + +Quickly switch providers via the system tray without opening the main interface. + +### Steps + +1. Right-click the CC Switch icon in the system tray +2. Find the corresponding app (Claude/Codex/Gemini/OpenCode) in the menu +3. Click the provider name you want to switch to +4. Switching completes with a brief tray notification + +### Tray Menu Structure + +![image-20260108004348993](../../assets/image-20260108004348993.png) + +## Activation Methods + +### Claude Code + +**Takes effect immediately after switching**, no restart needed. + +Claude Code supports hot reload and automatically detects configuration file changes and reloads. + +### Codex + +Requires restart after switching: +- Close the current terminal window +- Reopen the terminal + +### Gemini CLI + +**Takes effect immediately after switching**, no restart needed. + +Gemini CLI re-reads the `.env` file on each request. + +## Configuration File Changes + +When switching providers, CC Switch modifies the following files: + +### Claude + +``` +~/.claude/settings.json +``` + +Modified content: +```json +{ + "env": { + "ANTHROPIC_API_KEY": "new API Key", + "ANTHROPIC_BASE_URL": "new endpoint" + } +} +``` + +### Codex + +``` +~/.codex/auth.json +~/.codex/config.toml (if additional configuration exists) +``` + +### Gemini + +``` +~/.gemini/.env +~/.gemini/settings.json +``` + +## Handling Switch Failures + +If switching fails, possible reasons: + +### Configuration File Is Locked + +Another program is using the configuration file. + +**Solution**: Close the running CLI tool and try switching again. + +### Insufficient Permissions + +No write permission to the configuration file. + +**Solution**: Check the permission settings of the configuration directory. + +### Invalid Configuration Format + +The provider's JSON configuration has format errors. + +**Solution**: Edit the provider, check and fix the JSON format. diff --git a/docs/user-manual/en/2-providers/2.3-edit.md b/docs/user-manual/en/2-providers/2.3-edit.md new file mode 100644 index 000000000..9f148266e --- /dev/null +++ b/docs/user-manual/en/2-providers/2.3-edit.md @@ -0,0 +1,145 @@ +# 2.3 Edit Provider + +## Open the Edit Panel + +1. Find the provider card you want to edit +2. Hover over the card to reveal action buttons +3. Click the "Edit" button + +## Editable Content + +### Basic Information + +| Field | Description | +|-------|-------------| +| Name | Provider display name | +| Notes | Additional notes | +| Website Link | Provider website or console URL | +| Icon | Custom icon and color | + +### Icon Customization + +CC Switch provides rich icon customization features: + +#### Icon Picker + +1. Click the icon area to open the icon picker +2. Use the search box to search icons by name +3. Click to select the desired icon + +The icon library includes common AI service provider and technology icons, supporting: +- Fuzzy search by name +- Icon name tooltips +- Real-time preview of selected icon + +![image-20260108004734882](../../assets/image-20260108004734882.png) + +### Configuration + +JSON-formatted configuration content, including: + +- API Key +- Endpoint URL +- Other environment variables + +### Editing the Currently Active Provider + +When editing the currently active provider, a special "backfill" mechanism applies: + +1. When opening the edit panel, the latest content is read from the live configuration file +2. If you manually modified the configuration in the CLI tool, those changes are synced back +3. After saving, modifications are written to the live configuration file + +This ensures CC Switch and CLI tool configurations stay in sync. + +## Modify API Key + +When editing a provider, you can modify the key directly in the **API Key** input field: + +1. Click the "Edit" button on the provider card +2. Enter the new key in the "API Key" input field +3. Click "Save" + +> **Tip**: The API Key input field supports a show/hide toggle. Click the eye icon on the right to view the full key. + +## Modify Endpoint URL + +When editing a provider, you can modify the URL directly in the **Endpoint URL** input field: + +1. Click the "Edit" button on the provider card +2. Enter the new URL in the "Endpoint URL" input field +3. Click "Save" + +### Endpoint URL Format + +| Application | Format Example | +|-------------|----------------| +| Claude | `https://api.example.com` | +| Codex | `https://api.example.com/v1` | +| Gemini | `https://api.example.com` | + +## Add Custom Endpoints + +Providers can be configured with multiple endpoints for: + +- Testing multiple addresses during speed tests +- Backup endpoints for failover + +### Auto-collection + +When adding a provider, CC Switch automatically extracts endpoint URLs from the configuration. + +### Manual Addition + +When editing a provider, in the "Endpoint Management" area you can: + +- Add new endpoints +- Delete existing endpoints +- Set a default endpoint + +## JSON Editor + +Configuration uses JSON format, and the editor provides: + +- Syntax highlighting +- Format validation +- Error messages + +### Common Errors + +**Missing quotes**: +```json +// Wrong +{ env: { KEY: "value" } } + +// Correct +{ "env": { "KEY": "value" } } +``` + +**Trailing comma**: +```json +// Wrong +{ "env": { "KEY": "value", } } + +// Correct +{ "env": { "KEY": "value" } } +``` + +**Unclosed brackets**: +```json +// Wrong +{ "env": { "KEY": "value" } + +// Correct +{ "env": { "KEY": "value" } } +``` + +## Save and Activate + +1. Click the "Save" button +2. If this is the currently active provider, the configuration is immediately written to the live file +3. Restart the CLI tool for changes to take effect + +## Cancel Editing + +Click "Cancel" or press the `Esc` key to close the edit panel. All modifications will be discarded. diff --git a/docs/user-manual/en/2-providers/2.4-sort-duplicate.md b/docs/user-manual/en/2-providers/2.4-sort-duplicate.md new file mode 100644 index 000000000..3ec4d8db9 --- /dev/null +++ b/docs/user-manual/en/2-providers/2.4-sort-duplicate.md @@ -0,0 +1,76 @@ +# 2.4 Sort & Duplicate + +## Drag to Reorder + +Adjust the display order of providers by dragging. + +### Steps + +1. Move the mouse to the **≡** drag handle on the left side of the provider card +2. Hold the left mouse button +3. Drag up or down to the target position +4. Release the mouse to complete reordering + +### Reorder Uses + +- **Prioritize frequently used**: Place frequently used providers at the top of the list +- **Failover order**: Sorting affects the default order of the failover queue + +## Duplicate Provider + +Quickly create a copy of a provider, useful for: + +- Creating variations based on existing configurations +- Backing up current configurations +- Creating test configurations + +### Steps + +1. Hover over the provider card to reveal action buttons +2. Click the "Duplicate" button +3. A copy is automatically created with a `copy` name suffix +4. Edit the copy to modify the configuration + +### Duplicated Content + +Duplication creates a complete copy, including: + +| Content | Duplicated | +|---------|------------| +| Name | Yes (with `copy` suffix) | +| Configuration | Fully duplicated | +| Notes | Yes | +| Website Link | Yes | +| Icon | Yes | +| Endpoint List | Yes | +| Sort Position | Inserted below the original provider | + +### After Duplication + +After duplication, you typically need to modify: + +1. **Name**: Change to a meaningful name +2. **API Key**: If using a different account +3. **Endpoint**: If using a different service + +## Delete Provider + +### Steps + +1. Hover over the provider card to reveal action buttons +2. Click the "Delete" button +3. Confirm deletion + +### Deletion Confirmation + +A confirmation dialog appears before deletion, showing: + +- Provider name +- Warning that deletion cannot be undone + +### Deletion Restrictions + +- **Currently active provider**: Can be deleted, but it is recommended to switch to another provider first +- **Universal provider**: Deleting will also remove linked app configurations + +![image-20260108004946288](../../assets/image-20260108004946288.png) diff --git a/docs/user-manual/en/2-providers/2.5-usage-query.md b/docs/user-manual/en/2-providers/2.5-usage-query.md new file mode 100644 index 000000000..c9d782941 --- /dev/null +++ b/docs/user-manual/en/2-providers/2.5-usage-query.md @@ -0,0 +1,181 @@ +# 2.5 Usage Query + +## Overview + +The usage query feature allows you to configure custom scripts to query a provider's remaining balance, used amount, and other information in real time. + +**Use cases**: +- Check API account remaining balance +- Monitor plan usage +- Multi-plan balance summary display + +## Open Configuration + +1. Hover over the provider card to reveal action buttons +2. Click the "Usage Query" button (chart icon) +3. Opens the usage query configuration panel + +## Enable Usage Query + +At the top of the configuration panel, enable the "Enable Usage Query" toggle. + +## Preset Templates + +CC Switch provides three preset templates: + +### Custom Template + +Fully customizable request and extraction logic, suitable for special API formats. + +### Generic Template + +Suitable for most providers with standard API formats: + +```javascript +({ + request: { + url: "{{baseUrl}}/user/balance", + method: "GET", + headers: { + "Authorization": "Bearer {{apiKey}}", + "User-Agent": "cc-switch/1.0" + } + }, + extractor: function(response) { + return { + isValid: response.is_active || true, + remaining: response.balance, + unit: "USD" + }; + } +}) +``` + +**Configuration parameters**: +| Parameter | Description | +|-----------|-------------| +| API Key | Authentication key (optional, uses provider's key if empty) | +| Base URL | API base URL (optional, uses provider's endpoint if empty) | + +### New API Template + +Designed specifically for New API-type proxy services: + +```javascript +({ + request: { + url: "{{baseUrl}}/api/user/self", + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer {{accessToken}}", + "New-Api-User": "{{userId}}" + }, + }, + extractor: function (response) { + if (response.success && response.data) { + return { + planName: response.data.group || "Default Plan", + remaining: response.data.quota / 500000, + used: response.data.used_quota / 500000, + total: (response.data.quota + response.data.used_quota) / 500000, + unit: "USD", + }; + } + return { + isValid: false, + invalidMessage: response.message || "Query failed" + }; + }, +}) +``` + +**Configuration parameters**: +| Parameter | Description | +|-----------|-------------| +| Base URL | New API service URL | +| Access Token | Access token | +| User ID | User ID | + +## General Configuration + +### Timeout + +Request timeout in seconds, default 10 seconds. + +### Auto Query Interval + +Interval for automatically refreshing usage data (minutes): +- Set to `0` to disable auto query +- Range: 0-1440 minutes (up to 24 hours) +- Only effective when the provider is in "Currently Active" status + +## Extractor Return Format + +The extractor function must return an object containing the following fields: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `isValid` | boolean | No | Whether the account is valid, defaults to true | +| `invalidMessage` | string | No | Message when invalid | +| `remaining` | number | Yes | Remaining balance | +| `unit` | string | Yes | Unit (e.g., USD, CNY, times) | +| `planName` | string | No | Plan name (supports multi-plan) | +| `total` | number | No | Total balance | +| `used` | number | No | Used amount | +| `extra` | object | No | Additional information | + +## Test Script + +After configuration, click the "Test Script" button to verify: + +1. Sends a request to the configured URL +2. Executes the extractor function +3. Displays the returned result or error message + +## Display + +After successful configuration, the provider card displays: + +- **Single plan**: Directly shows remaining balance +- **Multi-plan**: Shows plan count, click to expand for details + +## Variable Placeholders + +The following placeholders can be used in scripts and are automatically replaced at runtime: + +| Placeholder | Description | +|-------------|-------------| +| `{{apiKey}}` | Configured API Key | +| `{{baseUrl}}` | Configured Base URL | +| `{{accessToken}}` | Configured Access Token (New API) | +| `{{userId}}` | Configured User ID (New API) | + +## Common Provider Configuration Examples + +### Troubleshooting + +### Query Failed + +**Check**: +1. Is the API Key correct +2. Is the Base URL correct +3. Is the network accessible +4. Is the timeout sufficient + +### Empty Response Data + +**Check**: +1. Does the extractor function have a `return` statement +2. Does the response data structure match the extractor +3. Use "Test Script" to view the raw response + +### Format Failed + +When there is a script syntax error, clicking the "Format" button will indicate the error location. + +## Notes + +- Usage queries consume a small amount of API request quota +- Set a reasonable auto query interval to avoid frequent requests +- Sensitive information (API Key, Token) is securely stored locally diff --git a/docs/user-manual/en/3-extensions/3.1-mcp.md b/docs/user-manual/en/3-extensions/3.1-mcp.md new file mode 100644 index 000000000..02dcece06 --- /dev/null +++ b/docs/user-manual/en/3-extensions/3.1-mcp.md @@ -0,0 +1,209 @@ +# 3.1 MCP Server Management + +## What is MCP + +MCP (Model Context Protocol) is a protocol that allows AI tools to access external data sources and tools. Through MCP servers, you can enable AI to: + +- Access file systems +- Make network requests +- Query databases +- Call external APIs + +## Open the MCP Panel + +Click the **MCP** button in the top navigation bar. + +## Panel Overview + +![image-20260108005723522](../../assets/image-20260108005723522.png) + +## Add MCP Server + +### Using Preset Templates + +1. Click the **+** button in the top-right corner +2. Select a template from the "Preset" dropdown +3. Modify the configuration as needed +4. Click "Save" + +![image-20260108005739731](../../assets/image-20260108005739731.png) + +### Common Presets + +| Preset | Package Name | Description | +|--------|-------------|-------------| +| fetch | mcp-server-fetch | HTTP request tool that enables AI to fetch web content | +| time | @modelcontextprotocol/server-time | Time tool that provides current time information | +| memory | @modelcontextprotocol/server-memory | Memory tool that enables AI to store and retrieve information | +| sequential-thinking | @modelcontextprotocol/server-sequential-thinking | Chain-of-thought tool that enhances AI reasoning | +| context7 | @upstash/context7-mcp | Documentation search tool for querying technical docs | + +### Custom Configuration + +After selecting "Custom", fill in: + +| Field | Required | Description | +|-------|----------|-------------| +| Server ID | Yes | Unique identifier | +| Name | No | Display name | +| Description | No | Function description | +| Transport Type | Yes | stdio / http / sse | +| Command | Yes* | Required for stdio type | +| Arguments | No | Command-line arguments | +| URL | Yes* | Required for http/sse type | +| Headers | No | Request headers for http/sse type | +| Environment Variables | No | Environment variables passed to the server | + +## Transport Types + +### stdio (Standard I/O) + +The most common type, communicating by launching a local process. + +```json +{ + "command": "uvx", + "args": ["mcp-server-fetch"], + "env": {} +} +``` + +**Requirements**: +- The corresponding command must be installed (e.g., `uvx`, `npx`) +- The server program must be in PATH + +### http + +Communicates with a remote server via HTTP protocol. + +```json +{ + "url": "http://localhost:8080/mcp" +} +``` + +### sse (Server-Sent Events) + +Communicates with a server via SSE protocol, supporting real-time push. + +```json +{ + "url": "http://localhost:8080/sse" +} +``` + +## App Binding + +Each MCP server can independently control which apps it is enabled for. + +### Toggle Description + +| Toggle | Effect | Configuration File Path | +|--------|--------|------------------------| +| Claude | Sync to Claude Code | `~/.claude.json`'s `mcpServers` | +| Codex | Sync to Codex | `~/.codex/config.toml`'s `[mcp_servers]` | +| Gemini | Sync to Gemini CLI | `~/.gemini/settings.json`'s `mcpServers` | +| OpenCode | Sync to OpenCode | `~/.opencode/config.json`'s `mcpServers` | + +> **Note**: OpenClaw does not currently support MCP server management. MCP functionality is currently only supported for Claude, Codex, Gemini, and OpenCode. + +### Toggle Implementation + +When enabling an app's toggle, CC Switch will: + +1. **Update database**: Set the server's `apps.claude/codex/gemini/opencode` status to `true` +2. **Sync to live configuration**: Write the server configuration to the corresponding app's configuration file +3. **Take effect immediately**: The new MCP server is automatically loaded the next time the CLI tool starts + +When disabling an app's toggle, CC Switch will: + +1. **Update database**: Set the corresponding app status to `false` +2. **Remove from live configuration**: Delete the server from the app's configuration file +3. **Take effect immediately**: The MCP server is no longer loaded the next time the CLI tool starts + +### Sync Conditions + +MCP server sync only executes when the corresponding app is installed: + +- **Claude**: Requires `~/.claude/` directory or `~/.claude.json` file to exist +- **Codex**: Requires `~/.codex/` directory to exist +- **Gemini**: Requires `~/.gemini/` directory to exist +- **OpenCode**: Requires `~/.opencode/` directory to exist + +> **Tip**: If a CLI tool is not installed, enabling its toggle will not cause an error, but the configuration will not be written. + +When the toggle is disabled, the configuration is removed from the file. + +## Edit Server + +1. Click the "Edit" button on the right side of the server row +2. Modify the configuration +3. Click "Save" + +Changes are immediately synced to enabled app configuration files. + +## Delete Server + +1. Click the "Delete" button on the right side of the server row +2. Confirm deletion + +After deletion, the configuration is removed from all app configuration files. + +## Import Existing Configurations + +If you have already configured MCP servers in CLI tools, you can import them into CC Switch: + +1. Click the "Import" button +2. Select the app to import from (Claude/Codex/Gemini/OpenCode) +3. CC Switch reads the existing configuration and imports it + +## Configuration File Formats + +### Claude (`~/.claude.json`) + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +### Codex (`~/.codex/config.toml`) + +```toml +[mcp_servers.mcp-fetch] +command = "uvx" +args = ["mcp-server-fetch"] +``` + +### Gemini (`~/.gemini/settings.json`) + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +## FAQ + +### Server Fails to Start + +Check: +- Is the command properly installed (e.g., `uvx`) +- Is the command in PATH +- Are the arguments correct + +### Configuration Not Taking Effect + +Ensure: +- The corresponding app toggle is enabled +- The CLI tool has been restarted diff --git a/docs/user-manual/en/3-extensions/3.2-prompts.md b/docs/user-manual/en/3-extensions/3.2-prompts.md new file mode 100644 index 000000000..d9bbd38ba --- /dev/null +++ b/docs/user-manual/en/3-extensions/3.2-prompts.md @@ -0,0 +1,160 @@ +# 3.2 Prompts Management + +## Overview + +The Prompts feature manages system prompt presets. System prompts influence the AI's behavior and response style. + +With CC Switch, you can: + +- Create multiple prompt presets +- Quickly switch prompts for different scenarios +- Sync prompt configurations across devices + +## Open the Prompts Panel + +Click the **Prompts** button in the top navigation bar. + +## Panel Overview + +![image-20260108010110382](../../assets/image-20260108010110382.png) + +## Create a Preset + +### Steps + +1. Click the **+** button in the top-right corner +2. Enter a preset name +3. Write the prompt in the Markdown editor +4. Click "Save" + +### Markdown Editor + +The editor provides: + +- Syntax highlighting +- Live preview +- Common format shortcuts + +### Prompt Writing Tips + +**Structured format**: + +```markdown +# Role Definition + +You are a professional code review expert. + +## Core Capabilities + +- Code quality analysis +- Performance optimization suggestions +- Security vulnerability detection + +## Response Style + +- Clear and concise +- Provide specific examples +- Give improvement suggestions + +## Notes + +- Do not modify business logic +- Maintain consistent code style +``` + +## Activate a Preset + +### How to Activate + +Click the toggle switch on the preset item to change its activation status. + +### Single Activation + +Only one preset can be active at a time. Activating a new preset automatically deactivates the previous one. + +### Sync Target + +After activation, the prompt is written to the corresponding app's file: + +| Application | File Path | +|-------------|-----------| +| Claude | `~/.claude/CLAUDE.md` | +| Codex | `~/.codex/AGENTS.md` | +| Gemini | `~/.gemini/GEMINI.md` | +| OpenCode | `~/.opencode/AGENTS.md` | +| OpenClaw | `~/.openclaw/AGENTS.md` | + +## Edit a Preset + +1. Click the "Edit" button on the preset item +2. Modify the name or content +3. Click "Save" + +If the currently active preset is edited, changes are immediately synced to the configuration file. + +## Delete a Preset + +1. Click the "Delete" button on the preset item +2. Confirm deletion + +Active presets cannot be deleted. Deactivate the preset first before deleting. + +## Smart Backfill + +CC Switch provides a smart backfill protection mechanism to ensure your manual modifications are not lost. + +### How It Works + +1. Before switching presets, automatically reads the current configuration file content +2. Compares file content with the preset in the database +3. If the content differs, it means the user has manually modified it +4. Saves the manually modified content to the current preset +5. Then switches to the new preset + +### Protection Scenarios + +| Scenario | Handling | +|----------|----------| +| Directly editing `CLAUDE.md` in CLI | Changes auto-saved to current preset | +| Modifying config file with external editor | Changes auto-saved to current preset | +| Switching to another preset | Current changes saved first, then switched | + +### Technical Details + +The backfill mechanism triggers at these moments: + +- **When switching presets**: Saves current live file content to the current preset +- **When editing the current preset**: Reads latest content from the live file +- **On first launch**: Automatically imports existing live file content + +### Notes + +- Backfill only triggers when switching to a different preset +- If no preset is currently active, backfill is not triggered +- Backfill failure does not affect the switching process + +## Cross-app Usage + +Prompts are managed separately per app: + +- When switched to Claude, Claude's presets are shown +- When switched to Codex, Codex's presets are shown +- When switched to Gemini, Gemini's presets are shown +- When switched to OpenCode, OpenCode's presets are shown +- When switched to OpenClaw, OpenClaw's presets are shown + +To use the same prompt across multiple apps, you need to create them separately. + +## Import & Export + +### Share via Deep Link + +You can generate deep links to share presets: + +``` +ccswitch://import/prompt?data= +``` + +### Via Configuration Export + +Exporting configuration includes all presets, which can be restored upon import. diff --git a/docs/user-manual/en/3-extensions/3.3-skills.md b/docs/user-manual/en/3-extensions/3.3-skills.md new file mode 100644 index 000000000..43dd84854 --- /dev/null +++ b/docs/user-manual/en/3-extensions/3.3-skills.md @@ -0,0 +1,207 @@ +# 3.3 Skills Management + +## Overview + +Skills are reusable capability extensions that give AI tools specialized abilities in specific domains. + +Skills exist as folders containing: + +- Prompt templates +- Tool definitions +- Example code + +## Supported Applications + +Skills are supported across all four applications: + +- **Claude Code** +- **Codex** +- **Gemini CLI** +- **OpenCode** + +## Open the Skills Page + +Click the **Skills** button in the top navigation bar. + +> Note: The Skills button is visible in all app modes. + +## Page Overview + +![image-20260108010253926](../../assets/image-20260108010253926.png) + +## Discover Skills + +### Pre-configured Repositories + +CC Switch comes pre-configured with the following GitHub repositories: + +| Repository | Description | +|------------|-------------| +| Anthropic Official | Official skills provided by Anthropic | +| ComposioHQ | Community-maintained skill collection | +| Community Picks | Curated high-quality skills | + +![image-20260108010308060](../../assets/image-20260108010308060.png) + +### Search & Filter + +CC Switch provides powerful search and filter features: + +#### Search Box + +- Search by skill name +- Search by skill description +- Search by directory name +- Real-time filtering, results update as you type + +#### Status Filter + +Use the dropdown menu to filter by installation status: + +| Option | Description | +|--------|-------------| +| All | Show all skills | +| Installed | Show only installed skills | +| Not Installed | Show only uninstalled skills | + +![image-20260108010324583](../../assets/image-20260108010324583.png) + +#### Combined Use + +Search and filter can be combined: + +- Select "Installed" filter first +- Then enter keywords to search +- Results show the match count + +### Refresh List + +Click the "Refresh" button to re-scan repositories for the latest skills. + +## Install Skills + +### Steps + +1. Find the skill card you want to install +2. Click the "Install" button +3. Wait for installation to complete + +### Installation Location + +| Application | Install Directory | +|-------------|-------------------| +| Claude | `~/.claude/skills/` | +| Codex | `~/.codex/skills/` | +| Gemini | `~/.gemini/skills/` | +| OpenCode | `~/.opencode/skills/` | + +### Installation Contents + +Installation copies the skill folder to your local machine: + +``` +~/.claude/skills/ +└── skill-name/ + ├── README.md + ├── prompt.md + └── tools/ + └── ... +``` + +## Uninstall Skills + +### Steps + +1. Find the installed skill card +2. Click the "Uninstall" button +3. Confirm uninstallation + +### Uninstall Effect + +- Deletes the local skill folder +- Updates installation status + +## Repository Management + +### Open Repository Management + +Click the "Repository Management" button at the top of the page. + +### Add Custom Repository + +1. Click "Add Repository" +2. Fill in repository information: + - Owner: GitHub username or organization name + - Name: Repository name + - Branch: Branch name (default: main) + - Subdirectory: Subdirectory containing skills (optional) +3. Click "Add" + +### Repository Format + +``` +https://github.com/{owner}/{name}/tree/{branch}/{subdirectory} +``` + +Example: + +``` +Owner: anthropics +Name: claude-skills +Branch: main +Subdirectory: skills +``` + +### Delete Repository + +1. Find the repository in the repository list +2. Click the "Delete" button +3. Confirm deletion + +After deleting a repository, its skills will not disappear from the list, but they can no longer be updated. + +## Skill Card Information + +Each skill card displays: + +| Information | Description | +|-------------|-------------| +| Name | Skill name | +| Description | Function description | +| Source | Source repository | +| Status | Installed / Not Installed | + +## Skill Updates + +Automatic updates are not currently supported. To update a skill: + +1. Uninstall the existing skill +2. Refresh the list +3. Reinstall + +### Empty Skill List + +Possible causes: + +- Network issues preventing GitHub access +- Incorrect repository configuration + +Solutions: + +- Check network connection +- Click "Refresh" to retry +- Verify repository configuration + +### Installation Failed + +Possible causes: + +- Network issues +- Insufficient disk space +- Permission issues + +Solutions: + +- Check network connection +- Check disk space +- Check directory permissions diff --git a/docs/user-manual/en/4-proxy/4.1-service.md b/docs/user-manual/en/4-proxy/4.1-service.md new file mode 100644 index 000000000..05b99597e --- /dev/null +++ b/docs/user-manual/en/4-proxy/4.1-service.md @@ -0,0 +1,222 @@ +# 4.1 Proxy Service + +## Overview + +The proxy service starts a local HTTP proxy through which all API requests are forwarded. + +**Primary uses**: +- Record request logs +- Track API usage +- Support failover +- Centrally manage requests from multiple applications + +## Start the Proxy + +### Option 1: Main Interface Toggle + +Click the **Proxy Toggle** button at the top of the main interface. + +Toggle states: +- White: Proxy not running +- Green: Proxy running + +![image-20260108011353927](../../assets/image-20260108011353927.png) + +### Option 2: Settings Page + +1. Open "Settings > Advanced > Proxy Service" +2. Click the toggle in the top-right corner + +![image-20260108011338922](../../assets/image-20260108011338922.png) + +## Proxy Configuration + +### Basic Configuration + +| Setting | Description | Default | +|---------|-------------|---------| +| Listen Address | IP address the proxy binds to | `127.0.0.1` | +| Listen Port | Port the proxy listens on | `15721` | +| Enable Logging | Whether to record request logs | Enabled | + +### Modify Configuration + +1. **Stop the proxy service** (must stop first) +2. Modify the listen address or port +3. Click "Save" +4. Restart the proxy + +> Modifying address/port requires stopping the proxy service first + +### Listen Address Options + +| Address | Description | +|---------|-------------| +| `127.0.0.1` | Only accessible from local machine (recommended) | +| `0.0.0.0` | Allow LAN access | + +## Running Status + +When the proxy is running, the panel displays the following information: + +### Service Address + +``` +http://127.0.0.1:15721 +``` + +Click the "Copy" button to copy the address. + +### Current Providers + +Displays the currently used provider for each app: + +``` +Claude: PackyCode +Codex: AIGoCode +Gemini: Google Official +``` + +### Statistics + +| Metric | Description | +|--------|-------------| +| Active Connections | Number of requests currently being processed | +| Total Requests | Total number of requests since startup | +| Success Rate | Percentage of successful requests (>90% green, <=90% yellow) | +| Uptime | How long the proxy has been running | + +### Failover Queue + +The proxy panel displays the failover queue by app type: + +``` +Claude +├── 1. PackyCode [Currently Using] ● +├── 2. AIGoCode ● +└── 3. Backup Provider ○ + +Codex +├── 1. AIGoCode [Currently Using] ● +└── 2. Backup Provider ● +``` + +Queue details: +- Numbers indicate priority order +- "Currently Using" label indicates the active provider +- Health badges show provider status: + - Green: Healthy (0 consecutive failures) + - Yellow: Degraded (1-2 consecutive failures) + - Red: Unhealthy (3+ consecutive failures) + +## How It Works + +### Request Flow + +```mermaid +sequenceDiagram + participant CLI as CLI Tool (Claude) + participant Proxy as Local Proxy (CC Switch) + participant API as API Provider (Anthropic) + participant DB as Data Store (Logger) + + CLI->>Proxy: Send API request + Proxy->>DB: Record request log / track usage + Proxy->>API: Forward request + API-->>Proxy: Return response + Proxy-->>CLI: Return response +``` + +### Configuration Changes + +After starting the proxy and enabling app takeover, CC Switch modifies app configurations: + +**Claude**: +```json +{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:15721" + } +} +``` + +**Codex**: +```toml +base_url = "http://127.0.0.1:15721/v1" +``` + +**Gemini**: +``` +GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721 +``` + +## Stop the Proxy + +### Option 1: Main Interface Toggle + +Click the proxy toggle button to turn it off. + +### Option 2: Settings Page + +Turn off the toggle in the proxy service panel. + +### Post-stop Processing + +When stopping the proxy, CC Switch will: + +1. Restore app configurations to their original state +2. Save request logs +3. Close all connections + +## Log Recording + +### Enable Logging + +Enable the "Enable Logging" toggle in the proxy panel. + +### Log Contents + +Each request record includes: + +| Field | Description | +|-------|-------------| +| Time | Request time | +| App | Claude / Codex / Gemini | +| Provider | Provider used | +| Model | Requested model | +| Tokens | Input/output token count | +| Latency | Request duration | +| Status | Success/failure | + +### View Logs + +View request logs in the "Settings > Usage" tab. + +## FAQ + +### Port Already in Use + +Error message: `Address already in use` + +Solution: +1. Change the port (e.g., to 5001) +2. Or close the program occupying the port + +### Proxy Fails to Start + +Check: +- Is the port occupied +- Are there sufficient permissions +- Is the firewall blocking it + +### Request Timeout + +Possible causes: +- Network issues +- Provider server issues +- Incorrect proxy configuration + +Solutions: +- Check network connection +- Try accessing the provider API directly +- Check provider configuration diff --git a/docs/user-manual/en/4-proxy/4.2-takeover.md b/docs/user-manual/en/4-proxy/4.2-takeover.md new file mode 100644 index 000000000..4107f9bfc --- /dev/null +++ b/docs/user-manual/en/4-proxy/4.2-takeover.md @@ -0,0 +1,195 @@ +# 4.2 App Takeover + +## Overview + +App takeover means letting CC Switch's proxy intercept and forward a specific application's API requests. + +When takeover is enabled: +- The app's API requests are forwarded through the local proxy +- Request logs and usage statistics can be recorded +- Failover functionality becomes available + +## Prerequisites + +The proxy service must be started before using the app takeover feature. + +## Enable Takeover + +### Location + +Settings > Advanced > Proxy Service > App Takeover area + +### Steps + +1. Ensure the proxy service is started +2. Find the "App Takeover" area +3. Enable the toggle for the desired apps + +### Takeover Toggles + +| Toggle | Effect | +|--------|--------| +| Claude Takeover | Intercept Claude Code requests | +| Codex Takeover | Intercept Codex requests | +| Gemini Takeover | Intercept Gemini CLI requests | + +Multiple app takeovers can be enabled simultaneously. + +## How Takeover Works + +### Configuration Changes + +When takeover is enabled, CC Switch modifies the app's configuration file to point the API endpoint to the local proxy. + +**Claude configuration change**: + +```json +// Before takeover +{ + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com" + } +} + +// After takeover +{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:15721" + } +} +``` + +**Codex configuration change**: + +```toml +# Before takeover +base_url = "https://api.openai.com/v1" + +# After takeover +base_url = "http://127.0.0.1:15721/v1" +``` + +**Gemini configuration change**: + +```bash +# Before takeover +GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com + +# After takeover +GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721 +``` + +### Request Forwarding + +When the proxy receives a request: + +1. Identifies the request source (Claude/Codex/Gemini) +2. Looks up the currently enabled provider for that app +3. Forwards the request to the provider's actual endpoint +4. Records the request log +5. Returns the response to the app + +## Takeover Status Indicators + +### Main Interface Indicators + +When takeover is enabled, the main interface shows the following changes: + +- **Proxy logo color**: Changes from colorless to green +- **Provider cards**: The currently active provider shows a green border + +### Provider Card States + +| State | Border Color | Description | +|-------|--------------|-------------| +| Currently Active | Blue | Provider in the config file (non-proxy mode) | +| Proxy Active | Green | Provider actually used by the proxy | +| Normal | Default | Unused provider | + +## Disable Takeover + +### Steps + +1. Turn off the corresponding app's takeover toggle in the proxy panel +2. Or directly stop the proxy service + +### Configuration Restoration + +When disabling takeover, CC Switch will: + +1. Restore the app configuration to its pre-takeover state +2. Save current request logs + +## Takeover and Provider Switching + +### Switching Providers in Takeover Mode + +When switching providers in takeover mode: + +1. Click the "Enable" button on a provider in the main interface +2. The proxy immediately uses the new provider to forward requests +3. **No need to restart the CLI tool** + +This is a major advantage of takeover mode: provider switching takes effect instantly. + +### Switching Without Takeover + +When switching providers without takeover: + +1. Configuration file is modified +2. CLI tool must be restarted for changes to take effect + +## Multi-app Takeover + +Multiple apps can be taken over simultaneously, each managed independently: + +- Independent provider configurations +- Independent failover queues +- Independent request statistics + +## Use Cases + +### Scenario 1: Usage Monitoring + +Enable takeover + log recording to monitor API usage. + +### Scenario 2: Quick Switching + +With takeover enabled, switching providers does not require restarting CLI tools. + +### Scenario 3: Failover + +Enabling takeover is a prerequisite for using the failover feature. + +## Notes + +### Performance Impact + +The proxy adds minimal latency (typically < 10ms), negligible for most scenarios. + +### Network Requirements + +In takeover mode, CLI tools must be able to access the local proxy address. + +### Configuration Backup + +Before enabling takeover, CC Switch backs up the original configuration and restores it when disabled. + +## FAQ + +### Requests Fail After Takeover + +Check: +- Is the proxy service running normally +- Is the provider configuration correct +- Is the network working properly + +### Configuration Not Restored After Disabling Takeover + +Possible causes: +- Proxy exited abnormally +- Configuration file was modified by another program + +Solutions: +- Manually edit the provider and re-save +- Or re-enable and then disable takeover diff --git a/docs/user-manual/en/4-proxy/4.3-failover.md b/docs/user-manual/en/4-proxy/4.3-failover.md new file mode 100644 index 000000000..ccd3c9ccd --- /dev/null +++ b/docs/user-manual/en/4-proxy/4.3-failover.md @@ -0,0 +1,232 @@ +# 4.3 Failover + +## Overview + +The failover feature automatically switches to a backup provider when the primary provider's request fails, ensuring uninterrupted service. + +**Applicable scenarios**: +- Unstable provider services +- High availability requirements +- Long-running tasks + +## Prerequisites + +Using the failover feature requires: + +1. Proxy service started +2. App takeover enabled +3. Failover queue configured +4. Auto failover enabled + +## Configure the Failover Queue + +### Open Configuration Page + +Settings > Advanced > Failover + +### Select Application + +Three tabs at the top of the page: +- Claude +- Codex +- Gemini + +Select the application to configure. + +### Add Backup Providers + +1. In the "Failover Queue" area +2. Click "Add Provider" +3. Select a provider from the dropdown list +4. The provider is added to the end of the queue + +### Adjust Priority + +Drag providers to adjust their order: +- Lower numbers mean higher priority +- After the primary provider fails, backup providers are tried in order + +### Remove Provider + +Click the "Remove" button to the right of the provider. + +## Main Interface Quick Actions + +When both proxy and failover are enabled, provider cards display a failover toggle. + +### Add to Queue + +1. Find the provider card +2. Enable the failover toggle +3. The provider is automatically added to the queue + +### Remove from Queue + +1. Disable the failover toggle on the provider card +2. The provider is removed from the queue + +## Enable Auto Failover + +### Steps + +1. On the failover configuration page +2. Enable the "Auto Failover" toggle + +### Toggle Description + +| State | Behavior | +|-------|----------| +| Off | Only records failures, no automatic switching | +| On | Automatically switches to the next provider on failure | + +## Failover Flow + +```mermaid +graph TD + Start[Request arrives at proxy] --> Send[Send to current provider] + Send --> CheckSuccess{Success?} + CheckSuccess -- Yes --> Return[Return response] + CheckSuccess -- No --> LogFail[Record failure] + LogFail --> CheckCircuit{Check circuit breaker} + CheckCircuit -- Tripped --> Skip[Skip this provider] + CheckCircuit -- Not tripped --> IncFail[Increment failure count] + Skip --> Next{Next in queue?} + IncFail --> Next + Next -- Yes --> Switch[Switch provider] + Switch --> Retry[Retry request] + Retry --> Send + Next -- No --> Error[Return error] +``` + +## Circuit Breaker Configuration + +The circuit breaker prevents frequent retries against failing providers. + +### Configuration Items + +Different apps have independent default configurations. Below are general defaults; Claude has its own relaxed configuration. + +| Setting | Description | General Default | Claude Default | Range | +|---------|-------------|-----------------|----------------|-------| +| Failure Threshold | Consecutive failures to trigger circuit breaker | 4 | 8 | 1-20 | +| Recovery Success Threshold | Successes needed in half-open state to close breaker | 2 | 3 | 1-10 | +| Recovery Wait Time | Time before attempting recovery after tripping (seconds) | 60 | 90 | 0-300 | +| Error Rate Threshold | Error rate that opens the circuit breaker | 60% | 70% | 0-100% | +| Minimum Requests | Minimum requests before calculating error rate | 10 | 15 | 5-100 | + +> Claude has more relaxed default settings due to longer request times, tolerating more failures. + +### Timeout Configuration + +| Setting | Description | General Default | Claude Default | Range | +|---------|-------------|-----------------|----------------|-------| +| Stream First Byte Timeout | Max wait time for first data chunk (seconds) | 60 | 90 | 1-120 | +| Stream Idle Timeout | Max interval between data chunks (seconds) | 120 | 180 | 60-600 (0 to disable) | +| Non-stream Timeout | Total timeout for non-streaming requests (seconds) | 600 | 600 | 60-1200 | + +### Retry Configuration + +| Setting | Description | General Default | Claude Default | Range | +|---------|-------------|-----------------|----------------|-------| +| Max Retries | Number of retries on request failure | 3 | 6 | 0-10 | + +> Gemini's default max retries is 5. + +### Circuit Breaker States + +| State | Description | +|-------|-------------| +| Closed | Normal state, requests allowed | +| Open | Circuit broken, this provider is skipped | +| Half-Open | Attempting recovery, sending probe requests | + +### State Transitions + +```mermaid +stateDiagram-v2 + [*] --> Closed: Initialize + Closed --> Open: Failures >= threshold + Open --> HalfOpen: Recovery wait time expires + HalfOpen --> Closed: Probe successes >= recovery threshold + HalfOpen --> Open: Probe failed +``` + +## Health Status Indicators + +### Provider Cards + +Cards display health status badges: + +| Badge | Status | Description | +|-------|--------|-------------| +| Green | Healthy | 0 consecutive failures | +| Yellow | Warning | Has failures but circuit not tripped | +| Red | Circuit Broken | Circuit breaker tripped, temporarily skipped | + +### Queue List + +The failover queue also displays each provider's health status. + +## Failover Logs + +Each failover event records: + +| Information | Description | +|-------------|-------------| +| Time | When it occurred | +| Original Provider | The provider that failed | +| New Provider | The provider switched to | +| Failure Reason | Error message | + +Viewable in the request logs within usage statistics. + +## Best Practices + +### Queue Configuration Recommendations + +1. **Primary provider**: The most stable and fastest provider +2. **First backup**: Second-best choice +3. **Second backup**: Last resort + +### Circuit Breaker Configuration Recommendations + +| Scenario | Failure Threshold | Recovery Wait | +|----------|-------------------|---------------| +| High availability requirement | 2 | 30 seconds | +| General scenario | 3 | 60 seconds | +| Tolerant of occasional failures | 5 | 120 seconds | + +### Monitoring Recommendations + +Periodically check: +- Health status of each provider +- Failover frequency +- Circuit breaker trigger frequency + +## FAQ + +### Failover Not Triggering + +Check: +1. Is the proxy service running +2. Is app takeover enabled +3. Is auto failover enabled +4. Are there backup providers in the queue + +### Failover Triggering Too Frequently + +Possible causes: +- Unstable primary provider +- Network issues +- Configuration errors + +Solutions: +- Check primary provider status +- Adjust circuit breaker parameters +- Consider changing the primary provider + +### All Providers Circuit-Broken + +Wait for the recovery wait time to expire for automatic recovery, or: +1. Manually restart the proxy service +2. Reset circuit breaker states diff --git a/docs/user-manual/en/4-proxy/4.4-usage.md b/docs/user-manual/en/4-proxy/4.4-usage.md new file mode 100644 index 000000000..d59fd27b4 --- /dev/null +++ b/docs/user-manual/en/4-proxy/4.4-usage.md @@ -0,0 +1,291 @@ +# 4.4 Usage Statistics + +## Overview + +The usage statistics feature records and analyzes API request data, helping you: + +- Understand API usage patterns +- Estimate cost expenditure +- Analyze usage patterns +- Troubleshoot issues + +## Prerequisites + +Using the usage statistics feature requires: + +1. Proxy service started +2. App takeover enabled +3. Log recording enabled + +## Open Usage Statistics + +Settings > Usage Tab + +## Statistics Overview + +### Summary Cards + +Key metrics displayed at the top of the page: + +| Metric | Description | +|--------|-------------| +| Total Requests | Total number of requests in the time period | +| Total Tokens | Total input + output tokens | +| Estimated Cost | Cost calculated based on pricing configuration | +| Success Rate | Percentage of successful requests | + +### Time Range + +Select the time range for statistics: + +| Option | Range | +|--------|-------| +| Today | From 00:00 today to now | +| Last 7 Days | Past 7 days | +| Last 30 Days | Past 30 days | + +![image-20260108011730105](../../assets/image-20260108011730105.png) + +## Trend Charts + +### Request Trend + +Line chart showing the trend of request counts: + +- X-axis: Time +- Y-axis: Request count +- Viewable by hour/day +- Supports zoom and drag + +### Token Trend + +Shows token usage trends: + +- Input Tokens (blue) - Prompt content sent by the user +- Output Tokens (green) - Response content generated by AI +- Cache Creation Tokens (orange) - Tokens consumed when first creating cache +- Cache Hit Tokens (purple) - Tokens saved by reusing cache +- Cost (red dashed line, right Y-axis) - Estimated cost + +> **Cache Token explanation**: Anthropic API supports Prompt Caching. Creating cache incurs a higher fee (typically 1.25x input price), but subsequent cache hits only charge 0.1x, significantly reducing costs for repeated requests. + +### Time Granularity + +- **Today**: Displayed by hour (24 data points) +- **7 Days/30 Days**: Displayed by day + + + +![image-20260108011742847](../../assets/image-20260108011742847.png) + +## Detailed Data + +Three data tabs at the bottom of the page: + +### Request Logs + +Detailed record of each request: + +| Field | Description | +|-------|-------------| +| Time | Request time | +| Provider | Provider name used | +| Model | Requested model (billing model) | +| Input Tokens | Number of input tokens | +| Output Tokens | Number of output tokens | +| Cache Read | Cache hit token count | +| Cache Creation | Cache creation token count | +| Total Cost | Estimated cost (USD) | +| Timing Info | Request duration, time to first token, streaming/non-streaming | +| Status | HTTP status code | + +#### Timing Information + +The timing info column displays multiple badges: + +| Badge | Description | Color Rules | +|-------|-------------|-------------| +| Total Duration | Total request time (seconds) | <=5s green, <=120s orange, >120s red | +| First Token | Time to first token in streaming requests | <=5s green, <=120s orange, >120s red | +| Stream/Non-stream | Request type | Streaming blue, non-streaming purple | + +#### View Details + +Click a request row to view detailed information: + +- Complete request parameters +- Response content summary +- Error messages (if failed) + +#### Filter Logs + +Supports filtering by the following criteria: + +| Filter | Options | +|--------|---------| +| App Type | All / Claude / Codex / Gemini | +| Status Code | All / 200 / 400 / 401 / 429 / 500 | +| Provider | Text search | +| Model | Text search | +| Time Range | Start time - End time (datetime picker) | + +Action buttons: +- **Search**: Apply filter criteria +- **Reset**: Restore defaults (past 24 hours) +- **Refresh**: Reload data + +![image-20260108011859974](../../assets/image-20260108011859974.png) + +### Provider Statistics + +Statistics grouped by provider: + +| Field | Description | +|-------|-------------| +| Provider | Provider name | +| Requests | Total requests for this provider | +| Successes | Number of successful requests | +| Failures | Number of failed requests | +| Success Rate | Success percentage | +| Total Tokens | Total token usage | +| Estimated Cost | Cost for this provider | + +![image-20260108011907928](../../assets/image-20260108011907928.png) + +### Model Statistics + +Statistics grouped by model: + +| Field | Description | +|-------|-------------| +| Model | Model name | +| Requests | Total requests for this model | +| Input Tokens | Total input tokens | +| Output Tokens | Total output tokens | +| Avg Latency | Average response time | +| Estimated Cost | Cost for this model | + +![image-20260108011915381](../../assets/image-20260108011915381.png) + +## Pricing Configuration + +### Open Pricing Configuration + +Settings > Advanced > Pricing Configuration + +### Configure Model Prices + +Set prices for each model (per million tokens): + +| Field | Description | +|-------|-------------| +| Model ID | Model identifier (e.g., claude-3-sonnet) | +| Display Name | Custom display name | +| Input Price | Price per million input tokens | +| Output Price | Price per million output tokens | +| Cache Read Price | Price per million cache hit tokens | +| Cache Creation Price | Price per million cache creation tokens | + +### Operations + +- **Add**: Click the "Add" button to add model pricing +- **Edit**: Click the edit icon at the end of the row to modify +- **Delete**: Click the delete icon at the end of the row to remove + +![image-20260108011933565](../../assets/image-20260108011933565.png) + +### Preset Prices + +CC Switch includes preset official prices for common models (per million tokens): + +**Claude Series (USD)**: + +| Model | Input | Output | Cache Read | Cache Creation | +|-------|-------|--------|------------|----------------| +| **Claude 4.5 Series** | | | | | +| claude-opus-4-5 | $5 | $25 | $0.50 | $6.25 | +| claude-sonnet-4-5 | $3 | $15 | $0.30 | $3.75 | +| claude-haiku-4-5 | $1 | $5 | $0.10 | $1.25 | +| **Claude 4 Series** | | | | | +| claude-opus-4 | $15 | $75 | $1.50 | $18.75 | +| claude-opus-4-1 | $15 | $75 | $1.50 | $18.75 | +| claude-sonnet-4 | $3 | $15 | $0.30 | $3.75 | +| **Claude 3.5 Series** | | | | | +| claude-3-5-sonnet | $3 | $15 | $0.30 | $3.75 | +| claude-3-5-haiku | $0.80 | $4 | $0.08 | $1.00 | + +**OpenAI Series / Codex (USD)**: + +| Model | Input | Output | Cache Read | +|-------|-------|--------|------------| +| **GPT-5.2 Series** | | | | +| gpt-5.2 | $1.75 | $14 | $0.175 | +| **GPT-5.1 Series** | | | | +| gpt-5.1 | $1.25 | $10 | $0.125 | +| **GPT-5 Series** | | | | +| gpt-5 | $1.25 | $10 | $0.125 | + +> Note: Codex presets include low/medium/high variants with prices identical to the base model. + +**Gemini Series (USD)**: + +| Model | Input | Output | Cache Read | +|-------|-------|--------|------------| +| **Gemini 3 Series** | | | | +| gemini-3-pro-preview | $2 | $12 | $0.20 | +| gemini-3-flash-preview | $0.50 | $3 | $0.05 | +| **Gemini 2.5 Series** | | | | +| gemini-2.5-pro | $1.25 | $10 | $0.125 | +| gemini-2.5-flash | $0.30 | $2.50 | $0.03 | + +**Chinese Provider Models (CNY)**: + +| Model | Input | Output | Cache Read | +|-------|-------|--------|------------| +| **DeepSeek** | | | | +| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 | +| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 | +| deepseek-v3 | ¥2.00 | ¥8.00 | ¥0.40 | +| **Kimi (Moonshot)** | | | | +| kimi-k2-thinking | ¥4.00 | ¥16.00 | ¥1.00 | +| kimi-k2 | ¥4.00 | ¥16.00 | ¥1.00 | +| kimi-k2-turbo | ¥8.00 | ¥58.00 | ¥1.00 | +| **MiniMax** | | | | +| minimax-m2.1 | ¥2.10 | ¥8.40 | ¥0.21 | +| minimax-m2.1-lightning | ¥2.10 | ¥16.80 | ¥0.21 | +| **Others** | | | | +| glm-4.7 | ¥2.00 | ¥8.00 | ¥0.40 | +| doubao-seed-code | ¥1.20 | ¥8.00 | ¥0.24 | +| mimo-v2-flash | Free | Free | - | + +### Custom Prices + +If using proxy services, prices may differ: + +1. Click the "Edit" button +2. Modify prices +3. Save + +## FAQ + +### Statistics Data Is Empty + +Check: +- Is the proxy service running +- Is app takeover enabled +- Is log recording enabled +- Have requests been going through the proxy + +### Cost Estimates Are Inaccurate + +Possible causes: +- Pricing configuration doesn't match actual prices +- Using a proxy service with special pricing + +Solutions: +- Update pricing configuration +- Refer to the provider's actual invoices + +### Token Count Differs from Provider + +CC Switch uses its own method to estimate token counts, which may slightly differ from the provider's calculation. Refer to the provider's invoice for authoritative numbers. diff --git a/docs/user-manual/en/4-proxy/4.5-model-test.md b/docs/user-manual/en/4-proxy/4.5-model-test.md new file mode 100644 index 000000000..144522ffa --- /dev/null +++ b/docs/user-manual/en/4-proxy/4.5-model-test.md @@ -0,0 +1,156 @@ +# 4.5 Model Test + +## Overview + +The model test feature verifies whether a provider's configured model is available by sending actual API requests to test: + +- Whether the model exists +- Whether the API Key is valid +- Whether the endpoint responds normally +- Whether the response latency is acceptable + +## Open Configuration + +Settings > Advanced > Model Test Config + +## Test Model Configuration + +Configure the model used for testing per application: + +| Application | Setting | Default | Notes | +|-------------|---------|---------|-------| +| Claude | Claude Model | System default | Recommend using Haiku series (low cost, fast) | +| Codex | Codex Model | System default | Recommend using mini series | +| Gemini | Gemini Model | System default | Recommend using Flash series | + +### Model Selection Tips + +When choosing a test model, consider: + +1. **Cost**: Choose lower-priced models (e.g., Haiku, Mini, Flash) +2. **Speed**: Choose fast-responding models +3. **Availability**: Choose models supported by the provider + +## Test Parameter Configuration + +### Timeout + +| Parameter | Description | Default | Range | +|-----------|-------------|---------|-------| +| Timeout | Single request timeout | 45 seconds | 10-120 seconds | + +Setting it too short may cause false negatives; too long delays fault detection. + +### Retries + +| Parameter | Description | Default | Range | +|-----------|-------------|---------|-------| +| Max Retries | Retries after failure | 2 times | 0-5 times | + +Increase retries when the network is unstable. + +### Degradation Threshold + +| Parameter | Description | Default | Range | +|-----------|-------------|---------|-------| +| Degradation Threshold | Responses exceeding this time are marked as degraded | 6000ms | 1000-30000ms | + +Providers exceeding the threshold are marked as "degraded" but remain usable. + +## Execute Model Test + +### Manual Test + +Click the "Test" button on the provider card: + +1. Sends a test request to the configured endpoint +2. Uses the configured test model +3. Waits for response or timeout +4. Displays the test result + +### Test Content + +The test request: +- Sends a short prompt (e.g., "Hi") +- Limits maximum output tokens (typically 10-50) +- Uses streaming response to detect time to first byte + +## Test Results + +### Health Status + +| Status | Icon | Description | +|--------|------|-------------| +| Healthy | Green | Normal response, latency within threshold | +| Degraded | Yellow | Normal response, but latency exceeds threshold | +| Unavailable | Red | Request failed or timed out | + +### Result Information + +After testing completes, displays: +- Response latency (milliseconds) +- Time to first byte (TTFB) +- Error message (if failed) + +## Integration with Failover + +Model testing works in conjunction with the failover feature: + +### Health Checks + +After enabling the proxy service, the system periodically performs health checks on providers in the failover queue: + +1. Sends a request using the configured test model +2. Updates health status based on the response +3. Unhealthy providers are temporarily skipped + +### Circuit Breaker Recovery + +When a provider recovers from a circuit-broken state: + +1. Performs a model test to verify availability +2. If the test passes, normal status is restored +3. If the test fails, the circuit breaker remains active + +## FAQ + +### Test Fails But Actually Available + +**Possible causes**: +- The test model differs from the actually used model +- The provider doesn't support the configured test model + +**Solutions**: +- Change the test model to one supported by the provider +- Check the provider's model list + +### High Latency + +**Possible causes**: +- Network latency +- High server load on the provider +- Slow model response + +**Solutions**: +- Use a faster test model +- Adjust the degradation threshold +- Consider using mirror endpoints + +### Frequent Timeouts + +**Possible causes**: +- Timeout set too short +- Unstable network +- Unstable provider service + +**Solutions**: +- Increase the timeout +- Increase retry count +- Check network connection + +## Notes + +- Model testing consumes a small amount of API quota +- Recommend using low-cost models for testing +- Testing frequency should not be too high to avoid wasting quota +- Different providers may support different models diff --git a/docs/user-manual/en/5-faq/5.1-config-files.md b/docs/user-manual/en/5-faq/5.1-config-files.md new file mode 100644 index 000000000..4ca20f3f8 --- /dev/null +++ b/docs/user-manual/en/5-faq/5.1-config-files.md @@ -0,0 +1,340 @@ +# 5.1 Configuration Files + +## CC Switch Data Storage + +### Storage Directory + +Default location: `~/.cc-switch/` + +Customizable location in settings (for cloud sync). + +### Directory Structure + +``` +~/.cc-switch/ +├── cc-switch.db # SQLite database +├── settings.json # Device-level settings +└── backups/ # Automatic backups + ├── backup-20251230-120000.json + ├── backup-20251229-180000.json + └── ... +``` + +### Database Contents + +`cc-switch.db` is a SQLite database that stores: + +| Table | Contents | +|-------|----------| +| providers | Provider configurations | +| provider_endpoints | Provider endpoint candidate list | +| mcp_servers | MCP server configurations | +| prompts | Prompt presets | +| skills | Skill installation status | +| skill_repos | Skill repository configurations | +| proxy_config | Proxy configuration | +| proxy_request_logs | Proxy request logs | +| provider_health | Provider health status | +| model_pricing | Model pricing | +| settings | App settings | + +### Device Settings + +`settings.json` stores device-level settings: + +```json +{ + "language": "zh", + "theme": "system", + "windowBehavior": "minimize", + "autoStart": false, + "claudeConfigDir": null, + "codexConfigDir": null, + "geminiConfigDir": null, + "opencodeConfigDir": null, + "openclawConfigDir": null +} +``` + +These settings are not synced across devices. + +### Automatic Backups + +The `backups/` directory stores automatic backups: + +- Automatically created before each configuration import +- Retains the most recent 10 backups +- File names include timestamps + +## Claude Code Configuration + +### Configuration Directory + +Default: `~/.claude/` + +### Key Files + +``` +~/.claude/ +├── settings.json # Main configuration file +├── CLAUDE.md # System prompt +└── skills/ # Skills directory + └── ... +``` + +### settings.json + +```json +{ + "env": { + "ANTHROPIC_API_KEY": "sk-xxx", + "ANTHROPIC_BASE_URL": "https://api.anthropic.com" + }, + "permissions": { + "allow_file_access": true + } +} +``` + +| Field | Description | +|-------|-------------| +| `env.ANTHROPIC_API_KEY` | API key | +| `env.ANTHROPIC_BASE_URL` | API endpoint (optional) | +| `env.ANTHROPIC_AUTH_TOKEN` | Alternative authentication method | + +### MCP Configuration + +MCP server configuration is in `~/.claude.json`: + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +## Codex Configuration + +### Configuration Directory + +Default: `~/.codex/` + +### Key Files + +``` +~/.codex/ +├── auth.json # Authentication configuration +├── config.toml # Main configuration + MCP +└── AGENTS.md # System prompt +``` + +### auth.json + +```json +{ + "OPENAI_API_KEY": "sk-xxx" +} +``` + +### config.toml + +```toml +# Basic configuration +base_url = "https://api.openai.com/v1" +model = "gpt-4" + +# MCP servers +[mcp_servers.mcp-fetch] +command = "uvx" +args = ["mcp-server-fetch"] +``` + +## Gemini CLI Configuration + +### Configuration Directory + +Default: `~/.gemini/` + +### Key Files + +``` +~/.gemini/ +├── .env # Environment variables (API Key) +├── settings.json # Main configuration + MCP +└── GEMINI.md # System prompt +``` + +### .env + +```bash +GEMINI_API_KEY=xxx +GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com +GEMINI_MODEL=gemini-pro +``` + +### settings.json + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +| Field | Description | +|-------|-------------| +| `mcpServers` | MCP server configuration | + +## OpenCode Configuration + +### Configuration Directory + +Default: `~/.opencode/` + +### Key Files + +``` +~/.opencode/ +├── config.json # Main configuration file +├── AGENTS.md # System prompt +└── skills/ # Skills directory + └── ... +``` + +## OpenClaw Configuration + +### Configuration Directory + +Default: `~/.openclaw/` + +### Key Files + +``` +~/.openclaw/ +├── openclaw.json # Main configuration file (JSON5 format) +├── AGENTS.md # System prompt +└── skills/ # Skills directory + └── ... +``` + +### openclaw.json + +OpenClaw uses a JSON5 format configuration file with the following main sections: + +```json5 +{ + // Model provider configuration + models: { + mode: "merge", + providers: { + "custom-provider": { + baseUrl: "https://api.example.com/v1", + apiKey: "your-api-key", + api: "openai-completions", + models: [{ id: "model-id", name: "Model Name" }] + } + } + }, + // Environment variables + env: { + ANTHROPIC_API_KEY: "sk-..." + }, + // Agent default model configuration + agents: { + defaults: { + model: { + primary: "provider/model" + } + } + }, + // Tool configuration + tools: {}, + // Workspace file configuration + workspace: {} +} +``` + +| Field | Description | +|-------|-------------| +| `models.providers` | Provider configuration (mapped to CC Switch's "providers") | +| `env` | Environment variable configuration | +| `agents.defaults` | Agent default model settings | +| `tools` | Tool configuration | +| `workspace` | Workspace file management | + +## Configuration Priority + +CC Switch's priority when modifying configurations: + +1. **CC Switch Database** - Single source of truth (SSOT) +2. **Live Configuration Files** - Written when switching providers +3. **Backfill Mechanism** - Reads from live files when editing the current provider + +## Manual Configuration Editing + +### Safe to Edit Manually + +- CLI tool configuration files (will be backfilled by CC Switch) +- CC Switch's `settings.json` + +### Not Recommended to Edit Manually + +- `cc-switch.db` database file +- Backup files + +### Sync After Editing + +If you manually edit CLI tool configurations: + +1. Open CC Switch +2. Edit the corresponding provider +3. You will see the manual changes have been backfilled +4. Save to sync to the database + +## Configuration Migration + +### Migrating from Older Versions + +CC Switch v3.7.0 migrated from JSON files to SQLite: + +- Automatic migration on first launch +- Displays a notification upon successful migration +- Old configuration files are retained as backups + +### Cross-device Migration + +1. Export configuration on the source device +2. Import configuration on the target device +3. Or use the cloud sync feature + +## Configuration Backup Recommendations + +### Regular Backups + +It is recommended to regularly export configurations: + +1. Settings > Advanced > Data Management +2. Click "Export" +3. Save to a secure location + +### Backup Contents + +The export file includes: + +- All provider configurations +- MCP server configurations +- Prompt presets +- App settings + +### Not Included + +- Usage logs (large data volume) +- Device-level settings (not suitable for cross-device) diff --git a/docs/user-manual/en/5-faq/5.2-questions.md b/docs/user-manual/en/5-faq/5.2-questions.md new file mode 100644 index 000000000..a0ad6b988 --- /dev/null +++ b/docs/user-manual/en/5-faq/5.2-questions.md @@ -0,0 +1,220 @@ +# 5.2 Frequently Asked Questions + +## Installation Issues + +### macOS Shows "Unidentified Developer" + +**Problem**: First launch shows "Cannot open because it is from an unidentified developer" + +**Solution 1**: Via System Settings +1. Close the warning dialog +2. Open "System Settings" > "Privacy & Security" +3. Find the CC Switch prompt +4. Click "Open Anyway" +5. Reopen the app + +**Solution 2**: Via Terminal command (recommended) +```bash +sudo xattr -dr com.apple.quarantine /Applications/CC\ Switch.app/ +``` + +The app can be opened normally after running this command. + +### Windows: App Doesn't Launch After Installation + +**Possible causes**: +- Missing WebView2 runtime +- Antivirus software blocking + +**Solutions**: +1. Install [Microsoft Edge WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) +2. Add CC Switch to your antivirus software's whitelist + +### Linux: Startup Error + +**Problem**: AppImage won't start + +**Solution**: +```bash +# Add execute permission +chmod +x CC-Switch-*.AppImage + +# If it still fails, try +./CC-Switch-*.AppImage --no-sandbox +``` + +## Provider Issues + +### Provider Switch Doesn't Take Effect + +**Cause**: The CLI tool needs to reload its configuration + +**Solutions**: +- Claude Code: Close and reopen the terminal, or restart the IDE +- Codex: Close and reopen the terminal +- Gemini: Tray switching takes effect immediately, no restart needed + +### API Key Invalid + +**Troubleshooting steps**: +1. Confirm the API Key is copied correctly (no extra spaces) +2. Confirm the API Key hasn't expired +3. Confirm the endpoint URL is correct +4. Use the speed test to verify connectivity + +### How to Restore Official Login + +**Steps**: +1. Select the "Official Login" preset (Claude/Codex) or "Google Official" preset (Gemini) +2. Click "Enable" +3. Restart the corresponding CLI tool +4. Follow the CLI tool's login flow + +## Proxy Issues + +### Proxy Service Fails to Start + +**Possible cause**: Port is occupied + +**Solution**: +1. Check port usage: + ```bash + # macOS/Linux + lsof -i :49152 + + # Windows + netstat -ano | findstr :49152 + ``` +2. Close the program occupying the port +3. Or try modifying the configuration to restore the default port: + - Open "Settings > Proxy Service" + - Click the "Reset to Default" button + +### Request Timeout in Proxy Mode + +**Possible causes**: +- Network issues +- Provider server issues +- Incorrect proxy configuration + +**Solutions**: +1. Check network connection +2. Try accessing the provider API directly (disable proxy) +3. Check if provider configuration is correct + +### Configuration Not Restored After Disabling Proxy + +**Possible cause**: Proxy exited abnormally + +**Solution**: +1. Edit the current provider +2. Check if the endpoint URL is correct +3. Save to update the configuration + +## Failover Issues + +### Failover Not Triggering + +**Checklist**: +- [ ] Is the proxy service running +- [ ] Is app takeover enabled +- [ ] Is auto failover enabled +- [ ] Are there backup providers in the queue + +### Failover Triggering Too Frequently + +**Possible causes**: +- Unstable primary provider +- Circuit breaker threshold set too low + +**Solutions**: +1. Check primary provider status +2. Increase the failure threshold (e.g., from 3 to 5) +3. Consider changing the primary provider + +### All Providers Are Circuit-Broken + +**Solutions**: +1. Wait for the recovery wait time to expire (default 60 seconds) +2. Or restart the proxy service to reset states + +## Data Issues + +### Configuration Lost + +**Possible causes**: +- Configuration directory was deleted +- Database corruption + +**Solutions**: +1. Check if the `~/.cc-switch/` directory exists +2. Restore from backup: `~/.cc-switch/backups/` +3. Or import from a previously exported configuration file + +### Import Configuration Failed + +**Possible causes**: +- Incorrect file format +- Version incompatibility + +**Solutions**: +1. Confirm the file was exported by CC Switch +2. Check if the file content is complete +3. Try opening with a text editor to check format + +### Usage Statistics Data Is Empty + +**Checklist**: +- [ ] Is the proxy service running +- [ ] Is app takeover enabled +- [ ] Is log recording enabled +- [ ] Have requests been going through the proxy + +## Other Issues + +### Tray Icon Not Showing + +**macOS**: +- Check menu bar icon settings in System Settings + +**Windows**: +- Check taskbar settings to ensure the CC Switch icon is not hidden + +**Linux**: +- System tray support may need to be installed (e.g., `libappindicator`) + +### UI Display Issues + +**Solutions**: +1. Try switching themes (light/dark) +2. Restart the app +3. Delete `~/.cc-switch/settings.json` to reset settings + +### Update Failed + +**Solutions**: +1. Check network connection +2. Manually download and install the latest version +3. If using Homebrew: `brew upgrade --cask cc-switch` + +## Getting Help + +### Submit an Issue + +If none of the above solutions work: + +1. Visit [GitHub Issues](https://github.com/farion1231/cc-switch/issues) +2. Search for similar issues +3. If none found, create a new Issue +4. Provide the following information: + - Operating system and version + - CC Switch version + - Problem description and reproduction steps + - Error messages (if any) + +### Log Files + +Attach log files when submitting an Issue: + +- macOS/Linux: `~/.cc-switch/logs/` +- Windows: `%APPDATA%\cc-switch\logs\` diff --git a/docs/user-manual/en/5-faq/5.3-deeplink.md b/docs/user-manual/en/5-faq/5.3-deeplink.md new file mode 100644 index 000000000..d42b981af --- /dev/null +++ b/docs/user-manual/en/5-faq/5.3-deeplink.md @@ -0,0 +1,256 @@ +# 5.3 Deep Link Protocol + +## Overview + +CC Switch supports the `ccswitch://` deep link protocol, enabling one-click configuration import via links. + +**Use cases**: +- Team configuration sharing +- One-click setup in tutorials +- Quick sync across devices + +## Online Generator Tool + +CC Switch provides an online deep link generator tool: + +**URL**: [https://farion1231.github.io/cc-switch/deplink.html](https://farion1231.github.io/cc-switch/deplink.html) + +### How to Use + +1. Open the above URL +2. Select the import type (Provider/MCP/Prompt) +3. Fill in the configuration information +4. Click "Generate Link" +5. Copy the generated deep link +6. Share with others or use on other devices + +## Protocol Format + +### V1 Protocol + +Uses URL parameter format, easy to read and generate: + +``` +ccswitch://v1/import?resource={type}&app={app}&name={name}&... +``` + +**Common parameters**: + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `resource` | Yes | Resource type: `provider` / `mcp` / `prompt` / `skill` | +| `app` | Yes | App type: `claude` / `codex` / `gemini` / `opencode` / `openclaw` | +| `name` | Yes | Name | + +**Provider parameters** (resource=provider): + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `endpoint` | No | API endpoint URL (supports comma-separated multiple URLs) | +| `apiKey` | No | API key | +| `homepage` | No | Provider website | +| `model` | No | Default model | +| `haikuModel` | No | Haiku model (Claude only) | +| `sonnetModel` | No | Sonnet model (Claude only) | +| `opusModel` | No | Opus model (Claude only) | +| `notes` | No | Notes | +| `icon` | No | Icon | +| `config` | No | Base64-encoded configuration content | +| `configFormat` | No | Configuration format: `json` / `toml` | +| `configUrl` | No | Remote configuration URL | +| `enabled` | No | Whether to enable (boolean) | +| `usageScript` | No | Usage query script | +| `usageEnabled` | No | Whether to enable usage query (default true) | +| `usageApiKey` | No | Usage query API Key | +| `usageBaseUrl` | No | Usage query base URL | +| `usageAccessToken` | No | Usage query access token | +| `usageUserId` | No | Usage query user ID | +| `usageAutoInterval` | No | Auto query interval (minutes) | + +**Prompt parameters** (resource=prompt): + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `content` | Yes | Prompt content | +| `description` | No | Description | +| `enabled` | No | Whether to enable (boolean) | + +**MCP parameters** (resource=mcp): + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `apps` | Yes | App list (comma-separated, e.g., `claude,codex,gemini,opencode`) | +| `config` | Yes | MCP server configuration (JSON format) | +| `enabled` | No | Whether to enable (boolean) | + +**Skill parameters** (resource=skill): + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `repo` | Yes | Repository (format: `owner/name`) | +| `directory` | No | Directory path | +| `branch` | No | Git branch | + +**Example**: +``` +ccswitch://v1/import?resource=provider&app=claude&name=My%20Provider&endpoint=https%3A%2F%2Fapi.example.com&apiKey=sk-xxx +``` + +## Import Type Examples + +### Import Provider + +``` +ccswitch://v1/import?resource=provider&app=claude&name=My%20Provider&endpoint=https%3A%2F%2Fapi.example.com&apiKey=sk-xxx +``` + +### Import MCP Server + +``` +ccswitch://v1/import?resource=mcp&apps=claude,codex&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D&name=mcp-fetch +``` + +### Import Prompt Preset + +``` +ccswitch://v1/import?resource=prompt&app=claude&name=%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5&content=%23%20%E8%A7%92%E8%89%B2%0A%E4%BD%A0%E6%98%AF%E4%B8%80%E4%B8%AA%E4%B8%93%E4%B8%9A%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E4%B8%93%E5%AE%B6 +``` + +### Import Skill + +``` +ccswitch://v1/import?resource=skill&name=my-skill&repo=owner/repo&directory=skills/my-skill&branch=main +``` + +## Generate Deep Links + +### Manual Generation + +1. Prepare parameters +2. Assemble the URL following V1 protocol format +3. URL-encode special characters + +**Example**: + +```javascript +const params = new URLSearchParams({ + resource: 'provider', + app: 'claude', + name: 'My Provider', + endpoint: 'https://api.example.com', + apiKey: 'sk-xxx' +}); + +const url = `ccswitch://v1/import?${params.toString()}`; +``` + +### Online Tool + +Using CC Switch's official online deep link generator tool is more convenient. + +## Using Deep Links + +### Click the Link + +Click a deep link in a browser or other application: + +1. The system asks whether to open CC Switch +2. After confirming, CC Switch opens +3. An import confirmation dialog is displayed +4. Confirm the import + +### Import Confirmation + +A confirmation dialog is shown before import, containing: + +- Import type +- Configuration preview +- Confirm/Cancel buttons + +**Security tip**: Only import configurations from trusted sources. + +## Protocol Registration + +### Automatic Registration + +CC Switch automatically registers the `ccswitch://` protocol during installation. + +### Manual Registration + +If the protocol is not registered correctly: + +**macOS**: +Reinstall the app, or run: +```bash +/usr/bin/open -a "CC Switch" --args --register-protocol +``` + +**Windows**: +Reinstall the app, or check the registry: +``` +HKEY_CLASSES_ROOT\ccswitch +``` + +**Linux**: +Check the `MimeType` configuration in the `.desktop` file. + +## Security Considerations + +### Sensitive Information + +Deep links may contain sensitive information (e.g., API Keys): + +- Do not share links containing API Keys in public +- Remove or replace sensitive information before sharing +- Use secure channels to transmit links + +### Source Verification + +Before import, CC Switch will: + +1. Validate the data format +2. Display a configuration preview +3. Require user confirmation + +### Malicious Link Protection + +CC Switch checks: + +- Whether the data format is valid +- Whether required fields are complete +- Whether configuration values are within reasonable ranges + +## Example Links + +### Example: Import Claude Provider + +``` +ccswitch://v1/import?resource=provider&app=claude&name=Test%20Provider&apiKey=sk-xxx&endpoint=https%3A%2F%2Fapi.example.com +``` + +### Example: Import MCP Server + +``` +ccswitch://v1/import?resource=mcp&name=mcp-fetch&apps=claude,codex,gemini&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D +``` + +## Troubleshooting + +### Link Won't Open + +**Check**: +1. Is CC Switch installed +2. Is the protocol registered correctly +3. Is the link format correct + +### Import Failed + +**Possible causes**: +- Base64 encoding error +- JSON format error +- Missing required fields + +**Solutions**: +1. Check the original JSON format +2. Re-encode in Base64 +3. Ensure all required fields are present diff --git a/docs/user-manual/en/5-faq/5.4-env-conflict.md b/docs/user-manual/en/5-faq/5.4-env-conflict.md new file mode 100644 index 000000000..61e8b5a0f --- /dev/null +++ b/docs/user-manual/en/5-faq/5.4-env-conflict.md @@ -0,0 +1,108 @@ +# 5.4 Environment Variable Conflicts + +## Overview + +CC Switch automatically detects conflicts between system environment variables and app configurations, preventing configurations from being unexpectedly overridden. + +**Detected environment variables**: +- `ANTHROPIC_API_KEY` - Claude API key +- `ANTHROPIC_BASE_URL` - Claude API endpoint +- `OPENAI_API_KEY` - OpenAI API key +- `GEMINI_API_KEY` - Gemini API key +- Other related environment variables + +## Conflict Warning + +When a conflict is detected, a yellow warning banner appears at the top of the interface: + +``` +Warning: Environment variable conflict detected +Found X environment variables that may conflict with CC Switch configuration +[Expand] [Dismiss] +``` + +## View Conflict Details + +Click the "Expand" button to view detailed information: + +| Field | Description | +|-------|-------------| +| Variable Name | Environment variable name | +| Variable Value | Currently set value | +| Source | Where the variable originates from | + +### Source Types + +| Source | Description | +|--------|-------------| +| User Registry | Windows user-level environment variable | +| System Registry | Windows system-level environment variable | +| Shell Configuration | macOS/Linux shell configuration file | +| System Environment | System-level environment variable | + +## Resolve Conflicts + +### Select Variables to Remove + +1. Check the environment variables you want to remove +2. Or click "Select All" to select all conflicting variables + +### Remove Variables + +1. Click the "Remove Selected" button +2. Confirm the removal operation +3. CC Switch will automatically back up and remove the selected variables + +### Automatic Backup + +A backup is automatically created before removal: + +- Backup location: `~/.cc-switch/env-backups/` +- Backup format: JSON file +- Includes variable name, value, source, and other information + +## Dismiss Warning + +If you confirm the conflict does not affect usage, you can: + +1. Click the "Dismiss" button on the right side of the warning banner +2. The warning will be temporarily hidden +3. Detection will run again on next launch + +## Manual Resolution + +If you prefer not to use CC Switch to remove variables, you can handle them manually: + +### Windows + +1. Open "System Properties > Advanced > Environment Variables" +2. Find the conflicting variable in User or System variables +3. Delete or modify the variable + +### macOS / Linux + +1. Edit the shell configuration file (e.g., `~/.zshrc`, `~/.bashrc`) +2. Delete or comment out the relevant `export` statements +3. Reload the configuration: `source ~/.zshrc` + +## Why Do Conflicts Occur + +Environment variables typically take priority over configuration files, which may cause: + +- CC Switch provider configurations being overridden +- API requests being sent to the wrong endpoint +- Using the wrong API key + +## Best Practices + +1. **Use CC Switch to manage configurations**: Avoid setting API keys in system environment variables +2. **Check regularly**: Pay attention to conflict warnings and address them promptly +3. **Back up important variables**: Confirm backups exist before removal + +## Restore Deleted Variables + +If you accidentally deleted environment variables: + +1. Find the backup file: `~/.cc-switch/env-backups/` +2. Open the corresponding JSON file +3. Manually restore the variable to the system environment diff --git a/docs/user-manual/en/README.md b/docs/user-manual/en/README.md new file mode 100644 index 000000000..1b19fe57d --- /dev/null +++ b/docs/user-manual/en/README.md @@ -0,0 +1,111 @@ +# CC Switch User Manual + +> All-in-One Assistant for Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw + +## Table of Contents + +``` +CC Switch User Manual +│ +├── 1. Getting Started +│ ├── 1.1 Introduction +│ ├── 1.2 Installation Guide +│ ├── 1.3 Interface Overview +│ ├── 1.4 Quick Start +│ └── 1.5 Personalization +│ +├── 2. Provider Management +│ ├── 2.1 Add Provider +│ ├── 2.2 Switch Provider +│ ├── 2.3 Edit Provider +│ ├── 2.4 Sort & Duplicate +│ └── 2.5 Usage Query +│ +├── 3. Extensions +│ ├── 3.1 MCP Server Management +│ ├── 3.2 Prompts Management +│ └── 3.3 Skills Management +│ +├── 4. Proxy & High Availability +│ ├── 4.1 Proxy Service +│ ├── 4.2 App Takeover +│ ├── 4.3 Failover +│ ├── 4.4 Usage Statistics +│ └── 4.5 Model Test +│ +└── 5. FAQ + ├── 5.1 Configuration Files + ├── 5.2 FAQ + ├── 5.3 Deep Link Protocol + └── 5.4 Environment Variable Conflicts +``` + +## File List + +### 1. Getting Started + +| File | Description | +|------|-------------| +| [1.1-introduction.md](./1-getting-started/1.1-introduction.md) | Introduction, core features, supported platforms | +| [1.2-installation.md](./1-getting-started/1.2-installation.md) | Windows/macOS/Linux installation guide | +| [1.3-interface.md](./1-getting-started/1.3-interface.md) | Interface layout, navigation bar, provider cards | +| [1.4-quickstart.md](./1-getting-started/1.4-quickstart.md) | 5-minute quick start tutorial | +| [1.5-settings.md](./1-getting-started/1.5-settings.md) | Language, theme, directories, cloud sync settings | + +### 2. Provider Management + +| File | Description | +|------|-------------| +| [2.1-add.md](./2-providers/2.1-add.md) | Using presets, custom configuration, universal providers | +| [2.2-switch.md](./2-providers/2.2-switch.md) | Main UI switching, tray switching, activation methods | +| [2.3-edit.md](./2-providers/2.3-edit.md) | Edit configuration, modify API Key, backfill mechanism | +| [2.4-sort-duplicate.md](./2-providers/2.4-sort-duplicate.md) | Drag-to-reorder, duplicate provider, delete | +| [2.5-usage-query.md](./2-providers/2.5-usage-query.md) | Usage query, remaining balance, multi-plan display | + +### 3. Extensions + +| File | Description | +|------|-------------| +| [3.1-mcp.md](./3-extensions/3.1-mcp.md) | MCP protocol, add servers, app binding | +| [3.2-prompts.md](./3-extensions/3.2-prompts.md) | Create presets, activate/switch, smart backfill | +| [3.3-skills.md](./3-extensions/3.3-skills.md) | Discover skills, install/uninstall, repository management | + +### 4. Proxy & High Availability + +| File | Description | +|------|-------------| +| [4.1-service.md](./4-proxy/4.1-service.md) | Start proxy, configuration, running status | +| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | App takeover, configuration changes, status indicators | +| [4.3-failover.md](./4-proxy/4.3-failover.md) | Failover queue, circuit breaker, health status | +| [4.4-usage.md](./4-proxy/4.4-usage.md) | Usage statistics, trend charts, pricing configuration | +| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | Model test, health check, latency testing | + +### 5. FAQ + +| File | Description | +|------|-------------| +| [5.1-config-files.md](./5-faq/5.1-config-files.md) | CC Switch storage, CLI configuration file formats | +| [5.2-questions.md](./5-faq/5.2-questions.md) | Frequently asked questions | +| [5.3-deeplink.md](./5-faq/5.3-deeplink.md) | Deep link protocol, generation and usage | +| [5.4-env-conflict.md](./5-faq/5.4-env-conflict.md) | Environment variable conflict detection and resolution | + +## Quick Links + +- **New users**: Start with [1.1 Introduction](./1-getting-started/1.1-introduction.md) +- **Installation issues**: See [1.2 Installation Guide](./1-getting-started/1.2-installation.md) +- **Configure providers**: See [2.1 Add Provider](./2-providers/2.1-add.md) +- **Using proxy**: See [4.1 Proxy Service](./4-proxy/4.1-service.md) +- **Having trouble**: See [5.2 FAQ](./5-faq/5.2-questions.md) + +## Version Information + +- Documentation version: v3.11.1 +- Last updated: 2026-03-02 +- Applicable to CC Switch v3.11.1+ + +## Contributing + +Feel free to submit Issues or PRs to improve the documentation: + +- [GitHub Issues](https://github.com/farion1231/cc-switch/issues) +- [GitHub Repository](https://github.com/farion1231/cc-switch) diff --git a/docs/user-manual/ja/1-getting-started/1.1-introduction.md b/docs/user-manual/ja/1-getting-started/1.1-introduction.md new file mode 100644 index 000000000..a86241003 --- /dev/null +++ b/docs/user-manual/ja/1-getting-started/1.1-introduction.md @@ -0,0 +1,65 @@ +# 1.1 ソフトウェア紹介 + +## CC Switch とは + +CC Switch はクロスプラットフォームのデスクトップアプリケーションで、AI プログラミングツールを使用する開発者向けに設計されています。**Claude Code**、**Codex**、**Gemini CLI**、**OpenCode**、**OpenClaw** の 5 つの AI プログラミングツールの設定を統一的に管理できます。 + +## どのような問題を解決するか + +日常の開発で、以下のような課題に直面することがあります: + +- **複数プロバイダーの切り替えが面倒**:異なる API プロバイダー(公式、中継サービスなど)を使用する際、設定ファイルを手動で変更する必要がある +- **設定が分散して管理しづらい**:Claude、Codex、Gemini、OpenCode、OpenClaw がそれぞれ独立した設定ファイルを持ち、フォーマットも異なる +- **使用量を監視できない**:API をどれだけ呼び出したか、いくらかかったかが分からない +- **サービスが不安定**:単一プロバイダーに問題が発生すると、ワークフロー全体が中断する + +CC Switch は統一されたインターフェースでこれらの問題を解決します。 + +## 主要機能 + +### プロバイダー管理 +- ワンクリックで複数の API プロバイダー設定を切り替え +- プリセットテンプレートで一般的なプロバイダーを素早く追加 +- 統一プロバイダー機能で、アプリ間で設定を共有 +- 使用量クエリと残額表示 +- エンドポイント速度テスト + +### 拡張機能 +- **MCP サーバー**:Model Context Protocol サーバーを管理し、AI の機能を拡張 +- **Prompts**:システムプロンプトのプリセットを管理し、さまざまなシーンで素早く切り替え +- **Skills**:スキル拡張のインストールと管理 + +### プロキシと高可用性 +- ローカルプロキシサービスで、リクエストログと使用量統計を記録 +- 自動フェイルオーバー、メインプロバイダーの障害時にバックアップへ自動切り替え +- サーキットブレーカー機能で、障害プロバイダーへの頻繁なリトライを防止 +- 詳細な Token 使用量トラッキングとコスト見積もり + +## 対応アプリケーション + +| アプリ | 説明 | +|------|------| +| **Claude Code** | Anthropic 公式の AI プログラミングアシスタント | +| **Codex** | OpenAI のコード生成ツール | +| **Gemini CLI** | Google の AI コマンドラインツール | +| **OpenCode** | オープンソース AI プログラミングターミナルツール | +| **OpenClaw** | オープンソース AI プログラミングアシスタント(マルチプロバイダーゲートウェイ) | + +## 対応プラットフォーム + +- **Windows** 10 以上 +- **macOS** 10.15 (Catalina) 以上 +- **Linux** Ubuntu 22.04+ / Debian 11+ / Fedora 34+ + +## 技術アーキテクチャ + +CC Switch はモダンな技術スタックで構築されています: + +- **フロントエンド**:React 18 + TypeScript + Tailwind CSS +- **バックエンド**:Tauri 2 + Rust +- **データストレージ**:SQLite(プロバイダー、MCP、Prompts)+ JSON(デバイス設定) + +このアーキテクチャにより: +- クロスプラットフォームでの一貫した体験 +- ネイティブレベルのパフォーマンス +- 安全なローカルデータストレージ diff --git a/docs/user-manual/ja/1-getting-started/1.2-installation.md b/docs/user-manual/ja/1-getting-started/1.2-installation.md new file mode 100644 index 000000000..0911150f0 --- /dev/null +++ b/docs/user-manual/ja/1-getting-started/1.2-installation.md @@ -0,0 +1,229 @@ +# 1.2 インストールガイド + +## 前提条件 + +### Node.js のインストール + +CC Switch が管理する CLI ツール(Claude Code、Codex、Gemini CLI)には Node.js 環境が必要です。 + +**推奨バージョン**:Node.js 18 LTS 以上 + +#### Windows + +1. [Node.js 公式サイト](https://nodejs.org/) にアクセス + +2. LTS バージョンのインストーラーをダウンロード + +3. インストーラーを実行し、指示に従ってインストール + +4. インストールの確認: + + ```bash + node --version + npm --version + ``` + +#### macOS + +```bash +# Homebrew でインストール +brew install node + +# または nvm を使用(推奨) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +nvm install --lts +``` + +#### Linux + +```bash +# Ubuntu/Debian +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt-get install -y nodejs + +# または nvm を使用 +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +nvm install --lts +``` + +### CLI ツールのインストール + +#### Claude Code + +**方法 1:Homebrew(macOS 推奨)** + +```bash +brew install claude-code +``` + +**方法 2:npm** + +```bash +npm install -g @anthropic-ai/claude-code +``` + +#### Codex + +**方法 1:Homebrew(macOS 推奨)** + +```bash +brew install codex +``` + +**方法 2:npm** + +```bash +npm install -g @openai/codex +``` + +#### Gemini CLI + +**方法 1:Homebrew(macOS 推奨)** + +```bash +brew install gemini-cli +``` + +**方法 2:npm** + +```bash +npm install -g @google/gemini-cli +``` + +--- + +## Windows + +### インストーラー方式 + +1. [Releases ページ](https://github.com/farion1231/cc-switch/releases) にアクセス +2. `CC-Switch-v{バージョン}-Windows.msi` をダウンロード +3. インストーラーをダブルクリックして実行 +4. 指示に従ってインストール + +### ポータブル版(インストール不要) + +1. `CC-Switch-v{バージョン}-Windows-Portable.zip` をダウンロード +2. 任意のディレクトリに展開 +3. `CC-Switch.exe` を実行 + +## macOS + +### 方法 1:Homebrew(推奨) + +```bash +# tap を追加 +brew tap farion1231/ccswitch + +# インストール +brew install --cask cc-switch +``` + +最新バージョンに更新: + +```bash +brew upgrade --cask cc-switch +``` + +### 方法 2:手動ダウンロード + +1. `CC-Switch-v{バージョン}-macOS.zip` をダウンロード +2. 展開して `CC Switch.app` を取得 +3. 「アプリケーション」フォルダにドラッグ + +### 初回起動時の警告 + +開発者が Apple 開発者アカウントを持っていないため、初回起動時に「不明な開発者」の警告が表示される場合があります: + +**推奨される解決方法**: +ターミナルで以下のコマンドを実行してください: +```bash +sudo xattr -dr com.apple.quarantine /Applications/CC\ Switch.app/ +``` + +**別の解決方法(システム設定から)**: +1. 警告ダイアログを閉じる +2. 「システム設定」→「プライバシーとセキュリティ」を開く +3. CC Switch に関する表示を見つけ、「このまま開く」をクリック +4. 再度アプリを開くと正常に使用可能 + +## Linux + +### ArchLinux + +AUR ヘルパーを使用してインストール: + +```bash +# paru を使用 +paru -S cc-switch-bin + +# または yay を使用 +yay -S cc-switch-bin +``` + +### Debian / Ubuntu + +1. `CC-Switch-v{バージョン}-Linux.deb` をダウンロード +2. インストール: + +```bash +sudo dpkg -i CC-Switch-v{バージョン}-Linux.deb + +# 依存関係に問題がある場合 +sudo apt-get install -f +``` + +### AppImage(汎用) + +1. `CC-Switch-v{バージョン}-Linux.AppImage` をダウンロード +2. 実行権限を追加: + +```bash +chmod +x CC-Switch-v{バージョン}-Linux.AppImage +``` + +3. 実行: + +```bash +./CC-Switch-v{バージョン}-Linux.AppImage +``` + +## インストールの確認 + +インストール完了後、CC Switch を起動します: + +1. アプリウィンドウが正常に表示される +2. システムトレイに CC Switch のアイコンが表示される +3. Claude / Codex / Gemini の 3 つのアプリを切り替えられる + +## 自動更新 + +CC Switch には自動更新機能が内蔵されています: + +- 起動時に自動で更新を確認 +- 新しいバージョンがある場合、画面に更新通知を表示 +- クリックするとダウンロードしてインストール + +「設定 → バージョン情報」から手動で更新を確認することもできます。 + +## アンインストール + +### Windows + +- 「設定 → アプリ」からアンインストール +- またはインストールディレクトリのアンインストーラーを実行 + +### macOS + +- `CC Switch.app` をゴミ箱に移動 +- オプション:設定ディレクトリ `~/.cc-switch/` を削除 + +### Linux + +```bash +# Debian/Ubuntu +sudo apt remove cc-switch + +# ArchLinux +paru -R cc-switch-bin +``` diff --git a/docs/user-manual/ja/1-getting-started/1.3-interface.md b/docs/user-manual/ja/1-getting-started/1.3-interface.md new file mode 100644 index 000000000..cbd07094f --- /dev/null +++ b/docs/user-manual/ja/1-getting-started/1.3-interface.md @@ -0,0 +1,170 @@ +# 1.3 インターフェース概要 + +## メイン画面のレイアウト + +![image-20260108001629138](../../assets/image-20260108001629138.png) + +## 上部ナビゲーションバー + +| 番号 | 要素 | 機能説明 | +|------|------|----------| +| ① | Logo | クリックで GitHub プロジェクトページにアクセス | +| ② | 設定ボタン | 設定ページを開く(ショートカット `Cmd/Ctrl + ,`) | +| ③ | プロキシスイッチ | ローカルプロキシサービスの起動/停止 | +| ④ | アプリ切り替え | Claude / Codex / Gemini / OpenCode / OpenClaw を切り替え | +| ⑤ | 機能エリア | Skills / Prompts / MCP の入口 | +| ⑥ | 追加ボタン | 新しいプロバイダーを追加 | + +### アプリ切り替え + +ドロップダウンメニューをクリックして、現在管理するアプリを切り替えます: + +- **Claude** - Claude Code の設定を管理 +- **Codex** - Codex の設定を管理 +- **Gemini** - Gemini CLI の設定を管理 +- **OpenCode** - OpenCode の設定を管理 +- **OpenClaw** - OpenClaw の設定を管理 + +切り替え後、プロバイダーリストに対応アプリの設定が表示されます。 + +### 機能エリアボタン + +| ボタン | 機能 | 表示条件 | +|------|------|----------| +| Skills | スキル拡張管理 | 常に表示 | +| Prompts | システムプロンプト管理 | 常に表示 | +| MCP | MCP サーバー管理 | 常に表示 | + +## プロバイダーカード + +各プロバイダーはカード形式で表示されます。左から右へ以下の要素が含まれています: + +### カード要素(左から右) + +| 番号 | 要素 | アイコン | 機能説明 | +|------|------|------|----------| +| ① | ドラッグハンドル | ≡ | 長押しして上下にドラッグしてプロバイダーの順序を調整 | +| ② | プロバイダーアイコン | - | プロバイダーのブランドアイコンを表示、カラーのカスタマイズ可能 | +| ③ | プロバイダー情報 | - | 名前、メモ/エンドポイントアドレス(クリックで公式サイトを開く) | +| ④ | 使用量情報 | - | 残額を表示、複数プランの場合はプラン数を表示 | +| ⑤ | 有効化ボタン | ▶ | 現在使用中のプロバイダーに切り替え | +| ⑥ | 編集ボタン | ✏️ | プロバイダー設定を編集 | +| ⑦ | 複製ボタン | - | プロバイダーを複製(コピーを作成) | +| ⑧ | テストボタン | - | モデルの可用性と応答速度をテスト | +| ⑨ | 使用量クエリ | - | 使用量クエリスクリプトを設定 | +| ⑩ | 削除ボタン | - | プロバイダーを削除(現在有効な場合は無効) | + +> **ヒント**:操作ボタンエリア(⑤-⑩)はマウスホバー時に表示され、通常は非表示で画面をすっきり保ちます。 + +### ボタンの詳細説明 + +| ボタン | 状態変化 | 説明 | +|------|----------|------| +| **有効化** | 有効化済みの場合は ✓ を表示して無効化 | フェイルオーバーモードでは「参加/参加済み」に変化 | +| **編集** | 常に使用可能 | 編集パネルを開いて設定を変更 | +| **複製** | 常に使用可能 | プロバイダーのコピーを作成、名前に `copy` が付加 | +| **テスト** | テスト中はローディングアニメーション | プロキシサービス実行中のみ使用可能 | +| **使用量クエリ** | 常に使用可能 | カスタム使用量クエリスクリプトを設定 | +| **削除** | 現在有効な場合は半透明で無効 | 先に他のプロバイダーに切り替える必要あり | + +### カードの状態 + +| 状態 | 枠の色 | 説明 | +|------|----------|------| +| **現在有効** | 青い枠 | 通常モードで現在使用中のプロバイダー | +| **プロキシアクティブ** | 緑の枠 | プロキシ接管モードで実際に使用中のプロバイダー | +| **通常状態** | デフォルトの枠 | 有効化されていないプロバイダー | +| **フェイルオーバー中** | 優先度バッジを表示 | P1、P2 などのフェイルオーバー優先度を表示 | + +### ヘルスステータスバッジ + +プロキシモードでは、フェイルオーバーキューに参加しているプロバイダーにヘルスステータスが表示されます: + +| バッジ | 色 | 説明 | +|------|------|------| +| 健康 | 緑 | 連続失敗回数 0 | +| 警告 | 黄 | 連続失敗回数 1-2 | +| 不健康 | 赤 | 連続失敗回数 ≥3、サーキットブレーカーが発動する可能性あり | + + +## システムトレイ + +CC Switch はシステムトレイにアイコンを表示し、クイック操作の入口を提供します。 + +### トレイメニュー構造 + +![image-20260108002153668](../../assets/image-20260108002153668.png) + +### メニュー機能 + +| メニュー項目 | 機能 | +|--------|------| +| メインウィンドウを開く | メインウィンドウを表示してフォーカス | +| アプリグループ | Claude/Codex/Gemini/OpenCode/OpenClaw ごとにプロバイダーを表示 | +| プロバイダーリスト | クリックで切り替え、現在有効なものにはチェックマークを表示 | +| 終了 | アプリを完全に終了 | + +### 多言語対応 + +トレイメニューは 3 つの言語に対応し、設定に応じて自動的に切り替わります: + +| 言語 | メインウィンドウを開く | 終了 | +|------|-----------|------| +| 中文 | 打开主界面 | 退出 | +| English | Open main window | Quit | +| 日本語 | メインウィンドウを開く | 終了 | + +### 使用シーン + +トレイからのプロバイダー切り替えはメイン画面を開く必要がなく、以下の場面に適しています: + +- 頻繁にプロバイダーを切り替える場合 +- メインウィンドウが最小化されているときの素早い操作 +- バックグラウンド実行中の設定管理 + +## 設定ページ + +設定ページは複数のタブに分かれています: + +### 一般タブ + +- 言語設定(中文/English/日本語) +- テーマ設定(システムに合わせる/ライト/ダーク) +- ウィンドウ動作(起動時に自動実行、閉じる動作) + +### 詳細タブ + +- 設定ディレクトリの設定 +- プロキシサービスの設定 +- フェイルオーバーの設定 +- 料金設定 +- データのインポート/エクスポート + +### 使用量タブ + +- リクエスト統計の概要 +- トレンドグラフ +- リクエストログ +- プロバイダー/モデル統計 + +### バージョン情報タブ + +- バージョン情報 +- 更新の確認 +- オープンソースライセンス + +## ショートカットキー + +| ショートカット | 機能 | +|--------|------| +| `Cmd/Ctrl + ,` | 設定を開く | +| `Cmd/Ctrl + F` | プロバイダーを検索 | +| `Esc` | ダイアログ/検索を閉じる | + +## 検索機能 + +`Cmd/Ctrl + F` で検索ボックスを開きます: + +- 名前、メモ、URL で検索可能 +- プロバイダーリストをリアルタイムでフィルタリング +- `Esc` で検索を閉じる diff --git a/docs/user-manual/ja/1-getting-started/1.4-quickstart.md b/docs/user-manual/ja/1-getting-started/1.4-quickstart.md new file mode 100644 index 000000000..024f0e854 --- /dev/null +++ b/docs/user-manual/ja/1-getting-started/1.4-quickstart.md @@ -0,0 +1,92 @@ +# 1.4 クイックスタート + +このセクションでは、5 分で初回設定を完了する方法を説明します。 + +## ステップ 1:プロバイダーの追加 + +1. メイン画面右上の **+** ボタンをクリック +2. 「プリセット」ドロップダウンからプロバイダーを選択 + - よく使われるプリセット:智谱 GLM、MiniMax、DeepSeek、Kimi、PackyCode + - または「カスタム」を選択して手動設定 +3. **API Key** を入力 +4. 「追加」をクリック + +![image-20260108002807657](../../assets/image-20260108002807657.png) + +> **ヒント**:プリセットではエンドポイントアドレスが自動入力されるため、API Key のみ入力すれば使用できます。 + +## ステップ 2:プロバイダーの切り替え + +追加が完了すると、プロバイダーがリストに表示されます。 + +**方法 1:メイン画面で切り替え** +- プロバイダーカードの「有効化」ボタンをクリック + +**方法 2:トレイで素早く切り替え** +- システムトレイアイコンを右クリック +- プロバイダー名を直接クリック + +## ステップ 3:反映方法 + +プロバイダーを切り替えた後、各 CLI ツールの反映方法は異なります: + +| アプリ | 反映方法 | +|------|----------| +| Claude Code | 即時反映(ホットリロード対応) | +| Codex | ターミナルを閉じて再度開く必要あり | +| Gemini | 即時反映(リクエストごとに設定を再読み込み) | + +### Claude Code の初回インストール時の注意 + +Claude Code を初めて起動するときに**ログイン**の要求や初期化ガイドが表示される場合は、CC Switch で「Claude Code の初回確認をスキップ」オプションを有効にしてください: + +1. CC Switch の「設定 → 一般」を開く +2. 「Claude Code の初回確認をスキップ」スイッチをオンにする +3. Claude Code を再起動 + +![image-20260108002626389](../../assets/image-20260108002626389.png) + +> **注意**:このオプションは `~/.claude/settings.json` の `skipIntroduction` フィールドに書き込まれ、公式の初回ガイドフローをスキップします。 + +## 設定の確認 + +再起動後、対応する CLI ツールを起動して簡単な質問でテストします: + +```bash +# Claude Code - 起動後にテスト質問を入力 +claude +> こんにちは、簡単に自己紹介してください + +# Codex - 起動後にテスト質問を入力 +codex +> こんにちは、簡単に自己紹介してください + +# Gemini - 起動後にテスト質問を入力 +gemini +> こんにちは、簡単に自己紹介してください +``` + +AI が正常に回答すれば、設定は成功です。 + +## 次のステップ + +基本設定が完了しました。次に以下のことができます: + +- [プロバイダーの追加](../2-providers/2.1-add.md) - 複数のプロバイダーを設定して簡単に切り替え +- [MCP サーバーの設定](../3-extensions/3.1-mcp.md) - AI ツールの機能を拡張 +- [システムプロンプトの設定](../3-extensions/3.2-prompts.md) - AI の動作をカスタマイズ +- [プロキシサービスの有効化](../4-proxy/4.1-service.md) - 使用量の監視と自動フェイルオーバー + +## よくある質問 + +### 切り替えても反映されない場合 + +ターミナルまたは CLI ツールを再起動してください。設定ファイルは切り替え時に更新されますが、実行中のプログラムは自動的に設定を再読み込みしません。 + +### プリセットが見つからない場合 + +プロバイダーがプリセットリストにない場合は、「カスタム」を選択して手動設定してください。設定形式については [プロバイダーの追加](../2-providers/2.1-add.md) を参照してください。 + +### 公式ログインに戻すには + +「公式ログイン」プリセット(Claude/Codex)または「Google 公式」プリセット(Gemini)を選択し、クライアントを再起動してログインフローに従ってください。 diff --git a/docs/user-manual/ja/1-getting-started/1.5-settings.md b/docs/user-manual/ja/1-getting-started/1.5-settings.md new file mode 100644 index 000000000..2630b6ca2 --- /dev/null +++ b/docs/user-manual/ja/1-getting-started/1.5-settings.md @@ -0,0 +1,255 @@ +# 1.5 個人設定 + +このセクションでは、個人の好みに合わせて CC Switch を設定する方法を説明します。 + +## 設定を開く + +- 左上の **⚙️** ボタンをクリック +- またはショートカット `Cmd/Ctrl + ,` + +## 言語設定 + +CC Switch は 3 つの言語に対応しています: + +| 言語 | 説明 | +| -------- | -------- | +| 簡体中文 | デフォルト言語 | +| English | 英語インターフェース | +| 日本語 | 日本語インターフェース | + +言語を切り替えると即座に反映され、再起動は不要です。 + +## テーマ設定 + +| オプション | 説明 | +| -------- | --------------------------- | +| システムに合わせる | システムのダーク/ライトモードに自動的に合わせる | +| ライト | 常にライトテーマを使用 | +| ダーク | 常にダークテーマを使用 | + +## ウィンドウ動作 + +### 起動時に自動実行 + +有効にすると、システム起動時に CC Switch が自動的に起動します。 + +- **Windows**:レジストリを使用 +- **macOS**:LaunchAgent を使用 +- **Linux**:XDG autostart を使用 + +### 閉じる動作 + +| オプション | 説明 | +| ------------ | ---------------------------- | +| トレイへ最小化 | 閉じるボタンをクリックするとシステムトレイに隠す | +| 直接終了 | 閉じるボタンをクリックするとアプリを完全に終了 | + +トレイからプロバイダーを素早く切り替えられるため、「トレイへ最小化」の使用を推奨します。 + +### Claude プラグイン連携 + +有効にすると、CC Switch はプロバイダー切り替え時に VS Code の Claude Code 拡張に設定を自動同期します(`~/.claude/config.json` の `primaryApiKey` に書き込み)。 + +> **使用シーン**:Claude Code CLI と VS Code プラグインを同時に使用する場合、このオプションを有効にすると両方の設定を一致させることができます。 + +### Claude 初回ガイドのスキップ + +有効にすると、Claude Code の初回ガイドフローをスキップします。Claude Code に慣れているユーザー向けです。 + +> **注意**:このオプションは `~/.claude/settings.json` の `skipIntroduction` フィールドに書き込まれます。 + +### アプリの表示設定 + +アプリ切り替えに表示するアプリを選択します。各アプリを個別にオン/オフできますが、少なくとも 1 つは有効にする必要があります。 + +設定可能なアプリ:Claude、Codex、Gemini、OpenCode、OpenClaw。 + +> **使用シーン**:Claude Code と Codex CLI のみを使用する場合、他のアプリを非表示にしてインターフェースをシンプルに保てます。 + +### Skills 同期方式 + +スキルを各アプリディレクトリにインストールする際の同期方式を設定します: + +| 方式 | 説明 | +| ----------------- | ---------------------------------------------------- | +| シンボリックリンク | スキルのソースファイルへのシンボリックリンクを作成、容量が少なく、リアルタイム同期 | +| ファイルコピー | スキルファイルをターゲットディレクトリに完全コピー | + +> **推奨**:デフォルトではシンボリックリンク方式を使用します。権限の問題が発生する場合は、コピー方式に切り替えてください。 + +### ターミナル設定 + +CC Switch がターミナルを開く際に使用するターミナルアプリケーションを選択します。 + +対応ターミナル(プラットフォーム別): + +| プラットフォーム | ターミナル選択肢 | +| ------- | ------------------------------------------------------------------ | +| macOS | Terminal、iTerm2、Alacritty、Kitty、Ghostty、WezTerm | +| Windows | CMD、PowerShell、Windows Terminal | +| Linux | GNOME Terminal、Konsole、Xfce4 Terminal、Alacritty、Kitty、Ghostty | + +## ディレクトリ設定 + +### アプリ設定ディレクトリ + +CC Switch 自体のデータの保存場所で、デフォルトは `~/.cc-switch/` です。 + +### CLI ツールディレクトリ + +各 CLI ツールの設定ディレクトリをカスタマイズできます: + +| 設定 | デフォルト値 | 説明 | +| ------------- | -------------- | -------------------- | +| Claude ディレクトリ | `~/.claude/` | Claude Code 設定ディレクトリ | +| Codex ディレクトリ | `~/.codex/` | Codex 設定ディレクトリ | +| Gemini ディレクトリ | `~/.gemini/` | Gemini CLI 設定ディレクトリ | +| OpenCode ディレクトリ | `~/.opencode/` | OpenCode 設定ディレクトリ | +| OpenClaw ディレクトリ | `~/.openclaw/` | OpenClaw 設定ディレクトリ | + +> **注意**:ディレクトリを変更した後はアプリの再起動が必要で、対応する CLI ツールも同じディレクトリを設定する必要があります。 + +## データ管理 + +### 設定のエクスポート + +「エクスポート」ボタンをクリックして、以下の内容を含むバックアップファイルを保存します: + +- すべてのプロバイダー設定 +- MCP サーバー設定 +- Prompts プリセット +- アプリ設定 + +バックアップファイルは JSON 形式で、テキストエディタで確認できます。 + +### 設定のインポート + +1. 「ファイルを選択」をクリック +2. 以前にエクスポートしたバックアップファイルを選択 +3. 「インポート」をクリック +4. 既存の設定の上書きを確認 + +> **注意**:インポートは既存の設定を上書きするため、事前に現在の設定をエクスポートしてバックアップすることをお勧めします。 + +## プロキシ設定 + +設定 → プロキシ タブ + +プロキシ タブではすべてのプロキシ関連機能を集中管理します: + +### ローカルプロキシ + +ローカルプロキシサービスの起動/停止、リスニングアドレスとポートの設定。詳しくは [4.1 プロキシサービス](../4-proxy/4.1-service.md) をご覧ください。 + +### フェイルオーバー + +アプリ(Claude/Codex/Gemini)ごとにフェイルオーバーキューと自動切り替え戦略を設定。詳しくは [4.3 フェイルオーバー](../4-proxy/4.3-failover.md) をご覧ください。 + +### 料金補正器 + +モデル料金補正ルールを設定し、プロキシの課金統計を調整します。 + +### グローバル送信プロキシ + +CC Switch の送信 HTTP/HTTPS プロキシを設定します。外部 API にプロキシ経由でアクセスする必要がある場合に使用します。 + +## 詳細設定 + +設定 → 詳細 タブ + +### 設定ディレクトリ + +各アプリの設定ファイルディレクトリをカスタマイズ。下記の「ディレクトリ設定」セクションを参照してください。 + +### データ管理 + +設定バックアップのインポート/エクスポート。下記の「データ管理」セクションを参照してください。 + +### バックアップと復元 + +自動バックアップの管理: + +| 設定 | 説明 | +| -------- | -------------------------- | +| バックアップ間隔 | 自動バックアップの時間間隔(時間) | +| 保持数量 | 保持するバックアップの数 | + +バックアップリストの表示とバックアップからの復元をサポートします。 + +### クラウド同期(WebDAV) + +WebDAV プロトコルを使用して複数のデバイス間で設定を同期します。 + +| 設定項目 | 説明 | +| -------- | ------------------------------------- | +| サービスプリセット | 坚果云 / Nextcloud / Synology / カスタム | +| サーバー URL | WebDAV サーバー URL | +| ユーザー名 | ログインユーザー名 | +| パスワード | ログインパスワード(アプリ専用パスワード) | +| リモートディレクトリ | リモート保存パス(デフォルト `cc-switch-sync`) | +| プロファイル名 | デバイスプロファイル名(デフォルト `default`) | +| 自動同期 | 有効にすると変更を自動アップロード | + +操作: + +- **接続テスト**:WebDAV 設定が正しいか確認 +- **保存**:設定を保存して自動テスト +- **アップロード**:ローカルデータをリモートにアップロード +- **ダウンロード**:リモートからローカルにデータをダウンロード + +> **注意**:アップロードはリモートデータを、ダウンロードはローカルデータを上書きします。操作前にご確認ください。 + +### ログ設定 + +| 設定項目 | 説明 | +| -------- | ----------------------------------- | +| ログを有効化 | アプリのログ記録のオン/オフ | +| ログレベル | error / warn / info / debug / trace | + +ログレベルの説明: + +- **error** - エラーのみ記録 +- **warn** - 警告とエラーを記録 +- **info** - 一般情報を記録(推奨) +- **debug** - デバッグ情報を記録 +- **trace** - すべての詳細情報を記録 + +## バージョン情報ページ + +設定 → バージョン情報 タブ + +### バージョン情報 + +現在の CC Switch バージョン番号を表示し、以下をサポートします: + +- リリースノートの表示 +- 更新の確認 +- 新バージョンのダウンロードとインストール + +### ローカル環境チェック + +インストール済みの CLI ツールのバージョンを自動検出: + +| ツール | 検出内容 | +| -------- | ------------------ | +| Claude | 現在のバージョン、最新バージョン | +| Codex | 現在のバージョン、最新バージョン | +| Gemini | 現在のバージョン、最新バージョン | +| OpenCode | 現在のバージョン、最新バージョン | +| OpenClaw | 現在のバージョン、最新バージョン | + +「更新」ボタンをクリックして再検出できます。 + +### ワンクリックインストールコマンド + +CLI ツールを素早くインストール/更新するコマンドを提供: + +```bash +npm i -g @anthropic-ai/claude-code@latest +npm i -g @openai/codex@latest +npm i -g @google/gemini-cli@latest +npm i -g opencode@latest +npm i -g openclaw@latest +``` + +「コピー」ボタンでクリップボードにコピーできます。 diff --git a/docs/user-manual/ja/2-providers/2.1-add.md b/docs/user-manual/ja/2-providers/2.1-add.md new file mode 100644 index 000000000..e1133041c --- /dev/null +++ b/docs/user-manual/ja/2-providers/2.1-add.md @@ -0,0 +1,354 @@ +# 2.1 プロバイダーの追加 + +## 追加パネルを開く + +メイン画面右上の **+** ボタンをクリックして、プロバイダー追加パネルを開きます。 + +パネルは 2 つのタブに分かれています: +- **アプリ専用プロバイダー**:現在選択中のアプリ(Claude/Codex/Gemini/OpenCode/OpenClaw)専用 +- **統一プロバイダー**:アプリ間で共有する設定 + +## プリセットで追加 + +プリセットは事前に設定されたプロバイダーテンプレートで、API Key を入力するだけで使用できます。 + +### 操作手順 + +1. 「プリセット」ドロップダウンからプロバイダーを選択 +2. 名前とエンドポイントが自動入力される +3. **API Key** を入力 +4. (任意)メモを入力 +5. 「追加」をクリック + +### 主なプリセット + +#### Claude プリセット + +| プリセット名 | 説明 | +|----------|------| +| Claude 公式 | Anthropic 公式アカウントでログイン | +| DeepSeek | DeepSeek モデル | +| 智谱 GLM | 智谱 AI の GLM モデル | +| 智谱 GLM en | 智谱 AI(英語版) | +| 百炼 | アリクラウド百炼(通义千問) | +| Kimi | Moonshot Kimi モデル | +| Kimi For Coding | Kimi プログラミング専用モデル | +| ModelScope | 魔搭コミュニティ | +| KAT-Coder | KAT-Coder モデル | +| Longcat | Longcat AI | +| MiniMax | MiniMax モデル | +| MiniMax en | MiniMax(英語版) | +| DouBaoSeed | 豆包 Seed モデル | +| BaiLing | 百灵 AI | +| AiHubMix | AiHubMix 統合サービス | +| SiliconFlow | SiliconFlow | +| SiliconFlow en | SiliconFlow(英語版) | +| DMXAPI | DMXAPI 中継サービス | +| PackyCode | PackyCode 中継サービス ⭐ | +| Cubence | Cubence サービス | +| AIGoCode | AIGoCode サービス | +| RightCode | RightCode サービス | +| AICodeMirror | AICodeMirror サービス | +| OpenRouter | 統合ルーティングサービス | +| Nvidia | Nvidia AI サービス | +| Xiaomi MiMo | Xiaomi MiMo モデル | + +> ⭐ は公式パートナーを示します。プリセットリストはバージョンの更新に伴い変更される場合があります。アプリ内の実際の表示を基準にしてください。 + +#### Codex プリセット + +| プリセット名 | 説明 | +|----------|------| +| OpenAI 公式 | OpenAI 公式アカウントでログイン | +| Azure OpenAI | Azure OpenAI サービス | +| AiHubMix | AiHubMix 統合サービス | +| DMXAPI | DMXAPI 中継サービス | +| PackyCode | PackyCode 中継サービス | +| Cubence | Cubence サービス | +| AIGoCode | AIGoCode サービス | +| RightCode | RightCode サービス | +| AICodeMirror | AICodeMirror サービス | +| OpenRouter | 統合ルーティングサービス | + +#### Gemini プリセット + +| プリセット名 | 説明 | +|----------|------| +| Google 公式 | Google OAuth でログイン | +| PackyCode | PackyCode 中継サービス | +| Cubence | Cubence サービス | +| AIGoCode | AIGoCode サービス | +| AICodeMirror | AICodeMirror サービス | +| OpenRouter | 統合ルーティングサービス | +| カスタム | すべてのパラメータを手動設定 | + +#### OpenCode プリセット + +| プリセット名 | 説明 | +|----------|------| +| DeepSeek | DeepSeek モデル | +| 智谱 GLM | 智谱 AI の GLM モデル | +| 智谱 GLM en | 智谱 AI(英語版) | +| 百炼 | アリクラウド百炼 | +| Kimi k2.5 | Moonshot Kimi-k2.5 モデル | +| Kimi For Coding | Kimi プログラミング専用モデル | +| ModelScope | 魔搭コミュニティ | +| KAT-Coder | KAT-Coder モデル | +| Longcat | Longcat AI | +| MiniMax | MiniMax モデル | +| MiniMax en | MiniMax(英語版) | +| DouBaoSeed | 豆包 Seed モデル | +| BaiLing | 百灵 AI | +| Xiaomi MiMo | Xiaomi MiMo モデル | +| AiHubMix | AiHubMix 統合サービス | +| DMXAPI | DMXAPI 中継サービス | +| OpenRouter | 統合ルーティングサービス | +| Nvidia | Nvidia AI サービス | +| PackyCode | PackyCode 中継サービス | +| Cubence | Cubence サービス | +| AIGoCode | AIGoCode サービス | +| RightCode | RightCode サービス | +| AICodeMirror | AICodeMirror サービス | +| OpenAI Compatible | OpenAI 互換インターフェース | +| Oh My OpenCode | Oh My OpenCode サービス | + +> プリセットリストは継続的に更新されています。アプリ内の実際の表示を基準にしてください。 + +#### OpenClaw プリセット + +| プリセット名 | 説明 | +|----------|------| +| DeepSeek | DeepSeek モデル | +| 智谱 GLM | 智谱 AI の GLM モデル | +| 智谱 GLM en | 智谱 AI(英語版) | +| Qwen Coder | 通义千問コーディングモデル | +| Kimi k2.5 | Moonshot Kimi-k2.5 モデル | +| Kimi For Coding | Kimi プログラミング専用モデル | +| MiniMax | MiniMax モデル | +| MiniMax en | MiniMax(英語版) | +| KAT-Coder | KAT-Coder モデル | +| Longcat | Longcat AI | +| DouBaoSeed | 豆包 Seed モデル | +| BaiLing | 百灵 AI | +| Xiaomi MiMo | Xiaomi MiMo モデル | +| AiHubMix | AiHubMix 統合サービス | +| DMXAPI | DMXAPI 中継サービス | +| OpenRouter | 統合ルーティングサービス | +| ModelScope | 魔搭コミュニティ | +| SiliconFlow | SiliconFlow | +| SiliconFlow en | SiliconFlow(英語版) | +| Nvidia | Nvidia AI サービス | +| PackyCode | PackyCode 中継サービス | +| Cubence | Cubence サービス | +| AIGoCode | AIGoCode サービス | +| RightCode | RightCode サービス | +| AICodeMirror | AICodeMirror サービス | +| AICoding | AICoding サービス | +| CrazyRouter | CrazyRouter サービス | +| SSSAiCode | SSSAiCode サービス | +| AWS Bedrock | AWS Bedrock サービス | +| OpenAI Compatible | OpenAI 互換インターフェース | + +## カスタム設定 + +「カスタム」プリセットを選択した場合、JSON 設定を手動で編集する必要があります。 + +### Claude 設定形式 + +```json +{ + "env": { + "ANTHROPIC_API_KEY": "your-api-key", + "ANTHROPIC_BASE_URL": "https://api.example.com" + } +} +``` + +| フィールド | 必須 | 説明 | +|------|------|------| +| `ANTHROPIC_API_KEY` | はい | API キー | +| `ANTHROPIC_BASE_URL` | いいえ | カスタムエンドポイントアドレス | +| `ANTHROPIC_AUTH_TOKEN` | いいえ | API_KEY の代替認証方式 | + +### Codex 設定形式 + +Codex は 2 つの設定ファイルを使用します: + +**1. auth.json**(`~/.codex/auth.json`)- API キーを保存: + +```json +{ + "OPENAI_API_KEY": "your-api-key" +} +``` + +**2. config.toml**(`~/.codex/config.toml`)- モデルとエンドポイントの設定を保存: + +```toml +# 基本設定 +model_provider = "custom" +model = "gpt-5.2" +model_reasoning_effort = "high" +disable_response_storage = true + +# カスタムプロバイダー設定 +[model_providers.custom] +name = "custom" +base_url = "https://api.example.com/v1" +wire_api = "responses" +requires_openai_auth = true +``` + +**auth.json フィールド説明**: + +| フィールド | 必須 | 説明 | +|------|------|------| +| `OPENAI_API_KEY` | はい | API キー | + +**config.toml フィールド説明**: + +| フィールド | 必須 | 説明 | +|------|------|------| +| `model_provider` | はい | モデルプロバイダー名(`[model_providers.xxx]` と一致する必要あり) | +| `model` | はい | 使用するモデル(例:`gpt-5.2`、`gpt-4o`) | +| `model_reasoning_effort` | いいえ | 推論強度:`low` / `medium` / `high` | +| `disable_response_storage` | いいえ | レスポンス保存を無効にするかどうか | +| `base_url` | はい | API エンドポイントアドレス | +| `wire_api` | いいえ | API プロトコルタイプ(通常 `responses`) | +| `requires_openai_auth` | いいえ | OpenAI 認証方式を使用するかどうか | + + +### Gemini 設定形式 + +```json +{ + "env": { + "GEMINI_API_KEY": "your-api-key", + "GOOGLE_GEMINI_BASE_URL": "https://api.example.com" + } +} +``` + +| フィールド | 必須 | 説明 | +|------|------|------| +| `GEMINI_API_KEY` | はい | API キー | +| `GOOGLE_GEMINI_BASE_URL` | いいえ | カスタムエンドポイントアドレス | +| `GEMINI_MODEL` | いいえ | モデルの指定 | + +> 認証タイプは CC Switch が自動的に検出します(PackyCode API プロキシ / Google OAuth / 汎用 API Key)。手動設定は不要です。 + +## 統一プロバイダー + +統一プロバイダーは Claude/Codex/Gemini/OpenCode/OpenClaw 間で設定を共有でき、複数の API 形式をサポートする中継サービスに適しています。 + +### 統一プロバイダーの作成 + +1. 「統一プロバイダー」タブに切り替え +2. 「統一プロバイダーを追加」をクリック +3. 共通設定を入力: + - 名前 + - API Key + - エンドポイントアドレス +4. 同期するアプリにチェック(Claude/Codex/Gemini/OpenCode/OpenClaw) +5. 保存 + +### 同期の仕組み + +統一プロバイダーはチェックしたアプリに自動的に同期されます: + +- 統一プロバイダーを変更すると、関連するすべてのアプリの設定が同期更新される +- 統一プロバイダーを削除すると、関連するアプリの設定も削除される + +### 保存して同期 + +統一プロバイダーの編集時に選択できます: + +| 操作 | 説明 | +|------|------| +| 保存 | 設定のみ保存、すぐに同期しない | +| 保存して同期 | 設定を保存し、有効なすべてのアプリに即座に同期 | + +### 手動同期 + +手動で同期をトリガーする場合: + +1. 統一プロバイダーカードの「同期」ボタンをクリック +2. 同期操作を確認 +3. 各アプリの関連プロバイダーの設定が上書きされる + +## プロバイダーのインポート + +CC Switch は 2 つの方法でプロバイダー設定をインポートできます: + +### 方法 1:ディープリンクでインポート + +`ccswitch://` プロトコルリンクでワンクリックインポート: + +1. ディープリンクをクリックまたはアクセス +2. CC Switch が自動的に開き、インポート確認を表示 +3. 設定情報をプレビュー +4. 「インポートを確認」をクリック + +**ディープリンクの取得方法**: +- 他の人からの共有で取得 +- [オンライン生成ツール](https://farion1231.github.io/cc-switch/deplink.html) で作成 + +### 方法 2:データベースバックアップからインポート + +SQL バックアップファイルから一括インポート: + +1. 「設定 → 詳細 → データ管理」を開く +2. 「ファイルを選択」をクリック +3. 以前にエクスポートした `.sql` バックアップファイルを選択 +4. 「インポート」をクリック +5. 既存の設定の上書きを確認 + +**インポート内容**: +- すべてのプロバイダー設定 +- MCP サーバー設定 +- Prompts プリセット +- 使用量ログ + +> **注意**:インポートは既存のデータベースを上書きするため、事前に現在の設定をエクスポートしてバックアップすることをお勧めします。エクスポートファイル名の形式は `cc-switch-export-{タイムスタンプ}.sql` です。 + +## 高度なオプション + +### カスタムアイコン + +名前の左側にあるアイコンエリアをクリックすると: + +- プリセットアイコンを選択 +- アイコンの色をカスタマイズ + +### Web サイトリンク + +プロバイダーの公式サイトやコンソールのアドレスを入力して、素早くアクセスできます: + +- プロバイダーカードのリンクアイコンをクリックすると直接開く +- 残額の確認や API Key の取得などに使用 + +### メモ + +以下のようなメモ情報を追加できます: + +- アカウントの用途(個人/仕事) +- プランの情報 +- 有効期限 + +メモはプロバイダーカードに表示され、検索にも対応しています。 + +### エンドポイント速度テスト + +プロバイダーを追加した後、API エンドポイントの速度テストができます: + +1. プロバイダーカードの「テスト」ボタンをクリック +2. テストパネルで複数のエンドポイント URL を追加 +3. 「テスト」をクリックして実行 +4. レイテンシが最も低いエンドポイントを選択 + +**テスト結果**: +- 緑:レイテンシ < 500ms(優秀) +- 黄:レイテンシ 500-1000ms(普通) +- 赤:レイテンシ > 1000ms(遅い) + +![image-20260108005327817](../../assets/image-20260108005327817.png) diff --git a/docs/user-manual/ja/2-providers/2.2-switch.md b/docs/user-manual/ja/2-providers/2.2-switch.md new file mode 100644 index 000000000..60525875b --- /dev/null +++ b/docs/user-manual/ja/2-providers/2.2-switch.md @@ -0,0 +1,111 @@ +# 2.2 プロバイダーの切り替え + +## メイン画面での切り替え + +プロバイダーリストで、対象のプロバイダーカードの「有効化」ボタンをクリックします。 + +### 切り替えフロー + +1. 「有効化」ボタンをクリック +2. CC Switch が設定ファイルを更新 +3. カードのステータスが「現在有効」に変更 +4. Claude/Gemini は即時反映、Codex はターミナルの再起動が必要 + +### ステータス表示 + +| ステータス | 表示 | 説明 | +|------|------|------| +| 現在有効 | 青い枠 + ラベル | 設定ファイル内の現在のプロバイダー | +| プロキシアクティブ | 緑の枠 | プロキシモードで実際に使用中のプロバイダー | +| 通常 | デフォルトのスタイル | 有効化されていないプロバイダー | + +## トレイでの素早い切り替え + +システムトレイから素早く切り替えられ、メイン画面を開く必要がありません。 + +### 操作手順 + +1. システムトレイの CC Switch アイコンを右クリック +2. メニューで対応するアプリ(Claude/Codex/Gemini/OpenCode)を見つける +3. 切り替えたいプロバイダー名をクリック +4. 切り替え完了、トレイに短い通知が表示 + +### トレイメニュー構造 + +![image-20260108004348993](../../assets/image-20260108004348993.png) + +## 反映方法 + +### Claude Code + +**切り替え後に即時反映**、再起動は不要です。 + +Claude Code はホットリロードに対応しており、設定ファイルの変更を自動検出して再読み込みします。 + +### Codex + +切り替え後は再起動が必要: +- 現在のターミナルウィンドウを閉じる +- ターミナルを再度開く + +### Gemini CLI + +**切り替え後に即時反映**、再起動は不要です。 + +Gemini CLI はリクエストごとに `.env` ファイルを再読み込みします。 + +## 設定ファイルの変更 + +プロバイダーを切り替える際、CC Switch は以下のファイルを変更します: + +### Claude + +``` +~/.claude/settings.json +``` + +変更内容: +```json +{ + "env": { + "ANTHROPIC_API_KEY": "新しい API Key", + "ANTHROPIC_BASE_URL": "新しいエンドポイント" + } +} +``` + +### Codex + +``` +~/.codex/auth.json +~/.codex/config.toml(追加設定がある場合) +``` + +### Gemini + +``` +~/.gemini/.env +~/.gemini/settings.json +``` + +## 切り替え失敗時の対処 + +切り替えに失敗した場合、考えられる原因: + +### 設定ファイルがロックされている + +他のプログラムが設定ファイルを使用中です。 + +**解決方法**:実行中の CLI ツールを閉じてから、再度切り替えを試みてください。 + +### 権限不足 + +設定ファイルへの書き込み権限がありません。 + +**解決方法**:設定ディレクトリの権限設定を確認してください。 + +### 設定形式エラー + +プロバイダー設定の JSON 形式に誤りがあります。 + +**解決方法**:プロバイダーを編集して、JSON 形式を確認・修正してください。 diff --git a/docs/user-manual/ja/2-providers/2.3-edit.md b/docs/user-manual/ja/2-providers/2.3-edit.md new file mode 100644 index 000000000..1f80f72e5 --- /dev/null +++ b/docs/user-manual/ja/2-providers/2.3-edit.md @@ -0,0 +1,145 @@ +# 2.3 プロバイダーの編集 + +## 編集パネルを開く + +1. 編集したいプロバイダーカードを見つける +2. カードにマウスをホバーして操作ボタンを表示 +3. 「編集」ボタンをクリック + +## 編集可能な内容 + +### 基本情報 + +| フィールド | 説明 | +|------|------| +| 名前 | プロバイダーの表示名 | +| メモ | 付加情報 | +| Web サイトリンク | プロバイダーの公式サイトまたはコンソールアドレス | +| アイコン | カスタムアイコンと色 | + +### アイコンのカスタマイズ + +CC Switch は豊富なアイコンカスタマイズ機能を提供しています: + +#### アイコン選択画面 + +1. アイコンエリアをクリックしてアイコン選択画面を開く +2. 検索ボックスで名前からアイコンを検索 +3. クリックしてアイコンを選択 + +アイコンライブラリには一般的な AI サービスプロバイダーと技術アイコンが含まれており、以下をサポートします: +- 名前によるあいまい検索 +- アイコン名のツールチップ表示 +- 選択結果のリアルタイムプレビュー + +![image-20260108004734882](../../assets/image-20260108004734882.png) + +### 設定情報 + +JSON 形式の設定内容(以下を含む): + +- API Key +- エンドポイントアドレス +- その他の環境変数 + +### 現在有効なプロバイダーの編集 + +現在有効なプロバイダーを編集する場合、特別な「バックフィル」機能があります: + +1. 編集パネルを開くと、live 設定ファイルから最新の内容を読み取る +2. CLI ツールで手動で設定を変更した場合、その変更が同期される +3. 保存すると、変更が live 設定ファイルに書き込まれる + +これにより、CC Switch と CLI ツールの設定が常に同期されます。 + +## API Key の変更 + +プロバイダーの編集時に、**API Key** 入力ボックスから直接変更できます: + +1. プロバイダーカードの「編集」ボタンをクリック +2. 「API Key」入力ボックスに新しいキーを入力 +3. 「保存」をクリック + +> **ヒント**:API Key 入力ボックスは表示/非表示の切り替えに対応しています。右側の目のアイコンをクリックするとキーの全文を確認できます。 + +## エンドポイントアドレスの変更 + +プロバイダーの編集時に、**エンドポイントアドレス** 入力ボックスから直接変更できます: + +1. プロバイダーカードの「編集」ボタンをクリック +2. 「エンドポイントアドレス」入力ボックスに新しい URL を入力 +3. 「保存」をクリック + +### エンドポイントアドレスの形式 + +| アプリ | 形式の例 | +|------|----------| +| Claude | `https://api.example.com` | +| Codex | `https://api.example.com/v1` | +| Gemini | `https://api.example.com` | + +## カスタムエンドポイントの追加 + +プロバイダーには複数のエンドポイントを設定でき、以下の用途に使用します: + +- 速度テスト時に複数のアドレスをテスト +- フェイルオーバー時のバックアップエンドポイント + +### 自動収集 + +プロバイダーの追加時に、CC Switch は設定からエンドポイントアドレスを自動抽出します。 + +### 手動追加 + +プロバイダーの編集時に、「エンドポイント管理」エリアで以下の操作が可能です: + +- 新しいエンドポイントの追加 +- 既存のエンドポイントの削除 +- デフォルトエンドポイントの設定 + +## JSON エディタ + +設定は JSON 形式を使用し、エディタは以下を提供します: + +- シンタックスハイライト +- フォーマット検証 +- エラー通知 + +### よくあるエラー + +**引用符の欠落**: +```json +// ❌ 間違い +{ env: { KEY: "value" } } + +// ✅ 正しい +{ "env": { "KEY": "value" } } +``` + +**余分なカンマ**: +```json +// ❌ 間違い +{ "env": { "KEY": "value", } } + +// ✅ 正しい +{ "env": { "KEY": "value" } } +``` + +**閉じ括弧の欠落**: +```json +// ❌ 間違い +{ "env": { "KEY": "value" } + +// ✅ 正しい +{ "env": { "KEY": "value" } } +``` + +## 保存と反映 + +1. 「保存」ボタンをクリック +2. 現在有効なプロバイダーの場合、設定は即座に live ファイルに書き込まれる +3. CLI ツールを再起動して反映 + +## 編集のキャンセル + +「キャンセル」ボタンをクリックするか `Esc` キーを押して編集パネルを閉じると、すべての変更は保存されません。 diff --git a/docs/user-manual/ja/2-providers/2.4-sort-duplicate.md b/docs/user-manual/ja/2-providers/2.4-sort-duplicate.md new file mode 100644 index 000000000..afb1773b8 --- /dev/null +++ b/docs/user-manual/ja/2-providers/2.4-sort-duplicate.md @@ -0,0 +1,76 @@ +# 2.4 並べ替えと複製 + +## ドラッグで並べ替え + +ドラッグでプロバイダーの表示順序を調整します。 + +### 操作手順 + +1. プロバイダーカード左側の **≡** ドラッグハンドルにマウスを合わせる +2. マウスの左ボタンを押し続ける +3. 目的の位置まで上下にドラッグ +4. マウスを離して並べ替え完了 + +### 並べ替えの用途 + +- **よく使うものを優先**:よく使うプロバイダーをリストの上部に配置 +- **フェイルオーバーの順序**:並び順はフェイルオーバーキューのデフォルト順序に影響 + +## プロバイダーの複製 + +プロバイダーのコピーを素早く作成します。以下のような場面に適しています: + +- 既存の設定をベースにバリエーションを作成 +- 現在の設定をバックアップ +- テスト用の設定を作成 + +### 操作手順 + +1. プロバイダーカードにマウスをホバーして操作ボタンを表示 +2. 「複製」ボタンをクリック +3. 名前に `copy` が付加されたコピーが自動作成 +4. コピーを編集して設定を変更 + +### コピーされる内容 + +コピーは完全な複製が作成され、以下を含みます: + +| 内容 | コピーされるか | +|------|----------| +| 名前 | コピーされる(`copy` が付加) | +| 設定 | 完全にコピー | +| メモ | コピーされる | +| Web サイトリンク | コピーされる | +| アイコン | コピーされる | +| エンドポイントリスト | コピーされる | +| 並び順の位置 | 元のプロバイダーの下に挿入 | + +### コピー後の編集 + +コピー完了後、通常は以下を変更します: + +1. **名前**:意味のある名前に変更 +2. **API Key**:異なるアカウントの場合 +3. **エンドポイント**:異なるサービスの場合 + +## プロバイダーの削除 + +### 操作手順 + +1. プロバイダーカードにマウスをホバーして操作ボタンを表示 +2. 「削除」ボタンをクリック +3. 削除を確認 + +### 削除の確認 + +削除前に確認ダイアログが表示され、以下が表示されます: + +- プロバイダー名 +- 削除後は元に戻せないという注意 + +### 削除の制限 + +- **現在有効なプロバイダー**:削除可能ですが、先に他のプロバイダーに切り替えることをお勧めします +- **統一プロバイダー**:削除すると、関連するアプリの設定も削除されます + +![image-20260108004946288](../../assets/image-20260108004946288.png) diff --git a/docs/user-manual/ja/2-providers/2.5-usage-query.md b/docs/user-manual/ja/2-providers/2.5-usage-query.md new file mode 100644 index 000000000..36d76ff8c --- /dev/null +++ b/docs/user-manual/ja/2-providers/2.5-usage-query.md @@ -0,0 +1,181 @@ +# 2.5 使用量クエリ + +## 機能説明 + +使用量クエリ機能により、カスタムスクリプトを設定して、プロバイダーの残額や使用量などの情報をリアルタイムでクエリできます。 + +**使用シーン**: +- API アカウントの残額確認 +- プランの使用状況の監視 +- 複数プランの残額を集約表示 + +## 設定を開く + +1. プロバイダーカードにマウスをホバーして操作ボタンを表示 +2. 「使用量クエリ」ボタンをクリック +3. 使用量クエリ設定パネルが開く + +## 使用量クエリの有効化 + +設定パネル上部の「使用量クエリを有効にする」スイッチをオンにします。 + +## プリセットテンプレート + +CC Switch は 3 種類のプリセットテンプレートを提供しています: + +### カスタムテンプレート + +リクエストと抽出ロジックを完全にカスタマイズします。特殊な API 形式に対応します。 + +### 汎用テンプレート + +ほとんどの標準的な API 形式のプロバイダーに適しています: + +```javascript +({ + request: { + url: "{{baseUrl}}/user/balance", + method: "GET", + headers: { + "Authorization": "Bearer {{apiKey}}", + "User-Agent": "cc-switch/1.0" + } + }, + extractor: function(response) { + return { + isValid: response.is_active || true, + remaining: response.balance, + unit: "USD" + }; + } +}) +``` + +**設定パラメータ**: +| パラメータ | 説明 | +|------|------| +| API Key | 認証用のキー(任意、空欄の場合はプロバイダーに設定されたキーを使用) | +| Base URL | API ベースアドレス(任意、空欄の場合はプロバイダーのエンドポイントを使用) | + +### New API テンプレート + +New API タイプの中継サービス専用に設計されています: + +```javascript +({ + request: { + url: "{{baseUrl}}/api/user/self", + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer {{accessToken}}", + "New-Api-User": "{{userId}}" + }, + }, + extractor: function (response) { + if (response.success && response.data) { + return { + planName: response.data.group || "デフォルトプラン", + remaining: response.data.quota / 500000, + used: response.data.used_quota / 500000, + total: (response.data.quota + response.data.used_quota) / 500000, + unit: "USD", + }; + } + return { + isValid: false, + invalidMessage: response.message || "クエリ失敗" + }; + }, +}) +``` + +**設定パラメータ**: +| パラメータ | 説明 | +|------|------| +| Base URL | New API サービスアドレス | +| Access Token | アクセストークン | +| User ID | ユーザー ID | + +## 共通設定 + +### タイムアウト時間 + +リクエストのタイムアウト時間(秒)、デフォルトは 10 秒。 + +### 自動クエリ間隔 + +使用量データの自動更新間隔(分): +- `0` に設定すると自動クエリを無効化 +- 範囲:0-1440 分(最長 24 時間) +- プロバイダーが「現在有効」のときのみ動作 + +## エクストラクターの戻り値形式 + +エクストラクター関数は以下のフィールドを含むオブジェクトを返す必要があります: + +| フィールド | 型 | 必須 | 説明 | +|------|------|------|------| +| `isValid` | boolean | いいえ | アカウントが有効かどうか、デフォルト true | +| `invalidMessage` | string | いいえ | 無効時の通知メッセージ | +| `remaining` | number | はい | 残額 | +| `unit` | string | はい | 単位(例:USD、CNY、回) | +| `planName` | string | いいえ | プラン名(複数プラン対応) | +| `total` | number | いいえ | 総額 | +| `used` | number | いいえ | 使用済み額 | +| `extra` | object | いいえ | 追加情報 | + +## スクリプトのテスト + +設定完了後、「スクリプトをテスト」ボタンをクリックして確認します: + +1. 設定された URL にリクエストを送信 +2. エクストラクター関数を実行 +3. 結果またはエラー情報を表示 + +## 表示効果 + +設定が成功すると、プロバイダーカードに以下が表示されます: + +- **単一プラン**:残額を直接表示 +- **複数プラン**:プラン数を表示、クリックで詳細を展開 + +## 変数プレースホルダー + +スクリプト内で以下のプレースホルダーを使用でき、実行時に自動的に置換されます: + +| プレースホルダー | 説明 | +|--------|------| +| `{{apiKey}}` | 設定された API Key | +| `{{baseUrl}}` | 設定された Base URL | +| `{{accessToken}}` | 設定された Access Token(New API) | +| `{{userId}}` | 設定された User ID(New API) | + +## 一般的なプロバイダーの設定例 + +### トラブルシューティング + +### クエリ失敗 + +**確認事項**: +1. API Key が正しいか +2. Base URL が正しいか +3. ネットワークがアクセス可能か +4. タイムアウト時間が十分か + +### 返却データが空 + +**確認事項**: +1. エクストラクター関数に `return` 文があるか +2. レスポンスのデータ構造がエクストラクターと一致しているか +3. 「スクリプトをテスト」で生のレスポンスを確認 + +### フォーマット失敗 + +スクリプトに構文エラーがある場合、「フォーマット」ボタンをクリックするとエラー箇所が表示されます。 + +## 注意事項 + +- 使用量クエリは少量の API リクエスト枠を消費します +- 頻繁なリクエストを避けるため、適切な自動クエリ間隔を設定してください +- 機密情報(API Key、Token)はローカルに安全に保存されます diff --git a/docs/user-manual/ja/3-extensions/3.1-mcp.md b/docs/user-manual/ja/3-extensions/3.1-mcp.md new file mode 100644 index 000000000..0ccb0063f --- /dev/null +++ b/docs/user-manual/ja/3-extensions/3.1-mcp.md @@ -0,0 +1,209 @@ +# 3.1 MCP サーバー管理 + +## MCP とは + +MCP (Model Context Protocol) は、AI ツールが外部データソースやツールにアクセスできるようにするプロトコルです。MCP サーバーにより、AI は以下のことが可能になります: + +- ファイルシステムへのアクセス +- ネットワークリクエストの実行 +- データベースのクエリ +- 外部 API の呼び出し + +## MCP パネルを開く + +上部ナビゲーションバーの **MCP** ボタンをクリックします。 + +## パネル概要 + +![image-20260108005723522](../../assets/image-20260108005723522.png) + +## MCP サーバーの追加 + +### プリセットテンプレートを使用 + +1. 右上の **+** ボタンをクリック +2. 「プリセット」ドロップダウンからテンプレートを選択 +3. 必要に応じて設定を変更 +4. 「保存」をクリック + +![image-20260108005739731](../../assets/image-20260108005739731.png) + +### 主なプリセット + +| プリセット | パッケージ名 | 機能説明 | +|------|------|----------| +| fetch | mcp-server-fetch | HTTP リクエストツール、AI が Web コンテンツを取得可能に | +| time | @modelcontextprotocol/server-time | 時間ツール、現在の時刻情報を提供 | +| memory | @modelcontextprotocol/server-memory | メモリツール、AI が情報を保存・検索可能に | +| sequential-thinking | @modelcontextprotocol/server-sequential-thinking | 思考連鎖ツール、AI の推論能力を強化 | +| context7 | @upstash/context7-mcp | ドキュメント検索ツール、技術ドキュメントをクエリ | + +### カスタム設定 + +「カスタム」を選択した場合、以下を入力する必要があります: + +| フィールド | 必須 | 説明 | +|------|------|------| +| サーバー ID | はい | 一意な識別子 | +| 名前 | いいえ | 表示名 | +| 説明 | いいえ | 機能の説明 | +| 転送タイプ | はい | stdio / http / sse | +| コマンド | はい* | stdio タイプの場合は必須 | +| 引数 | いいえ | コマンドライン引数 | +| URL | はい* | http/sse タイプの場合は必須 | +| Headers | いいえ | http/sse タイプのリクエストヘッダー | +| 環境変数 | いいえ | サーバーに渡す環境変数 | + +## 転送タイプ + +### stdio(標準入出力) + +最も一般的なタイプで、ローカルプロセスを起動して通信します。 + +```json +{ + "command": "uvx", + "args": ["mcp-server-fetch"], + "env": {} +} +``` + +**要件**: +- 対応するコマンド(例:`uvx`、`npx`)がインストールされている必要あり +- サーバープログラムが PATH に含まれている必要あり + +### http + +HTTP プロトコルでリモートサーバーと通信します。 + +```json +{ + "url": "http://localhost:8080/mcp" +} +``` + +### sse(Server-Sent Events) + +SSE プロトコルでサーバーと通信し、リアルタイムプッシュをサポートします。 + +```json +{ + "url": "http://localhost:8080/sse" +} +``` + +## アプリバインド + +各 MCP サーバーは、有効にするアプリを個別に制御できます。 + +### スイッチの説明 + +| スイッチ | 作用 | 設定ファイルパス | +|------|------|--------------| +| Claude | Claude Code に同期 | `~/.claude.json` の `mcpServers` | +| Codex | Codex に同期 | `~/.codex/config.toml` の `[mcp_servers]` | +| Gemini | Gemini CLI に同期 | `~/.gemini/settings.json` の `mcpServers` | +| OpenCode | OpenCode に同期 | `~/.opencode/config.json` の `mcpServers` | + +> **注意**:OpenClaw は現在 MCP サーバー管理に対応していません。MCP 機能は現在 Claude、Codex、Gemini、OpenCode の 4 つのアプリのみサポートしています。 + +### スイッチの動作 + +あるアプリのスイッチをオンにすると、CC Switch は以下を実行します: + +1. **データベースの更新**:サーバーの `apps.claude/codex/gemini/opencode` のステータスを `true` に設定 +2. **Live 設定に同期**:サーバー設定を対応アプリの設定ファイルに書き込み +3. **即時反映**:次回 CLI ツール起動時に新しい MCP サーバーが自動的にロード + +あるアプリのスイッチをオフにすると、CC Switch は以下を実行します: + +1. **データベースの更新**:対応アプリのステータスを `false` に設定 +2. **Live 設定から削除**:アプリの設定ファイルからそのサーバーを削除 +3. **即時反映**:次回 CLI ツール起動時にその MCP サーバーはロードされない + +### 同期条件 + +MCP サーバーの同期は、対応アプリがインストールされている場合のみ実行されます: + +- **Claude**:`~/.claude/` ディレクトリまたは `~/.claude.json` ファイルが存在する必要あり +- **Codex**:`~/.codex/` ディレクトリが存在する必要あり +- **Gemini**:`~/.gemini/` ディレクトリが存在する必要あり +- **OpenCode**:`~/.opencode/` ディレクトリが存在する必要あり + +> **ヒント**:CLI ツールがインストールされていない場合、対応するスイッチをオンにしてもエラーにはなりませんが、設定は書き込まれません。 + +スイッチをオフにすると、設定はファイルから削除されます。 + +## サーバーの編集 + +1. サーバー行の右側にある「編集」ボタンをクリック +2. 設定を変更 +3. 「保存」をクリック + +変更は有効になっているアプリの設定ファイルに即座に同期されます。 + +## サーバーの削除 + +1. サーバー行の右側にある「削除」ボタンをクリック +2. 削除を確認 + +削除後、設定はすべてのアプリの設定ファイルから削除されます。 + +## 既存の設定のインポート + +CLI ツールで既に MCP サーバーを設定している場合、CC Switch にインポートできます: + +1. 「インポート」ボタンをクリック +2. インポートするアプリを選択(Claude/Codex/Gemini/OpenCode) +3. CC Switch が既存の設定を読み取ってインポート + +## 設定ファイル形式 + +### Claude (`~/.claude.json`) + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +### Codex (`~/.codex/config.toml`) + +```toml +[mcp_servers.mcp-fetch] +command = "uvx" +args = ["mcp-server-fetch"] +``` + +### Gemini (`~/.gemini/settings.json`) + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +## よくある質問 + +### サーバーの起動に失敗する + +確認事項: +- コマンドが正しくインストールされているか(例:`uvx`) +- コマンドが PATH に含まれているか +- 引数が正しいか + +### 設定が反映されない + +確認事項: +- 対応するアプリのスイッチがオンになっているか +- CLI ツールを再起動したか diff --git a/docs/user-manual/ja/3-extensions/3.2-prompts.md b/docs/user-manual/ja/3-extensions/3.2-prompts.md new file mode 100644 index 000000000..ed3a004f0 --- /dev/null +++ b/docs/user-manual/ja/3-extensions/3.2-prompts.md @@ -0,0 +1,160 @@ +# 3.2 Prompts プロンプト管理 + +## 機能説明 + +Prompts 機能は、システムプロンプトのプリセットを管理します。システムプロンプトは AI の動作や回答スタイルに影響します。 + +CC Switch を使用すると: + +- 複数のプロンプトプリセットを作成 +- さまざまなシーンのプロンプトを素早く切り替え +- デバイス間でプロンプト設定を同期 + +## Prompts パネルを開く + +上部ナビゲーションバーの **Prompts** ボタンをクリックします。 + +## パネル概要 + +![image-20260108010110382](../../assets/image-20260108010110382.png) + +## プリセットの作成 + +### 操作手順 + +1. 右上の **+** ボタンをクリック +2. プリセット名を入力 +3. Markdown エディタでプロンプトを作成 +4. 「保存」をクリック + +### Markdown エディタ + +エディタは以下を提供します: + +- シンタックスハイライト +- リアルタイムプレビュー +- よく使うフォーマットのショートカットキー + +### プロンプトの書き方のヒント + +**構造化フォーマット**: + +```markdown +# 役割定義 + +あなたはプロのコードレビュー専門家です。 + +## コア能力 + +- コード品質分析 +- パフォーマンス最適化の提案 +- セキュリティ脆弱性の検出 + +## 回答スタイル + +- 簡潔明瞭 +- 具体的な例を提供 +- 改善提案を提示 + +## 注意事項 + +- ビジネスロジックを変更しない +- コードスタイルの一貫性を保つ +``` + +## プリセットの有効化 + +### 操作方法 + +プリセット項目のスイッチボタンをクリックして、有効/無効を切り替えます。 + +### 単一有効化 + +同時に有効にできるプリセットは 1 つだけです。新しいプリセットを有効にすると、以前のプリセットは自動的に無効になります。 + +### 同期先 + +有効化後、プロンプトは対応するアプリのファイルに書き込まれます: + +| アプリ | ファイルパス | +|------|----------| +| Claude | `~/.claude/CLAUDE.md` | +| Codex | `~/.codex/AGENTS.md` | +| Gemini | `~/.gemini/GEMINI.md` | +| OpenCode | `~/.opencode/AGENTS.md` | +| OpenClaw | `~/.openclaw/AGENTS.md` | + +## プリセットの編集 + +1. プリセット項目の「編集」ボタンをクリック +2. 名前や内容を変更 +3. 「保存」をクリック + +現在有効なプリセットを編集した場合、保存後に設定ファイルに即座に同期されます。 + +## プリセットの削除 + +1. プリセット項目の「削除」ボタンをクリック +2. 削除を確認 + +有効になっているプリセットは削除できません。先に無効にしてから削除してください。 + +## スマートバックフィル + +CC Switch は、手動での変更を失わないようにスマートバックフィル保護機能を提供しています。 + +### 動作原理 + +1. プリセットを切り替える前に、現在の設定ファイルの内容を自動的に読み取る +2. ファイルの内容とデータベース内のプリセットを比較 +3. 内容が異なる場合、ユーザーが手動で変更したことを示す +4. 手動変更の内容を現在のプリセットに保存 +5. その後、新しいプリセットに切り替え + +### 保護シーン + +| シーン | 処理方法 | +|------|----------| +| CLI 内で `CLAUDE.md` を直接編集 | 変更が自動的に現在のプリセットに保存 | +| 外部エディタで設定ファイルを変更 | 変更が自動的に現在のプリセットに保存 | +| 別のプリセットに切り替え | 現在の変更を保存してから切り替え | + +### 技術的な詳細 + +バックフィル機能は以下のタイミングでトリガーされます: + +- **プリセットの切り替え時**:現在の live ファイルの内容を現在のプリセットに保存 +- **現在のプリセットの編集時**:live ファイルから最新の内容を読み取り +- **初回起動時**:既存の live ファイルの内容を自動インポート + +### 注意事項 + +- バックフィルは異なるプリセットに切り替えるときにのみトリガーされる +- 現在有効なプリセットがない場合、バックフィルはトリガーされない +- バックフィルの失敗は切り替えフローに影響しない + +## アプリ間での使用 + +Prompts はアプリごとに個別に管理されます: + +- Claude に切り替えると、Claude のプリセットが表示 +- Codex に切り替えると、Codex のプリセットが表示 +- Gemini に切り替えると、Gemini のプリセットが表示 +- OpenCode に切り替えると、OpenCode のプリセットが表示 +- OpenClaw に切り替えると、OpenClaw のプリセットが表示 + +複数のアプリで同じプロンプトを使用する場合は、それぞれで作成する必要があります。 + +## インポート・エクスポート + +### ディープリンクで共有 + +ディープリンクを生成してプリセットを共有できます: + +``` +ccswitch://import/prompt?data= +``` + +### 設定のエクスポートで共有 + +設定をエクスポートするとすべてのプリセットが含まれ、インポートで復元できます。 diff --git a/docs/user-manual/ja/3-extensions/3.3-skills.md b/docs/user-manual/ja/3-extensions/3.3-skills.md new file mode 100644 index 000000000..79c2abe4b --- /dev/null +++ b/docs/user-manual/ja/3-extensions/3.3-skills.md @@ -0,0 +1,207 @@ +# 3.3 Skills スキル管理 + +## 機能説明 + +Skills は再利用可能な機能拡張で、AI ツールに特定分野の専門的な能力を与えます。 + +スキルはフォルダ形式で存在し、以下を含みます: + +- プロンプトテンプレート +- ツール定義 +- サンプルコード + +## 対応アプリ + +Skills 機能は以下の 4 つのアプリに対応しています: + +- **Claude Code** +- **Codex** +- **Gemini CLI** +- **OpenCode** + +## Skills ページを開く + +上部ナビゲーションバーの **Skills** ボタンをクリックします。 + +> 注意:Skills ボタンはすべてのアプリモードで表示されます。 + +## ページ概要 + +![image-20260108010253926](../../assets/image-20260108010253926.png) + +## スキルの発見 + +### プリセットリポジトリ + +CC Switch は以下の GitHub リポジトリをプリセットとして設定しています: + +| リポジトリ | 説明 | +| -------------- | ------------------------ | +| Anthropic 公式 | Anthropic 提供の公式スキル | +| ComposioHQ | コミュニティが管理するスキルコレクション | +| コミュニティ精選 | 厳選された高品質スキル | + +![image-20260108010308060](../../assets/image-20260108010308060.png) + +### 検索とフィルタリング + +CC Switch は強力な検索とフィルタリング機能を提供しています: + +#### 検索ボックス + +- スキル名で検索 +- スキルの説明で検索 +- ディレクトリ名で検索 +- リアルタイムフィルタリング、入力と同時に検索 + +#### ステータスフィルタ + +ドロップダウンメニューでインストール状態別にフィルタリング: + +| オプション | 説明 | +| ------ | ------------------ | +| すべて | すべてのスキルを表示 | +| インストール済み | インストール済みのスキルのみ表示 | +| 未インストール | 未インストールのスキルのみ表示 | + +![image-20260108010324583](../../assets/image-20260108010324583.png) + +#### 組み合わせて使用 + +検索とフィルタリングは組み合わせて使用できます: + +- まず「インストール済み」でフィルタリング +- 次にキーワードで検索 +- 結果にマッチ数が表示 + +### リストの更新 + +「更新」ボタンをクリックしてリポジトリを再スキャンし、最新のスキルを取得します。 + +## スキルのインストール + +### 操作手順 + +1. インストールしたいスキルカードを見つける +2. 「インストール」ボタンをクリック +3. インストール完了を待つ + +### インストール先 + +| アプリ | インストールディレクトリ | +| -------- | --------------------- | +| Claude | `~/.claude/skills/` | +| Codex | `~/.codex/skills/` | +| Gemini | `~/.gemini/skills/` | +| OpenCode | `~/.opencode/skills/` | + +### インストール内容 + +インストールによりスキルフォルダがローカルにコピーされます: + +``` +~/.claude/skills/ +└── skill-name/ + ├── README.md + ├── prompt.md + └── tools/ + └── ... +``` + +## スキルのアンインストール + +### 操作手順 + +1. インストール済みのスキルカードを見つける +2. 「アンインストール」ボタンをクリック +3. アンインストールを確認 + +### アンインストールの効果 + +- ローカルのスキルフォルダを削除 +- インストール状態を更新 + +## リポジトリ管理 + +### リポジトリ管理を開く + +ページ上部の「リポジトリ管理」ボタンをクリックします。 + +### カスタムリポジトリの追加 + +1. 「リポジトリを追加」をクリック +2. リポジトリ情報を入力: + - Owner:GitHub ユーザー名または組織名 + - Name:リポジトリ名 + - Branch:ブランチ名(デフォルト main) + - Subdirectory:スキルがあるサブディレクトリ(任意) +3. 「追加」をクリック + +### リポジトリの形式 + +``` +https://github.com/{owner}/{name}/tree/{branch}/{subdirectory} +``` + +例: + +``` +Owner: anthropics +Name: claude-skills +Branch: main +Subdirectory: skills +``` + +### リポジトリの削除 + +1. リポジトリリストで削除するリポジトリを見つける +2. 「削除」ボタンをクリック +3. 削除を確認 + +リポジトリを削除しても、そのリポジトリのスキルはリストから消えませんが、更新はできなくなります。 + +## スキルカードの情報 + +各スキルカードには以下が表示されます: + +| 情報 | 説明 | +| ---- | --------------- | +| 名前 | スキル名 | +| 説明 | 機能の説明 | +| ソース | 所属リポジトリ | +| ステータス | インストール済み / 未インストール | + +## スキルの更新 + +現在、自動更新には対応していません。スキルを更新するには: + +1. 既存のスキルをアンインストール +2. リストを更新 +3. 再度インストール + +### スキルリストが空の場合 + +考えられる原因: + +- ネットワークの問題で GitHub にアクセスできない +- リポジトリ設定のエラー + +解決方法: + +- ネットワーク接続を確認 +- 「更新」をクリックしてリトライ +- リポジトリ設定を確認 + +### インストールに失敗する場合 + +考えられる原因: + +- ネットワークの問題 +- ディスク容量不足 +- 権限の問題 + +解決方法: + +- ネットワーク接続を確認 +- ディスク容量を確認 +- ディレクトリの権限を確認 diff --git a/docs/user-manual/ja/4-proxy/4.1-service.md b/docs/user-manual/ja/4-proxy/4.1-service.md new file mode 100644 index 000000000..32b217bad --- /dev/null +++ b/docs/user-manual/ja/4-proxy/4.1-service.md @@ -0,0 +1,222 @@ +# 4.1 プロキシサービス + +## 機能説明 + +プロキシサービスは、ローカルで HTTP プロキシを起動し、すべての API リクエストをプロキシ経由で転送します。 + +**主な用途**: +- リクエストログの記録 +- API 使用量の統計 +- フェイルオーバーのサポート +- 複数アプリのリクエストを一元管理 + +## プロキシの起動 + +### 方法 1:メイン画面のスイッチ + +メイン画面上部の **プロキシスイッチ** ボタンをクリックします。 + +スイッチの状態: +- 白:プロキシ停止中 +- 緑:プロキシ実行中 + +![image-20260108011353927](../../assets/image-20260108011353927.png) + +### 方法 2:設定ページ + +1. 「設定 → 詳細 → プロキシサービス」を開く +2. 右上のスイッチをクリック + +![image-20260108011338922](../../assets/image-20260108011338922.png) + +## プロキシ設定 + +### 基本設定 + +| 設定項目 | 説明 | デフォルト値 | +|--------|------|--------| +| リスニングアドレス | プロキシがバインドする IP アドレス | `127.0.0.1` | +| リスニングポート | プロキシがリスニングするポート | `15721` | +| ログを有効化 | リクエストログを記録するかどうか | オン | + +### 設定の変更 + +1. **プロキシサービスを停止**(先に停止する必要あり) +2. リスニングアドレスまたはポートを変更 +3. 「保存」をクリック +4. プロキシを再起動 + +> アドレス/ポートの変更には、先にプロキシサービスの停止が必要です + +### リスニングアドレスの説明 + +| アドレス | 説明 | +|------|------| +| `127.0.0.1` | ローカルマシンのみアクセス可能(推奨) | +| `0.0.0.0` | LAN からのアクセスを許可 | + +## 実行状態 + +プロキシ実行中、パネルには以下の情報が表示されます: + +### サービスアドレス + +``` +http://127.0.0.1:15721 +``` + +「コピー」ボタンでアドレスをコピーできます。 + +### 現在のプロバイダー + +各アプリが現在使用しているプロバイダーを表示: + +``` +Claude: PackyCode +Codex: AIGoCode +Gemini: Google 公式 +``` + +### 統計データ + +| 指標 | 説明 | +|------|------| +| アクティブ接続 | 現在処理中のリクエスト数 | +| 総リクエスト数 | 起動以来の総リクエスト数 | +| 成功率 | リクエスト成功の割合(>90% 緑、≤90% 黄) | +| 実行時間 | プロキシの稼働時間 | + +### フェイルオーバーキュー + +プロキシパネルにはアプリタイプごとにフェイルオーバーキューが表示されます: + +``` +Claude +├── 1. PackyCode [使用中] ● +├── 2. AIGoCode ● +└── 3. バックアップ ○ + +Codex +├── 1. AIGoCode [使用中] ● +└── 2. バックアップ ● +``` + +キューの説明: +- 数字は優先順位を示す +- 「使用中」ラベルは現在使用しているプロバイダーを示す +- ヘルスバッジはプロバイダーの状態を示す: + - 緑:健康(連続失敗 0 回) + - 黄:低下(連続失敗 1-2 回) + - 赤:不健康(連続失敗 ≥3 回) + +## 動作原理 + +### リクエストフロー + +```mermaid +sequenceDiagram + participant CLI as CLI ツール (Claude) + participant Proxy as ローカルプロキシ (CC Switch) + participant API as API プロバイダー (Anthropic) + participant DB as データストレージ (Logger) + + CLI->>Proxy: API リクエストを送信 + Proxy->>DB: リクエストログの記録/使用量の統計 + Proxy->>API: リクエストを転送 + API-->>Proxy: レスポンスを返却 + Proxy-->>CLI: レスポンスを返却 +``` + +### 設定の変更 + +プロキシを起動してアプリケーション接管を有効にすると、CC Switch はアプリの設定を変更します: + +**Claude**: +```json +{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:15721" + } +} +``` + +**Codex**: +```toml +base_url = "http://127.0.0.1:15721/v1" +``` + +**Gemini**: +``` +GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721 +``` + +## プロキシの停止 + +### 方法 1:メイン画面のスイッチ + +プロキシスイッチボタンをクリックしてオフにします。 + +### 方法 2:設定ページ + +プロキシサービスパネルでスイッチをオフにします。 + +### 停止後の処理 + +プロキシの停止時、CC Switch は以下を実行します: + +1. アプリの設定を元の状態に復元 +2. リクエストログを保存 +3. すべての接続を閉じる + +## ログ記録 + +### ログの有効化 + +プロキシパネルの「ログを有効化」スイッチをオンにします。 + +### ログの内容 + +各リクエスト記録には以下が含まれます: + +| フィールド | 説明 | +|------|------| +| 時間 | リクエスト時刻 | +| アプリ | Claude / Codex / Gemini | +| プロバイダー | 使用されたプロバイダー | +| モデル | リクエストされたモデル | +| Token | 入力/出力の Token 数 | +| レイテンシ | リクエストにかかった時間 | +| ステータス | 成功/失敗 | + +### ログの表示 + +「設定 → 使用量」タブでリクエストログを表示できます。 + +## よくある質問 + +### ポートが使用中 + +エラーメッセージ:`Address already in use` + +解決方法: +1. ポートを変更する(例:5001) +2. またはそのポートを使用しているプログラムを終了する + +### プロキシの起動に失敗する + +確認事項: +- ポートが使用中でないか +- 十分な権限があるか +- ファイアウォールがブロックしていないか + +### リクエストがタイムアウトする + +考えられる原因: +- ネットワークの問題 +- プロバイダーのサーバーの問題 +- プロキシ設定のエラー + +解決方法: +- ネットワーク接続を確認 +- プロバイダーの API に直接アクセスを試みる +- プロバイダーの設定を確認 diff --git a/docs/user-manual/ja/4-proxy/4.2-takeover.md b/docs/user-manual/ja/4-proxy/4.2-takeover.md new file mode 100644 index 000000000..51da39d34 --- /dev/null +++ b/docs/user-manual/ja/4-proxy/4.2-takeover.md @@ -0,0 +1,195 @@ +# 4.2 アプリケーション接管 + +## 機能説明 + +アプリケーション接管とは、CC Switch のプロキシが特定アプリの API リクエストを接管することです。 + +接管を有効にすると: +- アプリの API リクエストがローカルプロキシ経由で転送される +- リクエストログと使用量の統計を記録できる +- フェイルオーバー機能を使用できる + +## 前提条件 + +アプリケーション接管機能を使用する前に、プロキシサービスを起動する必要があります。 + +## 接管の有効化 + +### 操作場所 + +設定 → 詳細 → プロキシサービス → アプリケーション接管エリア + +### 操作手順 + +1. プロキシサービスが起動していることを確認 +2. 「アプリケーション接管」エリアを見つける +3. 必要なアプリのスイッチをオンにする + +### 接管スイッチ + +| スイッチ | 作用 | +|------|------| +| Claude 接管 | Claude Code のリクエストを接管 | +| Codex 接管 | Codex のリクエストを接管 | +| Gemini 接管 | Gemini CLI のリクエストを接管 | + +複数のアプリの接管を同時に有効にできます。 + +## 接管の仕組み + +### 設定の変更 + +接管を有効にすると、CC Switch はアプリの設定ファイルを変更し、API エンドポイントをローカルプロキシに向けます。 + +**Claude 設定の変更**: + +```json +// 接管前 +{ + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com" + } +} + +// 接管後 +{ + "env": { + "ANTHROPIC_BASE_URL": "http://127.0.0.1:15721" + } +} +``` + +**Codex 設定の変更**: + +```toml +# 接管前 +base_url = "https://api.openai.com/v1" + +# 接管後 +base_url = "http://127.0.0.1:15721/v1" +``` + +**Gemini 設定の変更**: + +```bash +# 接管前 +GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com + +# 接管後 +GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721 +``` + +### リクエストの転送 + +プロキシがリクエストを受信すると: + +1. リクエスト元を識別(Claude/Codex/Gemini) +2. そのアプリで現在有効なプロバイダーを検索 +3. プロバイダーの実際のエンドポイントにリクエストを転送 +4. リクエストログを記録 +5. アプリにレスポンスを返却 + +## 接管ステータスの表示 + +### メイン画面の表示 + +接管を有効にすると、メイン画面に以下の変化があります: + +- **プロキシ Logo の色**:無色から緑に変化 +- **プロバイダーカード**:現在アクティブなプロバイダーに緑の枠が表示 + +### プロバイダーカードの状態 + +| 状態 | 枠の色 | 説明 | +|------|----------|------| +| 現在有効 | 青 | 設定ファイル内のプロバイダー(非プロキシモード) | +| プロキシアクティブ | 緑 | プロキシが実際に使用しているプロバイダー | +| 通常 | デフォルト | 使用されていないプロバイダー | + +## 接管の無効化 + +### 操作手順 + +1. プロキシパネルで対応するアプリの接管スイッチをオフにする +2. またはプロキシサービスを直接停止 + +### 設定の復元 + +接管を無効にすると、CC Switch は以下を実行します: + +1. アプリの設定を接管前の状態に復元 +2. 現在のリクエストログを保存 + +## 接管とプロバイダーの切り替え + +### 接管モードでのプロバイダー切り替え + +接管モードでプロバイダーを切り替える場合: + +1. メイン画面でプロバイダーの「有効化」ボタンをクリック +2. プロキシが新しいプロバイダーを使用してリクエストを即座に転送 +3. **CLI ツールの再起動は不要** + +これが接管モードの大きなメリットです:プロバイダーの切り替えが即座に反映されます。 + +### 非接管モードでのプロバイダー切り替え + +非接管モードでプロバイダーを切り替える場合: + +1. 設定ファイルを変更 +2. CLI ツールの再起動が必要 + +## 複数アプリの接管 + +複数のアプリを同時に接管でき、それぞれ独立して管理されます: + +- 独立したプロバイダー設定 +- 独立したフェイルオーバーキュー +- 独立したリクエスト統計 + +## 使用シーン + +### シーン 1:使用量の監視 + +接管 + ログ記録を有効にして、API の使用状況を監視します。 + +### シーン 2:素早い切り替え + +接管を有効にすると、プロバイダーの切り替えに CLI ツールの再起動が不要になります。 + +### シーン 3:フェイルオーバー + +接管の有効化はフェイルオーバー機能を使用するための前提条件です。 + +## 注意事項 + +### パフォーマンスへの影響 + +プロキシにより少量のレイテンシ(通常 < 10ms)が追加されますが、ほとんどのシーンでは無視できます。 + +### ネットワーク要件 + +接管モードでは、CLI ツールがローカルプロキシアドレスにアクセスできる必要があります。 + +### 設定のバックアップ + +接管を有効にする前に、CC Switch は元の設定をバックアップし、無効化時に復元します。 + +## よくある質問 + +### 接管後にリクエストが失敗する + +確認事項: +- プロキシサービスが正常に実行されているか +- プロバイダーの設定が正しいか +- ネットワークが正常か + +### 接管を無効にしても設定が復元されない + +考えられる原因: +- プロキシの異常終了 +- 設定ファイルが他のプログラムに変更された + +解決方法: +- プロバイダーを手動で編集して保存し直す +- または接管を再度有効にしてから無効にする diff --git a/docs/user-manual/ja/4-proxy/4.3-failover.md b/docs/user-manual/ja/4-proxy/4.3-failover.md new file mode 100644 index 000000000..1cda143af --- /dev/null +++ b/docs/user-manual/ja/4-proxy/4.3-failover.md @@ -0,0 +1,232 @@ +# 4.3 フェイルオーバー + +## 機能説明 + +フェイルオーバー機能は、メインプロバイダーのリクエストが失敗した場合に、自動的にバックアッププロバイダーに切り替えてサービスの中断を防ぎます。 + +**適用シーン**: +- プロバイダーのサービスが不安定な場合 +- 高可用性が必要な場合 +- 長時間実行するタスク + +## 前提条件 + +フェイルオーバー機能を使用するには: + +1. プロキシサービスを起動 +2. アプリケーション接管を有効化 +3. フェイルオーバーキューを設定 +4. 自動フェイルオーバーを有効化 + +## フェイルオーバーキューの設定 + +### 設定ページを開く + +設定 → 詳細 → フェイルオーバー + +### アプリの選択 + +ページ上部に 3 つのタブがあります: +- Claude +- Codex +- Gemini + +設定するアプリを選択します。 + +### バックアッププロバイダーの追加 + +1. 「フェイルオーバーキュー」エリアで +2. 「プロバイダーを追加」をクリック +3. ドロップダウンリストからプロバイダーを選択 +4. プロバイダーがキューの末尾に追加 + +### 優先順位の調整 + +プロバイダーをドラッグして順序を調整: +- 番号が小さいほど優先度が高い +- メインプロバイダーが失敗すると、順番にバックアッププロバイダーを試行 + +### プロバイダーの削除 + +プロバイダーの右側にある「削除」ボタンをクリックします。 + +## メイン画面でのクイック操作 + +プロキシとフェイルオーバーがどちらも有効な場合、プロバイダーカードにフェイルオーバースイッチが表示されます。 + +### キューに追加 + +1. プロバイダーカードを見つける +2. フェイルオーバースイッチをオンにする +3. プロバイダーが自動的にキューに追加 + +### キューから削除 + +1. プロバイダーカードのフェイルオーバースイッチをオフにする +2. プロバイダーがキューから削除 + +## 自動フェイルオーバーの有効化 + +### 操作手順 + +1. フェイルオーバー設定ページで +2. 「自動フェイルオーバー」スイッチをオンにする + +### スイッチの説明 + +| 状態 | 動作 | +|------|------| +| オフ | 失敗を記録するのみ、自動切り替えなし | +| オン | 失敗時に自動的に次のプロバイダーに切り替え | + +## フェイルオーバーのフロー + +```mermaid +graph TD + Start[リクエストがプロキシに到達] --> Send[現在のプロバイダーに送信] + Send --> CheckSuccess{成功?} + CheckSuccess -- はい --> Return[レスポンスを返却] + CheckSuccess -- いいえ --> LogFail[失敗を記録] + LogFail --> CheckCircuit{サーキットブレーカーの状態確認} + CheckCircuit -- 発動中 --> Skip[このプロバイダーをスキップ] + CheckCircuit -- 未発動 --> IncFail[失敗カウントを増加] + Skip --> Next{キューに次がある?} + IncFail --> Next + Next -- あり --> Switch[プロバイダーを切り替え] + Switch --> Retry[リクエストをリトライ] + Retry --> Send + Next -- なし --> Error[エラーを返却] +``` + +## サーキットブレーカーの設定 + +サーキットブレーカーは、失敗したプロバイダーへの頻繁なリトライを防止します。 + +### 設定項目 + +アプリごとに独立したデフォルト設定があります。以下は共通のデフォルト値で、Claude には独自の緩やかな設定があります。 + +| 設定 | 説明 | 共通デフォルト | Claude デフォルト | 範囲 | +|------|------|--------|--------|------| +| 失敗閾値 | 連続何回失敗でサーキットブレーカーが発動 | 4 | 8 | 1-20 | +| 復旧成功閾値 | ハーフオープン状態で何回成功したら閉じるか | 2 | 3 | 1-10 | +| 復旧待機時間 | サーキットブレーカー発動後の復旧試行までの時間(秒) | 60 | 90 | 0-300 | +| エラー率閾値 | この値を超えるとサーキットブレーカーが発動 | 60% | 70% | 0-100% | +| 最小リクエスト数 | エラー率計算前の最小リクエスト数 | 10 | 15 | 5-100 | + +> Claude はリクエストに時間がかかるため、デフォルト設定はより緩やかで、多くの失敗を許容します。 + +### タイムアウト設定 + +| 設定 | 説明 | 共通デフォルト | Claude デフォルト | 範囲 | +|------|------|--------|--------|------| +| ストリーム初バイトタイムアウト | 最初のデータチャンクの最大待機時間(秒) | 60 | 90 | 1-120 | +| ストリームサイレントタイムアウト | データチャンク間の最大間隔(秒) | 120 | 180 | 60-600(0 で無効化) | +| 非ストリームタイムアウト | 非ストリームリクエストの総タイムアウト時間(秒) | 600 | 600 | 60-1200 | + +### リトライ設定 + +| 設定 | 説明 | 共通デフォルト | Claude デフォルト | 範囲 | +|------|------|--------|--------|------| +| 最大リトライ回数 | リクエスト失敗時のリトライ回数 | 3 | 6 | 0-10 | + +> Gemini のデフォルト最大リトライ回数は 5 です。 + +### サーキットブレーカーの状態 + +| 状態 | 説明 | +|------|------| +| 閉(Closed) | 正常状態、リクエストを許可 | +| 開(Open) | サーキットブレーカー発動中、このプロバイダーをスキップ | +| 半開(Half-Open) | 復旧試行中、探査リクエストを送信 | + +### 状態遷移 + +```mermaid +stateDiagram-v2 + [*] --> Closed: 初期化 + Closed --> Open: 失敗回数 >= 閾値 + Open --> HalfOpen: サーキットブレーカー期間満了 + HalfOpen --> Closed: 探査成功 (>= 復旧閾値) + HalfOpen --> Open: 探査失敗 +``` + +## ヘルスステータスの表示 + +### プロバイダーカード + +カードにヘルスステータスバッジが表示されます: + +| バッジ | 状態 | 説明 | +|------|------|------| +| 緑 | 健康 | 連続失敗回数 0 | +| 黄 | 警告 | 失敗はあるがサーキットブレーカー未発動 | +| 赤 | サーキットブレーカー発動 | 一時的にスキップ | + +### キューリスト + +フェイルオーバーキューにも各プロバイダーのヘルスステータスが表示されます。 + +## フェイルオーバーログ + +各フェイルオーバーの記録内容: + +| 情報 | 説明 | +|------|------| +| 時間 | 発生時刻 | +| 元のプロバイダー | 失敗したプロバイダー | +| 新しいプロバイダー | 切り替え先のプロバイダー | +| 失敗理由 | エラー情報 | + +使用量統計のリクエストログで確認できます。 + +## ベストプラクティス + +### キュー設定のアドバイス + +1. **メインプロバイダー**:最も安定で高速なプロバイダー +2. **第 1 バックアップ**:次善の選択 +3. **第 2 バックアップ**:最後の手段 + +### サーキットブレーカー設定のアドバイス + +| シーン | 失敗閾値 | サーキットブレーカー期間 | +|------|----------|----------| +| 高可用性要件 | 2 | 30 秒 | +| 一般的なシーン | 3 | 60 秒 | +| 偶発的な失敗を許容 | 5 | 120 秒 | + +### 監視のアドバイス + +定期的に確認: +- 各プロバイダーのヘルスステータス +- フェイルオーバーの発生頻度 +- サーキットブレーカーの発動状況 + +## よくある質問 + +### フェイルオーバーがトリガーされない + +確認事項: +1. プロキシサービスが実行中か +2. アプリケーション接管が有効か +3. 自動フェイルオーバーが有効か +4. キューにバックアッププロバイダーがあるか + +### フェイルオーバーが頻繁にトリガーされる + +考えられる原因: +- メインプロバイダーが不安定 +- ネットワークの問題 +- 設定のエラー + +解決方法: +- メインプロバイダーの状態を確認 +- サーキットブレーカーのパラメータを調整 +- メインプロバイダーの変更を検討 + +### すべてのプロバイダーがサーキットブレーカー発動中 + +サーキットブレーカー期間満了後に自動復旧を待つか、以下を実行: +1. プロキシサービスを手動で再起動 +2. サーキットブレーカーの状態をリセット diff --git a/docs/user-manual/ja/4-proxy/4.4-usage.md b/docs/user-manual/ja/4-proxy/4.4-usage.md new file mode 100644 index 000000000..2ae9141ea --- /dev/null +++ b/docs/user-manual/ja/4-proxy/4.4-usage.md @@ -0,0 +1,291 @@ +# 4.4 使用量統計 + +## 機能説明 + +使用量統計機能は、API リクエストデータを記録・分析して、以下をサポートします: + +- API の使用状況の把握 +- 費用支出の見積もり +- 使用パターンの分析 +- 問題のトラブルシューティング + +## 前提条件 + +使用量統計機能を使用するには: + +1. プロキシサービスを起動 +2. アプリケーション接管を有効化 +3. ログ記録を有効化 + +## 使用量統計を開く + +設定 → 使用量 タブ + +## 統計概要 + +### 集計カード + +ページ上部に主要指標が表示されます: + +| 指標 | 説明 | +|------|------| +| 総リクエスト数 | 統計期間内のリクエスト総数 | +| 総 Token | 入力 + 出力 Token の合計 | +| 推定費用 | 料金設定に基づいて計算された費用 | +| 成功率 | 成功したリクエストの割合 | + +### 期間 + +統計の期間を選択できます: + +| オプション | 範囲 | +|------|------| +| 今日 | 当日 00:00 から現在まで | +| 過去 7 日間 | 直近 7 日間 | +| 過去 30 日間 | 直近 30 日間 | + +![image-20260108011730105](../../assets/image-20260108011730105.png) + +## トレンドグラフ + +### リクエストトレンド + +折れ線グラフでリクエスト数の変化傾向を表示: + +- X 軸:時間 +- Y 軸:リクエスト数 +- 時間単位/日単位で表示可能 +- ズームとドラッグに対応 + +### Token トレンド + +Token 使用量の変化を表示: + +- 入力 Token(青)- ユーザーが送信した prompt の内容 +- 出力 Token(緑)- AI が生成した回答の内容 +- キャッシュ作成 Token(オレンジ)- 初回キャッシュ作成で消費された Token +- キャッシュヒット Token(紫)- キャッシュ再利用で節約された Token +- コスト(赤い破線、右側 Y 軸)- 推定費用 + +> **キャッシュ Token の説明**:Anthropic API は Prompt Caching 機能をサポートしています。キャッシュ作成時は高い料金(通常、入力価格の 1.25 倍)がかかりますが、その後のキャッシュヒット時は 0.1 倍の価格のみで、繰り返しリクエストのコストを大幅に削減できます。 + +### 時間粒度 + +- **今日**:時間単位で表示(24 データポイント) +- **7 日間/30 日間**:日単位で表示 + + + +![image-20260108011742847](../../assets/image-20260108011742847.png) + +## 詳細データ + +ページ下部に 3 つのデータタブがあります: + +### リクエストログ + +各リクエストの詳細記録: + +| フィールド | 説明 | +|------|------| +| 時間 | リクエスト時刻 | +| プロバイダー | 使用されたプロバイダー名 | +| モデル | リクエストされたモデル(課金モデル) | +| 入力 Token | 入力の Token 数 | +| 出力 Token | 出力の Token 数 | +| キャッシュ読取 | キャッシュヒットの Token 数 | +| キャッシュ作成 | キャッシュ作成の Token 数 | +| 総費用 | 推定費用(ドル) | +| 所要時間情報 | リクエスト時間、初回 Token 時間、ストリーム/非ストリーム | +| ステータス | HTTP ステータスコード | + +#### 所要時間情報の説明 + +所要時間情報列には複数のバッジが表示されます: + +| バッジ | 説明 | 色のルール | +|------|------|----------| +| 総所要時間 | リクエストの総時間(秒) | ≤5s 緑、≤120s オレンジ、>120s 赤 | +| 初回 Token | ストリームリクエストの最初の Token 時間 | ≤5s 緑、≤120s オレンジ、>120s 赤 | +| ストリーム/非ストリーム | リクエストタイプ | ストリーム:青、非ストリーム:紫 | + +#### 詳細の表示 + +リクエスト行をクリックすると詳細情報を表示: + +- 完全なリクエストパラメータ +- レスポンス内容のサマリー +- エラー情報(失敗した場合) + +#### ログのフィルタリング + +以下の条件でフィルタリングできます: + +| フィルタ項目 | オプション | +|--------|------| +| アプリタイプ | すべて / Claude / Codex / Gemini | +| ステータスコード | すべて / 200 / 400 / 401 / 429 / 500 | +| プロバイダー | テキスト検索 | +| モデル | テキスト検索 | +| 期間 | 開始時刻 - 終了時刻(日時ピッカー) | + +操作ボタン: +- **検索**:フィルタ条件を適用 +- **リセット**:デフォルトに戻す(過去 24 時間) +- **更新**:データを再読み込み + +![image-20260108011859974](../../assets/image-20260108011859974.png) + +### プロバイダー統計 + +プロバイダー別の集計データ: + +| フィールド | 説明 | +|------|------| +| プロバイダー | プロバイダー名 | +| リクエスト数 | そのプロバイダーの総リクエスト数 | +| 成功数 | 成功したリクエスト数 | +| 失敗数 | 失敗したリクエスト数 | +| 成功率 | 成功の割合 | +| 総 Token | Token 使用量の合計 | +| 推定費用 | そのプロバイダーの費用 | + +![image-20260108011907928](../../assets/image-20260108011907928.png) + +### モデル統計 + +モデル別の集計データ: + +| フィールド | 説明 | +|------|------| +| モデル | モデル名 | +| リクエスト数 | そのモデルの総リクエスト数 | +| 入力 Token | 入力 Token の合計 | +| 出力 Token | 出力 Token の合計 | +| 平均レイテンシ | 平均応答時間 | +| 推定費用 | そのモデルの費用 | + +![image-20260108011915381](../../assets/image-20260108011915381.png) + +## 料金設定 + +### 料金設定を開く + +設定 → 詳細 → 料金設定 + +### モデル価格の設定 + +各モデルの価格を設定(100 万 Token あたり): + +| フィールド | 説明 | +|------|------| +| モデル ID | モデル識別子(例:claude-3-sonnet) | +| 表示名 | カスタム表示名 | +| 入力価格 | 100 万入力 Token あたりの価格 | +| 出力価格 | 100 万出力 Token あたりの価格 | +| キャッシュ読取価格 | 100 万キャッシュヒット Token あたりの価格 | +| キャッシュ作成価格 | 100 万キャッシュ作成 Token あたりの価格 | + +### 操作 + +- **追加**:「追加」ボタンで新しいモデル価格を追加 +- **編集**:行末の編集アイコンで変更 +- **削除**:行末の削除アイコンで削除 + +![image-20260108011933565](../../assets/image-20260108011933565.png) + +### プリセット価格 + +CC Switch は一般的なモデルの公式価格(100 万 Token あたり)をプリセットしています: + +**Claude シリーズ(ドル)**: + +| モデル | 入力 | 出力 | キャッシュ読取 | キャッシュ作成 | +|------|------|------|----------|----------| +| **Claude 4.5 シリーズ** | | | | | +| claude-opus-4-5 | $5 | $25 | $0.50 | $6.25 | +| claude-sonnet-4-5 | $3 | $15 | $0.30 | $3.75 | +| claude-haiku-4-5 | $1 | $5 | $0.10 | $1.25 | +| **Claude 4 シリーズ** | | | | | +| claude-opus-4 | $15 | $75 | $1.50 | $18.75 | +| claude-opus-4-1 | $15 | $75 | $1.50 | $18.75 | +| claude-sonnet-4 | $3 | $15 | $0.30 | $3.75 | +| **Claude 3.5 シリーズ** | | | | | +| claude-3-5-sonnet | $3 | $15 | $0.30 | $3.75 | +| claude-3-5-haiku | $0.80 | $4 | $0.08 | $1.00 | + +**OpenAI シリーズ / Codex(ドル)**: + +| モデル | 入力 | 出力 | キャッシュ読取 | +|------|------|------|----------| +| **GPT-5.2 シリーズ** | | | | +| gpt-5.2 | $1.75 | $14 | $0.175 | +| **GPT-5.1 シリーズ** | | | | +| gpt-5.1 | $1.25 | $10 | $0.125 | +| **GPT-5 シリーズ** | | | | +| gpt-5 | $1.25 | $10 | $0.125 | + +> 注:Codex プリセットには low/medium/high などの変種が含まれており、価格はベースモデルと同一です。 + +**Gemini シリーズ(ドル)**: + +| モデル | 入力 | 出力 | キャッシュ読取 | +|------|------|------|----------| +| **Gemini 3 シリーズ** | | | | +| gemini-3-pro-preview | $2 | $12 | $0.20 | +| gemini-3-flash-preview | $0.50 | $3 | $0.05 | +| **Gemini 2.5 シリーズ** | | | | +| gemini-2.5-pro | $1.25 | $10 | $0.125 | +| gemini-2.5-flash | $0.30 | $2.50 | $0.03 | + +**中国メーカーのモデル(人民元)**: + +| モデル | 入力 | 出力 | キャッシュ読取 | +|------|------|------|----------| +| **DeepSeek** | | | | +| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 | +| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 | +| deepseek-v3 | ¥2.00 | ¥8.00 | ¥0.40 | +| **Kimi (月之暗面)** | | | | +| kimi-k2-thinking | ¥4.00 | ¥16.00 | ¥1.00 | +| kimi-k2 | ¥4.00 | ¥16.00 | ¥1.00 | +| kimi-k2-turbo | ¥8.00 | ¥58.00 | ¥1.00 | +| **MiniMax** | | | | +| minimax-m2.1 | ¥2.10 | ¥8.40 | ¥0.21 | +| minimax-m2.1-lightning | ¥2.10 | ¥16.80 | ¥0.21 | +| **その他** | | | | +| glm-4.7 | ¥2.00 | ¥8.00 | ¥0.40 | +| doubao-seed-code | ¥1.20 | ¥8.00 | ¥0.24 | +| mimo-v2-flash | 無料 | 無料 | - | + +### カスタム価格 + +中継サービスを使用する場合、価格が異なる場合があります: + +1. 「編集」ボタンをクリック +2. 価格を変更 +3. 保存 + +## よくある質問 + +### 統計データが空 + +確認事項: +- プロキシサービスが実行中か +- アプリケーション接管が有効か +- ログ記録が有効か +- プロキシ経由でリクエストがあったか + +### 費用の見積もりが不正確 + +考えられる原因: +- 料金設定が実際と異なる +- 中継サービスの特別な料金体系を使用 + +解決方法: +- 料金設定を更新 +- プロバイダーの実際の請求書を参照 + +### Token 数がプロバイダーと一致しない + +CC Switch は独自の方法で Token 数を推定しており、プロバイダーの計算方法と若干の差異が生じる場合があります。プロバイダーの請求書を基準にしてください。 diff --git a/docs/user-manual/ja/4-proxy/4.5-model-test.md b/docs/user-manual/ja/4-proxy/4.5-model-test.md new file mode 100644 index 000000000..a1e0fa7c7 --- /dev/null +++ b/docs/user-manual/ja/4-proxy/4.5-model-test.md @@ -0,0 +1,156 @@ +# 4.5 モデルテスト + +## 機能説明 + +モデルテスト機能は、プロバイダーに設定されたモデルが使用可能かどうかを確認するために、実際の API リクエストを送信してテストします: + +- モデルが存在するか +- API Key が有効か +- エンドポイントが正常に応答するか +- 応答レイテンシが正常か + +## 設定を開く + +設定 → 詳細 → モデルテスト + +## テストモデルの設定 + +各アプリのテスト用モデルを設定します: + +| アプリ | 設定項目 | デフォルト値 | 説明 | +|------|--------|--------|------| +| Claude | Claude モデル | システムデフォルト | Haiku シリーズの使用を推奨(低コスト・高速) | +| Codex | Codex モデル | システムデフォルト | mini シリーズの使用を推奨 | +| Gemini | Gemini モデル | システムデフォルト | Flash シリーズの使用を推奨 | + +### モデル選択のアドバイス + +テストモデルを選択する際の考慮事項: + +1. **コスト**:低価格のモデルを選択(例:Haiku、Mini、Flash) +2. **速度**:応答が速いモデルを選択 +3. **可用性**:プロバイダーがサポートしているモデルを選択 + +## テストパラメータの設定 + +### タイムアウト時間 + +| パラメータ | 説明 | デフォルト値 | 範囲 | +|------|------|--------|------| +| タイムアウト時間 | 1 回のリクエストのタイムアウト | 45 秒 | 10-120 秒 | + +短すぎると誤判定の可能性があり、長すぎると障害検出が遅れます。 + +### リトライ回数 + +| パラメータ | 説明 | デフォルト値 | 範囲 | +|------|------|--------|------| +| 最大リトライ | 失敗時のリトライ回数 | 2 回 | 0-5 回 | + +ネットワークが不安定な場合はリトライ回数を増やすことを推奨します。 + +### デグレード閾値 + +| パラメータ | 説明 | デフォルト値 | 範囲 | +|------|------|--------|------| +| デグレード閾値 | この時間を超えるとデグレードとマーク | 6000ms | 1000-30000ms | + +閾値を超えたプロバイダーは「デグレード」状態としてマークされますが、引き続き使用可能です。 + +## モデルテストの実行 + +### 手動テスト + +プロバイダーカードの「テスト」ボタンをクリックします: + +1. 設定されたエンドポイントにテストリクエストを送信 +2. 設定されたテストモデルを使用 +3. レスポンスまたはタイムアウトを待機 +4. テスト結果を表示 + +### テスト内容 + +テストリクエストは: +- 短い prompt(例:"Hi")を送信 +- 最大出力 Token を制限(通常 10-50) +- ストリームレスポンスで初バイト時間を検出 + +## テスト結果 + +### ヘルスステータス + +| ステータス | アイコン | 説明 | +|------|------|------| +| 健康 | 緑 | レスポンス正常、レイテンシが閾値内 | +| デグレード | 黄 | レスポンス正常だが、レイテンシが閾値超過 | +| 利用不可 | 赤 | リクエスト失敗またはタイムアウト | + +### 結果情報 + +テスト完了後に表示: +- 応答レイテンシ(ミリ秒) +- 初バイト時間(TTFB) +- エラー情報(失敗した場合) + +## フェイルオーバーとの連携 + +モデルテストはフェイルオーバー機能と連携して使用します: + +### ヘルスチェック + +プロキシサービスを有効にすると、システムはフェイルオーバーキュー内のプロバイダーに対して定期的にヘルスチェックを実行します: + +1. 設定されたテストモデルでリクエストを送信 +2. レスポンスに基づいてヘルスステータスを更新 +3. 不健康なプロバイダーは一時的にスキップ + +### サーキットブレーカーからの復旧 + +プロバイダーがサーキットブレーカー状態から復旧する際: + +1. モデルテストで可用性を確認 +2. テスト合格後、正常状態に復旧 +3. テスト不合格の場合、サーキットブレーカーを継続 + +## よくある質問 + +### テストは失敗するが実際には使用可能 + +**考えられる原因**: +- テストモデルと実際に使用するモデルが異なる +- プロバイダーが設定されたテストモデルをサポートしていない + +**解決方法**: +- テストモデルをプロバイダーがサポートするモデルに変更 +- プロバイダーのモデルリストを確認 + +### レイテンシが高すぎる + +**考えられる原因**: +- ネットワークレイテンシ +- プロバイダーのサーバー負荷が高い +- モデルの応答が遅い + +**解決方法**: +- より高速なテストモデルを使用 +- デグレード閾値を調整 +- ミラーエンドポイントの使用を検討 + +### 頻繁にタイムアウトする + +**考えられる原因**: +- タイムアウト時間の設定が短すぎる +- ネットワークが不安定 +- プロバイダーのサービスが不安定 + +**解決方法**: +- タイムアウト時間を延長 +- リトライ回数を増加 +- ネットワーク接続を確認 + +## 注意事項 + +- モデルテストは少量の API 枠を消費します +- テストには低コストのモデルの使用を推奨 +- テスト頻度は高すぎないように、枠の浪費を避けてください +- プロバイダーごとにサポートするモデルが異なる場合があります diff --git a/docs/user-manual/ja/5-faq/5.1-config-files.md b/docs/user-manual/ja/5-faq/5.1-config-files.md new file mode 100644 index 000000000..135a0fc56 --- /dev/null +++ b/docs/user-manual/ja/5-faq/5.1-config-files.md @@ -0,0 +1,340 @@ +# 5.1 設定ファイルの説明 + +## CC Switch のデータストレージ + +### ストレージディレクトリ + +デフォルトの場所:`~/.cc-switch/` + +設定で場所をカスタマイズ可能です(クラウド同期用)。 + +### ディレクトリ構造 + +``` +~/.cc-switch/ +├── cc-switch.db # SQLite データベース +├── settings.json # デバイスレベルの設定 +└── backups/ # 自動バックアップ + ├── backup-20251230-120000.json + ├── backup-20251229-180000.json + └── ... +``` + +### データベースの内容 + +`cc-switch.db` は SQLite データベースで、以下を保存しています: + +| テーブル | 内容 | +|-----|------| +| providers | プロバイダー設定 | +| provider_endpoints | プロバイダーエンドポイント候補リスト | +| mcp_servers | MCP サーバー設定 | +| prompts | プロンプトプリセット | +| skills | スキルのインストール状態 | +| skill_repos | スキルリポジトリ設定 | +| proxy_config | プロキシ設定 | +| proxy_request_logs | プロキシリクエストログ | +| provider_health | プロバイダーヘルスステータス | +| model_pricing | モデル料金 | +| settings | アプリ設定 | + +### デバイス設定 + +`settings.json` はデバイスレベルの設定を保存します: + +```json +{ + "language": "zh", + "theme": "system", + "windowBehavior": "minimize", + "autoStart": false, + "claudeConfigDir": null, + "codexConfigDir": null, + "geminiConfigDir": null, + "opencodeConfigDir": null, + "openclawConfigDir": null +} +``` + +これらの設定はデバイス間で同期されません。 + +### 自動バックアップ + +`backups/` ディレクトリに自動バックアップが保存されます: + +- 設定インポートのたびに自動作成 +- 最新の 10 件のバックアップを保持 +- ファイル名にタイムスタンプを含む + +## Claude Code の設定 + +### 設定ディレクトリ + +デフォルト:`~/.claude/` + +### 主要ファイル + +``` +~/.claude/ +├── settings.json # メイン設定ファイル +├── CLAUDE.md # システムプロンプト +└── skills/ # スキルディレクトリ + └── ... +``` + +### settings.json + +```json +{ + "env": { + "ANTHROPIC_API_KEY": "sk-xxx", + "ANTHROPIC_BASE_URL": "https://api.anthropic.com" + }, + "permissions": { + "allow_file_access": true + } +} +``` + +| フィールド | 説明 | +|------|------| +| `env.ANTHROPIC_API_KEY` | API キー | +| `env.ANTHROPIC_BASE_URL` | API エンドポイント(任意) | +| `env.ANTHROPIC_AUTH_TOKEN` | 代替認証方式 | + +### MCP 設定 + +MCP サーバーの設定は `~/.claude.json` にあります: + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +## Codex の設定 + +### 設定ディレクトリ + +デフォルト:`~/.codex/` + +### 主要ファイル + +``` +~/.codex/ +├── auth.json # 認証設定 +├── config.toml # メイン設定 + MCP +└── AGENTS.md # システムプロンプト +``` + +### auth.json + +```json +{ + "OPENAI_API_KEY": "sk-xxx" +} +``` + +### config.toml + +```toml +# 基本設定 +base_url = "https://api.openai.com/v1" +model = "gpt-4" + +# MCP サーバー +[mcp_servers.mcp-fetch] +command = "uvx" +args = ["mcp-server-fetch"] +``` + +## Gemini CLI の設定 + +### 設定ディレクトリ + +デフォルト:`~/.gemini/` + +### 主要ファイル + +``` +~/.gemini/ +├── .env # 環境変数(API Key) +├── settings.json # メイン設定 + MCP +└── GEMINI.md # システムプロンプト +``` + +### .env + +```bash +GEMINI_API_KEY=xxx +GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com +GEMINI_MODEL=gemini-pro +``` + +### settings.json + +```json +{ + "mcpServers": { + "mcp-fetch": { + "command": "uvx", + "args": ["mcp-server-fetch"] + } + } +} +``` + +| フィールド | 説明 | +|------|------| +| `mcpServers` | MCP サーバー設定 | + +## OpenCode の設定 + +### 設定ディレクトリ + +デフォルト:`~/.opencode/` + +### 主要ファイル + +``` +~/.opencode/ +├── config.json # メイン設定ファイル +├── AGENTS.md # システムプロンプト +└── skills/ # スキルディレクトリ + └── ... +``` + +## OpenClaw の設定 + +### 設定ディレクトリ + +デフォルト:`~/.openclaw/` + +### 主要ファイル + +``` +~/.openclaw/ +├── openclaw.json # メイン設定ファイル(JSON5 形式) +├── AGENTS.md # システムプロンプト +└── skills/ # スキルディレクトリ + └── ... +``` + +### openclaw.json + +OpenClaw は JSON5 形式の設定ファイルを使用し、主に以下のセクションを含みます: + +```json5 +{ + // モデルプロバイダー設定 + models: { + mode: "merge", + providers: { + "custom-provider": { + baseUrl: "https://api.example.com/v1", + apiKey: "your-api-key", + api: "openai-completions", + models: [{ id: "model-id", name: "Model Name" }] + } + } + }, + // 環境変数 + env: { + ANTHROPIC_API_KEY: "sk-..." + }, + // Agent デフォルトモデル設定 + agents: { + defaults: { + model: { + primary: "provider/model" + } + } + }, + // ツール設定 + tools: {}, + // ワークスペースファイル設定 + workspace: {} +} +``` + +| フィールド | 説明 | +|------|------| +| `models.providers` | プロバイダー設定(CC Switch の「プロバイダー」にマッピング) | +| `env` | 環境変数設定 | +| `agents.defaults` | Agent デフォルトモデル設定 | +| `tools` | ツール設定 | +| `workspace` | ワークスペースファイル管理 | + +## 設定の優先順位 + +CC Switch が設定を変更する際の優先順位: + +1. **CC Switch データベース** - 単一事実源 (SSOT) +2. **Live 設定ファイル** - プロバイダー切り替え時に書き込み +3. **バックフィル機能** - 現在のプロバイダーの編集時に Live ファイルから読み取り + +## 手動での設定編集 + +### 手動編集可能なもの + +- CLI ツールの設定ファイル(CC Switch がバックフィルする) +- CC Switch の `settings.json` + +### 手動編集を推奨しないもの + +- `cc-switch.db` データベースファイル +- バックアップファイル + +### 編集後の同期 + +CLI ツールの設定を手動で編集した場合: + +1. CC Switch を開く +2. 対応するプロバイダーを編集 +3. 手動変更の内容がバックフィルされていることを確認 +4. 保存してデータベースに同期 + +## 設定の移行 + +### 旧バージョンからの移行 + +CC Switch v3.7.0 で JSON ファイルから SQLite に移行しました: + +- 初回起動時に自動的に移行 +- 移行成功後に通知を表示 +- 旧設定ファイルはバックアップとして保持 + +### デバイス間の移行 + +1. 移行元のデバイスで設定をエクスポート +2. 移行先のデバイスで設定をインポート +3. またはクラウド同期機能を使用 + +## 設定のバックアップに関するアドバイス + +### 定期的なバックアップ + +定期的に設定をエクスポートすることを推奨します: + +1. 設定 → 詳細 → データ管理 +2. 「エクスポート」をクリック +3. 安全な場所に保存 + +### バックアップに含まれる内容 + +エクスポートファイルには以下が含まれます: + +- すべてのプロバイダー設定 +- MCP サーバー設定 +- Prompts プリセット +- アプリ設定 + +### 含まれない内容 + +- 使用量ログ(データ量が大きいため) +- デバイスレベルの設定(デバイス間の移動に適さないため) diff --git a/docs/user-manual/ja/5-faq/5.2-questions.md b/docs/user-manual/ja/5-faq/5.2-questions.md new file mode 100644 index 000000000..ccb765023 --- /dev/null +++ b/docs/user-manual/ja/5-faq/5.2-questions.md @@ -0,0 +1,220 @@ +# 5.2 よくある質問 FAQ + +## インストールに関する問題 + +### macOS で「不明な開発者」と表示される + +**問題**:初回起動時に「開けません。身元不明の開発者のものです」と表示される + +**解決方法 1**:システム設定から +1. 警告ダイアログを閉じる +2. 「システム設定」→「プライバシーとセキュリティ」を開く +3. CC Switch に関する表示を見つける +4. 「このまま開く」をクリック +5. 再度アプリを開く + +**解決方法 2**:ターミナルコマンドから(推奨) +```bash +sudo xattr -dr com.apple.quarantine /Applications/CC\ Switch.app/ +``` + +実行後、正常にアプリを開けるようになります。 + +### Windows でインストール後に起動できない + +**考えられる原因**: +- WebView2 ランタイムが不足 +- ウイルス対策ソフトによるブロック + +**解決方法**: +1. [Microsoft Edge WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) をインストール +2. CC Switch をウイルス対策ソフトのホワイトリストに追加 + +### Linux で起動エラー + +**問題**:AppImage が起動しない + +**解決方法**: +```bash +# 実行権限を追加 +chmod +x CC-Switch-*.AppImage + +# それでも失敗する場合 +./CC-Switch-*.AppImage --no-sandbox +``` + +## プロバイダーに関する問題 + +### プロバイダーを切り替えても反映されない + +**原因**:CLI ツールが設定を再読み込みする必要がある + +**解決方法**: +- Claude Code:ターミナルを閉じて再度開く、または IDE を再起動 +- Codex:ターミナルを閉じて再度開く +- Gemini:トレイからの切り替えで即時反映、再起動不要 + +### API Key が無効 + +**確認手順**: +1. API Key が正しくコピーされているか(余分なスペースがないか) +2. API Key が期限切れでないか +3. エンドポイントアドレスが正しいか +4. 速度テストで接続を確認 + +### 公式ログインに戻すには + +**操作手順**: +1. 「公式ログイン」プリセット(Claude/Codex)または「Google 公式」プリセット(Gemini)を選択 +2. 「有効化」をクリック +3. 対応する CLI ツールを再起動 +4. CLI ツールのログインフローに従って操作 + +## プロキシに関する問題 + +### プロキシサービスの起動に失敗する + +**考えられる原因**:ポートが使用中 + +**解決方法**: +1. ポートの使用状況を確認: + ```bash + # macOS/Linux + lsof -i :49152 + + # Windows + netstat -ano | findstr :49152 + ``` +2. ポートを使用しているプログラムを終了 +3. または設定を変更してデフォルトポートに復旧: + - 「設定 → プロキシサービス」を開く + - 「デフォルトに戻す」ボタンをクリック + +### プロキシモードでリクエストがタイムアウトする + +**考えられる原因**: +- ネットワークの問題 +- プロバイダーのサーバーの問題 +- プロキシ設定のエラー + +**解決方法**: +1. ネットワーク接続を確認 +2. プロバイダーの API に直接アクセスを試みる(プロキシを無効にして) +3. プロバイダーの設定が正しいか確認 + +### プロキシを無効にしても設定が復元されない + +**考えられる原因**:プロキシの異常終了 + +**解決方法**: +1. 現在のプロバイダーを編集 +2. エンドポイントアドレスが正しいか確認 +3. 保存して設定を更新 + +## フェイルオーバーに関する問題 + +### フェイルオーバーがトリガーされない + +**チェックリスト**: +- [ ] プロキシサービスが実行中か +- [ ] アプリケーション接管が有効か +- [ ] 自動フェイルオーバーが有効か +- [ ] キューにバックアッププロバイダーがあるか + +### フェイルオーバーが頻繁にトリガーされる + +**考えられる原因**: +- メインプロバイダーが不安定 +- サーキットブレーカーの閾値が低すぎる + +**解決方法**: +1. メインプロバイダーの状態を確認 +2. 失敗閾値を引き上げる(例:3 → 5) +3. メインプロバイダーの変更を検討 + +### すべてのプロバイダーがサーキットブレーカー発動中 + +**解決方法**: +1. サーキットブレーカー期間満了を待つ(デフォルト 60 秒) +2. またはプロキシサービスを再起動して状態をリセット + +## データに関する問題 + +### 設定が消えた + +**考えられる原因**: +- 設定ディレクトリが削除された +- データベースが破損 + +**解決方法**: +1. `~/.cc-switch/` ディレクトリが存在するか確認 +2. バックアップから復元:`~/.cc-switch/backups/` +3. または以前にエクスポートした設定ファイルからインポート + +### 設定のインポートに失敗する + +**考えられる原因**: +- ファイル形式のエラー +- バージョンの非互換性 + +**解決方法**: +1. ファイルが CC Switch からエクスポートされた JSON ファイルであることを確認 +2. ファイル内容が完全であるか確認 +3. テキストエディタで開いてフォーマットを確認 + +### 使用量統計のデータが空 + +**チェックリスト**: +- [ ] プロキシサービスが実行中か +- [ ] アプリケーション接管が有効か +- [ ] ログ記録が有効か +- [ ] プロキシ経由でリクエストがあったか + +## その他の問題 + +### トレイアイコンが表示されない + +**macOS**: +- システム設定のメニューバーアイコン設定を確認 + +**Windows**: +- タスクバーの設定で、CC Switch のアイコンが非表示になっていないか確認 + +**Linux**: +- システムトレイのサポート(例:`libappindicator`)がインストールされている必要あり + +### インターフェースの表示が異常 + +**解決方法**: +1. テーマを切り替えてみる(ライト/ダーク) +2. アプリを再起動 +3. `~/.cc-switch/settings.json` を削除して設定をリセット + +### 更新に失敗する + +**解決方法**: +1. ネットワーク接続を確認 +2. 最新版を手動でダウンロードしてインストール +3. Homebrew を使用する場合:`brew upgrade --cask cc-switch` + +## ヘルプの入手 + +### Issue の提出 + +上記の方法で問題が解決しない場合: + +1. [GitHub Issues](https://github.com/farion1231/cc-switch/issues) にアクセス +2. 類似の問題がないか検索 +3. なければ新しい Issue を作成 +4. 以下の情報を提供: + - オペレーティングシステムとバージョン + - CC Switch のバージョン + - 問題の説明と再現手順 + - エラーメッセージ(ある場合) + +### ログファイル + +Issue を提出する際にログファイルを添付できます: + +- macOS/Linux:`~/.cc-switch/logs/` +- Windows:`%APPDATA%\cc-switch\logs\` diff --git a/docs/user-manual/ja/5-faq/5.3-deeplink.md b/docs/user-manual/ja/5-faq/5.3-deeplink.md new file mode 100644 index 000000000..e2f643ebc --- /dev/null +++ b/docs/user-manual/ja/5-faq/5.3-deeplink.md @@ -0,0 +1,256 @@ +# 5.3 ディープリンクプロトコル + +## 機能説明 + +CC Switch は `ccswitch://` ディープリンクプロトコルをサポートしており、リンクからワンクリックで設定をインポートできます。 + +**使用シーン**: +- チーム内での設定共有 +- チュートリアルでのワンクリック設定 +- デバイス間の素早い同期 + +## オンライン生成ツール + +CC Switch はオンラインのディープリンク生成ツールを提供しています: + +**アクセス先**:[https://farion1231.github.io/cc-switch/deplink.html](https://farion1231.github.io/cc-switch/deplink.html) + +### 使用方法 + +1. 上記の Web ページを開く +2. インポートタイプを選択(プロバイダー/MCP/Prompt) +3. 設定情報を入力 +4. 「リンクを生成」をクリック +5. 生成されたディープリンクをコピー +6. 他の人に共有するか、別のデバイスで使用 + +## プロトコル形式 + +### V1 プロトコル + +URL パラメータ形式で、読みやすく生成しやすい形式です: + +``` +ccswitch://v1/import?resource={type}&app={app}&name={name}&... +``` + +**共通パラメータ**: + +| パラメータ | 必須 | 説明 | +|------|------|------| +| `resource` | はい | リソースタイプ:`provider` / `mcp` / `prompt` / `skill` | +| `app` | はい | アプリタイプ:`claude` / `codex` / `gemini` / `opencode` / `openclaw` | +| `name` | はい | 名前 | + +**プロバイダーパラメータ**(resource=provider): + +| パラメータ | 必須 | 説明 | +|------|------|------| +| `endpoint` | いいえ | API エンドポイントアドレス(カンマ区切りで複数 URL 対応) | +| `apiKey` | いいえ | API キー | +| `homepage` | いいえ | プロバイダー公式サイト | +| `model` | いいえ | デフォルトモデル | +| `haikuModel` | いいえ | Haiku モデル(Claude のみ) | +| `sonnetModel` | いいえ | Sonnet モデル(Claude のみ) | +| `opusModel` | いいえ | Opus モデル(Claude のみ) | +| `notes` | いいえ | メモ | +| `icon` | いいえ | アイコン | +| `config` | いいえ | Base64 エンコードされた設定内容 | +| `configFormat` | いいえ | 設定形式:`json` / `toml` | +| `configUrl` | いいえ | リモート設定 URL | +| `enabled` | いいえ | 有効にするかどうか(ブール値) | +| `usageScript` | いいえ | 使用量クエリスクリプト | +| `usageEnabled` | いいえ | 使用量クエリを有効にするか(デフォルト true) | +| `usageApiKey` | いいえ | 使用量クエリ専用 API Key | +| `usageBaseUrl` | いいえ | 使用量クエリ専用アドレス | +| `usageAccessToken` | いいえ | 使用量クエリアクセストークン | +| `usageUserId` | いいえ | 使用量クエリユーザー ID | +| `usageAutoInterval` | いいえ | 自動クエリ間隔(分) | + +**プロンプトパラメータ**(resource=prompt): + +| パラメータ | 必須 | 説明 | +|------|------|------| +| `content` | はい | プロンプト内容 | +| `description` | いいえ | 説明 | +| `enabled` | いいえ | 有効にするかどうか(ブール値) | + +**MCP パラメータ**(resource=mcp): + +| パラメータ | 必須 | 説明 | +|------|------|------| +| `apps` | はい | アプリリスト(カンマ区切り、例:`claude,codex,gemini,opencode`) | +| `config` | はい | MCP サーバー設定(JSON 形式) | +| `enabled` | いいえ | 有効にするかどうか(ブール値) | + +**Skill パラメータ**(resource=skill): + +| パラメータ | 必須 | 説明 | +|------|------|------| +| `repo` | はい | リポジトリ(形式:`owner/name`) | +| `directory` | いいえ | ディレクトリパス | +| `branch` | いいえ | Git ブランチ | + +**例**: +``` +ccswitch://v1/import?resource=provider&app=claude&name=My%20Provider&endpoint=https%3A%2F%2Fapi.example.com&apiKey=sk-xxx +``` + +## インポートタイプの例 + +### プロバイダーのインポート + +``` +ccswitch://v1/import?resource=provider&app=claude&name=My%20Provider&endpoint=https%3A%2F%2Fapi.example.com&apiKey=sk-xxx +``` + +### MCP サーバーのインポート + +``` +ccswitch://v1/import?resource=mcp&apps=claude,codex&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D&name=mcp-fetch +``` + +### Prompt プリセットのインポート + +``` +ccswitch://v1/import?resource=prompt&app=claude&name=%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5&content=%23%20%E8%A7%92%E8%89%B2%0A%E4%BD%A0%E6%98%AF%E4%B8%80%E4%B8%AA%E4%B8%93%E4%B8%9A%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E4%B8%93%E5%AE%B6 +``` + +### Skill のインポート + +``` +ccswitch://v1/import?resource=skill&name=my-skill&repo=owner/repo&directory=skills/my-skill&branch=main +``` + +## ディープリンクの生成 + +### 手動生成 + +1. パラメータを準備 +2. V1 プロトコル形式で URL を組み立て +3. 特殊文字を URL エンコード + +**例**: + +```javascript +const params = new URLSearchParams({ + resource: 'provider', + app: 'claude', + name: 'My Provider', + endpoint: 'https://api.example.com', + apiKey: 'sk-xxx' +}); + +const url = `ccswitch://v1/import?${params.toString()}`; +``` + +### オンラインツール + +CC Switch 公式のオンラインディープリンク生成ツールを使用するとより便利です。 + +## ディープリンクの使用 + +### リンクのクリック + +ブラウザや他のアプリでディープリンクをクリック: + +1. システムが CC Switch を開くかどうかを確認 +2. 確認後、CC Switch が起動 +3. インポート確認ダイアログを表示 +4. インポートを確認 + +### インポートの確認 + +インポート前に確認ダイアログが表示され、以下が含まれます: + +- インポートタイプ +- 設定のプレビュー +- 確認/キャンセルボタン + +**セキュリティ上の注意**:信頼できるソースからの設定のみインポートしてください。 + +## プロトコルの登録 + +### 自動登録 + +CC Switch のインストール時に `ccswitch://` プロトコルが自動登録されます。 + +### 手動登録 + +プロトコルが正しく登録されていない場合: + +**macOS**: +アプリを再インストールするか、以下を実行: +```bash +/usr/bin/open -a "CC Switch" --args --register-protocol +``` + +**Windows**: +アプリを再インストールするか、レジストリを確認: +``` +HKEY_CLASSES_ROOT\ccswitch +``` + +**Linux**: +`.desktop` ファイルの `MimeType` 設定を確認。 + +## セキュリティに関する考慮事項 + +### 機密情報 + +ディープリンクには機密情報(API Key など)が含まれる場合があります: + +- API Key を含むリンクを公開の場で共有しない +- 共有前に機密情報を削除または置換 +- 安全なチャネルでリンクを送信 + +### ソースの確認 + +インポート前に CC Switch は以下を実行します: + +1. データ形式の検証 +2. 設定のプレビュー表示 +3. ユーザーの確認を要求 + +### 悪意のあるリンクからの防護 + +CC Switch は以下を確認します: + +- データ形式が正当か +- 必須フィールドが揃っているか +- 設定値が妥当な範囲内か + +## サンプルリンク + +### 例:Claude プロバイダーのインポート + +``` +ccswitch://v1/import?resource=provider&app=claude&name=Test%20Provider&apiKey=sk-xxx&endpoint=https%3A%2F%2Fapi.example.com +``` + +### 例:MCP サーバーのインポート + +``` +ccswitch://v1/import?resource=mcp&name=mcp-fetch&apps=claude,codex,gemini&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D +``` + +## トラブルシューティング + +### リンクが開けない + +**確認事項**: +1. CC Switch がインストールされているか +2. プロトコルが正しく登録されているか +3. リンクの形式が正しいか + +### インポートに失敗する + +**考えられる原因**: +- Base64 エンコードのエラー +- JSON 形式のエラー +- 必須フィールドの不足 + +**解決方法**: +1. 元の JSON 形式を確認 +2. Base64 エンコードをやり直す +3. すべての必須フィールドが存在することを確認 diff --git a/docs/user-manual/ja/5-faq/5.4-env-conflict.md b/docs/user-manual/ja/5-faq/5.4-env-conflict.md new file mode 100644 index 000000000..6d6d5a1e1 --- /dev/null +++ b/docs/user-manual/ja/5-faq/5.4-env-conflict.md @@ -0,0 +1,108 @@ +# 5.4 環境変数の競合 + +## 機能説明 + +CC Switch は、システム環境変数とアプリ設定の競合を自動的に検出し、設定が意図せず上書きされるのを防ぎます。 + +**検出される環境変数**: +- `ANTHROPIC_API_KEY` - Claude API キー +- `ANTHROPIC_BASE_URL` - Claude API エンドポイント +- `OPENAI_API_KEY` - OpenAI API キー +- `GEMINI_API_KEY` - Gemini API キー +- その他の関連環境変数 + +## 競合の警告 + +競合が検出されると、画面の上部に黄色い警告バナーが表示されます: + +``` +⚠️ 環境変数の競合を検出 +X 個の環境変数が CC Switch の設定と競合する可能性があります +[展開] [閉じる] +``` + +## 競合の詳細を確認 + +「展開」ボタンをクリックして詳細情報を表示します: + +| フィールド | 説明 | +|------|------| +| 変数名 | 環境変数名 | +| 変数値 | 現在設定されている値 | +| ソース | 変数の出処 | + +### ソースの種類 + +| ソース | 説明 | +|------|------| +| ユーザーレジストリ | Windows のユーザーレベル環境変数 | +| システムレジストリ | Windows のシステムレベル環境変数 | +| Shell 設定 | macOS/Linux の Shell 設定ファイル | +| システム環境 | システムレベルの環境変数 | + +## 競合の処理 + +### 削除する変数の選択 + +1. 削除する環境変数にチェックを入れる +2. または「すべて選択」ですべての競合変数を選択 + +### 変数の削除 + +1. 「選択を削除」ボタンをクリック +2. 削除操作を確認 +3. CC Switch が自動的にバックアップしてから、選択した変数を削除 + +### 自動バックアップ + +削除前に自動的にバックアップが作成されます: + +- バックアップの場所:`~/.cc-switch/env-backups/` +- バックアップ形式:JSON ファイル +- 変数名、値、ソースなどの情報を含む + +## 警告を無視する + +競合が使用に影響しないことが確認できる場合: + +1. 警告バナーの右側にある「閉じる」ボタンをクリック +2. 警告が一時的に非表示になる +3. 次回の起動時に再度検出が行われる + +## 手動での処理 + +CC Switch 経由で削除したくない場合は、手動で処理できます: + +### Windows + +1. 「システムのプロパティ → 詳細 → 環境変数」を開く +2. ユーザー変数またはシステム変数で競合する変数を見つける +3. 変数を削除または変更 + +### macOS / Linux + +1. Shell 設定ファイル(例:`~/.zshrc`、`~/.bashrc`)を編集 +2. 関連する `export` 文を削除またはコメントアウト +3. 設定を再読み込み:`source ~/.zshrc` + +## なぜ競合が発生するのか + +環境変数の優先度は通常、設定ファイルよりも高く、以下の問題を引き起こす可能性があります: + +- CC Switch で設定したプロバイダー設定が上書きされる +- API リクエストが間違ったエンドポイントに送信される +- 間違った API キーが使用される + +## ベストプラクティス + +1. **CC Switch で設定を管理**:システム環境変数に API キーを設定しないようにする +2. **定期的に確認**:競合の警告に注意し、速やかに対処 +3. **重要な変数のバックアップ**:削除前にバックアップを確認 + +## 削除した変数の復元 + +環境変数を誤って削除した場合: + +1. バックアップファイルを見つける:`~/.cc-switch/env-backups/` +2. 対応する JSON ファイルを開く +3. システム環境に手動で変数を復元 diff --git a/docs/user-manual/ja/README.md b/docs/user-manual/ja/README.md new file mode 100644 index 000000000..5fa84a7ba --- /dev/null +++ b/docs/user-manual/ja/README.md @@ -0,0 +1,111 @@ +# CC Switch ユーザーマニュアル + +> Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw オールインワンアシスタント + +## 目次構成 + +``` +CC Switch ユーザーマニュアル +│ +├── 1. はじめに +│ ├── 1.1 ソフトウェア紹介 +│ ├── 1.2 インストールガイド +│ ├── 1.3 インターフェース概要 +│ ├── 1.4 クイックスタート +│ └── 1.5 個人設定 +│ +├── 2. プロバイダー管理 +│ ├── 2.1 プロバイダーの追加 +│ ├── 2.2 プロバイダーの切り替え +│ ├── 2.3 プロバイダーの編集 +│ ├── 2.4 並べ替えと複製 +│ └── 2.5 使用量クエリ +│ +├── 3. 拡張機能 +│ ├── 3.1 MCP サーバー管理 +│ ├── 3.2 Prompts プロンプト管理 +│ └── 3.3 Skills スキル管理 +│ +├── 4. プロキシと高可用性 +│ ├── 4.1 プロキシサービス +│ ├── 4.2 アプリケーション接管 +│ ├── 4.3 フェイルオーバー +│ ├── 4.4 使用量統計 +│ └── 4.5 モデルテスト +│ +└── 5. よくある質問 + ├── 5.1 設定ファイルの説明 + ├── 5.2 FAQ + ├── 5.3 ディープリンクプロトコル + └── 5.4 環境変数の競合 +``` + +## ファイル一覧 + +### 1. はじめに + +| ファイル | 内容 | +|------|------| +| [1.1-introduction.md](./1-getting-started/1.1-introduction.md) | ソフトウェア紹介、主要機能、対応プラットフォーム | +| [1.2-installation.md](./1-getting-started/1.2-installation.md) | Windows/macOS/Linux インストールガイド | +| [1.3-interface.md](./1-getting-started/1.3-interface.md) | インターフェースレイアウト、ナビゲーションバー、プロバイダーカードの説明 | +| [1.4-quickstart.md](./1-getting-started/1.4-quickstart.md) | 5 分でできるクイックスタートチュートリアル | +| [1.5-settings.md](./1-getting-started/1.5-settings.md) | 言語、テーマ、ディレクトリ、クラウド同期の設定 | + +### 2. プロバイダー管理 + +| ファイル | 内容 | +|------|------| +| [2.1-add.md](./2-providers/2.1-add.md) | プリセットの使用、カスタム設定、統一プロバイダー | +| [2.2-switch.md](./2-providers/2.2-switch.md) | メイン画面での切り替え、トレイでの切り替え、反映方法 | +| [2.3-edit.md](./2-providers/2.3-edit.md) | 設定の編集、API Key の変更、バックフィル機能 | +| [2.4-sort-duplicate.md](./2-providers/2.4-sort-duplicate.md) | ドラッグで並べ替え、プロバイダーの複製、削除 | +| [2.5-usage-query.md](./2-providers/2.5-usage-query.md) | 使用量クエリ、残額表示、複数プラン表示 | + +### 3. 拡張機能 + +| ファイル | 内容 | +|------|------| +| [3.1-mcp.md](./3-extensions/3.1-mcp.md) | MCP プロトコル、サーバーの追加、アプリバインド | +| [3.2-prompts.md](./3-extensions/3.2-prompts.md) | プリセットの作成、有効化の切り替え、スマートバックフィル | +| [3.3-skills.md](./3-extensions/3.3-skills.md) | スキルの発見、インストール・アンインストール、リポジトリ管理 | + +### 4. プロキシと高可用性 + +| ファイル | 内容 | +|------|------| +| [4.1-service.md](./4-proxy/4.1-service.md) | プロキシの起動、設定項目、実行状態 | +| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | アプリケーション接管、設定変更、ステータス表示 | +| [4.3-failover.md](./4-proxy/4.3-failover.md) | フェイルオーバーキュー、サーキットブレーカー、ヘルスステータス | +| [4.4-usage.md](./4-proxy/4.4-usage.md) | 使用量統計、トレンドグラフ、料金設定 | +| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | モデルテスト、ヘルスチェック、レイテンシテスト | + +### 5. よくある質問 + +| ファイル | 内容 | +|------|------| +| [5.1-config-files.md](./5-faq/5.1-config-files.md) | CC Switch のストレージ、CLI 設定ファイル形式 | +| [5.2-questions.md](./5-faq/5.2-questions.md) | よくある質問と回答 | +| [5.3-deeplink.md](./5-faq/5.3-deeplink.md) | ディープリンクプロトコル、生成と使用方法 | +| [5.4-env-conflict.md](./5-faq/5.4-env-conflict.md) | 環境変数の競合検出と対処 | + +## クイックリンク + +- **初めての方**:[1.1 ソフトウェア紹介](./1-getting-started/1.1-introduction.md) からお読みください +- **インストールの問題**:[1.2 インストールガイド](./1-getting-started/1.2-installation.md) をご確認ください +- **プロバイダーの設定**:[2.1 プロバイダーの追加](./2-providers/2.1-add.md) をご確認ください +- **プロキシの使用**:[4.1 プロキシサービス](./4-proxy/4.1-service.md) をご確認ください +- **お困りの方**:[5.2 FAQ](./5-faq/5.2-questions.md) をご確認ください + +## バージョン情報 + +- ドキュメントバージョン:v3.11.1 +- 最終更新:2026-03-02 +- CC Switch v3.11.1+ 対応 + +## コントリビュート + +Issue や PR でドキュメントの改善にご協力ください: + +- [GitHub Issues](https://github.com/farion1231/cc-switch/issues) +- [GitHub Repository](https://github.com/farion1231/cc-switch) diff --git a/docs/user-manual/1-getting-started/1.1-introduction.md b/docs/user-manual/zh/1-getting-started/1.1-introduction.md similarity index 100% rename from docs/user-manual/1-getting-started/1.1-introduction.md rename to docs/user-manual/zh/1-getting-started/1.1-introduction.md diff --git a/docs/user-manual/1-getting-started/1.2-installation.md b/docs/user-manual/zh/1-getting-started/1.2-installation.md similarity index 100% rename from docs/user-manual/1-getting-started/1.2-installation.md rename to docs/user-manual/zh/1-getting-started/1.2-installation.md diff --git a/docs/user-manual/1-getting-started/1.3-interface.md b/docs/user-manual/zh/1-getting-started/1.3-interface.md similarity index 97% rename from docs/user-manual/1-getting-started/1.3-interface.md rename to docs/user-manual/zh/1-getting-started/1.3-interface.md index 358221e97..11554a1a0 100644 --- a/docs/user-manual/1-getting-started/1.3-interface.md +++ b/docs/user-manual/zh/1-getting-started/1.3-interface.md @@ -2,7 +2,7 @@ ## 主界面布局 -![image-20260108001629138](../assets/image-20260108001629138.png) +![image-20260108001629138](../../assets/image-20260108001629138.png) ## 顶部导航栏 @@ -93,7 +93,7 @@ CC Switch 在系统托盘显示图标,提供快速操作入口。 ### 托盘菜单结构 -![image-20260108002153668](../assets/image-20260108002153668.png) +![image-20260108002153668](../../assets/image-20260108002153668.png) ### 菜单功能 diff --git a/docs/user-manual/1-getting-started/1.4-quickstart.md b/docs/user-manual/zh/1-getting-started/1.4-quickstart.md similarity index 95% rename from docs/user-manual/1-getting-started/1.4-quickstart.md rename to docs/user-manual/zh/1-getting-started/1.4-quickstart.md index 15a7b8e99..63c4b3f67 100644 --- a/docs/user-manual/1-getting-started/1.4-quickstart.md +++ b/docs/user-manual/zh/1-getting-started/1.4-quickstart.md @@ -11,7 +11,7 @@ 3. 填写 **API Key** 4. 点击「添加」 -![image-20260108002807657](../assets/image-20260108002807657.png) +![image-20260108002807657](../../assets/image-20260108002807657.png) > 💡 **提示**:预设会自动填充端点地址,你只需要填写 API Key。 @@ -44,7 +44,7 @@ 2. 开启「跳过 Claude Code 初次安装确认」开关 3. 重新启动 Claude Code -![image-20260108002626389](../assets/image-20260108002626389.png) +![image-20260108002626389](../../assets/image-20260108002626389.png) > ⚠️ **注意**:此选项会写入 `~/.claude/settings.json` 的 `skipIntroduction` 字段,跳过官方的新手引导流程。 diff --git a/docs/user-manual/1-getting-started/1.5-settings.md b/docs/user-manual/zh/1-getting-started/1.5-settings.md similarity index 100% rename from docs/user-manual/1-getting-started/1.5-settings.md rename to docs/user-manual/zh/1-getting-started/1.5-settings.md diff --git a/docs/user-manual/2-providers/2.1-add.md b/docs/user-manual/zh/2-providers/2.1-add.md similarity index 99% rename from docs/user-manual/2-providers/2.1-add.md rename to docs/user-manual/zh/2-providers/2.1-add.md index 812761005..a9f831f97 100644 --- a/docs/user-manual/2-providers/2.1-add.md +++ b/docs/user-manual/zh/2-providers/2.1-add.md @@ -351,5 +351,5 @@ CC Switch 支持两种方式导入供应商配置: - 🟡 黄色:延迟 500-1000ms(一般) - 🔴 红色:延迟 > 1000ms(较慢) -![image-20260108005327817](../assets/image-20260108005327817.png) +![image-20260108005327817](../../assets/image-20260108005327817.png) diff --git a/docs/user-manual/2-providers/2.2-switch.md b/docs/user-manual/zh/2-providers/2.2-switch.md similarity index 96% rename from docs/user-manual/2-providers/2.2-switch.md rename to docs/user-manual/zh/2-providers/2.2-switch.md index 6e9833512..e61998f67 100644 --- a/docs/user-manual/2-providers/2.2-switch.md +++ b/docs/user-manual/zh/2-providers/2.2-switch.md @@ -32,7 +32,7 @@ ### 托盘菜单结构 -![image-20260108004348993](../assets/image-20260108004348993.png) +![image-20260108004348993](../../assets/image-20260108004348993.png) ## 生效方式 diff --git a/docs/user-manual/2-providers/2.3-edit.md b/docs/user-manual/zh/2-providers/2.3-edit.md similarity index 97% rename from docs/user-manual/2-providers/2.3-edit.md rename to docs/user-manual/zh/2-providers/2.3-edit.md index 57c22ad01..cc8c494ad 100644 --- a/docs/user-manual/2-providers/2.3-edit.md +++ b/docs/user-manual/zh/2-providers/2.3-edit.md @@ -32,7 +32,7 @@ CC Switch 提供丰富的图标自定义功能: - 显示图标名称提示 - 实时预览选中效果 -![image-20260108004734882](../assets/image-20260108004734882.png) +![image-20260108004734882](../../assets/image-20260108004734882.png) ### 配置信息 diff --git a/docs/user-manual/2-providers/2.4-sort-duplicate.md b/docs/user-manual/zh/2-providers/2.4-sort-duplicate.md similarity index 96% rename from docs/user-manual/2-providers/2.4-sort-duplicate.md rename to docs/user-manual/zh/2-providers/2.4-sort-duplicate.md index 9e87dae4e..2612dcbae 100644 --- a/docs/user-manual/2-providers/2.4-sort-duplicate.md +++ b/docs/user-manual/zh/2-providers/2.4-sort-duplicate.md @@ -73,4 +73,4 @@ - **当前启用的供应商**:可以删除,但建议先切换到其他供应商 - **统一供应商**:删除后,关联的应用配置也会被删除 -![image-20260108004946288](../assets/image-20260108004946288.png) +![image-20260108004946288](../../assets/image-20260108004946288.png) diff --git a/docs/user-manual/2-providers/2.5-usage-query.md b/docs/user-manual/zh/2-providers/2.5-usage-query.md similarity index 100% rename from docs/user-manual/2-providers/2.5-usage-query.md rename to docs/user-manual/zh/2-providers/2.5-usage-query.md diff --git a/docs/user-manual/3-extensions/3.1-mcp.md b/docs/user-manual/zh/3-extensions/3.1-mcp.md similarity index 97% rename from docs/user-manual/3-extensions/3.1-mcp.md rename to docs/user-manual/zh/3-extensions/3.1-mcp.md index d3eca8648..4a93d59df 100644 --- a/docs/user-manual/3-extensions/3.1-mcp.md +++ b/docs/user-manual/zh/3-extensions/3.1-mcp.md @@ -15,7 +15,7 @@ MCP (Model Context Protocol) 是一种协议,允许 AI 工具访问外部数 ## 面板概览 -![image-20260108005723522](../assets/image-20260108005723522.png) +![image-20260108005723522](../../assets/image-20260108005723522.png) ## 添加 MCP 服务器 @@ -26,7 +26,7 @@ MCP (Model Context Protocol) 是一种协议,允许 AI 工具访问外部数 3. 根据需要修改配置 4. 点击「保存」 -![image-20260108005739731](../assets/image-20260108005739731.png) +![image-20260108005739731](../../assets/image-20260108005739731.png) ### 常用预设 diff --git a/docs/user-manual/3-extensions/3.2-prompts.md b/docs/user-manual/zh/3-extensions/3.2-prompts.md similarity index 98% rename from docs/user-manual/3-extensions/3.2-prompts.md rename to docs/user-manual/zh/3-extensions/3.2-prompts.md index c5a0ce15d..c8eb82e51 100644 --- a/docs/user-manual/3-extensions/3.2-prompts.md +++ b/docs/user-manual/zh/3-extensions/3.2-prompts.md @@ -16,7 +16,7 @@ Prompts 功能用于管理系统提示词预设。系统提示词会影响 AI ## 面板概览 -![image-20260108010110382](../assets/image-20260108010110382.png) +![image-20260108010110382](../../assets/image-20260108010110382.png) ## 创建预设 diff --git a/docs/user-manual/3-extensions/3.3-skills.md b/docs/user-manual/zh/3-extensions/3.3-skills.md similarity index 94% rename from docs/user-manual/3-extensions/3.3-skills.md rename to docs/user-manual/zh/3-extensions/3.3-skills.md index 30d8e5124..ce48fdb06 100644 --- a/docs/user-manual/3-extensions/3.3-skills.md +++ b/docs/user-manual/zh/3-extensions/3.3-skills.md @@ -27,7 +27,7 @@ Skills 功能支持所有四种应用: ## 页面概览 -![image-20260108010253926](../assets/image-20260108010253926.png) +![image-20260108010253926](../../assets/image-20260108010253926.png) ## 发现技能 @@ -41,7 +41,7 @@ CC Switch 预配置了以下 GitHub 仓库: | ComposioHQ | 社区维护的技能集合 | | 社区精选 | 精选的高质量技能 | -![image-20260108010308060](../assets/image-20260108010308060.png) +![image-20260108010308060](../../assets/image-20260108010308060.png) ### 搜索过滤 @@ -64,7 +64,7 @@ CC Switch 提供强大的搜索和过滤功能: | 已安装 | 仅显示已安装的技能 | | 未安装 | 仅显示未安装的技能 | -![image-20260108010324583](../assets/image-20260108010324583.png) +![image-20260108010324583](../../assets/image-20260108010324583.png) #### 组合使用 diff --git a/docs/user-manual/4-proxy/4.1-service.md b/docs/user-manual/zh/4-proxy/4.1-service.md similarity index 96% rename from docs/user-manual/4-proxy/4.1-service.md rename to docs/user-manual/zh/4-proxy/4.1-service.md index 4f0fc3b39..7da918447 100644 --- a/docs/user-manual/4-proxy/4.1-service.md +++ b/docs/user-manual/zh/4-proxy/4.1-service.md @@ -20,14 +20,14 @@ - 🔴 白色:代理未运行 - 🟢 绿色:代理运行中 -![image-20260108011353927](../assets/image-20260108011353927.png) +![image-20260108011353927](../../assets/image-20260108011353927.png) ### 方式二:设置页面 1. 打开「设置 → 高级 → 代理服务」 2. 点击右上角的开关 -![image-20260108011338922](../assets/image-20260108011338922.png) +![image-20260108011338922](../../assets/image-20260108011338922.png) ## 代理配置 diff --git a/docs/user-manual/4-proxy/4.2-takeover.md b/docs/user-manual/zh/4-proxy/4.2-takeover.md similarity index 100% rename from docs/user-manual/4-proxy/4.2-takeover.md rename to docs/user-manual/zh/4-proxy/4.2-takeover.md diff --git a/docs/user-manual/4-proxy/4.3-failover.md b/docs/user-manual/zh/4-proxy/4.3-failover.md similarity index 100% rename from docs/user-manual/4-proxy/4.3-failover.md rename to docs/user-manual/zh/4-proxy/4.3-failover.md diff --git a/docs/user-manual/4-proxy/4.4-usage.md b/docs/user-manual/zh/4-proxy/4.4-usage.md similarity index 94% rename from docs/user-manual/4-proxy/4.4-usage.md rename to docs/user-manual/zh/4-proxy/4.4-usage.md index 7fea1c9fe..8e8b912d0 100644 --- a/docs/user-manual/4-proxy/4.4-usage.md +++ b/docs/user-manual/zh/4-proxy/4.4-usage.md @@ -44,7 +44,7 @@ | 最近 7 天 | 过去 7 天 | | 最近 30 天 | 过去 30 天 | -![image-20260108011730105](../assets/image-20260108011730105.png) +![image-20260108011730105](../../assets/image-20260108011730105.png) ## 趋势图表 @@ -76,7 +76,7 @@ -![image-20260108011742847](../assets/image-20260108011742847.png) +![image-20260108011742847](../../assets/image-20260108011742847.png) ## 详细数据 @@ -134,7 +134,7 @@ - **重置**:恢复默认(过去 24 小时) - **刷新**:重新加载数据 -![image-20260108011859974](../assets/image-20260108011859974.png) +![image-20260108011859974](../../assets/image-20260108011859974.png) ### 供应商统计 @@ -150,7 +150,7 @@ | 总 Token | Token 使用总量 | | 估算费用 | 该供应商的费用 | -![image-20260108011907928](../assets/image-20260108011907928.png) +![image-20260108011907928](../../assets/image-20260108011907928.png) ### 模型统计 @@ -165,7 +165,7 @@ | 平均延迟 | 平均响应时间 | | 估算费用 | 该模型的费用 | -![image-20260108011915381](../assets/image-20260108011915381.png) +![image-20260108011915381](../../assets/image-20260108011915381.png) ## 定价配置 @@ -192,7 +192,7 @@ - **编辑**:点击行末的编辑图标修改 - **删除**:点击行末的删除图标移除 -![image-20260108011933565](../assets/image-20260108011933565.png) +![image-20260108011933565](../../assets/image-20260108011933565.png) ### 预设价格 diff --git a/docs/user-manual/4-proxy/4.5-model-test.md b/docs/user-manual/zh/4-proxy/4.5-model-test.md similarity index 100% rename from docs/user-manual/4-proxy/4.5-model-test.md rename to docs/user-manual/zh/4-proxy/4.5-model-test.md diff --git a/docs/user-manual/5-faq/5.1-config-files.md b/docs/user-manual/zh/5-faq/5.1-config-files.md similarity index 100% rename from docs/user-manual/5-faq/5.1-config-files.md rename to docs/user-manual/zh/5-faq/5.1-config-files.md diff --git a/docs/user-manual/5-faq/5.2-questions.md b/docs/user-manual/zh/5-faq/5.2-questions.md similarity index 100% rename from docs/user-manual/5-faq/5.2-questions.md rename to docs/user-manual/zh/5-faq/5.2-questions.md diff --git a/docs/user-manual/5-faq/5.3-deeplink.md b/docs/user-manual/zh/5-faq/5.3-deeplink.md similarity index 100% rename from docs/user-manual/5-faq/5.3-deeplink.md rename to docs/user-manual/zh/5-faq/5.3-deeplink.md diff --git a/docs/user-manual/5-faq/5.4-env-conflict.md b/docs/user-manual/zh/5-faq/5.4-env-conflict.md similarity index 100% rename from docs/user-manual/5-faq/5.4-env-conflict.md rename to docs/user-manual/zh/5-faq/5.4-env-conflict.md diff --git a/docs/user-manual/zh/README.md b/docs/user-manual/zh/README.md new file mode 100644 index 000000000..5a3380635 --- /dev/null +++ b/docs/user-manual/zh/README.md @@ -0,0 +1,111 @@ +# CC Switch 用户手册 + +> Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw 全方位辅助工具 + +## 目录结构 + +``` +📚 CC Switch 用户手册 +│ +├── 1. 快速入门 +│ ├── 1.1 软件介绍 +│ ├── 1.2 安装指南 +│ ├── 1.3 界面概览 +│ ├── 1.4 快速上手 +│ └── 1.5 个性化配置 +│ +├── 2. 供应商管理 +│ ├── 2.1 添加供应商 +│ ├── 2.2 切换供应商 +│ ├── 2.3 编辑供应商 +│ ├── 2.4 排序与复制 +│ └── 2.5 用量查询 +│ +├── 3. 扩展功能 +│ ├── 3.1 MCP 服务器管理 +│ ├── 3.2 Prompts 提示词管理 +│ └── 3.3 Skills 技能管理 +│ +├── 4. 代理与高可用 +│ ├── 4.1 代理服务 +│ ├── 4.2 应用接管 +│ ├── 4.3 故障转移 +│ ├── 4.4 用量统计 +│ └── 4.5 模型检查 +│ +└── 5. 常见问题 + ├── 5.1 配置文件说明 + ├── 5.2 FAQ + ├── 5.3 深度链接协议 + └── 5.4 环境变量冲突 +``` + +## 文件列表 + +### 1. 快速入门 + +| 文件 | 内容 | +|------|------| +| [1.1-introduction.md](./1-getting-started/1.1-introduction.md) | 软件介绍、核心功能、支持平台 | +| [1.2-installation.md](./1-getting-started/1.2-installation.md) | Windows/macOS/Linux 安装指南 | +| [1.3-interface.md](./1-getting-started/1.3-interface.md) | 界面布局、导航栏、供应商卡片说明 | +| [1.4-quickstart.md](./1-getting-started/1.4-quickstart.md) | 5 分钟快速上手教程 | +| [1.5-settings.md](./1-getting-started/1.5-settings.md) | 语言、主题、目录、云同步配置 | + +### 2. 供应商管理 + +| 文件 | 内容 | +|------|------| +| [2.1-add.md](./2-providers/2.1-add.md) | 使用预设、自定义配置、统一供应商 | +| [2.2-switch.md](./2-providers/2.2-switch.md) | 主界面切换、托盘切换、生效方式 | +| [2.3-edit.md](./2-providers/2.3-edit.md) | 编辑配置、修改 API Key、回填机制 | +| [2.4-sort-duplicate.md](./2-providers/2.4-sort-duplicate.md) | 拖拽排序、复制供应商、删除 | +| [2.5-usage-query.md](./2-providers/2.5-usage-query.md) | 用量查询、剩余额度、多套餐显示 | + +### 3. 扩展功能 + +| 文件 | 内容 | +|------|------| +| [3.1-mcp.md](./3-extensions/3.1-mcp.md) | MCP 协议、添加服务器、应用绑定 | +| [3.2-prompts.md](./3-extensions/3.2-prompts.md) | 创建预设、激活切换、智能回填 | +| [3.3-skills.md](./3-extensions/3.3-skills.md) | 发现技能、安装卸载、仓库管理 | + +### 4. 代理与高可用 + +| 文件 | 内容 | +|------|------| +| [4.1-service.md](./4-proxy/4.1-service.md) | 启动代理、配置项、运行状态 | +| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | 应用接管、配置修改、状态指示 | +| [4.3-failover.md](./4-proxy/4.3-failover.md) | 故障转移队列、熔断器、健康状态 | +| [4.4-usage.md](./4-proxy/4.4-usage.md) | 用量统计、趋势图表、定价配置 | +| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | 模型检查、健康检测、延迟测试 | + +### 5. 常见问题 + +| 文件 | 内容 | +|------|------| +| [5.1-config-files.md](./5-faq/5.1-config-files.md) | CC Switch 存储、CLI 配置文件格式 | +| [5.2-questions.md](./5-faq/5.2-questions.md) | 常见问题解答 | +| [5.3-deeplink.md](./5-faq/5.3-deeplink.md) | 深度链接协议、生成和使用方法 | +| [5.4-env-conflict.md](./5-faq/5.4-env-conflict.md) | 环境变量冲突检测与处理 | + +## 快速链接 + +- **新用户**:从 [1.1 软件介绍](./1-getting-started/1.1-introduction.md) 开始 +- **安装问题**:查看 [1.2 安装指南](./1-getting-started/1.2-installation.md) +- **配置供应商**:查看 [2.1 添加供应商](./2-providers/2.1-add.md) +- **使用代理**:查看 [4.1 代理服务](./4-proxy/4.1-service.md) +- **遇到问题**:查看 [5.2 FAQ](./5-faq/5.2-questions.md) + +## 版本信息 + +- 文档版本:v3.11.1 +- 最后更新:2026-02-28 +- 适用于 CC Switch v3.11.1+ + +## 贡献 + +欢迎提交 Issue 或 PR 改进文档: + +- [GitHub Issues](https://github.com/farion1231/cc-switch/issues) +- [GitHub Repository](https://github.com/farion1231/cc-switch) From 0c78b38295e4aba804f7c9d8f6665f70b649cdac Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 08:43:57 +0800 Subject: [PATCH 13/34] docs: add user manual links to all three README files --- README.md | 6 ++++++ README_JA.md | 6 ++++++ README_ZH.md | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/README.md b/README.md index a2edeeebb..c7d0f2a87 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,12 @@ Add an official provider from the preset list. After switching to it, run the Lo +## Documentation + +For detailed guides on every feature, check out the **[User Manual](docs/user-manual/en/README.md)** — covering provider management, MCP/Prompts/Skills, proxy & failover, and more. + +> Also available in: [中文](docs/user-manual/zh/README.md) | [日本語](docs/user-manual/ja/README.md) + ## Quick Start ### Basic Usage diff --git a/README_JA.md b/README_JA.md index b683ac222..0ef677f46 100644 --- a/README_JA.md +++ b/README_JA.md @@ -189,6 +189,12 @@ CC Switch は「最小限の介入」という設計原則に従っています +## ドキュメント + +各機能の詳しい使い方については、**[ユーザーマニュアル](docs/user-manual/ja/README.md)** をご覧ください。プロバイダ管理、MCP/Prompts/Skills、プロキシとフェイルオーバーなど、すべての機能を網羅しています。 + +> 他の言語:[English](docs/user-manual/en/README.md) | [中文](docs/user-manual/zh/README.md) + ## クイックスタート ### 基本的な使い方 diff --git a/README_ZH.md b/README_ZH.md index aab319cab..6299d0d28 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -192,6 +192,12 @@ CC Switch 使用“通用配置片段”功能,在不同的供应商之间传 +## 文档 + +如需了解各项功能的详细使用方法,请查阅 **[用户手册](docs/user-manual/zh/README.md)** — 涵盖供应商管理、MCP/Prompts/Skills、代理与故障转移等全部功能。 + +> 其他语言:[English](docs/user-manual/en/README.md) | [日本語](docs/user-manual/ja/README.md) + ## 快速开始 ### 基本使用 From d5d7b3190d17a8b47d3cae2a0c5d20faabb5b617 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 08:48:52 +0800 Subject: [PATCH 14/34] docs: remove cross-language links from user manual sections in READMEs --- README.md | 2 -- README_JA.md | 2 -- README_ZH.md | 2 -- 3 files changed, 6 deletions(-) diff --git a/README.md b/README.md index c7d0f2a87..adefd2f85 100644 --- a/README.md +++ b/README.md @@ -193,8 +193,6 @@ Add an official provider from the preset list. After switching to it, run the Lo For detailed guides on every feature, check out the **[User Manual](docs/user-manual/en/README.md)** — covering provider management, MCP/Prompts/Skills, proxy & failover, and more. -> Also available in: [中文](docs/user-manual/zh/README.md) | [日本語](docs/user-manual/ja/README.md) - ## Quick Start ### Basic Usage diff --git a/README_JA.md b/README_JA.md index 0ef677f46..291b7ac1e 100644 --- a/README_JA.md +++ b/README_JA.md @@ -193,8 +193,6 @@ CC Switch は「最小限の介入」という設計原則に従っています 各機能の詳しい使い方については、**[ユーザーマニュアル](docs/user-manual/ja/README.md)** をご覧ください。プロバイダ管理、MCP/Prompts/Skills、プロキシとフェイルオーバーなど、すべての機能を網羅しています。 -> 他の言語:[English](docs/user-manual/en/README.md) | [中文](docs/user-manual/zh/README.md) - ## クイックスタート ### 基本的な使い方 diff --git a/README_ZH.md b/README_ZH.md index 6299d0d28..c31dc82ac 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -196,8 +196,6 @@ CC Switch 使用“通用配置片段”功能,在不同的供应商之间传 如需了解各项功能的详细使用方法,请查阅 **[用户手册](docs/user-manual/zh/README.md)** — 涵盖供应商管理、MCP/Prompts/Skills、代理与故障转移等全部功能。 -> 其他语言:[English](docs/user-manual/en/README.md) | [日本語](docs/user-manual/ja/README.md) - ## 快速开始 ### 基本使用 From c772874dcb8e8bbcdb2b180c03e42738f2020dca Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 09:28:48 +0800 Subject: [PATCH 15/34] docs: reorganize docs directory structure - Delete 9 completed planning/roadmap documents - Move 23 release notes into docs/release-notes/ with simplified filenames - Update all cross-references in READMEs, CHANGELOG, and release notes - Remove dangling doc reference in deeplink/mod.rs --- CHANGELOG.md | 2 +- README.md | 2 +- README_JA.md | 2 +- README_ZH.md | 2 +- docs/BACKEND_REFACTOR_PLAN.md | 169 -- docs/CODEX_MCP_RAW_TOML_PLAN.md | 1309 ------------- docs/REFACTORING_CHECKLIST.md | 490 ----- docs/REFACTORING_MASTER_PLAN.md | 1658 ----------------- docs/REFACTORING_REFERENCE.md | 834 --------- docs/TEST_DEVELOPMENT_PLAN.md | 73 - docs/opencode-implementation-plan.md | 485 ----- .../v3.10.0-en.md} | 2 +- .../v3.10.0-ja.md} | 2 +- .../v3.10.0-zh.md} | 2 +- .../v3.11.0-en.md} | 2 +- .../v3.11.0-ja.md} | 2 +- .../v3.11.0-zh.md} | 2 +- .../v3.11.1-en.md} | 2 +- .../v3.11.1-ja.md} | 2 +- .../v3.11.1-zh.md} | 2 +- .../v3.6.0-en.md} | 2 +- .../v3.6.0-zh.md} | 2 +- .../v3.6.1-en.md} | 2 +- .../v3.6.1-zh.md} | 2 +- .../v3.7.0-en.md} | 2 +- .../v3.7.0-zh.md} | 2 +- .../v3.7.1-en.md} | 2 +- .../v3.7.1-zh.md} | 2 +- .../v3.8.0-en.md} | 2 +- .../v3.8.0-ja.md} | 2 +- .../v3.8.0-zh.md} | 2 +- .../v3.9.0-en.md} | 2 +- .../v3.9.0-ja.md} | 2 +- .../v3.9.0-zh.md} | 2 +- docs/roadmap.md | 10 - docs/v3.7.0-unified-mcp-refactor.md | 863 --------- src-tauri/src/deeplink/mod.rs | 1 - 37 files changed, 27 insertions(+), 5919 deletions(-) delete mode 100644 docs/BACKEND_REFACTOR_PLAN.md delete mode 100644 docs/CODEX_MCP_RAW_TOML_PLAN.md delete mode 100644 docs/REFACTORING_CHECKLIST.md delete mode 100644 docs/REFACTORING_MASTER_PLAN.md delete mode 100644 docs/REFACTORING_REFERENCE.md delete mode 100644 docs/TEST_DEVELOPMENT_PLAN.md delete mode 100644 docs/opencode-implementation-plan.md rename docs/{release-note-v3.10.0-en.md => release-notes/v3.10.0-en.md} (98%) rename docs/{release-note-v3.10.0-ja.md => release-notes/v3.10.0-ja.md} (99%) rename docs/{release-note-v3.10.0-zh.md => release-notes/v3.10.0-zh.md} (98%) rename docs/{release-note-v3.11.0-en.md => release-notes/v3.11.0-en.md} (99%) rename docs/{release-note-v3.11.0-ja.md => release-notes/v3.11.0-ja.md} (99%) rename docs/{release-note-v3.11.0-zh.md => release-notes/v3.11.0-zh.md} (99%) rename docs/{release-note-v3.11.1-en.md => release-notes/v3.11.1-en.md} (98%) rename docs/{release-note-v3.11.1-ja.md => release-notes/v3.11.1-ja.md} (98%) rename docs/{release-note-v3.11.1-zh.md => release-notes/v3.11.1-zh.md} (98%) rename docs/{release-note-v3.6.0-en.md => release-notes/v3.6.0-en.md} (99%) rename docs/{release-note-v3.6.0-zh.md => release-notes/v3.6.0-zh.md} (99%) rename docs/{release-note-v3.6.1-en.md => release-notes/v3.6.1-en.md} (99%) rename docs/{release-note-v3.6.1-zh.md => release-notes/v3.6.1-zh.md} (99%) rename docs/{release-note-v3.7.0-en.md => release-notes/v3.7.0-en.md} (99%) rename docs/{release-note-v3.7.0-zh.md => release-notes/v3.7.0-zh.md} (99%) rename docs/{release-note-v3.7.1-en.md => release-notes/v3.7.1-en.md} (99%) rename docs/{release-note-v3.7.1-zh.md => release-notes/v3.7.1-zh.md} (99%) rename docs/{release-note-v3.8.0-en.md => release-notes/v3.8.0-en.md} (99%) rename docs/{release-note-v3.8.0-ja.md => release-notes/v3.8.0-ja.md} (99%) rename docs/{release-note-v3.8.0-zh.md => release-notes/v3.8.0-zh.md} (99%) rename docs/{release-note-v3.9.0-en.md => release-notes/v3.9.0-en.md} (98%) rename docs/{release-note-v3.9.0-ja.md => release-notes/v3.9.0-ja.md} (99%) rename docs/{release-note-v3.9.0-zh.md => release-notes/v3.9.0-zh.md} (98%) delete mode 100644 docs/roadmap.md delete mode 100644 docs/v3.7.0-unified-mcp-refactor.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f93cc35d8..4ed4fc5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -653,7 +653,7 @@ This beta release introduces the **Local API Proxy** feature, along with Skills ### Stats -- 51 commits since v3.7.1; 207 files changed; +17,297 / -6,870 lines. See [release-note-v3.8.0](docs/release-note-v3.8.0-en.md) for details. +- 51 commits since v3.7.1; 207 files changed; +17,297 / -6,870 lines. See [release-note-v3.8.0](docs/release-notes/v3.8.0-en.md) for details. --- diff --git a/README.md b/README.md index adefd2f85..4ea7f9022 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Modern AI-powered coding relies on CLI tools like Claude Code, Codex, Gemini CLI ## Features -[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.11.1-en.md) +[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-notes/v3.11.1-en.md) ### Provider Management diff --git a/README_JA.md b/README_JA.md index 291b7ac1e..e6647844f 100644 --- a/README_JA.md +++ b/README_JA.md @@ -101,7 +101,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / ## 特長 -[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.11.1-ja.md) +[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-notes/v3.11.1-ja.md) ### プロバイダ管理 diff --git a/README_ZH.md b/README_ZH.md index c31dc82ac..f7c3647b2 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -102,7 +102,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更 ## 功能特性 -[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.11.1-zh.md) +[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-notes/v3.11.1-zh.md) ### 供应商管理 diff --git a/docs/BACKEND_REFACTOR_PLAN.md b/docs/BACKEND_REFACTOR_PLAN.md deleted file mode 100644 index 9c8b75f10..000000000 --- a/docs/BACKEND_REFACTOR_PLAN.md +++ /dev/null @@ -1,169 +0,0 @@ -# CC Switch Rust 后端重构方案 - -## 目录 -- [背景与现状](#背景与现状) -- [问题确认](#问题确认) -- [方案评估](#方案评估) -- [渐进式重构路线](#渐进式重构路线) -- [测试策略](#测试策略) -- [风险与对策](#风险与对策) -- [总结](#总结) - -## 背景与现状 -- 前端已完成重构,后端 (Tauri + Rust) 仍维持历史结构。 -- 核心文件集中在 `src-tauri/src/commands.rs`、`lib.rs` 等超大文件中,业务逻辑与界面事件耦合严重。 -- 测试覆盖率低,只有零散单元测试,缺乏集成验证。 - -## 问题确认 - -| 提案问题 | 实际情况 | 严重程度 | -| --- | --- | --- | -| `commands.rs` 过长 | ✅ 1526 行,包含 32 个命令,职责混杂 | 🔴 高 | -| `lib.rs` 缺少服务层 | ✅ 541 行,托盘/事件/业务逻辑耦合 | 🟡 中 | -| `Result` 泛滥 | ✅ 118 处,错误上下文丢失 | 🟡 中 | -| 全局 `Mutex` 阻塞 | ✅ 31 处 `.lock()` 调用,读写不分离 | 🟡 中 | -| 配置逻辑分散 | ✅ 分布在 5 个文件 (`config`/`app_config`/`app_store`/`settings`/`codex_config`) | 🟢 低 | - -代码规模分布(约 5.4k SLOC): -- `commands.rs`: 1526 行(28%)→ 第一优先级 🎯 -- `lib.rs`: 541 行(10%)→ 托盘逻辑与业务耦合 -- `mcp.rs`: 732 行(14%)→ 相对清晰 -- `migration.rs`: 431 行(8%)→ 一次性逻辑 -- 其他文件合计:2156 行(40%) - -## 方案评估 - -### ✅ 优点 -1. **分层架构清晰** - - `commands/`:Tauri 命令薄层 - - `services/`:业务流程,如供应商切换、MCP 同步 - - `infrastructure/`:配置读写、外设交互 - - `domain/`:数据模型 (`Provider`, `AppType` 等) - → 提升可测试性、降低耦合度、方便团队协作。 - -2. **统一错误处理** - - 引入 `AppError`(`thiserror`),保留错误链和上下文。 - - Tauri 命令仍返回 `Result`,通过 `From` 自动转换。 - - 改善日志可读性,利于排查。 - -3. **并发优化** - - `AppState` 切换为 `RwLock`。 - - 读多写少的场景提升吞吐(如频繁查询供应商列表)。 - -### ⚠️ 风险 -1. **过度设计** - - 完整 DDD 四层在 5k 行项目中会增加 30-50% 维护成本。 - - Rust trait + repository 样板较多,收益不足。 - - 推荐“轻量分层”而非正统 DDD。 - -2. **迁移成本高** - - `commands.rs` 拆分、错误统一、锁改造触及多文件。 - - 测试缺失导致重构风险高,需先补测试。 - - 估算完整改造需 5-6 周;建议分阶段输出可落地价值。 - -3. **技术选型需谨慎** - - `parking_lot` 相比标准库 `RwLock` 提升有限,不必引入。 - - `spawn_blocking` 仅用于 >100ms 的阻塞任务,避免滥用。 - - 以现有依赖为主,控制复杂度。 - -## 实施进度 -- **阶段 1:统一错误处理 ✅** - - 引入 `thiserror` 并在 `src-tauri/src/error.rs` 定义 `AppError`,提供常用构造函数和 `From for String`,保留错误链路。 - - 配置、存储、同步等核心模块(`config.rs`、`app_config.rs`、`app_store.rs`、`store.rs`、`codex_config.rs`、`claude_mcp.rs`、`claude_plugin.rs`、`import_export.rs`、`mcp.rs`、`migration.rs`、`speedtest.rs`、`usage_script.rs`、`settings.rs`、`lib.rs` 等)已统一返回 `Result<_, AppError>`,避免字符串错误丢失上下文。 - - Tauri 命令层继续返回 `Result<_, String>`,通过 `?` + `Into` 统一转换,前端无需调整。 - - `cargo check` 通过,`rg "Result<[^>]+, String"` 巡检确认除命令层外已无字符串错误返回。 -- **阶段 2:拆分命令层 ✅** - - 已将单一 `src-tauri/src/commands.rs` 拆分为 `commands/{provider,mcp,config,settings,misc,plugin}.rs` 并通过 `commands/mod.rs` 统一导出,保持对外 API 不变。 - - 每个文件聚焦单一功能域(供应商、MCP、配置、设置、杂项、插件),命令函数平均 150-250 行,可读性与后续维护性显著提升。 - - 相关依赖调整后 `cargo check` 通过,静态巡检确认无重复定义或未注册命令。 -- **阶段 3:补充测试 ✅** - - `tests/import_export_sync.rs` 集成测试涵盖配置备份、Claude/Codex live 同步、MCP 投影与 Codex/Claude 双向导入流程,并新增启用项清理、非法 TOML 抛错等失败场景验证;统一使用隔离 HOME 目录避免污染真实用户环境。 - - 扩展 `lib.rs` re-export,暴露 `AppType`、`MultiAppConfig`、`AppError`、配置 IO 以及 Codex/Claude MCP 路径与同步函数,方便服务层及测试直接复用核心逻辑。 - - 新增负向测试验证 Codex 供应商缺少 `auth` 字段时的错误返回,并补充备份数量上限测试;顺带修复 `create_backup` 采用内存读写避免拷贝继承旧的修改时间,确保最新备份不会在清理阶段被误删。 - - 针对 `codex_config::write_codex_live_atomic` 补充成功与失败场景测试,覆盖 auth/config 原子写入与失败回滚逻辑(模拟目标路径为目录时的 rename 失败),降低 Codex live 写入回归风险。 - - 新增 `tests/provider_commands.rs` 覆盖 `switch_provider` 的 Codex 正常流程与供应商缺失分支,并抽取 `switch_provider_internal` 以复用 `AppError`,通过 `switch_provider_test_hook` 暴露测试入口;同时共享 `tests/support.rs` 提供隔离 HOME / 互斥工具函数。 - - 补充 Claude 切换集成测试,验证 live `settings.json` 覆写、新旧供应商快照回填以及 `.cc-switch/config.json` 持久化结果,确保阶段四提取服务层时拥有可回归的用例。 - - 增加 Codex 缺失 `auth` 场景测试,确认 `switch_provider_internal` 在关键字段缺失时返回带上下文的 `AppError`,同时保持内存状态未被污染。 - - 为配置导入命令抽取复用逻辑 `import_config_from_path` 并补充成功/失败集成测试,校验备份生成、状态同步、JSON 解析与文件缺失等错误回退路径;`export_config_to_file` 亦具备成功/缺失源文件的命令级回归。 - - 新增 `tests/mcp_commands.rs`,通过测试钩子覆盖 `import_default_config`、`import_mcp_from_claude`、`set_mcp_enabled` 等命令层行为,验证缺失文件/非法 JSON 的错误回滚以及成功路径落盘效果;阶段三目标达成,命令层关键边界已具备回归保障。 -- **阶段 4:服务层抽象 🚧(进行中)** - - 新增 `services/provider.rs` 并实现 `ProviderService::switch` / `delete`,集中处理供应商切换、回填、MCP 同步等核心业务;命令层改为薄封装并在 `tests/provider_service.rs`、`tests/provider_commands.rs` 中完成成功与失败路径的集成验证。 - - 新增 `services/mcp.rs` 提供 `McpService`,封装 MCP 服务器的查询、增删改、启用同步与导入流程;命令层改为参数解析 + 调用服务,`tests/mcp_commands.rs` 直接使用 `McpService` 验证成功与失败路径,阶段三测试继续适配。 - - `McpService` 在内部先复制内存快照、释放写锁,再执行文件同步,避免阶段五升级后的 `RwLock` 在 I/O 场景被长时间占用;`upsert/delete/set_enabled/sync_enabled` 均已修正。 - - 新增 `services/config.rs` 提供 `ConfigService`,统一处理配置导入导出、备份与 live 同步;命令层迁移至 `commands/import_export.rs`,在落盘操作前释放锁并复用现有集成测试。 - - 新增 `services/speedtest.rs` 并实现 `SpeedtestService::test_endpoints`,将 URL 校验、超时裁剪与网络请求封装在服务层,命令改为薄封装;补充单元测试覆盖空列表与非法 URL 分支。 - - 后续可选:应用设置(Store)命令仍较薄,可按需评估是否抽象;当前阶段四核心服务已基本齐备。 -- **阶段 5:锁与阻塞优化 ✅(首轮)** - - `AppState` 已由 `Mutex` 切换为 `RwLock`,托盘、命令与测试均按读写语义区分 `read()` / `write()`;`cargo test` 全量通过验证并未破坏现有流程。 - - 针对高开销 IO 的配置导入/导出命令提取 `load_config_for_import`,并通过 `tauri::async_runtime::spawn_blocking` 将文件读写与备份迁至阻塞线程,保持命令处理线程轻量。 - - 其余命令梳理后确认仍属轻量同步操作,暂不额外引入 `spawn_blocking`;若后续出现新的长耗时流程,再按同一模式扩展。 - -## 渐进式重构路线 - -### 阶段 1:统一错误处理(高收益 / 低风险) -- 新增 `src-tauri/src/error.rs`,定义 `AppError`。 -- 底层文件 IO、配置解析等函数返回 `Result`。 -- 命令层通过 `?` 自动传播,最终 `.map_err(Into::into)`。 -- 预估 3-5 天,立即启动。 - -### 阶段 2:拆分 `commands.rs`(高收益 / 中风险) -- 按业务拆分为 `commands/provider.rs`、`commands/mcp.rs`、`commands/config.rs`、`commands/settings.rs`、`commands/misc.rs`。 -- `commands/mod.rs` 统一导出和注册。 -- 文件行数降低到 200-300 行/文件,职责单一。 -- 预估 5-7 天,可并行进行部分重构。 - -### 阶段 3:补充测试(中收益 / 中风险) -- 引入 `tests/` 或 `src-tauri/tests/` 集成测试,覆盖供应商切换、MCP 同步、配置迁移。 -- 使用 `tempfile`/`tempdir` 隔离文件系统,组合少量回归脚本。 -- 预估 5-7 天,为后续重构提供安全网。 - -### 阶段 4:提取轻量服务层(中收益 / 中风险) -- 新增 `services/provider_service.rs`、`services/mcp_service.rs`。 -- 不强制使用 trait;直接以自由函数/结构体实现业务流程。 - ```rust - pub struct ProviderService; - impl ProviderService { - pub fn switch(config: &mut MultiAppConfig, app: AppType, id: &str) -> Result<(), AppError> { - // 业务流程:验证、回填、落盘、更新 current、触发事件 - } - } - ``` -- 命令层负责参数解析,服务层处理业务逻辑,托盘逻辑重用同一接口。 -- 预估 7-10 天,可在测试补齐后执行。 - -### 阶段 5:锁与阻塞优化(低收益 / 低风险) -- ✅ `AppState` 已从 `Mutex` 切换为 `RwLock`,命令与托盘读写按需区分,现有测试全部通过。 -- ✅ 配置导入/导出命令通过 `spawn_blocking` 处理高开销文件 IO;其他命令维持同步执行以避免不必要调度。 -- 🔄 持续监控:若后续引入新的批量迁移或耗时任务,再按相同模式扩展到阻塞线程;观察运行时锁竞争情况,必要时考虑进一步拆分状态或引入缓存。 - -## 测试策略 -- **优先覆盖场景** - - 供应商切换:状态更新 + live 配置同步 - - MCP 同步:enabled 服务器快照与落盘 - - 配置迁移:归档、备份与版本升级 -- **推荐结构** - ```rust - #[cfg(test)] - mod integration { - use super::*; - #[test] - fn switch_provider_updates_live_config() { /* ... */ } - #[test] - fn sync_mcp_to_codex_updates_claude_config() { /* ... */ } - #[test] - fn migration_preserves_backup() { /* ... */ } - } - ``` -- 目标覆盖率:关键路径 >80%,文件 IO/迁移 >70%。 - -## 风险与对策 -- **测试不足** → 阶段 3 强制补齐,建立基础集成测试。 -- **重构跨度大** → 按阶段在独立分支推进(如 `refactor/backend-step1` 等)。 -- **回滚困难** → 每阶段结束打 tag(如 `v3.6.0-backend-step1`),保留回滚点。 -- **功能回归** → 重构后执行手动冒烟流程:供应商切换、托盘操作、MCP 同步、配置导入导出。 - -## 总结 -- 当前规模下不建议整体引入完整 DDD/四层架构,避免过度设计。 -- 建议遵循“错误统一 → 命令拆分 → 补测试 → 服务层抽象 → 锁优化”的渐进式策略。 -- 完成阶段 1-3 后即可显著提升可维护性与可靠性;阶段 4-5 可根据资源灵活安排。 -- 重构过程中同步维护文档与测试,确保团队成员对架构演进保持一致认知。 diff --git a/docs/CODEX_MCP_RAW_TOML_PLAN.md b/docs/CODEX_MCP_RAW_TOML_PLAN.md deleted file mode 100644 index 0e99efa3e..000000000 --- a/docs/CODEX_MCP_RAW_TOML_PLAN.md +++ /dev/null @@ -1,1309 +0,0 @@ -# Codex MCP Raw TOML 重构方案 - -## 📋 目录 - -- [背景与目标](#背景与目标) -- [核心设计](#核心设计) -- [技术架构](#技术架构) -- [实施计划](#实施计划) -- [风险控制](#风险控制) -- [测试验证](#测试验证) - ---- - -## 背景与目标 - -### 当前问题 - -1. **数据丢失**:Codex MCP 配置在 TOML ↔ JSON 转换时丢失注释、格式、特殊值类型 -2. **配置复杂**:Codex TOML 支持复杂嵌套结构,强制结构化存储限制灵活性 -3. **用户体验差**:无法保留用户手写的注释和格式偏好 - -### 设计目标 - -1. **保真存储**:Codex MCP 使用 raw TOML 字符串存储,完全避免序列化损失 -2. **架构分离**:Claude/Gemini 继续用结构化 JSON,Codex 用原始文本 -3. **UI 解耦**:MCP 管理面板与当前 app 切换彻底分离 -4. **增量实施**:零改动现有 Claude/Gemini 逻辑,风险可控 - ---- - -## 核心设计 - -### 数据结构设计 - -#### config.json 顶层结构 - -```json -{ - "providers": [ - // 现有 provider 列表,不改 - ], - "mcp": { - // 统一 MCP 结构,仅用于 Claude & Gemini - // ✅ 完全移除 Codex 相关逻辑,apps 字段仅包含 claude/gemini - "servers": { - "fetch": { - "id": "fetch", - "name": "Fetch MCP", - "server": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-fetch"] - }, - "apps": { - "claude": true, - "gemini": false - }, - "description": null, - "homepage": null, - "docs": null, - "tags": [] - } - } - }, - "codexMcp": { - "rawToml": "[mcp]\n# Codex 专用 MCP TOML 片段\n..." - } -} -``` - -#### Rust 数据结构 - -```rust -// src-tauri/src/app_config.rs -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CodexMcpConfig { - /// 完整的 MCP TOML 片段(包含 [mcp] 等) - pub raw_toml: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MultiAppConfig { - /// 版本号(v2 起) - #[serde(default = "default_version")] - pub version: u32, - - /// 应用管理器(claude/codex/gemini) - #[serde(flatten)] - pub apps: HashMap, - - /// MCP 配置(统一结构 + 旧结构,用于迁移) - #[serde(default)] - pub mcp: McpRoot, - - /// Prompt 配置(按客户端分治) - #[serde(default)] - pub prompts: PromptRoot, - - /// 通用配置片段(按应用分治) - #[serde(default)] - pub common_config_snippets: CommonConfigSnippets, - - /// Claude 通用配置片段(旧字段,用于向后兼容迁移) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub claude_common_config_snippet: Option, - - /// Codex MCP raw TOML(新字段,仅 Codex 使用) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub codex_mcp: Option, -} -``` - -### 分层架构 - -``` -┌─────────────────────────────────────────────────┐ -│ UI 层 │ -│ ┌─────────────────────────────────────────┐ │ -│ │ MCP 面板(与 app 切换完全解耦) │ │ -│ │ ├─ Tab1: Claude & Gemini (结构化 JSON) │ │ -│ │ │ - 仅管理 mcp.servers │ │ -│ │ │ - apps 字段仅含 claude/gemini │ │ -│ │ └─ Tab2: Codex (raw TOML 编辑器) │ │ -│ │ - 独立管理 codexMcp.rawToml │ │ -│ └─────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 应用层 │ -│ switch_app() 根据 app 类型选择数据源: │ -│ - Claude/Gemini → mcp.servers (过滤 apps) │ -│ - Codex → codexMcp.rawToml (完全独立) │ -│ │ -│ ✅ 无优先级冲突:两者完全隔离 │ -└─────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 数据层 │ -│ config.json: │ -│ - mcp.servers: 仅 Claude & Gemini │ -│ - codexMcp.rawToml: 仅 Codex │ -│ │ -│ ✅ 单一职责:互不干扰 │ -└─────────────────────────────────────────────────┘ -``` - -### MCP 配置职责划分 - -| 配置源 | 职责 | 数据格式 | 管理方式 | -|--------|------|----------|----------| -| `mcp.servers` | Claude & Gemini MCP | 结构化 JSON | UI 表单(Tab1) | -| `codexMcp.rawToml` | Codex MCP | 原始 TOML 字符串 | 代码编辑器(Tab2) | - -**关键原则**: -- ✅ `mcp.servers` 中的 `apps` 字段**永不包含 `codex`** -- ✅ Codex MCP **仅存储**在 `codexMcp.rawToml` -- ✅ 切换逻辑**完全独立**,无优先级判断 - ---- - -## 技术架构 - -### 后端架构(Rust) - -#### 1. 配置管理 - -**文件**:`src-tauri/src/app_config.rs` - -```rust -impl MultiAppConfig { - pub fn load() -> Result { - // 1. 按 v2 结构加载 MultiAppConfig - let mut config = /* ... 现有 load 实现 ... */; - - let mut updated = false; - - // 2. 执行 Codex MCP → raw TOML 迁移 - // - 仅迁移 v3.6.2 的 mcp.codex.servers → codexMcp.rawToml - // - 迁移后清空 mcp.codex.servers,避免被后续 unified 迁移处理 - if migration::migrate_codex_mcp_to_raw_toml(&mut config)? { - updated = true; - } - - // 3. 执行 unified MCP 迁移(mcp.claude/gemini → mcp.servers) - // - ✅ 此时 mcp.codex 已清空,不会被迁移到 unified - // - unified 结构中 apps 字段仅包含 claude/gemini - if config.migrate_mcp_to_unified()? { - updated = true; - } - - // 4. 其他迁移(Prompt、通用片段等) - // ... - - if updated { - config.save()?; - } - - Ok(config) - } - - pub fn save(&self) -> Result<(), AppError> { - // 序列化时包含 codexMcp 字段 - // ... - } -} -``` - -#### 2. 数据迁移 - -**文件**:`src-tauri/src/migration.rs` - -```rust -/// 将 v3.6.2 的 mcp.codex.servers 迁移为 codexMcp.rawToml -/// -/// **关键行为**: -/// 1. 仅在 codex_mcp 为空且存在旧的 mcp.codex.servers 时执行 -/// 2. 转换后**立即清空 mcp.codex.servers**,避免被 unified 迁移重复处理 -/// 3. 返回 true 表示发生了迁移,需要保存配置 -pub fn migrate_codex_mcp_to_raw_toml( - config: &mut MultiAppConfig, -) -> Result { - // 已迁移过,跳过 - if config.codex_mcp.is_some() { - return Ok(false); - } - - let legacy_servers = &config.mcp.codex.servers; - if legacy_servers.is_empty() { - // 没有旧的 Codex MCP 配置,跳过 - return Ok(false); - } - - // 转换为 TOML - let toml = convert_legacy_codex_mcp_to_toml(legacy_servers)?; - config.codex_mcp = Some(CodexMcpConfig { raw_toml: toml }); - - // ✅ 关键:清空旧数据,确保 unified 迁移不会处理 Codex - config.mcp.codex.servers.clear(); - - log::info!( - "Migrated {} Codex MCP servers to raw TOML and cleared legacy storage", - legacy_servers.len() - ); - - Ok(true) -} - -/// 将 v3.6.2 时代的 mcp.codex.servers (HashMap) -/// 转换为 Codex 所需的 MCP TOML 片段 -fn convert_legacy_codex_mcp_to_toml( - servers: &HashMap, -) -> Result { - let mut toml = String::from("[mcp]\n\n"); - - for (id, entry) in servers { - // 旧结构:entry 是宽松 JSON 对象,包含 name/server/enabled 等字段 - let obj = entry - .as_object() - .ok_or_else(|| AppError::Config(format!( - "无效的 Codex MCP 条目 '{}': 必须为 JSON 对象", - id - )))?; - - let name = obj - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or(id); - - let server = obj.get("server").ok_or_else(|| { - AppError::Config(format!( - "无效的 Codex MCP 条目 '{}': 缺少 server 字段", - id - )) - })?; - - let server_obj = server.as_object().ok_or_else(|| { - AppError::Config(format!( - "无效的 Codex MCP 条目 '{}': server 必须是 JSON 对象", - id - )) - })?; - - toml.push_str("[[mcp.servers]]\n"); - toml.push_str(&format!("name = \"{}\"\n", name)); - - // stdio 类型字段 - if let Some(cmd) = server_obj.get("command").and_then(|v| v.as_str()) { - toml.push_str(&format!("command = \"{}\"\n", cmd)); - } - - if let Some(args) = server_obj.get("args").and_then(|v| v.as_array()) { - let args_str = args - .iter() - .filter_map(|a| a.as_str()) - .map(|a| format!("\"{}\"", a)) - .collect::>() - .join(", "); - if !args_str.is_empty() { - toml.push_str(&format!("args = [{}]\n", args_str)); - } - } - - if let Some(env) = server_obj.get("env").and_then(|v| v.as_object()) { - if !env.is_empty() { - toml.push_str("\n[mcp.servers.env]\n"); - for (k, v) in env { - if let Some(val) = v.as_str() { - toml.push_str(&format!("{} = \"{}\"\n", k, val)); - } - } - } - } - - if let Some(cwd) = server_obj.get("cwd").and_then(|v| v.as_str()) { - toml.push_str(&format!("cwd = \"{}\"\n", cwd)); - } - - // http 类型字段 - if let Some(url) = server_obj.get("url").and_then(|v| v.as_str()) { - toml.push_str(&format!("url = \"{}\"\n", url)); - } - - if let Some(t) = server_obj.get("type").and_then(|v| v.as_str()) { - toml.push_str(&format!("type = \"{}\"\n", t)); - } - - if let Some(headers) = server_obj.get("headers").and_then(|v| v.as_object()) { - if !headers.is_empty() { - toml.push_str("\n[mcp.servers.headers]\n"); - for (k, v) in headers { - if let Some(val) = v.as_str() { - toml.push_str(&format!("{} = \"{}\"\n", k, val)); - } - } - } - } - - toml.push_str("\n"); - } - - Ok(toml) -} -``` - -#### 3. Tauri 命令 - -**文件**:`src-tauri/src/commands/mcp.rs` - -```rust -/// 获取 Codex MCP 配置 -#[tauri::command] -pub async fn get_codex_mcp_config( - state: State<'_, AppState> -) -> Result { - let config = state.config.read().unwrap(); - - if let Some(codex_mcp) = &config.codex_mcp { - Ok(codex_mcp.raw_toml.clone()) - } else { - // 返回默认模板 - Ok(String::from( - "[mcp]\n# 在这里填写 Codex MCP 配置\n# 示例:\n# [[mcp.servers]]\n# name = \"example\"\n# command = \"npx\"\n# args = [\"-y\", \"@modelcontextprotocol/server-example\"]\n" - )) - } -} - -/// 更新 Codex MCP 配置 -#[tauri::command] -pub async fn update_codex_mcp_config( - state: State<'_, AppState>, - raw_toml: String, -) -> Result<(), String> { - // 1. 语法验证 - toml::from_str::(&raw_toml) - .map_err(|e| format!("TOML syntax error: {}", e))?; - - // 2. 可选警告 - if !raw_toml.contains("[mcp") { - log::warn!("Codex MCP TOML doesn't contain [mcp] section"); - } - - // 3. 保存 - let mut config = state.config.write().unwrap(); - config.codex_mcp = Some(CodexMcpConfig { - raw_toml: raw_toml.clone(), - }); - config.save() - .map_err(|e| format!("Failed to save config: {}", e))?; - - Ok(()) -} - -/// 验证 TOML 语法(前端可在保存前调用) -#[tauri::command] -pub async fn validate_codex_mcp_toml( - raw_toml: String -) -> Result { - match toml::from_str::(&raw_toml) { - Ok(_) => Ok(ValidateResult { - valid: true, - error: None, - warnings: vec![], - }), - Err(e) => Ok(ValidateResult { - valid: false, - error: Some(e.to_string()), - warnings: vec![], - }), - } -} - -#[derive(Debug, Serialize)] -pub struct ValidateResult { - pub valid: bool, - pub error: Option, - pub warnings: Vec, -} - -/// 从 Codex live 配置导入 MCP 段 -#[tauri::command] -pub async fn import_codex_mcp_from_live() -> Result { - let config_path = get_codex_config_path() - .map_err(|e| e.to_string())?; - - if !config_path.exists() { - return Ok(String::from("[mcp]\n# No existing Codex config found\n")); - } - - let content = fs::read_to_string(&config_path) - .map_err(|e| format!("Failed to read Codex config: {}", e))?; - - let mcp_section = extract_mcp_section_from_toml(&content)?; - Ok(mcp_section) -} - -fn extract_mcp_section_from_toml(content: &str) -> Result { - use toml_edit::DocumentMut; - - let doc = content.parse::() - .map_err(|e| format!("Invalid TOML: {}", e))?; - - if let Some(mcp_item) = doc.get("mcp") { - let mut result = String::from("[mcp]\n"); - result.push_str(&mcp_item.to_string()); - Ok(result) - } else { - Ok(String::from("[mcp]\n# No MCP config found in live file\n")) - } -} -``` - -#### 3. 切换逻辑 - -**文件**:`src-tauri/src/services/provider.rs` - -```rust -impl ProviderService { - /// 切换到 Codex provider - pub fn switch_to_codex( - &self, - provider: &Provider - ) -> Result<(), AppError> { - // 1. 读取 Codex MCP 配置(完全独立于 unified) - let codex_mcp = { - let config = self.state.config.read().unwrap(); - config.codex_mcp.clone() - }; - - // 2. 生成最终配置(base + MCP) - let final_toml = self.apply_codex_config(provider, &codex_mcp)?; - - // 3. 写入 live 文件 - self.write_codex_config(&final_toml)?; - - Ok(()) - } - - fn apply_codex_config( - &self, - provider: &Provider, - codex_mcp: &Option, - ) -> Result { - // 1. 生成基础配置(不含 MCP) - let mut base_config = self.generate_codex_base_config(provider)?; - - // 2. 追加 MCP 配置(如果有) - if let Some(mcp_cfg) = codex_mcp { - let trimmed = mcp_cfg.raw_toml.trim(); - if !trimmed.is_empty() { - // 确保有换行分隔 - if !base_config.ends_with('\n') { - base_config.push('\n'); - } - base_config.push('\n'); - base_config.push_str(trimmed); - } - } - - // 3. 验证最终 TOML 可解析 - toml::from_str::(&base_config) - .map_err(|e| AppError::Config(format!( - "Generated Codex config is invalid: {}", - e - )))?; - - Ok(base_config) - } - - /// 切换到 Claude/Gemini - pub fn switch_to_claude_or_gemini( - &self, - provider: &Provider, - app_type: AppType, - ) -> Result<(), AppError> { - // 从 unified MCP 读取配置(apps 字段仅含 claude/gemini) - let mcp_servers = { - let config = self.state.config.read().unwrap(); - config.mcp.servers - .values() - .filter(|s| s.apps.get(&app_type.to_string()).unwrap_or(&false)) - .cloned() - .collect::>() - }; - - // 生成并写入配置 - // ... - - Ok(()) - } -} -``` - -**关键点**: -- ✅ Codex 切换**完全不读取** `mcp.servers` -- ✅ Claude/Gemini 切换**完全不读取** `codexMcp` -- ✅ 无优先级判断,逻辑简单清晰 - -### 前端架构(React + TypeScript) - -#### 1. API 层 - -**文件**:`src/lib/api/mcp.ts` - -```typescript -export const codexMcpApi = { - /** - * 获取 Codex MCP 配置(raw TOML) - */ - get: () => invoke('get_codex_mcp_config'), - - /** - * 更新 Codex MCP 配置 - */ - update: (rawToml: string) => - invoke('update_codex_mcp_config', { rawToml }), - - /** - * 验证 TOML 语法 - */ - validate: (rawToml: string) => - invoke('validate_codex_mcp_toml', { rawToml }), - - /** - * 从 Codex live 配置导入 - */ - importFromLive: () => - invoke('import_codex_mcp_from_live'), -}; - -export interface ValidateResult { - valid: boolean; - error?: string; - warnings: string[]; -} -``` - -#### 2. Hooks - -**文件**:`src/hooks/useCodexMcp.ts` - -```typescript -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { codexMcpApi } from '@/lib/api/mcp'; -import { toast } from 'sonner'; - -export function useCodexMcp() { - const queryClient = useQueryClient(); - - // 查询 - const query = useQuery({ - queryKey: ['codexMcp'], - queryFn: codexMcpApi.get, - }); - - // 更新 - const updateMutation = useMutation({ - mutationFn: codexMcpApi.update, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['codexMcp'] }); - toast.success('Codex MCP 配置已保存'); - }, - onError: (error: Error) => { - toast.error(`保存失败: ${error.message}`); - }, - }); - - // 验证 - const validateMutation = useMutation({ - mutationFn: codexMcpApi.validate, - }); - - // 导入 - const importMutation = useMutation({ - mutationFn: codexMcpApi.importFromLive, - onSuccess: (data) => { - queryClient.setQueryData(['codexMcp'], data); - toast.success('已从 Codex 配置导入 MCP'); - }, - onError: (error: Error) => { - toast.error(`导入失败: ${error.message}`); - }, - }); - - return { - rawToml: query.data ?? '', - isLoading: query.isLoading, - update: updateMutation.mutate, - // 保存前需要拿到校验结果,因此对外暴露 mutateAsync,便于 await - validate: validateMutation.mutateAsync, - importFromLive: importMutation.mutate, - }; -} -``` - -#### 3. UI 组件 - -**文件**:`src/components/mcp/McpPanel.tsx` - -```typescript -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ClaudeGeminiMcpTab } from './ClaudeGeminiMcpTab'; -import { CodexMcpTab } from './CodexMcpTab'; - -export function McpPanel() { - return ( - - - - Claude & Gemini - - - Codex - - - - - - - - - - - - ); -} -``` - -**文件**:`src/components/mcp/ClaudeGeminiMcpTab.tsx` - -```typescript -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { useMcp } from '@/hooks/useMcp'; - -/** - * Claude & Gemini 的 MCP 管理 Tab - * - * ✅ 仅操作 mcp.servers - * ✅ apps 字段仅含 claude/gemini(不含 codex) - * ✅ 完全独立于当前选中的 app - */ -export function ClaudeGeminiMcpTab() { - const { servers, addServer, updateServer, deleteServer } = useMcp(); - - return ( -
- - - 管理 Claude 和 Gemini 的 MCP 服务器。 -
- 注意:Codex MCP 在专用 Tab 管理(raw TOML 格式)。 -
-
- - {/* 现有 MCP 列表组件,但需确保: */} - {/* 1. 表单中 apps 选项仅显示 claude/gemini */} - {/* 2. 过滤掉可能的历史遗留 codex 数据 */} - !s.apps.codex)} - onAdd={addServer} - onUpdate={updateServer} - onDelete={deleteServer} - availableApps={['claude', 'gemini']} // ✅ 限制可选应用 - /> -
- ); -} -``` - -**文件**:`src/components/mcp/CodexMcpTab.tsx` - -```typescript -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { CodexMcpEditor } from './CodexMcpEditor'; -import { useCodexMcp } from '@/hooks/useCodexMcp'; -import { Alert, AlertDescription } from '@/components/ui/alert'; - -export function CodexMcpTab() { - const { rawToml, isLoading, update, validate, importFromLive } = useCodexMcp(); - const [localValue, setLocalValue] = useState(rawToml); - const [validationError, setValidationError] = useState(null); - - // 当后端数据加载完成或导入时,同步到本地编辑器 - useEffect(() => { - setLocalValue(rawToml); - }, [rawToml]); - - const handleSave = async () => { - // 保存前验证 - const result = await validate(localValue); - - if (!result.valid) { - setValidationError(result.error ?? 'Unknown error'); - return; - } - - setValidationError(null); - update(localValue); - }; - - const handleImport = () => { - importFromLive(); - }; - - if (isLoading) { - return
加载中...
; - } - - return ( -
- - - 直接编辑 Codex MCP TOML 配置。修改会在下次切换到 Codex 时生效。 - - - - {validationError && ( - - - TOML 语法错误: {validationError} - - - )} - - - -
- - - -
-
- ); -} -``` - -**文件**:`src/components/mcp/CodexMcpEditor.tsx` - -```typescript -import { useEffect, useRef } from 'react'; -import { EditorView, basicSetup } from 'codemirror'; -import { toml } from '@codemirror/lang-toml'; -import { oneDark } from '@codemirror/theme-one-dark'; -import { linter, Diagnostic } from '@codemirror/lint'; -import * as TOML from 'smol-toml'; - -const tomlLinter = linter((view) => { - const diagnostics: Diagnostic[] = []; - const content = view.state.doc.toString(); - - try { - TOML.parse(content); - } catch (e: any) { - diagnostics.push({ - from: 0, - to: content.length, - severity: 'error', - message: `TOML Syntax Error: ${e.message}`, - }); - } - - return diagnostics; -}); - -interface Props { - value: string; - onChange: (value: string) => void; -} - -export function CodexMcpEditor({ value, onChange }: Props) { - const editorRef = useRef(null); - const viewRef = useRef(); - - useEffect(() => { - if (!editorRef.current) return; - - const view = new EditorView({ - doc: value, - extensions: [ - basicSetup, - toml(), - oneDark, - tomlLinter, - EditorView.updateListener.of((update) => { - if (update.docChanged) { - onChange(update.state.doc.toString()); - } - }), - ], - parent: editorRef.current, - }); - - viewRef.current = view; - - return () => view.destroy(); - }, []); - - // 外部值变化时更新编辑器 - useEffect(() => { - if (!viewRef.current) return; - const currentValue = viewRef.current.state.doc.toString(); - if (currentValue !== value) { - viewRef.current.dispatch({ - changes: { - from: 0, - to: currentValue.length, - insert: value, - }, - }); - } - }, [value]); - - return ( -
- ); -} -``` - ---- - -## 实施计划 - -### Phase 0: 准备工作 - -**时间**:0.5 天 - -- [ ] 创建开发分支 `feature/codex-mcp-raw-toml` -- [ ] 安装前端依赖:`pnpm add @codemirror/lang-toml` -- [ ] 备份现有配置文件用于测试 - -### Phase 1: 后端基础(P0) - -**时间**:1.5 天 - -**任务**: - -- [ ] 在 `app_config.rs` 中定义 `CodexMcpConfig` -- [ ] 修改 `MultiAppConfig` 添加 `codex_mcp` 字段 -- [ ] 更新 `MultiAppConfig::load()` 和 `save()` 支持新字段 -- [ ] 编写迁移函数 `migrate_codex_mcp_to_raw_toml` - - [ ] 实现 `convert_servers_map_to_toml` - - [ ] 处理 stdio 类型服务器 - - [ ] 处理 http 类型服务器 -- [ ] 在 `lib.rs` 启动时执行迁移 -- [ ] 单元测试:迁移逻辑正确性 - -**验收标准**: - -- 现有配置可正确迁移为 raw TOML -- config.json 包含 `codexMcp` 字段 -- 迁移不影响 Claude/Gemini 配置 - -### Phase 2: 命令层(P0) - -**时间**:1 天 - -**任务**: - -- [ ] 在 `commands/mcp.rs` 实现命令: - - [ ] `get_codex_mcp_config` - - [ ] `update_codex_mcp_config` - - [ ] `validate_codex_mcp_toml` - - [ ] `import_codex_mcp_from_live` -- [ ] 实现 `extract_mcp_section_from_toml` 辅助函数 -- [ ] 在 `lib.rs` 注册新命令 -- [ ] 集成测试:命令调用正确性 - -**验收标准**: - -- 所有命令可通过 Tauri invoke 正常调用 -- TOML 语法验证准确 -- 从 live 配置导入功能正常 - -### Phase 3: 切换逻辑(P0) - -**时间**:1 天 - -**任务**: - -- [ ] 修改 `services/provider.rs` 的 Codex 切换逻辑 - - [ ] 实现 `switch_to_codex`(仅读取 `codexMcp`) - - [ ] 实现 `apply_codex_config`(拼接 base + raw TOML) - - [ ] 添加最终 TOML 验证 -- [ ] 确保 Claude/Gemini 切换逻辑不读取 `codexMcp` -- [ ] 原子写入机制验证 -- [ ] 集成测试:Codex 切换后配置正确 - -**验收标准**: - -- Codex 切换时,config.toml 包含 raw TOML 的 MCP 段 -- Claude/Gemini 切换时,仅使用 `mcp.servers` 中 `apps.claude/gemini=true` 的项 -- 生成的配置可被对应应用正确解析 -- 切换失败时不损坏现有配置 - -### Phase 4: 前端 API(P0) - -**时间**:0.5 天 - -**任务**: - -- [ ] 在 `lib/api/mcp.ts` 创建 `codexMcpApi` -- [ ] 定义 TypeScript 类型 `ValidateResult` -- [ ] 在 `hooks/useCodexMcp.ts` 创建 Hook - - [ ] useQuery 读取配置 - - [ ] useMutation 更新配置 - - [ ] useMutation 验证语法 - - [ ] useMutation 导入配置 - -**验收标准**: - -- API 调用成功返回数据 -- Hook 状态管理正确 -- 错误处理完善 - -### Phase 5: UI 实现(P0) - -**时间**:2 天 - -**任务**: - -- [ ] 重构 `McpPanel.tsx` 为 Tabs 布局 -- [ ] 创建 `ClaudeGeminiMcpTab.tsx` - - [ ] 移除对 `currentApp` 的依赖 - - [ ] 直接操作 `mcp.servers` - - [ ] **限制 `availableApps` 为 `['claude', 'gemini']`** - - [ ] **过滤掉 `apps.codex` 的历史数据** - - [ ] 添加提示:"Codex MCP 在专用 Tab 管理" -- [ ] 创建 `CodexMcpTab.tsx` - - [ ] 集成编辑器组件 - - [ ] 实现保存/导入/重置逻辑 - - [ ] 添加验证错误提示 -- [ ] 创建 `CodexMcpEditor.tsx` - - [ ] 集成 CodeMirror 6 - - [ ] 配置 TOML 语法高亮 - - [ ] 集成 TOML linter - - [ ] 实现双向绑定 -- [ ] 国际化:添加相关翻译 key -- [ ] **更新现有 MCP 表单组件,移除 `codex` 选项** - -**验收标准**: - -- MCP 面板有两个独立 Tab -- Tab1 (Claude & Gemini): - - `apps` 选项仅显示 claude/gemini - - 不显示任何 `apps.codex=true` 的服务器 - - 无法添加/编辑 Codex MCP -- Tab2 (Codex): - - 可正常编辑 raw TOML - - 语法错误有实时提示 - - 保存后配置持久化 - -### Phase 6: 增强功能(P1) - -**时间**:1 天 - -**任务**: - -- [ ] 添加 TOML 模板快捷插入功能 -- [ ] 导出到 Codex live 配置功能 -- [ ] 配置历史记录(可选) -- [ ] 改进错误提示(显示行号) - -**验收标准**: - -- 模板插入功能可用 -- 导出功能正常 - -### Phase 7: 测试与文档(P0) - -**时间**:1 天 - -**任务**: - -- [ ] 端到端测试: - - [ ] 新用户首次启动 - - [ ] 现有用户迁移场景(v3.6.2 → v3.7.0) - - [ ] 验证迁移后 `mcp.codex.servers` 被清空 - - [ ] 验证 unified MCP 不包含 `apps.codex` - - [ ] Claude ↔ Codex ↔ Gemini 切换 - - [ ] Codex MCP 编辑后切换生效 - - [ ] Tab1 无法操作 Codex MCP -- [ ] 更新 `CLAUDE.md` 文档 - - [ ] 明确 MCP 配置职责划分 - - [ ] 更新配置文件路径说明 -- [ ] 编写 migration guide -- [ ] 添加 CHANGELOG 条目 - -**验收标准**: - -- 所有测试用例通过 -- 文档完整准确 -- 迁移逻辑无数据丢失 - ---- - -## 风险控制 - -### 1. 数据丢失风险 - -**风险**:迁移过程中旧配置丢失 - -**控制措施**: - -- ✅ 迁移前自动备份 config.json(带时间戳) -- ✅ **迁移后清空 `mcp.codex.servers`,但不删除 `mcp.codex` 根节点**(保留结构用于回滚) -- ✅ 迁移日志记录详细信息(服务器数量、时间戳等) -- ✅ 提供回滚命令(Phase 6+) - -### 2. TOML 格式错误 - -**风险**:用户手写 TOML 导致 Codex 配置损坏 - -**控制措施**: - -- ✅ 保存前强制验证语法 -- ✅ 实时 linting 提示错误 -- ✅ 切换前再次验证最终配置 -- ✅ 写入失败时自动回滚(已有 `.bak` 机制) - -### 3. 并发写入 - -**风险**:多实例同时修改配置 - -**控制措施**: - -- ✅ 使用 RwLock 保护 config 访问 -- ✅ 使用 tauri-plugin-single-instance(已集成) - -### 4. Unified MCP 污染 - -**风险**:历史数据中存在 `apps.codex=true` 的服务器 - -**控制措施**: - -- ✅ **迁移时清空 `mcp.codex.servers`**,阻止 unified 迁移处理 Codex -- ✅ **前端过滤**:Tab1 显示时过滤掉 `apps.codex=true` 的项 -- ✅ **表单限制**:`availableApps` 仅包含 `['claude', 'gemini']` -- ✅ **后端验证**(可选):保存 unified MCP 时检查并拒绝包含 `codex` 的 apps - ---- - -## 测试验证 - -### 单元测试 - -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_convert_stdio_server_to_toml() { - let server = McpServer { - name: "test".into(), - server: ServerSpec::Stdio { - command: "npx".into(), - args: Some(vec!["-y".into(), "test".into()]), - env: Some(HashMap::from([ - ("KEY".into(), "value".into()) - ])), - cwd: None, - }, - // ... - }; - - let toml = convert_server_to_toml("test", &server).unwrap(); - - assert!(toml.contains("command = \"npx\"")); - assert!(toml.contains("args = [\"-y\", \"test\"]")); - assert!(toml.contains("KEY = \"value\"")); - } - - #[test] - fn test_toml_validation() { - let valid_toml = "[mcp]\n[[mcp.servers]]\nname = \"test\"\n"; - assert!(validate_toml(valid_toml).is_ok()); - - let invalid_toml = "[mcp\n[[mcp.servers]]\n"; - assert!(validate_toml(invalid_toml).is_err()); - } -} -``` - -### 集成测试场景 - -| 场景 | 步骤 | 预期结果 | -|------|------|----------| -| 新用户首次启动 | 1. 删除 config.json
2. 启动应用
3. 打开 Codex MCP Tab | 显示默认模板 | -| 现有用户迁移 | 1. 使用 v3.6.2 config.json(含 `mcp.codex.servers`)
2. 启动应用
3. 检查 config.json | - `codexMcp.rawToml` 存在且内容正确
- `mcp.codex.servers` 为空对象 `{}`
- `mcp.servers` 不含 `apps.codex` | -| 编辑 Codex MCP | 1. 在 Tab2 编辑 TOML
2. 保存
3. 检查 config.json | `codexMcp.rawToml` 更新 | -| 切换到 Codex | 1. 编辑 Codex MCP
2. 切换到 Codex provider
3. 检查 `~/.codex/config.toml` | MCP 段正确写入,与 raw TOML 一致 | -| 切换到 Claude | 1. 在 Tab1 添加 Claude MCP
2. 切换到 Claude provider
3. 检查 `~/.claude/settings.json` | 仅包含 `apps.claude=true` 的服务器 | -| TOML 语法错误 | 1. 在 Tab2 输入错误 TOML
2. 保存 | 显示错误提示,拒绝保存 | -| Tab1 隔离性 | 1. 打开 Tab1
2. 尝试添加服务器 | - `apps` 选项仅显示 claude/gemini
- 无法选择 codex | -| 历史数据过滤 | 1. 手动在 config.json 添加 `apps.codex=true` 的服务器
2. 打开 Tab1 | 该服务器不在列表中显示 | -| 从 live 导入 | 1. 手动编辑 `~/.codex/config.toml`
2. 点击 Tab2 "导入"
3. 检查编辑器 | 显示导入的 MCP 配置 | - -### 性能测试 - -- [ ] 大型 TOML(>10KB)编辑性能 -- [ ] CodeMirror 初始化时间(<500ms) -- [ ] 配置切换时间(<200ms) - ---- - -## 依赖项 - -### 前端新增 - -```bash -pnpm add @codemirror/lang-toml -``` - -### 后端(已有) - -- `toml = "0.8"` -- `toml_edit = "0.22"` - ---- - -## 时间线 - -| Phase | 工作量 | 累计 | -|-------|--------|------| -| Phase 0: 准备工作 | 0.5 天 | 0.5 天 | -| Phase 1: 后端基础 | 1.5 天 | 2 天 | -| Phase 2: 命令层 | 1 天 | 3 天 | -| Phase 3: 切换逻辑 | 1 天 | 4 天 | -| Phase 4: 前端 API | 0.5 天 | 4.5 天 | -| Phase 5: UI 实现 | 2 天 | 6.5 天 | -| Phase 6: 增强功能(可选)| 1 天 | 7.5 天 | -| Phase 7: 测试与文档 | 1 天 | 8.5 天 | - -**总计**:8.5 天(约 2 周) - -**MVP(最小可行产品)**:Phase 0-5 + Phase 7 = 7 天 - ---- - -## 回滚计划 - -如果重构出现严重问题,执行以下步骤: - -1. **恢复代码**: - ```bash - git checkout main - git branch -D feature/codex-mcp-raw-toml - ``` - -2. **恢复配置**: - ```bash - # 迁移时会自动备份为 config.v3.backup..json - cp ~/.cc-switch/config.v3.backup.*.json ~/.cc-switch/config.json - ``` - -3. **重启应用** - ---- - -## 成功标准 - -- ✅ 现有用户配置无损迁移 -- ✅ Codex MCP 配置保留注释和格式 -- ✅ MCP 面板与 app 切换完全解耦 -- ✅ Claude/Gemini 逻辑零改动 -- ✅ 所有测试用例通过 -- ✅ 文档完整更新 - ---- - -## 附录 - -### 示例配置 - -#### 迁移前(v3.6.2) - -```json -{ - "providers": [...], - "mcp": { - "codex": { - "servers": { - "fetch": { - "id": "fetch", - "name": "Fetch MCP", - "server": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-fetch"] - }, - "enabled": true - } - } - } - } -} -``` - -#### 迁移后(v3.7.0) - -```json -{ - "providers": [...], - "mcp": { - "servers": { - "fetch": { - "id": "fetch", - "name": "Fetch MCP", - "server": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-fetch"] - }, - "apps": { - "claude": true, - "gemini": false - } - } - }, - "codex": { - "servers": {} // ✅ 已清空,但保留结构用于回滚 - } - }, - "codexMcp": { - "rawToml": "[mcp]\n\n[[mcp.servers]]\nname = \"Fetch MCP\"\ncommand = \"npx\"\nargs = [\"-y\", \"@modelcontextprotocol/server-fetch\"]\n" - } -} -``` - -### 相关文档 - -- [Codex 官方 MCP 文档](https://codex.dev/docs/mcp) -- [TOML 规范](https://toml.io/en/) -- [CodeMirror 6 文档](https://codemirror.net/docs/) -- [项目 CLAUDE.md](../CLAUDE.md) - ---- - -**文档版本**:2.0 -**创建时间**:2025-11-18 -**最后更新**:2025-11-18 -**负责人**:Jason Young - ---- - -## 版本历史 - -### v2.0 (2025-11-18) -- ✅ **架构简化**:完全移除 unified MCP 中的 Codex 支持 -- ✅ **单一职责**:`mcp.servers` 仅用于 Claude/Gemini,`codexMcp.rawToml` 仅用于 Codex -- ✅ **迁移增强**:清空 `mcp.codex.servers` 避免重复处理 -- ✅ **UI 隔离**:Tab1 限制 `availableApps`,过滤 Codex 数据 -- ✅ **测试覆盖**:增加 Tab1 隔离性、历史数据过滤等场景 - -### v1.0 (2025-11-18) -- 初始版本 diff --git a/docs/REFACTORING_CHECKLIST.md b/docs/REFACTORING_CHECKLIST.md deleted file mode 100644 index 657a8e161..000000000 --- a/docs/REFACTORING_CHECKLIST.md +++ /dev/null @@ -1,490 +0,0 @@ -# CC Switch 重构实施清单 - -> 用于跟踪重构进度的详细检查清单 - -**开始日期**: ___________ -**预计完成**: ___________ -**当前阶段**: ___________ - ---- - -## 📋 阶段 0: 准备阶段 (预计 1 天) - -### 环境准备 - -- [ ] 创建新分支 `refactor/modernization` -- [ ] 创建备份标签 `git tag backup-before-refactor` -- [ ] 备份用户配置文件 `~/.cc-switch/config.json` -- [ ] 通知团队成员重构开始 - -### 依赖安装 - -```bash -pnpm add @tanstack/react-query -pnpm add react-hook-form @hookform/resolvers -pnpm add zod -pnpm add sonner -pnpm add next-themes -pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu -pnpm add @radix-ui/react-label @radix-ui/react-select -pnpm add @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs -pnpm add class-variance-authority clsx tailwind-merge tailwindcss-animate -``` - -- [ ] 安装核心依赖 (上述命令) -- [ ] 验证依赖安装成功 `pnpm install` -- [ ] 验证编译通过 `pnpm typecheck` - -### 配置文件 - -- [ ] 创建 `components.json` -- [ ] 更新 `tsconfig.json` 添加路径别名 -- [ ] 更新 `vite.config.mts` 添加路径解析 -- [ ] 验证开发服务器启动 `pnpm dev` - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 1: 基础设施 (预计 2-3 天) - -### 1.1 工具函数和基础组件 - -- [ ] 创建 `src/lib/utils.ts` (cn 函数) -- [ ] 创建 `src/components/ui/button.tsx` -- [ ] 创建 `src/components/ui/dialog.tsx` -- [ ] 创建 `src/components/ui/input.tsx` -- [ ] 创建 `src/components/ui/label.tsx` -- [ ] 创建 `src/components/ui/textarea.tsx` -- [ ] 创建 `src/components/ui/select.tsx` -- [ ] 创建 `src/components/ui/switch.tsx` -- [ ] 创建 `src/components/ui/tabs.tsx` -- [ ] 创建 `src/components/ui/sonner.tsx` -- [ ] 创建 `src/components/ui/form.tsx` - -**测试**: -- [ ] 验证所有 UI 组件可以正常导入 -- [ ] 创建一个测试页面验证组件样式 - -### 1.2 Query Client 设置 - -- [ ] 创建 `src/lib/query/queryClient.ts` -- [ ] 配置默认选项 (retry, staleTime 等) -- [ ] 导出 queryClient 实例 - -### 1.3 API 层 - -- [ ] 创建 `src/lib/api/providers.ts` - - [ ] getAll - - [ ] getCurrent - - [ ] add - - [ ] update - - [ ] delete - - [ ] switch - - [ ] importDefault - - [ ] updateTrayMenu - -- [ ] 创建 `src/lib/api/settings.ts` - - [ ] get - - [ ] save - -- [ ] 创建 `src/lib/api/mcp.ts` - - [ ] getConfig - - [ ] upsertServer - - [ ] deleteServer - -- [ ] 创建 `src/lib/api/index.ts` (聚合导出) - -**测试**: -- [ ] 验证 API 调用不会出现运行时错误 -- [ ] 确认类型定义正确 - -### 1.4 Query Hooks - -- [ ] 创建 `src/lib/query/queries.ts` - - [ ] useProvidersQuery - - [ ] useSettingsQuery - - [ ] useMcpConfigQuery - -- [ ] 创建 `src/lib/query/mutations.ts` - - [ ] useAddProviderMutation - - [ ] useSwitchProviderMutation - - [ ] useDeleteProviderMutation - - [ ] useUpdateProviderMutation - - [ ] useSaveSettingsMutation - -- [ ] 创建 `src/lib/query/index.ts` (聚合导出) - -**测试**: -- [ ] 在临时组件中测试每个 hook -- [ ] 验证 loading/error 状态正确 -- [ ] 验证缓存和自动刷新工作 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 2: 核心功能重构 (预计 3-4 天) - -### 2.1 主题系统 - -- [ ] 创建 `src/components/theme-provider.tsx` -- [ ] 创建 `src/components/mode-toggle.tsx` -- [ ] 更新 `src/index.css` 添加主题变量 -- [ ] 删除 `src/hooks/useDarkMode.ts` -- [ ] 更新所有组件使用新的主题系统 - -**测试**: -- [ ] 验证主题切换正常工作 -- [ ] 验证系统主题跟随功能 -- [ ] 验证主题持久化 - -### 2.2 更新 main.tsx - -- [ ] 引入 QueryClientProvider -- [ ] 引入 ThemeProvider -- [ ] 添加 Toaster 组件 -- [ ] 移除旧的 API 导入 - -**测试**: -- [ ] 验证应用可以正常启动 -- [ ] 验证 Context 正确传递 - -### 2.3 重构 App.tsx - -- [ ] 使用 useProvidersQuery 替代手动状态管理 -- [ ] 移除所有 loadProviders 相关代码 -- [ ] 移除手动 notification 状态 -- [ ] 简化事件监听逻辑 -- [ ] 更新对话框为新的 Dialog 组件 - -**目标**: 将 412 行代码减少到 ~100 行 - -**测试**: -- [ ] 验证供应商列表正常加载 -- [ ] 验证切换 Claude/Codex 正常工作 -- [ ] 验证事件监听正常工作 - -### 2.4 重构 ProviderList - -- [ ] 创建 `src/components/providers/ProviderList.tsx` -- [ ] 使用 mutation hooks 处理操作 -- [ ] 移除 onNotify prop -- [ ] 移除手动状态管理 - -**测试**: -- [ ] 验证供应商列表渲染 -- [ ] 验证切换操作 -- [ ] 验证删除操作 - -### 2.5 重构表单系统 - -- [ ] 创建 `src/lib/schemas/provider.ts` (Zod schema) -- [ ] 创建 `src/components/providers/ProviderForm.tsx` - - [ ] 使用 react-hook-form - - [ ] 使用 zodResolver - - [ ] 字段级验证 - -- [ ] 创建 `src/components/providers/AddProviderDialog.tsx` - - [ ] 使用新的 Dialog 组件 - - [ ] 集成 ProviderForm - - [ ] 使用 useAddProviderMutation - -- [ ] 创建 `src/components/providers/EditProviderDialog.tsx` - - [ ] 使用新的 Dialog 组件 - - [ ] 集成 ProviderForm - - [ ] 使用 useUpdateProviderMutation - -**测试**: -- [ ] 验证表单验证正常工作 -- [ ] 验证错误提示显示正确 -- [ ] 验证提交操作成功 -- [ ] 验证表单重置功能 - -### 2.6 清理旧组件 - -- [x] 删除 `src/components/AddProviderModal.tsx` -- [x] 删除 `src/components/EditProviderModal.tsx` -- [x] 更新所有引用这些组件的地方 -- [x] 删除 `src/components/ProviderForm.tsx` 及 `src/components/ProviderForm/` - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 3: 设置和辅助功能 (预计 2-3 天) - -### 3.1 重构 SettingsDialog - -- [ ] 创建 `src/components/settings/SettingsDialog.tsx` - - [ ] 使用 Tabs 组件 - - [ ] 集成各个设置子组件 - -- [ ] 创建 `src/components/settings/GeneralSettings.tsx` - - [ ] 语言设置 - - [ ] 配置目录设置 - - [ ] 其他通用设置 - -- [ ] 创建 `src/components/settings/AboutSection.tsx` - - [ ] 版本信息 - - [ ] 更新检查 - - [ ] 链接 - -- [ ] 创建 `src/components/settings/ImportExportSection.tsx` - - [ ] 导入功能 - - [ ] 导出功能 - -**目标**: 将 643 行拆分为 4-5 个小组件,每个 100-150 行 - -**测试**: -- [ ] 验证设置保存功能 -- [ ] 验证导入导出功能 -- [ ] 验证更新检查功能 - -### 3.2 重构通知系统 - -- [ ] 在所有 mutations 中使用 `toast` 替代 `showNotification` -- [ ] 移除 App.tsx 中的 notification 状态 -- [ ] 移除自定义通知组件 - -**测试**: -- [ ] 验证成功通知显示 -- [ ] 验证错误通知显示 -- [ ] 验证通知自动消失 - -### 3.3 重构确认对话框 - -- [ ] 更新 `src/components/ConfirmDialog.tsx` 使用新的 Dialog -- [ ] 或者直接使用 shadcn/ui 的 AlertDialog - -**测试**: -- [ ] 验证删除确认对话框 -- [ ] 验证其他确认场景 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 4: 清理和优化 (预计 1-2 天) - -### 4.1 移除旧代码 - -- [x] 删除 `src/lib/styles.ts` -- [x] 从 `src/lib/tauri-api.ts` 移除 `window.api` 绑定 -- [x] 精简 `src/lib/tauri-api.ts`,只保留事件监听相关 -- [x] 删除或更新 `src/vite-env.d.ts` 中的过时类型 - -### 4.2 代码审查 - -- [ ] 检查所有 TODO 注释 -- [x] 检查是否还有 `window.api` 调用 -- [ ] 检查是否还有手动状态管理 -- [x] 统一代码风格 - -### 4.3 类型检查 - -- [x] 运行 `pnpm typecheck` 确保无错误 -- [x] 修复所有类型错误 -- [x] 更新类型定义 - -### 4.4 性能优化 - -- [ ] 检查是否有不必要的重渲染 -- [ ] 添加必要的 React.memo -- [ ] 优化 Query 缓存配置 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 5: 测试和修复 (预计 2-3 天) - -### 5.1 功能测试 - -#### 供应商管理 -- [ ] 添加供应商 (Claude) -- [ ] 添加供应商 (Codex) -- [ ] 编辑供应商 -- [ ] 删除供应商 -- [ ] 切换供应商 -- [ ] 导入默认配置 - -#### 应用切换 -- [ ] Claude <-> Codex 切换 -- [ ] 切换后数据正确加载 -- [ ] 切换后托盘菜单更新 - -#### 设置 -- [ ] 保存通用设置 -- [ ] 切换语言 -- [ ] 配置目录选择 -- [ ] 导入配置 -- [ ] 导出配置 - -#### UI 交互 -- [ ] 主题切换 (亮色/暗色) -- [ ] 对话框打开/关闭 -- [ ] 表单验证 -- [ ] Toast 通知 - -#### MCP 管理 -- [ ] 列表显示 -- [ ] 添加 MCP -- [ ] 编辑 MCP -- [ ] 删除 MCP -- [ ] 启用/禁用 MCP - -### 5.2 边界情况测试 - -- [ ] 空供应商列表 -- [ ] 无效配置文件 -- [ ] 网络错误 -- [ ] 后端错误响应 -- [ ] 并发操作 -- [ ] 表单输入边界值 - -### 5.3 兼容性测试 - -- [ ] Windows 测试 -- [ ] macOS 测试 -- [ ] Linux 测试 - -### 5.4 性能测试 - -- [ ] 100+ 供应商加载速度 -- [ ] 快速切换供应商 -- [ ] 内存使用情况 -- [ ] CPU 使用情况 - -### 5.5 Bug 修复 - -**Bug 列表** (发现后记录): - -1. ___________ - - [ ] 已修复 - - [ ] 已验证 - -2. ___________ - - [ ] 已修复 - - [ ] 已验证 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 最终检查 - -### 代码质量 - -- [ ] 所有 TypeScript 错误已修复 -- [ ] 运行 `pnpm format` 格式化代码 -- [ ] 运行 `pnpm typecheck` 通过 -- [ ] 代码审查完成 - -### 文档更新 - -- [ ] 更新 `CLAUDE.md` 反映新架构 -- [ ] 更新 `README.md` (如有必要) -- [ ] 添加 Migration Guide (可选) - -### 性能基准 - -记录性能数据: - -**旧版本**: -- 启动时间: _____ms -- 供应商加载: _____ms -- 内存占用: _____MB - -**新版本**: -- 启动时间: _____ms -- 供应商加载: _____ms -- 内存占用: _____MB - -### 代码统计 - -**代码行数对比**: - -| 文件 | 旧版本 | 新版本 | 减少 | -|------|--------|--------|------| -| App.tsx | 412 | ~100 | -76% | -| tauri-api.ts | 712 | ~50 | -93% | -| ProviderForm.tsx | 271 | ~150 | -45% | -| settings 模块 | 1046 | ~470 (拆分) | -55% | -| **总计** | 2038 | ~700 | **-66%** | - ---- - -## 📦 发布准备 - -### Pre-release 测试 - -- [ ] 创建 beta 版本 `v4.0.0-beta.1` -- [ ] 在测试环境验证 -- [ ] 收集用户反馈 - -### 正式发布 - -- [ ] 合并到 main 分支 -- [ ] 创建 Release Tag `v4.0.0` -- [ ] 更新 Changelog -- [ ] 发布 GitHub Release -- [ ] 通知用户更新 - ---- - -## 🚨 回滚触发条件 - -如果出现以下情况,考虑回滚: - -- [ ] 重大功能无法使用 -- [ ] 用户数据丢失 -- [ ] 严重性能问题 -- [ ] 无法修复的兼容性问题 - -**回滚命令**: -```bash -git reset --hard backup-before-refactor -# 或 -git revert -``` - ---- - -## 📝 总结报告 - -### 成功指标 - -- [ ] 所有现有功能正常工作 -- [ ] 代码量减少 40%+ -- [ ] 无用户数据丢失 -- [ ] 性能未下降 - -### 经验教训 - -**遇到的主要挑战**: -1. ___________ -2. ___________ -3. ___________ - -**解决方案**: -1. ___________ -2. ___________ -3. ___________ - -**未来改进**: -1. ___________ -2. ___________ -3. ___________ - ---- - -**重构完成日期**: ___________ -**总耗时**: _____ 天 -**参与人员**: ___________ diff --git a/docs/REFACTORING_MASTER_PLAN.md b/docs/REFACTORING_MASTER_PLAN.md deleted file mode 100644 index f38e19d3c..000000000 --- a/docs/REFACTORING_MASTER_PLAN.md +++ /dev/null @@ -1,1658 +0,0 @@ -# CC Switch 现代化重构完整方案 - -> Breaking Change 提醒(后续示例如仍出现 `app_type/appType` 字样,请按本规范理解与替换): -> -> - 后端 Tauri 命令统一仅接受 `app` 参数(值:`claude` 或 `codex`),不再接受 `app_type`/`appType`。 -> - 传入未知 `app` 会返回本地化错误,并提示“可选值: claude, codex”。 -> - 前端与文档中的旧示例如包含 `app_type`,一律替换为 `{ app }`。 - -## 📋 目录 - -- [第一部分: 战略规划](#第一部分-战略规划) - - [重构背景与目标](#重构背景与目标) - - [当前问题全面分析](#当前问题全面分析) - - [技术选型与理由](#技术选型与理由) -- [第二部分: 架构设计](#第二部分-架构设计) - - [新的目录结构](#新的目录结构) - - [数据流架构](#数据流架构) - - [组件拆分详细方案](#组件拆分详细方案) -- [第三部分: 实施计划](#第三部分-实施计划) - - [分阶段实施路线图](#分阶段实施路线图) - - [详细实施步骤](#详细实施步骤) -- [第四部分: 质量保障](#第四部分-质量保障) - - [测试策略](#测试策略) - - [风险控制](#风险控制) - - [回滚方案](#回滚方案) - ---- - -# 第一部分: 战略规划 - -## 🎯 重构背景与目标 - -### 为什么要重构? - -当前代码库存在以下核心问题: - -1. **状态管理混乱** - - 手动管理 20+ `useState` - - 大量复杂的 `useEffect` 依赖链 - - 数据同步逻辑分散 - -2. **组件过于臃肿** - - `SettingsModal.tsx`: **1046 行** 😱 - - `ProviderList.tsx`: **418 行** - - `ProviderForm.tsx`: **271 行** - -3. **代码重复严重** - - 相似的数据获取逻辑在多个组件重复 - - 表单验证逻辑手动编写 - - 错误处理不统一 - -4. **UI 缺乏统一性** - - 自定义样式分散 - - 缺乏设计系统 - - 响应式支持不足 - -5. **可维护性差** - - 组件职责不清晰 - - 耦合度高 - - 难以测试 - -### 重构目标 - -| 维度 | 目标 | 衡量标准 | -| -------------- | -------------------- | -------------- | -| **代码质量** | 减少 40-60% 样板代码 | 代码行数统计 | -| **开发效率** | 提升 50%+ 开发速度 | 新功能开发时间 | -| **用户体验** | 统一设计系统 | UI 一致性检查 | -| **可维护性** | 清晰的架构分层 | 代码审查时间 | -| **功能完整性** | 100% 功能无回归 | 全量测试通过 | - ---- - -## 🔍 当前问题全面分析 - -### 问题 1: App.tsx - 状态管理混乱 (412行) - -**现状**: - -```typescript -// 10+ 个 useState,状态管理混乱 -const [providers, setProviders] = useState>({}) -const [currentProviderId, setCurrentProviderId] = useState("") -const [notification, setNotification] = useState<{...} | null>(null) -const [isNotificationVisible, setIsNotificationVisible] = useState(false) -const [confirmDialog, setConfirmDialog] = useState<{...} | null>(null) -const [isSettingsOpen, setIsSettingsOpen] = useState(false) -const [isMcpOpen, setIsMcpOpen] = useState(false) -// ... 更多 - -// 手动数据加载,缺少 loading/error 状态 -const loadProviders = async () => { - const loadedProviders = await window.api.getProviders(activeApp) - const currentId = await window.api.getCurrentProvider(activeApp) - setProviders(loadedProviders) - setCurrentProviderId(currentId) -} - -// 复杂的 useEffect 依赖 -useEffect(() => { - loadProviders() -}, [activeApp]) -``` - -**核心问题**: - -- ❌ 状态同步困难 -- ❌ 没有 loading/error 处理 -- ❌ 错误处理不统一 -- ❌ 组件责任过重 - -**目标**: - -```typescript -// React Query: 3 行搞定 -const { data, isLoading, error } = useProvidersQuery(activeApp); -const providers = data?.providers || {}; -const currentProviderId = data?.currentProviderId || ""; -``` - ---- - -### 问题 2: SettingsModal.tsx - 超级巨无霸组件 (1046行) - -**现状结构**: - -``` -SettingsModal.tsx (1046 行) -├── 20+ useState (settings, configPath, version, isChecking...) -├── 15+ 处理函数 -│ ├── loadSettings() -│ ├── saveSettings() -│ ├── handleLanguageChange() -│ ├── handleCheckUpdate() -│ ├── handleExportConfig() -│ ├── handleImportConfig() -│ ├── handleBrowseConfigDir() -│ └── ... 更多 -├── 语言设置 UI -├── 窗口行为设置 UI -├── 配置文件位置 UI -├── 配置目录覆盖 UI (3个输入框) -├── 导入导出 UI -├── 关于和更新 UI -└── 2个子对话框 (ImportProgress, RestartConfirm) -``` - -**核心问题**: - -- ❌ 单个文件超过 1000 行 -- ❌ 多种职责混杂 -- ❌ 难以理解和维护 -- ❌ 无法并行开发 -- ❌ 难以测试 - -**目标**: 拆分为 **7 个小组件** (~470 行总计) - ---- - -### 问题 3: ProviderList.tsx - 内嵌组件和逻辑混杂 (418行) - -**现状结构**: - -``` -ProviderList.tsx (418 行) -├── SortableProviderItem (内嵌子组件, ~100行) -├── 拖拽排序逻辑 -├── 用量配置逻辑 -├── URL 处理逻辑 -├── Claude 插件同步逻辑 -└── 空状态 UI -``` - -**核心问题**: - -- ❌ 内嵌组件导致代码难读 -- ❌ 拖拽逻辑和 UI 混在一起 -- ❌ 业务逻辑分散 - -**目标**: 拆分为 **4 个独立组件** + **1 个自定义 Hook** - ---- - -### 问题 4: tauri-api.ts - 全局污染 (712行) - -**现状**: - -```typescript -// 问题 1: 污染全局命名空间 -if (typeof window !== "undefined") { - (window as any).api = tauriAPI; -} - -// 问题 2: 无缓存机制 -getProviders: async (app?: AppId) => { - try { - return await invoke("get_providers", { app }); - } catch (error) { - console.error("获取供应商列表失败:", error); - return {}; // 错误被吞掉 - } -}; -``` - -**核心问题**: - -- ❌ 全局 `window.api` 污染命名空间 -- ❌ 无缓存,重复请求 -- ❌ 无自动重试 -- ❌ 错误处理不统一 - -**目标**: - -- 封装为 API 层 (`lib/api/`) -- React Query 管理缓存和状态 - ---- - -### 问题 5: 表单验证 - 手动编写 (ProviderForm.tsx) - -**现状**: - -```typescript -const [name, setName] = useState(""); -const [nameError, setNameError] = useState(""); -const [apiKey, setApiKey] = useState(""); -const [apiKeyError, setApiKeyError] = useState(""); - -const validate = () => { - let valid = true; - if (!name) { - setNameError("请填写名称"); - valid = false; - } else { - setNameError(""); - } - if (!apiKey) { - setApiKeyError("请填写 API Key"); - valid = false; - } else if (apiKey.length < 10) { - setApiKeyError("API Key 长度不足"); - valid = false; - } else { - setApiKeyError(""); - } - return valid; -}; -``` - -**核心问题**: - -- ❌ 每个字段需要 2 个 state (值 + 错误) -- ❌ 验证逻辑手动编写 -- ❌ 代码冗长 - -**目标**: 使用 `react-hook-form` + `zod` - -```typescript -const schema = z.object({ - name: z.string().min(1, "请填写名称"), - apiKey: z.string().min(10, "API Key 长度不足"), -}); - -const form = useForm({ resolver: zodResolver(schema) }); -``` - ---- - -## 🛠 技术选型与理由 - -### 核心技术栈 - -| 技术 | 版本 | 用途 | 替代方案 | 为何选它? | -| ------------------------- | ------- | -------------- | --------------- | -------------------- | -| **@tanstack/react-query** | ^5.90.2 | 服务端状态管理 | SWR, RTK Query | 功能最全,生态最好 | -| **react-hook-form** | ^7.63.0 | 表单管理 | Formik | 性能更好,API 更简洁 | -| **zod** | ^4.1.11 | 运行时类型验证 | yup, joi | TypeScript 原生支持 | -| **shadcn/ui** | latest | UI 组件库 | Radix UI 原生 | 可定制,代码归属权 | -| **sonner** | ^2.0.7 | Toast 通知 | react-hot-toast | 更现代,动画更好 | -| **next-themes** | ^0.4.6 | 主题管理 | 自定义实现 | 开箱即用,SSR 友好 | - ---- - -# 第二部分: 架构设计 - -## 📁 新的目录结构 - -### 完整目录树 - -``` -src/ -├── components/ -│ ├── ui/ # shadcn/ui 基础组件 (由 CLI 生成) -│ │ ├── button.tsx -│ │ ├── dialog.tsx -│ │ ├── input.tsx -│ │ ├── label.tsx -│ │ ├── form.tsx -│ │ ├── select.tsx -│ │ ├── switch.tsx -│ │ ├── tabs.tsx -│ │ ├── card.tsx -│ │ ├── badge.tsx -│ │ └── sonner.tsx # Toast 组件 -│ │ -│ ├── providers/ # 供应商管理模块 -│ │ ├── ProviderList.tsx # 列表容器 (~100行) -│ │ ├── ProviderCard.tsx # 供应商卡片 (~120行) -│ │ ├── ProviderActions.tsx # 操作按钮组 (~80行) -│ │ ├── ProviderEmptyState.tsx # 空状态 (~30行) -│ │ ├── AddProviderDialog.tsx # 添加对话框 (~60行) -│ │ ├── EditProviderDialog.tsx # 编辑对话框 (~60行) -│ │ └── forms/ # 表单子模块 -│ │ ├── ProviderForm.tsx # 主表单 (~150行) -│ │ ├── PresetSelector.tsx # 预设选择器 (~60行) -│ │ ├── ApiKeyInput.tsx # API Key 输入 (~40行) -│ │ ├── ConfigEditor.tsx # 配置编辑器 (~80行) -│ │ └── KimiModelSelector.tsx # Kimi 模型选择器 (~40行) -│ │ -│ ├── settings/ # 设置管理模块 (拆分自 SettingsModal) -│ │ ├── SettingsDialog.tsx # 设置对话框容器 (~80行) -│ │ ├── LanguageSettings.tsx # 语言设置 (~40行) -│ │ ├── WindowSettings.tsx # 窗口行为设置 (~50行) -│ │ ├── ConfigPathDisplay.tsx # 配置路径显示 (~40行) -│ │ ├── DirectorySettings/ # 目录设置子模块 -│ │ │ ├── index.tsx # 目录设置容器 (~60行) -│ │ │ └── DirectoryInput.tsx # 单个目录输入组件 (~50行) -│ │ ├── ImportExportSection.tsx # 导入导出 (~120行) -│ │ ├── AboutSection.tsx # 关于和更新 (~100行) -│ │ └── RestartDialog.tsx # 重启确认对话框 (~40行) -│ │ -│ ├── usage/ # 用量查询模块 -│ │ ├── UsageFooter.tsx # 用量信息展示 -│ │ ├── UsageScriptModal.tsx # 用量脚本配置 -│ │ └── UsageEditor.tsx # 脚本编辑器 -│ │ -│ ├── mcp/ # MCP 管理模块 -│ │ ├── McpPanel.tsx # MCP 管理面板 -│ │ ├── McpList.tsx # MCP 列表 -│ │ ├── McpForm.tsx # MCP 表单 -│ │ └── McpTemplates.tsx # MCP 模板选择 -│ │ -│ ├── shared/ # 共享组件 -│ │ ├── AppSwitcher.tsx # Claude/Codex 切换器 -│ │ ├── ConfirmDialog.tsx # 确认对话框 -│ │ ├── UpdateBadge.tsx # 更新徽章 -│ │ ├── JsonEditor.tsx # JSON 编辑器 -│ │ ├── BrandIcons.tsx # 品牌图标 -│ │ └── ImportProgressModal.tsx # 导入进度 -│ │ -│ ├── theme-provider.tsx # 主题 Provider -│ └── mode-toggle.tsx # 主题切换按钮 -│ -├── hooks/ # 自定义 Hooks (业务逻辑层) -│ ├── useSettings.ts # 设置管理逻辑 -│ ├── useImportExport.ts # 导入导出逻辑 -│ ├── useDragSort.ts # 拖拽排序逻辑 -│ ├── useProviderActions.ts # 供应商操作 (可选) -│ ├── useVSCodeSync.ts # VS Code 同步 -│ ├── useClaudePlugin.ts # Claude 插件管理 -│ └── useAppVersion.ts # 版本信息 -│ -├── lib/ -│ ├── query/ # React Query 层 -│ │ ├── index.ts # 导出所有 hooks -│ │ ├── queryClient.ts # QueryClient 配置 -│ │ ├── queries.ts # 所有查询 hooks -│ │ └── mutations.ts # 所有变更 hooks -│ │ -│ ├── api/ # API 调用层 (封装 Tauri invoke) -│ │ ├── providers.ts # 供应商 API -│ │ ├── settings.ts # 设置 API -│ │ ├── mcp.ts # MCP API -│ │ ├── usage.ts # 用量查询 API -│ │ ├── vscode.ts # VS Code API -│ │ └── index.ts # 聚合导出 -│ │ -│ ├── schemas/ # Zod 验证 Schemas -│ │ ├── provider.ts # 供应商验证规则 -│ │ ├── settings.ts # 设置验证规则 -│ │ └── mcp.ts # MCP 验证规则 -│ │ -│ ├── utils/ # 工具函数 -│ │ ├── errorHandling.ts # 错误处理 -│ │ ├── providerUtils.ts # 供应商工具 -│ │ └── configUtils.ts # 配置工具 -│ │ -│ └── utils.ts # shadcn/ui 工具函数 (cn) -│ -├── types/ # TypeScript 类型定义 -│ └── index.ts -│ -├── contexts/ # React Contexts (保留现有) -│ └── UpdateContext.tsx # 更新管理 Context -│ -├── i18n/ # 国际化 (保留现有) -│ ├── index.ts -│ └── locales/ -│ -├── App.tsx # 主应用组件 (简化到 ~100行) -├── main.tsx # 入口文件 (添加 Providers) -└── index.css # 全局样式 -``` - -### 目录结构设计原则 - -1. **按功能模块分组** (providers/, settings/, mcp/) -2. **按技术层次分层** (components/, hooks/, lib/) -3. **UI 组件独立** (ui/ 目录) -4. **业务逻辑提取** (hooks/ 目录) -5. **数据层封装** (api/ 目录) - ---- - -## 🏗 数据流架构 - -### 分层架构图 - -``` -┌─────────────────────────────────────────┐ -│ UI 层 (Components) │ -│ ProviderList, SettingsDialog, etc. │ -└────────────────┬────────────────────────┘ - │ 使用 - ↓ -┌─────────────────────────────────────────┐ -│ 业务逻辑层 (Custom Hooks) │ -│ useSettings, useDragSort, etc. │ -└────────────────┬────────────────────────┘ - │ 调用 - ↓ -┌─────────────────────────────────────────┐ -│ 数据管理层 (React Query Hooks) │ -│ useProvidersQuery, useMutation, etc. │ -└────────────────┬────────────────────────┘ - │ 调用 - ↓ -┌─────────────────────────────────────────┐ -│ API 层 (API Functions) │ -│ providersApi, settingsApi, etc. │ -└────────────────┬────────────────────────┘ - │ invoke - ↓ -┌─────────────────────────────────────────┐ -│ Tauri Backend (Rust) │ -│ Commands, State, File System │ -└─────────────────────────────────────────┘ -``` - -### 数据流示例 - -**场景**: 切换供应商 - -``` -1. 用户点击按钮 - ↓ -2. ProviderCard 调用 onClick={() => switchMutation.mutate(id)} - ↓ -3. useSwitchProviderMutation (lib/query/mutations.ts) - - mutationFn: 调用 providersApi.switch(id, appType) - ↓ -4. providersApi.switch (lib/api/providers.ts) - - 调用 invoke('switch_provider', { id, app }) - ↓ -5. Tauri Backend (Rust) - - 执行切换逻辑 - - 更新配置文件 - - 返回结果 - ↓ -6. useSwitchProviderMutation - - onSuccess: invalidateQueries(['providers', appType]) - - onSuccess: updateTrayMenu() - - onSuccess: toast.success('切换成功') - ↓ -7. useProvidersQuery 自动重新获取数据 - ↓ -8. UI 自动更新 -``` - -### 关键设计原则 - -1. **单一职责**: 每层只做一件事 -2. **依赖倒置**: UI 依赖抽象 (hooks),不依赖具体实现 -3. **开闭原则**: 易于扩展,无需修改现有代码 -4. **状态分离**: - - 服务端状态 → React Query - - 客户端 UI 状态 → useState - - 全局状态 → Context - ---- - -## 🔧 组件拆分详细方案 - -### 拆分策略: SettingsModal (1046行 → 7个组件) - -#### 拆分前后对比 - -``` -┌───────────────────────────────────┐ -│ SettingsModal.tsx (1046 行) │ ❌ 过于臃肿 -│ │ -│ - 20+ useState │ -│ - 15+ 函数 │ -│ - 600+ 行 JSX │ -│ - 难以理解和维护 │ -└───────────────────────────────────┘ - - ↓ 重构 - -┌─────────────────────────────────────────────────┐ -│ settings/ 模块 (7个组件, ~470行) │ -│ │ -│ ├── SettingsDialog.tsx (容器, ~80行) │ -│ │ └── 使用 useSettings hook │ -│ │ │ -│ ├── LanguageSettings.tsx (~40行) │ -│ ├── WindowSettings.tsx (~50行) │ -│ ├── ConfigPathDisplay.tsx (~40行) │ -│ ├── DirectorySettings/ (~110行) │ -│ │ ├── index.tsx (~60行) │ -│ │ └── DirectoryInput.tsx (~50行) │ -│ ├── ImportExportSection.tsx (~120行) │ -│ │ └── 使用 useImportExport hook │ -│ └── AboutSection.tsx (~100行) │ -│ └── 使用 useAppVersion, useUpdate hooks │ -└─────────────────────────────────────────────────┘ - -✅ 每个组件 30-120 行 -✅ 职责清晰 -✅ 易于测试 -✅ 可独立开发 -``` - -#### 拆分详细方案 - -**1. SettingsDialog.tsx (容器组件, ~80行)** - -职责: 组织整体布局,协调子组件 - -```typescript -import { LanguageSettings } from './LanguageSettings' -import { WindowSettings } from './WindowSettings' -import { DirectorySettings } from './DirectorySettings' -import { ImportExportSection } from './ImportExportSection' -import { AboutSection } from './AboutSection' -import { useSettings } from '@/hooks/useSettings' - -export function SettingsDialog({ open, onOpenChange }) { - const { settings, updateSettings, saveSettings, isPending } = useSettings() - - return ( - - - - 设置 - - - - - 通用 - 高级 - 关于 - - - - updateSettings({ language: lang })} - /> - - - - - - - - - - - - - - - - - - - - - ) -} -``` - -**2. LanguageSettings.tsx (~40行)** - -职责: 语言切换 UI - -```typescript -interface LanguageSettingsProps { - value: 'zh' | 'en' - onChange: (lang: 'zh' | 'en') => void -} - -export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { - return ( -
-

语言设置

-
- - -
-
- ) -} -``` - -**3. DirectoryInput.tsx (~50行)** - -职责: 可复用的目录选择输入框 - -```typescript -import { FolderSearch, Undo2 } from 'lucide-react' - -interface DirectoryInputProps { - label: string - description?: string - value?: string - onChange: (value: string | undefined) => void - type: 'app' | 'claude' | 'codex' -} - -export function DirectoryInput({ label, description, value, onChange }: DirectoryInputProps) { - const handleBrowse = async () => { - const selected = await window.api.selectConfigDirectory(value) - if (selected) onChange(selected) - } - - const handleReset = () => { - onChange(undefined) - } - - return ( -
- - {description &&

{description}

} -
- onChange(e.target.value)} - className="flex-1 font-mono text-xs" - /> - - -
-
- ) -} -``` - -**4. useSettings Hook (业务逻辑提取)** - -```typescript -export function useSettings() { - const queryClient = useQueryClient(); - - // 获取设置 - const { data: settings, isLoading } = useQuery({ - queryKey: ["settings"], - queryFn: async () => await settingsApi.get(), - }); - - // 保存设置 - const saveMutation = useMutation({ - mutationFn: async (newSettings: Settings) => - await settingsApi.save(newSettings), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["settings"] }); - toast.success("设置已保存"); - }, - }); - - // 本地临时状态 (保存前) - const [localSettings, setLocalSettings] = useState(null); - const currentSettings = localSettings || settings || {}; - - return { - settings: currentSettings, - updateSettings: (updates: Partial) => { - setLocalSettings((prev) => ({ ...prev, ...updates })); - }, - saveSettings: () => { - if (localSettings) saveMutation.mutate(localSettings); - }, - resetSettings: () => setLocalSettings(null), - isPending: saveMutation.isPending, - isLoading, - }; -} -``` - ---- - -### 拆分策略: ProviderList (418行 → 4个组件 + 1个Hook) - -#### 拆分方案 - -``` -ProviderList.tsx (418 行) ❌ 内嵌组件、逻辑混杂 - - ↓ 重构 - -providers/ 模块 (4个组件 + 1个Hook, ~330行) - -├── ProviderList.tsx (容器, ~100行) -│ └── 使用 useDragSort hook -│ -├── ProviderCard.tsx (~120行) -│ └── 显示单个供应商信息 -│ -├── ProviderActions.tsx (~80行) -│ └── 操作按钮组 (switch, edit, delete, usage) -│ -├── ProviderEmptyState.tsx (~30行) -│ └── 空状态提示 -│ -└── hooks/useDragSort.ts (~100行) - └── 拖拽排序逻辑 -``` - -#### 代码示例 - -**ProviderList.tsx (容器)** - -```typescript -import { ProviderCard } from './ProviderCard' -import { ProviderEmptyState } from './ProviderEmptyState' -import { useDragSort } from '@/hooks/useDragSort' - -export function ProviderList({ providers, currentProviderId, appType }) { - const { sortedProviders, handleDragEnd, sensors } = useDragSort(providers, appType) - - if (sortedProviders.length === 0) { - return - } - - return ( - - p.id)} - strategy={verticalListSortingStrategy} - > -
- {sortedProviders.map(provider => ( - - ))} -
-
-
- ) -} -``` - -**useDragSort.ts (逻辑提取)** - -```typescript -export function useDragSort( - providers: Record, - appType: AppId -) { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - // 排序逻辑 - const sortedProviders = useMemo(() => { - return Object.values(providers).sort((a, b) => { - if (a.sortIndex !== undefined && b.sortIndex !== undefined) { - return a.sortIndex - b.sortIndex; - } - const timeA = a.createdAt || 0; - const timeB = b.createdAt || 0; - if (timeA === 0 && timeB === 0) { - return a.name.localeCompare(b.name, "zh-CN"); - } - return timeA === 0 ? -1 : timeB === 0 ? 1 : timeA - timeB; - }); - }, [providers]); - - // 拖拽传感器 - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), - useSensor(KeyboardSensor) - ); - - // 拖拽结束处理 - const handleDragEnd = useCallback( - async (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const oldIndex = sortedProviders.findIndex((p) => p.id === active.id); - const newIndex = sortedProviders.findIndex((p) => p.id === over.id); - - const reordered = arrayMove(sortedProviders, oldIndex, newIndex); - const updates = reordered.map((p, i) => ({ id: p.id, sortIndex: i })); - - try { - await providersApi.updateSortOrder(updates, appType); - queryClient.invalidateQueries({ queryKey: ["providers", appType] }); - toast.success(t("provider.sortUpdated")); - } catch (error) { - toast.error(t("provider.sortUpdateFailed")); - } - }, - [sortedProviders, appType, queryClient, t] - ); - - return { sortedProviders, sensors, handleDragEnd }; -} -``` - ---- - -### 代码量对比总结 - -| 组件 | 重构前 | 重构后 | 变化 | -| -------------------- | ---------- | ---------------- | -------- | -| **SettingsModal** | 1046 行 | 7个组件 ~470行 | **-55%** | -| **ProviderList** | 418 行 | 4个组件 ~330行 | **-21%** | -| **业务逻辑 (Hooks)** | 混在组件中 | 5个 hooks ~400行 | 提取独立 | -| **总计** | 1464 行 | ~1200 行 | **-18%** | - -**注意**: 代码总量略有减少,但**可维护性大幅提升**: - -- ✅ 每个文件 30-120 行,易于理解 -- ✅ 关注点分离,职责清晰 -- ✅ 业务逻辑可复用 -- ✅ 易于测试和调试 - ---- - -# 第三部分: 实施计划 - -## 📅 分阶段实施路线图 - -### 总览 - -| 阶段 | 目标 | 工期 | 产出 | -| ---------- | -------------- | ------------ | ---------------------------- | -| **阶段 0** | 准备环境 | 1 天 | 依赖安装、配置完成 | -| **阶段 1** | 搭建基础设施(✅ 已完成) | 2-3 天 | API 层、Query Hooks 完成 | -| **阶段 2** | 重构核心功能(✅ 已完成) | 3-4 天 | App.tsx、ProviderList 完成 | -| **阶段 3** | 重构设置和辅助(✅ 已完成) | 2-3 天 | SettingsDialog、通知系统完成 | -| **阶段 4** | 清理和优化 | 1-2 天 | 旧代码删除、优化完成 | -| **阶段 5** | 测试和修复 | 2-3 天 | 测试通过、Bug 修复 | -| **总计** | - | **11-16 天** | v4.0.0 发布 | - ---- - -### 阶段 0: 准备阶段 (1天) - -**目标**: 环境准备和依赖安装 - -#### 任务清单 - -- [ ] 创建新分支 `refactor/modernization` -- [ ] 创建备份标签 `git tag backup-before-refactor` -- [ ] 安装核心依赖 -- [ ] 配置 shadcn/ui -- [ ] 配置 TypeScript 路径别名 -- [ ] 配置 Vite 路径解析 -- [ ] 验证开发服务器启动 - -#### 详细步骤 - -**1. 创建分支和备份** - -```bash -# 创建新分支 -git checkout -b refactor/modernization - -# 创建备份标签 -git tag backup-before-refactor - -# 推送标签到远程 (可选) -git push origin backup-before-refactor -``` - -**2. 安装依赖** - -```bash -# 核心依赖 -pnpm add @tanstack/react-query -pnpm add react-hook-form @hookform/resolvers -pnpm add zod -pnpm add sonner -pnpm add next-themes - -# Radix UI 组件 (shadcn/ui 依赖) -pnpm add @radix-ui/react-dialog -pnpm add @radix-ui/react-dropdown-menu -pnpm add @radix-ui/react-label -pnpm add @radix-ui/react-select -pnpm add @radix-ui/react-slot -pnpm add @radix-ui/react-switch -pnpm add @radix-ui/react-tabs -pnpm add @radix-ui/react-checkbox - -# 样式工具 -pnpm add class-variance-authority -pnpm add clsx -pnpm add tailwind-merge -``` - -**3. 创建 `components.json`** - -```json -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} -``` - -**4. 更新 `tsconfig.json`** - -```json -{ - "compilerOptions": { - // ... 现有配置 - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } -} -``` - -**5. 更新 `vite.config.mts`** - -```typescript -import path from "path"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); -``` - -**6. 验证** - -```bash -pnpm dev # 确保开发服务器正常启动 -pnpm typecheck # 确保类型检查通过 -``` - ---- - -### 阶段 1: 基础设施 (2-3天) - -**目标**: 搭建新架构的基础层 - -#### 任务清单 - -- [x] 创建工具函数 (`lib/utils.ts`) -- [x] 添加基础 UI 组件 (Button, Dialog, Input, Form 等) -- [x] 创建 Query Client 配置 -- [x] 封装 API 层 (providers, settings, mcp) -- [x] 创建 Query Hooks (queries, mutations) -- [x] 创建 Zod Schemas - -#### 详细步骤 - -**Step 1.1: 创建 `src/lib/utils.ts`** - -```typescript -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} -``` - -**Step 1.2: 添加 shadcn/ui 基础组件** - -创建 `src/components/ui/button.tsx`: - -```typescript -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } -``` - -类似地创建: - -- `dialog.tsx` -- `input.tsx` -- `label.tsx` -- `form.tsx` -- `select.tsx` -- `switch.tsx` -- `tabs.tsx` -- `textarea.tsx` -- `sonner.tsx` - -**参考**: https://ui.shadcn.com/docs/components - -**Step 1.3: 创建 Query Client** - -`src/lib/query/queryClient.ts`: - -```typescript -import { QueryClient } from "@tanstack/react-query"; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 1, - refetchOnWindowFocus: false, - staleTime: 1000 * 60 * 5, // 5 分钟 - }, - mutations: { - retry: false, - }, - }, -}); -``` - -**Step 1.4: 封装 API 层** - -`src/lib/api/providers.ts`: - -```typescript -import { invoke } from "@tauri-apps/api/core"; -import { Provider } from "@/types"; -import type { AppId } from "@/lib/api"; - -export const providersApi = { - getAll: async (appId: AppId): Promise> => { - return await invoke("get_providers", { app: appId }); - }, - - getCurrent: async (appId: AppId): Promise => { - return await invoke("get_current_provider", { app: appId }); - }, - - add: async (provider: Provider, appId: AppId): Promise => { - return await invoke("add_provider", { provider, app: appId }); - }, - - update: async (provider: Provider, appId: AppId): Promise => { - return await invoke("update_provider", { provider, app: appId }); - }, - - delete: async (id: string, appId: AppId): Promise => { - return await invoke("delete_provider", { id, app: appId }); - }, - - switch: async (id: string, appId: AppId): Promise => { - return await invoke("switch_provider", { id, app: appId }); - }, - - importDefault: async (appId: AppId): Promise => { - return await invoke("import_default_config", { app: appId }); - }, - - updateTrayMenu: async (): Promise => { - return await invoke("update_tray_menu"); - }, - - updateSortOrder: async ( - updates: Array<{ id: string; sortIndex: number }>, - appId: AppId - ): Promise => { - return await invoke("update_providers_sort_order", { updates, app: appId }); - }, -}; -``` - -类似地创建: - -- `src/lib/api/settings.ts` -- `src/lib/api/mcp.ts` -- `src/lib/api/index.ts` (聚合导出) - -**Step 1.5: 创建 Query Hooks** - -`src/lib/query/queries.ts`: - -```typescript -import { useQuery } from "@tanstack/react-query"; -import { providersApi, type AppId } from "@/lib/api"; -import { Provider } from "@/types"; - -// 排序辅助函数 -const sortProviders = ( - providers: Record -): Record => { - return Object.fromEntries( - Object.values(providers) - .sort((a, b) => { - const timeA = a.createdAt || 0; - const timeB = b.createdAt || 0; - if (timeA === 0 && timeB === 0) { - return a.name.localeCompare(b.name, "zh-CN"); - } - if (timeA === 0) return -1; - if (timeB === 0) return 1; - return timeA - timeB; - }) - .map((provider) => [provider.id, provider]) - ); -}; - -export const useProvidersQuery = (appType: AppId) => { - return useQuery({ - queryKey: ["providers", appType], - queryFn: async () => { - let providers: Record = {}; - let currentProviderId = ""; - - try { - providers = await providersApi.getAll(appType); - } catch (error) { - console.error("获取供应商列表失败:", error); - } - - try { - currentProviderId = await providersApi.getCurrent(appType); - } catch (error) { - console.error("获取当前供应商失败:", error); - } - - // 自动导入默认配置 - if (Object.keys(providers).length === 0) { - try { - const success = await providersApi.importDefault(appType); - if (success) { - providers = await providersApi.getAll(appType); - currentProviderId = await providersApi.getCurrent(appType); - } - } catch (error) { - console.error("导入默认配置失败:", error); - } - } - - return { providers: sortProviders(providers), currentProviderId }; - }, - }); -}; -``` - -`src/lib/query/mutations.ts`: - -```typescript -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { providersApi, type AppId } from "@/lib/api"; -import { Provider } from "@/types"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; - -export const useAddProviderMutation = (appType: AppId) => { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - return useMutation({ - mutationFn: async (provider: Omit) => { - const newProvider: Provider = { - ...provider, - id: crypto.randomUUID(), - createdAt: Date.now(), - }; - await providersApi.add(newProvider, appType); - return newProvider; - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); - await providersApi.updateTrayMenu(); - toast.success(t("notifications.providerAdded")); - }, - onError: (error: Error) => { - toast.error(t("notifications.addFailed", { error: error.message })); - }, - }); -}; - -export const useSwitchProviderMutation = (appType: AppId) => { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - return useMutation({ - mutationFn: async (providerId: string) => { - return await providersApi.switch(providerId, appType); - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); - await providersApi.updateTrayMenu(); - toast.success( - t("notifications.switchSuccess", { appName: t(`apps.${appType}`) }) - ); - }, - onError: (error: Error) => { - toast.error(t("notifications.switchFailed") + ": " + error.message); - }, - }); -}; - -// 类似地创建: useDeleteProviderMutation, useUpdateProviderMutation -``` - -**Step 1.6: 创建 Zod Schemas** - -`src/lib/schemas/provider.ts`: - -```typescript -import { z } from "zod"; - -export const providerSchema = z.object({ - name: z.string().min(1, "请填写供应商名称"), - websiteUrl: z.string().url("请输入有效的网址").optional().or(z.literal("")), - settingsConfig: z - .string() - .min(1, "请填写配置内容") - .refine( - (val) => { - try { - JSON.parse(val); - return true; - } catch { - return false; - } - }, - { message: "配置 JSON 格式错误" } - ), -}); - -export type ProviderFormData = z.infer; -``` - ---- - -### 阶段 2: 核心功能重构 (3-4天) - -**目标**: 重构 App.tsx 和供应商管理 - -#### 任务清单 - -- [x] 更新 `main.tsx` (添加 Providers) -- [x] 创建主题 Provider -- [x] 重构 `App.tsx` (412行 → ~100行) -- [x] 拆分 ProviderList (4个组件) -- [x] 创建 `useDragSort` Hook -- [x] 重构表单组件 (使用 react-hook-form) -- [x] 创建 AddProvider / EditProvider Dialog - -#### 详细步骤 - -**Step 2.1: 更新 `main.tsx`** - -```typescript -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import { UpdateProvider } from './contexts/UpdateContext' -import './index.css' -import './i18n' -import { QueryClientProvider } from '@tanstack/react-query' -import { queryClient } from '@/lib/query' -import { ThemeProvider } from '@/components/theme-provider' -import { Toaster } from '@/components/ui/sonner' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - - - - -) -``` - -**Step 2.2: 创建 `theme-provider.tsx`** - -```typescript -import { createContext, useContext, useEffect, useState } from 'react' - -type Theme = 'dark' | 'light' | 'system' - -type ThemeProviderProps = { - children: React.ReactNode - defaultTheme?: Theme - storageKey?: string -} - -type ThemeProviderState = { - theme: Theme - setTheme: (theme: Theme) => void -} - -const ThemeProviderContext = createContext({ - theme: 'system', - setTheme: () => null, -}) - -export function ThemeProvider({ - children, - defaultTheme = 'system', - storageKey = 'ui-theme', - ...props -}: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) - - useEffect(() => { - const root = window.document.documentElement - root.classList.remove('light', 'dark') - - if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - root.classList.add(systemTheme) - return - } - - root.classList.add(theme) - }, [theme]) - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) - }, - } - - return ( - - {children} - - ) -} - -export const useTheme = () => { - const context = useContext(ThemeProviderContext) - if (context === undefined) { - throw new Error('useTheme must be used within a ThemeProvider') - } - return context -} -``` - -**Step 2.3: 重构 `App.tsx`** - -(参考前面的代码示例,从 412 行简化到 ~100 行) - -**Step 2.4-2.7: 拆分 ProviderList** - -(参考前面的组件拆分详细方案) - ---- - -### 阶段 3: 设置和辅助功能 (2-3天) - -**目标**: 重构设置模块和通知系统 - -#### 任务清单 - -- [x] 拆分 SettingsDialog (7个组件) -- [x] 创建 `useSettings` Hook -- [x] 创建 `useImportExport` Hook -- [x] 替换通知系统为 Sonner -- [x] 重构 ConfirmDialog - -#### 详细步骤 - -(参考前面的组件拆分详细方案) - ---- - -### 阶段 4: 清理和优化 (1-2天) - -**目标**: 清理旧代码,优化性能 - -#### 任务清单 - -- [x] 删除 `lib/styles.ts` -- [x] 删除旧的 Modal 组件 -- [x] 移除 `window.api` 全局绑定 -- [x] 清理无用的 state 和函数 -- [x] 更新类型定义 -- [x] 代码格式化 -- [x] TypeScript 检查 - ---- - -### 阶段 5: 测试和修复 (2-3天) - -**目标**: 全面测试,修复 Bug - -#### 功能测试清单 - -- [ ] 添加供应商 (Claude/Codex) -- [ ] 编辑供应商 -- [ ] 删除供应商 -- [ ] 切换供应商 -- [ ] 拖拽排序 -- [ ] 设置保存 -- [ ] 导入导出配置 -- [ ] 主题切换 -- [ ] MCP 管理 -- [ ] 用量查询 -- [ ] 托盘菜单同步 - -#### 边界情况测试 - -- [ ] 空供应商列表 -- [ ] 网络错误 -- [ ] 表单验证 -- [ ] 并发操作 -- [ ] 大量数据 (100+ 供应商) - ---- - -# 第四部分: 质量保障 - -## 🧪 测试策略 - -### 手动测试 - -每完成一个阶段后进行全量功能测试。 - -### 自动化测试 (可选) - -可以考虑添加: - -- Vitest 单元测试 (hooks, utils) -- Testing Library 组件测试 - ---- - -## 🚨 风险控制 - -### 潜在风险 - -1. **功能回归**: 重构可能引入 bug -2. **用户数据丢失**: 配置文件操作失败 -3. **性能下降**: 新架构可能影响性能 -4. **兼容性问题**: 依赖库平台兼容性 - -### 缓解措施 - -1. **逐步重构**: 按阶段进行,每阶段后测试 -2. **保留备份**: Git tag + 配置文件备份 -3. **Beta 测试**: 先发布 beta 版本 -4. **回滚方案**: 准备快速回滚机制 - ---- - -## ⏪ 回滚方案 - -### 如果需要回滚 - -```bash -# 方案 1: 回到重构前 -git reset --hard backup-before-refactor - -# 方案 2: 创建回滚分支 -git checkout -b rollback-refactor -git revert -``` - -### 用户数据保护 - -在重构前自动备份配置: - -```rust -// Rust 后端 -fn backup_config_before_refactor() -> Result<()> { - let config_path = get_app_config_path()?; - let backup_path = config_path.with_extension("backup.json"); - fs::copy(config_path, backup_path)?; - Ok(()) -} -``` - ---- - -## 🎯 成功标准 - -### 必须达成 (Must Have) - -- ✅ 所有现有功能正常工作 -- ✅ 无用户数据丢失 -- ✅ 性能不下降 -- ✅ TypeScript 检查通过 - -### 期望达成 (Should Have) - -- ✅ 代码量减少 40%+ -- ✅ 用户反馈积极 -- ✅ 开发体验提升明显 - -### 可选达成 (Nice to Have) - -- ⭕ 添加自动化测试 -- ⭕ 性能优化 20%+ - ---- - -## 📊 预期成果 - -### 代码质量 - -- **代码行数**: 减少 40-60% -- **文件数量**: UI 组件增加,但单文件更小 -- **可维护性**: 大幅提升 - -### 开发效率 - -- **新功能开发**: 提升 50%+ -- **Bug 修复**: 提升 30%+ -- **代码审查**: 提升 40%+ - -### 用户体验 - -- **界面一致性**: 统一的设计语言 -- **响应速度**: 更好的加载反馈 -- **错误提示**: 更友好的错误信息 - ---- - -## 📚 参考资料 - -- [TanStack Query 文档](https://tanstack.com/query/latest) -- [react-hook-form 文档](https://react-hook-form.com/) -- [shadcn/ui 文档](https://ui.shadcn.com/) -- [Zod 文档](https://zod.dev/) -- [原始 PR #76](https://github.com/farion1231/cc-switch/pull/76) - ---- - -## 📝 注意事项 - -1. **分支管理**: 在新分支进行,不要直接在 main 上修改 -2. **提交粒度**: 每完成一小步就提交,便于回滚 -3. **文档更新**: 同步更新 CLAUDE.md -4. **依赖锁定**: 锁定依赖版本 -5. **沟通协作**: 定期同步进度 - ---- - -**祝重构顺利! 🚀** diff --git a/docs/REFACTORING_REFERENCE.md b/docs/REFACTORING_REFERENCE.md deleted file mode 100644 index 9102d0f0b..000000000 --- a/docs/REFACTORING_REFERENCE.md +++ /dev/null @@ -1,834 +0,0 @@ -# 重构快速参考指南 - -> 常见模式和代码示例的速查表 - ---- - -## 📑 目录 - -1. [React Query 使用](#react-query-使用) -2. [react-hook-form 使用](#react-hook-form-使用) -3. [shadcn/ui 组件使用](#shadcnui-组件使用) -4. [代码迁移示例](#代码迁移示例) - ---- - -## React Query 使用 - -### 基础查询 - -```typescript -// 定义查询 Hook -export const useProvidersQuery = (appId: AppId) => { - return useQuery({ - queryKey: ['providers', appId], - queryFn: async () => { - const data = await providersApi.getAll(appId) - return data - }, - }) -} - -// 在组件中使用 -function MyComponent() { - const { data, isLoading, error } = useProvidersQuery('claude') - - if (isLoading) return
Loading...
- if (error) return
Error: {error.message}
- - return
{/* 使用 data */}
-} -``` - -### Mutation (变更操作) - -```typescript -// 定义 Mutation Hook -export const useAddProviderMutation = (appId: AppId) => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (provider: Provider) => { - return await providersApi.add(provider, appId) - }, - onSuccess: () => { - // 重新获取数据 - queryClient.invalidateQueries({ queryKey: ['providers', appId] }) - toast.success('添加成功') - }, - onError: (error: Error) => { - toast.error(`添加失败: ${error.message}`) - }, - }) -} - -// 在组件中使用 -function AddProviderDialog() { -const mutation = useAddProviderMutation('claude') - - const handleSubmit = (data: Provider) => { - mutation.mutate(data) - } - - return ( - - ) -} -``` - -### 乐观更新 - -```typescript -export const useSwitchProviderMutation = (appId: AppId) => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (providerId: string) => { - return await providersApi.switch(providerId, appId) - }, - // 乐观更新: 在请求发送前立即更新 UI - onMutate: async (providerId) => { - // 取消正在进行的查询 - await queryClient.cancelQueries({ queryKey: ['providers', appId] }) - - // 保存当前数据(以便回滚) - const previousData = queryClient.getQueryData(['providers', appId]) - - // 乐观更新 - queryClient.setQueryData(['providers', appId], (old: any) => ({ - ...old, - currentProviderId: providerId, - })) - - return { previousData } - }, - // 如果失败,回滚 - onError: (err, providerId, context) => { - queryClient.setQueryData(['providers', appId], context?.previousData) - toast.error('切换失败') - }, - // 无论成功失败,都重新获取数据 - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['providers', appId] }) - }, - }) -} -``` - -### 依赖查询 - -```typescript -// 第二个查询依赖第一个查询的结果 -const { data: providers } = useProvidersQuery(appId) -const currentProviderId = providers?.currentProviderId - -const { data: currentProvider } = useQuery({ - queryKey: ['provider', currentProviderId], - queryFn: () => providersApi.getById(currentProviderId!), - enabled: !!currentProviderId, // 只有当 ID 存在时才执行 -}) -``` - ---- - -## react-hook-form 使用 - -### 基础表单 - -```typescript -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' - -// 定义验证 schema -const schema = z.object({ - name: z.string().min(1, '请输入名称'), - email: z.string().email('邮箱格式不正确'), - age: z.number().min(18, '年龄必须大于18'), -}) - -type FormData = z.infer - -function MyForm() { - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - name: '', - email: '', - age: 0, - }, - }) - - const onSubmit = (data: FormData) => { - console.log(data) - } - - return ( -
- - {form.formState.errors.name && ( - {form.formState.errors.name.message} - )} - - -
- ) -} -``` - -### 使用 shadcn/ui Form 组件 - -```typescript -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form' -import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' - -function MyForm() { - const form = useForm({ - resolver: zodResolver(schema), - }) - - return ( -
- - ( - - 名称 - - - - - - )} - /> - - - - - ) -} -``` - -### 动态表单验证 - -```typescript -// 根据条件动态验证 -const schema = z.object({ - type: z.enum(['official', 'custom']), - apiKey: z.string().optional(), - baseUrl: z.string().optional(), -}).refine( - (data) => { - // 如果是自定义供应商,必须填写 baseUrl - if (data.type === 'custom') { - return !!data.baseUrl - } - return true - }, - { - message: '自定义供应商必须填写 Base URL', - path: ['baseUrl'], - } -) -``` - -### 手动触发验证 - -```typescript -function MyForm() { - const form = useForm() - - const handleBlur = async () => { - // 验证单个字段 - await form.trigger('name') - - // 验证多个字段 - await form.trigger(['name', 'email']) - - // 验证所有字段 - const isValid = await form.trigger() - } - - return
...
-} -``` - ---- - -## shadcn/ui 组件使用 - -### Dialog (对话框) - -```typescript -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' - -function MyDialog() { - const [open, setOpen] = useState(false) - - return ( - - - - 标题 - 描述信息 - - - {/* 内容 */} -
对话框内容
- - - - - -
-
- ) -} -``` - -### Select (选择器) - -```typescript -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' - -function MySelect() { - const [value, setValue] = useState('') - - return ( - - ) -} -``` - -### Tabs (标签页) - -```typescript -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' - -function MyTabs() { - return ( - - - 标签1 - 标签2 - 标签3 - - - -
标签1的内容
-
- - -
标签2的内容
-
- - -
标签3的内容
-
-
- ) -} -``` - -### Toast 通知 (Sonner) - -```typescript -import { toast } from 'sonner' - -// 成功通知 -toast.success('操作成功') - -// 错误通知 -toast.error('操作失败') - -// 加载中 -const toastId = toast.loading('处理中...') -// 完成后更新 -toast.success('处理完成', { id: toastId }) -// 或 -toast.dismiss(toastId) - -// 自定义持续时间 -toast.success('消息', { duration: 5000 }) - -// 带操作按钮 -toast('确认删除?', { - action: { - label: '删除', - onClick: () => handleDelete(), - }, -}) -``` - ---- - -## 代码迁移示例 - -### 示例 1: 状态管理迁移 - -**旧代码** (手动状态管理): - -```typescript -const [providers, setProviders] = useState>({}) -const [currentProviderId, setCurrentProviderId] = useState('') -const [loading, setLoading] = useState(false) -const [error, setError] = useState(null) - -useEffect(() => { - const load = async () => { - setLoading(true) - setError(null) - try { - const data = await window.api.getProviders(appType) - const currentId = await window.api.getCurrentProvider(appType) - setProviders(data) - setCurrentProviderId(currentId) - } catch (err) { - setError(err as Error) - } finally { - setLoading(false) - } - } - load() -}, [appId]) -``` - -**新代码** (React Query): - -```typescript -const { data, isLoading, error } = useProvidersQuery(appId) -const providers = data?.providers || {} -const currentProviderId = data?.currentProviderId || '' -``` - -**减少**: 从 20+ 行到 3 行 - ---- - -### 示例 2: 表单验证迁移 - -**旧代码** (手动验证): - -```typescript -const [name, setName] = useState('') -const [nameError, setNameError] = useState('') -const [apiKey, setApiKey] = useState('') -const [apiKeyError, setApiKeyError] = useState('') - -const validate = () => { - let valid = true - - if (!name.trim()) { - setNameError('请输入名称') - valid = false - } else { - setNameError('') - } - - if (!apiKey.trim()) { - setApiKeyError('请输入 API Key') - valid = false - } else if (apiKey.length < 10) { - setApiKeyError('API Key 长度不足') - valid = false - } else { - setApiKeyError('') - } - - return valid -} - -const handleSubmit = () => { - if (validate()) { - // 提交 - } -} - -return ( -
- setName(e.target.value)} /> - {nameError && {nameError}} - - setApiKey(e.target.value)} /> - {apiKeyError && {apiKeyError}} - - -
-) -``` - -**新代码** (react-hook-form + zod): - -```typescript -const schema = z.object({ - name: z.string().min(1, '请输入名称'), - apiKey: z.string().min(10, 'API Key 长度不足'), -}) - -const form = useForm({ - resolver: zodResolver(schema), -}) - -return ( -
- - ( - - - - - - - )} - /> - - ( - - - - - - - )} - /> - - - - -) -``` - -**减少**: 从 40+ 行到 30 行,且更健壮 - ---- - -### 示例 3: 通知系统迁移 - -**旧代码** (自定义通知): - -```typescript -const [notification, setNotification] = useState<{ - message: string - type: 'success' | 'error' -} | null>(null) -const [isVisible, setIsVisible] = useState(false) - -const showNotification = (message: string, type: 'success' | 'error') => { - setNotification({ message, type }) - setIsVisible(true) - setTimeout(() => { - setIsVisible(false) - setTimeout(() => setNotification(null), 300) - }, 3000) -} - -return ( - <> - {notification && ( -
- {notification.message} -
- )} - {/* 其他内容 */} - -) -``` - -**新代码** (Sonner): - -```typescript -import { toast } from 'sonner' - -// 在需要的地方直接调用 -toast.success('操作成功') -toast.error('操作失败') - -// 在 main.tsx 中只需添加一次 -import { Toaster } from '@/components/ui/sonner' - - -``` - -**减少**: 从 20+ 行到 1 行调用 - ---- - -### 示例 4: 对话框迁移 - -**旧代码** (自定义 Modal): - -```typescript -const [isOpen, setIsOpen] = useState(false) - -return ( - <> - - - {isOpen && ( -
setIsOpen(false)}> -
e.stopPropagation()}> -
-

标题

- -
-
- {/* 内容 */} -
-
- - -
-
-
- )} - -) -``` - -**新代码** (shadcn/ui Dialog): - -```typescript -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' - -const [isOpen, setIsOpen] = useState(false) - -return ( - <> - - - - - - 标题 - - {/* 内容 */} - - - - - - - -) -``` - -**优势**: -- 无需自定义样式 -- 内置无障碍支持 -- 自动管理焦点和 ESC 键 - ---- - -### 示例 5: API 调用迁移 - -**旧代码** (window.api): - -```typescript -// 添加供应商 -const handleAdd = async (provider: Provider) => { - try { - await window.api.addProvider(provider, appType) - await loadProviders() - showNotification('添加成功', 'success') - } catch (error) { - showNotification('添加失败', 'error') - } -} -``` - -**新代码** (React Query Mutation): - -```typescript -// 在组件中 -const addMutation = useAddProviderMutation(appId) - -const handleAdd = (provider: Provider) => { - addMutation.mutate(provider) - // 成功和错误处理已在 mutation 定义中处理 -} -``` - -**优势**: -- 自动处理 loading 状态 -- 统一的错误处理 -- 自动刷新数据 -- 更少的样板代码 - ---- - -## 常见问题 - -### Q: 如何在 mutation 成功后关闭对话框? - -```typescript -const mutation = useAddProviderMutation(appId) - -const handleSubmit = (data: Provider) => { - mutation.mutate(data, { - onSuccess: () => { - setIsOpen(false) // 关闭对话框 - }, - }) -} -``` - -### Q: 如何在表单中使用异步验证? - -```typescript -const schema = z.object({ - name: z.string().refine( - async (name) => { - // 检查名称是否已存在 - const exists = await checkNameExists(name) - return !exists - }, - { message: '名称已存在' } - ), -}) -``` - -### Q: 如何手动刷新 Query 数据? - -```typescript -const queryClient = useQueryClient() - -// 方式1: 使缓存失效,触发重新获取 -queryClient.invalidateQueries({ queryKey: ['providers', appId] }) - -// 方式2: 直接刷新 -queryClient.refetchQueries({ queryKey: ['providers', appId] }) - -// 方式3: 更新缓存数据 -queryClient.setQueryData(['providers', appId], newData) -``` - -### Q: 如何在组件外部使用 toast? - -```typescript -// 直接导入并使用即可 -import { toast } from 'sonner' - -export const someUtil = () => { - toast.success('工具函数中的通知') -} -``` - ---- - -## 调试技巧 - -### React Query DevTools - -```typescript -// 在 main.tsx 中添加 -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' - - - - - -``` - -### 查看表单状态 - -```typescript -const form = useForm() - -// 在开发模式下打印表单状态 -console.log('Form values:', form.watch()) -console.log('Form errors:', form.formState.errors) -console.log('Is valid:', form.formState.isValid) -``` - ---- - -## 性能优化建议 - -### 1. 避免不必要的重渲染 - -```typescript -// 使用 React.memo -export const ProviderCard = React.memo(({ provider, onEdit }: Props) => { - // ... -}) - -// 或使用 useMemo -const sortedProviders = useMemo( - () => Object.values(providers).sort(...), - [providers] -) -``` - -### 2. Query 配置优化 - -```typescript -const { data } = useQuery({ - queryKey: ['providers', appId], - queryFn: fetchProviders, - staleTime: 1000 * 60 * 5, // 5分钟内不重新获取 - gcTime: 1000 * 60 * 10, // 10分钟后清除缓存 -}) -``` - -### 3. 表单性能优化 - -```typescript -// 使用 mode 控制验证时机 -const form = useForm({ - mode: 'onBlur', // 失去焦点时验证 - // mode: 'onChange', // 每次输入都验证(较慢) - // mode: 'onSubmit', // 提交时验证(最快) -}) -``` - ---- - -**提示**: 将此文档保存在浏览器书签或编辑器中,方便随时查阅! diff --git a/docs/TEST_DEVELOPMENT_PLAN.md b/docs/TEST_DEVELOPMENT_PLAN.md deleted file mode 100644 index dc5d87039..000000000 --- a/docs/TEST_DEVELOPMENT_PLAN.md +++ /dev/null @@ -1,73 +0,0 @@ -# 前端测试开发计划 - -## 1. 背景与目标 -- **背景**:v3.5.0 起前端功能快速扩张(供应商管理、MCP、导入导出、端点测速、国际化),缺失系统化测试导致回归风险与人工验证成本攀升。 -- **目标**:在 3 个迭代内建立覆盖关键业务的自动化测试体系,形成稳定的手动冒烟流程,并将测试执行纳入 CI/CD。 - -## 2. 范围与优先级 -| 范围 | 内容 | 优先级 | -| --- | --- | --- | -| 供应商管理 | 列表、排序、预设/自定义表单、切换、复制、删除 | P0 | -| 配置导入导出 | JSON 校验、备份、进度反馈、失败回滚 | P0 | -| MCP 管理 | 列表、启停、模板、命令校验 | P1 | -| 设置面板 | 主题/语言切换、目录设置、关于、更新检查 | P1 | -| 端点速度测试 & 使用脚本 | 启动测试、状态指示、脚本保存 | P2 | -| 国际化 | 中英切换、缺省文案回退 | P2 | - -## 3. 测试分层策略 -- **单元测试(Vitest)**:纯函数与 Hook(`useProviderActions`、`useSettingsForm`、`useDragSort`、`useImportExport` 等)验证数据处理、错误分支、排序逻辑。 -- **组件测试(React Testing Library)**:关键组件(`ProviderList`、`AddProviderDialog`、`SettingsDialog`、`McpPanel`)模拟交互、校验、提示;结合 MSW 模拟 API。 -- **集成测试(App 级别)**:挂载 `App.tsx`,覆盖应用切换、编辑模式、导入导出回调、语言切换,验证状态同步与 toast 提示。 -- **端到端测试(Playwright)**:依赖 `pnpm dev:renderer`,串联供应商 CRUD、排序拖拽、MCP 启停、语言切换即时刷新、更新检查跳转。 -- **手动冒烟**:Tauri 桌面包 + dev server 双通道,验证托盘、系统权限、真实文件写入。 - -## 4. 环境与工具 -- 依赖:Node 18+、pnpm 8+、Vitest、React Testing Library、MSW、Playwright、Testing Library User Event、Playwright Trace Viewer。 -- 配置要点: - - 在 `tsconfig` 中共享别名,Vitest 配合 `vite.config.mts`。 - - `setupTests.ts` 统一注册 MSW/RTL、自定义 matcher。 - - Playwright 使用多浏览器矩阵(Chromium 必选,WebKit 可选),并共享 `.env.test`。 - - Mock `@tauri-apps/api` 与 `providersApi`/`settingsApi`,隔离 Rust 层。 - -## 5. 自动化建设里程碑 -| 周期 | 目标 | 交付 | -| --- | --- | --- | -| Sprint 1 | Vitest 基础设施、核心 Hook 单测(P0) | `pnpm test:unit`、覆盖率报告、10+ 用例 | -| Sprint 2 | 组件/集成测试、MSW Mock 层 | `pnpm test:component`、App 主流程用例 | -| Sprint 3 | Playwright E2E、CI 接入 | `pnpm test:e2e`、CI job、冒烟脚本 | -| 持续 | 回归用例补齐、视觉比对探索 | Playwright Trace、截图基线 | - -## 6. 用例规划概览 -- **供应商管理**:新增(预设+自定义)、编辑校验、复制排序、切换失败回退、删除确认、使用脚本保存。 -- **导入导出**:成功、重复导入、校验失败、备份失败提示、导入后托盘刷新。 -- **MCP**:模板应用、协议切换(stdio/http)、命令校验、启停状态持久化。 -- **设置**:主题/语言即时生效、目录路径更新、更新检查按钮外链、关于信息渲染。 -- **端点速度测试**:触发测试、loading/成功/失败状态、指示器颜色、测速数据排序。 -- **国际化**:默认中文、切换英文后主界面/对话框文案变化、缺失 key fallback。 - -## 7. 数据与 Mock 策略 -- 在 `tests/fixtures/` 维护标准供应商、MCP、设置数据集。 -- 使用 MSW 拦截 `providersApi`、`settingsApi`、`providersApi.onSwitched` 等调用;提供延迟/错误注入接口以覆盖异常分支。 -- Playwright 端提供临时用户目录(`TMP_CC_SWITCH_HOME`)+ 伪配置文件,以验证真实文件交互路径。 - -## 8. 质量门禁与指标 -- 覆盖率目标:单元 ≥75%,分支 ≥70%,逐步提升至 80%+。 -- CI 阶段:`pnpm typecheck` → `pnpm format:check` → `pnpm test:unit` → `pnpm test:component` → `pnpm test:e2e`(可在 nightly 执行)。 -- 缺陷处理:修复前补充最小复现测试;E2E 冒烟必须陪跑重大功能发布。 - -## 9. 工作流与职责 -- **测试负责人**:前端工程师轮值;负责测试计划维护、PR 流水线健康。 -- **开发者职责**:提交功能需附新增/更新测试、列出手动验证步骤、如涉及 UI 提交截图。 -- **Code Review 检查**:测试覆盖说明、mock 合理性、易读性。 - -## 10. 风险与缓解 -| 风险 | 影响 | 缓解 | -| --- | --- | --- | -| Tauri API Mock 难度高 | 单测无法稳定 | 抽象 API 适配层 + MSW 统一模拟 | -| Playwright 运行时间长 | CI 变慢 | 拆分冒烟/完整版,冒烟只跑关键路径 | -| 国际化文案频繁变化 | 用例脆弱 | 优先断言 data-testid/结构,文案使用翻译 key | - -## 11. 输出与维护 -- 文档维护者:前端团队;每个版本更新后检查测试覆盖清单。 -- 交付物:测试报告(CI artifact)、Playwright Trace、覆盖率摘要。 -- 复盘:每次发布后召开 30 分钟测试复盘,记录缺陷、补齐用例。 diff --git a/docs/opencode-implementation-plan.md b/docs/opencode-implementation-plan.md deleted file mode 100644 index 6aecc4a7b..000000000 --- a/docs/opencode-implementation-plan.md +++ /dev/null @@ -1,485 +0,0 @@ -# OpenCode 第四应用支持实现计划 - -> **范围说明**:本计划暂不包含统一供应商(UniversalProvider)对 OpenCode 的支持,以降低初期实现复杂度。 - -## 概述 - -为 CC Switch 添加 OpenCode 支持,这是第四个受管理的 CLI 应用。OpenCode 的核心差异在于采用**累加式**供应商管理(多供应商共存,应用内热切换),而非现有三应用的**替换式**管理。 - -## 关键设计决策 - -| 特性 | Claude/Codex/Gemini | OpenCode | -|------|---------------------|----------| -| 供应商模式 | 替换式(单一活跃) | 累加式(多供应商共存) | -| UI 按钮 | 启用/切换 | 添加/删除 | -| is_current | 需要 | 不需要 | -| 代理/故障转移 | 支持 | 不支持 | -| API 格式字段 | 无 | 需要(npm 包名) | -| 配置文件 | 各自独立 | `~/.config/opencode/opencode.json` | - -## 配置文件格式 - -### 供应商配置 -```json -{ - "$schema": "https://opencode.ai/config.json", - "provider": { - "provider-id": { - "npm": "@ai-sdk/openai-compatible", - "name": "Provider Name", - "options": { - "baseURL": "https://api.example.com/v1", - "apiKey": "{env:API_KEY}" - }, - "models": { - "model-id": { "name": "Model Name" } - } - } - } -} -``` - -### MCP 配置 -```json -{ - "mcp": { - "remote-server": { - "type": "remote", - "url": "https://example.com/mcp", - "enabled": true - }, - "local-server": { - "type": "local", - "command": ["npx", "-y", "my-mcp-command"], - "enabled": true, - "environment": { "KEY": "value" } - } - } -} -``` - ---- - -## 实现步骤 - -### Phase 1: 后端数据结构扩展 - -#### 1.1 AppType 枚举扩展 -**文件**: `src-tauri/src/app_config.rs` - -```rust -pub enum AppType { - Claude, - Codex, - Gemini, - OpenCode, // 新增 -} -``` - -#### 1.2 McpApps / SkillApps 扩展 -**文件**: `src-tauri/src/app_config.rs` - -```rust -pub struct McpApps { - pub claude: bool, - pub codex: bool, - pub gemini: bool, - pub opencode: bool, // 新增 -} - -pub struct SkillApps { - pub claude: bool, - pub codex: bool, - pub gemini: bool, - pub opencode: bool, // 新增 -} -``` - -#### 1.3 数据库 Schema 迁移 -**文件**: `src-tauri/src/database/schema.rs` - -- `SCHEMA_VERSION` 递增 -- 添加迁移: - ```sql - ALTER TABLE mcp_servers ADD COLUMN enabled_opencode BOOLEAN NOT NULL DEFAULT 0; - ALTER TABLE skills ADD COLUMN enabled_opencode BOOLEAN NOT NULL DEFAULT 0; - ``` - -### Phase 2: OpenCode 供应商数据结构 - -#### 2.1 OpenCode 专属配置结构 -**文件**: `src-tauri/src/provider.rs`(或新建 `opencode_provider.rs`) - -```rust -/// OpenCode 供应商的 settings_config 结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeProviderConfig { - /// AI SDK 包名,如 "@ai-sdk/openai-compatible" - pub npm: String, - /// 供应商选项 - pub options: OpenCodeProviderOptions, - /// 模型定义 - pub models: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeProviderOptions { - #[serde(rename = "baseURL", skip_serializing_if = "Option::is_none")] - pub base_url: Option, - #[serde(rename = "apiKey", skip_serializing_if = "Option::is_none")] - pub api_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeModel { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeModelLimit { - pub context: Option, - pub output: Option, -} -``` - -### Phase 3: OpenCode Live 配置读写 - -#### 3.1 新建 OpenCode 配置模块 -**文件**: `src-tauri/src/opencode_config.rs` - -核心功能: -- `get_opencode_config_path()` → `~/.config/opencode/opencode.json` -- `read_opencode_config()` → 读取整个配置文件 -- `write_opencode_config()` → 原子写入配置文件 -- `get_providers()` → 获取 `provider` 对象 -- `set_provider(id, config)` → 添加/更新供应商 -- `remove_provider(id)` → 删除供应商 -- `get_mcp_servers()` → 获取 `mcp` 对象 -- `set_mcp_server(id, config)` → 添加/更新 MCP 服务器 -- `remove_mcp_server(id)` → 删除 MCP 服务器 - -### Phase 4: MCP 同步模块 - -#### 4.1 新建 OpenCode MCP 同步 -**文件**: `src-tauri/src/mcp/opencode.rs` - -```rust -/// 同步所有 enabled_opencode=true 的服务器到 OpenCode 配置 -pub fn sync_enabled_to_opencode(config: &MultiAppConfig) -> Result<(), AppError> - -/// 同步单个服务器 -pub fn sync_single_server_to_opencode( - config: &MultiAppConfig, - id: &str, - server_spec: &Value -) -> Result<(), AppError> - -/// 从 OpenCode 配置移除服务器 -pub fn remove_server_from_opencode(id: &str) -> Result<(), AppError> - -/// 从 OpenCode 配置导入服务器 -pub fn import_from_opencode(config: &mut MultiAppConfig) -> Result -``` - -**格式转换**: -| CC Switch 统一格式 | OpenCode 格式 | -|-------------------|---------------| -| `type: "stdio"` | `type: "local"` | -| `command` + `args` | `command: [cmd, ...args]` | -| `env` | `environment` | -| `type: "sse"/"http"` | `type: "remote"` | -| `url` | `url` | - -### Phase 5: 供应商服务层 - -#### 5.1 OpenCode 供应商服务 -**文件**: `src-tauri/src/services/provider/opencode.rs` - -核心方法: -```rust -/// 获取所有 OpenCode 供应商 -pub fn list(state: &AppState) -> Result, AppError> - -/// 添加供应商(同时写入 live 配置) -pub fn add(state: &AppState, provider: Provider) -> Result - -/// 更新供应商 -pub fn update(state: &AppState, provider: Provider) -> Result - -/// 删除供应商(同时从 live 配置移除) -pub fn delete(state: &AppState, id: &str) -> Result<(), AppError> - -/// 从 live 配置导入供应商到数据库 -pub fn import_from_live(state: &AppState) -> Result -``` - -**关键差异**: -- 不需要 `switch()` 方法 -- 不需要 `is_current` 管理 -- `add()` 自动写入 live -- `delete()` 自动从 live 移除 - -### Phase 6: Tauri 命令扩展 - -#### 6.1 更新现有命令 -**文件**: `src-tauri/src/commands/providers.rs` - -- 所有命令支持 `app_type = "opencode"` -- OpenCode 特定逻辑分支 - -#### 6.2 新增 OpenCode 专属命令(如需要) -```rust -#[tauri::command] -pub async fn opencode_sync_all_providers(state: State<'_, AppState>) -> Result<(), AppError> -``` - -### Phase 7: 前端类型定义 - -#### 7.1 TypeScript 类型扩展 -**文件**: `src/types.ts` - -```typescript -// AppId 扩展 -type AppId = "claude" | "codex" | "gemini" | "opencode"; - -// OpenCode 专属配置 -interface OpenCodeProviderConfig { - npm: string; // AI SDK 包名 - options: { - baseURL?: string; - apiKey?: string; - headers?: Record; - }; - models: Record; -} - -interface OpenCodeModel { - name: string; - limit?: { - context?: number; - output?: number; - }; -} -``` - -#### 7.2 MCP 应用状态扩展 -**文件**: `src/types.ts` - -```typescript -interface McpApps { - claude: boolean; - codex: boolean; - gemini: boolean; - opencode: boolean; // 新增 -} -``` - -### Phase 8: 前端预设配置 - -#### 8.1 新建 OpenCode 供应商预设 -**文件**: `src/config/opencodeProviderPresets.ts` - -```typescript -export const opencodeProviderPresets: ProviderPreset[] = [ - { - name: "OpenAI", - npmPackage: "@ai-sdk/openai", - settingsConfig: { - npm: "@ai-sdk/openai", - options: { apiKey: "{env:OPENAI_API_KEY}" }, - models: { - "gpt-4o": { name: "GPT-4o" }, - "gpt-4o-mini": { name: "GPT-4o Mini" }, - }, - }, - theme: { icon: "openai", iconColor: "#00A67E" }, - }, - { - name: "Anthropic", - npmPackage: "@ai-sdk/anthropic", - settingsConfig: { - npm: "@ai-sdk/anthropic", - options: { apiKey: "{env:ANTHROPIC_API_KEY}" }, - models: { - "claude-sonnet-4-20250514": { name: "Claude Sonnet 4" }, - }, - }, - }, - { - name: "OpenAI Compatible", - npmPackage: "@ai-sdk/openai-compatible", - settingsConfig: { - npm: "@ai-sdk/openai-compatible", - options: { - baseURL: "", - apiKey: "{env:API_KEY}", - }, - models: {}, - }, - isCustomTemplate: true, - }, - // ... 更多预设 -]; - -// npm 包选项 -export const opencodeNpmPackages = [ - { value: "@ai-sdk/openai", label: "OpenAI" }, - { value: "@ai-sdk/anthropic", label: "Anthropic" }, - { value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" }, - { value: "@ai-sdk/google", label: "Google" }, - { value: "@ai-sdk/azure", label: "Azure OpenAI" }, - { value: "@ai-sdk/amazon-bedrock", label: "Amazon Bedrock" }, - // ... 更多选项 -]; -``` - -### Phase 9: 前端 UI 组件 - -#### 9.1 OpenCode 供应商表单 -**文件**: `src/components/providers/forms/OpenCodeFormFields.tsx` - -新增字段: -- npm 包选择器(下拉框 + 自定义输入) -- options 编辑器(baseURL, apiKey, headers) -- models 编辑器(动态添加/删除模型) - -#### 9.2 供应商卡片按钮适配 -**文件**: `src/components/providers/ProviderActions.tsx` - -```tsx -// OpenCode 使用不同的主按钮 -if (appId === "opencode") { - return ( - - ); -} -``` - -#### 9.3 隐藏 OpenCode 不需要的功能 - -在以下组件中检查 `appId !== "opencode"`: -- 代理设置面板 -- 故障转移队列 -- 供应商切换逻辑 - -### Phase 10: 国际化 - -#### 10.1 新增翻译 Key -**文件**: `src/locales/zh/translation.json` & `en/translation.json` - -```json -{ - "app.opencode": "OpenCode", - "provider.addToConfig": "添加到配置", - "provider.removeFromConfig": "从配置移除", - "provider.inConfig": "已添加", - "provider.npmPackage": "AI SDK 包", - "provider.models": "模型配置", - // ... -} -``` - ---- - -## 关键文件清单 - -### 后端(Rust) -| 操作 | 文件路径 | -|------|---------| -| 修改 | `src-tauri/src/app_config.rs` | -| 修改 | `src-tauri/src/database/schema.rs` | -| 修改 | `src-tauri/src/database/dao/mcp.rs` | -| 修改 | `src-tauri/src/database/dao/providers.rs` | -| 修改 | `src-tauri/src/services/provider/mod.rs` | -| 修改 | `src-tauri/src/services/mcp.rs` | -| 修改 | `src-tauri/src/commands/providers.rs` | -| 修改 | `src-tauri/src/commands/mcp.rs` | -| 修改 | `src-tauri/src/mcp/mod.rs` | -| 新建 | `src-tauri/src/opencode_config.rs` | -| 新建 | `src-tauri/src/mcp/opencode.rs` | -| 新建 | `src-tauri/src/services/provider/opencode.rs` | - -### 前端(TypeScript/React) -| 操作 | 文件路径 | -|------|---------| -| 修改 | `src/types.ts` | -| 修改 | `src/lib/api/types.ts` | -| 修改 | `src/lib/api/providers.ts` | -| 修改 | `src/components/providers/ProviderActions.tsx` | -| 修改 | `src/components/providers/ProviderCard.tsx` | -| 修改 | `src/components/providers/AddProviderDialog.tsx` | -| 修改 | `src/components/providers/forms/ProviderForm.tsx` | -| 修改 | `src/App.tsx` | -| 新建 | `src/config/opencodeProviderPresets.ts` | -| 新建 | `src/components/providers/forms/OpenCodeFormFields.tsx` | - -### 国际化 -| 操作 | 文件路径 | -|------|---------| -| 修改 | `src/locales/zh/translation.json` | -| 修改 | `src/locales/en/translation.json` | -| 修改 | `src/locales/ja/translation.json` | - ---- - -## 验证计划 - -### 单元测试 -1. OpenCode 配置读写测试 -2. MCP 格式转换测试(stdio ↔ local, sse ↔ remote) -3. 供应商 CRUD 操作测试 - -### 集成测试 -1. 添加 OpenCode 供应商 → 验证写入 `~/.config/opencode/opencode.json` -2. 删除供应商 → 验证从配置文件移除 -3. MCP 同步测试 → 验证格式正确转换 -4. 从 live 配置导入 → 验证正确解析 - -### 手动测试 -1. UI 流程:添加预设 → 编辑 → 删除 -2. 切换应用 Tab → OpenCode 显示正确的 UI(无代理/故障转移) -3. 托盘菜单正确显示 OpenCode 供应商 -4. 深链接导入 OpenCode 供应商 - ---- - -## 风险评估 - -1. **数据库迁移**:需要在升级时自动执行 `ALTER TABLE` 语句 -2. **配置文件冲突**:OpenCode 可能有自己的配置,需要合并而非覆盖 -3. **MCP 格式差异**:`stdio` → `local` 转换需要处理边界情况 -4. **UI 一致性**:OpenCode 的"添加/删除"模式需要与其他应用的"启用/切换"清晰区分 - ---- - -## 补充说明 - -### 托盘菜单特殊处理 - -由于 OpenCode 采用累加式管理,托盘菜单行为需要调整: - -- **现有三应用**:托盘菜单显示 `CheckMenuItem`(单选,切换当前供应商) -- **OpenCode**:显示当前所有启用的供应商(普通 MenuItem,无勾选逻辑),点击打开主界面 - -**修改文件**:`src-tauri/src/tray.rs`(`TRAY_SECTIONS` 常量) - -### 数据库约束更新 - -`proxy_config` 表的 CHECK 约束需要扩展: -```sql -CHECK (app_type IN ('claude','codex','gemini','opencode')) -``` - -### Settings 结构体扩展 - -**文件**:`src-tauri/src/settings.rs` - -需要添加: -- `current_provider_opencode: Option` - 对 OpenCode 可能无意义,但保持结构一致 -- `opencode_config_dir: Option` - 自定义配置目录 diff --git a/docs/release-note-v3.10.0-en.md b/docs/release-notes/v3.10.0-en.md similarity index 98% rename from docs/release-note-v3.10.0-en.md rename to docs/release-notes/v3.10.0-en.md index e7a2117b0..801c88f9a 100644 --- a/docs/release-note-v3.10.0-en.md +++ b/docs/release-notes/v3.10.0-en.md @@ -2,7 +2,7 @@ > OpenCode Support, Global Proxy, Claude Rectifier & Multi-App Experience Enhancements -**[中文版 →](release-note-v3.10.0-zh.md) | [日本語版 →](release-note-v3.10.0-ja.md)** +**[中文版 →](v3.10.0-zh.md) | [日本語版 →](v3.10.0-ja.md)** --- diff --git a/docs/release-note-v3.10.0-ja.md b/docs/release-notes/v3.10.0-ja.md similarity index 99% rename from docs/release-note-v3.10.0-ja.md rename to docs/release-notes/v3.10.0-ja.md index 081e17c36..c8c89feaf 100644 --- a/docs/release-note-v3.10.0-ja.md +++ b/docs/release-notes/v3.10.0-ja.md @@ -2,7 +2,7 @@ > OpenCode サポート、グローバルプロキシ、Claude Rectifier とマルチアプリ体験の強化 -**[中文版 →](release-note-v3.10.0-zh.md) | [English →](release-note-v3.10.0-en.md)** +**[中文版 →](v3.10.0-zh.md) | [English →](v3.10.0-en.md)** --- diff --git a/docs/release-note-v3.10.0-zh.md b/docs/release-notes/v3.10.0-zh.md similarity index 98% rename from docs/release-note-v3.10.0-zh.md rename to docs/release-notes/v3.10.0-zh.md index 4df403de8..259a8fbc3 100644 --- a/docs/release-note-v3.10.0-zh.md +++ b/docs/release-notes/v3.10.0-zh.md @@ -2,7 +2,7 @@ > OpenCode 支持、全局代理、Claude Rectifier 与多应用体验增强 -**[English →](release-note-v3.10.0-en.md) | [日本語版 →](release-note-v3.10.0-ja.md)** +**[English →](v3.10.0-en.md) | [日本語版 →](v3.10.0-ja.md)** --- diff --git a/docs/release-note-v3.11.0-en.md b/docs/release-notes/v3.11.0-en.md similarity index 99% rename from docs/release-note-v3.11.0-en.md rename to docs/release-notes/v3.11.0-en.md index 4567d81b5..371e6bcfb 100644 --- a/docs/release-note-v3.11.0-en.md +++ b/docs/release-notes/v3.11.0-en.md @@ -2,7 +2,7 @@ > OpenClaw Support, Session Manager, Backup Management & 50+ Improvements -**[中文版 →](release-note-v3.11.0-zh.md) | [日本語版 →](release-note-v3.11.0-ja.md)** +**[中文版 →](v3.11.0-zh.md) | [日本語版 →](v3.11.0-ja.md)** --- diff --git a/docs/release-note-v3.11.0-ja.md b/docs/release-notes/v3.11.0-ja.md similarity index 99% rename from docs/release-note-v3.11.0-ja.md rename to docs/release-notes/v3.11.0-ja.md index ce5d03729..bdd664539 100644 --- a/docs/release-note-v3.11.0-ja.md +++ b/docs/release-notes/v3.11.0-ja.md @@ -2,7 +2,7 @@ > OpenClaw サポート、セッションマネージャー、バックアップ管理と 50 以上の改善 -**[中文版 →](release-note-v3.11.0-zh.md) | [English →](release-note-v3.11.0-en.md)** +**[中文版 →](v3.11.0-zh.md) | [English →](v3.11.0-en.md)** --- diff --git a/docs/release-note-v3.11.0-zh.md b/docs/release-notes/v3.11.0-zh.md similarity index 99% rename from docs/release-note-v3.11.0-zh.md rename to docs/release-notes/v3.11.0-zh.md index 07041c4bb..629df7c7b 100644 --- a/docs/release-note-v3.11.0-zh.md +++ b/docs/release-notes/v3.11.0-zh.md @@ -2,7 +2,7 @@ > OpenClaw 支持、会话管理器、备份管理与 50+ 项改进 -**[English →](release-note-v3.11.0-en.md) | [日本語版 →](release-note-v3.11.0-ja.md)** +**[English →](v3.11.0-en.md) | [日本語版 →](v3.11.0-ja.md)** --- diff --git a/docs/release-note-v3.11.1-en.md b/docs/release-notes/v3.11.1-en.md similarity index 98% rename from docs/release-note-v3.11.1-en.md rename to docs/release-notes/v3.11.1-en.md index 48ff18156..66ecca9ee 100644 --- a/docs/release-note-v3.11.1-en.md +++ b/docs/release-notes/v3.11.1-en.md @@ -2,7 +2,7 @@ > Revert Partial Key-Field Merging, Restore Common Config Snippet & Bug Fixes -**[中文版 →](release-note-v3.11.1-zh.md) | [日本語版 →](release-note-v3.11.1-ja.md)** +**[中文版 →](v3.11.1-zh.md) | [日本語版 →](v3.11.1-ja.md)** --- diff --git a/docs/release-note-v3.11.1-ja.md b/docs/release-notes/v3.11.1-ja.md similarity index 98% rename from docs/release-note-v3.11.1-ja.md rename to docs/release-notes/v3.11.1-ja.md index 3d4aa862e..321663db4 100644 --- a/docs/release-note-v3.11.1-ja.md +++ b/docs/release-notes/v3.11.1-ja.md @@ -2,7 +2,7 @@ > 部分キーフィールドマージの撤回、共通設定スニペットの復元とバグ修正 -**[中文版 →](release-note-v3.11.1-zh.md) | [English →](release-note-v3.11.1-en.md)** +**[中文版 →](v3.11.1-zh.md) | [English →](v3.11.1-en.md)** --- diff --git a/docs/release-note-v3.11.1-zh.md b/docs/release-notes/v3.11.1-zh.md similarity index 98% rename from docs/release-note-v3.11.1-zh.md rename to docs/release-notes/v3.11.1-zh.md index 9c2dc1590..1df412104 100644 --- a/docs/release-note-v3.11.1-zh.md +++ b/docs/release-notes/v3.11.1-zh.md @@ -2,7 +2,7 @@ > 回退部分键值合并、恢复通用配置片段与多项修复 -**[English →](release-note-v3.11.1-en.md) | [日本語版 →](release-note-v3.11.1-ja.md)** +**[English →](v3.11.1-en.md) | [日本語版 →](v3.11.1-ja.md)** --- diff --git a/docs/release-note-v3.6.0-en.md b/docs/release-notes/v3.6.0-en.md similarity index 99% rename from docs/release-note-v3.6.0-en.md rename to docs/release-notes/v3.6.0-en.md index e2512d298..45f61c1c4 100644 --- a/docs/release-note-v3.6.0-en.md +++ b/docs/release-notes/v3.6.0-en.md @@ -1,6 +1,6 @@ ## Major architecture refactoring with enhanced config sync and data protection -**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-note-v3.6.0-zh.md)** +**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-notes/v3.6.0-zh.md)** --- diff --git a/docs/release-note-v3.6.0-zh.md b/docs/release-notes/v3.6.0-zh.md similarity index 99% rename from docs/release-note-v3.6.0-zh.md rename to docs/release-notes/v3.6.0-zh.md index e56b1b83a..32066a42a 100644 --- a/docs/release-note-v3.6.0-zh.md +++ b/docs/release-notes/v3.6.0-zh.md @@ -2,7 +2,7 @@ > 全栈架构重构,增强配置同步与数据保护 -**[English Version →](../release-note-v3.6.0.md)** +**[English Version →](v3.6.0-en.md)** --- diff --git a/docs/release-note-v3.6.1-en.md b/docs/release-notes/v3.6.1-en.md similarity index 99% rename from docs/release-note-v3.6.1-en.md rename to docs/release-notes/v3.6.1-en.md index a291f927b..0fdd2b913 100644 --- a/docs/release-note-v3.6.1-en.md +++ b/docs/release-notes/v3.6.1-en.md @@ -2,7 +2,7 @@ > Stability improvements and user experience optimization (based on v3.6.0) -**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-note-v3.6.1-zh.md)** +**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-notes/v3.6.1-zh.md)** --- diff --git a/docs/release-note-v3.6.1-zh.md b/docs/release-notes/v3.6.1-zh.md similarity index 99% rename from docs/release-note-v3.6.1-zh.md rename to docs/release-notes/v3.6.1-zh.md index 76943f455..bc803d69d 100644 --- a/docs/release-note-v3.6.1-zh.md +++ b/docs/release-notes/v3.6.1-zh.md @@ -2,7 +2,7 @@ > 稳定性提升与用户体验优化(基于 v3.6.0) -**[English Version →](../release-note-v3.6.1.md)** +**[English Version →](v3.6.1-en.md)** --- diff --git a/docs/release-note-v3.7.0-en.md b/docs/release-notes/v3.7.0-en.md similarity index 99% rename from docs/release-note-v3.7.0-en.md rename to docs/release-notes/v3.7.0-en.md index a115e4b74..1f4ca2a9f 100644 --- a/docs/release-note-v3.7.0-en.md +++ b/docs/release-notes/v3.7.0-en.md @@ -2,7 +2,7 @@ > From Provider Switcher to All-in-One AI CLI Management Platform -**[中文更新说明 Chinese Documentation →](release-note-v3.7.0-zh.md)** +**[中文更新说明 Chinese Documentation →](v3.7.0-zh.md)** --- diff --git a/docs/release-note-v3.7.0-zh.md b/docs/release-notes/v3.7.0-zh.md similarity index 99% rename from docs/release-note-v3.7.0-zh.md rename to docs/release-notes/v3.7.0-zh.md index 78e025c24..85ae6afaa 100644 --- a/docs/release-note-v3.7.0-zh.md +++ b/docs/release-notes/v3.7.0-zh.md @@ -2,7 +2,7 @@ > 从供应商切换器到 AI CLI 一体化管理平台 -**[English Version →](release-note-v3.7.0-en.md)** +**[English Version →](v3.7.0-en.md)** --- diff --git a/docs/release-note-v3.7.1-en.md b/docs/release-notes/v3.7.1-en.md similarity index 99% rename from docs/release-note-v3.7.1-en.md rename to docs/release-notes/v3.7.1-en.md index b549ecf18..69e893da5 100644 --- a/docs/release-note-v3.7.1-en.md +++ b/docs/release-notes/v3.7.1-en.md @@ -2,7 +2,7 @@ > Stability Enhancements and User Experience Improvements -**[中文更新说明 Chinese Documentation →](release-note-v3.7.1-zh.md)** +**[中文更新说明 Chinese Documentation →](v3.7.1-zh.md)** --- diff --git a/docs/release-note-v3.7.1-zh.md b/docs/release-notes/v3.7.1-zh.md similarity index 99% rename from docs/release-note-v3.7.1-zh.md rename to docs/release-notes/v3.7.1-zh.md index 67cc0ed6a..1ad8459d5 100644 --- a/docs/release-note-v3.7.1-zh.md +++ b/docs/release-notes/v3.7.1-zh.md @@ -2,7 +2,7 @@ > 稳定性增强与用户体验改进 -**[English Version →](release-note-v3.7.1-en.md)** +**[English Version →](v3.7.1-en.md)** --- diff --git a/docs/release-note-v3.8.0-en.md b/docs/release-notes/v3.8.0-en.md similarity index 99% rename from docs/release-note-v3.8.0-en.md rename to docs/release-notes/v3.8.0-en.md index dc605d2a4..13012d8e1 100644 --- a/docs/release-note-v3.8.0-en.md +++ b/docs/release-notes/v3.8.0-en.md @@ -2,7 +2,7 @@ > Persistence Architecture Upgrade, Laying the Foundation for Cloud Sync -**[中文版 →](release-note-v3.8.0-zh.md) | [日本語版 →](release-note-v3.8.0-ja.md)** +**[中文版 →](v3.8.0-zh.md) | [日本語版 →](v3.8.0-ja.md)** --- diff --git a/docs/release-note-v3.8.0-ja.md b/docs/release-notes/v3.8.0-ja.md similarity index 99% rename from docs/release-note-v3.8.0-ja.md rename to docs/release-notes/v3.8.0-ja.md index 09ae00c87..718245a69 100644 --- a/docs/release-note-v3.8.0-ja.md +++ b/docs/release-notes/v3.8.0-ja.md @@ -2,7 +2,7 @@ > 永続化アーキテクチャを刷新し、クラウド同期の土台を構築 -**[English →](release-note-v3.8.0-en.md) | [中文版 →](release-note-v3.8.0-zh.md)** +**[English →](v3.8.0-en.md) | [中文版 →](v3.8.0-zh.md)** --- diff --git a/docs/release-note-v3.8.0-zh.md b/docs/release-notes/v3.8.0-zh.md similarity index 99% rename from docs/release-note-v3.8.0-zh.md rename to docs/release-notes/v3.8.0-zh.md index 07a110d87..506451e40 100644 --- a/docs/release-note-v3.8.0-zh.md +++ b/docs/release-notes/v3.8.0-zh.md @@ -2,7 +2,7 @@ > 持久化架构升级,为云同步奠定基础 -**[English Version →](release-note-v3.8.0-en.md)** +**[English Version →](v3.8.0-en.md)** --- diff --git a/docs/release-note-v3.9.0-en.md b/docs/release-notes/v3.9.0-en.md similarity index 98% rename from docs/release-note-v3.9.0-en.md rename to docs/release-notes/v3.9.0-en.md index 1614f7b66..470c7e58a 100644 --- a/docs/release-note-v3.9.0-en.md +++ b/docs/release-notes/v3.9.0-en.md @@ -2,7 +2,7 @@ > Local API Proxy, Auto Failover, Universal Provider, and a more complete multi-app workflow -**[中文版 →](release-note-v3.9.0-zh.md) | [日本語版 →](release-note-v3.9.0-ja.md)** +**[中文版 →](v3.9.0-zh.md) | [日本語版 →](v3.9.0-ja.md)** --- diff --git a/docs/release-note-v3.9.0-ja.md b/docs/release-notes/v3.9.0-ja.md similarity index 99% rename from docs/release-note-v3.9.0-ja.md rename to docs/release-notes/v3.9.0-ja.md index 7fa1b9cf7..b87d8896c 100644 --- a/docs/release-note-v3.9.0-ja.md +++ b/docs/release-notes/v3.9.0-ja.md @@ -2,7 +2,7 @@ > ローカル API プロキシ、自動フェイルオーバー、Universal Provider、多アプリ対応の強化 -**[English →](release-note-v3.9.0-en.md) | [中文版 →](release-note-v3.9.0-zh.md)** +**[English →](v3.9.0-en.md) | [中文版 →](v3.9.0-zh.md)** --- diff --git a/docs/release-note-v3.9.0-zh.md b/docs/release-notes/v3.9.0-zh.md similarity index 98% rename from docs/release-note-v3.9.0-zh.md rename to docs/release-notes/v3.9.0-zh.md index 9d13fa05d..2f98c0114 100644 --- a/docs/release-note-v3.9.0-zh.md +++ b/docs/release-notes/v3.9.0-zh.md @@ -2,7 +2,7 @@ > 本地 API 代理、自动故障切换、统一供应商与多应用工作流增强 -**[English →](release-note-v3.9.0-en.md) | [日本語版 →](release-note-v3.9.0-ja.md)** +**[English →](v3.9.0-en.md) | [日本語版 →](v3.9.0-ja.md)** --- diff --git a/docs/roadmap.md b/docs/roadmap.md deleted file mode 100644 index ede2d515f..000000000 --- a/docs/roadmap.md +++ /dev/null @@ -1,10 +0,0 @@ -- 自动升级自定义路径 ✅ -- win 绿色版报毒问题 ✅ -- mcp 管理器 ✅ -- i18n ✅ -- gemini cli -- homebrew 支持 ✅ -- memory 管理 -- codex 更多预设供应商 -- 云同步 -- 本地代理 diff --git a/docs/v3.7.0-unified-mcp-refactor.md b/docs/v3.7.0-unified-mcp-refactor.md deleted file mode 100644 index 0e5b0f001..000000000 --- a/docs/v3.7.0-unified-mcp-refactor.md +++ /dev/null @@ -1,863 +0,0 @@ -# v3.7.0 统一 MCP 管理重构计划 - -## 📋 项目概述 - -**目标**:将原有的按应用分离的 MCP 管理(Claude/Codex/Gemini 各自独立管理)重构为统一管理面板,每个 MCP 服务器通过多选框控制应用到哪些客户端。 - -**版本**:v3.6.2 → v3.7.0 - -**开始时间**:2025-11-14 - ---- - -## 🎯 核心需求 - -### 原有架构(v3.6.x) - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Claude面板 │ │ Codex面板 │ │ Gemini面板 │ -│ MCP管理 │ │ MCP管理 │ │ MCP管理 │ -└─────────────┘ └─────────────┘ └─────────────┘ - ↓ ↓ ↓ - mcp.claude mcp.codex mcp.gemini - {servers} {servers} {servers} -``` - -### 新架构(v3.7.0) - -``` -┌───────────────────────────────────────┐ -│ 统一 MCP 管理面板 │ -│ ┌────────┬────────┬────────┬────┐ │ -│ │ 服务器 │ Claude │ Codex │Gem │ │ -│ ├────────┼────────┼────────┼────┤ │ -│ │ mcp-1 │ ✓ │ ✓ │ │ │ -│ │ mcp-2 │ ✓ │ │ ✓ │ │ -│ └────────┴────────┴────────┴────┘ │ -└───────────────────────────────────────┘ - ↓ - mcp.servers - { - "mcp-1": { - apps: {claude: true, codex: true, gemini: false} - } - } -``` - ---- - -## 📐 技术架构 - -### 数据结构设计 - -#### 新增:McpApps(应用启用状态) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] -pub struct McpApps { - pub claude: bool, - pub codex: bool, - pub gemini: bool, -} -``` - -#### 更新:McpServer(统一服务器定义) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct McpServer { - pub id: String, - pub name: String, - pub server: serde_json::Value, // 连接配置(stdio/http) - pub apps: McpApps, // 新增:标记应用到哪些客户端 - pub description: Option, - pub homepage: Option, - pub docs: Option, - pub tags: Vec, -} -``` - -#### 更新:McpRoot(新旧结构并存) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct McpRoot { - // v3.7.0 新结构 - #[serde(skip_serializing_if = "Option::is_none")] - pub servers: Option>, - - // v3.6.x 旧结构(保留用于迁移) - #[serde(default, skip_serializing_if = "McpConfig::is_empty")] - pub claude: McpConfig, - #[serde(default, skip_serializing_if = "McpConfig::is_empty")] - pub codex: McpConfig, - #[serde(default, skip_serializing_if = "McpConfig::is_empty")] - pub gemini: McpConfig, -} -``` - -### 迁移策略 - -``` -旧配置 (v3.6.x) 新配置 (v3.7.0) -───────────────── ───────────────── -mcp: mcp: - claude: servers: - servers: mcp-fetch: - mcp-fetch: {...} → id: "mcp-fetch" - codex: server: {...} - servers: apps: - mcp-filesystem: {...} claude: true - codex: true - gemini: false -``` - -**迁移逻辑**: -1. 检测 `mcp.servers` 是否存在 -2. 若不存在,从 `mcp.claude/codex/gemini.servers` 收集所有服务器 -3. 合并同 id 服务器的 apps 字段 -4. 清空旧结构字段 -5. 保存配置(自动触发) - ---- - -## ✅ 开发进度 - -### Phase 1: 后端数据结构与迁移 ✅ 已完成 - -#### 1.1 修改数据结构(app_config.rs)✅ - -**文件**:`src-tauri/src/app_config.rs` - -**变更**: -- ✅ 新增 `McpApps` 结构体(lines 30-62) -- ✅ 新增 `McpServer` 结构体(lines 64-79) -- ✅ 更新 `McpRoot` 支持新旧结构(lines 81-96) -- ✅ 添加辅助方法:`is_enabled_for`, `set_enabled_for`, `enabled_apps` - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - -#### 1.2 实现迁移逻辑 ✅ - -**文件**:`src-tauri/src/app_config.rs` - -**实现**: -- ✅ `migrate_mcp_to_unified()` 方法(lines 380-509) - - 从旧结构收集所有服务器 - - 按 id 合并重复服务器 - - 处理冲突(合并 apps 字段) - - 清空旧结构 -- ✅ 集成到 `MultiAppConfig::load()` 方法(lines 252-257) - - 自动检测并执行迁移 - - 迁移后保存配置 - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - ---- - -### Phase 2: 后端服务层重构 ✅ 已完成 - -#### 2.1 重写 McpService ✅ - -**文件**:`src-tauri/src/services/mcp.rs` - -**新增方法**: -- ✅ `get_all_servers()` - 获取所有服务器(lines 13-27) -- ✅ `upsert_server()` - 添加/更新服务器(lines 30-52) -- ✅ `delete_server()` - 删除服务器(lines 55-75) -- ✅ `toggle_app()` - 切换应用启用状态(lines 78-111) -- ✅ `sync_all_enabled()` - 同步所有启用的服务器(lines 180-188) - -**兼容层方法**(已废弃): -- ✅ `get_servers()` - 按应用过滤服务器(lines 196-210) -- ✅ `set_enabled()` - 委托到 toggle_app(lines 213-222) -- ✅ `sync_enabled()` - 同步特定应用(lines 225-236) -- ✅ `import_from_claude/codex/gemini()` - 导入包装(lines 239-266) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - -#### 2.2 新增同步函数(mcp.rs)✅ - -**文件**:`src-tauri/src/mcp.rs` - -**新增函数**(lines 800-965): -- ✅ `json_server_to_toml_table()` - JSON → TOML 转换助手(lines 828-889) -- ✅ `sync_single_server_to_claude()` - 同步单个服务器到 Claude(lines 800-814) -- ✅ `remove_server_from_claude()` - 从 Claude 移除服务器(lines 817-826) -- ✅ `sync_single_server_to_codex()` - 同步单个服务器到 Codex(lines 891-936) -- ✅ `remove_server_from_codex()` - 从 Codex 移除服务器(lines 939-965) -- ✅ `sync_single_server_to_gemini()` - 同步单个服务器到 Gemini(lines 967-977) -- ✅ `remove_server_from_gemini()` - 从 Gemini 移除服务器(lines 980-989) - -**关键修复**: -- ✅ 修复 toml_edit 类型转换(使用手动构建而非 serde 转换) -- ✅ 修复 get_codex_config_path() 调用(返回 PathBuf 而非 Result) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" -**修复提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - -#### 2.3 新增 Tauri Commands ✅ - -**文件**:`src-tauri/src/commands/mcp.rs` - -**新增命令**(lines 147-196): -- ✅ `get_mcp_servers()` - 获取所有服务器(lines 154-159) -- ✅ `upsert_mcp_server()` - 添加/更新服务器(lines 162-168) -- ✅ `delete_mcp_server()` - 删除服务器(lines 171-177) -- ✅ `toggle_mcp_app()` - 切换应用状态(lines 180-189) -- ✅ `sync_all_mcp_servers()` - 同步所有服务器(lines 192-195) - -**更新旧命令**(兼容层): -- ✅ `upsert_mcp_server_in_config()` - 转换为统一结构(lines 68-131) -- ✅ `delete_mcp_server_in_config()` - 忽略 app 参数(lines 134-141) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" -**修复提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - -#### 2.4 注册新命令(lib.rs)✅ - -**文件**:`src-tauri/src/lib.rs` - -**变更**: -- ✅ 导出 `McpServer` 类型(line 21) -- ✅ 导出新增的 mcp 同步函数(lines 26-31) -- ✅ 注册 5 个新命令到 invoke_handler(lines 550-555) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - -#### 2.5 添加缺失的函数(claude_mcp.rs & gemini_mcp.rs)✅ - -**文件**: -- `src-tauri/src/claude_mcp.rs` (lines 234-253) -- `src-tauri/src/gemini_mcp.rs` (lines 160-179) - -**新增**: -- ✅ `read_mcp_servers_map()` - 读取现有 MCP 服务器映射 - -**提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - -#### 2.6 编译验证 ✅ - -**状态**:✅ 编译成功 -- ⚠️ 16 个警告(8 个废弃警告 + 8 个未使用函数警告 - 预期内) -- ✅ 0 个错误 - -**提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - ---- - -### Phase 3: 前端开发 ⚠️ 部分完成 - -#### 3.1 TypeScript 类型定义 ✅ - -**文件**:`src/types.ts` - -**变更**: -- ✅ 新增 `McpApps` 接口(lines 129-133) -- ✅ 更新 `McpServer` 接口(lines 136-149) - - 新增 `apps: McpApps` 字段 - - `name` 改为必填 - - 标记 `enabled` 为废弃 -- ✅ 新增 `McpServersMap` 类型别名(line 152) -- ✅ 保持向后兼容(保留 `enabled`, `source` 等旧字段) - -**提交**:`ac09551` - "feat(frontend): add unified MCP types and API layer for v3.7.0" - -#### 3.2 API 层更新 ✅ - -**文件**:`src/lib/api/mcp.ts` - -**新增方法**(lines 99-141): -- ✅ `getAllServers()` - 获取所有服务器(lines 106-108) -- ✅ `upsertUnifiedServer()` - 添加/更新服务器(lines 113-115) -- ✅ `deleteUnifiedServer()` - 删除服务器(lines 120-122) -- ✅ `toggleApp()` - 切换应用状态(lines 127-133) -- ✅ `syncAllServers()` - 同步所有服务器(lines 138-140) - -**导入更新**: -- ✅ 导入 `McpServersMap` 类型(line 6) - -**提交**:`ac09551` - "feat(frontend): add unified MCP types and API layer for v3.7.0" - -#### 3.3 React Query Hooks 📝 待开发 - -**计划文件**:`src/hooks/useMcp.ts` - -**需要实现的 Hooks**: - -```typescript -// 查询 hooks -export function useAllMcpServers() { - return useQuery({ - queryKey: ['mcp', 'all'], - queryFn: () => mcpApi.getAllServers(), - }); -} - -// 变更 hooks -export function useUpsertMcpServer() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (server: McpServer) => mcpApi.upsertUnifiedServer(server), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] }); - }, - }); -} - -export function useToggleMcpApp() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ serverId, app, enabled }: { - serverId: string; - app: AppId; - enabled: boolean; - }) => mcpApi.toggleApp(serverId, app, enabled), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] }); - }, - }); -} - -export function useDeleteMcpServer() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => mcpApi.deleteUnifiedServer(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] }); - }, - }); -} - -export function useSyncAllMcpServers() { - return useMutation({ - mutationFn: () => mcpApi.syncAllServers(), - }); -} -``` - -**依赖**: -- `@tanstack/react-query` (已安装) -- `src/lib/api/mcp.ts` (✅ 已完成) -- `src/types.ts` (✅ 已完成) - -#### 3.4 统一 MCP 面板组件 📝 待开发 - -**计划文件**:`src/components/mcp/UnifiedMcpPanel.tsx` - -**组件结构**: - -```typescript -interface UnifiedMcpPanelProps { - className?: string; -} - -export function UnifiedMcpPanel({ className }: UnifiedMcpPanelProps) { - const { t } = useTranslation(); - const { data: servers, isLoading } = useAllMcpServers(); - const toggleApp = useToggleMcpApp(); - const deleteServer = useDeleteMcpServer(); - const syncAll = useSyncAllMcpServers(); - - // 组件实现... -} -``` - -**UI 设计**: - -``` -┌─────────────────────────────────────────────────────┐ -│ MCP 服务器管理 ┌──────────┐ │ -│ │ 添加服务器 │ │ -│ ┌─────┐ ┌──────────────┐ ┌─────────┐ └──────────┘ │ -│ │ 搜索 │ │ 导入自...▼ │ │ 同步全部 │ │ -│ └─────┘ └──────────────┘ └─────────┘ │ -├─────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ 名称 │ Claude │ Codex │ Gemini │操作│ │ -│ ├─────────────────────────────────────────────┤ │ -│ │ mcp-fetch │ ✓ │ ✓ │ │ ⚙️ │ │ -│ │ filesystem │ ✓ │ │ ✓ │ ⚙️ │ │ -│ │ brave-search │ │ ✓ │ ✓ │ ⚙️ │ │ -│ └─────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -``` - -**功能特性**: -- 📋 服务器列表展示(名称、描述、标签) -- ☑️ 三个复选框控制应用启用状态(Claude/Codex/Gemini) -- ➕ 添加新服务器(表单模态框) -- ✏️ 编辑服务器(表单模态框) -- 🗑️ 删除服务器(确认对话框) -- 📥 导入功能(从 Claude/Codex/Gemini 导入) -- 🔄 同步全部(手动触发同步到 live 配置) -- 🔍 搜索过滤 -- 🏷️ 标签过滤 - -**子组件**: - -1. **McpServerTable** (`McpServerTable.tsx`) - - 服务器列表表格 - - 应用复选框 - - 操作按钮(编辑、删除) - -2. **McpServerFormModal** (`McpServerFormModal.tsx`) - - 添加/编辑表单 - - stdio/http 类型切换 - - 应用选择(多选) - - 元信息编辑(描述、标签、链接) - -3. **McpImportDialog** (`McpImportDialog.tsx`) - - 选择导入来源(Claude/Codex/Gemini) - - 服务器预览 - - 批量导入 - -**依赖组件**(来自 shadcn/ui): -- `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow` -- `Checkbox` -- `Button` -- `Dialog`, `DialogContent`, `DialogHeader`, `DialogTitle` -- `Input`, `Textarea`, `Label` -- `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue` -- `Badge` -- `Tooltip` - -#### 3.5 主界面集成 📝 待开发 - -**文件**:`src/App.tsx` - -**变更计划**: - -```typescript -// 原有代码(v3.6.x) -{currentApp === 'claude' && } -{currentApp === 'codex' && } -{currentApp === 'gemini' && } - -// 新代码(v3.7.0) - -``` - -**移除的组件**: -- `ClaudeMcpPanel.tsx` -- `CodexMcpPanel.tsx` -- `GeminiMcpPanel.tsx` - -**注意**:保留旧组件文件备份,以便回滚 - -#### 3.6 国际化文本更新 📝 待开发 - -**文件**: -- `src/locales/zh/translation.json` -- `src/locales/en/translation.json` - -**需要添加的翻译键**: - -```json -{ - "mcp": { - "unifiedPanel": { - "title": "MCP 服务器管理 / MCP Server Management", - "addServer": "添加服务器 / Add Server", - "editServer": "编辑服务器 / Edit Server", - "deleteServer": "删除服务器 / Delete Server", - "deleteConfirm": "确定要删除此服务器吗?/ Are you sure to delete this server?", - "syncAll": "同步全部 / Sync All", - "syncAllSuccess": "已同步所有启用的服务器 / All enabled servers synced", - "importFrom": "导入自... / Import from...", - "search": "搜索服务器... / Search servers...", - "noServers": "暂无服务器 / No servers yet", - "enabledApps": "启用的应用 / Enabled Apps", - "apps": { - "claude": "Claude", - "codex": "Codex", - "gemini": "Gemini" - }, - "form": { - "id": "服务器 ID / Server ID", - "name": "显示名称 / Display Name", - "type": "类型 / Type", - "stdio": "本地进程 / Local Process", - "http": "远程服务 / Remote Service", - "command": "命令 / Command", - "args": "参数 / Arguments", - "env": "环境变量 / Environment Variables", - "cwd": "工作目录 / Working Directory", - "url": "URL", - "headers": "请求头 / Headers", - "description": "描述 / Description", - "tags": "标签 / Tags", - "homepage": "主页 / Homepage", - "docs": "文档 / Documentation", - "selectApps": "选择应用 / Select Apps", - "selectAppsHint": "勾选此服务器要应用到哪些客户端 / Check which clients this server applies to" - }, - "table": { - "name": "名称 / Name", - "type": "类型 / Type", - "apps": "应用 / Apps", - "actions": "操作 / Actions", - "edit": "编辑 / Edit", - "delete": "删除 / Delete" - }, - "import": { - "title": "导入 MCP 服务器 / Import MCP Servers", - "fromClaude": "从 Claude 导入 / Import from Claude", - "fromCodex": "从 Codex 导入 / Import from Codex", - "fromGemini": "从 Gemini 导入 / Import from Gemini", - "success": "成功导入 {{count}} 个服务器 / Successfully imported {{count}} server(s)", - "noServersFound": "未找到可导入的服务器 / No servers found to import" - } - } - } -} -``` - ---- - -## 🔄 迁移流程 - -### 用户体验 - -``` -1. 用户升级到 v3.7.0 - ↓ -2. 首次启动应用 - ↓ -3. 后端自动执行迁移 - - 检测旧结构 (mcp.claude/codex/gemini.servers) - - 合并到统一结构 (mcp.servers) - - 保存迁移后的配置 - - 日志记录迁移详情 - ↓ -4. 前端加载新面板 - - 显示所有服务器 - - 三个复选框显示各应用启用状态 - ↓ -5. 用户无缝使用 -``` - -### 数据完整性保证 - -1. **迁移前验证**: - - ✅ 校验旧结构合法性 - - ✅ 记录迁移前状态 - -2. **迁移中处理**: - - ✅ 合并同 id 服务器的 apps 字段 - - ✅ 处理 id 冲突(保留第一个,记录警告) - - ✅ 保留所有元信息(描述、标签、链接) - -3. **迁移后清理**: - - ✅ 清空旧结构(claude/codex/gemini) - - ✅ 自动保存新配置 - - ✅ 日志记录迁移完成 - -4. **回滚机制**: - - 配置文件有备份(`config.v1.backup..json`) - - 迁移失败时可手动回滚 - ---- - -## 🧪 测试计划 - -### 后端测试 ✅ 已验证 - -- [x] 编译测试(cargo check) -- [x] 数据结构序列化/反序列化 -- [ ] 迁移逻辑单元测试 -- [ ] 服务层方法测试 -- [ ] 同步函数测试 - -### 前端测试 ⏳ 待进行 - -- [ ] TypeScript 类型检查 -- [ ] API 调用测试 -- [ ] 组件渲染测试 -- [ ] 用户交互测试 -- [ ] 国际化文本检查 - -### 集成测试 ⏳ 待进行 - -- [ ] 完整迁移流程测试 - - [ ] 从空配置启动 - - [ ] 从 v3.6.x 配置升级 - - [ ] 多服务器合并场景 - - [ ] 冲突处理验证 -- [ ] 多应用同步测试 - - [ ] 启用单个应用 - - [ ] 启用多个应用 - - [ ] 动态切换应用 - - [ ] 同步到 live 配置验证 -- [ ] 边界情况测试 - - [ ] 空服务器列表 - - [ ] 超长服务器名称 - - [ ] 特殊字符处理 - - [ ] 并发操作 - ---- - -## 📦 交付清单 - -### 代码文件 - -#### 后端(Rust)✅ 已完成 - -- [x] `src-tauri/src/app_config.rs` - 数据结构定义与迁移 -- [x] `src-tauri/src/services/mcp.rs` - 服务层重构 -- [x] `src-tauri/src/mcp.rs` - 同步函数实现 -- [x] `src-tauri/src/commands/mcp.rs` - Tauri 命令 -- [x] `src-tauri/src/lib.rs` - 命令注册 -- [x] `src-tauri/src/claude_mcp.rs` - Claude MCP 操作 -- [x] `src-tauri/src/gemini_mcp.rs` - Gemini MCP 操作 - -#### 前端(TypeScript/React)⚠️ 部分完成 - -- [x] `src/types.ts` - 类型定义更新 -- [x] `src/lib/api/mcp.ts` - API 层更新 -- [ ] `src/hooks/useMcp.ts` - React Query Hooks -- [ ] `src/components/mcp/UnifiedMcpPanel.tsx` - 统一面板组件 -- [ ] `src/components/mcp/McpServerTable.tsx` - 服务器表格 -- [ ] `src/components/mcp/McpServerFormModal.tsx` - 表单模态框 -- [ ] `src/components/mcp/McpImportDialog.tsx` - 导入对话框 -- [ ] `src/App.tsx` - 主界面集成 -- [ ] `src/locales/zh/translation.json` - 中文翻译 -- [ ] `src/locales/en/translation.json` - 英文翻译 - -### 文档 - -- [x] 本重构计划文档 (`docs/v3.7.0-unified-mcp-refactor.md`) -- [ ] 用户升级指南 (`docs/upgrade-to-v3.7.0.md`) -- [ ] API 变更说明 (`docs/api-changes-v3.7.0.md`) - -### Git 提交记录 ✅ - -- [x] `c7b235b` - feat(mcp): implement unified MCP management for v3.7.0 -- [x] `7ae2a9f` - fix(mcp): resolve compilation errors and add backward compatibility -- [x] `ac09551` - feat(frontend): add unified MCP types and API layer for v3.7.0 - ---- - -## 🎯 下一步行动 - -### 立即任务(优先级 P0) - -1. ⬜ **实现 useMcp Hook** - - 文件:`src/hooks/useMcp.ts` - - 估时:1-2 小时 - - 依赖:API 层(已完成) - -2. ⬜ **创建 UnifiedMcpPanel 核心组件** - - 文件:`src/components/mcp/UnifiedMcpPanel.tsx` - - 估时:3-4 小时 - - 依赖:useMcp Hook - -3. ⬜ **添加国际化文本** - - 文件:`src/locales/{zh,en}/translation.json` - - 估时:30 分钟 - -4. ⬜ **集成到主界面** - - 文件:`src/App.tsx` - - 估时:30 分钟 - - 依赖:UnifiedMcpPanel 组件 - -### 次要任务(优先级 P1) - -5. ⬜ **实现子组件** - - McpServerTable - - McpServerFormModal - - McpImportDialog - - 估时:4-6 小时 - -6. ⬜ **编写测试用例** - - 后端单元测试 - - 前端组件测试 - - 集成测试 - - 估时:6-8 小时 - -7. ⬜ **编写用户文档** - - 升级指南 - - API 变更说明 - - 估时:2-3 小时 - -### 优化任务(优先级 P2) - -8. ⬜ **性能优化** - - 服务器列表虚拟滚动 - - 批量操作优化 - - 估时:2-3 小时 - -9. ⬜ **用户体验增强** - - 添加加载状态 - - 添加错误提示 - - 添加操作确认 - - 估时:2-3 小时 - -10. ⬜ **代码清理** - - 移除旧的分应用面板组件 - - 清理废弃代码 - - 代码格式化 - - 估时:1-2 小时 - ---- - -## 💡 技术亮点 - -### 1. 平滑迁移机制 - -- ✅ 自动检测旧配置并迁移 -- ✅ 新旧结构并存(过渡期) -- ✅ 无需用户手动操作 -- ✅ 保留所有历史数据 - -### 2. 向后兼容 - -- ✅ 旧命令继续可用(带废弃警告) -- ✅ 前端可增量更新 -- ✅ 渐进式重构策略 - -### 3. 类型安全 - -- ✅ Rust 强类型保证数据完整性 -- ✅ TypeScript 类型定义与后端一致 -- ✅ serde 序列化/反序列化自动处理 - -### 4. 清晰的架构分层 - -``` -Frontend (React) - ↓ (Tauri IPC) -Commands Layer - ↓ -Services Layer - ↓ -Data Layer (Config + Live Sync) -``` - -### 5. SSOT 原则 - -- 单一配置源:`~/.cc-switch/config.json` -- 统一管理:`mcp.servers` 字段 -- 按需同步:写入各应用 live 配置 - ---- - -## 📚 参考资源 - -### 内部文档 - -- [项目 README](../README.md) -- [CLAUDE.md](../CLAUDE.md) - Claude Code 工作指南 -- [架构文档](../CLAUDE.md#架构概述) - -### 相关 Issues/PRs - -- 无(新功能开发) - -### 技术栈文档 - -- [Tauri 2.0](https://tauri.app/v1/guides/) -- [React 18](https://react.dev/) -- [TanStack Query](https://tanstack.com/query/latest) -- [shadcn/ui](https://ui.shadcn.com/) -- [serde](https://serde.rs/) - ---- - -## 📝 变更日志 - -### 2025-11-14 - -- ✅ 完成后端 Phase 1 & 2(数据结构、服务层、命令层) -- ✅ 修复所有编译错误 -- ✅ 完成前端类型定义和 API 层 -- ✅ 创建本重构计划文档 - -### 待更新... - ---- - -## 👥 团队协作 - -**开发者**:Claude Code (AI Assistant) + User - -**审查者**:User - -**测试者**:User - ---- - -## ⚠️ 风险与对策 - -### 风险 1:迁移数据丢失 - -**概率**:低 -**影响**:高 -**对策**: -- ✅ 迁移前自动备份配置 -- ✅ 详细日志记录 -- ✅ 测试各种边界情况 - -### 风险 2:性能问题(大量服务器) - -**概率**:中 -**影响**:中 -**对策**: -- ⬜ 实现虚拟滚动 -- ⬜ 分页或懒加载 -- ⬜ 性能测试 - -### 风险 3:兼容性问题 - -**概率**:中 -**影响**:中 -**对策**: -- ✅ 保留旧命令兼容层 -- ✅ 前端增量更新 -- ⬜ 多版本测试 - -### 风险 4:用户学习成本 - -**概率**:低 -**影响**:低 -**对策**: -- ⬜ 清晰的 UI 设计 -- ⬜ 详细的升级指南 -- ⬜ 操作提示和引导 - ---- - -## 🎉 预期收益 - -### 用户体验提升 - -- ⭐ **简化操作**:不再需要在不同应用面板切换 -- ⭐ **统一视图**:一目了然看到所有 MCP 配置 -- ⭐ **灵活配置**:轻松控制每个 MCP 应用到哪些客户端 - -### 代码质量提升 - -- ⭐ **架构优化**:统一数据源,消除冗余 -- ⭐ **维护性**:单一面板组件,代码更简洁 -- ⭐ **扩展性**:未来添加新应用(如 Cursor)更容易 - -### 性能提升 - -- ⭐ **减少重复加载**:统一管理减少配置文件读写 -- ⭐ **更快同步**:批量操作更高效 - ---- - -## 📞 联系方式 - -**问题反馈**:[GitHub Issues](https://github.com/jasonyoungyang/cc-switch/issues) - -**功能建议**:[GitHub Discussions](https://github.com/jasonyoungyang/cc-switch/discussions) - ---- - -**文档版本**:v1.0 -**最后更新**:2025-11-14 -**状态**:🟡 开发中(后端完成 ✅,前端进行中 ⚠️) diff --git a/src-tauri/src/deeplink/mod.rs b/src-tauri/src/deeplink/mod.rs index c2d849686..4bd3c8036 100644 --- a/src-tauri/src/deeplink/mod.rs +++ b/src-tauri/src/deeplink/mod.rs @@ -7,7 +7,6 @@ //! - Prompts //! - Skills //! -//! See docs/ccswitch-deeplink-design.md for detailed design. mod mcp; mod parser; From af68d4549be1112085b1697adefca8a3f53a3bbb Mon Sep 17 00:00:00 2001 From: YewFence <88962785+YewFence@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:14:50 +0800 Subject: [PATCH 16/34] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9C=80?= =?UTF-8?q?=E5=B0=8F=E5=8C=96=E5=88=B0=E6=89=98=E7=9B=98=E5=90=8E=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E8=BF=87=E4=B8=80=E6=AE=B5=E6=97=B6=E9=97=B4=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=80=80=E5=87=BA=E7=9A=84=E9=97=AE=E9=A2=98=20(#1245?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExitRequested 事件处理器无条件执行清理并调用 std::process::exit(0), 导致 api.prevent_exit() 被完全抵消。当隐藏窗口的 WebView 被 Windows 后台优化策略回收、窗口对象销毁后,Tauri 运行时检测到无存活窗口自动 触发 ExitRequested,应用随即退出。 通过 ExitRequested 的 code 字段区分两种场景: - code 为 None(运行时自动触发):仅 prevent_exit(),保持托盘后台运行 - code 为 Some(_)(用户主动 app.exit()):执行清理后退出 Closes #728 --- src-tauri/src/lib.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d38a12f7c..65739696d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1094,9 +1094,18 @@ pub fn run() { app.run(|app_handle, event| { // 处理退出请求(所有平台) - if let RunEvent::ExitRequested { api, .. } = &event { - log::info!("收到退出请求,开始清理..."); - // 阻止立即退出,执行清理 + if let RunEvent::ExitRequested { api, code, .. } = &event { + // code 为 None 表示运行时自动触发(如隐藏窗口的 WebView 被回收导致无存活窗口), + // 此时应仅阻止退出、保持托盘后台运行; + // code 为 Some(_) 表示用户主动调用 app.exit() 退出(如托盘菜单"退出"), + // 此时执行清理后退出。 + if code.is_none() { + log::info!("运行时触发退出请求(无存活窗口),阻止退出以保持托盘后台运行"); + api.prevent_exit(); + return; + } + + log::info!("收到用户主动退出请求 (code={code:?}),开始清理..."); api.prevent_exit(); let app_handle = app_handle.clone(); From b05234c6df7abf241f8feb062f786a146b9611fa Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Mar 2026 22:57:12 +0800 Subject: [PATCH 17/34] feat(providers): add Novita presets and icon across supported apps (#1192) --- src/config/claudeProviderPresets.ts | 19 +++++++++++++++ src/config/iconInference.ts | 1 + src/config/openclawProviderPresets.ts | 34 +++++++++++++++++++++++++++ src/config/opencodeProviderPresets.ts | 26 ++++++++++++++++++++ src/icons/extracted/index.ts | 1 + src/icons/extracted/metadata.ts | 7 ++++++ src/icons/extracted/novita.svg | 11 +++++++++ 7 files changed, 99 insertions(+) create mode 100644 src/icons/extracted/novita.svg diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 0fdf6698c..926ebaee5 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -551,6 +551,25 @@ export const providerPresets: ProviderPreset[] = [ icon: "openrouter", iconColor: "#6566F1", }, + { + name: "Novita AI", + websiteUrl: "https://novita.ai", + apiKeyUrl: "https://novita.ai", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://api.novita.ai/anthropic", + ANTHROPIC_AUTH_TOKEN: "", + ANTHROPIC_MODEL: "zai-org/glm-5", + ANTHROPIC_DEFAULT_HAIKU_MODEL: "zai-org/glm-5", + ANTHROPIC_DEFAULT_SONNET_MODEL: "zai-org/glm-5", + ANTHROPIC_DEFAULT_OPUS_MODEL: "zai-org/glm-5", + }, + }, + category: "aggregator", + endpointCandidates: ["https://api.novita.ai/anthropic"], + icon: "novita", + iconColor: "#000000", + }, { name: "Nvidia", websiteUrl: "https://build.nvidia.com", diff --git a/src/config/iconInference.ts b/src/config/iconInference.ts index b2f37ab5e..9b318c69f 100644 --- a/src/config/iconInference.ts +++ b/src/config/iconInference.ts @@ -25,6 +25,7 @@ const iconMappings = { cohere: { icon: "cohere", iconColor: "#39594D" }, perplexity: { icon: "perplexity", iconColor: "#20808D" }, huggingface: { icon: "huggingface", iconColor: "#FFD21E" }, + novita: { icon: "novita", iconColor: "#000000" }, // 云平台 aws: { icon: "aws", iconColor: "#FF9900" }, diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index dcadce9c8..a36f3ceb3 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -786,6 +786,40 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, }, }, + { + name: "Novita AI", + websiteUrl: "https://novita.ai", + apiKeyUrl: "https://novita.ai", + settingsConfig: { + baseUrl: "https://api.novita.ai/openai", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "zai-org/glm-5", + name: "GLM-5", + contextWindow: 202800, + cost: { input: 1, output: 3.2, cacheRead: 0.2 }, + }, + ], + }, + category: "aggregator", + icon: "novita", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "novita/zai-org/glm-5" }, + modelCatalog: { + "novita/zai-org/glm-5": { alias: "GLM-5" }, + }, + }, + }, { name: "Nvidia", websiteUrl: "https://build.nvidia.com", diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 2caff2528..db5dd4b44 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -959,6 +959,32 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, }, + { + name: "Novita AI", + websiteUrl: "https://novita.ai", + apiKeyUrl: "https://novita.ai", + settingsConfig: { + npm: "@ai-sdk/openai-compatible", + name: "Novita AI", + options: { + baseURL: "https://api.novita.ai/openai", + apiKey: "", + }, + models: { + "zai-org/glm-5": { name: "GLM-5" }, + }, + }, + category: "aggregator", + icon: "novita", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, { name: "Nvidia", websiteUrl: "https://build.nvidia.com", diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index 4f32ce5c3..1e2fba31b 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -63,6 +63,7 @@ export const icons: Record = { sssaicode: `SSAI Code1001 11010110 101110 110SSSSAiCode`, catcoder: `KwaiKAT`, mcp: `ModelContextProtocol`, + novita: `Novita`, nvidia: `Nvidia`, bailian: `BaiLian`, }; diff --git a/src/icons/extracted/metadata.ts b/src/icons/extracted/metadata.ts index 6b2c8a8a1..8debacd37 100644 --- a/src/icons/extracted/metadata.ts +++ b/src/icons/extracted/metadata.ts @@ -373,6 +373,13 @@ export const iconMetadata: Record = { keywords: ["xiaomimimo", "xiaomi", "mimo"], defaultColor: "#000000", }, + novita: { + name: "novita", + displayName: "Novita AI", + category: "ai-provider", + keywords: ["novita", "novita ai"], + defaultColor: "#000000", + }, nvidia: { name: "nvidia", displayName: "NVIDIA", diff --git a/src/icons/extracted/novita.svg b/src/icons/extracted/novita.svg new file mode 100644 index 000000000..fd7c30da8 --- /dev/null +++ b/src/icons/extracted/novita.svg @@ -0,0 +1,11 @@ + + Novita + + + + + + + + + From 2dcec4178d891437f450627fc8ad31c3e2e51316 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 10:22:26 +0800 Subject: [PATCH 18/34] feat: restore model health check (stream check) UI Re-enable the stream check feature that was hidden in v3.11.0. All backend code, database schema, and i18n keys were preserved; only the frontend UI needed uncommenting across 4 files. OpenCode and OpenClaw are excluded as the backend does not support them. --- src/components/providers/ProviderActions.tsx | 10 +++---- src/components/providers/ProviderList.tsx | 17 ++++++++++-- .../forms/ProviderAdvancedConfig.tsx | 18 +++++-------- src/components/settings/SettingsPage.tsx | 26 +++++++++++++++++-- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/components/providers/ProviderActions.tsx b/src/components/providers/ProviderActions.tsx index 90bd60991..fdead3c21 100644 --- a/src/components/providers/ProviderActions.tsx +++ b/src/components/providers/ProviderActions.tsx @@ -3,12 +3,12 @@ import { Check, Copy, Edit, - // Loader2, // Hidden: stream check feature disabled + Loader2, Minus, Play, Plus, Terminal, - // TestTube2, // Hidden: stream check feature disabled + TestTube2, Trash2, Zap, } from "lucide-react"; @@ -45,13 +45,13 @@ export function ProviderActions({ appId, isCurrent, isInConfig = false, - isTesting: _isTesting, // Hidden: stream check feature disabled + isTesting, isProxyTakeover = false, isOmo = false, onSwitch, onEdit, onDuplicate, - onTest: _onTest, // Hidden: stream check feature disabled + onTest, onConfigureUsage, onDelete, onRemoveFromConfig, @@ -246,7 +246,6 @@ export function ProviderActions({ - {/* Hidden: stream check feature disabled {onTest && (
- */} {/* 代理配置 */}
diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index bc3d2bca1..0253317c8 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -8,6 +8,7 @@ import { Cloud, ScrollText, HardDriveDownload, + FlaskConical, } from "lucide-react"; import { toast } from "sonner"; import { @@ -38,8 +39,7 @@ import { BackupListSection } from "@/components/settings/BackupListSection"; import { WebdavSyncSection } from "@/components/settings/WebdavSyncSection"; import { AboutSection } from "@/components/settings/AboutSection"; import { ProxyTabContent } from "@/components/settings/ProxyTabContent"; -// Hidden: stream check feature disabled -// import { ModelTestConfigPanel } from "@/components/usage/ModelTestConfigPanel"; +import { ModelTestConfigPanel } from "@/components/usage/ModelTestConfigPanel"; import { UsageDashboard } from "@/components/usage/UsageDashboard"; import { LogConfigPanel } from "@/components/settings/LogConfigPanel"; import { useSettings } from "@/hooks/useSettings"; @@ -384,6 +384,28 @@ export function SettingsPage({ + + +
+ +
+

+ {t("modelTest.title")} +

+

+ {t("modelTest.description")} +

+
+
+
+ + + +
+ Date: Tue, 3 Mar 2026 15:14:17 +0800 Subject: [PATCH 19/34] fix: support openai_chat api_format in stream check Stream Check always used Anthropic Messages API format, causing false failures for providers with api_format="openai_chat" (e.g. NVIDIA). Now detects api_format from provider meta/settings_config and uses the correct endpoint (/v1/chat/completions) and headers accordingly. --- src-tauri/src/services/stream_check.rs | 125 ++++++++++++++++--------- 1 file changed, 82 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/services/stream_check.rs b/src-tauri/src/services/stream_check.rs index b5bf88887..158f615bf 100644 --- a/src-tauri/src/services/stream_check.rs +++ b/src-tauri/src/services/stream_check.rs @@ -207,6 +207,7 @@ impl StreamCheckService { &model_to_test, test_prompt, request_timeout, + provider, ) .await } @@ -283,7 +284,9 @@ impl StreamCheckService { /// Claude 流式检查 /// - /// 严格按照 Claude CLI 真实请求格式构建请求 + /// 根据供应商的 api_format 选择请求格式: + /// - "anthropic" (默认): Anthropic Messages API (/v1/messages) + /// - "openai_chat": OpenAI Chat Completions API (/v1/chat/completions) async fn check_claude_stream( client: &Client, base_url: &str, @@ -291,15 +294,42 @@ impl StreamCheckService { model: &str, test_prompt: &str, timeout: std::time::Duration, + provider: &Provider, ) -> Result<(u16, String), AppError> { let base = base_url.trim_end_matches('/'); - // URL 必须包含 ?beta=true 参数(某些中转服务依赖此参数验证请求来源) - let url = if base.ends_with("/v1") { - format!("{base}/messages?beta=true") + + // Detect api_format: meta.api_format > settings_config.api_format > default "anthropic" + let api_format = provider + .meta + .as_ref() + .and_then(|m| m.api_format.as_deref()) + .or_else(|| { + provider + .settings_config + .get("api_format") + .and_then(|v| v.as_str()) + }) + .unwrap_or("anthropic"); + + let is_openai_chat = api_format == "openai_chat"; + + // URL: /v1/chat/completions for openai_chat, /v1/messages?beta=true for anthropic + let url = if is_openai_chat { + if base.ends_with("/v1") { + format!("{base}/chat/completions") + } else { + format!("{base}/v1/chat/completions") + } } else { - format!("{base}/v1/messages?beta=true") + // ?beta=true is required by some relay services to verify request origin + if base.ends_with("/v1") { + format!("{base}/messages?beta=true") + } else { + format!("{base}/v1/messages?beta=true") + } }; + // Body: identical structure for minimal test (both APIs accept messages array) let body = json!({ "model": model, "max_tokens": 1, @@ -307,49 +337,58 @@ impl StreamCheckService { "stream": true }); - // 获取本地系统信息 - let os_name = Self::get_os_name(); - let arch_name = Self::get_arch_name(); + let mut request_builder = client.post(&url); - // 根据 auth.strategy 构建认证 headers - let mut request_builder = client - .post(&url) - .header("authorization", format!("Bearer {}", auth.api_key)); + if is_openai_chat { + // OpenAI-compatible: Bearer auth + standard headers only + request_builder = request_builder + .header("authorization", format!("Bearer {}", auth.api_key)) + .header("content-type", "application/json") + .header("accept", "application/json"); + } else { + // Anthropic native: full Claude CLI headers + let os_name = Self::get_os_name(); + let arch_name = Self::get_arch_name(); - // 只有 Anthropic 官方策略才添加 x-api-key - if auth.strategy == AuthStrategy::Anthropic { - request_builder = request_builder.header("x-api-key", &auth.api_key); + request_builder = + request_builder.header("authorization", format!("Bearer {}", auth.api_key)); + + // Only Anthropic official strategy adds x-api-key + if auth.strategy == AuthStrategy::Anthropic { + request_builder = request_builder.header("x-api-key", &auth.api_key); + } + + request_builder = request_builder + // Anthropic required headers + .header("anthropic-version", "2023-06-01") + .header( + "anthropic-beta", + "claude-code-20250219,interleaved-thinking-2025-05-14", + ) + .header("anthropic-dangerous-direct-browser-access", "true") + // Content type headers + .header("content-type", "application/json") + .header("accept", "application/json") + .header("accept-encoding", "identity") + .header("accept-language", "*") + // Client identification headers + .header("user-agent", "claude-cli/2.1.2 (external, cli)") + .header("x-app", "cli") + // x-stainless SDK headers (dynamic local system info) + .header("x-stainless-lang", "js") + .header("x-stainless-package-version", "0.70.0") + .header("x-stainless-os", os_name) + .header("x-stainless-arch", arch_name) + .header("x-stainless-runtime", "node") + .header("x-stainless-runtime-version", "v22.20.0") + .header("x-stainless-retry-count", "0") + .header("x-stainless-timeout", "600") + // Other headers + .header("sec-fetch-mode", "cors") + .header("connection", "keep-alive"); } - // 严格按照 Claude CLI 请求格式设置其他 headers let response = request_builder - // Anthropic 必需 headers - .header("anthropic-version", "2023-06-01") - .header( - "anthropic-beta", - "claude-code-20250219,interleaved-thinking-2025-05-14", - ) - .header("anthropic-dangerous-direct-browser-access", "true") - // 内容类型 headers - .header("content-type", "application/json") - .header("accept", "application/json") - .header("accept-encoding", "identity") - .header("accept-language", "*") - // 客户端标识 headers - .header("user-agent", "claude-cli/2.1.2 (external, cli)") - .header("x-app", "cli") - // x-stainless SDK headers(动态获取本地系统信息) - .header("x-stainless-lang", "js") - .header("x-stainless-package-version", "0.70.0") - .header("x-stainless-os", os_name) - .header("x-stainless-arch", arch_name) - .header("x-stainless-runtime", "node") - .header("x-stainless-runtime-version", "v22.20.0") - .header("x-stainless-retry-count", "0") - .header("x-stainless-timeout", "600") - // 其他 headers - .header("sec-fetch-mode", "cors") - .header("connection", "keep-alive") .timeout(timeout) .json(&body) .send() From 07568286fc606a392444ba11a4777efc54dd70c3 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 21:49:57 +0800 Subject: [PATCH 20/34] feat: add first-run confirmation dialog for stream check Show an informational dialog when users first click the health check button, explaining its limitations (OAuth providers, relay services, Bedrock). The dialog persists the confirmation in settings so it only appears once per device. --- src-tauri/src/settings.rs | 4 ++ src/components/providers/ProviderList.tsx | 48 ++++++++++++++++++++++- src/i18n/locales/en.json | 5 +++ src/i18n/locales/ja.json | 5 +++ src/i18n/locales/zh.json | 5 +++ src/types.ts | 2 + 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 8ee07e60b..a972d5131 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -196,6 +196,9 @@ pub struct AppSettings { /// User has confirmed the usage query first-run notice #[serde(default, skip_serializing_if = "Option::is_none")] pub usage_confirmed: Option, + /// User has confirmed the stream check first-run notice + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stream_check_confirmed: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub language: Option, @@ -282,6 +285,7 @@ impl Default for AppSettings { enable_local_proxy: false, proxy_confirmed: None, usage_confirmed: None, + stream_check_confirmed: None, language: None, visible_apps: None, claude_config_dir: None, diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index b95576d8c..6b8c8f453 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -41,6 +41,8 @@ import { import { useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; +import { settingsApi } from "@/lib/api/settings"; interface ProviderListProps { providers: Record; @@ -176,14 +178,43 @@ export function ProviderList({ const [searchTerm, setSearchTerm] = useState(""); const [isSearchOpen, setIsSearchOpen] = useState(false); const searchInputRef = useRef(null); + const [showStreamCheckConfirm, setShowStreamCheckConfirm] = useState(false); + const [pendingTestProvider, setPendingTestProvider] = useState(null); + + // Query settings for streamCheckConfirmed flag + const { data: settings } = useQuery({ + queryKey: ["settings"], + queryFn: () => settingsApi.get(), + }); const handleTest = useCallback( (provider: Provider) => { - checkProvider(provider.id, provider.name); + if (!settings?.streamCheckConfirmed) { + setPendingTestProvider(provider); + setShowStreamCheckConfirm(true); + } else { + checkProvider(provider.id, provider.name); + } }, - [checkProvider], + [checkProvider, settings?.streamCheckConfirmed], ); + const handleStreamCheckConfirm = async () => { + setShowStreamCheckConfirm(false); + try { + if (settings) { + await settingsApi.save({ ...settings, streamCheckConfirmed: true }); + await queryClient.invalidateQueries({ queryKey: ["settings"] }); + } + } catch (error) { + console.error("Failed to save stream check confirmed:", error); + } + if (pendingTestProvider) { + checkProvider(pendingTestProvider.id, pendingTestProvider.name); + setPendingTestProvider(null); + } + }; + // Import current live config as default provider const queryClient = useQueryClient(); const importMutation = useMutation({ @@ -417,6 +448,19 @@ export function ProviderList({ ) : ( renderProviderList() )} + + void handleStreamCheckConfirm()} + onCancel={() => { + setShowStreamCheckConfirm(false); + setPendingTestProvider(null); + }} + />
); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index fccbaa456..0a540232e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -191,6 +191,11 @@ "title": "Configure Usage Query", "message": "Usage query requires a custom script or API parameters. Please make sure you have obtained the necessary information from your provider.\n\nIf unsure how to configure, please consult your provider's documentation first.", "confirm": "I understand, configure" + }, + "streamCheck": { + "title": "Model Health Check", + "message": "Health check tests provider connectivity by sending a direct API request. The following may cause check failures:\n\n• Official providers (uses OAuth login, no standalone API Key)\n• Some relay services (verify requests come from Claude Code CLI)\n• AWS Bedrock (uses IAM signature authentication)\n\nA failed check does not mean the provider is unusable — it only means it cannot be verified via a standalone request. Please refer to actual behavior within the application.", + "confirm": "I understand, proceed" } }, "settings": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index f6b5508d6..0e0fc72b1 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -191,6 +191,11 @@ "title": "使用量クエリの設定", "message": "使用量クエリにはカスタムスクリプトまたは API パラメータが必要です。プロバイダーから必要な情報を取得していることをご確認ください。\n\n設定方法が不明な場合は、プロバイダーのドキュメントを先にご確認ください。", "confirm": "理解しました、設定する" + }, + "streamCheck": { + "title": "モデルヘルスチェック", + "message": "ヘルスチェックは API リクエストを直接送信してプロバイダーの接続性をテストします。以下の場合、チェックが失敗する可能性があります:\n\n• 公式プロバイダー(OAuth ログイン使用、独立した API キーなし)\n• 一部の中継サービス(リクエストが Claude Code CLI からのものか検証)\n• AWS Bedrock(IAM 署名認証を使用)\n\nチェック失敗はプロバイダーが使用不能であることを意味しません。独立したリクエストでの検証ができないことを示すだけです。アプリ内の実際の動作を基準にしてください。", + "confirm": "理解しました、続行する" } }, "settings": { diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 0fe3901c3..f18782cd1 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -191,6 +191,11 @@ "title": "配置用量查询", "message": "用量查询需要配置专用的查询脚本或 API 参数,请确保您已从供应商处获取相关信息。\n\n如不确定如何配置,请先查阅供应商文档。", "confirm": "我已了解,继续配置" + }, + "streamCheck": { + "title": "模型健康检测", + "message": "健康检测通过直接发送 API 请求来测试供应商连通性,以下情况可能导致检测失败:\n\n• 官方供应商(使用 OAuth 登录,无独立 API Key)\n• 部分中转服务(会校验请求是否来自 Claude Code CLI)\n• AWS Bedrock(使用 IAM 签名认证)\n\n检测失败不代表供应商不可用,仅表示无法通过独立请求验证。请以应用内的实际情况为准。", + "confirm": "我已了解,继续检测" } }, "settings": { diff --git a/src/types.ts b/src/types.ts index eda850887..72d25e08f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -219,6 +219,8 @@ export interface Settings { proxyConfirmed?: boolean; // User has confirmed the usage query first-run notice usageConfirmed?: boolean; + // User has confirmed the stream check first-run notice + streamCheckConfirmed?: boolean; // 首选语言(可选,默认中文) language?: "en" | "zh" | "ja"; From b3dda16b3ad961ebdb32221889e7f20ff0d03047 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 21:58:19 +0800 Subject: [PATCH 21/34] fix: remove HTTP status code display from endpoint speed test The status code from a simple GET request reflects route matching, not actual endpoint availability. Users only need latency and reachability info, which the latency number already conveys. --- src/components/providers/forms/EndpointSpeedTest.tsx | 5 ----- src/i18n/locales/en.json | 3 +-- src/i18n/locales/ja.json | 3 +-- src/i18n/locales/zh.json | 3 +-- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx index 2ede2f019..2143d21db 100644 --- a/src/components/providers/forms/EndpointSpeedTest.tsx +++ b/src/components/providers/forms/EndpointSpeedTest.tsx @@ -607,11 +607,6 @@ const EndpointSpeedTest: React.FC = ({ > {latency}ms
-
- {entry.status - ? t("endpointTest.status", { code: entry.status }) - : t("endpointTest.notTested")} -
) : isTesting ? ( diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0a540232e..7f2564b46 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -720,8 +720,7 @@ "pleaseAddEndpoint": "Please add an endpoint first", "testUnavailable": "Speed test unavailable", "noResult": "No result returned", - "testFailed": "Speed test failed: {{error}}", - "status": "Status: {{code}}" + "testFailed": "Speed test failed: {{error}}" }, "providerAdvanced": { "testConfig": "Model Test Config", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0e0fc72b1..9935ccc86 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -720,8 +720,7 @@ "pleaseAddEndpoint": "まずエンドポイントを追加してください", "testUnavailable": "速度テストを実行できません", "noResult": "結果がありません", - "testFailed": "速度テストに失敗しました: {{error}}", - "status": "ステータス: {{code}}" + "testFailed": "速度テストに失敗しました: {{error}}" }, "providerAdvanced": { "testConfig": "モデルテスト設定", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f18782cd1..72453e770 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -720,8 +720,7 @@ "pleaseAddEndpoint": "请先添加端点", "testUnavailable": "测速功能不可用", "noResult": "未返回结果", - "testFailed": "测速失败: {{error}}", - "status": "状态码:{{code}}" + "testFailed": "测速失败: {{error}}" }, "providerAdvanced": { "testConfig": "模型测试配置", From 7d4ffa9872784b461d95234a73c7c7bd0f86a1d1 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Mar 2026 22:34:22 +0800 Subject: [PATCH 22/34] feat: add model role badges and reorder presets to prioritize Opus - Add Primary/Fallback badge to each model card in OpenClaw form - Update modelsHint to explain model ordering semantics - Reorder 11 aggregator/third-party presets to put Opus first --- .../providers/forms/OpenClawFormFields.tsx | 20 +- src/config/openclawProviderPresets.ts | 198 +++++++++--------- 2 files changed, 118 insertions(+), 100 deletions(-) diff --git a/src/components/providers/forms/OpenClawFormFields.tsx b/src/components/providers/forms/OpenClawFormFields.tsx index bb4a26770..87d1cd4b8 100644 --- a/src/components/providers/forms/OpenClawFormFields.tsx +++ b/src/components/providers/forms/OpenClawFormFields.tsx @@ -236,6 +236,24 @@ export function OpenClawFormFields({ key={modelKeys[index]} className="p-3 border border-border/50 rounded-lg space-y-3" > + {/* Role badge */} +
+ + {index === 0 + ? t("openclaw.primaryModel", { + defaultValue: "默认模型", + }) + : t("openclaw.fallbackModel", { + defaultValue: "回退模型", + })} + +
{/* Model ID and Name row */}
@@ -463,7 +481,7 @@ export function OpenClawFormFields({

{t("openclaw.modelsHint", { defaultValue: - "配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。", + "配置该供应商支持的模型。第一个模型为默认模型(Primary),其余为回退模型(Fallback)。拖拽或调整顺序可更改默认模型。", })}

diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index a36f3ceb3..19b405232 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -557,18 +557,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "aggregator", @@ -583,12 +583,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aihubmix/claude-sonnet-4-6", - fallbacks: ["aihubmix/claude-opus-4-6"], + primary: "aihubmix/claude-opus-4-6", + fallbacks: ["aihubmix/claude-sonnet-4-6"], }, modelCatalog: { - "aihubmix/claude-sonnet-4-6": { alias: "Sonnet" }, "aihubmix/claude-opus-4-6": { alias: "Opus" }, + "aihubmix/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -601,18 +601,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "aggregator", @@ -627,12 +627,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "dmxapi/claude-sonnet-4-6", - fallbacks: ["dmxapi/claude-opus-4-6"], + primary: "dmxapi/claude-opus-4-6", + fallbacks: ["dmxapi/claude-sonnet-4-6"], }, modelCatalog: { - "dmxapi/claude-sonnet-4-6": { alias: "Sonnet" }, "dmxapi/claude-opus-4-6": { alias: "Opus" }, + "dmxapi/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -645,18 +645,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "openai-completions", models: [ - { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "anthropic/claude-sonnet-4.6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "aggregator", @@ -671,12 +671,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "openrouter/anthropic/claude-sonnet-4.6", - fallbacks: ["openrouter/anthropic/claude-opus-4.6"], + primary: "openrouter/anthropic/claude-opus-4.6", + fallbacks: ["openrouter/anthropic/claude-sonnet-4.6"], }, modelCatalog: { - "openrouter/anthropic/claude-sonnet-4.6": { alias: "Sonnet" }, "openrouter/anthropic/claude-opus-4.6": { alias: "Opus" }, + "openrouter/anthropic/claude-sonnet-4.6": { alias: "Sonnet" }, }, }, }, @@ -863,18 +863,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -890,12 +890,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "packycode/claude-sonnet-4-6", - fallbacks: ["packycode/claude-opus-4-6"], + primary: "packycode/claude-opus-4-6", + fallbacks: ["packycode/claude-sonnet-4-6"], }, modelCatalog: { - "packycode/claude-sonnet-4-6": { alias: "Sonnet" }, "packycode/claude-opus-4-6": { alias: "Opus" }, + "packycode/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -908,18 +908,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -936,12 +936,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "cubence/claude-sonnet-4-6", - fallbacks: ["cubence/claude-opus-4-6"], + primary: "cubence/claude-opus-4-6", + fallbacks: ["cubence/claude-sonnet-4-6"], }, modelCatalog: { - "cubence/claude-sonnet-4-6": { alias: "Sonnet" }, "cubence/claude-opus-4-6": { alias: "Opus" }, + "cubence/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -954,18 +954,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -982,12 +982,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aigocode/claude-sonnet-4-6", - fallbacks: ["aigocode/claude-opus-4-6"], + primary: "aigocode/claude-opus-4-6", + fallbacks: ["aigocode/claude-sonnet-4-6"], }, modelCatalog: { - "aigocode/claude-sonnet-4-6": { alias: "Sonnet" }, "aigocode/claude-opus-4-6": { alias: "Opus" }, + "aigocode/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -1000,18 +1000,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -1028,12 +1028,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "rightcode/claude-sonnet-4-6", - fallbacks: ["rightcode/claude-opus-4-6"], + primary: "rightcode/claude-opus-4-6", + fallbacks: ["rightcode/claude-sonnet-4-6"], }, modelCatalog: { - "rightcode/claude-sonnet-4-6": { alias: "Sonnet" }, "rightcode/claude-opus-4-6": { alias: "Opus" }, + "rightcode/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -1046,18 +1046,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -1074,12 +1074,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aicodemirror/claude-sonnet-4-6", - fallbacks: ["aicodemirror/claude-opus-4-6"], + primary: "aicodemirror/claude-opus-4-6", + fallbacks: ["aicodemirror/claude-sonnet-4-6"], }, modelCatalog: { - "aicodemirror/claude-sonnet-4-6": { alias: "Sonnet" }, "aicodemirror/claude-opus-4-6": { alias: "Opus" }, + "aicodemirror/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -1092,18 +1092,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -1120,12 +1120,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "aicoding/claude-sonnet-4-6", - fallbacks: ["aicoding/claude-opus-4-6"], + primary: "aicoding/claude-opus-4-6", + fallbacks: ["aicoding/claude-sonnet-4-6"], }, modelCatalog: { - "aicoding/claude-sonnet-4-6": { alias: "Sonnet" }, "aicoding/claude-opus-4-6": { alias: "Opus" }, + "aicoding/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -1138,18 +1138,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -1166,12 +1166,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "crazyrouter/claude-sonnet-4-6", - fallbacks: ["crazyrouter/claude-opus-4-6"], + primary: "crazyrouter/claude-opus-4-6", + fallbacks: ["crazyrouter/claude-sonnet-4-6"], }, modelCatalog: { - "crazyrouter/claude-sonnet-4-6": { alias: "Sonnet" }, "crazyrouter/claude-opus-4-6": { alias: "Opus" }, + "crazyrouter/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, @@ -1184,18 +1184,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic-messages", models: [ - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - contextWindow: 200000, - cost: { input: 3, output: 15 }, - }, { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 200000, cost: { input: 5, output: 25 }, }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, ], }, category: "third_party", @@ -1212,12 +1212,12 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, suggestedDefaults: { model: { - primary: "sssaicode/claude-sonnet-4-6", - fallbacks: ["sssaicode/claude-opus-4-6"], + primary: "sssaicode/claude-opus-4-6", + fallbacks: ["sssaicode/claude-sonnet-4-6"], }, modelCatalog: { - "sssaicode/claude-sonnet-4-6": { alias: "Sonnet" }, "sssaicode/claude-opus-4-6": { alias: "Opus" }, + "sssaicode/claude-sonnet-4-6": { alias: "Sonnet" }, }, }, }, From 4e0f9d955216c095d0b336345289b2ecebaa6dd2 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 4 Mar 2026 15:48:00 +0800 Subject: [PATCH 23/34] feat: replace text inputs with dropdown selects for OpenClaw agent model config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add useOpenClawModelOptions hook to aggregate models from all configured OpenClaw providers - Replace read-only primary model display with a searchable Select dropdown - Replace comma-separated fallback text input with add/remove Select rows - Filter out already-selected models from fallback options - Show "(not configured)" marker for values whose provider has been deleted - Unify terminology: rename "主模型/Primary Model" to "默认模型/Default Model" --- .../openclaw/AgentsDefaultsPanel.tsx | 207 +++++++++++++++--- .../openclaw/hooks/useOpenClawModelOptions.ts | 62 ++++++ src/i18n/locales/en.json | 9 +- src/i18n/locales/ja.json | 9 +- src/i18n/locales/zh.json | 9 +- 5 files changed, 259 insertions(+), 37 deletions(-) create mode 100644 src/components/openclaw/hooks/useOpenClawModelOptions.ts diff --git a/src/components/openclaw/AgentsDefaultsPanel.tsx b/src/components/openclaw/AgentsDefaultsPanel.tsx index c4a9abf18..e8611f078 100644 --- a/src/components/openclaw/AgentsDefaultsPanel.tsx +++ b/src/components/openclaw/AgentsDefaultsPanel.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Save } from "lucide-react"; +import { Save, Plus, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useOpenClawAgentsDefaults, @@ -10,14 +10,28 @@ import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import type { OpenClawAgentsDefaults } from "@/types"; +import { useOpenClawModelOptions } from "./hooks/useOpenClawModelOptions"; + +const UNSET_SENTINEL = "__unset__"; const AgentsDefaultsPanel: React.FC = () => { const { t } = useTranslation(); const { data: agentsData, isLoading } = useOpenClawAgentsDefaults(); const saveAgentsMutation = useSaveOpenClawAgentsDefaults(); + const { options: modelOptions, isLoading: modelsLoading } = + useOpenClawModelOptions(); + const [defaults, setDefaults] = useState(null); - const [fallbacks, setFallbacks] = useState(""); + const [primaryModel, setPrimaryModel] = useState(""); + const [fallbacks, setFallbacks] = useState([]); // Extra known fields from agents.defaults const [workspace, setWorkspace] = useState(""); @@ -25,16 +39,14 @@ const AgentsDefaultsPanel: React.FC = () => { const [contextTokens, setContextTokens] = useState(""); const [maxConcurrent, setMaxConcurrent] = useState(""); - // Primary model is read-only — set via the "Set as default model" button on provider cards - const primaryModel = agentsData?.model?.primary ?? ""; - useEffect(() => { // agentsData is undefined while loading, null when config section is absent if (agentsData === undefined) return; setDefaults(agentsData); if (agentsData) { - setFallbacks((agentsData.model?.fallbacks ?? []).join(", ")); + setPrimaryModel(agentsData.model?.primary ?? ""); + setFallbacks(agentsData.model?.fallbacks ?? []); // Extract known extra fields setWorkspace(String(agentsData.workspace ?? "")); @@ -44,25 +56,82 @@ const AgentsDefaultsPanel: React.FC = () => { } }, [agentsData]); + // Build primary options, including a "not in list" entry if current value is missing + const primaryOptions = useMemo(() => { + const result = [...modelOptions]; + if ( + primaryModel && + !modelOptions.some((opt) => opt.value === primaryModel) + ) { + result.unshift({ + value: primaryModel, + label: t("openclaw.agents.notInList", { + value: primaryModel, + defaultValue: "{{value}} (not configured)", + }), + }); + } + return result; + }, [modelOptions, primaryModel, t]); + + // For each fallback row, compute available options (exclude primary + other fallbacks) + const getFallbackOptions = (currentIndex: number) => { + const usedValues = new Set(); + if (primaryModel) usedValues.add(primaryModel); + fallbacks.forEach((fb, idx) => { + if (idx !== currentIndex && fb) usedValues.add(fb); + }); + + const filtered = modelOptions.filter((opt) => !usedValues.has(opt.value)); + + // If current fallback value is not in modelOptions, add a "not in list" entry + const currentValue = fallbacks[currentIndex]; + if ( + currentValue && + !modelOptions.some((opt) => opt.value === currentValue) + ) { + filtered.unshift({ + value: currentValue, + label: t("openclaw.agents.notInList", { + value: currentValue, + defaultValue: "{{value}} (not configured)", + }), + }); + } + + return filtered; + }; + + const handleAddFallback = () => { + setFallbacks((prev) => [...prev, ""]); + }; + + const handleRemoveFallback = (index: number) => { + setFallbacks((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleFallbackChange = (index: number, value: string) => { + setFallbacks((prev) => { + const next = [...prev]; + next[index] = value; + return next; + }); + }; + const handleSave = async () => { try { // Preserve all unknown fields from original data const updated: OpenClawAgentsDefaults = { ...defaults }; - // Model configuration — primary is read-only, preserve original value - const fallbackList = fallbacks - .split(",") - .map((s) => s.trim()) - .filter(Boolean); + // Model configuration + const fallbackList = fallbacks.filter(Boolean); - const origPrimary = defaults?.model?.primary; - if (origPrimary) { + if (primaryModel) { updated.model = { - primary: origPrimary, + primary: primaryModel, ...(fallbackList.length > 0 ? { fallbacks: fallbackList } : {}), }; } else if (fallbackList.length > 0) { - // No primary set but user provided fallbacks — keep fallbacks only updated.model = { primary: "", fallbacks: fallbackList }; } @@ -110,6 +179,8 @@ const AgentsDefaultsPanel: React.FC = () => { ); } + const noModels = modelOptions.length === 0 && !modelsLoading; + return (

@@ -123,31 +194,111 @@ const AgentsDefaultsPanel: React.FC = () => {

+ {/* Primary Model */}
-
- {primaryModel || t("openclaw.agents.notSet")} -
+ {noModels ? ( +

+ {t("openclaw.agents.noModels", { + defaultValue: + "No configured provider models. Please add an OpenClaw provider first.", + })} +

+ ) : ( + + )}

{t("openclaw.agents.primaryModelHint")}

+ {/* Fallback Models */}
- setFallbacks(e.target.value)} - placeholder="provider/model-a, provider/model-b" - className="font-mono text-xs" - /> -

- {t("openclaw.agents.fallbackModelsHint")} -

+ + {fallbacks.length === 0 && !noModels && ( +

+ {t("openclaw.agents.fallbackModelsHint")} +

+ )} + +
+ {fallbacks.map((fb, index) => { + const opts = getFallbackOptions(index); + return ( +
+ + +
+ ); + })} +
+ + {!noModels && ( + + )}
diff --git a/src/components/openclaw/hooks/useOpenClawModelOptions.ts b/src/components/openclaw/hooks/useOpenClawModelOptions.ts new file mode 100644 index 000000000..974162b64 --- /dev/null +++ b/src/components/openclaw/hooks/useOpenClawModelOptions.ts @@ -0,0 +1,62 @@ +import { useMemo } from "react"; +import { useProvidersQuery } from "@/lib/query/queries"; +import type { OpenClawProviderConfig } from "@/types"; + +export interface ModelOption { + value: string; // "providerId/modelId" + label: string; // "Provider Name / Model Name" +} + +export function useOpenClawModelOptions(): { + options: ModelOption[]; + isLoading: boolean; +} { + const { data: providersData, isLoading } = useProvidersQuery("openclaw"); + + const options = useMemo(() => { + const allProviders = providersData?.providers; + if (!allProviders) return []; + + const dedupedOptions = new Map(); + + for (const [providerKey, provider] of Object.entries(allProviders)) { + let config: OpenClawProviderConfig; + try { + config = + typeof provider.settingsConfig === "string" + ? (JSON.parse(provider.settingsConfig) as OpenClawProviderConfig) + : (provider.settingsConfig as OpenClawProviderConfig); + } catch { + continue; + } + + const models = config.models; + if (!Array.isArray(models)) continue; + + const providerDisplayName = + typeof provider.name === "string" && provider.name.trim() + ? provider.name + : providerKey; + + for (const model of models) { + if (!model.id) continue; + const value = `${providerKey}/${model.id}`; + const modelDisplayName = + typeof model.name === "string" && model.name.trim() + ? model.name + : model.id; + const label = `${providerDisplayName} / ${modelDisplayName}`; + + if (!dedupedOptions.has(value)) { + dedupedOptions.set(value, label); + } + } + } + + return Array.from(dedupedOptions.entries()) + .map(([value, label]) => ({ value, label })) + .sort((a, b) => a.label.localeCompare(b.label, "zh-CN")); + }, [providersData?.providers]); + + return { options, isLoading }; +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7f2564b46..f725f2cdf 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1292,11 +1292,14 @@ "title": "Agents Config", "description": "Manage agents.defaults in openclaw.json (default model, runtime parameters, etc.)", "modelSection": "Model Configuration", - "primaryModel": "Primary Model", - "primaryModelHint": "Set via the \"Set as Default Model\" button in the provider list", + "primaryModel": "Default Model", + "primaryModelHint": "Select from models of configured providers", "notSet": "Not set", "fallbackModels": "Fallback Models", - "fallbackModelsHint": "Comma-separated, ordered by priority", + "fallbackModelsHint": "When the primary model is unavailable, fallbacks are tried in order", + "addFallback": "Add fallback model", + "noModels": "No configured provider models. Please add an OpenClaw provider first.", + "notInList": "{{value}} (not configured)", "runtimeSection": "Runtime Parameters", "workspace": "Workspace Path", "timeout": "Timeout (seconds)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 9935ccc86..0440fb4aa 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1292,11 +1292,14 @@ "title": "Agents 設定", "description": "openclaw.json の agents.defaults 設定を管理(デフォルトモデル、ランタイムパラメータなど)", "modelSection": "モデル設定", - "primaryModel": "プライマリモデル", - "primaryModelHint": "プロバイダ一覧の「デフォルトモデルに設定」ボタンで設定します", + "primaryModel": "デフォルトモデル", + "primaryModelHint": "設定済みプロバイダのモデルから選択してください", "notSet": "未設定", "fallbackModels": "フォールバックモデル", - "fallbackModelsHint": "カンマ区切り、優先度順", + "fallbackModelsHint": "プライマリモデルが利用不可の場合、優先度順にフォールバックが試行されます", + "addFallback": "フォールバックモデルを追加", + "noModels": "設定済みのプロバイダモデルがありません。先に OpenClaw プロバイダを追加してください。", + "notInList": "{{value}} (未設定)", "runtimeSection": "ランタイムパラメータ", "workspace": "ワークスペースパス", "timeout": "タイムアウト(秒)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 72453e770..ad99f1dba 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1292,11 +1292,14 @@ "title": "Agents 配置", "description": "管理 openclaw.json 中的 agents.defaults 配置(默认模型、运行参数等)", "modelSection": "模型配置", - "primaryModel": "主模型", - "primaryModelHint": "在供应商列表中通过「设为默认模型」按钮设置", + "primaryModel": "默认模型", + "primaryModelHint": "从已配置供应商的模型中选择默认模型", "notSet": "未设置", "fallbackModels": "回退模型", - "fallbackModelsHint": "逗号分隔,按优先级排列", + "fallbackModelsHint": "当主模型不可用时,按优先级依次尝试以下回退模型", + "addFallback": "添加回退模型", + "noModels": "暂无已配置的供应商模型。请先添加 OpenClaw 供应商。", + "notInList": "{{value}} (供应商未配置)", "runtimeSection": "运行参数", "workspace": "工作区路径", "timeout": "超时时间(秒)", From d35c2962b9142290da1d66b46f886c32dc21f6d8 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 4 Mar 2026 15:49:06 +0800 Subject: [PATCH 24/34] style: format ProviderList.tsx --- src/components/providers/ProviderList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 6b8c8f453..bcc7550cf 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -179,7 +179,8 @@ export function ProviderList({ const [isSearchOpen, setIsSearchOpen] = useState(false); const searchInputRef = useRef(null); const [showStreamCheckConfirm, setShowStreamCheckConfirm] = useState(false); - const [pendingTestProvider, setPendingTestProvider] = useState(null); + const [pendingTestProvider, setPendingTestProvider] = + useState(null); // Query settings for streamCheckConfirmed flag const { data: settings } = useQuery({ From 083e48bf8cd9e05a323bf0ebbc78eb61d68b33ea Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 4 Mar 2026 22:52:21 +0800 Subject: [PATCH 25/34] fix: use structural TOML merge/subset for Codex common config snippet The text-based approach (string append + substring matching) failed to detect already-merged snippets when config.toml was reformatted by external tools (MCP sync, Codex CLI). Replace with smol-toml parse/ stringify + existing deepMerge/isSubset/deepRemove for correct structural operations. Falls back to text matching on parse failure. --- src/utils/providerConfigUtils.ts | 83 ++++++++++++-------------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index 059140888..fc4b46844 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -1,7 +1,8 @@ // 供应商配置处理工具函数 import type { TemplateValueConfig } from "../config/claudeProviderPresets"; -import { normalizeQuotes } from "@/utils/textNormalization"; +import { normalizeQuotes, normalizeTomlText } from "@/utils/textNormalization"; +import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; const isPlainObject = (value: unknown): value is Record => { return Object.prototype.toString.call(value) === "[object Object]"; @@ -359,76 +360,52 @@ export interface UpdateTomlCommonConfigResult { error?: string; } -// 保存之前的通用配置片段,用于替换操作 -let previousCommonSnippet = ""; - -// 将通用配置片段写入/移除 TOML 配置 +// Write/remove common config snippet to/from TOML config (structural merge) export const updateTomlCommonConfigSnippet = ( tomlString: string, snippetString: string, enabled: boolean, ): UpdateTomlCommonConfigResult => { if (!snippetString.trim()) { - // 如果片段为空,直接返回原始配置 - return { - updatedConfig: tomlString, - }; + return { updatedConfig: tomlString }; } - if (enabled) { - // 添加通用配置 - // 先移除旧的通用配置(如果有) - let updatedConfig = tomlString; - if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) { - updatedConfig = tomlString.replace(previousCommonSnippet, ""); + try { + const config = parseToml(normalizeTomlText(tomlString || "")); + const snippet = parseToml(normalizeTomlText(snippetString)); + + if (enabled) { + const merged = deepMerge( + deepClone(config) as Record, + deepClone(snippet) as Record, + ); + return { updatedConfig: stringifyToml(merged) }; + } else { + const result = deepClone(config) as Record; + deepRemove(result, snippet as Record); + return { updatedConfig: stringifyToml(result) }; } - - // 在文件末尾添加新的通用配置 - // 确保有适当的换行 - const needsNewline = updatedConfig && !updatedConfig.endsWith("\n"); - updatedConfig = - updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString; - - // 保存当前通用配置片段 - previousCommonSnippet = snippetString; - - return { - updatedConfig: updatedConfig.trim() + "\n", - }; - } else { - // 移除通用配置 - if (tomlString.includes(snippetString)) { - const updatedConfig = tomlString.replace(snippetString, ""); - // 清理多余的空行 - const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim(); - - // 清空保存的状态 - previousCommonSnippet = ""; - - return { - updatedConfig: cleaned ? cleaned + "\n" : "", - }; - } - return { - updatedConfig: tomlString, - }; + } catch (e) { + return { updatedConfig: tomlString, error: String(e) }; } }; -// 检查 TOML 配置是否已包含通用配置片段 +// Check if TOML config already contains the common config snippet (structural subset check) export const hasTomlCommonConfigSnippet = ( tomlString: string, snippetString: string, ): boolean => { if (!snippetString.trim()) return false; - // 简单检查配置是否包含片段内容 - // 去除空白字符后比较,避免格式差异影响 - const normalizeWhitespace = (str: string) => str.replace(/\s+/g, " ").trim(); - - return normalizeWhitespace(tomlString).includes( - normalizeWhitespace(snippetString), - ); + try { + const config = parseToml(normalizeTomlText(tomlString || "")); + const snippet = parseToml(normalizeTomlText(snippetString)); + return isSubset(config, snippet); + } catch { + // Fallback to text-based matching if TOML parsing fails + const norm = (s: string) => s.replace(/\s+/g, " ").trim(); + return norm(tomlString).includes(norm(snippetString)); + } }; // ========== Codex base_url utils ========== From 3d33f299a8aa90bc92fc2a4bfad8ba2e42754530 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 4 Mar 2026 23:23:27 +0800 Subject: [PATCH 26/34] docs: add UCloud CompShare as sponsor partner --- README.md | 5 +++++ README_JA.md | 5 +++++ README_ZH.md | 5 +++++ assets/partners/logos/ucloud.png | Bin 0 -> 31657 bytes 4 files changed, 15 insertions(+) create mode 100644 assets/partners/logos/ucloud.png diff --git a/README.md b/README.md index 4ea7f9022..7e96b8c00 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric Thanks to DMXAPI for sponsoring this project! DMXAPI provides global large model API services to 200+ enterprise users. One API key for all global models. Features include: instant invoicing, unlimited concurrency, starting from $0.15, 24/7 technical support. GPT/Claude/Gemini all at 32% off, domestic models 20-50% off, Claude Code exclusive models at 66% off! Register here + +Ucloud +Thanks to UCloud CompShare for sponsoring this project! CompShare is UCloud's AI cloud platform, providing stable and comprehensive domestic and international model APIs with just one key. Featuring cost-effective monthly and pay-as-you-go Coding Plan packages at 60-80% off official prices. Supports Claude Code, Codex, and API access. Enterprise-grade high concurrency, 24/7 technical support, and self-service invoicing. Users who register via this link will receive a free 5 CNY platform trial credit! + + RightCode Thank you to Right Code for sponsoring this project! Right Code reliably provides routing services for models such as Claude Code, Codex, and Gemini. It features a highly cost-effective Codex monthly subscription plan and supports quota rollovers—unused quota from one day can be carried over and used the next day. Invoices are available upon top-up. Enterprise and team users can receive dedicated one-on-one support. Right Code also offers an exclusive discount for CC Switch users: register via this link, and with every top-up you will receive pay-as-you-go credit equivalent to 25% of the amount paid. diff --git a/README_JA.md b/README_JA.md index e6647844f..44a00cf7f 100644 --- a/README_JA.md +++ b/README_JA.md @@ -55,6 +55,11 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / DMXAPI のご支援に感謝します!DMXAPI は 200 社以上の企業ユーザーにグローバル大規模モデル API サービスを提供しています。1 つの API キーで全世界のモデルにアクセス可能。即時請求書発行、同時接続数無制限、最低 $0.15 から、24 時間年中無休のテクニカルサポート。GPT/Claude/Gemini が全て 32% オフ、国内モデルは 20〜50% オフ、Claude Code 専用モデルは 66% オフ実施中!登録はこちら + +Ucloud +UCloud CompShare のご支援に感謝します!CompShare は UCloud 傘下の AI クラウドプラットフォームで、国内外の安定した包括的なモデル API を 1 つのキーだけで利用可能。月額・従量課金のコストパフォーマンスに優れた Coding Plan パッケージを提供し、公式価格の 60〜80% オフで利用できます。Claude Code、Codex および API アクセスに対応。エンタープライズ級の高同時接続、24 時間年中無休のテクニカルサポート、セルフサービス請求書発行に対応。こちらのリンクから登録すると、無料で 5 元分のプラットフォーム体験クレジットがもらえます! + + RightCode 本プロジェクトへのご支援として、Right Code にご協賛いただき誠にありがとうございます。Right Code は、Claude Code、Codex、Gemini などのモデルに対応した中継(プロキシ)サービスを安定して提供しています。特に高いコストパフォーマンスを誇る Codex の月額プランを主力としており、未使用分の利用枠を翌日に繰り越して利用できる(繰越対応)点が特長です。チャージ(入金)後に請求書の発行が可能で、企業・チーム向けには専任担当による個別対応も行っています。さらに CC Switch ユーザー向けの特別優待として、こちらのリンクからご登録いただくと、チャージのたびに実支払額の 25% 相当の従量課金クレジットが付与されます。 diff --git a/README_ZH.md b/README_ZH.md index f7c3647b2..8c84e8814 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -56,6 +56,11 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更 为200多家企业用户提供全球大模型API服务。· 充值即开票 ·当天开票 ·并发不限制 ·1元起充 · 7x24 在线技术辅导,GPT/Claude/Gemini全部6.8折,国内模型5~8折,Claude Code 专属模型3.4折进行中!点击这里注册 + +Ucloud +感谢优云智算赞助了本项目!优云智算是UCloud旗下AI云平台,提供稳定、全面的国内外模型API,仅一个key即可调用。主打包月、按量的高性价比 Coding Plan 套餐,基于官方2~5折优惠。支持接入 Claude Code、Codex 及 API 调用。支持企业高并发、7*24技术支持、自助开票。通过此链接注册的用户,可得免费5元平台体验金! + + RightCode 感谢 Right Code 赞助了本项目!Right Code 稳定提供 Claude Code、Codex、Gemini 等模型的中转服务。主打极高性价比的Codex包月套餐,提供额度转结,套餐当天用不完的额度,第二天还能接着用!充值即可开票,企业、团队用户一对一对接。同时为 CC Switch 的用户提供了特别优惠:通过此链接注册,每次充值均可获得实付金额25%的按量额度! diff --git a/assets/partners/logos/ucloud.png b/assets/partners/logos/ucloud.png new file mode 100644 index 0000000000000000000000000000000000000000..9db7346b2695c9d05d3d599462c0c3c6630674a0 GIT binary patch literal 31657 zcmXuKby$?&^FOQzN+T_Zgdkngy;9O4jl_b|E!|7EAl)q8(v37oH`3kRwXp0y_xtmF ze}4dbUBKS6bIzGFub6OEWf>eya?EGXp5e&JN~t}2_CgbQy@!qheD`oWas+;0ILPWc zJ$uF~|KHbhH&17!XV0jg$w`T8xMv)AN zmZ}?b7Z*O+mxZQ2XiXwV>RxEG9-B!Z+q zV4$MO$$mnYqkdj9_n1*TzF>ZGqh}4RzgyTdfyS@xg{PGJtT71gwRUt2=SF=DFk$t& ztDYkGsuNa89YBp9K;5#vnK`72OJzoNYSbScj0R8Co*3>RCl_>VS|daie~l{c{0Yf% zwSPprQ3 zVhSUgfslPNXINV46%tNWI`5mw`haj}6ZnRj$`x$rG%z)N{c%kIR6*m4^<>ua$(F*(b3~;O9H0rDh#nVFqesd zogL@DO3v37{}!$)7;dE#A>%=vrjIIa98)X`nB+(AzmBOIM4$Ix^opIKeJqW)iWI*4A-R-1I zd2OV^&5Jz-bg>_?25{X=M7+wvYFprRC5}>N#AhSd8sC*doJ#wdKXt7n%VOVWojEi7 zLffx&tcQK~T+nIoSoP1>yU~o^hzE=%wb=eZ9V&j6s)*6f{_VX6vDIeoYd{r-`d{W3E5v#|lDvMT>CBC;o&C5jj_W zvXhf2ZA<2Qh7`#A?BkyHHYMCk1-E1LyM&77RKp)=A%WZ}|H`axMmuCE!RUXzKi?e8 zPfy>m{Os(H(w&~phz_KFz!egMqGph0`N&-rDE6f1e3JCpnjKy0tP1K=VGjsI1U&hN z`f-~N6pj9+;>Z2m2UR?|*oFG_!fS2uMnM0j&fhPtsmwn4w_Pi2)sA9EmrT$6LCiY^eg~;84c~sdmQ9+0H!F!U0_>M5 zP&1FnS(0$O?MmDFEmSm_8eR>Y0~ty$S=0AsBzN9NUTGe(x^gRzm1e!B&78iyMUyHpzpe@fYKpI;PZ}e$dm5JSTloTbtA5U9vnWtb3Dz6yr{*gRZ&W11vwz z#`@7d{pSOR55-d)FJJX3;Yh@kL^FK~1f^=DwyL>yF>n{^L$gpD>hr*qE*Pi`RUQ|7 zWfP4W!{)NYv!*&?J~R}5IU@3o-1T@?_jgtC>!`j|;YzccQZNI&+~8_x@pzw_JEIR= zm~D?}_F0w>N$k+6SZt|pJ_$D0K0t0T^Z@JPd%LXQwEIt_$wB;FbNx`W{sHnMRJcX$ zVGqMTxe!*gW=TUocsF_a$=(0BAG9O3M1XQ0c5om3U8_m)gKpE?#9NQv8o!2@S<>N znak)+diz>RGgV>^9cANnI_#!Zl0@-B)Q3Y{(i>*lknr(jVWbNlTgA}&R1Ax4+RH-a z^Z*#7WM@QoYRbyS*L;EqGDPrGYL_bG1czNSF~)tLc78?4@CiA82qv?A>}vnjSyiaj z*zZr~9z32`VKKQMX-#q-@bRq>S6$`ZHc1;@7IogVoIs@9efn!u{ZxphWTMnZy3O%? z;J0~r9JaQ&rRnq-`>y-RPtk_h@WkHQC87H!rn5Q({ z<4Q$`Ie|cr<@zrEXZpMf#EOU4U-G@AxV6)gJy^HQ!w9@caeaIERaZzj5Q~2IXG6`P;(PbLYA8v(VXkmuELn=<$mAjIHQnUrw>? zW>8u;ytYiV__xLf^6KBxS1L8Xb7Qidn5uP@C}0=&KQ1qakJpeZ0?z0ejB$ojYgYw* zXMdp0z)=RvPpME`Co)n z)1S`4yqRmA*R;Q*C?BuE?jl8UjtDG@Yx6$Se*#_CI*!7^@TT`;FQxLdUzdD;F^z?aEREcyFH3 z8w?)c@m&yqO8qA4uy9xnDA%>^Rh4mH+G2fL>{>uKd=FBsfAh)l2)Y7Q{muq43tzfZ zBUOJAtCmg7Z7X!O?Z$}T9Z5g2RkoUeP+FdG()~1eKc81e^kB{`-FN4ql?IVW&}z~6 zBG1Z;Oi<{`$x$|qq7eSoEvII$zjZgRv6SS)OR8JPuorR$zG@DzuW-sSdt&fRxP(~}!YT0>0nD`sXiZp{w?#+>w_m#$aBl1-)kq$o`)vp z9z1S_KWh=RLGXrKUhM}JZdG;Fl#cm}pDpEo@@uRDoPr)4WV9uH@OiRV7vPe*ULm64jtg#hbNnW4sasvEZ*9A0O zCea*@R^JtRJo|ecWZ7_bSF|fs)ODHO8}*ARSPJb*r9Gz`uAq~ zgGo)Hn;E7{Bq9P1U!yY*q6;_U>FR~0PO&{ufhhm@w~{X7{*|i9Cgq9qu?V?0<|b{l zFay)U28tFcya%{HeobE@2$5nV0aeTH1-YyS&ssF<|6`S_C{N42(h>e{_6_0*oLNe< zw=z#fSl{!j!t_0e-wG?w&g&LQNls=+zgBbDvs2*dJUyvx>&h$}U)H4RLijqK*G@1f zIrpMpBfbCrTi(FCH{dJ-#iWKBa;JSsWKF@v9poZEB)oHDWn)n`kyQzs-`AiXgK=$CkZwm zE`1sVWdi8met*f$iRkg%r)3232Ww~^%+#6>PO`UHn-{l9R*~aVIcVN~QBA2__^JX; z82hfJWymEXiIOJB4&Z>0kkzbSAIa`6@PcBy-Q*->kRjb_F;!af5ZMN_scDC$b>%u8 zkD9t3auPrP&3SR<{GEz{uezLh`*djSy_@P$3@Jwb`4un8-WFw`IeLlJ%lKQ${(y;D zK|?NCUV$fn7h2V9f#`Bl9~M{2^vsXvJHb6!QNZ}X^ME!^bP?@aSPN4;@bdf2U9%1& zl$}P=S1)@D5&xx;CrA^@VcB$GiI`iO#`;T#or9cHT`LBi?Zh$EzwaR7x76{&;7u}f znhogo*2Wn9@955}4gy)4g}5`EQ8@M`9`EDVR3>H#Dm2xJ^x4?E*1vkJ6-c(A<3T#h z^CSpq(ra$ypk;dIrc(;Z#<+!c6wSa%Pf6u4wQGIu<7{v(ZC79Yh39)NCbZmvl@4fL z_ktT{Ee}AM+rNYMw~?9*wmep5WVo6LsILq$_p86ZFS9;6JzxdH##9!^5*r>gnSgG% zVLIK#pxZFfuI$qUQX%O}-r!teZDfComWJ||ufRoChG5lN2(>~IT;Z{69?c-sx7VLP z{2)9L!@q;S2gjauO}@5oN;~PHBaUacP(~!d8D0mNptgzn2=(1>gpPV_HTNLV0b6loTC-D_|cb%UCLK??|@s6y6t~KxAux zU}#<9*-;%3j;Lf3Jb@kjmiysB79^1T?)y+|&8rRoL>{QzY&1W1Z8A{ai2PFpwvK$@ z8=!nsPQ0#fYx8-Cp(sXr(=vqW>y9dCnzV{xW{Mo-7ZW5{0N2=(+6n;<^O^^N4{D5$ z+}%mRix;_ZyYUBeZ^0DkM|4iE0$;v}x9wOU7J|}j zoI(k3N?;6NW_7PW%A&qel&9GS_$=}T@NZ0hKZIY)y=;tKBg%oQi_``CqsRUF#H;q> zFTuw&OOFGO2lBXW&=ZP)W=}EzV%gFg>-icBgc1ca6Mi!NE-y{_Xx(Do7Wu&OA&P8x zsK090%=6sO1oO3K_{+&-+G1J8It9gW7=sC+gtppO{i!u7-1}480->Rp7&O|!Y^eMQ z?P#a9t)0SWc`JHkYJiJuw`7b+`-J40xXGKi@CTKvjCc(*wu3{{lO*8ruR7eIyPwam zyP0Oj+Y+iA-_wSBe}W%3Cj-UBV?upg8|b9IhZmKG+rZsto6;#C)pGxC1301@fM!X| z@;p3`=t3We2pn&*1*fF(ypZ0P?`#WlCW@+xCF z@`mtlnDIQF_9?pKKMgI>#GRcFoX89X9@Kh<#JB($gFz`+9=`MC;m7ylf>fzqa9wF~ z+Ix;mX<~2+T8aojlAN80Vu>LNTfCfJzq!OH`K2h%og}na?qLwC4ysU76ZKnIs(JE0A{6AyKB%^-Y%k zN#QER4Y=m~kj?IqEZ5&0S52O|Y~E$=En098<4l%hct{9EK9KcOeFOa|-!MDmuhS(3 z=LD$I?Ls0|M2Cq^TPYt|q(mvQ=Ittz`0H2KEU6(BjeS&up{6csL7}BEs2-5%1g6iG zXEr*Ga%(73PkZka{#B>+FogJhKk)mL&;5ohR;Hr;Z*hg0pkTNH55b3o+{n)ClJ@)9+^Yo~{%T3^#I7ihad?O%goI$J7C7nr zNof#K(d`M+>&U>~oKkIgtTp028$wzn-ee>f=&d+Ek7Pf4Gfb(9q)9XRg*1nGq@l6M zA&4zL|K)xUs{~l9lyU9+Gh`0J=_ii?o|cUi@tggPZug(EvamN=YC~yvK|+hR3@D?Z zA0>Ng-w2d$jx6xJd~t_V5}bvG@HDfvAiV^Y8#x0vW`Rg-XfM<#szbZ!RcM6-2;Zh|81|?hHmMk$A5HeIN z{EZLsT!hGfL&!bW&L=I7g7B#o@qEM=Zzt7@t`n%%PIMaroOyeRDGNOVW%IaoE0J6u z7%<2jyQj@P9%jG~J;W8)U=@`$11vkJaOF$ZA3w*|>@Po@r?G$kTt~WpFgaDzK35b?LD61DD z=S-v!*}yuex4tj~SLsI%PZWM>^X*8@LpVKG%-LFM9f$;BPu_kHDM4fUuA%A} zBF_FNY}r9_9*ZaTA(+H_oU~+dYv*7vetT=B4k5YdGa}6ZGMxHS6>bG&qD=eWLtNGS zpU+L*uJ(Z50U#vXCxMQ%cF*v#p>a0pDUuR#F-27&`!{gz$L^FC3WrZOHo4GyCdv5E zJZmFGR^uoE7hnUUiy$f7p+gayO#N>SRM+R_O4t(YWaEFMXIR`kKYb^*BLS(~lLGTG#!-;C)L;7Z577F_ z9V-Q-j39Xpi}Ot8YN zmN@)`F^pU6v#)y4?pEP-pm^<^-Ag5g@%Rt|XnHmHqm?a7YMG?jW68rnR7b+kw<>Am z#B%=QihIz>R%~PD6l0e2YzdG+jz# z#kf(QH*QvDbFSG&H359uc$Sx?^~z%;RDYYI<=-`uuY3vhFPwpv@?RWB6Q!2XQzcgU zExQ@DPEnhJx0S$)eG#)G+MXrfQZcTCxIy)K&3`r~hBNRjmG-CCuyxTEFaCqGWTF!x z#W@zR(#mb$Ebq%>Wy?(a*j% zfc58Nn?5_=lCtqv=LZ>hW&YR*E;q&VK^l0jf07r1iT7~O=_&2g{zRGZ2l})RAn&hj z`ck!hnP(vpy9wD-n=!1zjG?Iiiz_X)&QtRw22$6Zc%RN)88W`%<*sI1WbyqvZ=neXP8dyWK1bW_@1BfuTV8)3*QHJMlx&mHk* zqw|}%efDtG<}dPJPm$XQ>Nw+YIqKI|$^3I4E)y(1jsE9owKw=33c!y-;CaMAOUCiW zze^5(Fk=^zlClwCbkK3P%5CWDQB}>v3SIRGeS(wztuV;GSec+9f&n>G%>D5kESWXG zM})?=jd%~C&IzFV6CLbicv$f0P1i|625In`lcW zv>+xu?W~*ZEBtLS-Y)hB>Zy*L_KPgxj=ctAS%5JM1&zp1LM(R1kqbT8-`~#KVkT$x zB3w1ZH)v?o)p(t!lxyB<K1Zxs4^!l6;~1p8__m z(p(JlaoF%N-T9Cv-Gs;muTlH7PS*8v@#Im&Bjf1M&D4ND`;Yj?`%aRmNvMVJZm#wH zoyE74%mD&w>XjV>uAS`y5@3ap{S1_u!j<%@yq{C!N^`F4V4dA)&(B2JO%L$2o1R?c z2PTd>Drjx4%v{f@xv{HQ-_zr|#UZ^@l`>@QVDxS4C{+@R4oji-vh$y%<-(3`U}A_l^xMpPPp^hUpb}1&C1SQ zEY?{ww|jDYc+N>r9yiTL&*zU+cpqZ0DqcQz;FB0e&N zGt(5PaYl*~H#{Df&_ZN#&I4XL^26ZkG&uk=bBNrsaNwYZF4ixsvpou={w zPJ?oj5{nNhO*D(A-)8F!ttxNtRV7#P6!y)4pSqQ|XRYW`_6cqNJk`oJC*@zz@#g-hT4*UiB##p%VA&K(M9rvJ0gSjy>Y%rooUy;sU+V|!~ z3F76wRXoYFx3ED|&@q>sz}r26#+Y|r7* zI%Z*S9+G!iJ44Cwh`P8qu&QZe#oK(s=Xu&}^aInvhK@A4Fv&S^!FO!>9`hv6W2axn zKj)IL<|#Qd<)mtf>>*{YH7O7$;pENlZqU$?e*MT1(hmcJPCwo{t zKx!)AQDEo)9*Y$fsrp1i{p-z%|~eNw)ihLYR2mn;{>@SH}&T=l(3 zEnk@L-Lk)=4LB*XI+M6>UN;Fe>r*ThhAkuweO2to`UOjjMYjX>k%ElV2{f+Wjjk14 zv3U+caJ2~GL4hU~m*7PQVbN=YyD^!IJz?a1Z(e&Ch|rSto(k$t6angev5<)@3S5fD zJUB;+A%ae9IF<3{UvAMuT`2_YUO6rSf)D?i36-6B<9LyJVZN$c#&JBFv4792yiQyf zN{pq}PlAjTD>?Vtb_ZSmOhmsqOSA=;gr?IL5*AYRGO@Czd>kerKN4_$Q~sf=|K484 ztH{hCpZUIwtQ1h4{FV-K6ow<>_P4l|D2Z8as?nnf!7MhJVSv|VLQPD_@rvg6Vi5DTGP%zwNr16M0>n7S?Fx7>fL zn#W5cAB2vgm5vqNvX|0?(KP=-V!3*Bj!676KLI#C%0Yfg=w2(JB@26t#xT?X74UM zF{4)3YCXFg{+dqIe-2&0AQ)B)N&tyW8w1i1wFPSY zr6+u-6eAL#;ZXu5}tIlJp)Bgf*1^0Fp>rd2EgVOH3#$HS}f;F|PBZymf6^ z)fxR^MPxuMlUF4Z^DfNek?l5lEk{#xP(IG*ao{gVy8Wu44eOWZ)BuORj4QRg>6B6~ zJU=cvjHOB8@&(fI$Ui--iQ&m-x52`#SnC7Qau*4R)PW!AC}0L{hjSIDJ(~<9#@lnm z0FhgncE3DDCB`-{J!RR|Fk|brv7o6-`^frL@3yhwc&*A#9UwUY8tk?FYSfEXM^B5g zQ@tnqIsDsQ`YoVtr*a$VV*5DBGK*#STl>O3c$^B}RwAt^L=T$^XnlOP7a*pE*&k6M zW2LJ*SpMIc*%q-DU-wbjtlONZGTtOohLJ)xkY!k2#+vqjVBD<7nh;%%pdQi=<$FeD zH=;>trz{Mrbddc&1w>hS+WPwH0S~gwF||lU(cvG%l%jgiK;^5UAq7KESaa*?&zBy+ zb?OUry1|?O5w!nxN(&4KcXC(0|Il4dn)$$gn-Xu_U+f;jpc1Dd#(N7H17cI4hrZ9p znq2cxz|w7*DQ-?(<%90F^1m~L4mPrB+A39{vmp*|+KKI_O8aQ- z3Fu^XverO+@dxB7;s9gheZ5i8;SCcE4Q*M^yRy?&O+f<8U8lo@rKR0;s`R4mg?-J# z*$7!pA%tf1lzQ+CEZ9{U@&A&g7Gu;+M?y=k<93fni zBvDNF{aYI)f-rab6F`pak%FiGM{NPj)DfXvSt&Eu$F(ohi_S}M4yGr!b*%5> zl&+T=BRdUY&pL6?Yf!_ieZt)2tb(~vfSY{K8qHI5&Swihvez9q%j#oQs4 z$P9hFWmgj{J||@o`92teQ(W`h=&pf~{p_#+ECwj$5kxsG)H_bnr2L=KA z4yN6a%S3aUHNi66I3BO(X0)NN@Z!vUhv=2xufbnty4E+Q?}lHW-7-e{*`FBk9@wc4SGM`NKnGFSu+5}r|fkiQq{H;}k(4DJK#z5RH^xlZq~CndZnYXdSI z^!KaOxD}lD?F82`fpTIL^-<(;Iuht%U3tA)k{J6~X$waGi>;DJcSxcZM)d$80s05Z z7EuQKhp|lUJdgiyu~?Hs*Ln4`xsG=HC>*S>dW&4z$b&z6X8TLg(e($vF(-l%OG{?n zx6OYV6|8vm|L8>_(=Ovxx*u($H%4m%2v*|96MXaO3g@=mW1spGI?Yj0!nGNmk-6I!b8=FioS)>h7?3 zpo+Z;*p(EHYYqgX)#^Qqi6T_{$s@ku>qF%c`qDI)`(Z-yFGkZSOP?99*I~=cW-U!$ zpp)ElcoZ>hL2|YrF-+_i?P^OZM8TK0lixbEPdOZY-)i`lC|$<$kxL=R8F={7$-Hz21@)ytyb2>qG#>IO-(Vn7D|A#TdX-NNDXOUZ zK#9u|B8$S20m`w{qyM&j;~rUUVKQI{GsXaHOV?DgT;!un;uBXd&GJz|6`vD13S5)qn`RkoD21{f5*RmRi2mTX2@JO9hf4o6bp zhE1NN3Q|)gQ{SiF5%V}Z2S#9SrlnVje=eRLeCH zMrkTHUl@^@GNLT)FX@nQC}Wkc7is8=>Ukla-3@u3)NlG$ncc)j#dDTuL%kGIbTz7Maprd!7uheT2nA;KuX4Yb zz)?w{EkC6h0=8BJSh}r?^Ev}g%!B<>#if_V!zj@geJ6jgFufi-dD-2B3mz-$Qmu4P zu;@(E%lbAEg9VP#Stbe}%q-MtM`T2{UhN7ubHk>7>!mVN51%1|81bW`Zh z=@HU1`K4Ki)J_d=i&Z+$>`zCM&Ou%jEiib!1R#pa-7tJ$_n&Y|k(f^pHB~xd^*=g}y^1~;aGwctd6G=RI71+UN+<*L)vk?A`#%P@b zv%W|FbGE9gjX{(AlpqVJ+VzdBQ21peoG7l+NSI=iY;?Pdu8n7OC0>}YQ_NuYMxMCruZh}CNXoAyD?IgyWto1DAl-pze%?%ReSmI#j{ zU9&p{3%gkk>Te_2A*FUYlzw(C7bh|PWs4|wV{eMtu8sHxH{Km zU_aiSTp~u0yS8-ZATvkO=8?;dKl&Td<(MlTx|o!EB1|_8-$Xm^AZZYw^Q?)VRK@GlIuRyrQ&J z+prPqFRWmy+fXy_V4=2Whyz5mtl#_L;ml%?2Qr&+75F+=_pmrVIDXQ&(8R*l?s371 zqj}LEtnNc61z~?JF#wo@hG%uZ@e;n_Qed{!sxz@ObpwQHz)TvjfeHwDw_8-d9SlQX zUk3J5Ubanm0V#K>sSwpq&~wZ?iKLbP=KQgB9R-lavJTyv-{NuCqDfIR&;BWoGZ4+Z z*dbM0*qd1Gvm^)epBQA|(IwkSiJg|jLrr*#;q*#$cgt_d`Ei$JdI=wtEJWvm0K2eN zQnr>#W;tvjJ3g3O#B1dC_L>b~BaG(%(*UT79%RE1`qaA1=%1&N!{yfF?GYs4iH!XZ z#KEgV5r48d--*eG>F8R=1_WkgI0(2O3-miU$m>@nb?7E>KhbL$#^I(h`mnV_Oz43z^BxZA5h3BcmBg;Ovzn3btz2p6S+vA zN^P<&tM-HJNK6^mO@QTtAX6qfT`K()hH;dJzI?l9GYz_!b^|oo(!2RbFIpeQVX(z{VGLKZPa~p3+dt*IY+n> z3W?=EWY%83=pK`Ug+DTEKO7yvmkx@*tZTjZ%}%Y^YKGOeL!25LU`1rY!>ng?9wN?< zz+LGfyPGrt#rn_63OLdW>sUsAWvAkAxX$iI88vC{Sc_HSYLt(GiG1=n~-K85uh06VP9~g7;pIR1cZuG74C?4l%0|$Lt z^VRAu(~zo2{!>BGMkJB8lfQ*4uEZ~++=H8k3MBb8y{ih|#(kyka;Jor@;n+R2JSn1#^m57-{T>buco=@ zq+356!oT+$)OSzCE6$Z&!bdg$`wM(&|AybFlYb|l=M&(M9!+t{^o_!M0uAJbCbyg@ zChgXNoUn1XXi)Mh9*&;qGq0S|Fl;n^t)28Db~z?5JOL)c2l;!#TbZ4e_55i9 zU|0h(M(e`~l)YcmV9E_Nqiy7Oa>Pv0Mjzxe{c{krlJ7x3JBDKo1e1PVR?4A?%&yCp zitjTvZlH3qJ3}d;41+V{5<#jr0jXP+hZ zfFVS+GeRcM)O`LvNw}MR;9F?@p+kXPhKq{4!hYX(i7zl&S+7@*$IpbF(w69&Z`rvZ zlXhxxI^|mQ2YyvfYQCI<@d$#)|(UXy+!|?CbBDEIQpIB%)r~2tG zImoz^qNr4i)TrqSECVjs>i7>p(l1|H7*aBAtL<%}Xs3iKt*n0FPgX>J9s8&5G$eme z9JycoWuuZQUUDrXliQ9$X?``h_d33gaNA>xF zRCRf`u!#$~{8e}!KAsEs=NX2xUiBygV+eSfi%`F^IRgm7f^Gn{AKosmH%58VmfPNk4l; zoCW_0n;neN4h2j_t!FtP-}gK}a<|E26LQZan|$T~HVAO5y1osYk#ini7A(95=ov2n zQ`cmsJ73t04tszGh3wO{<$)Wi71EhOyHN z84!S}p>^(Y$$@*f)+7Kd!3`-HLl;ihZ7rX1&~Q#> zJN}Z6N7`g&U?gJ=bmM~iP*e9X-f)LRlf_TZ_ulfnEmBdSD@<9Ev0({Y@C7LAz7bHI za~u@b(;L2BiC_+zT+NZ54s}?fMgV8c|Dzz~NJn{w!0-!7D>1<6L-{j4*E1x0q z;qS2pYFN%^x>kSu5dDedpuer1&r-j!;LztrlR149_DQ1(T+T}x3G6u6xw+Q4 z-Ac_E>Rj=j9i5z9(fifp;VaHcRkz`f?2h*|zwdX;uNB_?E76v_!C)yRVIwcij42aE zP00RJnZg`9An$qH1=>3SuO#QX>1avkUfHDd;iIYh)2w zz2^$WaK;aF1~O;Op;=xP{VWn(0lf9rH=s+=6i5mZFaRF7X5mQ;0IZ^i!eedP=B>lV zR~Fmsf`U<7ly7k+;?z2WF%m&n9Gs-BGsOdHo|BUtV}BtjS@LQ>v^jquxe^^i7($c; zrt2|Hp8T=pD27Pk^|isq4r@(Z^!?$%hV2N0F!uoHqqKBD^e*Ucw9=zJ#b#LHuQ>WE zd7wh*0ZeGk2ZBGTkhD=`{`0V7qXRssD@*ffiyHzM^xD9F#kL%?!DwqkzprucDps?b zSRV;>=cy`6eyxP$V8NQNzIPL#{zpbydJMsn1$tIj3jl@=QSijmQXFz~-te0-HzLJMn> zdgVH|f0U)#PoK2AbmZ3`K($TR)#UAOZtXa}(F*x4uL}RNXUr6SPl3zMLh1sjeiMfw z5MVelN%ZYmGwP^+wldsE$Ft04uud<5~d^@ZINAbz2`Zbb)-*Pgms+(#uD`5J{ zayTW*J3cwN35pe7=kpOVGR~lo9HI(GBN>&z9F57H-R1L%-B^H6+aR=cG|>P4A-v$V zp2KtTR0nk1w8gp3a|e?Fa3laXR*Py~%nm^Bw0%>YO&0-0QXT%`>P^P$;W4*P+wYYo z=+X)lI)9cmCtPuJImLLxd?e>pJtxrJFf({!Ga>#`JteYDyX|e)qx=$3Z=1r-g$V85 zeU5UAhRW}F&!_K)UT!?n(v!i16OyN4jlu0=EE+mSXSfyzU;1w+d88sEC=#jeh%sY3 zp7bIr z?V7Z9#!U(}iK@Mi|21-1#k;0hBZgQ4occ)Rl#%Za63&MBjX&GsXp%X^gM-@u4Ps+h zD(u^GR=t@ox2)_BjJF1gVoNHID&giQr89X~oR-Vdtt}P<$Cl;v6}RX#e^8?anTD?- zGx4#4(lXU}6NK}H}ddS=jRNT|vU{7McC6&X~ycUu+t zsu?^6&OVrUq?q#ftPji;fY=XQ8^BpS-XlN(JuSDlDYjo$yA!Ky6dCb5=FmH8z&XJ{ z4Q*eIPhCR)%-zVb%O-?p+fI=bl82Pm$v?Vj71) z1?<*THMJZE-CYk&ns|Bzj&f_}h{D?1OtfFDG;(Y@1aHd3KEwQ!QdPm+8qL3IB0jId z=|~4#oZHuKkE``iXj0Esg+KpFtahaM@>P#hYu_n;EHg-cu#Yre&tCe1)Y%yYRcJg6 z05lJ#RKSSMpmWZ2ly0jPs}0|KJ4-ujTg#>9?~R9By0z%^E_M5TIH4FFzkS#UZ=k!9 z+6^J{wev_P76hfNDQ=lrw58C|zLRZ^%EXAYg%k>w?@kD`GzPm-VpR}meDh>1wR1tD z!=xfUX*>{l(DLU|vT>>VDvktCx`qVxve}1Np|576S2Wd6*xko&)Y3%6u^x>fVz8jL z3z-*-zSUl`uvn+@542a^{4xV^L{{RbEm*5ubPg7Oa|miS0oDJ^(~YA^G?m>s4Y3Ar zHNv>>l9Fc*nrx|j z&fYDq;Z=0v8>TPH`)?JA2S?T%ut_F>OJtu1dEb;>ySweT-C^s|aMm(`#3FpUg7eD= z>wGp!{WvHUI4{%jEe^wn!jl841+1;+4i$=lJBx1Zh6g0}=&8bYY%>L_4$@G`Jf5p~ z-2u!(s{cIzjfPe@)0@%A<$_PFzwv#4)g$>ysOQ2MsWKI&Gk#7e(KZ%BMFgP_d`ZU5 zM{#PiB%^}&dLW0xVynVoh`J*vbtq(^EQ}T;G*IhK+PUAcoIvSqzy@)tzKnDq4C{Fg z-+DOOkA9cm0jhx89QfUaOG;E<8j9cQuB~`;M|5C+0^#ngdo@SE2_%x}GS70!YHu^0 z>4%Dofhs8@ou0sNv?e7rNUwo0LtaD_vOs6%~))^n&yiGP{~#7m4wYMv8)z8!cP_mlR`*kXdIOV`>urjA-5|;205h`VdB6 zf!2T}PBW;&H6-m0qxxM&(o%i?L#N}RgU;mAq{T;=&;}%?X#X>ak%^sNhPD=%{0LmZ z`g1A2w-kzs0>1pEfAju7`(2ciHEY1@#x$p;iG|fL+g1rl zJ)$Lejs1#r6vcJA^JR~$pGC2~{$>f8m&2vjG%SQ75_wrq9&|pW*ndu_FIs$(A)Q0) z!_G*Ts9Ao+DSC|tYx-E{^hy+MC}`yy)1Uc;B{q?q;@PhZvqpVJW-(IlnX6{F9qEWq z6OA4h4AIFes0E$As-bYXvUZWJa-odM6;nK@HXTY+us^yldo6_aslps^-wW7|WNKe6 zEzXHtLvT?5iw+PiW+bN42T|V3E_Gea=PNrkmjpfswS~9!BU6wxpUWD8J;gZeZX5K& zoT3MSxo`i2Lr(`}yl=DP6bDp%zAh{A1^sdO%}X-gw6+J3U@LiCZ8?$uV$<6JSipr- z*bS^UAy*SULwU#C5#bT%S6PfExL1??ivmQ?geWeTy5iuTsQi_(+Or9|EX`ot3Wx{c7_?ff?Cf(ug&axWo&63BA$a~rqBvUp^Pec#*CDV@@t(%s$N2spHKH^>l@(xFlk zQi3$n-Q5k+9U{%Z!2B+s@4MFf=d5+_oPE#EyU(+qyWlf4XtSbG@UKhBR4(kbRa6oa z{4q5vU#mZ|K06hBg8&*vN4DK%u6aXIDJ(Z_=a0i`;_M!DGlDqfuM6#A=V#6@XB23Z znXlms^CtnATS>0R^vlrNZY%K^O7DA;GDdbdWga#*&KInU=|gYHEIB#f?;K(9SARBk zf2Ny%Db|0T>uz#63_9;mFlWHM&EdYqzV-IbE-xp*>(AE!F9G~Fu_U$_`mivN>u(Aj z?ZYKdUp$t?Tq0~=yO z->*r@+-xBW)vs-bwAGIENp>1C3;>cUci~(fkHhDxYW!%9W`r7ZLeUU7H6H=7gMe_5 zQe*yn)akKZ^d++VR~eV688H!&)KM2wyL5>AEHrjO;Mi|QO2l=J3P zrsWBP8SSwK6(3+3{S_{ll1D;%!4HwS)aa=U9Vo`W_O_3a#(M+{#8pU0DBNb+n-dzf zAF#t4Kw-3mG8(yPlk986+$mhoS?OIQ-2hUol9J;Iz`wrC_GB3DK;oCk0Hd7o7oymo zCM zY!CS{`&J5>y%)`473pzXOjL_p(5@!Xo6jo~*`A1yUCI2RuWsE2t2#tf%KQL!>+GE@ zPF~m<%8#Xmqa0!UzjS|APb|x9=u!PJaGEk#);5N!`T>HafNln6tjE6b zQ?L*F?YB&WO$1#}?9MTR{j@!AGo2WAG9|0t>u1GX@;*X~V3WF6+;rp3d^9};gxF&a zzT|AaTp=j7@mFj{czpscOV_bNE?To!yx+JeH%d(KFnIG6Bif{lb{$(e0v)^NrvIov zpLE70^sWt)T=Woh-|CKQ`IbqVVma|6$%gjWT+m|vi}7xS&HD+4j68%w zRXuJ(QYBr~K$Tm}!Ys*F@bT8^e(-97u0+EE_7zhK+`%ZJpk<`@FUt`0UtQB!kss{a zvs>4%$*z*jpetOn4awyMZ8N^s&IJZ>%gSPp%d5U|dj>RU7^dSw_veZ0PGjMh(&?PL z)KWHitro6-&c45%+JaOD9+Z?!eoixg08c|ZKdope67<+|1V!e4gE5_EEDd(Y9OLKi z%G#pwJQ80+KY5^E?Jubmc1wJ;xO2H36wKh?LczSplRoa2vE>4}HAUYG`5&`bEwGDx zz!C{Hh9__Xpg`Z64S?DPI7_DU(87yv2gYw%KHgRUa9ZGDOaW?vg%;=UqCMYoqJTzn z28-WuJ&Uaz;jd`wA-DgJ9(=)p{4>J>6wtOiEH9fCU$(p|c?JRN-2Bk-Gnad}YQagB zAI9u;$Vqpqi*g`hjtoxwyBzEt=k8e`wWHn=MSldg^})H_pJuX*)yY#_!sq8dQ6nq;j4rSL+vRbz&M5db3>Pwd=B z3$Z{%qwV!qNsM!O4!R5bH+vdeVj$8P29T(@0aMdaZR7b9^cj^YY(X@OPVScQ{JC|^ zX=K5-TAU`PX1!Cbtd|7ukQ7RaDrW^ z-(x#VBJZ+W?E>S99t`DWmSD)&eC;1yKdEKo_qHJNdwsEPrCwWMKtRp8i9gCVAeZKe z07eq~7g=Rl*f@=WHz-!RY9Pw?YZkNq>3@6n<{9)5crj%)7$0dCt`#c1cVK>xmjC`^ zuO_P?DGdywQ|g~M{&V|;L?-`G>7I??S&-?{m6-rCLL)4^joCJQW{-B$7CtByKl|3mK89k!7lHJs3u^lzI-9kA{W8WNA6$H+8`)={; z^G*Zv6UW8{XIEW*K#r6a6af;TB!ZchwFY2TfbFrl{?~o-ATe?oGKi^N^y!bdXA%0C z98cw+_T}M!2q_Vu&_;wC_h|UfG$7Rg_-WERIICf>MLrAt7R2I}Kx4HaZ0OgKxKr6g zCDbqK1q8DkjA)I+qafgHecsXgxB2V3TX?Ut?8~BgFK$!xHJ8_=hc2LVjVH# z)8#kq4Xzl|b-8}TrRy=+h)$lqsHXcyc7dfR=WCO*^^N=5#Mkc{CL%hV!hc6>kBo^eafo7tg7S?rs`CcO?~b~(kE zxt=*uB>c#@7Kb*J_t|1sx&~(Ie_L}CC@Ula|KmBTjW&g^r@*{{x5EAuU=#e^n*3A1 zUes+--K~}C8dI%o;HnQ?*GX)*r*E4Yo;ur{H@0M_q#d3XAi|D zw+Kof-h(l6=BX&YToeBB#AG+C5$FTIFeF(^Hc!#&kOs??62A!qx*TbdJmVI z6}rVx7GuQ^4R4(UvZUj(o31{1!xi#zd~azn($w?|gXov4oC;9sbxh$rLo79GIwq$GfU3oCd66-MTY&tbflT%Syh zD}OC1eXP7!jM2ng?VxMxA1D=m>29Qp6e+JwPc&xjzSMXU2Wezbf;;`oSk>vGdXtK5 z(ZAMFcp|1p^kWQE7>2a!{RukQSvT&zI|&PWA1vHN|G(V}Glee<2O2|>xtj+^WWjju zXbQh%1N&&goD9Ekd5koLIz9 z;^}pH%DOK$L}3Wpe$<|+iIR-!>Q4J=9*ckKh5o!|c`*k>Wno+x_BRW>UQHa@PdHg> z^kFG=n3BqIDN6#l*k|X2s(X26hQ@40mdxvrh|d^T+c8|V!Qt*rsq1bZnK>tYy8g5- zXMruBN(iuW%^>~hw?!h zaz5T~Z8Gf0>7CY40OsVaty#1_t_)fz-diZ0P24Fg?hK~3J)j(3EfB+4_vCkjiIkx(0pE}8EcqDf z1P4m*M2)chzyulAT-y7Z8bvgNo-Kp1_c^i}hqBEp7vG1w8`AT(=0dWV$zET7#3_1MCRr}^R}3o3qdb8-*>toH3=T8@=IUEGxj9Vn=CM z4Hif)^Z-nLTp7vTXp<8@2Kht`6(`dtN>k^6Cbh1N4jpCXp6PNu@bq?d_-Vr@W#K;( zG{lS~QrMmAKG&|2JTlEs-g?n!?3Fc$u7d37Y!3ipnV!79{#hYGk98?w; zu6}4kRXCket;$5LM65MWM$p(zo0ZC(PFG~#R!bC~#Y7j{CU<0COXzgGT^X%dQa3X% z^A3mr4=i1T0wC^Dj?WliPaC`4otEeA7=QoAiIyS7QT=&aMG3EE&xzIUu^TsEP1+JM ztTmDcEI z+MwyJP*mLA(@Nya9M?M6Kgu1ChV|7rZOH%I+Z3D_dHM>R!na$ z1b25MX>WOy*V;A;i7svzzJHzQPznGXPxXB$V$KKVgb6C8`X|g2sRl>rA6U}kxfSfn zWRLAXx;dphR9ueuL__m789X_fDjKJqTRw>a7BRMu@x&1+vRCdpuFS`ZejB3=@Wi~o z{k^BvOV7%zRGsnS@s-_U24a{@Tyn?a>;AC)m5MYgx)`ig=3BqzO+&$}<=vXrXNeQI za4^H=q(VD7VSwH8Hf&NZRO7=sUP2K4;qJ;Wb`E7MWW*n;n?jBG*5qFI;S9Y{^vX+E zwpG_m#Kk8mTiEx0MkqfjntTgQX)DQQ|aZ;_hObH0Rnx*_-RHV4s*-aE{ z(-uY65(*WptV1$+VJq-CU0bP&p`qSy?f}&Dt`xb!F{jE~g-uSKX^?fnNZJFK3!VLj z_$FS>)9oKSq$#E{0Jn|&e+rFU0}es19c2meml6VePK9Q9pnAV4+U9rd?l0JsMpUc? z74?FrmQyQPzXbP9uC<8#{fti7Z9q#`GXs}@#`FcW#Lj&KjOt6Y$Zh@=oggy?Q0=4(C@VZlUGzSwvw&C3az0}##^({}M+ zjfvz(v?$n9p>SD#c-|>v@u?)PQZ+}7zFYEj#b->oRU-&+7wA}hsKS9?cH{HR%q*6P z(IY^wVz;qhqFrkM{5WK$NPkt5}U_9b!9v{i@XzgO?YMkheqr%obf zy_zgWsla`1l48wxD6WrF|1P64NOF*kKg~CNg_huHQD!F|cR5cN5}})elIDx$F|`cx;^Kikk;LtOvb(D(J@~K^yyc3jBK6;{@D)7s zvar+h3r4MoE`6y`(8NxT@;WQf(%~6%Qy394gU{-Hxx?Til^%DTqO#WGLA5 zPZHCiA$~|Gmh`3v(_q%qkjw`o9PnoUOA(4gFEwtgkc5M zP83DEQ~+8!1DTb|#^CT^O!ItpVj2hxOZNPx)$Q`Fdt@Y|Zz3)CZKtMzJ8AQ;l(RLB ztjtpNdFh?kOQBqKI4i+2Nh1FxCAbS#(W#@^E!|q3C-yz!^s{fUs23v4&_QKbhFXRi zlfjj6#L33|3VU(_mY<%Eo76X( zmka8XU6DzfJy^3m2q8NG0lTQUzlmvpi6@sfw6HiOXG!XtLKiAiMTf`b>=n7^?d5Kz zu13+yE4fwdbgKVKLim`Q+zbN0-kbaw{AJPeYT2l7rBMWc^D(Ez4W{KuQ$1v?ayyWNjCe4v}|Q( z*5!SulU7l%2!?Ne40d2{bSLfI4)=jfiD_Tl9#60SI(PU``#NFO-o%8DWs2xH;!Q%d zZMwP=T(4$yV@vpA2y`H>x(O%$Vvn6&iQ6G?k%49C=}pDPW;;`nh>{sK=~5F=?FUG$3X=x&EROT5FeRZc7*6}| za{i^=Xi44wWu3CV-%8B|fUITMSq4dTNvtVI zx)iioe{whCK>szrE>C-t_Z=B!n?iyck-hFoBbQOQ`T7tyYnbWx6FZ?2nnLLR0({c~ zm=WL%)d$HR3y|do^&l3(|IV5Frwegti=BZVktmO+!u9PZfeZ)^g=0@+aA^GnbpP(1 znj(lmsRyhUZ#U-a&Ae3%BGWB5qSl3dIh^`Q7C^xj=&`P8)<|Bu&` zhyx>1*d>P60oRN(Dvow!odonI!aj%mg9K-_F>)TS&>nldjz?WcWa0 z|MUN1KhLWTz}p}Psgj+k!MDQxSLzG?-%1@s3yr&r%{7*bMY zRX)pt`taZ^QgZ7qQ(N;teexJOb$!Fsa@6uWe-=TG;k+;q=Vj_@5JMLQJk4e2ihXpB zjS>_gES5*EMt;@*3bOf1mqC5+(hGIDR8wd|nEBM8T%a13y>|DIbRdV^vq1yr@Wfiq;>kHFDw# zAK-Ehg!fgoVgG53F$wSKgU4RF*S1`XGg*1L)yo@hFc;D3mt=qIzordA@+vKpdwbJR zPy2IaM*VV`CZdGiA^}oGB}o5iD_FegH^=WbeQ#t6W9JSK##7uW;l^{?j*;~Sn3ZSR$Pe}*L&_ws|RV3T!T*M|fui3PMP zvx9{Z?Tj<-QBu`Or1m7c4_R@JRdiq@GLe8Znbk=j@aae*=d+vLIggN%Ouk!k6l){) zh;)y#Nb`N&s046=iZO%j`sv^-{@lxlGNRG?k}x$WO-MaHFw^H8uD7tzdT#N<25O|4 zTS03{YwKW8ers!>hSweant>MHK@O;}MQ46}LbM7sjz)r-5S;vpxou0yO}XNkw|Nu( zvI`-<;px=3(`qo0EueUaFBNNtDGB_ru^UOI%OT?7fy#@j=b^J#f{utWf}Id0W4(TM zy2M@keB)LkWwP|u_a5=*;M0&|sQ+8P2fm||qAfkijEO6z{n4Mw^n__?2|bSvGjmOf z5BVome`e|f6ZJkHjSvXfN#P(JZy_}mc66~PEZ6@^pK^&D2` z4$U#8!(>FRbCvrMAADh-J03Y!o4B(H4%6gO8q#(2U^~K|o_0?&?8yV9nZ-9xexEO0|kCL9<^!Y&Z%8f(?T{H&wyj+P15ZV^cgsY#>e zhtax9b3i+b2gF@ngA^Hz#DD0tFe}c)sm%UZQGnNI`7h0{I$IDn`Bc=n=Dt-QCu-IE z_xtE{7+e5)a%j5RGjJ$Ihk%fZRs^xKJs!HB0s7m+G!0RP|gM%A3ok#ZyF!WGa}M;3r?RkXi- z?8c)KQp62c5Kghxi)34Oqu;~~edZ1%fwJQ?O@&OyhYBQpnPA za0uo8^(}&)?os6J3Wr|qjQ0Sq2~g2EIv4GqTAIk=S; zR*E)-9t{TVh{E||Pb=CGr02niWf_v6`e|^gX5Nbd>Uwcvvszk0ke0nxI{8I*U9`{- z9#igBJTxKMVwhU7TCpzUtl+wNmSCN3N!qrnH6urv@TU78~%vX#NGz}u~;3*PPG#4! zTukIsWfD}4Qs^fFsU<{33ChPs^Fj~2@?VxFf&ed*4x5WhK(NIgu{M^@#n;GhoUxK1 z5-mhQZrG#@@aPPrUoR@+&xyjg9?pBp?pN;+v z?Yb5V*)_~h4{vz`r+LK2%fJc<4&|6MMI|mZuU?S}zI-D< z7R@UIcA|JeVWqGzw2eU0mVoyoqSlp^dcj~X56f)VVh-eZPh=-ZU~AMnQ*YPE_? zARNgQ^-V9K=O@v737eU06IbNk;4mQclp+e2In&?gJ$p)fJqzNXMk)uRrRDa_*U7Zc z5J<_;?^rxB>6**pT>Yj(0$kS9{B3?jS;zd{fAS8&bjMXb1f>2X0t2EYvaTfj@k!bY zf6yc=)?cUL++>J#&x00*nn%sa_UwviO75%D9dHnXeI&85ROA7~0zGD1C&K(<3J@7+bya{L2Ug10?U-%$Va z3%Gq>foos#c_g`Vf-WaVQPb-VNM_pdy{ak-dpW5GXGqQtYjob8;-~62S*H-eit;NZ zFwFi)B4Q_uI}DID4xmBih^t$RR)Y*<_?Z#is-!{-MCN8Ta+XgYv`u*&)|$9yE1P-p9Rc&mTXFXF)l3PVZ#{Q1Oz-A^X1RuQ zn-#`(-|b53Q?Jv&T+Pj;_~#XQ0u~7oo;+Za>EuVpVzpL}f7{4z0^n8k6Jj|Vp`THI44uI8arp^Nic`X^(>`A0) z2>2uWX?g;}-hq}E9x%9JCX)VQ1A>*D;g_*Cl&tPMv6T})%yJY@d`~-mIrNP43yPF> zjg-Z7;Zx8$xaHoDK;ott2GB7H z(rYi3vz~{j?(M`D;MY7`p;~p^{-9p}Cl50|bzTPlyOM2O3-GkjM=~PWt1Ux68eEUT z7YC#j1IM+OR})@cDy)k=B6FR>&%eDCl-KblK>4aoh(0z-cH{*Lm zQlwqfmsc}EbOkaXKAa8mNTwb&&&dcQ%&`Y&`S^Yb_w@k+7nPJZ2XOkP^ zFo71VxvRJxv-uK#P1F-<6pMi1J7F*3pp|WY9u)O6o8(E8Hi`z#Xn|xT5;;Z$0U9{l zkpw+vdysV`-V-jz%Rt3MKyj z=(3CVA{EpqisPYoLF0itfa{d8zVa=tzDRENZMnL7WENvf*HZq1xrJJs!?J-1H)Y(If_Zk@E__VVFe;>m~vUma?Gs^jxV} z)QPK#0aSR12fWi+M%>e#ld|5Ug5!e)OHoisF{HnX$BO#&m-*f7+Zkp&&BCI6q5cI{UYN(@( z$M9zLkOnq6h|&2x;KM<9)zCP)55VfMTCOZ3V1WURW|{FItfZ*6YagB&^<#a`a=xjerRHtdVrS*<@j;tvXxK zQ*?JD)z2!0_IMRMd|WD5`pZWq6mG#pKGLF}Vg^XDLviJ{g(X9@ZB9$8^iLYl8Xex= z*Os35+PEu;jxLlCjru3N1&Gd>$cB~@?Q`D%w-z-2;VO27#eG%P=aO(lseHuCy%gTy zqY*mbFcr1qDV&ehTEpxIIL#({b)~}J{L35-e;(^?sw9x#Lz7fO{r%c6FE8~hRUcA> zp_C$P)`XQDZTjLsL$|ZEtcf=a5umx_a5GtQB=a>{!bmTw@8m;M8587Su6fmm4>W~k z5pYqmMl9Z$=%f0XZ+gjKxB*E}|LEv^uJI_@xU7vMo$un}w<^-i5uaWriQj| zLD`0E6vzx)NmF^gc=aqI@T4J}x*P5$S0JdAxX-_6B#~Pi5)(T`IZQZs(}MXxctvQC zIuPU>hPM=a(Qea+@rFxJd`yg)%r$odi)Q5$k@?f@A689<;pKI|8OAz26rBi0{VFq& z131qlx|ukU`3I~{WQl@Hm>&8n+(X6#nq9M-HHhH-SI7Z%@>9AomZG*ilfDjh&M>%)Sjpj8n3*emFat+FC#pU- zx6CXd>PI<;X>9Ljw)m7lsWM+&`VgQV3{8w^FT2E&hA8X4g1DCp*X7MbQUeVlPGLQM zl!=5KHoOsBee^upv@<#1#CrEU`)A1NlN>|xM~&yJNgK5iNXd++j&MN$f2#Xtp*Llj z;ozW;a3#muR=+-)4PjXz^L#5Ro)o}wc?E0q?T~cN_I(BZt~$z3FHljPacud9AR3Nr zL8y|}ZA#|HD;2>@D+eN(S$&6x9r<;qnFYZol$9rhoH^puV%9emUlNE}^i-*xsVh`| z@k{Zevydt%g%`f1_7;Edmilo(c;hOFY9nWIAo5(%U%GOjgG%Gva|5JlTecWv#W}a$ z9e+$Z#(Co45pz#P2R2E$8fWHXkHL6nEoGxD5YnS<>@D}jh8YR-?JoLTHug~)EQ4oz zsaVr*+mhWV($nl8N@k)%@4CYhq~J7orJe`H?X{*R2C$4o+zz)zzTHIP{YBMr%7YnG zJH9MtQBq_06?OlO#igdsyxKt!@B7|QPxaYVa`Bgg`$5fCpzE2K8?XR#SI#h|7P^ti z(C1}8D)i?Fy0-$zkXV*64-Bm|?L0;;zR~5Me3p41H;++ELBf5G`3p`Qz`6?0i`$`B zoRzQhO4tWGZ&6d2reI?|t~=(dqS=n8HcYByW1Rdj2HpcU?T#Cv@oL@Lq93n>u4SHx%+*()v3c-VZw0R!B}D9t4bV{8;tq7Q-Q)7{q?Mcq#E2R zf(Hv3s!R;y`QIJ_H&#R;c)jQ5Kb7XFrXPhgrIf|Vq*5!(pCq~~DCXD?`t&dh>9mA% z!uyNgOf^pEpkUTF!sltxde!7@R_f>7b#H-{&_bU-d}$9n!cWvqJ)=nAyL~g3xauV{ zr-rtgeQ;U*oJf4Dx);R2b&#bkhq@cY`x%vYck-Mwg6$nTs>1{|b$BQ(t?4Ggzia^v zCcy=efeTcedUC?0IoGT@-txy@ylcl>P z%JFw9JLpxBbj7Ww)B_uG=4<;KIOH-Ff>#tx`q*^=n^ToU{;5J?htjaRfl!3{Mqfi_ zB_wU}^7wYGz^IJ*UA$NWa^VdXY)CKFQITpr9zOMi#qa0y1$8%;2J^az`d`#YYheFZ zEynw;XR=9ZaewwXrJTNZm-kNnTm^M%27!o;u=uD}jj2Zo&ILY;$47ogY12>FqWtnX zUWmhM)8LWIHnfBh)y^&=`8j;FbcXiVwdoH>dC`+rA4W={L#Jk*bOLqL z&yAdBrf3-(n}5gu%&1+zlYH?DISZpu8Zh2TLf;SjzZfkPE8}G* z5wsYXy;=xt{v6AH!Xm%axbHfbRvAQkFfvNS5uuG_ryI2YX1IV2+|{qIA8BYd0yz+Nk<`>u>X~K>@HhFT?5;U z!d9T9ngBW*qSJ+wP<1;%dpnw6-dp27s@-)DP{zV8;p$^!l&c627EmfN-@WEEM&jX8 zQbf$SmcBRX!z1fN5cfds@J>p15U?Na%@+AL{;HbgE&^!mHlXT!5(^_{ApRZ@+X0=a z1b-|FeuhNec4G0jt5x4NWRxR2O48>;&m4}4g;xbP3_8vD`yk({L98u9yFs)2C5BSW zr;5?q&&&+3U+cs_PtJTF;$N|Lqs#?qK=OZJlq-ucEa_A23zdBIXkW!^pCy4s*}E z%HZ61T$n5i%$n zFsbWw2{4v@MM0!0V2&k~bc|Q%yW@>EXXf(Akv!;^GRa4lh{FCYb>|vCKmYq$LUvdp z^f!qea1~_+;}SVe=b4OHjp>Ni3?8-UbZ?H_Q%^JHLafsv>LX*M*~jzAb6^9Xf~i)O zYHDj6xH?gh*7>ZP)u8D(er*N623<5!dt!}o$^FbeR&26WFrNT>t5BI3Q-!;m69$q_ zqD6d~B(=`)@=-y-F6*T`?g?{8rz9YIZ`N^I$HZiZWYA$U4y_`X~z(A~5pbOI!+_`9`zPIy@P|f>p ze`-e8eL$;(8+HTM*uI&m<5UvwOfjuAB@0}h-@U&oiqV)Woa3Q{hwzShZViE2jOBPn zK-Z`r4>a*ca(AjNC`;U0&6p;=mVom3Kec59XFejjzHRGs&~A((U%gh`%3@7Pq^3cD&22lADhN5UuJDBW(3;L0M+y z1+Ow0vgk6Qdrj1Yb3GZ1=26fBL3R5?${HGD z0LDnZmj<6Ql|03|El01_Cv;P|h#@vs^6R@ZQpRj@Vp%Bss@NBB- zD~J_%AIOO?d}Xy8-ByvK3k^Uf3hAfZ*K00@C&|&epZ__qm@DC;Q$baA4M8S1wIBW+ zSBI-I2}Zy>FQYHk0>%Ul&aD$IhGS66_dx_I5`br#_Eh? zP)up*9UteX#Ib5B!+T5<)U8pF$SJXeQsAWOclJi?KGN(KgtMv*LqpI>8pMWyCNsj} zac{fQE$8ekW|9*K%1kK4t77pkjpdcWAfcIUy=RS1tFYlP4Y=iIWeVW{_Lj~L1Ft9sCASk81*RM7%H|)CW+g zoqawE))Z%lmg|~U^ZuZ~?J=k;6Z8%}znY33957GU#|-A@KZab@=fmN5AX$3bypBEj z)@`;`ri7KdqY*ip^K;lT-db7>$h z+iH1xQgZ8%1mA~ZVwJ(Z8yH{d`U48MRlk~TXuVBYavm9>dNee6wzDmC^A$cip;JuA zgV136pZ~dQ&c|(B^}dLgu;m3#&2`?GL#w95o`r;c6DI!98CI5VfODa4@AW2|DBkJP z8^gW^PTbA?>@$~SzmdIGQ`hj^6Dkp<@Na%ia8#A>_FNp{bA|2D)9oKS(ApXT2cnaK`;H^ zIRBsus=hdD9Ke%mIFoe^KHi6ZkX}09!WMzBaWa)mPEAZu_%`6CGRFcryZ58>?+vE+ z;zR&GjU{*r78}O&ow}+WfL;GYKsffzk`En-1eyIv2o^m7uT_HrtPBk;3wggh(7+>h zcfwcQzI;n~7vZ{-mX@TjoBC-N|7KQGS3VT^idO*sWC6^ZdgbgyfADChKCU4-8U3wq z236``6$?QZ%?ALUu=?t{v*1TvYD42}qJ&N-REO{z)p8tet%an(!O2ZZhr}C+`q|01 zmXS5n?#%a#k?Aj(hE@3MPD|@Mmzh=7+>Zj>EN(tusu@Bayg&5|Z-@sf843!2d{gp3 zQ*2~r9RB5(9XgU*1OV_)I)i%vUFaApa+;~IlU`m79{X+_DfH6c2|0gCf@-U*tSWDO zHadD-{(Kf8qIGB)-!|74}<;px*y1?Dnn8DUr kO?bc+!l?IYK0&E4N)Jvm7^oq@x3^y@%WKG0%9w}!AC~+F(EtDd literal 0 HcmV?d00001 From 0135abde1c3332bf406c8bcb5ba86bcd1ff3ec8b Mon Sep 17 00:00:00 2001 From: Tsukumi <112180165+Tsukumi233@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:47:38 +0800 Subject: [PATCH 27/34] fix: support codex /responses/compact route (#1194) --- src-tauri/src/proxy/handlers.rs | 41 +++++++++++++++++++++++++++++++++ src-tauri/src/proxy/server.rs | 17 ++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs index 644ef353c..6cc9840b0 100644 --- a/src-tauri/src/proxy/handlers.rs +++ b/src-tauri/src/proxy/handlers.rs @@ -348,6 +348,47 @@ pub async fn handle_responses( process_response(response, &ctx, &state, &CODEX_PARSER_CONFIG).await } +/// 处理 /v1/responses/compact 请求(OpenAI Responses Compact API - Codex CLI 透传) +pub async fn handle_responses_compact( + State(state): State, + headers: axum::http::HeaderMap, + Json(body): Json, +) -> Result { + let mut ctx = + RequestContext::new(&state, &body, &headers, AppType::Codex, "Codex", "codex").await?; + + let is_stream = body + .get("stream") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let forwarder = ctx.create_forwarder(&state); + let result = match forwarder + .forward_with_retry( + &AppType::Codex, + "/responses/compact", + body, + headers, + ctx.get_providers(), + ) + .await + { + Ok(result) => result, + Err(mut err) => { + if let Some(provider) = err.provider.take() { + ctx.provider = provider; + } + log_forward_error(&state, &ctx, is_stream, &err.error); + return Err(err.error); + } + }; + + ctx.provider = result.provider; + let response = result.response; + + process_response(response, &ctx, &state, &CODEX_PARSER_CONFIG).await +} + // ============================================================================ // Gemini API 处理器 // ============================================================================ diff --git a/src-tauri/src/proxy/server.rs b/src-tauri/src/proxy/server.rs index 9de6b7292..46e3635f5 100644 --- a/src-tauri/src/proxy/server.rs +++ b/src-tauri/src/proxy/server.rs @@ -237,6 +237,23 @@ impl ProxyServer { .route("/v1/responses", post(handlers::handle_responses)) .route("/v1/v1/responses", post(handlers::handle_responses)) .route("/codex/v1/responses", post(handlers::handle_responses)) + // OpenAI Responses Compact API (Codex CLI 远程压缩,透传) + .route( + "/responses/compact", + post(handlers::handle_responses_compact), + ) + .route( + "/v1/responses/compact", + post(handlers::handle_responses_compact), + ) + .route( + "/v1/v1/responses/compact", + post(handlers::handle_responses_compact), + ) + .route( + "/codex/v1/responses/compact", + post(handlers::handle_responses_compact), + ) // Gemini API (支持带前缀和不带前缀) .route("/v1beta/*path", post(handlers::handle_gemini)) .route("/gemini/v1beta/*path", post(handlers::handle_gemini)) From 27d21c23ace926992ba0c50e34a611ecffd97c0c Mon Sep 17 00:00:00 2001 From: Suki <3907409812@qq.com> Date: Wed, 4 Mar 2026 23:49:14 +0800 Subject: [PATCH 28/34] Add Bailian For Coding preset configuration (#1263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题描述 bailian 模块使用的请求地址 https://dashscope.aliyuncs.com/apps/anthropic 仅适用于普通订阅用户,订阅 code 的用户调用该地址会无法正常使用 修改内容 增加bailian For Coding模块提供给 code 订阅用户: 正确地址: https://coding.dashscope.aliyuncs.com/apps/anthropic 测试验证 测试环境:本地搭建 ccswitch 运行环境,使用 code 订阅的阿里云 DashScope 账号 测试步骤: 1. 修改地址前:调用 bailian 接口提示请求失败/无权限 2. 修改地址后:成功调用接口,返回正常响应结果 影响范围:仅增加bailian For Coding模块,未改动其他逻辑,不影响非 code 订阅用户的使用 --- src/config/claudeProviderPresets.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 926ebaee5..c658f03f7 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -133,6 +133,19 @@ export const providerPresets: ProviderPreset[] = [ icon: "bailian", iconColor: "#624AFF", }, +{ + name: "Bailian For Coding", + websiteUrl: "https://bailian.console.aliyun.com", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://coding.dashscope.aliyuncs.com/apps/anthropic", + ANTHROPIC_AUTH_TOKEN: "", + }, + }, + category: "cn_official", + icon: "bailian", + iconColor: "#624AFF", + }, { name: "Kimi", websiteUrl: "https://platform.moonshot.cn/console", From 8217bfff50eec0c3261030582f7debd0c7f4d4df Mon Sep 17 00:00:00 2001 From: Keith Yu <103325445+keithyt06@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:57:21 +0800 Subject: [PATCH 29/34] feat: add Bedrock request optimizer (PRE-SEND thinking + cache injection) (#1301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Bedrock request optimizer (PRE-SEND thinking + cache injection) Add a PRE-SEND request optimizer that enhances Bedrock API requests before forwarding, complementing the existing POST-ERROR rectifier system. New modules: - thinking_optimizer: 3-path model detection (adaptive/legacy/skip) - Opus 4.6/Sonnet 4.6: adaptive thinking + effort max + 1M context beta - Legacy models: inject extended thinking with max budget - Haiku: skip (no modification) - cache_injector: auto-inject cache_control breakpoints (max 4) - Injects at tools/system/assistant message positions - TTL upgrade for existing breakpoints (5m → 1h) Gate: only activates for Bedrock providers (CLAUDE_CODE_USE_BEDROCK=1) Config: stored in SQLite settings table, default OFF, user opt-in UI: new Optimizer section in RectifierConfigPanel with 3 toggles + TTL 18 unit tests covering all paths. Verified against live Bedrock API. * chore: remove docs/plans directory * fix: address code review findings for Bedrock request optimizer P0 fixes: - Replace hardcoded Chinese with i18n t() calls in optimizer panel, add translation keys to zh/en/ja locale files - Fix u64 underflow: max_tokens - 1 → max_tokens.saturating_sub(1) - Move optimizer from before retry loop to per-provider with body cloning, preventing Bedrock fields leaking to non-Bedrock providers P1 fixes: - Replace .map() side-effect pattern with idiomatic if-let (clippy) - Fix module alphabetical ordering in mod.rs - Add cache_ttl whitelist validation in set_optimizer_config - Remove #[allow(unused_assignments)] and dead budget decrement --------- Co-authored-by: Keith (via OpenClaw) Co-authored-by: Jason --- src-tauri/src/commands/settings.rs | 30 ++ src-tauri/src/database/dao/settings.rs | 23 ++ src-tauri/src/lib.rs | 2 + src-tauri/src/proxy/cache_injector.rs | 374 ++++++++++++++++++ src-tauri/src/proxy/forwarder.rs | 63 ++- src-tauri/src/proxy/handler_context.rs | 7 +- src-tauri/src/proxy/mod.rs | 2 + src-tauri/src/proxy/thinking_optimizer.rs | 274 +++++++++++++ src-tauri/src/proxy/types.rs | 36 ++ .../settings/RectifierConfigPanel.tsx | 116 +++++- src/i18n/locales/en.json | 12 + src/i18n/locales/ja.json | 12 + src/i18n/locales/zh.json | 12 + src/lib/api/settings.ts | 15 + 14 files changed, 969 insertions(+), 9 deletions(-) create mode 100644 src-tauri/src/proxy/cache_injector.rs create mode 100644 src-tauri/src/proxy/thinking_optimizer.rs diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 8aac400f9..23304ebbc 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -145,6 +145,36 @@ pub async fn set_rectifier_config( Ok(true) } +/// 获取优化器配置 +#[tauri::command] +pub async fn get_optimizer_config( + state: tauri::State<'_, crate::AppState>, +) -> Result { + state.db.get_optimizer_config().map_err(|e| e.to_string()) +} + +/// 设置优化器配置 +#[tauri::command] +pub async fn set_optimizer_config( + state: tauri::State<'_, crate::AppState>, + config: crate::proxy::types::OptimizerConfig, +) -> Result { + // Validate cache_ttl: only allow known values + match config.cache_ttl.as_str() { + "5m" | "1h" => {} + other => { + return Err(format!( + "Invalid cache_ttl value: '{other}'. Allowed values: '5m', '1h'" + )) + } + } + state + .db + .set_optimizer_config(&config) + .map_err(|e| e.to_string())?; + Ok(true) +} + /// 获取日志配置 #[tauri::command] pub async fn get_log_config( diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index e39aa7b31..6f15b2585 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -187,6 +187,29 @@ impl Database { self.set_setting("rectifier_config", &json) } + // --- 优化器配置 --- + + /// 获取优化器配置 + /// + /// 返回优化器配置,如果不存在则返回默认值(默认关闭) + pub fn get_optimizer_config(&self) -> Result { + match self.get_setting("optimizer_config")? { + Some(json) => serde_json::from_str(&json) + .map_err(|e| AppError::Database(format!("解析优化器配置失败: {e}"))), + None => Ok(crate::proxy::types::OptimizerConfig::default()), + } + } + + /// 更新优化器配置 + pub fn set_optimizer_config( + &self, + config: &crate::proxy::types::OptimizerConfig, + ) -> Result<(), AppError> { + let json = serde_json::to_string(config) + .map_err(|e| AppError::Database(format!("序列化优化器配置失败: {e}")))?; + self.set_setting("optimizer_config", &json) + } + // --- 日志配置 --- /// 获取日志配置 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 65739696d..5715d7392 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -886,6 +886,8 @@ pub fn run() { commands::save_settings, commands::get_rectifier_config, commands::set_rectifier_config, + commands::get_optimizer_config, + commands::set_optimizer_config, commands::get_log_config, commands::set_log_config, commands::restart_app, diff --git a/src-tauri/src/proxy/cache_injector.rs b/src-tauri/src/proxy/cache_injector.rs new file mode 100644 index 000000000..d7e43b172 --- /dev/null +++ b/src-tauri/src/proxy/cache_injector.rs @@ -0,0 +1,374 @@ +//! Cache 断点注入器 +//! +//! 在请求转发前自动注入 cache_control 标记,启用 Bedrock Prompt Caching + +use super::types::OptimizerConfig; +use serde_json::{json, Value}; + +/// 在请求体关键位置注入 cache_control 断点 +pub fn inject(body: &mut Value, config: &OptimizerConfig) { + if !config.cache_injection { + return; + } + + let existing = count_existing(body); + + // 升级已有断点的 TTL + upgrade_existing_ttl(body, &config.cache_ttl); + + let mut budget = 4_usize.saturating_sub(existing); + if budget == 0 { + if existing > 0 { + log::info!( + "[OPT] cache: ttl-upgrade({existing}->{},existing={existing})", + config.cache_ttl + ); + } else { + log::info!("[OPT] cache: no-op(existing={existing})"); + } + return; + } + + let mut injected = Vec::new(); + + // (a) tools 末尾 + if budget > 0 { + if let Some(tools) = body.get_mut("tools").and_then(|t| t.as_array_mut()) { + if let Some(last) = tools.last_mut() { + if last.get("cache_control").is_none() { + if let Some(o) = last.as_object_mut() { + o.insert( + "cache_control".to_string(), + make_cache_control(&config.cache_ttl), + ); + } + budget -= 1; + injected.push("tools"); + } + } + } + } + + // (b) system 末尾 + if budget > 0 { + // 字符串 system → 转为数组 + if body.get("system").and_then(|s| s.as_str()).is_some() { + let text = body["system"].as_str().unwrap().to_string(); + body["system"] = json!([{"type": "text", "text": text}]); + } + + if let Some(system) = body.get_mut("system").and_then(|s| s.as_array_mut()) { + if let Some(last) = system.last_mut() { + if last.get("cache_control").is_none() { + if let Some(o) = last.as_object_mut() { + o.insert( + "cache_control".to_string(), + make_cache_control(&config.cache_ttl), + ); + } + budget -= 1; + injected.push("system"); + } + } + } + } + + // (c) 最后一条 assistant 消息的最后一个非 thinking block + if budget > 0 { + if let Some(messages) = body.get_mut("messages").and_then(|m| m.as_array_mut()) { + if let Some(assistant_msg) = messages + .iter_mut() + .rev() + .find(|m| m.get("role").and_then(|r| r.as_str()) == Some("assistant")) + { + if let Some(content) = assistant_msg + .get_mut("content") + .and_then(|c| c.as_array_mut()) + { + // 逆序找最后一个非 thinking/redacted_thinking block + if let Some(block) = content.iter_mut().rev().find(|b| { + let bt = b.get("type").and_then(|t| t.as_str()).unwrap_or(""); + bt != "thinking" && bt != "redacted_thinking" + }) { + if block.get("cache_control").is_none() { + if let Some(o) = block.as_object_mut() { + o.insert( + "cache_control".to_string(), + make_cache_control(&config.cache_ttl), + ); + } + injected.push("msgs"); + } + } + } + } + } + } + + log::info!( + "[OPT] cache: {}bp({},{},pre={existing})", + injected.len(), + injected.join("+"), + config.cache_ttl, + ); +} + +fn make_cache_control(ttl: &str) -> Value { + if ttl == "5m" { + json!({"type": "ephemeral"}) + } else { + json!({"type": "ephemeral", "ttl": ttl}) + } +} + +fn count_existing(body: &Value) -> usize { + let mut count = 0; + + if let Some(tools) = body.get("tools").and_then(|t| t.as_array()) { + count += tools + .iter() + .filter(|t| t.get("cache_control").is_some()) + .count(); + } + + if let Some(system) = body.get("system").and_then(|s| s.as_array()) { + count += system + .iter() + .filter(|b| b.get("cache_control").is_some()) + .count(); + } + + if let Some(messages) = body.get("messages").and_then(|m| m.as_array()) { + for msg in messages { + if let Some(content) = msg.get("content").and_then(|c| c.as_array()) { + count += content + .iter() + .filter(|b| b.get("cache_control").is_some()) + .count(); + } + } + } + + count +} + +fn upgrade_existing_ttl(body: &mut Value, ttl: &str) { + let upgrade = |val: &mut Value| { + if let Some(cc) = val.get_mut("cache_control").and_then(|c| c.as_object_mut()) { + if ttl == "5m" { + cc.remove("ttl"); + } else { + cc.insert("ttl".to_string(), json!(ttl)); + } + } + }; + + if let Some(tools) = body.get_mut("tools").and_then(|t| t.as_array_mut()) { + for tool in tools.iter_mut() { + upgrade(tool); + } + } + + if let Some(system) = body.get_mut("system").and_then(|s| s.as_array_mut()) { + for block in system.iter_mut() { + upgrade(block); + } + } + + if let Some(messages) = body.get_mut("messages").and_then(|m| m.as_array_mut()) { + for msg in messages.iter_mut() { + if let Some(content) = msg.get_mut("content").and_then(|c| c.as_array_mut()) { + for block in content.iter_mut() { + upgrade(block); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn default_config() -> OptimizerConfig { + OptimizerConfig { + enabled: true, + thinking_optimizer: true, + cache_injection: true, + cache_ttl: "1h".to_string(), + } + } + + #[test] + fn test_empty_body_no_injection() { + let mut body = json!({"model": "test", "messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]}); + let original = body.clone(); + inject(&mut body, &default_config()); + // No tools, no system, no assistant → no injection + assert_eq!(body, original); + } + + #[test] + fn test_inject_three_breakpoints() { + let mut body = json!({ + "model": "test", + "tools": [{"name": "tool1"}, {"name": "tool2"}], + "system": [{"type": "text", "text": "sys prompt"}], + "messages": [ + {"role": "user", "content": [{"type": "text", "text": "hi"}]}, + {"role": "assistant", "content": [ + {"type": "text", "text": "hello"} + ]} + ] + }); + + inject(&mut body, &default_config()); + + // tools last element + assert!(body["tools"][1].get("cache_control").is_some()); + assert_eq!(body["tools"][1]["cache_control"]["ttl"], "1h"); + // system last element + assert!(body["system"][0].get("cache_control").is_some()); + // assistant last non-thinking block + assert!(body["messages"][1]["content"][0] + .get("cache_control") + .is_some()); + } + + #[test] + fn test_existing_four_breakpoints_only_upgrades_ttl() { + let mut body = json!({ + "model": "test", + "tools": [ + {"name": "t1", "cache_control": {"type": "ephemeral", "ttl": "5m"}}, + {"name": "t2", "cache_control": {"type": "ephemeral", "ttl": "5m"}} + ], + "system": [ + {"type": "text", "text": "sys", "cache_control": {"type": "ephemeral", "ttl": "5m"}} + ], + "messages": [ + {"role": "assistant", "content": [ + {"type": "text", "text": "ok", "cache_control": {"type": "ephemeral", "ttl": "5m"}} + ]} + ] + }); + + inject(&mut body, &default_config()); + + // All TTLs upgraded to 1h, no new breakpoints + assert_eq!(body["tools"][0]["cache_control"]["ttl"], "1h"); + assert_eq!(body["tools"][1]["cache_control"]["ttl"], "1h"); + assert_eq!(body["system"][0]["cache_control"]["ttl"], "1h"); + assert_eq!( + body["messages"][0]["content"][0]["cache_control"]["ttl"], + "1h" + ); + } + + #[test] + fn test_existing_two_injects_two_more() { + let mut body = json!({ + "model": "test", + "tools": [ + {"name": "t1", "cache_control": {"type": "ephemeral"}}, + {"name": "t2", "cache_control": {"type": "ephemeral"}} + ], + "system": [{"type": "text", "text": "sys"}], + "messages": [ + {"role": "assistant", "content": [{"type": "text", "text": "ok"}]} + ] + }); + + inject(&mut body, &default_config()); + + // budget = 4 - 2 = 2, inject system + msgs + assert!(body["system"][0].get("cache_control").is_some()); + assert!(body["messages"][0]["content"][0] + .get("cache_control") + .is_some()); + } + + #[test] + fn test_system_string_converted_to_array() { + let mut body = json!({ + "model": "test", + "system": "You are a helpful assistant", + "messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}] + }); + + inject(&mut body, &default_config()); + + assert!(body["system"].is_array()); + let sys = body["system"].as_array().unwrap(); + assert_eq!(sys.len(), 1); + assert_eq!(sys[0]["type"], "text"); + assert_eq!(sys[0]["text"], "You are a helpful assistant"); + assert!(sys[0].get("cache_control").is_some()); + } + + #[test] + fn test_ttl_5m_no_ttl_field() { + let config = OptimizerConfig { + cache_ttl: "5m".to_string(), + ..default_config() + }; + let mut body = json!({ + "model": "test", + "tools": [{"name": "tool1"}], + "messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}] + }); + + inject(&mut body, &config); + + let cc = &body["tools"][0]["cache_control"]; + assert_eq!(cc["type"], "ephemeral"); + assert!(cc.get("ttl").is_none() || cc["ttl"].is_null()); + } + + #[test] + fn test_disabled_no_change() { + let config = OptimizerConfig { + cache_injection: false, + ..default_config() + }; + let mut body = json!({ + "model": "test", + "tools": [{"name": "tool1"}], + "system": [{"type": "text", "text": "sys"}], + "messages": [{"role": "assistant", "content": [{"type": "text", "text": "ok"}]}] + }); + let original = body.clone(); + + inject(&mut body, &config); + + assert_eq!(body, original); + } + + #[test] + fn test_skip_thinking_blocks_in_assistant() { + let mut body = json!({ + "model": "test", + "messages": [ + {"role": "assistant", "content": [ + {"type": "thinking", "thinking": "hmm"}, + {"type": "text", "text": "result"}, + {"type": "redacted_thinking", "data": "xxx"} + ]} + ] + }); + + inject(&mut body, &default_config()); + + // Should inject on "text" block (last non-thinking), not on thinking/redacted_thinking + assert!(body["messages"][0]["content"][1] + .get("cache_control") + .is_some()); + assert!(body["messages"][0]["content"][0] + .get("cache_control") + .is_none()); + assert!(body["messages"][0]["content"][2] + .get("cache_control") + .is_none()); + } +} diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index c26ceabf4..755ae49de 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -12,7 +12,7 @@ use super::{ thinking_rectifier::{ normalize_thinking_type, rectify_anthropic_request, should_rectify_thinking_signature, }, - types::{ProxyStatus, RectifierConfig}, + types::{OptimizerConfig, ProxyStatus, RectifierConfig}, ProxyError, }; use crate::{app_config::AppType, provider::Provider}; @@ -97,6 +97,8 @@ pub struct RequestForwarder { current_provider_id_at_start: String, /// 整流器配置 rectifier_config: RectifierConfig, + /// 优化器配置 + optimizer_config: OptimizerConfig, /// 非流式请求超时(秒) non_streaming_timeout: std::time::Duration, } @@ -114,6 +116,7 @@ impl RequestForwarder { _streaming_first_byte_timeout: u64, _streaming_idle_timeout: u64, rectifier_config: RectifierConfig, + optimizer_config: OptimizerConfig, ) -> Self { Self { router, @@ -123,6 +126,7 @@ impl RequestForwarder { app_handle, current_provider_id_at_start, rectifier_config, + optimizer_config, non_streaming_timeout: std::time::Duration::from_secs(non_streaming_timeout), } } @@ -139,7 +143,7 @@ impl RequestForwarder { &self, app_type: &AppType, endpoint: &str, - mut body: Value, + body: Value, headers: axum::http::HeaderMap, providers: Vec, ) -> Result { @@ -183,6 +187,22 @@ impl RequestForwarder { continue; } + // PRE-SEND 优化器:每个 provider 独立决定是否优化 + // clone body 以避免 Bedrock 优化字段泄漏到非 Bedrock provider(failover 场景) + let mut provider_body = + if self.optimizer_config.enabled && is_bedrock_provider(provider) { + let mut b = body.clone(); + if self.optimizer_config.thinking_optimizer { + super::thinking_optimizer::optimize(&mut b, &self.optimizer_config); + } + if self.optimizer_config.cache_injection { + super::cache_injector::inject(&mut b, &self.optimizer_config); + } + b + } else { + body.clone() + }; + attempted_providers += 1; // 更新状态中的当前Provider信息 @@ -196,7 +216,13 @@ impl RequestForwarder { // 转发请求(每个 Provider 只尝试一次,重试由客户端控制) match self - .forward(provider, endpoint, &body, &headers, adapter.as_ref()) + .forward( + provider, + endpoint, + &provider_body, + &headers, + adapter.as_ref(), + ) .await { Ok(response) => { @@ -296,7 +322,7 @@ impl RequestForwarder { } // 首次触发:整流请求体 - let rectified = rectify_anthropic_request(&mut body); + let rectified = rectify_anthropic_request(&mut provider_body); // 整流未生效:继续尝试 budget 整流路径,避免误判后短路 if !rectified.applied { @@ -318,7 +344,13 @@ impl RequestForwarder { // 使用同一供应商重试(不计入熔断器) match self - .forward(provider, endpoint, &body, &headers, adapter.as_ref()) + .forward( + provider, + endpoint, + &provider_body, + &headers, + adapter.as_ref(), + ) .await { Ok(response) => { @@ -472,7 +504,7 @@ impl RequestForwarder { }); } - let budget_rectified = rectify_thinking_budget(&mut body); + let budget_rectified = rectify_thinking_budget(&mut provider_body); if !budget_rectified.applied { log::warn!( "[{app_type_str}] [RECT-014] budget 整流器触发但无可整流内容,不做无意义重试" @@ -509,7 +541,13 @@ impl RequestForwarder { // 使用同一供应商重试(不计入熔断器) match self - .forward(provider, endpoint, &body, &headers, adapter.as_ref()) + .forward( + provider, + endpoint, + &provider_body, + &headers, + adapter.as_ref(), + ) .await { Ok(response) => { @@ -927,3 +965,14 @@ fn extract_error_message(error: &ProxyError) -> Option { _ => Some(error.to_string()), } } + +/// 检测 Provider 是否为 Bedrock(通过 CLAUDE_CODE_USE_BEDROCK 环境变量判断) +fn is_bedrock_provider(provider: &Provider) -> bool { + provider + .settings_config + .get("env") + .and_then(|e| e.get("CLAUDE_CODE_USE_BEDROCK")) + .and_then(|v| v.as_str()) + .map(|v| v == "1") + .unwrap_or(false) +} diff --git a/src-tauri/src/proxy/handler_context.rs b/src-tauri/src/proxy/handler_context.rs index b4de0374a..4f36434db 100644 --- a/src-tauri/src/proxy/handler_context.rs +++ b/src-tauri/src/proxy/handler_context.rs @@ -8,7 +8,7 @@ use crate::proxy::{ extract_session_id, forwarder::RequestForwarder, server::ProxyState, - types::{AppProxyConfig, RectifierConfig}, + types::{AppProxyConfig, OptimizerConfig, RectifierConfig}, ProxyError, }; use axum::http::HeaderMap; @@ -59,6 +59,8 @@ pub struct RequestContext { pub session_id: String, /// 整流器配置 pub rectifier_config: RectifierConfig, + /// 优化器配置 + pub optimizer_config: OptimizerConfig, } impl RequestContext { @@ -93,6 +95,7 @@ impl RequestContext { // 从数据库读取整流器配置 let rectifier_config = state.db.get_rectifier_config().unwrap_or_default(); + let optimizer_config = state.db.get_optimizer_config().unwrap_or_default(); let current_provider_id = crate::settings::get_current_provider(&app_type).unwrap_or_default(); @@ -156,6 +159,7 @@ impl RequestContext { app_type, session_id, rectifier_config, + optimizer_config, }) } @@ -216,6 +220,7 @@ impl RequestContext { first_byte_timeout, idle_timeout, self.rectifier_config.clone(), + self.optimizer_config.clone(), ) } diff --git a/src-tauri/src/proxy/mod.rs b/src-tauri/src/proxy/mod.rs index fd6e0ba88..5378823a6 100644 --- a/src-tauri/src/proxy/mod.rs +++ b/src-tauri/src/proxy/mod.rs @@ -3,6 +3,7 @@ //! 提供本地HTTP代理服务,支持多Provider故障转移和请求透传 pub mod body_filter; +pub mod cache_injector; pub mod circuit_breaker; pub mod error; pub mod error_mapper; @@ -22,6 +23,7 @@ pub mod response_processor; pub(crate) mod server; pub mod session; pub mod thinking_budget_rectifier; +pub mod thinking_optimizer; pub mod thinking_rectifier; pub(crate) mod types; pub mod usage; diff --git a/src-tauri/src/proxy/thinking_optimizer.rs b/src-tauri/src/proxy/thinking_optimizer.rs new file mode 100644 index 000000000..c9b770c0b --- /dev/null +++ b/src-tauri/src/proxy/thinking_optimizer.rs @@ -0,0 +1,274 @@ +//! Thinking 优化器 + +use super::types::OptimizerConfig; +use serde_json::{json, Value}; + +/// 根据模型类型自动优化 thinking 配置 +/// +/// 三路径分发: +/// - skip: haiku 模型直接跳过 +/// - adaptive: opus-4-6 / sonnet-4-6 使用 adaptive thinking +/// - legacy: 其他模型注入 enabled thinking + budget_tokens +pub fn optimize(body: &mut Value, config: &OptimizerConfig) { + if !config.thinking_optimizer { + return; + } + + let model = match body.get("model").and_then(|m| m.as_str()) { + Some(m) => m.to_lowercase(), + None => return, + }; + + if model.contains("haiku") { + log::info!("[OPT] thinking: skip(haiku)"); + return; + } + + if model.contains("opus-4-6") || model.contains("sonnet-4-6") { + log::info!("[OPT] thinking: adaptive({})", model); + body["thinking"] = json!({"type": "adaptive"}); + body["output_config"] = json!({"effort": "max"}); + append_beta(body, "context-1m-2025-08-07"); + return; + } + + // legacy path + log::info!("[OPT] thinking: legacy({})", model); + + let max_tokens = body + .get("max_tokens") + .and_then(|v| v.as_u64()) + .unwrap_or(16384); + + let budget_target = max_tokens.saturating_sub(1); + + let thinking_type = body + .get("thinking") + .and_then(|t| t.get("type")) + .and_then(|t| t.as_str()) + .map(|s| s.to_string()); + + match thinking_type.as_deref() { + None | Some("disabled") => { + body["thinking"] = json!({ + "type": "enabled", + "budget_tokens": budget_target + }); + append_beta(body, "interleaved-thinking-2025-05-14"); + } + Some("enabled") => { + let current_budget = body + .get("thinking") + .and_then(|t| t.get("budget_tokens")) + .and_then(|b| b.as_u64()) + .unwrap_or(0); + if current_budget < budget_target { + body["thinking"]["budget_tokens"] = json!(budget_target); + } + append_beta(body, "interleaved-thinking-2025-05-14"); + } + _ => { + append_beta(body, "interleaved-thinking-2025-05-14"); + } + } +} + +/// 追加 beta 标识到 anthropic_beta 数组(去重) +fn append_beta(body: &mut Value, beta: &str) { + match body.get("anthropic_beta") { + Some(Value::Array(arr)) => { + if arr.iter().any(|v| v.as_str() == Some(beta)) { + return; + } + body["anthropic_beta"] + .as_array_mut() + .unwrap() + .push(json!(beta)); + } + Some(Value::Null) | None => { + body["anthropic_beta"] = json!([beta]); + } + _ => { + body["anthropic_beta"] = json!([beta]); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn enabled_config() -> OptimizerConfig { + OptimizerConfig { + enabled: true, + thinking_optimizer: true, + cache_injection: true, + cache_ttl: "1h".to_string(), + } + } + + fn disabled_config() -> OptimizerConfig { + OptimizerConfig { + enabled: true, + thinking_optimizer: false, + cache_injection: true, + cache_ttl: "1h".to_string(), + } + } + + #[test] + fn test_adaptive_opus_4_6() { + let mut body = json!({ + "model": "anthropic.claude-opus-4-6-20250514-v1:0", + "max_tokens": 16384, + "thinking": {"type": "enabled", "budget_tokens": 8000}, + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body["thinking"]["type"], "adaptive"); + assert!(body["thinking"].get("budget_tokens").is_none()); + assert_eq!(body["output_config"]["effort"], "max"); + let betas = body["anthropic_beta"].as_array().unwrap(); + assert!(betas.iter().any(|v| v == "context-1m-2025-08-07")); + } + + #[test] + fn test_adaptive_sonnet_4_6() { + let mut body = json!({ + "model": "anthropic.claude-sonnet-4-6-20250514-v1:0", + "max_tokens": 16384, + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body["thinking"]["type"], "adaptive"); + assert!(body["thinking"].get("budget_tokens").is_none()); + assert_eq!(body["output_config"]["effort"], "max"); + let betas = body["anthropic_beta"].as_array().unwrap(); + assert!(betas.iter().any(|v| v == "context-1m-2025-08-07")); + } + + #[test] + fn test_legacy_sonnet_4_5_thinking_null() { + let mut body = json!({ + "model": "anthropic.claude-sonnet-4-5-20250514-v1:0", + "max_tokens": 16384, + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], 16383); + let betas = body["anthropic_beta"].as_array().unwrap(); + assert!(betas.iter().any(|v| v == "interleaved-thinking-2025-05-14")); + } + + #[test] + fn test_legacy_budget_too_small_upgraded() { + let mut body = json!({ + "model": "anthropic.claude-sonnet-4-5-20250514-v1:0", + "max_tokens": 16384, + "thinking": {"type": "enabled", "budget_tokens": 1024}, + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], 16383); + } + + #[test] + fn test_skip_haiku() { + let mut body = json!({ + "model": "anthropic.claude-haiku-4-5-20250514-v1:0", + "max_tokens": 8192, + "messages": [{"role": "user", "content": "hello"}] + }); + let original = body.clone(); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body, original); + } + + #[test] + fn test_thinking_optimizer_disabled() { + let mut body = json!({ + "model": "anthropic.claude-opus-4-6-20250514-v1:0", + "max_tokens": 16384, + "messages": [{"role": "user", "content": "hello"}] + }); + let original = body.clone(); + + optimize(&mut body, &disabled_config()); + + assert_eq!(body, original); + } + + #[test] + fn test_adaptive_dedup_beta() { + let mut body = json!({ + "model": "anthropic.claude-opus-4-6-20250514-v1:0", + "max_tokens": 16384, + "anthropic_beta": ["context-1m-2025-08-07"], + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + let betas = body["anthropic_beta"].as_array().unwrap(); + let count = betas + .iter() + .filter(|v| v == &&json!("context-1m-2025-08-07")) + .count(); + assert_eq!(count, 1); + } + + #[test] + fn test_legacy_disabled_thinking_injected() { + let mut body = json!({ + "model": "anthropic.claude-sonnet-4-5-20250514-v1:0", + "max_tokens": 8192, + "thinking": {"type": "disabled"}, + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], 8191); + } + + #[test] + fn test_legacy_default_max_tokens() { + let mut body = json!({ + "model": "anthropic.claude-sonnet-4-5-20250514-v1:0", + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], 16383); + } + + #[test] + fn test_append_beta_null_field() { + let mut body = json!({ + "model": "anthropic.claude-opus-4-6-20250514-v1:0", + "anthropic_beta": null, + "messages": [{"role": "user", "content": "hello"}] + }); + + optimize(&mut body, &enabled_config()); + + let betas = body["anthropic_beta"].as_array().unwrap(); + assert!(betas.iter().any(|v| v == "context-1m-2025-08-07")); + } +} diff --git a/src-tauri/src/proxy/types.rs b/src-tauri/src/proxy/types.rs index 5118c2bbe..6f5c19fe1 100644 --- a/src-tauri/src/proxy/types.rs +++ b/src-tauri/src/proxy/types.rs @@ -233,6 +233,42 @@ impl Default for RectifierConfig { } } +/// 请求优化器配置 +/// +/// 存储在 settings 表中,key = "optimizer_config" +/// 仅对 Bedrock provider 生效(CLAUDE_CODE_USE_BEDROCK = "1") +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OptimizerConfig { + /// 总开关(默认关闭,用户需手动启用) + #[serde(default)] + pub enabled: bool, + /// Thinking 优化子开关(总开关开启后默认生效) + #[serde(default = "default_true")] + pub thinking_optimizer: bool, + /// Cache 注入子开关(总开关开启后默认生效) + #[serde(default = "default_true")] + pub cache_injection: bool, + /// Cache TTL: "5m" | "1h"(默认 "1h") + #[serde(default = "default_cache_ttl")] + pub cache_ttl: String, +} + +fn default_cache_ttl() -> String { + "1h".to_string() +} + +impl Default for OptimizerConfig { + fn default() -> Self { + Self { + enabled: false, + thinking_optimizer: true, + cache_injection: true, + cache_ttl: "1h".to_string(), + } + } +} + /// 日志配置 /// /// 存储在 settings 表的 log_config 字段中(JSON 格式) diff --git a/src/components/settings/RectifierConfigPanel.tsx b/src/components/settings/RectifierConfigPanel.tsx index be75a2282..6b72c029d 100644 --- a/src/components/settings/RectifierConfigPanel.tsx +++ b/src/components/settings/RectifierConfigPanel.tsx @@ -3,7 +3,11 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; -import { settingsApi, type RectifierConfig } from "@/lib/api/settings"; +import { + settingsApi, + type RectifierConfig, + type OptimizerConfig, +} from "@/lib/api/settings"; export function RectifierConfigPanel() { const { t } = useTranslation(); @@ -12,6 +16,12 @@ export function RectifierConfigPanel() { requestThinkingSignature: true, requestThinkingBudget: true, }); + const [optimizerConfig, setOptimizerConfig] = useState({ + enabled: false, + thinkingOptimizer: true, + cacheInjection: true, + cacheTtl: "1h", + }); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -20,6 +30,10 @@ export function RectifierConfigPanel() { .then(setConfig) .catch((e) => console.error("Failed to load rectifier config:", e)) .finally(() => setIsLoading(false)); + settingsApi + .getOptimizerConfig() + .then(setOptimizerConfig) + .catch((e) => console.error("Failed to load optimizer config:", e)); }, []); const handleChange = async (updates: Partial) => { @@ -34,6 +48,18 @@ export function RectifierConfigPanel() { } }; + const handleOptimizerChange = async (updates: Partial) => { + const newConfig = { ...optimizerConfig, ...updates }; + setOptimizerConfig(newConfig); + try { + await settingsApi.setOptimizerConfig(newConfig); + } catch (e) { + console.error("Failed to save optimizer config:", e); + toast.error(String(e)); + setOptimizerConfig(optimizerConfig); + } + }; + if (isLoading) return null; return ( @@ -86,6 +112,94 @@ export function RectifierConfigPanel() { />
+ +
+
+

+ {t("settings.advanced.optimizer.title")} +

+

+ {t("settings.advanced.optimizer.description")} +

+
+ +
+
+
+ +
+ + handleOptimizerChange({ enabled: checked }) + } + /> +
+ +
+
+
+ +

+ {t( + "settings.advanced.optimizer.thinkingOptimizerDescription", + )} +

+
+ + handleOptimizerChange({ thinkingOptimizer: checked }) + } + /> +
+ +
+
+ +

+ {t("settings.advanced.optimizer.cacheInjectionDescription")} +

+
+ + handleOptimizerChange({ cacheInjection: checked }) + } + /> +
+ + {optimizerConfig.cacheInjection && ( +
+
+ +
+ +
+ )} +
+
+
); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f725f2cdf..91d736a05 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -257,6 +257,18 @@ "thinkingBudget": "Thinking Budget Rectification", "thinkingBudgetDescription": "When an Anthropic-type provider returns budget_tokens constraint errors (such as at least 1024), automatically normalizes thinking to enabled, sets thinking budget to 32000, and raises max_tokens to 64000 if needed, then retries once" }, + "optimizer": { + "title": "Bedrock Request Optimizer", + "description": "Automatically optimize Thinking and Cache configuration before sending requests (only applies to Bedrock providers)", + "enabled": "Enable Optimizer", + "thinkingOptimizer": "Thinking Optimization", + "thinkingOptimizerDescription": "Automatically enable Adaptive Thinking for Opus/Sonnet, and inject Extended Thinking for legacy models", + "cacheInjection": "Cache Injection", + "cacheInjectionDescription": "Automatically inject cache breakpoints at key positions in requests to reduce duplicate token billing", + "cacheTtl": "Cache TTL", + "cacheTtl5m": "5 minutes", + "cacheTtl1h": "1 hour" + }, "logConfig": { "title": "Log Management", "description": "Control log output level", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0440fb4aa..c995cd0fc 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -257,6 +257,18 @@ "thinkingBudget": "Thinking Budget 整流", "thinkingBudgetDescription": "Anthropic タイプのプロバイダーが budget_tokens 制約エラー(例: 1024 以上)を返した場合、thinking を enabled に正規化し、thinking 予算を 32000 に設定し、必要に応じて max_tokens を 64000 に引き上げて 1 回リトライします" }, + "optimizer": { + "title": "Bedrock リクエストオプティマイザー", + "description": "リクエスト送信前に Thinking と Cache の設定を自動最適化(Bedrock プロバイダーのみ有効)", + "enabled": "オプティマイザーを有効化", + "thinkingOptimizer": "Thinking 最適化", + "thinkingOptimizerDescription": "Opus/Sonnet に Adaptive Thinking を自動的に有効化し、レガシーモデルに Extended Thinking を注入", + "cacheInjection": "Cache 注入", + "cacheInjectionDescription": "リクエストの重要な位置に Cache ブレークポイントを自動注入し、重複トークンの課金を削減", + "cacheTtl": "Cache TTL", + "cacheTtl5m": "5 分", + "cacheTtl1h": "1 時間" + }, "logConfig": { "title": "ログ管理", "description": "ログ出力レベルを制御", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index ad99f1dba..c9a9ad8a2 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -257,6 +257,18 @@ "thinkingBudget": "Thinking Budget 整流", "thinkingBudgetDescription": "当 Anthropic 类型供应商返回 budget_tokens 约束错误(如至少 1024)时,自动将 thinking 规范为 enabled 并将 budget 设为 32000,同时在需要时将 max_tokens 设为 64000,然后重试一次" }, + "optimizer": { + "title": "Bedrock 请求优化器", + "description": "在请求发送前自动优化 Thinking 和 Cache 配置(仅 Bedrock 供应商生效)", + "enabled": "启用优化器", + "thinkingOptimizer": "Thinking 优化", + "thinkingOptimizerDescription": "自动为 Opus/Sonnet 启用 Adaptive Thinking,为旧模型注入 Extended Thinking", + "cacheInjection": "Cache 注入", + "cacheInjectionDescription": "自动在请求关键位置注入 Cache 断点,减少重复 token 计费", + "cacheTtl": "Cache TTL", + "cacheTtl5m": "5 分钟", + "cacheTtl1h": "1 小时" + }, "logConfig": { "title": "日志管理", "description": "控制日志输出级别", diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index e02e2a188..4d42c3533 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -196,6 +196,14 @@ export const settingsApi = { return await invoke("set_rectifier_config", { config }); }, + async getOptimizerConfig(): Promise { + return await invoke("get_optimizer_config"); + }, + + async setOptimizerConfig(config: OptimizerConfig): Promise { + return await invoke("set_optimizer_config", { config }); + }, + async getLogConfig(): Promise { return await invoke("get_log_config"); }, @@ -211,6 +219,13 @@ export interface RectifierConfig { requestThinkingBudget: boolean; } +export interface OptimizerConfig { + enabled: boolean; + thinkingOptimizer: boolean; + cacheInjection: boolean; + cacheTtl: string; +} + export interface LogConfig { enabled: boolean; level: "error" | "warn" | "info" | "debug" | "trace"; From a89f433ddeb5dd5cd50e6ad7c2cffbdb04e6b539 Mon Sep 17 00:00:00 2001 From: "Mr.XYS" <44491763+a1398394385@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:15:48 +0800 Subject: [PATCH 30/34] fix: fix the issue of missing token statistics for cache hits in streaming responses (#1244) --- src-tauri/src/proxy/usage/parser.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src-tauri/src/proxy/usage/parser.rs b/src-tauri/src/proxy/usage/parser.rs index 0a33d0187..f3ce1362f 100644 --- a/src-tauri/src/proxy/usage/parser.rs +++ b/src-tauri/src/proxy/usage/parser.rs @@ -109,6 +109,23 @@ impl TokenUsage { usage.input_tokens = input as u32; } } + // 从 message_delta 中处理缓存命中(cache_read_input_tokens) + if usage.cache_read_tokens == 0 { + if let Some(cache_read) = + delta_usage.get("cache_read_input_tokens").and_then(|v| v.as_u64()) + { + usage.cache_read_tokens = cache_read as u32; + } + } + // 从 message_delta 中处理缓存创建(cache_creation_input_tokens) + // 注: 现在 zhipu 没有返回 cache_creation_input_tokens 字段 + if usage.cache_creation_tokens == 0 { + if let Some(cache_creation) = + delta_usage.get("cache_creation_input_tokens").and_then(|v| v.as_u64()) + { + usage.cache_creation_tokens = cache_creation as u32; + } + } } } _ => {} From 078a81b86724335b9ff1c88e113d7c4563209693 Mon Sep 17 00:00:00 2001 From: Bowen Han Date: Sat, 7 Mar 2026 05:16:43 -0800 Subject: [PATCH 31/34] feat: add extra field display in UsageFooter component for normal mode (#1137) --- src/components/UsageFooter.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/UsageFooter.tsx b/src/components/UsageFooter.tsx index fc4b0ac2e..fff72dcff 100644 --- a/src/components/UsageFooter.tsx +++ b/src/components/UsageFooter.tsx @@ -180,6 +180,13 @@ const UsageFooter: React.FC = ({ {firstUsage.unit} )} + + {/* 扩展字段 extra */} + {firstUsage.extra && ( + + {firstUsage.extra} + + )}
); From 95a74020e10353c94f7ae47187b000bfe3304d37 Mon Sep 17 00:00:00 2001 From: Sube <53138073+Sube-py@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:17:33 +0800 Subject: [PATCH 32/34] fix: align outline button text tone with usage refresh control (#1222) Co-authored-by: zhangluguang --- src/components/ui/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 31c9f0da8..415ecd5e3 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -16,7 +16,7 @@ const buttonVariants = cva( "bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700", // 轮廓按钮 outline: - "border border-border-default bg-background hover:bg-gray-100 hover:border-border-hover dark:hover:bg-gray-800", + "border border-border-default bg-background text-muted-foreground hover:bg-gray-100 hover:text-gray-900 hover:border-border-hover dark:hover:bg-gray-800 dark:hover:text-gray-100", // 次按钮:灰色(对应旧版 secondary) secondary: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200", From 33d5f6985d9a7754d4df4811106532ba499fc40f Mon Sep 17 00:00:00 2001 From: Fan <53250672+fzzv@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:59:33 +0800 Subject: [PATCH 33/34] fix: skills count not displaying when adding (#1295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: skills count not displaying when adding * fix: get real skill count by awaiting discovery after adding repo The previous approach computed count before discovery, so it was always 0. Now we await refetchDiscoverable() after the mutation, then filter the fresh skills list to get the actual count for the toast message. Also reverts the SkillRepo.count field — keep the interface clean since count is a transient UI concern, not a data model property. --------- Co-authored-by: Jason --- src/components/skills/SkillsPage.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/skills/SkillsPage.tsx b/src/components/skills/SkillsPage.tsx index 3fa2c3f5e..0bdcd3b4c 100644 --- a/src/components/skills/SkillsPage.tsx +++ b/src/components/skills/SkillsPage.tsx @@ -164,10 +164,20 @@ export const SkillsPage = forwardRef( const handleAddRepo = async (repo: SkillRepo) => { try { await addRepoMutation.mutateAsync(repo); + // Await discovery so we can report the real count + const { data: freshSkills } = await refetchDiscoverable(); + const count = + freshSkills?.filter( + (s) => + s.repoOwner === repo.owner && + s.repoName === repo.name && + (s.repoBranch || "main") === (repo.branch || "main"), + ).length ?? 0; toast.success( t("skills.repo.addSuccess", { owner: repo.owner, name: repo.name, + count, }), { closeButton: true }, ); From fb8996d19c8be352d6ce99573153b0f801c9d021 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Sat, 7 Mar 2026 22:53:44 +0800 Subject: [PATCH 34/34] fix:Add a new vendor page, API endpoint, and model name. (#1155) * fix:Add a new vendor page, API endpoint, and model name. Fix the bug where, after entering characters, line breaks cannot be fully deleted. * fix: add missing i18n key codexConfig.modelNameHint for zh/en/ja --------- Co-authored-by: Jason --- .../providers/forms/CodexFormFields.tsx | 10 +++- .../providers/forms/hooks/useBaseUrlState.ts | 8 +-- .../forms/hooks/useCodexConfigState.ts | 20 ++----- src/i18n/locales/en.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/zh.json | 3 +- src/utils/providerConfigUtils.ts | 33 ++++++++--- tests/utils/providerConfigUtils.codex.test.ts | 55 +++++++++++++++++++ 8 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 tests/utils/providerConfigUtils.codex.test.ts diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index c527b991f..99d8aa7be 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -117,9 +117,13 @@ export function CodexFormFields({ className="w-full px-3 py-2 border border-border-default bg-background text-foreground rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 transition-colors" />

- {t("codexConfig.modelNameHint", { - defaultValue: "指定使用的模型,将自动更新到 config.toml 中", - })} + {modelName.trim() + ? t("codexConfig.modelNameHint", { + defaultValue: "指定使用的模型,将自动更新到 config.toml 中", + }) + : t("providerForm.modelHint", { + defaultValue: "💡 留空将使用供应商的默认模型", + })}

)} diff --git a/src/components/providers/forms/hooks/useBaseUrlState.ts b/src/components/providers/forms/hooks/useBaseUrlState.ts index 037dfeb3b..2944dfdf3 100644 --- a/src/components/providers/forms/hooks/useBaseUrlState.ts +++ b/src/components/providers/forms/hooks/useBaseUrlState.ts @@ -60,10 +60,8 @@ export function useBaseUrlState({ if (!codexConfig) return; const extracted = extractCodexBaseUrl(codexConfig) || ""; - if (extracted !== codexBaseUrl) { - setCodexBaseUrl(extracted); - } - }, [appType, category, codexConfig, codexBaseUrl]); + setCodexBaseUrl((prev) => (prev === extracted ? prev : extracted)); + }, [appType, category, codexConfig]); // 从Claude配置同步到 state(Gemini) useEffect(() => { @@ -116,7 +114,7 @@ export function useBaseUrlState({ const sanitized = url.trim(); setCodexBaseUrl(sanitized); - if (!sanitized || !onCodexConfigChange) { + if (!onCodexConfigChange) { return; } diff --git a/src/components/providers/forms/hooks/useCodexConfigState.ts b/src/components/providers/forms/hooks/useCodexConfigState.ts index a4271c067..1747d6c12 100644 --- a/src/components/providers/forms/hooks/useCodexConfigState.ts +++ b/src/components/providers/forms/hooks/useCodexConfigState.ts @@ -74,10 +74,8 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { return; } const extracted = extractCodexBaseUrl(codexConfig) || ""; - if (extracted !== codexBaseUrl) { - setCodexBaseUrl(extracted); - } - }, [codexConfig, codexBaseUrl]); + setCodexBaseUrl((prev) => (prev === extracted ? prev : extracted)); + }, [codexConfig]); // 与 TOML 配置保持模型名称同步 useEffect(() => { @@ -85,10 +83,8 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { return; } const extracted = extractCodexModelName(codexConfig) || ""; - if (extracted !== codexModelName) { - setCodexModelName(extracted); - } - }, [codexConfig, codexModelName]); + setCodexModelName((prev) => (prev === extracted ? prev : extracted)); + }, [codexConfig]); // 获取 API Key(从 auth JSON) const getCodexAuthApiKey = useCallback((authString: string): string => { @@ -165,10 +161,6 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { const sanitized = url.trim(); setCodexBaseUrl(sanitized); - if (!sanitized) { - return; - } - isUpdatingCodexBaseUrlRef.current = true; setCodexConfig((prev) => setCodexBaseUrlInConfig(prev, sanitized)); setTimeout(() => { @@ -184,10 +176,6 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { const trimmed = modelName.trim(); setCodexModelName(trimmed); - if (!trimmed) { - return; - } - isUpdatingCodexModelNameRef.current = true; setCodexConfig((prev) => setCodexModelNameInConfig(prev, trimmed)); setTimeout(() => { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 91d736a05..b434a149a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -775,7 +775,8 @@ "extractFromCurrent": "Extract from Editor", "extractNoCommonConfig": "No common config available to extract from editor", "extractFailed": "Extract failed: {{error}}", - "saveFailed": "Save failed: {{error}}" + "saveFailed": "Save failed: {{error}}", + "modelNameHint": "Specify the model to use, will be auto-updated in config.toml" }, "geminiConfig": { "envFile": "Environment Variables (.env)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index c995cd0fc..bd765e12c 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -775,7 +775,8 @@ "extractFromCurrent": "編集内容から抽出", "extractNoCommonConfig": "編集内容から抽出できる共通設定がありません", "extractFailed": "抽出に失敗しました: {{error}}", - "saveFailed": "保存に失敗しました: {{error}}" + "saveFailed": "保存に失敗しました: {{error}}", + "modelNameHint": "使用するモデルを指定します。config.toml に自動更新されます" }, "geminiConfig": { "envFile": "環境変数 (.env)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index c9a9ad8a2..9cb2e0d40 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -775,7 +775,8 @@ "extractFromCurrent": "从编辑内容提取", "extractNoCommonConfig": "当前编辑内容没有可提取的通用配置", "extractFailed": "提取失败: {{error}}", - "saveFailed": "保存失败: {{error}}" + "saveFailed": "保存失败: {{error}}", + "modelNameHint": "指定使用的模型,将自动更新到 config.toml 中" }, "geminiConfig": { "envFile": "环境变量 (.env)", diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index fc4b46844..ba685f076 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -447,12 +447,23 @@ export const setCodexBaseUrl = ( baseUrl: string, ): string => { const trimmed = baseUrl.trim(); - if (!trimmed) { - return configText; - } // 归一化原文本中的引号(既能匹配,也能输出稳定格式) const normalizedText = normalizeQuotes(configText); + // 允许清空:当 baseUrl 为空时,移除 base_url 行 + if (!trimmed) { + if (!normalizedText) return normalizedText; + const next = normalizedText + .split("\n") + .filter((line) => !/^\s*base_url\s*=/.test(line)) + .join("\n") + // 避免移除后留下过多空行 + .replace(/\n{3,}/g, "\n\n") + // 避免开头出现空行 + .replace(/^\n+/, ""); + return next; + } + const normalizedUrl = trimmed.replace(/\s+/g, ""); const replacementLine = `base_url = "${normalizedUrl}"`; const pattern = /base_url\s*=\s*(["'])([^"']+)\1/; @@ -494,13 +505,21 @@ export const setCodexModelName = ( modelName: string, ): string => { const trimmed = modelName.trim(); - if (!trimmed) { - return configText; - } - // 归一化原文本中的引号(既能匹配,也能输出稳定格式) const normalizedText = normalizeQuotes(configText); + // 允许清空:当 modelName 为空时,移除 model 行 + if (!trimmed) { + if (!normalizedText) return normalizedText; + const next = normalizedText + .split("\n") + .filter((line) => !/^\s*model\s*=/.test(line)) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .replace(/^\n+/, ""); + return next; + } + const replacementLine = `model = "${trimmed}"`; const pattern = /^model\s*=\s*["']([^"']+)["']/m; diff --git a/tests/utils/providerConfigUtils.codex.test.ts b/tests/utils/providerConfigUtils.codex.test.ts new file mode 100644 index 000000000..13dfa2ae7 --- /dev/null +++ b/tests/utils/providerConfigUtils.codex.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { + extractCodexBaseUrl, + extractCodexModelName, + setCodexBaseUrl, + setCodexModelName, +} from "@/utils/providerConfigUtils"; + +describe("Codex TOML utils", () => { + it("removes base_url line when set to empty", () => { + const input = [ + 'model_provider = "openai"', + 'base_url = "https://api.example.com/v1"', + 'model = "gpt-5-codex"', + "", + ].join("\n"); + + const output = setCodexBaseUrl(input, ""); + + expect(output).not.toMatch(/^\s*base_url\s*=/m); + expect(extractCodexBaseUrl(output)).toBeUndefined(); + expect(extractCodexModelName(output)).toBe("gpt-5-codex"); + }); + + it("removes model line when set to empty", () => { + const input = [ + 'model_provider = "openai"', + 'base_url = "https://api.example.com/v1"', + 'model = "gpt-5-codex"', + "", + ].join("\n"); + + const output = setCodexModelName(input, ""); + + expect(output).not.toMatch(/^\s*model\s*=/m); + expect(extractCodexModelName(output)).toBeUndefined(); + expect(extractCodexBaseUrl(output)).toBe("https://api.example.com/v1"); + }); + + it("updates existing values when non-empty", () => { + const input = [ + 'model_provider = "openai"', + "base_url = 'https://old.example/v1'", + 'model = "old-model"', + "", + ].join("\n"); + + const output1 = setCodexBaseUrl(input, " https://new.example/v1 \n"); + expect(extractCodexBaseUrl(output1)).toBe("https://new.example/v1"); + + const output2 = setCodexModelName(output1, " new-model \n"); + expect(extractCodexModelName(output2)).toBe("new-model"); + }); +}); +