TL;DR — 装上 dsh-pocket 后 dsh web 直接启动失败。已定位根因不在 dsh-pocket,而在 @deepseek-ai/dsh-client-connection:它在 0.1.5-alpha.1 的重构中从顶层 inject 移除了 webServer,却漏改了一处仍在使用 owner.webServer 的调用点。所有通过 connection.rpc.handle() 注册通道的第三方插件都会中招。本 issue 附逐版本证据、最小复现,以及建议本项目做的一处防御性改动(可选)。
环境
| 项 |
值 |
| dsh-pocket |
2.10.3(当前 npm latest) |
| DSH |
0.1.5-rc.1(npx,latest) |
| @deepseek-ai/cordis |
4.0.2 |
| Node |
v24.18.0 |
| OS |
Windows 10.0.19045 |
| profile |
$DSH_HOME/profiles/web |
现象
dsh plugin --profile web add dsh-pocket@2.10.3 安装成功,随后 dsh web 启动即崩:
Error: dsh: plugin tree failed to load: failed to apply loader entry dsh-pocket (dsh-pocket): cannot get property "webServer" without inject
Error: cannot get property "webServer" without inject
at Fiber.<anonymous> (.../@deepseek-ai/dsh-client-connection/lib/index.js:618:35)
at Proxy.register (.../@deepseek-ai/dsh-client-connection/lib/index.js:618:16)
at Object.handle (.../@deepseek-ai/dsh-client-connection/lib/index.js:543:39)
at installPocketRpc (.../dsh-pocket/lib/web-rpc.js:39:29)
at new apply (.../dsh-pocket/lib/index.js:349:22)
at Fiber.execute (.../@deepseek-ai/cordis/lib/index.js:1067:24)
at boot (.../@deepseek-ai/dsh-app-boot/lib/index.js:1545:9)
dshmarket 的热挂载路径同样失败,只是降级为“需重启”而不是中断启动:
{"event":"hot-mount","detail":"dsh-pocket: fell back to restart — failed to apply loader entry mkt-dsh-pocket (dsh-pocket): cannot get property \"webServer\" without inject"}
根因
dsh-client-connection 的 register():
// dsh-client-connection/lib/index.js
543: handle: (channel, handler) => this.register(owner, channel, handler) // owner = this.ctx
618: return owner.effect(() => owner.webServer.register(route), `client-connection: ${channel} rpc channel`);
owner 是该服务自己的 ctx,而该包的顶层 inject 在 0.1.5-alpha.1 起不再声明 webServer。cordis 4.x 对未声明 inject 的服务做属性读取会抛错,异常沿 fiber 冒到 boot(),导致整棵插件树加载失败。
逐版本核对 lib/index.js:
dsh-client-connection |
顶层 inject |
owner.webServer 使用 |
子 ctx 注入 webServer |
| 0.1.2-rc.1 |
["webServer","credentials"] |
1 处 |
无 |
| 0.1.3-alpha.2 |
["webServer","credentials"] |
1 处 |
无 |
| 0.1.5-alpha.1 |
["credentials"] |
1 处 |
有 |
| 0.1.5-alpha.2 |
["credentials"] |
1 处 |
有 |
| 0.1.5-rc.1 |
["credentials"] |
1 处 |
有 |
即 0.1.5-alpha.1 把 /api 路由的注册包进了子 ctx:
ctx.inject(["webServer"], (webCtx) => { ... webCtx.effect(() => webCtx.webServer.register(route), ...) });
于是从顶层 inject 移除了 webServer,但 register()(第三方通道路径)仍在同一个 ctx 上读 owner.webServer —— 重构漏改了一个调用点。
已排除 cordis 作为变量:@deepseek-ai/dsh 的 0.1.2-rc.1、0.1.3-alpha.2、0.1.5-rc.1 三个版本都钉 @deepseek-ai/cordis: ^4.0.2;且 cordis 4.0.1 与 4.0.2 的 lib/index.js 字节数完全一致(60228),严格服务访问检查两边都存在。横跨这段窗口变化的是 DSH 自己的 inject 列表。
影响范围
不只 dsh-pocket —— 任何通过 connection.rpc.handle() 注册 HTTP 通道的第三方插件都受影响(同类报告见 dsh-ssh、dsh-mnemon)。DSH 自家插件走 ctx.inject(['webServer'], ...) 拿子 ctx,因此不受影响。
两种表现形态:
- 插件作为 boot 路径条目加载时 → 异常冒到顶层,整个 DSH 起不来(本 issue 的情形)
- 插件在
ctx.inject 回调里注册时 → 异常被 fiber 吞掉,无任何控制台报错,通道静默缺失,任意 POST /<自定义通道> 返回 405
已实测的本地绕过(供使用者参考)
在 profile 的 cordis.patch.yml 给 connection 条目补回注入即可,无需改动任何已安装包:
- id: connection
inject:
- webRuntime
- webServer
验证(隔离 DSH_HOME,profile 内装 dsh-pocket@2.10.3):
- 打补丁前:复现上述堆栈,进程退出
- 打补丁后:正常启动;
POST /dsh-pocket → 401(通道已挂载,被认证栅栏拦下),对照组 POST /dsh-nonexistent-abc → 405
- 且修复后 dshmarket 热挂载可直接成功,不再要求重启
注意:改 inject 会让 loader 走完整 dispose + re-init,所以建议停掉 DSH 再改,然后重启,不要在运行中改。
对本项目的建议(可选)
本项目自身代码没有问题,2.10.3 在 0.1.3-alpha.2 及以前运行正常。但当前这个异常会让宿主完全启动不了,对使用者来说是最重的一档故障,而它发生在插件的 apply() 同步路径上。
lib/web-rpc.js:35 已有同思路的降级分支:
if (!ctx?.connection?.rpc?.handle) {
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
return () => {};
}
建议把紧随其后的 handle() 调用也纳入同样的降级,让宿主兼容性问题表现为“设置页不可用”而非“DSH 起不来”:
try {
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, handler, { authority: 'loopback' });
} catch (err) {
log.warn?.('dsh-pocket: RPC channel registration failed, settings tab disabled | RPC 通道注册失败,设置页不可用:%s', err?.message ?? err);
return () => {};
}
这只是在既有降级设计上多包一层,不影响正常环境。是否采纳由作者判断。
附:另一处 DSH 侧回归(与本项目相关,供参考)
0.1.5-rc.1 的 handle 签名是 handle: (channel, handler) => ...(lib/index.js:543),第三个参数被静默丢弃:
// dsh-pocket/lib/web-rpc.js:222
}, { authority: 'loopback' });
也就是说本项目请求的 loopback-only 限制实际没有生效,通道拿到的是通用栅栏(trustedHosts + 浏览器认证)。对局域网/公网扫码访问这一主场景影响不大,但如果项目里任何设计假设了“管理类端点仅本机可达”(例如 POCKET_ENDPOINTS.fileRead、pocketReset),这个假设在当前 DSH 版本下不成立。0.1.2-rc.1 / 0.1.3-alpha.2 上该参数正常生效。
上游线索
英文摘要 / English summary
After installing dsh-pocket 2.10.3 on DSH 0.1.5-rc.1, dsh web aborts during plugin-tree boot with cannot get property "webServer" without inject. The root cause is not in this plugin: @deepseek-ai/dsh-client-connection dropped webServer from its top-level inject in 0.1.5-alpha.1 while register() (line 618) still reads owner.webServer — a refactor that missed one call site. Cordis is ruled out (all three DSH versions pin ^4.0.2; cordis 4.0.1 and 4.0.2 are byte-identical in size and both contain the strict check). Every third-party plugin registering a channel via connection.rpc.handle() is affected. A profile-level inject patch on the connection entry is verified to fix it; a defensive try/catch around the handle() call here is suggested so this class of host incompatibility degrades to "settings tab disabled" instead of "DSH will not start".
TL;DR — 装上 dsh-pocket 后
dsh web直接启动失败。已定位根因不在 dsh-pocket,而在@deepseek-ai/dsh-client-connection:它在0.1.5-alpha.1的重构中从顶层inject移除了webServer,却漏改了一处仍在使用owner.webServer的调用点。所有通过connection.rpc.handle()注册通道的第三方插件都会中招。本 issue 附逐版本证据、最小复现,以及建议本项目做的一处防御性改动(可选)。环境
latest)latest)$DSH_HOME/profiles/web现象
dsh plugin --profile web add dsh-pocket@2.10.3安装成功,随后dsh web启动即崩:dshmarket的热挂载路径同样失败,只是降级为“需重启”而不是中断启动:{"event":"hot-mount","detail":"dsh-pocket: fell back to restart — failed to apply loader entry mkt-dsh-pocket (dsh-pocket): cannot get property \"webServer\" without inject"}根因
dsh-client-connection的register():owner是该服务自己的 ctx,而该包的顶层inject在 0.1.5-alpha.1 起不再声明webServer。cordis 4.x 对未声明 inject 的服务做属性读取会抛错,异常沿 fiber 冒到boot(),导致整棵插件树加载失败。逐版本核对
lib/index.js:dsh-client-connectioninjectowner.webServer使用webServer["webServer","credentials"]["webServer","credentials"]["credentials"]["credentials"]["credentials"]即 0.1.5-alpha.1 把
/api路由的注册包进了子 ctx:于是从顶层
inject移除了webServer,但register()(第三方通道路径)仍在同一个 ctx 上读owner.webServer—— 重构漏改了一个调用点。已排除 cordis 作为变量:
@deepseek-ai/dsh的 0.1.2-rc.1、0.1.3-alpha.2、0.1.5-rc.1 三个版本都钉@deepseek-ai/cordis: ^4.0.2;且 cordis 4.0.1 与 4.0.2 的lib/index.js字节数完全一致(60228),严格服务访问检查两边都存在。横跨这段窗口变化的是 DSH 自己的 inject 列表。影响范围
不只 dsh-pocket —— 任何通过
connection.rpc.handle()注册 HTTP 通道的第三方插件都受影响(同类报告见 dsh-ssh、dsh-mnemon)。DSH 自家插件走ctx.inject(['webServer'], ...)拿子 ctx,因此不受影响。两种表现形态:
ctx.inject回调里注册时 → 异常被 fiber 吞掉,无任何控制台报错,通道静默缺失,任意POST /<自定义通道>返回 405已实测的本地绕过(供使用者参考)
在 profile 的
cordis.patch.yml给connection条目补回注入即可,无需改动任何已安装包:验证(隔离
DSH_HOME,profile 内装 dsh-pocket@2.10.3):POST /dsh-pocket→ 401(通道已挂载,被认证栅栏拦下),对照组POST /dsh-nonexistent-abc→ 405注意:改
inject会让 loader 走完整 dispose + re-init,所以建议停掉 DSH 再改,然后重启,不要在运行中改。对本项目的建议(可选)
本项目自身代码没有问题,2.10.3 在 0.1.3-alpha.2 及以前运行正常。但当前这个异常会让宿主完全启动不了,对使用者来说是最重的一档故障,而它发生在插件的
apply()同步路径上。lib/web-rpc.js:35已有同思路的降级分支:建议把紧随其后的
handle()调用也纳入同样的降级,让宿主兼容性问题表现为“设置页不可用”而非“DSH 起不来”:这只是在既有降级设计上多包一层,不影响正常环境。是否采纳由作者判断。
附:另一处 DSH 侧回归(与本项目相关,供参考)
0.1.5-rc.1 的
handle签名是handle: (channel, handler) => ...(lib/index.js:543),第三个参数被静默丢弃:也就是说本项目请求的 loopback-only 限制实际没有生效,通道拿到的是通用栅栏(
trustedHosts+ 浏览器认证)。对局域网/公网扫码访问这一主场景影响不大,但如果项目里任何设计假设了“管理类端点仅本机可达”(例如POCKET_ENDPOINTS.fileRead、pocketReset),这个假设在当前 DSH 版本下不成立。0.1.2-rc.1 / 0.1.3-alpha.2 上该参数正常生效。上游线索
[Bug] connection fails to start when a third-party plugin registers an HTTP channel: cannot get property 'webServer' without inject:[Bug] connection fails to start when a third-party plugin registers an HTTP channel: cannot get property 'webServer' without inject deepseek-ai/deepseek-harness#5926register内改用ctx.get('webServer', false)以避开 inject 要求)英文摘要 / English summary
After installing dsh-pocket 2.10.3 on DSH 0.1.5-rc.1,
dsh webaborts during plugin-tree boot withcannot get property "webServer" without inject. The root cause is not in this plugin:@deepseek-ai/dsh-client-connectiondroppedwebServerfrom its top-levelinjectin 0.1.5-alpha.1 whileregister()(line 618) still readsowner.webServer— a refactor that missed one call site. Cordis is ruled out (all three DSH versions pin^4.0.2; cordis 4.0.1 and 4.0.2 are byte-identical in size and both contain the strict check). Every third-party plugin registering a channel viaconnection.rpc.handle()is affected. A profile-levelinjectpatch on theconnectionentry is verified to fix it; a defensivetry/catcharound thehandle()call here is suggested so this class of host incompatibility degrades to "settings tab disabled" instead of "DSH will not start".