DeepSeek Harness: 最初のプラグイン

最終更新:2026-08-31

最初のプラグインを書くことは、DSH を深く理解するための重要ステップ——「フレームワークを使う」から「フレームワークを拡張する」への跳躍です。このレッスンでは、ローカルプロジェクトの作成から始め、段階的にロード可能で実行可能な Cordis プラグインを完成させます。

💡 ヒント:DSH プラグインのコアプロトコルは最小限——nameapply をエクスポートするだけです。フレームワークが apply(ctx) を呼び出すと、プラグインは ctx を通じて機能を登録します;プラグインがアンロードされると、ctx に登録されたリソースは自動的に回収されます。

📋 前提知識08-community-plugins.md の完了、プラグインエコシステムの概要に精通していること

1. 学習内容

プラグインディレクトリ構造


2. ローカルプロジェクトの作成

(1) プロジェクトディレクトリの初期化

すべての DSH プラグインは本質的に Node.js パッケージです。ゼロからセットアップしましょう:

BASH
mkdir -p scratch-plugin/src
cd scratch-plugin
pnpm init

結果の package.json

JSON
{
  "name": "scratch-plugin",
  "version": "0.1.0",
  "main": "src/index.ts"
}

▶ サンプル 2:

BASH
pnpm add -D @deepseek-ai/cordis typescript

プロジェクト構造:

TEXT 📖 参照専用
scratch-plugin/
├── src/
│   └── index.ts      ← プラグインメインエントリ
├── package.json
└── node_modules/

(3) TypeScript 設定

tsconfig.json を作成:

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

3. プラグインコアプロトコル

(1) 最小プラグイン

DSH プラグインは2つの条件を満たすだけで済みます:

  1. name 文字列をエクスポート——プラグインの一意識別子
  2. apply 関数をエクスポート——プラグインのエントリポイント
TYPESCRIPT
import { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  ctx.logger.info('my-plugin loaded!')
}

これだけで完全なプラグインです。フレームワークがロード後、apply(ctx) を呼び出し、ctx.logger.info() がログを出力します。

▶ サンプル 2:

100%
graph LR
    LOAD[Framework Loads Plugin] --> CALL[Call apply<br/>ctx is the plugin's "world"]
    CALL --> RUN[Plugin Running]
    UNLOAD[Plugin Unload] --> CLEAN[Resources registered on ctx<br/>automatically reclaimed]

apply はプラグインのロード時に1回だけ呼び出されます。プラグインが継続的に実行する必要がある場合、apply 内でタイマー、リスナー等を登録します。

(3) name の目的

name はプラグインのアイデンティティで、以下の用途に使われます:

TYPESCRIPT
export const name = 'my-plugin'

⚠️ name はグローバルに一意でなければなりません;既存のプラグインと同じ名前を使用するとロードに失敗します。


4. 3つのプラグイン形式

Cordis は3つのプラグイン記述スタイルをサポートします。機能的には等価;複雑さに応じて選択:

(1) 関数形式(最もシンプル)

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

export const name = 'hello-fn'

export function apply(ctx: Context) {
  ctx.logger.info('hello from function plugin')
}

ユースケース:シンプルなツール、1回限りの登録。

(2) オブジェクト形式

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

export default {
  name: 'hello-obj',
  apply(ctx: Context) {
    ctx.logger.info('hello from object plugin')
  }
}

ユースケース:中程度の複雑さのプラグインで、複数のフィールド(例:Configinject)のエクスポートが必要な場合。

(3) クラス形式

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

export default class HelloClass {
  static name = 'hello-class'

  constructor(private ctx: Context) {
    ctx.logger.info('hello from class plugin')
  }
}

ユースケース:複雑なプラグインで、内部状態管理やサービス基底クラスの実装が必要な場合。

(4) 3形式の比較

次元 関数 オブジェクト クラス
複雑さ
状態管理 クロージャ クロージャ インスタンスプロパティ
Config のエクスポート 個別エクスポート オブジェクトフィールド 静的プロパティ
継承 非サポート 非サポート サポート
最適な用途 ツールプラグイン 標準プラグイン サービスプラグイン

5. プラグインに「何かをさせる」

(1) 定期ログの登録

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

export const name = 'heartbeat'

export function apply(ctx: Context) {
  ctx.setInterval(() => {
    ctx.logger.info('heartbeat tick')
  }, 60000)
}

ctx.setInterval で登録されたタイマーは、プラグインのアンロード時に自動的にクリアされます——これが Cordis 自動クリーンアップの中核的利点です。

(2) イベントのリッスン

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

export const name = 'welcome'

export function apply(ctx: Context) {
  ctx.on('session/created', (session) => {
    ctx.logger.info(`new session: ${session.id}`)
  })
}

ctx.on で登録されたリスナーも、アンロード時に自動的に削除されます。

(3) コマンドの登録

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

export const name = 'hello-cmd'

export function apply(ctx: Context) {
  ctx.command('hello <name:text>')
    .action(({ session }, name) => {
      return `Hello, ${name}!`
    })
}

6. cordis.yml への登録とロード

(1) cordis.yml 設定

DSH プロジェクトルートの cordis.yml にローカルプラグインを登録:

YAML
plugins:
  my-plugin:
    $insert: /absolute/path/to/scratch-plugin

$insert はローカルプラグインをプラグインリストに注入します。パスは絶対パスでなければなりません。

(2) 絶対パスと相対パス

YAML
plugins:
  my-plugin:
    $insert: /home/alice/plugins/scratch-plugin   # ✅ 絶対パス
    # $insert: ./scratch-plugin                    # ⚠️ 相対パスは動作するが非推奨

絶対パスを推奨する理由:

(3) 起動とロード

BASH
pnpm dsh web --patch

--patch パラメータは DSH に cordis.yml から $insert その他のオーバーライド操作を読み取るよう指示し、ローカルプラグインをデフォルト設定の上にレイヤーします。

(4) ロードの確認

起動後、ターミナルログを確認:

TEXT 📖 参照専用
[my-plugin] loaded!

または Web UI のプラグイン一覧で my-plugin を検索。


▶ サンプル 7:Greeter プラグイン

上記の知識を組み合わせた完全な例:

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

export const name = 'greeter'

export function apply(ctx: Context) {
  ctx.logger.info('greeter plugin loaded')

  ctx.on('session/created', (session) => {
    ctx.logger.info(`session started: ${session.id}`)
  })

  ctx.setInterval(() => {
    ctx.logger.info('greeter heartbeat')
  }, 300000)
}

cordis.yml 設定:

YAML
plugins:
  greeter:
    $insert: /home/alice/projects/scratch-plugin

起動と確認:

BASH
pnpm dsh web --patch
# [greeter] greeter plugin loaded
# [greeter] session started: abc-123

❓ よくある質問

Q プラグイン名にハイフンを含められますか?
A はい、my-plugin は有効な名前です。小文字とハイフンを推奨;キャメルケースは避けてください。
Q apply 関数は async にできますか?
A はい。async function apply(ctx) は完全に有効;フレームワークは async apply を待機します。注意:async apply が完了するまで、プラグインは保留状態にあり、それに依存するプラグインはロードされません。
Q $insert パスが間違っているとどうなりますか?
A DSH はエラーを報告し、そのプラグインをスキップします;アプリケーション全体はクラッシュしません。ターミナルに [error] plugin not found: /wrong/path/to/plugin のようなメッセージが表示されます。
Q 関数形式のプラグインから Config をエクスポートするには?
A 個別にエクスポートします:typescript export const name = 'my-plugin' export const Config = Schema.object({ ... }) export function apply(ctx: Context) { ... }
Q 同じプラグインを複数回ロードできますか?
A デフォルトでは不可——name はグローバルに一意です。複数インスタンスが必要な場合は、isolate 設定で独立スコープを作成してください(20-scope.md を参照)。
Q ローカル開発中、コードを変更するたびに再起動が必要ですか?
A はい、pnpm dsh web --patch はホットリロードをサポートしていません。開発中は --dump-config で設定を確認するか、18-hot-reload.md の HMR 機構を参照してください。

📖 まとめ


📝 練習問題

1. ⭐ 基礎:このレッスンの手順に従って、関数形式の hello-world プラグインを作成し、apply 内で "hello world!" というログを出力し、cordis.yml に登録し、起動して確認してください。

2. ⭐⭐ 応用:hello-world プラグインをオブジェクト形式とクラス形式の両方で書き直し、それぞれ別々にロードして、3つすべてが同じ出力を生成することを確認してください。

3. ⭐⭐⭐ チャレンジuptime プラグインを書いて、プラグインのロード時刻を記録し、ctx.setInterval で毎分「Running for N minutes」と出力してください。考察:プラグインがアンロード後に再ロードされた場合、タイマーはリセットされるべきか?なぜ?

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%