merge: L0-L4 self-check architecture

This commit is contained in:
知微
2026-07-20 19:40:24 +08:00
8 changed files with 1584 additions and 1409 deletions
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -156,7 +156,27 @@ specs/{module}.json
---
## 三、开发流程
## 三、自检体系责任矩阵(L0-L4
> 2026-07-20 确立。原则:**每层职责单一,不重叠不疏漏;判据是"功能是否达成",不是"进程是否活着"**。
| 层 | 组件 | 频率 | 职责(唯一) | 判据 |
|---|------|------|-------------|------|
| L0 执行心跳 | `agents_health_check.py`(系统 crontab | 5min | 端口/HTTP/DB 存活 + auto_heal 执行(gateway 重启/key 切换/bot 重启) | 端口通 + HTTP 200 + DB 可写 |
| L1 功能健康 | `functional_health_check.py`hermes cron | 交易时段 15min | 核心模块**功能是否达成**:检查输出物新鲜度/有效性(live_prices/market_snapshots/mtf_cache/macro_context/bot/LLM/cron引擎) | 每模块注册表判据(REGISTRY),输出 `functional_health.json` |
| L2 系统卫生 | `system_hygiene_audit.py`hermes cron | 每日 08:20 | 分叉副本/断裂硬链接/僵尸进程/孤儿文件/死cron/DB新鲜度(红线6-10 enforcement | 输出 `hygiene_report.json` |
| L3 修复循环 | `self_repair.py`hermes cron | 30min | 读 L1/L2 失败项 → LLM 诊断 → **白名单动作直接修复(报备制)**`repair_log.jsonl` + XMPP | LLM 只能选白名单动作;每模块每天≤2次防循环 |
| L4 元监控 | `meta_watchdog.py`hermes cron | 每小时 | **自检系统的自检**:L0-L3 输出物新鲜度 + L3 注册状态 + XMPP 桥 | 任何一层死亡直接 XMPP 点名(最后兜底) |
**已退休(职责被合并)**`Cron监护-高频`cron_watchdog,并入 L3)、`全局cron健康监控-每10分`cron_health_monitor,并入 L1)。
### 报备制(L3 的核心纪律)
发现问题 → **直接修复** → 记录日志 → XMPP 报备。不是"发现问题→报告→等指示"。LLM 介入诊断但**只能执行白名单动作**rerun_script/restart_service/sync_links/switch_llm_key/none),不允许任意代码执行。白名单兜不住的,在 XMPP 里明确说"需要人工"。
---
## 四、开发流程
### 新增功能流程
+65
View File
@@ -0,0 +1,65 @@
import json, urllib.request
msg = """[笑笑] 系统大扫除 + 防复发机制 — 全部完成
■ 清理执行结果
【小果生态 — 全灭】
- xmpp_xiaoguo_bot(root, 8天2.5GB) 已杀,3个systemd服务已删除
- 4个 cron job 已删(quick-scan/tunnel-watchdog/情感分析/独立扫描)
- 7个脚本 + 数据文件 + profile目录 全部归档 archive/xiaoguo-retired-20260720/
【第三库 — 已合并】
- sector_snapshots +4978行、market_snapshots +187行、todos +2 并入主库
- 第三库及其数据目录已移 trashbox
【文件源统一 — 31个分叉全处理】
- 库文件(strategy_lifecycle等5个):统一到 MoFin rootcron实际import的版本)
- cron脚本(13个):deploy为唯一源,旧副本全归档
- 新增 sync_profile_scripts.sh 防硬链接断裂
- crontab 三处改指 deploy 路径(cron_to_xmpp/market_watch/market_screener),已验证可跑
【其余清理】
- 21个遗留JSON + 3个废弃小库(market.db/market_data.db/stock_analysis.db) 归档
- 8个禁用cron任务删除(pa 52 jobs / default 17 jobs,禁用项清零)
- 9个旧日志 + 旧bot/bak文件归档
- projects/ 两个旧克隆(1.4G+2.2M) 归档
■ 问题是怎么产生的(根因)
1. 没有单一事实源规则——同一文件可以"合法"住在4个地方,悄悄分叉
2. 相对路径解析——mofin_db.py 用 __file__.parent 找数据目录,每个硬链接副本解析出不同的库(三库事件根源)
3. "备份留生产目录"的习惯——.bak/迁移残留留在数据目录,被监控扫描后变成假警报
4. 迁移只做一半——DB建了表,旧JSON的读写着还留着(price_events 就是这么分裂的)
5. 模块死了没人收尸——小果宣布"已废弃",但bot/gateway/tunnel/cron照跑
6. 监控查"在不在"不查"活不活"——拿遗留文件mtime当管道健康,报出"停滞14天"假警报
7. 压根没有"系统卫生"这个检查类别——分叉/僵尸/孤儿文件没人看
■ 防复发机制(已落地)
【规范层】dev-spec.md 升级 v2.0:五条红线 → 十条红线
#6 单一事实源(只许硬链接,禁止独立副本)
#7 数据路径必须绝对(禁止__file__相对解析数据路径)
#8 备份/遗留物禁止留生产数据目录
#9 死模块收尸六步清单(杀进程/停服务/删cron/归档脚本/归档数据/摘监控)
#10 监控查活不查在(DB表新鲜度才算健康证据)
+ 文件位置宪法:每类内容的唯一权威位置表
【监控层】新增 system_hygiene_audit.py
- 每周一 07:30 自动跑:分叉副本/断裂硬链接/僵尸进程/孤儿文件/死cron/DB新鲜度 六项检查
- 结果写 hygiene_report.json,有问题自动推 XMPP
- 已注册进 cronpa jobs.json
- specs/hygiene.json 模块 spec 已就位
- 首轮实测:发现5个真问题(1个漏网分叉+2个我清理时误删的default脚本+2个合法容器进程误报),已全部修复,复检六项全绿
这套机制保证:下次再出现"这一行那也可以"的冗余,周一早上 07:30 你就会收到 XMPP 点名。
代码已提交合并(246: c545a820)。"""
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"})
try:
print("XMPP:", urllib.request.urlopen(req, timeout=10).read().decode()[:80])
except Exception as e:
print("XMPP fail:", e)
+59
View File
@@ -0,0 +1,59 @@
import json, shutil, uuid
from datetime import datetime
jf = '/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'
shutil.copy(jf, jf + '.bak-20260720-l134')
d = json.load(open(jf))
is_list = isinstance(d, list)
jobs = d if is_list else d.get('jobs', [])
def has_script(s):
return any(j.get('script') == s for j in jobs)
def mkjob(name, script, schedule, display, next_run):
return {
"id": uuid.uuid4().hex[:12], "name": name, "prompt": "", "skills": [], "skill": None,
"model": None, "provider": None, "base_url": None, "script": script,
"no_agent": True, "context_from": None,
"schedule": {"kind": "cron", "expr": schedule, "display": display},
"schedule_display": display,
"repeat": {"times": None, "completed": 0}, "enabled": True, "state": "scheduled",
"paused_at": None, "paused_reason": None, "created_at": datetime.now().isoformat(),
"next_run_at": next_run, "last_run_at": None, "last_status": None,
}
# 1. 新增 L1/L3/L4 jobs
added = []
if not has_script('functional_health_check.py'):
jobs.append(mkjob('功能健康检查-L1', 'functional_health_check.py', '*/15 9-16,20-22 * * 1-5', '*/15 9-16,20-22 * * 1-5', '2026-07-20T20:30:00+08:00'))
added.append('功能健康检查-L1(15min)')
if not has_script('self_repair.py'):
jobs.append(mkjob('LLM修复循环-L3', 'self_repair.py', '*/30 9-16,20-22 * * 1-5', '*/30 9-16,20-22 * * 1-5', '2026-07-20T20:30:00+08:00'))
added.append('LLM修复循环-L3(30min)')
if not has_script('meta_watchdog.py'):
jobs.append(mkjob('元监控-自检系统的自检-L4', 'meta_watchdog.py', '5 * * * *', '5 * * * *', '2026-07-20T20:05:00+08:00'))
added.append('元监控-L4(每小时)')
# 2. hygiene 改每日(原每周一 07:30 → 每日 08:20
for j in jobs:
if j.get('script') == 'system_hygiene_audit.py':
j['schedule'] = {"kind": "cron", "expr": "20 8 * * *", "display": "20 8 * * *"}
j['schedule_display'] = "20 8 * * *"
j['name'] = '系统卫生审计-每日'
j['next_run_at'] = '2026-07-21T08:20:00+08:00'
added.append('hygiene改每日08:20')
# 3. 退休重叠组件:Cron监护-高频(cron_watchdog) + 全局cron健康监控(cron_health_monitor)
REMOVE = {'Cron监护-高频', '全局cron健康监控-每10分'}
removed = [j.get('name') for j in jobs if j.get('name') in REMOVE]
jobs = [j for j in jobs if j.get('name') not in REMOVE]
if is_list:
json.dump(jobs, open(jf, 'w'), ensure_ascii=False, indent=2)
else:
d['jobs'] = jobs
json.dump(d, open(jf, 'w'), ensure_ascii=False, indent=2)
print('added:', added)
print('removed:', removed)
print('total jobs:', len(jobs))
+15
View File
@@ -0,0 +1,15 @@
import json, shutil
jf = '/home/hmo/.hermes/cron/jobs.json'
shutil.copy(jf, jf + '.bak-20260720-l134')
d = json.load(open(jf))
is_list = isinstance(d, list)
jobs = d if is_list else d.get('jobs', [])
REMOVE = {'Cron监护-高频'}
removed = [j.get('name') for j in jobs if j.get('name') in REMOVE]
jobs = [j for j in jobs if j.get('name') not in REMOVE]
if is_list:
json.dump(jobs, open(jf, 'w'), ensure_ascii=False, indent=2)
else:
d['jobs'] = jobs
json.dump(d, open(jf, 'w'), ensure_ascii=False, indent=2)
print('removed:', removed, '| total:', len(jobs))
+6
View File
@@ -0,0 +1,6 @@
import sys
sys.path.insert(0, '/home/hmo/.hermes/profiles/position-analyst/scripts')
from self_repair import run_action
ok, detail = run_action({'action': 'rerun_script', 'script': 'mofin_health.py'}, 'health_collector')
print('ok:', ok)
print('detail:', detail[:200])
+10
View File
@@ -0,0 +1,10 @@
import json
d = json.load(open('/home/hmo/web-dashboard/static/mofin_health.json'))
sc = d.get('self_check', {})
print('functional:', bool(sc.get('functional')), '| meta:', bool(sc.get('meta_watchdog')),
'| hygiene:', bool(sc.get('hygiene')), '| repairs:', len(sc.get('recent_repairs', [])))
if sc.get('functional'):
fh = sc['functional']
print('functional status:', fh.get('status'), fh.get('summary'))
if sc.get('meta_watchdog'):
print('meta status:', sc['meta_watchdog'].get('status'))
+390 -390
View File
@@ -1,390 +1,390 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>MoFin 健康监控</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui,-apple-system,sans-serif}
body{background:#0d1117;color:#c9d1d9;padding:20px}
h1{font-size:22px;margin-bottom:16px;color:#58a6ff}
.tabs{display:flex;gap:4px;margin-bottom:16px;border-bottom:1px solid #30363d;flex-wrap:wrap}
.tab{padding:8px 20px;cursor:pointer;border:1px solid transparent;border-radius:4px 4px 0 0;color:#8b949e;font-size:14px}
.tab.active{background:#161b22;border-color:#30363d #30363d #161b22;color:#c9d1d9}
.tab:hover{color:#58a6ff}
.panel{display:none}
.panel.active{display:block}
.badge{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px}
.badge.ok{background:#3fb950}
.badge.warn{background:#d29922}
.badge.fail{background:#f85149}
table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:20px}
th,td{padding:5px 8px;text-align:left;border-bottom:1px solid #21262d;white-space:nowrap}
th{background:#161b22;color:#8b949e;font-weight:500;position:sticky;top:0}
tr:hover{background:#1c2128}
td.desc{color:#8b949e;font-size:11px;white-space:normal;max-width:260px}
td.wrap{white-space:normal;font-size:11px;max-width:200px}
.tree-node{padding:2px 0;font-size:13px}
.tree-children{padding-left:22px;border-left:1px solid #30363d;margin-left:8px}
.tree-node.ok{color:#3fb950}
.tree-node.warn{color:#d29922}
.tree-node.fail{color:#f85149}
.pipeline-ok{color:#3fb950}
.pipeline-warn{color:#d29922}
.pipeline-error{color:#f85149}
.pipeline-ever{color:#8b949e}
.orphan{background:#2d1517!important}
.orphan td{color:#f85149}
.summary{display:flex;gap:20px;margin-bottom:16px;font-size:13px;color:#8b949e;flex-wrap:wrap}
.summary span strong{color:#c9d1d9}
.toggle{color:#58a6ff;cursor:pointer;font-size:11px;margin-left:8px}
.search-box{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;padding:6px 12px;border-radius:4px;margin-bottom:10px;width:300px;font-size:13px}
.cron-detail{font-size:11px;color:#8b949e;padding-left:20px}
.cron-tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;margin:0 2px}
.cron-tag.ok{background:#1a3a1a;color:#3fb950;border:1px solid #3fb950}
.cron-tag.warn{background:#3a2a1a;color:#d29922;border:1px solid #d29922}
.cron-tag.error{background:#3a1a1a;color:#f85149;border:1px solid #f85149}
.cron-tag.new{background:#1a1a3a;color:#58a6ff;border:1px solid #58a6ff}
</style></head>
<body>
<h1>📊 MoFin 系统健康监控</h1>
<div class="summary" id="summary">加载中...</div>
<div class="tabs">
<div class="tab active" onclick="switchTab(0)">🌳 功能树</div>
<div class="tab" onclick="switchTab(1)">🔧 全部流程/Cron</div>
<div class="tab" onclick="switchTab(2)">🗃️ 数据实体</div>
<div class="tab" onclick="switchTab(3)">🔀 数据流</div>
<div class="tab" onclick="switchTab(4)">🩺 自检体系</div>
</div>
<div id="panel0" class="panel active"></div>
<div id="panel1" class="panel"></div>
<div id="panel2" class="panel"></div>
<div id="panel3" class="panel"></div>
<div id="panel4" class="panel"></div>
<script>
let data = null;
function renderSelfCheck(sc) {
if (!sc) return '<div style="color:#8b949e">自检体系数据未生成(等待 L1/L4 cron 首次运行)</div>';
let html = '';
const icon = s => s === 'ok' ? '✅' : s === 'warn' ? '🟡' : s === 'fail' ? '❌' : '⏭';
// L4 元监控(自检系统的自检)
if (sc.meta_watchdog) {
const mw = sc.meta_watchdog;
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">🐕 L4 元监控 — 自检系统的自检 <span style="color:${mw.status === 'ok' ? '#3fb950' : '#f85149'}">${icon(mw.status)} ${mw.status}</span> <span style="color:#8b949e;font-size:11px">${mw.generated_at || ''}</span></div>`;
html += '<table><thead><tr><th>层</th><th>状态</th><th>说明</th></tr></thead>';
(mw.layers || []).forEach(l => {
html += `<tr><td>${l.layer}</td><td class="pipeline-${l.status === 'ok' ? 'ok' : 'error'}">${icon(l.status)} ${l.status}</td><td style="font-size:11px;color:#8b949e">${l.reason || ''}</td></tr>`;
});
html += '</table></div>';
}
// L1 功能健康
if (sc.functional) {
const fh = sc.functional;
const s = fh.summary || {};
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">⚙️ L1 功能健康(功能是否达成,非进程存活) <span style="color:${fh.status === 'ok' ? '#3fb950' : fh.status === 'warn' ? '#d29922' : '#f85149'}">${icon(fh.status)} ${fh.status}</span> <span style="color:#8b949e;font-size:11px">ok:${s.ok} warn:${s.warn} fail:${s.fail} skip:${s.skip} · ${fh.generated_at || ''}</span></div>`;
html += '<table><thead><tr><th>模块</th><th>功能判据</th><th>状态</th><th>说明</th></tr></thead>';
(fh.checks || []).forEach(c => {
html += `<tr><td>${c.module}</td><td style="font-size:11px;color:#8b949e">${c.function}</td><td class="pipeline-${c.status === 'ok' ? 'ok' : c.status === 'warn' ? 'warn' : c.status === 'fail' ? 'error' : 'ever'}">${icon(c.status)} ${c.status}</td><td style="font-size:11px;color:#8b949e">${c.reason || ''}</td></tr>`;
});
html += '</table></div>';
}
// L2 卫生审计
if (sc.hygiene) {
const hy = sc.hygiene;
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">🧹 L2 系统卫生审计(每日08:20 <span style="color:${hy.status === 'ok' ? '#3fb950' : '#d29922'}">${icon(hy.status)} ${hy.status}</span> <span style="color:#8b949e;font-size:11px">${hy.issue_count} 个问题 · ${hy.generated_at || ''}</span></div>`;
(hy.issues || []).forEach(i => {
html += `<div style="font-size:11px;color:#d29922">• [${i.type}] ${i.file || i.job || i.table || ''}${i.action || ''}</div>`;
});
html += '</div>';
}
// L3 修复记录
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">🔧 L3 LLM 修复循环 — 最近修复动作(报备制)</div>`;
const reps = sc.recent_repairs || [];
if (!reps.length) {
html += '<div style="font-size:11px;color:#8b949e">暂无修复记录(系统健康或 L3 未触发)</div>';
} else {
reps.forEach(r => {
const okIcon = r.ok === true ? '✅' : r.ok === false ? '❌' : '⏭';
html += `<div style="font-size:11px;margin:3px 0"><span style="color:#8b949e">${(r.ts || '').slice(5, 16)}</span> ${okIcon} [${r.module}] ${(r.action || {}).action || r.action}: <span style="color:#8b949e">${(r.detail || r.reason || '').slice(0, 80)}</span></div>`;
});
}
html += '</div>';
return html;
}
function switchTab(idx) {
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',i==idx));
document.querySelectorAll('.panel').forEach((p,i)=>p.classList.toggle('active',i==idx));
}
function renderFeatureTree(tree) {
let grp = (function(){let i=0;return function(){return 'ftg-'+(++i);}})();
let html = '<table><thead><tr><th style="width:32%">功能模块</th><th style="width:35%">说明</th><th>来源</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead>';
function walk(node, depth, groupId) {
const indent = 'padding-left:'+(depth*20+8)+'px';
const cls = node.status || 'ok';
const hasKids = node.children && node.children.length > 0;
const pipes = node.pipes || [];
const isCat = hasKids && pipes.length === 0;
if (isCat) {
// 分类节点——生成一个可折叠组
const g = grp();
const desc = node.desc || '';
html += `<tr id="${g}-h" onclick="toggleFt('${g}')" style="cursor:pointer" data-group="${groupId||''}">`;
html += `<td style="${indent}"><span id="${g}-ico">▼</span> <span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
html += `<td style="font-size:11px;color:#8b949e">${desc}</td>`;
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls}">${cls}</td><td>-</td></tr>`;
// 子节点都带上这个组的data-sub
node.children.forEach(c => walk(c, depth+1, g));
return;
}
if (pipes.length > 0) {
pipes.forEach((p, i) => {
const indent2 = i===0 ? indent : 'padding-left:'+(depth*20+32)+'px';
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
const pBadge = p.profile==='position-analyst' ? '📋' : '📦';
const descText = i===0 ? (node.desc || '') : '';
const groupAttr = groupId ? `data-group="${groupId}"` : '';
if (i===0) {
html += `<tr ${groupAttr}><td style="${indent2}"><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
} else {
html += `<tr ${groupAttr}><td style="${indent2}"><span style="color:#30363d">└</span> ${p.name||p.script||'LLM'}</td>`;
}
html += `<td style="font-size:11px;color:#8b949e">${descText}</td>`;
html += `<td><span class="cron-tag ${tagCls}">${p.type||'LLM'}</span></td>`;
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
html += `<td class="pipeline-${tagCls}">${p.status}</td>`;
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
});
} else {
// 无cron的叶子
const groupAttr = groupId ? `data-group="${groupId}"` : '';
html += `<tr ${groupAttr}><td style="${indent}"><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls}">${cls}</td><td>-</td></tr>`;
}
if (hasKids && pipes.length > 0) {
node.children.forEach(c => walk(c, depth+1, groupId));
}
}
walk(tree, 0, null);
html += '</table>';
return html;
}
// 折叠切换:通过data-group控制
let ftState = {};
function toggleFt(g) {
const ico = document.getElementById(g+'-ico');
const isHidden = ftState[g];
ftState[g] = !isHidden;
ico.textContent = isHidden ? '▼' : '▶';
// 隐藏/显示所有带data-group={g}的行
document.querySelectorAll(`[data-group="${g}"]`).forEach(el => {
el.style.display = isHidden ? '' : 'none';
});
}
function renderDataFlow(entities, jsonFiles, architecture) {
// 只显示有写入方或读取方的实体
const filtered = entities.filter(e => (e.writers||[]).length > 0 || (e.readers||[]).length > 0);
// 只显示有读写关系的json
const jf = jsonFiles.filter(j => (j.readers||[]).length > 0);
let html = '<div style="margin-bottom:10px;color:#8b949e;font-size:13px">';
// 架构违规告警
const violations = (architecture||{}).price_api_violations || [];
if (violations.length > 0) {
const scripts = [...new Set(violations.map(v => v.script))];
html += `<div style="background:#3d1c02;border:1px solid #d29922;border-radius:6px;padding:10px;margin-bottom:12px">`;
html += `<div style="font-weight:bold;color:#d29922;margin-bottom:4px">⚠️ 架构违规: ${violations.length}处, ${scripts.length}个脚本自拉API不走live_prices</div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:6px">预期: price_monitor→live_prices→其他脚本读。以下脚本直接调腾讯API:</div>`;
html += `<div style="font-size:11px">`;
scripts.forEach(s => {
const lines = violations.filter(v => v.script === s).map(v => `L${v.line}`).join(',');
html += `<span class="cron-tag" style="border-color:#d29922;color:#d29922;margin-right:4px;margin-bottom:3px;display:inline-block">${s} (${lines})</span>`;
});
html += `</div></div>`;
}
html += `显示 ${filtered.length} 个数据表 + ${jf.length} 个JSON文件的读写流向。绿色→写入,蓝色→读取。</div>`;
// 按写入方数量排序,最活跃的排前面
filtered.sort((a,b) => (b.writers||[]).length - (a.writers||[]).length);
filtered.forEach(e => {
const w = e.writers || [];
const r = e.readers || [];
if (w.length === 0 && r.length === 0) return;
const fd = e.flow_detail || {};
const wDesc = fd.writers || {};
const rDesc = fd.readers || {};
html += '<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px">';
html += `<div style="font-weight:bold;font-size:14px;color:#58a6ff;margin-bottom:4px">🗄️ ${e.name}</div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:4px">${e.desc||''}</div>`;
// 综合总结
if (fd.summary) {
html += `<div style="font-size:11px;color:#c9d1d9;margin-bottom:6px;padding:4px 8px;background:#0d1117;border-radius:4px">📌 ${fd.summary}</div>`;
}
// 写入方(带详细说明)
if (w.length > 0) {
html += '<div style="margin-bottom:4px">';
html += '<span style="color:#3fb950;font-size:11px;font-weight:bold">✏️ 写入:</span> ';
w.forEach(wn => {
const desc = wDesc[wn] || '';
html += `<div style="display:inline-block;margin:2px 4px 2px 0">`;
html += `<span class="cron-tag ok" style="font-size:11px">${wn}</span>`;
if (desc) html += `<span style="font-size:10px;color:#8b949e;margin-left:2px">— ${desc}</span>`;
html += `</div>`;
});
html += '</div>';
}
// 读取方(带详细说明)
if (r.length > 0) {
html += '<div>';
html += '<span style="color:#58a6ff;font-size:11px;font-weight:bold">📖 读取:</span> ';
r.forEach(rn => {
const desc = rDesc[rn] || '';
html += `<div style="display:inline-block;margin:2px 4px 2px 0">`;
html += `<span class="cron-tag" style="border-color:#58a6ff;color:#58a6ff;font-size:11px">${rn}</span>`;
if (desc) html += `<span style="font-size:10px;color:#8b949e;margin-left:2px">— ${desc}</span>`;
html += `</div>`;
});
html += '</div>';
}
html += '</div>';
});
// JSON文件
jf.forEach(j => {
const r = j.readers || [];
html += '<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px">';
html += `<div style="font-weight:bold;font-size:14px;color:#d29922;margin-bottom:6px">📄 ${j.name}</div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:6px">${j.desc||''} · ${j.size_kb}KB</div>`;
if (r.length > 0) {
html += '<div>';
html += '<span style="color:#58a6ff;font-size:11px">📖 读取:</span> ';
html += r.map(s => `<span class="cron-tag" style="border-color:#58a6ff;color:#58a6ff;font-size:11px">${s}</span>`).join(' ');
html += '</div>';
}
html += '</div>';
});
return html;
}
function renderPipelineTable(pipelines) {
let html = '<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center">';
html += '<input class="search-box" id="pipeSearch" placeholder="搜索流程..." oninput="filterPipes()" style="flex:1">';
html += '<select id="pipeProfile" onchange="filterPipes()" style="background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:5px 8px;font-size:12px">';
html += '<option value="position-analyst">📋 知微</option>';
html += '<option value="all">📋+📦 全部</option>';
html += '</select></div>';
html += '<table><thead><tr><th>名称</th><th>来源</th><th>脚本/LLM</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody id="pipeBody">';
pipelines.forEach(p => {
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
const statusDisplay = p.last_run ? p.status : '待首次运行';
const profileBadge = p.profile==='position-analyst' ? '📋' : '📦';
html += `<tr class="pipe-row" data-profile="${p.profile||'default'}" data-name="${(p.name||'').toLowerCase()}"><td>${p.name||p.script||'LLM'}</td>`;
html += `<td style="font-size:11px">${profileBadge} ${p.profile||'?'}</td>`;
html += `<td style="font-size:11px">${p.script||'LLM'}</td>`;
html += `<td><span class="cron-tag ${tagCls}">${p.type||'cron'}</span></td>`;
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
html += `<td class="pipeline-${tagCls}">${statusDisplay}</td>`;
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
});
html += '</tbody></table>';
return html;
}
function filterPipes() {
const q = document.getElementById('pipeSearch').value.toLowerCase();
const profile = document.getElementById('pipeProfile').value;
document.querySelectorAll('.pipe-row').forEach(r => {
const name = r.getAttribute('data-name') || '';
const rp = r.getAttribute('data-profile') || 'default';
const matchName = !q || name.includes(q);
const matchProfile = profile === 'all' || rp === profile;
r.style.display = matchName && matchProfile ? '' : 'none';
});
}
function loadData() {
fetch('/mofin_health.json?_='+Date.now())
.then(r=>r.json())
.then(d=>{
data = d;
// 统计
const cnt = {ok:0,warn:0,fail:0};
function countStatus(n) { cnt[n.status]++; if(n.children) n.children.forEach(countStatus); }
countStatus(d.feature_tree);
const pipeOk = d.pipelines.filter(p=>p.status==='ok').length;
const pipeErr = d.pipelines.filter(p=>p.status==='error'||p.status==='fail').length;
document.getElementById('summary').innerHTML =
`<span>生成: ${d.generated_at}</span>` +
`<span>功能: ✅${cnt.ok} ⚠️${cnt.warn}${cnt.fail}</span>` +
`<span>Cron: ✅${pipeOk}${pipeErr}${d.pipelines.length}</span>` +
`<span>数据表: ${d.entities.length} | ` +
`<span style="color:#f85149">孤立${d.entities.filter(e=>e.flow_status==='orphan').length}</span> ` +
`<span style="color:#d29922">写无读${d.entities.filter(e=>e.flow_status==='write_only').length}</span> ` +
`<span style="color:#58a6ff">读无写${d.entities.filter(e=>e.flow_status==='read_only').length}</span>` +
`<span style="color:#d29922;margin-left:8px">架构⚠️${(d.architecture||{}).violation_count||0}</span></span>`;
// Tab 0: 功能树
document.getElementById('panel0').innerHTML = renderFeatureTree(d.feature_tree);
// Tab 1: 全部流程表
document.getElementById('panel1').innerHTML = renderPipelineTable(d.pipelines);
// Tab 2: 数据实体
let h2 = '<table><thead><tr><th>表名</th><th>作用</th><th>行数</th><th>写入方</th><th>读取方</th><th>数据流</th></tr></thead><tbody>';
d.entities.forEach(e => {
const flowMap = {healthy:'✅ 正常', write_only:'✏️ 写无读', read_only:'📖 读无写', orphan:'🔴 孤立'};
const flowColors = {healthy:'#3fb950', write_only:'#d29922', read_only:'#58a6ff', orphan:'#f85149'};
const badge = flowMap[e.flow_status] || '?';
const color = flowColors[e.flow_status] || '#8b949e';
const cls = e.orphan ? 'orphan' : '';
h2 += `<tr class="${cls}"><td><strong>${e.name}</strong></td><td class="desc">${e.desc||''}</td><td>${e.rows}</td>`;
h2 += `<td class="wrap">${(e.writers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td class="wrap">${(e.readers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td style="color:${color}">${badge}</td></tr>`;
});
(d.json_files||[]).forEach(j => {
h2 += `<tr><td>📄 ${j.name}</td><td class="desc">${j.desc||'JSON文件'}</td><td>${j.size_kb}KB</td>`;
h2 += `<td class="wrap">JSON写入</td><td class="wrap">${(j.readers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td>${j.warn?'⚠️':'✅'}</td></tr>`;
});
h2 += '</tbody></table>';
document.getElementById('panel2').innerHTML = h2;
// Tab 3: 数据流
document.getElementById('panel3').innerHTML = renderDataFlow(d.entities, d.json_files||[], d.architecture||{});
// Tab 4: 自检体系
document.getElementById('panel4').innerHTML = renderSelfCheck(d.self_check);
})
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
}
loadData();
setInterval(loadData, 60000);
</script>
</body></html>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>MoFin 健康监控</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui,-apple-system,sans-serif}
body{background:#0d1117;color:#c9d1d9;padding:20px}
h1{font-size:22px;margin-bottom:16px;color:#58a6ff}
.tabs{display:flex;gap:4px;margin-bottom:16px;border-bottom:1px solid #30363d;flex-wrap:wrap}
.tab{padding:8px 20px;cursor:pointer;border:1px solid transparent;border-radius:4px 4px 0 0;color:#8b949e;font-size:14px}
.tab.active{background:#161b22;border-color:#30363d #30363d #161b22;color:#c9d1d9}
.tab:hover{color:#58a6ff}
.panel{display:none}
.panel.active{display:block}
.badge{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px}
.badge.ok{background:#3fb950}
.badge.warn{background:#d29922}
.badge.fail{background:#f85149}
table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:20px}
th,td{padding:5px 8px;text-align:left;border-bottom:1px solid #21262d;white-space:nowrap}
th{background:#161b22;color:#8b949e;font-weight:500;position:sticky;top:0}
tr:hover{background:#1c2128}
td.desc{color:#8b949e;font-size:11px;white-space:normal;max-width:260px}
td.wrap{white-space:normal;font-size:11px;max-width:200px}
.tree-node{padding:2px 0;font-size:13px}
.tree-children{padding-left:22px;border-left:1px solid #30363d;margin-left:8px}
.tree-node.ok{color:#3fb950}
.tree-node.warn{color:#d29922}
.tree-node.fail{color:#f85149}
.pipeline-ok{color:#3fb950}
.pipeline-warn{color:#d29922}
.pipeline-error{color:#f85149}
.pipeline-ever{color:#8b949e}
.orphan{background:#2d1517!important}
.orphan td{color:#f85149}
.summary{display:flex;gap:20px;margin-bottom:16px;font-size:13px;color:#8b949e;flex-wrap:wrap}
.summary span strong{color:#c9d1d9}
.toggle{color:#58a6ff;cursor:pointer;font-size:11px;margin-left:8px}
.search-box{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;padding:6px 12px;border-radius:4px;margin-bottom:10px;width:300px;font-size:13px}
.cron-detail{font-size:11px;color:#8b949e;padding-left:20px}
.cron-tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;margin:0 2px}
.cron-tag.ok{background:#1a3a1a;color:#3fb950;border:1px solid #3fb950}
.cron-tag.warn{background:#3a2a1a;color:#d29922;border:1px solid #d29922}
.cron-tag.error{background:#3a1a1a;color:#f85149;border:1px solid #f85149}
.cron-tag.new{background:#1a1a3a;color:#58a6ff;border:1px solid #58a6ff}
</style></head>
<body>
<h1>📊 MoFin 系统健康监控</h1>
<div class="summary" id="summary">加载中...</div>
<div class="tabs">
<div class="tab active" onclick="switchTab(0)">🌳 功能树</div>
<div class="tab" onclick="switchTab(1)">🔧 全部流程/Cron</div>
<div class="tab" onclick="switchTab(2)">🗃️ 数据实体</div>
<div class="tab" onclick="switchTab(3)">🔀 数据流</div>
<div class="tab" onclick="switchTab(4)">🩺 自检体系</div>
</div>
<div id="panel0" class="panel active"></div>
<div id="panel1" class="panel"></div>
<div id="panel2" class="panel"></div>
<div id="panel3" class="panel"></div>
<div id="panel4" class="panel"></div>
<script>
let data = null;
function renderSelfCheck(sc) {
if (!sc) return '<div style="color:#8b949e">自检体系数据未生成(等待 L1/L4 cron 首次运行)</div>';
let html = '';
const icon = s => s === 'ok' ? '✅' : s === 'warn' ? '🟡' : s === 'fail' ? '❌' : '⏭';
// L4 元监控(自检系统的自检)
if (sc.meta_watchdog) {
const mw = sc.meta_watchdog;
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">🐕 L4 元监控 — 自检系统的自检 <span style="color:${mw.status === 'ok' ? '#3fb950' : '#f85149'}">${icon(mw.status)} ${mw.status}</span> <span style="color:#8b949e;font-size:11px">${mw.generated_at || ''}</span></div>`;
html += '<table><thead><tr><th>层</th><th>状态</th><th>说明</th></tr></thead>';
(mw.layers || []).forEach(l => {
html += `<tr><td>${l.layer}</td><td class="pipeline-${l.status === 'ok' ? 'ok' : 'error'}">${icon(l.status)} ${l.status}</td><td style="font-size:11px;color:#8b949e">${l.reason || ''}</td></tr>`;
});
html += '</table></div>';
}
// L1 功能健康
if (sc.functional) {
const fh = sc.functional;
const s = fh.summary || {};
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">⚙️ L1 功能健康(功能是否达成,非进程存活) <span style="color:${fh.status === 'ok' ? '#3fb950' : fh.status === 'warn' ? '#d29922' : '#f85149'}">${icon(fh.status)} ${fh.status}</span> <span style="color:#8b949e;font-size:11px">ok:${s.ok} warn:${s.warn} fail:${s.fail} skip:${s.skip} · ${fh.generated_at || ''}</span></div>`;
html += '<table><thead><tr><th>模块</th><th>功能判据</th><th>状态</th><th>说明</th></tr></thead>';
(fh.checks || []).forEach(c => {
html += `<tr><td>${c.module}</td><td style="font-size:11px;color:#8b949e">${c.function}</td><td class="pipeline-${c.status === 'ok' ? 'ok' : c.status === 'warn' ? 'warn' : c.status === 'fail' ? 'error' : 'ever'}">${icon(c.status)} ${c.status}</td><td style="font-size:11px;color:#8b949e">${c.reason || ''}</td></tr>`;
});
html += '</table></div>';
}
// L2 卫生审计
if (sc.hygiene) {
const hy = sc.hygiene;
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">🧹 L2 系统卫生审计(每日08:20 <span style="color:${hy.status === 'ok' ? '#3fb950' : '#d29922'}">${icon(hy.status)} ${hy.status}</span> <span style="color:#8b949e;font-size:11px">${hy.issue_count} 个问题 · ${hy.generated_at || ''}</span></div>`;
(hy.issues || []).forEach(i => {
html += `<div style="font-size:11px;color:#d29922">• [${i.type}] ${i.file || i.job || i.table || ''}${i.action || ''}</div>`;
});
html += '</div>';
}
// L3 修复记录
html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:10px">`;
html += `<div style="font-weight:bold;color:#58a6ff;margin-bottom:6px">🔧 L3 LLM 修复循环 — 最近修复动作(报备制)</div>`;
const reps = sc.recent_repairs || [];
if (!reps.length) {
html += '<div style="font-size:11px;color:#8b949e">暂无修复记录(系统健康或 L3 未触发)</div>';
} else {
reps.forEach(r => {
const okIcon = r.ok === true ? '✅' : r.ok === false ? '❌' : '⏭';
html += `<div style="font-size:11px;margin:3px 0"><span style="color:#8b949e">${(r.ts || '').slice(5, 16)}</span> ${okIcon} [${r.module}] ${(r.action || {}).action || r.action}: <span style="color:#8b949e">${(r.detail || r.reason || '').slice(0, 80)}</span></div>`;
});
}
html += '</div>';
return html;
}
function switchTab(idx) {
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',i==idx));
document.querySelectorAll('.panel').forEach((p,i)=>p.classList.toggle('active',i==idx));
}
function renderFeatureTree(tree) {
let grp = (function(){let i=0;return function(){return 'ftg-'+(++i);}})();
let html = '<table><thead><tr><th style="width:32%">功能模块</th><th style="width:35%">说明</th><th>来源</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead>';
function walk(node, depth, groupId) {
const indent = 'padding-left:'+(depth*20+8)+'px';
const cls = node.status || 'ok';
const hasKids = node.children && node.children.length > 0;
const pipes = node.pipes || [];
const isCat = hasKids && pipes.length === 0;
if (isCat) {
// 分类节点——生成一个可折叠组
const g = grp();
const desc = node.desc || '';
html += `<tr id="${g}-h" onclick="toggleFt('${g}')" style="cursor:pointer" data-group="${groupId||''}">`;
html += `<td style="${indent}"><span id="${g}-ico">▼</span> <span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
html += `<td style="font-size:11px;color:#8b949e">${desc}</td>`;
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls}">${cls}</td><td>-</td></tr>`;
// 子节点都带上这个组的data-sub
node.children.forEach(c => walk(c, depth+1, g));
return;
}
if (pipes.length > 0) {
pipes.forEach((p, i) => {
const indent2 = i===0 ? indent : 'padding-left:'+(depth*20+32)+'px';
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
const pBadge = p.profile==='position-analyst' ? '📋' : '📦';
const descText = i===0 ? (node.desc || '') : '';
const groupAttr = groupId ? `data-group="${groupId}"` : '';
if (i===0) {
html += `<tr ${groupAttr}><td style="${indent2}"><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
} else {
html += `<tr ${groupAttr}><td style="${indent2}"><span style="color:#30363d">└</span> ${p.name||p.script||'LLM'}</td>`;
}
html += `<td style="font-size:11px;color:#8b949e">${descText}</td>`;
html += `<td><span class="cron-tag ${tagCls}">${p.type||'LLM'}</span></td>`;
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
html += `<td class="pipeline-${tagCls}">${p.status}</td>`;
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
});
} else {
// 无cron的叶子
const groupAttr = groupId ? `data-group="${groupId}"` : '';
html += `<tr ${groupAttr}><td style="${indent}"><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls}">${cls}</td><td>-</td></tr>`;
}
if (hasKids && pipes.length > 0) {
node.children.forEach(c => walk(c, depth+1, groupId));
}
}
walk(tree, 0, null);
html += '</table>';
return html;
}
// 折叠切换:通过data-group控制
let ftState = {};
function toggleFt(g) {
const ico = document.getElementById(g+'-ico');
const isHidden = ftState[g];
ftState[g] = !isHidden;
ico.textContent = isHidden ? '▼' : '▶';
// 隐藏/显示所有带data-group={g}的行
document.querySelectorAll(`[data-group="${g}"]`).forEach(el => {
el.style.display = isHidden ? '' : 'none';
});
}
function renderDataFlow(entities, jsonFiles, architecture) {
// 只显示有写入方或读取方的实体
const filtered = entities.filter(e => (e.writers||[]).length > 0 || (e.readers||[]).length > 0);
// 只显示有读写关系的json
const jf = jsonFiles.filter(j => (j.readers||[]).length > 0);
let html = '<div style="margin-bottom:10px;color:#8b949e;font-size:13px">';
// 架构违规告警
const violations = (architecture||{}).price_api_violations || [];
if (violations.length > 0) {
const scripts = [...new Set(violations.map(v => v.script))];
html += `<div style="background:#3d1c02;border:1px solid #d29922;border-radius:6px;padding:10px;margin-bottom:12px">`;
html += `<div style="font-weight:bold;color:#d29922;margin-bottom:4px">⚠️ 架构违规: ${violations.length}处, ${scripts.length}个脚本自拉API不走live_prices</div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:6px">预期: price_monitor→live_prices→其他脚本读。以下脚本直接调腾讯API:</div>`;
html += `<div style="font-size:11px">`;
scripts.forEach(s => {
const lines = violations.filter(v => v.script === s).map(v => `L${v.line}`).join(',');
html += `<span class="cron-tag" style="border-color:#d29922;color:#d29922;margin-right:4px;margin-bottom:3px;display:inline-block">${s} (${lines})</span>`;
});
html += `</div></div>`;
}
html += `显示 ${filtered.length} 个数据表 + ${jf.length} 个JSON文件的读写流向。绿色→写入,蓝色→读取。</div>`;
// 按写入方数量排序,最活跃的排前面
filtered.sort((a,b) => (b.writers||[]).length - (a.writers||[]).length);
filtered.forEach(e => {
const w = e.writers || [];
const r = e.readers || [];
if (w.length === 0 && r.length === 0) return;
const fd = e.flow_detail || {};
const wDesc = fd.writers || {};
const rDesc = fd.readers || {};
html += '<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px">';
html += `<div style="font-weight:bold;font-size:14px;color:#58a6ff;margin-bottom:4px">🗄️ ${e.name}</div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:4px">${e.desc||''}</div>`;
// 综合总结
if (fd.summary) {
html += `<div style="font-size:11px;color:#c9d1d9;margin-bottom:6px;padding:4px 8px;background:#0d1117;border-radius:4px">📌 ${fd.summary}</div>`;
}
// 写入方(带详细说明)
if (w.length > 0) {
html += '<div style="margin-bottom:4px">';
html += '<span style="color:#3fb950;font-size:11px;font-weight:bold">✏️ 写入:</span> ';
w.forEach(wn => {
const desc = wDesc[wn] || '';
html += `<div style="display:inline-block;margin:2px 4px 2px 0">`;
html += `<span class="cron-tag ok" style="font-size:11px">${wn}</span>`;
if (desc) html += `<span style="font-size:10px;color:#8b949e;margin-left:2px">— ${desc}</span>`;
html += `</div>`;
});
html += '</div>';
}
// 读取方(带详细说明)
if (r.length > 0) {
html += '<div>';
html += '<span style="color:#58a6ff;font-size:11px;font-weight:bold">📖 读取:</span> ';
r.forEach(rn => {
const desc = rDesc[rn] || '';
html += `<div style="display:inline-block;margin:2px 4px 2px 0">`;
html += `<span class="cron-tag" style="border-color:#58a6ff;color:#58a6ff;font-size:11px">${rn}</span>`;
if (desc) html += `<span style="font-size:10px;color:#8b949e;margin-left:2px">— ${desc}</span>`;
html += `</div>`;
});
html += '</div>';
}
html += '</div>';
});
// JSON文件
jf.forEach(j => {
const r = j.readers || [];
html += '<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px">';
html += `<div style="font-weight:bold;font-size:14px;color:#d29922;margin-bottom:6px">📄 ${j.name}</div>`;
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:6px">${j.desc||''} · ${j.size_kb}KB</div>`;
if (r.length > 0) {
html += '<div>';
html += '<span style="color:#58a6ff;font-size:11px">📖 读取:</span> ';
html += r.map(s => `<span class="cron-tag" style="border-color:#58a6ff;color:#58a6ff;font-size:11px">${s}</span>`).join(' ');
html += '</div>';
}
html += '</div>';
});
return html;
}
function renderPipelineTable(pipelines) {
let html = '<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center">';
html += '<input class="search-box" id="pipeSearch" placeholder="搜索流程..." oninput="filterPipes()" style="flex:1">';
html += '<select id="pipeProfile" onchange="filterPipes()" style="background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:5px 8px;font-size:12px">';
html += '<option value="position-analyst">📋 知微</option>';
html += '<option value="all">📋+📦 全部</option>';
html += '</select></div>';
html += '<table><thead><tr><th>名称</th><th>来源</th><th>脚本/LLM</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody id="pipeBody">';
pipelines.forEach(p => {
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
const statusDisplay = p.last_run ? p.status : '待首次运行';
const profileBadge = p.profile==='position-analyst' ? '📋' : '📦';
html += `<tr class="pipe-row" data-profile="${p.profile||'default'}" data-name="${(p.name||'').toLowerCase()}"><td>${p.name||p.script||'LLM'}</td>`;
html += `<td style="font-size:11px">${profileBadge} ${p.profile||'?'}</td>`;
html += `<td style="font-size:11px">${p.script||'LLM'}</td>`;
html += `<td><span class="cron-tag ${tagCls}">${p.type||'cron'}</span></td>`;
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
html += `<td class="pipeline-${tagCls}">${statusDisplay}</td>`;
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
});
html += '</tbody></table>';
return html;
}
function filterPipes() {
const q = document.getElementById('pipeSearch').value.toLowerCase();
const profile = document.getElementById('pipeProfile').value;
document.querySelectorAll('.pipe-row').forEach(r => {
const name = r.getAttribute('data-name') || '';
const rp = r.getAttribute('data-profile') || 'default';
const matchName = !q || name.includes(q);
const matchProfile = profile === 'all' || rp === profile;
r.style.display = matchName && matchProfile ? '' : 'none';
});
}
function loadData() {
fetch('/mofin_health.json?_='+Date.now())
.then(r=>r.json())
.then(d=>{
data = d;
// 统计
const cnt = {ok:0,warn:0,fail:0};
function countStatus(n) { cnt[n.status]++; if(n.children) n.children.forEach(countStatus); }
countStatus(d.feature_tree);
const pipeOk = d.pipelines.filter(p=>p.status==='ok').length;
const pipeErr = d.pipelines.filter(p=>p.status==='error'||p.status==='fail').length;
document.getElementById('summary').innerHTML =
`<span>生成: ${d.generated_at}</span>` +
`<span>功能: ✅${cnt.ok} ⚠️${cnt.warn}${cnt.fail}</span>` +
`<span>Cron: ✅${pipeOk}${pipeErr}${d.pipelines.length}</span>` +
`<span>数据表: ${d.entities.length} | ` +
`<span style="color:#f85149">孤立${d.entities.filter(e=>e.flow_status==='orphan').length}</span> ` +
`<span style="color:#d29922">写无读${d.entities.filter(e=>e.flow_status==='write_only').length}</span> ` +
`<span style="color:#58a6ff">读无写${d.entities.filter(e=>e.flow_status==='read_only').length}</span>` +
`<span style="color:#d29922;margin-left:8px">架构⚠️${(d.architecture||{}).violation_count||0}</span></span>`;
// Tab 0: 功能树
document.getElementById('panel0').innerHTML = renderFeatureTree(d.feature_tree);
// Tab 1: 全部流程表
document.getElementById('panel1').innerHTML = renderPipelineTable(d.pipelines);
// Tab 2: 数据实体
let h2 = '<table><thead><tr><th>表名</th><th>作用</th><th>行数</th><th>写入方</th><th>读取方</th><th>数据流</th></tr></thead><tbody>';
d.entities.forEach(e => {
const flowMap = {healthy:'✅ 正常', write_only:'✏️ 写无读', read_only:'📖 读无写', orphan:'🔴 孤立'};
const flowColors = {healthy:'#3fb950', write_only:'#d29922', read_only:'#58a6ff', orphan:'#f85149'};
const badge = flowMap[e.flow_status] || '?';
const color = flowColors[e.flow_status] || '#8b949e';
const cls = e.orphan ? 'orphan' : '';
h2 += `<tr class="${cls}"><td><strong>${e.name}</strong></td><td class="desc">${e.desc||''}</td><td>${e.rows}</td>`;
h2 += `<td class="wrap">${(e.writers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td class="wrap">${(e.readers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td style="color:${color}">${badge}</td></tr>`;
});
(d.json_files||[]).forEach(j => {
h2 += `<tr><td>📄 ${j.name}</td><td class="desc">${j.desc||'JSON文件'}</td><td>${j.size_kb}KB</td>`;
h2 += `<td class="wrap">JSON写入</td><td class="wrap">${(j.readers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td>${j.warn?'⚠️':'✅'}</td></tr>`;
});
h2 += '</tbody></table>';
document.getElementById('panel2').innerHTML = h2;
// Tab 3: 数据流
document.getElementById('panel3').innerHTML = renderDataFlow(d.entities, d.json_files||[], d.architecture||{});
// Tab 4: 自检体系
document.getElementById('panel4').innerHTML = renderSelfCheck(d.self_check);
})
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
}
loadData();
setInterval(loadData, 60000);
</script>
</body></html>