DeepSeek Harness: 最初のプラグイン
最終更新:2026-08-31
最初のプラグインを書くことは、DSH を深く理解するための重要ステップ——「フレームワークを使う」から「フレームワークを拡張する」への跳躍です。このレッスンでは、ローカルプロジェクトの作成から始め、段階的にロード可能で実行可能な Cordis プラグインを完成させます。
name と apply をエクスポートするだけです。フレームワークが apply(ctx) を呼び出すと、プラグインは ctx を通じて機能を登録します;プラグインがアンロードされると、ctx に登録されたリソースは自動的に回収されます。
📋 前提知識:08-community-plugins.md の完了、プラグインエコシステムの概要に精通していること
1. 学習内容
- ローカルプラグインプロジェクト構造の作成
- プラグインの本質:apply 関数をエクスポートする TypeScript モジュール
export const nameとexport function apply(ctx)の意味- 3つのプラグイン形式:関数、オブジェクト、クラス
- cordis.yml への登録とロード
pnpm dsh web --patchでの起動と確認
2. ローカルプロジェクトの作成
(1) プロジェクトディレクトリの初期化
すべての DSH プラグインは本質的に Node.js パッケージです。ゼロからセットアップしましょう:
mkdir -p scratch-plugin/src
cd scratch-plugin
pnpm init
結果の package.json:
{
"name": "scratch-plugin",
"version": "0.1.0",
"main": "src/index.ts"
}
▶ サンプル 2:
pnpm add -D @deepseek-ai/cordis typescript
プロジェクト構造:
scratch-plugin/
├── src/
│ └── index.ts ← プラグインメインエントリ
├── package.json
└── node_modules/
(3) TypeScript 設定
tsconfig.json を作成:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
3. プラグインコアプロトコル
(1) 最小プラグイン
DSH プラグインは2つの条件を満たすだけで済みます:
name文字列をエクスポート——プラグインの一意識別子apply関数をエクスポート——プラグインのエントリポイント
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:
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 はプラグインのアイデンティティで、以下の用途に使われます:
- ログプレフィックス:
[my-plugin] loaded! - 設定名前空間:
plugins.my-plugin.config - 依存宣言:他のプラグインは名前で参照
export const name = 'my-plugin'
⚠️ name はグローバルに一意でなければなりません;既存のプラグインと同じ名前を使用するとロードに失敗します。
4. 3つのプラグイン形式
Cordis は3つのプラグイン記述スタイルをサポートします。機能的には等価;複雑さに応じて選択:
(1) 関数形式(最もシンプル)
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) オブジェクト形式
import { Context } from '@deepseek-ai/cordis'
export default {
name: 'hello-obj',
apply(ctx: Context) {
ctx.logger.info('hello from object plugin')
}
}
ユースケース:中程度の複雑さのプラグインで、複数のフィールド(例:Config、inject)のエクスポートが必要な場合。
(3) クラス形式
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) 定期ログの登録
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) イベントのリッスン
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) コマンドの登録
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 にローカルプラグインを登録:
plugins:
my-plugin:
$insert: /absolute/path/to/scratch-plugin
$insert はローカルプラグインをプラグインリストに注入します。パスは絶対パスでなければなりません。
(2) 絶対パスと相対パス
plugins:
my-plugin:
$insert: /home/alice/plugins/scratch-plugin # ✅ 絶対パス
# $insert: ./scratch-plugin # ⚠️ 相対パスは動作するが非推奨
絶対パスを推奨する理由:
- パス解決が作業ディレクトリに影響されない
- 異なる起動方法で一貫した動作
- デバッグ時に明確
(3) 起動とロード
pnpm dsh web --patch
--patch パラメータは DSH に cordis.yml から $insert その他のオーバーライド操作を読み取るよう指示し、ローカルプラグインをデフォルト設定の上にレイヤーします。
(4) ロードの確認
起動後、ターミナルログを確認:
[my-plugin] loaded!
または Web UI のプラグイン一覧で my-plugin を検索。
▶ サンプル 7:Greeter プラグイン
上記の知識を組み合わせた完全な例:
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 設定:
plugins:
greeter:
$insert: /home/alice/projects/scratch-plugin
起動と確認:
pnpm dsh web --patch
# [greeter] greeter plugin loaded
# [greeter] session started: abc-123
❓ よくある質問
my-plugin は有効な名前です。小文字とハイフンを推奨;キャメルケースは避けてください。async function apply(ctx) は完全に有効;フレームワークは async apply を待機します。注意:async apply が完了するまで、プラグインは保留状態にあり、それに依存するプラグインはロードされません。[error] plugin not found: /wrong/path/to/plugin のようなメッセージが表示されます。typescript export const name = 'my-plugin' export const Config = Schema.object({ ... }) export function apply(ctx: Context) { ... } pnpm dsh web --patch はホットリロードをサポートしていません。開発中は --dump-config で設定を確認するか、18-hot-reload.md の HMR 機構を参照してください。📖 まとめ
- プラグインは
name+apply(ctx)をエクスポートする TypeScript モジュール;フレームワークがロード時に apply を呼び出す ctxを通じてタイマー、イベントリスナー、コマンド等を登録;すべてアンロード時に自動回収- 3つのプラグイン形式:関数(最シンプル)、オブジェクト(標準)、クラス(複雑/継承が必要)
cordis.ymlで$insert+ 絶対パスでローカルプラグインを登録pnpm dsh web --patchでオーバーレイ設定を起動・ロード- name はグローバルに一意でなければならない;重複するとロード失敗
📝 練習問題
1. ⭐ 基礎:このレッスンの手順に従って、関数形式の hello-world プラグインを作成し、apply 内で "hello world!" というログを出力し、cordis.yml に登録し、起動して確認してください。
2. ⭐⭐ 応用:hello-world プラグインをオブジェクト形式とクラス形式の両方で書き直し、それぞれ別々にロードして、3つすべてが同じ出力を生成することを確認してください。
3. ⭐⭐⭐ チャレンジ:uptime プラグインを書いて、プラグインのロード時刻を記録し、ctx.setInterval で毎分「Running for N minutes」と出力してください。考察:プラグインがアンロード後に再ロードされた場合、タイマーはリセットされるべきか?なぜ?