Files
deepseek-harness/missions/tasks/20260720-0101-hostruntime-split-design/design.md
T
imccyu 0681ac47de chore(gui): mission work logs
chore(gui): mission work logs — cordis design finalization, tool-card wire archive, incident records

chore: missions

chore: missions

chore(gui): mission ledger — batch-2 answers, parallel dispatch state, jsdom coverage re-scope

chore(gui): ledger — night-mode standing orders (self-commit small, no push, 5-min refresh)

chore(gui): ledger — jsdom batches 2-4 landed (233 green), coverage probe next

chore(gui): ledger — web-ui coverage probe 65%, four-tier fill plan approved

chore(gui): ledger — cordis-impl B1 state after third API drop, decisions on file

chore(gui): ledger — 01:32 patrol snapshot (jsdom tier-1 landed, coverage-fixer probed)

chore(gui): ledger — 01:37 patrol (peer src trio landed, coverage-fixer still silent)

chore(gui): ledger — 01:42 patrol (jsdom tier-2 landed, peer committed x2, coverage-fixer 2nd probe)

chore(gui): ledger — coverage diagnosis complete (6-file gap list), web-ui at 91.4%

chore(gui): ledger — 01:46 patrol (B1 done, jsdom tier-3 landed, coverage fix batch running)

chore(gui): ledger — 01:51 patrol (jsdom tails x2 landed, B2 underway)

chore(gui): ledger — 01:56 patrol (gateway.ts 337 lines, checkpoint T-7min)

chore(gui): ledger — hold/pending split ruling, coverage-fixer externalize-or-restart ultimatum

chore(gui): ledger — 02:01 patrol (B2 done, jsdom final arms, coverage ultimatum pending)

chore(gui): ledger — 02:03 checkpoint executed (fixer2 respawn, four lanes released, three owners cold-started)

chore(gui): ledger — all six lanes acked, type isolation first live proof (client closure clean)

chore(gui): ledger — 02:08 patrol (all seven lanes active, wire carrier assembled)

docs(gui): respond-design task checkpoint — apiproxy wire-layer recon done

chore(gui): ledger — P0-2 contributor AGENTS.md landed (dd28a5019)

docs(gui): respond-design checkpoint 2 — host-side recon (stub respond, frame types, approval seam, ACP answerer precedent)

docs(gui): OOP debt inventory — seven territories, 2 real debts (createApiProxy, createFixtureApi), rest ruled keep-as-is

docs(gui): disambiguation note on the archived i18n design task

chore(gui): ledger — 02:12 patrol (exclude removed, mixed-knife incident under reconciliation)

chore(gui): ledger — 02:15 wave (jsdom mission closed, OOP audit done, B3 isolation proof, mixed-knife resolved)

chore(gui): ledger — 02:17 patrol (attribution reversal filed, arch-session probed)

chore(gui): ledger — 02:22 patrol (B4 done, B5+B6 merged batch, arch-session deadline set)

docs(gui): respond-design checkpoint 3 — client-side recon (pending map, PendingCard onRespond stub, AbstractApiClient.respond ready, bootHost missing approval mounts)

docs(gui): peer carrier territory review — 2 fixes (SSE cancel leak, route-reservation guard), 1 ruling ask (RPC-log visibility), compliance ledger

chore(gui): ledger — 02:27 (arch-shell respawn, territory review verdicts routed, ask-deny finding flagged)

docs(gui): P1-5 respond design page complete — pending registry, wire answerer, client state machine, first-wins arbitration

chore(gui): ledger — 02:31 patrol (respond design complete, B5 wire smoke green, shell knife 1 underway)

docs(gui): respond design — add §0 status warning (web host ask defaults to deny), mount-behavior delta, no-timeout ruling with Config discipline

chore(gui): ledger — 02:42 patrol (respond line closed pending review, B7 last piece underway)

docs(gui): respond design contract review — direction pass, 2 doc fixes (settle-order contradiction, answering-state race), A/B/C compliance ledger

docs(gui): respond design — contract review fixes (R1 verify-before-delete arbitration, R2 answering+resolved-frame transition, ask dual-source wording, rejected-is-ok-value note)

chore(gui): ledger — 02:46 patrol (respond line final, two user decisions distilled, arch-shell deadline)

docs(gui): respond review addendum — contract-gap ruling: approve plan A (ApprovalRequest.id), wire unchanged, drop plan B backscan

chore(gui): ledger — cordis B7 summit: real-browser 10/10 green, user acceptance criterion proven

docs(gui): respond design — contract gap #4 approved as plan A (ApprovalRequest.id), backscan fallback retired, blade 0 prepended

docs(gui): respond design — final polish (owner-approval vs user-go-ahead wording, implementation handoff notes)

chore(gui): ledger — 02:51 (shell-exec third respawn with operational script, respond line 4-knife final)

chore(gui): ledger — 02:53 wave (respond five-knife true final, B7 closed 12/12, client.ts green)

chore(gui): ledger — 02:56 patrol (cordis closeout bounced pending R1/R2/N1, shell-exec first sign of life)

chore(gui): ledger — 03:01 patrol (R1/R2/N1 remediation in flight across four files)

chore(gui): ledger — cordis line officially closed and archived, verified on disk (24 knives, 12/12, reviews closed)

chore(gui): ledger — 03:06 patrol (shell-exec final window, lowered first-knife bar)

chore(gui): ledger — 03:11 patrol (shell line iced-broken: three registries on disk)

chore(gui): ledger — 03:15 patrol (quiet window, both active lanes within threshold)

chore(gui): ledger — 03:20 patrol (shell five files up, api-proxy plan reported)

chore(gui): ledger — 03:25 patrol (shell migration in flight with history-preserving moves, webserver green)

chore(gui): ledger — 03:30 patrol (shell knife-1 in verification, api-proxy patching)

chore(gui): ledger — shell knife 1 accepted (694cecc53), knife 2 released

chore(gui): ledger — 03:40 patrol (knife 2 pre-move stage, cold-list spec appears)

chore(gui): ledger — 03:45 patrol (rpclog moves staged, api-proxy two specs in flight)

chore(gui): ledger — 03:54 patrol (knife-2 code done, coverage full-run final check)

docs(gui): coverage-fixer task ledger — fixer2 takeover, per-file fix log, isolated reportsDirectory pitfall

chore(gui): ledger — coverage lane closed and accepted (a19f069a5), the PR #443 CI fix knife

chore(gui): ledger — 04:04 patrol (knife-2 calibration, sole active lane)

chore(gui): ledger — shell knife 2 accepted (f8fb77b95), knife 3 released as final night task

chore(gui): ledger — 04:19 patrol (knife-3 past half: callback chain through, ToolCallDetail up)

chore(gui): ledger — night closeout summary: seven lanes closed, wake-up decision sheet

chore: missions

chore: missions

chore: missions

chore(gui): mission-local browser/probe verify scripts under missions/scripts/

The six acceptance/probe scripts move here as mission-side working
material (headers and relative imports adjusted for the new location):
carrier-errors, rpclog-panel, session, session-real,
webserver-backpressure, webserver-hardening.

chore(gui): verify-relocate mission log

chore(gui): verify-relocate mission log — R1 guard addendum

chore(gui): gates-continue mission log — CI-equivalent sequence all green

chore(gui): VS Code 扩展体系双边设计调研报告

chore(gui): 调研追加 4.5 节——git 扩展数据面与 scope 绑定

docs(gui): web plugin system RFC — walkthrough + design notes

docs(gui): RFC — restore existing SSE/POST as the v1 transport; envelope rides on it (D16)

docs(gui): RFC — envelope demoted to chan-dispatch, scope out of envelope, rpc-log cut, peer deferred, scope tree is native cordis (D17-D21)

docs(gui): RFC — hooks re-derived from component needs: useWatch/useAction only, useService removed; sessionHub cut, projections user-space, router rename, loader-only root (D22-D25)

docs(gui): RFC — drop stale fork vocabulary (vendored cordis has Fiber only; scope = mintScope pattern), hook idempotence contract (D26-D27)

docs(gui): RFC — session precision seam: plugins read scope key (host paradigm), React gets it from tree position via SlotOutlet (D28)

docs(gui): RFC — domain hooks owned by plugins over framework primitives; useConversation paradigm carried over (D29)

docs(gui): RFC — ctx services are the inter-plugin API (cordis proper); declarations are wire-only; get(id) returns scoped ctx (D30)

docs(gui): RFC — full ctx.conversation walkthrough: root-singleton scope-sensitive service, caller-ctx scope key, get(key) as scoped ctx (D31)

docs(gui): RFC — no client-side agents collection: session state machine already expresses the duality; agent resolution stays host authority (D32)

docs(gui): RFC — v1 stays session-precision, no agent-level isolation; incarnation/agent-axis designs archived in ledger (D33)

docs(gui): RFC walkthrough — full rewrite to final state (D16-D33 consolidated), end-to-end chain restored

docs(gui): RFC — apiproxy demoted to generic channel routing; domain RPCs dissolve into owner plugins (D34)

docs(gui): RFC — TS-interface-first wire contract (zod internal), conversation owns the dialogue frame with pluggable views (D35)

docs(gui): RFC — page skeleton (sidebar+conversation), projects as plugin not service, nested slots via owner registries (D36)

docs(gui): RFC — SlotMap declaration-merging slot model: single register API, inject-as-ownership, FC-typed registration, typed outlets (D37)

docs(gui): RFC — slot props whitelist: identity, display params, materialized snapshot slices, stable UI callbacks (D38)

docs(gui): RFC — end-to-end data flow: three transforms, equality protocol table, immer placement; i18n/theme kept standard (D39-D40)

docs(gui): RFC — full external-injection model: props carry values + stable injected hooks; shared/client/react example rewritten (D41-D43)

docs(gui): RFC final trio — modules.md (agent implementation spec), architecture.md (human walkthrough), plugins.md (business plugin inventory)

docs(gui): RFC — props three-source merge (scope-standard useSession auto-injected); keyed key vs list id disambiguated (D44)

docs(gui): RFC — inject comment says what it is (the React-facing props bundle); SessionHandle rename; snapshot-production story unified on buildSnapshot

docs(gui): RFC architecture — full React component tree walkthrough: props three sources, slot vs plain children, hook taxonomy per node

docs(gui): RFC — module map finalized (ui-slots/web-react/connection/runtime/ui-*/web); slots onChange replaced by cordis events; toolcall dimension; detail sidebar default-collapsed with toolName-keyed routing

docs(gui): RFC plugins — openDetail relay chain: toolcard calls chat-view injected action, chat-view relays to conversation sidebar

chore(gui): progress ledger — full archive rewrite: RFC outcome digest, open gaps, dispatch plan, cold-start entry

docs(gui): RFC grill pass 1 — SlotScope axis (root/session) on declares, Gate dependency inversion, inject handle by scope, W5 acceptance list, gantt relay chain fixed

docs(gui): figma analysis — sidebar/projects/sessions 区域交互视觉理解报告

docs(gui): figma 解析报告 — details 面板/多视图 tabs/未来功能区盘点 + slot 需求清单

docs(gui): figma 对话主区解析报告 — 消息流/tool calls 变体/审批接管输入框/Header tabs/视觉 token

docs(gui): plugins.md rewritten from figma analysis — three-column layout, full slot reservation table, selection channel, composer-takeover approvals, phased scope

docs(gui): layout dynamics ruled (drag+collapse both rails, details yields first, composer swap-panel, same-component transition); toolviews promoted to named scope-aware registry

docs(gui): P-I scope locked (details minimal, dual theme, chat-view, custom toolview sample); teammate dispatch plan — 6 owners by package, dependency-driven waves, contract arbitration

docs(gui): P-I api-contracts (full inter-package API spec) + dispatch plan (T0 skeleton knife, 7-dev roster, task briefs, milestones)

docs(gui): api-contracts v2 — scope tree in P-I, bundle loader + per-plugin CSS isolation in P-I, agent-scoped toolviews live, zustand engine, renames (SessionProvider/ObservableSnapshot/SessionBinding), router owns all shell view-state

docs(gui): services roster + progressive loading (no blocking loadAll), SlotsService as real cordis Service, renderSlot/renderSuspenseSlot duo, ui-traj teammate

docs(gui): loading-chain gaps ruled — dev=rebundle no HMR, ui-primitives package, externals on globals (no import map), host injects __DSH_BOOT__ into HTML (zero round-trip)

docs(gui): api-contracts v3 + dispatch v2 final — 12 packages, services merged in, progressive loader, global externals, __DSH_BOOT__ injection, 8-dev roster with convo split and ui-traj

docs(gui): v3 amendments — router renamed ctx.layout, ui-trajectory has no service (pure consumer sample), wait-for-settled loading (no Suspense in P-I, ledger 6b)

chore(gui): progress — pre-compact final state: v3 revision chain, 8-dev roster, T0 procedure, doc authority order

docs(gui): authority banners — modules/architecture get v3 term-mapping headers, walkthrough marked as archived process doc

docs(gui): cssdesign token set is THE theme source (--dsw-* variables, data-ds-dark-theme switch); recorded in contracts + progress

docs(gui): architecture.md full v3 rewrite — loading chain, 12-package map, service roster, slot/inject/toolviews, data flow, component tree, perf model, all current

docs(gui): contracts — UI plugins are dual-entry host plugins (node half serves client asset via ctx.webPlugins; __DSH_BOOT__ derives from it; client-closure gate back in scope)

docs(gui): contracts — closure-factory bundles with DI require (no globals), package.json dshWeb declarative discovery (no serve ritual), create-then-send empty state with project picker, ancestry() for breadcrumb, unload stubbed until HMR, props.renderSlot confirmed

docs(gui): dshClient declaration (inject/platform/immediately, exports./client), closure-DI require loading — synced across contracts/dispatch/modules/architecture/walkthrough

chore(gui): progress — record final loading-chain rulings (dshClient declaration, closure-DI require, startSession) before compact

docs(gui): architecture.md — developer-facing whole-web architecture on master baseline 6b16a67cb: what exists, what is new, no process narrative

docs(gui): architecture.md — self-contained whole-web architecture: absorbs still-valid substance from the branch RFCs (host layering, four-quadrant RPC, object layer, testing tiers) under the new plugin system as the override

chore(gui): progress — final pre-compact snapshot: contracts digest, apiproxy purity ruling, T0 procedure with first-action list

docs(gui): api-contracts v3 §3.1 — apiproxy purity principle with three-way existing-code verdicts

docs(gui): api-contracts v3 — immediately reinterpreted as static-infra group (8-package dshClient scope, boot manifest reconciliation)

docs(gui): api-contracts v3 — immediately corrected to early-load dynamic group (prod shell must not rebundle); loader shell-held; bundles register their export surface into module table

docs(gui): architecture — align with immediately=early-load dynamic group ruling; loader shell-held; module-table registration of loaded bundles

docs(gui): T0 checklist — 12-package skeleton table, 4-cut sequence, mv/attic/rewire rules (pre-drafted, awaiting go)

docs(gui): t0-checklist — pin figma-flows findings (missing font-family base vars, three alias vars behind upstream)

docs(gui): dispatch v2.1 — drop cordis-web salvage wording, two-wave staffing, loader/immediately boundary updates

docs(gui): progress + t0-checklist ledger — T0 landed, staffing status, execution accounting

docs(gui): api-contracts v3 — arbitration round 1: renderBody deps, RootBindingProvider, flush default sync, prune current, loader subpath, config-source P-I bar

docs(gui): v3 §3.2 connection 导出清单附录(rt-core 对账)+ rt-core 实现计划档案

docs(gui): progress — T1 milestone, arbitration round 1 ledger, fw-react timeout escalation

docs(gui): progress rolling update — per-line battlefield state at 00:2x, mailbox-vs-contract lesson, small-batch discipline reinforced

docs(gui): fw-react notes — v3 §2 complete, seven knives, T1/T2 follow-ups

docs(fw-slots): archive — four packages landed, open tails logged

docs(gui): progress — framework layer complete (web-react five, fw-slots four packages), T2 gated on rt-core runtime knife only

docs: api-contracts

docs: style-spec

docs

docs(gui): tsconfig convergence ruling — no host.json, root resumes host-aggregate duty, typecheck = root + client aggregates

docs(gui): missions 根三份 07-18 世代档案加「已被取代」头注——指向 web-plugin-rfc 现行权威并注明新旧对应

missions

docs(gui): progress rewritten for post-closeout state — wave ledger, architecture finale, teammate roster with handover notes, pending-user-command queue
2026-07-22 21:30:30 +08:00

25 KiB
Raw Blame History

hostruntime 拆包 + dsc 双命令 · 实现级设计(v2)

2026-07-20 v2:按用户五问裁决(Q1Q5+ 分层原则补钉 + Electron 载体澄清 + acp 前瞻整体重写。v1→v2 变更记录见文末。 读者:编码 teammate + apiproxy-designreview §③)。现状基线:step2 后工作树(apps/dsc/src/bin.ts 122 行、apiproxy 三层 api/+fetch/+impl/+index.ts 内 bootHost)。 范围红线:Electron/acp 不做(各留接缝一节);api/ 与 fetch/ 内容零改动;GUI 期不遵循仓库门禁。

⓪ 包结构总纲(用户裁决,全文档的宪法)

  1. 分层原则:packages/host/*packages/client/* 按「能力支持方」分层——host/ 包只提供 host 侧能力,client/ 包只提供 client 侧能力,每包单边不混;多种支持方的混合一律放 apps/(哪个 app 要混,拼装写在那个 app 里)。
  2. 消费面唯一经 ApiProxy(Q1,精确化):所有消费型 clientweb / Electron / headless)走 apiproxy,不同接入只是「fetch 形函数的伪造方式不一样」(HTTP / 进程内注入 / IPC 桥)。协议桥前门(ACP 这类把 core 暴露给外部生态的)不属消费型 client——直接挂 core ctx,不套 fetch。两类东西,不是例外。
  3. apiproxy = 前置层:契约 api/ + 载体 fetch/,做简单,所有接入方都要(现状已是,只做摘除)。
  4. hostruntime = 后置装配层 / 应用实体:配哪些插件、装哪些东西的装配入口;host 级配置的归属地——defaults、persistenceRoot将来的用户 profile(~/.dsc 一族)也归这里
  5. 每个接入方 = 自己一个拼装包/拼装模块web 形态 = host/webserver 包(HTTP+静态+SSE 桥)+ client/web-runtime(已有);headless = apps/dsc 内部模块(混合体不建包,见 ⓪-1);Electron 将来 = apps/electron 自己拼装。
  6. 进程模型(Q3Electron 走 sidecarspawn 独立 host 进程);本轮只保证 startHost 返回形状可被 sidecar 入口 bin 复用,不实现。
  7. 命名规则(用户定死)packages/host/*packages/client/* 下的包,npm 包名必须含目录组前缀——host/runtime → dsh-host-runtime、host/apiproxy → dsh-host-apiproxy、client/web-runtime → dsh-client-web-runtime、client/web-ui → dsh-client-web-ui。目录名不重复组前缀(host/ 已表达);因此这些包名尾段≠目录名,tsconfig.base.json 的 dsh-* 通配(按目录名解析)命不中,每包需显式 paths 条目。2026-07-20 02:0x 已全量改毕(含存量三包,与 session-design 的统一改名合流)。

已锁实现结论

  • 新包两个:packages/host/runtime@deepseek-ai/dsh-host-runtime)、packages/host/webserver@deepseek-ai/dsh-host-webserver)。
  • dsh-host-apiproxy 退化纯契约+载体;impl/ 与 bootHost 迁出到 hostruntime。
  • apps/dsc 瘦身:bin.ts 只剩 loadEnv + parseArgs 粗分发;dsc web(唯一起 HTTP 的形态)与 dsc -p "task"(零 HTTP、零端口、ApiProxy 同构直调、跑完打印退出)。
  • -p 是协议第二个真实消费者:new InProcessApiClient(host.handler) 全程真跑载体链(类体系后写法,commit 893421d50)。

① 依赖方向图(拆分后)

apps/dsc ── bin.ts 分发 ── web.ts / headless.ts 两拼装模块
  │ depsdsc-webdist 解析,web 用)· dsh-host-webserverweb 用)
  │       dsh-host-runtime(两命令共用)· dsh-host-apiproxy-p 的 client + 类型)
  ▼
packages/host/webserver     零 workspace 依赖(node:http + 注入的 fetch 形 handler
packages/host/runtime ──► dsh-host-apiproxy(契约+载体)
  │ ctx.plugin(...)                 ▲ /api /client 子路径(type-only + AbstractApiClient 子类)
  ▼                                 │
harness core 各包            packages/client/web-runtime(不变)
  • 方向纪律:hostruntime → apiproxy 单向;apiproxy 零 harness 运行时依赖;client 侧包永不 import host 侧包;webserver 不依赖 hostruntime——它收 { fetch } 形 handler(结构 typing,全局类型零 import),「webserver → hostruntime」只是运行时注入关系,不是包依赖。
  • webserver 定位(用户澄清后收窄):web 形态(浏览器访问)专用承载Electron 不复用它(renderer HTML 走 file://fetch 走 IPC 桥,§⑧)。

各包职责一句话

拆分后职责 变化
@deepseek-ai/dsh-host-apiproxy 前置层:TS 契约(api/+ fetch 载体(fetch/);Node/浏览器皆可 import impl/、bootHost 迁出;deps 16→6
@deepseek-ai/dsh-host-runtime 装配层/应用实体:bootHostcore spine 组合)+ createApiProxy + startHosthost 级配置归属地(defaults/persistenceRoot/将来 profile 新建(迁入+新增 start.ts
@deepseek-ai/dsh-host-webserver web 形态 HTTP 承载:静态服务 + /api→handler 桥 + SSE 写出 + close 语义 新建(从 bin.ts 4–7 段抽出)
@deepseek-ai/dsc 命令行入口:分发 + 两命令拼装模块(混合体属地,⓪-1) bin.ts 拆三文件
@deepseek-ai/dsc-web / client 两包 不变

② startHosthostruntime 的启动接缝,Electron/acp 前瞻的唯一权威)

语义:「boot core → 装配 ApiProxy → 装配 fetch handler」收为一步。返回物按「壳自选承载」设计,四类消费共用:node:http(dsc web)、进程内直调(dsc -p、测试)、IPC 桥(将来 Electron sidecar)、前门插件挂载(将来 dsc acp

// packages/host/runtime/src/start.ts
export interface StartHostOptions {
  /** 透传 bootHostBootHostOptions 全量:persistenceRoot 必填 + provider?/model?)。将来 profile/日志开关在此 additive。 */
  boot: BootHostOptions
}

export interface RunningHost {
  /** 契约实现(进程内消费者直调;IPC 适配层的输入)。 */
  api: ApiProxy
  /** WHATWG fetch 形载体(web 壳桥到 node:httpElectron IPC 桥的 host 侧终点)。 */
  handler: { fetch: typeof fetch }
  /** host 级默认路由(describe 与各壳共用同一来源)。 */
  defaults: HostDefaults
  /**
   * 根上下文——**正式接缝**(不是逃生舱):①协议桥前门插件的挂载点
   * `dsc acp` = startHost() → ctx.plugin(uiAcp, config),⓪-2 的第二类消费);
   * ②headless 的 session 事件订阅。纪律:消费型 client 不得经 ctx 绕开 api
   * 壳不得用 ctx.plugin 改«装配»(挂前门 ≠ 改装配:前门是壳形态本身)。
   */
  ctx: Context
  /** 停机单一出口(ctx.fiber.dispose())。幂等:二次调用返回同一 promise。 */
  dispose(): Promise<void>
}

export async function startHost(options: StartHostOptions): Promise<RunningHost> {
  const host = await bootHost(options.boot)
  const api = createApiProxy(host.ctx, host.defaults)
  const handler = toFetchHandler(api)
  let disposing: Promise<void> | undefined
  return { api, handler, defaults: host.defaults, ctx: host.ctx, dispose: () => (disposing ??= host.dispose()) }
}

结论注记:

  • handler 收进返回物:多壳共用装配;将来 handler 装配长参数(zod dev 开关、日志 tap)收在这一处。
  • stdout 纪律(acp 前瞻牵出)bootHost 现装配零 stdout 写手——十一个插件里没有 logger-console(与 acp-demo 同理:其 peers 刻意无 logger-consolestdout 留给纯 JSON-RPC)。打印是壳的事(web 壳的打印行在 web.ts)。将来任何给装配加日志/诊断输出的改动,必须走 StartHostOptions 可关(如 logSink?: (line)=>void,缺省丢弃)——写死这条纪律,quiet 开关本轮不做(现状无写手,无可关之物)。
  • dispose 幂等:信号竞态与正常收尾共用。
  • 不设生命周期钩子/事件:无现消费者,additive 空间在 StartHostOptions。

③ dsh-host-apiproxy 退化(迁移属地清单——apiproxy-design review 对象)

文件动向

现路径 去向 备注
src/api/**14 文件) 不动 契约属地仍归 apiproxy-design
src/fetch/handler.ts / client.ts 不动 载体=契约孪生面,零 harness 运行时依赖
src/impl/api-proxy.ts hostruntime src/api-proxy.ts 整文件平移;头部相对 import 改 @deepseek-ai/dsh-host-apiproxy/api@deepseek-ai/dsh-host-apiproxy/api/rpcTODO(step2) 注释群随文件走
src/index.ts 的 bootHost 一族 hostruntime src/boot.ts BootHostOptions/HostDefaults/HostHandle/bootHost 原样平移
src/index.ts re-export 段 改写 退化版见下

apiproxy 退化后出口

src/index.tsre-export api/index.ts 全部 + toFetchHandler + AbstractApiClient/InProcessApiClient/IApiClient(迁移当时为 createApiClient893421d50 换类体系),别无他物。package.json exports 保留 . / ./api / ./client / ./src/* / ./package.json新增 "./api/*": "./src/api/*.ts"hostruntime 迁入文件要 import ../api/rpc.ts 的对等物;比 ./src/api/... 深路径干净)。deps 收缩为:dsh-branddsh-llmdsh-sessiondsh-user-approvaldsh-user-interactionapi/ 的 type-only 上游)+ zod;删去 cordis、plugin-timer、agent、agent-loop、bash-local、llm-deepseek、session-persistence-jsonl、system-prompt、tasks、tools。tsconfig references 同步收缩。 已核实安全:全仓只有 apps/dsc 用 apiproxy 根入口;web-runtime 只吃 /api /client 子路径,零影响。

hostruntime 包

packages/host/runtime/package.json 照 apiproxy 现形状(main/types 指 lib、./src/* 通道、private、全平铺 deps);deps = apiproxy 删去的十项 + @deepseek-ai/dsh-host-apiproxytsconfig references = core 十包 + vendor(cordis/timer) + apiproxy。src 四文件:boot.ts(迁入)、api-proxy.ts(迁入)、start.ts(§②)、index.tsbarrelbootHost 一族 + createApiProxy/ApiProxyDefaults + startHost/StartHostOptions/RunningHost 全出口)。

④ dsh-host-webserver 包(web 形态承载)

从现 bin.ts 4–7 段抽出成包。零 workspace 依赖node:http/path/fs + 结构 typing 的 handler 参数),host 组内最底层。

// packages/host/webserver/src/index.ts —— 出口面全文
export interface WebServerOptions {
  /** 监听端口。 */
  port: number
  /** 静态根内 index.html 的绝对路径(调用方解析好传入——dist 定位是 dsc 的 workspace 知识,不属本包)。 */
  distIndex: string
  /** fetch 形 API 载体;/api/* 前缀请求桥给它(含 SSE 流式写出)。 */
  apiHandler: { fetch: typeof fetch }
}

export interface RunningWebServer {
  /** 实际监听端口(供打印;本轮恒等于 options.port)。 */
  port: number
  /** 停机:close + closeAllConnectionsSSE 长连接强制断,防 close 挂死)。幂等。 */
  close(): Promise<void>
}

/**
 * 起 web 形态 HTTP serverlisten(port, '0.0.0.0')。
 * 路由三段:/api/* → apiHandler 桥(node:http↔WHATWGreq close→abortSSE 逐 chunk 写出);
 * GET/HEAD 之外 405;静态 = step1 锁定语义(MIME 六项/403 穿越判定/未命中 SPA 回退 200)。
 * listen 失败(EADDRINUSE 等)reject——壳决定退出方式;listen 后的 server error 走 onError。
 */
export function startWebServer(options: WebServerOptions, onError: (err: Error) => void): Promise<RunningWebServer>

实现细节(编码 teammate 指引):

  • 文件布局:src/index.tsstartWebServer + 桥)+ src/static.tsMIME 表 + serveStatic 纯函数——现 bin.ts 5 段整体平移)。
  • /api/* 桥、静态逻辑、403/SPA 语义逐行平移现 bin.ts 5195,行为零改动(step1/step2 验收锁定)。
  • listen 包 Promiselistening 事件 resolve、首个 error 事件 rejectresolve 后的 error 转 onError(现 bin.ts 101104 的 disposeAndExit(1) 语义由壳在 onError 里做)。
  • close()server.close() + server.closeAllConnections() 包 Promiseclose 回调 resolve),??= 幂等。
  • 不打印dsc web: http://127.0.0.1:<port> 打印行归壳(web.ts)——sidecar/测试复用本包时不带 dsc 词汇。

package.json@deepseek-ai/dsh-host-webserver,形状照 hostruntimelib 入口 + ./src/*),dependencies 空对象省略。tsconfigreferences 为空数组(零依赖),其余同形。

⑤ apps/dsc 改造(混合体属地,⓪-1)

apps/dsc/src/
  bin.ts        ← loadEnv + 粗分发(全文见下)
  web.ts        ← runWeb(argv)web 形态拼装 = startHost + resolveDist + startWebServer + 打印 + 信号
  headless.ts   ← runHeadless(argv)-p 拼装 = startHost + InProcessApiClient 同构 + 事件消费(§⑥)

v1 曾设 static.ts——静态逻辑已随 §④ 入 webserver 包,dsc 不再持有。)

bin.ts 全文级

#!/usr/bin/env node
import { loadEnv } from '@deepseek-ai/dsh-app-boot'

loadEnv('dsc')
const argv = process.argv.slice(2)
if (argv[0] === 'web') {
  const { runWeb } = await import('./web.ts')          // 动态 import:形态互不加载
  await runWeb(argv.slice(1))
} else if (argv.includes('-p') || argv.includes('--prompt')) {
  const { runHeadless } = await import('./headless.ts')
  await runHeadless(argv)
} else {
  process.stderr.write('usage: dsc web [--port N] | dsc -p "task"\n')
  process.exit(1)
}

细命令 parseArgs 在各模块内(web 收 --portheadless 收 -p/--prompt),bin 层不聚合 options。

web.ts 动线(相对现 bin.ts 的重排)

parseArgs --port(默认 3080,非法 stderr+exit 1positional 已被 bin 层剥掉)
→ const host = await startHost({ boot: { persistenceRoot: './.sessions' } })
→ resolveDistcreateRequire(import.meta.url).resolve('@deepseek-ai/dsc-web/dist/index.html')
    catch → stderr「先跑 pnpm --filter @deepseek-ai/dsc-web build」→ exit 1dist 定位知识留在 dsc,§④ 结论)
→ const server = await startWebServer({ port, distIndex, apiHandler: host.handler },
    err => { stderr; void shutdown(1) })
    listen rejectEADDRINUSE)→ stderr + await host.dispose() + exit 1
→ console.log(`dsc web: http://127.0.0.1:${server.port}`)
→ shutdown(code)exiting 门闩 + try { await server.close(); await host.dispose() } finally { process.exit(code) }
   SIGTERM→0 / SIGINT→130jsonrpc-demo 样板不变,close 顺序:先 server 后 host

dsc -p "task" 动线(headless.ts

**确认:不 import webserver/node:http,不监听端口,不解析 dist。**同构注入 = 协议第二真实消费者(wire 序列化/zod/SSE 帧全被真实运行)。

import { parseArgs } from 'node:util'
import { startHost } from '@deepseek-ai/dsh-host-runtime'
import { InProcessApiClient } from '@deepseek-ai/dsh-host-apiproxy'

export async function runHeadless(argv: string[]): Promise<never> {
  const { values } = parseArgs({ args: argv, options: { prompt: { type: 'string', short: 'p' } }, allowPositionals: false })
  const task = values.prompt
  if (task === undefined || task === '') { /* usage stderr + exit 1 */ }

  const host = await startHost({ boot: { persistenceRoot: './.sessions' } })
  const api = new InProcessApiClient(host.handler)         // 同构点

  const abort = new AbortController()
  const created = unwrap(await api.sessions.create({ rpcId: mint(), payload: {} }))   // !ok → stderr+dispose+exit 1
  const frames = api.events.mux({ rpcId: mint(), payload: {} }, abort.signal)
  const done = consumeUntilTurnEnd(frames, created.sessionId)                          // 先开流
  unwrap(await api.sessions.prompt({ rpcId: mint(), payload: { sessionId: created.sessionId, mode: 'queue', content: [{ type: 'text', text: task }] } }))
  const outcome = await done

  process.stdout.write(outcome.text + '\n')
  abort.abort()
  await host.dispose()
  process.exit(outcome.reason === 'completed' ? 0 : 1)
}

consumeUntilTurnEnd(frames, sessionId)headless.ts 私有;照 cli-demo runOneShot 三步判定,cli.ts:252-262 先例,输入换 RpcRequest<MuxFrame>):

targetTurn?: numbertext=''reason?: string
for await frame:只取 payload.type==='session/event' && sessionId 匹配,event=payload.event
  1. targetTurn 未定 && event.type==='turn/start' && event.data.trigger.kind==='message' → targetTurn=event.data.turn(启动注入 turn 被跳过)
  2. event.type==='assistant/message' && event.data.turn===targetTurn → text=其 content 的 text block 拼接(后写覆盖,「最后一条为准」)
  3. event.type==='turn/end' && event.data.turn===targetTurn → reason=event.data.reason.kindreturn {text, reason}
payload.type==='stream/error' 或 for-await throw → stderr + return {text, reason:'error'}

边界结论:

  • 先开 mux 后 prompt:帧不丢;同进程无竞态仍保持此序——换远程 HTTP 时代码零改(同构纪律)。
  • mint = RpcId(randomUUID())unwrap = RpcResponse 拆封,!result.ok 打 stderrerror.code: error.message+ dispose + exit 1。
  • 退出码:completed→0,其余(aborted/error)→1boot 失败(缺 key)顶层 rejection fail-loud 非零。
  • Ctrl-C 走 Node 默认(无 server 可关;持久化由 core turn 边界 flush 保证)——台账 §⑨-3。
  • 审批/问答:现装配无审批 provider 不会挂等;将来加装配走 StartHostOptions additive——台账 §⑨-2。

⑦ 迁移步骤(顺序执行,每步 typecheck 可绿)

  1. 建 hostruntime:目录+package.json+tsconfigsrc/boot.tsapiproxy/src/index.ts:1-52 平移)、src/api-proxy.tsimpl/api-proxy.ts 平移,import 改 @deepseek-ai/dsh-host-apiproxy/api/api/rpc)、src/start.ts(§② 新写)、src/index.tsbarrel)。
  2. apiproxy 摘除:删 src/impl/index.ts 重写退化版;package.json 补 "./api/*": "./src/api/*.ts" export、deps 删十项;tsconfig references 收缩。
  3. 建 webserver:目录+package.json(零 deps+tsconfigreferences []);src/index.tsstartWebServer:现 bin.ts 51-104 平移改造成 §④ 签名)、src/static.tsMIME+serveStatic)。
  4. apps/dsc 改造package.json deps 换列(dsc-web、dsh-host-webserver、dsh-host-runtime、dsh-host-apiproxy、dsh-app-boot、dsh-session);tsconfig referencesapp-boot、host/runtime、host/apiproxy、host/webserver、core/session、vendor/cordis);src 拆三文件(§⑤)。
  5. 验证pnpm install → §⑩ 验收逐条。
  6. 不动api/ fetch/ 内容、client 三包、根四配置(packages/*/* glob 已覆盖两新包)。

⑧ Electron / acp 接缝(只写约定,不实现)

Electron(用户已澄清口径)

将来 Electron = apps/electron 自己的拼装(⓪-1 混合归 apps);进程模型 sidecarQ3):spawn 独立 host 进程,其入口 bin 复用 startHost——RunningHost 形状即 sidecar 入口的全部所需(api/handler/dispose)。

承载 约定
renderer HTML/静态资源 file:// 或自定义协议加载 dist,不走 web server webserver 包 Electron 不复用(§① 定位);dist 解析知识在 apps/electron 自理
fetch 载体 IPC 桥(第三种承载:HTTP / 进程内注入 / IPC 桥) renderer 侧 AbstractApiClient 的 IPC 子类(doFetch=IPC 序列化往返);契约类型 /api type-only import
host 侧终点 sidecar 进程内 host.handler.fetch IPC 桥 main 侧收到序列化 Request → 转 sidecar(或同进程直调)→ Response 序列化回
停机 app.on('before-quit') → sidecar dispose dispose 幂等保证多触发安全

留待 Electron 轮设计(本轮只标注载体位):IPC 桥版 fetchLike 的 Request/Response 序列化边界、SSE 流在 IPC 上的对等物(如 MessagePort 流式推送)。判据不变:以上皆为「fetchLike 的伪造方式」,apiproxy/hostruntime 零新接口。

acp(用户前瞻,⓪-2 第二类消费的第一个实例)

  • dsc acp = apps/dsc 又一混合拼装模块(acp.ts),不新建包:动线 = startHost()ctx.plugin(uiAcp, config)(复用 packages/ui/acp 前门插件)→ 编辑器拥有生命周期(照 acp-demo:正常运行无信号处理)。
  • ACP 不过 fetch 模型(双向 JSON-RPC/权限回路/编辑器生命周期,与四象限不同构)——走 RunningHost.ctx 正式接缝,不是绕 Q1apiproxy 是投影消费面,ACP 是 core 前门,两类东西(⓪-2)。
  • stdout 纪律已由 §② 保证:现装配零 stdout 写手;将来日志走 StartHostOptions 可关。
  • 开放问题(标注不展开):ACP 与 web 可否同 host 并跑(同一进程既 ctx.plugin(uiAcp) 又 startWebServer)——事件扇出与审批路由的多壳仲裁没想清,留 acp 轮。

⑨ 妥协台账(三段式:妥协 → 触发条件 → 返工点/预埋)

  1. ctx 在 RunningHost 上同时服务两类消费(前门挂载=正式接缝;headless 事件订阅=本可走契约面 mux 流,§⑥ 实际就走的 mux——ctx 对 headless 纯备胎)。触发:消费型 client 出现绕 api 摸 ctx 的用法。返工点:ctx 文档注释收紧为「仅前门挂载」;预埋=§② 注释纪律已写。
  2. startHost 无配置面boot 全透传;handler 装配无参数;无 quiet/logSink——现装配零 stdout 写手,无可关之物)。触发:zod dev 开关/请求日志/审批 provider/acp 要 logSink。返工点:StartHostOptions additiveRunningHost 形状不动。
  3. -p 无信号处理Ctrl-C 走 Node 默认死)。触发:headless 长任务要优雅中断(130+半途结果)。返工点:headless.ts 加 SIGINT → api.sessions.cancel + 打印已聚 text,契约面能力已够,纯 additive。
  4. -p 每次新建 session(无 --resume)。触发:要接续会话。返工点:parseArgs 加 --resume create 换 list+校验。
  5. webserver 的 onError 回调形(listen 后错误经回调而非事件/AbortSignal)。触发:壳需要区分错误类别或多监听者。返工点:换 EventEmitter 或 signal 形——现单壳单错误出口,回调最小。
  6. 新包零测试(GUI 期门禁豁免)。触发:首个 tagged release 前门禁回收。返工点:webserver 静态语义单测(403/SPA/mime+ startHost 三形态冒烟 + -p e2eecho 模型)。
  7. apiproxy type-only 上游仍在 deps。触发:apiproxy 发布给外部 client(浏览器包不该拉 harness)。返工点:类型下沉或 peer 化——归 apiproxy-design 裁量。
  8. 存量包名未含目录前缀 已消解2026-07-20 02:0x):host/client 目录前缀命名规则落地时,存量三包(apiproxy→dsh-host-apiproxy、web-runtime→dsh-client-web-runtime、web-ui→dsh-client-web-ui)与两新包在同一窗口一次性改毕(package.json name+全部 import+tsconfig paths+pnpm install),未再留债。留档原因:改名期间与在途工作并发撞车过一次(两处独立加 paths 条目),结论=将来再有全仓 rename 一律走冻结窗口(暂停其他工作一次改完)。

⑩ 验收清单(实现完成后逐条)

# 命令 期望
1 pnpm install 退出 0hostruntime/webserver 软链出现
2 pnpm run demo:web 后 step1 验收 37 抽测 打印行/GET / 200/assets mime/403(编码变体)/SPA 回退全部与拆包前一致
3 web 起着时 RPC 面板/左栏 session 列表 step2 现状不回归(/api 桥经 webserver 后行为不变)
4 node --import tsx apps/dsc/src/bin.ts -p "Reply with exactly: SPLIT-OK" stdout 尾行 SPLIT-OK,退出码 0;期间 ss -ltn 无 3080 监听
5 -pls .sessions/cwd-*/ 新增 session jsonlheadless 会话已持久化)
6 node --import tsx apps/dsc/src/bin.ts usage 两命令,退出码 1
7 grep -rn "impl/" packages/host/apiproxy/src 空;pnpm run typecheck 范围内 client 三包不动即绿 摘除干净、契约面零影响
8 SIGTERM/SIGINT 对 demo:web 0 / 130shutdown 先 server.close 后 host.dispose

⑪ 属地与 review 交接

  • §③ 动 apiproxy 属地(impl 迁出/index 重写/exports 补行/deps 收缩)——交 apiproxy-design reviewTODO(step2) 注释群随 api-proxy.ts 迁入 hostruntime,其补全工作(W2)落点随之改变,先后顺序 team-lead 排。
  • webserver 平移的 bin.ts 51-104 是 step2 W3 产出——纯平移不改行为,W3 无需 review,但迁移期间 W3 若有在途改动需协调。

v1 → v2 变更记录

  • 新增 webserver 包Q5):v1 静态服务留 apps/dsc static.ts → v2 独立 host/webserver 包(含 /api 桥与 close 语义),dsc 的 web.ts 变纯拼装。
  • -p 属地定案(Q4+分层补钉):混合体(host boot + client 消费)→ apps/dsc 内部模块,不建包、不做 ui/ 谱系论证。
  • ctx 升格:逃生舱 → 正式接缝(acp 前门挂载点);连带 stdout 纪律入 §②。
  • Electron 口径更换v1「protocol.handle 挂 handler.fetch、SSE 同码路」→ v2 用户澄清版(HTML 走 file://、fetch 走 IPC 桥、webserver 不复用、sidecar 进程模型)。
  • 总纲新增(⓪):能力支持方分层原则、两类消费边界(消费型 client vs 协议桥前门)、apiproxy 前置层/hostruntime 后置装配层世界观、profile 归属 hostruntime。
  • 妥协台账 6→7 条(新增 webserver onError 回调形);验收 6→8 条(新增 step2 UI 不回归、SIGTERM/SIGINT)。