# 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:` 会把参数转发给 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 是数组。