三种产品形态:Web、headless 与 SDK

专栏:DeepSeek Harness · 第 13 / 14 篇
DeepSeek Harness架构SDK

前几篇都在讲「运行中的 dsh 内部」。这篇往外看:同样的插件树(dsh-base 底座)如何顶上不同的壳,变成三种产品——第 5 篇那张 profile 表里的 webheadlesssdk(外加自动化的 acp)。

Web GUI:两个进程半面的分工

Web 形态是「宿主进程 + 浏览器」的协作,dsh 刻意把两侧做成对称的两半:

图表(product-forms-web-headless-sdk.md)

几个值得学的决策:

  • webserver 不懂任何业务。它只是具名路由注册表(ctx.webServer.register),/api 桥、/plugins bundle 路由、HMR SSE、SPA 静态资源各自认领,冲突即配置错误。功能插件与路由解耦:
// packages/client/modules/src/index.ts:585-591(host 侧模块认领路由)
ctx.effect(
  () => ctx.webServer.register({ kind: 'prefix', path: '/plugins', handler: this.serveBundle }),
  'client-modules: bundle route',
)
  • 浏览器也是 Cordis 应用。client 侧同样跑插件与 slot 系统,UI 功能不是一棵写死的组件树,而是约 45 个插件向类型化 slot(single/list/keyed/chain 四种基数)里注册内容。
  • 认证三道栅栏:每进程随机启动令牌(?token=... 只在首页接受一次,立刻换成签名 cookie)→ cookie 密钥存进凭据 grant → 请求信任检查(loopback/受信 + Origin 一致 + 拒绝 cross-site)。

会话数据如何到像素(读 Web 代码的地图):

链路通路
持久展示Host 会话日志 → Remote follow/page 历史 → client 会话窗口 → Conversation 组装器 → slot 视图 → React
实时流agent/assistant-stream → client 专属瞬态事件(断线重连后从日志结算重建)
交互决策Host waterfall(如审批)→ Remote $events → 浏览器 ctx.remote.$on() → 结果沿 waterfall 返回

浏览器侧消费 scoped waterfall 的真实代码(第 12 篇的审批卡片就是它):

// packages/client/ui-approval/src/client/index.ts:75-93(节选)
export function apply(ctx: ClientContext): void {
  const registerPendingInteraction = ctx.uiSession.registerPendingInteraction<PendingApproval>(() => 0)
  ctx.slots.inject('conversation.composer', () => ctx.slots.register({
    name: 'conversation.composer', priority: 1,
    select: ({ pendingInteraction }: ComposerChainProps): PendingApproval | null =>
      pendingInteraction instanceof PendingApproval ? pendingInteraction : null,
    ...
  }, ApprovalPanel))
  ctx.remote.$on('approval/request', function (request, next) {
    return answerApproval(ctx, this, request, next, registerPendingInteraction)
  })
}

headless:一次性运行

dsh --profile headless "任务":无 GUI、无服务器、不开端口。推理增量以 dsh: reasoning: 前缀流到 stderr,最终答案独占 stdout——这是给脚本和管道的消费约定。退出码即语义:最终 turn/endcompleted → 0;aborted/error/没有轮次 → 1。没有交互后续,缺任务文本在任何东西运行前就拒绝。

SDK:一条 JSON-RPC 管道

SDK 形态(dsh --profile sdk)把整个 harness 装进一个按换行分帧的 JSON-RPC 2.0 over stdio 协议里,方法表小得惊人:

// packages/sdk/protocol/src/types.ts:106-119
/** Server-to-client notifications by JSON-RPC method name. */
export interface HarnessSdkNotificationMap {
  'session.event': SessionEventNotification
  'session.status': SessionStatusNotification
  'subagent.started': SubagentStartedNotification
  'subagent.finished': SubagentFinishedNotification
}

/** Client-to-server request methods with their param and result shapes. */
export interface HarnessSdkRequestMap {
  'initialize': { params: InitializeParams; result: InitializeResult }
  'session/prompt': { params: SessionPromptParams; result: SessionPromptResult }
  'shutdown': { params: undefined; result: Record<string, never> }
}

语义上的关键决定:session/prompt 只返回持久入队回执(messageId),不等待结果——结果通过 session.event 无过滤广播回来,客户端自己收集到 session.status 变为 idle。没有版本协商,也没有取消方法(放弃轮次 = 关进程;关闭阶梯:shutdown 请求 → stdin EOF → SIGTERM → SIGKILL)。

一次 SDK 会话的时序:

图表(product-forms-web-headless-sdk.md)

Python SDK 是 TS 的设计孪生,但它不自带应用——随附的运行时 wheel 内嵌 dsh 可执行文件,以 --profile sdk 拉起子进程。两个硬规矩:dsh_home 必须显式给(绝不静默读 ~/.dsh);进程惰性启动、跨 run() 复用。

补充:多 agent 与后台任务

  • subagentctx.subagents):一个约定服务 + 任意多提供方——进程内 spawn/fork、ACP、真实 Codex、真实 Claude Code、完整 Harness 运行时——tool-subagent 把委派暴露给模型。「把一个轮次委派给另一个产品」和「新建一个子 agent」在同一个接口之后。
  • jobsctx.jobs):长任务注册为作业(归属启动它的会话,永不串台),完成以会话内通知送达,job_* 工具负责读取与终止。第 14 篇写后台任务时会用到。

与其他模块的联系

  • Web 的审批卡片(第 12 篇)是 scoped waterfall 的浏览器应答者;
  • headless 的退出码(completed→0)直接来自 turn/end 的 reason 枚举(第 7 篇);
  • SDK 的 session.event 广播的就是会话日志(第 6 篇)——订阅者拿到的是完整事件流而非摘要;
  • subagent 的六种后端(第 14 篇)意味着”委托给真实 Codex”在协议层是第一公民。

三种形态其实是同一个问题的三个答案:「agent 的输入输出边界画在哪」。Web 画在浏览器与宿主之间(事件流 + waterfall 透传),headless 画在进程边界(stdout/stderr/退出码),SDK 画在 stdio 协议(JSON-RPC)。底座完全相同——这正是第 5 篇「profile = 有序组合包」的回报。

下一篇是整个专栏的收官:把这些全部用起来,亲手写一个工具插件。

← 返回文章列表