fix: 功能树每个节点嵌入cron管道信息,修复数据采集节点不显示cron的问题
This commit is contained in:
+153
-58
@@ -129,70 +129,165 @@ def check_scripts():
|
||||
issues[py.stem] = "ok" if r == 0 else "syntax_error"
|
||||
return issues
|
||||
|
||||
|
||||
def match_cron(cron_jobs, name_keywords):
|
||||
"""匹配cron任务列表,返回匹配的cron信息列表(空格归一化后匹配)"""
|
||||
matches = []
|
||||
for j in cron_jobs:
|
||||
jname = j.get("name", "").replace(" ", "").replace("\u00a0", "") # 去空格再比
|
||||
if isinstance(name_keywords, str):
|
||||
if name_keywords.replace(" ", "") in jname:
|
||||
matches.append(j)
|
||||
elif isinstance(name_keywords, (list, tuple)):
|
||||
clean_kws = [k.replace(" ", "").replace("\u00a0", "") for k in name_keywords]
|
||||
if any(kw in jname for kw in clean_kws):
|
||||
matches.append(j)
|
||||
elif callable(name_keywords):
|
||||
if name_keywords(j):
|
||||
matches.append(j)
|
||||
# 去重(相同name只保留一条)
|
||||
seen = set()
|
||||
deduped = []
|
||||
for j in matches:
|
||||
n = j.get("name", "")
|
||||
if n not in seen:
|
||||
seen.add(n)
|
||||
deduped.append(j)
|
||||
return deduped
|
||||
|
||||
def build_feature_tree(cron_jobs, db_stats):
|
||||
# 匹配规则:每个节点名称→匹配的cron名称关键字
|
||||
rules = {
|
||||
"市场快照 (market_watch.py)": ["市场数据采集"],
|
||||
"宏观新闻 (macro_context_collector.py)": ["宏观采集"],
|
||||
"价格监控 (price_monitor.py)": ["价格监控"],
|
||||
"小果扫描 (xiaoguo_scanner.py)": ["小果独立扫描"],
|
||||
"资金流采集 (capital_flow_collector.py)": ["资金流"],
|
||||
"宏观上下文刷新 (refresh_macro_context.py)": ["宏观上下文刷新"],
|
||||
"策略重评 (mofin_collect→reassess_with_context)": ["策略重评"],
|
||||
"持仓自选新鲜度检查": ["策略时效性检查"],
|
||||
"自选买入区提醒 (stale_push_wlin.py)": ["自选买入区提醒"],
|
||||
"策略评估 (strategy_evaluator.py)": ["策略评估"],
|
||||
"分支自成长 (branch_scanner.py)": ["分支自成长"],
|
||||
"元自成长 (meta_growth.py)": ["元自成长"],
|
||||
"MoFin盘前中监控 (LLM cron)": ["MoFin盘前中监控"],
|
||||
"MoFin午后监控 (LLM cron)": ["MoFin午后监控"],
|
||||
"cron报告推XMPP (cron_to_xmpp.py)": ["cron报告推XMPP"],
|
||||
"开盘简报 (LLM cron)": ["开盘简报"],
|
||||
"收盘简报 (LLM cron)": ["收盘简报"],
|
||||
"市场精选推荐 (LLM cron)": ["市场精选推荐"],
|
||||
"小果情感分析 (LLM cron)": ["小果情感分析"],
|
||||
"系统全局审计": ["系统全局审计"],
|
||||
"全局cron健康监控": ["全局cron健康监控"],
|
||||
"重评管道审计": ["重评管道审计"],
|
||||
"健康监控数据采集": ["健康监控数据采集"],
|
||||
"持仓基本面复查": ["分析师-持仓复查"],
|
||||
"策略复牌": ["策略复盘"],
|
||||
"宏观风险扫描 (LLM cron)": ["宏观风险扫描"],
|
||||
"宏观风险信号消费 (macro_signal_consumer.py)": ["宏观风险信号消费"],
|
||||
"跨市场背离检测 (divergence_detector.py)": ["跨市场背离检测"],
|
||||
"自愈执行器 (self_todo_executor.py)": ["自愈执行器"],
|
||||
"策略质量门禁 (review_needed_watchdog.py)": ["策略质量门禁"],
|
||||
"自选自动清理 (clean_watchlist.py)": ["自选自动清理"],
|
||||
"建议对账 (advice_reconciliation.py)": ["建议对账"],
|
||||
"XMPP Bot (zhiwei)": None,
|
||||
"Gateway (8643)": None,
|
||||
"HTTP Bridge (5805)": None,
|
||||
"Dashboard (8899)": None,
|
||||
"state.db SQLite": None,
|
||||
}
|
||||
|
||||
def attach_pipes(node):
|
||||
label = node.get("label", "")
|
||||
keywords = rules.get(label)
|
||||
if keywords is not None:
|
||||
matched = match_cron(cron_jobs, keywords)
|
||||
node["pipes"] = [{
|
||||
"name": j.get("name", ""),
|
||||
"script": j.get("script", ""),
|
||||
"schedule": j.get("schedule", ""),
|
||||
"status": j.get("last_status", "unknown"),
|
||||
"last_run": (j.get("last_run_at", "") or "")[:16] if j.get("last_run_at") else "",
|
||||
"type": "no_agent" if j.get("no_agent") else "LLM",
|
||||
} for j in matched]
|
||||
if node.get("children"):
|
||||
for c in node["children"]:
|
||||
attach_pipes(c)
|
||||
|
||||
tree = {
|
||||
"label": "MoFin 系统",
|
||||
"status": "ok",
|
||||
"children": [
|
||||
{"label": "数据采集", "status": "ok", "children": [
|
||||
{"label": "市场快照 (market_watch.py)", "status": "ok" if any(j.get("name")=="市场数据采集" and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "宏观新闻 (macro_context_collector.py)", "status": "ok" if any("宏观采集" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "价格监控 (price_monitor.py)", "status": "ok" if any("价格监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "小果扫描 (xiaoguo_scanner.py)", "status": "ok" if any("小果独立扫描" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "资金流采集 (capital_flow_collector.py)", "status": "ok" if any("资金流" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "宏观上下文刷新 (refresh_macro_context.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "策略分析", "status": "ok", "children": [
|
||||
{"label": "策略重评 (mofin_collect→reassess_with_context)", "status": "ok" if db_stats.get("strategy_evaluations",0) > 200 else "warn"},
|
||||
{"label": "持仓自选新鲜度检查", "status": "ok"},
|
||||
{"label": "自选买入区提醒 (stale_push_wlin.py)", "status": "ok" if any("自选买入区提醒" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "策略评估 (strategy_evaluator.py)", "status": "ok" if any("策略评估" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "分支自成长 (branch_scanner.py)", "status": "ok"},
|
||||
{"label": "元自成长 (meta_growth.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "推荐推送", "status": "ok", "children": [
|
||||
{"label": "MoFin盘前中监控 (LLM cron)", "status": "ok" if any("MoFin盘前中监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "MoFin午后监控 (LLM cron)", "status": "ok" if any("MoFin午后监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "cron报告推XMPP (cron_to_xmpp.py)", "status": "ok"},
|
||||
{"label": "开盘简报 (LLM cron)", "status": "ok"},
|
||||
{"label": "收盘简报 (LLM cron)", "status": "ok"},
|
||||
{"label": "市场精选推荐 (LLM cron)", "status": "ok" if any("市场精选推荐" in j.get("name","") and j.get("enabled")==True for j in cron_jobs) else "warn"},
|
||||
]},
|
||||
{"label": "自检/审计", "status": "ok", "children": [
|
||||
{"label": "系统全局审计", "status": "ok"},
|
||||
{"label": "全局cron健康监控", "status": "ok"},
|
||||
{"label": "重评管道审计", "status": "ok"},
|
||||
{"label": "健康监控数据采集", "status": "ok"},
|
||||
]},
|
||||
{"label": "持仓复查", "status": "ok", "children": [
|
||||
{"label": "持仓基本面复查", "status": "ok"},
|
||||
{"label": "策略复牌", "status": "ok"},
|
||||
]},
|
||||
{"label": "其他", "status": "ok", "children": [
|
||||
{"label": "小果情感分析 (LLM cron)", "status": "ok"},
|
||||
]},
|
||||
]
|
||||
+ [
|
||||
{"label": "风险监控", "status": "ok", "children": [
|
||||
{"label": "宏观风险扫描 (LLM cron)", "status": "ok"},
|
||||
{"label": "宏观风险信号消费 (macro_signal_consumer.py)", "status": "ok" if any("宏观风险信号消费" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": "跨市场背离检测 (divergence_detector.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "执行/修复", "status": "ok", "children": [
|
||||
{"label": "自愈执行器 (self_todo_executor.py)", "status": "ok"},
|
||||
{"label": "策略质量门禁 (review_needed_watchdog.py)", "status": "ok"},
|
||||
{"label": "自选自动清理 (clean_watchlist.py)", "status": "warn" if any("自选自动清理" in j.get("name","") and j.get("last_status")=="error" for j in cron_jobs) else "ok"},
|
||||
{"label": "建议对账 (advice_reconciliation.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "系统服务", "status": "ok", "children": [
|
||||
{"label": "XMPP Bot (zhiwei)", "status": "ok" if os.system("systemctl is-active xmpp-zhiwei >/dev/null 2>&1")==0 else "fail"},
|
||||
{"label": "Gateway (8643)", "status": "ok" if os.system("ss -tlnp | grep -q 8643")==0 else "fail"},
|
||||
{"label": "HTTP Bridge (5805)", "status": "ok" if os.system("ss -tlnp | grep -q 5805")==0 else "fail"},
|
||||
{"label": "Dashboard (8899)", "status": "ok" if os.system("ss -tlnp | grep -q 8899")==0 else "warn"},
|
||||
{"label": "state.db SQLite", "status": "ok"},
|
||||
]},
|
||||
],
|
||||
}
|
||||
attach_pipes(tree)
|
||||
return tree
|
||||
|
||||
def build_report():
|
||||
cron_jobs = load_cron_jobs()
|
||||
db_stats = get_db_stats()
|
||||
flows = scan_data_flows()
|
||||
script_health = check_scripts()
|
||||
|
||||
# ── Tab 1: 功能树 ──
|
||||
feature_tree = {
|
||||
"label": "MoFin 系统",
|
||||
"status": "ok",
|
||||
"children": [
|
||||
{"label": "数据采集", "status": "ok", "children": [
|
||||
{"label": f"市场快照 (market_watch.py)", "status": "ok" if any(j.get("name")=="市场数据采集" and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"宏观新闻 (macro_context_collector.py)", "status": "ok" if any("宏观采集" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"价格监控 (price_monitor.py)", "status": "ok" if any("价格监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"小果扫描 (xiaoguo_scanner.py)", "status": "ok" if any("小果独立扫描" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"资金流采集 (capital_flow_collector.py)", "status": "ok" if any("资金流" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"宏观上下文刷新 (refresh_macro_context.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "策略分析", "status": "ok", "children": [
|
||||
{"label": f"策略重评 (mofin_collect→reassess_with_context)", "status": "ok" if db_stats.get("strategy_evaluations",0) > 200 else "warn"},
|
||||
{"label": f"持仓自选新鲜度检查", "status": "ok"},
|
||||
{"label": f"自选买入区提醒 (stale_push_wlin.py)", "status": "ok" if any("自选买入区提醒" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"策略评估 (strategy_evaluator.py)", "status": "ok" if any("策略评估" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"分支自成长 (branch_scanner.py)", "status": "ok"},
|
||||
{"label": f"元自成长 (meta_growth.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "推荐推送", "status": "ok", "children": [
|
||||
{"label": f"MoFin盘前中监控 (LLM cron)", "status": "ok" if any("MoFin盘前中监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"MoFin午后监控 (LLM cron)", "status": "ok" if any("MoFin午后监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"cron报告推XMPP (cron_to_xmpp.py)", "status": "ok"},
|
||||
{"label": f"开盘简报 (LLM cron)", "status": "ok"},
|
||||
{"label": f"收盘简报 (LLM cron)", "status": "ok"},
|
||||
{"label": f"市场精选推荐 (LLM cron)", "status": "ok" if any("市场精选推荐" in j.get("name","") and j.get("enabled")==True for j in cron_jobs) else "warn"},
|
||||
]},
|
||||
{"label": "自检/审计", "status": "ok", "children": [
|
||||
{"label": f"系统全局审计 (system_audit.py)", "status": "ok"},
|
||||
{"label": f"全局cron健康监控 (cron_health_monitor.py)", "status": "ok"},
|
||||
{"label": f"重评管道审计 (verify_reassess_pipeline.py)", "status": "ok"},
|
||||
{"label": f"系统体检 (morning_health_check.py)", "status": "ok"},
|
||||
{"label": f"盘中自检 (intraday_health_check.py)", "status": "ok"},
|
||||
{"label": f"记忆守卫 (memory_guardian.py)", "status": "ok"},
|
||||
{"label": f"硬编码扫描 (hardcode_scanner.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "风险监控", "status": "ok", "children": [
|
||||
{"label": f"宏观风险扫描 (LLM cron)", "status": "ok"},
|
||||
{"label": f"宏观风险信号消费 (macro_signal_consumer.py)", "status": "ok" if any("宏观风险信号消费" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"},
|
||||
{"label": f"跨市场背离检测 (divergence_detector.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "执行/修复", "status": "ok", "children": [
|
||||
{"label": f"自愈执行器 (self_todo_executor.py)", "status": "ok"},
|
||||
{"label": f"策略质量门禁 (review_needed_watchdog.py)", "status": "ok"},
|
||||
{"label": f"自选自动清理 (clean_watchlist.py)", "status": "warn" if any("自选自动清理" in j.get("name","") and j.get("last_status")=="error" for j in cron_jobs) else "ok"},
|
||||
{"label": f"建议对账 (advice_reconciliation.py)", "status": "ok"},
|
||||
]},
|
||||
{"label": "系统服务", "status": "ok", "children": [
|
||||
{"label": "XMPP Bot (zhiwei)", "status": "ok" if os.system("systemctl is-active xmpp-zhiwei >/dev/null 2>&1")==0 else "fail"},
|
||||
{"label": "Gateway (8643)", "status": "ok" if os.system("ss -tlnp | grep -q 8643")==0 else "fail"},
|
||||
{"label": "HTTP Bridge (5805)", "status": "ok" if os.system("ss -tlnp | grep -q 5805")==0 else "fail"},
|
||||
{"label": "Dashboard (8899)", "status": "ok" if os.system("ss -tlnp | grep -q 8899")==0 else "warn"},
|
||||
{"label": "state.db SQLite", "status": "ok"},
|
||||
]},
|
||||
]
|
||||
}
|
||||
# ── 功能树 ──
|
||||
feature_tree = build_feature_tree(cron_jobs, db_stats)
|
||||
# 递归计算节点状态
|
||||
def calc_status(node):
|
||||
if "children" in node:
|
||||
|
||||
+129
-52
@@ -5,7 +5,7 @@
|
||||
*{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}
|
||||
.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}
|
||||
@@ -22,27 +22,39 @@ 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:5px}
|
||||
.tree-label{display:flex;align-items:center;gap:6px;cursor:default}
|
||||
.tree-label .name{color:#c9d1d9}
|
||||
.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}
|
||||
.pipeline-ok{color:#3fb950}
|
||||
.pipeline-error{color:#f85149}
|
||||
.pipeline-unknown{color:#8b949e}
|
||||
.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)">🗃️ 数据实体</div>
|
||||
<div class="tab active" onclick="switchTab(0)">🌳 功能树</div>
|
||||
<div class="tab" onclick="switchTab(1)">🔧 全部流程/Cron (77)</div>
|
||||
<div class="tab" onclick="switchTab(2)">🗃️ 数据实体</div>
|
||||
</div>
|
||||
|
||||
<div id="panel0" class="panel active"></div>
|
||||
<div id="panel1" class="panel"></div>
|
||||
<div id="panel2" class="panel"></div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
@@ -52,36 +64,66 @@ function switchTab(idx) {
|
||||
document.querySelectorAll('.panel').forEach((p,i)=>p.classList.toggle('active',i==idx));
|
||||
}
|
||||
|
||||
function mergeView(featureTree, pipelines) {
|
||||
const pipeMap = {};
|
||||
pipelines.forEach(p => {
|
||||
const key = (p.name || '').toLowerCase();
|
||||
pipeMap[key] = p;
|
||||
});
|
||||
|
||||
const html = ['<table><thead><tr><th>功能</th><th>Cron/流程</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody>'];
|
||||
|
||||
function renderFeatureTree(tree) {
|
||||
let html = '<table><thead><tr><th style="width:50%">功能模块</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody>';
|
||||
function walk(node, depth) {
|
||||
const indent = 'style="padding-left:' + (depth*20+8) + 'px"';
|
||||
const pipe = pipeMap[(node.label || '').toLowerCase().split('(')[0].trim()]
|
||||
|| pipeMap[(node.label || '').toLowerCase().split(' ')[0]];
|
||||
|
||||
if (pipe) {
|
||||
const cls = pipe.status=='ok' ? 'pipeline-ok' : pipe.status=='error' ? 'pipeline-error' : '';
|
||||
const badge = pipe.status=='ok' ? '✅' : pipe.status=='error' ? '❌' : '❓';
|
||||
html.push(`<tr><td ${indent}><span class="badge ${node.status}"></span>${node.label}</td>`);
|
||||
html.push(`<td>${pipe.script||'LLM'}</td><td>${pipe.type}</td><td style="font-size:11px">${pipe.schedule||''}</td>`);
|
||||
html.push(`<td class="${cls}">${badge} ${pipe.status}</td><td style="font-size:11px">${pipe.last_run||'-'}</td></tr>`);
|
||||
const indent = 'style="padding-left:'+(depth*20+8)+'px"';
|
||||
const cls = node.status || 'ok';
|
||||
|
||||
// 尝试匹配cron
|
||||
const pipeKey = (node.label||'').toLowerCase().split('(')[0].trim();
|
||||
const pipes = node._pipes || [];
|
||||
|
||||
if (pipes.length === 0) {
|
||||
// 无对应cron的功能节点
|
||||
html += `<tr><td ${indent}><span class="badge ${cls}"></span><strong>${node.label}</strong><span class="cron-detail">(纯逻辑节点)</span></td>`;
|
||||
html += `<td>-</td><td>-</td><td class="pipeline-${cls==='ok'?'ok':'warn'}">${cls}</td><td>-</td></tr>`;
|
||||
} else {
|
||||
html.push(`<tr><td ${indent} colspan="5"><span class="badge ${node.status}"></span>${node.label}</td></tr>`);
|
||||
// 有对应cron的节点
|
||||
pipes.forEach((p, i) => {
|
||||
const indent2 = i===0 ? indent : 'style="padding-left:'+(depth*20+32)+'px"';
|
||||
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
|
||||
if (i===0) {
|
||||
html += `<tr><td ${indent2}><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
|
||||
} else {
|
||||
html += `<tr><td ${indent2}><span style="color:#30363d">└</span> ${p.name||p.script||'LLM'}</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>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (node.children) node.children.forEach(c => walk(c, depth+1));
|
||||
}
|
||||
walk(tree, 0);
|
||||
html += '</tbody></table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
walk(featureTree, 0);
|
||||
html.push('</tbody></table>');
|
||||
return html.join('');
|
||||
function renderPipelineTable(pipelines) {
|
||||
let html = '<input class="search-box" id="pipeSearch" placeholder="搜索流程..." oninput="filterPipes()">';
|
||||
html += '<table><thead><tr><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 : '待首次运行';
|
||||
html += `<tr class="pipe-row"><td>${p.name||p.script||'LLM'}</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();
|
||||
document.querySelectorAll('.pipe-row').forEach(r => {
|
||||
r.style.display = r.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
@@ -89,35 +131,70 @@ function loadData() {
|
||||
.then(r=>r.json())
|
||||
.then(d=>{
|
||||
data = d;
|
||||
// 把pipeline关联到feature tree节点
|
||||
const pipeMap = {};
|
||||
d.pipelines.forEach(p => {
|
||||
const key = (p.name||p.script||'').toLowerCase().split('.')[0].trim();
|
||||
const label = (p.name||p.script||'').toLowerCase();
|
||||
pipeMap[key] = pipeMap[key] || [];
|
||||
pipeMap[key].push(p);
|
||||
// 也存label搜索
|
||||
pipeMap[label] = pipeMap[label] || [];
|
||||
pipeMap[label].push(p);
|
||||
});
|
||||
|
||||
// 给feature tree节点挂_pipes
|
||||
function attachPipes(node) {
|
||||
const key = (node.label||'').toLowerCase().split('(')[0].trim();
|
||||
node._pipes = pipeMap[key] || [];
|
||||
// 尝试用脚本名匹配
|
||||
if (!node._pipes.length) {
|
||||
const scriptMatch = (node.label||'').match(/\(([^)]+)\)/);
|
||||
if (scriptMatch) {
|
||||
const s = scriptMatch[1].toLowerCase().split('.')[0];
|
||||
node._pipes = pipeMap[s] || [];
|
||||
}
|
||||
}
|
||||
if (node.children) node.children.forEach(c => attachPipes(c));
|
||||
}
|
||||
attachPipes(d.feature_tree);
|
||||
|
||||
// 统计
|
||||
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>数据表: ${d.entities.length} | 孤立: <strong style="color:#f85149">${d.entities.filter(e=>e.orphan).length}</strong></span>` +
|
||||
`<span>Cron: ${d.pipelines.length}</span>`;
|
||||
|
||||
// Tab 0: Merged feature + pipeline
|
||||
document.getElementById('panel0').innerHTML = mergeView(d.feature_tree, d.pipelines);
|
||||
|
||||
// Tab 1: Data entities with descriptions
|
||||
let h1 = '<table><thead><tr><th>表名</th><th>作用</th><th>行数</th><th>写入方</th><th>读取方</th><th>状态</th></tr></thead><tbody>';
|
||||
`<span>功能: ✅${cnt.ok} ⚠️${cnt.warn} ❌${cnt.fail}</span>` +
|
||||
`<span>Cron: ✅${pipeOk} ❌${pipeErr} 共${d.pipelines.length}</span>` +
|
||||
`<span>数据表: ${d.entities.length} | 孤立: <strong style="color:#f85149">${d.entities.filter(e=>e.orphan).length}</strong></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 cls = e.orphan ? 'orphan' : '';
|
||||
const badge = e.orphan ? '🔴 孤立' : (!e.has_input||!e.has_output) ? '⚠️ 单向' : '✅';
|
||||
h1 += `<tr class="${cls}"><td><strong>${e.name}</strong></td><td class="desc">${e.desc||''}</td><td>${e.rows}</td>`;
|
||||
h1 += `<td class="wrap">${e.writers.slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
|
||||
h1 += `<td class="wrap">${e.readers.slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
|
||||
h1 += `<td>${badge}</td></tr>`;
|
||||
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>${badge}</td></tr>`;
|
||||
});
|
||||
d.json_files.forEach(j => {
|
||||
h1 += `<tr class="${j.warn?'warn-row':''}"><td>📄 ${j.name}</td><td class="desc">${j.desc||'JSON文件'}</td><td>${j.size_kb}KB</td>`;
|
||||
h1 += `<td class="wrap">JSON写入</td><td class="wrap">${j.readers.slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
|
||||
h1 += `<td>${j.warn?'⚠️':'✅'}</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>`;
|
||||
});
|
||||
h1 += '</tbody></table>';
|
||||
document.getElementById('panel1').innerHTML = h1;
|
||||
h2 += '</tbody></table>';
|
||||
document.getElementById('panel2').innerHTML = h2;
|
||||
})
|
||||
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
|
||||
}
|
||||
|
||||
+769
-236
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user