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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user