Files
AgentsMeeting/gateway/scripts/agents_health_check.py
T
hmo 69fa67e24f F可视化: dashboard 完整实现 A-F 六原则呈现 + 弹窗修复
- A: git log + dirty status
- B: 服务架构矩阵 (/health + watchdog覆盖 + 拓扑依赖)
- C: Tier1/Tier2 监控结果 + 定时任务状态
- D: TODO执行记录 + escalation日志
- F: 期望状态 vs 实际状态合规矩阵
- Kanban: dashboard内嵌版块
- 弹窗修复: 计划任务改用pythonw.exe + subprocess加CREATE_NO_WINDOW
- 修复ast_grep_replace导致的
2026-07-14 02:49:07 +08:00

185 lines
6.3 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.
"""
agents_health_check.py — Tier 1 快速健康检查
每 5 分钟(Task Scheduler)触发。对所有注册服务做三合一检查:
PID存活 → 端口监听 → HTTP /health
异常时写 TODOhealth_todos.jsonl),由 self_todo_executor 消费。
正常时不输出任何噪音(silent-by-default)。
"""
import json, os, sys, time, subprocess, urllib.request, urllib.error
from datetime import datetime
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# 从共享注册表导入服务定义
sys.path.insert(0, SCRIPT_DIR)
from service_registry import SERVICES as _SR, TEMP as _TEMP, GATEWAY_DIR as _GD
TEMP = _TEMP
LOGS = os.path.join(_GD, "logs")
os.makedirs(TEMP, exist_ok=True)
os.makedirs(LOGS, exist_ok=True)
# 转成 Tier 1 需要的格式(补充 fix_* 字段)
SERVICES = []
for svc in _SR:
SERVICES.append({
"name": svc["name"],
"pid_file": svc["pid_file"],
"port": svc["port"],
"health_url": svc["health_url"],
"accept_401": svc.get("accept_401", False),
"fix_script": svc["script"],
"fix_args": svc["args"],
"fix_cwd": svc["workdir"],
})
TODO_FILE = os.path.join(TEMP, "health_todos.jsonl")
HEALTH_LOG = os.path.join(LOGS, "health_check_report.log")
NOW = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# ── Helpers ──────────────────────────────────────────────
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 read_pid(path):
try:
with open(path) as f:
return int(f.read().strip())
except:
return 0
def pid_alive(pid):
try:
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH"],
capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
return str(pid) in r.stdout
except:
return False
def port_open(port):
try:
r = subprocess.run(["netstat", "-ano"], capture_output=True,
text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
return any(f":{port} " in line and "LISTENING" in line
for line in r.stdout.splitlines())
except:
return False
def http_check(url, timeout=5):
try:
r = urllib.request.urlopen(url, timeout=timeout)
return (r.status == 200, f"HTTP {r.status}")
except urllib.error.HTTPError as e:
return (False, f"HTTP {e.code}")
except urllib.error.URLError as e:
return (False, str(e.reason)[:60])
except Exception as e:
return (False, str(e)[:60])
def write_todo(name, issue, fix_script, fix_args, fix_cwd):
"""写一条 TODO 给 self_todo_executor 消费。
JSONL 每行一条,含 service/issue/时间/fix_action。
"""
entry = {
"created": NOW,
"service": name,
"issue": issue,
"fix_script": fix_script,
"fix_args": fix_args,
"fix_cwd": fix_cwd,
"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}")
# ── Main check ───────────────────────────────────────────
def main():
report = {"time": NOW, "services": [], "summary": {"total": 0, "ok": 0, "fail": 0}}
dirty = False
for svc in SERVICES:
name = svc["name"]
entry = {"name": name}
report["services"].append(entry)
report["summary"]["total"] += 1
# 1. PID check(允许无 PID 文件的服务,仅靠 port+HTTP 判断)
pid_file = svc.get("pid_file")
if pid_file:
pid = read_pid(pid_file)
pid_ok = pid > 0 and pid_alive(pid)
else:
pid = 0
pid_ok = True # 无 PID 文件不视为异常
entry["pid"] = pid if pid_ok else 0
entry["pid_ok"] = pid_ok
# 2. Port check
port_ok = port_open(svc["port"])
entry["port_ok"] = port_ok
# 3. HTTP health check
http_msg = ""
if port_ok:
ok, detail = http_check(svc["health_url"])
# accept 401/403 as healthy if configured
if not ok and svc.get("accept_401") and ("401" in detail or "403" in detail):
http_msg = "auth (alive)"
ok = True
else:
http_msg = detail if not ok else "ok"
entry["http_ok"] = ok
entry["http_detail"] = http_msg
else:
entry["http_ok"] = False
entry["http_detail"] = "port_closed"
healthy = (pid_ok or not pid_file) and port_ok and entry.get("http_ok", False)
if healthy:
entry["status"] = "ok"
report["summary"]["ok"] += 1
else:
entry["status"] = "fail"
report["summary"]["fail"] += 1
dirty = True
# 收集具体失败原因
reasons = []
if not pid_ok: reasons.append("pid_dead")
if not port_ok: reasons.append("port_closed")
if not entry.get("http_ok") and port_ok: reasons.append(f"http_{http_msg}")
issue = f"异常: {' + '.join(reasons)}"
write_todo(name, issue, svc["fix_script"], svc.get("fix_args", []), svc["fix_cwd"])
# 只在有异常时写完整报告 + 输出到 stdout(给 Task Scheduler 日志)
if dirty:
summary = report["summary"]
log(f"=== 健康检查: {summary['ok']}/{summary['total']} 正常, {summary['fail']} 异常 ===")
# 只输出异常服务到 stdoutsilent-by-default
for svc in report["services"]:
if svc["status"] != "ok":
print(f"[FAIL] {svc['name']}: PID={svc['pid_ok']} PORT={svc['port_ok']} HTTP={svc.get('http_detail','?')}")
else:
# 全正常→完全静默(silent-by-default
pass
# 保留报告到文件(供 dashboard 读取)
with open(os.path.join(TEMP, "last_health_check.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False)
if __name__ == "__main__":
main()