CC Switch 点两下就并发跑了两个安装
一个桌面工具替你去跑 npm i -g 或者官方 installer,最怕的不是命令写错,而是同一条命令被同时跑了两遍。全局安装写的是同一个目录、同一份链接,两个进程叠在一起,结果既不是 A 也不是 B。
麻烦的地方在于:这类问题不会报错,它只会在你的机器上留下”多处安装”这个后果,而后果和成因之间隔着好几天。所以本文不讲”要小心点”,只讲 CC Switch 在 v3.20.1 里把这个窗口堵在哪一层、你怎么自己判定手上遇到的是不是它。
先说清楚这篇的依据
本文对应的是 CC Switch 仓库快照 3217f725(仓库内版本号 3.20.1),核对日 2026-08-31。所有结论都来自静态阅读源码与文档:我们没有编译过这个项目,没有安装过这个桌面应用,也没有在任何机器上点过那个升级按钮。下面凡是写”会怎样”的地方,指的都是代码路径本身,不是运行结果。
还有一点要先摆明:这个现象不是我们虚构的用户场景。src/components/settings/AboutSection.tsx 里定义 preflightTools 这个 state 的地方,紧挨着一段注释,把”快速双击会并发开两轮探测、各自再触发一次执行”这条因果链写得清清楚楚。也就是说,作者是先意识到这个窗口存在,才写了后面那把锁。我们只是把它读出来。
窗口在哪:探测阶段的那几秒
升级路径和安装路径不一样。点”安装”是直接进执行函数;点”升级”则要先跑一次 settingsApi.probeToolInstallations(),去枚举这个工具在本机到底有几处安装、命令行实际命中的是哪一处。
问题就出在这次探测上。注释把它形容成秒级的跨进程动作(原文标的是 1-3 秒),因为它要对每个工具跑一次版本查询、再把路径规范化。而在探测返回之前,标记”正在执行”的那个 toolActions 还没有被置位——按钮的禁用态是从执行状态派生出来的,状态没置位,禁用态就不成立。
AboutSection.tsx:749-756 的注释把需要堵的窗口列成了三条:
| 窗口 | 发生位置 | 为什么会漏 |
|---|---|---|
| ① 探测阶段 | 升级入口调 probeToolInstallations 期间 | 跨进程秒级,此时执行状态尚未置位 |
| ② React 提交前 | 执行函数内部 setToolActions 落到 commit 之前的几个 microtask | 状态已 set 但尚未生效 |
| ③ 安装直入 | 安装动作不经探测,直接进执行函数的同一段 microtask | 与②同源,但入口不同 |
第②和第③条容易被忽略:即使代码里写了”开始执行就置位”,React 的状态更新也不是同步生效的,那几个 microtask 里按钮的禁用条件依然是假。
判定动作:怎么确认你看的这一版堵没堵
三个可执行的检查,都在同一个文件里,不需要跑起来:
第一,看有没有 preflightTools 这个 state。 它是一个 Set<ToolName>,登记的是”正在预检中的工具”。注释解释了为什么用 Set 而不是布尔量:单个工具卡片的升级和批量升级可能落在不同工具上各自预检,用集合才能精确对应到各自按钮的禁用态。
第二,看忙碌判定是几项合成。 AboutSection.tsx:846-849 的 isAnyBusy 是三项取或:批量动作、逐工具动作表非空、以及 preflightTools.size > 0。第三项就是补上窗口①的那一项——少了它,探测那几秒就是敞开的。
第三,看入口处的早退检查。 handleRunToolAction 一进来就检查本次要动的工具里,有没有任何一个已经在 preflightTools 里、或者在 toolActions 里有登记,命中就直接 return。两个集合都查才算完整:一个盖预检期,一个盖执行期。
这三项对上了,说明这条路径已经收口;对不上,那就是另一回事了。
处置:一把入口锁,出口在 finally
handleRunToolAction 的写法是标准的”入口登记、出口解锁”:进入时把本次的工具名批量加进 preflightTools,用 new Set(prev) 做不可变更新(注释特意点了一句:直接改原 Set 会让 React 复用同一个引用、跳过重渲染),然后在 finally 里把这些工具名删掉——异常路径也走这里,不会把锁漏在里面。
有一条设计选择值得单独说:批量场景下,只要有一个工具被锁住,整批就不开新一轮。 理由写在注释里,而且是后端约束倒推出来的——后端把整批命令拼成单个脚本并带 set -e,这套语义假设的是”一次性单脚本”,跨两次 IPC 并发会把它破坏掉。所以前端宁可整批早退,也不做”跳过被锁的、剩下的照跑”。
顺带说一句,真正执行的那个循环本身是逐工具串行的,注释给的理由同样是后端的 set -e:整批拼成一个脚本会在首个失败处中止,后面的工具连坐。串行执行才能让每个工具独立成败、独立刷新版本。所以”多个工具依次在跑”不等于并发。
万一真的并发过:多处安装的知情确认
并发写入的后果,源码注释里给到的是写冲突(AboutSection.tsx:270-276,原文点名的是并发的 npm i -g 与官方 installer)。而同一个 CLI 在本机存在多处安装,是另一类需要知情确认的情况——两者会在升级这同一个入口相遇,但不是一回事。多处安装本身带来的问题是:升级只会动其中一处,其余不动。
ToolUpgradeConfirmDialog.tsx:23-26 的组件注释把触发条件写死了:仅当某个工具检测到两处及以上安装时才弹这个确认框,单处安装不会走到这里。对话框里给三样东西——命令行实际命中的是哪一处(标了”默认”的那处就是升级目标)、各处分别是什么版本或者标为不可运行、以及锚定之后将要执行的那条命令(plan.command 原样展示,不做改写)。如果这一处没有被锚定,还会多给一条提示。
这里有个复用细节:展示每一处安装的那一行是 ToolInstallRow,冲突诊断列表和这个确认框共用同一个组件。组件注释直接说明了动机——保证两处对”哪一处是默认”的判定始终一致。判定逻辑只有一份,就不会出现”诊断里说 A 是默认、确认框里说 B 是默认”这种事。
还要注意探测失败的分支:probeToolInstallations 抛错时,代码选择的是退回直接执行,而不是阻断升级。所以”没弹确认框”有两种可能,一是确实只有一处安装,二是探测本身失败了。而这条失败分支在源码里只落了一次 console.error(AboutSection.tsx:796),没有任何面向界面的提示——也就是说,探测失败在结果上不留下可供区分的痕迹。
处置后怎么验证
验证分两步,都别只看一个数字。
第一步,看冲突还在不在。 升级完成后会有一次静默补诊(AboutSection.tsx:526-545):单独重新探测这个工具,有冲突就写进结果,没冲突则主动把之前残留的冲突展示清掉。注释给的理由很实在——外部卸载或修复之后冲突可能已经消失,不清就会一直挂着旧列表。你也可以主动跑一次全量诊断,全部无冲突时会给一条提示级别的反馈。
第二步,别把退出码 0 当成功。 升级路径里有一条二次判定:命令退出码为 0 之后,还要拿刷新后的版本号跟目标版本再比一次,版本没变会被归成软失败(versionUnchanged),退出码 0 但仍然探不到版本则归成 notRunnable。注释里举的例子是某个 CLI 要求更高的 Node 版本——命令确实跑完了,装完的东西却跑不起来。
这里还有个容易踩的坑:版本信息带模块级缓存,v3.20.1 的源码里把有效期设成了 10 分钟(这是这一版的常量值,会随版本变),而且是”有缓存哪怕过期也先展示旧值”的策略。更麻烦的是,单个工具的刷新只更新数据、不重置缓存时间戳,时间戳只由全量加载在结束时盖上。所以你比对版本号的时候,要清楚自己看的是刚探到的值还是缓存里的旧值。
什么情况说明不是这个原因
这一段是本文相对”报错清单”的全部增量,别跳过:
- 只有一处安装,且诊断报无冲突。 那么无论你点了几下,问题都不在并发写入这条链路上。
- 确认框没出现。 单处安装本来就不弹,探测失败也不弹。缺席不构成任何证据。
- 多个工具在依次跑。 执行循环是串行的,这是设计如此,不是并发。
- 本机早就有多处安装。 诊断能看见多处,但看不见它们是怎么来的。外部手动装的、包管理器换过的、以前用别的方式装的,在诊断结果里长得一模一样。能观察到冲突,不等于能归因到并发。
- 安装动作出问题。 安装路径不经过探测,也就没有多处安装确认这一环,它的失败要按别的线索查。
- 拿源码注释里的数目当判据。
AboutSection.tsx:343-344、:547-548、:195这三处注释里各写死了一个受管工具的数目,而同一个文件:62-71的TOOL_NAMES常量现在比那个数目长。两处不一致,判定以常量表为准。
Windows 侧不一样的地方
一键安装/升级的命令文本是两套平台常量,由 isWindows() 在 AboutSection.tsx:163-165 选择。POSIX 侧对部分 CLI 用的是”官方脚本失败则退回包管理器”的双保险写法;Windows 侧其中一个工具走的是 PowerShell 的 -EncodedCommand,编码是文件里自己实现的 UTF-16LE 加 base64(:116-127)。
这意味着并发一旦真的发生,两边撞在一起的东西不同:一边是包管理器的全局目录,一边是 PowerShell 起的独立进程。但前端那把锁在两个平台是同一份代码,判定动作也就完全一致——上面三条检查在 Windows 上照样适用。
这一版相对上一版改了什么
在这块范围内,v3.19.2 到 v3.20.1 的改动很克制:暂存待确认升级的那个状态多了一个 fromBatchEntry 字段,用来记录本次动作是不是从”全部升级”入口进来的。原因是执行函数按工具数量判断是否批量,而”全部升级”只剩一个工具时传进去的长度也是 1,按数量判会漏掉批量入口的状态置位。v3.19.2 时这个状态只带工具名与待确认计划两项,v3.20.1 才补上入口来源这一项。
锁本身的结构、三个窗口的枚举、多处安装的确认条件,在这两版之间没有变化。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、路由指南与发布说明,以及 src/、src-tauri/、tests/ 的源码整理,核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,因此不涉及界面外观、操作手感与切换速度的任何描述。文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。