From ced38e6a9eb12c6b3fc0d73bd6a45b3f652663b1 Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Wed, 11 Feb 2026 12:54:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=A2=9E=E9=87=8F=E5=88=86=E6=9E=90=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=A4=B1=E8=B4=A5=E7=9A=84=E5=87=A0=E4=B8=AA?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E9=97=AE=E9=A2=98=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复报告分发器初始化错误: 在 AutoScheduler 中,初始化 ReportDispatcher 时漏传了 report_generator,导致在尝试生成报告(图片或文本)时出现 'NoneType' object has no attribute 'generate_image_report' 错误。 已更新 AutoScheduler 的构造函数并正确传递 report_generator。 修复 LLM 分析器在增量模式下的数据结构错误:UserTitleAnalyzer 报错:在增量模式下,用户活动数据被简化存储,导致 UserTitleAnalyzer 找不到 'hours' 键(Error: 'hours')。 补全数据维度:更新了 AnalysisApplicationService 的增量数据转换逻辑,保留了完整的每小时活跃分布 (hours) 和回复数 (reply_count)。 统一字段名:增量模式下使用了 nickname 字段以匹配分析器的预期(之前被错误转为了 name)。 增强容错性:优化了 UserTitleAnalyzer 的逻辑,现在它能安全地处理 hours 缺失或旧版本数据 schema,不再会因为找不到键而崩溃。 修复增量数据合并逻辑: 更新了 IncrementalMergeService,现在它能正确合并多个批次中的每小时发言统计和回复数统计,确保最终生成的报告数据准确。 --- main.py | 1 + .../services/analysis_application_service.py | 56 +++++++++---------- .../services/incremental_merge_service.py | 35 +++++++++--- .../analysis/analyzers/user_title_analyzer.py | 42 +++++++------- .../scheduler/auto_scheduler.py | 4 +- 5 files changed, 81 insertions(+), 57 deletions(-) diff --git a/main.py b/main.py index c5fb4bf..d6bd2c8 100644 --- a/main.py +++ b/main.py @@ -78,6 +78,7 @@ class QQGroupDailyAnalysis(Star): self.analysis_service, self.bot_manager, self.retry_manager, + self.report_generator, self.html_render, ) diff --git a/src/application/services/analysis_application_service.py b/src/application/services/analysis_application_service.py index c077caf..8a19c05 100644 --- a/src/application/services/analysis_application_service.py +++ b/src/application/services/analysis_application_service.py @@ -250,9 +250,7 @@ class AnalysisApplicationService: if last_analyzed_ts > 0: unified_messages = [ - msg - for msg in unified_messages - if msg.timestamp > last_analyzed_ts + msg for msg in unified_messages if msg.timestamp > last_analyzed_ts ] # 5. 检查最小消息阈值 @@ -282,7 +280,7 @@ class AnalysisApplicationService: # 7. LLM 增量分析(仅话题 + 金句) topics_per_batch = self.config_manager.get_incremental_topics_per_batch() quotes_per_batch = self.config_manager.get_incremental_quotes_per_batch() - + # 获取功能开关状态 topic_enabled = self.config_manager.get_topic_analysis_enabled() golden_quote_enabled = self.config_manager.get_golden_quote_analysis_enabled() @@ -295,15 +293,17 @@ class AnalysisApplicationService: f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id ) - topics, golden_quotes, token_usage = ( - await self.llm_analyzer.analyze_incremental_concurrent( - legacy_messages, - umo=unified_msg_origin, - topics_per_batch=topics_per_batch, - quotes_per_batch=quotes_per_batch, - topic_enabled=topic_enabled, - golden_quote_enabled=golden_quote_enabled, - ) + ( + topics, + golden_quotes, + token_usage, + ) = await self.llm_analyzer.analyze_incremental_concurrent( + legacy_messages, + umo=unified_msg_origin, + topics_per_batch=topics_per_batch, + quotes_per_batch=quotes_per_batch, + topic_enabled=topic_enabled, + golden_quote_enabled=golden_quote_enabled, ) # 8. 构建 IncrementalBatch @@ -441,9 +441,7 @@ class AnalysisApplicationService: # 3. 检查批次有效性 if not batches: - logger.warning( - f"群 {group_id} 滑动窗口内无增量分析数据,无法生成最终报告" - ) + logger.warning(f"群 {group_id} 滑动窗口内无增量分析数据,无法生成最终报告") return {"success": False, "reason": "no_incremental_data"} # 4. 合并批次为 IncrementalState @@ -466,19 +464,18 @@ class AnalysisApplicationService: top_users = state.get_user_activity_ranking(max_user_titles) unified_msg_origin = ( - f"{platform_id}:GroupMessage:{group_id}" - if platform_id - else group_id + f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id ) try: - user_titles_result, title_token_usage = ( - await self.llm_analyzer.analyze_user_titles( - messages=[], # 增量模式下不传原始消息 - user_analysis=state.user_activities, - umo=unified_msg_origin, - top_users=top_users, - ) + ( + user_titles_result, + title_token_usage, + ) = await self.llm_analyzer.analyze_user_titles( + messages=[], # 增量模式下不传原始消息 + user_analysis=state.user_activities, + umo=unified_msg_origin, + top_users=top_users, ) user_titles = user_titles_result @@ -579,11 +576,14 @@ class AnalysisApplicationService: result: dict[str, dict] = {} for user_id, stats in user_activity.items(): result[user_id] = { - "name": stats.get("nickname", user_id), + "nickname": stats.get("nickname", user_id), "message_count": stats.get("message_count", 0), "char_count": stats.get("char_count", 0), "emoji_count": stats.get("emoji_count", 0), - "active_hours": list(stats.get("hours", {}).keys()), + "reply_count": stats.get("reply_count", 0), + "hours": dict( + stats.get("hours", {}) + ), # 这里的 hours 是 defaultdict(int),转为 dict "last_message_time": user_last_time.get(user_id, 0), } diff --git a/src/domain/services/incremental_merge_service.py b/src/domain/services/incremental_merge_service.py index 35caa85..f72b909 100644 --- a/src/domain/services/incremental_merge_service.py +++ b/src/domain/services/incremental_merge_service.py @@ -86,29 +86,46 @@ class IncrementalMergeService: for user_id, stats in batch.user_stats.items(): if user_id not in state.user_activities: state.user_activities[user_id] = { - "name": stats.get("name", user_id), + "nickname": stats.get("nickname", stats.get("name", user_id)), "message_count": 0, "char_count": 0, "emoji_count": 0, - "active_hours": [], + "reply_count": 0, + "hours": {}, "last_message_time": 0, } existing = state.user_activities[user_id] existing["message_count"] += stats.get("message_count", 0) existing["char_count"] += stats.get("char_count", 0) existing["emoji_count"] += stats.get("emoji_count", 0) - # 合并活跃小时(去重) - existing_hours = set(existing.get("active_hours", [])) - existing_hours.update(stats.get("active_hours", [])) - existing["active_hours"] = list(existing_hours) + existing["reply_count"] += stats.get("reply_count", 0) + + # 合并每小时统计 + # 兼容旧版本 (active_hours 是 list) 和新版本 (hours 是 dict) + batch_hours = stats.get("hours", {}) + if isinstance(batch_hours, dict): + # 现代 schema: hours 是 dict {hour: count} + for h_str, h_count in batch_hours.items(): + h_int = int(h_str) + existing["hours"][h_int] = ( + existing["hours"].get(h_int, 0) + h_count + ) + else: + # 兼容旧 schema: 只有 active_hours (list) + active_hours = stats.get("active_hours", []) + for h in active_hours: + h_int = int(h) + existing["hours"][h_int] = existing["hours"].get(h_int, 0) + 1 + # 取最后消息时间的较大值 batch_last = stats.get("last_message_time", 0) if batch_last > existing.get("last_message_time", 0): existing["last_message_time"] = batch_last + # 更新昵称(使用最新批次的昵称) - name = stats.get("name", "") - if name: - existing["name"] = name + nickname = stats.get("nickname", stats.get("name", "")) + if nickname: + existing["nickname"] = nickname # 合并表情统计(按键累加) for emoji_key, count in batch.emoji_stats.items(): diff --git a/src/infrastructure/analysis/analyzers/user_title_analyzer.py b/src/infrastructure/analysis/analyzers/user_title_analyzer.py index c671821..55fe45d 100644 --- a/src/infrastructure/analysis/analyzers/user_title_analyzer.py +++ b/src/infrastructure/analysis/analyzers/user_title_analyzer.py @@ -187,32 +187,36 @@ class UserTitleAnalyzer(BaseAnalyzer): continue # 分析用户特征 (此处已基于已清理的 stats) - night_messages = sum(stats["hours"][h] for h in range(6)) - avg_chars = ( - stats["char_count"] / stats["message_count"] - if stats["message_count"] > 0 - else 0 - ) + # 兼容性处理:优先使用 hours (dict),如果没有则尝试从消息推断或使用空 + hours_data = stats.get("hours") + if hours_data is None: + # 尝试兼容旧 schema 或简化版 + active_hours = stats.get("active_hours", []) + hours_data = dict.fromkeys(active_hours, 1) + # 安全计算夜间发言数 + night_messages = sum(hours_data.get(h, 0) for h in range(6)) + + message_count = stats.get("message_count", 0) + if message_count <= 0: + continue + + avg_chars = stats.get("char_count", 0) / message_count + + # 称号所需维度 user_summaries.append( { - "name": stats["nickname"], + "name": stats.get("nickname", stats.get("name", user_id_str)), "user_id": user_id_str, - "message_count": stats["message_count"], + "message_count": message_count, "avg_chars": round(avg_chars, 1), "emoji_ratio": round( - stats["emoji_count"] / stats["message_count"], 2 - ) - if stats["message_count"] > 0 - else 0, - "night_ratio": round(night_messages / stats["message_count"], 2) - if stats["message_count"] > 0 - else 0, + stats.get("emoji_count", 0) / message_count, 2 + ), + "night_ratio": round(night_messages / message_count, 2), "reply_ratio": round( - stats["reply_count"] / stats["message_count"], 2 - ) - if stats["message_count"] > 0 - else 0, + stats.get("reply_count", 0) / message_count, 2 + ), } ) diff --git a/src/infrastructure/scheduler/auto_scheduler.py b/src/infrastructure/scheduler/auto_scheduler.py index 5414765..5c3e4d8 100644 --- a/src/infrastructure/scheduler/auto_scheduler.py +++ b/src/infrastructure/scheduler/auto_scheduler.py @@ -25,18 +25,20 @@ class AutoScheduler: analysis_service, bot_manager, retry_manager, + report_generator=None, html_render_func=None, ): self.config_manager = config_manager self.analysis_service = analysis_service self.bot_manager = bot_manager self.retry_manager = retry_manager + self.report_generator = report_generator self.html_render_func = html_render_func # 初始化核心组件 self.message_sender = MessageSender(bot_manager, config_manager, retry_manager) self.report_dispatcher = ReportDispatcher( - config_manager, None, self.message_sender, retry_manager + config_manager, report_generator, self.message_sender, retry_manager ) if html_render_func: self.report_dispatcher.set_html_render(html_render_func)