CC Switch 编辑框显示的密钥不是当前生效的

2026-08-31

有一类报错最难查:错误文案和根因中间隔着三层。你看到的是「model xxx not found」, 第一反应会去查模型名拼错没有、供应商支不支持这个模型、是不是模型下拉里选错了。 而在 v3.20.1 之前的 CC Switch 上,这条报错有一个完全不在模型这条线上的成因—— 你编辑某张 Codex 供应商卡时,表单里填着的密钥根本不是这张卡自己的, 保存一次,错的密钥就被固化进了数据库记录。反复几次,共享同一个 Base URL 的几张卡会开始密钥互相趋同。

这条问题在 v3.20.1 里有对应的修复(PR #6534,修复 issue #6414), 下面按「症状 → 判定 → 处置 → 验证 → 排除」的顺序拆一遍。

本文的口径

本文所有结论来自静态阅读仓库源码与仓库内的 docs/,核对日 2026-08-31, 对应快照 3217f725(仓库内版本号 3.20.1)。我们没有安装、编译或运行过这个桌面应用, 所以下文不会出现任何关于界面长相、按钮位置或操作快慢的描述—— 凡是提到「表单里显示什么」,依据都是发布说明的原文或代码里算出初始值的那几行。 文中的 ~/.codex/ 按各平台的用户主目录理解即可,具体解析规则以该 CLI 自身的文档为准。

症状:文档是怎么描述这件事的

docs/release-notes/v3.20.1-zh.md:110 有一整节写这件事,措辞可以直接当症状定义用: 开启官方登录保留时,auth.json 是一个没有供应商身份的共享槽位, 而编辑框播种表单时曾优先读它——编辑活跃的 Codex 供应商可能显示、并在保存时固化另一张卡遗留的密钥, 让共享同一 Base URL 的卡密钥互相趋同,最终表现为「model not found」。

同一份文件在 :14 的「你现在可以」清单里给了更短的一句:在编辑框里看到这张卡自己的密钥。 两处描述一致,且 PR #6534 的提交说明里用的也是同一个措辞——single-slot、无供应商身份。

关键在于「共享槽位」这四个字。~/.codex/auth.json 是被托管的 Codex CLI 自己的凭据文件, 它只有一个位置放密钥,文件里没有任何字段说明这个密钥属于哪张供应商卡。 CC Switch 在这台机器上同时管着多张卡,但落到那个文件时它们共用同一个格子。 一个没有所有权标记的共享可变状态,被当成 per-provider 的东西读——这就是全部故事。

定位:两个优先级叠在一起

有两处代码叠加才让症状浮出来,都能实读。

第一处是表单初始值的基底。src/components/providers/EditProviderDialog.tsx 打开编辑时会读一次 live 配置(只读一次::138 声明的 hasLoadedLive 布尔量,在 :163 处作为早退守卫, 挡住后续 re-render 覆盖用户正在编辑的内容,那一行上面的注释写的是「关键修复:只在首次打开时加载一次」), 然后把 live 当作表单基底。live 的 auth 部分,来源正是那个共享的 auth.json

第二处是密钥字段的取值顺序。src/components/providers/forms/hooks/useCodexConfigState.ts 里的 pickCodexApiKey 决定表单最终显示哪个值,它的次序是 auth.OPENAI_API_KEY、后 config.toml 里的 experimental_bearer_token。 这个回退次序本身是为「保留官方登录、同时把请求路由到第三方」那套用法准备的: 按仓库 docs/ 里的说明,开关打开后走的是 config-only 写入路径—— auth.json 原样保留官方登录缓存,第三方的模型、endpoint、model_providerexperimental_bearer_token 全部写进 config.toml。 所以「auth 里有 key 就先用 auth」是一个有道理的默认。 问题在于这个次序对「从数据库快照或预设播种」是对的,对「从共享 live 文件播种」就翻车了: auth.json 里那个格子非空,于是它赢;而它非空并不代表它属于当前这张卡。

两处合起来,就得到了那条隔着三层的因果链: 共享单槽文件 → 表单读到别张卡的密钥 → 保存写回数据库 → 多张卡密钥趋同 → 请求打到不匹配的上游 → model not found。

怎么确认是这个问题

三个判定动作,都不需要跑任何东西。

第一,确认自己处在会触发的模式里。 这条只在「保留官方登录」这套用法下成立。 设置项名字是 preserveCodexOfficialAuthOnSwitch, 在 src/components/settings/CodexAuthSettings.tsx:98-106 可以看到它的开关接线 (同一个组件里紧邻的 :108-114 是另一个开关「统一会话历史」,别看串了), 表单状态那一侧对应的是 src/hooks/useSettingsForm.ts 里的同名字段。 没开这个开关,本文说的路径基本不成立。

第二,比对两个文件里的密钥是不是同一个。 打开 ~/.codex/config.toml, 找当前这张卡对应的 [model_providers.*] 表里的 experimental_bearer_token; 再打开 ~/.codex/auth.jsonOPENAI_API_KEY两者不一致,就是本文这个形状。 Windows 上这两个文件都在当前用户主目录下的 .codex 目录里, config.toml 是 TOML、auth.json 是 JSON,用任意文本编辑器打开即可;不要在这一步改动它们。

第三,确认版本。 这套「从 config.toml 重建密钥」的逻辑是 v3.20.1 才有的: 把两个版本的源码放一起 diff 能确认 reconcileCodexLiveAuth 这个函数在 v3.19.2 快照中并不存在。 所以如果你手上是 v3.19.2 一线的版本,症状会在;v3.20.1 已经改成了下一节的做法。

处置:v3.20.1 的做法

修复落在前端,两个文件。PR #6534 的提交历史本身也值得一提: 它先做了一版后端修法,随后一条 Revert 把那版撤了,最终落地的是前端修法, 末尾还有一条把改动收窄到「live 编辑」的子提交。 所以读这条提交的说明时要留意自己读的是哪一段——同一条 commit 里同时保留着两套相反的论证。

核心是 EditProviderDialog.tsx:56 新增的 reconcileCodexLiveAuth,逻辑是三段判断加两道守卫:

  1. category === "official" 直接返回 live,官方卡不动。
  2. 从 live 的 config 文本里用 extractCodexExperimentalBearerToken (定义在 src/utils/providerConfigUtils.ts:1109)取出这张卡自己的 bearer token; 取不到就原样返回 live——这一条让默认模式下整个函数是个 no-op。
  3. auth 模板优先取数据库里存的 provider.settingsConfig.auth,而不是 live 的 auth, 然后只把 OPENAI_API_KEY 换成上一步取到的 bearer。

第二道守卫写在 :73-80,是这个函数里最值得读的几行:

// Match should_restore_codex_provider_token_for_backfill: an OAuth-only
// provider must not be silently converted into an API-key provider.
if (hasOauthLogin && !hasProviderApiKey) return liveSettings;

判据是:auth 模板里除 auth_modeOPENAI_API_KEY 之外还有非空字段(说明存在 OAuth 材料), 而这张卡自己又没有 API key——这种形态一律不替换。 注释明说这是在和后端的 should_restore_codex_provider_token_for_backfill 对齐, 两侧用同一条判据,避免前端悄悄把一张纯 OAuth 的卡改写成 API-key 卡。

调用点在 :242-250:只有 appId === "codex" 且确实拿到了 liveSettings 时才走 reconcile, 数据库快照与预设两条路径保持原来的 auth 优先次序,一个字没改。 配套改动在 useCodexConfigState.ts 的编辑模式初始化里(:118 附近): 把「设置 auth.json」的赋值挪到「设置 config.toml」之后,是纯顺序调整。

处置后怎么验证

围绕这条修复的测试用例分布在两个文件里,它们守住的行为就是这条修复的验收口径, 比任何描述都更直接地说明「改完之后应该是什么行为」:

测试文件守住的行为
tests/components/EditProviderDialog.test.tsx对话框这一侧:密钥取自这张卡自己的 bearer、auth 模板取自数据库;纯 OAuth 的卡不被悄悄改写成 API-key 卡;这套 reconcile 只作用在活跃卡的 live 编辑上
tests/hooks/useCodexConfigState.bearer.test.ts取值这一侧:pickCodexApiKey 的优先级组合,提交说明称是四个回归用例

第二行那个文件是随这条修复一起加进仓库的;第一行的用例增量则落在对话框已有的测试文件里。 表格第一行最后那半句,对应的正是提交序列末尾「把 bearer reconcile 收窄到 live 编辑」那一条子提交—— 同一件事的代码面与测试面。

自己这一侧的验证动作,回到上一节第二步:改完之后再比对一次 config.toml 里该卡的 experimental_bearer_token 与数据库里这张卡记的密钥是否一致。 需要说明的是,发布说明写明了本版起每次第三方切换都会把密钥写进供应商自己的 [model_providers.*] 表,所以「这张卡有没有自己的 bearer」在新版里会逐步变成常态。

什么情况说明不是这个原因

这一节比上面几节更有用——排掉不是它,才不至于顺着错误的方向修一整天。

你的卡是官方类。 category === "official" 在函数第一行就被放行了,走不到 reconcile。 官方卡的另一类串账问题是另一条修复,成因在代理接管的恢复备份上,与编辑表单无关。

你的卡是纯 OAuth 形态。 命中 :73-80 那道守卫,同样原样返回 live。

config.toml 里这张卡没有自己的 bearer token。 发布说明对这种形态给了明确交代: 旧版或手工维护出来的卡保持原有行为、继续读取 live auth.json(含手工修改)。 代码侧对应的就是「取不到 bearer 直接 return」那一步。 换句话说,如果你比对下来 config.toml 里压根没有这一项,那你的问题不在本文这条链上。

你编辑的不是当前活跃的那张卡。 reconcile 的前提是 :242-250 那里确实拿到了 liveSettings, 非活跃的卡走不进这条路径;提交序列末尾那条「收窄到 live 编辑」的子提交与对应测试, 守的就是这个边界。

你的症状是「保存了但没生效」,不是「显示错了」。 这是方向相反的另一类问题: docs/release-notes/v3.20.1-zh.md:104-106 那一节(标题在 :104、正文在 :106) 描述的是崩溃残留的接管备份行让活跃供应商的编辑 只更新数据库、真正的配置文件纹丝不动(PR #6779)。 本文这条是「显示的值错了并被保存」,那条是「值对了但没落到 live」,判定动作完全不同。

你看到的是模型映射表变空。 那对应 EditProviderDialog.tsx:253-269 那段 modelCatalog 保护要解决的问题:modelCatalog 的权威数据在数据库, live 的 config.toml 只投影一个指针,投影丢失时若放任 live 覆盖, 保存会连同数据库里的映射一起清空。它和密钥不是一条线。

你在代理接管态下编辑。 EditProviderDialog.tsx 里有一条提前返回, 接管态下直接回退数据库这一份权威数据,注释写明理由是读 live 会让编辑界面展示代理地址或占位符。 另外 opencodepi 以及走专用 API 的 openclaw 也各有自己的提前返回分支,都不进这条 live 路径。

一句话收尾

把这条问题抽象一层:一个被多方共享的可变槽位,如果没有所有权标记, 读它的人迟早会把别人的东西当成自己的。 auth.json 只有一个格子、格子里没写归属,于是「编辑表单读到别张卡的密钥」 和「多张卡密钥互相趋同」是同一件事的两个断面。 v3.20.1 的修法没有去改那个文件的格式,而是换了一个有身份的数据源—— config.toml 里每张卡自己的 [model_providers.*] 表——来重建密钥。 这也是这类问题通用的解法方向:不是把共享状态洗干净,而是别再拿它当身份凭据用。


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

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

留言讨论

评论发布后会被人工复核,违规内容将被删除。

    还没有人评论,来说说你的看法

    如果发表没有反应,可以前往联系我们告诉我们。

    在 CC Switch 里加一个国内直连的供应商

    力达云网关,注册送 ¥5 额度,一期提供 DeepSeek。

    去添加

    这个页面有问题?

    提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。