CC Switch 的可逆性五件套:迁移会话历史怎么做到能退回去

2026-08-31

一个桌面工具去改另一个命令行客户端的会话历史文件,这件事的风险结构和改自己的配置完全不同:文件不是你写的,格式不是你定的,用户也没有第二份副本。改坏了,丢的是别人几个月的对话记录。

CC Switch 有一个「统一 Codex 会话历史」的开关,作用是让 Codex 的官方订阅会话与它管理的第三方会话落进同一个桶里,从而在同一个历史列表中都能看到。要做到这一点,就必须去动 Codex 自己写下的 .jsonl 会话文件和索引数据库。这篇要拆的不是这个功能好不好用,而是它把「可以退回去」这件事拆成了哪几件具体的工程动作——每一件都能在仓库里找到对应的常量、原因码或函数。

先说清楚这篇的依据

本文对应的仓库快照是 v3.20.1,commit 3217f725,核对日 2026-08-31。全部结论来自静态阅读 docs/guides/codex-unified-session-history-guide-zh.md(下文简称迁移指南)与 src-tauri/ 下的 Rust 源码。我们没有安装、没有编译、也没有运行过这个桌面应用,因此不会出现任何关于界面、操作过程或速度的描述;文中的行号与常量都是这一版快照里的值,随版本会移位。

迁移指南把这套保障写在它的「迁移 / 还原的安全性」一节里(第 444 至 451 行),原文的说法是四层设计共同保证「在正常与异常的所有路径下,原始会话数据都不会被真正删除」。拆开看是五个互相独立又互相兜底的动作。

第一件:只改一个字段

迁移的实际写入动作,是把会话文件头部 session_meta 里的 model_provider 值在官方桶与共享桶之间切换。共享桶的标识是一个常量:src-tauri/src/codex_config.rs:18 定义 CC_SWITCH_CODEX_MODEL_PROVIDER_ID,取值是 custom

除此之外什么都不碰——对话正文、response_itemencrypted_content 一律原样保留(迁移指南 145、448 行)。

这条看起来平淡,其实是后面四条能成立的前提。改动面越窄,回滚需要的信息就越少:只要知道「这个会话当初的标签是什么」,就能把它翻回去,不需要保存整份旧文件的差异,也不需要理解会话正文的格式。反过来说,如果迁移顺手做了格式规整、字段补齐一类的事,可逆性的成本会立刻从「记一个值」涨到「存一份完整快照」。

第二件:改前必备份,而且备份分两套

迁移和还原各自有独立的备份目录,两个目录名在源码里是两个常量:

用途常量目录名
迁移前备份OFFICIAL_UNIFY_MIGRATION_NAMEcodex-official-history-unify-v1
还原前备份OFFICIAL_UNIFY_RESTORE_BACKUP_NAMEcodex-official-history-unify-restore-v1

两条都在 src-tauri/src/codex_history_migration.rs,分别是第 29 行和第 31 行,:197 的函数注释又把备份根路径 ~/.cc-switch/backups/ 重复了一遍。备份按时间戳分「代际目录」,每一代里包含 jsonl/(会话副本)、state/(索引库副本)和 meta.json(记录这次迁移属于哪个 Codex 目录),见迁移指南第 150 行。

值得说的是为什么要分两套。迁移备份不只是备份,它同时是还原动作的判定依据——还原时要从里面找出「当初标签是 openai」的那批会话 id,也就是下文第五件里会用到的那份账本。而还原本身也是一次写操作,也需要先把当前现场复制一份;如果这份现场副本落进同一个目录,账本里就会混进迁移之后的状态。两个常量分成两个目录名,账本这一侧才始终只装着迁移那一刻的东西。

第三件:原子替换,而且注入不许沉淀

所有 .jsonl 改写走「写临时文件 → 整体替换」,索引库 state_5.sqlite 的标签更新走事务化 UPDATE,全程没有任何删除会话或索引的动作(迁移指南 141 至 152 行)。索引库的文件名同样收敛成了一个常量:src-tauri/src/codex_state_db.rs:18CODEX_STATE_DB_FILENAME,取值 state_5.sqlite;同一个文件 :3 的注释交代了它的角色——Codex 把线程元数据存在这里。也就是说,这一层动的从来不是会话正文,而是会话的归类索引。

原子替换保证的是任一时刻磁盘上的文件要么是旧内容、要么是新内容,不会停在半截状态。这一点对崩溃或断电场景尤其重要——它让「异常路径」和「正常路径」得到同一种保障,而不是靠事后修补。

同一层还有一条更容易被忽略的不变量(迁移指南第 429 行):开关注入进 config.toml 的那段路由配置,只能存在于 live 配置里,绝不写进数据库中存储的那份配置。对应的两个函数一正一反:inject_codex_unified_session_bucket()src-tauri/src/codex_config.rs:3388strip_codex_unified_session_bucket() 在同文件 :3432

把它归进可逆性是因为:临时状态一旦沉淀进持久层,「关掉开关」就不再等于「回到原样」,而会变成「需要再写一次迁移逻辑去清理」。让注入只活在 live 层,退出路径就只剩一个函数。

第四件:悲观跳过

这一条的态度是——判断不了就别动。它在源码里有三处落点。

其一是迁移侧的原因码。codex_history_migration.rs:231 在 live 配置并未指向共享桶时直接返回,skipped_reason 置为 live_not_unified;迁移指南第 253 行用的是同一个词。指南把这种情况收进了「我感觉会话丢了」的场景对照表,归类写得很克制——迁移被安全跳过。宁可这一轮什么都不做,等条件真正成立后的下一次机会再迁。

其二是注入侧的两道拒绝闸,都在 inject_codex_unified_session_bucket() 里(codex_config.rs:3393 起):

  • 配置里已经有显式的 model_provider,原样返回,不覆盖用户自己的路由选择;
  • 已经存在一张形态不同的 [model_providers.custom] 表,则打一条 log::warn! 后原样返回。日志文案写的是「跳过统一会话路由注入以避免激活未知路由」,理由在函数上方注释里:那张表可能带着别人的地址与凭据字段,贸然把 model_provider 指过去,会把官方登录的流量路由到错误的后端。

其三是剥离侧的形态比对。table_matches_codex_unified_official_provider()codex_config.rs:3365,它要求那张表恰好只有四个键、且每个键的取值与注入产物逐字一致,才认定这是自己写进去的。只有认定成立才剥;第三方模板和用户自定义的同名条目原样保留。

三处放在一起是同一个判断原则:无法确认某个东西是自己写的,就不去改它。代价是功能可能对某些配置不生效,换来的是不会误伤。

第五件:幂等可重试

还原动作的判定条件是双重的(迁移指南 186 至 193 行):一个会话要被翻回官方桶,必须既出现在账本里(证明它当初确实是从官方桶迁进来的),当前又仍然是 custom(说明用户没有手动改过它)。两个条件都满足才动。前一个条件把「不是我搬来的」挡在外面,后一个条件把「用户后来自己改过的」挡在外面。

那么重复点还原会怎样?源码给了一个专门的出口:codex_history_migration.rs:417,当账本非空但没有任何「当前仍为 custom」的目标时,skipped_reason 返回 nothing_to_restore。迁移指南第 451 行给这个出口的定性是「幂等保护」而不是失败——用一个专门的原因码告诉前端「没有可做的」,而不是把「已还原 0 项」混成一次成功的空操作。这个行为不是靠人工记住的,同一个文件的 :1829 有对应的幂等回归测试守着。

原因码的价值在于它把「没做」和「做失败了」分成了两件事。仓库的多语言资源里也为这几种结果各留了一条文案键,src/i18n/locales/zh.json:711:714 依次是还原完成、还原失败、没有可恢复的迁移备份、开关已重新开启因而跳过还原——四种结果四个键,而不是一个笼统的失败提示。

五件套之外:哪一段是退不回去的

写完上面五条,还得把边界说清楚,否则容易读成「所以怎么折腾都没事」。

第一处边界是产品决策而非技术限制。迁移指南把会话分成三类(438 至 442 行):开启时迁入的存量官方会话有备份作账本,可以精确还原;开启期间新建的会话不在任何备份里、官方与第三方在共享桶中不可区分,因此永不自动搬动;开启之前的纯第三方历史绝不触碰。第 269 行明写了理由:为了不把第三方会话误塞进官方历史,产品决策是这些新会话一律留在 custom。可逆性覆盖的是「我搬过的」,不是「桶里所有的」。

第二处边界与这套设计无关。会话里的 encrypted_content 推理密文只能被当初生成它的后端解密,上游 Codex 按设计不支持跨后端解密(迁移指南第 455 行)。所以跨供应商续聊会失败——但丢的不是数据,是解密能力:会话文件完整躺在磁盘上,文字内容随时可读,换回原供应商续聊或者开新会话都正常。文件级的可逆性做得再好,也补不上这一层。

想自己核一遍的话

上面每条都给了坐标,按下面这个顺序读一遍最省事:先看 src-tauri/src/codex_history_migration.rs 开头那几行常量(:29:31)确认备份分两套,再跳到 :231:417 两个 skipped_reason 看跳过与幂等分别在什么条件下发生,最后翻 src-tauri/src/codex_config.rs:3365 / :3388 / :3432 三个函数看注入、比对、剥离这一组正反操作。这些行号是 v3.20.1 的位置,函数名与常量名比行号稳定,直接搜标识符更可靠。

需要提醒的是,这些路径下放的是本机上的真实 CLI 配置与会话记录,属于敏感数据;本文只解释源码里写了什么机制,不构成「这样就安全了」的结论。


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

    去添加

    这个页面有问题?

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