Skip to content

fix: 自注册 RPC 通道并迁移 sessionPersistence.stat,兼容 dsh 0.1.5-rc.2 - #4

Merged
better-er merged 4 commits into
mainfrom
fix/dsh-0.1.5-rc2-compat
Sep 10, 2026
Merged

better-er merged 4 commits into
mainfrom
fix/dsh-0.1.5-rc2-compat

Conversation

@better-er

Copy link
Copy Markdown
Owner

dsh-classic-coding 兼容 dsh 0.1.5-rc.2

背景

dsh 0.1.5-rc.1 与 @deepseek-ai/dsh-client-connection 0.1.5-rc.2 升级后,HostConnectionService.register 在登记路由时于 connection 服务自身上下文解析 webServer,而该上下文只 inject 了 credentials,于是抛 cannot get property "webServer" without inject,插件树因此加载失败。官方 /api 走 ctx.inject(['webServer'], ...),从不经过 rpc.handle,说明该 API 在 0.1.5 事实上已不可用。

改动

  • 新增 src/rpc-channel.ts:mountRpcChannel 直接向 webServer 注册一条 prefix 路由,复用 connection.requestRejection 做 Host 校验与浏览器鉴权,自行实现 client-request/server-response 信封。仅接受 POST,endpoint 段按官方字符集校验,请求体限长读取,非 web profile 下不挂载。
  • src/index.ts:不再调用 ctx.connection.rpc.handle,改经 mountRpcChannel 注册 /classic-coding 通道;会话冷读从 sessionPersistence.inspect 迁移到 stat,工作目录改取 snapshot.header.cwd;RpcResult.error.details 由 unknown 收窄为 object,匹配 RpcChannelResult。
  • tsconfig.json:开启 allowImportingTsExtensions,配合 import './rpc-channel.ts'。
  • smoke/verify.mjs:改为真实 HTTP 通道验证,覆盖 describe、listDir、readFile、writeFile、空端点 404、未授权 401,冷读路径走 stat。

验证

pnpm typecheck、pnpm build、pnpm smoke 全绿。

@better-er
better-er force-pushed the fix/dsh-0.1.5-rc2-compat branch from ce4b9b1 to 5c3029a Compare September 10, 2026 19:47
@better-er
better-er requested a lite review from Copilot September 10, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@better-er better-er left a comment •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

事实核对

1. 背景前提成立

已对照已安装的 dsh 0.1.5-rc.1 中的 @deepseek-ai/dsh-client-connection@0.1.5-rc.2:

  • HostConnectionService.get rpc() 取 const owner = this.ctx,即 connection 服务自身的上下文;register() 内执行 owner.webServer.register(route)。
  • connection 插件声明 inject = ["credentials"],其自身上下文不含 webServer。
  • 官方 apply 自己也是走 ctx.inject(["webServer"], (webCtx) => ...) 再注册 /api。
  • cordis 的上下文代理在未 inject 时抛 cannot get property "<prop>" without inject。

因此「任何调用方走 ctx.connection.rpc.handle 都会在登记路由时抛 without inject,该 API 在 0.1.5 事实上不可用」属实,绕开它自注册路由是必要且合理的方向。

2. 替代方案与官方一致

  • webServer.register({ kind: 'prefix', path, handler }) 返回同步 disposer,prefix 语义为 p 与 p/...,与插件用法一致。
  • connection.requestRejection(req) 实际执行 Host/Origin 校验加浏览器 cookie 鉴权,与官方 /api 路由同一条防线,复用正确。
  • endpoint 段正则与官方 ENDPOINT_SEGMENT_PATTERN 完全相同。
  • sessionPersistence.stat(id, { signal }) 在 rc.2 存在,返回 { header: SessionHeader },SessionHeader.cwd 为绝对工作目录;SessionStore.get(id) 返回的 Session.header.cwd 也正确。冷读迁移方向正确。

3. 本地验证

pnpm typecheck、pnpm build、pnpm smoke 全绿,smoke 覆盖 describe 的 live/cold/未知/空 cwd/缺 id、listDir 过滤与目录优先、readFile、writeFile 及缺 content、未知端点、空端点 404、未授权 401。构建产物 lib/index.d.ts 自包含,未残留对 ./rpc-channel.ts 的引用。

修改要求

P1 必须修:超长请求体返回 ECONNRESET,413 分支不可达

src/rpc-channel.ts:159-162 在超限时先 req.destroy() 再 throw,serve 随后才 res.writeHead(413),此时 socket 已被销毁,异常响应永远发不出去。实测:

7MiB   -> HTTP 200,落盘成功
8MiB   -> fetch 抛错 SocketError: other side closed
8.5MiB -> fetch 抛错 read ECONNRESET

对照最小复现,官方顺序先回再销毁可以正常送达 413:

先 res.writeHead(413, {connection:'close'}); res.end(); req.destroy() -> HTTP 413
先 req.destroy(); res.writeHead(413); res.end()                          -> fetch 抛错

官方 bridge 的做法是:先按 content-length 快速拒绝,流式读取超限时 res.writeHead(413, { connection: 'close' })、res.end(),最后才 req.destroy()。请照此顺序修正,并保证客户端至少能收到 413 而不是连接复位。

P2 必须处理:8MiB 上限会静默废掉大文件保存

MAX_BODY_BYTES = 8 * 1024 * 1024,而请求体是整份文件内容加 JSON 封装,实测约 8MiB 的文件即可写入失败。readFile 的响应不限长,于是会出现能打开大文件但保存不了的不一致。官方 bridge 默认上限为 300MiB,本次迁移把有效上限从 300MiB 降到 8MiB。请二选一:把上限提到与官方接近并说明依据,或明确说明 8MiB 是编辑器定位下的有意取舍,同时修好 P1 让超限有清晰报错。

P3 建议修:取消信号是死参数

serve 用 new AbortController().signal 调用 handler,从未 abort;官方传的是 request.signal,客户端断开即取消。因此 describe 的 stat 与 listDir 的 fs.resolve 收到的 signal 永远不会触发,断线后后台工作继续。建议绑定 res.on('close') 触发 abort,或复用官方 bridge 等价逻辑。

P4 建议修:未校验 content-type

官方对非 application/json 返回 415,插件实测接受 text/plain 并正常处理。鉴权不受影响,但属于与官方信封契约的偏离,建议补齐 415。

P5 建议修:README 与实现已矛盾

README.md:73 仍写「Host 端经 ctx.connection.rpc.handle('/classic-coding', ...) 注册」「插件端点必须走 handle 独立通道」,与本次改动正好相反。请同步为自注册 webServer prefix 路由的描述。

P6 建议:补测试与 CI

smoke 未覆盖请求体限长与 content-type 两条路径,正好是本次新引入且唯一出问题的地方,建议补上超限 413 与 content-type 拒绝的用例。另 .github/workflows/ci.yml 只跑 typecheck、build 与产物校验,未跑 pnpm smoke,可一并加上。

P7 建议:清理小问题

  • src/rpc-channel.ts 文件末尾缺换行。
  • src/index.ts 的 RpcResult 与 src/rpc-channel.ts 的 RpcChannelResult 结构重复,可只保留一处。
  • 插件绕开官方 connection API 自行注册 RPC 通道,是会波及其他插件的架构决策,建议按工作区规范补一份 ADR 并同步到 dsh-pause 等同样走 RPC 的插件。

结论

核心修复方向正确,背景前提与 stat 迁移都经源码核实,构建与现有 smoke 全绿。但请求体限长这条新逻辑存在确定的实现缺陷:超限时连接被复位、413 永远发不出,且有效上限从官方 300MiB 降到 8MiB 会直接影响大文件保存。P1、P2 修好之前不建议合并。

P1 超限先回 413 再断连接,并按 content-length 快速拒绝;P2 上限提到 300MiB 对齐官方 bridge;P3 取消信号绑定 res close;P4 非 application/json 返回 415;P5 README 同步自注册 prefix 路由;P6 smoke 补超限 413 与 content-type 415 用例、CI 加冒烟;P7 去掉重复的 RpcResult 结构。
smoke 加入 CI 后在 ubuntu 暴露既有问题:ABS_RE 只认 Windows 盘符与 UNC,/tmp 下的路径全被拒。补上 POSIX 根路径分支。

@better-er better-er left a comment •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审

事实核对

1. CI 已转绿

前一个 head bca0f34 曾双红,日志为 path 必须是绝对路径:收到 /tmp/cc-verify-...,根因是 ABS_RE = /^[A-Za-z]:[\\/]|^\\\\/ 只认 Windows 盘符与 UNC,冒烟测试进 CI 后在 ubuntu 把 /tmp 全判成相对路径。44fc74a 补上 POSIX 根路径分支后,远端 CI 两个 Node 版本均通过。

2. 前一轮 P1 到 P7 修复仍成立

  • P1 tooLarge 先回 413 再断连接,另有 content-length 快速拒绝;流式超限路径实测回 413。
  • P2 上限对齐官方 bridge 的 300MiB,9MiB 写入实测 200 并正确落盘。
  • P3 res.on('close') 触发 abort,实测断开后 signal 变为 aborted。
  • P4 非 application/json 返回 415,解析方式与官方一致。
  • P5 README 改为自注册 prefix 路由的描述。
  • P6 smoke 新增 415 与 413 断言,CI 加入 pnpm smoke。
  • P7 删除重复的 RpcResult,rpc-channel.ts 末尾补换行。

3. 本轮 44fc74a 改动核对

  • 只改 src/index.ts 两行,ABS_RE 扩为 /^[A-Za-z]:[\\/]|^\\\\|^\//,覆盖 Windows 盘符、UNC 与 POSIX 根路径。
  • 本地 pnpm build、pnpm smoke 全绿。
  • 独立复现:/tmp/cc-verify-xxx 不再报绝对路径错误,relative/x 仍被正确拒绝。
  • 未引入其他行为变更。

修改要求

本轮无阻塞项,以下均为可选建议。

R1 建议:改用 path.isAbsolute

手写正则在所有平台同时接受两种路径形式,例如 Linux 上也会接受 C:\x,虽无害但不精确。建议改用 node:path 的 isAbsolute,由运行时按平台判定,语义更清晰也更不易漏分支。

R2 建议:加固 smoke 的落盘断言

smoke/verify.mjs 中 check('writeFile 落盘', await readFile(target)) 未做保护,writeFile 一旦失败,该读取抛出的 ENOENT 会让整个脚本崩溃并吞掉真正的 FAIL 摘要,上一轮 CI 日志里就出现过。建议包进 try/catch,失败记一条 FAIL 后继续执行后续断言。

R3 建议:同步 PR 描述

描述仍是首轮内容,未覆盖 415、413 顺序、300MiB、CI 冒烟、README 更新与本轮的 Linux 修复。

R4 建议:补 ADR

插件绕开官方 connection.rpc.handle 自注册 RPC 通道,是会波及其他走 RPC 插件的架构决策,前两轮均已提,仍建议按工作区规范落一份 ADR。

结论

P1 到 P7 已全部修复并复现通过,44fc74a 进一步修掉 Linux CI 失败,远端 CI 现为双绿。无阻塞项,其余均为非阻塞建议。建议合并。

R1 绝对路径校验改用 node:path 的 isAbsolute,按平台判定,删掉手写正则;R2 smoke 的 writeFile 落盘断言包进 try/catch,失败时记 FAIL 而不是让整个脚本崩溃。
@better-er
better-er merged commit 5481f23 into main Sep 10, 2026
2 checks passed
@better-er
better-er deleted the fix/dsh-0.1.5-rc2-compat branch September 10, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants