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
25 KiB
hostruntime 拆包 + dsc 双命令 · 实现级设计(v2)
2026-07-20 v2:按用户五问裁决(Q1–Q5)+ 分层原则补钉 + Electron 载体澄清 + acp 前瞻整体重写。v1→v2 变更记录见文末。 读者:编码 teammate + apiproxy-design(review §③)。现状基线:step2 后工作树(apps/dsc/src/bin.ts 122 行、apiproxy 三层 api/+fetch/+impl/+index.ts 内 bootHost)。 范围红线:Electron/acp 不做(各留接缝一节);api/ 与 fetch/ 内容零改动;GUI 期不遵循仓库门禁。
⓪ 包结构总纲(用户裁决,全文档的宪法)
- 分层原则:
packages/host/*与packages/client/*按「能力支持方」分层——host/ 包只提供 host 侧能力,client/ 包只提供 client 侧能力,每包单边不混;多种支持方的混合一律放apps/(哪个 app 要混,拼装写在那个 app 里)。 - 消费面唯一经 ApiProxy(Q1,精确化):所有消费型 client(web / Electron / headless)走 apiproxy,不同接入只是「fetch 形函数的伪造方式不一样」(HTTP / 进程内注入 / IPC 桥)。协议桥前门(ACP 这类把 core 暴露给外部生态的)不属消费型 client——直接挂 core ctx,不套 fetch。两类东西,不是例外。
- apiproxy = 前置层:契约 api/ + 载体 fetch/,做简单,所有接入方都要(现状已是,只做摘除)。
- hostruntime = 后置装配层 / 应用实体:配哪些插件、装哪些东西的装配入口;host 级配置的归属地——defaults、persistenceRoot,将来的用户 profile(~/.dsc 一族)也归这里。
- 每个接入方 = 自己一个拼装包/拼装模块:web 形态 =
host/webserver包(HTTP+静态+SSE 桥)+client/web-runtime(已有);headless = apps/dsc 内部模块(混合体不建包,见 ⓪-1);Electron 将来 =apps/electron自己拼装。 - 进程模型(Q3):Electron 走 sidecar(spawn 独立 host 进程);本轮只保证 startHost 返回形状可被 sidecar 入口 bin 复用,不实现。
- 命名规则(用户定死):
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 两拼装模块
│ deps:dsc-web(dist 解析,web 用)· dsh-host-webserver(web 用)
│ 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 |
装配层/应用实体:bootHost(core spine 组合)+ createApiProxy + startHost;host 级配置归属地(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 两包 |
不变 | 无 |
② startHost(hostruntime 的启动接缝,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 {
/** 透传 bootHost(BootHostOptions 全量:persistenceRoot 必填 + provider?/model?)。将来 profile/日志开关在此 additive。 */
boot: BootHostOptions
}
export interface RunningHost {
/** 契约实现(进程内消费者直调;IPC 适配层的输入)。 */
api: ApiProxy
/** WHATWG fetch 形载体(web 壳桥到 node:http;Electron 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-console,stdout 留给纯 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/rpc;TODO(step2) 注释群随文件走 |
src/index.ts 的 bootHost 一族 |
迁 hostruntime src/boot.ts |
BootHostOptions/HostDefaults/HostHandle/bootHost 原样平移 |
src/index.ts re-export 段 |
改写 | 退化版见下 |
apiproxy 退化后出口
src/index.ts:re-export api/index.ts 全部 + toFetchHandler + AbstractApiClient/InProcessApiClient/IApiClient(迁移当时为 createApiClient;893421d50 换类体系),别无他物。package.json exports 保留 . / ./api / ./client / ./src/* / ./package.json,新增 "./api/*": "./src/api/*.ts"(hostruntime 迁入文件要 import ../api/rpc.ts 的对等物;比 ./src/api/... 深路径干净)。deps 收缩为:dsh-brand、dsh-llm、dsh-session、dsh-user-approval、dsh-user-interaction(api/ 的 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-apiproxy;tsconfig references = core 十包 + vendor(cordis/timer) + apiproxy。src 四文件:boot.ts(迁入)、api-proxy.ts(迁入)、start.ts(§②)、index.ts(barrel:bootHost 一族 + 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 + closeAllConnections(SSE 长连接强制断,防 close 挂死)。幂等。 */
close(): Promise<void>
}
/**
* 起 web 形态 HTTP server:listen(port, '0.0.0.0')。
* 路由三段:/api/* → apiHandler 桥(node:http↔WHATWG,req close→abort,SSE 逐 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.ts(startWebServer + 桥)+src/static.ts(MIME 表 + serveStatic 纯函数——现 bin.ts 5 段整体平移)。 /api/*桥、静态逻辑、403/SPA 语义逐行平移现 bin.ts 51–95,行为零改动(step1/step2 验收锁定)。listen包 Promise:listening事件 resolve、首个error事件 reject;resolve 后的 error 转 onError(现 bin.ts 101–104 的 disposeAndExit(1) 语义由壳在 onError 里做)。close():server.close()+server.closeAllConnections()包 Promise(close 回调 resolve),??=幂等。- 不打印:
dsc web: http://127.0.0.1:<port>打印行归壳(web.ts)——sidecar/测试复用本包时不带 dsc 词汇。
package.json:@deepseek-ai/dsh-host-webserver,形状照 hostruntime(lib 入口 + ./src/*),dependencies 空对象省略。tsconfig:references 为空数组(零依赖),其余同形。
⑤ 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 收 --port,headless 收 -p/--prompt),bin 层不聚合 options。
web.ts 动线(相对现 bin.ts 的重排)
parseArgs --port(默认 3080,非法 stderr+exit 1;positional 已被 bin 层剥掉)
→ const host = await startHost({ boot: { persistenceRoot: './.sessions' } })
→ resolveDist:createRequire(import.meta.url).resolve('@deepseek-ai/dsc-web/dist/index.html')
catch → stderr「先跑 pnpm --filter @deepseek-ai/dsc-web build」→ exit 1(dist 定位知识留在 dsc,§④ 结论)
→ const server = await startWebServer({ port, distIndex, apiHandler: host.handler },
err => { stderr; void shutdown(1) })
listen reject(EADDRINUSE)→ 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→130(jsonrpc-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?: number;text='';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.kind;return {text, reason}
payload.type==='stream/error' 或 for-await throw → stderr + return {text, reason:'error'}
边界结论:
- 先开 mux 后 prompt:帧不丢;同进程无竞态仍保持此序——换远程 HTTP 时代码零改(同构纪律)。
- mint =
RpcId(randomUUID());unwrap = RpcResponse 拆封,!result.ok打 stderr(error.code: error.message)+ dispose + exit 1。 - 退出码:completed→0,其余(aborted/error)→1;boot 失败(缺 key)顶层 rejection fail-loud 非零。
- Ctrl-C 走 Node 默认(无 server 可关;持久化由 core turn 边界 flush 保证)——台账 §⑨-3。
- 审批/问答:现装配无审批 provider 不会挂等;将来加装配走 StartHostOptions additive——台账 §⑨-2。
⑦ 迁移步骤(顺序执行,每步 typecheck 可绿)
- 建 hostruntime:目录+package.json+tsconfig;src/boot.ts(apiproxy/src/index.ts:1-52 平移)、src/api-proxy.ts(impl/api-proxy.ts 平移,import 改
@deepseek-ai/dsh-host-apiproxy/api与/api/rpc)、src/start.ts(§② 新写)、src/index.ts(barrel)。 - apiproxy 摘除:删 src/impl/;index.ts 重写退化版;package.json 补
"./api/*": "./src/api/*.ts"export、deps 删十项;tsconfig references 收缩。 - 建 webserver:目录+package.json(零 deps)+tsconfig(references []);src/index.ts(startWebServer:现 bin.ts 51-104 平移改造成 §④ 签名)、src/static.ts(MIME+serveStatic)。
- apps/dsc 改造:package.json deps 换列(dsc-web、dsh-host-webserver、dsh-host-runtime、dsh-host-apiproxy、dsh-app-boot、dsh-session);tsconfig references(app-boot、host/runtime、host/apiproxy、host/webserver、core/session、vendor/cordis);src 拆三文件(§⑤)。
- 验证:pnpm install → §⑩ 验收逐条。
- 不动:api/ fetch/ 内容、client 三包、根四配置(
packages/*/*glob 已覆盖两新包)。
⑧ Electron / acp 接缝(只写约定,不实现)
Electron(用户已澄清口径)
将来 Electron = apps/electron 自己的拼装(⓪-1 混合归 apps);进程模型 sidecar(Q3):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 正式接缝,不是绕 Q1:apiproxy 是投影消费面,ACP 是 core 前门,两类东西(⓪-2)。
- stdout 纪律已由 §② 保证:现装配零 stdout 写手;将来日志走 StartHostOptions 可关。
- 开放问题(标注不展开):ACP 与 web 可否同 host 并跑(同一进程既 ctx.plugin(uiAcp) 又 startWebServer)——事件扇出与审批路由的多壳仲裁没想清,留 acp 轮。
⑨ 妥协台账(三段式:妥协 → 触发条件 → 返工点/预埋)
- ctx 在 RunningHost 上同时服务两类消费(前门挂载=正式接缝;headless 事件订阅=本可走契约面 mux 流,§⑥ 实际就走的 mux——ctx 对 headless 纯备胎)。触发:消费型 client 出现绕 api 摸 ctx 的用法。返工点:ctx 文档注释收紧为「仅前门挂载」;预埋=§② 注释纪律已写。
- startHost 无配置面(boot 全透传;handler 装配无参数;无 quiet/logSink——现装配零 stdout 写手,无可关之物)。触发:zod dev 开关/请求日志/审批 provider/acp 要 logSink。返工点:StartHostOptions additive,RunningHost 形状不动。
- -p 无信号处理(Ctrl-C 走 Node 默认死)。触发:headless 长任务要优雅中断(130+半途结果)。返工点:headless.ts 加 SIGINT → api.sessions.cancel + 打印已聚 text,契约面能力已够,纯 additive。
- -p 每次新建 session(无 --resume)。触发:要接续会话。返工点:parseArgs 加 --resume ,create 换 list+校验。
- webserver 的 onError 回调形(listen 后错误经回调而非事件/AbortSignal)。触发:壳需要区分错误类别或多监听者。返工点:换 EventEmitter 或 signal 形——现单壳单错误出口,回调最小。
- 新包零测试(GUI 期门禁豁免)。触发:首个 tagged release 前门禁回收。返工点:webserver 静态语义单测(403/SPA/mime)+ startHost 三形态冒烟 + -p e2e(echo 模型)。
- apiproxy type-only 上游仍在 deps。触发:apiproxy 发布给外部 client(浏览器包不该拉 harness)。返工点:类型下沉或 peer 化——归 apiproxy-design 裁量。
存量包名未含目录前缀已消解(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 |
退出 0;hostruntime/webserver 软链出现 |
| 2 | pnpm run demo:web 后 step1 验收 3–7 抽测 |
打印行/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 | -p 后 ls .sessions/cwd-*/ |
新增 session jsonl(headless 会话已持久化) |
| 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 / 130(shutdown 先 server.close 后 host.dispose) |
⑪ 属地与 review 交接
- §③ 动 apiproxy 属地(impl 迁出/index 重写/exports 补行/deps 收缩)——交 apiproxy-design review;TODO(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)。