Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox-Core into dev
This commit is contained in:
132
docs/SLOW_QUERY_QUICK_REFERENCE.md
Normal file
132
docs/SLOW_QUERY_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# 慢查询监控快速参考
|
||||||
|
|
||||||
|
## 🚀 快速启用
|
||||||
|
|
||||||
|
### 方法 1:修改配置(推荐)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# config/bot_config.toml
|
||||||
|
[database]
|
||||||
|
enable_slow_query_logging = true # 改为 true 启用
|
||||||
|
slow_query_threshold = 0.5 # 选项:阈值(秒)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2:代码启用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import enable_slow_query_monitoring
|
||||||
|
|
||||||
|
enable_slow_query_monitoring() # 启用
|
||||||
|
|
||||||
|
# ... 你的代码 ...
|
||||||
|
|
||||||
|
disable_slow_query_monitoring() # 禁用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 3:检查状态
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import is_slow_query_monitoring_enabled
|
||||||
|
|
||||||
|
if is_slow_query_monitoring_enabled():
|
||||||
|
print("✅ 已启用")
|
||||||
|
else:
|
||||||
|
print("❌ 已禁用")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 关键命令
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 启用/禁用
|
||||||
|
from src.common.database.utils import (
|
||||||
|
enable_slow_query_monitoring,
|
||||||
|
disable_slow_query_monitoring,
|
||||||
|
is_slow_query_monitoring_enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
enable_slow_query_monitoring()
|
||||||
|
disable_slow_query_monitoring()
|
||||||
|
is_slow_query_monitoring_enabled()
|
||||||
|
|
||||||
|
# 获取数据
|
||||||
|
from src.common.database.utils import (
|
||||||
|
get_slow_queries,
|
||||||
|
get_slow_query_report
|
||||||
|
)
|
||||||
|
|
||||||
|
queries = get_slow_queries(limit=20)
|
||||||
|
report = get_slow_query_report()
|
||||||
|
|
||||||
|
# 生成报告
|
||||||
|
from src.common.database.utils.slow_query_analyzer import SlowQueryAnalyzer
|
||||||
|
|
||||||
|
SlowQueryAnalyzer.generate_html_report("report.html")
|
||||||
|
text = SlowQueryAnalyzer.generate_text_report()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 推荐配置
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# 生产环境(默认)
|
||||||
|
enable_slow_query_logging = false
|
||||||
|
|
||||||
|
# 测试环境
|
||||||
|
enable_slow_query_logging = true
|
||||||
|
slow_query_threshold = 0.5
|
||||||
|
|
||||||
|
# 开发环境
|
||||||
|
enable_slow_query_logging = true
|
||||||
|
slow_query_threshold = 0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 使用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. 启用监控
|
||||||
|
enable_slow_query_monitoring()
|
||||||
|
|
||||||
|
# 2. 自动监控函数
|
||||||
|
@measure_time()
|
||||||
|
async def slow_operation():
|
||||||
|
return await db.query(...)
|
||||||
|
|
||||||
|
# 3. 查看报告
|
||||||
|
report = get_slow_query_report()
|
||||||
|
print(f"总慢查询数: {report['total']}")
|
||||||
|
|
||||||
|
# 4. 禁用监控
|
||||||
|
disable_slow_query_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 性能
|
||||||
|
|
||||||
|
| 状态 | CPU 开销 | 内存占用 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 启用 | < 0.1% | ~50 KB |
|
||||||
|
| 禁用 | ~0% | 0 KB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 核心要点
|
||||||
|
|
||||||
|
✅ **默认关闭** - 无性能开销
|
||||||
|
✅ **按需启用** - 方便的启用/禁用
|
||||||
|
✅ **实时告警** - 超过阈值时输出
|
||||||
|
✅ **详细报告** - 关闭时输出分析
|
||||||
|
✅ **零成本** - 禁用时完全无开销
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**启用**: `enable_slow_query_monitoring()`
|
||||||
|
**禁用**: `disable_slow_query_monitoring()`
|
||||||
|
**查看**: `get_slow_query_report()`
|
||||||
|
|
||||||
|
更多信息: `docs/slow_query_monitoring_guide.md`
|
||||||
@@ -34,11 +34,11 @@ MoFox Bus 是 MoFox Bot 自研的统一消息中台,替换第三方 `maim_mess
|
|||||||
|
|
||||||
## 3. 消息模型
|
## 3. 消息模型
|
||||||
|
|
||||||
### 3.1 Envelope TypedDict<EFBFBD><EFBFBD>`types.py`<EFBFBD><EFBFBD>
|
### 3.1 Envelope TypedDict(`types.py`)
|
||||||
|
|
||||||
- `MessageEnvelope` <20><>ȫ<EFBFBD><C8AB>Ƶ<EFBFBD> maim_message <EFBFBD>ṹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ĵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> `message_info` + `message_segment` (SegPayload)<EFBFBD><EFBFBD>`direction`<EFBFBD><EFBFBD>`schema_version` <20><> raw <20><><EFBFBD><EFBFBD><EFBFBD>ֶβ<D6B6><CEB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˣ<EFBFBD><CBA3><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD> `channel`<EFBFBD><EFBFBD>`sender`<EFBFBD><EFBFBD>`content` <EFBFBD><EFBFBD> v0 <20>ֶΪ<D6B6><CEAA>ѡ<EFBFBD><D1A1>
|
- `MessageEnvelope`:完全对齐原 maim_message 结构,核心字段包括 `message_info` + `message_segment` (SegPayload)、`direction`、`schema_version`,同时保留 raw 相关字段;新增 `channel`、`sender`、`content` 字段并将 v0 字段标记为可选。
|
||||||
- `SegPayload` / `MessageInfoPayload` / `UserInfoPayload` / `GroupInfoPayload` / `FormatInfoPayload` / `TemplateInfoPayload` <20><> maim_message dataclass <EFBFBD>Դ<EFBFBD>TypedDict <20><>Ӧ<EFBFBD><D3A6><EFBFBD>ʺ<EFBFBD>ֱ<EFBFBD><D6B1> JSON <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
- `SegPayload` / `MessageInfoPayload` / `UserInfoPayload` / `GroupInfoPayload` / `FormatInfoPayload` / `TemplateInfoPayload`:与 maim_message dataclass 一一对应的 TypedDict,方便直接做 JSON 序列化。
|
||||||
- `Content` / `SenderInfo` / `ChannelInfo` <20>Ȳ<EFBFBD>Ȼ<EFBFBD><C8BB><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><DAA3><EFBFBD><EFBFBD>ܻ<EFBFBD><DCBB><EFBFBD> IDE ע<>⣬Ҳ<E2A3AC>Ƕ<EFBFBD> v0 content ģ<EFBFBD>͵Ļ<EFBFBD>֧
|
- `Content` / `SenderInfo` / `ChannelInfo`:仍在迭代中,可能出现 IDE 提示;同时兼容 v0 content 模型。
|
||||||
|
|
||||||
### 3.2 dataclass 消息段(`message_models.py`)
|
### 3.2 dataclass 消息段(`message_models.py`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# 增强命令系统使用指南
|
# 增强命令系统使用指南
|
||||||
|
|
||||||
|
> ⚠️ **重要:插件命令必须使用 PlusCommand!**
|
||||||
|
>
|
||||||
|
> - ✅ **推荐**:`PlusCommand` - 插件开发的标准基类
|
||||||
|
> - ❌ **禁止**:`BaseCommand` - 仅供框架内部使用
|
||||||
|
>
|
||||||
|
> 如果你直接使用 `BaseCommand`,将需要手动处理参数解析、正则匹配等复杂逻辑,并且 `execute()` 方法签名也不同。
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
增强命令系统是MoFox-Bot插件系统的一个扩展,让命令的定义和使用变得更加简单直观。你不再需要编写复杂的正则表达式,只需要定义命令名、别名和参数处理逻辑即可。
|
增强命令系统是MoFox-Bot插件系统的一个扩展,让命令的定义和使用变得更加简单直观。你不再需要编写复杂的正则表达式,只需要定义命令名、别名和参数处理逻辑即可。
|
||||||
@@ -224,24 +231,95 @@ class ConfigurableCommand(PlusCommand):
|
|||||||
|
|
||||||
## 返回值说明
|
## 返回值说明
|
||||||
|
|
||||||
`execute`方法需要返回一个三元组:
|
`execute`方法必须返回一个三元组:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
return (执行成功标志, 可选消息, 是否拦截后续处理)
|
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
# ... 你的逻辑 ...
|
||||||
|
return (执行成功标志, 日志描述, 是否拦截消息)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **执行成功标志** (bool): True表示命令执行成功,False表示失败
|
### 返回值详解
|
||||||
- **可选消息** (Optional[str]): 用于日志记录的消息
|
|
||||||
- **是否拦截后续处理** (bool): True表示拦截消息,不进行后续处理
|
| 位置 | 类型 | 名称 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 1 | `bool` | 执行成功标志 | `True` = 命令执行成功<br>`False` = 命令执行失败 |
|
||||||
|
| 2 | `Optional[str]` | 日志描述 | 用于内部日志记录的描述性文本<br>⚠️ **不是发给用户的消息!** |
|
||||||
|
| 3 | `bool` | 是否拦截消息 | `True` = 拦截,阻止后续处理(推荐)<br>`False` = 不拦截,继续后续处理 |
|
||||||
|
|
||||||
|
### 重要:消息发送 vs 日志描述
|
||||||
|
|
||||||
|
⚠️ **常见错误:在返回值中返回用户消息**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 错误做法 - 不要这样做!
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
message = "你好,这是给用户的消息"
|
||||||
|
return True, message, True # 这个消息不会发给用户!
|
||||||
|
|
||||||
|
# ✅ 正确做法 - 使用 self.send_text()
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
await self.send_text("你好,这是给用户的消息") # 发送给用户
|
||||||
|
return True, "执行了问候命令", True # 日志描述
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
"""execute 方法的完整示例"""
|
||||||
|
|
||||||
|
# 1. 参数验证
|
||||||
|
if args.is_empty():
|
||||||
|
await self.send_text("⚠️ 请提供参数")
|
||||||
|
return True, "缺少参数", True
|
||||||
|
|
||||||
|
# 2. 执行逻辑
|
||||||
|
user_input = args.get_raw()
|
||||||
|
result = process_input(user_input)
|
||||||
|
|
||||||
|
# 3. 发送消息给用户
|
||||||
|
await self.send_text(f"✅ 处理结果:{result}")
|
||||||
|
|
||||||
|
# 4. 返回:成功、日志描述、拦截消息
|
||||||
|
return True, f"处理了用户输入: {user_input[:20]}", True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 拦截标志使用指导
|
||||||
|
|
||||||
|
- **返回 `True`**(推荐):命令已完成处理,不需要后续处理(如 LLM 回复)
|
||||||
|
- **返回 `False`**:允许系统继续处理(例如让 LLM 也回复)
|
||||||
|
|
||||||
## 最佳实践
|
## 最佳实践
|
||||||
|
|
||||||
1. **命令命名**:使用简短、直观的命令名
|
### 1. 命令设计
|
||||||
2. **别名设置**:为常用命令提供简短别名
|
- ✅ **命令命名**:使用简短、直观的命令名(如 `time`、`help`、`status`)
|
||||||
3. **参数验证**:总是检查参数的有效性
|
- ✅ **别名设置**:为常用命令提供简短别名(如 `echo` -> `e`、`say`)
|
||||||
4. **错误处理**:提供清晰的错误提示和使用说明
|
- ✅ **聊天类型**:根据命令功能选择 `ChatType.ALL`/`GROUP`/`PRIVATE`
|
||||||
5. **配置支持**:重要设置应该可配置
|
|
||||||
6. **聊天类型**:根据命令功能选择合适的聊天类型限制
|
### 2. 参数处理
|
||||||
|
- ✅ **总是验证**:使用 `args.is_empty()`、`args.count()` 检查参数
|
||||||
|
- ✅ **友好提示**:参数错误时提供清晰的用法说明
|
||||||
|
- ✅ **默认值**:为可选参数提供合理的默认值
|
||||||
|
|
||||||
|
### 3. 消息发送
|
||||||
|
- ✅ **使用 `self.send_text()`**:发送消息给用户
|
||||||
|
- ❌ **不要在返回值中返回用户消息**:返回值是日志描述
|
||||||
|
- ✅ **拦截消息**:大多数情况返回 `True` 作为第三个参数
|
||||||
|
|
||||||
|
### 4. 错误处理
|
||||||
|
- ✅ **Try-Catch**:捕获并处理可能的异常
|
||||||
|
- ✅ **清晰反馈**:告诉用户发生了什么问题
|
||||||
|
- ✅ **记录日志**:在返回值中提供有用的调试信息
|
||||||
|
|
||||||
|
### 5. 配置管理
|
||||||
|
- ✅ **可配置化**:重要设置应该通过 `self.get_config()` 读取
|
||||||
|
- ✅ **提供默认值**:即使配置缺失也能正常工作
|
||||||
|
|
||||||
|
### 6. 代码质量
|
||||||
|
- ✅ **类型注解**:使用完整的类型提示
|
||||||
|
- ✅ **文档字符串**:为 `execute()` 方法添加文档说明
|
||||||
|
- ✅ **代码注释**:为复杂逻辑添加必要的注释
|
||||||
|
|
||||||
## 完整示例
|
## 完整示例
|
||||||
|
|
||||||
|
|||||||
265
docs/plugins/README.md
Normal file
265
docs/plugins/README.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# 📚 MoFox-Bot 插件开发文档导航
|
||||||
|
|
||||||
|
欢迎来到 MoFox-Bot 插件系统开发文档!本文档帮助你快速找到所需的学习资源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 我应该从哪里开始?
|
||||||
|
|
||||||
|
### 第一次接触插件开发?
|
||||||
|
👉 **从这里开始**:[快速开始指南](quick-start.md)
|
||||||
|
|
||||||
|
这是一个循序渐进的教程,带你从零开始创建第一个插件,包含完整的代码示例。
|
||||||
|
|
||||||
|
### 遇到问题了?
|
||||||
|
👉 **先看这里**:[故障排除指南](troubleshooting-guide.md) ⭐
|
||||||
|
|
||||||
|
包含10个最常见问题的解决方案,可能5分钟就能解决你的问题。
|
||||||
|
|
||||||
|
### 想深入了解特定功能?
|
||||||
|
👉 **查看下方分类导航**,找到你需要的文档。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 学习路径建议
|
||||||
|
|
||||||
|
### 🌟 新手路径(按顺序阅读)
|
||||||
|
|
||||||
|
1. **[快速开始指南](quick-start.md)** ⭐ 必读
|
||||||
|
- 创建插件目录和配置
|
||||||
|
- 实现第一个 Action 组件
|
||||||
|
- 实现第一个 Command 组件
|
||||||
|
- 添加配置文件
|
||||||
|
- 预计阅读时间:30-45分钟
|
||||||
|
|
||||||
|
2. **[增强命令指南](PLUS_COMMAND_GUIDE.md)** ⭐ 必读
|
||||||
|
- 理解 PlusCommand 与 BaseCommand 的区别
|
||||||
|
- 学习命令参数处理
|
||||||
|
- 掌握返回值规范
|
||||||
|
- 预计阅读时间:20-30分钟
|
||||||
|
|
||||||
|
3. **[Action 组件详解](action-components.md)** ⭐ 必读
|
||||||
|
- 理解 Action 的激活机制
|
||||||
|
- 学习自定义激活逻辑
|
||||||
|
- 掌握 Action 的使用场景
|
||||||
|
- 预计阅读时间:25-35分钟
|
||||||
|
|
||||||
|
4. **[故障排除指南](troubleshooting-guide.md)** ⭐ 建议收藏
|
||||||
|
- 常见错误及解决方案
|
||||||
|
- 最佳实践速查
|
||||||
|
- 调试技巧
|
||||||
|
- 随时查阅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚀 进阶路径(根据需求选择)
|
||||||
|
|
||||||
|
#### 需要配置系统?
|
||||||
|
- **[配置文件系统指南](configuration-guide.md)**
|
||||||
|
- 自动生成配置文件
|
||||||
|
- 配置 Schema 定义
|
||||||
|
- 配置读取和验证
|
||||||
|
|
||||||
|
#### 需要响应事件?
|
||||||
|
- **[事件系统指南](event-system-guide.md)**
|
||||||
|
- 订阅系统事件
|
||||||
|
- 创建自定义事件
|
||||||
|
- 事件处理器实现
|
||||||
|
|
||||||
|
#### 需要集成外部功能?
|
||||||
|
- **[Tool 组件指南](tool_guide.md)**
|
||||||
|
- 为 LLM 提供工具调用能力
|
||||||
|
- 函数调用集成
|
||||||
|
- Tool 参数定义
|
||||||
|
|
||||||
|
#### 需要依赖其他插件?
|
||||||
|
- **[依赖管理指南](dependency-management.md)**
|
||||||
|
- 声明插件依赖
|
||||||
|
- Python 包依赖
|
||||||
|
- 依赖版本管理
|
||||||
|
|
||||||
|
#### 需要高级激活控制?
|
||||||
|
- **[Action 激活机制重构指南](action-activation-guide.md)**
|
||||||
|
- 自定义激活逻辑
|
||||||
|
- 关键词匹配激活
|
||||||
|
- LLM 智能判断激活
|
||||||
|
- 随机激活策略
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 文档结构说明
|
||||||
|
|
||||||
|
### 核心文档(必读)
|
||||||
|
|
||||||
|
```
|
||||||
|
📄 quick-start.md 快速开始指南 ⭐ 新手必读
|
||||||
|
📄 PLUS_COMMAND_GUIDE.md 增强命令系统指南 ⭐ 必读
|
||||||
|
📄 action-components.md Action 组件详解 ⭐ 必读
|
||||||
|
📄 troubleshooting-guide.md 故障排除指南 ⭐ 遇到问题先看这个
|
||||||
|
```
|
||||||
|
|
||||||
|
### 进阶文档(按需阅读)
|
||||||
|
|
||||||
|
```
|
||||||
|
📄 configuration-guide.md 配置系统详解
|
||||||
|
📄 event-system-guide.md 事件系统详解
|
||||||
|
📄 tool_guide.md Tool 组件详解
|
||||||
|
📄 action-activation-guide.md Action 激活机制详解
|
||||||
|
📄 dependency-management.md 依赖管理详解
|
||||||
|
📄 manifest-guide.md Manifest 文件规范
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 参考文档
|
||||||
|
|
||||||
|
```
|
||||||
|
📁 api/ API 参考文档目录
|
||||||
|
├── 消息相关
|
||||||
|
│ ├── send-api.md 消息发送 API
|
||||||
|
│ ├── message-api.md 消息处理 API
|
||||||
|
│ └── chat-api.md 聊天流 API
|
||||||
|
│
|
||||||
|
├── AI 相关
|
||||||
|
│ ├── llm-api.md LLM 交互 API
|
||||||
|
│ └── generator-api.md 回复生成 API
|
||||||
|
│
|
||||||
|
├── 数据相关
|
||||||
|
│ ├── database-api.md 数据库操作 API
|
||||||
|
│ ├── config-api.md 配置读取 API
|
||||||
|
│ └── person-api.md 人物关系 API
|
||||||
|
│
|
||||||
|
├── 组件相关
|
||||||
|
│ ├── plugin-manage-api.md 插件管理 API
|
||||||
|
│ └── component-manage-api.md 组件管理 API
|
||||||
|
│
|
||||||
|
└── 其他
|
||||||
|
├── emoji-api.md 表情包 API
|
||||||
|
├── tool-api.md 工具 API
|
||||||
|
└── logging-api.md 日志 API
|
||||||
|
```
|
||||||
|
|
||||||
|
### 其他文件
|
||||||
|
|
||||||
|
```
|
||||||
|
📄 index.md 文档索引(旧版,建议查看本 README)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 按功能查找文档
|
||||||
|
|
||||||
|
### 我想创建...
|
||||||
|
|
||||||
|
| 目标 | 推荐文档 | 难度 |
|
||||||
|
|------|----------|------|
|
||||||
|
| **一个简单的命令** | [快速开始](quick-start.md) → [增强命令指南](PLUS_COMMAND_GUIDE.md) | ⭐ 入门 |
|
||||||
|
| **一个智能 Action** | [快速开始](quick-start.md) → [Action 组件](action-components.md) | ⭐⭐ 中级 |
|
||||||
|
| **带复杂参数的命令** | [增强命令指南](PLUS_COMMAND_GUIDE.md) | ⭐⭐ 中级 |
|
||||||
|
| **需要配置的插件** | [配置系统指南](configuration-guide.md) | ⭐⭐ 中级 |
|
||||||
|
| **响应系统事件的插件** | [事件系统指南](event-system-guide.md) | ⭐⭐⭐ 高级 |
|
||||||
|
| **为 LLM 提供工具** | [Tool 组件指南](tool_guide.md) | ⭐⭐⭐ 高级 |
|
||||||
|
| **依赖其他插件的插件** | [依赖管理指南](dependency-management.md) | ⭐⭐ 中级 |
|
||||||
|
|
||||||
|
### 我想学习...
|
||||||
|
|
||||||
|
| 主题 | 相关文档 |
|
||||||
|
|------|----------|
|
||||||
|
| **如何发送消息** | [发送 API](api/send-api.md) / [增强命令指南](PLUS_COMMAND_GUIDE.md) |
|
||||||
|
| **如何处理参数** | [增强命令指南](PLUS_COMMAND_GUIDE.md) |
|
||||||
|
| **如何使用 LLM** | [LLM API](api/llm-api.md) |
|
||||||
|
| **如何操作数据库** | [数据库 API](api/database-api.md) |
|
||||||
|
| **如何读取配置** | [配置 API](api/config-api.md) / [配置系统指南](configuration-guide.md) |
|
||||||
|
| **如何获取消息历史** | [消息 API](api/message-api.md) / [聊天流 API](api/chat-api.md) |
|
||||||
|
| **如何发送表情包** | [表情包 API](api/emoji-api.md) |
|
||||||
|
| **如何记录日志** | [日志 API](api/logging-api.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 遇到问题?
|
||||||
|
|
||||||
|
### 第一步:查看故障排除指南
|
||||||
|
👉 [故障排除指南](troubleshooting-guide.md) 包含10个最常见问题的解决方案
|
||||||
|
|
||||||
|
### 第二步:查看相关文档
|
||||||
|
- **插件无法加载?** → [快速开始指南](quick-start.md)
|
||||||
|
- **命令无响应?** → [增强命令指南](PLUS_COMMAND_GUIDE.md)
|
||||||
|
- **Action 不触发?** → [Action 组件详解](action-components.md)
|
||||||
|
- **配置不生效?** → [配置系统指南](configuration-guide.md)
|
||||||
|
|
||||||
|
### 第三步:检查日志
|
||||||
|
查看 `logs/app_*.jsonl` 获取详细错误信息
|
||||||
|
|
||||||
|
### 第四步:寻求帮助
|
||||||
|
- 在线文档:https://mofox-studio.github.io/MoFox-Bot-Docs/
|
||||||
|
- GitHub Issues:提交详细的问题报告
|
||||||
|
- 社区讨论:加入开发者社区
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 重要提示
|
||||||
|
|
||||||
|
### ⚠️ 常见陷阱
|
||||||
|
|
||||||
|
1. **不要使用 `BaseCommand`**
|
||||||
|
- ✅ 使用:`PlusCommand`
|
||||||
|
- ❌ 避免:`BaseCommand`(仅供框架内部使用)
|
||||||
|
|
||||||
|
2. **不要在返回值中返回用户消息**
|
||||||
|
- ✅ 使用:`await self.send_text("消息")`
|
||||||
|
- ❌ 避免:`return True, "消息", True`
|
||||||
|
|
||||||
|
3. **手动创建 ComponentInfo 时必须指定 component_type**
|
||||||
|
- ✅ 推荐:使用 `get_action_info()` 自动生成
|
||||||
|
- ⚠️ 手动创建时:必须指定 `component_type=ComponentType.ACTION`
|
||||||
|
|
||||||
|
### 💡 最佳实践
|
||||||
|
|
||||||
|
- ✅ 总是使用类型注解
|
||||||
|
- ✅ 为 `execute()` 方法添加文档字符串
|
||||||
|
- ✅ 使用 `self.get_config()` 读取配置
|
||||||
|
- ✅ 使用异步操作 `async/await`
|
||||||
|
- ✅ 在发送消息前验证参数
|
||||||
|
- ✅ 提供清晰的错误提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 文档更新记录
|
||||||
|
|
||||||
|
### v1.1.0 (2024-12-17)
|
||||||
|
- ✨ 新增 [故障排除指南](troubleshooting-guide.md)
|
||||||
|
- ✅ 修复 [快速开始指南](quick-start.md) 中的 BaseCommand 示例
|
||||||
|
- ✅ 增强 [增强命令指南](PLUS_COMMAND_GUIDE.md) 的返回值说明
|
||||||
|
- ✅ 完善 [Action 组件](action-components.md) 的 component_type 说明
|
||||||
|
- 📝 创建本导航文档
|
||||||
|
|
||||||
|
### v1.0.0 (2024-11)
|
||||||
|
- 📚 初始文档发布
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 反馈与贡献
|
||||||
|
|
||||||
|
如果你发现文档中的错误或有改进建议:
|
||||||
|
|
||||||
|
1. **提交 Issue**:在 GitHub 仓库提交文档问题
|
||||||
|
2. **提交 PR**:直接修改文档并提交 Pull Request
|
||||||
|
3. **社区反馈**:在社区讨论中提出建议
|
||||||
|
|
||||||
|
你的反馈对我们改进文档至关重要!🙏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 开始你的插件开发之旅
|
||||||
|
|
||||||
|
准备好了吗?从这里开始:
|
||||||
|
|
||||||
|
1. 📖 阅读 [快速开始指南](quick-start.md)
|
||||||
|
2. 💻 创建你的第一个插件
|
||||||
|
3. 🔧 遇到问题查看 [故障排除指南](troubleshooting-guide.md)
|
||||||
|
4. 🚀 探索更多高级功能
|
||||||
|
|
||||||
|
**祝你开发愉快!** 🎊
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**:2024-12-17
|
||||||
|
**文档版本**:v1.1.0
|
||||||
@@ -38,11 +38,44 @@ class ExampleAction(BaseAction):
|
|||||||
执行Action的主要逻辑
|
执行Action的主要逻辑
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[bool, str]: (是否成功, 执行结果描述)
|
Tuple[bool, str]: 两个元素的元组
|
||||||
|
- bool: 是否执行成功 (True=成功, False=失败)
|
||||||
|
- str: 执行结果的简短描述(用于日志记录)
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 使用 self.send_text() 等方法发送消息给用户
|
||||||
|
- 返回值中的描述仅用于内部日志,不会发送给用户
|
||||||
"""
|
"""
|
||||||
# ---- 执行动作的逻辑 ----
|
# 发送消息给用户
|
||||||
|
await self.send_text("这是发给用户的消息")
|
||||||
|
|
||||||
|
# 返回执行结果(用于日志)
|
||||||
return True, "执行成功"
|
return True, "执行成功"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### execute() 返回值 vs Command 返回值
|
||||||
|
|
||||||
|
⚠️ **重要:Action 和 Command 的返回值不同!**
|
||||||
|
|
||||||
|
| 组件类型 | 返回值 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| **Action** | `Tuple[bool, str]` | 2个元素:成功标志、日志描述 |
|
||||||
|
| **Command** | `Tuple[bool, Optional[str], bool]` | 3个元素:成功标志、日志描述、拦截标志 |
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Action 返回值
|
||||||
|
async def execute(self) -> Tuple[bool, str]:
|
||||||
|
await self.send_text("给用户的消息")
|
||||||
|
return True, "日志:执行了XX动作" # 2个元素
|
||||||
|
|
||||||
|
# Command 返回值
|
||||||
|
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
await self.send_text("给用户的消息")
|
||||||
|
return True, "日志:执行了XX命令", True # 3个元素
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### associated_types: 该Action会发送的消息类型,例如文本、表情等。
|
#### associated_types: 该Action会发送的消息类型,例如文本、表情等。
|
||||||
|
|
||||||
这部分由Adapter传递给处理器。
|
这部分由Adapter传递给处理器。
|
||||||
@@ -68,6 +101,65 @@ class ExampleAction(BaseAction):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 组件信息注册说明
|
||||||
|
|
||||||
|
### 自动生成 ComponentInfo(推荐)
|
||||||
|
|
||||||
|
大多数情况下,你不需要手动创建 `ActionInfo` 对象。系统提供了 `get_action_info()` 方法来自动生成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 推荐的方式 - 自动生成
|
||||||
|
class HelloAction(BaseAction):
|
||||||
|
action_name = "hello"
|
||||||
|
action_description = "问候动作"
|
||||||
|
# ... 其他配置 ...
|
||||||
|
|
||||||
|
# 在插件中注册
|
||||||
|
def get_plugin_components(self):
|
||||||
|
return [
|
||||||
|
(HelloAction.get_action_info(), HelloAction), # 自动生成 ActionInfo
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动创建 ActionInfo(高级用法)
|
||||||
|
|
||||||
|
⚠️ **重要:如果手动创建 ActionInfo,必须指定 `component_type` 参数!**
|
||||||
|
|
||||||
|
当你需要自定义 `ActionInfo` 时(例如动态生成组件),必须手动指定 `component_type`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.plugin_system import ActionInfo, ComponentType
|
||||||
|
|
||||||
|
# ❌ 错误 - 缺少 component_type
|
||||||
|
action_info = ActionInfo(
|
||||||
|
name="hello",
|
||||||
|
description="问候动作"
|
||||||
|
# 错误:会报错 "missing required argument: 'component_type'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ 正确 - 必须指定 component_type
|
||||||
|
action_info = ActionInfo(
|
||||||
|
name="hello",
|
||||||
|
description="问候动作",
|
||||||
|
component_type=ComponentType.ACTION # 必须指定!
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么需要手动指定?**
|
||||||
|
|
||||||
|
- `get_action_info()` 方法会自动设置 `component_type`
|
||||||
|
- 但手动创建时,系统无法自动推断类型,必须明确指定
|
||||||
|
|
||||||
|
**什么时候需要手动创建?**
|
||||||
|
|
||||||
|
- 动态生成组件
|
||||||
|
- 自定义 `get_handler_info()` 方法
|
||||||
|
- 需要特殊的 ComponentInfo 配置
|
||||||
|
|
||||||
|
大多数情况下,直接使用 `get_action_info()` 即可,无需手动创建。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🎯 Action 调用的决策机制
|
## 🎯 Action 调用的决策机制
|
||||||
|
|
||||||
Action采用**两层决策机制**来优化性能和决策质量:
|
Action采用**两层决策机制**来优化性能和决策质量:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
## 新手入门
|
## 新手入门
|
||||||
|
|
||||||
- [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件
|
- [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件
|
||||||
|
- [🔧 故障排除指南](troubleshooting-guide.md) - 快速解决常见问题 ⭐ **新增**
|
||||||
|
|
||||||
## 组件功能详解
|
## 组件功能详解
|
||||||
|
|
||||||
|
|||||||
@@ -195,29 +195,35 @@ Command是最简单,最直接的响应,不由LLM判断选择使用
|
|||||||
```python
|
```python
|
||||||
# 在现有代码基础上,添加Command组件
|
# 在现有代码基础上,添加Command组件
|
||||||
import datetime
|
import datetime
|
||||||
from src.plugin_system import BaseCommand
|
from src.plugin_system import PlusCommand, CommandArgs
|
||||||
#导入Command基类
|
# 导入增强命令基类 - 推荐使用!
|
||||||
|
|
||||||
class TimeCommand(BaseCommand):
|
class TimeCommand(PlusCommand):
|
||||||
"""时间查询Command - 响应/time命令"""
|
"""时间查询Command - 响应/time命令"""
|
||||||
|
|
||||||
command_name = "time"
|
command_name = "time"
|
||||||
command_description = "查询当前时间"
|
command_description = "查询当前时间"
|
||||||
|
|
||||||
# === 命令设置(必须填写)===
|
# 注意:使用 PlusCommand 不需要 command_pattern,会自动生成!
|
||||||
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
|
|
||||||
|
|
||||||
async def execute(self) -> Tuple[bool, Optional[str], bool]:
|
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||||
"""执行时间查询"""
|
"""执行时间查询
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: 命令参数(本例中不使用)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(成功标志, 日志描述, 是否拦截消息)
|
||||||
|
"""
|
||||||
# 获取当前时间
|
# 获取当前时间
|
||||||
time_format: str = "%Y-%m-%d %H:%M:%S"
|
time_format: str = "%Y-%m-%d %H:%M:%S"
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
time_str = now.strftime(time_format)
|
time_str = now.strftime(time_format)
|
||||||
|
|
||||||
# 发送时间信息
|
# 发送时间信息给用户
|
||||||
message = f"⏰ 当前时间:{time_str}"
|
await self.send_text(f"⏰ 当前时间:{time_str}")
|
||||||
await self.send_text(message)
|
|
||||||
|
|
||||||
|
# 返回:成功、日志描述、拦截消息
|
||||||
return True, f"显示了当前时间: {time_str}", True
|
return True, f"显示了当前时间: {time_str}", True
|
||||||
|
|
||||||
@register_plugin
|
@register_plugin
|
||||||
@@ -239,14 +245,29 @@ class HelloWorldPlugin(BasePlugin):
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
同样的,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `TimeCommand` 注册为插件的一个组件。
|
同样的,我们通过 `get_plugin_components()` 方法,通过调用`get_command_info()`这个内置方法将 `TimeCommand` 注册为插件的一个组件。
|
||||||
|
|
||||||
**Command组件解释:**
|
**Command组件解释:**
|
||||||
|
|
||||||
- `command_pattern` 使用正则表达式匹配用户输入
|
> ⚠️ **重要:请使用 PlusCommand 而不是 BaseCommand!**
|
||||||
- `^/time$` 表示精确匹配 "/time"
|
>
|
||||||
|
> - ✅ **PlusCommand**:推荐使用,自动处理参数解析,无需编写正则表达式
|
||||||
|
> - ❌ **BaseCommand**:仅供框架内部使用,插件开发者不应直接使用
|
||||||
|
|
||||||
有关 Command 组件的更多信息,请参考 [Command组件指南](./command-components.md)。
|
**PlusCommand 的优势:**
|
||||||
|
- ✅ 无需编写 `command_pattern` 正则表达式
|
||||||
|
- ✅ 自动解析命令参数(通过 `CommandArgs`)
|
||||||
|
- ✅ 支持命令别名(`command_aliases`)
|
||||||
|
- ✅ 更简单的 API,更容易上手
|
||||||
|
|
||||||
|
**execute() 方法说明:**
|
||||||
|
- 参数:`args: CommandArgs` - 包含解析后的命令参数
|
||||||
|
- 返回值:`(bool, str, bool)` 三元组
|
||||||
|
- `bool`:命令是否执行成功
|
||||||
|
- `str`:日志描述(**不是发给用户的消息**)
|
||||||
|
- `bool`:是否拦截消息,阻止后续处理
|
||||||
|
|
||||||
|
有关增强命令的详细信息,请参考 [增强命令指南](./PLUS_COMMAND_GUIDE.md)。
|
||||||
|
|
||||||
### 8. 测试时间查询Command
|
### 8. 测试时间查询Command
|
||||||
|
|
||||||
@@ -377,28 +398,31 @@ class HelloAction(BaseAction):
|
|||||||
|
|
||||||
return True, "发送了问候消息"
|
return True, "发送了问候消息"
|
||||||
|
|
||||||
class TimeCommand(BaseCommand):
|
class TimeCommand(PlusCommand):
|
||||||
"""时间查询Command - 响应/time命令"""
|
"""时间查询Command - 响应/time命令"""
|
||||||
|
|
||||||
command_name = "time"
|
command_name = "time"
|
||||||
command_description = "查询当前时间"
|
command_description = "查询当前时间"
|
||||||
|
|
||||||
# === 命令设置(必须填写)===
|
# 注意:PlusCommand 不需要 command_pattern!
|
||||||
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
|
|
||||||
|
|
||||||
async def execute(self) -> Tuple[bool, str, bool]:
|
async def execute(self, args: CommandArgs) -> Tuple[bool, str, bool]:
|
||||||
"""执行时间查询"""
|
"""执行时间查询
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: 命令参数对象
|
||||||
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
# 获取当前时间
|
# 从配置获取时间格式
|
||||||
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
|
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
time_str = now.strftime(time_format)
|
time_str = now.strftime(time_format)
|
||||||
|
|
||||||
# 发送时间信息
|
# 发送时间信息给用户
|
||||||
message = f"⏰ 当前时间:{time_str}"
|
await self.send_text(f"⏰ 当前时间:{time_str}")
|
||||||
await self.send_text(message)
|
|
||||||
|
|
||||||
|
# 返回:成功、日志描述、拦截消息
|
||||||
return True, f"显示了当前时间: {time_str}", True
|
return True, f"显示了当前时间: {time_str}", True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
395
docs/plugins/troubleshooting-guide.md
Normal file
395
docs/plugins/troubleshooting-guide.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# 🔧 插件开发故障排除指南
|
||||||
|
|
||||||
|
本指南帮助你快速解决 MoFox-Bot 插件开发中的常见问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 快速诊断清单
|
||||||
|
|
||||||
|
遇到问题时,首先按照以下步骤检查:
|
||||||
|
|
||||||
|
1. ✅ 检查日志文件 `logs/app_*.jsonl`
|
||||||
|
2. ✅ 确认插件已在 `_manifest.json` 中正确配置
|
||||||
|
3. ✅ 验证你使用的是 `PlusCommand` 而不是 `BaseCommand`
|
||||||
|
4. ✅ 检查 `execute()` 方法签名是否正确
|
||||||
|
5. ✅ 确认返回值格式正确
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 严重问题:插件无法加载
|
||||||
|
|
||||||
|
### 错误 #1: "未检测到插件"
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
- 插件目录存在,但日志中没有加载信息
|
||||||
|
- `get_plugin_components()` 返回空列表
|
||||||
|
|
||||||
|
**可能原因与解决方案**:
|
||||||
|
|
||||||
|
#### ❌ 缺少 `@register_plugin` 装饰器
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误 - 缺少装饰器
|
||||||
|
class MyPlugin(BasePlugin): # 不会被检测到
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 正确 - 添加装饰器
|
||||||
|
@register_plugin # 必须添加!
|
||||||
|
class MyPlugin(BasePlugin):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ❌ `plugin.py` 文件不存在或位置错误
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/
|
||||||
|
└── my_plugin/
|
||||||
|
├── _manifest.json ✅
|
||||||
|
└── plugin.py ✅ 必须在这里
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ❌ `_manifest.json` 格式错误
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"manifest_version": 1,
|
||||||
|
"name": "My Plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "插件描述",
|
||||||
|
"author": {
|
||||||
|
"name": "Your Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 错误 #2: "ActionInfo.__init__() missing required argument: 'component_type'"
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
```
|
||||||
|
TypeError: ActionInfo.__init__() missing 1 required positional argument: 'component_type'
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**:手动创建 `ActionInfo` 时未指定 `component_type` 参数
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.plugin_system import ActionInfo, ComponentType
|
||||||
|
|
||||||
|
# ❌ 错误 - 缺少 component_type
|
||||||
|
action_info = ActionInfo(
|
||||||
|
name="my_action",
|
||||||
|
description="我的动作"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ 正确方法 1 - 使用自动生成(推荐)
|
||||||
|
class MyAction(BaseAction):
|
||||||
|
action_name = "my_action"
|
||||||
|
action_description = "我的动作"
|
||||||
|
|
||||||
|
def get_plugin_components(self):
|
||||||
|
return [
|
||||||
|
(MyAction.get_action_info(), MyAction) # 自动生成,推荐!
|
||||||
|
]
|
||||||
|
|
||||||
|
# ✅ 正确方法 2 - 手动指定 component_type
|
||||||
|
action_info = ActionInfo(
|
||||||
|
name="my_action",
|
||||||
|
description="我的动作",
|
||||||
|
component_type=ComponentType.ACTION # 必须指定!
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 命令问题:命令无响应
|
||||||
|
|
||||||
|
### 错误 #3: 命令被识别但不执行
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
- 输入 `/mycommand` 后没有任何反应
|
||||||
|
- 日志显示命令已匹配但未执行
|
||||||
|
|
||||||
|
**可能原因与解决方案**:
|
||||||
|
|
||||||
|
#### ❌ 使用了 `BaseCommand` 而不是 `PlusCommand`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 错误 - 使用 BaseCommand
|
||||||
|
from src.plugin_system import BaseCommand
|
||||||
|
|
||||||
|
class MyCommand(BaseCommand): # 不推荐!
|
||||||
|
command_name = "mycommand"
|
||||||
|
command_pattern = r"^/mycommand$" # 需要手动写正则
|
||||||
|
|
||||||
|
async def execute(self): # 签名错误!
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ✅ 正确 - 使用 PlusCommand
|
||||||
|
from src.plugin_system import PlusCommand, CommandArgs
|
||||||
|
|
||||||
|
class MyCommand(PlusCommand): # 推荐!
|
||||||
|
command_name = "mycommand"
|
||||||
|
# 不需要 command_pattern,会自动生成!
|
||||||
|
|
||||||
|
async def execute(self, args: CommandArgs): # 正确签名
|
||||||
|
await self.send_text("命令执行成功")
|
||||||
|
return True, "执行了mycommand", True
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ❌ `execute()` 方法签名错误
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 错误的签名(缺少 args 参数)
|
||||||
|
async def execute(self) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ❌ 错误的签名(参数类型错误)
|
||||||
|
async def execute(self, args: list[str]) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ✅ 正确的签名
|
||||||
|
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
await self.send_text("响应用户")
|
||||||
|
return True, "日志描述", True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 错误 #4: 命令发送了消息但用户没收到
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
- 日志显示命令执行成功
|
||||||
|
- 但用户没有收到任何消息
|
||||||
|
|
||||||
|
**原因**:在返回值中返回消息,而不是使用 `self.send_text()`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 错误 - 在返回值中返回消息
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
message = "这是给用户的消息"
|
||||||
|
return True, message, True # 这不会发送给用户!
|
||||||
|
|
||||||
|
# ✅ 正确 - 使用 self.send_text()
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
# 发送消息给用户
|
||||||
|
await self.send_text("这是给用户的消息")
|
||||||
|
|
||||||
|
# 返回日志描述(不是用户消息)
|
||||||
|
return True, "执行了某个操作", True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 错误 #5: "notice处理失败" 或重复消息
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
- 日志中出现 "notice处理失败"
|
||||||
|
- 用户收到重复的消息
|
||||||
|
|
||||||
|
**原因**:同时使用了 `send_api.send_text()` 和返回消息
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 错误 - 混用不同的发送方式
|
||||||
|
from src.plugin_system.apis.chat_api import send_api
|
||||||
|
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
await send_api.send_text(self.stream_id, "消息1") # 不要这样做
|
||||||
|
return True, "消息2", True # 也不要返回消息
|
||||||
|
|
||||||
|
# ✅ 正确 - 只使用 self.send_text()
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
await self.send_text("这是唯一的消息") # 推荐方式
|
||||||
|
return True, "日志:执行成功", True # 仅用于日志
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 配置问题
|
||||||
|
|
||||||
|
### 错误 #6: 配置警告 "配置中不存在字空间或键"
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
```
|
||||||
|
获取全局配置 plugins.my_plugin 失败: "配置中不存在字空间或键 'plugins'"
|
||||||
|
```
|
||||||
|
|
||||||
|
**这是正常的吗?**
|
||||||
|
|
||||||
|
✅ **是的,这是正常行为!** 不需要修复。
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 系统首先尝试从全局配置加载:`config/plugins/my_plugin/config.toml`
|
||||||
|
- 如果不存在,会自动回退到插件本地配置:`plugins/my_plugin/config.toml`
|
||||||
|
- 这个警告可以安全忽略
|
||||||
|
|
||||||
|
**如果你想消除警告**:
|
||||||
|
1. 在 `config/plugins/` 目录创建你的插件配置目录
|
||||||
|
2. 或者直接忽略 - 使用本地配置完全正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 返回值问题
|
||||||
|
|
||||||
|
### 错误 #7: 返回值格式错误
|
||||||
|
|
||||||
|
**Action 返回值** (2个元素):
|
||||||
|
```python
|
||||||
|
async def execute(self) -> Tuple[bool, str]:
|
||||||
|
await self.send_text("消息")
|
||||||
|
return True, "日志描述" # 2个元素
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command 返回值** (3个元素):
|
||||||
|
```python
|
||||||
|
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||||
|
await self.send_text("消息")
|
||||||
|
return True, "日志描述", True # 3个元素(增加了拦截标志)
|
||||||
|
```
|
||||||
|
|
||||||
|
**对比表格**:
|
||||||
|
|
||||||
|
| 组件类型 | 返回值 | 元素说明 |
|
||||||
|
|----------|--------|----------|
|
||||||
|
| **Action** | `(bool, str)` | (成功标志, 日志描述) |
|
||||||
|
| **Command** | `(bool, str, bool)` | (成功标志, 日志描述, 拦截标志) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 参数解析问题
|
||||||
|
|
||||||
|
### 错误 #8: 无法获取命令参数
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
- `args` 为空或不包含预期的参数
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
# 检查是否有参数
|
||||||
|
if args.is_empty():
|
||||||
|
await self.send_text("❌ 缺少参数\n用法: /command <参数>")
|
||||||
|
return True, "缺少参数", True
|
||||||
|
|
||||||
|
# 获取原始参数字符串
|
||||||
|
raw_input = args.get_raw()
|
||||||
|
|
||||||
|
# 获取解析后的参数列表
|
||||||
|
arg_list = args.get_args()
|
||||||
|
|
||||||
|
# 获取第一个参数
|
||||||
|
first_arg = args.get_first("默认值")
|
||||||
|
|
||||||
|
# 获取指定索引的参数
|
||||||
|
second_arg = args.get_arg(1, "默认值")
|
||||||
|
|
||||||
|
# 检查标志
|
||||||
|
if args.has_flag("--verbose"):
|
||||||
|
# 处理 --verbose 模式
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 获取标志的值
|
||||||
|
output = args.get_flag_value("--output", "default.txt")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 类型注解问题
|
||||||
|
|
||||||
|
### 错误 #9: IDE 报类型错误
|
||||||
|
|
||||||
|
**解决方案**:确保使用正确的类型导入
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Tuple, Optional, List, Type
|
||||||
|
from src.plugin_system import (
|
||||||
|
BasePlugin,
|
||||||
|
PlusCommand,
|
||||||
|
BaseAction,
|
||||||
|
CommandArgs,
|
||||||
|
ComponentInfo,
|
||||||
|
CommandInfo,
|
||||||
|
ActionInfo,
|
||||||
|
ComponentType
|
||||||
|
)
|
||||||
|
|
||||||
|
# 正确的类型注解
|
||||||
|
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
||||||
|
return [
|
||||||
|
(MyCommand.get_command_info(), MyCommand),
|
||||||
|
(MyAction.get_action_info(), MyAction)
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 性能问题
|
||||||
|
|
||||||
|
### 错误 #10: 插件响应缓慢
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
|
||||||
|
1. **阻塞操作**:在 `execute()` 中使用了同步 I/O
|
||||||
|
2. **大量数据处理**:在主线程处理大文件或复杂计算
|
||||||
|
3. **频繁的数据库查询**:每次都查询数据库
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def execute(self, args: CommandArgs):
|
||||||
|
# ✅ 使用异步操作
|
||||||
|
result = await some_async_function()
|
||||||
|
|
||||||
|
# ✅ 对于同步操作,使用 asyncio.to_thread
|
||||||
|
result = await asyncio.to_thread(blocking_function)
|
||||||
|
|
||||||
|
# ✅ 批量数据库操作
|
||||||
|
from src.common.database.optimization.batch_scheduler import get_batch_scheduler
|
||||||
|
scheduler = get_batch_scheduler()
|
||||||
|
await scheduler.schedule_batch_insert(Model, data_list)
|
||||||
|
|
||||||
|
return True, "执行成功", True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 获取帮助
|
||||||
|
|
||||||
|
如果以上方案都无法解决你的问题:
|
||||||
|
|
||||||
|
1. **查看日志**:检查 `logs/app_*.jsonl` 获取详细错误信息
|
||||||
|
2. **查阅文档**:
|
||||||
|
- [快速开始指南](./quick-start.md)
|
||||||
|
- [增强命令指南](./PLUS_COMMAND_GUIDE.md)
|
||||||
|
- [Action组件指南](./action-components.md)
|
||||||
|
3. **在线文档**:https://mofox-studio.github.io/MoFox-Bot-Docs/
|
||||||
|
4. **提交 Issue**:在 GitHub 仓库提交详细的问题报告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 最佳实践速查
|
||||||
|
|
||||||
|
| 场景 | 推荐做法 | 避免 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 创建命令 | 使用 `PlusCommand` | ❌ 使用 `BaseCommand` |
|
||||||
|
| 发送消息 | `await self.send_text()` | ❌ 在返回值中返回消息 |
|
||||||
|
| 注册组件 | 使用 `get_action_info()` | ❌ 手动创建不带 `component_type` 的 Info |
|
||||||
|
| 参数处理 | 使用 `CommandArgs` 方法 | ❌ 手动解析字符串 |
|
||||||
|
| 异步操作 | 使用 `async/await` | ❌ 使用同步阻塞操作 |
|
||||||
|
| 配置读取 | `self.get_config()` | ❌ 硬编码配置值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**:2024-12-17
|
||||||
|
**版本**:v1.0.0
|
||||||
|
|
||||||
|
有问题欢迎反馈,帮助我们改进这份指南!
|
||||||
297
docs/slow_query_monitoring_guide.md
Normal file
297
docs/slow_query_monitoring_guide.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# 慢查询监控实现指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
我们已经完整实现了数据库慢查询监控系统,包括:
|
||||||
|
- ✅ 慢查询自动检测和收集(**默认关闭**)
|
||||||
|
- ✅ 实时性能监控和统计
|
||||||
|
- ✅ 详细的文本和HTML报告生成
|
||||||
|
- ✅ 优化建议和性能分析
|
||||||
|
- ✅ 用户可选的启用/禁用开关
|
||||||
|
|
||||||
|
## 快速启用
|
||||||
|
|
||||||
|
### 方法 1:配置文件启用(推荐)
|
||||||
|
|
||||||
|
编辑 `config/bot_config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[database]
|
||||||
|
enable_slow_query_logging = true # 改为 true 启用
|
||||||
|
slow_query_threshold = 0.5 # 设置阈值(秒)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2:代码动态启用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import enable_slow_query_monitoring
|
||||||
|
|
||||||
|
# 启用监控
|
||||||
|
enable_slow_query_monitoring()
|
||||||
|
|
||||||
|
# 禁用监控
|
||||||
|
disable_slow_query_monitoring()
|
||||||
|
|
||||||
|
# 检查状态
|
||||||
|
if is_slow_query_monitoring_enabled():
|
||||||
|
print("慢查询监控已启用")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
### bot_config.toml
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[database]
|
||||||
|
# 慢查询监控配置(默认关闭,需要时设置 enable_slow_query_logging = true 启用)
|
||||||
|
enable_slow_query_logging = false # 是否启用慢查询日志(设置为 true 启用)
|
||||||
|
slow_query_threshold = 0.5 # 慢查询阈值(秒)
|
||||||
|
query_timeout = 30 # 查询超时时间(秒)
|
||||||
|
collect_slow_queries = true # 是否收集慢查询统计
|
||||||
|
slow_query_buffer_size = 100 # 慢查询缓冲大小(最近N条)
|
||||||
|
```
|
||||||
|
|
||||||
|
**推荐参数**:
|
||||||
|
- **生产环境(推荐)**:`enable_slow_query_logging = false` - 最小性能开销
|
||||||
|
- **测试环境**:`enable_slow_query_logging = true` + `slow_query_threshold = 0.5`
|
||||||
|
- **开发环境**:`enable_slow_query_logging = true` + `slow_query_threshold = 0.1` - 捕获所有慢查询
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 1. 自动监控(推荐)
|
||||||
|
|
||||||
|
启用后,所有使用 `@measure_time()` 装饰器的函数都会被监控:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import measure_time
|
||||||
|
|
||||||
|
@measure_time() # 使用配置中的阈值
|
||||||
|
async def my_database_query():
|
||||||
|
return result
|
||||||
|
|
||||||
|
@measure_time(log_slow=1.0) # 自定义阈值
|
||||||
|
async def another_query():
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 手动记录慢查询
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import record_slow_query
|
||||||
|
|
||||||
|
record_slow_query(
|
||||||
|
operation_name="custom_query",
|
||||||
|
execution_time=1.5,
|
||||||
|
sql="SELECT * FROM users WHERE id = ?",
|
||||||
|
args=(123,)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 获取慢查询报告
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import get_slow_query_report
|
||||||
|
|
||||||
|
report = get_slow_query_report()
|
||||||
|
|
||||||
|
print(f"总慢查询数: {report['total']}")
|
||||||
|
print(f"阈值: {report['threshold']}")
|
||||||
|
|
||||||
|
for op in report['top_operations']:
|
||||||
|
print(f"{op['operation']}: {op['count']} 次")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 在代码中使用分析工具
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils.slow_query_analyzer import SlowQueryAnalyzer
|
||||||
|
|
||||||
|
# 生成文本报告
|
||||||
|
text_report = SlowQueryAnalyzer.generate_text_report()
|
||||||
|
print(text_report)
|
||||||
|
|
||||||
|
# 生成HTML报告
|
||||||
|
SlowQueryAnalyzer.generate_html_report("reports/slow_query.html")
|
||||||
|
|
||||||
|
# 获取最慢的查询
|
||||||
|
slowest = SlowQueryAnalyzer.get_slowest_queries(limit=20)
|
||||||
|
for query in slowest:
|
||||||
|
print(f"{query.operation_name}: {query.execution_time:.3f}s")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输出示例
|
||||||
|
|
||||||
|
### 启用时的初始化
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 慢查询监控已启用 (阈值: 0.5s, 缓冲: 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行时的慢查询告警
|
||||||
|
|
||||||
|
```
|
||||||
|
🐢 get_user_by_id 执行缓慢: 0.752s (阈值: 0.500s)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关闭时的性能报告(仅在启用时输出)
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
数据库性能统计
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
操作统计:
|
||||||
|
get_user_by_id: 次数=156, 平均=0.025s, 最小=0.001s, 最大=1.203s, 错误=0, 慢查询=3
|
||||||
|
|
||||||
|
缓存:
|
||||||
|
命中=8923, 未命中=1237, 命中率=87.82%
|
||||||
|
|
||||||
|
整体:
|
||||||
|
错误率=0.00%
|
||||||
|
慢查询总数=3
|
||||||
|
慢查询阈值=0.500s
|
||||||
|
|
||||||
|
🐢 慢查询报告:
|
||||||
|
按操作排名(Top 10):
|
||||||
|
1. get_user_by_id: 次数=3, 平均=0.752s, 最大=1.203s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: 如何知道监控是否启用了?
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import is_slow_query_monitoring_enabled
|
||||||
|
|
||||||
|
if is_slow_query_monitoring_enabled():
|
||||||
|
print("✅ 慢查询监控已启用")
|
||||||
|
else:
|
||||||
|
print("❌ 慢查询监控已禁用")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: 如何临时启用/禁用?
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import enable_slow_query_monitoring, disable_slow_query_monitoring
|
||||||
|
|
||||||
|
# 临时启用
|
||||||
|
enable_slow_query_monitoring()
|
||||||
|
|
||||||
|
# ... 执行需要监控的代码 ...
|
||||||
|
|
||||||
|
# 临时禁用
|
||||||
|
disable_slow_query_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: 默认关闭会影响性能吗?
|
||||||
|
|
||||||
|
完全不会。关闭后没有任何性能开销。
|
||||||
|
|
||||||
|
### Q4: 监控数据会持久化吗?
|
||||||
|
|
||||||
|
目前使用内存缓冲(默认最近 100 条),系统关闭时会输出报告。
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 生产环境配置
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# config/bot_config.toml
|
||||||
|
[database]
|
||||||
|
enable_slow_query_logging = false # 默认关闭
|
||||||
|
```
|
||||||
|
|
||||||
|
只在需要调试性能问题时临时启用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.common.database.utils import enable_slow_query_monitoring
|
||||||
|
|
||||||
|
# 在某个插件中启用
|
||||||
|
enable_slow_query_monitoring()
|
||||||
|
|
||||||
|
# 执行和监控需要优化的代码
|
||||||
|
|
||||||
|
disable_slow_query_monitoring()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 开发/测试环境配置
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# config/bot_config.toml
|
||||||
|
[database]
|
||||||
|
enable_slow_query_logging = true # 启用
|
||||||
|
slow_query_threshold = 0.5 # 500ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用 @measure_time() 装饰器
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ 推荐:自动监控所有 I/O 操作
|
||||||
|
@measure_time()
|
||||||
|
async def get_user_info(user_id: str):
|
||||||
|
return await user_crud.get_by_id(user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|-----|------|
|
||||||
|
| `monitoring.py` | 核心监控器,启用/禁用逻辑 |
|
||||||
|
| `decorators.py` | `@measure_time()` 装饰器 |
|
||||||
|
| `slow_query_analyzer.py` | 分析和报告生成 |
|
||||||
|
|
||||||
|
### 启用流程
|
||||||
|
|
||||||
|
```
|
||||||
|
enable_slow_query_logging = true
|
||||||
|
↓
|
||||||
|
main.py: set_slow_query_config()
|
||||||
|
↓
|
||||||
|
get_monitor().enable()
|
||||||
|
↓
|
||||||
|
is_enabled() = True
|
||||||
|
↓
|
||||||
|
record_operation() 检查并记录慢查询
|
||||||
|
↓
|
||||||
|
输出 🐢 警告信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 禁用流程
|
||||||
|
|
||||||
|
```
|
||||||
|
enable_slow_query_logging = false
|
||||||
|
↓
|
||||||
|
is_enabled() = False
|
||||||
|
↓
|
||||||
|
record_operation() 不记录慢查询
|
||||||
|
↓
|
||||||
|
无性能开销
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能影响
|
||||||
|
|
||||||
|
### 启用时
|
||||||
|
|
||||||
|
- CPU 开销: < 0.1%(仅在超过阈值时记录)
|
||||||
|
- 内存开销: ~50KB(缓冲 100 条慢查询)
|
||||||
|
|
||||||
|
### 禁用时
|
||||||
|
|
||||||
|
- CPU 开销: ~0%
|
||||||
|
- 内存开销: 0 KB(不收集数据)
|
||||||
|
|
||||||
|
**结论**:可以安全地在生产环境中默认禁用,需要时启用。
|
||||||
|
|
||||||
|
## 下一步优化
|
||||||
|
|
||||||
|
1. **自动启用**:在检测到性能问题时自动启用
|
||||||
|
2. **告警系统**:当慢查询比例超过阈值时发送告警
|
||||||
|
3. **Prometheus 集成**:导出监控指标
|
||||||
|
4. **Grafana 仪表板**:实时可视化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档更新**: 2025-12-17
|
||||||
|
**状态**: ✅ 默认关闭,用户可选启用
|
||||||
@@ -33,6 +33,13 @@ from .monitoring import (
|
|||||||
record_cache_miss,
|
record_cache_miss,
|
||||||
record_operation,
|
record_operation,
|
||||||
reset_stats,
|
reset_stats,
|
||||||
|
get_slow_queries,
|
||||||
|
get_slow_query_report,
|
||||||
|
record_slow_query,
|
||||||
|
set_slow_query_config,
|
||||||
|
enable_slow_query_monitoring,
|
||||||
|
disable_slow_query_monitoring,
|
||||||
|
is_slow_query_monitoring_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -57,6 +64,13 @@ __all__ = [
|
|||||||
"record_cache_miss",
|
"record_cache_miss",
|
||||||
"record_operation",
|
"record_operation",
|
||||||
"reset_stats",
|
"reset_stats",
|
||||||
|
"get_slow_queries",
|
||||||
|
"get_slow_query_report",
|
||||||
|
"record_slow_query",
|
||||||
|
"set_slow_query_config",
|
||||||
|
"enable_slow_query_monitoring",
|
||||||
|
"disable_slow_query_monitoring",
|
||||||
|
"is_slow_query_monitoring_enabled",
|
||||||
# 装饰器
|
# 装饰器
|
||||||
"retry",
|
"retry",
|
||||||
"timeout",
|
"timeout",
|
||||||
|
|||||||
@@ -213,37 +213,68 @@ def cached(
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def measure_time(log_slow: float | None = None):
|
def measure_time(log_slow: float | None = None, operation_name: str | None = None):
|
||||||
"""性能测量装饰器
|
"""性能测量装饰器
|
||||||
|
|
||||||
测量函数执行时间,可选择性记录慢查询
|
测量函数执行时间,可选择性记录慢查询并集成到监控系统
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
log_slow: 慢查询阈值(秒),超过此时间会记录warning日志
|
log_slow: 慢查询阈值(秒),None 表示使用配置中的阈值,0 表示禁用
|
||||||
|
operation_name: 操作名称,用于监控统计,None 表示使用函数名
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@measure_time(log_slow=1.0)
|
@measure_time(log_slow=1.0)
|
||||||
async def complex_query():
|
async def complex_query():
|
||||||
return await session.execute(stmt)
|
return await session.execute(stmt)
|
||||||
|
|
||||||
|
@measure_time() # 使用配置的阈值
|
||||||
|
async def database_query():
|
||||||
|
return await session.execute(stmt)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
|
def decorator(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
|
from src.common.database.utils.monitoring import get_monitor
|
||||||
|
|
||||||
|
# 确定操作名称
|
||||||
|
op_name = operation_name or func.__name__
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
success = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await func(*args, **kwargs)
|
result = await func(*args, **kwargs)
|
||||||
|
success = True
|
||||||
return result
|
return result
|
||||||
finally:
|
finally:
|
||||||
elapsed = time.perf_counter() - start_time
|
elapsed = time.perf_counter() - start_time
|
||||||
|
|
||||||
if log_slow and elapsed > log_slow:
|
# 获取监控器
|
||||||
|
monitor = get_monitor()
|
||||||
|
|
||||||
|
# 记录到监控系统
|
||||||
|
if success:
|
||||||
|
monitor.record_operation(op_name, elapsed, success=True)
|
||||||
|
|
||||||
|
# 只在监控启用时检查慢查询
|
||||||
|
if monitor.is_enabled():
|
||||||
|
# 判断是否为慢查询
|
||||||
|
threshold = log_slow
|
||||||
|
if threshold is None:
|
||||||
|
# 使用配置中的阈值
|
||||||
|
threshold = monitor.get_metrics().slow_query_threshold
|
||||||
|
|
||||||
|
if threshold > 0 and elapsed > threshold:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{func.__name__} 执行缓慢: {elapsed:.3f}s (阈值: {log_slow}s)"
|
f"🐢 {func.__name__} 执行缓慢: {elapsed:.3f}s (阈值: {threshold:.3f}s)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"{func.__name__} 执行时间: {elapsed:.3f}s")
|
logger.debug(f"{func.__name__} 执行时间: {elapsed:.3f}s")
|
||||||
|
else:
|
||||||
|
logger.debug(f"{func.__name__} 执行时间: {elapsed:.3f}s")
|
||||||
|
else:
|
||||||
|
monitor.record_operation(op_name, elapsed, success=False)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from collections import deque
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -12,6 +13,24 @@ from src.common.logger import get_logger
|
|||||||
logger = get_logger("database.monitoring")
|
logger = get_logger("database.monitoring")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SlowQueryRecord:
|
||||||
|
"""慢查询记录"""
|
||||||
|
|
||||||
|
operation_name: str
|
||||||
|
execution_time: float
|
||||||
|
timestamp: float
|
||||||
|
sql: str | None = None
|
||||||
|
args: tuple | None = None
|
||||||
|
stack_trace: str | None = None
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"[{self.operation_name}] {self.execution_time:.3f}s "
|
||||||
|
f"@ {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.timestamp))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OperationMetrics:
|
class OperationMetrics:
|
||||||
"""操作指标"""
|
"""操作指标"""
|
||||||
@@ -22,6 +41,7 @@ class OperationMetrics:
|
|||||||
max_time: float = 0.0
|
max_time: float = 0.0
|
||||||
error_count: int = 0
|
error_count: int = 0
|
||||||
last_execution_time: float | None = None
|
last_execution_time: float | None = None
|
||||||
|
slow_query_count: int = 0 # 该操作的慢查询数
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def avg_time(self) -> float:
|
def avg_time(self) -> float:
|
||||||
@@ -40,6 +60,10 @@ class OperationMetrics:
|
|||||||
"""记录错误"""
|
"""记录错误"""
|
||||||
self.error_count += 1
|
self.error_count += 1
|
||||||
|
|
||||||
|
def record_slow_query(self):
|
||||||
|
"""记录慢查询"""
|
||||||
|
self.slow_query_count += 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DatabaseMetrics:
|
class DatabaseMetrics:
|
||||||
@@ -64,6 +88,10 @@ class DatabaseMetrics:
|
|||||||
batch_items_total: int = 0
|
batch_items_total: int = 0
|
||||||
batch_avg_size: float = 0.0
|
batch_avg_size: float = 0.0
|
||||||
|
|
||||||
|
# 慢查询统计
|
||||||
|
slow_query_count: int = 0
|
||||||
|
slow_query_threshold: float = 0.5 # 慢查询阈值
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache_hit_rate(self) -> float:
|
def cache_hit_rate(self) -> float:
|
||||||
"""缓存命中率"""
|
"""缓存命中率"""
|
||||||
@@ -92,26 +120,83 @@ class DatabaseMonitor:
|
|||||||
|
|
||||||
_instance: Optional["DatabaseMonitor"] = None
|
_instance: Optional["DatabaseMonitor"] = None
|
||||||
_metrics: DatabaseMetrics
|
_metrics: DatabaseMetrics
|
||||||
|
_slow_queries: deque # 最近的慢查询记录
|
||||||
|
_slow_query_buffer_size: int = 100
|
||||||
|
_enabled: bool = False # 慢查询监控是否启用
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance._metrics = DatabaseMetrics()
|
cls._instance._metrics = DatabaseMetrics()
|
||||||
|
cls._instance._slow_queries = deque(maxlen=cls._slow_query_buffer_size)
|
||||||
|
cls._instance._enabled = False
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
"""启用慢查询监控"""
|
||||||
|
self._enabled = True
|
||||||
|
logger.info("✅ 慢查询监控已启用")
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
"""禁用慢查询监控"""
|
||||||
|
self._enabled = False
|
||||||
|
logger.info("❌ 慢查询监控已禁用")
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
"""检查慢查询监控是否启用"""
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
def set_slow_query_config(self, threshold: float, buffer_size: int):
|
||||||
|
"""设置慢查询配置"""
|
||||||
|
self._metrics.slow_query_threshold = threshold
|
||||||
|
self._slow_query_buffer_size = buffer_size
|
||||||
|
self._slow_queries = deque(maxlen=buffer_size)
|
||||||
|
# 设置配置时自动启用
|
||||||
|
self._enabled = True
|
||||||
|
|
||||||
def record_operation(
|
def record_operation(
|
||||||
self,
|
self,
|
||||||
operation_name: str,
|
operation_name: str,
|
||||||
execution_time: float,
|
execution_time: float,
|
||||||
success: bool = True,
|
success: bool = True,
|
||||||
|
sql: str | None = None,
|
||||||
):
|
):
|
||||||
"""记录操作"""
|
"""记录操作"""
|
||||||
metrics = self._metrics.get_operation_metrics(operation_name)
|
metrics = self._metrics.get_operation_metrics(operation_name)
|
||||||
if success:
|
if success:
|
||||||
metrics.record_success(execution_time)
|
metrics.record_success(execution_time)
|
||||||
|
|
||||||
|
# 只在启用时检查是否为慢查询
|
||||||
|
if self._enabled and execution_time > self._metrics.slow_query_threshold:
|
||||||
|
self.record_slow_query(operation_name, execution_time, sql)
|
||||||
else:
|
else:
|
||||||
metrics.record_error()
|
metrics.record_error()
|
||||||
|
|
||||||
|
def record_slow_query(
|
||||||
|
self,
|
||||||
|
operation_name: str,
|
||||||
|
execution_time: float,
|
||||||
|
sql: str | None = None,
|
||||||
|
args: tuple | None = None,
|
||||||
|
stack_trace: str | None = None,
|
||||||
|
):
|
||||||
|
"""记录慢查询"""
|
||||||
|
self._metrics.slow_query_count += 1
|
||||||
|
self._metrics.get_operation_metrics(operation_name).record_slow_query()
|
||||||
|
|
||||||
|
record = SlowQueryRecord(
|
||||||
|
operation_name=operation_name,
|
||||||
|
execution_time=execution_time,
|
||||||
|
timestamp=time.time(),
|
||||||
|
sql=sql,
|
||||||
|
args=args,
|
||||||
|
stack_trace=stack_trace,
|
||||||
|
)
|
||||||
|
self._slow_queries.append(record)
|
||||||
|
|
||||||
|
# 立即记录到日志(实时告警)
|
||||||
|
logger.warning(f"🐢 慢查询: {record}")
|
||||||
|
|
||||||
def record_connection_acquired(self):
|
def record_connection_acquired(self):
|
||||||
"""记录连接获取"""
|
"""记录连接获取"""
|
||||||
self._metrics.connection_acquired += 1
|
self._metrics.connection_acquired += 1
|
||||||
@@ -152,6 +237,81 @@ class DatabaseMonitor:
|
|||||||
"""获取指标"""
|
"""获取指标"""
|
||||||
return self._metrics
|
return self._metrics
|
||||||
|
|
||||||
|
def get_slow_queries(self, limit: int = 0) -> list[SlowQueryRecord]:
|
||||||
|
"""获取慢查询记录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: 返回数量限制,0 表示返回全部
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
慢查询记录列表
|
||||||
|
"""
|
||||||
|
records = list(self._slow_queries)
|
||||||
|
if limit > 0:
|
||||||
|
records = records[-limit:]
|
||||||
|
return records
|
||||||
|
|
||||||
|
def get_slow_query_report(self) -> dict[str, Any]:
|
||||||
|
"""获取慢查询报告"""
|
||||||
|
slow_queries = list(self._slow_queries)
|
||||||
|
|
||||||
|
if not slow_queries:
|
||||||
|
return {
|
||||||
|
"total": 0,
|
||||||
|
"threshold": f"{self._metrics.slow_query_threshold:.3f}s",
|
||||||
|
"top_operations": [],
|
||||||
|
"recent_queries": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 按操作分组统计
|
||||||
|
operation_stats = {}
|
||||||
|
for record in slow_queries:
|
||||||
|
if record.operation_name not in operation_stats:
|
||||||
|
operation_stats[record.operation_name] = {
|
||||||
|
"count": 0,
|
||||||
|
"total_time": 0.0,
|
||||||
|
"max_time": 0.0,
|
||||||
|
"min_time": float("inf"),
|
||||||
|
}
|
||||||
|
stats = operation_stats[record.operation_name]
|
||||||
|
stats["count"] += 1
|
||||||
|
stats["total_time"] += record.execution_time
|
||||||
|
stats["max_time"] = max(stats["max_time"], record.execution_time)
|
||||||
|
stats["min_time"] = min(stats["min_time"], record.execution_time)
|
||||||
|
|
||||||
|
# 按慢查询数排序
|
||||||
|
top_operations = sorted(
|
||||||
|
operation_stats.items(),
|
||||||
|
key=lambda x: x[1]["count"],
|
||||||
|
reverse=True,
|
||||||
|
)[:10]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(slow_queries),
|
||||||
|
"threshold": f"{self._metrics.slow_query_threshold:.3f}s",
|
||||||
|
"top_operations": [
|
||||||
|
{
|
||||||
|
"operation": op_name,
|
||||||
|
"count": stats["count"],
|
||||||
|
"avg_time": f"{stats['total_time'] / stats['count']:.3f}s",
|
||||||
|
"max_time": f"{stats['max_time']:.3f}s",
|
||||||
|
"min_time": f"{stats['min_time']:.3f}s",
|
||||||
|
}
|
||||||
|
for op_name, stats in top_operations
|
||||||
|
],
|
||||||
|
"recent_queries": [
|
||||||
|
{
|
||||||
|
"operation": record.operation_name,
|
||||||
|
"time": f"{record.execution_time:.3f}s",
|
||||||
|
"timestamp": time.strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
time.localtime(record.timestamp),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for record in slow_queries[-20:]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
def get_summary(self) -> dict[str, Any]:
|
def get_summary(self) -> dict[str, Any]:
|
||||||
"""获取统计摘要"""
|
"""获取统计摘要"""
|
||||||
metrics = self._metrics
|
metrics = self._metrics
|
||||||
@@ -164,6 +324,7 @@ class DatabaseMonitor:
|
|||||||
"min_time": f"{op_metrics.min_time:.3f}s",
|
"min_time": f"{op_metrics.min_time:.3f}s",
|
||||||
"max_time": f"{op_metrics.max_time:.3f}s",
|
"max_time": f"{op_metrics.max_time:.3f}s",
|
||||||
"error_count": op_metrics.error_count,
|
"error_count": op_metrics.error_count,
|
||||||
|
"slow_query_count": op_metrics.slow_query_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -188,6 +349,8 @@ class DatabaseMonitor:
|
|||||||
},
|
},
|
||||||
"overall": {
|
"overall": {
|
||||||
"error_rate": f"{metrics.error_rate:.2%}",
|
"error_rate": f"{metrics.error_rate:.2%}",
|
||||||
|
"slow_query_count": metrics.slow_query_count,
|
||||||
|
"slow_query_threshold": f"{metrics.slow_query_threshold:.3f}s",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +372,8 @@ class DatabaseMonitor:
|
|||||||
f"平均={stats['avg_time']}, "
|
f"平均={stats['avg_time']}, "
|
||||||
f"最小={stats['min_time']}, "
|
f"最小={stats['min_time']}, "
|
||||||
f"最大={stats['max_time']}, "
|
f"最大={stats['max_time']}, "
|
||||||
f"错误={stats['error_count']}"
|
f"错误={stats['error_count']}, "
|
||||||
|
f"慢查询={stats['slow_query_count']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 连接池统计
|
# 连接池统计
|
||||||
@@ -246,6 +410,24 @@ class DatabaseMonitor:
|
|||||||
logger.info("\n整体:")
|
logger.info("\n整体:")
|
||||||
overall = summary["overall"]
|
overall = summary["overall"]
|
||||||
logger.info(f" 错误率={overall['error_rate']}")
|
logger.info(f" 错误率={overall['error_rate']}")
|
||||||
|
logger.info(f" 慢查询总数={overall['slow_query_count']}")
|
||||||
|
logger.info(f" 慢查询阈值={overall['slow_query_threshold']}")
|
||||||
|
|
||||||
|
# 慢查询报告
|
||||||
|
if overall["slow_query_count"] > 0:
|
||||||
|
logger.info("\n🐢 慢查询报告:")
|
||||||
|
slow_report = self.get_slow_query_report()
|
||||||
|
|
||||||
|
if slow_report["top_operations"]:
|
||||||
|
logger.info(" 按操作排名(Top 10):")
|
||||||
|
for idx, op in enumerate(slow_report["top_operations"], 1):
|
||||||
|
logger.info(
|
||||||
|
f" {idx}. {op['operation']}: "
|
||||||
|
f"次数={op['count']}, "
|
||||||
|
f"平均={op['avg_time']}, "
|
||||||
|
f"最大={op['max_time']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
@@ -273,6 +455,46 @@ def record_operation(operation_name: str, execution_time: float, success: bool =
|
|||||||
get_monitor().record_operation(operation_name, execution_time, success)
|
get_monitor().record_operation(operation_name, execution_time, success)
|
||||||
|
|
||||||
|
|
||||||
|
def record_slow_query(
|
||||||
|
operation_name: str,
|
||||||
|
execution_time: float,
|
||||||
|
sql: str | None = None,
|
||||||
|
args: tuple | None = None,
|
||||||
|
):
|
||||||
|
"""记录慢查询"""
|
||||||
|
get_monitor().record_slow_query(operation_name, execution_time, sql, args)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slow_queries(limit: int = 0) -> list[SlowQueryRecord]:
|
||||||
|
"""获取慢查询记录"""
|
||||||
|
return get_monitor().get_slow_queries(limit)
|
||||||
|
|
||||||
|
|
||||||
|
def get_slow_query_report() -> dict[str, Any]:
|
||||||
|
"""获取慢查询报告"""
|
||||||
|
return get_monitor().get_slow_query_report()
|
||||||
|
|
||||||
|
|
||||||
|
def set_slow_query_config(threshold: float, buffer_size: int):
|
||||||
|
"""设置慢查询配置"""
|
||||||
|
get_monitor().set_slow_query_config(threshold, buffer_size)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_slow_query_monitoring():
|
||||||
|
"""启用慢查询监控"""
|
||||||
|
get_monitor().enable()
|
||||||
|
|
||||||
|
|
||||||
|
def disable_slow_query_monitoring():
|
||||||
|
"""禁用慢查询监控"""
|
||||||
|
get_monitor().disable()
|
||||||
|
|
||||||
|
|
||||||
|
def is_slow_query_monitoring_enabled() -> bool:
|
||||||
|
"""检查慢查询监控是否启用"""
|
||||||
|
return get_monitor().is_enabled()
|
||||||
|
|
||||||
|
|
||||||
def record_cache_hit():
|
def record_cache_hit():
|
||||||
"""记录缓存命中"""
|
"""记录缓存命中"""
|
||||||
get_monitor().record_cache_hit()
|
get_monitor().record_cache_hit()
|
||||||
|
|||||||
437
src/common/database/utils/slow_query_analyzer.py
Normal file
437
src/common/database/utils/slow_query_analyzer.py
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
"""慢查询分析工具
|
||||||
|
|
||||||
|
提供慢查询的详细分析和报告生成功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from src.common.database.utils.monitoring import get_monitor
|
||||||
|
from src.common.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("database.slow_query_analyzer")
|
||||||
|
|
||||||
|
|
||||||
|
class SlowQueryAnalyzer:
|
||||||
|
"""慢查询分析器"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_html_report(output_file: str | None = None) -> str:
|
||||||
|
"""生成HTML格式的慢查询报告
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_file: 输出文件路径,None 表示只返回HTML字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTML字符串
|
||||||
|
"""
|
||||||
|
monitor = get_monitor()
|
||||||
|
report = monitor.get_slow_query_report()
|
||||||
|
metrics = monitor.get_metrics()
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>数据库慢查询报告</title>
|
||||||
|
<style>
|
||||||
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
.container {{
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}}
|
||||||
|
header {{
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
header h1 {{
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}}
|
||||||
|
header p {{
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}}
|
||||||
|
.stats {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}}
|
||||||
|
.stat-card {{
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}}
|
||||||
|
.stat-card .value {{
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 10px 0;
|
||||||
|
}}
|
||||||
|
.stat-card .label {{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}}
|
||||||
|
.section {{
|
||||||
|
padding: 30px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}}
|
||||||
|
.section:last-child {{
|
||||||
|
border-bottom: none;
|
||||||
|
}}
|
||||||
|
.section h2 {{
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #667eea;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}}
|
||||||
|
table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}}
|
||||||
|
table thead {{
|
||||||
|
background: #f9f9f9;
|
||||||
|
}}
|
||||||
|
table th {{
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
}}
|
||||||
|
table td {{
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}}
|
||||||
|
table tbody tr:hover {{
|
||||||
|
background: #f9f9f9;
|
||||||
|
}}
|
||||||
|
.badge {{
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
.badge-warning {{
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}}
|
||||||
|
.badge-danger {{
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}}
|
||||||
|
.badge-success {{
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}}
|
||||||
|
.progress-bar {{
|
||||||
|
height: 4px;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 4px;
|
||||||
|
}}
|
||||||
|
.progress-bar-fill {{
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
}}
|
||||||
|
.empty-state {{
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
}}
|
||||||
|
.empty-state-icon {{
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🐢 数据库慢查询报告</h1>
|
||||||
|
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">总慢查询数</div>
|
||||||
|
<div class="value">{report['total']}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">慢查询阈值</div>
|
||||||
|
<div class="value">{report['threshold']}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">总操作数</div>
|
||||||
|
<div class="value">{sum(m.count for m in metrics.operations.values())}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">慢查询比例</div>
|
||||||
|
<div class="value">
|
||||||
|
{f"{(report['total'] / sum(m.count for m in metrics.operations.values()) * 100):.1f}%" if sum(m.count for m in metrics.operations.values()) > 0 else "0%"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>📊 按操作排名 (Top 10)</h2>
|
||||||
|
{_render_operations_table(report) if report['top_operations'] else '<div class="empty-state"><div class="empty-state-icon">📭</div><p>暂无数据</p></div>'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>⏱️ 最近的慢查询 (Top 20)</h2>
|
||||||
|
{_render_recent_queries_table(report) if report['recent_queries'] else '<div class="empty-state"><div class="empty-state-icon">📭</div><p>暂无数据</p></div>'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>💡 优化建议</h2>
|
||||||
|
{_render_suggestions(report, metrics)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if output_file:
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
logger.info(f"慢查询报告已生成: {output_file}")
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_text_report() -> str:
|
||||||
|
"""生成文本格式的慢查询报告
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
文本字符串
|
||||||
|
"""
|
||||||
|
monitor = get_monitor()
|
||||||
|
report = monitor.get_slow_query_report()
|
||||||
|
metrics = monitor.get_metrics()
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append("=" * 80)
|
||||||
|
lines.append("🐢 数据库慢查询报告".center(80))
|
||||||
|
lines.append("=" * 80)
|
||||||
|
lines.append(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 总体统计
|
||||||
|
total_ops = sum(m.count for m in metrics.operations.values())
|
||||||
|
lines.append("📊 总体统计")
|
||||||
|
lines.append("-" * 80)
|
||||||
|
lines.append(f" 总慢查询数: {report['total']}")
|
||||||
|
lines.append(f" 慢查询阈值: {report['threshold']}")
|
||||||
|
lines.append(f" 总操作数: {total_ops}")
|
||||||
|
if total_ops > 0:
|
||||||
|
lines.append(f" 慢查询比例: {report['total'] / total_ops * 100:.1f}%")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 按操作排名
|
||||||
|
if report["top_operations"]:
|
||||||
|
lines.append("📈 按操作排名 (Top 10)")
|
||||||
|
lines.append("-" * 80)
|
||||||
|
lines.append(f"{'#':<3} {'操作名':<30} {'次数':<8} {'平均时间':<12} {'最大时间':<12}")
|
||||||
|
lines.append("-" * 80)
|
||||||
|
for idx, op in enumerate(report["top_operations"], 1):
|
||||||
|
lines.append(
|
||||||
|
f"{idx:<3} {op['operation']:<30} {op['count']:<8} "
|
||||||
|
f"{op['avg_time']:<12} {op['max_time']:<12}"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 最近的慢查询
|
||||||
|
if report["recent_queries"]:
|
||||||
|
lines.append("⏱️ 最近的慢查询 (最近 20 条)")
|
||||||
|
lines.append("-" * 80)
|
||||||
|
lines.append(f"{'时间':<20} {'操作':<30} {'执行时间':<15}")
|
||||||
|
lines.append("-" * 80)
|
||||||
|
for record in report["recent_queries"]:
|
||||||
|
lines.append(
|
||||||
|
f"{record['timestamp']:<20} {record['operation']:<30} {record['time']:<15}"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 优化建议
|
||||||
|
lines.append("💡 优化建议")
|
||||||
|
lines.append("-" * 80)
|
||||||
|
suggestions = _get_suggestions(report, metrics)
|
||||||
|
for suggestion in suggestions:
|
||||||
|
lines.append(f" • {suggestion}")
|
||||||
|
|
||||||
|
lines.append("=" * 80)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_slow_queries_by_operation(operation_name: str) -> list[Any]:
|
||||||
|
"""获取特定操作的所有慢查询
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation_name: 操作名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
慢查询记录列表
|
||||||
|
"""
|
||||||
|
monitor = get_monitor()
|
||||||
|
slow_queries = monitor.get_slow_queries()
|
||||||
|
|
||||||
|
return [q for q in slow_queries if q.operation_name == operation_name]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_slowest_queries(limit: int = 20) -> list[Any]:
|
||||||
|
"""获取最慢的查询
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: 返回数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
按执行时间排序的慢查询记录列表
|
||||||
|
"""
|
||||||
|
monitor = get_monitor()
|
||||||
|
slow_queries = monitor.get_slow_queries()
|
||||||
|
|
||||||
|
return sorted(slow_queries, key=lambda q: q.execution_time, reverse=True)[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def _render_operations_table(report: dict) -> str:
|
||||||
|
"""渲染操作排名表格"""
|
||||||
|
if not report["top_operations"]:
|
||||||
|
return '<div class="empty-state"><p>暂无数据</p></div>'
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for idx, op in enumerate(report["top_operations"], 1):
|
||||||
|
rows.append(f"""
|
||||||
|
<tr>
|
||||||
|
<td>#{idx}</td>
|
||||||
|
<td><strong>{op['operation']}</strong></td>
|
||||||
|
<td><span class="badge badge-warning">{op['count']}</span></td>
|
||||||
|
<td>{op['avg_time']}</td>
|
||||||
|
<td>{op['max_time']}</td>
|
||||||
|
</tr>
|
||||||
|
""")
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 5%">#</th>
|
||||||
|
<th style="width: 40%">操作名</th>
|
||||||
|
<th style="width: 15%">慢查询次数</th>
|
||||||
|
<th style="width: 20%">平均执行时间</th>
|
||||||
|
<th style="width: 20%">最大执行时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{''.join(rows)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _render_recent_queries_table(report: dict) -> str:
|
||||||
|
"""渲染最近查询表格"""
|
||||||
|
if not report["recent_queries"]:
|
||||||
|
return '<div class="empty-state"><p>暂无数据</p></div>'
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for record in report["recent_queries"]:
|
||||||
|
rows.append(f"""
|
||||||
|
<tr>
|
||||||
|
<td>{record['timestamp']}</td>
|
||||||
|
<td>{record['operation']}</td>
|
||||||
|
<td><span class="badge badge-danger">{record['time']}</span></td>
|
||||||
|
</tr>
|
||||||
|
""")
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 25%">时间</th>
|
||||||
|
<th style="width: 50%">操作名</th>
|
||||||
|
<th style="width: 25%">执行时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{''.join(rows)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_suggestions(report: dict, metrics: Any) -> list[str]:
|
||||||
|
"""生成优化建议"""
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
if report["total"] == 0:
|
||||||
|
suggestions.append("✅ 没有检测到慢查询,性能良好!")
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
# 计算比例
|
||||||
|
total_ops = sum(m.count for m in metrics.operations.values())
|
||||||
|
slow_ratio = report["total"] / total_ops if total_ops > 0 else 0
|
||||||
|
|
||||||
|
if slow_ratio > 0.1:
|
||||||
|
suggestions.append(f"⚠️ 慢查询比例较高 ({slow_ratio * 100:.1f}%),建议检查数据库索引和查询优化")
|
||||||
|
|
||||||
|
if report["top_operations"]:
|
||||||
|
top_op = report["top_operations"][0]
|
||||||
|
suggestions.append(f"🔍 '{top_op['operation']}' 是最常见的慢查询,建议优先优化这个操作")
|
||||||
|
|
||||||
|
if top_op["count"] > total_ops * 0.3:
|
||||||
|
suggestions.append("🚀 优化最频繁的慢查询可能会显著提升性能")
|
||||||
|
|
||||||
|
# 分析操作执行时间
|
||||||
|
for op_name, op_metrics in metrics.operations.items():
|
||||||
|
if op_metrics.max_time > 5:
|
||||||
|
suggestions.append(
|
||||||
|
f"⏱️ '{op_name}' 的最大执行时间超过 5 秒 ({op_metrics.max_time:.1f}s),"
|
||||||
|
"这可能表明有异常的查询操作"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(report["top_operations"]) > 1:
|
||||||
|
top_2_count = sum(op["count"] for op in report["top_operations"][:2])
|
||||||
|
if top_2_count / report["total"] > 0.7:
|
||||||
|
suggestions.append("🎯 80% 的慢查询集中在少数操作上,建议针对这些操作进行优化")
|
||||||
|
|
||||||
|
if not suggestions:
|
||||||
|
suggestions.append("💡 考虑调整 slow_query_threshold 以获得更细致的分析")
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
def _render_suggestions(report: dict, metrics: Any) -> str:
|
||||||
|
"""渲染优化建议"""
|
||||||
|
suggestions = _get_suggestions(report, metrics)
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
{''.join(f'<li style="padding: 8px 0; line-height: 1.6;">{s}</li>' for s in suggestions)}
|
||||||
|
</ul>
|
||||||
|
"""
|
||||||
@@ -75,6 +75,13 @@ class DatabaseConfig(ValidatedConfigBase):
|
|||||||
redis_socket_timeout: float = Field(default=5.0, ge=1.0, le=30.0, description="Redis socket超时时间(秒)")
|
redis_socket_timeout: float = Field(default=5.0, ge=1.0, le=30.0, description="Redis socket超时时间(秒)")
|
||||||
redis_ssl: bool = Field(default=False, description="是否启用Redis SSL连接")
|
redis_ssl: bool = Field(default=False, description="是否启用Redis SSL连接")
|
||||||
|
|
||||||
|
# 慢查询监控配置
|
||||||
|
enable_slow_query_logging: bool = Field(default=False, description="是否启用慢查询日志(默认关闭,设置为 true 启用)")
|
||||||
|
slow_query_threshold: float = Field(default=0.5, ge=0.1, le=10.0, description="慢查询阈值(秒)")
|
||||||
|
query_timeout: int = Field(default=30, ge=5, le=300, description="查询超时时间(秒)")
|
||||||
|
collect_slow_queries: bool = Field(default=True, description="是否收集慢查询统计(用于生成报告)")
|
||||||
|
slow_query_buffer_size: int = Field(default=100, ge=10, le=1000, description="慢查询缓冲大小(最近N条)")
|
||||||
|
|
||||||
|
|
||||||
class BotConfig(ValidatedConfigBase):
|
class BotConfig(ValidatedConfigBase):
|
||||||
"""QQ机器人配置类"""
|
"""QQ机器人配置类"""
|
||||||
|
|||||||
@@ -979,7 +979,7 @@ class LLMRequest:
|
|||||||
def _resolve_system_prompt(self, model_set: TaskConfig) -> str | None:
|
def _resolve_system_prompt(self, model_set: TaskConfig) -> str | None:
|
||||||
"""确定是否需要附加统一的system prompt."""
|
"""确定是否需要附加统一的system prompt."""
|
||||||
try:
|
try:
|
||||||
if model_config and model_set is model_config.model_task_config.replyer:
|
if model_config and (model_set is model_config.model_task_config.replyer or model_set is model_config.model_task_config.replyer_private):
|
||||||
return SYSTEM_PROMPT
|
return SYSTEM_PROMPT
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.debug("模型配置缺少replyer定义,无法注入系统提示词")
|
logger.debug("模型配置缺少replyer定义,无法注入系统提示词")
|
||||||
|
|||||||
48
src/main.py
48
src/main.py
@@ -263,6 +263,35 @@ class MainSystem:
|
|||||||
logger.info("正在停止数据库服务...")
|
logger.info("正在停止数据库服务...")
|
||||||
await asyncio.wait_for(stop_database(), timeout=15.0)
|
await asyncio.wait_for(stop_database(), timeout=15.0)
|
||||||
logger.info("🛑 数据库服务已停止")
|
logger.info("🛑 数据库服务已停止")
|
||||||
|
|
||||||
|
# 输出数据库性能统计和慢查询报告
|
||||||
|
try:
|
||||||
|
from src.common.database.utils.monitoring import print_stats, get_slow_query_report
|
||||||
|
from src.common.database.utils.slow_query_analyzer import SlowQueryAnalyzer
|
||||||
|
|
||||||
|
logger.info("") # 空行
|
||||||
|
print_stats() # 打印数据库性能统计
|
||||||
|
|
||||||
|
# 如果有慢查询,尝试生成报告
|
||||||
|
slow_report = get_slow_query_report()
|
||||||
|
if slow_report.get("total", 0) > 0:
|
||||||
|
logger.info("") # 空行
|
||||||
|
logger.info("正在生成慢查询详细报告...")
|
||||||
|
try:
|
||||||
|
# 生成文本报告
|
||||||
|
text_report = SlowQueryAnalyzer.generate_text_report()
|
||||||
|
logger.info("") # 空行
|
||||||
|
logger.info(text_report)
|
||||||
|
|
||||||
|
# 尝试生成HTML报告
|
||||||
|
html_file = "logs/slow_query_report.html"
|
||||||
|
SlowQueryAnalyzer.generate_html_report(html_file)
|
||||||
|
logger.info(f"💡 HTML慢查询报告已生成: {html_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"生成慢查询报告失败: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法输出数据库统计信息: {e}")
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.error("停止数据库服务超时")
|
logger.error("停止数据库服务超时")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -291,6 +320,25 @@ class MainSystem:
|
|||||||
|
|
||||||
logger.debug(f"正在唤醒{global_config.bot.nickname}......")
|
logger.debug(f"正在唤醒{global_config.bot.nickname}......")
|
||||||
|
|
||||||
|
# 配置数据库慢查询监控
|
||||||
|
try:
|
||||||
|
from src.common.database.utils.monitoring import set_slow_query_config
|
||||||
|
|
||||||
|
if global_config.database:
|
||||||
|
db_config = global_config.database
|
||||||
|
if db_config.enable_slow_query_logging:
|
||||||
|
set_slow_query_config(
|
||||||
|
threshold=db_config.slow_query_threshold,
|
||||||
|
buffer_size=db_config.slow_query_buffer_size,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"✅ 数据库慢查询监控已启用 "
|
||||||
|
f"(阈值: {db_config.slow_query_threshold}s, "
|
||||||
|
f"缓冲: {db_config.slow_query_buffer_size})"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"配置数据库监控时出错: {e}")
|
||||||
|
|
||||||
# 初始化 CoreSinkManager(包含 MessageRuntime)
|
# 初始化 CoreSinkManager(包含 MessageRuntime)
|
||||||
logger.debug("正在初始化 CoreSinkManager...")
|
logger.debug("正在初始化 CoreSinkManager...")
|
||||||
self.core_sink_manager = await initialize_core_sink_manager()
|
self.core_sink_manager = await initialize_core_sink_manager()
|
||||||
|
|||||||
@@ -645,97 +645,17 @@ class ShortTermMemoryManager:
|
|||||||
|
|
||||||
def get_memories_for_transfer(self) -> list[ShortTermMemory]:
|
def get_memories_for_transfer(self) -> list[ShortTermMemory]:
|
||||||
"""
|
"""
|
||||||
获取需要转移到长期记忆的记忆
|
获取需要转移到长期记忆的记忆(简化版:满额整批转移)
|
||||||
|
|
||||||
根据 overflow_strategy 选择不同的转移策略:
|
策略:
|
||||||
- "transfer_all": 一次性转移所有记忆(满容量时),然后删除低重要性记忆
|
- 当短期记忆数量达到上限(>= max_memories)时,返回当前全部短期记忆;
|
||||||
- "selective_cleanup": 仅转移高重要性记忆,低重要性记忆直接删除
|
- 没满则返回空列表,不触发转移。
|
||||||
|
|
||||||
返回:
|
|
||||||
需要转移的记忆列表
|
|
||||||
"""
|
"""
|
||||||
if self.overflow_strategy == "transfer_all":
|
if self.max_memories <= 0:
|
||||||
return self._get_transfer_all_strategy()
|
|
||||||
else: # "selective_cleanup" 或其他值默认使用选择性清理
|
|
||||||
return self._get_selective_cleanup_strategy()
|
|
||||||
|
|
||||||
def _get_transfer_all_strategy(self) -> list[ShortTermMemory]:
|
|
||||||
"""
|
|
||||||
"一次性转移所有"策略:当短期记忆满了以后,将所有记忆转移到长期记忆
|
|
||||||
|
|
||||||
返回:
|
|
||||||
需要转移的记忆列表(满容量时返回所有记忆)
|
|
||||||
"""
|
|
||||||
# 如果短期记忆已满或接近满,一次性转移所有记忆
|
|
||||||
if len(self.memories) >= self.max_memories:
|
|
||||||
logger.info(
|
|
||||||
f"转移策略(transfer_all): 短期记忆已满 ({len(self.memories)}/{self.max_memories}),"
|
|
||||||
f"将转移所有 {len(self.memories)} 条记忆到长期记忆"
|
|
||||||
)
|
|
||||||
return self.memories.copy()
|
|
||||||
|
|
||||||
# 如果还没满,检查是否有高重要性记忆需要转移
|
|
||||||
high_importance_memories = [
|
|
||||||
mem for mem in self.memories
|
|
||||||
if mem.importance >= self.transfer_importance_threshold
|
|
||||||
]
|
|
||||||
|
|
||||||
if high_importance_memories:
|
|
||||||
logger.debug(
|
|
||||||
f"转移策略(transfer_all): 发现 {len(high_importance_memories)} 条高重要性记忆待转移"
|
|
||||||
)
|
|
||||||
return high_importance_memories
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"转移策略(transfer_all): 无需转移 (当前容量 {len(self.memories)}/{self.max_memories})"
|
|
||||||
)
|
|
||||||
return []
|
return []
|
||||||
|
if len(self.memories) >= self.max_memories:
|
||||||
def _get_selective_cleanup_strategy(self) -> list[ShortTermMemory]:
|
logger.debug(f"转移候选: 短期记忆已满,准备整批转移 {len(self.memories)} 条")
|
||||||
"""
|
return list(self.memories)
|
||||||
"选择性清理"策略(原有策略):优先转移重要记忆,低重要性记忆考虑直接删除
|
|
||||||
|
|
||||||
返回:
|
|
||||||
需要转移的记忆列表
|
|
||||||
"""
|
|
||||||
# 单次遍历:同时分类高重要性和低重要性记忆
|
|
||||||
high_importance_memories = []
|
|
||||||
low_importance_memories = []
|
|
||||||
|
|
||||||
for mem in self.memories:
|
|
||||||
if mem.importance >= self.transfer_importance_threshold:
|
|
||||||
high_importance_memories.append(mem)
|
|
||||||
else:
|
|
||||||
low_importance_memories.append(mem)
|
|
||||||
|
|
||||||
# 策略1:优先返回高重要性记忆进行转移
|
|
||||||
if high_importance_memories:
|
|
||||||
logger.debug(
|
|
||||||
f"转移策略(selective): 发现 {len(high_importance_memories)} 条高重要性记忆待转移"
|
|
||||||
)
|
|
||||||
return high_importance_memories
|
|
||||||
|
|
||||||
# 策略2:如果没有高重要性记忆但总体超过容量上限,
|
|
||||||
# 返回一部分低重要性记忆用于转移(而非删除)
|
|
||||||
if len(self.memories) > self.max_memories:
|
|
||||||
# 计算需要转移的数量(目标:降到上限)
|
|
||||||
num_to_transfer = len(self.memories) - self.max_memories
|
|
||||||
|
|
||||||
# 按创建时间排序低重要性记忆,优先转移最早的(可能包含过时信息)
|
|
||||||
low_importance_memories.sort(key=lambda x: x.created_at)
|
|
||||||
to_transfer = low_importance_memories[:num_to_transfer]
|
|
||||||
|
|
||||||
if to_transfer:
|
|
||||||
logger.debug(
|
|
||||||
f"转移策略(selective): 发现 {len(to_transfer)} 条低重要性记忆待转移 "
|
|
||||||
f"(当前容量 {len(self.memories)}/{self.max_memories})"
|
|
||||||
)
|
|
||||||
return to_transfer
|
|
||||||
|
|
||||||
# 策略3:容量充足,无需转移
|
|
||||||
logger.debug(
|
|
||||||
f"转移策略(selective): 无需转移 (当前容量 {len(self.memories)}/{self.max_memories})"
|
|
||||||
)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def force_cleanup_overflow(self, keep_ratio: float | None = None) -> int:
|
def force_cleanup_overflow(self, keep_ratio: float | None = None) -> int:
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
# 短期记忆压力泄压补丁
|
# 短期记忆压力泄压补丁(已弃用)
|
||||||
|
|
||||||
## 📋 概述
|
## 📋 概述
|
||||||
|
|
||||||
在高频消息场景下,短期记忆层(`ShortTermMemoryManager`)可能在自动转移机制触发前快速堆积大量记忆,当达到容量上限(`max_memories`)时可能阻塞后续写入。本功能提供一个**可选的泄压开关**,在容量溢出时自动删除低优先级记忆,防止系统阻塞。
|
该文档描述的“泄压删除”与“复杂自动转移”机制已不再作为默认策略使用:现在短期记忆采用最简单策略——**只有当短期记忆满额时,才整批转移全部短期记忆到长期记忆;没满就不处理**。因此,本补丁说明仅供历史参考。
|
||||||
|
|
||||||
**关键特性**:
|
**当前行为(简化版)**:
|
||||||
- ✅ 默认开启(在高频场景中保护系统),可关闭保持向后兼容
|
- ✅ 短期记忆未满:不触发转移
|
||||||
- ✅ 基于重要性和时间的智能删除策略
|
- ✅ 短期记忆满额:一次性整批转移全部短期记忆到长期记忆
|
||||||
- ✅ 异步持久化,不阻塞主流程
|
|
||||||
- ✅ 可通过配置文件或代码灵活控制
|
|
||||||
- ✅ 支持自定义保留比例
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -114,8 +113,6 @@ class UnifiedMemoryManager:
|
|||||||
self._initialized = False
|
self._initialized = False
|
||||||
self._auto_transfer_task: asyncio.Task | None = None
|
self._auto_transfer_task: asyncio.Task | None = None
|
||||||
self._auto_transfer_interval = max(10.0, float(long_term_auto_transfer_interval))
|
self._auto_transfer_interval = max(10.0, float(long_term_auto_transfer_interval))
|
||||||
# 优化:降低最大延迟时间,加快转移节奏 (原为 300.0)
|
|
||||||
self._max_transfer_delay = min(max(30.0, self._auto_transfer_interval), 60.0)
|
|
||||||
self._transfer_wakeup_event: asyncio.Event | None = None
|
self._transfer_wakeup_event: asyncio.Event | None = None
|
||||||
|
|
||||||
logger.info("统一记忆管理器已创建")
|
logger.info("统一记忆管理器已创建")
|
||||||
@@ -330,41 +327,41 @@ class UnifiedMemoryManager:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prompt = f"""你是一个记忆检索评估专家。你的任务是判断当前检索到的“感知记忆”(即时对话)和“短期记忆”(结构化信息)是否足以支撑一次有深度、有上下文的回复。
|
prompt = f"""你是“记忆判官”(记忆检索评估专家)。你的任务是:基于给定的历史消息、当前消息,以及我们已经检索到的“感知记忆”和“短期记忆”,判断是否还需要检索“长期记忆”(LTM)来支撑一次准确、完整、上下文一致的回复。
|
||||||
|
|
||||||
**核心原则:**
|
**总体偏好(重要):**
|
||||||
- **适当检索长期记忆有助于提升回复质量。** 当对话涉及到特定话题、人物、事件或需要回忆过去的经历时,应该检索长期记忆。
|
- 我们宁可多花一点资源去检索长期记忆,也不要在本该检索时漏检索。
|
||||||
- **判断标准:** 只有当现有记忆无法理解用户意图,或无法形成基本、连贯的回复时,才认为“不充足”。检索长期记忆耗时。除非有需要,否则不要检索。
|
- 因此:只要存在明显不确定、信息缺口、或需要更精确细节的情况,就倾向于判定“现有记忆不充足”(`is_sufficient: false`)。
|
||||||
- **如果用户在讨论某个具体话题,即使现有记忆有一些相关信息,也可以检索长期记忆来补充更多背景。**
|
|
||||||
|
|
||||||
**用户查询:**
|
**输入:**
|
||||||
|
**当前用户消息:**
|
||||||
{query}
|
{query}
|
||||||
|
|
||||||
{chat_history_block}**检索到的感知记忆(即时对话,格式:【时间 (聊天流)】消息列表):**
|
{chat_history_block}**已检索到的感知记忆(即时对话,格式:【时间 (聊天流)】消息列表):**
|
||||||
{perceptual_desc or '(无)'}
|
{perceptual_desc or '(无)'}
|
||||||
|
|
||||||
**检索到的短期记忆(结构化信息,自然语言描述):**
|
**已检索到的短期记忆(结构化信息,自然语言描述):**
|
||||||
{short_term_desc or '(无)'}
|
{short_term_desc or '(无)'}
|
||||||
|
|
||||||
**评估指南:**
|
**什么时候必须检索长期记忆(满足任一条 → `is_sufficient: false`):**
|
||||||
1. **分析用户意图**:用户在聊什么?是简单闲聊还是有具体话题?
|
1. **用户明确要求回忆/找回过去信息**:例如“你还记得…?”“上次我们说到…?”“帮我回忆一下…/之前…/那天…/某次…”
|
||||||
2. **检查现有记忆**:
|
2. **你对答案没有把握或存在不确定性**:例如无法确定人物/事件/时间/地点/偏好/承诺/任务细节,或只能给出模糊猜测。
|
||||||
- 对于闲聊、打招呼、无特定主题的互动 → 现有记忆充足 (`is_sufficient: true`)。
|
3. **现有记忆不足以给出精确回答**:要给出具体结论、细节、步骤、承诺、时间线、决定依据,但感知/短期记忆缺少关键事实。
|
||||||
- 如果涉及具体话题(人物、事件、知识),但现有记忆能提供基本信息 → 现有记忆充足 (`is_sufficient: true`)。
|
4. **对话依赖用户个体历史**:涉及用户的个人信息、偏好、长期目标、过往经历、已约定事项、持续进行的项目/任务,需要更早的上下文才能回答。
|
||||||
- 仅当用户明确问及过去的特定事件,或当前信息完全无法理解用户意图时 → 现有记忆不充足 (`is_sufficient: false`)。
|
5. **指代不清或背景缺失**:出现“那个/那件事/他/她/它/之前说的/你知道的”等省略指代,现有记忆不足以唯一指向。
|
||||||
|
6. **记忆冲突或碎片化**:感知/短期记忆之间存在矛盾、时间线断裂、或信息片段无法拼成完整图景。
|
||||||
|
|
||||||
**输出格式(JSON):**
|
**什么时候可以不检索(同时满足全部条件 → `is_sufficient: true`):**
|
||||||
```json
|
- 用户只是闲聊/打招呼/情绪表达/泛化问题(不依赖用户个人历史),且现有记忆已足以给出可靠且一致的回复;
|
||||||
{{
|
- 你能在不猜测的情况下回答,且不需要更早的细节来保证准确性。
|
||||||
"is_sufficient": true/false,
|
|
||||||
"confidence": 0.85,
|
|
||||||
"reasoning": "在这里解释你的判断理由。例如:‘用户只是在打招呼,现有记忆已足够,无需检索长期记忆。’或‘用户问到了一个具体的历史事件,现有记忆完全没有相关信息,必须检索长期记忆。’",
|
|
||||||
"missing_aspects": ["缺失的信息1", "缺失的信息2"],
|
|
||||||
"additional_queries": ["补充query1", "补充query2"]
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
|
|
||||||
请输出JSON:"""
|
**输出要求(JSON):**
|
||||||
|
- `is_sufficient`: `true` 表示“无需检索长期记忆”;`false` 表示“需要检索长期记忆”
|
||||||
|
- `confidence`: 0~1,表示你对该判断的把握;若你偏向检索但仍不确定,也应输出较低/中等置信度并保持 `is_sufficient: false`
|
||||||
|
- `missing_aspects`: 列出阻碍精确回答的缺失点(可为空数组)
|
||||||
|
- `additional_queries`: 给出 1~5 条用于检索长期记忆的补充 query(尽量短、可检索、包含关键实体/事件/时间线线索;可为空数组)
|
||||||
|
|
||||||
|
请仅输出 JSON(可以用 ```json 包裹,也可以直接输出纯 JSON):"""
|
||||||
|
|
||||||
# 调用记忆裁判模型
|
# 调用记忆裁判模型
|
||||||
if not model_config.model_task_config:
|
if not model_config.model_task_config:
|
||||||
@@ -576,11 +573,7 @@ class UnifiedMemoryManager:
|
|||||||
logger.debug("自动转移任务已启动并触发首次检查")
|
logger.debug("自动转移任务已启动并触发首次检查")
|
||||||
|
|
||||||
async def _auto_transfer_loop(self) -> None:
|
async def _auto_transfer_loop(self) -> None:
|
||||||
"""自动转移循环(批量缓存模式,优化:更高效的缓存管理)"""
|
"""自动转移循环(简化版:短期记忆满额时整批转移)"""
|
||||||
transfer_cache: list[ShortTermMemory] = []
|
|
||||||
cached_ids: set[str] = set()
|
|
||||||
cache_size_threshold = max(1, self._config["long_term"].get("batch_size", 1))
|
|
||||||
last_transfer_time = time.monotonic()
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -597,67 +590,25 @@ class UnifiedMemoryManager:
|
|||||||
else:
|
else:
|
||||||
await asyncio.sleep(sleep_interval)
|
await asyncio.sleep(sleep_interval)
|
||||||
|
|
||||||
memories_to_transfer = self.short_term_manager.get_memories_for_transfer()
|
# 最简单策略:仅当短期记忆满额时,直接整批转移全部短期记忆;没满则不处理
|
||||||
|
|
||||||
if memories_to_transfer:
|
|
||||||
# 优化:批量构建缓存而不是逐条添加
|
|
||||||
new_memories = []
|
|
||||||
for memory in memories_to_transfer:
|
|
||||||
mem_id = getattr(memory, "id", None)
|
|
||||||
if not (mem_id and mem_id in cached_ids):
|
|
||||||
new_memories.append(memory)
|
|
||||||
if mem_id:
|
|
||||||
cached_ids.add(mem_id)
|
|
||||||
|
|
||||||
if new_memories:
|
|
||||||
transfer_cache.extend(new_memories)
|
|
||||||
logger.debug(
|
|
||||||
f"自动转移缓存: 新增{len(new_memories)}条, 当前缓存{len(transfer_cache)}/{cache_size_threshold}"
|
|
||||||
)
|
|
||||||
|
|
||||||
max_memories = max(1, getattr(self.short_term_manager, "max_memories", 1))
|
max_memories = max(1, getattr(self.short_term_manager, "max_memories", 1))
|
||||||
occupancy_ratio = len(self.short_term_manager.memories) / max_memories
|
if len(self.short_term_manager.memories) < max_memories:
|
||||||
time_since_last_transfer = time.monotonic() - last_transfer_time
|
continue
|
||||||
|
|
||||||
if occupancy_ratio >= 1.0 and not transfer_cache:
|
batch = list(self.short_term_manager.memories)
|
||||||
removed = self.short_term_manager.force_cleanup_overflow()
|
if not batch:
|
||||||
if removed > 0:
|
continue
|
||||||
logger.warning(
|
|
||||||
f"短期记忆占用率 {occupancy_ratio:.0%},已强制删除 {removed} 条低重要性记忆泄压"
|
logger.info(
|
||||||
|
f"短期记忆已满({len(batch)}/{max_memories}),开始整批转移到长期记忆"
|
||||||
)
|
)
|
||||||
|
result = await self.long_term_manager.transfer_from_short_term(batch)
|
||||||
# 优化:优先级判断重构(早期 return)
|
|
||||||
should_transfer = (
|
|
||||||
len(transfer_cache) >= cache_size_threshold
|
|
||||||
or occupancy_ratio >= 0.5
|
|
||||||
or (transfer_cache and time_since_last_transfer >= self._max_transfer_delay)
|
|
||||||
or len(self.short_term_manager.memories) >= self.short_term_manager.max_memories
|
|
||||||
)
|
|
||||||
|
|
||||||
if should_transfer and transfer_cache:
|
|
||||||
logger.debug(
|
|
||||||
f"准备批量转移: {len(transfer_cache)}条短期记忆到长期记忆 (占用率 {occupancy_ratio:.0%})"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 优化:直接传递列表而不再复制
|
|
||||||
result = await self.long_term_manager.transfer_from_short_term(transfer_cache)
|
|
||||||
|
|
||||||
if result.get("transferred_memory_ids"):
|
if result.get("transferred_memory_ids"):
|
||||||
transferred_ids = set(result["transferred_memory_ids"])
|
|
||||||
await self.short_term_manager.clear_transferred_memories(
|
await self.short_term_manager.clear_transferred_memories(
|
||||||
result["transferred_memory_ids"]
|
result["transferred_memory_ids"]
|
||||||
)
|
)
|
||||||
|
logger.debug(f"✅ 整批转移完成: {result}")
|
||||||
# 优化:使用生成器表达式保留未转移的记忆
|
|
||||||
transfer_cache = [
|
|
||||||
m
|
|
||||||
for m in transfer_cache
|
|
||||||
if getattr(m, "id", None) not in transferred_ids
|
|
||||||
]
|
|
||||||
cached_ids.difference_update(transferred_ids)
|
|
||||||
|
|
||||||
last_transfer_time = time.monotonic()
|
|
||||||
logger.debug(f"✅ 批量转移完成: {result}")
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug("自动转移循环被取消")
|
logger.debug("自动转移循环被取消")
|
||||||
@@ -676,10 +627,16 @@ class UnifiedMemoryManager:
|
|||||||
await self.initialize()
|
await self.initialize()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
memories_to_transfer = self.short_term_manager.get_memories_for_transfer()
|
max_memories = max(1, getattr(self.short_term_manager, "max_memories", 1))
|
||||||
|
if len(self.short_term_manager.memories) < max_memories:
|
||||||
|
return {
|
||||||
|
"message": f"短期记忆未满({len(self.short_term_manager.memories)}/{max_memories}),不触发转移",
|
||||||
|
"transferred_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
memories_to_transfer = list(self.short_term_manager.memories)
|
||||||
if not memories_to_transfer:
|
if not memories_to_transfer:
|
||||||
return {"message": "没有需要转移的记忆", "transferred_count": 0}
|
return {"message": "短期记忆为空,无需转移", "transferred_count": 0}
|
||||||
|
|
||||||
# 执行转移
|
# 执行转移
|
||||||
result = await self.long_term_manager.transfer_from_short_term(memories_to_transfer)
|
result = await self.long_term_manager.transfer_from_short_term(memories_to_transfer)
|
||||||
|
|||||||
@@ -312,9 +312,6 @@ short_term_transfer_threshold = 0.6 # 转移到长期记忆的重要性阈值
|
|||||||
short_term_enable_force_cleanup = true # 开启压力泄压(建议高频场景开启)
|
short_term_enable_force_cleanup = true # 开启压力泄压(建议高频场景开启)
|
||||||
short_term_search_top_k = 5 # 搜索时返回的最大数量
|
short_term_search_top_k = 5 # 搜索时返回的最大数量
|
||||||
short_term_decay_factor = 0.98 # 衰减因子
|
short_term_decay_factor = 0.98 # 衰减因子
|
||||||
short_term_overflow_strategy = "transfer_all" # 短期记忆溢出策略
|
|
||||||
# "transfer_all": 一次性转移所有记忆到长期记忆,然后删除低重要性记忆(默认推荐)
|
|
||||||
# "selective_cleanup": 选择性清理,仅转移高重要性记忆,直接删除低重要性记忆
|
|
||||||
|
|
||||||
# 长期记忆层配置
|
# 长期记忆层配置
|
||||||
use_judge = true # 使用评判模型决定是否检索长期记忆
|
use_judge = true # 使用评判模型决定是否检索长期记忆
|
||||||
|
|||||||
Reference in New Issue
Block a user