From fefdc29d06e7731df0cee75e77bb099396f3c0d9 Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 15 Jul 2026 22:30:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OpenCode=20Go=20usage=20monitor=20?= =?UTF-8?q?=E2=80=94=20hybrid=20collector=20+=20246-centric=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 5 + gateway/scripts/dashboard.py | 82 +- gateway/scripts/specs/usage_monitor.json | 120 +++ gateway/scripts/templates/dashboard.html | 101 ++- gateway/scripts/usage_collector.py | 739 ++++++++++++++++++ .../scripts/usage_monitor/extract_cookies.py | 116 +++ xmpp_agent_core.py | 98 +++ 7 files changed, 1259 insertions(+), 2 deletions(-) create mode 100644 gateway/scripts/specs/usage_monitor.json create mode 100644 gateway/scripts/usage_collector.py create mode 100644 gateway/scripts/usage_monitor/extract_cookies.py diff --git a/.gitignore b/.gitignore index 74d8a20..bb72c78 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ mowechat.conf .DS_Store Thumbs.db .venv/ +# OpenCode Go Usage Monitor +gateway/scripts/usage_monitor/cookies/ +gateway/scripts/usage_monitor/accounts.json +gateway/temp/usage_stats.json +gateway/logs/usage_collector.log diff --git a/gateway/scripts/dashboard.py b/gateway/scripts/dashboard.py index 4db9721..ad09120 100644 --- a/gateway/scripts/dashboard.py +++ b/gateway/scripts/dashboard.py @@ -9,7 +9,7 @@ Flask app on :5803. Monitors agents across platforms via: Auto-recovery: restarts local Windows agents after 3 consecutive offline checks. """ -import os, sys, re, json, time, subprocess, logging, urllib.request, sqlite3, shutil +import os, sys, re, json, time, subprocess, logging, urllib.request, sqlite3, shutil, threading from pathlib import Path from datetime import datetime, timedelta from flask import Flask, jsonify, request, send_from_directory @@ -1032,6 +1032,85 @@ def api_rdp_toggle(): return jsonify({"ok": False, "error": str(e)}) +# ════════════════════════════════════════════════════════════ +# OpenCode Go Usage Monitor — 4个账号用量配额监控 (246 本地采集) +# See gateway/scripts/specs/usage_monitor.json +# ════════════════════════════════════════════════════════════ +_USAGE_STATS_FILE = TEMP_DIR / "usage_stats.json" +_USAGE_COLLECTOR_SCRIPT = _SCRIPT_DIR / "usage_collector.py" +_usage_collector_running = False + +@app.route("/api/usage") +def api_usage(): + """直读本地 usage_stats.json(秒级响应,不依赖 Windows bot)""" + if not _USAGE_STATS_FILE.exists(): + return jsonify({"ok": False, "error": "no data yet (collect never ran)", + "last_refresh_iso": None, "accounts": []}) + try: + with open(str(_USAGE_STATS_FILE), "r", encoding="utf-8") as f: + return jsonify(json.load(f)) + except Exception as e: + return jsonify({"ok": False, "error": f"read failed: {e}", "accounts": []}) + + +@app.route("/api/usage/refresh", methods=["POST"]) +def api_usage_refresh(): + """触发本地采集(异步 subprocess,立即返回)""" + global _usage_collector_running + if _usage_collector_running: + return jsonify({"ok": True, "message": "collection already in-flight"}) + _usage_collector_running = True + + def _runner(): + global _usage_collector_running + try: + log.info("usage: starting local collector subprocess") + proc = subprocess.run( + [sys.executable, str(_USAGE_COLLECTOR_SCRIPT)], + capture_output=True, text=True, timeout=120) + log.info(f"usage: collector done (rc={proc.returncode}, " + f"stdout={len(proc.stdout)}, stderr={len(proc.stderr)})") + if proc.returncode != 0 and proc.stderr: + log.warning(f"usage_collector stderr:\n{proc.stderr[-1000:]}") + except Exception as e: + log.error(f"usage: collector crash: {e}") + finally: + _usage_collector_running = False + + t = threading.Thread(target=_runner, name="usage_collector", daemon=True) + t.start() + return jsonify({"ok": True, "message": "collection triggered (~10s), poll /api/usage shortly"}) + + +# ── Auto-collect timer (every 5 min, keeps cookies alive) ── +_USAGE_AUTO_INTERVAL = 300 + +def _start_usage_auto_timer(): + """后台线程:每 5 分钟自动采集 + cookie 保活。246 自足,不依赖 Windows。""" + def _loop(): + time.sleep(15) # initial delay: let dashboard stabilize + while True: + try: + global _usage_collector_running + if not _usage_collector_running: + _usage_collector_running = True + proc = subprocess.run( + [sys.executable, str(_USAGE_COLLECTOR_SCRIPT)], + capture_output=True, text=True, timeout=120) + log.info(f"usage auto-timer: rc={proc.returncode}") + if proc.returncode != 0 and proc.stderr: + log.warning(f"usage auto-timer stderr:\n{proc.stderr[-800:]}") + _usage_collector_running = False + except Exception as e: + log.error(f"usage auto-timer error: {e}") + _usage_collector_running = False + time.sleep(_USAGE_AUTO_INTERVAL) + + t = threading.Thread(target=_loop, name="usage_auto_timer", daemon=True) + t.start() + log.info(f"usage auto-timer started (interval={_USAGE_AUTO_INTERVAL}s)") + + # ════════════════════════════════════════════════════════════ # Module Spec — 功能模块 co-located 文档 # ════════════════════════════════════════════════════════════ @@ -1164,6 +1243,7 @@ def main(): port = int(os.environ.get("DASHBOARD_PORT", 5803)) log.info(f"Dashboard starting on :{port}") print(f"[dashboard] Starting on http://127.0.0.1:{port}") + _start_usage_auto_timer() app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) diff --git a/gateway/scripts/specs/usage_monitor.json b/gateway/scripts/specs/usage_monitor.json new file mode 100644 index 0000000..5d98035 --- /dev/null +++ b/gateway/scripts/specs/usage_monitor.json @@ -0,0 +1,120 @@ +{ + "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 的