dsh-subagent-profile 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -40
- package/README.zh.md +111 -74
- package/docs/screenshots/dispatch-card.png +0 -0
- package/docs/screenshots/settings-page1.png +0 -0
- package/docs/screenshots/settings-page2.png +0 -0
- package/index.mjs +276 -81
- package/lib/client.js +3218 -166
- package/lib/core/adoption-reminder.mjs +48 -0
- package/lib/core/adoption-tracker.mjs +430 -0
- package/lib/core/background-ledger.mjs +71 -0
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +413 -0
- package/lib/core/delegation.mjs +111 -50
- package/lib/core/dispatch-gates.mjs +153 -0
- package/lib/core/dispatch-guard.mjs +156 -0
- package/lib/core/dispatch-schema.mjs +103 -14
- package/lib/core/dispatch-tool.mjs +220 -204
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-advice.mjs +224 -0
- package/lib/core/evolution-ledger.mjs +300 -0
- package/lib/core/evolution-summary.mjs +255 -0
- package/lib/core/http-routes.mjs +256 -72
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +161 -43
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +42 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/shims.mjs +67 -76
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +82 -83
- package/presets/orchestrator/agent.cordis.yml +59 -87
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -2,106 +2,39 @@
|
|
|
2
2
|
// 结果 schema 一致性锁与 syncTool 注册/注销逻辑,从 index.mjs 逐字拆出。
|
|
3
3
|
// 仅引用 lib + shims;无 @deepseek-ai 依赖(shims 是唯一入口)。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
// register ctx.tools.register
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// subagents ctx.subagents (start / startContinuable drive the child).
|
|
16
|
-
// The factory returns { syncTool, dispose }: syncTool is handed to the HTTP
|
|
17
|
-
// routes (/set-enabled), dispose runs on plugin teardown.
|
|
5
|
+
// 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
|
|
6
|
+
// register ctx.tools.register(工具由 syncTool 注册/注销,设置开关可运行时移除),
|
|
7
|
+
// store profile store(resolveProfile 取基方案、getAllowFailOpen 供 cost guard),
|
|
8
|
+
// getEnabled 读取 apply 闭包 `enabled` 标志(continuable fail-loud 门 + syncTool 注册条件),
|
|
9
|
+
// getService 请求时服务 getter(ctx.get('toolResultPruner') / ctx.get('jobs')
|
|
10
|
+
// 按次读取,绝不在 apply 时读),
|
|
11
|
+
// logger ctx.logger(决策级派发日志 + continuable 告警),
|
|
12
|
+
// subagents ctx.subagents(start / startContinuable 驱动子 Agent),
|
|
13
|
+
// guard dispatch guard(execute 入口 acquire / 结算 release+记账 / syncTool 禁用时 cancelAll+reset)。
|
|
14
|
+
// 工厂返回 { syncTool, dispose }:syncTool 交给 HTTP 路由(/set-enabled),dispose 在插件卸载时运行。
|
|
18
15
|
//
|
|
19
|
-
// execute 按预检段 +
|
|
20
|
-
// parameters 声明为纯数据,驻留模块级常量(与数据表同性质,不受函数行门约束);
|
|
21
|
-
// output 结果 schema 已拆至 lib/core/dispatch-schema.mjs(守文件行门)——
|
|
22
|
-
// 工厂保持装配态;除各任务明示的新增行为外,执行路径零漂移。
|
|
16
|
+
// execute 按预检段 + 三分支拆为模块级私有函数;parameters/output schema 拆至 dispatch-schema.mjs,预检闸拆至 dispatch-gates.mjs(守文件行门),零漂移。
|
|
23
17
|
|
|
24
18
|
import { defineTool } from './shims.mjs';
|
|
25
|
-
import {
|
|
19
|
+
import { textFrom, stopReasonError, withPartialText, pruneBlocks, assertResultSchemaConsistency, trustLabel, trustAudit, dispatchLabel } from './pure.mjs';
|
|
26
20
|
import { resolveWhitelist } from './whitelist.mjs';
|
|
27
|
-
import {
|
|
28
|
-
import { settleStart, collectChildUsage } from './delegation.mjs';
|
|
29
|
-
import { DISPATCH_OUTPUT_SCHEMA } from './dispatch-schema.mjs';
|
|
21
|
+
import { recordEscapeAllow } from './escape.mjs';
|
|
22
|
+
import { settleStart, collectChildUsage, collectChildCalls } from './delegation.mjs';
|
|
23
|
+
import { DISPATCH_OUTPUT_SCHEMA, DISPATCH_PARAMETERS, DISPATCH_RENDER } from './dispatch-schema.mjs';
|
|
24
|
+
import { createDecisionTrace, finalizeTrace, assertTraceSize, parentContextOf, requestedOf, effectiveMeta, recordApprovalGate, recordFailure, profileSnapshotOf } from './decision-trace.mjs';
|
|
25
|
+
import { profileDirectoryRows } from './profile-directory.mjs';
|
|
26
|
+
import { applyWhitelistGate, applyCostGuardGate, applyIntersectionGate, assertContinuableEnabled, readParentToolNames, applyBudgetGate } from './dispatch-gates.mjs';
|
|
27
|
+
import { trackForegroundInflight, trackBackgroundInflight, trackContinuableInflight } from './dispatch-guard.mjs';
|
|
28
|
+
import { finishForegroundLedger, finishBackgroundLedger, finishContinuableLedger } from './evolution-ledger.mjs';
|
|
29
|
+
import { profileKeyOf } from './evolution-summary.mjs';
|
|
30
|
+
import { promptExcerptOf } from './adoption-reminder.mjs';
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const DISPATCH_PARAMETERS = {
|
|
34
|
-
profile: { type: 'string', description: 'Optional profile id from the profile registry (built-ins: swap-standard, researcher, plus any you define in the settings page); omit to inherit the parent preset and tools as-is.' },
|
|
35
|
-
preset: { type: 'string', description: 'Explicit target preset override; must be a system-trust preset of this runtime.' },
|
|
36
|
-
model: { type: 'string', description: 'Explicit model override for the child.' },
|
|
37
|
-
provider: { type: 'string', description: 'Explicit provider override for the child.' },
|
|
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。' },
|
|
40
|
-
persona: { type: 'string', description: 'Persona text shadowing the child deployment:persona section.' },
|
|
41
|
-
toolFilter: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
// DSL 对象参数默认拒绝未知键,toolFilter 必须闭合其 schema,
|
|
44
|
-
// 否则 defineTool 在 apply 时 throw、插件加载失败。
|
|
45
|
-
additionalProperties: false,
|
|
46
|
-
description: 'Extra tool whitelist intersection for the child (intersected with the parent tool set).',
|
|
47
|
-
properties: {
|
|
48
|
-
allow: { type: 'array', items: { type: 'string' }, description: 'When present, only these tool names are kept.' },
|
|
49
|
-
deny: { type: 'array', items: { type: 'string' }, description: 'These tool names are always removed.' }
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
maxTokens: { type: 'number', description: 'Explicit max-tokens budget for the child.' },
|
|
53
|
-
maxDepth: { type: 'number', description: 'Absolute delegation-depth cap for this child.' },
|
|
54
|
-
run_in_background: { type: 'boolean', description: '异步 one-shot:走 jobs.start 包 start(),返回 jobId;仍单轮即弃,非 continuable' },
|
|
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.' },
|
|
56
|
-
// 信封模式 = opt-in(仅「中段即交付物」的任务用):true 时把信封骨架追加进
|
|
57
|
-
// 子 persona(systemPrompt 影子段,模型可见),子 Agent 按结构化信封汇报;
|
|
58
|
-
// 默认 false 走结果剪枝回收。
|
|
59
|
-
envelope: { type: 'boolean', description: 'true 时子 Agent 按结构化信封汇报(简短结论 + 结构分项 + 关键发现落点),适合中段即交付物的长任务;默认 false 走剪枝回收' },
|
|
60
|
-
prompt: { type: 'string', required: true, description: 'The complete, self-contained task for the child (it does not see this conversation).' }
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约。三分支共享此
|
|
64
|
-
// 常量;骨架只进 persona(systemPrompt 影子段、模型可见),不进 descriptor
|
|
65
|
-
// (model-hidden、仅 session 记录),故 execute 在三分支创建子 Agent 前统一
|
|
66
|
-
// 追加到 merged.persona。
|
|
32
|
+
// 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约(三分支共享);只进 persona(模型可见),不进 descriptor(仅 session 记录)。
|
|
67
33
|
const ENVELOPE_SKELETON = '完成前按以下骨架输出:## 结论(1-2 句)\n## 结构分项(逐项)\n## 关键发现落点(文件路径/数据位置)';
|
|
68
34
|
|
|
69
|
-
// continuable 丢弃 preset 换用与 reasoningEffort —— 渲染行把 `ignored`
|
|
70
|
-
// 列表回显出来(`reasoningEffort=<值>(ignored)`,再加 ignored 项明细),让模型
|
|
71
|
-
// 「看见」被丢弃项;background/foreground 无忽略项时该后缀为空。
|
|
72
|
-
const DISPATCH_RENDER = (_args, value) => {
|
|
73
|
-
const ignored = value.ignored !== undefined && value.ignored.length > 0
|
|
74
|
-
? `(ignored: ${value.ignored.join(', ')})`
|
|
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
|
-
: '';
|
|
94
|
-
const text = value.kind === 'background'
|
|
95
|
-
? `[dispatch] background job ${value.jobId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
|
|
96
|
-
: value.kind === 'continuable'
|
|
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}`;
|
|
99
|
-
return [{ type: 'text', text }];
|
|
100
|
-
};
|
|
101
|
-
|
|
102
35
|
// --- execute 预检段(从 execute 拆出;行为逐字不变)-----------------------------
|
|
103
36
|
|
|
104
|
-
//
|
|
37
|
+
// 解析基方案(旁路),再叠加显式参数。
|
|
105
38
|
function mergeProfileArgs(args, store) {
|
|
106
39
|
const base = args.profile !== undefined ? store.resolveProfile(args.profile) : {};
|
|
107
40
|
const merged = { ...base };
|
|
@@ -111,21 +44,7 @@ function mergeProfileArgs(args, store) {
|
|
|
111
44
|
return merged;
|
|
112
45
|
}
|
|
113
46
|
|
|
114
|
-
//
|
|
115
|
-
// a preset equal to the parent's composed preset is rewritten to
|
|
116
|
-
// 'inherit' (no swap).
|
|
117
|
-
async function assertPresetWhitelist(parent, merged) {
|
|
118
|
-
if (typeof merged.preset !== 'string' || merged.preset === 'inherit') return;
|
|
119
|
-
const whitelist = new Set(await resolveWhitelist(parent.ctx.get('agentPresets')));
|
|
120
|
-
if (!whitelist.has(merged.preset)) {
|
|
121
|
-
throw new Error(`dispatch: preset "${merged.preset}" is not in the target-preset whitelist`);
|
|
122
|
-
}
|
|
123
|
-
const parentPresets = parent.ctx.get('agentPresets');
|
|
124
|
-
const parentComposed = parentPresets !== undefined ? parentPresets.composedPreset(parent.ctx) : undefined;
|
|
125
|
-
if (merged.preset === parentComposed) merged.preset = 'inherit';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Effective delegation values for observability.
|
|
47
|
+
// 生效的委派值(供可观测元数据)。
|
|
129
48
|
function buildMeta(args, merged, parent) {
|
|
130
49
|
return {
|
|
131
50
|
profile: args.profile ?? '(inline)',
|
|
@@ -137,10 +56,10 @@ function buildMeta(args, merged, parent) {
|
|
|
137
56
|
};
|
|
138
57
|
}
|
|
139
58
|
|
|
140
|
-
//
|
|
59
|
+
// 组装前台/后台请求(continuable 在下方自建)。
|
|
141
60
|
function buildRequest(args, merged, parent, signal) {
|
|
142
61
|
return {
|
|
143
|
-
label:
|
|
62
|
+
label: dispatchLabel(args, merged),
|
|
144
63
|
prompt: [{ type: 'text', text: args.prompt }],
|
|
145
64
|
parent,
|
|
146
65
|
signal,
|
|
@@ -151,8 +70,7 @@ function buildRequest(args, merged, parent, signal) {
|
|
|
151
70
|
};
|
|
152
71
|
}
|
|
153
72
|
|
|
154
|
-
//
|
|
155
|
-
// guard and after request assembly, before dispatch.
|
|
73
|
+
// 决策级日志:经 cost guard 与请求组装之后、派发之前的生效委派输入。
|
|
156
74
|
function logDispatchDecision(logger, args, merged, parent) {
|
|
157
75
|
logger.info('[dsh-subagent-profile] dispatch:', JSON.stringify({
|
|
158
76
|
profile: args.profile ?? '(inline)',
|
|
@@ -174,10 +92,9 @@ function logDispatchDecision(logger, args, merged, parent) {
|
|
|
174
92
|
// 假设:continuable 继承父预设(preset swap 被忽略)⇒ 子工具集 ≈ 父工具集;
|
|
175
93
|
// 失效条件:任何导致子工具集与父工具集不一致的宿主行为变化,父集都可能含
|
|
176
94
|
// 子集上不存在之工具 → tools.restrict 抛「未知工具」→ 本缓解自动降级为
|
|
177
|
-
// fail-loud(保守安全)。
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const effectiveAllow = computeContinuableAllow(parentNames, merged.toolFilter);
|
|
95
|
+
// fail-loud(保守安全)。effectiveAllow 由调用方(runDispatch 交集闸处)算好
|
|
96
|
+
// 传入——同一结果同时用于 request 组装与决策轨迹记录,不重复计算。
|
|
97
|
+
function buildContinuableRequest(args, merged, parent, effectiveAllow) {
|
|
181
98
|
const hasAgentOptions = merged.provider !== undefined || merged.model !== undefined || merged.maxTokens !== undefined;
|
|
182
99
|
return {
|
|
183
100
|
prompt: [{ type: 'text', text: args.prompt }],
|
|
@@ -195,41 +112,40 @@ function buildContinuableRequest(args, merged, parent) {
|
|
|
195
112
|
};
|
|
196
113
|
}
|
|
197
114
|
|
|
198
|
-
// Continuable
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
// `start`,不拦 `startContinuable` —— 这里显式补上。当前 syncTool 会在
|
|
205
|
-
// 禁用时注销 dispatch 工具(间接门),此处是防御性兜底:禁用后
|
|
206
|
-
// dispatch(continuable:true) 必须 fail-loud,不得静默派生子树。
|
|
115
|
+
// Continuable(持久)路径:startContinuable 发布持久子会话并返回其持久 id,
|
|
116
|
+
// 后续轮次由官方 send_message 工具驱动。优先处理——同时要 background 与
|
|
117
|
+
// continuable 的调用方得到 continuable 子会话。
|
|
118
|
+
async function runContinuable(args, merged, meta, parent, exec, deps, trace, effectiveAllow, base) {
|
|
119
|
+
// provider `start` 的 !enabled 检查只拦 `start` 不拦 `startContinuable`——此处
|
|
120
|
+
// 显式兜底(syncTool 注销工具是间接门):禁用后不得静默派生子树,必须 fail-loud。
|
|
207
121
|
if (!deps.getEnabled()) {
|
|
208
122
|
throw new Error('dispatch: 插件已禁用(设置 → 子 Agent 方案 重新启用)');
|
|
209
123
|
}
|
|
210
124
|
if (args.run_in_background === true) {
|
|
211
125
|
deps.logger.warn('[dsh-subagent-profile] dispatch: both continuable and run_in_background are true; continuable takes precedence');
|
|
212
126
|
}
|
|
213
|
-
// 已知降级:continuable 标准路径不支持 preset swap 和 reasoningEffort
|
|
127
|
+
// 已知降级:continuable 标准路径不支持 preset swap 和 reasoningEffort(宿主请求面无对应字段)。
|
|
214
128
|
if (merged.preset !== undefined && merged.preset !== 'inherit') {
|
|
215
129
|
deps.logger.warn(`[dsh-subagent-profile] continuable mode cannot swap preset; ignoring "${merged.preset}" (child inherits the parent preset)`);
|
|
216
130
|
}
|
|
217
131
|
if (merged.reasoningEffort !== undefined) {
|
|
218
132
|
deps.logger.warn(`[dsh-subagent-profile] continuable mode cannot set reasoningEffort; ignoring "${merged.reasoningEffort}"`);
|
|
219
133
|
}
|
|
220
|
-
const continuableRequest = buildContinuableRequest(args, merged, parent);
|
|
134
|
+
const continuableRequest = buildContinuableRequest(args, merged, parent, effectiveAllow);
|
|
221
135
|
const { childId } = await deps.subagents.startContinuable({
|
|
222
136
|
provider: 'profile',
|
|
223
|
-
label:
|
|
137
|
+
label: dispatchLabel(args, merged),
|
|
224
138
|
request: continuableRequest,
|
|
225
139
|
signal: exec.signal
|
|
226
140
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
141
|
+
trackContinuableInflight(deps.guard, deps.subagents, childId, parent.session?.header?.id);
|
|
142
|
+
deps.adoptionTracker?.register({ parentSessionId: parent.session?.header?.id, id: childId, ledgerKey: profileKeyOf(base.effective), mode: 'continuable', dispatchedAt: Date.now(), promptExcerpt: promptExcerptOf(args?.prompt), requestedModel: merged.model, parentModel: parent.options?.model });
|
|
143
|
+
// Continuable 丢弃方案的 preset swap 与 reasoningEffort(子继承父预设),故可观测
|
|
144
|
+
// meta 必须报告实际生效值:reasoningEffort 回显请求值,preset:'inherit' 是生效值,
|
|
145
|
+
// ignored 明确列出被丢弃项。
|
|
146
|
+
// 信任标注不适用:continuable 只回 subagentId、无文本 output(后续经宿主的
|
|
147
|
+
// send_message 流转),没有可加前缀的回收文本,故此处不加 trustLabel。
|
|
148
|
+
const out = {
|
|
233
149
|
kind: 'continuable',
|
|
234
150
|
subagentId: childId,
|
|
235
151
|
profile: meta.profile,
|
|
@@ -240,44 +156,66 @@ async function runContinuable(args, merged, meta, parent, exec, deps) {
|
|
|
240
156
|
tokenTier: meta.tokenTier,
|
|
241
157
|
ignored: ['preset', 'reasoningEffort']
|
|
242
158
|
};
|
|
159
|
+
finalizeTrace(trace, {
|
|
160
|
+
effective: effectiveMeta({ ...meta, preset: 'inherit' }, ['preset', 'reasoningEffort']),
|
|
161
|
+
execution: { kind: 'continuable', parentSessionId: parent.session?.header?.id, childSessionId: childId, mode: 'continuable' },
|
|
162
|
+
});
|
|
163
|
+
out.decisionTrace = assertTraceSize(trace);
|
|
164
|
+
finishContinuableLedger(deps.evoLedger, base, childId); // 无结算:continuable 只写静态元数据(无 outcome)
|
|
165
|
+
return out;
|
|
243
166
|
}
|
|
244
167
|
|
|
245
168
|
// --- 后台 / 前台分支(从 execute 拆出)--------------------------------------------
|
|
246
169
|
|
|
247
|
-
//
|
|
248
|
-
// AbortController
|
|
249
|
-
|
|
250
|
-
async function runBackground(args, meta, request, parent, deps, pruneResultOutput, t0) {
|
|
170
|
+
// 后台 one-shot(job)路径:jobs.start 包 start() 并提供原生
|
|
171
|
+
// AbortController;仍是一轮即弃,非 continuable。
|
|
172
|
+
async function runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, exec) {
|
|
251
173
|
const jobs = deps.getService('jobs');
|
|
252
174
|
if (jobs === undefined) {
|
|
253
|
-
throw new Error('dispatch:
|
|
175
|
+
throw new Error('dispatch: 后台派发不可用:缺少 jobs 服务(请安装 @deepseek-ai/dsh-jobs 与 @deepseek-ai/dsh-tool-jobs,或改用前台派发)');
|
|
254
176
|
}
|
|
255
|
-
|
|
177
|
+
// jobId 由 jobs.start 同步返回,故用提升的 let 在结算(异步)时读取。
|
|
178
|
+
let jobId;
|
|
179
|
+
const onSettled = (settled) => {
|
|
180
|
+
guardHandle.finish(jobId, settled.childTotalTokens);
|
|
181
|
+
finishBackgroundLedger(deps.evoLedger, base, jobId, settled);
|
|
182
|
+
if (deps.backgroundLedger !== undefined) deps.backgroundLedger.record(base.session_id, jobId, settled, exec?.callId);
|
|
183
|
+
deps.adoptionTracker?.updateOutput(base.session_id, jobId, typeof settled.output === 'string' ? settled.output : '');
|
|
184
|
+
deps.adoptionTracker?.updateSettled?.(base.session_id, jobId, settled);
|
|
185
|
+
};
|
|
186
|
+
jobId = jobs.start({
|
|
256
187
|
kind: 'subagent',
|
|
257
|
-
label:
|
|
188
|
+
label: dispatchLabel(args, merged),
|
|
258
189
|
owner: parent,
|
|
259
190
|
run: () => {
|
|
260
191
|
const controller = new AbortController();
|
|
192
|
+
const startPromise = deps.subagents.start('profile', { ...request, signal: controller.signal });
|
|
193
|
+
startPromise.then((run) => { if (run !== null && run !== undefined && typeof run.id === 'string') deps.backgroundLedger?.recordChild?.(base.session_id, jobId, run.id); }).catch(() => {}); // 结算前补记 jobId → childSessionId,供页头识别后台派发。
|
|
261
194
|
return {
|
|
262
195
|
cancel: (reason) => controller.abort(reason ?? 'dispatch: background subagent task killed'),
|
|
263
196
|
done: settleStart(
|
|
264
|
-
|
|
197
|
+
startPromise,
|
|
265
198
|
controller.signal,
|
|
266
199
|
meta,
|
|
267
200
|
pruneResultOutput,
|
|
268
201
|
(session) => measureChildTokens(deps.getService, session),
|
|
269
|
-
t0
|
|
202
|
+
t0,
|
|
203
|
+
deps.logger,
|
|
204
|
+
onSettled
|
|
270
205
|
)
|
|
271
206
|
};
|
|
272
207
|
}
|
|
273
208
|
});
|
|
274
|
-
|
|
209
|
+
trackBackgroundInflight(deps.guard, jobs, jobId, parent);
|
|
210
|
+
deps.adoptionTracker?.register({ parentSessionId: parent.session?.header?.id, id: jobId, ledgerKey: profileKeyOf(base.effective), mode: 'background', dispatchedAt: t0, promptExcerpt: promptExcerptOf(args?.prompt), requestedModel: merged.model, parentModel: parent.options?.model }); // 后台结算经 job 结果携带,不入会话块。
|
|
211
|
+
const out = { kind: 'background', jobId, ...meta };
|
|
212
|
+
finalizeTrace(trace, { effective: effectiveMeta(meta, []), execution: { kind: 'background', parentSessionId: parent.session?.header?.id, jobId, mode: 'one-shot' } });
|
|
213
|
+
out.decisionTrace = assertTraceSize(trace);
|
|
214
|
+
return out;
|
|
275
215
|
}
|
|
276
216
|
|
|
277
|
-
// childTotalTokens
|
|
278
|
-
//
|
|
279
|
-
// measure 非函数、返回缺 totalTokens 或抛错时均返回 undefined(fail-soft),
|
|
280
|
-
// 使测量绝不阻断派发结算。
|
|
217
|
+
// childTotalTokens:宿主 tokenMeter 启发式估算(surface 口径,非计费 usage);仅
|
|
218
|
+
// completed 结算时测量,服务缺失/非函数/抛错均返回 undefined(fail-soft)。
|
|
281
219
|
function measureChildTokens(getService, session) {
|
|
282
220
|
if (session === undefined || session === null) return undefined;
|
|
283
221
|
const meter = getService('tokenMeter');
|
|
@@ -292,92 +230,167 @@ function measureChildTokens(getService, session) {
|
|
|
292
230
|
}
|
|
293
231
|
}
|
|
294
232
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
233
|
+
// 仅 completed 结算时测量一次(tokenMeter 估算 + usage 分解 + 调用明细),dispose
|
|
234
|
+
// 前读子 session 保证事件流完整;全 fail-soft(缺失/抛错 → undefined)绝不阻断结算。
|
|
235
|
+
function measureForegroundRun(deps, run) {
|
|
236
|
+
const childSession = run.localAgent?.session;
|
|
237
|
+
return {
|
|
238
|
+
childTotalTokens: measureChildTokens(deps.getService, childSession),
|
|
239
|
+
childUsage: collectChildUsage(childSession),
|
|
240
|
+
childCalls: collectChildCalls(childSession),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 前台:收集结果,无论 reject 与否都释放句柄,非 completed 停因 fail-loud;
|
|
245
|
+
// run.result reject(undefined)也写失败 outcome。
|
|
246
|
+
async function runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, adoptionBase) {
|
|
298
247
|
const run = await deps.subagents.start('profile', request);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
248
|
+
finalizeTrace(trace, { execution: { kind: 'foreground', parentSessionId: parent.session?.header?.id, childSessionId: run.id, mode: 'one-shot' } });
|
|
249
|
+
trackForegroundInflight(deps.guard, run);
|
|
250
|
+
deps.adoptionTracker?.register({ parentSessionId: parent.session?.header?.id, id: run.id, ledgerKey: profileKeyOf(base.effective), mode: 'foreground', dispatchedAt: t0, ...adoptionBase }); // 前台派发登记 parent_adopted 待判定。
|
|
251
|
+
let result, measurement;
|
|
302
252
|
try {
|
|
303
253
|
result = await run.result;
|
|
304
|
-
|
|
305
|
-
if (result.stopReason === 'completed') {
|
|
306
|
-
const childSession = run.localAgent?.session;
|
|
307
|
-
childTotalTokens = measureChildTokens(deps.getService, childSession);
|
|
308
|
-
childUsage = collectChildUsage(childSession);
|
|
309
|
-
}
|
|
254
|
+
if (result.stopReason === 'completed') measurement = measureForegroundRun(deps, run);
|
|
310
255
|
} finally {
|
|
256
|
+
if (result === undefined) finishForegroundLedger(deps.evoLedger, base, run.id, { stopReason: 'error', output: [] }, [], Date.now() - t0, undefined);
|
|
311
257
|
await run.dispose().catch(() => {});
|
|
258
|
+
guardHandle.finish(run.id, measurement?.childTotalTokens); // untrack + recordTokens + release
|
|
312
259
|
}
|
|
313
|
-
|
|
260
|
+
const { childTotalTokens, childUsage, childCalls } = measurement ?? {};
|
|
261
|
+
// 台账:completed 与非 completed 都写 outcome(对照 settleStart 语义),随后非 completed 仍 throw。
|
|
262
|
+
const pruned = pruneResultOutput(result.output);
|
|
263
|
+
const elapsedMs = Date.now() - t0;
|
|
264
|
+
finishForegroundLedger(deps.evoLedger, base, run.id, result, pruned, elapsedMs, childCalls, childTotalTokens, childUsage);
|
|
265
|
+
deps.adoptionTracker?.updateOutput(parent.session?.header?.id, run.id, textFrom(pruned));
|
|
266
|
+
deps.adoptionTracker?.updateSettled?.(parent.session?.header?.id, run.id, { stopReason: result.stopReason, elapsedMs, ...(childTotalTokens !== undefined ? { childTotalTokens } : {}), ...(childUsage !== undefined ? { childUsage } : {}), childSessionId: run.id }); // 结算摘要回填供未采纳提醒结果行,已拒/未完成同样登记。
|
|
314
267
|
const failure = stopReasonError(result);
|
|
315
268
|
if (failure !== undefined) throw new Error(withPartialText(failure, result.output));
|
|
316
|
-
// 结算 meta:elapsedMs 自 t0;stopReason 取底层值(此分支必 completed);
|
|
317
|
-
// childTotalTokens / childUsage 仅可测量时携带。
|
|
318
269
|
const metaOut = {
|
|
319
270
|
...meta,
|
|
320
271
|
...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
|
|
321
272
|
...(childUsage !== undefined ? { childUsage } : {}),
|
|
322
|
-
elapsedMs
|
|
273
|
+
elapsedMs,
|
|
323
274
|
stopReason: result.stopReason
|
|
324
275
|
};
|
|
325
|
-
|
|
276
|
+
finalizeTrace(trace, {
|
|
277
|
+
effective: effectiveMeta(metaOut, []),
|
|
278
|
+
settled: {
|
|
279
|
+
stopReason: result.stopReason,
|
|
280
|
+
elapsedMs: metaOut.elapsedMs,
|
|
281
|
+
...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
|
|
282
|
+
...(childUsage !== undefined ? { childUsage } : {}),
|
|
283
|
+
...(childCalls !== undefined ? { calls: childCalls } : {}),
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
// 信任标注:completed 子结果回灌父上下文前加结构化前缀(profile/preset 元数据、
|
|
287
|
+
// 无 prompt 原文),审计同字段 JSON 只进 logger、不进结果字符串;前缀只进 output 文本。
|
|
288
|
+
const output = `${trustLabel(metaOut)}${textFrom(pruned)}`;
|
|
289
|
+
deps.logger.info('[dsh-subagent-profile] trusted-output: ' + trustAudit(metaOut));
|
|
290
|
+
return { output, ...metaOut, decisionTrace: assertTraceSize(trace) };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 预检段读取:whitelist / parentComposed / parentToolNames 只读取一次,同时喂闸
|
|
294
|
+
// 与轨迹(避免重复 list() / schemas()),并组装 trace 骨架与交集闸 input。
|
|
295
|
+
async function prepareTrace(args, parent, deps) {
|
|
296
|
+
const agentPresets = parent.ctx.get('agentPresets');
|
|
297
|
+
const baseWhitelist = new Set(await resolveWhitelist(agentPresets));
|
|
298
|
+
const escapeSet = new Set(deps.getEscapeSet());
|
|
299
|
+
const whitelist = new Set([...baseWhitelist, ...escapeSet]);
|
|
300
|
+
const parentComposed = agentPresets !== undefined ? agentPresets.composedPreset(parent.ctx) : undefined;
|
|
301
|
+
const parentToolNames = readParentToolNames(parent);
|
|
302
|
+
const parentToolCount = parentToolNames.length;
|
|
303
|
+
// 决策输入快照:与 dispatch:profiles section 同源同序(profileDirectoryRows,
|
|
304
|
+
// enabled、cheap-first)——「模型当时看到哪些候选」是回答「为什么这么选」的记录面。
|
|
305
|
+
const rows = profileDirectoryRows(deps.store);
|
|
306
|
+
const snapshot = profileSnapshotOf(rows, { chosenId: args.profile });
|
|
307
|
+
const advicePresent = typeof deps.getEvolutionAdvice === 'function' && deps.getEvolutionAdvice() === true && parentComposed === 'orchestrator';
|
|
308
|
+
const trace = createDecisionTrace(
|
|
309
|
+
parentContextOf({
|
|
310
|
+
parentPreset: parentComposed,
|
|
311
|
+
parentProvider: parent.options.provider,
|
|
312
|
+
parentModel: parent.options.model,
|
|
313
|
+
parentToolCount,
|
|
314
|
+
allowFailOpen: deps.store.getAllowFailOpen(),
|
|
315
|
+
}),
|
|
316
|
+
requestedOf(args, { profiles: snapshot.entries, advicePresent })
|
|
317
|
+
);
|
|
318
|
+
const mode = args.continuable === true ? 'continuable' : (args.run_in_background === true ? 'background' : 'foreground');
|
|
319
|
+
const intersectionInput = { parentToolCount, requestedToolFilter: args.toolFilter, mode };
|
|
320
|
+
return { whitelist, baseWhitelist, escapeSet, parentComposed, parentToolNames, trace, mode, intersectionInput };
|
|
326
321
|
}
|
|
327
322
|
|
|
328
|
-
// execute 主体:预检段 +
|
|
323
|
+
// execute 主体:预检段 + 三分支分派。任一闸 fail 先记 fail 闸再 rethrow 原错误
|
|
324
|
+
// 对象,失败 trace 进进程内台账(deps.ledger)。
|
|
329
325
|
async function runDispatch(args, exec, deps) {
|
|
330
326
|
const parent = exec.agent;
|
|
331
|
-
if (!parent) throw new Error('dispatch
|
|
327
|
+
if (!parent) throw new Error('dispatch: 缺少调用 Agent(值需来自模型/编排者会话,请勿直接调用本工具)');
|
|
332
328
|
// 派发耗时基准:execute 入口记 t0,前台/后台各自在结算处算 elapsedMs。
|
|
333
|
-
// continuable 不结算、不携带,故 t0 仅穿线到前台/后台。
|
|
334
329
|
const t0 = Date.now();
|
|
335
|
-
const
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
330
|
+
const { whitelist, baseWhitelist, escapeSet, parentComposed, parentToolNames, trace, mode, intersectionInput } = await prepareTrace(args, parent, deps);
|
|
331
|
+
// 预算闸句柄提至 try 外:任何分支 throw(前台 start reject / 后台 jobs 缺失等)
|
|
332
|
+
// 都在 catch 里幂等 release 并发槽——防错误路径把父会话的并发额度永久耗尽
|
|
333
|
+
// (正常路径 finish 已 release,catch 再调为 no-op)。
|
|
334
|
+
let guardHandle = { release: () => {} };
|
|
335
|
+
try {
|
|
336
|
+
const merged = mergeProfileArgs(args, deps.store);
|
|
337
|
+
// 信封模式 opt-in:三分支创建子 Agent 前,把信封骨架追加进子 persona(systemPrompt
|
|
338
|
+
// 影子段、模型可见),不触碰 descriptor;既有 persona 非空时以空行衔接追加。
|
|
339
|
+
if (args.envelope === true) {
|
|
340
|
+
const hasPersona = merged.persona !== undefined && merged.persona.trim() !== '';
|
|
341
|
+
merged.persona = hasPersona ? `${merged.persona}\n\n${ENVELOPE_SKELETON}` : ENVELOPE_SKELETON;
|
|
342
|
+
}
|
|
343
|
+
assertContinuableEnabled(deps, args);
|
|
344
|
+
applyWhitelistGate(trace, merged, whitelist, parentComposed, escapeSet, baseWhitelist);
|
|
345
|
+
// Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控)。
|
|
346
|
+
await applyCostGuardGate(trace, merged, parent, deps);
|
|
347
|
+
// 交集闸(三分支统一闸序,见 dispatch-gates.mjs);continuable 的 effectiveAllow
|
|
348
|
+
// 同时用于 request 组装。
|
|
349
|
+
const effectiveAllow = applyIntersectionGate(trace, mode, intersectionInput, merged, parentToolNames);
|
|
350
|
+
recordApprovalGate(trace);
|
|
351
|
+
const meta = buildMeta(args, merged, parent);
|
|
352
|
+
const request = buildRequest(args, merged, parent, exec.signal);
|
|
353
|
+
const adoptionBase = { promptExcerpt: promptExcerptOf(args?.prompt), requestedModel: merged.model, parentModel: parent.options?.model };
|
|
354
|
+
// 结果回收默认剪枝:在 textFrom 前复用宿主 toolResultPruner.pruneContent 预剪;
|
|
355
|
+
// pruner 缺失时 pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。
|
|
356
|
+
const pruneResultOutput = (blocks) => pruneBlocks(blocks, deps.getService('toolResultPruner'));
|
|
357
|
+
logDispatchDecision(deps.logger, args, merged, parent);
|
|
358
|
+
// 总预算守卫:前/后台在 execute 入口占并发额度(continuable 不占),结算处 release
|
|
359
|
+
// + recordTokens。guardHandle 传入分支用于释放与父 sessionId 记账。
|
|
360
|
+
guardHandle = applyBudgetGate(deps, parent, args, trace);
|
|
361
|
+
// 派发台账静态元数据(双栏 cfg/task 指纹),分支结算处补 child_id+outcome 写盘。
|
|
362
|
+
const base = deps.evoLedger.baseEntry({ parent, args, merged, mode });
|
|
363
|
+
recordEscapeAllow(deps, parent, merged, trace);
|
|
364
|
+
if (args.continuable === true) return await runContinuable(args, merged, meta, parent, exec, deps, trace, effectiveAllow, base);
|
|
365
|
+
if (args.run_in_background === true) return await runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, exec);
|
|
366
|
+
return await runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, adoptionBase);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
guardHandle.release();
|
|
369
|
+
recordFailure(deps.ledger, parent, trace);
|
|
370
|
+
throw error;
|
|
345
371
|
}
|
|
346
|
-
await assertPresetWhitelist(parent, merged);
|
|
347
|
-
// Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控;
|
|
348
|
-
// llm 目录读取走共享 catalog 快照)。
|
|
349
|
-
await assertCostGuard(parent, merged, deps.store.getAllowFailOpen(), deps.logger, deps.catalog);
|
|
350
|
-
const meta = buildMeta(args, merged, parent);
|
|
351
|
-
const request = buildRequest(args, merged, parent, exec.signal);
|
|
352
|
-
// 结果回收默认剪枝:在 textFrom(result.output) 之前复用宿主
|
|
353
|
-
// toolResultPruner.pruneContent 预剪。`pruneResultOutput` 每次现取
|
|
354
|
-
// ctx.get('toolResultPruner') 以反映服务就绪状态;pruner 缺失时
|
|
355
|
-
// pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。envelope:true 时信封
|
|
356
|
-
// 骨架已在上方追加进 merged.persona(不改变此处的剪枝回收路径)。
|
|
357
|
-
const pruneResultOutput = (blocks) => pruneBlocks(blocks, deps.getService('toolResultPruner'));
|
|
358
|
-
logDispatchDecision(deps.logger, args, merged, parent);
|
|
359
|
-
if (args.continuable === true) return runContinuable(args, merged, meta, parent, exec, deps);
|
|
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);
|
|
362
372
|
}
|
|
363
373
|
|
|
364
|
-
export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog }) {
|
|
365
|
-
const deps = { store, getEnabled, getService, logger, subagents, catalog };
|
|
374
|
+
export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, getEscapeSet, getEvolutionAdvice }) {
|
|
375
|
+
const deps = { store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, getEscapeSet, getEvolutionAdvice };
|
|
366
376
|
const dispatchTool = defineTool({
|
|
367
377
|
name: 'dispatch',
|
|
368
|
-
description: '
|
|
378
|
+
description: '派发子任务给派生子 Agent,可逐个覆盖其 preset、model、provider、推理档位、persona、工具白名单、token 预算或递归深度。优先使用已保存的 profile,仅少数字段需要临时覆盖时才传 per-call 参数;未指定 model 时子代理继承父模型,查资料/汇总/读文件等轻任务请优先考虑 cheap 方案或显式 model=flash。前台等待结果;run_in_background: true 启动后台任务(单轮即弃);continuable: true 启动持久子 Agent,后续轮次经 send_message 工具延续对话。前瞻:continuable 模式忽略 preset 换用与 reasoningEffort(结果以 ignored 提示)。',
|
|
369
379
|
parameters: DISPATCH_PARAMETERS,
|
|
370
|
-
output: {
|
|
380
|
+
output: {
|
|
381
|
+
schema: DISPATCH_OUTPUT_SCHEMA,
|
|
382
|
+
render: DISPATCH_RENDER,
|
|
383
|
+
// 决策轨迹经 presentationMeta 投影进会话块 meta(客户端台账数据源),不进 render 行。
|
|
384
|
+
presentationMeta: (_args, value) => value.decisionTrace,
|
|
385
|
+
},
|
|
371
386
|
isConcurrencySafe: () => true,
|
|
372
387
|
execute: (args, exec) => runDispatch(args, exec, deps),
|
|
373
388
|
});
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// model-side schema never silently rejects a分支.
|
|
389
|
+
// 共享一致性规则锁:闭合 oneOf 结果 schema 的三个分支必须携带同一套共享
|
|
390
|
+
// 元数据键集。只在 apply 时触发;未来加元数据字段时若漏掉某个分支,在这里
|
|
391
|
+
// throw 一次(而不是让模型侧 schema 静默拒绝某个分支)。
|
|
378
392
|
assertResultSchemaConsistency(dispatchTool.output.schema);
|
|
379
|
-
//
|
|
380
|
-
// turns off so it disappears from the model's tool list without a restart.
|
|
393
|
+
// 只在启用时注册工具;开关一关立即注销,使工具无需重启就从模型的工具列表消失。
|
|
381
394
|
let disposeTool;
|
|
382
395
|
function syncTool() {
|
|
383
396
|
if (getEnabled() && disposeTool === undefined) {
|
|
@@ -385,7 +398,10 @@ export function createDispatchTool({ register, store, getEnabled, getService, lo
|
|
|
385
398
|
} else if (!getEnabled() && disposeTool !== undefined) {
|
|
386
399
|
const dispose = disposeTool;
|
|
387
400
|
disposeTool = undefined;
|
|
388
|
-
dispose();
|
|
401
|
+
if (typeof dispose === 'function') dispose();
|
|
402
|
+
// 禁用:级联取消在途派发 + 清空并发/token 记账(重开后记账从零起步)。
|
|
403
|
+
deps.guard.cancelAll();
|
|
404
|
+
deps.guard.reset();
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
407
|
syncTool();
|
|
@@ -395,7 +411,7 @@ export function createDispatchTool({ register, store, getEnabled, getService, lo
|
|
|
395
411
|
if (disposeTool !== undefined) {
|
|
396
412
|
const dispose = disposeTool;
|
|
397
413
|
disposeTool = undefined;
|
|
398
|
-
dispose();
|
|
414
|
+
if (typeof dispose === 'function') dispose();
|
|
399
415
|
}
|
|
400
416
|
}
|
|
401
417
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// lib/core/draft-gates.mjs — auto-profile S1 draft 落库前的闸评估。
|
|
2
|
+
// 与派发守卫闸同源(形状/白名单/目录),但不落库、不 throw——只产出结构化 checks
|
|
3
|
+
// 供 /draft/preview 与应用复用。依赖注入:ctx / catalog 由调用方传入;store 只读
|
|
4
|
+
// profiles Map 判断 id 冲突。
|
|
5
|
+
|
|
6
|
+
import { sanitizeProfile } from './pure.mjs';
|
|
7
|
+
import { resolveWhitelist } from './whitelist.mjs';
|
|
8
|
+
|
|
9
|
+
// 评估一个 draft:sanitize(形状/硬上限)→ id 冲突 → 白名单 → 目录。checks 每项
|
|
10
|
+
// 为 { name, verdict: 'pass'|'fail', reason? };ok = 全部 pass。任一闸 fail 时 clean
|
|
11
|
+
// 仍返回(sanitize 归一后的形态),调用方可用其展示「将被写入的生效值」。
|
|
12
|
+
export async function assessDraftProfile({ ctx, store, catalog, getEscapeSet, draft }) {
|
|
13
|
+
if (draft === null || typeof draft !== 'object') {
|
|
14
|
+
return { ok: false, checks: [{ name: 'draft', verdict: 'fail', reason: 'draft 不存在' }], clean: null };
|
|
15
|
+
}
|
|
16
|
+
const config = draft.config !== null && typeof draft.config === 'object' ? draft.config : {};
|
|
17
|
+
const profile = { ...config, name: draft.name, description: draft.description };
|
|
18
|
+
const { clean, warnings } = sanitizeProfile(profile, { strict: true });
|
|
19
|
+
const checks = [];
|
|
20
|
+
checks.push(warnings.length > 0
|
|
21
|
+
? { name: 'sanitize', verdict: 'fail', reason: warnings.map((w) => w.field + ':' + w.reason).join(';') }
|
|
22
|
+
: { name: 'sanitize', verdict: 'pass' });
|
|
23
|
+
if (typeof clean.id !== 'string' || clean.id === '') {
|
|
24
|
+
checks.push({ name: 'id', verdict: 'fail', reason: 'draft 缺少 profile id' });
|
|
25
|
+
} else if (store.profiles.has(clean.id)) {
|
|
26
|
+
checks.push({ name: 'id', verdict: 'fail', reason: 'profile ' + clean.id + ' 已存在(请先删除或改名)' });
|
|
27
|
+
} else {
|
|
28
|
+
checks.push({ name: 'id', verdict: 'pass' });
|
|
29
|
+
}
|
|
30
|
+
const whitelist = new Set(await resolveWhitelist(ctx.get('agentPresets'), getEscapeSet()));
|
|
31
|
+
if (typeof clean.preset === 'string' && clean.preset !== '' && clean.preset !== 'inherit' && !whitelist.has(clean.preset)) {
|
|
32
|
+
checks.push({ name: 'whitelist', verdict: 'fail', reason: '目标预设 ' + clean.preset + ' 不在 system-trust 白名单(可开启逃生舱并添加该预设后重试)' });
|
|
33
|
+
} else {
|
|
34
|
+
checks.push({ name: 'whitelist', verdict: 'pass' });
|
|
35
|
+
}
|
|
36
|
+
const snapshot = await catalog.getSnapshot();
|
|
37
|
+
if (typeof clean.model === 'string' && clean.model !== '' && !snapshot.models.some((m) => m && m.id === clean.model)) {
|
|
38
|
+
checks.push({ name: 'catalog', verdict: 'fail', reason: '模型 ' + clean.model + ' 不在当前模型目录' });
|
|
39
|
+
} else if (typeof clean.provider === 'string' && clean.provider !== '' && !snapshot.models.some((m) => m && m.provider === clean.provider)) {
|
|
40
|
+
checks.push({ name: 'catalog', verdict: 'fail', reason: '提供方 ' + clean.provider + ' 不在当前模型目录' });
|
|
41
|
+
} else {
|
|
42
|
+
checks.push({ name: 'catalog', verdict: 'pass' });
|
|
43
|
+
}
|
|
44
|
+
return { ok: checks.every((c) => c.verdict === 'pass'), checks, clean };
|
|
45
|
+
}
|