mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 05:15:22 +08:00
add: discord adapter (#54)
This commit is contained in:
@@ -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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 适配器的完整功能集。图像处理功能无需额外依赖,语音功能需要安装可选依赖。
|
||||
@@ -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 |
|
||||
| 高延迟 | 响应慢 | 检查网络延迟 | 优化网络,减少并发 |
|
||||
|
||||
---
|
||||
|
||||
> **提示**: 遇到问题时,首先启用详细日志记录,然后使用诊断工具进行系统性排查。大多数问题都与配置、权限或网络环境相关。
|
||||
Generated
+1358
-6
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -6,5 +6,8 @@
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"mermaid": "^11.8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user