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