dsh-plugin-tool-management 0.2.0 → 0.5.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/lib/index.js CHANGED
@@ -12,9 +12,14 @@
12
12
  // artifact — same convention as DSH's own packages, which ship compiled JS).
13
13
  import { defineTool } from '@deepseek-ai/dsh-tools';
14
14
  import { createRequire } from 'node:module';
15
- import { createSkillsService } from './skills/service.js';
15
+ import { createSkillsService, pluginLog } from './skills/service.js';
16
16
  import { createAgentsMdService } from './agents-md/service.js';
17
17
  import { createRulesService } from './rules/service.js';
18
+ import { createArchiveEngine } from './rules/archive-engine.js';
19
+ import { createSubagentService } from './subagents/service.js';
20
+ import { isApprovalNever } from './approval-policy.js';
21
+ import { hubPath, relocateEntries } from './hub.js';
22
+ import { defineSubagentListTool, defineSubagentRunTool } from './subagents/tools.js';
18
23
  import { detectFormat, extractText, parseGenericText, parseJsonlTranscript, parseMarkdownTranscript } from './imports/parsers.js';
19
24
  import { homedir } from 'node:os';
20
25
  import { spawn } from 'node:child_process';
@@ -71,7 +76,7 @@ export default {
71
76
  catch (e) { /* keep unknown */ }
72
77
  // Optional access token (defense in depth for LAN exposure). Enabled by
73
78
  // setting `config.token` on this plugin's loader row (profile
74
- // cordis.patch.yml override) or the DSH_SKILL_MCP_MANAGER_TOKEN env var. When
79
+ // cordis.patch.yml override) or the DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN env var. When
75
80
  // set, every state-changing op requires `x-dsh-token: <token>`. Read-only
76
81
  // ops (plugin-version, mcpm-list, skill-state) stay open so the UI still
77
82
  // renders; mcpm-export is guarded too because it leaks full configs.
@@ -80,29 +85,6 @@ export default {
80
85
  // is not an injected service and throws "cannot get property without
81
86
  // inject" at boot.
82
87
  const TOKEN = String(config?.token || process.env.DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN || '').trim();
83
- const WRITE_OPS = new Set([
84
- 'mcpm-add', 'mcpm-edit', 'mcpm-remove', 'mcpm-set-enabled', 'mcpm-set-all', 'mcpm-restart',
85
- 'mcpm-export', 'mcpm-import', 'mcpm-note', 'mcpm-settings', 'mcpm-tool-enabled',
86
- // mcpm-reveal returns UNMASKED secrets; even though it is a read, it is
87
- // token-gated like a write — on a LAN-exposed port the token must be the
88
- // last line of defense for plaintext credentials too, not just writes.
89
- 'mcpm-reveal',
90
- 'skill-enable', 'skill-disable', 'skill-source-enable', 'skill-source-disable',
91
- 'skill-create', 'skill-import', 'skill-upload', 'skill-delete',
92
- 'skill-trash-restore', 'skill-trash-delete', 'skill-open',
93
- 'skill-custom-add', 'skill-custom-remove',
94
- // agents-md 写操作(create/update/remove 改预设库;apply 写全局 AGENTS.md;import 从外部内容建预设)
95
- 'agentsmd-create', 'agentsmd-update', 'agentsmd-apply', 'agentsmd-remove', 'agentsmd-import',
96
- // history 写操作(archive/unarchive 改归档集合;delete 永久删除;retention-set 写保留期)
97
- 'history-archive', 'history-unarchive', 'history-delete', 'history-retention-set',
98
- 'history-unarchive-batch', 'history-delete-batch', 'history-import', 'history-export',
99
- 'history-archive-batch',
100
- // rules 写操作(v0.3:create/update/remove/restore 改记忆文件;toggle/set-index/
101
- // set-active 改侧车索引;create-scene/remove-scene 建删场景目录。
102
- // set-active = 插件内"启用场景"开关,全局持久化)
103
- 'rules-create', 'rules-update', 'rules-remove', 'rules-restore', 'rules-toggle', 'rules-set-index', 'rules-set-active',
104
- 'rules-create-scene', 'rules-remove-scene',
105
- ]);
106
88
  const wait = (ms) => ctx.timeout(ms);
107
89
  const message = (e) => String((e && e.message) || e);
108
90
  let writeChain = Promise.resolve();
@@ -125,9 +107,11 @@ export default {
125
107
  }
126
108
  // ---------- agents-md 预设库 + 切换 ----------
127
109
  // DSH 全局指令基线只有 ~/.dsh/AGENTS.md 一个文件,无内置多预设切换;
128
- // 本服务在插件目录内 data/agents-md-presets/ 维护预设库,「应用」= 写入
129
- // ~/.dsh/AGENTS.md,新会话生效(当前会话不变,DSH 本身如此)。
130
- // presetsDir 可由 config 注入(测试用),否则落到插件根 data/。
110
+ // 本服务在 hub 内 agents-md/ 维护预设库,「应用」= 写入 ~/.dsh/AGENTS.md,
111
+ // 新会话生效(当前会话不变,DSH 本身如此)。
112
+ // presetsDir 可由 config 注入(测试用),否则落到 $DSH_HOME/tool-management/agents-md。
113
+ // v0.4:旧位置(插件目录 data/agents-md-presets/)在启动时搬入 hub——
114
+ // 插件目录在 npm 安装下会被覆盖,放用户数据在那里本身就丢数据风险。
131
115
  const PLUGIN_ROOT = (() => {
132
116
  try {
133
117
  return dirname(createRequire(import.meta.url).resolve('../package.json'));
@@ -136,7 +120,10 @@ export default {
136
120
  return process.cwd();
137
121
  }
138
122
  })();
139
- const agentsMdPresetsDir = String(config?.presetsDir || join(PLUGIN_ROOT, 'data', 'agents-md-presets'));
123
+ const legacyAgentsMdPresetsDir = join(PLUGIN_ROOT, 'data', 'agents-md-presets');
124
+ const agentsMdPresetsDir = String(config?.presetsDir || hubPath('agents-md'));
125
+ void relocateEntries(legacyAgentsMdPresetsDir, agentsMdPresetsDir, (n) => n !== '__last-applied__')
126
+ .catch(() => 0);
140
127
  const agentsMdService = createAgentsMdService(ctx, {
141
128
  presetsDir: agentsMdPresetsDir,
142
129
  getGlobalAgentsMdPath: async () => {
@@ -145,14 +132,18 @@ export default {
145
132
  return p.home + sep + 'AGENTS.md';
146
133
  },
147
134
  });
148
- // ---------- rules(规则/记忆,v0.3)----------
149
- // 规则真源 $DSH_HOME/rules/<场景>/<name>.md(仅用户级,D1)。**场景 = 一级目录**;
135
+ // ---------- rules(规则/记忆,v0.4)----------
136
+ // 记忆真源 $DSH_HOME/tool-management/memories/<场景>/<name>.md(仅用户级,D1)。
137
+ // **场景是显式记录**(rules-index.json 的 scenes 切片):含保留场景 `global`(界面「全局」,
138
+ // 其记忆注入任何对话),空场景也合法存在——不再是"恰好有这个目录名"的隐式约定。
150
139
  // 单投影 = 活动场景记忆 → per-agent systemPrompt 段(自动在场,模型无需调用任何工具)。
151
140
  // 原"始终层写 ~/.dsh/AGENTS.md"已下线(变更单 01 §4/§10):公共基线由 _shared/ 承担。
152
- // rulesRoot / rulesStateDir 仅测试注入,生产留空由服务按 DSH_HOME 解析。
141
+ // 旧的 $DSH_HOME/scene-memory 与 $DSH_HOME/rules 由服务在首次读盘前搬入本目录。
142
+ // rulesRoot / rulesStateDir / rulesScenesDir 仅测试注入,生产留空由服务按 DSH_HOME 解析。
153
143
  const rulesService = createRulesService(ctx, {
154
144
  rulesRoot: String(config?.rulesRoot || ''),
155
145
  stateDir: String(config?.rulesStateDir || ''),
146
+ scenesDir: String(config?.rulesScenesDir || ''),
156
147
  // 场景记忆段预算(字节),默认 65536;仅用于测试与特殊部署调优。
157
148
  ...(Number.isFinite(Number(config?.rulesMaxBytes))
158
149
  ? { maxBytes: Number(config.rulesMaxBytes) }
@@ -164,6 +155,171 @@ export default {
164
155
  catch (e) {
165
156
  console.error('[dsh-plugin-tool-management] rules provider setup failed:', message(e));
166
157
  }
158
+ // ---------- 场景档案引擎(设计 §2)----------
159
+ // deps 把既有写通道(MCP 单工具启停 / 技能启停)注入引擎;引擎自身串行,
160
+ // 状态机与纯逻辑在 ./rules/archive*.ts。mcpmToolEnabled / readDisabledTools
161
+ // 为函数声明(提升),此处引用安全。
162
+ const toolKeyParts = (toolName) => {
163
+ if (!toolName.startsWith('mcp__'))
164
+ return null;
165
+ const rest = toolName.slice(5);
166
+ const i = rest.indexOf('__');
167
+ if (i <= 0)
168
+ return null;
169
+ const server = rest.slice(0, i);
170
+ const tool = rest.slice(i + 2);
171
+ return server && tool ? { key: server + '/' + tool, server, tool } : null;
172
+ };
173
+ async function toolStates() {
174
+ const disabled = await readDisabledTools();
175
+ const states = {};
176
+ let schemas = [];
177
+ try {
178
+ schemas = await tools.schemas();
179
+ }
180
+ catch { /* 无 live 工具 → 仅启停表 */ }
181
+ const liveByServer = {};
182
+ for (const s of schemas) {
183
+ const p = toolKeyParts(String((s && s.name) || ''));
184
+ if (!p)
185
+ continue;
186
+ const list = liveByServer[p.server] || (liveByServer[p.server] = []);
187
+ if (list.indexOf(p.tool) < 0)
188
+ list.push(p.tool);
189
+ }
190
+ for (const [server, list] of Object.entries(disabled)) {
191
+ // 整台停用(['*']):在 live 工具上展开成具体键,不产出 `server/*` 伪键(那会污染勾选器)。
192
+ if (list.indexOf('*') >= 0) {
193
+ for (const t of (liveByServer[server] || []))
194
+ states[server + '/' + t] = false;
195
+ continue;
196
+ }
197
+ for (const t of list)
198
+ states[server + '/' + t] = false;
199
+ }
200
+ for (const [server, list] of Object.entries(liveByServer)) {
201
+ for (const t of list)
202
+ if (!(server + '/' + t in states))
203
+ states[server + '/' + t] = true;
204
+ }
205
+ return states;
206
+ }
207
+ async function skillStates() {
208
+ const r = await skillsService.ops['skill-state']({});
209
+ const states = {};
210
+ for (const root of ((r && r.data && r.data.roots) || [])) {
211
+ for (const sk of (root.skills || [])) {
212
+ const name = String(sk.declaredName || sk.name || '');
213
+ if (name)
214
+ states[String(root.key || '') + '/' + name] = sk.enabled !== false;
215
+ }
216
+ }
217
+ return states;
218
+ }
219
+ const archiveService = createArchiveEngine({
220
+ loadSlice: async () => ({ ...(await rulesService.readArchiveSlice()) }),
221
+ saveSlice: (slice) => rulesService.patchIndex(slice),
222
+ configuredServers: async () => {
223
+ const r = await mcpmListView();
224
+ const out = [];
225
+ for (const row of ((r && r.rows) || [])) {
226
+ const n = String((row && row.serverName) || '');
227
+ if (n && out.indexOf(n) < 0)
228
+ out.push(n);
229
+ }
230
+ return out;
231
+ },
232
+ serverKnownTools: async () => {
233
+ const out = {};
234
+ const raw = await readDisabledTools();
235
+ for (const [server, list] of Object.entries(raw))
236
+ out[server] = list.filter((t) => t !== '*');
237
+ let schemas = [];
238
+ try {
239
+ schemas = await tools.schemas();
240
+ }
241
+ catch { /* 无 live 工具 → 仅启停表 */ }
242
+ for (const s of schemas) {
243
+ const p = toolKeyParts(String((s && s.name) || ''));
244
+ if (!p)
245
+ continue;
246
+ const list = out[p.server] || (out[p.server] = []);
247
+ if (list.indexOf(p.tool) < 0)
248
+ list.push(p.tool);
249
+ }
250
+ return out;
251
+ },
252
+ currentMcpRaw: () => readDisabledTools(),
253
+ applyMcpEntries: async (entries) => {
254
+ const p = await ensurePaths();
255
+ await withWriteLock(async () => {
256
+ await writeJsonFile(sidecarPath(p.home, DISABLED_TOOLS_FILE), entries);
257
+ });
258
+ disabledToolsCache = { at: Date.now(), value: entries };
259
+ scheduleToolRestrictions();
260
+ },
261
+ knownSkillKeys: async () => new Set(Object.keys(await skillStates())),
262
+ currentSkills: skillStates,
263
+ applySkills: async (target) => {
264
+ for (const [key, on] of Object.entries(target)) {
265
+ const i = key.indexOf('/');
266
+ if (i <= 0)
267
+ throw new Error(`技能 key 不合法: ${key}`);
268
+ const root = key.slice(0, i);
269
+ const name = key.slice(i + 1);
270
+ const r = await skillsService.ops[on ? 'skill-enable' : 'skill-disable']({ root, name });
271
+ if (r && r.ok === false)
272
+ throw new Error(`技能 ${key} 应用失败: ${r.error}`);
273
+ }
274
+ },
275
+ sceneExists: async (name) => {
276
+ const r = await rulesService.ops['rules-list']({});
277
+ return !!(r && r.ok !== false && (r.scenes || []).some((s) => s.name === name));
278
+ },
279
+ // 人设名全集(保存 subagents 段时校验并报 stale,与 mcp/skills 两段同口径)。
280
+ knownPersonas: async () => new Set((await subagentService.list()).map((p) => p.name)),
281
+ // 记忆 id 全集(保存 memories 段时校验并报 stale)。
282
+ knownMemoryIds: async () => {
283
+ const r = await rulesService.ops['rules-list']({});
284
+ return new Set(((r && r.rules) || []).filter((x) => !x.shadowed).map((x) => String(x.id)));
285
+ },
286
+ });
287
+ // ---------- 轻量子智能体(设计 §3)----------
288
+ // 人设 = $DSH_HOME/tool-management/agents/<name>.md;运行走官方 ctx.subagents.start(spawn provider)。
289
+ // sceneLists 供场景绑定校验:启用场景(rules-list 的 active 行)档案里的 subagents 并集。
290
+ const subagentService = createSubagentService(ctx, {});
291
+ const subagentSceneLists = async () => {
292
+ const slice = await rulesService.readArchiveSlice();
293
+ const r = await rulesService.ops['rules-list']({});
294
+ if (!r || r.ok === false)
295
+ return [];
296
+ return (r.scenes || [])
297
+ .filter((s) => s.active)
298
+ .map((s) => slice.archives[s.name]?.subagents ?? []);
299
+ };
300
+ // HTTP 写操作门禁清单。skills/rules/档案引擎域由各自 service 导出的 writeOps 派生
301
+ // (与其 ops 表同文件维护,新增写 op 改对应 service 即可);本文件内联域
302
+ // (mcpm-* / skill-open / agentsmd-* / history-*)在此列举。
303
+ // 注意:必须在上述 service 创建之后构造(依赖其 writeOps)。
304
+ const WRITE_OPS = new Set([
305
+ ...skillsService.writeOps,
306
+ ...rulesService.writeOps,
307
+ ...archiveService.writeOps,
308
+ ...subagentService.writeOps,
309
+ 'mcpm-add', 'mcpm-edit', 'mcpm-remove', 'mcpm-set-enabled', 'mcpm-set-all', 'mcpm-restart',
310
+ 'mcpm-export', 'mcpm-import', 'mcpm-note', 'mcpm-settings', 'mcpm-tool-enabled',
311
+ // mcpm-reveal returns UNMASKED secrets; even though it is a read, it is
312
+ // token-gated like a write — on a LAN-exposed port the token must be the
313
+ // last line of defense for plaintext credentials too, not just writes.
314
+ 'mcpm-reveal',
315
+ 'skill-open',
316
+ // agents-md 写操作(create/update/remove 改预设库;apply 写全局 AGENTS.md;import 从外部内容建预设)
317
+ 'agentsmd-create', 'agentsmd-update', 'agentsmd-apply', 'agentsmd-remove', 'agentsmd-import',
318
+ // history 写操作(archive/unarchive 改归档集合;delete 永久删除;retention-set 写保留期)
319
+ 'history-archive', 'history-unarchive', 'history-delete', 'history-retention-set',
320
+ 'history-unarchive-batch', 'history-delete-batch', 'history-import', 'history-export',
321
+ 'history-archive-batch',
322
+ ]);
167
323
  // ---------- history(归档会话管理,折叠自 dsh-archive-manager)----------
168
324
  // cordis.patch.yml 禁用官方 workspace 与 session-projection-cache,插入本插件
169
325
  // 的归档感知子类(lib/history/workspace.js + lib/history/projcache.js)。子类
@@ -196,15 +352,10 @@ export default {
196
352
  }
197
353
  }
198
354
  async function writeHistoryRetention(retentionDays) {
199
- try {
200
- const dir = dirname(historyRetentionPath);
201
- await mkdir(dir, { recursive: true });
202
- // updatedAt = 修改时刻:每次改保留期,已归档会话的到期基线重置为此时刻。
203
- await writeFile(historyRetentionPath, JSON.stringify({ retentionDays, updatedAt: Date.now() }), 'utf8');
204
- }
205
- catch (e) {
206
- console.error('[dsh-plugin-tool-management] write history-retention failed:', message(e));
207
- }
355
+ const dir = dirname(historyRetentionPath);
356
+ await mkdir(dir, { recursive: true });
357
+ // updatedAt = 修改时刻:每次改保留期,已归档会话的到期基线重置为此时刻。
358
+ await writeFile(historyRetentionPath, JSON.stringify({ retentionDays, updatedAt: Date.now() }), 'utf8');
208
359
  }
209
360
  /**
210
361
  * 计算到期应删的归档会话。基线 = max(archivedAt(账本)?? createdAt(元数据),
@@ -924,7 +1075,7 @@ export default {
924
1075
  return { ok: true, notes: map };
925
1076
  });
926
1077
  }
927
- const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0, requireConfirmForModelRuleWrite: true };
1078
+ const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0, requireConfirmForModelRuleWrite: true, requireConfirmForModelSubagentRun: true };
928
1079
  let pluginSettingsCache = null;
929
1080
  function clampInt(value, min, max, fallback) {
930
1081
  // Number(null) is 0 — treat missing/empty input as "use the default".
@@ -950,6 +1101,10 @@ export default {
950
1101
  requireConfirmForModelRuleWrite: (raw && typeof raw.requireConfirmForModelRuleWrite === 'boolean')
951
1102
  ? raw.requireConfirmForModelRuleWrite
952
1103
  : SETTINGS_DEFAULTS.requireConfirmForModelRuleWrite,
1104
+ // 子代理运行花真 token:默认确认,设置可关(设计 §3.2)。
1105
+ requireConfirmForModelSubagentRun: (raw && typeof raw.requireConfirmForModelSubagentRun === 'boolean')
1106
+ ? raw.requireConfirmForModelSubagentRun
1107
+ : SETTINGS_DEFAULTS.requireConfirmForModelSubagentRun,
953
1108
  },
954
1109
  };
955
1110
  return pluginSettingsCache.value;
@@ -964,6 +1119,9 @@ export default {
964
1119
  requireConfirmForModelRuleWrite: (args && typeof args.requireConfirmForModelRuleWrite === 'boolean')
965
1120
  ? args.requireConfirmForModelRuleWrite
966
1121
  : current.requireConfirmForModelRuleWrite,
1122
+ requireConfirmForModelSubagentRun: (args && typeof args.requireConfirmForModelSubagentRun === 'boolean')
1123
+ ? args.requireConfirmForModelSubagentRun
1124
+ : current.requireConfirmForModelSubagentRun,
967
1125
  };
968
1126
  const p = await ensurePaths();
969
1127
  return withWriteLock(async () => {
@@ -995,7 +1153,9 @@ export default {
995
1153
  for (const serverName of Object.keys(raw)) {
996
1154
  const list = raw[serverName];
997
1155
  if (Array.isArray(list)) {
998
- const names = list.map((name) => String(name)).filter((name) => /^[A-Za-z0-9_-]{1,128}$/.test(name));
1156
+ // `*` 必须原样保留:它是「整台服务器停用」的通配(场景档案未勾选的服务器写的就是它),
1157
+ // 被这里过滤掉的话 guard / restrict / 快照三条链路一起失效——整台停用变成空转。
1158
+ const names = list.map((name) => String(name)).filter((name) => name === '*' || /^[A-Za-z0-9_-]{1,128}$/.test(name));
999
1159
  if (names.length)
1000
1160
  out[serverName] = names;
1001
1161
  }
@@ -1013,6 +1173,21 @@ export default {
1013
1173
  }
1014
1174
  return out;
1015
1175
  }
1176
+ /** 单工具停用判定(含整服务器通配 `*`——场景档案的"MCP 工具集=整台"写的就是它)。 */
1177
+ function isToolDisabled(map, fullName) {
1178
+ if (!fullName.startsWith('mcp__'))
1179
+ return false;
1180
+ const rest = fullName.slice(5);
1181
+ const i = rest.indexOf('__');
1182
+ if (i <= 0)
1183
+ return false;
1184
+ const list = map[rest.slice(0, i)];
1185
+ if (!list)
1186
+ return false;
1187
+ if (list.indexOf('*') >= 0)
1188
+ return true;
1189
+ return list.indexOf(rest.slice(i + 2)) >= 0;
1190
+ }
1016
1191
  async function mcpmToolEnabled(args) {
1017
1192
  const serverName = String((args && args.serverName) || '').trim();
1018
1193
  const tool = String((args && args.tool) || '').trim();
@@ -1056,6 +1231,7 @@ export default {
1056
1231
  // Force re-read: this runs on the tools/change path, which is rare, so a
1057
1232
  // stale cache must not keep an externally edited deny list hidden.
1058
1233
  const map = await readDisabledTools(true);
1234
+ // 通配 `*`(整服务器停用)先展开成已注册的全名,再与精确名单合并。
1059
1235
  const wanted = disabledToolNames(map);
1060
1236
  let registered;
1061
1237
  try {
@@ -1064,6 +1240,14 @@ export default {
1064
1240
  catch (e) {
1065
1241
  return;
1066
1242
  }
1243
+ for (const [serverName, list] of Object.entries(map)) {
1244
+ if (list.indexOf('*') < 0)
1245
+ continue;
1246
+ const prefix = 'mcp__' + serverName + '__';
1247
+ for (const fullName of registered)
1248
+ if (fullName.startsWith(prefix))
1249
+ wanted.push(fullName);
1250
+ }
1067
1251
  const names = wanted.filter((name) => registered.has(name));
1068
1252
  if (restrictDisposer) {
1069
1253
  try {
@@ -1172,7 +1356,9 @@ export default {
1172
1356
  return { ok: false, error: message(e) };
1173
1357
  }
1174
1358
  const descriptionLimit = (await readPluginSettings()).toolDescriptionMaxLength;
1175
- const disabledHere = new Set((await readDisabledTools())[serverName] || []);
1359
+ const disabledMapHere = await readDisabledTools();
1360
+ const disabledHere = new Set(disabledMapHere[serverName] || []);
1361
+ const wildcardHere = disabledHere.has('*');
1176
1362
  const toolsList = [];
1177
1363
  for (const s of schemas) {
1178
1364
  const fullName = String(s && s.name || '');
@@ -1201,7 +1387,7 @@ export default {
1201
1387
  toolsList.push({
1202
1388
  name: rawName,
1203
1389
  description: truncateText(typeof s.description === 'string' ? s.description : '', descriptionLimit),
1204
- enabled: !disabledHere.has(rawName),
1390
+ enabled: wildcardHere ? false : !disabledHere.has(rawName),
1205
1391
  parameters: params,
1206
1392
  });
1207
1393
  }
@@ -1211,6 +1397,8 @@ export default {
1211
1397
  // stay reachable; the description is unavailable once the schema is gone.
1212
1398
  const listed = new Set(toolsList.map((t) => t.name));
1213
1399
  for (const name of disabledHere) {
1400
+ if (name === '*')
1401
+ continue; // 整服务器通配:已在上方逐工具体现,不再展示占位行
1214
1402
  if (!listed.has(name)) {
1215
1403
  toolsList.push({ name, description: '(已停用;描述暂不可用)', enabled: false, parameters: [] });
1216
1404
  }
@@ -1791,6 +1979,168 @@ export default {
1791
1979
  },
1792
1980
  };
1793
1981
  }
1982
+ /**
1983
+ * 记忆候选(档案编辑器第 4 段「记忆」):`[{ id, scene, name, description }]`。
1984
+ * 只读、不读正文(勾选集只存 id);任何异常降级为空列表,不让档案弹窗崩掉。
1985
+ */
1986
+ async function memoryCandidates() {
1987
+ try {
1988
+ const r = await rulesService.ops['rules-list']({});
1989
+ if (!r || r.ok === false)
1990
+ return [];
1991
+ return (r.rules || [])
1992
+ .filter((x) => !x.shadowed)
1993
+ .map((x) => ({
1994
+ id: String(x.id),
1995
+ scene: String(x.group || ''),
1996
+ name: String(x.name || ''),
1997
+ description: String(x.description || ''),
1998
+ }));
1999
+ }
2000
+ catch {
2001
+ return [];
2002
+ }
2003
+ }
2004
+ /** 场景候选(档案编辑器第 4 段的分组维度):`[{ name, label, description, count, global }]`。 */
2005
+ async function memorySceneCandidates() {
2006
+ try {
2007
+ const r = await rulesService.ops['rules-list']({});
2008
+ if (!r || r.ok === false)
2009
+ return [];
2010
+ return (r.scenes || []).map((s) => ({
2011
+ name: String(s.name),
2012
+ label: String(s.label || s.name),
2013
+ description: String(s.description || ''),
2014
+ count: Number(s.count || 0),
2015
+ global: s.global === true,
2016
+ }));
2017
+ }
2018
+ catch {
2019
+ return [];
2020
+ }
2021
+ }
2022
+ /**
2023
+ * 工具候选(人设的「工具白名单 / 黑名单」选择器):
2024
+ * 取**全体 Agent 预设工具名的并集**——人设可能在任意预设下被子代理复用,
2025
+ * 只列当前会话的工具会让换预设后的子代理启动失败(官方 `toolFilter` 对未知名直接拒绝)。
2026
+ * 同时标注 `current`:当前会话可见的工具(其余只是「本预设可用,当前会话看不到」)。
2027
+ *
2028
+ * 每次调用都会为尚未挂载的预设建立 standing mount(官方语义:一个预设在本进程内只挂一次,
2029
+ * 正常创建会话时同样会挂),因此结果会按需缓存 60 秒,避免频繁枚举。
2030
+ */
2031
+ let presetToolsCache = null;
2032
+ async function presetToolCandidates() {
2033
+ if (presetToolsCache && Date.now() - presetToolsCache.at < 60000)
2034
+ return presetToolsCache.value;
2035
+ const byName = new Map();
2036
+ const presets = [];
2037
+ // 当前会话的可见工具(用于标 current);拿不到就全部按「非当前」处理。
2038
+ const currentNames = new Set();
2039
+ try {
2040
+ const schemas = await tools.schemas();
2041
+ for (const s of schemas || [])
2042
+ currentNames.add(String(s.name));
2043
+ }
2044
+ catch { /* 无 live 工具 → current 全 false */ }
2045
+ const agentPresets = (typeof ctx.get === 'function' ? ctx.get('agentPresets') : undefined);
2046
+ if (agentPresets && typeof agentPresets.list === 'function') {
2047
+ try {
2048
+ const roster = await agentPresets.list();
2049
+ for (const p of roster || []) {
2050
+ const id = String((p && p.id) || '');
2051
+ if (!id)
2052
+ continue;
2053
+ presets.push({ id, name: String((p && (p.name || p.id)) || id) });
2054
+ let scopeKey;
2055
+ try {
2056
+ // 官方说明:该调用会确保预设的 standing mount(不创建 agent/session),
2057
+ // 已挂载的预设直接复用;正因如此才需要这里的 60 秒缓存。
2058
+ scopeKey = typeof agentPresets.standingKeyFor === 'function' ? await agentPresets.standingKeyFor(id) : undefined;
2059
+ }
2060
+ catch {
2061
+ scopeKey = undefined;
2062
+ }
2063
+ let names = [];
2064
+ try {
2065
+ names = (await tools.schemas(scopeKey) || []).map((s) => String(s.name)).filter(Boolean);
2066
+ }
2067
+ catch {
2068
+ names = [];
2069
+ }
2070
+ for (const name of names) {
2071
+ // MCP 工具名形如 `mcp__<server>__<tool>`:面向上百个条目,噪声大于价值 → 不进候选。
2072
+ if (name.startsWith('mcp__'))
2073
+ continue;
2074
+ const rec = byName.get(name) || { name, presets: new Set(), current: false };
2075
+ rec.presets.add(id);
2076
+ byName.set(name, rec);
2077
+ }
2078
+ }
2079
+ }
2080
+ catch { /* 预设服务不可用 → 退回「仅当前会话工具」 */ }
2081
+ }
2082
+ // 当前会话的工具即便没有任何预设可枚举,也要出现在候选里(否则选择器是空的)。
2083
+ for (const name of currentNames) {
2084
+ if (name.startsWith('mcp__'))
2085
+ continue;
2086
+ const rec = byName.get(name) || { name, presets: new Set(), current: false };
2087
+ byName.set(name, rec);
2088
+ }
2089
+ for (const rec of byName.values())
2090
+ rec.current = currentNames.has(rec.name);
2091
+ const value = {
2092
+ tools: [...byName.values()]
2093
+ .map((r) => ({ name: r.name, presets: [...r.presets].sort(), current: r.current }))
2094
+ .sort((a, b) => a.name.localeCompare(b.name)),
2095
+ presets: presets.sort((a, b) => a.id.localeCompare(b.id)),
2096
+ };
2097
+ presetToolsCache = { at: Date.now(), value };
2098
+ return value;
2099
+ }
2100
+ /**
2101
+ * 模型候选(人设的「模型」下拉):宿主已注册的 provider + 各 provider 能宣告的模型。
2102
+ * 只读宿主 LLM 目录,**不发起网络请求**(`discoverModels` 会打端点,这里不用);
2103
+ * 拿不到就返回空列表,UI 退回手填(跨来源模型如 sensenova 的手工条目仍需手填兜底)。
2104
+ * 返回扁平列表:DSH 的模型路由是 (provider, model) 一对,两个键必须同时给。
2105
+ */
2106
+ async function modelCandidates() {
2107
+ const llm = (typeof ctx.get === 'function' ? ctx.get('llm') : undefined);
2108
+ if (!llm || typeof llm.listProviders !== 'function')
2109
+ return { models: [] };
2110
+ let list = [];
2111
+ try {
2112
+ list = llm.listProviders() || [];
2113
+ }
2114
+ catch {
2115
+ return { models: [] };
2116
+ }
2117
+ const models = [];
2118
+ const seen = new Set();
2119
+ for (const p of list) {
2120
+ const provider = String((p && (p.id || p.provider)) || '');
2121
+ if (!provider)
2122
+ continue;
2123
+ const providerName = String((p && (p.name || p.displayName)) || provider);
2124
+ let discovered = [];
2125
+ try {
2126
+ // 适配器可选提供 listModels(官方 LlmAdapter 契约);未提供时该 provider 只报名字。
2127
+ if (typeof llm.listModels === 'function')
2128
+ discovered = (await llm.listModels(provider)) || [];
2129
+ }
2130
+ catch { /* 该 provider 的目录读失败 → 只报 provider 本身 */ }
2131
+ for (const m of discovered) {
2132
+ const id = String((m && (m.id || m.model)) || '');
2133
+ if (!id)
2134
+ continue;
2135
+ const key = provider + '\u0000' + id;
2136
+ if (seen.has(key))
2137
+ continue;
2138
+ seen.add(key);
2139
+ models.push({ provider, providerName, id, name: String((m && m.name) || id) });
2140
+ }
2141
+ }
2142
+ return { models };
2143
+ }
1794
2144
  const handlers = {
1795
2145
  'plugin-version': pluginVersion,
1796
2146
  'mcpm-list': mcpmListView,
@@ -1816,10 +2166,57 @@ export default {
1816
2166
  ...skillsService.ops,
1817
2167
  // rules ops(由 ./rules/service.js 提供):rules-list / rules-read / rules-budget /
1818
2168
  // rules-diagnose / rules-create / rules-update / rules-remove / rules-restore /
1819
- // rules-trash-list / rules-trash-remove / rules-toggle / rules-set-index /
1820
- // rules-set-active / rules-create-scene / rules-remove-scene。
2169
+ // rules-attach / rules-detach / rules-trash-list / rules-trash-remove /
2170
+ // rules-toggle / rules-set-index / rules-set-active / rules-create-scene /
2171
+ // rules-remove-scene。
1821
2172
  // 成功返回扁平 {ok:true, ...}(不套 data),失败 {ok:false, error, code?}。
1822
2173
  ...rulesService.ops,
2174
+ // 场景档案 ops(由 ./rules/archive-engine.ts 提供):scene-mode-get /
2175
+ // scene-archive-save / scene-mode-set。成功返回扁平 {ok:true, ...},
2176
+ // 失败 {ok:false, error};写 op 已含 archiveService.writeOps 门禁派生。
2177
+ ...archiveService.ops,
2178
+ // 子智能体 ops(由 ./subagents/service.ts 提供):subagent-list/get(只读)
2179
+ // + subagent-create/update/delete(写,writeOps 已派生进门禁)。
2180
+ ...subagentService.ops,
2181
+ // 人设表单的两个候选源(只读,均为「按需拉取」——不进 scene-inventory,避免每次开档案弹窗都枚举预设)。
2182
+ // preset-tools:全体 Agent 预设工具名并集 + 各工具所属预设 + 当前会话是否可见。
2183
+ 'preset-tools': async () => ({ ok: true, ...(await presetToolCandidates()) }),
2184
+ // model-candidates:宿主 LLM 目录里的 (provider, model) 对(不发网络请求)。
2185
+ 'model-candidates': async () => ({ ok: true, ...(await modelCandidates()) }),
2186
+ // 场景档案勾选器数据源 v2:全部 MCP 服务器(含未运行)+ 技能全集 + 人设清单。
2187
+ 'scene-inventory': async () => {
2188
+ const [rowsR, tools, skills, subs] = await Promise.all([mcpmListView(), toolStates(), skillStates(), subagentService.list()]);
2189
+ const disabledRaw = await readDisabledTools();
2190
+ const rows = (rowsR && rowsR.rows) || [];
2191
+ const mcpServers = [];
2192
+ const seen = new Set();
2193
+ for (const row of rows) {
2194
+ const name = String((row && row.serverName) || '');
2195
+ if (!name || seen.has(name))
2196
+ continue;
2197
+ seen.add(name);
2198
+ mcpServers.push({
2199
+ name,
2200
+ level: row.level || null,
2201
+ live: !!(row.live && row.live.enabled),
2202
+ serverDisabled: !!row.disabled,
2203
+ // 停用表里的 ['*'] = 整台工具停用(勾选器据此预勾,避免把"全停"读成"全启用")。
2204
+ allToolsDisabled: (disabledRaw[name] || []).indexOf('*') >= 0,
2205
+ toolCount: typeof row.toolCount === 'number' ? row.toolCount : null,
2206
+ });
2207
+ }
2208
+ return {
2209
+ ok: true,
2210
+ mcpServers,
2211
+ tools: Object.entries(tools).map(([key, enabled]) => ({ key, enabled })),
2212
+ skills: Object.entries(skills).map(([key, enabled]) => ({ key, enabled })),
2213
+ subagents: subs.map((p) => ({ name: p.name, description: p.description })),
2214
+ // 档案编辑器第 4 段「记忆」的候选:场景 + 每个场景里的记忆条数。
2215
+ // 记忆正文不在这里返回(勾选集只存 id,渲染时才读盘)。
2216
+ scenes: await memorySceneCandidates(),
2217
+ memories: await memoryCandidates(),
2218
+ };
2219
+ },
1823
2220
  // AGENTS.md 预设库 ops(由 ./agents-md/service.js 提供):agentsmd-list /
1824
2221
  // agentsmd-read / agentsmd-create / agentsmd-update / agentsmd-remove /
1825
2222
  // agentsmd-apply / agentsmd-get-current
@@ -2229,7 +2626,12 @@ export default {
2229
2626
  const days = Number((args && args.retentionDays) ?? -1);
2230
2627
  if (!Number.isFinite(days) || days < 0)
2231
2628
  return { ok: false, error: 'retentionDays 需为非负整数(0=永久不删除)' };
2232
- await writeHistoryRetention(Math.floor(days));
2629
+ try {
2630
+ await writeHistoryRetention(Math.floor(days));
2631
+ }
2632
+ catch (e) {
2633
+ return { ok: false, error: '写入保留期失败: ' + message(e) };
2634
+ }
2233
2635
  // 设置变更后立即扫一次,UI 反映新策略。
2234
2636
  await sweepHistory().catch(() => { });
2235
2637
  return { ok: true, retentionDays: Math.floor(days) };
@@ -2387,32 +2789,33 @@ export default {
2387
2789
  return 'OK: preset ' + args.id + ' applied to ~/.dsh/AGENTS.md (next session; current session unchanged' + (r.backedUp ? '; previous backed up to __last-applied__' : '') + ')';
2388
2790
  },
2389
2791
  }));
2390
- // ---------- rules model tools(v0.3)----------
2792
+ // ---------- rules model tools(v0.4)----------
2391
2793
  // 活动场景的记忆正文会自动进入系统提示词(无需调用工具读取);这里的工具用于
2392
2794
  // 查询/编辑规则本身。rule_manager_write 受 tools/pre-execute 审批门禁(D2)。
2795
+ // 路径锚点:$DSH_HOME/tool-management/memories/<场景>/…(场景 `global` = 界面「全局」)。
2393
2796
  tools.register(defineTool({
2394
2797
  name: 'rule_manager_list',
2395
- description: 'List rules/memories under ~/.dsh/scene-memory (id, scene, enabled, description).',
2798
+ description: 'List memories under ~/.dsh/tool-management/memories (id, scene, enabled, description).',
2396
2799
  parameters: {
2397
- group: { type: 'string', description: 'Optional group/scene filter.' },
2800
+ group: { type: 'string', description: 'Optional scene filter.' },
2398
2801
  },
2399
2802
  output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
2400
2803
  async execute(args) {
2401
2804
  const r = await rulesService.ops['rules-list'](args);
2402
2805
  if (!r || r.ok === false)
2403
2806
  throw new Error((r && r.error) || '读取规则失败');
2404
- const lines = (r.rules || []).map((x) => ('- ' + x.id + ' [' + (x.group || '全局') + '] ' + (x.enabled ? '已启用' : '已停用') +
2807
+ const lines = (r.rules || []).map((x) => ('- ' + x.id + ' [' + (x.group || '未归属场景') + '] ' + (x.enabled ? '已启用' : '已停用') +
2405
2808
  (x.description ? ' — ' + x.description : '')));
2406
- const scenes = (r.scenes || []).map((s) => s.name + (s.active ? '(启用)' : '(未启用)')).join('、');
2407
- return '规则(' + (r.rules || []).length + '):\n' + (lines.join('\n') || '(无规则)') +
2809
+ const scenes = (r.scenes || []).map((s) => (s.label || s.name) + (s.active ? '(启用)' : '(未启用)')).join('、');
2810
+ return '记忆(' + (r.rules || []).length + '):\n' + (lines.join('\n') || '(无记忆)') +
2408
2811
  '\n场景:' + (scenes || '(无)') + (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)');
2409
2812
  },
2410
2813
  }));
2411
2814
  tools.register(defineTool({
2412
2815
  name: 'rule_manager_read',
2413
- description: 'Read the full body of one rule/memory under ~/.dsh/scene-memory.',
2816
+ description: 'Read the full body of one memory under ~/.dsh/tool-management/memories.',
2414
2817
  parameters: {
2415
- id: { type: 'string', required: true, description: 'Rule id like <scene>/<name>.' },
2818
+ id: { type: 'string', required: true, description: 'Memory id like <scene>/<name>.' },
2416
2819
  },
2417
2820
  output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
2418
2821
  async execute(args) {
@@ -2424,9 +2827,9 @@ export default {
2424
2827
  }));
2425
2828
  tools.register(defineTool({
2426
2829
  name: 'rule_manager_write',
2427
- description: 'Create a new rule/memory as ~/.dsh/scene-memory/<scene>/<name>.md. It becomes active automatically once its scene is enabled. Requires user confirmation (configurable).',
2830
+ description: 'Create a new memory as ~/.dsh/tool-management/memories/<scene>/<name>.md. The scene must already exist (use global for the always-on scene). It becomes active automatically once its scene is enabled. Requires user confirmation (configurable).',
2428
2831
  parameters: {
2429
- group: { type: 'string', required: true, description: 'Scene/folder name under ~/.dsh/scene-memory (any Unicode except path separators and < > : " | ? *).' },
2832
+ group: { type: 'string', required: true, description: 'Scene name under ~/.dsh/tool-management/memories; `global` = the always-on scene (any Unicode except path separators and < > : " | ? *).' },
2430
2833
  name: { type: 'string', required: true, description: 'Memory name = the .md file name without the extension; any Unicode is fine (Chinese included), <=64 chars, no path separators or < > : " | ? *, must not start with a dot.' },
2431
2834
  description: { type: 'string', required: true, description: 'One-sentence description (<=500 chars).' },
2432
2835
  body: { type: 'string', required: true, description: 'Markdown body (<=256 KiB).' },
@@ -2436,25 +2839,62 @@ export default {
2436
2839
  const r = await rulesService.ops['rules-create'](args);
2437
2840
  if (!r || r.ok === false)
2438
2841
  throw new Error((r && r.error) || '创建规则失败');
2439
- return 'OK: rule ' + r.rule.id + '(场景「' + (r.rule.group || '全局') + '」启用后自动生效)';
2842
+ return 'OK: memory ' + r.rule.id + '(场景「' + (r.rule.group || '未归属') + '」启用后自动生效)';
2440
2843
  },
2441
2844
  }));
2845
+ // ---------- 子智能体工具(subagent_list / subagent_run)----------
2846
+ // exec.agent / exec.signal 由工具运行时提供(parent 与取消信号的官方通道)。
2847
+ try {
2848
+ // 与其余 12 个工具同一条注册通道:defineTool 负责编译 parameters(object root + required),
2849
+ // 裸 register 会把未编译的参数声明直接发给模型 API。
2850
+ tools.register(defineTool(defineSubagentListTool({ list: () => subagentService.list(), sceneLists: subagentSceneLists })));
2851
+ tools.register(defineTool(defineSubagentRunTool({ ...subagentService, sceneLists: subagentSceneLists })));
2852
+ }
2853
+ catch (e) {
2854
+ console.error('[dsh-plugin-tool-management] subagent tool registration failed:', message(e));
2855
+ }
2442
2856
  if (typeof ctx.on === 'function') {
2443
- ;
2857
+ // 三个确认门的统一前裁决。口径(用户裁定,方案 B):会话审批策略为 never(「完全权限」预设)
2858
+ // = 用户已在预设层面预先批准一切确认门,**直接放行**并写审计日志留痕。
2859
+ // 不能把 ask 丢给审批层:宿主 decide() 对 never 直接返回 rejected(fail-closed),确认卡
2860
+ // 永远弹不出,模型只会收到一句无信息量的「用户拒绝」。这与官方子代理工具(无 ask 门)
2861
+ // 在完全权限下的行为一致;探测实现与回归测试见 approval-policy.ts(必须 ctx.get('approval'),
2862
+ // 不能用 ctx.approval——inject 未声明该服务时 cordis 代理会抛 "cannot get property without inject")。
2863
+ const CONFIRM_LABELS = {
2864
+ skill_manager_create: '「新建技能」',
2865
+ rule_manager_write: '「写入记忆」',
2866
+ subagent_run: '「运行子代理」',
2867
+ };
2868
+ const bypassedByFullAccess = (exec) => {
2869
+ if (!isApprovalNever(ctx, exec))
2870
+ return false;
2871
+ // 留痕:完全权限下跳过确认属于「用户已授权」,但要可审计(日志失败不阻塞主流程)。
2872
+ pluginLog()('confirm-bypass', `完全权限(approval=never):跳过${CONFIRM_LABELS[String(exec && exec.name)]}的确认,直接放行`).catch(() => { });
2873
+ return true;
2874
+ };
2444
2875
  ctx.on('tools/pre-execute', (exec, next) => {
2445
- if (exec && exec.name === 'skill_manager_create') {
2876
+ if (!exec || !CONFIRM_LABELS[String(exec.name)])
2877
+ return next();
2878
+ if (bypassedByFullAccess(exec))
2879
+ return next();
2880
+ if (exec.name === 'skill_manager_create') {
2446
2881
  return Promise.resolve({ kind: 'ask', reason: 'Create a new skill under DSH_HOME/skills' });
2447
2882
  }
2448
- if (exec && exec.name === 'rule_manager_write') {
2883
+ if (exec.name === 'rule_manager_write') {
2449
2884
  // D2:模型写规则默认需确认;设置关闭后直接放行。ask 无应答者时降级为拒绝(fail-closed),
2450
2885
  // 不在此处做任何兜底放行。
2451
2886
  return readPluginSettings()
2452
2887
  .then((s) => (s.requireConfirmForModelRuleWrite
2453
- ? { kind: 'ask', reason: 'Write a rule under ~/.dsh/scene-memory' }
2888
+ ? { kind: 'ask', reason: 'Write a memory under ~/.dsh/tool-management/memories' }
2454
2889
  : next()))
2455
- .catch(() => ({ kind: 'ask', reason: 'Write a rule under ~/.dsh/scene-memory' }));
2456
- }
2457
- return next();
2890
+ .catch(() => ({ kind: 'ask', reason: 'Write a memory under ~/.dsh/tool-management/memories' }));
2891
+ }
2892
+ // subagent_run:子代理运行花真 token:默认确认(requireConfirmForModelSubagentRun !== false),可关。
2893
+ return readPluginSettings()
2894
+ .then((s) => (s.requireConfirmForModelSubagentRun !== false
2895
+ ? { kind: 'ask', reason: 'Run a subagent (consumes tokens)' }
2896
+ : next()))
2897
+ .catch(() => ({ kind: 'ask', reason: 'Run a subagent (consumes tokens)' }));
2458
2898
  });
2459
2899
  }
2460
2900
  // ---------- per-tool MCP switches: execution guard + visibility ----------
@@ -2467,7 +2907,7 @@ export default {
2467
2907
  try {
2468
2908
  // Reads the TTL cache without I/O: the guard runs on the hot path.
2469
2909
  const value = disabledToolsCache ? disabledToolsCache.value : {};
2470
- if (disabledToolNames(value).indexOf(String((exec && exec.name) || '')) >= 0) {
2910
+ if (isToolDisabled(value, String((exec && exec.name) || ''))) {
2471
2911
  return '该工具已在 MCP 管理页停用';
2472
2912
  }
2473
2913
  }
@@ -2606,93 +3046,10 @@ export default {
2606
3046
  console.error('[dsh-plugin-tool-management] webServer route registration failed:', message(e));
2607
3047
  }
2608
3048
  }
2609
- // ---------- human chat commands (/mcp, /skills) ----------
2610
- // The commands service is optional, so probe for it and skip silently when
2611
- // it is missing (same defensive shape as pluginInventory above).
2612
- try {
2613
- const commands = (typeof ctx.get === 'function' ? ctx.get('commands') : undefined);
2614
- if (commands && typeof commands.register === 'function') {
2615
- ctx.effect(() => commands.register({
2616
- name: 'mcp',
2617
- description: '列出已配置的 MCP 服务(级别 / 启停 / loader 状态 / 工具数)',
2618
- handler: async () => {
2619
- const r = await mcpmList();
2620
- if (!r || r.ok === false)
2621
- return { kind: 'error', text: (r && r.error) || '读取 MCP 服务失败' };
2622
- // 启用的在前,同组内按名称排序。
2623
- const rows = (r.rows || []).slice().sort((a, b) => {
2624
- if (!!a.disabled !== !!b.disabled)
2625
- return a.disabled ? 1 : -1;
2626
- return String(a.serverName).localeCompare(String(b.serverName));
2627
- });
2628
- if (!rows.length)
2629
- return { kind: 'success', text: '未配置任何 MCP 服务。' };
2630
- // Human-facing command: show localized levels, not the raw keys.
2631
- const labels = { project: 'Profile 级', global: '全局', loader: '已加载' };
2632
- const text = 'MCP 服务(' + rows.length + '):\n' + rows.map((row) => ('- ' + row.serverName + ' [' + (labels[row.level] || row.level) + '] ' + (row.disabled ? '已停用' : '已启用') +
2633
- (row.live ? ' · loader:' + (row.live.enabled ? 'on' : 'off') + (row.live.phase ? '/' + row.live.phase : '') : '') +
2634
- (typeof row.toolCount === 'number' ? ' · ' + row.toolCount + ' 工具' : ''))).join('\n');
2635
- return { kind: 'success', text };
2636
- },
2637
- }), 'dsh-plugin-tool-management: /mcp command');
2638
- ctx.effect(() => commands.register({
2639
- name: 'skills',
2640
- description: '列出各来源的技能及其启停状态',
2641
- handler: async () => {
2642
- const r = await skillsService.ops['skill-state']({});
2643
- if (!r || r.ok === false)
2644
- return { kind: 'error', text: (r && r.error) || '读取技能失败' };
2645
- const data = r.data || {};
2646
- const entries = [];
2647
- for (const root of data.roots || []) {
2648
- for (const skill of root.skills || []) {
2649
- entries.push({
2650
- line: '- ' + (skill.declaredName || skill.name) + ' [' + (root.label || root.key) + '] ' + (skill.enabled === false ? '已停用' : '已启用'),
2651
- enabled: skill.enabled !== false,
2652
- });
2653
- }
2654
- }
2655
- // 启用的在前,同组内保持原有顺序(来源与名称序)。
2656
- entries.sort((a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1));
2657
- const lines = entries.map((entry) => entry.line);
2658
- return { kind: 'success', text: lines.length ? '技能(' + lines.length + '):\n' + lines.join('\n') : '未发现任何技能。' };
2659
- },
2660
- }), 'dsh-plugin-tool-management: /skills command');
2661
- ctx.effect(() => commands.register({
2662
- name: 'agents-md',
2663
- description: '列出 AGENTS.md 预设及当前生效(应用后新会话生效,当前会话不变)',
2664
- handler: async () => {
2665
- const r = await agentsMdService.list();
2666
- if (!r || r.ok === false)
2667
- return { kind: 'error', text: (r && r.error) || '读取 AGENTS.md 预设失败' };
2668
- const presets = r.presets || [];
2669
- if (!presets.length)
2670
- return { kind: 'success', text: '未发现任何 AGENTS.md 预设。' };
2671
- const text = 'AGENTS.md 预设(' + presets.length + '):\n' + presets.map((p) => ('- ' + p.id + (p.active ? ' [生效中]' : ''))).join('\n') + '\n(应用后新会话生效,当前会话不变)';
2672
- return { kind: 'success', text };
2673
- },
2674
- }), 'dsh-plugin-tool-management: /agents-md command');
2675
- ctx.effect(() => commands.register({
2676
- name: 'scene-memory',
2677
- description: '列出场景记忆及其场景启用状态',
2678
- handler: async () => {
2679
- const r = await rulesService.ops['rules-list']({});
2680
- if (!r || r.ok === false)
2681
- return { kind: 'error', text: (r && r.error) || '读取记忆失败' };
2682
- const rules = r.rules || [];
2683
- if (!rules.length)
2684
- return { kind: 'success', text: '未发现任何记忆(~/.dsh/scene-memory 为空)。' };
2685
- const scenes = r.scenes || [];
2686
- const text = '场景记忆(' + rules.length + '):\n' + rules.map((rule) => ('- ' + rule.name + ' [' + (rule.group || '全局') + '] · ' + (rule.enabled ? '已启用' : '已停用'))).join('\n') + '\n场景:' + (scenes.map((s) => s.name + (s.active ? '(启用)' : '(未启用)')).join('、') || '(无)') +
2687
- (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)') +
2688
- '\n(启用场景的记忆正文自动进入系统提示词,无需任何工具调用)';
2689
- return { kind: 'success', text };
2690
- },
2691
- }), 'dsh-plugin-tool-management: /scene-memory command');
2692
- }
2693
- }
2694
- catch (e) {
2695
- console.error('[dsh-plugin-tool-management] command registration failed:', message(e));
2696
- }
3049
+ // ---------- 斜杠命令:已下线 ----------
3050
+ // 曾有 /mcp、/skills、/agents-md、/scene-memory 四条(注册到 commands 服务)。
3051
+ // 用户 2026-09-13 判定「斜杠命令没多大用」→ 全部删除:面板里每一项都有等价入口,
3052
+ // 而命令输出是纯文本快照,既不能操作也容易与面板状态不一致。
3053
+ // 保留这段注释是为了让「为什么没有 /mcp」这个问题有答案可查。
2697
3054
  },
2698
3055
  };