dsh-plugin-tool-management 0.5.1 → 0.7.0
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 +131 -0
- package/README.md +185 -238
- package/README_EN.md +185 -304
- package/cordis.patch.yml +9 -55
- package/docs/images/1/345/234/272/346/231/257.png +0 -0
- package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
- package/docs/images/2MCP.png +0 -0
- package/docs/images/2MCP_en.png +0 -0
- package/docs/images/3/346/212/200/350/203/275.png +0 -0
- package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
- package/docs/images/6/350/256/260/345/277/206.png +0 -0
- package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
- package/docs/images/7/344/274/232/350/257/235.png +0 -0
- package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
- package/docs/images/8/345/205/274/345/256/271.png +0 -0
- package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
- package/docs/update.md +55 -0
- package/lib/agents-md/preset-id.js +49 -0
- package/lib/agents-md/service.js +180 -57
- package/lib/client.js +4710 -3560
- package/lib/compat/preset-reach.js +425 -0
- package/lib/compat/probe.js +665 -0
- package/lib/history/bridge.js +293 -0
- package/lib/history/workspace.js +498 -52
- package/lib/http-fence.js +94 -0
- package/lib/hub.js +160 -2
- package/lib/index.js +759 -157
- package/lib/mcp/override-blocks.js +195 -0
- package/lib/rules/provider.js +3 -3
- package/lib/rules/service.js +342 -31
- package/lib/scene-prompt-sync.js +112 -0
- package/lib/skills/core.js +123 -35
- package/lib/skills/service.js +9 -1
- package/lib/subagents/service.js +197 -23
- package/lib/subagents/tools.js +8 -2
- package/package.json +6 -3
- package/screenshots.json +10 -9
- package/docs/Changelog.md +0 -517
- 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/subagents/service.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// v0.4 目录变更:人设由 `$DSH_HOME/subagents/` 搬到 `$DSH_HOME/tool-management/agents/`
|
|
6
6
|
// (插件产生的文件统一收在 tool-management/ 下)。旧目录在首次扫描时搬入,见 relocateLegacyPersonas。
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
-
import { mkdir, readdir, readFile, rename,
|
|
8
|
+
import { mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { resolveDshHome } from '../skills/core.js';
|
|
11
|
+
import { listTrashEntries, moveOutOfTrash, moveToTrash, purgeTrashEntry, readTrashEntry } from '../hub.js';
|
|
11
12
|
import { expandUploads, planPersonaImport } from '../imports/upload.js';
|
|
12
13
|
const message = (e) => String((e && e.message) || e);
|
|
13
14
|
/** 人设默认目录:`$DSH_HOME/tool-management/agents/`。 */
|
|
@@ -50,25 +51,66 @@ async function relocateLegacyPersonas(dir) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
/**
|
|
54
|
+
/** 逗号/顿号分隔的名字清单(空串 → undefined)。 */
|
|
55
|
+
function listOf(value) {
|
|
56
|
+
const text = String(value ?? '').trim();
|
|
57
|
+
if (!text)
|
|
58
|
+
return undefined;
|
|
59
|
+
const list = text.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
|
60
|
+
return list.length ? list : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 解析 `toolsByPreset:` 块 —— 每个 Agent 预设一行:
|
|
64
|
+
*
|
|
65
|
+
* ```yaml
|
|
66
|
+
* toolsByPreset:
|
|
67
|
+
* standard: allow: read, write
|
|
68
|
+
* ptc: deny: workflow
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* 缩进即"在块内",回到顶格即块结束(frontmatter 只有这一处嵌套,所以不需要完整
|
|
72
|
+
* YAML 解析器)。模式 id 只做形状校验,认不认得由运行时按当前预设名单判断。
|
|
73
|
+
*/
|
|
74
|
+
export function parsePresetToolRules(frontmatter) {
|
|
75
|
+
const rules = {};
|
|
76
|
+
let inBlock = false;
|
|
77
|
+
for (const raw of frontmatter.split(/\r?\n/)) {
|
|
78
|
+
const line = raw.replace(/\s+#.*$/, '');
|
|
79
|
+
if (!inBlock) {
|
|
80
|
+
if (/^toolsbypreset\s*:\s*$/i.test(line.trim()))
|
|
81
|
+
inBlock = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (line.trim() === '')
|
|
85
|
+
continue;
|
|
86
|
+
if (!/^\s/.test(line))
|
|
87
|
+
break;
|
|
88
|
+
const m = /^\s*([A-Za-z0-9._-]+)\s*:\s*(allow|deny)\s*:\s*(.*)$/i.exec(line);
|
|
89
|
+
if (!m)
|
|
90
|
+
continue;
|
|
91
|
+
const names = listOf(m[3]);
|
|
92
|
+
// 空名单 = 不限制 = 不落盘,读到时也直接忽略,免得把"没开"读成"开了但空"。
|
|
93
|
+
if (!names)
|
|
94
|
+
continue;
|
|
95
|
+
rules[m[1]] = { mode: m[2].toLowerCase() === 'deny' ? 'deny' : 'allow', names };
|
|
96
|
+
}
|
|
97
|
+
return Object.keys(rules).length ? rules : undefined;
|
|
98
|
+
}
|
|
99
|
+
/** 行式 frontmatter 解析:只认 description / provider / model / tools / toolsDeny / toolsByPreset。 */
|
|
54
100
|
export function parsePersona(raw, fallbackName) {
|
|
55
101
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
|
|
56
102
|
const data = {};
|
|
57
103
|
let body = raw;
|
|
104
|
+
let toolsByPreset;
|
|
58
105
|
if (m) {
|
|
59
106
|
for (const line of m[1].split(/\r?\n/)) {
|
|
60
107
|
const kv = /^([A-Za-z_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
61
108
|
if (kv)
|
|
62
109
|
data[kv[1].toLowerCase()] = kv[2].trim();
|
|
63
110
|
}
|
|
111
|
+
toolsByPreset = parsePresetToolRules(m[1]);
|
|
64
112
|
body = raw.slice(m[0].length);
|
|
65
113
|
}
|
|
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
114
|
const tools = listOf(data.tools);
|
|
73
115
|
// 兼容两种写法:`toolsdeny: a, b`(线上格式)与 `toolsDeny: a, b`(键名统一小写后同形)。
|
|
74
116
|
const toolsDeny = listOf(data.toolsdeny);
|
|
@@ -80,6 +122,7 @@ export function parsePersona(raw, fallbackName) {
|
|
|
80
122
|
model: data.model || undefined,
|
|
81
123
|
tools,
|
|
82
124
|
toolsDeny,
|
|
125
|
+
...(toolsByPreset === undefined ? {} : { toolsByPreset }),
|
|
83
126
|
body: body.trim(),
|
|
84
127
|
path: '',
|
|
85
128
|
};
|
|
@@ -155,8 +198,14 @@ export function createSubagentService(ctx, opts) {
|
|
|
155
198
|
throw new Error('spawn provider 挂载后仍未出现在 ctx.subagents.list()(宿主版本不匹配?)');
|
|
156
199
|
return runtime;
|
|
157
200
|
}
|
|
158
|
-
async function runOnce(parentAgent, p, task, signal) {
|
|
201
|
+
async function runOnce(parentAgent, p, task, signal, toolFilter) {
|
|
159
202
|
const runtime = await ensureSpawnProvider();
|
|
203
|
+
// 工具限制三态:对象 = 调用方已按当前预设决定;null = 调用方明确决定不加限制;
|
|
204
|
+
// undefined = 调用方没决定(如别的入口直接调 runSerial)→ 回落人设里的旧格式全局名单。
|
|
205
|
+
const legacyFilter = (p.tools?.length || p.toolsDeny?.length)
|
|
206
|
+
? { ...(p.tools?.length ? { allow: p.tools } : {}), ...(p.toolsDeny?.length ? { deny: p.toolsDeny } : {}) }
|
|
207
|
+
: null;
|
|
208
|
+
const resolved = toolFilter === undefined ? legacyFilter : toolFilter;
|
|
160
209
|
// 官方签名:start(name, request) —— name = ctx.subagents 上的 provider 注册名。
|
|
161
210
|
const run = await runtime.start('spawn', {
|
|
162
211
|
label: p.name,
|
|
@@ -166,10 +215,8 @@ export function createSubagentService(ctx, opts) {
|
|
|
166
215
|
prompt: [{ type: 'text', text: task }],
|
|
167
216
|
persona: p.body,
|
|
168
217
|
// 工具白/黑名单 → 官方 ToolRestriction(`deny` 优先级高于 `allow`;未知名官方会直接拒绝启动,
|
|
169
|
-
//
|
|
170
|
-
...(
|
|
171
|
-
? { toolFilter: { ...(p.tools?.length ? { allow: p.tools } : {}), ...(p.toolsDeny?.length ? { deny: p.toolsDeny } : {}) } }
|
|
172
|
-
: {}),
|
|
218
|
+
// 所以名单由 index.ts 按当前预设 + 当前真实存在的工具名算好再传进来,见 decideToolFilter)。
|
|
219
|
+
...(resolved === null ? {} : { toolFilter: resolved }),
|
|
173
220
|
maxDepth: 1,
|
|
174
221
|
// provider 与 model 是模型路由的两半:DSH 的 resolveModel(provider, model) 不做
|
|
175
222
|
// `provider/model` 字符串拆分,只改 model 会落在**主会话的 provider** 上——跨来源
|
|
@@ -197,8 +244,8 @@ export function createSubagentService(ctx, opts) {
|
|
|
197
244
|
}
|
|
198
245
|
// v1 串行:同一时刻至多一个子代理运行(设计 §3.2)。
|
|
199
246
|
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));
|
|
247
|
+
const runSerial = (parentAgent, p, task, signal, toolFilter) => {
|
|
248
|
+
const queued = chain.then(() => runOnce(parentAgent, p, task, signal, toolFilter), () => runOnce(parentAgent, p, task, signal, toolFilter));
|
|
202
249
|
chain = queued.catch(() => undefined);
|
|
203
250
|
return queued;
|
|
204
251
|
};
|
|
@@ -207,7 +254,7 @@ export function createSubagentService(ctx, opts) {
|
|
|
207
254
|
const docs = await list();
|
|
208
255
|
return {
|
|
209
256
|
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 })),
|
|
257
|
+
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, toolsByPreset: p.toolsByPreset ?? null })),
|
|
211
258
|
};
|
|
212
259
|
},
|
|
213
260
|
'subagent-get': async (args) => {
|
|
@@ -216,7 +263,7 @@ export function createSubagentService(ctx, opts) {
|
|
|
216
263
|
const p = docs.find((d) => d.name === name);
|
|
217
264
|
if (!p)
|
|
218
265
|
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 } };
|
|
266
|
+
return { ok: true, persona: { name: p.name, description: p.description, provider: p.provider ?? '', model: p.model ?? '', tools: p.tools ?? [], toolsDeny: p.toolsDeny ?? [], toolsByPreset: p.toolsByPreset ?? {}, body: p.body } };
|
|
220
267
|
},
|
|
221
268
|
'subagent-create': async (args) => {
|
|
222
269
|
const name = String((args && args.name) || '').trim();
|
|
@@ -250,13 +297,55 @@ export function createSubagentService(ctx, opts) {
|
|
|
250
297
|
cache = null;
|
|
251
298
|
return { ok: true, name };
|
|
252
299
|
},
|
|
300
|
+
/**
|
|
301
|
+
* 删除人设 = **移入回收站**(`hub/trash/agents-trash/<id>/persona.md`)。
|
|
302
|
+
* 早先这里是 `rm`,而界面按钮写着「移入回收站」——文案与行为不一致;现在行为追上文案,
|
|
303
|
+
* 误删可以从子智能体页的回收站里恢复。
|
|
304
|
+
*/
|
|
253
305
|
'subagent-delete': async (args) => {
|
|
254
306
|
const name = String((args && args.name) || '').trim();
|
|
255
307
|
if (!validPersonaName(name))
|
|
256
308
|
return { ok: false, error: `人设名不合法: ${name || '(空)'}` };
|
|
257
|
-
|
|
309
|
+
const target = join(dir, name + '.md');
|
|
310
|
+
const exists = await readFile(target, 'utf8').then(() => true).catch(() => false);
|
|
311
|
+
if (!exists)
|
|
312
|
+
return { ok: false, error: `人设不存在: ${name}` };
|
|
313
|
+
const moved = await moveToTrash('agents', name, [{ from: target, dest: 'persona.md' }]);
|
|
314
|
+
if (moved.ok === false)
|
|
315
|
+
return { ok: false, error: `移入回收站失败: ${moved.error}` };
|
|
258
316
|
cache = null;
|
|
259
|
-
return { ok: true, name };
|
|
317
|
+
return { ok: true, name, trashId: moved.id };
|
|
318
|
+
},
|
|
319
|
+
'subagent-trash-list': async () => ({ ok: true, trash: await listTrashEntries('agents') }),
|
|
320
|
+
'subagent-trash-restore': async (args) => {
|
|
321
|
+
const id = String((args && args.id) || '').trim();
|
|
322
|
+
const entry = await readTrashEntry('agents', id);
|
|
323
|
+
if (!entry)
|
|
324
|
+
return { ok: false, error: `回收站条目不存在: ${id}` };
|
|
325
|
+
if (!validPersonaName(entry.name))
|
|
326
|
+
return { ok: false, error: `回收站里的人设名不合法: ${entry.name}` };
|
|
327
|
+
const target = join(dir, entry.name + '.md');
|
|
328
|
+
const exists = await readFile(target, 'utf8').then(() => true).catch(() => false);
|
|
329
|
+
// 绝不覆盖:同名人设已经存在时如实拒绝,让人自己决定怎么办。
|
|
330
|
+
if (exists)
|
|
331
|
+
return { ok: false, error: `无法恢复,同名人设已存在: ${entry.name}` };
|
|
332
|
+
try {
|
|
333
|
+
await mkdir(dir, { recursive: true });
|
|
334
|
+
await moveOutOfTrash('agents', id, 'persona.md', target);
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
return { ok: false, error: `恢复失败: ${message(e)}` };
|
|
338
|
+
}
|
|
339
|
+
await purgeTrashEntry('agents', id);
|
|
340
|
+
cache = null;
|
|
341
|
+
return { ok: true, name: entry.name };
|
|
342
|
+
},
|
|
343
|
+
'subagent-trash-delete': async (args) => {
|
|
344
|
+
const id = String((args && args.id) || '').trim();
|
|
345
|
+
const gone = await purgeTrashEntry('agents', id);
|
|
346
|
+
if (!gone)
|
|
347
|
+
return { ok: false, error: `回收站条目不存在: ${id}` };
|
|
348
|
+
return { ok: true, id };
|
|
260
349
|
},
|
|
261
350
|
/**
|
|
262
351
|
* 导入人设(.md / .zip):一个 .md 一个人设,文件名(去扩展名)= 人设名;
|
|
@@ -302,20 +391,46 @@ export function createSubagentService(ctx, opts) {
|
|
|
302
391
|
return { ok: true, imported, skipped };
|
|
303
392
|
},
|
|
304
393
|
};
|
|
305
|
-
return { list, runSerial, ops, writeOps: new Set(['subagent-create', 'subagent-update', 'subagent-delete', 'subagent-import']) };
|
|
394
|
+
return { list, runSerial, ops, writeOps: new Set(['subagent-create', 'subagent-update', 'subagent-delete', 'subagent-import', 'subagent-trash-restore', 'subagent-trash-delete']) };
|
|
306
395
|
}
|
|
307
396
|
function validPersonaName(name) {
|
|
308
397
|
return name.length > 0 && name.length <= 64 && !name.startsWith('.') && !/[\\/<>:"|?*]/.test(name);
|
|
309
398
|
}
|
|
399
|
+
/** 字符串数组规范化(非数组/空项都丢掉)。 */
|
|
400
|
+
function toStringList(value) {
|
|
401
|
+
return Array.isArray(value) ? value.map((x) => String(x).trim()).filter(Boolean) : [];
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 规范化前端传来的 `toolsByPreset`:丢掉空名单(空 = 不限制 = 不落盘),
|
|
405
|
+
* 模式 id 只收形状合法的(预设 id 允许字母数字点横线)。
|
|
406
|
+
*/
|
|
407
|
+
export function presetRulesOf(value) {
|
|
408
|
+
if (value === null || typeof value !== 'object')
|
|
409
|
+
return [];
|
|
410
|
+
const out = [];
|
|
411
|
+
for (const [rawId, rawRule] of Object.entries(value)) {
|
|
412
|
+
const id = String(rawId).trim();
|
|
413
|
+
if (!id || !/^[A-Za-z0-9._-]+$/.test(id))
|
|
414
|
+
continue;
|
|
415
|
+
if (rawRule === null || typeof rawRule !== 'object')
|
|
416
|
+
continue;
|
|
417
|
+
const rec = rawRule;
|
|
418
|
+
const names = toStringList(rec.names);
|
|
419
|
+
if (!names.length)
|
|
420
|
+
continue;
|
|
421
|
+
out.push([id, { mode: String(rec.mode) === 'deny' ? 'deny' : 'allow', names }]);
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
310
425
|
/** 人设文件序列化:frontmatter 只写用户填过的键;正文 = 人设提示词。 */
|
|
311
426
|
export function serializePersona(args) {
|
|
312
427
|
const description = String((args && args.description) || '').replace(/\r?\n/g, ' ').trim();
|
|
313
428
|
const provider = String((args && args.provider) || '').trim();
|
|
314
429
|
const model = String((args && args.model) || '').trim();
|
|
315
|
-
const
|
|
316
|
-
const tools = toList(args?.tools);
|
|
430
|
+
const tools = toStringList(args?.tools);
|
|
317
431
|
// 黑名单字段兼容两种入参名:toolsDeny(UI/camel)与 tools_deny(snake)。
|
|
318
|
-
const toolsDeny =
|
|
432
|
+
const toolsDeny = toStringList(args?.toolsDeny ?? args?.tools_deny);
|
|
433
|
+
const byPreset = presetRulesOf(args?.toolsByPreset);
|
|
319
434
|
const body = String((args && args.body) ?? '').trim();
|
|
320
435
|
const lines = ['---'];
|
|
321
436
|
if (description)
|
|
@@ -328,6 +443,65 @@ export function serializePersona(args) {
|
|
|
328
443
|
lines.push('tools: ' + tools.join(', '));
|
|
329
444
|
if (toolsDeny.length)
|
|
330
445
|
lines.push('toolsDeny: ' + toolsDeny.join(', '));
|
|
446
|
+
if (byPreset.length) {
|
|
447
|
+
lines.push('toolsByPreset:');
|
|
448
|
+
for (const [id, rule] of byPreset)
|
|
449
|
+
lines.push(' ' + id + ': ' + rule.mode + ': ' + rule.names.join(', '));
|
|
450
|
+
}
|
|
331
451
|
lines.push('---', '', body, '');
|
|
332
452
|
return lines.join('\n');
|
|
333
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* 按**当前会话的 Agent 预设**决定这次委派下发什么工具限制。
|
|
456
|
+
*
|
|
457
|
+
* 为什么必须按预设分:子代理跑在父会话的预设里(官方 `composeFrom(childCtx, parent.ctx)`),
|
|
458
|
+
* 而各预设的工具集合差别极大(极简模式只有持久 shell),官方 `tools.restrict()` 遇到
|
|
459
|
+
* 名单里不存在的工具名会直接抛错、子代理根本起不来。所以:
|
|
460
|
+
*
|
|
461
|
+
* - 当前预设配了名单(且名单非空)→ 用它;白名单额外并入**当时真实在跑的 MCP 工具**
|
|
462
|
+
* (官方 allow 是"清单之外全砍",不并进来会把 MCP 一起砍掉;用户裁定:子代理要能
|
|
463
|
+
* 用当前启动的 MCP);
|
|
464
|
+
* - 当前预设没配 → 回落旧的全局 `tools` / `toolsDeny`(老文件行为不变);
|
|
465
|
+
* - 名单里有已经消失的工具名 → **丢掉并在 note 里如实说明**(不接受静默失效);
|
|
466
|
+
* - 判断不了当前预设(老宿主 / 异常)→ 不按模式施加,并在 note 里说明。
|
|
467
|
+
*
|
|
468
|
+
* 三态返回:`filter: null` = 明确不加限制;`filter: {...}` = 下发该限制。
|
|
469
|
+
*/
|
|
470
|
+
export function decideToolFilter(persona, presetId, known) {
|
|
471
|
+
const rule = presetId === null ? undefined : persona.toolsByPreset?.[presetId];
|
|
472
|
+
if (rule && rule.names.length) {
|
|
473
|
+
const usable = rule.names.filter((name) => known.names.has(name));
|
|
474
|
+
const dropped = rule.names.filter((name) => !known.names.has(name));
|
|
475
|
+
const droppedNote = dropped.length ? `名单里这些工具当前不存在,已忽略:${dropped.join('、')}` : undefined;
|
|
476
|
+
if (!usable.length) {
|
|
477
|
+
return { filter: null, note: `「${presetId}」的${rule.mode === 'allow' ? '白' : '黑'}名单里没有当前存在的工具,本次不施加工具限制` };
|
|
478
|
+
}
|
|
479
|
+
if (rule.mode === 'allow')
|
|
480
|
+
return { filter: { allow: [...new Set([...usable, ...known.mcp])] }, ...(droppedNote === undefined ? {} : { note: droppedNote }) };
|
|
481
|
+
return { filter: { deny: usable }, ...(droppedNote === undefined ? {} : { note: droppedNote }) };
|
|
482
|
+
}
|
|
483
|
+
// 旧格式(全局名单):保持老行为,白名单同样并入 MCP。
|
|
484
|
+
const legacyAllow = (persona.tools || []).filter((name) => known.names.has(name));
|
|
485
|
+
const legacyDeny = (persona.toolsDeny || []).filter((name) => known.names.has(name));
|
|
486
|
+
const legacyDropped = [...(persona.tools || []), ...(persona.toolsDeny || [])].filter((name) => !known.names.has(name));
|
|
487
|
+
const filter = {
|
|
488
|
+
...(legacyAllow.length ? { allow: [...new Set([...legacyAllow, ...known.mcp])] } : {}),
|
|
489
|
+
...(legacyDeny.length ? { deny: legacyDeny } : {}),
|
|
490
|
+
};
|
|
491
|
+
const notes = [];
|
|
492
|
+
const hasLegacy = Boolean((persona.tools || []).length || (persona.toolsDeny || []).length);
|
|
493
|
+
if (presetId === null) {
|
|
494
|
+
// 说清这一刻到底靠什么在跑:有旧格式就照旧格式,没有就明说"按模式配的限制这次不生效"。
|
|
495
|
+
notes.push(hasLegacy
|
|
496
|
+
? '没能判断当前会话的 Agent 预设,按旧格式的全局名单执行'
|
|
497
|
+
: '没能判断当前会话的 Agent 预设,按模式配的限制这次不生效');
|
|
498
|
+
}
|
|
499
|
+
if (legacyDropped.length)
|
|
500
|
+
notes.push(`名单里这些工具当前不存在,已忽略:${legacyDropped.join('、')}`);
|
|
501
|
+
if (!Object.keys(filter).length) {
|
|
502
|
+
if (presetId === null && !notes.length)
|
|
503
|
+
notes.push('没能判断当前会话的 Agent 预设,本次不施加工具限制');
|
|
504
|
+
return { filter: null, ...(notes.length ? { note: notes.join(';') } : {}) };
|
|
505
|
+
}
|
|
506
|
+
return { filter, ...(notes.length ? { note: notes.join(';') } : {}) };
|
|
507
|
+
}
|
package/lib/subagents/tools.js
CHANGED
|
@@ -54,9 +54,15 @@ export function defineSubagentRunTool(subagents) {
|
|
|
54
54
|
const available = allowed.map((p) => p.name).join('、') || '(无)';
|
|
55
55
|
throw new Error('人设不可用: ' + name + (reason ? '(' + reason + ')' : '(可用: ' + available + ')'));
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
// 按当前会话的 Agent 预设算工具限制:判断不了当前预设、或名单里的工具已经不存在时,
|
|
58
|
+
// 结论里会带一句实话,跟着结果一起返回——不静默改变限制的强度。
|
|
59
|
+
const decision = typeof subagents.toolFilterFor === 'function'
|
|
60
|
+
? await subagents.toolFilterFor(persona, exec.agent && exec.agent.ctx)
|
|
61
|
+
: undefined;
|
|
62
|
+
const r = await subagents.runSerial(exec.agent, persona, task, exec.signal, decision === undefined ? undefined : decision.filter);
|
|
58
63
|
const prefix = r.stopReason && r.stopReason !== 'completed' ? `[stopReason: ${r.stopReason}]\n` : '';
|
|
59
|
-
|
|
64
|
+
const note = decision && decision.note ? `⚠ ${decision.note}\n\n` : '';
|
|
65
|
+
return note + prefix + r.text;
|
|
60
66
|
},
|
|
61
67
|
};
|
|
62
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-tool-management",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"cordis.patch.yml",
|
|
21
21
|
"README.md",
|
|
22
22
|
"README_EN.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
23
24
|
"LICENSE",
|
|
24
25
|
"screenshots.json",
|
|
25
26
|
"docs"
|
|
@@ -37,7 +38,10 @@
|
|
|
37
38
|
"prepublishOnly": "npm run build",
|
|
38
39
|
"lint": "node --check lib/client.js && node --check lib/index.js",
|
|
39
40
|
"check:i18n": "node scripts/check-i18n.mjs src/client.js",
|
|
40
|
-
"
|
|
41
|
+
"check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
|
|
42
|
+
"host-deps": "node scripts/host-deps.mjs --fix",
|
|
43
|
+
"doctor": "node scripts/doctor.mjs",
|
|
44
|
+
"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 test/compat-probe.test.mjs test/compat-fallback.test.mjs test/preset-reach.test.mjs test/rules-toggle-args.test.mjs"
|
|
41
45
|
},
|
|
42
46
|
"keywords": [
|
|
43
47
|
"dsh",
|
|
@@ -56,7 +60,6 @@
|
|
|
56
60
|
"client": {
|
|
57
61
|
"platform": "web",
|
|
58
62
|
"inject": [
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
60
63
|
"@deepseek-ai/dsh-client-locale"
|
|
61
64
|
]
|
|
62
65
|
}
|
package/screenshots.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
[
|
|
2
|
-
"docs/images/
|
|
3
|
-
"docs/images
|
|
4
|
-
"docs/images
|
|
5
|
-
"docs/images
|
|
6
|
-
"docs/images
|
|
7
|
-
"docs/images
|
|
8
|
-
"docs/images
|
|
9
|
-
|
|
1
|
+
[
|
|
2
|
+
"docs/images/1场景.png",
|
|
3
|
+
"docs/images/2MCP.png",
|
|
4
|
+
"docs/images/3技能.png",
|
|
5
|
+
"docs/images/4子智能体.png",
|
|
6
|
+
"docs/images/5提示词.png",
|
|
7
|
+
"docs/images/6记忆.png",
|
|
8
|
+
"docs/images/7会话.png",
|
|
9
|
+
"docs/images/8兼容.png"
|
|
10
|
+
]
|