このチュートリアルでは、ゼロから最初の DeepSeek Harness プラグインを開発します。ソースリポジトリ内にプラグインモジュールを作成し、patch 設定で登録し、Web UI を起動して読み込みを確認。ライフサイクルのクリーンアップ、依存性注入、3つのプラグイン形式を習得します。完了すれば、自分の機能を Harness に組み込めるようになります。
前提条件
始める前に、以下を確認してください:
- 公式 README の run-from-source の手順に従って
deepseek-ai/deepseek-harnessリポジトリをチェックアウトし、依存関係をインストール済みであること; pnpmが利用可能であること——Harness が依存関係と起動コマンドの管理に使用します;- TypeScript を書けるエディタがあること。
プラグインプロジェクトを作成
リポジトリのルートに scratch-plugin ディレクトリを作成し、プラグインコードは src の下に置きます:
cd /path/to/deepseek-harness
mkdir -p scratch-plugin/src
最終的な構造は次のとおりです。このチュートリアルのファイルはすべてここに置きます:
scratch-plugin/
├── cordis.yml
└── src/
└── my-plugin.ts
最初のプラグインを書く
scratch-plugin/src/my-plugin.ts を作成します:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
プラグインは本質的に apply 関数をエクスポートする TypeScript モジュールです。フレームワークはプラグインを読み込むと apply を呼び出し、ctx コンテキストを渡します。プラグインと Harness のやり取りはすべてこれを通じて行われます。name はプラグインの一意の識別子で、ログと依存関係の管理に使われます。
name と apply の規約
プラグインを書くときに3つの規約を守れば、読み込みの問題の大部分を避けられます:
nameは一意で kebab-case にする:すべて小文字で単語をハイフンでつなぎます(例:hello-plugin)。グローバルな識別子であり、重複すると読み込み競合の原因になります。ログと依存関係の管理もこれで特定します。applyはプラグインスコープの Context を受け取る:渡されるctxはこのプラグイン専用のコンテキストです。ここに登録したリスナーやタイマーは、プラグインのアンロード時に自動的にクリーンアップされ、グローバルを汚しません。- プラグインモジュールは起動時に1回だけ読み込まれる:フレームワークは起動時にモジュールを読み込んでトップレベルのコードを実行し、その後は再読み込みしません。プラグインファイルの編集を反映するにはプロセスの再起動が必要です(最後のトラブルシューティングを参照)。
プラグインを登録
Harness はまだこのファイルの存在を知りません。patch 設定で登録する必要があります。まずリポジトリのルートで pwd を実行して絶対パスを取得し、scratch-plugin/cordis.yml を作成します:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
読み込んで確認
--patch で作成した設定を指定し、Harness Web UI を起動します:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# http://127.0.0.1:3080 を開く
ターミナルには起動時にプラグインの読み込みログが表示されます。[hello-plugin] plugin loaded! が見えれば、プラグインの読み込みは成功です。ブラウザで http://127.0.0.1:3080 にアクセスし、UI が正常に起動していることを確認してください。
ライフサイクルと自動クリーンアップ
プラグインがリソース(タイマー、リスナー、接続)を保持する場合は、アンロード時に解放する必要があります。ctx.effect は関数を受け取り、その戻り値がクリーンアップ関数になります。クリーンアップ関数はプラグインのアンロード時に実行されます:
export const name = 'hello-plugin'
export function apply(ctx: Context) {
const timer = setInterval(() => {
console.log('[hello-plugin] heartbeat')
}, 5000)
// 戻り値のクリーンアップ関数はプラグインのアンロード時に実行される
ctx.effect(() => {
return () => clearInterval(timer)
})
}
さらに、ctx 経由で登録したリスナーやタイマーなどは、フレームワークがアンロード時に自動でクリーンアップするため、解放コードを書く必要はありません。ctx.effect は ctx を介さずに直接作成したリソースの管理に使います。
依存性注入
プラグインは Harness の組み込みサービス(ツールサービス tools など)を使いたくなることがよくあります。inject で依存関係を宣言すると、フレームワークはそれらのサービスが準備できてからプラグインを読み込みます:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// この時点で tools サービスは準備済み。安全に呼び出せる
ctx.tools.register(/* ... */)
}
inject がないとプラグインは即座に読み込まれ、その時点では ctx.tools がまだ準備できていない可能性があります。依存関係を宣言することは、この種のタイミングの問題を解消する標準的な方法です。
3つのプラグイン形式
ここまでは関数形式を使ってきました。Harness はオブジェクト形式とクラス形式もサポートしています:
// オブジェクト形式:メタデータとロジックを1つのデフォルトエクスポートにまとめる
export default {
name: 'hello-plugin',
inject: ['tools'],
apply(ctx: Context) {
console.log('[hello-plugin] loaded!')
},
}
// クラス形式:他のプラグインにサービスを提供する場合に使用
import { Service } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
トラブルシューティング
初めてプラグインを動かすときによく詰まる4つの問題を、次の順番で確認してください。
プラグインが読み込まれない
起動ログに [hello-plugin] plugin loaded! が見えない場合、十中八九 cordis.yml のパスが間違っています。
cd /path/to/deepseek-harness
pwd
# 出力例:/Users/you/code/deepseek-harness
# これを scratch-plugin/src/my-plugin.ts の前に連結する
Cannot find module エラー
ターミナルに Cannot find module が表示された場合、原因は2つだけです。cordis.yml の .ts ファイルのパスの打ち間違い(ディレクトリ名、ファイル名、大文字小文字を1文字ずつ確認)か、そのファイルに構文エラーがあってモジュールが解析できないか。まずパスを確認し、次にエディタの TypeScript チェックでファイルに赤いエラーがないか確認してください。
3080 番ポートが使用中
起動時にポート使用中のエラーが出た場合、別のプロセスが 3080 をリッスンしています。たいていは前回の Harness が正常に終了していないだけです。まず使用中のプロセスを特定します:
lsof -i :3080
そのプロセスを停止してから再起動してください。停止したくない場合は、別のポートで起動することもできます。
コードの変更が反映されない
プラグインモジュールは起動時に1回だけ読み込まれます。my-plugin.ts や cordis.yml を編集したら、変更を確認するために起動コマンドを再起動する必要があります:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
開発中の patch は設定を提供するだけでグローバル profile には書き込まないため、安心して何度でも再起動できます。
プラグイン開発のよくある質問
プラグインのパスはなぜ絶対パスでなければならないのですか?
patch 設定はファイルシステムのパスで直接モジュールを読み込み、Harness は設定ファイルやリポジトリルートからの相対パスを解決しません。pwd で取得した絶対パスを name フィールドに入れることで、毎回の起動でプラグインファイルが確実に見つかります。
関数形式、オブジェクト形式、クラス形式はどう選べばよいですか?
デフォルトは関数形式です。ほとんどのプラグインは name、inject、apply 関数をエクスポートするだけで十分です。オブジェクト形式はメタデータとロジックを1つのデフォルトエクスポートにまとめたい場合に適しています。クラス形式が必要になるのは、他のプラグインにサービスを提供する(他のプラグインがあなたの機能を inject する)場合だけです。
プラグインのコードを変更したら再起動が必要ですか?
必要です。プラグインモジュールは起動時に1回だけ読み込まれます。my-plugin.ts や cordis.yml を編集したら、pnpm dsh web --patch ./scratch-plugin/cordis.yml を再実行すると変更が反映されます。開発中の patch は設定を提供するだけでグローバル profile には書き込まないため、再起動を繰り返しても副作用はありません。
起動時にエラーが出てプラグインが読み込まれない場合は?
まずターミナルのエラーの最初の行を見てください。失敗のほとんどは2つの問題のどちらかです——cordis.yml のパスが絶対パスでないか、my-plugin.ts に構文エラーがあるか。このチュートリアル最後の「トラブルシューティング」の項目を順に確認し、修正してから再起動してください。
Context の TypeScript 型ヒントを得るには?
@deepseek-ai/cordis から型をインポートして apply の引数に注釈を付けます。import type { Context } として、apply(ctx: Context) と書きます。inject で宣言したサービス(tools など)は ctx 上のプロパティとして型ヒントが得られ、エディタで補完できます。