- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
导读
DeepSeek Harness 是一个「万物皆插件」的多包仓库(monorepo),其代码门禁经常需要判断 TypeScript 语法本身并不携带的事实——某个调用接收者是不是 CordisContext、哪些具体事件名会流入转发辅助函数、声明合并是否改变了事件签名。本篇文章基于仓库中已落地(Status: implemented)的实现方案,讲解 DeepSeek Harness 如何用ts.Program汇集项目级类型信息、借助TypeChecker提取强类型事实,替换掉命名约定、手写表格与 JSDoc 元数据,为事件关系图(docs/event-producer-consumer.md)与 scoped 事件路由解析表(packages/core/scope/src/scoped-events.generated.ts)两套门禁提供单一语义真源。读完你既能复现这两个生成器的运行方式,也能掌握在大型 TS monorepo 中做「跨文件语义检查」的通用方法。
背景:语法解析门禁的先天局限
仓库门禁在演进中遇到了三类语法层面不可见的事实:
- 接收者身份:某次调用到底是不是在向 Cordis
Context、AgentEventDispatch或EventsService发事件; - 事件名集合:哪些具体事件名会经由转发辅助函数进入
EventsService.dispatch(); - 声明合并:
declare module '@deepseek-ai/cordis'中的合并是否会改变事件签名。
此前的门禁基于 TypeScript 单文件语法解析,用命名约定、手写的表格、JSDoc 注解来维护这些信息。问题在于:这类「第二份表示」与源码极易失同步——重命名事件、新增辅助函数形态时,手写表格必须同步更新,而完备性检查只能发现「生产方缺失」,无法证明某个覆盖项仍然与源码一致。
因此仓库需要一个语义真源,同时满足三条硬约束:
- 不引入运行时包之间的循环依赖;
- 不做宽泛的兜底启发式逻辑;
- 不复述 TypeScript 本已掌握的信息(即不做机器可读的元数据标注)。
核心决策:用 ts.Program + TypeChecker 提取强类型事实
仓库的决策是:门禁通过ts.Program汇集项目级类型信息,再用TypeChecker提取强类型事实,从而把对命名约定、手写表格和 JSDoc 的依赖降到最低。这一模型被应用到两个门禁上:
- 门禁 A:
gen-doc-graphs生成事件生产者/消费者关系矩阵(docs/event-producer-consumer.md); - 门禁 B:
gen-scoped-events生成 scoped 事件的路由解析函数表(packages/core/scope/src/scoped-events.generated.ts)。
一个项目模型展开根项目配置:TypeScriptProject
两个生成器共享同一个封装:scripts/ts-project.ts中的TypeScriptProject类。它做了三件事:
- 解析根 tsconfig 并递归展开项目引用:
loadProjectGraph()从根目录读取tsconfig.host.json(CompilerFace = 'host' | 'client'),对每个项目引用调用ts.resolveProjectReferencePath递归收集fileNames,把所有引用项目的源码根合并为一个不输出文件的语义 Program; - 清除仅发射选项:
semanticCompilerOptions()把noEmit设为true,同时关闭composite、declaration、declarationMap、sourceMap、incremental,得到一个纯语义视图; - 统一暴露配置诊断、语义编译选项、仓库相对路径、源码查找和共享 checker:
sourceFiles()、relativePath()、sourceFile()三个方法与共享的program/checker属性,让各门禁不再自行按文件通配模式扫描包源码,也不再各自构建不完整的 Program。
// scripts/ts-project.ts(节选) const graph = loadProjectGraph(projectRoot, face) this.program = ts.createProgram(graph.rootNames, semanticCompilerOptions(graph.options)) this.checker = this.program.getTypeChecker()两点设计细节值得注意:
- 不直接以根 solution 配置建 Program:源码注释明确指出,「flattening host+client into one program collides the cordis Context merges」——直接把根配置转成普通 Program,TypeScript 可能把引用项目重定向到构建后的
.d.ts声明文件,且 host/client 两面的Context合并会冲突。显式展开能保留各包src文件供 AST 遍历,并保持符号同一性; - 配置诊断即失败:
parseConfig()对每个 tsconfig 调用ts.getParsedCommandLineOfConfigFile,一旦出现任何配置诊断立刻抛错。语义门禁因此天然依赖一个有效的根项目图。
门禁 A:事件关系由接收者类型与值类型决定
gen-doc-graphs.ts的核心是EventRelationCollector类。它不读变量名,而是用isTypeAssignableTo做接收者分类:
// scripts/gen-doc-graphs.ts(节选) if (this.project.checker.isTypeAssignableTo(type, this.eventsServiceType)) return 'events-service' if (this.project.checker.isTypeAssignableTo(type, this.contextType)) return 'context' if (this.project.checker.isTypeAssignableTo(type, this.agentDispatchType)) return 'agent-dispatch'三个锚点类型都从仓库真实声明解析而来(vendor/cordis/src/context.ts的Context、packages/core/agent/src/dispatch.ts的AgentEventDispatch、vendor/cordis/src/events.ts的EventsService),而不是手写的白名单。
有限事件名集合的恢复路径
- Context 与 agent-dispatch 调用只贡献由字符串字面量构成的有限事件集合:
finiteStringValues()只接受字面量、闭合的字符串字面量联合类型,凡被拓宽成string或保持泛型的一律拒绝;AgentEventDispatch转发对象内的上下文参数通过isForwardedAgentEventParameter()被显式排除——泛型转发参数不算生产方,事件归属始终回到「传入封闭事件值」的调用点; - 直接
EventsService.dispatch()调用:eventNamesFromArgumentList()会沿数组字面量、const常量别名、条件分支逐级恢复事件槽位,并进一步通过未导出本地辅助函数的已解析调用点回溯参数; - 调用点预过滤:
EVENT_API_METHODS集合(on、once、emit、parallel、serial、waterfall、dispatch)先行过滤,只有方法名命中后才做接收者类型分类,避免对每个调用都求解签名。
需求式辅助函数索引:证明只影响开销,不影响结果
这是实现中最精巧的部分。callSitesFor()对每个辅助函数先尝试provenLocalCallee():如果一个函数未导出、位于真正的 ES 模块文件、且同文件所有引用都是直接调用位,那么按模块作用域规则,它的全部调用必然在本文件内——此时只索引这一个文件。任一前提无法证明(带导出修饰符、位于全局 script 文件、存在别名化引用如 re-export 或默认导出、存在无法归类的引用)就回退到原全部包源码索引。
// scripts/gen-doc-graphs.ts(节选) if (!this.globalCallSites && !this.provenLocalCallee(owner)) { this.globalCallSites = this.buildCallSiteIndex(this.packageSourceFiles) } if (this.globalCallSites) return this.globalCallSites.get(owner) ?? [] // 仅索引 owner 所在文件回退路径就是原来的语义本身,因此证明失败只会增加扫描成本,绝不改变结果。文档同时记录了被否决的方案:惰性单一全局索引被放弃,因为当前源码树确实会走到辅助函数参数路径,它仍要支付几乎全额的getResolvedSignature扫描成本。
完备性契约
renderEventRelations()生成的矩阵要求每个已声明的 harness 事件都存在扫描得到的生产方:
- 找不到生产方 → 直接抛错,视为「死词汇」或尚不支持的语义 dispatch 形态(
docs/event-producer-consumer.md的生成失败信息会明确提示); - 没有监听方的扩展点仍然合法(listener-free extension points remain valid);
internal/dispatch插桩不会被当作它观察的每个事件的订阅,关系矩阵只记录直接的产品监听方;- 客户端声明的事件(
packages/client/前缀)豁免生产方检查,因为关系扫描只以 host 聚合为种子(见源码 TODO 注释:host+client 不能共享一个 Program,Client 包仅在 host 文件 import 它时才进入)。
门禁 B:带作用域的事件路由生成强类型解析函数表
gen-scoped-events.ts负责dsh-scope的运行时不变式。它的输入契约是两段真实代码:
- 真实的
scopeTarget(base, key)调用(定义在packages/core/scope/src/index.ts)——为每种 scoped 基础对象确立路由键类型; - 带
this: Scoped<Base>的 CordisEvents成员(通过isCordisModuleInterface()限定在declare module '@deepseek-ai/cordis'内部)。
生成器对每个事件成员执行三步:
- 解析路由键类型:
routingKeyType()收集所有 base 类型可赋值给该 scoped base 的scopeTarget调用,去重后若出现多个不同的键类型,报「inconsistent routing-key types」; - 搜索 payload 候选:
subjectCandidates()枚举每个事件参数及其第一层公开属性(剔除__@内部符号与 private/protected 声明),用typesEquivalent()做精确类型同一性比较(先getNonNullableType()移除null/undefined,拒绝any/unknown); - 三分支裁决:
- 恰好一个匹配→ 生成解析函数,如
'agent/created': args => (args[0] as Record<string, unknown>)['agent']; - 多个匹配→ 含义不明确,收集 violation 并失败;
- 零匹配→ 事件必须标记
@dshScopeScan unsupported。
- 恰好一个匹配→ 生成解析函数,如
@dshScopeScan unsupported只用于路由键有意留在事件参数之外的场景,例如按所属 agent 路由的会话事件(session/created、session/event)和按父 agent 路由的 subagent 生命周期事件(subagent/start、subagent/end)。该标记只表达「扫描不受支持」,不编码事件名、参数下标、属性路径或替代类型——生成器对非unsupported形态的 tag、多余 tag、以及「有 tag 但并非 scoped 事件」等组合都会逐一报错。
生成的scoped-events.generated.ts是纯运行时映射:Object.freeze冻结的Record<string, ScopedSubjectResolver | null>加上一个scopedSubjectResolverFor(event)查询函数。它位于 scoped dispatch 所属的dsh-scope包内,不 import 任何事件声明方包——null解析器表示「payload 无法暴露外部路由键,不变式只检查载体存在性」,undefined表示「该事件不是 scope-filtered 事件」。
// packages/core/scope/src/scoped-events.generated.ts(节选) export function scopedSubjectResolverFor(event: string): ScopedSubjectResolver | null | undefined { return scopedSubjectResolvers[event] }消费端packages/core/scope/src/invariant.ts直接 import 这份映射做主体提取,不再维护手写事件表。因为 Program 分析发生在仓库门禁内而非依赖生成的类型导入,dsh-scope与dsh-invariants都无需依赖所有事件声明方——这正是文档强调的「没有循环依赖」约束的落地。
语义缺口必须显式失败
两个生成器对语义缺口一律失败快、失败明。触发拒绝的情况包括:
| 类别 | 示例 |
|---|---|
| 声明缺失 | 无法解析Context、EventsService等锚点类型 |
| 配置诊断 | 任意 tsconfig 出现解析错误 |
| 事件名被拓宽或保持泛型 | finiteStringValues()返回undefined |
| 路由键类型不一致 | 同一 scoped base 出现多个键类型 |
| 事件参数匹配不唯一 | 多个 payload 候选与键类型同一 |
| 不必要的 unsupported 标记 | payload 明明暴露了路由键却标注不支持 |
| 生成产物陈旧 | --check模式下与已提交文件不一致 |
设计上,通过本地辅助函数调用点恢复信息的能力被刻意限制在窄范围:如果数据流经过导出或无法解析的边界,正确的做法是新增一条通用语义规则,而不是添加特定包的覆盖项。
验证与门禁集成
两个生成器都提供「生成」与「校验」双模式,见根package.json的 scripts:
pnpm run gen-doc-graphs # 生成文档图(含事件关系矩阵) pnpm run verify-doc-graphs # 对语义生产方/监听方扫描做新鲜度检查 pnpm run gen-scoped-events # 生成 scoped 事件解析表 pnpm run verify-scoped-events # 重跑 Program 分析并校验生成映射新鲜度在生成器内部,--check模式会把渲染结果与已提交文件逐字比对(gen-doc-graphs.ts的main()中committed !== doc.content即判陈旧;gen-scoped-events.ts同理)。此外两套校验都被挂进统一门禁编排器scripts/run-gates.ts,与文档构建、类型等价、Cordis 目录、翻译配对等检查并行执行:
pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), // ... pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),配套验证还包括:根 TypeScript 构建会编译运行时适配器(scoped-events.generated.ts随dsh-scope包一起编译);workspace 约束与运行时依赖闭包检查确保事件声明方聚合不会进入部署依赖。
考虑过的替代方案
仓库明确评估并否决了「保留语法扫描 + 接收者白名单 + 手写覆盖项」的方案:
每个例外都容易单独处理,但重命名和新增辅助函数形态时还必须更新第二份表示。完整性检查能够发现生产方缺失,却无法证明覆盖项仍与源码一致。
这正是语义方案的核心优势:生成器自己就是完整性的证明——根 Program 枚举所有 scopedEvents声明与真实scopeTarget约定,用 checker 解析唯一 payload 路径,并在渲染unknown[]运行时边界之前拒绝缺失、陈旧或含义不明确的条目。
后果与权衡
- 正向收益:事件关系生成依据语义接收者身份和封闭事件值,不再依赖局部命名约定;scoped 事件成员关系、主体提取和运行时不变式覆盖来自事件声明与真实 dispatch 约定,不再来自手写表;修改事件名、参数位置、主体属性或路由键类型时,会在其所属约定处触发生成失败——重构错误在提交前就被拦截;
- 成本:构建扁平化 Program 比解析孤立文件消耗更多启动时间和内存;语义门禁依赖有效的根项目图,任何 tsconfig 破损都会让校验直接失败;
- 提交约束:生成的 TypeScript 仍属于提交到仓库的源码。事件声明方或 dispatch 形态发生变化后,必须重新运行
pnpm run gen-scoped-events与pnpm run gen-doc-graphs并提交受影响文件和文档——新鲜度检查(verify-*)正是为此设计。
这套模式给出了一条可迁移的路径:当语法无法表达的事实需要被门禁约束时,与其手写第二份表示,不如构建一次不输出的语义 Program,让 TypeChecker 替你回答。代价是更长的启动时间与对项目图完整性的依赖,换来的是重命名、签名变更、声明合并等场景下的即时、精确且可证明的失败。
- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
相关推荐
DeepSeek Harness 的 TypeScript Program 语义化检查门:用 `ts.Program` + `TypeChecker` 取代命名约定与手写表格
DeepSeek Harness 的 TypeScript Program 语义化检查门:用 ts.Program + TypeChecker 取代命名约定与手
人工智能AI AgentAgent 框架DeepSeekDeepSeek Harness 持久化日志事件目录:基于 TypeScript AST 生成 docs/persistence-catalog.md 与新鲜度门禁
DeepSeek Harness 持久化日志事件目录:基于 TypeScript AST 生成 docs/persistence catalog.md 与新鲜度
人工智能AI AgentAgent 框架DeepSeekty 类型检查器中的 TypeVar 下标与切片语义解析(基于 ruff 仓库 mdtest 测试套件)
ty 类型检查器中的 TypeVar 下标与切片语义解析(基于 ruff 仓库 mdtest 测试套件) 导读 本文以 ruff 仓库内 ty 类型检查器的 M
开发工具Lint格式化静态分析CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考