#!/usr/bin/env python3 """ agents_health_check.py — Tier 1 快速健康检查(Linux/246 部署版) ============================================================ 每 5 分钟触发。对所有注册服务做检查:PID存活 → 端口监听 → HTTP /health。 异常时写 TODO 给 self_todo_executor 消费。正常时静默。 部署:Linux crontab: */5 * * * * cd /home/hmo/AgentsMeeting/gateway/scripts && /usr/bin/python3 agents_health_check.py >> ../logs/health_check.log 2>&1 """ import json, os, sys, socket, subprocess from datetime import datetime from urllib.request import urlopen, Request from urllib.error import HTTPError, URLError SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) GATEWAY_DIR = os.path.dirname(SCRIPT_DIR) BASE = os.path.dirname(GATEWAY_DIR) TEMP = os.path.join(GATEWAY_DIR, "temp") LOGS = os.path.join(GATEWAY_DIR, "logs") os.makedirs(TEMP, exist_ok=True) os.makedirs(LOGS, exist_ok=True) NOW = datetime.now().strftime("%Y-%m-%d %H:%M:%S") TODO_FILE = os.path.join(TEMP, "health_todos.jsonl") REPORT_FILE = os.path.join(TEMP, "last_health_check.json") HEALTH_LOG = os.path.join(LOGS, "health_check_report.log") # ── 服务注册表(Linux 246 视角)────────────────────────────── # 本机服务用 127.0.0.1,远程服务用实际 IP + HTTP only SERVICES = [ # === 本机服务(246)=== { "name": "dashboard", "host": "127.0.0.1", "port": 5803, "health_url": "http://127.0.0.1:5803/api/health", "fix_cmd": ["sudo", "systemctl", "restart", "agentsmeeting-dashboard"], "remote": False, "critical": True, }, { "name": "hermes_gateway_mohe", "host": "127.0.0.1", "port": 8642, "health_url": "http://127.0.0.1:8642/v1/health", "fix_cmd": ["sudo", "systemctl", "restart", "hermes-gateway@mohe"], "remote": False, "critical": True, }, { "name": "hermes_gateway_zhiwei", "host": "127.0.0.1", "port": 8643, "health_url": "http://127.0.0.1:8643/v1/health", "fix_cmd": ["sudo", "systemctl", "restart", "hermes-gateway@zhiwei"], "remote": False, "critical": True, }, { "name": "wechat_bridge", "host": "127.0.0.1", "port": 3001, "health_url": None, # TCP only "fix_cmd": ["docker", "restart", "wxBotWebhook"], "remote": False, "critical": True, }, # === 远程服务(跨网 HTTP 检查)=== { "name": "xmpp_bot_xxm", "host": "192.168.1.16", "port": 5802, "health_url": "http://192.168.1.16:5802/health", "fix_cmd": None, # 远程服务无法自动修复 "remote": True, "critical": True, }, { "name": "article_processor", "host": "192.168.1.16", "port": 5810, "health_url": "http://192.168.1.16:5810/health", "fix_cmd": None, "remote": True, "critical": False, }, ] # ── 检查基元(Linux 原生)──────────────────────────────── def log(msg): ts = datetime.now().strftime("%H:%M:%S") line = f"[{ts}] {msg}" with open(HEALTH_LOG, "a", encoding="utf-8") as f: f.write(line + "\n") def pid_alive(pid): """发送信号 0 测试进程存活。""" try: os.kill(pid, 0) return True except (ProcessLookupError, PermissionError): return False def port_open(host, port, timeout=3): """socket connect 测试端口是否监听。""" try: s = socket.create_connection((host, port), timeout=timeout) s.close() return True except (socket.timeout, ConnectionRefusedError, OSError): return False def http_check(url, timeout=5): """HTTP /health 检查。返回 (ok, detail)。""" if not url: return (True, "no_health_url") try: req = Request(url) with urlopen(req, timeout=timeout) as r: return (r.status == 200, f"HTTP {r.status}") except HTTPError as e: return (e.code in (401, 403), f"HTTP {e.code}") except URLError as e: return (False, str(e.reason)[:60]) except Exception as e: return (False, str(e)[:60]) def write_todo(name, issue): """写 TODO 供 executor 消费。""" entry = { "created": NOW, "service": name, "issue": issue, "status": "pending", } with open(TODO_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") log(f"TODO: {name} — {issue}") # ── 主检查 ──────────────────────────────────────────── def main(): report = {"time": NOW, "services": [], "summary": {"total": 0, "ok": 0, "fail": 0}} dirty = False for svc in SERVICES: name = svc["name"] host = svc["host"] port = svc["port"] url = svc.get("health_url") is_remote = svc.get("remote", False) critical = svc.get("critical", True) entry = {"name": name, "critical": critical} report["services"].append(entry) report["summary"]["total"] += 1 # 1. Port check(socket connect,跨平台) port_ok = port_open(host, port) entry["port_ok"] = port_ok # 2. HTTP check if port_ok: ok, detail = http_check(url, timeout=5 if is_remote else 8) entry["http_ok"] = ok entry["http_detail"] = detail else: entry["http_ok"] = False entry["http_detail"] = "port_closed" # 3. 判定:Port+HTTP OK = 健康 primary_ok = port_ok and entry.get("http_ok", False) entry["status"] = "ok" if primary_ok else "fail" if primary_ok: report["summary"]["ok"] += 1 else: report["summary"]["fail"] += 1 dirty = True reasons = [] if not port_ok: reasons.append("port_closed") if port_ok and not entry.get("http_ok"): reasons.append(f"http_{entry.get('http_detail','?')}") issue = f"异常: {' + '.join(reasons)}" write_todo(name, issue) # 输出报告 summary = report["summary"] if dirty: log(f"=== 健康检查: {summary['ok']}/{summary['total']} OK, {summary['fail']} FAIL ===") for s in report["services"]: if s["status"] == "fail": print(f"[FAIL] {s['name']}: PORT={s['port_ok']} HTTP={s.get('http_detail','?')}") else: log(f"=== 健康检查: {summary['ok']}/{summary['total']} 全部正常 ===") with open(REPORT_FILE, "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) if __name__ == "__main__": main()