首页
/ deepseek-harness 中 dsh 的 Node 原生 TypeScript 源码启动链路:设计、门禁与演进

deepseek-harness 中 dsh 的 Node 原生 TypeScript 源码启动链路:设计、门禁与演进

2026-09-04 17:21:36作者:滕妙奇

本篇技术文章解读 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 承担,存在两个隐性耦合:

  1. TypeScript 转换与路径解析都由同一个第三方 loader 隐式处理。 tsx 同时负责把 .ts 变成可执行 JS,并应用根 tsconfig.jsonpaths 映射,把 workspace 裸包名(如 @deepseek-ai/dsh-session)解析到 .ts 源文件。这个能力是"顺带"的,没有显式契约。
  2. 改用 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 统一启动链路

决策部分(笔记 "决策"一节)规定:

  • dshTUI、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 负责"。其解析规则:

  1. 配置文件选择:设置了 TSX_TSCONFIG_PATH 环境变量时使用该路径(相对路径从调用方的 cwd 解析),否则读取根 tsconfig.json
  2. 沿 extends 链解析TsconfigPathsResolver 复用仓库已有的 TypeScript 开发工具沿配置的 extends 链读取,按 tsconfig 规则选择精确(exact)或 wildcard 的 paths 条目。这与根 tsconfig.json 的注释相呼应——该 solution 文件刻意保持 files: [] 且通过 extends 携带 base paths,正是为了让"从仓库根启动、没有就近 tsconfig"的脚本(当时是 tsx 启动的 scripts/)也能解析 workspace import。
  3. 命中即映射到源文件:命中的 workspace bare specifier 被映射到 .ts/.mts/.cts 源文件或目录 index 文件。
  4. 未命中一律回退:未命中 tsconfig paths、引用未声明依赖、或根本不是 bare specifier 的说明符,全部交回 Node 默认解析。

两条设计边界值得注意:

  • 该 loader 不属于构建后的 CLI。 它是"源码专用"的,使用 checkout 根目录的开发依赖,apps/cli/package.jsondependencies没有 typescripttypescript 只出现在根级开发依赖中),保证发布产物的运行时依赖面不被开发期能力污染。
  • 钩子只管 URL,不碰源码。 这与"在 loader 内转换 import"的备选方案直接对撞(见第四节):感知类型的源码改写会让 loader 重新变成事实上的 TypeScript 编译器。

四、"最近一层 manifest 持有依赖":运行时声明门禁

paths 映射是无条件的,但源码启动不应无条件:如果 tsconfig paths 兜底一切,未声明的跨包 import 和 Cordis 插件将继续成功解析,manifest 与实际运行图之间的不一致就被永久掩盖。

因此源码 import 的重定向受一条显式规则约束:只有当目标包是"最近一层包 manifest 的自身名称"或"该 manifest 已声明的运行时依赖"时,才允许重定向到 workspace 源码。 规则的两个关键场景:

  1. 插件源码内的 import:解析方包的 package.json 自身声明了这个依赖,才允许从 lib/ 兜底中改道到 .ts 源文件。
  2. 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-modedsh-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

  • missingPluginDependenciesscripts/verify-cordis-config.ts)实现上述单向检查:收集所有 name 行引用的包名,凡不在依赖面(dependencies,测试配置可含 devDependencies)中即报错 ... must be declared in <owner>
  • validateSourcePlaneResolutionscripts/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 钩子

决策笔记 "后果" 一节的五条结论:

  1. TUI/无头界面保留零构建源码回路;Web 仍在启动 CLI 源码入口前构建前端产物。TypeScript 语法只经过 Node 原生转换;仅处理 URL 的 loader 使用 checkout 根目录的开发依赖,不增加 CLI 运行时依赖。
  2. workspace package import 和 Cordis 配置依赖都必须在解析方 manifest 中明确声明;静态门禁防止配置先于依赖落地,额外依赖不构成错误。
  3. 插件 import 失败不再留下退出码 0 的残缺应用;最终错误同时说明 Cordis 启动失败及具体插件名。
  4. CLI 源码图中的 vendor 源码必须与 Node transform-types 模块语义兼容;本地修改记录明确上游同步义务。
  5. CI 的 lib 模式、测试/E2E 启动器和其他示例启动器保留各自现有策略。

重要演进提示(截至当前仓库状态):该笔记状态为 implemented 且已归档(Archived: 2026-08-07)。笔记开头即声明:Node 26.0.0 移除了 --experimental-transform-types(进程以 bad option 拒绝该 flag),本方案描述的 paths loader(scripts/tspath-loader.tsapps/cli/src/tsconfig-paths-loader.ts)已被删除,dsh 源码启动改由 dsh 通过 tsx ESM hook 源码启动 的决策接管:

  • 新启动向量为 node --import tsx/esm,由 tsx 的 ESM-only 钩子同时负责转换与 tsconfig paths 投影(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 源码"的项目都有参考价值:

  1. 启动向量的显式化:与其让某个 loader 隐式兜底转换+解析,不如把转换(Node 或 tsx)、路径解析(tsconfig paths 投影)、依赖声明(manifest)分成三个显式可验证的环节,每个环节有独立的静态门禁。
  2. 单向完整性检查是低成本高收益的门禁形态:配置引用的包必须在 manifest 中声明,manifest 多出不报错——既挡住"配置先于依赖落地",又不惩罚合理的依赖预留。
  3. fail-loud 诊断放在应用启动层而非改动底层 Loader:底层保持"记录错误、不中断"的宽容语义,由 app-boot 在停稳后统一裁决,错误信息聚合全部失败项。
  4. 实验性 flag 的依赖要可观测:本方案的终结不是代码腐化,而是 Node 26 移除一个引擎 flag 且"没有任何 CI 任务执行过真实启动向量"。仓库的补救——按生产启动向量做冒烟断言、纳入多版本 CI 矩阵——值得所有依赖引擎实验特性的项目对照。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384