news 2026/9/5 18:12:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
deepseek-harness 中 dsh 的 Node 原生 TypeScript 源码启动链路:设计、门禁与演进

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承担,存在两个隐性耦合:

  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 只覆盖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 负责"。其解析规则:

  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

  • 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)明确了上游同步义务。

七、被否决的四个备选方案

笔记 "曾考虑的替代方案" 一节给出了完整的取舍记录,是理解整套设计约束的好材料:

备选方案否决理由
继续使用tsxtsx/esbuild 会继续负责 TypeScript 转换,本链路无法证明 Node 原生转换可用——这正是一次启动向量改造要达成的验证目标
源码入口通过包导出加载构建后的lib/混淆 source plane 与 artifact plane;零构建的开发启动可能读到陈旧产物或直接失败
无条件应用根 tsconfigpaths未声明的跨包 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 钩子同时负责转换与 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 源码"的项目都有参考价值:

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

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 18:12:16

GitHub中文排行榜完全指南:发现高分优秀中文项目的终极攻略

GitHub中文排行榜完全指南&#xff1a;发现高分优秀中文项目的终极攻略 GitHub中文排行榜是开发者发现高分优秀中文项目的重要平台&#xff0c;它帮助开发者更高效地吸收国人的优秀经验成果。无论你是编程新手还是有经验的开发者&#xff0c;都能在这里找到适合自己的项目进行学…

作者头像 李华
网站建设 2026/9/5 18:10:46

有源电力滤波器DSOGI-PLL:从正负序分离到参数整定

有源电力滤波器专题做到下半&#xff0c;DSOGI-PLL的核心问题已经不是“能不能锁相”&#xff0c;而是“在电网不平衡、畸变和频率偏移同时出现时&#xff0c;锁出来的角度还能不能稳定地用于指令提取”。常规 SRF-PLL 在电压不平衡时&#xff0c;dq 轴上会出现二倍频振荡&…

作者头像 李华
网站建设 2026/9/5 18:05:04

从“钢铁之胸”看游戏装备系统的完整开发链路

项目表里出现了一个配置项&#xff0c;名字很好念&#xff1a;“钢铁之胸”。策划在文档里写了几行&#xff1a;部位胸甲&#xff0c;品质紫&#xff0c;定位重甲防护&#xff0c;顺手补一句“这名字一听就很硬&#xff0c;玩家能记住”。运营那边也点头&#xff0c;说这个名字…

作者头像 李华
网站建设 2026/9/5 18:04:13

STM32 HAL库驱动I2C OLED:从点亮到稳定交付的完整指南

接到类似“把这块OLED点亮&#xff0c;显示几个参数”的任务时&#xff0c;很多人第一反应是去搜索现成代码&#xff0c;看到标题里有“OLED驱动”就直接进工程复制。但我见过太多人最后不是卡在编译上&#xff0c;而是卡在一句“屏幕怎么不亮”上。尤其是用STM32的HAL库驱动一…

作者头像 李华
网站建设 2026/9/5 17:59:07

SpringBoot3+Vue3+MySQL智能课程学习系统全栈实战

这次我们来看一个完整的全栈实战项目&#xff1a;智能课程学习系统。技术栈就是标题里写的那一套&#xff0c;Java 后端 SpringBoot3 Vue.js3 前端 MySQL 数据库。不做单体演示项目&#xff0c;而是按真实业务场景拆解功能模块、数据库设计和接口开发&#xff0c;适合正在准…

作者头像 李华