dsh-subagent-profile 0.3.2 → 0.3.3
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 +38 -29
- package/README.zh.md +38 -29
- package/index.mjs +188 -62
- package/lib/client.js +1240 -47
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +433 -0
- package/lib/core/delegation.mjs +106 -48
- package/lib/core/dispatch-gates.mjs +146 -0
- package/lib/core/dispatch-guard.mjs +150 -0
- package/lib/core/dispatch-schema.mjs +98 -14
- package/lib/core/dispatch-tool.mjs +208 -199
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-ledger.mjs +289 -0
- package/lib/core/evolution-summary.mjs +432 -0
- package/lib/core/http-routes.mjs +155 -40
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +256 -136
- package/lib/core/profile-provider.mjs +41 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/shims.mjs +56 -75
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +2 -3
- package/presets/orchestrator/agent.cordis.yml +243 -271
- package/presets/orchestrator/NOTICE +0 -3
package/lib/client.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-subagent-profile —
|
|
3
|
-
*
|
|
2
|
+
* dsh-subagent-profile — 浏览器侧:只读 `dispatch` 工具调用卡片 +
|
|
3
|
+
* 管理方案注册表的设置页。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `slots` service. The card lives on the keyed `tool.call.toolview` seat (key
|
|
10
|
-
* `dispatch`); the settings page lives on the `settings.section` list.
|
|
5
|
+
* 手写 `__ModuleLoader__` 工厂(无构建步骤),参照 @dsh-external/dsh-persona-ref:
|
|
6
|
+
* React 来自共享模块宿主,所有数据操作走宿主侧(index.mjs)提供的 loopback
|
|
7
|
+
* `/subagent-profiles/*` HTTP 路由,slot 经共享 `slots` 服务注册。卡片挂在
|
|
8
|
+
* keyed `tool.call.toolview` 座(key `dispatch`);设置页挂在 `settings.section` 列表。
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* on settings open), `/options/efforts` (per-model, lazy on model select) and
|
|
19
|
-
* `/options/tools` (tool directory, lazy when the restriction panel opens)
|
|
20
|
-
* routes from the live presets roster and llm directory.
|
|
10
|
+
* 设置页视觉语言对齐官方「模型」设置节(dsh-client-ui-settings-models):同一套
|
|
11
|
+
* 设计 token,标题 + 引言,每行一张带边框卡片(名称/描述/元信息分层布局),
|
|
12
|
+
* 虚线「+ 新增」按钮展开内联表单卡片。下拉选项(preset / provider / model /
|
|
13
|
+
* reasoning effort)由宿主 `/options/summary`(模型 + 预设,设置页打开时)、
|
|
14
|
+
* `/options/efforts`(按模型,选择模型后懒加载)与 `/options/tools`(工具目录,
|
|
15
|
+
* 限制面板打开时懒加载)路由提供,数据来自实时 presets roster 与 llm 目录。
|
|
21
16
|
*
|
|
22
17
|
* 结构:纯数据(CSS/ZH 文案表)与纯函数(api/解析/文案映射等)驻留模块作用域
|
|
23
18
|
* (load 调用之前的顶层声明);依赖 React 的组件经参数注入的 maker 函数装配
|
|
@@ -103,6 +98,70 @@ const CSS = [
|
|
|
103
98
|
'.sap-toolList{flex-wrap:wrap;gap:2px 16px;display:flex}',
|
|
104
99
|
'.sap-toolItem{align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;line-height:20px;user-select:none;display:inline-flex}',
|
|
105
100
|
'.sap-toolItem input{margin:0;accent-color:var(--dsw-alias-brand-primary)}',
|
|
101
|
+
'.sap-ledger{flex-direction:column;gap:12px;display:flex}',
|
|
102
|
+
'.sap-ledgerSummary{position:sticky;top:0;z-index:1;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;flex-direction:column;gap:6px;padding:12px 14px;display:flex}',
|
|
103
|
+
'.sap-ledgerSummaryTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}',
|
|
104
|
+
'.sap-ledgerSummaryLine{flex-wrap:wrap;gap:2px 12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;display:flex}',
|
|
105
|
+
'.sap-ledgerStatSeg{color:var(--dsw-alias-label-secondary)}',
|
|
106
|
+
'.sap-ledgerStat{color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
107
|
+
'.sap-ledgerSep{color:var(--dsw-alias-label-tertiary)}',
|
|
108
|
+
'.sap-ledgerRows{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}',
|
|
109
|
+
'.sap-ledgerRow{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;display:flex}',
|
|
110
|
+
'.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
|
+
'.sap-ledgerRowHead:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
112
|
+
'.sap-ledgerTime{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace;flex:none}',
|
|
113
|
+
'.sap-ledgerMeta{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;min-width:0;word-break:break-word}',
|
|
114
|
+
'.sap-ledgerToggle{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;transition:transform .12s;margin-left:auto;flex:none;display:inline-flex}',
|
|
115
|
+
'.sap-ledgerRowOpen .sap-ledgerToggle{transform:rotate(90deg)}',
|
|
116
|
+
'.sap-ledgerExpand{flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 14px 12px;display:flex}',
|
|
117
|
+
'.sap-ledgerSection{flex-direction:column;gap:6px;display:flex}',
|
|
118
|
+
'.sap-ledgerSectionTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}',
|
|
119
|
+
'.sap-ledgerTimeline{flex-direction:column;gap:8px;display:flex}',
|
|
120
|
+
'.sap-ledgerStep{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}',
|
|
121
|
+
'.sap-ledgerStepHead{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}',
|
|
122
|
+
'.sap-ledgerStepBody{flex-direction:column;gap:4px;display:flex}',
|
|
123
|
+
'.sap-ledgerGate{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}',
|
|
124
|
+
'.sap-ledgerGateFail{background:var(--dsw-alias-interactive-bg-hover-danger);border-color:var(--dsw-alias-state-error-secondary)}',
|
|
125
|
+
'.sap-ledgerGateHead{justify-content:space-between;align-items:center;gap:8px;display:flex}',
|
|
126
|
+
'.sap-ledgerGateName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}',
|
|
127
|
+
'.sap-ledgerGateVerdict{font-size:12px;font-weight:500;line-height:18px;flex:none}',
|
|
128
|
+
'.sap-ledgerGateVerdictPass{color:var(--dsw-alias-state-success-primary)}',
|
|
129
|
+
'.sap-ledgerGateVerdictFail{color:var(--dsw-alias-state-error-primary)}',
|
|
130
|
+
'.sap-ledgerGateBody{flex-direction:column;gap:4px;display:flex}',
|
|
131
|
+
'.sap-ledgerSubBlock{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}',
|
|
132
|
+
'.sap-ledgerSubBlockTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;align-items:center;gap:6px;display:flex}',
|
|
133
|
+
'.sap-ledgerSubBlockBody{flex-direction:column;gap:4px;display:flex}',
|
|
134
|
+
'.sap-ledgerGateNested{flex-direction:column;gap:2px;border-left:2px solid var(--dsw-alias-border-l2);padding-left:10px;margin-left:2px;display:flex}',
|
|
135
|
+
'.sap-ledgerGateKV{flex-wrap:wrap;align-items:baseline;gap:2px 8px;display:flex}',
|
|
136
|
+
'.sap-ledgerGateKey{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;flex:none}',
|
|
137
|
+
'.sap-ledgerGateValue{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-family:monospace;word-break:break-all}',
|
|
138
|
+
'.sap-ledgerReject{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}',
|
|
139
|
+
'.sap-ledgerRejectFix{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}',
|
|
140
|
+
'.sap-ledgerUsage{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
|
|
141
|
+
'.sap-ledgerManual{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;align-items:baseline;gap:6px;display:flex}',
|
|
142
|
+
'.sap-ledgerManualId{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-family:monospace;word-break:break-all}',
|
|
143
|
+
'.sap-ledgerEmpty{flex-direction:column;gap:4px;padding:8px 2px;display:flex}',
|
|
144
|
+
'.sap-ledgerEmptyTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}',
|
|
145
|
+
'.sap-ledgerLoadOlder{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);background:0 0;border-radius:10px;font:inherit;font-size:12px;line-height:18px;height:28px;padding:0 12px;cursor:pointer;align-self:flex-start;align-items:center;display:inline-flex}',
|
|
146
|
+
'.sap-ledgerLoadOlder:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
147
|
+
'.sap-ledgerLoadOlder:disabled{opacity:.4;cursor:default}',
|
|
148
|
+
'.sap-ledgerLegacy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
|
|
149
|
+
'.sap-ledgerCallsTable{border-collapse:collapse;width:100%;font-size:11px;line-height:16px}',
|
|
150
|
+
'.sap-ledgerCallsTable th{color:var(--dsw-alias-label-secondary);font-weight:500;text-align:right;padding:2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
151
|
+
'.sap-ledgerCallsTable td{color:var(--dsw-alias-label-primary);font-family:monospace;text-align:right;padding:2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
152
|
+
'.sap-ledgerCallsIndex{color:var(--dsw-alias-label-tertiary);text-align:left!important;font-family:inherit!important}',
|
|
153
|
+
'.sap-ledgerCallsTotal{font-weight:600;border-bottom:none}',
|
|
154
|
+
'.sap-ledgerCallsNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
|
|
155
|
+
'.sap-ledgerGateGroup{flex-direction:column;gap:2px;display:flex}',
|
|
156
|
+
'.sap-ledgerRawJson{box-sizing:border-box;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:4px;cursor:pointer;font:inherit;font-size:11px;line-height:16px;padding:0 4px;margin-left:auto;flex:none}',
|
|
157
|
+
'.sap-ledgerRawJson:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
158
|
+
'.sap-ledgerJsonBody{flex-direction:column;gap:6px;display:flex}',
|
|
159
|
+
'.sap-ledgerJsonBlock{flex-direction:column;gap:2px;display:flex}',
|
|
160
|
+
'.sap-ledgerScope{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:0}',
|
|
161
|
+
'.sap-ledgerWindowNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
|
|
162
|
+
'.sap-escapeAdd{align-items:center;gap:8px;display:flex}',
|
|
163
|
+
'.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
|
+
'.sap-chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}',
|
|
106
165
|
].join('\n')
|
|
107
166
|
|
|
108
167
|
// ── 纯数据:dispatch 卡片面向用户固定文案 ──────────────────────────────────
|
|
@@ -139,7 +198,66 @@ const ZH = {
|
|
|
139
198
|
usageCacheWrite: '缓存写',
|
|
140
199
|
usageReasoning: '推理',
|
|
141
200
|
persistFail: '已保存但未持久化',
|
|
142
|
-
persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)'
|
|
201
|
+
persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)',
|
|
202
|
+
// 审计分级(设置页红字 + 丢失计数)
|
|
203
|
+
auditDegraded: '审计记录写入异常(degraded):请检查磁盘',
|
|
204
|
+
auditLost: '审计丢失计数',
|
|
205
|
+
// 派发账本(conversation.view 会话标签页)文案
|
|
206
|
+
ledgerTab: '派发账本',
|
|
207
|
+
ledgerSummary: '本会话派发',
|
|
208
|
+
ledgerDispatch: '派发',
|
|
209
|
+
ledgerTimes: '次',
|
|
210
|
+
ledgerSuccess: '成功',
|
|
211
|
+
ledgerReject: '拒绝',
|
|
212
|
+
ledgerPass: '通过',
|
|
213
|
+
ledgerTotalTokens: '总 token',
|
|
214
|
+
ledgerTotalElapsed: '总耗时',
|
|
215
|
+
ledgerElapsed: '耗时',
|
|
216
|
+
ledgerStopReason: '结束原因',
|
|
217
|
+
ledgerGateRejectDist: '各闸拒绝分布',
|
|
218
|
+
ledgerByProfile: '按方案',
|
|
219
|
+
ledgerByModel: '按模型',
|
|
220
|
+
ledgerByTier: '按档位',
|
|
221
|
+
ledgerEmpty: '本会话暂无派发记录',
|
|
222
|
+
ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的派发决策轨迹。',
|
|
223
|
+
ledgerLoadOlder: '加载更早',
|
|
224
|
+
ledgerLoadingOlder: '加载中…',
|
|
225
|
+
ledgerLegacy: '该记录产生于决策轨迹上线前,仅有结果摘要',
|
|
226
|
+
ledgerGateInput: '输入',
|
|
227
|
+
ledgerGateOutput: '输出',
|
|
228
|
+
ledgerRejectFix: '修复方向',
|
|
229
|
+
ledgerJumpChild: '打开子会话',
|
|
230
|
+
ledgerChildManual: '子会话 ID',
|
|
231
|
+
ledgerJobManual: '后台任务 ID',
|
|
232
|
+
ledgerStarted: '发起',
|
|
233
|
+
ledgerParentContext: '父会话上下文',
|
|
234
|
+
ledgerRequested: '请求值',
|
|
235
|
+
ledgerEffectiveStep: '生效值',
|
|
236
|
+
ledgerDecisionTimeline: '决策时间线',
|
|
237
|
+
ledgerSettledStep: '结算',
|
|
238
|
+
ledgerRawJson: '原始 JSON',
|
|
239
|
+
ledgerFieldView: '字段视图',
|
|
240
|
+
ledgerCallDetail: '逐次调用明细',
|
|
241
|
+
ledgerCallsTotal: '合计',
|
|
242
|
+
ledgerCallsTruncated: '仅显示前 {shown} 条,共 {total} 次调用',
|
|
243
|
+
ledgerCallsIndex: '序号',
|
|
244
|
+
ledgerExecutionStep: '执行信息',
|
|
245
|
+
ledgerScopeNote: '本页仅展示本会话的派发决策轨迹;跨会话进化台账(dispatch.jsonl)与审计记录见设置页。',
|
|
246
|
+
ledgerWindowNote: '已加载最近 {n} 条,尚有更早记录',
|
|
247
|
+
gateFixUnknown: '未能识别的闸拒绝,请核对请求参数',
|
|
248
|
+
ignoredChipTitle: '请求值 "{value}" 已忽略',
|
|
249
|
+
ignoredChipTitleEmpty: '请求值已忽略',
|
|
250
|
+
// 逃生舱(非 system-trust 预设放行)
|
|
251
|
+
escapeTitle: '逃生舱(非 system-trust 预设放行)',
|
|
252
|
+
escapeWarn: '逃生舱已开启:允许派发非 system-trust 预设。风险自负——其余三道安全闸仍全量生效,每个放行都会留审计记录。',
|
|
253
|
+
escapeEmpty: '暂无放行预设',
|
|
254
|
+
escapeAdd: '添加',
|
|
255
|
+
escapeRemove: '移除',
|
|
256
|
+
escapeInputPlaceholder: '非 system-trust 预设 id',
|
|
257
|
+
escapeAddEmpty: '逃生舱预设 id 不能为空',
|
|
258
|
+
// 只读建议注入(evolution:advice)
|
|
259
|
+
adviceTitle: '只读建议注入(evolution:advice)',
|
|
260
|
+
adviceDesc: '开启后编排者会话注入派发统计与确定性建议(仅父 Agent、只读、默认关)'
|
|
143
261
|
}
|
|
144
262
|
|
|
145
263
|
// ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
|
|
@@ -153,8 +271,60 @@ const TOOL_MODES = [
|
|
|
153
271
|
]
|
|
154
272
|
const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
|
|
155
273
|
const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
|
|
274
|
+
// 派发账本:闸名中文标签(展示层同时保留英文技术标识)。
|
|
275
|
+
const GATE_LABELS = { whitelist: '白名单', cost: '成本', intersection: '交集', approval: '审批', budget: '预算', other: '其它' }
|
|
276
|
+
// 派发账本:失败闸的修复方向提示(指向设置页对应字段)。
|
|
277
|
+
const GATE_FIX_HINTS = {
|
|
278
|
+
whitelist: '目标预设不在 system-trust 候选内,可在设置页改用受信任的预设',
|
|
279
|
+
cost: '参数超出模型目录或成本上限,可在设置页调整提供方/模型/推理强度',
|
|
280
|
+
intersection: '工具交集为空,可在设置页调整该方案的 toolFilter',
|
|
281
|
+
approval: '审批策略冲突,请核对审批配置',
|
|
282
|
+
budget: '并发派发数或本会话累计 token 已达上限,请等待在途任务完成或开启新会话'
|
|
283
|
+
}
|
|
284
|
+
// 派发账本:决策轨迹字段键名 → 中文标签(字段级渲染去「JSON 墙」用)。
|
|
285
|
+
// 未知键回退原样(不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
|
|
286
|
+
const GATE_KEY_LABELS = {
|
|
287
|
+
requestedPreset: '目标预设', systemTrustCandidates: '候选白名单', allowed: '放行', resolvedPreset: '解析后预设',
|
|
288
|
+
checks: '检查项', field: '字段', checkedAgainst: '对照项', verdict: '结论', detail: '详情', policy: '策略', applied: '应用值',
|
|
289
|
+
effectiveAllowCount: '生效工具数', parentSessionId: '父会话', childSessionId: '子会话', jobId: '任务ID', reason: '原因', limit: '上限',
|
|
290
|
+
skipped: '跳过', allowFailOpen: '兼容模式', mode: '模式', maxTokens: 'token 上限', maxDepth: '深度上限',
|
|
291
|
+
requestedToolFilter: '请求工具过滤', parentToolCount: '父工具数', providers: '提供方', modelEfforts: '模型支持档位',
|
|
292
|
+
effectiveToolNames: '生效工具', preset: '目标预设', profile: '方案', provider: '提供方', model: '模型',
|
|
293
|
+
reasoningEffort: '推理强度', tokenTier: '档位', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤',
|
|
294
|
+
prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断', envelope: '信封模式', parentPreset: '父预设', parentProvider: '父提供方',
|
|
295
|
+
parentModel: '父模型', outcome: '结果', status: '状态', stop_reason: '结束原因', elapsed_ms: '耗时',
|
|
296
|
+
output_len: '输出长度', tool_count: '工具调用数'
|
|
297
|
+
}
|
|
156
298
|
const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
|
|
157
299
|
|
|
300
|
+
// 审计分级的默认态(summary 缺 audit 字段 / 值非法时回退):health ok、零丢失。
|
|
301
|
+
const AUDIT_DEFAULTS = { lostTelemetry: 0, lostGovernance: 0, health: 'ok' }
|
|
302
|
+
|
|
303
|
+
// 从 /options/summary 响应抽取 audit 分级;缺失/非法字段 fallback 到默认态。
|
|
304
|
+
function readAuditFromData(data) {
|
|
305
|
+
const a = data !== null && typeof data === 'object' && data.audit !== null && typeof data.audit === 'object' ? data.audit : {}
|
|
306
|
+
return {
|
|
307
|
+
lostTelemetry: typeof a.lostTelemetry === 'number' ? a.lostTelemetry : 0,
|
|
308
|
+
lostGovernance: typeof a.lostGovernance === 'number' ? a.lostGovernance : 0,
|
|
309
|
+
health: a.health === 'degraded' ? 'degraded' : 'ok'
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// /options/summary 响应 → 设置页状态(enabled/options/audit/逃生舱开关与放行集)。
|
|
314
|
+
// set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开。
|
|
315
|
+
function applySummaryData(data, set) {
|
|
316
|
+
set.enabled(data.enabled !== false)
|
|
317
|
+
set.options({
|
|
318
|
+
models: Array.isArray(data.models) ? data.models : [],
|
|
319
|
+
efforts: {},
|
|
320
|
+
presets: Array.isArray(data.presets) ? data.presets : []
|
|
321
|
+
})
|
|
322
|
+
set.audit(readAuditFromData(data))
|
|
323
|
+
set.escapeEnabled(data.escapeEnabled === true)
|
|
324
|
+
set.escapePresets(Array.isArray(data.escapePresets) ? data.escapePresets : [])
|
|
325
|
+
set.evolutionAdvice(data.evolutionAdvice === true)
|
|
326
|
+
}
|
|
327
|
+
|
|
158
328
|
// ── 纯函数:api / 样式注入 / 文案映射 ──────────────────────────────────────
|
|
159
329
|
|
|
160
330
|
// loopback HTTP,host 侧服务 /subagent-profiles/*。
|
|
@@ -163,7 +333,9 @@ async function api(path, payload) {
|
|
|
163
333
|
? undefined
|
|
164
334
|
: {
|
|
165
335
|
method: 'POST',
|
|
166
|
-
|
|
336
|
+
// 写请求带 CSRF 三件套里的 Content-Type + 自定义头(Origin 由浏览器自动带)。
|
|
337
|
+
// 缺自定义头会被 host 侧写路由 CSRF 守卫 403 拒绝。
|
|
338
|
+
headers: { 'Content-Type': 'application/json', 'X-DSH-Plugin': 'dsh-subagent-profile' },
|
|
167
339
|
body: JSON.stringify(payload),
|
|
168
340
|
}
|
|
169
341
|
const res = await fetch(`/subagent-profiles${path}`, init)
|
|
@@ -195,13 +367,20 @@ const effortZh = (id) => {
|
|
|
195
367
|
}
|
|
196
368
|
const presetZh = (v) => (v === 'inherit' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
|
|
197
369
|
const profileZh = (v) => (v === undefined || v === '' || v === '(inline)' ? ZH.inlineProfile : v)
|
|
370
|
+
// profile id → 中文显示名:注册表有 name 时「标准编码(swap-standard)」中英双表达,
|
|
371
|
+
// 无 name(自定义方案可能只填了 description)时回退纯 id。技术标识 id 恒保留。
|
|
372
|
+
function profileDisplay(profileId, profileNames) {
|
|
373
|
+
if (profileId === undefined || profileId === '' || profileId === '(inline)') return ZH.inlineProfile
|
|
374
|
+
const name = profileNames !== undefined && profileNames !== null ? profileNames[profileId] : undefined
|
|
375
|
+
return typeof name === 'string' && name !== '' ? `${name}(${profileId})` : profileId
|
|
376
|
+
}
|
|
198
377
|
const rawOrInherit = (v) => (v === '(parent)' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
|
|
199
378
|
const effortLabel = (effort) => {
|
|
200
379
|
const id = effort && effort.id
|
|
201
380
|
return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
|
|
202
381
|
}
|
|
203
382
|
|
|
204
|
-
//
|
|
383
|
+
// 不同 provider 列表,由模型列表派生。
|
|
205
384
|
function deriveProviders(models) {
|
|
206
385
|
const providers = []
|
|
207
386
|
const seenProvider = new Set()
|
|
@@ -238,9 +417,9 @@ function readDispatchRequest(block) {
|
|
|
238
417
|
}
|
|
239
418
|
|
|
240
419
|
// 结果 meta(生效值):宿主 render 行("[dispatch] …")是持久回放的唯一
|
|
241
|
-
// 生效值来源——结构化 value 只存在于执行局部(dsh-tools
|
|
242
|
-
//
|
|
243
|
-
// {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
|
|
420
|
+
// 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定)。output.presentationMeta
|
|
421
|
+
// 现投影 decisionTrace(block.meta 承载决策轨迹,非生效值),生效值仍需从 render 行解析。
|
|
422
|
+
// 解析成功返回 {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
|
|
244
423
|
const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier', 'childTotalTokens', 'childUsage', 'elapsedMs', 'stopReason']
|
|
245
424
|
function parseDispatchText(text) {
|
|
246
425
|
if (typeof text !== 'string') return null
|
|
@@ -393,6 +572,292 @@ function usageBreakdownText(usage) {
|
|
|
393
572
|
return parts.join(' · ')
|
|
394
573
|
}
|
|
395
574
|
|
|
575
|
+
// ── 纯函数:派发账本数据层(快照遍历 / 轨迹归一 / 汇总统计 / 失败合并)────────
|
|
576
|
+
|
|
577
|
+
// 把 string|number 归一为有限数,不可得返回 null(汇总与行级数字的统一口径)。
|
|
578
|
+
function toFiniteNumber(v) {
|
|
579
|
+
const n = typeof v === 'number' ? v : (typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN)
|
|
580
|
+
return Number.isFinite(n) ? n : null
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 非数组对象守卫:对象原样返回,其它(null/数组/原始值)返回空对象。
|
|
584
|
+
function asObject(v) {
|
|
585
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 从 result 内容块取首行文本(只取第一块非空文本,无文本返回空串)。
|
|
589
|
+
function firstContentLine(content) {
|
|
590
|
+
if (!Array.isArray(content)) return ''
|
|
591
|
+
for (const item of content) {
|
|
592
|
+
if (item && typeof item === 'object' && typeof item.text === 'string' && item.text !== '') {
|
|
593
|
+
return item.text.split('\n')[0] ?? ''
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return ''
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// argsRaw 摘要:dispatch 参数为单行 JSON,取首行即完整摘要;空/坏返回空串。
|
|
600
|
+
function summarizeArgsRaw(argsRaw) {
|
|
601
|
+
if (typeof argsRaw !== 'string' || argsRaw === '') return ''
|
|
602
|
+
return argsRaw.split('\n')[0] ?? ''
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// gate 条目归一:只保留渲染需要的五字段,name/verdict/reason 逐字段 typeof 守卫、
|
|
606
|
+
// 缺失补空串;input/output 缺省为空对象;非对象条目整条丢弃。
|
|
607
|
+
function normalizeGateEntries(gates) {
|
|
608
|
+
if (!Array.isArray(gates)) return []
|
|
609
|
+
const out = []
|
|
610
|
+
for (const gate of gates) {
|
|
611
|
+
if (gate === null || typeof gate !== 'object' || Array.isArray(gate)) continue
|
|
612
|
+
out.push({
|
|
613
|
+
name: typeof gate.name === 'string' ? gate.name : '',
|
|
614
|
+
input: asObject(gate.input),
|
|
615
|
+
output: asObject(gate.output),
|
|
616
|
+
verdict: typeof gate.verdict === 'string' ? gate.verdict : '',
|
|
617
|
+
reason: typeof gate.reason === 'string' ? gate.reason : ''
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
return out
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// decisionTrace 归一:坏数据/缺字段 fail-soft(逐段 typeof 守卫,不 throw)。
|
|
624
|
+
// 非对象或无任何可识别 shape(version/startedAt/gates 三者皆缺)返回 null;
|
|
625
|
+
// 各段缺失补空对象,settled 缺省为 null(区分「未结算」与「结算但为空」)。
|
|
626
|
+
function normalizeDecisionTrace(raw) {
|
|
627
|
+
if (raw === undefined || raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
628
|
+
if (typeof raw.version !== 'number' && typeof raw.startedAt !== 'number' && !Array.isArray(raw.gates)) return null
|
|
629
|
+
return {
|
|
630
|
+
version: typeof raw.version === 'number' ? raw.version : 1,
|
|
631
|
+
startedAt: typeof raw.startedAt === 'number' ? raw.startedAt : null,
|
|
632
|
+
parentContext: asObject(raw.parentContext),
|
|
633
|
+
requested: asObject(raw.requested),
|
|
634
|
+
gates: normalizeGateEntries(raw.gates),
|
|
635
|
+
effective: asObject(raw.effective),
|
|
636
|
+
execution: asObject(raw.execution),
|
|
637
|
+
settled: raw.settled && typeof raw.settled === 'object' && !Array.isArray(raw.settled) ? raw.settled : null
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 从 tool-result 节点读决策轨迹:宿主 presentationMeta 把 decisionTrace 直接投影为
|
|
642
|
+
// 节点 meta(meta 即 trace 本身);同时兼容未来包一层 .decisionTrace 的形态,
|
|
643
|
+
// 两种都尝试、绝不 throw。
|
|
644
|
+
function readNodeTrace(node) {
|
|
645
|
+
const meta = node && typeof node === 'object' ? node.meta : null
|
|
646
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) return null
|
|
647
|
+
const direct = normalizeDecisionTrace(meta)
|
|
648
|
+
if (direct !== null) return direct
|
|
649
|
+
if (meta.decisionTrace && typeof meta.decisionTrace === 'object' && !Array.isArray(meta.decisionTrace)) {
|
|
650
|
+
return normalizeDecisionTrace(meta.decisionTrace)
|
|
651
|
+
}
|
|
652
|
+
return null
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 快照遍历:过滤 chat.legacy.nodes 中 kind==='tool-result' 且 call.name==='dispatch'
|
|
656
|
+
// 的节点,产出账本记录。记录携带 callId/time/isError/trace(已归一,null 即旧记录
|
|
657
|
+
// 降级 legacy)/request/result/argsSummary/contentFirstLine。输入为空或非数组返回
|
|
658
|
+
// 空数组;逐节点守卫、坏节点跳过。
|
|
659
|
+
function collectDispatchRecords(nodes) {
|
|
660
|
+
if (!Array.isArray(nodes)) return []
|
|
661
|
+
const records = []
|
|
662
|
+
for (const node of nodes) {
|
|
663
|
+
if (!node || typeof node !== 'object') continue
|
|
664
|
+
if (node.kind !== 'tool-result') continue
|
|
665
|
+
if (!node.call || typeof node.call !== 'object' || node.call.name !== 'dispatch') continue
|
|
666
|
+
const trace = readNodeTrace(node)
|
|
667
|
+
records.push({
|
|
668
|
+
callId: typeof node.callId === 'string' ? node.callId : '',
|
|
669
|
+
time: typeof node.time === 'number' ? node.time : (typeof node.callTime === 'number' ? node.callTime : null),
|
|
670
|
+
isError: node.isError === true,
|
|
671
|
+
legacy: trace === null,
|
|
672
|
+
trace,
|
|
673
|
+
request: readDispatchRequest(node),
|
|
674
|
+
result: readDispatchResult(node),
|
|
675
|
+
argsSummary: summarizeArgsRaw(node.call.argsRaw),
|
|
676
|
+
contentFirstLine: firstContentLine(node.content)
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
return records
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 从单条记录提取结算数字:trace.settled 优先,缺失回退 result.fields(旧记录口径)。
|
|
683
|
+
// 返回 { tokens, cacheRead, elapsedMs };不可得字段为 null/0。
|
|
684
|
+
function recordNumbers(record) {
|
|
685
|
+
let tokens = null
|
|
686
|
+
let elapsedMs = null
|
|
687
|
+
let cacheRead = 0
|
|
688
|
+
const trace = record && record.trace
|
|
689
|
+
const result = record && record.result
|
|
690
|
+
const settled = trace && trace.settled
|
|
691
|
+
if (settled) {
|
|
692
|
+
tokens = toFiniteNumber(settled.childTotalTokens)
|
|
693
|
+
elapsedMs = toFiniteNumber(settled.elapsedMs)
|
|
694
|
+
if (settled.childUsage && typeof settled.childUsage.cacheReadTokens === 'number') cacheRead = settled.childUsage.cacheReadTokens
|
|
695
|
+
}
|
|
696
|
+
if (result && result.fields) {
|
|
697
|
+
if (tokens === null) tokens = toFiniteNumber(result.fields.childTotalTokens)
|
|
698
|
+
if (elapsedMs === null) elapsedMs = toFiniteNumber(result.fields.elapsedMs)
|
|
699
|
+
if (cacheRead === 0) {
|
|
700
|
+
const usage = parseChildUsage(result.fields.childUsage)
|
|
701
|
+
if (usage && typeof usage.cacheReadTokens === 'number') cacheRead = usage.cacheReadTokens
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { tokens, cacheRead, elapsedMs }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 判定记录是否被拒绝:trace 任一闸 verdict==='fail' 视为闸拒绝;无 trace 且 isError
|
|
708
|
+
// 视为拒绝(旧错误块无法区分闸拒绝与运行时错误,统一计拒绝)。
|
|
709
|
+
function recordIsRejected(record) {
|
|
710
|
+
if (!record) return false
|
|
711
|
+
const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
|
|
712
|
+
if (gates.some((gate) => gate.verdict === 'fail')) return true
|
|
713
|
+
return record.isError === true
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 累加记录各失败闸到闸拒绝分布;未知闸名归入 other(counts 已含五键)。
|
|
717
|
+
function countGateRejects(record, counts) {
|
|
718
|
+
if (!record || !record.trace || !Array.isArray(record.trace.gates)) return
|
|
719
|
+
for (const gate of record.trace.gates) {
|
|
720
|
+
if (gate.verdict !== 'fail') continue
|
|
721
|
+
const key = Object.prototype.hasOwnProperty.call(counts, gate.name) ? gate.name : 'other'
|
|
722
|
+
counts[key] += 1
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 记录归属方案:effective.profile → requested.profile → request.profile 依次回退;
|
|
727
|
+
// 缺失返回空串(不参与 byProfile 计数)。
|
|
728
|
+
function recordProfile(record) {
|
|
729
|
+
if (!record) return ''
|
|
730
|
+
const trace = record.trace
|
|
731
|
+
if (trace && trace.effective && typeof trace.effective.profile === 'string' && trace.effective.profile !== '') return trace.effective.profile
|
|
732
|
+
if (trace && trace.requested && typeof trace.requested.profile === 'string' && trace.requested.profile !== '') return trace.requested.profile
|
|
733
|
+
if (record.request && typeof record.request.profile === 'string') return record.request.profile
|
|
734
|
+
return ''
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 会话汇总:与账本汇总带对齐。records 为合并后的完整记录列表(含失败记录)。
|
|
738
|
+
// 旧记录贡献可得字段(次数/耗时/token),不可得记 0/省略;闸拒绝按 trace.gates
|
|
739
|
+
// 的 fail verdict 分布。byProfile/byModel/byTier 均按次数降序。
|
|
740
|
+
function computeLedgerSummary(records) {
|
|
741
|
+
const list = Array.isArray(records) ? records : []
|
|
742
|
+
const summary = {
|
|
743
|
+
dispatchCount: list.length,
|
|
744
|
+
successCount: 0,
|
|
745
|
+
rejectCount: 0,
|
|
746
|
+
totalTokens: 0,
|
|
747
|
+
cacheHitTokens: 0,
|
|
748
|
+
totalElapsedMs: 0,
|
|
749
|
+
gateRejectCounts: { whitelist: 0, cost: 0, intersection: 0, approval: 0, other: 0 },
|
|
750
|
+
byProfile: [],
|
|
751
|
+
byModel: [],
|
|
752
|
+
byTier: []
|
|
753
|
+
}
|
|
754
|
+
const profileMap = new Map()
|
|
755
|
+
const modelMap = new Map()
|
|
756
|
+
const tierMap = new Map()
|
|
757
|
+
for (const record of list) {
|
|
758
|
+
if (recordIsRejected(record)) summary.rejectCount += 1
|
|
759
|
+
else summary.successCount += 1
|
|
760
|
+
const { tokens, cacheRead, elapsedMs } = recordNumbers(record)
|
|
761
|
+
if (tokens !== null) summary.totalTokens += tokens
|
|
762
|
+
summary.cacheHitTokens += cacheRead
|
|
763
|
+
if (elapsedMs !== null) summary.totalElapsedMs += elapsedMs
|
|
764
|
+
countGateRejects(record, summary.gateRejectCounts)
|
|
765
|
+
const profile = recordProfile(record)
|
|
766
|
+
if (profile !== '') profileMap.set(profile, (profileMap.get(profile) || 0) + 1)
|
|
767
|
+
// 模型:空/'(parent)'(继承父标记)并入继承桶(键为空串,展示层经 rawOrInherit 归一)。
|
|
768
|
+
const model = recordModel(record)
|
|
769
|
+
if (model === '' || model === '(parent)') modelMap.set('', (modelMap.get('') || 0) + 1)
|
|
770
|
+
else modelMap.set(model, (modelMap.get(model) || 0) + 1)
|
|
771
|
+
const tier = recordTier(record)
|
|
772
|
+
if (tier !== '') tierMap.set(tier, (tierMap.get(tier) || 0) + 1)
|
|
773
|
+
}
|
|
774
|
+
summary.byProfile = [...profileMap.entries()]
|
|
775
|
+
.map(([profile, count]) => ({ profile, count }))
|
|
776
|
+
.sort((a, b) => b.count - a.count)
|
|
777
|
+
summary.byModel = [...modelMap.entries()]
|
|
778
|
+
.map(([model, count]) => ({ model, count }))
|
|
779
|
+
.sort((a, b) => b.count - a.count)
|
|
780
|
+
summary.byTier = [...tierMap.entries()]
|
|
781
|
+
.map(([tier, count]) => ({ tier, count }))
|
|
782
|
+
.sort((a, b) => b.count - a.count)
|
|
783
|
+
return summary
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 拉取失败台账:经既有 api() 走 GET /ledger/failures?session=<id>,失败项同样经
|
|
787
|
+
// normalizeDecisionTrace 归一。host 侧经 stateFile(subagent-profiles.failed-traces.json)
|
|
788
|
+
// 落盘,重启后仍可读;host 不可达/路由缺失/坏数据一律 fail-soft 返回空数组。
|
|
789
|
+
async function fetchLedgerFailures(sessionId) {
|
|
790
|
+
if (typeof sessionId !== 'string' || sessionId === '') return []
|
|
791
|
+
try {
|
|
792
|
+
const data = await api(`/ledger/failures?session=${encodeURIComponent(sessionId)}`)
|
|
793
|
+
const list = Array.isArray(data.failures) ? data.failures : []
|
|
794
|
+
const out = []
|
|
795
|
+
for (const item of list) {
|
|
796
|
+
const trace = normalizeDecisionTrace(item)
|
|
797
|
+
if (trace !== null) out.push(trace)
|
|
798
|
+
}
|
|
799
|
+
return out
|
|
800
|
+
} catch {
|
|
801
|
+
return []
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 由失败 trace 构造独立账本记录(快照内无对应错误块时):request 从 requested/
|
|
806
|
+
// execution 还原,callId/argsSummary/contentFirstLine 为空,标记 failureOnly。
|
|
807
|
+
function failureRecord(failure) {
|
|
808
|
+
const requested = asObject(failure && failure.requested)
|
|
809
|
+
const execution = asObject(failure && failure.execution)
|
|
810
|
+
const request = {
|
|
811
|
+
profile: typeof requested.profile === 'string' ? requested.profile : '',
|
|
812
|
+
preset: typeof requested.preset === 'string' ? requested.preset : '',
|
|
813
|
+
provider: typeof requested.provider === 'string' ? requested.provider : '',
|
|
814
|
+
model: typeof requested.model === 'string' ? requested.model : '',
|
|
815
|
+
reasoningEffort: typeof requested.reasoningEffort === 'string' ? requested.reasoningEffort : '',
|
|
816
|
+
continuable: execution.kind === 'continuable' || execution.mode === 'continuable',
|
|
817
|
+
runInBackground: execution.kind === 'background'
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
callId: '',
|
|
821
|
+
time: failure && typeof failure.startedAt === 'number' ? failure.startedAt : null,
|
|
822
|
+
isError: true,
|
|
823
|
+
legacy: false,
|
|
824
|
+
trace: failure,
|
|
825
|
+
request,
|
|
826
|
+
result: null,
|
|
827
|
+
argsSummary: '',
|
|
828
|
+
contentFirstLine: '',
|
|
829
|
+
failureOnly: true
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// 失败记录合并:把失败台账与快照错误块按 startedAt 就近匹配(±2s 窗口)。
|
|
834
|
+
// 匹配成功 → 失败 trace 回填进对应错误块记录(isError 且无 trace);匹配失败 →
|
|
835
|
+
// 独立列为新记录(failureOnly 标记)。返回合并后的完整记录列表,不修改输入数组。
|
|
836
|
+
function mergeFailureRecords(settledRecords, failures) {
|
|
837
|
+
const records = (Array.isArray(settledRecords) ? settledRecords : []).map((record) => ({ ...record }))
|
|
838
|
+
const failureList = Array.isArray(failures) ? failures : []
|
|
839
|
+
const unmatched = []
|
|
840
|
+
for (const failure of failureList) {
|
|
841
|
+
const ts = failure && typeof failure.startedAt === 'number' ? failure.startedAt : null
|
|
842
|
+
let matched = false
|
|
843
|
+
if (ts !== null) {
|
|
844
|
+
for (const record of records) {
|
|
845
|
+
if (record.isError !== true || record.trace !== null) continue
|
|
846
|
+
if (typeof record.time !== 'number') continue
|
|
847
|
+
if (Math.abs(record.time - ts) <= 2000) {
|
|
848
|
+
record.trace = failure
|
|
849
|
+
record.legacy = false
|
|
850
|
+
matched = true
|
|
851
|
+
break
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (!matched) unmatched.push(failure)
|
|
856
|
+
}
|
|
857
|
+
for (const failure of unmatched) records.push(failureRecord(failure))
|
|
858
|
+
return records
|
|
859
|
+
}
|
|
860
|
+
|
|
396
861
|
// ── dispatch 工具卡(React 依赖经参数注入)─────────────────────────────────
|
|
397
862
|
|
|
398
863
|
// 生效值:结果解析成功用结果(优先),字段缺失回退请求值。
|
|
@@ -654,17 +1119,24 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
|
|
|
654
1119
|
return { submit }
|
|
655
1120
|
}
|
|
656
1121
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1122
|
+
// 乐观开关:next=!get() 先落 UI,成功保留;失败回滚 + 经 setError 展示。
|
|
1123
|
+
function makeToggle(api, path, payloadKey, get, set, setError) {
|
|
1124
|
+
return () => {
|
|
1125
|
+
const next = !get()
|
|
1126
|
+
set(next)
|
|
1127
|
+
setError('')
|
|
1128
|
+
api(path, { [payloadKey]: next })
|
|
663
1129
|
.catch((err) => {
|
|
664
|
-
|
|
665
|
-
|
|
1130
|
+
set(!next)
|
|
1131
|
+
setError(String((err && err.message) || err))
|
|
666
1132
|
})
|
|
667
1133
|
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function makeRowActions(api, s, refresh, applyWrite) {
|
|
1137
|
+
const toggleEnabled = makeToggle(api, '/set-enabled', 'enabled', () => s.enabled, s.setEnabled, s.setError)
|
|
1138
|
+
const toggleEscape = makeToggle(api, '/set-escape', 'enabled', () => s.escapeEnabled, s.setEscapeEnabled, s.setError)
|
|
1139
|
+
const toggleAdvice = makeToggle(api, '/set-evolution-advice', 'advice', () => s.evolutionAdvice, s.setEvolutionAdvice, s.setError)
|
|
668
1140
|
const setProfileEnabled = (profile, next) => () => {
|
|
669
1141
|
s.setError('')
|
|
670
1142
|
api('/set-profile-enabled', { id: profile.id, enabled: next })
|
|
@@ -674,7 +1146,7 @@ function makeRowActions(api, s, refresh, applyWrite) {
|
|
|
674
1146
|
const resetAll = () => {
|
|
675
1147
|
if (!window.confirm('将所有内置方案重置为默认配置?已删除的内置也会恢复。')) return
|
|
676
1148
|
s.setError('')
|
|
677
|
-
api('/reset-all')
|
|
1149
|
+
api('/reset-all', {})
|
|
678
1150
|
.then((data) => { applyWrite(data, '已重置所有内置方案'); refresh() })
|
|
679
1151
|
.catch((err) => s.setError(String((err && err.message) || err)))
|
|
680
1152
|
}
|
|
@@ -688,7 +1160,32 @@ function makeRowActions(api, s, refresh, applyWrite) {
|
|
|
688
1160
|
})
|
|
689
1161
|
.catch((err) => s.setError(String((err && err.message) || err)))
|
|
690
1162
|
}
|
|
691
|
-
return { toggleEnabled, setProfileEnabled, resetAll, remove }
|
|
1163
|
+
return { toggleEnabled, toggleEscape, toggleAdvice, setProfileEnabled, resetAll, remove }
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// 逃生舱放行集增删:/escape-add 成功后用响应 presets 刷新本地列表并清空输入;
|
|
1167
|
+
// /escape-remove 成功后同样用响应 presets 刷新。失败统一经 s.setError 展示。
|
|
1168
|
+
function makeEscapeActions(api, s) {
|
|
1169
|
+
const escapeAdd = () => {
|
|
1170
|
+
const value = typeof s.escapeInput === 'string' ? s.escapeInput.trim() : ''
|
|
1171
|
+
if (value === '') { s.setError(ZH.escapeAddEmpty); return }
|
|
1172
|
+
s.setError('')
|
|
1173
|
+
api('/escape-add', { preset: value })
|
|
1174
|
+
.then((data) => {
|
|
1175
|
+
s.setEscapeInput('')
|
|
1176
|
+
if (Array.isArray(data.presets)) s.setEscapePresets(data.presets)
|
|
1177
|
+
})
|
|
1178
|
+
.catch((err) => s.setError(String((err && err.message) || err)))
|
|
1179
|
+
}
|
|
1180
|
+
const escapeRemove = (id) => () => {
|
|
1181
|
+
s.setError('')
|
|
1182
|
+
api('/escape-remove', { preset: id })
|
|
1183
|
+
.then((data) => {
|
|
1184
|
+
if (Array.isArray(data.presets)) s.setEscapePresets(data.presets)
|
|
1185
|
+
})
|
|
1186
|
+
.catch((err) => s.setError(String((err && err.message) || err)))
|
|
1187
|
+
}
|
|
1188
|
+
return { escapeAdd, escapeRemove }
|
|
692
1189
|
}
|
|
693
1190
|
|
|
694
1191
|
// 选中模型 → 懒拉 /options/efforts(按模型 id 存入 options.efforts)。
|
|
@@ -728,6 +1225,7 @@ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools
|
|
|
728
1225
|
...makeEditActions(s, loadEfforts, loadTools),
|
|
729
1226
|
...makeSubmitActions(api, s, refresh, applyWrite),
|
|
730
1227
|
...makeRowActions(api, s, refresh, applyWrite),
|
|
1228
|
+
...makeEscapeActions(api, s),
|
|
731
1229
|
}
|
|
732
1230
|
}
|
|
733
1231
|
|
|
@@ -903,6 +1401,64 @@ function buildAddCard(el, s, actions, toolSection) {
|
|
|
903
1401
|
)
|
|
904
1402
|
}
|
|
905
1403
|
|
|
1404
|
+
// 只读建议注入(evolution:advice)开关卡片:仅父 Agent、只读、默认关。
|
|
1405
|
+
function buildAdviceSection(el, s, actions) {
|
|
1406
|
+
return el('div', { className: 'sap-rowCard' },
|
|
1407
|
+
el('div', { className: 'sap-rowHead' },
|
|
1408
|
+
el('span', { className: 'sap-rowIdentity' },
|
|
1409
|
+
el('span', { className: 'sap-rowName' }, ZH.adviceTitle)
|
|
1410
|
+
),
|
|
1411
|
+
el('span', { className: 'sap-rowActions' },
|
|
1412
|
+
el('label', { className: 'sap-switch', title: ZH.adviceTitle },
|
|
1413
|
+
el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
|
|
1414
|
+
el('span', null, s.evolutionAdvice ? '已启用' : '已禁用')
|
|
1415
|
+
)
|
|
1416
|
+
)
|
|
1417
|
+
),
|
|
1418
|
+
el('p', { className: 'sap-desc' }, ZH.adviceDesc)
|
|
1419
|
+
)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
|
|
1423
|
+
// 每项「移除」按钮)。增删走 /escape-add /escape-remove,成功后用响应 presets 刷新。
|
|
1424
|
+
function buildEscapeSection(el, s, actions) {
|
|
1425
|
+
const list = Array.isArray(s.escapePresets) ? s.escapePresets : []
|
|
1426
|
+
return el('div', { className: 'sap-rowCard' },
|
|
1427
|
+
el('div', { className: 'sap-rowHead' },
|
|
1428
|
+
el('span', { className: 'sap-rowIdentity' },
|
|
1429
|
+
el('span', { className: 'sap-rowName' }, ZH.escapeTitle)
|
|
1430
|
+
),
|
|
1431
|
+
el('span', { className: 'sap-rowActions' },
|
|
1432
|
+
el('label', { className: 'sap-switch', title: '逃生舱开关' },
|
|
1433
|
+
el('input', { type: 'checkbox', checked: s.escapeEnabled, onChange: actions.toggleEscape }),
|
|
1434
|
+
el('span', null, s.escapeEnabled ? '已启用' : '已禁用')
|
|
1435
|
+
)
|
|
1436
|
+
)
|
|
1437
|
+
),
|
|
1438
|
+
s.escapeEnabled
|
|
1439
|
+
? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
|
|
1440
|
+
: null,
|
|
1441
|
+
el('div', { className: 'sap-escapeAdd' },
|
|
1442
|
+
el('input', {
|
|
1443
|
+
className: 'sap-input',
|
|
1444
|
+
style: { flex: '1 1 0', minWidth: '160px' },
|
|
1445
|
+
value: s.escapeInput,
|
|
1446
|
+
placeholder: ZH.escapeInputPlaceholder,
|
|
1447
|
+
onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
|
|
1448
|
+
}),
|
|
1449
|
+
el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
|
|
1450
|
+
),
|
|
1451
|
+
el('div', { className: 'sap-chips' },
|
|
1452
|
+
list.length === 0
|
|
1453
|
+
? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
|
|
1454
|
+
: list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
|
|
1455
|
+
String(id),
|
|
1456
|
+
el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
|
|
1457
|
+
))
|
|
1458
|
+
)
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1461
|
+
|
|
906
1462
|
function buildSectionReturn(el, s, actions, rows, addCard) {
|
|
907
1463
|
return el('div', { className: 'sap-section' },
|
|
908
1464
|
el('div', { className: 'sap-titleRow' },
|
|
@@ -915,10 +1471,16 @@ function buildSectionReturn(el, s, actions, rows, addCard) {
|
|
|
915
1471
|
s.versionWarnings.length > 0
|
|
916
1472
|
? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
|
|
917
1473
|
: null,
|
|
1474
|
+
s.audit.health === 'degraded'
|
|
1475
|
+
? el('p', { className: 'sap-notice', role: 'alert' },
|
|
1476
|
+
`${ZH.auditDegraded}(${ZH.auditLost}:派发遥测 ${s.audit.lostTelemetry} · 治理 ${s.audit.lostGovernance})`)
|
|
1477
|
+
: null,
|
|
918
1478
|
el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
|
|
919
1479
|
el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
|
|
920
1480
|
el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.resetAll }, '重置所有内置方案')
|
|
921
1481
|
),
|
|
1482
|
+
buildAdviceSection(el, s, actions),
|
|
1483
|
+
buildEscapeSection(el, s, actions),
|
|
922
1484
|
!s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
|
|
923
1485
|
s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
|
|
924
1486
|
s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
|
|
@@ -945,8 +1507,12 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
|
|
|
945
1507
|
const [form, setForm] = useState(EMPTY_FORM)
|
|
946
1508
|
const [tools, setTools] = useState([])
|
|
947
1509
|
const [versionWarnings, setVersionWarnings] = useState([])
|
|
948
|
-
const
|
|
949
|
-
|
|
1510
|
+
const [audit, setAudit] = useState(AUDIT_DEFAULTS)
|
|
1511
|
+
const [escapeEnabled, setEscapeEnabled] = useState(false)
|
|
1512
|
+
const [escapePresets, setEscapePresets] = useState([])
|
|
1513
|
+
const [escapeInput, setEscapeInput] = useState('')
|
|
1514
|
+
const [evolutionAdvice, setEvolutionAdvice] = useState(false)
|
|
1515
|
+
const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice }
|
|
950
1516
|
const refresh = useCallback(() => {
|
|
951
1517
|
api('/list')
|
|
952
1518
|
.then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
|
|
@@ -954,20 +1520,12 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
|
|
|
954
1520
|
}, [])
|
|
955
1521
|
const loadOptions = useCallback(() => {
|
|
956
1522
|
api('/options/summary')
|
|
957
|
-
.then((data) => {
|
|
958
|
-
setEnabled(data.enabled !== false)
|
|
959
|
-
setOptions({
|
|
960
|
-
models: Array.isArray(data.models) ? data.models : [],
|
|
961
|
-
efforts: {},
|
|
962
|
-
presets: Array.isArray(data.presets) ? data.presets : []
|
|
963
|
-
})
|
|
964
|
-
})
|
|
1523
|
+
.then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice }))
|
|
965
1524
|
.catch(() => {})
|
|
966
1525
|
}, [])
|
|
967
1526
|
const loadEfforts = makeEffortsLoader(api, setOptions)
|
|
968
1527
|
const loadTools = makeToolsLoader(api, setTools)
|
|
969
|
-
// 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect
|
|
970
|
-
// 导致设置页打开期间无限重取(与 loadOptions 同款稳定化)。
|
|
1528
|
+
// 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变导致设置页打开期间无限重取。
|
|
971
1529
|
const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings)(), [])
|
|
972
1530
|
useEffect(() => {
|
|
973
1531
|
refresh()
|
|
@@ -983,6 +1541,627 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
|
|
|
983
1541
|
return ProfilesSection
|
|
984
1542
|
}
|
|
985
1543
|
|
|
1544
|
+
// ── 派发账本:会话标签页(React 依赖经参数注入)────────────────────────────
|
|
1545
|
+
|
|
1546
|
+
// epoch ms → 本地 HH:MM:SS;非有限数返回空串。
|
|
1547
|
+
function formatClockTime(time) {
|
|
1548
|
+
if (typeof time !== 'number' || !Number.isFinite(time)) return ''
|
|
1549
|
+
const d = new Date(time)
|
|
1550
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
1551
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// 毫秒 → 人读耗时:<1s 显示 ms,<1m 显示 s(1 位小数),否则 m+s;无值/0 返回空串。
|
|
1555
|
+
function formatDurationMs(ms) {
|
|
1556
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) return ''
|
|
1557
|
+
if (ms < 1000) return `${Math.round(ms)}ms`
|
|
1558
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
1559
|
+
const m = Math.floor(ms / 60000)
|
|
1560
|
+
const s = Math.round((ms % 60000) / 1000)
|
|
1561
|
+
return s > 0 ? `${m}m${s}s` : `${m}m`
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// 闸输入/输出值 → 单行 JSON(等宽次级色展示);不可序列化回退 String。
|
|
1565
|
+
function formatGateValue(value) {
|
|
1566
|
+
if (value === undefined || value === null) return ''
|
|
1567
|
+
try {
|
|
1568
|
+
const s = JSON.stringify(value)
|
|
1569
|
+
return s === undefined ? '' : s
|
|
1570
|
+
} catch {
|
|
1571
|
+
return String(value)
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// 闸名中文标签(仅中文,供汇总带紧凑展示);未知/空名原样返回。
|
|
1576
|
+
function gateLabelZh(name) {
|
|
1577
|
+
return GATE_LABELS[name] || name
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// 闸名标签(中文 + 英文技术标识,供时间线闸头展示)。
|
|
1581
|
+
function gateLabel(name) {
|
|
1582
|
+
const zh = GATE_LABELS[name]
|
|
1583
|
+
return zh !== undefined ? `${zh}(${name})` : name
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// 失败闸修复方向;未知闸给通用提示。
|
|
1587
|
+
function gateFixHint(name) {
|
|
1588
|
+
return GATE_FIX_HINTS[name] || ZH.gateFixUnknown
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// 派发方式 → 摘要行 chip 文案。
|
|
1592
|
+
function kindLabel(kind) {
|
|
1593
|
+
if (kind === 'background') return ZH.toolBackground
|
|
1594
|
+
if (kind === 'continuable') return ZH.cont
|
|
1595
|
+
if (kind === 'foreground') return ZH.toolForeground
|
|
1596
|
+
return kind
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// 记录档位:effective.tokenTier 优先,回退 result.fields.tokenTier(旧记录)。
|
|
1600
|
+
function recordTier(record) {
|
|
1601
|
+
const trace = record && record.trace
|
|
1602
|
+
const result = record && record.result
|
|
1603
|
+
const eff = trace && trace.effective
|
|
1604
|
+
if (eff && typeof eff.tokenTier === 'string' && eff.tokenTier !== '') return eff.tokenTier
|
|
1605
|
+
if (result && result.fields && typeof result.fields.tokenTier === 'string') return result.fields.tokenTier
|
|
1606
|
+
return ''
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// 记录模型:effective.model 优先,回退 result.fields.model(旧记录口径,同档位读取)。
|
|
1610
|
+
function recordModel(record) {
|
|
1611
|
+
const trace = record && record.trace
|
|
1612
|
+
const result = record && record.result
|
|
1613
|
+
const eff = trace && trace.effective
|
|
1614
|
+
if (eff && typeof eff.model === 'string' && eff.model !== '') return eff.model
|
|
1615
|
+
if (result && result.fields && typeof result.fields.model === 'string') return result.fields.model
|
|
1616
|
+
return ''
|
|
1617
|
+
}
|
|
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
|
+
function recordUsage(record) {
|
|
1635
|
+
const trace = record && record.trace
|
|
1636
|
+
const result = record && record.result
|
|
1637
|
+
if (trace && trace.settled) {
|
|
1638
|
+
const usage = parseChildUsage(trace.settled.childUsage)
|
|
1639
|
+
if (usage !== null) return usage
|
|
1640
|
+
}
|
|
1641
|
+
if (result && result.fields && result.fields.childUsage !== undefined && result.fields.childUsage !== '') {
|
|
1642
|
+
return parseChildUsage(result.fields.childUsage)
|
|
1643
|
+
}
|
|
1644
|
+
return null
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// 逐次调用明细:读 trace.settled.calls({calls:[{五段}], truncated, totalCalls}),
|
|
1648
|
+
// 逐字段 typeof 守卫、坏条目跳过;无 calls 或全坏返回 null(整区不渲染)。calls 仅
|
|
1649
|
+
// 账本展开区专用,不进 render 行 / 模型可见面。
|
|
1650
|
+
function recordCalls(record) {
|
|
1651
|
+
const settled = record && record.trace && record.trace.settled
|
|
1652
|
+
const raw = settled && settled.calls
|
|
1653
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
1654
|
+
if (!Array.isArray(raw.calls) || raw.calls.length === 0) return null
|
|
1655
|
+
const keys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
|
|
1656
|
+
const calls = []
|
|
1657
|
+
for (const item of raw.calls) {
|
|
1658
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
|
|
1659
|
+
const call = {}
|
|
1660
|
+
for (const key of keys) {
|
|
1661
|
+
const value = item[key]
|
|
1662
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) call[key] = value
|
|
1663
|
+
}
|
|
1664
|
+
calls.push(call)
|
|
1665
|
+
}
|
|
1666
|
+
if (calls.length === 0) return null
|
|
1667
|
+
const totalCalls = typeof raw.totalCalls === 'number' && Number.isFinite(raw.totalCalls) && raw.totalCalls >= 0
|
|
1668
|
+
? raw.totalCalls
|
|
1669
|
+
: calls.length
|
|
1670
|
+
return { calls, truncated: raw.truncated === true, totalCalls }
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// 记录结束原因:结算 stopReason 优先,回退 result.fields.stopReason;映射中文。
|
|
1674
|
+
function recordStopZh(record) {
|
|
1675
|
+
const settled = record && record.trace && record.trace.settled
|
|
1676
|
+
const result = record && record.result
|
|
1677
|
+
const raw = settled && typeof settled.stopReason === 'string' ? settled.stopReason
|
|
1678
|
+
: (result && result.fields && typeof result.fields.stopReason === 'string' ? result.fields.stopReason : '')
|
|
1679
|
+
return raw === '' ? '' : (ZH.stopReasonZh[raw] || raw)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// 记录行稳定 key:callId 优先,其次时间戳,最后下标兜底。
|
|
1683
|
+
function ledgerKey(record, index) {
|
|
1684
|
+
if (record.callId !== '') return record.callId
|
|
1685
|
+
if (record.time !== null) return `ledger-${record.time}-${index}`
|
|
1686
|
+
return `ledger-${index}`
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ── 账本渲染 helpers(纯元素组装)──────────────────────────────────────────
|
|
1690
|
+
|
|
1691
|
+
// 汇总带单条统计段:label 次级色 + 值加粗,段间「·」分隔。
|
|
1692
|
+
function summarySegments(el, pairs) {
|
|
1693
|
+
const out = []
|
|
1694
|
+
pairs.forEach((pair, i) => {
|
|
1695
|
+
if (i > 0) out.push(el('span', { key: `sep${i}`, className: 'sap-ledgerSep' }, ' · '))
|
|
1696
|
+
out.push(el('span', { key: `seg${i}`, className: 'sap-ledgerStatSeg' }, pair[0], ' ', el('span', { className: 'sap-ledgerStat' }, pair[1])))
|
|
1697
|
+
})
|
|
1698
|
+
return out
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// 汇总带:派发/成功/拒绝/token/耗时 + 闸拒绝分布 + 按方案(中文名双表达)+ 按模型/按档位。
|
|
1702
|
+
function buildLedgerSummary(el, summary, profileNames) {
|
|
1703
|
+
const elapsed = formatDurationMs(summary.totalElapsedMs)
|
|
1704
|
+
const pairs = [
|
|
1705
|
+
[ZH.ledgerDispatch, `${summary.dispatchCount} ${ZH.ledgerTimes}`],
|
|
1706
|
+
[ZH.ledgerSuccess, String(summary.successCount)],
|
|
1707
|
+
[ZH.ledgerReject, String(summary.rejectCount)],
|
|
1708
|
+
[ZH.ledgerTotalTokens, `≈${summary.totalTokens}(${ZH.cacheHit} ${summary.cacheHitTokens})`]
|
|
1709
|
+
]
|
|
1710
|
+
if (elapsed !== '') pairs.push([ZH.ledgerTotalElapsed, elapsed])
|
|
1711
|
+
const gateEntries = ['whitelist', 'cost', 'intersection', 'approval', 'other']
|
|
1712
|
+
.filter((name) => summary.gateRejectCounts[name] > 0)
|
|
1713
|
+
.map((name) => `${gateLabelZh(name)} ${summary.gateRejectCounts[name]}`)
|
|
1714
|
+
const profileText = summary.byProfile.map((item) => `${profileDisplay(item.profile, profileNames)} ×${item.count}`).join(' · ')
|
|
1715
|
+
// 模型原文显示;继承桶(空串)与 'inherit'/'(parent)'/以 '(' 开头的标记一律归一为「继承父」。
|
|
1716
|
+
const modelText = summary.byModel.map((item) => {
|
|
1717
|
+
const display = item.model === 'inherit' || item.model.startsWith('(') ? ZH.inherit : rawOrInherit(item.model)
|
|
1718
|
+
return `${display} ×${item.count}`
|
|
1719
|
+
}).join(' · ')
|
|
1720
|
+
const tierText = summary.byTier.map((item) => `${ZH.tierZh[item.tier] || item.tier} ×${item.count}`).join(' · ')
|
|
1721
|
+
return el('div', { className: 'sap-ledgerSummary' },
|
|
1722
|
+
el('div', { className: 'sap-ledgerSummaryTitle' }, ZH.ledgerSummary),
|
|
1723
|
+
el('div', { className: 'sap-ledgerSummaryLine' }, summarySegments(el, pairs)),
|
|
1724
|
+
gateEntries.length > 0
|
|
1725
|
+
? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerGateRejectDist}:${gateEntries.join(' · ')}`)
|
|
1726
|
+
: null,
|
|
1727
|
+
profileText !== ''
|
|
1728
|
+
? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByProfile}:${profileText}`)
|
|
1729
|
+
: null,
|
|
1730
|
+
modelText !== ''
|
|
1731
|
+
? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByModel}:${modelText}`)
|
|
1732
|
+
: null,
|
|
1733
|
+
tierText !== ''
|
|
1734
|
+
? el('div', { className: 'sap-ledgerSummaryLine' }, `${ZH.ledgerByTier}:${tierText}`)
|
|
1735
|
+
: null
|
|
1736
|
+
)
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// 空态:暂无派发记录 + 一句引导。
|
|
1740
|
+
function buildEmptyState(el) {
|
|
1741
|
+
return el('div', { className: 'sap-ledgerEmpty' },
|
|
1742
|
+
el('p', { className: 'sap-ledgerEmptyTitle' }, ZH.ledgerEmpty),
|
|
1743
|
+
el('p', { className: 'sap-intro' }, ZH.ledgerEmptyHint)
|
|
1744
|
+
)
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// 「加载更早」按钮:hasMore 为真且未在补载时可用;点击逐页补载更早记录。
|
|
1748
|
+
function buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder) {
|
|
1749
|
+
return el('button', {
|
|
1750
|
+
type: 'button',
|
|
1751
|
+
className: 'sap-ledgerLoadOlder',
|
|
1752
|
+
disabled: !hasMore || loadingOlder,
|
|
1753
|
+
onClick: () => { loadOlder() }
|
|
1754
|
+
}, loadingOlder ? ZH.ledgerLoadingOlder : ZH.ledgerLoadOlder)
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// 账本顶部说明行:定位说明(恒显)+ 窗口提示(hasMore 时显示已加载条数)。
|
|
1758
|
+
function buildLedgerTopNote(el, hasMore, count) {
|
|
1759
|
+
return el('div', null,
|
|
1760
|
+
el('div', { className: 'sap-ledgerScope' }, ZH.ledgerScopeNote),
|
|
1761
|
+
hasMore
|
|
1762
|
+
? el('div', { className: 'sap-ledgerWindowNote' }, ZH.ledgerWindowNote.replace('{n}', String(count)))
|
|
1763
|
+
: null
|
|
1764
|
+
)
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// 标量 → 直接文本(字符串不带引号,null/undefined 空串)。
|
|
1768
|
+
function formatGateScalar(v) {
|
|
1769
|
+
if (v === undefined || v === null) return ''
|
|
1770
|
+
if (typeof v === 'string') return v
|
|
1771
|
+
return String(v)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// 对象/数组 → 逐字段键值行数组(depth 上限 2 防深层爆炸);标量 → 单行文本。
|
|
1775
|
+
// 对象逐键「中文标签(键名): 值」;嵌套容器递归 1 层并包缩进块(sap-ledgerGateNested,
|
|
1776
|
+
// 左侧竖线视觉分层,profiles_snapshot 等嵌套结构可读);数组前 5 项逐项展示,
|
|
1777
|
+
// 超出补「…共 N 项」。未知键回退原样(不崩溃)。
|
|
1778
|
+
function gateValueRows(el, value, depth = 0) {
|
|
1779
|
+
if (value === null || typeof value !== 'object') {
|
|
1780
|
+
return [el('div', { className: 'sap-ledgerGateKV' },
|
|
1781
|
+
el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(value)))]
|
|
1782
|
+
}
|
|
1783
|
+
if (Array.isArray(value)) {
|
|
1784
|
+
const rows = []
|
|
1785
|
+
for (const item of value.slice(0, 5)) {
|
|
1786
|
+
if (item !== null && typeof item === 'object' && depth < 2) {
|
|
1787
|
+
rows.push(el('div', { className: 'sap-ledgerGateNested' }, ...gateValueRows(el, item, depth + 1)))
|
|
1788
|
+
} else {
|
|
1789
|
+
rows.push(el('div', { className: 'sap-ledgerGateKV' },
|
|
1790
|
+
el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(item))))
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (value.length > 5) {
|
|
1794
|
+
rows.push(el('div', { className: 'sap-ledgerGateKV' },
|
|
1795
|
+
el('span', { className: 'sap-ledgerGateValue' }, `…共 ${value.length} 项`)))
|
|
1796
|
+
}
|
|
1797
|
+
return rows
|
|
1798
|
+
}
|
|
1799
|
+
const rows = []
|
|
1800
|
+
for (const [key, v] of Object.entries(value)) {
|
|
1801
|
+
const label = GATE_KEY_LABELS[key] !== undefined ? `${GATE_KEY_LABELS[key]}(${key})` : key
|
|
1802
|
+
if (v !== null && typeof v === 'object' && depth < 2) {
|
|
1803
|
+
rows.push(el('div', { className: 'sap-ledgerGateKV' },
|
|
1804
|
+
el('span', { className: 'sap-ledgerGateKey' }, label), ':'))
|
|
1805
|
+
rows.push(el('div', { className: 'sap-ledgerGateNested' }, ...gateValueRows(el, v, depth + 1)))
|
|
1806
|
+
} else {
|
|
1807
|
+
rows.push(el('div', { className: 'sap-ledgerGateKV' },
|
|
1808
|
+
el('span', { className: 'sap-ledgerGateKey' }, label), ':',
|
|
1809
|
+
el('span', { className: 'sap-ledgerGateValue' }, formatGateScalar(v))))
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return rows
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// 子块卡片:标题 + 字段级行(空对象/空数组显示 `{}`/`[]` 单行,不空白)。
|
|
1816
|
+
// 供步骤①(父会话上下文/请求值)与各闸(输入/输出)共用——每段独立卡片、
|
|
1817
|
+
// 独立标题,与闸头/拒绝信息视觉分层(不再平铺扎堆)。
|
|
1818
|
+
function buildSubBlock(el, title, value) {
|
|
1819
|
+
const rows = value !== null && typeof value === 'object' && Object.keys(value).length > 0
|
|
1820
|
+
? gateValueRows(el, value, 0)
|
|
1821
|
+
: [el('div', { className: 'sap-ledgerGateKV' },
|
|
1822
|
+
el('span', { className: 'sap-ledgerGateValue' }, formatGateValue(value)))]
|
|
1823
|
+
return el('div', { className: 'sap-ledgerSubBlock' },
|
|
1824
|
+
el('div', { className: 'sap-ledgerSubBlockTitle' }, title),
|
|
1825
|
+
el('div', { className: 'sap-ledgerSubBlockBody' }, rows)
|
|
1826
|
+
)
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// 区块视图切换按钮:字段视图 ↔ 原始 JSON(点击整体切换,非追加)。active 为真时
|
|
1830
|
+
// 显示「字段视图」提示可切回。
|
|
1831
|
+
function buildViewToggle(el, active, onToggle) {
|
|
1832
|
+
return el('button', {
|
|
1833
|
+
type: 'button',
|
|
1834
|
+
className: 'sap-ledgerRawJson',
|
|
1835
|
+
onClick: onToggle,
|
|
1836
|
+
'aria-expanded': active
|
|
1837
|
+
}, active ? ZH.ledgerFieldView : ZH.ledgerRawJson)
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// JSON 视图内容:kv 各段原始 JSON 逐块展示(等宽可换行),与字段视图互斥。
|
|
1841
|
+
function buildJsonBody(el, kv) {
|
|
1842
|
+
return el('div', { className: 'sap-ledgerJsonBody' },
|
|
1843
|
+
kv.map(({ label, value }) => el('div', { key: label, className: 'sap-ledgerJsonBlock' },
|
|
1844
|
+
el('div', { className: 'sap-ledgerGateKey' }, label),
|
|
1845
|
+
el('div', { className: 'sap-ledgerGateValue' }, formatGateValue(value))
|
|
1846
|
+
))
|
|
1847
|
+
)
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// 决策时间线步骤 ①:发起时间 + 父会话上下文 + 请求参数(各为独立子块卡片;
|
|
1851
|
+
// 头部右侧按钮切换 字段视图 ↔ 原始 JSON)。
|
|
1852
|
+
function buildStartStep(el, trace, jsonView, toggleJson) {
|
|
1853
|
+
const startedText = formatClockTime(trace.startedAt)
|
|
1854
|
+
const head = startedText !== '' ? `${ZH.ledgerStarted} · ${startedText}` : ZH.ledgerStarted
|
|
1855
|
+
const jsonMode = jsonView['start'] === true
|
|
1856
|
+
const kv = [
|
|
1857
|
+
{ label: ZH.ledgerParentContext, value: trace.parentContext },
|
|
1858
|
+
{ label: ZH.ledgerRequested, value: trace.requested }
|
|
1859
|
+
]
|
|
1860
|
+
return el('div', { key: 'step-start', className: 'sap-ledgerStep' },
|
|
1861
|
+
el('div', { className: 'sap-ledgerStepHead' },
|
|
1862
|
+
el('span', null, head),
|
|
1863
|
+
buildViewToggle(el, jsonMode, () => toggleJson('start'))
|
|
1864
|
+
),
|
|
1865
|
+
jsonMode
|
|
1866
|
+
? buildJsonBody(el, kv)
|
|
1867
|
+
: el('div', { className: 'sap-ledgerStepBody' },
|
|
1868
|
+
buildSubBlock(el, ZH.ledgerParentContext, trace.parentContext),
|
|
1869
|
+
buildSubBlock(el, ZH.ledgerRequested, trace.requested)
|
|
1870
|
+
)
|
|
1871
|
+
)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// 单道闸三段式:闸头(名称 + verdict + 视图切换)→ 输入/输出子块(或合并 JSON 视图)
|
|
1875
|
+
// → 结论;失败闸浅红底 + 原因 + 修复方向。拒绝信息两种视图下都显示。
|
|
1876
|
+
function buildGateStep(el, gate, index, jsonView, toggleJson) {
|
|
1877
|
+
const failed = gate.verdict === 'fail'
|
|
1878
|
+
const passed = gate.verdict === 'pass'
|
|
1879
|
+
const verdict = passed ? `✅ ${ZH.ledgerPass}` : (failed ? `❌ ${ZH.ledgerReject}` : gate.verdict)
|
|
1880
|
+
const verdictCls = failed ? 'sap-ledgerGateVerdictFail' : (passed ? 'sap-ledgerGateVerdictPass' : '')
|
|
1881
|
+
const key = `gate-${index}`
|
|
1882
|
+
const jsonMode = jsonView[key] === true
|
|
1883
|
+
const kv = [
|
|
1884
|
+
{ label: ZH.ledgerGateInput, value: gate.input },
|
|
1885
|
+
{ label: ZH.ledgerGateOutput, value: gate.output }
|
|
1886
|
+
]
|
|
1887
|
+
const rejectRows = failed
|
|
1888
|
+
? [
|
|
1889
|
+
el('div', { key: 'reject', className: 'sap-ledgerReject' }, gate.reason !== '' ? `${ZH.ledgerReject}:${gate.reason}` : ZH.ledgerReject),
|
|
1890
|
+
el('div', { key: 'rejectFix', className: 'sap-ledgerRejectFix' }, `${ZH.ledgerRejectFix}:${gateFixHint(gate.name)}`)
|
|
1891
|
+
]
|
|
1892
|
+
: []
|
|
1893
|
+
return el('div', { key, className: failed ? 'sap-ledgerGate sap-ledgerGateFail' : 'sap-ledgerGate' },
|
|
1894
|
+
el('div', { className: 'sap-ledgerGateHead' },
|
|
1895
|
+
el('span', { className: 'sap-ledgerGateName' }, gateLabel(gate.name)),
|
|
1896
|
+
el('span', { className: `sap-ledgerGateVerdict ${verdictCls}` }, verdict),
|
|
1897
|
+
buildViewToggle(el, jsonMode, () => toggleJson(key))
|
|
1898
|
+
),
|
|
1899
|
+
jsonMode
|
|
1900
|
+
? el('div', { className: 'sap-ledgerGateBody' }, buildJsonBody(el, kv), ...rejectRows)
|
|
1901
|
+
: el('div', { className: 'sap-ledgerGateBody' },
|
|
1902
|
+
buildSubBlock(el, ZH.ledgerGateInput, gate.input),
|
|
1903
|
+
buildSubBlock(el, ZH.ledgerGateOutput, gate.output),
|
|
1904
|
+
...rejectRows
|
|
1905
|
+
)
|
|
1906
|
+
)
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// 决策时间线:① 发起 + 各闸。步骤元素带稳定 key(静态列表也满足 React key 契约)。
|
|
1910
|
+
// jsonView/toggleJson:区块级「字段视图 ↔ 原始 JSON」切换状态(LedgerRow 持有)。
|
|
1911
|
+
function buildDecisionTimeline(el, record, jsonView, toggleJson) {
|
|
1912
|
+
const gates = record.trace && Array.isArray(record.trace.gates) ? record.trace.gates : []
|
|
1913
|
+
const blocks = [buildStartStep(el, record.trace, jsonView, toggleJson)]
|
|
1914
|
+
gates.forEach((gate, index) => blocks.push(buildGateStep(el, gate, index, jsonView, toggleJson)))
|
|
1915
|
+
return el('div', { className: 'sap-ledgerSection' },
|
|
1916
|
+
el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerDecisionTimeline),
|
|
1917
|
+
el('div', { className: 'sap-ledgerTimeline' }, blocks)
|
|
1918
|
+
)
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// 生效值 chips + ⬆被忽略项(沿用既有 ignored chip 风格)。
|
|
1922
|
+
function buildEffectiveSection(el, chip, record, profileNames) {
|
|
1923
|
+
const eff = record.trace.effective || {}
|
|
1924
|
+
// 拒绝路径无生效值(闸 fail 即中止派发)——整节省略。
|
|
1925
|
+
if (Object.keys(eff).length === 0) return null
|
|
1926
|
+
const requested = record.trace.requested || {}
|
|
1927
|
+
const chips = []
|
|
1928
|
+
chips.push(chip('profile', ZH.chipProfile, profileDisplay(eff.profile, profileNames)))
|
|
1929
|
+
chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: !eff.preset }))
|
|
1930
|
+
const p = rawOrInherit(eff.provider)
|
|
1931
|
+
const m = rawOrInherit(eff.model)
|
|
1932
|
+
chips.push(chip('providerModel', `${ZH.chipProvider}·${ZH.chipModel}`, p === ZH.inherit && m === ZH.inherit ? ZH.inherit : `${p} / ${m}`, { dim: p === ZH.inherit && m === ZH.inherit }))
|
|
1933
|
+
chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: !eff.reasoningEffort }))
|
|
1934
|
+
const tier = typeof eff.tokenTier === 'string' && eff.tokenTier !== '' ? eff.tokenTier : ''
|
|
1935
|
+
if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
|
|
1936
|
+
const ignored = Array.isArray(eff.ignored) ? eff.ignored : []
|
|
1937
|
+
for (const key of ignored) {
|
|
1938
|
+
const reqVal = typeof requested[key] === 'string' && requested[key] !== '' ? requested[key] : ''
|
|
1939
|
+
chips.push(el('span', {
|
|
1940
|
+
key: `ignored-${key}`,
|
|
1941
|
+
className: 'sap-chip sap-chipWarn',
|
|
1942
|
+
title: reqVal !== '' ? ZH.ignoredChipTitle.replace('{value}', reqVal) : ZH.ignoredChipTitleEmpty
|
|
1943
|
+
}, `⬆${key}(${ZH.ignoredMark})`))
|
|
1944
|
+
}
|
|
1945
|
+
return el('div', { className: 'sap-ledgerSection' },
|
|
1946
|
+
el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerEffectiveStep),
|
|
1947
|
+
el('div', { className: 'sap-chips' }, chips)
|
|
1948
|
+
)
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// 结算:stopReason / 耗时 / token + 五段分解(复用 usageBreakdownText)。
|
|
1952
|
+
// 仅 trace.settled 有值时渲染;settled 为 null(运行中/拒绝)整节省略。
|
|
1953
|
+
function buildSettledSection(el, record) {
|
|
1954
|
+
const settled = record.trace && record.trace.settled
|
|
1955
|
+
if (!settled) return null
|
|
1956
|
+
const { tokens, elapsedMs } = recordNumbers(record)
|
|
1957
|
+
const stopZh = recordStopZh(record)
|
|
1958
|
+
const usageText = usageBreakdownText(recordUsage(record))
|
|
1959
|
+
const parts = []
|
|
1960
|
+
if (stopZh !== '') parts.push(`${ZH.ledgerStopReason}=${stopZh}`)
|
|
1961
|
+
if (elapsedMs !== null) parts.push(`${ZH.ledgerElapsed}=${formatDurationMs(elapsedMs)}`)
|
|
1962
|
+
if (tokens !== null) parts.push(`≈${tokens} tokens`)
|
|
1963
|
+
if (usageText !== '') parts.push(usageText)
|
|
1964
|
+
if (parts.length === 0) return null
|
|
1965
|
+
return el('div', { className: 'sap-ledgerSection' },
|
|
1966
|
+
el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerSettledStep),
|
|
1967
|
+
el('div', { className: 'sap-ledgerUsage' }, parts.join(' · '))
|
|
1968
|
+
)
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// 逐次调用明细折叠区(默认收起):trace.settled.calls 逐条五段紧凑表格(序号 + 五列,
|
|
1972
|
+
// 缺失段显示 —)+ 末行合计(∑已存 calls,未截断时与 childUsage 五段分解对账一致)+
|
|
1973
|
+
// 截断提示。无 calls 数据时整区不渲染。
|
|
1974
|
+
function buildCallDetailSection(el, record) {
|
|
1975
|
+
const data = recordCalls(record)
|
|
1976
|
+
if (data === null) return null
|
|
1977
|
+
const keys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
|
|
1978
|
+
const labels = [ZH.usageInput, ZH.usageOutput, ZH.usageCacheRead, ZH.usageCacheWrite, ZH.usageReasoning]
|
|
1979
|
+
const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0 }
|
|
1980
|
+
const rows = data.calls.map((call, i) => {
|
|
1981
|
+
const cells = keys.map((key) => {
|
|
1982
|
+
const value = call[key]
|
|
1983
|
+
if (typeof value === 'number') totals[key] += value
|
|
1984
|
+
return el('td', { key }, typeof value === 'number' ? String(value) : '—')
|
|
1985
|
+
})
|
|
1986
|
+
return el('tr', { key: i },
|
|
1987
|
+
el('td', { className: 'sap-ledgerCallsIndex' }, String(i + 1)),
|
|
1988
|
+
...cells
|
|
1989
|
+
)
|
|
1990
|
+
})
|
|
1991
|
+
const totalCells = keys.map((key) => el('td', { key, className: 'sap-ledgerCallsTotal' }, String(totals[key])))
|
|
1992
|
+
const note = data.truncated
|
|
1993
|
+
? el('div', { className: 'sap-ledgerCallsNote' },
|
|
1994
|
+
ZH.ledgerCallsTruncated.replace('{shown}', String(data.calls.length)).replace('{total}', String(data.totalCalls)))
|
|
1995
|
+
: null
|
|
1996
|
+
return el('details', { className: 'sap-customized' },
|
|
1997
|
+
el('summary', { className: 'sap-customizedSummary' }, ZH.ledgerCallDetail),
|
|
1998
|
+
el('div', { className: 'sap-customizedBody' },
|
|
1999
|
+
el('table', { className: 'sap-ledgerCallsTable' },
|
|
2000
|
+
el('thead', null,
|
|
2001
|
+
el('tr', null,
|
|
2002
|
+
el('th', { className: 'sap-ledgerCallsIndex' }, ZH.ledgerCallsIndex),
|
|
2003
|
+
keys.map((key, i) => el('th', { key }, labels[i]))
|
|
2004
|
+
)
|
|
2005
|
+
),
|
|
2006
|
+
el('tbody', null,
|
|
2007
|
+
rows,
|
|
2008
|
+
el('tr', { key: 'total' },
|
|
2009
|
+
el('td', { className: 'sap-ledgerCallsIndex sap-ledgerCallsTotal' }, ZH.ledgerCallsTotal),
|
|
2010
|
+
...totalCells
|
|
2011
|
+
)
|
|
2012
|
+
)
|
|
2013
|
+
),
|
|
2014
|
+
note
|
|
2015
|
+
)
|
|
2016
|
+
)
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// 子会话跳转:地址三字段齐全 →「打开子会话」;不完整 → 显示 id 供手动检索。
|
|
2020
|
+
// 后台派发只有 jobId(execute 返回时子会话尚未启动),无子会话可跳,恒走手动检索。
|
|
2021
|
+
function buildHierarchyHint(el, record, openChild) {
|
|
2022
|
+
const execution = (record.trace && record.trace.execution) || {}
|
|
2023
|
+
const parentSessionId = typeof execution.parentSessionId === 'string' ? execution.parentSessionId : ''
|
|
2024
|
+
const childSessionId = typeof execution.childSessionId === 'string' ? execution.childSessionId : ''
|
|
2025
|
+
const jobId = typeof execution.jobId === 'string' ? execution.jobId : ''
|
|
2026
|
+
const mode = typeof execution.mode === 'string' ? execution.mode : ''
|
|
2027
|
+
const canJump = parentSessionId !== '' && childSessionId !== '' && mode !== ''
|
|
2028
|
+
let body = null
|
|
2029
|
+
if (canJump) {
|
|
2030
|
+
body = el('button', {
|
|
2031
|
+
type: 'button',
|
|
2032
|
+
className: 'sap-detailToggle',
|
|
2033
|
+
onClick: () => openChild({ parentSessionId, childSessionId, mode })
|
|
2034
|
+
}, ZH.ledgerJumpChild)
|
|
2035
|
+
} else if (childSessionId !== '') {
|
|
2036
|
+
body = el('div', { className: 'sap-ledgerManual' }, `${ZH.ledgerChildManual}:`, el('code', { className: 'sap-ledgerManualId' }, childSessionId))
|
|
2037
|
+
} else if (jobId !== '') {
|
|
2038
|
+
body = el('div', { className: 'sap-ledgerManual' }, `${ZH.ledgerJobManual}:`, el('code', { className: 'sap-ledgerManualId' }, jobId))
|
|
2039
|
+
}
|
|
2040
|
+
if (body === null) return null
|
|
2041
|
+
return el('div', { className: 'sap-ledgerSection' },
|
|
2042
|
+
el('div', { className: 'sap-ledgerSectionTitle' }, ZH.ledgerExecutionStep),
|
|
2043
|
+
body
|
|
2044
|
+
)
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// 记录摘要行 chips:后台/continuable 派发方式 + 方案 + 档位。
|
|
2048
|
+
function buildRowChips(el, chip, record, profileNames) {
|
|
2049
|
+
const chips = []
|
|
2050
|
+
const kind = recordKind(record)
|
|
2051
|
+
if (kind === 'background' || kind === 'continuable') {
|
|
2052
|
+
chips.push(el('span', { key: 'kind', className: 'sap-chip' }, kindLabel(kind)))
|
|
2053
|
+
}
|
|
2054
|
+
const profile = recordProfile(record)
|
|
2055
|
+
if (profile !== '') chips.push(chip('profile', ZH.chipProfile, profileDisplay(profile, profileNames)))
|
|
2056
|
+
const tier = recordTier(record)
|
|
2057
|
+
if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
|
|
2058
|
+
return chips
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// 展开区:旧记录降级提示;新记录 = 决策时间线 → 生效值 → 执行信息
|
|
2062
|
+
// → 结算 → 调用级明细。
|
|
2063
|
+
function buildExpandArea(el, chip, record, isLegacy, openChild, profileNames, jsonView, toggleJson) {
|
|
2064
|
+
if (isLegacy) {
|
|
2065
|
+
return el('div', { className: 'sap-ledgerExpand' },
|
|
2066
|
+
el('div', { className: 'sap-ledgerLegacy' }, ZH.ledgerLegacy)
|
|
2067
|
+
)
|
|
2068
|
+
}
|
|
2069
|
+
return el('div', { className: 'sap-ledgerExpand' },
|
|
2070
|
+
buildDecisionTimeline(el, record, jsonView, toggleJson),
|
|
2071
|
+
buildEffectiveSection(el, chip, record, profileNames),
|
|
2072
|
+
buildHierarchyHint(el, record, openChild),
|
|
2073
|
+
buildSettledSection(el, record),
|
|
2074
|
+
buildCallDetailSection(el, record)
|
|
2075
|
+
)
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// ── 账本 maker(React 依赖经参数注入)───────────────────────────────────────
|
|
2079
|
+
|
|
2080
|
+
function makeLedgerRow(el, chip, useState, openChild) {
|
|
2081
|
+
function LedgerRow({ record, profileNames }) {
|
|
2082
|
+
const [open, setOpen] = useState(false)
|
|
2083
|
+
// 区块级视图切换:jsonView[key] 为 true 时该区块显示原始 JSON(key='start' 或
|
|
2084
|
+
// `gate-<index>`),false/缺省显示字段级子块视图。各区块独立切换。
|
|
2085
|
+
const [jsonView, setJsonView] = useState({})
|
|
2086
|
+
const toggleJson = (key) => setJsonView((prev) => ({ ...prev, [key]: !(prev[key] === true) }))
|
|
2087
|
+
const isLegacy = record.legacy === true
|
|
2088
|
+
const numbers = recordNumbers(record)
|
|
2089
|
+
const rejected = recordIsRejected(record)
|
|
2090
|
+
const stopZh = rejected ? '' : recordStopZh(record)
|
|
2091
|
+
const metaParts = []
|
|
2092
|
+
if (numbers.tokens !== null) metaParts.push(`≈${numbers.tokens} tokens`)
|
|
2093
|
+
if (numbers.elapsedMs !== null) metaParts.push(`${ZH.ledgerElapsed}=${numbers.elapsedMs}ms`)
|
|
2094
|
+
if (stopZh !== '') metaParts.push(`${ZH.ledgerStopReason}=${stopZh}`)
|
|
2095
|
+
return el('li', { className: open ? 'sap-ledgerRow sap-ledgerRowOpen' : 'sap-ledgerRow' },
|
|
2096
|
+
el('button', {
|
|
2097
|
+
type: 'button',
|
|
2098
|
+
className: 'sap-ledgerRowHead',
|
|
2099
|
+
'aria-expanded': open,
|
|
2100
|
+
onClick: () => setOpen((prev) => !prev)
|
|
2101
|
+
},
|
|
2102
|
+
el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
|
|
2103
|
+
el('span', { className: 'sap-chips' }, buildRowChips(el, chip, record, profileNames)),
|
|
2104
|
+
rejected ? el('span', { className: 'sap-ledgerReject' }, `❌ ${ZH.ledgerReject}`) : null,
|
|
2105
|
+
el('span', { className: 'sap-ledgerMeta' }, metaParts.join(' · ')),
|
|
2106
|
+
el('span', { className: 'sap-ledgerToggle' }, '▸')
|
|
2107
|
+
),
|
|
2108
|
+
open ? buildExpandArea(el, chip, record, isLegacy, openChild, profileNames, jsonView, toggleJson) : null
|
|
2109
|
+
)
|
|
2110
|
+
}
|
|
2111
|
+
return LedgerRow
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function makeDispatchLedgerView(el, chip, useState, useEffect, openChild) {
|
|
2115
|
+
const LedgerRow = makeLedgerRow(el, chip, useState, openChild)
|
|
2116
|
+
function DispatchLedgerView({ useSession, sessionId, loadOlder = () => {} }) {
|
|
2117
|
+
const nodes = useSession((s) => s?.chat?.legacy?.nodes)
|
|
2118
|
+
const hasMore = useSession((s) => s?.hasMore)
|
|
2119
|
+
const loadingOlder = useSession((s) => s?.loadingOlder)
|
|
2120
|
+
const [failures, setFailures] = useState([])
|
|
2121
|
+
const [profileNames, setProfileNames] = useState({})
|
|
2122
|
+
useEffect(() => {
|
|
2123
|
+
let cancelled = false
|
|
2124
|
+
fetchLedgerFailures(sessionId).then((list) => {
|
|
2125
|
+
if (!cancelled) setFailures(list)
|
|
2126
|
+
})
|
|
2127
|
+
return () => { cancelled = true }
|
|
2128
|
+
}, [sessionId])
|
|
2129
|
+
// profile 注册表 id→中文名映射:账本挂载时拉一次 /list,用于方案名中英双表达
|
|
2130
|
+
// (「标准编码(swap-standard)」);拉取失败保留空映射,显示层回退纯 id。
|
|
2131
|
+
useEffect(() => {
|
|
2132
|
+
let cancelled = false
|
|
2133
|
+
api('/list').then((data) => {
|
|
2134
|
+
if (cancelled || data === undefined || !Array.isArray(data.profiles)) return
|
|
2135
|
+
const map = {}
|
|
2136
|
+
for (const profile of data.profiles) {
|
|
2137
|
+
if (profile !== null && typeof profile === 'object' && typeof profile.id === 'string' && typeof profile.name === 'string' && profile.name !== '') {
|
|
2138
|
+
map[profile.id] = profile.name
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
setProfileNames(map)
|
|
2142
|
+
}).catch(() => {})
|
|
2143
|
+
return () => { cancelled = true }
|
|
2144
|
+
}, [])
|
|
2145
|
+
// 账本默认只统计已加载窗口;经「加载更早」逐页补载更早记录后 nodes 变多、
|
|
2146
|
+
// 统计自然扩大。
|
|
2147
|
+
const records = mergeFailureRecords(collectDispatchRecords(nodes), failures)
|
|
2148
|
+
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
|
+
return el('div', { className: 'sap-ledger' },
|
|
2156
|
+
buildLedgerTopNote(el, hasMore, records.length),
|
|
2157
|
+
buildLoadOlderButton(el, hasMore, loadingOlder, loadOlder),
|
|
2158
|
+
summary === null ? null : buildLedgerSummary(el, summary, profileNames),
|
|
2159
|
+
rows
|
|
2160
|
+
)
|
|
2161
|
+
}
|
|
2162
|
+
return DispatchLedgerView
|
|
2163
|
+
}
|
|
2164
|
+
|
|
986
2165
|
// ── plugin 装配 ─────────────────────────────────────────────────────────────
|
|
987
2166
|
|
|
988
2167
|
window.__ModuleLoader__.load({
|
|
@@ -992,8 +2171,8 @@ window.__ModuleLoader__.load({
|
|
|
992
2171
|
const { useEffect, useState, useCallback } = React
|
|
993
2172
|
const el = React.createElement
|
|
994
2173
|
|
|
995
|
-
|
|
996
|
-
const inject = ['slots']
|
|
2174
|
+
// slots:槽注册;sessions:子会话跳转(openSubagent)。两者皆 client 核心服务。
|
|
2175
|
+
const inject = ['slots', 'sessions']
|
|
997
2176
|
|
|
998
2177
|
const chip = makeChip(el)
|
|
999
2178
|
const DispatchToolview = makeDispatchToolview(el, chip, useState)
|
|
@@ -1011,6 +2190,20 @@ window.__ModuleLoader__.load({
|
|
|
1011
2190
|
order: 100,
|
|
1012
2191
|
label: '子 Agent 方案'
|
|
1013
2192
|
}, ProfilesSection))
|
|
2193
|
+
// 子会话跳转回调:经 sessions 服务按 SubagentAddress 打开子会话。
|
|
2194
|
+
const openChild = (address) => ctx.sessions.openSubagent(address)
|
|
2195
|
+
const DispatchLedgerView = makeDispatchLedgerView(el, chip, useState, useEffect, openChild)
|
|
2196
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
2197
|
+
name: 'conversation.view',
|
|
2198
|
+
id: 'dispatch-ledger',
|
|
2199
|
+
order: 20,
|
|
2200
|
+
label: ZH.ledgerTab,
|
|
2201
|
+
inject: (sessionId) => {
|
|
2202
|
+
const session = ctx.sessions.binding(sessionId)?.session
|
|
2203
|
+
if (session === void 0) throw new Error(`dsh-subagent-profile: 会话 "${sessionId}" 不可用(可能已归档或未加载)`)
|
|
2204
|
+
return { loadOlder: async () => { await session.loadOlder() } }
|
|
2205
|
+
}
|
|
2206
|
+
}, DispatchLedgerView))
|
|
1014
2207
|
}
|
|
1015
2208
|
|
|
1016
2209
|
return { apply, inject }
|