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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/core/dispatch-tool.mjs — `dispatch` 工具(defineTool schema + execute)、
|
|
2
2
|
// 结果 schema 一致性锁与 syncTool 注册/注销逻辑,从 index.mjs 逐字拆出。
|
|
3
3
|
// 仅引用 lib + shims;无 @deepseek-ai 依赖(shims 是唯一入口)。
|
|
4
4
|
//
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
// routes (/set-enabled), dispose runs on plugin teardown.
|
|
18
18
|
//
|
|
19
19
|
// execute 按预检段 + 前台/后台/continuable 三分支拆为模块级私有函数;
|
|
20
|
-
// parameters
|
|
21
|
-
//
|
|
20
|
+
// parameters 声明为纯数据,驻留模块级常量(与数据表同性质,不受函数行门约束);
|
|
21
|
+
// output 结果 schema 已拆至 lib/core/dispatch-schema.mjs(守文件行门)——
|
|
22
|
+
// 工厂保持装配态;除各任务明示的新增行为外,执行路径零漂移。
|
|
22
23
|
|
|
23
24
|
import { defineTool } from './shims.mjs';
|
|
24
25
|
import { computeContinuableAllow, textFrom, stopReasonError, withPartialText, pruneBlocks, assertResultSchemaConsistency } from './pure.mjs';
|
|
25
26
|
import { resolveWhitelist } from './whitelist.mjs';
|
|
26
27
|
import { assertCostGuard } from './cost-guard.mjs';
|
|
27
|
-
import { settleStart } from './delegation.mjs';
|
|
28
|
+
import { settleStart, collectChildUsage } from './delegation.mjs';
|
|
29
|
+
import { DISPATCH_OUTPUT_SCHEMA } from './dispatch-schema.mjs';
|
|
28
30
|
|
|
29
31
|
// --- 工具声明纯数据(从工厂提到模块级;defineTool 只读不改)----------------------
|
|
30
32
|
|
|
@@ -34,6 +36,7 @@ const DISPATCH_PARAMETERS = {
|
|
|
34
36
|
model: { type: 'string', description: 'Explicit model override for the child.' },
|
|
35
37
|
provider: { type: 'string', description: 'Explicit provider override for the child.' },
|
|
36
38
|
reasoningEffort: { type: 'string', description: 'Explicit reasoning-effort override injected into every child request.' },
|
|
39
|
+
tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费):cheap 省 token、balanced 均衡、premium 高成本。覆盖 profile 的 tokenTier;缺省 balanced。' },
|
|
37
40
|
persona: { type: 'string', description: 'Persona text shadowing the child deployment:persona section.' },
|
|
38
41
|
toolFilter: {
|
|
39
42
|
type: 'object',
|
|
@@ -50,65 +53,18 @@ const DISPATCH_PARAMETERS = {
|
|
|
50
53
|
maxDepth: { type: 'number', description: 'Absolute delegation-depth cap for this child.' },
|
|
51
54
|
run_in_background: { type: 'boolean', description: '异步 one-shot:走 jobs.start 包 start(),返回 jobId;仍单轮即弃,非 continuable' },
|
|
52
55
|
continuable: { type: 'boolean', description: 'Start a durable continuable subagent instead of a one-shot: returns a subagentId immediately and keeps the child conversation available for later turns via the send_message tool. Defaults to false.' },
|
|
53
|
-
// 信封模式 = opt-in
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
envelope: { type: 'boolean', description: '
|
|
56
|
+
// 信封模式 = opt-in(仅「中段即交付物」的任务用):true 时把信封骨架追加进
|
|
57
|
+
// 子 persona(systemPrompt 影子段,模型可见),子 Agent 按结构化信封汇报;
|
|
58
|
+
// 默认 false 走结果剪枝回收。
|
|
59
|
+
envelope: { type: 'boolean', description: 'true 时子 Agent 按结构化信封汇报(简短结论 + 结构分项 + 关键发现落点),适合中段即交付物的长任务;默认 false 走剪枝回收' },
|
|
57
60
|
prompt: { type: 'string', required: true, description: 'The complete, self-contained task for the child (it does not see this conversation).' }
|
|
58
61
|
};
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// / `provider` / `model` / `reasoningEffort` / `profile`), keeping the
|
|
66
|
-
// closed oneOf consistent — assertResultSchemaConsistency(dispatchTool
|
|
67
|
-
// .output.schema) in apply() fires if any 分支 忘补该字段.
|
|
68
|
-
oneOf: [
|
|
69
|
-
{
|
|
70
|
-
type: 'object',
|
|
71
|
-
additionalProperties: false,
|
|
72
|
-
properties: {
|
|
73
|
-
kind: { type: 'string', required: true, const: 'background' },
|
|
74
|
-
jobId: { type: 'string', required: true },
|
|
75
|
-
profile: { type: 'string' },
|
|
76
|
-
preset: { type: 'string' },
|
|
77
|
-
provider: { type: 'string' },
|
|
78
|
-
model: { type: 'string' },
|
|
79
|
-
reasoningEffort: { type: 'string' },
|
|
80
|
-
ignored: { type: 'array', items: { type: 'string' } }
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
type: 'object',
|
|
85
|
-
additionalProperties: false,
|
|
86
|
-
properties: {
|
|
87
|
-
kind: { type: 'string', required: true, const: 'continuable' },
|
|
88
|
-
subagentId: { type: 'string', required: true },
|
|
89
|
-
profile: { type: 'string' },
|
|
90
|
-
preset: { type: 'string' },
|
|
91
|
-
provider: { type: 'string' },
|
|
92
|
-
model: { type: 'string' },
|
|
93
|
-
reasoningEffort: { type: 'string' },
|
|
94
|
-
ignored: { type: 'array', items: { type: 'string' } }
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
type: 'object',
|
|
99
|
-
additionalProperties: false,
|
|
100
|
-
properties: {
|
|
101
|
-
output: { type: 'string', required: true },
|
|
102
|
-
profile: { type: 'string' },
|
|
103
|
-
preset: { type: 'string' },
|
|
104
|
-
provider: { type: 'string' },
|
|
105
|
-
model: { type: 'string' },
|
|
106
|
-
reasoningEffort: { type: 'string' },
|
|
107
|
-
ignored: { type: 'array', items: { type: 'string' } }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
]
|
|
111
|
-
};
|
|
63
|
+
// 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约。三分支共享此
|
|
64
|
+
// 常量;骨架只进 persona(systemPrompt 影子段、模型可见),不进 descriptor
|
|
65
|
+
// (model-hidden、仅 session 记录),故 execute 在三分支创建子 Agent 前统一
|
|
66
|
+
// 追加到 merged.persona。
|
|
67
|
+
const ENVELOPE_SKELETON = '完成前按以下骨架输出:## 结论(1-2 句)\n## 结构分项(逐项)\n## 关键发现落点(文件路径/数据位置)';
|
|
112
68
|
|
|
113
69
|
// continuable 丢弃 preset 换用与 reasoningEffort —— 渲染行把 `ignored`
|
|
114
70
|
// 列表回显出来(`reasoningEffort=<值>(ignored)`,再加 ignored 项明细),让模型
|
|
@@ -117,11 +73,29 @@ const DISPATCH_RENDER = (_args, value) => {
|
|
|
117
73
|
const ignored = value.ignored !== undefined && value.ignored.length > 0
|
|
118
74
|
? `(ignored: ${value.ignored.join(', ')})`
|
|
119
75
|
: '';
|
|
76
|
+
const tier = typeof value.tokenTier === 'string' && value.tokenTier !== ''
|
|
77
|
+
? ` · tokenTier=${value.tokenTier}`
|
|
78
|
+
: '';
|
|
79
|
+
const tokens = typeof value.childTotalTokens === 'number'
|
|
80
|
+
? ` · childTotalTokens=${value.childTotalTokens}`
|
|
81
|
+
: '';
|
|
82
|
+
const elapsed = typeof value.elapsedMs === 'number'
|
|
83
|
+
? ` · elapsedMs=${value.elapsedMs}`
|
|
84
|
+
: '';
|
|
85
|
+
const stopReason = typeof value.stopReason === 'string' && value.stopReason !== ''
|
|
86
|
+
? ` · stopReason=${value.stopReason}`
|
|
87
|
+
: '';
|
|
88
|
+
// childUsage 是嵌套对象,render 行是扁平的 key=value,故 JSON 序列化为单段
|
|
89
|
+
// 供 client 侧 parseDispatchText 解析回对象(五段分解)。仅前台 completed 结算
|
|
90
|
+
// 携带;后台结算经 job 结果携带,continuable 不携带,故该段只在 foreground 行。
|
|
91
|
+
const usage = value.childUsage !== undefined && value.childUsage !== null && typeof value.childUsage === 'object'
|
|
92
|
+
? ` · childUsage=${JSON.stringify(value.childUsage)}`
|
|
93
|
+
: '';
|
|
120
94
|
const text = value.kind === 'background'
|
|
121
|
-
? `[dispatch] background job ${value.jobId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}`
|
|
95
|
+
? `[dispatch] background job ${value.jobId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
|
|
122
96
|
: value.kind === 'continuable'
|
|
123
|
-
? `[dispatch] started subagent ${value.subagentId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}`
|
|
124
|
-
: `[dispatch] profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}\n\n${value.output}`;
|
|
97
|
+
? `[dispatch] started subagent ${value.subagentId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
|
|
98
|
+
: `[dispatch] profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}${tokens}${usage}${elapsed}${stopReason}\n\n${value.output}`;
|
|
125
99
|
return [{ type: 'text', text }];
|
|
126
100
|
};
|
|
127
101
|
|
|
@@ -131,7 +105,7 @@ const DISPATCH_RENDER = (_args, value) => {
|
|
|
131
105
|
function mergeProfileArgs(args, store) {
|
|
132
106
|
const base = args.profile !== undefined ? store.resolveProfile(args.profile) : {};
|
|
133
107
|
const merged = { ...base };
|
|
134
|
-
for (const key of ['preset', 'model', 'provider', 'reasoningEffort', 'persona', 'toolFilter', 'maxTokens', 'maxDepth']) {
|
|
108
|
+
for (const key of ['preset', 'model', 'provider', 'reasoningEffort', 'persona', 'toolFilter', 'maxTokens', 'maxDepth', 'tokenTier']) {
|
|
135
109
|
if (args[key] !== undefined) merged[key] = args[key];
|
|
136
110
|
}
|
|
137
111
|
return merged;
|
|
@@ -158,7 +132,8 @@ function buildMeta(args, merged, parent) {
|
|
|
158
132
|
preset: merged.preset ?? 'inherit',
|
|
159
133
|
provider: merged.provider ?? parent.options.provider ?? '(parent)',
|
|
160
134
|
model: merged.model ?? parent.options.model ?? '(parent)',
|
|
161
|
-
reasoningEffort: merged.reasoningEffort ?? '(default)'
|
|
135
|
+
reasoningEffort: merged.reasoningEffort ?? '(default)',
|
|
136
|
+
tokenTier: merged.tokenTier ?? 'balanced'
|
|
162
137
|
};
|
|
163
138
|
}
|
|
164
139
|
|
|
@@ -262,6 +237,7 @@ async function runContinuable(args, merged, meta, parent, exec, deps) {
|
|
|
262
237
|
provider: meta.provider,
|
|
263
238
|
model: meta.model,
|
|
264
239
|
reasoningEffort: meta.reasoningEffort,
|
|
240
|
+
tokenTier: meta.tokenTier,
|
|
265
241
|
ignored: ['preset', 'reasoningEffort']
|
|
266
242
|
};
|
|
267
243
|
}
|
|
@@ -271,7 +247,7 @@ async function runContinuable(args, merged, meta, parent, exec, deps) {
|
|
|
271
247
|
// Background one-shot (job) path — jobs.start wraps start() with a native
|
|
272
248
|
// AbortController (a Node global in a bundle; the dynamic-plugin sandbox needed
|
|
273
249
|
// the hand-rolled shim instead); still one turn, not continuable.
|
|
274
|
-
async function runBackground(args, meta, request, parent, deps, pruneResultOutput) {
|
|
250
|
+
async function runBackground(args, meta, request, parent, deps, pruneResultOutput, t0) {
|
|
275
251
|
const jobs = deps.getService('jobs');
|
|
276
252
|
if (jobs === undefined) {
|
|
277
253
|
throw new Error('dispatch: background jobs unavailable (load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs)');
|
|
@@ -284,54 +260,109 @@ async function runBackground(args, meta, request, parent, deps, pruneResultOutpu
|
|
|
284
260
|
const controller = new AbortController();
|
|
285
261
|
return {
|
|
286
262
|
cancel: (reason) => controller.abort(reason ?? 'dispatch: background subagent task killed'),
|
|
287
|
-
done: settleStart(
|
|
263
|
+
done: settleStart(
|
|
264
|
+
deps.subagents.start('profile', { ...request, signal: controller.signal }),
|
|
265
|
+
controller.signal,
|
|
266
|
+
meta,
|
|
267
|
+
pruneResultOutput,
|
|
268
|
+
(session) => measureChildTokens(deps.getService, session),
|
|
269
|
+
t0
|
|
270
|
+
)
|
|
288
271
|
};
|
|
289
272
|
}
|
|
290
273
|
});
|
|
291
274
|
return { kind: 'background', jobId, ...meta };
|
|
292
275
|
}
|
|
293
276
|
|
|
277
|
+
// childTotalTokens:子 Agent 会话的宿主启发式估算 token 总量(surface 口径),
|
|
278
|
+
// 非 provider 计费 usage token。仅 completed 结算时测量;tokenMeter 服务缺失、
|
|
279
|
+
// measure 非函数、返回缺 totalTokens 或抛错时均返回 undefined(fail-soft),
|
|
280
|
+
// 使测量绝不阻断派发结算。
|
|
281
|
+
function measureChildTokens(getService, session) {
|
|
282
|
+
if (session === undefined || session === null) return undefined;
|
|
283
|
+
const meter = getService('tokenMeter');
|
|
284
|
+
if (meter === undefined || typeof meter.measure !== 'function') return undefined;
|
|
285
|
+
try {
|
|
286
|
+
const measured = meter.measure(session);
|
|
287
|
+
return measured !== null && typeof measured === 'object' && typeof measured.totalTokens === 'number'
|
|
288
|
+
? measured.totalTokens
|
|
289
|
+
: undefined;
|
|
290
|
+
} catch {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
294
295
|
// Foreground: collect, always release the handle (dispose even when
|
|
295
296
|
// run.result rejects), then fail loud on a non-completed stop reason.
|
|
296
|
-
async function runForeground(meta, request, parent, deps, pruneResultOutput) {
|
|
297
|
+
async function runForeground(meta, request, parent, deps, pruneResultOutput, t0) {
|
|
297
298
|
const run = await deps.subagents.start('profile', request);
|
|
298
299
|
let result;
|
|
300
|
+
let childTotalTokens;
|
|
301
|
+
let childUsage;
|
|
299
302
|
try {
|
|
300
303
|
result = await run.result;
|
|
304
|
+
// 仅 completed 结算时测量;dispose 前读子 session 保证事件流完整。
|
|
305
|
+
if (result.stopReason === 'completed') {
|
|
306
|
+
const childSession = run.localAgent?.session;
|
|
307
|
+
childTotalTokens = measureChildTokens(deps.getService, childSession);
|
|
308
|
+
childUsage = collectChildUsage(childSession);
|
|
309
|
+
}
|
|
301
310
|
} finally {
|
|
302
311
|
await run.dispose().catch(() => {});
|
|
303
312
|
}
|
|
304
|
-
//
|
|
305
|
-
// partial output text (withPartialText style).
|
|
313
|
+
// 非 completed 视为失败,附子输出部分文本。
|
|
306
314
|
const failure = stopReasonError(result);
|
|
307
315
|
if (failure !== undefined) throw new Error(withPartialText(failure, result.output));
|
|
308
|
-
|
|
316
|
+
// 结算 meta:elapsedMs 自 t0;stopReason 取底层值(此分支必 completed);
|
|
317
|
+
// childTotalTokens / childUsage 仅可测量时携带。
|
|
318
|
+
const metaOut = {
|
|
319
|
+
...meta,
|
|
320
|
+
...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
|
|
321
|
+
...(childUsage !== undefined ? { childUsage } : {}),
|
|
322
|
+
elapsedMs: Date.now() - t0,
|
|
323
|
+
stopReason: result.stopReason
|
|
324
|
+
};
|
|
325
|
+
return { output: textFrom(pruneResultOutput(result.output)), ...metaOut };
|
|
309
326
|
}
|
|
310
327
|
|
|
311
328
|
// execute 主体:预检段 + 三分支分派,行为逐字不变。
|
|
312
329
|
async function runDispatch(args, exec, deps) {
|
|
313
330
|
const parent = exec.agent;
|
|
314
331
|
if (!parent) throw new Error('dispatch requires calling agent');
|
|
332
|
+
// 派发耗时基准:execute 入口记 t0,前台/后台各自在结算处算 elapsedMs。
|
|
333
|
+
// continuable 不结算、不携带,故 t0 仅穿线到前台/后台。
|
|
334
|
+
const t0 = Date.now();
|
|
315
335
|
const merged = mergeProfileArgs(args, deps.store);
|
|
336
|
+
// 信封模式 opt-in:三分支创建子 Agent 前,把信封骨架追加进子 persona。
|
|
337
|
+
// persona 是 systemPrompt 影子段(模型可见);descriptor 是 model-hidden,
|
|
338
|
+
// 故骨架只追加 persona、不触碰 descriptor。既有 persona 为空/未设时直接
|
|
339
|
+
// 置为骨架,非空时以空行衔接追加,不覆盖原文。
|
|
340
|
+
if (args.envelope === true) {
|
|
341
|
+
const hasPersona = merged.persona !== undefined && merged.persona.trim() !== '';
|
|
342
|
+
merged.persona = hasPersona
|
|
343
|
+
? `${merged.persona}\n\n${ENVELOPE_SKELETON}`
|
|
344
|
+
: ENVELOPE_SKELETON;
|
|
345
|
+
}
|
|
316
346
|
await assertPresetWhitelist(parent, merged);
|
|
317
|
-
// Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen
|
|
318
|
-
|
|
347
|
+
// Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控;
|
|
348
|
+
// llm 目录读取走共享 catalog 快照)。
|
|
349
|
+
await assertCostGuard(parent, merged, deps.store.getAllowFailOpen(), deps.logger, deps.catalog);
|
|
319
350
|
const meta = buildMeta(args, merged, parent);
|
|
320
351
|
const request = buildRequest(args, merged, parent, exec.signal);
|
|
321
352
|
// 结果回收默认剪枝:在 textFrom(result.output) 之前复用宿主
|
|
322
353
|
// toolResultPruner.pruneContent 预剪。`pruneResultOutput` 每次现取
|
|
323
354
|
// ctx.get('toolResultPruner') 以反映服务就绪状态;pruner 缺失时
|
|
324
|
-
// pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。envelope
|
|
325
|
-
//
|
|
355
|
+
// pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。envelope:true 时信封
|
|
356
|
+
// 骨架已在上方追加进 merged.persona(不改变此处的剪枝回收路径)。
|
|
326
357
|
const pruneResultOutput = (blocks) => pruneBlocks(blocks, deps.getService('toolResultPruner'));
|
|
327
358
|
logDispatchDecision(deps.logger, args, merged, parent);
|
|
328
359
|
if (args.continuable === true) return runContinuable(args, merged, meta, parent, exec, deps);
|
|
329
|
-
if (args.run_in_background === true) return runBackground(args, meta, request, parent, deps, pruneResultOutput);
|
|
330
|
-
return runForeground(meta, request, parent, deps, pruneResultOutput);
|
|
360
|
+
if (args.run_in_background === true) return runBackground(args, meta, request, parent, deps, pruneResultOutput, t0);
|
|
361
|
+
return runForeground(meta, request, parent, deps, pruneResultOutput, t0);
|
|
331
362
|
}
|
|
332
363
|
|
|
333
|
-
export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents }) {
|
|
334
|
-
const deps = { store, getEnabled, getService, logger, subagents };
|
|
364
|
+
export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog }) {
|
|
365
|
+
const deps = { store, getEnabled, getService, logger, subagents, catalog };
|
|
335
366
|
const dispatchTool = defineTool({
|
|
336
367
|
name: 'dispatch',
|
|
337
368
|
description: 'Dispatch a subtask to a derived subagent, optionally overriding its preset, model, provider, reasoning effort, persona, tool whitelist, token budget, or recursion depth. Foreground waits for the result; run_in_background: true starts a background job (single turn); continuable: true starts a durable subagent whose conversation stays available for later turns via the send_message tool. 前瞻:continuable 模式忽略 preset 换用与 reasoningEffort(结果以 ignored 提示)。',
|
package/lib/core/http-routes.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// lib/core/
|
|
1
|
+
// lib/core/http-routes.mjs — settings HTTP loopback routes for the Client UI.
|
|
2
|
+
// Read routes (list / options summary / per-model efforts / tools) + write
|
|
3
|
+
// routes (set-enabled / add / remove / reset / reset-all / set-profile-enabled).
|
|
4
|
+
// The /options catalog (models / presets / tools / efforts) is served from the
|
|
5
|
+
// shared catalog cache (lib/core/catalog-cache.mjs), so these routes no longer
|
|
6
|
+
// walk the llm directory themselves.
|
|
6
7
|
//
|
|
7
8
|
// Injection: every apply-closure / ctx dependency is an explicit parameter —
|
|
8
9
|
// store the profile store (profiles Map / persistProfiles /
|
|
@@ -11,20 +12,17 @@
|
|
|
11
12
|
// /set-enabled),
|
|
12
13
|
// setEnabled writes it,
|
|
13
14
|
// syncTool unregisters/registers the dispatch tool on /set-enabled,
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// per request, never at apply time),
|
|
17
|
-
// logger ctx.logger (route error / tools-directory warnings).
|
|
15
|
+
// catalog the shared catalog cache (getSnapshot / invalidate),
|
|
16
|
+
// logger ctx.logger (route error warnings).
|
|
18
17
|
// The factory returns the scope.effect setup function so the caller keeps the
|
|
19
18
|
// exact original registration shape: effect(() => register + disposer).
|
|
20
19
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// 行为逐字不变(错误文案/状态码/schema)。
|
|
20
|
+
// 写路由各抽为模块级处理函数(if 链分发保持);/options 拆三读路由 + 手动刷新,
|
|
21
|
+
// 目录数据统一来自 catalog 快照。
|
|
24
22
|
|
|
25
23
|
import { sanitizeProfile } from './pure.mjs';
|
|
26
|
-
import { TOOL_ZH, TOOL_CATEGORY } from './catalog.mjs';
|
|
27
24
|
import { BUILTIN_SEEDS } from './profiles-store.mjs';
|
|
25
|
+
import { detectVersions } from './shims.mjs';
|
|
28
26
|
|
|
29
27
|
// Only the loopback interfaces may drive the settings HTTP routes.
|
|
30
28
|
const LOOPBACKS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
@@ -80,123 +78,36 @@ async function handleList(deps, res) {
|
|
|
80
78
|
return json(res, 200, { ok: true, profiles: listClean(deps.store) });
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
//
|
|
84
|
-
async function
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const providers = await llm.listProviders();
|
|
88
|
-
for (const provider of (providers ?? [])) {
|
|
89
|
-
const providerId = provider && provider.id;
|
|
90
|
-
if (typeof providerId !== 'string') continue;
|
|
91
|
-
let modelList = [];
|
|
92
|
-
try { modelList = await llm.listModels(providerId); } catch { /* skip this provider's catalog */ }
|
|
93
|
-
for (const model of (modelList ?? [])) {
|
|
94
|
-
if (!model || typeof model.id !== 'string') continue;
|
|
95
|
-
models.push({
|
|
96
|
-
provider: providerId,
|
|
97
|
-
providerName: provider.name ?? providerId,
|
|
98
|
-
id: model.id,
|
|
99
|
-
name: model.name ?? model.id
|
|
100
|
-
});
|
|
101
|
-
try {
|
|
102
|
-
const info = await llm.resolveModelInfo(providerId, model.id);
|
|
103
|
-
const effortsList = info && info.reasoning && Array.isArray(info.reasoning.efforts) ? info.reasoning.efforts : [];
|
|
104
|
-
efforts[model.id] = effortsList.map((effort) => ({
|
|
105
|
-
id: effort.id,
|
|
106
|
-
name: effort.name ?? effort.id,
|
|
107
|
-
...(effort.description !== undefined ? { description: effort.description } : {})
|
|
108
|
-
}));
|
|
109
|
-
} catch { /* exact-model lookup may reject; skip its efforts */ }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { models, efforts };
|
|
81
|
+
// 轻量摘要:enabled + 模型目录(含 provider 信息,客户端据此派生提供方列表)+ 预设名册。
|
|
82
|
+
async function handleSummary(deps, res) {
|
|
83
|
+
const snapshot = await deps.catalog.getSnapshot();
|
|
84
|
+
return json(res, 200, { ok: true, enabled: deps.getEnabled(), models: snapshot.models, presets: snapshot.presets });
|
|
113
85
|
}
|
|
114
86
|
|
|
115
|
-
//
|
|
116
|
-
async function
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (preset && preset.trust === 'system') {
|
|
122
|
-
presets.push({ id: preset.id, name: preset.name ?? preset.id });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} catch { /* presets roster unavailable; leave empty */ }
|
|
126
|
-
return presets;
|
|
87
|
+
// 每模型 reasoning-effort 等级(懒加载;model 命中快照的 efforts 表)。
|
|
88
|
+
async function handleEfforts(deps, url, res) {
|
|
89
|
+
const model = url.searchParams.get('model');
|
|
90
|
+
const snapshot = await deps.catalog.getSnapshot();
|
|
91
|
+
const efforts = (typeof model === 'string' && model !== '' && snapshot.efforts[model] !== undefined) ? snapshot.efforts[model] : [];
|
|
92
|
+
return json(res, 200, { ok: true, efforts });
|
|
127
93
|
}
|
|
128
94
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
async function collectToolsDirectory(getTools, getAgentPresets) {
|
|
134
|
-
const tools = [];
|
|
135
|
-
const seen = new Set();
|
|
136
|
-
const OFFICIAL_PRESETS = ['standard', 'code', 'minimal', 'cordis'];
|
|
137
|
-
const layerOf = (source) => {
|
|
138
|
-
if (source === 'global') return 'plugin';
|
|
139
|
-
if (OFFICIAL_PRESETS.includes(source)) return 'core';
|
|
140
|
-
return 'custom';
|
|
141
|
-
};
|
|
142
|
-
const groupOf = (name, source) => {
|
|
143
|
-
const layer = layerOf(source);
|
|
144
|
-
if (layer === 'core') return TOOL_CATEGORY[name] ?? '其他';
|
|
145
|
-
if (layer === 'plugin') return name.includes('_') ? name.split('_')[0] : name;
|
|
146
|
-
return source;
|
|
147
|
-
};
|
|
148
|
-
const push = (schemas, source) => {
|
|
149
|
-
for (const s of (Array.isArray(schemas) ? schemas : [])) {
|
|
150
|
-
if (!s || typeof s.name !== 'string' || s.name === 'run_code' || seen.has(s.name)) continue;
|
|
151
|
-
seen.add(s.name);
|
|
152
|
-
tools.push({ name: s.name, description: typeof s.description === 'string' ? s.description : '', zh: TOOL_ZH[s.name] ?? '', source, layer: layerOf(source), group: groupOf(s.name, source) });
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
const toolsService = getTools();
|
|
156
|
-
if (toolsService && typeof toolsService.schemas === 'function') {
|
|
157
|
-
push(toolsService.schemas(), 'global');
|
|
158
|
-
const agentPresets = getAgentPresets();
|
|
159
|
-
if (agentPresets !== undefined && typeof agentPresets.list === 'function' && typeof agentPresets.standingKeyFor === 'function') {
|
|
160
|
-
const presets = await agentPresets.list();
|
|
161
|
-
for (const preset of (presets ?? [])) {
|
|
162
|
-
if (!preset || typeof preset.id !== 'string') continue;
|
|
163
|
-
try {
|
|
164
|
-
push(toolsService.schemas(await agentPresets.standingKeyFor(preset.id)), preset.id);
|
|
165
|
-
} catch { /* one preset's standing scope unavailable; skip */ }
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return tools;
|
|
95
|
+
// 完整工具目录(面板打开时取)。
|
|
96
|
+
async function handleTools(deps, res) {
|
|
97
|
+
const snapshot = await deps.catalog.getSnapshot();
|
|
98
|
+
return json(res, 200, { ok: true, tools: snapshot.tools });
|
|
170
99
|
}
|
|
171
100
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const { models: found, efforts: levels } = await collectModelDirectory(llm);
|
|
183
|
-
models.push(...found);
|
|
184
|
-
Object.assign(efforts, levels);
|
|
185
|
-
} catch { /* llm directory unavailable; leave options empty */ }
|
|
186
|
-
}
|
|
187
|
-
// System-trust presets (agentPresets is optional; fail-soft).
|
|
188
|
-
const agentPresets = deps.getAgentPresets();
|
|
189
|
-
if (agentPresets !== undefined) {
|
|
190
|
-
presets.push(...await collectSystemPresets(agentPresets));
|
|
191
|
-
}
|
|
192
|
-
// Full tool directory = global layer + every preset's standing scope.
|
|
193
|
-
let tools = [];
|
|
194
|
-
try {
|
|
195
|
-
tools = await collectToolsDirectory(deps.getTools, deps.getAgentPresets);
|
|
196
|
-
} catch (error) {
|
|
197
|
-
deps.logger.warn('[dsh-subagent-profile] tools directory failed:', error instanceof Error ? error.message : String(error));
|
|
198
|
-
}
|
|
199
|
-
return json(res, 200, { ok: true, enabled: deps.getEnabled(), models, efforts, presets, tools });
|
|
101
|
+
// 版本探测:三包 version + 越界/未知的中文 warnings(纯探测,同步)。
|
|
102
|
+
function handleVersions(res) {
|
|
103
|
+
const { versions, warnings } = detectVersions();
|
|
104
|
+
return json(res, 200, { ok: true, versions, warnings });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 手动刷新:清缓存兜底(TTL 过期前的目录变更经此立即生效)。
|
|
108
|
+
async function handleRefresh(deps, res) {
|
|
109
|
+
deps.catalog.invalidate();
|
|
110
|
+
return json(res, 200, { ok: true, refreshed: true });
|
|
200
111
|
}
|
|
201
112
|
|
|
202
113
|
async function handleSetEnabled(deps, req, res) {
|
|
@@ -222,6 +133,11 @@ async function handleAdd(deps, req, res) {
|
|
|
222
133
|
return json(res, 400, { ok: false, error: `写入被拒绝:${detail}` });
|
|
223
134
|
}
|
|
224
135
|
const hadToolFilter = profile.toolFilter !== undefined;
|
|
136
|
+
// tokenTier 不参与上方通用 merge 循环:sanitizeProfile 对「未提供」恒回填
|
|
137
|
+
// balanced,若进循环会破坏「未传→保留 existing」语义(编辑内置 researcher
|
|
138
|
+
// 时 cheap 会被重置为 balanced)。故单独用 raw-body 守卫:传了才写(strict
|
|
139
|
+
// 模式下非法值已在上面 400 拒绝,clean.tokenTier 必为合法 enum)。
|
|
140
|
+
const hadTokenTier = profile.tokenTier !== undefined;
|
|
225
141
|
const existing = deps.store.profiles.get(clean.id);
|
|
226
142
|
const seed = BUILTIN_SEEDS.find((s) => s.id === clean.id);
|
|
227
143
|
const isBuiltin = (existing !== undefined && existing.builtin === true) || seed !== undefined;
|
|
@@ -245,6 +161,7 @@ async function handleAdd(deps, req, res) {
|
|
|
245
161
|
delete merged.toolFilter;
|
|
246
162
|
}
|
|
247
163
|
}
|
|
164
|
+
if (hadTokenTier) merged.tokenTier = clean.tokenTier;
|
|
248
165
|
if (merged.enabled !== undefined) merged.enabled = merged.enabled === false ? false : true;
|
|
249
166
|
deps.store.profiles.set(merged.id, { ...merged, ...(isBuiltin ? { builtin: true } : {}), persisted: true });
|
|
250
167
|
deps.store.deletedBuiltins.delete(merged.id);
|
|
@@ -297,8 +214,8 @@ async function handleSetProfileEnabled(deps, req, res) {
|
|
|
297
214
|
return persistOk(res, { id, enabled: existing.enabled }, deps.store.persistProfiles());
|
|
298
215
|
}
|
|
299
216
|
|
|
300
|
-
export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syncTool,
|
|
301
|
-
const deps = { store, getEnabled, setEnabled, syncTool,
|
|
217
|
+
export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syncTool, catalog, logger }) {
|
|
218
|
+
const deps = { store, getEnabled, setEnabled, syncTool, catalog, logger };
|
|
302
219
|
// 路由分发(if 链保持,判断顺序与 404/500 兜底不变)。
|
|
303
220
|
const handler = async (req, res) => {
|
|
304
221
|
const remote = req.socket?.remoteAddress;
|
|
@@ -307,7 +224,11 @@ export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syn
|
|
|
307
224
|
const sub = (url.pathname.replace(/^\/subagent-profiles/, '') || '/').replace(/\/+$/, '') || '/';
|
|
308
225
|
try {
|
|
309
226
|
if (req.method === 'GET' && (sub === '/' || sub === '/list')) return handleList(deps, res);
|
|
310
|
-
if (req.method === 'GET' && sub === '/options') return
|
|
227
|
+
if (req.method === 'GET' && sub === '/options/summary') return handleSummary(deps, res);
|
|
228
|
+
if (req.method === 'GET' && sub === '/options/versions') return handleVersions(res);
|
|
229
|
+
if (req.method === 'GET' && sub === '/options/efforts') return handleEfforts(deps, url, res);
|
|
230
|
+
if (req.method === 'GET' && sub === '/options/tools') return handleTools(deps, res);
|
|
231
|
+
if (req.method === 'POST' && sub === '/options/refresh') return handleRefresh(deps, res);
|
|
311
232
|
if (req.method === 'POST' && sub === '/set-enabled') return handleSetEnabled(deps, req, res);
|
|
312
233
|
if (req.method === 'POST' && sub === '/add') return handleAdd(deps, req, res);
|
|
313
234
|
if (req.method === 'POST' && sub === '/remove') return handleRemove(deps, req, res);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/core/profile-provider.mjs — `profile` 子 Agent provider
|
|
2
2
|
// (setup/start/prepareContinuable),从 index.mjs 的
|
|
3
3
|
// `ctx.subagents.registerProvider({...})` 块逐字拆出。仅引用 lib + shims;
|
|
4
4
|
// 无 @deepseek-ai 依赖(shims 是唯一入口)。
|
|
@@ -55,8 +55,9 @@ async function runStartPreflight(request, deps) {
|
|
|
55
55
|
if (typeof profile.preset === 'string' && profile.preset !== 'inherit' && !whitelist.has(profile.preset)) {
|
|
56
56
|
throw new Error(`dispatch: preset "${profile.preset}" is not in the target-preset whitelist`);
|
|
57
57
|
}
|
|
58
|
-
// 权威 cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen
|
|
59
|
-
|
|
58
|
+
// 权威 cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控;
|
|
59
|
+
// llm 目录读取走共享 catalog 快照)。
|
|
60
|
+
await assertCostGuard(parent, profile, deps.store.getAllowFailOpen(), deps.logger, deps.catalog);
|
|
60
61
|
// Delegation depth: shipped helpers — assert the cap value, then resolve
|
|
61
62
|
// the child depth (parent floor + 1) and enforce the cap.
|
|
62
63
|
assertSubagentMaxDepth(profile.maxDepth);
|
|
@@ -229,8 +230,8 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
export function createProfileProvider({ subagents, store, getEnabled, logger }) {
|
|
233
|
-
const deps = { store, getEnabled, logger };
|
|
233
|
+
export function createProfileProvider({ subagents, store, getEnabled, logger, catalog }) {
|
|
234
|
+
const deps = { store, getEnabled, logger, catalog };
|
|
234
235
|
return subagents.registerProvider({
|
|
235
236
|
name: 'profile',
|
|
236
237
|
capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/core/profiles-store.mjs — profile registry store and enable/disable switch
|
|
2
2
|
// (moved verbatim from index.mjs and refactored into a factory; import-free —
|
|
3
3
|
// node builtins + lib/core/pure.mjs only, no @deepseek-ai dependency).
|
|
4
4
|
//
|
|
@@ -42,8 +42,8 @@ export function dshHome() {
|
|
|
42
42
|
// connection.defaults.thinking — this deployment leaves thinking unset,
|
|
43
43
|
// so the full set is advertised for deepseek-v4-flash).
|
|
44
44
|
export const BUILTIN_SEEDS = [
|
|
45
|
-
{ id: 'swap-standard', name: '标准编码', description: '切换到 standard 预设的完整编码工具集。当父会话不是 standard、但子任务需要完整编码能力时用。', preset: 'standard', builtin: true },
|
|
46
|
-
{ id: 'researcher', name: '调研检索', description: '关闭深度推理省 token,继承父工具。适合查资料、汇总、背景调研,不适合改代码。', reasoningEffort: 'off', persona: 'You are a research subagent: search, read, and summarize only. Do not modify code or files.', builtin: true }
|
|
45
|
+
{ id: 'swap-standard', name: '标准编码', description: '切换到 standard 预设的完整编码工具集。当父会话不是 standard、但子任务需要完整编码能力时用。', preset: 'standard', tokenTier: 'balanced', builtin: true },
|
|
46
|
+
{ id: 'researcher', name: '调研检索', description: '关闭深度推理省 token,继承父工具。适合查资料、汇总、背景调研,不适合改代码。', reasoningEffort: 'off', persona: 'You are a research subagent: search, read, and summarize only. Do not modify code or files.', tokenTier: 'cheap', builtin: true }
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
// --- 模块级 store 函数(从工厂拆出;per-apply 状态经 `state` 注入)---------------
|