DeepSeek Harness: 实战:写一个可替换的能力

最后更新:2026-08-31

理论理解了三角色,但"纸上得来终觉浅"。本课通过一个完整的实战项目,从零实现一个可替换的文件统计能力——定义接口、写本地实现、写沙箱实现、消费能力、切换 Provider,全流程走一遍。

💡 提示:这个实战项目的重点是"切换 Provider 前后 Consumer 代码不变"——如果 Consumer 需要改代码,说明你的 Definition 抽象不够好。

📋 前置知识:已完成 22-capability.md,理解能力三角色

1. 你将学到


2. 项目结构

MyCap 结构

(1) ▶ 示例 1

TEXT 📖 仅展示
file-stats-capability/
├── definition.ts            ← Definition
├── providers/
│   ├── local.ts             ← 本地 Provider
│   └── sandbox.ts           ← 远程沙箱 Provider
├── consumer/
│   └── file-info-tool.ts    ← Consumer 插件
├── types.ts                 ← 类型声明
└── index.ts                 ← 导出

(2) ▶ 示例 2

100%
graph TB
    DEF[definition.ts] --> LOCAL[providers/local.ts]
    DEF --> SANDBOX[providers/sandbox.ts]
    DEF --> TOOL[consumer/file-info-tool.ts]

3. 定义文件统计能力

(1) 需求分析

文件统计能力需要提供:

(2) ▶ 示例 2

TYPESCRIPT
// definition.ts
import { defineCapability } from '@deepseek-ai/cordis'

export interface FileStatsResult {
  totalFiles: number
  totalDirs: number
  totalSize: number
  byExtension: Record<string, number>
}

export interface FileStatsCapability {
  countFiles(dir: string, recursive?: boolean): Promise<number>
  calcSize(dir: string): Promise<number>
  analyze(dir: string): Promise<FileStatsResult>
  exists(path: string): Promise<boolean>
}

export const FileStats = defineCapability({
  name: 'file-stats',
  description: 'File statistics and analysis capability',
  interface: {} as FileStatsCapability
})

(3) 类型声明

TYPESCRIPT
// types.ts
import { FileStatsCapability } from './definition'

declare module '@deepseek-ai/cordis' {
  interface Context {
    'file-stats': FileStatsCapability
  }
}

4. 实现本地 Provider

(1) 代码

TYPESCRIPT
// providers/local.ts
import { Service, Context } from '@deepseek-ai/cordis'
import { FileStats, FileStatsResult } from '../definition'
import { readdir, stat } from 'fs/promises'
import { join } from 'path'

export default class LocalFileStatsProvider extends Service {
  static inject = ['fs']

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    
    ctx.implement(FileStats, {
      async countFiles(dir: string, recursive = false): Promise<number> {
        const result = await this._walk(dir, recursive)
        return result.totalFiles
      },

      async calcSize(dir: string): Promise<number> {
        const result = await this._walk(dir, true)
        return result.totalSize
      },

      async analyze(dir: string): Promise<FileStatsResult> {
        return await this._walk(dir, true)
      },

      async exists(path: string): Promise<boolean> {
        try {
          await stat(path)
          return true
        } catch {
          return false
        }
      }
    })
  }

  private async _walk(dir: string, recursive: boolean): Promise<FileStatsResult> {
    let totalFiles = 0
    let totalDirs = 0
    let totalSize = 0
    const byExtension: Record<string, number> = {}

    await this._walkInner(dir, recursive, (fileStat) => {
      totalFiles++
      totalSize += fileStat.size
      const ext = fileStat.name.includes('.')
        ? '.' + fileStat.name.split('.').pop()!.toLowerCase()
        : '(no extension)'
      byExtension[ext] = (byExtension[ext] || 0) + 1
    }, () => {
      totalDirs++
    })

    return { totalFiles, totalDirs, totalSize, byExtension }
  }

  private async _walkInner(
    dir: string,
    recursive: boolean,
    onFile: (f: { name: string; size: number }) => void,
    onDir: () => void
  ): Promise<void> {
    const entries = await readdir(dir, { withFileTypes: true })
    for (const entry of entries) {
      if (entry.isFile()) {
        const s = await stat(join(dir, entry.name))
        onFile({ name: entry.name, size: s.size })
      } else if (entry.isDirectory()) {
        onDir()
        if (recursive) {
          await this._walkInner(join(dir, entry.name), recursive, onFile, onDir)
        }
      }
    }
  }
}

(2) 注册到 cordis.yml

YAML
plugins:
  file-stats:
    $insert: /home/alice/dev/file-stats-capability/providers/local

5. 实现远程沙箱 Provider

(1) 设计

沙箱 Provider 将文件操作委托给远程服务:

100%
graph LR
    TOOL[Consumer] -->|调用| CAP[file-stats 能力]
    CAP -->|HTTP| SANDBOX[沙箱服务<br/>sandbox:8080]
    SANDBOX -->|操作| FS[隔离文件系统]

(2) 代码

TYPESCRIPT
// providers/sandbox.ts
import { Service, Context } from '@deepseek-ai/cordis'
import { FileStats, FileStatsResult } from '../definition'

export const Config = Schema.object({
  endpoint: Schema.string().default('http://sandbox:8080').description('Sandbox API endpoint'),
  timeout: Schema.number().default(30000).description('Request timeout in ms')
})

export default class SandboxFileStatsProvider extends Service {
  static inject = []

  private endpoint: string
  private timeout: number

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    this.endpoint = ctx.config.endpoint
    this.timeout = ctx.config.timeout
    
    ctx.implement(FileStats, {
      countFiles: (dir, recursive) => 
        this._call('count-files', { dir, recursive }).then(r => r.count),
      
      calcSize: (dir) => 
        this._call('calc-size', { dir }).then(r => r.size),
      
      analyze: (dir) => 
        this._call<FileStatsResult>('analyze', { dir }),
      
      exists: (path) => 
        this._call('exists', { path }).then(r => r.exists)
    })
  }

  private async _call<T = any>(action: string, params: Record<string, any>): Promise<T> {
    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), this.timeout)

    try {
      const response = await fetch(`${this.endpoint}/file-stats/${action}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
        signal: controller.signal
      })

      if (!response.ok) {
        throw new Error(`sandbox error: ${response.status} ${response.statusText}`)
      }

      return await response.json() as T
    } finally {
      clearTimeout(timer)
    }
  }
}

(3) 注册到 cordis.yml

YAML
plugins:
  file-stats:
    $replace: /home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint: http://sandbox:8080
      timeout: 15000

6. 在工具中消费能力

(1) Consumer 插件代码

TYPESCRIPT
// consumer/file-info-tool.ts
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'

export const name = 'tool-file-info'
export const inject = ['tools', 'file-stats']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'file_info',
    description: 'Get detailed file statistics for a directory. Returns file count, total size, and breakdown by extension.',
    parameters: {
      type: 'object',
      properties: {
        path: {
          type: 'string',
          description: 'Directory path to analyze'
        },
        recursive: {
          type: 'boolean',
          description: 'Include subdirectories in analysis',
          default: true
        }
      },
      required: ['path']
    },
    async execute({ path, recursive }, ctx) {
      try {
        const exists = await ctx['file-stats'].exists(path)
        if (!exists) {
          return { error: true, message: `Path does not exist: ${path}` }
        }

        const stats = await ctx['file-stats'].analyze(path)
        
        return {
          path,
          recursive,
          totalFiles: stats.totalFiles,
          totalDirs: stats.totalDirs,
          totalSize: stats.totalSize,
          totalSizeHuman: formatSize(stats.totalSize),
          byExtension: stats.byExtension
        }
      } catch (error: any) {
        return {
          error: true,
          message: `Failed to analyze directory: ${error.message}`
        }
      }
    }
  }))
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
}

(2) 关键观察

Consumer 代码中没有任何 localsandbox 的字样——它只依赖 file-stats 能力的接口。这正是能力系统的价值。


7. 切换 Provider 的效果

(1) 使用本地 Provider

YAML
# cordis.yml — 本地开发
plugins:
  file-stats:
    $insert: /home/alice/dev/file-stats-capability/providers/local

Agent 调用 file_info 工具:

TEXT 📖 仅展示
👤 Alice: 分析 /home/alice/project 目录

🤖 Agent:
🔧 Using tool: file_info
  → path: /home/alice/project
  
  Result: {
    path: "/home/alice/project",
    totalFiles: 42,
    totalDirs: 5,
    totalSize: 245760,
    totalSizeHuman: "240.0 KB",
    byExtension: { ".ts": 28, ".js": 10, ".json": 4 }
  }

(2) 切换到沙箱 Provider

YAML
# cordis.yml — 沙箱环境
plugins:
  file-stats:
    $replace: /home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint: http://sandbox:8080

重启后,Agent 调用同一个工具:

TEXT 📖 仅展示
👤 Alice: 分析 /workspace/project 目录

🤖 Agent:
🔧 Using tool: file_info
  → path: /workspace/project
  
  Result: {
    path: "/workspace/project",
    totalFiles: 42,
    totalDirs: 5,
    totalSize: 245760,
    totalSizeHuman: "240.0 KB",
    byExtension: { ".ts": 28, ".js": 10, ".json": 4 }
  }

Consumer 代码完全不变,结果格式一致——只是底层从本地文件系统变成了远程沙箱 API。

(3) 对比

维度 本地 Provider 沙箱 Provider
实现方式 直接 fs 调用 HTTP API 调用
文件系统 本地磁盘 沙箱隔离环境
延迟 < 1ms 50-200ms
安全性 直接访问 沙箱隔离
Consumer 代码 相同 相同
返回格式 相同 相同

❓ 常见问题

Q 两个 Provider 的返回格式必须完全一致吗?
A 是的。这是 Definition 的核心约束——所有 Provider 必须遵循同一个接口。返回格式不同会导致 Consumer 解析错误。
Q 沙箱 Provider 的延迟更高,Consumer 需要处理吗?
A 不需要。延迟对 Consumer 透明。如果需要优化,可以在 Consumer 层加缓存,或用异步提示用户等待。
Q 如何保证 Provider 实现了所有方法?
A TypeScript 编译时检查。如果 Provider 缺少 Definition 中的方法,编译报错。
Q 能力系统可以用在插件配置中吗?
A 可以。不同的 Provider 可以有不同的 Config: yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # sandbox Provider 专属配置
Q Consumer 如何知道当前使用的是哪个 Provider?
A 通常不需要知道。如果确实需要,可以在 Definition 中加 providerInfo() 方法,各 Provider 返回自身信息。 ---

📖 小节


📝 作业

1. ⭐ 基础题:按照本课步骤,创建 FileStats 能力的 Definition 和本地 Provider,注册后通过 Consumer 工具调用验证。

2. ⭐⭐ 进阶题:实现一个 InMemoryProvider——文件数据预存在内存 Map 中(不访问真实文件系统),适合单元测试。验证 Consumer 工具在 InMemoryProvider 下也能正常工作。

3. ⭐⭐⭐ 挑战题:实现一个 CachedProvider——装饰器模式,在另一个 Provider 前加缓存层。缓存最近 N 次的分析结果,相同路径直接返回缓存。注意:CachedProvider 本身也是一个 Provider,它内部委托给另一个 Provider。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏