mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: 添加 hello_plugin 示例,包含插件结构、命令处理和测试用例
This commit is contained in:
221
README.md
221
README.md
@@ -0,0 +1,221 @@
|
||||
# AstrBot SDK
|
||||
|
||||
面向 AstrBot 插件作者的 v4 SDK。它提供三件核心能力:
|
||||
|
||||
- 用 `Star`、`Context`、`MessageEvent` 编写插件
|
||||
- 用 `astrbot-sdk dev --local` / `--watch` 做本地调试
|
||||
- 用 `astrbot_sdk.testing` 写不依赖真实 Core 的插件测试
|
||||
|
||||
## 5 分钟跑通第一个插件
|
||||
|
||||
### 1. 创建插件骨架
|
||||
|
||||
```bash
|
||||
astrbot-sdk init my_plugin
|
||||
cd my_plugin
|
||||
```
|
||||
|
||||
生成后的目录结构:
|
||||
|
||||
```text
|
||||
my_plugin/
|
||||
├── README.md
|
||||
├── plugin.yaml
|
||||
├── requirements.txt
|
||||
├── main.py
|
||||
└── tests
|
||||
└── test_plugin.py
|
||||
```
|
||||
|
||||
### 2. 校验插件
|
||||
|
||||
```bash
|
||||
astrbot-sdk validate --plugin-dir .
|
||||
```
|
||||
|
||||
### 3. 本地运行一次
|
||||
|
||||
```bash
|
||||
astrbot-sdk dev --local --plugin-dir . --event-text hello
|
||||
```
|
||||
|
||||
### 4. 开启热重载
|
||||
|
||||
```bash
|
||||
astrbot-sdk dev --local --watch --plugin-dir . --event-text hello
|
||||
```
|
||||
|
||||
保存 `main.py` 后,本地 harness 会自动重载并重新派发这条消息。
|
||||
|
||||
## 最小插件示例
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Context, MessageEvent, Star, on_command
|
||||
|
||||
|
||||
class MyPlugin(Star):
|
||||
@on_command("hello", description="发送问候")
|
||||
async def hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
await event.reply("Hello, World!")
|
||||
```
|
||||
|
||||
对应 `plugin.yaml`:
|
||||
|
||||
```yaml
|
||||
name: my_plugin
|
||||
display_name: My Plugin
|
||||
desc: 一个最小可运行的 AstrBot SDK 插件
|
||||
author: your-name
|
||||
version: 0.1.0
|
||||
runtime:
|
||||
python: "3.12"
|
||||
components:
|
||||
- class: main:MyPlugin
|
||||
```
|
||||
|
||||
## 插件作者最常用的 API
|
||||
|
||||
### 事件和上下文
|
||||
|
||||
- `MessageEvent.text`: 当前消息文本
|
||||
- `MessageEvent.reply(text)`: 回复文本
|
||||
- `Context.plugin_id`: 当前插件 ID
|
||||
- `Context.logger`: 已绑定插件 ID 的日志器
|
||||
|
||||
### 平台能力
|
||||
|
||||
- `ctx.platform.send(session, text)`
|
||||
- `ctx.platform.send_image(session, image_url)`
|
||||
- `ctx.platform.send_chain(session, chain)`
|
||||
- `ctx.platform.get_members(session)`
|
||||
|
||||
### LLM
|
||||
|
||||
- `ctx.llm.chat(prompt)`
|
||||
- `ctx.llm.chat_raw(prompt, ...)`
|
||||
- `ctx.llm.stream_chat(prompt, ...)`
|
||||
|
||||
### 存储
|
||||
|
||||
- `ctx.db.get/set/delete/list`
|
||||
- `ctx.memory.save/get/delete/search`
|
||||
|
||||
### 插件元数据和 HTTP
|
||||
|
||||
- `ctx.metadata.get_current_plugin()`
|
||||
- `ctx.metadata.get_plugin_config()`
|
||||
- `ctx.http.register_api(...)`
|
||||
- `ctx.http.unregister_api(...)`
|
||||
- `ctx.http.list_apis()`
|
||||
|
||||
## 本地调试
|
||||
|
||||
### 单次派发
|
||||
|
||||
```bash
|
||||
astrbot-sdk dev --local --plugin-dir . --event-text hello
|
||||
```
|
||||
|
||||
### 交互模式
|
||||
|
||||
```bash
|
||||
astrbot-sdk dev --local --plugin-dir . --interactive
|
||||
```
|
||||
|
||||
交互模式支持:
|
||||
|
||||
- `/session <id>`
|
||||
- `/user <id>`
|
||||
- `/platform <name>`
|
||||
- `/group <id>`
|
||||
- `/private`
|
||||
- `/event <type>`
|
||||
- `/exit`
|
||||
|
||||
### 热重载
|
||||
|
||||
```bash
|
||||
astrbot-sdk dev --local --watch --plugin-dir . --interactive
|
||||
```
|
||||
|
||||
适合边改边测。代码变更后会自动重建插件运行时。
|
||||
|
||||
## 测试插件
|
||||
|
||||
最小测试示例:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.testing import MockContext, MockMessageEvent
|
||||
from main import MyPlugin
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hello_handler():
|
||||
plugin = MyPlugin()
|
||||
ctx = MockContext(plugin_id="my_plugin")
|
||||
event = MockMessageEvent(text="/hello", context=ctx)
|
||||
|
||||
await plugin.hello(event, ctx)
|
||||
|
||||
assert event.replies == ["Hello, World!"]
|
||||
ctx.platform.assert_sent("Hello, World!")
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_plugin.py -v
|
||||
```
|
||||
|
||||
## 示例插件
|
||||
|
||||
面向插件作者的最小示例在:
|
||||
|
||||
- [examples/hello_plugin/README.md](examples/hello_plugin/README.md)
|
||||
|
||||
仓库里的 `test_plugin/new` 是运行时/集成测试夹具,不是作者入门模板。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. `validate` 通过了,但 `dev` 还是失败
|
||||
|
||||
通常是组件导入或实例化阶段异常。现在错误信息会包含:
|
||||
|
||||
- 插件名
|
||||
- `plugin.yaml` 路径
|
||||
- `components[i].class`
|
||||
- 原始异常原因
|
||||
|
||||
优先检查:
|
||||
|
||||
- `plugin.yaml` 的 `components`
|
||||
- 导入路径是否真实存在
|
||||
- 组件类是否继承 `astrbot_sdk.Star`
|
||||
- `__init__()` 是否做了会抛异常的工作
|
||||
|
||||
### 2. handler 参数为什么无法注入
|
||||
|
||||
默认支持:
|
||||
|
||||
- 按类型注入:`MessageEvent`、`Context`
|
||||
- 按名字注入:`event`、`ctx`、`context`
|
||||
- 命令参数字典中的同名字段
|
||||
|
||||
如果参数不在这几类里,请自己从 `event` 或 `ctx` 取。
|
||||
|
||||
### 3. capability schema 校验失败怎么看
|
||||
|
||||
错误会明确指出:
|
||||
|
||||
- 哪个 capability
|
||||
- 输入还是输出校验失败
|
||||
- 具体字段路径
|
||||
- 期望类型和实际类型
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 先跑通 [examples/hello_plugin/README.md](examples/hello_plugin/README.md)
|
||||
2. 再看 `src-new/astrbot_sdk/testing.py` 里的 `PluginHarness` / `MockContext`
|
||||
3. 需要更复杂的 capability、HTTP、metadata 示例时,再参考 `test_plugin/new`
|
||||
|
||||
43
examples/hello_plugin/README.md
Normal file
43
examples/hello_plugin/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Hello Plugin
|
||||
|
||||
这是给 AstrBot SDK 插件作者准备的最小示例。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
hello_plugin/
|
||||
├── plugin.yaml
|
||||
├── requirements.txt
|
||||
├── main.py
|
||||
└── tests
|
||||
└── test_plugin.py
|
||||
```
|
||||
|
||||
## 能学到什么
|
||||
|
||||
- 如何定义一个 `Star` 插件
|
||||
- 如何注册命令 handler
|
||||
- 如何使用 `MessageEvent.reply()`
|
||||
- 如何从 `Context` 里读取当前插件元数据
|
||||
- 如何用 `MockContext` / `MockMessageEvent` 写插件测试
|
||||
|
||||
## 运行
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
astrbot-sdk validate --plugin-dir examples/hello_plugin
|
||||
astrbot-sdk dev --local --plugin-dir examples/hello_plugin --event-text hello
|
||||
astrbot-sdk dev --local --watch --plugin-dir examples/hello_plugin --event-text hello
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
python -m pytest examples/hello_plugin/tests/test_plugin.py -v
|
||||
```
|
||||
|
||||
## 代码说明
|
||||
|
||||
- `hello`: 最小命令,收到 `hello` 时回复 `Hello, World!`
|
||||
- `about`: 读取 `ctx.metadata.get_current_plugin()`,演示 capability 客户端的基础用法
|
||||
13
examples/hello_plugin/main.py
Normal file
13
examples/hello_plugin/main.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from astrbot_sdk import Context, MessageEvent, Star, on_command
|
||||
|
||||
|
||||
class HelloPlugin(Star):
|
||||
@on_command("hello", description="发送最小问候")
|
||||
async def hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
await event.reply("Hello, World!")
|
||||
|
||||
@on_command("about", description="返回当前插件信息")
|
||||
async def about(self, event: MessageEvent, ctx: Context) -> None:
|
||||
plugin = await ctx.metadata.get_current_plugin()
|
||||
display_name = plugin.display_name if plugin is not None else ctx.plugin_id
|
||||
await event.reply(f"我是 {display_name}")
|
||||
9
examples/hello_plugin/plugin.yaml
Normal file
9
examples/hello_plugin/plugin.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: hello_plugin
|
||||
display_name: Hello Plugin
|
||||
desc: 一个适合插件作者入门的最小示例插件
|
||||
author: your-name
|
||||
version: 0.1.0
|
||||
runtime:
|
||||
python: "3.12"
|
||||
components:
|
||||
- class: main:HelloPlugin
|
||||
1
examples/hello_plugin/requirements.txt
Normal file
1
examples/hello_plugin/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
27
examples/hello_plugin/tests/test_plugin.py
Normal file
27
examples/hello_plugin/tests/test_plugin.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.testing import MockContext, MockMessageEvent
|
||||
from main import HelloPlugin
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hello_handler() -> None:
|
||||
plugin = HelloPlugin()
|
||||
ctx = MockContext(plugin_id="hello_plugin")
|
||||
event = MockMessageEvent(text="/hello", context=ctx)
|
||||
|
||||
await plugin.hello(event, ctx)
|
||||
|
||||
assert event.replies == ["Hello, World!"]
|
||||
ctx.platform.assert_sent("Hello, World!")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_about_handler() -> None:
|
||||
plugin = HelloPlugin()
|
||||
ctx = MockContext(plugin_id="hello_plugin")
|
||||
event = MockMessageEvent(text="/about", context=ctx)
|
||||
|
||||
await plugin.about(event, ctx)
|
||||
|
||||
assert any("hello_plugin" in reply for reply in event.replies)
|
||||
@@ -599,6 +599,41 @@ def _render_init_main_py(*, plugin_name: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _render_init_readme(*, plugin_name: str) -> str:
|
||||
return dedent(
|
||||
f"""\
|
||||
# {plugin_name}
|
||||
|
||||
一个最小可运行的 AstrBot SDK v4 插件。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
.
|
||||
├── plugin.yaml
|
||||
├── requirements.txt
|
||||
├── main.py
|
||||
└── tests
|
||||
└── test_plugin.py
|
||||
```
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
astrbot-sdk validate --plugin-dir .
|
||||
astrbot-sdk dev --local --plugin-dir . --event-text hello
|
||||
astrbot-sdk dev --local --watch --plugin-dir . --event-text hello
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
python -m pytest tests/test_plugin.py -v
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _render_init_test_py(*, plugin_name: str) -> str:
|
||||
class_name = _class_name_for_plugin(plugin_name)
|
||||
return dedent(
|
||||
@@ -701,6 +736,10 @@ def _init_plugin(name: str) -> None:
|
||||
_render_init_main_py(plugin_name=plugin_name),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(target_dir / "README.md").write_text(
|
||||
_render_init_readme(plugin_name=plugin_name),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(target_dir / "tests" / "test_plugin.py").write_text(
|
||||
_render_init_test_py(plugin_name=plugin_name),
|
||||
encoding="utf-8",
|
||||
|
||||
14
test_plugin/new/README.md
Normal file
14
test_plugin/new/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# test_plugin/new
|
||||
|
||||
这个目录是运行时与集成测试夹具,不是给插件作者直接照抄的入门模板。
|
||||
|
||||
它的目标是覆盖更多 SDK surface,例如:
|
||||
|
||||
- 生命周期
|
||||
- LLM / DB / Memory / Platform / HTTP / Metadata client
|
||||
- 自定义 capability
|
||||
- schedule / event / message / command handler
|
||||
|
||||
如果你是在找最小可学习示例,请改看:
|
||||
|
||||
- `examples/hello_plugin/`
|
||||
@@ -55,6 +55,20 @@ def test_dev_help_lists_watch_option() -> None:
|
||||
assert "--watch" in process.stdout
|
||||
|
||||
|
||||
def test_init_plugin_template_includes_readme(tmp_path: Path, monkeypatch) -> None:
|
||||
from astrbot_sdk.cli import _init_plugin
|
||||
|
||||
target = tmp_path / "demo_plugin"
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
_init_plugin(target.name)
|
||||
|
||||
assert (target / "README.md").exists()
|
||||
assert "astrbot-sdk dev --local --watch" in (
|
||||
target / "README.md"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_harness_dispatches_sample_plugin() -> None:
|
||||
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness
|
||||
@@ -85,6 +99,21 @@ async def test_plugin_harness_supports_metadata_and_http_commands() -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example_hello_plugin_dispatches_commands() -> None:
|
||||
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness
|
||||
|
||||
plugin_dir = _repo_root() / "examples" / "hello_plugin"
|
||||
|
||||
async with PluginHarness(LocalRuntimeConfig(plugin_dir=plugin_dir)) as harness:
|
||||
hello_records = await harness.dispatch_text("hello")
|
||||
about_records = await harness.dispatch_text("about")
|
||||
|
||||
assert any(record.text == "Hello, World!" for record in hello_records)
|
||||
# about 命令返回 display_name "Hello Plugin",不是 name "hello_plugin"
|
||||
assert any("Hello Plugin" in (record.text or "") for record in about_records)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_harness_reports_component_load_errors(tmp_path: Path) -> None:
|
||||
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness, _PluginLoadError
|
||||
|
||||
Reference in New Issue
Block a user