feat: OpenCode Go usage monitor — hybrid collector + 246-centric dashboard

- 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
This commit is contained in:
hmo
2026-07-15 22:30:20 +08:00
parent ac34556588
commit fefdc29d06
7 changed files with 1259 additions and 2 deletions
+81 -1
View File
@@ -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)