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:
import { Service } from '@deepseek-ai/cordis'
export default class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'my-service')
}
}
Service 基底クラスのコンストラクタは2つのパラメータを受け取ります:
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) サービス名の目的
サービス名はグローバルレジストリのキーです:
super(ctx, 'cache')
// → 他のプラグインは ctx.cache でこのサービスインスタンスにアクセス
(2) 命名規則
// ✅ 推奨:kebab-case または camelCase
super(ctx, 'cache')
super(ctx, 'rate-limiter')
super(ctx, 'metricsCollector')
// ❌ 非推奨
super(ctx, 'Cache') // 大文字開始
super(ctx, 'cache_service') // アンダースコア
(3) サービス名の競合
2つの Service が同じ名前を登録すると、後から登録されたものが前を上書き:
Plugin A が 'cache' を登録 → CacheServiceA
Plugin B が '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) サービス初期化タイミング
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'] ✅ → constructor 実行 → 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)
// このサービスに依存する保留中のプラグインのアクティブ化をトリガー
}
}
(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:
// 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/自動クリーンアップ |
| 継承 | ✅ サポート | ❌ 非サポート |
| テスト容易性 | ✅ モックしやすい | ⚠️ 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 に移行:
// 変更前:関数プラグイン
export function apply(ctx: Context) {
const cache = new Map()
ctx.provide('cache', {
get: (k) => cache.get(k),
set: (k, v) => cache.set(k, v)
})
}
// 変更後: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) }
}
❓ よくある質問
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 メソッド(1分間に最大60回)を実装してください。rate-limiter サービスとして登録し、別の関数プラグインから inject で使用すること。
2. ⭐⭐ 応用:CacheService の型拡張(declare module)を追加し、ctx.cache.get() がコンシューマプラグインで適切に型付けされるようにしてください。コンシューマプラグインを書き、TypeScript の型チェックが通ることを確認。
3. ⭐⭐⭐ チャレンジ:MetricsService を実装し、ツール呼び出し回数とタイミング統計を収集してください。tools サービスに依存し、ツール実行の前後でデータを記録。getStats():Record<string, { count, avgMs }> メソッドを提供。Web UI に metrics コマンドを登録して統計を表示すること。