dsh-plugin-tool-management 0.20.3 → 0.21.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/CHANGELOG.md +72 -0
- package/README.md +9 -5
- package/README_EN.md +9 -5
- package/docs/images/icon.png +0 -0
- package/docs/images/icon.svg +1 -0
- package/docs/update.md +2 -0
- package/lib/client.js +829 -153
- package/lib/compat/capability-specs.js +60 -1
- package/lib/compat/host-contracts.js +72 -0
- package/lib/compat/host-probe.js +59 -0
- package/lib/compat/host-roots.js +1 -1
- package/lib/compat/probe.js +84 -13
- package/lib/context-inject-frame.js +168 -29
- package/lib/context-inject.js +13 -10
- package/lib/http-api.js +3 -2
- package/lib/http-fence.js +26 -1
- package/lib/index.js +34 -7
- package/lib/mcp/manager.js +6 -1
- package/lib/mcp/state-section.js +75 -34
- package/lib/memories/constants.js +39 -7
- package/lib/memories/projection.js +18 -18
- package/lib/memories/service.js +8 -7
- package/lib/memories/snapshot.js +39 -21
- package/lib/op-registry.js +3 -0
- package/lib/ops/compat.js +64 -2
- package/lib/ops/memory.js +2 -2
- package/lib/ops/prompts.js +30 -2
- package/lib/ops/trash.js +12 -1
- package/lib/request-gate.js +5 -4
- package/lib/settings-sidecars.js +37 -0
- package/lib/sidebar-settings.js +13 -0
- package/lib/skills/catalog.js +2 -2
- package/lib/subagents/catalog.js +4 -4
- package/lib/subagents/child-preset.js +120 -0
- package/lib/subagents/persona-parse.js +9 -9
- package/lib/subagents/run-provider.js +218 -62
- package/lib/subagents/tool-filter.js +12 -12
- package/lib/subagents/tools.js +29 -22
- package/lib/tools/deps.js +4 -4
- package/lib/tools/mcp.js +34 -34
- package/lib/tools/memory.js +34 -34
- package/lib/tools/prompt.js +5 -5
- package/lib/tools/remove.js +17 -17
- package/lib/tools/scene.js +66 -66
- package/lib/tools/skills.js +21 -21
- package/lib/tools/subagent.js +13 -13
- package/lib/tools/table.js +2 -2
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +28 -24
package/lib/ops/compat.js
CHANGED
|
@@ -11,7 +11,22 @@ import { INJECT_DOMAIN_KEYS } from '../context-inject.js';
|
|
|
11
11
|
import { MCP_TOOL_PREFIX } from '../host-names.js';
|
|
12
12
|
import { assessPresetReach } from '../compat/preset-reach.js';
|
|
13
13
|
import { runtimeNotes } from '../compat/runtime-notes.js';
|
|
14
|
+
import { AGENTS_MD_REFRESH_PACKAGE } from '../compat/host-contracts.js';
|
|
14
15
|
import { pluginLog } from '../skills/service.js';
|
|
16
|
+
/**
|
|
17
|
+
* 功能总览里那几条「宿主契约」行对应的**能力 id**(capability-specs.ts 那张表)。
|
|
18
|
+
*
|
|
19
|
+
* 为什么要一张显式映射:`findings` 的 id 是宿主侧的稳定契约名(`agentPreset.register` /
|
|
20
|
+
* `tools.restrict-method` / `agent.created-event` / `connection.admit`),而行 key 是本插件自己
|
|
21
|
+
* 的界面标识(`subagent-persona` / ...)。两边命名空间不同,写成字面量表最直白,也最不容易
|
|
22
|
+
* 在改名时悄悄对错。
|
|
23
|
+
*/
|
|
24
|
+
const CONTRACT_CAPABILITY_IDS = {
|
|
25
|
+
'subagent-persona': 'agentPreset.register',
|
|
26
|
+
'subagent-tool-restrict': 'tools.restrict-method',
|
|
27
|
+
'subagent-created-event': 'agent.created-event',
|
|
28
|
+
'web-admission': 'connection.admit',
|
|
29
|
+
};
|
|
15
30
|
export function buildCompatOps(deps) {
|
|
16
31
|
return {
|
|
17
32
|
'compat-status': async (args) => {
|
|
@@ -72,6 +87,9 @@ export function buildCompatOps(deps) {
|
|
|
72
87
|
// 修复指引按实际形态给:`scripts/` 不在 `files` 里,npm 安装形态下没有
|
|
73
88
|
// `scripts/doctor.mjs` 与 `scripts/host-deps.mjs`,指了也跑不了。
|
|
74
89
|
sourceInstall: isSourceInstall(),
|
|
90
|
+
// 宿主非 peer 契约的实况:插件按裸模块名 require 的那几个官方包。与 findings 不同源
|
|
91
|
+
// (模块层 vs 服务层),所以单独一个字段,界面据此列出"裸模块解析"那一块。
|
|
92
|
+
...(assessment.contracts === undefined ? {} : { contracts: assessment.contracts }),
|
|
75
93
|
findings,
|
|
76
94
|
degraded,
|
|
77
95
|
blockers: assessment.identity.blockers,
|
|
@@ -297,9 +315,53 @@ export function buildCompatOps(deps) {
|
|
|
297
315
|
...(asarHost === undefined ? {} : { asarHost: true, asarName: asarHost.asarName }),
|
|
298
316
|
}),
|
|
299
317
|
});
|
|
300
|
-
//
|
|
318
|
+
// 宿主契约层(0.2.1 新增的两处 + 一个连接面):这几条**不是**上面那些从 0.1.x 起
|
|
319
|
+
// 就在表里的能力,而是本版适配真实依赖的宿主成员。它们的共同点是缺了**不抛错**,
|
|
320
|
+
// 只是功能少一半 —— 所以必须在总览里占一行,让"升级后某处悄悄不工作"有地方看。
|
|
321
|
+
//
|
|
322
|
+
// 逐条的后果(与 capability-specs.ts 里的 fallback 一一对应):
|
|
323
|
+
// · agentPreset.register 没了 → 带人设的子代理委派失败(临时预设注册不上去);
|
|
324
|
+
// · tools.restrict-method 没了 → 人设的 tools/toolsDeny 不生效,子代理继承父级全部工具;
|
|
325
|
+
// · agent.created-event 串不起来 → 工具限制慢一步(首轮看到的还是继承来的工具集);
|
|
326
|
+
// · connection.admit 没了 → Web 鉴权退回 requestRejection,再缺则退回本地回环判定。
|
|
327
|
+
const findingOf = (id) => assessment === undefined ? undefined : assessment.findings.find((item) => item.id === id);
|
|
328
|
+
const contractRows = [
|
|
329
|
+
['subagent-persona', '子智能体人设委派', 'subagents'],
|
|
330
|
+
['subagent-tool-restrict', '子代理工具限制', 'subagents'],
|
|
331
|
+
['subagent-created-event', '子代理创建时序', 'subagents'],
|
|
332
|
+
['web-admission', 'Web 连接准入', 'compat'],
|
|
333
|
+
];
|
|
334
|
+
for (const [key, label, tab] of contractRows) {
|
|
335
|
+
const finding = findingOf(CONTRACT_CAPABILITY_IDS[key] ?? '');
|
|
336
|
+
push({
|
|
337
|
+
key, label, tab,
|
|
338
|
+
state: finding === undefined ? 'unknown' : finding.state === 'ok' ? 'ok' : 'degraded',
|
|
339
|
+
detail: finding === undefined ? '能力探测不可用(归档服务未挂载)' : finding.detail,
|
|
340
|
+
...(finding === undefined ? {} : { capabilityId: finding.id }),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// 无独立装配点的域:这一行的依据必须是**真的查过什么**,不能拿一句"无降级上报"糊过去 ——
|
|
344
|
+
// 那句话对没有上报通道的域恒真(它只说明"没坏到会上报",不说明"现在好不好")。
|
|
345
|
+
// 有宿主模块可核的域(提示词走 AGENTS.md 裸模块,子智能体还要加上人设包)按实况说;
|
|
346
|
+
// 记忆 / 场景档案确实没有可核的宿主模块,那才停留在"无降级上报"。
|
|
347
|
+
const contractFeature = (packageName) => assessment?.contracts?.packages.find((item) => item.package === packageName);
|
|
301
348
|
for (const [key, label, tab] of [['memory', '记忆', 'memory'], ['prompts', '提示词', 'prompts'], ['subagents', '子智能体', 'subagents'], ['scenes', '场景档案', 'scenes']]) {
|
|
302
|
-
|
|
349
|
+
// 子智能体的依据是那三条宿主契约 + AGENTS.md 裸模块(人设正文要走它注入)。
|
|
350
|
+
const persona = contractFeature('@deepseek-ai/dsh-persona');
|
|
351
|
+
const instructions = contractFeature(AGENTS_MD_REFRESH_PACKAGE);
|
|
352
|
+
const deps = key === 'subagents'
|
|
353
|
+
? [persona, instructions].filter((item) => item !== undefined)
|
|
354
|
+
: key === 'prompts' ? [instructions].filter((item) => item !== undefined) : [];
|
|
355
|
+
const broken = deps.filter((item) => item.resolved === null || item.missingMarkers.length > 0);
|
|
356
|
+
push({
|
|
357
|
+
key, label, tab,
|
|
358
|
+
state: broken.length > 0 ? 'degraded' : 'ok',
|
|
359
|
+
detail: broken.length > 0
|
|
360
|
+
? '宿主契约缺失:' + broken.map((item) => item.feature).join('、')
|
|
361
|
+
: deps.length > 0
|
|
362
|
+
? '依赖的宿主模块在位(' + deps.map((item) => item.feature).join('、') + ')'
|
|
363
|
+
: '无降级上报(装配失败会出现在这里)',
|
|
364
|
+
});
|
|
303
365
|
}
|
|
304
366
|
return { ok: true, rows, generatedAt: Date.now() };
|
|
305
367
|
}
|
package/lib/ops/memory.js
CHANGED
|
@@ -9,7 +9,7 @@ import { lstat, mkdir, readFile, rename, rm } from 'node:fs/promises';
|
|
|
9
9
|
import { basename, dirname, join } from 'node:path';
|
|
10
10
|
import { parseSkillDoc, unquote } from '../skills/core.js';
|
|
11
11
|
import { expandUploads, planMemoryImport } from '../imports/upload.js';
|
|
12
|
-
import { MAX_DESCRIPTION_LENGTH, MAX_RULE_BYTES, DEFAULT_ORDER, DEFAULT_GROUP_ORDER, GLOBAL_SCENE, GLOBAL_SCENE_LABEL,
|
|
12
|
+
import { MAX_DESCRIPTION_LENGTH, MAX_RULE_BYTES, DEFAULT_ORDER, DEFAULT_GROUP_ORDER, GLOBAL_SCENE, GLOBAL_SCENE_LABEL, MAX_ATTACH_ENTRY_BYTES, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_ENTRIES, LEGACY_BUNDLE_DOC, bundleDocName, SEGMENT_RULE_HINT, isValidGroupSegment, isValidGroupPath, message, fail } from '../memories/constants.js';
|
|
13
13
|
import { normalizeActive, enabledSceneOf, sceneOf, sceneLabel, attachmentSummarySync } from '../memories/projection.js';
|
|
14
14
|
import { writeFileAtomically, writeFileAtomicBinary, readIndex, writeIndex, pathExists } from '../memories/index-io.js';
|
|
15
15
|
import { deriveDescription } from '../memories/snapshot.js';
|
|
@@ -195,7 +195,7 @@ export function buildMemoryOps(rc) {
|
|
|
195
195
|
issues.push({
|
|
196
196
|
severity: 'warning',
|
|
197
197
|
code: 'sceneMemoryTruncated',
|
|
198
|
-
message: `场景记忆段超出预算(${projection.bytes} 字节 > ${projection.maxBytes}
|
|
198
|
+
message: `场景记忆段超出预算(${projection.bytes} 字节 > ${projection.maxBytes} 字节),已按确定性顺序截断,段尾附未注入清单与截断标记;请精简记忆或只保留 _shared。`,
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
return {
|
package/lib/ops/prompts.js
CHANGED
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 与 ops/compat.ts 同一条纪律:只负责 op 实现,依赖显式传参;MCP 写后刷新、场景锁定守卫
|
|
4
4
|
// 仍作用在组装后的整张表上(见 index.ts)。
|
|
5
|
+
import { resolveAgentsHome } from '../skills/homes.js';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
5
8
|
export function buildPromptOps(deps) {
|
|
9
|
+
/**
|
|
10
|
+
* 共享 agents 根的 `AGENTS.md`(`~/.agents/AGENTS.md`)。
|
|
11
|
+
*
|
|
12
|
+
* 0.2.1 起这条文件**也在被注入**:官方 `dsh-agent-instructions` 的用户全局链是
|
|
13
|
+
* `$DSH_HOME/AGENTS.md` → `<agentsHome>/AGENTS.md` 两份并列(内容相同只渲染一次),
|
|
14
|
+
* 而本插件的提示词页只管得到第一份。于是"我改了什么都没变化"有了第二个成因:
|
|
15
|
+
* 真正在说话的是第二份。这里只回答**它在不在、路径在哪**,不提供写入入口 ——
|
|
16
|
+
* 写入会与官方自己的去重/刷新规则互相覆盖,而这份文件的读者还包括别的 agent 工具。
|
|
17
|
+
*/
|
|
18
|
+
const agentsHomeAgentsMd = () => {
|
|
19
|
+
const path = join(resolveAgentsHome(), 'AGENTS.md');
|
|
20
|
+
let exists = false;
|
|
21
|
+
try {
|
|
22
|
+
exists = existsSync(path);
|
|
23
|
+
}
|
|
24
|
+
catch { /* 读不到 = 当作不在,不阻塞列表 */ }
|
|
25
|
+
return { path, exists };
|
|
26
|
+
};
|
|
6
27
|
return {
|
|
7
28
|
// 提示词预设库(由 ../prompts/service.ts 提供)。这里把两件事对齐成用户看到的一份状态:
|
|
8
29
|
// - `active`(界面「生效中」)= **当前真正在起作用的基线**:启用的场景绑了预设就是它,
|
|
@@ -40,7 +61,7 @@ export function buildPromptOps(deps) {
|
|
|
40
61
|
refs: refs ? refs.get(p.id) || [] : [],
|
|
41
62
|
};
|
|
42
63
|
});
|
|
43
|
-
return { ...base, presets, ...(scenePrompt ? { scenePrompt } : {}) };
|
|
64
|
+
return { ...base, presets, ...(scenePrompt ? { scenePrompt } : {}), agentsHomeAgentsMd: agentsHomeAgentsMd() };
|
|
44
65
|
},
|
|
45
66
|
'agentsmd-read': (args) => deps.promptsService.read(String((args && args.id) || '')),
|
|
46
67
|
// content 与 from 都可选:新建弹窗里直接写正文(content),或从现有预设复制(from)。
|
|
@@ -107,7 +128,14 @@ export function buildPromptOps(deps) {
|
|
|
107
128
|
}
|
|
108
129
|
return res;
|
|
109
130
|
},
|
|
110
|
-
|
|
131
|
+
// 与 `agentsmd-list` 同一份事实:两处都要展示"共享 agents 根那一份在不在"(列表用来
|
|
132
|
+
// 提示,当前页用来对照),分成两个实现必然会出现"列表说在、当前页说不在"。
|
|
133
|
+
'agentsmd-get-current': async () => {
|
|
134
|
+
const res = await deps.promptsService.getCurrent();
|
|
135
|
+
if (!res || res.ok === false)
|
|
136
|
+
return res;
|
|
137
|
+
return { ...res, agentsHomeAgentsMd: agentsHomeAgentsMd() };
|
|
138
|
+
},
|
|
111
139
|
// 删除:**场景绑定**与**当前应用的那份**拦得住(判据见下方注释)。拒绝时逐条说明「谁在用」,
|
|
112
140
|
// 用户知道该先改哪里。引用清单由 scene-prompt-sync 与显示/注入同源算出。
|
|
113
141
|
//
|
package/lib/ops/trash.js
CHANGED
|
@@ -269,7 +269,18 @@ export function buildTrashOps(rc) {
|
|
|
269
269
|
catch {
|
|
270
270
|
return fail('error.rules.notFound', `回收站条目不存在:${id}`);
|
|
271
271
|
}
|
|
272
|
-
|
|
272
|
+
// 清单也一并回给界面(用户 2026-10-18:「回收站的详细功能做的不够好,显示的内容不够全,
|
|
273
|
+
// 例如场景没有显示勾选了什么」):场景的档案、记忆的场景 / 形态都在 `manifest.json` 里,
|
|
274
|
+
// 只回文件列表的话,详情页能看到的就只剩"有几个文件"。
|
|
275
|
+
// 读不到 / 坏了就回 null —— 详情页按"只有文件"那一档渲染,不因为清单缺了就整页报错。
|
|
276
|
+
let manifest = null;
|
|
277
|
+
try {
|
|
278
|
+
manifest = JSON.parse(await readFile(join(dir, 'manifest.json'), 'utf8'));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
manifest = null;
|
|
282
|
+
}
|
|
283
|
+
return { ok: true, id, domain, manifest, files: await readTrashPayloadFiles(dir) };
|
|
273
284
|
}
|
|
274
285
|
return { rulesRemove, rulesRestore, rulesTrashList, rulesTrashRemove, trashPreview };
|
|
275
286
|
}
|
package/lib/request-gate.js
CHANGED
|
@@ -31,9 +31,10 @@ export function createAccessToken(deps) {
|
|
|
31
31
|
// 2. defense in depth — when set, every state-changing op additionally
|
|
32
32
|
// requires `x-dsh-token: <token>`.
|
|
33
33
|
// It is NOT the primary gate anymore: the route calls the host's
|
|
34
|
-
// connection.
|
|
35
|
-
//
|
|
36
|
-
// so an unset token no longer
|
|
34
|
+
// connection.admit fence first (Host must be loopback/LAN IP-literal →
|
|
35
|
+
// defeats DNS rebinding, plus browser-session cookie auth, plus the
|
|
36
|
+
// owner-Principal check an exact route owes), so an unset token no longer
|
|
37
|
+
// means "anyone may call writes".
|
|
37
38
|
// NOTE: the entry config arrives as the SECOND apply argument (Cordis
|
|
38
39
|
// calls `callback(ctx, config)`) — never read it off `ctx.config`, which
|
|
39
40
|
// is not an injected service and throws "cannot get property without
|
|
@@ -151,7 +152,7 @@ export function createSceneLock(deps) {
|
|
|
151
152
|
if (!scene)
|
|
152
153
|
return null;
|
|
153
154
|
const locked = await lockedSceneNames();
|
|
154
|
-
return locked.includes(scene) ?
|
|
155
|
+
return locked.includes(scene) ? `scene "${scene}" is locked, so this write was refused: that domain cannot be modified while the scene is locked; the user has to unlock it first.` : null;
|
|
155
156
|
}
|
|
156
157
|
return { lockedSceneNames, activeSceneName, lockedSceneGuard };
|
|
157
158
|
}
|
package/lib/settings-sidecars.js
CHANGED
|
@@ -10,6 +10,7 @@ import { hubPath } from './hub.js';
|
|
|
10
10
|
import { DEFAULT_INJECT_SETTINGS, normalizeInjectSettings } from './context-inject.js';
|
|
11
11
|
import { normalizeSceneSettings } from './scene-settings.js';
|
|
12
12
|
import { normalizeSlashSettings } from './slash-settings.js';
|
|
13
|
+
import { normalizeSidebarSettings } from './sidebar-settings.js';
|
|
13
14
|
export function createSettingsSidecars(deps) {
|
|
14
15
|
// 注入设置(侧车 `inject-settings.json`,界面在「兼容」页):
|
|
15
16
|
// - underSuppressingPresets:压制型预设(persona complete / 关运行时上下文,如极简)
|
|
@@ -131,6 +132,40 @@ export function createSettingsSidecars(deps) {
|
|
|
131
132
|
return { ok: true, settings: next };
|
|
132
133
|
});
|
|
133
134
|
}
|
|
135
|
+
// 侧栏入口的开关(侧车 `sidebar-settings.json`):只一项 —— 侧栏里挂不挂本插件那一行。
|
|
136
|
+
// 与 `slash-settings` 同规格:入口开关,不冻结;读侧不带 `set` 是纯读(客户端 boot 时
|
|
137
|
+
// 必问一次决定注不注册,那一次不能要令牌,否则没配令牌的宿主上这个入口永远出不来)。
|
|
138
|
+
const SIDEBAR_SETTINGS_FILE = 'sidebar-settings.json';
|
|
139
|
+
const SIDEBAR_SETTINGS_TTL_MS = 3000;
|
|
140
|
+
let sidebarSettingsCache = null;
|
|
141
|
+
async function readSidebarSettings(force = false) {
|
|
142
|
+
if (sidebarSettingsCache && !force && Date.now() - sidebarSettingsCache.at < SIDEBAR_SETTINGS_TTL_MS)
|
|
143
|
+
return sidebarSettingsCache.value;
|
|
144
|
+
await deps.ensurePaths();
|
|
145
|
+
const raw = await deps.readJsonFile(hubPath(SIDEBAR_SETTINGS_FILE));
|
|
146
|
+
const value = normalizeSidebarSettings(raw);
|
|
147
|
+
sidebarSettingsCache = { at: Date.now(), value };
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
async function sidebarSettingsOp(args) {
|
|
151
|
+
const current = await readSidebarSettings();
|
|
152
|
+
if (!args || args.set !== true)
|
|
153
|
+
return { ok: true, settings: current };
|
|
154
|
+
const next = normalizeSidebarSettings({
|
|
155
|
+
enabled: typeof args.enabled === 'boolean' ? args.enabled : current.enabled,
|
|
156
|
+
});
|
|
157
|
+
await deps.ensurePaths();
|
|
158
|
+
return deps.withWriteLock(async () => {
|
|
159
|
+
try {
|
|
160
|
+
await deps.writeJsonFile(hubPath(SIDEBAR_SETTINGS_FILE), next);
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
return { ok: false, error: '设置保存失败: ' + deps.message(e) };
|
|
164
|
+
}
|
|
165
|
+
sidebarSettingsCache = { at: Date.now(), value: next };
|
|
166
|
+
return { ok: true, settings: next };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
134
169
|
return {
|
|
135
170
|
readInjectSettings,
|
|
136
171
|
injectSettingsSync,
|
|
@@ -140,5 +175,7 @@ export function createSettingsSidecars(deps) {
|
|
|
140
175
|
sceneSettingsOp,
|
|
141
176
|
readSlashSettings,
|
|
142
177
|
slashSettingsOp,
|
|
178
|
+
readSidebarSettings,
|
|
179
|
+
sidebarSettingsOp,
|
|
143
180
|
};
|
|
144
181
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 侧栏入口的开关(侧车 `sidebar-settings.json`,按钮在「兼容」页页头)。
|
|
2
|
+
//
|
|
3
|
+
// 与 `slash-settings.ts` 同一档:它是个**入口开关**,不是管理动作 —— 所以不进场景冻结
|
|
4
|
+
// (锁着场景的人照样能把它关掉),也不参与任何门禁判定。
|
|
5
|
+
//
|
|
6
|
+
// 为什么存服务端而不是 localStorage:与其余入口开关同一个家,换浏览器也在;存本地就
|
|
7
|
+
// 只有那个浏览器认,而这条开关的用途正是"这台机器上我不想在侧栏看到它"。
|
|
8
|
+
export const DEFAULT_SIDEBAR_SETTINGS = { enabled: true };
|
|
9
|
+
/** 把任意输入夹成合法设置:读不到 / 形状不对一律按默认开启。 */
|
|
10
|
+
export function normalizeSidebarSettings(raw) {
|
|
11
|
+
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
12
|
+
return { enabled: typeof obj.enabled === 'boolean' ? obj.enabled : DEFAULT_SIDEBAR_SETTINGS.enabled };
|
|
13
|
+
}
|
package/lib/skills/catalog.js
CHANGED
|
@@ -81,8 +81,8 @@ export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES,
|
|
|
81
81
|
// 它只会去猜名字。数量照报 —— 那是实话,与有没有出口无关。
|
|
82
82
|
if (hidden > 0) {
|
|
83
83
|
out.push('', listToolVisible
|
|
84
|
-
?
|
|
85
|
-
:
|
|
84
|
+
? `(${hidden} more not listed — see them with \`skill_manager_list\`.)`
|
|
85
|
+
: `(${hidden} more not listed.)`);
|
|
86
86
|
}
|
|
87
87
|
return out.join('\n');
|
|
88
88
|
}
|
package/lib/subagents/catalog.js
CHANGED
|
@@ -13,7 +13,7 @@ export const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
|
13
13
|
/** 段里最多列几个人设;超出部分只报数量,让模型自己去调 subagent_manager_list。 */
|
|
14
14
|
export const DEFAULT_CATALOG_MAX_ENTRIES = 50;
|
|
15
15
|
/** 无描述时的占位,与 `subagent_manager_list` 工具的输出保持同一口径。 */
|
|
16
|
-
const NO_DESCRIPTION = '(
|
|
16
|
+
const NO_DESCRIPTION = '(no description)';
|
|
17
17
|
/**
|
|
18
18
|
* 描述归一化 + 截断 —— 逐字对齐官方 `dsh-tool-skill` 的实现:
|
|
19
19
|
* 先把所有空白折叠成单空格,再按长度截断并补省略号。保证「一人设一行」。
|
|
@@ -57,12 +57,12 @@ export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_
|
|
|
57
57
|
const out = [...lines];
|
|
58
58
|
// 查询工具**只在真被 40 条上限截掉时**才出现:常态下不提,省常驻字符,也免得模型为了
|
|
59
59
|
// 「确认一遍」去调它(用户裁定:没列出来的就是当前不想要的)。与 MCP 状态段的
|
|
60
|
-
//
|
|
60
|
+
// `(${N} more not listed.)` 同一句式,但这里多给一个出口——不给人设就真的找不回来了。
|
|
61
61
|
if (hidden > 0) {
|
|
62
62
|
// 与技能目录同一条纪律:点名一个被关掉的工具只会让模型去猜名字。数量照报。
|
|
63
63
|
out.push('', listToolVisible
|
|
64
|
-
?
|
|
65
|
-
:
|
|
64
|
+
? `(${hidden} more not listed — see them with \`subagent_manager_list\`.)`
|
|
65
|
+
: `(${hidden} more not listed.)`);
|
|
66
66
|
}
|
|
67
67
|
return out.join('\n');
|
|
68
68
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Agent Preset helpers for persona-scoped child composition.
|
|
2
|
+
//
|
|
3
|
+
// DSH 0.2.1 removed persona/toolFilter from SubagentStartRequest. A spawn child
|
|
4
|
+
// can still select an independent preset, so this module clones the parent's
|
|
5
|
+
// declared preset, replaces its persona row, and registers a short-lived preset
|
|
6
|
+
// id for that child. The clone is mounted by the official registry; nothing in
|
|
7
|
+
// the official package is patched.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { JSON_SCHEMA, Type, load } from 'js-yaml';
|
|
10
|
+
import { renderPersonaPrompt } from './persona-parse.js';
|
|
11
|
+
const CHILD_PRESET_PREFIX = 'tool-management-child-';
|
|
12
|
+
const JS_EXPR_TAG = 'tag:yaml.org,2002:js';
|
|
13
|
+
const jsExprType = new Type(JS_EXPR_TAG, {
|
|
14
|
+
kind: 'scalar',
|
|
15
|
+
resolve: (data) => typeof data === 'string',
|
|
16
|
+
construct: (data) => ({ __jsExpr: data }),
|
|
17
|
+
represent: (data) => data.__jsExpr,
|
|
18
|
+
});
|
|
19
|
+
const entryListSchema = JSON_SCHEMA.extend(jsExprType);
|
|
20
|
+
/** `!!js` 标量(官方 `entryListSchema` 的 `JsExpr`):克隆时只带 `__jsExpr` 一个键。 */
|
|
21
|
+
function isJsExpr(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && '__jsExpr' in value;
|
|
23
|
+
}
|
|
24
|
+
function presetRosterOf(ctx) {
|
|
25
|
+
try {
|
|
26
|
+
return ctx?.get?.('agentPresets');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function cloneEntry(value) {
|
|
33
|
+
if (isJsExpr(value))
|
|
34
|
+
return { __jsExpr: value.__jsExpr };
|
|
35
|
+
if (!value || typeof value !== 'object')
|
|
36
|
+
return value;
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.map(cloneEntry);
|
|
39
|
+
const copy = {};
|
|
40
|
+
for (const [key, item] of Object.entries(value))
|
|
41
|
+
copy[key] = cloneEntry(item);
|
|
42
|
+
return copy;
|
|
43
|
+
}
|
|
44
|
+
function isPersonaOrPresetSwitch(row) {
|
|
45
|
+
if (row.name === '@deepseek-ai/dsh-persona')
|
|
46
|
+
return true;
|
|
47
|
+
// A custom preset row would replace the whole composition; cloning it would
|
|
48
|
+
// silently change the parent instead of merely overriding its persona.
|
|
49
|
+
return row.name === '@deepseek-ai/dsh-agent-preset';
|
|
50
|
+
}
|
|
51
|
+
function removePersonaRows(rows) {
|
|
52
|
+
const kept = [];
|
|
53
|
+
for (const value of rows) {
|
|
54
|
+
// 不是对象的行:官方 entry list 不接受它,说明这份组合已不是本插件读懂的形状。
|
|
55
|
+
// 静默原样保留会把"人设"语义悄悄换掉(克隆出来的预设带着看不懂的行),所以明确报错。
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
throw new Error('the parent agent preset composition contains a non-object plugin row; cannot clone it safely');
|
|
58
|
+
}
|
|
59
|
+
const row = value;
|
|
60
|
+
if (isPersonaOrPresetSwitch(row))
|
|
61
|
+
continue;
|
|
62
|
+
if (row.group === true) {
|
|
63
|
+
const config = Array.isArray(row.config) ? removePersonaRows(row.config) : row.config;
|
|
64
|
+
kept.push(cloneEntry({ ...row, config }));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
kept.push(cloneEntry(row));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return kept;
|
|
71
|
+
}
|
|
72
|
+
/** Read the parent preset's declared rows and return a child definition. */
|
|
73
|
+
export async function childPresetDefinition(parentAgent, persona) {
|
|
74
|
+
const parentCtx = parentAgent?.ctx;
|
|
75
|
+
const roster = presetRosterOf(parentCtx);
|
|
76
|
+
if (!roster || typeof roster.composedPreset !== 'function' || typeof roster.readDocument !== 'function' || typeof roster.register !== 'function') {
|
|
77
|
+
throw new Error('this host lacks the agent-preset read/register API, so no standalone persona can be created for the subagent; upgrade to DSH >= 0.2.1 and enable the agentPresets service');
|
|
78
|
+
}
|
|
79
|
+
const parentPresetId = String(roster.composedPreset(parentCtx) ?? '');
|
|
80
|
+
if (!parentPresetId)
|
|
81
|
+
throw new Error('this parent session has no agent preset bound; cannot safely create a subagent with a persona');
|
|
82
|
+
const document = await roster.readDocument(parentPresetId);
|
|
83
|
+
if (!document || typeof document.content !== 'string') {
|
|
84
|
+
throw new Error(`failed to read the parent agent preset "${parentPresetId}": readDocument returned no composition YAML`);
|
|
85
|
+
}
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = load(document.content, { schema: entryListSchema });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new Error(`failed to parse the parent agent preset "${parentPresetId}": ${String(error?.message ?? error)}`);
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(parsed)) {
|
|
94
|
+
throw new Error(`the composition of the parent agent preset "${parentPresetId}" is not a list of plugin rows; cannot clone it`);
|
|
95
|
+
}
|
|
96
|
+
const plugins = removePersonaRows(parsed);
|
|
97
|
+
plugins.unshift({
|
|
98
|
+
id: 'tool-management-child-persona',
|
|
99
|
+
name: '@deepseek-ai/dsh-persona',
|
|
100
|
+
config: { prefix: renderPersonaPrompt(persona) },
|
|
101
|
+
});
|
|
102
|
+
// hash 必须覆盖**父预设的组合正文**:同一个 id 的内容可以被 profile 补丁改写,只按
|
|
103
|
+
// id + 人设定哈希会让改过的内容继续复用旧定义(冷恢复/追问时加载的是上一版插件树)。
|
|
104
|
+
const hash = createHash('sha256')
|
|
105
|
+
.update(parentPresetId)
|
|
106
|
+
.update('\0')
|
|
107
|
+
.update(document.content)
|
|
108
|
+
.update('\0')
|
|
109
|
+
.update(persona.name)
|
|
110
|
+
.update('\0')
|
|
111
|
+
.update(renderPersonaPrompt(persona))
|
|
112
|
+
.digest('hex')
|
|
113
|
+
.slice(0, 16);
|
|
114
|
+
return {
|
|
115
|
+
id: CHILD_PRESET_PREFIX + hash,
|
|
116
|
+
name: `persona:${persona.name}`,
|
|
117
|
+
description: `Ephemeral child preset cloned from ${parentPresetId}`,
|
|
118
|
+
plugins: plugins,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -238,12 +238,12 @@ export const DEFAULT_PERSONA_CATALOG_DEPTH = 1;
|
|
|
238
238
|
* 放在本模块(叶子)而不是 `catalog.ts`:`tools.ts` 也要用它,而 `catalog.ts` 反向依赖
|
|
239
239
|
* `tools.ts` 的 `filterBySceneBinding` —— 放那边会形成 tools ↔ catalog 的循环。
|
|
240
240
|
*/
|
|
241
|
-
export const CONTINUABLE_MARK = '
|
|
241
|
+
export const CONTINUABLE_MARK = ' (continuable)';
|
|
242
242
|
/**
|
|
243
243
|
* 「不限制嵌套」的目录注入深度(界面下拉的第四档,前端同值见 client.js 的
|
|
244
244
|
* `CATALOG_DEPTH_UNLIMITED`)。
|
|
245
245
|
*
|
|
246
|
-
*
|
|
246
|
+
* 取值远大于宿主能嵌套到的深度(0.2.1 的 `dsh-subagent` 默认 `maxDepth: 1`),所以判据
|
|
247
247
|
* `深度 < catalogDepth` 在任何可达的会话里都成立 —— 判据本身不必为它加特例分支。
|
|
248
248
|
*/
|
|
249
249
|
export const UNLIMITED_PERSONA_CATALOG_DEPTH = 99;
|
|
@@ -330,18 +330,18 @@ export function textOfBlocks(blocks, type) {
|
|
|
330
330
|
export function emptyResultNote(output, stopReason) {
|
|
331
331
|
// 连思考都没有 → 子代理确实什么都没产出,这时说「没干活」是对的。
|
|
332
332
|
if (textOfBlocks(output, 'reasoning') === '')
|
|
333
|
-
return '(
|
|
334
|
-
const head = '
|
|
333
|
+
return '(the subagent produced no output)';
|
|
334
|
+
const head = 'the subagent returned no text — only reasoning';
|
|
335
335
|
switch (stopReason) {
|
|
336
336
|
case 'max-tokens':
|
|
337
|
-
return `(${head}
|
|
337
|
+
return `(${head}, but it hit the token limit before writing a conclusion; split the task smaller and delegate again if you need a result.)`;
|
|
338
338
|
case 'error':
|
|
339
|
-
return `(${head}
|
|
339
|
+
return `(${head}, but it errored; diagnostics follow.)`;
|
|
340
340
|
case 'aborted':
|
|
341
|
-
return `(${head}
|
|
341
|
+
return `(${head}, but it was aborted before writing a conclusion.)`;
|
|
342
342
|
case 'refusal':
|
|
343
|
-
return `(${head}
|
|
343
|
+
return `(${head}, but it refused this task.)`;
|
|
344
344
|
default:
|
|
345
|
-
return `(${head}
|
|
345
|
+
return `(${head}, and wrote no conclusion; split the task and delegate again, or carry on yourself.)`;
|
|
346
346
|
}
|
|
347
347
|
}
|