dsh-subagent-profile 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * 设置页视觉语言对齐官方「模型」设置节(dsh-client-ui-settings-models):同一套
11
11
  * 设计 token,标题 + 引言,每行一张带边框卡片(名称/描述/元信息分层布局),
12
- * 虚线「+ 新增」按钮展开内联表单卡片。下拉选项(preset / provider / model /
12
+ * 虚线「+ 新增」按钮展开就地表单卡片。下拉选项(preset / provider / model /
13
13
  * reasoning effort)由宿主 `/options/summary`(模型 + 预设,设置页打开时)、
14
14
  * `/options/efforts`(按模型,选择模型后懒加载)与 `/options/tools`(工具目录,
15
15
  * 限制面板打开时懒加载)路由提供,数据来自实时 presets roster 与 llm 目录。
@@ -23,6 +23,59 @@
23
23
 
24
24
  const CSS = [
25
25
  '.sap-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
26
+ '.sap-statusBar{position:sticky;top:0;z-index:1;background:transparent;border:0;border-bottom:1px solid var(--dsw-alias-border-l2);flex-wrap:wrap;gap:4px 14px;padding:4px 0 8px;display:flex;font-size:12px;line-height:18px}',
27
+ '.sap-statusDim{color:var(--dsw-alias-label-tertiary)}',
28
+ '.sap-rowHighlight{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}',
29
+ '.sap-subagentBanner{flex:none;display:flex;align-items:flex-end;padding:0 2px;max-width:42%;min-width:0}',
30
+ '.sap-subagentBannerTag{box-sizing:border-box;max-width:260px;border:1px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:2px 8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:help;display:inline-block}',
31
+ '.sap-dispatchPhase{flex-wrap:wrap;gap:2px 6px;display:flex;font-size:11px;line-height:16px}',
32
+ '.sap-phaseStep{color:var(--dsw-alias-label-tertiary)}',
33
+ '.sap-phaseStep-done{color:var(--dsw-alias-state-success-primary);font-weight:500}',
34
+ '.sap-phaseStep-fail{color:var(--dsw-alias-state-error-primary);font-weight:500}',
35
+ '.sap-phaseStep-running{color:var(--dsw-alias-state-warn-label);font-weight:500}',
36
+ '.sap-reminderP0{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary);font-weight:600}',
37
+ '.sap-reminderP1{color:var(--dsw-alias-state-warn-label);font-weight:600}',
38
+ '.sap-reminderUnread{border-color:var(--dsw-alias-brand-primary)}',
39
+ '.sap-reminderHead{display:flex;align-items:center;gap:6px;min-width:0}',
40
+ '.sap-reminderTitle{font-size:14px;font-weight:500;line-height:22px;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1}',
41
+ '.sap-reminderMeta{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
42
+ '.sap-reminderActions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;margin-top:6px}',
43
+ '.sap-reminderActions .sap-detailToggle,.sap-reminderActions .sap-dangerButton{border-radius:5px}',
44
+ '.sap-reminderFields{flex-direction:column;gap:4px;display:flex}',
45
+ '.sap-reminderField{display:flex;gap:6px;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);min-width:0;align-items:baseline}',
46
+ '.sap-reminderFieldLabel{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
47
+ '.sap-reminderFieldValue{min-width:0;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary)}',
48
+ '.sap-reminderJump{box-sizing:border-box;width:100%;border:none;background:0 0;font:inherit;text-align:left;cursor:pointer;padding:0;margin:0;display:flex;gap:6px;min-width:0;align-items:baseline;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
49
+ '.sap-reminderJump:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
50
+ '.sap-reminderJump:disabled{opacity:.4;cursor:default}',
51
+ '.sap-reminderJumpText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
52
+ '.sap-overlayBadge{position:fixed;top:12px;right:12px;z-index:9000;pointer-events:auto;flex-direction:column;gap:6px;align-items:flex-end;display:flex}',
53
+ '.sap-overlayBadgeBtn{box-sizing:border-box;border:none;border-radius:14px;background:var(--dsw-alias-state-error-primary);color:#fff;font:inherit;font-size:12px;line-height:18px;height:28px;padding:0 12px;cursor:pointer;font-weight:600}',
54
+ '.sap-overlayPanel{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-direction:column;gap:6px;padding:8px 10px;display:flex;max-width:360px}',
55
+ '.sap-overlayRow{align-items:center;gap:8px;display:flex;font-size:12px;line-height:18px}',
56
+ '.sap-statusBad{color:var(--dsw-alias-state-success-primary)}',
57
+ '.sap-statusBadFail{color:var(--dsw-alias-state-error-primary)}',
58
+ '.sap-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}',
59
+ '.sap-card + .sap-card{margin-top:16px}',
60
+ '.sap-cardHead{box-sizing:border-box;width:100%;border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;padding:12px 16px;display:flex;align-items:center;gap:8px}',
61
+ '.sap-cardTitleBox{flex:1;min-width:0}',
62
+ '.sap-cardTitle{font-size:14px;font-weight:550;line-height:20px}',
63
+ '.sap-cardDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
64
+ '.sap-cardToggle{cursor:pointer;transition:background .15s ease}',
65
+ '.sap-cardToggle:hover{background:var(--dsw-alias-interactive-bg-hover)}',
66
+ '.sap-cardChevron{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}',
67
+ '.sap-cardOpen .sap-cardChevron{transform:rotate(180deg)}',
68
+ '.sap-cardBody{display:none;border-top:1px solid var(--dsw-alias-border-l2);padding:4px 16px 14px}',
69
+ '.sap-cardOpen .sap-cardBody{display:block}',
70
+ '.sap-cardStatic .sap-cardHead{cursor:default}',
71
+ '.sap-cardStatic .sap-cardHead:hover{background:0 0}',
72
+ '.sap-cardStatic .sap-cardBody{display:block}',
73
+ '.sap-subgroup{flex-direction:column;gap:6px;padding:2px 0;display:flex}',
74
+ '.sap-subgroup + .sap-subgroup{margin-top:20px}',
75
+ '.sap-subgroupTitle{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-weight:600}',
76
+ '.sap-subgroupHint{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:18px;margin:0}',
77
+ '.sap-subgroup .sap-desc,.sap-subgroup .sap-costLine{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:18px}',
78
+ '.sap-costLine{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
26
79
  '.sap-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}',
27
80
  '.sap-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}',
28
81
  '.sap-notice{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}',
@@ -43,6 +96,7 @@ const CSS = [
43
96
  '.sap-chipLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
44
97
  '.sap-chipValue{color:var(--dsw-alias-label-primary)}',
45
98
  '.sap-chipValueDim{color:var(--dsw-alias-label-tertiary)}',
99
+ '.sap-chipDot{color:var(--dsw-alias-label-tertiary)}',
46
100
  '.sap-chipWarn{background:var(--dsw-alias-state-warn-tertiary);border-color:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font-weight:500}',
47
101
  '.sap-chipSuccess{color:var(--dsw-alias-state-success-primary);font-weight:500}',
48
102
  '.sap-chipDisabled{color:var(--dsw-alias-state-warn-label);font-weight:500}',
@@ -83,6 +137,15 @@ const CSS = [
83
137
  '.sap-switch input:checked{background:var(--dsw-alias-brand-primary)}',
84
138
  '.sap-switch input:checked::before{left:18px}',
85
139
  '.sap-toolview{flex-direction:column;align-items:flex-start;gap:6px;padding:4px 8px;display:flex}',
140
+ '.sap-toolMeta{flex-wrap:wrap;gap:4px 14px;display:flex}',
141
+ '.sap-draftCard{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;gap:8px;padding:10px 12px;display:flex}',
142
+ '.sap-drafts{flex-direction:column;gap:8px;display:flex}',
143
+ '.sap-costSection{flex-direction:column;gap:8px;display:flex}',
144
+ '.sap-infoLine{justify-content:space-between;gap:12px;display:flex}',
145
+ '.sap-infoLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;flex:none}',
146
+ '.sap-infoValue{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;text-align:right}',
147
+ '.sap-draftTitle{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
148
+ '.sap-toolMetaItem{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}',
86
149
  '.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
87
150
  '.sap-toolSummary{align-items:center;gap:6px;flex-wrap:wrap;display:flex}',
88
151
  '.sap-detailToggle{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);background:0 0;border-radius:10px;font:inherit;font-size:11px;line-height:16px;height:20px;padding:0 8px;cursor:pointer;flex:none;align-items:center;display:inline-flex}',
@@ -106,6 +169,58 @@ const CSS = [
106
169
  '.sap-ledgerStat{color:var(--dsw-alias-label-primary);font-weight:600}',
107
170
  '.sap-ledgerSep{color:var(--dsw-alias-label-tertiary)}',
108
171
  '.sap-ledgerRows{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}',
172
+ '.sap-ledgerRowFail{border-left:3px solid var(--dsw-alias-state-error-secondary)}',
173
+ '.sap-ledgerStatus{font-size:12px;line-height:18px;font-weight:600;flex:none}',
174
+ '.sap-ledgerStatusOk{color:var(--dsw-alias-state-success-primary)}',
175
+ '.sap-ledgerStatusFail{color:var(--dsw-alias-state-error-primary)}',
176
+ '.sap-ledgerStatusRun{color:var(--dsw-alias-state-warn-label)}',
177
+ '.sap-ledgerCostText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;flex:none}',
178
+ '.sap-ledgerToolsText{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;flex:none}',
179
+ '@keyframes sap-ledgerPulse{0%,100%{opacity:1}50%{opacity:.35}}',
180
+ '.sap-ledgerProgress{align-items:center;gap:6px;display:flex;flex-wrap:wrap}',
181
+ '.sap-ledgerProgressStep{align-items:center;gap:6px;display:inline-flex;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
182
+ '.sap-ledgerProgressDot{width:8px;height:8px;border-radius:50%;flex:none}',
183
+ '.sap-ledgerProgressDot-done{background:var(--dsw-alias-state-success-primary)}',
184
+ '.sap-ledgerProgressDot-run{background:var(--dsw-alias-state-warn-label);animation:sap-ledgerPulse 1.4s infinite}',
185
+ '.sap-ledgerProgressDot-todo{background:var(--dsw-alias-border-l3)}',
186
+ '.sap-ledgerProgressDot-warn{background:var(--dsw-alias-state-warn-label)}',
187
+ '.sap-ledgerProgressSep{width:14px;height:1px;background:var(--dsw-alias-border-l2);flex:none}',
188
+ '.sap-ledgerSafety{flex-wrap:wrap;gap:6px;display:flex}',
189
+ '.sap-ledgerBadge{font-size:12px;line-height:18px;padding:2px 10px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}',
190
+ '.sap-ledgerBadgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}',
191
+ '.sap-ledgerBadgeFail{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary);background:var(--dsw-alias-interactive-bg-hover-danger)}',
192
+ '.sap-ledgerBadgeWarn{color:var(--dsw-alias-state-warn-label);border-color:var(--dsw-alias-state-warn-label)}',
193
+ '.sap-ledgerCostCard{flex-wrap:wrap;gap:8px 24px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 14px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);display:flex}',
194
+ '.sap-ledgerCostCard b{color:var(--dsw-alias-label-primary);font-weight:600}',
195
+ '.sap-ledgerCostSave b{color:var(--dsw-alias-state-success-primary)}',
196
+ '.sap-ledgerRejectBlock{border:1px solid var(--dsw-alias-state-error-secondary);background:var(--dsw-alias-interactive-bg-hover-danger);border-radius:8px;flex-direction:column;gap:4px;padding:10px 14px;font-size:12px;line-height:18px;display:flex}',
197
+ '.sap-ledgerIntervention{color:var(--dsw-alias-state-warn-label);font-size:12px;line-height:18px}',
198
+ '.sap-ledgerEvidenceToggle{box-sizing:border-box;border:0;background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:18px;cursor:pointer;padding:2px 0;align-self:flex-start}',
199
+ '.sap-ledgerEvidenceToggle:hover{color:var(--dsw-alias-brand-primary)}',
200
+ '.sap-ledgerVsHead{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);margin:2px 0 4px}',
201
+ '.sap-ledgerVsTable{border-collapse:collapse;width:100%;font-size:12px;line-height:18px}',
202
+ '.sap-ledgerVsTable th{text-align:left;color:var(--dsw-alias-label-tertiary);font-weight:500;font-size:11px;line-height:16px;padding:4px 10px 4px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}',
203
+ '.sap-ledgerVsTable td{padding:6px 10px 6px 0;border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);vertical-align:top}',
204
+ '.sap-ledgerVsTable td:first-child{color:var(--dsw-alias-label-tertiary);width:60px}',
205
+ '.sap-ledgerVsTable tr.sap-ledgerVsDiff td{background:var(--dsw-alias-state-warn-tertiary)}',
206
+ '.sap-ledgerVsTable tr.sap-ledgerVsDiff .sap-ledgerVsEff{color:var(--dsw-alias-state-warn-label);font-weight:600}',
207
+ '.sap-ledgerVsNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
208
+ '.sap-ledgerToolScroll{max-height:220px;overflow-y:auto}',
209
+ '.sap-ledgerToolTable{border-collapse:collapse;width:100%;font-size:12px;line-height:18px}',
210
+ '.sap-ledgerToolTable th{text-align:left;color:var(--dsw-alias-label-tertiary);font-weight:500;font-size:11px;line-height:16px;padding:4px 10px 4px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}',
211
+ '.sap-ledgerToolTable td{padding:6px 10px 6px 0;border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}',
212
+ '.sap-ledgerToolName{font-family:monospace;font-size:11px}',
213
+ '.sap-ledgerEventLine{position:relative;padding-left:18px;margin-top:2px}',
214
+ '.sap-ledgerEventLine:before{content:"";position:absolute;left:5px;top:4px;bottom:4px;width:1px;background:var(--dsw-alias-border-l2)}',
215
+ '.sap-ledgerEvent{position:relative;padding:3px 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
216
+ '.sap-ledgerEvent:before{content:"";position:absolute;left:-17px;top:9px;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-border-l3)}',
217
+ '.sap-ledgerEvent-done:before{background:var(--dsw-alias-state-success-primary)}',
218
+ '.sap-ledgerEvent-warn{color:var(--dsw-alias-state-warn-label)}',
219
+ '.sap-ledgerEvent-warn:before{background:var(--dsw-alias-state-warn-label)}',
220
+ '.sap-ledgerEvent-int{color:var(--dsw-alias-brand-primary)}',
221
+ '.sap-ledgerEvent-int:before{background:var(--dsw-alias-brand-primary)}',
222
+ '.sap-ledgerEventTime{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;margin-right:8px}',
223
+ '.sap-ledgerEventAct{color:var(--dsw-alias-label-primary)}',
109
224
  '.sap-ledgerRow{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;display:flex}',
110
225
  '.sap-ledgerRowHead{box-sizing:border-box;width:100%;border:0;background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:10px;padding:10px 14px;font:inherit;text-align:left;flex-wrap:wrap;display:flex}',
111
226
  '.sap-ledgerRowHead:hover{background:var(--dsw-alias-interactive-bg-hover)}',
@@ -139,6 +254,13 @@ const CSS = [
139
254
  '.sap-ledgerRejectFix{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}',
140
255
  '.sap-ledgerUsage{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
141
256
  '.sap-ledgerManual{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;align-items:baseline;gap:6px;display:flex}',
257
+ '.sap-ledgerSource{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
258
+ '.sap-ledgerSourceDim{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-style:italic}',
259
+ // 次级说明小字:供安全检查卡片说明沿用(原跳变线右下角注记样式)
260
+ '.sap-cogJumpDim{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
261
+ '.sap-cogParentText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}',
262
+ '.sap-ledgerChildRow{align-items:center;gap:8px;display:flex}',
263
+ '.sap-ledgerChildCount{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
142
264
  '.sap-ledgerManualId{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-family:monospace;word-break:break-all}',
143
265
  '.sap-ledgerEmpty{flex-direction:column;gap:4px;padding:8px 2px;display:flex}',
144
266
  '.sap-ledgerEmptyTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}',
@@ -169,9 +291,9 @@ const CSS = [
169
291
  // 面向用户的固定文案。注意:此表仅集中 dispatch 卡片文案;设置页文案仍为
170
292
  // 散落硬编码,属待迁移债(迁移时一次性并入此表)。
171
293
  const ZH = {
172
- inlineProfile: '内联',
173
- inherit: '继承父',
174
- effortDefault: '默认(继承父)',
294
+ inlineProfile: '未存方案',
295
+ inherit: '继承父会话',
296
+ effortDefault: '默认(继承父会话)',
175
297
  chipProfile: '方案',
176
298
  chipPreset: '预设',
177
299
  chipProvider: '提供方',
@@ -188,22 +310,58 @@ const ZH = {
188
310
  canContinue: '可续跑',
189
311
  toolBackground: '后台任务',
190
312
  toolForeground: '前台',
313
+ chipMode: '模式',
314
+ modeForeground: '前台',
315
+ modeBackground: '后台',
316
+ modeContinuable: 'continuable',
317
+ bannerInheritValue: '继承父会话',
318
+ bannerModePersistent: '持久',
319
+ bannerOriginTitle: '来源:由父会话派发',
320
+ // 派发链路状态机 phase 徽标
321
+ phaseInitiate: '发起',
322
+ phaseGuards: '检查通过',
323
+ phaseCreate: '创建',
324
+ phaseRun: '执行中',
325
+ phaseDone: '完成',
326
+ phaseFailed: '失败',
327
+ phaseLegacy: '已结算(旧版)',
328
+ // 提醒系统
329
+ reminderCenterTitle: '提醒中心',
330
+ reminderEmpty: '暂无提醒(治理事件会在这里留痕)',
331
+ reminderAck: '标记已读',
332
+ reminderAdopt: '采纳',
333
+ reminderIgnore: '忽略',
334
+ reminderReject: '拒绝',
335
+ reminderDismiss: '关闭',
336
+ reminderGlobal: '全局',
337
+ reminderSessionLabel: '会话 ',
338
+ reminderP0Badge: '⚠ 紧急 {n}',
339
+ reminderActed: '已处理:{action}',
340
+ reminderActionZh: { adopt: '采纳', ignore: '忽略', reject: '拒绝', dismiss: '关闭' },
341
+ reminderViewChild: '查看子会话',
342
+ reminderParentLabel: '主会话',
343
+ reminderChildLabel: '子会话',
344
+ reminderTaskLabel: '任务',
345
+ reminderResultLabel: '结果',
346
+ reminderNoteLabel: '说明',
347
+ reminderNoSettlement: '未结算',
348
+ reminderNoExcerpt: '(未记录)',
191
349
  toolRunning: '运行中',
192
350
  tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
193
351
  stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
194
352
  cacheHit: '缓存命中',
195
353
  usageInput: '输入',
196
354
  usageOutput: '输出',
197
- usageCacheRead: '缓存读',
355
+ usageCacheRead: '缓存命中',
198
356
  usageCacheWrite: '缓存写',
199
357
  usageReasoning: '推理',
200
358
  persistFail: '已保存但未持久化',
201
359
  persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)',
202
360
  // 审计分级(设置页红字 + 丢失计数)
203
- auditDegraded: '审计记录写入异常(degraded):请检查磁盘',
361
+ auditDegraded: '审计记录写入异常(已降级):请检查磁盘',
204
362
  auditLost: '审计丢失计数',
205
- // 派发账本(conversation.view 会话标签页)文案
206
- ledgerTab: '派发账本',
363
+ // 派发决策台账(conversation.view 会话标签页)文案
364
+ ledgerTab: '派发决策台账',
207
365
  ledgerSummary: '本会话派发',
208
366
  ledgerDispatch: '派发',
209
367
  ledgerTimes: '次',
@@ -214,12 +372,12 @@ const ZH = {
214
372
  ledgerTotalElapsed: '总耗时',
215
373
  ledgerElapsed: '耗时',
216
374
  ledgerStopReason: '结束原因',
217
- ledgerGateRejectDist: '各闸拒绝分布',
375
+ ledgerGateRejectDist: '安全检查拒绝分布',
218
376
  ledgerByProfile: '按方案',
219
377
  ledgerByModel: '按模型',
220
378
  ledgerByTier: '按档位',
221
379
  ledgerEmpty: '本会话暂无派发记录',
222
- ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的派发决策轨迹。',
380
+ ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的决策轨迹。',
223
381
  ledgerLoadOlder: '加载更早',
224
382
  ledgerLoadingOlder: '加载中…',
225
383
  ledgerLegacy: '该记录产生于决策轨迹上线前,仅有结果摘要',
@@ -242,22 +400,128 @@ const ZH = {
242
400
  ledgerCallsTruncated: '仅显示前 {shown} 条,共 {total} 次调用',
243
401
  ledgerCallsIndex: '序号',
244
402
  ledgerExecutionStep: '执行信息',
245
- ledgerScopeNote: '本页仅展示本会话的派发决策轨迹;跨会话进化台账(dispatch.jsonl)与审计记录见设置页。',
403
+ cogTimelineTitle: '派发决策时间线',
404
+ cogPerceiveDecide: '① 发起',
405
+ cogDecide: '② 决策 · 生效值',
406
+ cogExecute: '③ 执行',
407
+ cogNewPerceive: '④ 结算',
408
+ // ── 三层信息架构(折叠态 / 结论层 / 证据层)文案 ──
409
+ ledgerStatusSuccess: '✅ 成功',
410
+ ledgerStatusReject: '❌ 拒绝',
411
+ ledgerStatusRunning: '🔄 执行中',
412
+ ledgerStatusAbnormal: '⚠ 异常',
413
+ ledgerCostRunning: '进行中…',
414
+ ledgerToolsPending: '未派发',
415
+ ledgerInterventionBadge: '⚠ 干预 {n} 次',
416
+ ledgerSafetyAllPass: '安全检查 ✅ 5/5 通过',
417
+ ledgerSafetyFail: '安全检查 ❌ 未通过({gate})',
418
+ ledgerSafetyBadge: '安全检查',
419
+ ledgerGateBadgeZh: { whitelist: '白名单', cost: '成本', intersection: '交集非空', approval: '审批不豁免', budget: '预算' },
420
+ ledgerProgressZh: { start: '发起', decide: '决策', run: '执行', settle: '结算', stop: '终止' },
421
+ ledgerProgressStepZh: { start: '① 发起', decide: '② 决策', run: '③ 执行', settle: '④ 结算' },
422
+ ledgerCostCardScope: '本会话口径',
423
+ ledgerCostCardCounter: '若继承父会话',
424
+ ledgerCostCardSave: '省',
425
+ ledgerEvidenceToggle: '查看完整因果链 ▾',
426
+ ledgerEvidenceClose: '收起因果链 ▴',
427
+ ledgerVsTitle: '请求 vs 生效',
428
+ ledgerVsDiff: '{n} 项与请求不同(琥珀底行)',
429
+ ledgerVsAllMatch: '请求全部生效 ✓',
430
+ ledgerVsItem: '项',
431
+ ledgerVsRequest: '请求',
432
+ ledgerVsEffective: '生效',
433
+ ledgerVsNote: '说明',
434
+ ledgerVsUnset: '未指定',
435
+ ledgerVsDefault: '方案默认',
436
+ ledgerVsInherit: '继承父会话({model})',
437
+ ledgerVsIgnored: '请求了 {v},未生效(被忽略)',
438
+ ledgerToolsetTitle: '工具集',
439
+ ledgerToolsetFormula: '白名单 {x} ∩ 父会话 {y} → 实得 {z}',
440
+ ledgerToolsetGranted: '由系统授予',
441
+ ledgerToolsetRemoved: '移除 {n} 个',
442
+ ledgerToolsetHostNarrow: '工具由系统最终授予(插件侧未收录移除明细)',
443
+ ledgerToolsetTruncated: '(清单已截断,前 {n} 项)',
444
+ ledgerToolRemoveRunCode: '安全策略:子代理不执行代码',
445
+ ledgerToolRemoveNotInWhitelist: '不在白名单',
446
+ ledgerToolRemoveNotInAllow: '不在方案白名单',
447
+ ledgerToolCol: '工具',
448
+ ledgerToolReasonCol: '移除原因',
449
+ ledgerPrecheckTitle: '派发前 · 安全检查',
450
+ ledgerParentSessionLabel: '父会话',
451
+ ledgerRequestHuman: '请求参数',
452
+ ledgerPromptQuote: '任务指令(派发前)',
453
+ ledgerDecisionQuote: '决策原文(派发后)',
454
+ ledgerExecInterval: '{start} → {end}(共 {dur})',
455
+ ledgerExecToolCalls: '工具调用 {n} 次',
456
+ ledgerExecLiveNote: '后台执行中:过程事件随活动更新',
457
+ ledgerEventStart: '开始执行',
458
+ ledgerEventCall: '第 {n} 次工具调用',
459
+ ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过阈值 {t}s)',
460
+ ledgerEventStalling: '⚠ 停滞中(已 {s}s 无活动)',
461
+ ledgerEventIntervene: '主 agent 干预',
462
+ ledgerEventResume: '恢复执行',
463
+ ledgerEventDone: '完成',
464
+ ledgerEventRunning: '执行中…',
465
+ ledgerEventMoreCalls: '…其间还有 {n} 次调用',
466
+ ledgerStallThresholdNote: '停滞阈值 30s(待实测)',
467
+ ledgerSettleOutput: '输出摘要',
468
+ ledgerSettleCostBreakdown: '成本分解',
469
+ ledgerSettleInheritNote: '未指定模型,按继承父会话估算',
470
+ ledgerSafetyRejectDist: '安全检查拒绝分布',
471
+ ledgerScopeNote: '本页仅展示本会话的决策轨迹,成本为「本会话」口径;跨会话进化台账、审计记录与「全局」成本口径见设置页。',
246
472
  ledgerWindowNote: '已加载最近 {n} 条,尚有更早记录',
247
- gateFixUnknown: '未能识别的闸拒绝,请核对请求参数',
473
+ ledgerCostLine: '本会话口径 · 估算成本 ≈{actual} 元(若继承父会话 ≈{counter} 元 · 省 ≈{saving} 元 · 约 {ratio}×)',
474
+ ledgerInheritCost: '继承父会话模型派发 {count} 次({ratio}%)——未指定模型,无降本机会',
475
+ costNoPrice: '无单价/无用量,成本不显示',
476
+ costInheritModel: '未指定模型(继承父会话)',
477
+ inheritModelShort: '继承父会话模型',
478
+ tokenDetailPrefix: 'Token:',
479
+ whyUsable: '安全检查',
480
+ toolAllowlist: '工具',
481
+ toolHostNarrow: '工具由系统最终授予',
482
+ gateFixUnknown: '未能识别的安全检查拒绝,请核对请求参数',
248
483
  ignoredChipTitle: '请求值 "{value}" 已忽略',
249
484
  ignoredChipTitleEmpty: '请求值已忽略',
250
- // 逃生舱(非 system-trust 预设放行)
251
- escapeTitle: '逃生舱(非 system-trust 预设放行)',
252
- escapeWarn: '逃生舱已开启:允许派发非 system-trust 预设。风险自负——其余三道安全闸仍全量生效,每个放行都会留审计记录。',
485
+ // 逃生舱(放行非官方预设)
486
+ escapeTitle: '逃生舱(放行非官方预设)',
487
+ escapeWarn: '逃生舱已开启:允许派发非官方预设。风险自负——其余安全检查仍全量生效,每个放行都会留审计记录。',
253
488
  escapeEmpty: '暂无放行预设',
254
489
  escapeAdd: '添加',
255
490
  escapeRemove: '移除',
256
- escapeInputPlaceholder: ' system-trust 预设 id',
491
+ escapeInputPlaceholder: '非官方预设 id',
257
492
  escapeAddEmpty: '逃生舱预设 id 不能为空',
258
- // 只读建议注入(evolution:advice)
259
- adviceTitle: '只读建议注入(evolution:advice)',
260
- adviceDesc: '开启后编排者会话注入派发统计与确定性建议(仅父 Agent、只读、默认关)'
493
+ // 派发优化建议注入(开关 + 只提示面板)
494
+ adviceTitle: '派发优化建议',
495
+ adviceSwitchOn: '开(只提示,不自动改配置)',
496
+ adviceSwitchOff: '关(只提示,不自动改配置)',
497
+ advicePanelEmpty: '暂无建议(需累计 ≥3 次派发且加权成功分低于阈值)',
498
+ advicePanelGlobal: '全局',
499
+ advicePanelReadonly: '(仅提示:统计与建议仅供参考,不改变派发行为)',
500
+ adviceDirUp: '可上调预算',
501
+ adviceDirDown: '建议降级',
502
+ adviceDirFlat: '维持现状',
503
+ adviceConfidence: '置信度',
504
+ adviceRowLocate: '定位方案',
505
+ adviceRowNoMatch: '无匹配方案',
506
+ costEditProfile: '定位方案',
507
+ costNoProfile: '无对应方案',
508
+ advancedSafetyLine: '安全保证:派发不豁免审批 · 子代理工具不会超出父会话 · 成本与深度有硬上限。',
509
+ resetAllBuiltins: '重置所有内置方案',
510
+ statusLost: '审计丢失',
511
+ profilesIntro: '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可在「高级」区重置回默认;每个方案可单独启用/禁用。',
512
+ draftSavedWithId: '已保存到 draft,可在设置页「方案管理」中应用;方案 id:{id}',
513
+ draftCopyId: '复制 id',
514
+ ledgerCopyTarget: '复制',
515
+ ledgerCopied: '已复制 ✓',
516
+ costEstimated: '估算成本',
517
+ costInherit: '继承父会话成本',
518
+ costSaving: '节省',
519
+ costPriced: '可计价',
520
+ costInheritRatio: '继承占比',
521
+ costByModel: '按模型',
522
+ draftPreview: '安全检查预览',
523
+ draftPreviewPass: '安全检查预览:全部通过 ✓',
524
+ draftPreviewFail: '安全检查预览:存在拒绝项'
261
525
  }
262
526
 
263
527
  // ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
@@ -265,36 +529,47 @@ const ZH = {
265
529
  const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
266
530
  const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
267
531
  const TOOL_MODES = [
268
- ['none', '不限制(继承父的全部工具,自动剔除 run_code)'],
532
+ ['none', '不限制(继承父会话的全部工具,自动剔除 run_code)'],
269
533
  ['allow', '白名单(只允许勾选的工具)'],
270
534
  ['deny', '黑名单(排除勾选的工具)'],
271
535
  ]
272
536
  const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
273
537
  const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
274
- // 派发账本:闸名中文标签(展示层同时保留英文技术标识)。
275
- const GATE_LABELS = { whitelist: '白名单', cost: '成本', intersection: '交集', approval: '审批', budget: '预算', other: '其它' }
276
- // 派发账本:失败闸的修复方向提示(指向设置页对应字段)。
538
+ // 派发决策台账:闸名中文标签(展示层同时保留英文技术标识)。
539
+ const GATE_LABELS = { whitelist: '白名单', cost: '成本', intersection: '交集非空', approval: '审批不豁免', budget: '预算', other: '其它' }
540
+
277
541
  const GATE_FIX_HINTS = {
278
- whitelist: '目标预设不在 system-trust 候选内,可在设置页改用受信任的预设',
542
+ whitelist: '目标预设不在候选白名单内,可在设置页改用受信任的预设',
279
543
  cost: '参数超出模型目录或成本上限,可在设置页调整提供方/模型/推理强度',
280
- intersection: '工具交集为空,可在设置页调整该方案的 toolFilter',
544
+ intersection: '方案勾选的工具父会话都没有,可在设置页调整该方案的「工具限制」',
281
545
  approval: '审批策略冲突,请核对审批配置',
282
546
  budget: '并发派发数或本会话累计 token 已达上限,请等待在途任务完成或开启新会话'
283
547
  }
284
- // 派发账本:决策轨迹字段键名 → 中文标签(字段级渲染去「JSON 墙」用)。
285
- // 未知键回退原样(不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
286
- const GATE_KEY_LABELS = {
287
- requestedPreset: '目标预设', systemTrustCandidates: '候选白名单', allowed: '放行', resolvedPreset: '解析后预设',
288
- checks: '检查项', field: '字段', checkedAgainst: '对照项', verdict: '结论', detail: '详情', policy: '策略', applied: '应用值',
289
- effectiveAllowCount: '生效工具数', parentSessionId: '父会话', childSessionId: '子会话', jobId: '任务ID', reason: '原因', limit: '上限',
290
- skipped: '跳过', allowFailOpen: '兼容模式', mode: '模式', maxTokens: 'token 上限', maxDepth: '深度上限',
291
- requestedToolFilter: '请求工具过滤', parentToolCount: '父工具数', providers: '提供方', modelEfforts: '模型支持档位',
292
- effectiveToolNames: '生效工具', preset: '目标预设', profile: '方案', provider: '提供方', model: '模型',
293
- reasoningEffort: '推理强度', tokenTier: '档位', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤',
294
- prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断', envelope: '信封模式', parentPreset: '父预设', parentProvider: '父提供方',
295
- parentModel: '父模型', outcome: '结果', status: '状态', stop_reason: '结束原因', elapsed_ms: '耗时',
548
+ // 派发决策台账:决策轨迹字段键名 → 中文标签(字段级渲染去「JSON 墙」用)。
549
+ // 派发决策台账:决策轨迹字段键名 → 中文标签(FIELD_ZH,字段级渲染去「JSON 墙」用)。
550
+ // 未知键回退原样(fail-soft 不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
551
+ const FIELD_ZH = {
552
+ parentPreset: '父预设', parentProvider: '父提供方', parentModel: '父模型', parentToolCount: '父工具数',
553
+ tokenTier: '档位', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤', envelope: '信封模式',
554
+ prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断',
555
+ systemTrustCandidates: '候选白名单', resolvedPreset: '解析后预设', allowed: '放行',
556
+ field: '检查项', checkedAgainst: '对照', verdict: '结论', cap: '上限',
557
+ policy: '策略', applied: '应用值',
558
+ mode: '模式', deferred: '延迟结算', background: '后台',
559
+ concurrency: '当前并发', tokens: '当前 token', maxConcurrent: '并发上限', maxParentTokens: '父会话 token 上限',
560
+ parentSessionId: '父会话', childSessionId: '子会话',
561
+ id: '方案 id', description: '描述', preset: '目标预设', profile: '方案',
562
+ requestedPreset: '目标预设', checks: '检查项', detail: '详情',
563
+ effectiveAllowCount: '生效工具数', jobId: '任务ID', reason: '原因', limit: '上限',
564
+ skipped: '跳过', allowFailOpen: '兼容模式', maxTokens: 'token 上限', maxDepth: '深度上限',
565
+ requestedToolFilter: '请求工具过滤', providers: '提供方', modelEfforts: '模型支持档位',
566
+ effectiveToolNames: '生效工具', provider: '提供方', model: '模型',
567
+ reasoningEffort: '推理强度',
568
+ unknown_keys: '未知字段(已被拒绝)',
569
+ outcome: '结果', status: '状态', stop_reason: '结束原因', elapsed_ms: '耗时',
296
570
  output_len: '输出长度', tool_count: '工具调用数'
297
571
  }
572
+
298
573
  const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
299
574
 
300
575
  // 审计分级的默认态(summary 缺 audit 字段 / 值非法时回退):health ok、零丢失。
@@ -375,6 +650,69 @@ function profileDisplay(profileId, profileNames) {
375
650
  return typeof name === 'string' && name !== '' ? `${name}(${profileId})` : profileId
376
651
  }
377
652
  const rawOrInherit = (v) => (v === '(parent)' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
653
+
654
+ // ── 省 token 证据(client 侧反事实对照)─────────────────────────
655
+ // 与 host 侧 lib/core/cost-evidence.mjs 同源同价(估算、非精确计费);只读展示。
656
+ const CLIENT_MODEL_AVG_COST = { 'deepseek-v4-flash': 0.0039, 'deepseek-v4-pro': 0.0104 }
657
+ const clientPriceKey = (model) => {
658
+ if (typeof model !== 'string' || model === '') return undefined
659
+ const m = model.trim().toLowerCase()
660
+ if (CLIENT_MODEL_AVG_COST[m] !== undefined) return m
661
+ if (m.includes('flash')) return 'deepseek-v4-flash'
662
+ if (m.includes('pro')) return 'deepseek-v4-pro'
663
+ return undefined
664
+ }
665
+ const clientAvgCost = (model) => {
666
+ const key = clientPriceKey(model)
667
+ return key === undefined ? undefined : CLIENT_MODEL_AVG_COST[key]
668
+ }
669
+ // 单条派发的估算成本(元)。trace 已归一;请求值 requested.model 才是「显式选了什么」,
670
+ // effective.model 会把继承父会话模型名写进去,不能用于判定继承。
671
+ function traceCostInfo(trace) {
672
+ if (!trace) return null
673
+ const requestedModel = trace.requested && typeof trace.requested.model === 'string' ? trace.requested.model : ''
674
+ const parentModel = trace.parentContext && typeof trace.parentContext.parentModel === 'string' ? trace.parentContext.parentModel : ''
675
+ if (requestedModel === '') {
676
+ const actual = clientAvgCost(parentModel)
677
+ if (actual === undefined) return null
678
+ return { inherit: true, actual, counterfactual: actual, saving: 0, ratio: null }
679
+ }
680
+ const actual = clientAvgCost(requestedModel)
681
+ const counterfactual = clientAvgCost(parentModel)
682
+ if (actual === undefined || counterfactual === undefined) return null
683
+ const saving = Number((counterfactual - actual).toFixed(6))
684
+ return { inherit: false, actual, counterfactual, saving, ratio: actual > 0 ? Number((counterfactual / actual).toFixed(2)) : null }
685
+ }
686
+ function recordCostInfo(record) {
687
+ return traceCostInfo(record && record.trace)
688
+ }
689
+
690
+ // 供 ② 决策段与行摘要 chip 使用;所有判定只读 trace 记录面,不做模型偏好推断。
691
+ function auditModelChoice(trace) {
692
+ if (!trace) return []
693
+ const requested = trace.requested || {}
694
+ const effective = trace.effective || {}
695
+ const out = []
696
+ const profile = typeof requested.profile === 'string' && requested.profile !== '' && requested.profile !== '(inline)' ? requested.profile : ''
697
+ const snapshot = Array.isArray(requested.profiles_snapshot) ? requested.profiles_snapshot : []
698
+ if (profile !== '' && snapshot.length > 0 && !snapshot.some((e) => e && e.id === profile)) {
699
+ out.push({ key: 'outside-snapshot', label: '快照外选择', title: '请求的方案不在记录面目录快照内(可能被截断或已变更)' })
700
+ }
701
+ const requestedModel = typeof requested.model === 'string' && requested.model !== '' ? requested.model : ''
702
+ const effectiveModel = typeof effective.model === 'string' && effective.model !== '' ? effective.model : ''
703
+ if (requestedModel !== '' && effectiveModel !== '' && requestedModel !== effectiveModel) {
704
+ out.push({ key: 'rewritten', label: '请求被改写', title: '请求模型与生效模型不一致(命中即 bug 信号)' })
705
+ }
706
+ const info = traceCostInfo(trace)
707
+ if (info && !info.inherit && info.actual !== undefined) {
708
+ const cheaper = snapshot.find((e) => e && typeof e.avgCost === 'number' && Number.isFinite(e.avgCost) && e.avgCost < info.actual * 0.7)
709
+ if (cheaper !== undefined) {
710
+ out.push({ key: 'cheaper-available', label: '更便宜候选可用', title: '目录内存在成本低 30% 以上的候选方案(仅供参考)' })
711
+ }
712
+ }
713
+ return out
714
+ }
715
+
378
716
  const effortLabel = (effort) => {
379
717
  const id = effort && effort.id
380
718
  return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
@@ -392,6 +730,16 @@ function deriveProviders(models) {
392
730
  return providers
393
731
  }
394
732
 
733
+ // provider 由 model 一对一派生(不可独立编辑):返回选中 model 的 { id, name };
734
+ // 未选中/未知 model 返回 null(调用方回退表单存量 provider 值照常展示)。
735
+ function providerOfModel(models, modelId) {
736
+ if (typeof modelId !== 'string' || modelId === '') return null
737
+ const model = Array.isArray(models) ? models.find((m) => m && m.id === modelId) : undefined
738
+ if (model === undefined) return null
739
+ const entry = deriveProviders(models).find((p) => p.id === model.provider)
740
+ return entry !== undefined ? entry : { id: model.provider, name: model.provider }
741
+ }
742
+
395
743
  // ── 纯函数:dispatch 块解析 ────────────────────────────────────────────────
396
744
 
397
745
  // 请求值:argsRaw(运行中 / 已结算两种 block 形态都有)。
@@ -411,7 +759,7 @@ function readDispatchRequest(block) {
411
759
  out.runInBackground = parsed.run_in_background === true
412
760
  }
413
761
  } catch {
414
- // unparsable argsRaw:保持默认(继承/内联口径)。
762
+ // unparsable argsRaw:保持默认(继承父会话 / 未存方案)。
415
763
  }
416
764
  return out
417
765
  }
@@ -475,44 +823,111 @@ function readDispatchResult(block) {
475
823
  return null
476
824
  }
477
825
 
478
- // childTotalTokens 文本后缀:宿主 render 行里的 `childTotalTokens=<number>` 经
479
- // parseDispatchText 解析为字符串,此处归一为「≈N tokens」。仅前台/后台 completed
480
- // 结算携带;无值返回空串(不显示)。渲染位置紧邻后续的耗时显示。
481
- function tokensSuffix(result, block) {
482
- let raw = null
826
+
827
+ // 结算数字提取:tokens 数与缓存命中分别取,供「≈N tokens(缓存命中 M)」
828
+ // 单括号合并展示。tokens 不可得返回 null。
829
+ function summaryNumbersOf(result, block) {
830
+ let tokens = null
483
831
  if (result && result.fields && result.fields.childTotalTokens !== undefined && result.fields.childTotalTokens !== '') {
484
- raw = result.fields.childTotalTokens
832
+ const n = typeof result.fields.childTotalTokens === 'number' ? result.fields.childTotalTokens : Number(result.fields.childTotalTokens)
833
+ if (Number.isFinite(n)) tokens = n
485
834
  } else if (block && block.meta && typeof block.meta === 'object' && block.meta.childTotalTokens !== undefined) {
486
- raw = block.meta.childTotalTokens
835
+ const n = typeof block.meta.childTotalTokens === 'number' ? block.meta.childTotalTokens : Number(block.meta.childTotalTokens)
836
+ if (Number.isFinite(n)) tokens = n
487
837
  }
488
- const n = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw !== '' ? Number(raw) : NaN)
489
- return Number.isFinite(n) ? ` · ≈${n} tokens` : ''
838
+ return tokens
490
839
  }
491
840
 
492
- // 耗时 + 结束原因后缀:宿主 render 行里的 `elapsedMs=<n>` / `stopReason=<enum>`
493
- // parseDispatchText 解析为字符串,此处归一为「耗时=<n>ms · stopReason=<中文映射>」。
494
- // 仅前台 completed 结算携带;后台结算经 job 结果呈现(不进 dispatch 卡片),
495
- // continuable 不结算、不携带。任一字段缺失即省略该段。
496
- function observabilitySuffix(result, block) {
841
+ // 耗时 + 结束原因提取:键名中文「耗时 X / 结束原因:Y」,
842
+ // 时间单位规则(≥1s 秒一位小数,<1s 毫秒)由 formatDurationMs 统一。
843
+ function summaryObservablesOf(result, block) {
497
844
  let elapsedRaw = null
498
845
  let stopRaw = null
499
846
  if (result && result.fields) {
500
847
  if (result.fields.elapsedMs !== undefined && result.fields.elapsedMs !== '') elapsedRaw = result.fields.elapsedMs
501
848
  if (result.fields.stopReason !== undefined && result.fields.stopReason !== '') stopRaw = result.fields.stopReason
502
849
  } else if (block && block.meta && typeof block.meta === 'object') {
503
- // 运行中(无结果)或旧版结果:回退调用块 meta(向前兼容,与 tokenTier 同口径)。
504
850
  if (block.meta.elapsedMs !== undefined) elapsedRaw = block.meta.elapsedMs
505
851
  if (block.meta.stopReason !== undefined) stopRaw = block.meta.stopReason
506
852
  }
507
853
  const ms = typeof elapsedRaw === 'number' ? elapsedRaw : (typeof elapsedRaw === 'string' && elapsedRaw !== '' ? Number(elapsedRaw) : NaN)
508
854
  const stopZh = typeof stopRaw === 'string' ? (ZH.stopReasonZh[stopRaw] || stopRaw) : ''
509
- if (!Number.isFinite(ms) && stopZh === '') return ''
510
- let out = ''
511
- if (Number.isFinite(ms)) out += ` · 耗时=${ms}ms`
512
- if (stopZh !== '') out += ` · stopReason=${stopZh}`
513
- return out
855
+ return { ms: Number.isFinite(ms) ? ms : null, stopZh }
856
+ }
857
+
858
+ // dispatch 卡片摘要行:分隔符统一「·」、键名中文、缓存命中并入
859
+ // token 括号、成本前缀「成本 ≈X 元」、括号单层不嵌套;拒绝时走拒绝分支。
860
+ function dispatchSummaryText(request, result, block, trace, kindLabel) {
861
+ const usage = usageFrom(result, block)
862
+ const numbers = summaryNumbersOf(result, block)
863
+ const cacheRead = usage && typeof usage.cacheReadTokens === 'number' && usage.cacheReadTokens > 0 ? usage.cacheReadTokens : 0
864
+ const rejected = trace !== null && Array.isArray(trace.gates) && trace.gates.some((g) => g && g.verdict === 'fail')
865
+ if (rejected) {
866
+ const gate = trace.gates.find((g) => g && g.verdict === 'fail')
867
+ const reason = gate && typeof gate.reason === 'string' && gate.reason !== '' ? gate.reason : ZH.ledgerReject
868
+ return [ZH.toolLabel, ZH.ledgerReject + ':' + reason, ZH.ledgerRejectFix + ':' + gateFixHint(gate ? gate.name : '')].join(' · ')
869
+ }
870
+ const parts = [ZH.toolLabel + ' · ' + kindLabel]
871
+ if (numbers !== null) parts.push('≈' + numbers + ' tokens' + (cacheRead > 0 ? '(' + ZH.cacheHit + ' ' + cacheRead + ')' : ''))
872
+ const obs = summaryObservablesOf(result, block)
873
+ if (obs.ms !== null) parts.push(ZH.ledgerElapsed + ' ' + formatDurationMs(obs.ms))
874
+ if (obs.stopZh !== '') parts.push(ZH.ledgerStopReason + ':' + obs.stopZh)
875
+ const info = traceCostInfo(trace)
876
+ if (info !== null) {
877
+ if (info.inherit) parts.push(ZH.costEstimated + ' ≈' + info.actual + ' 元 · ' + ZH.inheritModelShort)
878
+ else parts.push(ZH.costEstimated + ' ≈' + info.actual + ' 元(' + ZH.ledgerCostCardCounter + ' ≈' + info.counterfactual + ' 元 · ' + ZH.ledgerCostCardSave + ' ≈' + info.saving + ' 元)')
879
+ }
880
+ return parts.join(' · ')
514
881
  }
515
882
 
883
+ // 卡片安全检查行(差异驱动):全过「安全检查 ✅」单徽标;失败展开具体项
884
+ // (如「安全检查:交集非空 ❌ 为空」);审批不豁免并入五项徽标,不再单列。
885
+ function safetyLineText(trace) {
886
+ if (!trace || !Array.isArray(trace.gates) || trace.gates.length === 0) return ''
887
+ const names = ['whitelist', 'cost', 'intersection', 'approval', 'budget']
888
+ const found = names.map((n) => trace.gates.find((g) => g && g.name === n)).filter((g) => g !== undefined)
889
+ if (found.length === 0) return ''
890
+ const failed = found.filter((g) => g.verdict === 'fail')
891
+ if (failed.length > 0) {
892
+ return ZH.ledgerSafetyBadge + ':' + failed.map((g) => (ZH.ledgerGateBadgeZh[g.name] || g.name) + ' ❌ ' + (typeof g.reason === 'string' && g.reason !== '' ? g.reason : '')).join(' · ')
893
+ }
894
+ return ZH.ledgerSafetyBadge + ' ✅'
895
+ }
896
+
897
+ // 卡片工具行:continuable 有真实交集时显示数量(工具 34 → 32,剔
898
+ // run_code);非 continuable 诚实降级「工具由系统最终授予」。
899
+ function toolLineText(request, trace) {
900
+ const effective = trace && trace.effective
901
+ const count = effective && Number.isFinite(effective.effectiveToolCount) ? effective.effectiveToolCount : null
902
+ if (request && request.continuable) {
903
+ const allow = request.toolFilter && Array.isArray(request.toolFilter.allow) ? request.toolFilter.allow : null
904
+ if (allow !== null) {
905
+ const granted = trace && Array.isArray(trace.gates)
906
+ ? trace.gates.find((g) => g && g.name === 'intersection' && g.output && Number.isFinite(g.output.effectiveAllowCount))
907
+ : null
908
+ const m = granted && granted.output ? granted.output.effectiveAllowCount : allow.length
909
+ return ZH.toolAllowlist + ' ' + allow.length + ' → ' + m + '(剔 run_code)'
910
+ }
911
+ return ZH.toolHostNarrow
912
+ }
913
+ if (count !== null) return ZH.toolAllowlist + ' ' + count
914
+ return ZH.toolHostNarrow
915
+ }
916
+
917
+ // 详情 token 行:前缀「Token:」定义单位域,数字不带单位;
918
+ // 缓存读统一为「缓存命中」。
919
+ function usageDetailText(usage) {
920
+ if (usage === null || typeof usage !== 'object') return ''
921
+ const parts = []
922
+ const push = (label, value) => {
923
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) parts.push(label + ' ' + value)
924
+ }
925
+ push(ZH.usageInput, usage.inputTokens)
926
+ push(ZH.usageOutput, usage.outputTokens)
927
+ push(ZH.cacheHit, usage.cacheReadTokens)
928
+ push(ZH.usageReasoning, usage.reasoningTokens)
929
+ return parts.length > 0 ? ZH.tokenDetailPrefix + parts.join(' · ') : ''
930
+ }
516
931
  // 把 childUsage 的(render 行 JSON 字符串或 block.meta 对象)归一为五段对象;
517
932
  // 非 object / 非 number 段忽略,无任何有效段返回 null。可选字段(缓存读/写/推理)
518
933
  // 缺值省略键,与 host 侧 collectChildUsage 输出对齐。
@@ -548,15 +963,6 @@ function usageFrom(result, block) {
548
963
  return null
549
964
  }
550
965
 
551
- // 缓存命中后缀:childUsage.cacheReadTokens 存在且 >0 时追加「 · 缓存命中 M」到
552
- // ≈N tokens 之后(前导分隔符与 tokensSuffix/observabilitySuffix 同口径,即使
553
- // tokens 段缺失也不会粘在一起);无分解或缓存读为 0/缺失时返回空串。
554
- function cacheHitSuffix(usage) {
555
- if (usage === null) return ''
556
- const cacheRead = usage.cacheReadTokens
557
- return typeof cacheRead === 'number' && cacheRead > 0 ? ` · ${ZH.cacheHit} ${cacheRead}` : ''
558
- }
559
-
560
966
  // 五段分解行文案(纯函数,供卡片详情区渲染);无任何有效段返回空串。
561
967
  function usageBreakdownText(usage) {
562
968
  if (usage === null || typeof usage !== 'object') return ''
@@ -572,7 +978,7 @@ function usageBreakdownText(usage) {
572
978
  return parts.join(' · ')
573
979
  }
574
980
 
575
- // ── 纯函数:派发账本数据层(快照遍历 / 轨迹归一 / 汇总统计 / 失败合并)────────
981
+ // ── 纯函数:派发决策台账数据层(快照遍历 / 轨迹归一 / 汇总统计 / 失败合并)────
576
982
 
577
983
  // 把 string|number 归一为有限数,不可得返回 null(汇总与行级数字的统一口径)。
578
984
  function toFiniteNumber(v) {
@@ -652,18 +1058,398 @@ function readNodeTrace(node) {
652
1058
  return null
653
1059
  }
654
1060
 
1061
+ // ── 子会话页头 banner(层 1)─────────────────────────────────────
1062
+
1063
+ // 从父会话快照节点解析该子会话的 preset(本插件自产 dispatch 节点,零 host 依赖):
1064
+ // 按 decisionTrace.execution.childSessionId 匹配,读生效值 preset;解析不到返回 null(显「继承父会话」)。
1065
+ function parentDispatchPresetOf(parentNodes, childSessionId) {
1066
+ if (!Array.isArray(parentNodes) || typeof childSessionId !== 'string' || childSessionId === '') return null
1067
+ for (const node of parentNodes) {
1068
+ if (!node || typeof node !== 'object' || node.kind !== 'tool-result') continue
1069
+ if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
1070
+ const trace = readNodeTrace(node)
1071
+ const execChild = trace !== null && trace.execution !== null && typeof trace.execution === 'object' && typeof trace.execution.childSessionId === 'string' ? trace.execution.childSessionId : ''
1072
+ if (execChild !== childSessionId) continue
1073
+ const result = readDispatchResult(node)
1074
+ const preset = result !== null && result.fields !== null && typeof result.fields === 'object' && typeof result.fields.preset === 'string' && result.fields.preset !== '' ? result.fields.preset : ''
1075
+ return preset !== '' ? preset : 'inherit'
1076
+ }
1077
+ return null
1078
+ }
1079
+
1080
+ // 从父会话快照节点解析该子会话的派发模式(前台/后台/持久):按
1081
+ // decisionTrace.execution.childSessionId 匹配 execution.kind;后台派发在父 trace
1082
+ // 无 childSessionId,匹配不到返回 null(调用方经 /ledger/jobs 兜底识别)。
1083
+ function parentDispatchKindOf(parentNodes, childSessionId) {
1084
+ if (!Array.isArray(parentNodes) || typeof childSessionId !== 'string' || childSessionId === '') return null
1085
+ for (const node of parentNodes) {
1086
+ if (!node || typeof node !== 'object' || node.kind !== 'tool-result') continue
1087
+ if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
1088
+ const trace = readNodeTrace(node)
1089
+ const execution = trace !== null && trace.execution !== null && typeof trace.execution === 'object' ? trace.execution : null
1090
+ const execChild = execution !== null && typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
1091
+ if (execChild !== childSessionId) continue
1092
+ const kind = execution !== null && typeof execution.kind === 'string' ? execution.kind : ''
1093
+ return kind === 'continuable' ? 'continuable' : (kind === 'background' ? 'background' : 'foreground')
1094
+ }
1095
+ return null
1096
+ }
1097
+
1098
+ // banner 数据装载(副作用助手):preset/模式解析自父会话快照,model/effort 订阅每会话
1099
+ // 共享目录 store(ModelDirectoryResolver,宿主唯一事实源);均 fail-soft,不可得显
1100
+ // 「继承父会话」。后台派发父 trace 无 childSessionId 匹配时,经 /ledger/jobs 按子会话 id 兜底。
1101
+ function loadSubagentBanner(subagent, sessionId, setParentPreset, setModelState, getModelDirectories, getSessions, setBannerMode) {
1102
+ const address = subagent.address !== null && typeof subagent.address === 'object' ? subagent.address : {}
1103
+ const parentId = typeof address.parentSessionId === 'string' ? address.parentSessionId : ''
1104
+ const childSessionId = typeof address.childSessionId === 'string' ? address.childSessionId : ''
1105
+ try {
1106
+ const parent = getSessions().binding(parentId)
1107
+ const nodes = parent !== undefined && parent !== null && parent.session !== undefined ? parent.session.chat?.legacy?.nodes : undefined
1108
+ setParentPreset(parentDispatchPresetOf(nodes, childSessionId) ?? '')
1109
+ const kind = parentDispatchKindOf(nodes, childSessionId)
1110
+ if (kind !== null) setBannerMode(kind)
1111
+ else if (parentId !== '' && childSessionId !== '') {
1112
+ api('/ledger/jobs?session=' + encodeURIComponent(parentId)).then((data) => {
1113
+ const jobs = Array.isArray(data.jobs) ? data.jobs : []
1114
+ if (jobs.some((job) => job !== null && typeof job === 'object' && job.childSessionId === childSessionId)) setBannerMode('background')
1115
+ }).catch(() => {})
1116
+ }
1117
+ } catch { setParentPreset('') }
1118
+ try {
1119
+ const directories = getModelDirectories()
1120
+ if (directories === undefined || directories === null || typeof directories.directoryFor !== 'function') { setModelState(null); return undefined }
1121
+ const dir = directories.directoryFor(sessionId)
1122
+ const store = dir !== null && typeof dir === 'object' && dir.store !== null && typeof dir.store === 'object' ? dir.store : null
1123
+ if (store === null || typeof store.getSnapshot !== 'function') { setModelState(null); return undefined }
1124
+ setModelState(store.getSnapshot())
1125
+ return typeof store.subscribe === 'function' ? store.subscribe(() => setModelState(store.getSnapshot())) : undefined
1126
+ } catch { setModelState(null); return undefined }
1127
+ }
1128
+
1129
+ // banner 模式短词:持久/后台/前台(fail-soft,未知值按前台)。
1130
+ function bannerModeText(mode) {
1131
+ if (mode === 'continuable') return ZH.bannerModePersistent
1132
+ if (mode === 'background') return ZH.modeBackground
1133
+ return ZH.modeForeground
1134
+ }
1135
+
1136
+ function bannerModelText(model) {
1137
+ return model !== '' ? model : ZH.bannerInheritValue
1138
+ }
1139
+
1140
+ function bannerEffortText(effort) {
1141
+ if (effort === '') return ZH.bannerInheritValue
1142
+ return EFFORT_ZH[effort] !== undefined ? EFFORT_ZH[effort] : effort
1143
+ }
1144
+
1145
+ function bannerPresetText(preset) {
1146
+ return preset !== '' && preset !== 'inherit' ? presetZh(preset) : ZH.bannerInheritValue
1147
+ }
1148
+
1149
+ // 摘要:全继承场景固定「继承父会话 · 模式」;有值场景按「模型 · 强度 · 模式」摘要。
1150
+ function bannerSummaryText(model, effort, preset, mode) {
1151
+ const inherited = model === '' && effort === '' && (preset === '' || preset === 'inherit')
1152
+ if (inherited) return ZH.bannerInheritValue + ' · ' + bannerModeText(mode)
1153
+ return [bannerModelText(model), bannerEffortText(effort), bannerModeText(mode)].join(' · ')
1154
+ }
1155
+
1156
+ // title 全量详情(原生多行悬停):模型/推理强度/预设/模式/来源(完整父会话 id)。
1157
+ function bannerTitleText(model, effort, preset, mode, parentId) {
1158
+ return [
1159
+ ZH.chipModel + ':' + bannerModelText(model),
1160
+ ZH.chipEffort + ':' + bannerEffortText(effort),
1161
+ ZH.chipPreset + ':' + bannerPresetText(preset),
1162
+ ZH.chipMode + ':' + bannerModeText(mode),
1163
+ ZH.bannerOriginTitle + (parentId !== '' ? '(' + parentId + ')' : '')
1164
+ ].join('\n')
1165
+ }
1166
+
1167
+ // banner 渲染:单一摘要标签 + title 全量详情。标签上限 260px 且 ellipsis,任何窗口
1168
+ // 宽度下宿主 Session log/返回按钮完整可见;来源不再单独渲染常驻行。
1169
+ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode) {
1170
+ const current = modelState !== null && typeof modelState === 'object' && modelState.current !== null && typeof modelState.current === 'object' ? modelState.current : null
1171
+ const model = current !== null && current !== undefined && typeof current.model === 'string' ? current.model : ''
1172
+ const effort = current !== null && current !== undefined && typeof current.reasoningEffort === 'string' ? current.reasoningEffort : ''
1173
+ const address = subagent.address !== null && typeof subagent.address === 'object' ? subagent.address : {}
1174
+ const addressMode = typeof address.mode === 'string' && address.mode !== '' ? address.mode : 'one-shot'
1175
+ const mode = bannerMode !== '' ? bannerMode : addressMode
1176
+ const parentId = typeof address.parentSessionId === 'string' ? address.parentSessionId : ''
1177
+ return el('div', { className: 'sap-subagentBanner' },
1178
+ el('span', { className: 'sap-subagentBannerTag', title: bannerTitleText(model, effort, parentPreset, mode, parentId) },
1179
+ bannerSummaryText(model, effort, parentPreset, mode)
1180
+ )
1181
+ )
1182
+ }
1183
+
1184
+ // ── 提醒系统 ─────────────────────────────────────────────────────
1185
+
1186
+ // 通知中心(设置页高级区内):读 /reminders(与 [6] 审计同源 host store),全动作
1187
+ // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1188
+ // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1189
+ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1190
+ function ReminderCenter() {
1191
+ const [reminders, setReminders] = useState([])
1192
+ const [error, setError] = useState('')
1193
+ const refresh = () => {
1194
+ api('/reminders').then((data) => setReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
1195
+ }
1196
+ useEffect(() => { refresh() }, [])
1197
+ const act = (id, action) => () => {
1198
+ setError('')
1199
+ const request = action === 'ack' ? api('/reminders/ack', { ids: [id] }) : api('/reminders/action', { id, action })
1200
+ request.then(refresh).catch((err) => setError(String((err && err.message) || err)))
1201
+ }
1202
+ return buildReminderCenter(el, reminders, error, act, getSessions(), openChild)
1203
+ }
1204
+ return ReminderCenter
1205
+ }
1206
+
1207
+ function buildReminderCenter(el, reminders, error, act, sessions, openChild) {
1208
+ return el('div', { className: 'sap-rowCard' },
1209
+ el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName' }, ZH.reminderCenterTitle)),
1210
+ error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1211
+ reminders.length === 0
1212
+ ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1213
+ : el('ul', { className: 'sap-rows' }, reminders.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1214
+ )
1215
+ }
1216
+
1217
+ // 提醒会话标识:非全局时截断前 8 位加省略号(悬停显示完整会话标识),避免长串撑破卡片。
1218
+ function reminderSessionText(sessionId) {
1219
+ if (typeof sessionId !== 'string' || sessionId === '') return { text: ZH.reminderGlobal, title: ZH.reminderGlobal }
1220
+ return { text: ZH.reminderSessionLabel + sessionId.slice(0, 8) + '…', title: sessionId }
1221
+ }
1222
+
1223
+ // 会话标题查询:宿主 sessions 列表快照的 durable title 优先,其次 displayTitle
1224
+ // (与 id 相同视为无标题);快照缺失/异常返回空串(调用方退化 ID 截断)。
1225
+ function reminderSessionTitle(sessions, id) {
1226
+ try {
1227
+ const summary = sessions.list.getSnapshot().byId[id]
1228
+ if (summary === undefined || summary === null) return ''
1229
+ if (typeof summary.title === 'string' && summary.title !== '') return summary.title
1230
+ const display = typeof summary.displayTitle === 'string' ? summary.displayTitle : ''
1231
+ return display !== '' && display !== id ? display : ''
1232
+ } catch {
1233
+ return ''
1234
+ }
1235
+ }
1236
+
1237
+ // 主/子会话跳转行文案:有标题为「主会话 {标题} · id截断」,查不到退化为
1238
+ // 「主会话 · id截断」;title 属性悬停显示完整 id。
1239
+ function reminderJumpText(sessions, id, label) {
1240
+ const shortId = typeof id === 'string' && id !== '' ? id.slice(0, 8) + '…' : ''
1241
+ const title = reminderSessionTitle(sessions, id)
1242
+ const body = title !== '' ? title + ' · ' + shortId : shortId
1243
+ return { label, body, title: typeof id === 'string' ? id : '' }
1244
+ }
1245
+
1246
+ // tracker 内部 mode → 宿主 SubagentAddress mode(前台/后台 one-shot 归一)。
1247
+ function reminderChildMode(mode) {
1248
+ return mode === 'continuable' ? 'continuable' : 'one-shot'
1249
+ }
1250
+
1251
+ // 未采纳条目结果行:stopReason 中文 · 耗时 · 估算成本;结算摘要缺失显示未结算。
1252
+ function reminderResultText(r) {
1253
+ const settled = r.settled
1254
+ if (settled === null || typeof settled !== 'object') return ZH.reminderNoSettlement
1255
+ const parts = []
1256
+ const stop = typeof settled.stopReason === 'string' ? (ZH.stopReasonZh[settled.stopReason] || settled.stopReason) : ''
1257
+ if (stop !== '') parts.push(stop)
1258
+ const elapsed = formatDurationMs(settled.elapsedMs)
1259
+ if (elapsed !== '') parts.push(ZH.ledgerElapsed + ' ' + elapsed)
1260
+ const cost = typeof r.costEstimated === 'number' && Number.isFinite(r.costEstimated) ? '≈' + r.costEstimated + ' 元' : ''
1261
+ if (cost !== '') parts.push(cost)
1262
+ return parts.length > 0 ? parts.join(' · ') : ZH.reminderNoSettlement
1263
+ }
1264
+
1265
+ function reminderFieldLine(el, label, value) {
1266
+ return el('p', { className: 'sap-reminderField' },
1267
+ el('span', { className: 'sap-reminderFieldLabel' }, label), ' ',
1268
+ el('span', { className: 'sap-reminderFieldValue' }, value)
1269
+ )
1270
+ }
1271
+
1272
+ function reminderJumpButton(el, jump, disabled, onClick) {
1273
+ return el('button', { type: 'button', className: 'sap-reminderJump', title: jump.title, disabled: disabled === true, onClick },
1274
+ el('span', { className: 'sap-reminderFieldLabel' }, jump.label), ' ',
1275
+ el('span', { className: 'sap-reminderJumpText' }, jump.body)
1276
+ )
1277
+ }
1278
+
1279
+ // 未采纳条目五字段区:主会话/子会话可点击跳转,任务/结果/说明只读展示。
1280
+ function buildUnadoptedFields(el, r, sessions, openChild) {
1281
+ const parentJump = reminderJumpText(sessions, r.sessionId, ZH.reminderParentLabel)
1282
+ const childJump = reminderJumpText(sessions, r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId, ZH.reminderChildLabel)
1283
+ const openParent = () => { try { sessions.open(r.sessionId) } catch { /* 会话不可用:静默 */ } }
1284
+ const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1285
+ ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
1286
+ : null
1287
+ const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1288
+ const excerpt = typeof r.promptExcerpt === 'string' && r.promptExcerpt !== '' ? '「' + r.promptExcerpt + '」' : ZH.reminderNoExcerpt
1289
+ return el('div', { className: 'sap-reminderFields' },
1290
+ reminderJumpButton(el, parentJump, typeof r.sessionId !== 'string' || r.sessionId === '', openParent),
1291
+ reminderJumpButton(el, childJump, childAddress === null, jumpChild),
1292
+ reminderFieldLine(el, ZH.reminderTaskLabel, excerpt),
1293
+ reminderFieldLine(el, ZH.reminderResultLabel, reminderResultText(r)),
1294
+ reminderFieldLine(el, ZH.reminderNoteLabel, r.detail)
1295
+ )
1296
+ }
1297
+
1298
+ // 未采纳条目按钮四件套:查看子会话(跳转)/ 采纳(审计回写,未采纳即可用)/
1299
+ // 标记已读(未读显示)/ 关闭。已采纳后不再重复展示采纳。
1300
+ function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1301
+ return el('div', { className: 'sap-reminderActions' }, [
1302
+ el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null, onClick: jumpChild }, ZH.reminderViewChild),
1303
+ r.action !== 'adopt'
1304
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'adopt') }, ZH.reminderAdopt)
1305
+ : null,
1306
+ r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1307
+ el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1308
+ ])
1309
+ }
1310
+
1311
+ // 未采纳提醒条目(完整版):标题 + 五字段 + 按钮四件套 + 已处理标注。
1312
+ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1313
+ const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1314
+ const severityText = r.severity === 'P0' ? '紧急' : '注意'
1315
+ const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1316
+ ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
1317
+ : null
1318
+ const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1319
+ return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1320
+ el('div', { className: 'sap-reminderHead' },
1321
+ el('span', { className: 'sap-chip ' + severityCls }, severityText),
1322
+ el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1323
+ ),
1324
+ buildUnadoptedFields(el, r, sessions, openChild),
1325
+ buildUnadoptedActions(el, r, act, childAddress, jumpChild),
1326
+ typeof r.action === 'string' && r.action !== ''
1327
+ ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1328
+ : null
1329
+ )
1330
+ }
1331
+
1332
+ function buildReminderRow(el, r, act, sessions, openChild) {
1333
+ if (r.kind === 'adoption-false') return buildUnadoptedReminderRow(el, r, act, sessions, openChild)
1334
+ const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1335
+ const severityText = r.severity === 'P0' ? '紧急' : '注意'
1336
+ const session = reminderSessionText(r.sessionId)
1337
+ return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1338
+ el('div', { className: 'sap-reminderHead' },
1339
+ el('span', { className: 'sap-chip ' + severityCls }, severityText),
1340
+ el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1341
+ ),
1342
+ el('div', { className: 'sap-reminderMeta', title: session.title }, session.text),
1343
+ el('p', { className: 'sap-desc' }, r.detail),
1344
+ el('div', { className: 'sap-reminderActions' }, [
1345
+ r.unread === true ? el('button', { className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1346
+ el('button', { className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1347
+ ]),
1348
+ typeof r.action === 'string' && r.action !== ''
1349
+ ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1350
+ : null
1351
+ )
1352
+ }
1353
+
1354
+ // P0 角标(shell.overlay 根悬浮层):红色常驻角标 + 未读 P0 数 + 最近一条的 session
1355
+ // 标识;点击展开面板逐条「标记已读」。P0 三要件:①severity 上色(红)②常驻至已读
1356
+ // (unread>0 恒渲染,绝不自动消失)③带 session 标识(全局异常显「全局」)。
1357
+ // 高价值提醒无 toast(toast 只留写失败瞬态,本版不实现)。
1358
+ function makeReminderOverlayBadge(el, useState, useEffect) {
1359
+ function ReminderOverlayBadge() {
1360
+ const [data, setData] = useState({ reminders: [], unread: 0 })
1361
+ const [open, setOpen] = useState(false)
1362
+ const refresh = () => {
1363
+ api('/reminders').then((d) => setData({ reminders: Array.isArray(d.reminders) ? d.reminders : [], unread: typeof d.unread === 'number' ? d.unread : 0 })).catch(() => {})
1364
+ }
1365
+ useEffect(() => {
1366
+ refresh()
1367
+ const timer = setInterval(refresh, 10000)
1368
+ return () => clearInterval(timer)
1369
+ }, [])
1370
+ const p0Unread = data.reminders.filter((r) => r.severity === 'P0' && r.unread === true)
1371
+ if (p0Unread.length === 0) return null
1372
+ const latest = p0Unread[0]
1373
+ const session = reminderSessionText(latest.sessionId)
1374
+ return el('div', { className: 'sap-overlayBadge' },
1375
+ el('button', { type: 'button', className: 'sap-overlayBadgeBtn', onClick: () => setOpen((prev) => !prev) }, ZH.reminderP0Badge.replace('{n}', String(p0Unread.length)) + ' · ' + session.text),
1376
+ open ? el('div', { className: 'sap-overlayPanel' },
1377
+ p0Unread.map((r) => el('div', { key: String(r.id), className: 'sap-overlayRow' },
1378
+ el('span', null, r.title + '(' + reminderSessionText(r.sessionId).text + ')'),
1379
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: () => { api('/reminders/ack', { ids: [r.id] }).then(refresh).catch(() => {}) } }, ZH.reminderAck)
1380
+ ))
1381
+ ) : null
1382
+ )
1383
+ }
1384
+ return ReminderOverlayBadge
1385
+ }
1386
+
1387
+ // 子会话页头 utilities 组件(conversation.session.header.utilities 槽):仅在子会话渲染。
1388
+ // bannerMode 为空时回退地址 mode(one-shot/continuable);父快照解析到的前台/后台/
1389
+ // 持久模式优先,后台经 /ledger/jobs 兜底异步回填。
1390
+ function makeSubagentHeaderUtility(el, useState, useEffect, getModelDirectories, getSessions) {
1391
+ function SubagentHeaderUtility({ useSession, sessionId }) {
1392
+ const subagent = useSession((s) => (s !== undefined && s !== null && s.subagent !== null && s.subagent !== undefined ? s.subagent : null))
1393
+ const [parentPreset, setParentPreset] = useState('')
1394
+ const [modelState, setModelState] = useState(null)
1395
+ const [bannerMode, setBannerMode] = useState('')
1396
+ useEffect(() => {
1397
+ if (subagent === null) return
1398
+ return loadSubagentBanner(subagent, sessionId, setParentPreset, setModelState, getModelDirectories, getSessions, setBannerMode)
1399
+ }, [subagent])
1400
+ if (subagent === null) return null
1401
+ return buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1402
+ }
1403
+ return SubagentHeaderUtility
1404
+ }
1405
+
655
1406
  // 快照遍历:过滤 chat.legacy.nodes 中 kind==='tool-result' 且 call.name==='dispatch'
656
- // 的节点,产出账本记录。记录携带 callId/time/isError/trace(已归一,null 即旧记录
1407
+ // 的节点,产出台账记录。记录携带 callId/time/isError/trace(已归一,null 即旧记录
657
1408
  // 降级 legacy)/request/result/argsSummary/contentFirstLine。输入为空或非数组返回
658
1409
  // 空数组;逐节点守卫、坏节点跳过。
1410
+ // 派发节点前后的可见父文字(非 CoT):向前/向后各取 1 条带文本的非 tool-result 节点,
1411
+ // 用于「父消息(派发前/后)」上下文展示,不读取内部推理。
1412
+ function surroundingText(nodes, index) {
1413
+ if (!Array.isArray(nodes)) return { before: '', after: '' }
1414
+ const pick = (dir) => {
1415
+ let i = index + dir
1416
+ while (i >= 0 && i < nodes.length) {
1417
+ const node = nodes[i]
1418
+ if (node && typeof node === 'object' && node.kind !== 'tool-result' && node.kind !== 'tool-call') {
1419
+ const text = firstContentLine(node.content)
1420
+ if (text !== '') return text.slice(0, 200)
1421
+ }
1422
+ i += dir
1423
+ }
1424
+ return ''
1425
+ }
1426
+ return { before: pick(-1), after: pick(1) }
1427
+ }
1428
+
1429
+ // 台账 tab 后台任务 live 轮询的待配对 jobId:仅未结算的后台派发记录(前台/continuable
1430
+ // /已结算不启动)。来自快照 trace.execution,无 jobId 忽略。
1431
+ function pendingBackgroundJobIds(nodes) {
1432
+ const out = []
1433
+ for (const record of collectDispatchRecords(nodes)) {
1434
+ const trace = record && record.trace
1435
+ if (trace === null || trace === undefined || trace.execution === null || typeof trace.execution !== 'object' || trace.execution.kind !== 'background') continue
1436
+ if (trace.settled !== null && trace.settled !== undefined) continue
1437
+ const jobId = trace.execution !== null && typeof trace.execution.jobId === 'string' ? trace.execution.jobId : ''
1438
+ if (jobId !== '') out.push(jobId)
1439
+ }
1440
+ return out
1441
+ }
1442
+
659
1443
  function collectDispatchRecords(nodes) {
660
1444
  if (!Array.isArray(nodes)) return []
661
1445
  const records = []
662
- for (const node of nodes) {
1446
+ for (let index = 0; index < nodes.length; index += 1) {
1447
+ const node = nodes[index]
663
1448
  if (!node || typeof node !== 'object') continue
664
1449
  if (node.kind !== 'tool-result') continue
665
1450
  if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
666
1451
  const trace = readNodeTrace(node)
1452
+ if (trace !== null) trace.parentText = surroundingText(nodes, index)
667
1453
  records.push({
668
1454
  callId: typeof node.callId === 'string' ? node.callId : '',
669
1455
  time: typeof node.time === 'number' ? node.time : (typeof node.callTime === 'number' ? node.callTime : null),
@@ -734,26 +1520,48 @@ function recordProfile(record) {
734
1520
  return ''
735
1521
  }
736
1522
 
737
- // 会话汇总:与账本汇总带对齐。records 为合并后的完整记录列表(含失败记录)。
1523
+ // 累加单条记录的反事实成本到汇总。
1524
+ function accumulateRecordCost(summary, record) {
1525
+ const cost = recordCostInfo(record)
1526
+ if (cost === null) return
1527
+ summary.pricedCount += 1
1528
+ summary.estimatedCost += cost.actual
1529
+ summary.estimatedInheritCost += cost.counterfactual
1530
+ summary.estimatedSaving += cost.saving
1531
+ if (cost.inherit) summary.inheritCount += 1
1532
+ }
1533
+
1534
+ // 成本汇总定稿:四舍五入到可展示精度,继承占比按百分数取整。
1535
+ function finalizeLedgerCost(summary) {
1536
+ summary.estimatedCost = Number(summary.estimatedCost.toFixed(4))
1537
+ summary.estimatedInheritCost = Number(summary.estimatedInheritCost.toFixed(4))
1538
+ summary.estimatedSaving = Number(summary.estimatedSaving.toFixed(4))
1539
+ summary.inheritRatio = summary.pricedCount > 0 ? Number(((summary.inheritCount / summary.pricedCount) * 100).toFixed(0)) : 0
1540
+ }
1541
+
1542
+ // 会话汇总:与台账汇总带对齐。records 为合并后的完整记录列表(含失败记录)。
738
1543
  // 旧记录贡献可得字段(次数/耗时/token),不可得记 0/省略;闸拒绝按 trace.gates
739
1544
  // 的 fail verdict 分布。byProfile/byModel/byTier 均按次数降序。
740
1545
  function computeLedgerSummary(records) {
741
- const list = Array.isArray(records) ? records : []
742
- const summary = {
1546
+ const list = Array.isArray(records) ? records : []; const summary = {
743
1547
  dispatchCount: list.length,
744
1548
  successCount: 0,
745
1549
  rejectCount: 0,
746
1550
  totalTokens: 0,
747
1551
  cacheHitTokens: 0,
748
1552
  totalElapsedMs: 0,
1553
+ estimatedCost: 0,
1554
+ estimatedInheritCost: 0,
1555
+ estimatedSaving: 0,
1556
+ inheritCount: 0,
1557
+ inheritRatio: 0,
1558
+ pricedCount: 0,
749
1559
  gateRejectCounts: { whitelist: 0, cost: 0, intersection: 0, approval: 0, other: 0 },
750
1560
  byProfile: [],
751
1561
  byModel: [],
752
1562
  byTier: []
753
1563
  }
754
- const profileMap = new Map()
755
- const modelMap = new Map()
756
- const tierMap = new Map()
1564
+ const profileMap = new Map(), modelMap = new Map(), tierMap = new Map()
757
1565
  for (const record of list) {
758
1566
  if (recordIsRejected(record)) summary.rejectCount += 1
759
1567
  else summary.successCount += 1
@@ -761,10 +1569,11 @@ function computeLedgerSummary(records) {
761
1569
  if (tokens !== null) summary.totalTokens += tokens
762
1570
  summary.cacheHitTokens += cacheRead
763
1571
  if (elapsedMs !== null) summary.totalElapsedMs += elapsedMs
1572
+ accumulateRecordCost(summary, record)
764
1573
  countGateRejects(record, summary.gateRejectCounts)
765
1574
  const profile = recordProfile(record)
766
1575
  if (profile !== '') profileMap.set(profile, (profileMap.get(profile) || 0) + 1)
767
- // 模型:空/'(parent)'(继承父标记)并入继承桶(键为空串,展示层经 rawOrInherit 归一)。
1576
+ // 模型:空/'(parent)'(继承父会话标记)并入继承桶(键为空串,展示层经 rawOrInherit 归一)。
768
1577
  const model = recordModel(record)
769
1578
  if (model === '' || model === '(parent)') modelMap.set('', (modelMap.get('') || 0) + 1)
770
1579
  else modelMap.set(model, (modelMap.get(model) || 0) + 1)
@@ -780,6 +1589,7 @@ function computeLedgerSummary(records) {
780
1589
  summary.byTier = [...tierMap.entries()]
781
1590
  .map(([tier, count]) => ({ tier, count }))
782
1591
  .sort((a, b) => b.count - a.count)
1592
+ finalizeLedgerCost(summary)
783
1593
  return summary
784
1594
  }
785
1595
 
@@ -802,7 +1612,75 @@ async function fetchLedgerFailures(sessionId) {
802
1612
  }
803
1613
  }
804
1614
 
805
- // 由失败 trace 构造独立账本记录(快照内无对应错误块时):request requested/
1615
+ // 拉取后台派发结算台账:GET /ledger/jobs?session=<id>,按 jobId join 到后台记录。
1616
+ async function fetchLedgerJobs(sessionId) {
1617
+ if (typeof sessionId !== 'string' || sessionId === '') return []
1618
+ try {
1619
+ const data = await api(`/ledger/jobs?session=${encodeURIComponent(sessionId)}`)
1620
+ return Array.isArray(data.jobs) ? data.jobs : []
1621
+ } catch {
1622
+ return []
1623
+ }
1624
+ }
1625
+
1626
+ // 全部待配对 jobId 都已在拉取结果中出现 settled → 轮询可以停止。
1627
+ function allJobsSettled(list, pending) {
1628
+ const settled = new Set()
1629
+ for (const item of list) {
1630
+ if (item !== null && typeof item === 'object' && typeof item.jobId === 'string' && item.settled !== null && item.settled !== undefined) settled.add(item.jobId)
1631
+ }
1632
+ for (const id of pending) {
1633
+ if (!settled.has(id)) return false
1634
+ }
1635
+ return true
1636
+ }
1637
+
1638
+ // 台账 tab 后台任务 live 轮询:与派发卡片同节奏(3 秒、100 次上限),仅待配对
1639
+ // jobId 非空时启动;配对齐全或达上限即停。返回清理函数。
1640
+ function startLedgerJobsPoll(sessionId, pendingJobIds, setJobs) {
1641
+ let cancelled = false
1642
+ let timer = null
1643
+ let attempts = 0
1644
+ const pending = new Set(Array.isArray(pendingJobIds) ? pendingJobIds : [])
1645
+ const poll = () => {
1646
+ fetchLedgerJobs(sessionId).then((list) => {
1647
+ if (cancelled) return
1648
+ setJobs(list)
1649
+ if (allJobsSettled(list, pending) && timer !== null) clearInterval(timer)
1650
+ }).catch(() => {})
1651
+ }
1652
+ poll()
1653
+ if (pending.size > 0) {
1654
+ timer = setInterval(() => {
1655
+ attempts += 1
1656
+ if (attempts >= 100) { if (timer !== null) clearInterval(timer); return }
1657
+ poll()
1658
+ }, 3000)
1659
+ }
1660
+ return () => { cancelled = true; if (timer !== null) clearInterval(timer) }
1661
+ }
1662
+
1663
+ // 把后台结算台账按 jobId join 回记录:仅补 execution.kind==='background' 且尚无
1664
+ // settled 的记录;已结算/非后台/无 jobId 原样返回(幂等、不覆盖已有 settled)。
1665
+ function mergeBackgroundJobs(records, jobs) {
1666
+ if (!Array.isArray(records) || !Array.isArray(jobs) || jobs.length === 0) return records
1667
+ const byJob = new Map()
1668
+ for (const item of jobs) {
1669
+ if (item === null || typeof item !== 'object' || typeof item.jobId !== 'string') continue
1670
+ byJob.set(item.jobId, item.settled)
1671
+ }
1672
+ if (byJob.size === 0) return records
1673
+ return records.map((record) => {
1674
+ const trace = record && record.trace
1675
+ const jobId = trace && trace.execution && typeof trace.execution.jobId === 'string' ? trace.execution.jobId : ''
1676
+ if (jobId === '' || trace.settled !== null && trace.settled !== undefined) return record
1677
+ const settled = byJob.get(jobId)
1678
+ if (settled === undefined) return record
1679
+ return { ...record, trace: { ...trace, settled } }
1680
+ })
1681
+ }
1682
+
1683
+ // 由失败 trace 构造独立台账记录(快照内无对应错误块时):request 从 requested/
806
1684
  // execution 还原,callId/argsSummary/contentFirstLine 为空,标记 failureOnly。
807
1685
  function failureRecord(failure) {
808
1686
  const requested = asObject(failure && failure.requested)
@@ -902,7 +1780,7 @@ function resolveDispatchState(request, result, settled) {
902
1780
  function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
903
1781
  const chips = []
904
1782
  chips.push(chip('profile', ZH.chipProfile, profileZh(eff.profile)))
905
- // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父」)与
1783
+ // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父会话」)与
906
1784
  // provider·model / effort 一样弱显。
907
1785
  chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: eff.preset === 'inherit' || eff.preset === '' }))
908
1786
  const p = rawOrInherit(eff.provider)
@@ -924,7 +1802,7 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
924
1802
  key: `ignored-${key}`,
925
1803
  className: 'sap-chip sap-chipWarn',
926
1804
  title: reqVal !== '' ? `请求值 "${reqVal}" 已忽略` : '请求值已忽略'
927
- }, `⬆${key}(${ZH.ignoredMark})`))
1805
+ }, `⬆${FIELD_ZH[key] || key}(${ZH.ignoredMark})`))
928
1806
  }
929
1807
  return chips
930
1808
  }
@@ -933,31 +1811,180 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
933
1811
  function makeChip(el) {
934
1812
  const chip = (key, label, value, opts) => {
935
1813
  const valueCls = opts && opts.dim ? 'sap-chipValueDim' : 'sap-chipValue'
1814
+ const dot = opts && opts.dot === true ? el('span', { className: 'sap-chipDot' }, ' · ') : null
936
1815
  return el('span', {
937
1816
  key,
938
1817
  className: 'sap-chip',
939
1818
  ...(opts && typeof opts.title === 'string' && opts.title !== '' ? { title: opts.title } : {})
940
1819
  },
941
1820
  el('span', { className: 'sap-chipLabel' }, label),
1821
+ dot,
942
1822
  el('span', { className: valueCls }, value)
943
1823
  )
944
1824
  }
945
1825
  return chip
946
1826
  }
947
1827
 
948
- function makeDispatchToolview(el, chip, useState) {
1828
+ // 从一次派发快照生成 draft 建议(只取配置快照,不落 prompt/persona 原文)。
1829
+ function draftSuggestionFromDispatch(request, trace, result) {
1830
+ const ts = Date.now().toString(36).slice(-4)
1831
+ const profile = request && typeof request.profile === 'string' && request.profile !== '' && request.profile !== '(inline)' ? request.profile : 'inline'
1832
+ const config = { id: 'captured-' + profile + '-' + ts }
1833
+ if (request.preset) config.preset = request.preset
1834
+ if (request.provider) config.provider = request.provider
1835
+ if (request.model) config.model = request.model
1836
+ if (request.reasoningEffort) config.reasoningEffort = request.reasoningEffort
1837
+ const tier = result && result.fields && typeof result.fields.tokenTier === 'string' ? result.fields.tokenTier : (trace && trace.effective && typeof trace.effective.tokenTier === 'string' ? trace.effective.tokenTier : '')
1838
+ if (tier) config.tokenTier = tier
1839
+ return {
1840
+ config,
1841
+ name: profile === 'inline' ? '我的派发方案' : '我的 ' + profile + ' 方案',
1842
+ description: '来自一次 dispatch 的配置快照,可继续编辑。'
1843
+ }
1844
+ }
1845
+
1846
+ // draft 捕获的 hooks 与渲染分离,保持 DispatchToolview 短小。
1847
+ function useDraftCapture(useState, suggestion) {
1848
+ const [draftOpen, setDraftOpen] = useState(false)
1849
+ const [draftName, setDraftName] = useState('')
1850
+ const [draftDesc, setDraftDesc] = useState('')
1851
+ const [draftError, setDraftError] = useState('')
1852
+ const [draftSaved, setDraftSaved] = useState(false)
1853
+ const [draftCopied, setDraftCopied] = useState(false)
1854
+ const openDraft = () => { setDraftName(suggestion.name); setDraftDesc(suggestion.description); setDraftError(''); setDraftSaved(false); setDraftOpen(true) }
1855
+ const closeDraft = () => setDraftOpen(false)
1856
+ const saveDraft = () => {
1857
+ setDraftError(''); setDraftSaved(false)
1858
+ api('/draft', { source: 'dispatch', config: suggestion.config, name: draftName, description: draftDesc })
1859
+ .then(() => setDraftSaved(true))
1860
+ .catch((err) => setDraftError(String((err && err.message) || err)))
1861
+ }
1862
+ // 成功文案的「复制方案 id」动作(clipboard fail-soft,反馈经 draftCopied 状态)。
1863
+ const copyDraftId = () => { copyToClipboard(suggestion.config.id); setDraftCopied(true) }
1864
+ return { draftOpen, draftName, setDraftName, draftDesc, setDraftDesc, draftError, draftSaved, draftCopied, draftConfigId: suggestion.config.id, openDraft, closeDraft, saveDraft, copyDraftId }
1865
+ }
1866
+
1867
+ function buildDraftButton(el, draft) {
1868
+ return el('button', { type: 'button', className: 'sap-detailToggle', onClick: draft.openDraft }, draft.draftSaved ? '已存为方案 ✓' : '存为方案')
1869
+ }
1870
+
1871
+ function buildDraftForm(el, draft) {
1872
+ if (!draft.draftOpen) return null
1873
+ return el('div', { className: 'sap-draftCard' },
1874
+ el('div', { className: 'sap-draftTitle' }, '存为方案(draft)'),
1875
+ el('div', { className: 'sap-field' },
1876
+ el('span', { className: 'sap-fieldLabel' }, '方案名'),
1877
+ el('input', { className: 'sap-input', value: draft.draftName, onChange: (event) => draft.setDraftName(event && event.target ? event.target.value : '') })
1878
+ ),
1879
+ el('div', { className: 'sap-field' },
1880
+ el('span', { className: 'sap-fieldLabel' }, '描述'),
1881
+ el('input', { className: 'sap-input', value: draft.draftDesc, onChange: (event) => draft.setDraftDesc(event && event.target ? event.target.value : '') })
1882
+ ),
1883
+ el('div', { className: 'sap-editorActions' },
1884
+ el('button', { className: 'sap-secondaryButton', onClick: draft.closeDraft }, '取消'),
1885
+ el('button', { className: 'sap-primaryButton', onClick: draft.saveDraft }, '确认保存')
1886
+ ),
1887
+ draft.draftError !== '' ? el('p', { className: 'sap-notice' }, draft.draftError) : null,
1888
+ draft.draftSaved ? el('div', { className: 'sap-toolSummary' },
1889
+ el('p', { className: 'sap-savedNotice' }, ZH.draftSavedWithId.replace('{id}', draft.draftConfigId)),
1890
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: draft.copyDraftId }, draft.draftCopied ? ZH.ledgerCopied : ZH.draftCopyId)
1891
+ ) : null
1892
+ )
1893
+ }
1894
+
1895
+ // ── 派发链路 live 状态机 ──────────────────────────────────────
1896
+
1897
+ // phase 徽标推导(纯函数):前台 execute 返回即终态(静态 ✅/❌);后台运行中块无
1898
+ // jobId,靠 /ledger/jobs 轮询结果(jobSettled)按 callId 精确配对 live
1899
+ // 「执行中 → 完成/失败」;continuable 启动即返回 subagentId,无结算概念 →「已启动」。
1900
+ function phaseStepsOf(request, result, settled, trace, jobSettled) {
1901
+ const steps = [{ key: 'initiate', label: ZH.phaseInitiate, state: 'done' }]
1902
+ if (jobSettled !== null && jobSettled !== undefined && typeof jobSettled === 'object') {
1903
+ const completed = jobSettled.status === 'completed'
1904
+ steps.push({ key: 'guards', label: ZH.phaseGuards, state: 'done' })
1905
+ steps.push({ key: 'create', label: ZH.phaseCreate, state: 'done' })
1906
+ steps.push({ key: 'run', label: ZH.phaseRun, state: 'done' })
1907
+ steps.push({ key: 'end', label: completed ? ZH.phaseDone : ZH.phaseFailed, state: completed ? 'done' : 'fail' })
1908
+ return steps
1909
+ }
1910
+ if (settled && result !== null) {
1911
+ const gates = trace !== null && Array.isArray(trace.gates) ? trace.gates : []
1912
+ const gateFailed = gates.some((g) => g && g.verdict === 'fail')
1913
+ steps.push({ key: 'guards', label: ZH.phaseGuards, state: gateFailed ? 'fail' : 'done' })
1914
+ steps.push({ key: 'create', label: ZH.phaseCreate, state: 'done' })
1915
+ steps.push({ key: 'run', label: ZH.phaseRun, state: 'done' })
1916
+ const stop = result.fields && typeof result.fields.stopReason === 'string' ? result.fields.stopReason : ''
1917
+ const failed = stop !== '' && stop !== 'completed'
1918
+ steps.push({ key: 'end', label: failed ? ZH.phaseFailed : ZH.phaseDone, state: failed ? 'fail' : 'done' })
1919
+ return steps
1920
+ }
1921
+ if (settled && result === null) {
1922
+ steps.push({ key: 'settled', label: ZH.phaseLegacy, state: 'done' })
1923
+ return steps
1924
+ }
1925
+ // 运行中:未知安全检查/创建态不虚报,只宣告 发起 → 执行中。
1926
+ steps.push({ key: 'run', label: ZH.phaseRun, state: 'running' })
1927
+ return steps
1928
+ }
1929
+
1930
+ // phase 徽标行渲染:恒在摘要行上方(未展开即渲染)。
1931
+ function buildDispatchPhaseLine(el, request, result, settled, trace, jobSettled) {
1932
+ const steps = phaseStepsOf(request, result, settled, trace, jobSettled)
1933
+ return el('div', { className: 'sap-dispatchPhase' },
1934
+ steps.map((s, index) => el('span', { key: s.key, className: 'sap-phaseStep sap-phaseStep-' + s.state }, (index > 0 ? '→ ' : '') + s.label))
1935
+ )
1936
+ }
1937
+
1938
+ // 后台派发 live 轮询(兜底):每 3 秒拉 /ledger/jobs?session=,按 callId 配对
1939
+ // 结算;配对成功或达到轮询上限(100 次 ≈ 5 分钟)即停。非后台/已结算/无 callId 不启动。
1940
+ function useBackgroundJobPoll(useState, useEffect, api, sessionId, callId, settled, request) {
1941
+ const [jobSettled, setJobSettled] = useState(null)
1942
+ useEffect(() => {
1943
+ if (settled || !request || request.runInBackground !== true) return
1944
+ if (typeof sessionId !== 'string' || sessionId === '' || typeof callId !== 'string' || callId === '') return
1945
+ let attempts = 0
1946
+ let timer = null
1947
+ const poll = () => {
1948
+ attempts += 1
1949
+ api('/ledger/jobs?session=' + encodeURIComponent(sessionId))
1950
+ .then((data) => {
1951
+ const jobs = Array.isArray(data.jobs) ? data.jobs : []
1952
+ const hit = jobs.find((j) => j && j.callId === callId && j.settled !== null && j.settled !== undefined)
1953
+ if (hit !== undefined) {
1954
+ setJobSettled(hit.settled)
1955
+ if (timer !== null) clearInterval(timer)
1956
+ } else if (attempts >= 100) {
1957
+ if (timer !== null) clearInterval(timer)
1958
+ }
1959
+ })
1960
+ .catch(() => {})
1961
+ }
1962
+ poll()
1963
+ timer = setInterval(poll, 3000)
1964
+ return () => { if (timer !== null) clearInterval(timer) }
1965
+ }, [sessionId, callId, settled, request])
1966
+ return jobSettled
1967
+ }
1968
+
1969
+ function makeDispatchToolview(el, chip, useState, useEffect) {
949
1970
  function DispatchToolview(props) {
950
- const block = props && props.block
951
- const request = readDispatchRequest(block)
952
- const result = readDispatchResult(block)
1971
+ const block = props && props.block; const request = readDispatchRequest(block); const result = readDispatchResult(block)
953
1972
  const settled = !!(block && typeof block === 'object' && 'kind' in block)
1973
+ const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
954
1974
  const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
955
1975
  const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
1976
+ const safetyLine = safetyLineText(trace); const toolLine = toolLineText(request, trace)
1977
+ const metaLine = (safetyLine !== '' || toolLine !== '') ? el('div', { className: 'sap-toolMeta' },
1978
+ safetyLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, safetyLine) : null,
1979
+ toolLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, toolLine) : null
1980
+ ) : null
956
1981
  const usage = usageFrom(result, block)
957
- const [open, setOpen] = useState(false)
1982
+ // 后台运行中按 callId 轮询 /ledger/jobs 配对结算(前台/continuable 静态终态)。
1983
+ const jobSettled = useBackgroundJobPoll(useState, useEffect, api, props.sessionId, props.callId, settled, request)
1984
+ const [open, setOpen] = useState(false); const suggestion = draftSuggestionFromDispatch(request, trace, result); const draft = useDraftCapture(useState, suggestion)
958
1985
  // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
959
1986
  // 提供方/模型/推理强度/档位)全部在 chip 行——避免同一组信息重复两遍。
960
- const text = `${ZH.toolLabel}:${kindLabel}${tokensSuffix(result, block)}${cacheHitSuffix(usage)}${observabilitySuffix(result, block)}`
1987
+ const text = dispatchSummaryText(request, result, block, trace, kindLabel)
961
1988
  // 详情折叠入口:仅当存在真实计费分解(childUsage)时渲染;无分解数据
962
1989
  // (旧结果/无 usage 会话)不显示折叠区,向前兼容。
963
1990
  const detailToggle = usage === null ? null : el('button', {
@@ -974,12 +2001,17 @@ function makeDispatchToolview(el, chip, useState) {
974
2001
  detailToggle
975
2002
  )
976
2003
  const detailBody = open && usage !== null
977
- ? el('div', { className: 'sap-toolDetail' }, usageBreakdownText(usage))
2004
+ ? el('div', { className: 'sap-toolDetail' }, usageDetailText(usage))
978
2005
  : null
2006
+ const draftButton = buildDraftButton(el, draft); const draftForm = buildDraftForm(el, draft)
979
2007
  return el('div', { className: 'sap-toolview' },
2008
+ buildDispatchPhaseLine(el, request, result, settled, trace, jobSettled),
980
2009
  el('div', { className: 'sap-chips' }, chips),
2010
+ metaLine,
981
2011
  summary,
982
- detailBody
2012
+ detailBody,
2013
+ draftButton,
2014
+ draftForm
983
2015
  )
984
2016
  }
985
2017
  return DispatchToolview
@@ -1008,7 +2040,12 @@ function makeFormActions(s, loadEfforts, loadTools) {
1008
2040
  const value = event && event.target ? event.target.value : ''
1009
2041
  s.setForm((prev) => {
1010
2042
  const next = { ...prev, [key]: value }
1011
- if (key === 'model') next.reasoningEffort = '' // 换模型时重置推理强度
2043
+ if (key === 'model') {
2044
+ next.reasoningEffort = '' // 换模型时重置推理强度
2045
+ // provider 由 model 联动派生:选中模型即同步表单 provider(不可独立编辑)。
2046
+ const derived = providerOfModel(s.options.models, value)
2047
+ if (derived !== null) next.provider = derived.id
2048
+ }
1012
2049
  return next
1013
2050
  })
1014
2051
  if (key === 'model' && value !== '') {
@@ -1090,7 +2127,7 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
1090
2127
  const submit = () => {
1091
2128
  const payload = { id: typeof s.form.id === 'string' ? s.form.id.trim() : '' }
1092
2129
  // 字符串字段总是发送(含空串):后端 merge 语义里空串 = 清除该字段,
1093
- // 否则把字段清回“继承父/默认”会被静默忽略(保留旧值)。
2130
+ // 否则把字段清回“继承父会话/默认”会被静默忽略(保留旧值)。
1094
2131
  const add = (key, value) => { if (typeof value === 'string') payload[key] = value.trim() }
1095
2132
  add('description', s.form.description)
1096
2133
  add('preset', s.form.preset)
@@ -1210,15 +2247,31 @@ function makeToolsLoader(api, setTools) {
1210
2247
  }
1211
2248
  }
1212
2249
 
1213
- // 版本探测 → 拉 /options/versions;warnings 非空时设置页顶部常驻 amber 提示条。
1214
- function makeVersionsLoader(api, setVersionWarnings) {
2250
+ // 版本探测 → 拉 /options/versions;warnings 非空时维护区展示告警,否则显示插件版本。
2251
+ function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
1215
2252
  return () => {
1216
2253
  api('/options/versions')
1217
- .then((data) => setVersionWarnings(Array.isArray(data.warnings) ? data.warnings : []))
2254
+ .then((data) => {
2255
+ setVersionWarnings(Array.isArray(data.warnings) ? data.warnings : [])
2256
+ setPluginVersion(typeof data.pluginVersion === 'string' ? data.pluginVersion : '')
2257
+ })
1218
2258
  .catch(() => {})
1219
2259
  }
1220
2260
  }
1221
2261
 
2262
+ // 定位方案:展开方案管理区 + 高亮对应卡片 + 滚动到可视区(建议/成本行的去向)。
2263
+ function makeLocateProfile(s) {
2264
+ return (profileId) => () => {
2265
+ s.setOpenSections((prev) => ({ ...prev, 1: true }))
2266
+ s.setHighlightId(String(profileId))
2267
+ setTimeout(() => {
2268
+ const node = document.getElementById('sap-profile-' + String(profileId))
2269
+ if (node !== null && typeof node.scrollIntoView === 'function') node.scrollIntoView({ behavior: 'smooth', block: 'center' })
2270
+ }, 60)
2271
+ setTimeout(() => s.setHighlightId(null), 8000)
2272
+ }
2273
+ }
2274
+
1222
2275
  function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
1223
2276
  return {
1224
2277
  ...makeFormActions(s, loadEfforts, loadTools),
@@ -1226,6 +2279,7 @@ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools
1226
2279
  ...makeSubmitActions(api, s, refresh, applyWrite),
1227
2280
  ...makeRowActions(api, s, refresh, applyWrite),
1228
2281
  ...makeEscapeActions(api, s),
2282
+ locateProfile: makeLocateProfile(s),
1229
2283
  }
1230
2284
  }
1231
2285
 
@@ -1235,7 +2289,7 @@ function buildSelectField(el, s, setField, label, key, optionsList, renderOption
1235
2289
  return el('div', { className: 'sap-field' },
1236
2290
  el('span', { className: 'sap-fieldLabel' }, label),
1237
2291
  el('select', { className: 'sap-input sap-select', value: s.form[key], onChange: setField(key) },
1238
- el('option', { value: '' }, '继承父(不指定)'),
2292
+ el('option', { value: '' }, '继承父会话(不指定)'),
1239
2293
  optionsList.map(renderOption)
1240
2294
  )
1241
2295
  )
@@ -1331,7 +2385,8 @@ function buildProfileRows(el, chip, s, actions) {
1331
2385
  ? el('span', { key: 'state', className: 'sap-chip sap-chipDisabled' }, ZH.chipDisabled)
1332
2386
  : el('span', { key: 'state', className: 'sap-chip sap-chipSuccess' }, ZH.chipEnabled)
1333
2387
  ]
1334
- return el('li', { key: String(profile.id), className: 'sap-rowCard' },
2388
+ const highlighted = s.highlightId !== null && s.highlightId !== undefined && String(profile.id) === String(s.highlightId)
2389
+ return el('li', { key: String(profile.id), id: 'sap-profile-' + String(profile.id), className: highlighted ? 'sap-rowCard sap-rowHighlight' : 'sap-rowCard' },
1335
2390
  el('div', { className: 'sap-rowHead' },
1336
2391
  el('span', { className: 'sap-rowIdentity' },
1337
2392
  el('span', { className: 'sap-rowName' }, hasName ? profile.name : String(profile.id)),
@@ -1350,13 +2405,33 @@ function buildProfileRows(el, chip, s, actions) {
1350
2405
  el('div', { className: 'sap-chips' }, chipItems),
1351
2406
  typeof profile.description === 'string' && profile.description !== ''
1352
2407
  ? el('p', { className: 'sap-desc' }, profile.description)
2408
+ : null,
2409
+ (typeof profile.avgCost === 'number' || typeof profile.successRate === 'number')
2410
+ ? el('p', { className: 'sap-costLine' },
2411
+ [
2412
+ typeof profile.avgCost === 'number' ? `平均 ≈${profile.avgCost} 元/次` : '',
2413
+ typeof profile.successRate === 'number' ? `成功率 ${profile.successRate}${typeof profile.n === 'number' ? `(N=${profile.n})` : ''}` : ''
2414
+ ].filter(Boolean).join(' · '))
1353
2415
  : null
1354
2416
  )
1355
2417
  })
1356
2418
  }
1357
2419
 
2420
+ // 提供方只读展示:provider 由已选 model 联动派生;未选模型时回退表单存量
2421
+ // provider 值(编辑存量 profile 时照常展示),不可独立编辑。
2422
+ function buildProviderReadonlyLine(el, s) {
2423
+ const derived = providerOfModel(s.options.models, s.form.model)
2424
+ const fallback = typeof s.form.provider === 'string' && s.form.provider !== '' ? s.form.provider : ''
2425
+ const display = derived !== null
2426
+ ? (derived.name && derived.name !== derived.id ? derived.name + '(' + derived.id + ')' : derived.id)
2427
+ : fallback
2428
+ return el('div', { className: 'sap-field' },
2429
+ el('span', { className: 'sap-fieldLabel' }, '提供方(由模型联动,只读)'),
2430
+ el('span', { className: 'sap-metaValueDefault' }, display === '' ? '选择模型后自动确定' : display)
2431
+ )
2432
+ }
2433
+
1358
2434
  function buildAddCard(el, s, actions, toolSection) {
1359
- const providers = deriveProviders(s.options.models)
1360
2435
  const effortsForModel = s.form.model ? (s.options.efforts[s.form.model] ?? []) : []
1361
2436
  const renderEffortOption = (effort) => el('option', { key: effort.id, value: effort.id }, effortLabel(effort))
1362
2437
  return el('div', { className: 'sap-addCard' },
@@ -1373,16 +2448,15 @@ function buildAddCard(el, s, actions, toolSection) {
1373
2448
  ),
1374
2449
  buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
1375
2450
  (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
1376
- s.options.models.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到模型目录(headless 部署)——派发成本护栏将按「兼容模式」开关决定放行或拒绝') : null,
2451
+ s.options.models.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到模型目录(headless 部署)——派发成本安全检查将按「兼容模式」开关决定放行或拒绝') : null,
1377
2452
  el('details', { className: 'sap-customized' },
1378
2453
  el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
1379
2454
  el('div', { className: 'sap-customizedBody' },
1380
- el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父预设),派发结果以「ignored」标注'),
2455
+ el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父会话预设),派发结果以「ignored」标注'),
1381
2456
  buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
1382
2457
  (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
1383
- s.options.presets.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到 system-trust 预设名册(rosterless)——目标预设下拉不可用') : null,
1384
- buildSelectField(el, s, actions.setField, '提供方(模型服务商)', 'provider', providers,
1385
- (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
2458
+ s.options.presets.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到受信任预设名册(rosterless)——目标预设下拉不可用') : null,
2459
+ buildProviderReadonlyLine(el, s),
1386
2460
  s.form.model === ''
1387
2461
  ? el('div', { className: 'sap-field' },
1388
2462
  el('span', { className: 'sap-fieldLabel' }, '推理强度(思考深度)'),
@@ -1401,7 +2475,7 @@ function buildAddCard(el, s, actions, toolSection) {
1401
2475
  )
1402
2476
  }
1403
2477
 
1404
- // 只读建议注入(evolution:advice)开关卡片:仅父 Agent、只读、默认关。
2478
+ // 派发优化建议注入:一行开关,只提示不改变派发行为。
1405
2479
  function buildAdviceSection(el, s, actions) {
1406
2480
  return el('div', { className: 'sap-rowCard' },
1407
2481
  el('div', { className: 'sap-rowHead' },
@@ -1411,11 +2485,54 @@ function buildAdviceSection(el, s, actions) {
1411
2485
  el('span', { className: 'sap-rowActions' },
1412
2486
  el('label', { className: 'sap-switch', title: ZH.adviceTitle },
1413
2487
  el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
1414
- el('span', null, s.evolutionAdvice ? '已启用' : '已禁用')
2488
+ el('span', null, s.evolutionAdvice ? ZH.adviceSwitchOn : ZH.adviceSwitchOff)
1415
2489
  )
1416
2490
  )
1417
- ),
1418
- el('p', { className: 'sap-desc' }, ZH.adviceDesc)
2491
+ )
2492
+ )
2493
+ }
2494
+
2495
+ // 按 profileKey 的 preset/model 段匹配方案管理区的 profile 卡片(建议行的去向)。
2496
+ function matchingProfileOf(profiles, profileKey) {
2497
+ if (!Array.isArray(profiles) || typeof profileKey !== 'string') return null
2498
+ const segment = (prefix) => {
2499
+ for (const part of profileKey.split('|')) {
2500
+ if (part.startsWith(prefix)) return part.slice(prefix.length)
2501
+ }
2502
+ return ''
2503
+ }
2504
+ const preset = segment('preset:')
2505
+ const model = segment('model:')
2506
+ if (preset !== '') { const hit = profiles.find((p) => p && p.preset === preset); if (hit !== undefined) return hit }
2507
+ if (model !== '') { const hit = profiles.find((p) => p && p.model === model); if (hit !== undefined) return hit }
2508
+ return null
2509
+ }
2510
+
2511
+ // 派发优化建议预览面板:每条建议可「定位方案」(高亮方案管理区对应卡片,数据有去向)。
2512
+ function buildAdvicePanel(el, s, actions) {
2513
+ const advice = Array.isArray(s.advice) ? s.advice : []
2514
+ const global = s.adviceGlobal !== null && typeof s.adviceGlobal === 'object' ? s.adviceGlobal : null
2515
+ const directionZh = (d) => (d === '升' ? ZH.adviceDirUp : d === '降' ? ZH.adviceDirDown : ZH.adviceDirFlat)
2516
+ if (global === null) return null
2517
+ if (advice.length === 0) return el('p', { className: 'sap-desc' }, ZH.advicePanelEmpty)
2518
+ return el('div', { className: 'sap-advicePanel' },
2519
+ el('p', { className: 'sap-metaValueDefault' }, ZH.advicePanelGlobal + ':累计 ' + global.total + ' 次(完成 ' + global.completed + '、失败 ' + global.failed + '、终止 ' + global.killed + ')' + ZH.advicePanelReadonly),
2520
+ el('ul', { className: 'sap-rows' },
2521
+ advice.map((a) => {
2522
+ const profile = matchingProfileOf(s.profiles, a.profileKey)
2523
+ return el('li', { key: String(a.profileKey), className: 'sap-rowCard' },
2524
+ el('div', { className: 'sap-rowHead' },
2525
+ el('span', { className: 'sap-rowName' }, String(a.profileKey)),
2526
+ el('span', { className: 'sap-rowActions' },
2527
+ profile !== null
2528
+ ? el('button', { className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate)
2529
+ : el('span', { className: 'sap-metaValueDefault' }, ZH.adviceRowNoMatch)
2530
+ )
2531
+ ),
2532
+ el('p', { className: 'sap-desc' }, a.performanceText + ';' + directionZh(a.suggestion) + '(' + ZH.adviceConfidence + ' ' + a.confidence + ')')
2533
+ )
2534
+ })
2535
+ )
1419
2536
  )
1420
2537
  }
1421
2538
 
@@ -1459,7 +2576,188 @@ function buildEscapeSection(el, s, actions) {
1459
2576
  )
1460
2577
  }
1461
2578
 
1462
- function buildSectionReturn(el, s, actions, rows, addCard) {
2579
+ // 设置页卡片骨架:折叠卡 = 标题 + 一句话描述 + SVG chevron(固定 14px,展开旋转 180°);
2580
+ // 静态卡无 chevron,内容常驻展示。折叠的唯一理由是内容多占空间。
2581
+ function buildSettingsCard(el, key, title, desc, open, toggle, body, isStatic) {
2582
+ const titleBox = el('div', { className: 'sap-cardTitleBox' },
2583
+ el('div', { className: 'sap-cardTitle' }, title),
2584
+ el('div', { className: 'sap-cardDesc' }, desc)
2585
+ )
2586
+ const head = isStatic === true
2587
+ ? el('div', { className: 'sap-cardHead' }, titleBox)
2588
+ : el('button', { type: 'button', className: 'sap-cardHead sap-cardToggle', 'aria-expanded': open, onClick: () => toggle(key) }, titleBox,
2589
+ el('svg', { className: 'sap-cardChevron', viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' },
2590
+ el('path', { d: 'M 4 6l4 4 4-4' })
2591
+ )
2592
+ )
2593
+ const cardClass = isStatic === true ? 'sap-card sap-cardStatic' : 'sap-card' + (open === true ? ' sap-cardOpen' : '')
2594
+ return el('div', { className: cardClass }, head,
2595
+ isStatic === true || open === true ? el('div', { className: 'sap-cardBody' }, body) : null
2596
+ )
2597
+ }
2598
+
2599
+ // 常驻状态条:health + 审计丢失(health 唯一位置;丢失 >0 红字)。目录/告警计数
2600
+ // 不再重复(版本告警正文在高级区,catalog 计数随 [2] 区一并删除——一个数字只出现一次)。
2601
+ function buildStatusBar(el, s) {
2602
+ const degraded = s.audit.health === 'degraded'
2603
+ const lost = s.audit.lostTelemetry + s.audit.lostGovernance
2604
+ return el('div', { className: 'sap-statusBar' },
2605
+ el('span', { className: degraded ? 'sap-statusBad sap-statusBadFail' : 'sap-statusBad' }, degraded ? '健康 ⚠ 已降级' : '健康 ✅'),
2606
+ el('span', { className: lost > 0 ? 'sap-statusBadFail' : 'sap-statusDim' }, ZH.statusLost + ' ' + lost + (lost > 0 ? ' 条(请检查磁盘)' : ' 条'))
2607
+ )
2608
+ }
2609
+
2610
+ function buildSectionNotices(el, s) {
2611
+ return [
2612
+ !s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
2613
+ s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
2614
+ s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
2615
+ s.persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${s.persistWarning}${ZH.persistFailHint}`) : null
2616
+ ]
2617
+ }
2618
+
2619
+ // draft 安全检查预览结果:ok 全绿/存在拒绝,逐项展示 verdict + reason。
2620
+ function buildDraftPreview(el, preview) {
2621
+ const checks = Array.isArray(preview.checks) ? preview.checks : []
2622
+ return el('div', { className: 'sap-draftPreview' },
2623
+ el('p', { className: preview.ok ? 'sap-savedNotice' : 'sap-warn', role: 'status' }, preview.ok ? ZH.draftPreviewPass : ZH.draftPreviewFail),
2624
+ el('ul', { className: 'sap-rows' },
2625
+ checks.map((c) => el('li', { key: String(c.name), className: 'sap-rowCard' },
2626
+ el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName' }, (c.verdict === 'pass' ? '✅ ' : '❌ ') + c.name)),
2627
+ c.reason ? el('p', { className: 'sap-desc' }, c.reason) : null
2628
+ ))
2629
+ )
2630
+ )
2631
+ }
2632
+
2633
+ // draft 列表(设置页 [1] 内):每个 draft 可预览过闸结果、应用或删除。
2634
+ function buildDraftRows(el, s, actions) {
2635
+ if (!Array.isArray(s.drafts) || s.drafts.length === 0) return null
2636
+ return el('div', { className: 'sap-drafts' },
2637
+ el('div', { className: 'sap-draftTitle' }, '待确认 draft'),
2638
+ el('ul', { className: 'sap-rows' },
2639
+ s.drafts.map((draft) => {
2640
+ const preview = s.draftPreviews && typeof s.draftPreviews === 'object' ? s.draftPreviews[draft.id] : undefined
2641
+ return el('li', { key: String(draft.id), className: 'sap-rowCard' },
2642
+ el('div', { className: 'sap-rowHead' },
2643
+ el('span', { className: 'sap-rowIdentity' },
2644
+ el('span', { className: 'sap-rowName' }, draft.name || draft.id),
2645
+ el('span', { className: 'sap-rowId' }, draft.config && draft.config.id || '')
2646
+ ),
2647
+ el('span', { className: 'sap-rowActions' },
2648
+ el('button', { className: 'sap-secondaryButton', style: smallBtn, onClick: actions.previewDraft(draft) }, ZH.draftPreview),
2649
+ el('button', { className: 'sap-secondaryButton', style: smallBtn, onClick: actions.applyDraft(draft) }, '应用'),
2650
+ el('button', { className: 'sap-dangerButton', onClick: actions.removeDraft(draft) }, '删除')
2651
+ )
2652
+ ),
2653
+ draft.description !== '' ? el('p', { className: 'sap-desc' }, draft.description) : null,
2654
+ preview !== undefined ? buildDraftPreview(el, preview) : null
2655
+ )
2656
+ })
2657
+ )
2658
+ )
2659
+ }
2660
+
2661
+ // 全局成本数据行:只展示「全部会话」口径,不带内部技术词;各会话明细另行走会话页。
2662
+ function buildCostGlobalCard(el, global) {
2663
+ if (global === null || typeof global !== 'object') return null
2664
+ return el('p', { className: 'sap-costLine' },
2665
+ [
2666
+ global.priced > 0 ? '全部会话估算 ≈' + global.estimated_cost + ' 元' : '',
2667
+ ZH.costInherit + ' ≈' + global.estimated_inherit_cost + ' 元',
2668
+ ZH.costSaving + ' ≈' + global.estimated_saving + ' 元',
2669
+ ZH.costPriced + ' ' + global.priced + ' 次',
2670
+ ZH.costInheritRatio + ' ' + Math.round((global.inherit_ratio ?? 0) * 100) + '%'
2671
+ ].filter(Boolean).join(' · ')
2672
+ )
2673
+ }
2674
+
2675
+ function buildCostByModel(el, byModel, profiles, actions) {
2676
+ if (!Array.isArray(byModel) || byModel.length === 0) return null
2677
+ return el('div', null,
2678
+ el('div', { className: 'sap-draftTitle' }, ZH.costByModel),
2679
+ el('ul', { className: 'sap-rows' },
2680
+ byModel.map((m) => {
2681
+ const profile = Array.isArray(profiles) ? profiles.find((p) => p && p.model === m.model) : undefined
2682
+ return el('li', { key: String(m.model), className: 'sap-rowCard' },
2683
+ el('div', { className: 'sap-rowHead' },
2684
+ el('span', { className: 'sap-rowIdentity' },
2685
+ el('span', { className: 'sap-rowName' }, String(m.model)),
2686
+ el('span', { className: 'sap-rowId' }, m.deployments + ' 次')
2687
+ ),
2688
+ el('span', { className: 'sap-rowActions' },
2689
+ profile !== undefined
2690
+ ? el('button', { className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.costEditProfile)
2691
+ : el('span', { className: 'sap-metaValueDefault' }, ZH.costNoProfile)
2692
+ )
2693
+ ),
2694
+ el('p', { className: 'sap-costLine' },
2695
+ [
2696
+ m.avg_cost !== null && m.avg_cost !== undefined ? '平均 ≈' + m.avg_cost + ' 元/次' : '',
2697
+ m.priced > 0 ? '估算 ≈' + m.estimated_cost + ' 元' : ''
2698
+ ].filter(Boolean).join(' · '))
2699
+ )
2700
+ })
2701
+ )
2702
+ )
2703
+ }
2704
+
2705
+ function buildCostObservabilitySection(el, s, actions) {
2706
+ const summary = s.costSummary
2707
+ const global = summary && typeof summary.global === 'object' ? summary.global : null
2708
+ const byModel = summary && Array.isArray(summary.byModel) ? summary.byModel : []
2709
+ const globalBlock = buildCostGlobalCard(el, global)
2710
+ const modelBlock = buildCostByModel(el, byModel, s.profiles, actions)
2711
+ const scopeGuide = el('p', { className: 'sap-metaValueDefault' }, '各会话的成本明细见会话页「派发决策台账」')
2712
+ if (globalBlock === null && modelBlock === null) {
2713
+ return el('div', null,
2714
+ el('p', { className: 'sap-desc' }, '估算成本按已记录的派发统计得出——记录较少时数值可能不准,会随使用逐步修正。'),
2715
+ scopeGuide
2716
+ )
2717
+ }
2718
+ return el('div', { className: 'sap-costSection' },
2719
+ globalBlock,
2720
+ modelBlock,
2721
+ scopeGuide
2722
+ )
2723
+ }
2724
+
2725
+ // 高级区(默认折叠):卡内按四个子组展开,子组间用 12px 深灰加粗小标题 + 20px
2726
+ // 组间距区分,不嵌套折叠;组内正文 12.5px 中灰(标题强于内容)。总开关在标题行,
2727
+ // 方案表单在方案管理卡,其余开关全在此区。
2728
+ function buildAdvancedSubgroup(el, title, hint, children) {
2729
+ return el('div', { className: 'sap-subgroup' },
2730
+ el('div', { className: 'sap-subgroupTitle' }, title),
2731
+ hint !== '' ? el('p', { className: 'sap-subgroupHint' }, hint) : null,
2732
+ ...children
2733
+ )
2734
+ }
2735
+
2736
+ function buildAdvancedSection(el, s, actions, reminderCenter) {
2737
+ const versionText = s.pluginVersion !== '' ? '当前版本 v' + s.pluginVersion + ' · 未发现兼容性问题' : '未发现兼容性问题'
2738
+ const versionBlock = s.versionWarnings.length > 0
2739
+ ? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
2740
+ : el('p', { className: 'sap-desc' }, versionText)
2741
+ return el('div', null,
2742
+ buildAdvancedSubgroup(el, '安全', '放行非官方预设的高风险通道,默认关闭', [
2743
+ el('p', { className: 'sap-desc' }, ZH.advancedSafetyLine),
2744
+ buildEscapeSection(el, s, actions)
2745
+ ]),
2746
+ buildAdvancedSubgroup(el, '建议注入', '根据历史派发数据向父 Agent 附优化提示(例:某方案近期成功率偏低,可考虑换方案)。只提示,不自动改配置', [
2747
+ buildAdviceSection(el, s, actions),
2748
+ buildAdvicePanel(el, s, actions)
2749
+ ]),
2750
+ buildAdvancedSubgroup(el, '通知', '需要你关注的事件汇总,所有动作均有审计记录', [
2751
+ reminderCenter
2752
+ ]),
2753
+ buildAdvancedSubgroup(el, '维护', '插件版本与宿主兼容性状态', [
2754
+ versionBlock
2755
+ ])
2756
+ )
2757
+ }
2758
+
2759
+ function buildSectionReturn(el, s, actions, rows, addCard, openSections, toggleSection, reminderCenter) {
2760
+ const notices = buildSectionNotices(el, s)
1463
2761
  return el('div', { className: 'sap-section' },
1464
2762
  el('div', { className: 'sap-titleRow' },
1465
2763
  el('h2', { className: 'sap-title' }, '子 Agent 方案'),
@@ -1468,55 +2766,90 @@ function buildSectionReturn(el, s, actions, rows, addCard) {
1468
2766
  el('span', null, s.enabled ? '已启用' : '已禁用')
1469
2767
  )
1470
2768
  ),
1471
- s.versionWarnings.length > 0
1472
- ? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
1473
- : null,
1474
- s.audit.health === 'degraded'
1475
- ? el('p', { className: 'sap-notice', role: 'alert' },
1476
- `${ZH.auditDegraded}(${ZH.auditLost}:派发遥测 ${s.audit.lostTelemetry} · 治理 ${s.audit.lostGovernance})`)
1477
- : null,
1478
- el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
1479
- el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
1480
- el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.resetAll }, '重置所有内置方案')
1481
- ),
1482
- buildAdviceSection(el, s, actions),
1483
- buildEscapeSection(el, s, actions),
1484
- !s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
1485
- s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
1486
- s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
1487
- s.persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${s.persistWarning}${ZH.persistFailHint}`) : null,
1488
- el('ul', { className: 'sap-rows' }, rows),
1489
- el('div', { className: 'sap-addBlock' },
1490
- s.adding ? addCard : el('div', { className: 'sap-addActions' },
1491
- el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
2769
+ buildStatusBar(el, s),
2770
+ ...notices,
2771
+ buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', openSections[1] === true, toggleSection, [
2772
+ el('p', { className: 'sap-intro' }, ZH.profilesIntro),
2773
+ el('ul', { className: 'sap-rows' }, rows),
2774
+ buildDraftRows(el, s, actions),
2775
+ el('div', { className: 'sap-addBlock' },
2776
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
2777
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
2778
+ )
2779
+ ),
2780
+ el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
2781
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
1492
2782
  )
1493
- )
2783
+ ], false),
2784
+ buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, toggleSection, [
2785
+ buildCostObservabilitySection(el, s, actions)
2786
+ ], true),
2787
+ buildSettingsCard(el, 3, '高级', '逃生舱、建议注入、通知中心与维护项', openSections[3] === true, toggleSection, [
2788
+ buildAdvancedSection(el, s, actions, reminderCenter)
2789
+ ], false)
1494
2790
  )
1495
2791
  }
1496
2792
 
1497
- function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
2793
+ // draft 预览/应用/删除动作挂到设置页 actions(保持 ProfilesSection 短小)。
2794
+ function attachDraftActions(api, s, refresh, applyWrite, actions) {
2795
+ actions.previewDraft = (draft) => () => {
2796
+ s.setError('')
2797
+ api('/draft/preview', { config: draft.config, name: draft.name, description: draft.description, source: draft.source })
2798
+ .then((data) => {
2799
+ const previews = { ...(s.draftPreviews || {}) }
2800
+ previews[draft.id] = { ok: data.ok === true, checks: Array.isArray(data.checks) ? data.checks : [] }
2801
+ s.setDraftPreviews(previews)
2802
+ })
2803
+ .catch((err) => s.setError(String((err && err.message) || err)))
2804
+ }
2805
+ actions.applyDraft = (draft) => () => {
2806
+ s.setError('')
2807
+ api('/draft/apply', { id: draft.id, name: draft.name, description: draft.description })
2808
+ .then((data) => {
2809
+ applyWrite(data, '已应用 draft:' + draft.name)
2810
+ // 闭环可视化:应用成功后展开方案管理区并高亮新方案卡片。
2811
+ if (typeof data.id === 'string' && data.id !== '') {
2812
+ s.setOpenSections((prev) => ({ ...prev, 1: true }))
2813
+ s.setHighlightId(data.id)
2814
+ setTimeout(() => s.setHighlightId(null), 8000)
2815
+ }
2816
+ refresh()
2817
+ })
2818
+ .catch((err) => s.setError(String((err && err.message) || err)))
2819
+ }
2820
+ actions.removeDraft = (draft) => () => {
2821
+ s.setError('')
2822
+ api('/draft/remove', { id: draft.id })
2823
+ .then((data) => { applyWrite(data, '已删除 draft'); refresh() })
2824
+ .catch((err) => s.setError(String((err && err.message) || err)))
2825
+ }
2826
+ }
2827
+
2828
+ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter) {
1498
2829
  function ProfilesSection() {
1499
- const [profiles, setProfiles] = useState([])
1500
- const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] })
1501
- const [enabled, setEnabled] = useState(true)
1502
- const [adding, setAdding] = useState(false)
1503
- const [editingId, setEditingId] = useState(null)
1504
- const [error, setError] = useState('')
1505
- const [savedNotice, setSavedNotice] = useState('')
1506
- const [persistWarning, setPersistWarning] = useState('')
1507
- const [form, setForm] = useState(EMPTY_FORM)
1508
- const [tools, setTools] = useState([])
1509
- const [versionWarnings, setVersionWarnings] = useState([])
1510
- const [audit, setAudit] = useState(AUDIT_DEFAULTS)
1511
- const [escapeEnabled, setEscapeEnabled] = useState(false)
1512
- const [escapePresets, setEscapePresets] = useState([])
1513
- const [escapeInput, setEscapeInput] = useState('')
1514
- const [evolutionAdvice, setEvolutionAdvice] = useState(false)
1515
- const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice }
2830
+ const [profiles, setProfiles] = useState([]); const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] }); const [enabled, setEnabled] = useState(true)
2831
+ const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState('')
2832
+ const [savedNotice, setSavedNotice] = useState(''); const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM)
2833
+ const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([]); const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS)
2834
+ const [escapeEnabled, setEscapeEnabled] = useState(false); const [escapePresets, setEscapePresets] = useState([]); const [escapeInput, setEscapeInput] = useState('')
2835
+ const [evolutionAdvice, setEvolutionAdvice] = useState(false); const [drafts, setDrafts] = useState([]); const [advice, setAdvice] = useState([])
2836
+ const [adviceGlobal, setAdviceGlobal] = useState(null); const [costSummary, setCostSummary] = useState({ global: null, byModel: [] }); const [draftPreviews, setDraftPreviews] = useState({})
2837
+ const [openSections, setOpenSections] = useState({ 1: true })
2838
+ const [highlightId, setHighlightId] = useState(null)
2839
+ const toggleSection = (key) => setOpenSections((prev) => ({ ...prev, [key]: !(prev[key] === true) }))
2840
+ const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, adviceGlobal, costSummary, draftPreviews, openSections, highlightId, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setAdviceGlobal, setCostSummary, setDraftPreviews, setOpenSections, setHighlightId }
1516
2841
  const refresh = useCallback(() => {
1517
2842
  api('/list')
1518
- .then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
2843
+ .then((data) => {
2844
+ setProfiles(Array.isArray(data.profiles) ? data.profiles : [])
2845
+ setAdvice(Array.isArray(data.advice) ? data.advice : [])
2846
+ setAdviceGlobal(data.adviceGlobal !== null && typeof data.adviceGlobal === 'object' ? data.adviceGlobal : null)
2847
+ setCostSummary(data && typeof data.costSummary === 'object' && data.costSummary !== null && typeof data.costSummary.global === 'object' ? { global: data.costSummary.global, byModel: Array.isArray(data.costSummary.byModel) ? data.costSummary.byModel : [] } : { global: null, byModel: [] })
2848
+ })
1519
2849
  .catch((err) => setError(String((err && err.message) || err)))
2850
+ api('/drafts')
2851
+ .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
2852
+ .catch(() => {})
1520
2853
  }, [])
1521
2854
  const loadOptions = useCallback(() => {
1522
2855
  api('/options/summary')
@@ -1526,22 +2859,22 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
1526
2859
  const loadEfforts = makeEffortsLoader(api, setOptions)
1527
2860
  const loadTools = makeToolsLoader(api, setTools)
1528
2861
  // 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变导致设置页打开期间无限重取。
1529
- const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings)(), [])
2862
+ const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), [])
1530
2863
  useEffect(() => {
1531
2864
  refresh()
1532
2865
  loadOptions()
1533
2866
  loadVersions()
1534
2867
  }, [refresh, loadOptions, loadVersions])
1535
- const applyWrite = makeApplyWrite(s)
1536
- const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
2868
+ const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
2869
+ attachDraftActions(api, s, refresh, applyWrite, actions)
1537
2870
  const rows = buildProfileRows(el, chip, s, actions)
1538
2871
  const toolSection = buildToolSection(el, s, actions)
1539
- return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection))
2872
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), openSections, toggleSection, el(ReminderCenter))
1540
2873
  }
1541
2874
  return ProfilesSection
1542
2875
  }
1543
2876
 
1544
- // ── 派发账本:会话标签页(React 依赖经参数注入)────────────────────────────
2877
+ // ── 派发决策台账:会话标签页(React 依赖经参数注入)──────────────────────────
1545
2878
 
1546
2879
  // epoch ms → 本地 HH:MM:SS;非有限数返回空串。
1547
2880
  function formatClockTime(time) {
@@ -1577,25 +2910,21 @@ function gateLabelZh(name) {
1577
2910
  return GATE_LABELS[name] || name
1578
2911
  }
1579
2912
 
1580
- // 闸名标签(中文 + 英文技术标识,供时间线闸头展示)。
1581
- function gateLabel(name) {
1582
- const zh = GATE_LABELS[name]
1583
- return zh !== undefined ? `${zh}(${name})` : name
1584
- }
1585
-
1586
2913
  // 失败闸修复方向;未知闸给通用提示。
1587
2914
  function gateFixHint(name) {
1588
2915
  return GATE_FIX_HINTS[name] || ZH.gateFixUnknown
1589
2916
  }
1590
2917
 
1591
- // 派发方式 → 摘要行 chip 文案。
1592
- function kindLabel(kind) {
1593
- if (kind === 'background') return ZH.toolBackground
1594
- if (kind === 'continuable') return ZH.cont
1595
- if (kind === 'foreground') return ZH.toolForeground
1596
- return kind
2918
+ // 失败闸的可操作目标:优先该记录的方案 id(成本/交集闸可去设置页改该方案),
2919
+ // 其次 whitelist 闸的请求预设(逃生舱/白名单语境)。无目标返回空串。
2920
+ function gateFixTarget(record, gate) {
2921
+ const profileId = recordProfile(record)
2922
+ if (profileId !== '') return profileId
2923
+ const requestedPreset = gate && gate.input && typeof gate.input.requestedPreset === 'string' ? gate.input.requestedPreset : ''
2924
+ return requestedPreset
1597
2925
  }
1598
2926
 
2927
+
1599
2928
  // 记录档位:effective.tokenTier 优先,回退 result.fields.tokenTier(旧记录)。
1600
2929
  function recordTier(record) {
1601
2930
  const trace = record && record.trace
@@ -1615,22 +2944,6 @@ function recordModel(record) {
1615
2944
  if (result && result.fields && typeof result.fields.model === 'string') return result.fields.model
1616
2945
  return ''
1617
2946
  }
1618
-
1619
- // 记录派发方式:execution.kind → request 标志 → result.kind 依次回退。
1620
- function recordKind(record) {
1621
- const trace = record && record.trace
1622
- const result = record && record.result
1623
- const request = record && record.request
1624
- const kind = trace && trace.execution && typeof trace.execution.kind === 'string' ? trace.execution.kind : ''
1625
- if (kind !== '') return kind
1626
- if (request && request.continuable) return 'continuable'
1627
- if (request && request.runInBackground) return 'background'
1628
- if (result && result.kind === 'background') return 'background'
1629
- if (result && result.kind === 'continuable') return 'continuable'
1630
- return 'foreground'
1631
- }
1632
-
1633
- // 记录五段分解:trace.settled.childUsage 优先,回退 result.fields.childUsage。
1634
2947
  function recordUsage(record) {
1635
2948
  const trace = record && record.trace
1636
2949
  const result = record && record.result
@@ -1646,7 +2959,7 @@ function recordUsage(record) {
1646
2959
 
1647
2960
  // 逐次调用明细:读 trace.settled.calls({calls:[{五段}], truncated, totalCalls}),
1648
2961
  // 逐字段 typeof 守卫、坏条目跳过;无 calls 或全坏返回 null(整区不渲染)。calls 仅
1649
- // 账本展开区专用,不进 render 行 / 模型可见面。
2962
+ // 台账展开区专用,不进 render 行 / 模型可见面。
1650
2963
  function recordCalls(record) {
1651
2964
  const settled = record && record.trace && record.trace.settled
1652
2965
  const raw = settled && settled.calls
@@ -1686,7 +2999,7 @@ function ledgerKey(record, index) {
1686
2999
  return `ledger-${index}`
1687
3000
  }
1688
3001
 
1689
- // ── 账本渲染 helpers(纯元素组装)──────────────────────────────────────────
3002
+ // ── 台账渲染 helpers(纯元素组装)──────────────────────────────────────────
1690
3003
 
1691
3004
  // 汇总带单条统计段:label 次级色 + 值加粗,段间「·」分隔。
1692
3005
  function summarySegments(el, pairs) {
@@ -1712,7 +3025,7 @@ function buildLedgerSummary(el, summary, profileNames) {
1712
3025
  .filter((name) => summary.gateRejectCounts[name] > 0)
1713
3026
  .map((name) => `${gateLabelZh(name)} ${summary.gateRejectCounts[name]}`)
1714
3027
  const profileText = summary.byProfile.map((item) => `${profileDisplay(item.profile, profileNames)} ×${item.count}`).join(' · ')
1715
- // 模型原文显示;继承桶(空串)与 'inherit'/'(parent)'/以 '(' 开头的标记一律归一为「继承父」。
3028
+ // 模型原文显示;继承桶(空串)与 'inherit'/'(parent)'/以 '(' 开头的标记一律归一为「继承父会话」。
1716
3029
  const modelText = summary.byModel.map((item) => {
1717
3030
  const display = item.model === 'inherit' || item.model.startsWith('(') ? ZH.inherit : rawOrInherit(item.model)
1718
3031
  return `${display} ×${item.count}`
@@ -1732,6 +3045,18 @@ function buildLedgerSummary(el, summary, profileNames) {
1732
3045
  : null,
1733
3046
  tierText !== ''
1734
3047
  ? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByTier}:${tierText}`)
3048
+ : null,
3049
+ summary.pricedCount > 0
3050
+ ? el('div', { className: 'sap-ledgerSummaryLine' },
3051
+ ZH.ledgerCostLine
3052
+ .replace('{actual}', String(summary.estimatedCost))
3053
+ .replace('{counter}', String(summary.estimatedInheritCost))
3054
+ .replace('{saving}', String(summary.estimatedSaving))
3055
+ .replace('{ratio}', summary.estimatedSaving > 0 && summary.estimatedCost > 0 ? String(Number((summary.estimatedInheritCost / summary.estimatedCost).toFixed(2))) : '1'))
3056
+ : null,
3057
+ summary.inheritCount > 0
3058
+ ? el('div', { className: 'sap-ledgerSummaryLine' },
3059
+ ZH.ledgerInheritCost.replace('{count}', String(summary.inheritCount)).replace('{ratio}', String(summary.inheritRatio)))
1735
3060
  : null
1736
3061
  )
1737
3062
  }
@@ -1754,7 +3079,7 @@ function buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder) {
1754
3079
  }, loadingOlder ? ZH.ledgerLoadingOlder : ZH.ledgerLoadOlder)
1755
3080
  }
1756
3081
 
1757
- // 账本顶部说明行:定位说明(恒显)+ 窗口提示(hasMore 时显示已加载条数)。
3082
+ // 台账顶部说明行:定位说明(恒显)+ 窗口提示(hasMore 时显示已加载条数)。
1758
3083
  function buildLedgerTopNote(el, hasMore, count) {
1759
3084
  return el('div', null,
1760
3085
  el('div', { className: 'sap-ledgerScope' }, ZH.ledgerScopeNote),
@@ -1771,6 +3096,12 @@ function formatGateScalar(v) {
1771
3096
  return String(v)
1772
3097
  }
1773
3098
 
3099
+ // 父会话 UUID 截断:前 8 位 + …,悬停 title 显示全文(不足 8 位原样,fail-soft)。
3100
+ function truncateSessionId(v) {
3101
+ if (typeof v !== 'string' || v === '') return v
3102
+ return v.length > 8 ? v.slice(0, 8) + '…' : v
3103
+ }
3104
+
1774
3105
  // 对象/数组 → 逐字段键值行数组(depth 上限 2 防深层爆炸);标量 → 单行文本。
1775
3106
  // 对象逐键「中文标签(键名): 值」;嵌套容器递归 1 层并包缩进块(sap-ledgerGateNested,
1776
3107
  // 左侧竖线视觉分层,profiles_snapshot 等嵌套结构可读);数组前 5 项逐项展示,
@@ -1798,7 +3129,7 @@ function gateValueRows(el, value, depth = 0) {
1798
3129
  }
1799
3130
  const rows = []
1800
3131
  for (const [key, v] of Object.entries(value)) {
1801
- const label = GATE_KEY_LABELS[key] !== undefined ? `${GATE_KEY_LABELS[key]}(${key})` : key
3132
+ const label = FIELD_ZH[key] !== undefined ? `${FIELD_ZH[key]}(${key})` : key
1802
3133
  if (v !== null && typeof v === 'object' && depth < 2) {
1803
3134
  rows.push(el('div', { className: 'sap-ledgerGateKV' },
1804
3135
  el('span', { className: 'sap-ledgerGateKey' }, label), ':'))
@@ -1806,7 +3137,9 @@ function gateValueRows(el, value, depth = 0) {
1806
3137
  } else {
1807
3138
  rows.push(el('div', { className: 'sap-ledgerGateKV' },
1808
3139
  el('span', { className: 'sap-ledgerGateKey' }, label), ':',
1809
- el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(v))))
3140
+ key === 'parentSessionId' && typeof v === 'string' && v !== ''
3141
+ ? el('span', { className: 'sap-ledgerGateValue', title: v }, truncateSessionId(v))
3142
+ : el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(v))))
1810
3143
  }
1811
3144
  }
1812
3145
  return rows
@@ -1847,17 +3180,237 @@ function buildJsonBody(el, kv) {
1847
3180
  )
1848
3181
  }
1849
3182
 
1850
- // 决策时间线步骤 ①:发起时间 + 父会话上下文 + 请求参数(各为独立子块卡片;
1851
- // 头部右侧按钮切换 字段视图 原始 JSON)。
1852
- function buildStartStep(el, trace, jsonView, toggleJson) {
3183
+
3184
+ // ── 三层信息架构(结论层 / 证据层)纯渲染 ─────────────────────────
3185
+
3186
+ // 停滞检测阈值(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
3187
+ const STALL_THRESHOLD_MS = 30000
3188
+
3189
+ // 折叠态状态 chip:拒绝(红)/执行中(琥珀脉冲)/异常(琥珀)/成功(绿)。
3190
+ function ledgerStatusOf(record) {
3191
+ if (recordIsRejected(record)) return { cls: 'sap-ledgerStatusFail', label: ZH.ledgerStatusReject }
3192
+ const settled = record.trace && record.trace.settled
3193
+ if (record.trace && !settled) return { cls: 'sap-ledgerStatusRun', label: ZH.ledgerStatusRunning }
3194
+ if (settled) {
3195
+ const stop = recordStopZh(record)
3196
+ if (stop !== '' && stop !== ZH.stopReasonZh.completed) return { cls: 'sap-ledgerStatusRun', label: ZH.ledgerStatusAbnormal + '(' + stop + ')' }
3197
+ }
3198
+ return { cls: 'sap-ledgerStatusOk', label: ZH.ledgerStatusSuccess }
3199
+ }
3200
+
3201
+ // 折叠态工具数:continuable 交集实得数优先,其次子会话快照推导(实际调用过的工具)。
3202
+ function ledgerToolsCountOf(record, proc) {
3203
+ if (recordIsRejected(record)) return ZH.ledgerToolsPending
3204
+ const n = effectiveToolsCountOf(record)
3205
+ if (n !== null) return ZH.toolAllowlist + ' ' + n
3206
+ if (proc !== null && proc.toolsUsed !== null) return ZH.toolAllowlist + ' ' + proc.toolsUsed.length
3207
+ return ''
3208
+ }
3209
+
3210
+ // 执行过程事件线(台账记事实不记判断):从子会话快照节点推导——
3211
+ // 开始/工具调用/停滞/主 agent 干预/恢复/完成。干预 = 非 seed 的 user 节点(内容
3212
+ // 只取 40 字摘要,不落原文)。nodes 不可得返回空事件集(fail-soft 不虚报)。
3213
+ function processEventsOf(nodes, settled) {
3214
+ const out = { events: [], toolCalls: 0, interventions: 0, stalled: false, toolsUsed: null }
3215
+ if (!Array.isArray(nodes) || nodes.length === 0) return out
3216
+ const used = new Set()
3217
+ const first = nodes[0] && typeof nodes[0].time === 'number' ? nodes[0].time : null
3218
+ out.events.push({ kind: 'start', time: first })
3219
+ let lastActivity = null
3220
+ let stallOpen = false
3221
+ let sawUser = false
3222
+ for (let i = 0; i < nodes.length; i += 1) {
3223
+ const node = nodes[i]
3224
+ if (!node || typeof node !== 'object') continue
3225
+ const time = typeof node.time === 'number' ? node.time : null
3226
+ if (node.kind === 'tool-result') {
3227
+ const name = node.call && typeof node.call.name === 'string' ? node.call.name : ''
3228
+ if (name !== '') used.add(name)
3229
+ out.toolCalls += 1
3230
+ if (lastActivity !== null && time !== null && time - lastActivity > STALL_THRESHOLD_MS) {
3231
+ out.events.push({ kind: 'stall', time, seconds: Math.round((time - lastActivity) / 1000) })
3232
+ out.stalled = true
3233
+ stallOpen = true
3234
+ }
3235
+ if (stallOpen) { out.events.push({ kind: 'resume', time }); stallOpen = false }
3236
+ out.events.push({ kind: 'call', time, index: out.toolCalls, tool: name })
3237
+ lastActivity = time !== null ? time : lastActivity
3238
+ } else if (node.kind === 'user') {
3239
+ if (sawUser) {
3240
+ out.interventions += 1
3241
+ out.events.push({ kind: 'intervene', time, text: firstContentLine(node.content).slice(0, 40) })
3242
+ lastActivity = time !== null ? time : lastActivity
3243
+ }
3244
+ sawUser = true
3245
+ }
3246
+ }
3247
+ out.events.push(settled !== null && settled !== undefined ? { kind: 'done', time: null } : { kind: 'running', time: null })
3248
+ out.toolsUsed = [...used]
3249
+ return out
3250
+ }
3251
+
3252
+ // 结论层进度点:拒绝只显示到达阶段;运行中执行琥珀脉冲;结算异常结算琥珀。
3253
+ function progressStepsOf(record, proc) {
3254
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
3255
+ const gateFailed = gates.some((g) => g.verdict === 'fail')
3256
+ const settled = record.trace && record.trace.settled
3257
+ if (recordIsRejected(record)) {
3258
+ return [
3259
+ { key: 'start', label: ZH.ledgerProgressZh.start, state: 'done' },
3260
+ { key: 'decide', label: ZH.ledgerProgressZh.decide, state: gateFailed ? 'warn' : 'done' },
3261
+ { key: 'stop', label: ZH.ledgerProgressZh.stop, state: gateFailed ? 'warn' : 'done' }
3262
+ ]
3263
+ }
3264
+ if (record.trace && !settled) {
3265
+ return [
3266
+ { key: 'start', label: ZH.ledgerProgressZh.start, state: 'done' },
3267
+ { key: 'decide', label: ZH.ledgerProgressZh.decide, state: 'done' },
3268
+ { key: 'run', label: ZH.ledgerProgressZh.run, state: 'run' },
3269
+ { key: 'settle', label: ZH.ledgerProgressZh.settle, state: 'todo' }
3270
+ ]
3271
+ }
3272
+ const stop = recordStopZh(record)
3273
+ const settleState = stop !== '' && stop !== ZH.stopReasonZh.completed ? 'warn' : 'done'
3274
+ const runState = proc !== null && proc.stalled ? 'warn' : 'done'
3275
+ return [
3276
+ { key: 'start', label: ZH.ledgerProgressZh.start, state: 'done' },
3277
+ { key: 'decide', label: ZH.ledgerProgressZh.decide, state: 'done' },
3278
+ { key: 'run', label: ZH.ledgerProgressZh.run, state: runState },
3279
+ { key: 'settle', label: ZH.ledgerProgressZh.settle, state: settleState }
3280
+ ]
3281
+ }
3282
+
3283
+ // 安全检查五道闸徽标汇总:全过(5/5)单徽标;有失败展开五项。
3284
+ function safetyBadgesOf(record) {
3285
+ const names = ['whitelist', 'cost', 'intersection', 'approval', 'budget']
3286
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
3287
+ const badges = []
3288
+ let failedName = ''
3289
+ for (const name of names) {
3290
+ const gate = gates.find((g) => g.name === name)
3291
+ const verdict = gate ? gate.verdict : ''
3292
+ if (verdict === 'fail') { badges.push({ name, ok: false }); if (failedName === '') failedName = name } else if (verdict === 'pass' || verdict === 'deferred') badges.push({ name, ok: true })
3293
+ }
3294
+ const allPass = badges.length === names.length && failedName === ''
3295
+ return { allPass, failedName, badges }
3296
+ }
3297
+
3298
+ // 结论层:安全检查徽标(全过单徽标;失败展开五项,❌ 项红色)。
3299
+ function buildSafetyBadges(el, record) {
3300
+ const s = safetyBadgesOf(record)
3301
+ if (s.allPass) return el('div', { className: 'sap-ledgerSafety' },
3302
+ el('span', { className: 'sap-ledgerBadge sap-ledgerBadgeOk' }, ZH.ledgerSafetyAllPass)
3303
+ )
3304
+ return el('div', { className: 'sap-ledgerSafety' },
3305
+ el('span', { className: 'sap-ledgerBadge sap-ledgerBadgeFail' }, ZH.ledgerSafetyFail.replace('{gate}', ZH.ledgerGateBadgeZh[s.failedName] || s.failedName)),
3306
+ s.badges.map((b) => el('span', {
3307
+ key: 'gate-badge-' + b.name,
3308
+ className: b.ok ? 'sap-ledgerBadge sap-ledgerBadgeOk' : 'sap-ledgerBadge sap-ledgerBadgeFail'
3309
+ }, (ZH.ledgerGateBadgeZh[b.name] || b.name) + ' ' + (b.ok ? '✓' : '❌')))
3310
+ )
3311
+ }
3312
+
3313
+ // 结论层:进度点一行(发起 → 决策 → 执行 → 结算)。
3314
+ function buildProgressDots(el, record, proc) {
3315
+ const steps = progressStepsOf(record, proc)
3316
+ const items = []
3317
+ steps.forEach((step, i) => {
3318
+ items.push(el('span', { key: step.key, className: 'sap-ledgerProgressStep' },
3319
+ el('span', { className: 'sap-ledgerProgressDot sap-ledgerProgressDot-' + step.state }),
3320
+ el('span', null, step.label)
3321
+ ))
3322
+ if (i < steps.length - 1) items.push(el('span', { key: step.key + '-sep', className: 'sap-ledgerProgressSep' }))
3323
+ })
3324
+ return el('div', { className: 'sap-ledgerProgress' }, items)
3325
+ }
3326
+
3327
+ // 结论层:成本卡(本会话口径 · ≈X 元 · 若继承父会话 ≈Y 元 · 省 ≈Z 元 · 模型/强度摘要)。
3328
+ function buildCostCard(el, record) {
3329
+ const info = recordCostInfo(record)
3330
+ if (!info) return null
3331
+ const eff = record.trace && record.trace.effective ? record.trace.effective : {}
3332
+ const summary = ZH.chipModel + ':' + rawOrInherit(eff.model) + ' · ' + ZH.chipEffort + ':' + effortZh(eff.reasoningEffort)
3333
+ if (info.inherit) {
3334
+ return el('div', { className: 'sap-ledgerCostCard' },
3335
+ el('span', null, ZH.ledgerCostCardScope, ' · ', el('b', null, '≈' + info.actual + ' 元')),
3336
+ el('span', null, ZH.ledgerSettleInheritNote),
3337
+ el('span', null, summary)
3338
+ )
3339
+ }
3340
+ return el('div', { className: 'sap-ledgerCostCard' },
3341
+ el('span', null, ZH.ledgerCostCardScope, ' · ', el('b', null, '≈' + info.actual + ' 元')),
3342
+ el('span', null, ZH.ledgerCostCardCounter, ' ', el('b', null, '≈' + info.counterfactual + ' 元')),
3343
+ el('span', { className: 'sap-ledgerCostSave' }, ZH.ledgerCostCardSave, ' ', el('b', null, '≈' + info.saving + ' 元')),
3344
+ el('span', null, summary)
3345
+ )
3346
+ }
3347
+
3348
+ // 结论层:拒绝块(拒绝:原因 + 修复方向可点击复制目标)。
3349
+ function rejectGateOf(record) {
3350
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
3351
+ return gates.find((g) => g.verdict === 'fail') || null
3352
+ }
3353
+ function buildRejectBlock(el, record, copiedTarget, onCopyTarget) {
3354
+ const gate = rejectGateOf(record)
3355
+ if (gate === null && record.isError !== true) return null
3356
+ const reason = gate !== null && typeof gate.reason === 'string' && gate.reason !== '' ? gate.reason : ZH.ledgerReject
3357
+ const hint = gate !== null ? gateFixHint(gate.name) : ZH.gateFixUnknown
3358
+ const fixTarget = gate !== null ? gateFixTarget(record, gate) : ''
3359
+ return el('div', { className: 'sap-ledgerRejectBlock' },
3360
+ el('div', { className: 'sap-ledgerReject' }, ZH.ledgerReject + ':' + reason),
3361
+ el('div', { className: 'sap-ledgerRejectFix' },
3362
+ ZH.ledgerRejectFix + ':' + hint,
3363
+ fixTarget !== ''
3364
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: onCopyTarget(fixTarget) }, copiedTarget === fixTarget ? ZH.ledgerCopied : ZH.ledgerCopyTarget + ' ' + fixTarget)
3365
+ : null
3366
+ )
3367
+ )
3368
+ }
3369
+
3370
+ // 结论层:干预摘要(仅存在干预事件时显示)。
3371
+ function buildInterventionSummary(el, record, proc) {
3372
+ if (proc === null || proc.interventions === 0) return null
3373
+ const lead = proc.stalled ? '执行中出现停滞' : '执行过程'
3374
+ return el('div', { className: 'sap-ledgerIntervention' }, lead + ',主 agent 干预 ' + proc.interventions + ' 次')
3375
+ }
3376
+
3377
+ // 结论层(点行展开):进度点 → 安全检查徽标 → 成本卡 → 拒绝块 → 干预摘要 → 因果链入口。
3378
+ function buildConclusionArea(el, record, proc, copiedTarget, onCopyTarget, evidenceOpen, setEvidence) {
3379
+ return el('div', { className: 'sap-ledgerExpand' },
3380
+ buildProgressDots(el, record, proc),
3381
+ buildSafetyBadges(el, record),
3382
+ buildCostCard(el, record),
3383
+ buildRejectBlock(el, record, copiedTarget, onCopyTarget),
3384
+ buildInterventionSummary(el, record, proc),
3385
+ el('button', { type: 'button', className: 'sap-ledgerEvidenceToggle', onClick: () => setEvidence(!evidenceOpen) }, evidenceOpen ? ZH.ledgerEvidenceClose : ZH.ledgerEvidenceToggle)
3386
+ )
3387
+ }
3388
+
3389
+ // 请求参数人话格式('=' 仅用于本行键值,非内部键残留):模型=pro · 强度=未指定 · 档位=cheap。
3390
+ function requestHumanText(requested) {
3391
+ const req = requested || {}
3392
+ const model = typeof req.model === 'string' && req.model !== '' ? req.model : ZH.ledgerVsUnset
3393
+ const effort = typeof req.reasoningEffort === 'string' && req.reasoningEffort !== '' ? effortZh(req.reasoningEffort) : ZH.ledgerVsUnset
3394
+ const tier = typeof req.tokenTier === 'string' && req.tokenTier !== '' ? (ZH.tierZh[req.tokenTier] || req.tokenTier) : ZH.ledgerVsUnset
3395
+ return ZH.chipModel + '=' + model + ' · ' + ZH.chipEffort + '=' + effort + ' · ' + ZH.chipTier + '=' + tier
3396
+ }
3397
+
3398
+ // 证据层 ① 发起:时间 · 父会话(UUID 截断,悬停全文)· 请求参数 · 任务指令(派发前)。
3399
+ // 任务指令取 trace.requested.prompt_excerpt(与任务摘要同源 200 字符截断);旧记录
3400
+ // 无摘要时不渲染该行,绝不拿结果块首行冒充指令。
3401
+ function buildStartBlock(el, record, jsonView, toggleJson) {
3402
+ const trace = record.trace
1853
3403
  const startedText = formatClockTime(trace.startedAt)
1854
- const head = startedText !== '' ? `${ZH.ledgerStarted} · ${startedText}` : ZH.ledgerStarted
1855
- const jsonMode = jsonView['start'] === true
3404
+ const head = startedText !== '' ? ZH.cogPerceiveDecide + ' · ' + startedText : ZH.cogPerceiveDecide
3405
+ const jsonMode = jsonView.start === true
3406
+ const execution = trace.execution || {}
3407
+ const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
3408
+ const promptExcerpt = trace.requested && typeof trace.requested.prompt_excerpt === 'string' && trace.requested.prompt_excerpt !== '' ? trace.requested.prompt_excerpt : ''
1856
3409
  const kv = [
1857
3410
  { label: ZH.ledgerParentContext, value: trace.parentContext },
1858
3411
  { label: ZH.ledgerRequested, value: trace.requested }
1859
3412
  ]
1860
- return el('div', { key: 'step-start', className: 'sap-ledgerStep' },
3413
+ return el('div', { key: 'block-start', className: 'sap-ledgerStep' },
1861
3414
  el('div', { className: 'sap-ledgerStepHead' },
1862
3415
  el('span', null, head),
1863
3416
  buildViewToggle(el, jsonMode, () => toggleJson('start'))
@@ -1865,109 +3418,284 @@ function buildStartStep(el, trace, jsonView, toggleJson) {
1865
3418
  jsonMode
1866
3419
  ? buildJsonBody(el, kv)
1867
3420
  : el('div', { className: 'sap-ledgerStepBody' },
3421
+ parentSessionId !== ''
3422
+ ? el('div', { className: 'sap-ledgerGateKV' },
3423
+ el('span', { className: 'sap-ledgerGateKey' }, ZH.ledgerParentSessionLabel), ':',
3424
+ el('span', { className: 'sap-ledgerGateValue', title: parentSessionId }, truncateSessionId(parentSessionId)))
3425
+ : null,
3426
+ el('div', { className: 'sap-ledgerGateKV' },
3427
+ el('span', { className: 'sap-ledgerGateKey' }, ZH.ledgerRequestHuman), ':',
3428
+ el('span', { className: 'sap-ledgerGateValue' }, requestHumanText(trace.requested))),
3429
+ el('div', { className: 'sap-ledgerSource' }, '来源:系统(父会话快照)'),
1868
3430
  buildSubBlock(el, ZH.ledgerParentContext, trace.parentContext),
1869
- buildSubBlock(el, ZH.ledgerRequested, trace.requested)
1870
- )
1871
- )
1872
- }
1873
-
1874
- // 单道闸三段式:闸头(名称 + verdict + 视图切换)→ 输入/输出子块(或合并 JSON 视图)
1875
- // → 结论;失败闸浅红底 + 原因 + 修复方向。拒绝信息两种视图下都显示。
1876
- function buildGateStep(el, gate, index, jsonView, toggleJson) {
1877
- const failed = gate.verdict === 'fail'
1878
- const passed = gate.verdict === 'pass'
1879
- const verdict = passed ? `✅ ${ZH.ledgerPass}` : (failed ? `❌ ${ZH.ledgerReject}` : gate.verdict)
1880
- const verdictCls = failed ? 'sap-ledgerGateVerdictFail' : (passed ? 'sap-ledgerGateVerdictPass' : '')
1881
- const key = `gate-${index}`
1882
- const jsonMode = jsonView[key] === true
1883
- const kv = [
1884
- { label: ZH.ledgerGateInput, value: gate.input },
1885
- { label: ZH.ledgerGateOutput, value: gate.output }
1886
- ]
1887
- const rejectRows = failed
1888
- ? [
1889
- el('div', { key: 'reject', className: 'sap-ledgerReject' }, gate.reason !== '' ? `${ZH.ledgerReject}:${gate.reason}` : ZH.ledgerReject),
1890
- el('div', { key: 'rejectFix', className: 'sap-ledgerRejectFix' }, `${ZH.ledgerRejectFix}:${gateFixHint(gate.name)}`)
1891
- ]
1892
- : []
1893
- return el('div', { key, className: failed ? 'sap-ledgerGate sap-ledgerGateFail' : 'sap-ledgerGate' },
1894
- el('div', { className: 'sap-ledgerGateHead' },
1895
- el('span', { className: 'sap-ledgerGateName' }, gateLabel(gate.name)),
1896
- el('span', { className: `sap-ledgerGateVerdict ${verdictCls}` }, verdict),
1897
- buildViewToggle(el, jsonMode, () => toggleJson(key))
1898
- ),
1899
- jsonMode
1900
- ? el('div', { className: 'sap-ledgerGateBody' }, buildJsonBody(el, kv), ...rejectRows)
1901
- : el('div', { className: 'sap-ledgerGateBody' },
1902
- buildSubBlock(el, ZH.ledgerGateInput, gate.input),
1903
- buildSubBlock(el, ZH.ledgerGateOutput, gate.output),
1904
- ...rejectRows
3431
+ el('div', { className: 'sap-ledgerSource' }, '来源:用户/父上下文(输入原文摘要)'),
3432
+ buildSubBlock(el, ZH.ledgerRequested, trace.requested),
3433
+ promptExcerpt !== ''
3434
+ ? el('div', { className: 'sap-cogParentText' }, ZH.ledgerPromptQuote + ':『' + promptExcerpt + '』')
3435
+ : null
1905
3436
  )
1906
3437
  )
1907
3438
  }
1908
3439
 
1909
- // 决策时间线:① 发起 + 各闸。步骤元素带稳定 key(静态列表也满足 React key 契约)。
1910
- // jsonView/toggleJson:区块级「字段视图 ↔ 原始 JSON」切换状态(LedgerRow 持有)。
1911
- function buildDecisionTimeline(el, record, jsonView, toggleJson) {
3440
+ // 生效工具数:continuable 交集实得数 effective.effectiveToolCount 未知 null。
3441
+ function effectiveToolsCountOf(record) {
1912
3442
  const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
1913
- const blocks = [buildStartStep(el, record.trace, jsonView, toggleJson)]
1914
- gates.forEach((gate, index) => blocks.push(buildGateStep(el, gate, index, jsonView, toggleJson)))
1915
- return el('div', { className: 'sap-ledgerSection' },
1916
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerDecisionTimeline),
1917
- el('div', { className: 'sap-ledgerTimeline' }, blocks)
1918
- )
3443
+ const inter = gates.find((g) => g.name === 'intersection')
3444
+ if (inter && inter.output && Number.isFinite(inter.output.effectiveAllowCount)) return inter.output.effectiveAllowCount
3445
+ const eff = record.trace && record.trace.effective
3446
+ if (eff && Number.isFinite(eff.effectiveToolCount)) return eff.effectiveToolCount
3447
+ return null
1919
3448
  }
1920
3449
 
1921
- // 生效值 chips + ⬆被忽略项(沿用既有 ignored chip 风格)。
1922
- function buildEffectiveSection(el, chip, record, profileNames) {
3450
+ // 证据层 决策 chips 行(中文对照,忽略项键名中文化)。
3451
+ function buildEffectiveChips(el, chip, record, profileNames) {
1923
3452
  const eff = record.trace.effective || {}
1924
- // 拒绝路径无生效值(闸 fail 即中止派发)——整节省略。
1925
3453
  if (Object.keys(eff).length === 0) return null
1926
3454
  const requested = record.trace.requested || {}
1927
3455
  const chips = []
1928
3456
  chips.push(chip('profile', ZH.chipProfile, profileDisplay(eff.profile, profileNames)))
1929
- chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: !eff.preset }))
3457
+ chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: !eff.preset || eff.preset === 'inherit' }))
1930
3458
  const p = rawOrInherit(eff.provider)
1931
3459
  const m = rawOrInherit(eff.model)
1932
- chips.push(chip('providerModel', `${ZH.chipProvider}·${ZH.chipModel}`, p === ZH.inherit && m === ZH.inherit ? ZH.inherit : `${p} / ${m}`, { dim: p === ZH.inherit && m === ZH.inherit }))
1933
- chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: !eff.reasoningEffort }))
3460
+ chips.push(chip('providerModel', ZH.chipProvider + '·' + ZH.chipModel, p === ZH.inherit && m === ZH.inherit ? ZH.inherit : p + ' / ' + m, { dim: p === ZH.inherit && m === ZH.inherit }))
3461
+ chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: !eff.reasoningEffort || eff.reasoningEffort === '(default)' }))
1934
3462
  const tier = typeof eff.tokenTier === 'string' && eff.tokenTier !== '' ? eff.tokenTier : ''
1935
3463
  if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
3464
+ const toolsCount = effectiveToolsCountOf(record)
3465
+ if (toolsCount !== null) chips.push(chip('tools', ZH.toolAllowlist, String(toolsCount)))
1936
3466
  const ignored = Array.isArray(eff.ignored) ? eff.ignored : []
1937
3467
  for (const key of ignored) {
1938
3468
  const reqVal = typeof requested[key] === 'string' && requested[key] !== '' ? requested[key] : ''
1939
3469
  chips.push(el('span', {
1940
- key: `ignored-${key}`,
3470
+ key: 'ignored-' + key,
1941
3471
  className: 'sap-chip sap-chipWarn',
1942
3472
  title: reqVal !== '' ? ZH.ignoredChipTitle.replace('{value}', reqVal) : ZH.ignoredChipTitleEmpty
1943
- }, `⬆${key}(${ZH.ignoredMark})`))
3473
+ }, '⬆' + (FIELD_ZH[key] || key) + '(' + ZH.ignoredMark + ')'))
3474
+ }
3475
+ chips.push(...buildAuditChips(el, record.trace))
3476
+ return el('div', { className: 'sap-chips' }, chips)
3477
+ }
3478
+
3479
+ // 请求 vs 生效对照表单行:差异标在生效侧,说明列写原因;继承表述「继承父会话(模型名)」。
3480
+ function requestVsRowOf(key, label, requested, effective) {
3481
+ const ignored = Array.isArray(effective.ignored) ? effective.ignored : []
3482
+ const reqRaw = typeof requested[key] === 'string' ? requested[key] : ''
3483
+ const effRaw = typeof effective[key] === 'string' ? effective[key] : ''
3484
+ const zhOf = (v) => {
3485
+ if (v === '') return ZH.ledgerVsUnset
3486
+ if (key === 'reasoningEffort') return effortZh(v)
3487
+ if (key === 'tokenTier') return ZH.tierZh[v] || v
3488
+ return v
3489
+ }
3490
+ const reqText = zhOf(reqRaw)
3491
+ let effText = zhOf(effRaw)
3492
+ let note = ''
3493
+ let diff = false
3494
+ if (ignored.includes(key)) {
3495
+ diff = true
3496
+ effText = ZH.inherit
3497
+ note = ZH.ledgerVsIgnored.replace('{v}', reqText)
3498
+ } else if (key === 'model' && reqRaw === '' && effRaw !== '') {
3499
+ effText = ZH.ledgerVsInherit.replace('{model}', effRaw)
3500
+ } else if (reqRaw === '' && effRaw !== '') {
3501
+ note = ZH.ledgerVsDefault
3502
+ } else if (reqRaw !== '' && effRaw !== '' && reqRaw !== effRaw) {
3503
+ diff = true
3504
+ note = '请求了 ' + reqText + ',未生效'
3505
+ }
3506
+ return { key, label, request: reqText, effective: effText, note, diff }
3507
+ }
3508
+
3509
+ // 证据层 ② 请求 vs 生效对照表(差异行微琥珀底;全一致折叠为「请求全部生效 ✓」)。
3510
+ function buildRequestVsTable(el, record) {
3511
+ const trace = record.trace
3512
+ const requested = trace.requested || {}
3513
+ const effective = trace.effective || {}
3514
+ if (Object.keys(effective).length === 0) return null
3515
+ const rows = [
3516
+ requestVsRowOf('model', ZH.chipModel, requested, effective),
3517
+ requestVsRowOf('reasoningEffort', ZH.chipEffort, requested, effective),
3518
+ requestVsRowOf('tokenTier', ZH.chipTier, requested, effective)
3519
+ ]
3520
+ const diffCount = rows.filter((r) => r.diff).length
3521
+ const headText = diffCount === 0
3522
+ ? ZH.ledgerVsTitle + ' · ' + ZH.ledgerVsAllMatch
3523
+ : ZH.ledgerVsTitle + ' · ' + ZH.ledgerVsDiff.replace('{n}', String(diffCount))
3524
+ const table = el('table', { className: 'sap-ledgerVsTable' },
3525
+ el('thead', null, el('tr', null,
3526
+ el('th', null, ZH.ledgerVsItem),
3527
+ el('th', null, ZH.ledgerVsRequest),
3528
+ el('th', null, ZH.ledgerVsEffective),
3529
+ el('th', null, ZH.ledgerVsNote)
3530
+ )),
3531
+ el('tbody', null, rows.map((r) => el('tr', { key: r.key, className: r.diff ? 'sap-ledgerVsDiff' : null },
3532
+ el('td', null, r.label),
3533
+ el('td', null, r.request),
3534
+ el('td', null, el('span', { className: 'sap-ledgerVsEff' }, r.effective)),
3535
+ el('td', { className: 'sap-ledgerVsNote' }, r.note)
3536
+ )))
3537
+ )
3538
+ if (diffCount === 0) {
3539
+ return el('details', { className: 'sap-customized' },
3540
+ el('summary', { className: 'sap-customizedSummary' }, headText),
3541
+ el('div', { className: 'sap-customizedBody' }, table)
3542
+ )
1944
3543
  }
1945
- return el('div', { className: 'sap-ledgerSection' },
1946
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerEffectiveStep),
1947
- el('div', { className: 'sap-chips' }, chips)
3544
+ return el('div', null,
3545
+ el('div', { className: 'sap-ledgerVsHead' }, headText),
3546
+ table
3547
+ )
3548
+ }
3549
+
3550
+ // 白名单候选数(whitelist 闸 input,截断名单认 values)。
3551
+ function whitelistCountOf(record) {
3552
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
3553
+ const wl = gates.find((g) => g.name === 'whitelist')
3554
+ const raw = wl && wl.input ? wl.input.systemTrustCandidates : null
3555
+ if (Array.isArray(raw)) return raw.length
3556
+ if (raw && typeof raw === 'object' && Array.isArray(raw.values)) return raw.values.length
3557
+ return null
3558
+ }
3559
+ function intersectionGateOf(record) {
3560
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
3561
+ return gates.find((g) => g.name === 'intersection') || null
3562
+ }
3563
+ function removedToolsOf(record) {
3564
+ const inter = intersectionGateOf(record)
3565
+ if (!inter || !inter.output) return null
3566
+ const raw = inter.output.removedTools
3567
+ if (Array.isArray(raw)) return raw
3568
+ if (raw && typeof raw === 'object' && Array.isArray(raw.values)) return raw.values
3569
+ return null
3570
+ }
3571
+ // 工具名「英文(中文名)」(中文名查工具目录映射,查不到原样)。
3572
+ function toolNameZh(name, toolZh) {
3573
+ const zh = toolZh !== null && typeof toolZh === 'object' ? toolZh[name] : undefined
3574
+ return typeof zh === 'string' && zh !== '' ? name + '(' + zh + ')' : name
3575
+ }
3576
+
3577
+ // 证据层 ② 工具集子节(默认折叠):白名单 X ∩ 父会话 Y → 实得 Z · 移除 N 个;
3578
+ // 移除清单完整展示(表格滚动兜底);非 continuable 诚实降级。
3579
+ function buildToolsetSubSection(el, record, toolZh) {
3580
+ const x = whitelistCountOf(record)
3581
+ const inter = intersectionGateOf(record)
3582
+ const y = inter && inter.input ? inter.input.parentToolCount : null
3583
+ const z = effectiveToolsCountOf(record)
3584
+ const removed = removedToolsOf(record)
3585
+ const zText = z !== null ? String(z) : ZH.ledgerToolsetGranted
3586
+ const formula = ZH.ledgerToolsetFormula
3587
+ .replace('{x}', x !== null ? String(x) : '?')
3588
+ .replace('{y}', y !== undefined && y !== null ? String(y) : '?')
3589
+ .replace('{z}', zText)
3590
+ const removedText = removed !== null && removed.length > 0 ? ' · ' + ZH.ledgerToolsetRemoved.replace('{n}', String(removed.length)) : ''
3591
+ const body = removed !== null && removed.length > 0
3592
+ ? el('div', { className: 'sap-ledgerToolScroll' },
3593
+ el('table', { className: 'sap-ledgerToolTable' },
3594
+ el('thead', null, el('tr', null, el('th', null, ZH.ledgerToolCol), el('th', null, ZH.ledgerToolReasonCol))),
3595
+ el('tbody', null, removed.map((item) => el('tr', { key: item.name },
3596
+ el('td', { className: 'sap-ledgerToolName' }, toolNameZh(item.name, toolZh)),
3597
+ el('td', null, typeof item.reason === 'string' ? item.reason : '')
3598
+ )))
3599
+ ))
3600
+ : el('div', { className: 'sap-ledgerUsage' }, ZH.ledgerToolsetHostNarrow)
3601
+ return el('details', { className: 'sap-customized' },
3602
+ el('summary', { className: 'sap-customizedSummary' }, ZH.ledgerToolsetTitle + ' · ' + formula + removedText),
3603
+ el('div', { className: 'sap-customizedBody' }, body)
3604
+ )
3605
+ }
3606
+
3607
+ // 证据层 ② 派发前 · 安全检查子节(默认折叠;失败默认展开并附拒绝块)。
3608
+ function buildPrecheckSubSection(el, record, copiedTarget, onCopyTarget) {
3609
+ const s = safetyBadgesOf(record)
3610
+ const title = s.allPass
3611
+ ? ZH.ledgerPrecheckTitle + ' ✅ 5/5 ' + ZH.ledgerPass
3612
+ : ZH.ledgerPrecheckTitle + ' ' + ZH.ledgerSafetyFail.replace('{gate}', ZH.ledgerGateBadgeZh[s.failedName] || s.failedName)
3613
+ const badges = el('div', { className: 'sap-ledgerSafety' },
3614
+ s.badges.map((b) => el('span', {
3615
+ key: 'precheck-' + b.name,
3616
+ className: b.ok ? 'sap-ledgerBadge sap-ledgerBadgeOk' : 'sap-ledgerBadge sap-ledgerBadgeFail'
3617
+ }, (ZH.ledgerGateBadgeZh[b.name] || b.name) + ' ' + (b.ok ? '✓' : '❌')))
3618
+ )
3619
+ const reject = s.failedName !== '' ? buildRejectBlock(el, record, copiedTarget, onCopyTarget) : null
3620
+ return el('details', { className: 'sap-customized', open: !s.allPass },
3621
+ el('summary', { className: 'sap-customizedSummary' }, title),
3622
+ el('div', { className: 'sap-customizedBody' }, badges, reject)
3623
+ )
3624
+ }
3625
+
3626
+ // 证据层 ② 决策 · 生效值:chips(中文对照)+ 决策原文 + 请求vs生效表 + 工具集 + 安全检查。
3627
+ function buildDecisionBlock(el, chip, record, profileNames, toolZh, jsonView, toggleJson, copiedTarget, onCopyTarget) {
3628
+ const trace = record.trace
3629
+ const jsonMode = jsonView.decide === true
3630
+ const kv = [
3631
+ { label: ZH.ledgerEffectiveStep, value: trace.effective },
3632
+ { label: 'gates', value: trace.gates }
3633
+ ]
3634
+ return el('div', { key: 'block-decide', className: 'sap-ledgerStep' },
3635
+ el('div', { className: 'sap-ledgerStepHead' },
3636
+ el('span', null, ZH.cogDecide),
3637
+ buildViewToggle(el, jsonMode, () => toggleJson('decide'))
3638
+ ),
3639
+ jsonMode
3640
+ ? buildJsonBody(el, kv)
3641
+ : el('div', { className: 'sap-ledgerStepBody' },
3642
+ buildEffectiveChips(el, chip, record, profileNames),
3643
+ trace.parentText && trace.parentText.after
3644
+ ? el('div', { className: 'sap-cogParentText' }, ZH.ledgerDecisionQuote + ':『' + trace.parentText.after + '』')
3645
+ : null,
3646
+ buildRequestVsTable(el, record),
3647
+ buildToolsetSubSection(el, record, toolZh),
3648
+ buildPrecheckSubSection(el, record, copiedTarget, onCopyTarget)
3649
+ )
1948
3650
  )
1949
3651
  }
3652
+ // 模型选择审计标注 chips:警示色 chip,title 给出确定性判定依据。
3653
+ function buildAuditChips(el, trace) {
3654
+ return auditModelChoice(trace).map((a) => el('span', {
3655
+ key: 'audit-' + a.key,
3656
+ className: 'sap-chip sap-chipWarn',
3657
+ title: a.title
3658
+ }, a.label))
3659
+ }
1950
3660
 
1951
- // 结算:stopReason / 耗时 / token + 五段分解(复用 usageBreakdownText)。
1952
- // trace.settled 有值时渲染;settled null(运行中/拒绝)整节省略。
1953
- function buildSettledSection(el, record) {
3661
+
3662
+ // 证据层 结算:结束原因(中文)· 耗时 · tokens · 输出摘要 · 成本分解。
3663
+ function buildSettleBlock(el, record, jsonView, toggleJson, openChild) {
1954
3664
  const settled = record.trace && record.trace.settled
1955
3665
  if (!settled) return null
1956
- const { tokens, elapsedMs } = recordNumbers(record)
3666
+ const jsonMode = jsonView.settle === true
3667
+ const kv = [{ label: ZH.ledgerSettledStep, value: settled }]
3668
+ const numbers = recordNumbers(record)
1957
3669
  const stopZh = recordStopZh(record)
1958
- const usageText = usageBreakdownText(recordUsage(record))
1959
3670
  const parts = []
1960
- if (stopZh !== '') parts.push(`${ZH.ledgerStopReason}=${stopZh}`)
1961
- if (elapsedMs !== null) parts.push(`${ZH.ledgerElapsed}=${formatDurationMs(elapsedMs)}`)
1962
- if (tokens !== null) parts.push(`≈${tokens} tokens`)
1963
- if (usageText !== '') parts.push(usageText)
1964
- if (parts.length === 0) return null
1965
- return el('div', { className: 'sap-ledgerSection' },
1966
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerSettledStep),
1967
- el('div', { className: 'sap-ledgerUsage' }, parts.join(' · '))
3671
+ if (stopZh !== '') parts.push(ZH.ledgerStopReason + ':' + stopZh)
3672
+ if (numbers.elapsedMs !== null) parts.push(ZH.ledgerElapsed + ' ' + formatDurationMs(numbers.elapsedMs))
3673
+ if (numbers.tokens !== null) parts.push('≈' + numbers.tokens + ' tokens')
3674
+ const usageText = usageBreakdownText(recordUsage(record))
3675
+ if (usageText !== '') parts.push(ZH.ledgerSettleCostBreakdown + ':' + usageText)
3676
+ const output = record.contentFirstLine
3677
+ const outputText = output !== '' ? ZH.ledgerSettleOutput + '' + output : ''
3678
+ const execution = (record.trace && record.trace.execution) || {}
3679
+ const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
3680
+ const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
3681
+ const mode = typeof execution.mode === 'string' ? execution.mode : ''
3682
+ const jump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
3683
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: () => openChild({ parentSessionId, childSessionId, mode }) }, ZH.ledgerJumpChild + ' ▸')
3684
+ : null
3685
+ return el('div', { key: 'block-settle', className: 'sap-ledgerStep' },
3686
+ el('div', { className: 'sap-ledgerStepHead' },
3687
+ el('span', null, ZH.cogNewPerceive),
3688
+ buildViewToggle(el, jsonMode, () => toggleJson('settle'))
3689
+ ),
3690
+ jsonMode
3691
+ ? buildJsonBody(el, kv)
3692
+ : el('div', { className: 'sap-ledgerStepBody' },
3693
+ parts.length > 0 ? el('div', { className: 'sap-ledgerUsage' }, parts.join(' · ')) : null,
3694
+ outputText !== '' ? el('div', { className: 'sap-ledgerUsage' }, outputText) : null,
3695
+ jump
3696
+ )
1968
3697
  )
1969
3698
  }
1970
-
1971
3699
  // 逐次调用明细折叠区(默认收起):trace.settled.calls 逐条五段紧凑表格(序号 + 五列,
1972
3700
  // 缺失段显示 —)+ 末行合计(∑已存 calls,未截断时与 childUsage 五段分解对账一致)+
1973
3701
  // 截断提示。无 calls 数据时整区不渲染。
@@ -2016,183 +3744,314 @@ function buildCallDetailSection(el, record) {
2016
3744
  )
2017
3745
  }
2018
3746
 
2019
- // 子会话跳转:地址三字段齐全 →「打开子会话」;不完整 → 显示 id 供手动检索。
2020
- // 后台派发只有 jobId(execute 返回时子会话尚未启动),无子会话可跳,恒走手动检索。
2021
- function buildHierarchyHint(el, record, openChild) {
2022
- const execution = (record.trace && record.trace.execution) || {}
3747
+
3748
+ // 证据层 执行:时间区间 + 工具调用次数 + 过程事件线(停滞/干预/恢复)+ 跳转 + 逐次调用明细。
3749
+ function buildExecutionBlock(el, record, proc, toolZh, jsonView, toggleJson, openChild) {
3750
+ const trace = record.trace
3751
+ const execution = trace.execution || {}
3752
+ if (recordIsRejected(record) && Object.keys(execution).length === 0) return null
3753
+ const jsonMode = jsonView.execute === true
3754
+ const settled = trace.settled
3755
+ const numbers = recordNumbers(record)
3756
+ const startedText = formatClockTime(trace.startedAt)
3757
+ const endTime = settled && typeof settled.elapsedMs === 'number' && trace.startedAt !== null ? trace.startedAt + settled.elapsedMs : null
3758
+ const endText = endTime !== null ? formatClockTime(endTime) : ''
3759
+ const durText = formatDurationMs(numbers.elapsedMs)
3760
+ const interval = startedText !== '' && endText !== ''
3761
+ ? ZH.ledgerExecInterval.replace('{start}', startedText).replace('{end}', endText).replace('{dur}', durText)
3762
+ : (durText !== '' ? ZH.ledgerElapsed + ' ' + durText : '')
3763
+ const callsText = proc !== null && proc.toolCalls > 0 ? ZH.ledgerExecToolCalls.replace('{n}', String(proc.toolCalls)) : ''
2023
3764
  const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
2024
3765
  const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
2025
- const jobId = typeof execution.jobId === 'string' ? execution.jobId : ''
2026
3766
  const mode = typeof execution.mode === 'string' ? execution.mode : ''
2027
- const canJump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
2028
- let body = null
2029
- if (canJump) {
2030
- body = el('button', {
2031
- type: 'button',
2032
- className: 'sap-detailToggle',
2033
- onClick: () => openChild({ parentSessionId, childSessionId, mode })
2034
- }, ZH.ledgerJumpChild)
2035
- } else if (childSessionId !== '') {
2036
- body = el('div', { className: 'sap-ledgerManual' }, `${ZH.ledgerChildManual}:`, el('code', { className: 'sap-ledgerManualId' }, childSessionId))
2037
- } else if (jobId !== '') {
2038
- body = el('div', { className: 'sap-ledgerManual' }, `${ZH.ledgerJobManual}:`, el('code', { className: 'sap-ledgerManualId' }, jobId))
2039
- }
2040
- if (body === null) return null
2041
- return el('div', { className: 'sap-ledgerSection' },
2042
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerExecutionStep),
2043
- body
3767
+ const jump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
3768
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: () => openChild({ parentSessionId, childSessionId, mode }) }, ZH.ledgerJumpChild + ' ▸')
3769
+ : (childSessionId !== ''
3770
+ ? el('div', { className: 'sap-ledgerManual' }, ZH.ledgerChildManual + ':', el('code', { className: 'sap-ledgerManualId' }, childSessionId))
3771
+ : null)
3772
+ const kv = [
3773
+ { label: ZH.ledgerExecutionStep, value: execution },
3774
+ { label: 'calls', value: settled && settled.calls ? settled.calls : null }
3775
+ ]
3776
+ return el('div', { key: 'block-execute', className: 'sap-ledgerStep' },
3777
+ el('div', { className: 'sap-ledgerStepHead' },
3778
+ el('span', null, ZH.cogExecute),
3779
+ buildViewToggle(el, jsonMode, () => toggleJson('execute'))
3780
+ ),
3781
+ jsonMode
3782
+ ? buildJsonBody(el, kv)
3783
+ : el('div', { className: 'sap-ledgerStepBody' },
3784
+ interval !== '' ? el('div', { className: 'sap-ledgerUsage' }, interval) : null,
3785
+ callsText !== '' ? el('div', { className: 'sap-ledgerUsage' }, callsText) : null,
3786
+ !settled ? el('div', { className: 'sap-ledgerUsage' }, ZH.ledgerExecLiveNote) : null,
3787
+ proc !== null ? buildProcessTimeline(el, proc, toolZh) : null,
3788
+ jump,
3789
+ buildCallDetailSection(el, record)
3790
+ )
2044
3791
  )
2045
3792
  }
2046
3793
 
2047
- // 记录摘要行 chips:后台/continuable 派发方式 + 方案 + 档位。
2048
- function buildRowChips(el, chip, record, profileNames) {
2049
- const chips = []
2050
- const kind = recordKind(record)
2051
- if (kind === 'background' || kind === 'continuable') {
2052
- chips.push(el('span', { key: 'kind', className: 'sap-chip' }, kindLabel(kind)))
3794
+ // 过程事件线单条:时间 + 文案;停滞琥珀点、干预蓝点、里程碑绿点。
3795
+ function buildProcessEvent(el, event, toolZh) {
3796
+ const time = event.time !== null && event.time !== undefined ? formatClockTime(event.time) : ''
3797
+ const cls = event.kind === 'stall' ? 'sap-ledgerEvent sap-ledgerEvent-warn'
3798
+ : (event.kind === 'intervene' ? 'sap-ledgerEvent sap-ledgerEvent-int' : 'sap-ledgerEvent sap-ledgerEvent-done')
3799
+ const bodyOf = () => {
3800
+ if (event.kind === 'start') return ZH.ledgerEventStart
3801
+ if (event.kind === 'call') return ZH.ledgerEventCall.replace('{n}', String(event.index)) + '(' + (event.tool !== '' ? toolNameZh(event.tool, toolZh) : '?') + ')'
3802
+ if (event.kind === 'stall') return ZH.ledgerEventStall.replace('{s}', String(event.seconds)).replace('{t}', String(STALL_THRESHOLD_MS / 1000))
3803
+ if (event.kind === 'intervene') return ZH.ledgerEventIntervene + ':' + event.text
3804
+ if (event.kind === 'resume') return ZH.ledgerEventResume
3805
+ if (event.kind === 'done') return ZH.ledgerEventDone
3806
+ return ZH.ledgerEventRunning
3807
+ }
3808
+ const body = bodyOf()
3809
+ return el('div', { key: event.kind + '-' + (event.time !== null ? String(event.time) : 'end'), className: cls },
3810
+ time !== '' ? el('span', { className: 'sap-ledgerEventTime' }, time) : null,
3811
+ event.kind === 'intervene' ? el('span', { className: 'sap-ledgerEventAct' }, body) : body
3812
+ )
3813
+ }
3814
+ // 过程事件线(工具调用 >4 次时节选:前 2 + 省略 + 最后 1;停滞/干预/恢复不节选)。
3815
+ function buildProcessTimeline(el, proc, toolZh) {
3816
+ if (proc.events.length === 0) return null
3817
+ const calls = proc.events.filter((e) => e.kind === 'call')
3818
+ const showAll = calls.length <= 4
3819
+ const items = []
3820
+ let skipInserted = false
3821
+ for (const event of proc.events) {
3822
+ if (event.kind === 'call' && !showAll) {
3823
+ const callIdx = calls.indexOf(event)
3824
+ if (callIdx >= 2 && callIdx < calls.length - 1) {
3825
+ if (!skipInserted) { items.push(el('div', { key: 'call-more', className: 'sap-ledgerEvent sap-ledgerEvent-done' }, '…')); skipInserted = true }
3826
+ continue
3827
+ }
3828
+ }
3829
+ items.push(buildProcessEvent(el, event, toolZh))
2053
3830
  }
2054
- const profile = recordProfile(record)
2055
- if (profile !== '') chips.push(chip('profile', ZH.chipProfile, profileDisplay(profile, profileNames)))
2056
- const tier = recordTier(record)
2057
- if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
2058
- return chips
3831
+ return el('div', null,
3832
+ el('div', { className: 'sap-ledgerVsHead' }, ZH.ledgerStallThresholdNote),
3833
+ el('div', { className: 'sap-ledgerEventLine' }, items)
3834
+ )
2059
3835
  }
2060
3836
 
2061
- // 展开区:旧记录降级提示;新记录 = 决策时间线生效值执行信息
2062
- // 结算 调用级明细。
2063
- function buildExpandArea(el, chip, record, isLegacy, openChild, profileNames, jsonView, toggleJson) {
2064
- if (isLegacy) {
2065
- return el('div', { className: 'sap-ledgerExpand' },
2066
- el('div', { className: 'sap-ledgerLegacy' }, ZH.ledgerLegacy)
2067
- )
2068
- }
3837
+ // 证据层装配:① 发起 ② 决策 执行 ④ 结算(拒绝场景 ③④ 自动省略)。
3838
+ function buildEvidenceArea(el, chip, record, profileNames, toolZh, proc, jsonView, toggleJson, openChild, copiedTarget, onCopyTarget) {
2069
3839
  return el('div', { className: 'sap-ledgerExpand' },
2070
- buildDecisionTimeline(el, record, jsonView, toggleJson),
2071
- buildEffectiveSection(el, chip, record, profileNames),
2072
- buildHierarchyHint(el, record, openChild),
2073
- buildSettledSection(el, record),
2074
- buildCallDetailSection(el, record)
3840
+ buildStartBlock(el, record, jsonView, toggleJson),
3841
+ buildDecisionBlock(el, chip, record, profileNames, toolZh, jsonView, toggleJson, copiedTarget, onCopyTarget),
3842
+ buildExecutionBlock(el, record, proc, toolZh, jsonView, toggleJson, openChild),
3843
+ buildSettleBlock(el, record, jsonView, toggleJson, openChild)
2075
3844
  )
2076
3845
  }
2077
3846
 
2078
- // ── 账本 maker(React 依赖经参数注入)───────────────────────────────────────
3847
+ // 折叠态行:时间 · 方案中文名 · 状态chip · 成本 · 工具数 · [⚠干预 N] · ▸。
3848
+ function buildLedgerRowHead(el, record, profileNames, proc, open, onToggle) {
3849
+ const status = ledgerStatusOf(record)
3850
+ const rejected = recordIsRejected(record)
3851
+ const settled = record.trace && record.trace.settled
3852
+ const costInfo = recordCostInfo(record)
3853
+ const costText = rejected ? '—' : (!settled ? ZH.ledgerCostRunning : (costInfo !== null ? '≈' + costInfo.actual + ' 元' : ''))
3854
+ const profileId = recordProfile(record)
3855
+ const profileText = profileId !== '' ? profileDisplay(profileId, profileNames) : ZH.inlineProfile
3856
+ const toolsText = ledgerToolsCountOf(record, proc)
3857
+ const interveneText = proc !== null && proc.interventions > 0 ? ZH.ledgerInterventionBadge.replace('{n}', String(proc.interventions)) : ''
3858
+ return el('button', {
3859
+ type: 'button',
3860
+ className: 'sap-ledgerRowHead',
3861
+ 'aria-expanded': open,
3862
+ onClick: onToggle
3863
+ },
3864
+ el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
3865
+ el('span', { className: 'sap-ledgerMeta' }, profileText),
3866
+ el('span', { className: 'sap-ledgerStatus ' + status.cls }, status.label),
3867
+ costText !== '' ? el('span', { className: 'sap-ledgerCostText' }, costText) : null,
3868
+ toolsText !== '' ? el('span', { className: 'sap-ledgerToolsText' }, toolsText) : null,
3869
+ interveneText !== ''
3870
+ ? el('span', { className: 'sap-ledgerBadge sap-ledgerBadgeWarn' }, interveneText)
3871
+ : null,
3872
+ el('span', { className: 'sap-ledgerToggle' }, '▸')
3873
+ )
3874
+ }
3875
+ // 剪贴板复制(fail-soft:无 clipboard API 时仍置反馈状态,目标文本可见可手动复制)。
3876
+ function copyToClipboard(text) {
3877
+ try {
3878
+ if (window.navigator && window.navigator.clipboard && typeof window.navigator.clipboard.writeText === 'function') {
3879
+ window.navigator.clipboard.writeText(text)
3880
+ }
3881
+ } catch { /* 复制失败仍显示目标文本供手动复制 */ }
3882
+ }
3883
+
3884
+ function makeCopyTarget(setCopiedTarget) {
3885
+ return (target) => () => {
3886
+ copyToClipboard(target)
3887
+ setCopiedTarget(target)
3888
+ }
3889
+ }
3890
+
2079
3891
 
3892
+ // ── 台账 maker(React 依赖经参数注入)───────────────────────────────────────
3893
+
3894
+ // 单条记录三层:折叠态行 → 结论层(点行展开)→ 证据层(「查看完整因果链」再展开)。
2080
3895
  function makeLedgerRow(el, chip, useState, openChild) {
2081
- function LedgerRow({ record, profileNames }) {
3896
+ function LedgerRow({ record, profileNames, childNodesOf, toolZh }) {
2082
3897
  const [open, setOpen] = useState(false)
2083
- // 区块级视图切换:jsonView[key] 为 true 时该区块显示原始 JSON(key='start'
2084
- // `gate-<index>`),false/缺省显示字段级子块视图。各区块独立切换。
3898
+ const [evidenceOpen, setEvidenceOpen] = useState(false)
3899
+ // 修复方向复制反馈:点击「复制 <目标>」后按钮短暂显示「已复制」。
3900
+ const [copiedTarget, setCopiedTarget] = useState('')
3901
+ // 区块级视图切换:jsonView[key] 为 true 时该区块显示原始 JSON
3902
+ // (key='start'/'decide'/'execute'/'settle')。各区块独立切换。
2085
3903
  const [jsonView, setJsonView] = useState({})
2086
3904
  const toggleJson = (key) => setJsonView((prev) => ({ ...prev, [key]: !(prev[key] === true) }))
2087
3905
  const isLegacy = record.legacy === true
2088
- const numbers = recordNumbers(record)
2089
3906
  const rejected = recordIsRejected(record)
2090
- const stopZh = rejected ? '' : recordStopZh(record)
2091
- const metaParts = []
2092
- if (numbers.tokens !== null) metaParts.push(`≈${numbers.tokens} tokens`)
2093
- if (numbers.elapsedMs !== null) metaParts.push(`${ZH.ledgerElapsed}=${numbers.elapsedMs}ms`)
2094
- if (stopZh !== '') metaParts.push(`${ZH.ledgerStopReason}=${stopZh}`)
2095
- return el('li', { className: open ? 'sap-ledgerRow sap-ledgerRowOpen' : 'sap-ledgerRow' },
2096
- el('button', {
2097
- type: 'button',
2098
- className: 'sap-ledgerRowHead',
2099
- 'aria-expanded': open,
2100
- onClick: () => setOpen((prev) => !prev)
2101
- },
2102
- el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
2103
- el('span', { className: 'sap-chips' }, buildRowChips(el, chip, record, profileNames)),
2104
- rejected ? el('span', { className: 'sap-ledgerReject' }, `❌ ${ZH.ledgerReject}`) : null,
2105
- el('span', { className: 'sap-ledgerMeta' }, metaParts.join(' · ')),
2106
- el('span', { className: 'sap-ledgerToggle' }, '▸')
2107
- ),
2108
- open ? buildExpandArea(el, chip, record, isLegacy, openChild, profileNames, jsonView, toggleJson) : null
3907
+ const execution = record.trace && record.trace.execution ? record.trace.execution : {}
3908
+ const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
3909
+ const proc = childSessionId !== '' && typeof childNodesOf === 'function'
3910
+ ? processEventsOf(childNodesOf(childSessionId), record.trace && record.trace.settled)
3911
+ : processEventsOf(null, record.trace && record.trace.settled)
3912
+ const copyTarget = makeCopyTarget(setCopiedTarget)
3913
+ return el('li', {
3914
+ className: 'sap-ledgerRow' + (open ? ' sap-ledgerRowOpen' : '') + (rejected ? ' sap-ledgerRowFail' : '')
3915
+ },
3916
+ buildLedgerRowHead(el, record, profileNames, proc, open, () => setOpen((prev) => !prev)),
3917
+ open && isLegacy
3918
+ ? el('div', { className: 'sap-ledgerExpand' }, el('div', { className: 'sap-ledgerLegacy' }, ZH.ledgerLegacy))
3919
+ : null,
3920
+ open && !isLegacy
3921
+ ? buildConclusionArea(el, record, proc, copiedTarget, copyTarget, evidenceOpen, setEvidenceOpen)
3922
+ : null,
3923
+ open && !isLegacy && evidenceOpen
3924
+ ? buildEvidenceArea(el, chip, record, profileNames, toolZh, proc, jsonView, toggleJson, openChild, copiedTarget, copyTarget)
3925
+ : null
2109
3926
  )
2110
3927
  }
2111
3928
  return LedgerRow
2112
3929
  }
2113
3930
 
2114
- function makeDispatchLedgerView(el, chip, useState, useEffect, openChild) {
3931
+
3932
+ // 台账后台任务状态 hook:挂载即拉一次;有待配对后台记录时 3 秒轮询至全部结算。
3933
+ function useLedgerJobs(useState, useEffect, sessionId, pendingJobIds) {
3934
+ const [jobs, setJobs] = useState([])
3935
+ const pendingKey = Array.isArray(pendingJobIds) ? pendingJobIds.join(',') : ''
3936
+ useEffect(() => startLedgerJobsPoll(sessionId, pendingJobIds, setJobs), [sessionId, pendingKey])
3937
+ return jobs
3938
+ }
3939
+
3940
+ // 台账视图数据装载 hook:失败台账 / 后台任务 / 方案名映射 / 工具中文名(name→zh)。
3941
+ function useLedgerViewData(useState, useEffect, sessionId, pendingJobIds) {
3942
+ const [failures, setFailures] = useState([])
3943
+ const jobs = useLedgerJobs(useState, useEffect, sessionId, pendingJobIds)
3944
+ const [profileNames, setProfileNames] = useState({})
3945
+ const [toolZh, setToolZh] = useState({})
3946
+ useEffect(() => {
3947
+ let cancelled = false
3948
+ fetchLedgerFailures(sessionId).then((list) => {
3949
+ if (!cancelled) setFailures(list)
3950
+ })
3951
+ api('/options/tools').then((data) => {
3952
+ if (cancelled) return
3953
+ const map = {}
3954
+ for (const tool of (Array.isArray(data.tools) ? data.tools : [])) {
3955
+ if (tool !== null && typeof tool === 'object' && typeof tool.name === 'string' && typeof tool.zh === 'string' && tool.zh !== '') {
3956
+ map[tool.name] = tool.zh
3957
+ }
3958
+ }
3959
+ setToolZh(map)
3960
+ }).catch(() => {})
3961
+ return () => { cancelled = true }
3962
+ }, [sessionId])
3963
+ useEffect(() => {
3964
+ let cancelled = false
3965
+ api('/list').then((data) => {
3966
+ if (cancelled || data === undefined || !Array.isArray(data.profiles)) return
3967
+ const map = {}
3968
+ for (const profile of data.profiles) {
3969
+ if (profile !== null && typeof profile === 'object' && typeof profile.id === 'string' && typeof profile.name === 'string' && profile.name !== '') {
3970
+ map[profile.id] = profile.name
3971
+ }
3972
+ }
3973
+ setProfileNames(map)
3974
+ }).catch(() => {})
3975
+ return () => { cancelled = true }
3976
+ }, [])
3977
+ return { failures, jobs, profileNames, toolZh }
3978
+ }
3979
+
3980
+ // 记录行列表(含空态)。
3981
+ function buildLedgerRows(el, records, LedgerRow, profileNames, childNodesOf, toolZh) {
3982
+ if (records.length === 0) return buildEmptyState(el)
3983
+ const ordered = records.slice().reverse()
3984
+ return el('ul', { className: 'sap-ledgerRows' },
3985
+ ordered.map((record, index) => el(LedgerRow, { key: ledgerKey(record, index), record, profileNames, childNodesOf, toolZh }))
3986
+ )
3987
+ }
3988
+
3989
+ function makeDispatchLedgerView(el, chip, useState, useEffect, openChild, getSessions) {
2115
3990
  const LedgerRow = makeLedgerRow(el, chip, useState, openChild)
2116
3991
  function DispatchLedgerView({ useSession, sessionId, loadOlder = () => {} }) {
2117
3992
  const nodes = useSession((s) => s?.chat?.legacy?.nodes)
2118
3993
  const hasMore = useSession((s) => s?.hasMore)
2119
3994
  const loadingOlder = useSession((s) => s?.loadingOlder)
2120
- const [failures, setFailures] = useState([])
2121
- const [profileNames, setProfileNames] = useState({})
2122
- useEffect(() => {
2123
- let cancelled = false
2124
- fetchLedgerFailures(sessionId).then((list) => {
2125
- if (!cancelled) setFailures(list)
2126
- })
2127
- return () => { cancelled = true }
2128
- }, [sessionId])
2129
- // profile 注册表 id→中文名映射:账本挂载时拉一次 /list,用于方案名中英双表达
2130
- // (「标准编码(swap-standard)」);拉取失败保留空映射,显示层回退纯 id。
2131
- useEffect(() => {
2132
- let cancelled = false
2133
- api('/list').then((data) => {
2134
- if (cancelled || data === undefined || !Array.isArray(data.profiles)) return
2135
- const map = {}
2136
- for (const profile of data.profiles) {
2137
- if (profile !== null && typeof profile === 'object' && typeof profile.id === 'string' && typeof profile.name === 'string' && profile.name !== '') {
2138
- map[profile.id] = profile.name
2139
- }
2140
- }
2141
- setProfileNames(map)
2142
- }).catch(() => {})
2143
- return () => { cancelled = true }
2144
- }, [])
2145
- // 账本默认只统计已加载窗口;经「加载更早」逐页补载更早记录后 nodes 变多、
2146
- // 统计自然扩大。
2147
- const records = mergeFailureRecords(collectDispatchRecords(nodes), failures)
3995
+ const pendingJobs = pendingBackgroundJobIds(nodes)
3996
+ const data = useLedgerViewData(useState, useEffect, sessionId, pendingJobs)
3997
+ // 子会话快照读取(执行过程事件线数据源);拿不到返回 null(fail-soft 不虚报)。
3998
+ const childNodesOf = (childSessionId) => {
3999
+ try {
4000
+ const child = getSessions().binding(childSessionId)
4001
+ const childNodes = child !== undefined && child !== null && child.session !== undefined ? child.session.chat?.legacy?.nodes : undefined
4002
+ return Array.isArray(childNodes) ? childNodes : null
4003
+ } catch {
4004
+ return null
4005
+ }
4006
+ }
4007
+ // 台账默认只统计已加载窗口;经「加载更早」逐页补载更早记录后 nodes 变多、统计自然扩大。
4008
+ const records = mergeBackgroundJobs(mergeFailureRecords(collectDispatchRecords(nodes), data.failures), data.jobs)
2148
4009
  const summary = records.length === 0 ? null : computeLedgerSummary(records)
2149
- const ordered = records.slice().reverse()
2150
- const rows = records.length === 0
2151
- ? buildEmptyState(el)
2152
- : el('ul', { className: 'sap-ledgerRows' },
2153
- ordered.map((record, index) => el(LedgerRow, { key: ledgerKey(record, index), record, profileNames }))
2154
- )
2155
4010
  return el('div', { className: 'sap-ledger' },
2156
4011
  buildLedgerTopNote(el, hasMore, records.length),
2157
4012
  buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder),
2158
- summary === null ? null : buildLedgerSummary(el, summary, profileNames),
2159
- rows
4013
+ summary === null ? null : buildLedgerSummary(el, summary, data.profileNames),
4014
+ buildLedgerRows(el, records, LedgerRow, data.profileNames, childNodesOf, data.toolZh)
2160
4015
  )
2161
4016
  }
2162
4017
  return DispatchLedgerView
2163
4018
  }
2164
-
2165
4019
  // ── plugin 装配 ─────────────────────────────────────────────────────────────
2166
4020
 
2167
4021
  window.__ModuleLoader__.load({
2168
4022
  id: 'dsh-subagent-profile',
2169
4023
  factory: (require) => {
2170
- const React = require('react')
2171
- const { useEffect, useState, useCallback } = React
4024
+ const React = require('react'); const { useEffect, useState, useCallback } = React
2172
4025
  const el = React.createElement
2173
4026
 
2174
4027
  // slots:槽注册;sessions:子会话跳转(openSubagent)。两者皆 client 核心服务。
2175
4028
  const inject = ['slots', 'sessions']
2176
4029
 
2177
4030
  const chip = makeChip(el)
2178
- const DispatchToolview = makeDispatchToolview(el, chip, useState)
2179
- const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
4031
+ const DispatchToolview = makeDispatchToolview(el, chip, useState, useEffect)
4032
+ const ReminderOverlayBadge = makeReminderOverlayBadge(el, useState, useEffect)
2180
4033
 
2181
4034
  function apply(ctx) {
4035
+ // 提醒中心需要会话跳转(主会话 open / 子会话 openSubagent)与标题查询,
4036
+ // 与派发决策台账 openChild 同一 sessions 服务来源。
4037
+ const openChild = (address) => ctx.sessions.openSubagent(address)
4038
+ const ReminderCenter = makeReminderCenter(el, useState, useEffect, openChild, () => ctx.sessions)
4039
+ const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter)
2182
4040
  ensureStyles()
2183
- ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
2184
- name: 'tool.call.toolview',
2185
- key: 'dispatch'
2186
- }, DispatchToolview))
4041
+ const SubagentHeaderUtility = makeSubagentHeaderUtility(el, useState, useEffect, () => ctx.get('modelDirectories'), () => ctx.sessions)
4042
+ ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: 'dispatch' }, DispatchToolview))
2187
4043
  ctx.slots.inject('settings.section', () => ctx.slots.register({
2188
4044
  name: 'settings.section',
2189
4045
  id: 'subagent-profiles',
2190
4046
  order: 100,
2191
4047
  label: '子 Agent 方案'
2192
4048
  }, ProfilesSection))
4049
+ // 子会话页头常驻 banner(仅子会话渲染;同 chip 族;宿主未投影前不渲染)。
4050
+ ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-subagent-profile-subagent', order: 20 }, SubagentHeaderUtility))
4051
+ // P0 红角标(severity 红 + 常驻至已读 + 带 session 标识)。
4052
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-subagent-profile-reminders', order: 800 }, ReminderOverlayBadge))
2193
4053
  // 子会话跳转回调:经 sessions 服务按 SubagentAddress 打开子会话。
2194
- const openChild = (address) => ctx.sessions.openSubagent(address)
2195
- const DispatchLedgerView = makeDispatchLedgerView(el, chip, useState, useEffect, openChild)
4054
+ const DispatchLedgerView = makeDispatchLedgerView(el, chip, useState, useEffect, openChild, () => ctx.sessions)
2196
4055
  ctx.slots.inject('conversation.view', () => ctx.slots.register({
2197
4056
  name: 'conversation.view',
2198
4057
  id: 'dispatch-ledger',