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