dsh-subagent-profile 0.3.0 → 0.3.2
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 +1 -1
- package/README.zh.md +1 -1
- package/index.mjs +24 -7
- package/lib/client.js +210 -23
- package/lib/core/catalog-cache.mjs +235 -0
- package/lib/core/catalog.mjs +2 -2
- package/lib/core/cost-guard.mjs +45 -31
- package/lib/core/delegation.mjs +76 -6
- package/lib/core/dispatch-schema.mjs +107 -0
- package/lib/core/dispatch-tool.mjs +110 -79
- package/lib/core/http-routes.mjs +48 -127
- package/lib/core/profile-provider.mjs +6 -5
- package/lib/core/profiles-store.mjs +3 -3
- package/lib/core/pure.mjs +33 -2
- package/lib/core/shims.mjs +87 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div align="center">
|
|
5
5
|
<b style="font-size: 1.15em;">Subagent dispatch, profiled — the right agent for the right task (preset / model / reasoning effort)</b><br /><br />
|
|
6
6
|
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
|
|
7
|
-
<img alt="Version: v0.3.
|
|
7
|
+
<img alt="Version: v0.3.2" src="https://img.shields.io/badge/Version-v0.3.2-blue.svg" />
|
|
8
8
|
<img alt="npm" src="https://img.shields.io/npm/v/dsh-subagent-profile.svg" />
|
|
9
9
|
<img alt="DSH" src="https://img.shields.io/badge/DSH-0.1.0--rc.6%20~%200.1.1--rc.2-blue.svg" />
|
|
10
10
|
</div>
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div align="center">
|
|
5
5
|
<b style="font-size: 1.15em;">子 Agent 派发方案化插件 —— 用对的人(预设 / 模型 / 推理强度)干对的事</b><br /><br />
|
|
6
6
|
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
|
|
7
|
-
<img alt="Version: v0.3.
|
|
7
|
+
<img alt="Version: v0.3.2" src="https://img.shields.io/badge/Version-v0.3.2-blue.svg" />
|
|
8
8
|
<img alt="npm" src="https://img.shields.io/npm/v/dsh-subagent-profile.svg" />
|
|
9
9
|
<img alt="DSH" src="https://img.shields.io/badge/DSH-0.1.0--rc.6%20~%200.1.1--rc.2-blue.svg" />
|
|
10
10
|
</div>
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// index.mjs — dsh-subagent-profile 宿主侧正式插件 bundle(只做装配)。
|
|
2
2
|
// 源自原型动态插件 `code.host` 主体;宿主导入面(registerTool/defineTool/
|
|
3
3
|
// handle)收敛在 lib/core/shims.mjs,装配块按模块拆分驻留 lib/core/(catalog /
|
|
4
4
|
// presets-sync / profiles-store / cost-guard / whitelist / intersection /
|
|
@@ -13,6 +13,8 @@ import { dshHome, createProfileStore } from './lib/core/profiles-store.mjs';
|
|
|
13
13
|
import { createHttpRoutes } from './lib/core/http-routes.mjs';
|
|
14
14
|
import { createProfileProvider } from './lib/core/profile-provider.mjs';
|
|
15
15
|
import { createDispatchTool } from './lib/core/dispatch-tool.mjs';
|
|
16
|
+
import { createCatalogCache } from './lib/core/catalog-cache.mjs';
|
|
17
|
+
import { tierSortKey } from './lib/core/pure.mjs';
|
|
16
18
|
|
|
17
19
|
export const name = 'dsh-subagent-profile';
|
|
18
20
|
export const inject = ['subagents', 'tools', 'agents'];
|
|
@@ -98,12 +100,13 @@ function profileSectionText(store, gate, context) {
|
|
|
98
100
|
if (!gate(context)) return '';
|
|
99
101
|
const rows = [...store.profiles.values()]
|
|
100
102
|
.filter((p) => p.enabled !== false)
|
|
103
|
+
.sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier))
|
|
101
104
|
.map((p) => {
|
|
102
105
|
const desc = typeof p.description === 'string' && p.description.length > 0 ? `"${p.description}"` : '(无描述)';
|
|
103
106
|
return `- ${p.id}: ${desc}${p.preset !== undefined ? ` (preset: ${p.preset})` : ''}`;
|
|
104
107
|
});
|
|
105
108
|
if (rows.length === 0) return '';
|
|
106
|
-
const note = '- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
|
|
109
|
+
const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
|
|
107
110
|
return `Available dispatch profiles (dispatch.profile):\n${rows.join('\n')}\n${note}`;
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -141,7 +144,7 @@ function registerSystemPromptSections(ctx, store, getEnabled) {
|
|
|
141
144
|
// (listen) is async and may not be ready when this plugin's inject deps
|
|
142
145
|
// resolve, so register inside an inject sub-scope that waits for it
|
|
143
146
|
// (ctx.get would read undefined at apply time).
|
|
144
|
-
function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool) {
|
|
147
|
+
function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog) {
|
|
145
148
|
ctx.inject(['webServer'], (scope) => {
|
|
146
149
|
scope.effect(createHttpRoutes({
|
|
147
150
|
webServer: scope.webServer,
|
|
@@ -149,14 +152,24 @@ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool) {
|
|
|
149
152
|
getEnabled,
|
|
150
153
|
setEnabled,
|
|
151
154
|
syncTool,
|
|
152
|
-
|
|
153
|
-
getAgentPresets: () => ctx.get('agentPresets'),
|
|
154
|
-
getTools: () => ctx.tools,
|
|
155
|
+
catalog,
|
|
155
156
|
logger: ctx.logger,
|
|
156
157
|
}), 'dsh-subagent-profile: settings routes');
|
|
157
158
|
});
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
// 进程级共享 catalog 快照工厂:一处快照同时喂 /options 三路由与 dispatch 的
|
|
162
|
+
// cost guard(cost guard 经 parent.ctx.get('llm') 传父实例,同一 host llm 下
|
|
163
|
+
// 命中同一条目)。
|
|
164
|
+
function createSharedCatalog(ctx) {
|
|
165
|
+
return createCatalogCache({
|
|
166
|
+
getLlm: () => ctx.get('llm'),
|
|
167
|
+
getAgentPresets: () => ctx.get('agentPresets'),
|
|
168
|
+
getTools: () => ctx.tools,
|
|
169
|
+
logger: ctx.logger,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
160
173
|
export async function apply(ctx) {
|
|
161
174
|
// Enable/disable switch (default on, runtime-toggled by the settings
|
|
162
175
|
// page, persisted across restarts) + profile registry — lib/profiles-store
|
|
@@ -167,6 +180,8 @@ export async function apply(ctx) {
|
|
|
167
180
|
store.loadProfiles();
|
|
168
181
|
// Self-install the bundled "orchestrator" preset (idempotent, fail-soft).
|
|
169
182
|
syncBundledPresetsToHome(ctx);
|
|
183
|
+
// 进程级共享 catalog 快照(/options 三路由 + dispatch cost guard 共用)。
|
|
184
|
+
const catalog = createSharedCatalog(ctx);
|
|
170
185
|
// `dispatch` tool — lib/core/dispatch-tool.mjs: defineTool block (schema +
|
|
171
186
|
// execute), the result-schema consistency lock and the syncTool
|
|
172
187
|
// register/unregister logic.
|
|
@@ -179,6 +194,7 @@ export async function apply(ctx) {
|
|
|
179
194
|
getService: (name) => ctx.get(name),
|
|
180
195
|
logger: ctx.logger,
|
|
181
196
|
subagents: ctx.subagents,
|
|
197
|
+
catalog,
|
|
182
198
|
});
|
|
183
199
|
// subagent-profiles service over the store's per-apply profiles Map.
|
|
184
200
|
provideProfileService(ctx, store);
|
|
@@ -191,10 +207,11 @@ export async function apply(ctx) {
|
|
|
191
207
|
store,
|
|
192
208
|
getEnabled: () => enabled,
|
|
193
209
|
logger: ctx.logger,
|
|
210
|
+
catalog,
|
|
194
211
|
});
|
|
195
212
|
if (typeof disposeProvider === 'function') ctx.effect(() => disposeProvider);
|
|
196
213
|
// HTTP loopback routes for the Client settings UI — lib/core/http-routes.mjs.
|
|
197
|
-
registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool);
|
|
214
|
+
registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool, catalog);
|
|
198
215
|
// Teardown: unregister the dispatch tool (if still registered).
|
|
199
216
|
ctx.effect(() => dispatch.dispose);
|
|
200
217
|
}
|
package/lib/client.js
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
* bordered card per row with a layered name / description / meta layout, and a
|
|
15
15
|
* dashed "+ 新增" button that expands an inline form card instead of a
|
|
16
16
|
* permanently-visible input row. Dropdown options (preset / provider / model /
|
|
17
|
-
* reasoning effort) are served by the host `/options`
|
|
18
|
-
*
|
|
17
|
+
* reasoning effort) are served by the host `/options/summary` (models + presets,
|
|
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.
|
|
19
21
|
*
|
|
20
22
|
* 结构:纯数据(CSS/ZH 文案表)与纯函数(api/解析/文案映射等)驻留模块作用域
|
|
21
23
|
* (load 调用之前的顶层声明);依赖 React 的组件经参数注入的 maker 函数装配
|
|
@@ -40,6 +42,7 @@ const CSS = [
|
|
|
40
42
|
'.sap-rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}',
|
|
41
43
|
'.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
|
|
42
44
|
'.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
|
|
45
|
+
'.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
46
|
'.sap-chips{flex-wrap:wrap;gap:6px;display:flex}',
|
|
44
47
|
'.sap-chip{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:6px;flex:none;align-items:center;gap:5px;padding:2px 8px;font-size:12px;line-height:18px;display:inline-flex}',
|
|
45
48
|
'.sap-chipLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
|
|
@@ -86,6 +89,10 @@ const CSS = [
|
|
|
86
89
|
'.sap-switch input:checked::before{left:18px}',
|
|
87
90
|
'.sap-toolview{flex-direction:column;align-items:flex-start;gap:6px;padding:4px 8px;display:flex}',
|
|
88
91
|
'.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
|
|
92
|
+
'.sap-toolSummary{align-items:center;gap:6px;flex-wrap:wrap;display:flex}',
|
|
93
|
+
'.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}',
|
|
94
|
+
'.sap-detailToggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
95
|
+
'.sap-toolDetail{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);display:flex}',
|
|
89
96
|
'.sap-toolModes{flex-wrap:wrap;gap:4px 16px;display:flex}',
|
|
90
97
|
'.sap-toolGroups{flex-direction:column;gap:8px;display:flex}',
|
|
91
98
|
'.sap-toolLayerCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px}',
|
|
@@ -116,12 +123,21 @@ const ZH = {
|
|
|
116
123
|
chipDisabled: '已禁用',
|
|
117
124
|
ignoredMark: '已忽略',
|
|
118
125
|
toolLabel: '派发子 Agent',
|
|
126
|
+
detailOpen: '详情',
|
|
127
|
+
detailClose: '收起',
|
|
119
128
|
cont: 'continuable 子 Agent',
|
|
120
129
|
canContinue: '可续跑',
|
|
121
130
|
toolBackground: '后台任务',
|
|
122
131
|
toolForeground: '前台',
|
|
123
132
|
toolRunning: '运行中',
|
|
124
133
|
tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
|
|
134
|
+
stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
|
|
135
|
+
cacheHit: '缓存命中',
|
|
136
|
+
usageInput: '输入',
|
|
137
|
+
usageOutput: '输出',
|
|
138
|
+
usageCacheRead: '缓存读',
|
|
139
|
+
usageCacheWrite: '缓存写',
|
|
140
|
+
usageReasoning: '推理',
|
|
125
141
|
persistFail: '已保存但未持久化',
|
|
126
142
|
persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)'
|
|
127
143
|
}
|
|
@@ -225,7 +241,7 @@ function readDispatchRequest(block) {
|
|
|
225
241
|
// 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定),且宿主未
|
|
226
242
|
// 定义 output.presentationMeta(block.meta 为空)。解析成功返回
|
|
227
243
|
// {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
|
|
228
|
-
const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier']
|
|
244
|
+
const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier', 'childTotalTokens', 'childUsage', 'elapsedMs', 'stopReason']
|
|
229
245
|
function parseDispatchText(text) {
|
|
230
246
|
if (typeof text !== 'string') return null
|
|
231
247
|
const firstLine = text.split('\n')[0] ?? ''
|
|
@@ -280,6 +296,103 @@ function readDispatchResult(block) {
|
|
|
280
296
|
return null
|
|
281
297
|
}
|
|
282
298
|
|
|
299
|
+
// childTotalTokens 文本后缀:宿主 render 行里的 `childTotalTokens=<number>` 经
|
|
300
|
+
// parseDispatchText 解析为字符串,此处归一为「≈N tokens」。仅前台/后台 completed
|
|
301
|
+
// 结算携带;无值返回空串(不显示)。渲染位置紧邻后续的耗时显示。
|
|
302
|
+
function tokensSuffix(result, block) {
|
|
303
|
+
let raw = null
|
|
304
|
+
if (result && result.fields && result.fields.childTotalTokens !== undefined && result.fields.childTotalTokens !== '') {
|
|
305
|
+
raw = result.fields.childTotalTokens
|
|
306
|
+
} else if (block && block.meta && typeof block.meta === 'object' && block.meta.childTotalTokens !== undefined) {
|
|
307
|
+
raw = block.meta.childTotalTokens
|
|
308
|
+
}
|
|
309
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw !== '' ? Number(raw) : NaN)
|
|
310
|
+
return Number.isFinite(n) ? ` · ≈${n} tokens` : ''
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 耗时 + 结束原因后缀:宿主 render 行里的 `elapsedMs=<n>` / `stopReason=<enum>`
|
|
314
|
+
// 经 parseDispatchText 解析为字符串,此处归一为「耗时=<n>ms · stopReason=<中文映射>」。
|
|
315
|
+
// 仅前台 completed 结算携带;后台结算经 job 结果呈现(不进 dispatch 卡片),
|
|
316
|
+
// continuable 不结算、不携带。任一字段缺失即省略该段。
|
|
317
|
+
function observabilitySuffix(result, block) {
|
|
318
|
+
let elapsedRaw = null
|
|
319
|
+
let stopRaw = null
|
|
320
|
+
if (result && result.fields) {
|
|
321
|
+
if (result.fields.elapsedMs !== undefined && result.fields.elapsedMs !== '') elapsedRaw = result.fields.elapsedMs
|
|
322
|
+
if (result.fields.stopReason !== undefined && result.fields.stopReason !== '') stopRaw = result.fields.stopReason
|
|
323
|
+
} else if (block && block.meta && typeof block.meta === 'object') {
|
|
324
|
+
// 运行中(无结果)或旧版结果:回退调用块 meta(向前兼容,与 tokenTier 同口径)。
|
|
325
|
+
if (block.meta.elapsedMs !== undefined) elapsedRaw = block.meta.elapsedMs
|
|
326
|
+
if (block.meta.stopReason !== undefined) stopRaw = block.meta.stopReason
|
|
327
|
+
}
|
|
328
|
+
const ms = typeof elapsedRaw === 'number' ? elapsedRaw : (typeof elapsedRaw === 'string' && elapsedRaw !== '' ? Number(elapsedRaw) : NaN)
|
|
329
|
+
const stopZh = typeof stopRaw === 'string' ? (ZH.stopReasonZh[stopRaw] || stopRaw) : ''
|
|
330
|
+
if (!Number.isFinite(ms) && stopZh === '') return ''
|
|
331
|
+
let out = ''
|
|
332
|
+
if (Number.isFinite(ms)) out += ` · 耗时=${ms}ms`
|
|
333
|
+
if (stopZh !== '') out += ` · stopReason=${stopZh}`
|
|
334
|
+
return out
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 把 childUsage 的(render 行 JSON 字符串或 block.meta 对象)归一为五段对象;
|
|
338
|
+
// 非 object / 非 number 段忽略,无任何有效段返回 null。可选字段(缓存读/写/推理)
|
|
339
|
+
// 缺值省略键,与 host 侧 collectChildUsage 输出对齐。
|
|
340
|
+
function parseChildUsage(raw) {
|
|
341
|
+
if (raw === undefined || raw === null || raw === '') return null
|
|
342
|
+
let usage = raw
|
|
343
|
+
if (typeof raw === 'string') {
|
|
344
|
+
try { usage = JSON.parse(raw) } catch { return null }
|
|
345
|
+
}
|
|
346
|
+
if (typeof usage !== 'object' || Array.isArray(usage)) return null
|
|
347
|
+
const out = {}
|
|
348
|
+
let any = false
|
|
349
|
+
for (const key of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']) {
|
|
350
|
+
const value = usage[key]
|
|
351
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
352
|
+
out[key] = value
|
|
353
|
+
any = true
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return any ? out : null
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 读取真实计费五段分解:result.fields.childUsage(render 行 JSON 串)优先,回退
|
|
360
|
+
// block.meta.childUsage(对象,向前兼容旧结果/未来 presentationMeta 投射)。
|
|
361
|
+
function usageFrom(result, block) {
|
|
362
|
+
if (result && result.fields && result.fields.childUsage !== undefined && result.fields.childUsage !== '') {
|
|
363
|
+
const parsed = parseChildUsage(result.fields.childUsage)
|
|
364
|
+
if (parsed !== null) return parsed
|
|
365
|
+
}
|
|
366
|
+
if (block && block.meta && typeof block.meta === 'object' && block.meta.childUsage !== undefined) {
|
|
367
|
+
return parseChildUsage(block.meta.childUsage)
|
|
368
|
+
}
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 缓存命中后缀:childUsage.cacheReadTokens 存在且 >0 时追加「 · 缓存命中 M」到
|
|
373
|
+
// ≈N tokens 之后(前导分隔符与 tokensSuffix/observabilitySuffix 同口径,即使
|
|
374
|
+
// tokens 段缺失也不会粘在一起);无分解或缓存读为 0/缺失时返回空串。
|
|
375
|
+
function cacheHitSuffix(usage) {
|
|
376
|
+
if (usage === null) return ''
|
|
377
|
+
const cacheRead = usage.cacheReadTokens
|
|
378
|
+
return typeof cacheRead === 'number' && cacheRead > 0 ? ` · ${ZH.cacheHit} ${cacheRead}` : ''
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 五段分解行文案(纯函数,供卡片详情区渲染);无任何有效段返回空串。
|
|
382
|
+
function usageBreakdownText(usage) {
|
|
383
|
+
if (usage === null || typeof usage !== 'object') return ''
|
|
384
|
+
const parts = []
|
|
385
|
+
const push = (label, value) => {
|
|
386
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) parts.push(`${label} ${value}`)
|
|
387
|
+
}
|
|
388
|
+
push(ZH.usageInput, usage.inputTokens)
|
|
389
|
+
push(ZH.usageOutput, usage.outputTokens)
|
|
390
|
+
push(ZH.usageCacheRead, usage.cacheReadTokens)
|
|
391
|
+
push(ZH.usageCacheWrite, usage.cacheWriteTokens)
|
|
392
|
+
push(ZH.usageReasoning, usage.reasoningTokens)
|
|
393
|
+
return parts.join(' · ')
|
|
394
|
+
}
|
|
395
|
+
|
|
283
396
|
// ── dispatch 工具卡(React 依赖经参数注入)─────────────────────────────────
|
|
284
397
|
|
|
285
398
|
// 生效值:结果解析成功用结果(优先),字段缺失回退请求值。
|
|
@@ -331,7 +444,7 @@ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
|
|
|
331
444
|
const m = rawOrInherit(eff.model)
|
|
332
445
|
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 }))
|
|
333
446
|
chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: eff.reasoningEffort === '' || eff.reasoningEffort === '(default)' }))
|
|
334
|
-
// tokenTier
|
|
447
|
+
// tokenTier:宿主 render 行已产出;result.fields 缺失时回退 block.meta(向前兼容旧结果)。
|
|
335
448
|
let tier = ''
|
|
336
449
|
if (result && result.fields && typeof result.fields.tokenTier === 'string' && result.fields.tokenTier !== '') {
|
|
337
450
|
tier = result.fields.tokenTier
|
|
@@ -367,7 +480,7 @@ function makeChip(el) {
|
|
|
367
480
|
return chip
|
|
368
481
|
}
|
|
369
482
|
|
|
370
|
-
function makeDispatchToolview(el, chip) {
|
|
483
|
+
function makeDispatchToolview(el, chip, useState) {
|
|
371
484
|
function DispatchToolview(props) {
|
|
372
485
|
const block = props && props.block
|
|
373
486
|
const request = readDispatchRequest(block)
|
|
@@ -375,12 +488,33 @@ function makeDispatchToolview(el, chip) {
|
|
|
375
488
|
const settled = !!(block && typeof block === 'object' && 'kind' in block)
|
|
376
489
|
const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
|
|
377
490
|
const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
491
|
+
const usage = usageFrom(result, block)
|
|
492
|
+
const [open, setOpen] = useState(false)
|
|
493
|
+
// 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
|
|
494
|
+
// 提供方/模型/推理强度/档位)全部在 chip 行——避免同一组信息重复两遍。
|
|
495
|
+
const text = `${ZH.toolLabel}:${kindLabel}${tokensSuffix(result, block)}${cacheHitSuffix(usage)}${observabilitySuffix(result, block)}`
|
|
496
|
+
// 详情折叠入口:仅当存在真实计费分解(childUsage)时渲染;无分解数据
|
|
497
|
+
// (旧结果/无 usage 会话)不显示折叠区,向前兼容。
|
|
498
|
+
const detailToggle = usage === null ? null : el('button', {
|
|
499
|
+
type: 'button',
|
|
500
|
+
className: 'sap-detailToggle',
|
|
501
|
+
'aria-expanded': open,
|
|
502
|
+
onClick: () => setOpen((prev) => !prev)
|
|
503
|
+
}, open ? ZH.detailClose : ZH.detailOpen)
|
|
504
|
+
// 无分解数据时保持原摘要行结构(零漂移);有分解时才加折叠入口与详情行。
|
|
505
|
+
const summary = usage === null
|
|
506
|
+
? el('div', { className: 'sap-toolText' }, text)
|
|
507
|
+
: el('div', { className: 'sap-toolSummary' },
|
|
508
|
+
el('div', { className: 'sap-toolText' }, text),
|
|
509
|
+
detailToggle
|
|
510
|
+
)
|
|
511
|
+
const detailBody = open && usage !== null
|
|
512
|
+
? el('div', { className: 'sap-toolDetail' }, usageBreakdownText(usage))
|
|
513
|
+
: null
|
|
381
514
|
return el('div', { className: 'sap-toolview' },
|
|
382
515
|
el('div', { className: 'sap-chips' }, chips),
|
|
383
|
-
|
|
516
|
+
summary,
|
|
517
|
+
detailBody
|
|
384
518
|
)
|
|
385
519
|
}
|
|
386
520
|
return DispatchToolview
|
|
@@ -404,7 +538,7 @@ function makeApplyWrite(s) {
|
|
|
404
538
|
return applyWrite
|
|
405
539
|
}
|
|
406
540
|
|
|
407
|
-
function makeFormActions(s) {
|
|
541
|
+
function makeFormActions(s, loadEfforts, loadTools) {
|
|
408
542
|
const setField = (key) => (event) => {
|
|
409
543
|
const value = event && event.target ? event.target.value : ''
|
|
410
544
|
s.setForm((prev) => {
|
|
@@ -412,8 +546,14 @@ function makeFormActions(s) {
|
|
|
412
546
|
if (key === 'model') next.reasoningEffort = '' // 换模型时重置推理强度
|
|
413
547
|
return next
|
|
414
548
|
})
|
|
549
|
+
if (key === 'model' && value !== '') {
|
|
550
|
+
loadEfforts(value) // 选中模型 → 懒拉该模型 efforts
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const setToolMode = (mode) => () => {
|
|
554
|
+
if (mode !== 'none') loadTools() // 工具限制面板启用 → 拉完整工具目录
|
|
555
|
+
s.setForm((prev) => ({ ...prev, toolMode: mode }))
|
|
415
556
|
}
|
|
416
|
-
const setToolMode = (mode) => () => s.setForm((prev) => ({ ...prev, toolMode: mode }))
|
|
417
557
|
const toggleTool = (name) => () => {
|
|
418
558
|
s.setForm((prev) => {
|
|
419
559
|
const cur = Array.isArray(prev.toolList) ? prev.toolList : []
|
|
@@ -439,7 +579,7 @@ function makeFormActions(s) {
|
|
|
439
579
|
return { setField, setToolMode, toggleTool, selectLayer, clearLayer, invertLayer }
|
|
440
580
|
}
|
|
441
581
|
|
|
442
|
-
function makeEditActions(s) {
|
|
582
|
+
function makeEditActions(s, loadEfforts, loadTools) {
|
|
443
583
|
const openAdd = () => {
|
|
444
584
|
s.setError('')
|
|
445
585
|
s.setSavedNotice('')
|
|
@@ -454,6 +594,10 @@ function makeEditActions(s) {
|
|
|
454
594
|
const allowArr = Array.isArray(tf.allow) ? tf.allow.slice() : []
|
|
455
595
|
const denyArr = Array.isArray(tf.deny) ? tf.deny.slice() : []
|
|
456
596
|
const toolMode = allowArr.length > 0 ? 'allow' : (denyArr.length > 0 ? 'deny' : 'none')
|
|
597
|
+
if (toolMode !== 'none') loadTools() // 编辑含 toolFilter 的 profile → 预拉工具目录
|
|
598
|
+
if (typeof profile.model === 'string' && profile.model !== '') {
|
|
599
|
+
loadEfforts(profile.model) // 编辑含 model 的 profile → 预拉该模型 efforts(下拉不空白)
|
|
600
|
+
}
|
|
457
601
|
s.setForm({
|
|
458
602
|
id: profile.id,
|
|
459
603
|
description: typeof profile.description === 'string' ? profile.description : '',
|
|
@@ -547,10 +691,41 @@ function makeRowActions(api, s, refresh, applyWrite) {
|
|
|
547
691
|
return { toggleEnabled, setProfileEnabled, resetAll, remove }
|
|
548
692
|
}
|
|
549
693
|
|
|
550
|
-
|
|
694
|
+
// 选中模型 → 懒拉 /options/efforts(按模型 id 存入 options.efforts)。
|
|
695
|
+
function makeEffortsLoader(api, setOptions) {
|
|
696
|
+
return (model) => {
|
|
697
|
+
const q = `?model=${encodeURIComponent(model)}`
|
|
698
|
+
api(`/options/efforts${q}`)
|
|
699
|
+
.then((data) => {
|
|
700
|
+
const list = Array.isArray(data.efforts) ? data.efforts : []
|
|
701
|
+
setOptions((prev) => ({ ...prev, efforts: { ...prev.efforts, [model]: list } }))
|
|
702
|
+
})
|
|
703
|
+
.catch(() => {})
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 工具限制面板启用 → 拉 /options/tools(完整工具目录)。
|
|
708
|
+
function makeToolsLoader(api, setTools) {
|
|
709
|
+
return () => {
|
|
710
|
+
api('/options/tools')
|
|
711
|
+
.then((data) => setTools(Array.isArray(data.tools) ? data.tools : []))
|
|
712
|
+
.catch(() => {})
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 版本探测 → 拉 /options/versions;warnings 非空时设置页顶部常驻 amber 提示条。
|
|
717
|
+
function makeVersionsLoader(api, setVersionWarnings) {
|
|
718
|
+
return () => {
|
|
719
|
+
api('/options/versions')
|
|
720
|
+
.then((data) => setVersionWarnings(Array.isArray(data.warnings) ? data.warnings : []))
|
|
721
|
+
.catch(() => {})
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
|
|
551
726
|
return {
|
|
552
|
-
...makeFormActions(s),
|
|
553
|
-
...makeEditActions(s),
|
|
727
|
+
...makeFormActions(s, loadEfforts, loadTools),
|
|
728
|
+
...makeEditActions(s, loadEfforts, loadTools),
|
|
554
729
|
...makeSubmitActions(api, s, refresh, applyWrite),
|
|
555
730
|
...makeRowActions(api, s, refresh, applyWrite),
|
|
556
731
|
}
|
|
@@ -700,11 +875,14 @@ function buildAddCard(el, s, actions, toolSection) {
|
|
|
700
875
|
),
|
|
701
876
|
buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
|
|
702
877
|
(m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
|
|
878
|
+
s.options.models.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到模型目录(headless 部署)——派发成本护栏将按「兼容模式」开关决定放行或拒绝') : null,
|
|
703
879
|
el('details', { className: 'sap-customized' },
|
|
704
880
|
el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
|
|
705
881
|
el('div', { className: 'sap-customizedBody' },
|
|
882
|
+
el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父预设),派发结果以「ignored」标注'),
|
|
706
883
|
buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
|
|
707
884
|
(p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
|
|
885
|
+
s.options.presets.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到 system-trust 预设名册(rosterless)——目标预设下拉不可用') : null,
|
|
708
886
|
buildSelectField(el, s, actions.setField, '提供方(模型服务商)', 'provider', providers,
|
|
709
887
|
(p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
|
|
710
888
|
s.form.model === ''
|
|
@@ -734,6 +912,9 @@ function buildSectionReturn(el, s, actions, rows, addCard) {
|
|
|
734
912
|
el('span', null, s.enabled ? '已启用' : '已禁用')
|
|
735
913
|
)
|
|
736
914
|
),
|
|
915
|
+
s.versionWarnings.length > 0
|
|
916
|
+
? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
|
|
917
|
+
: null,
|
|
737
918
|
el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
|
|
738
919
|
el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
|
|
739
920
|
el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.resetAll }, '重置所有内置方案')
|
|
@@ -763,32 +944,38 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
|
|
|
763
944
|
const [persistWarning, setPersistWarning] = useState('')
|
|
764
945
|
const [form, setForm] = useState(EMPTY_FORM)
|
|
765
946
|
const [tools, setTools] = useState([])
|
|
766
|
-
const
|
|
767
|
-
|
|
947
|
+
const [versionWarnings, setVersionWarnings] = useState([])
|
|
948
|
+
const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings,
|
|
949
|
+
setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings }
|
|
768
950
|
const refresh = useCallback(() => {
|
|
769
951
|
api('/list')
|
|
770
952
|
.then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
|
|
771
953
|
.catch((err) => setError(String((err && err.message) || err)))
|
|
772
954
|
}, [])
|
|
773
955
|
const loadOptions = useCallback(() => {
|
|
774
|
-
api('/options')
|
|
956
|
+
api('/options/summary')
|
|
775
957
|
.then((data) => {
|
|
776
958
|
setEnabled(data.enabled !== false)
|
|
777
959
|
setOptions({
|
|
778
960
|
models: Array.isArray(data.models) ? data.models : [],
|
|
779
|
-
efforts:
|
|
961
|
+
efforts: {},
|
|
780
962
|
presets: Array.isArray(data.presets) ? data.presets : []
|
|
781
963
|
})
|
|
782
|
-
setTools(Array.isArray(data.tools) ? data.tools : [])
|
|
783
964
|
})
|
|
784
965
|
.catch(() => {})
|
|
785
966
|
}, [])
|
|
967
|
+
const loadEfforts = makeEffortsLoader(api, setOptions)
|
|
968
|
+
const loadTools = makeToolsLoader(api, setTools)
|
|
969
|
+
// 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变
|
|
970
|
+
// 导致设置页打开期间无限重取(与 loadOptions 同款稳定化)。
|
|
971
|
+
const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings)(), [])
|
|
786
972
|
useEffect(() => {
|
|
787
973
|
refresh()
|
|
788
974
|
loadOptions()
|
|
789
|
-
|
|
975
|
+
loadVersions()
|
|
976
|
+
}, [refresh, loadOptions, loadVersions])
|
|
790
977
|
const applyWrite = makeApplyWrite(s)
|
|
791
|
-
const actions = makeProfilesActions(api, s, refresh, applyWrite)
|
|
978
|
+
const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
|
|
792
979
|
const rows = buildProfileRows(el, chip, s, actions)
|
|
793
980
|
const toolSection = buildToolSection(el, s, actions)
|
|
794
981
|
return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection))
|
|
@@ -809,7 +996,7 @@ window.__ModuleLoader__.load({
|
|
|
809
996
|
const inject = ['slots']
|
|
810
997
|
|
|
811
998
|
const chip = makeChip(el)
|
|
812
|
-
const DispatchToolview = makeDispatchToolview(el, chip)
|
|
999
|
+
const DispatchToolview = makeDispatchToolview(el, chip, useState)
|
|
813
1000
|
const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
|
|
814
1001
|
|
|
815
1002
|
function apply(ctx) {
|