feat: introduce spec system + dashboard + health pipeline (AgentsMeeting template)
This commit is contained in:
@@ -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()
|
||||||
+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", 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/<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)
|
||||||
@@ -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/<module>` | 读取 `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/<module> ← 读取 specs/
|
||||||
|
|
||||||
|
前端 (dashboard.html)
|
||||||
|
├── 5s 轮询 /api/services
|
||||||
|
└── 按需请求 /api/monitor(F Tab 打开时)
|
||||||
|
```
|
||||||
@@ -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'` |
|
||||||
@@ -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 同步 |
|
||||||
@@ -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'` |
|
||||||
@@ -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(✅ 选择 — 零风险,不影响现有服务)
|
||||||
@@ -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` |
|
||||||
@@ -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/` | 架构决策日志 |
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 经验教训记录
|
||||||
|
|
||||||
|
每次被纠正后追加一条记录。每次新任务前先扫一遍本文档。
|
||||||
|
|
||||||
|
## 格式
|
||||||
|
- [YYYY-MM-DD] 问题: xxx | 根因: xxx | 正确做法: xxx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 记录
|
||||||
|
|
||||||
|
- [2026-07-19] 问题: MoFin 缺少 spec 体系和 Dashboard,功能模块不可见、不可监控 | 根因: 项目早期未引入"不可见即不存在"原则 | 正确做法: 参照 AgentsMeeting 样板重构,先建立 dev-spec.md + spec 体系 + Dashboard,再逐步迁移
|
||||||
@@ -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/<module>", "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/<module> 读取 spec JSON"
|
||||||
|
],
|
||||||
|
"must_not": [
|
||||||
|
"不要修改 server.py :8899 来集成 Dashboard(保持独立)",
|
||||||
|
"不要在 Dashboard 中硬编码业务数据(只做监控和文档展示)",
|
||||||
|
"不要移除 ?§ 按钮系统(这是 spec 可视化的核心)"
|
||||||
|
],
|
||||||
|
"related_files": [
|
||||||
|
"dashboard.py — Flask 后端",
|
||||||
|
"templates/dashboard.html — 前端",
|
||||||
|
"specs/ — Spec 文件目录",
|
||||||
|
"gateway/logs/ — 运行时日志"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 表"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 文件"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 — 全市场筛选"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 — 港币汇率"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 表"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>MoFin Dashboard</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d29922}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}body{background:var(--bg);color:var(--text);font:20px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:28px}
|
||||||
|
h1{font-size:32px;font-weight:600;color:var(--accent);margin-bottom:4px}.subtitle{color:var(--dim);font-size:18px;margin-bottom:20px}
|
||||||
|
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||||||
|
.tab-btn{padding:12px 24px;cursor:pointer;font-size:19px;color:var(--dim);border:none;background:none;border-bottom:2px solid transparent}
|
||||||
|
.tab-btn:hover{color:var(--text)}.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||||
|
.tab-pane{display:none}.tab-pane.active{display:block}
|
||||||
|
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}
|
||||||
|
.stat-c{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px}
|
||||||
|
.stat-c .n{font-size:42px;font-weight:700}.stat-c .l{font-size:18px;color:var(--dim)}
|
||||||
|
.stat-c.g .n{color:var(--green)}.stat-c.r .n{color:var(--red)}.stat-c.y .n{color:var(--yellow)}
|
||||||
|
.section{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:12px}
|
||||||
|
.section h2{font-size:19px;font-weight:600;color:var(--accent);display:flex;align-items:center}
|
||||||
|
.help-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;min-width:20px;border-radius:50%;border:1px solid var(--border);background:var(--card);color:var(--dim);cursor:pointer;font-size:14px;line-height:1;margin-left:8px;flex-shrink:0}
|
||||||
|
.help-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
|
.help-btn.ai{color:var(--yellow);border-color:var(--yellow);font-size:12px}
|
||||||
|
.help-btn.ai:hover{background:var(--yellow);color:#000}
|
||||||
|
.spec-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);z-index:1000;display:none;align-items:center;justify-content:center}
|
||||||
|
.spec-modal.show{display:flex}
|
||||||
|
.spec-modal .modal-box{background:var(--card);border:1px solid var(--border);border-radius:12px;max-width:700px;max-height:80vh;overflow-y:auto;padding:28px;margin:20px}
|
||||||
|
.spec-modal .modal-box h3{font-size:22px;color:var(--accent);margin-bottom:12px}
|
||||||
|
.spec-modal .modal-box .close{float:right;cursor:pointer;color:var(--dim);font-size:22px}
|
||||||
|
.spec-modal .modal-box .close:hover{color:var(--text)}
|
||||||
|
.spec-modal .modal-box h4{font-size:19px;color:var(--accent);margin:16px 0 6px}
|
||||||
|
.spec-modal .modal-box p{font-size:18px;color:var(--text);margin:4px 0}
|
||||||
|
.spec-modal .modal-box ul{margin:4px 0 4px 20px}
|
||||||
|
.spec-modal .modal-box li{font-size:18px;color:var(--text);margin:2px 0}
|
||||||
|
.spec-modal .modal-box pre{background:var(--bg);padding:12px;border-radius:6px;font-size:15px;overflow-x:auto;margin:8px 0}
|
||||||
|
.svc-row{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border)}
|
||||||
|
.svc-row:last-child{border-bottom:none}
|
||||||
|
.svc-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
||||||
|
.svc-dot.ok{background:var(--green)}.svc-dot.fail{background:var(--red)}
|
||||||
|
.svc-name{flex:1;min-width:0;font-size:18px}
|
||||||
|
.svc-status{font-size:16px;font-weight:600}
|
||||||
|
.svc-status.ok{color:var(--green)}.svc-status.fail{color:var(--red)}
|
||||||
|
.svc-meta{font-size:16px;color:var(--dim)}
|
||||||
|
.sub-bar{display:flex;gap:2px;margin-bottom:10px;flex-wrap:wrap}
|
||||||
|
.sub-btn{padding:4px 12px;font-size:18px;background:0 0;border:1px solid transparent;border-radius:12px;color:var(--dim);cursor:pointer}
|
||||||
|
.sub-btn:hover{color:var(--text);border-color:var(--border)}.sub-btn.active{color:var(--accent);border-color:var(--accent);background:var(--bg)}
|
||||||
|
.toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:19px;z-index:999;opacity:0;pointer-events:none;transition:opacity .3s}
|
||||||
|
.toast.show{opacity:1}.toast.ok{background:#238636;color:#fff}.toast.err{background:#da3633;color:#fff}
|
||||||
|
.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:16px;font-weight:600;margin-left:6px}
|
||||||
|
.tag.g{background:#3fb95020;color:var(--green)}.tag.r{background:#f8514920;color:var(--red)}
|
||||||
|
.tag.y{background:#d2992220;color:var(--yellow)}
|
||||||
|
.task-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
|
||||||
|
.task-row:last-child{border-bottom:none}
|
||||||
|
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:16px}
|
||||||
|
.section p,.section li{font-size:18px;line-height:1.8}.section code{font-size:18px}
|
||||||
|
</style></head><body>
|
||||||
|
<h1>MoFin</h1><div class="subtitle">持仓情报系统 · Dashboard</div>
|
||||||
|
<div class="tabs" id="tabs"></div><div id="panes"></div>
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
<div id="spec-modal" class="spec-modal"><div class="modal-box" id="spec-modal-box"></div></div>
|
||||||
|
<script>
|
||||||
|
var T=[
|
||||||
|
{id:"services",label:"Services"},
|
||||||
|
{id:"principles",label:"开发原则",children:[
|
||||||
|
{id:"spec",label:"G 规范"},
|
||||||
|
{id:"health",label:"F 健康"}
|
||||||
|
]}
|
||||||
|
];
|
||||||
|
var at=localStorage.getItem('mt')||'services',as=localStorage.getItem('ms')||'spec';
|
||||||
|
|
||||||
|
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||||||
|
function qs(id){return document.getElementById(id)}
|
||||||
|
function toast(m,t){var e=qs('toast');e.textContent=m;e.className='toast show '+(t||'ok');setTimeout(function(){e.className='toast'},2500)}
|
||||||
|
|
||||||
|
// ---- Spec System ----
|
||||||
|
var _specCache={};
|
||||||
|
function showModuleHelp(module,type){
|
||||||
|
var modal=qs('spec-modal');var box=qs('spec-modal-box');
|
||||||
|
if(_specCache[module]){_renderSpecModal(_specCache[module],type);modal.classList.add('show');return}
|
||||||
|
fetch('/api/module-spec/'+module).then(function(r){return r.json()}).then(function(spec){
|
||||||
|
_specCache[module]=spec;_renderSpecModal(spec,type);modal.classList.add('show');
|
||||||
|
}).catch(function(){
|
||||||
|
box.innerHTML='<span class="close" onclick="qs(\'spec-modal\').classList.remove(\'show\')">×</span><h3>Spec not found</h3><p>Module: '+esc(module)+'</p>';modal.classList.add('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _renderSpecModal(spec,type){
|
||||||
|
var box=qs('spec-modal-box');var h='';
|
||||||
|
h+='<span class="close" onclick="qs(\'spec-modal\').classList.remove(\'show\')">×</span>';
|
||||||
|
if(type==='human'){
|
||||||
|
var hh=spec.human_help||{};
|
||||||
|
h+='<h3>'+esc(hh.title||spec.module||'Help')+'</h3>';
|
||||||
|
if(hh.description){h+='<h4>说明</h4><ul>';hh.description.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
|
||||||
|
if(hh.usage){h+='<h4>使用方法</h4><ul>';hh.usage.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
|
||||||
|
if(hh.troubleshooting){h+='<h4>常见问题</h4><ul>';hh.troubleshooting.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
|
||||||
|
}else{
|
||||||
|
var ai=spec.ai_spec||{};
|
||||||
|
h+='<h3>§ AI Spec — '+esc(spec.module||'')+'</h3>';
|
||||||
|
h+='<p style="color:var(--dim)">'+esc(spec.purpose||'')+'</p>';
|
||||||
|
if(ai.apis){h+='<h4>API</h4><ul>';ai.apis.forEach(function(a){h+='<li><code>'+esc(a.method)+' '+esc(a.path)+'</code> — '+esc(a.returns||'')+'</li>'});h+='</ul>'}
|
||||||
|
if(ai.dependencies){h+='<h4>依赖</h4><ul>';ai.dependencies.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
|
||||||
|
if(ai.constraints){h+='<h4>关键约束</h4><ul>';ai.constraints.forEach(function(d){h+='<li style="color:var(--yellow)">'+esc(d)+'</li>'});h+='</ul>'}
|
||||||
|
if(ai.must_not){h+='<h4>禁止行为</h4><ul>';ai.must_not.forEach(function(d){h+='<li style="color:var(--red)">'+esc(d)+'</li>'});h+='</ul>'}
|
||||||
|
if(ai.related_files){h+='<h4>相关文件</h4><ul>';ai.related_files.forEach(function(f){h+='<li><code>'+esc(f)+'</code></li>'});h+='</ul>'}
|
||||||
|
}
|
||||||
|
box.innerHTML=h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Tab System ----
|
||||||
|
function init(){
|
||||||
|
var b='',p='';
|
||||||
|
for(var i=0;i<T.length;i++){
|
||||||
|
var t=T[i],a=t.id===at||(t.children&&t.children.some(function(c){return c.id===at}));
|
||||||
|
b+='<button class="tab-btn'+(a?' active':'')+'" onclick="sw(\''+t.id+'\')">'+t.label+'</button>';
|
||||||
|
if(t.children){
|
||||||
|
p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div class="sub-bar">';
|
||||||
|
for(var j=0;j<t.children.length;j++){
|
||||||
|
var c=t.children[j];
|
||||||
|
p+='<button class="sub-btn'+(c.id===as?' active':'')+'" onclick="sws(\''+c.id+'\')">'+c.label+'</button>';
|
||||||
|
}
|
||||||
|
p+='</div>';
|
||||||
|
for(var j=0;j<t.children.length;j++){
|
||||||
|
var c=t.children[j];
|
||||||
|
p+='<div id="sp-'+c.id+'"'+(c.id===as?'':' style="display:none"')+'><div id="ct-'+c.id+'"><span style="color:var(--dim)">Loading...</span></div></div>';
|
||||||
|
}
|
||||||
|
p+='</div>';
|
||||||
|
}else{
|
||||||
|
p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div id="ct-'+t.id+'"><span style="color:var(--dim)">Loading...</span></div></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qs('tabs').innerHTML=b;qs('panes').innerHTML=p;
|
||||||
|
}
|
||||||
|
function sw(id){
|
||||||
|
if(id==='principles'){at='principles';as=localStorage.getItem('ms')||'spec'}
|
||||||
|
else{at=id;localStorage.setItem('mt',id)}
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T.find(function(t){return t.id===at||(t.children&&t.children.some(function(c){return c.id===at}))}).label)});
|
||||||
|
document.querySelectorAll('.tab-pane').forEach(function(p){p.classList.toggle('active',p.id==='pn-'+id)});
|
||||||
|
if(id==='principles'){document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[1].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+as?'':'none'});(FETCHES[as]||function(){})()}
|
||||||
|
}
|
||||||
|
function sws(id){as=id;localStorage.setItem('ms',id);document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[1].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+id?'':'none'});(FETCHES[id]||function(){})()}
|
||||||
|
function fill(id,h){var e=qs('ct-'+id);if(e)e.innerHTML=h}
|
||||||
|
|
||||||
|
// ====== Services Tab ======
|
||||||
|
async function loadServices(){
|
||||||
|
try{
|
||||||
|
var r=await fetch('/api/services'),d=await r.json();
|
||||||
|
var svcs=d.services||[],ok=d.summary.ok||0,total=d.summary.total||0;
|
||||||
|
var hasIssue=ok<total,allOk=ok===total;
|
||||||
|
var h='<h2>服务状态<span class="help-btn" onclick="showModuleHelp(\'dashboard\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'dashboard\',\'ai\')" title="AI Spec">§</span></h2>';
|
||||||
|
h+='<div class="stats"><div class="stat-c '+(allOk?'g':'r')+'"><div class="n">'+ok+'/'+total+'</div><div class="l">Services Online</div></div></div>';
|
||||||
|
h+='<div class="section"><h2>全部服务</h2>';
|
||||||
|
// Group by layer
|
||||||
|
var layers={};
|
||||||
|
svcs.forEach(function(s){
|
||||||
|
var l=s.layer||'Other';
|
||||||
|
if(!layers[l])layers[l]=[];
|
||||||
|
layers[l].push(s);
|
||||||
|
});
|
||||||
|
var layerOrder=['通信层','核心服务','AI 网关','数据层','Other'];
|
||||||
|
layerOrder.forEach(function(ln){
|
||||||
|
if(!layers[ln])return;
|
||||||
|
h+='<div style="margin-bottom:10px"><div style="color:var(--dim);font-size:16px;margin-bottom:4px">'+esc(ln)+'</div>';
|
||||||
|
layers[ln].forEach(function(s){
|
||||||
|
var hOk=s.health&&s.health.ok;
|
||||||
|
h+='<div class="svc-row">';
|
||||||
|
h+='<div class="svc-dot '+(hOk?'ok':'fail')+'"></div>';
|
||||||
|
h+='<div class="svc-name">'+esc(s.label||s.name);
|
||||||
|
if(s.port)h+=' <span class="mono">:'+s.port+'</span>';
|
||||||
|
h+='</div>';
|
||||||
|
h+='<div class="svc-meta">'+esc(s.type)+'</div>';
|
||||||
|
h+='<div class="svc-status '+(hOk?'ok':'fail')+'">'+(hOk?'UP':'DOWN')+'</div>';
|
||||||
|
h+='<span class="help-btn" onclick="showModuleHelp(\''+esc(s.name)+'\',\'human\')" title="帮助">?</span>';
|
||||||
|
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+esc(s.name)+'\',\'ai\')" title="AI Spec">§</span>';
|
||||||
|
h+='</div>';
|
||||||
|
});
|
||||||
|
h+='</div>';
|
||||||
|
});
|
||||||
|
h+='</div>';
|
||||||
|
fill('services',h);
|
||||||
|
}catch(e){fill('services','<div class="section"><span style="color:var(--red)">Failed to load services: '+esc(e.message)+'</span></div>')}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== G 规范 Tab ======
|
||||||
|
async function loadSpec(){
|
||||||
|
var h='<div class="section">';
|
||||||
|
h+='<h2>开发规范<span class="help-btn" onclick="showModuleHelp(\'dashboard\',\'human\')" title="说明">?</span></h2>';
|
||||||
|
h+='<p>MoFin 开发规范定义了五条红线和 spec 双轨同源体系。</p>';
|
||||||
|
h+='<h4 style="color:var(--accent);margin-top:12px">五条红线</h4>';
|
||||||
|
h+='<ol><li>先读/写 Spec,再写代码</li><li>部署必验</li><li>不可见即不存在</li><li>实现后同步 Spec</li><li>部署目标即验收标准(Linux 246)</li></ol>';
|
||||||
|
h+='<h4 style="color:var(--accent);margin-top:12px">模块 Spec 清单</h4>';
|
||||||
|
h+='<p>以下模块已有 spec 文件,点击 ?§ 查看详情:</p>';
|
||||||
|
var modules=[
|
||||||
|
{id:'portfolio',label:'持仓管理'},
|
||||||
|
{id:'watchlist',label:'自选股'},
|
||||||
|
{id:'decisions',label:'策略决策库'},
|
||||||
|
{id:'market',label:'市场观察'},
|
||||||
|
{id:'signals',label:'信号与扫描'},
|
||||||
|
{id:'evaluation',label:'策略评估'},
|
||||||
|
{id:'dashboard',label:'Dashboard'},
|
||||||
|
{id:'health',label:'健康监控'}
|
||||||
|
];
|
||||||
|
h+='<div style="margin-top:8px">';
|
||||||
|
modules.forEach(function(m){
|
||||||
|
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 0">';
|
||||||
|
h+='<code style="flex:1">'+esc(m.id)+'</code><span style="color:var(--dim)">'+esc(m.label)+'</span>';
|
||||||
|
h+='<span class="help-btn" onclick="showModuleHelp(\''+esc(m.id)+'\',\'human\')" title="人类帮助">?</span>';
|
||||||
|
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+esc(m.id)+'\',\'ai\')" title="AI Spec">§</span>';
|
||||||
|
h+='</div>';
|
||||||
|
});
|
||||||
|
h+='</div>';
|
||||||
|
h+='<p style="margin-top:12px;font-size:16px;color:var(--dim)">完整规范见 <code>docs/dev-spec.md</code> 和 <code>docs/DEVELOPMENT_STANDARDS.md</code></p>';
|
||||||
|
h+='</div>';
|
||||||
|
fill('spec',h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== F 健康 Tab ======
|
||||||
|
async function loadHealth(){
|
||||||
|
try{
|
||||||
|
var r=await fetch('/api/monitor'),d=await r.json();
|
||||||
|
var tasks=d.tasks||[],tier1=d.tier1||{},tier2=d.tier2||{};
|
||||||
|
var h='<h2>系统健康<span class="help-btn" onclick="showModuleHelp(\'health\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'health\',\'ai\')" title="AI Spec">§</span></h2>';
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
var t1Ok=tier1.summary?tier1.summary.ok||0:0;
|
||||||
|
var t1Total=tier1.summary?tier1.summary.total||0:0;
|
||||||
|
var allOk=t1Ok===t1Total&&t1Total>0;
|
||||||
|
h+='<div class="stats"><div class="stat-c '+(allOk?'g':'y')+'"><div class="n">'+t1Ok+'/'+t1Total+'</div><div class="l">Tier1 通过</div></div></div>';
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
h+='<div class="section"><h2>定时任务</h2>';
|
||||||
|
tasks.forEach(function(t){
|
||||||
|
var cls=t.status==='cron_ok'||t.status==='running'?'g':(t.status==='not_deployed'?'y':'r');
|
||||||
|
var label=t.status==='cron_ok'?'正常':(t.status==='running'?'运行中':(t.status==='not_deployed'?'未部署':t.status));
|
||||||
|
h+='<div class="task-row">';
|
||||||
|
h+='<div class="tag '+cls+'">'+esc(label)+'</div>';
|
||||||
|
h+='<div>'+esc(t.name)+'</div>';
|
||||||
|
if(t.detail)h+='<div style="color:var(--dim);font-size:16px">'+esc(t.detail)+'</div>';
|
||||||
|
h+='</div>';
|
||||||
|
});
|
||||||
|
h+='</div>';
|
||||||
|
|
||||||
|
// Tier1 details
|
||||||
|
if(tier1.services&&tier1.services.length){
|
||||||
|
h+='<div class="section"><h2>Tier1 服务详情</h2>';
|
||||||
|
tier1.services.forEach(function(s){
|
||||||
|
var ok=s.health&&s.health.ok;
|
||||||
|
h+='<div class="svc-row"><div class="svc-dot '+(ok?'ok':'fail')+'"></div><div class="svc-name">'+esc(s.label||s.name)+'</div><div class="svc-status '+(ok?'ok':'fail')+'">'+(ok?'OK':'FAIL')+'</div></div>';
|
||||||
|
});
|
||||||
|
h+='</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
h+='<p style="font-size:16px;color:var(--dim);margin-top:8px">生成时间: '+esc(d.generated_at||'')+'</p>';
|
||||||
|
fill('health',h);
|
||||||
|
}catch(e){fill('health','<div class="section"><span style="color:var(--red)">Failed to load health data: '+esc(e.message)+'</span></div>')}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Fetch dispatch ----
|
||||||
|
var FETCHES={'services':loadServices,'spec':loadSpec,'health':loadHealth};
|
||||||
|
|
||||||
|
// ---- Auto-refresh ----
|
||||||
|
function autoRefresh(){
|
||||||
|
var activeTab=localStorage.getItem('mt')||'services';
|
||||||
|
if(activeTab==='services')loadServices();
|
||||||
|
else if(activeTab==='principles'){
|
||||||
|
var sub=localStorage.getItem('ms')||'spec';
|
||||||
|
if(sub==='health')loadHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Boot ----
|
||||||
|
init();
|
||||||
|
// Load initial tab
|
||||||
|
var tab=localStorage.getItem('mt')||'services';
|
||||||
|
if(tab==='services')loadServices();
|
||||||
|
else if(tab==='principles'){
|
||||||
|
var sub=localStorage.getItem('ms')||'spec';
|
||||||
|
if(sub==='spec')loadSpec();
|
||||||
|
else if(sub==='health')loadHealth();
|
||||||
|
}
|
||||||
|
// Auto-refresh every 5 seconds
|
||||||
|
setInterval(autoRefresh,5000);
|
||||||
|
</script></body></html>
|
||||||
Reference in New Issue
Block a user