在前面几章中,我们学习了服务——通过继承 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 互不依赖。

三大好处:

  1. Provider可替换:换一个 Provider,Consumer 完全无感知。

  2. 独立演进:三种角色可以各自独立迭代。

  3. 依赖解耦: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 解析到,必须满足:

  1. 包有 package.json(第 1-3 步已完成)。

  2. pnpm-workspace.yaml 的 glob 覆盖包目录(packages/*/* 已覆盖)。

  3. 某个 workspace 项目的 package.json 声明了对它的依赖(这一步补上)。

  4. 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 脚本

下一步

三层跑通后,你可以:

  1. 替换 Provider:新建 my-cap-remote 包,继承 MyCapService,把 toUpperCase 换成调用某个云 API。Consumer 层完全不用改。

  2. 接入 Agent Loop:在 cordis.yml 里声明 my-cap-local 和 tool-my-cap 插件,让 AI 模型能看到 my_cap 工具并自主调用它。

  3. 正式构建:把三个 package.json 的 exports[.].default 从 ./src/index.ts 改回 ./lib/index.js,跑 pnpm run build:lib:host 编译出 lib/ 产物。