feat: add platform client documentation and examples

- Introduced platform client documentation in `docs/v4/clients/platform.md` detailing methods for sending messages, images, and managing group members.
- Added example plugins for LLM chat and database functionalities in `docs/v4/examples/README.md`, `docs/v4/examples/llm-chat/README.md`, and `docs/v4/examples/database/README.md`.
- Enhanced quickstart guide with links to new documentation and example plugins.
- Implemented runtime contract tests to ensure compatibility of public capabilities and hooks.
This commit is contained in:
whatevertogo
2026-03-13 21:09:13 +08:00
parent 1c4c9677bb
commit 1fec76eda1
11 changed files with 2712 additions and 1 deletions

View File

@@ -0,0 +1,478 @@
# 数据库插件示例
本示例演示如何使用数据库客户端存储和管理插件数据。
## 完整代码
### plugin.yaml
```yaml
name: database_demo
display_name: 数据库演示
desc: 演示数据库客户端的各种用法
author: your-name
version: 1.0.0
runtime:
python: "3.12"
components:
- class: main:DatabasePlugin
```
### main.py
```python
"""数据库插件示例。
功能演示:
- 用户设置存储
- 计数器
- 批量操作
- 数据查询
"""
from __future__ import annotations
from astrbot_sdk import Context, MessageEvent, Star, on_command
class DatabasePlugin(Star):
"""数据库演示插件。"""
# ==================== 用户设置 ====================
@on_command("set", description="设置用户配置")
async def set_config(self, event: MessageEvent, ctx: Context) -> None:
"""设置用户配置项。"""
args = event.text.removeprefix("/set").strip().split(maxsplit=1)
if len(args) < 2:
await event.reply("用法: /set <键名> <值>")
return
key, value = args
user_id = event.user_id or "unknown"
# 获取现有配置
config_key = f"user_config:{user_id}"
config = await ctx.db.get(config_key) or {}
# 更新配置
config[key] = value
await ctx.db.set(config_key, config)
await event.reply(f"已设置 {key} = {value}")
@on_command("get", description="获取用户配置")
async def get_config(self, event: MessageEvent, ctx: Context) -> None:
"""获取用户配置项。"""
key = event.text.removeprefix("/get").strip()
if not key:
await event.reply("用法: /get <键名>")
return
user_id = event.user_id or "unknown"
config_key = f"user_config:{user_id}"
config = await ctx.db.get(config_key) or {}
if key in config:
await event.reply(f"{key} = {config[key]}")
else:
await event.reply(f"未找到配置项: {key}")
@on_command("config", description="显示所有配置")
async def show_config(self, event: MessageEvent, ctx: Context) -> None:
"""显示用户的所有配置。"""
user_id = event.user_id or "unknown"
config_key = f"user_config:{user_id}"
config = await ctx.db.get(config_key)
if not config:
await event.reply("您还没有设置任何配置")
return
lines = ["📋 您的配置:"]
for key, value in config.items():
lines.append(f" {key} = {value}")
await event.reply("\n".join(lines))
# ==================== 计数器 ====================
@on_command("count", description="计数器 +1")
async def increment_counter(self, event: MessageEvent, ctx: Context) -> None:
"""计数器增加。"""
user_id = event.user_id or "unknown"
key = f"counter:{user_id}"
# 读取并增加
count = await ctx.db.get(key) or 0
count += 1
await ctx.db.set(key, count)
await event.reply(f"计数器: {count}")
@on_command("reset", description="重置计数器")
async def reset_counter(self, event: MessageEvent, ctx: Context) -> None:
"""重置计数器。"""
user_id = event.user_id or "unknown"
key = f"counter:{user_id}"
await ctx.db.delete(key)
await event.reply("计数器已重置")
# ==================== 待办事项 ====================
@on_command("todo", description="添加待办事项")
async def add_todo(self, event: MessageEvent, ctx: Context) -> None:
"""添加待办事项。"""
content = event.text.removeprefix("/todo").strip()
if not content:
await event.reply("用法: /todo <事项内容>")
return
user_id = event.user_id or "unknown"
# 获取现有待办列表
todo_key = f"todos:{user_id}"
todos = await ctx.db.get(todo_key) or []
# 添加新事项
todos.append({
"id": len(todos) + 1,
"content": content,
"done": False
})
await ctx.db.set(todo_key, todos)
await event.reply(f"已添加待办事项 #{len(todos)}")
@on_command("todos", description="显示待办列表")
async def show_todos(self, event: MessageEvent, ctx: Context) -> None:
"""显示待办列表。"""
user_id = event.user_id or "unknown"
todo_key = f"todos:{user_id}"
todos = await ctx.db.get(todo_key) or []
if not todos:
await event.reply("待办列表为空")
return
lines = ["📝 待办事项:"]
for todo in todos:
status = "" if todo.get("done") else ""
lines.append(f" {status} #{todo['id']} {todo['content']}")
await event.reply("\n".join(lines))
@on_command("done", description="标记待办完成")
async def complete_todo(self, event: MessageEvent, ctx: Context) -> None:
"""标记待办事项完成。"""
arg = event.text.removeprefix("/done").strip()
if not arg:
await event.reply("用法: /done <序号>")
return
try:
todo_id = int(arg)
except ValueError:
await event.reply("序号必须是数字")
return
user_id = event.user_id or "unknown"
todo_key = f"todos:{user_id}"
todos = await ctx.db.get(todo_key) or []
for todo in todos:
if todo.get("id") == todo_id:
todo["done"] = True
await ctx.db.set(todo_key, todos)
await event.reply(f"已完成 #{todo_id}")
return
await event.reply(f"未找到待办事项 #{todo_id}")
# ==================== 批量操作 ====================
@on_command("batch_set", description="批量设置测试数据")
async def batch_set(self, event: MessageEvent, ctx: Context) -> None:
"""批量写入数据演示。"""
user_id = event.user_id or "unknown"
# 批量写入
items = {
f"test:{user_id}:a": {"value": 1, "desc": "第一项"},
f"test:{user_id}:b": {"value": 2, "desc": "第二项"},
f"test:{user_id}:c": {"value": 3, "desc": "第三项"},
}
await ctx.db.set_many(items)
await event.reply(f"已批量写入 {len(items)} 条数据")
@on_command("batch_get", description="批量读取测试数据")
async def batch_get(self, event: MessageEvent, ctx: Context) -> None:
"""批量读取数据演示。"""
user_id = event.user_id or "unknown"
# 批量读取
keys = [f"test:{user_id}:a", f"test:{user_id}:b", f"test:{user_id}:c"]
values = await ctx.db.get_many(keys)
lines = ["📦 批量读取结果:"]
for key, value in values.items():
if value:
lines.append(f" {key}: {value.get('value')} - {value.get('desc')}")
else:
lines.append(f" {key}: 不存在")
await event.reply("\n".join(lines))
# ==================== 数据管理 ====================
@on_command("keys", description="列出所有键")
async def list_keys(self, event: MessageEvent, ctx: Context) -> None:
"""列出用户的所有数据键。"""
user_id = event.user_id or "unknown"
prefix = f"{user_id}:"
keys = await ctx.db.list(prefix)
if not keys:
await event.reply("没有找到数据")
return
lines = [f"🔑 数据键 ({len(keys)} 个):"]
for key in keys[:10]:
lines.append(f" {key}")
if len(keys) > 10:
lines.append(f" ... 还有 {len(keys) - 10}")
await event.reply("\n".join(lines))
@on_command("clear", description="清除所有数据")
async def clear_all(self, event: MessageEvent, ctx: Context) -> None:
"""清除用户的所有数据。"""
user_id = event.user_id or "unknown"
# 列出并删除所有键
keys = await ctx.db.list(f"{user_id}:")
for key in keys:
await ctx.db.delete(key)
await event.reply(f"已清除 {len(keys)} 条数据")
```
### requirements.txt
```
# 无额外依赖
```
## 功能说明
### 用户设置
```bash
# 设置配置
用户: /set theme dark
机器人: 已设置 theme = dark
用户: /set lang zh
机器人: 已设置 lang = zh
# 获取配置
用户: /get theme
机器人: theme = dark
# 显示所有配置
用户: /config
机器人:
📋 您的配置:
theme = dark
lang = zh
```
### 计数器
```bash
用户: /count
机器人: 计数器: 1
用户: /count
机器人: 计数器: 2
用户: /reset
机器人: 计数器已重置
```
### 待办事项
```bash
用户: /todo 买菜
机器人: 已添加待办事项 #1
用户: /todo 写作业
机器人: 已添加待办事项 #2
用户: /todos
机器人:
📝 待办事项:
#1 买菜
#2 写作业
用户: /done 1
机器人: 已完成 #1
用户: /todos
机器人:
📝 待办事项:
#1 买菜
#2 写作业
```
### 批量操作
```bash
用户: /batch_set
机器人: 已批量写入 3 条数据
用户: /batch_get
机器人:
📦 批量读取结果:
test:user1:a: 1 - 第一项
test:user1:b: 2 - 第二项
test:user1:c: 3 - 第三项
```
## 测试代码
### tests/test_plugin.py
```python
import pytest
from astrbot_sdk.testing import MockContext, MockMessageEvent
class TestDatabasePlugin:
"""数据库插件测试。"""
@pytest.mark.asyncio
async def test_set_and_get_config(self):
"""测试配置存取。"""
from main import DatabasePlugin
plugin = DatabasePlugin()
ctx = MockContext(plugin_id="test")
# 设置配置
event = MockMessageEvent(text="/set theme dark", context=ctx, user_id="user1")
await plugin.set_config(event, ctx)
# 获取配置
event2 = MockMessageEvent(text="/get theme", context=ctx, user_id="user1")
await plugin.get_config(event2, ctx)
assert "dark" in event2.replies[-1]
@pytest.mark.asyncio
async def test_counter(self):
"""测试计数器。"""
from main import DatabasePlugin
plugin = DatabasePlugin()
ctx = MockContext(plugin_id="test")
# 第一次计数
event1 = MockMessageEvent(text="/count", context=ctx, user_id="user1")
await plugin.increment_counter(event1, ctx)
assert "1" in event1.replies[-1]
# 第二次计数
event2 = MockMessageEvent(text="/count", context=ctx, user_id="user1")
await plugin.increment_counter(event2, ctx)
assert "2" in event2.replies[-1]
@pytest.mark.asyncio
async def test_todos(self):
"""测试待办事项。"""
from main import DatabasePlugin
plugin = DatabasePlugin()
ctx = MockContext(plugin_id="test")
# 添加待办
event1 = MockMessageEvent(text="/todo 测试事项", context=ctx, user_id="user1")
await plugin.add_todo(event1, ctx)
# 显示待办
event2 = MockMessageEvent(text="/todos", context=ctx, user_id="user1")
await plugin.show_todos(event2, ctx)
assert "测试事项" in event2.replies[-1]
# 完成待办
event3 = MockMessageEvent(text="/done 1", context=ctx, user_id="user1")
await plugin.complete_todo(event3, ctx)
@pytest.mark.asyncio
async def test_batch_operations(self):
"""测试批量操作。"""
from main import DatabasePlugin
plugin = DatabasePlugin()
ctx = MockContext(plugin_id="test")
# 批量写入
event1 = MockMessageEvent(text="/batch_set", context=ctx, user_id="user1")
await plugin.batch_set(event1, ctx)
assert "3" in event1.replies[-1]
# 验证数据
assert await ctx.router.db.get("test:user1:a") is not None
assert await ctx.router.db.get("test:user1:b") is not None
assert await ctx.router.db.get("test:user1:c") is not None
```
## 最佳实践
### 1. 使用有意义的键前缀
```python
# 推荐
"user_config:{user_id}" # 用户配置
"todos:{user_id}" # 待办事项
"counter:{user_id}" # 计数器
"cache:{type}:{id}" # 缓存数据
"temp:{session_id}" # 临时数据
```
### 2. 处理空值
```python
# 使用 or 提供默认值
config = await ctx.db.get(key) or {}
count = await ctx.db.get(key) or 0
todos = await ctx.db.get(key) or []
```
### 3. 限制数据大小
```python
# 只保留最近 N 条记录
history = history[-100:] # 最多 100 条
await ctx.db.set(key, history)
```
## 相关文档
- [DB 客户端文档](../clients/db.md)
- [API 参考](../api-reference.md)
- [快速开始](../quickstart.md)