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