dsh-plugin-tool-management 0.16.0 → 0.16.6
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 +42 -0
- package/docs/update.md +21 -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 +277 -10
- package/lib/skills/service.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,48 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [0.16.6] - 2026-09-30
|
|
14
|
+
|
|
15
|
+
官方内置技能管理补全 web 侧:web 宿主照注的沙盒诊断技能接入技能页(可见、可停),Office 三件套按裁定在 web 上一并暴露;并补齐接管后缺失的 LibreOffice Kit 路径段。
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- **web 宿主注入的沙盒诊断技能现在看得见、关得掉了**(0.16.5 的误判):0.16.5 按「web 不注入官方技能」把 web 整个挡在官方内置管理之外,但官方宿主的沙盒技能门控是 `process.platform === "win32"`(`dsh-sandbox-local` 源码核对)——只看操作系统,web 照样向每个会话注入 diagnose-windows-sandbox-acl,于是 web 上「页面不显示、会话照注入」。现在 web 也物化官方技能并以 rank 599 接管:来源出现在技能页,停用即不注入。
|
|
20
|
+
- **接管后缺「Installed LibreOffice Kit」段**(0.16.5 桌面上已存在):官方 provider 在返回 office 技能正文时追加一段 LibreOffice Kit 的 node/cli 绝对路径,SKILL.md 明文要求使用这段路径、且禁止自行搜索;以 rank 599 接管后官方 provider 不再被问到,这段就丢了 —— office 技能里的 LibreOffice 渲染/转换命令实际没有可用路径。现在由插件按官方 `officeRuntime()` 契约逐字复刻补上:桌面 node 取宿主自带的独立 node(`<resources>/runtime/primary-runtime/dependencies/node/bin/`,官方明令 packaged 应用必须独立 node,electron 本体不行;`DSH_PRIMARY_RUNTIME` / `DSH_BUNDLED_PRIMARY_RUNTIME` 环境变量优先,SDK 部署照官方取值),cli 取 `app.asar.unpacked` 里的真实文件(独立 node 读不了 asar 内部);web node 即宿主 node 进程(官方默认 `process.execPath`),cli 走宿主进程解析链。任一路径不是真实文件时改用官方口径的「LibreOffice Kit is disabled in this deployment」降级文案 —— 绝不给跑不起来的路径。
|
|
21
|
+
|
|
22
|
+
### 变更
|
|
23
|
+
|
|
24
|
+
- **官方内置技能按宿主形态各物化一份**:物化目录从各宿主共用 `$DSH_HOME/tool-management/official-skills/` 改为 `official-skills/<desktop|web>/` 各写各的 —— 0.16.5 靠「web 完全不参与」压住竞态(web 重建会把桌面物化好的 4 个清成它解析到的 1 个,实测),分形态目录让 web 也能参与管理而不互踩;0.16.5 共享布局的顶层残留按已知名单自动清理。启停状态按技能名记在来源 `official` 下,两种形态共用一份。
|
|
25
|
+
- **web 技能页也暴露 Office 三件套**(用户裁定):web 宿主不装配 office 插件、不注入,但 npm 包自带完整 assets —— web 上从宿主进程解析链物化后照常列出、可启停(启用后随技能目录注入会话)。
|
|
26
|
+
|
|
27
|
+
### 说明
|
|
28
|
+
|
|
29
|
+
- **实测**:官方源码逐处核对(沙盒技能门控 `dsh-sandbox-local` 只看 win32;office 插件仅桌面装配;`officeRuntime()` 契约与桌面装配的 node/cli 落点),本机桌面安装核实独立 node 与 unpacked cli 真实存在。合成夹具四模式全过:web(4 个技能物化,Kit 段 node=宿主进程、cli=解析链包内 cli)、web 无 libreoffice-kit(官方降级文案)、桌面(Kit 段 node=primary-runtime 独立 node、cli=app.asar.unpacked 真实文件)、桌面目录布局不同(office/沙盒全走解析链兜底物化,Kit 降级文案,正文不含指向不存在布局的路径)。
|
|
30
|
+
|
|
31
|
+
## [0.16.5] - 2026-09-30
|
|
32
|
+
|
|
33
|
+
issue #1 收尾与桌面版第二轮适配:兼容页身份比对改为逐包解析、逐包核对(桌面版不再误报「两份不同拷贝」),「全部可用」与「N 项不可用」不再同屏;桌面版注入会话的 4 个官方内置技能接入技能页管理。
|
|
34
|
+
|
|
35
|
+
### 修复
|
|
36
|
+
|
|
37
|
+
- **桌面版兼容页误报 5 条「两份不同拷贝」**:宿主侧的身份比对把「realpath 机制整轮不变」错写成了连**解析结果**一起复用 —— 第一个包(cordis)的宿主侧入口路径被当成其余五个包的宿主侧路径,五条全部比成「与 cordis 不同」→ 5 条假阻塞项,banner「能力 18/18 · 全部可用」与红卡「5 项不可用」同屏(桌面版 0.2.0-rc.2 实测全中)。现在逐包解析、逐包取真实形态 —— 解析本就便宜,Node 自身还有缓存;同日新旧对比实测:旧代码 1 真 / 5 假 + 5 条阻塞,修复后 6 真 / 0 阻塞。
|
|
38
|
+
- **安装形态下的修复指引不再是跑不了的命令**(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 查看」同理分两键,安装形态下不再指命令。
|
|
39
|
+
- **路径取不到真实形态时不再编一个「两份拷贝」的结论**(issue #1 次要问题 2):`realPathOf()` 以前 `catch { return value }`,把「这条路径不可解析」静默降级成「拿输入去比较」—— 于是路径/权限问题看起来就是「插件与宿主加载的是两份不同拷贝」,用户照着它去修只会白费功夫。现在取不到真实形态就**如实记原因**(`error.code` 与消息原文),这类包给 `sameAsHost = null`(比不了)并单独出一条说明,带包名、哪一侧(`host` / `plugin`)、解析到的路径与失败原因;界面显示「无法比较(路径取不到真实形态)」并列出两侧。
|
|
40
|
+
- **兼容页不再一边「全部可用」一边挂着红卡**:能力注脚与红卡高亮此前把「身份待处理」(两份拷贝这类 —— 不影响能力可用性)混进了能力降级计数。现在两处只看能力降级数;汇总行三分:全可用 / 能力降级 / 身份待处理(新增词典键 `compat.summary.identity`),各说各的。
|
|
41
|
+
|
|
42
|
+
### 新增
|
|
43
|
+
|
|
44
|
+
- **官方内置技能接入管理**:桌面版宿主向每个会话注入 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)不注入会话上下文,不列(用户裁定)。
|
|
45
|
+
|
|
46
|
+
### 变更
|
|
47
|
+
|
|
48
|
+
- **源码检出与 npm 安装形态的行为差异,只在指引上一处**:`/dsh-plugin-tool-management` 里有没有 `scripts/host-deps.mjs` 决定给哪句指引(本插件不从 `scripts/` 加载任何东西),其余判定完全一致。
|
|
49
|
+
|
|
50
|
+
### 说明
|
|
51
|
+
|
|
52
|
+
- **收尾范围**:issue #1 的主因(`import.meta.resolve` 的 URL 未解码,含空格路径必现;macOS 上开头斜杠被一起删掉)已在 0.16.0 随 `fileURLToPath` 修掉,本版带次要两条的收尾、身份比对的逐包核对与官方内置技能管理。
|
|
53
|
+
- **实测**:源码检出与安装形态两种位置的 `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/` 相对结构保留。
|
|
54
|
+
|
|
13
55
|
## [0.16.0] - 2026-09-29
|
|
14
56
|
|
|
15
57
|
### 修复
|
package/docs/update.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
| 版本 | 发布 | 主题 |
|
|
6
6
|
| ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| [0.16.6](#0166---2026-09-30) | 2026-09-30 | 官方内置技能补全 web 侧:web 照注的沙盒诊断技能可见可停 + Office 三件套 web 也暴露 + 补上接管后缺失的 LibreOffice Kit 路径段(0.16.5 桌面已缺) |
|
|
8
|
+
| [0.16.5](#0165---2026-09-30) | 2026-09-30 | 桌面版误报 5 条「两份拷贝」修掉(身份逐包核对)+ 「全部可用」与红卡不再同屏 + 官方内置技能接入管理(桌面版注入的 4 个可启停;web 不出现) |
|
|
7
9
|
| [0.16.0](#0160---2026-09-29) | 2026-09-29 | 桌面版宿主身份误判修好(归档宿主按版本核对,带空格路径的宿主版本不再显示 unknown)+ 兼容页三张指标卡各删一行、适配版本改标 0.2.0-rc.2 + 四个域的行状态统一成名字后面的胶囊(已启用 / 未启用,停用类改灰)+ MCP 体检点挪到名字前 + 体检补端点探测 + 删除同步清引用 + 轮询不空转 + 子智能体补上「已启用」 |
|
|
8
10
|
| [0.15.0](#0150---2026-09-28) | 2026-09-28 | 适配 DSH 0.1.7-rc.2:修好 MCP 列表与注入 + 预设边界与极简兜底注入 + 会话标题 + 设置令牌 + host-deps / doctor;新增 MCP 导入导出、配置体检、最近改动流水、场景绑工具表方案、整机迁移快照;功能总览英文补齐、兼容页收弹窗与界面打磨 |
|
|
9
11
|
| [0.14.0](#0140---2026-09-23) | 2026-09-23 | 场景与记忆分家(共 20 条工具)+ 模型能进入 / 退出场景 + 场景档案对模型可见、改档案会报出被挤掉的项 + 注入只写"现在是什么"(省 21.9%)+ 十五条工具出厂默认关(每轮实发 5 条 ≈1,090)+ 工具表可存方案一键换回 + 人设思考强度 + 改 MCP 配置不碰明文凭据 + 提示词「生效中」修好 |
|
|
@@ -24,6 +26,25 @@
|
|
|
24
26
|
|
|
25
27
|
---
|
|
26
28
|
|
|
29
|
+
## 0.16.6 - 2026-09-30
|
|
30
|
+
|
|
31
|
+
- **web 上注入的沙盒诊断技能看得见、关得掉了**:官方宿主的沙盒技能门控只看 Windows(web 照样向每个会话注入),0.16.5 却把 web 挡在官方内置管理之外——页面不显示、会话照注入。现在 web 也物化官方技能、以更高优先级接管,停用即不注入。
|
|
32
|
+
- **web 技能页也列出 Office 三件套**:web 宿主不装配 office 插件(不注入),但包里自带完整资源——按裁定照常暴露,可启停(启用后会进会话技能目录)。
|
|
33
|
+
- **补上接管后缺失的 LibreOffice Kit 路径段**:官方 provider 返回 office 技能正文时会追加一段 LibreOffice 的 node/cli 绝对路径(SKILL.md 明文要用、禁止自己找),接管后这段丢了(0.16.5 桌面已缺)——渲染/转换命令实际没有可用路径。现在由插件按官方契约补上:桌面用宿主自带的独立 node + asar 解包出来的真实 cli,web 用宿主 node 进程 + 包内 cli;路径取不到时给官方的「已停用」文案,绝不给跑不起来的路径。
|
|
34
|
+
- **官方内置技能按宿主形态各存一份**(`official-skills/desktop/` 与 `/web/`):互不覆盖,0.16.5 共享目录的竞态由此根治;旧布局残留自动清理,启停状态两种形态通用。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 0.16.5 - 2026-09-30
|
|
39
|
+
|
|
40
|
+
- **桌面版不再误报 5 条「两份不同拷贝」**:宿主侧身份比对把第一个包(cordis)的路径当成其余五个包的,五条全部比成「不同」。现在逐包解析、逐包核对。
|
|
41
|
+
- **「全部可用」与红卡不再同屏**:身份待处理(不影响能力可用性)不再混进能力降级计数,汇总行按 全可用 / 能力降级 / 身份待处理 分开说。
|
|
42
|
+
- **官方内置技能能管了**:桌面版注入每个会话的 4 个官方技能(Office 三件套 + Windows 沙盒诊断)以「官方内置」来源出现在技能页、紧跟 DSH 技能,单个可启停、脚本照常能跑;web 不注入,这个来源在 web 上不出现。
|
|
43
|
+
- **修复指引按装法给**(issue #1):那块「两份不同拷贝」以前一律让人去跑 `node scripts/host-deps.mjs --fix`,可 npm 装出来的插件里**没有** `scripts/` 这个目录(发布不带开发脚本),照着做只会 `MODULE_NOT_FOUND`。现在源码检出给原指引(并写明要在源码目录跑),npm 安装形态改说「升级或重装后仍然如此再报」;兼容页底部那句自查命令同样分两种形态。
|
|
44
|
+
- **路径问题不再冒充模块问题**(issue #1):以前路径取不到真实形态时会静默拿原字符串去比对,于是「读不到路径」被报成「插件与宿主加载的是两份不同拷贝」—— 结论是编的。现在如实说「无法比较(路径取不到真实形态)」,并列出是哪一侧、解析到哪条路径、失败原因原文。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
27
48
|
## 0.16.0 - 2026-09-29
|
|
28
49
|
|
|
29
50
|
- **桌面版不再被误判成「两份不同拷贝」**:桌面版把官方包封在 `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, statSync, } 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,267 @@ export function resolveDshHome() {
|
|
|
57
58
|
export function resolveAgentsHome() {
|
|
58
59
|
return process.env.DSH_AGENTS_HOME || join(homedir(), ".agents");
|
|
59
60
|
}
|
|
61
|
+
// ── 官方内置技能来源(随宿主注入会话上下文的内置技能)──────────────────────
|
|
62
|
+
//
|
|
63
|
+
// 宿主按形态注入不同的官方技能(官方源码逐一核对,2026-09-30):
|
|
64
|
+
// - 桌面版 4 个:office-docx / office-pptx / office-xlsx(dsh-skill-office 只在桌面
|
|
65
|
+
// 宿主装配,assetRoot 指到 `<resources>/runtime/office-skills`)+ diagnose-windows-
|
|
66
|
+
// sandbox-acl(dsh-sandbox-local 的门控是 `process.platform === "win32"`,rank 600)。
|
|
67
|
+
// - web 版:沙盒诊断技能由宿主注入(门控只看 win32 —— 用户 2026-09-30 截图实锤,
|
|
68
|
+
// 0.16.5 按「web 不注入」的误判把 web 挡在门外,导致页面不显示、宿主照注);office
|
|
69
|
+
// 插件 web 宿主不装配、不注入,但用户裁定(2026-09-30)技能页照样把这三个暴露出来
|
|
70
|
+
// 统一管理(npm 包自带完整 assets,接管后可启停)。
|
|
71
|
+
//
|
|
72
|
+
// 它们或住在官方 asar 里、或由官方 provider 复制进临时目录自管,插件直接扫描/接管有两个
|
|
73
|
+
// 坑:宿主子进程里对 asar 的 promises 读法与拦截器解析都出现过时灵时不灵(同机两次启动
|
|
74
|
+
// 一组 4/1、一组全 0,实测);沙盒技能的脚本要交给 PowerShell 子进程,asar 路径读不了。
|
|
75
|
+
//
|
|
76
|
+
// 因此这里把来源**物化**成插件自有的一份真实目录,**每种宿主形态各一份**
|
|
77
|
+
// (`$DSH_HOME/tool-management/official-skills/<desktop|web>/`):每次宿主启动从官方源头
|
|
78
|
+
// 整目录覆盖拷贝(与官方 provider 的「私有资源副本」同一做法),扫描、启停、overlay、
|
|
79
|
+
// 脚本执行全部落在真实文件上。599 压过宿主 provider 的 600,停用即不注入。分形态是竞态
|
|
80
|
+
// 教训:0.16.5 之前共用一份目录,web 启动把它解析到的沙盒技能盖进去,桌面物化好的
|
|
81
|
+
// 4 个只剩 1 个(实测);改成各写各的子目录,互不清对方的账。共享布局的残留(旧条目
|
|
82
|
+
// 直接躺在 official-skills/ 下)按已知名单清理。
|
|
83
|
+
//
|
|
84
|
+
// - 预设参考技能(dsh-agent-preset/skills)不注入会话上下文,不列入(用户裁定)。
|
|
85
|
+
//
|
|
86
|
+
// 单个来源、随 DSH 技能之后排列,来源层语义同默认来源(不可移除、无来源开关)。
|
|
87
|
+
const OFFICIAL_SOURCE_KEY = "official";
|
|
88
|
+
/** electron 的 resources 目录;非桌面宿主(web 等普通 node 进程)没有这个属性。 */
|
|
89
|
+
function resourcesDir() {
|
|
90
|
+
const resources = process.resourcesPath;
|
|
91
|
+
return typeof resources === "string" && resources !== "" ? resources : null;
|
|
92
|
+
}
|
|
93
|
+
/** 桌面版官方 asar 里的包内目录(同步 fs 对 asar 的支持经过实测可靠)。 */
|
|
94
|
+
function desktopPackageDir(packageName, subpath) {
|
|
95
|
+
const resources = resourcesDir();
|
|
96
|
+
if (resources === null)
|
|
97
|
+
return null;
|
|
98
|
+
const dir = join(resources, "app.asar", "dsh", "node_modules", ...packageName.split("/"), ...subpath.split("/"));
|
|
99
|
+
return existsSync(dir) ? dir : null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 从插件自身的解析链找官方包内目录(web 等非桌面宿主的途径;宿主进程里的裸名
|
|
103
|
+
* 解析由官方模块拦截器路由到宿主副本)。包不可解析(旧宿主 / 单测环境)返回 null。
|
|
104
|
+
*/
|
|
105
|
+
function resolvedPackageDir(packageName, subpath) {
|
|
106
|
+
try {
|
|
107
|
+
const require = createRequire(import.meta.url);
|
|
108
|
+
let manifest;
|
|
109
|
+
try {
|
|
110
|
+
manifest = require.resolve(`${packageName}/package.json`);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// exports 未暴露 ./package.json 的包:从入口文件向上找同名 manifest。
|
|
114
|
+
manifest = "";
|
|
115
|
+
let dir = dirname(require.resolve(packageName));
|
|
116
|
+
for (let i = 0; i < 6; i += 1) {
|
|
117
|
+
const candidate = join(dir, "package.json");
|
|
118
|
+
if (existsSync(candidate)) {
|
|
119
|
+
manifest = candidate;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const parent = dirname(dir);
|
|
123
|
+
if (parent === dir)
|
|
124
|
+
break;
|
|
125
|
+
dir = parent;
|
|
126
|
+
}
|
|
127
|
+
if (manifest === "")
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const dir = join(dirname(manifest), ...subpath.split("/"));
|
|
131
|
+
return existsSync(dir) ? dir : null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* office 技能源头。桌面版由宿主挂载:运行时真实目录优先(官方 assetRoot),其次 asar
|
|
139
|
+
* 包内 assets。web 宿主不装配 office 插件(不注入),但 npm 包自带完整 assets —— 用户
|
|
140
|
+
* 裁定(2026-09-30)技能页照样暴露这三个技能,走宿主进程解析链取包内一份。解析不到
|
|
141
|
+
* 一律 null —— 不把拿不到真实文件的技能凭空列出来。
|
|
142
|
+
*/
|
|
143
|
+
function officialOfficeSource() {
|
|
144
|
+
const resources = resourcesDir();
|
|
145
|
+
if (resources !== null) {
|
|
146
|
+
const runtimeDir = join(resources, "runtime", "office-skills");
|
|
147
|
+
if (existsSync(join(runtimeDir, "office-docx")))
|
|
148
|
+
return runtimeDir;
|
|
149
|
+
const asarDir = desktopPackageDir("@deepseek-ai/dsh-skill-office", "assets");
|
|
150
|
+
if (asarDir !== null && existsSync(join(asarDir, "office-docx")))
|
|
151
|
+
return asarDir;
|
|
152
|
+
}
|
|
153
|
+
return resolvedPackageDir("@deepseek-ai/dsh-skill-office", "assets");
|
|
154
|
+
}
|
|
155
|
+
/** 沙盒诊断技能源头(桌面 asar 直连优先,其次宿主进程解析链;宿主门控仅看 win32,web/桌面皆注入)。 */
|
|
156
|
+
function officialSandboxSource() {
|
|
157
|
+
if (process.platform !== "win32")
|
|
158
|
+
return null;
|
|
159
|
+
return (desktopPackageDir("@deepseek-ai/dsh-sandbox-windows-acl", "assets/diagnose-windows-sandbox-acl") ??
|
|
160
|
+
resolvedPackageDir("@deepseek-ai/dsh-sandbox-windows-acl", "assets/diagnose-windows-sandbox-acl"));
|
|
161
|
+
}
|
|
162
|
+
// ── office 技能的「Installed LibreOffice Kit」段 ────────────────────────────
|
|
163
|
+
//
|
|
164
|
+
// 官方 dsh-office provider 在 get 时给三个 office 技能的正文**追加**一段
|
|
165
|
+
// 「Installed LibreOffice Kit」:SKILL.md 明文要求「使用这段提供的 libreofficeKit.node
|
|
166
|
+
// 与 .cli 绝对路径,禁止自行搜索/猜测」。我们以 599 接管后官方 provider 不再被问到,
|
|
167
|
+
// 这段必须由我们补上 —— 否则模型拿到的 office 技能缺执行 LibreOffice 命令所需的全部
|
|
168
|
+
// 路径(这个缺口 0.16.5 在桌面上就已存在)。文案与 JSON 形状逐字取自官方
|
|
169
|
+
// dsh-skill-office lib/index.js 的 officeRuntime();node/cli 的落点按宿主形态各自核实:
|
|
170
|
+
// - 桌面:官方明确要求 packaged 应用必须给**独立 node**(electron 本体不行)。
|
|
171
|
+
// 实测桌面把运行时装在 `<resources>/runtime/primary-runtime/`(DSH_PRIMARY_RUNTIME
|
|
172
|
+
// 指向它,SDK 部署可被同名环境变量改写),node 在其 dependencies/node/bin 下;cli
|
|
173
|
+
// 必须用 `app.asar.unpacked` 里的真实文件 —— 独立 node 读不了 asar 内部。
|
|
174
|
+
// - web:官方默认 process.execPath(宿主是普通 node 进程),cli 从宿主进程解析链取。
|
|
175
|
+
// 任一路径不是真实文件 → 用官方口径的「已停用」文案(SKILL.md 自有降级分支,模型不会
|
|
176
|
+
// 去猜路径),绝不给一个跑不起来的路径。
|
|
177
|
+
const OFFICE_SKILL_NAMES = new Set([
|
|
178
|
+
"office-docx",
|
|
179
|
+
"office-pptx",
|
|
180
|
+
"office-xlsx",
|
|
181
|
+
]);
|
|
182
|
+
const OFFICE_RUNTIME_DISABLED = "\n\nLibreOffice Kit is disabled in this deployment.";
|
|
183
|
+
let officeRuntimeSectionCache;
|
|
184
|
+
/** 官方 provider 在 get 时追加的 LibreOffice Kit 段(每进程算一次,路径不随会话变)。 */
|
|
185
|
+
function officialOfficeRuntimeSection() {
|
|
186
|
+
if (officeRuntimeSectionCache === undefined)
|
|
187
|
+
officeRuntimeSectionCache = computeOfficeRuntimeSection();
|
|
188
|
+
return officeRuntimeSectionCache;
|
|
189
|
+
}
|
|
190
|
+
function computeOfficeRuntimeSection() {
|
|
191
|
+
const resources = resourcesDir();
|
|
192
|
+
let node;
|
|
193
|
+
let cli;
|
|
194
|
+
const runtimeRoot = process.env.DSH_PRIMARY_RUNTIME || process.env.DSH_BUNDLED_PRIMARY_RUNTIME;
|
|
195
|
+
if (runtimeRoot) {
|
|
196
|
+
// SDK / 容器部署:官方配置从运行时根推导(cordis 装配逐字核对)。
|
|
197
|
+
node = join(resolve(runtimeRoot), "dependencies", "node", "bin", process.platform === "win32" ? "node.exe" : "node");
|
|
198
|
+
cli = resolvedPackageDir("@deepseek-ai/libreoffice-kit", "lib/cli.js");
|
|
199
|
+
}
|
|
200
|
+
else if (resources !== null) {
|
|
201
|
+
node = join(resources, "runtime", "primary-runtime", "dependencies", "node", "bin", process.platform === "win32" ? "node.exe" : "node");
|
|
202
|
+
// 桌面的 cli 只认 unpacked 真实文件(独立 node 读不了 asar;electron 的 fs 补丁
|
|
203
|
+
// 会让 existsSync 对 asar 内部也返回真,所以这里不用解析链兜底)。
|
|
204
|
+
cli = join(resources, "app.asar.unpacked", "dsh", "node_modules", "@deepseek-ai", "libreoffice-kit", "lib", "cli.js");
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
node = process.execPath;
|
|
208
|
+
cli = resolvedPackageDir("@deepseek-ai/libreoffice-kit", "lib/cli.js");
|
|
209
|
+
}
|
|
210
|
+
const usable = (path) => {
|
|
211
|
+
try {
|
|
212
|
+
return isAbsolute(path) && statSync(path).isFile();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
if (!usable(node) || cli === null || !usable(cli))
|
|
219
|
+
return OFFICE_RUNTIME_DISABLED;
|
|
220
|
+
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)}`;
|
|
221
|
+
}
|
|
222
|
+
function copyDirectorySync(from, to) {
|
|
223
|
+
mkdirSync(to, { recursive: true });
|
|
224
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
225
|
+
const source = join(from, entry.name);
|
|
226
|
+
const target = join(to, entry.name);
|
|
227
|
+
if (entry.isDirectory())
|
|
228
|
+
copyDirectorySync(source, target);
|
|
229
|
+
else if (entry.isFile())
|
|
230
|
+
copyFileSync(source, target);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 物化官方内置技能到插件自有目录,返回该目录;无可用源头或拷贝失败返回 null。
|
|
235
|
+
* 每次宿主启动整目录重建(官方包只随宿主更新变化,宿主更新必然重启宿主)。
|
|
236
|
+
*/
|
|
237
|
+
let officialRootCache;
|
|
238
|
+
/** 宿主形态子目录名:桌面(有 process.resourcesPath)与 web 等普通 node 进程各物化一份。 */
|
|
239
|
+
function officialFormName() {
|
|
240
|
+
return resourcesDir() === null ? "web" : "desktop";
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 0.16.5 之前各形态共用一份目录(旧条目直接躺在 official-skills/ 下);分形态目录后
|
|
244
|
+
* 这些残留只会变成幽灵条目,按这份已知名单清掉。只清名单内的名字,绝不碰旁边的
|
|
245
|
+
* `<form>/` 子目录 —— 那是另一种形态物化的现役目录。
|
|
246
|
+
*/
|
|
247
|
+
const LEGACY_OFFICIAL_ENTRY_NAMES = Object.freeze([
|
|
248
|
+
"office-docx",
|
|
249
|
+
"office-pptx",
|
|
250
|
+
"office-xlsx",
|
|
251
|
+
"scripts",
|
|
252
|
+
"diagnose-windows-sandbox-acl",
|
|
253
|
+
]);
|
|
254
|
+
function cleanupLegacyOfficialLayout(root) {
|
|
255
|
+
for (const name of LEGACY_OFFICIAL_ENTRY_NAMES) {
|
|
256
|
+
try {
|
|
257
|
+
rmSync(join(root, name), { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* 残留清理失败不影响物化,下轮启动再试 */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function materializeOfficialSkills() {
|
|
265
|
+
const office = officialOfficeSource();
|
|
266
|
+
const sandbox = officialSandboxSource();
|
|
267
|
+
if (office === null && sandbox === null)
|
|
268
|
+
return null;
|
|
269
|
+
// 每种宿主形态只整目录重建**自己**这份(竞态教训见上方块注释);对方形态的子目录
|
|
270
|
+
// 与共享层残留清理互不干涉。
|
|
271
|
+
const root = join(resolveDshHome(), "tool-management", "official-skills", officialFormName());
|
|
272
|
+
try {
|
|
273
|
+
rmSync(root, { recursive: true, force: true });
|
|
274
|
+
// office:整个 assets 根拷进来(office-*/ 与 scripts/ 的相对结构必须保留 ——
|
|
275
|
+
// SKILL.md 用 `<skill-directory>/../scripts/check_office.py` 引用检查脚本)。
|
|
276
|
+
if (office !== null)
|
|
277
|
+
copyDirectorySync(office, root);
|
|
278
|
+
if (sandbox !== null) {
|
|
279
|
+
copyDirectorySync(sandbox, join(root, basename(sandbox)));
|
|
280
|
+
}
|
|
281
|
+
cleanupLegacyOfficialLayout(dirname(root));
|
|
282
|
+
return root;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
console.error("[dsh-plugin-tool-management] official skills materialization failed:", String((error && error.message) || error));
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function officialSkillRoot() {
|
|
290
|
+
if (officialRootCache !== undefined)
|
|
291
|
+
return officialRootCache;
|
|
292
|
+
const dir = materializeOfficialSkills();
|
|
293
|
+
officialRootCache =
|
|
294
|
+
dir === null
|
|
295
|
+
? null
|
|
296
|
+
: {
|
|
297
|
+
key: OFFICIAL_SOURCE_KEY,
|
|
298
|
+
path: dir,
|
|
299
|
+
label: "官方内置",
|
|
300
|
+
localeKey: "official",
|
|
301
|
+
mutable: false,
|
|
302
|
+
deletable: false,
|
|
303
|
+
// 来源层无开关(同默认来源),单个技能可启停;overlay 以 599 压过官方
|
|
304
|
+
// provider 的 600,脚本从物化副本的真实路径执行。
|
|
305
|
+
toggleable: true,
|
|
306
|
+
native: false,
|
|
307
|
+
scope: "user",
|
|
308
|
+
rank: 599,
|
|
309
|
+
};
|
|
310
|
+
return officialRootCache;
|
|
311
|
+
}
|
|
60
312
|
/**
|
|
61
313
|
* DSH 与外部 Agent 的用户级技能目录(4 个来源;外部来源由 manager provider 接入)。
|
|
62
314
|
*
|
|
63
315
|
* rank 与官方 filesystem provider 的用户级优先级衔接:DSH=400、Agents=500。
|
|
64
316
|
* manager provider 以 450 接管公共 Agents(仍低于 DSH),Codex/Claude 依次排在其后。
|
|
317
|
+
* DSH 之后紧随官方内置来源(随宿主注入会话的内置技能,见 materializeOfficialSkills;
|
|
318
|
+
* 无可用源头时自然缺席)。
|
|
65
319
|
*/
|
|
66
320
|
export function userRoots() {
|
|
321
|
+
const official = officialSkillRoot();
|
|
67
322
|
return [
|
|
68
323
|
{
|
|
69
324
|
key: "dsh",
|
|
@@ -78,6 +333,8 @@ export function userRoots() {
|
|
|
78
333
|
native: true,
|
|
79
334
|
rank: 400,
|
|
80
335
|
},
|
|
336
|
+
// 官方内置紧随 DSH 技能之后(用户裁定):同为一眼要看的来源,排在导入技能之前。
|
|
337
|
+
...(official === null ? [] : [official]),
|
|
81
338
|
{
|
|
82
339
|
// v0.4:插件**新建/导入**的技能落到 hub 内(用户要求:插件产生的文件收在
|
|
83
340
|
// $DSH_HOME/tool-management/)。rank 高于 DSH 技能,同名时 hub 版本遮蔽官方目录里的;
|
|
@@ -139,8 +396,8 @@ export function userRoots() {
|
|
|
139
396
|
*
|
|
140
397
|
* 一句话记法:**`mutable` 与 `deletable` 同向,`removable` 与 `mutable` 反向。**
|
|
141
398
|
*/
|
|
142
|
-
const DEFAULT_SOURCE_KEYS = Object.freeze(["dsh", "hub"]);
|
|
143
|
-
/** 是否为默认来源。接受 root 对象或 key 字符串;`dsh` / `hub` 的所有特判都走这里。 */
|
|
399
|
+
const DEFAULT_SOURCE_KEYS = Object.freeze(["dsh", "hub", OFFICIAL_SOURCE_KEY]);
|
|
400
|
+
/** 是否为默认来源。接受 root 对象或 key 字符串;`dsh` / `hub` / `official` 的所有特判都走这里。 */
|
|
144
401
|
export function isDefaultSkillSource(rootOrKey) {
|
|
145
402
|
const key = rootOrKey && typeof rootOrKey === "object"
|
|
146
403
|
? rootOrKey.key
|
|
@@ -582,7 +839,9 @@ function reservedSourceError(root) {
|
|
|
582
839
|
const key = root && root.key ? root.key : "";
|
|
583
840
|
const origin = key === "dsh"
|
|
584
841
|
? "DSH 技能目录是默认来源,必须读取"
|
|
585
|
-
: "
|
|
842
|
+
: key === "hub"
|
|
843
|
+
? "导入技能目录是默认来源、插件新建/导入的落点,必须读取"
|
|
844
|
+
: "官方内置技能目录随宿主发布,不能移除(移除只会让插件失去管理权)";
|
|
586
845
|
return {
|
|
587
846
|
ok: false,
|
|
588
847
|
code: "error.source.reserved",
|
|
@@ -2894,11 +3153,13 @@ export async function listProviderCandidates(options = {}) {
|
|
|
2894
3153
|
provider: "dsh-plugin-tool-management-external",
|
|
2895
3154
|
source: project
|
|
2896
3155
|
? root.kind
|
|
2897
|
-
: root.key === "
|
|
2898
|
-
? "
|
|
2899
|
-
:
|
|
2900
|
-
? "
|
|
2901
|
-
:
|
|
3156
|
+
: root.key === "official"
|
|
3157
|
+
? "bundled"
|
|
3158
|
+
: root.key === "dsh"
|
|
3159
|
+
? "user-dsh"
|
|
3160
|
+
: CUSTOM_ROOT_KEY_RE.test(root.key)
|
|
3161
|
+
? "custom"
|
|
3162
|
+
: `agent-${root.key}`,
|
|
2902
3163
|
rank: project
|
|
2903
3164
|
? root.rank - 1
|
|
2904
3165
|
: root.key === "dsh"
|
|
@@ -2965,6 +3226,12 @@ export async function getProviderSkill(candidate, options = {}) {
|
|
|
2965
3226
|
const summary = entryOf(locator.entryName, entry.kind, entry.docPath, doc);
|
|
2966
3227
|
if (!summary.loadable || summary.declaredName !== candidate.name)
|
|
2967
3228
|
return undefined;
|
|
3229
|
+
// 官方 office 技能:补上官方 provider 在 get 时追加的 LibreOffice Kit 段
|
|
3230
|
+
// (SKILL.md 明文引用这段的 node/cli 路径,见上方块注释)。
|
|
3231
|
+
const officeRuntime = locator.rootKey === OFFICIAL_SOURCE_KEY &&
|
|
3232
|
+
OFFICE_SKILL_NAMES.has(locator.entryName)
|
|
3233
|
+
? officialOfficeRuntimeSection()
|
|
3234
|
+
: "";
|
|
2968
3235
|
return {
|
|
2969
3236
|
name: candidate.name,
|
|
2970
3237
|
description: candidate.description,
|
|
@@ -2974,7 +3241,7 @@ export async function getProviderSkill(candidate, options = {}) {
|
|
|
2974
3241
|
resourceBase: candidate.resourceBase,
|
|
2975
3242
|
path: candidate.path,
|
|
2976
3243
|
metadata: candidate.metadata,
|
|
2977
|
-
content: doc.body.trim(),
|
|
3244
|
+
content: doc.body.trim() + officeRuntime,
|
|
2978
3245
|
};
|
|
2979
3246
|
}
|
|
2980
3247
|
catch {
|
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.6",
|
|
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",
|