dsh-plugin-tool-management 0.2.0 → 0.5.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 +238 -226
- package/README_EN.md +304 -231
- package/cordis.patch.yml +11 -0
- package/docs/Changelog.md +517 -0
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
- package/lib/approval-policy.js +46 -0
- package/lib/client.js +3560 -2173
- package/lib/hub.js +94 -0
- package/lib/imports/upload.js +286 -0
- package/lib/index.js +512 -155
- package/lib/rules/archive-engine.js +191 -0
- package/lib/rules/archive.js +140 -0
- package/lib/rules/service.js +609 -100
- package/lib/skills/core.js +356 -22
- package/lib/skills/service.js +31 -5
- package/lib/subagents/service.js +333 -0
- package/lib/subagents/tools.js +62 -0
- package/package.json +12 -3
- package/screenshots.json +7 -5
- package/docs/images/agents-md.png +0 -0
- package/docs/images/history.png +0 -0
- package/docs/images/mcp.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images//345/234/272/346/231/257/350/256/260/345/277/206.png +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// src/subagents/service.ts —— 轻量子智能体:人设发现(TTL 扫描)+ 官方 ctx.subagents.start 薄封装。
|
|
2
|
+
// 设计 §3:人设 = $DSH_HOME/tool-management/agents/<name>.md(frontmatter 可选,缺省派生);v1 串行运行;
|
|
3
|
+
// spawn provider 缺失时经 createRequire 挂载官方 dsh-subagent-spawn-in-process(宿主侧包)。
|
|
4
|
+
//
|
|
5
|
+
// v0.4 目录变更:人设由 `$DSH_HOME/subagents/` 搬到 `$DSH_HOME/tool-management/agents/`
|
|
6
|
+
// (插件产生的文件统一收在 tool-management/ 下)。旧目录在首次扫描时搬入,见 relocateLegacyPersonas。
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { resolveDshHome } from '../skills/core.js';
|
|
11
|
+
import { expandUploads, planPersonaImport } from '../imports/upload.js';
|
|
12
|
+
const message = (e) => String((e && e.message) || e);
|
|
13
|
+
/** 人设默认目录:`$DSH_HOME/tool-management/agents/`。 */
|
|
14
|
+
export function defaultPersonasDir() {
|
|
15
|
+
return join(resolveDshHome(), 'tool-management', 'agents');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 旧目录 `$DSH_HOME/subagents/` → `tool-management/agents/` 一次性搬移(幂等)。
|
|
19
|
+
* 只在目标不存在同名文件时搬(绝不覆盖);源目录保留空壳。每个进程只跑一次。
|
|
20
|
+
*/
|
|
21
|
+
let personasRelocated = false;
|
|
22
|
+
async function relocateLegacyPersonas(dir) {
|
|
23
|
+
if (personasRelocated)
|
|
24
|
+
return;
|
|
25
|
+
personasRelocated = true;
|
|
26
|
+
const legacy = join(resolveDshHome(), 'subagents');
|
|
27
|
+
if (legacy === dir)
|
|
28
|
+
return;
|
|
29
|
+
let names;
|
|
30
|
+
try {
|
|
31
|
+
names = (await readdir(legacy)).filter((n) => n.toLowerCase().endsWith('.md'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!names.length)
|
|
37
|
+
return;
|
|
38
|
+
await mkdir(dir, { recursive: true }).catch(() => undefined);
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const from = join(legacy, name);
|
|
41
|
+
const to = join(dir, name);
|
|
42
|
+
try {
|
|
43
|
+
const exists = await stat(to).then(() => true).catch(() => false);
|
|
44
|
+
if (exists)
|
|
45
|
+
continue;
|
|
46
|
+
await rename(from, to);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* 单项失败(被占用等)不阻断其余;保留源文件,下次启动再试 */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** 行式 frontmatter 解析:只认 description / provider / model / tools / toolsDeny 五个键。 */
|
|
54
|
+
export function parsePersona(raw, fallbackName) {
|
|
55
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
|
|
56
|
+
const data = {};
|
|
57
|
+
let body = raw;
|
|
58
|
+
if (m) {
|
|
59
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
60
|
+
const kv = /^([A-Za-z_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
61
|
+
if (kv)
|
|
62
|
+
data[kv[1].toLowerCase()] = kv[2].trim();
|
|
63
|
+
}
|
|
64
|
+
body = raw.slice(m[0].length);
|
|
65
|
+
}
|
|
66
|
+
const listOf = (value) => {
|
|
67
|
+
if (!value)
|
|
68
|
+
return undefined;
|
|
69
|
+
const list = value.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
|
70
|
+
return list.length ? list : undefined;
|
|
71
|
+
};
|
|
72
|
+
const tools = listOf(data.tools);
|
|
73
|
+
// 兼容两种写法:`toolsdeny: a, b`(线上格式)与 `toolsDeny: a, b`(键名统一小写后同形)。
|
|
74
|
+
const toolsDeny = listOf(data.toolsdeny);
|
|
75
|
+
const firstLine = body.split(/\r?\n/).find((l) => l.trim())?.trim() ?? '';
|
|
76
|
+
return {
|
|
77
|
+
name: fallbackName,
|
|
78
|
+
description: data.description || firstLine,
|
|
79
|
+
provider: data.provider || undefined,
|
|
80
|
+
model: data.model || undefined,
|
|
81
|
+
tools,
|
|
82
|
+
toolsDeny,
|
|
83
|
+
body: body.trim(),
|
|
84
|
+
path: '',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const RESULT_MAX = 16 * 1024;
|
|
88
|
+
export function createSubagentService(ctx, opts) {
|
|
89
|
+
const dir = opts?.subagentsDir || defaultPersonasDir();
|
|
90
|
+
const req = createRequire(import.meta.url);
|
|
91
|
+
let cache = null;
|
|
92
|
+
async function list() {
|
|
93
|
+
if (cache && Date.now() - cache.at < 1000)
|
|
94
|
+
return cache.value;
|
|
95
|
+
// 旧目录搬家放在首次扫描前(幂等;失败不阻断,旧目录仍会被下面的读取兜底看到)。
|
|
96
|
+
await relocateLegacyPersonas(dir).catch(() => undefined);
|
|
97
|
+
const docs = [];
|
|
98
|
+
try {
|
|
99
|
+
const entries = await readdir(dir);
|
|
100
|
+
for (const fileName of entries.filter((e) => e.endsWith('.md')).sort()) {
|
|
101
|
+
const raw = await readFile(join(dir, fileName), 'utf8').catch(() => '');
|
|
102
|
+
if (!raw.trim())
|
|
103
|
+
continue;
|
|
104
|
+
docs.push({ ...parsePersona(raw, fileName.slice(0, -3)), path: join(dir, fileName) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { /* 目录缺失 = 无人设 */ }
|
|
108
|
+
cache = { at: Date.now(), value: docs };
|
|
109
|
+
return docs;
|
|
110
|
+
}
|
|
111
|
+
// spawn provider 在场性(设计 §3.2,落地方式见 cordis.patch.yml 的取舍注释):
|
|
112
|
+
// 官方通道探测(ctx.subagents.list())→ 缺失才挂载 → 失败把原因原样带出(不吞、不谎报)。
|
|
113
|
+
// 不是一次性静默标记:每次调用都先探测,宿主后来注册了 provider 也能立刻跟上。
|
|
114
|
+
let spawnMount = null;
|
|
115
|
+
function hasSpawnProvider(runtime) {
|
|
116
|
+
if (typeof runtime.list !== 'function')
|
|
117
|
+
return false;
|
|
118
|
+
try {
|
|
119
|
+
const names = runtime.list();
|
|
120
|
+
return Array.isArray(names) && names.indexOf('spawn') >= 0;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function ensureSpawnProvider() {
|
|
127
|
+
const runtime = ctx.get?.('subagents');
|
|
128
|
+
if (!runtime || typeof runtime.start !== 'function') {
|
|
129
|
+
throw new Error('子代理服务未挂载(ctx.subagents 缺失;请确认 DSH 版本 ≥0.1.5-rc.2)');
|
|
130
|
+
}
|
|
131
|
+
if (hasSpawnProvider(runtime))
|
|
132
|
+
return runtime;
|
|
133
|
+
// 宿主没有 list() 就探测不了:不重复挂载(避免同名 provider 二次注册),交给 start 报官方错误兜底。
|
|
134
|
+
if (typeof runtime.list !== 'function')
|
|
135
|
+
return runtime;
|
|
136
|
+
if (!spawnMount) {
|
|
137
|
+
try {
|
|
138
|
+
const mod = req('@deepseek-ai/dsh-subagent-spawn-in-process');
|
|
139
|
+
if (!mod || typeof mod.apply !== 'function')
|
|
140
|
+
throw new Error('模块未导出 apply(ctx, config)(版本不匹配?)');
|
|
141
|
+
mod.apply(ctx, { providerName: 'spawn' });
|
|
142
|
+
spawnMount = { ok: true };
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
spawnMount = {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: 'spawn provider 不可用:宿主没有注册它,本插件挂载 @deepseek-ai/dsh-subagent-spawn-in-process 也失败('
|
|
148
|
+
+ message(e) + ')。请在宿主 profile 挂载该包(本插件已声明为可选 peerDependency),或安装后重启 DSH。',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!spawnMount.ok)
|
|
153
|
+
throw new Error(spawnMount.error);
|
|
154
|
+
if (!hasSpawnProvider(runtime))
|
|
155
|
+
throw new Error('spawn provider 挂载后仍未出现在 ctx.subagents.list()(宿主版本不匹配?)');
|
|
156
|
+
return runtime;
|
|
157
|
+
}
|
|
158
|
+
async function runOnce(parentAgent, p, task, signal) {
|
|
159
|
+
const runtime = await ensureSpawnProvider();
|
|
160
|
+
// 官方签名:start(name, request) —— name = ctx.subagents 上的 provider 注册名。
|
|
161
|
+
const run = await runtime.start('spawn', {
|
|
162
|
+
label: p.name,
|
|
163
|
+
parent: parentAgent,
|
|
164
|
+
// 官方 SubagentStartRequest.signal 为必填:调用方缺省时给一个永不中止的信号,不传 undefined。
|
|
165
|
+
signal: signal ?? new AbortController().signal,
|
|
166
|
+
prompt: [{ type: 'text', text: task }],
|
|
167
|
+
persona: p.body,
|
|
168
|
+
// 工具白/黑名单 → 官方 ToolRestriction(`deny` 优先级高于 `allow`;未知名官方会直接拒绝启动,
|
|
169
|
+
// 所以选择器给的是「全体预设工具并集」,见 index.ts presetToolCandidates)。
|
|
170
|
+
...((p.tools?.length || p.toolsDeny?.length)
|
|
171
|
+
? { toolFilter: { ...(p.tools?.length ? { allow: p.tools } : {}), ...(p.toolsDeny?.length ? { deny: p.toolsDeny } : {}) } }
|
|
172
|
+
: {}),
|
|
173
|
+
maxDepth: 1,
|
|
174
|
+
// provider 与 model 是模型路由的两半:DSH 的 resolveModel(provider, model) 不做
|
|
175
|
+
// `provider/model` 字符串拆分,只改 model 会落在**主会话的 provider** 上——跨来源
|
|
176
|
+
// 指定模型(如 sensenova 的 sensenova-6.8-flash-lite)必须两个键一起给。
|
|
177
|
+
...(p.provider || p.model
|
|
178
|
+
? { agentOptions: { ...(p.provider ? { provider: p.provider } : {}), ...(p.model ? { model: p.model } : {}) } }
|
|
179
|
+
: {}),
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
const result = await run.result; // 官方契约:child 级失败不 reject(stopReason 体现)
|
|
183
|
+
const text = (result.output ?? [])
|
|
184
|
+
.map((b) => (b && typeof b === 'object' && typeof b.text === 'string' ? b.text : ''))
|
|
185
|
+
.join('').trim();
|
|
186
|
+
// 官方 SubagentResult 的诊断字段是 `diagnostic`(旧代码读 .detail 恒空 → 失败时模型只见空输出)。
|
|
187
|
+
const diagnostic = result.diagnostic ? `\n\n[provider] ${String(result.diagnostic)}` : '';
|
|
188
|
+
return {
|
|
189
|
+
text: ((text || '(子代理无输出)') + diagnostic).slice(0, RESULT_MAX),
|
|
190
|
+
runId: String(run.id ?? ''),
|
|
191
|
+
stopReason: String(result.stopReason ?? 'completed'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await run.dispose().catch(() => { }); // 官方前台语义:collection 后即弃
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// v1 串行:同一时刻至多一个子代理运行(设计 §3.2)。
|
|
199
|
+
let chain = Promise.resolve();
|
|
200
|
+
const runSerial = (parentAgent, p, task, signal) => {
|
|
201
|
+
const queued = chain.then(() => runOnce(parentAgent, p, task, signal), () => runOnce(parentAgent, p, task, signal));
|
|
202
|
+
chain = queued.catch(() => undefined);
|
|
203
|
+
return queued;
|
|
204
|
+
};
|
|
205
|
+
const ops = {
|
|
206
|
+
'subagent-list': async () => {
|
|
207
|
+
const docs = await list();
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
subagents: docs.map((p) => ({ name: p.name, description: p.description, provider: p.provider ?? null, model: p.model ?? null, tools: p.tools ?? null, toolsDeny: p.toolsDeny ?? null })),
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
'subagent-get': async (args) => {
|
|
214
|
+
const name = String((args && args.name) || '').trim();
|
|
215
|
+
const docs = await list();
|
|
216
|
+
const p = docs.find((d) => d.name === name);
|
|
217
|
+
if (!p)
|
|
218
|
+
return { ok: false, error: `人设不存在: ${name}` };
|
|
219
|
+
return { ok: true, persona: { name: p.name, description: p.description, provider: p.provider ?? '', model: p.model ?? '', tools: p.tools ?? [], toolsDeny: p.toolsDeny ?? [], body: p.body } };
|
|
220
|
+
},
|
|
221
|
+
'subagent-create': async (args) => {
|
|
222
|
+
const name = String((args && args.name) || '').trim();
|
|
223
|
+
if (!validPersonaName(name))
|
|
224
|
+
return { ok: false, error: `人设名不合法: ${name || '(空)'}(≤64 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头)` };
|
|
225
|
+
const target = join(dir, name + '.md');
|
|
226
|
+
const exists = await readFile(target, 'utf8').then(() => true).catch(() => false);
|
|
227
|
+
if (exists)
|
|
228
|
+
return { ok: false, error: `人设已存在: ${name}` };
|
|
229
|
+
// 目录必须先建:v0.4 起人设落在 hub 内的 agents/,全新安装时它还不存在
|
|
230
|
+
// (旧版本落在 $DSH_HOME/subagents/,那个目录一直有人建,所以这个坑以前不显形)。
|
|
231
|
+
try {
|
|
232
|
+
await mkdir(dir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
return { ok: false, error: `创建人设目录失败: ${dir}(${message(e)})` };
|
|
236
|
+
}
|
|
237
|
+
await writeFile(target, serializePersona(args), 'utf8');
|
|
238
|
+
cache = null;
|
|
239
|
+
return { ok: true, name };
|
|
240
|
+
},
|
|
241
|
+
'subagent-update': async (args) => {
|
|
242
|
+
const name = String((args && args.name) || '').trim();
|
|
243
|
+
if (!validPersonaName(name))
|
|
244
|
+
return { ok: false, error: `人设名不合法: ${name || '(空)'}` };
|
|
245
|
+
const target = join(dir, name + '.md');
|
|
246
|
+
const exists = await readFile(target, 'utf8').then(() => true).catch(() => false);
|
|
247
|
+
if (!exists)
|
|
248
|
+
return { ok: false, error: `人设不存在: ${name}` };
|
|
249
|
+
await writeFile(target, serializePersona(args), 'utf8');
|
|
250
|
+
cache = null;
|
|
251
|
+
return { ok: true, name };
|
|
252
|
+
},
|
|
253
|
+
'subagent-delete': async (args) => {
|
|
254
|
+
const name = String((args && args.name) || '').trim();
|
|
255
|
+
if (!validPersonaName(name))
|
|
256
|
+
return { ok: false, error: `人设名不合法: ${name || '(空)'}` };
|
|
257
|
+
await rm(join(dir, name + '.md')).catch(() => { });
|
|
258
|
+
cache = null;
|
|
259
|
+
return { ok: true, name };
|
|
260
|
+
},
|
|
261
|
+
/**
|
|
262
|
+
* 导入人设(.md / .zip):一个 .md 一个人设,文件名(去扩展名)= 人设名;
|
|
263
|
+
* zip 内任意层级的 .md 都按文件名导入(目录层级忽略)。同名**跳过并报告**,不覆盖既有文件。
|
|
264
|
+
* 部分成功:单条失败只记 skipped。
|
|
265
|
+
*/
|
|
266
|
+
'subagent-import': async (args) => {
|
|
267
|
+
const files = args && args.files;
|
|
268
|
+
if (!Array.isArray(files) || !files.length)
|
|
269
|
+
return { ok: false, error: '没有选择要导入的文件' };
|
|
270
|
+
const { entries, problems } = expandUploads(files);
|
|
271
|
+
const planned = planPersonaImport(entries);
|
|
272
|
+
const skipped = [...problems, ...planned.problems];
|
|
273
|
+
const imported = [];
|
|
274
|
+
for (const target of planned.targets) {
|
|
275
|
+
if (!validPersonaName(target.name)) {
|
|
276
|
+
skipped.push({ name: target.name, reason: '人设名不合法(≤64 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头)' });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const text = Buffer.from(target.bytes).toString('utf8').replace(/^\uFEFF/, '');
|
|
280
|
+
if (!text.trim()) {
|
|
281
|
+
skipped.push({ name: target.name, reason: '内容为空,已跳过' });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const targetPath = join(dir, target.name + '.md');
|
|
285
|
+
const exists = await readFile(targetPath, 'utf8').then(() => true).catch(() => false);
|
|
286
|
+
if (exists) {
|
|
287
|
+
skipped.push({ name: target.name, reason: '同名已存在(已跳过)' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await mkdir(dir, { recursive: true });
|
|
292
|
+
await writeFile(targetPath, text, 'utf8');
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
skipped.push({ name: target.name, reason: '写入失败:' + message(e) });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
imported.push(target.name);
|
|
299
|
+
}
|
|
300
|
+
if (imported.length)
|
|
301
|
+
cache = null;
|
|
302
|
+
return { ok: true, imported, skipped };
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
return { list, runSerial, ops, writeOps: new Set(['subagent-create', 'subagent-update', 'subagent-delete', 'subagent-import']) };
|
|
306
|
+
}
|
|
307
|
+
function validPersonaName(name) {
|
|
308
|
+
return name.length > 0 && name.length <= 64 && !name.startsWith('.') && !/[\\/<>:"|?*]/.test(name);
|
|
309
|
+
}
|
|
310
|
+
/** 人设文件序列化:frontmatter 只写用户填过的键;正文 = 人设提示词。 */
|
|
311
|
+
export function serializePersona(args) {
|
|
312
|
+
const description = String((args && args.description) || '').replace(/\r?\n/g, ' ').trim();
|
|
313
|
+
const provider = String((args && args.provider) || '').trim();
|
|
314
|
+
const model = String((args && args.model) || '').trim();
|
|
315
|
+
const toList = (v) => (Array.isArray(v) ? v.map((x) => String(x).trim()).filter(Boolean) : []);
|
|
316
|
+
const tools = toList(args?.tools);
|
|
317
|
+
// 黑名单字段兼容两种入参名:toolsDeny(UI/camel)与 tools_deny(snake)。
|
|
318
|
+
const toolsDeny = toList(args?.toolsDeny ?? args?.tools_deny);
|
|
319
|
+
const body = String((args && args.body) ?? '').trim();
|
|
320
|
+
const lines = ['---'];
|
|
321
|
+
if (description)
|
|
322
|
+
lines.push('description: ' + description);
|
|
323
|
+
if (provider)
|
|
324
|
+
lines.push('provider: ' + provider);
|
|
325
|
+
if (model)
|
|
326
|
+
lines.push('model: ' + model);
|
|
327
|
+
if (tools.length)
|
|
328
|
+
lines.push('tools: ' + tools.join(', '));
|
|
329
|
+
if (toolsDeny.length)
|
|
330
|
+
lines.push('toolsDeny: ' + toolsDeny.join(', '));
|
|
331
|
+
lines.push('---', '', body, '');
|
|
332
|
+
return lines.join('\n');
|
|
333
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function text(v) {
|
|
2
|
+
return [{ type: 'text', text: v }];
|
|
3
|
+
}
|
|
4
|
+
/** 场景绑定校验(设计 §3.3):启用场景的 subagents 并集;空并集 = 全部可用。 */
|
|
5
|
+
export async function filterBySceneBinding(docs, enabledSceneLists) {
|
|
6
|
+
const bound = new Set(enabledSceneLists.flat());
|
|
7
|
+
if (!bound.size)
|
|
8
|
+
return { allowed: docs, reason: null };
|
|
9
|
+
const allowed = docs.filter((d) => bound.has(d.name));
|
|
10
|
+
return { allowed, reason: allowed.length ? null : `当前启用场景的子智能体绑定: ${[...bound].join('、')}` };
|
|
11
|
+
}
|
|
12
|
+
export function defineSubagentListTool(subagents) {
|
|
13
|
+
return {
|
|
14
|
+
name: 'subagent_list',
|
|
15
|
+
description: 'List available personas (pre-configured subagent profiles) with their descriptions. Call before subagent_run.',
|
|
16
|
+
parameters: {
|
|
17
|
+
scene: { type: 'string', description: 'Optional scene name filter (informational; the tool already reflects scene binding).' },
|
|
18
|
+
},
|
|
19
|
+
output: {
|
|
20
|
+
schema: { type: 'string' },
|
|
21
|
+
render: (_a, v) => text(String(v)),
|
|
22
|
+
},
|
|
23
|
+
async execute() {
|
|
24
|
+
const { allowed, reason } = await filterBySceneBinding(await subagents.list(), await subagents.sceneLists());
|
|
25
|
+
const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(无描述)'));
|
|
26
|
+
return '人设子智能体(' + allowed.length + '):\n' + (lines.join('\n') || '(无)') + (reason ? '\n注意:' + reason : '');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function defineSubagentRunTool(subagents) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'subagent_run',
|
|
33
|
+
description: 'Run a named persona as a one-shot subagent: it receives the persona as its own system prompt, works on `task` in a fresh context, and returns only its final output. Discover personas with subagent_list first. Each run is stateless and ephemeral; it does not see this conversation.',
|
|
34
|
+
parameters: {
|
|
35
|
+
agent: { type: 'string', required: true, description: 'Persona name from subagent_list.' },
|
|
36
|
+
task: { type: 'string', required: true, description: 'Complete, self-contained task for the subagent (include all context it needs).' },
|
|
37
|
+
},
|
|
38
|
+
output: {
|
|
39
|
+
schema: { type: 'string' },
|
|
40
|
+
render: (_a, v) => text(String(v)),
|
|
41
|
+
},
|
|
42
|
+
async execute(args, exec) {
|
|
43
|
+
const name = String((args && args.agent) || '').trim();
|
|
44
|
+
const task = String((args && args.task) || '').trim();
|
|
45
|
+
if (!task)
|
|
46
|
+
throw new Error('task 不能为空');
|
|
47
|
+
// 官方 SubagentStartRequest.parent 必填:调用方 agent 缺失时给结构化错误,不把 undefined 透传下去。
|
|
48
|
+
if (!exec || !exec.agent)
|
|
49
|
+
throw new Error('缺少调用方 agent(exec.agent 不可用):无法创建子代理运行');
|
|
50
|
+
const docs = await subagents.list();
|
|
51
|
+
const { allowed, reason } = await filterBySceneBinding(docs, await subagents.sceneLists());
|
|
52
|
+
const persona = allowed.find((p) => p.name === name);
|
|
53
|
+
if (!persona) {
|
|
54
|
+
const available = allowed.map((p) => p.name).join('、') || '(无)';
|
|
55
|
+
throw new Error('人设不可用: ' + name + (reason ? '(' + reason + ')' : '(可用: ' + available + ')'));
|
|
56
|
+
}
|
|
57
|
+
const r = await subagents.runSerial(exec.agent, persona, task, exec.signal);
|
|
58
|
+
const prefix = r.stopReason && r.stopReason !== 'completed' ? `[stopReason: ${r.stopReason}]\n` : '';
|
|
59
|
+
return prefix + r.text;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-tool-management",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH plugin:
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "DSH plugin: one settings panel for MCP servers, Skills, AGENTS.md presets, archived sessions and scene memories — plus scene memory injected into the system prompt and one-shot persona subagents. Settings UI + HTTP API + 14 model tools (skill_mcp_manager_*, skill_manager_*, agentsmd_*, rule_manager_*, subagent_*). Install with one command: dsh plugin --profile web add dsh-plugin-tool-management@latest",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"build": "tsc -p tsconfig.json && node scripts/sync-client.mjs",
|
|
36
36
|
"build:client": "node scripts/sync-client.mjs",
|
|
37
37
|
"prepublishOnly": "npm run build",
|
|
38
|
-
"lint": "node --check lib/client.js && node --check lib/index.js"
|
|
38
|
+
"lint": "node --check lib/client.js && node --check lib/index.js",
|
|
39
|
+
"check:i18n": "node scripts/check-i18n.mjs src/client.js",
|
|
40
|
+
"test": "npm run build && npm run check:i18n && node --test test/archive.test.mjs test/import.test.mjs test/approval-policy.test.mjs test/subagent-scene.test.mjs test/subagent-persona.test.mjs test/hub-layout.test.mjs test/skills-state.test.mjs test/skills-delete.test.mjs test/skills-source-remove.test.mjs test/client-exports.test.mjs test/client-render.test.mjs"
|
|
39
41
|
},
|
|
40
42
|
"keywords": [
|
|
41
43
|
"dsh",
|
|
@@ -66,14 +68,21 @@
|
|
|
66
68
|
"@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2 <0.2.0-0",
|
|
67
69
|
"@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2 <0.2.0-0",
|
|
68
70
|
"@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2 <0.2.0-0",
|
|
71
|
+
"@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2 <0.2.0-0",
|
|
69
72
|
"react": "^18.2.0"
|
|
70
73
|
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"@deepseek-ai/dsh-subagent-spawn-in-process": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
71
79
|
"devDependencies": {
|
|
72
80
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
73
81
|
"@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
|
|
74
82
|
"@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
|
|
75
83
|
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
76
84
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
|
|
77
86
|
"@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
|
|
78
87
|
"@types/node": "^24.0.0",
|
|
79
88
|
"typescript": "^6.0.0"
|
package/screenshots.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
[
|
|
2
|
-
"docs/images/
|
|
3
|
-
"docs/images
|
|
4
|
-
"docs/images
|
|
5
|
-
"docs/images
|
|
6
|
-
"docs/images
|
|
2
|
+
"docs/images/MCP.png",
|
|
3
|
+
"docs/images/技能.png",
|
|
4
|
+
"docs/images/提示词.png",
|
|
5
|
+
"docs/images/会话.png",
|
|
6
|
+
"docs/images/场景.png",
|
|
7
|
+
"docs/images/记忆.png",
|
|
8
|
+
"docs/images/子智能体.png"
|
|
7
9
|
]
|
|
Binary file
|
package/docs/images/history.png
DELETED
|
Binary file
|
package/docs/images/mcp.png
DELETED
|
Binary file
|
package/docs/images/skills.png
DELETED
|
Binary file
|
|
Binary file
|