dsh-plugin-tool-management 0.15.0 → 0.16.5

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.
@@ -13,7 +13,8 @@
13
13
  // 文件写入错误由路由返回给调用方。
14
14
  import { homedir } from "node:os";
15
15
  import { join, basename, dirname, resolve, isAbsolute, } from "node:path";
16
- import { promises as fs } from "node:fs";
16
+ import { promises as fs, existsSync, mkdirSync, readdirSync, copyFileSync, rmSync, } from "node:fs";
17
+ import { createRequire } from "node:module";
17
18
  import { createHash, randomUUID } from "node:crypto";
18
19
  import { unzipSync } from "fflate";
19
20
  import { discoverReadonlyEntries, validDiscoveryName, } from "./readonly-discovery.js";
@@ -57,13 +58,180 @@ export function resolveDshHome() {
57
58
  export function resolveAgentsHome() {
58
59
  return process.env.DSH_AGENTS_HOME || join(homedir(), ".agents");
59
60
  }
61
+ // ── 官方内置技能来源(随宿主注入会话上下文的内置技能)──────────────────────
62
+ //
63
+ // 桌面版把 4 个官方技能注入每个会话(用户 2026-09-30 实测目录):office-docx /
64
+ // office-pptx / office-xlsx(dsh-skill-office,桌面宿主把 assetRoot 指到
65
+ // `<resources>/runtime/office-skills`)与 diagnose-windows-sandbox-acl
66
+ // (dsh-sandbox-windows-acl,source 'bundled',rank 600)。web 宿主一个都不注入
67
+ // (用户裁定:web 用不上这个功能)。它们或住在官方 asar 里、或由官方 provider
68
+ // 复制进临时目录自管,插件直接扫描/接管有两个坑:宿主子进程里对 asar 的 promises
69
+ // 读法与拦截器解析都出现过时灵时不灵(同机两次启动一组 4/1、一组全 0,实测);
70
+ // 沙盒技能的脚本要交给 PowerShell 子进程,asar 路径读不了。
71
+ //
72
+ // 因此这里把来源**物化**成插件自有的一份真实目录(`$DSH_HOME/tool-management/
73
+ // official-skills/`):每次宿主启动从官方源头整目录覆盖拷贝(与官方 provider 的
74
+ // 「私有资源副本」同一做法),扫描、启停、overlay、脚本执行全部落在真实文件上。
75
+ //
76
+ // 两个边界(都出过事):
77
+ // - 只有桌面版(有 process.resourcesPath)才允许进来。物化目录在 $DSH_HOME 下
78
+ // 与各宿主**共用**,而 web 宿主进程里的官方模块拦截器能把 @deepseek-ai 裸名
79
+ // 解析到宿主副本 —— web 若也进来重建,会把桌面物化好的整目录清成它解析到的
80
+ // 那几个(2026-09-30 实测:桌面物化好 4 个,web 一启动,两份 UI 同时只剩沙盒
81
+ // 1 个)。所以非桌面在碰目录**之前**就退出,一个技能都不列。
82
+ // - 预设参考技能(dsh-agent-preset/skills)不注入会话上下文,不列入(用户裁定)。
83
+ //
84
+ // 单个来源、随 DSH 技能之后排列,来源层语义同默认来源(不可移除、无来源开关)。
85
+ const OFFICIAL_SOURCE_KEY = "official";
86
+ /** electron 的 resources 目录;非桌面宿主(web 等普通 node 进程)没有这个属性。 */
87
+ function resourcesDir() {
88
+ const resources = process.resourcesPath;
89
+ return typeof resources === "string" && resources !== "" ? resources : null;
90
+ }
91
+ /** 桌面版官方 asar 里的包内目录(同步 fs 对 asar 的支持经过实测可靠)。 */
92
+ function desktopPackageDir(packageName, subpath) {
93
+ const resources = resourcesDir();
94
+ if (resources === null)
95
+ return null;
96
+ const dir = join(resources, "app.asar", "dsh", "node_modules", ...packageName.split("/"), ...subpath.split("/"));
97
+ return existsSync(dir) ? dir : null;
98
+ }
99
+ /**
100
+ * 从插件自身的解析链找官方包内目录(web 等非桌面宿主的途径;宿主进程里的裸名
101
+ * 解析由官方模块拦截器路由到宿主副本)。包不可解析(旧宿主 / 单测环境)返回 null。
102
+ */
103
+ function resolvedPackageDir(packageName, subpath) {
104
+ try {
105
+ const require = createRequire(import.meta.url);
106
+ let manifest;
107
+ try {
108
+ manifest = require.resolve(`${packageName}/package.json`);
109
+ }
110
+ catch {
111
+ // exports 未暴露 ./package.json 的包:从入口文件向上找同名 manifest。
112
+ manifest = "";
113
+ let dir = dirname(require.resolve(packageName));
114
+ for (let i = 0; i < 6; i += 1) {
115
+ const candidate = join(dir, "package.json");
116
+ if (existsSync(candidate)) {
117
+ manifest = candidate;
118
+ break;
119
+ }
120
+ const parent = dirname(dir);
121
+ if (parent === dir)
122
+ break;
123
+ dir = parent;
124
+ }
125
+ if (manifest === "")
126
+ return null;
127
+ }
128
+ const dir = join(dirname(manifest), ...subpath.split("/"));
129
+ return existsSync(dir) ? dir : null;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ /**
136
+ * office 技能源头。office 只在桌面版由宿主挂载(web 的补丁层没有它),所以只在
137
+ * 桌面环境里找:运行时真实目录优先(官方 assetRoot),其次 asar 包内 assets。
138
+ * 非桌面环境一律 null —— 不把没被注入的技能凭空列出来。
139
+ */
140
+ function officialOfficeSource() {
141
+ const resources = resourcesDir();
142
+ if (resources === null)
143
+ return null;
144
+ const runtimeDir = join(resources, "runtime", "office-skills");
145
+ if (existsSync(join(runtimeDir, "office-docx")))
146
+ return runtimeDir;
147
+ const asarDir = desktopPackageDir("@deepseek-ai/dsh-skill-office", "assets");
148
+ if (asarDir !== null && existsSync(join(asarDir, "office-docx")))
149
+ return asarDir;
150
+ return null;
151
+ }
152
+ /** 沙盒诊断技能源头(桌面版 asar 直连优先,其次本机解析链;仅 Windows 注入)。 */
153
+ function officialSandboxSource() {
154
+ if (process.platform !== "win32")
155
+ return null;
156
+ return (desktopPackageDir("@deepseek-ai/dsh-sandbox-windows-acl", "assets/diagnose-windows-sandbox-acl") ??
157
+ resolvedPackageDir("@deepseek-ai/dsh-sandbox-windows-acl", "assets/diagnose-windows-sandbox-acl"));
158
+ }
159
+ function copyDirectorySync(from, to) {
160
+ mkdirSync(to, { recursive: true });
161
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
162
+ const source = join(from, entry.name);
163
+ const target = join(to, entry.name);
164
+ if (entry.isDirectory())
165
+ copyDirectorySync(source, target);
166
+ else if (entry.isFile())
167
+ copyFileSync(source, target);
168
+ }
169
+ }
170
+ /**
171
+ * 物化官方内置技能到插件自有目录,返回该目录;无可用源头或拷贝失败返回 null。
172
+ * 每次宿主启动整目录重建(官方包只随宿主更新变化,宿主更新必然重启宿主)。
173
+ */
174
+ let officialRootCache;
175
+ function materializeOfficialSkills() {
176
+ // 非桌面宿主(web 等普通 node 进程)一个官方技能都不注入,也不许碰共享的物化
177
+ // 目录 —— 否则 web 启动会把它解析到的那几个盖进目录,桌面版物化好的就没了
178
+ // (见上方块注释)。必须在任何 fs 访问之前退出。
179
+ if (resourcesDir() === null)
180
+ return null;
181
+ const office = officialOfficeSource();
182
+ const sandbox = officialSandboxSource();
183
+ if (office === null && sandbox === null)
184
+ return null;
185
+ const root = join(resolveDshHome(), "tool-management", "official-skills");
186
+ try {
187
+ rmSync(root, { recursive: true, force: true });
188
+ // office:整个 assets 根拷进来(office-*/ 与 scripts/ 的相对结构必须保留 ——
189
+ // SKILL.md 用 `<skill-directory>/../scripts/check_office.py` 引用检查脚本)。
190
+ if (office !== null)
191
+ copyDirectorySync(office, root);
192
+ if (sandbox !== null) {
193
+ copyDirectorySync(sandbox, join(root, basename(sandbox)));
194
+ }
195
+ return root;
196
+ }
197
+ catch (error) {
198
+ console.error("[dsh-plugin-tool-management] official skills materialization failed:", String((error && error.message) || error));
199
+ return null;
200
+ }
201
+ }
202
+ function officialSkillRoot() {
203
+ if (officialRootCache !== undefined)
204
+ return officialRootCache;
205
+ const dir = materializeOfficialSkills();
206
+ officialRootCache =
207
+ dir === null
208
+ ? null
209
+ : {
210
+ key: OFFICIAL_SOURCE_KEY,
211
+ path: dir,
212
+ label: "官方内置",
213
+ localeKey: "official",
214
+ mutable: false,
215
+ deletable: false,
216
+ // 来源层无开关(同默认来源),单个技能可启停;overlay 以 599 压过官方
217
+ // provider 的 600,脚本从物化副本的真实路径执行。
218
+ toggleable: true,
219
+ native: false,
220
+ scope: "user",
221
+ rank: 599,
222
+ };
223
+ return officialRootCache;
224
+ }
60
225
  /**
61
226
  * DSH 与外部 Agent 的用户级技能目录(4 个来源;外部来源由 manager provider 接入)。
62
227
  *
63
228
  * rank 与官方 filesystem provider 的用户级优先级衔接:DSH=400、Agents=500。
64
229
  * manager provider 以 450 接管公共 Agents(仍低于 DSH),Codex/Claude 依次排在其后。
230
+ * DSH 之后紧随官方内置来源(随宿主注入会话的内置技能,见 materializeOfficialSkills;
231
+ * 无可用源头时自然缺席)。
65
232
  */
66
233
  export function userRoots() {
234
+ const official = officialSkillRoot();
67
235
  return [
68
236
  {
69
237
  key: "dsh",
@@ -78,6 +246,8 @@ export function userRoots() {
78
246
  native: true,
79
247
  rank: 400,
80
248
  },
249
+ // 官方内置紧随 DSH 技能之后(用户裁定):同为一眼要看的来源,排在导入技能之前。
250
+ ...(official === null ? [] : [official]),
81
251
  {
82
252
  // v0.4:插件**新建/导入**的技能落到 hub 内(用户要求:插件产生的文件收在
83
253
  // $DSH_HOME/tool-management/)。rank 高于 DSH 技能,同名时 hub 版本遮蔽官方目录里的;
@@ -139,8 +309,8 @@ export function userRoots() {
139
309
  *
140
310
  * 一句话记法:**`mutable` 与 `deletable` 同向,`removable` 与 `mutable` 反向。**
141
311
  */
142
- const DEFAULT_SOURCE_KEYS = Object.freeze(["dsh", "hub"]);
143
- /** 是否为默认来源。接受 root 对象或 key 字符串;`dsh` / `hub` 的所有特判都走这里。 */
312
+ const DEFAULT_SOURCE_KEYS = Object.freeze(["dsh", "hub", OFFICIAL_SOURCE_KEY]);
313
+ /** 是否为默认来源。接受 root 对象或 key 字符串;`dsh` / `hub` / `official` 的所有特判都走这里。 */
144
314
  export function isDefaultSkillSource(rootOrKey) {
145
315
  const key = rootOrKey && typeof rootOrKey === "object"
146
316
  ? rootOrKey.key
@@ -582,7 +752,9 @@ function reservedSourceError(root) {
582
752
  const key = root && root.key ? root.key : "";
583
753
  const origin = key === "dsh"
584
754
  ? "DSH 技能目录是默认来源,必须读取"
585
- : "导入技能目录是默认来源、插件新建/导入的落点,必须读取";
755
+ : key === "hub"
756
+ ? "导入技能目录是默认来源、插件新建/导入的落点,必须读取"
757
+ : "官方内置技能目录随宿主发布,不能移除(移除只会让插件失去管理权)";
586
758
  return {
587
759
  ok: false,
588
760
  code: "error.source.reserved",
@@ -2894,11 +3066,13 @@ export async function listProviderCandidates(options = {}) {
2894
3066
  provider: "dsh-plugin-tool-management-external",
2895
3067
  source: project
2896
3068
  ? root.kind
2897
- : root.key === "dsh"
2898
- ? "user-dsh"
2899
- : CUSTOM_ROOT_KEY_RE.test(root.key)
2900
- ? "custom"
2901
- : `agent-${root.key}`,
3069
+ : root.key === "official"
3070
+ ? "bundled"
3071
+ : root.key === "dsh"
3072
+ ? "user-dsh"
3073
+ : CUSTOM_ROOT_KEY_RE.test(root.key)
3074
+ ? "custom"
3075
+ : `agent-${root.key}`,
2902
3076
  rank: project
2903
3077
  ? root.rank - 1
2904
3078
  : root.key === "dsh"
@@ -410,7 +410,11 @@ export function createSkillsService(ctx) {
410
410
  console.error('[dsh-plugin-tool-management] agent-scoped provider setup failed:', message(e));
411
411
  }
412
412
  try {
413
- const watchPaths = roots.map((root) => String((root && root.path) || '')).filter(Boolean);
413
+ const watchPaths = roots
414
+ // 官方内置来源是宿主启动时物化的只读副本,不会中途变化,不起 watcher。
415
+ .filter((root) => root.key !== 'official')
416
+ .map((root) => String((root && root.path) || ''))
417
+ .filter(Boolean);
414
418
  disposers.push(watchDirectories(watchPaths, invalidateSkills));
415
419
  // 自定义目录在状态文件里,注册时读取一次并一并监听(后添加的目录由
416
420
  // provider 首次 list 天然覆盖,等插件重载后才有 watcher)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-tool-management",
3
- "version": "0.15.0",
3
+ "version": "0.16.5",
4
4
  "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagent personas and AGENTS.md presets — a scene switches the whole environment, and the model can drive it from chat too.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,17 +66,17 @@
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@deepseek-ai/cordis": "^4.0.2",
69
- "@deepseek-ai/dsh-agent-preset-registry": "^0.1.5-rc.2 || ^0.1.7-rc.2",
70
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2 || ^0.1.7-rc.2",
71
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2 || ^0.1.7-rc.2",
72
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2 || ^0.1.7-rc.2",
73
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2 || ^0.1.7-rc.2",
74
- "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2 || ^0.1.7-rc.2",
75
- "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2 || ^0.1.7-rc.2",
76
- "@deepseek-ai/dsh-subagent-fork-in-process": "^0.1.5-rc.2 || ^0.1.7-rc.2",
77
- "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.1.5-rc.2 || ^0.1.7-rc.2",
78
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2 || ^0.1.7-rc.2",
79
- "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2 || ^0.1.7-rc.2",
69
+ "@deepseek-ai/dsh-agent-preset-registry": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
70
+ "@deepseek-ai/dsh-llm": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
71
+ "@deepseek-ai/dsh-session": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
72
+ "@deepseek-ai/dsh-session-projection-cache": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
73
+ "@deepseek-ai/dsh-settings": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
74
+ "@deepseek-ai/dsh-spill-local": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
75
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
76
+ "@deepseek-ai/dsh-subagent-fork-in-process": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
77
+ "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
78
+ "@deepseek-ai/dsh-tools": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
79
+ "@deepseek-ai/dsh-workspace": ">=0.1.7-rc.2 || >=0.2.0-rc.1",
80
80
  "react": "^18.2.0"
81
81
  },
82
82
  "peerDependenciesMeta": {
@@ -89,19 +89,20 @@
89
89
  },
90
90
  "devDependencies": {
91
91
  "@deepseek-ai/cordis": "^4.0.1",
92
- "@deepseek-ai/dsh-agent-preset-registry": "^0.1.5-rc.2 || ^0.1.7-rc.2",
93
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2 || ^0.1.7-rc.2",
94
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2 || ^0.1.7-rc.2",
95
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2 || ^0.1.7-rc.2",
96
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2 || ^0.1.7-rc.2",
97
- "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2 || ^0.1.7-rc.2",
98
- "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2 || ^0.1.7-rc.2",
99
- "@deepseek-ai/dsh-subagent-fork-in-process": "^0.1.5-rc.2 || ^0.1.7-rc.2",
100
- "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.1.5-rc.2 || ^0.1.7-rc.2",
101
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2 || ^0.1.7-rc.2",
102
- "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2 || ^0.1.7-rc.2",
92
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.2.0-rc.2",
93
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
94
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.2",
95
+ "@deepseek-ai/dsh-session-projection-cache": "^0.2.0-rc.2",
96
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
97
+ "@deepseek-ai/dsh-spill-local": "^0.2.0-rc.2",
98
+ "@deepseek-ai/dsh-storage-domain": "^0.2.0-rc.2",
99
+ "@deepseek-ai/dsh-subagent-fork-in-process": "^0.2.0-rc.2",
100
+ "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.2.0-rc.2",
101
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
102
+ "@deepseek-ai/dsh-workspace": "^0.2.0-rc.2",
103
103
  "@types/js-yaml": "^4.0.9",
104
104
  "@types/node": "^24.0.0",
105
+ "react": "^18.2.0",
105
106
  "typescript": "^6.0.0"
106
107
  },
107
108
  "license": "MIT",