dsh-plugin-tool-management 0.16.6 → 0.18.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 (96) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +20 -4
  3. package/README_EN.md +20 -4
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +59 -0
  21. package/lib/audit-log.js +6 -2
  22. package/lib/client.js +1615 -148
  23. package/lib/compat/capability-specs.js +321 -0
  24. package/lib/compat/host-probe.js +261 -0
  25. package/lib/compat/host-roots.js +168 -0
  26. package/lib/compat/probe-types.js +6 -0
  27. package/lib/compat/probe.js +54 -714
  28. package/lib/context-inject-contract.js +114 -0
  29. package/lib/context-inject-frame.js +147 -0
  30. package/lib/context-inject-select.js +127 -0
  31. package/lib/context-inject-surface.js +97 -0
  32. package/lib/context-inject.js +57 -459
  33. package/lib/host-names.js +150 -0
  34. package/lib/http-api.js +229 -0
  35. package/lib/hub.js +84 -11
  36. package/lib/imports/parsers.js +40 -15
  37. package/lib/index.js +235 -843
  38. package/lib/mcp/inspect-probe.js +199 -0
  39. package/lib/mcp/loader-token.js +26 -0
  40. package/lib/mcp/manager.js +267 -558
  41. package/lib/mcp/patch-yaml.js +207 -40
  42. package/lib/mcp/secret-guard.js +122 -16
  43. package/lib/mcp/tool-visibility.js +388 -0
  44. package/lib/memories/archive-engine.js +163 -16
  45. package/lib/memories/archive.js +10 -1
  46. package/lib/memories/discovery.js +261 -0
  47. package/lib/memories/export-plan.js +42 -0
  48. package/lib/memories/index-io.js +125 -12
  49. package/lib/memories/memories-types.js +1 -0
  50. package/lib/memories/service.js +167 -78
  51. package/lib/memories/snapshot.js +41 -260
  52. package/lib/op-registry.js +72 -8
  53. package/lib/ops/compat.js +17 -2
  54. package/lib/ops/prompts.js +18 -5
  55. package/lib/ops/quick-prompts.js +26 -0
  56. package/lib/ops/scene-records.js +157 -10
  57. package/lib/ops/scene-sync.js +71 -1
  58. package/lib/ops/sessions.js +63 -13
  59. package/lib/ops/snapshot.js +123 -24
  60. package/lib/ops/trash.js +45 -9
  61. package/lib/prompts/quick-service.js +211 -0
  62. package/lib/prompts/service.js +7 -2
  63. package/lib/request-gate.js +115 -56
  64. package/lib/scene-linkage.js +184 -0
  65. package/lib/scene-prompt-sync.js +46 -7
  66. package/lib/scenes/candidates.js +21 -3
  67. package/lib/sessions/archive-schemas.js +121 -0
  68. package/lib/sessions/path-key.js +46 -0
  69. package/lib/sessions/workspace.js +45 -168
  70. package/lib/settings-sidecars.js +143 -0
  71. package/lib/skills/core-types.js +8 -0
  72. package/lib/skills/core-util.js +81 -0
  73. package/lib/skills/core.js +97 -2297
  74. package/lib/skills/dir-watch.js +71 -0
  75. package/lib/skills/frontmatter.js +96 -0
  76. package/lib/skills/homes.js +14 -0
  77. package/lib/skills/import-pipeline.js +231 -0
  78. package/lib/skills/manager-state.js +800 -0
  79. package/lib/skills/official.js +269 -0
  80. package/lib/skills/provider-registration.js +89 -0
  81. package/lib/skills/service.js +4 -234
  82. package/lib/skills/skill-roots.js +287 -0
  83. package/lib/skills/skill-trash.js +265 -0
  84. package/lib/skills/system-trash.js +97 -0
  85. package/lib/skills/upload-pipeline.js +374 -0
  86. package/lib/slash-settings.js +25 -0
  87. package/lib/subagents/persona-parse.js +312 -0
  88. package/lib/subagents/persona-types.js +5 -0
  89. package/lib/subagents/persona-write.js +93 -0
  90. package/lib/subagents/run-provider.js +159 -0
  91. package/lib/subagents/service.js +20 -639
  92. package/lib/subagents/tool-filter.js +98 -0
  93. package/lib/token-config.js +240 -0
  94. package/lib/tool-table-switch.js +163 -0
  95. package/lib/tools/scene.js +40 -9
  96. package/package.json +3 -2
@@ -0,0 +1,269 @@
1
+ // 官方内置技能来源:把宿主随会话注入的内置技能物成本插件自有的真实目录(单技能可启停),
2
+ // 并给出 Office 三件套那段「Installed LibreOffice Kit」运行时说明。
3
+ //
4
+ // 从 core.ts 整段搬来,一行未改。这一段是本插件里唯一直接摸宿主安装形态的代码
5
+ // (resourcesDir / desktopPackageDir / resolvedPackageDir),单独成层后 core.ts 只管
6
+ // 文件与状态,宿主耦合集中在这里;两份模块级缓存(officeRuntimeSectionCache、
7
+ // officialRootCache)随段同搬,仍是每进程一份的单例。
8
+ import { createRequire } from "node:module";
9
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
10
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
11
+ import { resolveDshHome } from "./homes.js";
12
+ // ── 官方内置技能来源(随宿主注入会话上下文的内置技能)──────────────────────
13
+ //
14
+ // 宿主按形态注入不同的官方技能(官方源码逐一核对,2026-09-30):
15
+ // - 桌面版 4 个:office-docx / office-pptx / office-xlsx(dsh-skill-office 只在桌面
16
+ // 宿主装配,assetRoot 指到 `<resources>/runtime/office-skills`)+ diagnose-windows-
17
+ // sandbox-acl(dsh-sandbox-local 的门控是 `process.platform === "win32"`,rank 600)。
18
+ // - web 版:沙盒诊断技能由宿主注入(门控只看 win32 —— 用户 2026-09-30 截图实锤,
19
+ // 0.16.5 按「web 不注入」的误判把 web 挡在门外,导致页面不显示、宿主照注);office
20
+ // 插件 web 宿主不装配、不注入,但用户裁定(2026-09-30)技能页照样把这三个暴露出来
21
+ // 统一管理(npm 包自带完整 assets,接管后可启停)。
22
+ //
23
+ // 它们或住在官方 asar 里、或由官方 provider 复制进临时目录自管,插件直接扫描/接管有两个
24
+ // 坑:宿主子进程里对 asar 的 promises 读法与拦截器解析都出现过时灵时不灵(同机两次启动
25
+ // 一组 4/1、一组全 0,实测);沙盒技能的脚本要交给 PowerShell 子进程,asar 路径读不了。
26
+ //
27
+ // 因此这里把来源**物化**成插件自有的一份真实目录,**每种宿主形态各一份**
28
+ // (`$DSH_HOME/tool-management/official-skills/<desktop|web>/`):每次宿主启动从官方源头
29
+ // 整目录覆盖拷贝(与官方 provider 的「私有资源副本」同一做法),扫描、启停、overlay、
30
+ // 脚本执行全部落在真实文件上。599 压过宿主 provider 的 600,停用即不注入。分形态是竞态
31
+ // 教训:0.16.5 之前共用一份目录,web 启动把它解析到的沙盒技能盖进去,桌面物化好的
32
+ // 4 个只剩 1 个(实测);改成各写各的子目录,互不清对方的账。共享布局的残留(旧条目
33
+ // 直接躺在 official-skills/ 下)按已知名单清理。
34
+ //
35
+ // - 预设参考技能(dsh-agent-preset/skills)不注入会话上下文,不列入(用户裁定)。
36
+ //
37
+ // 单个来源、随 DSH 技能之后排列,来源层语义同默认来源(不可移除、无来源开关)。
38
+ export const OFFICIAL_SOURCE_KEY = "official";
39
+ /** electron 的 resources 目录;非桌面宿主(web 等普通 node 进程)没有这个属性。 */
40
+ function resourcesDir() {
41
+ const resources = process.resourcesPath;
42
+ return typeof resources === "string" && resources !== "" ? resources : null;
43
+ }
44
+ /** 桌面版官方 asar 里的包内目录(同步 fs 对 asar 的支持经过实测可靠)。 */
45
+ function desktopPackageDir(packageName, subpath) {
46
+ const resources = resourcesDir();
47
+ if (resources === null)
48
+ return null;
49
+ const dir = join(resources, "app.asar", "dsh", "node_modules", ...packageName.split("/"), ...subpath.split("/"));
50
+ return existsSync(dir) ? dir : null;
51
+ }
52
+ /**
53
+ * 从插件自身的解析链找官方包内目录(web 等非桌面宿主的途径;宿主进程里的裸名
54
+ * 解析由官方模块拦截器路由到宿主副本)。包不可解析(旧宿主 / 单测环境)返回 null。
55
+ */
56
+ function resolvedPackageDir(packageName, subpath) {
57
+ try {
58
+ const require = createRequire(import.meta.url);
59
+ let manifest;
60
+ try {
61
+ manifest = require.resolve(`${packageName}/package.json`);
62
+ }
63
+ catch {
64
+ // exports 未暴露 ./package.json 的包:从入口文件向上找同名 manifest。
65
+ manifest = "";
66
+ let dir = dirname(require.resolve(packageName));
67
+ for (let i = 0; i < 6; i += 1) {
68
+ const candidate = join(dir, "package.json");
69
+ if (existsSync(candidate)) {
70
+ manifest = candidate;
71
+ break;
72
+ }
73
+ const parent = dirname(dir);
74
+ if (parent === dir)
75
+ break;
76
+ dir = parent;
77
+ }
78
+ if (manifest === "")
79
+ return null;
80
+ }
81
+ const dir = join(dirname(manifest), ...subpath.split("/"));
82
+ return existsSync(dir) ? dir : null;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ /**
89
+ * office 技能源头。桌面版由宿主挂载:运行时真实目录优先(官方 assetRoot),其次 asar
90
+ * 包内 assets。web 宿主不装配 office 插件(不注入),但 npm 包自带完整 assets —— 用户
91
+ * 裁定(2026-09-30)技能页照样暴露这三个技能,走宿主进程解析链取包内一份。解析不到
92
+ * 一律 null —— 不把拿不到真实文件的技能凭空列出来。
93
+ */
94
+ function officialOfficeSource() {
95
+ const resources = resourcesDir();
96
+ if (resources !== null) {
97
+ const runtimeDir = join(resources, "runtime", "office-skills");
98
+ if (existsSync(join(runtimeDir, "office-docx")))
99
+ return runtimeDir;
100
+ const asarDir = desktopPackageDir("@deepseek-ai/dsh-skill-office", "assets");
101
+ if (asarDir !== null && existsSync(join(asarDir, "office-docx")))
102
+ return asarDir;
103
+ }
104
+ return resolvedPackageDir("@deepseek-ai/dsh-skill-office", "assets");
105
+ }
106
+ /** 沙盒诊断技能源头(桌面 asar 直连优先,其次宿主进程解析链;宿主门控仅看 win32,web/桌面皆注入)。 */
107
+ function officialSandboxSource() {
108
+ if (process.platform !== "win32")
109
+ return null;
110
+ return (desktopPackageDir("@deepseek-ai/dsh-sandbox-windows-acl", "assets/diagnose-windows-sandbox-acl") ??
111
+ resolvedPackageDir("@deepseek-ai/dsh-sandbox-windows-acl", "assets/diagnose-windows-sandbox-acl"));
112
+ }
113
+ // ── office 技能的「Installed LibreOffice Kit」段 ────────────────────────────
114
+ //
115
+ // 官方 dsh-office provider 在 get 时给三个 office 技能的正文**追加**一段
116
+ // 「Installed LibreOffice Kit」:SKILL.md 明文要求「使用这段提供的 libreofficeKit.node
117
+ // 与 .cli 绝对路径,禁止自行搜索/猜测」。我们以 599 接管后官方 provider 不再被问到,
118
+ // 这段必须由我们补上 —— 否则模型拿到的 office 技能缺执行 LibreOffice 命令所需的全部
119
+ // 路径(这个缺口 0.16.5 在桌面上就已存在)。文案与 JSON 形状逐字取自官方
120
+ // dsh-skill-office lib/index.js 的 officeRuntime();node/cli 的落点按宿主形态各自核实:
121
+ // - 桌面:官方明确要求 packaged 应用必须给**独立 node**(electron 本体不行)。
122
+ // 实测桌面把运行时装在 `<resources>/runtime/primary-runtime/`(DSH_PRIMARY_RUNTIME
123
+ // 指向它,SDK 部署可被同名环境变量改写),node 在其 dependencies/node/bin 下;cli
124
+ // 必须用 `app.asar.unpacked` 里的真实文件 —— 独立 node 读不了 asar 内部。
125
+ // - web:官方默认 process.execPath(宿主是普通 node 进程),cli 从宿主进程解析链取。
126
+ // 任一路径不是真实文件 → 用官方口径的「已停用」文案(SKILL.md 自有降级分支,模型不会
127
+ // 去猜路径),绝不给一个跑不起来的路径。
128
+ export const OFFICE_SKILL_NAMES = new Set([
129
+ "office-docx",
130
+ "office-pptx",
131
+ "office-xlsx",
132
+ ]);
133
+ const OFFICE_RUNTIME_DISABLED = "\n\nLibreOffice Kit is disabled in this deployment.";
134
+ let officeRuntimeSectionCache;
135
+ /** 官方 provider 在 get 时追加的 LibreOffice Kit 段(每进程算一次,路径不随会话变)。 */
136
+ export function officialOfficeRuntimeSection() {
137
+ if (officeRuntimeSectionCache === undefined)
138
+ officeRuntimeSectionCache = computeOfficeRuntimeSection();
139
+ return officeRuntimeSectionCache;
140
+ }
141
+ function computeOfficeRuntimeSection() {
142
+ const resources = resourcesDir();
143
+ let node;
144
+ let cli;
145
+ const runtimeRoot = process.env.DSH_PRIMARY_RUNTIME || process.env.DSH_BUNDLED_PRIMARY_RUNTIME;
146
+ if (runtimeRoot) {
147
+ // SDK / 容器部署:官方配置从运行时根推导(cordis 装配逐字核对)。
148
+ node = join(resolve(runtimeRoot), "dependencies", "node", "bin", process.platform === "win32" ? "node.exe" : "node");
149
+ cli = resolvedPackageDir("@deepseek-ai/libreoffice-kit", "lib/cli.js");
150
+ }
151
+ else if (resources !== null) {
152
+ node = join(resources, "runtime", "primary-runtime", "dependencies", "node", "bin", process.platform === "win32" ? "node.exe" : "node");
153
+ // 桌面的 cli 只认 unpacked 真实文件(独立 node 读不了 asar;electron 的 fs 补丁
154
+ // 会让 existsSync 对 asar 内部也返回真,所以这里不用解析链兜底)。
155
+ cli = join(resources, "app.asar.unpacked", "dsh", "node_modules", "@deepseek-ai", "libreoffice-kit", "lib", "cli.js");
156
+ }
157
+ else {
158
+ node = process.execPath;
159
+ cli = resolvedPackageDir("@deepseek-ai/libreoffice-kit", "lib/cli.js");
160
+ }
161
+ const usable = (path) => {
162
+ try {
163
+ return isAbsolute(path) && statSync(path).isFile();
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ };
169
+ if (!usable(node) || cli === null || !usable(cli))
170
+ return OFFICE_RUNTIME_DISABLED;
171
+ return `\n\n## Installed LibreOffice Kit\n\nUse these absolute paths for every LibreOffice Kit command. Pass the CLI entry as the first argument to Node.\n\n${JSON.stringify({ libreofficeKit: { node, cli } }, null, 2)}`;
172
+ }
173
+ function copyDirectorySync(from, to) {
174
+ mkdirSync(to, { recursive: true });
175
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
176
+ const source = join(from, entry.name);
177
+ const target = join(to, entry.name);
178
+ if (entry.isDirectory())
179
+ copyDirectorySync(source, target);
180
+ else if (entry.isFile())
181
+ copyFileSync(source, target);
182
+ }
183
+ }
184
+ /**
185
+ * 物化官方内置技能到插件自有目录,返回该目录;无可用源头或拷贝失败返回 null。
186
+ * 每次宿主启动整目录重建(官方包只随宿主更新变化,宿主更新必然重启宿主)。
187
+ */
188
+ let officialRootCache;
189
+ /** 宿主形态子目录名:桌面(有 process.resourcesPath)与 web 等普通 node 进程各物化一份。 */
190
+ function officialFormName() {
191
+ return resourcesDir() === null ? "web" : "desktop";
192
+ }
193
+ /**
194
+ * 0.16.5 之前各形态共用一份目录(旧条目直接躺在 official-skills/ 下);分形态目录后
195
+ * 这些残留只会变成幽灵条目,按这份已知名单清掉。只清名单内的名字,绝不碰旁边的
196
+ * `<form>/` 子目录 —— 那是另一种形态物化的现役目录。
197
+ */
198
+ const LEGACY_OFFICIAL_ENTRY_NAMES = Object.freeze([
199
+ "office-docx",
200
+ "office-pptx",
201
+ "office-xlsx",
202
+ "scripts",
203
+ "diagnose-windows-sandbox-acl",
204
+ ]);
205
+ function cleanupLegacyOfficialLayout(root) {
206
+ for (const name of LEGACY_OFFICIAL_ENTRY_NAMES) {
207
+ try {
208
+ rmSync(join(root, name), { recursive: true, force: true });
209
+ }
210
+ catch {
211
+ /* 残留清理失败不影响物化,下轮启动再试 */
212
+ }
213
+ }
214
+ }
215
+ function materializeOfficialSkills() {
216
+ const office = officialOfficeSource();
217
+ const sandbox = officialSandboxSource();
218
+ if (office === null && sandbox === null)
219
+ return null;
220
+ // 每种宿主形态只整目录重建**自己**这份(竞态教训见上方块注释);对方形态的子目录
221
+ // 与共享层残留清理互不干涉。
222
+ const root = join(resolveDshHome(), "tool-management", "official-skills", officialFormName());
223
+ try {
224
+ rmSync(root, { recursive: true, force: true });
225
+ // office:整个 assets 根拷进来(office-*/ 与 scripts/ 的相对结构必须保留 ——
226
+ // SKILL.md 用 `<skill-directory>/../scripts/check_office.py` 引用检查脚本)。
227
+ if (office !== null)
228
+ copyDirectorySync(office, root);
229
+ if (sandbox !== null) {
230
+ copyDirectorySync(sandbox, join(root, basename(sandbox)));
231
+ }
232
+ cleanupLegacyOfficialLayout(dirname(root));
233
+ return root;
234
+ }
235
+ catch (error) {
236
+ console.error("[dsh-plugin-tool-management] official skills materialization failed:", String((error && error.message) || error));
237
+ return null;
238
+ }
239
+ }
240
+ export function officialSkillRoot() {
241
+ if (officialRootCache !== undefined)
242
+ return officialRootCache;
243
+ const dir = materializeOfficialSkills();
244
+ officialRootCache =
245
+ dir === null
246
+ ? null
247
+ : {
248
+ key: OFFICIAL_SOURCE_KEY,
249
+ path: dir,
250
+ label: "官方内置",
251
+ localeKey: "official",
252
+ mutable: false,
253
+ deletable: false,
254
+ // 来源层无开关(同默认来源),单个技能可启停;overlay 以 599 压过官方
255
+ // provider 的 600,脚本从物化副本的真实路径执行。
256
+ //
257
+ // 残余风险(2026-09-30 审查 §5 F12,如实标注、不修):`599` 是个**数字契约** ——
258
+ // 它只在"官方那个 provider 的 rank 恰好是 600"时成立。官方调档位(把内置技能提到
259
+ // 599 以上、或降到 599 以下)会让接管失效:要么官方内置技能重新注入、与 599 层并存
260
+ // (同名两份),要么本插件反向遮蔽掉本不该遮蔽的东西。rank 是纯数字、没有能力探测
261
+ // 能提前发现,doctor 也查不了(要枚举官方 provider 的注册参数)。所以记为**已知
262
+ // 假设**,列进 CHANGELOG 的宿主适配矩阵,官方更新时人工核对这一处。
263
+ toggleable: true,
264
+ native: false,
265
+ scope: "user",
266
+ rank: 599,
267
+ };
268
+ return officialRootCache;
269
+ }
@@ -0,0 +1,89 @@
1
+ // manager provider 接线:以 core 的候选/正文接口接管四个外部来源(含启停策略),并随来源
2
+ // 集合变化注册/注销。
3
+ //
4
+ // 从 skills/service.ts 整段搬来,一行未改。invalidators 由调用方注入,本层不持有状态 ——
5
+ // 它是官方 provider 通道的使用方,不改官方包、也不 monkey-patch。
6
+ import { getProviderSkill, listProviderCandidates } from './core.js';
7
+ const PROVIDER_NAME = 'dsh-plugin-tool-management-external';
8
+ const message = (e) => String((e && e.message) || e);
9
+ /** manager provider:以 core 的候选/正文接口接管四个来源(含启停策略)。 */
10
+ export function externalSkillProvider(control, invalidators) {
11
+ if (control && typeof control.invalidate === 'function')
12
+ invalidators.add(control.invalidate);
13
+ if (control && control.signal && typeof control.signal.addEventListener === 'function') {
14
+ control.signal.addEventListener('abort', () => { invalidators.delete(control.invalidate); }, { once: true });
15
+ }
16
+ return {
17
+ name: PROVIDER_NAME,
18
+ list: async (options) => listProviderCandidates(options),
19
+ get: async (candidate, options) => getProviderSkill(candidate, options),
20
+ };
21
+ }
22
+ /**
23
+ * 在每个活动 agent 的 scope 内注册同一 provider。
24
+ * 用户级技能由 agent-preset 的 scoped 层解析;只有在 agent 自己的层里注册,
25
+ * 候选 rank 覆盖才能对它们生效(详见上游 registerAgentSkillProviders)。
26
+ */
27
+ export function registerAgentSkillProviders(ctx, invalidators) {
28
+ const on = ctx && ctx.on;
29
+ if (typeof on !== 'function')
30
+ return () => { };
31
+ const registrations = new Map();
32
+ const skillsOf = (agent) => {
33
+ const agentCtx = agent && agent.ctx;
34
+ if (!agentCtx)
35
+ return null;
36
+ const skills = typeof agentCtx.get === 'function' ? agentCtx.get('skills') : agentCtx.skills;
37
+ return skills && typeof skills.registerProvider === 'function' ? skills : null;
38
+ };
39
+ const install = (agent) => {
40
+ if (!agent || agent.id == null || registrations.has(agent.id))
41
+ return;
42
+ const skills = skillsOf(agent);
43
+ if (!skills)
44
+ return;
45
+ try {
46
+ const dispose = skills.registerProvider((control) => externalSkillProvider(control, invalidators));
47
+ registrations.set(agent.id, typeof dispose === 'function' ? dispose : () => { });
48
+ }
49
+ catch (e) {
50
+ console.error('[dsh-plugin-tool-management] agent-scoped skills provider registration failed:', message(e));
51
+ }
52
+ };
53
+ const uninstall = (agent) => {
54
+ const id = agent && agent.id;
55
+ if (id == null)
56
+ return;
57
+ const dispose = registrations.get(id);
58
+ registrations.delete(id);
59
+ if (typeof dispose === 'function') {
60
+ try {
61
+ dispose();
62
+ }
63
+ catch { /* ignore */ }
64
+ }
65
+ };
66
+ const stopCreated = on('agent/created', (payload) => install(payload && payload.agent));
67
+ const stopDisposed = on('agent/disposed', (payload) => uninstall(payload && payload.agent));
68
+ try {
69
+ const agents = typeof ctx.get === 'function' ? ctx.get('agents') : undefined;
70
+ if (agents && typeof agents.list === 'function') {
71
+ for (const agent of agents.list())
72
+ install(agent);
73
+ }
74
+ }
75
+ catch { /* agents 服务不可用 → 仅靠事件 */ }
76
+ return () => {
77
+ if (typeof stopCreated === 'function')
78
+ stopCreated();
79
+ if (typeof stopDisposed === 'function')
80
+ stopDisposed();
81
+ for (const dispose of registrations.values()) {
82
+ try {
83
+ dispose();
84
+ }
85
+ catch { /* ignore */ }
86
+ }
87
+ registrations.clear();
88
+ };
89
+ }
@@ -4,15 +4,12 @@
4
4
  // { ok:false, error, code?, params? })。provider / agent-scope 注册与变更通知
5
5
  // 沿用上游机制:全局层接管 + 每个活动 agent scope 内二次注册。
6
6
  import { appendFile, rename, rm, stat } from 'node:fs/promises';
7
- import { existsSync } from 'node:fs';
8
- import { Worker } from 'node:worker_threads';
9
- import { spawn } from 'node:child_process';
10
- import { homedir } from 'node:os';
11
- import { basename, join } from 'node:path';
12
7
  import { isInsideRootResolved } from '../paths.js';
13
- import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, permanentlyDeleteTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, updateSkill, skillDetail, listProviderCandidates, getProviderSkill, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, trashRootPath, } from './core.js';
8
+ import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, updateSkill, skillDetail, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, } from './core.js';
9
+ import { watchDirectories } from './dir-watch.js';
10
+ import { registerAgentSkillProviders, externalSkillProvider } from './provider-registration.js';
11
+ import { permanentlyDeleteTrashSafely } from './system-trash.js';
14
12
  const MAX_LOG_BYTES = 1 << 20;
15
- const PROVIDER_NAME = 'dsh-plugin-tool-management-external';
16
13
  const message = (e) => String((e && e.message) || e);
17
14
  /** 串行写日志,超过上限滚动为 .1;日志失败不阻塞主流程(移植自上游)。 */
18
15
  function makeLog() {
@@ -35,152 +32,6 @@ function makeLog() {
35
32
  await queue;
36
33
  };
37
34
  }
38
- // ── 系统回收站(永久删除的最后一道保险) ──────────────────────────────────
39
- /**
40
- * 外部进程(PowerShell / gio)的超时上限。这些调用在技能的**写队列**里被 await
41
- * (skill-trash-delete),进程一旦挂起,该域全部写操作会永久停摆且无任何提示 ——
42
- * 回收站 API 在文件被占用、系统弹窗等情况下确实会挂住。超时后杀掉进程并返回 false,
43
- * 让调用方按既有的「回收站失败 → 硬删除」兜底走下去,队列得以继续。
44
- */
45
- const EXTERNAL_CMD_TIMEOUT_MS = 30_000;
46
- function runQuietly(command, args) {
47
- return new Promise((resolve) => {
48
- let settled = false;
49
- let timer = null;
50
- const finish = (ok) => {
51
- if (settled)
52
- return;
53
- settled = true;
54
- if (timer) {
55
- clearTimeout(timer);
56
- timer = null;
57
- }
58
- resolve(ok);
59
- };
60
- let child;
61
- try {
62
- child = spawn(command, args, { stdio: 'ignore', windowsHide: true });
63
- }
64
- catch {
65
- finish(false);
66
- return;
67
- }
68
- timer = setTimeout(() => {
69
- try {
70
- child.kill('SIGKILL');
71
- }
72
- catch { /* 进程可能已自行退出 */ }
73
- finish(false);
74
- }, EXTERNAL_CMD_TIMEOUT_MS);
75
- child.on('error', () => finish(false));
76
- child.on('close', (code) => finish(code === 0));
77
- });
78
- }
79
- function psQuote(value) {
80
- return "'" + String(value).replace(/'/g, "''") + "'";
81
- }
82
- /** Win → 回收站;macOS → ~/.Trash;Linux → gio trash。失败返回 false。 */
83
- async function moveToSystemTrash(abs) {
84
- try {
85
- if (process.platform === 'win32') {
86
- const script = [
87
- 'Add-Type -AssemblyName Microsoft.VisualBasic',
88
- "[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(" + psQuote(abs) + ", 'OnlyErrorDialogs', 'SendToRecycleBin')",
89
- ].join('; ');
90
- return await runQuietly('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
91
- }
92
- if (process.platform === 'darwin') {
93
- await rename(abs, join(homedir(), '.Trash', basename(abs) + '-' + Date.now()));
94
- return true;
95
- }
96
- return await runQuietly('gio', ['trash', abs]);
97
- }
98
- catch {
99
- return false;
100
- }
101
- }
102
- /**
103
- * 永久删除回收站条目:先尝试系统回收站(用户仍可在系统里找回),
104
- * 失败才落到 core 的硬删除 —— 让「永久删除」也不会静默丢数据。
105
- */
106
- async function permanentlyDeleteTrashSafely(id, log) {
107
- const clean = String(id || '').trim();
108
- const root = trashRootPath();
109
- const sep = root.indexOf('\\') >= 0 ? '\\' : '/';
110
- const target = /^[A-Za-z0-9._-]{1,128}$/.test(clean) ? root + sep + clean : null;
111
- if (target && target.indexOf(root + sep) === 0 && existsSync(target)) {
112
- if (await moveToSystemTrash(target)) {
113
- await log('trash-delete-system', { id: clean, path: target });
114
- return { id: clean, method: 'system-trash' };
115
- }
116
- }
117
- return permanentlyDeleteTrash(clean, log);
118
- }
119
- // ── 文件监听:外部改动自动失效 ────────────────────────────────────────────
120
- /**
121
- * 监听目录,200ms 防抖后回调:在编辑器或其他工具里新增/修改/删除文件后,无需手动
122
- * 刷新即可看到变化。技能来源与规则根目录共用(见 src/memories/service.ts 的场景记忆段缓存)。
123
- *
124
- * 为什么用 worker_threads:Windows 上 fs.watch(recursive) 的句柄在「被监听
125
- * 目录被删除」时会静默卡死事件循环(不触发 error、unref 也无效)——宿主
126
- * 进程将永远无法退出。把 watcher 放进独立的 worker 线程,主线程对 worker
127
- * unref(),无论目录发生什么,宿主与测试进程都能正常收尾;worker 内部失败
128
- * 也不影响主线程。目录不存在或平台不支持递归监听时静默跳过。
129
- */
130
- function watchDirectories(paths, invalidate) {
131
- const valid = paths.filter((dir) => dir && existsSync(dir));
132
- if (!valid.length)
133
- return () => { };
134
- let timer = null;
135
- const fire = () => {
136
- if (timer)
137
- clearTimeout(timer);
138
- timer = setTimeout(() => {
139
- timer = null;
140
- try {
141
- invalidate();
142
- }
143
- catch { /* 失效失败不影响监听 */ }
144
- }, 200);
145
- };
146
- let worker = null;
147
- try {
148
- // eval 模式内联 worker 代码:无需额外文件与构建步骤。worker 事件循环独立,
149
- // 主线程 unref 后完全不参与宿主进程的退出判定。
150
- const src = `
151
- const { watch } = require('node:fs');
152
- const { parentPort, workerData } = require('node:worker_threads');
153
- for (const dir of workerData.paths) {
154
- try {
155
- const w = watch(dir, { recursive: true, persistent: false }, () => {
156
- try { parentPort.postMessage('change') } catch { /* worker 正在关闭 */ }
157
- });
158
- w.on('error', () => { /* 目录被移除等:忽略,不向主线程传播 */ });
159
- } catch { /* 不可监听(权限/平台)时跳过该目录 */ }
160
- }
161
- `;
162
- worker = new Worker(src, { eval: true, workerData: { paths: valid } });
163
- worker.unref();
164
- worker.on('message', () => fire());
165
- worker.on('error', () => { });
166
- }
167
- catch { /* worker 不可用时静默跳过,功能退化为依赖 op 全量扫描 */ }
168
- return () => {
169
- if (timer) {
170
- clearTimeout(timer);
171
- timer = null;
172
- }
173
- if (worker) {
174
- const w = worker;
175
- worker = null;
176
- try {
177
- w.removeAllListeners();
178
- w.terminate();
179
- }
180
- catch { /* ignore */ }
181
- }
182
- };
183
- }
184
35
  /** 活动 Session 的宿主 cwd;项目 Skill 只从这些已知 workspace 推导。 */
185
36
  export function activeSessionCwds(ctx) {
186
37
  try {
@@ -204,87 +55,6 @@ export function activeSessionCwds(ctx) {
204
55
  return [];
205
56
  }
206
57
  }
207
- /** manager provider:以 core 的候选/正文接口接管四个来源(含启停策略)。 */
208
- function externalSkillProvider(control, invalidators) {
209
- if (control && typeof control.invalidate === 'function')
210
- invalidators.add(control.invalidate);
211
- if (control && control.signal && typeof control.signal.addEventListener === 'function') {
212
- control.signal.addEventListener('abort', () => { invalidators.delete(control.invalidate); }, { once: true });
213
- }
214
- return {
215
- name: PROVIDER_NAME,
216
- list: async (options) => listProviderCandidates(options),
217
- get: async (candidate, options) => getProviderSkill(candidate, options),
218
- };
219
- }
220
- /**
221
- * 在每个活动 agent 的 scope 内注册同一 provider。
222
- * 用户级技能由 agent-preset 的 scoped 层解析;只有在 agent 自己的层里注册,
223
- * 候选 rank 覆盖才能对它们生效(详见上游 registerAgentSkillProviders)。
224
- */
225
- function registerAgentSkillProviders(ctx, invalidators) {
226
- const on = ctx && ctx.on;
227
- if (typeof on !== 'function')
228
- return () => { };
229
- const registrations = new Map();
230
- const skillsOf = (agent) => {
231
- const agentCtx = agent && agent.ctx;
232
- if (!agentCtx)
233
- return null;
234
- const skills = typeof agentCtx.get === 'function' ? agentCtx.get('skills') : agentCtx.skills;
235
- return skills && typeof skills.registerProvider === 'function' ? skills : null;
236
- };
237
- const install = (agent) => {
238
- if (!agent || agent.id == null || registrations.has(agent.id))
239
- return;
240
- const skills = skillsOf(agent);
241
- if (!skills)
242
- return;
243
- try {
244
- const dispose = skills.registerProvider((control) => externalSkillProvider(control, invalidators));
245
- registrations.set(agent.id, typeof dispose === 'function' ? dispose : () => { });
246
- }
247
- catch (e) {
248
- console.error('[dsh-plugin-tool-management] agent-scoped skills provider registration failed:', message(e));
249
- }
250
- };
251
- const uninstall = (agent) => {
252
- const id = agent && agent.id;
253
- if (id == null)
254
- return;
255
- const dispose = registrations.get(id);
256
- registrations.delete(id);
257
- if (typeof dispose === 'function') {
258
- try {
259
- dispose();
260
- }
261
- catch { /* ignore */ }
262
- }
263
- };
264
- const stopCreated = on('agent/created', (payload) => install(payload && payload.agent));
265
- const stopDisposed = on('agent/disposed', (payload) => uninstall(payload && payload.agent));
266
- try {
267
- const agents = typeof ctx.get === 'function' ? ctx.get('agents') : undefined;
268
- if (agents && typeof agents.list === 'function') {
269
- for (const agent of agents.list())
270
- install(agent);
271
- }
272
- }
273
- catch { /* agents 服务不可用 → 仅靠事件 */ }
274
- return () => {
275
- if (typeof stopCreated === 'function')
276
- stopCreated();
277
- if (typeof stopDisposed === 'function')
278
- stopDisposed();
279
- for (const dispose of registrations.values()) {
280
- try {
281
- dispose();
282
- }
283
- catch { /* ignore */ }
284
- }
285
- registrations.clear();
286
- };
287
- }
288
58
  /** 文件写入后刷新技能目录,并通知 Web 端重拉 `/` 菜单缓存(移植自上游)。 */
289
59
  function notifyChatCatalog(ctx, invalidateSkills) {
290
60
  try {