dsh-plugin-tool-management 0.16.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.
- package/CHANGELOG.md +24 -0
- package/docs/update.md +11 -0
- package/lib/client.js +57 -12
- package/lib/compat/probe.js +72 -12
- package/lib/mcp/manager.js +2 -2
- package/lib/ops/compat.js +7 -3
- package/lib/skills/core.js +183 -9
- package/lib/skills/service.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [0.16.5] - 2026-09-30
|
|
14
|
+
|
|
15
|
+
issue #1 收尾与桌面版第二轮适配:兼容页身份比对改为逐包解析、逐包核对(桌面版不再误报「两份不同拷贝」),「全部可用」与「N 项不可用」不再同屏;桌面版注入会话的 4 个官方内置技能接入技能页管理。
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- **桌面版兼容页误报 5 条「两份不同拷贝」**:宿主侧的身份比对把「realpath 机制整轮不变」错写成了连**解析结果**一起复用 —— 第一个包(cordis)的宿主侧入口路径被当成其余五个包的宿主侧路径,五条全部比成「与 cordis 不同」→ 5 条假阻塞项,banner「能力 18/18 · 全部可用」与红卡「5 项不可用」同屏(桌面版 0.2.0-rc.2 实测全中)。现在逐包解析、逐包取真实形态 —— 解析本就便宜,Node 自身还有缓存;同日新旧对比实测:旧代码 1 真 / 5 假 + 5 条阻塞,修复后 6 真 / 0 阻塞。
|
|
20
|
+
- **安装形态下的修复指引不再是跑不了的命令**(issue #1 次要问题 1):阻塞项原文让人去跑 `node scripts/host-deps.mjs --fix`,而 `package.json` 的 `files` 不含 `scripts/`(开发者脚本本就不该随发布走),npm 装出来的用户照做只会拿到 `MODULE_NOT_FOUND` —— 指引指了一条不存在的路。现在按**实际形态**给:源码检出(插件旁边就有 `scripts/host-deps.mjs`)保留原文,并点明要「在插件源码目录」跑;npm 安装形态改说「升级或重装本插件后仍然如此再报」。兼容页底部那句「可运行 node scripts/doctor.mjs 查看」同理分两键,安装形态下不再指命令。
|
|
21
|
+
- **路径取不到真实形态时不再编一个「两份拷贝」的结论**(issue #1 次要问题 2):`realPathOf()` 以前 `catch { return value }`,把「这条路径不可解析」静默降级成「拿输入去比较」—— 于是路径/权限问题看起来就是「插件与宿主加载的是两份不同拷贝」,用户照着它去修只会白费功夫。现在取不到真实形态就**如实记原因**(`error.code` 与消息原文),这类包给 `sameAsHost = null`(比不了)并单独出一条说明,带包名、哪一侧(`host` / `plugin`)、解析到的路径与失败原因;界面显示「无法比较(路径取不到真实形态)」并列出两侧。
|
|
22
|
+
- **兼容页不再一边「全部可用」一边挂着红卡**:能力注脚与红卡高亮此前把「身份待处理」(两份拷贝这类 —— 不影响能力可用性)混进了能力降级计数。现在两处只看能力降级数;汇总行三分:全可用 / 能力降级 / 身份待处理(新增词典键 `compat.summary.identity`),各说各的。
|
|
23
|
+
|
|
24
|
+
### 新增
|
|
25
|
+
|
|
26
|
+
- **官方内置技能接入管理**:桌面版宿主向每个会话注入 4 个官方内置技能(office-docx / office-pptx / office-xlsx / diagnose-windows-sandbox-acl),此前技能页看不见、也关不掉。现在它们以单一「官方内置」来源出现在技能页,紧跟 DSH 技能之后:每次宿主启动从官方源头整目录**物化**成插件自有的一份真实目录(`$DSH_HOME/tool-management/official-skills/`;office 取自桌面运行时目录 `<resources>/runtime/office-skills`,沙盒诊断取自官方 asar 包内资源 —— 官方自己的副本一个在 asar 里、一个由官方 provider 复制进临时目录自管,直接指着读既不稳,也没法让 PowerShell / Python 子进程执行脚本),扫描、启停、脚本执行全部落在真实文件上;单个技能可启停(overlay 以 rank 599 压过官方 provider 的 600),来源层与默认来源同语义(不可移除、无来源开关、源文件只读)。web 宿主不注入任何官方内置技能,这个来源**不出现** —— 物化目录虽与各宿主共用,非桌面宿主绝不进来重建(web 宿主进程里的官方模块拦截器能把 `@deepseek-ai` 裸名解析到宿主副本,若也重建会把桌面物化好的整目录清成它解析到的那几个;2026-09-30 实测两份 UI 同时只剩沙盒 1 个)。预设参考技能(dsh-agent-preset/skills)不注入会话上下文,不列(用户裁定)。
|
|
27
|
+
|
|
28
|
+
### 变更
|
|
29
|
+
|
|
30
|
+
- **源码检出与 npm 安装形态的行为差异,只在指引上一处**:`/dsh-plugin-tool-management` 里有没有 `scripts/host-deps.mjs` 决定给哪句指引(本插件不从 `scripts/` 加载任何东西),其余判定完全一致。
|
|
31
|
+
|
|
32
|
+
### 说明
|
|
33
|
+
|
|
34
|
+
- **收尾范围**:issue #1 的主因(`import.meta.resolve` 的 URL 未解码,含空格路径必现;macOS 上开头斜杠被一起删掉)已在 0.16.0 随 `fileURLToPath` 修掉,本版带次要两条的收尾、身份比对的逐包核对与官方内置技能管理。
|
|
35
|
+
- **实测**:源码检出与安装形态两种位置的 `isSourceInstall()` 分别得 `true` / `false`;注入一个「能解析、realpath 必失败」的目标跑真实代码,产出 `kind:"realpath"`、`side:"plugin"`、原因的 `ENOENT` 原文,且**不再出现在阻塞项里**;悬空 junction 的语义实测为 `existsSync=false`、`import.meta.resolve` → `ERR_MODULE_NOT_FOUND`(因此走「宿主锚点里找不到它」的 `unverified`,同样不会误报成两份拷贝)。官方内置技能按双模式离线验证:web(无 `process.resourcesPath`)不出现该来源且不触碰共享物化目录,桌面物化 4 个技能、来源紧跟 DSH 技能、office 的共享 `scripts/` 相对结构保留。
|
|
36
|
+
|
|
13
37
|
## [0.16.0] - 2026-09-29
|
|
14
38
|
|
|
15
39
|
### 修复
|
package/docs/update.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| 版本 | 发布 | 主题 |
|
|
6
6
|
| ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| [0.16.5](#0165---2026-09-30) | 2026-09-30 | 桌面版误报 5 条「两份拷贝」修掉(身份逐包核对)+ 「全部可用」与红卡不再同屏 + 官方内置技能接入管理(桌面版注入的 4 个可启停;web 不出现) |
|
|
7
8
|
| [0.16.0](#0160---2026-09-29) | 2026-09-29 | 桌面版宿主身份误判修好(归档宿主按版本核对,带空格路径的宿主版本不再显示 unknown)+ 兼容页三张指标卡各删一行、适配版本改标 0.2.0-rc.2 + 四个域的行状态统一成名字后面的胶囊(已启用 / 未启用,停用类改灰)+ MCP 体检点挪到名字前 + 体检补端点探测 + 删除同步清引用 + 轮询不空转 + 子智能体补上「已启用」 |
|
|
8
9
|
| [0.15.0](#0150---2026-09-28) | 2026-09-28 | 适配 DSH 0.1.7-rc.2:修好 MCP 列表与注入 + 预设边界与极简兜底注入 + 会话标题 + 设置令牌 + host-deps / doctor;新增 MCP 导入导出、配置体检、最近改动流水、场景绑工具表方案、整机迁移快照;功能总览英文补齐、兼容页收弹窗与界面打磨 |
|
|
9
10
|
| [0.14.0](#0140---2026-09-23) | 2026-09-23 | 场景与记忆分家(共 20 条工具)+ 模型能进入 / 退出场景 + 场景档案对模型可见、改档案会报出被挤掉的项 + 注入只写"现在是什么"(省 21.9%)+ 十五条工具出厂默认关(每轮实发 5 条 ≈1,090)+ 工具表可存方案一键换回 + 人设思考强度 + 改 MCP 配置不碰明文凭据 + 提示词「生效中」修好 |
|
|
@@ -24,6 +25,16 @@
|
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
28
|
+
## 0.16.5 - 2026-09-30
|
|
29
|
+
|
|
30
|
+
- **桌面版不再误报 5 条「两份不同拷贝」**:宿主侧身份比对把第一个包(cordis)的路径当成其余五个包的,五条全部比成「不同」。现在逐包解析、逐包核对。
|
|
31
|
+
- **「全部可用」与红卡不再同屏**:身份待处理(不影响能力可用性)不再混进能力降级计数,汇总行按 全可用 / 能力降级 / 身份待处理 分开说。
|
|
32
|
+
- **官方内置技能能管了**:桌面版注入每个会话的 4 个官方技能(Office 三件套 + Windows 沙盒诊断)以「官方内置」来源出现在技能页、紧跟 DSH 技能,单个可启停、脚本照常能跑;web 不注入,这个来源在 web 上不出现。
|
|
33
|
+
- **修复指引按装法给**(issue #1):那块「两份不同拷贝」以前一律让人去跑 `node scripts/host-deps.mjs --fix`,可 npm 装出来的插件里**没有** `scripts/` 这个目录(发布不带开发脚本),照着做只会 `MODULE_NOT_FOUND`。现在源码检出给原指引(并写明要在源码目录跑),npm 安装形态改说「升级或重装后仍然如此再报」;兼容页底部那句自查命令同样分两种形态。
|
|
34
|
+
- **路径问题不再冒充模块问题**(issue #1):以前路径取不到真实形态时会静默拿原字符串去比对,于是「读不到路径」被报成「插件与宿主加载的是两份不同拷贝」—— 结论是编的。现在如实说「无法比较(路径取不到真实形态)」,并列出是哪一侧、解析到哪条路径、失败原因原文。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
27
38
|
## 0.16.0 - 2026-09-29
|
|
28
39
|
|
|
29
40
|
- **桌面版不再被误判成「两份不同拷贝」**:桌面版把官方包封在 `resources/app.asar` 里,插件在归档外,位置天生不同 —— 以前兼容页把这当成 6 条阻塞项,还指路一条会把依赖降级到旧宿主的命令。现在归档宿主按**版本**核对:版本一致就只记一句说明(同版本、仅打包方式不同),版本真不一致才报阻塞;顺带修好带空格安装路径下「宿主版本显示 unknown」的老毛病(`import.meta.resolve` 的 `%20` 没解码)。web 版行为不变。
|
package/lib/client.js
CHANGED
|
@@ -2359,13 +2359,18 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2359
2359
|
// 它自带的 summary 是中文,直接渲染会让英文界面露出中文)。
|
|
2360
2360
|
const findingsN = Array.isArray(data.findings) ? data.findings.length : 0
|
|
2361
2361
|
const degradedN = Array.isArray(data.degraded) ? data.degraded.length : 0
|
|
2362
|
+
// 摘要三形态:能力降级 / 仅身份待处理 / 全部可用。仅身份待处理时**不能**说
|
|
2363
|
+
// 「全部可用」—— 横幅已转红(有阻塞项),摘要却报一切正常,两句自相矛盾
|
|
2364
|
+
// (2026-09-30 桌面版实测)。
|
|
2365
|
+
const summaryKey = degradedN > 0 ? 'compat.summary.degraded'
|
|
2366
|
+
: blockedCount > 0 ? 'compat.summary.identity' : 'compat.summary.ok'
|
|
2362
2367
|
const summaryText = findingsN === 0
|
|
2363
2368
|
? (data.summary || '')
|
|
2364
|
-
: t(
|
|
2369
|
+
: t(summaryKey)
|
|
2365
2370
|
.replace('{version}', String((data.host && data.host.version) || '?'))
|
|
2366
2371
|
.replace('{ok}', String(findingsN - degradedN))
|
|
2367
2372
|
.replace('{total}', String(findingsN))
|
|
2368
|
-
.replace('{count}', String(degradedN))
|
|
2373
|
+
.replace('{count}', String(degradedN > 0 ? degradedN : blockedCount))
|
|
2369
2374
|
push(React.createElement('div', { className: 'dsm-compat-bar' + (blockedCount > 0 ? ' dsm-compat-bar-warn' : '') },
|
|
2370
2375
|
React.createElement('span', { className: 'dsm-compat-dot' + (blockedCount > 0 ? ' dsm-compat-dot-warn' : '') }),
|
|
2371
2376
|
React.createElement('strong', null, blockedCount > 0 ? t('compat.state.blocked') : t('compat.state.ok')),
|
|
@@ -2377,13 +2382,16 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2377
2382
|
// 「本插件按此版本验证」—— 前者是安装期门槛,后者是同一件事说两遍);
|
|
2378
2383
|
// * 可用能力:全通过时不再写「全部通过」(分子分母已经说了这件事),只在有降级
|
|
2379
2384
|
// 或有插件自补时说清楚是哪一种(2026-09-29 用户裁定)。
|
|
2380
|
-
|
|
2381
|
-
|
|
2385
|
+
// * 注脚与红色只跟**能力**降级走(degraded):身份阻塞项(两份拷贝 / 待处理)
|
|
2386
|
+
// 不是能力不可用 —— 此前用 blockedCount(能力 + 身份)当注脚数,桌面版上出现
|
|
2387
|
+
// 过「可用能力 18/18」旁边标红「5 项不可用」的自相矛盾(2026-09-30 实测)。
|
|
2388
|
+
const capsNote = degraded.length > 0
|
|
2389
|
+
? t('compat.caps.degraded').replace('{count}', String(degraded.length))
|
|
2382
2390
|
: (usableCount === findings.length ? null : t('compat.caps.substituted'))
|
|
2383
2391
|
push(React.createElement('div', { className: 'dsm-compat-grid' },
|
|
2384
2392
|
card(t('compat.host'), (data.host && data.host.version) || '?', null, false),
|
|
2385
2393
|
card(t('compat.verified'), data.verifiedVersion || '?', null, false),
|
|
2386
|
-
card(t('compat.caps'), String(usableCount) + '/' + String(findings.length), capsNote,
|
|
2394
|
+
card(t('compat.caps'), String(usableCount) + '/' + String(findings.length), capsNote, degraded.length > 0)))
|
|
2387
2395
|
|
|
2388
2396
|
// 版本栅栏(B3):实际宿主与本插件验证过的版本不同时说出来 —— 否则"照旧版本的印象
|
|
2389
2397
|
// 判断"会让人把探测结果当成 bug。只提示、不拦(peer 保持无上界是既定决策)。
|
|
@@ -2490,15 +2498,24 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2490
2498
|
})[0]
|
|
2491
2499
|
const asarPackages = (asarNote && asarNote.packages) || []
|
|
2492
2500
|
const asarNames = asarPackages.map(function (item) { return String(item && item.name || '') })
|
|
2501
|
+
// 真实形态取不到的包:那一格同样「无法比较」,但原因与归档无关,得分开标。
|
|
2502
|
+
const realpathNoteForPill = identityNotes.filter(function (note) {
|
|
2503
|
+
return note && note.kind === 'realpath'
|
|
2504
|
+
})[0]
|
|
2505
|
+
const realpathNames = ((realpathNoteForPill && realpathNoteForPill.failures) || []).map(function (item) {
|
|
2506
|
+
return String(item && item.name || '')
|
|
2507
|
+
})
|
|
2493
2508
|
push(section(t('compat.modules'), t('compat.modules.hint'),
|
|
2494
2509
|
React.createElement('div', { className: 'dsm-compat-mod-list' },
|
|
2495
2510
|
Object.keys(sameAsHost).map(function (name) {
|
|
2496
2511
|
const same = sameAsHost[name]
|
|
2512
|
+
const inRealpath = realpathNames.indexOf(name) !== -1
|
|
2497
2513
|
const inAsar = asarNames.indexOf(name) !== -1
|
|
2498
2514
|
const cls = same === true ? 'dsm-compat-pill-ok' : same === false ? 'dsm-compat-pill-bad' : ''
|
|
2499
2515
|
const text = same === true ? t('compat.module.same')
|
|
2500
2516
|
: same === false ? t('compat.module.separate')
|
|
2501
|
-
:
|
|
2517
|
+
: inRealpath ? t('compat.module.realpath')
|
|
2518
|
+
: inAsar ? t('compat.module.sameVersion') : t('compat.module.unknown')
|
|
2502
2519
|
return React.createElement('div', { className: 'dsm-compat-mod-row', key: name },
|
|
2503
2520
|
React.createElement('span', { className: 'dsm-compat-name dsm-compat-mod' }, name),
|
|
2504
2521
|
React.createElement('span', { className: 'dsm-compat-pill ' + cls }, text))
|
|
@@ -2527,10 +2544,34 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2527
2544
|
.replace('{count}', String(unverified.length))
|
|
2528
2545
|
.replace('{names}', unverified.join('、'))))
|
|
2529
2546
|
}
|
|
2547
|
+
// 路径取不到真实形态:这一格是「比不了」,不是「两份拷贝」。必须把原因摆出来 ——
|
|
2548
|
+
// 否则用户只会看到一排「无法比较」,而不知道是路径/权限的问题(issue #1)。
|
|
2549
|
+
const realpathNote = identityNotes.filter(function (note) {
|
|
2550
|
+
return note && note.kind === 'realpath'
|
|
2551
|
+
})[0]
|
|
2552
|
+
if (realpathNote) {
|
|
2553
|
+
const failures = realpathNote.failures || []
|
|
2554
|
+
const names = failures.map(function (item) { return String(item && item.name || '') })
|
|
2555
|
+
const sideText = function (side) {
|
|
2556
|
+
return failures.filter(function (item) { return item && item.side === side })
|
|
2557
|
+
.map(function (item) { return String(item && item.path || '') + '(' + String(item && item.reason || '') + ')' })
|
|
2558
|
+
.join(';')
|
|
2559
|
+
}
|
|
2560
|
+
push(React.createElement('p', { className: 'dsm-help' },
|
|
2561
|
+
t('compat.modules.realpath')
|
|
2562
|
+
.replace('{count}', String(failures.length))
|
|
2563
|
+
.replace('{names}', names.join('、'))))
|
|
2564
|
+
push(React.createElement('p', { className: 'dsm-help' },
|
|
2565
|
+
t('compat.modules.realpathSides')
|
|
2566
|
+
.replace('{host}', sideText('host') || t('compat.modules.realpathNone'))
|
|
2567
|
+
.replace('{plugin}', sideText('plugin') || t('compat.modules.realpathNone'))))
|
|
2568
|
+
}
|
|
2530
2569
|
|
|
2531
2570
|
push(React.createElement('p', { className: 'dsm-help' },
|
|
2532
|
-
t('compat.hint.doctor'),
|
|
2533
|
-
|
|
2571
|
+
t(data && data.sourceInstall === true ? 'compat.hint.doctor' : 'compat.hint.doctorRelease'),
|
|
2572
|
+
data && data.sourceInstall === true
|
|
2573
|
+
? React.createElement('code', { className: 'dsm-compat-code' }, ' node scripts/doctor.mjs')
|
|
2574
|
+
: null))
|
|
2534
2575
|
}
|
|
2535
2576
|
|
|
2536
2577
|
// 预设注入边界:五类内容(记忆 / 全局提示词 / 技能 / MCP / 子智能体)在**每个 Agent
|
|
@@ -3271,7 +3312,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3271
3312
|
"mcp.inspect.check.badTransport": "transport 取值不认识:{transport}",
|
|
3272
3313
|
"mcp.inspect.check.noEndpoint": "既没有 command 也没有 url",
|
|
3273
3314
|
"mcp.inspect.check.nameDup": "服务名「{name}」被 {count} 个不同 id 共用:模型看到的工具名会撞车",
|
|
3274
|
-
"root.dsh": "DSH 技能", "root.hub": "导入技能", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
|
|
3315
|
+
"root.dsh": "DSH 技能", "root.hub": "导入技能", "root.official": "官方内置", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
|
|
3275
3316
|
"memory.title": "记忆",
|
|
3276
3317
|
"tabs.scenes": "场景", "tabs.skills": "技能", "tabs.subagents": "子智能体", "tabs.prompts": "提示词", "tabs.memory": "记忆", "tabs.sessions": "会话", "tabs.compat": "兼容",
|
|
3277
3318
|
"compat.title": "宿主兼容", "compat.desc": "本插件与 DSH 的对接体检(只读)。",
|
|
@@ -3321,6 +3362,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3321
3362
|
"compat.state.ok": "一切正常", "compat.state.blocked": "有需要处理的问题",
|
|
3322
3363
|
"compat.checking": "检查中…", "compat.empty": "尚未检查", "compat.failed": "检查失败",
|
|
3323
3364
|
"compat.summary.ok": "宿主 {version} · 能力 {ok}/{total} · 全部可用", "compat.summary.degraded": "宿主 {version} · 能力 {ok}/{total} · 降级 {count} 项",
|
|
3365
|
+
"compat.summary.identity": "宿主 {version} · 能力 {ok}/{total} · 身份 {count} 项待处理",
|
|
3324
3366
|
"compat.host": "宿主版本", "compat.verified": "插件适配版本",
|
|
3325
3367
|
"compat.caps": "可用能力", "compat.caps.degraded": "{count} 项不可用",
|
|
3326
3368
|
"compat.caps.substituted": "缺口由插件自补",
|
|
@@ -3333,7 +3375,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3333
3375
|
"compat.others": "其它能力项", "compat.others.hint": "不算降级:宿主没有这些入口时,由插件自己顶上",
|
|
3334
3376
|
"compat.fallback.substituted": "插件适配层(宿主无此入口)",
|
|
3335
3377
|
"compat.fallback.native": "改走原生入口", "compat.fallback.refuse": "该操作会被拒绝", "compat.fallback.blocked": "相关按钮已禁用", "compat.fallback.inform": "不影响写入",
|
|
3336
|
-
"compat.modules": "模块实体(插件 vs 宿主)", "compat.modules.hint": "同一份模块才谈得上适配", "compat.module.same": "同一份模块", "compat.module.separate": "两份拷贝(需修复)", "compat.module.sameVersion": "同版本(分处归档内外)", "compat.module.unknown": "无法比较", "compat.modules.unverified": "有 {count} 个包没能比对(宿主的解析锚点里找不到:{names})—— 这不是「两份拷贝」,而是这项校验没做成。可运行 node scripts/doctor.mjs 查看。",
|
|
3378
|
+
"compat.modules": "模块实体(插件 vs 宿主)", "compat.modules.hint": "同一份模块才谈得上适配", "compat.module.same": "同一份模块", "compat.module.separate": "两份拷贝(需修复)", "compat.module.sameVersion": "同版本(分处归档内外)", "compat.module.unknown": "无法比较", "compat.module.realpath": "无法比较(路径取不到真实形态)", "compat.modules.realpath": "有 {count} 个包**比不了**:它们的路径取不到真实形态({names})—— 这不是「两份拷贝」,而是这项校验没做成。", "compat.modules.realpathSides": "宿主侧:{host};插件侧:{plugin}。修好路径(权限 / 路径长度 / 重新安装)后重新打开本页即重算。", "compat.modules.realpathNone": "无", "compat.modules.unverified": "有 {count} 个包没能比对(宿主的解析锚点里找不到:{names})—— 这不是「两份拷贝」,而是这项校验没做成。可运行 node scripts/doctor.mjs 查看。",
|
|
3337
3379
|
"compat.modules.asarHost": "宿主是桌面版:官方包装在 {name} 里,插件在归档外,两边不可能是同一份文件。下面 {count} 个包按版本核对:{version},一致。",
|
|
3338
3380
|
"compat.modules.asarHostPackages": "同版本、仅打包方式不同的 {count} 个包({version}):{names}。本机不要跑 host-deps.mjs --fix —— 那条命令会把插件依赖指向 npx 缓存里旧版宿主,而不是归档里的当前宿主。",
|
|
3339
3381
|
"compat.blockers": "阻塞项(按此修复)",
|
|
@@ -3537,6 +3579,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3537
3579
|
"compat.reach.tools.2": "场景 / 记忆 / MCP / 技能 / 子智能体 / 提示词 —— 由本插件按上面的「注入」块补齐。",
|
|
3538
3580
|
"compat.reach.tools.3": "提示词与技能官方自己也会送;官方有的不重复送,官方缺的(如极简)由本插件补上。",
|
|
3539
3581
|
"compat.hint.doctor": "命令行体检:",
|
|
3582
|
+
"compat.hint.doctorRelease": "本插件是从 npm 安装的(不带开发脚本);要命令行自查请在插件源码目录运行 node scripts/doctor.mjs。",
|
|
3540
3583
|
"scenes.title": "场景", "scenes.desc": "管理场景:预设 MCP、技能、子智能体与记忆的组合,一键切换。",
|
|
3541
3584
|
"scenes.stat.total": "个场景", "scenes.stat.active": "个已启用", "scenes.stat.archives": "个有档案",
|
|
3542
3585
|
"scenes.noDesc": "还没有描述",
|
|
@@ -3831,7 +3874,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3831
3874
|
"mcp.inspect.check.badTransport": "Unrecognized transport: {transport}",
|
|
3832
3875
|
"mcp.inspect.check.noEndpoint": "Neither command nor URL",
|
|
3833
3876
|
"mcp.inspect.check.nameDup": "Server name “{name}” is shared by {count} different ids — the model sees colliding tool names",
|
|
3834
|
-
"root.dsh": "DSH skills", "root.hub": "Imported skills", "root.agents": "Shared Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
|
|
3877
|
+
"root.dsh": "DSH skills", "root.hub": "Imported skills", "root.official": "Official built-in", "root.agents": "Shared Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
|
|
3835
3878
|
"memory.title": "Memories",
|
|
3836
3879
|
"tabs.scenes": "Scenes", "tabs.skills": "Skills", "tabs.subagents": "Subagents", "tabs.prompts": "Prompts", "tabs.memory": "Memories", "tabs.sessions": "Sessions", "tabs.compat": "Host",
|
|
3837
3880
|
"compat.title": "Host compatibility", "compat.desc": "How this plugin attaches to DSH (read-only).",
|
|
@@ -3881,6 +3924,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3881
3924
|
"compat.state.ok": "All good", "compat.state.blocked": "Needs attention",
|
|
3882
3925
|
"compat.checking": "Checking…", "compat.empty": "Not checked yet", "compat.failed": "Check failed",
|
|
3883
3926
|
"compat.summary.ok": "host {version} · capabilities {ok}/{total} · all usable", "compat.summary.degraded": "host {version} · capabilities {ok}/{total} · {count} degraded",
|
|
3927
|
+
"compat.summary.identity": "host {version} · capabilities {ok}/{total} · {count} identity issue(s) to resolve",
|
|
3884
3928
|
"compat.host": "Host version", "compat.verified": "Verified against",
|
|
3885
3929
|
"compat.caps": "Usable capabilities", "compat.caps.degraded": "{count} unavailable",
|
|
3886
3930
|
"compat.caps.substituted": "gaps covered by the plugin",
|
|
@@ -3893,7 +3937,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3893
3937
|
"compat.others": "Other capability slots", "compat.others.hint": "not degraded: the plugin supplies its own implementation when the host has no such entry",
|
|
3894
3938
|
"compat.fallback.substituted": "Plugin adapter (no host entry)",
|
|
3895
3939
|
"compat.fallback.native": "Falls back to native entry", "compat.fallback.refuse": "Operation refused", "compat.fallback.blocked": "Affected buttons disabled", "compat.fallback.inform": "Writes unaffected",
|
|
3896
|
-
"compat.modules": "Module identity (plugin vs host)", "compat.modules.hint": "adapting only works on one shared module", "compat.module.same": "same module", "compat.module.separate": "separate copy (repair needed)", "compat.module.sameVersion": "same version (inside/outside the archive)", "compat.module.unknown": "not comparable", "compat.modules.unverified": "{count} package(s) could not be compared (not resolvable from the host anchor: {names}) — that is not \"a separate copy\", it means the check could not run. Try node scripts/doctor.mjs.",
|
|
3940
|
+
"compat.modules": "Module identity (plugin vs host)", "compat.modules.hint": "adapting only works on one shared module", "compat.module.same": "same module", "compat.module.separate": "separate copy (repair needed)", "compat.module.sameVersion": "same version (inside/outside the archive)", "compat.module.unknown": "not comparable", "compat.module.realpath": "not comparable (real path unavailable)", "compat.modules.realpath": "{count} package(s) could **not be compared**: their real path could not be read ({names}) — that is not \"a separate copy\", it means the check could not run.", "compat.modules.realpathSides": "Host side: {host}; plugin side: {plugin}. Fix the path (permissions / path length / reinstall) and reopen this page to re-check.", "compat.modules.realpathNone": "none", "compat.modules.unverified": "{count} package(s) could not be compared (not resolvable from the host anchor: {names}) — that is not \"a separate copy\", it means the check could not run. Try node scripts/doctor.mjs.",
|
|
3897
3941
|
"compat.modules.asarHost": "The host is the desktop app: the official packages live inside {name} while the plugin sits outside the archive, so the two can never be the same file. The {count} package(s) below were checked by version: {version} on both sides.",
|
|
3898
3942
|
"compat.modules.asarHostPackages": "{count} package(s) at the same version ({version}), differing only in packaging: {names}. Do not run host-deps.mjs --fix on this machine — it would point the plugin's dependencies at an older host in the npx cache instead of the running one inside the archive.",
|
|
3899
3943
|
"compat.blockers": "Blockers (fix these)",
|
|
@@ -4093,6 +4137,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4093
4137
|
"compat.reach.tools.2": "Scene / memory / MCP / skills / subagents / prompt — filled in by this plugin according to the Injection block above.",
|
|
4094
4138
|
"compat.reach.tools.3": "The prompt and the skill catalog also come from the official rows; this plugin only fills in what they miss (e.g. under Minimal).",
|
|
4095
4139
|
"compat.hint.doctor": "Command-line check:",
|
|
4140
|
+
"compat.hint.doctorRelease": "This plugin was installed from npm, which does not ship the developer scripts; to run a command-line check, use node scripts/doctor.mjs inside a source checkout.",
|
|
4096
4141
|
"scenes.title": "Scenes", "scenes.desc": "Manage scenes: preset combinations of MCP, skills, subagents and memories, switch with one click.",
|
|
4097
4142
|
"scenes.stat.total": "scene(s)", "scenes.stat.active": "enabled", "scenes.stat.archives": "with a profile",
|
|
4098
4143
|
"scenes.noDesc": "No description yet",
|
package/lib/compat/probe.js
CHANGED
|
@@ -42,6 +42,18 @@ export const IDENTITY_PACKAGES = [
|
|
|
42
42
|
'@deepseek-ai/dsh-storage-domain',
|
|
43
43
|
'@deepseek-ai/dsh-spill-local',
|
|
44
44
|
];
|
|
45
|
+
/**
|
|
46
|
+
* 本插件是不是从**源码检出**跑起来的(仓库里那份,`scripts/host-deps.mjs` 就躺在旁边)。
|
|
47
|
+
*
|
|
48
|
+
* 用来决定「两份拷贝」那条阻塞项给什么修复指引:`scripts/` 不在 `package.json` 的
|
|
49
|
+
* `files` 里(发布形态本就不该带开发者脚本),所以 npm 装出来的用户照那条指引去跑
|
|
50
|
+
* 只会拿到 `MODULE_NOT_FOUND` —— 这是 issue #1 的次要问题之一。指引必须按实际形态给。
|
|
51
|
+
*/
|
|
52
|
+
const SOURCE_INSTALL = existsSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'host-deps.mjs'));
|
|
53
|
+
/** 供界面决定修复指引:`true` = 源码检出(`scripts/` 就在旁边),`false` = npm 安装形态。 */
|
|
54
|
+
export function isSourceInstall() {
|
|
55
|
+
return SOURCE_INSTALL;
|
|
56
|
+
}
|
|
45
57
|
/**
|
|
46
58
|
* peer range 与已验证版本。官方全程走 prerelease 渠道(x.y.z-rc.N),semver 的
|
|
47
59
|
* 预发布规则(prerelease 版本只匹配同 [major,minor,patch] 元组的比较器)意味着**每一代
|
|
@@ -110,15 +122,21 @@ function isAsarPath(value) {
|
|
|
110
122
|
* realpath to one file ARE the same module — which is exactly the property
|
|
111
123
|
* this plugin depends on, and the reason `@deepseek-ai/*` is junctioned into
|
|
112
124
|
* the host installation instead of being copied.
|
|
125
|
+
*
|
|
126
|
+
* 取不到真实形态时**不再拿输入去比**(issue #1 要求):那会把「路径不可解析」
|
|
127
|
+
* 伪装成「两份不同拷贝」,给出一个编出来的结论。改由调用方把原因报成说明。
|
|
113
128
|
*/
|
|
114
|
-
function
|
|
129
|
+
function realPathWithReason(value) {
|
|
115
130
|
if (value === null)
|
|
116
|
-
return
|
|
131
|
+
return { reason: '路径为空' };
|
|
117
132
|
try {
|
|
118
|
-
return realpathSync.native !== undefined ? realpathSync.native(value) : realpathSync(value);
|
|
133
|
+
return { path: realpathSync.native !== undefined ? realpathSync.native(value) : realpathSync(value) };
|
|
119
134
|
}
|
|
120
|
-
catch {
|
|
121
|
-
|
|
135
|
+
catch (error) {
|
|
136
|
+
const detail = error;
|
|
137
|
+
const code = typeof detail.code === 'string' ? detail.code : '';
|
|
138
|
+
const message = typeof detail.message === 'string' ? detail.message : String(error);
|
|
139
|
+
return { reason: code === '' ? message : `${code} ${message}` };
|
|
122
140
|
}
|
|
123
141
|
}
|
|
124
142
|
/** Read a package version from a resolvable manifest. */
|
|
@@ -835,6 +853,10 @@ export function assessHost(ctx) {
|
|
|
835
853
|
const hostIsAsar = isAsarPath(hostRoot);
|
|
836
854
|
// 桌面版(宿主在 app.asar 里)两组「版本」的容器:插件自己解析到的、宿主锚点解析到的。
|
|
837
855
|
const asarHostPackages = [];
|
|
856
|
+
// realpath 失败(路径取不到真实形态):两侧各算一次就够,与是哪个包无关。
|
|
857
|
+
const realpathFailures = [];
|
|
858
|
+
let hostRealpathNote = null;
|
|
859
|
+
let pluginRealpathNote = null;
|
|
838
860
|
let asarHostVersion = null;
|
|
839
861
|
let asarPluginVersion = null;
|
|
840
862
|
for (const name of IDENTITY_PACKAGES) {
|
|
@@ -852,15 +874,36 @@ export function assessHost(ctx) {
|
|
|
852
874
|
}
|
|
853
875
|
// Resolve the same name from the host installation's own anchor, then
|
|
854
876
|
// compare PHYSICAL files: a junction is the same module, not a copy.
|
|
877
|
+
//
|
|
878
|
+
// 逐包解析、逐包取真实形态。realpath 的**机制**确实整轮不变,但上一版曾把
|
|
879
|
+
// 这句话错写成连**解析结果**一起复用:第一个包(cordis)的宿主侧入口路径被当成
|
|
880
|
+
// 其余五个包的宿主侧路径,于是五条全部比成「与 cordis 不同」→ 假「两份不同拷贝」
|
|
881
|
+
// (官方桌面版 0.2.0-rc.2 实测,2026-09-30;解析本就便宜,节点自身还有缓存)。
|
|
855
882
|
let hostResolved = null;
|
|
883
|
+
let hostRealpath = null;
|
|
856
884
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
885
|
+
hostResolved = createRequire(join(dirname(hostRoot), 'package.json')).resolve(name);
|
|
886
|
+
hostRealpath = realPathWithReason(hostResolved);
|
|
859
887
|
}
|
|
860
888
|
catch {
|
|
861
889
|
hostResolved = null;
|
|
862
890
|
}
|
|
863
|
-
|
|
891
|
+
// 真实形态取不到 → 这一格是「比不了」,不是「两份拷贝」。理由如实上报,
|
|
892
|
+
// 绝不把路径问题说成模块问题(issue #1 的次要问题之二)。
|
|
893
|
+
if (hostRealpath !== null && 'reason' in hostRealpath) {
|
|
894
|
+
sameAsHost[name] = null;
|
|
895
|
+
hostRealpathNote = hostRealpathNote ?? hostRealpath.reason;
|
|
896
|
+
realpathFailures.push({ name, side: 'host', path: hostResolved ?? '(未解析到)', reason: hostRealpath.reason });
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const pluginRealpath = realPathWithReason(resolved);
|
|
900
|
+
if ('reason' in pluginRealpath) {
|
|
901
|
+
sameAsHost[name] = null;
|
|
902
|
+
pluginRealpathNote = pluginRealpathNote ?? pluginRealpath.reason;
|
|
903
|
+
realpathFailures.push({ name, side: 'plugin', path: resolved, reason: pluginRealpath.reason });
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const same = hostResolved === null ? null : hostRealpath.path === pluginRealpath.path;
|
|
864
907
|
// 归档宿主:路径必然不同(归档 vs 磁盘),能比的是版本。相同 → 这一格是
|
|
865
908
|
// 「比不了(同版本、打包方式不同)」,记进说明并从 unverified 里摘出来(那句
|
|
866
909
|
// 「宿主的解析锚点里找不到它」对它不成立 —— 锚点找得到,在归档里);不同 →
|
|
@@ -877,13 +920,27 @@ export function assessHost(ctx) {
|
|
|
877
920
|
}
|
|
878
921
|
}
|
|
879
922
|
sameAsHost[name] = same;
|
|
880
|
-
if (same === false)
|
|
881
|
-
|
|
923
|
+
if (same === false) {
|
|
924
|
+
// 指引按**实际形态**给:`scripts/host-deps.mjs` 只存在于源码检出里
|
|
925
|
+
// (`files` 不含 `scripts/`,npm 安装形态下没有这个文件),
|
|
926
|
+
// 发布形态照那条指引去跑必然 `MODULE_NOT_FOUND` —— issue #1 的次要问题之一。
|
|
927
|
+
blockers.push(SOURCE_INSTALL
|
|
928
|
+
? `${name}:插件与宿主加载的是两份不同拷贝(在插件源码目录运行 node scripts/host-deps.mjs --fix)`
|
|
929
|
+
: `${name}:插件与宿主加载的是两份不同拷贝(升级或重装本插件后仍然如此再报)`);
|
|
930
|
+
}
|
|
882
931
|
// 解析得到、却比不了:宿主锚点里找不到它。不能静默 —— 否则整块身份校验等于没做,
|
|
883
932
|
// 而页头仍报「全部可用」(pnpm 的 .pnpm 隔离目录就是这种情形,见 hostPackageRoot)。
|
|
884
933
|
if (same === null)
|
|
885
934
|
unverified.push(name);
|
|
886
935
|
}
|
|
936
|
+
if (realpathFailures.length > 0) {
|
|
937
|
+
notes.push({
|
|
938
|
+
kind: 'realpath',
|
|
939
|
+
failures: realpathFailures,
|
|
940
|
+
...(hostRealpathNote === null ? {} : { hostReason: hostRealpathNote }),
|
|
941
|
+
...(pluginRealpathNote === null ? {} : { pluginReason: pluginRealpathNote }),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
887
944
|
if (asarHostPackages.length > 0 && hostRoot !== null) {
|
|
888
945
|
notes.push({
|
|
889
946
|
kind: 'asar-host',
|
|
@@ -949,9 +1006,12 @@ function hostPackageRoot() {
|
|
|
949
1006
|
if (asarHost !== null)
|
|
950
1007
|
candidates.push(asarHost);
|
|
951
1008
|
for (const anchor of IDENTITY_PACKAGES) {
|
|
952
|
-
|
|
953
|
-
|
|
1009
|
+
// 锚点候选只认**真实形态**:取不到就跳过(这里没有可比的对象,如实跳过即可,
|
|
1010
|
+
// 与身份比对不同 —— 那边取不到必须报出来,见 assessHost 的 realpath 说明)。
|
|
1011
|
+
const real = realPathWithReason(safeResolve(anchor));
|
|
1012
|
+
if ('reason' in real)
|
|
954
1013
|
continue;
|
|
1014
|
+
const resolved = real.path;
|
|
955
1015
|
let dir = dirname(resolved);
|
|
956
1016
|
for (let i = 0; i < 4; i += 1) {
|
|
957
1017
|
if (dir.endsWith(join('node_modules', '@deepseek-ai'))) {
|
package/lib/mcp/manager.js
CHANGED
|
@@ -1655,7 +1655,7 @@ export function createMcpManager(deps) {
|
|
|
1655
1655
|
return { ok: true, removed: total, ids: removed, files };
|
|
1656
1656
|
});
|
|
1657
1657
|
}
|
|
1658
|
-
// ---------- 配置体检(0.15.0 B1;0.16.
|
|
1658
|
+
// ---------- 配置体检(0.15.0 B1;0.16.5 起 streamable-http 含端点探测)--------------
|
|
1659
1659
|
// 对补丁文件**只读**:不写盘、不拉起子进程。对外调用两处 ——
|
|
1660
1660
|
// · `where` / `which` 查 PATH:只查「这个命令名能不能找到」,绝不执行配置里的命令本身;
|
|
1661
1661
|
// · streamable-http 行对配置地址发一次 GET 连通性探测(带配置的鉴权头、5 秒超时、
|
|
@@ -1777,7 +1777,7 @@ export function createMcpManager(deps) {
|
|
|
1777
1777
|
else if (!/^https?:\/\//.test(url))
|
|
1778
1778
|
checks.push({ id: 'badUrl', level: 'warn', params: { url } });
|
|
1779
1779
|
else {
|
|
1780
|
-
// 格式对了再做一次真实探测(0.16.
|
|
1780
|
+
// 格式对了再做一次真实探测(0.16.5):此前的体检止步于格式校验,一条编造的
|
|
1781
1781
|
// 地址也能拿绿点 —— 绿点承诺的「没发现问题」其实只覆盖了一半(2026-09-29 实测)。
|
|
1782
1782
|
const probe = await probeHttpEndpoint(url, row.headers);
|
|
1783
1783
|
if (!probe.reachable)
|
package/lib/ops/compat.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// 边界纪律:本文件只负责「op 的实现」,不负责 ops 表之外的接线(MCP 写后刷新、场景锁定
|
|
8
8
|
// 守卫仍作用在组装后的整张表上,见 index.ts)。
|
|
9
|
-
import { EXPECTED_MIN_HOST_VERSION, EXPECTED_PEER_RANGE, VERIFIED_HOST_VERSION, routeFor, summarize, } from '../compat/probe.js';
|
|
9
|
+
import { EXPECTED_MIN_HOST_VERSION, EXPECTED_PEER_RANGE, VERIFIED_HOST_VERSION, isSourceInstall, routeFor, summarize, } from '../compat/probe.js';
|
|
10
10
|
import { INJECT_DOMAIN_KEYS } from '../context-inject.js';
|
|
11
11
|
import { assessPresetReach } from '../compat/preset-reach.js';
|
|
12
12
|
import { runtimeNotes } from '../compat/runtime-notes.js';
|
|
@@ -64,9 +64,13 @@ export function buildCompatOps(deps) {
|
|
|
64
64
|
host: { version: assessment.identity.version, modules: assessment.identity.modules },
|
|
65
65
|
sameAsHost: assessment.identity.sameAsHost,
|
|
66
66
|
unverified: assessment.identity.unverified,
|
|
67
|
-
//
|
|
68
|
-
//
|
|
67
|
+
// 身份校验的说明:不是阻塞项,界面据此解释那一排「无法比较」,而不是留给
|
|
68
|
+
// 用户一句「两份拷贝,去 --fix」。两种来源 —— 宿主在 app.asar 里(桌面版)、
|
|
69
|
+
// 路径取不到真实形态(realpath 失败,issue #1)。
|
|
69
70
|
notes: assessment.identity.notes,
|
|
71
|
+
// 修复指引按实际形态给:`scripts/` 不在 `files` 里,npm 安装形态下没有
|
|
72
|
+
// `scripts/doctor.mjs` 与 `scripts/host-deps.mjs`,指了也跑不了。
|
|
73
|
+
sourceInstall: isSourceInstall(),
|
|
70
74
|
findings,
|
|
71
75
|
degraded,
|
|
72
76
|
blockers: assessment.identity.blockers,
|
package/lib/skills/core.js
CHANGED
|
@@ -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 === "
|
|
2898
|
-
? "
|
|
2899
|
-
:
|
|
2900
|
-
? "
|
|
2901
|
-
:
|
|
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"
|
package/lib/skills/service.js
CHANGED
|
@@ -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
|
|
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.16.
|
|
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",
|