CC Switch 绑多个官方账号,怎么不切一次串一次

2026-08-31

同一个人手上有两个以上 Codex 官方账号并不罕见:一个是自己的,一个是团队的。麻烦在于 Codex CLI 的官方登录只有一份落脚点——~/.codex/auth.json 里的那份令牌。谁写进去,裸跑 codex 时就以谁的身份运行。一个想同时管住多张官方卡的工具,第一件要回答的事不是「怎么切」,而是「切完之后,怎么保证这一次的请求确实是以我选的那个账号发出去的」。

CC Switch 在 v3.20.0 把这块重做了一遍:官方卡可以逐张绑定账号、多张共存,同时在代理层加了一道账号一致性校验。下面把这次改动的前后差别、判据落在哪一行、以及为此主动放弃了什么,逐条摊开。

这篇是怎么核的

依据是 cc-switch 仓库的 3217f725 快照(仓库内版本号 3.20.1),核对日 2026-08-31,对比基线是 v3.19.2。文中引用的发布说明是 docs/release-notes/v3.20.0-zh.md,源码位置全部在 v3.20.1 快照上重新对过一遍。需要说清楚的是:我们只静态读源码和文档,没有编译、没有运行、也没有安装过这个桌面应用,所以下面不会出现任何关于界面长什么样、点哪里、切得快不快的描述。凡是发布说明的说法与源码实读对不上的地方,会分别标出来。

两种官方卡,差别在「写不写 auth.json」

发布说明(v3.20.0-zh.md:13,78)把官方卡分成了两类,这张表是理解后面一切的前提:

绑定账号的官方卡不绑定的空官方卡(旧方式)
建卡方式新建 Codex 官方供应商时,从已完成 OAuth 授权的账号里直接选定添加一张不绑定账号的空官方卡
认证来源CC Switch 认证中心里持有的该账号令牌跟随 Codex CLI 自己的登录,读取本地 access token
切换时写什么把该账号完整的令牌包写入 ~/.codex/auth.json不写
codex CLI 的行为也以该账号运行,且能在访问令牌过期后自行续期用 CLI 自己登录的账号
数量限制不限,两种卡可以共存不限

表里最容易被忽略的是那个「不写」。空官方卡是「跟随」语义,不往 auth.json 里放东西;绑定卡是「写入」语义,切过去就意味着覆盖那份令牌。两种语义共存,是后面所有边界条件的来源。

还有一句配套机制值得单独拎出来(v3.20.0-zh.md:78):CLI 轮转过的刷新令牌会在每次写入前被回采。意思是写自己那份令牌包之前,先把 CLI 可能已经自行轮转过的 refresh token 读回来。少了这一步,「切走再切回」就会用旧的登录材料盖掉 CLI 在这期间续期得到的新材料。文档还写了绑定与解绑会保留卡的身份、端点与健康历史——也就是说,绑定是卡的一个属性,不是卡的身份。

另外提醒一句:~/.codex/ 里的令牌包是本机上的真实凭据,这个工具读写的也是你自己的这些目录。涉及它们的操作都要按敏感数据对待,本文不对安全性作任何保证性表述。

变更三件套:一个「允许多张」的提交,主要动作是删代码

版本追更最怕把发布说明翻译一遍,所以直接看提交。这次主线挂了四个 PR,发布说明分散在三处(v3.20.0-zh.md:13,80,118):

PR提交diff 规模
#3879a2e22f33 Add managed OAuth account selection for providers54 files changed, +11601 / −993
#65350455a92c fix(codex): allow multiple follow-login providers21 files changed, +829 / −1195
#6506f62c854a fix(codex): cancel stale device login after clear
#6537897ca892 fix(codex): make OAuth usage queries configurable

#6535 是这四条里最值得看的一条:标题写的是「允许多张跟随登录的供应商」,而它的净效果是删掉 366 行,其中 src-tauri/src/services/provider/mod.rs 一个文件就是一处 816 行、绝大部分为删除的改动。一个增加能力的提交以删代码为主,通常说明被去掉的限制原本是靠一批特判撑着的——去掉限制等于把特判删掉、让路径回归通用。这是「把限制写成特判」和「把能力写成通用路径」两种做法之间的差价,在 diff 里看得最直白。

#3879 按目录聚合,触及 src/components 9 处、src-tauri/src/proxy 5 处、src-tauri/src/commands 5 处、src/i18n 4 处、src-tauri/src/services 3 处、src/utils 2 处、src/lib 2 处,外加 tray.rsstore.rslib.rsdatabasecodex_config.rs 各 1 处。proxytray 都在名单里:请求转发路径要知道这张卡绑的是谁,托盘的展示口径也得跟着改。

串账闸门落在哪一行

发布说明的说法是「接管下的请求会校验账号一致性,绝不静默把账单记到另一个账号头上」(v3.20.0-zh.md:13,80)。实现在 src-tauri/src/proxy/forwarder.rs:57-98,函数名 validate_codex_official_authorization,四路分支:

match authorization {
    // ① 没有 Authorization 头
    None | Some("") => Err(ProxyError::AuthError(
        "Codex 官方登录不可用,请先在 Codex 中完成 ChatGPT 登录")),
    // ② 拿到的还是代理占位符
    Some(value) if value.contains(PROXY_AUTH_PLACEHOLDER) => Err(ProxyError::AuthError(
        "已切换到 OpenAI 官方供应商,请重启 Codex 或新建会话以加载官方登录配置")),
    Some(_) => {
        let managed_account_id = provider.meta.as_ref()
            .and_then(|meta| meta.managed_account_id_for("codex_oauth")) ... ;
        // ③ 只有绑了账号的卡才做一致性校验
        if managed_account_id.is_some() {
            let request_account_id = headers.get("chatgpt-account-id") ... ;
            if request_account_id != expected_chatgpt_account_id
                || managed_session_matches != Some(true) {
                return Err(ProxyError::AuthError(
                    "当前 Codex 会话未加载所选 ChatGPT 账号,请重启 Codex 或新建会话后重试"));
            }
        }
        Ok(())  // ④
    }
}

四件事值得逐条看:

判据是请求头,不是令牌内容。 比对的是 chatgpt-account-id 这个头与卡上绑定的 id:代理层不解 token,只看客户端自报的账号 id 对不对得上。

双条件里的第二个是三态。 managed_session_matches 的类型是 Option<bool>,判定写的是 != Some(true)——Some(false) 失败,None 同样失败,「状态未知」被归到不放行那一侧。这是典型的 fail-closed:代价是可能多挡一次合法请求,收益是不会在状态没确定时把请求发出去。

只对绑定卡生效。 if managed_account_id.is_some() 这层判断意味着空官方卡完全不进这段校验,走的还是旧路径。新机制没有改掉旧行为,这也是两种卡能共存的前提。

其中两条报错文案指向同一个动作:重启 Codex 或新建会话。 分支 ② 与分支 ③ 的文案都落在这个动作上,原因是 Codex CLI 进程在启动时读一次 auth.json,之后不再重读;代理端换了账号,已经跑起来的客户端进程里还是旧的那份,分支 ② 拿到占位符就是这件事的另一个表现。分支 ① 则是另一层的前提缺失——Authorization 头压根是空的,文案给的动作是「先在 Codex 中完成 ChatGPT 登录」,那是根本没登录,不是登录了没重载。对应的测试在 forwarder.rs:4621, 4651 两处,断言的形式是 assert!(matches!(error, ProxyError::AuthError(message) if message.contains("重启 Codex")))——文案里的「重启 Codex」这半句是被测试钉住的。

另一半:官方卡被踢出故障转移队列

上面那道闸门管的是「已经发出去的请求校验账号」,还有一半问题它管不了:请求失败之后自动换一家重试,怎么办?发布说明在变更章(v3.20.0-zh.md:114)给的答案是干脆关掉:

  • 官方 ChatGPT 卡不再被加入、列出或经由故障转移队列重试;
  • 官方路由上的所有错误类别一律不可重试;
  • 理由原文是「对着另一个供应商重试会把入站的 ChatGPT 授权用到别的账号头上」;
  • 内置官方卡的存量队列行在读取时过滤——不改数据,读的时候跳过;
  • 官方卡的判定不再只看类别标签:存了真实 API key 或显式第三方上游的卡按普通供应商对待,保留直连路径与故障转移资格。

这是一个为了正确性主动放弃可用性的取舍。故障转移是这个软件被反复提到的能力之一(队列怎么挑下一家,我们在故障转移候选队列的那篇里逐行拆过),却在这条路径上被刻意关掉。理由不是技术上做不到,而是重试的语义在这里恰好等于串账。配套的升级提醒(v3.20.0-zh.md:258)也把后果写明了:如果内置的 Codex 官方卡在你的自动故障转移队列里,它现在会被过滤,Auto 模式不会从官方卡启动。

恢复路径上的那次仲裁

同一版的修复章里有一条与账号绑定强相关(v3.20.0-zh.md:173,提交 d2b070c9 fix(proxy): never clobber Codex official ChatGPT login on takeover restore)。问题链条是这样的:接管的恢复备份是接管开始时的快照;如果你在接管期间跑过一次 codex login,那么每一次恢复——停止接管、退出应用、崩溃恢复——都会用登录前的快照盖掉新登录;而启动时的自动重接管让这次抹除每次重启都重演一遍。

修法不是简单跳过恢复,而是仲裁:live 登录材料永远胜出,理由原文写的是「只有 Codex 自己能推进它,必然比快照新」;备份里的第三方 API key 则降级写进 config.toml 保留,而不是拿去砸 auth.json。文档同时说明被早期版本毁掉的登录不会被恢复,重新跑一次 codex login 即可。

这条推翻的是「备份恢复 = 整体回滚」这个默认心智:当被恢复的文件里混着只能单向前进的凭据时,整体回滚就是数据丢失,只能给不同字段定不同的冲突解决策略。切回官方登录时具体写了哪些文件,我们在官方登录实现的那篇里标过位置。

一并收紧的几处

发布说明在新功能章(v3.20.0-zh.md:80)与变更章(:118)还列了几条,都是多账号成立之后才浮出来的问题:

  • 账号状态加载失败时显示带重试的警告,不再冒充「未登录」,也不再在下次保存时静默解绑。加载失败与未登录是两种状态,混成一种的代价是自动解绑。
  • 登出与移除账号,和供应商切换严格互斥。
  • OAuth 请求超时从十分钟收紧到 30 秒。源码对得上:src-tauri/src/proxy/providers/codex_oauth_auth.rs:58 写的是 const OAUTH_HTTP_TIMEOUT: Duration = Duration::from_secs(30);。这是 v3.20.1 快照里的默认配置,随版本可能变动。注意同仓还有一个 src-tauri/src/services/codex_oauth_models.rs:11CODEX_OAUTH_FETCH_TIMEOUT_SECS,那是模型拉取的超时,与 OAuth 请求超时是两个不同的常量,别混着看。
  • 登出会取消仍在网络往返中的设备登录(#6506),文档的说法是「被放弃的登录流程无法再在后台悄悄完成并复活账号」——没人取消的话,它成功回来时会把刚被移除的账号又装回去。
  • 绑定账号的卡拿到与脚本类供应商相同的「配置用量查询」入口,配额页脚可以关闭、刷新间隔可改;发布说明写的是此前硬编码五分钟且没有关闭开关。对话框的测试按钮查询的是绑定的账号,不是 CLI 恰好登录的那个。
  • 托盘不再给账号绑定卡装饰全局订阅百分比。单账号时那个百分比还有主,多账号下它无主——去掉比挑一个更诚实。

最后是升级提醒里与这条主线直接相关的两条。第 5 条(v3.20.0-zh.md:254):本版之前登录的账号早于 id_token 持久化,会显示「需要重新登录」的标记,绑定到卡之前需要先重登一次。第 7 条(:262):在 WSL 或 exFAT 上停止 Codex 接管改为拒绝恢复,明确报「文件系统不支持安全恢复」;它不删任何东西,但接管前的凭据也不会被写回,之后需要重跑 codex login

这套机制的边界

合起来看,多账号共存在 v3.20.1 快照里是这样成立的:卡上存一个绑定的账号 id,切换时把令牌包写进 auth.json(写之前先回采 CLI 轮转过的 refresh token),请求过代理时用 chatgpt-account-id 头比对一次,不匹配或状态未知就拒绝,同时把官方卡从故障转移队列里摘出去。

它管不到的部分同样要说清:校验只发生在代理接管的路径上,空官方卡不进这段校验,比对的是客户端自报的头而不是令牌本身。这不是缺陷,是这段代码写明的作用范围。至于多账号在各家平台服务条款下如何使用,本文不提供任何建议,请以你所用服务的官方条款为准。


本文依据 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。

    去添加

    这个页面有问题?

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