{
  "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)"
    ]
  }
}
