deepseek-harness 中 dsh 的 Node 原生 TypeScript 源码启动链路:设计、门禁与演进
本篇技术文章解读 deepseek-harness(下称 dsh,Everything is a Plugin 的 Agent 运行时)仓库中一份关键的架构决策笔记:dsh 的 TUI、Web 与无头模式如何在不经过 tsx/esbuild、不预构建 lib/ 产物的前提下,用 Node 原生能力直接启动 TypeScript 源码。读完后你会掌握:node --experimental-transform-types 启动链路的设计动机与边界、只做 resolve 钩子的 tspath-loader 的解析规则、verify-cordis-config 静态门禁与 app-boot 的 fail-loud 插件诊断如何防止"退出码 0 的残缺应用",以及该方案被 Node 26 移除特性取代后仓库的演进路径。
一、背景:为什么必须重建源码启动链路
dsh 是 monorepo 形态:apps/cli 是 CLI 应用入口(dsh 命令),packages/ 下是一两百个以 Cordis 插件为单元的 workspace 包,vendor/ 内嵌 Cordis、Loader、Include、HMR、Schemastery 等框架源码。开发时希望 pnpm dsh 一类的命令能零构建直接跑源码,这条链路原本由 tsx 承担,存在两个隐性耦合:
- TypeScript 转换与路径解析都由同一个第三方 loader 隐式处理。
tsx同时负责把.ts变成可执行 JS,并应用根tsconfig.json的paths映射,把 workspace 裸包名(如@deepseek-ai/dsh-session)解析到.ts源文件。这个能力是"顺带"的,没有显式契约。 - 改用 Node 原生处理 TypeScript 后,这条隐式契约断裂。 Node 的 transform-types 模式不会应用 tsconfig 路径映射;如果退而通过包
exports解析,源码启动就会混入可能陈旧甚至不存在的lib/构建产物——"构建好的开发树能启动、干净 checkout 反而失败"正是这类问题的典型症状。
文档还指出了 Node 转换层的两个语义坑(.agents/notes/archived/architecture/2026-07-28-dsh-native-typescript-source-launch.zh.md "问题"一节):
- Node 的转换不做类型分析。 通过普通值 import 导入的类型会保留为运行时 ESM 请求(即类型导入会在运行时真的去解析模块),而 TypeScript 的
export =会被转成 CommonJS 赋值而不是 ESM default export。因此源码图必须显式使用仅类型导入(import type)和原生 ESM 导出,且 resolve hook 无法修复不兼容的源码语法——契约只能写在源码里。 - Cordis 配置引入了第二条解析边界。
cordis.yml中的 bare 插件包不经过 TypeScript import 分析,其解析方 manifest(package.json)可能漏掉所需依赖。Cordis Loader 对插件 import 失败只是记录日志、留下一个"没有 fiber 的 entry",不会让启动本身失败——配置里一个拼写错误就能产出退出码 0 的残缺应用。
二、核心决策:node --experimental-transform-types 统一启动链路
决策部分(笔记 "决策"一节)规定:
dsh的 TUI、Web 与无头源码启动统一使用node --experimental-transform-types,由 Node 完成 TypeScript 转换,启动链路上不加载tsx或 esbuild。bin/dsh、根级dsh/TUI/Web demo 以及 Code Mode TUI 全部进入同一条apps/cli/src/bin.ts启动链路,避免多入口分叉。- 测试与 e2e 启动器保留各自现有策略;构建后的
lib/bin.js继续由普通 Node 运行(这一点可从apps/cli/package.json得到印证:"bin": { "dsh": "lib/bin.js" },发布产物与源码启动是两个平面)。
这里有一个刻意的范围约束:该原生源码 loader 只覆盖 dsh CLI 应用链路。CI 的 lib 模式、测试/E2E 启动器和其他示例启动器均不受影响,避免一个启动向量改动波及整个仓库的验证矩阵。
适用前提值得强调:根 package.json 声明 "engines": { "node": "^22.19.0 || >=24.0.0" }。--experimental-transform-types 是 Node 22.18+/24+ 引入的实验性能力,这条链路天然依赖引擎版本支持——这一假设后来正是导致整个方案被取代的原因(见第六节)。
三、tspath-loader:只注册一个 resolve 钩子的源码路径解析器
Node 原生启动后最大的缺口是 tsconfig paths 失效。仓库的解法是 scripts/tspath-loader.ts(该文件已随后续演进删除,此处按决策笔记还原其设计)——它只注册一个模块解析(resolve)钩子,不做任何代码转换,"代码转换始终只由 Node 负责"。其解析规则:
- 配置文件选择:设置了
TSX_TSCONFIG_PATH环境变量时使用该路径(相对路径从调用方的 cwd 解析),否则读取根tsconfig.json。 - 沿
extends链解析:TsconfigPathsResolver复用仓库已有的 TypeScript 开发工具沿配置的extends链读取,按 tsconfig 规则选择精确(exact)或 wildcard 的paths条目。这与根tsconfig.json的注释相呼应——该 solution 文件刻意保持files: []且通过extends携带 base paths,正是为了让"从仓库根启动、没有就近 tsconfig"的脚本(当时是 tsx 启动的 scripts/)也能解析 workspace import。 - 命中即映射到源文件:命中的 workspace bare specifier 被映射到
.ts/.mts/.cts源文件或目录 index 文件。 - 未命中一律回退:未命中 tsconfig paths、引用未声明依赖、或根本不是 bare specifier 的说明符,全部交回 Node 默认解析。
两条设计边界值得注意:
- 该 loader 不属于构建后的 CLI。 它是"源码专用"的,使用 checkout 根目录的开发依赖,
apps/cli/package.json的dependencies中没有typescript(typescript只出现在根级开发依赖中),保证发布产物的运行时依赖面不被开发期能力污染。 - 钩子只管 URL,不碰源码。 这与"在 loader 内转换 import"的备选方案直接对撞(见第四节):感知类型的源码改写会让 loader 重新变成事实上的 TypeScript 编译器。
四、"最近一层 manifest 持有依赖":运行时声明门禁
paths 映射是无条件的,但源码启动不应无条件:如果 tsconfig paths 兜底一切,未声明的跨包 import 和 Cordis 插件将继续成功解析,manifest 与实际运行图之间的不一致就被永久掩盖。
因此源码 import 的重定向受一条显式规则约束:只有当目标包是"最近一层包 manifest 的自身名称"或"该 manifest 已声明的运行时依赖"时,才允许重定向到 workspace 源码。 规则的两个关键场景:
- 插件源码内的 import:解析方包的
package.json自身声明了这个依赖,才允许从lib/兜底中改道到.ts源文件。 - Cordis 插件的解析 parent:Cordis Loader 使用配置目录的 URL 作为 import parent;resolver 此时向上查找声明该插件的 workspace manifest。于是随附的
apps/cli/config/base.cordis.yml(及其界面覆盖层)所需依赖,由apps/cli/package.json持有——可以从该文件的dependencies清单验证:@deepseek-ai/dsh-app-boot、@deepseek-ai/dsh-tool-bash、@deepseek-ai/cordis-plugin-loader等插件包都以workspace:^形式显式列出,与配置行一一对应。
这条运行时规则确实抓到过真实缺陷:取代它的后续决策笔记(.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md)记录了 dsh-plan-mode 与 dsh-tool-jobs 导入 @deepseek-ai/dsh-llm 却只声明在 devDependencies 的问题,后已修复。运行时强制不是摆设。
五、静态门禁与 fail-loud 诊断:配置与启动不再"静默残缺"
运行时规则管住"源码 import"一侧,另一侧靠两道静态/应用层门禁补齐:
1. verify-cordis-config 的单向完整性检查。 配置中的每个 bare plugin 包都必须出现在对应 manifest 的 dependencies 中,反向不要求(manifest 可以包含该配置未引用的额外依赖,多出不算错)。当前仓库中该门禁已扩展为更完整的 Loader 元数据与包解析校验器 scripts/verify-cordis-config.ts:
missingPluginDependencies(scripts/verify-cordis-config.ts)实现上述单向检查:收集所有name行引用的包名,凡不在依赖面(dependencies,测试配置可含devDependencies)中即报错... must be declared in <owner>;validateSourcePlaneResolution(scripts/verify-cordis-config.ts)进一步保证每个本地 workspace 包都能通过 tsconfig.base.json 的 paths 解析到.ts/.tsx源文件——注释明确说明:没有 paths 命中就会回退到包exports抵达构建lib/,"在构建过的开发树能启动、在干净 checkout 上炸掉";- 同一脚本还覆盖 bundle patch 行、包级 Loader fixture、目录选择器(chooser)后端包等扩展面。
根 AGENTS.md 把"同步更新配置和依赖"定为常驻规则:改配置必须同时改 manifest,门禁防止配置先于依赖落地。
2. app-boot 的 fail-loud 插件诊断。 Loader 完全停稳后,共享的 dsh-app-boot 检查每个已启用但没有 fiber 的 entry 并拒绝启动,报错为:
plugin(s) failed to load: ...; Cordis startup failed because these plugin(s) could not be resolved
同时列出全部加载失败的插件。该诊断位于应用层(packages/boot/app-boot/src/index.ts 中可见此错误字符串),不改变 vendor 中 Loader 自身的启动行为——Loader 依旧只是记录错误并留下空 entry,"让失败显形"的责任上移到应用启动层。结果是:插件 import 失败不再留下退出码 0 的残缺应用,最终错误同时说明 Cordis 启动失败原因与具体插件名,Loader 的原始错误仍保留在更早的日志中。
六、Node 兼容 TypeScript 契约:vendor 源码的显式标注
决策笔记把"Node-compatible TypeScript"列为源码启动契约的一部分,落到 vendor 源码上的具体约定:
- Cordis、Loader、Include、HMR、Schemastery 对会被擦除的类型导入统一使用
import type标记——避免 Node transform 把类型当作运行时导出去请求。 - Schemastery 源码使用原生 ESM default export 并声明
type: module;其.mjs与.cjs构建产物分别保留现有的 ESM default export 行为和require()返回可调用值的行为。 - 这些 vendor 与上游的差异记录在
vendor/README.md中(其中第 10 条即"Vendored Node-compatible TypeScript",与笔记一一对应),且没有为 vendor 框架新增任何运行时行为——只是把既有行为对齐到 Node 的模块语义。 - 后果面上:CLI 源码图中的 vendor 源码必须持续兼容 Node transform-types 的模块语义;vendor 的"本地修改记录"(local-modification log)明确了上游同步义务。
七、被否决的四个备选方案
笔记 "曾考虑的替代方案" 一节给出了完整的取舍记录,是理解整套设计约束的好材料:
| 备选方案 | 否决理由 |
|---|---|
继续使用 tsx |
tsx/esbuild 会继续负责 TypeScript 转换,本链路无法证明 Node 原生转换可用——这正是一次启动向量改造要达成的验证目标 |
源码入口通过包导出加载构建后的 lib/ |
混淆 source plane 与 artifact plane;零构建的开发启动可能读到陈旧产物或直接失败 |
无条件应用根 tsconfig paths |
未声明的跨包 import 和 Cordis 插件继续成功解析,掩盖 manifest 与实际运行图之间的不一致 |
| 在自定义 loader 内转换 import | 感知类型的源码改写重新引入编译器式转换,让 loader 而非 Node 负责执行 TypeScript;使签入仓库的源码兼容 Node,才能让启动边界保持显式 |
四个否决项共同指向同一个原则:转换归 Node,解析归显式规则,声明归 manifest,诊断归应用层——每一层各管一段,边界可静态验证。
八、后果与后续演进:从"原生链"到 tsx ESM 钩子
决策笔记 "后果" 一节的五条结论:
- TUI/无头界面保留零构建源码回路;Web 仍在启动 CLI 源码入口前构建前端产物。TypeScript 语法只经过 Node 原生转换;仅处理 URL 的 loader 使用 checkout 根目录的开发依赖,不增加 CLI 运行时依赖。
- workspace package import 和 Cordis 配置依赖都必须在解析方 manifest 中明确声明;静态门禁防止配置先于依赖落地,额外依赖不构成错误。
- 插件 import 失败不再留下退出码 0 的残缺应用;最终错误同时说明 Cordis 启动失败及具体插件名。
- CLI 源码图中的 vendor 源码必须与 Node transform-types 模块语义兼容;本地修改记录明确上游同步义务。
- CI 的
lib模式、测试/E2E 启动器和其他示例启动器保留各自现有策略。
重要演进提示(截至当前仓库状态):该笔记状态为 implemented 且已归档(Archived: 2026-08-07)。笔记开头即声明:Node 26.0.0 移除了 --experimental-transform-types(进程以 bad option 拒绝该 flag),本方案描述的 paths loader(scripts/tspath-loader.ts、apps/cli/src/tsconfig-paths-loader.ts)已被删除,dsh 源码启动改由 dsh 通过 tsx ESM hook 源码启动 的决策接管:
- 新启动向量为
node --import tsx/esm,由 tsx 的 ESM-only 钩子同时负责转换与 tsconfigpaths投影(CLI 源码图是纯 ESM,故 CJS 钩子保持关闭); - 随之消失的是 tspath-loader 的运行时依赖声明强制,声明完整性仅由静态门禁保障(配置的裸插件走
verify-cordis-config,manifest 走 workspace constraints); - 仓库新增
dsh-source-launch-smoke门禁(apps/cli/tests/source-launch.compat.spec.ts,在 Node 22.19 与 26 的 node-compat CI 矩阵中强制执行):用精确的生产运行时启动向量做 keyless 管道 stdio 启动,断言进程会因 TTY 拒绝而以非零状态退出——未来 Node 对模块钩子或 TypeScript 处理的任何改动会让该门禁变红,而不是悄悄破坏开发者的启动体验。
而本笔记确立的三块资产仍然有效:verify-cordis-config 配置声明门禁、app-boot 的显式失败插件诊断、以及 vendor 中的 import type 标注。换言之,Node 原生转换这条"路"被换掉了,但"配置必须声明、失败必须响亮、源码必须显式类型导入"这套契约经受住了版本更替,成为 dsh 源码启动可持续演进的底盘。
九、可复用的工程结论
把这份 ADR 的机制抽象出来,对任何"Node 上直接跑 TypeScript monorepo 源码"的项目都有参考价值:
- 启动向量的显式化:与其让某个 loader 隐式兜底转换+解析,不如把转换(Node 或 tsx)、路径解析(tsconfig
paths投影)、依赖声明(manifest)分成三个显式可验证的环节,每个环节有独立的静态门禁。 - 单向完整性检查是低成本高收益的门禁形态:配置引用的包必须在 manifest 中声明,manifest 多出不报错——既挡住"配置先于依赖落地",又不惩罚合理的依赖预留。
- fail-loud 诊断放在应用启动层而非改动底层 Loader:底层保持"记录错误、不中断"的宽容语义,由
app-boot在停稳后统一裁决,错误信息聚合全部失败项。 - 实验性 flag 的依赖要可观测:本方案的终结不是代码腐化,而是 Node 26 移除一个引擎 flag 且"没有任何 CI 任务执行过真实启动向量"。仓库的补救——按生产启动向量做冒烟断言、纳入多版本 CI 矩阵——值得所有依赖引擎实验特性的项目对照。
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