套餐配额查询功能实现详解
日期: 2026-06-30
作者: 雨轩
标签: 配额查询, 火山引擎, 智谱, web-panel, cookie, 技术实现
一、背景与需求
在 Nanobot & Hermes Agent 升级完成后,我们将模型 Provider 切换到了火山引擎 Ark Coding Plan(字节跳动方舟编码套餐)。由于该套餐有月度配额限制,需要实时监控用量,避免超限导致服务中断。
此外,智谱 GLM 也有独立的套餐配额(Pro 版,含 Token 限额和 MCP 调用次数限额),同样需要可视化的监控手段。
初始需求:
-
查询火山引擎 Coding Plan 的会话级/周度/月度三级配额
-
查询智谱 GLM 的 Token 配额和 MCP 调用次数
-
在 Web 管理面板(
hms.want.biz)上以环形进度条和卡片形式展示 -
通过 Hermes 和 Nanobot 的技能系统,支持在微信/钉钉中直接查询
二、整体架构
2.1 数据流
火山引擎控制台 API ──→ web-panel (server.py) ──→ index.html 前端
│ │
│ ├── /api/coding-quota (供前端调用)
│ ├── /api/glm-quota (供前端调用)
│ ├── /api/coding-cookies (存储 Cookie)
│ ├── /api/glm-auth (存储智谱认证)
│ └── localhost:9100
│
智谱控制台 API ──────→ check.py 脚本 ──→ Hermes/Nanobot 技能
整个系统分为四层:
第一层:数据源 — 火山引擎和智谱的控制台 API。这两个 API 都需要认证信息(Cookie / JWT Token),不公开开放。
第二层:代理服务 — web-panel/server.py 是运行在 localhost:9100 的 FastAPI 服务,作为中间代理层。前端不直接调用第三方 API,而是通过 web-panel 中转,这样认证信息只需要在服务端保存,不暴露给客户端。
第三层:前端展示 — static/index.html 是单页 Web 应用,通过调用 web-panel 的 API 展示配额数据,支持双标签切换、环形进度条、倒计时显示。
第四层:技能系统 — Hermes 和 Nanobot 的技能脚本(check.py),可以直接在命令行或通过微信/钉钉调用查询。
2.2 认证机制
| 平台 | 认证方式 | 存储文件 | 更新方式 |
|:----|:---------|:---------|:---------|
| 火山引擎 | Cookie(5个字段)+ CSRF Token | .coding_cookies + .coding_csrf | 粘贴 Cookie 字符串或 JSON |
| 智谱 GLM | Authorization JWT + 组织/项目 ID | .glm_auth | 粘贴 Cookie JSON 或手动填写 |
所有认证信息存储在 /root/.hermes/web-panel/ 目录下,与 web-panel 的 token 文件同目录。
三、火山引擎 Coding Plan 配额实现
3.1 发现 API
火山引擎控制台的配额数据通过内部 API https://console.volcengine.com/api/top/ark/cn-beijing/2024-01-01/GetCodingPlanUsage 返回。该 API 需要:
-
Cookie — 包含登录会话的 JWT(
digest)、用户 ID(AccountID)、设备标识(volcfe-uuid)、语言(user_locale)、CSRF 防护(csrfToken) -
x-csrf-token 请求头 — 与 Cookie 中的
csrfToken值一致,双重认证防 CSRF 攻击
通过 F12 开发者工具找到该 API 后,提取了必要的认证字段。最初复制了 32 个 Cookie 字段,经过逐一测试精简到 5 个必需字段。同时发现 userInfo 字段虽然包含用户信息,但查询配额时并非必要。
3.2 Cookie 管理
Cookie 管理经历了多次迭代:
v1 — 完整 Cookie 字符串
直接将 32 个字段的完整 Cookie 字符串存入文件。缺点:字段过多,容易混淆,且包含大量埋点/统计类无关字段。
v2 — 精简为 5 个必需字段
通过测试确认只需要 user_locale、volcfe-uuid、digest、AccountID、csrfToken 五个字段。其他 27 个字段(monitor_*、__spti、acw_tc 等)均为统计/埋点用途,可以丢弃。
v3 — 前端自动提取
用户在粘贴 Cookie 时,前端 JS 自动从完整字符串中提取 5 个必需字段,丢弃其余字段。同时增加去重逻辑,避免 user_locale 等字段出现两次导致计数异常。
v4 — Cookie JSON 解析
支持浏览器插件导出的 JSON 格式 Cookie 数组([{"name":"digest","value":"eyJ..."},...]),用户可以从 ExportCookies 类插件导出后直接粘贴,前端自动解析并提取必需字段。
3.3 CSRF Token 的存储问题
这是一个关键的坑点。火山引擎的 API 需要双重认证:
Cookie: digest=xxx...; csrfToken=yyy...
x-csrf-token: yyy... ← 与 Cookie 中的 csrfToken 值相同
最初我们把 Cookie 和 CSRF token 分开存储在两个文件:
| 文件 | 内容 |
|:-----|:------|
| .coding_cookies | 5 个 Cookie 字段的字符串 |
| .coding_csrf | CSRF token 值 |
但前端 saveCookies() 函数只保存了 Cookie 字符串到 .coding_cookies,没有同时更新 .coding_csrf。导致用户更新 Cookie 后,CSRF token 仍然是旧值,查询始终返回 InvalidCSRFToken 错误。
修复方案:后端 /api/coding-cookies 接口在收到 Cookie 字符串后,自动从中提取 csrfToken 字段并写入 .coding_csrf 文件,无需前端额外处理。
3.4 过期管理
Cookie 中的 digest 字段是一个 JWT(JSON Web Token),解码后可以看到 exp(过期时间)字段。web-panel 后端自动解析该时间:
b64 = p.split("=", 1)[1].split(".")[1]
b64 += "=" * (4 - len(b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(b64))
exp = payload.get("exp", 0)
前端根据剩余时间显示不同颜色:
-
🟢 >24小时:绿色
-
🟡 1~24小时:黄色
-
🔴 <1小时:红色
3.5 返回数据结构
火山引擎 API 返回的三级配额:
{
"Status": "Running",
"UpdateTimestamp": 1782723231,
"QuotaUsage": [
{"Level": "session", "Percent": 2.1, "ResetTimestamp": 1782738375},
{"Level": "weekly", "Percent": 38.2, "ResetTimestamp": 1783267200},
{"Level": "monthly", "Percent": 46.7, "ResetTimestamp": 1783439999}
]
}
-
session— 会话级配额,约几小时重置一次 -
weekly— 周度配额,每周重置 -
monthly— 月度配额,每月重置,最需要关注
四、智谱 GLM 配额实现
4.1 发现 API
智谱控制台的配额 API 为 https://bigmodel.cn/api/monitor/usage/quota/limit。认证方式与火山引擎不同:
-
Authorization 请求头 — JWT 格式的登录令牌,对应 Cookie 中的
bigmodel_token_production -
bigmodel-organization 请求头 — 组织 ID
-
bigmodel-project 请求头 — 项目 ID
4.2 认证信息管理
智谱的认证信息通过 JSON 文件存储:
{
"token": "eyJ...",
"org": "org-xxx...",
"project": "proj_xxx..."
}
前端展示时自动回填到输入框,用户修改后保存。支持两种更新方式:
-
方式一:粘贴 Cookie JSON,自动提取
bigmodel_token_production的值填入 token 字段 -
方式二:手动填写 token/org/project 三个输入框
4.3 配额字段映射
智谱 API 返回的字段命名和含义不够直观。通过对比智谱控制台页面的实际显示,我们做了以下映射:
| API 字段 | 控制台显示 | 我们的标签 |
|:---------|:----------|:-----------|
| TIME_LIMIT, unit=5, number=1 | MCP 每月额度 | 🤖 MCP 月度额度(次数) |
| TOKENS_LIMIT, unit=3, number=5 | 每 5 小时使用额度 | ⏱️ 每 5 小时额度(Token) |
| TOKENS_LIMIT, unit=6, number=1 | 每周使用额度 | 📅 每周额度(Token) |
每次遇到新的 unit/number 组合时,都需要查看控制台页面确认对应关系。
五、前端实现
5.1 双标签切换
配额页面分为两个标签页,通过 JS 控制显示/隐藏:
function switchQuotaTab(tab) {
document.getElementById('quotaCoding').style.display = tab === 'coding' ? 'block' : 'none';
document.getElementById('quotaGlm').style.display = tab === 'glm' ? 'block' : 'none';
// 切换按钮样式
}
火山引擎标签默认激活,智谱标签为次要。
5.2 环形进度条
使用 SVG 绘制环形进度条:
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 ..."
stroke-dasharray="46.7, 100" />
stroke-dasharray 的第一个值等于百分比,第二个值固定为 100。SVG 的 transform: rotate(-90deg) 将起点从右侧移到顶部。
5.3 手机自适应
手机屏幕上三列环图会溢出,通过检测容器宽度动态调整列数:
const cw = document.getElementById('page-quota').clientWidth;
const cols = cw < 400 ? '1fr' : cw < 600 ? 'repeat(2,1fr)' : 'repeat(3,1fr)';
-
窄屏(<400px):单列居中
-
中屏(400~600px):两列
-
宽屏(>600px):三列
5.4 认证回填
两个标签页都实现了认证信息的回填。加载时自动调用 API 获取已保存的值并填入输入框:
// 火山引擎
const st = await api('coding-cookie-status');
if (st.cookies) {
const pairs = essential.map(name => `${name}=${st.cookies[name]}`);
document.getElementById('cookieInput').value = pairs.join('; ');
}
// 智谱 GLM
const st = await api('glm-auth-status');
if (st.set) {
document.getElementById('glmToken').value = st.token;
document.getElementById('glmOrg').value = st.org;
document.getElementById('glmProject').value = st.project;
}
六、技能系统集成
6.1 Hermes 技能
Hermes 技能目录在 /root/.hermes/skills/ark-quota-check/,包含:
-
SKILL.md— 技能描述,定义了触发词("查一下配额"、"查一下额度") -
scripts/check.py— Python 脚本,调用 web-panel API 获取两个平台的数据并格式化输出
6.2 Nanobot 技能
Nanobot 技能目录在 /home/nanobot/.nanobot/skills/coding-quota/,同样的 SKILL.md + check.py 结构。被 weclaw 的 n/nb 别名调用。
6.3 输出格式
两个技能使用相同的格式化函数,输出一致性:
🔥 火山引擎 Coding Plan
------------------------------
🟢 会话级: 3.0% (12h 48m后重置)
🟡 周度: 38.2% (5d 04h 后重置)
🔴 月度: 46.7% (7d 04h 后重置)
状态: ✅ 运行中
🧪 智谱 GLM (Pro)
------------------------------
🤖 MCP 月度额度 (次数): 剩余 1000/1000 (0%)
🟢 每5小时额度 (Token): 0%
🟢 每周额度 (Token): 17% (5d 06h 后重置)
等级: pro
6.4 调用链
微信 → weclaw → 用户说 "h 查一下额度"
→ Hermes 匹配 ark-quota-check 技能
→ 执行 check.py → 调用 web-panel API(localhost:9100)
→ 返回格式化结果 → 微信回复
七、过程中遇到的坑
7.1 CSRF Token 不同步
症状:保存 Cookie 后查询仍然返回 InvalidCSRFToken
原因:Cookie 中的 csrfToken 已更新,但 .coding_csrf 文件仍是旧值
修复:后端自动从 Cookie 字符串提取 csrfToken 并写入独立文件
7.2 Cookie 字段重复
症状:保存时显示 "8/5 字段"
原因:原始 Cookie 字符串中 user_locale、AccountID 等字段出现两次
修复:前端提取时按字段名去重
7.3 手机端布局溢出
症状:三个环图在手机上右侧冲出框外
原因:grid-template-columns: repeat(3, 1fr) 在窄屏上不适用
修复:动态检测容器宽度,自动切换单列/双列/三列
7.4 认证字段不回填
症状:关闭页面再打开,输入框为空
原因:没有在页面加载时从 API 获取已保存的认证信息
修复:loadQuota() 时自动请求 coding-cookie-status 和 glm-auth-status 接口,回填输入框
7.5 窗口期显示问题
症状:GLM 的 "每5小时额度" 有时显示下次重置时间,有时不显示
原因:API 返回数据中 nextResetTime 字段对部分配额项缺失
修复:前端增加 if (q.nextResetTime) 判断,缺失时不显示重置时间
八、总结
整个配额查询功能从需求提出到实现完成,经过了多次迭代。核心经验:
-
认证信息管理是重中之重 — 两个平台使用不同的认证方式(Cookie vs JWT),需要分开存储和管理。CSRF 双重认证是常见的坑点,需要特别注意两个 token 的同步。
-
前端体验要闭环 — 保存→验证→回填,每个环节都需要用户能感知到。尤其是认证信息的回填,否则用户每次打开页面都看到空的输入框,会怀疑是否已保存。
-
Cookie 有生命周期 — 火山引擎的 digest(约 2 天过期)和 csrfToken(可能更短)有不同的过期时间。需要在面板上明确显示过期倒计时,并在过期后引导用户更新。
-
三方 API 字段映射需要实测 — 智谱 API 返回的
unit/number组合需要对照控制台页面逐一确认,不能凭猜测命名。
功能覆盖三个渠道:
-
🖥️ Web 面板(hms.want.biz):可视化图表,双标签切换
-
🧠 Hermes(weclaw
h):技能查询 -
🤖 Nanobot(weclaw
n):技能查询
雨轩于听雨轩 🌧️🏠