- usage_collector.py v3.0: HTTP+stored cookies (primary) + CDP (bonus)
- Cookie keepalive: captures Set-Cookie from responses, saves back to JSON
- Per-metric regex for SSR parsing (handles nested \[N]={...} structure)
- Cache merge: preserves other accounts' data on partial collection
- extract_cookies.py: one-time CDP /cookies endpoint → per-account JSON
- dashboard.py: /api/usage reads local file (no Windows bot dependency)
- /api/usage/refresh: local subprocess collector
- Auto-timer: 5min background thread (cookie keepalive + fresh data)
- xmpp_agent_core.py: /usage endpoint + auto-timer (for local testing)
- dashboard.html: usage-monitor section (4 cards × 3 metrics + alerts)
- accounts.json: 4 accounts configured (key1-4, gitignored)
- .gitignore: cookies/, accounts.json, usage_stats.json
121 lines
12 KiB
JSON
121 lines
12 KiB
JSON
{
|
||
"module": "usage_monitor",
|
||
"version": "2.0",
|
||
"purpose": "监控 4 个 OpenCode Go 订阅账号的用量配额 (5h rolling / weekly / monthly),避免跑超限额被限流至免费模型。通过 CDP proxy 在 Chrome tab 内执行 fetch(location.href, {credentials:'include'}) 获取 SSR HTML,浏览器自动带 httpOnly cookies,无需提取/存储 cookies。",
|
||
"ui_location": "I Tab (Infrastructure) → OpenCode Go Usage section",
|
||
|
||
"human_help": {
|
||
"title": "OpenCode Go Usage Monitor",
|
||
"description": [
|
||
"4 个 OpenCode Go 订阅账号 (每个对应独立的 OAuth 登录: 2 Google + 2 GitHub) 的实时用量监控。",
|
||
"每个账号采集 3 个指标: 5h rolling (5小时滚动用量) + weekly (周用量) + monthly (月用量),含百分比+到期倒计时。",
|
||
"数据源: opencode.ai 的 SSR hydration script — 用量数据由服务端注入到页面 HTML 的 <script> 中,无公开 API。",
|
||
"采集方式: CDP-driven — 通过 CDP proxy (/eval) 在 Chrome tab 内执行 fetch(location.href, {credentials:'include'}),浏览器自动带 httpOnly cookies 获取 SSR HTML,正则解析 rollingUsage/weeklyUsage/monthlyUsage 的 usagePercent + resetInSec。",
|
||
"数据更新: 采集脚本每 5 分钟跑一次 (--daemon 模式) 或仪表板手动点击 Refresh 触发按需采集 (collect_now)。",
|
||
"告警: 任一账号任一指标 usagePercent > 80% 时卡片标橙;> 95% 时标红并提示考虑切号。",
|
||
"架构关键: Chrome 单 profile 同时只能登录 1 个 opencode 账号 → 每次采集只更新当前登录账号的数据,其他账号保留上次采集值。用户切换账号后触发 Refresh 即可更新新账号数据。"
|
||
],
|
||
"participants": [
|
||
{"name": "老莫", "device": "Windows 192.168.1.16", "role": "4 个 opencode 账号的持有者 + Chrome 登录态提供方 + dashboard 使用者", "note": "运行 usage_collector.py + xmpp_bot /usage endpoint (5807) + CDP proxy (3456)"},
|
||
{"name": "莫荷", "device": "Linux 192.168.1.246", "role": "dashboard 服务 (5803) 代理 /api/usage 到 Windows xmpp_bot", "note": "通过 _bridge_post(/usage) 代理到 Windows:5807"},
|
||
{"name": "Aliyun 47.115.32.206", "device": "Aliyun 公网服务器", "role": "Dashboard 前端访问入口 (ags.yoin.fun via nginx → 5803)", "note": "公网入口"}
|
||
],
|
||
"usage": [
|
||
"1. 首次配置: 逐个登录 4 个 opencode 账号到 Windows Chrome → 从 URL 获取 workspace ID (wrk_XXX) 填入 accounts.json → 触发 Refresh 采集",
|
||
"2. 采集脚本自动每 5min 运行 (--daemon),或仪表板点击 [Refresh Now] 按钮触发按需采集 (collect_now)",
|
||
"3. 仪表板显示 4 张卡片,每张卡片含 5h / weekly / monthly 三个进度条 + 倒计时",
|
||
"4. 当用量超 80% 卡片变橙,超 95% 变红 + 告警文字",
|
||
"5. 当某账号 session 过期 (Chrome tab 跳转到 auth.opencode.ai/authorize),卡片显示 'session expired — 请重新登录' 提示",
|
||
"6. 切换账号采集: 在 Chrome 中登录另一个 opencode 账号 → 点击 Refresh → 新账号数据更新,旧账号保留上次值"
|
||
],
|
||
"troubleshooting": [
|
||
"如果卡片显示 'session expired': 在 Windows Chrome 中重新登录该 opencode 账号,然后点击 Refresh",
|
||
"如果所有账号都显示过期: Chrome 可能已关闭或 CDP proxy (3456) 未运行 — 检查 Chrome 进程和 cdp-proxy.mjs",
|
||
"如果 Refresh 按钮无响应: 检查 xmpp_bot (Windows:5807) 是否 LISTENING + dashboard (246:5803) 的 _bridge_post 是否能联通 Windows",
|
||
"如果某账号 usagePercent 为 null 且无过期提示: 可能该账号没有 Go 订阅,或 SSR hydration 脚本结构变更 — 需手动在 Chrome 中打开页面验证",
|
||
"采集数据过旧 (更新时间 > 10min): 检查 usage_collector.py --daemon 是否在运行,或 CDP proxy 是否正常",
|
||
"CDP proxy 不可达: 确认 Chrome 以 --remote-debugging-port=9222 启动 + cdp-proxy.mjs (localhost:3456) 在运行"
|
||
],
|
||
"related": "依赖 xmpp_bot HTTP bridge (5807) — 与 EasyTier/RDP 共享同一 bridge 端口; 依赖 CDP proxy (3456) — 与 web-access skill 共享"
|
||
},
|
||
|
||
"ai_spec": {
|
||
"apis": [
|
||
{"method": "POST", "path": "/usage", "body": "{\"action\":\"status\"}", "returns": "{ok, accounts: [{key_id, workspace_id, label, rolling, weekly, monthly, last_update, session_expired, error}], last_refresh, collected_key_id}", "note": "读取缓存,即时返回"},
|
||
{"method": "POST", "path": "/usage", "body": "{\"action\":\"collect_now\"}", "returns": "{ok, message, triggered_at}", "note": "触发后台采集,~10-15s后 poll status 看结果"},
|
||
{"method": "GET", "path": "/api/usage", "returns": "同 /usage action=status", "proxied_to": "xmpp_bot /usage action=status via _bridge_post"},
|
||
{"method": "POST", "path": "/api/usage/refresh", "body": "{}", "returns": "同 /usage action=collect_now", "proxied_to": "xmpp_bot /usage action=collect_now via _bridge_post"}
|
||
],
|
||
"dependencies": [
|
||
"xmpp_bot on Windows 192.168.1.16:5807 — /usage HTTP endpoint (POST, 触发采集+读取缓存)",
|
||
"_bridge_post() + _BRIDGE_KEY in dashboard.py — proxy 机制同 EasyTier/RDP",
|
||
"gateway/scripts/usage_collector.py (Windows) — Python CDP-driven 采集脚本",
|
||
"gateway/scripts/usage_monitor/accounts.json — workspace ID × key_id × label 映射 (配置文件, .gitignore)",
|
||
"CDP proxy (cdp-proxy.mjs) localhost:3456 — Chrome 远程调试代理 (/eval, /targets, /navigate)",
|
||
"Chrome (Windows) with --remote-debugging-port=9222 — 单 profile, 当前登录 1 个 opencode 账号",
|
||
"Python 3.10+ (stdlib only: urllib, json, re, logging) — 无第三方依赖"
|
||
],
|
||
"architecture": {
|
||
"flow": "Dashboard(246:5803) → _bridge_post() → xmpp_bot(Windows:5807) → 触发/读取 usage_collector.py 采集结果",
|
||
"collection_mechanism": "usage_collector.py: 1) CDP /targets 查找 opencode.ai workspace tab → 2) CDP /eval 执行 JS: fetch(location.href, {credentials:'include'}) → 浏览器自动带 httpOnly cookies → 3) 从 SSR HTML 正则匹配 rollingUsage:$R[N]={status,usagePercent,resetInSec} (per-metric regex, 处理嵌套 $R[N]={...}) → 4) 合并已有缓存(保留其他账号上次值) → 5) 写入 temp/usage_stats.json",
|
||
"pipeline": "CDP /eval + fetch → SSR HTML → 正则解析 → 合并缓存 → temp/usage_stats.json → xmpp_bot 读取缓存 → _bridge_post 代理回 dashboard → HTML 卡片渲染",
|
||
"participants": {
|
||
"windows_192_168_1_16": {"role": "采集执行 + CDP proxy + xmpp_bot /usage 端点 (5807)", "agent": "xxm"},
|
||
"linux_246": {"role": "Dashboard 服务 (5803) 代理 /api/usage 到 Windows xmpp_bot", "agent": "mohe"},
|
||
"opencode_ai_servers": {"role": "数据源; SSR hydration 注入了 lite.subscription.get 资源到页面 HTML <script>; tab 跳转到 auth.opencode.ai/authorize 表示 session 失效"}
|
||
},
|
||
"cdp_driven_mechanism": {
|
||
"key_insight": "httpOnly cookies 无法通过 document.cookie 提取,但 fetch(location.href, {credentials:'include'}) 从浏览器内部发起请求时会自动带上所有 cookies (含 httpOnly) → 无需提取/存储 cookies",
|
||
"collection_flow": "CDP /targets → 找 opencode.ai tab → CDP /eval → JS fetch(location.href, {credentials:'include'}) → 获取 SSR HTML → 正则解析 usagePercent + resetInSec",
|
||
"single_account_limitation": "Chrome 单 profile 同时只能登录 1 个 opencode 账号 → 每次采集只更新当前登录账号; 其他账号保留上次值; 用户切换账号后 Refresh 即可采集新账号"
|
||
}
|
||
},
|
||
"constraints": [
|
||
"采集必须通过 CDP /eval + fetch 在 Chrome tab 内执行 — 不能用 Python requests + cookies (httpOnly cookies 无法提取)",
|
||
"采集脚本必须在 Windows 执行 (CDP proxy 连接本机 Chrome :9222)",
|
||
"Chrome 必须以 --remote-debugging-port=9222 启动 + cdp-proxy.mjs (3456) 必须运行",
|
||
"Workspace ID 必须从已登录的 opencode.ai 页面 URL 提取 — 不要手动猜测",
|
||
"解析 SSR 时用 per-metric regex (rollingUsage:.*?\\{[^}]*\\}) 而非 blob regex — SSR 格式是嵌套 $R[N]={...}, blob regex 在遇到内层 } 时会断",
|
||
"采集频率: 默认 5min — 过密会触发 opencode.ai 风控",
|
||
"session_expired 判定: Chrome tab URL 包含 'auth.opencode.ai' 或 'auth/authorize' → session 失效",
|
||
"accounts.json 不可提交 git — 已加入 .gitignore (含 workspace IDs)"
|
||
],
|
||
"must_not": [
|
||
"不要把 accounts.json 提交到 git — 已加入 .gitignore",
|
||
"不要在采集脚本中硬编码 workspace ID — 必须从 accounts.json 配置读取",
|
||
"不要尝试用 Python requests + 提取的 cookies 采集 — httpOnly cookies 无法通过 document.cookie 或 CDP /eval 提取",
|
||
"不要用 blob regex 解析 SSR — 嵌套 $R[N]={...} 会导致 [^}]* 在内层 } 处断; 用 per-metric regex 替代",
|
||
"不要重写整个 fI() 函数 在 dashboard.html — 用 create-once/update-state pattern 像 EasyTier/RDP 那样添加 usage section",
|
||
"不要在 CDP /eval 中返回整个 HTML (~16KB) — 尽量在 JS 中提取关键数据后返回小 JSON (当前实现返回完整 HTML, 可优化)"
|
||
],
|
||
"related_modules": [
|
||
{"module": "easytier", "relation": "共享 xmpp_bot HTTP bridge (5807); EasyTier VPN 不影响 usage 监控,但二者均通过 _bridge_post 代理触发"},
|
||
{"module": "rdp", "relation": "共享 xmpp_bot HTTP bridge (5807); 采集脚本运行在 Windows 同一机器上,但功能相互独立"}
|
||
],
|
||
"tests": [
|
||
{"id": "UM01", "name": "POST /usage action=status returns accounts array", "endpoint": "POST /usage {action:status} — 期望 response.accounts 是数组, 含 key_id/rolling/weekly/monthly 字段"},
|
||
{"id": "UM02", "name": "POST /usage action=collect_now triggers CDP collection", "endpoint": "POST /usage {action:collect_now} — 期望 response.ok=true, 几秒后 cache 文件更新"},
|
||
{"id": "UM03", "name": "Session expired account shows auth redirect message", "endpoint": "POST /usage — 当 tab 在 auth.opencode.ai 时, account.error 应含 'session expired'"},
|
||
{"id": "UM04", "name": "Usage cards render without flicker on consecutive polls", "endpoint": "连续 3 次 GET /api/usage — dashboard 卡片不应闪烁/recreate DOM"},
|
||
{"id": "UM05", "name": "High-usage warning displays when usagePercent > 80", "endpoint": "GET /api/usage — usagePercent > 80 卡片应标橙;> 95 标红"}
|
||
],
|
||
"known_issues": [
|
||
"opencode.ai Go 用量目前无公开 API — PR #16513 未上线,数据唯一来源是 SSR hydration script",
|
||
"Chrome 单 profile 限制: 一次只能登录一个 opencode 账号 → 每次采集只更新当前登录账号; 需用户手动切换账号",
|
||
"Session 有效期: opencode.ai session cookies 可能随时过期 → 采集脚本检测 tab URL 是否在 auth.opencode.ai 来判定 session_expired",
|
||
"CDP proxy 依赖: 若 Chrome 关闭或 cdp-proxy.mjs 未运行,采集将失败 — 需确保两个进程都在运行",
|
||
"HTML 返回大小: 当前 CDP /eval 返回完整 SSR HTML (~16KB), 未来可优化为 JS 端提取关键数据后返回小 JSON"
|
||
],
|
||
"related_files": [
|
||
"gateway/scripts/usage_collector.py — Python CDP-driven 采集脚本 (v2.0, 重写)",
|
||
"gateway/scripts/usage_monitor/accounts.json — workspace ID 配置 (.gitignore)",
|
||
"gateway/scripts/dashboard.py — /api/usage + /api/usage/refresh 端点 + _bridge_post 代理",
|
||
"gateway/scripts/templates/dashboard.html — fI() function 中添加 usage section (create-once pattern)",
|
||
"gateway/scripts/specs/usage_monitor.json — 本 spec 文件",
|
||
"xmpp_agent_core.py — /usage HTTP endpoint in _BridgeHandler.do_POST()",
|
||
"gateway/temp/usage_stats.json — 采集结果缓存文件 (.gitignore)",
|
||
".opencode/skills/web-access/scripts/cdp-proxy.mjs — CDP proxy (localhost:3456, 提供 /eval /targets /navigate)"
|
||
]
|
||
}
|
||
}
|