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