mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
delete no need thing
This commit is contained in:
@@ -1,898 +0,0 @@
|
||||
# AstrBot SDK 项目完整架构分析文档
|
||||
|
||||
> 作者:whatevertogo
|
||||
> 更新时间:2026-03-14
|
||||
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述](#项目概述)
|
||||
2. [目录结构](#目录结构)
|
||||
3. [核心架构层次](#核心架构层次)
|
||||
4. [协议层设计](#协议层设计)
|
||||
5. [运行时架构](#运行时架构)
|
||||
6. [客户端层设计](#客户端层设计)
|
||||
7. [新旧架构对比](#新旧架构对比)
|
||||
8. [插件开发指南](#插件开发指南)
|
||||
9. [关键设计模式](#关键设计模式)
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
AstrBot SDK 是一个基于 Python 3.12+ 的机器人插件开发框架,采用**进程隔离**和**能力路由**架构,支持插件的动态加载、独立运行和跨进程通信。
|
||||
|
||||
### 核心特性
|
||||
|
||||
| 特性 | 描述 |
|
||||
|------|------|
|
||||
| 进程隔离 | 每个插件运行在独立 Worker 进程,崩溃不影响其他插件 |
|
||||
| 环境分组 | 多插件可共享同一 Python 虚拟环境,节省资源 |
|
||||
| 能力路由 | 显式声明的 Capability 系统,支持 JSON Schema 验证 |
|
||||
| 流式支持 | 原生支持流式 LLM 调用和增量结果返回 |
|
||||
| 向后兼容 | 完整的旧版 API 兼容层,支持无修改迁移 |
|
||||
| 协议优先 | 基于 v4 协议的统一通信模型,支持多种传输方式 |
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **Python**: 3.12+
|
||||
- **异步框架**: asyncio
|
||||
- **Web 框架**: aiohttp
|
||||
- **数据验证**: pydantic
|
||||
- **日志**: loguru
|
||||
- **配置**: pyyaml
|
||||
- **LLM**: openai, anthropic, google-genai
|
||||
- **包管理**: uv (环境分组)
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
astrbot-sdk/
|
||||
├── src/ # 旧版实现 (已停止更新)
|
||||
│ └── astrbot_sdk/ # 旧版 SDK
|
||||
├── src-new/ # 新版 v4 实现 (当前活跃)
|
||||
│ └── astrbot_sdk/ # v4 SDK 主包
|
||||
│ ├── __init__.py # 顶层公共 API
|
||||
│ ├── __main__.py # CLI 入口点
|
||||
│ ├── star.py # v4 原生插件基类
|
||||
│ ├── context.py # 运行时上下文
|
||||
│ ├── decorators.py # v4 原生装饰器
|
||||
│ ├── events.py # v4 原生事件对象
|
||||
│ ├── errors.py # 统一错误模型
|
||||
│ ├── cli.py # 命令行工具
|
||||
│ ├── testing.py # 测试辅助模块
|
||||
│ ├── _invocation_context.py # 调用上下文管理
|
||||
│ │
|
||||
│ ├── clients/ # 能力客户端层
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── _proxy.py # CapabilityProxy 能力代理
|
||||
│ │ ├── llm.py # LLM 客户端
|
||||
│ │ ├── memory.py # 记忆存储客户端
|
||||
│ │ ├── db.py # KV 存储客户端
|
||||
│ │ ├── platform.py # 平台消息客户端
|
||||
│ │ ├── http.py # HTTP 注册客户端
|
||||
│ │ └── metadata.py # 插件元数据客户端
|
||||
│ │
|
||||
│ ├── protocol/ # 协议层
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── messages.py # v4 协议消息模型
|
||||
│ │ └── descriptors.py # Handler/Capability 描述符
|
||||
│ │
|
||||
│ └── runtime/ # 运行时层
|
||||
│ ├── __init__.py
|
||||
│ ├── peer.py # 协议对等端
|
||||
│ ├── transport.py # 传输抽象与实现
|
||||
│ ├── handler_dispatcher.py # Handler 执行分发
|
||||
│ ├── capability_router.py # Capability 路由
|
||||
│ ├── loader.py # 插件加载
|
||||
│ ├── bootstrap.py # 启动引导
|
||||
│ ├── worker.py # Worker 运行时
|
||||
│ ├── supervisor.py # Supervisor 运行时
|
||||
│ └── environment_groups.py # 环境分组管理
|
||||
│
|
||||
├── tests_v4/ # v4 测试套件
|
||||
│ ├── unit/ # 单元测试
|
||||
│ ├── integration/ # 集成测试
|
||||
│ └── external_plugin_matrix.json # 外部插件兼容矩阵
|
||||
│
|
||||
├── test_plugin/ # 测试插件样本
|
||||
│ ├── new/ # v4 原生插件示例
|
||||
│ │ ├── plugin.yaml
|
||||
│ │ └── commands/
|
||||
│ │ └── hello.py
|
||||
│ │
|
||||
│ └── old/ # 旧版兼容插件示例 (deprecated)
|
||||
│ ├── plugin.yaml
|
||||
│ └── main.py
|
||||
│
|
||||
├── examples/ # 示例插件
|
||||
│ └── hello_plugin/ # 入门示例
|
||||
│ ├── plugin.yaml
|
||||
│ ├── main.py
|
||||
│ └── README.md
|
||||
│
|
||||
├── astrBot/ # 参考 AstrBot 应用
|
||||
│
|
||||
├── pyproject.toml # 项目配置
|
||||
├── ARCHITECTURE.md # 架构文档
|
||||
├── refactor.md # 重构历史
|
||||
├── PROJECT_ARCHITECTURE.md # 本文档
|
||||
└── run_tests.py # 测试入口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心架构层次
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 用户层 (Plugin Developer) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ v4 入口: astrbot_sdk.{Star, Context, MessageEvent} │
|
||||
│ 装饰器: on_command, on_message, on_event, provide_capability│
|
||||
└────────────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────▼─────────────────────────────────────────────┐
|
||||
│ 高层 API (High-Level API) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 能力客户端: │
|
||||
│ - LLMClient (llm.chat, llm.chat_raw, llm.stream_chat)│
|
||||
│ - MemoryClient (memory.save, memory.search, ...) │
|
||||
│ - DBClient (db.get, db.set, db.watch, ...) │
|
||||
│ - PlatformClient (platform.send, platform.send_image, ...)│
|
||||
│ - HTTPClient (http.register_api, http.list_apis) │
|
||||
│ - MetadataClient (metadata.get_plugin, ...) │
|
||||
└────────────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────▼─────────────────────────────────────────────┐
|
||||
│ 执行边界 (Execution Boundary) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ runtime 主干: │
|
||||
│ - loader.py (插件发现、加载) │
|
||||
│ - bootstrap.py (Supervisor/Worker 启动) │
|
||||
│ - handler_dispatcher.py (Handler 执行分发) │
|
||||
│ - capability_router.py (Capability 路由) │
|
||||
│ - peer.py (协议对等端) │
|
||||
│ - transport.py (传输抽象) │
|
||||
│ - supervisor.py (Supervisor 运行时) │
|
||||
│ - worker.py (Worker 运行时) │
|
||||
└────────────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────▼─────────────────────────────────────────────┐
|
||||
│ 协议与传输 (Protocol & Transport) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ protocol/ │
|
||||
│ - messages.py (协议消息模型) │
|
||||
│ - descriptors.py (Handler/Capability 描述符) │
|
||||
│ transport 实现: │
|
||||
│ - StdioTransport (标准输入输出) │
|
||||
│ - WebSocketServerTransport (WebSocket 服务端) │
|
||||
│ - WebSocketClientTransport (WebSocket 客户端) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 层次职责
|
||||
|
||||
| 层次 | 职责 | 主要模块 |
|
||||
|------|------|---------|
|
||||
| 用户层 | 插件开发者 API | `Star`, `Context`, `MessageEvent`, 装饰器 |
|
||||
| 高层 API | 类型化的能力客户端 | `clients/{llm, memory, db, platform, http, metadata}` |
|
||||
| 执行边界 | 插件加载、路由、分发 | `runtime/loader.py`, `runtime/supervisor.py` |
|
||||
| 协议层 | 消息模型、描述符 | `protocol/` |
|
||||
| 传输层 | 底层通信抽象 | `runtime/transport.py` |
|
||||
|
||||
---
|
||||
|
||||
## 协议层设计
|
||||
|
||||
### 消息模型
|
||||
|
||||
v4 协议定义了 5 种消息类型:
|
||||
|
||||
| 消息类型 | 用途 | 关键字段 |
|
||||
|---------|------|---------|
|
||||
| `InitializeMessage` | 握手初始化 | `protocol_version`, `peer`, `handlers`, `provided_capabilities` |
|
||||
| `InvokeMessage` | 调用能力 | `capability`, `input`, `stream` |
|
||||
| `ResultMessage` | 返回结果 | `success`, `output`, `error` |
|
||||
| `EventMessage` | 流式事件 | `phase` (started/delta/completed/failed), `data` |
|
||||
| `CancelMessage` | 取消调用 | `reason` |
|
||||
|
||||
### 握手流程
|
||||
|
||||
```
|
||||
Worker (Plugin) Supervisor (Core)
|
||||
| |
|
||||
| InitializeMessage |
|
||||
|----------------------------->|
|
||||
| | 创建 CapabilityRouter
|
||||
| | 注册 handler.invoke
|
||||
| |
|
||||
| ResultMessage(kind="init") |
|
||||
|<-----------------------------|
|
||||
| | 等待 handler.invoke 调用
|
||||
| | 执行 CapabilityRouter.execute()
|
||||
| |
|
||||
| InvokeMessage(handler.invoke) |
|
||||
|<-----------------------------|
|
||||
| HandlerDispatcher.invoke() |
|
||||
| 执行用户 handler |
|
||||
| |
|
||||
| ResultMessage(output) |
|
||||
|----------------------------->|
|
||||
```
|
||||
|
||||
### 描述符模型
|
||||
|
||||
#### HandlerDescriptor
|
||||
|
||||
```python
|
||||
{
|
||||
"id": "plugin.module:handler_name",
|
||||
"trigger": {
|
||||
"type": "command",
|
||||
"command": "hello",
|
||||
"aliases": ["hi"],
|
||||
"description": "打招呼命令"
|
||||
},
|
||||
"priority": 0,
|
||||
"permissions": {
|
||||
"require_admin": False,
|
||||
"level": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CapabilityDescriptor
|
||||
|
||||
```python
|
||||
{
|
||||
"name": "llm.chat",
|
||||
"description": "发送对话请求,返回文本",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {"type": "string"}
|
||||
},
|
||||
"required": ["prompt"]
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string"}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"supports_stream": False,
|
||||
"cancelable": False
|
||||
}
|
||||
```
|
||||
|
||||
### 内置 Capabilities (28个)
|
||||
|
||||
| 命名空间 | 能力 | 说明 |
|
||||
|----------|------|------|
|
||||
| `llm` | `chat` | 同步对话,返回文本 |
|
||||
| `llm` | `chat_raw` | 同步对话,返回完整响应 |
|
||||
| `llm` | `stream_chat` | 流式对话 |
|
||||
| `memory` | `search` | 搜索记忆 |
|
||||
| `memory` | `save` | 保存记忆 |
|
||||
| `memory` | `save_with_ttl` | 保存带过期时间的记忆 |
|
||||
| `memory` | `get` | 读取单条记忆 |
|
||||
| `memory` | `get_many` | 批量获取记忆 |
|
||||
| `memory` | `delete` | 删除记忆 |
|
||||
| `memory` | `delete_many` | 批量删除记忆 |
|
||||
| `memory` | `stats` | 获取记忆统计信息 |
|
||||
| `db` | `get` | 读取 KV |
|
||||
| `db` | `set` | 写入 KV |
|
||||
| `db` | `delete` | 删除 KV |
|
||||
| `db` | `list` | 列出 KV 键 |
|
||||
| `db` | `get_many` | 批量读取 KV |
|
||||
| `db` | `set_many` | 批量写入 KV |
|
||||
| `db` | `watch` | 订阅 KV 变更 |
|
||||
| `platform` | `send` | 发送消息 |
|
||||
| `platform` | `send_image` | 发送图片 |
|
||||
| `platform` | `send_chain` | 发送消息链 |
|
||||
| `platform` | `get_members` | 获取群成员 |
|
||||
| `http` | `register_api` | 注册 HTTP API 端点 |
|
||||
| `http` | `unregister_api` | 注销 HTTP API 端点 |
|
||||
| `http` | `list_apis` | 列出已注册的 API |
|
||||
| `metadata` | `get_plugin` | 获取单个插件元数据 |
|
||||
| `metadata` | `list_plugins` | 列出所有插件元数据 |
|
||||
| `metadata` | `get_plugin_config` | 获取当前插件配置 |
|
||||
|
||||
---
|
||||
|
||||
## 运行时架构
|
||||
|
||||
### 组件关系图
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ AstrBot │
|
||||
│ Core │
|
||||
└──────┬─────┘
|
||||
│
|
||||
┌──────▼─────┐
|
||||
│ Supervisor │
|
||||
│ Runtime │
|
||||
└──────┬─────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ Peer │ │ Peer │ │ Peer │
|
||||
│ (stdio) │ │ (stdio) │ │ (stdio) │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ Worker │ │ Worker │ │ Worker │
|
||||
│ Runtime │ │ Runtime │ │ Runtime │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ Plugin A │ │ Plugin B │ │ Plugin C │
|
||||
│ (v4/old) │ │ (v4/old) │ │ (v4/old) │
|
||||
└───────────┘ └───────────┘ └───────────┘
|
||||
```
|
||||
|
||||
### SupervisorRuntime
|
||||
|
||||
职责:管理多个 Worker 进程,聚合所有 handler
|
||||
|
||||
```python
|
||||
class SupervisorRuntime:
|
||||
def __init__(self, *, transport, plugins_dir, env_manager):
|
||||
self.transport = transport # 与 Core 的传输层
|
||||
self.plugins_dir = plugins_dir # 插件目录
|
||||
self.capability_router = CapabilityRouter() # 能力路由器
|
||||
self.peer = Peer(...) # 与 Core 的对等端
|
||||
self.worker_sessions = {} # Worker 会话映射
|
||||
self.handler_to_worker = {} # Handler → Worker 映射
|
||||
|
||||
async def start(self):
|
||||
# 1. 发现所有插件
|
||||
discovery = discover_plugins(self.plugins_dir)
|
||||
|
||||
# 2. 规划环境分组
|
||||
plan_result = self.env_manager.plan(discovery.plugins)
|
||||
|
||||
# 3. 为每个分组启动 Worker
|
||||
for group in plan_result.groups:
|
||||
session = WorkerSession(group=group, ...)
|
||||
await session.start()
|
||||
self.worker_sessions[group.id] = session
|
||||
|
||||
# 4. 聚合所有 handler 和 capability
|
||||
await self.peer.initialize(
|
||||
handlers=[...],
|
||||
provided_capabilities=self.capability_router.descriptors()
|
||||
)
|
||||
```
|
||||
|
||||
### WorkerSession
|
||||
|
||||
职责:管理单个 Worker 进程的生命周期
|
||||
|
||||
```python
|
||||
class WorkerSession:
|
||||
def __init__(self, *, group, env_manager, capability_router):
|
||||
self.group = group # 环境分组
|
||||
self.peer = Peer(...) # 与 Worker 的对等端
|
||||
self.capability_router = capability_router
|
||||
self.handlers = [] # Worker 注册的 handlers
|
||||
self.provided_capabilities = [] # Worker 提供的 capabilities
|
||||
|
||||
async def start(self):
|
||||
# 启动 Worker 子进程
|
||||
python_path = self.env_manager.prepare_group_environment(self.group)
|
||||
transport = StdioTransport(
|
||||
command=[python_path, "-m", "astrbot_sdk", "worker", "--group-metadata", ...]
|
||||
)
|
||||
self.peer = Peer(transport=transport, ...)
|
||||
|
||||
# 等待 Worker 初始化完成
|
||||
await self.peer.start()
|
||||
await self.peer.wait_until_remote_initialized()
|
||||
|
||||
# 获取 Worker 的注册信息
|
||||
self.handlers = list(self.peer.remote_handlers)
|
||||
self.provided_capabilities = list(self.peer.remote_provided_capabilities)
|
||||
|
||||
async def invoke_capability(self, capability_name, payload, *, request_id):
|
||||
# 转发能力调用到 Worker
|
||||
return await self.peer.invoke(capability_name, payload, request_id=request_id)
|
||||
```
|
||||
|
||||
### PluginWorkerRuntime
|
||||
|
||||
职责:Worker 进程内的插件加载与执行
|
||||
|
||||
```python
|
||||
class PluginWorkerRuntime:
|
||||
def __init__(self, *, plugin_dir, transport):
|
||||
self.plugin = load_plugin_spec(plugin_dir)
|
||||
self.loaded_plugin = load_plugin(self.plugin)
|
||||
self.peer = Peer(transport=transport, ...)
|
||||
self.dispatcher = HandlerDispatcher(...)
|
||||
self.capability_dispatcher = CapabilityDispatcher(...)
|
||||
|
||||
async def start(self):
|
||||
# 1. 向 Supervisor 注册 handlers 和 capabilities
|
||||
await self.peer.initialize(
|
||||
handlers=[h.descriptor for h in self.loaded_plugin.handlers],
|
||||
provided_capabilities=[c.descriptor for c in self.loaded_plugin.capabilities]
|
||||
)
|
||||
|
||||
# 2. 执行 on_start 生命周期
|
||||
await self._run_lifecycle("on_start")
|
||||
|
||||
# 3. 设置消息处理器
|
||||
self.peer.set_invoke_handler(self._handle_invoke)
|
||||
self.peer.set_cancel_handler(self._handle_cancel)
|
||||
|
||||
async def _handle_invoke(self, message, cancel_token):
|
||||
if message.capability == "handler.invoke":
|
||||
return await self.dispatcher.invoke(message, cancel_token)
|
||||
return await self.capability_dispatcher.invoke(message, cancel_token)
|
||||
```
|
||||
|
||||
### HandlerDispatcher
|
||||
|
||||
职责:将 handler.invoke 请求转成真实 Python 调用
|
||||
|
||||
```python
|
||||
class HandlerDispatcher:
|
||||
def __init__(self, *, plugin_id, peer, handlers):
|
||||
self._handlers = {item.descriptor.id: item for item in handlers}
|
||||
self._peer = peer
|
||||
self._active = {} # request_id → (task, cancel_token)
|
||||
|
||||
async def invoke(self, message, cancel_token):
|
||||
# 1. 查找 handler
|
||||
loaded = self._handlers[message.input["handler_id"]]
|
||||
|
||||
# 2. 创建上下文
|
||||
ctx = Context(peer=self._peer, plugin_id=plugin_id, cancel_token=cancel_token)
|
||||
event = MessageEvent.from_payload(message.input["event"], context=ctx)
|
||||
|
||||
# 3. 构建参数 (支持类型注解注入)
|
||||
args = self._build_args(loaded.callable, event, ctx)
|
||||
|
||||
# 4. 执行 handler
|
||||
result = loaded.callable(*args)
|
||||
|
||||
# 5. 处理返回值
|
||||
await self._consume_result(result, event, ctx)
|
||||
```
|
||||
|
||||
**参数注入优先级**:
|
||||
1. 按类型注解注入(`MessageEvent`, `Context`)
|
||||
2. 按参数名注入(`event`, `ctx`, `context`)
|
||||
3. 从 legacy_args 注入(命令参数等)
|
||||
|
||||
### CapabilityRouter
|
||||
|
||||
职责:能力注册、发现和执行路由
|
||||
|
||||
```python
|
||||
class CapabilityRouter:
|
||||
def __init__(self):
|
||||
self._registrations = {} # capability_name → registration
|
||||
self.db_store = {} # 内置 KV 存储
|
||||
self.memory_store = {} # 内置记忆存储
|
||||
self._register_builtin_capabilities()
|
||||
|
||||
def register(self, descriptor, *, call_handler, stream_handler, finalize):
|
||||
"""注册能力"""
|
||||
self._registrations[descriptor.name] = _CapabilityRegistration(
|
||||
descriptor=descriptor,
|
||||
call_handler=call_handler,
|
||||
stream_handler=stream_handler,
|
||||
finalize=finalize
|
||||
)
|
||||
|
||||
async def execute(self, capability, payload, *, stream, cancel_token, request_id):
|
||||
"""执行能力调用"""
|
||||
registration = self._registrations[capability]
|
||||
|
||||
if stream:
|
||||
# 流式调用
|
||||
raw_execution = registration.stream_handler(request_id, payload, cancel_token)
|
||||
return StreamExecution(iterator=raw_execution, finalize=finalize)
|
||||
else:
|
||||
# 同步调用
|
||||
output = await registration.call_handler(request_id, payload, cancel_token)
|
||||
return output
|
||||
```
|
||||
|
||||
### 环境分组管理
|
||||
|
||||
```python
|
||||
class EnvironmentPlanner:
|
||||
def plan(self, plugins):
|
||||
"""根据 Python 版本和依赖兼容性分组"""
|
||||
# 1. 按版本分组
|
||||
# 2. 按依赖兼容性合并
|
||||
# 3. 生成分组元数据
|
||||
return EnvironmentPlanResult(groups=[...])
|
||||
|
||||
class GroupEnvironmentManager:
|
||||
def prepare(self, group):
|
||||
"""准备分组虚拟环境"""
|
||||
# 1. 生成 lock/source/metadata 工件
|
||||
# 2. 必要时重建虚拟环境
|
||||
# 3. 返回 Python 解释器路径
|
||||
return venv_python_path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 客户端层设计
|
||||
|
||||
### 客户端架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Plugin │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ctx.llm.chat() │
|
||||
│ ctx.memory.save() │
|
||||
│ ctx.db.set() │
|
||||
│ ctx.platform.send() │
|
||||
└────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────▼──────────────────────────────────────────────┐
|
||||
│ CapabilityProxy │
|
||||
│ - call(name, payload) │
|
||||
│ - stream(name, payload) │
|
||||
└────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────▼──────────────────────────────────────────────┐
|
||||
│ Peer │
|
||||
│ - invoke(capability, payload, stream=False) │
|
||||
│ - invoke_stream(capability, payload) │
|
||||
└────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────▼──────────────────────────────────────────────┐
|
||||
│ Transport │
|
||||
│ - send(json_string) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### CapabilityProxy
|
||||
|
||||
职责:封装 Peer 的能力调用接口
|
||||
|
||||
```python
|
||||
class CapabilityProxy:
|
||||
def __init__(self, peer):
|
||||
self._peer = peer
|
||||
|
||||
async def call(self, name, payload):
|
||||
"""普通能力调用"""
|
||||
# 1. 检查能力是否可用
|
||||
descriptor = self._peer.remote_capability_map.get(name)
|
||||
if descriptor is None:
|
||||
raise AstrBotError.capability_not_found(name)
|
||||
|
||||
# 2. 调用 Peer.invoke
|
||||
return await self._peer.invoke(name, payload, stream=False)
|
||||
|
||||
async def stream(self, name, payload):
|
||||
"""流式能力调用"""
|
||||
# 1. 检查流式支持
|
||||
descriptor = self._peer.remote_capability_map.get(name)
|
||||
if not descriptor.supports_stream:
|
||||
raise AstrBotError.invalid_input(f"{name} 不支持 stream")
|
||||
|
||||
# 2. 调用 Peer.invoke_stream
|
||||
event_stream = await self._peer.invoke_stream(name, payload)
|
||||
async for event in event_stream:
|
||||
if event.phase == "delta":
|
||||
yield event.data
|
||||
```
|
||||
|
||||
### LLMClient
|
||||
|
||||
```python
|
||||
class LLMClient:
|
||||
def __init__(self, proxy: CapabilityProxy):
|
||||
self._proxy = proxy
|
||||
|
||||
async def chat(self, prompt, *, system=None, history=None, **kwargs) -> str:
|
||||
"""发送聊天请求,返回文本"""
|
||||
output = await self._proxy.call("llm.chat", {
|
||||
"prompt": prompt,
|
||||
"system": system,
|
||||
"history": self._serialize_history(history),
|
||||
**kwargs
|
||||
})
|
||||
return output["text"]
|
||||
|
||||
async def chat_raw(self, prompt, **kwargs) -> LLMResponse:
|
||||
"""发送聊天请求,返回完整响应"""
|
||||
output = await self._proxy.call("llm.chat_raw", {"prompt": prompt, **kwargs})
|
||||
return LLMResponse.model_validate(output)
|
||||
|
||||
async def stream_chat(self, prompt, **kwargs) -> AsyncGenerator[str]:
|
||||
"""流式聊天"""
|
||||
async for delta in self._proxy.stream("llm.stream_chat", {"prompt": prompt, **kwargs}):
|
||||
yield delta["text"]
|
||||
```
|
||||
|
||||
### 其他客户端
|
||||
|
||||
| 客户端 | 主要方法 | 对应 Capability |
|
||||
|--------|---------|-----------------|
|
||||
| `MemoryClient` | `search()`, `save()`, `save_with_ttl()`, `get()`, `get_many()`, `delete()`, `delete_many()`, `stats()` | `memory.*` |
|
||||
| `DBClient` | `get()`, `set()`, `delete()`, `list()`, `get_many()`, `set_many()`, `watch()` | `db.*` |
|
||||
| `PlatformClient` | `send()`, `send_image()`, `send_chain()`, `get_members()` | `platform.*` |
|
||||
| `HTTPClient` | `register_api()`, `unregister_api()`, `list_apis()` | `http.*` |
|
||||
| `MetadataClient` | `get_plugin()`, `list_plugins()`, `get_current_plugin()`, `get_plugin_config()` | `metadata.*` |
|
||||
|
||||
---
|
||||
|
||||
## 新旧架构对比
|
||||
|
||||
### 协议对比
|
||||
|
||||
| 特性 | 旧版 JSON-RPC | 新版 v4 协议 |
|
||||
|------|---------------|--------------|
|
||||
| 消息格式 | `{"jsonrpc": "2.0", ...}` | `{"type": "invoke", ...}` |
|
||||
| 方法区分 | `method` 字段 | `type` 字段 |
|
||||
| 错误码 | 整数 (`-32000`) | 字符串 (`"internal_error"`) |
|
||||
| 流式支持 | 独立 notification 方法 | 统一 `EventMessage` phase |
|
||||
| 握手 | `handshake` method | `InitializeMessage` type |
|
||||
| 能力声明 | 隐式(method 名称) | 显式 `CapabilityDescriptor` |
|
||||
|
||||
### 运行时对比
|
||||
|
||||
| 特性 | 旧版 | 新版 |
|
||||
|------|------|------|
|
||||
| Peer 抽象 | 分离 `JSONRPCClient/Server` | 统一 `Peer` |
|
||||
| Handler 分发 | 直接调用 `handler(event)` | `HandlerDispatcher` 参数注入 |
|
||||
| 能力路由 | 无显式路由 | `CapabilityRouter` |
|
||||
| 环境管理 | 无 | `PluginEnvironmentManager` 分组 |
|
||||
| 传输层 | 每个实现处理 JSON-RPC | 传输层只处理字符串 |
|
||||
|
||||
### 代码对比
|
||||
|
||||
#### 旧版 Handler
|
||||
|
||||
```python
|
||||
from astrbot.api.star import Star
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
|
||||
class MyPlugin(Star):
|
||||
@command_handler("hello", aliases=["hi"])
|
||||
def hello_handler(self, event: AstrMessageEvent):
|
||||
reply = self.call_context_function("llm_generate", prompt=event.message_plain)
|
||||
event.reply(reply)
|
||||
```
|
||||
|
||||
#### 新版 Handler
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Star, Context, MessageEvent
|
||||
from astrbot_sdk.decorators import on_command
|
||||
|
||||
class MyPlugin(Star):
|
||||
@on_command("hello", aliases=["hi"])
|
||||
async def hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
reply = await ctx.llm.chat(event.text)
|
||||
await event.reply(reply)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 新旧架构对比
|
||||
|
||||
---
|
||||
|
||||
## 插件开发指南
|
||||
|
||||
### v4 原生插件
|
||||
|
||||
#### plugin.yaml
|
||||
|
||||
```yaml
|
||||
_schema_version: 2
|
||||
name: my_plugin
|
||||
author: your_name
|
||||
version: 1.0.0
|
||||
runtime:
|
||||
python: "3.12"
|
||||
components:
|
||||
- class: main:MyPlugin
|
||||
```
|
||||
|
||||
#### main.py
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Star, Context, MessageEvent
|
||||
from astrbot_sdk.decorators import on_command, on_message, provide_capability
|
||||
|
||||
class MyPlugin(Star):
|
||||
# 命令处理器
|
||||
@on_command("hello", aliases=["hi"])
|
||||
async def hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
await event.reply(f"你好,{event.user_id}!")
|
||||
|
||||
# 消息处理器
|
||||
@on_message(keywords=["帮助"])
|
||||
async def help(self, event: MessageEvent, ctx: Context) -> None:
|
||||
await event.reply("可用命令:hello, help")
|
||||
|
||||
# 提供能力
|
||||
@provide_capability(
|
||||
"my_plugin.calculate",
|
||||
description="执行计算",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"x": {"type": "number"}},
|
||||
"required": ["x"]
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {"result": {"type": "number"}},
|
||||
"required": ["result"]
|
||||
}
|
||||
)
|
||||
async def calculate_capability(
|
||||
self,
|
||||
payload: dict,
|
||||
ctx: Context
|
||||
) -> dict:
|
||||
x = payload.get("x", 0)
|
||||
return {"result": x * 2}
|
||||
```
|
||||
|
||||
### 旧版兼容插件
|
||||
|
||||
#### plugin.yaml
|
||||
|
||||
```yaml
|
||||
name: my_old_plugin
|
||||
version: 1.0.0
|
||||
components:
|
||||
- class: main:MyOldPlugin
|
||||
```
|
||||
|
||||
#### main.py
|
||||
|
||||
```python
|
||||
from astrbot.api.star import Star
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
|
||||
class MyOldPlugin(Star):
|
||||
# 旧版装饰器仍然支持
|
||||
@command_handler("old_hello")
|
||||
def old_hello_handler(self, event: AstrMessageEvent):
|
||||
# 旧版 API 调用
|
||||
reply = self.call_context_function("llm_generate", prompt="你好")
|
||||
event.reply(reply)
|
||||
|
||||
# 生命周期钩子
|
||||
async def on_start(self):
|
||||
self.put_kv_data("started", True)
|
||||
|
||||
async def on_stop(self):
|
||||
self.put_kv_data("started", False)
|
||||
```
|
||||
|
||||
### 生命周期钩子
|
||||
|
||||
| 钩子 | 说明 |
|
||||
|------|------|
|
||||
| `on_start()` | 插件启动时调用 |
|
||||
| `on_stop()` | 插件停止时调用 |
|
||||
| `on_error(exc, event, ctx)` | Handler 执行出错时调用 |
|
||||
|
||||
---
|
||||
|
||||
## 关键设计模式
|
||||
|
||||
### 1. 协议优先模式
|
||||
|
||||
- 所有跨进程通信都通过 v4 协议
|
||||
- 传输层只处理字符串,协议由 Peer 层处理
|
||||
- 支持多种传输方式(Stdio, WebSocket)
|
||||
|
||||
### 2. 能力路由模式
|
||||
|
||||
- 显式声明 Capability 和输入/输出 Schema
|
||||
- 通过 CapabilityRouter 统一路由
|
||||
- 支持同步和流式两种调用模式
|
||||
- 冲突处理:保留命名空间冲突直接跳过,非保留命名空间冲突自动添加插件名前缀
|
||||
|
||||
### 3. 环境分组模式
|
||||
|
||||
- 多插件可共享同一 Python 虚拟环境
|
||||
- 按版本和依赖兼容性自动分组
|
||||
- 节省资源,加快启动速度
|
||||
|
||||
### 4. 参数注入模式
|
||||
|
||||
- HandlerDispatcher 支持类型注解注入
|
||||
- 优先级:类型注解 > 参数名 > legacy_args
|
||||
- 支持可选类型 `Optional[Type]`
|
||||
|
||||
### 5. 取消传播模式
|
||||
|
||||
- CancelToken 统一取消机制
|
||||
- 跨进程取消通过 CancelMessage
|
||||
- 早到取消避免竞态条件
|
||||
|
||||
### 6. 插件隔离模式
|
||||
|
||||
- 每个插件运行在独立 Worker 进程
|
||||
- 崩溃不影响其他插件
|
||||
- 支持 GroupWorkerRuntime 共享环境
|
||||
|
||||
### 7. 热重载模式
|
||||
|
||||
- `dev --watch` 支持文件变更检测
|
||||
- 按插件目录清理 `sys.modules` 缓存
|
||||
- 确保代码变更后正确重载
|
||||
|
||||
---
|
||||
|
||||
## 附录:关键文件速查
|
||||
|
||||
| 文件 | 核心类/函数 | 说明 |
|
||||
|------|------------|------|
|
||||
| `src-new/astrbot_sdk/__init__.py` | `Star`, `Context`, `MessageEvent` | 顶层入口 |
|
||||
| `src-new/astrbot_sdk/star.py` | `Star` | v4 原生插件基类 |
|
||||
| `src-new/astrbot_sdk/context.py` | `Context` | 运行时上下文 |
|
||||
| `src-new/astrbot_sdk/decorators.py` | `on_command`, `on_message`, `provide_capability` | v4 装饰器 |
|
||||
| `src-new/astrbot_sdk/errors.py` | `AstrBotError` | 统一错误模型 |
|
||||
| `src-new/astrbot_sdk/cli.py` | CLI 命令 | 命令行工具 |
|
||||
| `src-new/astrbot_sdk/testing.py` | `PluginHarness`, `MockContext` | 测试辅助 |
|
||||
| `src-new/astrbot_sdk/runtime/peer.py` | `Peer` | 协议对等端 |
|
||||
| `src-new/astrbot_sdk/runtime/supervisor.py` | `SupervisorRuntime` | Supervisor 运行时 |
|
||||
| `src-new/astrbot_sdk/runtime/worker.py` | `PluginWorkerRuntime` | Worker 运行时 |
|
||||
| `src-new/astrbot_sdk/runtime/loader.py` | `load_plugin()`, `_ResolvedComponent` | 插件加载 |
|
||||
| `src-new/astrbot_sdk/runtime/handler_dispatcher.py` | `HandlerDispatcher` | Handler 执行分发 |
|
||||
| `src-new/astrbot_sdk/runtime/capability_router.py` | `CapabilityRouter` | Capability 路由 |
|
||||
| `src-new/astrbot_sdk/runtime/environment_groups.py` | `EnvironmentGroup` | 环境分组 |
|
||||
| `src-new/astrbot_sdk/protocol/messages.py` | `InitializeMessage`, `InvokeMessage` | 协议消息 |
|
||||
| `src-new/astrbot_sdk/protocol/descriptors.py` | `HandlerDescriptor`, `CapabilityDescriptor` | 描述符 |
|
||||
| `src-new/astrbot_sdk/clients/_proxy.py` | `CapabilityProxy` | 能力代理 |
|
||||
| `src-new/astrbot_sdk/clients/llm.py` | `LLMClient` | LLM 客户端 |
|
||||
| `src-new/astrbot_sdk/clients/memory.py` | `MemoryClient` | 记忆客户端 |
|
||||
| `src-new/astrbot_sdk/clients/db.py` | `DBClient` | 数据库客户端 |
|
||||
| `src-new/astrbot_sdk/clients/platform.py` | `PlatformClient` | 平台客户端 |
|
||||
| `src-new/astrbot_sdk/clients/http.py` | `HTTPClient` | HTTP 客户端 |
|
||||
| `src-new/astrbot_sdk/clients/metadata.py` | `MetadataClient`, `PluginMetadata` | 元数据客户端 |
|
||||
| `examples/hello_plugin/` | - | 入门示例插件 |
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2026-03-14 (v2)
|
||||
- 添加兼容层弃用通知
|
||||
- 更新目录结构,移除已删除的 `api/` 和 `astrbot/` 目录
|
||||
- 更新内置 Capabilities 列表至 28 个(新增 memory 扩展方法、http、metadata)
|
||||
- 更新客户端方法表,补充完整方法列表
|
||||
- 移除兼容层设计章节(已弃用)
|
||||
- 更新关键文件速查表
|
||||
- 添加热重载模式说明
|
||||
|
||||
### 2026-03-14
|
||||
- 添加环境分组详细说明
|
||||
- 完善 CapabilityRouter 内置能力列表
|
||||
- 添加客户端层架构图
|
||||
- 补充新旧代码对比示例
|
||||
|
||||
### 2026-03-13
|
||||
- 初始版本
|
||||
- 完成整体架构分析
|
||||
- 新旧对比整理
|
||||
|
||||
---
|
||||
|
||||
> 本文档基于 AstrBot SDK 当前版本 (`refact1/refactsome`) 整理
|
||||
> 如有疑问请查阅源代码或提交 Issue
|
||||
@@ -1,532 +0,0 @@
|
||||
# AstrBot SDK v4 新旧对比文档
|
||||
|
||||
## 目录
|
||||
|
||||
1. [整体架构变化](#整体架构变化)
|
||||
2. [文件级对比](#文件级对比)
|
||||
- [__init__.py](#__init__py)
|
||||
- [cli.py](#clipy)
|
||||
- [context.py](#contextpy)
|
||||
- [decorators.py](#decoratorspy)
|
||||
- [events.py](#eventspy)
|
||||
- [star.py](#starpy)
|
||||
- [errors.py](#errorspy)
|
||||
- [compat.py](#compatpy)
|
||||
- [_legacy_api.py](#_legacy_apipy)
|
||||
3. [优点总结](#优点总结)
|
||||
4. [缺点与待改进项](#缺点与待改进项)
|
||||
5. [改进建议](#改进建议)
|
||||
|
||||
---
|
||||
|
||||
## 整体架构变化
|
||||
|
||||
| 维度 | 旧版 (v3) | 新版 (v4) |
|
||||
|------|-----------|-----------|
|
||||
| **架构模式** | 单体架构,插件与核心在同一进程 | 分布式架构,插件独立进程,通过 RPC 通信 |
|
||||
| **Context 设计** | 抽象基类 (ABC),由 Core 实现 | 具体类,通过 CapabilityProxy 代理 |
|
||||
| **文件组织** | 功能分散在子目录 (api/, cli/, runtime/) | 核心概念提升到第一层,便于导入 |
|
||||
| **兼容层** | 无独立兼容层 | 新增 `_legacy_api.py`、`compat.py`,并在 `api/` 下保留薄兼容导出 |
|
||||
| **错误处理** | 无统一错误类 | 新增 AstrBotError 支持跨进程传递 |
|
||||
| **取消控制** | 无统一机制 | 新增 CancelToken 数据类 |
|
||||
|
||||
### 目录结构对比
|
||||
|
||||
```
|
||||
旧版 src/astrbot_sdk/
|
||||
├── __main__.py # 仅入口
|
||||
├── api/ # API 定义
|
||||
│ ├── event/ # 事件相关
|
||||
│ ├── star/ # Star 插件
|
||||
│ └── basic/ # 基础类型
|
||||
├── cli/ # CLI 命令
|
||||
├── runtime/ # 运行时
|
||||
└── tests/ # 测试
|
||||
|
||||
新版 src-new/astrbot_sdk/
|
||||
├── __init__.py # 包入口,导出公共 API
|
||||
├── __main__.py # 入口
|
||||
├── cli.py # CLI 命令(提升到第一层)
|
||||
├── context.py # Context(提升到第一层)
|
||||
├── decorators.py # 装饰器(提升到第一层)
|
||||
├── events.py # 事件类(提升到第一层)
|
||||
├── star.py # Star 基类(提升到第一层)
|
||||
├── errors.py # 错误类(新增)
|
||||
├── compat.py # 兼容层(新增)
|
||||
├── _legacy_api.py # 旧版 API 兼容(新增)
|
||||
├── api/ # API 子模块
|
||||
├── clients/ # 客户端模块(新增)
|
||||
├── protocol/ # 协议模块(新增)
|
||||
└── runtime/ # 运行时
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件级对比
|
||||
|
||||
### __init__.py
|
||||
|
||||
**旧版**: 无 `__init__.py` 文件
|
||||
|
||||
**新版**:
|
||||
```python
|
||||
from .context import Context
|
||||
from .decorators import on_command, on_event, on_message, on_schedule, require_admin
|
||||
from .errors import AstrBotError
|
||||
from .events import MessageEvent
|
||||
from .star import Star
|
||||
```
|
||||
|
||||
| 方面 | 评价 |
|
||||
|------|------|
|
||||
| **优点** | 清晰的公共 API 入口,便于用户导入核心类型 |
|
||||
| **缺点** | 缺少版本号导出,缺少 `__version__` 变量 |
|
||||
| **改进建议** | 添加 `__version__ = "4.0.0"` 便于版本检查 |
|
||||
|
||||
---
|
||||
|
||||
### cli.py
|
||||
|
||||
**旧版位置**: `src/astrbot_sdk/cli/main.py`
|
||||
|
||||
| 对比项 | 旧版 | 新版 |
|
||||
|--------|------|------|
|
||||
| 文件位置 | cli/ 文件夹 | 第一层单文件 |
|
||||
| docstring | 有完整命令文档 | 缺少 docstring |
|
||||
| 日志输出 | 有启动日志 | 无日志输出 |
|
||||
| help 参数 | 完整 | 部分缺失 |
|
||||
|
||||
**优点**:
|
||||
- 简化为单文件,更易维护
|
||||
- 使用 `Path` 类型替代 `str`,类型更明确
|
||||
|
||||
**缺点**:
|
||||
- 缺少命令文档字符串,用户难以通过 `--help` 了解用法
|
||||
- 缺少启动日志,调试困难
|
||||
|
||||
**改进建议**:
|
||||
```python
|
||||
@cli.command()
|
||||
@click.option(...)
|
||||
def run(plugins_dir: Path) -> None:
|
||||
"""Start the plugin supervisor over stdio."""
|
||||
logger.info(f"Starting plugin supervisor with plugins dir: {plugins_dir}")
|
||||
asyncio.run(run_supervisor(plugins_dir=plugins_dir))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### context.py
|
||||
|
||||
**旧版位置**: `src/astrbot_sdk/api/star/context.py`
|
||||
|
||||
| 对比项 | 旧版 | 新版 |
|
||||
|--------|------|------|
|
||||
| 类型 | 抽象基类 (ABC) | 具体类 |
|
||||
| 属性 | conversation_manager, persona_manager | llm, memory, db, platform 客户端 |
|
||||
| 通信方式 | 直接调用 | CapabilityProxy 代理 |
|
||||
| 取消控制 | 无 | CancelToken |
|
||||
|
||||
**优点**:
|
||||
- 分布式架构,插件与核心解耦
|
||||
- 客户端模式清晰,职责单一
|
||||
- CancelToken 提供统一的取消机制
|
||||
|
||||
**缺点**:
|
||||
- 顶层 `Context` 不再直接暴露 `conversation_manager`
|
||||
- 缺少 `persona_manager` 属性
|
||||
- 方法签名变化较大,迁移成本高
|
||||
|
||||
**兼容现状**:
|
||||
- 旧式 `conversation_manager`、`_register_component()`、`call_context_function()` 由 `_legacy_api.py` 中的 `LegacyContext` 承接
|
||||
- legacy 组件在同一插件内会共享一个 `LegacyContext`,保持旧版 `StarManager` 的上下文语义
|
||||
|
||||
**改进建议**:
|
||||
1. 在 clients/ 中添加 `PersonaClient` 补全功能
|
||||
2. 在文档中明确迁移路径:`ctx.llm_generate()` → `ctx.llm.chat_raw()`
|
||||
3. 考虑添加便捷方法减少迁移成本
|
||||
|
||||
---
|
||||
|
||||
### decorators.py
|
||||
|
||||
**旧版位置**: `src/astrbot_sdk/api/event/filter.py`
|
||||
|
||||
| 对比项 | 旧版 | 新版 |
|
||||
|--------|------|------|
|
||||
| 装饰器数量 | 15+ | 顶层最小集 + `api.event.filter` compat 子集 |
|
||||
| 类型定义 | 完整 | 核心最小化,compat 层补回高频入口 |
|
||||
| 钩子装饰器 | 有 | 顶层无,compat 中部分保留名称但显式未实现 |
|
||||
|
||||
**当前 compat 已可运行的装饰器**:
|
||||
- `command`
|
||||
- `regex`
|
||||
- `permission`
|
||||
- `permission_type`
|
||||
|
||||
**当前仍未完整支持,调用会显式抛出 `NotImplementedError` 的装饰器/辅助项**:
|
||||
- `custom_filter`: 自定义过滤器
|
||||
- `event_message_type`: 消息类型过滤
|
||||
- `platform_adapter_type`: 平台类型过滤
|
||||
- `after_message_sent`: 消息发送后钩子
|
||||
- `on_astrbot_loaded`: AstrBot 加载完成钩子
|
||||
- `on_platform_loaded`: 平台加载完成钩子
|
||||
- `on_decorating_result`: 结果装饰钩子
|
||||
- `on_llm_request`: LLM 请求钩子
|
||||
- `on_llm_response`: LLM 响应钩子
|
||||
- `command_group`: 命令组
|
||||
- `llm_tool`: LLM 工具注册
|
||||
|
||||
**优点**:
|
||||
- 简化设计,降低学习曲线
|
||||
- `on_schedule` 为新增功能
|
||||
|
||||
**缺点**:
|
||||
- 高级钩子与扩展过滤器仍不完整,复杂插件不能完全无改动迁移
|
||||
- compat 面分散在顶层 `decorators.py` 与 `api.event.filter`,需要文档明确边界
|
||||
|
||||
**改进建议**:
|
||||
1. 按优先级逐步补全高频未实现装饰器
|
||||
2. 添加 `CustomFilter` 基类支持自定义过滤逻辑
|
||||
3. 优先实现 `llm_tool` 与 LLM 相关钩子
|
||||
|
||||
---
|
||||
|
||||
### events.py
|
||||
|
||||
**旧版位置**: `src/astrbot_sdk/api/event/astr_message_event.py`
|
||||
|
||||
| 对比项 | 旧版 | 新版 |
|
||||
|--------|------|------|
|
||||
| 事件类 | AstrMessageEvent (370+ 行) | MessageEvent (~130 行) |
|
||||
| 属性 | message_obj, platform_meta, role, session 等 | text, user_id, group_id, platform, session_id |
|
||||
| 方法 | 30+ 方法 | reply(), plain_result() |
|
||||
|
||||
**顶层 `events.py` 仍缺失的功能**:
|
||||
- `get_platform_name()`, `get_platform_id()`: 平台信息
|
||||
- `get_messages()`: 获取消息链
|
||||
- `is_private_chat()`, `is_admin()`: 状态判断
|
||||
- `set_result()`, `stop_event()`: 事件控制
|
||||
- `send()`, `react()`: 消息操作
|
||||
|
||||
**已由 compat 子模块补回的旧类型**:
|
||||
- `api.event.AstrMessageEvent`
|
||||
- `api.event.AstrBotMessage`
|
||||
- `api.event.MessageEventResult`
|
||||
- `api.event.MessageSession`
|
||||
- `api.event.MessageType`
|
||||
- `api.platform.PlatformMetadata`
|
||||
|
||||
**优点**:
|
||||
- 简化设计,专注核心功能
|
||||
- 通过 `reply_handler` 实现依赖注入
|
||||
- 支持序列化 (`to_payload`, `from_payload`)
|
||||
- `api.event` compat 层已补回常见旧类型和 `AstrMessageEvent` 包装
|
||||
|
||||
**缺点**:
|
||||
- 顶层 `MessageEvent` 依然是精简模型,rich event 行为主要靠 compat 层兜底
|
||||
- 缺少完整消息链操作能力
|
||||
|
||||
**改进建议**:
|
||||
1. 在 `api/` 子模块中添加扩展事件类
|
||||
2. 添加 `AstrBotMessage` 类型支持富文本消息
|
||||
3. 补充 `MessageType` 枚举用于消息类型判断
|
||||
|
||||
---
|
||||
|
||||
### star.py
|
||||
|
||||
**旧版位置**: `src/astrbot_sdk/api/star/star.py`
|
||||
|
||||
| 对比项 | 旧版 | 新版 |
|
||||
|--------|------|------|
|
||||
| 主要类型 | StarMetadata (dataclass) | Star (基类) |
|
||||
| 元数据管理 | dataclass 字段 | 装饰器自动收集 |
|
||||
| 生命周期 | 无 | on_start, on_stop, on_error |
|
||||
|
||||
**旧版曾依赖的元数据类型**:
|
||||
```python
|
||||
@dataclass
|
||||
class StarMetadata:
|
||||
name: str
|
||||
author: str
|
||||
desc: str
|
||||
version: str
|
||||
repo: str
|
||||
module_path: str
|
||||
root_dir_name: str
|
||||
reserved: bool
|
||||
activated: bool
|
||||
config: dict
|
||||
star_handler_full_names: list[str]
|
||||
display_name: str
|
||||
logo_path: str
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- Star 基类设计清晰,生命周期钩子完整
|
||||
- `__init_subclass__` 自动收集处理器,减少样板代码
|
||||
- `on_error` 提供默认错误处理
|
||||
- `api.star` compat 层已补回 `StarMetadata`
|
||||
|
||||
**缺点**:
|
||||
- 顶层 `star.py` 不直接承载旧版元数据类型,需要通过 `api.star` compat 导入
|
||||
- 类型注解不够精确 (`ctx: Any | None`)
|
||||
|
||||
**改进建议**:
|
||||
1. 添加 `StarMetadata` dataclass 或使用装饰器参数
|
||||
2. 改进类型注解,使用 `Context` 替代 `Any`
|
||||
3. 考虑添加 `__star_metadata__` 类属性存储元信息
|
||||
|
||||
---
|
||||
|
||||
### errors.py
|
||||
|
||||
**旧版**: 无独立 errors.py 文件
|
||||
|
||||
**新版**:
|
||||
```python
|
||||
@dataclass(slots=True)
|
||||
class AstrBotError(Exception):
|
||||
code: str
|
||||
message: str
|
||||
hint: str = ""
|
||||
retryable: bool = False
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 统一的错误表示,便于跨进程传递
|
||||
- 工厂方法设计优雅 (`cancelled()`, `capability_not_found()`)
|
||||
- 支持 `to_payload()` / `from_payload()` 序列化
|
||||
|
||||
**缺点**:
|
||||
- 缺少错误码常量或枚举
|
||||
- 与旧版异常类可能不兼容
|
||||
|
||||
**改进建议**:
|
||||
```python
|
||||
class ErrorCode:
|
||||
CANCELLED = "cancelled"
|
||||
CAPABILITY_NOT_FOUND = "capability_not_found"
|
||||
INVALID_INPUT = "invalid_input"
|
||||
# ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### compat.py
|
||||
|
||||
**旧版**: 无
|
||||
|
||||
**新版**: 兼容层入口之一
|
||||
|
||||
```python
|
||||
from ._legacy_api import (
|
||||
CommandComponent,
|
||||
Context,
|
||||
LegacyContext,
|
||||
LegacyConversationManager,
|
||||
)
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 提供平滑迁移路径
|
||||
- 隔离新旧 API,避免污染主命名空间
|
||||
- 用户可按需导入旧版类型
|
||||
- 可与 `astrbot_sdk.api.*` 薄兼容导出配合使用
|
||||
|
||||
**缺点**:
|
||||
- 仅重新导出,无额外文档
|
||||
- 兼容入口分布在 `compat.py` 与 `api/`,用户可能不清楚何时使用哪一个
|
||||
|
||||
**改进建议**:
|
||||
添加迁移说明文档链接
|
||||
|
||||
---
|
||||
|
||||
### _legacy_api.py
|
||||
|
||||
**旧版**: 功能分散在 `api/star/` 目录
|
||||
|
||||
**新版**: 集中的旧版 API 兼容实现
|
||||
|
||||
**包含类型**:
|
||||
- `LegacyContext`: 旧版 Context 兼容实现
|
||||
- `LegacyConversationManager`: 旧版会话管理器
|
||||
- `CommandComponent`: 旧版命令组件基类
|
||||
|
||||
**优点**:
|
||||
- 完整的旧版方法签名兼容
|
||||
- 渐进式警告,引导用户迁移
|
||||
- 会话数据使用 db 客户端存储
|
||||
- `LegacyContext` 已补回 `_register_component()` / `call_context_function()` 链路
|
||||
- `LegacyConversationManager` 会按旧名称 `ConversationManager.*` 自动注册
|
||||
- loader 会为同一 legacy 插件复用一个 `LegacyContext`
|
||||
|
||||
**缺点**:
|
||||
- 部分方法抛出 `NotImplementedError`:
|
||||
- `get_filtered_conversations()`
|
||||
- `get_human_readable_context()`
|
||||
- `add_llm_tools()`
|
||||
- 缺少旧版依赖类型的兼容导入
|
||||
|
||||
**改进建议**:
|
||||
1. 补全 `NotImplementedError` 方法的实现或提供替代方案
|
||||
2. 添加 `ToolSet`, `FunctionTool`, `Message` 类型的兼容导入
|
||||
3. 更新 `MIGRATION_DOC_URL` 为实际文档地址
|
||||
|
||||
---
|
||||
|
||||
## 优点总结
|
||||
|
||||
### 架构设计
|
||||
|
||||
| 优点 | 说明 |
|
||||
|------|------|
|
||||
| **分布式架构** | 插件独立进程,崩溃不影响核心,提高稳定性 |
|
||||
| **清晰的职责划分** | Context → Clients,每个客户端专注单一能力 |
|
||||
| **统一的取消机制** | CancelToken 提供优雅的中断处理 |
|
||||
| **跨进程错误传递** | AstrBotError 支持序列化,错误信息完整 |
|
||||
| **简化的导入路径** | 核心类型提升到第一层,`from astrbot_sdk import Context` |
|
||||
|
||||
### 兼容性
|
||||
|
||||
| 优点 | 说明 |
|
||||
|------|------|
|
||||
| **平滑迁移** | `compat.py`、`_legacy_api.py` 与 `api/` 薄兼容导出共同提供旧版入口 |
|
||||
| **渐进式警告** | `_warn_once()` 避免重复警告刷屏 |
|
||||
| **文档引导** | 错误消息包含迁移文档链接 |
|
||||
|
||||
---
|
||||
|
||||
## 缺点与待改进项
|
||||
|
||||
### 功能缺失
|
||||
|
||||
| 类别 | 缺失项 | 影响 |
|
||||
|------|--------|------|
|
||||
| **装饰器** | 多个高级装饰器/钩子未实现 | 复杂插件无法完全无改动迁移 |
|
||||
| **事件** | 顶层 rich event 行为仍大幅精简 | 消息处理能力受限 |
|
||||
| **类型** | 部分旧类型只存在于 compat 子模块 | 需要调整导入路径认知 |
|
||||
| **钩子** | on_llm_request, after_message_sent 等 | 无法拦截关键流程 |
|
||||
|
||||
### 文档缺失
|
||||
|
||||
| 类别 | 缺失项 |
|
||||
|------|--------|
|
||||
| **CLI** | 命令 docstring 缺失 |
|
||||
| **迁移** | MIGRATION_DOC_URL 未更新 |
|
||||
| **类型** | 部分类型注解为 `Any` |
|
||||
|
||||
### 实现不完整
|
||||
|
||||
| 类别 | 问题 |
|
||||
|------|------|
|
||||
| **_legacy_api.py** | 3 个方法抛出 NotImplementedError |
|
||||
| **clients/** | 缺少 PersonaClient |
|
||||
| **compat 文档** | `compat.py` 与 `api/` 的边界说明仍不足 |
|
||||
|
||||
---
|
||||
|
||||
## 改进建议
|
||||
|
||||
### 短期(优先级高)
|
||||
|
||||
1. **补全 CLI 文档字符串**
|
||||
- 为所有命令添加 docstring
|
||||
- 补充 help 参数
|
||||
- 添加启动日志
|
||||
|
||||
2. **更新 MIGRATION_DOC_URL**
|
||||
- 指向实际迁移文档
|
||||
|
||||
3. **补全 NotImplementedError 方法**
|
||||
- 为 `get_filtered_conversations()` 提供替代实现
|
||||
- 或在文档中明确说明替代方案
|
||||
- 保持 `call_context_function()` 的 `{data: ...}` 旧语义不变
|
||||
|
||||
### 中期
|
||||
|
||||
4. **添加 StarMetadata 支持**
|
||||
```python
|
||||
@dataclass
|
||||
class StarMetadata:
|
||||
name: str = ""
|
||||
author: str = ""
|
||||
version: str = "1.0.0"
|
||||
# ...
|
||||
```
|
||||
|
||||
5. **补全关键装饰器与钩子**
|
||||
- `llm_tool`: LLM 工具注册
|
||||
- `custom_filter`: 自定义过滤
|
||||
- `on_llm_request` / `on_llm_response`: LLM 钩子
|
||||
|
||||
6. **添加缺失类型**
|
||||
- 优先考虑顶层直出或统一导入文档,而不是重复实现 compat 类型
|
||||
|
||||
### 长期
|
||||
|
||||
7. **扩展 MessageEvent**
|
||||
- 添加消息链操作方法
|
||||
- 支持平台特定功能
|
||||
|
||||
8. **添加 PersonaClient**
|
||||
- 在 clients/ 中实现 Persona 管理
|
||||
|
||||
9. **完善类型系统**
|
||||
- 减少使用 `Any`
|
||||
- 添加 Protocol 定义
|
||||
|
||||
---
|
||||
|
||||
## 迁移示例
|
||||
|
||||
### 基础插件迁移
|
||||
|
||||
**旧版**:
|
||||
```python
|
||||
from astrbot_sdk.api.star import Context, Star, command
|
||||
|
||||
class MyPlugin(Star):
|
||||
@command("hello")
|
||||
async def hello(self, ctx: Context, event):
|
||||
await ctx.send_message(event.session, "Hello!")
|
||||
```
|
||||
|
||||
**新版**:
|
||||
```python
|
||||
from astrbot_sdk import Star, Context, on_command
|
||||
|
||||
class MyPlugin(Star):
|
||||
@on_command("hello")
|
||||
async def hello(self, ctx: Context, event):
|
||||
await ctx.platform.send(event.session_id, "Hello!")
|
||||
```
|
||||
|
||||
### 兼容模式
|
||||
|
||||
**旧版代码保持不变**:
|
||||
```python
|
||||
from astrbot_sdk.compat import Context, CommandComponent
|
||||
|
||||
class MyPlugin(CommandComponent):
|
||||
# 使用旧版 API,会有警告提示
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
新版 SDK 在架构设计上有明显改进,分布式架构提高了系统稳定性,清晰的责任划分便于维护和扩展。兼容层的引入为旧版插件提供了平滑的迁移路径。
|
||||
|
||||
主要不足在于高级装饰器/钩子覆盖仍不完整,顶层事件模型仍偏精简,而兼容入口又分散在 `compat.py`、`_legacy_api.py` 与 `api/` 薄导出之间。建议继续按优先级补全高频能力,并把兼容边界写清楚,避免把“已迁移”误判成“缺失”。
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| **架构设计** | ⭐⭐⭐⭐⭐ | 分布式架构,解耦清晰 |
|
||||
| **功能完整** | ⭐⭐⭐ | 核心功能完整,高级功能待补全 |
|
||||
| **兼容性** | ⭐⭐⭐⭐ | 兼容层设计良好,部分方法待实现 |
|
||||
| **文档** | ⭐⭐ | 代码注释完整,用户文档待补充 |
|
||||
| **类型系统** | ⭐⭐⭐ | 基础类型完整,部分使用 Any |
|
||||
@@ -1,467 +0,0 @@
|
||||
# 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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,370 +0,0 @@
|
||||
# 数据库客户端
|
||||
|
||||
数据库客户端提供键值存储能力,用于持久化插件数据。
|
||||
|
||||
## 概述
|
||||
|
||||
```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/)
|
||||
@@ -1,283 +0,0 @@
|
||||
# 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/)
|
||||
@@ -1,309 +0,0 @@
|
||||
# 记忆客户端
|
||||
|
||||
记忆客户端提供 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 能力
|
||||
@@ -1,320 +0,0 @@
|
||||
# 平台客户端
|
||||
|
||||
平台客户端提供向聊天平台发送消息和获取信息的能力。
|
||||
|
||||
## 概述
|
||||
|
||||
```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)
|
||||
@@ -1,55 +0,0 @@
|
||||
# 示例插件索引
|
||||
|
||||
这里收集了 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/)
|
||||
@@ -1,478 +0,0 @@
|
||||
# 数据库插件示例
|
||||
|
||||
本示例演示如何使用数据库客户端存储和管理插件数据。
|
||||
|
||||
## 完整代码
|
||||
|
||||
### 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)
|
||||
@@ -1,333 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,164 +0,0 @@
|
||||
# AstrBot SDK v4 Quickstart
|
||||
|
||||
这份 quickstart 只覆盖当前已经落地的能力:`Star`、`Context`、`MessageEvent`、`astr dev --local`、`astrbot_sdk.testing`。
|
||||
|
||||
## 1. 创建插件目录
|
||||
|
||||
现在可以直接生成一个符合当前 loader 契约的骨架:
|
||||
|
||||
```bash
|
||||
astrbot-sdk init my-plugin
|
||||
```
|
||||
|
||||
生成结果大致是:
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
├── plugin.yaml
|
||||
├── requirements.txt
|
||||
├── main.py
|
||||
└── tests/
|
||||
└── test_plugin.py
|
||||
```
|
||||
|
||||
如果你想手动创建,目录结构也至少应包含这些文件。`requirements.txt` 可以先留空。
|
||||
|
||||
## 2. 编写 `plugin.yaml`
|
||||
|
||||
```yaml
|
||||
name: my_plugin
|
||||
display_name: My Plugin
|
||||
desc: 我的第一个 AstrBot SDK v4 插件
|
||||
author: you
|
||||
version: 0.1.0
|
||||
runtime:
|
||||
python: "3.12"
|
||||
components:
|
||||
- class: main:MyPlugin
|
||||
```
|
||||
|
||||
## 3. 编写 `main.py`
|
||||
|
||||
```python
|
||||
from astrbot_sdk import Context, MessageEvent, Star, on_command
|
||||
|
||||
|
||||
class MyPlugin(Star):
|
||||
@on_command("hello")
|
||||
async def hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
reply = await ctx.llm.chat("say hello")
|
||||
await event.reply(reply)
|
||||
```
|
||||
|
||||
## 4. 本地运行
|
||||
|
||||
安装当前仓库后,可以直接用本地 mock core 跑插件:
|
||||
|
||||
```bash
|
||||
astr dev --local --plugin-dir my-plugin --event-text "hello"
|
||||
```
|
||||
|
||||
或者:
|
||||
|
||||
```bash
|
||||
astrbot-sdk dev --local --plugin-dir my-plugin --event-text "hello"
|
||||
```
|
||||
|
||||
进入交互模式:
|
||||
|
||||
```bash
|
||||
astr dev --local --plugin-dir my-plugin --interactive
|
||||
```
|
||||
|
||||
交互模式下支持这些元命令:
|
||||
|
||||
- `/session <id>` 切换 session
|
||||
- `/user <id>` 切换 user
|
||||
- `/platform <name>` 切换 platform
|
||||
- `/group <id>` 切换为群消息
|
||||
- `/private` 切回私聊
|
||||
- `/event <type>` 切换事件类型
|
||||
- `/exit` 退出
|
||||
|
||||
## 4.1 校验与打包
|
||||
|
||||
本地写完插件后,可以先做静态校验,再构建 zip 包:
|
||||
|
||||
```bash
|
||||
astrbot-sdk validate --plugin-dir my-plugin
|
||||
astrbot-sdk build --plugin-dir my-plugin
|
||||
```
|
||||
|
||||
默认构建产物会写到 `my-plugin/dist/`。
|
||||
|
||||
## 5. 直接写 handler 单元测试
|
||||
|
||||
如果你不想每次都起完整 harness,可以直接用 `MockContext` 和 `MockMessageEvent`:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.testing import MockContext, MockMessageEvent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hello_handler():
|
||||
ctx = MockContext(plugin_id="demo")
|
||||
event = MockMessageEvent(text="hello", context=ctx)
|
||||
ctx.llm.mock_response("你好!")
|
||||
|
||||
async def handler(event, ctx):
|
||||
text = await ctx.llm.chat("hello")
|
||||
await event.reply(text)
|
||||
|
||||
await handler(event, ctx)
|
||||
|
||||
assert event.replies == ["你好!"]
|
||||
ctx.platform.assert_sent("你好!")
|
||||
```
|
||||
|
||||
## 6. 用 `PluginHarness` 跑真实插件
|
||||
|
||||
如果你想复用真实的 `loader` / `HandlerDispatcher` / compat 链路,用 `PluginHarness`:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_directory():
|
||||
harness = PluginHarness(
|
||||
LocalRuntimeConfig(plugin_dir=Path("my-plugin")),
|
||||
)
|
||||
|
||||
async with harness:
|
||||
records = await harness.dispatch_text("hello")
|
||||
|
||||
assert any(item.text for item in records)
|
||||
```
|
||||
|
||||
## 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 执行器
|
||||
|
||||
如果你需要查看当前架构与兼容边界,请看 [ARCHITECTURE.md](../../ARCHITECTURE.md)。
|
||||
@@ -1,565 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# AstrBot 配置文件
|
||||
|
||||
## data/cmd_config.json
|
||||
|
||||
AstrBot 的配置文件是一个 JSON 格式的文件。AstrBot 会在启动时读取这个文件,并根据文件中的配置来初始化 AstrBot,其路径位于 `data/cmd_config.json`。
|
||||
|
||||
> 在 AstrBot v4.0.0 版本及之后,我们引入了[多配置文件](https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)的概念。`data/cmd_config.json` 作为默认配置文件 `default`。其他您在 WebUI 新建的配置文件会存储在 `data/config/` 目录下,以 `abconf_` 开头。
|
||||
|
||||
AstrBot 默认配置如下:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"config_version": 2,
|
||||
"platform_settings": {
|
||||
"unique_session": False,
|
||||
"rate_limit": {
|
||||
"time": 60,
|
||||
"count": 30,
|
||||
"strategy": "stall", # stall, discard
|
||||
},
|
||||
"reply_prefix": "",
|
||||
"forward_threshold": 1500,
|
||||
"enable_id_white_list": True,
|
||||
"id_whitelist": [],
|
||||
"id_whitelist_log": True,
|
||||
"wl_ignore_admin_on_group": True,
|
||||
"wl_ignore_admin_on_friend": True,
|
||||
"reply_with_mention": False,
|
||||
"reply_with_quote": False,
|
||||
"path_mapping": [],
|
||||
"segmented_reply": {
|
||||
"enable": False,
|
||||
"only_llm_result": True,
|
||||
"interval_method": "random",
|
||||
"interval": "1.5,3.5",
|
||||
"log_base": 2.6,
|
||||
"words_count_threshold": 150,
|
||||
"regex": ".*?[。?!~…]+|.+$",
|
||||
"content_cleanup_rule": "",
|
||||
},
|
||||
"no_permission_reply": True,
|
||||
"empty_mention_waiting": True,
|
||||
"empty_mention_waiting_need_reply": True,
|
||||
"friend_message_needs_wake_prefix": False,
|
||||
"ignore_bot_self_message": False,
|
||||
"ignore_at_all": False,
|
||||
},
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"enable": True,
|
||||
"default_provider_id": "",
|
||||
"default_image_caption_provider_id": "",
|
||||
"image_caption_prompt": "Please describe the image using Chinese.",
|
||||
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者
|
||||
"wake_prefix": "",
|
||||
"web_search": False,
|
||||
"websearch_provider": "default",
|
||||
"websearch_tavily_key": [],
|
||||
"web_search_link": False,
|
||||
"display_reasoning_text": False,
|
||||
"identifier": False,
|
||||
"group_name_display": False,
|
||||
"datetime_system_prompt": True,
|
||||
"default_personality": "default",
|
||||
"persona_pool": ["*"],
|
||||
"prompt_prefix": "{{prompt}}",
|
||||
"max_context_length": -1,
|
||||
"dequeue_context_length": 1,
|
||||
"streaming_response": False,
|
||||
"show_tool_use_status": False,
|
||||
"streaming_segmented": False,
|
||||
"max_agent_step": 30,
|
||||
"tool_call_timeout": 60,
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": False,
|
||||
"provider_id": "",
|
||||
},
|
||||
"provider_tts_settings": {
|
||||
"enable": False,
|
||||
"provider_id": "",
|
||||
"dual_output": False,
|
||||
"use_file_service": False,
|
||||
},
|
||||
"provider_ltm_settings": {
|
||||
"group_icl_enable": False,
|
||||
"group_message_max_cnt": 300,
|
||||
"image_caption": False,
|
||||
"active_reply": {
|
||||
"enable": False,
|
||||
"method": "possibility_reply",
|
||||
"possibility_reply": 0.1,
|
||||
"whitelist": [],
|
||||
},
|
||||
},
|
||||
"content_safety": {
|
||||
"also_use_in_response": False,
|
||||
"internal_keywords": {"enable": True, "extra_keywords": []},
|
||||
"baidu_aip": {"enable": False, "app_id": "", "api_key": "", "secret_key": ""},
|
||||
},
|
||||
"admins_id": ["astrbot"],
|
||||
"t2i": False,
|
||||
"t2i_word_threshold": 150,
|
||||
"t2i_strategy": "remote",
|
||||
"t2i_endpoint": "",
|
||||
"t2i_use_file_service": False,
|
||||
"t2i_active_template": "base",
|
||||
"http_proxy": "",
|
||||
"no_proxy": ["localhost", "127.0.0.1", "::1"],
|
||||
"dashboard": {
|
||||
"enable": True,
|
||||
"username": "astrbot",
|
||||
"password": "77b90590a8945a7d36c963981a307dc9",
|
||||
"jwt_secret": "",
|
||||
"host": "0.0.0.0",
|
||||
"port": 6185,
|
||||
},
|
||||
"platform": [],
|
||||
"platform_specific": {
|
||||
# 平台特异配置:按平台分类,平台下按功能分组
|
||||
"lark": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["Typing"]},
|
||||
},
|
||||
"telegram": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
|
||||
},
|
||||
"discord": {
|
||||
"pre_ack_emoji": {"enable": False, "emojis": ["🤔"]},
|
||||
},
|
||||
},
|
||||
"wake_prefix": ["/"],
|
||||
"log_level": "INFO",
|
||||
"trace_enable": False,
|
||||
"pip_install_arg": "",
|
||||
"pypi_index_url": "https://mirrors.aliyun.com/pypi/simple/",
|
||||
"persona": [], # deprecated
|
||||
"timezone": "Asia/Shanghai",
|
||||
"callback_api_base": "",
|
||||
"default_kb_collection": "", # 默认知识库名称
|
||||
"plugin_set": ["*"], # "*" 表示使用所有可用的插件, 空列表表示不使用任何插件
|
||||
}
|
||||
```
|
||||
|
||||
## 字段详解
|
||||
|
||||
### `config_version`
|
||||
|
||||
配置文件版本,请勿修改。
|
||||
|
||||
### `platform_settings`
|
||||
|
||||
消息平台适配器的通用设置。
|
||||
|
||||
#### `platform_settings.unique_session`
|
||||
|
||||
是否启用会话隔离。默认为 `false`。启用后,在群组或者频道中,每个人的对话的上下文都是独立的。
|
||||
|
||||
#### `platform_settings.rate_limit`
|
||||
|
||||
当消息速率超过限制时的处理策略。`time` 为时间窗口,`count` 为消息数量,`strategy` 为限制策略。`stall` 为等待,`discard` 为丢弃。
|
||||
|
||||
#### `platform_settings.reply_prefix`
|
||||
|
||||
回复消息时的固定前缀字符串。默认为空。
|
||||
|
||||
#### `platform_settings.forward_threshold`
|
||||
|
||||
> 目前仅 QQ 平台适配器适用。
|
||||
|
||||
消息转发阈值。当回复内容超过一定字数后,机器人会将消息折叠成 QQ 群聊的 “转发消息”,以防止刷屏。
|
||||
|
||||
#### `platform_settings.enable_id_white_list`
|
||||
|
||||
是否启用 ID 白名单。默认为 `true`。启用后,只有在白名单中的 ID 发来的消息才会被处理。
|
||||
|
||||
#### `platform_settings.id_whitelist`
|
||||
|
||||
ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。为空时表示不启用白名单过滤。可以使用 `/sid` 指令获取在某个平台上的会话 ID。
|
||||
|
||||
也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志,格式类似 `aiocqhttp:GroupMessage:547540978`
|
||||
|
||||
#### `platform_settings.id_whitelist_log`
|
||||
|
||||
是否打印未通过 ID 白名单的消息日志。默认为 `true`。
|
||||
|
||||
#### `platform_settings.wl_ignore_admin_on_group` & `platform_settings.wl_ignore_admin_on_friend`
|
||||
|
||||
- `wl_ignore_admin_on_group`: 是否管理员发送的群组消息无视 ID 白名单。默认为 `true`。
|
||||
|
||||
- `wl_ignore_admin_on_friend`: 是否管理员发送的私聊消息无视 ID 白名单。默认为 `true`。
|
||||
|
||||
#### `platform_settings.reply_with_mention`
|
||||
|
||||
是否在回复消息时 @ 提到用户。默认为 `false`。
|
||||
|
||||
#### `platform_settings.reply_with_quote`
|
||||
|
||||
是否在回复消息时引用用户的消息。默认为 `false`。
|
||||
|
||||
#### `platform_settings.path_mapping`
|
||||
|
||||
*该配置项已经在 v4.0.0 版本之后被废弃。*
|
||||
|
||||
路径映射列表。用于将消息中的文件路径进行替换。每个映射项包含 `from` 和 `to` 两个字段,表示将消息中的 `from` 路径替换为 `to` 路径。
|
||||
|
||||
#### `platform_settings.segmented_reply`
|
||||
|
||||
分段回复设置。
|
||||
|
||||
- `enable`: 是否启用分段回复。默认为 `false`。
|
||||
- `only_llm_result`: 是否仅对 LLM 生成的回复进行分段。默认为 `true`。
|
||||
- `interval_method`: 分段间隔方法。可选值为 `random` 和 `log`。默认为 `random`。
|
||||
- `interval`: 分段间隔时间。对于 `random` 方法,填写两个逗号分隔的数字,表示最小和最大间隔时间(单位:秒)。对于 `log` 方法,填写一个数字,表示对数基底。默认为 `"1.5,3.5"`。
|
||||
- `log_base`: 对数基底,仅在 `interval_method` 为 `log` 时适用。默认为 `2.6`。
|
||||
- `words_count_threshold`: 分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 `150`。
|
||||
- `regex`: 用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。`re.findall(r'<regex>', text)`。默认值为 `".*?[。?!~…]+|.+$"`。
|
||||
- `content_cleanup_rule`: 移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。`re.sub(r'<regex>', '', text)`。
|
||||
|
||||
#### `platform_settings.no_permission_reply`
|
||||
|
||||
是否在用户没有权限时回复无权限的提示消息。默认为 `true`。
|
||||
|
||||
#### `platform_settings.empty_mention_waiting`
|
||||
|
||||
是否启用空 @ 等待机制。默认为 `true`。启用后,当用户发送一条仅包含 @ 机器人的消息时,机器人会等待用户在 60 秒内发送下一条消息,并将两条消息合并后进行处理。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。
|
||||
|
||||
#### `platform_settings.empty_mention_waiting_need_reply`
|
||||
|
||||
在上面一个配置项(`empty_mention_waiting`)中,如果启用了触发等待,启用此项后,机器人会立即使用 LLM 生成一条回复。否则,将不回复而只是等待。默认为 `true`。
|
||||
|
||||
#### `platform_settings.friend_message_needs_wake_prefix`
|
||||
|
||||
是否在消息平台的私聊消息中需要唤醒前缀。默认为 `false`。启用后,在私聊消息中,用户需要使用唤醒前缀才能触发机器人的响应。
|
||||
|
||||
#### `platform_settings.ignore_bot_self_message`
|
||||
|
||||
是否忽略机器人自己发送的消息。默认为 `false`。启用后,机器人将不会处理自己发送的消息,在某些平台可以防止死循环。
|
||||
|
||||
#### `platform_settings.ignore_at_all`
|
||||
|
||||
是否忽略 @ 全体成员的消息。默认为 `false`。启用后,机器人将不会响应包含 @ 全体成员的消息。
|
||||
|
||||
### `provider`
|
||||
|
||||
> 此配置项仅在 `data/cmd_config.json` 中生效,AstrBot 不会读取 `data/config/` 目录下的配置文件中的此项。
|
||||
|
||||
已配置的模型服务提供商的配置列表。
|
||||
|
||||
### `provider_settings`
|
||||
|
||||
大语言模型提供商的通用设置。
|
||||
|
||||
#### `provider_settings.enable`
|
||||
|
||||
是否启用大语言模型聊天。默认为 `true`。
|
||||
|
||||
#### `provider_settings.default_provider_id`
|
||||
|
||||
默认的对话模型提供商 ID。必须是 `provider` 列表中已配置的提供商 ID。如果为空,则使用配置列表中的第一个对话模型提供商。
|
||||
|
||||
#### `provider_settings.default_image_caption_provider_id`
|
||||
|
||||
默认的图像描述模型提供商 ID。必须是 `provider` 列表中已配置的提供商 ID。如果为空,则代表不使用图像描述功能。
|
||||
|
||||
此配置项的意思是,当用户发送一张图片时,AstrBot 会使用此提供商来生成对图片的描述文本,并将描述文本作为对话的上下文之一。这在对话模型不支持多模态输入时特别有用。
|
||||
|
||||
#### `provider_settings.image_caption_prompt`
|
||||
|
||||
图像描述的提示词模板。默认为 `"Please describe the image using Chinese."`。
|
||||
|
||||
#### `provider_settings.provider_pool`
|
||||
|
||||
*此配置项尚未实际使用*
|
||||
|
||||
#### `provider_settings.wake_prefix`
|
||||
|
||||
使用 LLM 聊天额外的触发条件。如填写 `chat`,则需要发送消息时要以 `/chat` 才能触发 LLM 聊天。其中 `/` 是机器人的唤醒前缀。是一个防止滥用的手段。
|
||||
|
||||
#### `provider_settings.web_search`
|
||||
|
||||
是否启用 AstrBot 自带的网页搜索能力。默认为 `false`。启用后,LLM 可能会自动搜索网页并根据内容回答。
|
||||
|
||||
#### `provider_settings.websearch_provider`
|
||||
|
||||
网页搜索提供商类型。默认为 `default`。目前支持 `default` 和 `tavily`。
|
||||
|
||||
- `default`:能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。
|
||||
|
||||
- `tavily`:使用 Tavily 搜索引擎。
|
||||
|
||||
#### `provider_settings.websearch_tavily_key`
|
||||
|
||||
Tavily 搜索引擎的 API Key 列表。使用 `tavily` 作为网页搜索提供商时需要填写。
|
||||
|
||||
#### `provider_settings.web_search_link`
|
||||
|
||||
是否在回复中提示模型附上搜索结果的链接。默认为 `false`。
|
||||
|
||||
#### `provider_settings.display_reasoning_text`
|
||||
|
||||
是否在回复中显示模型的推理过程。默认为 `false`。
|
||||
|
||||
#### `provider_settings.identifier`
|
||||
|
||||
是否在 Prompt 前加上群成员的名字以让模型更好地了解群聊状态。默认为 `false`。启用将略微增加 token 开销。
|
||||
|
||||
#### `provider_settings.group_name_display`
|
||||
|
||||
是否在提示模型了解所在群的名称。默认为 `false`。此配置项目前仅在 QQ 平台适配器中生效。
|
||||
|
||||
#### `provider_settings.datetime_system_prompt`
|
||||
|
||||
是否在系统提示词中加上当前机器的日期时间。默认为 `true`。
|
||||
|
||||
#### `provider_settings.default_personality`
|
||||
|
||||
默认使用的人格的 ID。请在 WebUI 配置人格。
|
||||
|
||||
#### `provider_settings.persona_pool`
|
||||
|
||||
*此配置项尚未实际使用*
|
||||
|
||||
#### `provider_settings.prompt_prefix`
|
||||
|
||||
用户提示词。可使用 `{{prompt}}` 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。
|
||||
|
||||
#### `provider_settings.max_context_length`
|
||||
|
||||
当对话上下文超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条。-1 为不限制。
|
||||
|
||||
#### `provider_settings.dequeue_context_length`
|
||||
|
||||
当触发上面提到的 `max_context_length` 限制时,每次丢弃的对话轮数。
|
||||
|
||||
#### `provider_settings.streaming_response`
|
||||
|
||||
是否启用流式响应。默认为 `false`。启用后,模型的回复会实时类似打字机的效果发送给用户。此配置项仅在 WebChat、Telegram、飞书平台生效。
|
||||
|
||||
#### `provider_settings.show_tool_use_status`
|
||||
|
||||
是否显示工具使用状态。默认为 `false`。启用后,模型在使用工具时会显示工具的名称和输入参数。
|
||||
|
||||
#### `provider_settings.streaming_segmented`
|
||||
|
||||
不支持流式响应的消息平台是否降级为使用分段回复。默认为 `false`。意思是,如果启用了流式响应,但当前消息平台不支持流式响应,那么是否使用分段多次回复来代替。
|
||||
|
||||
#### `provider_settings.max_agent_step`
|
||||
|
||||
Agent 最大步骤数限制。默认为 `30`。模型的每次工具调用算作一步。
|
||||
|
||||
#### `provider_settings.tool_call_timeout`
|
||||
|
||||
Added in `v4.3.5`
|
||||
|
||||
工具调用的最大超时时间(秒),默认为 `60` 秒。
|
||||
|
||||
#### `provider_stt_settings`
|
||||
|
||||
语音转文本服务提供商的通用设置。
|
||||
|
||||
#### `provider_stt_settings.enable`
|
||||
|
||||
是否启用语音转文本服务。默认为 `false`。
|
||||
|
||||
#### `provider_stt_settings.provider_id`
|
||||
|
||||
语音转文本服务提供商 ID。必须是 `provider` 列表中已配置的 STT 提供商 ID。
|
||||
|
||||
#### `provider_tts_settings`
|
||||
|
||||
文本转语音服务提供商的通用设置。
|
||||
|
||||
#### `provider_tts_settings.enable`
|
||||
|
||||
是否启用文本转语音服务。默认为 `false`。
|
||||
|
||||
#### `provider_tts_settings.provider_id`
|
||||
|
||||
文本转语音服务提供商 ID。必须是 `provider` 列表中已配置的 TTS 提供商 ID。
|
||||
|
||||
#### `provider_tts_settings.dual_output`
|
||||
|
||||
是否启用双输出。默认为 `false`。启用后,机器人会同时发送文本和语音消息。
|
||||
|
||||
#### `provider_tts_settings.use_file_service`
|
||||
|
||||
是否启用文件服务。默认为 `false`。启用后,机器人会将输出的语音文件以 HTTP 文件外链的形式提供给消息平台。此配置项依赖于 `callback_api_base` 的配置。
|
||||
|
||||
#### `provider_ltm_settings`
|
||||
|
||||
群聊上下文感知服务提供商的通用设置。
|
||||
|
||||
#### `provider_ltm_settings.group_icl_enable`
|
||||
|
||||
是否启用群聊上下文感知。默认为 `false`。启用后,机器人会记录群聊中的对话内容,以便更好地理解群聊的上下文。
|
||||
|
||||
上下文的内容会被放在对话的系统提示词中。
|
||||
|
||||
#### `provider_ltm_settings.group_message_max_cnt`
|
||||
|
||||
群聊消息的最大记录数量。默认为 `100`。超过此数量的消息将被丢弃。
|
||||
|
||||
#### `provider_ltm_settings.image_caption`
|
||||
|
||||
是否记录群聊中的图片,并自动使用图像描述模型生成图片的描述文本。默认为 `false`。此配置项依赖于 `provider_settings.default_image_caption_provider_id` 的配置。请谨慎使用,因为这可能会增加大量的 API 调用和 token 开销。
|
||||
|
||||
#### `provider_ltm_settings.active_reply`
|
||||
|
||||
- `enable`: 是否启用主动回复。默认为 `false`。
|
||||
- `method`: 主动回复的方法。可选值为 `possibility_reply`。
|
||||
- `possibility_reply`: 主动回复的概率。默认为 `0.1`。仅在 `method` 为 `possibility_reply` 时适用。
|
||||
- `whitelist`: 主动回复的 ID 白名单。仅在此列表中的 ID 才会触发主动回复。为空时表示不启用白名单过滤。可以使用 `/sid` 指令获取在某个平台上的会话 ID。
|
||||
|
||||
### `content_safety`
|
||||
|
||||
内容安全设置。
|
||||
|
||||
#### `content_safety.also_use_in_response`
|
||||
|
||||
是否在 LLM 回复中也进行内容安全检查。默认为 `false`。启用后,机器人生成的回复也会经过内容安全检查,以防止生成不当内容。
|
||||
|
||||
#### `content_safety.internal_keywords`
|
||||
|
||||
内部关键词检测设置。
|
||||
|
||||
- `enable`: 是否启用内部关键词检测。默认为 `true`。
|
||||
- `extra_keywords`: 额外的关键词列表,支持正则表达式。默认为空。
|
||||
|
||||
#### `content_safety.baidu_aip`
|
||||
|
||||
百度 AI 内容审核设置。
|
||||
|
||||
- `enable`: 是否启用百度 AI 内容审核。默认为 `false`。
|
||||
- `app_id`: 百度 AI 内容审核的 App ID。
|
||||
- `api_key`: 百度 AI 内容审核的 API Key。
|
||||
- `secret_key`: 百度 AI 内容审核的 Secret Key。
|
||||
|
||||
> [!TIP]
|
||||
> 如果要启用百度 AI 内容审核,请先 `pip install baidu-aip`。
|
||||
|
||||
### `admins_id`
|
||||
|
||||
管理员 ID 列表。此外,还可以使用 `/op`, `/deop` 指令来添加或删除管理员。
|
||||
|
||||
### `t2i`
|
||||
|
||||
是否启用文本转图像功能。默认为 `false`。启用后,当用户发送的消息超过一定字数时,机器人会将消息渲染成图片发送给用户,以提高可读性并防止刷屏。支持 Markdown 渲染。
|
||||
|
||||
### `t2i_word_threshold`
|
||||
|
||||
文本转图像的字数阈值。默认为 `150`。当用户发送的消息超过此字数时,机器人会将消息渲染成图片发送给用户。
|
||||
|
||||
### `t2i_strategy`
|
||||
|
||||
文本转图像的渲染策略。可选值为 `local` 和 `remote`。默认为 `remote`。
|
||||
|
||||
- `local`: 使用 AstrBot 本地的文本转图像服务进行渲染。效果较差,但不依赖外部服务。
|
||||
- `remote`: 使用远程的文本转图像服务进行渲染。默认使用 AstrBot 官方提供的服务,效果较好。
|
||||
|
||||
### `t2i_endpoint`
|
||||
|
||||
AstrBot API 的地址。用于渲染 Markdown 图片。当 `t2i_strategy` 为 `remote` 时生效。默认为空,表示使用 AstrBot 官方提供的服务。
|
||||
|
||||
### `t2i_use_file_service`
|
||||
|
||||
是否启用文件服务。默认为 `false`。启用后,机器人会将渲染的图片以 HTTP 文件外链的形式提供给消息平台。此配置项依赖于 `callback_api_base` 的配置。
|
||||
|
||||
### `http_proxy`
|
||||
|
||||
HTTP 代理。如 `http://localhost:7890`。
|
||||
|
||||
### `no_proxy`
|
||||
|
||||
不使用代理的地址列表。如 `["localhost", "127.0.0.1"]`。
|
||||
|
||||
### `dashboard`
|
||||
|
||||
AstrBot WebUI 配置。
|
||||
|
||||
请不要随意修改 `password` 的值。它是一个经过 `md5` 编码的密码。请在控制面板修改密码。
|
||||
|
||||
- `enable`: 是否启用 AstrBot WebUI。默认为 `true`。
|
||||
- `username`: AstrBot WebUI 的用户名。默认为 `astrbot`。
|
||||
- `password`: AstrBot WebUI 的密码。默认为 `astrbot` 的 `md5` 编码值。请勿直接修改,除非您知道自己在做什么。
|
||||
- `jwt_secret`: JWT 的密钥。AstrBot 会在初始化时随机生成。请勿修改,除非您知道自己在做什么。
|
||||
- `host`: AstrBot WebUI 监听的地址。默认为 `0.0.0.0`。
|
||||
- `port`: AstrBot WebUI 监听的端口。默认为 `6185`。
|
||||
|
||||
### `platform`
|
||||
|
||||
> 此配置项仅在 `data/cmd_config.json` 中生效,AstrBot 不会读取 `data/config/` 目录下的配置文件中的此项。
|
||||
|
||||
已配置的 AstrBot 消息平台适配器的配置列表。
|
||||
|
||||
### `platform_specific`
|
||||
|
||||
平台特异配置。按平台分类,平台下按功能分组。
|
||||
|
||||
#### `platform_specific.<platform>.pre_ack_emoji`
|
||||
|
||||
启用后,当请求 LLM 前,AstrBot 会先发送一个预回复的表情以告知用户正在处理请求。此功能目前仅在飞书平台适配器和 Telegram 中生效。
|
||||
|
||||
##### lark (飞书)
|
||||
|
||||
- `enable`: 是否启用飞书消息预回复表情。默认为 `false`。
|
||||
- `emojis`: 预回复的表情列表。默认为 `["Typing"]`。表情枚举名参考:[表情文案说明](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce)
|
||||
|
||||
##### telegram
|
||||
|
||||
- `enable`: 是否启用 Telegram 消息预回复表情。默认为 `false`。
|
||||
- `emojis`: 预回复的表情列表。默认为 `["✍️"]`。Telegram 仅支持固定反应集合,参考:[reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9)
|
||||
|
||||
##### discord
|
||||
|
||||
- `enable`: 是否启用 Discord 消息预回复表情。默认为 `false`。
|
||||
- `emojis`: 预回复的表情列表。默认为 `["🤔"]`。Discord反应支持参考:[Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ)
|
||||
|
||||
### `wake_prefix`
|
||||
|
||||
唤醒前缀。默认为 `/`。当消息以 `/` 开头时,AstrBot 会被唤醒。
|
||||
|
||||
> [!TIP]
|
||||
> 如果唤醒的会话不在 ID 白名单中,AstrBot 将不会响应。
|
||||
|
||||
### `log_level`
|
||||
|
||||
日志级别。默认为 `INFO`。可以设置为 `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`。
|
||||
|
||||
### `trace_enable`
|
||||
|
||||
是否启用追踪记录。默认为 `false`。启用后,AstrBot 会记录运行追踪信息,可以在管理面板的 Trace 页面查看。
|
||||
|
||||
### `pip_install_arg`
|
||||
|
||||
`pip install` 的参数。如 `-i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`。
|
||||
|
||||
### `pypi_index_url`
|
||||
|
||||
PyPI 镜像源地址。默认为 `https://mirrors.aliyun.com/pypi/simple/`。
|
||||
|
||||
### `persona`
|
||||
|
||||
*此配置项已经在 v4.0.0 版本之后被废弃。请使用 WebUI 来配置人格。*
|
||||
|
||||
已配置的人格列表。每个人格包含 `id`, `name`, `description`, `system_prompt` 四个字段。
|
||||
|
||||
### `timezone`
|
||||
|
||||
时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: [IANA Time Zone Database](https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab)。
|
||||
|
||||
### `callback_api_base`
|
||||
|
||||
AstrBot API 的基础地址。用于文件服务和插件回调等功能。如 `http://example.com:6185`。默认为空,表示不启用文件服务和插件回调功能。
|
||||
|
||||
### `default_kb_collection`
|
||||
|
||||
默认知识库名称。用于 RAG 功能。如果为空,则不使用知识库。
|
||||
|
||||
### `plugin_set`
|
||||
|
||||
已启用的插件列表。`*` 表示启用所有可用的插件。默认为 `["*"]`。
|
||||
@@ -1,150 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# AstrBot HTTP API
|
||||
|
||||
从 v4.18.0 开始,AstrBot 提供基于 API Key 的 HTTP API,开发者可以通过标准 HTTP 请求访问核心能力。
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 在 WebUI - 设置中创建 API Key。
|
||||
2. 在请求头中携带 API Key:
|
||||
|
||||
```http
|
||||
Authorization: Bearer abk_xxx
|
||||
```
|
||||
|
||||
也支持:
|
||||
|
||||
```http
|
||||
X-API-Key: abk_xxx
|
||||
```
|
||||
|
||||
3. 对于对话接口,`username` 为必填参数:
|
||||
|
||||
- `POST /api/v1/chat`:请求体必须包含 `username`
|
||||
- `GET /api/v1/chat/sessions`:查询参数必须包含 `username`
|
||||
|
||||
## Scope 权限说明
|
||||
|
||||
创建 API Key 时可配置 `scopes`。每个 scope 控制可访问的接口范围:
|
||||
|
||||
| Scope | 作用 | 可访问接口 |
|
||||
| --- | --- | --- |
|
||||
| `chat` | 调用对话能力、查询对话会话 | `POST /api/v1/chat`、`GET /api/v1/chat/sessions` |
|
||||
| `config` | 获取可用配置文件列表 | `GET /api/v1/configs` |
|
||||
| `file` | 上传附件文件,获取 `attachment_id` | `POST /api/v1/file` |
|
||||
| `im` | 主动发 IM 消息、查询 bot/platform 列表 | `POST /api/v1/im/message`、`GET /api/v1/im/bots` |
|
||||
|
||||
如果 API Key 未包含目标接口所需 scope,请求会返回 `403 Insufficient API key scope`。
|
||||
|
||||
## 常用接口
|
||||
|
||||
**对话类**
|
||||
|
||||
调用 AstrBot 内建的 Agent 进行对话交互。支持插件调用、工具调用等能力,与 IM 端对话能力一致。
|
||||
|
||||
- `POST /api/v1/chat`:发送对话消息(SSE 流式返回,不传 `session_id` 会自动创建 UUID)
|
||||
- `GET /api/v1/chat/sessions`:分页获取指定 `username` 的会话
|
||||
- `GET /api/v1/configs`:获取可用配置文件列表
|
||||
|
||||
**文件上传**
|
||||
|
||||
- `POST /api/v1/file`:上传附件
|
||||
|
||||
**IM 消息发送**
|
||||
|
||||
- `POST /api/v1/im/message`:按 UMO 主动发消息
|
||||
- `GET /api/v1/im/bots`:获取 bot/platform ID 列表
|
||||
|
||||
## `message` 字段格式(重点)
|
||||
|
||||
`POST /api/v1/chat` 和 `POST /api/v1/im/message` 的 `message` 字段支持两种格式:
|
||||
|
||||
1. 字符串:纯文本消息
|
||||
2. 数组:消息段(message chain)
|
||||
|
||||
### 1. 纯文本格式
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 消息段数组格式
|
||||
|
||||
```json
|
||||
{
|
||||
"message": [
|
||||
{ "type": "plain", "text": "请看这个文件" },
|
||||
{ "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
支持的 `type`:
|
||||
|
||||
| type | 必填字段 | 可选字段 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `plain` | `text` | - | 文本段 |
|
||||
| `reply` | `message_id` | `selected_text` | 引用回复某条消息 |
|
||||
| `image` | `attachment_id` | - | 图片附件段 |
|
||||
| `record` | `attachment_id` | - | 音频附件段 |
|
||||
| `file` | `attachment_id` | - | 通用文件段 |
|
||||
| `video` | `attachment_id` | - | 视频附件段 |
|
||||
|
||||
* reply 消息段目前仅适配 `/api/v1/chat`,不适用于 `POST /api/v1/im/message`。
|
||||
|
||||
|
||||
说明:
|
||||
|
||||
- `attachment_id` 来自 `POST /api/v1/file` 上传结果。
|
||||
- `reply` 不能单独作为唯一内容,至少需要一个有实际内容的段(如 `plain/image/file/...`)。
|
||||
- 仅 `reply` 或空内容会返回错误。
|
||||
|
||||
### Chat API 的 `message` 用法
|
||||
|
||||
`POST /api/v1/chat` 额外需要 `username`,可选 `session_id`(不传会自动创建 UUID)。
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "alice",
|
||||
"session_id": "my_session_001",
|
||||
"message": [
|
||||
{ "type": "plain", "text": "帮我总结这个 PDF" },
|
||||
{ "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" }
|
||||
],
|
||||
"enable_streaming": true
|
||||
}
|
||||
```
|
||||
|
||||
### IM Message API 的 `message` 用法
|
||||
|
||||
`POST /api/v1/im/message` 需要 `umo` + `message`。
|
||||
|
||||
```json
|
||||
{
|
||||
"umo": "webchat:FriendMessage:openapi_probe",
|
||||
"message": [
|
||||
{ "type": "plain", "text": "这是主动消息" },
|
||||
{ "type": "image", "attachment_id": "9a2f8c72-e7af-4c0e-b352-222222222222" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
curl -N 'http://localhost:6185/api/v1/chat' \
|
||||
-H 'Authorization: Bearer abk_xxx' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"message":"Hello","username":"alice"}'
|
||||
```
|
||||
|
||||
## 完整 API 文档
|
||||
|
||||
交互式 API 文档请查看:
|
||||
|
||||
- https://docs.astrbot.app/scalar.html
|
||||
@@ -1,185 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 开发一个平台适配器
|
||||
|
||||
AstrBot 支持以插件的形式接入平台适配器,你可以自行接入 AstrBot 没有的平台。如飞书、钉钉甚至是哔哩哔哩私信、Minecraft。
|
||||
|
||||
我们以一个平台 `FakePlatform` 为例展开讲解。
|
||||
|
||||
首先,在插件目录下新增 `fake_platform_adapter.py` 和 `fake_platform_event.py` 文件。前者主要是平台适配器的实现,后者是平台事件的定义。
|
||||
|
||||
## 平台适配器
|
||||
|
||||
假设 FakePlatform 的客户端 SDK 是这样:
|
||||
|
||||
```py
|
||||
import asyncio
|
||||
|
||||
class FakeClient():
|
||||
'''模拟一个消息平台,这里 5 秒钟下发一个消息'''
|
||||
def __init__(self, token: str, username: str):
|
||||
self.token = token
|
||||
self.username = username
|
||||
# ...
|
||||
|
||||
async def start_polling(self):
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
await getattr(self, 'on_message_received')({
|
||||
'bot_id': '123',
|
||||
'content': '新消息',
|
||||
'username': 'zhangsan',
|
||||
'userid': '123',
|
||||
'message_id': 'asdhoashd',
|
||||
'group_id': 'group123',
|
||||
})
|
||||
|
||||
async def send_text(self, to: str, message: str):
|
||||
print('发了消息:', to, message)
|
||||
|
||||
async def send_image(self, to: str, image_path: str):
|
||||
print('发了消息:', to, image_path)
|
||||
```
|
||||
|
||||
我们创建 `fake_platform_adapter.py`:
|
||||
|
||||
```py
|
||||
import asyncio
|
||||
|
||||
from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, PlatformMetadata, MessageType
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.api.message_components import Plain, Image, Record # 消息链中的组件,可以根据需要导入
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.api.platform import register_platform_adapter
|
||||
from astrbot import logger
|
||||
from .client import FakeClient
|
||||
from .fake_platform_event import FakePlatformEvent
|
||||
|
||||
# 注册平台适配器。第一个参数为平台名,第二个为描述。第三个为默认配置。
|
||||
@register_platform_adapter("fake", "fake 适配器", default_config_tmpl={
|
||||
"token": "your_token",
|
||||
"username": "bot_username"
|
||||
})
|
||||
class FakePlatformAdapter(Platform):
|
||||
|
||||
def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config # 上面的默认配置,用户填写后会传到这里
|
||||
self.settings = platform_settings # platform_settings 平台设置。
|
||||
|
||||
async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
|
||||
# 必须实现
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
def meta(self) -> PlatformMetadata:
|
||||
# 必须实现,直接像下面一样返回即可。
|
||||
return PlatformMetadata(
|
||||
"fake",
|
||||
"fake 适配器",
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
# 必须实现,这里是主要逻辑。
|
||||
|
||||
# FakeClient 是我们自己定义的,这里只是示例。这个是其回调函数
|
||||
async def on_received(data):
|
||||
logger.info(data)
|
||||
abm = await self.convert_message(data=data) # 转换成 AstrBotMessage
|
||||
await self.handle_msg(abm)
|
||||
|
||||
# 初始化 FakeClient
|
||||
self.client = FakeClient(self.config['token'], self.config['username'])
|
||||
self.client.on_message_received = on_received
|
||||
await self.client.start_polling() # 持续监听消息,这是个堵塞方法。
|
||||
|
||||
async def convert_message(self, data: dict) -> AstrBotMessage:
|
||||
# 将平台消息转换成 AstrBotMessage
|
||||
# 这里就体现了适配程度,不同平台的消息结构不一样,这里需要根据实际情况进行转换。
|
||||
abm = AstrBotMessage()
|
||||
abm.type = MessageType.GROUP_MESSAGE # 还有 friend_message,对应私聊。具体平台具体分析。重要!
|
||||
abm.group_id = data['group_id'] # 如果是私聊,这里可以不填
|
||||
abm.message_str = data['content'] # 纯文本消息。重要!
|
||||
abm.sender = MessageMember(user_id=data['userid'], nickname=data['username']) # 发送者。重要!
|
||||
abm.message = [Plain(text=data['content'])] # 消息链。如果有其他类型的消息,直接 append 即可。重要!
|
||||
abm.raw_message = data # 原始消息。
|
||||
abm.self_id = data['bot_id']
|
||||
abm.session_id = data['userid'] # 会话 ID。重要!
|
||||
abm.message_id = data['message_id'] # 消息 ID。
|
||||
|
||||
return abm
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
# 处理消息
|
||||
message_event = FakePlatformEvent(
|
||||
message_str=message.message_str,
|
||||
message_obj=message,
|
||||
platform_meta=self.meta(),
|
||||
session_id=message.session_id,
|
||||
client=self.client
|
||||
)
|
||||
self.commit_event(message_event) # 提交事件到事件队列。不要忘记!
|
||||
```
|
||||
|
||||
|
||||
`fake_platform_event.py`:
|
||||
|
||||
```py
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from .client import FakeClient
|
||||
from astrbot.core.utils.io import download_image_by_url
|
||||
|
||||
class FakePlatformEvent(AstrMessageEvent):
|
||||
def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: FakeClient):
|
||||
super().__init__(message_str, message_obj, platform_meta, session_id)
|
||||
self.client = client
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
for i in message.chain: # 遍历消息链
|
||||
if isinstance(i, Plain): # 如果是文字类型的
|
||||
await self.client.send_text(to=self.get_sender_id(), message=i.text)
|
||||
elif isinstance(i, Image): # 如果是图片类型的
|
||||
img_url = i.file
|
||||
img_path = ""
|
||||
# 下面的三个条件可以直接参考一下。
|
||||
if img_url.startswith("file:///"):
|
||||
img_path = img_url[8:]
|
||||
elif i.file and i.file.startswith("http"):
|
||||
img_path = await download_image_by_url(i.file)
|
||||
else:
|
||||
img_path = img_url
|
||||
|
||||
# 请善于 Debug!
|
||||
|
||||
await self.client.send_image(to=self.get_sender_id(), image_path=img_path)
|
||||
|
||||
await super().send(message) # 需要最后加上这一段,执行父类的 send 方法。
|
||||
```
|
||||
|
||||
最后,main.py 只需这样,在初始化的时候导入 fake_platform_adapter 模块。装饰器会自动注册。
|
||||
|
||||
```py
|
||||
from astrbot.api.star import Context, Star
|
||||
|
||||
class MyPlugin(Star):
|
||||
def __init__(self, context: Context):
|
||||
from .fake_platform_adapter import FakePlatformAdapter # noqa
|
||||
```
|
||||
|
||||
搞好后,运行 AstrBot:
|
||||
|
||||

|
||||
|
||||
这里出现了我们创建的 fake。
|
||||
|
||||

|
||||
|
||||
启动后,可以看到正常工作:
|
||||
|
||||

|
||||
|
||||
|
||||
有任何疑问欢迎加群询问~
|
||||
@@ -1 +0,0 @@
|
||||
本页面已经迁移至 [插件基础开发](/dev/star/plugin)。
|
||||
@@ -1,553 +0,0 @@
|
||||
|
||||
# AI
|
||||
|
||||
AstrBot 内置了对多种大语言模型(LLM)提供商的支持,并且提供了统一的接口,方便插件开发者调用各种 LLM 服务。
|
||||
|
||||
您可以使用 AstrBot 提供的 LLM / Agent 接口来实现自己的智能体。
|
||||
|
||||
我们在 `v4.5.7` 版本之后对 LLM 提供商的调用方式进行了较大调整,推荐使用新的调用方式。新的调用方式更加简洁,并且支持更多的功能。当然,您仍然可以使用[旧的调用方式](/dev/star/plugin#ai)。
|
||||
|
||||
## 获取当前会话使用的聊天模型 ID
|
||||
|
||||
> [!TIP]
|
||||
> 在 v4.5.7 时加入
|
||||
|
||||
```py
|
||||
umo = event.unified_msg_origin
|
||||
provider_id = await self.context.get_current_chat_provider_id(umo=umo)
|
||||
```
|
||||
|
||||
## 调用大模型
|
||||
|
||||
> [!TIP]
|
||||
> 在 v4.5.7 时加入
|
||||
|
||||
```py
|
||||
llm_resp = await self.context.llm_generate(
|
||||
chat_provider_id=provider_id, # 聊天模型 ID
|
||||
prompt="Hello, world!",
|
||||
)
|
||||
# print(llm_resp.completion_text) # 获取返回的文本
|
||||
```
|
||||
|
||||
## 定义 Tool
|
||||
|
||||
Tool 是大语言模型调用外部工具的能力。
|
||||
|
||||
```py
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class BilibiliTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "bilibili_videos" # 工具名称
|
||||
description: str = "A tool to fetch Bilibili videos." # 工具描述
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keywords": {
|
||||
"type": "string",
|
||||
"description": "Keywords to search for Bilibili videos.",
|
||||
},
|
||||
},
|
||||
"required": ["keywords"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> ToolExecResult:
|
||||
return "1. 视频标题:如何使用AstrBot\n视频链接:xxxxxx"
|
||||
```
|
||||
|
||||
## 注册 Tool 到 AstrBot
|
||||
|
||||
在上面定义好 Tool 之后,如果你需要实现的功能是让用户在使用 AstrBot 进行对话时自动调用该 Tool,那么你需要在插件的 __init__ 方法中将 Tool 注册到 AstrBot 中:
|
||||
|
||||
```py
|
||||
class MyPlugin(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
# >= v4.5.1 使用:
|
||||
self.context.add_llm_tools(BilibiliTool(), SecondTool(), ...)
|
||||
|
||||
# < v4.5.1 之前使用:
|
||||
tool_mgr = self.context.provider_manager.llm_tools
|
||||
tool_mgr.func_list.append(BilibiliTool())
|
||||
```
|
||||
|
||||
### 通过装饰器定义 Tool 和注册 Tool
|
||||
|
||||
除了上述的通过 `@dataclass` 定义 Tool 的方式之外,你也可以使用装饰器的方式注册 tool 到 AstrBot。如果请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会解析该函数注释,请务必将注释格式写对)
|
||||
|
||||
```py{3,4,5,6,7}
|
||||
@filter.llm_tool(name="get_weather") # 如果 name 不填,将使用函数名
|
||||
async def get_weather(self, event: AstrMessageEvent, location: str) -> MessageEventResult:
|
||||
'''获取天气信息。
|
||||
|
||||
Args:
|
||||
location(string): 地点
|
||||
'''
|
||||
resp = self.get_weather_from_api(location)
|
||||
yield event.plain_result("天气信息: " + resp)
|
||||
```
|
||||
|
||||
在 `location(string): 地点` 中,`location` 是参数名,`string` 是参数类型,`地点` 是参数描述。
|
||||
|
||||
支持的参数类型有 `string`, `number`, `object`, `boolean`, `array`。在 v4.5.7 之后,支持对 `array` 类型参数指定子类型,例如 `array[string]`。
|
||||
|
||||
## 调用 Agent
|
||||
|
||||
> [!TIP]
|
||||
> 在 v4.5.7 时加入
|
||||
|
||||
Agent 可以被定义为 system_prompt + tools + llm 的结合体,可以实现更复杂的智能体行为。
|
||||
|
||||
在上面定义好 Tool 之后,可以通过以下方式调用 Agent:
|
||||
|
||||
```py
|
||||
llm_resp = await self.context.tool_loop_agent(
|
||||
event=event,
|
||||
chat_provider_id=prov_id,
|
||||
prompt="搜索一下 bilibili 上关于 AstrBot 的相关视频。",
|
||||
tools=ToolSet([BilibiliTool()]),
|
||||
max_steps=30, # Agent 最大执行步骤
|
||||
tool_call_timeout=60, # 工具调用超时时间
|
||||
)
|
||||
# print(llm_resp.completion_text) # 获取返回的文本
|
||||
```
|
||||
|
||||
`tool_loop_agent()` 方法会自动处理工具调用和大模型请求的循环,直到大模型不再调用工具或者达到最大步骤数为止。
|
||||
|
||||
## Multi-Agent
|
||||
|
||||
> [!TIP]
|
||||
> 在 v4.5.7 时加入
|
||||
|
||||
Multi-Agent(多智能体)系统将复杂应用分解为多个专业化智能体,它们协同解决问题。不同于依赖单个智能体处理每一步,多智能体架构允许将更小、更专注的智能体组合成协调的工作流程。我们使用 `agent-as-tool` 模式来实现多智能体系统。
|
||||
|
||||
在下面的例子中,我们定义了一个主智能体(Main Agent),它负责根据用户查询将任务分配给不同的子智能体(Sub-Agents)。每个子智能体专注于特定任务,例如获取天气信息。
|
||||
|
||||

|
||||
|
||||
定义 Tools:
|
||||
|
||||
```py
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
|
||||
@dataclass
|
||||
class AssignAgentTool(FunctionTool[AstrAgentContext]):
|
||||
"""Main agent uses this tool to decide which sub-agent to delegate a task to."""
|
||||
|
||||
name: str = "assign_agent"
|
||||
description: str = "Assign an agent to a task based on the given query"
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The query to call the sub-agent with.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> ToolExecResult:
|
||||
# Here you would implement the actual agent assignment logic.
|
||||
# For demonstration purposes, we'll return a dummy response.
|
||||
return "Based on the query, you should assign agent 1."
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherTool(FunctionTool[AstrAgentContext]):
|
||||
"""In this example, sub agent 1 uses this tool to get weather information."""
|
||||
|
||||
name: str = "weather"
|
||||
description: str = "Get weather information for a location"
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "The city to get weather information for.",
|
||||
},
|
||||
},
|
||||
"required": ["city"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> ToolExecResult:
|
||||
city = kwargs["city"]
|
||||
# Here you would implement the actual weather fetching logic.
|
||||
# For demonstration purposes, we'll return a dummy response.
|
||||
return f"The current weather in {city} is sunny with a temperature of 25°C."
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubAgent1(FunctionTool[AstrAgentContext]):
|
||||
"""Define a sub-agent as a function tool."""
|
||||
|
||||
name: str = "subagent1_name"
|
||||
description: str = "subagent1_description"
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The query to call the sub-agent with.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> ToolExecResult:
|
||||
ctx = context.context.context
|
||||
event = context.context.event
|
||||
logger.info(f"the llm context messages: {context.messages}")
|
||||
llm_resp = await ctx.tool_loop_agent(
|
||||
event=event,
|
||||
chat_provider_id=await ctx.get_current_chat_provider_id(
|
||||
event.unified_msg_origin
|
||||
),
|
||||
prompt=kwargs["query"],
|
||||
tools=ToolSet([WeatherTool()]),
|
||||
max_steps=30,
|
||||
)
|
||||
return llm_resp.completion_text
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubAgent2(FunctionTool[AstrAgentContext]):
|
||||
"""Define a sub-agent as a function tool."""
|
||||
|
||||
name: str = "subagent2_name"
|
||||
description: str = "subagent2_description"
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The query to call the sub-agent with.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> ToolExecResult:
|
||||
return "I am useless :(, you shouldn't call me :("
|
||||
```
|
||||
|
||||
然后,同样地,通过 `tool_loop_agent()` 方法调用 Agent:
|
||||
|
||||
```py
|
||||
@filter.command("test")
|
||||
async def test(self, event: AstrMessageEvent):
|
||||
umo = event.unified_msg_origin
|
||||
prov_id = await self.context.get_current_chat_provider_id(umo)
|
||||
llm_resp = await self.context.tool_loop_agent(
|
||||
event=event,
|
||||
chat_provider_id=prov_id,
|
||||
prompt="Test calling sub-agent for Beijing's weather information.",
|
||||
system_prompt=(
|
||||
"You are the main agent. Your task is to delegate tasks to sub-agents based on user queries."
|
||||
"Before delegating, use the 'assign_agent' tool to determine which sub-agent is best suited for the task."
|
||||
),
|
||||
tools=ToolSet([SubAgent1(), SubAgent2(), AssignAgentTool()]),
|
||||
max_steps=30,
|
||||
)
|
||||
yield event.plain_result(llm_resp.completion_text)
|
||||
```
|
||||
|
||||
## 对话管理器
|
||||
|
||||
### 获取会话当前的 LLM 对话历史 `get_conversation`
|
||||
|
||||
```py
|
||||
from astrbot.core.conversation_mgr import Conversation
|
||||
|
||||
uid = event.unified_msg_origin
|
||||
conv_mgr = self.context.conversation_manager
|
||||
curr_cid = await conv_mgr.get_curr_conversation_id(uid)
|
||||
conversation = await conv_mgr.get_conversation(uid, curr_cid) # Conversation
|
||||
```
|
||||
|
||||
::: details Conversation 类型定义
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class Conversation:
|
||||
"""The conversation entity representing a chat session."""
|
||||
|
||||
platform_id: str
|
||||
"""The platform ID in AstrBot"""
|
||||
user_id: str
|
||||
"""The user ID associated with the conversation."""
|
||||
cid: str
|
||||
"""The conversation ID, in UUID format."""
|
||||
history: str = ""
|
||||
"""The conversation history as a string."""
|
||||
title: str | None = ""
|
||||
"""The title of the conversation. For now, it's only used in WebChat."""
|
||||
persona_id: str | None = ""
|
||||
"""The persona ID associated with the conversation."""
|
||||
created_at: int = 0
|
||||
"""The timestamp when the conversation was created."""
|
||||
updated_at: int = 0
|
||||
"""The timestamp when the conversation was last updated."""
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 快速添加 LLM 记录到对话 `add_message_pair`
|
||||
|
||||
```py
|
||||
from astrbot.core.agent.message import (
|
||||
AssistantMessageSegment,
|
||||
UserMessageSegment,
|
||||
TextPart,
|
||||
)
|
||||
|
||||
curr_cid = await conv_mgr.get_curr_conversation_id(event.unified_msg_origin)
|
||||
user_msg = UserMessageSegment(content=[TextPart(text="hi")])
|
||||
llm_resp = await self.context.llm_generate(
|
||||
chat_provider_id=provider_id, # 聊天模型 ID
|
||||
contexts=[user_msg], # 当未指定 prompt 时,使用 contexts 作为输入;同时指定 prompt 和 contexts 时,prompt 会被添加到 LLM 输入的最后
|
||||
)
|
||||
await conv_mgr.add_message_pair(
|
||||
cid=curr_cid,
|
||||
user_message=user_msg,
|
||||
assistant_message=AssistantMessageSegment(
|
||||
content=[TextPart(text=llm_resp.completion_text)]
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### 主要方法
|
||||
|
||||
#### `new_conversation`
|
||||
|
||||
- __Usage__
|
||||
在当前会话中新建一条对话,并自动切换为该对话。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str` – 形如 `platform_name:message_type:session_id`
|
||||
- `platform_id: str | None` – 平台标识,默认从 `unified_msg_origin` 解析
|
||||
- `content: list[dict] | None` – 初始历史消息
|
||||
- `title: str | None` – 对话标题
|
||||
- `persona_id: str | None` – 绑定的 persona ID
|
||||
- __Returns__
|
||||
`str` – 新生成的 UUID 对话 ID
|
||||
|
||||
#### `switch_conversation`
|
||||
|
||||
- __Usage__
|
||||
将会话切换到指定的对话。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str`
|
||||
- `conversation_id: str`
|
||||
- __Returns__
|
||||
`None`
|
||||
|
||||
#### `delete_conversation`
|
||||
|
||||
- __Usage__
|
||||
删除会话中的某条对话;若 `conversation_id` 为 `None`,则删除当前对话。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str`
|
||||
- `conversation_id: str | None`
|
||||
- __Returns__
|
||||
`None`
|
||||
|
||||
#### `get_curr_conversation_id`
|
||||
|
||||
- __Usage__
|
||||
获取当前会话正在使用的对话 ID。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str`
|
||||
- __Returns__
|
||||
`str | None` – 当前对话 ID,不存在时返回 `None`
|
||||
|
||||
#### `get_conversation`
|
||||
|
||||
- __Usage__
|
||||
获取指定对话的完整对象;若不存在且 `create_if_not_exists=True` 则自动创建。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str`
|
||||
- `conversation_id: str`
|
||||
- `create_if_not_exists: bool = False`
|
||||
- __Returns__
|
||||
`Conversation | None`
|
||||
|
||||
#### `get_conversations`
|
||||
|
||||
- __Usage__
|
||||
拉取用户或平台下的全部对话列表。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str | None` – 为 `None` 时不过滤用户
|
||||
- `platform_id: str | None`
|
||||
- __Returns__
|
||||
`List[Conversation]`
|
||||
|
||||
#### `update_conversation`
|
||||
|
||||
- __Usage__
|
||||
更新对话的标题、历史记录或 persona_id。
|
||||
- __Arguments__
|
||||
- `unified_msg_origin: str`
|
||||
- `conversation_id: str | None` – 为 `None` 时使用当前对话
|
||||
- `history: list[dict] | None`
|
||||
- `title: str | None`
|
||||
- `persona_id: str | None`
|
||||
- __Returns__
|
||||
`None`
|
||||
|
||||
## 人格设定管理器
|
||||
|
||||
`PersonaManager` 负责统一加载、缓存并提供所有人格(Persona)的增删改查接口,同时兼容 AstrBot 4.x 之前的旧版人格格式(v3)。
|
||||
初始化时会自动从数据库读取全部人格,并生成一份 v3 兼容数据,供旧代码无缝使用。
|
||||
|
||||
```py
|
||||
persona_mgr = self.context.persona_manager
|
||||
```
|
||||
|
||||
### 主要方法
|
||||
|
||||
#### `get_persona`
|
||||
|
||||
- __Usage__
|
||||
获取根据人格 ID 获取人格数据。
|
||||
- __Arguments__
|
||||
- `persona_id: str` – 人格 ID
|
||||
- __Returns__
|
||||
`Persona` – 人格数据,若不存在则返回 None
|
||||
- __Raises__
|
||||
`ValueError` – 当不存在时抛出
|
||||
|
||||
#### `get_all_personas`
|
||||
|
||||
- __Usage__
|
||||
一次性获取数据库中所有人格。
|
||||
- __Returns__
|
||||
`list[Persona]` – 人格列表,可能为空
|
||||
|
||||
#### `create_persona`
|
||||
|
||||
- __Usage__
|
||||
新建人格并立即写入数据库,成功后自动刷新本地缓存。
|
||||
- __Arguments__
|
||||
- `persona_id: str` – 新人格 ID(唯一)
|
||||
- `system_prompt: str` – 系统提示词
|
||||
- `begin_dialogs: list[str]` – 可选,开场对话(偶数条,user/assistant 交替)
|
||||
- `tools: list[str]` – 可选,允许使用的工具列表;`None`=全部工具,`[]`=禁用全部
|
||||
- __Returns__
|
||||
`Persona` – 新建后的人格对象
|
||||
- __Raises__
|
||||
`ValueError` – 若 `persona_id` 已存在
|
||||
|
||||
#### `update_persona`
|
||||
|
||||
- __Usage__
|
||||
更新现有人格的任意字段,并同步到数据库与缓存。
|
||||
- __Arguments__
|
||||
- `persona_id: str` – 待更新的人格 ID
|
||||
- `system_prompt: str` – 可选,新的系统提示词
|
||||
- `begin_dialogs: list[str]` – 可选,新的开场对话
|
||||
- `tools: list[str]` – 可选,新的工具列表;语义同 `create_persona`
|
||||
- __Returns__
|
||||
`Persona` – 更新后的人格对象
|
||||
- __Raises__
|
||||
`ValueError` – 若 `persona_id` 不存在
|
||||
|
||||
#### `delete_persona`
|
||||
|
||||
- __Usage__
|
||||
删除指定人格,同时清理数据库与缓存。
|
||||
- __Arguments__
|
||||
- `persona_id: str` – 待删除的人格 ID
|
||||
- __Raises__
|
||||
`Valueable` – 若 `persona_id` 不存在
|
||||
|
||||
#### `get_default_persona_v3`
|
||||
|
||||
- __Usage__
|
||||
根据当前会话配置,获取应使用的默认人格(v3 格式)。
|
||||
若配置未指定或指定的人格不存在,则回退到 `DEFAULT_PERSONALITY`。
|
||||
- __Arguments__
|
||||
- `umo: str | MessageSession | None` – 会话标识,用于读取用户级配置
|
||||
- __Returns__
|
||||
`Personality` – v3 格式的默认人格对象
|
||||
|
||||
::: details Persona / Personality 类型定义
|
||||
|
||||
```py
|
||||
|
||||
class Persona(SQLModel, table=True):
|
||||
"""Persona is a set of instructions for LLMs to follow.
|
||||
|
||||
It can be used to customize the behavior of LLMs.
|
||||
"""
|
||||
|
||||
__tablename__ = "personas"
|
||||
|
||||
id: int = Field(primary_key=True, sa_column_kwargs={"autoincrement": True})
|
||||
persona_id: str = Field(max_length=255, nullable=False)
|
||||
system_prompt: str = Field(sa_type=Text, nullable=False)
|
||||
begin_dialogs: Optional[list] = Field(default=None, sa_type=JSON)
|
||||
"""a list of strings, each representing a dialog to start with"""
|
||||
tools: Optional[list] = Field(default=None, sa_type=JSON)
|
||||
"""None means use ALL tools for default, empty list means no tools, otherwise a list of tool names."""
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc),
|
||||
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"persona_id",
|
||||
name="uix_persona_id",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Personality(TypedDict):
|
||||
"""LLM 人格类。
|
||||
|
||||
在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。
|
||||
"""
|
||||
|
||||
prompt: str
|
||||
name: str
|
||||
begin_dialogs: list[str]
|
||||
mood_imitation_dialogs: list[str]
|
||||
"""情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。"""
|
||||
tools: list[str] | None
|
||||
"""工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
# 开发环境准备
|
||||
|
||||
## 获取插件模板
|
||||
|
||||
1. 打开 AstrBot 插件模板: [helloworld](https://github.com/Soulter/helloworld)
|
||||
2. 点击右上角的 `Use this template`
|
||||
3. 然后点击 `Create new repository`。
|
||||
4. 在 `Repository name` 处填写您的插件名。插件名格式:
|
||||
- 推荐以 `astrbot_plugin_` 开头;
|
||||
- 不能包含空格;
|
||||
- 保持全部字母小写;
|
||||
- 尽量简短。
|
||||
5. 点击右下角的 `Create repository`。
|
||||
|
||||

|
||||
|
||||
## Clone 插件和 AstrBot 项目
|
||||
|
||||
Clone AstrBot 项目本体和刚刚创建的插件仓库到本地。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
mkdir -p AstrBot/data/plugins
|
||||
cd AstrBot/data/plugins
|
||||
git clone 插件仓库地址
|
||||
```
|
||||
|
||||
然后,使用 `VSCode` 打开 `AstrBot` 项目。找到 `data/plugins/<你的插件名字>` 目录。
|
||||
|
||||
更新 `metadata.yaml` 文件,填写插件的元数据信息。
|
||||
|
||||
> [!NOTE]
|
||||
> AstrBot 插件市场的信息展示依赖于 `metadata.yaml` 文件。
|
||||
|
||||
## 调试插件
|
||||
|
||||
AstrBot 采用在运行时注入插件的机制。因此,在调试插件时,需要启动 AstrBot 本体。
|
||||
|
||||
您可以使用 AstrBot 的热重载功能简化开发流程。
|
||||
|
||||
插件的代码修改后,可以在 AstrBot WebUI 的插件管理处找到自己的插件,点击右上角 `...` 按钮,选择 `重载插件`。
|
||||
|
||||
## 插件依赖管理
|
||||
|
||||
目前 AstrBot 对插件的依赖管理使用 `pip` 自带的 `requirements.txt` 文件。如果你的插件需要依赖第三方库,请务必在插件目录下创建 `requirements.txt` 文件并写入所使用的依赖库,以防止用户在安装你的插件时出现依赖未找到(Module Not Found)的问题。
|
||||
|
||||
> `requirements.txt` 的完整格式可以参考 [pip 官方文档](https://pip.pypa.io/en/stable/reference/requirements-file-format/)。
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
# 文转图
|
||||
|
||||
> [!TIP]
|
||||
> 为了方便开发,您可以使用 [AstrBot Text2Image Playground](https://t2i-playground.astrbot.app/) 在线可视化编辑和测试 HTML 模板。
|
||||
|
||||
## 基本
|
||||
|
||||
AstrBot 支持将文字渲染成图片。
|
||||
|
||||
```python
|
||||
@filter.command("image") # 注册一个 /image 指令,接收 text 参数。
|
||||
async def on_aiocqhttp(self, event: AstrMessageEvent, text: str):
|
||||
url = await self.text_to_image(text) # text_to_image() 是 Star 类的一个方法。
|
||||
# path = await self.text_to_image(text, return_url = False) # 如果你想保存图片到本地
|
||||
yield event.image_result(url)
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 自定义(基于 HTML)
|
||||
|
||||
如果你觉得上面渲染出来的图片不够美观,你可以使用自定义的 HTML 模板来渲染图片。
|
||||
|
||||
AstrBot 支持使用 `HTML + Jinja2` 的方式来渲染文转图模板。
|
||||
|
||||
```py{7}
|
||||
# 自定义的 Jinja2 模板,支持 CSS
|
||||
TMPL = '''
|
||||
<div style="font-size: 32px;">
|
||||
<h1 style="color: black">Todo List</h1>
|
||||
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
'''
|
||||
|
||||
@filter.command("todo")
|
||||
async def custom_t2i_tmpl(self, event: AstrMessageEvent):
|
||||
options = {} # 可选择传入渲染选项。
|
||||
url = await self.html_render(TMPL, {"items": ["吃饭", "睡觉", "玩原神"]}, options=options) # 第二个参数是 Jinja2 的渲染数据
|
||||
yield event.image_result(url)
|
||||
```
|
||||
|
||||
返回的结果:
|
||||
|
||||

|
||||
|
||||
这只是一个简单的例子。得益于 HTML 和 DOM 渲染器的强大性,你可以进行更复杂和更美观的的设计。除此之外,Jinja2 支持循环、条件等语法以适应列表、字典等数据结构。你可以从网上了解更多关于 Jinja2 的知识。
|
||||
|
||||
**图片渲染选项(options)**:
|
||||
|
||||
请参考 Playwright 的 [screenshot](https://playwright.dev/python/docs/api/class-page#page-screenshot) API。
|
||||
|
||||
- `timeout` (float, optional): 截图超时时间.
|
||||
- `type` (Literal["jpeg", "png"], optional): 截图图片类型.
|
||||
- `quality` (int, optional): 截图质量,仅适用于 JPEG 格式图片.
|
||||
- `omit_background` (bool, optional): 是否允许隐藏默认的白色背景,这样就可以截透明图了,仅适用于 PNG 格式
|
||||
- `full_page` (bool, optional): 是否截整个页面而不是仅设置的视口大小,默认为 True.
|
||||
- `clip` (dict, optional): 截图后裁切的区域。参考 Playwright screenshot API。
|
||||
- `animations`: (Literal["allow", "disabled"], optional): 是否允许播放 CSS 动画.
|
||||
- `caret`: (Literal["hide", "initial"], optional): 当设置为 hide 时,截图时将隐藏文本插入符号,默认为 hide.
|
||||
- `scale`: (Literal["css", "device"], optional): 页面缩放设置. 当设置为 css 时,则将设备分辨率与 CSS 中的像素一一对应,在高分屏上会使得截图变小. 当设置为 device 时,则根据设备的屏幕缩放设置或当前 Playwright 的 Page/Context 中的 device_scale_factor 参数来缩放.
|
||||
@@ -1,364 +0,0 @@
|
||||
|
||||
# 处理消息事件
|
||||
|
||||
事件监听器可以收到平台下发的消息内容,可以实现指令、指令组、事件监听等功能。
|
||||
|
||||
事件监听器的注册器在 `astrbot.api.event.filter` 下,需要先导入。请务必导入,否则会和 python 的高阶函数 filter 冲突。
|
||||
|
||||
```py
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
```
|
||||
|
||||
## 消息与事件
|
||||
|
||||
AstrBot 接收消息平台下发的消息,并将其封装为 `AstrMessageEvent` 对象,传递给插件进行处理。
|
||||
|
||||

|
||||
|
||||
### 消息事件
|
||||
|
||||
`AstrMessageEvent` 是 AstrBot 的消息事件对象,其中存储了消息发送者、消息内容等信息。
|
||||
|
||||
### 消息对象
|
||||
|
||||
`AstrBotMessage` 是 AstrBot 的消息对象,其中存储了消息平台下发的消息具体内容,`AstrMessageEvent` 对象中包含一个 `message_obj` 属性用于获取该消息对象。
|
||||
|
||||
```py{11}
|
||||
class AstrBotMessage:
|
||||
'''AstrBot 的消息对象'''
|
||||
type: MessageType # 消息类型
|
||||
self_id: str # 机器人的识别id
|
||||
session_id: str # 会话id。取决于 unique_session 的设置。
|
||||
message_id: str # 消息id
|
||||
group_id: str = "" # 群组id,如果为私聊,则为空
|
||||
sender: MessageMember # 发送者
|
||||
message: List[BaseMessageComponent] # 消息链。比如 [Plain("Hello"), At(qq=123456)]
|
||||
message_str: str # 最直观的纯文本消息字符串,将消息链中的 Plain 消息(文本消息)连接起来
|
||||
raw_message: object
|
||||
timestamp: int # 消息时间戳
|
||||
```
|
||||
|
||||
其中,`raw_message` 是消息平台适配器的**原始消息对象**。
|
||||
|
||||
### 消息链
|
||||
|
||||

|
||||
|
||||
`消息链`描述一个消息的结构,是一个有序列表,列表中每一个元素称为`消息段`。
|
||||
|
||||
常见的消息段类型有:
|
||||
|
||||
- `Plain`:文本消息段
|
||||
- `At`:提及消息段
|
||||
- `Image`:图片消息段
|
||||
- `Record`:语音消息段
|
||||
- `Video`:视频消息段
|
||||
- `File`:文件消息段
|
||||
|
||||
大多数消息平台都支持上面的消息段类型。
|
||||
|
||||
此外,OneBot v11 平台(QQ 个人号等)还支持以下较为常见的消息段类型:
|
||||
|
||||
- `Face`:表情消息段
|
||||
- `Node`:合并转发消息中的一个节点
|
||||
- `Nodes`:合并转发消息中的多个节点
|
||||
- `Poke`:戳一戳消息段
|
||||
|
||||
在 AstrBot 中,消息链表示为 `List[BaseMessageComponent]` 类型的列表。
|
||||
|
||||
## 指令
|
||||
|
||||

|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
from astrbot.api.star import Context, Star
|
||||
|
||||
class MyPlugin(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
@filter.command("helloworld") # from astrbot.api.event.filter import command
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
'''这是 hello world 指令'''
|
||||
user_name = event.get_sender_name()
|
||||
message_str = event.message_str # 获取消息的纯文本内容
|
||||
yield event.plain_result(f"Hello, {user_name}!")
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 指令不能带空格,否则 AstrBot 会将其解析到第二个参数。可以使用下面的指令组功能,或者也使用监听器自己解析消息内容。
|
||||
|
||||
## 带参指令
|
||||
|
||||

|
||||
|
||||
AstrBot 会自动帮你解析指令的参数。
|
||||
|
||||
```python
|
||||
@filter.command("add")
|
||||
def add(self, event: AstrMessageEvent, a: int, b: int):
|
||||
# /add 1 2 -> 结果是: 3
|
||||
yield event.plain_result(f"Wow! The anwser is {a + b}!")
|
||||
```
|
||||
|
||||
## 指令组
|
||||
|
||||
指令组可以帮助你组织指令。
|
||||
|
||||
```python
|
||||
@filter.command_group("math")
|
||||
def math(self):
|
||||
pass
|
||||
|
||||
@math.command("add")
|
||||
async def add(self, event: AstrMessageEvent, a: int, b: int):
|
||||
# /math add 1 2 -> 结果是: 3
|
||||
yield event.plain_result(f"结果是: {a + b}")
|
||||
|
||||
@math.command("sub")
|
||||
async def sub(self, event: AstrMessageEvent, a: int, b: int):
|
||||
# /math sub 1 2 -> 结果是: -1
|
||||
yield event.plain_result(f"结果是: {a - b}")
|
||||
```
|
||||
|
||||
指令组函数内不需要实现任何函数,请直接 `pass` 或者添加函数内注释。指令组的子指令使用 `指令组名.command` 来注册。
|
||||
|
||||
当用户没有输入子指令时,会报错并,并渲染出该指令组的树形结构。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
理论上,指令组可以无限嵌套!
|
||||
|
||||
```py
|
||||
'''
|
||||
math
|
||||
├── calc
|
||||
│ ├── add (a(int),b(int),)
|
||||
│ ├── sub (a(int),b(int),)
|
||||
│ ├── help (无参数指令)
|
||||
'''
|
||||
|
||||
@filter.command_group("math")
|
||||
def math():
|
||||
pass
|
||||
|
||||
@math.group("calc") # 请注意,这里是 group,而不是 command_group
|
||||
def calc():
|
||||
pass
|
||||
|
||||
@calc.command("add")
|
||||
async def add(self, event: AstrMessageEvent, a: int, b: int):
|
||||
yield event.plain_result(f"结果是: {a + b}")
|
||||
|
||||
@calc.command("sub")
|
||||
async def sub(self, event: AstrMessageEvent, a: int, b: int):
|
||||
yield event.plain_result(f"结果是: {a - b}")
|
||||
|
||||
@calc.command("help")
|
||||
def calc_help(self, event: AstrMessageEvent):
|
||||
# /math calc help
|
||||
yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。")
|
||||
```
|
||||
|
||||
## 指令别名
|
||||
|
||||
> v3.4.28 后
|
||||
|
||||
可以为指令或指令组添加不同的别名:
|
||||
|
||||
```python
|
||||
@filter.command("help", alias={'帮助', 'helpme'})
|
||||
def help(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。")
|
||||
```
|
||||
|
||||
### 事件类型过滤
|
||||
|
||||
#### 接收所有
|
||||
|
||||
这将接收所有的事件。
|
||||
|
||||
```python
|
||||
@filter.event_message_type(filter.EventMessageType.ALL)
|
||||
async def on_all_message(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("收到了一条消息。")
|
||||
```
|
||||
|
||||
#### 群聊和私聊
|
||||
|
||||
```python
|
||||
@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE)
|
||||
async def on_private_message(self, event: AstrMessageEvent):
|
||||
message_str = event.message_str # 获取消息的纯文本内容
|
||||
yield event.plain_result("收到了一条私聊消息。")
|
||||
```
|
||||
|
||||
`EventMessageType` 是一个 `Enum` 类型,包含了所有的事件类型。当前的事件类型有 `PRIVATE_MESSAGE` 和 `GROUP_MESSAGE`。
|
||||
|
||||
#### 消息平台
|
||||
|
||||
```python
|
||||
@filter.platform_adapter_type(filter.PlatformAdapterType.AIOCQHTTP | filter.PlatformAdapterType.QQOFFICIAL)
|
||||
async def on_aiocqhttp(self, event: AstrMessageEvent):
|
||||
'''只接收 AIOCQHTTP 和 QQOFFICIAL 的消息'''
|
||||
yield event.plain_result("收到了一条信息")
|
||||
```
|
||||
|
||||
当前版本下,`PlatformAdapterType` 有 `AIOCQHTTP`, `QQOFFICIAL`, `GEWECHAT`, `ALL`。
|
||||
|
||||
#### 管理员指令
|
||||
|
||||
```python
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("test")
|
||||
async def test(self, event: AstrMessageEvent):
|
||||
pass
|
||||
```
|
||||
|
||||
仅管理员才能使用 `test` 指令。
|
||||
|
||||
### 多个过滤器
|
||||
|
||||
支持同时使用多个过滤器,只需要在函数上添加多个装饰器即可。过滤器使用 `AND` 逻辑。也就是说,只有所有的过滤器都通过了,才会执行函数。
|
||||
|
||||
```python
|
||||
@filter.command("helloworld")
|
||||
@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE)
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("你好!")
|
||||
```
|
||||
|
||||
### 事件钩子
|
||||
|
||||
> [!TIP]
|
||||
> 事件钩子不支持与上面的 @filter.command, @filter.command_group, @filter.event_message_type, @filter.platform_adapter_type, @filter.permission_type 一起使用。
|
||||
|
||||
#### Bot 初始化完成时
|
||||
|
||||
> v3.4.34 后
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.on_astrbot_loaded()
|
||||
async def on_astrbot_loaded(self):
|
||||
print("AstrBot 初始化完成")
|
||||
|
||||
```
|
||||
|
||||
#### 等待 LLM 请求时
|
||||
|
||||
在 AstrBot 准备调用 LLM 但还未获取会话锁时,会触发 `on_waiting_llm_request` 钩子。
|
||||
|
||||
这个钩子适合用于发送"正在等待请求..."等用户反馈提示,亦或是在锁外及时获取LLM请求而不用等到锁被释放。
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.on_waiting_llm_request()
|
||||
async def on_waiting_llm(self, event: AstrMessageEvent):
|
||||
await event.send("🤔 正在等待请求...")
|
||||
```
|
||||
|
||||
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
|
||||
|
||||
#### LLM 请求时
|
||||
|
||||
在 AstrBot 默认的执行流程中,在调用 LLM 前,会触发 `on_llm_request` 钩子。
|
||||
|
||||
可以获取到 `ProviderRequest` 对象,可以对其进行修改。
|
||||
|
||||
ProviderRequest 对象包含了 LLM 请求的所有信息,包括请求的文本、系统提示等。
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
from astrbot.api.provider import ProviderRequest
|
||||
|
||||
@filter.on_llm_request()
|
||||
async def my_custom_hook_1(self, event: AstrMessageEvent, req: ProviderRequest): # 请注意有三个参数
|
||||
print(req) # 打印请求的文本
|
||||
req.system_prompt += "自定义 system_prompt"
|
||||
|
||||
```
|
||||
|
||||
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
|
||||
|
||||
#### LLM 请求完成时
|
||||
|
||||
在 LLM 请求完成后,会触发 `on_llm_response` 钩子。
|
||||
|
||||
可以获取到 `ProviderResponse` 对象,可以对其进行修改。
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
from astrbot.api.provider import LLMResponse
|
||||
|
||||
@filter.on_llm_response()
|
||||
async def on_llm_resp(self, event: AstrMessageEvent, resp: LLMResponse): # 请注意有三个参数
|
||||
print(resp)
|
||||
```
|
||||
|
||||
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
|
||||
|
||||
#### 发送消息前
|
||||
|
||||
在发送消息前,会触发 `on_decorating_result` 钩子。
|
||||
|
||||
可以在这里实现一些消息的装饰,比如转语音、转图片、加前缀等等
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.on_decorating_result()
|
||||
async def on_decorating_result(self, event: AstrMessageEvent):
|
||||
result = event.get_result()
|
||||
chain = result.chain
|
||||
print(chain) # 打印消息链
|
||||
chain.append(Plain("!")) # 在消息链的最后添加一个感叹号
|
||||
```
|
||||
|
||||
> 这里不能使用 yield 来发送消息。这个钩子只是用来装饰 event.get_result().chain 的。如需发送,请直接使用 `event.send()` 方法。
|
||||
|
||||
#### 发送消息后
|
||||
|
||||
在发送消息给消息平台后,会触发 `after_message_sent` 钩子。
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.after_message_sent()
|
||||
async def after_message_sent(self, event: AstrMessageEvent):
|
||||
pass
|
||||
```
|
||||
|
||||
> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。
|
||||
|
||||
### 优先级
|
||||
|
||||
指令、事件监听器、事件钩子可以设置优先级,先于其他指令、监听器、钩子执行。默认优先级是 `0`。
|
||||
|
||||
```python
|
||||
@filter.command("helloworld", priority=1)
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("Hello!")
|
||||
```
|
||||
|
||||
## 控制事件传播
|
||||
|
||||
```python{6}
|
||||
@filter.command("check_ok")
|
||||
async def check_ok(self, event: AstrMessageEvent):
|
||||
ok = self.check() # 自己的逻辑
|
||||
if not ok:
|
||||
yield event.plain_result("检查失败")
|
||||
event.stop_event() # 停止事件传播
|
||||
```
|
||||
|
||||
当事件停止传播,后续所有步骤将不会被执行。
|
||||
|
||||
假设有一个插件 A,A 终止事件传播之后所有后续操作都不会执行,比如执行其它插件的 handler、请求 LLM。
|
||||
@@ -1,52 +0,0 @@
|
||||
# 杂项
|
||||
|
||||
## 获取消息平台实例
|
||||
|
||||
> v3.4.34 后
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.command("test")
|
||||
async def test_(self, event: AstrMessageEvent):
|
||||
from astrbot.api.platform import AiocqhttpAdapter # 其他平台同理
|
||||
platform = self.context.get_platform(filter.PlatformAdapterType.AIOCQHTTP)
|
||||
assert isinstance(platform, AiocqhttpAdapter)
|
||||
# platform.get_client().api.call_action()
|
||||
```
|
||||
|
||||
## 调用 QQ 协议端 API
|
||||
|
||||
```py
|
||||
@filter.command("helloworld")
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
if event.get_platform_name() == "aiocqhttp":
|
||||
# qq
|
||||
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent
|
||||
assert isinstance(event, AiocqhttpMessageEvent)
|
||||
client = event.bot # 得到 client
|
||||
payloads = {
|
||||
"message_id": event.message_obj.message_id,
|
||||
}
|
||||
ret = await client.api.call_action('delete_msg', **payloads) # 调用 协议端 API
|
||||
logger.info(f"delete_msg: {ret}")
|
||||
```
|
||||
|
||||
关于 CQHTTP API,请参考如下文档:
|
||||
|
||||
Napcat API 文档:<https://napcat.apifox.cn/>
|
||||
|
||||
Lagrange API 文档:<https://lagrange-onebot.apifox.cn/>
|
||||
|
||||
## 获取载入的所有插件
|
||||
|
||||
```py
|
||||
plugins = self.context.get_all_stars() # 返回 StarMetadata 包含了插件类实例、配置等等
|
||||
```
|
||||
|
||||
## 获取加载的所有平台
|
||||
|
||||
```py
|
||||
from astrbot.api.platform import Platform
|
||||
platforms = self.context.platform_manager.get_insts() # List[Platform]
|
||||
```
|
||||
@@ -1,210 +0,0 @@
|
||||
|
||||
# 插件配置
|
||||
|
||||
随着插件功能的增加,可能需要定义一些配置以让用户自定义插件的行为。
|
||||
|
||||
AstrBot 提供了”强大“的配置解析和可视化功能。能够让用户在管理面板上直接配置插件,而不需要修改代码。
|
||||
|
||||
## 配置定义
|
||||
|
||||
要注册配置,首先需要在您的插件目录下添加一个 `_conf_schema.json` 的 json 文件。
|
||||
|
||||
文件内容是一个 `Schema`(模式),用于表示配置。Schema 是 json 格式的,例如上图的 Schema 是:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"description": "Bot Token",
|
||||
"type": "string",
|
||||
},
|
||||
"sub_config": {
|
||||
"description": "测试嵌套配置",
|
||||
"type": "object",
|
||||
"hint": "xxxx",
|
||||
"items": {
|
||||
"name": {
|
||||
"description": "testsub",
|
||||
"type": "string",
|
||||
"hint": "xxxx"
|
||||
},
|
||||
"id": {
|
||||
"description": "testsub",
|
||||
"type": "int",
|
||||
"hint": "xxxx"
|
||||
},
|
||||
"time": {
|
||||
"description": "testsub",
|
||||
"type": "int",
|
||||
"hint": "xxxx",
|
||||
"default": 123
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: **此项必填**。配置的类型。支持 `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `template_list`。当类型为 `text` 时,将会可视化为一个更大的可拖拽宽高的 textarea 组件,以适应大文本。
|
||||
- `description`: 可选。配置的描述。建议一句话描述配置的行为。
|
||||
- `hint`: 可选。配置的提示信息,表现在上图中右边的问号按钮,当鼠标悬浮在问号按钮上时显示。
|
||||
- `obvious_hint`: 可选。配置的 hint 是否醒目显示。如上图的 `token`。
|
||||
- `default`: 可选。配置的默认值。如果用户没有配置,将使用默认值。int 是 0,float 是 0.0,bool 是 False,string 是 "",object 是 {},list 是 []。
|
||||
- `items`: 可选。如果配置的类型是 `object`,需要添加 `items` 字段。`items` 的内容是这个配置项的子 Schema。理论上可以无限嵌套,但是不建议过多嵌套。
|
||||
- `invisible`: 可选。配置是否隐藏。默认是 `false`。如果设置为 `true`,则不会在管理面板上显示。
|
||||
- `options`: 可选。一个列表,如 `"options": ["chat", "agent", "workflow"]`。提供下拉列表可选项。
|
||||
- `editor_mode`: 可选。是否启用代码编辑器模式。需要 AstrBot >= `v3.5.10`, 低于这个版本不会报错,但不会生效。默认是 false。
|
||||
- `editor_language`: 可选。代码编辑器的代码语言,默认为 `json`。
|
||||
- `editor_theme`: 可选。代码编辑器的主题,可选值有 `vs-light`(默认), `vs-dark`。
|
||||
- `_special`: 可选。用于调用 AstrBot 提供的可视化提供商选取、人格选取、知识库选取等功能,详见下文。
|
||||
|
||||
其中,如果启用了代码编辑器,效果如下图所示:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**_special** 字段仅 v4.0.0 之后可用。目前支持填写 `select_provider`, `select_provider_tts`, `select_provider_stt`, `select_persona`,用于让用户快速选择用户在 WebUI 上已经配置好的模型提供商、人设等数据。结果均为字符串。以 select_provider 为例,将呈现以下效果:
|
||||
|
||||

|
||||
|
||||
### file 类型的 schema
|
||||
|
||||
在 v4.13.0 之后引入,允许插件定义文件上传配置项,引导用户上传插件所需的文件。
|
||||
|
||||
```json
|
||||
{
|
||||
"demo_files": {
|
||||
"type": "file",
|
||||
"description": "Uploaded files for demo",
|
||||
"default": [], // 支持多文件上传,默认值为一个空列表
|
||||
"file_types": ["pdf", "docx"] // 允许上传的文件类型列表
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### dict 类型的 schema
|
||||
|
||||
用于可视化编辑一个 Python 的 dict 类型的配置。如 AstrBot Core 中的自定义请求体参数配置项:
|
||||
|
||||
```py
|
||||
"custom_extra_body": {
|
||||
"description": "自定义请求体参数",
|
||||
"type": "dict",
|
||||
"items": {},
|
||||
"hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。",
|
||||
"template_schema": { # 可选填写 template schema,当设置之后,用户可以透过 WebUI 快速编辑。
|
||||
"temperature": {
|
||||
"name": "Temperature",
|
||||
"description": "温度参数",
|
||||
"hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。",
|
||||
"type": "float",
|
||||
"default": 0.6,
|
||||
"slider": {"min": 0, "max": 2, "step": 0.1},
|
||||
},
|
||||
"top_p": {
|
||||
"name": "Top-p",
|
||||
"description": "Top-p 采样",
|
||||
"hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"slider": {"min": 0, "max": 1, "step": 0.01},
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "最大令牌数",
|
||||
"hint": "生成的最大令牌数。",
|
||||
"type": "int",
|
||||
"default": 8192,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### template_list 类型的 schema
|
||||
|
||||
> [!NOTE]
|
||||
> v4.10.4 引入。更多信息请查看:[#4208](https://github.com/AstrBotDevs/AstrBot/pull/4208)
|
||||
|
||||
插件开发者可以在_conf_schema中按照以下格式添加模板配置项(有点类似于原有的嵌套配置)
|
||||
|
||||
```json
|
||||
"field_id": {
|
||||
"type": "template_list",
|
||||
"description": "Template List Field",
|
||||
"templates": {
|
||||
"template_1": {
|
||||
"name": "Template One",
|
||||
"hint":"hint",
|
||||
"items": {
|
||||
"attr_a": {
|
||||
"description": "Attribute A",
|
||||
"type": "int",
|
||||
"default": 10
|
||||
},
|
||||
"attr_b": {
|
||||
"description": "Attribute B",
|
||||
"hint": "This is a boolean attribute",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"template_2": {
|
||||
"name": "Template Two",
|
||||
"hint":"hint",
|
||||
"items": {
|
||||
"attr_c": {
|
||||
"description": "Attribute A",
|
||||
"type": "int",
|
||||
"default": 10
|
||||
},
|
||||
"attr_d": {
|
||||
"description": "Attribute B",
|
||||
"hint": "This is a boolean attribute",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
保存后的 config 为
|
||||
|
||||
```json
|
||||
"field_id": [
|
||||
{
|
||||
"__template_key": "template_1",
|
||||
"attr_a": 10,
|
||||
"attr_b": true
|
||||
},
|
||||
{
|
||||
"__template_key": "template_2",
|
||||
"attr_c": 10,
|
||||
"attr_d": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
<img width="1000" alt="image" src="https://github.com/user-attachments/assets/74876d30-11a4-491b-a7a0-8ebe8d603782" />
|
||||
|
||||
## 在插件中使用配置
|
||||
|
||||
AstrBot 在载入插件时会检测插件目录下是否有 `_conf_schema.json` 文件,如果有,会自动解析配置并保存在 `data/config/<plugin_name>_config.json` 下(依照 Schema 创建的配置文件实体),并在实例化插件类时传入给 `__init__()`。
|
||||
|
||||
```py
|
||||
from astrbot.api import AstrBotConfig
|
||||
|
||||
class ConfigPlugin(Star):
|
||||
def __init__(self, context: Context, config: AstrBotConfig): # AstrBotConfig 继承自 Dict,拥有字典的所有方法
|
||||
super().__init__(context)
|
||||
self.config = config
|
||||
print(self.config)
|
||||
|
||||
# 支持直接保存配置
|
||||
# self.config.save_config() # 保存配置
|
||||
```
|
||||
|
||||
## 配置更新
|
||||
|
||||
您在发布不同版本更新 Schema 时,AstrBot 会递归检查 Schema 的配置项,自动为缺失的配置项添加默认值、移除不存在的配置项。
|
||||
@@ -1,131 +0,0 @@
|
||||
|
||||
# 消息的发送
|
||||
|
||||
## 被动消息
|
||||
|
||||
被动消息指的是机器人被动回复消息。
|
||||
|
||||
```python
|
||||
@filter.command("helloworld")
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("Hello!")
|
||||
yield event.plain_result("你好!")
|
||||
|
||||
yield event.image_result("path/to/image.jpg") # 发送图片
|
||||
yield event.image_result("https://example.com/image.jpg") # 发送 URL 图片,务必以 http 或 https 开头
|
||||
```
|
||||
|
||||
## 主动消息
|
||||
|
||||
主动消息指的是机器人主动推送消息。某些平台可能不支持主动消息发送。
|
||||
|
||||
如果是一些定时任务或者不想立即发送消息,可以使用 `event.unified_msg_origin` 得到一个字符串并将其存储,然后在想发送消息的时候使用 `self.context.send_message(unified_msg_origin, chains)` 来发送消息。
|
||||
|
||||
```python
|
||||
from astrbot.api.event import MessageChain
|
||||
|
||||
@filter.command("helloworld")
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
umo = event.unified_msg_origin
|
||||
message_chain = MessageChain().message("Hello!").file_image("path/to/image.jpg")
|
||||
await self.context.send_message(event.unified_msg_origin, message_chain)
|
||||
```
|
||||
|
||||
通过这个特性,你可以将 unified_msg_origin 存储起来,然后在需要的时候发送消息。
|
||||
|
||||
> [!TIP]
|
||||
> 关于 unified_msg_origin。
|
||||
> unified_msg_origin 是一个字符串,记录了一个会话的唯一 ID,AstrBot 能够据此找到属于哪个消息平台的哪个会话。这样就能够实现在 `send_message` 的时候,发送消息到正确的会话。有关 MessageChain,请参见接下来的一节。
|
||||
|
||||
## 富媒体消息
|
||||
|
||||
AstrBot 支持发送富媒体消息,比如图片、语音、视频等。使用 `MessageChain` 来构建消息。
|
||||
|
||||
```python
|
||||
import astrbot.api.message_components as Comp
|
||||
|
||||
@filter.command("helloworld")
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
chain = [
|
||||
Comp.At(qq=event.get_sender_id()), # At 消息发送者
|
||||
Comp.Plain("来看这个图:"),
|
||||
Comp.Image.fromURL("https://example.com/image.jpg"), # 从 URL 发送图片
|
||||
Comp.Image.fromFileSystem("path/to/image.jpg"), # 从本地文件目录发送图片
|
||||
Comp.Plain("这是一个图片。")
|
||||
]
|
||||
yield event.chain_result(chain)
|
||||
```
|
||||
|
||||
上面构建了一个 `message chain`,也就是消息链,最终会发送一条包含了图片和文字的消息,并且保留顺序。
|
||||
|
||||
> [!TIP]
|
||||
> 在 aiocqhttp 消息适配器中,对于 `plain` 类型的消息,在发送中会使用 `strip()` 方法去除空格及换行符,可以在消息前后添加零宽空格 `\u200b` 以解决这个问题。
|
||||
|
||||
类似地,
|
||||
|
||||
**文件 File**
|
||||
|
||||
```py
|
||||
Comp.File(file="path/to/file.txt", name="file.txt") # 部分平台不支持
|
||||
```
|
||||
|
||||
**语音 Record**
|
||||
|
||||
```py
|
||||
path = "path/to/record.wav" # 暂时只接受 wav 格式,其他格式请自行转换
|
||||
Comp.Record(file=path, url=path)
|
||||
```
|
||||
|
||||
**视频 Video**
|
||||
|
||||
```py
|
||||
path = "path/to/video.mp4"
|
||||
Comp.Video.fromFileSystem(path=path)
|
||||
Comp.Video.fromURL(url="https://example.com/video.mp4")
|
||||
```
|
||||
|
||||
## 发送视频消息
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.command("test")
|
||||
async def test(self, event: AstrMessageEvent):
|
||||
from astrbot.api.message_components import Video
|
||||
# fromFileSystem 需要用户的协议端和机器人端处于一个系统中。
|
||||
music = Video.fromFileSystem(
|
||||
path="test.mp4"
|
||||
)
|
||||
# 更通用
|
||||
music = Video.fromURL(
|
||||
url="https://example.com/video.mp4"
|
||||
)
|
||||
yield event.chain_result([music])
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 发送群合并转发消息
|
||||
|
||||
> 大多数平台都不支持此种消息类型,当前适配情况:OneBot v11
|
||||
|
||||
可以按照如下方式发送群合并转发消息。
|
||||
|
||||
```py
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.command("test")
|
||||
async def test(self, event: AstrMessageEvent):
|
||||
from astrbot.api.message_components import Node, Plain, Image
|
||||
node = Node(
|
||||
uin=905617992,
|
||||
name="Soulter",
|
||||
content=[
|
||||
Plain("hi"),
|
||||
Image.fromFileSystem("test.jpg")
|
||||
]
|
||||
)
|
||||
yield event.chain_result([node])
|
||||
```
|
||||
|
||||

|
||||
@@ -1,113 +0,0 @@
|
||||
|
||||
# 会话控制
|
||||
|
||||
> 大于等于 v3.4.36
|
||||
|
||||
为什么需要会话控制?考虑一个 成语接龙 插件,某个/群用户需要和机器人进行多次对话,而不是一次性的指令。这时候就需要会话控制。
|
||||
|
||||
```txt
|
||||
用户: /成语接龙
|
||||
机器人: 请发送一个成语
|
||||
用户: 一马当先
|
||||
机器人: 先见之明
|
||||
用户: 明察秋毫
|
||||
...
|
||||
```
|
||||
|
||||
AstrBot 提供了开箱即用的会话控制功能:
|
||||
|
||||
导入:
|
||||
|
||||
```py
|
||||
import astrbot.api.message_components as Comp
|
||||
from astrbot.core.utils.session_waiter import (
|
||||
session_waiter,
|
||||
SessionController,
|
||||
)
|
||||
```
|
||||
|
||||
handler 内的代码可以如下:
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent
|
||||
|
||||
@filter.command("成语接龙")
|
||||
async def handle_empty_mention(self, event: AstrMessageEvent):
|
||||
"""成语接龙具体实现"""
|
||||
try:
|
||||
yield event.plain_result("请发送一个成语~")
|
||||
|
||||
# 具体的会话控制器使用方法
|
||||
@session_waiter(timeout=60, record_history_chains=False) # 注册一个会话控制器,设置超时时间为 60 秒,不记录历史消息链
|
||||
async def empty_mention_waiter(controller: SessionController, event: AstrMessageEvent):
|
||||
idiom = event.message_str # 用户发来的成语,假设是 "一马当先"
|
||||
|
||||
if idiom == "退出": # 假设用户想主动退出成语接龙,输入了 "退出"
|
||||
await event.send(event.plain_result("已退出成语接龙~"))
|
||||
controller.stop() # 停止会话控制器,会立即结束。
|
||||
return
|
||||
|
||||
if len(idiom) != 4: # 假设用户输入的不是4字成语
|
||||
await event.send(event.plain_result("成语必须是四个字的呢~")) # 发送回复,不能使用 yield
|
||||
return
|
||||
# 退出当前方法,不执行后续逻辑,但此会话并未中断,后续的用户输入仍然会进入当前会话
|
||||
|
||||
# ...
|
||||
message_result = event.make_result()
|
||||
message_result.chain = [Comp.Plain("先见之明")] # import astrbot.api.message_components as Comp
|
||||
await event.send(message_result) # 发送回复,不能使用 yield
|
||||
|
||||
controller.keep(timeout=60, reset_timeout=True) # 重置超时时间为 60s,如果不重置,则会继续之前的超时时间计时。
|
||||
|
||||
# controller.stop() # 停止会话控制器,会立即结束。
|
||||
# 如果记录了历史消息链,可以通过 controller.get_history_chains() 获取历史消息链
|
||||
|
||||
try:
|
||||
await empty_mention_waiter(event)
|
||||
except TimeoutError as _: # 当超时后,会话控制器会抛出 TimeoutError
|
||||
yield event.plain_result("你超时了!")
|
||||
except Exception as e:
|
||||
yield event.plain_result("发生错误,请联系管理员: " + str(e))
|
||||
finally:
|
||||
event.stop_event()
|
||||
except Exception as e:
|
||||
logger.error("handle_empty_mention error: " + str(e))
|
||||
```
|
||||
|
||||
当激活会话控制器后,该发送人之后发送的消息会首先经过上面你定义的 `empty_mention_waiter` 函数处理,直到会话控制器被停止或者超时。
|
||||
|
||||
## SessionController
|
||||
|
||||
用于开发者控制这个会话是否应该结束,并且可以拿到历史消息链。
|
||||
|
||||
- keep(): 保持这个会话
|
||||
- timeout (float): 必填。会话超时时间。
|
||||
- reset_timeout (bool): 设置为 True 时, 代表重置超时时间, timeout 必须 > 0, 如果 <= 0 则立即结束会话。设置为 False 时, 代表继续维持原来的超时时间, 新 timeout = 原来剩余的 timeout + timeout (可以 < 0)
|
||||
- stop(): 结束这个会话
|
||||
- get_history_chains() -> List[List[Comp.BaseMessageComponent]]: 获取历史消息链
|
||||
|
||||
## 自定义会话 ID 算子
|
||||
|
||||
默认情况下,AstrBot 会话控制器会将基于 `sender_id` (发送人的 ID)作为识别不同会话的标识,如果想将一整个群作为一个会话,则需要自定义会话 ID 算子。
|
||||
|
||||
```py
|
||||
import astrbot.api.message_components as Comp
|
||||
from astrbot.core.utils.session_waiter import (
|
||||
session_waiter,
|
||||
SessionFilter,
|
||||
SessionController,
|
||||
)
|
||||
|
||||
# 沿用上面的 handler
|
||||
# ...
|
||||
class CustomFilter(SessionFilter):
|
||||
def filter(self, event: AstrMessageEvent) -> str:
|
||||
return event.get_group_id() if event.get_group_id() else event.unified_msg_origin
|
||||
|
||||
await empty_mention_waiter(event, session_filter=CustomFilter()) # 这里传入 session_filter
|
||||
# ...
|
||||
```
|
||||
|
||||
这样之后,当群内一个用户发送消息后,会话控制器会将这个群作为一个会话,群内其他用户发送的消息也会被认为是同一个会话。
|
||||
|
||||
甚至,可以使用这个特性来让群内组队!
|
||||
@@ -1,41 +0,0 @@
|
||||
# 最小实例
|
||||
|
||||
插件模版中的 `main.py` 是一个最小的插件实例。
|
||||
|
||||
```python
|
||||
from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult
|
||||
from astrbot.api.star import Context, Star, register
|
||||
from astrbot.api import logger # 使用 astrbot 提供的 logger 接口
|
||||
|
||||
class MyPlugin(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
# 注册指令的装饰器。指令名为 helloworld。注册成功后,发送 `/helloworld` 就会触发这个指令,并回复 `你好, {user_name}!`
|
||||
@filter.command("helloworld")
|
||||
async def helloworld(self, event: AstrMessageEvent):
|
||||
'''这是一个 hello world 指令''' # 这是 handler 的描述,将会被解析方便用户了解插件内容。非常建议填写。
|
||||
user_name = event.get_sender_name()
|
||||
message_str = event.message_str # 获取消息的纯文本内容
|
||||
logger.info("触发hello world指令!")
|
||||
yield event.plain_result(f"Hello, {user_name}!") # 发送一条纯文本消息
|
||||
|
||||
async def terminate(self):
|
||||
'''可选择实现 terminate 函数,当插件被卸载/停用时会调用。'''
|
||||
```
|
||||
|
||||
解释如下:
|
||||
|
||||
- 插件需要继承 `Star` 类。
|
||||
- `Context` 类用于插件与 AstrBot Core 交互,可以由此调用 AstrBot Core 提供的各种 API。
|
||||
- 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。
|
||||
- `AstrMessageEvent` 是 AstrBot 的消息事件对象,存储了消息发送者、消息内容等信息。
|
||||
- `AstrBotMessage` 是 AstrBot 的消息对象,存储了消息平台下发的消息的具体内容。可以通过 `event.message_obj` 获取。
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> `Handler` 一定需要在插件类中注册,前两个参数必须为 `self` 和 `event`。如果文件行数过长,可以将服务写在外部,然后在 `Handler` 中调用。
|
||||
>
|
||||
> 插件类所在的文件名需要命名为 `main.py`。
|
||||
|
||||
所有的处理函数都需写在插件类中。为了精简内容,在之后的章节中,我们可能会忽略插件类的定义。
|
||||
@@ -1,31 +0,0 @@
|
||||
# 插件存储
|
||||
|
||||
## 简单 KV 存储
|
||||
|
||||
> [!TIP]
|
||||
> 该功能需要 AstrBot 版本 >= 4.9.2。
|
||||
|
||||
插件可以使用 AstrBot 提供的简单 KV 存储功能来存储一些配置信息或临时数据。该存储是基于插件维度的,每个插件有独立的存储空间,互不干扰。
|
||||
|
||||
```py
|
||||
class Main(star.Star):
|
||||
@filter.command("hello")
|
||||
async def hello(self, event: AstrMessageEvent):
|
||||
"""Aloha!"""
|
||||
await self.put_kv_data("greeted", True)
|
||||
greeted = await self.get_kv_data("greeted", False)
|
||||
await self.delete_kv_data("greeted")
|
||||
```
|
||||
|
||||
|
||||
## 存储大文件规范
|
||||
|
||||
为了规范插件存储大文件的行为,请将大文件存储于 `data/plugin_data/{plugin_name}/` 目录下。
|
||||
|
||||
你可以通过以下代码获取插件数据目录:
|
||||
|
||||
```py
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
plugin_data_path = get_astrbot_data_path() / "plugin_data" / self.name # self.name 为插件名称,在 v4.9.2 及以上版本可用,低于此版本请自行指定插件名称
|
||||
```
|
||||
@@ -1,130 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# AstrBot 插件开发指南 🌠
|
||||
|
||||
欢迎来到 AstrBot 插件开发指南!本章节将引导您如何开发 AstrBot 插件。在我们开始之前,希望你能具备以下基础知识:
|
||||
|
||||
1. 有一定的 Python 编程经验。
|
||||
2. 有一定的 Git、GitHub 使用经验。
|
||||
|
||||
欢迎加入我们的开发者专用 QQ 群: `975206796`。
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 获取插件模板
|
||||
|
||||
1. 打开 AstrBot 插件模板: [helloworld](https://github.com/Soulter/helloworld)
|
||||
2. 点击右上角的 `Use this template`
|
||||
3. 然后点击 `Create new repository`。
|
||||
4. 在 `Repository name` 处填写您的插件名。插件名格式:
|
||||
- 推荐以 `astrbot_plugin_` 开头;
|
||||
- 不能包含空格;
|
||||
- 保持全部字母小写;
|
||||
- 尽量简短。
|
||||
5. 点击右下角的 `Create repository`。
|
||||
|
||||
### 克隆项目到本地
|
||||
|
||||
克隆 AstrBot 项目本体和刚刚创建的插件仓库到本地。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
mkdir -p AstrBot/data/plugins
|
||||
cd AstrBot/data/plugins
|
||||
git clone 插件仓库地址
|
||||
```
|
||||
|
||||
然后,使用 `VSCode` 打开 `AstrBot` 项目。找到 `data/plugins/<你的插件名字>` 目录。
|
||||
|
||||
更新 `metadata.yaml` 文件,填写插件的元数据信息。
|
||||
|
||||
> [!WARNING]
|
||||
> 请务必修改此文件,AstrBot 识别插件元数据依赖于 `metadata.yaml` 文件。
|
||||
|
||||
### 设置插件 Logo(可选)
|
||||
|
||||
可以在插件目录下添加 `logo.png` 文件作为插件的 Logo。请保持长宽比为 1:1,推荐尺寸为 256x256。
|
||||
|
||||

|
||||
|
||||
### 插件展示名(可选)
|
||||
|
||||
可以修改(或添加) `metadata.yaml` 文件中的 `display_name` 字段,作为插件在插件市场等场景中的展示名,以方便用户阅读。
|
||||
|
||||
### 声明支持平台(Optional)
|
||||
|
||||
你可以在 `metadata.yaml` 中新增 `support_platforms` 字段(`list[str]`),声明插件支持的平台适配器。WebUI 插件页会展示该字段。
|
||||
|
||||
```yaml
|
||||
support_platforms:
|
||||
- telegram
|
||||
- discord
|
||||
```
|
||||
|
||||
`support_platforms` 中的值需要使用 `ADAPTER_NAME_2_TYPE` 的 key,目前支持:
|
||||
|
||||
- `aiocqhttp`
|
||||
- `qq_official`
|
||||
- `telegram`
|
||||
- `wecom`
|
||||
- `lark`
|
||||
- `dingtalk`
|
||||
- `discord`
|
||||
- `slack`
|
||||
- `kook`
|
||||
- `vocechat`
|
||||
- `weixin_official_account`
|
||||
- `satori`
|
||||
- `misskey`
|
||||
- `line`
|
||||
|
||||
### 声明 AstrBot 版本范围(Optional)
|
||||
|
||||
你可以在 `metadata.yaml` 中新增 `astrbot_version` 字段,声明插件要求的 AstrBot 版本范围。格式与 `pyproject.toml` 依赖版本约束一致(PEP 440),且不要加 `v` 前缀。
|
||||
|
||||
```yaml
|
||||
astrbot_version: ">=4.16,<5"
|
||||
```
|
||||
|
||||
可选示例:
|
||||
|
||||
- `>=4.17.0`
|
||||
- `>=4.16,<5`
|
||||
- `~=4.17`
|
||||
|
||||
如果你只想声明最低版本,可以直接写:
|
||||
|
||||
- `>=4.17.0`
|
||||
|
||||
当当前 AstrBot 版本不满足该范围时,插件会被阻止加载并提示版本不兼容。
|
||||
在 WebUI 安装插件时,你可以选择“无视警告,继续安装”来跳过这个检查。
|
||||
|
||||
### 调试插件
|
||||
|
||||
AstrBot 采用在运行时注入插件的机制。因此,在调试插件时,需要启动 AstrBot 本体。
|
||||
|
||||
您可以使用 AstrBot 的热重载功能简化开发流程。
|
||||
|
||||
插件的代码修改后,可以在 AstrBot WebUI 的插件管理处找到自己的插件,点击右上角 `...` 按钮,选择 `重载插件`。
|
||||
|
||||
如果插件因为代码错误等原因加载失败,你也可以在管理面板的错误提示中点击 **“尝试一键重载修复”** 来重新加载。
|
||||
|
||||
### 插件依赖管理
|
||||
|
||||
目前 AstrBot 对插件的依赖管理使用 `pip` 自带的 `requirements.txt` 文件。如果你的插件需要依赖第三方库,请务必在插件目录下创建 `requirements.txt` 文件并写入所使用的依赖库,以防止用户在安装你的插件时出现依赖未找到(Module Not Found)的问题。
|
||||
|
||||
> `requirements.txt` 的完整格式可以参考 [pip 官方文档](https://pip.pypa.io/en/stable/reference/requirements-file-format/)。
|
||||
|
||||
## 开发原则
|
||||
|
||||
感谢您为 AstrBot 生态做出贡献,开发插件请遵守以下原则,这也是良好的编程习惯。
|
||||
|
||||
- 功能需经过测试。
|
||||
- 需包含良好的注释。
|
||||
- 持久化数据请存储于 `data` 目录下,而非插件自身目录,防止更新/重装插件时数据被覆盖。
|
||||
- 良好的错误处理机制,不要让插件因一个错误而崩溃。
|
||||
- 在进行提交前,请使用 [ruff](https://docs.astral.sh/ruff/) 工具格式化您的代码。
|
||||
- 不要使用 `requests` 库来进行网络请求,可以使用 `aiohttp`, `httpx` 等异步网络请求库。
|
||||
- 如果是对某个插件进行功能扩增,请优先给那个插件提交 PR 而不是单独再写一个插件(除非原插件作者已经停止维护)。
|
||||
@@ -1,9 +0,0 @@
|
||||
# 发布插件到插件市场
|
||||
|
||||
在编写完插件后,你可以选择将插件发布到 AstrBot 的插件市场,让更多用户使用你的插件。
|
||||
|
||||
AstrBot 使用 GitHub 托管插件,因此你需要先将插件代码推送到之前创建的 GitHub 插件仓库中。
|
||||
|
||||
你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GTIHUB` 按钮,你将会被导航到 AstrBot 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。
|
||||
|
||||

|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
# Agent 执行器
|
||||
|
||||
Agent 执行器是 AstrBot 中用于执行 Agent 的组件。
|
||||
|
||||
在 v4.7.0 版本之后,我们将 Dify、Coze、阿里云百炼应用这三个提供商迁移到了 Agent 执行器层面,减少了与 AstrBot 目前功能的一些冲突。请放心,如果您从旧版本升级到 v4.7.0 版本,您无需进行任何操作,AstrBot 会自动为您迁移。此后,AstrBot 也新增了 DeerFlow Agent 执行器支持。
|
||||
|
||||
AstrBot 目前支持五种 Agent 执行器:
|
||||
|
||||
- AstrBot 内置 Agent 执行器
|
||||
- Dify Agent 执行器
|
||||
- Coze Agent 执行器
|
||||
- 阿里云百炼应用 Agent 执行器
|
||||
- DeerFlow Agent 执行器
|
||||
|
||||
默认情况下,AstrBot 内置 Agent 执行器为默认执行器。
|
||||
|
||||
## 为什么需要抽象出 Agent 执行器
|
||||
|
||||
在早期版本中,Dify、Coze、阿里云百炼应用这类「自带 Agent 能力」的平台,是作为普通 Chat Provider 集成进 AstrBot 的。实践下来会发现,它们和传统「只负责补全文本」的 Chat Provider 有本质差异,强行放在同一层会带来很多设计和使用上的冲突。因此,从 v4.7.0 起,我们将它们抽象为独立的 Agent 执行器(Agent Runner)。
|
||||
|
||||
从架构上看,可以理解为:
|
||||
|
||||
- Chat Provider 负责「说话」;
|
||||
- Agent 执行器负责「思考 + 做事」。
|
||||
|
||||
Agent 执行器会调用 Chat Provider 的接口,并根据 Chat Provider 的回复,进行多轮「感知 → 规划 → 执行动作 → 观察结果 → 再规划」的循环。
|
||||
|
||||
Chat Provider 本质上是一个 `单轮补全接口`,输入 prompt + 历史对话 + 工具列表,输出模型回复(文本、工具调用指令等)。
|
||||
|
||||
而 Agent Runner 通常是一个 `循环(Loop)`,接收用户意图、上下文与环境状态,基于策略 / 模型做出规划(Plan),选择并调用工具(Act),从环境中读取结果(Observe),再次理解结果、更新内部状态,决定下一步动作,重复上述过程,直到任务完成或超时。
|
||||
|
||||

|
||||
|
||||
Dify、Coze、百炼应用、DeerFlow 等平台已经内置了这个循环,如果把它们当成普通 Chat Provider,会和 AstrBot 的内置 Agent 执行器功能冲突。
|
||||
|
||||
## 使用
|
||||
|
||||
默认情况下,AstrBot 内置 Agent 执行器为默认执行器。使用默认执行器已经可以满足大部分需求,并且可以使用 AstrBot 的 MCP、知识库、网页搜索等功能。
|
||||
|
||||
如果你需要使用 Dify、Coze、百炼应用、DeerFlow 等平台的能力,可以创建一个 Agent 执行器,并选择相应的提供商。
|
||||
|
||||
## 创建 Agent 执行器
|
||||
|
||||

|
||||
|
||||
在 WebUI 中,点击「模型提供商」->「新增提供商」,选择「Agent 执行器」,选择你想接入的平台或执行器类型,填写相关信息即可。
|
||||
|
||||
## 更换默认 Agent 执行器
|
||||
|
||||

|
||||
|
||||
在 WebUI 中,点击「配置」->「Agent 执行方式」,将执行器类型更换为你刚刚创建的 Agent 执行器类型,然后选择 `XX Agent 执行器提供商 ID` 为你刚刚创建的 Agent 执行器提供商的 ID,点击保存即可。
|
||||
@@ -1,90 +0,0 @@
|
||||
# Agent 沙盒环境 ⛵️
|
||||
|
||||
> [!TIP]
|
||||
> 此功能目前处于技术预览阶段,可能会存在一些 Bug。如果您遇到了问题,请在 [GitHub](https://github.com/AstrBotDevs/AstrBot/issues) 上提交 issue。
|
||||
|
||||
在 `v4.12.0` 版本及之后,AstrBot 引入了 Agent 沙盒环境,以替代之前的代码执行器功能。沙盒环境给 Agent 提供了更安全、更灵活的代码执行和自动化操作能力。
|
||||
|
||||

|
||||
|
||||
## 启用沙盒环境
|
||||
|
||||
目前,沙盒环境仅支持通过 Docker 来运行。我们目前使用了 [Shipyard](https://github.com/AstrBotDevs/shipyard) 项目作为 AstrBot 的沙盒环境驱动器。未来,我们会支持更多类型的沙盒环境驱动器,如 e2b。
|
||||
|
||||
## 性能要求
|
||||
|
||||
AstrBot 给每个沙盒环境限制最高 1 CPU 和 512 MB 内存。
|
||||
|
||||
我们建议您的宿主机至少有 2 个 CPU 和 4 GB 内存,并开启 Swap,以保证多个沙盒环境实例可以稳定运行。
|
||||
|
||||
### 使用 Docker Compose 部署 AstrBot 和 Shipyard
|
||||
|
||||
如果您还没有部署 AstrBot,或者想更换为我们推荐的带沙盒环境的部署方式,推荐使用 Docker Compose 来部署 AstrBot,代码如下:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
cd AstrBot
|
||||
# 修改 compose-with-shipyard.yml 文件中的环境变量配置,例如 Shipyard 的 access token 等
|
||||
docker compose -f compose-with-shipyard.yml up -d
|
||||
docker pull soulter/shipyard-ship:latest
|
||||
```
|
||||
|
||||
这会启动一个包含 AstrBot 主程序和沙盒环境的 Docker Compose 服务。
|
||||
|
||||
### 单独部署 Shipyard
|
||||
|
||||
如果您已经部署了 AstrBot,但没有部署沙盒环境,可以单独部署 Shipyard。
|
||||
|
||||
代码如下:
|
||||
|
||||
```bash
|
||||
mkdir astrbot-shipyard
|
||||
cd astrbot-shipyard
|
||||
wget https://raw.githubusercontent.com/AstrBotDevs/shipyard/refs/heads/main/pkgs/bay/docker-compose.yml -O docker-compose.yml
|
||||
# 修改 compose-with-shipyard.yml 文件中的环境变量配置,例如 Shipyard 的 access token 等
|
||||
docker compose -f docker-compose.yml up -d
|
||||
docker pull soulter/shipyard-ship:latest
|
||||
```
|
||||
|
||||
部署成功后,上述命令会启动一个 Shipyard 服务,默认监听在 `http://<your-host>:8156`。
|
||||
|
||||
> [!TIP]
|
||||
> 如果您使用 Docker 部署 AstrBot,您也可以修改上面的 Compose 文件,将 Shipyard 的网络与 AstrBot 放在同一个 Docker 网络中,这样就不需要暴露 Shipyard 的端口到宿主机。
|
||||
|
||||
## 配置 AstrBot 使用沙盒环境
|
||||
|
||||
> [!TIP]
|
||||
> 请确保您的 AstrBot 版本在 `v4.12.0` 及之后。
|
||||
|
||||
在 AstrBot 控制台,进入 “配置文件” 页面,找到 “Agent 沙箱环境”,启用沙箱环境开关。
|
||||
|
||||
在出现的配置项中,
|
||||
|
||||
对于 `Shipyard API Endpoint`,如果您使用上述的 Docker Compose 部署方式,填写 `http://shipyard:8156` 即可。如果您是单独部署的 Shipyard,请填写对应的地址,例如 `http://<your-host>:8156`。
|
||||
|
||||
对于 `Shipyard Access Token`,请填写您在部署 Shipyard 时配置的访问令牌。
|
||||
|
||||
对于 `Shipyard Ship 存活时间(秒)`,这个定义了每个沙箱环境实例的存活时间,默认值为 3600 秒(1 小时)。您可以根据需要调整这个值。
|
||||
|
||||
对于 `Shipyard Ship 会话复用上限`,这个定义了每个沙箱环境实例可以复用的最大会话数,默认值为 10。也就是 10 个会话会共享同一个沙箱环境实例。您可以根据需要调整这个值。
|
||||
|
||||
填写好之后,点击右下角 “保存” 即可。
|
||||
|
||||
## 关于 `Shipyard Ship 存活时间(秒)`
|
||||
|
||||
沙箱环境实例的存活时间定义了每个实例在被销毁之前可以存在的最长时间,这个时间的设置需要根据您的使用场景以及资源来决定。
|
||||
|
||||
- 新的会话加入已有的沙箱环境实例时,该实例会自动延长存活时间到这个会话请求的 TTL。
|
||||
- 当对沙箱环境实例执行操作后,该实例会自动延长存活时间到当前时间加上 TTL。
|
||||
|
||||
## 关于沙盒环境的数据持久化
|
||||
|
||||
Shipyard 会给每个会话分配一个工作目录,在 `/home/<会话唯一 ID>` 目录下。
|
||||
|
||||
Shipyard 会自动将沙盒环境中的 /home 目录挂载到宿主机的 `${PWD}/data/shipyard/ship_mnt_data` 目录下,当沙盒环境实例被销毁后,如果某个会话继续请求调用沙箱,Shipyard 会重新创建一个新的沙盒环境实例,并将之前持久化的数据重新挂载进去,保证数据的连续性。
|
||||
|
||||
## 其他同类社区插件
|
||||
|
||||
### luosheng520qaq/astrobot_plugin_code_executor
|
||||
|
||||
如果您资源有限,不希望使用沙盒环境来执行代码,可以尝试 luosheng520qaq 开发的 [astrobot_plugin_code_executor](https://github.com/luosheng520qaq/astrobot_plugin_code_executor) 插件。该插件会直接在宿主机上执行代码。插件已经尽力提升安全性,但仍需留意代码安全性问题。
|
||||
@@ -1,96 +0,0 @@
|
||||
# 基于 Docker 的代码执行器
|
||||
|
||||
> [!WARNING]
|
||||
> 已过时,请参考最新的 [Agent 沙盒环境](/use/astrbot-agent-sandbox.md) 文档。在 v4.12.0 之后,该功能不可用。
|
||||
|
||||
在 `v3.4.2` 版本及之后,AstrBot 支持代码执行器以强化 LLM 的能力,并实现一些自动化的操作。
|
||||
|
||||
> [!TIP]
|
||||
> 此功能目前处于实验阶段,可能会有一些问题。如果您遇到了问题,请在 [GitHub](https://github.com/AstrBotDevs/AstrBot/issues) 上提交 issue。欢迎加群讨论:[322154837](https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft)。
|
||||
|
||||
如果您要使用此功能,请确保您的机器安装了 `Docker`。因为此功能需要启动专用的 Docker 沙箱环境以执行代码,以防止 LLM 生成恶意代码对您的机器造成损害。
|
||||
|
||||
|
||||
## Linux Docker 启动 AstrBot
|
||||
|
||||
如果您使用 Docker 部署了 AstrBot,需要多做一些工作。
|
||||
|
||||
1. 您需要在启动 Docker 容器时,请将 `/var/run/docker.sock` 挂载到容器内部。这样 AstrBot 才能够启动沙箱容器。
|
||||
|
||||
```bash
|
||||
sudo docker run -itd -p 6180-6200:6180-6200 -p 11451:11451 -v $PWD/data:/AstrBot/data -v /var/run/docker.sock:/var/run/docker.sock --name astrbot soulter/astrbot:latest
|
||||
```
|
||||
|
||||
2. 在聊天时使用 `/pi absdir <绝对路径地址>` 设置您宿主机上 AstrBot 的 data 目录的所在目录的绝对路径。
|
||||
|
||||
例子:
|
||||
|
||||

|
||||
|
||||
## Linux 手动源码 启动 AstrBot
|
||||
|
||||
**如果你的 Docker 指令需要 sudo 权限来执行**,那么你需要在启动 AstrBot 时,使用 `sudo` 来启动,否则代码执行器会因为权限不足而无法调用 Docker。
|
||||
|
||||
```bash
|
||||
sudo —E python3 main.py
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
本功能使用的镜像是 `soulter/astrbot-code-interpreter-sandbox`,您可以在 [Docker Hub](https://hub.docker.com/r/soulter/astrbot-code-interpreter-sandbox) 上查看镜像的详细信息。
|
||||
|
||||
镜像中提供了常用的 Python 库:
|
||||
|
||||
- Pillow
|
||||
- requests
|
||||
- numpy
|
||||
- matplotlib
|
||||
- scipy
|
||||
- scikit-learn
|
||||
- beautifulsoup4
|
||||
- pandas
|
||||
- opencv-python
|
||||
- python-docx
|
||||
- python-pptx
|
||||
- pymupdf
|
||||
- mplfonts
|
||||
|
||||
基本上能够实现的任务:
|
||||
|
||||
- 图片编辑
|
||||
- 网页抓取等
|
||||
- 数据分析、简单的机器学习
|
||||
- 文档处理,如读写 Word、PPT、PDF 等
|
||||
- 数学计算,如画图、求解方程等
|
||||
|
||||
由于中国大陆无法访问 docker hub,因此如果您的环境在中国大陆,请使用 `/pi mirror` 来查看/设置镜像源。比如,截至本文档编写时,您可以使用 `cjie.eu.org` 作为镜像源。即设置 `/pi mirror cjie.eu.org`。
|
||||
|
||||
在第一次触发代码执行器时,AstrBot 会自动拉取镜像,这可能需要一些时间。请耐心等待。
|
||||
|
||||
镜像可能会不定时间更新以提供更多的功能,因此请定期查看镜像的更新。如果需要更新镜像,可以使用 `/pi repull` 命令重新拉取镜像。
|
||||
|
||||
> [!TIP]
|
||||
> 如果一开始没有正常启动此功能,在启动成功之后,需要执行 `/tool on python_interpreter` 来开启此功能。
|
||||
> 您可以通过 `/tool ls` 查看所有的工具以及它们的启用状态。
|
||||
|
||||

|
||||
|
||||
## 图片和文件的输入
|
||||
|
||||
代码执行器除了能够识别和处理图片、文字任务,还能够识别您发送的文件,并且能够发送文件。
|
||||
|
||||
v3.4.34 后,使用 `/pi file` 指令开始上传文件。上传文件后,您可以使用 `/pi list` 查看您上传的文件,使用 `/pi clean` 清空您上传的文件。
|
||||
|
||||
上传的文件将会用于代码执行器的输入。
|
||||
|
||||
比如您希望对一张图片添加圆角,您可以使用 `/pi file` 上传图片,然后再提问:`请运行代码,对这张图片添加圆角`。
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -1,5 +0,0 @@
|
||||
# 内置指令
|
||||
|
||||
AstrBot 具有很多内置指令,它们通过插件的形式被导入。位于 `packages/astrbot` 目录下。
|
||||
|
||||
使用 `/help` 可以查看所有内置指令。
|
||||
@@ -1,41 +0,0 @@
|
||||
# 上下文压缩
|
||||
|
||||
在 v4.11.0 之后,AstrBot 引入了自动上下文压缩功能。
|
||||
|
||||

|
||||
|
||||
AstrBot 会在对话上下文达到**使用的对话模型上下文窗口的最大长度的 82% 时**,自动对上下文进行压缩,以确保在不丢失关键信息的情况下,尽可能多地保留对话内容。
|
||||
|
||||
## 压缩策略
|
||||
|
||||
目前有两种压缩策略
|
||||
|
||||
1. 按照对话轮数截断。这种策略会简单地删除最早的对话内容,直到上下文长度符合要求。您可以指定一次性丢弃的对话轮数,默认为 1 轮。这种策略为**默认策略**。
|
||||
2. 由 LLM 压缩上下文。这种策略会调用您指定的模型本身来总结和压缩对话内容,从而保留更多的关键信息。您可以指定压缩时使用的对话模型,如果不选择,将会自动回退到 “按照对话轮数截断” 策略。您可以设置压缩时保留最近对话轮数,默认为 4。您还可以自定义压缩时的提示词。默认提示词为:
|
||||
|
||||
```
|
||||
Based on our full conversation history, produce a concise summary of key takeaways and/or project progress.
|
||||
1. Systematically cover all core topics discussed and the final conclusion/outcome for each; clearly highlight the latest primary focus.
|
||||
2. If any tools were used, summarize tool usage (total call count) and extract the most valuable insights from tool outputs.
|
||||
3. If there was an initial user goal, state it first and describe the current progress/status.
|
||||
4. Write the summary in the user's language.
|
||||
```
|
||||
|
||||
在压缩一轮之后,AstrBot 会二次检查当前上下文长度是否符合要求。如果仍然不符合要求,则会采用对半砍策略,即将当前上下文内容砍掉一半,直到符合要求为止。
|
||||
|
||||
- AstrBot 会在每次对话请求前调用压缩器进行检查。
|
||||
- 当前版本下 AstrBot 不会在工具调用过程中进行上下文压缩,未来我们会支持这一功能,敬请期待。
|
||||
|
||||
## ‼️ 重要:模型上下文窗口设置
|
||||
|
||||
默认情况下,当您添加模型时,AstrBot 会自动根据模型的 id,从 [MODELS.DEV](https://models.dev/) 提供的接口中获取模型的上下文窗口大小。但由于模型种类繁多,部分提供商甚至会修改模型的 id,因此 AstrBot 不能自动推断出您所添加的模型的上下文窗口大小。
|
||||
|
||||
您可以手动在模型配置中设置模型的上下文窗口大小,参考下图:
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> 如果没有看到上图中的配置项,请您删除该模型,然后重新添加模型即可。
|
||||
|
||||
当模型上下文窗口大小被设置为 0 时,在每次请求时,AstrBot 仍会自动从 MODELS.DEV 获取模型的上下文窗口大小。如果仍为 0,则这次请求不会启用上下文压缩功能。
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# 自定义规则
|
||||
|
||||
> [!NOTE]
|
||||
> 下文的「消息会话来源」指的是 UMO。一个 UMO 唯一指定了一个消息平台下的具体的某个会话。
|
||||
|
||||
在 v4.7.0 版本之后,我们重构了 AstrBot 原来的「会话管理」功能为「自定义规则」功能。以减少和配置文件的冲突。
|
||||
|
||||
你可以把自定义规则理解为对指定消息来源更加灵活的自定义强制处理规则,其优先级高于配置文件。
|
||||
|
||||
例如,原本一个消息平台使用配置文件 “default”,这个消息平台下的所有会话都按照配置文件中的规则进行处理。如果你希望对某个会话来源 A 进行特殊处理,在原来,你需要单独创建一个配置文件,然后将 A 绑定到这个配置文件中。而现在,你只需要在 WebUI 的自定义规则页中创建一个自定义规则,然后选择消息来源 A 即可。你可以定义如下规则:
|
||||
|
||||
1. 是否启用该消息会话来源的消息处理。如果不启用,其效果相当于将该消息会话来源拉入黑名单。
|
||||
2. 是否对该消息会话来源的消息启用 LLM。如果不启用,则不会使用 AI 能力。
|
||||
3. 是否对该消息会话来源的消息启用 TTS。如果不启用,则不会使用 TTS 能力。
|
||||
4. 对该消息会话来源配置特定的聊天模型、语音识别模型(STT)、语音合成模型(TTS)。
|
||||
5. 对该消息会话来源配置特定的人格。
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 函数调用(Function-calling)
|
||||
|
||||
## 简介
|
||||
|
||||
函数调用旨在提供大模型**调用外部工具的能力**,以此实现 Agentic 的一些功能。
|
||||
|
||||
比如,问大模型:帮我搜索一下关于“猫”的信息,大模型会调用用于搜索的外部工具,比如搜索引擎,然后返回搜索结果。
|
||||
|
||||
目前,支持的模型包括但远不限于
|
||||
|
||||
- GPT-5.x 系列
|
||||
- Gemini 3.x 系列
|
||||
- Claude 4.x 系列
|
||||
- Deepseek v3.2(deepseek-chat)
|
||||
- Qwen 3.x 系列
|
||||
|
||||
2025年后推出的主流模型通常已支持函数调用。
|
||||
|
||||
不支持的模型比较常见的有 Deepseek-R1, Gemini 2.0 的 thinking 类等较老模型。
|
||||
|
||||
在 AstrBot 中,默认提供了网页搜索、待办提醒、代码执行器这些工具。很多插件,如:
|
||||
|
||||
- astrbot_plugin_cloudmusic
|
||||
- astrbot_plugin_bilibili
|
||||
- ...
|
||||
|
||||
等在提供传统的指令调用的基础上,也提供了函数调用的功能。
|
||||
|
||||
相关指令:
|
||||
|
||||
- `/tool ls` 查看当前具有的工具列表
|
||||
- `/tool on` 开启某个工具
|
||||
- `/tool off` 关闭某个工具
|
||||
- `/tool off_all` 关闭所有工具
|
||||
|
||||
某些模型可能不支持函数调用,会返回诸如 `tool call is not supported`, `function calling is not supported`, `tool use is not supported` 等错误。在大多数情况下,AstrBot 能够检测到这种错误并自动帮您去除函数调用工具。如果你发现某个模型不支持函数调用,也可使用 `/tool off_all` 命令关闭所有工具,然后再次尝试。或者更换为支持函数调用的模型。
|
||||
|
||||
|
||||
下面是一些常见的工具调用 Demo:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## MCP
|
||||
|
||||
请前往此文档 [AstrBot - MCP](/use/mcp) 查看。
|
||||
@@ -1,49 +0,0 @@
|
||||
# AstrBot 知识库
|
||||
|
||||

|
||||
|
||||
## 配置嵌入模型
|
||||
|
||||
打开服务提供商页面,点击新增服务提供商,选择 Embedding。
|
||||
|
||||
目前 AstrBot 支持兼容 OpenAI API 和 Gemini API 的嵌入向量服务。
|
||||
|
||||
点击上面的提供商卡片进入配置页面,填写配置。
|
||||
|
||||
配置完成后,点击保存。
|
||||
|
||||
## 配置重排序模型(可选)
|
||||
|
||||
重排序模型可以一定程度上提高最终召回结果的精度。
|
||||
|
||||
和嵌入模型的配置类似,打开服务提供商页面,点击新增服务提供商,选择重排序。有关重排序模型的更多信息请参考网络。
|
||||
|
||||
## 创建知识库
|
||||
|
||||
AstrBot 支持多知识库管理。在聊天时,您可以**自由指定知识库**。
|
||||
|
||||
进入知识库页面,点击创建知识库,如下图所示:
|
||||
|
||||

|
||||
|
||||
填写相关信息。在嵌入模型下拉菜单中您将看到刚刚创建好的嵌入模型和重排序模型(重排序模型可选)。
|
||||
|
||||
> [!TIP]
|
||||
> 一旦选择了一个知识库的嵌入模型,请不要再修改该提供商的**模型**或者**向量维度信息**,否则将**严重影响**该知识库的召回率甚至**报错**。
|
||||
|
||||
## 上传文件
|
||||
|
||||
|
||||
|
||||
## 附录 2:免费的嵌入模型申请
|
||||
|
||||
### PPIO 派欧云
|
||||
|
||||
1. 打开 [PPIO 派欧云官网](https://ppio.cn/user/register?invited_by=AIOONE),并注册账户(通过此链接注册的账户将会获得 15 元人民币的代金券)。
|
||||
2. 进入 [模型广场](https://ppio.cn/model-api/console),点击嵌入模型
|
||||
3. 点击 BAAI:BGE-M3 (截止至 2025-06-02,该模型在该平台免费)。
|
||||
4. 找到 API 接入指南,申请 Key。
|
||||
5. 填写 AstrBot OpenAI Embedding 模型提供商配置:
|
||||
1. API Key 为刚刚申请的 PPIO 的 API Key
|
||||
2. embedding api base 填写 `https://api.ppinfra.com/v3/openai`
|
||||
3. model 填写你选择的模型,此例子中为 `baai/bge-m3`。
|
||||
@@ -1,60 +0,0 @@
|
||||
# AstrBot 知识库
|
||||
|
||||
> [!TIP]
|
||||
> 需要 AstrBot 版本 >= 4.5.0。
|
||||
>
|
||||
> 我们在 4.5.0 版本中重新设计了全新的知识库系统,AstrBot 将原生支持知识库功能。下文介绍的是新版知识库的使用方法。如果您使用的是之前的版本,请参考[旧版知识库使用文档](https://docs.astrbot.app/zh/use/knowledge-base-old), 我们建议您升级到最新版以获得更好的体验。
|
||||
|
||||

|
||||
|
||||
## 配置嵌入模型
|
||||
|
||||
打开服务提供商页面,点击新增服务提供商,选择 Embedding。
|
||||
|
||||
目前 AstrBot 支持兼容 OpenAI API 和 Gemini API 的嵌入向量服务。
|
||||
|
||||
点击上面的提供商卡片进入配置页面,填写配置。
|
||||
|
||||
配置完成后,点击保存。
|
||||
|
||||
## 配置重排序模型(可选)
|
||||
|
||||
重排序模型可以一定程度上提高最终召回结果的精度。
|
||||
|
||||
和嵌入模型的配置类似,打开服务提供商页面,点击新增服务提供商,选择重排序。有关重排序模型的更多信息请参考网络。
|
||||
|
||||
## 创建知识库
|
||||
|
||||
AstrBot 支持多知识库管理。在聊天时,您可以**自由指定知识库**。
|
||||
|
||||
进入知识库页面,点击创建知识库,如下图所示:
|
||||
|
||||

|
||||
|
||||
填写相关信息。在嵌入模型下拉菜单中您将看到刚刚创建好的嵌入模型和重排序模型(重排序模型可选)。
|
||||
|
||||
> [!TIP]
|
||||
> 一旦选择了一个知识库的嵌入模型,请不要再修改该提供商的**模型**或者**向量维度信息**,否则将**严重影响**该知识库的召回率甚至**报错**。
|
||||
|
||||
## 上传文件
|
||||
|
||||
创建好知识库之后,可以为知识库上传文档。支持同时上传最多 10 个文件,单个文件大小不超过 128 MB。
|
||||
|
||||

|
||||
|
||||
## 使用知识库
|
||||
|
||||
在配置文件中,可以为不同的配置文件指定不同的知识库。
|
||||
|
||||
## 附录 2:高性价比的嵌入模型申请
|
||||
|
||||
### PPIO
|
||||
|
||||
1. 打开 [PPIO 派欧云官网](https://ppio.cn/user/register?invited_by=AIOONE),并注册账户(通过此链接注册的账户将会获得 15 元人民币的代金券)。
|
||||
2. 进入 [模型广场](https://ppio.cn/model-api/console),点击嵌入模型
|
||||
3. 点击 BAAI:BGE-M3 (截止至 2025-06-02,该模型在该平台免费)。
|
||||
4. 找到 API 接入指南,申请 Key。
|
||||
5. 填写 AstrBot OpenAI Embedding 模型提供商配置:
|
||||
1. API Key 为刚刚申请的 PPIO 的 API Key
|
||||
2. embedding api base 填写 `https://api.ppinfra.com/v3/openai`
|
||||
3. model 填写你选择的模型,此例子中为 `baai/bge-m3`。
|
||||
@@ -1,101 +0,0 @@
|
||||
# MCP
|
||||
|
||||
MCP(Model Context Protocol,模型上下文协议) 是一种新的开放标准协议,用来在大模型和数据源之间建立安全双向的链接。简单来说,它将函数工具单独抽离出来作为一个独立的服务,AstrBot 通过 MCP 协议远程调用函数工具,函数工具返回结果给 AstrBot。
|
||||
|
||||

|
||||
|
||||
AstrBot v3.5.0 支持 MCP 协议,可以添加多个 MCP 服务器、使用 MCP 服务器的函数工具。
|
||||
|
||||

|
||||
|
||||
## 初始状态配置
|
||||
|
||||
MCP 服务器一般使用 `uv` 或者 `npm` 来启动,因此您需要安装这两个工具。
|
||||
|
||||
对于 `uv`,您可以直接通过 pip 来安装。可在 AstrBot WebUI 快捷安装:
|
||||
|
||||

|
||||
|
||||
输入 `uv` 即可。
|
||||
|
||||
如果您使用 Docker 部署 AstrBot,也可以执行以下指令快捷安装。
|
||||
|
||||
```bash
|
||||
docker exec astrbot python -m pip install uv
|
||||
```
|
||||
|
||||
如果您通过源码部署 AstrBot,请在创建的虚拟环境内安装。
|
||||
|
||||
对于 `npm`,您需要安装 `node`。
|
||||
|
||||
如果您通过源码/一键安装部署 AstrBot,请参考 [Download Node.js](https://nodejs.org/en/download) 下载到您的本机。
|
||||
|
||||
如果您使用 Docker 部署 AstrBot,您需要在容器中安装 `node`(后期 AstrBot Docker 镜像将自带 `node`),请参考执行以下指令:
|
||||
|
||||
```bash
|
||||
sudo docker exec -it astrbot /bin/bash
|
||||
apt update && apt install curl -y
|
||||
export NVM_NODEJS_ORG_MIRROR=http://nodejs.org/dist
|
||||
# Download and install nvm:
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
|
||||
\. "$HOME/.nvm/nvm.sh"
|
||||
nvm install 22
|
||||
# Verify version:
|
||||
node -v
|
||||
nvm current
|
||||
npm -v
|
||||
npx -v
|
||||
```
|
||||
|
||||
安装好 `node` 之后,需要重启 `AstrBot` 以应用新的环境变量。
|
||||
|
||||
## 安装 MCP 服务器
|
||||
|
||||
如果您使用 Docker 部署 AstrBot,请将 MCP 服务器安装在 data 目录下。
|
||||
|
||||
### 一个例子
|
||||
|
||||
我想安装一个查询 Arxiv 上论文的 MCP 服务器,发现了这个 Repo: [arxiv-mcp-server](https://github.com/blazickjp/arxiv-mcp-server),参考它的 README,
|
||||
|
||||
我们抽取出需要的信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"tool",
|
||||
"run",
|
||||
"arxiv-mcp-server",
|
||||
"--storage-path", "data/arxiv"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
如果要使用的 MCP 服务器需要通过环境变量配置 Token 等信息,可以使用 `env` 这个工具:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "env",
|
||||
"args": [
|
||||
"XXX_RESOURCE_FROM=local",
|
||||
"XXX_API_URL=https://xxx.com",
|
||||
"XXX_API_TOKEN=sk-xxxxx",
|
||||
"uv",
|
||||
"tool",
|
||||
"run",
|
||||
"xxx-mcp-server",
|
||||
"--storage-path", "data/res"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
在 AstrBot WebUI 中设置:
|
||||
|
||||

|
||||
|
||||
即可。
|
||||
|
||||
参考链接:
|
||||
|
||||
1. 在这里了解如何使用 MCP: [Model Context Protocol](https://modelcontextprotocol.io/introduction)
|
||||
2. 在这里获取常用的 MCP 服务器: [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers/blob/main/README-zh.md#what-is-mcp), [Model Context Protocol servers](https://github.com/modelcontextprotocol/servers), [MCP.so](https://mcp.so)
|
||||
@@ -1,7 +0,0 @@
|
||||
# AstrBot Star
|
||||
|
||||
在 `3.4.0` 版本之后,AstrBot 将插件命名为 `Star`。AstrBot 是一个高度模块化的项目,通过插件可以发挥这种模块化的能力,实现各种功能。
|
||||
|
||||
使用 `/plugin` 可以看到所有插件。在管理面板中也可管理已经安装的插件。
|
||||
|
||||
如果想自己开发插件,详见 [几行代码实现一个插件](/dev/star/plugin)。
|
||||
@@ -1,53 +0,0 @@
|
||||
# 主动型能力
|
||||
|
||||
AstrBot 引入了主动 Agent(Proactive Agent)系统,使 AstrBot 不仅能被动响应用户,还能通过给自己下达未来的任务来在未来的指定时刻主动执行任务并向用户主动反馈结果(文本、图片、文件都可)。
|
||||
|
||||

|
||||
|
||||
在 v4.14.0 引入,目前是**实验性功能**,未稳定。
|
||||
|
||||
## 未来任务 (FutureTask)
|
||||
|
||||
主 Agent 现在可以管理一个全局的 **Cron Job 列表**,为未来的自己设置任务。
|
||||
|
||||
### 功能特点
|
||||
|
||||
- **自我唤醒**:AstrBot 会在预定时间自动唤醒并执行任务。
|
||||
- **任务反馈**:执行完成后,AstrBot 会将结果告知任务布置方。
|
||||
- **WebUI 管理**:你可以在 WebUI 的“定时任务”页面查看、编辑或删除已设置的任务。
|
||||
|
||||
### 如何使用
|
||||
|
||||
> [!TIP]
|
||||
> 首先,确保配置中 “主动型能力” 已启用。
|
||||
|
||||
主 Agent 拥有管理定时任务的能力。你可以直接对它说:
|
||||
- “明天早上 8 点提醒我开会”
|
||||
- “每周五下午 5 点总结本周的工作日志”
|
||||
- “帮我定一个 10 分钟后的闹钟”
|
||||
|
||||
主 Agent 会调用内置的定时任务工具来安排这些计划。
|
||||
|
||||
你可以在 AstrBot WebUI 左侧导航栏中点击 **未来任务** 来查看和管理所有未来任务。
|
||||
|
||||

|
||||
|
||||
### 支持的平台
|
||||
|
||||
“定时任务”的设置支持所有平台,然而,由于部分平台没有开放主动消息推送的 API,因此只有以下平台支持 AstrBot 主动向用户推送结果:
|
||||
|
||||
- Telegram
|
||||
- OneBot v11
|
||||
- Slack
|
||||
- 飞书 (Lark)
|
||||
- Discord
|
||||
- Misskey
|
||||
- Satori
|
||||
|
||||
## 多媒体消息的发送
|
||||
|
||||
为了方便 Agent 直接向用户发送图片、音频、视频等文件,AstrBot 默认提供了一个 `send_message_to_user` 工具。
|
||||
|
||||
### 功能特点
|
||||
- **直接发送**:Agent 可以直接将生成或获取的多媒体文件发送给用户,而无需通过复杂的文本转换。
|
||||
- **支持多种格式**:支持图片、文件、音频、视频等。
|
||||
@@ -1,38 +0,0 @@
|
||||
# Anthropic Skills
|
||||
|
||||
Anthropic 推出的 Agent Skills(智能体技能)是一套模块化的功能扩展标准,旨在将 Claude 从一个“通用聊天机器人”转变为具备特定领域专业知识的“任务执行者”。Skills 是包含指令、脚本、元数据和参考资源的结构化文件夹。它不仅仅是提示词(Prompt),更像是一本专门的“操作手册”,在 Agent 需要执行特定任务时才会动态加载。Tool 是模型用来与外部世界交互的“具体工具/函数接口”,而 Skill 是将指令、模板和工具组合在一起的“标准化任务执行手册”。传统 Tool 需要在对话开始时一次性将所有 API 定义填入 Prompt。如果工具超过 50 个,可能还没开始说话就消耗了数万个 Token,导致响应变慢且昂贵。
|
||||
|
||||
AstrBot 在 v4.13.0 之后引入了对 Anthropic Skills 的支持,使得用户可以轻松集成和使用各种预定义的技能模块,提升 Agent 在特定任务上的表现。
|
||||
|
||||
## 关键特性
|
||||
|
||||
- 按需加载 (Progressive Disclosure):模型初始只加载技能名称和简短描述。只有当任务匹配时,才会加载详细的 SKILL.md 指令,从而节省上下文窗口并降低成本。
|
||||
- 高度可复用:技能可以在不同的 Claude API 项目、Claude Code 或 Claude.ai 中通用。
|
||||
- 执行能力:技能可以包含可执行代码脚本,配合 Anthropic 代码执行环境(Code Execution)直接生成或处理文件。
|
||||
|
||||
## 上传 Skills 到 AstrBot
|
||||
|
||||
进入 AstrBot 管理面板,导航到 `插件` 页面,找到 `Skills`。
|
||||
|
||||

|
||||
|
||||
你可以上传 Skills,上传格式要求如下:
|
||||
|
||||
1. 是一个 .zip 压缩包
|
||||
2. **解压后是一个 Skill 文件夹,Skill 文件夹的名字即为这个 Skill 在 AstrBot 中的标识,请用英文命名**。
|
||||
3. Skill 文件夹内必须包含一个名为 `SKILL.md` 的文件,且该文件内容最好符合 Anthropic Skills 规范。你可以参考 [Anthropic 技能](https://code.claude.com/docs/zh-CN/skills)
|
||||
|
||||
## 在 AstrBot 使用 Skills
|
||||
|
||||
Skills 提供了 Agent 操作说明书,并且内容通常包含 Python 代码段、脚本等可执行内容。因此,Agent 需要一个**执行环境**。
|
||||
|
||||
目前,AstrBot 提供两种执行环境:
|
||||
|
||||
- Local(Agent 将在你的 AstrBot 运行环境中运行。**请谨慎使用,因为这会允许 Agent 在你的环境执行任意代码,可能带来安全风险**)
|
||||
- Sandbox (Agent 在隔离化的沙盒环境中运行。**需要先启动 AstrBot 沙盒模式**,请参考:[沙盒模式](/use/astrbot-agent-sandbox),如果这个模式下不启动沙盒模式,将不会将 Skills 传给 Agent)
|
||||
|
||||
你可以在 `配置` 页面 - 使用电脑能力 中选择默认的执行环境。
|
||||
|
||||
> [!NOTE]
|
||||
> 需要说明的是,如果您使用 Local 作为执行环境,AstrBot 目前仅允许 **AstrBot 管理员**请求时才真正让 Agent 操作你的本地环境,普通用户将会被禁止,Agent 将无法通过 Shell、Python 等 Tool 在本地环境执行代码,会收到相应的权限限制提示,如 `Sorry, I cannot execute code on your local environment due to permission restrictions.`。
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# Agent Handsoff 与 Subagent
|
||||
|
||||
SubAgent 编排是 AstrBot 提供的一种高级 Agent 组织方式。它允许你将复杂的任务分解给多个专门的子 Agent(SubAgent)来完成,从而降低主 Agent 的 Prompt 长度,提高任务执行的成功率。
|
||||
|
||||
在 v4.14.0 引入,目前是**实验性功能**,未稳定。
|
||||
|
||||

|
||||
|
||||
## 动机
|
||||
|
||||
在传统的架构中,所有的工具(Tools)都直接挂载在主 Agent 上。当工具数量较多时,会带来以下问题:
|
||||
1. **Prompt 爆炸**:主 Agent 需要在 System Prompt 中包含所有工具的描述,导致上下文占用过多。
|
||||
2. **调用失误**:面对大量工具,LLM 容易混淆工具用途或产生错误的调用参数。
|
||||
3. **逻辑复杂**:主 Agent 既要负责对话,又要负责组织和调用大量工具,负担过重。
|
||||
|
||||
通过 SubAgent 编排,主 Agent 仅负责与用户对话以及**任务委派**。具体的工具调用由专门的 SubAgent 负责。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **主 Agent 委派**:开启 SubAgent 模式后,主 Agent 只能看到一系列名为 `transfer_to_<subagent_name>` 的委派工具。
|
||||
2. **任务移交**:当主 Agent 认为需要执行某项任务时,它会调用对应的委派工具,将任务描述传递给 SubAgent。
|
||||
3. **子 Agent 执行**:SubAgent 接收到任务后,使用其挂载的工具进行操作,并将结果整理后回传给主 Agent。
|
||||
4. **结果反馈**:主 Agent 收到 SubAgent 的执行结果,继续与用户对话。
|
||||
|
||||

|
||||
|
||||
## 配置方法
|
||||
|
||||
在 AstrBot WebUI 中,点击左侧导航栏的 **SubAgent 编排**。
|
||||
|
||||
### 1. 启用 SubAgent 模式
|
||||
|
||||
在页面顶部开启“启用 SubAgent 编排”。
|
||||
|
||||
### 2. 创建 SubAgent
|
||||
|
||||
点击“新增 SubAgent”按钮:
|
||||
|
||||
- **Agent 名称**:用于生成委派工具名(如 `transfer_to_weather`)。建议使用英文小写和下划线。
|
||||
- **选择 Persona**:选择一个预设的 Persona,即人格,作为该子 Agent 的基础性格、行为指导和可以使用的 Tools 集合。你可以在“人格设定”页面创建和管理 Persona。
|
||||
- **对主 LLM 的描述**:这段描述会告诉主 Agent 这个子 Agent 擅长做什么,以便主 Agent 准确委派。
|
||||
- **分配工具**:选择该子 Agent 可以调用的工具。
|
||||
- **Provider 覆盖(可选)**:你可以为特定的子 Agent 指定不同的模型提供商。例如,主 Agent 使用 GPT-4o,而负责简单查询的子 Agent 使用 GPT-4o-mini 以节省成本。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
- **职责单一**:每个 SubAgent 应该只负责一类相关的任务(如:搜索、文件处理、智能家居控制)。
|
||||
- **清晰的描述**:给主 Agent 的描述应当简洁明了,突出该子 Agent 的核心能力。
|
||||
- **分层管理**:对于极其复杂的任务,可以考虑多级委派(如果需要)。
|
||||
|
||||
## 已知问题
|
||||
|
||||
SubAgent 系统目前是**实验性功能**,未稳定。
|
||||
|
||||
1. 目前无法隔离人格的 Skills。
|
||||
2. 子 Agent 的对话历史暂时不会被保存。
|
||||
@@ -1,32 +0,0 @@
|
||||
# 统一 Webhook 模式
|
||||
|
||||
在 v4.8.0 版本开始,AstrBot 支持统一 Webhook 模式 (unified_webhook_mode)。开启该模式后,所有支持该模式的平台适配器都将使用同一个 Webhook 回调接口,从而简化了反向代理和域名配置,不再需要给每一个机器人适配器单独配置端口、域名和反向代理。
|
||||
|
||||
支持统一 Webhook 模式的平台适配器包括:
|
||||
|
||||
- Slack Webhook 模式
|
||||
- 微信公众平台
|
||||
- 企业微信客服机器人
|
||||
- 企业微信智能机器人
|
||||
- 微信客服机器人
|
||||
- QQ 官方机器人 Webhook 模式
|
||||
- ...
|
||||
|
||||
## 如何使用统一 Webhook 模式
|
||||
|
||||
1. 拥有一个域名(如 example.com)和公网 IP 服务器
|
||||
2. 配置 DNS 解析(如 astrbot.example.com)
|
||||
3. 配置反向代理,将域名的 80 或 443 端口请求转发到 AstrBot 的 WebUI 端口(默认为 6185)
|
||||
4. 前往 AstrBot `配置文件` 页,点击 `系统`,将 `对外可达的回调接口地址` 为配置的 URL 地址。(如 https://astrbot.example.com),点击保存,等待重启。
|
||||
|
||||
|
||||
在之后配置各个平台适配器时,选择开启 `统一 Webhook 模式 (unified_webhook_mode)`。
|
||||
|
||||
> [!TIP]
|
||||
> 如果您正在尝试更新 v4.8.0 之前配置的机器人适配器,你可能无法看到 `统一 Webhook 模式 (unified_webhook_mode)` 选项。请重新创建一个新的适配器实例,即可看到该选项。
|
||||
|
||||

|
||||
|
||||
开启该模式后,AstrBot 会为你生成一个唯一的 Webhook 回调链接,你只需要将该链接填写到各个平台的回调地址处即可。
|
||||
|
||||

|
||||
@@ -1,34 +0,0 @@
|
||||
# 网页搜索
|
||||
|
||||
网页搜索功能旨在提供大模型调用 Google,Bing,搜狗等搜索引擎以获取世界最近信息的能力,一定程度上能够提高大模型的回复准确度,减少幻觉。
|
||||
|
||||
AstrBot 内置的网页搜索功能依赖大模型提供 `函数调用` 能力。如果你不了解函数调用,请参考:[函数调用](/use/websearch)。
|
||||
|
||||
在使用支持函数调用的大模型且开启了网页搜索功能的情况下,您可以试着说:
|
||||
|
||||
- `帮我搜索一下 xxx`
|
||||
- `帮我总结一下这个链接:https://soulter.top`
|
||||
- `查一下 xxx`
|
||||
- `最近 xxxx`
|
||||
|
||||
等等带有搜索意味的提示让大模型触发调用搜索工具。
|
||||
|
||||
AstrBot 支持 3 种网页搜索源接入方式:`默认`、`Tavily`、`百度 AI 搜索`。
|
||||
|
||||
前者使用 AstrBot 内置的网页搜索请求器请求 Google、Bing、搜狗搜索引擎,在能够使用 Google 的网络环境下表现最佳。**我们推荐使用 Tavily**。
|
||||
|
||||

|
||||
|
||||
进入 `配置`,下拉找到网页搜索,您可选择 `default`(默认,不推荐) 或 `Tavily`。
|
||||
|
||||
### default(不推荐)
|
||||
|
||||
如果您的设备在国内并且有代理,可以开启代理并在 `管理面板-其他配置-HTTP代理` 填入 HTTP 代理地址以应用代理。
|
||||
|
||||
### Tavily
|
||||
|
||||
前往 [Tavily](https://app.tavily.com/home) 得到 API Key,然后填写在相应的配置项。
|
||||
|
||||
如果您使用 Tavily 作为网页搜索源,在 AstrBot ChatUI 上将会获得更好的体验优化,包括引用来源展示等:
|
||||
|
||||

|
||||
@@ -1,79 +0,0 @@
|
||||
# 管理面板
|
||||
|
||||
AstrBot 管理面板具有管理插件、查看日志、可视化配置、查看统计信息等功能。
|
||||
|
||||

|
||||
|
||||
## 管理面板的访问
|
||||
|
||||
当启动 AstrBot 之后,你可以通过浏览器访问 `http://localhost:6185` 来访问管理面板。
|
||||
|
||||
> [!TIP]
|
||||
> - 如果你正在云服务器上部署 AstrBot,需要将 `localhost` 替换为你的服务器 IP 地址。
|
||||
|
||||
## 登录
|
||||
|
||||
默认用户名和密码是 `astrbot` 和 `astrbot`。
|
||||
|
||||
## 可视化配置
|
||||
|
||||
在管理面板中,你可以通过可视化配置来配置 AstrBot 的插件。点击左栏 `配置` 即可进入配置页面。
|
||||
|
||||

|
||||
|
||||
当修改完配置后,你需要点击右下角 `保存` 按钮才能成功保存配置。
|
||||
|
||||
使用右下角第一个圆形按钮可以切换至 `代码编辑配置`。在 `代码编辑配置` 中,你可以直接编辑配置文件。
|
||||
|
||||
编辑完后首先点击`应用此配置`,此时配置将应用到可视化配置中,然后再点击右下角`保存`按钮来保存配置。如果你不点击`应用此配置`,那么你的修改将不会生效。
|
||||
|
||||

|
||||
|
||||
## 插件
|
||||
|
||||
在管理面板中,你可以通过左栏的 `插件` 来查看已安装的插件,以及安装新插件。
|
||||
|
||||
点击插件市场标签栏,你可以浏览由 AstrBot 官方上架的插件。
|
||||
|
||||

|
||||
|
||||
你也可以点击右下角 + 按钮,以 URL / 文件上传的方式手动安装插件。
|
||||
|
||||
> 由于插件更新机制,AstrBot Team 无法完全保证插件市场中插件的安全性,请您仔细甄别。因为插件原因造成损失的,AstrBot Team 不予负责。
|
||||
|
||||
### 插件加载失败处理
|
||||
|
||||
如果插件加载失败,管理面板会显示错误信息,并提供 **“尝试一键重载修复”** 按钮。这允许你在修复环境(如安装缺失依赖)或修改代码后,无需重启整个程序即可快速重新加载插件。
|
||||
|
||||
## 指令管理
|
||||
|
||||
通过左侧菜单 `指令管理`,可以集中管理所有已注册的指令,默认不显示系统插件。
|
||||
|
||||
支持按插件、类型(指令 / 指令组 / 子指令)、权限与状态过滤,配合搜索框快速定位。指令组行可展开查看子指令,徽章显示子指令数量,子指令行会缩进区分层级。
|
||||
|
||||
可以对每个指令 启用/禁用、重命名。
|
||||
|
||||
## 追踪 (Trace)
|
||||
|
||||
在管理面板的 `Trace` 页面中,你可以实时查看 AstrBot 的运行追踪记录。这对于调试模型调用路径、工具调用过程等非常有用。
|
||||
|
||||
你可以通过页面顶部的开关来启用或禁用追踪记录。
|
||||
|
||||
> [!NOTE]
|
||||
> 当前仅记录部分 AstrBot 主 Agent 的模型调用路径,后续会不断完善。
|
||||
|
||||
## 更新管理面板
|
||||
|
||||
在 AstrBot 启动时,会自动检查管理面板是否需要更新,如果需要,第一条日志(黄色)会进行提示。
|
||||
|
||||
使用 `/dashboard_update` 命令可以手动更新管理面板(管理员指令)。
|
||||
|
||||
管理面板文件在 data/dist 目录下。如果需要手动替换,请在 https://github.com/AstrBotDevs/AstrBot/releases/ 下载 `dist.zip` 然后解压到 data 目录下。
|
||||
|
||||
## 自定义 WebUI 端口
|
||||
|
||||
修改 data/cmd_config.json 文件内 `dashboard` 配置中的 `port`。
|
||||
|
||||
## 忘记密码
|
||||
|
||||
修改 data/cmd_config.json 文件内 `dashboard` 配置中的 `password`,将 password 整个键值对删除。
|
||||
55
refactor.md
55
refactor.md
@@ -1,55 +0,0 @@
|
||||
# AstrBot SDK v4 重构设计(历史说明)
|
||||
|
||||
本文档保留最初的 v4 重构意图与设计取舍,**不再作为当前实现文档**。
|
||||
当前代码、兼容面、能力集合、目录结构与版本语义,请以 [ARCHITECTURE.md](D:/GitObjectsOwn/astrbot-sdk/ARCHITECTURE.md) 为准。
|
||||
|
||||
## 1. 这份文档现在的用途
|
||||
|
||||
- 记录最初为什么要做 v4 分层与协议化重构
|
||||
- 保留当时的重要设计原则,供后续判断“方向有没有跑偏”
|
||||
- 帮助阅读历史提交和旧讨论
|
||||
|
||||
它**不再负责**描述当前仓库现状。
|
||||
|
||||
## 2. 仍然有效的核心原则
|
||||
|
||||
以下原则仍然是当前实现的主线:
|
||||
|
||||
- 协议优先:插件与宿主通过显式协议消息交互
|
||||
- 统一 `id`:所有请求/响应使用单一关联字段
|
||||
- `handler.invoke`:handler 回调不引入额外消息类型
|
||||
- `event` 只服务于 `stream=true`
|
||||
- runtime 根导出保持窄接口
|
||||
- legacy 适配与原生 v4 协议模型分开管理
|
||||
|
||||
## 3. 已经演化的地方
|
||||
|
||||
最初方案中的下列假设,当前已经不再成立或只部分成立:
|
||||
|
||||
- `compat.py` 不是当前 compat 的全部实现,compat 已演化为长期维护子系统
|
||||
- runtime 不能完全“感知不到 compat”,但 compat 执行细节应继续收口到 `_legacy_runtime.py`
|
||||
- 环境管理不再只是“每插件一个独立 venv”,现在有 `runtime.environment_groups` 做共享环境规划
|
||||
- capability 集合已经扩展,当前不止早期文档中的那一组
|
||||
- 旧包名兼容不再只有 `astrbot_sdk.api.*`,还包括受控的 `src-new/astrbot` facade
|
||||
|
||||
## 4. 当前维护约定
|
||||
|
||||
如果你要修改实现,请按下面的顺序看文档:
|
||||
|
||||
1. 先看 `ARCHITECTURE.md`
|
||||
2. 再看相关代码和 `tests_v4`
|
||||
3. 最后把本文档当作历史背景材料
|
||||
|
||||
如果 `ARCHITECTURE.md` 与本文档冲突:
|
||||
|
||||
- 以 `ARCHITECTURE.md` 为准
|
||||
- 若仍有歧义,以代码和测试为准
|
||||
|
||||
## 5. 对后续重构的约束
|
||||
|
||||
后续清理实现时,应继续坚持:
|
||||
|
||||
- 不破坏旧插件现有兼容面
|
||||
- 不把 legacy 逻辑重新扩散进 runtime 主干
|
||||
- 不把 `src-new/astrbot` 扩张成旧应用整棵树
|
||||
- 不让文档再次脱离代码与测试
|
||||
BIN
src-new.rar
BIN
src-new.rar
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
# test_plugin/new
|
||||
|
||||
这个目录是运行时与集成测试夹具,不是给插件作者直接照抄的入门模板。
|
||||
|
||||
它的目标是覆盖更多 SDK surface,例如:
|
||||
|
||||
- 生命周期
|
||||
- LLM / DB / Memory / Platform / HTTP / Metadata client
|
||||
- 自定义 capability
|
||||
- schedule / event / message / command handler
|
||||
|
||||
如果你是在找最小可学习示例,请改看:
|
||||
|
||||
- `examples/hello_plugin/`
|
||||
@@ -1 +0,0 @@
|
||||
"""V4 sample plugin commands package."""
|
||||
@@ -1,438 +0,0 @@
|
||||
"""V4 sample plugin used by integration tests.
|
||||
|
||||
This fixture exercises the full public v4 API surface:
|
||||
|
||||
Decorators:
|
||||
- on_command: command handling with aliases and description
|
||||
- on_message: message handling with regex, keywords, platforms
|
||||
- on_event: event subscription
|
||||
- on_schedule: scheduled tasks
|
||||
- require_admin: permission control
|
||||
- provide_capability: custom capabilities (normal + stream)
|
||||
|
||||
Context clients:
|
||||
- ctx.llm: LLM client (chat, chat_raw, stream_chat, with images)
|
||||
- ctx.memory: Memory client (save, get, delete, search)
|
||||
- ctx.db: DB client (get, set, delete, list, get_many, set_many, watch)
|
||||
- ctx.platform: Platform client (send, send_image, send_chain, get_members)
|
||||
- ctx.http: HTTP client (register_api, unregister_api, list_apis)
|
||||
- ctx.metadata: Metadata client (get_plugin, list_plugins, get_plugin_config)
|
||||
|
||||
MessageEvent:
|
||||
- reply(), plain_result(), target, to_payload()
|
||||
- user_id, group_id, session_id, platform, text
|
||||
|
||||
Star lifecycle:
|
||||
- on_start, on_stop, on_error
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from astrbot_sdk import (
|
||||
Context,
|
||||
MessageEvent,
|
||||
Star,
|
||||
on_command,
|
||||
on_event,
|
||||
on_message,
|
||||
on_schedule,
|
||||
provide_capability,
|
||||
require_admin,
|
||||
)
|
||||
from astrbot_sdk.context import CancelToken
|
||||
|
||||
|
||||
class EchoCapabilityInput(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class EchoCapabilityOutput(BaseModel):
|
||||
echo: str
|
||||
plugin_id: str
|
||||
|
||||
|
||||
class StreamCapabilityInput(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class StreamCapabilityOutput(BaseModel):
|
||||
items: list[dict[str, object]]
|
||||
|
||||
|
||||
class HttpHandlerInput(BaseModel):
|
||||
method: str = "GET"
|
||||
body: dict[str, object] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class HttpHandlerOutput(BaseModel):
|
||||
status: int
|
||||
body: dict[str, object]
|
||||
|
||||
|
||||
class HelloPlugin(Star):
|
||||
"""Representative v4 plugin fixture covering all SDK capabilities."""
|
||||
|
||||
# ============================================================
|
||||
# Lifecycle hooks
|
||||
# ============================================================
|
||||
|
||||
async def on_start(self, ctx: Context) -> None:
|
||||
"""Called when the plugin starts."""
|
||||
ctx.logger.info("HelloPlugin starting up")
|
||||
# Store startup timestamp
|
||||
await ctx.db.set("demo:started", {"status": "ok"})
|
||||
|
||||
async def on_stop(self, ctx: Context) -> None:
|
||||
"""Called when the plugin stops."""
|
||||
ctx.logger.info("HelloPlugin shutting down")
|
||||
# Cleanup
|
||||
await ctx.db.delete("demo:started")
|
||||
|
||||
# ============================================================
|
||||
# Command handlers
|
||||
# ============================================================
|
||||
|
||||
@on_command("hello", aliases=["hi"], description="发送问候消息")
|
||||
async def hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Basic command with LLM response."""
|
||||
reply = await ctx.llm.chat(event.text)
|
||||
await event.reply(reply)
|
||||
|
||||
@on_command("raw", description="调用 llm.chat_raw 并返回结构化信息")
|
||||
async def raw(self, event: MessageEvent, ctx: Context):
|
||||
"""LLM chat with full response metadata."""
|
||||
response = await ctx.llm.chat_raw(
|
||||
event.text,
|
||||
system="be concise",
|
||||
history=[{"role": "user", "content": "history"}],
|
||||
)
|
||||
return event.plain_result(
|
||||
f"raw={response.text}|finish={response.finish_reason}|"
|
||||
f"usage={response.usage}"
|
||||
)
|
||||
|
||||
@on_command("stream", description="流式 LLM 调用")
|
||||
async def stream(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Streaming LLM response."""
|
||||
chunks: list[str] = []
|
||||
async for chunk in ctx.llm.stream_chat(event.text or "stream"):
|
||||
chunks.append(chunk)
|
||||
# Real-time feedback
|
||||
await event.reply(f"[streaming...] {chunk}")
|
||||
await event.reply(f"[完成] {''.join(chunks)}")
|
||||
|
||||
@on_command("vision", description="带图片的 LLM 调用")
|
||||
async def vision(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""LLM with image input."""
|
||||
# Extract image URL from message or use default
|
||||
image_url = "https://example.com/demo.png"
|
||||
response = await ctx.llm.chat(
|
||||
event.text or "描述这张图片",
|
||||
image_urls=[image_url],
|
||||
)
|
||||
await event.reply(response)
|
||||
|
||||
# ============================================================
|
||||
# Memory operations
|
||||
# ============================================================
|
||||
|
||||
@on_command("remember", description="记忆操作演示")
|
||||
async def remember(self, event: MessageEvent, ctx: Context):
|
||||
"""Memory client full API demo."""
|
||||
# Save with metadata
|
||||
await ctx.memory.save(
|
||||
"demo:last_message",
|
||||
{"user_id": event.user_id or "", "text": event.text},
|
||||
source="fixture",
|
||||
tags=["demo"],
|
||||
)
|
||||
|
||||
# Get exact match
|
||||
remembered = await ctx.memory.get("demo:last_message") or {}
|
||||
|
||||
# Semantic search
|
||||
searched = await ctx.memory.search("demo")
|
||||
|
||||
# Delete
|
||||
await ctx.memory.delete("demo:last_message")
|
||||
|
||||
return event.plain_result(
|
||||
f"remembered={remembered.get('user_id', 'unknown')}|"
|
||||
f"searched={len(searched)}"
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Database operations
|
||||
# ============================================================
|
||||
|
||||
@on_command("db", description="数据库操作演示")
|
||||
async def db_ops(self, event: MessageEvent, ctx: Context):
|
||||
"""DB client full API demo."""
|
||||
# Basic operations
|
||||
await ctx.db.set("demo:key1", {"value": "data1"})
|
||||
await ctx.db.set("demo:key2", {"value": "data2"})
|
||||
await ctx.db.set("demo:key3", {"value": "data3"})
|
||||
|
||||
value1 = await ctx.db.get("demo:key1")
|
||||
|
||||
# List keys with prefix
|
||||
keys = await ctx.db.list("demo:")
|
||||
|
||||
# Batch operations
|
||||
values = await ctx.db.get_many(["demo:key1", "demo:key2"])
|
||||
await ctx.db.set_many(
|
||||
{
|
||||
"demo:batch1": {"batch": True},
|
||||
"demo:batch2": {"batch": True},
|
||||
}
|
||||
)
|
||||
|
||||
# Cleanup
|
||||
for key in [
|
||||
"demo:key1",
|
||||
"demo:key2",
|
||||
"demo:key3",
|
||||
"demo:batch1",
|
||||
"demo:batch2",
|
||||
]:
|
||||
await ctx.db.delete(key)
|
||||
|
||||
return event.plain_result(
|
||||
f"value1={value1}|keys={len(keys)}|batch_get={len(values)}"
|
||||
)
|
||||
|
||||
@on_command("watch", description="监听数据库变更")
|
||||
async def watch_db(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Watch for DB changes (demonstration)."""
|
||||
await event.reply("开始监听 demo: 前缀的变更 (5秒)...")
|
||||
|
||||
async def watcher():
|
||||
count = 0
|
||||
async for change in ctx.db.watch("demo:"):
|
||||
count += 1
|
||||
await event.reply(f"变更: {change['op']} {change['key']}")
|
||||
if count >= 3:
|
||||
break
|
||||
|
||||
# Run watcher with timeout
|
||||
try:
|
||||
await asyncio.wait_for(watcher(), timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
await event.reply("监听超时结束")
|
||||
|
||||
# ============================================================
|
||||
# Platform operations
|
||||
# ============================================================
|
||||
|
||||
@on_command("platforms", description="平台操作演示")
|
||||
async def platforms(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Platform client full API demo."""
|
||||
target = event.target or event.session_id
|
||||
|
||||
# Get group members
|
||||
members = await ctx.platform.get_members(target)
|
||||
|
||||
# Send text
|
||||
await ctx.platform.send(target, f"成员数: {len(members)}")
|
||||
|
||||
# Send image back to the current conversation
|
||||
await event.reply_image("https://example.com/demo.png")
|
||||
|
||||
# Send message chain back to the current conversation
|
||||
await event.reply_chain(
|
||||
[
|
||||
{"type": "Plain", "text": "消息链 "},
|
||||
{"type": "Image", "file": "https://example.com/demo.png"},
|
||||
],
|
||||
)
|
||||
|
||||
@on_command("announce", description="发送富消息链")
|
||||
async def announce(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Send rich message chain."""
|
||||
await event.reply_chain(
|
||||
[
|
||||
{"type": "Plain", "text": "公告: "},
|
||||
{"type": "Plain", "text": event.text or "无内容"},
|
||||
],
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# HTTP API operations
|
||||
# ============================================================
|
||||
|
||||
@on_command("register_api", description="注册 HTTP API")
|
||||
async def register_http_api(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Register a custom HTTP API endpoint."""
|
||||
await ctx.http.register_api(
|
||||
route="/demo/api",
|
||||
handler=self.http_handler_capability,
|
||||
methods=["GET", "POST"],
|
||||
description="Demo HTTP API",
|
||||
)
|
||||
apis = await ctx.http.list_apis()
|
||||
return event.plain_result(f"已注册 API,当前共 {len(apis)} 个")
|
||||
|
||||
@on_command("unregister_api", description="注销 HTTP API")
|
||||
async def unregister_http_api(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Unregister the HTTP API endpoint."""
|
||||
await ctx.http.unregister_api("/demo/api")
|
||||
return event.plain_result("已注销 API")
|
||||
|
||||
# ============================================================
|
||||
# Metadata operations
|
||||
# ============================================================
|
||||
|
||||
@on_command("plugins", description="列出所有插件")
|
||||
async def list_plugins(self, event: MessageEvent, ctx: Context):
|
||||
"""List all loaded plugins."""
|
||||
plugins = await ctx.metadata.list_plugins()
|
||||
names = [p.name for p in plugins]
|
||||
return event.plain_result(f"插件: {', '.join(names)}")
|
||||
|
||||
@on_command("plugin_info", description="获取插件信息")
|
||||
async def plugin_info(self, event: MessageEvent, ctx: Context):
|
||||
"""Get current plugin metadata."""
|
||||
me = await ctx.metadata.get_current_plugin()
|
||||
if me:
|
||||
return event.plain_result(
|
||||
f"name={me.name}|version={me.version}|author={me.author}"
|
||||
)
|
||||
return event.plain_result("无法获取插件信息")
|
||||
|
||||
@on_command("config", description="获取插件配置")
|
||||
async def get_config(self, event: MessageEvent, ctx: Context):
|
||||
"""Get plugin configuration."""
|
||||
config = await ctx.metadata.get_plugin_config()
|
||||
if config:
|
||||
return event.plain_result(f"config={config}")
|
||||
return event.plain_result("无配置")
|
||||
|
||||
# ============================================================
|
||||
# Permission control
|
||||
# ============================================================
|
||||
|
||||
@require_admin
|
||||
@on_command("secure", description="管理员专用命令")
|
||||
async def secure(self, event: MessageEvent):
|
||||
"""Admin-only command."""
|
||||
return event.plain_result(f"secure:{event.user_id or 'unknown'}")
|
||||
|
||||
# ============================================================
|
||||
# Message handlers
|
||||
# ============================================================
|
||||
|
||||
@on_message(regex=r"^ping$", keywords=["ping"], platforms=["test"])
|
||||
async def ping(self, event: MessageEvent):
|
||||
"""Regex and keyword matching."""
|
||||
return event.plain_result("pong")
|
||||
|
||||
@on_message(keywords=["hello"])
|
||||
async def on_hello(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Keyword-based message handler."""
|
||||
await event.reply("检测到 hello 关键词!")
|
||||
|
||||
# ============================================================
|
||||
# Event handlers
|
||||
# ============================================================
|
||||
|
||||
@on_event("group_join")
|
||||
async def on_group_join(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Handle group join events."""
|
||||
ctx.logger.info("用户加入群组: {}", event.user_id)
|
||||
await ctx.platform.send(event.session_id, f"欢迎 {event.user_id} 加入群组!")
|
||||
|
||||
@on_event("group_leave")
|
||||
async def on_group_leave(self, event: MessageEvent, ctx: Context) -> None:
|
||||
"""Handle group leave events."""
|
||||
ctx.logger.info("用户离开群组: {}", event.user_id)
|
||||
|
||||
# ============================================================
|
||||
# Scheduled tasks
|
||||
# ============================================================
|
||||
|
||||
@on_schedule(interval_seconds=3600)
|
||||
async def hourly_heartbeat(self, ctx: Context) -> None:
|
||||
"""Hourly scheduled task."""
|
||||
await ctx.db.set("demo:last_heartbeat", {"time": "hourly"})
|
||||
ctx.logger.info("执行每小时心跳")
|
||||
|
||||
@on_schedule(cron="0 9 * * *")
|
||||
async def morning_greeting(self, ctx: Context) -> None:
|
||||
"""Cron-based scheduled task (9 AM daily)."""
|
||||
ctx.logger.info("早安问候任务触发")
|
||||
|
||||
# ============================================================
|
||||
# Custom capabilities
|
||||
# ============================================================
|
||||
|
||||
@provide_capability(
|
||||
"demo.echo",
|
||||
description="回显输入文本",
|
||||
input_model=EchoCapabilityInput,
|
||||
output_model=EchoCapabilityOutput,
|
||||
)
|
||||
async def echo_capability(
|
||||
self,
|
||||
payload: dict[str, object],
|
||||
ctx: Context,
|
||||
cancel_token: CancelToken,
|
||||
) -> dict[str, str]:
|
||||
"""Simple echo capability."""
|
||||
cancel_token.raise_if_cancelled()
|
||||
text = str(payload.get("text", ""))
|
||||
await ctx.db.set("demo:capability_echo", {"text": text})
|
||||
return {
|
||||
"echo": text,
|
||||
"plugin_id": ctx.plugin_id,
|
||||
}
|
||||
|
||||
@provide_capability(
|
||||
"demo.stream",
|
||||
description="流式回显输入文本",
|
||||
input_model=StreamCapabilityInput,
|
||||
output_model=StreamCapabilityOutput,
|
||||
supports_stream=True,
|
||||
cancelable=True,
|
||||
)
|
||||
async def stream_capability(
|
||||
self,
|
||||
payload: dict[str, object],
|
||||
ctx: Context,
|
||||
cancel_token: CancelToken,
|
||||
):
|
||||
"""Streaming echo capability."""
|
||||
text = str(payload.get("text", ""))
|
||||
await ctx.db.set("demo:last_stream", {"text": text})
|
||||
for char in text:
|
||||
cancel_token.raise_if_cancelled()
|
||||
await asyncio.sleep(0)
|
||||
yield {"text": char}
|
||||
|
||||
@provide_capability(
|
||||
"demo.http_handler",
|
||||
description="处理 /demo/api HTTP 请求",
|
||||
input_model=HttpHandlerInput,
|
||||
output_model=HttpHandlerOutput,
|
||||
)
|
||||
async def http_handler_capability(
|
||||
self,
|
||||
payload: dict[str, object],
|
||||
ctx: Context,
|
||||
cancel_token: CancelToken,
|
||||
) -> dict[str, object]:
|
||||
"""Handle HTTP API requests."""
|
||||
method = payload.get("method", "GET")
|
||||
body = payload.get("body", {})
|
||||
ctx.logger.info(f"HTTP {method} request: {body}")
|
||||
return {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"message": "Hello from plugin!",
|
||||
"method": method,
|
||||
"plugin_id": ctx.plugin_id,
|
||||
},
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
_schema_version: 2
|
||||
name: astrbot_plugin_v4demo
|
||||
display_name: V4 Demo 插件
|
||||
desc: 一个覆盖 v4 原生命令、消息处理和 capability 的示例插件
|
||||
author: Soulter
|
||||
version: 0.1.0
|
||||
runtime:
|
||||
python: "3.12"
|
||||
components:
|
||||
- class: commands.hello:HelloPlugin
|
||||
type: command
|
||||
name: hello
|
||||
description: 发送问候消息
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Reference in New Issue
Block a user