Files
Auto_Bangumi/docs/ja/dev/plugins/concepts.md
Estrella Pan 3258fcca4b docs(4.0-P7): 插件开发文档拆分为总览加每个扩展点一页,补齐中英日三语
- docs/dev/plugins.md 改为总览与导航,旧内容拆入 docs/dev/plugins/
- 新增核心概念、配置表单、事件、前端挂载点、命令行、签名与分发、内置插件、示例页
- 每个扩展点一页,侧边栏由页面表生成
- 改正旧文档中下载器配置、hardlink path_map 与补链按钮的过时描述
2026-10-08 14:51:00 +02:00

114 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 基本概念
## ディレクトリ構成とマニフェスト
```
config/plugins/local/my-plugin/ # ディレクトリ名はマニフェストの id と同じにします
├── plugin.toml
├── my_plugin/ # パッケージ(または単一の my_plugin.py)。相対 import が使えます
│ └── __init__.py
├── web/ # 任意:フロントエンドコンポーネント(「フロントエンドスロット」を参照)
└── vendor/ # 任意:純 Python の依存(.so / .pyd / .dylib / .dll は禁止)
```
```toml
# plugin.toml
[plugin]
id = "my-plugin" # 小文字・数字・ハイフン。core と local は予約済み
name = "My plugin"
version = "0.1.0"
sdk = ">=0.5,<1" # 対応する ab_sdk のバージョン範囲
entry = "my_plugin:MyPlugin" # モジュール:Plugin サブクラス(プラグインディレクトリからの相対)
description = "一文の説明"
authors = ["me"]
permissions = ["network"] # ユーザーへの表示のみ。強制はされません
extension_points = ["notifier"] # 任意:署名付きカタログの表示にだけ使います
```
プラグインは AB と同じプロセスで動作し、すべての権限を持ちます。`permissions` はユーザーへの宣言であり、サンドボックスではありません。`ab-plugin validate` はマニフェスト、SDK のバージョン範囲、エントリモジュール、フロントエンドのエントリファイルを検証します。
## Plugin と設定
- `Plugin[設定モデル]` を継承し、`config_model` を設定します。AB はモデルでユーザー設定を検証します。プラグインは `self.config` で型付きのインスタンスを受け取ります。WebUI で不正な値を保存しようとすると拒否されます(HTTP 422)。
- `async setup()` は読み込み後に一度、`async teardown()` はアンロード前に一度呼ばれます。設定が変わると、teardown の後でプラグインを作り直し、setup を呼びます。`setup()` が例外を送出すると、プラグインはエラー状態になります。
- フォームについては [設定フォーム](/ja/dev/plugins/config-forms) を参照してください。
`self.ctx` は、プラグインから見えるホストの機能のすべてです。
| 属性 | 説明 |
| --- | --- |
| `plugin_id` | プラグイン id |
| `config` | 検証済みの設定モデルのインスタンス(`config_model` がなければ `None`) |
| `log` | `plugin.<id>` のプレフィックス付き logger |
| `kv` | プラグイン専用の永続キーバリューストア(`get` / `set` / `delete`)。値は JSON にできるものに限ります |
| `data_dir` | `config/plugin-data/<id>/`。初回アクセス時に作成されます |
| `bus` | イベントバス:`publish(event)`、`subscribe(kind, handler)` |
プラグインでは `ab_sdk` だけを import してください。`module.*` は import しないでください。ホストの内部実装であり、いつでも変更される可能性があります。
## 3 種類の拡張宣言
| デコレーター | 用途 |
| --- | --- |
| `@provider(point, id=...)` | 拡張ポイントが定める実装を返すファクトリメソッド。`id` はユーザーが選ぶときの名前です |
| `@hook(point, priority=..., timeout=...)` | ホストが宣言した filter / transform 拡張ポイントに接続します。同じ拡張ポイントでは `priority` の昇順、同順位ならプラグイン id の順に実行します。ユーザーは `plugins.hook_order` で順序を指定できます |
| `@subscribe(kind, timeout=...)` | イベントを購読します。`"*"` は全イベントです。`timeout` で 1 イベントの処理時間の上限(既定 30 秒)を変更できます |
デコレーターは目印を付けるだけです。拡張ポイント名が間違っていると、プラグインの読み込みが失敗し、プラグイン一覧に理由が表示されます。黙って無視されることはありません。
- **filter フック**は `Verdict`(または `bool`)を返します。1 つでも拒否すると、そこで打ち切ります。
- **transform フック**は前のフックの結果を受け取り、新しい値を返します。`None` は変更なしです。フックが受け取るのは凍結されたスナップショットです。変更したコピーは `dataclasses.replace` で作って返します。
## 分離とサーキットブレーカー
- フック、イベントハンドラー、定期タスク、ルートハンドラーにはタイムアウトがあります。
- プラグインの例外はホストに影響しません。同じプラグインが 5 回連続で失敗すると、AB は自動的に無効にし、プラグイン一覧に理由を表示します。そのプラグインの設定を変更すると、再度読み込みを試みます。
- プラグインの戻り値は、ブレーカーの保護下で検証されます。型が違えば失敗として扱い、ホストの既定の動作に戻します。
- 例外:`RenameSkipped` は「入力または設定に問題がある」ことを表し、ブレーカーには数えません。
## テスト
`ab_sdk.testing` を使うと、AB を起動せずにプラグインを作成して動かせます。
```python
from ab_sdk.testing import create_plugin
def test_filter(tmp_path):
plugin, ctx = create_plugin(MyPlugin, {"key": "k"}, data_dir=tmp_path)
...
assert ctx.bus.published == []
```
`create_plugin` はホストと同じ規則で設定を検証し、プラグインを作成します(`setup` は呼びません)。`ctx.bus` はプラグインが発行したイベントを記録します。`await ctx.bus.deliver(event)` で、イベントをプラグイン自身の購読者に同期的に配信できます。
Provider 系の拡張ポイントには、SDK がコントラクトテストスイートを用意しています。スイートを継承して `create()` を実装すると、pytest がテストケースを収集します。
| スイート | 検査対象 |
| --- | --- |
| `DownloaderContract` | ダウンローダークライアント。`behavioral = True` でログインやトレント追加などの動作も検査します |
| `RenameStrategyContract` | リネーム方式:トレント内の相対パスを返す、拡張子を保つ、結果が決定的 |
| `NotifierContract` | 通知チャンネル:成功時は `True`、バックエンドが拒否したときは例外ではなく `False` |
| `SearchSiteContract` | 検索サイト:URL に `%s` が 1 つ、パーサーは `mikan` または `tmdb` |
```python
from ab_sdk.testing import RenameStrategyContract, create_plugin
class TestMyStrategy(RenameStrategyContract):
def create(self):
plugin, _ = create_plugin(MyPlugin)
return plugin.strategy()
```
テストケースは同期関数です(内部で `asyncio.run` を使います)。pytest-asyncio は不要です。
## pip パッケージとして公開する
パッケージの `pyproject.toml` に entry point を宣言し、**トップレベルのパッケージ内**に `plugin.toml` を置きます。
```toml
[project.entry-points."autobangumi.plugins"]
my-plugin = "my_plugin:MyPlugin"
```
pip でインストールしたプラグインも未署名です。「未署名プラグインを許可」をオンにする必要があります。Docker イメージには pip でインストールできません。その場合はローカルディレクトリを使い、依存を `vendor/` に入れてください。署名付きプラグインは [署名と配布](/ja/dev/plugins/signing) を参照してください。