7 种用量脚本模板与那个 QuickJS 沙箱

2026-08-10

第一次翻 CC Switch 的用量查询这块代码,最容易走神的是模板列表——七个字符串常量,看一眼就过去了。真正值得停下来的是它下面那层:这些脚本不是在 Rust 里跑的固定逻辑,而是一段 JavaScript,交给嵌入的 QuickJS 运行时执行,而且运行时被套了三道硬限额。

一个查余额的小脚本,为什么要限内存、限栈、限执行时间?源码注释自己把答案写在旁边了,而那句话才是这块设计的题眼。

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

七个常量,一处定义

模板类型的权威定义只有一处:src/config/constants.ts:25-33TEMPLATE_TYPES,七个值。i18n 里各自有一个文案键,都在 src/i18n/locales/en.jsonusageScript 节下:

常量值i18n 文案键
customtemplateCustom
generaltemplateGeneral
newapitemplateNewAPI
github_copilottemplateCopilot
token_plantemplateTokenPlan
balancetemplateBalance
official_subscriptiontemplateOfficialSubscription

这张表照抄的是代码里的字面量本身,本文不介绍、不评价其中提到的任何第三方产品或服务,也不做任何供应商推荐——这里只关心工具的机制。

七个里有一个的行为在文案层被单独交代过:official_subscription 的说明写明「默认关闭,只有你启用后才会发起请求」(en.jsonusageScript.officialSubscriptionHint)。这句话之所以要专门写,从代码分工上能看出缘由:官方订阅额度那条路走的是本机 CLI 的 OAuth 凭据,而 src-tauri/src/services/subscription.rs(1379 行)的文件头第一句就是「第一层:仅读取凭据,不实现登录/刷新」(subscription.rs:1-4),凭据状态被建模成 CredentialStatus 四个值:Valid / Expired / NotFound / ParseErrorsubscription.rs:16-23)。这是读你本机凭据的动作,默认不开、由你显式启用,是设计上把选择权留给了使用者。

顺带把它旁边那两条路也定位一下,免得混淆:账户余额查询在 src-tauri/src/services/balance.rs(454 行),按 base_url 子串检测分派到 6 个分支(balance.rs:17-44);国产套餐查询在 src-tauri/src/services/coding_plan.rs(2237 行),同样按 base_url 子串检测,7 个分支(coding_plan.rs:13-45)。本文按采集约束,不列出这些分支各自对应的服务名与域名。这三套查询的错误通道语义是统一声明过的:Err(String) 表示瞬时传输失败,前端会 retry 并保留上次成功的数据;Ok(success:false) 表示确定性失败(空 key、未知供应商、鉴权失败、非 2xx、响应体非法 JSON),注释里写明与 coding_plansubscription 两个服务口径一致(balance.rs:6-10)。

「三种预设模板」与 7 种:两层差异

用户手册 docs/user-manual/zh/2-providers/2.5-usage-query.md 写的是「CC Switch 提供三种预设模板」,下辖三节,依次对应 custom / general / newapi 三种类型(:86-124)。代码里的 TEMPLATE_TYPES 是 7 种(src/config/constants.ts:25-33)。

差异有两层:一是手册 2.5 节的「三种」与 src/config/constants.ts 里的 7 种对不上;二是同一篇文档的上半部分(:40-49)又描述了这三种之外的模板——Token Plan、第三方余额、官方订阅。也就是说,同一个文件里,一处说三种,另一处又讲了三种之外的东西。

两处位置我们都标出来了,你可以自己去核。按本站的纪律,说完差异就停——不推断哪一处「才算数」,也不拿它去评价文档或项目。

对你实际有用的只有一句:想知道当前版本有几种模板类型,唯一可靠的入口是 src/config/constants.ts 里的 TEMPLATE_TYPES,手册那句「三种」不能当清单用。

沙箱的三道限额,和它防的那件事

执行侧在 src-tauri/src/usage_script.rs,脚本执行用的是 rquickjs。限额有三个,都写死在代码里(usage_script.rs:33-66):

  • 执行超时常量 USAGE_SCRIPT_TIMEOUT_SECS = 5
  • 内存上限 USAGE_SCRIPT_MEMORY_LIMIT_BYTES = 16 MiB
  • 栈上限 256 KiB

超时不是靠外部计时器掐断,而是给运行时装了一个中断器,写法是逐轮检查是否超时usage_script.rs:33-66)。这三道限额分别对应执行时间、内存与栈,注释把它们的目的写成了限制脚本的 CPU / 内存 / 栈占用——具体到某段脚本会被怎么处置,源码里怎么写我们就怎么转述,不替它推断运行结果。

为什么要这么防?注释把威胁模型写得很直白:「脚本来自不可信来源(deeplink、同步导入),必须限制其 CPU / 内存 / 栈占用」(usage_script.rs:31-33)。

这就是本篇最反直觉的一处。多数人读到「用量脚本」,脑子里的画面是自己写一段 JS 去查自己的额度——那这沙箱看着就像过度设计。但按注释的口径,脚本未必是你写的:它可以经由 ccswitch:// deeplink 一键导入,也可以随云同步的数据一起落到你本机。SECURITY.md 的信任边界一节把这两条路都列进了「在范围内的不可信输入」,同时列入的还有本地 HTTP 代理的入站请求、从 WebDAV / S3 还原的同步数据、SQL 导入导出与配置导入文件、代理处理的上游 API 响应等(SECURITY.md:59-70)。

也就是说,限额防的不是「你手滑写了个死循环」,而是「一段你没细看就导进来的脚本」。这个前提一旦看懂,5 秒、16 MiB、256 KiB 这几个偏紧的数字就不显得奇怪了——它们是按不可信输入的标准定的,不是按你自己写代码的舒适度定的。

需要说明的是:这些是源码里的默认配置,不构成对实际运行结果的任何保证,也不等于「这样就安全了」。用量脚本要用你的 API Key 去打上游端点,凭据与脚本都落在你本机,风险仍然要你自己评估。导入来路不明的用量脚本前,先把它读一遍——这是通用做法,不是该项目文档里的建议。

六步流程:校验落在哪两步,以及 custom 的那处放宽

usage_script.rs:22-130 的执行流程是六步:

  1. 替换模板变量
  2. 校验 base_url
  3. 独立作用域里取 request 配置(保证 Runtime 在 await 之前被释放)
  4. 解析 request
  5. 校验请求 URL(HTTPS 强制 + 同源检查)
  6. 发请求

两处需要单独说。

第一处是第 3 步的作用域。把取配置这段包在独立作用域里、让 QuickJS 的 Runtime 在进入异步等待之前先释放掉,是一个刻意的写法——JS 运行时的生命周期不跨 await,网络请求发出时它已经不在了。

第二处更值得记住:当 template_type == "custom" 时,第 2 步的 base_url 校验会放宽,理由写在注释里——自定义模板下用户可能不使用模板变量,而是直接在脚本里写完整 URL(usage_script.rs:19-30)。

这一条是反直觉的:选了「自定义」,拿到的不只是更大的自由度,还顺带松掉了一道校验。所以排查用量查询相关问题时,先看这个供应商用的是哪个模板类型。如果是 custom,第 2 步的 base_url 校验就被放宽了(usage_script.rs:19-30),脚本正文里写死的那个 URL 不再受这条校验的约束。判定动作很具体:打开这条供应商的用量脚本正文,找到它构造 URL 的那一行,与该供应商配置里的 base_url 逐字比对,看两者是不是同一个地址。

反过来说,如果模板类型不是 custom,第 2 步的 base_url 校验按原样生效,那条路上就没有这处放宽可言;再去怀疑「地址被脚本改掉了」,方向就不对,得换个地方查(凭据、上游返回、缓存)。至于第 5 步那道请求 URL 校验(HTTPS 强制 + 同源检查),我们只核到它是这条六步流程里的一步,它在各模板类型下分别怎么走,本文不做推断——要确认,直接读 usage_script.rs:22-130 这段流程本身。

两个超时不是同一个数

这里有一个容易看混的地方,务必分开记:

  • USAGE_SCRIPT_TIMEOUT_SECS = 5:Rust 常量,写死在 usage_script.rs:33-66
  • 「Range: 2-30 seconds」:i18n 文案,在 en.jsonusageScript.timeoutHint

两者出现在不同层——一个是代码常量,一个是给用户看的可配范围提示,量级也不同。看到「2-30 秒」就以为脚本能跑 30 秒,是把两层混成了一层。

同一节文案里还有一条常被忽略的:自动查询间隔的提示是「0 to disable; recommend 5-60 minutes」(en.jsonusageScript.autoQueryIntervalHint)。注意这是文案里的建议值,不是代码强制的边界;具体该设多少取决于你的用法与上游的限制,项目没有给出通用值。

查询结果落在哪:一张进程内缓存

最后补一环,否则容易误以为脚本查回来的数据进了数据库。查询结果写进的是 UsageCache——进程内缓存,不持久化,进程重启即空,供系统托盘构建菜单读取(src-tauri/src/services/usage_cache.rs:1-6)。缓存分两张表:subscription 与 script,key 分别是 AppType(AppType, provider_id)usage_cache.rs:12-16)。

用量脚本的结果落在 script 那张表,key 带上了 provider_id,也就是按「哪个应用 + 哪个供应商」分别缓存。这跟代理请求与本地会话日志那套走 SQLite 的用量明细完全是两码事——那部分我们另有一篇专门讲。

缓存的写入策略也和上面提过的错误通道对得上:get_subscription_quota 命令在 Ok 时写快照、emit usage-cache-updated 事件并刷新托盘;Err(瞬时失败)则不写不 emit(src-tauri/src/commands/subscription.rs:8-41)。按注释的说法,不写不 emit 是为了在瞬时失败时保留上一份托盘快照;代价是你看到的可能是上一次的结果。

你可以自己核的几件事

不用装应用,clone 仓库就能核完本文的全部论断:

  1. 打开 src/config/constants.ts,看第 25-33 行,数 TEMPLATE_TYPES 的值有几个
  2. 打开 docs/user-manual/zh/2-providers/2.5-usage-query.md,对照 :86-124:40-49 两处
  3. 打开 src-tauri/src/usage_script.rs,看第 19-30 行的放宽条件、第 31-33 行的威胁模型注释、第 33-66 行的三道限额
  4. 打开 SECURITY.md 第 59-70 行,看 deeplink 载荷与同步还原数据是不是都在不可信输入清单里
  5. 用 Python 读 src/i18n/locales/en.json,取 usageScript 节,把 template* 开头的键与 timeoutHint / autoQueryIntervalHint 打出来比对

这五步全是纯读文本的动作,不需要运行任何东西。数字请带上时间锚点:以上是 2026-08-10 我们读到的 c39c903 快照的状态,模板数量、限额常量、文案措辞都可能随版本变动,你 clone 之后以自己读到的为准。


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

安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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