Phase 1-2: 监控体系全面就绪

- 多进程看门狗: xmpp_watchdog.py (守护 bot + article_processor)
- Tier 1 快速健康检查: agents_health_check.py (5min)
- Tier 2 全面健康检查: agents_daily_health.py (每日08:00)
- TODO 自修复执行器: self_todo_executor.py (10min)
- 共享服务注册表: service_registry.py
- xmpp_agent_core: /health 免认证 + /send 端点
- Dashboard: SSH检测修复 + kanban版块 + 路径跨平台兼容
- 修复kanban-api-reference.md误导内容
- 修复R02: frp隧道heartbeat保活
This commit is contained in:
hmo
2026-07-14 02:49:07 +08:00
parent d53aa1e06b
commit 28ae15d49e
9 changed files with 1287 additions and 193 deletions
+97 -31
View File
@@ -9,22 +9,35 @@ 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
import os, sys, re, json, time, subprocess, logging, urllib.request, sqlite3
from pathlib import Path
from datetime import datetime, timedelta
from flask import Flask, jsonify, request, send_from_directory
# ---- Paths ----
PROJECT_ROOT = Path("/home/hmo/agentsmeeting-venv")
GATEWAY_ROOT = Path("/home/hmo/agentsmeeting-venv")
CONFIG_DIR = Path("/home/hmo/agentsmeeting-venv/config")
LOGS_DIR = Path("/home/hmo/agentsmeeting-venv/logs")
TEMPLATES_DIR = Path("/home/hmo/agentsmeeting-venv/templates")
# ---- Paths (platform-agnostic: auto-detect from script location) ----
_SCRIPT_DIR = Path(__file__).resolve().parent # gateway/scripts/
_GATEWAY_DIR = _SCRIPT_DIR.parent # gateway/
_PROJECT_DIR = _GATEWAY_DIR.parent # AgentsMeeting/
TEMPLATES_DIR = _SCRIPT_DIR / "templates"
CONFIG_DIR = _PROJECT_DIR / "config"
LOGS_DIR = _GATEWAY_DIR / "logs"
TEMP_DIR = _GATEWAY_DIR / "temp"
XMPP_BRIDGE_URL = os.environ.get("XMPP_BRIDGE_URL", "http://127.0.0.1:5802")
EJABBERD_HOST = os.environ.get("EJABBERD_HOST", "192.168.1.246")
sys.path.insert(0, str(GATEWAY_ROOT / "scripts"))
# On Linux (mohe), dashboard runs from systemd with different paths. Support explicit override.
_PROJECT_OVERRIDE = os.environ.get("AGENTSMEETING_ROOT")
if _PROJECT_OVERRIDE:
_PROJECT_DIR = Path(_PROJECT_OVERRIDE)
_GATEWAY_DIR = _PROJECT_DIR / "gateway"
TEMPLATES_DIR = _GATEWAY_DIR / "scripts" / "templates"
CONFIG_DIR = _PROJECT_DIR / "config"
LOGS_DIR = _GATEWAY_DIR / "logs"
TEMP_DIR = _GATEWAY_DIR / "temp"
sys.path.insert(0, str(_GATEWAY_DIR / "scripts"))
from proc_guard import guard
# ---- Flask ----
app = Flask(__name__, template_folder=str(TEMPLATES_DIR))
# ---- Logging ----
@@ -39,18 +52,8 @@ log = logging.getLogger("dashboard")
# ---- Constants ----
AGENTS_YAML = CONFIG_DIR / "agents.yaml"
SCRIPT_NAMES = {
"xmpp_bot": "xmpp_bot.py",
"wechat_bridge": "wechat_agent.py",
"api_proxy": "api_proxy.py",
"health_check": "health_check_xxm.py",
"mohe_watcher": "mohe_watcher.py",
"watchdog": "xmpp_watchdog.py",
}
PYTHON = "/home/hmo/agentsmeeting-venv/bin/python3"
SCRIPTS_DIR = Path("/home/hmo/agentsmeeting-venv")
XMPP_BRIDGE_URL = "http://192.168.1.16:5802"
EJABBERD_HOST = "192.168.1.246"
PYTHON = os.environ.get("PYTHON", sys.executable)
SCRIPTS_DIR = _GATEWAY_DIR / "scripts"
# Auto-recovery: restart after this many consecutive offline checks
AUTO_RECOVER_THRESHOLD = 3
@@ -113,13 +116,24 @@ def _xmpp_health():
def _ejabberd_online_jids():
"""SSH to Linux and run ejabberdctl connected_users.
Returns set of bare JIDs currently connected to ejabberd.
This is the authoritative cross-platform presence source."""
"""Query ejabberdctl connected_users.
On Linux (local): direct docker exec.
On Windows (remote): SSH to EJABBERD_HOST.
Returns set of bare JIDs currently connected to ejabberd."""
try:
cmd = ["docker", "exec", "ejabberd", "ejabberdctl", "connected_users"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if sys.platform == "win32":
# Windows: SSH to Linux server
ssh_cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5",
"-o", "BatchMode=yes", f"hmo@{EJABBERD_HOST}",
"docker exec ejabberd ejabberdctl connected_users"]
result = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=15)
else:
# Linux: direct docker exec
result = subprocess.run(
["docker", "exec", "ejabberd", "ejabberdctl", "connected_users"],
capture_output=True, text=True, timeout=10)
if result.returncode != 0:
log.debug(f"ejabberdctl failed (exit={result.returncode}): {result.stderr[:100]}")
return set()
jids = set()
for line in result.stdout.strip().split("\n"):
@@ -128,7 +142,7 @@ def _ejabberd_online_jids():
jids.add(line.split("/")[0])
return jids
except Exception as e:
log.debug(f"ejabberd SSH query failed: {e}")
log.debug(f"ejabberd query failed: {e}")
return set()
@@ -454,6 +468,9 @@ PLATFORM_SERVICES = [
{"id": "api_proxy", "name": "API Proxy", "type": "APIRouter",
"desc": "proxies volcengine API with retry/fallback",
"host": "192.168.1.16", "port": 8787},
{"id": "article_processor", "name": "文章抓取服务 (5810)", "type": "wechat-fetch",
"desc": "fetches wechat article content + OCR",
"host": "192.168.1.16", "health_url": "http://192.168.1.16:5810/health"},
]
@app.route("/api/platform")
@@ -463,12 +480,18 @@ def api_platform():
result = []
for ps in PLATFORM_SERVICES:
status = "stopped"
health_data = None
health_url = ps.get("health_url", "")
if health_url:
try:
req = _ur.Request(health_url)
_ur.urlopen(req, timeout=3)
status = "running"
with _ur.urlopen(req, timeout=3) as resp:
body = resp.read()
status = "running"
try:
health_data = json.loads(body)
except Exception:
pass
except Exception:
status = "stopped"
elif "port" in ps:
@@ -481,16 +504,59 @@ def api_platform():
status = "running"
except Exception:
status = "stopped"
result.append({
entry = {
"id": ps["id"],
"name": ps["name"],
"type": ps["type"],
"desc": ps["desc"],
"status": status,
})
}
if health_data:
entry["health_data"] = health_data
result.append(entry)
return jsonify(result)
@app.route("/api/service/5810/logs")
def api_article_processor_logs():
"""Proxy logs from article-processor service on Windows."""
try:
req = urllib.request.Request("http://192.168.1.16:5810/logs?lines=200")
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read())
lines = data.get("lines", [])
response = jsonify({"lines": lines})
response.headers["Access-Control-Allow-Origin"] = "*"
return response
except Exception as e:
response = jsonify({"lines": ["<error: {}>".format(str(e))], "error": True})
response.headers["Access-Control-Allow-Origin"] = "*"
return response
@app.route("/api/kanban")
def api_kanban():
"""Return kanban tasks from shared Hermes kanban.db."""
kanban_db = Path(os.path.expanduser("~/.hermes/kanban.db"))
if not kanban_db.exists():
return jsonify({"tasks": [], "db_exists": False})
try:
db = sqlite3.connect(str(kanban_db))
rows = db.execute(
"SELECT id, title, body, status, assignee, created_by, created_at FROM tasks ORDER BY created_at DESC"
).fetchall()
db.close()
tasks = []
for r in rows:
tasks.append({
"id": r[0], "title": r[1], "body": r[2], "status": r[3],
"assignee": r[4], "created_by": r[5], "created_at": r[6]
})
return jsonify({"tasks": tasks, "db_exists": True, "count": len(tasks)})
except Exception as e:
return jsonify({"error": str(e), "db_exists": True})
@app.route("/api/platform")
@app.route("/api/health")
def api_health():