deepseek-harness 中 dsh 的 Node 原生 TypeScript 源码启动链路:设计、门禁与演进
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
本篇技术文章解读 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 只覆盖dshCLI 应用链路。CI 的lib模式、测试/E2E 启动器和其他示例启动器均不受影响,避免一个启动向量改动波及整个仓库的验证矩阵。
适用前提值得强调:根package.json声明"engines": { "node": "^22.19.0 || >=24.0.0" }。--experimental-transform-types是 Node 22.18+/24+ 引入的实验性能力,这条链路天然依赖引擎版本支持——这一假设后来正是导致整个方案被取代的原因(见第六节)。
三、tspath-loader:只注册一个 resolve 钩子的源码路径解析器
Node 原生启动后最大的缺口是 tsconfigpaths失效。仓库的解法是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;零构建的开发启动可能读到陈旧产物或直接失败 |
无条件应用根 tsconfigpaths | 未声明的跨包 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 矩阵——值得所有依赖引擎实验特性的项目对照。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考