このチュートリアルでは、ゼロから最初の DeepSeek Harness プラグインを開発します。ソースリポジトリ内にプラグインモジュールを作成し、patch 設定で登録し、Web UI を起動して読み込みを確認。ライフサイクルのクリーンアップ、依存性注入、3つのプラグイン形式を習得します。完了すれば、自分の機能を Harness に組み込めるようになります。

プラグインの構造図:cordis.yml の登録設定が src/my-plugin.ts モジュールを指し、モジュールは name、inject、apply をエクスポートし、Harness コアが読み込むcordis.yml- insert:id: helloname: /abs/path登録設定src/my-plugin.tsexport const nameexport const injectexport function apply(ctx)ctx.effect(() => …)name:プラグイン識別子inject:依存関係の宣言apply:エントリ関数Harness コア読み込んで実行登録読み込み
プラグインの構造:cordis.yml がモジュールを登録し、Harness コアが読み込んで apply を呼び出す。

前提条件

始める前に、以下を確認してください:

  • 公式 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 を作成します:

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 を作成します:

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.effectctx を介さずに直接作成したリソースの管理に使います。

依存性注入

プラグインは Harness の組み込みサービス(ツールサービス tools など)を使いたくなることがよくあります。inject で依存関係を宣言すると、フレームワークはそれらのサービスが準備できてからプラグインを読み込みます:

tools サービスを宣言して使用
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 が正常に終了していないだけです。まず使用中のプロセスを特定します:

3080 を使用中のプロセスを確認
lsof -i :3080

そのプロセスを停止してから再起動してください。停止したくない場合は、別のポートで起動することもできます。

コードの変更が反映されない

プラグインモジュールは起動時に1回だけ読み込まれます。my-plugin.tscordis.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 上のプロパティとして型ヒントが得られ、エディタで補完できます。