CC Switch 在 WSL 路径下改不动配置怎么办
有一类故障的迷惑性特别强:新建能用,改已有的不能用。
具体表现是这样——你的用户目录(或者只是被管理的 CLI 的配置目录)落在 \\wsl.localhost\... 或 \\wsl$\... 这样的 WSL UNC 路径上,在 CC Switch 里新添一个供应商、第一次落盘,没问题;但只要是去改一个已经存在的供应商,或者把当前生效的供应商切到另一个,写入就直接失败。存量的东西既更新不了也切不动,新建的却好好的。
这种”部分可用”的形态几乎不可能让人往正确的方向猜。一个普通用户的第一反应是权限、是杀软、是这个软件对 WSL 支持不好,而真正的原因藏在一个 Windows 文件替换 API 的错误码里。
先说清楚这篇的依据
本文对应的快照是 cc-switch 仓库的 v3.20.1(3217f725),核对日 2026-08-31。文中每一处结论都来自静态读源码、读仓库自带的发布说明与 CI 配置,我们没有安装、没有编译、也没有运行过这个桌面应用,因此不会出现任何关于界面、操作过程或写入耗时的描述。凡是引用发布说明的地方都会标明是文档口径,凡是引用代码的地方都给到文件与行号,你可以自己去仓库里翻同一行。
症状的准确边界
发布说明把这条记成 v3.19.2 引入的一次回归,文档正文(docs/release-notes/v3.20.0-zh.md:157)给出的边界很清楚:
- 受影响的是 v3.19.2,这是一次回归,不是一直如此;
- 失败的是替换已有 live 配置的写入,表现为存量供应商无法更新或切换;
- 只有配置文件的首次创建仍然可用,因为”目标缺失”这条路本来就走的是另一个分支;
- 文档同时写明:该版本内无任何绕过手段,受影响用户请直接升级(
v3.20.0-zh.md:246的升级提醒里又重复了一遍)。
最后一条值得单独拎出来。很多故障你还能靠改配置、换目录、加个开关绕一绕,这条不行——它卡在写入路径的最底层,上面所有功能只要落盘就都得过它。
根因:回退写了,但触发条件只写了一半
因果链可以拆成八步,每一步都能在仓库里找到落点:
- v3.19.2 把 Windows 上的原子写切到了
ReplaceFileW; - WSL 文件系统以
ERROR_NOT_SUPPORTED(错误码 50)拒绝这个调用; - 代码里是有 rename 回退的,但它的触发条件只认
NotFound; - 错误 50 不等于
NotFound,于是回退不触发,整个写入直接失败; - 结果就是 WSL UNC 路径上任何替换已有文件的写入全挂;
- 唯独首次创建走得通——目标文件本来就不存在,
NotFound天然成立,正好落进回退; - 修复是把错误 50 也纳入”可以退回 rename”的条件集;
- 修完之后,CI 增加了在真实 WSL2 文件系统上跑的测试。
第 3 步是真正的 bug。**回退逻辑本身没缺,缺的是对失败原因的覆盖面。**这比压根没写回退更难查:没写回退,第一个 WSL 用户就会报错;写了一半,故障只在特定文件系统上、只在替换路径上出现,看起来像玄学。
落到代码。写入入口是 src-tauri/src/config.rs:327 的 atomic_write,Windows 分支在 :414 处引入 windows_sys::Win32::Foundation::ERROR_NOT_SUPPORTED,判定发生在 :449-453:
// WSL UNC paths reject ReplaceFileW with ERROR_NOT_SUPPORTED (50).
// std::fs::rename uses a different replace-existing API on Windows.
let replace_not_supported = replace_error.raw_os_error() == Some(ERROR_NOT_SUPPORTED as i32);
if replace_error.kind() != std::io::ErrorKind::NotFound && !replace_not_supported {
last_error = Some(replace_error);
break;
}
判定逻辑就是上面这五行(src-tauri/src/config.rs:449-453);整个提交是 c39c9032 fix(windows): fall back when WSL rejects atomic replace (#6232),单文件 +8/−2,其余是注释与上下文,由三个子提交合成。注释顺手回答了”为什么退回 rename 就能成”:Windows 上 std::fs::rename 走的是另一套 replace-existing API,不是 ReplaceFileW 那一条。
有一个版本口径必须说明白:这条修复的提交日期在 v3.19.2 发布之后,属于紧急热修,随 v3.20.0 一起放出。所以”v3.19.2 时是写不进去的,v3.20.0 起改为把错误 50 也归入回退”这种说法才准确;如果你手上的构建是从 v3.19.2 标签之后的某个提交打的,它可能已经带了这条修复。
怎么确认自己撞的就是这个
三个判定动作,都不需要跑任何东西:
第一,看路径。 确认被写的配置文件的绝对路径是不是以 \\wsl.localhost\ 或 \\wsl$\ 开头。这两个前缀不是随口举例——仓库里那条 WSL 契约测试的前置断言就是照这两个前缀判的(src-tauri/src/config.rs:563-579 逐个检查 test root、home、临时目录三条路径是否都以它们开头)。如果你的路径是普通的本地盘符,那这条基本可以排除。
第二,看版本。 只有 v3.19.2 这一版处在”已经切到 ReplaceFileW、但回退条件还没放宽”的窗口里。不在这个窗口,症状再像也得另找原因。
第三,看失败的形状。 关键分辨点是”新建 vs 替换”:新建一个供应商能落盘、改已有的落不了,这个组合是这条 bug 的指纹。如果新建也失败,说明卡在这条写入路径之前的某个环节,本文的判据只能排除这一条,具体是什么需要另行定位。
顺带提醒一句关于 home 目录解析的坑,它容易和本条混淆。get_home_dir() 在 src-tauri/src/config.rs:22-34,源码注释(:14-16)专门警告不要直接用 HOME 环境变量——它可能被 Git/Cygwin/MSYS 一类工具注入,不一定等于用户目录,会让数据库路径整个变掉,“看起来像数据丢失”。这类症状是东西找不到了,不是写不进去,两者别混。
处置:文档只给了一条路
发布说明的口径没有留余地:仅 v3.19.2 受影响,该版本内无任何绕过手段,直接升级。这里不做任何扩展建议——挪目录、改权限之类的做法仓库文档没提,我们也不替它发明。
配置目录里存着被管理的 CLI 的真实凭据与本机配置,任何涉及移动、复制、重建这些文件的操作都属于对本机敏感数据的改动,请自行评估再动手。
处置后怎么验证
这条修复留下的验证资产比修复本身还厚,可以直接拿来当自查清单。
src-tauri/src/config.rs 里有三条相关测试:
| 测试 | 位置 | 守的是什么 |
|---|---|---|
atomic_write_replaces_existing_file | :527 | 普通目录上的替换必须成功 |
atomic_write_preserves_destination_when_windows_replace_fails | :534 | Windows 替换失败时,目标文件内容必须保持原样、不留临时文件 |
atomic_write_replaces_existing_wsl_unc_file | :558 | WSL UNC 目录上的替换必须成功 |
中间那条尤其能说明设计取向:替换失败可以,但绝不能把原文件搞成半截。这是原子写这个抽象的底线,比”这次能不能写成功”更重要。
第三条挂了三重门禁:#[cfg(windows)] 只在 Windows 编译,#[ignore] 默认不跑,运行时还要求环境变量 CC_SWITCH_WSL_TEST_DIR 存在(缺了直接 panic)。跑起来之后它先测环境搭没搭对——:563-579 会断言 test root、get_home_dir()、std::env::temp_dir() 三条路径全部落在 WSL UNC 前缀之下且都在 test root 里面。环境不对就明确失败,而不是在一个普通本地目录上跑出一个假的绿灯。
CI 侧是两级门禁:.github/workflows/ci.yml:155-251 的 backend-windows-wsl2 每个 PR 都跑,但只跑那一条原子写契约测试;.github/workflows/wsl2-nightly.yml 的 backend-windows-wsl2-full 每晚跑全量后端测试,单线程。拆开的理由写在文件头注释里:全量后端套件在 \\wsl.localhost 的 9P 文件系统上要 50 分钟以上,扛不住每个 PR 跑一遍。
这里还埋了一个特别值得记的工程坑。后来的 0ae561b8 fix(ci): run WSL2 contract tests via prebuilt binaries (#6472) 发现:TEMP 指向 WSL UNC 路径时,link.exe / mt.exe 生成 manifest 会失败(LNK1327)。于是 CI 改成先用原生 TEMP 把测试二进制编出来,再把 TEMP 切到 WSL 路径去直接执行那个二进制,而不能继续用 cargo test(它可能重新链接)。为了测一个 WSL 上的文件系统 bug,CI 自己先在 WSL 上踩了另一个 WSL 相关的限制。
CI 里另有一层保险:执行前先列出被忽略的测试、断言目标测试名确实在列表里,不在就直接抛错。防的是”测试被改名或删掉,CI 却静默变绿”——按名字精确匹配去跑,匹配不到本身是不报错的。
什么情况说明不是这个原因
这一节比上面几节更重要。以下几种症状看着沾边,根子完全不同:
- 停止 Codex 接管时被明确拒绝。 发布说明的升级提醒第 7 条(
v3.20.0-zh.md:262)写明:在 WSL 或 exFAT 配置目录上,停止接管现在会以「文件系统不支持安全恢复」明确报错拒绝执行,而不是冒险写出半份 auth 文件;不会删除任何东西,但接管前的凭据也不会被写回,之后需要重跑codex login。这是有意为之的行为,不是原子写那条 bug。 - 编辑保存了、数据库里也变了,可真正的配置文件纹丝不动。 这指向的是 live 配置所有权判定那条线(提交
926af949,#6779),和文件系统无关。 - 终端里能跑的 CLI,应用说没装。 那是 Windows PATH 解析的问题(提交
de9af49a,#6284),根因是进程继承的环境和登录 shell 的环境不是同一份。 - 路径不在 WSL UNC 上。 前缀对不上,这条链的第 2 步就不成立,后面全不成立。
- 新建也失败。 前面说过,指纹对不上。
最后留一句结论:回退条件写窄了,比没写回退更危险。 没写回退,故障是普遍且显眼的;写了却只覆盖一种失败原因,故障就变成”只在某类文件系统、只在某条路径上”的间歇性怪象。写 fallback 的时候,值得多花几分钟把”我到底在兜哪些失败”列全——这五行判定的背后,是一整套 WSL2 CI 基建的代价。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、
路由指南与发布说明,以及 src/、src-tauri/、tests/ 的源码整理,
核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。