调整总结业务。支持每天早上9点总结昨天的消息

This commit is contained in:
liuwei
2026-01-09 14:51:27 +08:00
parent 0fa430f085
commit edc63fad0a
4 changed files with 415 additions and 0 deletions

View File

@@ -342,3 +342,59 @@ class MessageStorageDB(BaseDBOperator):
# 限制最大返回数量5000条足以覆盖1-2天的活跃群聊 # 限制最大返回数量5000条足以覆盖1-2天的活跃群聊
return messages[:max_results] if messages else [] 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

View 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"]`

View File

@@ -1,6 +1,8 @@
import asyncio
import json import json
import re import re
import time import time
from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Tuple, Optional, List 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.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus from base.plugin_common.plugin_interface import PluginStatus
from utils.compress_chat_data import compress_chat_data 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.plugin_decorators import plugin_stats_decorator
from utils.decorator.points_decorator import plugin_points_cost from utils.decorator.points_decorator import plugin_points_cost
from utils.decorator.rate_limit_decorator import group_feature_rate_limit from utils.decorator.rate_limit_decorator import group_feature_rate_limit
@@ -67,6 +70,8 @@ class MessageSummaryPlugin(MessagePluginInterface):
self.revoke = None self.revoke = None
# 注册功能权限 # 注册功能权限
self.feature = self.register_feature() self.feature = self.register_feature()
# 注册定时任务每天早上9点总结昨天的聊天信息
async_job.at_times(["09:00"])(self.daily_summary_job)
def initialize(self, context: Dict[str, Any]) -> bool: def initialize(self, context: Dict[str, Any]) -> bool:
"""初始化插件""" """初始化插件"""
@@ -288,3 +293,107 @@ class MessageSummaryPlugin(MessagePluginInterface):
except Exception as e: except Exception as e:
self.LOG.error(f"处理总结时出现未知错误: {e}") self.LOG.error(f"处理总结时出现未知错误: {e}")
return f"生成总结时出现未知错误: {str(e)}", None 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)

View File

@@ -496,6 +496,55 @@ class MessageStorage:
logger.error(f"获取消息出错: {e}") logger.error(f"获取消息出错: {e}")
return "" 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: def _format_messages_optimized(self, messages: list, all_contacts: dict) -> str:
"""优化的消息格式化方法,减少冗余 """优化的消息格式化方法,减少冗余