在前面几章中,我们学习了服务——通过继承 Service 基类,一个插件可以对外提供能力,供其他插件通过 inject 来消费。
但你可能注意到了一个问题:我们之前写的服务,提供方和消费方是紧耦合的。
比如,你写了一个 MetricsService,其他插件通过 inject = ['metrics'] 来使用它。如果有一天你想把监控后端从 MetricsService 换成另一个实现——比如换成 PrometheusService——所有依赖它的插件都得改代码。这显然不够灵活。
在实际的大型项目中,很多能力需要可替换的实现方。比如:
Bash 执行:在本地执行、在 Docker 容器中执行、在远程沙箱中执行。
文件系统操作:操作本地目录、操作远程存储、操作内存文件系统。
网络搜索:使用 Exa、Perplexity、DeepSeek 搜索等不同提供商。
用户可能想在不同的部署环境中切换不同的实现,而不需要修改任何消费方的代码。
Harness 通过三种角色设计模式 来解决这个问题:Service Definition(服务定义)、Service Provider(服务提供方)和 Consumer(消费方) 。
这一章,我们将以 Bash 执行能力为例,深入理解这三种角色如何协作,以及这种拆分带来的三大好处: 提供方可替换、独立演进和依赖解耦。
什么是Capability Seam?
在深入三种角色之前,先理解一个核心概念:Capability Seam。
在 Harness 中,一个完整的“能力”由三个角色共同构成。Service Definition 定义契约,Service Provider 实现契约,Consumer 将能力以模型可用的形式暴露出来。
单个角色不是seam,完整的三个角色才是。
这句话是什么意思呢?
Service Definition 本身只是“一张纸上的约定”。只有当它有了 Service Provider和 Consumer,它才真正成为一个“可替换的 Capability Seam”——你可以在不改变接口和使用方的情况下,随时换掉 Service Provider。
给初中级开发者的比喻:可以把 Capability Seam 想象成 USB 接口。USB 接口本身(Service Definition)只是一套标准;U 盘、鼠标、键盘(Service Provider)是不同的实现;电脑上的应用程序(Consumer)通过 USB 接口来使用这些设备,而完全不需要关心插进去的到底是什么。你可以随时把 U 盘换成鼠标,应用程序不受影响——这就是“可替换”的力量。
Bash 示例:三种角色各司其职
Harness 中的 Bash 执行能力就是三种角色设计的典型范例。
Service Definition——dsh-shell
Service Definition 是能力的“契约”。它定义了:
服务本身:继承 Service 基类,挂载到 ctx 上。
请求和结果的数据结构:调用方用什么格式发起请求,提供方用什么格式返回结果。
dsh-shell 包定义了 shell 服务,以及 Bash 执行的请求类型和结果类型。它不包含任何具体实现——它只是一份“说明书”。
Service Provider——dsh-bash-local
Service Provider 是能力的“具体实现”。它实现了 Service Definition 中声明的抽象方法。
dsh-bash-local 包在本地机器上执行 Bash 命令。如果换一个 Provider——比如 dsh-bash-sandbox——它可以在 Docker 容器或远程沙箱中执行命令,但对外暴露的接口完全一样。
Consumer——dsh-tool-bash
Consumer 是能力的“使用者”。它不直接调用 Provider,而是通过 Service Definition 定义的接口来使用能力。
dsh-tool-bash 包将 Bash 执行能力封装成一个 模型可调用的工具(Tool)。当模型调用这个工具时,Consumer 会通过 ctx.shell 服务来执行命令,并把结果返回给模型。
┌───────┐ ┌─────────┐ ┌────────┐
│ dsh-shell │───▶│ dsh-bash-local│ │ dsh-tool-bash │
│(definition) │ │ (provider) │ │(consumer/tool)│
└───────┘ └─────────┘ └────────┘
▲ │
└──────────────────────┘
inject: ['shell']给初中级开发者的理解:Service Definition 是“合同”,Service Provider 是“干活的人”,Consumer 是“把活儿包装成工具给模型用的人”。合同上写的是“我要执行一个 Bash 命令,你给我返回结果”——至于这个命令是在本地执行还是在云端执行,Consumer 不关心,Provider 来操心。
拆分的三大好处
把能力拆成三个角色,不是为了让代码更复杂——恰恰相反,是为了让系统更灵活、更健壮。
好处一:Service Provider 可替换
一个 Service Definition 可以有多个 Provider,通过 cordis.yml 来选择使用哪一个。
# 本地执行
- name: '@deepseek-ai/dsh-bash-local'
# 替换成沙箱执行——Service Definition 和 Consumer 完全不用改
- name: '@deepseek-ai/dsh-bash-sandbox'Service Definition 和工具保持不变,只有 Provider 变了。
这意味着什么?假设你开发了一个工具插件,它依赖 shell 服务来执行命令。在开发环境中,你用 dsh-bash-local 在本地跑;在生产环境中,你换成 dsh-bash-sandbox 在沙箱里跑——你的工具插件一行代码都不用改。
好处二:独立演进
三种角色可以各自独立迭代,互不影响:
Service Definition 很少变化——因为依赖它的 Provider 和 Consumer 都基于这个契约工作。
Service Provider 可以独立优化性能、增强安全性,只要它仍然遵守契约。
Consumer 可以独立调整“如何将能力呈现给模型”——比如改变工具的描述、参数的展示方式,而不影响 Provider 的执行逻辑。
好处三:依赖解耦
这是三种角色设计最核心的价值:
Service Provider 依赖 Service Definition(它要实现契约)。
Consumer 依赖 Service Definition(它要使用契约)。
Service Provider 和 Consumer 互不依赖。
Consumer 只知道“我需要 shell 这个能力”,而完全不知道这个能力是由哪个 Provider 提供的。Provider 也不知道有哪些 Consumer 在使用它。
这种“背靠背”的依赖关系,让系统的各个部分可以独立变化、独立部署、独立测试。
三步构建一个可替换的能力
理论讲完了,我们来动手实现一个完整的三层能力。我们会创建一个名为 myCap 的能力,功能很简单:接收一个字符串,返回它的大写形式。不过这里的重点在于架构本身,而不是功能的复杂度。下面是一份完整的入门教程(详见本文尾部),涵盖从代码编写到成功运行的每一个步骤,包括类型检查所需的 tsconfig 配置。
给初中级开发者的提示:这个示例虽然简单,但它包含了三层架构的所有核心要素。理解了这个模式,你就能把它应用到任何复杂的能力上。
设计要点:什么时候该用,什么时候不该用
三种角色设计很强大,但不是所有场景都需要它。
什么时候该用
能力需要多个可替换的实现方——比如 Bash 执行有本地、沙箱、远程等多种实现。
Provider 和 Consumer 需要独立演进——比如 Provider 的性能优化不应该影响 Consumer 的代码.
能力足够通用,可能被多个 Consumer 使用。
什么时候不该用
只是一个简单的工具插件——不需要对外提供服务,也不需要被替换。
能力只有一个固定的实现方——拆分三层只会增加复杂度,没有实际收益。
给初中级开发者的建议:“不要过早拆分”是一个重要的工程原则。在刚开始开发一个能力时,先用最简单的方式实现——一个函数形式的插件就足够了。当它确实需要被多个 Provider 实现、或者需要被多个 Consumer 使用时,再重构为三层架构。过度设计比设计不足更可怕。
Harness 中的内置Capability Seam
Harness 本身大量使用了三种角色设计模式。以下是一些内置的Capability Seam:
ctx 键 | 角色 | 说明 |
|---|---|---|
ctx.llm | seam | LLM 服务,可接入 DeepSeek、OpenAI 等多种 Provider |
ctx.shell | seam | Shell 执行,可切换本地、沙箱等 Provider |
ctx.sandbox | seam | 沙箱服务,可切换不同沙箱后端 |
ctx.attachments | seam | 附件服务,可切换不同存储后端 |
这些Capability Seam的完整列表和维护包链接,由capability-seams 参考页统一维护。
总结
本章我们学习了 Harness 中构建可替换能力体系的三种角色设计模式。核心收获如下:
角色 | 职责 | 示例 |
|---|---|---|
Service Definition | 定义契约:服务接口 + 请求/结果类型 | dsh-shell 定义 shell 服务 |
Service Provider | 实现契约:具体的能力执行逻辑 | dsh-bash-local 在本地执行命令 |
Consumer | 使用契约:将能力暴露给模型或用户 | dsh-tool-bash 封装为模型工具 |
三种角色之间的依赖关系:
Provider → Service Definition(Provider 依赖契约)。
Consumer → Service Definition(Consumer 依赖契约)。
Provider ↔ Consumer 互不依赖。
三大好处:
Provider可替换:换一个 Provider,Consumer 完全无感知。
独立演进:三种角色可以各自独立迭代。
依赖解耦:Provider 和 Consumer 互不依赖,系统更灵活。
何时使用:
能力需要多个可替换的Provider。
Provider 和 Consumer 需要独立演进。
简单工具插件不需要拆分。
到此,你应该已经掌握了 Harness 中最核心的架构设计模式。这种“契约-实现-使用”三层分离的思想,不仅是 Harness 的基石,也是构建任何可扩展软件系统的通用原则。
下一章,我们前往LLM 适配器,学习如何为 Harness 接入自定义的大语言模型提供方——这是 Capability Seam 模式在 LLM 领域的精彩应用。
完整示例:
能力:接收一个字符串,返回它的大写形式。
目录结构
最终会创建三个包,放在 packages/my-cap/ 下:
deepseek-harness/
├── packages/
│ └── my-cap/ ← 新建的 group 目录
│ ├── my-cap/ ← 第 1 层:Definition
│ │ ├── src/index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── my-cap-local/ ← 第 2 层:Provider
│ │ ├── src/index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── tool-my-cap/ ← 第 3 层:Consumer
│ ├── src/index.ts
│ ├── package.json
│ └── tsconfig.json
├── demo/
│ └── run-direct.ts ← 测试脚本
└── package.json ← 根 package.json(要改)第一步:写 Service Definition(my-cap)
1.1 创建 packages/my-cap/my-cap/src/index.ts
import {Service, type Context} from '@deepseek-ai/cordis';
// 类型声明:让 TypeScript 认识 ctx.myCap
declare module '@deepseek-ai/cordis' {
interface Context {
myCap: MyCapService
}
}
// 请求和结果的数据结构——由 Service Definition 拥有
export interface MyCapRequest {
input: string
}
export interface MyCapResult {
output: string
}
// 抽象服务类——定义契约,不包含实现
export abstract class MyCapService extends Service {
protected constructor(ctx: Context) {
super(ctx, 'myCap') // 'myCap' 是服务名
}
/** 执行能力——具体实现由 Provider 完成 */
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}解读
extends Service:Cordis 的 Service 是所有服务的基类。继承它后,这个类就能被注册到 ctx 上。
super(ctx, myCap):第二个参数 'myCap' 是服务名。注册后,ctx.myCap 就能访问到这个服务的实例。
declare module '@deepseek-ai/cordis':这是 TypeScript 的"模块扩展"语法。它告诉编译器:"Context 接口上多了一个 myCap 属性"。没有这段,ctx.myCap 会报类型错误。
abstract:抽象类不能直接实例化,只定义"有什么方法",不定义"怎么实现"。具体实现交给第 2 层。
1.2 创建 packages/my-cap/my-cap/package.json
{
"name": "@deepseek-ai/dsh-my-cap",
"description": "String-to-uppercase capability definition for the DeepSeek Harness practice project",
"version": "0.1.0-rc.5",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/my-cap/my-cap"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": { "types": "./lib/types/index.d.ts", "default": "./src/index.ts" },
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": ["lib/index.js", "lib/types/**/*.d.ts"],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^"
}
}解读:
"name": "@deepseek-ai/dsh-my-cap":包名。dsh- 是 DeepSeek Harness 的前缀,所有包都用这个前缀。
"type": "module":使用 ESM(ES Modules),整个仓库统一用 ESM。
"exports":Node 的包入口声明。"." 表示包根入口。
"default": "./src/index.ts":开发期指向源码,让 tsx 能直接执行 .ts。正式发布后改为 "./lib/index.js"(编译产物)。
"./src/*": "./src/*":允许直接引用源码路径(如 @deepseek-ai/dsh-my-cap/src/index.ts)。
"peerDependencies":声明"我需要 cordis,但由消费者安装"。避免重复打包。
"devDependencies":开发时需要 cordis 来做类型检查。
"workspace:^":pnpm workspace 协议,指向仓库内的本地包,不会从 npm 下载。
1.3 创建 packages/my-cap/my-cap/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
"include": ["src"],
"references": [
{ "path": "../../../vendor/cordis" }
]
}解读:
"extends":继承仓库根目录的 tsconfig.base.json,复用所有通用编译选项。
"rootDir": "src" / "outDir": "lib/types":源码在 src/,编译产物输出到 lib/types/。
"include": ["src"]:只编译 src/ 下的文件。
"references":TypeScript 项目引用。声明"我依赖 cordis 这个项目",让 tsc -b 按正确顺序编译。
第二步:写 Service Provider(my-cap-local)
2.1 创建 packages/my-cap/my-cap-local/src/index.ts
import type { Context } from '@deepseek-ai/cordis';
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
// 实现抽象服务类
class MyCapLocal extends MyCapService {
constructor(ctx: Context) {
super(ctx)
}
async execute(request: MyCapRequest): Promise<MyCapResult> {
// 具体实现:将输入转为大写
return {
output: request.input.toUpperCase()
}
}
}
export const name = 'my-cap-local'
export function apply(ctx: Context) {
// 将服务实现注册到上下文
ctx.plugin(MyCapLocal)
}解读:
import { MyCapService } from '@deepseek-ai/dsh-my-cap':从第 1 层导入抽象类。Provider 依赖 Definition,不依赖 Consumer。
class MyCapLocal extends MyCapService:继承抽象类,提供具体实现。
request.input.toUpperCase():这就是能力的核心——把字符串转大写。
export const name = 'my-cap-local':Cordis Loader 要求每个插件导出一个 name。
export function apply(ctx: Context):Cordis Loader 要求每个插件导出一个 apply 函数。Loader 加载插件时会调用 apply(ctx)。
ctx.plugin(MyCapLocal):这一行触发 Cordis 实例化 MyCapLocal。它的构造函数会自动调用父类的 super(ctx, 'myCap'),于是 ctx.myCap 就被挂载到上下文上了。
2.2 创建 packages/my-cap/my-cap-local/package.json
{
"name": "@deepseek-ai/dsh-my-cap-local",
"description": "Local provider implementation of the my-cap string-to-uppercase service",
"version": "0.1.0-rc.5",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/my-cap/my-cap-local"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": { "types": "./lib/types/index.d.ts", "default": "./src/index.ts" },
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": ["lib/index.js", "lib/types/**/*.d.ts"],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-my-cap": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-my-cap": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}和第 1 层的区别:peerDependencies 和 devDependencies 多了 @deepseek-ai/dsh-my-cap——因为 Provider 依赖 Definition。
2.3 创建 packages/my-cap/my-cap-local/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
"include": ["src"],
"references": [
{ "path": "../../../vendor/cordis" },
{ "path": "../my-cap" }
]
}和第 1 层的区别:references 多了 { "path": "../my-cap" }——告诉 TypeScript "我依赖第 1 层的项目"。
第三步:写 Consumer(tool-my-cap)
3.1 创建 packages/my-cap/tool-my-cap/src/index.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-my-cap'
export const name = 'tool-my-cap'
export const inject = ['tools', 'myCap'] // 依赖 tools 和 myCap 两个服务
export function apply(ctx: Context) {
ctx.tools.register(
defineTool({
name: 'my_cap',
description: 'Execute my capability.',
parameters: {
input: {
type: 'string',
required: true,
},
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args,_exec) {
// 通过 ctx.myCap 调用能力——不关心具体是谁实现的
const result = await ctx.myCap.execute({ input: args.input })
return result.output
},
})
)
}
解读:
import { defineTool } from '@deepseek-ai/dsh-tools':从核心 dsh-tools 包导入工具定义函数。
export const inject = ['tools', 'myCap']:关键! 声明这个插件依赖 tools(工具注册表)和 myCap(第 1+2 层注册的服务)。Cordis Loader 看到这个声明后,会确保这两个服务都就绪后才执行 apply。
ctx.tools.register(defineTool({ ... })):把工具注册到工具注册表。
name: 'my_cap':模型看到的工具名。
parameters:JSON Schema,告诉模型"这个工具接收一个 input 字符串参数"。
output.schema:验证返回值类型。
output.render:把返回值渲染成模型可读的 ContentBlock。
execute(args, _exec):实际执行逻辑。通过 ctx.myCap.execute(...) 调用第 1 层的抽象服务—— 不关心底下是 MyCapLocal 还是 MyCapRemote。
3.2 创建 packages/my-cap/tool-my-cap/package.json
{
"name": "@deepseek-ai/dsh-tool-my-cap",
"description": "Model-facing my_cap tool that exposes the uppercase capability to agent loops",
"version": "0.1.0-rc.5",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/my-cap/tool-my-cap"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": { "types": "./lib/types/index.d.ts", "default": "./src/index.ts" },
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": ["lib/index.js", "lib/types/**/*.d.ts"],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-my-cap": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-my-cap": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}和前两层的区别:依赖多了 @deepseek-ai/dsh-tools——因为 Consumer 需要用 defineTool 和 ctx.tools.register。
3.3 创建 packages/my-cap/tool-my-cap/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
"include": ["src"],
"references": [
{ "path": "../../../vendor/cordis" },
{ "path": "../../core/tools" },
{ "path": "../my-cap" }
]
}和前两层的区别:references 多了 { "path": "../../core/tools" }——指向 dsh-tools 包的源码。
第四步:在仓库的 tsconfig 中注册(类型检查用)
这一步 不影响 npx tsx 运行(tsx 靠 node_modules 软链解析),但影响 tsc -b 类型检查和 vitest 测试。新手建议也做上。
4.1 在 tsconfig.base.json 的 paths 白名单加一行
打开仓库根目录的 tsconfig.base.json,找到 @deepseek-ai/dsh-* 通配符的 group 列表(约第 255 行开始),在 "./packages/todo/*/src" 后面加一行:
"./packages/todo/*/src",
"./packages/my-cap/*/src", ← 新增这一行
"./packages/bundle/*/src",作用:让 TypeScript 的 paths 解析器知道 @deepseek-ai/dsh-my-cap → ./packages/my-cap/my-cap/src。vitest 的 vite-tsconfig-paths 和 tsx 在某些场景下也会用到它。
4.2 在 tsconfig.host.json 的 references 加三行
打开仓库根目录的 tsconfig.host.json,找到 references 数组(约第 272 行开始),在 { "path": "./packages/todo/tool-todo" } 后面加三行:
{ "path": "./packages/todo/tool-todo" },
{ "path": "./packages/my-cap/my-cap" }, ← 新增
{ "path": "./packages/my-cap/my-cap-local" }, ← 新增
{ "path": "./packages/my-cap/tool-my-cap" }, ← 新增
{ "path": "./packages/plan/plan-mode" },作用:让 tsc -b tsconfig.host.json 知道这三个新项目,按依赖顺序编译它们。
第五步:在根 package.json 声明依赖
这一步是 npx tsx 能跑通的关键! 没有它,pnpm install 不会在 node_modules 下创建软链。
打开仓库根目录的 package.json,找到 devDependencies(约第 144 行),在 @agentclientprotocol/sdk 后面加三行:
"devDependencies": {
"@agentclientprotocol/sdk": "0.25.1",
"@deepseek-ai/dsh-my-cap": "workspace:^", ← 新增
"@deepseek-ai/dsh-my-cap-local": "workspace:^", ← 新增
"@deepseek-ai/dsh-tool-my-cap": "workspace:^", ← 新增
"@deepseek-ai/dsh-tool-session-query": "workspace:^",
...为什么需要这一步? pnpm 和 npm 不同——它用"严格依赖声明"。一个包能被 import 解析到,必须满足:
包有 package.json(第 1-3 步已完成)。
pnpm-workspace.yaml 的 glob 覆盖包目录(packages/*/* 已覆盖)。
某个 workspace 项目的 package.json 声明了对它的依赖(这一步补上)。
exports 指向的文件存在(指向 ./src/index.ts,源码存在)。
第 3 步是 pnpm 区别于 npm 的关键。npm 会把所有包扁平化到 node_modules,即使没声明也能"碰巧"解析到;pnpm 不会,必须显式声明。
第六步:安装依赖并运行
6.1 运行 pnpm install
在仓库根目录执行:
cd E:\ProjectsAI\deepseek-harness
pnpm install这一步会:
识别 package.json 里新增的三个 workspace:^ 依赖
在 node_modules/@deepseek-ai/ 下创建三个软链:
dsh-my-cap → packages/my-cap/my-cap/。
dsh-my-cap-local → packages/my-cap/my-cap-local/。
dsh-tool-my-cap → packages/my-cap/tool-my-cap/。
6.2 创建测试脚本 demo/run-direct.ts
// demo/run-direct.ts
import { Context } from '@deepseek-ai/cordis'
import * as myCapLocal from '@deepseek-ai/dsh-my-cap-local' // 你的 Provider 包
async function main() {
// 1. 创建 Cordis 上下文——整个框架的"容器"
const ctx = new Context()
// 2. 加载 Provider——这一行会触发:
// my-cap-local 的 apply(ctx) → ctx.plugin(MyCapLocal)
// → MyCapLocal 构造函数自动调用父类 super(ctx, 'myCap')
// → 于是 ctx.myCap 这个服务就挂到 ctx 上了
await ctx.plugin(myCapLocal)
// 3. 直接调用——不经过 Tool,也不经过 Agent
const result = await ctx.myCap.execute({ input: 'hello world' })
console.log('结果:', result.output) // 预期输出: "HELLO WORLD"
// 4. 再试一个,证明是活的
const r2 = await ctx.myCap.execute({ input: 'deepseek harness' })
console.log('结果2:', r2.output) // 预期输出: "DEEPSEEK HARNESS"
}
main().catch(console.error)6.3 运行
npx tsx demo/run-direct.ts预期输出:
结果: HELLO WORLD
结果2: DEEPSEEK HARNESS看到这两行,说明三层能力完整跑通了!
运行链路图解
npx tsx demo/run-direct.ts
│
├─ import '@deepseek-ai/dsh-my-cap-local'
│ ↓ Node 解析:node_modules/@deepseek-ai/dsh-my-cap-local
│ ↓ 软链指向:packages/my-cap/my-cap-local/
│ ↓ 读 package.json exports → ./src/index.ts
│ ↓ tsx 接管 .ts 编译 → 加载模块
│
├─ ctx.plugin(myCapLocal)
│ ↓ Cordis 调用 apply(ctx)
│ ↓ apply 里执行 ctx.plugin(MyCapLocal)
│ ↓ Cordis 实例化 MyCapLocal → 构造函数调 super(ctx, 'myCap')
│ ↓ ctx.myCap 挂载完成
│
└─ ctx.myCap.execute({ input: 'hello world' })
↓ 调用 MyCapLocal.execute(第 2 层的实现)
↓ request.input.toUpperCase() → 'HELLO WORLD'
↓ 返回 { output: 'HELLO WORLD' }踩坑清单
现象 | 原因 | 解决 |
|---|---|---|
ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-my-cap-local' | 根 package.json 没声明依赖,或没跑 pnpm install | 第五步 + 第六步 6.1 |
ERR_MODULE_NOT_FOUND 但包已声明 | exports[.].default 指向 /lib/index.js 但 lib/ 不存在 | 把 default 改为 ./src/index.ts (开发期) |
TypeError: ctx.myCap is undefined | Provider 的 apply 没执行,或 ctx.plugin(MyCapLocal) 漏了 | 检查第 2 层 apply 函数 |
Property 'myCap' does not exist on type 'Context' | declare module 声明缺失或没被加载 | 检查第 1 层 src/index.ts 的 declare module 块 |
tsc -b 报错找不到 @deepseek-ai/dsh-my-cap | tsconfig.base.json paths 白名单没加 my-cap group | 第四步 4.1 |
PowerShell 报"禁止运行脚本" | 执行策略限制 | 用pnpm.cmd 代替pnpm,或用 npx tsx 代替 .ps1 脚本 |
下一步
三层跑通后,你可以:
替换 Provider:新建 my-cap-remote 包,继承 MyCapService,把 toUpperCase 换成调用某个云 API。Consumer 层完全不用改。
接入 Agent Loop:在 cordis.yml 里声明 my-cap-local 和 tool-my-cap 插件,让 AI 模型能看到 my_cap 工具并自主调用它。
正式构建:把三个 package.json 的 exports[.].default 从 ./src/index.ts 改回 ./lib/index.js,跑 pnpm run build:lib:host 编译出 lib/ 产物。