CC Switch 首次导入既有配置:哪些碰哪些不碰

2026-08-31

装一个「集中管理各家 AI CLI 配置」的工具,最让人心里没底的从来不是它能干什么,而是它第一次启动时,对我硬盘上已经有的那几个配置文件做了什么。你之前手动写进去的供应商还在不在?密钥会不会被挪位置?下次直接敲命令行,用的还是我原来那套吗?

把 Pi 接进来是 v3.20.0 干的事(主 PR 是 #6064,对应提交 84e75ad2),我们读的 v3.20.1 快照上它已经在位。而 Pi 的接入方式恰好把「首次导入」这件事的边界摊得很开:哪些东西它认领,哪些东西它读一眼就放下,哪些东西它连读都不读,源码里一条一条有落点。这篇就顺着这些落点走一遍。

先说清楚这篇的依据

本文只静态读了仓库快照,核对日 2026-08-31,对应 commit 3217f725(仓库内版本号 3.20.1),另外对照了 docs/release-notes/v3.20.0-zh.md 这份发布说明。我们没有安装、没有编译、也没有运行过这个桌面应用,所以下面不会有任何关于窗口长什么样、按钮排在哪里、操作起来顺不顺手的描述——凡是讲行为的地方,要么是源码里的分支,要么是文档里的原话,两者不一致时我会把两处都标出来。

首次启动那一步,文档说了三件事

发布说明的「升级提醒」章有单独一条讲 Pi 的首启行为(v3.20.0-zh.md:242),逐条列了这么几点:Pi 默认作为新应用出现,而不是默认隐藏;~/.pi/agent/models.json 里已经写着的供应商会被导入成可管理的条目;首次用量同步会扫描所有可发现的 Pi 会话并回填历史用量,原文提醒「看板总数可能跳涨」。

第三条是最容易被误读的。看板数字忽然变大,本能反应是「它是不是把我的用量重算错了」,但按文档的说法,这只是把此前从未被这个工具统计过的历史会话一次性摄取进来。它属于一次性的口径变化,不是新产生的消耗。

同一条提醒里还有一句技术限制:Pi 的 sessionDir 如果配成相对路径,会话浏览和用量导入都拿不到东西。这句在源码里能对上,而且措辞比文档更准确。src-tauri/src/session_manager/providers/pi.rs:106:219 两处的报错文案是 "Pi sessionDir '{configured_path}' requires a project cwd and cannot be globally enumerated":251 那处更短:Err("Relative Pi sessionDir cannot be globally resolved".to_string())。分支函数分别是 classify_configured_session_dirpi.rs:158)和 resolve_global_session_dirpi.rs:196),回退顺序写在 pi.rs:126-137:先看显式配置,再看 defaults.session_dir,来源标记是 "settings"

注意报错说的是 requires a project cwd,不是 invalid path。这个区别很重要:相对路径不是被判成非法,是它在语义上没法全局解析——相对路径要相对于某个项目工作目录才有意义,而一次全局扫描根本没有「当前项目」这个概念。想让回填生效,改成绝对路径是唯一的出路,跟路径写得对不对无关。

明确声明不碰的那几样

发布说明的原话很硬(v3.20.0-zh.md:70):绝不把 Pi 的内置供应商物化进 models.json,绝不读写 Pi 的 auth.json,绝不碰 defaultProviderdefaultModel,Pi 自己的登录与模型选择归 Pi。

源码这边三句话对下来,两句完全对得上,第三句需要更精确的说法。

auth.json 那句在源码注释里有原文,位置是 src-tauri/src/pi_config/mod.rs:247

/// Pi's '/login' credentials live in 'auth.json' and are never read here.

注释与文档一致。凭据文件不进这个模块的视野,这一点在代码组织上就框死了。

defaultProvider / defaultModel 就不能照抄「绝不碰」了。src-tauri/src/pi_config/mod.rs:96-97 实际上是要读它们的:

optional_string(object, "defaultProvider", &path)
optional_string(object, "defaultModel", &path)

这两行是同一种调用,会把值解析进内存里的结构体,但不写回。所以「绝不碰」在源码里的准确含义是「从不修改」,不是「从不读取」。口语里说「碰」通常指改,文档这么写不算错,但你如果照着「它连看都不看」去理解,排查问题时会走岔——它是看的,只是不改。

这两条「不动」也不是只靠自觉。src-tauri/src/services/provider/pi.rs:403,423,435,442 是一组测试夹具,测试里显式构造了带 defaultProviderdefaultModel 的文档,还造了一个 auth.json,用来验证一系列操作前后这些内容原样不变。有回归测试兜底的「不碰」,和写在发布说明里的「不碰」,可信度是两回事。

为什么这里的「导入」不等于「接管」

同样是首次导入既有配置,Pi 和别的应用在语义上不是一回事,原因在注册表里。src/config/appConfig.tsx:76-81 有一个 ADDITIVE_APP_IDS 常量,Pi 在其中——发布说明把这类叫「累加模式」,判据是「供应商的启用与否等于其键是否存在于 models.json,多个供应商共存」(v3.20.0-zh.md:68)。

对照另一类是「切换」语义:一次只有一个供应商处于生效状态,切过去就意味着把上一个换下来。累加模式没有这个动作,写进去就算启用,删掉就算关闭,多条并存。

这条架构事实直接决定了首次导入长什么样:它不需要「接管」你的文件,也不需要先把你原有的配置搬到别处再放一份自己的进去。它只是把 models.json 里已经存在的那些键读出来,登记成自己可以管理的对象。你原来手写的那几条,还在原来的位置上,用的还是原来的格式。

顺带一提,这种「配置文件是双向事实源」的特性在别处也留下了痕迹。发布说明讲备份恢复行为变化那一节(v3.20.0-zh.md:193)说,恢复数据库之后会把恢复结果向外投影到各个受管应用的 live 配置上,但明确写了一句「除 Pi 外」,理由是 Pi 的 models.json 仍是事实源、下次启动反向导入。单向覆盖对累加模式来说是错的,所以这里给它开了口子。

models.json 的三层写保护

既然这个文件是双向的——CC Switch 会写它,Pi 自己也会写它,你还可能拿编辑器直接改它——那么「首次导入之后你能不能继续手动改」就成了一个必须回答的问题。答案藏在写入路径的骨架里。

src-tauri/src/pi_config/mod.rs 里每一个写操作都是同一套结构(对应 mod.rs:145-235 一段):

let _guard = lock_models_file()?;                                  // 1. 进程内文件锁
let path = get_pi_models_path()?;
let (mut document, expected_revision) = read_models_document_with_revision(&path)?;  // 2. 读 + 取修订号
let providers = providers_mut(&mut document, &path)?;
let current = providers.get(provider_key).ok_or_else(|| {
    AppError::Conflict(format!("Pi provider '{provider_key}' is no longer present in models.json"))
})?;                                                               // 3. 键还在吗
if current != expected {
    return Err(AppError::Conflict(format!("Pi provider '{provider_key}' changed outside CC Switch")));
}                                                                  // 4. 内容还是我上次读到的那份吗
if current == replacement { return Ok(()); }                       // 5. 没变化就不写
// write_models_document(&path, &document, &expected_revision)     // 6. 带修订号写回

三层防护各挡一类风险,缺一不可。进程内锁挡的是这个应用自己的并发写;内容比对(current != expected)挡的是别的进程在这中间改过;expected_revision 在最终写回时再校验一次,兜住读到写之间的窗口。错误类型统一是 AppError::Conflict,出现位置是 src-tauri/src/pi_config/mod.rs:154, 159, 213, 232, 419

删除路径同样带 expected 比对(mod.rs:210-217)。恢复路径的措辞尤其值得抄下来(mod.rs:230-234):

cannot restore Pi provider '{provider_key}' because another value now owns the key

说的是「另一个值现在拥有这个键」,不是笼统的「文件变了」。差别在于它告诉你冲突发生在哪一个键上、以及冲突的形态是占位而不是丢失——这两点决定了你该去看哪一行。

还有一条测试名把边界画得很清楚,在 src-tauri/src/pi_config/mod.rs:581

duplicate_provider_key_is_validation_not_a_write_conflict

重复键是校验错误,不是写冲突。这两类错误被刻意分开了:一个是你的输入本身不合法,改输入就行;另一个是文件在你背后动了,得重新读一遍再决定。合在一起报同一种错,排查时就没法区分「我填错了」和「有人跟我抢」。

需要说明的是,以上都是源码里的默认逻辑,属于 v3.20.1 快照的实现,可能随版本变化。它们能挡住的是并发写场景下的相互覆盖,不构成对文件不会损坏的任何保证。

首次启动还会动一次数据库

除了配置文件,首启还有一件事会改本机状态:数据库迁移。发布说明升级提醒的第一条(v3.20.0-zh.md:238)写的是 schema 从 v16 迁移到 v17,迁移前自动创建备份,本版运行过一次之后旧版会拒绝打开这个数据库。

这一条不能照抄。到 v3.20.1 快照,src-tauri/src/database/mod.rs:56 里的 SCHEMA_VERSION 已经是 18 了——v3.20.0 之后又叠了一次 migrate_v17_to_v18src-tauri/src/database/schema.rs:1588),给 session_log_sync 表加了 last_byte_offsetlast_tail_fingerprint 两列。对应的提交是 bcee61be perf(usage): incremental byte-cursor scan for Claude session logs。所以口径应该是:v3.20.0 的发布说明写的是 v16→v17,v3.20.1 已经是 18。

这次追加的迁移,来源恰好是 Pi 自己的技术方案:Pi 的用量同步用的就是「对文件尾部做指纹、只解析新追加的字节」这套增量做法,bcee61be 把同一思路搬到了 Claude 的会话日志上,session_log_sync 表因此要多存两个游标字段,schema 也就又抬了一级。

「迁移前自动备份」这句在 src-tauri/src/database/mod.rs:132-134 能看到条件:

if version > 0 && version < SCHEMA_VERSION {
    // "Creating pre-migration database backup (v{version} → v{SCHEMA_VERSION})"
}

两个条件缺一不可:version > 0 排除掉全新库(没什么可备份的),version < SCHEMA_VERSION 才说明确实要迁。至于「降级要还原备份」,那不是一条政策,是 src-tauri/src/database/schema.rs:449 主动拒绝打开版本号比自己新的库,报错文案原文是「数据库版本过新({version}),当前应用仅支持 {SCHEMA_VERSION},请升级应用后再尝试。」

想自己核一遍,看这几个地方

如果你要在升级前确认边界,不必等它跑起来:~/.pi/agent/models.jsonproviders 对象里有哪些键,就是会被登记为可管理条目的那一批;同一份文件里的 defaultProvider / defaultModel 会被读、不会被改;auth.json 按源码注释根本不进这条路径。sessionDir 是相对路径的话,回填那一步先不用抱期待。

最后照例提醒一句:这个工具会读写本机上真实的 CLI 配置文件,其中一些文件里有你的密钥。上面讲的每一条边界都只对应 v3.20.1 快照的源码,项目迭代很快,升级前值得自己再看一眼对应版本的实现。


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

    去添加

    这个页面有问题?

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