切换之后配置少了一段:CC Switch 通用配置片段的提取与回填

2026-08-10

有一类现象在 CC Switch 的仓库里能找到完整解释:切换供应商之后,你把这个供应商在 CC Switch 里保存的配置,和切换前 ~/.claude 那份 live 配置逐字比对,会发现少了一段——那段往往正是你自己在 CLI 里加的东西(装的插件、加的 hook、改的偏好)。

它不是被删了,而是被搬走了。源码里管这个动作叫「通用配置片段」(common config snippet)的提取与回填,位置在 src-tauri/src/services/provider/mod.rs。这篇把这条路径按行号拆开,并给出你自己怎么核对的动作。

以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码与文档文本,没有安装也没有运行过这个桌面应用

切换那一刻,回填先于写入

供应商切换的入口是 ProviderService::switchsrc-tauri/src/services/provider/mod.rs:2966)。走到普通路径 switch_normal:3057 起)之后,第一件事不是写新配置,而是回填旧配置

这段回填代码在 :3089-3134,顺序是三步:

  1. get_effective_current_provider 拿到当前供应商,并且只在「切到的 id 与当前 id 不同」且「不是累加模式应用」时才继续;
  2. 读 live 配置,调 sync_common_config_snippet_from_live——把 live 里可共享的改动同步进通用配置片段。代码注释写得很直白:「切走前先把 live 里的可共享改动(含用户直接在应用内装插件/加 hook/改偏好)同步进通用配置片段,再做剥离回填」;
  3. strip_common_config_from_live_settings,把通用片段剥离掉之后的剩余部分写回该供应商并落库。保存失败会打一条 warning,并往结果里塞 backfill_failed:{id}

到这一步才轮到写新配置:write_live_with_common_config(db, app_type, provider):3146),它先 build_effective_settings_with_common_config 合成有效配置再落盘(src-tauri/src/services/provider/live.rs:698-718)。

把这两头连起来看,反直觉的那一处就出来了:共享的那一段并没有留在任何一个供应商身上,它被抽到了一个跨供应商的位置,等到写 live 的时候再合回去。 所以你去看单个供应商的配置,它是「剥掉共享部分之后的残余」,本来就不该和 live 文件长得一样。你要找的那段插件配置,应该去通用配置片段里找,不是去供应商配置里找。

这也解释了为什么这段逻辑必须发生在切走之前:如果先写新供应商的 live,你在旧供应商期间加的那些改动就没机会被采集了。

哪些键会被剥掉:三份名单

extract_common_config_snippet 的职责就是「取当前供应商配置,去掉供应商专属字段(API key、模型、端点)生成可复用片段」(:3464-3492)。真正决定「什么算供应商专属」的,是下面三份名单。

第一份是机密键判定 is_sensitive_config_key:3512-3578)。 它由三组模式构成:SENSITIVE_SUFFIXES 17 条后缀、SENSITIVE_EXACT 6 条精确名、SENSITIVE_CONTAINS 6 条包含式,任一命中即判为机密,一律不进共享片段。

这里的取舍在注释里写死了,值得原样引一遍:「故意从严——多剥一个非机密键只是它不被共享(可恢复的小不便),漏剥一个凭据则会把密钥注入到每个供应商(不可恢复的泄漏)。」 也就是说,宁可误伤,不可漏网。这条取向直接决定了共享片段的边界:凡是名字命中上述模式的环境变量,都不会进入通用配置片段——按代码语义,这是有意为之,不是缺陷。

从严之后还得处理误伤,注释里点了三处边界:

  • 单数 _TOKEN 命中,但误伤复数 _TOKENS——因为 CLAUDE_CODE_MAX_OUTPUT_TOKENSMAX_THINKING_TOKENS 是正常可共享的配置;
  • _PASS 不误伤以 _BYPASS 结尾的键;
  • _PWD 不误伤 shell 的 PWD / OLDPWD

第二份是 Claude 侧的 ENV_PROVIDER_SPECIFIC_EXCLUDES,14 条(:3585-3607)。 这些键不是机密,但同样属于供应商专属,内容是模型系列相关的一批键、ANTHROPIC_BASE_URL,再加两个上下文上限键。其中 ANTHROPIC_DEFAULT_FABLE_MODEL 那两条带了一句注释:Fable 是 v3.16.3 新增的第四档模型映射,与其它档同属供应商专属,不得进入通用配置片段,否则会污染其它供应商(issue #4272)。这条注释本身就是这套机制存在意义的说明——模型映射跨供应商共享就是污染

第三份是 TOP_LEVEL_EXCLUDES,只有 3 条(:3609-3614apiBaseUrl,以及两个 legacy 模型字段 primaryModel / smallFastModel

Codex 侧另有一处单点处理:提取时会从 root 移除顶层的 experimental_bearer_token,注释说明正常写法应该在 [model_providers.<id>] 段内(:3708-3711)。另外还有一个防泄漏兜底函数 scrub_leaked_gemini_common_config:3812)。这两处放在一起看,思路是一致的:凭据能不进共享层就不进,进了也要有地方兜。

三个应用根本不产出通用片段

extract_common_config_snippet 是按 app_type 分派的(:3464-3492),这张对照表建议照着核一遍:

应用提取行为
Claude有专用提取函数
Codex有专用提取函数
Gemini有专用提取函数
OpenCode有专用提取函数
OpenClaw有专用提取函数
ClaudeDesktop直接返回空字符串
GrokBuild直接返回空字符串
Hermes直接返回空字符串(注释:doesn’t use common config snippets)

这张表的读法是:后三个应用不存在「通用配置片段」这回事,所以如果你用的是 Claude Desktop、Grok Build 或 Hermes,本文这条路径就不是首要怀疑对象。这里要补一句边界:ClaudeDesktop 与 GrokBuild 仍是非累加模式,切换时照样会走到下面说的回填分支,那一段对这两个应用是否等于空操作,我们没有核实。

还有一处边界值得单独指出来,因为它和上面这张表不重合:回填只在非累加模式下发生,而 AppType::is_additive_mode() 返回 true 的是 OpenCode / OpenClaw / Hermes 三个(src-tauri/src/app_config.rs:404-409)。也就是说 OpenCode 与 OpenClaw 有专用的提取函数,但它们在切换时走不到那个回填分支——累加模式下所有供应商共存于同一个 live 文件,没有「当前供应商」的概念,自然也不需要「切走前把 live 采集回去」。两条边界的交集只有 Hermes:它既不产出片段,也不走回填。

顺带一提,同一文件里还有另一个入口 extract_common_config_snippet_from_settings:3494-3510),分派规则一样,区别是输入换成任意配置值(比如编辑器里的内容)而不是当前供应商。以及两个 legacy 迁移函数 migrate_legacy_common_config_usage:3294)与 migrate_legacy_common_config_usage_if_needed:3348),说明这套机制经历过口径变更。

怎么确认是这个原因

按这个顺序查,四步:

第一步,确认应用类型。 你用的是 Claude Desktop / Grok Build / Hermes 中的一个吗?是的话到此为止,不是这条路径。

第二步,确认走的是普通路径还是热切换路径。 切换时会先判断 should_hot_switchis_app_taken_over(数据库里存在 live 备份)或 live_taken_over(live 配置里检出接管痕迹)任一为真即成立(:3005-3016)。热切换走 hot_switch_provider_inner不还原上游 live 配置,由代理层按 is_current 路由,直接返回 SwitchResult::default():3033-3053)——它压根不经过 switch_normal 的回填段。所以本地代理接管开着的时候,你观察到的行为和本文讲的不是一条路。

第三步,比对两处内容。 拿该供应商在 CC Switch 里保存的配置,和你切换前的 live 文件比对,看少掉的那些键名,逐个对照上面三份名单:命中 is_sensitive_config_key 的模式(17 条后缀 / 6 条精确名 / 6 条包含式),或者落在 Claude 那 14 条 ENV_PROVIDER_SPECIFIC_EXCLUDES 与 3 条 TOP_LEVEL_EXCLUDES 里的,都是被有意剥掉的。

第四步,看有没有 backfill_failed:{id} 回填保存失败时会打 warning 并追加这个标记(:3089-3134)。如果出现了它,那就不是「被剥离」,而是回填这一步本身没成功——两回事。

什么情况说明不是这个原因: 你切的是同一个供应商(id 相同则整段回填不执行);应用是累加模式的三个之一;当前是热切换路径;或者 live 文件本身读取失败(读 live 配置这一步失败时,整段回填不执行)。这几种情形下配置对不上,得另找原因。

两句必须说清的话

第一,这套机制处理的是本机敏感数据。CC Switch 读写 ~/.claude~/.codex 等真实 CLI 配置文件,并在本机保存 API Key。is_sensitive_config_key 那句「故意从严」的注释说明作者在这件事上留了余量,但这不等于「配好就安全了」——本文只描述代码的判定逻辑,不对任何配置方式的安全性下结论。

第二,别把「共享」理解成「同步到别处」。后端 Provider 结构体上的 meta 字段有一句注释:不写入 live 配置,仅存于 ~/.cc-switch/config.json;同一处还写着「SSOT 模式:不再写供应商副本文件」(src-tauri/src/provider.rs:7-43)。整套设计的落点都在本机那一份单一事实源上。

最后按纪律收一句:本文列出的行号与名单条数,都是 2026-08-10 我们读到的 c39c903 快照的状态。这类内部函数的位置与名单内容随版本变动,你要复现就以自己 clone 出来的那一份为准——能复用的是「先查应用类型、再查切换路径、最后比对三份名单」这个顺序,不是具体的行号。


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、 src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。 本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用, 因此不涉及界面外观、操作手感与切换速度的任何描述。 文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。 该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。