fix: F Tab unique keys per instance + spec buttons + dedup + naming rules

This commit is contained in:
hmo
2026-07-18 13:23:43 +08:00
parent c43486a6f2
commit 0ac756b241
3 changed files with 28 additions and 13 deletions
+3
View File
@@ -141,6 +141,9 @@ H: 需求文档 — 双轨体系覆盖不到的架构级/跨模块需求(性
- **监控数据**Tier15min)/ Tier2(日报)作为实时状态输入
- **服务拓扑**:所有服务的健康、端口、看门狗状态
- **跨平台检测**:远程服务(非本机)标记为 `remote(见平台Tab`,避免误报
- **命名唯一性**:F Tab 中的每条服务必须有唯一的 `key`(实例级,如 `agent-001:xmpp_bot`),禁止按类型折叠导致多条同名。M 映射表必须逐条定义友好标签,不可复用同一条目
- **跨 Tab 一致性**:同一模块在 F Tab、I Tab、K Tab 中的展示名称必须一致。例如 `article_processor` 在 F Tab 叫"文章抓取服务",在 I Tab 也应叫"文章抓取服务",不能一处叫"微信全文抓取"
- **?§ 覆盖**:F Tab 中的每条服务必须有对应的 ?human_help)和 §(ai_spec)按钮,链接到 `specs/{spec_module}.json`。没有 spec 的服务不应出现在 F Tab 中
### H — 需求文档(Dashboard H Tab
+5 -4
View File
@@ -975,9 +975,10 @@ def api_expected():
if bot_type == "hermes" and svc_type == "xmpp_bot":
continue
port = svc.get("port")
# key 供前端 M 映射表查找友好标签
key = svc_type
entry = {"name": f"{display}({host}):{svc_type}", "key": key, "host": host, "expected": "running", "critical": True}
# key 供前端 M 映射表查找唯一标签(实例级,不按类型折叠)
key = f"{agent['id']}:{svc_type}"
spec = svc_type # spec 模块名,供 ?§ 按钮链接 specs/{module}.json
entry = {"name": f"{display}({host}):{svc_type}", "key": key, "spec": spec, "host": host, "expected": "running", "critical": True}
if port:
entry["port"] = port
entry["check"] = "tcp"
@@ -990,7 +991,7 @@ def api_expected():
# 2. 从 PLATFORM_SERVICES 生成平台服务的期望
for ps in PLATFORM_SERVICES:
entry = {"name": f"{ps['name']} ({ps['id']})", "key": ps["id"], "host": ps.get("host", "127.0.0.1"), "expected": "running", "critical": True}
entry = {"name": f"{ps['name']} ({ps['id']})", "key": ps["id"], "spec": ps["id"], "host": ps.get("host", "127.0.0.1"), "expected": "running", "critical": True}
if ps.get("health_url"):
entry["health_url"] = ps["health_url"]
entry["check"] = "health_url"
+20 -9
View File
@@ -158,14 +158,21 @@ async function fL(id){var el=qs('lc-'+id);if(!el||!L.has(id))return;try{var r=aw
async function fHealth(){try{
// Helpers
function isHealthy(st){return st==='running'||st==='online'||(st&&st.indexOf('remote')===0);}
// Service metadata: label / layer / impact
// Service metadata: label / layer / impact (per-instance key → metadata)
var M={};
M['agent-001:xmpp_bot']={n:'研发助理-XMPP Bot',l:'通信层',i:'小小莫(WindowsXMPP 消息收发中断',spec:'xmpp_bot'};
M['agent-002:hermes_gateway']={n:'Hermes网关-莫荷',l:'AI网关',i:'莫荷智能体(246 Linux)不可用',spec:'hermes_gateway'};
M['agent-003:xmpp_bot']={n:'本地推理-XMPP Bot',l:'通信层',i:'小果(MacXMPP 消息收发中断',spec:'xmpp_bot'};
M['agent-003:omlx_server']={n:'OMLX推理服务(小果)',l:'AI网关',i:'小果(Mac)本地推理服务不可用',spec:'omlx_server'};
M['agent-004:hermes_gateway']={n:'Hermes网关-知微',l:'AI网关',i:'知微智能体(246 Linux)不可用',spec:'hermes_gateway'};
M.wechat_bridge={n:'莫荷微信桥接',l:'通信层',i:'微信消息收发中断',spec:'wechat_bridge'};
M.article_processor={n:'文章抓取服务',l:'辅助服务',i:'微信长文章无法读取',spec:'article_processor'};
// 兼容层:/api/services 返回的旧名称仍然映射到友好标签
M.xmpp_bot={n:'XMPP消息收发',l:'通信层',i:'消息收发中断'};
M.hermes_gateway={n:'Hermes网关',l:'AI网关',i:'Hermes Agent不可用'};
M.ejabberd={n:'XMPP服务器',l:'通信层',i:'所有XMPP连接不可用'};
M.xmpp_bot={n:'XMPP消息收发',l:'通信层',i:'所有Agent消息收发中断'};
M.hermes_gateway={n:'Hermes AI网关',l:'AI网关',i:'Hermes Agent不可用'};
M.article_processor={n:'微信全文抓取',l:'辅助服务',i:'微信长文章无法读取'};
M.wechat_bridge={n:'莫荷微信桥接',l:'通信层',i:'微信消息收发中断'};
M.omlx_server={n:'OMLX推理服务',l:'AI网关',i:'本地推理服务不可用'};
M.gateway_mohe={n:'Gateway-莫荷',l:'AI网关',i:'莫荷智能体不可用'};
M.gateway_zhiwei={n:'Gateway-知微',l:'AI网关',i:'知微智能体不可用'};
M.dashboard={n:'管理门户',l:'辅助服务',i:'Dashboard无法访问'};
M.watchdog={n:'进程看门狗',l:'辅助服务',i:'异常进程无自动恢复'};
M['agents-health-check']={n:'健康检查(5min)',l:'定时任务',i:'5分钟级健康检测未运行'};
@@ -217,10 +224,12 @@ async function fHealth(){try{
var layers={};
var added={};
for(var i=0;i<exp.length;i++){var e=exp[i];
var ma=M[e.key||e.name]||{n:e.name,l:'辅助服务',i:''};
var ma=M[e.key||e.name]||{n:e.name,l:'辅助服务',i:'',spec:''};
var ly=ma.l||'其他';
if(!layers[ly])layers[ly]=[];added[e.name]=true;
layers[ly].push({name:e.name,label:ma.n,port:e.port,status:isHealthy(act[e.name])?'running':act[e.name],critical:e.critical});}
if(!layers[ly])layers[ly]=[];
added[e.name]=true;
if(e.key) added[e.key]=true; // 同时标记 key,防止 services 同名重复
layers[ly].push({name:e.name,label:ma.n,port:e.port,status:isHealthy(act[e.name])?'running':act[e.name],critical:e.critical,spec:ma.spec||e.spec||''});}
for(var i=0;i<svcs.length;i++){var s=svcs[i];
if(!added[s.name]){
var rawL=(M[s.name]||{}).l||(s.type==='infra'?'通信层':s.type==='gateway'?'AI网关':'辅助服务');
@@ -239,6 +248,8 @@ async function fHealth(){try{
h+='<span class="sd '+(run?'g':'r')+'"></span>';
h+='<span style=flex:1>'+esc(it.label)+'</span>';
if(it.port)h+='<span style=color:var(--dim);font-size:18px>:'+it.port+'</span>';
if(it.spec){h+='<span class="help-btn" onclick="showModuleHelp(\''+it.spec+'\',\'human\')" title="使用说明">?</span>';
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+it.spec+'\',\'ai\')" title="AI Spec">§</span>';}
h+='<span class="tag '+(run?'g':'r')+'">'+(run?'运行中':esc(it.status))+'</span>';
h+='</div>';}
}