Files
MoFin/agents_health_check.py
T
hmo 9509b80d8b feat(xmpp_monitor): dynamic key switching + coalesce auto-heal
- xmpp_logger.py:
  - Add switch_key(key_id): switch Hermes model.provider via sed + systemctl restart
  - Add current_provider() reading model: block correctly (not just first '  provider:' line)
  - Add KEY_TO_PROVIDER mapping (AgentsMeeting key_id -> Hermes provider name)
  - Add RESTART_COOLDOWN_FILE/SEC = 180s to prevent restart loops
  - auto_heal(): detect HTTP 429 / Weekly usage limit -> call best_key() -> switch_key()
  - auto_heal(): detect timeout/error -> async systemctl restart (Popen, not blocking)
  - _verify_llm(): bump timeout 25s -> 90s (cold-start gateway takes 20-40s)
  - health(): urlopen timeout 8s -> 90s (match verify window)
  - Use sudo NOPASSWD (hmo ALL=(ALL) NOPASSWD: ALL already configured)

- agents_health_check.py:
  - Replace inline pkill+Popen restart logic (caused multiple instances) with systemctl
  - Add RESTART_COOLDOWN_FILE state to skip restart within 3 min of last
  - Call xmpp_logger.auto_heal() at end of every cron cycle
  - Both inline restart and auto_heal restarts share cooldown file

- scripts/key_status.py: Reports weekly/monthly/rolling status of all 6 OCG keys
- scripts/test_llm.py: 90s timeout test (was 15s, gateway cold-start >= 30s)
- scripts/test_production.py: smoke test on /home/hmo/MoFin/ (hardlinked to web-dashboard)

Fixes:
- Old assumption 'Cloudflare blocks Python User-Agent' was WRONG.
  True cause was HTTP 429 Weekly usage limit on key5 (12hr reset window).
  Hermes silently ignored providers.X.headers config keys; only model.default_headers works.
  Config already has model.default_headers: User-Agent: curl/8.5.0 (defense in depth).
- Multiple gateway instances were caused by 3 competing systemd units
  (hermes-gateway@.service template + hermes-gateway-zhiwei.service named).
  Masked the template unit @position-analyst and @zhiwei so only the named one wins.
2026-07-19 20:12:47 +08:00

191 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
agents_health_check.py — MoFin Tier1 快速健康检查
====================================================
每 5 分钟运行一次(crontab)。检查关键服务的端口/HTTP/DB 可用性。
全正常时静默(不输出)。异常时写入 TODO 文件和 JSON 报告。
同时采集 XMPP 通道健康数据到时间序列日志。
部署: crontab */5 * * * * cd /home/hmo/MoFin && python3 agents_health_check.py
"""
import json, os, sys, socket, sqlite3, urllib.request
from datetime import datetime
from pathlib import Path
# ---- Config ----
SCRIPT_DIR = Path(__file__).resolve().parent
TEMP_DIR = SCRIPT_DIR / "gateway" / "temp"
LOGS_DIR = SCRIPT_DIR / "gateway" / "logs"
# Ensure dirs
TEMP_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
REPORT_FILE = TEMP_DIR / "last_health_check.json"
TODO_FILE = TEMP_DIR / "health_todos.jsonl"
LOG_FILE = LOGS_DIR / "health_check.log"
# ---- Service List ----
SERVICES = [
{"name": "mofin_api", "label": "MoFin API", "host": "127.0.0.1", "port": 8899, "type": "http", "check": "/api/health"},
{"name": "zhiwei_gateway", "label": "知微 Gateway", "host": "127.0.0.1", "port": 8643, "type": "http", "check": "/v1/health"},
{"name": "ejabberd", "label": "ejabberd XMPP", "host": "127.0.0.1", "port": 5222, "type": "tcp", "check": None},
{"name": "mofin_db", "label": "MoFin 数据库", "host": "127.0.0.1", "port": 0, "type": "db", "check": "/home/hmo/web-dashboard/data/mofin.db"},
]
# ── XMPP 通道健康采集 ──────────────────────────────
XMPP_HEALTH_LOG = LOGS_DIR / "xmpp_health_log.jsonl"
def _collect_xmpp_health(now):
"""采集 XMPP 通道健康数据,追加到时间序列日志。"""
try:
from xmpp_logger import health as xmpp_health
h = xmpp_health()
except Exception as e:
h = {"status": "collector_error", "error": str(e)[:200]}
entry = {"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"), **h}
XMPP_HEALTH_LOG.parent.mkdir(parents=True, exist_ok=True)
with open(XMPP_HEALTH_LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
# ---- Checkers ----
def check_tcp(host, port, timeout=3):
try:
sock = socket.create_connection((host, port), timeout=timeout)
sock.close()
return True, "ok"
except Exception as e:
return False, str(e)
def check_http(host, port, path, timeout=3):
try:
url = f"http://{host}:{port}{path}"
req = urllib.request.Request(url)
resp = urllib.request.urlopen(req, timeout=timeout)
return 200 <= resp.status < 300, f"HTTP {resp.status}"
except Exception as e:
return False, str(e)
def check_db(db_path):
try:
conn = sqlite3.connect(db_path)
conn.execute("SELECT 1")
conn.close()
return True, "ok"
except Exception as e:
return False, str(e)
# ---- Main ----
def run():
now = datetime.now()
results = []
issues = []
for svc in SERVICES:
if svc["type"] == "tcp":
ok, detail = check_tcp(svc["host"], svc["port"])
elif svc["type"] == "http":
ok, detail = check_http(svc["host"], svc["port"], svc["check"])
elif svc["type"] == "db":
ok, detail = check_db(svc["check"])
else:
ok, detail = False, "unknown type"
results.append({
"name": svc["name"], "label": svc["label"],
"type": svc["type"], "port": svc["port"],
"health": {"ok": ok}, "detail": detail,
})
if not ok:
issues.append(svc)
# Write report
report = {
"services": results,
"summary": {"ok": sum(1 for r in results if r["health"]["ok"]), "total": len(results)},
"generated_at": now.strftime("%Y-%m-%d %H:%M:%S"),
}
with open(REPORT_FILE, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
# ── XMPP 通道健康采集 ──
_collect_xmpp_health(now)
# ── 自愈:检测 rate-limit → 切 keyGateway 异常 → systemctl restart ──
try:
from xmpp_logger import auto_heal as _xmpp_auto_heal
heal_result = _xmpp_auto_heal()
actions = heal_result.get("actions", [])
if actions:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] AUTO-HEAL actions:\n")
for a in actions:
f.write(f" {json.dumps(a, ensure_ascii=False)}\n")
print(f"[{now.strftime('%H:%M')}] Auto-heal: {len(actions)} action(s) — {heal_result.get('status')}")
for a in actions:
print(f" → {a.get('action', '?')}: success={a.get('success', a.get('switched', '?'))}")
except Exception as e:
print(f"[{now.strftime('%H:%M')}] Auto-heal error: {e}")
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] AUTO-HEAL ERROR: {e}\n")
# ── Gateway 端口宕机兜底:异步 systemctl 重启(带 cooldown 防频繁触发)──
gateway_ok = any(r["name"] == "zhiwei_gateway" and r["health"]["ok"] for r in results)
if not gateway_ok:
import time as _t
import subprocess as _sp
cooldown_file = LOGS_DIR / "last_restart.txt"
cooldown_sec = 180
try:
last_ts = float(cooldown_file.read_text().strip()) if cooldown_file.exists() else 0
except Exception:
last_ts = 0
elapsed = _t.time() - last_ts
if elapsed < cooldown_sec:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] Gateway DOWN but cooldown {int(elapsed)}s < {cooldown_sec}s — skip restart\n")
print(f"[{now.strftime('%H:%M')}] Gateway DOWN but in cooldown ({int(elapsed)}s)")
else:
try:
_sp.Popen(["sudo", "-n", "systemctl", "restart", "hermes-gateway-zhiwei.service"],
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL, start_new_session=True)
cooldown_file.write_text(str(_t.time()))
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] Gateway DOWN → systemctl restart triggered (async, cooldown set)\n")
print(f"[{now.strftime('%H:%M')}] Gateway DOWN → systemctl restart triggered")
except Exception as e:
print(f"[{now.strftime('%H:%M')}] Gateway restart error: {e}")
# Handle issues
if issues:
with open(TODO_FILE, "a", encoding="utf-8") as f:
for svc in issues:
entry = {
"service": svc["name"], "label": svc["label"],
"reason": next((r["detail"] for r in results if r["name"] == svc["name"]), "unknown"),
"timestamp": now.isoformat(),
}
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] ISSUES: {len(issues)} failed\n")
for svc in issues:
detail = next((r["detail"] for r in results if r["name"] == svc["name"]), "")
f.write(f" - {svc['label']}: {detail}\n")
print(f"[{now.strftime('%H:%M')}] Health check: {len(issues)}/{len(SERVICES)} services failed")
for svc in issues:
detail = next((r["detail"] for r in results if r["name"] == svc["name"]), "")
print(f" FAIL: {svc['label']} ({svc['name']}) — {detail}")
if __name__ == "__main__":
run()