fix(bot): 防递归重连 guard - 防止 on_disconnect → reconnect → disconnect 无限递归导致栈溢出
This commit is contained in:
+304
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
dashboard.py - MoFin management dashboard backend
|
||||
==================================================
|
||||
Minimal Flask app on :5804. Monitors MoFin services and serves
|
||||
module specs (human_help + ai_spec) via ?§ button system.
|
||||
|
||||
Adapted from AgentsMeeting dashboard.py. Does NOT modify server.py.
|
||||
"""
|
||||
import os, sys, json, socket, logging, time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
|
||||
# ---- Paths (auto-detect from script location) ----
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent # MoFin/
|
||||
_TEMPLATES_DIR = _SCRIPT_DIR / "templates"
|
||||
_SPECS_DIR = _SCRIPT_DIR / "specs"
|
||||
_GATEWAY_DIR = _SCRIPT_DIR / "gateway"
|
||||
_LOGS_DIR = _GATEWAY_DIR / "logs"
|
||||
_TEMP_DIR = _GATEWAY_DIR / "temp"
|
||||
|
||||
# Allow override via env
|
||||
_PROJECT_ROOT = os.environ.get("MOFIN_ROOT")
|
||||
if _PROJECT_ROOT:
|
||||
_SCRIPT_DIR = Path(_PROJECT_ROOT)
|
||||
_TEMPLATES_DIR = _SCRIPT_DIR / "templates"
|
||||
_SPECS_DIR = _SCRIPT_DIR / "specs"
|
||||
_GATEWAY_DIR = _SCRIPT_DIR / "gateway"
|
||||
_LOGS_DIR = _GATEWAY_DIR / "logs"
|
||||
_TEMP_DIR = _GATEWAY_DIR / "temp"
|
||||
|
||||
app = Flask(__name__, template_folder=str(_TEMPLATES_DIR))
|
||||
|
||||
# ---- Logging ----
|
||||
_LOG_FILE = _LOGS_DIR / "dashboard.log"
|
||||
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
filename=str(_LOG_FILE),
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [dashboard] %(message)s",
|
||||
)
|
||||
log = logging.getLogger("dashboard")
|
||||
|
||||
# ---- Constants ----
|
||||
PORT = int(os.environ.get("MOFIN_DASHBOARD_PORT", 5807))
|
||||
START_TIME = time.time()
|
||||
|
||||
# ---- Monitored Services ----
|
||||
SERVICES = [
|
||||
{
|
||||
"name": "mofin_api",
|
||||
"label": "MoFin API",
|
||||
"port": 8899,
|
||||
"host": "127.0.0.1",
|
||||
"type": "http",
|
||||
"check": "/api/portfolio",
|
||||
"layer": "核心服务",
|
||||
"critical": True,
|
||||
},
|
||||
{
|
||||
"name": "mofin_dashboard",
|
||||
"label": "Dashboard",
|
||||
"port": 5807,
|
||||
"host": "127.0.0.1",
|
||||
"type": "http",
|
||||
"check": "/api/health",
|
||||
"layer": "核心服务",
|
||||
"critical": True,
|
||||
},
|
||||
{
|
||||
"name": "zhiwei_gateway",
|
||||
"label": "知微 Gateway",
|
||||
"port": 8643,
|
||||
"host": "127.0.0.1",
|
||||
"type": "http",
|
||||
"check": "/v1/health",
|
||||
"layer": "AI 网关",
|
||||
"critical": True,
|
||||
},
|
||||
{
|
||||
"name": "ejabberd",
|
||||
"label": "ejabberd XMPP",
|
||||
"port": 5222,
|
||||
"host": "127.0.0.1",
|
||||
"type": "tcp",
|
||||
"check": None,
|
||||
"layer": "通信层",
|
||||
"critical": True,
|
||||
},
|
||||
{
|
||||
"name": "mofin_db",
|
||||
"label": "MoFin 数据库",
|
||||
"port": 0,
|
||||
"host": "127.0.0.1",
|
||||
"type": "db",
|
||||
"check": "/home/hmo/web-dashboard/data/mofin.db",
|
||||
"layer": "数据层",
|
||||
"critical": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ---- Service Check Helpers ----
|
||||
|
||||
def _check_tcp(host, port, timeout=3):
|
||||
"""Check if TCP port is open."""
|
||||
try:
|
||||
sock = socket.create_connection((host, port), timeout=timeout)
|
||||
sock.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _check_http(host, port, path, timeout=3):
|
||||
"""Check HTTP endpoint returns 2xx."""
|
||||
import urllib.request
|
||||
try:
|
||||
url = f"http://{host}:{port}{path}" if host else f"http://127.0.0.1:{port}{path}"
|
||||
req = urllib.request.Request(url)
|
||||
resp = urllib.request.urlopen(req, timeout=timeout)
|
||||
return 200 <= resp.status < 300
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _check_db(db_path):
|
||||
"""Check SQLite database is accessible."""
|
||||
import sqlite3
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("SELECT 1")
|
||||
conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _check_service(svc):
|
||||
"""Check a single service, return (ok, detail)."""
|
||||
if svc["type"] == "tcp":
|
||||
ok = _check_tcp(svc["host"], svc["port"])
|
||||
return ok, "port open" if ok else "port closed"
|
||||
elif svc["type"] == "http":
|
||||
ok = _check_http(svc["host"], svc["port"], svc["check"])
|
||||
return ok, "HTTP 2xx" if ok else "HTTP fail"
|
||||
elif svc["type"] == "db":
|
||||
ok = _check_db(svc["check"])
|
||||
return ok, "DB accessible" if ok else "DB fail"
|
||||
return False, "unknown type"
|
||||
|
||||
|
||||
# ---- API Endpoints ----
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return send_from_directory(str(_TEMPLATES_DIR), "dashboard.html")
|
||||
|
||||
|
||||
@app.route("/api/health")
|
||||
def api_health():
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"uptime": int(time.time() - START_TIME),
|
||||
"version": "1.0",
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/services")
|
||||
def api_services():
|
||||
"""Return status of all monitored services."""
|
||||
result = []
|
||||
for svc in SERVICES:
|
||||
ok, detail = _check_service(svc)
|
||||
result.append({
|
||||
"name": svc["name"],
|
||||
"label": svc["label"],
|
||||
"port": svc["port"],
|
||||
"type": svc["type"],
|
||||
"layer": svc["layer"],
|
||||
"critical": svc["critical"],
|
||||
"health": {"ok": ok},
|
||||
"detail": detail,
|
||||
})
|
||||
ok_count = sum(1 for s in result if s["health"]["ok"])
|
||||
return jsonify({
|
||||
"services": result,
|
||||
"summary": {"ok": ok_count, "total": len(result)},
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/expected")
|
||||
def api_expected():
|
||||
"""Return expectation matrix."""
|
||||
expected = []
|
||||
for svc in SERVICES:
|
||||
expected.append({
|
||||
"name": svc["name"],
|
||||
"label": svc["label"],
|
||||
"port": svc["port"],
|
||||
"expected": "running",
|
||||
"critical": svc["critical"],
|
||||
"layer": svc["layer"],
|
||||
"check": f"{svc['type']}:{svc['port']}" if svc["port"] else svc["type"],
|
||||
})
|
||||
|
||||
# Actual status
|
||||
actual = {}
|
||||
for svc in SERVICES:
|
||||
ok, _ = _check_service(svc)
|
||||
actual[svc["name"]] = "running" if ok else "stopped"
|
||||
|
||||
return jsonify({
|
||||
"expected": expected,
|
||||
"actual": actual,
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/monitor")
|
||||
def api_monitor():
|
||||
"""Aggregate health check data from Tier1/Tier2 reports."""
|
||||
tasks = []
|
||||
tier1 = {"summary": {"ok": 0, "total": 0}, "services": []}
|
||||
tier2 = {"summary": {"ok": 0, "total": 0}, "services": []}
|
||||
|
||||
# Try to read Tier1 report
|
||||
t1_path = _TEMP_DIR / "last_health_check.json"
|
||||
if t1_path.exists():
|
||||
try:
|
||||
with open(t1_path, encoding="utf-8") as f:
|
||||
tier1 = json.load(f)
|
||||
tasks.append({"name": "agents-health-check", "status": "cron_ok"})
|
||||
except Exception:
|
||||
tasks.append({"name": "agents-health-check", "status": "error"})
|
||||
else:
|
||||
tasks.append({"name": "agents-health-check", "status": "not_deployed"})
|
||||
|
||||
# Try to read Tier2 report
|
||||
t2_path = _TEMP_DIR / "last_daily_health.json"
|
||||
if t2_path.exists():
|
||||
try:
|
||||
with open(t2_path, encoding="utf-8") as f:
|
||||
tier2 = json.load(f)
|
||||
tasks.append({"name": "agents-daily-health", "status": "cron_ok"})
|
||||
except Exception:
|
||||
tasks.append({"name": "agents-daily-health", "status": "error"})
|
||||
else:
|
||||
tasks.append({"name": "agents-daily-health", "status": "not_deployed"})
|
||||
|
||||
# Self-check: are we running?
|
||||
svc_result = api_services().get_json()
|
||||
tasks.append({
|
||||
"name": "dashboard",
|
||||
"status": "running",
|
||||
"detail": f"services: {svc_result.get('summary', {}).get('ok', 0)}/{svc_result.get('summary', {}).get('total', 0)}",
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"tasks": tasks,
|
||||
"tier1": tier1,
|
||||
"tier2": tier2,
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/module-spec/<module>")
|
||||
def api_module_spec(module):
|
||||
"""Serve spec JSON for a module."""
|
||||
# Safety: prevent path traversal
|
||||
module = module.replace("..", "").replace("/", "").replace("\\", "")
|
||||
spec_path = _SPECS_DIR / f"{module}.json"
|
||||
if spec_path.exists():
|
||||
try:
|
||||
with open(spec_path, encoding="utf-8") as f:
|
||||
return jsonify(json.load(f))
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Failed to read spec: {e}"}), 500
|
||||
return jsonify({"error": f"Module '{module}' not found"}), 404
|
||||
|
||||
|
||||
# ---- Main ----
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Ensure directories exist
|
||||
_LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log.info(f"MoFin Dashboard starting on port {PORT}")
|
||||
log.info(f"Specs dir: {_SPECS_DIR}")
|
||||
log.info(f"Templates dir: {_TEMPLATES_DIR}")
|
||||
|
||||
# Optional: PID guard
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
from proc_guard import guard
|
||||
if not guard("mofin_dashboard"):
|
||||
log.error("Another dashboard instance is already running")
|
||||
sys.exit(1)
|
||||
except ImportError:
|
||||
log.warning("proc_guard not available, skipping PID lock")
|
||||
|
||||
app.run(host="0.0.0.0", port=PORT, debug=False)
|
||||
Reference in New Issue
Block a user