dsh-plugin-tool-management 0.10.0 → 0.12.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 (77) hide show
  1. package/CHANGELOG.md +100 -1
  2. package/README.md +67 -50
  3. package/README_EN.md +61 -38
  4. package/cordis.patch.yml +10 -1
  5. package/docs/images/1-EN.png +0 -0
  6. package/docs/images/1.png +0 -0
  7. package/docs/images/2-EN.png +0 -0
  8. package/docs/images/2.png +0 -0
  9. package/docs/images/3-EN.png +0 -0
  10. package/docs/images/3.png +0 -0
  11. package/docs/images/4-EN.png +0 -0
  12. package/docs/images/4.png +0 -0
  13. package/docs/images/5-EN.png +0 -0
  14. package/docs/images/5.png +0 -0
  15. package/docs/images/6-EN.png +0 -0
  16. package/docs/images/6.png +0 -0
  17. package/docs/images/7-EN.png +0 -0
  18. package/docs/images/7.png +0 -0
  19. package/docs/images/8-EN.png +0 -0
  20. package/docs/images/8.png +0 -0
  21. package/docs/update.md +132 -12
  22. package/lib/client.js +2826 -547
  23. package/lib/compat/patch-dialect.js +173 -0
  24. package/lib/compat/preset-reach.js +1 -10
  25. package/lib/compat/probe.js +205 -24
  26. package/lib/compat/runtime-notes.js +25 -0
  27. package/lib/context-inject.js +83 -9
  28. package/lib/host-names.js +12 -0
  29. package/lib/http-fence.js +35 -15
  30. package/lib/hub.js +28 -2
  31. package/lib/imports/parsers.js +15 -9
  32. package/lib/imports/upload.js +43 -4
  33. package/lib/index.js +927 -3897
  34. package/lib/mcp/loader-token.js +238 -0
  35. package/lib/mcp/manager.js +1769 -0
  36. package/lib/mcp/override-blocks.js +10 -3
  37. package/lib/mcp/patch-yaml.js +351 -0
  38. package/lib/mcp/secret-guard.js +145 -0
  39. package/lib/{rules → memories}/archive-engine.js +1 -1
  40. package/lib/{rules → memories}/archive.js +1 -1
  41. package/lib/memories/constants.js +128 -0
  42. package/lib/memories/index-io.js +330 -0
  43. package/lib/memories/projection.js +280 -0
  44. package/lib/memories/service.js +686 -0
  45. package/lib/memories/snapshot.js +672 -0
  46. package/lib/ops/candidates.js +64 -0
  47. package/lib/ops/compat.js +226 -0
  48. package/lib/ops/ctx.js +9 -0
  49. package/lib/ops/memory.js +678 -0
  50. package/lib/ops/prompts.js +107 -0
  51. package/lib/ops/scene-records.js +460 -0
  52. package/lib/ops/scene-sync.js +17 -0
  53. package/lib/ops/sessions.js +603 -0
  54. package/lib/ops/trash.js +140 -0
  55. package/lib/paths.js +103 -0
  56. package/lib/prompts/preset-id.js +49 -0
  57. package/lib/{agents-md → prompts}/service.js +1 -1
  58. package/lib/request-gate.js +320 -0
  59. package/lib/scene-prompt-sync.js +4 -4
  60. package/lib/scenes/candidates.js +344 -0
  61. package/lib/{history → sessions}/bridge.js +124 -36
  62. package/lib/sessions/history.js +323 -0
  63. package/lib/{history → sessions}/tombstone.js +1 -1
  64. package/lib/{history → sessions}/workspace.js +151 -50
  65. package/lib/skills/core.js +74 -39
  66. package/lib/skills/readonly-discovery.js +4 -1
  67. package/lib/skills/service.js +98 -13
  68. package/lib/subagents/service.js +199 -55
  69. package/lib/tools/deps.js +8 -0
  70. package/lib/tools/mcp.js +110 -0
  71. package/lib/tools/memory.js +87 -0
  72. package/lib/tools/prompt.js +70 -0
  73. package/lib/tools/skills.js +139 -0
  74. package/lib/tools/subagent.js +40 -0
  75. package/package.json +13 -10
  76. package/lib/agents-md/preset-id.js +0 -49
  77. package/lib/rules/service.js +0 -3078
@@ -0,0 +1,64 @@
1
+ // 场景档案与候选源域的 HTTP ops(2026-09-19 从 index.ts 的 handlers 表抽出)。
2
+ //
3
+ // 三个 op 都是**只读候选源**:preset-tools(人设表单的工具候选)、model-candidates
4
+ // (宿主 LLM 目录)、scene-inventory(场景档案勾选器的数据源 v2)。
5
+ export function buildCandidateOps(deps) {
6
+ return {
7
+ // 人设表单的两个候选源之一(只读,均为「按需拉取」——不进 scene-inventory,避免每次开
8
+ // 档案弹窗都枚举预设)。
9
+ // preset-tools:全体 Agent 预设工具名并集 + 各工具所属预设 + 当前会话是否可见。
10
+ // `unavailable` = 本插件自己没注册上的工具:工具列表接口是唯一按名字枚举工具的地方,
11
+ // 注册失败只能在这里被如实回答。
12
+ 'preset-tools': async () => ({
13
+ ok: true,
14
+ ...(await deps.presetToolCandidates()),
15
+ unavailable: deps.getToolFailures().map((f) => ({ ...f })),
16
+ }),
17
+ // model-candidates:宿主 LLM 目录里的 (provider, model) 对(不发网络请求)。
18
+ 'model-candidates': async () => ({ ok: true, ...(await deps.modelCandidates()) }),
19
+ // 场景档案勾选器数据源 v2:全部 MCP 服务器(含未运行)+ 技能全集 + 人设清单。
20
+ 'scene-inventory': async () => {
21
+ const [rowsR, tools, skills, subs] = await Promise.all([deps.mcpmListView(), deps.toolStates(), deps.skillRows(), deps.subagentList()]);
22
+ const disabledRaw = await deps.readDisabledTools();
23
+ const rows = (rowsR && rowsR.rows) || [];
24
+ const mcpServers = [];
25
+ const seen = new Set();
26
+ for (const row of rows) {
27
+ const name = String((row && row.serverName) || '');
28
+ if (!name || seen.has(name))
29
+ continue;
30
+ seen.add(name);
31
+ const live = !!(row.live && row.live.enabled);
32
+ mcpServers.push({
33
+ name,
34
+ level: row.level || null,
35
+ live,
36
+ serverDisabled: !!row.disabled,
37
+ // 停用表里的 ['*'] = 整台工具停用(勾选器据此预勾,避免把"全停"读成"全启用")。
38
+ allToolsDisabled: (disabledRaw[name] || []).indexOf('*') >= 0,
39
+ // 未运行且没有任何已知工具 ⇒ null(客户端显示「工具数未知」并引导读取),
40
+ // 不再给出误导性的 0。
41
+ toolCount: live || Number(row.toolCount) > 0
42
+ ? (typeof row.toolCount === 'number' ? row.toolCount : null)
43
+ : null,
44
+ });
45
+ }
46
+ return {
47
+ ok: true,
48
+ mcpServers,
49
+ tools: Object.entries(tools).map(([key, enabled]) => ({ key, enabled })),
50
+ // 技能行:技能名与来源名分开给(早先只给 `<来源 key>/<技能名>`,自定义目录的
51
+ // 来源 key 是 `custom-<hash>`,于是导入的技能在场景里显示成一串哈希)。
52
+ skills,
53
+ // 人设行要显示"按模式配了什么限制",所以把按模式名单一起带上;
54
+ // presets 只给名字与来源(不枚举工具,避免打开档案弹窗就挂载每个预设)。
55
+ subagents: subs.map((p) => ({ name: p.name, description: p.description, toolsByPreset: p.toolsByPreset ?? null })),
56
+ presets: await deps.presetNames(),
57
+ // 档案编辑器第 4 段「记忆」的候选:场景 + 每个场景里的记忆条数。
58
+ // 记忆正文不在这里返回(勾选集只存 id,渲染时才读盘)。
59
+ scenes: await deps.memorySceneCandidates(),
60
+ memories: await deps.memoryCandidates(),
61
+ };
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,226 @@
1
+ // 兼容 / 注入 / 备份域的 HTTP ops(2026-09-19 从 index.ts 的 handlers 表抽出)。
2
+ //
3
+ // 为什么抽出来:index.ts 的 apply() 闭包里塞了 40 个 op 的内联实现,改一处要在 700 行
4
+ // 的对象字面量里找。这里按域收成一个 `buildCompatOps(deps)` —— 依赖全部显式传参,
5
+ // 不再靠闭包隐式捕获,各域可以单独读、单独改。
6
+ //
7
+ // 边界纪律:本文件只负责「op 的实现」,不负责 ops 表之外的接线(MCP 写后刷新、场景锁定
8
+ // 守卫仍作用在组装后的整张表上,见 index.ts)。
9
+ import { EXPECTED_MIN_HOST_VERSION, EXPECTED_PEER_RANGE, VERIFIED_HOST_VERSION, routeFor, summarize, } from '../compat/probe.js';
10
+ import { INJECT_DOMAIN_KEYS } from '../context-inject.js';
11
+ import { assessPresetReach } from '../compat/preset-reach.js';
12
+ import { runtimeNotes } from '../compat/runtime-notes.js';
13
+ import { pluginLog } from '../skills/service.js';
14
+ export function buildCompatOps(deps) {
15
+ return {
16
+ 'compat-status': async (args) => {
17
+ const force = Boolean(args && args.refresh);
18
+ try {
19
+ const registry = deps.getSessionsRegistry();
20
+ const assessment = registry && typeof registry.capabilities === 'function'
21
+ ? registry.capabilities(force)
22
+ : undefined;
23
+ if (assessment === undefined) {
24
+ return {
25
+ ok: true,
26
+ host: { version: 'unknown' },
27
+ findings: [],
28
+ degraded: [],
29
+ blockers: ['归档服务未挂载:无法探测宿主能力'],
30
+ summary: '宿主能力探测不可用(归档服务未挂载)',
31
+ };
32
+ }
33
+ const summary = summarize(assessment);
34
+ // 运行时降级(A1 的补丁校验、A2 的投影缓存适配、B3 的装配层…)统一在这里并进
35
+ // findings/degraded:它们不是宿主能力,但必须各占一行 —— 否则用户只能在事故之后
36
+ // 才知道某道保险没生效。上报口径见 compat/runtime-notes.ts。
37
+ const extraFindings = runtimeNotes().map((note) => ({
38
+ id: note.id,
39
+ label: note.label,
40
+ kind: note.kind,
41
+ owner: 'plugin',
42
+ fallback: note.fallback,
43
+ state: 'not-available',
44
+ missing: [],
45
+ detail: note.detail,
46
+ }));
47
+ const findings = [...assessment.findings, ...extraFindings];
48
+ const degraded = [...assessment.degraded, ...extraFindings];
49
+ const merged = { ...assessment, findings, degraded };
50
+ const summaryMerged = extraFindings.length === 0 ? summary : summarize(merged);
51
+ // 每次刷新都留一条结构化日志:升级当天就能在日志里看到降级发生。
52
+ if (force || !deps.compatLog.version || deps.compatLog.version !== assessment.identity.version) {
53
+ deps.compatLog.version = assessment.identity.version;
54
+ void pluginLog()('compat/probe', {
55
+ hostVersion: assessment.identity.version,
56
+ summary: summaryMerged,
57
+ degraded: degraded.map((item) => ({ id: item.id, state: item.state, detail: item.detail })),
58
+ blockers: assessment.identity.blockers,
59
+ sameAsHost: assessment.identity.sameAsHost,
60
+ }).catch(() => { });
61
+ }
62
+ return {
63
+ ok: true,
64
+ host: { version: assessment.identity.version, modules: assessment.identity.modules },
65
+ sameAsHost: assessment.identity.sameAsHost,
66
+ unverified: assessment.identity.unverified,
67
+ findings,
68
+ degraded,
69
+ blockers: assessment.identity.blockers,
70
+ mayDelete: assessment.mayDelete,
71
+ verifiedVersion: VERIFIED_HOST_VERSION,
72
+ expectedPeerRange: EXPECTED_PEER_RANGE,
73
+ minHostVersion: EXPECTED_MIN_HOST_VERSION,
74
+ generatedAt: assessment.generatedAt,
75
+ summary: summaryMerged,
76
+ };
77
+ }
78
+ catch (e) {
79
+ return { ok: false, error: deps.message(e) };
80
+ }
81
+ },
82
+ // 预设可达性矩阵:每个 Agent 预设下,本插件的注入类能力到不到得了模型。
83
+ // 只读预设组合文本,不挂载任何预设、不改任何数据。回答的是"面板上说注入了,
84
+ // 模型真的看得到吗" —— 压制型预设(persona complete / includeRuntimeContext:false,
85
+ // 如极简)默认不注入,可在「注入」设置里改成强制注入。
86
+ 'preset-reach': async () => {
87
+ try {
88
+ // 宿主平面的 MCP 工具数:四个官方预设都不在组合里挂 MCP,MCP 由
89
+ // $DSH_HOME/cordis.patch.yml 这一层挂载,所以它对所有预设一视同仁;
90
+ // 这一列要回答的只是"现在有没有 MCP 工具可用"。探测失败 → 不传该字段,
91
+ // 矩阵那一格显示"判断不了"而不是猜。
92
+ let mcpTools;
93
+ try {
94
+ const schemas = (await deps.tools.schemas());
95
+ mcpTools = schemas.filter((s) => String(s && s.name || '').startsWith('mcp__')).length;
96
+ }
97
+ catch {
98
+ mcpTools = undefined;
99
+ }
100
+ const settings = await deps.readInjectSettings();
101
+ const report = await assessPresetReach(deps.presetRoster(), {
102
+ mcpTools,
103
+ inject: { underSuppressingPresets: settings.underSuppressingPresets, domains: settings.domains },
104
+ });
105
+ return { ok: true, ...report };
106
+ }
107
+ catch (e) {
108
+ return { ok: false, error: deps.message(e) };
109
+ }
110
+ },
111
+ // 注入设置(读 / 写):压制型预设下是否仍然注入 + 各域开关。界面在「兼容」页。
112
+ 'inject-settings': (args) => deps.injectSettingsOp(args),
113
+ // 功能总览(B6):**按功能点**回答"现在每一项到底能不能用",三层合成 ——
114
+ // ① 装配层(listener / provider / 适配是否真的挂上,来自运行时上报通道)
115
+ // ② 宿主能力层(compat 探测的路由判定)
116
+ // ③ 用户配置层(开关 / 令牌 / 场景锁定)
117
+ // 数据全部来自已有的缓存状态与运行时上报,**不新增宿主探测**(避免哨兵真调一类的开销)。
118
+ // 行内 `tab` 供界面跳转到对应页签;`state` 取值见下方约定。
119
+ 'feature-overview': async () => {
120
+ try {
121
+ const registry = deps.getSessionsRegistry();
122
+ const assessment = registry && typeof registry.capabilities === 'function' ? registry.capabilities() : undefined;
123
+ const notes = new Map(runtimeNotes().map((note) => [note.id, note]));
124
+ const settings = await deps.readInjectSettings();
125
+ const domainsOn = INJECT_DOMAIN_KEYS.filter((key) => settings.domains[key] !== false);
126
+ const token = await deps.readTokenState();
127
+ const lockedScenes = await deps.lockedSceneNames();
128
+ const disabledTools = deps.disabledToolCount();
129
+ const routeOf = (operation) => assessment === undefined ? undefined : routeFor(assessment, operation);
130
+ /** 一行一个功能点:状态三层合成,`detail` 写明依据。 */
131
+ const rows = [];
132
+ const push = (key, label, tab, state, detail) => rows.push({ key, label, tab, state, detail });
133
+ // 前置项:插件挂载(这一行本身说明 12 个 inject 都解析了 —— 否则 apply 根本不会跑);
134
+ // 启动期那段 `!!js` 表达式若抛错,插件同样不会挂上(它的 try/catch 就是为了不抛)。
135
+ const mountNote = notes.get('cordis-original-symbol');
136
+ push('mount', '插件挂载', 'compat', mountNote === undefined ? 'ok' : 'degraded', mountNote === undefined ? '12 个 inject 服务全部解析,插件已挂载(详见 doctor 的挂载心跳)' : mountNote.detail);
137
+ // 装配层:有上报就是降级,附上报里的原因;没有就是正常。
138
+ const assemblyRows = [
139
+ ['patch-write-guard', '宿主配置写入(补丁校验)', 'mcp'],
140
+ ['context-injection', '上下文注入通道', 'compat'],
141
+ ['official-suppression', '官方注入的关域拦截', 'compat'],
142
+ ['skills-provider', '技能 provider 装配', 'skills'],
143
+ ['projection-cache-adapter', '投影缓存删除屏障', 'sessions'],
144
+ ['mcp-tool-visibility', '停用工具的可见性', 'mcp'],
145
+ ];
146
+ for (const [noteId, label, tab] of assemblyRows) {
147
+ const note = notes.get(noteId);
148
+ push(noteId, label, tab, note === undefined ? 'ok' : 'degraded', note === undefined ? '装配正常,无降级上报' : note.detail);
149
+ }
150
+ // 宿主能力层:删除 / 归档 / 恢复 / 批量各自由路由判定回答(与服务端执行同源)。
151
+ const deleteRoute = routeOf('delete');
152
+ push('session-delete', '会话删除', 'sessions', deleteRoute === undefined ? 'unknown' : deleteRoute.via === 'none' ? 'unavailable' : 'ok', deleteRoute === undefined ? '能力探测不可用(归档服务未挂载)'
153
+ : deleteRoute.via === 'none' ? '宿主缺少该路径所需能力:' + deleteRoute.refusals.map((item) => item.label).join('、')
154
+ : `可用(路径:${deleteRoute.via})`);
155
+ for (const [operation, label] of [['archive', '归档'], ['unarchive', '恢复'], ['batch', '批量操作'], ['list', '历史列表']]) {
156
+ const decision = routeOf(operation);
157
+ push('session-' + operation, `会话${label}`, 'sessions', decision === undefined ? 'unknown' : decision.via === 'none' ? 'unavailable' : 'ok', decision === undefined ? '能力探测不可用(归档服务未挂载)'
158
+ : decision.via === 'none' ? '宿主缺少该路径所需能力:' + decision.refusals.map((item) => item.label).join('、')
159
+ : `可用(路径:${decision.via})`);
160
+ }
161
+ // 用户配置层:注入域 / 令牌 / 场景锁定 / 停用工具数。
162
+ push('injection-domains', '注入域开关', 'compat', domainsOn.length === 0 ? 'disabled' : 'ok', domainsOn.length === 0 ? '五个注入域全部关闭(用户设置)' : `${domainsOn.length}/5 个域开启:${domainsOn.join('、')}`);
163
+ push('token', '访问令牌', 'compat', !token.active ? 'disabled' : token.accepted ? 'ok' : 'locked', !token.active ? '未启用(宿主没配令牌,或令牌功能被关掉)'
164
+ : token.accepted ? '已生效且本次启动已通过验证' : '已生效,本次启动尚未验证:写操作与对话会被拦住,到本页下方填写令牌');
165
+ push('scene-lock', '场景锁定', 'scenes', lockedScenes.length === 0 ? 'ok' : 'locked', lockedScenes.length === 0 ? '无锁定场景' : `锁定中:${lockedScenes.join('、')}(写门禁按锁定场景生效)`);
166
+ push('mcp-tools', 'MCP 工具停用', 'mcp', disabledTools === 0 ? 'ok' : 'partial', disabledTools === 0 ? '没有停用的工具' : `${disabledTools} 个工具处于停用态(执行拦截 + 可见性摘除)`);
167
+ push('native-delete', '宿主原生删除入口', 'compat', notes.has('workspace.delete-native') ? 'partial' : 'ok', notes.get('workspace.delete-native')?.detail ?? '宿主未提供原生删除入口(本插件自有完整序列)');
168
+ // 身份 / 版本:与 compat-status 同一份数据。
169
+ const identity = assessment?.identity;
170
+ const identityIssue = identity !== undefined && (identity.blockers.length > 0 || Object.values(identity.sameAsHost).some((same) => same === false));
171
+ push('host-identity', '宿主身份与模块', 'compat', assessment === undefined ? 'unknown' : identityIssue ? 'degraded' : 'ok', assessment === undefined ? '能力探测不可用(归档服务未挂载)'
172
+ : identityIssue ? '存在阻塞项:' + (identity?.blockers ?? []).join(';')
173
+ : `模块与宿主同源(宿主 DSH ${identity?.version ?? '?'})`);
174
+ // 无独立装配点的域:没有上报就是正常(如实说明依据是"没有降级上报")。
175
+ for (const [key, label, tab] of [['memory', '记忆', 'memory'], ['prompts', '提示词', 'prompts'], ['subagents', '子智能体', 'subagents'], ['scenes', '场景档案', 'scenes']]) {
176
+ push(key, label, tab, 'ok', '无降级上报(装配失败会出现在这里)');
177
+ }
178
+ return { ok: true, rows, generatedAt: Date.now() };
179
+ }
180
+ catch (e) {
181
+ return { ok: false, error: deps.message(e) };
182
+ }
183
+ },
184
+ // patch 备份(改宿主配置前的整份副本):列清单只读,清理按写门禁。
185
+ // 每份备份里都是**明文**凭据,所以给一个显式出口让人能删掉多余的副本(理由见
186
+ // cleanPatchBackups 的注释)。清单不带文件内容,只给名字 / 层级 / 时间 / 大小。
187
+ 'backups-list': async () => {
188
+ const files = await deps.listPatchBackups();
189
+ return { ok: true, files, total: files.length, totalSize: files.reduce((s, f) => s + f.size, 0) };
190
+ },
191
+ 'backups-clean': async (args) => {
192
+ // 口径是**每个层级各删最旧的几份**:`del` 是 `{ 层级: 份数 }`,键与 listPatchBackups
193
+ // 给的层级标签同源(global / profile:<名> / legacy)。认不出的键天然落空(没有这个
194
+ // 层级就没得删),非法值一律忽略 —— 参数畸形时**一份都不删**,这是安全的默认。
195
+ const raw = (args && args.del) || {};
196
+ const del = {};
197
+ if (raw && typeof raw === 'object') {
198
+ for (const key of Object.keys(raw)) {
199
+ const n = Number(raw[key]);
200
+ if (Number.isSafeInteger(n) && n >= 1 && n <= 20)
201
+ del[key] = n;
202
+ }
203
+ }
204
+ const r = await deps.cleanPatchBackups(del);
205
+ // 失败原因(文件被占用等)由界面按当前语言拼:这里只回结构化字段。回中文串会让
206
+ // 英文界面原样露出中文(与 compat-status 的 summary 同一条纪律)。
207
+ return {
208
+ ok: true, removed: r.removed.length, kept: r.kept,
209
+ ...(r.failed.length ? { failedCount: r.failed.length, failedNames: r.failed } : {}),
210
+ };
211
+ },
212
+ // 注入实况(只读):最近活跃会话里模型**真正看到**的五域文本 + 本次运行的投递统计。
213
+ // 回答"勾了开关到底送没送到"——界面配置与实际注入不一致时,这里一眼可见。
214
+ 'injection-live': async () => {
215
+ const live = deps.getContextInjectorLive();
216
+ if (live === null)
217
+ return { ok: false, error: '注入通道未装配' };
218
+ try {
219
+ return { ok: true, ...live() };
220
+ }
221
+ catch (e) {
222
+ return { ok: false, error: deps.message(e) };
223
+ }
224
+ },
225
+ };
226
+ }
package/lib/ops/ctx.js ADDED
@@ -0,0 +1,9 @@
1
+ // 记忆域 op 的**显式上下文契约**(2026-09-19 抽出,配合 memories/service.ts 组装)。
2
+ //
3
+ // 为什么要有它:24 个 op 原先写在 createMemoriesService 闭包里,靠闭包隐式捕获读模型
4
+ // (snapshot / sceneMemory / scenePrompt)、写管道(invalidateSnapshot)、入口
5
+ // (parseId / locateRule / buildProjected)与三个路径配置。抽成独立文件后,这些依赖必须
6
+ // 显式传入 —— 本文件就是那份清单,签名与原声明逐字对应(改这里等于改 op 的可见面)。
7
+ //
8
+ // 刻意不做成 `typeof rc`:那会把契约藏回 service.ts,域文件又得反向引它。
9
+ export {};