DeepSeek Harness: 服务与依赖:Service 基类
最后更新:2026-08-31
函数插件通过 apply 注册"做事"的能力,而 Service 插件通过类暴露"提供服务"的能力。当你的插件需要维护内部状态、提供可调用的 API、或作为其他插件的依赖基础时,Service 基类是最佳选择。
📋 前置知识:已完成 14-inject.md 和 17-fiber.md
1. 你将学到
- Service 类定义
- constructor(ctx, 'serviceName')
- static inject 声明
- 服务注册与发现
- 类型安全的服务访问
- Service 与函数插件的对比
2. Service 类定义
(1) ▶ 示例 1
import { Service } from '@deepseek-ai/cordis'
export default class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'my-service')
}
}
Service 基类的构造函数接收两个参数:
ctx:Cordis 上下文serviceName:服务标识,其他插件通过此名称引用
▶ 示例 2:缓存服务
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:
super(ctx, 'cache')
// → 其他插件通过 ctx.cache 访问此服务实例
(2) 命名规范
// ✅ 推荐:短横线或驼峰
super(ctx, 'cache')
super(ctx, 'rate-limiter')
super(ctx, 'metricsCollector')
// ❌ 不推荐
super(ctx, 'Cache') // 大写开头
super(ctx, 'cache_service') // 下划线
(3) 服务名冲突
如果两个 Service 注册同名服务,后注册的覆盖先注册的:
Plugin A registers 'cache' → CacheServiceA
Plugin B registers 'cache' → CacheServiceB
→ 最终 ctx.cache = CacheServiceB
(4) 访问服务
// 其他插件
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 声明自身依赖:
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) 可选依赖
static inject = ['fs', 'cache?']
与函数插件语法一致,? 表示可选。
(3) Service 初始化时序
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) 构造函数中使用依赖
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) 触发注册:
// 框架内部(伪代码)
class Service {
constructor(ctx: Context, name: string) {
ctx.provide(name, this)
// 触发依赖此服务的 pending 插件激活
}
}
(2) 发现机制
其他插件通过 inject 声明依赖,框架在依赖就绪后注入:
// 依赖插件
export const inject = ['cache']
export function apply(ctx: Context) {
// ctx.cache 自动注入,类型安全
ctx.cache.set('key', 'value')
}
(3) 运行时服务查询
// 检查服务是否已注册
if (ctx.cache) {
ctx.cache.get('key')
}
// 列出所有已注册服务
ctx.logger.info('available services:', ctx.serviceNames)
(4) 服务依赖图
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 有正确的类型提示,需要声明类型扩展:
// 在插件的类型声明文件中
declare module '@deepseek-ai/cordis' {
interface Context {
cache: CacheService
}
}
(2) ▶ 示例 2
// 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) 消费端的类型安全
// 消费插件
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) 泛型服务
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) 选择指南
选择 Service 的场景:
→ 需要暴露 API 给其他插件
→ 需要维护有状态的数据
→ 需要继承和复用
→ 作为多个插件的基础依赖
选择函数插件的场景:
→ 只注册工具和监听器
→ 无状态或状态简单
→ 代码量少,快速开发
→ 不需要被其他插件依赖
(3) 混合使用
一个项目中可以同时使用两种形态:
// 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:
// 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) }
}
❓ 常见问题
super,但不推荐。一个 Service 应该只注册一个服务名,职责单一。this.ctx 是构造函数接收的 ctx 参数,Service 基类自动保存。它就是插件的上下文,可以访问所有已注入的服务。dispose() 方法: typescript export default class DbService extends Service { private pool: Pool dispose() { this.pool.end() } } static inject 声明依赖,在构造函数中通过 ctx.serviceName 访问。.d.ts 文件中声明 declare module '@deepseek-ai/cordis' 并扩展 Context 接口。确保此声明文件被 TypeScript 编译器包含。 ---📖 小节
- Service 是 Cordis 的类形态插件,通过
super(ctx, 'serviceName')自动注册服务 static inject声明 Service 的依赖,保证构造函数中 ctx 上的服务已就绪- 类型扩展通过
declare module让 TypeScript 识别ctx.serviceName的类型 - Service 适合有状态、需暴露 API、需继承的场景;函数插件适合无状态、轻量的场景
- 两种形态可以混合使用:Service 提供基础服务,函数插件消费服务注册工具
📝 作业
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 命令展示统计信息。