7.4 KiB
Agent Note:pack 期 lowering 与单构建 preview
状态:已实施
English | 中文
问题
浏览器 worker 既不能在装载期编译模块,也不能由产品 webserver 提供页面:每个模块体必须以可直接运行的形态到达,页面必须是静态产物。两个面早期都发生过漂移。装载器曾携带一个兜底编译器,于是收集器的缺口表现为「启动变慢」而不是「镜像坏了」——而且 acorn 经包 barrel 混进了 lib/worker.js,一个只包装预 lowered 模块体的运行时根本不需要解析器。preview 曾是服务页面旁的第二份 HTML 模板,一个 served index 可以悄悄漂离的页面。
决定
Lowering 只发生在 pack 期。 @deepseek-ai/dsh-experimental-webworker-packer 组合 profile、物化闭包、lower 每个 JavaScript 模块体;LOWERING_VERSION 与 WRAPPER_PARAMS 是 pack↔worker 的契约,与镜像布局的其余部分一起放在 src/image-layout.ts。装载器完全按镜像持有的形态包装模块体:仍带模块语法的模块体是一次点名镜像的拒绝,且 startWorkerHost 在挂载任何模块之前要求 manifest 的 lowered 等于本构建的契约。lowerModuleSource 是转换器唯一的面、packer 是它唯一的调用方;worker 图内部的 import 一律指向拥有该值的模块——绝不指向包 barrel,那正是把解析器偷运进来的那条边。源码目录排除只用于运行期使用已构建 lib/ 的 workspace 与 vendored 包;已安装第三方包会保留 src/ 和 dist/ 下的 JavaScript,因为其发布入口可能解析到这些位置。
preview 就是服务页面加一个标签。 一次 Vite 构建产出共享全部 chunk 的 dist/index.html 与 dist/preview.html;唯一差异是前插的一个引导入口,其模块负责连接 worker host。启动随之汇于一个协议:应用注入表的一方 settle __DSH_BOOT_READY__ deferred——served 渲染器在渲染完的行之后用尾部脚本 resolve,worker 引导段在首个 await 之前安装、末行生效后 settle——client 入口在读取任何注入状态前 await 它,因此从标准入口起的链路逐字就是 served 链路。插件 combo 脚本与 map 都通过 tunnel;页面侧 loader 会在执行脚本 Blob 前,把每个仅 tunnel 可达的 map 内嵌为 Base64 data URL,从而不依赖另一条 object URL 的生命周期,并在 DevTools 中保留 indexed map 的组件名称。构建使用相对 base,产物可挂载于任意静态目录;served 形态在 serve 期渲染 <base href="/"> 锚定深层 SPA fallback 路径,磁盘上的两个页面保持字节共享。
仓库 preview 携带可选择的文件系统来源。 Packer 产出一份基础镜像,并为每套具名内置 fixture 产出一份小型 overlay 归档。没有来源 query 时,preview.html 会停在选择面板,可选择空文件系统、内置 fixtures,或归另一实现所有的 WebFS provider。合法的 preview-fixture=none|<built-in-id> query 会直接选择并跳过面板,供确定性的浏览器流程使用;该独立名称避开 Client 既有的 fixture transport 开关。Worker 先挂载基础镜像,再按顺序把所选 overlays 应用到仅限 home/ 和 workspace/ 的路径,随后才校验基础 manifest 并启动 Cordis。packages/experimental/webworker-runtime/tests/fixtures/vfs-example/ 提供其中一套内置 overlay,Packer 无需理解 Session 或 Workspace。明文 JSONL 日志使用 persistence backend 的真实 project/session 目录布局,因此 Session Persistence 会冷读取它们,Workspace Registry 则根据其 /dsh/workspace header 派生 Workspace。主 Session 超过 Client 的 50-message page,并把代表性工具结果留在尾页;持久化的 one-shot 与 continuable child 用于验证 subagent catalog。WebFS 授权与用户数据仍属于独立 provider,绝不与该 fixture 共用目录。
两个包以 @deepseek-ai/dsh-experimental-* 名义放在 packages/experimental/,私有且在官方发布之外。承载产品承诺的边界仍在产品包里:注入表、__DSH_TRANSPORT__ 与 /plugins bundle 字节由 dsh-host-webserver、dsh-client-modules、dsh-client-connection 拥有。
曾考虑的替代方案
保留装载期转换器作安全网。 它把坏镜像变成无人归因的耗时回归,并且让「这个模块体是谁 lower 的」从外部不可回答。
契约常量留在转换器里,信任 tree shaking。 转换函数确实被摇掉了,但 acorn 未声明 sideEffects,仅 barrel 一条边就把整个解析器带进了 worker bundle。
独立的 preview 模板。 已退役的 preview.html 模板复制了服务文档并发生漂移(语言、标题、入口接线)。在 closeBundle 从 built index 派生页面则彻底消灭了第二份文档。
用顶层 await 顺序而非 deferred 去闸标准入口。 兄弟 module script 互不等待对方的顶层 await;??= 安装的 deferred 使握手与求值顺序无关,且失败的握手能 reject 进 boot 页的失败呈现。
在 Worker 启动时生成示例状态。 Preview 专用的 Session 或 Workspace 创建分支会绕过冷 persistence 读取,还会让 runtime 拥有测试数据。静态镜像文件与既有用户数据经过相同的发现和分页路径。
通过 WebFS 注入示例。 WebFS 拥有用户选定的 durable storage 及其生命周期。让内置演示依赖它,会使静态 preview 受浏览器持久化状态影响,并模糊哪些字节来自部署。
每套 fixture 各打一份完整基础镜像。 完整镜像变体会重复 runtime package closure,并使组合数量平方增长。受限 overlay 只保留一个不可变基础镜像,选择面板可组合零到多个数据源,未来 WebFS 水合也能复用同一个 pre-boot 应用点。
后果
lib/worker.js不含解析器(当刀落时为 423.5 kB → 246.3 kB,早于 shell 进程层落地)。diff dist/index.html dist/preview.html恰为一个 script 标签;packages/experimental/webworker-packer/tests/image-loadable.spec.ts钉住装载器契约的两半,apps/web/tests/preview-boot.e2e.ts在 web 浏览器车道钉住 preview 可用性(boot 到可交互页面),替代已撤编的apps/web/scripts/preview/探针脚本。- 转换 corpus 会先通过 Node 导入每个已构建 bundle,再比较 lowered export。固定豁免会点名真正不可导入的 bundle,并在其恢复可导入时失败:
win32-process是 Koffi 类型 owner 并承担重复类型豁免;sandbox-windows-acl可正常导入,不承担该豁免。 - served 的
<base href="/">锚存在的原因是:相对资产 URL 在 SPA fallback 深路径下会解析进请求目录;只有与相对构建 base 一起才可移除它。 - 镜像以确定性 gzip 压缩的 tar 交付(
vfs-image.tar.gz;MTIME 0、OS 字节 0xff):静态托管不压缩二进制 content-type(类型白名单、CDN 尺寸帽),压缩必须随制品走;worker 用浏览器原生DecompressionStream在下载的同时解压 fetch body。 - Preview 会停在 pre-boot 来源选择面板。内置示例提供可复现的 Workspace 与冷 Session 语料,无凭据、零模型调用即可检查工具卡、subagent 导航和向前分页;空白选项保留首次启动覆盖。Fixture 测试通过生产 reader 校验物理日志,浏览器验收同时验证两种选择。