dsh-plugin-tool-management 0.7.0 → 0.8.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +16 -5
  3. package/README_EN.md +16 -5
  4. package/docs/images/1/345/234/272/346/231/257.png +0 -0
  5. package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
  6. package/docs/images/2MCP.png +0 -0
  7. package/docs/images/2MCP_en.png +0 -0
  8. package/docs/images/3/346/212/200/350/203/275.png +0 -0
  9. package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
  10. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  11. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
  12. package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
  13. package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
  14. package/docs/images/6/350/256/260/345/277/206.png +0 -0
  15. package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
  16. package/docs/images/7/344/274/232/350/257/235.png +0 -0
  17. package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
  18. package/docs/images/8/345/205/274/345/256/271.png +0 -0
  19. package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
  20. package/docs/update.md +17 -0
  21. package/lib/agents-md/service.js +41 -3
  22. package/lib/client.js +820 -375
  23. package/lib/compat/preset-reach.js +1 -1
  24. package/lib/compat/probe.js +8 -0
  25. package/lib/index.js +433 -10
  26. package/lib/mcp/state-section.js +90 -0
  27. package/lib/prompt-sections.js +148 -0
  28. package/lib/rules/archive-engine.js +99 -29
  29. package/lib/rules/archive.js +56 -28
  30. package/lib/rules/service.js +259 -39
  31. package/lib/skills/core.js +4 -1
  32. package/lib/subagents/catalog.js +89 -0
  33. package/lib/subagents/service.js +189 -20
  34. package/lib/subagents/tools.js +10 -3
  35. package/package.json +97 -97
  36. package/lib/rules/provider.js +0 -121
@@ -130,26 +130,98 @@ export function parsePersona(raw, fallbackName) {
130
130
  const RESULT_MAX = 16 * 1024;
131
131
  export function createSubagentService(ctx, opts) {
132
132
  const dir = opts?.subagentsDir || defaultPersonasDir();
133
+ const stateDir = opts?.stateDir && opts.stateDir.trim() !== '' ? opts.stateDir : join(resolveDshHome(), 'tool-management');
134
+ const stateFile = join(stateDir, 'agents-index.json');
133
135
  const req = createRequire(import.meta.url);
134
136
  let cache = null;
135
- async function list() {
136
- if (cache && Date.now() - cache.at < 1000)
137
- return cache.value;
138
- // 旧目录搬家放在首次扫描前(幂等;失败不阻断,旧目录仍会被下面的读取兜底看到)。
139
- await relocateLegacyPersonas(dir).catch(() => undefined);
140
- const docs = [];
137
+ // ── 人设启用集合(子智能体开关)────────────────────────────────────────
138
+ // agents-index.json:{ version: 1, enabled: string[] }(与 rules-index.json 同目录约定)。
139
+ // - 文件缺失/损坏 = **全部启用**(老用户升级零感知,行为与开关上线前一致);
140
+ // - 文件一旦写出即为权威:之后新建/导入/回收站恢复的人设**自动启用**(刚建就想用是常理);
141
+ // - 停用只影响注入与 subagent_* 工具的可见性,人设文件一个字节不动。
142
+ // 缓存约定:undefined = 还没读过盘;null = 文件缺失(全部启用);数组 = 权威集合。
143
+ let enabledCache = undefined;
144
+ async function readEnabled() {
145
+ if (enabledCache !== undefined)
146
+ return enabledCache;
147
+ // 用局部变量过渡:await 之后 TS 对闭包级缓存变量的收窄会失效,直接返回会报 undefined。
148
+ let next;
141
149
  try {
142
- const entries = await readdir(dir);
143
- for (const fileName of entries.filter((e) => e.endsWith('.md')).sort()) {
144
- const raw = await readFile(join(dir, fileName), 'utf8').catch(() => '');
145
- if (!raw.trim())
146
- continue;
147
- docs.push({ ...parsePersona(raw, fileName.slice(0, -3)), path: join(dir, fileName) });
150
+ const raw = JSON.parse(await readFile(stateFile, 'utf8'));
151
+ next = Array.isArray(raw && raw.enabled) ? raw.enabled.map((x) => String(x)) : [];
152
+ }
153
+ catch {
154
+ next = null;
155
+ }
156
+ enabledCache = next;
157
+ return next;
158
+ }
159
+ async function writeEnabled(list) {
160
+ await mkdir(stateDir, { recursive: true });
161
+ await writeFile(stateFile, JSON.stringify({ version: 1, enabled: list }, null, 2), 'utf8');
162
+ enabledCache = list;
163
+ }
164
+ /** 新建/导入/恢复的人设默认启用;文件缺失(本来就全启用)时无需写。 */
165
+ async function enablePersonaNames(names) {
166
+ const set = await readEnabled();
167
+ if (set === null)
168
+ return;
169
+ let dirty = false;
170
+ for (const n of names)
171
+ if (set.indexOf(n) < 0) {
172
+ set.push(n);
173
+ dirty = true;
148
174
  }
175
+ if (dirty)
176
+ await writeEnabled(set);
177
+ }
178
+ /** 改名跟随:旧名在集合里就改新名;不在(停用中)保持停用。 */
179
+ async function renamePersonaInEnabled(from, to) {
180
+ const set = await readEnabled();
181
+ if (set === null)
182
+ return;
183
+ const i = set.indexOf(from);
184
+ if (i < 0)
185
+ return;
186
+ set[i] = to;
187
+ await writeEnabled(set);
188
+ }
189
+ /** 删除后从集合摘掉,别留悬空名(列表时无害,但会让集合越攒越脏)。 */
190
+ async function removePersonaFromEnabled(name) {
191
+ const set = await readEnabled();
192
+ if (set === null || set.indexOf(name) < 0)
193
+ return;
194
+ await writeEnabled(set.filter((n) => n !== name));
195
+ }
196
+ /** list() 输出统一附上 enabled:文件缺失 → 全 true;否则按集合。 */
197
+ function withEnabled(docs, set) {
198
+ if (set === null)
199
+ return docs.map((d) => ({ ...d, enabled: true }));
200
+ return docs.map((d) => ({ ...d, enabled: set.indexOf(d.name) >= 0 }));
201
+ }
202
+ async function list() {
203
+ let docs;
204
+ if (cache && Date.now() - cache.at < 1000) {
205
+ docs = cache.value;
149
206
  }
150
- catch { /* 目录缺失 = 无人设 */ }
151
- cache = { at: Date.now(), value: docs };
152
- return docs;
207
+ else {
208
+ // 旧目录搬家放在首次扫描前(幂等;失败不阻断,旧目录仍会被下面的读取兜底看到)。
209
+ await relocateLegacyPersonas(dir).catch(() => undefined);
210
+ const fresh = [];
211
+ try {
212
+ const entries = await readdir(dir);
213
+ for (const fileName of entries.filter((e) => e.endsWith('.md')).sort()) {
214
+ const raw = await readFile(join(dir, fileName), 'utf8').catch(() => '');
215
+ if (!raw.trim())
216
+ continue;
217
+ fresh.push({ ...parsePersona(raw, fileName.slice(0, -3)), path: join(dir, fileName) });
218
+ }
219
+ }
220
+ catch { /* 目录缺失 = 无人设 */ }
221
+ cache = { at: Date.now(), value: fresh };
222
+ docs = fresh;
223
+ }
224
+ return withEnabled(docs, await readEnabled());
153
225
  }
154
226
  // spawn provider 在场性(设计 §3.2,落地方式见 cordis.patch.yml 的取舍注释):
155
227
  // 官方通道探测(ctx.subagents.list())→ 缺失才挂载 → 失败把原因原样带出(不吞、不谎报)。
@@ -254,7 +326,7 @@ export function createSubagentService(ctx, opts) {
254
326
  const docs = await list();
255
327
  return {
256
328
  ok: true,
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 })),
329
+ subagents: docs.map((p) => ({ name: p.name, enabled: p.enabled !== false, description: p.description, provider: p.provider ?? null, model: p.model ?? null, tools: p.tools ?? null, toolsDeny: p.toolsDeny ?? null, toolsByPreset: p.toolsByPreset ?? null })),
258
330
  };
259
331
  },
260
332
  'subagent-get': async (args) => {
@@ -282,9 +354,17 @@ export function createSubagentService(ctx, opts) {
282
354
  return { ok: false, error: `创建人设目录失败: ${dir}(${message(e)})` };
283
355
  }
284
356
  await writeFile(target, serializePersona(args), 'utf8');
357
+ // 新建即启用(文件缺失 = 本来全启用,无需写);缓存失效让下次 list 带上新成员。
358
+ await enablePersonaNames([name]).catch(() => undefined);
285
359
  cache = null;
286
360
  return { ok: true, name };
287
361
  },
362
+ /**
363
+ * 保存人设,**可选改名**(nextName)。
364
+ * 人设名就是文件名(`agents/<名>.md`),所以改名 = 重命名文件;
365
+ * 目标名已存在直接拒绝,绝不覆盖别人的文件。
366
+ * 返回 `renamedFrom` 供调用方把场景档案里的绑定一起改掉(档案存的是人设名)。
367
+ */
288
368
  'subagent-update': async (args) => {
289
369
  const name = String((args && args.name) || '').trim();
290
370
  if (!validPersonaName(name))
@@ -293,9 +373,31 @@ export function createSubagentService(ctx, opts) {
293
373
  const exists = await readFile(target, 'utf8').then(() => true).catch(() => false);
294
374
  if (!exists)
295
375
  return { ok: false, error: `人设不存在: ${name}` };
296
- await writeFile(target, serializePersona(args), 'utf8');
376
+ let finalName = name;
377
+ let renamedFrom;
378
+ const nextRaw = args && args.nextName !== undefined ? String(args.nextName).trim() : '';
379
+ if (nextRaw !== '' && nextRaw !== name) {
380
+ if (!validPersonaName(nextRaw)) {
381
+ return { ok: false, error: `新人设名不合法: ${nextRaw}(非空、≤64 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头)` };
382
+ }
383
+ const nextTarget = join(dir, nextRaw + '.md');
384
+ const taken = await readFile(nextTarget, 'utf8').then(() => true).catch(() => false);
385
+ if (taken)
386
+ return { ok: false, error: `人设已存在: ${nextRaw}` };
387
+ try {
388
+ await rename(target, nextTarget);
389
+ }
390
+ catch (e) {
391
+ return { ok: false, error: `人设改名失败: ${message(e)}` };
392
+ }
393
+ finalName = nextRaw;
394
+ renamedFrom = name;
395
+ }
396
+ await writeFile(join(dir, finalName + '.md'), serializePersona({ ...args, name: finalName }), 'utf8');
397
+ if (renamedFrom)
398
+ await renamePersonaInEnabled(renamedFrom, finalName).catch(() => undefined);
297
399
  cache = null;
298
- return { ok: true, name };
400
+ return { ok: true, name: finalName, ...(renamedFrom ? { renamedFrom } : {}) };
299
401
  },
300
402
  /**
301
403
  * 删除人设 = **移入回收站**(`hub/trash/agents-trash/<id>/persona.md`)。
@@ -313,6 +415,7 @@ export function createSubagentService(ctx, opts) {
313
415
  const moved = await moveToTrash('agents', name, [{ from: target, dest: 'persona.md' }]);
314
416
  if (moved.ok === false)
315
417
  return { ok: false, error: `移入回收站失败: ${moved.error}` };
418
+ await removePersonaFromEnabled(name).catch(() => undefined);
316
419
  cache = null;
317
420
  return { ok: true, name, trashId: moved.id };
318
421
  },
@@ -337,6 +440,8 @@ export function createSubagentService(ctx, opts) {
337
440
  return { ok: false, error: `恢复失败: ${message(e)}` };
338
441
  }
339
442
  await purgeTrashEntry('agents', id);
443
+ // 恢复 = 拿回来用:自动启用(文件缺失本来就全启用,无需写)。
444
+ await enablePersonaNames([entry.name]).catch(() => undefined);
340
445
  cache = null;
341
446
  return { ok: true, name: entry.name };
342
447
  },
@@ -386,12 +491,76 @@ export function createSubagentService(ctx, opts) {
386
491
  }
387
492
  imported.push(target.name);
388
493
  }
389
- if (imported.length)
494
+ if (imported.length) {
495
+ await enablePersonaNames(imported).catch(() => undefined);
390
496
  cache = null;
497
+ }
391
498
  return { ok: true, imported, skipped };
392
499
  },
500
+ /**
501
+ * 子智能体开关(v0.8):停用 = 不注入目录段、subagent_list/run 不可见;文件本体不动。
502
+ * `enabled` 必须显式给布尔值 —— 与 rules-toggle 同一条口径:不按"翻转"推断,
503
+ * 免得参数丢了时界面以为改了、实际什么都没动。
504
+ */
505
+ 'subagent-toggle': async (args) => {
506
+ const name = String((args && args.name) || '').trim();
507
+ if (!validPersonaName(name))
508
+ return { ok: false, error: `人设名不合法: ${name || '(空)'}` };
509
+ if (typeof (args && args.enabled) !== 'boolean') {
510
+ return { ok: false, error: '缺少参数:enabled 必须是布尔值(只按传入值写入,不做"翻转"推断)' };
511
+ }
512
+ const docs = await list();
513
+ if (!docs.some((d) => d.name === name))
514
+ return { ok: false, error: `人设不存在: ${name}` };
515
+ const set = await readEnabled();
516
+ // 文件缺失 = 当前全启用:首次开关时把现状物化成显式集合(否则"部分停用"无处落笔)。
517
+ const base = (set === null ? docs.map((d) => d.name) : set.slice()).filter((n) => docs.some((d) => d.name === n));
518
+ const i = base.indexOf(name);
519
+ if (args.enabled === true) {
520
+ if (i < 0)
521
+ base.push(name);
522
+ }
523
+ else if (i >= 0) {
524
+ base.splice(i, 1);
525
+ }
526
+ await writeEnabled(base);
527
+ cache = null;
528
+ return { ok: true, name, enabled: args.enabled === true };
529
+ },
530
+ };
531
+ const enabledStore = {
532
+ /** 指定名单里当前被停用的(进入模式拍快照用:只记将被启用的行,退出时精确停回)。 */
533
+ async disabledAmong(names) {
534
+ const docs = await list();
535
+ const state = new Map(docs.map((d) => [d.name, d.enabled !== false]));
536
+ return names.filter((n) => state.get(n) === false);
537
+ },
538
+ /** 批量启停;只碰给出的名字,人设已不存在的跳过(别把悬空名写进集合)。 */
539
+ async setEnabled(names, enabled) {
540
+ const docs = await list();
541
+ const set = await readEnabled();
542
+ const base = (set === null ? docs.map((d) => d.name) : set.slice()).filter((n) => docs.some((d) => d.name === n));
543
+ let dirty = false;
544
+ for (const n of names) {
545
+ if (!docs.some((d) => d.name === n))
546
+ continue;
547
+ const i = base.indexOf(n);
548
+ if (enabled && i < 0) {
549
+ base.push(n);
550
+ dirty = true;
551
+ }
552
+ if (!enabled && i >= 0) {
553
+ base.splice(i, 1);
554
+ dirty = true;
555
+ }
556
+ }
557
+ if (dirty) {
558
+ await writeEnabled(base);
559
+ cache = null;
560
+ }
561
+ },
393
562
  };
394
- return { list, runSerial, ops, writeOps: new Set(['subagent-create', 'subagent-update', 'subagent-delete', 'subagent-import', 'subagent-trash-restore', 'subagent-trash-delete']) };
563
+ return { list, runSerial, ops, enabledStore, writeOps: new Set(['subagent-create', 'subagent-update', 'subagent-delete', 'subagent-import', 'subagent-toggle', 'subagent-trash-restore', 'subagent-trash-delete']) };
395
564
  }
396
565
  function validPersonaName(name) {
397
566
  return name.length > 0 && name.length <= 64 && !name.startsWith('.') && !/[\\/<>:"|?*]/.test(name);
@@ -1,12 +1,19 @@
1
1
  export function text(v) {
2
2
  return [{ type: 'text', text: v }];
3
3
  }
4
- /** 场景绑定校验(设计 §3.3):启用场景的 subagents 并集;空并集 = 全部可用。 */
4
+ /**
5
+ * 可见性过滤(两道闸,按序):
6
+ * ① 人设开关(全局):停用的绝不进上下文 —— 即便它被场景绑定(场景启动时会自动把
7
+ * 绑定的人设启用,见 archive-engine 的 applySubagentSwitches)。
8
+ * ② 场景绑定校验(设计 §3.3):启用场景的 subagents 并集;空并集 = 全部可用。
9
+ * 之前只有②:全局默认所有子智能体都进上下文,没有开关可言。
10
+ */
5
11
  export async function filterBySceneBinding(docs, enabledSceneLists) {
12
+ const live = docs.filter((d) => d.enabled !== false);
6
13
  const bound = new Set(enabledSceneLists.flat());
7
14
  if (!bound.size)
8
- return { allowed: docs, reason: null };
9
- const allowed = docs.filter((d) => bound.has(d.name));
15
+ return { allowed: live, reason: null };
16
+ const allowed = live.filter((d) => bound.has(d.name));
10
17
  return { allowed, reason: allowed.length ? null : `当前启用场景的子智能体绑定: ${[...bound].join('、')}` };
11
18
  }
12
19
  export function defineSubagentListTool(subagents) {
package/package.json CHANGED
@@ -1,97 +1,97 @@
1
- {
2
- "name": "dsh-plugin-tool-management",
3
- "version": "0.7.0",
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
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
8
- },
9
- "type": "module",
10
- "main": "lib/index.js",
11
- "exports": {
12
- ".": "./lib/index.js",
13
- "./client": "./lib/client.js",
14
- "./workspace": "./lib/history/workspace.js",
15
- "./projcache": "./lib/history/projcache.js",
16
- "./package.json": "./package.json"
17
- },
18
- "files": [
19
- "lib",
20
- "cordis.patch.yml",
21
- "README.md",
22
- "README_EN.md",
23
- "CHANGELOG.md",
24
- "LICENSE",
25
- "screenshots.json",
26
- "docs"
27
- ],
28
- "engines": {
29
- "node": ">=18"
30
- },
31
- "publishConfig": {
32
- "registry": "https://registry.npmjs.org",
33
- "access": "public"
34
- },
35
- "scripts": {
36
- "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs",
37
- "build:client": "node scripts/sync-client.mjs",
38
- "prepublishOnly": "npm run build",
39
- "lint": "node --check lib/client.js && node --check lib/index.js",
40
- "check:i18n": "node scripts/check-i18n.mjs src/client.js",
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"
45
- },
46
- "keywords": [
47
- "dsh",
48
- "deepseek-harness",
49
- "skill",
50
- "skills",
51
- "mcp",
52
- "mcp-server",
53
- "plugin",
54
- "cordis"
55
- ],
56
- "dsh": {
57
- "bundle": {
58
- "patch": "./cordis.patch.yml"
59
- },
60
- "client": {
61
- "platform": "web",
62
- "inject": [
63
- "@deepseek-ai/dsh-client-locale"
64
- ]
65
- }
66
- },
67
- "peerDependencies": {
68
- "@deepseek-ai/cordis": "^4.0.1",
69
- "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2 <0.2.0-0",
70
- "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2 <0.2.0-0",
71
- "@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2 <0.2.0-0",
72
- "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2 <0.2.0-0",
73
- "@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2 <0.2.0-0",
74
- "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2 <0.2.0-0",
75
- "react": "^18.2.0"
76
- },
77
- "peerDependenciesMeta": {
78
- "@deepseek-ai/dsh-subagent-spawn-in-process": {
79
- "optional": true
80
- }
81
- },
82
- "devDependencies": {
83
- "@deepseek-ai/cordis": "^4.0.1",
84
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
85
- "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
86
- "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
87
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
88
- "@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
89
- "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
90
- "@types/node": "^24.0.0",
91
- "typescript": "^6.0.0"
92
- },
93
- "license": "MIT",
94
- "dependencies": {
95
- "fflate": "^0.8.3"
96
- }
97
- }
1
+ {
2
+ "name": "dsh-plugin-tool-management",
3
+ "version": "0.8.0",
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
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
8
+ },
9
+ "type": "module",
10
+ "main": "lib/index.js",
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./client": "./lib/client.js",
14
+ "./workspace": "./lib/history/workspace.js",
15
+ "./projcache": "./lib/history/projcache.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "cordis.patch.yml",
21
+ "README.md",
22
+ "README_EN.md",
23
+ "CHANGELOG.md",
24
+ "LICENSE",
25
+ "screenshots.json",
26
+ "docs"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "publishConfig": {
32
+ "registry": "https://registry.npmjs.org",
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs",
37
+ "build:client": "node scripts/sync-client.mjs",
38
+ "prepublishOnly": "npm run build",
39
+ "lint": "node --check lib/client.js && node --check lib/index.js",
40
+ "check:i18n": "node scripts/check-i18n.mjs src/client.js",
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"
45
+ },
46
+ "keywords": [
47
+ "dsh",
48
+ "deepseek-harness",
49
+ "skill",
50
+ "skills",
51
+ "mcp",
52
+ "mcp-server",
53
+ "plugin",
54
+ "cordis"
55
+ ],
56
+ "dsh": {
57
+ "bundle": {
58
+ "patch": "./cordis.patch.yml"
59
+ },
60
+ "client": {
61
+ "platform": "web",
62
+ "inject": [
63
+ "@deepseek-ai/dsh-client-locale"
64
+ ]
65
+ }
66
+ },
67
+ "peerDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.1",
69
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2 <0.2.0-0",
70
+ "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2 <0.2.0-0",
71
+ "@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2 <0.2.0-0",
72
+ "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2 <0.2.0-0",
73
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2 <0.2.0-0",
74
+ "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2 <0.2.0-0",
75
+ "react": "^18.2.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@deepseek-ai/dsh-subagent-spawn-in-process": {
79
+ "optional": true
80
+ }
81
+ },
82
+ "devDependencies": {
83
+ "@deepseek-ai/cordis": "^4.0.1",
84
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
85
+ "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
86
+ "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
87
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
88
+ "@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
89
+ "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
90
+ "@types/node": "^24.0.0",
91
+ "typescript": "^6.0.0"
92
+ },
93
+ "license": "MIT",
94
+ "dependencies": {
95
+ "fflate": "^0.8.3"
96
+ }
97
+ }
@@ -1,121 +0,0 @@
1
- // dsh-plugin-tool-management —— 规则「自动在场层」投影(systemPrompt 段注册)。
2
- //
3
- // 变更单 01 的核心:把规则从"投影为 skill 目录条目(模型按需 load)"改为
4
- // **投影为 system prompt 段(自动在场)**。前者即使命中,模型也要自己决定看不看,
5
- // 正是用户抱怨的"每次都要解释"。
6
- //
7
- // 官方接口语义(@deepseek-ai/dsh-system-prompt):
8
- // - `section({ name, order, text })` 在**调用者的 scope** 注册;scoped 段遮蔽同名全局段。
9
- // - `text` 可以是**每次装配时求值的 provider 函数** → 切换启用场景后**下一个请求即生效**,
10
- // 无需重开会话、无需重载插件。
11
- // - `renderPrompt` 会**删除空段** → 无启用场景/空目录时返回 `''`,不产生空标题。
12
- //
13
- // 为什么注册两处(全局 + per-agent):
14
- // 全局注册覆盖所有 agent(含子 agent);per-agent 注册保证即使插件 ctx 自带 scope
15
- // (全局段对某些 agent 不可见)也能命中 —— 两者同名,scoped 遮蔽全局,不会重复注入。
16
- //
17
- // 缓存契约(§5.2):段文本是**会话级恒定**的(只由启用场景与文件内容决定),
18
- // 因此前缀逐字节稳定 → 前缀缓存可命中;禁止在段里放时间戳/计数/相对时间。
19
- const SECTION_NAME = 'tool-management:scene-memory';
20
- /**
21
- * §5.1 硬约束 2:官方 `SECTION_ORDERS` 里 2901–4999 是空档(TOOL_REPORT 2900 →
22
- * TOOLS_SDK 5000),取 **3000** —— 语义上是"工具能力说明之后、输出与 SDK 约定之前",
23
- * 且远离任何既有段。**不要**用官方已有数值(同号按名称排序,虽确定但难读,日后易撞)。
24
- */
25
- const SECTION_ORDER = 3000;
26
- const GLOBAL_KEY = '@global';
27
- /** 取 scope 上的 systemPrompt 服务(Cordis 的 ctx.get 可能抛错,全部吞掉)。 */
28
- function systemPromptOf(scope) {
29
- if (!scope)
30
- return undefined;
31
- try {
32
- if (typeof scope.get === 'function')
33
- return scope.get('systemPrompt');
34
- return scope.systemPrompt;
35
- }
36
- catch {
37
- return undefined;
38
- }
39
- }
40
- /** 注册全局 + agent-scope 的场景记忆段与场景提示词段。 */
41
- export function createRuleProviderRegistrar(ctx, facade) {
42
- const registrations = new Map();
43
- const install = (scope, key) => {
44
- if (!scope || registrations.has(key))
45
- return;
46
- const systemPrompt = systemPromptOf(scope);
47
- if (!systemPrompt || typeof systemPrompt.section !== 'function')
48
- return;
49
- try {
50
- const dispose = systemPrompt.section({
51
- name: SECTION_NAME,
52
- order: SECTION_ORDER,
53
- // 每次装配重读活动场景 → 切换启用状态后下一个请求即生效。
54
- // ⚠️ 硬约束 1:必须**同步返回 string**(不能是 Promise)——异步读取会破坏 renderPrompt。
55
- text: () => facade.renderActiveScenes(),
56
- });
57
- registrations.set(key, { scope, dispose: typeof dispose === 'function' ? dispose : () => { } });
58
- }
59
- catch (e) {
60
- // 同层重名注册会抛错:降级为"该段不注入",不影响其余 scope。
61
- console.error(`[dsh-plugin-tool-management] ${SECTION_NAME} section registration failed:`, String((e && e.message) || e));
62
- }
63
- };
64
- const uninstall = (key) => {
65
- const entry = registrations.get(key);
66
- if (!entry)
67
- return;
68
- registrations.delete(key);
69
- try {
70
- entry.dispose();
71
- }
72
- catch { /* ignore */ }
73
- };
74
- // ① 全局:插件 ctx 的 scope(覆盖所有 agent)
75
- install(ctx, GLOBAL_KEY);
76
- // ② per-agent:与既有 install/uninstall 生命周期一致(agent/created、agent/disposed)
77
- const on = ctx && ctx.on;
78
- let stopCreated;
79
- let stopDisposed;
80
- if (typeof on === 'function') {
81
- stopCreated = on('agent/created', (payload) => {
82
- const agent = payload && payload.agent;
83
- if (agent && agent.id != null)
84
- install(agent.ctx, String(agent.id));
85
- });
86
- stopDisposed = on('agent/disposed', (payload) => {
87
- const agent = payload && payload.agent;
88
- if (agent && agent.id != null)
89
- uninstall(String(agent.id));
90
- });
91
- }
92
- // 已存在的 agent(插件热加载场景):补注册。
93
- try {
94
- const agents = typeof ctx.get === 'function' ? ctx.get('agents') : undefined;
95
- if (agents && typeof agents.list === 'function') {
96
- for (const agent of agents.list()) {
97
- if (agent && agent.id != null)
98
- install(agent.ctx, String(agent.id));
99
- }
100
- }
101
- }
102
- catch { /* agents 服务不可用 → 仅靠事件 */ }
103
- return {
104
- invalidate: () => {
105
- for (const entry of registrations.values()) {
106
- try {
107
- entry.scope?.emit?.('system-prompt/change');
108
- }
109
- catch { /* 宿主无该事件 → 忽略 */ }
110
- }
111
- },
112
- dispose: () => {
113
- if (typeof stopCreated === 'function')
114
- stopCreated();
115
- if (typeof stopDisposed === 'function')
116
- stopDisposed();
117
- for (const key of [...registrations.keys()])
118
- uninstall(key);
119
- },
120
- };
121
- }