Files
deepseek-harness/packages/host/apiproxy/README.zh.md
Chinesezjc d2bb2a809b fix(web-todo): dock-owned selection, keyboard-openable row, documented tail todos field
Three ds-review-bot round-8 findings on the todo display surfaces.

TodoPanel took the whole `useSession` hook and cast the snapshot to reach
`todos`, which put slot plumbing and an unchecked cast inside the presentation
component. The panel now takes `todos: readonly TodoItem[]`; TodoDock does the
selecting, matching the QueueDock posture the dock slot already establishes.

The todo row carried `onClick` with no keyboard route, so its details panel was
mouse-only. It now takes ToolRow's route verbatim: `role="button"`,
`tabIndex={0}`, and an Enter/Space handler that claims the event. The row stays
a `<div>` because a `<button>` flattens its inline spans.

`session.history`'s tail-only `todos` field was documented at the TS signature
but not in the apiproxy README pair, and the Agent Note claimed the feature
added no new wire vocabulary. Both README sides now state the tail/omission
semantics (an omitted field on a tail response is the empty plan, not unchanged
state), and the note records the one added field instead of denying it.

Tests: TodoPanel specs render the plain list; new TodoDock specs cover selection,
live follow, rollback-to-empty, and the registration shape; a new row spec pins
Enter/Space activation and non-activating keys.
2026-07-27 12:08:46 +08:00

5.4 KiB
Raw Blame History

@deepseek-ai/dsh-host-apiproxy

English | 中文

所有客户端形态共用的 API 网关TS 契约(src/api/,不依赖 Node可从浏览器导入、fetch 载体对(src/fetch/:宿主侧的 toFetchHandler,以及客户端侧的 AbstractApiClient 与平台子类)和宿主侧实现(src/api-proxy.tscreateApiProxy 加上默认导出的 ApiProxyService 网关插件,其配置为 {provider, model, workspaceRoot?},提供 ctx.apiProxy。该包package在设计上与传输方式无关不注册任何路由载体目前为 HTTP未来可以是 IPC自行包装 ctx.apiProxy。已发布的核心组合位于 apps/cli/cordis.yml

契约层(/api

协议消息组成一个四象限可辨识联合:发起方 × 请求/响应,与物理通道解耦。四种消息分别是 ClientRequestPOST /api/<method> 的请求体)、ServerResponse(该 POST 的响应体)、ServerRequestSSE 帧)和 ClientResponsePOST /api/respond 的请求体)。响应始终回显对应请求的 rpcId,绝不签发新值。方法的参数与返回值结构只存在于领域接口签名(SessionsApiHostApiEventsApi)中;RpcMethodMap 注册方法,其他所有位置均通过 RequestPayload<K>ResponseValue<K> 派生。Zod schema 以 satisfies z.ZodType<Wire<T>> 锚定类型,并分两层解析:先解析信封,再解析业务载荷,随后按方法分发。业务错误由 RpcResult 的错误分支承载(RpcErrorDetailsMap 封闭错误码集合HTTP 状态只表达载体层结果。

分层与协议决策记录在 GUI 分层与 RPC 协议 RFC中;浏览器侧消费架构记录在 Web 客户端架构 RFC中。

mux 流会在每个已附加会话的订阅基线之后,以及对应的实时原始标题事件之后,立即把基于日志的最新标题投影为经过校验的 session/title 控制帧。该投影不会把标题加入 session.list;冷会话在其中仍只有元数据,直到打开或恢复操作附加其日志。

Workspace 列表与 Session 列表是相互独立的重连基线。workspace.create 会创建唯一名称或接纳现有目录,session.create 接受可选的预分配 Session idhost/workspace-changedhost/session-added 则以任意到达顺序携带已提交的增量。SessionSummary.blankhost/session-added 帧携带派生的零事件位:客户端隐藏空白会话并按 workspace 复用它们,在首个 host/session-status(running:true) 时翻转 blank并以 session.list 作为重连权威;冷会话摘要永远不是空白——惰性持久化让从未追加过事件的会话根本不出现在 list() 中。

session.history 按消息边界分页,其尾页(不带 beforeSeq)额外携带两项页窗口本身无法提供的会话级数据:进行中局部消息的 chunk 事件,以及 todos——整份日志上最后一次 todo/write 的整表投影。较早的页面不带 todos,因为该投影是会话级而非分页级的;尾页响应缺少该字段意味着整份日志中没有任何 todo/write,因此客户端要把缺失字段读作空计划,而不是读作「状态未变」。

command.*skill.* 领域向客户端暴露宿主命令注册表和技能目录。每个方法都通过 sessionId 寻址一个会话的 Agent被服务的会话必有 Agentcommand.* 经由与 session.* 相同的路径恢复冷会话,而 skill.list 从会话头解析项目根目录,不触碰 Agent 注册表)。command.execute 在宿主侧运行一条斜杠命令行并返回脱耦结果;载体的请求信号可取消正在运行的处理器。host/commands-changed 是目录失效帧:客户端重新拉取 command.list 而不是做差分。

载体层(/client + 根路径)

AbstractApiClient 持有全部协议不变量:签发 rpcId、包装解包信封、Zod 解析、SSE 帧解码、一元请求超时,以及按微任务批处理的信封观测(subscribeEnvelopes);平台子类只提供 doFetch 传输环节。InProcessApiClienttoFetchHandler(api) 为基础,是同构接点:它运行完整的协议序列化与校验路径而不经过网络,供 dsh -p headless 模式使用。

模型体验

无。该包定义客户端与宿主间的协议契约和载体,其中没有任何内容会进入模型请求。

KV 缓存影响

无;该包既不组装也不发送提供方请求。

已知限制与延期工作

  • respond 路由已经发布,但待处理交互状态仍属宿主侧工作协议形状POST /api/respondRpcReceipt)已经定型;使延迟或重复回答具有明确语义的待处理表位于 src/api-proxy.ts,目前仍很精简(只支持问题,不支持审批)。
  • 预留 seam 不进入 RpcMethodMapsession.forkprompt.mode: 'inject'task.listhost.listModels 和描述字段 hostInstanceId 都是已记录的预留项;未知方法会在信封解析时直接失败,而不会返回「尚未实现」错误码。
  • 没有协议版本字段:客户端与宿主一同发布;只有出现独立发布的客户端后,host.describe 才会增加版本协商字段。