DeepSeek Harness 修复解析:`dsh web` 的插件客户端 Bundle 从何而来,以及 Web 演示为何必须先完整构建
本篇技术指南围绕 DeepSeek Harness(dsh)仓库中一篇已归档的 bug 修复记录展开:Web UI 的插件客户端 bundle(lib/client.js)只在根级完整构建时产出,仅执行 Vite 前端构建会导致 dsh web 下所有插件端点 404 并显示 "Failed to load plugins"。读完本文,你将掌握 dsh Web 的两层构建模型(前端 shell 与插件 bundle)、各构建脚本的产物边界,以及"演示入口必须先完整构建"这一修复背后的权衡与验证方法。
背景:dsh Web 的两层构建模型
dsh web 启动的 Web UI 实际上由两类独立产物组成,二者由不同的构建链路生成,这是理解本次 bug 修复的前提:
- 前端 shell(编译壳):
apps/web(包名@deepseek-ai/dsh-web-frontend)用 Vite 构建出dist/,由dsh web直接静态服务。其 package.json 的自述为 "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web"。 - 插件客户端 bundle:
dsh web通过GET /plugins/<id>/client.js逐个服务每个 web-client 插件的 bundle,路径从各插件包的exports["./client"](即lib/client.js)解析。这些 bundle 只能由根级pnpm run build产出——先tsc -b类型编译,再由各包的tsdown.client.ts配置打包。
根 package.json 中的构建脚本清晰体现了这两层分工:
| 脚本 | 命令 | 产物 |
|---|---|---|
build |
tsx scripts/build.ts |
完整仓库构建(lib 产物 + 前端 shell) |
build:lib:host |
node --max-old-space-size=4096 tsc -b tsconfig.host.json && tsdown --env.DSH_BUILD_FACE host |
Host 面(Node 侧)lib 产物 |
build:lib:client |
tsc -b tsconfig.client.json && tsdown --env.DSH_BUILD_FACE client |
Client 面(浏览器侧)lib 产物,含各插件 lib/client.js |
build:web |
pnpm --filter @deepseek-ai/dsh-web-frontend run build |
仅 Vite 前端 shell 的 apps/web/dist |
完整构建的编排逻辑见 scripts/build.ts:它先 runScript('build:lib') 再 runScript('build:web'),随后通过 writeClientBuildRecord 记录客户端产物文件数与公开环境值("build: recorded N client artifact(s) with M public value(s)")。
Client 面打包的关键配置在根 tsdown.config.ts:配置通过环境变量 DSH_BUILD_FACE 区分两个面——host 面以 lib/types/{index,invariant,startup}.js 为入口(tsc -b 的产物)并挂载 Typert 插件;client 面则改为空入口 entry: '',"selects packages that declare a browser bundle and lets their package-local configs emit both their Node loader entry and browser artifact"(选择声明了浏览器 bundle 的包,让包级配置同时产出 Node loader 入口与浏览器产物)。UI 插件 bundle 的共享打包预设见 packages/client/tsdown.client.ts:它产出闭包工厂(closure-factory)产物——bundle 执行时调用 window.__ModuleLoader__.load({id, factory}),外部依赖通过注入的 require(loader 模块表)解析,CSS 由 lightningcss 在 bundle 内编译并注入。这也解释了为什么浏览器侧加载器能独立工作:每个插件 bundle 是自包含的工厂函数,不依赖 import map 或全局变量。
问题:干净检出下的 "Failed to load plugins"
原始缺陷记录(.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.md,状态 implemented,归档于 2026-07-26)描述的问题链条如下:
- 插件 bundle(
lib/client.js)只由根pnpm run build产出;Vite 的build:web只构建前端 shell。 - 当时的
demo:web演示入口以及 README 的 Web UI 说明只运行了build:web。 - 因此在一个没有先执行过完整构建的检出目录中,
dsh web服务的每个GET /plugins/<id>/client.js全部 404,客户端 loader 将每个插件标记为 failed,启动屏显示 "Failed to load plugins"。 - 更具迷惑性的是:前端 shell 本身构建完全正常,缺失的产物被掩盖成一个运行时浏览器错误,而不是构建失败——问题不会在构建阶段暴露,只会出现在用户打开浏览器的那一刻。
修复:演示入口先执行完整构建
修复决策同样记录在该 note 中,核心是保证服务前产物存在:
demo:web在运行npm run build:web之前先运行npm run build,使各插件的lib/client.jsbundle 在dsh web服务它们之前已经存在。- README 的 Web UI 章节对安装器落盘到
~/.dsh/source的检出(安装过程从不构建它)给出pnpm run build && pnpm run build:web的说明。
当前仓库的 README.md "Run from source" 一节即体现了修复后的最终形态:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
README 明确指出 "pnpm run build prepares the repository artifacts. pnpm dsh web uses those built artifacts without rebuilding."(dsh web 直接使用已构建产物,不再重新构建)——这正对应了 note 中"服务方不拥有构建步骤"的原则。
验证方式(见 note 的 Verification 一节):完整构建后,全部 8 个 /plugins/<id>/client.js 端点均返回 200;用无头 Chromium 加载 http://127.0.0.1:3080,shell 正常渲染且不再出现 "Failed to load plugins" 状态。
备选方案及其被否决的原因
该 note 记录了两个被否决的替代方案,其否决理由体现了项目的架构约束,值得展开:
方案一:在 dsh web 启动时构建 bundle。 被否。应用本身通过 tsx 从源码运行,不拥有任何构建步骤;把产物构建塞进服务器启动流程会跨越源码/产物(source/artifact)的分界线,并且拖慢每一次启动。这也是 README 中 "dsh web uses those built artifacts without rebuilding" 一句的由来——服务方只消费产物,不生产产物。
方案二:扩展 tsdown 根配置,让 build:web 顺带产出客户端 bundle。 被否。build:web 的语义是 Vite 前端构建;客户端 bundle 是对 lib/types(tsc -b tsconfig.client.json 的产物)的一次独立的 tsdown 打包,见 tsdown.config.ts 中 client 面的入口定义与 scripts/build.ts 中 build:lib 先于 build:web 的执行顺序。合并两者会把 shell 构建与包构建混为一谈,而且根级 build 仍然是唯一的产物生产者,扩展 build:web 并不能消除这条依赖链,只会让脚本语义更模糊。
代价与后续影响:全量构建的成本由演示入口承担
note 的 Consequences 一节给出了明确的取舍:demo:web 现在每次调用都要支付完整 tsc -b && tsdown 的成本,而不是只跑 Vite 构建。这是"从干净目录即可运行 Web 演示"必须付出的代价;已经构建过的调用方可以直接调用 dsh web,无需重复付费。从当前根 package.json 的脚本清单看,演示相关入口为 demo:ptc、demo:inspector(web --patch ... 形式)与 dev:web;该 note 已归档,README 面向读者的标准流程则是 pnpm run build 后 pnpm dsh web。
对日常开发场景,仓库提供了增量替代路径 scripts/dev-web.ts。其文件头注释完整描述了三层增量重建循环:"rebuilds every artifact the browser reads from a source edit"——tsc -b tsconfig.client.json 产出 lib/types,tsdown 打包 lib/index.js 与 lib/client.js,vite build 重写 apps/web/dist,而 dsh web 服务的宿主 web server 会 stat 轮询这些 bundle 并自行广播 rebuilt 帧。注释同时给出两条重要约束:该脚本必须先执行过一次 pnpm run build(每层都是对上一层输出的增量编译,没有任何一层能自建缺失的目录树),且不能与 pnpm run build 并发运行(两者写同一棵 lib/ 与 apps/web/dist/ 树);另外在 inotify 事件不可达的网络挂载上需使用 --poll 轮询模式。
小结与核对清单
这篇归档修复的核心经验可以归纳为三点:
- 识别产物边界:dsh Web 的"shell 正常、插件全 404"症状,本质是 Vite shell 构建与 tsdown 插件 bundle 构建两条链路被单一入口(只跑
build:web)错误地裁剪。核对插件端点应直接请求GET /plugins/<id>/client.js并确认返回 200。 - 服务方不构建:
dsh web从源码(tsx)运行但不拥有构建步骤,修复方向是让构建发生在演示入口/文档指引中,而不是塞进服务启动流程,保持源码与产物的分界。 - 验证要落到运行态:修复验证不止于端点 200,还包括无头浏览器加载
http://127.0.0.1:3080后 shell 渲染且无 "Failed to load plugins" 状态。
关键文件索引:
- 原始修复记录:.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.md(附 中文版)
- 构建编排:scripts/build.ts、tsdown.config.ts、根 package.json
- 插件 bundle 打包预设:packages/client/tsdown.client.ts
- 前端 shell 构建:apps/web/package.json、scripts/dev-web.ts
- 面向读者的运行流程:README.md "Run from source" 一节
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00