add: discord adapter (#54)

This commit is contained in:
Matthew_Astral
2025-07-05 20:50:37 +08:00
committed by GitHub
parent e4817a21a4
commit 51babe8f21
8 changed files with 3966 additions and 7 deletions
+17
View File
@@ -200,6 +200,23 @@ export default defineConfig({
items: [
{ text: "开发配置", link: "/zh/develop/dev-config" },
{ text: "组件架构", link: "/zh/develop/comp-arch" },
{
text: "适配器开发",
collapsed: true,
items: [
{
text: "Discord 适配器",
collapsed: true,
items: [
{ text: "概述", link: "/zh/develop/adapter/discord/README" },
{ text: "快速开始", link: "/zh/develop/adapter/discord/quick_start" },
{ text: "架构设计", link: "/zh/develop/adapter/discord/design" },
{ text: "API 参考", link: "/zh/develop/adapter/discord/api_reference" },
{ text: "故障排除", link: "/zh/develop/adapter/discord/troubleshooting" },
],
},
],
},
],
},
],
+176
View File
@@ -0,0 +1,176 @@
# Discord 适配器文档
## 概述
这是 LangBot 平台的 Discord 适配器完整文档,基于 discord.py 库实现了与 Discord 平台的全面集成,包括文本消息、图像处理、语音功能等完整功能集。
## 文档导航
### 核心文档
- **[API 参考](api_reference.md)** - Discord 适配器完整 API 文档
- **[快速开始](quick_start.md)** - 基本使用指南
- **[架构设计](design.md)** - 适配器架构设计
- **[故障排除](troubleshooting.md)** - 常见问题解决方案
## 完整功能集
### 消息处理
- 文本消息发送和接收
- 图像消息处理(支持多种格式)
- 消息引用和回复
- 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,551 @@
# 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 适配器的完整功能集。图像处理功能无需额外依赖,语音功能需要安装可选依赖。
+397
View File
@@ -0,0 +1,397 @@
# 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,551 @@
# 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,912 @@
# 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 |
| 高延迟 | 响应慢 | 检查网络延迟 | 优化网络,减少并发 |
---
> **提示**: 遇到问题时,首先启用详细日志记录,然后使用诊断工具进行系统性排查。大多数问题都与配置、权限或网络环境相关。
+1358 -6
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -6,5 +6,8 @@
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"mermaid": "^11.8.0"
}
}
}