mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-02 23:33:58 +00:00
fix(webui): 运行态面板不再闪、通道分配带归属(含驻留子)、改图形化
三个用户可见问题,逐个说明根因与改法。 1) 首页「一闪一闪」——运行态每 3s 轮询一次,renderRuntime 无条件重建 #rt-panel 的 innerHTML:数据没变也把整块 DOM(含各级条的 transition) 推倒重来。改法:缓存数据签名(**不含 uptime**——它每秒都变,带上等于没缓存), 签名相同直接 return,一个字节都不动。另:renderOverview 会整块重建 #rt-panel(面板本身是空的),所以那里必须让签名失效,否则空面板填不上。 2) 通道分配只显示内核/根 agent,看不见驻留子——根因是状态面只暴露了设备能力 (KernelStatus.Channels,来自 iom.ListChannels),而「这条输入归谁」是 ChannelRegistry 的属性(InputChannel.Owner/Capacity/Output),从未出过内核。 而登记表本来就是根 agent 与驻留子**共用同一份**,所以数据一直都在,只是没画。 改法:KernelStatus 新增 InputChannels(+ sdk.InputChannelInfo), /api/v1/runtime 带出 input_channels;前端把它按 owner 分进「归属容器」, 驻留子即使一条 inputch 都没划到也照样出现在图里(否则"子存在但看不见" 与"子不存在"无法区分),并显示其 allowed_outputs / 轮次 / 上下文满标记。 3) 「这些信息明明可以图形化」——四级中断的登记/抢占由纯文本改成并排迷你条; 通道分配用归属容器 + 容量滑块(轨道/填充/把手/读数),并把回程通道、 注册插件做成胶囊标签。设备能力拓扑(原有)保留。 验证:go build/vet 干净;go test ./internal/agent/... ./internal/plugin/... ./internal/sdk/... ./cmd/... 全绿;node --check dashboard.js 语法通过。 TestRuntimeEndpoint 扩展为同时钉住 input_channels 的归属与「驻留子划走的那条」。
This commit is contained in:
@ -583,6 +583,38 @@
|
||||
];
|
||||
var RT_CAP_NAMES = { 1: "text", 2: "file", 4: "image", 8: "audio", 16: "structured" };
|
||||
|
||||
// _rtSig 缓存上一次渲染的数据签名。
|
||||
//
|
||||
// 为什么必须缓存:运行态每 3s 轮询一次,数据绝大多数时候是**没变**的;
|
||||
// 无条件 `innerHTML =` 会把整块 DOM(含各级条的 transition)每 3s 重建一遍,
|
||||
// 视觉效果就是“首页一闪一闪”。签名相同就一个字节也不动。
|
||||
var _rtSig = null;
|
||||
|
||||
// rtSlider 把“值 / 上限”画成轨道 + 滑块 + 读数(比纯文本数字直观)。
|
||||
function rtSlider(value, max, label, display) {
|
||||
var pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
|
||||
return (
|
||||
'<span class="rt-slider"><span class="rt-slider-label">' +
|
||||
escHtml(label) +
|
||||
'</span><span class="rt-slider-track"><i style="width:' + pct + '%"></i><b style="left:' + pct + '%"></b></span>' +
|
||||
'<span class="rt-slider-val">' +
|
||||
escHtml(String(display === undefined ? value : display)) +
|
||||
"</span></span>"
|
||||
);
|
||||
}
|
||||
|
||||
// rtMini 是行内迷你条(登记/抢占这类要并排两个的量)。
|
||||
function rtMini(value, max, label) {
|
||||
var pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
|
||||
return (
|
||||
'<span class="rt-mini" title="' +
|
||||
escHtml(label) + " " + value +
|
||||
'"><span class="rt-mini-track"><i style="width:' + pct + '%"></i></span>' +
|
||||
value +
|
||||
"</span>"
|
||||
);
|
||||
}
|
||||
|
||||
function rtTile(num, label, sub, pct, active, warn) {
|
||||
return (
|
||||
'<div class="rt-tile' +
|
||||
@ -616,11 +648,18 @@
|
||||
var rt = state.runtime;
|
||||
if (!rt) {
|
||||
el.innerHTML = '<div class="rt-empty">' + __("运行态数据不可用", "runtime unavailable") + "</div>";
|
||||
_rtSig = null;
|
||||
return;
|
||||
}
|
||||
var sc = rt.scheduler || {};
|
||||
var residents = rt.residents || [];
|
||||
var channels = rt.channels || [];
|
||||
var inputs = rt.input_channels || [];
|
||||
// 数据签名(**不含 uptime**——它每秒都变,带上就等于没缓存):
|
||||
// 与上次相同则直接返回,一个字节都不动(“首页一闪一闪”的根因)。
|
||||
var sig = JSON.stringify([sc, residents, channels, inputs]);
|
||||
if (sig === _rtSig) return;
|
||||
_rtSig = sig;
|
||||
var q = sc.interrupt_queues || [0, 0, 0, 0, 0];
|
||||
var pending = sc.pending_interrupts || 0;
|
||||
var ready = sc.ready_queue_depth || 0;
|
||||
@ -643,6 +682,12 @@
|
||||
// 四级中断队列
|
||||
html += '<div class="rt-section-title">' + __("中断队列(按级别)", "Interrupt queues by level") + "</div>";
|
||||
var maxQ = Math.max(1, q[1] || 0, q[2] || 0, q[3] || 0, q[4] || 0);
|
||||
var maxReg = 1;
|
||||
var maxPre = 1;
|
||||
RT_LEVELS.forEach(function (L) {
|
||||
maxReg = Math.max(maxReg, byLv[L.lv] || 0);
|
||||
maxPre = Math.max(maxPre, preLv[L.lv] || 0);
|
||||
});
|
||||
html += '<div class="rt-levels">';
|
||||
RT_LEVELS.forEach(function (L) {
|
||||
var depth = q[L.lv] || 0;
|
||||
@ -654,7 +699,8 @@
|
||||
(depth ? Math.max(4, (depth / maxQ) * 100) : 0) +
|
||||
'%"></i></span>' +
|
||||
'<span class="rt-lv-meta">' + depth + " · " +
|
||||
__("登记", "reg") + " " + reg + " / " + __("抢占", "pre") + " " + pre +
|
||||
rtMini(reg, maxReg, __("登记", "registered")) +
|
||||
rtMini(pre, maxPre, __("抢占", "preempted")) +
|
||||
"</span></div>";
|
||||
});
|
||||
html += "</div>";
|
||||
@ -683,7 +729,82 @@
|
||||
escHtml(sc.running.kind || "") + " #" + sc.running.id + " (L" + (sc.running.level || 0) + ")</div>";
|
||||
}
|
||||
|
||||
// 通道拓扑
|
||||
// 通道**分配(按归属)**:inputch 是输入路由单位,而登记表由根 agent 与
|
||||
// 驻留子**共用同一份**——所以“这条输入归谁”必须画出来。只画设备能力
|
||||
// (下面的拓扑)等于把“路由发生在进内核之前”这条设计事实藏起来,
|
||||
// 于是驻留子的通道分配在界面上完全不可见。
|
||||
html += '<div class="rt-section-title">' + __("通道分配(按归属)", "Input channels by owner") + "</div>";
|
||||
var maxCap = 1;
|
||||
inputs.forEach(function (c) {
|
||||
if ((c.capacity || 0) > maxCap) maxCap = c.capacity;
|
||||
});
|
||||
var owners = [];
|
||||
var byOwner = {};
|
||||
inputs.forEach(function (c) {
|
||||
var o = c.owner || "";
|
||||
if (!byOwner[o]) {
|
||||
byOwner[o] = [];
|
||||
owners.push(o);
|
||||
}
|
||||
byOwner[o].push(c);
|
||||
});
|
||||
// 驻留子即使一条 inputch 也没划到,也要出现在分配图里——
|
||||
// 否则“子存在但界面上看不见”与“子不存在”无法区分。
|
||||
residents.forEach(function (r) {
|
||||
var o = r.id || "";
|
||||
if (o && !byOwner[o]) {
|
||||
byOwner[o] = [];
|
||||
owners.push(o);
|
||||
}
|
||||
});
|
||||
owners.sort(function (a, b) {
|
||||
if (a === "") return -1;
|
||||
if (b === "") return 1;
|
||||
return a < b ? -1 : 1;
|
||||
});
|
||||
if (!owners.length) {
|
||||
html += '<div class="rt-empty">' + __("暂无通道登记", "no channel registered") + "</div>";
|
||||
}
|
||||
owners.forEach(function (o) {
|
||||
var list = byOwner[o] || [];
|
||||
var res = null;
|
||||
if (o) {
|
||||
residents.forEach(function (r) {
|
||||
if (r.id === o) res = r;
|
||||
});
|
||||
}
|
||||
html += '<div class="rt-owner' + (o ? " rt-owner-child" : "") + '">';
|
||||
html += '<div class="rt-owner-head"><span class="rt-owner-name">' +
|
||||
(o ? "▸ " : "◆ ") +
|
||||
(o ? __("驻留子 ", "resident ") + escHtml(o) : __("根 agent / 内核默认", "root agent / kernel default")) +
|
||||
'</span><span class="rt-owner-meta">' + list.length + " " + __("条通道", "channels") +
|
||||
(res ? " · " + __("轮次", "rounds") + " " + (res.rounds || 0) : "") +
|
||||
(res && res.context_full ? ' <span class="rt-badge-warn">' + __("上下文已满", "ctx full") + "</span>" : "") +
|
||||
"</span></div>";
|
||||
if (list.length) {
|
||||
list.forEach(function (c) {
|
||||
var cap = c.capacity || 0;
|
||||
html += '<div class="rt-assign">' +
|
||||
'<span class="rt-chan-name">' + escHtml(c.name) + "</span>" +
|
||||
(c.plugin ? '<span class="rt-chip">' + escHtml(c.plugin) + "</span>" : "") +
|
||||
rtSlider(cap, maxCap, __("容量", "cap"), cap ? String(cap) : __("默认", "default")) +
|
||||
'<span class="rt-chan-sub">' +
|
||||
(c.output ? __("回程 ", "out ") + escHtml(c.output) : __("回程由来源决定", "out by source")) +
|
||||
"</span></div>";
|
||||
});
|
||||
} else {
|
||||
var allowed = (res && res.allowed_outputs) || [];
|
||||
html += '<div class="rt-chan-sub">' +
|
||||
(allowed.length
|
||||
? __("可发往输出通道:", "allowed outputs: ") +
|
||||
allowed.map(function (x) { return '<span class="rt-chip">' + escHtml(x) + "</span>"; }).join("")
|
||||
: __("未划入任何 inputch", "no input channel assigned")) +
|
||||
"</div>";
|
||||
}
|
||||
html += "</div>";
|
||||
});
|
||||
|
||||
// 通道拓扑(设备能力面:能收什么、能发什么、有哪些工具)
|
||||
html += '<div class="rt-section-title">' + __("通道拓扑", "Channel topology") + "</div>";
|
||||
var ins = channels.filter(function (c) { return c.direction === "in" || c.direction === "io"; });
|
||||
var outs = channels.filter(function (c) { return c.direction === "out" || c.direction === "io"; });
|
||||
@ -730,6 +851,9 @@
|
||||
}
|
||||
|
||||
function renderOverview() {
|
||||
// 总览页会整块重建 #rt-panel(面板本身是空的),所以必须让运行态的
|
||||
// 数据签名失效,否则 renderRuntime 会以为“没变化”而不去填这块空面板。
|
||||
_rtSig = null;
|
||||
var s = state.status || {};
|
||||
var k = state.kernel;
|
||||
var html =
|
||||
|
||||
Reference in New Issue
Block a user