From 9534d4ea68dad49e8bf500e2120ccfcd60f4c53b Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Sun, 8 Feb 2026 00:24:09 +0800 Subject: [PATCH] feat: implement sub-daily history tracking for analysis results --- main.py | 7 ++- src/core/history_manager.py | 103 ++++++++++++++++++++++++++++++++ src/scheduler/auto_scheduler.py | 24 +++++++- tests/verify_history.py | 60 +++++++++++++++++++ 4 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 src/core/history_manager.py create mode 100644 tests/verify_history.py diff --git a/main.py b/main.py index cc181a0..f413ccd 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,7 @@ from .src.core.bot_manager import BotManager # 导入重构后的模块 from .src.core.config import ConfigManager +from .src.core.history_manager import HistoryManager from .src.reports.generators import ReportGenerator from .src.scheduler.auto_scheduler import AutoScheduler from .src.scheduler.retry import RetryManager @@ -40,6 +41,7 @@ class QQGroupDailyAnalysis(Star): context, self.config_manager, self.bot_manager ) self.report_generator = ReportGenerator(self.config_manager) + self.history_manager = HistoryManager(self) self.retry_manager = RetryManager( self.bot_manager, self.html_render, self.report_generator ) @@ -50,6 +52,7 @@ class QQGroupDailyAnalysis(Star): self.report_generator, self.bot_manager, self.retry_manager, + self.history_manager, self.html_render, # 传入html_render函数 ) @@ -187,11 +190,13 @@ class QQGroupDailyAnalysis(Star): messages, group_id, event.unified_msg_origin ) - # 检查分析结果 if not analysis_result or not analysis_result.get("statistics"): yield event.plain_result("❌ 分析过程中出现错误,请稍后重试") return + # 保存到历史记录 + await self.history_manager.save_analysis(group_id, analysis_result) + # 生成报告 output_format = self.config_manager.get_output_format() if output_format == "image": diff --git a/src/core/history_manager.py b/src/core/history_manager.py new file mode 100644 index 0000000..922616b --- /dev/null +++ b/src/core/history_manager.py @@ -0,0 +1,103 @@ +""" +历史记录管理器模块 +负责存储和查询群聊分析报告的摘要信息 +使用 AstrBot 的 put_kv_data/get_kv_data 实现 +""" + +import datetime +from typing import Any + +from astrbot.api import logger + + +class HistoryManager: + """历史分析记录管理器""" + + def __init__(self, star_instance): + """ + 初始化历史记录管理器 + + Args: + star_instance: Star 插件实例,用于访问 put_kv_data/get_kv_data + """ + self.plugin = star_instance + + async def save_analysis( + self, + group_id: str, + analysis_result: dict[str, Any], + date_str: str | None = None, + time_str: str | None = None, + ) -> bool: + """ + 保存分析结果摘要到历史记录 + + Args: + group_id: 群组ID + analysis_result: 分析结果对象 + date_str: 日期字符串 (格式: YYYY-MM-DD),如果不提供则使用当前日期 + time_str: 时间字符串 (格式: HH-MM),如果不提供则使用当前时间 + """ + try: + now = datetime.datetime.now() + if not date_str: + date_str = now.strftime("%Y-%m-%d") + if not time_str: + time_str = now.strftime("%H-%M") + + # 格式化 time_str,确保文件名/Key 安全 (替换 : 为 -) + time_str = time_str.replace(":", "-") + + # 提取摘要数据 + stats = analysis_result.get("statistics") + topics = analysis_result.get("topics", []) + user_titles = analysis_result.get("user_titles", []) + + summary = { + "message_count": getattr(stats, "message_count", 0) if stats else 0, + "participant_count": getattr(stats, "participant_count", 0) + if stats + else 0, + "topics": [{"topic": t.topic, "detail": t.detail} for t in topics], + "user_titles_count": len(user_titles), + "generated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + + key = f"analysis_{group_id}_{date_str}_{time_str}" + await self.plugin.put_kv_data(key, summary) + + logger.info( + f"已保存群 {group_id} 在 {date_str} {time_str} 的分析摘要到历史记录 (Key: {key})" + ) + return True + except Exception as e: + logger.error(f"保存历史分析记录失败: {e}", exc_info=True) + return False + + async def get_history( + self, group_id: str, date_str: str, time_str: str + ) -> dict[str, Any] | None: + """ + 获取指定日期、时间点和群组的分析摘要 + + Args: + group_id: 群组ID + date_str: 日期字符串 (YYYY-MM-DD) + time_str: 时间字符串 (HH-MM) + """ + # 确保格式统一 + time_str = time_str.replace(":", "-") + key = f"analysis_{group_id}_{date_str}_{time_str}" + return await self.plugin.get_kv_data(key, None) + + async def has_history(self, group_id: str, date_str: str, time_str: str) -> bool: + """ + 检查指定日期、时间点和群组是否已有分析记录 + + Args: + group_id: 群组ID + date_str: 日期字符串 (YYYY-MM-DD) + time_str: 时间字符串 (HH-MM) + """ + history = await self.get_history(group_id, date_str, time_str) + return history is not None diff --git a/src/scheduler/auto_scheduler.py b/src/scheduler/auto_scheduler.py index 5cc782a..74d99dd 100644 --- a/src/scheduler/auto_scheduler.py +++ b/src/scheduler/auto_scheduler.py @@ -25,7 +25,8 @@ class AutoScheduler: analyzer, report_generator, bot_manager, - retry_manager, # 新增 + retry_manager, + history_manager, html_render_func=None, ): self.config_manager = config_manager @@ -34,6 +35,7 @@ class AutoScheduler: self.report_generator = report_generator self.bot_manager = bot_manager self.retry_manager = retry_manager # 保存引用 + self.history_manager = history_manager self.html_render_func = html_render_func # Initialize Core Components @@ -282,6 +284,21 @@ class AutoScheduler: # 设置 TraceID trace_id = TraceContext.generate(prefix=f"group_{group_id}") TraceContext.set(trace_id) + + # 获取当前日期和时间槽 (HH-MM) + import datetime + + now = datetime.datetime.now() + date_str = now.strftime("%Y-%m-%d") + time_str = now.strftime("%H-%M") + + # 检查是否已有该时间段分析记录 + if await self.history_manager.has_history(group_id, date_str, time_str): + logger.info( + f"群 {group_id} 在 {date_str} {time_str} 已有分析记录,跳过自动分析" + ) + return + logger.info(f"开始为群 {group_id} 执行自动分析(并发任务)") # 检查bot管理器状态 @@ -411,6 +428,11 @@ class AutoScheduler: group_id, analysis_result, platform_id ) + # 保存到历史记录 + await self.history_manager.save_analysis( + group_id, analysis_result, date_str, time_str + ) + # 记录执行时间 end_time = asyncio.get_event_loop().time() execution_time = end_time - start_time diff --git a/tests/verify_history.py b/tests/verify_history.py new file mode 100644 index 0000000..0ca3f14 --- /dev/null +++ b/tests/verify_history.py @@ -0,0 +1,60 @@ +import asyncio +import sys +from unittest.mock import AsyncMock, MagicMock +from datetime import datetime + +# 模拟环境 +sys.modules['astrbot'] = MagicMock() +sys.modules['astrbot.api'] = MagicMock() + +async def verify_history(): + print("开始验证 HistoryManager...") + + # 模拟 Star 实例 + mock_star = MagicMock() + mock_star.put_kv_data = AsyncMock() + mock_star.get_kv_data = AsyncMock() + + # 导入 HistoryManager (使用相对路径或动态导入) + from src.core.history_manager import HistoryManager + + hm = HistoryManager(mock_star) + + # 1. 测试保存分析 + group_id = "123456" + mock_stats = MagicMock() + mock_stats.message_count = 100 + mock_stats.participant_count = 10 + + analysis_result = { + "statistics": mock_stats, + "topics": [], + "user_titles": [] + } + + print(f"测试保存群 {group_id} 的分析摘要 (带时间槽)...") + await hm.save_analysis(group_id, analysis_result, "2026-02-08", "12-00") + + # 验证 put_kv_data 被调用,且 key 正确 + expected_key = f"analysis_{group_id}_2026-02-08_12-00" + mock_star.put_kv_data.assert_called_once() + actual_key = mock_star.put_kv_data.call_args[0][0] + actual_data = mock_star.put_kv_data.call_args[0][1] + + assert actual_key == expected_key + assert actual_data["message_count"] == 100 + print(f"✅ 保存验证成功: Key={actual_key}") + + # 2. 测试检查历史 (不同时间点) + mock_star.get_kv_data.side_effect = lambda type, pid, key, default: {"message_count": 100} if "12-00" in key else None + + has_history_12 = await hm.has_history(group_id, "2026-02-08", "12-00") + assert has_history_12 is True + print(f"✅ 12:00 历史检查成功") + + has_history_13 = await hm.has_history(group_id, "2026-02-08", "13-00") + assert has_history_13 is False + print(f"✅ 13:00 (无历史) 检查成功") + +if __name__ == "__main__": + asyncio.run(verify_history())