dsh-subagent-profile 0.3.0 → 0.3.1
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 +23 -6
- package/lib/client.js +113 -18
- 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 +26 -6
- package/lib/core/dispatch-schema.mjs +71 -0
- package/lib/core/dispatch-tool.mjs +99 -76
- 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/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` 注入)---------------
|
package/lib/core/pure.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/core/pure.mjs — import-free pure helpers (extracted from index.mjs and grown
|
|
2
2
|
// by later refactors). No @deepseek-ai imports and no external dependencies:
|
|
3
3
|
// @deepseek-ai symbols converge in lib/core/shims.mjs (the only such entry point),
|
|
4
4
|
// and this module is safe to import from bare-CI tests without the junction
|
|
@@ -197,6 +197,19 @@ function sanitizeToolFilterField(value, clean, warnings) {
|
|
|
197
197
|
if (Object.keys(tf).length > 0) clean.toolFilter = tf;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// tokenTier:成本/深度分层(cheap/balanced/premium),供目录排序与结果卡
|
|
201
|
+
// 展示分层。合法值透传;非法值剔除 + warn(与 maxTokens 等超限字段同口径——
|
|
202
|
+
// 剔除而非回填缺省值)。缺省 'balanced' 在 sanitizeProfile 末尾按「字段未提供」
|
|
203
|
+
// 单独回填,非法值不受该回填影响。
|
|
204
|
+
const TOKEN_TIERS = new Set(['cheap', 'balanced', 'premium']);
|
|
205
|
+
function sanitizeTokenTierField(value, clean, warnings) {
|
|
206
|
+
if (typeof value !== 'string' || !TOKEN_TIERS.has(value)) {
|
|
207
|
+
warnings.push({ field: 'tokenTier', reason: 'tokenTier 必须为 cheap/balanced/premium 之一' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
clean.tokenTier = value;
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
// --- 结果回收默认剪枝 ----------------------------------------------------------
|
|
201
214
|
// 子结果默认复用宿主 `toolResultPruner.pruneContent` 预剪(纯函数、零 LLM),
|
|
202
215
|
// 在 `textFrom` 之前执行,把回灌进父上下文的体积压到阈值内。
|
|
@@ -281,7 +294,7 @@ export function assertResultSchemaConsistency(schema) {
|
|
|
281
294
|
const KNOWN_PROFILE_FIELDS = new Set([
|
|
282
295
|
'id', 'name', 'description', 'persona', 'preset', 'provider', 'model',
|
|
283
296
|
'reasoningEffort', 'enabled', 'maxTokens', 'maxDepth', 'toolFilter',
|
|
284
|
-
'builtin', 'deleted',
|
|
297
|
+
'tokenTier', 'builtin', 'deleted',
|
|
285
298
|
]);
|
|
286
299
|
|
|
287
300
|
/**
|
|
@@ -305,6 +318,7 @@ export function sanitizeProfile(profile, options = {}) {
|
|
|
305
318
|
if (profile === null || typeof profile !== 'object' || Array.isArray(profile)) {
|
|
306
319
|
return { clean, warnings: [{ field: '(root)', reason: 'profile 不是对象' }] };
|
|
307
320
|
}
|
|
321
|
+
let tokenTierProvided = false;
|
|
308
322
|
for (const [key, value] of Object.entries(profile)) {
|
|
309
323
|
if (!KNOWN_PROFILE_FIELDS.has(key)) {
|
|
310
324
|
warnings.push({ field: key, reason: '未知字段已忽略' });
|
|
@@ -327,10 +341,27 @@ export function sanitizeProfile(profile, options = {}) {
|
|
|
327
341
|
case 'toolFilter':
|
|
328
342
|
sanitizeToolFilterField(value, clean, warnings);
|
|
329
343
|
break;
|
|
344
|
+
case 'tokenTier':
|
|
345
|
+
tokenTierProvided = true;
|
|
346
|
+
sanitizeTokenTierField(value, clean, warnings);
|
|
347
|
+
break;
|
|
330
348
|
default:
|
|
331
349
|
clean[key] = value;
|
|
332
350
|
break;
|
|
333
351
|
}
|
|
334
352
|
}
|
|
353
|
+
// tokenTier 缺省 'balanced':仅当字段未被提供时回填;被提供的非法值已在
|
|
354
|
+
// sanitizeTokenTierField 剔除(clean 保持无该键),不会被此回填覆盖。
|
|
355
|
+
if (!tokenTierProvided) clean.tokenTier = 'balanced';
|
|
335
356
|
return { clean, warnings };
|
|
336
357
|
}
|
|
358
|
+
|
|
359
|
+
// --- tokenTier 目录排序 ---------------------------------------------------------
|
|
360
|
+
// tokenTier 排序权重:cheap→balanced→premium。dispatch:profiles 目录行按此
|
|
361
|
+
// 升序排列(省 token 方案在前)。缺省/未知 tier 按 balanced 处理,保证旧数据
|
|
362
|
+
// 与无 tokenTier 字段的 profile 落到中间档而非报错。
|
|
363
|
+
export const TIER_ORDER = { cheap: 0, balanced: 1, premium: 2 };
|
|
364
|
+
export function tierSortKey(tokenTier) {
|
|
365
|
+
const order = TIER_ORDER[tokenTier];
|
|
366
|
+
return order === undefined ? TIER_ORDER.balanced : order;
|
|
367
|
+
}
|
package/lib/core/shims.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/core/shims.mjs — facade. The ONLY module that imports the
|
|
2
2
|
// @deepseek-ai symbols index.mjs relies on, converging the previously
|
|
3
3
|
// top-level-scattered import surface. Two failure classes:
|
|
4
4
|
//
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// also verifies the value is a function before accepting it.
|
|
24
24
|
|
|
25
25
|
import { randomUUID } from 'node:crypto';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
26
27
|
// Guard-type: static, fail-loud — no fallback. Kept as the only two
|
|
27
28
|
// static @deepseek-ai imports; a missing export aborts module load with a clear
|
|
28
29
|
// error BEFORE apply can run, which is the isolation this class exists for.
|
|
@@ -186,6 +187,89 @@ function readResult(child, boundary, cancelled) {
|
|
|
186
187
|
return { output: finalAssistantOutput(own) ?? [], stopReason };
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
// --- version detection (pure probe) ------------------------------------------
|
|
191
|
+
// 读取 @deepseek-ai 三包(subagent/agent/llm)的 package.json version,与
|
|
192
|
+
// peerDependencies 范围(>=PEER_MIN <PEER_MAX)比对后产出中文 warnings。纯探测:
|
|
193
|
+
// 只读 manifest、不 import 新符号、不触发副作用;每包独立 try/catch,失败记
|
|
194
|
+
// 'unknown'。headless / 宿主裁剪部署下任一包都可能缺失,此时 warnings 非空,
|
|
195
|
+
// 设置页据此在顶部渲染 amber 提示条(不阻断派发)。
|
|
196
|
+
const requirePkg = createRequire(import.meta.url);
|
|
197
|
+
const PEER_MIN = '0.1.0-rc.6';
|
|
198
|
+
const PEER_MAX = '0.2.0';
|
|
199
|
+
const PROBED_PACKAGES = ['dsh-subagent', 'dsh-agent', 'dsh-llm'];
|
|
200
|
+
|
|
201
|
+
// 读单包 version(每包独立 try/catch,失败记 'unknown')。用 createRequire 直接
|
|
202
|
+
// require 包的 package.json(返回解析后的对象),免去 fs 读取与 JSON.parse。
|
|
203
|
+
function readPackageVersion(pkg) {
|
|
204
|
+
try {
|
|
205
|
+
const manifest = requirePkg(`@deepseek-ai/${pkg}/package.json`);
|
|
206
|
+
return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : 'unknown';
|
|
207
|
+
} catch {
|
|
208
|
+
return 'unknown';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 极简 semver 比较:major.minor.patch + 可选 `-预发布` 段(覆盖 peerDependencies
|
|
213
|
+
// 范围判断所需)。预发布 < 正式版;预发布段逐段比较,纯数字段按数值、否则按字典序
|
|
214
|
+
// (`0.1.0-rc.10` > `0.1.0-rc.6`)。
|
|
215
|
+
function compareSemver(a, b) {
|
|
216
|
+
const [aCore, aPre = ''] = a.split('-');
|
|
217
|
+
const [bCore, bPre = ''] = b.split('-');
|
|
218
|
+
const aNums = aCore.split('.').map((n) => Number(n));
|
|
219
|
+
const bNums = bCore.split('.').map((n) => Number(n));
|
|
220
|
+
for (let i = 0; i < 3; i++) {
|
|
221
|
+
const x = aNums[i] ?? 0;
|
|
222
|
+
const y = bNums[i] ?? 0;
|
|
223
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
224
|
+
}
|
|
225
|
+
if (aPre === bPre) return 0;
|
|
226
|
+
if (aPre === '') return 1; // 正式版 > 预发布
|
|
227
|
+
if (bPre === '') return -1;
|
|
228
|
+
const aParts = aPre.split('.');
|
|
229
|
+
const bParts = bPre.split('.');
|
|
230
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
231
|
+
for (let i = 0; i < len; i++) {
|
|
232
|
+
const x = aParts[i];
|
|
233
|
+
const y = bParts[i];
|
|
234
|
+
if (x === undefined) return -1;
|
|
235
|
+
if (y === undefined) return 1;
|
|
236
|
+
const xNumeric = /^\d+$/.test(x);
|
|
237
|
+
const yNumeric = /^\d+$/.test(y);
|
|
238
|
+
if (xNumeric && yNumeric) {
|
|
239
|
+
const diff = Number(x) - Number(y);
|
|
240
|
+
if (diff !== 0) return diff;
|
|
241
|
+
} else if (xNumeric !== yNumeric) {
|
|
242
|
+
return xNumeric ? -1 : 1; // 数字标识 < 非数字标识(semver 约定)
|
|
243
|
+
} else if (x !== y) {
|
|
244
|
+
return x < y ? -1 : 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 版本是否落在 peerDependencies 范围(>=PEER_MIN 且 <PEER_MAX)。
|
|
251
|
+
function inPeerRange(version) {
|
|
252
|
+
return compareSemver(version, PEER_MIN) >= 0 && compareSemver(version, PEER_MAX) < 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// detectVersions(reader?) — 纯探测三包版本并产出 { versions, warnings }。
|
|
256
|
+
// `reader` 可选注入(默认 readPackageVersion),供测试模拟「包缺失 / 版本越界」
|
|
257
|
+
// 而无需删除 node_modules(与 loadSoft 的 importer 注入同一思路)。
|
|
258
|
+
export function detectVersions(reader = readPackageVersion) {
|
|
259
|
+
const versions = {};
|
|
260
|
+
for (const pkg of PROBED_PACKAGES) versions[pkg] = reader(pkg);
|
|
261
|
+
const warnings = [];
|
|
262
|
+
for (const pkg of PROBED_PACKAGES) {
|
|
263
|
+
const version = versions[pkg];
|
|
264
|
+
if (version === 'unknown') {
|
|
265
|
+
warnings.push(`未检测到 @deepseek-ai/${pkg} 版本(包缺失或被宿主裁剪)——请确认其已按 peerDependencies 范围安装`);
|
|
266
|
+
} else if (!inPeerRange(version)) {
|
|
267
|
+
warnings.push(`@deepseek-ai/${pkg} 版本 ${version} 超出 peerDependencies 范围(>=${PEER_MIN} <${PEER_MAX}),派发行为可能与预期不符`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { versions, warnings };
|
|
271
|
+
}
|
|
272
|
+
|
|
189
273
|
// Test-only access to the local degraded implementations (package imports
|
|
190
274
|
// resolve here, so the real functions win; __fallbacks lets a test exercise the
|
|
191
275
|
// fail-soft path without deleting node_modules).
|
|
@@ -212,4 +296,6 @@ export {
|
|
|
212
296
|
resolveChildAgentOptions,
|
|
213
297
|
defineTool,
|
|
214
298
|
readResult,
|
|
299
|
+
// ---- version probe seam (see detectVersions doc) ----
|
|
300
|
+
readPackageVersion,
|
|
215
301
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subagent-profile",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Dispatch one-shot subtasks to derived subagents with per-task overrides (preset/model/provider/reasoningEffort/persona/tool whitelist), a runtime-derived cost guard, a subagent-profiles service, observability metadata, and a web-GUI settings page plus a dispatch tool-call card.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"release": "node scripts/release.mjs",
|
|
78
78
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
79
79
|
"test:bare": "node --test test/pure.test.mjs test/input-schema.test.mjs test/catalog-integrity.test.mjs",
|
|
80
|
-
"preflight": "node scripts/preflight.mjs"
|
|
80
|
+
"preflight": "node scripts/preflight.mjs",
|
|
81
|
+
"leak-scan": "node scripts/leak-scan.mjs"
|
|
81
82
|
}
|
|
82
83
|
}
|