- 零第三方依赖,Node >=20 原生 ESM - 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM - 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报 - H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json - H5 零依赖三栏控制台
140 lines
7.0 KiB
Markdown
140 lines
7.0 KiB
Markdown
# PLUGIN-MOUNT — DSH 桌面端插件实测挂载步骤
|
||
|
||
> 本机(Windows + DSH Desktop 3.x + pnpm)实测通过:插件行已进入桌面 profile 合成配置,
|
||
> 且 `import("scheduler-gateway-merged")` 可从 profile 解析出 4 个 gateway_* 工具。
|
||
|
||
## 为什么不能直接 `dsh plugin add`
|
||
|
||
`dsh plugin --profile desktop add link:<dir>` 会把参数转发给 profile 目录里的 pnpm。
|
||
本机 Desktop 自带的 pnpm 是 **11.22.0**(要求 store v11),而 desktop profile 的
|
||
node_modules 由 **pnpm 10.27.0**(store v10)安装 —— pnpm 11 直接报
|
||
`ERR_PNPM_UNEXPECTED_STORE` 拒绝操作,reconcile 步骤也不会执行。
|
||
|
||
## 实测步骤(管理员/普通用户均可)
|
||
|
||
```powershell
|
||
# 1) 用与现有 store 匹配的 pnpm 安装 link 依赖(PATH 上 pnpm 10.x)
|
||
cd C:Userslxy.dshprofilesdesktop
|
||
pnpm add link:C:Userslxy互联网关scheduler-gateway-merged
|
||
|
||
# 2) 把它加入 bundle 层(等价于 dsh plugin 成功后的 reconcile 写回)
|
||
# 在 profiles/desktop/package.json 的 dsh.profile.bundles 追加:
|
||
# "scheduler-gateway-merged"
|
||
|
||
# 3) 依赖解析(关键):插件源目录在 profile 之外,Node 按真实路径解析
|
||
# 依赖时找不到 @deepseek-ai/dsh-tools,需建立 junction:
|
||
mkdir "C:Userslxy互联网关scheduler-gateway-merged
|
||
ode_modules@deepseek-ai"
|
||
mklink /J "C:Userslxy互联网关scheduler-gateway-merged
|
||
ode_modules@deepseek-aidsh-tools" "C:Userslxy.dshprofilesdesktop
|
||
ode_modules@deepseek-aidsh-tools"
|
||
|
||
# 4) 重启 DSH Desktop(宿主进程启动时加载插件,工具表出现 gateway_* 四个工具)
|
||
```
|
||
|
||
## 验证(无需重启即可离线验证)
|
||
|
||
```powershell
|
||
# 合成配置里出现插件行:
|
||
node "D:UserslxyAppDataLocalProgramsDeepSeek Harness Desktop
|
||
esourcesapp.asar.unpacked
|
||
ode_modules@deepseek-aidshlibin.js" --profile desktop --dump-config | findstr /C:"scheduler-gateway-merged"
|
||
|
||
# 插件入口可从 profile 解析并注册工具:
|
||
cd C:Userslxy.dshprofilesdesktop
|
||
node -e "import('scheduler-gateway-merged').then(m=>console.log(m.tools.map(t=>t.name)))"
|
||
# → [ gateway_status, gateway_run, gateway_board, gateway_nodes ]
|
||
```
|
||
|
||
## 备注
|
||
|
||
- node_modules/ 已加入 .gitignore,junction 不进入交付清单
|
||
- 换机器时:把步骤 1 的 link 指向新路径,重复 2/3;或先运行一次 `dsh plugin add`(若 store 匹配会全自动)
|
||
- 插件工具默认连 http://127.0.0.1:4180(GATEWAY_URL 可覆盖);工具 `gateway_run` 的 serve 动作会自动拉起后台网关
|
||
|
||
## ⚠️ 重要:cordis 插件导出形态(踩坑记录)
|
||
|
||
DSH 宿主(cordis-plugin-loader)要求插件入口 **default 导出(unwrapExports 取 default)**
|
||
必须是「函数」或「带 apply 方法的对象」,否则启动直接崩:
|
||
|
||
```
|
||
Error: failed to apply loader entry scheduler-gateway-merged:
|
||
invalid plugin, expect function or object with an "apply" method, received object
|
||
```
|
||
|
||
正确形态(参考 @linxin666/dsh-ssh):
|
||
|
||
```js
|
||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||
const name = "scheduler-gateway-merged";
|
||
const inject = ["tools"]; // 需要的宿主服务
|
||
function apply(ctx) {
|
||
ctx.effect(() => {
|
||
const disposers = tools.map((t) => ctx.tools.register(t));
|
||
return () => disposers.forEach((d) => d());
|
||
}, "scheduler-gateway-merged: tools");
|
||
}
|
||
export default { name, inject, apply }; // 不能导出工具数组当 default!
|
||
```
|
||
|
||
- 工具在 `apply(ctx)` 里经 `ctx.tools.register` 注册;模块顶层只允许 `defineTool` 构建描述对象
|
||
- 若 default 形态错误,宿主会在启动时崩溃;其 plugin-recovery 会把「可疑插件」从 profile 里移除(dep/bundles/links 全回滚),应用恢复可启动但插件消失——重新注册前必须先修好代码
|
||
- 修复验证:`node bin.js --profile desktop --dump-config` 出现插件行 + `import("scheduler-gateway-merged")` 后 `typeof mod.default.apply === "function"` + 全量引导 `--port 0` 无报错
|
||
|
||
## ⚠️ 适配声明(插件管理器的「未声明适配」)
|
||
|
||
桌面端插件管理器按 package.json 的 `dsh.compatibility` 判定插件是否适配当前 Desktop:
|
||
不声明则显示「未声明适配 / 社区」,插件可能不被加载。必须照已验证插件(@linxin666/dsh-ssh)声明:
|
||
|
||
```json
|
||
"dsh": {
|
||
"bundle": { "patch": "./cordis.patch.yml" },
|
||
"client": "./lib/client.js",
|
||
"compatibility": {
|
||
"desktop": { "range": ">=2.7.0 <4.0.0", "api": "^1.2.0" },
|
||
"runtime": { "range": ">=0.1.1-rc.1 <0.2.0" },
|
||
"surfaces": ["main"]
|
||
}
|
||
}
|
||
```
|
||
|
||
Desktop 3.0.1 在 `>=2.7.0 <4.0.0` 范围内;lock 文件里 compatibility.status 会从 unknown 变为 compatible。
|
||
## ⚠️ 最致命坑:dsh.client 形态(桌面端「invalid plugin, received object」)
|
||
|
||
桌面端会把 `dsh.client` 指向的模块**当成 cordis 插件**加载并要求其有 `apply`。
|
||
若写成**字符串**路径且目标文件不是 apply 插件,桌面端启动即崩:
|
||
|
||
```
|
||
failed to apply loader entry scheduler-gateway-merged: invalid plugin, expect function or object with an "apply" method, received object
|
||
```
|
||
|
||
- **正确(如 @linxin666/dsh-ssh)**:`dsh.client` 为对象 `{ inject:[...client runtime 依赖], platform:"web" }`,且 `lib/client.js` default 导出**带 `apply` 的客户端插件**。
|
||
- **纯工具型插件(本网关)推荐**:**直接不声明 `dsh.client`**(host-only),只保留 `dsh.bundle.patch` + `dsh.compatibility`。工具由 host 半 `lib/index.js` 注册。
|
||
- **为什么 headless CLI 测不出**:`dsh --profile desktop` 只装配 host 插件树,**不加载 web-client 半**;只有桌面 App 会加载 client 半并对它做 cordis 校验。所以「CLI 引导通过 ≠ 桌面端能启动」。
|
||
- 排查方法:`import("scheduler-gateway-merged")` 后看 `default` 的 `apply` 是否为 function;再确认 `dsh.client` 是否声明了非 apply 模块。
|
||
## ⚠️ 致命坑:工具 result content 必须是内容块数组(不是字符串)
|
||
|
||
DSH 工具的 `output.render` 必须返回**内容块数组** `[{ type: "text", text: ... }]`,
|
||
若返回纯字符串,会写入会话记录为 `tool-result.content = "..."`(字符串)。
|
||
而 `@deepseek-ai/dsh-session` 的校验器要求 `Array.isArray(content)`,导致:
|
||
|
||
- **历史加载失败**:`SessionPersistenceCorruptionError: session event at seq N message must contain one tool-result block`
|
||
- **运行时崩溃**:`content.some is not a function`(LLM 运行时把字符串当数组迭代)
|
||
- 且会话文件一旦写入坏事件,整份历史校验失败 → 需要在会话文件里把坏事件的字符串 content 包成数组、或用 zstd 修复。
|
||
|
||
**正确写法**(对照 @linxin666/dsh-ssh):
|
||
```js
|
||
function textOut(v) {
|
||
return {
|
||
schema: { type: "object", additionalProperties: true, properties: { text: { type: "string" } } },
|
||
render: (_a, v) => [{ type: "text", text: v.text }], // ← 必须返回数组
|
||
};
|
||
}
|
||
```
|
||
|
||
`nativeExecute`/`makeAdapterExecute` 等执行器返回的 `output` 若是字符串,
|
||
经网关 bridge 写回工具结果时也应保证最终 content 是数组。
|
||
|
||
|
||
|
||
|