diff --git a/agents_health_check.py b/agents_health_check.py new file mode 100644 index 00000000..542eb4ce --- /dev/null +++ b/agents_health_check.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +agents_health_check.py — MoFin Tier1 快速健康检查 +==================================================== +每 5 分钟运行一次(crontab)。检查关键服务的端口/HTTP/DB 可用性。 +全正常时静默(不输出)。异常时写入 TODO 文件和 JSON 报告。 + +部署: crontab */5 * * * * cd /home/hmo/MoFin && python3 agents_health_check.py +""" +import json, os, sys, socket, sqlite3, urllib.request +from datetime import datetime +from pathlib import Path + +# ---- Config ---- +SCRIPT_DIR = Path(__file__).resolve().parent +TEMP_DIR = SCRIPT_DIR / "gateway" / "temp" +LOGS_DIR = SCRIPT_DIR / "gateway" / "logs" + +# Ensure dirs +TEMP_DIR.mkdir(parents=True, exist_ok=True) +LOGS_DIR.mkdir(parents=True, exist_ok=True) + +REPORT_FILE = TEMP_DIR / "last_health_check.json" +TODO_FILE = TEMP_DIR / "health_todos.jsonl" +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": 5804, "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"}, +] + + +# ---- Checkers ---- + +def check_tcp(host, port, timeout=3): + try: + sock = socket.create_connection((host, port), timeout=timeout) + sock.close() + return True, "ok" + except Exception as e: + return False, str(e) + + +def check_http(host, port, path, timeout=3): + try: + url = f"http://{host}:{port}{path}" + req = urllib.request.Request(url) + resp = urllib.request.urlopen(req, timeout=timeout) + return 200 <= resp.status < 300, f"HTTP {resp.status}" + except Exception as e: + return False, str(e) + + +def check_db(db_path): + try: + conn = sqlite3.connect(db_path) + conn.execute("SELECT 1") + conn.close() + return True, "ok" + except Exception as e: + return False, str(e) + + +# ---- Main ---- + +def run(): + now = datetime.now() + results = [] + issues = [] + + for svc in SERVICES: + if svc["type"] == "tcp": + ok, detail = check_tcp(svc["host"], svc["port"]) + elif svc["type"] == "http": + ok, detail = check_http(svc["host"], svc["port"], svc["check"]) + elif svc["type"] == "db": + ok, detail = check_db(svc["check"]) + else: + ok, detail = False, "unknown type" + + results.append({ + "name": svc["name"], + "label": svc["label"], + "type": svc["type"], + "port": svc["port"], + "health": {"ok": ok}, + "detail": detail, + }) + + if not ok: + issues.append(svc) + + # Write report + report = { + "services": results, + "summary": { + "ok": sum(1 for r in results if r["health"]["ok"]), + "total": len(results), + }, + "generated_at": now.strftime("%Y-%m-%d %H:%M:%S"), + } + with open(REPORT_FILE, "w", encoding="utf-8") as f: + json.dump(report, f, ensure_ascii=False, indent=2) + + # Handle issues + if issues: + # Write TODO entries + with open(TODO_FILE, "a", encoding="utf-8") as f: + for svc in issues: + entry = { + "service": svc["name"], + "label": svc["label"], + "reason": next((r["detail"] for r in results if r["name"] == svc["name"]), "unknown"), + "timestamp": now.isoformat(), + } + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + + # Log to file + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] ISSUES: {len(issues)} failed\n") + for svc in issues: + detail = next((r["detail"] for r in results if r["name"] == svc["name"]), "") + f.write(f" - {svc['label']}: {detail}\n") + + # Print to stdout (visible in cron log) + print(f"[{now.strftime('%H:%M')}] Health check: {len(issues)}/{len(SERVICES)} services failed") + for svc in issues: + detail = next((r["detail"] for r in results if r["name"] == svc["name"]), "") + print(f" FAIL: {svc['label']} ({svc['name']}) — {detail}") + else: + # All OK → silent + pass + + +if __name__ == "__main__": + run() diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 00000000..b097b312 --- /dev/null +++ b/dashboard.py @@ -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", 5804)) +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": 5804, + "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 new file mode 100644 index 00000000..5c1a4a3c --- /dev/null +++ b/docs/DASHBOARD.md @@ -0,0 +1,105 @@ +# MoFin — Dashboard API 参考 + +> 版本: v1.0 | 端口: 5804 | 入口: http://192.168.1.246:5804 + +--- + +## Tab 结构 + +``` +MoFin Dashboard +├── Services — 服务状态总览(MoFin API / Dashboard / 知微 Gateway / ejabberd / DB) +└── 开发原则 + ├── G 规范 — 开发规范 + Spec 文档 + └── F 健康 — 系统健康监控(Tier1/Tier2) +``` + +--- + +## API 端点清单 + +### 服务监控 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/services` | 所有注册服务状态(含健康检查结果) | +| GET | `/api/expected` | 期望状态矩阵(含实际状态对比) | +| GET | `/api/monitor` | 聚合监控数据(tasks + Tier1 + Tier2) | +| GET | `/api/health` | Dashboard 自身健康检查 | + +### 知识管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/module-spec/` | 读取 `specs/{module}.json`(?§ 按钮后端) | + +### 响应格式 + +**GET /api/services**: +```json +{ + "services": [ + { + "name": "mofin_api", + "label": "MoFin API", + "port": 8899, + "type": "http", + "layer": "核心服务", + "critical": true, + "health": {"ok": true}, + "detail": "HTTP 200" + } + ], + "summary": {"ok": 5, "total": 5} +} +``` + +**GET /api/monitor**: +```json +{ + "tasks": [ + {"name": "agents-health-check", "status": "cron_ok"}, + {"name": "agents-daily-health", "status": "not_deployed"}, + {"name": "dashboard", "status": "running", "detail": "services: 5/5"} + ], + "tier1": {"summary": {"ok": 5, "total": 5}, "services": [...]}, + "tier2": {"summary": {"ok": 0, "total": 0}, "services": []}, + "generated_at": "2026-07-19 10:00:00" +} +``` + +--- + +## 前端架构 + +- 纯 HTML/CSS/JS(无框架) +- 深色主题(GitHub Dark 风格) +- 5 秒自动轮询(Services Tab) +- ?§ Spec 系统(human_help + ai_spec) + +### Spec 系统(?§ 按钮) + +每个有 spec 的模块在 UI 上显示两个按钮: +- `?` → 读取 `human_help` → 人类可读的帮助文档 +- `§` → 读取 `ai_spec` → AI 可用的接口/约束/依赖 + +**Spec 文件位置**: `specs/{module}.json` +**API 端点**: `GET /api/module-spec/{module}` + +--- + +## 数据流 + +``` +crontab (每 5 分钟) + └── agents_health_check.py → last_health_check.json + +Dashboard (:5804) + ├── /api/services ← 实时 TCP/HTTP 检测 + ├── /api/monitor ← 聚合读取 health check JSON + └── /api/module-spec/ ← 读取 specs/ + +前端 (dashboard.html) + ├── 5s 轮询 /api/services + └── 按需请求 /api/monitor(F Tab 打开时) +``` diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 00000000..f7c7963a --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,99 @@ +# MoFin — 部署指南 + +> 版本: v1.0 | 部署目标: Linux 192.168.1.246 + +--- + +## 部署概览 + +| 组件 | 守护方式 | 端口 | 说明 | +|------|---------|------|------| +| **server.py** | systemd `mofin-api` | 8899 | 持仓情报 API(已有,不动) | +| **dashboard.py** | systemd `mofin-dashboard` | 5804 | 管理门户(新增) | +| **health_check** | crontab `*/5 * * * *` | — | Tier1 健康检查(新增) | + +--- + +## 1. Dashboard 部署 + +### 1.1 创建 systemd 服务 + +```bash +sudo tee /etc/systemd/system/mofin-dashboard.service << 'EOF' +[Unit] +Description=MoFin Dashboard +After=network.target + +[Service] +Type=simple +User=hmo +WorkingDirectory=/home/hmo/MoFin +Environment=MOFIN_ROOT=/home/hmo/MoFin +ExecStart=/usr/bin/python3 /home/hmo/MoFin/dashboard.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF +``` + +### 1.2 启动 + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now mofin-dashboard +sudo systemctl status mofin-dashboard +``` + +### 1.3 验证 + +```bash +curl http://127.0.0.1:5804/api/health +curl http://127.0.0.1:5804/api/services | python3 -m json.tool +# 浏览器访问: http://192.168.1.246:5804 +``` + +--- + +## 2. 健康管线部署 + +```bash +# 添加到 crontab +(crontab -l 2>/dev/null; echo '# MoFin health pipeline'; echo '*/5 * * * * cd /home/hmo/MoFin && /usr/bin/python3 agents_health_check.py >> gateway/logs/health_check_cron.log 2>&1') | crontab - + +# 验证 +crontab -l | grep health +``` + +--- + +## 3. 防火墙 + +```bash +sudo ufw status | grep -E '8899|5804' +# 如果未开放: +# sudo ufw allow 5804/tcp +``` + +--- + +## 4. 部署后验证清单 + +- [ ] `curl http://127.0.0.1:5804/api/health` → `{"status":"ok"}` +- [ ] `curl http://127.0.0.1:5804/api/services` → 返回 5 个服务状态 +- [ ] 浏览器打开 `http://192.168.1.246:5804` → Services Tab 显示服务状态 +- [ ] Dashboard F 健康 Tab → 定时任务状态显示正常 +- [ ] 点击各模块 ?§ 按钮 → 弹出 spec 帮助内容 +- [ ] `python3 agents_health_check.py` → 无输出(全正常) + +--- + +## 5. 故障恢复 + +| 问题 | 命令 | +|------|------| +| Dashboard 挂了 | `ssh hmo@246 'sudo systemctl restart mofin-dashboard'` | +| 健康检查不运行 | `ssh hmo@246 'crontab -l \| grep health'` | +| MoFin API 挂了 | `ssh hmo@246 'sudo systemctl restart mofin-api'` | +| 知微 Gateway 挂了 | `ssh hmo@246 'sudo systemctl restart hermes-gateway@zhiwei'` | diff --git a/docs/HEALTH-PIPELINE.md b/docs/HEALTH-PIPELINE.md new file mode 100644 index 00000000..67cd334c --- /dev/null +++ b/docs/HEALTH-PIPELINE.md @@ -0,0 +1,111 @@ +# MoFin — 健康监控管线 + +> 版本: v1.0 | 部署目标: Linux 246 + +--- + +## 概述 + +两层监控,通过 crontab 调度,聚合到 Dashboard F Tab。 + +``` + ┌─────────────────────────────────┐ + │ Dashboard F Tab │ + │ /api/monitor 聚合展示 │ + └──────────┬──────────────────────┘ + │ 读取报告文件 + ┌──────────┴──────────┐ + │ │ + ┌────▼─────┐ ┌────▼─────┐ + │ Tier 1 │ │ Tier 2 │ + │ 每 5 分钟 │ │ 每天 8:00│ + └──────────┘ └──────────┘ + │ │ + agents_health_check agents_daily_health + │ (规划中) + ┌────▼─────┐ + │ TODO 文件 │ + │ .jsonl │ + └──────────┘ +``` + +--- + +## Tier 1: 快速健康检查(每 5 分钟) + +**脚本**: `agents_health_check.py` +**调度**: `crontab: */5 * * * *` + +**检查内容**: +- 5 个服务:MoFin API (:8899) / Dashboard (:5804) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB +- 检查方式:socket 端口 + HTTP /health + SQLite connect +- 全正常时静默(不输出、不写日志) + +**异常处理**: +- 写入 `gateway/temp/health_todos.jsonl` +- 每条 TODO 包含:服务名、失败原因、时间戳 +- 写入 `gateway/temp/last_health_check.json` 供 Dashboard 读取 + +**日志**: `gateway/logs/health_check.log` +**报告**: `gateway/temp/last_health_check.json` + +--- + +## Tier 2: 每日全面检查(规划中) + +**计划脚本**: `agents_daily_health.py` +**计划调度**: `crontab: 0 8 * * * 1-5`(交易日 8:00) + +**计划检查内容**: +- 在 Tier 1 基础上增加: + - 磁盘空间检查(阈值 10G 警告 / 2G 严重) + - crontab 存活检查(验证关键定时任务) + - MoFin DB 大小和新鲜度检查 + - 生成结构化 JSON 报告 + +**注意**: MoFin 已有 `system_health_check.py`(每日 9:00)和 `morning_health_check.py`(交易日 8:00,8层48项),Tier2 将与现有检查互补,不重复。 + +--- + +## Dashboard 集成 + +### /api/monitor 端点 + +聚合展示两层数据: + +```json +{ + "tasks": [ + {"name": "agents-health-check", "status": "cron_ok"}, + {"name": "agents-daily-health", "status": "not_deployed"}, + {"name": "dashboard", "status": "running"} + ], + "tier1": { "services": [...], "summary": {"ok": 5, "total": 5} }, + "tier2": { "services": [...], "summary": {"ok": 0, "total": 0} } +} +``` + +### F Tab 展示 + +- 系统概览(Tier1 通过率) +- 定时任务状态(绿色=正常,黄色=未部署,红色=异常) +- Tier1 服务详情 + +--- + +## 如何新增监控 + +1. **添加服务到 Tier 1** — 编辑 `agents_health_check.py` 的 `SERVICES` 列表 +2. **更新 Dashboard** — 在 `dashboard.py` 的 `SERVICES` 中同步添加 +3. **写 Spec** — 在 `specs/` 创建或更新对应模块的 JSON + +--- + +## 故障排查 + +| 现象 | 检查 | +|------|------| +| F Tab 无数据 | `cat ~/MoFin/gateway/temp/last_health_check.json` 确认文件存在 | +| Tier1 任务显示"未部署" | `crontab -l \| grep health` 确认 crontab 条目 | +| TODO 堆积 | 手动检查失败服务的实际状态 | +| Dashboard 不显示新服务 | 确认 dashboard.py 的 SERVICES 列表和 health_check 同步 | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 00000000..b28d3be3 --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,86 @@ +# MoFin — 快速操作手册 + +> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 5804 + +--- + +## 日常检查 + +```bash +# 打开 Dashboard 看全局 +http://192.168.1.246:5804 + +# 命令行快速状态 +ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5804/api/services | python3 -m json.tool | head -20" +``` + +--- + +## Dashboard + +```bash +# 查看状态 +ssh hmo@192.168.1.246 "sudo systemctl status mofin-dashboard" + +# 重启 +ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard" + +# 查看日志 +ssh hmo@192.168.1.246 "tail -50 ~/MoFin/gateway/logs/dashboard.log" +``` + +--- + +## MoFin API + +```bash +# 查看状态 +ssh hmo@192.168.1.246 "sudo systemctl status mofin-api" + +# 重启 +ssh hmo@192.168.1.246 "sudo systemctl restart mofin-api" + +# 测试 API +curl http://192.168.1.246:8899/api/portfolio +``` + +--- + +## 健康检查 + +```bash +# 查看定时任务 +ssh hmo@192.168.1.246 "crontab -l | grep health" + +# 手动运行(无输出 = 全正常) +ssh hmo@192.168.1.246 "cd ~/MoFin && python3 agents_health_check.py" + +# 查看最近报告 +ssh hmo@192.168.1.246 "cat ~/MoFin/gateway/temp/last_health_check.json | python3 -m json.tool" +``` + +--- + +## 部署更新 + +```bash +# 1. 拉代码 +ssh hmo@192.168.1.246 "cd ~/MoFin && git pull --rebase" + +# 2. 重启受影响的服务 +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:5804/api/health" +``` + +--- + +## 常见问题 + +| 现象 | 操作 | +|------|------| +| Dashboard 不响应 | `ssh hmo@246 sudo systemctl restart mofin-dashboard` | +| F Tab 定时任务显示"未部署" | `ssh hmo@246 crontab -l \| grep health` 确认 | +| MoFin API 不响应 | `ssh hmo@246 sudo systemctl restart mofin-api` | +| 数据库查询失败 | `ssh hmo@246 'ls -la /home/hmo/web-dashboard/data/mofin.db'` | diff --git a/docs/decisions/2026-07-19-spec-and-dashboard.md b/docs/decisions/2026-07-19-spec-and-dashboard.md new file mode 100644 index 00000000..98bafd23 --- /dev/null +++ b/docs/decisions/2026-07-19-spec-and-dashboard.md @@ -0,0 +1,26 @@ +# 决策: 引入 spec 体系 + Dashboard + +## Context +MoFin 项目已运行数月,有 30 个 API 端点、38 个 cron 任务、完善的编码规范(DEVELOPMENT_STANDARDS.md)和架构文档(SYSTEM_ARCHITECTURE.md)。但缺少: +- 统一的模块可见性("不可见即不存在") +- AI 和人类共享的接口文档(spec 过期即等于没写) +- 系统健康状态的一站式监控面板 + +## Decision +参照 AgentsMeeting 样板,为 MoFin 引入: +1. **spec 双轨体系** — 每个模块的 `specs/{module}.json`(human_help + ai_spec) +2. **Dashboard** — 独立 `dashboard.py`(端口 5804),深色主题 Web UI + ?§ 按钮 +3. **健康管线** — Tier1(5min)+ Tier2(日检),聚合到 Dashboard F Tab +4. **开发规范** — `docs/dev-spec.md`(五条红线) + +不改动任何现有业务代码(server.py :8899 保持不变)。 + +## Consequences +- 新增 Dashboard 维护负担(但代码最小化,复用 AgentsMeeting 模板) +- AI 开发前必须先读 spec,短期可能感觉慢,长期减少架构理解错误 +- 健康检查需要纳入 crontab,增加系统负载(但轻量级,可忽略) + +## Alternatives Considered +- **方案 A**: 在现有 server.py 中嵌入 Dashboard(被否 — 改动运行中业务代码风险大) +- **方案 B**: 不做 Dashboard,只补文档(被否 — "不可见即不存在",没有面板等于没做) +- **方案 C**: 独立 dashboard.py(✅ 选择 — 零风险,不影响现有服务) diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 00000000..e29ec511 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,29 @@ +# 架构决策日志 + +每次架构决策(引入新组件、增加抽象层、变更接口)记录在此目录下。 + +## 格式 + +文件名: `YYYY-MM-DD-简短描述.md` + +```markdown +# 决策: [标题] + +## Context +为什么需要做这个决策?当前状态是什么? + +## Decision +做了什么选择? + +## Consequences +这个选择的影响和后果是什么? + +## Alternatives Considered +考虑了哪些替代方案?为什么没选? +``` + +## 已有决策 + +| 日期 | 决策 | 文件 | +|------|------|------| +| 2026-07-19 | 引入 spec 体系 + Dashboard(参照 AgentsMeeting 样板) | `2026-07-19-spec-and-dashboard.md` | diff --git a/docs/dev-spec.md b/docs/dev-spec.md new file mode 100644 index 00000000..cb22b9fd --- /dev/null +++ b/docs/dev-spec.md @@ -0,0 +1,214 @@ +# MoFin 开发规范 + +> 版本: v1.0 | 更新: 2026-07-19 | 基于 AgentsMeeting 样板重构 +> +> 📋 样板参考: [AgentsMeeting TEMPLATE-GUIDE.md](../AgentsMeeting/docs/TEMPLATE-GUIDE.md) + +--- + +## 五条红线 + +1. **先读/写 Spec,再写代码** — 新增功能先写 spec 再实现;修改已有功能先读对应 spec 了解架构和约束再动手。没有 spec 的模块在 Dashboard 不可见,视为未完成 +2. **部署必验** — 部署后不打开 Dashboard F Tab 验证 = 部署未完成 +3. **不可见即不存在** — 组件不在 Dashboard 中显示 = 等于没部署。离线不告警 = 监控缺陷 +4. **实现后同步 Spec** — 每轮开发完毕后,必须将 `specs/{module}.json` 更新为与实际实现一致的状态。文档过期 = 等于没写 +5. **部署目标即验收标准** — 所有代码必须以部署目标环境(Linux 246)为基准编写和测试。禁止使用 Windows 专属 API(`tasklist`、`netstat`、`schtasks`、`wmic`)在 246 部署的代码中 + +--- + +## 一、双轨同源规范体系 + +每新增/修改一个独立功能模块,必须先写 `specs/{module}.json`。 +一个来源同时产出两套文档: + +``` +specs/{module}.json +├── human_help → ? 按钮(人类看说明/排错) +└── ai_spec → § 按钮(AI 看接口/约束/依赖) +``` + +### 什么算一个模块 + +满足以下任一条件即视为独立模块,必须写 spec: + +- 暴露独立的 HTTP API 端点 +- 在 Dashboard 上有独立 UI 面板(`?` + `§` 按钮) +- 有独立的配置文件 / 数据文件 +- 可独立部署(如定时任务、数据采集脚本) + +### Spec 字段标准 + +```json +{ + "module": "模块名(与 Dashboard 引用名一致)", + "version": "1.0", + "purpose": "一句话说明这个模块干什么", + + "human_help": { + "title": "面向人类的标题", + "description": ["说明段落数组"], + "usage": ["使用步骤数组"], + "troubleshooting": ["常见问题数组"] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/xxx", "returns": "返回值说明"} + ], + "dependencies": ["依赖的服务或文件"], + "constraints": ["AI 必须遵守的约束"], + "must_not": ["AI 绝对不能做的事"], + "tests": [{"id": "T1", "name": "测试用例名"}], + "related_files": ["实现文件路径"] + } +} +``` + +### 当前模块清单 + +| 模块 | spec 路径 | 说明 | 状态 | +|------|----------|------|------| +| portfolio | `specs/portfolio.json` | 持仓数据 + 总览 | ✅ | +| watchlist | `specs/watchlist.json` | 自选股管理 | ✅ | +| decisions | `specs/decisions.json` | 策略决策库 | ✅ | +| market | `specs/market.json` | 市场观察数据 | ✅ | +| signals | `specs/signals.json` | 信号 + 小果扫描 | ✅ | +| evaluation | `specs/evaluation.json` | 策略评估 | ✅ | +| dashboard | `specs/dashboard.json` | Dashboard 自身 | ✅ | +| health | `specs/health.json` | 健康监控管线 | ✅ | +| price_monitor | `specs/price_monitor.json` | 价格监控 cron | 📋 | +| strategy_lifecycle | `specs/strategy_lifecycle.json` | 策略生命周期 | 📋 | + +> 状态: ✅ = spec 已完成 | 📋 = 待编写 + +--- + +## 二、验证闭环 + +``` +┌────────────┐ ┌──────────┐ ┌──────────┐ +│ G: 规范体系 │────→│ K: 测试 │────→│ F: 健康 │ +│ 定义期望 │ │ 验证实现 │ │ 持续监控 │ +└────────────┘ └──────────┘ └──────────┘ + ↑ ↑ │ + └────────────────┼────────────────┘ + │ + ┌────────┴────────┐ + │ F 异常 → 触发 K │ + │ K 失败 → 更新 G │ + └─────────────────┘ +``` + +### 核心反馈链路 + +| 方向 | 触发条件 | 动作 | +|------|---------|------| +| G → K | 新增/修改 spec | 对应测试 ID 必须新增/更新 | +| K → F | 测试全部通过 | F Tab 组件标记为已验证 | +| **F → K** | **F Tab 发现异常** | 触发对应测试重跑,确认是服务故障还是测试过期 | +| **F → G** | **F Tab 持续异常但测试通过** | 期望矩阵或 spec 过时,应更新 G 和对应 spec | + +### F — 系统健康度(Dashboard F Tab) + +- **期望矩阵**:应该运行的服务 vs 实际状态 +- **监控数据**:Tier1(5min)/ Tier2(日报)作为实时状态输入 +- **服务拓扑**:所有服务的健康、端口状态 +- **?§ 覆盖**:F Tab 中的每条服务必须有对应的 ?(human_help)和 §(ai_spec)按钮 + +--- + +## 三、开发流程 + +### 新增功能流程 + +``` +确定模块边界 + │ + ├─ 1. 创建 specs/{module}.json + │ human_help + ai_spec + │ + ├─ 2. 实现功能代码 + │ 包含 /health 端点 + PID 锁(proc_guard) + │ + ├─ 3. 注册到系统 + │ - 端口注册 + │ - 添加到期望矩阵(F Tab 自动检测) + │ + ├─ 4. 编写测试 + │ - ai_spec.tests 添加对应测试标识 + │ + ├─ 5. 同步更新 Spec + │ - 将 specs/{module}.json 更新为与实际实现一致 + │ + └─ 6. 提交 → 部署 → 验证 +``` + +### 修改已有功能流程 + +``` +识别要修改的模块(查看模块清单确定 module 名) + │ + ├─ 1. 读 specs/{module}.json + │ 重点读 ai_spec:apis / constraints / dependencies / must_not + │ + ├─ 2. 确认理解 + │ - 如果 spec 描述与代码实际行为不一致,优先怀疑 spec 过期 + │ + ├─ 3. 修改功能代码 + │ 只改动需求直接涉及的部分,不顺手优化无关代码 + │ + ├─ 4. 同步更新 Spec + │ + └─ 5. 提交 → 部署 → 验证 +``` + +### Git 操作规范 + +| # | 规则 | 说明 | +|---|------|------| +| 1 | 开工前必 pull | `git pull --rebase` | +| 2 | 改完即 commit | 一个逻辑单元一次提交。禁止含密钥 | +| 3 | 推前必拉 + 配代理 | push 前 `git pull --rebase`。远程操作前配 `:15000` 代理 | +| 4 | trunk-based | 日常在 main。仅长周期大改开分支 | + +### 已有编码规范 + +MoFin 已有的编码规范见 `docs/DEVELOPMENT_STANDARDS.md`,包含: +- 代码结构(mo_models → mo_data → mofin_db 三层) +- 数据规范(币种、汇率、数据源) +- DB 规范(表设计、迁移) +- LLM Prompt 规范 +- Cron 规范(独立运行、幂等性) +- 测试要求(`run_all_tests.py`) + +以上规范与本文件互补,不冲突。本文件侧重"先 spec 后代码"和"通过 Dashboard 保证可见性"。 + +--- + +## 四、部署环境 + +| 项目 | 值 | +|------|-----| +| **生产环境** | 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`(新增) | +| **Python** | 系统 Python 3 | + +--- + +## 五、文档索引 + +| 文档 | 用途 | +|------|------| +| `docs/dev-spec.md` | 本文件 — 开发规范(含五条红线) | +| `docs/DEVELOPMENT_STANDARDS.md` | 编码规范(已有) | +| `SYSTEM_ARCHITECTURE.md` | 系统架构(已有) | +| `docs/cron-catalog.md` | Cron 任务清单(已有) | +| `docs/DEPLOY.md` | 部署指南 | +| `docs/QUICKSTART.md` | 快速操作 | +| `docs/DASHBOARD.md` | Dashboard API 参考 | +| `docs/HEALTH-PIPELINE.md` | 健康管线文档 | +| `docs/learned.md` | 经验教训记录 | +| `docs/decisions/` | 架构决策日志 | diff --git a/docs/learned.md b/docs/learned.md new file mode 100644 index 00000000..0e00c081 --- /dev/null +++ b/docs/learned.md @@ -0,0 +1,12 @@ +# 经验教训记录 + +每次被纠正后追加一条记录。每次新任务前先扫一遍本文档。 + +## 格式 +- [YYYY-MM-DD] 问题: xxx | 根因: xxx | 正确做法: xxx + +--- + +## 记录 + +- [2026-07-19] 问题: MoFin 缺少 spec 体系和 Dashboard,功能模块不可见、不可监控 | 根因: 项目早期未引入"不可见即不存在"原则 | 正确做法: 参照 AgentsMeeting 样板重构,先建立 dev-spec.md + spec 体系 + Dashboard,再逐步迁移 diff --git a/specs/dashboard.json b/specs/dashboard.json new file mode 100644 index 00000000..b52d5e50 --- /dev/null +++ b/specs/dashboard.json @@ -0,0 +1,60 @@ +{ + "module": "dashboard", + "version": "1.0", + "purpose": "MoFin 管理门户。独立 Flask 应用(端口 5804),统一展示系统健康状态、模块 spec 帮助和监控数据。", + + "human_help": { + "title": "Dashboard — 管理门户", + "description": [ + "MoFin 统一管理面板,提供系统健康状态总览和各模块的帮助文档。", + "采用深色主题 Web UI,与 AgentsMeeting Dashboard 一致的视觉风格。", + "访问地址: http://192.168.1.246:5804" + ], + "usage": [ + "打开浏览器访问 http://192.168.1.246:5804", + "F 健康 Tab — 查看所有服务运行状态和健康管线数据", + "G 规范 Tab — 查看开发规范文档", + "点击 ? 按钮 — 查看面向人类的模块帮助", + "点击 § 按钮 — 查看面向 AI 的接口约束文档" + ], + "troubleshooting": [ + "Dashboard 不响应 → ssh 246 'sudo systemctl restart mofin-dashboard'", + "F Tab 无数据 → 检查 crontab 中健康检查任务是否运行", + "Spec 加载失败 → 检查 specs/ 目录权限和 JSON 格式" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/health", "returns": "{status:'ok', uptime:N}"}, + {"method": "GET", "path": "/api/services", "returns": "{services[{name, type, port, status}]} — 所有注册服务的运行状态"}, + {"method": "GET", "path": "/api/expected", "returns": "{expected[{name, port, expected, critical}], actual{name:status}} — 期望状态矩阵"}, + {"method": "GET", "path": "/api/monitor", "returns": "{tier1{summary{ok,total}}, tier2{summary{ok,total}}, tasks[{name,status}]} — 聚合监控数据"}, + {"method": "GET", "path": "/api/module-spec/", "returns": "specs/{module}.json 内容"} + ], + "dependencies": [ + "specs/ 目录 — 所有模块的 spec JSON 文件", + "agents_health_check.py — Tier1 健康检查(生成 last_health_check.json)", + "agents_daily_health.py — Tier2 日检(生成 last_daily_health.json)", + "server.py :8899 — 业务 API(Dashboard 通过 HTTP 检测其可达性)" + ], + "constraints": [ + "Dashboard 部署在 Linux 246 上,端口 5804", + "通过 systemd 守护(mofin-dashboard.service)", + "不依赖 server.py :8899(可独立运行)", + "前端 5 秒轮询 /api/services + /api/expected", + "?§ 按钮通过 /api/module-spec/ 读取 spec JSON" + ], + "must_not": [ + "不要修改 server.py :8899 来集成 Dashboard(保持独立)", + "不要在 Dashboard 中硬编码业务数据(只做监控和文档展示)", + "不要移除 ?§ 按钮系统(这是 spec 可视化的核心)" + ], + "related_files": [ + "dashboard.py — Flask 后端", + "templates/dashboard.html — 前端", + "specs/ — Spec 文件目录", + "gateway/logs/ — 运行时日志" + ] + } +} diff --git a/specs/decisions.json b/specs/decisions.json new file mode 100644 index 00000000..625bd197 --- /dev/null +++ b/specs/decisions.json @@ -0,0 +1,53 @@ +{ + "module": "decisions", + "version": "1.0", + "purpose": "策略决策库。管理持仓和自选股的策略决策(止损/止盈/买入区/操作建议),支持新旧格式兼容。", + + "human_help": { + "title": "策略决策库", + "description": [ + "存储每只持仓/自选股的策略决策数据,包括止损价、止盈价、买入区间、操作建议等。", + "数据来自知微 LLM 分析,通过 /api/analysis/batch 写入。", + "支持新旧两种数据格式的自动兼容(新格式:stop_loss/take_profit 顶层字段;旧格式:trigger 对象)。" + ], + "usage": [ + "GET /api/decisions — 获取全部决策(按标签+执行状态排序)", + "POST /api/decisions/add — 新增/更新一条决策(同股票旧决策自动标记 superseded)", + "POST /api/decisions/tag — 设置/清除推荐标签(current_recommend / active_manual)", + "GET /api/decisions/pending — 获取有未确认建议的条目" + ], + "troubleshooting": [ + "决策列表为空 → 确认 regenerate_all 已执行或知微已写入", + "旧格式不显示 → 检查归一化逻辑(/api/decisions GET handler)" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/decisions", "returns": "{decisions[{code, name, type, status, tag, action, trigger{stop_loss, take_profit, entry_zone}, current, zone_breach, updated_reason, advice_timeline, changelog, execution, analysis}], total, regenerated_at}"}, + {"method": "POST", "path": "/api/decisions/add", "returns": "{status:'ok', entry:{...}}"}, + {"method": "POST", "path": "/api/decisions/tag", "returns": "{status:'ok', code, tag}"}, + {"method": "GET", "path": "/api/decisions/pending", "returns": "[{code, name, current, pending_advice[{date, direction, price, summary, status}]}]"} + ], + "dependencies": [ + "mo_data.py — read_decisions()", + "mofin_db.py — write_holding_strategy()", + "strategy_lifecycle.py — regenerate_all 触发全量重评" + ], + "constraints": [ + "支持新旧两种格式的读取兼容(normalized 逻辑)", + "同股票新决策会自动将旧决策标记为 superseded", + "排序规则:current_recommend 标签 > 执行状态(partial_exit > executing > observing) > code", + "advice_timeline 去重:同日期+同方向+摘要前40字相同 → skip" + ], + "must_not": [ + "不要在决策中硬编码价格阈值(应由 LLM 分析生成)", + "不要删除 superseded 的旧决策(保留历史记录)" + ], + "related_files": [ + "server.py — /api/decisions* 路由", + "mo_data.py — read_decisions()", + "mofin_db.py — holding_strategies 表" + ] + } +} diff --git a/specs/evaluation.json b/specs/evaluation.json new file mode 100644 index 00000000..4e6042d1 --- /dev/null +++ b/specs/evaluation.json @@ -0,0 +1,48 @@ +{ + "module": "evaluation", + "version": "1.0", + "purpose": "策略评估系统。提供策略双维度评估结果查询、手动触发评估和准确率统计。", + + "human_help": { + "title": "策略评估", + "description": [ + "评估每只股票策略的有效性,来自 strategy_evaluator.py(每周六 21:00 自动运行)。", + "支持手动触发评估(POST /api/evaluation/trigger)。", + "评估数据主源为 evaluation.json,备选为 decisions.json 中的 evaluation 字段。" + ], + "usage": [ + "GET /api/evaluation — 获取全部策略评估结果", + "POST /api/evaluation/trigger — 手动触发策略评估(执行 strategy_evaluator.py)", + "GET /api/stats/accuracy — 获取准确率统计数据", + "GET /api/feedback — 获取策略反馈数据" + ], + "troubleshooting": [ + "评估数据为空 → 确认 strategy_evaluator.py 已运行过", + "触发失败 → 检查 strategy_evaluator.py 路径是否正确" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/evaluation", "returns": "[{code, name, type, current, evaluations[{date, dimension, score, reason}]}]"}, + {"method": "POST", "path": "/api/evaluation/trigger", "returns": "{status:'ok', output, error} — 执行 strategy_evaluator.py,timeout 60s"}, + {"method": "GET", "path": "/api/stats/accuracy", "returns": "accuracy_stats.json 内容"}, + {"method": "GET", "path": "/api/feedback", "returns": "strategy_feedback.json 内容"} + ], + "dependencies": [ + "strategy_evaluator.py — 双维度评估脚本(cron: 周六 21:00)", + "evaluation.json — 评估结果主数据源", + "accuracy_stats.json — 准确率统计", + "strategy_feedback.json — 反馈数据" + ], + "constraints": [ + "POST /api/evaluation/trigger 会阻塞最多 60 秒", + "评估数据优先读 evaluation.json,fallback 读 decisions.json 的 evaluation 字段" + ], + "related_files": [ + "server.py — /api/evaluation, /api/evaluation/trigger, /api/stats/accuracy, /api/feedback", + "strategy_evaluator.py", + "strategy_feedback.py" + ] + } +} diff --git a/specs/health.json b/specs/health.json new file mode 100644 index 00000000..34c7e63b --- /dev/null +++ b/specs/health.json @@ -0,0 +1,73 @@ +{ + "module": "health", + "version": "1.0", + "purpose": "MoFin 系统健康监控管线。三层监控(Tier1 快速检查 + Tier2 日检),聚合到 Dashboard F Tab 展示。", + + "human_help": { + "title": "F 健康 — 系统健康", + "description": [ + "实时监控 MoFin 所有关键服务(Flask API、数据库、cron 任务)的运行状态。", + "两层监控:", + " Tier1 — 每 5 分钟快速端口/进程检查", + " Tier2 — 每日 8:00 开盘前全面体检(进程+端口+DB+磁盘+cron)", + "检查结果聚合到 Dashboard F Tab,异常自动告警。" + ], + "usage": [ + "1. 打开 Dashboard → F 健康 Tab 查看概览", + "2. 绿色 = 正常,黄色 = 部分异常,红色 = 严重异常", + "3. 异常服务列表直接显示影响描述", + "4. 定时任务区域检查所有 cron 是否正常运行" + ], + "troubleshooting": [ + "F Tab 显示无数据 → 检查 crontab 中健康检查是否部署", + "服务显示 down 但实际在运行 → 检查端口或 health 端点是否正确", + "TODO 堆积 → 检查 self_todo_executor 是否在 crontab 中" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/services", "returns": "{services[{name, type, port, health{ok}, status}]}"}, + {"method": "GET", "path": "/api/expected", "returns": "{expected[{name, port, expected, critical}], actual{name:status}}"}, + {"method": "GET", "path": "/api/monitor", "returns": "{tier1{summary{ok,total}}, tier2{summary{ok,total}}, tasks[{name,status}]}"} + ], + "dependencies": [ + "agents_health_check.py — Tier1(每 5 分钟,socket 端口 + HTTP /health)", + "agents_daily_health.py — Tier2(每日 8:00,端口+进程+DB+磁盘+cron)", + "mofin.db — SQLite 数据库(检查可读写)" + ], + "architecture": { + "monitored_services": [ + {"name": "mofin_api", "port": 8899, "type": "http", "check": "GET /api/portfolio"}, + {"name": "mofin_dashboard", "port": 5804, "type": "http", "check": "GET /api/health"}, + {"name": "zhiwei_gateway", "port": 8643, "type": "http", "check": "GET /v1/health"}, + {"name": "ejabberd", "port": 5222, "type": "tcp", "check": "socket connect"}, + {"name": "mofin_db", "port": 0, "type": "file", "check": "sqlite3 connect + SELECT"} + ] + }, + "constraints": [ + "Tier1 全正常时静默(不输出日志)", + "Tier1 异常写入 gateway/temp/health_todos.jsonl", + "Tier1 报告写入 gateway/temp/last_health_check.json", + "Tier2 报告写入 gateway/temp/last_daily_health.json", + "Dashboard /api/monitor 聚合读取以上 JSON 文件" + ], + "must_not": [ + "不要在健康检查中修改业务数据", + "不要硬编码 Windows 路径或命令", + "不要检查已停用的服务(wechat_agent 等)" + ], + "tests": [ + {"id": "H01", "name": "/api/services 返回服务列表", "endpoint": "GET /api/services"}, + {"id": "H02", "name": "/api/expected 返回期望矩阵", "endpoint": "GET /api/expected"}, + {"id": "H03", "name": "/api/monitor 返回监控数据", "endpoint": "GET /api/monitor"} + ], + "related_files": [ + "agents_health_check.py — Tier1 健康检查", + "agents_daily_health.py — Tier2 日检", + "dashboard.py — /api/services, /api/expected, /api/monitor", + "templates/dashboard.html — F Tab 渲染", + "specs/health.json — 本 spec 文件" + ] + } +} diff --git a/specs/market.json b/specs/market.json new file mode 100644 index 00000000..96a32168 --- /dev/null +++ b/specs/market.json @@ -0,0 +1,42 @@ +{ + "module": "market", + "version": "1.0", + "purpose": "市场观察数据。提供大盘指数和板块数据的查询和更新。", + + "human_help": { + "title": "市场观察", + "description": [ + "展示大盘指数(上证、深证、恒生等)和板块热度数据。", + "数据由 market_watch.py cron(每 30 分钟)自动采集。", + "优先从 DB 读取(market_snapshots / sector_snapshots 表),DB 无数据时 fallback 到 market.json。" + ], + "usage": [ + "GET /api/market — 获取最新市场数据(指数 + 板块)", + "POST /api/update/market — 更新市场数据(由 market_watch 调用)" + ], + "troubleshooting": [ + "市场数据为空 → 检查 market_watch cron 是否正常运行", + "数据显示旧 → 手动运行 python3 market_watch.py 更新" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/market", "returns": "{indices[{name, code, price, change_pct}], sectors[{name, change_pct, leader}]}"}, + {"method": "POST", "path": "/api/update/market", "returns": "{status:'ok'}"} + ], + "dependencies": [ + "mofin_db.py — market_snapshots / sector_snapshots 表", + "market_watch.py — 大盘采集 cron(*/30 9-15)", + "market_screener.py — 全市场筛选 cron" + ], + "constraints": [ + "DB 优先读取,JSON 仅做 fallback" + ], + "related_files": [ + "server.py — /api/market, /api/update/market", + "market_watch.py — 大盘数据采集", + "market_screener.py — 全市场筛选" + ] + } +} diff --git a/specs/portfolio.json b/specs/portfolio.json new file mode 100644 index 00000000..450b8b69 --- /dev/null +++ b/specs/portfolio.json @@ -0,0 +1,60 @@ +{ + "module": "portfolio", + "version": "1.0", + "purpose": "持仓数据查询与管理。提供持仓列表、资产概览、实时价格更新。", + + "human_help": { + "title": "持仓管理", + "description": [ + "本模块管理老爸的股票持仓数据,包括个股持仓明细和总资产概览。", + "数据存储在 SQLite (mofin.db),由 price_monitor cron 每 2 分钟更新价格。", + "港股以 HKD 存储,汇总时自动转换为 CNY。" + ], + "usage": [ + "GET /api/portfolio — 获取完整持仓列表(含价格、涨跌幅、盈亏)", + "GET /api/overview — 获取总资产概览(总资产、股票市值、现金、仓位、top movers)", + "POST /api/update/portfolio — 批量更新持仓数据(由 cron 调用)", + "POST /api/update/realtime — 实时价格更新(由 price_monitor 调用)" + ], + "troubleshooting": [ + "数据库查询失败 → 检查 mofin.db 是否存在且可读写", + "港股价格异常 → 确认 hk_rate.py 汇率 API 可达", + "数据不更新 → 检查 crontab 中 price_monitor 是否正常运行" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/portfolio", "returns": "{total_assets, stock_value, cash, position_pct, total_pnl, holdings[{code, name, price, cost, shares, change_pct, currency, ...}]}"}, + {"method": "GET", "path": "/api/overview", "returns": "{total_assets, stock_value, cash, position_pct, total_pnl, top_movers, market, alerts, updated_at}"}, + {"method": "POST", "path": "/api/update/portfolio", "returns": "{status:'ok'}"}, + {"method": "POST", "path": "/api/update/realtime", "returns": "{status:'ok'}"} + ], + "dependencies": [ + "mo_data.py — read_portfolio() 统一读取层", + "mofin_db.py — get_conn(), query_holdings(), query_portfolio_summary()", + "price_monitor.py — 唯一价格写入者,cron: */2 9-16 1-5" + ], + "constraints": [ + "港股个股价格/成本以 HKD 存储,currency='HKD'", + "A股个股价格/成本以 CNY 存储,currency='CNY'", + "总资产/总市值以 CNY 汇总(calc_total_assets 自动转换)", + "禁止跨币种直接比较或加减", + "price_monitor 是唯一的价格写入源,其他脚本禁止直接写价格" + ], + "must_not": [ + "不要在各业务脚本中直接写 SQL(必须通过 mofin_db.py)", + "不要硬编码汇率(必须通过 hk_rate.py 的 get_hk_rate())", + "不要直接 json.load 读数据(必须通过 mo_data.py)", + "不要自己实现 calc_total_assets / is_hk_stock(必须用 mo_models.py)" + ], + "related_files": [ + "server.py — API 路由定义", + "mo_models.py — 数据模型(calc_total_assets, is_hk_stock, to_cny)", + "mo_data.py — 统一读取层", + "mofin_db.py — DB 层", + "price_monitor.py — 价格更新 cron", + "hk_rate.py — 港币汇率" + ] + } +} diff --git a/specs/signals.json b/specs/signals.json new file mode 100644 index 00000000..574d2efa --- /dev/null +++ b/specs/signals.json @@ -0,0 +1,44 @@ +{ + "module": "signals", + "version": "1.0", + "purpose": "信号与扫描数据。提供市场信号查询和小果扫描统计。", + + "human_help": { + "title": "信号与扫描", + "description": [ + "展示系统产生的交易信号和小果 LLM 扫描结果。", + "信号来自多个来源:xiaoguo_scanner(全市场)、macro_context_collector(宏观)、divergence_detector(背离)。", + "所有信号存储在 signal_news 表中。" + ], + "usage": [ + "GET /api/signals — 获取最近 20 条信号(含板块信号关联)", + "GET /api/xiaoguo-scan — 获取小果扫描统计(扫描总数/发现信号数/近期记录/今日来源分布)" + ], + "troubleshooting": [ + "信号为空 → 检查 xiaoguo_scanner 和 macro_context_collector cron 状态", + "小果扫描数据停更 → 检查小果 LLM API 是否可达" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/signals", "returns": "[{id, sector, overall_sentiment, summary, source, created_at, signal_type, severity}] — 最近 20 条信号"}, + {"method": "GET", "path": "/api/xiaoguo-scan", "returns": "{total_scanned, found_signals, recent[{code, name, last_scanned_at, found_count}], source_today{source:cnt}}"} + ], + "dependencies": [ + "mofin_db.py — signal_news / xiaoguo_scan_tracker 表", + "xiaoguo_scanner.py — 全市场扫描 cron(*/5 9-15)", + "macro_context_collector.py — 宏观信号采集 cron", + "divergence_detector.py — 背离检测 cron" + ], + "constraints": [ + "signal_news 和 sector_signals 通过 LEFT JOIN 关联", + "xiaoguo_scan_tracker 的 source 统计只取最近 24 小时" + ], + "related_files": [ + "server.py — /api/signals, /api/xiaoguo-scan", + "xiaoguo_scanner.py", + "macro_context_collector.py" + ] + } +} diff --git a/specs/watchlist.json b/specs/watchlist.json new file mode 100644 index 00000000..9c975101 --- /dev/null +++ b/specs/watchlist.json @@ -0,0 +1,42 @@ +{ + "module": "watchlist", + "version": "1.0", + "purpose": "自选股列表管理。提供自选股查询和批量更新。", + + "human_help": { + "title": "自选股管理", + "description": [ + "管理老爸的自选股列表,与持仓分开存储。", + "数据存储在 SQLite (mofin.db) 的 watchlist_stocks 表。", + "小果扫描器会消费自选股信号并自动添加到自选列表。" + ], + "usage": [ + "GET /api/watchlist — 获取完整自选股列表", + "POST /api/update/watchlist — 批量更新自选股(由 cron 调用)" + ], + "troubleshooting": [ + "数据库查询失败 → 检查 mofin.db 是否可读写", + "自选股列表为空 → 确认 regenerate_all 或手动添加过自选股" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/watchlist", "returns": "{stocks[{code, name, price, change_pct, currency, analysis{...}}]}"}, + {"method": "POST", "path": "/api/update/watchlist", "returns": "{status:'ok'}"} + ], + "dependencies": [ + "mo_data.py — read_watchlist()", + "mofin_db.py — query_watchlist(), write_watchlist_stock()", + "xiaoguo_signal_consumer.py — 消费小果信号,自动加自选" + ], + "constraints": [ + "自选股也必须标注 currency 字段(HKD/CNY)" + ], + "related_files": [ + "server.py — API 路由定义", + "mo_data.py — read_watchlist()", + "mofin_db.py — watchlist_stocks 表" + ] + } +} diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 00000000..de651ae1 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,277 @@ + +MoFin Dashboard + +

MoFin

持仓情报系统 · Dashboard
+
+
+
+