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

467
docs/v4/api-reference.md Normal file
View 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
View 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
View 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
View 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
View 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)

View 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/)

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)

View 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)

View File

@@ -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 执行器