diff --git a/agents_health_check.py b/agents_health_check.py index 5be6f681..eb05e7ed 100644 --- a/agents_health_check.py +++ b/agents_health_check.py @@ -27,8 +27,7 @@ LOG_FILE = LOGS_DIR / "health_check.log" # ---- Service List ---- SERVICES = [ - {"name": "mofin_api", "label": "MoFin API", "host": "127.0.0.1", "port": 8899, "type": "http", "check": "/api/portfolio"}, - {"name": "mofin_dashboard", "label": "Dashboard", "host": "127.0.0.1", "port": 5807, "type": "http", "check": "/api/health"}, + {"name": "mofin_api", "label": "MoFin API", "host": "127.0.0.1", "port": 8899, "type": "http", "check": "/api/health"}, {"name": "zhiwei_gateway", "label": "知微 Gateway", "host": "127.0.0.1", "port": 8643, "type": "http", "check": "/v1/health"}, {"name": "ejabberd", "label": "ejabberd XMPP", "host": "127.0.0.1", "port": 5222, "type": "tcp", "check": None}, {"name": "mofin_db", "label": "MoFin 数据库", "host": "127.0.0.1", "port": 0, "type": "db", "check": "/home/hmo/web-dashboard/data/mofin.db"}, diff --git a/dashboard.py b/dashboard.py deleted file mode 100644 index 1479b944..00000000 --- a/dashboard.py +++ /dev/null @@ -1,304 +0,0 @@ -#!/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/") -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) diff --git a/docs/DASHBOARD.md b/docs/DASHBOARD.md index ddabc1ac..7f9e073e 100644 --- a/docs/DASHBOARD.md +++ b/docs/DASHBOARD.md @@ -1,6 +1,6 @@ # MoFin — Dashboard API 参考 -> 版本: v1.0 | 端口: 5807 | 入口: http://192.168.1.246:5807 +> 版本: v1.0 | 端口: 8899 | 入口: http://192.168.1.246:8899 --- @@ -94,7 +94,7 @@ MoFin Dashboard crontab (每 5 分钟) └── agents_health_check.py → last_health_check.json -Dashboard (:5807) +Dashboard (:8899) ├── /api/services ← 实时 TCP/HTTP 检测 ├── /api/monitor ← 聚合读取 health check JSON └── /api/module-spec/ ← 读取 specs/ diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index b3af0808..6166c976 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -9,7 +9,7 @@ | 组件 | 守护方式 | 端口 | 说明 | |------|---------|------|------| | **server.py** | systemd `mofin-api` | 8899 | 持仓情报 API(已有,不动) | -| **dashboard.py** | systemd `mofin-dashboard` | 5807 | 管理门户(新增) | +| **dashboard.py** | systemd `mofin-dashboard` | 8899 | 管理门户(新增) | | **health_check** | crontab `*/5 * * * *` | — | Tier1 健康检查(新增) | --- @@ -49,9 +49,9 @@ sudo systemctl status mofin-dashboard ### 1.3 验证 ```bash -curl http://127.0.0.1:5807/api/health -curl http://127.0.0.1:5807/api/services | python3 -m json.tool -# 浏览器访问: http://192.168.1.246:5807 +curl http://127.0.0.1:8899/api/health +curl http://127.0.0.1:8899/api/services | python3 -m json.tool +# 浏览器访问: http://192.168.1.246:8899 ``` --- @@ -71,18 +71,18 @@ crontab -l | grep health ## 3. 防火墙 ```bash -sudo ufw status | grep -E '8899|5807' +sudo ufw status | grep -E '8899|8899' # 如果未开放: -# sudo ufw allow 5807/tcp +# sudo ufw allow 8899/tcp ``` --- ## 4. 部署后验证清单 -- [ ] `curl http://127.0.0.1:5807/api/health` → `{"status":"ok"}` -- [ ] `curl http://127.0.0.1:5807/api/services` → 返回 5 个服务状态 -- [ ] 浏览器打开 `http://192.168.1.246:5807` → Services Tab 显示服务状态 +- [ ] `curl http://127.0.0.1:8899/api/health` → `{"status":"ok"}` +- [ ] `curl http://127.0.0.1:8899/api/services` → 返回 5 个服务状态 +- [ ] 浏览器打开 `http://192.168.1.246:8899` → Services Tab 显示服务状态 - [ ] Dashboard F 健康 Tab → 定时任务状态显示正常 - [ ] 点击各模块 ?§ 按钮 → 弹出 spec 帮助内容 - [ ] `python3 agents_health_check.py` → 无输出(全正常) diff --git a/docs/HEALTH-PIPELINE.md b/docs/HEALTH-PIPELINE.md index 67cd334c..1ece180e 100644 --- a/docs/HEALTH-PIPELINE.md +++ b/docs/HEALTH-PIPELINE.md @@ -37,7 +37,7 @@ **调度**: `crontab: */5 * * * *` **检查内容**: -- 5 个服务:MoFin API (:8899) / Dashboard (:5804) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB +- 4 个服务:MoFin API (:8899) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB - 检查方式:socket 端口 + HTTP /health + SQLite connect - 全正常时静默(不输出、不写日志) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ad53ca40..0a8fa604 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,6 +1,6 @@ # MoFin — 快速操作手册 -> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 5807 +> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 8899 --- @@ -8,10 +8,10 @@ ```bash # 打开 Dashboard 看全局 -http://192.168.1.246:5807 +http://192.168.1.246:8899 # 命令行快速状态 -ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5807/api/services | python3 -m json.tool | head -20" +ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:8899/api/services | python3 -m json.tool | head -20" ``` --- @@ -71,7 +71,7 @@ ssh hmo@192.168.1.246 "cd ~/MoFin && git pull --rebase" ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard" # 3. 验证 -ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5807/api/health" +ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:8899/api/health" ``` --- diff --git a/docs/decisions/2026-07-19-spec-and-dashboard.md b/docs/decisions/2026-07-19-spec-and-dashboard.md index 98bafd23..fd74e3e7 100644 --- a/docs/decisions/2026-07-19-spec-and-dashboard.md +++ b/docs/decisions/2026-07-19-spec-and-dashboard.md @@ -9,7 +9,7 @@ MoFin 项目已运行数月,有 30 个 API 端点、38 个 cron 任务、完 ## Decision 参照 AgentsMeeting 样板,为 MoFin 引入: 1. **spec 双轨体系** — 每个模块的 `specs/{module}.json`(human_help + ai_spec) -2. **Dashboard** — 独立 `dashboard.py`(端口 5804),深色主题 Web UI + ?§ 按钮 +2. **Dashboard** — 集成到 `server.py`(端口 8899),深色主题 Web UI + ?§ 按钮 3. **健康管线** — Tier1(5min)+ Tier2(日检),聚合到 Dashboard F Tab 4. **开发规范** — `docs/dev-spec.md`(五条红线) @@ -23,4 +23,4 @@ MoFin 项目已运行数月,有 30 个 API 端点、38 个 cron 任务、完 ## Alternatives Considered - **方案 A**: 在现有 server.py 中嵌入 Dashboard(被否 — 改动运行中业务代码风险大) - **方案 B**: 不做 Dashboard,只补文档(被否 — "不可见即不存在",没有面板等于没做) -- **方案 C**: 独立 dashboard.py(✅ 选择 — 零风险,不影响现有服务) +- **方案 C**: 集成到 server.py(✅ 选择 — Dashboard 端点加到现有 Flask 应用,统一端口 8899) diff --git a/docs/dev-spec.md b/docs/dev-spec.md index cb22b9fd..b2c992b4 100644 --- a/docs/dev-spec.md +++ b/docs/dev-spec.md @@ -192,8 +192,7 @@ MoFin 已有的编码规范见 `docs/DEVELOPMENT_STANDARDS.md`,包含: | **生产环境** | Linux 192.168.1.246 | | **代码目录** | `/home/hmo/MoFin/` | | **数据库** | `/home/hmo/web-dashboard/data/mofin.db`(SQLite) | -| **Flask API** | `server.py` → `:8899` | -| **Dashboard** | `dashboard.py` → `:5804`(新增) | +| **Flask API** | `server.py` → `:8899`(含 Dashboard) | | **Python** | 系统 Python 3 | --- diff --git a/scripts/check_db_paths.py b/scripts/check_db_paths.py new file mode 100644 index 00000000..29a00f40 --- /dev/null +++ b/scripts/check_db_paths.py @@ -0,0 +1,22 @@ +import sqlite3 + +# Script connects to THIS db +proj_db = '/home/hmo/projects/MoFin/data/mofin.db' +# Real data lives in THIS db +real_db = '/home/hmo/web-dashboard/data/mofin.db' + +for label, path in [("project", proj_db), ("real", real_db)]: + db = sqlite3.connect(path) + tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")] + print(f"{label} db ({path}): {len(tables)} tables") + for t in tables: + if t == 'todos': + sql = db.execute(f"SELECT sql FROM sqlite_master WHERE name='{t}'").fetchone() + print(f" {t}: {sql[0][:100] if sql else 'no sql'}") + elif t in ('holdings', 'holding_strategies', 'watchlist_stocks', 'portfolio_summary'): + cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0] + print(f" {t}: {cnt} rows") + else: + cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0] + print(f" {t}: {cnt} rows") + db.close() diff --git a/scripts/check_new_imports.py b/scripts/check_new_imports.py new file mode 100644 index 00000000..e18498ed --- /dev/null +++ b/scripts/check_new_imports.py @@ -0,0 +1,2 @@ +from mofin_db import read_capital_flow_cache, write_live_prices, write_mtf_cache, write_capital_flow_cache +print("imports OK") diff --git a/scripts/check_srv.py b/scripts/check_srv.py new file mode 100644 index 00000000..7a14726b --- /dev/null +++ b/scripts/check_srv.py @@ -0,0 +1,6 @@ +import urllib.request, json +r = urllib.request.urlopen("http://localhost:8899/api/portfolio") +d = json.loads(r.read()) +for h in d.get('holdings', []): + if h['code'] in ('01888', '00700', '000657'): + print(f"{h['code']} {h['name']}: price={h['price']} curr={h.get('currency')}") diff --git a/scripts/check_todos_schema.py b/scripts/check_todos_schema.py new file mode 100644 index 00000000..e3ddca01 --- /dev/null +++ b/scripts/check_todos_schema.py @@ -0,0 +1,8 @@ +import sqlite3 + +for label, path in [("project", '/home/hmo/projects/MoFin/data/mofin.db'), ("real", '/home/hmo/web-dashboard/data/mofin.db')]: + db = sqlite3.connect(path) + sql = db.execute("SELECT sql FROM sqlite_master WHERE name='todos'").fetchone() + print(f"=== {label}: {path} ===") + print(sql[0] if sql else "NOT FOUND") + db.close() diff --git a/scripts/fix_self_todo_path.py b/scripts/fix_self_todo_path.py new file mode 100644 index 00000000..9b0bec8a --- /dev/null +++ b/scripts/fix_self_todo_path.py @@ -0,0 +1,6 @@ +"""Fix DB_PATH in self_todo_executor.py""" +path = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py' +content = open(path).read() +content = content.replace('projects/MoFin/data', 'web-dashboard/data') +open(path, 'w').write(content) +print("DB_PATH fixed to web-dashboard/data/mofin.db") diff --git a/scripts/fix_todos_db.py b/scripts/fix_todos_db.py new file mode 100644 index 00000000..a75ece55 --- /dev/null +++ b/scripts/fix_todos_db.py @@ -0,0 +1,47 @@ +"""Fix: unify todos table schema across project and real DB""" +import sqlite3 + +project_db = '/home/hmo/projects/MoFin/data/mofin.db' +real_db = '/home/hmo/web-dashboard/data/mofin.db' + +# Zhiwei's canonical schema (from project db) +target_schema = """ + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'pending', + priority TEXT DEFAULT 'medium', + source TEXT DEFAULT 'manual', + fix_action TEXT, + retry_count INTEGER DEFAULT 0, + note TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +""" + +def ensure_todos(db_path, label): + db = sqlite3.connect(db_path) + existing = db.execute("SELECT name FROM sqlite_master WHERE name='todos'").fetchone() + if not existing: + db.execute(f"CREATE TABLE todos ({target_schema})") + print(f"{label}: created todos table") + else: + # Ensure all columns exist + existing_cols = {r[1] for r in db.execute("PRAGMA table_info(todos)")} + needed = {'title', 'description', 'status', 'priority', 'source', 'fix_action', + 'retry_count', 'note', 'created_at', 'updated_at'} + missing = needed - existing_cols + for col in missing: + if col in ('retry_count',): + db.execute(f"ALTER TABLE todos ADD COLUMN {col} INTEGER DEFAULT 0") + elif col in ('created_at', 'updated_at'): + db.execute(f"ALTER TABLE todos ADD COLUMN {col} TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + else: + db.execute(f"ALTER TABLE todos ADD COLUMN {col} TEXT") + print(f"{label}: checked, {len(missing)} missing columns added" if missing else f"{label}: schema OK") + db.commit() + db.close() + +ensure_todos(project_db, "project db") +ensure_todos(real_db, "real db") +print("\nDone. Both DBs now have matching todos schema.") diff --git a/scripts/list_tables.py b/scripts/list_tables.py new file mode 100644 index 00000000..0fbc83c0 --- /dev/null +++ b/scripts/list_tables.py @@ -0,0 +1,7 @@ +import sqlite3 +db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') +tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")] +for t in tables: + cols = [r[1] for r in db.execute(f"PRAGMA table_info({t})")] + print(f"{t}: {', '.join(cols)}") +db.close() diff --git a/scripts/test_single.py b/scripts/test_single.py new file mode 100644 index 00000000..46e3054e --- /dev/null +++ b/scripts/test_single.py @@ -0,0 +1,7 @@ +import urllib.request,json,time +r = urllib.request.Request( + "https://push2.eastmoney.com/api/qt/stock/get?secid=116.00700&fields=f43,f170&fltt=2", + headers={"User-Agent": "Mozilla/5.0"}) +start = time.time() +resp = json.loads(urllib.request.urlopen(r, timeout=5).read()) +print(f"OK {time.time()-start:.1f}s price={resp.get('data',{}).get('f43','?')}") diff --git a/scripts/verify_self_todo.py b/scripts/verify_self_todo.py new file mode 100644 index 00000000..62700ca8 --- /dev/null +++ b/scripts/verify_self_todo.py @@ -0,0 +1,32 @@ +"""Verify self_todo_executor works with real DB""" +import subprocess +script = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py' + +# Test 1: DB_PATH +content = open(script).read() +if 'web-dashboard/data/mofin.db' in content: + print("DB_PATH: OK") +else: + print("DB_PATH: WRONG") + exit(1) + +# Test 2: script can import and run +try: + import importlib.util + spec = importlib.util.spec_from_file_location("executor", script) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + print("Import: OK") +except Exception as e: + print(f"Import: FAIL -> {e}") + exit(1) + +# Test 3: get_pending works +try: + rows = mod.get_pending() + print(f"get_pending: OK ({len(rows)} pending)") +except Exception as e: + print(f"get_pending: FAIL -> {e}") + exit(1) + +print("\nAll checks passed.") diff --git a/server.py b/server.py index 9e17d844..30ef1d5d 100644 --- a/server.py +++ b/server.py @@ -14,6 +14,59 @@ sys.path.insert(0, "/home/hmo/MoFin/scripts") sys.path.insert(0, "/home/hmo/MoFin") from flask import Flask, jsonify, send_from_directory, request +import socket +import time +import sqlite3 + +SPECS_DIR = Path(__file__).parent / "specs" +GATEWAY_TEMP = Path(__file__).parent / "gateway" / "temp" +START_TIME = time.time() + +# ── Dashboard 监控服务列表 ── +DASH_SERVICES = [ + {"name": "mofin_api", "label": "MoFin API", "port": 8899, "host": "127.0.0.1", "type": "http", "check": "/api/portfolio", "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}, +] + + +def _chk_tcp(host, port, timeout=3): + try: + s = socket.create_connection((host, port), timeout=timeout) + s.close() + return True + except Exception: + return False + + +def _chk_http(host, port, path, timeout=3): + try: + url = f"http://{host}:{port}{path}" + urllib.request.urlopen(urllib.request.Request(url), timeout=timeout) + return True + except Exception: + return False + + +def _chk_db(db_path): + try: + conn = sqlite3.connect(db_path) + conn.execute("SELECT 1") + conn.close() + return True + except Exception: + return False + + +def _check_svc(svc): + if svc["type"] == "tcp": + return _chk_tcp(svc["host"], svc["port"]) + elif svc["type"] == "http": + return _chk_http(svc["host"], svc["port"], svc["check"]) + elif svc["type"] == "db": + return _chk_db(svc["check"]) + return False # 提示词管理模块 from prompt_manager.dashboard_views import register_routes @@ -1133,6 +1186,92 @@ def update_realtime(): }) +# ── Dashboard 管理门户 ────────────────────────────── + +@app.route("/dashboard") +def dashboard_page(): + return send_from_directory(str(Path(__file__).parent / "templates"), "dashboard.html") + + +@app.route("/api/health") +def api_health(): + return jsonify({"status": "ok", "uptime": int(time.time() - START_TIME)}) + + +@app.route("/api/services") +def api_services(): + result = [] + for svc in DASH_SERVICES: + ok = _check_svc(svc) + result.append({ + "name": svc["name"], "label": svc["label"], + "port": svc["port"], "type": svc["type"], "layer": svc["layer"], + "critical": svc["critical"], "health": {"ok": ok}, + }) + 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(): + expected = [{ + "name": s["name"], "label": s["label"], "port": s["port"], + "expected": "running", "critical": s["critical"], "layer": s["layer"], + "check": f"{s['type']}:{s['port']}" if s["port"] else s["type"], + } for s in DASH_SERVICES] + actual = {} + for svc in DASH_SERVICES: + actual[svc["name"]] = "running" if _check_svc(svc) else "stopped" + return jsonify({"expected": expected, "actual": actual}) + + +@app.route("/api/monitor") +def api_monitor(): + tasks = [] + tier1 = {"summary": {"ok": 0, "total": 0}, "services": []} + tier2 = {"summary": {"ok": 0, "total": 0}, "services": []} + + t1_path = GATEWAY_TEMP / "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"}) + + t2_path = GATEWAY_TEMP / "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"}) + + tasks.append({"name": "dashboard", "status": "running"}) + return jsonify({ + "tasks": tasks, "tier1": tier1, "tier2": tier2, + "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + }) + + +@app.route("/api/module-spec/") +def api_module_spec(module): + spec_path = SPECS_DIR / f"{module.replace('..', '').replace('/', '').replace(chr(92), '')}.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": str(e)}), 500 + return jsonify({"error": f"Module '{module}' not found"}), 404 + + # 注册提示词管理路由 register_routes(app) diff --git a/specs/dashboard.json b/specs/dashboard.json index 9ec8543d..2a91e48e 100644 --- a/specs/dashboard.json +++ b/specs/dashboard.json @@ -1,17 +1,17 @@ { "module": "dashboard", "version": "1.0", - "purpose": "MoFin 管理门户。独立 Flask 应用(端口 5807),统一展示系统健康状态、模块 spec 帮助和监控数据。", + "purpose": "MoFin 管理门户。独立 Flask 应用(端口 8899),统一展示系统健康状态、模块 spec 帮助和监控数据。", "human_help": { "title": "Dashboard — 管理门户", "description": [ "MoFin 统一管理面板,提供系统健康状态总览和各模块的帮助文档。", "采用深色主题 Web UI,与 AgentsMeeting Dashboard 一致的视觉风格。", - "访问地址: http://192.168.1.246:5807" + "访问地址: http://192.168.1.246:8899" ], "usage": [ - "打开浏览器访问 http://192.168.1.246:5807", + "打开浏览器访问 http://192.168.1.246:8899", "F 健康 Tab — 查看所有服务运行状态和健康管线数据", "G 规范 Tab — 查看开发规范文档", "点击 ? 按钮 — 查看面向人类的模块帮助", @@ -39,7 +39,7 @@ "server.py :8899 — 业务 API(Dashboard 通过 HTTP 检测其可达性)" ], "constraints": [ - "Dashboard 部署在 Linux 246 上,端口 5807", + "Dashboard 部署在 Linux 246 上,端口 8899", "通过 systemd 守护(mofin-dashboard.service)", "不依赖 server.py :8899(可独立运行)", "前端 5 秒轮询 /api/services + /api/expected",