dsh-subagent-profile 0.3.3 → 0.4.0

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 (42) hide show
  1. package/README.md +49 -20
  2. package/README.zh.md +102 -73
  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 +153 -75
  7. package/lib/client.js +3081 -444
  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/cost-evidence.mjs +145 -0
  12. package/lib/core/cost-guard.mjs +1 -1
  13. package/lib/core/decision-trace.mjs +11 -31
  14. package/lib/core/delegation.mjs +11 -7
  15. package/lib/core/dispatch-gates.mjs +12 -5
  16. package/lib/core/dispatch-guard.mjs +6 -0
  17. package/lib/core/dispatch-schema.mjs +10 -5
  18. package/lib/core/dispatch-tool.mjs +52 -45
  19. package/lib/core/draft-gates.mjs +45 -0
  20. package/lib/core/drafts-store.mjs +45 -0
  21. package/lib/core/evolution-advice.mjs +323 -0
  22. package/lib/core/evolution-assets.mjs +167 -0
  23. package/lib/core/evolution-draft.mjs +312 -0
  24. package/lib/core/evolution-engine.mjs +269 -0
  25. package/lib/core/evolution-generate.mjs +196 -0
  26. package/lib/core/evolution-ledger.mjs +149 -33
  27. package/lib/core/evolution-persistence.mjs +213 -0
  28. package/lib/core/evolution-renewal.mjs +64 -0
  29. package/lib/core/evolution-routes.mjs +109 -0
  30. package/lib/core/evolution-summary.mjs +46 -223
  31. package/lib/core/http-helpers.mjs +35 -0
  32. package/lib/core/http-routes.mjs +169 -115
  33. package/lib/core/presets-sync.mjs +254 -256
  34. package/lib/core/prices.mjs +46 -0
  35. package/lib/core/profile-directory.mjs +139 -0
  36. package/lib/core/profile-provider.mjs +5 -3
  37. package/lib/core/profiles-store.mjs +24 -6
  38. package/lib/core/reminder-store.mjs +172 -0
  39. package/lib/core/session-read.mjs +17 -0
  40. package/lib/core/shims.mjs +13 -2
  41. package/package.json +82 -82
  42. package/presets/orchestrator/agent.cordis.yml +243 -243
package/lib/client.js CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * 设置页视觉语言对齐官方「模型」设置节(dsh-client-ui-settings-models):同一套
11
11
  * 设计 token,标题 + 引言,每行一张带边框卡片(名称/描述/元信息分层布局),
12
- * 虚线「+ 新增」按钮展开内联表单卡片。下拉选项(preset / provider / model /
12
+ * 虚线「+ 新增」按钮展开就地表单卡片。下拉选项(preset / provider / model /
13
13
  * reasoning effort)由宿主 `/options/summary`(模型 + 预设,设置页打开时)、
14
14
  * `/options/efforts`(按模型,选择模型后懒加载)与 `/options/tools`(工具目录,
15
15
  * 限制面板打开时懒加载)路由提供,数据来自实时 presets roster 与 llm 目录。
@@ -22,7 +22,55 @@
22
22
  // ── 纯数据:样式(tokens 对齐 shipped「模型」设置节)────────────────────────
23
23
 
24
24
  const CSS = [
25
- '.sap-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
25
+ '.sap-section{width:100%;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-label-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-costLine{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
26
74
  '.sap-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}',
27
75
  '.sap-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}',
28
76
  '.sap-notice{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}',
@@ -35,6 +83,67 @@ const CSS = [
35
83
  '.sap-rowId{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace}',
36
84
  '.sap-rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}',
37
85
  '.sap-rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}',
86
+ '.sap-adviceManageCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;flex-direction:column;gap:6px;display:flex}',
87
+ '.sap-adviceCardHead{flex-direction:column;align-items:stretch;gap:2px}',
88
+ '.sap-adviceHeadTitleRow{display:flex;align-items:center;gap:8px}',
89
+ '.sap-adviceGlobalLine{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}',
90
+ '.sap-adviceSection{flex-direction:column;display:flex}',
91
+ '.sap-adviceSection + .sap-adviceSection{margin-top:10px}',
92
+ '.sap-adviceSectionTitle{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;font-weight:700;margin:0 0 2px}',
93
+ '.sap-adviceSectionBody{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;margin:0}',
94
+ '.sap-candidateHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}',
95
+ '.sap-candidateBody{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:4px;padding-top:8px;padding-left:16px;flex-direction:column;gap:6px;display:flex}',
96
+ '.sap-candidateChanges{flex-direction:column;gap:4px;margin:0;padding:0;list-style:none;display:flex}',
97
+ '.sap-candidateChange{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
98
+ '.sap-adviceDetailBlock{flex-direction:column;gap:4px;display:flex}',
99
+ '.sap-adviceDetailToggle{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex;align-self:flex-start}',
100
+ '.sap-adviceDetailToggle:hover{color:var(--dsw-alias-label-primary)}',
101
+ '.sap-adviceDetailArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
102
+ '.sap-adviceDetailToggle[aria-expanded="true"] .sap-adviceDetailArrow{transform:rotate(90deg)}',
103
+ '.sap-adviceDetailTable{width:100%;border-collapse:collapse;font-size:12px;line-height:18px}',
104
+ '.sap-adviceDetailTable th{text-align:left;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:3px 8px 3px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}',
105
+ '.sap-adviceDetailTable td{text-align:left;padding:4px 8px 4px 0;border-bottom:1px dotted var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);vertical-align:top}',
106
+ '.sap-adviceDetailTime{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
107
+ '.sap-adviceDetailSession{color:var(--dsw-alias-label-primary)}',
108
+ '.sap-adviceDetailSessionId{color:var(--dsw-alias-label-tertiary);font-size:11px;margin-left:4px}',
109
+ '.sap-adviceDetailChild{font-family:ui-monospace,Consolas,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
110
+ '.sap-warn-x{color:var(--dsw-alias-state-error-primary)}',
111
+ '.sap-candidates{flex-direction:column;gap:6px;margin-top:14px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);display:flex}',
112
+ '.sap-candidatesTitle{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;font-weight:700;margin:0}',
113
+ '.sap-candidatesTitle .sap-candidatesNotApplied{color:var(--dsw-alias-label-tertiary);font-weight:400}',
114
+ '.sap-candidatesCount{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:2px 0 0}',
115
+ '.sap-candidate{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1)}',
116
+ '.sap-candidateSelected{border-color:var(--dsw-alias-label-primary)}',
117
+ '.sap-candidateRow{box-sizing:border-box;width:100%;border:0;background:0 0;padding:8px 10px;margin:0;font:inherit;text-align:left;cursor:pointer;align-items:center;gap:8px;display:flex}',
118
+ '.sap-candidateRadio{box-sizing:border-box;width:14px;height:14px;border:1.5px solid var(--dsw-alias-border-l2);border-radius:7px;flex:none}',
119
+ '.sap-candidateSelected .sap-candidateRadio{border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-label-primary);box-shadow:inset 0 0 0 3px var(--dsw-alias-bg-layer-1)}',
120
+ '.sap-candidateName{font-weight:600;flex:1;min-width:0;padding-right:8px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;overflow-wrap:anywhere}',
121
+ '.sap-candidateValidity{margin-left:auto;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);border-radius:3px;padding:1px 6px;white-space:nowrap;flex:none}',
122
+ '.sap-candidateArrow{margin-left:6px;color:var(--dsw-alias-label-tertiary);font-size:9px;line-height:1;flex:none;transition:transform .15s}',
123
+ '.sap-candidateOpen .sap-candidateArrow{transform:rotate(90deg)}',
124
+ '.sap-candidateGroup{flex-direction:column;gap:4px;display:flex}',
125
+ '.sap-candidateGroup + .sap-candidateGroup{margin-top:6px}',
126
+ '.sap-candidateGroupTitle{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-weight:700;margin:0}',
127
+ '.sap-candidateSafetyList{margin:0 0 0 16px;padding:0;list-style:none;flex-direction:column;gap:2px;display:flex}',
128
+ '.sap-candidateSafetyItem{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
129
+ '.sap-candidateSafetyToggle{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);align-items:center;gap:4px;font-size:12px;line-height:18px;display:inline-flex;align-self:flex-start;font-weight:600}',
130
+ '.sap-candidateSafetyToggle:hover{color:var(--dsw-alias-label-primary)}',
131
+ '.sap-candidateSafetyArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
132
+ '.sap-candidateSafetyToggleOpen .sap-candidateSafetyArrow{transform:rotate(90deg)}',
133
+ '.sap-adviceFoot{align-items:center;gap:10px;flex-wrap:wrap;display:flex;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px;margin-top:2px}',
134
+ '.sap-adviceFootNotes{flex-direction:column;gap:2px;display:flex;min-width:0}',
135
+ '.sap-adviceFootNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}',
136
+ '.sap-candidateSettingsRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
137
+ '.sap-candidateSettingsRow b{font-weight:600;color:var(--dsw-alias-label-primary)}',
138
+ '.sap-candidateSettingsEdit{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-decoration:underline;text-underline-offset:3px}',
139
+ '.sap-candidateSettingsEdit:hover{color:var(--dsw-alias-label-primary)}',
140
+ '.sap-candidateSettingsPanel{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:12px 14px;margin-top:8px;flex-direction:column;gap:10px;display:flex;background:var(--dsw-alias-bg-layer-1)}',
141
+ '.sap-candidateSettingsField{flex-direction:column;gap:6px;display:flex}',
142
+ '.sap-candidateSettingsOption{align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);display:inline-flex;cursor:pointer}',
143
+ '.sap-candidateSettingsOptions{flex-wrap:wrap;gap:4px 14px;display:flex}',
144
+ '.sap-candidateSettingsCustom{align-items:center;gap:8px;display:flex}',
145
+ '.sap-candidateSettingsCustom .sap-input{max-width:120px}',
146
+ '.sap-candidateSettingsActions{align-items:center;gap:10px;display:flex}',
38
147
  '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
39
148
  '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
40
149
  '.sap-versionWarn{background:var(--dsw-alias-state-warn-tertiary);border:1px solid var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);border-radius:8px;flex-direction:column;gap:4px;padding:10px 12px;font-size:12px;line-height:18px;margin:0;display:flex}',
@@ -43,6 +152,7 @@ const CSS = [
43
152
  '.sap-chipLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
44
153
  '.sap-chipValue{color:var(--dsw-alias-label-primary)}',
45
154
  '.sap-chipValueDim{color:var(--dsw-alias-label-tertiary)}',
155
+ '.sap-chipDot{color:var(--dsw-alias-label-tertiary)}',
46
156
  '.sap-chipWarn{background:var(--dsw-alias-state-warn-tertiary);border-color:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font-weight:500}',
47
157
  '.sap-chipSuccess{color:var(--dsw-alias-state-success-primary);font-weight:500}',
48
158
  '.sap-chipDisabled{color:var(--dsw-alias-state-warn-label);font-weight:500}',
@@ -67,6 +177,10 @@ const CSS = [
67
177
  '.sap-customized[open]>.sap-customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}',
68
178
  '.sap-customizedSummary:hover{color:var(--dsw-alias-label-primary)}',
69
179
  '.sap-customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}',
180
+ '.sap-customizedArrowEnd .sap-customizedSummary:before{display:none}',
181
+ '.sap-customizedEndArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
182
+ '.sap-customizedArrowEnd[open] .sap-customizedEndArrow{transform:rotate(90deg)}',
183
+ '.sap-reminderHandled{border-top:none;padding-top:8px}',
70
184
  '.sap-editorActions{justify-content:flex-end;gap:8px;display:flex}',
71
185
  '.sap-primaryButton,.sap-secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}',
72
186
  '.sap-primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}',
@@ -83,6 +197,15 @@ const CSS = [
83
197
  '.sap-switch input:checked{background:var(--dsw-alias-brand-primary)}',
84
198
  '.sap-switch input:checked::before{left:18px}',
85
199
  '.sap-toolview{flex-direction:column;align-items:flex-start;gap:6px;padding:4px 8px;display:flex}',
200
+ '.sap-toolMeta{flex-wrap:wrap;gap:4px 14px;display:flex}',
201
+ '.sap-draftCard{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-direction:column;gap:8px;padding:10px 12px;display:flex}',
202
+ '.sap-drafts{flex-direction:column;gap:8px;display:flex}',
203
+ '.sap-costSection{flex-direction:column;gap:8px;display:flex}',
204
+ '.sap-infoLine{justify-content:space-between;gap:12px;display:flex}',
205
+ '.sap-infoLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;flex:none}',
206
+ '.sap-infoValue{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;text-align:right}',
207
+ '.sap-draftTitle{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
208
+ '.sap-toolMetaItem{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}',
86
209
  '.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
87
210
  '.sap-toolSummary{align-items:center;gap:6px;flex-wrap:wrap;display:flex}',
88
211
  '.sap-detailToggle{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);background:0 0;border-radius:10px;font:inherit;font-size:11px;line-height:16px;height:20px;padding:0 8px;cursor:pointer;flex:none;align-items:center;display:inline-flex}',
@@ -106,6 +229,58 @@ const CSS = [
106
229
  '.sap-ledgerStat{color:var(--dsw-alias-label-primary);font-weight:600}',
107
230
  '.sap-ledgerSep{color:var(--dsw-alias-label-tertiary)}',
108
231
  '.sap-ledgerRows{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}',
232
+ '.sap-ledgerRowFail{border-left:3px solid var(--dsw-alias-state-error-secondary)}',
233
+ '.sap-ledgerStatus{font-size:12px;line-height:18px;font-weight:600;flex:none}',
234
+ '.sap-ledgerStatusOk{color:var(--dsw-alias-state-success-primary)}',
235
+ '.sap-ledgerStatusFail{color:var(--dsw-alias-state-error-primary)}',
236
+ '.sap-ledgerStatusRun{color:var(--dsw-alias-state-warn-label)}',
237
+ '.sap-ledgerCostText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;flex:none}',
238
+ '.sap-ledgerToolsText{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;flex:none}',
239
+ '@keyframes sap-ledgerPulse{0%,100%{opacity:1}50%{opacity:.35}}',
240
+ '.sap-ledgerProgress{align-items:center;gap:6px;display:flex;flex-wrap:wrap}',
241
+ '.sap-ledgerProgressStep{align-items:center;gap:6px;display:inline-flex;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
242
+ '.sap-ledgerProgressDot{width:8px;height:8px;border-radius:50%;flex:none}',
243
+ '.sap-ledgerProgressDot-done{background:var(--dsw-alias-state-success-primary)}',
244
+ '.sap-ledgerProgressDot-run{background:var(--dsw-alias-state-warn-label);animation:sap-ledgerPulse 1.4s infinite}',
245
+ '.sap-ledgerProgressDot-todo{background:var(--dsw-alias-border-l3)}',
246
+ '.sap-ledgerProgressDot-warn{background:var(--dsw-alias-state-warn-label)}',
247
+ '.sap-ledgerProgressSep{width:14px;height:1px;background:var(--dsw-alias-border-l2);flex:none}',
248
+ '.sap-ledgerSafety{flex-wrap:wrap;gap:6px;display:flex}',
249
+ '.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)}',
250
+ '.sap-ledgerBadgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}',
251
+ '.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)}',
252
+ '.sap-ledgerBadgeWarn{color:var(--dsw-alias-state-warn-label);border-color:var(--dsw-alias-state-warn-label)}',
253
+ '.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}',
254
+ '.sap-ledgerCostCard b{color:var(--dsw-alias-label-primary);font-weight:600}',
255
+ '.sap-ledgerCostSave b{color:var(--dsw-alias-state-success-primary)}',
256
+ '.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}',
257
+ '.sap-ledgerIntervention{color:var(--dsw-alias-state-warn-label);font-size:12px;line-height:18px}',
258
+ '.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}',
259
+ '.sap-ledgerEvidenceToggle:hover{color:var(--dsw-alias-brand-primary)}',
260
+ '.sap-ledgerVsHead{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);margin:2px 0 4px}',
261
+ '.sap-ledgerVsTable{border-collapse:collapse;width:100%;font-size:12px;line-height:18px}',
262
+ '.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)}',
263
+ '.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}',
264
+ '.sap-ledgerVsTable td:first-child{color:var(--dsw-alias-label-tertiary);width:60px}',
265
+ '.sap-ledgerVsTable tr.sap-ledgerVsDiff td{background:var(--dsw-alias-state-warn-tertiary)}',
266
+ '.sap-ledgerVsTable tr.sap-ledgerVsDiff .sap-ledgerVsEff{color:var(--dsw-alias-state-warn-label);font-weight:600}',
267
+ '.sap-ledgerVsNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
268
+ '.sap-ledgerToolScroll{max-height:220px;overflow-y:auto}',
269
+ '.sap-ledgerToolTable{border-collapse:collapse;width:100%;font-size:12px;line-height:18px}',
270
+ '.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)}',
271
+ '.sap-ledgerToolTable td{padding:6px 10px 6px 0;border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}',
272
+ '.sap-ledgerToolName{font-family:monospace;font-size:11px}',
273
+ '.sap-ledgerEventLine{position:relative;padding-left:18px;margin-top:2px}',
274
+ '.sap-ledgerEventLine:before{content:"";position:absolute;left:5px;top:4px;bottom:4px;width:1px;background:var(--dsw-alias-border-l2)}',
275
+ '.sap-ledgerEvent{position:relative;padding:3px 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
276
+ '.sap-ledgerEvent:before{content:"";position:absolute;left:-17px;top:9px;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-border-l3)}',
277
+ '.sap-ledgerEvent-done:before{background:var(--dsw-alias-state-success-primary)}',
278
+ '.sap-ledgerEvent-warn{color:var(--dsw-alias-state-warn-label)}',
279
+ '.sap-ledgerEvent-warn:before{background:var(--dsw-alias-state-warn-label)}',
280
+ '.sap-ledgerEvent-int{color:var(--dsw-alias-brand-primary)}',
281
+ '.sap-ledgerEvent-int:before{background:var(--dsw-alias-brand-primary)}',
282
+ '.sap-ledgerEventTime{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;margin-right:8px}',
283
+ '.sap-ledgerEventAct{color:var(--dsw-alias-label-primary)}',
109
284
  '.sap-ledgerRow{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;display:flex}',
110
285
  '.sap-ledgerRowHead{box-sizing:border-box;width:100%;border:0;background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:10px;padding:10px 14px;font:inherit;text-align:left;flex-wrap:wrap;display:flex}',
111
286
  '.sap-ledgerRowHead:hover{background:var(--dsw-alias-interactive-bg-hover)}',
@@ -139,6 +314,13 @@ const CSS = [
139
314
  '.sap-ledgerRejectFix{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}',
140
315
  '.sap-ledgerUsage{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
141
316
  '.sap-ledgerManual{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;align-items:baseline;gap:6px;display:flex}',
317
+ '.sap-ledgerSource{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
318
+ '.sap-ledgerSourceDim{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-style:italic}',
319
+ // 次级说明小字:供安全检查卡片说明沿用(原跳变线右下角注记样式)
320
+ '.sap-cogJumpDim{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
321
+ '.sap-cogParentText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}',
322
+ '.sap-ledgerChildRow{align-items:center;gap:8px;display:flex}',
323
+ '.sap-ledgerChildCount{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
142
324
  '.sap-ledgerManualId{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-family:monospace;word-break:break-all}',
143
325
  '.sap-ledgerEmpty{flex-direction:column;gap:4px;padding:8px 2px;display:flex}',
144
326
  '.sap-ledgerEmptyTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}',
@@ -162,6 +344,37 @@ const CSS = [
162
344
  '.sap-escapeAdd{align-items:center;gap:8px;display:flex}',
163
345
  '.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}',
164
346
  '.sap-chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}',
347
+ // 六页签:横向 tab(下划线激活+加粗)+ 内容区(非激活隐藏,全部挂载保状态)
348
+ '.sap-tabs{display:flex;flex-wrap:wrap;gap:2px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}',
349
+ '.sap-tab{box-sizing:border-box;border:0;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:20px;padding:9px 14px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;user-select:none}',
350
+ '.sap-tab:hover{color:var(--dsw-alias-label-primary)}',
351
+ '.sap-tabActive{color:var(--dsw-alias-label-primary);font-weight:700;border-bottom-color:var(--dsw-alias-label-primary)}',
352
+ '.sap-zone{display:none}',
353
+ '.sap-zoneActive{display:block}',
354
+ // 建议摘要行 + 展开区(v8:卡片边界 .advice-item,无二级标题)
355
+ '.sap-adviceItem{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex-direction:column;display:flex}',
356
+ '.sap-adviceSummary{box-sizing:border-box;width:100%;border:0;background:0 0;padding:10px 14px;margin:0;font:inherit;text-align:left;cursor:pointer;flex-direction:column;align-items:stretch;gap:2px;display:flex}',
357
+ '.sap-adviceObjLine{align-items:center;gap:6px;flex-wrap:wrap;min-width:0;display:flex}',
358
+ '.sap-adviceSummaryLine{align-items:center;gap:8px;flex-wrap:wrap;min-width:0;display:flex}',
359
+ '.sap-adviceSummary:hover{background:var(--dsw-alias-interactive-bg-hover)}',
360
+ '.sap-adviceObj{color:var(--dsw-alias-label-primary);font-weight:600;font-size:13px;line-height:20px}',
361
+ '.sap-adviceSummaryText{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}',
362
+ '.sap-adviceConf{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:nowrap}',
363
+ '.sap-adviceSummaryArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
364
+ '.sap-adviceItemOpen .sap-adviceSummaryArrow{transform:rotate(90deg)}',
365
+ '.sap-adviceNoProfileTag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:3px;padding:0 5px;flex:none}',
366
+ '.sap-adviceDetail{border-top:1px dashed var(--dsw-alias-border-l2);padding:10px 14px 12px;flex-direction:column;gap:8px;display:flex}',
367
+ // 审计明细折叠表(只读展示)
368
+ '.sap-auditCollapse{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex;align-self:flex-start}',
369
+ '.sap-auditCollapse:hover{color:var(--dsw-alias-label-primary)}',
370
+ '.sap-auditArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
371
+ '.sap-auditCollapse[aria-expanded="true"] .sap-auditArrow{transform:rotate(90deg)}',
372
+ '.sap-auditTable{width:100%;border-collapse:collapse;font-size:12px;line-height:18px;margin-top:4px}',
373
+ '.sap-auditTable th{text-align:left;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:3px 8px 3px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}',
374
+ '.sap-auditTable td{text-align:left;padding:4px 8px 4px 0;border-bottom:1px dotted var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);vertical-align:top}',
375
+ '.sap-auditTime{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
376
+ '.sap-auditEvent{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
377
+ '.sap-auditObject{color:var(--dsw-alias-label-primary)}',
165
378
  ].join('\n')
166
379
 
167
380
  // ── 纯数据:dispatch 卡片面向用户固定文案 ──────────────────────────────────
@@ -169,15 +382,15 @@ const CSS = [
169
382
  // 面向用户的固定文案。注意:此表仅集中 dispatch 卡片文案;设置页文案仍为
170
383
  // 散落硬编码,属待迁移债(迁移时一次性并入此表)。
171
384
  const ZH = {
172
- inlineProfile: '内联',
173
- inherit: '继承父',
174
- effortDefault: '默认(继承父)',
385
+ inlineProfile: '未存方案',
386
+ inherit: '继承父会话',
387
+ effortDefault: '默认(继承父会话)',
175
388
  chipProfile: '方案',
176
389
  chipPreset: '预设',
177
390
  chipProvider: '提供方',
178
391
  chipModel: '模型',
179
392
  chipEffort: '推理强度',
180
- chipTier: '档位',
393
+ chipTier: '档位(估算口径)',
181
394
  chipEnabled: '已启用',
182
395
  chipDisabled: '已禁用',
183
396
  ignoredMark: '已忽略',
@@ -188,22 +401,59 @@ const ZH = {
188
401
  canContinue: '可续跑',
189
402
  toolBackground: '后台任务',
190
403
  toolForeground: '前台',
404
+ chipMode: '模式',
405
+ modeForeground: '前台',
406
+ modeBackground: '后台',
407
+ modeContinuable: 'continuable',
408
+ bannerInheritValue: '继承父会话',
409
+ bannerModePersistent: '持久',
410
+ bannerOriginTitle: '来源:由父会话派发',
411
+ // 派发链路状态机 phase 徽标
412
+ phaseInitiate: '发起',
413
+ phaseGuards: '检查通过',
414
+ phaseCreate: '创建',
415
+ phaseRun: '执行中',
416
+ phaseDone: '完成',
417
+ phaseFailed: '失败',
418
+ phaseLegacy: '已结算(旧版)',
419
+ // 提醒系统
420
+ reminderCenterTitle: '提醒中心',
421
+ reminderCenterDesc: '需要你关注的事件汇总,所有动作均有审计记录;处理过的条目折叠保存',
422
+ reminderEmpty: '暂无提醒(治理事件会在这里留痕)',
423
+ reminderAck: '标记已读',
424
+ reminderAdopt: '采纳',
425
+ reminderIgnore: '忽略',
426
+ reminderReject: '拒绝',
427
+ reminderDismiss: '关闭',
428
+ reminderGlobal: '全局',
429
+ reminderSessionLabel: '会话 ',
430
+ reminderP0Badge: '⚠ 紧急 {n}',
431
+ reminderActed: '已处理:{action}',
432
+ reminderActionZh: { adopt: '采纳', ignore: '忽略', reject: '拒绝', dismiss: '关闭' },
433
+ reminderViewChild: '查看子会话',
434
+ reminderParentLabel: '主会话',
435
+ reminderChildLabel: '子会话',
436
+ reminderTaskLabel: '任务',
437
+ reminderResultLabel: '结果',
438
+ reminderNoteLabel: '说明',
439
+ reminderNoSettlement: '未结算',
440
+ reminderNoExcerpt: '(未记录)',
191
441
  toolRunning: '运行中',
192
442
  tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
193
443
  stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
194
444
  cacheHit: '缓存命中',
195
445
  usageInput: '输入',
196
446
  usageOutput: '输出',
197
- usageCacheRead: '缓存读',
447
+ usageCacheRead: '缓存命中',
198
448
  usageCacheWrite: '缓存写',
199
449
  usageReasoning: '推理',
200
450
  persistFail: '已保存但未持久化',
201
451
  persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)',
202
452
  // 审计分级(设置页红字 + 丢失计数)
203
- auditDegraded: '审计记录写入异常(degraded):请检查磁盘',
453
+ auditDegraded: '审计记录写入异常(已降级):请检查磁盘',
204
454
  auditLost: '审计丢失计数',
205
- // 派发账本(conversation.view 会话标签页)文案
206
- ledgerTab: '派发账本',
455
+ // 派发决策台账(conversation.view 会话标签页)文案
456
+ ledgerTab: '派发决策台账',
207
457
  ledgerSummary: '本会话派发',
208
458
  ledgerDispatch: '派发',
209
459
  ledgerTimes: '次',
@@ -214,12 +464,12 @@ const ZH = {
214
464
  ledgerTotalElapsed: '总耗时',
215
465
  ledgerElapsed: '耗时',
216
466
  ledgerStopReason: '结束原因',
217
- ledgerGateRejectDist: '各闸拒绝分布',
467
+ ledgerGateRejectDist: '安全检查拒绝分布',
218
468
  ledgerByProfile: '按方案',
219
469
  ledgerByModel: '按模型',
220
- ledgerByTier: '按档位',
470
+ ledgerByTier: '按档位(估算口径)',
221
471
  ledgerEmpty: '本会话暂无派发记录',
222
- ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的派发决策轨迹。',
472
+ ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的决策轨迹。',
223
473
  ledgerLoadOlder: '加载更早',
224
474
  ledgerLoadingOlder: '加载中…',
225
475
  ledgerLegacy: '该记录产生于决策轨迹上线前,仅有结果摘要',
@@ -242,59 +492,302 @@ const ZH = {
242
492
  ledgerCallsTruncated: '仅显示前 {shown} 条,共 {total} 次调用',
243
493
  ledgerCallsIndex: '序号',
244
494
  ledgerExecutionStep: '执行信息',
245
- ledgerScopeNote: '本页仅展示本会话的派发决策轨迹;跨会话进化台账(dispatch.jsonl)与审计记录见设置页。',
495
+ cogTimelineTitle: '派发决策时间线',
496
+ cogPerceiveDecide: '① 发起',
497
+ cogDecide: '② 决策 · 生效值',
498
+ cogExecute: '③ 执行',
499
+ cogNewPerceive: '④ 结算',
500
+ // ── 三层信息架构(折叠态 / 结论层 / 证据层)文案 ──
501
+ ledgerStatusSuccess: '✅ 成功',
502
+ ledgerStatusReject: '❌ 拒绝',
503
+ ledgerStatusRunning: '🔄 执行中',
504
+ ledgerStatusAbnormal: '⚠ 异常',
505
+ ledgerCostRunning: '进行中…',
506
+ ledgerToolsPending: '未派发',
507
+ ledgerInterventionBadge: '⚠ 干预 {n} 次',
508
+ ledgerSafetyAllPass: '安全检查 ✅ 5/5 通过',
509
+ ledgerSafetyFail: '安全检查 ❌ 未通过({gate})',
510
+ ledgerSafetyBadge: '安全检查',
511
+ ledgerGateBadgeZh: { whitelist: '白名单', cost: '成本', intersection: '交集非空', approval: '审批不豁免', budget: '预算' },
512
+ ledgerProgressZh: { start: '发起', decide: '决策', run: '执行', settle: '结算', stop: '终止' },
513
+ ledgerProgressStepZh: { start: '① 发起', decide: '② 决策', run: '③ 执行', settle: '④ 结算' },
514
+ ledgerCostCardScope: '本会话口径',
515
+ ledgerCostCardCounter: '若继承父会话',
516
+ ledgerCostCardSave: '省',
517
+ ledgerEvidenceToggle: '查看完整因果链 ▾',
518
+ ledgerEvidenceClose: '收起因果链 ▴',
519
+ ledgerVsTitle: '请求 vs 生效',
520
+ ledgerVsDiff: '{n} 项与请求不同(琥珀底行)',
521
+ ledgerVsAllMatch: '请求全部生效 ✓',
522
+ ledgerVsItem: '项',
523
+ ledgerVsRequest: '请求',
524
+ ledgerVsEffective: '生效',
525
+ ledgerVsNote: '说明',
526
+ ledgerVsUnset: '未指定',
527
+ ledgerVsDefault: '方案默认',
528
+ ledgerVsInherit: '继承父会话({model})',
529
+ ledgerVsIgnored: '请求了 {v},未生效(被忽略)',
530
+ ledgerToolsetTitle: '工具集',
531
+ ledgerToolsetFormula: '白名单 {x} ∩ 父会话 {y} → 实得 {z}',
532
+ ledgerToolsetGranted: '由系统授予',
533
+ ledgerToolsetRemoved: '移除 {n} 个',
534
+ ledgerToolsetHostNarrow: '工具由系统最终授予(插件侧未收录移除明细)',
535
+ ledgerToolsetTruncated: '(清单已截断,前 {n} 项)',
536
+ ledgerToolRemoveRunCode: '安全策略:子代理不执行代码',
537
+ ledgerToolRemoveNotInWhitelist: '不在白名单',
538
+ ledgerToolRemoveNotInAllow: '不在方案白名单',
539
+ ledgerToolCol: '工具',
540
+ ledgerToolReasonCol: '移除原因',
541
+ ledgerPrecheckTitle: '派发前 · 安全检查',
542
+ ledgerParentSessionLabel: '父会话',
543
+ ledgerRequestHuman: '请求参数',
544
+ ledgerPromptQuote: '任务指令(派发前)',
545
+ ledgerDecisionQuote: '决策原文(派发后)',
546
+ ledgerExecInterval: '{start} → {end}(共 {dur})',
547
+ ledgerExecToolCalls: '工具调用 {n} 次',
548
+ ledgerExecLiveNote: '后台执行中:过程事件随活动更新',
549
+ ledgerEventStart: '开始执行',
550
+ ledgerEventCall: '第 {n} 次工具调用',
551
+ ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过阈值 {t}s)',
552
+ ledgerEventStalling: '⚠ 停滞中(已 {s}s 无活动)',
553
+ ledgerEventIntervene: '主 agent 干预',
554
+ ledgerEventResume: '恢复执行',
555
+ ledgerEventDone: '完成',
556
+ ledgerEventRunning: '执行中…',
557
+ ledgerEventMoreCalls: '…其间还有 {n} 次调用',
558
+ ledgerStallThresholdNote: '停滞阈值 30s(待实测)',
559
+ ledgerSettleOutput: '输出摘要',
560
+ ledgerSettleCostBreakdown: '成本分解',
561
+ ledgerSettleInheritNote: '未指定模型,按继承父会话估算',
562
+ ledgerSafetyRejectDist: '安全检查拒绝分布',
563
+ ledgerScopeNote: '本页仅展示本会话的决策轨迹,成本为「本会话」口径;跨会话进化台账、审计记录与「全局」成本口径见设置页。',
246
564
  ledgerWindowNote: '已加载最近 {n} 条,尚有更早记录',
247
- gateFixUnknown: '未能识别的闸拒绝,请核对请求参数',
565
+ ledgerCostLine: '本会话口径 · 估算成本 ≈{actual} 元(若继承父会话 ≈{counter} 元 · 省 ≈{saving} 元 · 约 {ratio}×)',
566
+ ledgerInheritCost: '继承父会话模型派发 {count} 次({ratio}%)——未指定模型,无降本机会',
567
+ costNoPrice: '无单价/无用量,成本不显示',
568
+ costInheritModel: '未指定模型(继承父会话)',
569
+ inheritModelShort: '继承父会话模型',
570
+ tokenDetailPrefix: 'Token:',
571
+ whyUsable: '安全检查',
572
+ toolAllowlist: '工具',
573
+ toolHostNarrow: '工具由系统最终授予',
574
+ gateFixUnknown: '未能识别的安全检查拒绝,请核对请求参数',
248
575
  ignoredChipTitle: '请求值 "{value}" 已忽略',
249
576
  ignoredChipTitleEmpty: '请求值已忽略',
250
- // 逃生舱(非 system-trust 预设放行)
251
- escapeTitle: '逃生舱(非 system-trust 预设放行)',
252
- escapeWarn: '逃生舱已开启:允许派发非 system-trust 预设。风险自负——其余三道安全闸仍全量生效,每个放行都会留审计记录。',
577
+ // 逃生舱(放行非官方预设)
578
+ escapeTitle: '逃生舱(放行非官方预设)',
579
+ escapeCardDesc: '放行非官方预设的高风险通道,默认关闭',
580
+ escapeWarn: '逃生舱已开启:允许派发非官方预设。风险自负——其余安全检查仍全量生效,每个放行都会留审计记录。',
253
581
  escapeEmpty: '暂无放行预设',
254
582
  escapeAdd: '添加',
255
583
  escapeRemove: '移除',
256
- escapeInputPlaceholder: ' system-trust 预设 id',
584
+ escapeInputPlaceholder: '非官方预设 id',
257
585
  escapeAddEmpty: '逃生舱预设 id 不能为空',
258
- // 只读建议注入(evolution:advice)
259
- adviceTitle: '只读建议注入(evolution:advice)',
260
- adviceDesc: '开启后编排者会话注入派发统计与确定性建议(仅父 Agent、只读、默认关)'
586
+ // 派发优化建议注入(开关 + 只提示面板)
587
+ adviceTitle: '派发优化建议',
588
+ advicePanelDesc: '根据历史派发数据向父会话附优化提示(例:某方案近期成功率偏低,可考虑换方案)。只提示,不自动改配置',
589
+ adviceEnableLabel: '开启建议',
590
+ adviceSectionReason: '理由',
591
+ adviceSectionPerf: '近期表现',
592
+ adviceSectionDetail: '派发明细',
593
+ adviceSwitchOn: '开(只提示,不自动改配置)',
594
+ adviceSwitchOff: '关(只提示,不自动改配置)',
595
+ advicePanelEmpty: '暂无建议(需累计 ≥3 次派发且加权成功分低于阈值)',
596
+ advicePanelGlobal: '全局',
597
+ advicePanelReadonly: '(仅提示:统计与建议仅供参考,不改变派发行为)',
598
+ adviceDirUp: '可上调预算',
599
+ adviceDirDown: '建议降级',
600
+ adviceDirFlat: '维持现状',
601
+ adviceConfidence: '置信度:',
602
+ confidenceZh: { low: '低', medium: '中等', high: '高' },
603
+ adviceRowLocate: '定位到方案管理',
604
+ // 建议行/理由行(建议直接带对象;理由灰字小字)
605
+ adviceSuggestionPrefix: '建议:',
606
+ adviceReasonPrefix: '理由:',
607
+ adviceDirShortDown: '降级',
608
+ adviceDirShortUp: '升预算',
609
+ // 建议对象标题(摘要行对象与底部备注共用)
610
+ adviceTitleNoProfile: '手动配置的派发',
611
+ adviceTitleProfilePrefix: '方案:',
612
+ costEditProfile: '定位方案',
613
+ costNoProfile: '无对应方案',
614
+ advancedSafetyLine: '安全保证:派发不豁免审批 · 子代理工具不会超出父会话 · 成本与深度有硬上限。',
615
+ resetAllBuiltins: '重置所有内置方案',
616
+ statusLost: '审计丢失',
617
+ profilesIntro: '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可在此重置回默认;每个方案可单独启用/禁用。',
618
+ draftSavedWithId: '已保存到 draft,可在设置页「方案管理」中应用;方案 id:{id}',
619
+ draftCopyId: '复制 id',
620
+ ledgerCopyTarget: '复制',
621
+ ledgerCopied: '已复制 ✓',
622
+ costEstimated: '估算成本',
623
+ costInherit: '继承父会话成本',
624
+ costSaving: '节省',
625
+ costPriced: '可计价',
626
+ costInheritRatio: '继承占比',
627
+ costByModel: '按模型',
628
+ draftPreview: '安全检查预览',
629
+ draftPreviewPass: '安全检查:全部通过 ✓',
630
+ draftPreviewFail: '安全检查:存在拒绝项',
631
+ // 建议面板候选方案区(三层:标题行 → 说明句 → 候选项列表;有效期徽标在候选行右侧;
632
+ // 应用确认流随后续版本开放,不可应用提示收进展开区尾部)。
633
+ candidateTitle: '候选方案',
634
+ candidateNotApplied: '(未应用)',
635
+ candidateCountSuffix: ' 个 · 单选',
636
+ candidateCountNote: ' · 仅参考,不会自动应用',
637
+ candidateIntro: '系统基于近期表现生成的改进方案,仅参考、不自动应用。',
638
+ candidateRegenerate: '重新生成候选',
639
+ candidateSafetyPending: '安全检查:正在核对…',
640
+ candidateGroupChanges: '改动',
641
+ candidateGroupGenerated: '生成信息',
642
+ candidateSafetyPrefix: '安全检查:',
643
+ candidateSafetyPass: '全部通过 ✓',
644
+ candidateSafetyFail: '存在拒绝项',
645
+ candidateNotApplicable: '应用功能在后续版本开放(届时会先经你确认)',
646
+ candidateValidityForever: '⏱ 无期限',
647
+ candidateValidityPrefix: '⏱ 至 ',
648
+ candidateGeneratedPrefix: '生成于 ',
649
+ candidateSamplesPrefix: '基于最近 ',
650
+ candidateSamplesSuffix: ' 次派发',
651
+ candidatePerfPrefix: '近期表现 ',
652
+ candidatePerfSuffix: '(满分 100)',
653
+ candidateNoSample: '暂无近期派发样本,仅供参考',
654
+ // 候选卡底部:无对应方案存来源配置为新方案;有对应方案调整按钮灰置 + 定位。
655
+ adviceSaveProfile: '保存为方案',
656
+ adviceApplyDisabled: '按建议调整方案',
657
+ adviceSavedNotice: '已保存为新方案:{id}',
658
+ adviceFootNoteSave: '仅保存预设、模型与提供方;自定义人格与工具过滤无法还原(隐私保护不落盘),如需完整配置请手动编辑方案。',
659
+ adviceFootNoteMatchedPrefix: '对应方案:',
660
+ // 建议摘要行(v8:对象/建议/置信度/加权成功分;展开后见理由与明细)
661
+ adviceNoProfileTag: '无方案',
662
+ // 安全页签:安全保证卡 + 审计健康卡(只读明细表)
663
+ safetyCardTitle: '安全保证',
664
+ safetyCardDesc: '派发与审计的安全保证,明细只读',
665
+ safetyCheckNote: '派发前安全检查:白名单 / 成本 / 工具面 / 审批不豁免——拒绝分布见下方审计明细。',
666
+ auditCardTitle: '审计健康',
667
+ auditCardDesc: '治理审计状态与留痕,异常红字提示',
668
+ auditDetailToggle: '查看审计明细(点击展开)',
669
+ auditDetailEmpty: '暂无审计明细(治理事件会在这里留痕)',
670
+ auditDetailScope: '只读展示,暂不提供导出与过滤',
671
+ auditColTime: '时间',
672
+ auditColEvent: '事件',
673
+ auditColObject: '对象',
674
+ auditColResult: '结果',
675
+ auditEventZh: { 'adoption-false': '派发未采纳', 'escape-allow': '逃生舱放行', 'audit-degraded': '审计降级' },
676
+ auditResultPending: '未处理',
677
+ auditResultRead: '已读',
678
+ auditResultActedPrefix: '已处理:',
679
+ // 近期派发明细折叠表
680
+ adviceDetailTogglePrefix: '近期派发明细 · ',
681
+ adviceDetailToggleSuffix: ' 次(点击展开)',
682
+ adviceDetailEmpty: '近期暂无明细',
683
+ adviceDetailColTime: '时间',
684
+ adviceDetailColSession: '主会话(发起者)',
685
+ adviceDetailColChild: '子 Agent',
686
+ adviceDetailColOutcome: '结果',
687
+ adviceDetailColElapsed: '耗时',
688
+ adviceDetailColMode: '模式',
689
+ dispatchOutcomeZh: { completed: '完成', failed: '失败', killed: '终止' },
690
+ // 候选管理设置(顶部设置行 + 就地浮层)
691
+ candidateSettingsRow: '候选时效:',
692
+ candidateSettingsEdit: '修改 ⚙',
693
+ candidateSettingsSaved: '候选设置已保存',
694
+ candidateModeAuto: '自动换新',
695
+ candidateModeManual: '手动维护',
696
+ candidateTtlLabel: '有效期',
697
+ candidateTtl24: '24 小时',
698
+ candidateTtl72: '3 天',
699
+ candidateTtl168: '7 天',
700
+ candidateTtl0: '不自动过期',
701
+ candidateTtlCustom: '自定义',
702
+ candidateSettingsModeLabel: '更新模式',
703
+ candidateSettingsModeAutoText: '自动换新:数据显著变化时换代',
704
+ candidateSettingsModeManualText: '手动维护:不自动换代,候选保留,可点「重新生成候选」刷新',
705
+ candidateSettingsCustomLabel: '自定义天数(1~365)',
706
+ candidateSettingsCustomInvalid: '自定义有效期无效:请输入 1~365 的整数天数',
707
+ candidateSettingsSave: '保存',
708
+ candidateSettingsCancel: '取消',
709
+ // 编辑表单档位选择器口径说明(档位是估算口径,不决定模型)
710
+ tierFormHint: '档位仅影响估算与方案排序,不改变派发模型;显式选择模型才决定实际模型'
711
+ }
712
+
713
+ // profileKey 内部表示 → 人话(仅展示层;工具提示/日志保留原串):preset:X→预设:X、
714
+ // persona:1→含自定义人格、toolFilter:1→含工具过滤,段组合以 · 连接;
715
+ // (inline) 即未存方案;空键兜底未指定方案特征。
716
+ function profileKeyZh(key) {
717
+ const text = typeof key === 'string' ? key : ''
718
+ const parts = text.split('|').filter((part) => part !== '').map((part) => {
719
+ if (part === '(inline)') return '未存方案'
720
+ const idx = part.indexOf(':')
721
+ const head = idx >= 0 ? part.slice(0, idx) : part
722
+ const value = idx >= 0 ? part.slice(idx + 1) : ''
723
+ if (head === 'preset') return '预设:' + value
724
+ if (head === 'provider') return '提供方:' + value
725
+ if (head === 'model') return '模型:' + value
726
+ if (head === 'persona') return '含自定义人格'
727
+ if (head === 'toolFilter') return '含工具过滤'
728
+ if (head === 'effort') return '推理强度:' + value
729
+ return part
730
+ })
731
+ const zh = parts.join(' · ')
732
+ return zh === '' ? '未指定方案特征' : zh
261
733
  }
262
734
 
263
735
  // ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
264
736
 
265
- const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
737
+ const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', tokenTier: 'balanced', toolMode: 'none', toolList: [] }
266
738
  const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
739
+ // 设置页五页签顺序(v9:维护并入方案管理尾部折叠卡;方案管理第一位并默认激活)。
740
+ const SETTINGS_TABS = [
741
+ { key: 'profiles', label: '方案管理' },
742
+ { key: 'cost', label: '成本与统计' },
743
+ { key: 'advice', label: '自进化与建议' },
744
+ { key: 'safety', label: '安全' },
745
+ { key: 'reminders', label: '通知与提醒' }
746
+ ]
267
747
  const TOOL_MODES = [
268
- ['none', '不限制(继承父的全部工具,自动剔除 run_code)'],
748
+ ['none', '不限制(继承父会话的全部工具,自动剔除 run_code)'],
269
749
  ['allow', '白名单(只允许勾选的工具)'],
270
750
  ['deny', '黑名单(排除勾选的工具)'],
271
751
  ]
272
752
  const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
273
753
  const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
274
- // 派发账本:闸名中文标签(展示层同时保留英文技术标识)。
275
- const GATE_LABELS = { whitelist: '白名单', cost: '成本', intersection: '交集', approval: '审批', budget: '预算', other: '其它' }
276
- // 派发账本:失败闸的修复方向提示(指向设置页对应字段)。
754
+ // 派发决策台账:闸名中文标签(展示层同时保留英文技术标识)。
755
+ const GATE_LABELS = { whitelist: '白名单', cost: '成本', intersection: '交集非空', approval: '审批不豁免', budget: '预算', other: '其它' }
756
+
277
757
  const GATE_FIX_HINTS = {
278
- whitelist: '目标预设不在 system-trust 候选内,可在设置页改用受信任的预设',
758
+ whitelist: '目标预设不在候选白名单内,可在设置页改用受信任的预设',
279
759
  cost: '参数超出模型目录或成本上限,可在设置页调整提供方/模型/推理强度',
280
- intersection: '工具交集为空,可在设置页调整该方案的 toolFilter',
760
+ intersection: '方案勾选的工具父会话都没有,可在设置页调整该方案的「工具限制」',
281
761
  approval: '审批策略冲突,请核对审批配置',
282
762
  budget: '并发派发数或本会话累计 token 已达上限,请等待在途任务完成或开启新会话'
283
763
  }
284
- // 派发账本:决策轨迹字段键名中文标签(字段级渲染去「JSON 墙」用)。
285
- // 未知键回退原样(不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
286
- const GATE_KEY_LABELS = {
287
- requestedPreset: '目标预设', systemTrustCandidates: '候选白名单', allowed: '放行', resolvedPreset: '解析后预设',
288
- checks: '检查项', field: '字段', checkedAgainst: '对照项', verdict: '结论', detail: '详情', policy: '策略', applied: '应用值',
289
- effectiveAllowCount: '生效工具数', parentSessionId: '父会话', childSessionId: '子会话', jobId: '任务ID', reason: '原因', limit: '上限',
290
- skipped: '跳过', allowFailOpen: '兼容模式', mode: '模式', maxTokens: 'token 上限', maxDepth: '深度上限',
291
- requestedToolFilter: '请求工具过滤', parentToolCount: '父工具数', providers: '提供方', modelEfforts: '模型支持档位',
292
- effectiveToolNames: '生效工具', preset: '目标预设', profile: '方案', provider: '提供方', model: '模型',
293
- reasoningEffort: '推理强度', tokenTier: '档位', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤',
294
- prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断', envelope: '信封模式', parentPreset: '父预设', parentProvider: '父提供方',
295
- parentModel: '父模型', outcome: '结果', status: '状态', stop_reason: '结束原因', elapsed_ms: '耗时',
764
+ // 安全检查项人话映射(可见面零内部术语;title 保留原串供排查;未知项原样回退)。
765
+ const CHECK_ZH = { sanitize: '配置格式检查', id: '方案编号不冲突', whitelist: '工具范围安全检查', catalog: '模型目录有效' }
766
+ // 派发决策台账:决策轨迹字段键名 中文标签(字段级渲染去「JSON 墙」用)。
767
+ // 派发决策台账:决策轨迹字段键名 中文标签(FIELD_ZH,字段级渲染去「JSON 墙」用)。
768
+ // 未知键回退原样(fail-soft 不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
769
+ const FIELD_ZH = {
770
+ parentPreset: '父预设', parentProvider: '父提供方', parentModel: '父模型', parentToolCount: '父工具数',
771
+ tokenTier: '档位(估算口径)', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤', envelope: '信封模式',
772
+ prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断',
773
+ systemTrustCandidates: '候选白名单', resolvedPreset: '解析后预设', allowed: '放行',
774
+ field: '检查项', checkedAgainst: '对照', verdict: '结论', cap: '上限',
775
+ policy: '策略', applied: '应用值',
776
+ mode: '模式', deferred: '延迟结算', background: '后台',
777
+ concurrency: '当前并发', tokens: '当前 token', maxConcurrent: '并发上限', maxParentTokens: '父会话 token 上限',
778
+ parentSessionId: '父会话', childSessionId: '子会话',
779
+ id: '方案 id', description: '描述', preset: '目标预设', profile: '方案',
780
+ requestedPreset: '目标预设', checks: '检查项', detail: '详情',
781
+ effectiveAllowCount: '生效工具数', jobId: '任务ID', reason: '原因', limit: '上限',
782
+ skipped: '跳过', allowFailOpen: '兼容模式', maxTokens: 'token 上限', maxDepth: '深度上限',
783
+ requestedToolFilter: '请求工具过滤', providers: '提供方', modelEfforts: '模型支持的推理强度',
784
+ effectiveToolNames: '生效工具', provider: '提供方', model: '模型',
785
+ reasoningEffort: '推理强度',
786
+ unknown_keys: '未知字段(已被拒绝)',
787
+ outcome: '结果', status: '状态', stop_reason: '结束原因', elapsed_ms: '耗时',
296
788
  output_len: '输出长度', tool_count: '工具调用数'
297
789
  }
790
+
298
791
  const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
299
792
 
300
793
  // 审计分级的默认态(summary 缺 audit 字段 / 值非法时回退):health ok、零丢失。
@@ -311,7 +804,8 @@ function readAuditFromData(data) {
311
804
  }
312
805
 
313
806
  // /options/summary 响应 → 设置页状态(enabled/options/audit/逃生舱开关与放行集)。
314
- // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开。
807
+ // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开;
808
+ // 候选设置(更新模式/有效期)缺失时回退默认。
315
809
  function applySummaryData(data, set) {
316
810
  set.enabled(data.enabled !== false)
317
811
  set.options({
@@ -323,6 +817,10 @@ function applySummaryData(data, set) {
323
817
  set.escapeEnabled(data.escapeEnabled === true)
324
818
  set.escapePresets(Array.isArray(data.escapePresets) ? data.escapePresets : [])
325
819
  set.evolutionAdvice(data.evolutionAdvice === true)
820
+ set.candidateSettings({
821
+ candidateMode: data.candidateMode === 'manual' ? 'manual' : 'auto',
822
+ candidateTtlH: Number.isFinite(data.candidateTtlH) ? data.candidateTtlH : 24
823
+ })
326
824
  }
327
825
 
328
826
  // ── 纯函数:api / 样式注入 / 文案映射 ──────────────────────────────────────
@@ -375,6 +873,69 @@ function profileDisplay(profileId, profileNames) {
375
873
  return typeof name === 'string' && name !== '' ? `${name}(${profileId})` : profileId
376
874
  }
377
875
  const rawOrInherit = (v) => (v === '(parent)' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
876
+
877
+ // ── 省 token 证据(client 侧反事实对照)─────────────────────────
878
+ // 与 host 侧 lib/core/cost-evidence.mjs 同源同价(估算、非精确计费);只读展示。
879
+ const CLIENT_MODEL_AVG_COST = { 'deepseek-v4-flash': 0.0039, 'deepseek-v4-pro': 0.0104 }
880
+ const clientPriceKey = (model) => {
881
+ if (typeof model !== 'string' || model === '') return undefined
882
+ const m = model.trim().toLowerCase()
883
+ if (CLIENT_MODEL_AVG_COST[m] !== undefined) return m
884
+ if (m.includes('flash')) return 'deepseek-v4-flash'
885
+ if (m.includes('pro')) return 'deepseek-v4-pro'
886
+ return undefined
887
+ }
888
+ const clientAvgCost = (model) => {
889
+ const key = clientPriceKey(model)
890
+ return key === undefined ? undefined : CLIENT_MODEL_AVG_COST[key]
891
+ }
892
+ // 单条派发的估算成本(元)。trace 已归一;请求值 requested.model 才是「显式选了什么」,
893
+ // effective.model 会把继承父会话模型名写进去,不能用于判定继承。
894
+ function traceCostInfo(trace) {
895
+ if (!trace) return null
896
+ const requestedModel = trace.requested && typeof trace.requested.model === 'string' ? trace.requested.model : ''
897
+ const parentModel = trace.parentContext && typeof trace.parentContext.parentModel === 'string' ? trace.parentContext.parentModel : ''
898
+ if (requestedModel === '') {
899
+ const actual = clientAvgCost(parentModel)
900
+ if (actual === undefined) return null
901
+ return { inherit: true, actual, counterfactual: actual, saving: 0, ratio: null }
902
+ }
903
+ const actual = clientAvgCost(requestedModel)
904
+ const counterfactual = clientAvgCost(parentModel)
905
+ if (actual === undefined || counterfactual === undefined) return null
906
+ const saving = Number((counterfactual - actual).toFixed(6))
907
+ return { inherit: false, actual, counterfactual, saving, ratio: actual > 0 ? Number((counterfactual / actual).toFixed(2)) : null }
908
+ }
909
+ function recordCostInfo(record) {
910
+ return traceCostInfo(record && record.trace)
911
+ }
912
+
913
+ // 供 ② 决策段与行摘要 chip 使用;所有判定只读 trace 记录面,不做模型偏好推断。
914
+ function auditModelChoice(trace) {
915
+ if (!trace) return []
916
+ const requested = trace.requested || {}
917
+ const effective = trace.effective || {}
918
+ const out = []
919
+ const profile = typeof requested.profile === 'string' && requested.profile !== '' && requested.profile !== '(inline)' ? requested.profile : ''
920
+ const snapshot = Array.isArray(requested.profiles_snapshot) ? requested.profiles_snapshot : []
921
+ if (profile !== '' && snapshot.length > 0 && !snapshot.some((e) => e && e.id === profile)) {
922
+ out.push({ key: 'outside-snapshot', label: '快照外选择', title: '请求的方案不在记录面目录快照内(可能被截断或已变更)' })
923
+ }
924
+ const requestedModel = typeof requested.model === 'string' && requested.model !== '' ? requested.model : ''
925
+ const effectiveModel = typeof effective.model === 'string' && effective.model !== '' ? effective.model : ''
926
+ if (requestedModel !== '' && effectiveModel !== '' && requestedModel !== effectiveModel) {
927
+ out.push({ key: 'rewritten', label: '请求被改写', title: '请求模型与生效模型不一致(命中即 bug 信号)' })
928
+ }
929
+ const info = traceCostInfo(trace)
930
+ if (info && !info.inherit && info.actual !== undefined) {
931
+ const cheaper = snapshot.find((e) => e && typeof e.avgCost === 'number' && Number.isFinite(e.avgCost) && e.avgCost < info.actual * 0.7)
932
+ if (cheaper !== undefined) {
933
+ out.push({ key: 'cheaper-available', label: '更便宜候选可用', title: '目录内存在成本低 30% 以上的候选方案(仅供参考)' })
934
+ }
935
+ }
936
+ return out
937
+ }
938
+
378
939
  const effortLabel = (effort) => {
379
940
  const id = effort && effort.id
380
941
  return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
@@ -392,6 +953,16 @@ function deriveProviders(models) {
392
953
  return providers
393
954
  }
394
955
 
956
+ // provider 由 model 一对一派生(不可独立编辑):返回选中 model 的 { id, name };
957
+ // 未选中/未知 model 返回 null(调用方回退表单存量 provider 值照常展示)。
958
+ function providerOfModel(models, modelId) {
959
+ if (typeof modelId !== 'string' || modelId === '') return null
960
+ const model = Array.isArray(models) ? models.find((m) => m && m.id === modelId) : undefined
961
+ if (model === undefined) return null
962
+ const entry = deriveProviders(models).find((p) => p.id === model.provider)
963
+ return entry !== undefined ? entry : { id: model.provider, name: model.provider }
964
+ }
965
+
395
966
  // ── 纯函数:dispatch 块解析 ────────────────────────────────────────────────
396
967
 
397
968
  // 请求值:argsRaw(运行中 / 已结算两种 block 形态都有)。
@@ -411,7 +982,7 @@ function readDispatchRequest(block) {
411
982
  out.runInBackground = parsed.run_in_background === true
412
983
  }
413
984
  } catch {
414
- // unparsable argsRaw:保持默认(继承/内联口径)。
985
+ // unparsable argsRaw:保持默认(继承父会话 / 未存方案)。
415
986
  }
416
987
  return out
417
988
  }
@@ -475,44 +1046,111 @@ function readDispatchResult(block) {
475
1046
  return null
476
1047
  }
477
1048
 
478
- // childTotalTokens 文本后缀:宿主 render 行里的 `childTotalTokens=<number>` 经
479
- // parseDispatchText 解析为字符串,此处归一为「≈N tokens」。仅前台/后台 completed
480
- // 结算携带;无值返回空串(不显示)。渲染位置紧邻后续的耗时显示。
481
- function tokensSuffix(result, block) {
482
- let raw = null
1049
+
1050
+ // 结算数字提取:tokens 数与缓存命中分别取,供「≈N tokens(缓存命中 M)」
1051
+ // 单括号合并展示。tokens 不可得返回 null。
1052
+ function summaryNumbersOf(result, block) {
1053
+ let tokens = null
483
1054
  if (result && result.fields && result.fields.childTotalTokens !== undefined && result.fields.childTotalTokens !== '') {
484
- raw = result.fields.childTotalTokens
1055
+ const n = typeof result.fields.childTotalTokens === 'number' ? result.fields.childTotalTokens : Number(result.fields.childTotalTokens)
1056
+ if (Number.isFinite(n)) tokens = n
485
1057
  } else if (block && block.meta && typeof block.meta === 'object' && block.meta.childTotalTokens !== undefined) {
486
- raw = block.meta.childTotalTokens
1058
+ const n = typeof block.meta.childTotalTokens === 'number' ? block.meta.childTotalTokens : Number(block.meta.childTotalTokens)
1059
+ if (Number.isFinite(n)) tokens = n
487
1060
  }
488
- const n = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw !== '' ? Number(raw) : NaN)
489
- return Number.isFinite(n) ? ` · ≈${n} tokens` : ''
1061
+ return tokens
490
1062
  }
491
1063
 
492
- // 耗时 + 结束原因后缀:宿主 render 行里的 `elapsedMs=<n>` / `stopReason=<enum>`
493
- // parseDispatchText 解析为字符串,此处归一为「耗时=<n>ms · stopReason=<中文映射>」。
494
- // 仅前台 completed 结算携带;后台结算经 job 结果呈现(不进 dispatch 卡片),
495
- // continuable 不结算、不携带。任一字段缺失即省略该段。
496
- function observabilitySuffix(result, block) {
1064
+ // 耗时 + 结束原因提取:键名中文「耗时 X / 结束原因:Y」,
1065
+ // 时间单位规则(≥1s 秒一位小数,<1s 毫秒)由 formatDurationMs 统一。
1066
+ function summaryObservablesOf(result, block) {
497
1067
  let elapsedRaw = null
498
1068
  let stopRaw = null
499
1069
  if (result && result.fields) {
500
1070
  if (result.fields.elapsedMs !== undefined && result.fields.elapsedMs !== '') elapsedRaw = result.fields.elapsedMs
501
1071
  if (result.fields.stopReason !== undefined && result.fields.stopReason !== '') stopRaw = result.fields.stopReason
502
1072
  } else if (block && block.meta && typeof block.meta === 'object') {
503
- // 运行中(无结果)或旧版结果:回退调用块 meta(向前兼容,与 tokenTier 同口径)。
504
1073
  if (block.meta.elapsedMs !== undefined) elapsedRaw = block.meta.elapsedMs
505
1074
  if (block.meta.stopReason !== undefined) stopRaw = block.meta.stopReason
506
1075
  }
507
1076
  const ms = typeof elapsedRaw === 'number' ? elapsedRaw : (typeof elapsedRaw === 'string' && elapsedRaw !== '' ? Number(elapsedRaw) : NaN)
508
1077
  const stopZh = typeof stopRaw === 'string' ? (ZH.stopReasonZh[stopRaw] || stopRaw) : ''
509
- if (!Number.isFinite(ms) && stopZh === '') return ''
510
- let out = ''
511
- if (Number.isFinite(ms)) out += ` · 耗时=${ms}ms`
512
- if (stopZh !== '') out += ` · stopReason=${stopZh}`
513
- return out
1078
+ return { ms: Number.isFinite(ms) ? ms : null, stopZh }
1079
+ }
1080
+
1081
+ // dispatch 卡片摘要行:分隔符统一「·」、键名中文、缓存命中并入
1082
+ // token 括号、成本前缀「成本 ≈X 元」、括号单层不嵌套;拒绝时走拒绝分支。
1083
+ function dispatchSummaryText(request, result, block, trace, kindLabel) {
1084
+ const usage = usageFrom(result, block)
1085
+ const numbers = summaryNumbersOf(result, block)
1086
+ const cacheRead = usage && typeof usage.cacheReadTokens === 'number' && usage.cacheReadTokens > 0 ? usage.cacheReadTokens : 0
1087
+ const rejected = trace !== null && Array.isArray(trace.gates) && trace.gates.some((g) => g && g.verdict === 'fail')
1088
+ if (rejected) {
1089
+ const gate = trace.gates.find((g) => g && g.verdict === 'fail')
1090
+ const reason = gate && typeof gate.reason === 'string' && gate.reason !== '' ? gate.reason : ZH.ledgerReject
1091
+ return [ZH.toolLabel, ZH.ledgerReject + ':' + reason, ZH.ledgerRejectFix + ':' + gateFixHint(gate ? gate.name : '')].join(' · ')
1092
+ }
1093
+ const parts = [ZH.toolLabel + ' · ' + kindLabel]
1094
+ if (numbers !== null) parts.push('≈' + numbers + ' tokens' + (cacheRead > 0 ? '(' + ZH.cacheHit + ' ' + cacheRead + ')' : ''))
1095
+ const obs = summaryObservablesOf(result, block)
1096
+ if (obs.ms !== null) parts.push(ZH.ledgerElapsed + ' ' + formatDurationMs(obs.ms))
1097
+ if (obs.stopZh !== '') parts.push(ZH.ledgerStopReason + ':' + obs.stopZh)
1098
+ const info = traceCostInfo(trace)
1099
+ if (info !== null) {
1100
+ if (info.inherit) parts.push(ZH.costEstimated + ' ≈' + info.actual + ' 元 · ' + ZH.inheritModelShort)
1101
+ else parts.push(ZH.costEstimated + ' ≈' + info.actual + ' 元(' + ZH.ledgerCostCardCounter + ' ≈' + info.counterfactual + ' 元 · ' + ZH.ledgerCostCardSave + ' ≈' + info.saving + ' 元)')
1102
+ }
1103
+ return parts.join(' · ')
514
1104
  }
515
1105
 
1106
+ // 卡片安全检查行(差异驱动):全过「安全检查 ✅」单徽标;失败展开具体项
1107
+ // (如「安全检查:交集非空 ❌ 为空」);审批不豁免并入五项徽标,不再单列。
1108
+ function safetyLineText(trace) {
1109
+ if (!trace || !Array.isArray(trace.gates) || trace.gates.length === 0) return ''
1110
+ const names = ['whitelist', 'cost', 'intersection', 'approval', 'budget']
1111
+ const found = names.map((n) => trace.gates.find((g) => g && g.name === n)).filter((g) => g !== undefined)
1112
+ if (found.length === 0) return ''
1113
+ const failed = found.filter((g) => g.verdict === 'fail')
1114
+ if (failed.length > 0) {
1115
+ return ZH.ledgerSafetyBadge + ':' + failed.map((g) => (ZH.ledgerGateBadgeZh[g.name] || g.name) + ' ❌ ' + (typeof g.reason === 'string' && g.reason !== '' ? g.reason : '')).join(' · ')
1116
+ }
1117
+ return ZH.ledgerSafetyBadge + ' ✅'
1118
+ }
1119
+
1120
+ // 卡片工具行:continuable 有真实交集时显示数量(工具 34 → 32,剔
1121
+ // run_code);非 continuable 诚实降级「工具由系统最终授予」。
1122
+ function toolLineText(request, trace) {
1123
+ const effective = trace && trace.effective
1124
+ const count = effective && Number.isFinite(effective.effectiveToolCount) ? effective.effectiveToolCount : null
1125
+ if (request && request.continuable) {
1126
+ const allow = request.toolFilter && Array.isArray(request.toolFilter.allow) ? request.toolFilter.allow : null
1127
+ if (allow !== null) {
1128
+ const granted = trace && Array.isArray(trace.gates)
1129
+ ? trace.gates.find((g) => g && g.name === 'intersection' && g.output && Number.isFinite(g.output.effectiveAllowCount))
1130
+ : null
1131
+ const m = granted && granted.output ? granted.output.effectiveAllowCount : allow.length
1132
+ return ZH.toolAllowlist + ' ' + allow.length + ' → ' + m + '(剔 run_code)'
1133
+ }
1134
+ return ZH.toolHostNarrow
1135
+ }
1136
+ if (count !== null) return ZH.toolAllowlist + ' ' + count
1137
+ return ZH.toolHostNarrow
1138
+ }
1139
+
1140
+ // 详情 token 行:前缀「Token:」定义单位域,数字不带单位;
1141
+ // 缓存读统一为「缓存命中」。
1142
+ function usageDetailText(usage) {
1143
+ if (usage === null || typeof usage !== 'object') return ''
1144
+ const parts = []
1145
+ const push = (label, value) => {
1146
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) parts.push(label + ' ' + value)
1147
+ }
1148
+ push(ZH.usageInput, usage.inputTokens)
1149
+ push(ZH.usageOutput, usage.outputTokens)
1150
+ push(ZH.cacheHit, usage.cacheReadTokens)
1151
+ push(ZH.usageReasoning, usage.reasoningTokens)
1152
+ return parts.length > 0 ? ZH.tokenDetailPrefix + parts.join(' · ') : ''
1153
+ }
516
1154
  // 把 childUsage 的(render 行 JSON 字符串或 block.meta 对象)归一为五段对象;
517
1155
  // 非 object / 非 number 段忽略,无任何有效段返回 null。可选字段(缓存读/写/推理)
518
1156
  // 缺值省略键,与 host 侧 collectChildUsage 输出对齐。
@@ -548,15 +1186,6 @@ function usageFrom(result, block) {
548
1186
  return null
549
1187
  }
550
1188
 
551
- // 缓存命中后缀:childUsage.cacheReadTokens 存在且 >0 时追加「 · 缓存命中 M」到
552
- // ≈N tokens 之后(前导分隔符与 tokensSuffix/observabilitySuffix 同口径,即使
553
- // tokens 段缺失也不会粘在一起);无分解或缓存读为 0/缺失时返回空串。
554
- function cacheHitSuffix(usage) {
555
- if (usage === null) return ''
556
- const cacheRead = usage.cacheReadTokens
557
- return typeof cacheRead === 'number' && cacheRead > 0 ? ` · ${ZH.cacheHit} ${cacheRead}` : ''
558
- }
559
-
560
1189
  // 五段分解行文案(纯函数,供卡片详情区渲染);无任何有效段返回空串。
561
1190
  function usageBreakdownText(usage) {
562
1191
  if (usage === null || typeof usage !== 'object') return ''
@@ -572,7 +1201,7 @@ function usageBreakdownText(usage) {
572
1201
  return parts.join(' · ')
573
1202
  }
574
1203
 
575
- // ── 纯函数:派发账本数据层(快照遍历 / 轨迹归一 / 汇总统计 / 失败合并)────────
1204
+ // ── 纯函数:派发决策台账数据层(快照遍历 / 轨迹归一 / 汇总统计 / 失败合并)────
576
1205
 
577
1206
  // 把 string|number 归一为有限数,不可得返回 null(汇总与行级数字的统一口径)。
578
1207
  function toFiniteNumber(v) {
@@ -652,18 +1281,428 @@ function readNodeTrace(node) {
652
1281
  return null
653
1282
  }
654
1283
 
1284
+ // ── 子会话页头 banner(层 1)─────────────────────────────────────
1285
+
1286
+ // 从父会话快照节点解析该子会话的 preset(本插件自产 dispatch 节点,零 host 依赖):
1287
+ // 按 decisionTrace.execution.childSessionId 匹配,读生效值 preset;解析不到返回 null(显「继承父会话」)。
1288
+ function parentDispatchPresetOf(parentNodes, childSessionId) {
1289
+ if (!Array.isArray(parentNodes) || typeof childSessionId !== 'string' || childSessionId === '') return null
1290
+ for (const node of parentNodes) {
1291
+ if (!node || typeof node !== 'object' || node.kind !== 'tool-result') continue
1292
+ if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
1293
+ const trace = readNodeTrace(node)
1294
+ const execChild = trace !== null && trace.execution !== null && typeof trace.execution === 'object' && typeof trace.execution.childSessionId === 'string' ? trace.execution.childSessionId : ''
1295
+ if (execChild !== childSessionId) continue
1296
+ const result = readDispatchResult(node)
1297
+ const preset = result !== null && result.fields !== null && typeof result.fields === 'object' && typeof result.fields.preset === 'string' && result.fields.preset !== '' ? result.fields.preset : ''
1298
+ return preset !== '' ? preset : 'inherit'
1299
+ }
1300
+ return null
1301
+ }
1302
+
1303
+ // 从父会话快照节点解析该子会话的派发模式(前台/后台/持久):按
1304
+ // decisionTrace.execution.childSessionId 匹配 execution.kind;后台派发在父 trace
1305
+ // 无 childSessionId,匹配不到返回 null(调用方经 /ledger/jobs 兜底识别)。
1306
+ function parentDispatchKindOf(parentNodes, childSessionId) {
1307
+ if (!Array.isArray(parentNodes) || typeof childSessionId !== 'string' || childSessionId === '') return null
1308
+ for (const node of parentNodes) {
1309
+ if (!node || typeof node !== 'object' || node.kind !== 'tool-result') continue
1310
+ if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
1311
+ const trace = readNodeTrace(node)
1312
+ const execution = trace !== null && trace.execution !== null && typeof trace.execution === 'object' ? trace.execution : null
1313
+ const execChild = execution !== null && typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
1314
+ if (execChild !== childSessionId) continue
1315
+ const kind = execution !== null && typeof execution.kind === 'string' ? execution.kind : ''
1316
+ return kind === 'continuable' ? 'continuable' : (kind === 'background' ? 'background' : 'foreground')
1317
+ }
1318
+ return null
1319
+ }
1320
+
1321
+ // banner 数据装载(副作用助手):preset/模式解析自父会话快照,model/effort 订阅每会话
1322
+ // 共享目录 store(ModelDirectoryResolver,宿主唯一事实源);均 fail-soft,不可得显
1323
+ // 「继承父会话」。后台派发父 trace 无 childSessionId 匹配时,经 /ledger/jobs 按子会话 id 兜底。
1324
+ function loadSubagentBanner(subagent, sessionId, setParentPreset, setModelState, getModelDirectories, getSessions, setBannerMode) {
1325
+ const address = subagent.address !== null && typeof subagent.address === 'object' ? subagent.address : {}
1326
+ const parentId = typeof address.parentSessionId === 'string' ? address.parentSessionId : ''
1327
+ const childSessionId = typeof address.childSessionId === 'string' ? address.childSessionId : ''
1328
+ try {
1329
+ const parent = getSessions().binding(parentId)
1330
+ const nodes = parent !== undefined && parent !== null && parent.session !== undefined ? parent.session.chat?.legacy?.nodes : undefined
1331
+ setParentPreset(parentDispatchPresetOf(nodes, childSessionId) ?? '')
1332
+ const kind = parentDispatchKindOf(nodes, childSessionId)
1333
+ if (kind !== null) setBannerMode(kind)
1334
+ else if (parentId !== '' && childSessionId !== '') {
1335
+ api('/ledger/jobs?session=' + encodeURIComponent(parentId)).then((data) => {
1336
+ const jobs = Array.isArray(data.jobs) ? data.jobs : []
1337
+ if (jobs.some((job) => job !== null && typeof job === 'object' && job.childSessionId === childSessionId)) setBannerMode('background')
1338
+ }).catch(() => {})
1339
+ }
1340
+ } catch { setParentPreset('') }
1341
+ try {
1342
+ const directories = getModelDirectories()
1343
+ if (directories === undefined || directories === null || typeof directories.directoryFor !== 'function') { setModelState(null); return undefined }
1344
+ const dir = directories.directoryFor(sessionId)
1345
+ const store = dir !== null && typeof dir === 'object' && dir.store !== null && typeof dir.store === 'object' ? dir.store : null
1346
+ if (store === null || typeof store.getSnapshot !== 'function') { setModelState(null); return undefined }
1347
+ setModelState(store.getSnapshot())
1348
+ return typeof store.subscribe === 'function' ? store.subscribe(() => setModelState(store.getSnapshot())) : undefined
1349
+ } catch { setModelState(null); return undefined }
1350
+ }
1351
+
1352
+ // banner 模式短词:持久/后台/前台(fail-soft,未知值按前台)。
1353
+ function bannerModeText(mode) {
1354
+ if (mode === 'continuable') return ZH.bannerModePersistent
1355
+ if (mode === 'background') return ZH.modeBackground
1356
+ return ZH.modeForeground
1357
+ }
1358
+
1359
+ function bannerModelText(model) {
1360
+ return model !== '' ? model : ZH.bannerInheritValue
1361
+ }
1362
+
1363
+ function bannerEffortText(effort) {
1364
+ if (effort === '') return ZH.bannerInheritValue
1365
+ return EFFORT_ZH[effort] !== undefined ? EFFORT_ZH[effort] : effort
1366
+ }
1367
+
1368
+ function bannerPresetText(preset) {
1369
+ return preset !== '' && preset !== 'inherit' ? presetZh(preset) : ZH.bannerInheritValue
1370
+ }
1371
+
1372
+ // 摘要:全继承场景固定「继承父会话 · 模式」;有值场景按「模型 · 强度 · 模式」摘要。
1373
+ function bannerSummaryText(model, effort, preset, mode) {
1374
+ const inherited = model === '' && effort === '' && (preset === '' || preset === 'inherit')
1375
+ if (inherited) return ZH.bannerInheritValue + ' · ' + bannerModeText(mode)
1376
+ return [bannerModelText(model), bannerEffortText(effort), bannerModeText(mode)].join(' · ')
1377
+ }
1378
+
1379
+ // title 全量详情(原生多行悬停):模型/推理强度/预设/模式/来源(完整父会话 id)。
1380
+ function bannerTitleText(model, effort, preset, mode, parentId) {
1381
+ return [
1382
+ ZH.chipModel + ':' + bannerModelText(model),
1383
+ ZH.chipEffort + ':' + bannerEffortText(effort),
1384
+ ZH.chipPreset + ':' + bannerPresetText(preset),
1385
+ ZH.chipMode + ':' + bannerModeText(mode),
1386
+ ZH.bannerOriginTitle + (parentId !== '' ? '(' + parentId + ')' : '')
1387
+ ].join('\n')
1388
+ }
1389
+
1390
+ // banner 渲染:单一摘要标签 + title 全量详情。标签上限 260px 且 ellipsis,任何窗口
1391
+ // 宽度下宿主 Session log/返回按钮完整可见;来源不再单独渲染常驻行。
1392
+ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode) {
1393
+ const current = modelState !== null && typeof modelState === 'object' && modelState.current !== null && typeof modelState.current === 'object' ? modelState.current : null
1394
+ const model = current !== null && current !== undefined && typeof current.model === 'string' ? current.model : ''
1395
+ const effort = current !== null && current !== undefined && typeof current.reasoningEffort === 'string' ? current.reasoningEffort : ''
1396
+ const address = subagent.address !== null && typeof subagent.address === 'object' ? subagent.address : {}
1397
+ const addressMode = typeof address.mode === 'string' && address.mode !== '' ? address.mode : 'one-shot'
1398
+ const mode = bannerMode !== '' ? bannerMode : addressMode
1399
+ const parentId = typeof address.parentSessionId === 'string' ? address.parentSessionId : ''
1400
+ return el('div', { className: 'sap-subagentBanner' },
1401
+ el('span', { className: 'sap-subagentBannerTag', title: bannerTitleText(model, effort, parentPreset, mode, parentId) },
1402
+ bannerSummaryText(model, effort, parentPreset, mode)
1403
+ )
1404
+ )
1405
+ }
1406
+
1407
+ // ── 提醒系统 ─────────────────────────────────────────────────────
1408
+
1409
+ // 通知中心(设置页通知与提醒页签):读 /reminders(与审计同源 host store),全动作
1410
+ // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1411
+ // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1412
+ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1413
+ function ReminderCenter() {
1414
+ const [reminders, setReminders] = useState([])
1415
+ const [error, setError] = useState('')
1416
+ const refresh = () => {
1417
+ api('/reminders').then((data) => setReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
1418
+ }
1419
+ useEffect(() => { refresh() }, [])
1420
+ const act = (id, action) => () => {
1421
+ setError('')
1422
+ const request = action === 'ack' ? api('/reminders/ack', { ids: [id] }) : api('/reminders/action', { id, action })
1423
+ request.then(refresh).catch((err) => setError(String((err && err.message) || err)))
1424
+ }
1425
+ return buildReminderCenter(el, reminders, error, act, getSessions(), openChild)
1426
+ }
1427
+ return ReminderCenter
1428
+ }
1429
+
1430
+ // 提醒中心分层:未终态条目(action 为空)进默认列表;已终态条目折叠为「已处理(N)」
1431
+ // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯)。审计留痕
1432
+ // 由 host 侧动作回写,已处理条目不再堆在默认列表里。全空才显示空态文案;仅剩已处理时
1433
+ // 只显示折叠区,不显示空态。
1434
+ function buildReminderCenter(el, reminders, error, act, sessions, openChild) {
1435
+ const handled = reminders.filter((r) => typeof r.action === 'string' && r.action !== '')
1436
+ const pending = reminders.filter((r) => !(typeof r.action === 'string' && r.action !== ''))
1437
+ const pendingList = pending.length > 0
1438
+ ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1439
+ : null
1440
+ const handledList = handled.length > 0
1441
+ ? el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled' },
1442
+ el('summary', { className: 'sap-customizedSummary' }, '已处理(' + handled.length + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1443
+ el('div', { className: 'sap-customizedBody' },
1444
+ el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1445
+ )
1446
+ )
1447
+ : null
1448
+ return el('div', { className: 'sap-card sap-cardStatic' },
1449
+ el('div', { className: 'sap-cardHead' },
1450
+ el('div', { className: 'sap-cardTitleBox' },
1451
+ el('div', { className: 'sap-cardTitle' }, ZH.reminderCenterTitle),
1452
+ el('div', { className: 'sap-cardDesc' }, ZH.reminderCenterDesc)
1453
+ )
1454
+ ),
1455
+ el('div', { className: 'sap-cardBody' },
1456
+ error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1457
+ pendingList === null && handledList === null
1458
+ ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1459
+ : [pendingList, handledList]
1460
+ )
1461
+ )
1462
+ }
1463
+
1464
+ // 提醒会话标识:非全局时截断前 8 位加省略号(悬停显示完整会话标识),避免长串撑破卡片。
1465
+ function reminderSessionText(sessionId) {
1466
+ if (typeof sessionId !== 'string' || sessionId === '') return { text: ZH.reminderGlobal, title: ZH.reminderGlobal }
1467
+ return { text: ZH.reminderSessionLabel + sessionId.slice(0, 8) + '…', title: sessionId }
1468
+ }
1469
+
1470
+ // 会话标题查询:宿主 sessions 列表快照的 durable title 优先,其次 displayTitle
1471
+ // (与 id 相同视为无标题);快照缺失/异常返回空串(调用方退化 ID 截断)。
1472
+ function reminderSessionTitle(sessions, id) {
1473
+ try {
1474
+ const summary = sessions.list.getSnapshot().byId[id]
1475
+ if (summary === undefined || summary === null) return ''
1476
+ if (typeof summary.title === 'string' && summary.title !== '') return summary.title
1477
+ const display = typeof summary.displayTitle === 'string' ? summary.displayTitle : ''
1478
+ return display !== '' && display !== id ? display : ''
1479
+ } catch {
1480
+ return ''
1481
+ }
1482
+ }
1483
+
1484
+ // 主/子会话跳转行文案:有标题为「主会话 {标题} · id截断」,查不到退化为
1485
+ // 「主会话 · id截断」;title 属性悬停显示完整 id。
1486
+ function reminderJumpText(sessions, id, label) {
1487
+ const shortId = typeof id === 'string' && id !== '' ? id.slice(0, 8) + '…' : ''
1488
+ const title = reminderSessionTitle(sessions, id)
1489
+ const body = title !== '' ? title + ' · ' + shortId : shortId
1490
+ return { label, body, title: typeof id === 'string' ? id : '' }
1491
+ }
1492
+
1493
+ // tracker 内部 mode → 宿主 SubagentAddress mode(前台/后台 one-shot 归一)。
1494
+ function reminderChildMode(mode) {
1495
+ return mode === 'continuable' ? 'continuable' : 'one-shot'
1496
+ }
1497
+
1498
+ // 未采纳条目结果行:stopReason 中文 · 耗时 · 估算成本;结算摘要缺失显示未结算。
1499
+ function reminderResultText(r) {
1500
+ const settled = r.settled
1501
+ if (settled === null || typeof settled !== 'object') return ZH.reminderNoSettlement
1502
+ const parts = []
1503
+ const stop = typeof settled.stopReason === 'string' ? (ZH.stopReasonZh[settled.stopReason] || settled.stopReason) : ''
1504
+ if (stop !== '') parts.push(stop)
1505
+ const elapsed = formatDurationMs(settled.elapsedMs)
1506
+ if (elapsed !== '') parts.push(ZH.ledgerElapsed + ' ' + elapsed)
1507
+ const cost = typeof r.costEstimated === 'number' && Number.isFinite(r.costEstimated) ? '≈' + r.costEstimated + ' 元' : ''
1508
+ if (cost !== '') parts.push(cost)
1509
+ return parts.length > 0 ? parts.join(' · ') : ZH.reminderNoSettlement
1510
+ }
1511
+
1512
+ function reminderFieldLine(el, label, value) {
1513
+ return el('p', { className: 'sap-reminderField' },
1514
+ el('span', { className: 'sap-reminderFieldLabel' }, label), ' ',
1515
+ el('span', { className: 'sap-reminderFieldValue' }, value)
1516
+ )
1517
+ }
1518
+
1519
+ function reminderJumpButton(el, jump, disabled, onClick) {
1520
+ return el('button', { type: 'button', className: 'sap-reminderJump', title: jump.title, disabled: disabled === true, onClick },
1521
+ el('span', { className: 'sap-reminderFieldLabel' }, jump.label), ' ',
1522
+ el('span', { className: 'sap-reminderJumpText' }, jump.body)
1523
+ )
1524
+ }
1525
+
1526
+ // 未采纳条目五字段区:主会话/子会话可点击跳转,任务/结果/说明只读展示。
1527
+ function buildUnadoptedFields(el, r, sessions, openChild) {
1528
+ const parentJump = reminderJumpText(sessions, r.sessionId, ZH.reminderParentLabel)
1529
+ const childJump = reminderJumpText(sessions, r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId, ZH.reminderChildLabel)
1530
+ const openParent = () => { try { sessions.open(r.sessionId) } catch { /* 会话不可用:静默 */ } }
1531
+ const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1532
+ ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
1533
+ : null
1534
+ const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1535
+ const excerpt = typeof r.promptExcerpt === 'string' && r.promptExcerpt !== '' ? '「' + r.promptExcerpt + '」' : ZH.reminderNoExcerpt
1536
+ return el('div', { className: 'sap-reminderFields' },
1537
+ reminderJumpButton(el, parentJump, typeof r.sessionId !== 'string' || r.sessionId === '', openParent),
1538
+ reminderJumpButton(el, childJump, childAddress === null, jumpChild),
1539
+ reminderFieldLine(el, ZH.reminderTaskLabel, excerpt),
1540
+ reminderFieldLine(el, ZH.reminderResultLabel, reminderResultText(r)),
1541
+ reminderFieldLine(el, ZH.reminderNoteLabel, r.detail)
1542
+ )
1543
+ }
1544
+
1545
+ // 未采纳条目按钮三件套:查看子会话(跳转,恒保留)/ 标记已读(未读显示)/ 关闭。
1546
+ // 已终态条目隐藏标记已读与关闭(只留只读跳转),防对终态条目重复处置产生重复审计;
1547
+ // 提醒上的处置只改变提醒自身状态(已读/终态),不改变采纳判定——采纳与否由事件流自动汇总。
1548
+ function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1549
+ const terminated = typeof r.action === 'string' && r.action !== ''
1550
+ return el('div', { className: 'sap-reminderActions' }, [
1551
+ el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null, onClick: jumpChild }, ZH.reminderViewChild),
1552
+ terminated ? null : r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1553
+ terminated ? null : el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1554
+ ])
1555
+ }
1556
+
1557
+ // 未采纳提醒条目(完整版):标题 + 五字段 + 按钮三件套 + 已处理标注。
1558
+ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1559
+ const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1560
+ const severityText = r.severity === 'P0' ? '紧急' : '注意'
1561
+ const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1562
+ ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
1563
+ : null
1564
+ const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1565
+ return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1566
+ el('div', { className: 'sap-reminderHead' },
1567
+ el('span', { className: 'sap-chip ' + severityCls }, severityText),
1568
+ el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1569
+ ),
1570
+ buildUnadoptedFields(el, r, sessions, openChild),
1571
+ buildUnadoptedActions(el, r, act, childAddress, jumpChild),
1572
+ typeof r.action === 'string' && r.action !== ''
1573
+ ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1574
+ : null
1575
+ )
1576
+ }
1577
+
1578
+ // 通知类提醒行:P0 未读只保留「标记已读」(常驻至已读,不可跳过未读);已读后出现
1579
+ // 「关闭」(与 P1/P2 同);P1/P2 恒保留「标记已读 / 关闭」。已终态条目隐藏动作按钮,
1580
+ // 只显示已处理灰字标注(折叠区可追溯、防对终态条目误点产生重复审计)。
1581
+ function buildReminderRow(el, r, act, sessions, openChild) {
1582
+ if (r.kind === 'adoption-false') return buildUnadoptedReminderRow(el, r, act, sessions, openChild)
1583
+ const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1584
+ const severityText = r.severity === 'P0' ? '紧急' : '注意'
1585
+ const session = reminderSessionText(r.sessionId)
1586
+ return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1587
+ el('div', { className: 'sap-reminderHead' },
1588
+ el('span', { className: 'sap-chip ' + severityCls }, severityText),
1589
+ el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1590
+ ),
1591
+ el('div', { className: 'sap-reminderMeta', title: session.title }, session.text),
1592
+ el('p', { className: 'sap-desc' }, r.detail),
1593
+ typeof r.action === 'string' && r.action !== ''
1594
+ ? null
1595
+ : el('div', { className: 'sap-reminderActions' }, [
1596
+ r.unread === true ? el('button', { className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1597
+ r.severity !== 'P0' || r.unread !== true
1598
+ ? el('button', { className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1599
+ : null
1600
+ ]),
1601
+ typeof r.action === 'string' && r.action !== ''
1602
+ ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1603
+ : null
1604
+ )
1605
+ }
1606
+
1607
+ // P0 角标(shell.overlay 根悬浮层):红色常驻角标 + 未读 P0 数 + 最近一条的 session
1608
+ // 标识;点击展开面板逐条「标记已读」。P0 三要件:①severity 上色(红)②常驻至已读
1609
+ // (unread>0 恒渲染,绝不自动消失)③带 session 标识(全局异常显「全局」)。
1610
+ // 高价值提醒无 toast(toast 只留写失败瞬态,本版不实现)。
1611
+ function makeReminderOverlayBadge(el, useState, useEffect) {
1612
+ function ReminderOverlayBadge() {
1613
+ const [data, setData] = useState({ reminders: [], unread: 0 })
1614
+ const [open, setOpen] = useState(false)
1615
+ const refresh = () => {
1616
+ api('/reminders').then((d) => setData({ reminders: Array.isArray(d.reminders) ? d.reminders : [], unread: typeof d.unread === 'number' ? d.unread : 0 })).catch(() => {})
1617
+ }
1618
+ useEffect(() => {
1619
+ refresh()
1620
+ const timer = setInterval(refresh, 10000)
1621
+ return () => clearInterval(timer)
1622
+ }, [])
1623
+ const p0Unread = data.reminders.filter((r) => r.severity === 'P0' && r.unread === true)
1624
+ if (p0Unread.length === 0) return null
1625
+ const latest = p0Unread[0]
1626
+ const session = reminderSessionText(latest.sessionId)
1627
+ return el('div', { className: 'sap-overlayBadge' },
1628
+ el('button', { type: 'button', className: 'sap-overlayBadgeBtn', onClick: () => setOpen((prev) => !prev) }, ZH.reminderP0Badge.replace('{n}', String(p0Unread.length)) + ' · ' + session.text),
1629
+ open ? el('div', { className: 'sap-overlayPanel' },
1630
+ p0Unread.map((r) => el('div', { key: String(r.id), className: 'sap-overlayRow' },
1631
+ el('span', null, r.title + '(' + reminderSessionText(r.sessionId).text + ')'),
1632
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: () => { api('/reminders/ack', { ids: [r.id] }).then(refresh).catch(() => {}) } }, ZH.reminderAck)
1633
+ ))
1634
+ ) : null
1635
+ )
1636
+ }
1637
+ return ReminderOverlayBadge
1638
+ }
1639
+
1640
+ // 子会话页头 utilities 组件(conversation.session.header.utilities 槽):仅在子会话渲染。
1641
+ // bannerMode 为空时回退地址 mode(one-shot/continuable);父快照解析到的前台/后台/
1642
+ // 持久模式优先,后台经 /ledger/jobs 兜底异步回填。
1643
+ function makeSubagentHeaderUtility(el, useState, useEffect, getModelDirectories, getSessions) {
1644
+ function SubagentHeaderUtility({ useSession, sessionId }) {
1645
+ const subagent = useSession((s) => (s !== undefined && s !== null && s.subagent !== null && s.subagent !== undefined ? s.subagent : null))
1646
+ const [parentPreset, setParentPreset] = useState('')
1647
+ const [modelState, setModelState] = useState(null)
1648
+ const [bannerMode, setBannerMode] = useState('')
1649
+ useEffect(() => {
1650
+ if (subagent === null) return
1651
+ return loadSubagentBanner(subagent, sessionId, setParentPreset, setModelState, getModelDirectories, getSessions, setBannerMode)
1652
+ }, [subagent])
1653
+ if (subagent === null) return null
1654
+ return buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1655
+ }
1656
+ return SubagentHeaderUtility
1657
+ }
1658
+
655
1659
  // 快照遍历:过滤 chat.legacy.nodes 中 kind==='tool-result' 且 call.name==='dispatch'
656
- // 的节点,产出账本记录。记录携带 callId/time/isError/trace(已归一,null 即旧记录
1660
+ // 的节点,产出台账记录。记录携带 callId/time/isError/trace(已归一,null 即旧记录
657
1661
  // 降级 legacy)/request/result/argsSummary/contentFirstLine。输入为空或非数组返回
658
1662
  // 空数组;逐节点守卫、坏节点跳过。
1663
+ // 派发节点前后的可见父文字(非 CoT):向前/向后各取 1 条带文本的非 tool-result 节点,
1664
+ // 用于「父消息(派发前/后)」上下文展示,不读取内部推理。
1665
+ function surroundingText(nodes, index) {
1666
+ if (!Array.isArray(nodes)) return { before: '', after: '' }
1667
+ const pick = (dir) => {
1668
+ let i = index + dir
1669
+ while (i >= 0 && i < nodes.length) {
1670
+ const node = nodes[i]
1671
+ if (node && typeof node === 'object' && node.kind !== 'tool-result' && node.kind !== 'tool-call') {
1672
+ const text = firstContentLine(node.content)
1673
+ if (text !== '') return text.slice(0, 200)
1674
+ }
1675
+ i += dir
1676
+ }
1677
+ return ''
1678
+ }
1679
+ return { before: pick(-1), after: pick(1) }
1680
+ }
1681
+
1682
+ // 台账 tab 后台任务 live 轮询的待配对 jobId:仅未结算的后台派发记录(前台/continuable
1683
+ // /已结算不启动)。来自快照 trace.execution,无 jobId 忽略。
1684
+ function pendingBackgroundJobIds(nodes) {
1685
+ const out = []
1686
+ for (const record of collectDispatchRecords(nodes)) {
1687
+ const trace = record && record.trace
1688
+ if (trace === null || trace === undefined || trace.execution === null || typeof trace.execution !== 'object' || trace.execution.kind !== 'background') continue
1689
+ if (trace.settled !== null && trace.settled !== undefined) continue
1690
+ const jobId = trace.execution !== null && typeof trace.execution.jobId === 'string' ? trace.execution.jobId : ''
1691
+ if (jobId !== '') out.push(jobId)
1692
+ }
1693
+ return out
1694
+ }
1695
+
659
1696
  function collectDispatchRecords(nodes) {
660
1697
  if (!Array.isArray(nodes)) return []
661
1698
  const records = []
662
- for (const node of nodes) {
1699
+ for (let index = 0; index < nodes.length; index += 1) {
1700
+ const node = nodes[index]
663
1701
  if (!node || typeof node !== 'object') continue
664
1702
  if (node.kind !== 'tool-result') continue
665
1703
  if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
666
1704
  const trace = readNodeTrace(node)
1705
+ if (trace !== null) trace.parentText = surroundingText(nodes, index)
667
1706
  records.push({
668
1707
  callId: typeof node.callId === 'string' ? node.callId : '',
669
1708
  time: typeof node.time === 'number' ? node.time : (typeof node.callTime === 'number' ? node.callTime : null),
@@ -734,26 +1773,48 @@ function recordProfile(record) {
734
1773
  return ''
735
1774
  }
736
1775
 
737
- // 会话汇总:与账本汇总带对齐。records 为合并后的完整记录列表(含失败记录)。
1776
+ // 累加单条记录的反事实成本到汇总。
1777
+ function accumulateRecordCost(summary, record) {
1778
+ const cost = recordCostInfo(record)
1779
+ if (cost === null) return
1780
+ summary.pricedCount += 1
1781
+ summary.estimatedCost += cost.actual
1782
+ summary.estimatedInheritCost += cost.counterfactual
1783
+ summary.estimatedSaving += cost.saving
1784
+ if (cost.inherit) summary.inheritCount += 1
1785
+ }
1786
+
1787
+ // 成本汇总定稿:四舍五入到可展示精度,继承占比按百分数取整。
1788
+ function finalizeLedgerCost(summary) {
1789
+ summary.estimatedCost = Number(summary.estimatedCost.toFixed(4))
1790
+ summary.estimatedInheritCost = Number(summary.estimatedInheritCost.toFixed(4))
1791
+ summary.estimatedSaving = Number(summary.estimatedSaving.toFixed(4))
1792
+ summary.inheritRatio = summary.pricedCount > 0 ? Number(((summary.inheritCount / summary.pricedCount) * 100).toFixed(0)) : 0
1793
+ }
1794
+
1795
+ // 会话汇总:与台账汇总带对齐。records 为合并后的完整记录列表(含失败记录)。
738
1796
  // 旧记录贡献可得字段(次数/耗时/token),不可得记 0/省略;闸拒绝按 trace.gates
739
1797
  // 的 fail verdict 分布。byProfile/byModel/byTier 均按次数降序。
740
1798
  function computeLedgerSummary(records) {
741
- const list = Array.isArray(records) ? records : []
742
- const summary = {
1799
+ const list = Array.isArray(records) ? records : []; const summary = {
743
1800
  dispatchCount: list.length,
744
1801
  successCount: 0,
745
1802
  rejectCount: 0,
746
1803
  totalTokens: 0,
747
1804
  cacheHitTokens: 0,
748
1805
  totalElapsedMs: 0,
1806
+ estimatedCost: 0,
1807
+ estimatedInheritCost: 0,
1808
+ estimatedSaving: 0,
1809
+ inheritCount: 0,
1810
+ inheritRatio: 0,
1811
+ pricedCount: 0,
749
1812
  gateRejectCounts: { whitelist: 0, cost: 0, intersection: 0, approval: 0, other: 0 },
750
1813
  byProfile: [],
751
1814
  byModel: [],
752
1815
  byTier: []
753
1816
  }
754
- const profileMap = new Map()
755
- const modelMap = new Map()
756
- const tierMap = new Map()
1817
+ const profileMap = new Map(), modelMap = new Map(), tierMap = new Map()
757
1818
  for (const record of list) {
758
1819
  if (recordIsRejected(record)) summary.rejectCount += 1
759
1820
  else summary.successCount += 1
@@ -761,10 +1822,11 @@ function computeLedgerSummary(records) {
761
1822
  if (tokens !== null) summary.totalTokens += tokens
762
1823
  summary.cacheHitTokens += cacheRead
763
1824
  if (elapsedMs !== null) summary.totalElapsedMs += elapsedMs
1825
+ accumulateRecordCost(summary, record)
764
1826
  countGateRejects(record, summary.gateRejectCounts)
765
1827
  const profile = recordProfile(record)
766
1828
  if (profile !== '') profileMap.set(profile, (profileMap.get(profile) || 0) + 1)
767
- // 模型:空/'(parent)'(继承父标记)并入继承桶(键为空串,展示层经 rawOrInherit 归一)。
1829
+ // 模型:空/'(parent)'(继承父会话标记)并入继承桶(键为空串,展示层经 rawOrInherit 归一)。
768
1830
  const model = recordModel(record)
769
1831
  if (model === '' || model === '(parent)') modelMap.set('', (modelMap.get('') || 0) + 1)
770
1832
  else modelMap.set(model, (modelMap.get(model) || 0) + 1)
@@ -780,6 +1842,7 @@ function computeLedgerSummary(records) {
780
1842
  summary.byTier = [...tierMap.entries()]
781
1843
  .map(([tier, count]) => ({ tier, count }))
782
1844
  .sort((a, b) => b.count - a.count)
1845
+ finalizeLedgerCost(summary)
783
1846
  return summary
784
1847
  }
785
1848
 
@@ -802,7 +1865,75 @@ async function fetchLedgerFailures(sessionId) {
802
1865
  }
803
1866
  }
804
1867
 
805
- // 由失败 trace 构造独立账本记录(快照内无对应错误块时):request requested/
1868
+ // 拉取后台派发结算台账:GET /ledger/jobs?session=<id>,按 jobId join 到后台记录。
1869
+ async function fetchLedgerJobs(sessionId) {
1870
+ if (typeof sessionId !== 'string' || sessionId === '') return []
1871
+ try {
1872
+ const data = await api(`/ledger/jobs?session=${encodeURIComponent(sessionId)}`)
1873
+ return Array.isArray(data.jobs) ? data.jobs : []
1874
+ } catch {
1875
+ return []
1876
+ }
1877
+ }
1878
+
1879
+ // 全部待配对 jobId 都已在拉取结果中出现 settled → 轮询可以停止。
1880
+ function allJobsSettled(list, pending) {
1881
+ const settled = new Set()
1882
+ for (const item of list) {
1883
+ if (item !== null && typeof item === 'object' && typeof item.jobId === 'string' && item.settled !== null && item.settled !== undefined) settled.add(item.jobId)
1884
+ }
1885
+ for (const id of pending) {
1886
+ if (!settled.has(id)) return false
1887
+ }
1888
+ return true
1889
+ }
1890
+
1891
+ // 台账 tab 后台任务 live 轮询:与派发卡片同节奏(3 秒、100 次上限),仅待配对
1892
+ // jobId 非空时启动;配对齐全或达上限即停。返回清理函数。
1893
+ function startLedgerJobsPoll(sessionId, pendingJobIds, setJobs) {
1894
+ let cancelled = false
1895
+ let timer = null
1896
+ let attempts = 0
1897
+ const pending = new Set(Array.isArray(pendingJobIds) ? pendingJobIds : [])
1898
+ const poll = () => {
1899
+ fetchLedgerJobs(sessionId).then((list) => {
1900
+ if (cancelled) return
1901
+ setJobs(list)
1902
+ if (allJobsSettled(list, pending) && timer !== null) clearInterval(timer)
1903
+ }).catch(() => {})
1904
+ }
1905
+ poll()
1906
+ if (pending.size > 0) {
1907
+ timer = setInterval(() => {
1908
+ attempts += 1
1909
+ if (attempts >= 100) { if (timer !== null) clearInterval(timer); return }
1910
+ poll()
1911
+ }, 3000)
1912
+ }
1913
+ return () => { cancelled = true; if (timer !== null) clearInterval(timer) }
1914
+ }
1915
+
1916
+ // 把后台结算台账按 jobId join 回记录:仅补 execution.kind==='background' 且尚无
1917
+ // settled 的记录;已结算/非后台/无 jobId 原样返回(幂等、不覆盖已有 settled)。
1918
+ function mergeBackgroundJobs(records, jobs) {
1919
+ if (!Array.isArray(records) || !Array.isArray(jobs) || jobs.length === 0) return records
1920
+ const byJob = new Map()
1921
+ for (const item of jobs) {
1922
+ if (item === null || typeof item !== 'object' || typeof item.jobId !== 'string') continue
1923
+ byJob.set(item.jobId, item.settled)
1924
+ }
1925
+ if (byJob.size === 0) return records
1926
+ return records.map((record) => {
1927
+ const trace = record && record.trace
1928
+ const jobId = trace && trace.execution && typeof trace.execution.jobId === 'string' ? trace.execution.jobId : ''
1929
+ if (jobId === '' || trace.settled !== null && trace.settled !== undefined) return record
1930
+ const settled = byJob.get(jobId)
1931
+ if (settled === undefined) return record
1932
+ return { ...record, trace: { ...trace, settled } }
1933
+ })
1934
+ }
1935
+
1936
+ // 由失败 trace 构造独立台账记录(快照内无对应错误块时):request 从 requested/
806
1937
  // execution 还原,callId/argsSummary/contentFirstLine 为空,标记 failureOnly。
807
1938
  function failureRecord(failure) {
808
1939
  const requested = asObject(failure && failure.requested)
@@ -902,7 +2033,7 @@ function resolveDispatchState(request, result, settled) {
902
2033
  function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
903
2034
  const chips = []
904
2035
  chips.push(chip('profile', ZH.chipProfile, profileZh(eff.profile)))
905
- // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父」)与
2036
+ // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父会话」)与
906
2037
  // provider·model / effort 一样弱显。
907
2038
  chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: eff.preset === 'inherit' || eff.preset === '' }))
908
2039
  const p = rawOrInherit(eff.provider)
@@ -924,7 +2055,7 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
924
2055
  key: `ignored-${key}`,
925
2056
  className: 'sap-chip sap-chipWarn',
926
2057
  title: reqVal !== '' ? `请求值 "${reqVal}" 已忽略` : '请求值已忽略'
927
- }, `⬆${key}(${ZH.ignoredMark})`))
2058
+ }, `⬆${FIELD_ZH[key] || key}(${ZH.ignoredMark})`))
928
2059
  }
929
2060
  return chips
930
2061
  }
@@ -933,31 +2064,180 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
933
2064
  function makeChip(el) {
934
2065
  const chip = (key, label, value, opts) => {
935
2066
  const valueCls = opts && opts.dim ? 'sap-chipValueDim' : 'sap-chipValue'
2067
+ const dot = opts && opts.dot === true ? el('span', { className: 'sap-chipDot' }, ' · ') : null
936
2068
  return el('span', {
937
2069
  key,
938
2070
  className: 'sap-chip',
939
2071
  ...(opts && typeof opts.title === 'string' && opts.title !== '' ? { title: opts.title } : {})
940
2072
  },
941
2073
  el('span', { className: 'sap-chipLabel' }, label),
2074
+ dot,
942
2075
  el('span', { className: valueCls }, value)
943
2076
  )
944
2077
  }
945
2078
  return chip
946
2079
  }
947
2080
 
948
- function makeDispatchToolview(el, chip, useState) {
2081
+ // 从一次派发快照生成 draft 建议(只取配置快照,不落 prompt/persona 原文)。
2082
+ function draftSuggestionFromDispatch(request, trace, result) {
2083
+ const ts = Date.now().toString(36).slice(-4)
2084
+ const profile = request && typeof request.profile === 'string' && request.profile !== '' && request.profile !== '(inline)' ? request.profile : 'inline'
2085
+ const config = { id: 'captured-' + profile + '-' + ts }
2086
+ if (request.preset) config.preset = request.preset
2087
+ if (request.provider) config.provider = request.provider
2088
+ if (request.model) config.model = request.model
2089
+ if (request.reasoningEffort) config.reasoningEffort = request.reasoningEffort
2090
+ const tier = result && result.fields && typeof result.fields.tokenTier === 'string' ? result.fields.tokenTier : (trace && trace.effective && typeof trace.effective.tokenTier === 'string' ? trace.effective.tokenTier : '')
2091
+ if (tier) config.tokenTier = tier
2092
+ return {
2093
+ config,
2094
+ name: profile === 'inline' ? '我的派发方案' : '我的 ' + profile + ' 方案',
2095
+ description: '来自一次 dispatch 的配置快照,可继续编辑。'
2096
+ }
2097
+ }
2098
+
2099
+ // draft 捕获的 hooks 与渲染分离,保持 DispatchToolview 短小。
2100
+ function useDraftCapture(useState, suggestion) {
2101
+ const [draftOpen, setDraftOpen] = useState(false)
2102
+ const [draftName, setDraftName] = useState('')
2103
+ const [draftDesc, setDraftDesc] = useState('')
2104
+ const [draftError, setDraftError] = useState('')
2105
+ const [draftSaved, setDraftSaved] = useState(false)
2106
+ const [draftCopied, setDraftCopied] = useState(false)
2107
+ const openDraft = () => { setDraftName(suggestion.name); setDraftDesc(suggestion.description); setDraftError(''); setDraftSaved(false); setDraftOpen(true) }
2108
+ const closeDraft = () => setDraftOpen(false)
2109
+ const saveDraft = () => {
2110
+ setDraftError(''); setDraftSaved(false)
2111
+ api('/draft', { source: 'dispatch', config: suggestion.config, name: draftName, description: draftDesc })
2112
+ .then(() => setDraftSaved(true))
2113
+ .catch((err) => setDraftError(String((err && err.message) || err)))
2114
+ }
2115
+ // 成功文案的「复制方案 id」动作(clipboard fail-soft,反馈经 draftCopied 状态)。
2116
+ const copyDraftId = () => { copyToClipboard(suggestion.config.id); setDraftCopied(true) }
2117
+ return { draftOpen, draftName, setDraftName, draftDesc, setDraftDesc, draftError, draftSaved, draftCopied, draftConfigId: suggestion.config.id, openDraft, closeDraft, saveDraft, copyDraftId }
2118
+ }
2119
+
2120
+ function buildDraftButton(el, draft) {
2121
+ return el('button', { type: 'button', className: 'sap-detailToggle', onClick: draft.openDraft }, draft.draftSaved ? '已存为方案 ✓' : '存为方案')
2122
+ }
2123
+
2124
+ function buildDraftForm(el, draft) {
2125
+ if (!draft.draftOpen) return null
2126
+ return el('div', { className: 'sap-draftCard' },
2127
+ el('div', { className: 'sap-draftTitle' }, '存为方案(draft)'),
2128
+ el('div', { className: 'sap-field' },
2129
+ el('span', { className: 'sap-fieldLabel' }, '方案名'),
2130
+ el('input', { className: 'sap-input', value: draft.draftName, onChange: (event) => draft.setDraftName(event && event.target ? event.target.value : '') })
2131
+ ),
2132
+ el('div', { className: 'sap-field' },
2133
+ el('span', { className: 'sap-fieldLabel' }, '描述'),
2134
+ el('input', { className: 'sap-input', value: draft.draftDesc, onChange: (event) => draft.setDraftDesc(event && event.target ? event.target.value : '') })
2135
+ ),
2136
+ el('div', { className: 'sap-editorActions' },
2137
+ el('button', { className: 'sap-secondaryButton', onClick: draft.closeDraft }, '取消'),
2138
+ el('button', { className: 'sap-primaryButton', onClick: draft.saveDraft }, '确认保存')
2139
+ ),
2140
+ draft.draftError !== '' ? el('p', { className: 'sap-notice' }, draft.draftError) : null,
2141
+ draft.draftSaved ? el('div', { className: 'sap-toolSummary' },
2142
+ el('p', { className: 'sap-savedNotice' }, ZH.draftSavedWithId.replace('{id}', draft.draftConfigId)),
2143
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: draft.copyDraftId }, draft.draftCopied ? ZH.ledgerCopied : ZH.draftCopyId)
2144
+ ) : null
2145
+ )
2146
+ }
2147
+
2148
+ // ── 派发链路 live 状态机 ──────────────────────────────────────
2149
+
2150
+ // phase 徽标推导(纯函数):前台 execute 返回即终态(静态 ✅/❌);后台运行中块无
2151
+ // jobId,靠 /ledger/jobs 轮询结果(jobSettled)按 callId 精确配对 live
2152
+ // 「执行中 → 完成/失败」;continuable 启动即返回 subagentId,无结算概念 →「已启动」。
2153
+ function phaseStepsOf(request, result, settled, trace, jobSettled) {
2154
+ const steps = [{ key: 'initiate', label: ZH.phaseInitiate, state: 'done' }]
2155
+ if (jobSettled !== null && jobSettled !== undefined && typeof jobSettled === 'object') {
2156
+ const completed = jobSettled.status === 'completed'
2157
+ steps.push({ key: 'guards', label: ZH.phaseGuards, state: 'done' })
2158
+ steps.push({ key: 'create', label: ZH.phaseCreate, state: 'done' })
2159
+ steps.push({ key: 'run', label: ZH.phaseRun, state: 'done' })
2160
+ steps.push({ key: 'end', label: completed ? ZH.phaseDone : ZH.phaseFailed, state: completed ? 'done' : 'fail' })
2161
+ return steps
2162
+ }
2163
+ if (settled && result !== null) {
2164
+ const gates = trace !== null && Array.isArray(trace.gates) ? trace.gates : []
2165
+ const gateFailed = gates.some((g) => g && g.verdict === 'fail')
2166
+ steps.push({ key: 'guards', label: ZH.phaseGuards, state: gateFailed ? 'fail' : 'done' })
2167
+ steps.push({ key: 'create', label: ZH.phaseCreate, state: 'done' })
2168
+ steps.push({ key: 'run', label: ZH.phaseRun, state: 'done' })
2169
+ const stop = result.fields && typeof result.fields.stopReason === 'string' ? result.fields.stopReason : ''
2170
+ const failed = stop !== '' && stop !== 'completed'
2171
+ steps.push({ key: 'end', label: failed ? ZH.phaseFailed : ZH.phaseDone, state: failed ? 'fail' : 'done' })
2172
+ return steps
2173
+ }
2174
+ if (settled && result === null) {
2175
+ steps.push({ key: 'settled', label: ZH.phaseLegacy, state: 'done' })
2176
+ return steps
2177
+ }
2178
+ // 运行中:未知安全检查/创建态不虚报,只宣告 发起 → 执行中。
2179
+ steps.push({ key: 'run', label: ZH.phaseRun, state: 'running' })
2180
+ return steps
2181
+ }
2182
+
2183
+ // phase 徽标行渲染:恒在摘要行上方(未展开即渲染)。
2184
+ function buildDispatchPhaseLine(el, request, result, settled, trace, jobSettled) {
2185
+ const steps = phaseStepsOf(request, result, settled, trace, jobSettled)
2186
+ return el('div', { className: 'sap-dispatchPhase' },
2187
+ steps.map((s, index) => el('span', { key: s.key, className: 'sap-phaseStep sap-phaseStep-' + s.state }, (index > 0 ? '→ ' : '') + s.label))
2188
+ )
2189
+ }
2190
+
2191
+ // 后台派发 live 轮询(兜底):每 3 秒拉 /ledger/jobs?session=,按 callId 配对
2192
+ // 结算;配对成功或达到轮询上限(100 次 ≈ 5 分钟)即停。非后台/已结算/无 callId 不启动。
2193
+ function useBackgroundJobPoll(useState, useEffect, api, sessionId, callId, settled, request) {
2194
+ const [jobSettled, setJobSettled] = useState(null)
2195
+ useEffect(() => {
2196
+ if (settled || !request || request.runInBackground !== true) return
2197
+ if (typeof sessionId !== 'string' || sessionId === '' || typeof callId !== 'string' || callId === '') return
2198
+ let attempts = 0
2199
+ let timer = null
2200
+ const poll = () => {
2201
+ attempts += 1
2202
+ api('/ledger/jobs?session=' + encodeURIComponent(sessionId))
2203
+ .then((data) => {
2204
+ const jobs = Array.isArray(data.jobs) ? data.jobs : []
2205
+ const hit = jobs.find((j) => j && j.callId === callId && j.settled !== null && j.settled !== undefined)
2206
+ if (hit !== undefined) {
2207
+ setJobSettled(hit.settled)
2208
+ if (timer !== null) clearInterval(timer)
2209
+ } else if (attempts >= 100) {
2210
+ if (timer !== null) clearInterval(timer)
2211
+ }
2212
+ })
2213
+ .catch(() => {})
2214
+ }
2215
+ poll()
2216
+ timer = setInterval(poll, 3000)
2217
+ return () => { if (timer !== null) clearInterval(timer) }
2218
+ }, [sessionId, callId, settled, request])
2219
+ return jobSettled
2220
+ }
2221
+
2222
+ function makeDispatchToolview(el, chip, useState, useEffect) {
949
2223
  function DispatchToolview(props) {
950
- const block = props && props.block
951
- const request = readDispatchRequest(block)
952
- const result = readDispatchResult(block)
2224
+ const block = props && props.block; const request = readDispatchRequest(block); const result = readDispatchResult(block)
953
2225
  const settled = !!(block && typeof block === 'object' && 'kind' in block)
2226
+ const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
954
2227
  const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
955
2228
  const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
2229
+ const safetyLine = safetyLineText(trace); const toolLine = toolLineText(request, trace)
2230
+ const metaLine = (safetyLine !== '' || toolLine !== '') ? el('div', { className: 'sap-toolMeta' },
2231
+ safetyLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, safetyLine) : null,
2232
+ toolLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, toolLine) : null
2233
+ ) : null
956
2234
  const usage = usageFrom(result, block)
957
- const [open, setOpen] = useState(false)
2235
+ // 后台运行中按 callId 轮询 /ledger/jobs 配对结算(前台/continuable 静态终态)。
2236
+ const jobSettled = useBackgroundJobPoll(useState, useEffect, api, props.sessionId, props.callId, settled, request)
2237
+ const [open, setOpen] = useState(false); const suggestion = draftSuggestionFromDispatch(request, trace, result); const draft = useDraftCapture(useState, suggestion)
958
2238
  // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
959
2239
  // 提供方/模型/推理强度/档位)全部在 chip 行——避免同一组信息重复两遍。
960
- const text = `${ZH.toolLabel}:${kindLabel}${tokensSuffix(result, block)}${cacheHitSuffix(usage)}${observabilitySuffix(result, block)}`
2240
+ const text = dispatchSummaryText(request, result, block, trace, kindLabel)
961
2241
  // 详情折叠入口:仅当存在真实计费分解(childUsage)时渲染;无分解数据
962
2242
  // (旧结果/无 usage 会话)不显示折叠区,向前兼容。
963
2243
  const detailToggle = usage === null ? null : el('button', {
@@ -974,12 +2254,17 @@ function makeDispatchToolview(el, chip, useState) {
974
2254
  detailToggle
975
2255
  )
976
2256
  const detailBody = open && usage !== null
977
- ? el('div', { className: 'sap-toolDetail' }, usageBreakdownText(usage))
2257
+ ? el('div', { className: 'sap-toolDetail' }, usageDetailText(usage))
978
2258
  : null
2259
+ const draftButton = buildDraftButton(el, draft); const draftForm = buildDraftForm(el, draft)
979
2260
  return el('div', { className: 'sap-toolview' },
2261
+ buildDispatchPhaseLine(el, request, result, settled, trace, jobSettled),
980
2262
  el('div', { className: 'sap-chips' }, chips),
2263
+ metaLine,
981
2264
  summary,
982
- detailBody
2265
+ detailBody,
2266
+ draftButton,
2267
+ draftForm
983
2268
  )
984
2269
  }
985
2270
  return DispatchToolview
@@ -1008,7 +2293,12 @@ function makeFormActions(s, loadEfforts, loadTools) {
1008
2293
  const value = event && event.target ? event.target.value : ''
1009
2294
  s.setForm((prev) => {
1010
2295
  const next = { ...prev, [key]: value }
1011
- if (key === 'model') next.reasoningEffort = '' // 换模型时重置推理强度
2296
+ if (key === 'model') {
2297
+ next.reasoningEffort = '' // 换模型时重置推理强度
2298
+ // provider 由 model 联动派生:选中模型即同步表单 provider(不可独立编辑)。
2299
+ const derived = providerOfModel(s.options.models, value)
2300
+ if (derived !== null) next.provider = derived.id
2301
+ }
1012
2302
  return next
1013
2303
  })
1014
2304
  if (key === 'model' && value !== '') {
@@ -1070,6 +2360,7 @@ function makeEditActions(s, loadEfforts, loadTools) {
1070
2360
  provider: typeof profile.provider === 'string' ? profile.provider : '',
1071
2361
  model: typeof profile.model === 'string' ? profile.model : '',
1072
2362
  reasoningEffort: typeof profile.reasoningEffort === 'string' ? profile.reasoningEffort : '',
2363
+ tokenTier: typeof profile.tokenTier === 'string' && (profile.tokenTier === 'cheap' || profile.tokenTier === 'balanced' || profile.tokenTier === 'premium') ? profile.tokenTier : 'balanced',
1073
2364
  toolMode,
1074
2365
  toolList: toolMode === 'allow' ? allowArr : denyArr
1075
2366
  })
@@ -1090,13 +2381,14 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
1090
2381
  const submit = () => {
1091
2382
  const payload = { id: typeof s.form.id === 'string' ? s.form.id.trim() : '' }
1092
2383
  // 字符串字段总是发送(含空串):后端 merge 语义里空串 = 清除该字段,
1093
- // 否则把字段清回“继承父/默认”会被静默忽略(保留旧值)。
2384
+ // 否则把字段清回“继承父会话/默认”会被静默忽略(保留旧值)。
1094
2385
  const add = (key, value) => { if (typeof value === 'string') payload[key] = value.trim() }
1095
2386
  add('description', s.form.description)
1096
2387
  add('preset', s.form.preset)
1097
2388
  add('provider', s.form.provider)
1098
2389
  add('model', s.form.model)
1099
2390
  add('reasoningEffort', s.form.reasoningEffort)
2391
+ if (typeof s.form.tokenTier === 'string' && s.form.tokenTier !== '') payload.tokenTier = s.form.tokenTier
1100
2392
  const list = Array.isArray(s.form.toolList) ? s.form.toolList : []
1101
2393
  if (s.form.toolMode === 'allow' && list.length > 0) payload.toolFilter = { allow: list }
1102
2394
  else if (s.form.toolMode === 'deny' && list.length > 0) payload.toolFilter = { deny: list }
@@ -1136,7 +2428,18 @@ function makeToggle(api, path, payloadKey, get, set, setError) {
1136
2428
  function makeRowActions(api, s, refresh, applyWrite) {
1137
2429
  const toggleEnabled = makeToggle(api, '/set-enabled', 'enabled', () => s.enabled, s.setEnabled, s.setError)
1138
2430
  const toggleEscape = makeToggle(api, '/set-escape', 'enabled', () => s.escapeEnabled, s.setEscapeEnabled, s.setError)
1139
- const toggleAdvice = makeToggle(api, '/set-evolution-advice', 'advice', () => s.evolutionAdvice, s.setEvolutionAdvice, s.setError)
2431
+ // 建议开关切换后立即刷新 /list:候选随建议链同生共死(开→生成可见,关→不再下发)。
2432
+ const toggleAdvice = () => {
2433
+ const next = !s.evolutionAdvice
2434
+ s.setEvolutionAdvice(next)
2435
+ s.setError('')
2436
+ api('/set-evolution-advice', { advice: next })
2437
+ .then(() => refresh())
2438
+ .catch((err) => {
2439
+ s.setEvolutionAdvice(!next)
2440
+ s.setError(String((err && err.message) || err))
2441
+ })
2442
+ }
1140
2443
  const setProfileEnabled = (profile, next) => () => {
1141
2444
  s.setError('')
1142
2445
  api('/set-profile-enabled', { id: profile.id, enabled: next })
@@ -1210,35 +2513,174 @@ function makeToolsLoader(api, setTools) {
1210
2513
  }
1211
2514
  }
1212
2515
 
1213
- // 版本探测 → 拉 /options/versions;warnings 非空时设置页顶部常驻 amber 提示条。
1214
- function makeVersionsLoader(api, setVersionWarnings) {
2516
+ // 版本探测 → 拉 /options/versions;维护区 0.5.0 再统一设计归属,数据暂留。
2517
+ function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
1215
2518
  return () => {
1216
2519
  api('/options/versions')
1217
- .then((data) => setVersionWarnings(Array.isArray(data.warnings) ? data.warnings : []))
2520
+ .then((data) => {
2521
+ setVersionWarnings(Array.isArray(data.warnings) ? data.warnings : [])
2522
+ setPluginVersion(typeof data.pluginVersion === 'string' ? data.pluginVersion : '')
2523
+ })
1218
2524
  .catch(() => {})
1219
2525
  }
1220
2526
  }
1221
2527
 
1222
- function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
1223
- return {
1224
- ...makeFormActions(s, loadEfforts, loadTools),
1225
- ...makeEditActions(s, loadEfforts, loadTools),
1226
- ...makeSubmitActions(api, s, refresh, applyWrite),
1227
- ...makeRowActions(api, s, refresh, applyWrite),
1228
- ...makeEscapeActions(api, s),
2528
+ // 会话服务快照(派发明细表查主会话标题用);拿不到回退 null(fail-soft)。
2529
+ function sessionsSnapshot(getSessions) {
2530
+ try {
2531
+ return typeof getSessions === 'function' ? getSessions() : null
2532
+ } catch {
2533
+ return null
1229
2534
  }
1230
2535
  }
1231
2536
 
1232
- // ── 设置页:渲染 helpers ────────────────────────────────────────────────────
2537
+ // 定位方案:展开方案管理区 + 高亮对应卡片 + 滚动到可视区(建议/成本行的去向)。
2538
+ function makeLocateProfile(s) {
2539
+ return (profileId) => () => {
2540
+ s.setActiveTab('profiles')
2541
+ s.setHighlightId(String(profileId))
2542
+ setTimeout(() => {
2543
+ const node = document.getElementById('sap-profile-' + String(profileId))
2544
+ if (node !== null && typeof node.scrollIntoView === 'function') node.scrollIntoView({ behavior: 'smooth', block: 'center' })
2545
+ }, 60)
2546
+ setTimeout(() => s.setHighlightId(null), 8000)
2547
+ }
2548
+ }
1233
2549
 
1234
- function buildSelectField(el, s, setField, label, key, optionsList, renderOption) {
1235
- return el('div', { className: 'sap-field' },
1236
- el('span', { className: 'sap-fieldLabel' }, label),
1237
- el('select', { className: 'sap-input sap-select', value: s.form[key], onChange: setField(key) },
1238
- el('option', { value: '' }, '继承父(不指定)'),
1239
- optionsList.map(renderOption)
1240
- )
1241
- )
2550
+ // 候选动作:行点击展开/收起(默认收起,展开态不持久化;展开时复用 /draft/preview
2551
+ // 只读安全检查流);radio 仅选中(不改展开态);安全检查在展开区内部默认收起;
2552
+ // 派发明细折叠行独立开关。
2553
+ function makeCandidateActions(api, s) {
2554
+ const previewCandidate = (candidate) => () => {
2555
+ s.setError('')
2556
+ api('/draft/preview', { config: candidate.config ?? {}, name: candidate.name ?? '', description: candidate.description ?? '', source: 'evolution' })
2557
+ .then((data) => {
2558
+ const previews = { ...(s.draftPreviews || {}) }
2559
+ previews[candidate.id] = { ok: data.ok === true, checks: Array.isArray(data.checks) ? data.checks : [] }
2560
+ s.setDraftPreviews(previews)
2561
+ })
2562
+ .catch((err) => s.setError(String((err && err.message) || err)))
2563
+ }
2564
+ const toggleCandidateOpen = (candidate) => () => {
2565
+ const openState = s.candidateOpen !== null && typeof s.candidateOpen === 'object' ? s.candidateOpen : {}
2566
+ if (openState[candidate.id] === true) {
2567
+ s.setCandidateOpen({ ...openState, [candidate.id]: false })
2568
+ } else {
2569
+ s.setCandidateOpen({ ...openState, [candidate.id]: true })
2570
+ previewCandidate(candidate)()
2571
+ }
2572
+ }
2573
+ const toggleCandidateSafety = (candidate) => () => {
2574
+ const openState = s.candidateSafetyOpen !== null && typeof s.candidateSafetyOpen === 'object' ? s.candidateSafetyOpen : {}
2575
+ s.setCandidateSafetyOpen({ ...openState, [candidate.id]: openState[candidate.id] !== true })
2576
+ }
2577
+ const selectCandidate = (profileKey, candidateId) => () => {
2578
+ const map = s.candidateSelected !== null && typeof s.candidateSelected === 'object' ? s.candidateSelected : {}
2579
+ s.setCandidateSelected({ ...map, [profileKey]: candidateId })
2580
+ }
2581
+ const toggleDetail = (profileKey) => () => {
2582
+ const openState = s.detailOpen !== null && typeof s.detailOpen === 'object' ? s.detailOpen : {}
2583
+ s.setDetailOpen({ ...openState, [profileKey]: openState[profileKey] !== true })
2584
+ }
2585
+ return { previewCandidate, toggleCandidateOpen, toggleCandidateSafety, selectCandidate, toggleDetail }
2586
+ }
2587
+
2588
+ // 表单 → 有效期小时数:自定义档按天数×24(1~365 整数),其余档原样;
2589
+ // 非法输入返回 null(调用方就地报错不提交)。
2590
+ function candidateTtlFromForm(form) {
2591
+ if (form.candidateTtlChoice === 'custom') {
2592
+ const days = Number(String(form.customDays ?? '').trim())
2593
+ if (!Number.isInteger(days) || days < 1 || days > 365) return null
2594
+ return days * 24
2595
+ }
2596
+ const ttlH = Number(form.candidateTtlChoice)
2597
+ if (!Number.isInteger(ttlH) || !(ttlH === 0 || (ttlH >= 1 && ttlH <= 8760))) return 24
2598
+ return ttlH
2599
+ }
2600
+
2601
+ // 页签内交互动作:建议摘要行展开(按 profileKey)与审计明细折叠。
2602
+ function makeTabActions(s) {
2603
+ return {
2604
+ toggleAdviceOpen: (profileKey) => () => {
2605
+ const map = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
2606
+ s.setAdviceOpen({ ...map, [profileKey]: map[profileKey] !== true })
2607
+ },
2608
+ toggleAuditDetail: () => s.setAuditDetailOpen(!(s.auditDetailOpen === true))
2609
+ }
2610
+ }
2611
+
2612
+ // 候选管理设置动作:设置浮层开合、表单字段、保存(POST /settings/candidate)与
2613
+ // 取消;重新生成候选(POST /evolution/regenerate)与保存为方案(POST /advice/save-profile)。
2614
+ function makeCandidateSettingsActions(api, s, refresh, applyWrite) {
2615
+ const formOf = () => (s.candidateSettingsForm !== null && typeof s.candidateSettingsForm === 'object' ? s.candidateSettingsForm : {})
2616
+ const openSettings = () => {
2617
+ const settings = s.candidateSettings !== null && typeof s.candidateSettings === 'object' ? s.candidateSettings : { candidateMode: 'auto', candidateTtlH: 24 }
2618
+ const choice = candidateTtlChoiceOf(settings.candidateTtlH)
2619
+ s.setCandidateSettingsOpen(true)
2620
+ s.setCandidateSettingsForm({ candidateMode: settings.candidateMode, candidateTtlChoice: choice, customDays: choice === 'custom' ? String(settings.candidateTtlH / 24) : '', error: '' })
2621
+ }
2622
+ const closeSettings = () => {
2623
+ s.setCandidateSettingsOpen(false)
2624
+ s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), error: '' }))
2625
+ }
2626
+ const setForm = (patch) => s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), ...patch, error: '' }))
2627
+ const save = () => {
2628
+ const form = formOf()
2629
+ const ttlH = candidateTtlFromForm(form)
2630
+ if (ttlH === null) {
2631
+ s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), error: ZH.candidateSettingsCustomInvalid }))
2632
+ return
2633
+ }
2634
+ s.setError('')
2635
+ api('/settings/candidate', { candidateMode: form.candidateMode === 'manual' ? 'manual' : 'auto', candidateTtlH: ttlH })
2636
+ .then((data) => {
2637
+ s.setCandidateSettings({ candidateMode: data.candidateMode === 'manual' ? 'manual' : 'auto', candidateTtlH: Number.isFinite(data.candidateTtlH) ? data.candidateTtlH : 24 })
2638
+ s.setCandidateSettingsOpen(false)
2639
+ applyWrite(data, ZH.candidateSettingsSaved)
2640
+ refresh()
2641
+ })
2642
+ .catch((err) => s.setError(String((err && err.message) || err)))
2643
+ }
2644
+ const post = (path, body, onData) => {
2645
+ s.setError('')
2646
+ api(path, body).then(onData).catch((err) => s.setError(String((err && err.message) || err)))
2647
+ }
2648
+ return {
2649
+ toggleCandidateSettings: () => (s.candidateSettingsOpen === true ? closeSettings() : openSettings()),
2650
+ setCandidateFormMode: (mode) => () => setForm({ candidateMode: mode }),
2651
+ setCandidateFormTtl: (choice) => () => setForm({ candidateTtlChoice: choice }),
2652
+ setCandidateFormCustomDays: (value) => setForm({ customDays: value }),
2653
+ saveCandidateSettings: save,
2654
+ cancelCandidateSettings: closeSettings,
2655
+ regenerateCandidates: (profileKey) => () => post('/evolution/regenerate', { profileKey }, () => { s.setSavedNotice(ZH.candidateRegenerate + ' 完成'); refresh() }),
2656
+ saveAdviceProfile: (profileKey) => () => post('/advice/save-profile', { profileKey }, (data) => { applyWrite(data, ZH.adviceSavedNotice.replace('{id}', String(data.id ?? ''))); refresh() }),
2657
+ }
2658
+ }
2659
+
2660
+ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
2661
+ return {
2662
+ ...makeFormActions(s, loadEfforts, loadTools),
2663
+ ...makeEditActions(s, loadEfforts, loadTools),
2664
+ ...makeSubmitActions(api, s, refresh, applyWrite),
2665
+ ...makeRowActions(api, s, refresh, applyWrite),
2666
+ ...makeEscapeActions(api, s),
2667
+ ...makeCandidateActions(api, s),
2668
+ ...makeCandidateSettingsActions(api, s, refresh, applyWrite),
2669
+ ...makeTabActions(s),
2670
+ locateProfile: makeLocateProfile(s),
2671
+ }
2672
+ }
2673
+
2674
+ // ── 设置页:渲染 helpers ────────────────────────────────────────────────────
2675
+
2676
+ function buildSelectField(el, s, setField, label, key, optionsList, renderOption) {
2677
+ return el('div', { className: 'sap-field' },
2678
+ el('span', { className: 'sap-fieldLabel' }, label),
2679
+ el('select', { className: 'sap-input sap-select', value: s.form[key], onChange: setField(key) },
2680
+ el('option', { value: '' }, '继承父会话(不指定)'),
2681
+ optionsList.map(renderOption)
2682
+ )
2683
+ )
1242
2684
  }
1243
2685
 
1244
2686
  function renderTool(el, s, actions, t) {
@@ -1331,7 +2773,8 @@ function buildProfileRows(el, chip, s, actions) {
1331
2773
  ? el('span', { key: 'state', className: 'sap-chip sap-chipDisabled' }, ZH.chipDisabled)
1332
2774
  : el('span', { key: 'state', className: 'sap-chip sap-chipSuccess' }, ZH.chipEnabled)
1333
2775
  ]
1334
- return el('li', { key: String(profile.id), className: 'sap-rowCard' },
2776
+ const highlighted = s.highlightId !== null && s.highlightId !== undefined && String(profile.id) === String(s.highlightId)
2777
+ return el('li', { key: String(profile.id), id: 'sap-profile-' + String(profile.id), className: highlighted ? 'sap-rowCard sap-rowHighlight' : 'sap-rowCard' },
1335
2778
  el('div', { className: 'sap-rowHead' },
1336
2779
  el('span', { className: 'sap-rowIdentity' },
1337
2780
  el('span', { className: 'sap-rowName' }, hasName ? profile.name : String(profile.id)),
@@ -1350,13 +2793,44 @@ function buildProfileRows(el, chip, s, actions) {
1350
2793
  el('div', { className: 'sap-chips' }, chipItems),
1351
2794
  typeof profile.description === 'string' && profile.description !== ''
1352
2795
  ? el('p', { className: 'sap-desc' }, profile.description)
2796
+ : null,
2797
+ (typeof profile.avgCost === 'number' || typeof profile.successRate === 'number')
2798
+ ? el('p', { className: 'sap-costLine' },
2799
+ [
2800
+ typeof profile.avgCost === 'number' ? `平均 ≈${profile.avgCost} 元/次` : '',
2801
+ typeof profile.successRate === 'number' ? `成功率 ${profile.successRate}${typeof profile.n === 'number' ? `(N=${profile.n})` : ''}` : ''
2802
+ ].filter(Boolean).join(' · '))
1353
2803
  : null
1354
2804
  )
1355
2805
  })
1356
2806
  }
1357
2807
 
2808
+ // 档位选择器(估算口径):仅影响估算与方案排序,不决定模型;下方附口径说明。
2809
+ function buildTierField(el, s, actions) {
2810
+ return el('div', { className: 'sap-field' },
2811
+ el('span', { className: 'sap-fieldLabel' }, ZH.chipTier),
2812
+ el('select', { className: 'sap-input sap-select', value: s.form.tokenTier, onChange: actions.setField('tokenTier') },
2813
+ Object.entries(ZH.tierZh).map(([id, label]) => el('option', { key: id, value: id }, label))
2814
+ ),
2815
+ el('p', { className: 'sap-metaValueDefault', style: { margin: '4px 0 0' } }, ZH.tierFormHint)
2816
+ )
2817
+ }
2818
+
2819
+ // 提供方只读展示:provider 由已选 model 联动派生;未选模型时回退表单存量
2820
+ // provider 值(编辑存量 profile 时照常展示),不可独立编辑。
2821
+ function buildProviderReadonlyLine(el, s) {
2822
+ const derived = providerOfModel(s.options.models, s.form.model)
2823
+ const fallback = typeof s.form.provider === 'string' && s.form.provider !== '' ? s.form.provider : ''
2824
+ const display = derived !== null
2825
+ ? (derived.name && derived.name !== derived.id ? derived.name + '(' + derived.id + ')' : derived.id)
2826
+ : fallback
2827
+ return el('div', { className: 'sap-field' },
2828
+ el('span', { className: 'sap-fieldLabel' }, '提供方(由模型联动,只读)'),
2829
+ el('span', { className: 'sap-metaValueDefault' }, display === '' ? '选择模型后自动确定' : display)
2830
+ )
2831
+ }
2832
+
1358
2833
  function buildAddCard(el, s, actions, toolSection) {
1359
- const providers = deriveProviders(s.options.models)
1360
2834
  const effortsForModel = s.form.model ? (s.options.efforts[s.form.model] ?? []) : []
1361
2835
  const renderEffortOption = (effort) => el('option', { key: effort.id, value: effort.id }, effortLabel(effort))
1362
2836
  return el('div', { className: 'sap-addCard' },
@@ -1373,16 +2847,15 @@ function buildAddCard(el, s, actions, toolSection) {
1373
2847
  ),
1374
2848
  buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
1375
2849
  (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
1376
- s.options.models.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到模型目录(headless 部署)——派发成本护栏将按「兼容模式」开关决定放行或拒绝') : null,
1377
- el('details', { className: 'sap-customized' },
1378
- el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
2850
+ s.options.models.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到模型目录(headless 部署)——派发成本安全检查将按「兼容模式」开关决定放行或拒绝') : null,
2851
+ el('details', { className: 'sap-customized sap-customizedArrowEnd' },
2852
+ el('summary', { className: 'sap-customizedSummary' }, '高级选项', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1379
2853
  el('div', { className: 'sap-customizedBody' },
1380
- el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父预设),派发结果以「ignored」标注'),
2854
+ el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父会话预设),派发结果以「ignored」标注'),
1381
2855
  buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
1382
2856
  (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
1383
- s.options.presets.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到 system-trust 预设名册(rosterless)——目标预设下拉不可用') : null,
1384
- buildSelectField(el, s, actions.setField, '提供方(模型服务商)', 'provider', providers,
1385
- (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
2857
+ s.options.presets.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到受信任预设名册(rosterless)——目标预设下拉不可用') : null,
2858
+ buildProviderReadonlyLine(el, s),
1386
2859
  s.form.model === ''
1387
2860
  ? el('div', { className: 'sap-field' },
1388
2861
  el('span', { className: 'sap-fieldLabel' }, '推理强度(思考深度)'),
@@ -1391,6 +2864,7 @@ function buildAddCard(el, s, actions, toolSection) {
1391
2864
  )
1392
2865
  )
1393
2866
  : buildSelectField(el, s, actions.setField, '推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
2867
+ buildTierField(el, s, actions),
1394
2868
  toolSection
1395
2869
  )
1396
2870
  ),
@@ -1401,32 +2875,411 @@ function buildAddCard(el, s, actions, toolSection) {
1401
2875
  )
1402
2876
  }
1403
2877
 
1404
- // 只读建议注入(evolution:advice)开关卡片:仅父 Agent、只读、默认关。
1405
- function buildAdviceSection(el, s, actions) {
1406
- return el('div', { className: 'sap-rowCard' },
1407
- el('div', { className: 'sap-rowHead' },
1408
- el('span', { className: 'sap-rowIdentity' },
1409
- el('span', { className: 'sap-rowName' }, ZH.adviceTitle)
1410
- ),
1411
- el('span', { className: 'sap-rowActions' },
1412
- el('label', { className: 'sap-switch', title: ZH.adviceTitle },
1413
- el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
1414
- el('span', null, s.evolutionAdvice ? '已启用' : '已禁用')
2878
+ // profileKey 的 preset/model 段匹配方案管理区的 profile 卡片(建议行的去向)。
2879
+ function matchingProfileOf(profiles, profileKey) {
2880
+ if (!Array.isArray(profiles) || typeof profileKey !== 'string') return null
2881
+ const segment = (prefix) => {
2882
+ for (const part of profileKey.split('|')) {
2883
+ if (part.startsWith(prefix)) return part.slice(prefix.length)
2884
+ }
2885
+ return ''
2886
+ }
2887
+ const preset = segment('preset:')
2888
+ const model = segment('model:')
2889
+ if (preset !== '') { const hit = profiles.find((p) => p && p.preset === preset); if (hit !== undefined) return hit }
2890
+ if (model !== '') { const hit = profiles.find((p) => p && p.model === model); if (hit !== undefined) return hit }
2891
+ return null
2892
+ }
2893
+
2894
+ // ── 建议面板纯函数(epoch 格式化 / 文案换算 / 建议一句话)────────────────────
2895
+
2896
+ // epoch ms → MM-DD HH:mm(本地时区,与原型同口径);非有限数返回空串。
2897
+ function formatMonthDayTime(ts) {
2898
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) return ''
2899
+ const d = new Date(ts)
2900
+ const p = (n) => String(n).padStart(2, '0')
2901
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
2902
+ }
2903
+
2904
+ // 候选有效期徽标文案:null(不自动过期)= ⏱ 无期限;数字 = ⏱ 至 MM-DD HH:mm。
2905
+ function candidateValidityText(expiry) {
2906
+ if (typeof expiry !== 'number' || !Number.isFinite(expiry)) return ZH.candidateValidityForever
2907
+ return ZH.candidateValidityPrefix + formatMonthDayTime(expiry)
2908
+ }
2909
+
2910
+ // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等),其余指标原样(信息不删)。
2911
+ function adviceMetricsOf(performanceText) {
2912
+ return adviceEvidenceOf(performanceText).metrics.map((metric) => {
2913
+ const hit = /^平均耗时 (.+)$/.exec(metric)
2914
+ if (hit !== null) {
2915
+ const ms = Number(hit[1])
2916
+ return Number.isFinite(ms) ? '平均耗时 ' + formatDurationMs(ms) : metric
2917
+ }
2918
+ return metric
2919
+ })
2920
+ }
2921
+
2922
+ // 方向与置信度中文(模块级共用:面板建议行与旧格式回退)。
2923
+ function directionZh(d) {
2924
+ return d === '升' ? ZH.adviceDirUp : d === '降' ? ZH.adviceDirDown : ZH.adviceDirFlat
2925
+ }
2926
+
2927
+ function confidenceZhOf(level) {
2928
+ return ZH.confidenceZh[level] || level
2929
+ }
2930
+
2931
+ // 摘要行建议短句:只带对象与方向(置信度在摘要行独立成列,不再并入一句)。
2932
+ function adviceSuggestionShort(a) {
2933
+ const object = typeof a.objectText === 'string' && a.objectText !== '' ? a.objectText : ''
2934
+ if (object === '') return directionZh(a.suggestion)
2935
+ const word = a.suggestion === '降' ? ZH.adviceDirShortDown : a.suggestion === '升' ? ZH.adviceDirShortUp : ''
2936
+ return ZH.adviceSuggestionPrefix + object + (word !== '' ? '(' + word + ')' : '')
2937
+ }
2938
+
2939
+ // 摘要行对象文本:已有方案「方案:名称」;无方案「手动配置的派发(含自定义人格…)」
2940
+ // 并附「无方案」小标签(特征取 profileKeyZh,与候选/对应关系同一口径)。
2941
+ function adviceSummaryObjectText(profile, profileKey) {
2942
+ if (profile !== null) return adviceObjectTitleOf(profile)
2943
+ const features = profileKeyZh(profileKey).split(' · ').filter((part) => part !== '未存方案' && part !== '未指定方案特征').join(' · ')
2944
+ return adviceObjectTitleOf(null) + (features !== '' ? '(' + features + ')' : '')
2945
+ }
2946
+
2947
+ // 卡头标题 = 建议对象:已有方案用「方案:名称」,未匹配用「手动配置的派发」。
2948
+ function adviceObjectTitleOf(profile) {
2949
+ return profile !== null ? ZH.adviceTitleProfilePrefix + (profile.name || profile.id) : ZH.adviceTitleNoProfile
2950
+ }
2951
+
2952
+ // 会话/子 Agent 标识截断(悬停由原生 title 保留全文;不足长度原样)。
2953
+ function sessionIdShortOf(id) {
2954
+ const text = typeof id === 'string' ? id : ''
2955
+ return text.length > 12 ? text.slice(0, 12) + '…' : text
2956
+ }
2957
+
2958
+ function childIdShortOf(id) {
2959
+ const text = typeof id === 'string' ? id : ''
2960
+ return text.length > 8 ? text.slice(0, 8) + '…' : text
2961
+ }
2962
+
2963
+ // 派发模式中文(前台/后台/持久);未知值原样(fail-soft)。
2964
+ function dispatchModeZh(mode) {
2965
+ if (mode === 'background') return ZH.modeBackground
2966
+ if (mode === 'continuable') return ZH.bannerModePersistent
2967
+ if (mode === 'foreground') return ZH.modeForeground
2968
+ return mode
2969
+ }
2970
+
2971
+ // 表现摘要拆两层:计数(完成/失败/终止)与指标(加权成功分/平均耗时/平均输出),
2972
+ // 各渲染一遍;解析不出的部分原样保留(信息不删)。
2973
+ function adviceEvidenceOf(performanceText) {
2974
+ const text = typeof performanceText === 'string' ? performanceText : ''
2975
+ const withoutWindow = text.replace(/^过去 \d+ 次:/, '')
2976
+ const parts = withoutWindow.split(',').map((part) => part.trim()).filter((part) => part !== '')
2977
+ return { counts: parts.length > 0 ? parts[0] : '', metrics: parts.slice(1) }
2978
+ }
2979
+
2980
+ // 候选安全检查四项:缩进列表对齐折叠文字下方(margin-left 16px),人话映射;
2981
+ // 只渲染四项本身,不重复「安全检查:全部通过 ✓」标题行(标题 = 折叠行)。
2982
+ function buildCandidateSafetyChecks(el, preview) {
2983
+ const checks = Array.isArray(preview.checks) ? preview.checks : []
2984
+ return el('ul', { className: 'sap-candidateSafetyList' },
2985
+ checks.map((c) => el('li', { key: String(c.name), className: 'sap-candidateSafetyItem', title: c.name },
2986
+ (c.verdict === 'pass' ? '✅ ' : '❌ ') + (CHECK_ZH[c.name] || c.name) + (typeof c.reason === 'string' && c.reason !== '' ? ':' + c.reason : '')
2987
+ ))
2988
+ )
2989
+ }
2990
+
2991
+ // 候选改动项:候选 config 与建议来源 profileKey 对比成人话列表(不加前缀,
2992
+ // 展开区语境已是改动清单)。
2993
+ // persona/toolFilter 从键里消失 = 去掉该结构;其余字段值不同 = 换成新值。
2994
+ function candidateChangesOf(profileKey, cfg) {
2995
+ const config = cfg !== null && typeof cfg === 'object' ? cfg : {}
2996
+ const text = typeof profileKey === 'string' ? profileKey : ''
2997
+ const seg = {}
2998
+ for (const part of text.split('|')) {
2999
+ const idx = part.indexOf(':')
3000
+ if (idx >= 0) seg[part.slice(0, idx)] = part.slice(idx + 1)
3001
+ }
3002
+ const changes = []
3003
+ const changed = (field, value) => typeof value === 'string' && value !== '' && seg[field] !== value
3004
+ if (seg.persona === '1' && config.persona_present !== true) changes.push('去掉自定义人格')
3005
+ if (seg.toolFilter === '1' && config.toolFilter_present !== true) changes.push('去掉工具过滤')
3006
+ if (changed('preset', config.preset)) changes.push('预设 → ' + config.preset)
3007
+ if (changed('model', config.model)) changes.push('模型 → ' + config.model)
3008
+ if (changed('effort', config.reasoningEffort)) changes.push('推理强度 → ' + config.reasoningEffort)
3009
+ if (typeof config.maxTokens === 'number' && config.maxTokens > 0) changes.push('token 上限 → ' + String(config.maxTokens))
3010
+ return changes
3011
+ }
3012
+
3013
+ // 建议大卡(v16):卡头只留标题 + 右侧「开启建议」开关;卡体顶部 = 管理子卡
3014
+ // (浅底紧凑,仅候选时效行 + 全局行,就地浮层随行);下方 = 建议列表。
3015
+ function buildAdvicePanel(el, s, actions, sessions) {
3016
+ const advice = Array.isArray(s.advice) ? s.advice : []
3017
+ const global = s.adviceGlobal !== null && typeof s.adviceGlobal === 'object' ? s.adviceGlobal : null
3018
+ const candidates = Array.isArray(s.evolutionCandidates) ? s.evolutionCandidates : []
3019
+ const candidatesOf = (profileKey) => candidates.filter((c) => c && typeof c === 'object' && c.profile_key === profileKey)
3020
+ if (global === null) return null
3021
+ return el('div', { className: 'sap-card sap-cardStatic' },
3022
+ el('div', { className: 'sap-cardHead sap-adviceCardHead' },
3023
+ el('div', { className: 'sap-adviceHeadTitleRow' },
3024
+ el('div', { className: 'sap-cardTitleBox' },
3025
+ el('div', { className: 'sap-cardTitle' }, ZH.adviceTitle)
3026
+ ),
3027
+ el('span', { className: 'sap-rowActions' },
3028
+ ZH.adviceEnableLabel,
3029
+ el('label', { className: 'sap-switch', title: ZH.adviceTitle },
3030
+ el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
3031
+ el('span', null, s.evolutionAdvice ? ZH.adviceSwitchOn : ZH.adviceSwitchOff)
3032
+ )
1415
3033
  )
1416
- )
3034
+ ),
3035
+ el('div', { className: 'sap-cardDesc' }, ZH.advicePanelDesc)
3036
+ ),
3037
+ el('div', { className: 'sap-cardBody' },
3038
+ el('div', { className: 'sap-adviceManageCard' },
3039
+ buildCandidateSettingsRow(el, s, actions),
3040
+ el('p', { className: 'sap-adviceGlobalLine' }, ZH.advicePanelGlobal + ':累计 ' + global.total + ' 次(完成 ' + global.completed + '、失败 ' + global.failed + '、终止 ' + global.killed + ')' + ZH.advicePanelReadonly),
3041
+ s.candidateSettingsOpen === true ? buildCandidateSettingsPanel(el, s, actions) : null
3042
+ ),
3043
+ advice.length === 0
3044
+ ? el('p', { className: 'sap-desc' }, ZH.advicePanelEmpty)
3045
+ : el('ul', { className: 'sap-rows' },
3046
+ advice.map((a) => buildAdviceCard(el, s, actions, a, candidatesOf(a.profileKey), sessions))
3047
+ )
3048
+ )
3049
+ )
3050
+ }
3051
+
3052
+ // 详情小节:纯文字加粗小标题(12px label-primary)+ 内容(13px),无左线/无底块。
3053
+ function adviceSectionOf(el, title, children) {
3054
+ return el('div', { className: 'sap-adviceSection' },
3055
+ el('p', { className: 'sap-adviceSectionTitle' }, title),
3056
+ children
3057
+ )
3058
+ }
3059
+
3060
+ // 单张建议卡(v10):摘要行 = 对象/建议/置信度/▸(结论三件套,无加权分);
3061
+ // 展开区 = 小节序列 理由 → 近期表现 → 派发明细 → 候选方案(未应用)→ 操作区。
3062
+ function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3063
+ const profile = matchingProfileOf(s.profiles, a.profileKey)
3064
+ const metrics = adviceMetricsOf(a.performanceText)
3065
+ const rows = s.adviceDetails && typeof s.adviceDetails === 'object' && Array.isArray(s.adviceDetails[a.profileKey]) ? s.adviceDetails[a.profileKey] : []
3066
+ const openMap = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
3067
+ const open = openMap[a.profileKey] === true
3068
+ const summary = el('button', { type: 'button', className: 'sap-adviceSummary', 'aria-expanded': open, onClick: actions.toggleAdviceOpen(a.profileKey) },
3069
+ el('span', { className: 'sap-adviceObjLine' },
3070
+ el('span', { className: 'sap-adviceObj' }, adviceSummaryObjectText(profile, a.profileKey)),
3071
+ profile === null ? el('span', { className: 'sap-adviceNoProfileTag' }, ZH.adviceNoProfileTag) : null
1417
3072
  ),
1418
- el('p', { className: 'sap-desc' }, ZH.adviceDesc)
3073
+ el('span', { className: 'sap-adviceSummaryLine' },
3074
+ el('span', { className: 'sap-adviceSummaryText' }, adviceSuggestionShort(a)),
3075
+ el('span', { className: 'sap-adviceConf' }, ZH.adviceConfidence + confidenceZhOf(a.confidence)),
3076
+ el('span', { className: 'sap-adviceSummaryArrow' }, '▸')))
3077
+ const detail = open !== true ? null : el('div', { className: 'sap-adviceDetail' },
3078
+ typeof a.reasonText === 'string' && a.reasonText !== '' ? adviceSectionOf(el, ZH.adviceSectionReason, el('p', { className: 'sap-adviceSectionBody' }, a.reasonText)) : null,
3079
+ metrics.length > 0 ? adviceSectionOf(el, ZH.adviceSectionPerf, el('p', { className: 'sap-adviceSectionBody' }, metrics.join(' · '))) : null,
3080
+ adviceSectionOf(el, ZH.adviceSectionDetail, buildAdviceDetails(el, s, actions, a.profileKey, rows, sessions)),
3081
+ candidates.length > 0 ? buildAdviceCandidates(el, s, actions, a, candidates) : null,
3082
+ buildAdviceFoot(el, s, actions, a, profile, candidates))
3083
+ return el('li', { key: String(a.profileKey), className: 'sap-adviceItem' + (open === true ? ' sap-adviceItemOpen' : '') }, summary, detail)
3084
+ }
3085
+
3086
+ // 近期派发明细折叠行:▸ 近期派发明细 · N 次(点击展开)→ 六列表格;默认收起。
3087
+ function buildAdviceDetails(el, s, actions, profileKey, rows, sessions) {
3088
+ const map = s.detailOpen !== null && typeof s.detailOpen === 'object' ? s.detailOpen : {}
3089
+ const open = map[profileKey] === true
3090
+ const header = el('tr', null,
3091
+ el('th', null, ZH.adviceDetailColTime), el('th', null, ZH.adviceDetailColSession), el('th', null, ZH.adviceDetailColChild),
3092
+ el('th', null, ZH.adviceDetailColOutcome), el('th', null, ZH.adviceDetailColElapsed), el('th', null, ZH.adviceDetailColMode))
3093
+ const cell = (row) => {
3094
+ const outcomeZh = ZH.dispatchOutcomeZh[row.outcome] || row.outcome || ''
3095
+ const bad = row.outcome === 'failed' || row.outcome === 'killed'
3096
+ const title = reminderSessionTitle(sessions, row.sessionId)
3097
+ return el('tr', { key: String(row.ts ?? '') + String(row.childId ?? '') },
3098
+ el('td', { className: 'sap-adviceDetailTime' }, formatMonthDayTime(row.ts)),
3099
+ el('td', null, title !== '' ? el('span', { className: 'sap-adviceDetailSession' }, title) : null,
3100
+ sessionIdShortOf(row.sessionId) !== '' ? el('span', { className: 'sap-adviceDetailSessionId' }, sessionIdShortOf(row.sessionId)) : null),
3101
+ el('td', { className: 'sap-adviceDetailChild' }, childIdShortOf(row.childId)),
3102
+ el('td', { className: bad ? 'sap-warn-x' : '' }, outcomeZh),
3103
+ el('td', null, formatDurationMs(row.elapsedMs)),
3104
+ el('td', null, dispatchModeZh(row.mode)))
3105
+ }
3106
+ return el('div', { className: 'sap-adviceDetailBlock' },
3107
+ el('button', { type: 'button', className: 'sap-adviceDetailToggle', 'aria-expanded': open, onClick: actions.toggleDetail(profileKey) },
3108
+ el('span', null, ZH.adviceDetailTogglePrefix + rows.length + ZH.adviceDetailToggleSuffix),
3109
+ el('span', { className: 'sap-adviceDetailArrow' }, '▸')),
3110
+ open !== true ? null : (rows.length === 0
3111
+ ? el('p', { className: 'sap-candidateHint' }, ZH.adviceDetailEmpty)
3112
+ : el('table', { className: 'sap-adviceDetailTable' }, el('thead', null, header), el('tbody', null, rows.map(cell))))
1419
3113
  )
1420
3114
  }
1421
3115
 
3116
+ // 候选管理设置(顶部一行 + 就地浮层):更新模式与有效期用户可配;
3117
+ // 自定义按天数输入(1~365),提交时换算小时数。
3118
+ function candidateModeTextOf(mode) {
3119
+ return mode === 'manual' ? ZH.candidateModeManual : ZH.candidateModeAuto
3120
+ }
3121
+
3122
+ function candidateTtlTextOf(ttlH) {
3123
+ if (ttlH === 0) return ZH.candidateTtl0
3124
+ if (ttlH === 24) return ZH.candidateTtl24
3125
+ if (ttlH === 72) return ZH.candidateTtl72
3126
+ if (ttlH === 168) return ZH.candidateTtl168
3127
+ return ttlH + ' 小时'
3128
+ }
3129
+
3130
+ function candidateTtlChoiceOf(ttlH) {
3131
+ return ttlH === 0 || ttlH === 24 || ttlH === 72 || ttlH === 168 ? String(ttlH) : 'custom'
3132
+ }
3133
+
3134
+ function buildCandidateSettingsRow(el, s, actions) {
3135
+ const settings = s.candidateSettings !== null && typeof s.candidateSettings === 'object' ? s.candidateSettings : { candidateMode: 'auto', candidateTtlH: 24 }
3136
+ return el('p', { className: 'sap-candidateSettingsRow' },
3137
+ el('b', null, ZH.candidateSettingsRow),
3138
+ el('span', null, candidateModeTextOf(settings.candidateMode) + ' · ' + ZH.candidateTtlLabel + ' ' + candidateTtlTextOf(settings.candidateTtlH)),
3139
+ el('button', { type: 'button', className: 'sap-candidateSettingsEdit', onClick: actions.toggleCandidateSettings }, ZH.candidateSettingsEdit))
3140
+ }
3141
+
3142
+ function candidateSettingsRadio(el, label, value, current, onChange) {
3143
+ return el('label', { key: value, className: 'sap-candidateSettingsOption' },
3144
+ el('input', { type: 'radio', checked: current === value, onChange }),
3145
+ el('span', null, label))
3146
+ }
3147
+
3148
+ function buildCandidateSettingsPanel(el, s, actions) {
3149
+ const form = s.candidateSettingsForm !== null && typeof s.candidateSettingsForm === 'object' ? s.candidateSettingsForm : {}
3150
+ const mode = form.candidateMode === 'manual' ? 'manual' : 'auto'
3151
+ const ttlChoice = typeof form.candidateTtlChoice === 'string' ? form.candidateTtlChoice : '24'
3152
+ const ttlOptions = [['24', ZH.candidateTtl24], ['72', ZH.candidateTtl72], ['168', ZH.candidateTtl168], ['0', ZH.candidateTtl0], ['custom', ZH.candidateTtlCustom]]
3153
+ return el('div', { className: 'sap-candidateSettingsPanel' },
3154
+ el('div', { className: 'sap-candidateSettingsField' },
3155
+ el('span', { className: 'sap-fieldLabel' }, ZH.candidateSettingsModeLabel),
3156
+ el('div', { className: 'sap-candidateSettingsOptions' },
3157
+ candidateSettingsRadio(el, ZH.candidateModeAuto + '(推荐)', 'auto', mode, actions.setCandidateFormMode('auto')),
3158
+ candidateSettingsRadio(el, ZH.candidateModeManual, 'manual', mode, actions.setCandidateFormMode('manual'))),
3159
+ el('p', { className: 'sap-metaValueDefault' }, mode === 'manual' ? ZH.candidateSettingsModeManualText : ZH.candidateSettingsModeAutoText)),
3160
+ el('div', { className: 'sap-candidateSettingsField' },
3161
+ el('span', { className: 'sap-fieldLabel' }, ZH.candidateTtlLabel),
3162
+ el('div', { className: 'sap-candidateSettingsOptions' },
3163
+ ttlOptions.map(([value, label]) => candidateSettingsRadio(el, label, value, ttlChoice, actions.setCandidateFormTtl(value)))),
3164
+ ttlChoice === 'custom'
3165
+ ? el('div', { className: 'sap-candidateSettingsCustom' },
3166
+ el('input', { className: 'sap-input', value: typeof form.customDays === 'string' ? form.customDays : '', placeholder: '1~365', onChange: (event) => actions.setCandidateFormCustomDays(event && event.target ? event.target.value : '') }),
3167
+ el('span', { className: 'sap-metaValueDefault' }, ZH.candidateSettingsCustomLabel))
3168
+ : null,
3169
+ form.error !== '' ? el('p', { className: 'sap-notice' }, form.error) : null),
3170
+ el('div', { className: 'sap-candidateSettingsActions' },
3171
+ el('button', { type: 'button', className: 'sap-primaryButton', onClick: actions.saveCandidateSettings }, ZH.candidateSettingsSave),
3172
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.cancelCandidateSettings }, ZH.candidateSettingsCancel)))
3173
+ }
3174
+
3175
+ // 候选方案区(v10 小节):纯文字加粗小节标题「候选方案(未应用)」+ 上分隔线 →
3176
+ // 数量/单选说明行(手动维护附重新生成)→ 候选项列表(行点击只展开,radio 只选中)。
3177
+ function buildAdviceCandidates(el, s, actions, a, candidates) {
3178
+ const manual = s.candidateSettings !== null && typeof s.candidateSettings === 'object' && s.candidateSettings.candidateMode === 'manual'
3179
+ const selected = candidateSelectedIdOf(s, a.profileKey, candidates)
3180
+ return el('div', { className: 'sap-candidates' },
3181
+ el('p', { className: 'sap-candidatesTitle' }, ZH.candidateTitle, el('span', { className: 'sap-candidatesNotApplied' }, ZH.candidateNotApplied)),
3182
+ el('p', { className: 'sap-candidatesCount' }, candidates.length + ZH.candidateCountSuffix + ZH.candidateCountNote),
3183
+ manual === true
3184
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.regenerateCandidates(a.profileKey) }, ZH.candidateRegenerate)
3185
+ : null,
3186
+ candidates.map((candidate, index) => buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected))
3187
+ )
3188
+ }
3189
+
3190
+ // 默认选中第一条候选;用户选择存进状态(仅选中,行点击只展开)。
3191
+ function candidateSelectedIdOf(s, profileKey, candidates) {
3192
+ const map = s.candidateSelected !== null && typeof s.candidateSelected === 'object' ? s.candidateSelected : {}
3193
+ const current = typeof map[profileKey] === 'string' ? map[profileKey] : ''
3194
+ if (current !== '' && candidates.some((c) => c && c.id === current)) return current
3195
+ return candidates.length > 0 ? candidates[0].id : ''
3196
+ }
3197
+
3198
+ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected) {
3199
+ const cfg = candidate.config && typeof candidate.config === 'object' ? candidate.config : {}
3200
+ const openState = s.candidateOpen !== null && typeof s.candidateOpen === 'object' ? s.candidateOpen : {}
3201
+ const safetyState = s.candidateSafetyOpen !== null && typeof s.candidateSafetyOpen === 'object' ? s.candidateSafetyOpen : {}
3202
+ const open = openState[candidate.id] === true
3203
+ const safetyOpen = safetyState[candidate.id] === true
3204
+ const preview = s.draftPreviews && typeof s.draftPreviews === 'object' ? s.draftPreviews[candidate.id] : undefined
3205
+ const changes = candidateChangesOf(a.profileKey, cfg)
3206
+ const onRadio = (event) => {
3207
+ if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
3208
+ actions.selectCandidate(a.profileKey, candidate.id)()
3209
+ }
3210
+ return el('div', { key: String(candidate.id), className: 'sap-candidate' + (selected === candidate.id ? ' sap-candidateSelected' : '') },
3211
+ el('button', { type: 'button', className: 'sap-candidateRow', 'aria-expanded': open, onClick: actions.toggleCandidateOpen(candidate) },
3212
+ el('span', { role: 'radio', 'aria-checked': selected === candidate.id, className: 'sap-candidateRadio', onClick: onRadio }),
3213
+ el('span', { className: 'sap-candidateName' }, '候选 ' + (index + 1) + ' · ' + candidate.name),
3214
+ el('span', { className: 'sap-candidateValidity' }, candidateValidityText(candidate.expiry)),
3215
+ el('span', { className: 'sap-candidateArrow' }, '▸')),
3216
+ open === true ? el('div', { className: 'sap-candidateBody' },
3217
+ changes.length > 0 ? el('div', { className: 'sap-candidateGroup' },
3218
+ el('p', { className: 'sap-candidateGroupTitle' }, ZH.candidateGroupChanges),
3219
+ el('ul', { className: 'sap-candidateChanges' },
3220
+ changes.map((change) => el('li', { key: change, className: 'sap-candidateChange' }, change)))) : null,
3221
+ preview !== undefined
3222
+ ? el('div', { className: 'sap-candidateGroup' },
3223
+ el('button', { type: 'button', className: 'sap-candidateSafetyToggle' + (safetyOpen === true ? ' sap-candidateSafetyToggleOpen' : ''), onClick: actions.toggleCandidateSafety(candidate) },
3224
+ ZH.candidateSafetyPrefix + (preview.ok === true ? ZH.candidateSafetyPass : ZH.candidateSafetyFail),
3225
+ el('span', { className: 'sap-candidateSafetyArrow' }, '▸')),
3226
+ safetyOpen === true ? buildCandidateSafetyChecks(el, preview) : null)
3227
+ : el('div', { className: 'sap-candidateGroup' },
3228
+ el('p', { className: 'sap-candidateHint' }, ZH.candidateSafetyPending)),
3229
+ el('div', { className: 'sap-candidateGroup' },
3230
+ el('p', { className: 'sap-candidateGroupTitle' }, ZH.candidateGroupGenerated),
3231
+ candidateGeneratedLine(el, candidate),
3232
+ el('p', { className: 'sap-candidateHint' }, ZH.candidateNotApplicable))) : null)
3233
+ }
3234
+
3235
+ // 生成信息行(最浅灰,两分支模板):有样本 = 生成于 MM-DD · 基于最近 N 次派发 ·
3236
+ // 近期表现 X(满分 100,X=round(加权分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3237
+ // 样本,仅供参考(不显示 0 分);无生成时间省去「生成于」段。
3238
+ function candidateGeneratedLine(el, candidate) {
3239
+ const basis = candidate.basis !== null && typeof candidate.basis === 'object' ? candidate.basis : {}
3240
+ const n = Number.isFinite(basis.n) ? basis.n : 0
3241
+ const score100 = Number.isFinite(basis.score) ? Math.round(basis.score * 100) : 0
3242
+ const generated = formatMonthDayTime(basis.generatedAt)
3243
+ const parts = []
3244
+ if (generated !== '') parts.push(ZH.candidateGeneratedPrefix + generated)
3245
+ if (n >= 1) {
3246
+ parts.push(ZH.candidateSamplesPrefix + n + ZH.candidateSamplesSuffix)
3247
+ parts.push(ZH.candidatePerfPrefix + score100 + ZH.candidatePerfSuffix)
3248
+ } else {
3249
+ parts.push(ZH.candidateNoSample)
3250
+ }
3251
+ return el('p', { className: 'sap-candidateHint' }, parts.join(' · '))
3252
+ }
3253
+
3254
+ // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注两行;有对应方案 =
3255
+ // 灰置[按建议调整方案](统一次级按钮)+ 定位;说明与「生成于…」拆两行不连排。
3256
+ function buildAdviceFoot(el, s, actions, a, profile, candidates) {
3257
+ const basis = (Array.isArray(candidates) && candidates.length > 0 && candidates[0] !== null && typeof candidates[0] === 'object' && candidates[0].basis !== null && typeof candidates[0].basis === 'object') ? candidates[0].basis : {}
3258
+ const generatedAt = formatMonthDayTime(basis.generatedAt)
3259
+ const generatedText = generatedAt !== '' ? ZH.candidateGeneratedPrefix + generatedAt : ''
3260
+ const noteLines = (line) => el('div', { className: 'sap-adviceFootNotes' },
3261
+ el('p', { className: 'sap-adviceFootNote' }, line),
3262
+ generatedText !== '' ? el('p', { className: 'sap-adviceFootNote' }, generatedText) : null)
3263
+ if (profile !== null) {
3264
+ return el('div', { className: 'sap-adviceFoot' },
3265
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled: true, title: ZH.candidateNotApplicable }, ZH.adviceApplyDisabled),
3266
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate),
3267
+ noteLines(ZH.adviceFootNoteMatchedPrefix + (profile.name || profile.id) + '(' + profile.id + ')'))
3268
+ }
3269
+ return el('div', { className: 'sap-adviceFoot' },
3270
+ el('button', { type: 'button', className: 'sap-primaryButton', style: smallBtn, onClick: actions.saveAdviceProfile(a.profileKey) }, ZH.adviceSaveProfile),
3271
+ noteLines(ZH.adviceFootNoteSave))
3272
+ }
3273
+
1422
3274
  // 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
1423
3275
  // 每项「移除」按钮)。增删走 /escape-add /escape-remove,成功后用响应 presets 刷新。
1424
3276
  function buildEscapeSection(el, s, actions) {
1425
3277
  const list = Array.isArray(s.escapePresets) ? s.escapePresets : []
1426
- return el('div', { className: 'sap-rowCard' },
1427
- el('div', { className: 'sap-rowHead' },
1428
- el('span', { className: 'sap-rowIdentity' },
1429
- el('span', { className: 'sap-rowName' }, ZH.escapeTitle)
3278
+ return el('div', { className: 'sap-card sap-cardStatic' },
3279
+ el('div', { className: 'sap-cardHead' },
3280
+ el('div', { className: 'sap-cardTitleBox' },
3281
+ el('div', { className: 'sap-cardTitle' }, ZH.escapeTitle),
3282
+ el('div', { className: 'sap-cardDesc' }, ZH.escapeCardDesc)
1430
3283
  ),
1431
3284
  el('span', { className: 'sap-rowActions' },
1432
3285
  el('label', { className: 'sap-switch', title: '逃生舱开关' },
@@ -1435,31 +3288,250 @@ function buildEscapeSection(el, s, actions) {
1435
3288
  )
1436
3289
  )
1437
3290
  ),
1438
- s.escapeEnabled
1439
- ? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
1440
- : null,
1441
- el('div', { className: 'sap-escapeAdd' },
1442
- el('input', {
1443
- className: 'sap-input',
1444
- style: { flex: '1 1 0', minWidth: '160px' },
1445
- value: s.escapeInput,
1446
- placeholder: ZH.escapeInputPlaceholder,
1447
- onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
1448
- }),
1449
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
1450
- ),
1451
- el('div', { className: 'sap-chips' },
1452
- list.length === 0
1453
- ? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
1454
- : list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
1455
- String(id),
1456
- el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
1457
- ))
3291
+ el('div', { className: 'sap-cardBody' },
3292
+ s.escapeEnabled
3293
+ ? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
3294
+ : null,
3295
+ el('div', { className: 'sap-escapeAdd' },
3296
+ el('input', {
3297
+ className: 'sap-input',
3298
+ style: { flex: '1 1 0', minWidth: '160px' },
3299
+ value: s.escapeInput,
3300
+ placeholder: ZH.escapeInputPlaceholder,
3301
+ onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
3302
+ }),
3303
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
3304
+ ),
3305
+ el('div', { className: 'sap-chips' },
3306
+ list.length === 0
3307
+ ? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
3308
+ : list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
3309
+ String(id),
3310
+ el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
3311
+ ))
3312
+ )
3313
+ )
3314
+ )
3315
+ }
3316
+
3317
+ // 设置页卡片骨架:折叠卡 = 标题 + 一句话描述 + SVG chevron(固定 14px,展开旋转 180°);
3318
+ // 静态卡无 chevron,内容常驻展示。折叠的唯一理由是内容多占空间。
3319
+ function buildSettingsCard(el, key, title, desc, open, toggle, body, isStatic) {
3320
+ const titleBox = el('div', { className: 'sap-cardTitleBox' },
3321
+ el('div', { className: 'sap-cardTitle' }, title),
3322
+ el('div', { className: 'sap-cardDesc' }, desc)
3323
+ )
3324
+ const head = isStatic === true
3325
+ ? el('div', { className: 'sap-cardHead' }, titleBox)
3326
+ : el('button', { type: 'button', className: 'sap-cardHead sap-cardToggle', 'aria-expanded': open, onClick: () => toggle(key) }, titleBox,
3327
+ el('svg', { className: 'sap-cardChevron', viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' },
3328
+ el('path', { d: 'M 4 6l4 4 4-4' })
3329
+ )
3330
+ )
3331
+ const cardClass = isStatic === true ? 'sap-card sap-cardStatic' : 'sap-card' + (open === true ? ' sap-cardOpen' : '')
3332
+ return el('div', { className: cardClass }, head,
3333
+ isStatic === true || open === true ? el('div', { className: 'sap-cardBody' }, body) : null
3334
+ )
3335
+ }
3336
+
3337
+ // 审计健康状态行:health + 审计丢失(health 唯一位置;丢失 >0 红字)。目录/告警
3338
+ // 计数不重复(版本告警正文在维护页签,一个数字只出现一次);现挂在安全页签审计健康卡。
3339
+ function buildStatusBar(el, s) {
3340
+ const degraded = s.audit.health === 'degraded'
3341
+ const lost = s.audit.lostTelemetry + s.audit.lostGovernance
3342
+ return el('div', { className: 'sap-statusBar' },
3343
+ el('span', { className: degraded ? 'sap-statusBad sap-statusBadFail' : 'sap-statusBad' }, degraded ? '健康 ⚠ 已降级' : '健康 ✅'),
3344
+ el('span', { className: lost > 0 ? 'sap-statusBadFail' : 'sap-statusDim' }, ZH.statusLost + ' ' + lost + (lost > 0 ? ' 条(请检查磁盘)' : ' 条'))
3345
+ )
3346
+ }
3347
+
3348
+ function buildSectionNotices(el, s) {
3349
+ return [
3350
+ !s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
3351
+ s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
3352
+ s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
3353
+ s.persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${s.persistWarning}${ZH.persistFailHint}`) : null
3354
+ ]
3355
+ }
3356
+
3357
+ // draft 安全检查预览结果:ok 全绿/存在拒绝,逐项展示 verdict + reason。
3358
+ // 检查项名走 CHECK_ZH 人话映射(可见面零内部术语),title 保留原串供排查。
3359
+ function buildDraftPreview(el, preview) {
3360
+ const checks = Array.isArray(preview.checks) ? preview.checks : []
3361
+ return el('div', { className: 'sap-draftPreview' },
3362
+ el('p', { className: preview.ok ? 'sap-savedNotice' : 'sap-warn', role: 'status' }, preview.ok ? ZH.draftPreviewPass : ZH.draftPreviewFail),
3363
+ el('ul', { className: 'sap-rows' },
3364
+ checks.map((c) => el('li', { key: String(c.name), className: 'sap-rowCard' },
3365
+ el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName', title: c.name }, (c.verdict === 'pass' ? '✅ ' : '❌ ') + (CHECK_ZH[c.name] || c.name))),
3366
+ c.reason ? el('p', { className: 'sap-desc' }, c.reason) : null
3367
+ ))
3368
+ )
3369
+ )
3370
+ }
3371
+
3372
+ // draft 列表(设置页 [1] 内):每个 draft 可预览过闸结果、应用或删除。
3373
+ function buildDraftRows(el, s, actions) {
3374
+ if (!Array.isArray(s.drafts) || s.drafts.length === 0) return null
3375
+ return el('div', { className: 'sap-drafts' },
3376
+ el('div', { className: 'sap-draftTitle' }, '待确认 draft'),
3377
+ el('ul', { className: 'sap-rows' },
3378
+ s.drafts.map((draft) => {
3379
+ const preview = s.draftPreviews && typeof s.draftPreviews === 'object' ? s.draftPreviews[draft.id] : undefined
3380
+ return el('li', { key: String(draft.id), className: 'sap-rowCard' },
3381
+ el('div', { className: 'sap-rowHead' },
3382
+ el('span', { className: 'sap-rowIdentity' },
3383
+ el('span', { className: 'sap-rowName' }, draft.name || draft.id),
3384
+ el('span', { className: 'sap-rowId' }, draft.config && draft.config.id || '')
3385
+ ),
3386
+ el('span', { className: 'sap-rowActions' },
3387
+ el('button', { className: 'sap-secondaryButton', style: smallBtn, onClick: actions.previewDraft(draft) }, ZH.draftPreview),
3388
+ el('button', { className: 'sap-secondaryButton', style: smallBtn, onClick: actions.applyDraft(draft) }, '应用'),
3389
+ el('button', { className: 'sap-dangerButton', onClick: actions.removeDraft(draft) }, '删除')
3390
+ )
3391
+ ),
3392
+ draft.description !== '' ? el('p', { className: 'sap-desc' }, draft.description) : null,
3393
+ preview !== undefined ? buildDraftPreview(el, preview) : null
3394
+ )
3395
+ })
1458
3396
  )
1459
3397
  )
1460
3398
  }
1461
3399
 
1462
- function buildSectionReturn(el, s, actions, rows, addCard) {
3400
+ // 全局成本数据行:只展示「全部会话」口径,不带内部技术词;各会话明细另行走会话页。
3401
+ function buildCostGlobalCard(el, global) {
3402
+ if (global === null || typeof global !== 'object') return null
3403
+ return el('p', { className: 'sap-costLine' },
3404
+ [
3405
+ global.priced > 0 ? '全部会话估算 ≈' + global.estimated_cost + ' 元' : '',
3406
+ ZH.costInherit + ' ≈' + global.estimated_inherit_cost + ' 元',
3407
+ ZH.costSaving + ' ≈' + global.estimated_saving + ' 元',
3408
+ ZH.costPriced + ' ' + global.priced + ' 次',
3409
+ ZH.costInheritRatio + ' ' + Math.round((global.inherit_ratio ?? 0) * 100) + '%'
3410
+ ].filter(Boolean).join(' · ')
3411
+ )
3412
+ }
3413
+
3414
+ function buildCostByModel(el, byModel, profiles, actions) {
3415
+ if (!Array.isArray(byModel) || byModel.length === 0) return null
3416
+ return el('div', null,
3417
+ el('div', { className: 'sap-draftTitle' }, ZH.costByModel),
3418
+ el('ul', { className: 'sap-rows' },
3419
+ byModel.map((m) => {
3420
+ const profile = Array.isArray(profiles) ? profiles.find((p) => p && p.model === m.model) : undefined
3421
+ return el('li', { key: String(m.model), className: 'sap-rowCard' },
3422
+ el('div', { className: 'sap-rowHead' },
3423
+ el('span', { className: 'sap-rowIdentity' },
3424
+ el('span', { className: 'sap-rowName' }, String(m.model)),
3425
+ el('span', { className: 'sap-rowId' }, m.deployments + ' 次')
3426
+ ),
3427
+ el('span', { className: 'sap-rowActions' },
3428
+ profile !== undefined
3429
+ ? el('button', { className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.costEditProfile)
3430
+ : el('span', { className: 'sap-metaValueDefault' }, ZH.costNoProfile)
3431
+ )
3432
+ ),
3433
+ el('p', { className: 'sap-costLine' },
3434
+ [
3435
+ m.avg_cost !== null && m.avg_cost !== undefined ? '平均 ≈' + m.avg_cost + ' 元/次' : '',
3436
+ m.priced > 0 ? '估算 ≈' + m.estimated_cost + ' 元' : ''
3437
+ ].filter(Boolean).join(' · '))
3438
+ )
3439
+ })
3440
+ )
3441
+ )
3442
+ }
3443
+
3444
+ function buildCostObservabilitySection(el, s, actions) {
3445
+ const summary = s.costSummary
3446
+ const global = summary && typeof summary.global === 'object' ? summary.global : null
3447
+ const byModel = summary && Array.isArray(summary.byModel) ? summary.byModel : []
3448
+ const globalBlock = buildCostGlobalCard(el, global)
3449
+ const modelBlock = buildCostByModel(el, byModel, s.profiles, actions)
3450
+ const scopeGuide = el('p', { className: 'sap-metaValueDefault' }, '各会话的成本明细见会话页「派发决策台账」')
3451
+ if (globalBlock === null && modelBlock === null) {
3452
+ return el('div', null,
3453
+ el('p', { className: 'sap-desc' }, '估算成本按已记录的派发统计得出——记录较少时数值可能不准,会随使用逐步修正。'),
3454
+ scopeGuide
3455
+ )
3456
+ }
3457
+ return el('div', { className: 'sap-costSection' },
3458
+ globalBlock,
3459
+ modelBlock,
3460
+ scopeGuide
3461
+ )
3462
+ }
3463
+
3464
+ // 六页签骨架:横向 tab(下划线激活 + 加粗,样式对齐官方「插件」节)+ 内容区。
3465
+ function buildTabBar(el, s) {
3466
+ return el('div', { className: 'sap-tabs', role: 'tablist' },
3467
+ SETTINGS_TABS.map((tab) => el('button', {
3468
+ key: tab.key, type: 'button', role: 'tab', 'aria-selected': s.activeTab === tab.key,
3469
+ className: 'sap-tab' + (s.activeTab === tab.key ? ' sap-tabActive' : ''),
3470
+ onClick: () => s.setActiveTab(tab.key)
3471
+ }, tab.label))
3472
+ )
3473
+ }
3474
+
3475
+ // 页签内容区:全部挂载、非激活隐藏(组件状态不因切换丢失)。
3476
+ function buildTabZone(el, s, key, children) {
3477
+ return el('div', { role: 'tabpanel', className: 'sap-zone' + (s.activeTab === key ? ' sap-zoneActive' : '') }, children)
3478
+ }
3479
+
3480
+ // 安全页签:三卡一律 sap-card(与①方案管理同款)——安全保证、逃生舱、
3481
+ // 审计健康(状态行 + 只读审计明细折叠表,明细表类 = sap-audit*)。
3482
+ function buildSafetySection(el, s, actions, reminders) {
3483
+ return el('div', null,
3484
+ buildSettingsCard(el, 5, ZH.safetyCardTitle, ZH.safetyCardDesc, true, () => {}, [
3485
+ el('p', { className: 'sap-desc' }, ZH.advancedSafetyLine),
3486
+ el('p', { className: 'sap-desc' }, ZH.safetyCheckNote)
3487
+ ], true),
3488
+ buildEscapeSection(el, s, actions),
3489
+ buildSettingsCard(el, 6, ZH.auditCardTitle, ZH.auditCardDesc, true, () => {}, [
3490
+ buildStatusBar(el, s),
3491
+ buildAuditDetails(el, s, actions, reminders)
3492
+ ], true)
3493
+ )
3494
+ }
3495
+
3496
+ // 审计明细事件与结果人话(与提醒同源;未知类别原样回退,信息不删)。
3497
+ function auditEventText(r) {
3498
+ const text = ZH.auditEventZh[r.kind] || r.kind
3499
+ return r.severity === 'P0' ? text + '(紧急)' : text
3500
+ }
3501
+
3502
+ function auditResultText(r) {
3503
+ if (typeof r.action === 'string' && r.action !== '') {
3504
+ const time = formatMonthDayTime(r.actedAt)
3505
+ return ZH.auditResultActedPrefix + (ZH.reminderActionZh[r.action] || r.action) + (time !== '' ? ' ' + time : '')
3506
+ }
3507
+ return r.unread === true ? ZH.auditResultPending : ZH.auditResultRead
3508
+ }
3509
+
3510
+ // 审计明细折叠表(只读展示,无导出/过滤):最近 20 条治理留痕。
3511
+ function buildAuditDetails(el, s, actions, reminders) {
3512
+ const rows = Array.isArray(reminders) ? reminders.slice(0, 20) : []
3513
+ const cell = (r) => el('tr', { key: String(r.id) },
3514
+ el('td', { className: 'sap-auditTime' }, formatMonthDayTime(r.createdAt)),
3515
+ el('td', { className: 'sap-auditEvent' }, auditEventText(r)),
3516
+ el('td', { className: 'sap-auditObject' }, r.title + ' · ' + (typeof r.sessionId === 'string' && r.sessionId !== '' ? '会话 ' + r.sessionId.slice(0, 8) + '…' : ZH.reminderGlobal)),
3517
+ el('td', null, auditResultText(r)))
3518
+ return el('div', null,
3519
+ el('button', { type: 'button', className: 'sap-auditCollapse', 'aria-expanded': s.auditDetailOpen === true, onClick: actions.toggleAuditDetail },
3520
+ el('span', null, ZH.auditDetailToggle),
3521
+ el('span', { className: 'sap-auditArrow' }, '▸')),
3522
+ s.auditDetailOpen !== true ? null : (rows.length === 0
3523
+ ? el('p', { className: 'sap-candidateHint' }, ZH.auditDetailEmpty)
3524
+ : [el('p', { className: 'sap-candidateHint' }, ZH.auditDetailScope),
3525
+ el('table', { className: 'sap-auditTable' },
3526
+ el('thead', null, el('tr', null,
3527
+ el('th', null, ZH.auditColTime), el('th', null, ZH.auditColEvent),
3528
+ el('th', null, ZH.auditColObject), el('th', null, ZH.auditColResult))),
3529
+ el('tbody', null, rows.map(cell)))])
3530
+ )
3531
+ }
3532
+
3533
+ function buildSectionReturn(el, s, actions, rows, addCard, reminderCenter, sessions) {
3534
+ const notices = buildSectionNotices(el, s)
1463
3535
  return el('div', { className: 'sap-section' },
1464
3536
  el('div', { className: 'sap-titleRow' },
1465
3537
  el('h2', { className: 'sap-title' }, '子 Agent 方案'),
@@ -1468,80 +3540,119 @@ function buildSectionReturn(el, s, actions, rows, addCard) {
1468
3540
  el('span', null, s.enabled ? '已启用' : '已禁用')
1469
3541
  )
1470
3542
  ),
1471
- s.versionWarnings.length > 0
1472
- ? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
1473
- : null,
1474
- s.audit.health === 'degraded'
1475
- ? el('p', { className: 'sap-notice', role: 'alert' },
1476
- `${ZH.auditDegraded}(${ZH.auditLost}:派发遥测 ${s.audit.lostTelemetry} · 治理 ${s.audit.lostGovernance})`)
1477
- : null,
1478
- el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
1479
- el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
1480
- el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.resetAll }, '重置所有内置方案')
1481
- ),
1482
- buildAdviceSection(el, s, actions),
1483
- buildEscapeSection(el, s, actions),
1484
- !s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
1485
- s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
1486
- s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
1487
- s.persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${s.persistWarning}${ZH.persistFailHint}`) : null,
1488
- el('ul', { className: 'sap-rows' }, rows),
1489
- el('div', { className: 'sap-addBlock' },
1490
- s.adding ? addCard : el('div', { className: 'sap-addActions' },
1491
- el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
1492
- )
1493
- )
3543
+ ...notices,
3544
+ buildTabBar(el, s),
3545
+ buildTabZone(el, s, 'profiles',
3546
+ buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3547
+ el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3548
+ el('ul', { className: 'sap-rows' }, rows),
3549
+ buildDraftRows(el, s, actions),
3550
+ el('div', { className: 'sap-addBlock' },
3551
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
3552
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3553
+ )
3554
+ ),
3555
+ el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3556
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
3557
+ )
3558
+ ], true)),
3559
+ buildTabZone(el, s, 'cost',
3560
+ buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, () => {}, [
3561
+ buildCostObservabilitySection(el, s, actions)
3562
+ ], true)),
3563
+ buildTabZone(el, s, 'advice', buildAdvicePanel(el, s, actions, sessions)),
3564
+ buildTabZone(el, s, 'safety', buildSafetySection(el, s, actions, s.auditReminders)),
3565
+ buildTabZone(el, s, 'reminders', reminderCenter)
1494
3566
  )
1495
3567
  }
1496
3568
 
1497
- function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
3569
+ // draft 预览/应用/删除动作挂到设置页 actions(保持 ProfilesSection 短小)。
3570
+ function attachDraftActions(api, s, refresh, applyWrite, actions) {
3571
+ actions.previewDraft = (draft) => () => {
3572
+ s.setError('')
3573
+ api('/draft/preview', { config: draft.config, name: draft.name, description: draft.description, source: draft.source })
3574
+ .then((data) => {
3575
+ const previews = { ...(s.draftPreviews || {}) }
3576
+ previews[draft.id] = { ok: data.ok === true, checks: Array.isArray(data.checks) ? data.checks : [] }
3577
+ s.setDraftPreviews(previews)
3578
+ })
3579
+ .catch((err) => s.setError(String((err && err.message) || err)))
3580
+ }
3581
+ actions.applyDraft = (draft) => () => {
3582
+ s.setError('')
3583
+ api('/draft/apply', { id: draft.id, name: draft.name, description: draft.description })
3584
+ .then((data) => {
3585
+ applyWrite(data, '已应用 draft:' + draft.name)
3586
+ // 闭环可视化:应用成功后展开方案管理区并高亮新方案卡片。
3587
+ if (typeof data.id === 'string' && data.id !== '') {
3588
+ s.setActiveTab('profiles')
3589
+ s.setHighlightId(data.id)
3590
+ setTimeout(() => s.setHighlightId(null), 8000)
3591
+ }
3592
+ refresh()
3593
+ })
3594
+ .catch((err) => s.setError(String((err && err.message) || err)))
3595
+ }
3596
+ actions.removeDraft = (draft) => () => {
3597
+ s.setError('')
3598
+ api('/draft/remove', { id: draft.id })
3599
+ .then((data) => { applyWrite(data, '已删除 draft'); refresh() })
3600
+ .catch((err) => s.setError(String((err && err.message) || err)))
3601
+ }
3602
+ }
3603
+
3604
+ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, getSessions) {
1498
3605
  function ProfilesSection() {
1499
- const [profiles, setProfiles] = useState([])
1500
- const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] })
1501
- const [enabled, setEnabled] = useState(true)
1502
- const [adding, setAdding] = useState(false)
1503
- const [editingId, setEditingId] = useState(null)
1504
- const [error, setError] = useState('')
1505
- const [savedNotice, setSavedNotice] = useState('')
1506
- const [persistWarning, setPersistWarning] = useState('')
1507
- const [form, setForm] = useState(EMPTY_FORM)
1508
- const [tools, setTools] = useState([])
1509
- const [versionWarnings, setVersionWarnings] = useState([])
1510
- const [audit, setAudit] = useState(AUDIT_DEFAULTS)
1511
- const [escapeEnabled, setEscapeEnabled] = useState(false)
1512
- const [escapePresets, setEscapePresets] = useState([])
1513
- const [escapeInput, setEscapeInput] = useState('')
1514
- const [evolutionAdvice, setEvolutionAdvice] = useState(false)
1515
- const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice }
3606
+ const [profiles, setProfiles] = useState([]); const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] }); const [enabled, setEnabled] = useState(true)
3607
+ const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState('')
3608
+ const [savedNotice, setSavedNotice] = useState(''); const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM)
3609
+ const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([]); const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS)
3610
+ const [escapeEnabled, setEscapeEnabled] = useState(false); const [escapePresets, setEscapePresets] = useState([]); const [escapeInput, setEscapeInput] = useState('')
3611
+ const [evolutionAdvice, setEvolutionAdvice] = useState(false); const [drafts, setDrafts] = useState([]); const [advice, setAdvice] = useState([]); const [evolutionCandidates, setEvolutionCandidates] = useState([])
3612
+ const [adviceGlobal, setAdviceGlobal] = useState(null); const [costSummary, setCostSummary] = useState({ global: null, byModel: [] }); const [draftPreviews, setDraftPreviews] = useState({})
3613
+ const [candidateOpen, setCandidateOpen] = useState({}); const [candidateSelected, setCandidateSelected] = useState({}); const [candidateSafetyOpen, setCandidateSafetyOpen] = useState({})
3614
+ const [candidateSettings, setCandidateSettings] = useState({ candidateMode: 'auto', candidateTtlH: 24 }); const [candidateSettingsOpen, setCandidateSettingsOpen] = useState(false)
3615
+ const [candidateSettingsForm, setCandidateSettingsForm] = useState({ candidateMode: 'auto', candidateTtlChoice: '24', customDays: '', error: '' }); const [adviceDetails, setAdviceDetails] = useState({}); const [detailOpen, setDetailOpen] = useState({})
3616
+ const [activeTab, setActiveTab] = useState('profiles'); const [highlightId, setHighlightId] = useState(null)
3617
+ const [adviceOpen, setAdviceOpen] = useState({}); const [auditDetailOpen, setAuditDetailOpen] = useState(false); const [auditReminders, setAuditReminders] = useState([])
3618
+ const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, evolutionCandidates, adviceGlobal, costSummary, draftPreviews, candidateOpen, candidateSelected, candidateSafetyOpen, candidateSettings, candidateSettingsOpen, candidateSettingsForm, adviceDetails, detailOpen, activeTab, highlightId, adviceOpen, auditDetailOpen, auditReminders, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setEvolutionCandidates, setAdviceGlobal, setCostSummary, setDraftPreviews, setCandidateOpen, setCandidateSelected, setCandidateSafetyOpen, setCandidateSettings, setCandidateSettingsOpen, setCandidateSettingsForm, setAdviceDetails, setDetailOpen, setActiveTab, setHighlightId, setAdviceOpen, setAuditDetailOpen, setAuditReminders }
1516
3619
  const refresh = useCallback(() => {
1517
3620
  api('/list')
1518
- .then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
3621
+ .then((data) => {
3622
+ setProfiles(Array.isArray(data.profiles) ? data.profiles : [])
3623
+ setAdvice(Array.isArray(data.advice) ? data.advice : [])
3624
+ setEvolutionCandidates(Array.isArray(data.evolutionCandidates) ? data.evolutionCandidates : [])
3625
+ setAdviceGlobal(data.adviceGlobal !== null && typeof data.adviceGlobal === 'object' ? data.adviceGlobal : null)
3626
+ setAdviceDetails(data.adviceDetails !== null && typeof data.adviceDetails === 'object' ? data.adviceDetails : {})
3627
+ 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: [] })
3628
+ })
1519
3629
  .catch((err) => setError(String((err && err.message) || err)))
3630
+ api('/drafts')
3631
+ .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
3632
+ .catch(() => {})
3633
+ api('/reminders').then((data) => setAuditReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
1520
3634
  }, [])
1521
3635
  const loadOptions = useCallback(() => {
1522
3636
  api('/options/summary')
1523
- .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice }))
3637
+ .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice, candidateSettings: setCandidateSettings }))
1524
3638
  .catch(() => {})
1525
3639
  }, [])
1526
- const loadEfforts = makeEffortsLoader(api, setOptions)
1527
- const loadTools = makeToolsLoader(api, setTools)
1528
- // 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变导致设置页打开期间无限重取。
1529
- const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings)(), [])
3640
+ const loadEfforts = makeEffortsLoader(api, setOptions); const loadTools = makeToolsLoader(api, setTools)
3641
+ const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), []) // 只挂载时拉一次:稳定引用防 effect 依赖恒变
1530
3642
  useEffect(() => {
1531
3643
  refresh()
1532
3644
  loadOptions()
1533
3645
  loadVersions()
1534
3646
  }, [refresh, loadOptions, loadVersions])
1535
- const applyWrite = makeApplyWrite(s)
1536
- const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
1537
- const rows = buildProfileRows(el, chip, s, actions)
1538
- const toolSection = buildToolSection(el, s, actions)
1539
- return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection))
3647
+ const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
3648
+ attachDraftActions(api, s, refresh, applyWrite, actions)
3649
+ const rows = buildProfileRows(el, chip, s, actions); const toolSection = buildToolSection(el, s, actions)
3650
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), el(ReminderCenter), sessionsSnapshot(getSessions))
1540
3651
  }
1541
3652
  return ProfilesSection
1542
3653
  }
1543
3654
 
1544
- // ── 派发账本:会话标签页(React 依赖经参数注入)────────────────────────────
3655
+ // ── 派发决策台账:会话标签页(React 依赖经参数注入)──────────────────────────
1545
3656
 
1546
3657
  // epoch ms → 本地 HH:MM:SS;非有限数返回空串。
1547
3658
  function formatClockTime(time) {
@@ -1577,25 +3688,21 @@ function gateLabelZh(name) {
1577
3688
  return GATE_LABELS[name] || name
1578
3689
  }
1579
3690
 
1580
- // 闸名标签(中文 + 英文技术标识,供时间线闸头展示)。
1581
- function gateLabel(name) {
1582
- const zh = GATE_LABELS[name]
1583
- return zh !== undefined ? `${zh}(${name})` : name
1584
- }
1585
-
1586
3691
  // 失败闸修复方向;未知闸给通用提示。
1587
3692
  function gateFixHint(name) {
1588
3693
  return GATE_FIX_HINTS[name] || ZH.gateFixUnknown
1589
3694
  }
1590
3695
 
1591
- // 派发方式 → 摘要行 chip 文案。
1592
- function kindLabel(kind) {
1593
- if (kind === 'background') return ZH.toolBackground
1594
- if (kind === 'continuable') return ZH.cont
1595
- if (kind === 'foreground') return ZH.toolForeground
1596
- return kind
3696
+ // 失败闸的可操作目标:优先该记录的方案 id(成本/交集闸可去设置页改该方案),
3697
+ // 其次 whitelist 闸的请求预设(逃生舱/白名单语境)。无目标返回空串。
3698
+ function gateFixTarget(record, gate) {
3699
+ const profileId = recordProfile(record)
3700
+ if (profileId !== '') return profileId
3701
+ const requestedPreset = gate && gate.input && typeof gate.input.requestedPreset === 'string' ? gate.input.requestedPreset : ''
3702
+ return requestedPreset
1597
3703
  }
1598
3704
 
3705
+
1599
3706
  // 记录档位:effective.tokenTier 优先,回退 result.fields.tokenTier(旧记录)。
1600
3707
  function recordTier(record) {
1601
3708
  const trace = record && record.trace
@@ -1615,22 +3722,6 @@ function recordModel(record) {
1615
3722
  if (result && result.fields && typeof result.fields.model === 'string') return result.fields.model
1616
3723
  return ''
1617
3724
  }
1618
-
1619
- // 记录派发方式:execution.kind → request 标志 → result.kind 依次回退。
1620
- function recordKind(record) {
1621
- const trace = record && record.trace
1622
- const result = record && record.result
1623
- const request = record && record.request
1624
- const kind = trace && trace.execution && typeof trace.execution.kind === 'string' ? trace.execution.kind : ''
1625
- if (kind !== '') return kind
1626
- if (request && request.continuable) return 'continuable'
1627
- if (request && request.runInBackground) return 'background'
1628
- if (result && result.kind === 'background') return 'background'
1629
- if (result && result.kind === 'continuable') return 'continuable'
1630
- return 'foreground'
1631
- }
1632
-
1633
- // 记录五段分解:trace.settled.childUsage 优先,回退 result.fields.childUsage。
1634
3725
  function recordUsage(record) {
1635
3726
  const trace = record && record.trace
1636
3727
  const result = record && record.result
@@ -1646,7 +3737,7 @@ function recordUsage(record) {
1646
3737
 
1647
3738
  // 逐次调用明细:读 trace.settled.calls({calls:[{五段}], truncated, totalCalls}),
1648
3739
  // 逐字段 typeof 守卫、坏条目跳过;无 calls 或全坏返回 null(整区不渲染)。calls 仅
1649
- // 账本展开区专用,不进 render 行 / 模型可见面。
3740
+ // 台账展开区专用,不进 render 行 / 模型可见面。
1650
3741
  function recordCalls(record) {
1651
3742
  const settled = record && record.trace && record.trace.settled
1652
3743
  const raw = settled && settled.calls
@@ -1686,7 +3777,7 @@ function ledgerKey(record, index) {
1686
3777
  return `ledger-${index}`
1687
3778
  }
1688
3779
 
1689
- // ── 账本渲染 helpers(纯元素组装)──────────────────────────────────────────
3780
+ // ── 台账渲染 helpers(纯元素组装)──────────────────────────────────────────
1690
3781
 
1691
3782
  // 汇总带单条统计段:label 次级色 + 值加粗,段间「·」分隔。
1692
3783
  function summarySegments(el, pairs) {
@@ -1712,7 +3803,7 @@ function buildLedgerSummary(el, summary, profileNames) {
1712
3803
  .filter((name) => summary.gateRejectCounts[name] > 0)
1713
3804
  .map((name) => `${gateLabelZh(name)} ${summary.gateRejectCounts[name]}`)
1714
3805
  const profileText = summary.byProfile.map((item) => `${profileDisplay(item.profile, profileNames)} ×${item.count}`).join(' · ')
1715
- // 模型原文显示;继承桶(空串)与 'inherit'/'(parent)'/以 '(' 开头的标记一律归一为「继承父」。
3806
+ // 模型原文显示;继承桶(空串)与 'inherit'/'(parent)'/以 '(' 开头的标记一律归一为「继承父会话」。
1716
3807
  const modelText = summary.byModel.map((item) => {
1717
3808
  const display = item.model === 'inherit' || item.model.startsWith('(') ? ZH.inherit : rawOrInherit(item.model)
1718
3809
  return `${display} ×${item.count}`
@@ -1732,6 +3823,18 @@ function buildLedgerSummary(el, summary, profileNames) {
1732
3823
  : null,
1733
3824
  tierText !== ''
1734
3825
  ? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByTier}:${tierText}`)
3826
+ : null,
3827
+ summary.pricedCount > 0
3828
+ ? el('div', { className: 'sap-ledgerSummaryLine' },
3829
+ ZH.ledgerCostLine
3830
+ .replace('{actual}', String(summary.estimatedCost))
3831
+ .replace('{counter}', String(summary.estimatedInheritCost))
3832
+ .replace('{saving}', String(summary.estimatedSaving))
3833
+ .replace('{ratio}', summary.estimatedSaving > 0 && summary.estimatedCost > 0 ? String(Number((summary.estimatedInheritCost / summary.estimatedCost).toFixed(2))) : '1'))
3834
+ : null,
3835
+ summary.inheritCount > 0
3836
+ ? el('div', { className: 'sap-ledgerSummaryLine' },
3837
+ ZH.ledgerInheritCost.replace('{count}', String(summary.inheritCount)).replace('{ratio}', String(summary.inheritRatio)))
1735
3838
  : null
1736
3839
  )
1737
3840
  }
@@ -1754,7 +3857,7 @@ function buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder) {
1754
3857
  }, loadingOlder ? ZH.ledgerLoadingOlder : ZH.ledgerLoadOlder)
1755
3858
  }
1756
3859
 
1757
- // 账本顶部说明行:定位说明(恒显)+ 窗口提示(hasMore 时显示已加载条数)。
3860
+ // 台账顶部说明行:定位说明(恒显)+ 窗口提示(hasMore 时显示已加载条数)。
1758
3861
  function buildLedgerTopNote(el, hasMore, count) {
1759
3862
  return el('div', null,
1760
3863
  el('div', { className: 'sap-ledgerScope' }, ZH.ledgerScopeNote),
@@ -1771,6 +3874,12 @@ function formatGateScalar(v) {
1771
3874
  return String(v)
1772
3875
  }
1773
3876
 
3877
+ // 父会话 UUID 截断:前 8 位 + …,悬停 title 显示全文(不足 8 位原样,fail-soft)。
3878
+ function truncateSessionId(v) {
3879
+ if (typeof v !== 'string' || v === '') return v
3880
+ return v.length > 8 ? v.slice(0, 8) + '…' : v
3881
+ }
3882
+
1774
3883
  // 对象/数组 → 逐字段键值行数组(depth 上限 2 防深层爆炸);标量 → 单行文本。
1775
3884
  // 对象逐键「中文标签(键名): 值」;嵌套容器递归 1 层并包缩进块(sap-ledgerGateNested,
1776
3885
  // 左侧竖线视觉分层,profiles_snapshot 等嵌套结构可读);数组前 5 项逐项展示,
@@ -1798,7 +3907,7 @@ function gateValueRows(el, value, depth = 0) {
1798
3907
  }
1799
3908
  const rows = []
1800
3909
  for (const [key, v] of Object.entries(value)) {
1801
- const label = GATE_KEY_LABELS[key] !== undefined ? `${GATE_KEY_LABELS[key]}(${key})` : key
3910
+ const label = FIELD_ZH[key] !== undefined ? `${FIELD_ZH[key]}(${key})` : key
1802
3911
  if (v !== null && typeof v === 'object' && depth < 2) {
1803
3912
  rows.push(el('div', { className: 'sap-ledgerGateKV' },
1804
3913
  el('span', { className: 'sap-ledgerGateKey' }, label), ':'))
@@ -1806,7 +3915,9 @@ function gateValueRows(el, value, depth = 0) {
1806
3915
  } else {
1807
3916
  rows.push(el('div', { className: 'sap-ledgerGateKV' },
1808
3917
  el('span', { className: 'sap-ledgerGateKey' }, label), ':',
1809
- el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(v))))
3918
+ key === 'parentSessionId' && typeof v === 'string' && v !== ''
3919
+ ? el('span', { className: 'sap-ledgerGateValue', title: v }, truncateSessionId(v))
3920
+ : el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(v))))
1810
3921
  }
1811
3922
  }
1812
3923
  return rows
@@ -1847,17 +3958,237 @@ function buildJsonBody(el, kv) {
1847
3958
  )
1848
3959
  }
1849
3960
 
1850
- // 决策时间线步骤 ①:发起时间 + 父会话上下文 + 请求参数(各为独立子块卡片;
1851
- // 头部右侧按钮切换 字段视图 原始 JSON)。
1852
- function buildStartStep(el, trace, jsonView, toggleJson) {
3961
+
3962
+ // ── 三层信息架构(结论层 / 证据层)纯渲染 ─────────────────────────
3963
+
3964
+ // 停滞检测阈值(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
3965
+ const STALL_THRESHOLD_MS = 30000
3966
+
3967
+ // 折叠态状态 chip:拒绝(红)/执行中(琥珀脉冲)/异常(琥珀)/成功(绿)。
3968
+ function ledgerStatusOf(record) {
3969
+ if (recordIsRejected(record)) return { cls: 'sap-ledgerStatusFail', label: ZH.ledgerStatusReject }
3970
+ const settled = record.trace && record.trace.settled
3971
+ if (record.trace && !settled) return { cls: 'sap-ledgerStatusRun', label: ZH.ledgerStatusRunning }
3972
+ if (settled) {
3973
+ const stop = recordStopZh(record)
3974
+ if (stop !== '' && stop !== ZH.stopReasonZh.completed) return { cls: 'sap-ledgerStatusRun', label: ZH.ledgerStatusAbnormal + '(' + stop + ')' }
3975
+ }
3976
+ return { cls: 'sap-ledgerStatusOk', label: ZH.ledgerStatusSuccess }
3977
+ }
3978
+
3979
+ // 折叠态工具数:continuable 交集实得数优先,其次子会话快照推导(实际调用过的工具)。
3980
+ function ledgerToolsCountOf(record, proc) {
3981
+ if (recordIsRejected(record)) return ZH.ledgerToolsPending
3982
+ const n = effectiveToolsCountOf(record)
3983
+ if (n !== null) return ZH.toolAllowlist + ' ' + n
3984
+ if (proc !== null && proc.toolsUsed !== null) return ZH.toolAllowlist + ' ' + proc.toolsUsed.length
3985
+ return ''
3986
+ }
3987
+
3988
+ // 执行过程事件线(台账记事实不记判断):从子会话快照节点推导——
3989
+ // 开始/工具调用/停滞/主 agent 干预/恢复/完成。干预 = 非 seed 的 user 节点(内容
3990
+ // 只取 40 字摘要,不落原文)。nodes 不可得返回空事件集(fail-soft 不虚报)。
3991
+ function processEventsOf(nodes, settled) {
3992
+ const out = { events: [], toolCalls: 0, interventions: 0, stalled: false, toolsUsed: null }
3993
+ if (!Array.isArray(nodes) || nodes.length === 0) return out
3994
+ const used = new Set()
3995
+ const first = nodes[0] && typeof nodes[0].time === 'number' ? nodes[0].time : null
3996
+ out.events.push({ kind: 'start', time: first })
3997
+ let lastActivity = null
3998
+ let stallOpen = false
3999
+ let sawUser = false
4000
+ for (let i = 0; i < nodes.length; i += 1) {
4001
+ const node = nodes[i]
4002
+ if (!node || typeof node !== 'object') continue
4003
+ const time = typeof node.time === 'number' ? node.time : null
4004
+ if (node.kind === 'tool-result') {
4005
+ const name = node.call && typeof node.call.name === 'string' ? node.call.name : ''
4006
+ if (name !== '') used.add(name)
4007
+ out.toolCalls += 1
4008
+ if (lastActivity !== null && time !== null && time - lastActivity > STALL_THRESHOLD_MS) {
4009
+ out.events.push({ kind: 'stall', time, seconds: Math.round((time - lastActivity) / 1000) })
4010
+ out.stalled = true
4011
+ stallOpen = true
4012
+ }
4013
+ if (stallOpen) { out.events.push({ kind: 'resume', time }); stallOpen = false }
4014
+ out.events.push({ kind: 'call', time, index: out.toolCalls, tool: name })
4015
+ lastActivity = time !== null ? time : lastActivity
4016
+ } else if (node.kind === 'user') {
4017
+ if (sawUser) {
4018
+ out.interventions += 1
4019
+ out.events.push({ kind: 'intervene', time, text: firstContentLine(node.content).slice(0, 40) })
4020
+ lastActivity = time !== null ? time : lastActivity
4021
+ }
4022
+ sawUser = true
4023
+ }
4024
+ }
4025
+ out.events.push(settled !== null && settled !== undefined ? { kind: 'done', time: null } : { kind: 'running', time: null })
4026
+ out.toolsUsed = [...used]
4027
+ return out
4028
+ }
4029
+
4030
+ // 结论层进度点:拒绝只显示到达阶段;运行中执行琥珀脉冲;结算异常结算琥珀。
4031
+ function progressStepsOf(record, proc) {
4032
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
4033
+ const gateFailed = gates.some((g) => g.verdict === 'fail')
4034
+ const settled = record.trace && record.trace.settled
4035
+ if (recordIsRejected(record)) {
4036
+ return [
4037
+ { key: 'start', label: ZH.ledgerProgressZh.start, state: 'done' },
4038
+ { key: 'decide', label: ZH.ledgerProgressZh.decide, state: gateFailed ? 'warn' : 'done' },
4039
+ { key: 'stop', label: ZH.ledgerProgressZh.stop, state: gateFailed ? 'warn' : 'done' }
4040
+ ]
4041
+ }
4042
+ if (record.trace && !settled) {
4043
+ return [
4044
+ { key: 'start', label: ZH.ledgerProgressZh.start, state: 'done' },
4045
+ { key: 'decide', label: ZH.ledgerProgressZh.decide, state: 'done' },
4046
+ { key: 'run', label: ZH.ledgerProgressZh.run, state: 'run' },
4047
+ { key: 'settle', label: ZH.ledgerProgressZh.settle, state: 'todo' }
4048
+ ]
4049
+ }
4050
+ const stop = recordStopZh(record)
4051
+ const settleState = stop !== '' && stop !== ZH.stopReasonZh.completed ? 'warn' : 'done'
4052
+ const runState = proc !== null && proc.stalled ? 'warn' : 'done'
4053
+ return [
4054
+ { key: 'start', label: ZH.ledgerProgressZh.start, state: 'done' },
4055
+ { key: 'decide', label: ZH.ledgerProgressZh.decide, state: 'done' },
4056
+ { key: 'run', label: ZH.ledgerProgressZh.run, state: runState },
4057
+ { key: 'settle', label: ZH.ledgerProgressZh.settle, state: settleState }
4058
+ ]
4059
+ }
4060
+
4061
+ // 安全检查五道闸徽标汇总:全过(5/5)单徽标;有失败展开五项。
4062
+ function safetyBadgesOf(record) {
4063
+ const names = ['whitelist', 'cost', 'intersection', 'approval', 'budget']
4064
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
4065
+ const badges = []
4066
+ let failedName = ''
4067
+ for (const name of names) {
4068
+ const gate = gates.find((g) => g.name === name)
4069
+ const verdict = gate ? gate.verdict : ''
4070
+ if (verdict === 'fail') { badges.push({ name, ok: false }); if (failedName === '') failedName = name } else if (verdict === 'pass' || verdict === 'deferred') badges.push({ name, ok: true })
4071
+ }
4072
+ const allPass = badges.length === names.length && failedName === ''
4073
+ return { allPass, failedName, badges }
4074
+ }
4075
+
4076
+ // 结论层:安全检查徽标(全过单徽标;失败展开五项,❌ 项红色)。
4077
+ function buildSafetyBadges(el, record) {
4078
+ const s = safetyBadgesOf(record)
4079
+ if (s.allPass) return el('div', { className: 'sap-ledgerSafety' },
4080
+ el('span', { className: 'sap-ledgerBadge sap-ledgerBadgeOk' }, ZH.ledgerSafetyAllPass)
4081
+ )
4082
+ return el('div', { className: 'sap-ledgerSafety' },
4083
+ el('span', { className: 'sap-ledgerBadge sap-ledgerBadgeFail' }, ZH.ledgerSafetyFail.replace('{gate}', ZH.ledgerGateBadgeZh[s.failedName] || s.failedName)),
4084
+ s.badges.map((b) => el('span', {
4085
+ key: 'gate-badge-' + b.name,
4086
+ className: b.ok ? 'sap-ledgerBadge sap-ledgerBadgeOk' : 'sap-ledgerBadge sap-ledgerBadgeFail'
4087
+ }, (ZH.ledgerGateBadgeZh[b.name] || b.name) + ' ' + (b.ok ? '✓' : '❌')))
4088
+ )
4089
+ }
4090
+
4091
+ // 结论层:进度点一行(发起 → 决策 → 执行 → 结算)。
4092
+ function buildProgressDots(el, record, proc) {
4093
+ const steps = progressStepsOf(record, proc)
4094
+ const items = []
4095
+ steps.forEach((step, i) => {
4096
+ items.push(el('span', { key: step.key, className: 'sap-ledgerProgressStep' },
4097
+ el('span', { className: 'sap-ledgerProgressDot sap-ledgerProgressDot-' + step.state }),
4098
+ el('span', null, step.label)
4099
+ ))
4100
+ if (i < steps.length - 1) items.push(el('span', { key: step.key + '-sep', className: 'sap-ledgerProgressSep' }))
4101
+ })
4102
+ return el('div', { className: 'sap-ledgerProgress' }, items)
4103
+ }
4104
+
4105
+ // 结论层:成本卡(本会话口径 · ≈X 元 · 若继承父会话 ≈Y 元 · 省 ≈Z 元 · 模型/强度摘要)。
4106
+ function buildCostCard(el, record) {
4107
+ const info = recordCostInfo(record)
4108
+ if (!info) return null
4109
+ const eff = record.trace && record.trace.effective ? record.trace.effective : {}
4110
+ const summary = ZH.chipModel + ':' + rawOrInherit(eff.model) + ' · ' + ZH.chipEffort + ':' + effortZh(eff.reasoningEffort)
4111
+ if (info.inherit) {
4112
+ return el('div', { className: 'sap-ledgerCostCard' },
4113
+ el('span', null, ZH.ledgerCostCardScope, ' · ', el('b', null, '≈' + info.actual + ' 元')),
4114
+ el('span', null, ZH.ledgerSettleInheritNote),
4115
+ el('span', null, summary)
4116
+ )
4117
+ }
4118
+ return el('div', { className: 'sap-ledgerCostCard' },
4119
+ el('span', null, ZH.ledgerCostCardScope, ' · ', el('b', null, '≈' + info.actual + ' 元')),
4120
+ el('span', null, ZH.ledgerCostCardCounter, ' ', el('b', null, '≈' + info.counterfactual + ' 元')),
4121
+ el('span', { className: 'sap-ledgerCostSave' }, ZH.ledgerCostCardSave, ' ', el('b', null, '≈' + info.saving + ' 元')),
4122
+ el('span', null, summary)
4123
+ )
4124
+ }
4125
+
4126
+ // 结论层:拒绝块(拒绝:原因 + 修复方向可点击复制目标)。
4127
+ function rejectGateOf(record) {
4128
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
4129
+ return gates.find((g) => g.verdict === 'fail') || null
4130
+ }
4131
+ function buildRejectBlock(el, record, copiedTarget, onCopyTarget) {
4132
+ const gate = rejectGateOf(record)
4133
+ if (gate === null && record.isError !== true) return null
4134
+ const reason = gate !== null && typeof gate.reason === 'string' && gate.reason !== '' ? gate.reason : ZH.ledgerReject
4135
+ const hint = gate !== null ? gateFixHint(gate.name) : ZH.gateFixUnknown
4136
+ const fixTarget = gate !== null ? gateFixTarget(record, gate) : ''
4137
+ return el('div', { className: 'sap-ledgerRejectBlock' },
4138
+ el('div', { className: 'sap-ledgerReject' }, ZH.ledgerReject + ':' + reason),
4139
+ el('div', { className: 'sap-ledgerRejectFix' },
4140
+ ZH.ledgerRejectFix + ':' + hint,
4141
+ fixTarget !== ''
4142
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: onCopyTarget(fixTarget) }, copiedTarget === fixTarget ? ZH.ledgerCopied : ZH.ledgerCopyTarget + ' ' + fixTarget)
4143
+ : null
4144
+ )
4145
+ )
4146
+ }
4147
+
4148
+ // 结论层:干预摘要(仅存在干预事件时显示)。
4149
+ function buildInterventionSummary(el, record, proc) {
4150
+ if (proc === null || proc.interventions === 0) return null
4151
+ const lead = proc.stalled ? '执行中出现停滞' : '执行过程'
4152
+ return el('div', { className: 'sap-ledgerIntervention' }, lead + ',主 agent 干预 ' + proc.interventions + ' 次')
4153
+ }
4154
+
4155
+ // 结论层(点行展开):进度点 → 安全检查徽标 → 成本卡 → 拒绝块 → 干预摘要 → 因果链入口。
4156
+ function buildConclusionArea(el, record, proc, copiedTarget, onCopyTarget, evidenceOpen, setEvidence) {
4157
+ return el('div', { className: 'sap-ledgerExpand' },
4158
+ buildProgressDots(el, record, proc),
4159
+ buildSafetyBadges(el, record),
4160
+ buildCostCard(el, record),
4161
+ buildRejectBlock(el, record, copiedTarget, onCopyTarget),
4162
+ buildInterventionSummary(el, record, proc),
4163
+ el('button', { type: 'button', className: 'sap-ledgerEvidenceToggle', onClick: () => setEvidence(!evidenceOpen) }, evidenceOpen ? ZH.ledgerEvidenceClose : ZH.ledgerEvidenceToggle)
4164
+ )
4165
+ }
4166
+
4167
+ // 请求参数人话格式('=' 仅用于本行键值,非内部键残留):模型=pro · 强度=未指定 · 档位=cheap。
4168
+ function requestHumanText(requested) {
4169
+ const req = requested || {}
4170
+ const model = typeof req.model === 'string' && req.model !== '' ? req.model : ZH.ledgerVsUnset
4171
+ const effort = typeof req.reasoningEffort === 'string' && req.reasoningEffort !== '' ? effortZh(req.reasoningEffort) : ZH.ledgerVsUnset
4172
+ const tier = typeof req.tokenTier === 'string' && req.tokenTier !== '' ? (ZH.tierZh[req.tokenTier] || req.tokenTier) : ZH.ledgerVsUnset
4173
+ return ZH.chipModel + '=' + model + ' · ' + ZH.chipEffort + '=' + effort + ' · ' + ZH.chipTier + '=' + tier
4174
+ }
4175
+
4176
+ // 证据层 ① 发起:时间 · 父会话(UUID 截断,悬停全文)· 请求参数 · 任务指令(派发前)。
4177
+ // 任务指令取 trace.requested.prompt_excerpt(与任务摘要同源 200 字符截断);旧记录
4178
+ // 无摘要时不渲染该行,绝不拿结果块首行冒充指令。
4179
+ function buildStartBlock(el, record, jsonView, toggleJson) {
4180
+ const trace = record.trace
1853
4181
  const startedText = formatClockTime(trace.startedAt)
1854
- const head = startedText !== '' ? `${ZH.ledgerStarted} · ${startedText}` : ZH.ledgerStarted
1855
- const jsonMode = jsonView['start'] === true
4182
+ const head = startedText !== '' ? ZH.cogPerceiveDecide + ' · ' + startedText : ZH.cogPerceiveDecide
4183
+ const jsonMode = jsonView.start === true
4184
+ const execution = trace.execution || {}
4185
+ const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
4186
+ const promptExcerpt = trace.requested && typeof trace.requested.prompt_excerpt === 'string' && trace.requested.prompt_excerpt !== '' ? trace.requested.prompt_excerpt : ''
1856
4187
  const kv = [
1857
4188
  { label: ZH.ledgerParentContext, value: trace.parentContext },
1858
4189
  { label: ZH.ledgerRequested, value: trace.requested }
1859
4190
  ]
1860
- return el('div', { key: 'step-start', className: 'sap-ledgerStep' },
4191
+ return el('div', { key: 'block-start', className: 'sap-ledgerStep' },
1861
4192
  el('div', { className: 'sap-ledgerStepHead' },
1862
4193
  el('span', null, head),
1863
4194
  buildViewToggle(el, jsonMode, () => toggleJson('start'))
@@ -1865,109 +4196,284 @@ function buildStartStep(el, trace, jsonView, toggleJson) {
1865
4196
  jsonMode
1866
4197
  ? buildJsonBody(el, kv)
1867
4198
  : el('div', { className: 'sap-ledgerStepBody' },
4199
+ parentSessionId !== ''
4200
+ ? el('div', { className: 'sap-ledgerGateKV' },
4201
+ el('span', { className: 'sap-ledgerGateKey' }, ZH.ledgerParentSessionLabel), ':',
4202
+ el('span', { className: 'sap-ledgerGateValue', title: parentSessionId }, truncateSessionId(parentSessionId)))
4203
+ : null,
4204
+ el('div', { className: 'sap-ledgerGateKV' },
4205
+ el('span', { className: 'sap-ledgerGateKey' }, ZH.ledgerRequestHuman), ':',
4206
+ el('span', { className: 'sap-ledgerGateValue' }, requestHumanText(trace.requested))),
4207
+ el('div', { className: 'sap-ledgerSource' }, '来源:系统(父会话快照)'),
1868
4208
  buildSubBlock(el, ZH.ledgerParentContext, trace.parentContext),
1869
- buildSubBlock(el, ZH.ledgerRequested, trace.requested)
1870
- )
1871
- )
1872
- }
1873
-
1874
- // 单道闸三段式:闸头(名称 + verdict + 视图切换)→ 输入/输出子块(或合并 JSON 视图)
1875
- // → 结论;失败闸浅红底 + 原因 + 修复方向。拒绝信息两种视图下都显示。
1876
- function buildGateStep(el, gate, index, jsonView, toggleJson) {
1877
- const failed = gate.verdict === 'fail'
1878
- const passed = gate.verdict === 'pass'
1879
- const verdict = passed ? `✅ ${ZH.ledgerPass}` : (failed ? `❌ ${ZH.ledgerReject}` : gate.verdict)
1880
- const verdictCls = failed ? 'sap-ledgerGateVerdictFail' : (passed ? 'sap-ledgerGateVerdictPass' : '')
1881
- const key = `gate-${index}`
1882
- const jsonMode = jsonView[key] === true
1883
- const kv = [
1884
- { label: ZH.ledgerGateInput, value: gate.input },
1885
- { label: ZH.ledgerGateOutput, value: gate.output }
1886
- ]
1887
- const rejectRows = failed
1888
- ? [
1889
- el('div', { key: 'reject', className: 'sap-ledgerReject' }, gate.reason !== '' ? `${ZH.ledgerReject}:${gate.reason}` : ZH.ledgerReject),
1890
- el('div', { key: 'rejectFix', className: 'sap-ledgerRejectFix' }, `${ZH.ledgerRejectFix}:${gateFixHint(gate.name)}`)
1891
- ]
1892
- : []
1893
- return el('div', { key, className: failed ? 'sap-ledgerGate sap-ledgerGateFail' : 'sap-ledgerGate' },
1894
- el('div', { className: 'sap-ledgerGateHead' },
1895
- el('span', { className: 'sap-ledgerGateName' }, gateLabel(gate.name)),
1896
- el('span', { className: `sap-ledgerGateVerdict ${verdictCls}` }, verdict),
1897
- buildViewToggle(el, jsonMode, () => toggleJson(key))
1898
- ),
1899
- jsonMode
1900
- ? el('div', { className: 'sap-ledgerGateBody' }, buildJsonBody(el, kv), ...rejectRows)
1901
- : el('div', { className: 'sap-ledgerGateBody' },
1902
- buildSubBlock(el, ZH.ledgerGateInput, gate.input),
1903
- buildSubBlock(el, ZH.ledgerGateOutput, gate.output),
1904
- ...rejectRows
4209
+ el('div', { className: 'sap-ledgerSource' }, '来源:用户/父上下文(输入原文摘要)'),
4210
+ buildSubBlock(el, ZH.ledgerRequested, trace.requested),
4211
+ promptExcerpt !== ''
4212
+ ? el('div', { className: 'sap-cogParentText' }, ZH.ledgerPromptQuote + ':『' + promptExcerpt + '』')
4213
+ : null
1905
4214
  )
1906
4215
  )
1907
4216
  }
1908
4217
 
1909
- // 决策时间线:① 发起 + 各闸。步骤元素带稳定 key(静态列表也满足 React key 契约)。
1910
- // jsonView/toggleJson:区块级「字段视图 ↔ 原始 JSON」切换状态(LedgerRow 持有)。
1911
- function buildDecisionTimeline(el, record, jsonView, toggleJson) {
4218
+ // 生效工具数:continuable 交集实得数 effective.effectiveToolCount 未知 null。
4219
+ function effectiveToolsCountOf(record) {
1912
4220
  const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
1913
- const blocks = [buildStartStep(el, record.trace, jsonView, toggleJson)]
1914
- gates.forEach((gate, index) => blocks.push(buildGateStep(el, gate, index, jsonView, toggleJson)))
1915
- return el('div', { className: 'sap-ledgerSection' },
1916
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerDecisionTimeline),
1917
- el('div', { className: 'sap-ledgerTimeline' }, blocks)
1918
- )
4221
+ const inter = gates.find((g) => g.name === 'intersection')
4222
+ if (inter && inter.output && Number.isFinite(inter.output.effectiveAllowCount)) return inter.output.effectiveAllowCount
4223
+ const eff = record.trace && record.trace.effective
4224
+ if (eff && Number.isFinite(eff.effectiveToolCount)) return eff.effectiveToolCount
4225
+ return null
1919
4226
  }
1920
4227
 
1921
- // 生效值 chips + ⬆被忽略项(沿用既有 ignored chip 风格)。
1922
- function buildEffectiveSection(el, chip, record, profileNames) {
4228
+ // 证据层 决策 chips 行(中文对照,忽略项键名中文化)。
4229
+ function buildEffectiveChips(el, chip, record, profileNames) {
1923
4230
  const eff = record.trace.effective || {}
1924
- // 拒绝路径无生效值(闸 fail 即中止派发)——整节省略。
1925
4231
  if (Object.keys(eff).length === 0) return null
1926
4232
  const requested = record.trace.requested || {}
1927
4233
  const chips = []
1928
4234
  chips.push(chip('profile', ZH.chipProfile, profileDisplay(eff.profile, profileNames)))
1929
- chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: !eff.preset }))
4235
+ chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: !eff.preset || eff.preset === 'inherit' }))
1930
4236
  const p = rawOrInherit(eff.provider)
1931
4237
  const m = rawOrInherit(eff.model)
1932
- chips.push(chip('providerModel', `${ZH.chipProvider}·${ZH.chipModel}`, p === ZH.inherit && m === ZH.inherit ? ZH.inherit : `${p} / ${m}`, { dim: p === ZH.inherit && m === ZH.inherit }))
1933
- chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: !eff.reasoningEffort }))
4238
+ 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 }))
4239
+ chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: !eff.reasoningEffort || eff.reasoningEffort === '(default)' }))
1934
4240
  const tier = typeof eff.tokenTier === 'string' && eff.tokenTier !== '' ? eff.tokenTier : ''
1935
4241
  if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
4242
+ const toolsCount = effectiveToolsCountOf(record)
4243
+ if (toolsCount !== null) chips.push(chip('tools', ZH.toolAllowlist, String(toolsCount)))
1936
4244
  const ignored = Array.isArray(eff.ignored) ? eff.ignored : []
1937
4245
  for (const key of ignored) {
1938
4246
  const reqVal = typeof requested[key] === 'string' && requested[key] !== '' ? requested[key] : ''
1939
4247
  chips.push(el('span', {
1940
- key: `ignored-${key}`,
4248
+ key: 'ignored-' + key,
1941
4249
  className: 'sap-chip sap-chipWarn',
1942
4250
  title: reqVal !== '' ? ZH.ignoredChipTitle.replace('{value}', reqVal) : ZH.ignoredChipTitleEmpty
1943
- }, `⬆${key}(${ZH.ignoredMark})`))
4251
+ }, '⬆' + (FIELD_ZH[key] || key) + '(' + ZH.ignoredMark + ')'))
4252
+ }
4253
+ chips.push(...buildAuditChips(el, record.trace))
4254
+ return el('div', { className: 'sap-chips' }, chips)
4255
+ }
4256
+
4257
+ // 请求 vs 生效对照表单行:差异标在生效侧,说明列写原因;继承表述「继承父会话(模型名)」。
4258
+ function requestVsRowOf(key, label, requested, effective) {
4259
+ const ignored = Array.isArray(effective.ignored) ? effective.ignored : []
4260
+ const reqRaw = typeof requested[key] === 'string' ? requested[key] : ''
4261
+ const effRaw = typeof effective[key] === 'string' ? effective[key] : ''
4262
+ const zhOf = (v) => {
4263
+ if (v === '') return ZH.ledgerVsUnset
4264
+ if (key === 'reasoningEffort') return effortZh(v)
4265
+ if (key === 'tokenTier') return ZH.tierZh[v] || v
4266
+ return v
4267
+ }
4268
+ const reqText = zhOf(reqRaw)
4269
+ let effText = zhOf(effRaw)
4270
+ let note = ''
4271
+ let diff = false
4272
+ if (ignored.includes(key)) {
4273
+ diff = true
4274
+ effText = ZH.inherit
4275
+ note = ZH.ledgerVsIgnored.replace('{v}', reqText)
4276
+ } else if (key === 'model' && reqRaw === '' && effRaw !== '') {
4277
+ effText = ZH.ledgerVsInherit.replace('{model}', effRaw)
4278
+ } else if (reqRaw === '' && effRaw !== '') {
4279
+ note = ZH.ledgerVsDefault
4280
+ } else if (reqRaw !== '' && effRaw !== '' && reqRaw !== effRaw) {
4281
+ diff = true
4282
+ note = '请求了 ' + reqText + ',未生效'
4283
+ }
4284
+ return { key, label, request: reqText, effective: effText, note, diff }
4285
+ }
4286
+
4287
+ // 证据层 ② 请求 vs 生效对照表(差异行微琥珀底;全一致折叠为「请求全部生效 ✓」)。
4288
+ function buildRequestVsTable(el, record) {
4289
+ const trace = record.trace
4290
+ const requested = trace.requested || {}
4291
+ const effective = trace.effective || {}
4292
+ if (Object.keys(effective).length === 0) return null
4293
+ const rows = [
4294
+ requestVsRowOf('model', ZH.chipModel, requested, effective),
4295
+ requestVsRowOf('reasoningEffort', ZH.chipEffort, requested, effective),
4296
+ requestVsRowOf('tokenTier', ZH.chipTier, requested, effective)
4297
+ ]
4298
+ const diffCount = rows.filter((r) => r.diff).length
4299
+ const headText = diffCount === 0
4300
+ ? ZH.ledgerVsTitle + ' · ' + ZH.ledgerVsAllMatch
4301
+ : ZH.ledgerVsTitle + ' · ' + ZH.ledgerVsDiff.replace('{n}', String(diffCount))
4302
+ const table = el('table', { className: 'sap-ledgerVsTable' },
4303
+ el('thead', null, el('tr', null,
4304
+ el('th', null, ZH.ledgerVsItem),
4305
+ el('th', null, ZH.ledgerVsRequest),
4306
+ el('th', null, ZH.ledgerVsEffective),
4307
+ el('th', null, ZH.ledgerVsNote)
4308
+ )),
4309
+ el('tbody', null, rows.map((r) => el('tr', { key: r.key, className: r.diff ? 'sap-ledgerVsDiff' : null },
4310
+ el('td', null, r.label),
4311
+ el('td', null, r.request),
4312
+ el('td', null, el('span', { className: 'sap-ledgerVsEff' }, r.effective)),
4313
+ el('td', { className: 'sap-ledgerVsNote' }, r.note)
4314
+ )))
4315
+ )
4316
+ if (diffCount === 0) {
4317
+ return el('details', { className: 'sap-customized' },
4318
+ el('summary', { className: 'sap-customizedSummary' }, headText),
4319
+ el('div', { className: 'sap-customizedBody' }, table)
4320
+ )
1944
4321
  }
1945
- return el('div', { className: 'sap-ledgerSection' },
1946
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerEffectiveStep),
1947
- el('div', { className: 'sap-chips' }, chips)
4322
+ return el('div', null,
4323
+ el('div', { className: 'sap-ledgerVsHead' }, headText),
4324
+ table
4325
+ )
4326
+ }
4327
+
4328
+ // 白名单候选数(whitelist 闸 input,截断名单认 values)。
4329
+ function whitelistCountOf(record) {
4330
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
4331
+ const wl = gates.find((g) => g.name === 'whitelist')
4332
+ const raw = wl && wl.input ? wl.input.systemTrustCandidates : null
4333
+ if (Array.isArray(raw)) return raw.length
4334
+ if (raw && typeof raw === 'object' && Array.isArray(raw.values)) return raw.values.length
4335
+ return null
4336
+ }
4337
+ function intersectionGateOf(record) {
4338
+ const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
4339
+ return gates.find((g) => g.name === 'intersection') || null
4340
+ }
4341
+ function removedToolsOf(record) {
4342
+ const inter = intersectionGateOf(record)
4343
+ if (!inter || !inter.output) return null
4344
+ const raw = inter.output.removedTools
4345
+ if (Array.isArray(raw)) return raw
4346
+ if (raw && typeof raw === 'object' && Array.isArray(raw.values)) return raw.values
4347
+ return null
4348
+ }
4349
+ // 工具名「英文(中文名)」(中文名查工具目录映射,查不到原样)。
4350
+ function toolNameZh(name, toolZh) {
4351
+ const zh = toolZh !== null && typeof toolZh === 'object' ? toolZh[name] : undefined
4352
+ return typeof zh === 'string' && zh !== '' ? name + '(' + zh + ')' : name
4353
+ }
4354
+
4355
+ // 证据层 ② 工具集子节(默认折叠):白名单 X ∩ 父会话 Y → 实得 Z · 移除 N 个;
4356
+ // 移除清单完整展示(表格滚动兜底);非 continuable 诚实降级。
4357
+ function buildToolsetSubSection(el, record, toolZh) {
4358
+ const x = whitelistCountOf(record)
4359
+ const inter = intersectionGateOf(record)
4360
+ const y = inter && inter.input ? inter.input.parentToolCount : null
4361
+ const z = effectiveToolsCountOf(record)
4362
+ const removed = removedToolsOf(record)
4363
+ const zText = z !== null ? String(z) : ZH.ledgerToolsetGranted
4364
+ const formula = ZH.ledgerToolsetFormula
4365
+ .replace('{x}', x !== null ? String(x) : '?')
4366
+ .replace('{y}', y !== undefined && y !== null ? String(y) : '?')
4367
+ .replace('{z}', zText)
4368
+ const removedText = removed !== null && removed.length > 0 ? ' · ' + ZH.ledgerToolsetRemoved.replace('{n}', String(removed.length)) : ''
4369
+ const body = removed !== null && removed.length > 0
4370
+ ? el('div', { className: 'sap-ledgerToolScroll' },
4371
+ el('table', { className: 'sap-ledgerToolTable' },
4372
+ el('thead', null, el('tr', null, el('th', null, ZH.ledgerToolCol), el('th', null, ZH.ledgerToolReasonCol))),
4373
+ el('tbody', null, removed.map((item) => el('tr', { key: item.name },
4374
+ el('td', { className: 'sap-ledgerToolName' }, toolNameZh(item.name, toolZh)),
4375
+ el('td', null, typeof item.reason === 'string' ? item.reason : '')
4376
+ )))
4377
+ ))
4378
+ : el('div', { className: 'sap-ledgerUsage' }, ZH.ledgerToolsetHostNarrow)
4379
+ return el('details', { className: 'sap-customized' },
4380
+ el('summary', { className: 'sap-customizedSummary' }, ZH.ledgerToolsetTitle + ' · ' + formula + removedText),
4381
+ el('div', { className: 'sap-customizedBody' }, body)
4382
+ )
4383
+ }
4384
+
4385
+ // 证据层 ② 派发前 · 安全检查子节(默认折叠;失败默认展开并附拒绝块)。
4386
+ function buildPrecheckSubSection(el, record, copiedTarget, onCopyTarget) {
4387
+ const s = safetyBadgesOf(record)
4388
+ const title = s.allPass
4389
+ ? ZH.ledgerPrecheckTitle + ' ✅ 5/5 ' + ZH.ledgerPass
4390
+ : ZH.ledgerPrecheckTitle + ' ' + ZH.ledgerSafetyFail.replace('{gate}', ZH.ledgerGateBadgeZh[s.failedName] || s.failedName)
4391
+ const badges = el('div', { className: 'sap-ledgerSafety' },
4392
+ s.badges.map((b) => el('span', {
4393
+ key: 'precheck-' + b.name,
4394
+ className: b.ok ? 'sap-ledgerBadge sap-ledgerBadgeOk' : 'sap-ledgerBadge sap-ledgerBadgeFail'
4395
+ }, (ZH.ledgerGateBadgeZh[b.name] || b.name) + ' ' + (b.ok ? '✓' : '❌')))
1948
4396
  )
4397
+ const reject = s.failedName !== '' ? buildRejectBlock(el, record, copiedTarget, onCopyTarget) : null
4398
+ return el('details', { className: 'sap-customized', open: !s.allPass },
4399
+ el('summary', { className: 'sap-customizedSummary' }, title),
4400
+ el('div', { className: 'sap-customizedBody' }, badges, reject)
4401
+ )
4402
+ }
4403
+
4404
+ // 证据层 ② 决策 · 生效值:chips(中文对照)+ 决策原文 + 请求vs生效表 + 工具集 + 安全检查。
4405
+ function buildDecisionBlock(el, chip, record, profileNames, toolZh, jsonView, toggleJson, copiedTarget, onCopyTarget) {
4406
+ const trace = record.trace
4407
+ const jsonMode = jsonView.decide === true
4408
+ const kv = [
4409
+ { label: ZH.ledgerEffectiveStep, value: trace.effective },
4410
+ { label: 'gates', value: trace.gates }
4411
+ ]
4412
+ return el('div', { key: 'block-decide', className: 'sap-ledgerStep' },
4413
+ el('div', { className: 'sap-ledgerStepHead' },
4414
+ el('span', null, ZH.cogDecide),
4415
+ buildViewToggle(el, jsonMode, () => toggleJson('decide'))
4416
+ ),
4417
+ jsonMode
4418
+ ? buildJsonBody(el, kv)
4419
+ : el('div', { className: 'sap-ledgerStepBody' },
4420
+ buildEffectiveChips(el, chip, record, profileNames),
4421
+ trace.parentText && trace.parentText.after
4422
+ ? el('div', { className: 'sap-cogParentText' }, ZH.ledgerDecisionQuote + ':『' + trace.parentText.after + '』')
4423
+ : null,
4424
+ buildRequestVsTable(el, record),
4425
+ buildToolsetSubSection(el, record, toolZh),
4426
+ buildPrecheckSubSection(el, record, copiedTarget, onCopyTarget)
4427
+ )
4428
+ )
4429
+ }
4430
+ // 模型选择审计标注 chips:警示色 chip,title 给出确定性判定依据。
4431
+ function buildAuditChips(el, trace) {
4432
+ return auditModelChoice(trace).map((a) => el('span', {
4433
+ key: 'audit-' + a.key,
4434
+ className: 'sap-chip sap-chipWarn',
4435
+ title: a.title
4436
+ }, a.label))
1949
4437
  }
1950
4438
 
1951
- // 结算:stopReason / 耗时 / token + 五段分解(复用 usageBreakdownText)。
1952
- // trace.settled 有值时渲染;settled null(运行中/拒绝)整节省略。
1953
- function buildSettledSection(el, record) {
4439
+
4440
+ // 证据层 结算:结束原因(中文)· 耗时 · tokens · 输出摘要 · 成本分解。
4441
+ function buildSettleBlock(el, record, jsonView, toggleJson, openChild) {
1954
4442
  const settled = record.trace && record.trace.settled
1955
4443
  if (!settled) return null
1956
- const { tokens, elapsedMs } = recordNumbers(record)
4444
+ const jsonMode = jsonView.settle === true
4445
+ const kv = [{ label: ZH.ledgerSettledStep, value: settled }]
4446
+ const numbers = recordNumbers(record)
1957
4447
  const stopZh = recordStopZh(record)
1958
- const usageText = usageBreakdownText(recordUsage(record))
1959
4448
  const parts = []
1960
- if (stopZh !== '') parts.push(`${ZH.ledgerStopReason}=${stopZh}`)
1961
- if (elapsedMs !== null) parts.push(`${ZH.ledgerElapsed}=${formatDurationMs(elapsedMs)}`)
1962
- if (tokens !== null) parts.push(`≈${tokens} tokens`)
1963
- if (usageText !== '') parts.push(usageText)
1964
- if (parts.length === 0) return null
1965
- return el('div', { className: 'sap-ledgerSection' },
1966
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerSettledStep),
1967
- el('div', { className: 'sap-ledgerUsage' }, parts.join(' · '))
4449
+ if (stopZh !== '') parts.push(ZH.ledgerStopReason + ':' + stopZh)
4450
+ if (numbers.elapsedMs !== null) parts.push(ZH.ledgerElapsed + ' ' + formatDurationMs(numbers.elapsedMs))
4451
+ if (numbers.tokens !== null) parts.push('≈' + numbers.tokens + ' tokens')
4452
+ const usageText = usageBreakdownText(recordUsage(record))
4453
+ if (usageText !== '') parts.push(ZH.ledgerSettleCostBreakdown + ':' + usageText)
4454
+ const output = record.contentFirstLine
4455
+ const outputText = output !== '' ? ZH.ledgerSettleOutput + '' + output : ''
4456
+ const execution = (record.trace && record.trace.execution) || {}
4457
+ const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
4458
+ const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
4459
+ const mode = typeof execution.mode === 'string' ? execution.mode : ''
4460
+ const jump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
4461
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: () => openChild({ parentSessionId, childSessionId, mode }) }, ZH.ledgerJumpChild + ' ▸')
4462
+ : null
4463
+ return el('div', { key: 'block-settle', className: 'sap-ledgerStep' },
4464
+ el('div', { className: 'sap-ledgerStepHead' },
4465
+ el('span', null, ZH.cogNewPerceive),
4466
+ buildViewToggle(el, jsonMode, () => toggleJson('settle'))
4467
+ ),
4468
+ jsonMode
4469
+ ? buildJsonBody(el, kv)
4470
+ : el('div', { className: 'sap-ledgerStepBody' },
4471
+ parts.length > 0 ? el('div', { className: 'sap-ledgerUsage' }, parts.join(' · ')) : null,
4472
+ outputText !== '' ? el('div', { className: 'sap-ledgerUsage' }, outputText) : null,
4473
+ jump
4474
+ )
1968
4475
  )
1969
4476
  }
1970
-
1971
4477
  // 逐次调用明细折叠区(默认收起):trace.settled.calls 逐条五段紧凑表格(序号 + 五列,
1972
4478
  // 缺失段显示 —)+ 末行合计(∑已存 calls,未截断时与 childUsage 五段分解对账一致)+
1973
4479
  // 截断提示。无 calls 数据时整区不渲染。
@@ -2016,183 +4522,314 @@ function buildCallDetailSection(el, record) {
2016
4522
  )
2017
4523
  }
2018
4524
 
2019
- // 子会话跳转:地址三字段齐全 →「打开子会话」;不完整 → 显示 id 供手动检索。
2020
- // 后台派发只有 jobId(execute 返回时子会话尚未启动),无子会话可跳,恒走手动检索。
2021
- function buildHierarchyHint(el, record, openChild) {
2022
- const execution = (record.trace && record.trace.execution) || {}
4525
+
4526
+ // 证据层 执行:时间区间 + 工具调用次数 + 过程事件线(停滞/干预/恢复)+ 跳转 + 逐次调用明细。
4527
+ function buildExecutionBlock(el, record, proc, toolZh, jsonView, toggleJson, openChild) {
4528
+ const trace = record.trace
4529
+ const execution = trace.execution || {}
4530
+ if (recordIsRejected(record) && Object.keys(execution).length === 0) return null
4531
+ const jsonMode = jsonView.execute === true
4532
+ const settled = trace.settled
4533
+ const numbers = recordNumbers(record)
4534
+ const startedText = formatClockTime(trace.startedAt)
4535
+ const endTime = settled && typeof settled.elapsedMs === 'number' && trace.startedAt !== null ? trace.startedAt + settled.elapsedMs : null
4536
+ const endText = endTime !== null ? formatClockTime(endTime) : ''
4537
+ const durText = formatDurationMs(numbers.elapsedMs)
4538
+ const interval = startedText !== '' && endText !== ''
4539
+ ? ZH.ledgerExecInterval.replace('{start}', startedText).replace('{end}', endText).replace('{dur}', durText)
4540
+ : (durText !== '' ? ZH.ledgerElapsed + ' ' + durText : '')
4541
+ const callsText = proc !== null && proc.toolCalls > 0 ? ZH.ledgerExecToolCalls.replace('{n}', String(proc.toolCalls)) : ''
2023
4542
  const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
2024
4543
  const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
2025
- const jobId = typeof execution.jobId === 'string' ? execution.jobId : ''
2026
4544
  const mode = typeof execution.mode === 'string' ? execution.mode : ''
2027
- const canJump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
2028
- let body = null
2029
- if (canJump) {
2030
- body = el('button', {
2031
- type: 'button',
2032
- className: 'sap-detailToggle',
2033
- onClick: () => openChild({ parentSessionId, childSessionId, mode })
2034
- }, ZH.ledgerJumpChild)
2035
- } else if (childSessionId !== '') {
2036
- body = el('div', { className: 'sap-ledgerManual' }, `${ZH.ledgerChildManual}:`, el('code', { className: 'sap-ledgerManualId' }, childSessionId))
2037
- } else if (jobId !== '') {
2038
- body = el('div', { className: 'sap-ledgerManual' }, `${ZH.ledgerJobManual}:`, el('code', { className: 'sap-ledgerManualId' }, jobId))
2039
- }
2040
- if (body === null) return null
2041
- return el('div', { className: 'sap-ledgerSection' },
2042
- el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerExecutionStep),
2043
- body
4545
+ const jump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
4546
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: () => openChild({ parentSessionId, childSessionId, mode }) }, ZH.ledgerJumpChild + ' ▸')
4547
+ : (childSessionId !== ''
4548
+ ? el('div', { className: 'sap-ledgerManual' }, ZH.ledgerChildManual + ':', el('code', { className: 'sap-ledgerManualId' }, childSessionId))
4549
+ : null)
4550
+ const kv = [
4551
+ { label: ZH.ledgerExecutionStep, value: execution },
4552
+ { label: 'calls', value: settled && settled.calls ? settled.calls : null }
4553
+ ]
4554
+ return el('div', { key: 'block-execute', className: 'sap-ledgerStep' },
4555
+ el('div', { className: 'sap-ledgerStepHead' },
4556
+ el('span', null, ZH.cogExecute),
4557
+ buildViewToggle(el, jsonMode, () => toggleJson('execute'))
4558
+ ),
4559
+ jsonMode
4560
+ ? buildJsonBody(el, kv)
4561
+ : el('div', { className: 'sap-ledgerStepBody' },
4562
+ interval !== '' ? el('div', { className: 'sap-ledgerUsage' }, interval) : null,
4563
+ callsText !== '' ? el('div', { className: 'sap-ledgerUsage' }, callsText) : null,
4564
+ !settled ? el('div', { className: 'sap-ledgerUsage' }, ZH.ledgerExecLiveNote) : null,
4565
+ proc !== null ? buildProcessTimeline(el, proc, toolZh) : null,
4566
+ jump,
4567
+ buildCallDetailSection(el, record)
4568
+ )
2044
4569
  )
2045
4570
  }
2046
4571
 
2047
- // 记录摘要行 chips:后台/continuable 派发方式 + 方案 + 档位。
2048
- function buildRowChips(el, chip, record, profileNames) {
2049
- const chips = []
2050
- const kind = recordKind(record)
2051
- if (kind === 'background' || kind === 'continuable') {
2052
- chips.push(el('span', { key: 'kind', className: 'sap-chip' }, kindLabel(kind)))
4572
+ // 过程事件线单条:时间 + 文案;停滞琥珀点、干预蓝点、里程碑绿点。
4573
+ function buildProcessEvent(el, event, toolZh) {
4574
+ const time = event.time !== null && event.time !== undefined ? formatClockTime(event.time) : ''
4575
+ const cls = event.kind === 'stall' ? 'sap-ledgerEvent sap-ledgerEvent-warn'
4576
+ : (event.kind === 'intervene' ? 'sap-ledgerEvent sap-ledgerEvent-int' : 'sap-ledgerEvent sap-ledgerEvent-done')
4577
+ const bodyOf = () => {
4578
+ if (event.kind === 'start') return ZH.ledgerEventStart
4579
+ if (event.kind === 'call') return ZH.ledgerEventCall.replace('{n}', String(event.index)) + '(' + (event.tool !== '' ? toolNameZh(event.tool, toolZh) : '?') + ')'
4580
+ if (event.kind === 'stall') return ZH.ledgerEventStall.replace('{s}', String(event.seconds)).replace('{t}', String(STALL_THRESHOLD_MS / 1000))
4581
+ if (event.kind === 'intervene') return ZH.ledgerEventIntervene + ':' + event.text
4582
+ if (event.kind === 'resume') return ZH.ledgerEventResume
4583
+ if (event.kind === 'done') return ZH.ledgerEventDone
4584
+ return ZH.ledgerEventRunning
2053
4585
  }
2054
- const profile = recordProfile(record)
2055
- if (profile !== '') chips.push(chip('profile', ZH.chipProfile, profileDisplay(profile, profileNames)))
2056
- const tier = recordTier(record)
2057
- if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
2058
- return chips
4586
+ const body = bodyOf()
4587
+ return el('div', { key: event.kind + '-' + (event.time !== null ? String(event.time) : 'end'), className: cls },
4588
+ time !== '' ? el('span', { className: 'sap-ledgerEventTime' }, time) : null,
4589
+ event.kind === 'intervene' ? el('span', { className: 'sap-ledgerEventAct' }, body) : body
4590
+ )
2059
4591
  }
2060
-
2061
- // 展开区:旧记录降级提示;新记录 = 决策时间线 → 生效值 → 执行信息
2062
- // 结算 调用级明细。
2063
- function buildExpandArea(el, chip, record, isLegacy, openChild, profileNames, jsonView, toggleJson) {
2064
- if (isLegacy) {
2065
- return el('div', { className: 'sap-ledgerExpand' },
2066
- el('div', { className: 'sap-ledgerLegacy' }, ZH.ledgerLegacy)
2067
- )
4592
+ // 过程事件线(工具调用 >4 次时节选:前 2 + 省略 + 最后 1;停滞/干预/恢复不节选)。
4593
+ function buildProcessTimeline(el, proc, toolZh) {
4594
+ if (proc.events.length === 0) return null
4595
+ const calls = proc.events.filter((e) => e.kind === 'call')
4596
+ const showAll = calls.length <= 4
4597
+ const items = []
4598
+ let skipInserted = false
4599
+ for (const event of proc.events) {
4600
+ if (event.kind === 'call' && !showAll) {
4601
+ const callIdx = calls.indexOf(event)
4602
+ if (callIdx >= 2 && callIdx < calls.length - 1) {
4603
+ if (!skipInserted) { items.push(el('div', { key: 'call-more', className: 'sap-ledgerEvent sap-ledgerEvent-done' }, '…')); skipInserted = true }
4604
+ continue
4605
+ }
4606
+ }
4607
+ items.push(buildProcessEvent(el, event, toolZh))
2068
4608
  }
4609
+ return el('div', null,
4610
+ el('div', { className: 'sap-ledgerVsHead' }, ZH.ledgerStallThresholdNote),
4611
+ el('div', { className: 'sap-ledgerEventLine' }, items)
4612
+ )
4613
+ }
4614
+
4615
+ // 证据层装配:① 发起 → ② 决策 → ③ 执行 → ④ 结算(拒绝场景 ③④ 自动省略)。
4616
+ function buildEvidenceArea(el, chip, record, profileNames, toolZh, proc, jsonView, toggleJson, openChild, copiedTarget, onCopyTarget) {
2069
4617
  return el('div', { className: 'sap-ledgerExpand' },
2070
- buildDecisionTimeline(el, record, jsonView, toggleJson),
2071
- buildEffectiveSection(el, chip, record, profileNames),
2072
- buildHierarchyHint(el, record, openChild),
2073
- buildSettledSection(el, record),
2074
- buildCallDetailSection(el, record)
4618
+ buildStartBlock(el, record, jsonView, toggleJson),
4619
+ buildDecisionBlock(el, chip, record, profileNames, toolZh, jsonView, toggleJson, copiedTarget, onCopyTarget),
4620
+ buildExecutionBlock(el, record, proc, toolZh, jsonView, toggleJson, openChild),
4621
+ buildSettleBlock(el, record, jsonView, toggleJson, openChild)
2075
4622
  )
2076
4623
  }
2077
4624
 
2078
- // ── 账本 maker(React 依赖经参数注入)───────────────────────────────────────
4625
+ // 折叠态行:时间 · 方案中文名 · 状态chip · 成本 · 工具数 · [⚠干预 N] · ▸。
4626
+ function buildLedgerRowHead(el, record, profileNames, proc, open, onToggle) {
4627
+ const status = ledgerStatusOf(record)
4628
+ const rejected = recordIsRejected(record)
4629
+ const settled = record.trace && record.trace.settled
4630
+ const costInfo = recordCostInfo(record)
4631
+ const costText = rejected ? '—' : (!settled ? ZH.ledgerCostRunning : (costInfo !== null ? '≈' + costInfo.actual + ' 元' : ''))
4632
+ const profileId = recordProfile(record)
4633
+ const profileText = profileId !== '' ? profileDisplay(profileId, profileNames) : ZH.inlineProfile
4634
+ const toolsText = ledgerToolsCountOf(record, proc)
4635
+ const interveneText = proc !== null && proc.interventions > 0 ? ZH.ledgerInterventionBadge.replace('{n}', String(proc.interventions)) : ''
4636
+ return el('button', {
4637
+ type: 'button',
4638
+ className: 'sap-ledgerRowHead',
4639
+ 'aria-expanded': open,
4640
+ onClick: onToggle
4641
+ },
4642
+ el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
4643
+ el('span', { className: 'sap-ledgerMeta' }, profileText),
4644
+ el('span', { className: 'sap-ledgerStatus ' + status.cls }, status.label),
4645
+ costText !== '' ? el('span', { className: 'sap-ledgerCostText' }, costText) : null,
4646
+ toolsText !== '' ? el('span', { className: 'sap-ledgerToolsText' }, toolsText) : null,
4647
+ interveneText !== ''
4648
+ ? el('span', { className: 'sap-ledgerBadge sap-ledgerBadgeWarn' }, interveneText)
4649
+ : null,
4650
+ el('span', { className: 'sap-ledgerToggle' }, '▸')
4651
+ )
4652
+ }
4653
+ // 剪贴板复制(fail-soft:无 clipboard API 时仍置反馈状态,目标文本可见可手动复制)。
4654
+ function copyToClipboard(text) {
4655
+ try {
4656
+ if (window.navigator && window.navigator.clipboard && typeof window.navigator.clipboard.writeText === 'function') {
4657
+ window.navigator.clipboard.writeText(text)
4658
+ }
4659
+ } catch { /* 复制失败仍显示目标文本供手动复制 */ }
4660
+ }
4661
+
4662
+ function makeCopyTarget(setCopiedTarget) {
4663
+ return (target) => () => {
4664
+ copyToClipboard(target)
4665
+ setCopiedTarget(target)
4666
+ }
4667
+ }
2079
4668
 
4669
+
4670
+ // ── 台账 maker(React 依赖经参数注入)───────────────────────────────────────
4671
+
4672
+ // 单条记录三层:折叠态行 → 结论层(点行展开)→ 证据层(「查看完整因果链」再展开)。
2080
4673
  function makeLedgerRow(el, chip, useState, openChild) {
2081
- function LedgerRow({ record, profileNames }) {
4674
+ function LedgerRow({ record, profileNames, childNodesOf, toolZh }) {
2082
4675
  const [open, setOpen] = useState(false)
2083
- // 区块级视图切换:jsonView[key] 为 true 时该区块显示原始 JSON(key='start'
2084
- // `gate-<index>`),false/缺省显示字段级子块视图。各区块独立切换。
4676
+ const [evidenceOpen, setEvidenceOpen] = useState(false)
4677
+ // 修复方向复制反馈:点击「复制 <目标>」后按钮短暂显示「已复制」。
4678
+ const [copiedTarget, setCopiedTarget] = useState('')
4679
+ // 区块级视图切换:jsonView[key] 为 true 时该区块显示原始 JSON
4680
+ // (key='start'/'decide'/'execute'/'settle')。各区块独立切换。
2085
4681
  const [jsonView, setJsonView] = useState({})
2086
4682
  const toggleJson = (key) => setJsonView((prev) => ({ ...prev, [key]: !(prev[key] === true) }))
2087
4683
  const isLegacy = record.legacy === true
2088
- const numbers = recordNumbers(record)
2089
4684
  const rejected = recordIsRejected(record)
2090
- const stopZh = rejected ? '' : recordStopZh(record)
2091
- const metaParts = []
2092
- if (numbers.tokens !== null) metaParts.push(`≈${numbers.tokens} tokens`)
2093
- if (numbers.elapsedMs !== null) metaParts.push(`${ZH.ledgerElapsed}=${numbers.elapsedMs}ms`)
2094
- if (stopZh !== '') metaParts.push(`${ZH.ledgerStopReason}=${stopZh}`)
2095
- return el('li', { className: open ? 'sap-ledgerRow sap-ledgerRowOpen' : 'sap-ledgerRow' },
2096
- el('button', {
2097
- type: 'button',
2098
- className: 'sap-ledgerRowHead',
2099
- 'aria-expanded': open,
2100
- onClick: () => setOpen((prev) => !prev)
2101
- },
2102
- el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
2103
- el('span', { className: 'sap-chips' }, buildRowChips(el, chip, record, profileNames)),
2104
- rejected ? el('span', { className: 'sap-ledgerReject' }, `❌ ${ZH.ledgerReject}`) : null,
2105
- el('span', { className: 'sap-ledgerMeta' }, metaParts.join(' · ')),
2106
- el('span', { className: 'sap-ledgerToggle' }, '▸')
2107
- ),
2108
- open ? buildExpandArea(el, chip, record, isLegacy, openChild, profileNames, jsonView, toggleJson) : null
4685
+ const execution = record.trace && record.trace.execution ? record.trace.execution : {}
4686
+ const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
4687
+ const proc = childSessionId !== '' && typeof childNodesOf === 'function'
4688
+ ? processEventsOf(childNodesOf(childSessionId), record.trace && record.trace.settled)
4689
+ : processEventsOf(null, record.trace && record.trace.settled)
4690
+ const copyTarget = makeCopyTarget(setCopiedTarget)
4691
+ return el('li', {
4692
+ className: 'sap-ledgerRow' + (open ? ' sap-ledgerRowOpen' : '') + (rejected ? ' sap-ledgerRowFail' : '')
4693
+ },
4694
+ buildLedgerRowHead(el, record, profileNames, proc, open, () => setOpen((prev) => !prev)),
4695
+ open && isLegacy
4696
+ ? el('div', { className: 'sap-ledgerExpand' }, el('div', { className: 'sap-ledgerLegacy' }, ZH.ledgerLegacy))
4697
+ : null,
4698
+ open && !isLegacy
4699
+ ? buildConclusionArea(el, record, proc, copiedTarget, copyTarget, evidenceOpen, setEvidenceOpen)
4700
+ : null,
4701
+ open && !isLegacy && evidenceOpen
4702
+ ? buildEvidenceArea(el, chip, record, profileNames, toolZh, proc, jsonView, toggleJson, openChild, copiedTarget, copyTarget)
4703
+ : null
2109
4704
  )
2110
4705
  }
2111
4706
  return LedgerRow
2112
4707
  }
2113
4708
 
2114
- function makeDispatchLedgerView(el, chip, useState, useEffect, openChild) {
4709
+
4710
+ // 台账后台任务状态 hook:挂载即拉一次;有待配对后台记录时 3 秒轮询至全部结算。
4711
+ function useLedgerJobs(useState, useEffect, sessionId, pendingJobIds) {
4712
+ const [jobs, setJobs] = useState([])
4713
+ const pendingKey = Array.isArray(pendingJobIds) ? pendingJobIds.join(',') : ''
4714
+ useEffect(() => startLedgerJobsPoll(sessionId, pendingJobIds, setJobs), [sessionId, pendingKey])
4715
+ return jobs
4716
+ }
4717
+
4718
+ // 台账视图数据装载 hook:失败台账 / 后台任务 / 方案名映射 / 工具中文名(name→zh)。
4719
+ function useLedgerViewData(useState, useEffect, sessionId, pendingJobIds) {
4720
+ const [failures, setFailures] = useState([])
4721
+ const jobs = useLedgerJobs(useState, useEffect, sessionId, pendingJobIds)
4722
+ const [profileNames, setProfileNames] = useState({})
4723
+ const [toolZh, setToolZh] = useState({})
4724
+ useEffect(() => {
4725
+ let cancelled = false
4726
+ fetchLedgerFailures(sessionId).then((list) => {
4727
+ if (!cancelled) setFailures(list)
4728
+ })
4729
+ api('/options/tools').then((data) => {
4730
+ if (cancelled) return
4731
+ const map = {}
4732
+ for (const tool of (Array.isArray(data.tools) ? data.tools : [])) {
4733
+ if (tool !== null && typeof tool === 'object' && typeof tool.name === 'string' && typeof tool.zh === 'string' && tool.zh !== '') {
4734
+ map[tool.name] = tool.zh
4735
+ }
4736
+ }
4737
+ setToolZh(map)
4738
+ }).catch(() => {})
4739
+ return () => { cancelled = true }
4740
+ }, [sessionId])
4741
+ useEffect(() => {
4742
+ let cancelled = false
4743
+ api('/list').then((data) => {
4744
+ if (cancelled || data === undefined || !Array.isArray(data.profiles)) return
4745
+ const map = {}
4746
+ for (const profile of data.profiles) {
4747
+ if (profile !== null && typeof profile === 'object' && typeof profile.id === 'string' && typeof profile.name === 'string' && profile.name !== '') {
4748
+ map[profile.id] = profile.name
4749
+ }
4750
+ }
4751
+ setProfileNames(map)
4752
+ }).catch(() => {})
4753
+ return () => { cancelled = true }
4754
+ }, [])
4755
+ return { failures, jobs, profileNames, toolZh }
4756
+ }
4757
+
4758
+ // 记录行列表(含空态)。
4759
+ function buildLedgerRows(el, records, LedgerRow, profileNames, childNodesOf, toolZh) {
4760
+ if (records.length === 0) return buildEmptyState(el)
4761
+ const ordered = records.slice().reverse()
4762
+ return el('ul', { className: 'sap-ledgerRows' },
4763
+ ordered.map((record, index) => el(LedgerRow, { key: ledgerKey(record, index), record, profileNames, childNodesOf, toolZh }))
4764
+ )
4765
+ }
4766
+
4767
+ function makeDispatchLedgerView(el, chip, useState, useEffect, openChild, getSessions) {
2115
4768
  const LedgerRow = makeLedgerRow(el, chip, useState, openChild)
2116
4769
  function DispatchLedgerView({ useSession, sessionId, loadOlder = () => {} }) {
2117
4770
  const nodes = useSession((s) => s?.chat?.legacy?.nodes)
2118
4771
  const hasMore = useSession((s) => s?.hasMore)
2119
4772
  const loadingOlder = useSession((s) => s?.loadingOlder)
2120
- const [failures, setFailures] = useState([])
2121
- const [profileNames, setProfileNames] = useState({})
2122
- useEffect(() => {
2123
- let cancelled = false
2124
- fetchLedgerFailures(sessionId).then((list) => {
2125
- if (!cancelled) setFailures(list)
2126
- })
2127
- return () => { cancelled = true }
2128
- }, [sessionId])
2129
- // profile 注册表 id→中文名映射:账本挂载时拉一次 /list,用于方案名中英双表达
2130
- // (「标准编码(swap-standard)」);拉取失败保留空映射,显示层回退纯 id。
2131
- useEffect(() => {
2132
- let cancelled = false
2133
- api('/list').then((data) => {
2134
- if (cancelled || data === undefined || !Array.isArray(data.profiles)) return
2135
- const map = {}
2136
- for (const profile of data.profiles) {
2137
- if (profile !== null && typeof profile === 'object' && typeof profile.id === 'string' && typeof profile.name === 'string' && profile.name !== '') {
2138
- map[profile.id] = profile.name
2139
- }
2140
- }
2141
- setProfileNames(map)
2142
- }).catch(() => {})
2143
- return () => { cancelled = true }
2144
- }, [])
2145
- // 账本默认只统计已加载窗口;经「加载更早」逐页补载更早记录后 nodes 变多、
2146
- // 统计自然扩大。
2147
- const records = mergeFailureRecords(collectDispatchRecords(nodes), failures)
4773
+ const pendingJobs = pendingBackgroundJobIds(nodes)
4774
+ const data = useLedgerViewData(useState, useEffect, sessionId, pendingJobs)
4775
+ // 子会话快照读取(执行过程事件线数据源);拿不到返回 null(fail-soft 不虚报)。
4776
+ const childNodesOf = (childSessionId) => {
4777
+ try {
4778
+ const child = getSessions().binding(childSessionId)
4779
+ const childNodes = child !== undefined && child !== null && child.session !== undefined ? child.session.chat?.legacy?.nodes : undefined
4780
+ return Array.isArray(childNodes) ? childNodes : null
4781
+ } catch {
4782
+ return null
4783
+ }
4784
+ }
4785
+ // 台账默认只统计已加载窗口;经「加载更早」逐页补载更早记录后 nodes 变多、统计自然扩大。
4786
+ const records = mergeBackgroundJobs(mergeFailureRecords(collectDispatchRecords(nodes), data.failures), data.jobs)
2148
4787
  const summary = records.length === 0 ? null : computeLedgerSummary(records)
2149
- const ordered = records.slice().reverse()
2150
- const rows = records.length === 0
2151
- ? buildEmptyState(el)
2152
- : el('ul', { className: 'sap-ledgerRows' },
2153
- ordered.map((record, index) => el(LedgerRow, { key: ledgerKey(record, index), record, profileNames }))
2154
- )
2155
4788
  return el('div', { className: 'sap-ledger' },
2156
4789
  buildLedgerTopNote(el, hasMore, records.length),
2157
4790
  buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder),
2158
- summary === null ? null : buildLedgerSummary(el, summary, profileNames),
2159
- rows
4791
+ summary === null ? null : buildLedgerSummary(el, summary, data.profileNames),
4792
+ buildLedgerRows(el, records, LedgerRow, data.profileNames, childNodesOf, data.toolZh)
2160
4793
  )
2161
4794
  }
2162
4795
  return DispatchLedgerView
2163
4796
  }
2164
-
2165
4797
  // ── plugin 装配 ─────────────────────────────────────────────────────────────
2166
4798
 
2167
4799
  window.__ModuleLoader__.load({
2168
4800
  id: 'dsh-subagent-profile',
2169
4801
  factory: (require) => {
2170
- const React = require('react')
2171
- const { useEffect, useState, useCallback } = React
4802
+ const React = require('react'); const { useEffect, useState, useCallback } = React
2172
4803
  const el = React.createElement
2173
4804
 
2174
4805
  // slots:槽注册;sessions:子会话跳转(openSubagent)。两者皆 client 核心服务。
2175
4806
  const inject = ['slots', 'sessions']
2176
4807
 
2177
4808
  const chip = makeChip(el)
2178
- const DispatchToolview = makeDispatchToolview(el, chip, useState)
2179
- const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
4809
+ const DispatchToolview = makeDispatchToolview(el, chip, useState, useEffect)
4810
+ const ReminderOverlayBadge = makeReminderOverlayBadge(el, useState, useEffect)
2180
4811
 
2181
4812
  function apply(ctx) {
4813
+ // 提醒中心需要会话跳转(主会话 open / 子会话 openSubagent)与标题查询,
4814
+ // 与派发决策台账 openChild 同一 sessions 服务来源。
4815
+ const openChild = (address) => ctx.sessions.openSubagent(address)
4816
+ const ReminderCenter = makeReminderCenter(el, useState, useEffect, openChild, () => ctx.sessions)
4817
+ const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, () => ctx.sessions)
2182
4818
  ensureStyles()
2183
- ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
2184
- name: 'tool.call.toolview',
2185
- key: 'dispatch'
2186
- }, DispatchToolview))
4819
+ const SubagentHeaderUtility = makeSubagentHeaderUtility(el, useState, useEffect, () => ctx.get('modelDirectories'), () => ctx.sessions)
4820
+ ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: 'dispatch' }, DispatchToolview))
2187
4821
  ctx.slots.inject('settings.section', () => ctx.slots.register({
2188
4822
  name: 'settings.section',
2189
4823
  id: 'subagent-profiles',
2190
4824
  order: 100,
2191
4825
  label: '子 Agent 方案'
2192
4826
  }, ProfilesSection))
4827
+ // 子会话页头常驻 banner(仅子会话渲染;同 chip 族;宿主未投影前不渲染)。
4828
+ ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-subagent-profile-subagent', order: 20 }, SubagentHeaderUtility))
4829
+ // P0 红角标(severity 红 + 常驻至已读 + 带 session 标识)。
4830
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-subagent-profile-reminders', order: 800 }, ReminderOverlayBadge))
2193
4831
  // 子会话跳转回调:经 sessions 服务按 SubagentAddress 打开子会话。
2194
- const openChild = (address) => ctx.sessions.openSubagent(address)
2195
- const DispatchLedgerView = makeDispatchLedgerView(el, chip, useState, useEffect, openChild)
4832
+ const DispatchLedgerView = makeDispatchLedgerView(el, chip, useState, useEffect, openChild, () => ctx.sessions)
2196
4833
  ctx.slots.inject('conversation.view', () => ctx.slots.register({
2197
4834
  name: 'conversation.view',
2198
4835
  id: 'dispatch-ledger',
@@ -2208,4 +4845,4 @@ window.__ModuleLoader__.load({
2208
4845
 
2209
4846
  return { apply, inject }
2210
4847
  },
2211
- })
4848
+ })