CC Switch 用量看板里 N/A 与 0 不是一回事

2026-08-31

一格空数字,可能有四种意思

看任何一张用量看板,最难受的不是数字大,是数字空。缓存写入那一格写着 0——它到底是「这段时间确实没有写缓存」,还是「这条协议压根不上报这个字段」?费用列写着 $0.0000——是真的免费,还是价目表里没配这个模型所以算出来是零?

这两种情况在数据库里长得一模一样,但对使用者的意义完全相反。前者是「你没花钱」,后者是「这个数不能信」。CC Switch 在 v3.20.1 的源码里没有把它们混在一起:缓存写入不可得时渲染的是字符串 N/A 而不是 0;金额解析不出来时回落成 --;成本算出来是零但明明有 token 消耗时打的标是「未定价」。0 这个值被专门留给了真的是零的那一类。

这几条分支散在三个文件里,判定条件都不长,但每一条背后都对应一个具体的协议差异。下面逐条落到行号上。

本文的核对口径

本文依据的是仓库快照 3217f725(仓库内版本号 3.20.1),核对日 2026-08-31。所有结论都来自静态阅读 src/ 下的 TypeScript 源码与 docs/ 下的文档,我们没有编译、没有运行、也没有安装过这个桌面应用,因此不会出现任何关于界面长相、操作手感或刷新速度的描述。凡是提到「渲染成什么」的地方,指的都是源码里那个分支把什么值传给了组件,而不是我们看到过什么。

三态函数:getCacheWriteAvailability

核心是 src/types/usage.ts:236-248 的一个函数,输入是一组 appType,输出是 "ok" | "partial" | "na" 三选一。它依赖上面两个白名单集合:

  • CACHE_INCLUSIVE_APP_TYPES:223-227),v3.20.1 里装着 codexgeminigrokbuild 三个 appType;
  • PARTIAL_CACHE_WRITE_APP_TYPES:232),v3.20.1 里只有 pi 一个。

这两个集合是 v3.20.1 的当前配置,往里加一个 appType 就是加一行,随版本变动很正常,所以下面讲的是判定规则本身,不是这几个名字。

判定顺序是四条,可以直接照着读:

输入的 appType 集合返回
空数组ok
全部落在 CACHE_INCLUSIVE_APP_TYPESna
一个都没落在里面,且没有命中 PARTIAL_CACHE_WRITE_APP_TYPESok
其余情况(部分落在包含型白名单里,或命中了 PARTIAL_CACHE_WRITE_APP_TYPESpartial

注意第三条那个「且」。如果只按「有没有命中包含型白名单」二分,pi 会被判成 ok,那就等于宣称这一格的数字是完整的。多出来的这条判定就是为了不让它掉进 ok

三态各自的下游在 src/components/usage/UsageHero.tsx:182-184 算出状态,:195-211 组装成一个显示对象:na 时值直接是字符串 "N/A"、同时置灰(:197-198),并挂一条说明性的 tooltip;partial 时值仍然是那个数字,但挂一条「数值可能偏低」口径的 tooltip(:205-210);ok 时不挂 tooltip(:210)。也就是说,只有 na 会改变显示的值,partial 只改变解释,不改变数字。

为什么 na 不能写 0

src/types/usage.ts:211-222 那段注释把原因写得很直白,两条后果并列:

第一,这类协议的 inputTokens 里已经含了缓存那部分,要减掉 cacheReadTokens 才是「新增输入」的语义;第二,这类协议不单独上报缓存写入,只上报缓存读取,所以 cacheCreationTokens 恒为 0——注释原话就是这种情况下界面应当标成 N/A 而不是 0。这段注释还注明该白名单是 Rust 侧同名白名单的镜像,也就是前后端共享同一份判定口径,不是前端自己拍的。

这个区分之所以必要,是因为渲染层根本区分不出来。src/components/usage/format.ts:75-97formatTokensShortvalue <= 0 或非有限数一律返回字符串 "0":80)。假如不在上游拦一道,恒为 0 的字段和真实值为 0 的字段进到这个函数以后就完全一样了,谁也没法再分开。所以 na 的判断必须发生在调用格式化函数之前——UsageHero.tsx:197 那行三目就是这道闸:状态是 na 就根本不进 formatTokensShort

混用两系 API 时,为什么给标注而不给一个数字

partial 这个返回值 v3.19.2 就有了,那时它只在「部分命中包含型白名单」的情况下出现。v3.20.1 新增的是让 pi 也落进 partial 的这条白名单判定,理由写在 src/types/usage.ts:229-231 的注释里:一次 Pi 会话可能混用两个不同体系的 API,而看板只按 appType 这一个维度做聚合。

这就构成一个死结。同一个 appType 底下的请求,有的上报了缓存写入,有的结构上就没有这个字段,聚合出来的合计数必然偏低;但看板拿不到「这条请求属于哪一系」的分组维度,也就没法把它拆成两个桶分别显示。

面对这种情况有三种选项:照常显示那个偏低的合计数、显示 N/A、或者显示数字但标明口径。代码选了第三条。理由不难反推:显示合计数会让人以为它是完整的;显示 N/A 又抹掉了确实存在的那部分数据——那部分是真实统计到的。第三条是唯一一个既不丢数据又不假装完整的选择。

同一段注释还有半句同样重要:按「部分覆盖」处理,但不改 Pi 的 fresh-input 语义。也就是说 pi 只进 PARTIAL_CACHE_WRITE_APP_TYPES,不进 CACHE_INCLUSIVE_APP_TYPES,它的输入 token 不做减法。两个白名单管的是两件事,一个管「缓存写入这一格能不能信」,一个管「输入 token 要不要扣掉缓存部分」,不能顺手合并。

同一份白名单的第二个用途:那个减法

getFreshInputTokenssrc/types/usage.ts:262-270)用的是同一个 CACHE_INCLUSIVE_APP_TYPES,做的是上面注释里的第一条后果:把 inputTokens 减去 cacheReadTokens,得到「新增输入」。

值得看的是它的守卫条件。减法只在两个条件同时成立时才做:appType 命中白名单,并且 inputTokens >= cacheReadTokens。任何一条不成立就把 inputTokens 原样返回。第二个条件是防负数——上报的两个值如果对不上,宁可把原始值放出去,也不让一个负的 token 数进到界面里。这跟前面那条思路是一致的:拿不准的时候,不编一个更好看的数出来。

这个减法在请求日志表里有一处配套写法。src/components/usage/RequestLogTable.tsx:238-269 的输入列显示的是 getFreshInputTokens(log) 的结果,而当它与原始 inputTokens 不相等时(也就是确实做了减法),会额外挂一条 Raw: <原始值> 的 tooltip(:246-250)。列里显示的是口径统一后的值,原始值不丢,放在提示里。同一格下方还用 R<缓存读>·W<缓存写> 的紧凑小字带出两个缓存分量(:256-268)。

第三种和第四种空值

除了 N/A0,看板上还有两个长得像空值的东西。

一个是 --format.ts:14-32fmtIntfmtUsd 在解析失败时一律回落到这个字符串。会用到它是因为后端有些字段是以字符串传上来的——src/types/usage.ts:24-28totalCostUsd 这类金额字段的类型确实是 string,所以格式化前统一走 parseFiniteNumberformat.ts:1-12)。解析不出有限数就是 --,这一态的含义是「这个值没拿到」,跟协议不上报是两回事。

另一个是「未定价」。isUnpricedUsagesrc/types/usage.ts:300-314)的判定条件是一串与:状态码落在 2xx、这条记录确实有 token 消耗、总成本能解析成有限数、成本倍率不是 0、并且总成本恰好等于 0。五条同时成立,RequestLogTable.tsx:279-281 就渲染 usage.unpriced(中文默认文案「未定价」)而不是 $0.0000

这五条里每一条都在排除一种「合理的零」:没有 token 消耗的请求本来就该是零;倍率被设成 0 的是人为免费,也该是零。都排掉之后剩下的那种零——有消耗、倍率正常、算出来还是零——才是「价目表里查不到这个模型」。把这一类从 $0.0000 里挑出来,是这四条分支里判定条件最长的一条。

至此,四种显示各有各的含义:

显示含义判定位置
N/A该协议结构上不上报这个字段types/usage.ts:236-248UsageHero.tsx:197
0值确实是零format.ts:80 的兜底返回
--值没拿到或解析失败format.ts:14-32
「未定价」有消耗但价目表里没有这个模型types/usage.ts:300-314

这一块在两个版本之间挪过位置

三态本身不是 v3.20.1 才有的,但它的位置和覆盖面变了。

v3.19.2 时,这段逻辑是组件内部的一个私有函数 deriveCacheWriteState,写在 src/components/usage/UsageHero.tsx 里面,返回值同样是 ok / partial / na 三条,但没有 PARTIAL_CACHE_WRITE_APP_TYPES 这条额外判定——当时的判定是「一个都没命中包含型白名单就是 ok」,partial 只在部分命中时才出现。

v3.20.1 把它提到了 src/types/usage.ts,改名 getCacheWriteAvailability 并导出,同时补上了 PARTIAL_CACHE_WRITE_APP_TYPES 这条分支,还配了单测 tests/types/usage.test.tsdescribe("getCacheWriteAvailability"):4-5)。

这个顺序值得留意:不是先设计了一个通用函数再用,而是先在组件里长出来,等到需要被第二种情况(混用两系 API)复用、并且需要被单独测试时,才提出去。提出去的动作本身也说明它从「一个组件的显示细节」变成了「一条全局口径」。

你可以自己核到哪一步

不用装应用就能核对上面的每一条。这几条命令在仓库根目录跑即可:

grep -n "CACHE_INCLUSIVE_APP_TYPES\|PARTIAL_CACHE_WRITE_APP_TYPES" src/types/usage.ts
grep -rn "getCacheWriteAvailability\|getFreshInputTokens\|isUnpricedUsage" src tests

第一条把两个白名单的定义位置与全部引用点列出来,第二条能看清三个函数各自被谁消费、有没有配套测试。以上为按仓库中的符号名组合的检索示例,未经实测,以实际执行结果为准。

至于这几个白名单里下个版本会装着哪些 appType——那是会变的,别背名字,记住那四条判定顺序就够了。


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

    去添加

    这个页面有问题?

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