Skip to content

Commit c558783

Browse files
committed
feat(docs): enhance plugin documentation with Extensions and Adapters
- Added detailed sections on Extensions and Adapters, explaining their purpose, usage, and implementation examples. - Updated cross-plugin communication methods and async programming guidelines to reflect best practices. - Improved clarity and consistency in the documentation, ensuring it aligns with the latest SDK features.
1 parent 328952f commit c558783

23 files changed

Lines changed: 3677 additions & 2723 deletions

docs/ja/plugins/advanced.md

Lines changed: 197 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,187 @@
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")
9174
async 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
20187
import 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
57214
import json
58215

59216
class 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
80242
class 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

Comments
 (0)