mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
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:
467
docs/v4/api-reference.md
Normal file
467
docs/v4/api-reference.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# AstrBot SDK v4 API 参考
|
||||
|
||||
本文档提供 AstrBot SDK v4 的完整 API 参考。
|
||||
|
||||
## 目录
|
||||
|
||||
- [核心概念](#核心概念)
|
||||
- [顶层 API](#顶层-api)
|
||||
- [装饰器](#装饰器)
|
||||
- [Context 上下文](#context-上下文)
|
||||
- [MessageEvent 消息事件](#messageevent-消息事件)
|
||||
- [客户端 API](#客户端-api)
|
||||
- [错误处理](#错误处理)
|
||||
- [测试工具](#测试工具)
|
||||
|
||||
---
|
||||
|
||||
## 核心概念
|
||||
|
||||
AstrBot SDK v4 采用**协议优先**的设计,插件与宿主通过显式协议消息交互:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 插件代码 │
|
||||
├─────────────────┤
|
||||
│ Context │ ← 运行时上下文
|
||||
│ ├─ llm │ ← LLM 客户端
|
||||
│ ├─ memory │ ← 记忆客户端
|
||||
│ ├─ db │ ← 数据库客户端
|
||||
│ └─ platform │ ← 平台客户端
|
||||
├─────────────────┤
|
||||
│ CapabilityProxy│ ← 能力代理
|
||||
├─────────────────┤
|
||||
│ Peer │ ← 对等节点通信
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 顶层 API
|
||||
|
||||
从 `astrbot_sdk` 直接导入的推荐入口:
|
||||
|
||||
```python
|
||||
from astrbot_sdk import (
|
||||
Star, # 插件基类
|
||||
Context, # 运行时上下文
|
||||
MessageEvent, # 消息事件
|
||||
AstrBotError, # 错误类型
|
||||
on_command, # 命令装饰器
|
||||
on_message, # 消息装饰器
|
||||
on_event, # 事件装饰器
|
||||
on_schedule, # 定时任务装饰器
|
||||
provide_capability, # 能力提供装饰器
|
||||
require_admin, # 管理员权限装饰器
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 装饰器
|
||||
|
||||
### @on_command
|
||||
|
||||
注册命令处理器。
|
||||
|
||||
```python
|
||||
@on_command(
|
||||
command: str, # 命令名称
|
||||
*,
|
||||
aliases: list[str] | None = None, # 命令别名
|
||||
description: str | None = None, # 命令描述
|
||||
)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
@on_command("hello", aliases=["hi"], description="发送问候")
|
||||
async def hello(self, event: MessageEvent, ctx: Context):
|
||||
await event.reply("Hello!")
|
||||
```
|
||||
|
||||
### @on_message
|
||||
|
||||
注册消息处理器,支持正则匹配或关键词匹配。
|
||||
|
||||
```python
|
||||
@on_message(
|
||||
*,
|
||||
regex: str | None = None, # 正则表达式
|
||||
keywords: list[str] | None = None, # 关键词列表
|
||||
platforms: list[str] | None = None, # 平台过滤
|
||||
)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
@on_message(regex=r"^ping$")
|
||||
async def ping(self, event: MessageEvent):
|
||||
await event.reply("pong")
|
||||
|
||||
@on_message(keywords=["帮助", "help"])
|
||||
async def help_handler(self, event: MessageEvent):
|
||||
await event.reply("这是帮助信息...")
|
||||
```
|
||||
|
||||
### @on_event
|
||||
|
||||
注册事件处理器。
|
||||
|
||||
```python
|
||||
@on_event(event_type: str) # 事件类型
|
||||
```
|
||||
|
||||
**常见事件类型**:
|
||||
- `"message"` - 消息事件
|
||||
- `"group_join"` - 群加入事件
|
||||
- `"group_leave"` - 群退出事件
|
||||
- `"friend_add"` - 好友添加事件
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
@on_event("group_join")
|
||||
async def on_group_join(self, event: MessageEvent, ctx: Context):
|
||||
await ctx.platform.send(event.session_id, "欢迎加入群组!")
|
||||
```
|
||||
|
||||
### @on_schedule
|
||||
|
||||
注册定时任务。
|
||||
|
||||
```python
|
||||
@on_schedule(
|
||||
*,
|
||||
cron: str | None = None, # Cron 表达式
|
||||
interval_seconds: int | None = None, # 间隔秒数
|
||||
)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 每 60 秒执行一次
|
||||
@on_schedule(interval_seconds=60)
|
||||
async def heartbeat(self, ctx: Context):
|
||||
await ctx.db.set("last_heartbeat", {"time": "now"})
|
||||
|
||||
# 使用 cron 表达式(每天 9 点)
|
||||
@on_schedule(cron="0 9 * * *")
|
||||
async def daily_greeting(self, ctx: Context):
|
||||
pass
|
||||
```
|
||||
|
||||
### @require_admin
|
||||
|
||||
要求管理员权限才能执行。
|
||||
|
||||
```python
|
||||
@require_admin
|
||||
@on_command("admin")
|
||||
async def admin_only(self, event: MessageEvent):
|
||||
await event.reply("管理员命令已执行")
|
||||
```
|
||||
|
||||
### @provide_capability
|
||||
|
||||
声明插件对外暴露的能力。
|
||||
|
||||
```python
|
||||
@provide_capability(
|
||||
name: str, # 能力名称
|
||||
*,
|
||||
description: str, # 能力描述
|
||||
input_schema: dict | None = None, # 输入 JSON Schema
|
||||
output_schema: dict | None = None, # 输出 JSON Schema
|
||||
supports_stream: bool = False, # 是否支持流式
|
||||
cancelable: bool = False, # 是否可取消
|
||||
)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
@provide_capability(
|
||||
"demo.echo",
|
||||
description="回显输入文本",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"text": {"type": "string"}},
|
||||
"required": ["text"],
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {"echo": {"type": "string"}},
|
||||
},
|
||||
)
|
||||
async def echo_capability(self, payload: dict, ctx: Context, cancel_token):
|
||||
return {"echo": payload.get("text", "")}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context 上下文
|
||||
|
||||
运行时上下文,提供所有能力客户端。
|
||||
|
||||
```python
|
||||
class Context:
|
||||
llm: LLMClient # LLM 客户端
|
||||
memory: MemoryClient # 记忆客户端
|
||||
db: DBClient # 数据库客户端
|
||||
platform: PlatformClient # 平台客户端
|
||||
plugin_id: str # 插件 ID
|
||||
logger: Logger # 日志器
|
||||
cancel_token: CancelToken # 取消令牌
|
||||
```
|
||||
|
||||
### CancelToken
|
||||
|
||||
取消信号,用于处理中断请求。
|
||||
|
||||
```python
|
||||
class CancelToken:
|
||||
@property
|
||||
def cancelled(self) -> bool # 是否已取消
|
||||
|
||||
def cancel(self) -> None # 发送取消信号
|
||||
|
||||
async def wait(self) -> None # 等待取消
|
||||
|
||||
def raise_if_cancelled(self) -> None # 如果已取消则抛出异常
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
async def long_task(self, ctx: Context):
|
||||
for i in range(100):
|
||||
ctx.cancel_token.raise_if_cancelled() # 检查取消信号
|
||||
await asyncio.sleep(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MessageEvent 消息事件
|
||||
|
||||
消息事件对象,包含消息信息和操作方法。
|
||||
|
||||
```python
|
||||
class MessageEvent:
|
||||
text: str # 消息文本
|
||||
user_id: str | None # 用户 ID
|
||||
session_id: str # 会话 ID
|
||||
group_id: str | None # 群组 ID(私聊为 None)
|
||||
platform: str # 平台名称
|
||||
raw: dict # 原始消息数据
|
||||
```
|
||||
|
||||
### 方法
|
||||
|
||||
#### event.reply()
|
||||
|
||||
回复消息。
|
||||
|
||||
```python
|
||||
async def reply(self, text: str) -> None
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
await event.reply("收到您的消息!")
|
||||
```
|
||||
|
||||
#### event.plain_result()
|
||||
|
||||
创建纯文本结果。
|
||||
|
||||
```python
|
||||
def plain_result(self, text: str) -> MessageEventResult
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
return event.plain_result("处理完成")
|
||||
```
|
||||
|
||||
#### event.to_payload()
|
||||
|
||||
转换为字典格式。
|
||||
|
||||
```python
|
||||
def to_payload(self) -> dict[str, Any]
|
||||
```
|
||||
|
||||
#### event.session_ref
|
||||
|
||||
获取结构化会话引用。
|
||||
|
||||
```python
|
||||
@property
|
||||
def session_ref(self) -> SessionRef | None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 客户端 API
|
||||
|
||||
### LLMClient
|
||||
|
||||
[详细文档](clients/llm.md)
|
||||
|
||||
```python
|
||||
# 简单对话
|
||||
reply = await ctx.llm.chat("你好")
|
||||
|
||||
# 带历史对话
|
||||
reply = await ctx.llm.chat("继续", history=[
|
||||
{"role": "user", "content": "你好"},
|
||||
{"role": "assistant", "content": "你好!"},
|
||||
])
|
||||
|
||||
# 流式对话
|
||||
async for chunk in ctx.llm.stream_chat("讲个故事"):
|
||||
print(chunk, end="")
|
||||
```
|
||||
|
||||
### DBClient
|
||||
|
||||
[详细文档](clients/db.md)
|
||||
|
||||
```python
|
||||
# 读写数据
|
||||
await ctx.db.set("user:1", {"name": "张三"})
|
||||
data = await ctx.db.get("user:1")
|
||||
|
||||
# 前缀查询
|
||||
keys = await ctx.db.list("user:")
|
||||
|
||||
# 批量操作
|
||||
await ctx.db.set_many({"a": 1, "b": 2})
|
||||
values = await ctx.db.get_many(["a", "b"])
|
||||
```
|
||||
|
||||
### MemoryClient
|
||||
|
||||
[详细文档](clients/memory.md)
|
||||
|
||||
```python
|
||||
# 保存记忆
|
||||
await ctx.memory.save("user_pref", {"theme": "dark"})
|
||||
|
||||
# 语义搜索
|
||||
results = await ctx.memory.search("用户偏好")
|
||||
|
||||
# 精确获取
|
||||
pref = await ctx.memory.get("user_pref")
|
||||
```
|
||||
|
||||
### PlatformClient
|
||||
|
||||
[详细文档](clients/platform.md)
|
||||
|
||||
```python
|
||||
# 发送消息
|
||||
await ctx.platform.send(event.session_id, "你好")
|
||||
|
||||
# 发送图片
|
||||
await ctx.platform.send_image(event.session_id, "https://example.com/img.png")
|
||||
|
||||
# 获取群成员
|
||||
members = await ctx.platform.get_members(event.session_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### AstrBotError
|
||||
|
||||
统一的错误类型。
|
||||
|
||||
```python
|
||||
class AstrBotError(Exception):
|
||||
code: str # 错误码
|
||||
message: str # 错误消息
|
||||
hint: str # 解决建议
|
||||
retryable: bool # 是否可重试
|
||||
```
|
||||
|
||||
### 错误码
|
||||
|
||||
| 错误码 | 说明 | 可重试 |
|
||||
|--------|------|--------|
|
||||
| `llm_not_configured` | LLM 未配置 | 否 |
|
||||
| `capability_not_found` | 能力未找到 | 否 |
|
||||
| `permission_denied` | 权限不足 | 否 |
|
||||
| `invalid_input` | 输入无效 | 否 |
|
||||
| `cancelled` | 操作已取消 | 否 |
|
||||
| `capability_timeout` | 能力调用超时 | 是 |
|
||||
| `network_error` | 网络错误 | 是 |
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
from astrbot_sdk import AstrBotError
|
||||
|
||||
try:
|
||||
result = await ctx.llm.chat("hello")
|
||||
except AstrBotError as e:
|
||||
print(f"[{e.code}] {e.message}")
|
||||
if e.hint:
|
||||
print(f"建议: {e.hint}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试工具
|
||||
|
||||
### MockContext
|
||||
|
||||
用于单元测试的模拟上下文。
|
||||
|
||||
```python
|
||||
from astrbot_sdk.testing import MockContext, MockMessageEvent
|
||||
|
||||
ctx = MockContext(plugin_id="test")
|
||||
event = MockMessageEvent(text="hello", context=ctx)
|
||||
|
||||
# 模拟 LLM 响应
|
||||
ctx.llm.mock_response("你好!")
|
||||
|
||||
# 断言发送内容
|
||||
await event.reply("测试")
|
||||
ctx.platform.assert_sent("测试")
|
||||
```
|
||||
|
||||
### PluginHarness
|
||||
|
||||
完整的插件测试工具。
|
||||
|
||||
```python
|
||||
from astrbot_sdk.testing import PluginHarness, LocalRuntimeConfig
|
||||
|
||||
harness = PluginHarness(
|
||||
LocalRuntimeConfig(plugin_dir=Path("my-plugin"))
|
||||
)
|
||||
|
||||
async with harness:
|
||||
records = await harness.dispatch_text("hello")
|
||||
assert any(r.text for r in records)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [快速开始](quickstart.md)
|
||||
- [LLM 客户端文档](clients/llm.md)
|
||||
- [数据库客户端文档](clients/db.md)
|
||||
- [平台客户端文档](clients/platform.md)
|
||||
- [记忆客户端文档](clients/memory.md)
|
||||
- [架构设计](../../ARCHITECTURE.md)
|
||||
370
docs/v4/clients/db.md
Normal file
370
docs/v4/clients/db.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 数据库客户端
|
||||
|
||||
数据库客户端提供键值存储能力,用于持久化插件数据。
|
||||
|
||||
## 概述
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Context
|
||||
|
||||
# 通过 Context 访问
|
||||
ctx.db # DBClient 实例
|
||||
```
|
||||
|
||||
特点:
|
||||
- 数据永久存储,除非显式删除
|
||||
- 支持任意 JSON 数据类型
|
||||
- 支持前缀查询
|
||||
- 支持批量读写
|
||||
- 支持变更订阅
|
||||
|
||||
---
|
||||
|
||||
## 方法
|
||||
|
||||
### get()
|
||||
|
||||
获取指定键的值。
|
||||
|
||||
```python
|
||||
async def get(self, key: str) -> Any | None
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `key: str` - 数据键名
|
||||
|
||||
**返回**:`Any | None` - 存储的值,不存在则返回 `None`
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 获取数据
|
||||
data = await ctx.db.get("user_settings")
|
||||
if data:
|
||||
print(data["theme"])
|
||||
|
||||
# 获取不存在的键
|
||||
value = await ctx.db.get("nonexistent") # None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### set()
|
||||
|
||||
设置键值对。
|
||||
|
||||
```python
|
||||
async def set(self, key: str, value: Any) -> None
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `key: str` - 数据键名
|
||||
- `value: Any` - 要存储的 JSON 值
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 存储字典
|
||||
await ctx.db.set("user_settings", {
|
||||
"theme": "dark",
|
||||
"lang": "zh",
|
||||
"notifications": True
|
||||
})
|
||||
|
||||
# 存储列表
|
||||
await ctx.db.set("history", ["msg1", "msg2", "msg3"])
|
||||
|
||||
# 存储简单值
|
||||
await ctx.db.set("greeted", True)
|
||||
await ctx.db.set("count", 42)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### delete()
|
||||
|
||||
删除指定键的数据。
|
||||
|
||||
```python
|
||||
async def delete(self, key: str) -> None
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
await ctx.db.delete("user_settings")
|
||||
await ctx.db.delete("temp_data")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### list()
|
||||
|
||||
列出匹配前缀的所有键。
|
||||
|
||||
```python
|
||||
async def list(self, prefix: str | None = None) -> list[str]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `prefix: str | None` - 键前缀过滤,`None` 表示列出所有键
|
||||
|
||||
**返回**:`list[str]` - 匹配的键名列表
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 列出所有键
|
||||
all_keys = await ctx.db.list()
|
||||
# ["settings", "user:1", "user:2", "temp"]
|
||||
|
||||
# 列出前缀为 "user:" 的键
|
||||
user_keys = await ctx.db.list("user:")
|
||||
# ["user:1", "user:2"]
|
||||
|
||||
# 使用前缀组织数据
|
||||
await ctx.db.set("user:1", {"name": "张三"})
|
||||
await ctx.db.set("user:2", {"name": "李四"})
|
||||
await ctx.db.set("config:theme", "dark")
|
||||
|
||||
user_keys = await ctx.db.list("user:") # ["user:1", "user:2"]
|
||||
config_keys = await ctx.db.list("config:") # ["config:theme"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### get_many()
|
||||
|
||||
批量获取多个键的值。
|
||||
|
||||
```python
|
||||
async def get_many(self, keys: Sequence[str]) -> dict[str, Any | None]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `keys: Sequence[str]` - 要读取的键列表
|
||||
|
||||
**返回**:`dict[str, Any | None]` - 键值对字典,不存在的键值为 `None`
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 批量读取
|
||||
values = await ctx.db.get_many(["user:1", "user:2", "user:3"])
|
||||
|
||||
for key, value in values.items():
|
||||
if value is None:
|
||||
print(f"{key} 不存在")
|
||||
else:
|
||||
print(f"{key}: {value['name']}")
|
||||
|
||||
# 处理部分缺失的情况
|
||||
values = await ctx.db.get_many(["a", "b", "c"])
|
||||
# {"a": {"data": 1}, "b": None, "c": {"data": 3}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### set_many()
|
||||
|
||||
批量写入多个键值对。
|
||||
|
||||
```python
|
||||
async def set_many(
|
||||
self,
|
||||
items: Mapping[str, Any] | Sequence[tuple[str, Any]]
|
||||
) -> None
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `items` - 键值对集合(字典或二元组列表)
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 使用字典
|
||||
await ctx.db.set_many({
|
||||
"user:1": {"name": "张三", "age": 25},
|
||||
"user:2": {"name": "李四", "age": 30},
|
||||
"user:3": {"name": "王五", "age": 28}
|
||||
})
|
||||
|
||||
# 使用二元组列表
|
||||
await ctx.db.set_many([
|
||||
("counter:page_views", 100),
|
||||
("counter:unique_visitors", 42)
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### watch()
|
||||
|
||||
订阅 KV 变更事件(流式)。
|
||||
|
||||
```python
|
||||
def watch(self, prefix: str | None = None) -> AsyncIterator[dict[str, Any]]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `prefix: str | None` - 键前缀过滤,`None` 表示订阅所有键
|
||||
|
||||
**返回**:`AsyncIterator[dict]` - 变更事件流
|
||||
|
||||
**事件格式**:
|
||||
```python
|
||||
{
|
||||
"op": "set" | "delete", # 操作类型
|
||||
"key": str, # 变更的键
|
||||
"value": Any | None # 新值(delete 时为 None)
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 订阅所有变更
|
||||
async for event in ctx.db.watch():
|
||||
if event["op"] == "set":
|
||||
print(f"设置 {event['key']} = {event['value']}")
|
||||
else:
|
||||
print(f"删除 {event['key']}")
|
||||
|
||||
# 只订阅特定前缀
|
||||
async for event in ctx.db.watch("user:"):
|
||||
print(f"用户数据变更: {event['key']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1:用户设置存储
|
||||
|
||||
```python
|
||||
@on_command("settheme")
|
||||
async def set_theme(self, event: MessageEvent, ctx: Context):
|
||||
theme = event.text.split()[-1]
|
||||
user_id = event.user_id
|
||||
|
||||
# 读取现有设置
|
||||
settings = await ctx.db.get(f"settings:{user_id}") or {}
|
||||
settings["theme"] = theme
|
||||
|
||||
# 保存设置
|
||||
await ctx.db.set(f"settings:{user_id}", settings)
|
||||
await event.reply(f"已将主题设置为 {theme}")
|
||||
|
||||
@on_command("mytheme")
|
||||
async def get_theme(self, event: MessageEvent, ctx: Context):
|
||||
settings = await ctx.db.get(f"settings:{event.user_id}") or {}
|
||||
theme = settings.get("theme", "默认")
|
||||
await event.reply(f"当前主题: {theme}")
|
||||
```
|
||||
|
||||
### 场景 2:计数器
|
||||
|
||||
```python
|
||||
@on_command("count")
|
||||
async def count(self, event: MessageEvent, ctx: Context):
|
||||
key = f"counter:{event.user_id}"
|
||||
|
||||
# 读取并增加计数
|
||||
count = await ctx.db.get(key) or 0
|
||||
count += 1
|
||||
await ctx.db.set(key, count)
|
||||
|
||||
await event.reply(f"您已使用此命令 {count} 次")
|
||||
```
|
||||
|
||||
### 场景 3:批量用户管理
|
||||
|
||||
```python
|
||||
@on_command("listusers")
|
||||
async def list_users(self, event: MessageEvent, ctx: Context):
|
||||
# 列出所有用户键
|
||||
user_keys = await ctx.db.list("user:")
|
||||
|
||||
if not user_keys:
|
||||
await event.reply("暂无用户数据")
|
||||
return
|
||||
|
||||
# 批量获取用户数据
|
||||
users = await ctx.db.get_many(user_keys)
|
||||
|
||||
lines = ["用户列表:"]
|
||||
for key, data in users.items():
|
||||
if data:
|
||||
lines.append(f"- {data.get('name', '未知')}")
|
||||
|
||||
await event.reply("\n".join(lines))
|
||||
```
|
||||
|
||||
### 场景 4:缓存层
|
||||
|
||||
```python
|
||||
async def get_user_info(self, user_id: str, ctx: Context):
|
||||
# 先查缓存
|
||||
cache_key = f"cache:user:{user_id}"
|
||||
cached = await ctx.db.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
# 模拟从外部获取数据
|
||||
data = await self._fetch_from_api(user_id)
|
||||
|
||||
# 写入缓存
|
||||
await ctx.db.set(cache_key, data)
|
||||
return data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用前缀组织数据
|
||||
|
||||
```python
|
||||
# 推荐:使用有意义的键前缀
|
||||
"settings:{user_id}" # 用户设置
|
||||
"cache:{type}:{id}" # 缓存数据
|
||||
"counter:{name}" # 计数器
|
||||
"temp:{session_id}" # 临时数据
|
||||
|
||||
# 避免:无组织的键名
|
||||
"data"
|
||||
"info"
|
||||
"temp"
|
||||
```
|
||||
|
||||
### 2. 处理空值
|
||||
|
||||
```python
|
||||
# 使用 or 提供默认值
|
||||
data = await ctx.db.get("key") or {}
|
||||
count = await ctx.db.get("counter") or 0
|
||||
|
||||
# 或显式检查
|
||||
data = await ctx.db.get("key")
|
||||
if data is None:
|
||||
data = self._get_default()
|
||||
```
|
||||
|
||||
### 3. 批量操作减少调用
|
||||
|
||||
```python
|
||||
# 不好:多次单独调用
|
||||
for key, value in items:
|
||||
await ctx.db.set(key, value)
|
||||
|
||||
# 好:批量写入
|
||||
await ctx.db.set_many(items)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [API 参考](../api-reference.md)
|
||||
- [Memory 客户端](memory.md) - 语义搜索存储
|
||||
- [示例:数据库插件](../examples/database/)
|
||||
283
docs/v4/clients/llm.md
Normal file
283
docs/v4/clients/llm.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# LLM 客户端
|
||||
|
||||
LLM 客户端提供与大语言模型交互的能力。
|
||||
|
||||
## 概述
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Context
|
||||
|
||||
# 通过 Context 访问
|
||||
ctx.llm # LLMClient 实例
|
||||
```
|
||||
|
||||
LLM 客户端支持三种调用模式:
|
||||
- `chat()` - 简单对话,返回文本
|
||||
- `chat_raw()` - 完整响应,包含 usage 和 tool_calls
|
||||
- `stream_chat()` - 流式对话,逐块返回
|
||||
|
||||
---
|
||||
|
||||
## 方法
|
||||
|
||||
### chat()
|
||||
|
||||
发送聊天请求并返回文本响应。
|
||||
|
||||
```python
|
||||
async def chat(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
system: str | None = None,
|
||||
history: Sequence[ChatHistoryItem] | None = None,
|
||||
model: str | None = None,
|
||||
temperature: float | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str
|
||||
```
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `prompt` | `str` | 用户输入的提示文本 |
|
||||
| `system` | `str \| None` | 系统提示词 |
|
||||
| `history` | `list \| None` | 对话历史 |
|
||||
| `model` | `str \| None` | 指定模型名称 |
|
||||
| `temperature` | `float \| None` | 生成温度 (0-1) |
|
||||
| `**kwargs` | `Any` | 额外参数 |
|
||||
|
||||
**返回**:`str` - 生成的文本内容
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 简单对话
|
||||
reply = await ctx.llm.chat("你好")
|
||||
print(reply) # "你好!有什么可以帮助你的?"
|
||||
|
||||
# 带系统提示词
|
||||
reply = await ctx.llm.chat(
|
||||
"介绍一下自己",
|
||||
system="你是一个友好的助手,用简洁的语言回答"
|
||||
)
|
||||
|
||||
# 带历史对话
|
||||
history = [
|
||||
ChatMessage(role="user", content="我叫小明"),
|
||||
ChatMessage(role="assistant", content="你好小明!"),
|
||||
]
|
||||
reply = await ctx.llm.chat("你记得我的名字吗?", history=history)
|
||||
|
||||
# 控制生成温度
|
||||
reply = await ctx.llm.chat("写一首诗", temperature=0.8)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### chat_raw()
|
||||
|
||||
发送聊天请求并返回完整响应。
|
||||
|
||||
```python
|
||||
async def chat_raw(
|
||||
self,
|
||||
prompt: str,
|
||||
**kwargs: Any,
|
||||
) -> LLMResponse
|
||||
```
|
||||
|
||||
**返回**:`LLMResponse` - 完整响应对象
|
||||
|
||||
```python
|
||||
class LLMResponse:
|
||||
text: str # 生成的文本
|
||||
usage: dict | None # Token 使用统计
|
||||
finish_reason: str | None # 结束原因
|
||||
tool_calls: list[dict] # 工具调用列表
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
response = await ctx.llm.chat_raw(
|
||||
"写一首关于春天的诗",
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
print(f"生成文本: {response.text}")
|
||||
print(f"Token 使用: {response.usage}")
|
||||
# {'input_tokens': 15, 'output_tokens': 120}
|
||||
|
||||
print(f"结束原因: {response.finish_reason}")
|
||||
# "stop"
|
||||
|
||||
if response.tool_calls:
|
||||
for tool in response.tool_calls:
|
||||
print(f"工具调用: {tool['name']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### stream_chat()
|
||||
|
||||
流式聊天,逐块返回响应文本。
|
||||
|
||||
```python
|
||||
async def stream_chat(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
system: str | None = None,
|
||||
history: Sequence[ChatHistoryItem] | None = None,
|
||||
model: str | None = None,
|
||||
temperature: float | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncGenerator[str, None]
|
||||
```
|
||||
|
||||
**返回**:`AsyncGenerator[str, None]` - 文本块迭代器
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 实时输出生成内容
|
||||
async for chunk in ctx.llm.stream_chat("讲一个短故事"):
|
||||
print(chunk, end="", flush=True)
|
||||
print() # 换行
|
||||
|
||||
# 收集完整响应
|
||||
chunks = []
|
||||
async for chunk in ctx.llm.stream_chat("写一首诗"):
|
||||
chunks.append(chunk)
|
||||
full_text = "".join(chunks)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ChatMessage
|
||||
|
||||
对话消息模型,用于构建历史。
|
||||
|
||||
```python
|
||||
from astrbot_sdk.clients.llm import ChatMessage
|
||||
|
||||
message = ChatMessage(
|
||||
role="user", # "user", "assistant", "system"
|
||||
content="消息内容"
|
||||
)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
from astrbot_sdk.clients.llm import ChatMessage
|
||||
|
||||
history = [
|
||||
ChatMessage(role="user", content="你好"),
|
||||
ChatMessage(role="assistant", content="你好!"),
|
||||
ChatMessage(role="user", content="今天天气怎么样?"),
|
||||
]
|
||||
|
||||
reply = await ctx.llm.chat("继续聊", history=history)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1:智能问答
|
||||
|
||||
```python
|
||||
@on_command("ask")
|
||||
async def ask(self, event: MessageEvent, ctx: Context):
|
||||
question = event.text.removeprefix("/ask").strip()
|
||||
if not question:
|
||||
await event.reply("请输入问题,如:/ask 什么是人工智能?")
|
||||
return
|
||||
|
||||
reply = await ctx.llm.chat(question)
|
||||
await event.reply(reply)
|
||||
```
|
||||
|
||||
### 场景 2:流式回复
|
||||
|
||||
```python
|
||||
@on_command("chat")
|
||||
async def chat(self, event: MessageEvent, ctx: Context):
|
||||
prompt = event.text.removeprefix("/chat").strip()
|
||||
|
||||
# 流式回复,实时显示
|
||||
reply_text = ""
|
||||
async for chunk in ctx.llm.stream_chat(prompt):
|
||||
reply_text += chunk
|
||||
# 可以选择实时更新消息或最后一次性发送
|
||||
pass
|
||||
|
||||
await event.reply(reply_text)
|
||||
```
|
||||
|
||||
### 场景 3:带上下文的对话
|
||||
|
||||
```python
|
||||
@on_command("continue")
|
||||
async def continue_chat(self, event: MessageEvent, ctx: Context):
|
||||
# 从数据库加载历史
|
||||
history = await ctx.db.get("chat_history") or []
|
||||
|
||||
# 添加当前消息
|
||||
prompt = event.text.removeprefix("/continue").strip()
|
||||
reply = await ctx.llm.chat(prompt, history=history)
|
||||
|
||||
# 保存更新后的历史
|
||||
history.append({"role": "user", "content": prompt})
|
||||
history.append({"role": "assistant", "content": reply})
|
||||
await ctx.db.set("chat_history", history[-10:]) # 保留最近 10 条
|
||||
|
||||
await event.reply(reply)
|
||||
```
|
||||
|
||||
### 场景 4:指定模型和参数
|
||||
|
||||
```python
|
||||
@on_command("creative")
|
||||
async def creative(self, event: MessageEvent, ctx: Context):
|
||||
prompt = event.text.removeprefix("/creative").strip()
|
||||
|
||||
# 使用更高的温度增加创造性
|
||||
reply = await ctx.llm.chat(
|
||||
prompt,
|
||||
temperature=0.9,
|
||||
system="你是一个富有创意的作家"
|
||||
)
|
||||
await event.reply(reply)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Token 限制**:注意对话历史不要过长,可能会超出模型上下文限制
|
||||
2. **错误处理**:LLM 调用可能失败,建议添加错误处理
|
||||
3. **超时**:长文本生成可能需要较长时间
|
||||
|
||||
```python
|
||||
from astrbot_sdk import AstrBotError
|
||||
|
||||
try:
|
||||
reply = await ctx.llm.chat("hello")
|
||||
except AstrBotError as e:
|
||||
if e.code == "llm_not_configured":
|
||||
await event.reply("LLM 未配置,请联系管理员")
|
||||
else:
|
||||
await event.reply(f"LLM 调用失败: {e.message}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [API 参考](../api-reference.md)
|
||||
- [快速开始](../quickstart.md)
|
||||
- [示例:LLM 对话插件](../examples/llm-chat/)
|
||||
309
docs/v4/clients/memory.md
Normal file
309
docs/v4/clients/memory.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# 记忆客户端
|
||||
|
||||
记忆客户端提供 AI 记忆存储能力,支持语义搜索。
|
||||
|
||||
## 概述
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Context
|
||||
|
||||
# 通过 Context 访问
|
||||
ctx.memory # MemoryClient 实例
|
||||
```
|
||||
|
||||
### Memory vs DB 的区别
|
||||
|
||||
| 特性 | DBClient | MemoryClient |
|
||||
|------|----------|--------------|
|
||||
| 存储方式 | 键值存储 | 语义向量存储 |
|
||||
| 检索方式 | 精确匹配 | 语义搜索 |
|
||||
| 适用场景 | 配置、计数器、简单数据 | AI 上下文、用户偏好、对话记忆 |
|
||||
|
||||
**选择建议**:
|
||||
- 需要精确键查找 → 使用 `db`
|
||||
- 需要语义搜索 → 使用 `memory`
|
||||
|
||||
---
|
||||
|
||||
## 方法
|
||||
|
||||
### save()
|
||||
|
||||
保存记忆项。
|
||||
|
||||
```python
|
||||
async def save(
|
||||
self,
|
||||
key: str,
|
||||
value: dict[str, Any] | None = None,
|
||||
**extra: Any,
|
||||
) -> None
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `key: str` - 记忆项的唯一标识键
|
||||
- `value: dict | None` - 要存储的数据字典
|
||||
- `**extra: Any` - 额外的键值对
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 保存用户偏好
|
||||
await ctx.memory.save("user_pref", {
|
||||
"theme": "dark",
|
||||
"language": "zh",
|
||||
"interests": ["游戏", "音乐"]
|
||||
})
|
||||
|
||||
# 使用关键字参数
|
||||
await ctx.memory.save(
|
||||
"note:1",
|
||||
None,
|
||||
content="重要笔记",
|
||||
tags=["work", "urgent"],
|
||||
created_at="2024-01-01"
|
||||
)
|
||||
|
||||
# 保存对话摘要
|
||||
await ctx.memory.save("conversation:session_123", {
|
||||
"summary": "用户询问了天气,推荐了晴天出行",
|
||||
"topics": ["天气", "出行"],
|
||||
"sentiment": "positive"
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### get()
|
||||
|
||||
精确获取单个记忆项。
|
||||
|
||||
```python
|
||||
async def get(self, key: str) -> dict[str, Any] | None
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `key: str` - 记忆项的唯一键
|
||||
|
||||
**返回**:`dict | None` - 记忆内容,不存在则返回 `None`
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 获取用户偏好
|
||||
pref = await ctx.memory.get("user_pref")
|
||||
if pref:
|
||||
print(f"用户偏好主题: {pref.get('theme')}")
|
||||
print(f"用户兴趣: {pref.get('interests')}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### search()
|
||||
|
||||
语义搜索记忆项。
|
||||
|
||||
```python
|
||||
async def search(self, query: str) -> list[dict[str, Any]]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `query: str` - 搜索查询文本
|
||||
|
||||
**返回**:`list[dict]` - 匹配的记忆项列表,按相关度排序
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 搜索用户偏好相关记忆
|
||||
results = await ctx.memory.search("用户喜欢什么颜色")
|
||||
for item in results:
|
||||
print(f"键: {item['key']}")
|
||||
print(f"内容: {item['content']}")
|
||||
print(f"相关度: {item.get('score', 0)}")
|
||||
print("---")
|
||||
|
||||
# 搜索对话历史
|
||||
results = await ctx.memory.search("之前讨论过天气吗")
|
||||
if results:
|
||||
await event.reply("是的,我们之前讨论过天气话题")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### delete()
|
||||
|
||||
删除记忆项。
|
||||
|
||||
```python
|
||||
async def delete(self, key: str) -> None
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 删除过期记忆
|
||||
await ctx.memory.delete("old_note")
|
||||
|
||||
# 删除用户数据
|
||||
await ctx.memory.delete(f"user_data:{user_id}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1:用户偏好记忆
|
||||
|
||||
```python
|
||||
@on_command("remember")
|
||||
async def remember_preference(self, event: MessageEvent, ctx: Context):
|
||||
"""记住用户偏好"""
|
||||
preference = event.text.removeprefix("/remember").strip()
|
||||
|
||||
# 保存偏好
|
||||
key = f"pref:{event.user_id}"
|
||||
prefs = await ctx.memory.get(key) or {"items": []}
|
||||
prefs["items"].append(preference)
|
||||
await ctx.memory.save(key, prefs)
|
||||
|
||||
await event.reply(f"已记住:{preference}")
|
||||
|
||||
@on_command("what_do_i_like")
|
||||
async def recall_preference(self, event: MessageEvent, ctx: Context):
|
||||
"""回忆用户偏好"""
|
||||
query = "用户偏好 喜欢"
|
||||
results = await ctx.memory.search(query)
|
||||
|
||||
if results:
|
||||
lines = ["您之前告诉过我:"]
|
||||
for item in results[:3]:
|
||||
lines.append(f"- {item.get('content', '未知')}")
|
||||
await event.reply("\n".join(lines))
|
||||
else:
|
||||
await event.reply("我还没有记住您的偏好")
|
||||
```
|
||||
|
||||
### 场景 2:对话上下文记忆
|
||||
|
||||
```python
|
||||
@on_message(keywords=["我"])
|
||||
async def track_context(self, event: MessageEvent, ctx: Context):
|
||||
"""跟踪用户提到的个人信息"""
|
||||
# 保存到记忆
|
||||
await ctx.memory.save(
|
||||
f"user_info:{event.user_id}:{event.session_id}",
|
||||
{
|
||||
"message": event.text,
|
||||
"timestamp": "2024-01-01",
|
||||
"type": "personal_info"
|
||||
}
|
||||
)
|
||||
|
||||
@on_command("recall")
|
||||
async def recall_context(self, event: MessageEvent, ctx: Context):
|
||||
"""回忆对话内容"""
|
||||
query = event.text.removeprefix("/recall").strip() or "用户说过什么"
|
||||
|
||||
results = await ctx.memory.search(query)
|
||||
if results:
|
||||
await event.reply(f"您之前提到:{results[0].get('message', '未知')}")
|
||||
else:
|
||||
await event.reply("我没有找到相关记忆")
|
||||
```
|
||||
|
||||
### 场景 3:智能推荐
|
||||
|
||||
```python
|
||||
@on_command("recommend")
|
||||
async def recommend(self, event: MessageEvent, ctx: Context):
|
||||
"""基于记忆的智能推荐"""
|
||||
# 搜索用户兴趣相关的记忆
|
||||
interests = await ctx.memory.search(f"{event.user_id} 兴趣 爱好")
|
||||
|
||||
if not interests:
|
||||
await event.reply("告诉我您的兴趣,我可以给您推荐内容!")
|
||||
return
|
||||
|
||||
# 基于兴趣生成推荐
|
||||
interest_text = ", ".join(
|
||||
item.get("content", "")
|
||||
for item in interests[:3]
|
||||
)
|
||||
|
||||
prompt = f"用户喜欢 {interest_text},推荐一些相关内容"
|
||||
recommendation = await ctx.llm.chat(prompt)
|
||||
await event.reply(recommendation)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用结构化键名
|
||||
|
||||
```python
|
||||
# 推荐:有层次结构的键名
|
||||
"user:{user_id}:preferences"
|
||||
"user:{user_id}:history:{session_id}"
|
||||
"conversation:{session_id}:summary"
|
||||
|
||||
# 避免:无组织的键名
|
||||
"data"
|
||||
"info"
|
||||
"temp"
|
||||
```
|
||||
|
||||
### 2. 为搜索优化内容
|
||||
|
||||
```python
|
||||
# 好:包含可搜索的描述性文本
|
||||
await ctx.memory.save("user_pref", {
|
||||
"description": "用户喜欢玩游戏和听音乐",
|
||||
"interests": ["游戏", "音乐"],
|
||||
"level": "advanced"
|
||||
})
|
||||
|
||||
# 不好:过于抽象,难以语义搜索
|
||||
await ctx.memory.save("user_pref", {
|
||||
"a": ["x", "y"],
|
||||
"b": 2
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 结合 DB 和 Memory
|
||||
|
||||
```python
|
||||
# DB:存储精确配置
|
||||
await ctx.db.set("config:theme", "dark")
|
||||
|
||||
# Memory:存储语义可搜索的内容
|
||||
await ctx.memory.save("user_interests", {
|
||||
"description": "用户对游戏开发感兴趣",
|
||||
"tags": ["游戏", "开发", "Unity"]
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **值必须是字典**:`memory.save()` 的 value 参数必须是 `dict` 类型
|
||||
|
||||
```python
|
||||
# 正确
|
||||
await ctx.memory.save("key", {"value": 123})
|
||||
|
||||
# 错误
|
||||
await ctx.memory.save("key", 123) # TypeError
|
||||
```
|
||||
|
||||
2. **语义搜索依赖宿主实现**:搜索质量取决于宿主的向量存储配置
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [API 参考](../api-reference.md)
|
||||
- [DB 客户端](db.md) - 精确键值存储
|
||||
- [LLM 客户端](llm.md) - 结合 AI 能力
|
||||
320
docs/v4/clients/platform.md
Normal file
320
docs/v4/clients/platform.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# 平台客户端
|
||||
|
||||
平台客户端提供向聊天平台发送消息和获取信息的能力。
|
||||
|
||||
## 概述
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Context
|
||||
|
||||
# 通过 Context 访问
|
||||
ctx.platform # PlatformClient 实例
|
||||
```
|
||||
|
||||
支持的平台能力:
|
||||
- `send()` - 发送文本消息
|
||||
- `send_image()` - 发送图片
|
||||
- `send_chain()` - 发送富消息链
|
||||
- `get_members()` - 获取群成员
|
||||
|
||||
---
|
||||
|
||||
## 方法
|
||||
|
||||
### send()
|
||||
|
||||
发送文本消息。
|
||||
|
||||
```python
|
||||
async def send(
|
||||
self,
|
||||
session: str | SessionRef,
|
||||
text: str
|
||||
) -> dict[str, Any]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `session: str | SessionRef` - 目标会话标识
|
||||
- `text: str` - 要发送的文本内容
|
||||
|
||||
**返回**:`dict` - 发送结果,可能包含消息 ID 等
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 发送到当前会话
|
||||
await ctx.platform.send(event.session_id, "收到您的消息!")
|
||||
|
||||
# 发送到指定用户(需要知道 session_id)
|
||||
await ctx.platform.send("qq:bot:123456", "私信消息")
|
||||
|
||||
# 使用 event.target
|
||||
if event.target:
|
||||
await ctx.platform.send(event.target, "回复到引用的消息来源")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### send_image()
|
||||
|
||||
发送图片消息。
|
||||
|
||||
```python
|
||||
async def send_image(
|
||||
self,
|
||||
session: str | SessionRef,
|
||||
image_url: str
|
||||
) -> dict[str, Any]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `session: str | SessionRef` - 目标会话标识
|
||||
- `image_url: str` - 图片 URL 或本地文件路径
|
||||
|
||||
**返回**:`dict` - 发送结果
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 发送网络图片
|
||||
await ctx.platform.send_image(
|
||||
event.session_id,
|
||||
"https://example.com/image.png"
|
||||
)
|
||||
|
||||
# 发送本地图片
|
||||
await ctx.platform.send_image(
|
||||
event.session_id,
|
||||
"/path/to/local/image.jpg"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### send_chain()
|
||||
|
||||
发送富消息链。
|
||||
|
||||
```python
|
||||
async def send_chain(
|
||||
self,
|
||||
session: str | SessionRef,
|
||||
chain: list[dict[str, Any]]
|
||||
) -> dict[str, Any]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `session: str | SessionRef` - 目标会话标识
|
||||
- `chain: list[dict]` - 消息组件数组
|
||||
|
||||
**返回**:`dict` - 发送结果
|
||||
|
||||
**消息组件格式**:
|
||||
|
||||
```python
|
||||
# 纯文本
|
||||
{"type": "Plain", "text": "文本内容"}
|
||||
|
||||
# 图片
|
||||
{"type": "Image", "file": "https://example.com/img.png"}
|
||||
|
||||
# @某人
|
||||
{"type": "At", "user_id": "123456"}
|
||||
|
||||
# 表情
|
||||
{"type": "Face", "id": "123"}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# 发送混合内容
|
||||
await ctx.platform.send_chain(event.session_id, [
|
||||
{"type": "Plain", "text": "你好!"},
|
||||
{"type": "Image", "file": "https://example.com/welcome.png"},
|
||||
{"type": "Plain", "text": "欢迎加入群组"}
|
||||
])
|
||||
|
||||
# @用户并发送消息
|
||||
await ctx.platform.send_chain(event.session_id, [
|
||||
{"type": "At", "user_id": event.user_id},
|
||||
{"type": "Plain", "text": " 这是一条通知消息"}
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### get_members()
|
||||
|
||||
获取群组成员列表。
|
||||
|
||||
```python
|
||||
async def get_members(
|
||||
self,
|
||||
session: str | SessionRef
|
||||
) -> list[dict[str, Any]]
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `session: str | SessionRef` - 群组会话标识
|
||||
|
||||
**返回**:`list[dict]` - 成员信息列表
|
||||
|
||||
**成员信息格式**:
|
||||
```python
|
||||
{
|
||||
"user_id": str, # 用户 ID
|
||||
"nickname": str, # 昵称
|
||||
"role": str, # 角色: "owner", "admin", "member"
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
@on_command("members")
|
||||
async def list_members(self, event: MessageEvent, ctx: Context):
|
||||
# 仅群聊有效
|
||||
if not event.group_id:
|
||||
await event.reply("此命令仅在群聊中可用")
|
||||
return
|
||||
|
||||
members = await ctx.platform.get_members(event.session_id)
|
||||
|
||||
lines = [f"群成员 ({len(members)} 人):"]
|
||||
for member in members[:10]: # 只显示前 10 个
|
||||
role = f"[{member.get('role', 'member')}]"
|
||||
name = member.get('nickname', member.get('user_id', '未知'))
|
||||
lines.append(f" {role} {name}")
|
||||
|
||||
if len(members) > 10:
|
||||
lines.append(f" ... 还有 {len(members) - 10} 人")
|
||||
|
||||
await event.reply("\n".join(lines))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SessionRef
|
||||
|
||||
结构化会话引用,用于精确指定消息目标。
|
||||
|
||||
```python
|
||||
from astrbot_sdk.protocol.descriptors import SessionRef
|
||||
|
||||
ref = SessionRef(
|
||||
platform="qq", # 平台名称
|
||||
instance="bot1", # 实例标识
|
||||
user_id="123456", # 用户 ID
|
||||
group_id="654321", # 群组 ID(可选)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1:自动回复
|
||||
|
||||
```python
|
||||
@on_message(keywords=["hello", "hi"])
|
||||
async def auto_reply(self, event: MessageEvent, ctx: Context):
|
||||
await ctx.platform.send(event.session_id, "你好!我是机器人")
|
||||
```
|
||||
|
||||
### 场景 2:命令响应
|
||||
|
||||
```python
|
||||
@on_command("status")
|
||||
async def status(self, event: MessageEvent, ctx: Context):
|
||||
# 发送状态信息
|
||||
await ctx.platform.send(event.session_id, "系统状态:正常运行")
|
||||
|
||||
# 发送状态图片
|
||||
await ctx.platform.send_image(
|
||||
event.session_id,
|
||||
"https://example.com/status.png"
|
||||
)
|
||||
```
|
||||
|
||||
### 场景 3:群管理
|
||||
|
||||
```python
|
||||
@on_command("admin")
|
||||
@require_admin
|
||||
async def admin_cmd(self, event: MessageEvent, ctx: Context):
|
||||
if not event.group_id:
|
||||
await event.reply("此命令仅在群聊中可用")
|
||||
return
|
||||
|
||||
# 获取成员列表
|
||||
members = await ctx.platform.get_members(event.session_id)
|
||||
|
||||
# 统计
|
||||
admins = [m for m in members if m.get('role') in ('owner', 'admin')]
|
||||
await event.reply(f"群管理员数量: {len(admins)}")
|
||||
```
|
||||
|
||||
### 场景 4:富消息回复
|
||||
|
||||
```python
|
||||
@on_command("card")
|
||||
async def send_card(self, event: MessageEvent, ctx: Context):
|
||||
# 发送复杂的富消息
|
||||
await ctx.platform.send_chain(event.session_id, [
|
||||
{"type": "Plain", "text": "📊 统计报告\n\n"},
|
||||
{"type": "Plain", "text": "用户数: 1000\n"},
|
||||
{"type": "Plain", "text": "消息数: 50000\n"},
|
||||
{"type": "Image", "file": "https://example.com/chart.png"},
|
||||
{"type": "Plain", "text": "\n— 来自 AstrBot"},
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 私聊 vs 群聊
|
||||
|
||||
```python
|
||||
if event.group_id:
|
||||
# 群聊消息
|
||||
await ctx.platform.send(event.session_id, "群消息")
|
||||
else:
|
||||
# 私聊消息
|
||||
await ctx.platform.send(event.session_id, "私聊消息")
|
||||
```
|
||||
|
||||
### 2. 发送频率
|
||||
|
||||
避免频繁发送消息,部分平台有频率限制:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
for msg in messages:
|
||||
await ctx.platform.send(event.session_id, msg)
|
||||
await asyncio.sleep(1) # 间隔 1 秒
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```python
|
||||
from astrbot_sdk import AstrBotError
|
||||
|
||||
try:
|
||||
await ctx.platform.send(event.session_id, "消息")
|
||||
except AstrBotError as e:
|
||||
if e.code == "permission_denied":
|
||||
print("没有发送权限")
|
||||
else:
|
||||
print(f"发送失败: {e.message}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [API 参考](../api-reference.md)
|
||||
- [MessageEvent 消息事件](../api-reference.md#messageevent-消息事件)
|
||||
- [快速开始](../quickstart.md)
|
||||
55
docs/v4/examples/README.md
Normal file
55
docs/v4/examples/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 示例插件索引
|
||||
|
||||
这里收集了 AstrBot SDK v4 的示例插件,帮助你快速学习各种功能的用法。
|
||||
|
||||
## 示例列表
|
||||
|
||||
### [LLM 对话插件](llm-chat/)
|
||||
|
||||
演示如何使用 LLM 客户端:
|
||||
|
||||
- 简单对话
|
||||
- 流式对话
|
||||
- 带历史记录的对话
|
||||
- 模型和参数控制
|
||||
|
||||
```python
|
||||
# 简单对话
|
||||
reply = await ctx.llm.chat("你好")
|
||||
|
||||
# 流式对话
|
||||
async for chunk in ctx.llm.stream_chat("讲个故事"):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### [数据库插件](database/)
|
||||
|
||||
演示如何使用数据库客户端:
|
||||
|
||||
- 用户设置存储
|
||||
- 计数器
|
||||
- 待办事项
|
||||
- 批量操作
|
||||
|
||||
```python
|
||||
# 存储数据
|
||||
await ctx.db.set("user:1", {"name": "张三"})
|
||||
|
||||
# 读取数据
|
||||
data = await ctx.db.get("user:1")
|
||||
|
||||
# 批量操作
|
||||
await ctx.db.set_many({"a": 1, "b": 2})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更多示例
|
||||
|
||||
如果你想贡献更多示例,请提交 PR 到 [astrbot-sdk 仓库](https://github.com/Soulter/astrbot-sdk)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [快速开始](../quickstart.md)
|
||||
- [API 参考](../api-reference.md)
|
||||
- [客户端文档](../clients/)
|
||||
478
docs/v4/examples/database/README.md
Normal file
478
docs/v4/examples/database/README.md
Normal 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)
|
||||
333
docs/v4/examples/llm-chat/README.md
Normal file
333
docs/v4/examples/llm-chat/README.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# LLM 对话插件示例
|
||||
|
||||
本示例演示如何创建一个功能完整的 AI 对话插件。
|
||||
|
||||
## 完整代码
|
||||
|
||||
### plugin.yaml
|
||||
|
||||
```yaml
|
||||
name: llm_chat_demo
|
||||
display_name: LLM 对话演示
|
||||
desc: 一个支持上下文对话的 AI 聊天插件
|
||||
author: your-name
|
||||
version: 1.0.0
|
||||
runtime:
|
||||
python: "3.12"
|
||||
components:
|
||||
- class: main:LLMChatPlugin
|
||||
```
|
||||
|
||||
### main.py
|
||||
|
||||
```python
|
||||
"""LLM 对话插件示例。
|
||||
|
||||
功能演示:
|
||||
- 简单对话
|
||||
- 流式对话
|
||||
- 带历史记录的对话
|
||||
- 模型和参数控制
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from astrbot_sdk import Context, MessageEvent, Star, on_command
|
||||
from astrbot_sdk.clients.llm import ChatMessage
|
||||
|
||||
|
||||
class LLMChatPlugin(Star):
|
||||
"""LLM 对话插件。"""
|
||||
|
||||
@on_command("chat", description="与 AI 对话")
|
||||
async def chat(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""简单对话示例。"""
|
||||
prompt = event.text.removeprefix("/chat").strip()
|
||||
|
||||
if not prompt:
|
||||
await event.reply("用法: /chat <问题>")
|
||||
return
|
||||
|
||||
# 调用 LLM
|
||||
reply = await ctx.llm.chat(prompt)
|
||||
await event.reply(reply)
|
||||
|
||||
@on_command("stream", description="流式对话")
|
||||
async def stream_chat(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""流式对话示例。"""
|
||||
prompt = event.text.removeprefix("/stream").strip()
|
||||
|
||||
if not prompt:
|
||||
await event.reply("用法: /stream <问题>")
|
||||
return
|
||||
|
||||
# 收集流式响应
|
||||
chunks = []
|
||||
async for chunk in ctx.llm.stream_chat(prompt):
|
||||
chunks.append(chunk)
|
||||
|
||||
# 发送完整响应
|
||||
full_response = "".join(chunks)
|
||||
await event.reply(full_response)
|
||||
|
||||
@on_command("creative", description="创造性写作")
|
||||
async def creative_chat(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""使用更高温度的创造性对话。"""
|
||||
prompt = event.text.removeprefix("/creative").strip()
|
||||
|
||||
if not prompt:
|
||||
await event.reply("用法: /creative <主题>")
|
||||
return
|
||||
|
||||
# 使用更高的温度增加创造性
|
||||
reply = await ctx.llm.chat(
|
||||
prompt,
|
||||
temperature=0.9,
|
||||
system="你是一个富有创意的作家,善于用生动的语言创作内容"
|
||||
)
|
||||
await event.reply(reply)
|
||||
|
||||
@on_command("ask", description="带历史的对话")
|
||||
async def ask_with_history(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""带对话历史的聊天。"""
|
||||
prompt = event.text.removeprefix("/ask").strip()
|
||||
|
||||
if not prompt:
|
||||
await event.reply("用法: /ask <问题>")
|
||||
return
|
||||
|
||||
user_id = event.user_id or "unknown"
|
||||
history_key = f"chat_history:{user_id}"
|
||||
|
||||
# 加载历史记录
|
||||
history_data = await ctx.db.get(history_key) or []
|
||||
history = [
|
||||
ChatMessage(role=item["role"], content=item["content"])
|
||||
for item in history_data
|
||||
]
|
||||
|
||||
# 调用 LLM
|
||||
reply = await ctx.llm.chat(prompt, history=history)
|
||||
|
||||
# 保存历史
|
||||
history_data.append({"role": "user", "content": prompt})
|
||||
history_data.append({"role": "assistant", "content": reply})
|
||||
|
||||
# 只保留最近 10 轮对话
|
||||
if len(history_data) > 20:
|
||||
history_data = history_data[-20:]
|
||||
|
||||
await ctx.db.set(history_key, history_data)
|
||||
|
||||
await event.reply(reply)
|
||||
|
||||
@on_command("clear", description="清除对话历史")
|
||||
async def clear_history(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""清除用户的对话历史。"""
|
||||
user_id = event.user_id or "unknown"
|
||||
history_key = f"chat_history:{user_id}"
|
||||
|
||||
await ctx.db.delete(history_key)
|
||||
await event.reply("对话历史已清除")
|
||||
|
||||
@on_command("raw", description="获取完整响应信息")
|
||||
async def raw_chat(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""获取 LLM 的完整响应。"""
|
||||
prompt = event.text.removeprefix("/raw").strip()
|
||||
|
||||
if not prompt:
|
||||
await event.reply("用法: /raw <问题>")
|
||||
return
|
||||
|
||||
# 获取完整响应
|
||||
response = await ctx.llm.chat_raw(prompt)
|
||||
|
||||
# 构建响应信息
|
||||
lines = [
|
||||
f"📝 响应: {response.text}",
|
||||
f"",
|
||||
f"📊 Token 使用:",
|
||||
f" - 输入: {response.usage.get('input_tokens', 'N/A') if response.usage else 'N/A'}",
|
||||
f" - 输出: {response.usage.get('output_tokens', 'N/A') if response.usage else 'N/A'}",
|
||||
f"",
|
||||
f"🏁 结束原因: {response.finish_reason or 'N/A'}",
|
||||
]
|
||||
|
||||
if response.tool_calls:
|
||||
lines.append(f"🔧 工具调用: {len(response.tool_calls)} 个")
|
||||
|
||||
await event.reply("\n".join(lines))
|
||||
```
|
||||
|
||||
### requirements.txt
|
||||
|
||||
```
|
||||
# 无额外依赖
|
||||
```
|
||||
|
||||
## 功能说明
|
||||
|
||||
### 1. 简单对话 (`/chat`)
|
||||
|
||||
```bash
|
||||
用户: /chat 你好
|
||||
机器人: 你好!有什么可以帮助你的?
|
||||
```
|
||||
|
||||
### 2. 流式对话 (`/stream`)
|
||||
|
||||
```bash
|
||||
用户: /stream 讲一个短故事
|
||||
机器人: [流式输出的故事内容...]
|
||||
```
|
||||
|
||||
### 3. 创造性写作 (`/creative`)
|
||||
|
||||
```bash
|
||||
用户: /creative 写一首关于春天的诗
|
||||
机器人: [生成的诗歌...]
|
||||
```
|
||||
|
||||
### 4. 带历史的对话 (`/ask`)
|
||||
|
||||
```bash
|
||||
用户: /ask 我叫小明
|
||||
机器人: 你好小明!
|
||||
|
||||
用户: /ask 你记得我的名字吗
|
||||
机器人: 当然记得,你叫小明!
|
||||
```
|
||||
|
||||
### 5. 完整响应信息 (`/raw`)
|
||||
|
||||
```bash
|
||||
用户: /raw hello
|
||||
机器人:
|
||||
📝 响应: Hello! How can I help you today?
|
||||
|
||||
📊 Token 使用:
|
||||
- 输入: 5
|
||||
- 输出: 12
|
||||
|
||||
🏁 结束原因: stop
|
||||
```
|
||||
|
||||
## 本地测试
|
||||
|
||||
```bash
|
||||
# 创建插件目录
|
||||
astrbot-sdk init llm-chat-demo
|
||||
|
||||
# 复制上述代码到对应文件
|
||||
|
||||
# 本地运行
|
||||
astrbot-sdk dev --local --plugin-dir llm-chat-demo --interactive
|
||||
|
||||
# 在交互模式中测试
|
||||
> /chat 你好
|
||||
> /creative 写一首诗
|
||||
```
|
||||
|
||||
## 测试代码
|
||||
|
||||
### tests/test_plugin.py
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot_sdk.testing import (
|
||||
MockContext,
|
||||
MockMessageEvent,
|
||||
PluginHarness,
|
||||
LocalRuntimeConfig,
|
||||
)
|
||||
|
||||
|
||||
class TestLLMChatPlugin:
|
||||
"""LLM 对话插件测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_simple_chat(self):
|
||||
"""测试简单对话。"""
|
||||
from main import LLMChatPlugin
|
||||
|
||||
plugin = LLMChatPlugin()
|
||||
ctx = MockContext(plugin_id="test")
|
||||
event = MockMessageEvent(text="/chat 你好", context=ctx)
|
||||
|
||||
# 模拟 LLM 响应
|
||||
ctx.llm.mock_response("你好!有什么可以帮助你的?")
|
||||
|
||||
await plugin.chat(event, ctx)
|
||||
|
||||
# 验证回复
|
||||
assert "你好" in event.replies[0]
|
||||
ctx.platform.assert_sent("你好!有什么可以帮助你的?")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_creative_chat(self):
|
||||
"""测试创造性对话。"""
|
||||
from main import LLMChatPlugin
|
||||
|
||||
plugin = LLMChatPlugin()
|
||||
ctx = MockContext(plugin_id="test")
|
||||
event = MockMessageEvent(text="/creative 写一首诗", context=ctx)
|
||||
|
||||
ctx.llm.mock_response("春风吹绿柳枝头...")
|
||||
|
||||
await plugin.creative_chat(event, ctx)
|
||||
|
||||
assert len(event.replies) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_with_history(self):
|
||||
"""测试带历史的对话。"""
|
||||
from main import LLMChatPlugin
|
||||
|
||||
plugin = LLMChatPlugin()
|
||||
ctx = MockContext(plugin_id="test")
|
||||
|
||||
# 第一次对话
|
||||
event1 = MockMessageEvent(text="/ask 我叫小明", context=ctx, user_id="user1")
|
||||
ctx.llm.mock_response("你好小明!")
|
||||
await plugin.ask_with_history(event1, ctx)
|
||||
|
||||
# 验证历史被保存
|
||||
history = await ctx.db.get("chat_history:user1")
|
||||
assert history is not None
|
||||
assert len(history) == 2
|
||||
|
||||
# 第二次对话
|
||||
ctx.llm.mock_response("你叫小明")
|
||||
event2 = MockMessageEvent(text="/ask 我叫什么", context=ctx, user_id="user1")
|
||||
await plugin.ask_with_history(event2, ctx)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_harness(self):
|
||||
"""使用完整 harness 测试。"""
|
||||
plugin_dir = Path(__file__).parent.parent
|
||||
|
||||
harness = PluginHarness(
|
||||
LocalRuntimeConfig(plugin_dir=plugin_dir)
|
||||
)
|
||||
|
||||
async with harness:
|
||||
harness.router.enqueue_llm_response("测试响应")
|
||||
records = await harness.dispatch_text("chat 测试")
|
||||
|
||||
assert any("测试响应" in (r.text or "") for r in records)
|
||||
```
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **添加更多系统提示词**:支持用户选择不同的 AI 人设
|
||||
2. **支持图片输入**:使用 `image_urls` 参数
|
||||
3. **工具调用**:结合 `tool_calls` 实现功能扩展
|
||||
4. **多模型支持**:让用户选择不同的模型
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [LLM 客户端文档](../clients/llm.md)
|
||||
- [API 参考](../api-reference.md)
|
||||
- [快速开始](../quickstart.md)
|
||||
@@ -140,10 +140,24 @@ async def test_plugin_directory():
|
||||
assert any(item.text for item in records)
|
||||
```
|
||||
|
||||
## 7. 当前边界
|
||||
## 7. 更多文档
|
||||
|
||||
- [API 参考](api-reference.md) - 完整的 API 文档
|
||||
- [LLM 客户端](clients/llm.md) - 大语言模型调用
|
||||
- [数据库客户端](clients/db.md) - 数据持久化存储
|
||||
- [平台客户端](clients/platform.md) - 消息发送与群管理
|
||||
- [记忆客户端](clients/memory.md) - 语义搜索存储
|
||||
|
||||
### 示例插件
|
||||
|
||||
- [LLM 对话插件](examples/llm-chat/) - AI 对话功能演示
|
||||
- [数据库插件](examples/database/) - 数据存储功能演示
|
||||
|
||||
## 8. 当前边界
|
||||
|
||||
当前 quickstart 对应的是已经存在的能力,不包含这些后续项:
|
||||
|
||||
TODO: 这些功能正在开发中:
|
||||
- `ctx.http` / `ctx.cache` / `ctx.storage` / `ctx.i18n`
|
||||
- 完整宿主调度下的 schedule 执行器
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import pytest
|
||||
LEVEL_ONE_MODULES = [
|
||||
"astrbot.api",
|
||||
"astrbot.api.all",
|
||||
"astrbot.api.components",
|
||||
"astrbot.api.components.command",
|
||||
"astrbot.api.message_components",
|
||||
"astrbot.api.event",
|
||||
"astrbot.api.event.filter",
|
||||
@@ -25,6 +27,10 @@ LEVEL_TWO_MODULES = [
|
||||
"astrbot.core.message",
|
||||
"astrbot.core.message.components",
|
||||
"astrbot.core.message.message_event_result",
|
||||
"astrbot.core.agent",
|
||||
"astrbot.core.agent.message",
|
||||
"astrbot.core.db",
|
||||
"astrbot.core.db.po",
|
||||
"astrbot.core.platform",
|
||||
"astrbot.core.platform.astr_message_event",
|
||||
"astrbot.core.platform.astrbot_message",
|
||||
@@ -32,7 +38,11 @@ LEVEL_TWO_MODULES = [
|
||||
"astrbot.core.platform.platform_metadata",
|
||||
"astrbot.core.platform.register",
|
||||
"astrbot.core.platform.sources.aiocqhttp",
|
||||
"astrbot.core.provider",
|
||||
"astrbot.core.provider.entities",
|
||||
"astrbot.core.provider.provider",
|
||||
"astrbot.core.utils",
|
||||
"astrbot.core.utils.astrbot_path",
|
||||
"astrbot.core.utils.session_waiter",
|
||||
]
|
||||
|
||||
|
||||
72
tests_v4/test_runtime_contracts.py
Normal file
72
tests_v4/test_runtime_contracts.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Contract guards for the current public runtime/compat surface."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from astrbot_sdk.api.event import filter as compat_filter_namespace
|
||||
from astrbot_sdk.protocol.descriptors import BUILTIN_CAPABILITY_SCHEMAS
|
||||
from astrbot_sdk.runtime.capability_router import CapabilityRouter
|
||||
|
||||
EXPECTED_PUBLIC_BUILTIN_CAPABILITIES = {
|
||||
"llm.chat": False,
|
||||
"llm.chat_raw": False,
|
||||
"llm.stream_chat": True,
|
||||
"memory.search": False,
|
||||
"memory.save": False,
|
||||
"memory.get": False,
|
||||
"memory.delete": False,
|
||||
"db.get": False,
|
||||
"db.set": False,
|
||||
"db.delete": False,
|
||||
"db.list": False,
|
||||
"db.get_many": False,
|
||||
"db.set_many": False,
|
||||
"db.watch": True,
|
||||
"platform.send": False,
|
||||
"platform.send_image": False,
|
||||
"platform.send_chain": False,
|
||||
"platform.get_members": False,
|
||||
}
|
||||
EXPECTED_CANCELABLE_CAPABILITIES = {"llm.stream_chat", "db.watch"}
|
||||
EXPECTED_PUBLIC_COMPAT_HOOKS = {
|
||||
"after_message_sent",
|
||||
"on_astrbot_loaded",
|
||||
"on_platform_loaded",
|
||||
"on_decorating_result",
|
||||
"on_llm_request",
|
||||
"on_llm_response",
|
||||
"on_waiting_llm_request",
|
||||
"on_using_llm_tool",
|
||||
"on_llm_tool_respond",
|
||||
"on_plugin_error",
|
||||
"on_plugin_loaded",
|
||||
"on_plugin_unloaded",
|
||||
}
|
||||
|
||||
|
||||
def test_builtin_capability_schema_registry_matches_public_contract():
|
||||
"""协议层公开的内建 capability 集合必须保持稳定。"""
|
||||
assert set(BUILTIN_CAPABILITY_SCHEMAS) == set(EXPECTED_PUBLIC_BUILTIN_CAPABILITIES)
|
||||
|
||||
|
||||
def test_capability_router_descriptors_match_public_contract():
|
||||
"""Runtime 层内建 capability 的名字、stream 和 cancel 语义必须对齐契约。"""
|
||||
descriptors = {item.name: item for item in CapabilityRouter().descriptors()}
|
||||
|
||||
assert set(descriptors) == set(EXPECTED_PUBLIC_BUILTIN_CAPABILITIES)
|
||||
assert {
|
||||
name: descriptor.supports_stream for name, descriptor in descriptors.items()
|
||||
} == EXPECTED_PUBLIC_BUILTIN_CAPABILITIES
|
||||
assert {
|
||||
name for name, descriptor in descriptors.items() if descriptor.cancelable
|
||||
} == EXPECTED_CANCELABLE_CAPABILITIES
|
||||
|
||||
|
||||
def test_public_compat_hook_factories_remain_available():
|
||||
"""兼容 hook 名称必须同时保留模块级和 namespace 级入口。"""
|
||||
compat_filter_module = import_module("astrbot_sdk.api.event.filter")
|
||||
|
||||
for name in EXPECTED_PUBLIC_COMPAT_HOOKS:
|
||||
assert callable(getattr(compat_filter_module, name))
|
||||
assert callable(getattr(compat_filter_namespace, name))
|
||||
Reference in New Issue
Block a user