diff --git a/internal/plugins/webui/dashboard.css b/internal/plugins/webui/dashboard.css
index 994eca5..5de23aa 100644
--- a/internal/plugins/webui/dashboard.css
+++ b/internal/plugins/webui/dashboard.css
@@ -2397,6 +2397,19 @@
.rt-svg text {
fill: var(--text-primary);
}
+ /* 拓扑图的文字全部走类(不用 font-size/fill 属性):
+ 1) 字号统一可读(原来 9–11px 且低对比度,实机很难看清);
+ 2) 颜色交给主题变量,切主题/配色时不用重算 SVG。
+ 长名截断由 JS 的 rtClip 负责(SVG 没有 text-overflow)。 */
+ .rt-topo-svg .tp-name { font-size: 12px; fill: var(--text-primary); }
+ .rt-topo-svg .tp-sub { font-size: 10.5px; fill: var(--text-muted); font-variant-numeric: tabular-nums; }
+ .rt-topo-svg .tp-val { font-size: 12.5px; font-weight: 700; fill: var(--text-primary); }
+ .rt-topo-svg .tp-lab { font-size: 11px; font-weight: 600; }
+ .rt-topo-svg .tp-warn { font-size: 10.5px; fill: #ffb86b; }
+ .rt-topo-svg .tp-hint { font-size: 10.5px; fill: var(--text-muted); }
+ .rt-topo-svg .tp-edge { fill: none; }
+ .rt-topo-svg .tp-edge-in { stroke-opacity: 0.45; stroke-width: 1.4; }
+ .rt-topo-svg .tp-edge-out { stroke-opacity: 0.35; stroke-width: 1.2; }
/* 第五条:排队队列(无级别)。用虚线分隔 + 中性色,与“有级别”的四条区分开——
它不是优先级,而是另一**类别**(排队 vs 中断),同一套配色会误导。 */
.rt-level.rt-level-queued {
diff --git a/internal/plugins/webui/dashboard.js b/internal/plugins/webui/dashboard.js
index cccd555..eed9a04 100644
--- a/internal/plugins/webui/dashboard.js
+++ b/internal/plugins/webui/dashboard.js
@@ -925,9 +925,41 @@
// 右边是它能写的 outputch。连线即路由;光点沿连线跑表示消息正在流动。
// 与旧版「归属框 → 单个内核盒」的差别:内核盒只有一个,看不出"这条输入到底
// 喂给了哪个子",而子 agent 才是运行态里最该看清的东西。
+ // rtTopoWidth 用**实测容器宽**当 viewBox 宽:viewBox 宽与渲染宽一致时缩放才是
+ // 1:1。此前固定 640 而容器 ~920,浏览器按 "meet" 把内容顶在左上、右侧空出一大块,
+ // 观感就是"间距不对"。
+ function rtTopoWidth() {
+ var host = document.getElementById("rt-sec-topo");
+ var w = host ? host.clientWidth : 0;
+ if (!w) {
+ var panel = document.getElementById("rt-panel");
+ w = panel ? panel.clientWidth : 0;
+ }
+ if (!w || w < 360) w = 900;
+ return Math.round(w) - 4;
+ }
+
+ // rtClip 按估算宽度截断长通道名(SVG 没有 text-overflow,只能自己量)。
+ // 全角按 1em、其余按 0.56em 估宽:只求不越界,不求像素级精确。
+ function rtClip(text, maxPx, fontPx) {
+ var s = String(text == null ? "" : text);
+ var w = 0;
+ var i = 0;
+ for (; i < s.length; i++) {
+ var cw = s.charCodeAt(i) > 0x2e80 ? fontPx : fontPx * 0.56;
+ if (w + cw > maxPx) break;
+ w += cw;
+ }
+ if (i >= s.length) return s;
+ return s.slice(0, Math.max(1, i - 1)) + "…";
+ }
+
function rtAgentTopology(agents) {
- var ROW = 26, GAP = 16, NODE_R = 19, W = 640, MAX_OUT = 8;
- var IN_X = 6, IN_W = 176, NX = 272, OUT_X = 344, OUT_W = 244;
+ var W = rtTopoWidth();
+ var ROW = 30, BOX_H = 20, GAP = 14, NODE_R = 20, MAX_OUT = 8;
+ // 三列按比例分:输入 0~32%,节点居中在 44%,输出 56%~100%,两侧留白对称。
+ var IN_X = 0, IN_W = Math.round(W * 0.32);
+ var NX = Math.round(W * 0.44), OUT_X = Math.round(W * 0.56), OUT_W = W - Math.round(W * 0.56) - 2;
var out = [];
_rtEdgeIn = {};
_rtEdgeOut = {};
@@ -942,27 +974,37 @@
}
var y = 8;
if (!agents.length) {
- out.push('