`category` 不只是个标签:它同时决定 UI 分组、路由判定和代理接管拦截

2026-08-10

在 CC Switch 的预设定义里,category 是个很不起眼的字段:一个小写字符串,看名字像是给列表分组用的。但在 c39c903 这个快照里读下去会发现,它至少在三个地方被当成判定开关读——决定要不要走本地路由、决定代理接管时能不能切过去、决定卡片上挂哪个徽章。也就是说,一条预设填没填 category、填的是哪个值,影响的不止是它排在界面的哪一栏。

以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用,所以本文不会描述它长什么样。

八个取值,各自的注释语义

类型定义在 src/types.ts 开头,ProviderCategory 是个八值的联合类型,多数取值后面跟着一行注释:

取值源码注释里的说法
official官方
cn_official开源官方(原「国产官方」)
cloud_provider云服务商
aggregator聚合网站
third_party第三方供应商
custom自定义
omoOh My OpenCode
omo-slim—(列在同一组里,本文不额外补释义)

src/types.ts:1-9

这张表值得留意两处。一是 cn_official 的注释自己记录了一次改名:现在叫「开源官方」,括号里注明原来叫「国产官方」——同一个枚举值的注释里记录了一次改名,读老代码或老资料时容易对不上。二是 omoomo-slim 这两个值一看就是给某一个应用专用的,后面会看到它们确实只出现在 OpenCode 的预设里。

它在哪三处被当成开关读

第一处是路由判定。 providerNeedsRouting(appId, provider) 是「这个供应商要不要走本地代理路由」的权威判定,实现在 src/utils/providerCapabilities.ts:39-82。它的第一行就是:

if (provider.category === "official") return false;

也就是说 official 是个早退分支,命中之后 apiFormatproviderTypeisFullUrl 这些后续条件一概不看。这个函数往下才轮到 claude-desktop 的特判、托管 OAuth 一律为 true、以及 claude / codex / grokbuild 三个 appId 各自的格式判断(其余 appId 直接返回 false)。

第二处是代理接管下的切换拦截。 后端 ProviderService::switch 里有一段针对 official 的判断:在判定为热切换(即代理接管已生效)的前提下,若目标供应商 categoryofficial 且不满足例外,直接返回错误 key switch.official_blocked_by_proxysrc-tauri/src/services/provider/mod.rs:3020-3031)。仓库里给的中文文案大意是「代理接管模式下不能切换到官方供应商,使用代理访问官方 API 可能导致账号被封禁……」。例外由 official_provider_supports_proxy_takeover 判定(同文件 :49),前端同源规则 supportsOfficialProxyTakeover 要求 appId === "codex"provider.id === "codex-official"category === "official" 三个条件同时成立才返回 true(src/utils/providerCapabilities.ts:14-23)——是一条写死的窄例外,不是一类。

第三处是卡片徽章。 src/components/providers/ProviderCard.tsx:370-400 里,official 分类会挂一个「不支持路由」徽章(i18n key claudeCode.noRoutingSupport),而需要路由的情况按应用分别用 claudeDesktop.modeProxyclaudeCode.needsRoutingcodex.needsRouting。徽章文案和上面那个早退分支是同一件事的两个表现面。

再加上 UI 侧本来就有的分类筛选,category 至少有四处消费方——下文还会看到切换与删除路径上另有按 category 分流的分支。一个字段串起「显示归属」和「运行时行为」两件事,这是读这份代码时最容易低估的一处。

八个应用的分类分布

我们对 src/config/ 下八个应用预设文件逐个统计了 category 的取值分布,方法是 grep -o 'category: "[a-z_-]*"' <文件> | sort | uniq -c

应用预设分布
Claude Code(72 条)aggregator 26 / third_party 23 / cn_official 20 / cloud_provider 2 / official 1
Codex(64 条带分类)aggregator 25 / cn_official 19 / third_party 19 / official 1
Claude Desktop(69 条)aggregator 26 / third_party 23 / cn_official 19 / official 1
Hermes(61 条)aggregator 26 / cn_official 18 / third_party 16 / official 1
OpenClaw(60 条)aggregator 25 / cn_official 18 / third_party 16 / cloud_provider 1
OpenCode(60 条)aggregator 21 / cn_official 19 / third_party 17 / cloud_provider 1 / omo 1 / omo-slim 1
Gemini(22 条)third_party 12 / aggregator 8 / official 1 / custom 1
Grok Build(数组内带分类 35 条;本行分布为含数组外官方常量的口径)aggregator 19 / third_party 16 / official 1

按纪律,本文只统计数量与分类,不列任何条目的名称与域名。表里能看出来的结构性事实有三条:aggregatorthird_party 是绝对主体;official 在多数应用里只有一条;customomoomo-slimcloud_provider 都属于个位数的边角取值,其中 omo / omo-slim 确实只出现在 OpenCode。

omo / omo-slim 在后端也有对应分支:切换时 OpenCode 的这两个分类与 Claude Desktop 一起走 switch_normal 专用路径(src-tauri/src/services/provider/mod.rs:2973-2988);删除时 OpenCode 的 omo/omo-slim 分类也有专用分支,若被删的是当前项则连带删掉对应配置文件(:2833-2878)。所以这两个分类值不是纯标签,它们同样牵着落盘行为。

反直觉的那一处:有五条预设根本没有 category

把上面这张表和预设条目总数对一下,会发现两处对不齐:

  • Codex 预设数组是 67 条(src/config/codexProviderPresets.ts:112),但带 category 的只有 64 条——3 条没有 category
  • Grok Build 预设数组是 37 条(src/config/grokBuildProviderPresets.ts:79),带 category35 条——2 条没有 category

这两个差是用剥掉注释的花括号深度解析器统计每个条目的一级字段出现次数得到的,与 grep -o 的分类计数互相印证。其余六个应用的预设里,category 的出现次数与条目数相等。

要自己核这一处,不需要读懂任何一条预设的内容,两条命令就够——但两条都必须锁四空格缩进,这是能不能数对的关键:

# 数组内带 category 的条目
grep -c '^    category: ' src/config/codexProviderPresets.ts   # 64
# 数组内的条目总数(四空格缩进的 name: 只出现在顶层条目上)
grep -c '^    name: ' src/config/codexProviderPresets.ts       # 67

两个数一减就是缺 category 的条数,Codex 得 3。Grok Build 那个文件同法再跑一遍,得到 35 与 37,差 2。

这里有个口径坑值得单独说清楚。如果第一条命令图省事写成不锁缩进的 grep -c 'category: "',Codex 那个文件结果不变(还是 64),Grok Build 却会变成 36,两数一减只剩 1,跟上面说的 2 直接冲突。原因是 Grok Build 多了一条数组外的单独常量 grokBuildOfficialPresetsrc/config/grokBuildProviderPresets.ts:46):它的 category 是两空格缩进,会被不锁缩进的写法计进去;而它的 name 同样是两空格缩进,不会被第二条命令计进去——一边计一边不计,差值就凭空少了一条。上面那张分布表里 Grok Build 的 official 1 正是这条常量,那一行用的就是含它的口径。

至于这 5 条为什么没填,我们不做推断。能确定的只有机制层面的事实:按 providerCapabilities.ts:39-82 的判定顺序,category 不等于 official 就不会命中那个早退分支,而 undefined 显然不等于 official;同样地,src-tauri/src/services/provider/mod.rs:3020-3031 那段接管拦截同样以 category 是否为 official 作为判定条件。UI 上的分类筛选同样依赖这个字段。仅此而已,缺失会具体表现成什么,取决于你怎么用,我们没有依据往下写。

另一处对不齐:定义了但没被预设触发的分支

还有一处方向相反的差异。OpenClaw 与 OpenCode 这两个应用的预设文件里,official 分类的条目数是 0(见上面的分布表,两者都只有 aggregator / cn_official / third_party / cloud_provider,OpenCode 另加 omo 与 omo-slim)。但 ProviderCategory 里有 officialproviderNeedsRouting 也确实为它准备了那条早退分支。结论只能写到这一步:这两个应用的内置预设当前不会触发 official 早退分支。用户自己新建的供应商能不能落到这个分类上,我们没核实,不写。

同一份代码里还有一个类似形态的字段可以对照:ProviderPreset.hiddensrc/config/claudeProviderPresets.ts:69-70,语义是预设仍存在但不在列表显示),在八个预设文件里 grep -c 'hidden: true' 全部为 0。定义存在、当前零使用,和 official 早退分支是同一类现象。

你实际会用到的部分

如果你在读这份代码、准备改预设或者提 PR,category 这个字段的注意事项就三条:

一,它不是纯展示字段。 改一条预设的 category,改到或改离 official,等于同时改了它的路由判定结果、代理接管下能不能被切换、以及卡片徽章。要评估影响,去看 src/utils/providerCapabilities.ts:39-82src-tauri/src/services/provider/mod.rs:3020-3031 这两处。

二,前后端各有一份判定,改的时候要成对看。 路由判定前端在 providerCapabilities.ts,接管拦截在 Rust 侧的 provider/mod.rs,official 例外规则在两边各写了一份(official_provider_supports_proxy_takeoversupportsOfficialProxyTakeover)。

三,别拿分类分布去推别的东西。 上面那张表只说明各应用预设文件里各类条目各有多少条,不能推出「哪类更好用」「哪类更稳」这类结论——那既不在本文的事实范围内,也不是这个字段能承载的信息。同理,official 那条拦截的存在只是代码事实,它涉及的是各平台自己的服务条款,具体怎么用请自行判断,本文不给任何绕开限制的做法。

最后按惯例带上时间锚点:这些条目数、分布与行号,是我们在 2026-08-10 读到的 c39c903 快照的状态。预设文件是随版本变动的内容,你 clone 之后应以自己数出来的为准,能复用的是上面那套数法与那几个行号定位,不是这几个数字本身


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、 src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。 本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用, 因此不涉及界面外观、操作手感与切换速度的任何描述。 文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。 该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。