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,280 @@
1
+ // 规则/记忆域的**投影与渲染**纯函数(2026-09-19 从 memories/service.ts 抽出)。
2
+ //
3
+ // 界定:这里只放「给定索引 + 已发现的条目,算出界面/注入看到的那份结果」的函数 ——
4
+ // 不读索引文件、不改索引、不碰服务状态。快照构建(buildSnapshot)与 op 实现留在 service.ts。
5
+ //
6
+ // 为什么抽出来:service.ts 里这些函数夹在「索引读写」与「24 个 op」之间,而它们其实是
7
+ // 另一件事——把磁盘事实翻译成模型与用户看到的文本。分开之后改渲染不必在 op 之间穿行。
8
+ import { readdirSync, statSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { ATTACHMENT_LIST_MAX, bundleDocName, DEFAULT_GROUP_ORDER, DEFAULT_ORDER, GLOBAL_SCENE, GLOBAL_SCENE_LABEL, INLINE_BODY_MAX, LEGACY_BUNDLE_DOC, SHARED_GROUP, } from './constants.js';
11
+ // ── 索引归一化与场景解析 ───────────────────────────────────────────────────
12
+ /** 启用场景集合归一化:非数组 → null(= 全部启用);数组 → 去重后的字符串数组。 */
13
+ export function normalizeActive(raw) {
14
+ if (!Array.isArray(raw))
15
+ return null;
16
+ const out = [];
17
+ const seen = new Set();
18
+ for (const item of raw) {
19
+ if (typeof item !== 'string')
20
+ continue;
21
+ const name = item.trim();
22
+ if (name === '' || seen.has(name))
23
+ continue;
24
+ seen.add(name);
25
+ out.push(name);
26
+ }
27
+ return out;
28
+ }
29
+ export function ensureSceneRecords(index, probeScenes) {
30
+ if (!index.scenes)
31
+ index.scenes = {};
32
+ let dirty = false;
33
+ if (!index.scenes[GLOBAL_SCENE]) {
34
+ index.scenes[GLOBAL_SCENE] = { label: GLOBAL_SCENE_LABEL, order: 0 };
35
+ dirty = true;
36
+ }
37
+ // 有目录但没记录(旧布局搬进来的、或用户手工建的目录)→ 补记录,label 用目录名。
38
+ for (const name of probeScenes) {
39
+ if (index.scenes[name])
40
+ continue;
41
+ index.scenes[name] = { order: DEFAULT_GROUP_ORDER };
42
+ dirty = true;
43
+ }
44
+ return dirty;
45
+ }
46
+ /**
47
+ * 活动场景解析(用户裁定 2026-09-15:**除「全局」外同时只能启用一个场景**):
48
+ * - `index.active` 为数组(新写入的唯一形态)→ 至多一个非保留场景启用;
49
+ * - `index.active` 缺失 / null → 历史默认("全部场景启用");**不再产生**新值,
50
+ * 新建场景时会被收敛成显式数组(见 `collapseActiveForNewScene`),
51
+ * 因此"新建即启用"不会发生;存量数据仍按老语义读,避免升级后注入范围突变。
52
+ * - `_shared` 恒常启用(公共基线),不受开关影响
53
+ * - 保留场景 `global` 恒常启用:它的记忆对任何对话都成立
54
+ * 缺失 memories-index.json 一律按默认值运行,不抛错(§9.3)。
55
+ * 场景生效与否只由 index.active 决定(场景档案/模式也写这一份)。
56
+ */
57
+ export function resolveActiveScenes(index, knownScenes) {
58
+ const stored = normalizeActive(index.active);
59
+ const mode = stored === null ? 'all' : 'custom';
60
+ const active = new Set(stored === null ? knownScenes : stored);
61
+ active.add(SHARED_GROUP);
62
+ active.add(GLOBAL_SCENE);
63
+ return { active, mode };
64
+ }
65
+ /** 当前启用的**非保留**场景(单选模型下至多一个;多个时按场景顺序取第一个)。 */
66
+ export function enabledSceneOf(index) {
67
+ const names = Object.keys(index.scenes || {});
68
+ const { active } = resolveActiveScenes(index, names);
69
+ const enabled = names
70
+ .filter((n) => n !== SHARED_GROUP && n !== GLOBAL_SCENE && active.has(n))
71
+ .sort((a, b) => sceneOrderOf(index, a) - sceneOrderOf(index, b) || a.localeCompare(b));
72
+ return enabled[0] ?? null;
73
+ }
74
+ /**
75
+ * 新建场景前把历史默认(`active = null` = 全部启用)收敛成显式数组,保证
76
+ * **新场景默认不启动**、且收敛后仍满足"至多一个非保留场景启用":
77
+ * - 已存在其它非保留场景 → 取顺序第一个作为启用场景(其余收敛掉,返回 `collapsed: true`)
78
+ * - 不存在 → 空数组(什么都不启用)
79
+ * @returns 是否发生了收敛(供 UI 如实提示)。
80
+ */
81
+ export function collapseActiveForNewScene(index) {
82
+ if (normalizeActive(index.active) !== null)
83
+ return false;
84
+ const names = Object.keys(index.scenes || {}).filter((n) => n !== SHARED_GROUP && n !== GLOBAL_SCENE);
85
+ const first = names.sort((a, b) => sceneOrderOf(index, a) - sceneOrderOf(index, b) || a.localeCompare(b))[0];
86
+ index.active = first === undefined ? [] : [first];
87
+ return true;
88
+ }
89
+ /** 场景名 = 分组路径的第一段(`web/frontend` 属于场景 `web`)。 */
90
+ export function sceneOf(group) {
91
+ const idx = String(group || '').indexOf('/');
92
+ return idx >= 0 ? group.slice(0, idx) : group;
93
+ }
94
+ // ── 指纹 ───────────────────────────────────────────────────────────────────
95
+ /** 指纹里必须包含一切影响渲染的索引字段(active / enabled / order / groups.order / scenes.order / label / description)。 */
96
+ export function signatureOfIndex(index) {
97
+ const active = normalizeActive(index.active);
98
+ const rules = Object.keys(index.rules).sort().map((id) => {
99
+ const e = index.rules[id];
100
+ return `${id}\u0000${e.enabled === false ? '0' : '1'}\u0000${e.order ?? DEFAULT_ORDER}`;
101
+ });
102
+ const groups = Object.keys(index.groups).sort().map((g) => `${g}\u0000${index.groups[g]?.order ?? DEFAULT_GROUP_ORDER}`);
103
+ // 场景顺序决定段内场景的先后 → 必须进指纹,否则改顺序后段文本不会重算。
104
+ // label 与 description 同理(sceneHeader 的「场景说明」一行直接渲染 description)——
105
+ // 手改索引文件(带外变更)时只有指纹变化才会触发重算。
106
+ const scenes = Object.keys(index.scenes || {}).sort().map((s) => {
107
+ const e = index.scenes[s];
108
+ return `${s}\u0000${e.order ?? (s === GLOBAL_SCENE ? 0 : DEFAULT_GROUP_ORDER)}\u0000${e.label ?? ''}\u0000${e.description ?? ''}`;
109
+ });
110
+ return `A:${active === null ? '*' : active.join(',')}|R:${rules.join(';')}|G:${groups.join(';')}|S:${scenes.join(';')}`;
111
+ }
112
+ // ── 场景排序与标题 ─────────────────────────────────────────────────────────
113
+ /** 场景排序键:索引 scenes.order 优先,回退到旧 groups.order,再回退默认值。 */
114
+ export function sceneOrderOf(index, scene) {
115
+ if (scene === GLOBAL_SCENE)
116
+ return 0;
117
+ const s = index.scenes?.[scene]?.order;
118
+ if (typeof s === 'number' && Number.isFinite(s))
119
+ return s;
120
+ return index.groups[scene]?.order ?? DEFAULT_GROUP_ORDER;
121
+ }
122
+ /** 场景显示名:`global` → 「全局」(磁盘名保持 ASCII),其余用索引 label 或场景名。 */
123
+ export function sceneLabel(scene, index) {
124
+ if (scene === GLOBAL_SCENE)
125
+ return index?.scenes?.[GLOBAL_SCENE]?.label || GLOBAL_SCENE_LABEL;
126
+ const label = index?.scenes?.[scene]?.label;
127
+ return label && label !== '' ? label : scene;
128
+ }
129
+ /** 单个场景的标题:**场景在最顶层**(`##`,与「子智能体」「MCP 服务器」等段同级)。 */
130
+ export const sceneHeading = (scene) => `## 场景:${sceneLabel(scene)}`;
131
+ /**
132
+ * 没填描述时的默认「场景说明」(用户裁定 2026-09-16:全局桶一直没有描述,读起来像缺了一块,
133
+ * 统一成"每个场景块都有场景说明")。
134
+ *
135
+ * 默认句同时承担"这个场景是什么"的答疑(此前只有光秃秃的 `## 场景:X`,模型读不懂 —— 用户实测):
136
+ * 两个恒常桶说明生效范围,用户场景说明它是当前启用的那份配置。
137
+ */
138
+ export const defaultSceneDescription = (scene) => (scene === GLOBAL_SCENE ? '全局记忆,任何对话都生效'
139
+ : scene === SHARED_GROUP ? '共享记忆,任何对话都生效'
140
+ : '用户配置的上下文,当前启用');
141
+ /**
142
+ * 单个场景的段头:场景标题 + **恒有**的 `场景说明:<描述>`。
143
+ *
144
+ * 场景描述(界面「描述(可选)」,≤60 字符)**此前从未注入过** —— 它正是「这个场景是
145
+ * 干什么的」的答案,属于模型做判断需要的上下文,而不是只给人看的元数据;界面上的文案
146
+ * 也从没把它标成「只给使用者看」(对比 AGENTS.md 预设的描述,那里是明确标注的)。
147
+ * 描述为空时给 `defaultSceneDescription` 的默认句(用户裁定:全局桶没描述时读起来像
148
+ * 缺了一块,统一成每个场景块都有说明)。
149
+ */
150
+ export function sceneHeader(scene, index) {
151
+ const head = sceneHeading(scene);
152
+ const described = String(index?.scenes?.[scene]?.description ?? '').replaceAll(/\s+/g, ' ').trim();
153
+ const description = described === '' ? defaultSceneDescription(scene) : described;
154
+ // 加粗(用户裁定 2026-09-16):与引导语同款强调,别让"场景说明"读起来像可忽略的普通正文。
155
+ return `${head}\n\n**场景说明:${description}**\n\n`;
156
+ }
157
+ /** 场景渲染顺序:全局 `global` 最先(它的记忆对任何对话都成立,先讲总则),
158
+ * 其次 `_shared`(历史保留名),其余按(索引 scenes.order, 场景名)。 */
159
+ export function compareSceneBuckets(a, b, index) {
160
+ if (a === b)
161
+ return 0;
162
+ if (a === GLOBAL_SCENE)
163
+ return -1;
164
+ if (b === GLOBAL_SCENE)
165
+ return 1;
166
+ if (a === SHARED_GROUP)
167
+ return -1;
168
+ if (b === SHARED_GROUP)
169
+ return 1;
170
+ const ao = sceneOrderOf(index, a);
171
+ const bo = sceneOrderOf(index, b);
172
+ return ao - bo || a.localeCompare(b);
173
+ }
174
+ // ── bundle 附件 ────────────────────────────────────────────────────────────
175
+ /**
176
+ * bundle 记忆的附件名(**同步**版 —— 段渲染必须同步返回,`listAttachments` 是异步的)。
177
+ * 口径与异步版一致:跳过正文本体(`<名>.md`,旧数据可能是 `SKILL.md`),只认普通文件。
178
+ */
179
+ export function attachmentNamesSync(bundleDir, name) {
180
+ try {
181
+ const docNames = new Set([bundleDocName(name), LEGACY_BUNDLE_DOC]);
182
+ return readdirSync(bundleDir, { withFileTypes: true })
183
+ .filter((e) => e.isFile() && !docNames.has(e.name))
184
+ .map((e) => e.name)
185
+ .sort((a, b) => a.localeCompare(b));
186
+ }
187
+ catch {
188
+ return [];
189
+ }
190
+ }
191
+ /**
192
+ * bundle 记忆的附件**摘要**(列表页用):数量 / 总体积 / 前几个文件名。
193
+ *
194
+ * 口径与 `attachmentNamesSync`(也就是注入给模型的那份清单)逐字一致:跳过正文本体,
195
+ * 只认普通文件 —— 所以页面上的数字与模型实际看到的一致,不会出现「界面说 3 个、模型只见 2 个」。
196
+ * `names` 截到 ATTACHMENT_LIST_MAX:tooltip 列不下更多,要全看到编辑弹窗里去看。
197
+ *
198
+ * 目录读不到(索引残留了已消失的条目)→ 返回 null,让界面**什么都不显示**,
199
+ * 而不是谎报「0 个附件」。
200
+ */
201
+ export function attachmentSummarySync(bundleDir, name) {
202
+ let names;
203
+ try {
204
+ const docNames = new Set([bundleDocName(name), LEGACY_BUNDLE_DOC]);
205
+ names = readdirSync(bundleDir, { withFileTypes: true })
206
+ .filter((e) => e.isFile() && !docNames.has(e.name))
207
+ .map((e) => e.name)
208
+ .sort((a, b) => a.localeCompare(b));
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ let bytes = 0;
214
+ for (const entry of names) {
215
+ try {
216
+ bytes += statSync(join(bundleDir, entry)).size;
217
+ }
218
+ catch { /* 读不到的条目不计体积 */ }
219
+ }
220
+ return { count: names.length, bytes, names: names.slice(0, ATTACHMENT_LIST_MAX) };
221
+ }
222
+ /**
223
+ * 附件行(只有 bundle 记忆才有):**给目录与文件名,不给内容**。
224
+ * 附件可能是图片、二进制、大 md —— 全文注入又贵又会把段预算吃光;给路径,模型需要时自己读。
225
+ */
226
+ export function attachmentLine(file) {
227
+ if (file.kind !== 'bundle')
228
+ return '';
229
+ const names = attachmentNamesSync(file.bundleDir, file.name);
230
+ if (names.length === 0)
231
+ return '';
232
+ const shown = names.slice(0, ATTACHMENT_LIST_MAX);
233
+ const more = names.length - shown.length;
234
+ return `附件目录:${file.bundleDir}(未注入正文,共 ${names.length} 个:${shown.join('、')}${more > 0 ? `,另 ${more} 个` : ''})`;
235
+ }
236
+ // ── 单条记忆的渲染 ─────────────────────────────────────────────────────────
237
+ /** 多行正文整体缩进 2 格(列在条目内容列上),空行保持空行、不加尾随空白。 */
238
+ export const indentBody = (text) => text
239
+ .split('\n')
240
+ .map((line) => (line === '' ? line : ' ' + line))
241
+ .join('\n');
242
+ /** 括号注解用的显式描述:派生描述与正文重复、不进段(只存在于界面投影);换行压成单行,避免把「一行一条」的列表项撑断。 */
243
+ export const explicitDescriptionOf = (f) => (f.descriptionDerived || !f.description ? '' : String(f.description).replaceAll(/\s+/g, ' ').trim());
244
+ /**
245
+ * 单条信息的渲染形态 —— **能一行就一行,但恒为列表项**。
246
+ *
247
+ * 单行且不长的正文 → `- **名称**(描述) — 正文`(与 MCP / 子智能体两个段的列表同形;无显式描述时括号不出现)
248
+ * 多行或过长的正文 → `- **名称**(描述)` + 空行 + 缩进 2 格的正文(挂在条目下)
249
+ *
250
+ * 名称恒为标题:它就是这条记忆的身份(工具 id `<场景>/<名称>`、bundle 目录/文件名都以它为准),
251
+ * 用户说「记忆里的 X」、模型再调 `memory_manager_*` 时都对得上号;显式描述是括号注解,不抢标题。
252
+ *
253
+ * 为什么全都做成列表项:早先多行正文走 `### 名称` 标题块,附件行只能退化成与记忆**同级**的
254
+ * `- 附件目录:…`(没有父列表项可挂)—— 既可能被读成一条独立记忆,某些渲染器里还会把下一个
255
+ * `###` 标题吞进列表(与上一条粘连)。统一成「一条记忆 = 一个列表项、正文与附件都缩进挂在
256
+ * 条目下」后,两种记忆外观完全一致,归属也不再靠位置猜测。
257
+ *
258
+ * 为什么要分两种:用户常有十几条「一句话事实」(「提交格式:PDF」),每条都占标题 + 空行 +
259
+ * 正文三行,整段会散成一长串标题;压成一行后十条信息就是十行。多行正文是**用户写的完整
260
+ * Markdown**(可能自带标题、代码块、嵌套列表),整体缩进 2 格挂到条目下,结构原样保留。
261
+ *
262
+ * 返回值带 `inline`:调用方据此决定下一条记忆前要不要空行(单行条目连续排列,其余空行分隔)。
263
+ */
264
+ export function memoryBlock(file) {
265
+ const desc = explicitDescriptionOf(file);
266
+ // 加粗的只有名称:`- **名称**(描述) — 正文`,与 MCP 段 `- **server**(N 个工具) — …` 同形。
267
+ const title = `**${file.name}**` + (desc === '' ? '' : `(${desc})`);
268
+ const text = String(file.body ?? '').trim();
269
+ const inline = text === '' || (!text.includes('\n') && text.length <= INLINE_BODY_MAX);
270
+ const head = text === ''
271
+ ? `- ${title}`
272
+ : (inline ? `- ${title} — ${text}` : `- ${title}\n\n${indentBody(text)}`);
273
+ const attach = attachmentLine(file);
274
+ if (attach === '')
275
+ return { text: `${head}\n`, inline };
276
+ // 附件行恒为缩进子项:只用 `- ` 会被解析成与记忆**同级**的列表项(`- A` / `- 附件目录:A的` /
277
+ // `- B` … 四条平级,归属读不出来)。单行条目紧跟其后保持列表连续;多行条目前面空一行,
278
+ // 免得被读成用户正文自己的列表项。
279
+ return { text: inline ? `${head}\n - ${attach}\n` : `${head}\n\n - ${attach}\n`, inline };
280
+ }