套餐配额查询功能实现详解

套餐配额查询功能实现详解

日期: 2026-06-30

作者: 雨轩

标签: 配额查询, 火山引擎, 智谱, web-panel, cookie, 技术实现


一、背景与需求

在 Nanobot & Hermes Agent 升级完成后,我们将模型 Provider 切换到了火山引擎 Ark Coding Plan(字节跳动方舟编码套餐)。由于该套餐有月度配额限制,需要实时监控用量,避免超限导致服务中断。

此外,智谱 GLM 也有独立的套餐配额(Pro 版,含 Token 限额和 MCP 调用次数限额),同样需要可视化的监控手段。

初始需求:

  1. 查询火山引擎 Coding Plan 的会话级/周度/月度三级配额

  2. 查询智谱 GLM 的 Token 配额和 MCP 调用次数

  3. 在 Web 管理面板(hms.want.biz)上以环形进度条和卡片形式展示

  4. 通过 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 需要:

  1. Cookie — 包含登录会话的 JWT(digest)、用户 ID(AccountID)、设备标识(volcfe-uuid)、语言(user_locale)、CSRF 防护(csrfToken

  2. x-csrf-token 请求头 — 与 Cookie 中的 csrfToken 值一致,双重认证防 CSRF 攻击

通过 F12 开发者工具找到该 API 后,提取了必要的认证字段。最初复制了 32 个 Cookie 字段,经过逐一测试精简到 5 个必需字段。同时发现 userInfo 字段虽然包含用户信息,但查询配额时并非必要。

3.2 Cookie 管理

Cookie 管理经历了多次迭代:

v1 — 完整 Cookie 字符串

直接将 32 个字段的完整 Cookie 字符串存入文件。缺点:字段过多,容易混淆,且包含大量埋点/统计类无关字段。

v2 — 精简为 5 个必需字段

通过测试确认只需要 user_localevolcfe-uuiddigestAccountIDcsrfToken 五个字段。其他 27 个字段(monitor_*__sptiacw_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。认证方式与火山引擎不同:

  1. Authorization 请求头 — JWT 格式的登录令牌,对应 Cookie 中的 bigmodel_token_production

  2. bigmodel-organization 请求头 — 组织 ID

  3. 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_localeAccountID 等字段出现两次

修复:前端提取时按字段名去重

7.3 手机端布局溢出

症状:三个环图在手机上右侧冲出框外

原因grid-template-columns: repeat(3, 1fr) 在窄屏上不适用

修复:动态检测容器宽度,自动切换单列/双列/三列

7.4 认证字段不回填

症状:关闭页面再打开,输入框为空

原因:没有在页面加载时从 API 获取已保存的认证信息

修复loadQuota() 时自动请求 coding-cookie-statusglm-auth-status 接口,回填输入框

7.5 窗口期显示问题

症状:GLM 的 "每5小时额度" 有时显示下次重置时间,有时不显示

原因:API 返回数据中 nextResetTime 字段对部分配额项缺失

修复:前端增加 if (q.nextResetTime) 判断,缺失时不显示重置时间


八、总结

整个配额查询功能从需求提出到实现完成,经过了多次迭代。核心经验:

  1. 认证信息管理是重中之重 — 两个平台使用不同的认证方式(Cookie vs JWT),需要分开存储和管理。CSRF 双重认证是常见的坑点,需要特别注意两个 token 的同步。

  2. 前端体验要闭环 — 保存→验证→回填,每个环节都需要用户能感知到。尤其是认证信息的回填,否则用户每次打开页面都看到空的输入框,会怀疑是否已保存。

  3. Cookie 有生命周期 — 火山引擎的 digest(约 2 天过期)和 csrfToken(可能更短)有不同的过期时间。需要在面板上明确显示过期倒计时,并在过期后引导用户更新。

  4. 三方 API 字段映射需要实测 — 智谱 API 返回的 unit/number 组合需要对照控制台页面逐一确认,不能凭猜测命名。

功能覆盖三个渠道:

  • 🖥️ Web 面板(hms.want.biz):可视化图表,双标签切换

  • 🧠 Hermes(weclaw h):技能查询

  • 🤖 Nanobot(weclaw n):技能查询


雨轩于听雨轩 🌧️🏠