dsh-subagent-profile 0.3.2 → 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.
Files changed (40) hide show
  1. package/README.md +77 -40
  2. package/README.zh.md +111 -74
  3. package/docs/screenshots/dispatch-card.png +0 -0
  4. package/docs/screenshots/settings-page1.png +0 -0
  5. package/docs/screenshots/settings-page2.png +0 -0
  6. package/index.mjs +276 -81
  7. package/lib/client.js +3218 -166
  8. package/lib/core/adoption-reminder.mjs +48 -0
  9. package/lib/core/adoption-tracker.mjs +430 -0
  10. package/lib/core/background-ledger.mjs +71 -0
  11. package/lib/core/catalog-cache.mjs +45 -7
  12. package/lib/core/catalog.mjs +6 -6
  13. package/lib/core/cost-evidence.mjs +145 -0
  14. package/lib/core/cost-guard.mjs +71 -44
  15. package/lib/core/decision-trace.mjs +413 -0
  16. package/lib/core/delegation.mjs +111 -50
  17. package/lib/core/dispatch-gates.mjs +153 -0
  18. package/lib/core/dispatch-guard.mjs +156 -0
  19. package/lib/core/dispatch-schema.mjs +103 -14
  20. package/lib/core/dispatch-tool.mjs +220 -204
  21. package/lib/core/draft-gates.mjs +45 -0
  22. package/lib/core/drafts-store.mjs +45 -0
  23. package/lib/core/escape.mjs +130 -0
  24. package/lib/core/evolution-advice.mjs +224 -0
  25. package/lib/core/evolution-ledger.mjs +300 -0
  26. package/lib/core/evolution-summary.mjs +255 -0
  27. package/lib/core/http-routes.mjs +256 -72
  28. package/lib/core/intersection.mjs +6 -9
  29. package/lib/core/presets-sync.mjs +161 -43
  30. package/lib/core/prices.mjs +46 -0
  31. package/lib/core/profile-directory.mjs +139 -0
  32. package/lib/core/profile-provider.mjs +42 -39
  33. package/lib/core/profiles-store.mjs +103 -76
  34. package/lib/core/pure.mjs +110 -66
  35. package/lib/core/reminder-store.mjs +172 -0
  36. package/lib/core/shims.mjs +67 -76
  37. package/lib/core/whitelist.mjs +23 -17
  38. package/package.json +82 -83
  39. package/presets/orchestrator/agent.cordis.yml +59 -87
  40. package/presets/orchestrator/NOTICE +0 -3
package/lib/client.js CHANGED
@@ -1,23 +1,18 @@
1
1
  /**
2
- * dsh-subagent-profile — browser side: a read-only `dispatch` tool-call card
3
- * plus a settings page that manages the profile registry.
2
+ * dsh-subagent-profile — 浏览器侧:只读 `dispatch` 工具调用卡片 +
3
+ * 管理方案注册表的设置页。
4
4
  *
5
- * Hand-written `__ModuleLoader__` factory (no build step), modeled on
6
- * @dsh-external/dsh-persona-ref: React comes from the shared module host, every
7
- * data operation goes through the loopback /subagent-profiles/* HTTP routes
8
- * served by the host side (index.mjs), and slots register through the shared
9
- * `slots` service. The card lives on the keyed `tool.call.toolview` seat (key
10
- * `dispatch`); the settings page lives on the `settings.section` list.
5
+ * 手写 `__ModuleLoader__` 工厂(无构建步骤),参照 @dsh-external/dsh-persona-ref:
6
+ * React 来自共享模块宿主,所有数据操作走宿主侧(index.mjs)提供的 loopback
7
+ * `/subagent-profiles/*` HTTP 路由,slot 经共享 `slots` 服务注册。卡片挂在
8
+ * keyed `tool.call.toolview` 座(key `dispatch`);设置页挂在 `settings.section` 列表。
11
9
  *
12
- * The settings page visual language follows the shipped "模型" settings section
13
- * (dsh-client-ui-settings-models): same design tokens, a title + intro, one
14
- * bordered card per row with a layered name / description / meta layout, and a
15
- * dashed "+ 新增" button that expands an inline form card instead of a
16
- * permanently-visible input row. Dropdown options (preset / provider / model /
17
- * reasoning effort) are served by the host `/options/summary` (models + presets,
18
- * on settings open), `/options/efforts` (per-model, lazy on model select) and
19
- * `/options/tools` (tool directory, lazy when the restriction panel opens)
20
- * routes from the live presets roster and llm directory.
10
+ * 设置页视觉语言对齐官方「模型」设置节(dsh-client-ui-settings-models):同一套
11
+ * 设计 token,标题 + 引言,每行一张带边框卡片(名称/描述/元信息分层布局),
12
+ * 虚线「+ 新增」按钮展开就地表单卡片。下拉选项(preset / provider / model /
13
+ * reasoning effort)由宿主 `/options/summary`(模型 + 预设,设置页打开时)、
14
+ * `/options/efforts`(按模型,选择模型后懒加载)与 `/options/tools`(工具目录,
15
+ * 限制面板打开时懒加载)路由提供,数据来自实时 presets roster llm 目录。
21
16
  *
22
17
  * 结构:纯数据(CSS/ZH 文案表)与纯函数(api/解析/文案映射等)驻留模块作用域
23
18
  * (load 调用之前的顶层声明);依赖 React 的组件经参数注入的 maker 函数装配
@@ -28,6 +23,59 @@
28
23
 
29
24
  const CSS = [
30
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}',
31
79
  '.sap-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}',
32
80
  '.sap-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}',
33
81
  '.sap-notice{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}',
@@ -48,6 +96,7 @@ const CSS = [
48
96
  '.sap-chipLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
49
97
  '.sap-chipValue{color:var(--dsw-alias-label-primary)}',
50
98
  '.sap-chipValueDim{color:var(--dsw-alias-label-tertiary)}',
99
+ '.sap-chipDot{color:var(--dsw-alias-label-tertiary)}',
51
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}',
52
101
  '.sap-chipSuccess{color:var(--dsw-alias-state-success-primary);font-weight:500}',
53
102
  '.sap-chipDisabled{color:var(--dsw-alias-state-warn-label);font-weight:500}',
@@ -88,6 +137,15 @@ const CSS = [
88
137
  '.sap-switch input:checked{background:var(--dsw-alias-brand-primary)}',
89
138
  '.sap-switch input:checked::before{left:18px}',
90
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)}',
91
149
  '.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
92
150
  '.sap-toolSummary{align-items:center;gap:6px;flex-wrap:wrap;display:flex}',
93
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}',
@@ -103,6 +161,129 @@ const CSS = [
103
161
  '.sap-toolList{flex-wrap:wrap;gap:2px 16px;display:flex}',
104
162
  '.sap-toolItem{align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;line-height:20px;user-select:none;display:inline-flex}',
105
163
  '.sap-toolItem input{margin:0;accent-color:var(--dsw-alias-brand-primary)}',
164
+ '.sap-ledger{flex-direction:column;gap:12px;display:flex}',
165
+ '.sap-ledgerSummary{position:sticky;top:0;z-index:1;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;flex-direction:column;gap:6px;padding:12px 14px;display:flex}',
166
+ '.sap-ledgerSummaryTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}',
167
+ '.sap-ledgerSummaryLine{flex-wrap:wrap;gap:2px 12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;display:flex}',
168
+ '.sap-ledgerStatSeg{color:var(--dsw-alias-label-secondary)}',
169
+ '.sap-ledgerStat{color:var(--dsw-alias-label-primary);font-weight:600}',
170
+ '.sap-ledgerSep{color:var(--dsw-alias-label-tertiary)}',
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)}',
224
+ '.sap-ledgerRow{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;display:flex}',
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}',
226
+ '.sap-ledgerRowHead:hover{background:var(--dsw-alias-interactive-bg-hover)}',
227
+ '.sap-ledgerTime{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace;flex:none}',
228
+ '.sap-ledgerMeta{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;min-width:0;word-break:break-word}',
229
+ '.sap-ledgerToggle{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;transition:transform .12s;margin-left:auto;flex:none;display:inline-flex}',
230
+ '.sap-ledgerRowOpen .sap-ledgerToggle{transform:rotate(90deg)}',
231
+ '.sap-ledgerExpand{flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 14px 12px;display:flex}',
232
+ '.sap-ledgerSection{flex-direction:column;gap:6px;display:flex}',
233
+ '.sap-ledgerSectionTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}',
234
+ '.sap-ledgerTimeline{flex-direction:column;gap:8px;display:flex}',
235
+ '.sap-ledgerStep{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}',
236
+ '.sap-ledgerStepHead{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}',
237
+ '.sap-ledgerStepBody{flex-direction:column;gap:4px;display:flex}',
238
+ '.sap-ledgerGate{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}',
239
+ '.sap-ledgerGateFail{background:var(--dsw-alias-interactive-bg-hover-danger);border-color:var(--dsw-alias-state-error-secondary)}',
240
+ '.sap-ledgerGateHead{justify-content:space-between;align-items:center;gap:8px;display:flex}',
241
+ '.sap-ledgerGateName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}',
242
+ '.sap-ledgerGateVerdict{font-size:12px;font-weight:500;line-height:18px;flex:none}',
243
+ '.sap-ledgerGateVerdictPass{color:var(--dsw-alias-state-success-primary)}',
244
+ '.sap-ledgerGateVerdictFail{color:var(--dsw-alias-state-error-primary)}',
245
+ '.sap-ledgerGateBody{flex-direction:column;gap:4px;display:flex}',
246
+ '.sap-ledgerSubBlock{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}',
247
+ '.sap-ledgerSubBlockTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;align-items:center;gap:6px;display:flex}',
248
+ '.sap-ledgerSubBlockBody{flex-direction:column;gap:4px;display:flex}',
249
+ '.sap-ledgerGateNested{flex-direction:column;gap:2px;border-left:2px solid var(--dsw-alias-border-l2);padding-left:10px;margin-left:2px;display:flex}',
250
+ '.sap-ledgerGateKV{flex-wrap:wrap;align-items:baseline;gap:2px 8px;display:flex}',
251
+ '.sap-ledgerGateKey{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;flex:none}',
252
+ '.sap-ledgerGateValue{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-family:monospace;word-break:break-all}',
253
+ '.sap-ledgerReject{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}',
254
+ '.sap-ledgerRejectFix{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}',
255
+ '.sap-ledgerUsage{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
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}',
264
+ '.sap-ledgerManualId{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-family:monospace;word-break:break-all}',
265
+ '.sap-ledgerEmpty{flex-direction:column;gap:4px;padding:8px 2px;display:flex}',
266
+ '.sap-ledgerEmptyTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}',
267
+ '.sap-ledgerLoadOlder{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:12px;line-height:18px;height:28px;padding:0 12px;cursor:pointer;align-self:flex-start;align-items:center;display:inline-flex}',
268
+ '.sap-ledgerLoadOlder:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
269
+ '.sap-ledgerLoadOlder:disabled{opacity:.4;cursor:default}',
270
+ '.sap-ledgerLegacy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
271
+ '.sap-ledgerCallsTable{border-collapse:collapse;width:100%;font-size:11px;line-height:16px}',
272
+ '.sap-ledgerCallsTable th{color:var(--dsw-alias-label-secondary);font-weight:500;text-align:right;padding:2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
273
+ '.sap-ledgerCallsTable td{color:var(--dsw-alias-label-primary);font-family:monospace;text-align:right;padding:2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
274
+ '.sap-ledgerCallsIndex{color:var(--dsw-alias-label-tertiary);text-align:left!important;font-family:inherit!important}',
275
+ '.sap-ledgerCallsTotal{font-weight:600;border-bottom:none}',
276
+ '.sap-ledgerCallsNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
277
+ '.sap-ledgerGateGroup{flex-direction:column;gap:2px;display:flex}',
278
+ '.sap-ledgerRawJson{box-sizing:border-box;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:4px;cursor:pointer;font:inherit;font-size:11px;line-height:16px;padding:0 4px;margin-left:auto;flex:none}',
279
+ '.sap-ledgerRawJson:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
280
+ '.sap-ledgerJsonBody{flex-direction:column;gap:6px;display:flex}',
281
+ '.sap-ledgerJsonBlock{flex-direction:column;gap:2px;display:flex}',
282
+ '.sap-ledgerScope{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:0}',
283
+ '.sap-ledgerWindowNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
284
+ '.sap-escapeAdd{align-items:center;gap:8px;display:flex}',
285
+ '.sap-chipRemove{box-sizing:border-box;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:4px;padding:0 4px;font-size:11px;line-height:16px}',
286
+ '.sap-chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}',
106
287
  ].join('\n')
107
288
 
108
289
  // ── 纯数据:dispatch 卡片面向用户固定文案 ──────────────────────────────────
@@ -110,9 +291,9 @@ const CSS = [
110
291
  // 面向用户的固定文案。注意:此表仅集中 dispatch 卡片文案;设置页文案仍为
111
292
  // 散落硬编码,属待迁移债(迁移时一次性并入此表)。
112
293
  const ZH = {
113
- inlineProfile: '内联',
114
- inherit: '继承父',
115
- effortDefault: '默认(继承父)',
294
+ inlineProfile: '未存方案',
295
+ inherit: '继承父会话',
296
+ effortDefault: '默认(继承父会话)',
116
297
  chipProfile: '方案',
117
298
  chipPreset: '预设',
118
299
  chipProvider: '提供方',
@@ -129,17 +310,218 @@ const ZH = {
129
310
  canContinue: '可续跑',
130
311
  toolBackground: '后台任务',
131
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: '(未记录)',
132
349
  toolRunning: '运行中',
133
350
  tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
134
351
  stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
135
352
  cacheHit: '缓存命中',
136
353
  usageInput: '输入',
137
354
  usageOutput: '输出',
138
- usageCacheRead: '缓存读',
355
+ usageCacheRead: '缓存命中',
139
356
  usageCacheWrite: '缓存写',
140
357
  usageReasoning: '推理',
141
358
  persistFail: '已保存但未持久化',
142
- persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)'
359
+ persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)',
360
+ // 审计分级(设置页红字 + 丢失计数)
361
+ auditDegraded: '审计记录写入异常(已降级):请检查磁盘',
362
+ auditLost: '审计丢失计数',
363
+ // 派发决策台账(conversation.view 会话标签页)文案
364
+ ledgerTab: '派发决策台账',
365
+ ledgerSummary: '本会话派发',
366
+ ledgerDispatch: '派发',
367
+ ledgerTimes: '次',
368
+ ledgerSuccess: '成功',
369
+ ledgerReject: '拒绝',
370
+ ledgerPass: '通过',
371
+ ledgerTotalTokens: '总 token',
372
+ ledgerTotalElapsed: '总耗时',
373
+ ledgerElapsed: '耗时',
374
+ ledgerStopReason: '结束原因',
375
+ ledgerGateRejectDist: '安全检查拒绝分布',
376
+ ledgerByProfile: '按方案',
377
+ ledgerByModel: '按模型',
378
+ ledgerByTier: '按档位',
379
+ ledgerEmpty: '本会话暂无派发记录',
380
+ ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的决策轨迹。',
381
+ ledgerLoadOlder: '加载更早',
382
+ ledgerLoadingOlder: '加载中…',
383
+ ledgerLegacy: '该记录产生于决策轨迹上线前,仅有结果摘要',
384
+ ledgerGateInput: '输入',
385
+ ledgerGateOutput: '输出',
386
+ ledgerRejectFix: '修复方向',
387
+ ledgerJumpChild: '打开子会话',
388
+ ledgerChildManual: '子会话 ID',
389
+ ledgerJobManual: '后台任务 ID',
390
+ ledgerStarted: '发起',
391
+ ledgerParentContext: '父会话上下文',
392
+ ledgerRequested: '请求值',
393
+ ledgerEffectiveStep: '生效值',
394
+ ledgerDecisionTimeline: '决策时间线',
395
+ ledgerSettledStep: '结算',
396
+ ledgerRawJson: '原始 JSON',
397
+ ledgerFieldView: '字段视图',
398
+ ledgerCallDetail: '逐次调用明细',
399
+ ledgerCallsTotal: '合计',
400
+ ledgerCallsTruncated: '仅显示前 {shown} 条,共 {total} 次调用',
401
+ ledgerCallsIndex: '序号',
402
+ ledgerExecutionStep: '执行信息',
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: '本页仅展示本会话的决策轨迹,成本为「本会话」口径;跨会话进化台账、审计记录与「全局」成本口径见设置页。',
472
+ ledgerWindowNote: '已加载最近 {n} 条,尚有更早记录',
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: '未能识别的安全检查拒绝,请核对请求参数',
483
+ ignoredChipTitle: '请求值 "{value}" 已忽略',
484
+ ignoredChipTitleEmpty: '请求值已忽略',
485
+ // 逃生舱(放行非官方预设)
486
+ escapeTitle: '逃生舱(放行非官方预设)',
487
+ escapeWarn: '逃生舱已开启:允许派发非官方预设。风险自负——其余安全检查仍全量生效,每个放行都会留审计记录。',
488
+ escapeEmpty: '暂无放行预设',
489
+ escapeAdd: '添加',
490
+ escapeRemove: '移除',
491
+ escapeInputPlaceholder: '非官方预设 id',
492
+ escapeAddEmpty: '逃生舱预设 id 不能为空',
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: '安全检查预览:存在拒绝项'
143
525
  }
144
526
 
145
527
  // ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
@@ -147,14 +529,77 @@ const ZH = {
147
529
  const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
148
530
  const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
149
531
  const TOOL_MODES = [
150
- ['none', '不限制(继承父的全部工具,自动剔除 run_code)'],
532
+ ['none', '不限制(继承父会话的全部工具,自动剔除 run_code)'],
151
533
  ['allow', '白名单(只允许勾选的工具)'],
152
534
  ['deny', '黑名单(排除勾选的工具)'],
153
535
  ]
154
536
  const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
155
537
  const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
538
+ // 派发决策台账:闸名中文标签(展示层同时保留英文技术标识)。
539
+ const GATE_LABELS = { whitelist: '白名单', cost: '成本', intersection: '交集非空', approval: '审批不豁免', budget: '预算', other: '其它' }
540
+
541
+ const GATE_FIX_HINTS = {
542
+ whitelist: '目标预设不在候选白名单内,可在设置页改用受信任的预设',
543
+ cost: '参数超出模型目录或成本上限,可在设置页调整提供方/模型/推理强度',
544
+ intersection: '方案勾选的工具父会话都没有,可在设置页调整该方案的「工具限制」',
545
+ approval: '审批策略冲突,请核对审批配置',
546
+ budget: '并发派发数或本会话累计 token 已达上限,请等待在途任务完成或开启新会话'
547
+ }
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: '耗时',
570
+ output_len: '输出长度', tool_count: '工具调用数'
571
+ }
572
+
156
573
  const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
157
574
 
575
+ // 审计分级的默认态(summary 缺 audit 字段 / 值非法时回退):health ok、零丢失。
576
+ const AUDIT_DEFAULTS = { lostTelemetry: 0, lostGovernance: 0, health: 'ok' }
577
+
578
+ // 从 /options/summary 响应抽取 audit 分级;缺失/非法字段 fallback 到默认态。
579
+ function readAuditFromData(data) {
580
+ const a = data !== null && typeof data === 'object' && data.audit !== null && typeof data.audit === 'object' ? data.audit : {}
581
+ return {
582
+ lostTelemetry: typeof a.lostTelemetry === 'number' ? a.lostTelemetry : 0,
583
+ lostGovernance: typeof a.lostGovernance === 'number' ? a.lostGovernance : 0,
584
+ health: a.health === 'degraded' ? 'degraded' : 'ok'
585
+ }
586
+ }
587
+
588
+ // /options/summary 响应 → 设置页状态(enabled/options/audit/逃生舱开关与放行集)。
589
+ // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开。
590
+ function applySummaryData(data, set) {
591
+ set.enabled(data.enabled !== false)
592
+ set.options({
593
+ models: Array.isArray(data.models) ? data.models : [],
594
+ efforts: {},
595
+ presets: Array.isArray(data.presets) ? data.presets : []
596
+ })
597
+ set.audit(readAuditFromData(data))
598
+ set.escapeEnabled(data.escapeEnabled === true)
599
+ set.escapePresets(Array.isArray(data.escapePresets) ? data.escapePresets : [])
600
+ set.evolutionAdvice(data.evolutionAdvice === true)
601
+ }
602
+
158
603
  // ── 纯函数:api / 样式注入 / 文案映射 ──────────────────────────────────────
159
604
 
160
605
  // loopback HTTP,host 侧服务 /subagent-profiles/*。
@@ -163,7 +608,9 @@ async function api(path, payload) {
163
608
  ? undefined
164
609
  : {
165
610
  method: 'POST',
166
- headers: { 'Content-Type': 'application/json' },
611
+ // 写请求带 CSRF 三件套里的 Content-Type + 自定义头(Origin 由浏览器自动带)。
612
+ // 缺自定义头会被 host 侧写路由 CSRF 守卫 403 拒绝。
613
+ headers: { 'Content-Type': 'application/json', 'X-DSH-Plugin': 'dsh-subagent-profile' },
167
614
  body: JSON.stringify(payload),
168
615
  }
169
616
  const res = await fetch(`/subagent-profiles${path}`, init)
@@ -195,13 +642,83 @@ const effortZh = (id) => {
195
642
  }
196
643
  const presetZh = (v) => (v === 'inherit' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
197
644
  const profileZh = (v) => (v === undefined || v === '' || v === '(inline)' ? ZH.inlineProfile : v)
645
+ // profile id → 中文显示名:注册表有 name 时「标准编码(swap-standard)」中英双表达,
646
+ // 无 name(自定义方案可能只填了 description)时回退纯 id。技术标识 id 恒保留。
647
+ function profileDisplay(profileId, profileNames) {
648
+ if (profileId === undefined || profileId === '' || profileId === '(inline)') return ZH.inlineProfile
649
+ const name = profileNames !== undefined && profileNames !== null ? profileNames[profileId] : undefined
650
+ return typeof name === 'string' && name !== '' ? `${name}(${profileId})` : profileId
651
+ }
198
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
+
199
716
  const effortLabel = (effort) => {
200
717
  const id = effort && effort.id
201
718
  return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
202
719
  }
203
720
 
204
- // Distinct providers, derived from the model list.
721
+ // 不同 provider 列表,由模型列表派生。
205
722
  function deriveProviders(models) {
206
723
  const providers = []
207
724
  const seenProvider = new Set()
@@ -213,6 +730,16 @@ function deriveProviders(models) {
213
730
  return providers
214
731
  }
215
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
+
216
743
  // ── 纯函数:dispatch 块解析 ────────────────────────────────────────────────
217
744
 
218
745
  // 请求值:argsRaw(运行中 / 已结算两种 block 形态都有)。
@@ -232,15 +759,15 @@ function readDispatchRequest(block) {
232
759
  out.runInBackground = parsed.run_in_background === true
233
760
  }
234
761
  } catch {
235
- // unparsable argsRaw:保持默认(继承/内联口径)。
762
+ // unparsable argsRaw:保持默认(继承父会话 / 未存方案)。
236
763
  }
237
764
  return out
238
765
  }
239
766
 
240
767
  // 结果 meta(生效值):宿主 render 行("[dispatch] …")是持久回放的唯一
241
- // 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定),且宿主未
242
- // 定义 output.presentationMeta(block.meta 为空)。解析成功返回
243
- // {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
768
+ // 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定)。output.presentationMeta
769
+ // 现投影 decisionTrace(block.meta 承载决策轨迹,非生效值),生效值仍需从 render 行解析。
770
+ // 解析成功返回 {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
244
771
  const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier', 'childTotalTokens', 'childUsage', 'elapsedMs', 'stopReason']
245
772
  function parseDispatchText(text) {
246
773
  if (typeof text !== 'string') return null
@@ -296,44 +823,111 @@ function readDispatchResult(block) {
296
823
  return null
297
824
  }
298
825
 
299
- // childTotalTokens 文本后缀:宿主 render 行里的 `childTotalTokens=<number>` 经
300
- // parseDispatchText 解析为字符串,此处归一为「≈N tokens」。仅前台/后台 completed
301
- // 结算携带;无值返回空串(不显示)。渲染位置紧邻后续的耗时显示。
302
- function tokensSuffix(result, block) {
303
- let raw = null
826
+
827
+ // 结算数字提取:tokens 数与缓存命中分别取,供「≈N tokens(缓存命中 M)」
828
+ // 单括号合并展示。tokens 不可得返回 null。
829
+ function summaryNumbersOf(result, block) {
830
+ let tokens = null
304
831
  if (result && result.fields && result.fields.childTotalTokens !== undefined && result.fields.childTotalTokens !== '') {
305
- 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
306
834
  } else if (block && block.meta && typeof block.meta === 'object' && block.meta.childTotalTokens !== undefined) {
307
- 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
308
837
  }
309
- const n = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw !== '' ? Number(raw) : NaN)
310
- return Number.isFinite(n) ? ` · ≈${n} tokens` : ''
838
+ return tokens
311
839
  }
312
840
 
313
- // 耗时 + 结束原因后缀:宿主 render 行里的 `elapsedMs=<n>` / `stopReason=<enum>`
314
- // parseDispatchText 解析为字符串,此处归一为「耗时=<n>ms · stopReason=<中文映射>」。
315
- // 仅前台 completed 结算携带;后台结算经 job 结果呈现(不进 dispatch 卡片),
316
- // continuable 不结算、不携带。任一字段缺失即省略该段。
317
- function observabilitySuffix(result, block) {
841
+ // 耗时 + 结束原因提取:键名中文「耗时 X / 结束原因:Y」,
842
+ // 时间单位规则(≥1s 秒一位小数,<1s 毫秒)由 formatDurationMs 统一。
843
+ function summaryObservablesOf(result, block) {
318
844
  let elapsedRaw = null
319
845
  let stopRaw = null
320
846
  if (result && result.fields) {
321
847
  if (result.fields.elapsedMs !== undefined && result.fields.elapsedMs !== '') elapsedRaw = result.fields.elapsedMs
322
848
  if (result.fields.stopReason !== undefined && result.fields.stopReason !== '') stopRaw = result.fields.stopReason
323
849
  } else if (block && block.meta && typeof block.meta === 'object') {
324
- // 运行中(无结果)或旧版结果:回退调用块 meta(向前兼容,与 tokenTier 同口径)。
325
850
  if (block.meta.elapsedMs !== undefined) elapsedRaw = block.meta.elapsedMs
326
851
  if (block.meta.stopReason !== undefined) stopRaw = block.meta.stopReason
327
852
  }
328
853
  const ms = typeof elapsedRaw === 'number' ? elapsedRaw : (typeof elapsedRaw === 'string' && elapsedRaw !== '' ? Number(elapsedRaw) : NaN)
329
854
  const stopZh = typeof stopRaw === 'string' ? (ZH.stopReasonZh[stopRaw] || stopRaw) : ''
330
- if (!Number.isFinite(ms) && stopZh === '') return ''
331
- let out = ''
332
- if (Number.isFinite(ms)) out += ` · 耗时=${ms}ms`
333
- if (stopZh !== '') out += ` · stopReason=${stopZh}`
334
- 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(' · ')
881
+ }
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
335
915
  }
336
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
+ }
337
931
  // 把 childUsage 的(render 行 JSON 字符串或 block.meta 对象)归一为五段对象;
338
932
  // 非 object / 非 number 段忽略,无任何有效段返回 null。可选字段(缓存读/写/推理)
339
933
  // 缺值省略键,与 host 侧 collectChildUsage 输出对齐。
@@ -369,15 +963,6 @@ function usageFrom(result, block) {
369
963
  return null
370
964
  }
371
965
 
372
- // 缓存命中后缀:childUsage.cacheReadTokens 存在且 >0 时追加「 · 缓存命中 M」到
373
- // ≈N tokens 之后(前导分隔符与 tokensSuffix/observabilitySuffix 同口径,即使
374
- // tokens 段缺失也不会粘在一起);无分解或缓存读为 0/缺失时返回空串。
375
- function cacheHitSuffix(usage) {
376
- if (usage === null) return ''
377
- const cacheRead = usage.cacheReadTokens
378
- return typeof cacheRead === 'number' && cacheRead > 0 ? ` · ${ZH.cacheHit} ${cacheRead}` : ''
379
- }
380
-
381
966
  // 五段分解行文案(纯函数,供卡片详情区渲染);无任何有效段返回空串。
382
967
  function usageBreakdownText(usage) {
383
968
  if (usage === null || typeof usage !== 'object') return ''
@@ -393,6 +978,764 @@ function usageBreakdownText(usage) {
393
978
  return parts.join(' · ')
394
979
  }
395
980
 
981
+ // ── 纯函数:派发决策台账数据层(快照遍历 / 轨迹归一 / 汇总统计 / 失败合并)────
982
+
983
+ // 把 string|number 归一为有限数,不可得返回 null(汇总与行级数字的统一口径)。
984
+ function toFiniteNumber(v) {
985
+ const n = typeof v === 'number' ? v : (typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN)
986
+ return Number.isFinite(n) ? n : null
987
+ }
988
+
989
+ // 非数组对象守卫:对象原样返回,其它(null/数组/原始值)返回空对象。
990
+ function asObject(v) {
991
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : {}
992
+ }
993
+
994
+ // 从 result 内容块取首行文本(只取第一块非空文本,无文本返回空串)。
995
+ function firstContentLine(content) {
996
+ if (!Array.isArray(content)) return ''
997
+ for (const item of content) {
998
+ if (item && typeof item === 'object' && typeof item.text === 'string' && item.text !== '') {
999
+ return item.text.split('\n')[0] ?? ''
1000
+ }
1001
+ }
1002
+ return ''
1003
+ }
1004
+
1005
+ // argsRaw 摘要:dispatch 参数为单行 JSON,取首行即完整摘要;空/坏返回空串。
1006
+ function summarizeArgsRaw(argsRaw) {
1007
+ if (typeof argsRaw !== 'string' || argsRaw === '') return ''
1008
+ return argsRaw.split('\n')[0] ?? ''
1009
+ }
1010
+
1011
+ // gate 条目归一:只保留渲染需要的五字段,name/verdict/reason 逐字段 typeof 守卫、
1012
+ // 缺失补空串;input/output 缺省为空对象;非对象条目整条丢弃。
1013
+ function normalizeGateEntries(gates) {
1014
+ if (!Array.isArray(gates)) return []
1015
+ const out = []
1016
+ for (const gate of gates) {
1017
+ if (gate === null || typeof gate !== 'object' || Array.isArray(gate)) continue
1018
+ out.push({
1019
+ name: typeof gate.name === 'string' ? gate.name : '',
1020
+ input: asObject(gate.input),
1021
+ output: asObject(gate.output),
1022
+ verdict: typeof gate.verdict === 'string' ? gate.verdict : '',
1023
+ reason: typeof gate.reason === 'string' ? gate.reason : ''
1024
+ })
1025
+ }
1026
+ return out
1027
+ }
1028
+
1029
+ // decisionTrace 归一:坏数据/缺字段 fail-soft(逐段 typeof 守卫,不 throw)。
1030
+ // 非对象或无任何可识别 shape(version/startedAt/gates 三者皆缺)返回 null;
1031
+ // 各段缺失补空对象,settled 缺省为 null(区分「未结算」与「结算但为空」)。
1032
+ function normalizeDecisionTrace(raw) {
1033
+ if (raw === undefined || raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
1034
+ if (typeof raw.version !== 'number' && typeof raw.startedAt !== 'number' && !Array.isArray(raw.gates)) return null
1035
+ return {
1036
+ version: typeof raw.version === 'number' ? raw.version : 1,
1037
+ startedAt: typeof raw.startedAt === 'number' ? raw.startedAt : null,
1038
+ parentContext: asObject(raw.parentContext),
1039
+ requested: asObject(raw.requested),
1040
+ gates: normalizeGateEntries(raw.gates),
1041
+ effective: asObject(raw.effective),
1042
+ execution: asObject(raw.execution),
1043
+ settled: raw.settled && typeof raw.settled === 'object' && !Array.isArray(raw.settled) ? raw.settled : null
1044
+ }
1045
+ }
1046
+
1047
+ // 从 tool-result 节点读决策轨迹:宿主 presentationMeta 把 decisionTrace 直接投影为
1048
+ // 节点 meta(meta 即 trace 本身);同时兼容未来包一层 .decisionTrace 的形态,
1049
+ // 两种都尝试、绝不 throw。
1050
+ function readNodeTrace(node) {
1051
+ const meta = node && typeof node === 'object' ? node.meta : null
1052
+ if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) return null
1053
+ const direct = normalizeDecisionTrace(meta)
1054
+ if (direct !== null) return direct
1055
+ if (meta.decisionTrace && typeof meta.decisionTrace === 'object' && !Array.isArray(meta.decisionTrace)) {
1056
+ return normalizeDecisionTrace(meta.decisionTrace)
1057
+ }
1058
+ return null
1059
+ }
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
+
1406
+ // 快照遍历:过滤 chat.legacy.nodes 中 kind==='tool-result' 且 call.name==='dispatch'
1407
+ // 的节点,产出台账记录。记录携带 callId/time/isError/trace(已归一,null 即旧记录
1408
+ // 降级 legacy)/request/result/argsSummary/contentFirstLine。输入为空或非数组返回
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
+
1443
+ function collectDispatchRecords(nodes) {
1444
+ if (!Array.isArray(nodes)) return []
1445
+ const records = []
1446
+ for (let index = 0; index < nodes.length; index += 1) {
1447
+ const node = nodes[index]
1448
+ if (!node || typeof node !== 'object') continue
1449
+ if (node.kind !== 'tool-result') continue
1450
+ if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
1451
+ const trace = readNodeTrace(node)
1452
+ if (trace !== null) trace.parentText = surroundingText(nodes, index)
1453
+ records.push({
1454
+ callId: typeof node.callId === 'string' ? node.callId : '',
1455
+ time: typeof node.time === 'number' ? node.time : (typeof node.callTime === 'number' ? node.callTime : null),
1456
+ isError: node.isError === true,
1457
+ legacy: trace === null,
1458
+ trace,
1459
+ request: readDispatchRequest(node),
1460
+ result: readDispatchResult(node),
1461
+ argsSummary: summarizeArgsRaw(node.call.argsRaw),
1462
+ contentFirstLine: firstContentLine(node.content)
1463
+ })
1464
+ }
1465
+ return records
1466
+ }
1467
+
1468
+ // 从单条记录提取结算数字:trace.settled 优先,缺失回退 result.fields(旧记录口径)。
1469
+ // 返回 { tokens, cacheRead, elapsedMs };不可得字段为 null/0。
1470
+ function recordNumbers(record) {
1471
+ let tokens = null
1472
+ let elapsedMs = null
1473
+ let cacheRead = 0
1474
+ const trace = record && record.trace
1475
+ const result = record && record.result
1476
+ const settled = trace && trace.settled
1477
+ if (settled) {
1478
+ tokens = toFiniteNumber(settled.childTotalTokens)
1479
+ elapsedMs = toFiniteNumber(settled.elapsedMs)
1480
+ if (settled.childUsage && typeof settled.childUsage.cacheReadTokens === 'number') cacheRead = settled.childUsage.cacheReadTokens
1481
+ }
1482
+ if (result && result.fields) {
1483
+ if (tokens === null) tokens = toFiniteNumber(result.fields.childTotalTokens)
1484
+ if (elapsedMs === null) elapsedMs = toFiniteNumber(result.fields.elapsedMs)
1485
+ if (cacheRead === 0) {
1486
+ const usage = parseChildUsage(result.fields.childUsage)
1487
+ if (usage && typeof usage.cacheReadTokens === 'number') cacheRead = usage.cacheReadTokens
1488
+ }
1489
+ }
1490
+ return { tokens, cacheRead, elapsedMs }
1491
+ }
1492
+
1493
+ // 判定记录是否被拒绝:trace 任一闸 verdict==='fail' 视为闸拒绝;无 trace 且 isError
1494
+ // 视为拒绝(旧错误块无法区分闸拒绝与运行时错误,统一计拒绝)。
1495
+ function recordIsRejected(record) {
1496
+ if (!record) return false
1497
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
1498
+ if (gates.some((gate) => gate.verdict === 'fail')) return true
1499
+ return record.isError === true
1500
+ }
1501
+
1502
+ // 累加记录各失败闸到闸拒绝分布;未知闸名归入 other(counts 已含五键)。
1503
+ function countGateRejects(record, counts) {
1504
+ if (!record || !record.trace || !Array.isArray(record.trace.gates)) return
1505
+ for (const gate of record.trace.gates) {
1506
+ if (gate.verdict !== 'fail') continue
1507
+ const key = Object.prototype.hasOwnProperty.call(counts, gate.name) ? gate.name : 'other'
1508
+ counts[key] += 1
1509
+ }
1510
+ }
1511
+
1512
+ // 记录归属方案:effective.profile → requested.profile → request.profile 依次回退;
1513
+ // 缺失返回空串(不参与 byProfile 计数)。
1514
+ function recordProfile(record) {
1515
+ if (!record) return ''
1516
+ const trace = record.trace
1517
+ if (trace && trace.effective && typeof trace.effective.profile === 'string' && trace.effective.profile !== '') return trace.effective.profile
1518
+ if (trace && trace.requested && typeof trace.requested.profile === 'string' && trace.requested.profile !== '') return trace.requested.profile
1519
+ if (record.request && typeof record.request.profile === 'string') return record.request.profile
1520
+ return ''
1521
+ }
1522
+
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 为合并后的完整记录列表(含失败记录)。
1543
+ // 旧记录贡献可得字段(次数/耗时/token),不可得记 0/省略;闸拒绝按 trace.gates
1544
+ // 的 fail verdict 分布。byProfile/byModel/byTier 均按次数降序。
1545
+ function computeLedgerSummary(records) {
1546
+ const list = Array.isArray(records) ? records : []; const summary = {
1547
+ dispatchCount: list.length,
1548
+ successCount: 0,
1549
+ rejectCount: 0,
1550
+ totalTokens: 0,
1551
+ cacheHitTokens: 0,
1552
+ totalElapsedMs: 0,
1553
+ estimatedCost: 0,
1554
+ estimatedInheritCost: 0,
1555
+ estimatedSaving: 0,
1556
+ inheritCount: 0,
1557
+ inheritRatio: 0,
1558
+ pricedCount: 0,
1559
+ gateRejectCounts: { whitelist: 0, cost: 0, intersection: 0, approval: 0, other: 0 },
1560
+ byProfile: [],
1561
+ byModel: [],
1562
+ byTier: []
1563
+ }
1564
+ const profileMap = new Map(), modelMap = new Map(), tierMap = new Map()
1565
+ for (const record of list) {
1566
+ if (recordIsRejected(record)) summary.rejectCount += 1
1567
+ else summary.successCount += 1
1568
+ const { tokens, cacheRead, elapsedMs } = recordNumbers(record)
1569
+ if (tokens !== null) summary.totalTokens += tokens
1570
+ summary.cacheHitTokens += cacheRead
1571
+ if (elapsedMs !== null) summary.totalElapsedMs += elapsedMs
1572
+ accumulateRecordCost(summary, record)
1573
+ countGateRejects(record, summary.gateRejectCounts)
1574
+ const profile = recordProfile(record)
1575
+ if (profile !== '') profileMap.set(profile, (profileMap.get(profile) || 0) + 1)
1576
+ // 模型:空/'(parent)'(继承父会话标记)并入继承桶(键为空串,展示层经 rawOrInherit 归一)。
1577
+ const model = recordModel(record)
1578
+ if (model === '' || model === '(parent)') modelMap.set('', (modelMap.get('') || 0) + 1)
1579
+ else modelMap.set(model, (modelMap.get(model) || 0) + 1)
1580
+ const tier = recordTier(record)
1581
+ if (tier !== '') tierMap.set(tier, (tierMap.get(tier) || 0) + 1)
1582
+ }
1583
+ summary.byProfile = [...profileMap.entries()]
1584
+ .map(([profile, count]) => ({ profile, count }))
1585
+ .sort((a, b) => b.count - a.count)
1586
+ summary.byModel = [...modelMap.entries()]
1587
+ .map(([model, count]) => ({ model, count }))
1588
+ .sort((a, b) => b.count - a.count)
1589
+ summary.byTier = [...tierMap.entries()]
1590
+ .map(([tier, count]) => ({ tier, count }))
1591
+ .sort((a, b) => b.count - a.count)
1592
+ finalizeLedgerCost(summary)
1593
+ return summary
1594
+ }
1595
+
1596
+ // 拉取失败台账:经既有 api() 走 GET /ledger/failures?session=<id>,失败项同样经
1597
+ // normalizeDecisionTrace 归一。host 侧经 stateFile(subagent-profiles.failed-traces.json)
1598
+ // 落盘,重启后仍可读;host 不可达/路由缺失/坏数据一律 fail-soft 返回空数组。
1599
+ async function fetchLedgerFailures(sessionId) {
1600
+ if (typeof sessionId !== 'string' || sessionId === '') return []
1601
+ try {
1602
+ const data = await api(`/ledger/failures?session=${encodeURIComponent(sessionId)}`)
1603
+ const list = Array.isArray(data.failures) ? data.failures : []
1604
+ const out = []
1605
+ for (const item of list) {
1606
+ const trace = normalizeDecisionTrace(item)
1607
+ if (trace !== null) out.push(trace)
1608
+ }
1609
+ return out
1610
+ } catch {
1611
+ return []
1612
+ }
1613
+ }
1614
+
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/
1684
+ // execution 还原,callId/argsSummary/contentFirstLine 为空,标记 failureOnly。
1685
+ function failureRecord(failure) {
1686
+ const requested = asObject(failure && failure.requested)
1687
+ const execution = asObject(failure && failure.execution)
1688
+ const request = {
1689
+ profile: typeof requested.profile === 'string' ? requested.profile : '',
1690
+ preset: typeof requested.preset === 'string' ? requested.preset : '',
1691
+ provider: typeof requested.provider === 'string' ? requested.provider : '',
1692
+ model: typeof requested.model === 'string' ? requested.model : '',
1693
+ reasoningEffort: typeof requested.reasoningEffort === 'string' ? requested.reasoningEffort : '',
1694
+ continuable: execution.kind === 'continuable' || execution.mode === 'continuable',
1695
+ runInBackground: execution.kind === 'background'
1696
+ }
1697
+ return {
1698
+ callId: '',
1699
+ time: failure && typeof failure.startedAt === 'number' ? failure.startedAt : null,
1700
+ isError: true,
1701
+ legacy: false,
1702
+ trace: failure,
1703
+ request,
1704
+ result: null,
1705
+ argsSummary: '',
1706
+ contentFirstLine: '',
1707
+ failureOnly: true
1708
+ }
1709
+ }
1710
+
1711
+ // 失败记录合并:把失败台账与快照错误块按 startedAt 就近匹配(±2s 窗口)。
1712
+ // 匹配成功 → 失败 trace 回填进对应错误块记录(isError 且无 trace);匹配失败 →
1713
+ // 独立列为新记录(failureOnly 标记)。返回合并后的完整记录列表,不修改输入数组。
1714
+ function mergeFailureRecords(settledRecords, failures) {
1715
+ const records = (Array.isArray(settledRecords) ? settledRecords : []).map((record) => ({ ...record }))
1716
+ const failureList = Array.isArray(failures) ? failures : []
1717
+ const unmatched = []
1718
+ for (const failure of failureList) {
1719
+ const ts = failure && typeof failure.startedAt === 'number' ? failure.startedAt : null
1720
+ let matched = false
1721
+ if (ts !== null) {
1722
+ for (const record of records) {
1723
+ if (record.isError !== true || record.trace !== null) continue
1724
+ if (typeof record.time !== 'number') continue
1725
+ if (Math.abs(record.time - ts) <= 2000) {
1726
+ record.trace = failure
1727
+ record.legacy = false
1728
+ matched = true
1729
+ break
1730
+ }
1731
+ }
1732
+ }
1733
+ if (!matched) unmatched.push(failure)
1734
+ }
1735
+ for (const failure of unmatched) records.push(failureRecord(failure))
1736
+ return records
1737
+ }
1738
+
396
1739
  // ── dispatch 工具卡(React 依赖经参数注入)─────────────────────────────────
397
1740
 
398
1741
  // 生效值:结果解析成功用结果(优先),字段缺失回退请求值。
@@ -437,7 +1780,7 @@ function resolveDispatchState(request, result, settled) {
437
1780
  function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
438
1781
  const chips = []
439
1782
  chips.push(chip('profile', ZH.chipProfile, profileZh(eff.profile)))
440
- // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父」)与
1783
+ // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父会话」)与
441
1784
  // provider·model / effort 一样弱显。
442
1785
  chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: eff.preset === 'inherit' || eff.preset === '' }))
443
1786
  const p = rawOrInherit(eff.provider)
@@ -459,7 +1802,7 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
459
1802
  key: `ignored-${key}`,
460
1803
  className: 'sap-chip sap-chipWarn',
461
1804
  title: reqVal !== '' ? `请求值 "${reqVal}" 已忽略` : '请求值已忽略'
462
- }, `⬆${key}(${ZH.ignoredMark})`))
1805
+ }, `⬆${FIELD_ZH[key] || key}(${ZH.ignoredMark})`))
463
1806
  }
464
1807
  return chips
465
1808
  }
@@ -468,53 +1811,207 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
468
1811
  function makeChip(el) {
469
1812
  const chip = (key, label, value, opts) => {
470
1813
  const valueCls = opts && opts.dim ? 'sap-chipValueDim' : 'sap-chipValue'
1814
+ const dot = opts && opts.dot === true ? el('span', { className: 'sap-chipDot' }, ' · ') : null
471
1815
  return el('span', {
472
1816
  key,
473
1817
  className: 'sap-chip',
474
1818
  ...(opts && typeof opts.title === 'string' && opts.title !== '' ? { title: opts.title } : {})
475
1819
  },
476
1820
  el('span', { className: 'sap-chipLabel' }, label),
1821
+ dot,
477
1822
  el('span', { className: valueCls }, value)
478
1823
  )
479
1824
  }
480
1825
  return chip
481
1826
  }
482
1827
 
483
- function makeDispatchToolview(el, chip, useState) {
484
- function DispatchToolview(props) {
485
- const block = props && props.block
486
- const request = readDispatchRequest(block)
487
- const result = readDispatchResult(block)
488
- const settled = !!(block && typeof block === 'object' && 'kind' in block)
489
- const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
490
- const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
491
- const usage = usageFrom(result, block)
492
- const [open, setOpen] = useState(false)
493
- // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
494
- // 提供方/模型/推理强度/档位)全部在 chip 行——避免同一组信息重复两遍。
495
- const text = `${ZH.toolLabel}:${kindLabel}${tokensSuffix(result, block)}${cacheHitSuffix(usage)}${observabilitySuffix(result, block)}`
496
- // 详情折叠入口:仅当存在真实计费分解(childUsage)时渲染;无分解数据
497
- // (旧结果/无 usage 会话)不显示折叠区,向前兼容。
498
- const detailToggle = usage === null ? null : el('button', {
499
- type: 'button',
500
- className: 'sap-detailToggle',
501
- 'aria-expanded': open,
502
- onClick: () => setOpen((prev) => !prev)
503
- }, open ? ZH.detailClose : ZH.detailOpen)
504
- // 无分解数据时保持原摘要行结构(零漂移);有分解时才加折叠入口与详情行。
505
- const summary = usage === null
506
- ? el('div', { className: 'sap-toolText' }, text)
507
- : el('div', { className: 'sap-toolSummary' },
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) {
1970
+ function DispatchToolview(props) {
1971
+ const block = props && props.block; const request = readDispatchRequest(block); const result = readDispatchResult(block)
1972
+ const settled = !!(block && typeof block === 'object' && 'kind' in block)
1973
+ const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
1974
+ const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
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
1981
+ const usage = usageFrom(result, block)
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)
1985
+ // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
1986
+ // 提供方/模型/推理强度/档位)全部在 chip 行——避免同一组信息重复两遍。
1987
+ const text = dispatchSummaryText(request, result, block, trace, kindLabel)
1988
+ // 详情折叠入口:仅当存在真实计费分解(childUsage)时渲染;无分解数据
1989
+ // (旧结果/无 usage 会话)不显示折叠区,向前兼容。
1990
+ const detailToggle = usage === null ? null : el('button', {
1991
+ type: 'button',
1992
+ className: 'sap-detailToggle',
1993
+ 'aria-expanded': open,
1994
+ onClick: () => setOpen((prev) => !prev)
1995
+ }, open ? ZH.detailClose : ZH.detailOpen)
1996
+ // 无分解数据时保持原摘要行结构(零漂移);有分解时才加折叠入口与详情行。
1997
+ const summary = usage === null
1998
+ ? el('div', { className: 'sap-toolText' }, text)
1999
+ : el('div', { className: 'sap-toolSummary' },
508
2000
  el('div', { className: 'sap-toolText' }, text),
509
2001
  detailToggle
510
2002
  )
511
2003
  const detailBody = open && usage !== null
512
- ? el('div', { className: 'sap-toolDetail' }, usageBreakdownText(usage))
2004
+ ? el('div', { className: 'sap-toolDetail' }, usageDetailText(usage))
513
2005
  : null
2006
+ const draftButton = buildDraftButton(el, draft); const draftForm = buildDraftForm(el, draft)
514
2007
  return el('div', { className: 'sap-toolview' },
2008
+ buildDispatchPhaseLine(el, request, result, settled, trace, jobSettled),
515
2009
  el('div', { className: 'sap-chips' }, chips),
2010
+ metaLine,
516
2011
  summary,
517
- detailBody
2012
+ detailBody,
2013
+ draftButton,
2014
+ draftForm
518
2015
  )
519
2016
  }
520
2017
  return DispatchToolview
@@ -543,7 +2040,12 @@ function makeFormActions(s, loadEfforts, loadTools) {
543
2040
  const value = event && event.target ? event.target.value : ''
544
2041
  s.setForm((prev) => {
545
2042
  const next = { ...prev, [key]: value }
546
- 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
+ }
547
2049
  return next
548
2050
  })
549
2051
  if (key === 'model' && value !== '') {
@@ -625,7 +2127,7 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
625
2127
  const submit = () => {
626
2128
  const payload = { id: typeof s.form.id === 'string' ? s.form.id.trim() : '' }
627
2129
  // 字符串字段总是发送(含空串):后端 merge 语义里空串 = 清除该字段,
628
- // 否则把字段清回“继承父/默认”会被静默忽略(保留旧值)。
2130
+ // 否则把字段清回“继承父会话/默认”会被静默忽略(保留旧值)。
629
2131
  const add = (key, value) => { if (typeof value === 'string') payload[key] = value.trim() }
630
2132
  add('description', s.form.description)
631
2133
  add('preset', s.form.preset)
@@ -654,17 +2156,24 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
654
2156
  return { submit }
655
2157
  }
656
2158
 
657
- function makeRowActions(api, s, refresh, applyWrite) {
658
- const toggleEnabled = () => {
659
- const next = !s.enabled
660
- s.setEnabled(next)
661
- s.setError('')
662
- api('/set-enabled', { enabled: next })
2159
+ // 乐观开关:next=!get() 先落 UI,成功保留;失败回滚 + 经 setError 展示。
2160
+ function makeToggle(api, path, payloadKey, get, set, setError) {
2161
+ return () => {
2162
+ const next = !get()
2163
+ set(next)
2164
+ setError('')
2165
+ api(path, { [payloadKey]: next })
663
2166
  .catch((err) => {
664
- s.setEnabled(!next)
665
- s.setError(String((err && err.message) || err))
2167
+ set(!next)
2168
+ setError(String((err && err.message) || err))
666
2169
  })
667
2170
  }
2171
+ }
2172
+
2173
+ function makeRowActions(api, s, refresh, applyWrite) {
2174
+ const toggleEnabled = makeToggle(api, '/set-enabled', 'enabled', () => s.enabled, s.setEnabled, s.setError)
2175
+ const toggleEscape = makeToggle(api, '/set-escape', 'enabled', () => s.escapeEnabled, s.setEscapeEnabled, s.setError)
2176
+ const toggleAdvice = makeToggle(api, '/set-evolution-advice', 'advice', () => s.evolutionAdvice, s.setEvolutionAdvice, s.setError)
668
2177
  const setProfileEnabled = (profile, next) => () => {
669
2178
  s.setError('')
670
2179
  api('/set-profile-enabled', { id: profile.id, enabled: next })
@@ -674,7 +2183,7 @@ function makeRowActions(api, s, refresh, applyWrite) {
674
2183
  const resetAll = () => {
675
2184
  if (!window.confirm('将所有内置方案重置为默认配置?已删除的内置也会恢复。')) return
676
2185
  s.setError('')
677
- api('/reset-all')
2186
+ api('/reset-all', {})
678
2187
  .then((data) => { applyWrite(data, '已重置所有内置方案'); refresh() })
679
2188
  .catch((err) => s.setError(String((err && err.message) || err)))
680
2189
  }
@@ -688,7 +2197,32 @@ function makeRowActions(api, s, refresh, applyWrite) {
688
2197
  })
689
2198
  .catch((err) => s.setError(String((err && err.message) || err)))
690
2199
  }
691
- return { toggleEnabled, setProfileEnabled, resetAll, remove }
2200
+ return { toggleEnabled, toggleEscape, toggleAdvice, setProfileEnabled, resetAll, remove }
2201
+ }
2202
+
2203
+ // 逃生舱放行集增删:/escape-add 成功后用响应 presets 刷新本地列表并清空输入;
2204
+ // /escape-remove 成功后同样用响应 presets 刷新。失败统一经 s.setError 展示。
2205
+ function makeEscapeActions(api, s) {
2206
+ const escapeAdd = () => {
2207
+ const value = typeof s.escapeInput === 'string' ? s.escapeInput.trim() : ''
2208
+ if (value === '') { s.setError(ZH.escapeAddEmpty); return }
2209
+ s.setError('')
2210
+ api('/escape-add', { preset: value })
2211
+ .then((data) => {
2212
+ s.setEscapeInput('')
2213
+ if (Array.isArray(data.presets)) s.setEscapePresets(data.presets)
2214
+ })
2215
+ .catch((err) => s.setError(String((err && err.message) || err)))
2216
+ }
2217
+ const escapeRemove = (id) => () => {
2218
+ s.setError('')
2219
+ api('/escape-remove', { preset: id })
2220
+ .then((data) => {
2221
+ if (Array.isArray(data.presets)) s.setEscapePresets(data.presets)
2222
+ })
2223
+ .catch((err) => s.setError(String((err && err.message) || err)))
2224
+ }
2225
+ return { escapeAdd, escapeRemove }
692
2226
  }
693
2227
 
694
2228
  // 选中模型 → 懒拉 /options/efforts(按模型 id 存入 options.efforts)。
@@ -713,21 +2247,39 @@ function makeToolsLoader(api, setTools) {
713
2247
  }
714
2248
  }
715
2249
 
716
- // 版本探测 → 拉 /options/versions;warnings 非空时设置页顶部常驻 amber 提示条。
717
- function makeVersionsLoader(api, setVersionWarnings) {
2250
+ // 版本探测 → 拉 /options/versions;warnings 非空时维护区展示告警,否则显示插件版本。
2251
+ function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
718
2252
  return () => {
719
2253
  api('/options/versions')
720
- .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
+ })
721
2258
  .catch(() => {})
722
2259
  }
723
2260
  }
724
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
+
725
2275
  function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
726
2276
  return {
727
2277
  ...makeFormActions(s, loadEfforts, loadTools),
728
2278
  ...makeEditActions(s, loadEfforts, loadTools),
729
2279
  ...makeSubmitActions(api, s, refresh, applyWrite),
730
2280
  ...makeRowActions(api, s, refresh, applyWrite),
2281
+ ...makeEscapeActions(api, s),
2282
+ locateProfile: makeLocateProfile(s),
731
2283
  }
732
2284
  }
733
2285
 
@@ -737,7 +2289,7 @@ function buildSelectField(el, s, setField, label, key, optionsList, renderOption
737
2289
  return el('div', { className: 'sap-field' },
738
2290
  el('span', { className: 'sap-fieldLabel' }, label),
739
2291
  el('select', { className: 'sap-input sap-select', value: s.form[key], onChange: setField(key) },
740
- el('option', { value: '' }, '继承父(不指定)'),
2292
+ el('option', { value: '' }, '继承父会话(不指定)'),
741
2293
  optionsList.map(renderOption)
742
2294
  )
743
2295
  )
@@ -833,7 +2385,8 @@ function buildProfileRows(el, chip, s, actions) {
833
2385
  ? el('span', { key: 'state', className: 'sap-chip sap-chipDisabled' }, ZH.chipDisabled)
834
2386
  : el('span', { key: 'state', className: 'sap-chip sap-chipSuccess' }, ZH.chipEnabled)
835
2387
  ]
836
- 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' },
837
2390
  el('div', { className: 'sap-rowHead' },
838
2391
  el('span', { className: 'sap-rowIdentity' },
839
2392
  el('span', { className: 'sap-rowName' }, hasName ? profile.name : String(profile.id)),
@@ -852,13 +2405,33 @@ function buildProfileRows(el, chip, s, actions) {
852
2405
  el('div', { className: 'sap-chips' }, chipItems),
853
2406
  typeof profile.description === 'string' && profile.description !== ''
854
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(' · '))
855
2415
  : null
856
2416
  )
857
2417
  })
858
2418
  }
859
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
+
860
2434
  function buildAddCard(el, s, actions, toolSection) {
861
- const providers = deriveProviders(s.options.models)
862
2435
  const effortsForModel = s.form.model ? (s.options.efforts[s.form.model] ?? []) : []
863
2436
  const renderEffortOption = (effort) => el('option', { key: effort.id, value: effort.id }, effortLabel(effort))
864
2437
  return el('div', { className: 'sap-addCard' },
@@ -875,16 +2448,15 @@ function buildAddCard(el, s, actions, toolSection) {
875
2448
  ),
876
2449
  buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
877
2450
  (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
878
- 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,
879
2452
  el('details', { className: 'sap-customized' },
880
2453
  el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
881
2454
  el('div', { className: 'sap-customizedBody' },
882
- 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」标注'),
883
2456
  buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
884
2457
  (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
885
- s.options.presets.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到 system-trust 预设名册(rosterless)——目标预设下拉不可用') : null,
886
- buildSelectField(el, s, actions.setField, '提供方(模型服务商)', 'provider', providers,
887
- (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),
888
2460
  s.form.model === ''
889
2461
  ? el('div', { className: 'sap-field' },
890
2462
  el('span', { className: 'sap-fieldLabel' }, '推理强度(思考深度)'),
@@ -903,7 +2475,289 @@ function buildAddCard(el, s, actions, toolSection) {
903
2475
  )
904
2476
  }
905
2477
 
906
- function buildSectionReturn(el, s, actions, rows, addCard) {
2478
+ // 派发优化建议注入:一行开关,只提示不改变派发行为。
2479
+ function buildAdviceSection(el, s, actions) {
2480
+ return el('div', { className: 'sap-rowCard' },
2481
+ el('div', { className: 'sap-rowHead' },
2482
+ el('span', { className: 'sap-rowIdentity' },
2483
+ el('span', { className: 'sap-rowName' }, ZH.adviceTitle)
2484
+ ),
2485
+ el('span', { className: 'sap-rowActions' },
2486
+ el('label', { className: 'sap-switch', title: ZH.adviceTitle },
2487
+ el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
2488
+ el('span', null, s.evolutionAdvice ? ZH.adviceSwitchOn : ZH.adviceSwitchOff)
2489
+ )
2490
+ )
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
+ )
2536
+ )
2537
+ }
2538
+
2539
+ // 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
2540
+ // 每项「移除」按钮)。增删走 /escape-add /escape-remove,成功后用响应 presets 刷新。
2541
+ function buildEscapeSection(el, s, actions) {
2542
+ const list = Array.isArray(s.escapePresets) ? s.escapePresets : []
2543
+ return el('div', { className: 'sap-rowCard' },
2544
+ el('div', { className: 'sap-rowHead' },
2545
+ el('span', { className: 'sap-rowIdentity' },
2546
+ el('span', { className: 'sap-rowName' }, ZH.escapeTitle)
2547
+ ),
2548
+ el('span', { className: 'sap-rowActions' },
2549
+ el('label', { className: 'sap-switch', title: '逃生舱开关' },
2550
+ el('input', { type: 'checkbox', checked: s.escapeEnabled, onChange: actions.toggleEscape }),
2551
+ el('span', null, s.escapeEnabled ? '已启用' : '已禁用')
2552
+ )
2553
+ )
2554
+ ),
2555
+ s.escapeEnabled
2556
+ ? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
2557
+ : null,
2558
+ el('div', { className: 'sap-escapeAdd' },
2559
+ el('input', {
2560
+ className: 'sap-input',
2561
+ style: { flex: '1 1 0', minWidth: '160px' },
2562
+ value: s.escapeInput,
2563
+ placeholder: ZH.escapeInputPlaceholder,
2564
+ onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
2565
+ }),
2566
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
2567
+ ),
2568
+ el('div', { className: 'sap-chips' },
2569
+ list.length === 0
2570
+ ? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
2571
+ : list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
2572
+ String(id),
2573
+ el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
2574
+ ))
2575
+ )
2576
+ )
2577
+ }
2578
+
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)
907
2761
  return el('div', { className: 'sap-section' },
908
2762
  el('div', { className: 'sap-titleRow' },
909
2763
  el('h2', { className: 'sap-title' }, '子 Agent 方案'),
@@ -912,105 +2766,1303 @@ function buildSectionReturn(el, s, actions, rows, addCard) {
912
2766
  el('span', null, s.enabled ? '已启用' : '已禁用')
913
2767
  )
914
2768
  ),
915
- s.versionWarnings.length > 0
916
- ? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
917
- : null,
918
- el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
919
- el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
920
- el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.resetAll }, '重置所有内置方案')
921
- ),
922
- !s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
923
- s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
924
- s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
925
- s.persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${s.persistWarning}${ZH.persistFailHint}`) : null,
926
- el('ul', { className: 'sap-rows' }, rows),
927
- el('div', { className: 'sap-addBlock' },
928
- s.adding ? addCard : el('div', { className: 'sap-addActions' },
929
- 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)
930
2782
  )
931
- )
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)
932
2790
  )
933
2791
  }
934
2792
 
935
- 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) {
936
2829
  function ProfilesSection() {
937
- const [profiles, setProfiles] = useState([])
938
- const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] })
939
- const [enabled, setEnabled] = useState(true)
940
- const [adding, setAdding] = useState(false)
941
- const [editingId, setEditingId] = useState(null)
942
- const [error, setError] = useState('')
943
- const [savedNotice, setSavedNotice] = useState('')
944
- const [persistWarning, setPersistWarning] = useState('')
945
- const [form, setForm] = useState(EMPTY_FORM)
946
- const [tools, setTools] = useState([])
947
- const [versionWarnings, setVersionWarnings] = useState([])
948
- const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings,
949
- setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings }
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 }
950
2841
  const refresh = useCallback(() => {
951
2842
  api('/list')
952
- .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
+ })
953
2849
  .catch((err) => setError(String((err && err.message) || err)))
2850
+ api('/drafts')
2851
+ .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
2852
+ .catch(() => {})
954
2853
  }, [])
955
2854
  const loadOptions = useCallback(() => {
956
2855
  api('/options/summary')
957
- .then((data) => {
958
- setEnabled(data.enabled !== false)
959
- setOptions({
960
- models: Array.isArray(data.models) ? data.models : [],
961
- efforts: {},
962
- presets: Array.isArray(data.presets) ? data.presets : []
963
- })
964
- })
2856
+ .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice }))
965
2857
  .catch(() => {})
966
2858
  }, [])
967
2859
  const loadEfforts = makeEffortsLoader(api, setOptions)
968
2860
  const loadTools = makeToolsLoader(api, setTools)
969
- // 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变
970
- // 导致设置页打开期间无限重取(与 loadOptions 同款稳定化)。
971
- const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings)(), [])
2861
+ // 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变导致设置页打开期间无限重取。
2862
+ const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), [])
972
2863
  useEffect(() => {
973
2864
  refresh()
974
2865
  loadOptions()
975
2866
  loadVersions()
976
2867
  }, [refresh, loadOptions, loadVersions])
977
- const applyWrite = makeApplyWrite(s)
978
- 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)
979
2870
  const rows = buildProfileRows(el, chip, s, actions)
980
2871
  const toolSection = buildToolSection(el, s, actions)
981
- 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))
982
2873
  }
983
2874
  return ProfilesSection
984
2875
  }
985
2876
 
2877
+ // ── 派发决策台账:会话标签页(React 依赖经参数注入)──────────────────────────
2878
+
2879
+ // epoch ms → 本地 HH:MM:SS;非有限数返回空串。
2880
+ function formatClockTime(time) {
2881
+ if (typeof time !== 'number' || !Number.isFinite(time)) return ''
2882
+ const d = new Date(time)
2883
+ const p = (n) => String(n).padStart(2, '0')
2884
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
2885
+ }
2886
+
2887
+ // 毫秒 → 人读耗时:<1s 显示 ms,<1m 显示 s(1 位小数),否则 m+s;无值/0 返回空串。
2888
+ function formatDurationMs(ms) {
2889
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) return ''
2890
+ if (ms < 1000) return `${Math.round(ms)}ms`
2891
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
2892
+ const m = Math.floor(ms / 60000)
2893
+ const s = Math.round((ms % 60000) / 1000)
2894
+ return s > 0 ? `${m}m${s}s` : `${m}m`
2895
+ }
2896
+
2897
+ // 闸输入/输出值 → 单行 JSON(等宽次级色展示);不可序列化回退 String。
2898
+ function formatGateValue(value) {
2899
+ if (value === undefined || value === null) return ''
2900
+ try {
2901
+ const s = JSON.stringify(value)
2902
+ return s === undefined ? '' : s
2903
+ } catch {
2904
+ return String(value)
2905
+ }
2906
+ }
2907
+
2908
+ // 闸名中文标签(仅中文,供汇总带紧凑展示);未知/空名原样返回。
2909
+ function gateLabelZh(name) {
2910
+ return GATE_LABELS[name] || name
2911
+ }
2912
+
2913
+ // 失败闸修复方向;未知闸给通用提示。
2914
+ function gateFixHint(name) {
2915
+ return GATE_FIX_HINTS[name] || ZH.gateFixUnknown
2916
+ }
2917
+
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
2925
+ }
2926
+
2927
+
2928
+ // 记录档位:effective.tokenTier 优先,回退 result.fields.tokenTier(旧记录)。
2929
+ function recordTier(record) {
2930
+ const trace = record && record.trace
2931
+ const result = record && record.result
2932
+ const eff = trace && trace.effective
2933
+ if (eff && typeof eff.tokenTier === 'string' && eff.tokenTier !== '') return eff.tokenTier
2934
+ if (result && result.fields && typeof result.fields.tokenTier === 'string') return result.fields.tokenTier
2935
+ return ''
2936
+ }
2937
+
2938
+ // 记录模型:effective.model 优先,回退 result.fields.model(旧记录口径,同档位读取)。
2939
+ function recordModel(record) {
2940
+ const trace = record && record.trace
2941
+ const result = record && record.result
2942
+ const eff = trace && trace.effective
2943
+ if (eff && typeof eff.model === 'string' && eff.model !== '') return eff.model
2944
+ if (result && result.fields && typeof result.fields.model === 'string') return result.fields.model
2945
+ return ''
2946
+ }
2947
+ function recordUsage(record) {
2948
+ const trace = record && record.trace
2949
+ const result = record && record.result
2950
+ if (trace && trace.settled) {
2951
+ const usage = parseChildUsage(trace.settled.childUsage)
2952
+ if (usage !== null) return usage
2953
+ }
2954
+ if (result && result.fields && result.fields.childUsage !== undefined && result.fields.childUsage !== '') {
2955
+ return parseChildUsage(result.fields.childUsage)
2956
+ }
2957
+ return null
2958
+ }
2959
+
2960
+ // 逐次调用明细:读 trace.settled.calls({calls:[{五段}], truncated, totalCalls}),
2961
+ // 逐字段 typeof 守卫、坏条目跳过;无 calls 或全坏返回 null(整区不渲染)。calls 仅
2962
+ // 台账展开区专用,不进 render 行 / 模型可见面。
2963
+ function recordCalls(record) {
2964
+ const settled = record && record.trace && record.trace.settled
2965
+ const raw = settled && settled.calls
2966
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
2967
+ if (!Array.isArray(raw.calls) || raw.calls.length === 0) return null
2968
+ const keys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
2969
+ const calls = []
2970
+ for (const item of raw.calls) {
2971
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
2972
+ const call = {}
2973
+ for (const key of keys) {
2974
+ const value = item[key]
2975
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) call[key] = value
2976
+ }
2977
+ calls.push(call)
2978
+ }
2979
+ if (calls.length === 0) return null
2980
+ const totalCalls = typeof raw.totalCalls === 'number' && Number.isFinite(raw.totalCalls) && raw.totalCalls >= 0
2981
+ ? raw.totalCalls
2982
+ : calls.length
2983
+ return { calls, truncated: raw.truncated === true, totalCalls }
2984
+ }
2985
+
2986
+ // 记录结束原因:结算 stopReason 优先,回退 result.fields.stopReason;映射中文。
2987
+ function recordStopZh(record) {
2988
+ const settled = record && record.trace && record.trace.settled
2989
+ const result = record && record.result
2990
+ const raw = settled && typeof settled.stopReason === 'string' ? settled.stopReason
2991
+ : (result && result.fields && typeof result.fields.stopReason === 'string' ? result.fields.stopReason : '')
2992
+ return raw === '' ? '' : (ZH.stopReasonZh[raw] || raw)
2993
+ }
2994
+
2995
+ // 记录行稳定 key:callId 优先,其次时间戳,最后下标兜底。
2996
+ function ledgerKey(record, index) {
2997
+ if (record.callId !== '') return record.callId
2998
+ if (record.time !== null) return `ledger-${record.time}-${index}`
2999
+ return `ledger-${index}`
3000
+ }
3001
+
3002
+ // ── 台账渲染 helpers(纯元素组装)──────────────────────────────────────────
3003
+
3004
+ // 汇总带单条统计段:label 次级色 + 值加粗,段间「·」分隔。
3005
+ function summarySegments(el, pairs) {
3006
+ const out = []
3007
+ pairs.forEach((pair, i) => {
3008
+ if (i > 0) out.push(el('span', { key: `sep${i}`, className: 'sap-ledgerSep' }, ' · '))
3009
+ out.push(el('span', { key: `seg${i}`, className: 'sap-ledgerStatSeg' }, pair[0], ' ', el('span', { className: 'sap-ledgerStat' }, pair[1])))
3010
+ })
3011
+ return out
3012
+ }
3013
+
3014
+ // 汇总带:派发/成功/拒绝/token/耗时 + 闸拒绝分布 + 按方案(中文名双表达)+ 按模型/按档位。
3015
+ function buildLedgerSummary(el, summary, profileNames) {
3016
+ const elapsed = formatDurationMs(summary.totalElapsedMs)
3017
+ const pairs = [
3018
+ [ZH.ledgerDispatch, `${summary.dispatchCount} ${ZH.ledgerTimes}`],
3019
+ [ZH.ledgerSuccess, String(summary.successCount)],
3020
+ [ZH.ledgerReject, String(summary.rejectCount)],
3021
+ [ZH.ledgerTotalTokens, `≈${summary.totalTokens}(${ZH.cacheHit} ${summary.cacheHitTokens})`]
3022
+ ]
3023
+ if (elapsed !== '') pairs.push([ZH.ledgerTotalElapsed, elapsed])
3024
+ const gateEntries = ['whitelist', 'cost', 'intersection', 'approval', 'other']
3025
+ .filter((name) => summary.gateRejectCounts[name] > 0)
3026
+ .map((name) => `${gateLabelZh(name)} ${summary.gateRejectCounts[name]}`)
3027
+ const profileText = summary.byProfile.map((item) => `${profileDisplay(item.profile, profileNames)} ×${item.count}`).join(' · ')
3028
+ // 模型原文显示;继承桶(空串)与 'inherit'/'(parent)'/以 '(' 开头的标记一律归一为「继承父会话」。
3029
+ const modelText = summary.byModel.map((item) => {
3030
+ const display = item.model === 'inherit' || item.model.startsWith('(') ? ZH.inherit : rawOrInherit(item.model)
3031
+ return `${display} ×${item.count}`
3032
+ }).join(' · ')
3033
+ const tierText = summary.byTier.map((item) => `${ZH.tierZh[item.tier] || item.tier} ×${item.count}`).join(' · ')
3034
+ return el('div', { className: 'sap-ledgerSummary' },
3035
+ el('div', { className: 'sap-ledgerSummaryTitle' }, ZH.ledgerSummary),
3036
+ el('div', { className: 'sap-ledgerSummaryLine' }, summarySegments(el, pairs)),
3037
+ gateEntries.length > 0
3038
+ ? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerGateRejectDist}:${gateEntries.join(' · ')}`)
3039
+ : null,
3040
+ profileText !== ''
3041
+ ? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByProfile}:${profileText}`)
3042
+ : null,
3043
+ modelText !== ''
3044
+ ? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByModel}:${modelText}`)
3045
+ : null,
3046
+ tierText !== ''
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)))
3060
+ : null
3061
+ )
3062
+ }
3063
+
3064
+ // 空态:暂无派发记录 + 一句引导。
3065
+ function buildEmptyState(el) {
3066
+ return el('div', { className: 'sap-ledgerEmpty' },
3067
+ el('p', { className: 'sap-ledgerEmptyTitle' }, ZH.ledgerEmpty),
3068
+ el('p', { className: 'sap-intro' }, ZH.ledgerEmptyHint)
3069
+ )
3070
+ }
3071
+
3072
+ // 「加载更早」按钮:hasMore 为真且未在补载时可用;点击逐页补载更早记录。
3073
+ function buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder) {
3074
+ return el('button', {
3075
+ type: 'button',
3076
+ className: 'sap-ledgerLoadOlder',
3077
+ disabled: !hasMore || loadingOlder,
3078
+ onClick: () => { loadOlder() }
3079
+ }, loadingOlder ? ZH.ledgerLoadingOlder : ZH.ledgerLoadOlder)
3080
+ }
3081
+
3082
+ // 台账顶部说明行:定位说明(恒显)+ 窗口提示(hasMore 时显示已加载条数)。
3083
+ function buildLedgerTopNote(el, hasMore, count) {
3084
+ return el('div', null,
3085
+ el('div', { className: 'sap-ledgerScope' }, ZH.ledgerScopeNote),
3086
+ hasMore
3087
+ ? el('div', { className: 'sap-ledgerWindowNote' }, ZH.ledgerWindowNote.replace('{n}', String(count)))
3088
+ : null
3089
+ )
3090
+ }
3091
+
3092
+ // 标量 → 直接文本(字符串不带引号,null/undefined 空串)。
3093
+ function formatGateScalar(v) {
3094
+ if (v === undefined || v === null) return ''
3095
+ if (typeof v === 'string') return v
3096
+ return String(v)
3097
+ }
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
+
3105
+ // 对象/数组 → 逐字段键值行数组(depth 上限 2 防深层爆炸);标量 → 单行文本。
3106
+ // 对象逐键「中文标签(键名): 值」;嵌套容器递归 1 层并包缩进块(sap-ledgerGateNested,
3107
+ // 左侧竖线视觉分层,profiles_snapshot 等嵌套结构可读);数组前 5 项逐项展示,
3108
+ // 超出补「…共 N 项」。未知键回退原样(不崩溃)。
3109
+ function gateValueRows(el, value, depth = 0) {
3110
+ if (value === null || typeof value !== 'object') {
3111
+ return [el('div', { className: 'sap-ledgerGateKV' },
3112
+ el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(value)))]
3113
+ }
3114
+ if (Array.isArray(value)) {
3115
+ const rows = []
3116
+ for (const item of value.slice(0, 5)) {
3117
+ if (item !== null && typeof item === 'object' && depth < 2) {
3118
+ rows.push(el('div', { className: 'sap-ledgerGateNested' }, ...gateValueRows(el, item, depth + 1)))
3119
+ } else {
3120
+ rows.push(el('div', { className: 'sap-ledgerGateKV' },
3121
+ el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(item))))
3122
+ }
3123
+ }
3124
+ if (value.length > 5) {
3125
+ rows.push(el('div', { className: 'sap-ledgerGateKV' },
3126
+ el('span', { className: 'sap-ledgerGateValue' }, `…共 ${value.length} 项`)))
3127
+ }
3128
+ return rows
3129
+ }
3130
+ const rows = []
3131
+ for (const [key, v] of Object.entries(value)) {
3132
+ const label = FIELD_ZH[key] !== undefined ? `${FIELD_ZH[key]}(${key})` : key
3133
+ if (v !== null && typeof v === 'object' && depth < 2) {
3134
+ rows.push(el('div', { className: 'sap-ledgerGateKV' },
3135
+ el('span', { className: 'sap-ledgerGateKey' }, label), ':'))
3136
+ rows.push(el('div', { className: 'sap-ledgerGateNested' }, ...gateValueRows(el, v, depth + 1)))
3137
+ } else {
3138
+ rows.push(el('div', { className: 'sap-ledgerGateKV' },
3139
+ el('span', { className: 'sap-ledgerGateKey' }, label), ':',
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))))
3143
+ }
3144
+ }
3145
+ return rows
3146
+ }
3147
+
3148
+ // 子块卡片:标题 + 字段级行(空对象/空数组显示 `{}`/`[]` 单行,不空白)。
3149
+ // 供步骤①(父会话上下文/请求值)与各闸(输入/输出)共用——每段独立卡片、
3150
+ // 独立标题,与闸头/拒绝信息视觉分层(不再平铺扎堆)。
3151
+ function buildSubBlock(el, title, value) {
3152
+ const rows = value !== null && typeof value === 'object' && Object.keys(value).length > 0
3153
+ ? gateValueRows(el, value, 0)
3154
+ : [el('div', { className: 'sap-ledgerGateKV' },
3155
+ el('span', { className: 'sap-ledgerGateValue' }, formatGateValue(value)))]
3156
+ return el('div', { className: 'sap-ledgerSubBlock' },
3157
+ el('div', { className: 'sap-ledgerSubBlockTitle' }, title),
3158
+ el('div', { className: 'sap-ledgerSubBlockBody' }, rows)
3159
+ )
3160
+ }
3161
+
3162
+ // 区块视图切换按钮:字段视图 ↔ 原始 JSON(点击整体切换,非追加)。active 为真时
3163
+ // 显示「字段视图」提示可切回。
3164
+ function buildViewToggle(el, active, onToggle) {
3165
+ return el('button', {
3166
+ type: 'button',
3167
+ className: 'sap-ledgerRawJson',
3168
+ onClick: onToggle,
3169
+ 'aria-expanded': active
3170
+ }, active ? ZH.ledgerFieldView : ZH.ledgerRawJson)
3171
+ }
3172
+
3173
+ // JSON 视图内容:kv 各段原始 JSON 逐块展示(等宽可换行),与字段视图互斥。
3174
+ function buildJsonBody(el, kv) {
3175
+ return el('div', { className: 'sap-ledgerJsonBody' },
3176
+ kv.map(({ label, value }) => el('div', { key: label, className: 'sap-ledgerJsonBlock' },
3177
+ el('div', { className: 'sap-ledgerGateKey' }, label),
3178
+ el('div', { className: 'sap-ledgerGateValue' }, formatGateValue(value))
3179
+ ))
3180
+ )
3181
+ }
3182
+
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
3403
+ const startedText = formatClockTime(trace.startedAt)
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 : ''
3409
+ const kv = [
3410
+ { label: ZH.ledgerParentContext, value: trace.parentContext },
3411
+ { label: ZH.ledgerRequested, value: trace.requested }
3412
+ ]
3413
+ return el('div', { key: 'block-start', className: 'sap-ledgerStep' },
3414
+ el('div', { className: 'sap-ledgerStepHead' },
3415
+ el('span', null, head),
3416
+ buildViewToggle(el, jsonMode, () => toggleJson('start'))
3417
+ ),
3418
+ jsonMode
3419
+ ? buildJsonBody(el, kv)
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' }, '来源:系统(父会话快照)'),
3430
+ buildSubBlock(el, ZH.ledgerParentContext, trace.parentContext),
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
3436
+ )
3437
+ )
3438
+ }
3439
+
3440
+ // 生效工具数:continuable 交集实得数 → effective.effectiveToolCount → 未知 null。
3441
+ function effectiveToolsCountOf(record) {
3442
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
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
3448
+ }
3449
+
3450
+ // 证据层 ② 决策 chips 行(中文对照,忽略项键名中文化)。
3451
+ function buildEffectiveChips(el, chip, record, profileNames) {
3452
+ const eff = record.trace.effective || {}
3453
+ if (Object.keys(eff).length === 0) return null
3454
+ const requested = record.trace.requested || {}
3455
+ const chips = []
3456
+ chips.push(chip('profile', ZH.chipProfile, profileDisplay(eff.profile, profileNames)))
3457
+ chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: !eff.preset || eff.preset === 'inherit' }))
3458
+ const p = rawOrInherit(eff.provider)
3459
+ const m = rawOrInherit(eff.model)
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)' }))
3462
+ const tier = typeof eff.tokenTier === 'string' && eff.tokenTier !== '' ? eff.tokenTier : ''
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)))
3466
+ const ignored = Array.isArray(eff.ignored) ? eff.ignored : []
3467
+ for (const key of ignored) {
3468
+ const reqVal = typeof requested[key] === 'string' && requested[key] !== '' ? requested[key] : ''
3469
+ chips.push(el('span', {
3470
+ key: 'ignored-' + key,
3471
+ className: 'sap-chip sap-chipWarn',
3472
+ title: reqVal !== '' ? ZH.ignoredChipTitle.replace('{value}', reqVal) : ZH.ignoredChipTitleEmpty
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
+ )
3543
+ }
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
+ )
3650
+ )
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
+ }
3660
+
3661
+
3662
+ // 证据层 ④ 结算:结束原因(中文)· 耗时 · tokens · 输出摘要 · 成本分解。
3663
+ function buildSettleBlock(el, record, jsonView, toggleJson, openChild) {
3664
+ const settled = record.trace && record.trace.settled
3665
+ if (!settled) return null
3666
+ const jsonMode = jsonView.settle === true
3667
+ const kv = [{ label: ZH.ledgerSettledStep, value: settled }]
3668
+ const numbers = recordNumbers(record)
3669
+ const stopZh = recordStopZh(record)
3670
+ const parts = []
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
+ )
3697
+ )
3698
+ }
3699
+ // 逐次调用明细折叠区(默认收起):trace.settled.calls 逐条五段紧凑表格(序号 + 五列,
3700
+ // 缺失段显示 —)+ 末行合计(∑已存 calls,未截断时与 childUsage 五段分解对账一致)+
3701
+ // 截断提示。无 calls 数据时整区不渲染。
3702
+ function buildCallDetailSection(el, record) {
3703
+ const data = recordCalls(record)
3704
+ if (data === null) return null
3705
+ const keys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
3706
+ const labels = [ZH.usageInput, ZH.usageOutput, ZH.usageCacheRead, ZH.usageCacheWrite, ZH.usageReasoning]
3707
+ const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0 }
3708
+ const rows = data.calls.map((call, i) => {
3709
+ const cells = keys.map((key) => {
3710
+ const value = call[key]
3711
+ if (typeof value === 'number') totals[key] += value
3712
+ return el('td', { key }, typeof value === 'number' ? String(value) : '—')
3713
+ })
3714
+ return el('tr', { key: i },
3715
+ el('td', { className: 'sap-ledgerCallsIndex' }, String(i + 1)),
3716
+ ...cells
3717
+ )
3718
+ })
3719
+ const totalCells = keys.map((key) => el('td', { key, className: 'sap-ledgerCallsTotal' }, String(totals[key])))
3720
+ const note = data.truncated
3721
+ ? el('div', { className: 'sap-ledgerCallsNote' },
3722
+ ZH.ledgerCallsTruncated.replace('{shown}', String(data.calls.length)).replace('{total}', String(data.totalCalls)))
3723
+ : null
3724
+ return el('details', { className: 'sap-customized' },
3725
+ el('summary', { className: 'sap-customizedSummary' }, ZH.ledgerCallDetail),
3726
+ el('div', { className: 'sap-customizedBody' },
3727
+ el('table', { className: 'sap-ledgerCallsTable' },
3728
+ el('thead', null,
3729
+ el('tr', null,
3730
+ el('th', { className: 'sap-ledgerCallsIndex' }, ZH.ledgerCallsIndex),
3731
+ keys.map((key, i) => el('th', { key }, labels[i]))
3732
+ )
3733
+ ),
3734
+ el('tbody', null,
3735
+ rows,
3736
+ el('tr', { key: 'total' },
3737
+ el('td', { className: 'sap-ledgerCallsIndex sap-ledgerCallsTotal' }, ZH.ledgerCallsTotal),
3738
+ ...totalCells
3739
+ )
3740
+ )
3741
+ ),
3742
+ note
3743
+ )
3744
+ )
3745
+ }
3746
+
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)) : ''
3764
+ const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
3765
+ const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
3766
+ const mode = typeof execution.mode === 'string' ? execution.mode : ''
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
+ )
3791
+ )
3792
+ }
3793
+
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))
3830
+ }
3831
+ return el('div', null,
3832
+ el('div', { className: 'sap-ledgerVsHead' }, ZH.ledgerStallThresholdNote),
3833
+ el('div', { className: 'sap-ledgerEventLine' }, items)
3834
+ )
3835
+ }
3836
+
3837
+ // 证据层装配:① 发起 → ② 决策 → ③ 执行 → ④ 结算(拒绝场景 ③④ 自动省略)。
3838
+ function buildEvidenceArea(el, chip, record, profileNames, toolZh, proc, jsonView, toggleJson, openChild, copiedTarget, onCopyTarget) {
3839
+ return el('div', { className: 'sap-ledgerExpand' },
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)
3844
+ )
3845
+ }
3846
+
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
+
3891
+
3892
+ // ── 台账 maker(React 依赖经参数注入)───────────────────────────────────────
3893
+
3894
+ // 单条记录三层:折叠态行 → 结论层(点行展开)→ 证据层(「查看完整因果链」再展开)。
3895
+ function makeLedgerRow(el, chip, useState, openChild) {
3896
+ function LedgerRow({ record, profileNames, childNodesOf, toolZh }) {
3897
+ const [open, setOpen] = useState(false)
3898
+ const [evidenceOpen, setEvidenceOpen] = useState(false)
3899
+ // 修复方向复制反馈:点击「复制 <目标>」后按钮短暂显示「已复制」。
3900
+ const [copiedTarget, setCopiedTarget] = useState('')
3901
+ // 区块级视图切换:jsonView[key] 为 true 时该区块显示原始 JSON
3902
+ // (key='start'/'decide'/'execute'/'settle')。各区块独立切换。
3903
+ const [jsonView, setJsonView] = useState({})
3904
+ const toggleJson = (key) => setJsonView((prev) => ({ ...prev, [key]: !(prev[key] === true) }))
3905
+ const isLegacy = record.legacy === true
3906
+ const rejected = recordIsRejected(record)
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
3926
+ )
3927
+ }
3928
+ return LedgerRow
3929
+ }
3930
+
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) {
3990
+ const LedgerRow = makeLedgerRow(el, chip, useState, openChild)
3991
+ function DispatchLedgerView({ useSession, sessionId, loadOlder = () => {} }) {
3992
+ const nodes = useSession((s) => s?.chat?.legacy?.nodes)
3993
+ const hasMore = useSession((s) => s?.hasMore)
3994
+ const loadingOlder = useSession((s) => s?.loadingOlder)
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)
4009
+ const summary = records.length === 0 ? null : computeLedgerSummary(records)
4010
+ return el('div', { className: 'sap-ledger' },
4011
+ buildLedgerTopNote(el, hasMore, records.length),
4012
+ buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder),
4013
+ summary === null ? null : buildLedgerSummary(el, summary, data.profileNames),
4014
+ buildLedgerRows(el, records, LedgerRow, data.profileNames, childNodesOf, data.toolZh)
4015
+ )
4016
+ }
4017
+ return DispatchLedgerView
4018
+ }
986
4019
  // ── plugin 装配 ─────────────────────────────────────────────────────────────
987
4020
 
988
4021
  window.__ModuleLoader__.load({
989
4022
  id: 'dsh-subagent-profile',
990
4023
  factory: (require) => {
991
- const React = require('react')
992
- const { useEffect, useState, useCallback } = React
4024
+ const React = require('react'); const { useEffect, useState, useCallback } = React
993
4025
  const el = React.createElement
994
4026
 
995
- /** Required cordis services: the slot registry only. */
996
- const inject = ['slots']
4027
+ // slots:槽注册;sessions:子会话跳转(openSubagent)。两者皆 client 核心服务。
4028
+ const inject = ['slots', 'sessions']
997
4029
 
998
4030
  const chip = makeChip(el)
999
- const DispatchToolview = makeDispatchToolview(el, chip, useState)
1000
- const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
4031
+ const DispatchToolview = makeDispatchToolview(el, chip, useState, useEffect)
4032
+ const ReminderOverlayBadge = makeReminderOverlayBadge(el, useState, useEffect)
1001
4033
 
1002
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)
1003
4040
  ensureStyles()
1004
- ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
1005
- name: 'tool.call.toolview',
1006
- key: 'dispatch'
1007
- }, 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))
1008
4043
  ctx.slots.inject('settings.section', () => ctx.slots.register({
1009
4044
  name: 'settings.section',
1010
4045
  id: 'subagent-profiles',
1011
4046
  order: 100,
1012
4047
  label: '子 Agent 方案'
1013
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))
4053
+ // 子会话跳转回调:经 sessions 服务按 SubagentAddress 打开子会话。
4054
+ const DispatchLedgerView = makeDispatchLedgerView(el, chip, useState, useEffect, openChild, () => ctx.sessions)
4055
+ ctx.slots.inject('conversation.view', () => ctx.slots.register({
4056
+ name: 'conversation.view',
4057
+ id: 'dispatch-ledger',
4058
+ order: 20,
4059
+ label: ZH.ledgerTab,
4060
+ inject: (sessionId) => {
4061
+ const session = ctx.sessions.binding(sessionId)?.session
4062
+ if (session === void 0) throw new Error(`dsh-subagent-profile: 会话 "${sessionId}" 不可用(可能已归档或未加载)`)
4063
+ return { loadOlder: async () => { await session.loadOlder() } }
4064
+ }
4065
+ }, DispatchLedgerView))
1014
4066
  }
1015
4067
 
1016
4068
  return { apply, inject }