diff --git a/internal/agent/core/resident.go b/internal/agent/core/resident.go
index 77b6d9b..156ba94 100644
--- a/internal/agent/core/resident.go
+++ b/internal/agent/core/resident.go
@@ -61,6 +61,17 @@ type ResidentInfo struct {
CreatedAt time.Time `json:"created_at"`
TableSize int `json:"table_size"`
Table []InputchRecord `json:"table,omitempty"`
+
+ // Sched* 是这个驻留子**自己的**输入调度器积压摘要(排队 / 待处理中断 /
+ // 中断栈 / 四级中断队列)。
+ //
+ // 为什么必须单列:每个驻留子是独立 agent,跑自己的事件循环与调度器
+ // (见 agent.go 的 newScheduler)。KernelStatus.Scheduler 只是**根**那份,
+ // 拿它代表全部子会让界面把「某个子堵死」显示成「一切正常」。
+ SchedReady int `json:"sched_ready"`
+ SchedPending int `json:"sched_pending"`
+ SchedStack int `json:"sched_stack"`
+ SchedQueues [5]int `json:"sched_queues"`
}
type residentChild struct {
@@ -527,10 +538,17 @@ func (rc *residentChild) info() ResidentInfo {
state, full := rc.state, rc.state == "contextfull"
rc.mu.Unlock()
table := rc.agent.inputchTableSnapshot()
+ // 子自己的调度器积压(per-agent 负载展示用)。schedulerStatus 只读快照,
+ // 每次状态轮询为每个子算一次,成本可忽略(驻留子数量是个位数)。
+ sc := rc.agent.schedulerStatus()
info := ResidentInfo{
ID: rc.id, State: state, InputChs: append([]string(nil), rc.inputChs...),
AllowedOutputs: append([]string(nil), rc.allowed...),
ContextFull: full, CreatedAt: rc.createdAt, TableSize: len(table),
+ SchedReady: sc.ReadyQueueDepth,
+ SchedPending: sc.PendingInterrupts,
+ SchedStack: sc.SuspendStack,
+ SchedQueues: sc.InterruptQueues,
// Rounds = 子**已执行的轮次数**(调度器的执行计数,单调不减)。
//
// 此前这里根本没填这个字段 ⇒ 父看到的永远是 `轮次=0`,与"处理表已有 N 条"
diff --git a/internal/agent/core/status.go b/internal/agent/core/status.go
index ff612b3..e855d4a 100644
--- a/internal/agent/core/status.go
+++ b/internal/agent/core/status.go
@@ -312,6 +312,11 @@ func residentStatuses(list []ResidentInfo) []ResidentStatus {
InputChs: r.InputChs,
AllowedOutputs: r.AllowedOutputs,
InputChTable: r.TableSize,
+ // 子自己的调度器积压:per-agent 负载图靠这四项,缺了就只能画根。
+ ReadyQueueDepth: r.SchedReady,
+ PendingInterrupts: r.SchedPending,
+ SuspendStack: r.SchedStack,
+ InterruptQueues: r.SchedQueues,
}
if !r.CreatedAt.IsZero() {
st.CreatedAt = r.CreatedAt.Format(time.RFC3339)
diff --git a/internal/plugins/webui/dashboard.css b/internal/plugins/webui/dashboard.css
index 982a809..4cb5144 100644
--- a/internal/plugins/webui/dashboard.css
+++ b/internal/plugins/webui/dashboard.css
@@ -895,8 +895,8 @@
select {
background: var(--bg-input);
border: 1px solid var(--border-color);
- border-radius: var(--radius-sm);
- padding: 8px 12px;
+ border-radius: var(--radius-md);
+ padding: 9px 12px;
color: var(--text-primary);
font-size: 13px;
width: 100%;
@@ -1216,20 +1216,29 @@
min-height: 60vh;
}
.chat-tabs {
- display: flex;
- gap: 4px;
+ /* 分段控件(segmented control)而不是一排药丸 chip:
+ 四个页签是同一组互斥选项,「一个容器 + 一个高亮块」比「四个各自带边框的胶囊」
+ 更准确地表达了这层关系,也少四道描边噪声。 */
+ display: inline-flex;
+ gap: 3px;
+ padding: 3px;
flex-wrap: wrap;
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 10px;
+ align-self: flex-start;
+ border-radius: var(--radius-md);
+ background: var(--bg-input);
+ border: 1px solid var(--border-color);
}
.chat-tabs span {
padding: 6px 14px;
- font-size: 13px;
+ font-size: 12.5px;
+ font-weight: 500;
cursor: pointer;
color: var(--text-muted);
- border-radius: var(--radius-pill);
- border: 1px solid transparent;
- transition: all 0.15s;
+ border-radius: 7px;
+ border: none;
+ transition:
+ background var(--dur-micro) var(--ease-out),
+ color var(--dur-micro) var(--ease-out);
}
.chat-tabs span:hover {
color: var(--text-primary);
@@ -1238,7 +1247,8 @@
.chat-tabs span.active {
color: var(--accent);
background: var(--accent-bg);
- border-color: rgba(255, 127, 172, 0.35);
+ font-weight: 600;
+ box-shadow: inset 0 0 0 1px rgba(255, 127, 172, 0.28);
}
.chat-panel {
display: none;
@@ -1274,17 +1284,19 @@
.chat-messages {
flex: 1;
overflow-y: auto;
- padding: 18px 18px 26px;
+ padding: 20px 20px 28px;
margin-bottom: 0;
display: flex;
flex-direction: column;
- gap: 6px;
+ /* 消息间距从 6px 拉到 14px:原来每条贴在一起,一屏十几条像一堵墙,
+ 分不清哪句是哪句。 */
+ gap: 14px;
min-height: 0;
}
.msg {
display: flex;
- gap: 8px;
- margin-bottom: 2px;
+ gap: 10px;
+ margin-bottom: 0;
align-items: flex-start;
max-width: 100%;
}
@@ -1300,8 +1312,8 @@
max-width: 90%;
}
.msg-avatar {
- width: 28px;
- height: 28px;
+ width: 30px;
+ height: 30px;
border-radius: 50%;
overflow: hidden;
display: flex;
@@ -1309,6 +1321,8 @@
justify-content: center;
font-size: 12px;
flex-shrink: 0;
+ /* 细描边把头像从同色背景里“拓”出来,否则暗色下就是一团 */
+ box-shadow: 0 0 0 1px var(--glass-border);
}
.msg-avatar img {
width: 100%;
@@ -1337,7 +1351,10 @@
}
.msg-content {
min-width: 0;
- flex: 1;
+ flex: 1 1 auto;
+ /* 行长上限:整屏宽的一行文字没人读得下去(~78ch 是舒适区),
+ 也让「同一侧连续多条」自然堆成一列而不是横铺开。 */
+ max-width: min(78ch, 86%);
}
.msg-content .msg-bubble + .msg-bubble {
margin-top: 6px;
@@ -1367,25 +1384,27 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}
.msg-bubble {
- padding: 9px 14px;
- border-radius: var(--radius-md);
- font-size: 15px;
- line-height: 1.62;
+ padding: 10px 14px;
+ border-radius: 12px;
+ font-size: 14.5px;
+ line-height: 1.65;
word-break: break-word;
position: relative;
border: 1px solid transparent;
transition: box-shadow var(--dur-normal) var(--ease-out);
}
.msg-user .msg-bubble {
- background: var(--msg-bubble-bg);
- color: var(--msg-bubble-color);
- border-bottom-right-radius: 4px;
- border-color: var(--msg-bubble-border);
+ /* 用户与助手必须**看得出不一样**:原来两侧同底色、只差一个圆角,
+ 整段对话读下来分不清谁说的。用户侧用 accent 渲染,助手侧保持中性面。 */
+ background: var(--msg-user-bg);
+ color: var(--text-primary);
+ border-bottom-right-radius: 5px;
+ border-color: rgba(255, 127, 172, 0.26);
}
.msg-assistant .msg-bubble {
background: var(--msg-bubble-bg);
color: var(--msg-bubble-color);
- border-bottom-left-radius: 4px;
+ border-bottom-left-radius: 5px;
border-color: var(--msg-bubble-border);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
@@ -1817,7 +1836,11 @@
display: flex;
gap: 8px;
flex-shrink: 0;
- padding-top: 10px;
+ padding-top: 14px;
+ margin-top: 4px;
+ /* 输入区与消息流之间加一道分隔:否则最后一条消息与输入框糊在一起,
+ 看不出“消息到头了”。 */
+ border-top: 1px solid var(--border-color);
}
.chat-input-row input {
flex: 1;
@@ -2519,3 +2542,90 @@
margin: 12px 0 6px;
text-transform: uppercase;
}
+
+ /* ===== 阶段管道滑块(总览)=====
+ 七个阶段各一格,滑块停在当前阶段;空闲时整条降透明度,不做假动画。
+ 滑块用绝对定位 + left 过渡实现"滑过去"(内联 left 由 rtPipelineHtml 给)。 */
+ .rt-pipe {
+ position: relative;
+ padding: 4px 0 2px;
+ margin: 2px 0 8px;
+ transition: opacity 0.3s var(--ease-out);
+ }
+ .rt-pipe-idle {
+ opacity: 0.5;
+ }
+ .rt-pipe-track {
+ position: absolute;
+ left: 22px;
+ right: 22px;
+ top: 9px;
+ height: 2px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.12);
+ }
+ .rt-pipe-nodes {
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ }
+ .rt-pipe-node {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ width: 44px;
+ }
+ .rt-pipe-node > i {
+ width: 11px;
+ height: 11px;
+ border-radius: 50%;
+ background: var(--bg-input);
+ border: 2px solid var(--border-color);
+ position: relative;
+ z-index: 1;
+ transition:
+ background 0.25s var(--ease-out),
+ border-color 0.25s var(--ease-out),
+ box-shadow 0.25s var(--ease-out);
+ }
+ .rt-pipe-node > b {
+ font-size: 9px;
+ font-weight: 500;
+ color: var(--text-muted);
+ white-space: nowrap;
+ }
+ .rt-pipe-node.done > i {
+ background: var(--frost-300);
+ border-color: var(--frost-300);
+ }
+ .rt-pipe-node.active > i {
+ background: var(--sakura-400);
+ border-color: var(--sakura-400);
+ box-shadow: 0 0 0 4px rgba(255, 127, 172, 0.18);
+ }
+ .rt-pipe-node.active > b {
+ color: var(--text-primary);
+ font-weight: 600;
+ }
+ .rt-pipe-knob {
+ position: absolute;
+ top: 4px;
+ width: 11px;
+ height: 11px;
+ margin-left: -5.5px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 0 10px rgba(255, 127, 172, 0.85);
+ transition: left 0.45s var(--ease-out);
+ z-index: 2;
+ }
+ .rt-pipe-idle .rt-pipe-knob {
+ box-shadow: none;
+ opacity: 0.5;
+ }
+ /* 拓扑光点:沿 animateMotion 给的路径跑;不吃鼠标事件,跑完由 JS 摘掉。 */
+ .rt-spark {
+ pointer-events: none;
+ filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.8));
+ }
diff --git a/internal/plugins/webui/dashboard.js b/internal/plugins/webui/dashboard.js
index 0baea14..5e91e46 100644
--- a/internal/plugins/webui/dashboard.js
+++ b/internal/plugins/webui/dashboard.js
@@ -12,6 +12,8 @@
messages: [],
chatLoading: false,
chatStage: "",
+ pipelinePhase: "", // 当前阶段(SSE stage 事件驱动总览页的滑块)
+ pipelineTimer: null,
healthResult: null,
starmapInit: false,
starmapLoading: false,
@@ -570,9 +572,9 @@
// 数据来自 /api/v1/runtime(内核 internal/sdk 暴露的 KernelStatus 子集),
// 每 3 秒刷一次——运行态要"实时",而 /kernel 是 30KB 级的全量状态,不适合秒级轮询。
//
- // 四个数字块回答"现在忙不忙":排队任务、待处理中断、中断栈深度、驻留子数量;
- // 下面的四级条形回答"堵在哪一级";中断栈图回答"谁打断了谁";
- // 通道拓扑回答"消息从哪儿进、能往哪儿出"。
+ // 面板从上到下:四个数字块回答"现在忙不忙";阶段管道滑块回答"这一轮走到哪";
+ // 五条队列条形 + 中断栈回答"堵在哪一级/压了几层";最后的分带拓扑回答
+ // "哪条输入喂给哪个 agent、哪个 agent 往哪条输出写、各自负载多高"。
// 四级语义直接照抄内核(internal/agent/core/scheduler.go 的 Level 定义),
// 别自己起名字——前端叫法一旦和内核不一致,看板就成了误导。
var RT_LEVELS = [
@@ -691,119 +693,237 @@
});
}
- // rtTopologySvg 把「通道 + 归属 + 路由」画成**一张图**:
- // 左侧按归属框出输入通道 → 汇集母线 → 内核 → 输出母线 → 右侧输出通道。
- // 连线即路由;归属是容器与配色;容量是节点里的细条;输出能力是彩色圆点。
- // 为什么合进拓扑而不是单开一项:通道属于谁是**拓扑的一部分**
- // (左边这些输入口分别被谁接管),拆两张表反而看不出关系。
- function rtTopologySvg(groups, channels) {
- var ROW = 24, HEAD = 18, GPAD = 8, GAP = 10;
- var W = 660;
- var LX = 6, LW = 226;
- var KX = 292, KW = 76;
- var RX = 408, RW = 246;
- var BUS_IN = 262, BUS_OUT = 396;
- var top = 6;
+ // ---- 阶段管道滑块 ----
+ //
+ // 七阶段与内核 sdk.Stage 一一对应(顺序即执行顺序),由 SSE `stage` 事件驱动:
+ // 哪个阶段在执行,滑块就滑到哪一格。空闲时整条管道降透明度,不做假动画。
+ //
+ // 为什么放进总览:阶段管道回答"这一轮走到哪一步",总览其余图形回答"积压了多少",
+ // 两者合起来才是运行态。此前它只是对话页一个 10px 的角标,等于看不见。
+ var RT_PIPE = [
+ { k: "on_input", zh: "输入", en: "input" },
+ { k: "pre_action", zh: "行动前", en: "pre-action" },
+ { k: "post_action", zh: "行动后", en: "post-action" },
+ { k: "before_toolcall", zh: "工具前", en: "pre-tool" },
+ { k: "after_toolcall", zh: "工具后", en: "post-tool" },
+ { k: "before_output", zh: "输出前", en: "pre-output" },
+ { k: "after_output", zh: "输出后", en: "post-output" },
+ ];
+
+ function rtPipelineHtml(phase) {
+ var idx = -1;
+ for (var i = 0; i < RT_PIPE.length; i++) if (RT_PIPE[i].k === phase) idx = i;
+ var n = RT_PIPE.length;
+ var pct = idx < 0 ? 0 : idx / (n - 1);
+ var nodes = RT_PIPE.map(function (s, i) {
+ var cls = "rt-pipe-node";
+ if (i === idx) cls += " active";
+ else if (idx >= 0 && i < idx) cls += " done";
+ return '' + __(s.zh, s.en) + "";
+ }).join("");
+ return (
+ '
' +
+ __("阶段管道", "Stage pipeline") +
+ (idx < 0 ? " " + __("(空闲)", "(idle)") : "") +
+ "
" +
+ '' +
+ '
' +
+ '
' +
+ '
' + nodes + "
" +
+ "
"
+ );
+ }
+
+ // ---- per-agent 负载 ----
+ //
+ // 负载 = 该 agent **自己的**调度器积压折算成的百分比。
+ //
+ // 为什么按级别加权:四级中断里 L4 是内核独占、L3 是交互,堵在 L4 一条比堵在
+ // L1 五条更严重;中断栈深度意味着有现场被压着。权重是启发式的("满载"没有
+ // 硬定义),因此函数做成饱和式 backlog/(backlog+K):单调、上界 100、不越界。
+ // context_full 单独加分:子上下文满了以后每轮都要压缩,本身就是高负载。
+ function rtLoadPct(sc, ctxFull) {
+ sc = sc || {};
+ var q = sc.interrupt_queues || [0, 0, 0, 0, 0];
+ var backlog =
+ (sc.ready_queue_depth || 0) +
+ (sc.pending_interrupts || 0) * 1.5 +
+ (sc.suspend_stack || 0) * 2 +
+ (q[1] || 0) * 1 +
+ (q[2] || 0) * 1.2 +
+ (q[3] || 0) * 1.5 +
+ (q[4] || 0) * 2;
+ var pct = 100 * (backlog / (backlog + 8));
+ if (ctxFull) pct += 15;
+ return Math.max(0, Math.min(100, Math.round(pct)));
+ }
+
+ // rtAgents 把「inputch 归属 + 驻留子 + 各自的调度器积压」整理成每个 agent 一行。
+ // 根 agent 的积压来自 rt.scheduler;驻留子的来自它自己的 ResidentStatus
+ // (见 internal/sdk/status.go 的 ReadyQueueDepth 等四项)。
+ function rtAgents(rt) {
+ var rootID = rt.agent_id || "";
+ var groups = rtOwnerGroups(rt.input_channels || [], rt.residents || [], rootID);
+ var outChans = (rt.channels || []).filter(function (c) {
+ return c.direction === "out" || c.direction === "io";
+ });
+ return groups.map(function (g) {
+ var id = g.owner || rootID;
+ var outputs = [];
+ var seen = {};
+ function add(name) {
+ if (!name || seen[name]) return;
+ seen[name] = 1;
+ outputs.push(name);
+ }
+ if (g.child) {
+ // 驻留子:优先用父显式授权的输出;没有授权登记时退回它自己 inputch 的
+ // 默认回程(`output` 字段)。
+ ((g.res && g.res.allowed_outputs) || []).forEach(add);
+ if (!outputs.length) g.list.forEach(function (c) { add(c.output); });
+ } else {
+ // 根 agent 可以写任何输出通道;上限交给渲染侧截断。
+ outChans.forEach(function (c) { add(c.name); });
+ }
+ var load = g.child
+ ? rtLoadPct(
+ {
+ ready_queue_depth: g.res && g.res.ready_queue_depth,
+ pending_interrupts: g.res && g.res.pending_interrupts,
+ suspend_stack: g.res && g.res.suspend_stack,
+ interrupt_queues: (g.res && g.res.interrupt_queues) || [],
+ },
+ g.res && g.res.context_full,
+ )
+ : rtLoadPct(rt.scheduler || {}, false);
+ return {
+ id: id, child: g.child, label: g.label, color: g.color,
+ inputs: g.list, outputs: outputs, load: load, res: g.res || null,
+ };
+ });
+ }
+
+ // 连线路径表:通道名 → path d。光点动画靠它把「哪个通道」翻成一条曲线。
+ var _rtEdgeIn = {};
+ var _rtEdgeOut = {};
+ var RT_SVGNS = "http://www.w3.org/2000/svg";
+
+ // rtAgentTopology 画「通道 → agent → 通道」的分带拓扑。
+ //
+ // 每个 agent 一条横带:左边是归它的 inputch,中间是它自己(圆环 = 负载),
+ // 右边是它能写的 outputch。连线即路由;光点沿连线跑表示消息正在流动。
+ // 与旧版「归属框 → 单个内核盒」的差别:内核盒只有一个,看不出"这条输入到底
+ // 喂给了哪个子",而子 agent 才是运行态里最该看清的东西。
+ 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 out = [];
+ _rtEdgeIn = {};
+ _rtEdgeOut = {};
function esc(s) {
return String(s == null ? "" : s).replace(/[<>&]/g, function (m) {
return m === "<" ? "<" : m === ">" ? ">" : "&";
});
}
-
- // ---- 左:归属容器 + 通道行 ----
- var y = top;
- var inRows = [];
- groups.forEach(function (g) {
- var boxH = HEAD + g.list.length * ROW + GPAD;
- out.push(
- '"
- );
- out.push(
- '' +
- esc((g.child ? "▸ " : "◆ ") + g.label) +
- (g.res ? " " + __("轮次", "rounds") + " " + (g.res.rounds || 0) : "") +
- (g.res && g.res.context_full ? " " + __("上下文已满", "ctx full") : "") +
- ""
- );
- g.list.forEach(function (c, i) {
- var ry = y + HEAD + i * ROW;
- inRows.push({ y: ry + ROW / 2, color: g.color });
- out.push('' + esc(c.name) + "");
- var cap = c.capacity || 0;
- var tx = LX + LW - 76;
- out.push('');
- if (cap > 0) {
- var wpx = Math.max(3, Math.min(46, (46 * Math.min(cap, 64)) / 64));
- out.push('');
- // 只有**非默认**容量才写数字:默认容量用空轨表达,
- // 否则整张图会排满十几行「默认」,与“少文字”背道而驰。
- out.push('' + esc(cap) + "");
- }
- });
- y += boxH + GAP;
- });
- var leftBottom = Math.max(y - GAP, top + 40);
-
- // ---- 右:输出通道 ----
- var outs = (channels || []).filter(function (c) {
- return c.direction === "out" || c.direction === "io";
- });
- var outRows = [];
- var oy = top;
- var CAPS = [[1, "#88c0d0"], [2, "#a3be8c"], [4, "#ebcb8b"], [8, "#d08770"], [16, "#b48ead"]];
- outs.forEach(function (c) {
- outRows.push({ y: oy + ROW / 2 });
- out.push('');
- out.push('' + esc(c.name) + "");
- var cx = RX + RW - 76;
- CAPS.forEach(function (b) {
- if ((c.output_caps || 0) & b[0]) {
- out.push('');
- }
- cx += 14;
- });
- oy += ROW;
- });
- var rightBottom = Math.max(oy, top + 40);
-
- var H = Math.max(leftBottom, rightBottom) + 10;
- var ky = Math.round((Math.min(leftBottom, rightBottom) + Math.max(leftBottom, rightBottom)) / 2) - 14;
-
- // ---- 输入母线 ----
- if (inRows.length) {
- var iy0 = inRows[0].y;
- var iy1 = inRows[inRows.length - 1].y;
- out.push('');
- inRows.forEach(function (r) {
- out.push('');
- });
- out.push('');
+ function pathD(x1, y1, x2, y2) {
+ var mx = (x1 + x2) / 2;
+ return "M" + x1 + " " + y1 + " C" + mx + " " + y1 + " " + mx + " " + y2 + " " + x2 + " " + y2;
}
- // ---- 内核 ----
- out.push('');
- out.push('' + __("内核", "Kernel") + "");
- // ---- 输出母线 ----
- if (outRows.length) {
- var oy0 = outRows[0].y;
- var oy1 = outRows[outRows.length - 1].y;
- out.push('');
- outRows.forEach(function (r) {
- out.push('');
- });
- out.push('');
+ var y = 8;
+ if (!agents.length) {
+ out.push('' + __("暂无通道 / agent", "no channels / agents") + "");
+ y = 44;
}
+ agents.forEach(function (a) {
+ var rows = Math.max(a.inputs.length, a.outputs.length, 1);
+ var bandH = rows * ROW + GAP;
+ var cy = y + (rows * ROW) / 2;
+ out.push('');
+ out.push('');
+ // 输入通道 → agent
+ a.inputs.forEach(function (c, i) {
+ var ry = y + i * ROW + ROW / 2;
+ out.push('');
+ out.push('' + esc(c.name) + "");
+ if (c.capacity) {
+ out.push('' + esc(c.capacity) + "");
+ }
+ var d = pathD(IN_X + IN_W, ry, NX - NODE_R - 2, cy);
+ _rtEdgeIn[c.name] = d;
+ out.push('');
+ });
+
+ // agent 节点:两圈 + 一段负载弧(stroke-dasharray 画进度)
+ var C = 2 * Math.PI * (NODE_R - 3);
+ out.push('');
+ out.push('');
+ out.push(
+ '',
+ );
+ out.push('' + a.load + "");
+ out.push('' + esc(a.child ? a.id : __("根 agent", "root")) + "");
+ if (a.res && a.res.context_full) {
+ out.push('' + __("上下文已满", "ctx full") + "");
+ }
+
+ // agent → 输出通道
+ a.outputs.slice(0, MAX_OUT).forEach(function (name, i) {
+ var ry = y + i * ROW + ROW / 2;
+ out.push('');
+ out.push('' + esc(name) + "");
+ var d = pathD(NX + NODE_R + 2, cy, OUT_X, ry);
+ _rtEdgeOut[a.id + "\u0000" + name] = d;
+ out.push('');
+ });
+ if (a.outputs.length > MAX_OUT) {
+ out.push('+' + (a.outputs.length - MAX_OUT) + " " + __("更多", "more") + "");
+ }
+ y += bandH;
+ });
+ var H = Math.max(60, y);
return (
- '' + __("通道拓扑(连线即路由;左框 = 归属)", "Channel topology (edges = routing; boxes = owner)") + "
" +
- '