From a05c118cbcd865abd33ad30de70dbe5134749de8 Mon Sep 17 00:00:00 2001 From: hmo Date: Mon, 20 Jul 2026 00:40:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20restore=20lost=20monitoring?= =?UTF-8?q?=20+=20add=20=E5=BC=80=E5=8F=91=E5=8E=9F=E5=88=99=20tab=20(Agen?= =?UTF-8?q?tsMeeting=20parity)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions fixed: 1. RESTORED: original health monitoring (功能树/全部Cron/数据实体/数据流) was an iframe to /mofin_health.html — refactor replaced it with a minimal services-only panel and lost all of it. mofin_health.json (86KB, fresh) was still being generated the whole time. - 健康 tab: XMPP panel (native) + restored iframe below 2. ADDED: 开发原则 parent tab replicating AgentsMeeting structure: - G 规范: /api/spec renders docs/dev-spec.md + git history - K 测试: /api/tests renders agents_health_check report as pass/fail - F 健康: iframe to /mofin_health.html + link to 健康 tab - H 需求: /api/prd (placeholder — prd.md not yet created) - mdRender() ported from AgentsMeeting dashboard --- scripts/sc2.py | 8 +++ server.py | 66 +++++++++++++++++ static/index.html | 176 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 scripts/sc2.py diff --git a/scripts/sc2.py b/scripts/sc2.py new file mode 100644 index 00000000..fec840ab --- /dev/null +++ b/scripts/sc2.py @@ -0,0 +1,8 @@ +import ast, sys +p = '/home/hmo/MoFin/server.py' +try: + ast.parse(open(p).read()) + print('SYNTAX OK:', p) +except SyntaxError as e: + print('SYNTAX ERROR:', e) + sys.exit(1) \ No newline at end of file diff --git a/server.py b/server.py index baa15f4a..99b2648b 100644 --- a/server.py +++ b/server.py @@ -1340,6 +1340,72 @@ def api_xmpp_keys(): return jsonify({"error": "xmpp_logger not available"}), 500 +# ── 开发原则 Tab 端点(仿 AgentsMeeting: G规范/K测试/H需求)── + +DOCS_DIR = Path(__file__).resolve().parent / "docs" +HEALTH_REPORT = Path(__file__).resolve().parent / "gateway" / "temp" / "last_health_check.json" + + +@app.route("/api/spec") +def api_spec(): + """G 规范:docs/dev-spec.md 内容""" + f = DOCS_DIR / "dev-spec.md" + if not f.exists(): + return jsonify({"ok": False, "error": "dev-spec.md not found"}) + return jsonify({"ok": True, "content": f.read_text(encoding="utf-8")}) + + +@app.route("/api/spec/history") +def api_spec_history(): + """dev-spec.md 的 git 历史""" + import subprocess + try: + r = subprocess.run( + ["git", "log", "--oneline", "-20", "--", "docs/dev-spec.md"], + capture_output=True, timeout=10, text=True, + cwd=str(Path(__file__).resolve().parent)) + lines = [l for l in r.stdout.splitlines() if l.strip()] + return jsonify({"ok": True, "log": lines, "count": len(lines)}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)[:100]}) + + +@app.route("/api/tests") +def api_tests(): + """K 测试:agents_health_check 服务检查结果,渲染为 pass/fail 测试报告""" + if not HEALTH_REPORT.exists(): + return jsonify({"ok": False, "error": "health report not found (cron not run yet)"}) + try: + rep = json.loads(HEALTH_REPORT.read_text(encoding="utf-8")) + except Exception as e: + return jsonify({"ok": False, "error": str(e)[:100]}) + tests = [] + for svc in rep.get("services", []): + ok = svc.get("health", {}).get("ok", False) + tests.append({ + "name": f"{svc.get('label', svc.get('name'))} ({svc.get('name')})", + "ok": ok, + "expected": False, + "detail": svc.get("detail", ""), + }) + passed = sum(1 for t in tests if t["ok"]) + return jsonify({ + "ok": True, + "tests": tests, + "summary": {"total": len(tests), "passed": passed, "failed": len(tests) - passed}, + "time": rep.get("generated_at", ""), + }) + + +@app.route("/api/prd") +def api_prd(): + """H 需求:docs/prd.md(不存在则返回未建立)""" + f = DOCS_DIR / "prd.md" + if not f.exists(): + return jsonify({"ok": False, "error": "prd.md 尚未建立"}) + return jsonify({"ok": True, "content": f.read_text(encoding="utf-8")}) + + # 注册提示词管理路由 register_routes(app) diff --git a/static/index.html b/static/index.html index 4c275241..fba53f7c 100644 --- a/static/index.html +++ b/static/index.html @@ -12,6 +12,8 @@ body { background: #0f0f13; color: #e2e8f0; } .tab-btn { transition: all 0.15s; } .tab-btn.active { background: #1e293b; color: #60a5fa; border-color: #60a5fa; } +.princ-btn { transition: all 0.15s; color: #8b949e; } +.princ-btn.active { background: #1e293b; color: #60a5fa; } .card { background: #1a1a24; border: 1px solid #2a2a3a; border-radius: 12px; } .badge-up { color: #22c55e; } .badge-down { color: #ef4444; } @@ -63,6 +65,7 @@ body { background: #0f0f13; color: #e2e8f0; } + 📸 上传 @@ -78,6 +81,18 @@ body { background: #0f0f13; color: #e2e8f0; } + '; } + + // ── 原有健康监控(功能树/Cron/数据实体/数据流)—— 恢复自 mofin_health.html ── + html += '
'; + html += '
📊 系统健康监控(功能树 · Cron · 数据实体 · 数据流)
'; + html += ''; + html += '
'; + el.innerHTML = html; el.dataset.rendered = '1'; // 标记已首渲 } catch(e) { @@ -1748,6 +1771,159 @@ async function renderWatch() { watchTimer = setInterval(renderWatch, 60000); } } + +// ── 开发原则 Tab(仿 AgentsMeeting: G规范/K测试/F健康/H需求)── +let _princLoaded = {}; +function renderPrinciples() { + // 绑定子tab按钮 + document.querySelectorAll('.princ-btn').forEach(btn => { + btn.onclick = () => { + document.querySelectorAll('.princ-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.princ-pane').forEach(p => p.classList.add('hidden')); + btn.classList.add('active'); + const id = btn.dataset.princ; + document.getElementById('princ-' + id).classList.remove('hidden'); + _loadPrinc(id); + }; + }); + // 默认加载当前激活的子tab + const active = document.querySelector('.princ-btn.active'); + _loadPrinc(active ? active.dataset.princ : 'spec'); +} + +function _loadPrinc(id) { + if (id === 'spec') return _renderPrincSpec(); + if (id === 'tests') return _renderPrincTests(); + if (id === 'health') return _renderPrincHealth(); + if (id === 'prd') return _renderPrincPrd(); +} + +async function _renderPrincSpec() { + const el = document.getElementById('princ-spec'); + el.innerHTML = '
加载中...
'; + try { + const d = await fetchJSON('/api/spec'); + if (!d.ok) { el.innerHTML = '
' + (d.error || '加载失败') + '
'; return; } + let h = '
' + mdRender(d.content) + '
'; + try { + const dh = await fetchJSON('/api/spec/history'); + if (dh.ok && dh.log && dh.log.length) { + h += '
历史版本 (' + dh.count + ' commits)
'; + } + } catch(e) {} + el.innerHTML = h; + } catch(e) { el.innerHTML = '
加载失败: ' + e.message + '
'; } +} + +async function _renderPrincTests() { + const el = document.getElementById('princ-tests'); + el.innerHTML = '
加载中...
'; + try { + const d = await fetchJSON('/api/tests'); + if (!d.ok) { el.innerHTML = '
' + (d.error || '加载失败') + '
'; return; } + const s = d.summary || {}; + let h = '
'; + h += '
' + (s.total||0) + '
Total
'; + h += '
' + (s.passed||0) + '
Passed
'; + h += '
' + (s.failed||0) + '
Failed
'; + h += '
'; + const fails = (d.tests||[]).filter(t => !t.ok); + if (fails.length) { + h += '
Failed (' + fails.length + ')
'; + fails.forEach(t => { + h += '
FAIL ' + t.name.replace(/
' + (t.detail||'').replace(/
'; + }); + } + const passes = (d.tests||[]).filter(t => t.ok); + h += '
Passed (' + passes.length + ')
'; + passes.forEach(t => { + h += '
PASS ' + t.name.replace(/ ' + (t.detail||'').replace(/
'; + }); + h += '
数据源: agents_health_check (cron */5min) · 最后运行: ' + (d.time||'?') + '
'; + el.innerHTML = h; + } catch(e) { el.innerHTML = '
加载失败: ' + e.message + '
'; } +} + +function _renderPrincHealth() { + const el = document.getElementById('princ-health'); + if (_princLoaded.health) return; + el.innerHTML = '
完整健康监控与 🏥 健康 Tab 同源。快捷入口:🏥 健康 Tab
' + + '
'; + _princLoaded.health = true; +} + +async function _renderPrincPrd() { + const el = document.getElementById('princ-prd'); + el.innerHTML = '
加载中...
'; + try { + const d = await fetchJSON('/api/prd'); + if (!d.ok) { el.innerHTML = '
' + (d.error || 'prd.md 尚未建立') + '
'; return; } + el.innerHTML = '
' + mdRender(d.content) + '
'; + } catch(e) { el.innerHTML = '
加载失败: ' + e.message + '
'; } +} + +// markdown 渲染(移植自 AgentsMeeting dashboard) +function mdRender(md) { + if (!md) return ''; + let h = ''; + const blocks = md.split('\n\n'); + for (const raw of blocks) { + const block = raw.trim(); + if (!block) continue; + if (block.startsWith('```')) { + const code = block.replace(/^```[\s\S]*?\n/, '').replace(/```$/, '').trim(); + h += '
' + code.replace(/';
+      continue;
+    }
+    if (block.startsWith('#### ')) { h += '

' + inMd(block.slice(5).replace(/'; continue; } + if (block.startsWith('### ')) { h += '

' + inMd(block.slice(4).replace(/'; continue; } + if (block.startsWith('## ')) { h += '

' + inMd(block.slice(3).replace(/'; continue; } + if (block.startsWith('# ')) { h += '

' + inMd(block.slice(2).replace(/'; continue; } + if (block.startsWith('> ')) { h += '
' + inMd(block.slice(2).replace(/'; continue; } + if (block.startsWith('---') || block.startsWith('***')) { h += '
'; continue; } + if (block.startsWith('- ') || block.startsWith('* ')) { + const items = block.split('\n'); + h += '
    '; + items.forEach(li => { const t = li.replace(/^[-*]\s*/, '').trim(); if (t) h += '
  • ' + inMd(t.replace(/'; }); + h += '
'; + continue; + } + if (/^\d+\.\s/.test(block)) { + const items = block.split('\n'); + h += '
    '; + items.forEach(li => { const t = li.replace(/^\d+\.\s*/, '').trim(); if (t) h += '
  1. ' + inMd(t.replace(/'; }); + h += '
'; + continue; + } + if (block.indexOf('|') >= 0 && block.indexOf('\n|') >= 0) { + const rows = block.split('\n'); + let inTable = false; + rows.forEach(row => { + if (row.indexOf('---') >= 0) return; + const cells = row.split('|').filter(c => c.trim()); + if (cells.length < 2) return; + if (!inTable) { + h += '' + cells.map(c => ''; + inTable = true; + } else { + h += '' + cells.map(c => ''; + } + }); + if (inTable) h += '
' + inMd(c.trim().replace(/').join('') + '
' + inMd(c.trim().replace(/').join('') + '
'; + continue; + } + h += '

' + inMd(block.replace(/'; + } + return h; +} +function inMd(t) { + return t.replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); +} \ No newline at end of file