mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-04 14:52:12 +08:00
Add 'docs/' from commit '51dddcb3f891d60bc38645e88b2dbd515344da81'
git-subtree-dir: docs git-subtree-mainline:0582d1b3d4git-subtree-split:51dddcb3f8
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
---
|
||||
title: "Discord 适配器文档"
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
这是 LangBot 平台的 Discord 适配器完整文档,基于 discord.py 库实现了与 Discord 平台的全面集成,包括文本消息、图像处理、语音功能等完整功能集。
|
||||
|
||||
## 文档导航
|
||||
|
||||
### 核心文档
|
||||
- **[API 参考](api_reference)** - Discord 适配器完整 API 文档
|
||||
- **[快速开始](quick_start)** - 基本使用指南
|
||||
- **[架构设计](design)** - 适配器架构设计
|
||||
- **[故障排除](troubleshooting)** - 常见问题解决方案
|
||||
|
||||
## 完整功能集
|
||||
|
||||
### 消息处理
|
||||
- 文本消息发送和接收
|
||||
- 图像消息处理(支持多种格式)
|
||||
- 消息引用和回复
|
||||
- At 功能支持
|
||||
- 转发消息处理
|
||||
|
||||
### 图像功能
|
||||
- 多格式图像支持(PNG、JPG、GIF、WebP)
|
||||
- Base64 编码图像处理
|
||||
- URL 图像下载和转换
|
||||
- 本地文件图像读取
|
||||
- 自动文件类型检测
|
||||
- 批量图像发送
|
||||
|
||||
### 语音功能
|
||||
- 语音频道连接管理
|
||||
- 音频播放控制
|
||||
- 连接状态监控
|
||||
- 权限验证系统
|
||||
- 自动连接清理
|
||||
|
||||
### 事件系统
|
||||
- 消息事件处理
|
||||
- 用户状态监控
|
||||
- 频道状态变化
|
||||
- Bot 生命周期管理
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **LangBot**: 框架环境
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 核心组件
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Discord Adapter] --> B[DiscordAdapter]
|
||||
A --> C[DiscordMessageConverter]
|
||||
A --> D[DiscordEventConverter]
|
||||
A --> E[VoiceConnectionManager]
|
||||
A --> F[MyClient]
|
||||
|
||||
B --> G[消息发送/接收]
|
||||
C --> H[格式转换]
|
||||
D --> I[事件处理]
|
||||
E --> J[语音连接]
|
||||
F --> K[Discord API]
|
||||
```
|
||||
|
||||
### 消息处理流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Discord User
|
||||
participant D as Discord API
|
||||
participant A as DiscordAdapter
|
||||
participant E as EventConverter
|
||||
participant P as Plugin
|
||||
participant M as MessageConverter
|
||||
|
||||
U->>D: 发送消息
|
||||
D->>A: Discord Message
|
||||
A->>E: target2yiri()
|
||||
E->>P: MessageEvent
|
||||
P->>A: 回复请求
|
||||
A->>M: yiri2target()
|
||||
M->>D: Discord 格式
|
||||
D->>U: 显示回复
|
||||
```
|
||||
|
||||
### 图像处理流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Image Element] --> B{图像来源}
|
||||
B -->|base64| C[Base64 解码]
|
||||
B -->|url| D[HTTP 下载]
|
||||
B -->|path| E[文件读取]
|
||||
|
||||
C --> F[格式检测]
|
||||
D --> F
|
||||
E --> F
|
||||
|
||||
F --> G[文件名生成]
|
||||
G --> H[discord.File 创建]
|
||||
H --> I[上传到 Discord]
|
||||
```
|
||||
|
||||
### 语音处理流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[语音指令] --> B[权限检查]
|
||||
B --> C{用户在频道?}
|
||||
C -->|否| D[权限错误]
|
||||
C -->|是| E[Bot权限检查]
|
||||
E --> F{有连接权限?}
|
||||
F -->|否| D
|
||||
F -->|是| G[建立连接]
|
||||
G --> H[音频处理]
|
||||
H --> I[Discord Voice API]
|
||||
```
|
||||
|
||||
## 核心特性详解
|
||||
|
||||
### 消息转换器 (DiscordMessageConverter)
|
||||
|
||||
负责在 LangBot 内部消息格式和 Discord API 格式之间进行转换:
|
||||
|
||||
- **yiri2target**: 将 LangBot MessageChain 转换为 Discord 格式
|
||||
- **target2yiri**: 将 Discord 消息转换为 LangBot 格式
|
||||
- 支持文本、图像、转发消息等多种类型
|
||||
|
||||
### 图像处理特性
|
||||
|
||||
- **多源支持**: Base64、URL、本地文件路径
|
||||
- **格式检测**: 自动检测 PNG、JPG、GIF、WebP 格式
|
||||
- **智能命名**: 基于内容和来源的文件命名
|
||||
- **错误处理**: 优雅处理无效图像和网络错误
|
||||
- **批量处理**: 单次消息支持多张图像
|
||||
|
||||
### 语音连接管理器 (VoiceConnectionManager)
|
||||
|
||||
提供完整的语音功能管理:
|
||||
|
||||
- **连接池管理**: 多服务器连接状态跟踪
|
||||
- **权限验证**: 用户和机器人权限检查
|
||||
- **状态监控**: 连接健康度和延迟监控
|
||||
- **异常处理**: 分类的异常体系
|
||||
- **自动清理**: 定期清理无效连接
|
||||
|
||||
### 事件系统
|
||||
|
||||
- **消息事件**: 处理用户消息和机器人响应
|
||||
- **状态监控**: 跟踪连接状态变化
|
||||
- **生命周期**: 管理适配器启动和关闭
|
||||
|
||||
## 权限配置
|
||||
|
||||
Bot 需要以下 Discord 权限:
|
||||
|
||||
- **基础权限**:
|
||||
- Read Messages
|
||||
- Send Messages
|
||||
- Embed Links
|
||||
- Attach Files
|
||||
- Read Message History
|
||||
|
||||
- **语音权限**:
|
||||
- Connect
|
||||
- Speak
|
||||
- Use Voice Activity
|
||||
|
||||
---
|
||||
|
||||
> **注意**: 本文档基于实际的 discord.py 实现,涵盖了 Discord 适配器的完整功能集。语音功能需要额外的依赖安装,图像功能支持多种来源和格式。详细的使用方法和故障排除请参考其他专门文档。
|
||||
@@ -0,0 +1,553 @@
|
||||
---
|
||||
title: "Discord 适配器 API 参考"
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
Discord 适配器提供完整的 Discord 平台集成功能,包括消息处理、图像管理、语音连接等。所有功能基于 discord.py 库的原生 API 实现。
|
||||
|
||||
## 核心 API
|
||||
|
||||
### DiscordAdapter 类
|
||||
|
||||
Discord 平台适配器主类,提供与 Discord 平台交互的完整接口。
|
||||
|
||||
#### 消息相关方法
|
||||
|
||||
##### `send_message(target_type, target_id, message)`
|
||||
|
||||
发送消息到指定频道。
|
||||
|
||||
**参数:**
|
||||
- `target_type` (str): 目标类型,通常为 "channel"
|
||||
- `target_id` (str): 目标频道 ID
|
||||
- `message` (MessageChain): 要发送的消息链
|
||||
|
||||
**功能:**
|
||||
- 支持文本消息发送
|
||||
- 支持图像文件批量发送
|
||||
- 自动处理消息格式转换
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
# 发送纯文本消息
|
||||
await adapter.send_message("channel", "123456789", MessageChain([Plain("Hello!")]))
|
||||
|
||||
# 发送带图像的消息
|
||||
message_chain = MessageChain([
|
||||
Plain("看看这张图片:"),
|
||||
Image(path="path/to/image.jpg"),
|
||||
Image(url="https://example.com/image.png")
|
||||
])
|
||||
await adapter.send_message("channel", "123456789", message_chain)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `reply_message(message_source, message, quote_origin=False)`
|
||||
|
||||
回复指定消息。
|
||||
|
||||
**参数:**
|
||||
- `message_source` (MessageEvent): 要回复的原始消息事件
|
||||
- `message` (MessageChain): 回复内容
|
||||
- `quote_origin` (bool): 是否引用原消息
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
# 简单回复
|
||||
await adapter.reply_message(
|
||||
message_event,
|
||||
MessageChain([Plain("收到你的消息了!")])
|
||||
)
|
||||
|
||||
# 引用回复
|
||||
await adapter.reply_message(
|
||||
message_event,
|
||||
MessageChain([Plain("这是对你消息的回复")]),
|
||||
quote_origin=True
|
||||
)
|
||||
```
|
||||
|
||||
#### 语音相关方法
|
||||
|
||||
##### `join_voice_channel(guild_id, channel_id, user_id=None)`
|
||||
|
||||
加入指定的语音频道。
|
||||
|
||||
**参数:**
|
||||
- `guild_id` (int): Discord 服务器 ID
|
||||
- `channel_id` (int): 语音频道 ID
|
||||
- `user_id` (int, optional): 请求用户 ID,用于权限验证
|
||||
|
||||
**返回值:**
|
||||
- `discord.VoiceClient`: 语音客户端对象
|
||||
|
||||
**异常:**
|
||||
- `VoicePermissionError`: 权限不足
|
||||
- `VoiceNetworkError`: 网络连接问题
|
||||
- `VoiceConnectionError`: 其他连接错误
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
try:
|
||||
voice_client = await adapter.join_voice_channel(
|
||||
guild_id=123456789,
|
||||
channel_id=987654321,
|
||||
user_id=555666777
|
||||
)
|
||||
print(f"成功连接,延迟: {voice_client.latency*1000:.2f}ms")
|
||||
except VoicePermissionError as e:
|
||||
print(f"权限错误: {e.missing_permissions}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `leave_voice_channel(guild_id)`
|
||||
|
||||
离开指定服务器的语音频道。
|
||||
|
||||
**参数:**
|
||||
- `guild_id` (int): Discord 服务器 ID
|
||||
|
||||
**返回值:**
|
||||
- `bool`: 成功断开返回 `True`
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
success = await adapter.leave_voice_channel(guild_id=123456789)
|
||||
if success:
|
||||
print("成功离开语音频道")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `get_voice_client(guild_id)`
|
||||
|
||||
获取指定服务器的语音客户端对象。
|
||||
|
||||
**参数:**
|
||||
- `guild_id` (int): Discord 服务器 ID
|
||||
|
||||
**返回值:**
|
||||
- `discord.VoiceClient`: 语音客户端对象,如果存在连接
|
||||
- `None`: 如果没有活跃连接
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
voice_client = await adapter.get_voice_client(guild_id=123456789)
|
||||
if voice_client:
|
||||
# 播放音频
|
||||
import discord
|
||||
audio = discord.FFmpegPCMAudio("path/to/audio.mp3")
|
||||
voice_client.play(audio)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `is_connected_to_voice(guild_id)`
|
||||
|
||||
检查是否连接到指定服务器的语音频道。
|
||||
|
||||
**参数:**
|
||||
- `guild_id` (int): Discord 服务器 ID
|
||||
|
||||
**返回值:**
|
||||
- `bool`: 已连接返回 `True`
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
if await adapter.is_connected_to_voice(guild_id=123456789):
|
||||
print("已连接到语音频道")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `get_voice_connection_status(guild_id)`
|
||||
|
||||
获取语音连接的详细状态信息。
|
||||
|
||||
**参数:**
|
||||
- `guild_id` (int): Discord 服务器 ID
|
||||
|
||||
**返回值:**
|
||||
- `dict`: 连接状态信息字典,如果存在连接
|
||||
- `None`: 如果没有连接
|
||||
|
||||
**状态信息结构:**
|
||||
```python
|
||||
{
|
||||
'guild_id': int,
|
||||
'channel_id': int,
|
||||
'channel_name': str,
|
||||
'connected': bool,
|
||||
'connection_time': datetime,
|
||||
'last_activity': datetime,
|
||||
'status': str, # VoiceConnectionStatus 枚举值
|
||||
'user_count': int,
|
||||
'latency': float,
|
||||
'connection_health': str
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `list_active_voice_connections()`
|
||||
|
||||
获取所有活跃的语音连接列表。
|
||||
|
||||
**返回值:**
|
||||
- `List[dict]`: 活跃连接状态列表
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
connections = await adapter.list_active_voice_connections()
|
||||
for conn in connections:
|
||||
print(f"服务器: {conn['guild_id']}, 频道: {conn['channel_name']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##### `get_voice_channel_info(guild_id, channel_id)`
|
||||
|
||||
获取指定语音频道的详细信息。
|
||||
|
||||
**参数:**
|
||||
- `guild_id` (int): 服务器 ID
|
||||
- `channel_id` (int): 频道 ID
|
||||
|
||||
**返回值:**
|
||||
- `dict`: 频道信息字典
|
||||
- `None`: 如果频道不存在
|
||||
|
||||
**频道信息结构:**
|
||||
```python
|
||||
{
|
||||
'channel_id': int,
|
||||
'channel_name': str,
|
||||
'guild_id': int,
|
||||
'guild_name': str,
|
||||
'user_limit': int,
|
||||
'current_users': List[dict],
|
||||
'user_count': int,
|
||||
'bitrate': int,
|
||||
'permissions': dict
|
||||
}
|
||||
```
|
||||
|
||||
#### 管理方法
|
||||
|
||||
##### `is_muted(group_id)`
|
||||
|
||||
检查群组是否被静音(Discord 适配器始终返回 False)。
|
||||
|
||||
**参数:**
|
||||
- `group_id` (int): 群组 ID
|
||||
|
||||
**返回值:**
|
||||
- `bool`: 是否静音
|
||||
|
||||
---
|
||||
|
||||
##### `register_listener(event_type, callback)`
|
||||
|
||||
注册事件监听器。
|
||||
|
||||
**参数:**
|
||||
- `event_type` (Type[Event]): 事件类型
|
||||
- `callback` (Callable): 回调函数
|
||||
|
||||
---
|
||||
|
||||
##### `unregister_listener(event_type, callback)`
|
||||
|
||||
注销事件监听器。
|
||||
|
||||
**参数:**
|
||||
- `event_type` (Type[Event]): 事件类型
|
||||
- `callback` (Callable): 回调函数
|
||||
|
||||
---
|
||||
|
||||
##### `run_async()`
|
||||
|
||||
启动 Discord 适配器,建立与 Discord 的连接。
|
||||
|
||||
**功能:**
|
||||
- 初始化语音管理器
|
||||
- 启动连接监控
|
||||
- 建立 WebSocket 连接
|
||||
|
||||
---
|
||||
|
||||
##### `kill()`
|
||||
|
||||
关闭 Discord 适配器。
|
||||
|
||||
**返回值:**
|
||||
- `bool`: 是否成功关闭
|
||||
|
||||
**功能:**
|
||||
- 断开所有语音连接
|
||||
- 关闭 Discord 客户端
|
||||
- 清理资源
|
||||
|
||||
## 消息转换器 API
|
||||
|
||||
### DiscordMessageConverter 类
|
||||
|
||||
负责在 LangBot 和 Discord 消息格式之间进行转换。
|
||||
|
||||
#### `yiri2target(message_chain)`
|
||||
|
||||
将 LangBot MessageChain 转换为 Discord 格式。
|
||||
|
||||
**参数:**
|
||||
- `message_chain` (MessageChain): LangBot 消息链
|
||||
|
||||
**返回值:**
|
||||
- `Tuple[str, List[discord.File]]`: (文本内容, 文件列表)
|
||||
|
||||
**支持的消息元素:**
|
||||
- `Plain`: 纯文本内容
|
||||
- `Image`: 图像内容(支持 base64、URL、文件路径)
|
||||
- `At`: @ 提及(会被移除)
|
||||
- `Forward`: 转发消息
|
||||
|
||||
**图像处理特性:**
|
||||
- **Base64 图像**: 自动检测格式并解码
|
||||
- **URL 图像**: 异步下载并转换
|
||||
- **文件路径**: 读取本地文件
|
||||
- **格式检测**: 支持 PNG、JPG、GIF、WebP
|
||||
- **错误处理**: 跳过无效图像
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
message_chain = MessageChain([
|
||||
Plain("文本内容"),
|
||||
Image(base64="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="),
|
||||
Image(url="https://example.com/image.jpg"),
|
||||
Image(path="/path/to/local/image.png")
|
||||
])
|
||||
|
||||
text, files = await DiscordMessageConverter.yiri2target(message_chain)
|
||||
# text: "文本内容"
|
||||
# files: [discord.File, discord.File, discord.File]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `target2yiri(message)`
|
||||
|
||||
将 Discord 消息转换为 LangBot MessageChain。
|
||||
|
||||
**参数:**
|
||||
- `message` (discord.Message): Discord 消息对象
|
||||
|
||||
**返回值:**
|
||||
- `MessageChain`: LangBot 消息链
|
||||
|
||||
**转换规则:**
|
||||
- 文本内容 → `Plain` 元素
|
||||
- 附件图像 → `Image` 元素
|
||||
- 嵌入内容 → `Plain` 元素(描述文本)
|
||||
|
||||
## 事件转换器 API
|
||||
|
||||
### DiscordEventConverter 类
|
||||
|
||||
负责将 Discord 事件转换为 LangBot 平台事件。
|
||||
|
||||
#### `target2yiri(message)`
|
||||
|
||||
将 Discord 消息转换为 LangBot MessageEvent。
|
||||
|
||||
**参数:**
|
||||
- `message` (discord.Message): Discord 消息对象
|
||||
|
||||
**返回值:**
|
||||
- `MessageEvent`: LangBot 消息事件
|
||||
|
||||
**事件字段:**
|
||||
- `message_chain`: 转换后的消息链
|
||||
- `sender`: 发送者信息
|
||||
- `query`: 查询上下文信息
|
||||
|
||||
## 异常类
|
||||
|
||||
### VoiceConnectionError
|
||||
|
||||
基础语音连接异常类。
|
||||
|
||||
**属性:**
|
||||
- `error_code` (str): 错误代码
|
||||
- `guild_id` (int): 相关服务器 ID
|
||||
- `timestamp` (datetime): 错误发生时间
|
||||
|
||||
**常见错误代码:**
|
||||
- `"GUILD_NOT_FOUND"`: 服务器不存在
|
||||
- `"CHANNEL_NOT_FOUND"`: 频道不存在
|
||||
- `"MANAGER_NOT_READY"`: 语音管理器未初始化
|
||||
- `"OPUS_NOT_LOADED"`: Opus编码器未加载
|
||||
- `"UNKNOWN_ERROR"`: 未知错误
|
||||
|
||||
### VoicePermissionError
|
||||
|
||||
语音权限异常,继承自 `VoiceConnectionError`。
|
||||
|
||||
**属性:**
|
||||
- `missing_permissions` (List[str]): 缺失的权限列表
|
||||
- `user_id` (int): 相关用户 ID
|
||||
- `channel_id` (int): 相关频道 ID
|
||||
|
||||
**常见权限错误:**
|
||||
- `"user_not_in_channel"`: 用户不在语音频道中
|
||||
- `"member_not_found"`: 找不到用户
|
||||
- `"connect"`: Bot缺少连接权限
|
||||
- `"speak"`: Bot缺少发言权限
|
||||
|
||||
### VoiceNetworkError
|
||||
|
||||
语音网络异常,继承自 `VoiceConnectionError`。
|
||||
|
||||
**属性:**
|
||||
- `retry_count` (int): 重试次数
|
||||
- `last_attempt` (datetime): 最后尝试时间
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 综合消息处理
|
||||
|
||||
```python
|
||||
async def handle_complex_message(adapter, channel_id):
|
||||
"""处理包含多种元素的复杂消息"""
|
||||
|
||||
# 构建包含文本和图像的消息
|
||||
message_chain = MessageChain([
|
||||
Plain("这是一个包含多种内容的消息:\\n"),
|
||||
Plain("1. 文本内容\\n"),
|
||||
Plain("2. 网络图像\\n"),
|
||||
Image(url="https://example.com/image1.jpg"),
|
||||
Plain("3. 本地图像\\n"),
|
||||
Image(path="/path/to/local/image.png"),
|
||||
Plain("4. Base64 图像\\n"),
|
||||
Image(base64="data:image/png;base64,iVBORw0KGgo...")
|
||||
])
|
||||
|
||||
# 发送消息
|
||||
await adapter.send_message("channel", channel_id, message_chain)
|
||||
```
|
||||
|
||||
### 语音播放控制
|
||||
|
||||
```python
|
||||
async def voice_player_example(adapter, guild_id, channel_id, audio_files):
|
||||
"""语音播放器示例"""
|
||||
|
||||
# 连接到语音频道
|
||||
try:
|
||||
voice_client = await adapter.join_voice_channel(guild_id, channel_id)
|
||||
except VoicePermissionError as e:
|
||||
print(f"权限不足: {e.missing_permissions}")
|
||||
return
|
||||
|
||||
# 播放音频队列
|
||||
for audio_file in audio_files:
|
||||
if voice_client.is_connected():
|
||||
# 等待当前播放完成
|
||||
while voice_client.is_playing():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 播放下一个音频
|
||||
audio_source = discord.FFmpegPCMAudio(audio_file)
|
||||
voice_client.play(audio_source)
|
||||
print(f"正在播放: {audio_file}")
|
||||
|
||||
# 播放完成后离开频道
|
||||
await adapter.leave_voice_channel(guild_id)
|
||||
```
|
||||
|
||||
### 状态监控
|
||||
|
||||
```python
|
||||
async def monitor_connections(adapter):
|
||||
"""监控所有语音连接状态"""
|
||||
|
||||
connections = await adapter.list_active_voice_connections()
|
||||
|
||||
for conn in connections:
|
||||
print(f"服务器: {conn['guild_id']}")
|
||||
print(f"频道: {conn['channel_name']}")
|
||||
print(f"延迟: {conn['latency']:.2f}ms")
|
||||
print(f"用户数: {conn['user_count']}")
|
||||
print(f"健康度: {conn['connection_health']}")
|
||||
print("---")
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 错误处理
|
||||
|
||||
```python
|
||||
async def safe_discord_operation():
|
||||
try:
|
||||
# Discord 操作
|
||||
pass
|
||||
except VoicePermissionError as e:
|
||||
if "user_not_in_channel" in e.missing_permissions:
|
||||
await send_error("请先加入语音频道")
|
||||
elif "connect" in e.missing_permissions:
|
||||
await send_error("机器人没有连接权限")
|
||||
except discord.HTTPException as e:
|
||||
if e.status == 403:
|
||||
await send_error("权限不足")
|
||||
elif e.status == 404:
|
||||
await send_error("频道不存在")
|
||||
except Exception as e:
|
||||
await send_error(f"未知错误: {e}")
|
||||
```
|
||||
|
||||
### 资源管理
|
||||
|
||||
```python
|
||||
async def with_voice_connection(adapter, guild_id, channel_id, operation):
|
||||
"""安全的语音连接管理"""
|
||||
voice_client = None
|
||||
try:
|
||||
voice_client = await adapter.join_voice_channel(guild_id, channel_id)
|
||||
await operation(voice_client)
|
||||
finally:
|
||||
if voice_client:
|
||||
await adapter.leave_voice_channel(guild_id)
|
||||
```
|
||||
|
||||
### 图像处理优化
|
||||
|
||||
```python
|
||||
async def batch_image_processing(image_list):
|
||||
"""批量图像处理"""
|
||||
processed_images = []
|
||||
|
||||
for image_element in image_list:
|
||||
try:
|
||||
if image_element.url:
|
||||
# 异步下载
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(image_element.url) as response:
|
||||
image_data = await response.read()
|
||||
processed_images.append(process_image_data(image_data))
|
||||
elif image_element.path:
|
||||
# 本地文件处理
|
||||
with open(image_element.path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
processed_images.append(process_image_data(image_data))
|
||||
except Exception as e:
|
||||
print(f"图像处理失败: {e}")
|
||||
continue
|
||||
|
||||
return processed_images
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **注意**: 本 API 文档基于实际的 discord.py 实现,涵盖了 Discord 适配器的完整功能集。图像处理功能无需额外依赖,语音功能需要安装可选依赖。
|
||||
@@ -0,0 +1,399 @@
|
||||
---
|
||||
title: "Discord 适配器架构设计"
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细描述 LangBot Discord 适配器的架构设计,包括消息处理、图像管理、语音功能等完整系统的设计理念和实现架构。
|
||||
|
||||
## 整体架构
|
||||
|
||||
### 系统组件图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "LangBot 框架层"
|
||||
A[EventContext] --> B[DiscordAdapter]
|
||||
A --> C[MessageEvent]
|
||||
A --> D[Plugin System]
|
||||
end
|
||||
|
||||
subgraph "Discord 适配器层"
|
||||
B --> E[DiscordMessageConverter]
|
||||
B --> F[DiscordEventConverter]
|
||||
B --> G[VoiceConnectionManager]
|
||||
B --> H[MyClient]
|
||||
end
|
||||
|
||||
subgraph "Discord.py 库层"
|
||||
H --> I[discord.Client]
|
||||
G --> J[discord.VoiceClient]
|
||||
E --> K[discord.File]
|
||||
I --> L[Discord WebSocket]
|
||||
J --> L
|
||||
end
|
||||
|
||||
subgraph "外部服务"
|
||||
L --> M[Discord API]
|
||||
N[HTTP Requests] --> O[Image URLs]
|
||||
P[Local Files] --> E
|
||||
Q[Audio Files] --> J
|
||||
end
|
||||
|
||||
subgraph "依赖库"
|
||||
R[aiohttp] --> N
|
||||
S[PyNaCl] --> J
|
||||
T[FFmpeg] --> J
|
||||
end
|
||||
```
|
||||
|
||||
### 数据流架构
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant D as Discord
|
||||
participant A as DiscordAdapter
|
||||
participant E as EventConverter
|
||||
participant M as MessageConverter
|
||||
participant P as Plugin
|
||||
|
||||
U->>D: 发送消息
|
||||
D->>A: Discord Message
|
||||
A->>E: target2yiri()
|
||||
E->>A: MessageEvent
|
||||
A->>P: 事件分发
|
||||
P->>A: 回复消息
|
||||
A->>M: yiri2target()
|
||||
M->>A: Discord格式
|
||||
A->>D: 发送回复
|
||||
D->>U: 显示回复
|
||||
```
|
||||
|
||||
## 核心组件设计
|
||||
|
||||
### 1. DiscordAdapter 主适配器
|
||||
|
||||
#### 职责
|
||||
- **连接管理**: 维护与 Discord 的 WebSocket 连接
|
||||
- **事件路由**: 将 Discord 事件转换并分发到插件系统
|
||||
- **消息发送**: 处理外发消息的格式转换和发送
|
||||
- **语音管理**: 协调语音连接的生命周期
|
||||
- **资源清理**: 管理连接池和内存资源
|
||||
|
||||
#### 核心接口
|
||||
```python
|
||||
class DiscordAdapter(MessagePlatformAdapter):
|
||||
# 消息相关
|
||||
async def send_message(target_type: str, target_id: str, message: MessageChain)
|
||||
async def reply_message(message_source: MessageEvent, message: MessageChain, quote_origin: bool)
|
||||
|
||||
# 语音相关
|
||||
async def join_voice_channel(guild_id: int, channel_id: int, user_id: int) -> VoiceClient
|
||||
async def leave_voice_channel(guild_id: int) -> bool
|
||||
async def get_voice_client(guild_id: int) -> Optional[VoiceClient]
|
||||
|
||||
# 管理相关
|
||||
async def run_async()
|
||||
async def kill() -> bool
|
||||
```
|
||||
|
||||
#### 设计模式
|
||||
- **适配器模式**: 将 Discord API 适配到 LangBot 接口
|
||||
- **单例模式**: 每个 Discord 配置对应一个适配器实例
|
||||
- **观察者模式**: 事件监听和分发机制
|
||||
|
||||
### 2. 消息转换器 (DiscordMessageConverter)
|
||||
|
||||
#### 设计目标
|
||||
- **格式无关**: 屏蔽 Discord 和 LangBot 格式差异
|
||||
- **类型安全**: 保证消息元素类型转换的正确性
|
||||
- **性能优化**: 减少不必要的数据复制和转换
|
||||
- **错误恢复**: 优雅处理格式错误和网络异常
|
||||
|
||||
#### 转换策略
|
||||
|
||||
##### LangBot → Discord (yiri2target)
|
||||
```python
|
||||
转换流程:
|
||||
MessageChain → 解析元素 → 分类处理 → (text: str, files: List[discord.File])
|
||||
|
||||
元素处理策略:
|
||||
- Plain → 文本累加
|
||||
- Image → 文件对象创建
|
||||
- At → 移除处理
|
||||
- Forward → 递归展开
|
||||
```
|
||||
|
||||
##### Discord → LangBot (target2yiri)
|
||||
```python
|
||||
转换流程:
|
||||
discord.Message → 提取内容 → 构建元素 → MessageChain
|
||||
|
||||
内容映射:
|
||||
- message.content → Plain
|
||||
- message.attachments → Image
|
||||
- message.embeds → Plain (description)
|
||||
```
|
||||
|
||||
#### 图像处理架构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Image Element] --> B{来源类型}
|
||||
B -->|base64| C[Base64 解码]
|
||||
B -->|url| D[HTTP 下载]
|
||||
B -->|path| E[文件读取]
|
||||
|
||||
C --> F[格式检测]
|
||||
D --> F
|
||||
E --> F
|
||||
|
||||
F --> G[文件名生成]
|
||||
G --> H[discord.File 创建]
|
||||
|
||||
I[错误处理] --> J[跳过无效图像]
|
||||
F --> I
|
||||
D --> I
|
||||
E --> I
|
||||
```
|
||||
|
||||
#### 图像处理特性
|
||||
- **多源支持**: Base64、URL、本地文件
|
||||
- **格式智能检测**: 基于内容和元数据
|
||||
- **异步下载**: 并发处理多个网络图像
|
||||
- **内存优化**: 使用 BytesIO 避免临时文件
|
||||
- **错误隔离**: 单个图像失败不影响其他处理
|
||||
|
||||
### 3. 语音连接管理器 (VoiceConnectionManager)
|
||||
|
||||
#### 设计原则
|
||||
- **连接复用**: 避免重复连接开销
|
||||
- **状态一致性**: 维护准确的连接状态
|
||||
- **权限验证**: 多层次权限检查
|
||||
- **故障恢复**: 自动重连和清理机制
|
||||
- **性能监控**: 连接质量实时监控
|
||||
|
||||
#### 连接生命周期
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> IDLE
|
||||
IDLE --> CONNECTING: join_voice_channel()
|
||||
CONNECTING --> CONNECTED: 连接成功
|
||||
CONNECTING --> FAILED: 连接失败
|
||||
CONNECTED --> PLAYING: 播放音频
|
||||
PLAYING --> CONNECTED: 播放完成/停止
|
||||
CONNECTED --> RECONNECTING: 网络中断
|
||||
RECONNECTING --> CONNECTED: 重连成功
|
||||
RECONNECTING --> FAILED: 重连失败
|
||||
FAILED --> IDLE: 清理资源
|
||||
CONNECTED --> IDLE: leave_voice_channel()
|
||||
PLAYING --> IDLE: leave_voice_channel()
|
||||
```
|
||||
|
||||
#### 连接池设计
|
||||
```python
|
||||
class VoiceConnectionManager:
|
||||
connections: Dict[int, VoiceConnectionInfo] # guild_id -> 连接信息
|
||||
|
||||
# 连接管理
|
||||
async def join_voice_channel() -> VoiceClient
|
||||
async def leave_voice_channel() -> bool
|
||||
async def get_connection_status() -> Optional[dict]
|
||||
|
||||
# 监控和清理
|
||||
async def start_monitoring()
|
||||
async def cleanup_inactive_connections()
|
||||
async def disconnect_all()
|
||||
```
|
||||
|
||||
#### 权限验证架构
|
||||
```mermaid
|
||||
graph TD
|
||||
A[连接请求] --> B[用户权限检查]
|
||||
B --> C{用户在频道?}
|
||||
C -->|否| D[VoicePermissionError]
|
||||
C -->|是| E[Bot权限检查]
|
||||
E --> F{Connect权限?}
|
||||
F -->|否| G[VoicePermissionError]
|
||||
F -->|是| H{Speak权限?}
|
||||
H -->|否| I[VoicePermissionError]
|
||||
H -->|是| J[建立连接]
|
||||
```
|
||||
|
||||
### 4. 事件转换器 (DiscordEventConverter)
|
||||
|
||||
#### 转换策略
|
||||
- **事件映射**: Discord 事件到 LangBot 事件的标准化
|
||||
- **上下文构建**: 构建完整的事件上下文信息
|
||||
- **用户信息**: 提取和标准化用户身份信息
|
||||
- **频道信息**: 处理频道和服务器相关信息
|
||||
|
||||
#### 事件转换流程
|
||||
```python
|
||||
discord.Message
|
||||
└── 提取基础信息 (作者、频道、时间)
|
||||
└── 转换消息内容 (DiscordMessageConverter)
|
||||
└── 构建查询对象 (Query)
|
||||
└── 创建发送者对象 (Sender)
|
||||
└── 生成 MessageEvent
|
||||
```
|
||||
|
||||
## 异常处理设计
|
||||
|
||||
### 异常体系架构
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
Exception <|-- VoiceConnectionError
|
||||
VoiceConnectionError <|-- VoicePermissionError
|
||||
VoiceConnectionError <|-- VoiceNetworkError
|
||||
|
||||
class VoiceConnectionError {
|
||||
+error_code: str
|
||||
+guild_id: int
|
||||
+timestamp: datetime
|
||||
}
|
||||
|
||||
class VoicePermissionError {
|
||||
+missing_permissions: List[str]
|
||||
+user_id: int
|
||||
+channel_id: int
|
||||
}
|
||||
|
||||
class VoiceNetworkError {
|
||||
+retry_count: int
|
||||
+last_attempt: datetime
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理策略
|
||||
- **分层处理**: 不同层次处理不同类型的错误
|
||||
- **优雅降级**: 单个功能失败不影响整体系统
|
||||
- **用户友好**: 提供清晰的错误信息和解决建议
|
||||
- **日志记录**: 完整的错误追踪和调试信息
|
||||
|
||||
## 性能设计
|
||||
|
||||
### 内存管理
|
||||
- **连接池**: 复用语音连接减少开销
|
||||
- **图像缓存**: 临时图像数据及时清理
|
||||
- **异步处理**: 避免阻塞主事件循环
|
||||
- **资源限制**: 控制并发连接和文件大小
|
||||
|
||||
### 并发设计
|
||||
- **异步架构**: 全面采用 async/await
|
||||
- **连接锁**: 防止语音连接竞态条件
|
||||
- **批量处理**: 图像和消息的批量操作
|
||||
- **错误隔离**: 单个操作失败不影响其他操作
|
||||
|
||||
### 缓存策略
|
||||
```python
|
||||
# 连接状态缓存
|
||||
connections: Dict[int, VoiceConnectionInfo]
|
||||
|
||||
# 临时数据缓存
|
||||
image_cache: Dict[str, bytes] # URL -> 图像数据
|
||||
channel_cache: Dict[int, discord.Channel] # 频道对象缓存
|
||||
```
|
||||
|
||||
## 扩展性设计
|
||||
|
||||
### 插件接口
|
||||
- **标准化事件**: 提供统一的事件接口
|
||||
- **类型安全**: 强类型的消息元素系统
|
||||
- **回调机制**: 灵活的事件监听和处理
|
||||
- **生命周期**: 完整的插件生命周期管理
|
||||
|
||||
### 配置管理
|
||||
```python
|
||||
# 适配器配置
|
||||
class DiscordConfig:
|
||||
client_id: str
|
||||
token: str
|
||||
proxy: Optional[str]
|
||||
voice_enabled: bool = True
|
||||
image_max_size: int = 25 * 1024 * 1024 # 25MB
|
||||
max_concurrent_downloads: int = 5
|
||||
```
|
||||
|
||||
### 监控接口
|
||||
- **连接状态**: 实时语音连接状态
|
||||
- **性能指标**: 延迟、吞吐量等指标
|
||||
- **错误统计**: 各类错误的统计和趋势
|
||||
- **资源使用**: 内存、连接数等资源监控
|
||||
|
||||
## 测试设计
|
||||
|
||||
### 单元测试架构
|
||||
```python
|
||||
# 消息转换测试
|
||||
class TestDiscordMessageConverter:
|
||||
async def test_yiri2target_text()
|
||||
async def test_yiri2target_images()
|
||||
async def test_target2yiri()
|
||||
|
||||
# 语音功能测试
|
||||
class TestVoiceConnectionManager:
|
||||
async def test_join_voice_channel()
|
||||
async def test_permission_validation()
|
||||
async def test_connection_cleanup()
|
||||
|
||||
# 适配器集成测试
|
||||
class TestDiscordAdapter:
|
||||
async def test_message_sending()
|
||||
async def test_event_handling()
|
||||
async def test_voice_integration()
|
||||
```
|
||||
|
||||
### 模拟测试
|
||||
- **Mock Discord API**: 模拟 Discord 服务器响应
|
||||
- **虚拟语音频道**: 测试语音功能无需真实连接
|
||||
- **错误注入**: 测试各种异常情况的处理
|
||||
- **性能测试**: 并发和高负载测试
|
||||
|
||||
## 部署架构
|
||||
|
||||
### 环境要求
|
||||
- 与LangBot相同
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 日志设计
|
||||
```python
|
||||
# 分级日志
|
||||
logger.debug("详细调试信息")
|
||||
logger.info("一般操作信息")
|
||||
logger.warning("警告信息")
|
||||
logger.error("错误信息")
|
||||
logger.critical("严重错误")
|
||||
|
||||
# 结构化日志
|
||||
{
|
||||
"timestamp": "2025-07-04T10:30:00Z",
|
||||
"level": "INFO",
|
||||
"component": "VoiceConnectionManager",
|
||||
"action": "join_voice_channel",
|
||||
"guild_id": 123456789,
|
||||
"channel_id": 987654321,
|
||||
"latency": 45.2,
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
- **连接状态**: Discord WebSocket 连接健康度
|
||||
- **语音质量**: 语音连接延迟和稳定性
|
||||
- **资源使用**: 内存和连接池状态
|
||||
- **错误率**: 各类操作的成功率统计
|
||||
|
||||
### 故障恢复
|
||||
- **自动重连**: WebSocket 连接断开后自动重连
|
||||
- **连接清理**: 定期清理无效的语音连接
|
||||
- **错误重试**: 网络错误的智能重试机制
|
||||
- **降级服务**: 语音功能异常时的降级策略
|
||||
|
||||
---
|
||||
|
||||
> **设计原则**: 本架构设计遵循模块化、可扩展、高性能的原则,确保 Discord 适配器能够稳定、高效地处理各种类型的消息和连接需求。
|
||||
@@ -0,0 +1,553 @@
|
||||
---
|
||||
title: "Discord 适配器快速入门"
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本指南将引导您快速上手 LangBot Discord 适配器的完整功能,包括消息处理、图像功能和语音连接等。
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 环境要求
|
||||
- Python 3.8+
|
||||
- LangBot 框架环境
|
||||
- discord.py、aiohttp(自动安装)
|
||||
- discord.py[voice]、PyNaCl、FFmpeg(语音功能可选)
|
||||
|
||||
### Discord 设置
|
||||
|
||||
本指南假设您已经创建好了 Discord 应用和 Bot。如需创建,请访问 [Discord Developer Portal](https://discord.com/developers/applications)。
|
||||
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 1. 文本消息处理
|
||||
|
||||
```python
|
||||
from pkg.platform.types import message as platform_message
|
||||
from pkg.platform.types import events as platform_events
|
||||
|
||||
@handler(platform_events.MessageEvent)
|
||||
async def text_message_handler(ctx: EventContext):
|
||||
"""处理文本消息的基本示例"""
|
||||
|
||||
# 获取适配器实例
|
||||
adapter = ctx.event.query.adapter
|
||||
|
||||
# 提取消息文本
|
||||
text_content = ""
|
||||
for element in ctx.event.message_chain:
|
||||
if isinstance(element, platform_message.Plain):
|
||||
text_content += element.text
|
||||
|
||||
# 发送回复
|
||||
reply_chain = platform_message.MessageChain([
|
||||
platform_message.Plain(f"您说了: {text_content}")
|
||||
])
|
||||
|
||||
await adapter.reply_message(ctx.event, reply_chain, quote_origin=True)
|
||||
```
|
||||
|
||||
### 2. 图像消息处理
|
||||
|
||||
```python
|
||||
@handler(platform_events.MessageEvent)
|
||||
async def image_message_handler(ctx: EventContext):
|
||||
"""处理图像消息的示例"""
|
||||
|
||||
adapter = ctx.event.query.adapter
|
||||
|
||||
# 检测消息中的图像
|
||||
image_count = 0
|
||||
for element in ctx.event.message_chain:
|
||||
if isinstance(element, platform_message.Image):
|
||||
image_count += 1
|
||||
|
||||
if image_count > 0:
|
||||
# 回复图像信息
|
||||
reply_chain = platform_message.MessageChain([
|
||||
platform_message.Plain(f"收到 {image_count} 张图片!"),
|
||||
platform_message.Plain("\\n正在处理...")
|
||||
])
|
||||
await adapter.reply_message(ctx.event, reply_chain)
|
||||
|
||||
# 可以添加图像处理逻辑
|
||||
# processed_image = process_images(images)
|
||||
|
||||
# 发送处理结果
|
||||
result_chain = platform_message.MessageChain([
|
||||
platform_message.Plain("处理完成!"),
|
||||
# platform_message.Image(path="path/to/processed/image.jpg")
|
||||
])
|
||||
await adapter.send_message("channel", ctx.event.query.channel_id, result_chain)
|
||||
```
|
||||
|
||||
### 3. 发送不同类型的图像
|
||||
|
||||
```python
|
||||
async def send_various_images(adapter, channel_id):
|
||||
"""发送不同来源的图像示例"""
|
||||
|
||||
message_chain = platform_message.MessageChain([
|
||||
platform_message.Plain("以下是不同类型的图像:\\n"),
|
||||
|
||||
# 本地文件图像
|
||||
platform_message.Plain("1. 本地文件:\\n"),
|
||||
platform_message.Image(path="/path/to/local/image.jpg"),
|
||||
|
||||
# 网络图像
|
||||
platform_message.Plain("\\n2. 网络图像:\\n"),
|
||||
platform_message.Image(url="https://example.com/image.png"),
|
||||
|
||||
# Base64 图像
|
||||
platform_message.Plain("\\n3. Base64 图像:\\n"),
|
||||
platform_message.Image(base64="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==")
|
||||
])
|
||||
|
||||
await adapter.send_message("channel", channel_id, message_chain)
|
||||
```
|
||||
|
||||
## 语音功能使用
|
||||
|
||||
### 语音连接管理
|
||||
|
||||
```python
|
||||
@handler(platform_events.MessageEvent)
|
||||
async def voice_connect_handler(ctx: EventContext):
|
||||
"""语音连接示例"""
|
||||
|
||||
adapter = ctx.event.query.adapter
|
||||
|
||||
# 解析指令
|
||||
text = ""
|
||||
for element in ctx.event.message_chain:
|
||||
if isinstance(element, platform_message.Plain):
|
||||
text += element.text
|
||||
|
||||
if "加入语音" in text:
|
||||
try:
|
||||
# 这里需要根据实际情况获取 guild_id 和 channel_id
|
||||
guild_id = get_guild_id_from_context(ctx)
|
||||
channel_id = get_voice_channel_id_from_user(ctx.event.sender.id, guild_id)
|
||||
|
||||
voice_client = await adapter.join_voice_channel(
|
||||
guild_id=guild_id,
|
||||
channel_id=channel_id,
|
||||
user_id=ctx.event.sender.id
|
||||
)
|
||||
|
||||
await ctx.reply("成功加入语音频道!")
|
||||
|
||||
except VoicePermissionError as e:
|
||||
if "user_not_in_channel" in e.missing_permissions:
|
||||
await ctx.reply("请先加入语音频道")
|
||||
elif "connect" in e.missing_permissions:
|
||||
await ctx.reply("机器人没有连接权限")
|
||||
else:
|
||||
await ctx.reply(f"权限错误: {', '.join(e.missing_permissions)}")
|
||||
|
||||
except Exception as e:
|
||||
await ctx.reply(f"连接失败: {e}")
|
||||
|
||||
elif "离开语音" in text:
|
||||
guild_id = get_guild_id_from_context(ctx)
|
||||
success = await adapter.leave_voice_channel(guild_id)
|
||||
|
||||
if success:
|
||||
await ctx.reply("已离开语音频道")
|
||||
else:
|
||||
await ctx.reply("未连接到语音频道")
|
||||
```
|
||||
|
||||
### 音频播放
|
||||
|
||||
```python
|
||||
@handler(platform_events.MessageEvent)
|
||||
async def audio_play_handler(ctx: EventContext):
|
||||
"""音频播放示例"""
|
||||
|
||||
adapter = ctx.event.query.adapter
|
||||
|
||||
# 检查是否有播放音频的指令
|
||||
text = extract_text_from_message(ctx.event.message_chain)
|
||||
|
||||
if "播放音频" in text:
|
||||
guild_id = get_guild_id_from_context(ctx)
|
||||
|
||||
# 获取语音客户端
|
||||
voice_client = await adapter.get_voice_client(guild_id)
|
||||
|
||||
if not voice_client:
|
||||
await ctx.reply("请先连接到语音频道")
|
||||
return
|
||||
|
||||
if voice_client.is_playing():
|
||||
await ctx.reply("正在播放其他音频,请稍候")
|
||||
return
|
||||
|
||||
try:
|
||||
# 播放音频文件
|
||||
import discord
|
||||
|
||||
# 配置 FFmpeg 选项
|
||||
ffmpeg_options = {
|
||||
'before_options': '-loglevel quiet',
|
||||
'options': '-vn -filter:a "volume=0.5"'
|
||||
}
|
||||
|
||||
# 创建音频源
|
||||
audio_source = discord.FFmpegPCMAudio(
|
||||
"path/to/your/audio.mp3",
|
||||
**ffmpeg_options
|
||||
)
|
||||
|
||||
# 开始播放
|
||||
voice_client.play(audio_source)
|
||||
await ctx.reply("开始播放音频")
|
||||
|
||||
except Exception as e:
|
||||
await ctx.reply(f"播放失败: {e}")
|
||||
|
||||
elif "停止播放" in text:
|
||||
guild_id = get_guild_id_from_context(ctx)
|
||||
voice_client = await adapter.get_voice_client(guild_id)
|
||||
|
||||
if voice_client and voice_client.is_playing():
|
||||
voice_client.stop()
|
||||
await ctx.reply("已停止播放")
|
||||
else:
|
||||
await ctx.reply("当前没有播放音频")
|
||||
```
|
||||
|
||||
### 语音状态查询
|
||||
|
||||
```python
|
||||
@handler(platform_events.MessageEvent)
|
||||
async def voice_status_handler(ctx: EventContext):
|
||||
"""语音状态查询示例"""
|
||||
|
||||
adapter = ctx.event.query.adapter
|
||||
text = extract_text_from_message(ctx.event.message_chain)
|
||||
|
||||
if "语音状态" in text:
|
||||
guild_id = get_guild_id_from_context(ctx)
|
||||
|
||||
# 获取详细状态
|
||||
status = await adapter.get_voice_connection_status(guild_id)
|
||||
|
||||
if status:
|
||||
status_text = f"""语音连接状态:
|
||||
频道: {status['channel_name']}
|
||||
连接时间: {status['connection_time'].strftime('%H:%M:%S')}
|
||||
延迟: {status['latency']:.2f}ms
|
||||
用户数: {status['user_count']}
|
||||
健康度: {status['connection_health']}
|
||||
状态: {status['status']}"""
|
||||
|
||||
await ctx.reply(status_text)
|
||||
else:
|
||||
await ctx.reply("当前未连接到语音频道")
|
||||
|
||||
elif "连接列表" in text:
|
||||
# 获取所有活跃连接
|
||||
connections = await adapter.list_active_voice_connections()
|
||||
|
||||
if connections:
|
||||
conn_list = "活跃语音连接:\\n"
|
||||
for i, conn in enumerate(connections, 1):
|
||||
conn_list += f"{i}. {conn['channel_name']} ({conn['user_count']} 用户)\\n"
|
||||
await ctx.reply(conn_list)
|
||||
else:
|
||||
await ctx.reply("当前没有活跃的语音连接")
|
||||
```
|
||||
|
||||
## 完整插件示例
|
||||
|
||||
以下是一个完整的 Discord 插件示例,包含所有主要功能:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
from pkg.platform.types import message as platform_message
|
||||
from pkg.platform.types import events as platform_events
|
||||
from pkg.core.entities.classes import EventContext
|
||||
from pkg.plugin import handler
|
||||
|
||||
# 辅助函数
|
||||
def extract_text_from_message(message_chain):
|
||||
"""从消息链中提取文本"""
|
||||
text = ""
|
||||
for element in message_chain:
|
||||
if isinstance(element, platform_message.Plain):
|
||||
text += element.text
|
||||
return text.strip()
|
||||
|
||||
def extract_images_from_message(message_chain):
|
||||
"""从消息链中提取图像"""
|
||||
images = []
|
||||
for element in message_chain:
|
||||
if isinstance(element, platform_message.Image):
|
||||
images.append(element)
|
||||
return images
|
||||
|
||||
# 消息处理器
|
||||
@handler(platform_events.MessageEvent)
|
||||
async def discord_message_handler(ctx: EventContext):
|
||||
"""Discord 消息处理主函数"""
|
||||
|
||||
adapter = ctx.event.query.adapter
|
||||
text = extract_text_from_message(ctx.event.message_chain)
|
||||
images = extract_images_from_message(ctx.event.message_chain)
|
||||
|
||||
# 文本处理
|
||||
if text.startswith("!echo"):
|
||||
echo_text = text[5:].strip()
|
||||
await ctx.reply(f"回声: {echo_text}")
|
||||
|
||||
# 图像处理
|
||||
elif text.startswith("!image"):
|
||||
if images:
|
||||
reply_chain = platform_message.MessageChain([
|
||||
platform_message.Plain(f"检测到 {len(images)} 张图像"),
|
||||
platform_message.Plain("\\n图像信息:\\n")
|
||||
])
|
||||
|
||||
for i, img in enumerate(images, 1):
|
||||
if img.url:
|
||||
reply_chain.append(platform_message.Plain(f"{i}. 网络图像: {img.url}\\n"))
|
||||
elif img.path:
|
||||
reply_chain.append(platform_message.Plain(f"{i}. 本地图像: {os.path.basename(img.path)}\\n"))
|
||||
elif img.base64:
|
||||
reply_chain.append(platform_message.Plain(f"{i}. Base64 图像\\n"))
|
||||
|
||||
await ctx.reply(reply_chain)
|
||||
else:
|
||||
await ctx.reply("请发送图像")
|
||||
|
||||
# 语音功能
|
||||
elif text.startswith("!voice"):
|
||||
await handle_voice_command(ctx, text[6:].strip())
|
||||
|
||||
# 帮助信息
|
||||
elif text.startswith("!help"):
|
||||
help_text = """Discord 插件命令:
|
||||
!echo <文本> - 回声功能
|
||||
!image - 分析图像信息
|
||||
!voice join - 加入语音频道
|
||||
!voice leave - 离开语音频道
|
||||
!voice play <文件> - 播放音频
|
||||
!voice stop - 停止播放
|
||||
!voice status - 查看状态
|
||||
!help - 显示此帮助"""
|
||||
await ctx.reply(help_text)
|
||||
|
||||
async def handle_voice_command(ctx: EventContext, command: str):
|
||||
"""处理语音相关命令"""
|
||||
|
||||
adapter = ctx.event.query.adapter
|
||||
guild_id = get_guild_id_from_context(ctx)
|
||||
|
||||
if command == "join":
|
||||
try:
|
||||
channel_id = get_user_voice_channel_id(ctx.event.sender.id, guild_id)
|
||||
if not channel_id:
|
||||
await ctx.reply("请先加入语音频道")
|
||||
return
|
||||
|
||||
voice_client = await adapter.join_voice_channel(
|
||||
guild_id=guild_id,
|
||||
channel_id=channel_id,
|
||||
user_id=ctx.event.sender.id
|
||||
)
|
||||
|
||||
await ctx.reply("成功加入语音频道!")
|
||||
|
||||
except Exception as e:
|
||||
await ctx.reply(f"加入失败: {e}")
|
||||
|
||||
elif command == "leave":
|
||||
success = await adapter.leave_voice_channel(guild_id)
|
||||
if success:
|
||||
await ctx.reply("已离开语音频道")
|
||||
else:
|
||||
await ctx.reply("未连接到语音频道")
|
||||
|
||||
elif command.startswith("play"):
|
||||
audio_file = command[4:].strip()
|
||||
if not audio_file:
|
||||
audio_file = "path/to/default/audio.mp3"
|
||||
|
||||
voice_client = await adapter.get_voice_client(guild_id)
|
||||
if not voice_client:
|
||||
await ctx.reply("请先加入语音频道")
|
||||
return
|
||||
|
||||
if voice_client.is_playing():
|
||||
await ctx.reply("正在播放其他音频")
|
||||
return
|
||||
|
||||
try:
|
||||
import discord
|
||||
audio_source = discord.FFmpegPCMAudio(audio_file)
|
||||
voice_client.play(audio_source)
|
||||
await ctx.reply(f"开始播放: {os.path.basename(audio_file)}")
|
||||
except Exception as e:
|
||||
await ctx.reply(f"播放失败: {e}")
|
||||
|
||||
elif command == "stop":
|
||||
voice_client = await adapter.get_voice_client(guild_id)
|
||||
if voice_client and voice_client.is_playing():
|
||||
voice_client.stop()
|
||||
await ctx.reply("已停止播放")
|
||||
else:
|
||||
await ctx.reply("当前没有播放音频")
|
||||
|
||||
elif command == "status":
|
||||
status = await adapter.get_voice_connection_status(guild_id)
|
||||
if status:
|
||||
status_text = f"""语音状态:
|
||||
频道: {status['channel_name']}
|
||||
延迟: {status['latency']:.2f}ms
|
||||
用户数: {status['user_count']}
|
||||
状态: {status['status']}"""
|
||||
await ctx.reply(status_text)
|
||||
else:
|
||||
await ctx.reply("未连接到语音频道")
|
||||
|
||||
else:
|
||||
await ctx.reply("未知的语音命令,使用 !help 查看帮助")
|
||||
|
||||
# 辅助函数实现(需要根据实际情况调整)
|
||||
def get_guild_id_from_context(ctx):
|
||||
"""从上下文获取 guild_id"""
|
||||
# 这里需要根据实际的 LangBot 实现来获取 guild_id
|
||||
# 可能需要从 ctx.event.query 或其他地方获取
|
||||
return ctx.event.query.channel_id # 示例实现
|
||||
|
||||
def get_user_voice_channel_id(user_id, guild_id):
|
||||
"""获取用户当前所在的语音频道 ID"""
|
||||
# 这里需要实现获取用户语音频道的逻辑
|
||||
# 可能需要通过 Discord API 查询
|
||||
return None # 示例实现,实际需要实现具体逻辑
|
||||
```
|
||||
|
||||
## 错误处理最佳实践
|
||||
|
||||
### 语音错误处理
|
||||
|
||||
```python
|
||||
async def safe_voice_operation(adapter, operation, *args, **kwargs):
|
||||
"""安全的语音操作包装器"""
|
||||
try:
|
||||
return await operation(*args, **kwargs)
|
||||
except VoicePermissionError as e:
|
||||
if "user_not_in_channel" in e.missing_permissions:
|
||||
return "错误: 用户不在语音频道中"
|
||||
elif "connect" in e.missing_permissions:
|
||||
return "错误: 机器人没有连接权限"
|
||||
elif "speak" in e.missing_permissions:
|
||||
return "错误: 机器人没有发言权限"
|
||||
else:
|
||||
return f"权限错误: {', '.join(e.missing_permissions)}"
|
||||
except VoiceNetworkError as e:
|
||||
return f"网络错误: {e}"
|
||||
except Exception as e:
|
||||
return f"未知错误: {e}"
|
||||
```
|
||||
|
||||
### 图像错误处理
|
||||
|
||||
```python
|
||||
async def safe_image_processing(message_chain):
|
||||
"""安全的图像处理"""
|
||||
processed_images = []
|
||||
errors = []
|
||||
|
||||
for element in message_chain:
|
||||
if isinstance(element, platform_message.Image):
|
||||
try:
|
||||
# 处理图像
|
||||
if element.url:
|
||||
# 处理网络图像
|
||||
pass
|
||||
elif element.path:
|
||||
# 处理本地图像
|
||||
if not os.path.exists(element.path):
|
||||
errors.append(f"文件不存在: {element.path}")
|
||||
continue
|
||||
elif element.base64:
|
||||
# 处理 Base64 图像
|
||||
pass
|
||||
|
||||
processed_images.append(element)
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"图像处理失败: {e}")
|
||||
|
||||
return processed_images, errors
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 异步图像下载
|
||||
|
||||
```python
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
async def download_images_concurrently(image_urls, max_concurrent=5):
|
||||
"""并发下载多张图像"""
|
||||
semaphore = asyncio.Semaphore(max_concurrent)
|
||||
|
||||
async def download_single(session, url):
|
||||
async with semaphore:
|
||||
try:
|
||||
async with session.get(url) as response:
|
||||
return await response.read()
|
||||
except Exception as e:
|
||||
print(f"下载失败 {url}: {e}")
|
||||
return None
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = [download_single(session, url) for url in image_urls]
|
||||
return await asyncio.gather(*tasks)
|
||||
```
|
||||
|
||||
### 语音连接复用
|
||||
|
||||
```python
|
||||
class VoiceConnectionPool:
|
||||
"""语音连接池管理"""
|
||||
|
||||
def __init__(self, adapter):
|
||||
self.adapter = adapter
|
||||
self.connections = {}
|
||||
|
||||
async def get_connection(self, guild_id, channel_id, user_id=None):
|
||||
"""获取或创建语音连接"""
|
||||
if guild_id in self.connections:
|
||||
# 检查现有连接是否有效
|
||||
if await self.adapter.is_connected_to_voice(guild_id):
|
||||
return self.connections[guild_id]
|
||||
|
||||
# 创建新连接
|
||||
voice_client = await self.adapter.join_voice_channel(
|
||||
guild_id, channel_id, user_id
|
||||
)
|
||||
self.connections[guild_id] = voice_client
|
||||
return voice_client
|
||||
|
||||
async def cleanup_invalid_connections(self):
|
||||
"""清理无效连接"""
|
||||
invalid_guilds = []
|
||||
for guild_id in self.connections:
|
||||
if not await self.adapter.is_connected_to_voice(guild_id):
|
||||
invalid_guilds.append(guild_id)
|
||||
|
||||
for guild_id in invalid_guilds:
|
||||
del self.connections[guild_id]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **提示**: 此快速入门基于实际的 discord.py 实现。建议在开发过程中参考 [Discord Developer Documentation](https://discord.com/developers/docs) 了解更多 Discord API 详情。
|
||||
@@ -0,0 +1,914 @@
|
||||
---
|
||||
title: "Discord 适配器故障排除"
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文档提供 Discord 适配器常见问题的诊断和解决方案,涵盖连接问题、消息处理、图像功能、语音连接等各个方面。
|
||||
|
||||
## 连接相关问题
|
||||
|
||||
### 1. 无法启动 Discord 适配器
|
||||
|
||||
#### 症状
|
||||
- 适配器启动失败
|
||||
- 连接超时错误
|
||||
- 认证失败
|
||||
|
||||
#### 可能原因和解决方案
|
||||
|
||||
##### 1.1 Token 配置错误
|
||||
|
||||
```python
|
||||
# 错误信息示例
|
||||
discord.errors.LoginFailure: Improper token has been passed.
|
||||
|
||||
# 解决方案
|
||||
1. 检查配置文件中的 token 是否正确
|
||||
2. 确认 token 没有包含额外的空格或特殊字符
|
||||
3. 验证 token 是否仍然有效
|
||||
|
||||
# 验证 token 的方法
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
async def verify_token(token):
|
||||
headers = {'Authorization': f'Bot {token}'}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('https://discord.com/api/v10/users/@me', headers=headers) as response:
|
||||
if response.status == 200:
|
||||
print("Token 有效")
|
||||
data = await response.json()
|
||||
print(f"Bot 用户名: {data['username']}")
|
||||
else:
|
||||
print(f"Token 无效: {response.status}")
|
||||
```
|
||||
|
||||
##### 1.2 网络连接问题
|
||||
|
||||
```python
|
||||
# 错误信息示例
|
||||
aiohttp.client_exceptions.ClientConnectorError: Cannot connect to host discord.com
|
||||
|
||||
# 解决方案
|
||||
1. 检查网络连接
|
||||
2. 配置代理(如果需要)
|
||||
3. 检查防火墙设置
|
||||
|
||||
# 代理配置示例
|
||||
export http_proxy="http://proxy.example.com:8080"
|
||||
export https_proxy="http://proxy.example.com:8080"
|
||||
|
||||
# 或在代码中设置
|
||||
import os
|
||||
os.environ['http_proxy'] = 'http://proxy.example.com:8080'
|
||||
```
|
||||
|
||||
##### 1.3 权限配置问题
|
||||
|
||||
```python
|
||||
# 确保 Bot 具有必要权限
|
||||
required_permissions = [
|
||||
'Read Messages',
|
||||
'Send Messages',
|
||||
'Embed Links',
|
||||
'Attach Files',
|
||||
'Read Message History'
|
||||
]
|
||||
|
||||
# 语音功能额外权限
|
||||
voice_permissions = [
|
||||
'Connect',
|
||||
'Speak',
|
||||
'Use Voice Activity'
|
||||
]
|
||||
```
|
||||
|
||||
### 2. 连接频繁断开
|
||||
|
||||
#### 症状
|
||||
- WebSocket 连接频繁中断
|
||||
- 消息发送失败
|
||||
- 事件接收延迟
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```python
|
||||
# 启用详细日志查看连接状态
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# 检查网络稳定性
|
||||
async def network_stability_test():
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
for i in range(10):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
async with session.get('https://discord.com/api/v10/gateway') as response:
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
latency = (end_time - start_time) * 1000
|
||||
print(f"测试 {i+1}: {response.status}, 延迟: {latency:.2f}ms")
|
||||
except Exception as e:
|
||||
print(f"测试 {i+1} 失败: {e}")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
```
|
||||
|
||||
## 消息处理问题
|
||||
|
||||
### 1. 消息发送失败
|
||||
|
||||
#### 症状
|
||||
- 发送消息时抛出异常
|
||||
- 消息内容被截断
|
||||
- 消息格式错误
|
||||
|
||||
#### 常见问题和解决方案
|
||||
|
||||
##### 1.1 消息长度超限
|
||||
|
||||
```python
|
||||
# Discord 消息长度限制: 2000 字符
|
||||
def check_message_length(text):
|
||||
if len(text) > 2000:
|
||||
print(f"消息过长: {len(text)} 字符,需要分割")
|
||||
return False
|
||||
return True
|
||||
|
||||
# 自动分割长消息
|
||||
def split_long_message(text, max_length=2000):
|
||||
chunks = []
|
||||
while len(text) > max_length:
|
||||
# 找到合适的分割点
|
||||
split_pos = text.rfind('\n', 0, max_length)
|
||||
if split_pos == -1:
|
||||
split_pos = text.rfind(' ', 0, max_length)
|
||||
if split_pos == -1:
|
||||
split_pos = max_length
|
||||
|
||||
chunks.append(text[:split_pos])
|
||||
text = text[split_pos:].lstrip()
|
||||
|
||||
if text:
|
||||
chunks.append(text)
|
||||
|
||||
return chunks
|
||||
|
||||
# 使用示例
|
||||
async def send_long_message(adapter, channel_id, long_text):
|
||||
chunks = split_long_message(long_text)
|
||||
for chunk in chunks:
|
||||
await adapter.send_message("channel", channel_id, MessageChain([Plain(chunk)]))
|
||||
await asyncio.sleep(0.5) # 避免速率限制
|
||||
```
|
||||
|
||||
##### 1.2 频道权限不足
|
||||
|
||||
```python
|
||||
# 检查 Bot 在频道中的权限
|
||||
async def check_channel_permissions(bot, channel_id):
|
||||
try:
|
||||
channel = bot.get_channel(channel_id)
|
||||
if not channel:
|
||||
channel = await bot.fetch_channel(channel_id)
|
||||
|
||||
permissions = channel.permissions_for(channel.guild.me)
|
||||
|
||||
required_perms = {
|
||||
'send_messages': permissions.send_messages,
|
||||
'embed_links': permissions.embed_links,
|
||||
'attach_files': permissions.attach_files,
|
||||
'read_messages': permissions.read_messages
|
||||
}
|
||||
|
||||
print("频道权限检查:")
|
||||
for perm, has_perm in required_perms.items():
|
||||
status = "✓" if has_perm else "✗"
|
||||
print(f" {status} {perm}: {has_perm}")
|
||||
|
||||
return all(required_perms.values())
|
||||
|
||||
except Exception as e:
|
||||
print(f"权限检查失败: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
### 2. 消息接收问题
|
||||
|
||||
#### 症状
|
||||
- 收不到用户消息
|
||||
- 事件处理延迟
|
||||
- 消息内容缺失
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```python
|
||||
# 检查 Intents 配置
|
||||
import discord
|
||||
|
||||
# 确保启用了必要的 intents
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True # 必须启用才能读取消息内容
|
||||
intents.guild_messages = True
|
||||
intents.dm_messages = True
|
||||
|
||||
# 在 Discord Developer Portal 中也需要启用对应的 Privileged Gateway Intents
|
||||
|
||||
# 调试消息接收
|
||||
class DebugClient(discord.Client):
|
||||
async def on_message(self, message):
|
||||
print(f"收到消息: {message.author} -> {message.content}")
|
||||
if message.author.bot:
|
||||
print("跳过 Bot 消息")
|
||||
return
|
||||
|
||||
# 处理消息...
|
||||
```
|
||||
|
||||
## 图像处理问题
|
||||
|
||||
### 1. 图像发送失败
|
||||
|
||||
#### 症状
|
||||
- 图像无法上传
|
||||
- 图像格式错误
|
||||
- 文件大小超限
|
||||
|
||||
#### 解决方案
|
||||
|
||||
##### 1.1 文件大小检查
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
def check_image_size(image_path, max_size=25*1024*1024): # 25MB 限制
|
||||
"""检查图像文件大小"""
|
||||
if not os.path.exists(image_path):
|
||||
return False, "文件不存在"
|
||||
|
||||
size = os.path.getsize(image_path)
|
||||
if size > max_size:
|
||||
return False, f"文件过大: {size/1024/1024:.2f}MB, 限制: {max_size/1024/1024}MB"
|
||||
|
||||
return True, "文件大小正常"
|
||||
|
||||
# 图像压缩示例
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
def compress_image(image_path, max_size=1024, quality=85):
|
||||
"""压缩图像以减小文件大小"""
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
# 调整尺寸
|
||||
if max(img.size) > max_size:
|
||||
ratio = max_size / max(img.size)
|
||||
new_size = tuple(int(dim * ratio) for dim in img.size)
|
||||
img = img.resize(new_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# 保存为 JPEG 以减小文件大小
|
||||
output = io.BytesIO()
|
||||
img.convert('RGB').save(output, format='JPEG', quality=quality, optimize=True)
|
||||
return output.getvalue()
|
||||
|
||||
except Exception as e:
|
||||
print(f"图像压缩失败: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
##### 1.2 格式检测和转换
|
||||
|
||||
```python
|
||||
def detect_image_format(image_data):
|
||||
"""检测图像格式"""
|
||||
if image_data.startswith(b'\xff\xd8\xff'):
|
||||
return 'jpeg'
|
||||
elif image_data.startswith(b'\x89PNG\r\n\x1a\n'):
|
||||
return 'png'
|
||||
elif image_data.startswith(b'GIF87a') or image_data.startswith(b'GIF89a'):
|
||||
return 'gif'
|
||||
elif image_data.startswith(b'RIFF') and b'WEBP' in image_data[:20]:
|
||||
return 'webp'
|
||||
else:
|
||||
return 'unknown'
|
||||
|
||||
# 安全的图像读取
|
||||
async def safe_image_processing(image_element):
|
||||
"""安全地处理图像元素"""
|
||||
try:
|
||||
image_data = None
|
||||
filename = f'{uuid.uuid4()}.png'
|
||||
|
||||
if image_element.base64:
|
||||
# 处理 Base64 图像
|
||||
if image_element.base64.startswith('data:'):
|
||||
base64_data = image_element.base64.split(',')[1]
|
||||
else:
|
||||
base64_data = image_element.base64
|
||||
|
||||
image_data = base64.b64decode(base64_data)
|
||||
|
||||
elif image_element.url:
|
||||
# 下载网络图像
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(image_element.url, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
image_data = await response.read()
|
||||
else:
|
||||
raise Exception(f"下载失败: HTTP {response.status}")
|
||||
|
||||
elif image_element.path:
|
||||
# 读取本地文件
|
||||
clean_path = os.path.abspath(image_element.path.replace('\x00', ''))
|
||||
if os.path.exists(clean_path):
|
||||
with open(clean_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
else:
|
||||
raise Exception(f"文件不存在: {clean_path}")
|
||||
|
||||
if image_data:
|
||||
# 检测格式并设置文件名
|
||||
format_name = detect_image_format(image_data)
|
||||
if format_name != 'unknown':
|
||||
filename = f'{uuid.uuid4()}.{format_name}'
|
||||
|
||||
# 检查文件大小
|
||||
if len(image_data) > 25*1024*1024: # 25MB
|
||||
raise Exception(f"图像过大: {len(image_data)/1024/1024:.2f}MB")
|
||||
|
||||
return discord.File(fp=io.BytesIO(image_data), filename=filename)
|
||||
|
||||
except Exception as e:
|
||||
print(f"图像处理失败: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
### 2. 网络图像下载问题
|
||||
|
||||
#### 症状
|
||||
- 图像下载超时
|
||||
- 网络连接错误
|
||||
- 图像内容损坏
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```python
|
||||
async def robust_image_download(url, max_retries=3, timeout=30):
|
||||
"""健壮的图像下载"""
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session:
|
||||
async with session.get(url, headers={'User-Agent': 'LangBot/1.0'}) as response:
|
||||
if response.status == 200:
|
||||
content_length = response.headers.get('Content-Length')
|
||||
if content_length and int(content_length) > 25*1024*1024:
|
||||
raise Exception("图像文件过大")
|
||||
|
||||
image_data = await response.read()
|
||||
|
||||
# 验证图像数据
|
||||
if len(image_data) < 100: # 太小可能不是有效图像
|
||||
raise Exception("图像数据太小")
|
||||
|
||||
# 检查格式
|
||||
if detect_image_format(image_data) == 'unknown':
|
||||
raise Exception("不支持的图像格式")
|
||||
|
||||
return image_data
|
||||
else:
|
||||
raise Exception(f"HTTP错误: {response.status}")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f"下载超时 (尝试 {attempt + 1}/{max_retries}): {url}")
|
||||
except Exception as e:
|
||||
print(f"下载失败 (尝试 {attempt + 1}/{max_retries}): {e}")
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(2 ** attempt) # 指数退避
|
||||
|
||||
raise Exception(f"下载失败,已重试 {max_retries} 次")
|
||||
```
|
||||
|
||||
## 语音功能问题
|
||||
|
||||
### 1. 语音连接失败
|
||||
|
||||
#### 症状
|
||||
- 无法加入语音频道
|
||||
- 连接超时
|
||||
- 权限错误
|
||||
|
||||
#### 解决方案
|
||||
|
||||
##### 1.1 依赖检查
|
||||
|
||||
```python
|
||||
def check_voice_dependencies():
|
||||
"""检查语音功能依赖"""
|
||||
try:
|
||||
import discord.voice_client
|
||||
print("✓ discord.py 语音支持正常")
|
||||
except ImportError:
|
||||
print("✗ discord.py 语音支持未安装,请运行: pip install discord.py[voice]")
|
||||
return False
|
||||
|
||||
try:
|
||||
import nacl
|
||||
print("✓ PyNaCl 已安装")
|
||||
except ImportError:
|
||||
print("✗ PyNaCl 未安装,请运行: pip install PyNaCl")
|
||||
return False
|
||||
|
||||
# 检查 FFmpeg
|
||||
import shutil
|
||||
if shutil.which('ffmpeg'):
|
||||
print("✓ FFmpeg 已安装")
|
||||
else:
|
||||
print("✗ FFmpeg 未安装,请安装 FFmpeg")
|
||||
return False
|
||||
|
||||
try:
|
||||
import discord
|
||||
if not discord.opus.is_loaded():
|
||||
discord.opus.load_opus()
|
||||
print("✓ Opus 编码器已加载")
|
||||
except Exception as e:
|
||||
print(f"✗ Opus 编码器加载失败: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
##### 1.2 权限诊断
|
||||
|
||||
```python
|
||||
async def diagnose_voice_permissions(bot, guild_id, channel_id, user_id):
|
||||
"""诊断语音权限问题"""
|
||||
try:
|
||||
guild = bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return "错误: 找不到服务器"
|
||||
|
||||
channel = guild.get_channel(channel_id)
|
||||
if not channel or not isinstance(channel, discord.VoiceChannel):
|
||||
return "错误: 找不到语音频道"
|
||||
|
||||
# 检查用户是否在频道中
|
||||
member = guild.get_member(user_id)
|
||||
if not member:
|
||||
return "错误: 找不到用户"
|
||||
|
||||
if not member.voice or member.voice.channel != channel:
|
||||
return "错误: 用户不在指定语音频道中"
|
||||
|
||||
# 检查 Bot 权限
|
||||
bot_member = guild.me
|
||||
permissions = channel.permissions_for(bot_member)
|
||||
|
||||
missing_perms = []
|
||||
if not permissions.connect:
|
||||
missing_perms.append("Connect")
|
||||
if not permissions.speak:
|
||||
missing_perms.append("Speak")
|
||||
if not permissions.use_voice_activation:
|
||||
missing_perms.append("Use Voice Activity")
|
||||
|
||||
if missing_perms:
|
||||
return f"权限不足: {', '.join(missing_perms)}"
|
||||
|
||||
return "权限检查通过"
|
||||
|
||||
except Exception as e:
|
||||
return f"权限检查失败: {e}"
|
||||
```
|
||||
|
||||
### 2. 音频播放问题
|
||||
|
||||
#### 症状
|
||||
- 音频无法播放
|
||||
- 播放中断
|
||||
- 音质问题
|
||||
|
||||
#### 解决方案
|
||||
|
||||
##### 2.1 音频文件检查
|
||||
|
||||
```python
|
||||
def check_audio_file(file_path):
|
||||
"""检查音频文件有效性"""
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return False, "文件不存在"
|
||||
|
||||
# 使用 FFmpeg 检查文件
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'ffprobe', '-v', 'quiet', '-print_format', 'json',
|
||||
'-show_format', '-show_streams', file_path
|
||||
], capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, "不是有效的音频文件"
|
||||
|
||||
import json
|
||||
info = json.loads(result.stdout)
|
||||
|
||||
# 检查是否有音频流
|
||||
audio_streams = [s for s in info['streams'] if s['codec_type'] == 'audio']
|
||||
if not audio_streams:
|
||||
return False, "文件中没有音频流"
|
||||
|
||||
return True, f"音频文件正常 ({info['format']['format_name']})"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "文件检查超时"
|
||||
except Exception as e:
|
||||
return False, f"文件检查失败: {e}"
|
||||
```
|
||||
|
||||
##### 2.2 音频播放优化
|
||||
|
||||
```python
|
||||
async def play_audio_with_error_handling(voice_client, audio_path):
|
||||
"""带错误处理的音频播放"""
|
||||
|
||||
if not voice_client or not voice_client.is_connected():
|
||||
raise Exception("语音客户端未连接")
|
||||
|
||||
if voice_client.is_playing():
|
||||
voice_client.stop()
|
||||
await asyncio.sleep(0.5) # 等待停止完成
|
||||
|
||||
# 检查音频文件
|
||||
is_valid, message = check_audio_file(audio_path)
|
||||
if not is_valid:
|
||||
raise Exception(f"音频文件无效: {message}")
|
||||
|
||||
try:
|
||||
# 优化的 FFmpeg 选项
|
||||
ffmpeg_options = {
|
||||
'before_options': (
|
||||
'-loglevel quiet '
|
||||
'-reconnect 1 '
|
||||
'-reconnect_streamed 1 '
|
||||
'-reconnect_delay_max 5'
|
||||
),
|
||||
'options': (
|
||||
'-vn ' # 禁用视频
|
||||
'-filter:a "volume=0.5" ' # 音量控制
|
||||
'-ac 2 ' # 立体声
|
||||
'-ar 48000 ' # 采样率
|
||||
'-f s16le' # 输出格式
|
||||
)
|
||||
}
|
||||
|
||||
audio_source = discord.FFmpegPCMAudio(audio_path, **ffmpeg_options)
|
||||
|
||||
# 播放音频
|
||||
voice_client.play(audio_source)
|
||||
|
||||
# 监控播放状态
|
||||
while voice_client.is_playing():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
print("音频播放完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"音频播放失败: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
### 3. 连接稳定性问题
|
||||
|
||||
#### 症状
|
||||
- 连接频繁断开
|
||||
- 延迟过高
|
||||
- 音频卡顿
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```python
|
||||
async def monitor_voice_connection(adapter, guild_id, monitoring_duration=60):
|
||||
"""监控语音连接质量"""
|
||||
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
latencies = []
|
||||
disconnection_count = 0
|
||||
|
||||
while asyncio.get_event_loop().time() - start_time < monitoring_duration:
|
||||
try:
|
||||
status = await adapter.get_voice_connection_status(guild_id)
|
||||
|
||||
if status:
|
||||
latencies.append(status['latency'])
|
||||
print(f"延迟: {status['latency']:.2f}ms, 健康度: {status['connection_health']}")
|
||||
else:
|
||||
disconnection_count += 1
|
||||
print("连接已断开")
|
||||
|
||||
# 尝试重连
|
||||
if disconnection_count < 3:
|
||||
print("尝试重连...")
|
||||
# 这里需要实现重连逻辑
|
||||
|
||||
except Exception as e:
|
||||
print(f"监控错误: {e}")
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if latencies:
|
||||
avg_latency = sum(latencies) / len(latencies)
|
||||
max_latency = max(latencies)
|
||||
print(f"平均延迟: {avg_latency:.2f}ms, 最大延迟: {max_latency:.2f}ms")
|
||||
print(f"断开次数: {disconnection_count}")
|
||||
```
|
||||
|
||||
## 性能问题
|
||||
|
||||
### 1. 内存使用过高
|
||||
|
||||
#### 症状
|
||||
- 内存占用持续增长
|
||||
- 程序响应变慢
|
||||
- 内存溢出错误
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```python
|
||||
import gc
|
||||
import psutil
|
||||
import os
|
||||
|
||||
def monitor_memory_usage():
|
||||
"""监控内存使用情况"""
|
||||
process = psutil.Process(os.getpid())
|
||||
memory_info = process.memory_info()
|
||||
memory_mb = memory_info.rss / 1024 / 1024
|
||||
|
||||
print(f"内存使用: {memory_mb:.2f} MB")
|
||||
return memory_mb
|
||||
|
||||
async def cleanup_resources(adapter):
|
||||
"""清理资源"""
|
||||
# 清理语音连接
|
||||
if hasattr(adapter, 'voice_manager') and adapter.voice_manager:
|
||||
await adapter.voice_manager.cleanup_inactive_connections()
|
||||
|
||||
# 强制垃圾回收
|
||||
gc.collect()
|
||||
|
||||
print(f"清理后内存使用: {monitor_memory_usage():.2f} MB")
|
||||
|
||||
# 定期内存监控
|
||||
async def memory_monitor_task(adapter, interval=300): # 5分钟检查一次
|
||||
"""内存监控任务"""
|
||||
while True:
|
||||
memory_usage = monitor_memory_usage()
|
||||
|
||||
if memory_usage > 500: # 500MB 阈值
|
||||
print("内存使用过高,执行清理...")
|
||||
await cleanup_resources(adapter)
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
```
|
||||
|
||||
### 2. 连接数过多
|
||||
|
||||
#### 症状
|
||||
- 达到连接限制
|
||||
- 新连接失败
|
||||
- 性能下降
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```python
|
||||
class ConnectionLimiter:
|
||||
"""连接数限制器"""
|
||||
|
||||
def __init__(self, max_connections=10):
|
||||
self.max_connections = max_connections
|
||||
self.active_connections = {}
|
||||
|
||||
async def acquire_connection(self, guild_id, adapter):
|
||||
"""获取连接"""
|
||||
if len(self.active_connections) >= self.max_connections:
|
||||
# 断开最旧的连接
|
||||
oldest_guild = min(self.active_connections.keys(),
|
||||
key=lambda g: self.active_connections[g]['created_at'])
|
||||
|
||||
await adapter.leave_voice_channel(oldest_guild)
|
||||
del self.active_connections[oldest_guild]
|
||||
print(f"断开最旧连接: {oldest_guild}")
|
||||
|
||||
self.active_connections[guild_id] = {
|
||||
'created_at': asyncio.get_event_loop().time(),
|
||||
'last_activity': asyncio.get_event_loop().time()
|
||||
}
|
||||
|
||||
def release_connection(self, guild_id):
|
||||
"""释放连接"""
|
||||
if guild_id in self.active_connections:
|
||||
del self.active_connections[guild_id]
|
||||
|
||||
def update_activity(self, guild_id):
|
||||
"""更新活动时间"""
|
||||
if guild_id in self.active_connections:
|
||||
self.active_connections[guild_id]['last_activity'] = asyncio.get_event_loop().time()
|
||||
```
|
||||
|
||||
## 调试工具
|
||||
|
||||
### 1. 详细日志配置
|
||||
|
||||
```python
|
||||
import logging
|
||||
import sys
|
||||
|
||||
def setup_detailed_logging():
|
||||
"""设置详细日志"""
|
||||
|
||||
# 创建格式化器
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
|
||||
)
|
||||
|
||||
# 控制台处理器
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# 文件处理器
|
||||
file_handler = logging.FileHandler('discord_debug.log', encoding='utf-8')
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# Discord.py 日志
|
||||
discord_logger = logging.getLogger('discord')
|
||||
discord_logger.setLevel(logging.DEBUG)
|
||||
discord_logger.addHandler(console_handler)
|
||||
discord_logger.addHandler(file_handler)
|
||||
|
||||
# 适配器日志
|
||||
adapter_logger = logging.getLogger('discord_adapter')
|
||||
adapter_logger.setLevel(logging.DEBUG)
|
||||
adapter_logger.addHandler(console_handler)
|
||||
adapter_logger.addHandler(file_handler)
|
||||
|
||||
return discord_logger, adapter_logger
|
||||
```
|
||||
|
||||
### 2. 连接诊断工具
|
||||
|
||||
```python
|
||||
async def comprehensive_diagnostic(adapter):
|
||||
"""综合诊断工具"""
|
||||
|
||||
print("=== Discord 适配器诊断 ===")
|
||||
|
||||
# 1. 基础连接检查
|
||||
print("1. 基础连接状态:")
|
||||
if hasattr(adapter, 'bot') and adapter.bot.is_ready():
|
||||
print(" ✓ Discord 连接正常")
|
||||
print(f" ✓ Bot 用户: {adapter.bot.user}")
|
||||
print(f" ✓ 延迟: {adapter.bot.latency*1000:.2f}ms")
|
||||
else:
|
||||
print(" ✗ Discord 连接异常")
|
||||
|
||||
# 2. 语音功能检查
|
||||
print("\\n2. 语音功能状态:")
|
||||
voice_deps_ok = check_voice_dependencies()
|
||||
if voice_deps_ok:
|
||||
print(" ✓ 语音依赖正常")
|
||||
else:
|
||||
print(" ✗ 语音依赖异常")
|
||||
|
||||
if hasattr(adapter, 'voice_manager') and adapter.voice_manager:
|
||||
connections = await adapter.list_active_voice_connections()
|
||||
print(f" ✓ 语音管理器正常,活跃连接: {len(connections)}")
|
||||
for conn in connections:
|
||||
print(f" - {conn['guild_id']}: {conn['channel_name']} ({conn['latency']:.2f}ms)")
|
||||
else:
|
||||
print(" ✗ 语音管理器未初始化")
|
||||
|
||||
# 3. 内存使用检查
|
||||
print("\\n3. 资源使用状态:")
|
||||
memory_usage = monitor_memory_usage()
|
||||
print(f" 内存使用: {memory_usage:.2f} MB")
|
||||
|
||||
# 4. 网络连接测试
|
||||
print("\\n4. 网络连接测试:")
|
||||
await network_stability_test()
|
||||
|
||||
print("\\n=== 诊断完成 ===")
|
||||
```
|
||||
|
||||
### 3. 实时监控面板
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
class DiscordMonitor:
|
||||
"""Discord 适配器实时监控"""
|
||||
|
||||
def __init__(self, adapter):
|
||||
self.adapter = adapter
|
||||
self.stats = {
|
||||
'messages_sent': 0,
|
||||
'messages_received': 0,
|
||||
'voice_connections': 0,
|
||||
'errors': 0,
|
||||
'start_time': time.time()
|
||||
}
|
||||
|
||||
async def start_monitoring(self):
|
||||
"""启动监控"""
|
||||
while True:
|
||||
await self.display_status()
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def display_status(self):
|
||||
"""显示状态信息"""
|
||||
uptime = time.time() - self.stats['start_time']
|
||||
uptime_str = f"{int(uptime//3600)}h {int((uptime%3600)//60)}m {int(uptime%60)}s"
|
||||
|
||||
print("\\n" + "="*50)
|
||||
print(f"Discord 适配器监控 - 运行时间: {uptime_str}")
|
||||
print("="*50)
|
||||
|
||||
# 基础状态
|
||||
if hasattr(self.adapter, 'bot') and self.adapter.bot.is_ready():
|
||||
print(f"连接状态: ✓ 正常 (延迟: {self.adapter.bot.latency*1000:.2f}ms)")
|
||||
else:
|
||||
print("连接状态: ✗ 异常")
|
||||
|
||||
# 统计信息
|
||||
print(f"消息发送: {self.stats['messages_sent']}")
|
||||
print(f"消息接收: {self.stats['messages_received']}")
|
||||
print(f"语音连接: {self.stats['voice_connections']}")
|
||||
print(f"错误次数: {self.stats['errors']}")
|
||||
|
||||
# 内存使用
|
||||
memory = monitor_memory_usage()
|
||||
print(f"内存使用: {memory:.2f} MB")
|
||||
|
||||
# 语音连接详情
|
||||
if hasattr(self.adapter, 'voice_manager') and self.adapter.voice_manager:
|
||||
connections = await self.adapter.list_active_voice_connections()
|
||||
if connections:
|
||||
print("\\n语音连接:")
|
||||
for conn in connections:
|
||||
print(f" {conn['guild_id']}: {conn['channel_name']} "
|
||||
f"({conn['user_count']} 用户, {conn['latency']:.2f}ms)")
|
||||
|
||||
print("="*50)
|
||||
|
||||
def record_message_sent(self):
|
||||
self.stats['messages_sent'] += 1
|
||||
|
||||
def record_message_received(self):
|
||||
self.stats['messages_received'] += 1
|
||||
|
||||
def record_voice_connection(self):
|
||||
self.stats['voice_connections'] += 1
|
||||
|
||||
def record_error(self):
|
||||
self.stats['errors'] += 1
|
||||
|
||||
# 使用示例
|
||||
async def run_with_monitoring(adapter):
|
||||
monitor = DiscordMonitor(adapter)
|
||||
|
||||
# 启动监控任务
|
||||
monitor_task = asyncio.create_task(monitor.start_monitoring())
|
||||
|
||||
try:
|
||||
# 运行适配器
|
||||
await adapter.run_async()
|
||||
finally:
|
||||
monitor_task.cancel()
|
||||
```
|
||||
|
||||
## 常见问题速查表
|
||||
|
||||
| 问题类型 | 症状 | 快速检查 | 解决方案 |
|
||||
|---------|------|----------|----------|
|
||||
| 连接失败 | LoginFailure | 检查 token | 验证并更新 token |
|
||||
| 网络超时 | ClientConnectorError | 检查网络 | 配置代理或检查防火墙 |
|
||||
| 权限不足 | Forbidden 403 | 检查 Bot 权限 | 在 Discord 中添加权限 |
|
||||
| 消息过长 | 消息被截断 | 检查字符数 | 分割长消息 |
|
||||
| 图像失败 | 文件上传错误 | 检查文件大小/格式 | 压缩或转换格式 |
|
||||
| 语音连接失败 | VoiceConnectionError | 检查依赖和权限 | 安装依赖,检查频道权限 |
|
||||
| 音频播放失败 | FFmpeg 错误 | 检查 FFmpeg 和文件 | 安装 FFmpeg,验证音频文件 |
|
||||
| 内存泄漏 | 内存持续增长 | 监控内存使用 | 清理连接,执行 GC |
|
||||
| 高延迟 | 响应慢 | 检查网络延迟 | 优化网络,减少并发 |
|
||||
|
||||
---
|
||||
|
||||
> **提示**: 遇到问题时,首先启用详细日志记录,然后使用诊断工具进行系统性排查。大多数问题都与配置、权限或网络环境相关。
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
title: "AI 助手开发上下文"
|
||||
description: "可直接整页复制、粘贴给 AI 编程助手的 LangBot 项目上下文:目录结构、组件组织方式、运行期连接关系与开发环境配置。"
|
||||
---
|
||||
|
||||
<Info title="本页面是为 AI 编程助手(Claude Code / Cursor / Copilot / Codex 等)准备的。">
|
||||
直接把本页**全部内容**复制粘贴给你的 AI 助手,它即可快速了解 LangBot 的整体架构、各组件如何组织、运行期如何连接,以及如何配置开发环境,无需你再逐条解释。
|
||||
</Info>
|
||||
|
||||
本页内容与各仓库根目录的 `AGENTS.md`(`CLAUDE.md` 为其软链接)保持一致,是 LangBot 项目面向开发者与 AI 助手的"一页上手"说明。
|
||||
|
||||
## 项目概览
|
||||
|
||||
LangBot 是一个开源的、LLM 原生的即时通信机器人开发平台,提供开箱即用的 IM 机器人开发体验,内置 Agent、RAG、MCP 等 LLM 应用能力,支持全球主流 IM 平台,并提供丰富的 API 以供二次开发。
|
||||
|
||||
LangBot 拥有完整的 Web 前端,几乎所有操作都可以通过前端完成。
|
||||
|
||||
- **后端**:Python(`>=3.11,<4.0`),使用 `uv` 管理依赖;Web 框架为 Quart(Flask 的异步版本)。HTTP API 与预编译好的前端都由后端在 `http://127.0.0.1:5300` 上提供服务。
|
||||
- **前端**:`web/` 是 **Vite + React Router 7 + shadcn/ui + Tailwind CSS** 的 SPA,使用 `pnpm` 管理依赖(注意:**不是** Next.js,`dev` 脚本就是 `vite`)。
|
||||
- **插件系统**:插件 SDK、CLI(`lbp`)、插件运行时、Box 沙箱运行时以及 LangBot 与插件共享的实体/API 定义,全部位于独立仓库 [`langbot-plugin-sdk`](https://github.com/langbot-app/langbot-plugin-sdk)。LangBot 通过 `pyproject.toml` 中固定版本的 `langbot-plugin` 包依赖它。
|
||||
|
||||
## 仓库目录结构
|
||||
|
||||
```
|
||||
LangBot/
|
||||
├── main.py # 入口垫片 -> langbot.__main__.main()
|
||||
├── pyproject.toml # Python 项目与依赖(uv),固定 langbot-plugin==<x.y.z>
|
||||
├── src/langbot/
|
||||
│ ├── __main__.py # 真正的入口,定义 CLI 参数(--standalone-runtime / --standalone-box / --debug)
|
||||
│ ├── pkg/ # 核心后端包
|
||||
│ │ ├── api/ # HTTP API 控制器 + 服务(Quart)
|
||||
│ │ ├── core/ # 应用引导、启动阶段(stages)、任务管理器
|
||||
│ │ ├── platform/ # IM 平台适配器、机器人管理、会话管理
|
||||
│ │ ├── provider/ # LLM 提供商、请求器、工具提供者
|
||||
│ │ ├── pipeline/ # 流水线、阶段、查询池
|
||||
│ │ ├── plugin/ # 连接插件运行时的桥接层(connector.py / handler.py)
|
||||
│ │ ├── box/ # 代码沙箱子系统(Docker / nsjail / E2B 后端)
|
||||
│ │ ├── skill/ # 技能子系统
|
||||
│ │ ├── rag/ , vector/ # RAG 与向量存储
|
||||
│ │ ├── command/ # 内置命令
|
||||
│ │ ├── persistence/ # ORM 模型 + Alembic 迁移(SQLite 与 PostgreSQL)
|
||||
│ │ ├── storage/ # 对象/文件存储抽象
|
||||
│ │ ├── config/, entity/, discover/, utils/, telemetry/, survey/
|
||||
│ ├── libs/ # 内置 SDK(qq_official_api、wecom_api 等)
|
||||
│ └── templates/ # 配置/组件模板(如 templates/config.yaml)
|
||||
├── web/ # 前端 SPA(Vite + React Router 7 + shadcn + Tailwind)
|
||||
└── docker/ # docker-compose 部署文件
|
||||
```
|
||||
|
||||
## 组件如何组织
|
||||
|
||||
LangBot 后端的核心包 `src/langbot/pkg/` 按职责拆分为若干子模块,彼此低耦合:
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `core` | 应用生命周期:启动阶段(stages)按序加载配置、连接数据库、拉起各子系统,并管理后台任务。 |
|
||||
| `platform` | 各 IM 平台的适配器(Discord、Telegram、QQ、企业微信、飞书等),负责收发消息、管理机器人与会话。 |
|
||||
| `provider` | LLM 提供商与请求器(OpenAI 兼容、各家原生 API),以及供 Agent 调用的工具提供者。 |
|
||||
| `pipeline` | 消息处理流水线:把一条消息经过触发、AI 处理、输出、安全等阶段串联起来。 |
|
||||
| `plugin` | LangBot 主程序与插件运行时之间的桥接层,负责连接、收发动作、转发事件。 |
|
||||
| `box` | 代码沙箱子系统,为技能/工具提供隔离的代码执行环境,按可用性选择 Docker / nsjail / E2B 后端。 |
|
||||
| `skill` / `rag` / `vector` | 技能、检索增强生成与向量存储能力。 |
|
||||
| `persistence` | ORM 实体定义与 Alembic 数据库迁移,单套脚本同时兼容 SQLite 与 PostgreSQL。 |
|
||||
|
||||
插件侧的组件(在 `langbot-plugin-sdk` 中定义,由 `lbp comp` 生成)则围绕一个 `BasePlugin` 组织,目前支持六种组件类型:
|
||||
|
||||
- **Command(命令)**:用户主动触发的指令(如 `!weather tokyo`)。
|
||||
- **Tool(工具)**:供 LLM 在 Agent 执行过程中调用的函数(如查天气、查数据库)。
|
||||
- **EventListener(事件监听器)**:监听消息流水线中的事件(如自动回复、内容过滤)。
|
||||
- **KnowledgeEngine(知识引擎)**:自定义知识库的检索/接入实现,供 RAG 使用。
|
||||
- **Parser(解析器)**:自定义消息/内容的解析处理。
|
||||
- **Page(页面)**:插件提供的自定义 Web 页面,可嵌入 LangBot 管理面板。
|
||||
|
||||
每个插件运行在独立进程中,由插件运行时统一管理生命周期(发现 → 安装依赖 → 加载 → 初始化 → 注册组件 → 就绪 → 终止)。
|
||||
|
||||
## 运行期如何连接
|
||||
|
||||
```
|
||||
┌──────────────┐ HTTP/API ┌──────────────┐
|
||||
│ 浏览器/前端 │ ──────────► │ │
|
||||
│ (Vite SPA) │ :3000/同域 │ LangBot │
|
||||
└──────────────┘ │ 后端 │
|
||||
│ (Quart) │
|
||||
┌─────────────────────────┤ :5300 ├─────────────────────────┐
|
||||
│ stdio 或 WebSocket └──────────────┘ stdio 或 WebSocket │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ 插件运行时 │ 独立进程,逐个拉起插件 │ Box 运行时 │ 代码沙箱
|
||||
│ :5400 / :5401│ │ :5410 │ Docker/nsjail/E2B
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
- **前端 ↔ 后端**:开发时前端独立跑在 `:3000`,通过 `web/.env` 的 `VITE_API_BASE_URL` 访问后端 `:5300`;生产环境前端被预编译为静态文件由后端同域提供。
|
||||
- **后端 ↔ 插件运行时**:
|
||||
- 用户**直接启动** LangBot(非容器)时,后端自己**拉起**运行时并通过 **stdio** 通信(轻量/个人场景)。stdio **不能自动重连**——断开后需重启 LangBot;常见故障是上一个后端遗留的**孤儿运行时进程**仍占用 `5400`/`5401`,杀掉后重启即可。
|
||||
- LangBot 运行在**容器**中时,通过 **WebSocket** 连接到独立的运行时(生产场景)。控制端口默认 `5400`,调试端口默认 `5401`。相关配置:`data/config.yaml` 的 `plugin.runtime_ws_url`(如 `ws://langbot_plugin_runtime:5400/control/ws`)。
|
||||
- **后端 ↔ Box 运行时**:Box 子系统通过控制通道连接到 Box 运行时(默认端口 `5410`),运行时在 Docker / nsjail / E2B 中执行沙箱代码。相关配置(`data/config.yaml` 的 `box:` 段):`box.enabled`(总开关)、`box.backend`(`'local'`/`'docker'`/`'nsjail'`/`'e2b'`)、`box.runtime.endpoint`(外部 Box 运行时地址,如 `ws://127.0.0.1:5410`,留空则本地自动托管)。与插件运行时类似,配置该地址并以 `--standalone-box` 启动即可连接外部 Box 运行时。
|
||||
- 调试运行时、CLI 与 SDK 的完整方法见[《调试插件运行时、CLI、SDK》](/zh/develop/plugin-runtime),详细参数与架构在 `langbot-plugin-sdk` 仓库的 `AGENTS.md` 中。
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
完整说明见[《开发配置》](/zh/develop/dev-config)。要点如下:
|
||||
|
||||
### 后端
|
||||
|
||||
```bash
|
||||
pip install uv
|
||||
uv sync --dev # uv 会自动创建 .venv/;请把编辑器解释器指向它
|
||||
uv run main.py # 在 http://127.0.0.1:5300 提供 API + 前端
|
||||
```
|
||||
|
||||
首次启动会在 `data/config.yaml` 生成配置文件。默认使用 SQLite(零配置),同时支持 PostgreSQL;迁移在启动时自动执行。
|
||||
|
||||
### 前端
|
||||
|
||||
需要 Node.js 与 [pnpm](https://pnpm.io/zh/installation)。
|
||||
|
||||
```bash
|
||||
cd web
|
||||
cp .env.example .env # Windows: copy .env.example .env
|
||||
pnpm install
|
||||
pnpm dev # http://127.0.0.1:3000(也可用 npm install / npm run dev)
|
||||
```
|
||||
|
||||
`pnpm dev` 会读取 `web/.env` 中的 `VITE_API_BASE_URL`,使开发态前端能访问后端 `:5300`。
|
||||
|
||||
### 代码格式化
|
||||
|
||||
仓库 CI 会跑 lint + format 检查。安装 pre-commit 钩子,让同样的检查在每次提交前于本地运行:
|
||||
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
修改 ORM 模型后生成迁移:
|
||||
|
||||
```bash
|
||||
# 在项目根目录运行(需已有 data/config.yaml)
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner autogenerate "描述你的变更"
|
||||
```
|
||||
|
||||
`autogenerate` 能检测 schema 变更(增删列/表、类型变更),但**数据迁移**(如修改 JSON 字段内容)需手写进生成的脚本。`env.py` 设置了 `render_as_batch=True`,会自动处理 SQLite 的 ALTER TABLE 限制,无需按数据库类型分支。迁移在 LangBot 启动时自动执行。
|
||||
|
||||
## 开发规范
|
||||
|
||||
- LangBot 是全球化项目:**所有代码注释与文档字符串必须为英文**,所有面向用户的文案必须支持 **i18n**(至少 `en_US` + `zh_Hans`,仓库已有日文处则补 `ja_JP`)。
|
||||
- LangBot 同时被 toC 与 toB 场景采用,请始终考虑兼容性与安全性。
|
||||
- **提交信息格式**:`<type>(<scope>): <subject>`
|
||||
- `type`:`feat`、`fix`、`docs`、`style`、`refactor`、`perf`、`test`、`chore` 等。
|
||||
- `scope`:受影响的包/模块/文件/类。
|
||||
- `subject`:简洁的变更描述。
|
||||
|
||||
## 面向 Agent 的能力(MCP + 技能)
|
||||
|
||||
LangBot 从设计上就**对 AI Agent 友好**,提供三类面向 Agent 的能力,并与 HTTP API 严格对齐:
|
||||
|
||||
1. **MCP 服务**:`src/langbot/pkg/api/mcp/` 在 `/mcp` 上以 MCP 工具形式开放了 API 的精选子集(使用 API Key 鉴权,含 config.yaml 中的 `api.global_api_key`)。`server.py` 定义工具(直接调用 service 层),`mount.py` 是 ASGI 分发器。
|
||||
2. **仓库内技能**:`skills/` 目录是 LangBot 全部 Agent 技能(插件开发、核心开发、部署、e2e、MCP 操作等)的**唯一事实来源**。文档与落地页都链接到此处,而非各自复制。
|
||||
3. **API Key 鉴权**:`api.global_api_key`(config.yaml)可在无登录态下鉴权 API 与 MCP,详见 `docs/API_KEY_AUTH.md`。
|
||||
|
||||
> **维护规则(重要)**:当你新增、删除或修改某个应对 Agent 开放的 HTTP API 端点时,必须**同时**更新 `src/langbot/pkg/api/mcp/server.py` 中对应的 MCP 工具**以及** `skills/` 下相关技能(尤其是 `skills/skills/langbot-mcp-ops`)。API、MCP 工具集与技能是同一套系统,三者漂移即为 bug。
|
||||
|
||||
## 一些原则
|
||||
|
||||
- Keep it simple, stupid.
|
||||
- 如无必要,勿增实体。
|
||||
- 八荣八耻:
|
||||
|
||||
以瞎猜接口为耻,以认真查询为荣。
|
||||
以模糊执行为耻,以寻求确认为荣。
|
||||
以臆想业务为耻,以人类确认为荣。
|
||||
以创造接口为耻,以复用现有为荣。
|
||||
以跳过验证为耻,以主动测试为荣。
|
||||
以破坏架构为耻,以遵循规范为荣。
|
||||
以假装理解为耻,以诚实无知为荣。
|
||||
以盲目修改为耻,以谨慎重构为荣。
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "组件架构"
|
||||
---
|
||||
|
||||

|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: "开发配置"
|
||||
---
|
||||
|
||||
LangBot 分为前端和后端,前端使用 Vite + React Router + shadcn 开发,后端通过 Quart(Flask 的异步版本)开发。
|
||||
|
||||
## 后端
|
||||
|
||||
代码位于 `pkg` 目录下,由根目录的 `main.py` 文件引导启动。
|
||||
|
||||
安装依赖,我们使用 uv 管理依赖。
|
||||
|
||||
```bash
|
||||
pip install uv
|
||||
uv sync --dev
|
||||
```
|
||||
|
||||
启动后端
|
||||
|
||||
```bash
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
此时配置文件会自动生成到 `data/config.yaml` 文件中。
|
||||
|
||||
## 前端
|
||||
|
||||
代码位于 `web` 目录下,需要安装 Node.js,[pnpm](https://pnpm.io/zh/installation)。
|
||||
|
||||
复制 `.env.example` 到 `.env`。
|
||||
|
||||
- linux环境使用
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
- windows环境使用
|
||||
|
||||
```bash
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
安装依赖并启动前端
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
|
||||
# 若未安装pnpm,也可以使用npm来解决依赖并启动
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
然后根据输出信息,访问`http://127.0.0.1:3000`查看独立启动的前端页面。
|
||||
|
||||
<Info title="本地使用`pnpm dev`启动时,会携带`.env`中的环境变量`VITE_API_BASE_URL`,该变量会自动被前端使用,以确保前端可以访问到本地启动的后端的`5300`端口。">
|
||||
|
||||
生产环境中,前端会被预编译成静态文件,由后端提供服务,前端会自动访问同域的后端地址。
|
||||
</Info>
|
||||
|
||||
## 代码格式化
|
||||
|
||||
仓库中包含 lint 和 format 检查工作流,您推送代码后将被自动检查。请配置 pre-commit 来在提交时在本地提前检查代码。
|
||||
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
我们在开发每个接口之前都会先在 APIFox 中编写接口文档,请查看 [API 文档](https://ok52vhsenr.apifox.cn/)。
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
LangBot 使用 [Alembic](https://alembic.sqlalchemy.org/) 管理数据库迁移,同时支持 SQLite 和 PostgreSQL,无需针对不同数据库编写不同的迁移代码。
|
||||
|
||||
### 迁移文件位置
|
||||
|
||||
```
|
||||
src/langbot/pkg/persistence/alembic/versions/
|
||||
```
|
||||
|
||||
### 创建新迁移
|
||||
|
||||
推荐使用 **autogenerate** 自动生成迁移脚本,它会对比 ORM 模型和数据库 schema 的差异:
|
||||
|
||||
```bash
|
||||
# 在项目根目录下运行(需要已有 data/config.yaml)
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner autogenerate "描述你的变更"
|
||||
```
|
||||
|
||||
生成的文件会出现在 `src/langbot/pkg/persistence/alembic/versions/` 目录下,请检查并编辑生成的脚本,确认变更无误后提交。
|
||||
|
||||
<Info>
|
||||
autogenerate 能自动检测列的增删、表的增删、类型变更等。但对于**数据迁移**(如修改 JSON 字段的内容),需要手动在生成的脚本中添加代码。
|
||||
</Info>
|
||||
|
||||
也可以手动创建迁移文件,遵循命名规范 `NNNN_description.py`:
|
||||
|
||||
```python
|
||||
"""描述你的迁移
|
||||
|
||||
Revision ID: 0003_add_bot_description
|
||||
Revises: 0002_sample
|
||||
Create Date: 2026-04-08
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0003_add_bot_description'
|
||||
down_revision = '0002_sample' # 指向前一个迁移的 revision
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Schema 变更(SQLite 和 PostgreSQL 通用)
|
||||
op.add_column('bots', sa.Column('description', sa.String(512), server_default=''))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('bots', 'description')
|
||||
```
|
||||
|
||||
### 迁移模式
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Schema 变更(增/删列、建表)">
|
||||
```python
|
||||
def upgrade() -> None:
|
||||
# 添加列
|
||||
op.add_column('table_name', sa.Column('new_col', sa.String(255), server_default=''))
|
||||
|
||||
# 新建表
|
||||
op.create_table(
|
||||
'new_table',
|
||||
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column('name', sa.String(255), nullable=False),
|
||||
)
|
||||
```
|
||||
|
||||
<Info>
|
||||
`env.py` 中设置了 `render_as_batch=True`,Alembic 会自动处理 SQLite 的 ALTER TABLE 限制(通过临时表重建),无需手动区分数据库类型。
|
||||
</Info>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="数据迁移(读取并修改数据)">
|
||||
```python
|
||||
import json
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
rows = conn.execute(sa.text("SELECT uuid, config FROM pipelines")).fetchall()
|
||||
for row in rows:
|
||||
config = json.loads(row[1]) if isinstance(row[1], str) else row[1]
|
||||
config.setdefault('ai', {})['new_key'] = 'default_value'
|
||||
conn.execute(
|
||||
sa.text("UPDATE pipelines SET config = :cfg WHERE uuid = :uuid"),
|
||||
{"cfg": json.dumps(config), "uuid": row[0]}
|
||||
)
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### 运行机制
|
||||
|
||||
迁移在 LangBot 启动时自动执行,无需手动运行命令:
|
||||
|
||||
1. 首次启动时,自动 stamp 基线版本(标记现有数据库)
|
||||
2. 之后每次启动,自动执行所有未应用的迁移(`alembic upgrade head`)
|
||||
|
||||
### CI 测试
|
||||
|
||||
仓库中包含 `test-migrations.yml` 工作流,当 `persistence/` 相关文件变更时会自动在 SQLite 和 PostgreSQL 上测试迁移脚本。
|
||||
|
||||
### CLI 工具
|
||||
|
||||
```bash
|
||||
# 自动生成迁移脚本(对比 ORM 模型和数据库)
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner autogenerate "描述"
|
||||
|
||||
# 升级数据库
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner upgrade
|
||||
|
||||
# 查看当前版本
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner current
|
||||
|
||||
# 标记版本(不执行迁移)
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner stamp head
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: "调试插件运行时、CLI、SDK"
|
||||
---
|
||||
|
||||
<Info>
|
||||
插件运行时、CLI、SDK 开源在: https://github.com/langbot-app/langbot-plugin-sdk
|
||||
</Info>
|
||||
|
||||
由于 LangBot 需要依赖 langbot-plugin-sdk 中定义的实体,我们推荐您在一个新建目录下打开 VS Code,将 LangBot 和 langbot-plugin-sdk(git clone https://github.com/langbot-app/langbot-plugin-sdk) 作为子目录放入其中,目录结构如下:
|
||||
|
||||
```bash
|
||||
langbot-projects
|
||||
├── LangBot
|
||||
├── langbot-plugin-sdk
|
||||
```
|
||||
|
||||
进入 LangBot 目录,安装依赖:
|
||||
|
||||
```bash
|
||||
cd LangBot
|
||||
uv sync --dev
|
||||
```
|
||||
|
||||
此时,uv 将自动为您创建虚拟环境(.venv),若您的编辑器询问您是否使用此虚拟环境,请选择`是`。
|
||||
|
||||
<img
|
||||
width="600"
|
||||
src="/images/zh/develop/confirm_venv.png"
|
||||
/>
|
||||
|
||||
若未弹出,请手动在右下角设置 Python 解释器路径为该 venv 中的解释器。
|
||||
|
||||
然后打开 VS Code 底部的终端,这将自动激活 venv。
|
||||
|
||||
或者请您手动激活此虚拟环境:
|
||||
|
||||
```bash
|
||||
# 请自行根据.venv路径来修改命令
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
## 启动插件运行时
|
||||
|
||||
```bash
|
||||
python -m langbot_plugin.cli.__init__ rt
|
||||
```
|
||||
|
||||
Plugin Runtime 接受以下参数:
|
||||
|
||||
- `--debug-only`: 不启动`data/plugins`目录下的插件,仅允许通过调试连接加载插件。
|
||||
- `--ws-debug-port`: 监听的调试端口,默认是`5401`。
|
||||
- `--ws-control-port`: 监听的控制端口(供 LangBot 主程序连接),默认是`5400`。
|
||||
- `-s`: 使用`stdio`接受控制连接。**仅在生产环境使用**。
|
||||
- `--skip-deps-check`: 为了确保插件依赖均已安装,Runtime 会在每次启动时检查并安装所有已安装插件的依赖。携带此参数可禁用此检查。
|
||||
|
||||
### 使 LangBot 使用您本地修改过的 langbot-plugin-sdk
|
||||
|
||||
若您修改了诸如消息实体、插件数据定义等内容,需要将其更新到 LangBot 环境,以确保运行期间数据格式兼容。
|
||||
|
||||
请在**确保已激活 LangBot 目录下的虚拟环境(.venv)的终端**中,切换目录到 langbot-plugin-sdk 目录下,执行:
|
||||
|
||||
```bash
|
||||
uv pip install .
|
||||
```
|
||||
|
||||
这将把您修改后的 langbot-plugin-sdk 安装到 LangBot 的环境。
|
||||
|
||||
### 使 LangBot 连接到此运行时
|
||||
|
||||
在 LangBot 的`data/config.yaml`中配置`plugin.runtime_ws_url`为`ws://localhost:5400/control/ws`。
|
||||
|
||||
```yaml
|
||||
plugin:
|
||||
runtime_ws_url: ws://localhost:5400/control/ws
|
||||
```
|
||||
|
||||
并在已激活 LangBot 虚拟环境的终端中,直接使用 Python 启动主程序并携带参数`--standalone-runtime`(如:`python main.py --standalone-runtime`)。直接调用当前虚拟环境的 Python 不会再次同步依赖,因此不会把刚刚安装的本地 langbot-plugin-sdk 覆盖为远程版本。\
|
||||
重启 LangBot,将会使用 WebSocket 连接到此运行时。
|
||||
|
||||
默认情况下无需配置 `LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN`:当 LangBot 与 Runtime 两端都未设置时,OSS 的本地控制连接直接建立。若部署环境需要为 5400 控制端口增加共享密钥,则应在两端配置同一个至少 32 位的高熵值。Runtime 端配置后,未携带相同密钥的 LangBot 会被拒绝;只在 LangBot 端配置并不会启用 Runtime 端的认证。
|
||||
|
||||
### 使用 `lbp run` 调试插件
|
||||
|
||||
多 Workspace 版本不再允许调试插件仅凭 `5401` 端口接入 Runtime。每个 Workspace 都有独立且会过期的调试密钥:
|
||||
|
||||
1. 确认 LangBot 与 Plugin Runtime 已按上文启动并连接成功。
|
||||
2. 在 LangBot WebUI 的“插件”页面点击“调试信息”,复制调试 URL 和调试密钥。该操作需要当前 Workspace 的资源管理权限。
|
||||
3. 在插件项目的 `.env` 中配置:
|
||||
|
||||
```bash
|
||||
DEBUG_RUNTIME_WS_URL=ws://localhost:5401/plugin/debug/ws
|
||||
PLUGIN_DEBUG_KEY=<从 WebUI 复制的调试密钥>
|
||||
```
|
||||
|
||||
4. 在插件项目目录中启动:
|
||||
|
||||
```bash
|
||||
python -m langbot_plugin.cli.__init__ run
|
||||
```
|
||||
|
||||
也可以不写入 `.env`,改用 `python -m langbot_plugin.cli.__init__ run --plugin-debug-key '<调试密钥>'`。调试密钥按 Workspace 隔离且有效期为两小时;密钥过期、Runtime 重启或切换 Workspace 后,请回到 WebUI 重新获取。只配置 `DEBUG_RUNTIME_WS_URL` 会被 Runtime 拒绝。
|
||||
|
||||
## 以 standalone 模式启动 Box Runtime
|
||||
|
||||
Box Runtime 与 Plugin Runtime 的控制连接规则一致:OSS standalone 开发环境默认不强制配置 token,两端都未设置 `LANGBOT_BOX_CONTROL_TOKEN` 时可以直接连接:
|
||||
|
||||
```bash
|
||||
# 终端 1:langbot-plugin-sdk 目录
|
||||
python -m langbot_plugin.cli.__init__ box
|
||||
```
|
||||
|
||||
LangBot 的 `data/config.yaml` 使用本地 Box 地址:
|
||||
|
||||
```yaml
|
||||
box:
|
||||
enabled: true
|
||||
backend: local
|
||||
runtime:
|
||||
endpoint: ws://127.0.0.1:5410
|
||||
```
|
||||
|
||||
```bash
|
||||
# 终端 2:LangBot 目录
|
||||
python main.py --standalone-runtime --standalone-box
|
||||
```
|
||||
|
||||
若需要保护暴露的 5410 端口,请在启动两个进程前分别设置完全相同、至少 32 位且不含空白的高熵值:
|
||||
|
||||
```bash
|
||||
export LANGBOT_BOX_CONTROL_TOKEN='<两端完全相同的高熵密钥>'
|
||||
```
|
||||
|
||||
Box Runtime 端一旦配置 token,就会拒绝未携带相同 token 的 LangBot。只在 LangBot 端设置不会启用 Box Runtime 端认证;显式配置但不足 32 位的值仍会被两端拒绝。不要把真实密钥提交到配置文件或 Git。
|
||||
|
||||
## langbot-plugin-sdk 架构
|
||||
|
||||
本代码库中包含以下内容:
|
||||
|
||||
- `langbot_plugin.api`:插件相关实体和 API 定义。
|
||||
- `langbot_plugin.assets`:插件模板。
|
||||
- `langbot_plugin.cli`:插件开发 CLI 工具。
|
||||
- `langbot_plugin.entities`:插件系统中非 API 定义的相关实体。
|
||||
- `langbot_plugin.runtime`:插件运行时和底层通信(stdio 和 websocket)实现。
|
||||
|
||||
## `lbp` CLI 工具
|
||||
|
||||
CLI 工具提供 Runtime 启动、插件初始化、插件组件管理、Marketplace 交互等功能。
|
||||
|
||||
详细的程序入口请查看[`langbot_plugin.cli.__init__`](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/cli/__init__.py)。
|
||||
Reference in New Issue
Block a user