调整总结业务。支持每天早上9点总结昨天的消息
This commit is contained in:
@@ -342,3 +342,59 @@ class MessageStorageDB(BaseDBOperator):
|
||||
|
||||
# 限制最大返回数量(5000条足以覆盖1-2天的活跃群聊)
|
||||
return messages[:max_results] if messages else []
|
||||
|
||||
def get_messages_by_date_range(self, group_id: str, start_time: datetime, end_time: datetime) -> List[Dict]:
|
||||
"""获取指定时间范围内的消息
|
||||
|
||||
Args:
|
||||
group_id: 群组ID
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
sql = """
|
||||
SELECT timestamp, sender, content, message_type
|
||||
FROM messages
|
||||
WHERE timestamp >= %s
|
||||
AND timestamp <= %s
|
||||
AND message_type in (1, 49)
|
||||
AND group_id = %s
|
||||
AND length(content) > 6
|
||||
AND CHAR_LENGTH(content) < 300
|
||||
AND content NOT LIKE '/%'
|
||||
ORDER BY timestamp ASC
|
||||
"""
|
||||
params = (start_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
end_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
group_id)
|
||||
return self.execute_query(sql, params) or []
|
||||
|
||||
def count_messages_by_date_range(self, group_id: str, start_time: datetime, end_time: datetime) -> int:
|
||||
"""统计指定时间范围内的消息数量
|
||||
|
||||
Args:
|
||||
group_id: 群组ID
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
消息数量
|
||||
"""
|
||||
sql = """
|
||||
SELECT COUNT(*) as count
|
||||
FROM messages
|
||||
WHERE timestamp >= %s
|
||||
AND timestamp <= %s
|
||||
AND message_type in (1, 49)
|
||||
AND group_id = %s
|
||||
AND length(content) > 6
|
||||
AND CHAR_LENGTH(content) < 300
|
||||
AND content NOT LIKE '/%'
|
||||
"""
|
||||
params = (start_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
end_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
group_id)
|
||||
result = self.execute_query(sql, params)
|
||||
return result[0]['count'] if result else 0
|
||||
|
||||
201
plugins/message_summary/README_SCHEDULED.md
Normal file
201
plugins/message_summary/README_SCHEDULED.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 消息总结定时任务使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
已为 `message_summary` 插件添加了定时任务功能,每天早上 9:00 自动总结昨天的群聊信息并发送到群聊中。
|
||||
|
||||
## 实现原理
|
||||
|
||||
参考了 `game_task` 插件的定时任务策略,在 `message_summary` 插件初始化时注册定时任务:
|
||||
|
||||
```python
|
||||
async_job.at_times(["09:00"])(self.daily_summary_job)
|
||||
```
|
||||
|
||||
## 核心修改
|
||||
|
||||
### 1. plugins/message_summary/main.py
|
||||
|
||||
#### 添加导入
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
from utils.decorator.async_job import async_job
|
||||
import asyncio
|
||||
```
|
||||
|
||||
#### 在 `__init__` 中注册定时任务
|
||||
```python
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.message_storage = None
|
||||
self.revoke = None
|
||||
self.feature = self.register_feature()
|
||||
# 注册定时任务:每天早上9点总结昨天的聊天信息
|
||||
async_job.at_times(["09:00"])(self.daily_summary_job)
|
||||
```
|
||||
|
||||
#### 添加定时任务方法
|
||||
```python
|
||||
async def daily_summary_job(self):
|
||||
"""定时任务:每天早上9点总结昨天的聊天信息"""
|
||||
# 计算昨天的时间范围
|
||||
yesterday = datetime.now() - timedelta(days=1)
|
||||
yesterday_start = yesterday.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
yesterday_end = yesterday.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
|
||||
# 获取所有启用了群机器人的群聊
|
||||
all_groups = GroupBotManager.get_group_list()
|
||||
|
||||
# 筛选出开启了总结功能的群聊
|
||||
enabled_groups = []
|
||||
for group_id in all_groups:
|
||||
if GroupBotManager.get_group_permission(group_id, self.feature) == PermissionStatus.ENABLED:
|
||||
enabled_groups.append(group_id)
|
||||
|
||||
# 为每个群生成总结
|
||||
for group_id in enabled_groups:
|
||||
# 获取昨天的聊天记录
|
||||
chat_content = self.message_storage.get_messages_by_date_range(
|
||||
group_id, all_contacts, yesterday_start, yesterday_end
|
||||
)
|
||||
|
||||
# 生成并发送总结
|
||||
summary, image_path = await self._generate_summary(chat_content, group_name)
|
||||
# 发送到群聊...
|
||||
```
|
||||
|
||||
### 2. db/message_storage.py
|
||||
|
||||
添加了按时间范围查询消息的方法:
|
||||
|
||||
```python
|
||||
def get_messages_by_date_range(self, group_id: str, start_time: datetime, end_time: datetime) -> List[Dict]:
|
||||
"""获取指定时间范围内的消息"""
|
||||
sql = """
|
||||
SELECT timestamp, sender, content, message_type
|
||||
FROM messages
|
||||
WHERE timestamp >= %s AND timestamp <= %s
|
||||
AND message_type in (1, 49)
|
||||
AND group_id = %s
|
||||
AND length(content) > 6
|
||||
AND CHAR_LENGTH(content) < 300
|
||||
AND content NOT LIKE '/%'
|
||||
ORDER BY timestamp ASC
|
||||
"""
|
||||
params = (start_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
end_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
group_id)
|
||||
return self.execute_query(sql, params) or []
|
||||
```
|
||||
|
||||
### 3. utils/wechat/message_to_db.py
|
||||
|
||||
添加了对应的包装方法:
|
||||
|
||||
```python
|
||||
def get_messages_by_date_range(self, group_id, all_contacts: dict, start_time: datetime, end_time: datetime):
|
||||
"""获取指定时间范围内的消息"""
|
||||
messages = self.message_db.get_messages_by_date_range(group_id, start_time, end_time)
|
||||
result_str = self._format_messages_optimized(messages, all_contacts)
|
||||
return result_str
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 启用插件
|
||||
|
||||
确保 `message_summary` 插件已启用,并且已在插件配置中开启。
|
||||
|
||||
### 2. 配置群权限
|
||||
|
||||
对于需要自动总结的群聊,需要开启"群总结能力"功能:
|
||||
|
||||
- 通过机器人群管理命令设置群权限为 `ENABLED`
|
||||
- 功能键:`SUMMARY_CAPABILITY`
|
||||
|
||||
### 3. 自动运行
|
||||
|
||||
- 定时任务会在每天早上 9:00 自动执行
|
||||
- 只会总结昨天(00:00:00 - 23:59:59)的聊天记录
|
||||
- **只有消息数量超过 50 条的群才会生成总结**(按消息条数统计,不是字符数)
|
||||
|
||||
### 4. 手动触发
|
||||
|
||||
除了定时任务外,原有的手动总结功能仍然保留:
|
||||
|
||||
- 在群聊中发送 `#总结` 或 `#summary`
|
||||
- 会总结最近 8 小时的聊天记录
|
||||
|
||||
## 特性说明
|
||||
|
||||
1. **智能过滤**
|
||||
- 只总结文本消息(message_type 1 和 49)
|
||||
- 过滤掉命令消息(以 `/` 开头)
|
||||
- 过滤掉过短(<6字)和过长(>300字)的消息
|
||||
|
||||
2. **权限控制**
|
||||
- 只有开启了总结功能的群才会自动总结
|
||||
- 使用 `GroupBotManager` 管理群权限
|
||||
|
||||
3. **消息数量过滤**
|
||||
- **只有消息数量 >= 50 条的群才会生成总结**
|
||||
- 先统计消息数量,不足 50 条直接跳过,避免浪费资源
|
||||
- 按消息条数统计,不是按字符数
|
||||
|
||||
4. **错误处理**
|
||||
- 如果某个群的总结失败,不会影响其他群
|
||||
- 详细的日志记录便于排查问题
|
||||
|
||||
5. **性能优化**
|
||||
- 群之间间隔 2 秒,避免 API 请求过快
|
||||
- 使用现有的消息格式化方法,保持一致性
|
||||
|
||||
## 日志示例
|
||||
|
||||
```
|
||||
2026-01-09 09:00:00 | INFO | 开始执行每日聊天总结任务
|
||||
2026-01-09 09:00:00 | INFO | 总结时间范围: 2026-01-08 00:00:00 至 2026-01-08 23:59:59
|
||||
2026-01-09 09:00:00 | INFO | 找到 3 个开启定时总结的群聊
|
||||
2026-01-09 09:00:01 | INFO | 群 test_group_1 昨天有 234 条消息,开始获取内容
|
||||
2026-01-09 09:00:01 | INFO | 群 test_group_2 昨天只有 35 条消息,不足50条,跳过总结
|
||||
2026-01-09 09:00:01 | INFO | 群 test_group_3 昨天有 156 条消息,开始获取内容
|
||||
2026-01-09 09:00:02 | INFO | 获取到 234 条消息(时间范围:2026-01-08 00:00:00 至 2026-01-08 23:59:59),格式化后长度: 12580
|
||||
2026-01-09 09:00:02 | INFO | 开始为群 测试群1 生成总结,消息数量: 234,内容长度: 12580
|
||||
2026-01-09 09:00:15 | INFO | 成功发送群 测试群 的昨日总结图片
|
||||
2026-01-09 09:00:17 | INFO | 每日聊天总结任务执行完成
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **时间精度**:定时任务使用系统时间,确保服务器时间准确
|
||||
2. **数据库时区**:确保数据库时区设置正确,存储的 timestamp 使用的是本地时间
|
||||
3. **API 限流**:如果总结的群很多,注意调整 `await asyncio.sleep(2)` 的间隔时间
|
||||
4. **消息量**:如果某个群昨天的消息量很大(>5000条),可能需要调整 `max_results` 参数
|
||||
|
||||
## 自定义配置
|
||||
|
||||
如需修改执行时间,可以修改 `message_summary/main.py` 中的定时任务注册:
|
||||
|
||||
```python
|
||||
# 改为每天早上 8:00
|
||||
async_job.at_times(["08:00"])(self.daily_summary_job)
|
||||
|
||||
# 改为每天晚上 22:00
|
||||
async_job.at_times(["22:00"])(self.daily_summary_job)
|
||||
|
||||
# 改为每天多个时间点
|
||||
async_job.at_times(["09:00", "21:00"])(self.daily_summary_job)
|
||||
```
|
||||
|
||||
## 测试方法
|
||||
|
||||
为了测试定时任务功能,可以临时修改时间:
|
||||
|
||||
```python
|
||||
# 临时修改为当前时间后 1 分钟(用于测试)
|
||||
import datetime
|
||||
next_minute = (datetime.datetime.now() + datetime.timedelta(minutes=1)).strftime("%H:%M")
|
||||
async_job.at_times([next_minute])(self.daily_summary_job)
|
||||
```
|
||||
|
||||
测试完成后记得改回 `["09:00"]`。
|
||||
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
|
||||
@@ -10,6 +12,7 @@ from loguru import logger
|
||||
from base.plugin_common.message_plugin_interface import MessagePluginInterface
|
||||
from base.plugin_common.plugin_interface import PluginStatus
|
||||
from utils.compress_chat_data import compress_chat_data
|
||||
from utils.decorator.async_job import async_job
|
||||
from utils.decorator.plugin_decorators import plugin_stats_decorator
|
||||
from utils.decorator.points_decorator import plugin_points_cost
|
||||
from utils.decorator.rate_limit_decorator import group_feature_rate_limit
|
||||
@@ -67,6 +70,8 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
self.revoke = None
|
||||
# 注册功能权限
|
||||
self.feature = self.register_feature()
|
||||
# 注册定时任务:每天早上9点总结昨天的聊天信息
|
||||
async_job.at_times(["09:00"])(self.daily_summary_job)
|
||||
|
||||
def initialize(self, context: Dict[str, Any]) -> bool:
|
||||
"""初始化插件"""
|
||||
@@ -288,3 +293,107 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
except Exception as e:
|
||||
self.LOG.error(f"处理总结时出现未知错误: {e}")
|
||||
return f"生成总结时出现未知错误: {str(e)}", None
|
||||
|
||||
async def daily_summary_job(self):
|
||||
"""定时任务:每天早上9点总结昨天的聊天信息"""
|
||||
try:
|
||||
self.LOG.info("开始执行每日聊天总结任务")
|
||||
|
||||
# 计算昨天的时间范围
|
||||
yesterday = datetime.now() - timedelta(days=1)
|
||||
yesterday_start = yesterday.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
yesterday_end = yesterday.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
|
||||
self.LOG.info(f"总结时间范围: {yesterday_start.strftime('%Y-%m-%d %H:%M:%S')} 至 {yesterday_end.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 获取所有启用了群机器人的群聊
|
||||
all_groups = GroupBotManager.get_group_list()
|
||||
|
||||
if not all_groups:
|
||||
self.LOG.info("没有群聊启用群机器人,跳过定时总结")
|
||||
return
|
||||
|
||||
# 筛选出开启了总结功能的群聊
|
||||
enabled_groups = []
|
||||
for group_id in all_groups:
|
||||
if GroupBotManager.get_group_permission(group_id, self.feature) == PermissionStatus.ENABLED:
|
||||
enabled_groups.append(group_id)
|
||||
|
||||
if not enabled_groups:
|
||||
self.LOG.info("没有群聊开启定时总结功能,跳过")
|
||||
return
|
||||
|
||||
self.LOG.info(f"找到 {len(enabled_groups)} 个开启定时总结的群聊")
|
||||
|
||||
# 获取所有联系人
|
||||
all_contacts = ContactManager.get_instance().get_all_contacts()
|
||||
|
||||
# 为每个群生成总结
|
||||
for group_id in enabled_groups:
|
||||
try:
|
||||
# 先统计昨天的消息数量
|
||||
message_count = self.message_storage.count_messages_by_date_range(
|
||||
group_id,
|
||||
yesterday_start,
|
||||
yesterday_end
|
||||
)
|
||||
|
||||
# 消息少于50条,跳过总结
|
||||
if message_count < 50:
|
||||
self.LOG.info(f"群 {group_id} 昨天只有 {message_count} 条消息,不足50条,跳过总结")
|
||||
continue
|
||||
|
||||
self.LOG.info(f"群 {group_id} 昨天有 {message_count} 条消息,开始获取内容")
|
||||
|
||||
# 获取群名
|
||||
group_name = all_contacts.get(group_id, group_id)
|
||||
group_name = self._sanitize_group_name(group_name)
|
||||
|
||||
# 获取昨天的聊天记录
|
||||
chat_content = self.message_storage.get_messages_by_date_range(
|
||||
group_id,
|
||||
all_contacts,
|
||||
yesterday_start,
|
||||
yesterday_end
|
||||
)
|
||||
|
||||
if not chat_content:
|
||||
self.LOG.info(f"群 {group_id} 昨天聊天记录为空,跳过总结")
|
||||
continue
|
||||
|
||||
self.LOG.info(f"开始为群 {group_name} 生成总结,消息数量: {message_count},内容长度: {len(chat_content)}")
|
||||
|
||||
# 发送提示消息
|
||||
await self.bot.send_text_message(
|
||||
group_id,
|
||||
f"⏳ 正在生成昨日聊天总结 ({yesterday.strftime('%Y-%m-%d')})… 😊"
|
||||
)
|
||||
|
||||
# 生成总结
|
||||
summary, image_path = await self._generate_summary(chat_content, group_name)
|
||||
|
||||
if image_path:
|
||||
# 图片生成成功,发送图片
|
||||
await self.bot.send_image_message(group_id, Path(image_path))
|
||||
self.LOG.info(f"成功发送群 {group_name} 的昨日总结图片")
|
||||
else:
|
||||
# 图片生成失败,发送文本消息
|
||||
if summary and len(summary.strip()) > 0:
|
||||
max_length = 2000
|
||||
if len(summary) > max_length:
|
||||
summary = summary[:max_length] + "\n\n... (内容过长,已截断)"
|
||||
|
||||
await self.bot.send_text_message(group_id, summary)
|
||||
self.LOG.info(f"成功发送群 {group_name} 的昨日总结文本")
|
||||
|
||||
# 避免请求过快
|
||||
await asyncio.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
self.LOG.error(f"为群 {group_id} 生成昨日总结失败: {e}", exc_info=True)
|
||||
continue
|
||||
|
||||
self.LOG.info("每日聊天总结任务执行完成")
|
||||
|
||||
except Exception as e:
|
||||
self.LOG.error(f"每日聊天总结任务执行失败: {e}", exc_info=True)
|
||||
|
||||
@@ -496,6 +496,55 @@ class MessageStorage:
|
||||
logger.error(f"获取消息出错: {e}")
|
||||
return ""
|
||||
|
||||
def get_messages_by_date_range(self, group_id, all_contacts: dict, start_time: datetime, end_time: datetime):
|
||||
"""获取指定时间范围内的消息
|
||||
|
||||
Args:
|
||||
group_id: 群组ID
|
||||
all_contacts: 联系人字典
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
格式化后的消息字符串
|
||||
"""
|
||||
try:
|
||||
# 使用新的数据库方法获取指定时间范围的消息
|
||||
messages = self.message_db.get_messages_by_date_range(
|
||||
group_id,
|
||||
start_time,
|
||||
end_time
|
||||
)
|
||||
|
||||
# 使用优化后的格式化方法
|
||||
result_str = self._format_messages_optimized(messages, all_contacts)
|
||||
logger.info(f"获取到 {len(messages)} 条消息(时间范围:{start_time} 至 {end_time}),格式化后长度: {len(result_str)}")
|
||||
|
||||
return result_str
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"按时间范围获取消息出错: {e}")
|
||||
return ""
|
||||
|
||||
def count_messages_by_date_range(self, group_id, start_time: datetime, end_time: datetime) -> int:
|
||||
"""统计指定时间范围内的消息数量
|
||||
|
||||
Args:
|
||||
group_id: 群组ID
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
消息数量
|
||||
"""
|
||||
try:
|
||||
count = self.message_db.count_messages_by_date_range(group_id, start_time, end_time)
|
||||
logger.info(f"群 {group_id} 在 {start_time} 至 {end_time} 之间有 {count} 条消息")
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(f"统计消息数量出错: {e}")
|
||||
return 0
|
||||
|
||||
def _format_messages_optimized(self, messages: list, all_contacts: dict) -> str:
|
||||
"""优化的消息格式化方法,减少冗余
|
||||
|
||||
|
||||
Reference in New Issue
Block a user