11# 応用トピック
22
3+ ## Extension
4+
5+ Extension は、既存のプラグインを変更せずにルートやフックを追加します。ホストプラグインのプロセス内で実行されます(別プロセスではありません)。
6+
7+ ### Extension を使うべき場合
8+
9+ - 既存プラグインに新しいコマンドを追加したい
10+ - 他のプラグインのエントリーポイントにフックしたい
11+ - プラグイン内でモジュール化されたコード構成にしたい
12+
13+ ### Extension の作成
14+
15+ ``` python
16+ from plugin.sdk.extension import (
17+ NekoExtensionBase, extension, extension_entry, extension_hook,
18+ Ok, Err,
19+ )
20+
21+ @extension
22+ class MyExtension (NekoExtensionBase ):
23+ """ ホストプラグインに追加コマンドを提供します。"""
24+
25+ @extension_entry (id = " extra_command" , description = " An extra command added by extension" )
26+ def extra_command (self , param : str = " " , ** _ ):
27+ return Ok({" extended" : True , " param" : param})
28+
29+ @extension_hook (target = " original_entry" , timing = " before" )
30+ def validate (self , * , args , ** _ ):
31+ # ホストプラグインの "original_entry" の前に実行
32+ if not args.get(" required_field" ):
33+ return Err(" Missing required_field" )
34+ ```
35+
36+ ### Extension の仕組み
37+
38+ 1 . ホストが設定で Extension を登録する
39+ 2 . 起動時に、ホストが Extension を ` PluginRouter ` インスタンスとしてインジェクトする
40+ 3 . Extension のエントリーはホストプラグインの名前空間でアクセス可能になる
41+ 4 . Extension のフックがホストのエントリーポイントをインターセプトする
42+
43+ ---
44+
45+ ## Adapter
46+
47+ Adapter は外部プロトコル(MCP、NoneBot など)を内部プラグイン呼び出しにブリッジします。** ゲートウェイパイプライン** パターンを実装します。
48+
49+ ### Adapter を使うべき場合
50+
51+ - N.E.K.O. プラグインを MCP(Model Context Protocol)経由で公開したい
52+ - NoneBot メッセージを受け付けてプラグインにルーティングしたい
53+ - 外部プロトコルをプラグインシステムにブリッジしたい
54+
55+ ### Adapter ゲートウェイパイプライン
56+
57+ ```
58+ External Request → Normalizer → PolicyEngine → RouteEngine → PluginInvoker → ResponseSerializer → External Response
59+ ```
60+
61+ | ステージ | 責務 |
62+ | ---------| ------|
63+ | ** Normalizer** | 外部プロトコル形式を ` GatewayRequest ` に変換 |
64+ | ** PolicyEngine** | アクセス制御、レート制限、バリデーション |
65+ | ** RouteEngine** | 呼び出すプラグイン/エントリーを決定 |
66+ | ** PluginInvoker** | 実際のプラグイン呼び出しを実行 |
67+ | ** ResponseSerializer** | 結果を外部プロトコル形式に変換 |
68+
69+ ### Adapter の作成
70+
71+ ``` python
72+ from plugin.sdk.plugin import neko_plugin, plugin_entry, lifecycle, Ok, Err, SdkError
73+ from plugin.sdk.adapter import (
74+ AdapterGatewayCore, DefaultPolicyEngine, NekoAdapterPlugin,
75+ )
76+ from plugin.sdk.adapter.gateway_models import ExternalRequest
77+
78+ @neko_plugin
79+ class MyProtocolAdapter (NekoAdapterPlugin ):
80+ def __init__ (self , ctx ):
81+ super ().__init__ (ctx)
82+ self .gateway = None
83+
84+ @lifecycle (id = " startup" )
85+ async def startup (self , ** _ ):
86+ self .gateway = AdapterGatewayCore(
87+ normalizer = MyNormalizer(),
88+ policy_engine = DefaultPolicyEngine(),
89+ route_engine = MyRouteEngine(),
90+ invoker = MyInvoker(self .ctx),
91+ serializer = MySerializer(),
92+ logger = self .logger,
93+ )
94+ return Ok({" status" : " ready" })
95+
96+ @plugin_entry (id = " handle_request" )
97+ async def handle_request (self , raw_data : dict , ** _ ):
98+ external = ExternalRequest(protocol = " my_protocol" , raw = raw_data)
99+ response = await self .gateway.process(external)
100+ return Ok(response.to_dict())
101+ ```
102+
103+ ### Adapter モード
104+
105+ | モード | 説明 |
106+ | --------| ------|
107+ | ` GATEWAY ` | 完全なパイプライン処理 |
108+ | ` ROUTER ` | ルーティングのみ(ポリシーをスキップ) |
109+ | ` BRIDGE ` | 直接パススルー |
110+ | ` HYBRID ` | リクエストごとにモードを選択 |
111+
112+ ### 組み込みリファレンス: MCP Adapter
113+
114+ ` plugin/plugins/mcp_adapter/ ` に、MCP プロトコルを N.E.K.O. プラグインにブリッジする完全な Adapter 実装があります。以下を実演しています:
115+ - カスタム Normalizer(` MCPRequestNormalizer ` )
116+ - カスタム RouteEngine(` MCPRouteEngine ` )
117+ - カスタム Invoker(` MCPPluginInvoker ` )
118+ - カスタム Serializer(` MCPResponseSerializer ` )
119+ - カスタム Transport(` MCPTransportAdapter ` )
120+
121+ ---
122+
123+ ## プラグイン間通信
124+
125+ ### 直接エントリー呼び出し
126+
127+ ``` python
128+ # 他のプラグインのエントリーポイントを呼び出す
129+ result = await self .plugins.call_entry(" target_plugin:entry_id" , {" arg" : " value" })
130+
131+ if isinstance (result, Ok):
132+ data = result.value
133+ else :
134+ self .logger.error(f " Call failed: { result.error} " )
135+ ```
136+
137+ ### ディスカバリ
138+
139+ ``` python
140+ # 利用可能なすべてのプラグインを一覧表示
141+ plugins = await self .plugins.list(enabled = True )
142+
143+ # 依存関係が存在するか確認
144+ exists = await self .plugins.exists(" required_plugin" )
145+
146+ # プラグインを要求する(見つからない場合は即座に失敗)
147+ dep = await self .plugins.require_enabled(" required_plugin" )
148+ ```
149+
150+ ### イベントバス
151+
152+ ``` python
153+ # バス経由でイベントを発行
154+ self .bus.emit(" my_event" , {" key" : " value" })
155+
156+ # イベントをサブスクライブ(通常は startup で行う)
157+ self .bus.on(" some_event" , self ._handle_event)
158+ ```
159+
160+ ---
161+
3162## 非同期プログラミング
4163
5- プラグインのエントリーポイントは I/O 集約型の操作に非同期関数を使用できます :
164+ エントリーポイントは同期でも非同期でも定義できます :
6165
7166``` python
167+ # 同期エントリー(スレッドプールで実行)
168+ @plugin_entry (id = " sync_task" )
169+ def sync_task (self , ** _ ):
170+ return Ok({" result" : " done" })
171+
172+ # 非同期エントリー(イベントループで実行)
8173@plugin_entry (id = " async_task" )
9174async def async_task (self , url : str , ** _ ):
10175 async with aiohttp.ClientSession() as session:
11176 async with session.get(url) as response:
12- return {" data" : await response.json()}
177+ return Ok( {" data" : await response.json()})
13178```
14179
180+ ---
181+
15182## スレッドセーフティ
16183
17- プラグインがスレッド間で共有状態を使用する場合(例:タイマータスクが共有データにアクセスする場合)、ロックを使用してください :
184+ タイマータスクは別スレッドで実行されます。共有状態を保護してください :
18185
19186``` python
20187import threading
@@ -24,54 +191,49 @@ class ThreadSafePlugin(NekoPluginBase):
24191 def __init__ (self , ctx ):
25192 super ().__init__ (ctx)
26193 self ._lock = threading.Lock()
27- self ._shared_data = {}
194+ self ._counter = 0
28195
29- @plugin_entry (id = " update " )
30- def update (self , key : str , value : str , ** _ ):
196+ @plugin_entry (id = " increment " )
197+ def increment (self , ** _ ):
31198 with self ._lock:
32- self ._shared_data[key] = value
33- return {" updated" : True }
34- ```
35-
36- ## リトライ付きエラーハンドリング
199+ self ._counter += 1
200+ return Ok({" count" : self ._counter})
37201
38- 一時的な障害に対する自動リトライには ` tenacity ` を使用します:
39-
40- ``` python
41- from tenacity import retry, stop_after_attempt, wait_exponential
42-
43- @plugin_entry (id = " reliable_fetch" )
44- @retry (stop = stop_after_attempt(3 ), wait = wait_exponential(min = 4 , max = 10 ))
45- def reliable_fetch (self , url : str , ** _ ):
46- import requests
47- response = requests.get(url)
48- response.raise_for_status()
49- return {" data" : response.json()}
202+ @timer_interval (id = " report" , seconds = 60 , auto_start = True )
203+ def report (self , ** _ ):
204+ with self ._lock:
205+ count = self ._counter
206+ self .report_status({" count" : count})
50207```
51208
52- ## カスタム設定
209+ ---
53210
54- プラグイン固有の設定を ` plugin.toml ` の横に保存します:
211+ ## カスタム設定
55212
56213``` python
57214import json
58215
59216class ConfigurablePlugin (NekoPluginBase ):
60217 def __init__ (self , ctx ):
61218 super ().__init__ (ctx)
62- self .config_file = ctx.config_path.parent / " config.json"
63- self ._load_config()
64-
65- def _load_config (self ):
66- if self .config_file.exists():
67- self .config = json.loads(self .config_file.read_text())
219+ config_file = self .config_dir / " config.json"
220+ if config_file.exists():
221+ self .config = json.loads(config_file.read_text())
68222 else :
69223 self .config = {" timeout" : 30 }
224+ ```
70225
71- def _save_config (self ):
72- self .config_file.write_text(json.dumps(self .config, indent = 2 ))
226+ または、プロファイル付きの構造化された設定には ` PluginConfig ` を使用します:
227+
228+ ``` python
229+ from plugin.sdk.plugin import PluginConfig
230+
231+ config = PluginConfig(self .ctx)
232+ timeout = config.get(" timeout" , default = 30 )
73233```
74234
235+ ---
236+
75237## SQLite によるデータ永続化
76238
77239``` python
@@ -80,7 +242,8 @@ import sqlite3
80242class PersistentPlugin (NekoPluginBase ):
81243 def __init__ (self , ctx ):
82244 super ().__init__ (ctx)
83- self .db_path = ctx.config_path.parent / " data.db"
245+ self .db_path = self .data_path(" records.db" )
246+ self .data_path().mkdir(parents = True , exist_ok = True )
84247 self ._init_db()
85248
86249 def _init_db (self ):
@@ -95,15 +258,4 @@ class PersistentPlugin(NekoPluginBase):
95258 """ )
96259 conn.commit()
97260 conn.close()
98-
99- @plugin_entry (id = " save" )
100- def save (self , key : str , value : str , ** _ ):
101- conn = sqlite3.connect(self .db_path)
102- conn.execute(
103- " INSERT OR REPLACE INTO records (key, value) VALUES (?, ?)" ,
104- (key, value)
105- )
106- conn.commit()
107- conn.close()
108- return {" saved" : True }
109261```
0 commit comments