dsh-plugin-tool-management 0.1.3 → 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/README.md +108 -38
- package/README_EN.md +161 -37
- package/cordis.patch.yml +11 -0
- package/docs/Changelog.md +471 -0
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
- package/lib/approval-policy.js +46 -0
- package/lib/client.js +2288 -256
- package/lib/hub.js +94 -0
- package/lib/imports/upload.js +286 -0
- package/lib/index.js +586 -113
- package/lib/rules/archive-engine.js +191 -0
- package/lib/rules/archive.js +140 -0
- package/lib/rules/provider.js +121 -0
- package/lib/rules/service.js +2166 -0
- package/lib/skills/core.js +366 -23
- package/lib/skills/service.js +36 -10
- package/lib/subagents/service.js +333 -0
- package/lib/subagents/tools.js +62 -0
- package/package.json +20 -12
- package/screenshots.json +9 -0
package/lib/index.js
CHANGED
|
@@ -12,8 +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
|
+
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';
|
|
17
23
|
import { detectFormat, extractText, parseGenericText, parseJsonlTranscript, parseMarkdownTranscript } from './imports/parsers.js';
|
|
18
24
|
import { homedir } from 'node:os';
|
|
19
25
|
import { spawn } from 'node:child_process';
|
|
@@ -70,7 +76,7 @@ export default {
|
|
|
70
76
|
catch (e) { /* keep unknown */ }
|
|
71
77
|
// Optional access token (defense in depth for LAN exposure). Enabled by
|
|
72
78
|
// setting `config.token` on this plugin's loader row (profile
|
|
73
|
-
// cordis.patch.yml override) or the
|
|
79
|
+
// cordis.patch.yml override) or the DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN env var. When
|
|
74
80
|
// set, every state-changing op requires `x-dsh-token: <token>`. Read-only
|
|
75
81
|
// ops (plugin-version, mcpm-list, skill-state) stay open so the UI still
|
|
76
82
|
// renders; mcpm-export is guarded too because it leaks full configs.
|
|
@@ -79,24 +85,6 @@ export default {
|
|
|
79
85
|
// is not an injected service and throws "cannot get property without
|
|
80
86
|
// inject" at boot.
|
|
81
87
|
const TOKEN = String(config?.token || process.env.DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN || '').trim();
|
|
82
|
-
const WRITE_OPS = new Set([
|
|
83
|
-
'mcpm-add', 'mcpm-edit', 'mcpm-remove', 'mcpm-set-enabled', 'mcpm-set-all', 'mcpm-restart',
|
|
84
|
-
'mcpm-export', 'mcpm-import', 'mcpm-note', 'mcpm-settings', 'mcpm-tool-enabled',
|
|
85
|
-
// mcpm-reveal returns UNMASKED secrets; even though it is a read, it is
|
|
86
|
-
// token-gated like a write — on a LAN-exposed port the token must be the
|
|
87
|
-
// last line of defense for plaintext credentials too, not just writes.
|
|
88
|
-
'mcpm-reveal',
|
|
89
|
-
'skill-enable', 'skill-disable', 'skill-source-enable', 'skill-source-disable',
|
|
90
|
-
'skill-create', 'skill-import', 'skill-upload', 'skill-delete',
|
|
91
|
-
'skill-trash-restore', 'skill-trash-delete', 'skill-open',
|
|
92
|
-
'skill-custom-add', 'skill-custom-remove',
|
|
93
|
-
// agents-md 写操作(create/update/remove 改预设库;apply 写全局 AGENTS.md;import 从外部内容建预设)
|
|
94
|
-
'agentsmd-create', 'agentsmd-update', 'agentsmd-apply', 'agentsmd-remove', 'agentsmd-import',
|
|
95
|
-
// history 写操作(archive/unarchive 改归档集合;delete 永久删除;retention-set 写保留期)
|
|
96
|
-
'history-archive', 'history-unarchive', 'history-delete', 'history-retention-set',
|
|
97
|
-
'history-unarchive-batch', 'history-delete-batch', 'history-import', 'history-export',
|
|
98
|
-
'history-archive-batch',
|
|
99
|
-
]);
|
|
100
88
|
const wait = (ms) => ctx.timeout(ms);
|
|
101
89
|
const message = (e) => String((e && e.message) || e);
|
|
102
90
|
let writeChain = Promise.resolve();
|
|
@@ -119,9 +107,11 @@ export default {
|
|
|
119
107
|
}
|
|
120
108
|
// ---------- agents-md 预设库 + 切换 ----------
|
|
121
109
|
// DSH 全局指令基线只有 ~/.dsh/AGENTS.md 一个文件,无内置多预设切换;
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// presetsDir 可由 config
|
|
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 安装下会被覆盖,放用户数据在那里本身就丢数据风险。
|
|
125
115
|
const PLUGIN_ROOT = (() => {
|
|
126
116
|
try {
|
|
127
117
|
return dirname(createRequire(import.meta.url).resolve('../package.json'));
|
|
@@ -130,7 +120,10 @@ export default {
|
|
|
130
120
|
return process.cwd();
|
|
131
121
|
}
|
|
132
122
|
})();
|
|
133
|
-
const
|
|
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);
|
|
134
127
|
const agentsMdService = createAgentsMdService(ctx, {
|
|
135
128
|
presetsDir: agentsMdPresetsDir,
|
|
136
129
|
getGlobalAgentsMdPath: async () => {
|
|
@@ -139,6 +132,194 @@ export default {
|
|
|
139
132
|
return p.home + sep + 'AGENTS.md';
|
|
140
133
|
},
|
|
141
134
|
});
|
|
135
|
+
// ---------- rules(规则/记忆,v0.4)----------
|
|
136
|
+
// 记忆真源 $DSH_HOME/tool-management/memories/<场景>/<name>.md(仅用户级,D1)。
|
|
137
|
+
// **场景是显式记录**(rules-index.json 的 scenes 切片):含保留场景 `global`(界面「全局」,
|
|
138
|
+
// 其记忆注入任何对话),空场景也合法存在——不再是"恰好有这个目录名"的隐式约定。
|
|
139
|
+
// 单投影 = 活动场景记忆 → per-agent systemPrompt 段(自动在场,模型无需调用任何工具)。
|
|
140
|
+
// 原"始终层写 ~/.dsh/AGENTS.md"已下线(变更单 01 §4/§10):公共基线由 _shared/ 承担。
|
|
141
|
+
// 旧的 $DSH_HOME/scene-memory 与 $DSH_HOME/rules 由服务在首次读盘前搬入本目录。
|
|
142
|
+
// rulesRoot / rulesStateDir / rulesScenesDir 仅测试注入,生产留空由服务按 DSH_HOME 解析。
|
|
143
|
+
const rulesService = createRulesService(ctx, {
|
|
144
|
+
rulesRoot: String(config?.rulesRoot || ''),
|
|
145
|
+
stateDir: String(config?.rulesStateDir || ''),
|
|
146
|
+
scenesDir: String(config?.rulesScenesDir || ''),
|
|
147
|
+
// 场景记忆段预算(字节),默认 65536;仅用于测试与特殊部署调优。
|
|
148
|
+
...(Number.isFinite(Number(config?.rulesMaxBytes))
|
|
149
|
+
? { maxBytes: Number(config.rulesMaxBytes) }
|
|
150
|
+
: {}),
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
ctx.effect(() => rulesService.registerProviders(), 'dsh-plugin-tool-management: rules providers');
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.error('[dsh-plugin-tool-management] rules provider setup failed:', message(e));
|
|
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
|
+
]);
|
|
142
323
|
// ---------- history(归档会话管理,折叠自 dsh-archive-manager)----------
|
|
143
324
|
// cordis.patch.yml 禁用官方 workspace 与 session-projection-cache,插入本插件
|
|
144
325
|
// 的归档感知子类(lib/history/workspace.js + lib/history/projcache.js)。子类
|
|
@@ -171,15 +352,10 @@ export default {
|
|
|
171
352
|
}
|
|
172
353
|
}
|
|
173
354
|
async function writeHistoryRetention(retentionDays) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await writeFile(historyRetentionPath, JSON.stringify({ retentionDays, updatedAt: Date.now() }), 'utf8');
|
|
179
|
-
}
|
|
180
|
-
catch (e) {
|
|
181
|
-
console.error('[dsh-plugin-tool-management] write history-retention failed:', message(e));
|
|
182
|
-
}
|
|
355
|
+
const dir = dirname(historyRetentionPath);
|
|
356
|
+
await mkdir(dir, { recursive: true });
|
|
357
|
+
// updatedAt = 修改时刻:每次改保留期,已归档会话的到期基线重置为此时刻。
|
|
358
|
+
await writeFile(historyRetentionPath, JSON.stringify({ retentionDays, updatedAt: Date.now() }), 'utf8');
|
|
183
359
|
}
|
|
184
360
|
/**
|
|
185
361
|
* 计算到期应删的归档会话。基线 = max(archivedAt(账本)?? createdAt(元数据),
|
|
@@ -899,7 +1075,7 @@ export default {
|
|
|
899
1075
|
return { ok: true, notes: map };
|
|
900
1076
|
});
|
|
901
1077
|
}
|
|
902
|
-
const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0 };
|
|
1078
|
+
const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0, requireConfirmForModelRuleWrite: true, requireConfirmForModelSubagentRun: true };
|
|
903
1079
|
let pluginSettingsCache = null;
|
|
904
1080
|
function clampInt(value, min, max, fallback) {
|
|
905
1081
|
// Number(null) is 0 — treat missing/empty input as "use the default".
|
|
@@ -921,6 +1097,14 @@ export default {
|
|
|
921
1097
|
pollIntervalMs: clampInt(raw && raw.pollIntervalMs, 2000, 60000, SETTINGS_DEFAULTS.pollIntervalMs),
|
|
922
1098
|
// 0 = keep descriptions in full (the UI default); > 0 truncates.
|
|
923
1099
|
toolDescriptionMaxLength: clampInt(raw && raw.toolDescriptionMaxLength, 0, 2000, SETTINGS_DEFAULTS.toolDescriptionMaxLength),
|
|
1100
|
+
// 模型写规则需确认(D2):程序化强制,只读设置供 tools/pre-execute 判定。
|
|
1101
|
+
requireConfirmForModelRuleWrite: (raw && typeof raw.requireConfirmForModelRuleWrite === 'boolean')
|
|
1102
|
+
? raw.requireConfirmForModelRuleWrite
|
|
1103
|
+
: SETTINGS_DEFAULTS.requireConfirmForModelRuleWrite,
|
|
1104
|
+
// 子代理运行花真 token:默认确认,设置可关(设计 §3.2)。
|
|
1105
|
+
requireConfirmForModelSubagentRun: (raw && typeof raw.requireConfirmForModelSubagentRun === 'boolean')
|
|
1106
|
+
? raw.requireConfirmForModelSubagentRun
|
|
1107
|
+
: SETTINGS_DEFAULTS.requireConfirmForModelSubagentRun,
|
|
924
1108
|
},
|
|
925
1109
|
};
|
|
926
1110
|
return pluginSettingsCache.value;
|
|
@@ -932,6 +1116,12 @@ export default {
|
|
|
932
1116
|
const next = {
|
|
933
1117
|
pollIntervalMs: clampInt(args.pollIntervalMs, 2000, 60000, current.pollIntervalMs),
|
|
934
1118
|
toolDescriptionMaxLength: clampInt(args.toolDescriptionMaxLength, 0, 2000, current.toolDescriptionMaxLength),
|
|
1119
|
+
requireConfirmForModelRuleWrite: (args && typeof args.requireConfirmForModelRuleWrite === 'boolean')
|
|
1120
|
+
? args.requireConfirmForModelRuleWrite
|
|
1121
|
+
: current.requireConfirmForModelRuleWrite,
|
|
1122
|
+
requireConfirmForModelSubagentRun: (args && typeof args.requireConfirmForModelSubagentRun === 'boolean')
|
|
1123
|
+
? args.requireConfirmForModelSubagentRun
|
|
1124
|
+
: current.requireConfirmForModelSubagentRun,
|
|
935
1125
|
};
|
|
936
1126
|
const p = await ensurePaths();
|
|
937
1127
|
return withWriteLock(async () => {
|
|
@@ -963,7 +1153,9 @@ export default {
|
|
|
963
1153
|
for (const serverName of Object.keys(raw)) {
|
|
964
1154
|
const list = raw[serverName];
|
|
965
1155
|
if (Array.isArray(list)) {
|
|
966
|
-
|
|
1156
|
+
// `*` 必须原样保留:它是「整台服务器停用」的通配(场景档案未勾选的服务器写的就是它),
|
|
1157
|
+
// 被这里过滤掉的话 guard / restrict / 快照三条链路一起失效——整台停用变成空转。
|
|
1158
|
+
const names = list.map((name) => String(name)).filter((name) => name === '*' || /^[A-Za-z0-9_-]{1,128}$/.test(name));
|
|
967
1159
|
if (names.length)
|
|
968
1160
|
out[serverName] = names;
|
|
969
1161
|
}
|
|
@@ -981,6 +1173,21 @@ export default {
|
|
|
981
1173
|
}
|
|
982
1174
|
return out;
|
|
983
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
|
+
}
|
|
984
1191
|
async function mcpmToolEnabled(args) {
|
|
985
1192
|
const serverName = String((args && args.serverName) || '').trim();
|
|
986
1193
|
const tool = String((args && args.tool) || '').trim();
|
|
@@ -1024,6 +1231,7 @@ export default {
|
|
|
1024
1231
|
// Force re-read: this runs on the tools/change path, which is rare, so a
|
|
1025
1232
|
// stale cache must not keep an externally edited deny list hidden.
|
|
1026
1233
|
const map = await readDisabledTools(true);
|
|
1234
|
+
// 通配 `*`(整服务器停用)先展开成已注册的全名,再与精确名单合并。
|
|
1027
1235
|
const wanted = disabledToolNames(map);
|
|
1028
1236
|
let registered;
|
|
1029
1237
|
try {
|
|
@@ -1032,6 +1240,14 @@ export default {
|
|
|
1032
1240
|
catch (e) {
|
|
1033
1241
|
return;
|
|
1034
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
|
+
}
|
|
1035
1251
|
const names = wanted.filter((name) => registered.has(name));
|
|
1036
1252
|
if (restrictDisposer) {
|
|
1037
1253
|
try {
|
|
@@ -1140,7 +1356,9 @@ export default {
|
|
|
1140
1356
|
return { ok: false, error: message(e) };
|
|
1141
1357
|
}
|
|
1142
1358
|
const descriptionLimit = (await readPluginSettings()).toolDescriptionMaxLength;
|
|
1143
|
-
const
|
|
1359
|
+
const disabledMapHere = await readDisabledTools();
|
|
1360
|
+
const disabledHere = new Set(disabledMapHere[serverName] || []);
|
|
1361
|
+
const wildcardHere = disabledHere.has('*');
|
|
1144
1362
|
const toolsList = [];
|
|
1145
1363
|
for (const s of schemas) {
|
|
1146
1364
|
const fullName = String(s && s.name || '');
|
|
@@ -1169,7 +1387,7 @@ export default {
|
|
|
1169
1387
|
toolsList.push({
|
|
1170
1388
|
name: rawName,
|
|
1171
1389
|
description: truncateText(typeof s.description === 'string' ? s.description : '', descriptionLimit),
|
|
1172
|
-
enabled: !disabledHere.has(rawName),
|
|
1390
|
+
enabled: wildcardHere ? false : !disabledHere.has(rawName),
|
|
1173
1391
|
parameters: params,
|
|
1174
1392
|
});
|
|
1175
1393
|
}
|
|
@@ -1179,6 +1397,8 @@ export default {
|
|
|
1179
1397
|
// stay reachable; the description is unavailable once the schema is gone.
|
|
1180
1398
|
const listed = new Set(toolsList.map((t) => t.name));
|
|
1181
1399
|
for (const name of disabledHere) {
|
|
1400
|
+
if (name === '*')
|
|
1401
|
+
continue; // 整服务器通配:已在上方逐工具体现,不再展示占位行
|
|
1182
1402
|
if (!listed.has(name)) {
|
|
1183
1403
|
toolsList.push({ name, description: '(已停用;描述暂不可用)', enabled: false, parameters: [] });
|
|
1184
1404
|
}
|
|
@@ -1759,6 +1979,168 @@ export default {
|
|
|
1759
1979
|
},
|
|
1760
1980
|
};
|
|
1761
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
|
+
}
|
|
1762
2144
|
const handlers = {
|
|
1763
2145
|
'plugin-version': pluginVersion,
|
|
1764
2146
|
'mcpm-list': mcpmListView,
|
|
@@ -1782,6 +2164,59 @@ export default {
|
|
|
1782
2164
|
// skill-delete / skill-trash-restore / skill-trash-delete
|
|
1783
2165
|
// 成功返回 {ok:true, data};core 业务失败原样透传 {ok:false, error, code?, params?}。
|
|
1784
2166
|
...skillsService.ops,
|
|
2167
|
+
// rules ops(由 ./rules/service.js 提供):rules-list / rules-read / rules-budget /
|
|
2168
|
+
// rules-diagnose / rules-create / rules-update / rules-remove / rules-restore /
|
|
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。
|
|
2172
|
+
// 成功返回扁平 {ok:true, ...}(不套 data),失败 {ok:false, error, code?}。
|
|
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
|
+
},
|
|
1785
2220
|
// AGENTS.md 预设库 ops(由 ./agents-md/service.js 提供):agentsmd-list /
|
|
1786
2221
|
// agentsmd-read / agentsmd-create / agentsmd-update / agentsmd-remove /
|
|
1787
2222
|
// agentsmd-apply / agentsmd-get-current
|
|
@@ -2191,7 +2626,12 @@ export default {
|
|
|
2191
2626
|
const days = Number((args && args.retentionDays) ?? -1);
|
|
2192
2627
|
if (!Number.isFinite(days) || days < 0)
|
|
2193
2628
|
return { ok: false, error: 'retentionDays 需为非负整数(0=永久不删除)' };
|
|
2194
|
-
|
|
2629
|
+
try {
|
|
2630
|
+
await writeHistoryRetention(Math.floor(days));
|
|
2631
|
+
}
|
|
2632
|
+
catch (e) {
|
|
2633
|
+
return { ok: false, error: '写入保留期失败: ' + message(e) };
|
|
2634
|
+
}
|
|
2195
2635
|
// 设置变更后立即扫一次,UI 反映新策略。
|
|
2196
2636
|
await sweepHistory().catch(() => { });
|
|
2197
2637
|
return { ok: true, retentionDays: Math.floor(days) };
|
|
@@ -2349,13 +2789,112 @@ export default {
|
|
|
2349
2789
|
return 'OK: preset ' + args.id + ' applied to ~/.dsh/AGENTS.md (next session; current session unchanged' + (r.backedUp ? '; previous backed up to __last-applied__' : '') + ')';
|
|
2350
2790
|
},
|
|
2351
2791
|
}));
|
|
2792
|
+
// ---------- rules model tools(v0.4)----------
|
|
2793
|
+
// 活动场景的记忆正文会自动进入系统提示词(无需调用工具读取);这里的工具用于
|
|
2794
|
+
// 查询/编辑规则本身。rule_manager_write 受 tools/pre-execute 审批门禁(D2)。
|
|
2795
|
+
// 路径锚点:$DSH_HOME/tool-management/memories/<场景>/…(场景 `global` = 界面「全局」)。
|
|
2796
|
+
tools.register(defineTool({
|
|
2797
|
+
name: 'rule_manager_list',
|
|
2798
|
+
description: 'List memories under ~/.dsh/tool-management/memories (id, scene, enabled, description).',
|
|
2799
|
+
parameters: {
|
|
2800
|
+
group: { type: 'string', description: 'Optional scene filter.' },
|
|
2801
|
+
},
|
|
2802
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2803
|
+
async execute(args) {
|
|
2804
|
+
const r = await rulesService.ops['rules-list'](args);
|
|
2805
|
+
if (!r || r.ok === false)
|
|
2806
|
+
throw new Error((r && r.error) || '读取规则失败');
|
|
2807
|
+
const lines = (r.rules || []).map((x) => ('- ' + x.id + ' [' + (x.group || '未归属场景') + '] ' + (x.enabled ? '已启用' : '已停用') +
|
|
2808
|
+
(x.description ? ' — ' + x.description : '')));
|
|
2809
|
+
const scenes = (r.scenes || []).map((s) => (s.label || s.name) + (s.active ? '(启用)' : '(未启用)')).join('、');
|
|
2810
|
+
return '记忆(' + (r.rules || []).length + '):\n' + (lines.join('\n') || '(无记忆)') +
|
|
2811
|
+
'\n场景:' + (scenes || '(无)') + (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)');
|
|
2812
|
+
},
|
|
2813
|
+
}));
|
|
2814
|
+
tools.register(defineTool({
|
|
2815
|
+
name: 'rule_manager_read',
|
|
2816
|
+
description: 'Read the full body of one memory under ~/.dsh/tool-management/memories.',
|
|
2817
|
+
parameters: {
|
|
2818
|
+
id: { type: 'string', required: true, description: 'Memory id like <scene>/<name>.' },
|
|
2819
|
+
},
|
|
2820
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2821
|
+
async execute(args) {
|
|
2822
|
+
const r = await rulesService.ops['rules-read'](args);
|
|
2823
|
+
if (!r || r.ok === false)
|
|
2824
|
+
throw new Error((r && r.error) || '读取规则失败');
|
|
2825
|
+
return '# ' + r.rule.id + '\n\n' + (r.rule.body || '');
|
|
2826
|
+
},
|
|
2827
|
+
}));
|
|
2828
|
+
tools.register(defineTool({
|
|
2829
|
+
name: 'rule_manager_write',
|
|
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).',
|
|
2831
|
+
parameters: {
|
|
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 < > : " | ? *).' },
|
|
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.' },
|
|
2834
|
+
description: { type: 'string', required: true, description: 'One-sentence description (<=500 chars).' },
|
|
2835
|
+
body: { type: 'string', required: true, description: 'Markdown body (<=256 KiB).' },
|
|
2836
|
+
},
|
|
2837
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2838
|
+
async execute(args) {
|
|
2839
|
+
const r = await rulesService.ops['rules-create'](args);
|
|
2840
|
+
if (!r || r.ok === false)
|
|
2841
|
+
throw new Error((r && r.error) || '创建规则失败');
|
|
2842
|
+
return 'OK: memory ' + r.rule.id + '(场景「' + (r.rule.group || '未归属') + '」启用后自动生效)';
|
|
2843
|
+
},
|
|
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
|
+
}
|
|
2352
2856
|
if (typeof ctx.on === 'function') {
|
|
2353
|
-
|
|
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
|
+
};
|
|
2354
2875
|
ctx.on('tools/pre-execute', (exec, next) => {
|
|
2355
|
-
if (exec
|
|
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') {
|
|
2356
2881
|
return Promise.resolve({ kind: 'ask', reason: 'Create a new skill under DSH_HOME/skills' });
|
|
2357
2882
|
}
|
|
2358
|
-
|
|
2883
|
+
if (exec.name === 'rule_manager_write') {
|
|
2884
|
+
// D2:模型写规则默认需确认;设置关闭后直接放行。ask 无应答者时降级为拒绝(fail-closed),
|
|
2885
|
+
// 不在此处做任何兜底放行。
|
|
2886
|
+
return readPluginSettings()
|
|
2887
|
+
.then((s) => (s.requireConfirmForModelRuleWrite
|
|
2888
|
+
? { kind: 'ask', reason: 'Write a memory under ~/.dsh/tool-management/memories' }
|
|
2889
|
+
: 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)' }));
|
|
2359
2898
|
});
|
|
2360
2899
|
}
|
|
2361
2900
|
// ---------- per-tool MCP switches: execution guard + visibility ----------
|
|
@@ -2368,7 +2907,7 @@ export default {
|
|
|
2368
2907
|
try {
|
|
2369
2908
|
// Reads the TTL cache without I/O: the guard runs on the hot path.
|
|
2370
2909
|
const value = disabledToolsCache ? disabledToolsCache.value : {};
|
|
2371
|
-
if (
|
|
2910
|
+
if (isToolDisabled(value, String((exec && exec.name) || ''))) {
|
|
2372
2911
|
return '该工具已在 MCP 管理页停用';
|
|
2373
2912
|
}
|
|
2374
2913
|
}
|
|
@@ -2507,76 +3046,10 @@ export default {
|
|
|
2507
3046
|
console.error('[dsh-plugin-tool-management] webServer route registration failed:', message(e));
|
|
2508
3047
|
}
|
|
2509
3048
|
}
|
|
2510
|
-
// ----------
|
|
2511
|
-
//
|
|
2512
|
-
//
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
if (commands && typeof commands.register === 'function') {
|
|
2516
|
-
ctx.effect(() => commands.register({
|
|
2517
|
-
name: 'mcp',
|
|
2518
|
-
description: '列出已配置的 MCP 服务(级别 / 启停 / loader 状态 / 工具数)',
|
|
2519
|
-
handler: async () => {
|
|
2520
|
-
const r = await mcpmList();
|
|
2521
|
-
if (!r || r.ok === false)
|
|
2522
|
-
return { kind: 'error', text: (r && r.error) || '读取 MCP 服务失败' };
|
|
2523
|
-
// 启用的在前,同组内按名称排序。
|
|
2524
|
-
const rows = (r.rows || []).slice().sort((a, b) => {
|
|
2525
|
-
if (!!a.disabled !== !!b.disabled)
|
|
2526
|
-
return a.disabled ? 1 : -1;
|
|
2527
|
-
return String(a.serverName).localeCompare(String(b.serverName));
|
|
2528
|
-
});
|
|
2529
|
-
if (!rows.length)
|
|
2530
|
-
return { kind: 'success', text: '未配置任何 MCP 服务。' };
|
|
2531
|
-
// Human-facing command: show localized levels, not the raw keys.
|
|
2532
|
-
const labels = { project: 'Profile 级', global: '全局', loader: '已加载' };
|
|
2533
|
-
const text = 'MCP 服务(' + rows.length + '):\n' + rows.map((row) => ('- ' + row.serverName + ' [' + (labels[row.level] || row.level) + '] ' + (row.disabled ? '已停用' : '已启用') +
|
|
2534
|
-
(row.live ? ' · loader:' + (row.live.enabled ? 'on' : 'off') + (row.live.phase ? '/' + row.live.phase : '') : '') +
|
|
2535
|
-
(typeof row.toolCount === 'number' ? ' · ' + row.toolCount + ' 工具' : ''))).join('\n');
|
|
2536
|
-
return { kind: 'success', text };
|
|
2537
|
-
},
|
|
2538
|
-
}), 'dsh-plugin-tool-management: /mcp command');
|
|
2539
|
-
ctx.effect(() => commands.register({
|
|
2540
|
-
name: 'skills',
|
|
2541
|
-
description: '列出各来源的技能及其启停状态',
|
|
2542
|
-
handler: async () => {
|
|
2543
|
-
const r = await skillsService.ops['skill-state']({});
|
|
2544
|
-
if (!r || r.ok === false)
|
|
2545
|
-
return { kind: 'error', text: (r && r.error) || '读取技能失败' };
|
|
2546
|
-
const data = r.data || {};
|
|
2547
|
-
const entries = [];
|
|
2548
|
-
for (const root of data.roots || []) {
|
|
2549
|
-
for (const skill of root.skills || []) {
|
|
2550
|
-
entries.push({
|
|
2551
|
-
line: '- ' + (skill.declaredName || skill.name) + ' [' + (root.label || root.key) + '] ' + (skill.enabled === false ? '已停用' : '已启用'),
|
|
2552
|
-
enabled: skill.enabled !== false,
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
// 启用的在前,同组内保持原有顺序(来源与名称序)。
|
|
2557
|
-
entries.sort((a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1));
|
|
2558
|
-
const lines = entries.map((entry) => entry.line);
|
|
2559
|
-
return { kind: 'success', text: lines.length ? '技能(' + lines.length + '):\n' + lines.join('\n') : '未发现任何技能。' };
|
|
2560
|
-
},
|
|
2561
|
-
}), 'dsh-plugin-tool-management: /skills command');
|
|
2562
|
-
ctx.effect(() => commands.register({
|
|
2563
|
-
name: 'agents-md',
|
|
2564
|
-
description: '列出 AGENTS.md 预设及当前生效(应用后新会话生效,当前会话不变)',
|
|
2565
|
-
handler: async () => {
|
|
2566
|
-
const r = await agentsMdService.list();
|
|
2567
|
-
if (!r || r.ok === false)
|
|
2568
|
-
return { kind: 'error', text: (r && r.error) || '读取 AGENTS.md 预设失败' };
|
|
2569
|
-
const presets = r.presets || [];
|
|
2570
|
-
if (!presets.length)
|
|
2571
|
-
return { kind: 'success', text: '未发现任何 AGENTS.md 预设。' };
|
|
2572
|
-
const text = 'AGENTS.md 预设(' + presets.length + '):\n' + presets.map((p) => ('- ' + p.id + (p.active ? ' [生效中]' : ''))).join('\n') + '\n(应用后新会话生效,当前会话不变)';
|
|
2573
|
-
return { kind: 'success', text };
|
|
2574
|
-
},
|
|
2575
|
-
}), 'dsh-plugin-tool-management: /agents-md command');
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
catch (e) {
|
|
2579
|
-
console.error('[dsh-plugin-tool-management] command registration failed:', message(e));
|
|
2580
|
-
}
|
|
3049
|
+
// ---------- 斜杠命令:已下线 ----------
|
|
3050
|
+
// 曾有 /mcp、/skills、/agents-md、/scene-memory 四条(注册到 commands 服务)。
|
|
3051
|
+
// 用户 2026-09-13 判定「斜杠命令没多大用」→ 全部删除:面板里每一项都有等价入口,
|
|
3052
|
+
// 而命令输出是纯文本快照,既不能操作也容易与面板状态不一致。
|
|
3053
|
+
// 保留这段注释是为了让「为什么没有 /mcp」这个问题有答案可查。
|
|
2581
3054
|
},
|
|
2582
3055
|
};
|