DeepSeek Harness: 服务与依赖:Service 基类

最后更新:2026-08-31

函数插件通过 apply 注册"做事"的能力,而 Service 插件通过类暴露"提供服务"的能力。当你的插件需要维护内部状态、提供可调用的 API、或作为其他插件的依赖基础时,Service 基类是最佳选择。

💡 提示:Service 的核心价值是"有状态的服务"——类的实例属性保存状态,方法暴露 API,其他插件通过 inject 声明依赖后使用。如果你的插件只是注册工具和监听器,函数形态更简洁。

📋 前置知识:已完成 14-inject.md17-fiber.md

1. 你将学到


2. Service 类定义

服务隔离与作用域

(1) ▶ 示例 1

TYPESCRIPT
import { Service } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'my-service')
  }
}

Service 基类的构造函数接收两个参数:

▶ 示例 2:缓存服务

TYPESCRIPT
import { Service, Context } from '@deepseek-ai/cordis'

export default class CacheService extends Service {
  private cache = new Map<string, { value: any; expires: number }>()

  constructor(ctx: Context) {
    super(ctx, 'cache')
  }

  get(key: string): any {
    const entry = this.cache.get(key)
    if (!entry) return undefined
    if (Date.now() > entry.expires) {
      this.cache.delete(key)
      return undefined
    }
    return entry.value
  }

  set(key: string, value: any, ttlMs: number = 60000): void {
    this.cache.set(key, { value, expires: Date.now() + ttlMs })
  }

  delete(key: string): boolean {
    return this.cache.delete(key)
  }

  clear(): void {
    this.cache.clear()
  }
}

(3) 自动注册

Service 构造函数中 super(ctx, 'cache') 自动将实例注册为 cache 服务。其他插件声明 inject: ['cache'] 即可使用。


3. constructor 与服务名

(1) 服务名的作用

服务名是全局注册表的 key:

TYPESCRIPT
super(ctx, 'cache')
// → 其他插件通过 ctx.cache 访问此服务实例

(2) 命名规范

TYPESCRIPT
// ✅ 推荐:短横线或驼峰
super(ctx, 'cache')
super(ctx, 'rate-limiter')
super(ctx, 'metricsCollector')

// ❌ 不推荐
super(ctx, 'Cache')        // 大写开头
super(ctx, 'cache_service') // 下划线

(3) 服务名冲突

如果两个 Service 注册同名服务,后注册的覆盖先注册的:

TEXT 📖 仅展示
Plugin A registers 'cache' → CacheServiceA
Plugin B registers 'cache' → CacheServiceB
→ 最终 ctx.cache = CacheServiceB

(4) 访问服务

TYPESCRIPT
// 其他插件
export const inject = ['cache']

export function apply(ctx: Context) {
  ctx.cache.set('user:1', { name: 'Alice' }, 300000)
  const user = ctx.cache.get('user:1')
}

4. static inject 声明

(1) Service 的依赖

Service 类通过 static inject 声明自身依赖:

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

  constructor(ctx: Context) {
    super(ctx, 'database')
  }

  async query(sql: string) {
    const schema = await ctx.fs.readFile('schema.json')
    // ...
  }
}

(2) 可选依赖

TYPESCRIPT
static inject = ['fs', 'cache?']

与函数插件语法一致,? 表示可选。

(3) Service 初始化时序

100%
sequenceDiagram
    participant F as Framework
    participant FS as FS Service
    participant DB as Database Service
    participant Tool as Tool Plugin

    F->>FS: 加载 → active
    F->>DB: inject ['fs'] ✅ → 构造函数执行 → active
    F->>Tool: inject ['database'] ✅ → apply 执行 → active

(4) 构造函数中使用依赖

TYPESCRIPT
export default class MyService extends Service {
  static inject = ['tools']
  
  private defaultTool: string

  constructor(ctx: Context) {
    super(ctx, 'my-service')
    // ctx.tools 已就绪(inject 保证)
    this.defaultTool = ctx.tools.getDefault()
  }
}

5. 服务注册与发现

(1) 注册机制

Service 构造函数中的 super(ctx, name) 触发注册:

TYPESCRIPT
// 框架内部(伪代码)
class Service {
  constructor(ctx: Context, name: string) {
    ctx.provide(name, this)
    // 触发依赖此服务的 pending 插件激活
  }
}

(2) 发现机制

其他插件通过 inject 声明依赖,框架在依赖就绪后注入:

TYPESCRIPT
// 依赖插件
export const inject = ['cache']

export function apply(ctx: Context) {
  // ctx.cache 自动注入,类型安全
  ctx.cache.set('key', 'value')
}

(3) 运行时服务查询

TYPESCRIPT
// 检查服务是否已注册
if (ctx.cache) {
  ctx.cache.get('key')
}

// 列出所有已注册服务
ctx.logger.info('available services:', ctx.serviceNames)

(4) 服务依赖图

100%
graph TB
    FS[fs Service] --> DB[database Service]
    CACHE[cache Service] --> DB
    DB --> TOOL1[tool-a Plugin]
    DB --> TOOL2[tool-b Plugin]
    CACHE --> TOOL1

6. 类型安全的服务访问

(1) TypeScript 类型扩展

为了让 ctx.cache 有正确的类型提示,需要声明类型扩展:

TYPESCRIPT
// 在插件的类型声明文件中
declare module '@deepseek-ai/cordis' {
  interface Context {
    cache: CacheService
  }
}

(2) ▶ 示例 2

TYPESCRIPT
// cache-service.ts
import { Service, Context } from '@deepseek-ai/cordis'

export default class CacheService extends Service {
  private cache = new Map<string, any>()

  constructor(ctx: Context) {
    super(ctx, 'cache')
  }

  get(key: string): any {
    return this.cache.get(key)
  }

  set(key: string, value: any, ttl?: number): void {
    this.cache.set(key, value)
  }

  has(key: string): boolean {
    return this.cache.has(key)
  }

  delete(key: string): boolean {
    return this.cache.delete(key)
  }

  clear(): void {
    this.cache.clear()
  }
}

// 类型扩展
declare module '@deepseek-ai/cordis' {
  interface Context {
    cache: CacheService
  }
}

(3) 消费端的类型安全

TYPESCRIPT
// 消费插件
import { Context } from '@deepseek-ai/cordis'

export const inject = ['cache']

export function apply(ctx: Context) {
  ctx.cache.set('key', 'value')   // ✅ 类型正确
  ctx.cache.invalid()             // ❌ 编译错误:方法不存在
  ctx.cache.get('key').foo()      // ⚠️ any 类型,需进一步类型化
}

(4) 泛型服务

TYPESCRIPT
export default class CacheService<T = any> extends Service {
  private cache = new Map<string, T>()

  get(key: string): T | undefined {
    return this.cache.get(key)
  }

  set(key: string, value: T): void {
    this.cache.set(key, value)
  }
}

7. Service 与函数插件的对比

(1) 功能对比

维度 Service 插件 函数插件
形态 函数/对象
状态管理 实例属性 闭包变量
服务暴露 super(ctx, name) 自动注册 ctx.provide() 手动注册
依赖声明 static inject export const inject
生命周期 构造/销毁 apply/自动清理
继承 ✅ 支持 ❌ 不支持
可测试性 ✅ 易于 mock ⚠️ 需要 mock ctx
代码量 较多 较少

(2) 选择指南

TEXT 📖 仅展示
选择 Service 的场景:
  → 需要暴露 API 给其他插件
  → 需要维护有状态的数据
  → 需要继承和复用
  → 作为多个插件的基础依赖

选择函数插件的场景:
  → 只注册工具和监听器
  → 无状态或状态简单
  → 代码量少,快速开发
  → 不需要被其他插件依赖

(3) 混合使用

一个项目中可以同时使用两种形态:

TYPESCRIPT
// CacheService — Service 插件
export default class CacheService extends Service { ... }

// CacheTool — 函数插件,消费 CacheService
export const inject = ['cache', 'tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'cache_get',
    // ...
    async execute({ key }, ctx) {
      return ctx.cache.get(key)
    }
  }))
}

(4) 从函数插件迁移到 Service

当函数插件逐渐变复杂时,可以迁移为 Service:

TYPESCRIPT
// Before: 函数插件
export function apply(ctx: Context) {
  const cache = new Map()
  ctx.provide('cache', {
    get: (k) => cache.get(k),
    set: (k, v) => cache.set(k, v)
  })
}

// After: Service 插件
export default class CacheService extends Service {
  private cache = new Map()
  
  constructor(ctx: Context) {
    super(ctx, 'cache')
  }
  
  get(k: string) { return this.cache.get(k) }
  set(k: string, v: any) { this.cache.set(k, v) }
}

❓ 常见问题

Q Service 可以注册多个服务名吗?
A 技术上可以在构造函数中多次调用 super,但不推荐。一个 Service 应该只注册一个服务名,职责单一。
Q Service 的 this.ctx 是什么?
A this.ctx 是构造函数接收的 ctx 参数,Service 基类自动保存。它就是插件的上下文,可以访问所有已注入的服务。
Q Service 的销毁逻辑怎么写?
A 覆写 dispose() 方法: typescript export default class DbService extends Service { private pool: Pool dispose() { this.pool.end() } }
Q 函数插件用 ctx.provide() 注册的服务和 Service 有区别吗?
A 功能等价。区别在于 Service 有类实例和继承支持,ctx.provide() 只是手动注册一个对象。
Q Service 可以依赖另一个 Service 吗?
A 可以。用 static inject 声明依赖,在构造函数中通过 ctx.serviceName 访问。
Q 如何为第三方 Service 写类型扩展?
A 在项目的 .d.ts 文件中声明 declare module '@deepseek-ai/cordis' 并扩展 Context 接口。确保此声明文件被 TypeScript 编译器包含。 ---

📖 小节


📝 作业

1. ⭐ 基础题:编写一个 RateLimiterService,提供 check(key): boolean 方法(每分钟最多 60 次调用)。注册为 rate-limiter 服务,在另一个函数插件中注入并使用。

2. ⭐⭐ 进阶题:为 CacheService 添加类型扩展(declare module),让消费插件中 ctx.cache.get() 返回值有正确的类型提示。编写一个消费插件,验证 TypeScript 类型检查通过。

3. ⭐⭐⭐ 挑战题:实现一个 MetricsService,收集工具调用的次数和耗时统计。它依赖 tools 服务,在工具执行前后记录数据。提供 getStats(): Record<string, { count, avgMs }> 方法。在 Web UI 中注册一个 metrics 命令展示统计信息。

Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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