给 CC Switch 接一个新的 AI CLI,要改多少处
如果你把 CC Switch 当成一个「配置文件搬运工」,那么给它多接一个 AI 编程 CLI 听起来像是加一个配置文件的事:写一份这个 CLI 的配置模板,写一段读写逻辑,收工。
实际不是。v3.20.1 里新接进来的 Pi 这一路,改动从前端组件一直穿到 Rust 服务层、命令层、数据库和四份语料 JSON。更麻烦的是,这些改点相当一部分不是「新增一个文件」,而是「在已有的某个清单里补一行」——同一份应用清单在这个仓库里写了不止一遍,你得挨个找出来。
下面按改点性质分成七类,每类给出确切位置。
这篇文章的依据
结论全部来自对 farion1231/cc-switch 仓库的静态阅读,快照是 3217f725(仓库内版本号 3.20.1),核对日 2026-08-31。凡涉及版本前后对比的,旧侧取 v3.19.2 快照做 diff。
需要先讲清楚一件事:我们没有安装、编译或运行过这个桌面应用。下面所有说法都是「某个文件的某一行写着什么」「两个快照之间某个文件差了多少行」,不是「用起来会怎样」。界面长什么样、切换快不快,本文不会写。
第一类:注册表——同一份 id 清单被写了好几遍
最先要改的是「这个软件认识哪些应用」。这件事在 v3.20.1 里散落在这些地方:
| 位置 | 是什么 |
|---|---|
src/lib/api/types.ts:2-11 | AppId 联合类型,类型层的唯一真值 |
src/config/appConfig.tsx:19-29 | APP_IDS 数组,运行时遍历用 |
src/config/appConfig.tsx:31-41 | DEFAULT_VISIBLE_APPS,每个 app 一个默认可见布尔 |
src/types.ts:291-301 | VisibleApps 接口,上一条的类型 |
src/config/appConfig.tsx:102-196 | APP_ICON_MAP,每个 app 的 label / icon / 配色类名 |
src/components/AppSwitcher.tsx:30-40 | APP_ICON_NAME,第二份清单 |
src/components/AppSwitcher.tsx:42-52 | APP_DISPLAY_NAME,第三份清单 |
这里有一处硬证据说明「抄几遍」的代价:同一个 app 在两处的显示名已经不一样了——APP_DISPLAY_NAME.claude 是 "Claude Code",而 APP_ICON_MAP.claude.label 是 "Claude"。两处字面量直接摆在那里。说完差异就停,我们不去猜哪个才是作者想要的。
v3.19.2 到 v3.20.1 之间,这一层其实减少了一份副本:AppSwitcher.tsx 的整个 diff 只有 17 个增删行,做的事是删掉组件内本地的 ALL_APPS 数组,改为从 appConfig import APP_IDS(:13、:106),再给两张映射表各补一行。也就是说 id 清单的副本从三份收敛成两份,但显示信息的两份映射表仍然要手工同步。
同一批还有一处收敛:DEFAULT_VISIBLE_APPS 在 v3.19.2 时是写死在 App.tsx 里的对象字面量,v3.20.1 抽到了 appConfig.tsx:31-41。App.tsx:209-215 用 { ...DEFAULT_VISIBLE_APPS, ...settingsData?.visibleApps } 展开——展开顺序保证了用户设置里没有这个新键时会吃默认值,所以新加的应用默认可见,不用去改用户的存量设置。
第二类:能力常量——它能干什么,不能干什么
光在注册表里补一行,这个新应用会被当成「什么都支持」。真正框定能力边界的是 appConfig.tsx 里那几组按功能维度切出来的子集,v3.20.1 里是这几个:
PROXY_APP_IDS(:60-65)配isProxyAppId()(:67-69),头上的注释(:59)写的是「有完整本地网关 + 故障转移数据面的应用」ADDITIVE_APP_IDS(:76-81)配isAdditiveAppId()(:83-85),指「叠加式」写配置的应用MCP_APP_IDS(:89-96)配isMcpAppId()(:98-100)SKILLS_APP_IDS(:44-52)
关键在于这几个子集不是包含关系,是按不同功能维度各切一刀,AppId 只是它们的并集。所以接一个新应用时,你要对每一刀单独做判断:它有没有本地网关?它是切换语义还是叠加语义?它有没有原生 MCP 注册表?
这一层在两个版本之间发生过一次真实的结构变化。v3.19.2 时 MCP_APP_IDS 只是 [...SKILLS_APP_IDS] 的一份拷贝(旧文件 :38)——因为当时「有 Skills」和「有 MCP」这两件事恰好同一批成员。v3.20.1 把它拆成了独立清单,McpAppId 类型(:88)改用 Exclude<AppId, "claude-desktop" | "openclaw" | "pi"> 表达,上面那行注释是:
/** Pi has no native MCP registry; do not manufacture a disabled mirror. */(src/config/appConfig.tsx:87)
「不要给它造一个假的禁用镜像」——这句注释比发布说明更能说明为什么拆。集合复用能省事,前提是所有成员的能力完全同构;来一个不同构的成员,隐式复用就必须还债。整个 appConfig.tsx 从 128 行涨到 200 行、80 个增删行,主要增量就是新应用这一路加上这几组子集与守卫函数。
改点还没完。App.tsx 里还有几条基于成员判定的散装分支要跟着补::229-232 的自愈守卫(当前视图是 MCP 而当前应用不支持时退回供应商列表)、:233-245 会话视图下的一串 !== 白名单、:310-320 那三个决定顶栏功能按钮可见性的布尔量。这些是写在业务代码里的判据,不在注册表里,容易漏。
还有一条 v3.19.2 到 v3.20.1 的改进:getFirstVisibleApp(App.tsx:217-219)以前是八个 if 顺序判断,现在是一行 APP_IDS.find(app => visibleApps[app]) ?? "claude"。顺序语义等价,但从此不必每接一个新应用就去补一个 if。这类改动的价值不在当下这一版,在下一次接入。
第三类:预设数据——一个新文件,加一堆消费方分支
每个被托管的应用在 src/config/ 下各占一个预设文件,文件名统一是 <appId>ProviderPresets.ts。新接的这一路对应 src/config/piProviderPresets.ts,那一次提交往里写了 1510 行。
行数口径先说清:新增文件的「多少行」取自接入主提交 84e75ad2 的 --stat 新增行数,既有文件的「从 X 行涨到 Y 行」取自 v3.19.2 与 v3.20.1 之间的 diff。都是定格的历史数据,不随后续版本变;但文件此刻多大是另一回事。
麻烦的是这个目录没有 barrel 文件——src/config/ 下没有 index.ts,每个消费方都直接 import ... from "@/config/<具体文件>"。覆盖最全的一处是 src/components/providers/forms/ProviderPresetSelector.tsx,它把当前受管应用的预设文件几乎全 import 了一遍;ProviderForm.tsx、useProviderCategory.ts、AddProviderDialog.tsx、useApiKeyLink.ts 则各按自己要用到的字段只 import 其中一部分——useApiKeyLink.ts 要的只是预设里的 apiKeyUrl,就只引带这个字段的那几份。所以新增一个预设文件,得挨个到这几个汇聚点补分支,没有一个 barrel 能让它自动生效。
这个目录还有一条设计取向:宁可复制,不要联动。 三处独立注释指向同一个决定——Grok Build 的预设说自己初始取自 Codex 预设快照、此后两边各自演进(grokBuildProviderPresets.ts 文件头);Claude Desktop 的预设说自己是从 Claude Code 预设「翻译」出来的(claudeDesktopProviderPresets.ts:1-10);新接的这一路则在 piProviderPresets.ts:78-85 写明初始对齐了 OpenCode 的目录,但运行时不 import 任何其它应用的预设。
所以「新增一个预设文件」不是复制粘贴一份就完了,而是从此多一份要独立维护的清单。关于预设对象有哪些字段、哪些必填,见 ProviderPreset 的字段全集。
还有一处没有任何字段承载、只靠书写顺序表达的东西:grokBuildProviderPresets.ts:82 有一行注释明写「文件顺序 = 应用内展示顺序」。也就是说往数组里加一条是有位置讲究的,而这一层在 code review 里最容易被忽略、merge 时最容易被打乱。
第四类:表单——单应用表单是最大的一块
新应用的结构化表单是 src/components/providers/forms/PiProviderForm.tsx,2038 行,是这一路里最大的单个新增实现文件。为什么单应用表单会这么大:因为每个被托管的 CLI 的配置载体形态都不一样。有的预设里存的是 JSON 对象(settingsConfig: object),有的存的是一段 TOML 源文本(Codex 版的 config: string,注释写明「将写入 ~/.codex/config.toml」,见 codexProviderPresets.ts:20),有的顶级字段与嵌套字段两套表达并存。表单要把这些差异吃下去。
配套 hook 也是按应用切的:useSpeedTestEndpoints.ts、useTemplateValues.ts、useHermesFormState.ts 都属这一类,新接一路多半要跟一个。
第五类:命令层与后端
Tauri 命令那一侧,新应用专属的入口文件 src-tauri/src/commands/pi.rs 只有 26 行,但这一路的改动一共触及了命令目录下八个文件——也就是说,除了新增的那个入口,命令目录下另有七个既有文件被动过。命令层不是「加一个新命令」,而是「已有的一批通用命令要学会认识这个新成员」。命令层本身的结构可以参考我们之前拆过的 Tauri 命令计数与分层。
后端服务层同样是「新增 + 改存量」双份:新增的有 src-tauri/src/services/provider/pi.rs(862 行)、src-tauri/src/pi_config/mod.rs(640 行)、src-tauri/src/services/pi_prompt_files.rs(521 行)、src-tauri/src/session_manager/providers/pi.rs(1042 行);被改的存量则散在服务、会话管理、代理、深链、数据库几层。
这一路还带出一个连带修复,v3.20.0 发布说明是这么写的:「代理与故障转移命令现在对所有无本地网关的应用显式拒绝,而不是写下一堆死配置。」和第二类的 PROXY_APP_IDS 放在一起看更清楚——接一个不具备某项能力的新成员,逼着把「不具备这项能力时该怎么办」这个一直没人问的问题补上了。
第六类:测试夹具
这一路里最反直觉的一个数字:PiProviderForm.tsx 实现 2038 行,配套的 tests/components/PiProviderForm.test.tsx 是 2279 行——测试比实现还长。
测试这一层要改的分两种:新写针对这个应用的用例,以及在既有的集成测试里把这个新成员当作输入喂进去。后一种在 tests/integration/App.test.tsx 里能直接看到::207-208 每个用例前清掉 cc-switch-last-view 与 cc-switch-last-app 两个 localStorage 键,:354 用 localStorage.setItem("cc-switch-last-app", "pi") 预置初始应用。测试直接把 localStorage 当作外壳组件的输入参数,反过来也说明这两个键就是外壳的持久化入口。
后端的夹具写法更值得看。发布说明声称这个工具「绝不读写 Pi 的 auth.json、绝不碰 defaultProvider / defaultModel」,而 src-tauri/src/services/provider/pi.rs:403,423,435,442 的测试夹具做的就是显式构造一份带 defaultProvider / defaultModel 的文档和一个 auth.json,再断言操作前后这些内容原样不变。
这里必须精确表述:源码里 src-tauri/src/pi_config/mod.rs:96-97 对 defaultProvider / defaultModel 是做了 optional_string(...) 读取解析的,会进内存。所以发布说明的「绝不碰」,在源码里的准确含义是不修改,不是不读取。同文件 :247 的注释则是另一回事:/// Pi's '/login' credentials live in 'auth.json' and are never read here.——这一句和文档口径一致。
第七类:翻译
src/i18n/locales/ 下是四份语料 JSON:zh.json、zh-TW.json、en.json、ja.json。新接一路应用意味着这四份都要补键。
这一层有个隐蔽的地方:src/i18n/index.ts 的默认语言是 zh,但 fallbackLng 是 en;同时全站文案调用普遍带 defaultValue,也就是代码里内嵌了一份中文兜底(App.tsx:494、:576、:851、:1312 都是这个写法)。这样一来,语料缺键时取到的是代码里那份中文兜底,而不是 key 字符串;代价是漏翻译不会以任何显眼的方式暴露出来。
另有一处是显式写进注释的翻译约定:MCP 预设的 description 字段走 i18n key,key 是模板拼出来的 `mcp.presets.${preset.id}.description`(mcpPresets.ts:39),所以新增一条 MCP 预设必须同时在语料里加对应 key,否则描述是空的(mcpPresets.ts:93-102 的 getMcpPresetWithDescription 在使用点补描述)。
顺带说一句没被改的部分
同一次 diff 里,前端骨架的这几个文件两侧行数完全相同、增删行为 0:src/main.tsx(155 行)、src/components/theme-provider.tsx(154 行)、src/lib/query/queryClient.ts(14 行)、src/lib/platform.ts(48 行)。启动链路、主题层、React Query 默认配置、平台探测在这一版原封不动。
也就是说,接一个新的托管对象,改动集中在注册表与能力判定这条线上,没有渗到启动与渲染骨架里。App.tsx 的 315 个增删行看着不少,拆开看主要是新成员判定与代理应用判定的重构。
什么情况下这份清单不适用
上面这七类是从一次具体的接入里读出来的改点分布,不是项目官方给的贡献指南——仓库里我们没有读到一份「新增应用检查表」之类的文档。所以:
- 如果新接的应用在能力上恰好和某个既有成员同构,第二类的改动会明显更少;反过来,像 MCP 那样出现「有 Skills 但没有 MCP」的新组合,就可能像 v3.20.1 这次一样逼出一次集合拆分。
- 第三类到第五类的体量完全取决于目标 CLI 的配置形态:载体是结构化 JSON 还是 TOML 源文本、是「切换」语义还是「存在即启用」的叠加语义,直接决定表单与服务层要写多少。
- 上面所有行号都对应
3217f725这个快照。这个项目迭代很快,行号大概率会漂。用标识符名去 grep,别照抄行号。
要自己核,最省事的入口是:读 src/config/appConfig.tsx 全文(注册表和能力常量都在这一个文件里),再 ls src/config/*ProviderPresets.ts 和 ls src/i18n/locales/。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、
路由指南与发布说明,以及 src/、src-tauri/、tests/ 的源码整理,
核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。