mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
fix: 修复了导致增量分析报告发送失败的几个核心问题:
修复报告分发器初始化错误: 在 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,现在它能正确合并多个批次中的每小时发言统计和回复数统计,确保最终生成的报告数据准确。
This commit is contained in:
@@ -78,6 +78,7 @@ class QQGroupDailyAnalysis(Star):
|
|||||||
self.analysis_service,
|
self.analysis_service,
|
||||||
self.bot_manager,
|
self.bot_manager,
|
||||||
self.retry_manager,
|
self.retry_manager,
|
||||||
|
self.report_generator,
|
||||||
self.html_render,
|
self.html_render,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -250,9 +250,7 @@ class AnalysisApplicationService:
|
|||||||
|
|
||||||
if last_analyzed_ts > 0:
|
if last_analyzed_ts > 0:
|
||||||
unified_messages = [
|
unified_messages = [
|
||||||
msg
|
msg for msg in unified_messages if msg.timestamp > last_analyzed_ts
|
||||||
for msg in unified_messages
|
|
||||||
if msg.timestamp > last_analyzed_ts
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# 5. 检查最小消息阈值
|
# 5. 检查最小消息阈值
|
||||||
@@ -282,7 +280,7 @@ class AnalysisApplicationService:
|
|||||||
# 7. LLM 增量分析(仅话题 + 金句)
|
# 7. LLM 增量分析(仅话题 + 金句)
|
||||||
topics_per_batch = self.config_manager.get_incremental_topics_per_batch()
|
topics_per_batch = self.config_manager.get_incremental_topics_per_batch()
|
||||||
quotes_per_batch = self.config_manager.get_incremental_quotes_per_batch()
|
quotes_per_batch = self.config_manager.get_incremental_quotes_per_batch()
|
||||||
|
|
||||||
# 获取功能开关状态
|
# 获取功能开关状态
|
||||||
topic_enabled = self.config_manager.get_topic_analysis_enabled()
|
topic_enabled = self.config_manager.get_topic_analysis_enabled()
|
||||||
golden_quote_enabled = self.config_manager.get_golden_quote_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
|
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
|
||||||
)
|
)
|
||||||
|
|
||||||
topics, golden_quotes, token_usage = (
|
(
|
||||||
await self.llm_analyzer.analyze_incremental_concurrent(
|
topics,
|
||||||
legacy_messages,
|
golden_quotes,
|
||||||
umo=unified_msg_origin,
|
token_usage,
|
||||||
topics_per_batch=topics_per_batch,
|
) = await self.llm_analyzer.analyze_incremental_concurrent(
|
||||||
quotes_per_batch=quotes_per_batch,
|
legacy_messages,
|
||||||
topic_enabled=topic_enabled,
|
umo=unified_msg_origin,
|
||||||
golden_quote_enabled=golden_quote_enabled,
|
topics_per_batch=topics_per_batch,
|
||||||
)
|
quotes_per_batch=quotes_per_batch,
|
||||||
|
topic_enabled=topic_enabled,
|
||||||
|
golden_quote_enabled=golden_quote_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 8. 构建 IncrementalBatch
|
# 8. 构建 IncrementalBatch
|
||||||
@@ -441,9 +441,7 @@ class AnalysisApplicationService:
|
|||||||
|
|
||||||
# 3. 检查批次有效性
|
# 3. 检查批次有效性
|
||||||
if not batches:
|
if not batches:
|
||||||
logger.warning(
|
logger.warning(f"群 {group_id} 滑动窗口内无增量分析数据,无法生成最终报告")
|
||||||
f"群 {group_id} 滑动窗口内无增量分析数据,无法生成最终报告"
|
|
||||||
)
|
|
||||||
return {"success": False, "reason": "no_incremental_data"}
|
return {"success": False, "reason": "no_incremental_data"}
|
||||||
|
|
||||||
# 4. 合并批次为 IncrementalState
|
# 4. 合并批次为 IncrementalState
|
||||||
@@ -466,19 +464,18 @@ class AnalysisApplicationService:
|
|||||||
top_users = state.get_user_activity_ranking(max_user_titles)
|
top_users = state.get_user_activity_ranking(max_user_titles)
|
||||||
|
|
||||||
unified_msg_origin = (
|
unified_msg_origin = (
|
||||||
f"{platform_id}:GroupMessage:{group_id}"
|
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
|
||||||
if platform_id
|
|
||||||
else group_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_titles_result, title_token_usage = (
|
(
|
||||||
await self.llm_analyzer.analyze_user_titles(
|
user_titles_result,
|
||||||
messages=[], # 增量模式下不传原始消息
|
title_token_usage,
|
||||||
user_analysis=state.user_activities,
|
) = await self.llm_analyzer.analyze_user_titles(
|
||||||
umo=unified_msg_origin,
|
messages=[], # 增量模式下不传原始消息
|
||||||
top_users=top_users,
|
user_analysis=state.user_activities,
|
||||||
)
|
umo=unified_msg_origin,
|
||||||
|
top_users=top_users,
|
||||||
)
|
)
|
||||||
user_titles = user_titles_result
|
user_titles = user_titles_result
|
||||||
|
|
||||||
@@ -579,11 +576,14 @@ class AnalysisApplicationService:
|
|||||||
result: dict[str, dict] = {}
|
result: dict[str, dict] = {}
|
||||||
for user_id, stats in user_activity.items():
|
for user_id, stats in user_activity.items():
|
||||||
result[user_id] = {
|
result[user_id] = {
|
||||||
"name": stats.get("nickname", user_id),
|
"nickname": stats.get("nickname", user_id),
|
||||||
"message_count": stats.get("message_count", 0),
|
"message_count": stats.get("message_count", 0),
|
||||||
"char_count": stats.get("char_count", 0),
|
"char_count": stats.get("char_count", 0),
|
||||||
"emoji_count": stats.get("emoji_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),
|
"last_message_time": user_last_time.get(user_id, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,29 +86,46 @@ class IncrementalMergeService:
|
|||||||
for user_id, stats in batch.user_stats.items():
|
for user_id, stats in batch.user_stats.items():
|
||||||
if user_id not in state.user_activities:
|
if user_id not in state.user_activities:
|
||||||
state.user_activities[user_id] = {
|
state.user_activities[user_id] = {
|
||||||
"name": stats.get("name", user_id),
|
"nickname": stats.get("nickname", stats.get("name", user_id)),
|
||||||
"message_count": 0,
|
"message_count": 0,
|
||||||
"char_count": 0,
|
"char_count": 0,
|
||||||
"emoji_count": 0,
|
"emoji_count": 0,
|
||||||
"active_hours": [],
|
"reply_count": 0,
|
||||||
|
"hours": {},
|
||||||
"last_message_time": 0,
|
"last_message_time": 0,
|
||||||
}
|
}
|
||||||
existing = state.user_activities[user_id]
|
existing = state.user_activities[user_id]
|
||||||
existing["message_count"] += stats.get("message_count", 0)
|
existing["message_count"] += stats.get("message_count", 0)
|
||||||
existing["char_count"] += stats.get("char_count", 0)
|
existing["char_count"] += stats.get("char_count", 0)
|
||||||
existing["emoji_count"] += stats.get("emoji_count", 0)
|
existing["emoji_count"] += stats.get("emoji_count", 0)
|
||||||
# 合并活跃小时(去重)
|
existing["reply_count"] += stats.get("reply_count", 0)
|
||||||
existing_hours = set(existing.get("active_hours", []))
|
|
||||||
existing_hours.update(stats.get("active_hours", []))
|
# 合并每小时统计
|
||||||
existing["active_hours"] = list(existing_hours)
|
# 兼容旧版本 (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)
|
batch_last = stats.get("last_message_time", 0)
|
||||||
if batch_last > existing.get("last_message_time", 0):
|
if batch_last > existing.get("last_message_time", 0):
|
||||||
existing["last_message_time"] = batch_last
|
existing["last_message_time"] = batch_last
|
||||||
|
|
||||||
# 更新昵称(使用最新批次的昵称)
|
# 更新昵称(使用最新批次的昵称)
|
||||||
name = stats.get("name", "")
|
nickname = stats.get("nickname", stats.get("name", ""))
|
||||||
if name:
|
if nickname:
|
||||||
existing["name"] = name
|
existing["nickname"] = nickname
|
||||||
|
|
||||||
# 合并表情统计(按键累加)
|
# 合并表情统计(按键累加)
|
||||||
for emoji_key, count in batch.emoji_stats.items():
|
for emoji_key, count in batch.emoji_stats.items():
|
||||||
|
|||||||
@@ -187,32 +187,36 @@ class UserTitleAnalyzer(BaseAnalyzer):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 分析用户特征 (此处已基于已清理的 stats)
|
# 分析用户特征 (此处已基于已清理的 stats)
|
||||||
night_messages = sum(stats["hours"][h] for h in range(6))
|
# 兼容性处理:优先使用 hours (dict),如果没有则尝试从消息推断或使用空
|
||||||
avg_chars = (
|
hours_data = stats.get("hours")
|
||||||
stats["char_count"] / stats["message_count"]
|
if hours_data is None:
|
||||||
if stats["message_count"] > 0
|
# 尝试兼容旧 schema 或简化版
|
||||||
else 0
|
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(
|
user_summaries.append(
|
||||||
{
|
{
|
||||||
"name": stats["nickname"],
|
"name": stats.get("nickname", stats.get("name", user_id_str)),
|
||||||
"user_id": user_id_str,
|
"user_id": user_id_str,
|
||||||
"message_count": stats["message_count"],
|
"message_count": message_count,
|
||||||
"avg_chars": round(avg_chars, 1),
|
"avg_chars": round(avg_chars, 1),
|
||||||
"emoji_ratio": round(
|
"emoji_ratio": round(
|
||||||
stats["emoji_count"] / stats["message_count"], 2
|
stats.get("emoji_count", 0) / message_count, 2
|
||||||
)
|
),
|
||||||
if stats["message_count"] > 0
|
"night_ratio": round(night_messages / message_count, 2),
|
||||||
else 0,
|
|
||||||
"night_ratio": round(night_messages / stats["message_count"], 2)
|
|
||||||
if stats["message_count"] > 0
|
|
||||||
else 0,
|
|
||||||
"reply_ratio": round(
|
"reply_ratio": round(
|
||||||
stats["reply_count"] / stats["message_count"], 2
|
stats.get("reply_count", 0) / message_count, 2
|
||||||
)
|
),
|
||||||
if stats["message_count"] > 0
|
|
||||||
else 0,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,18 +25,20 @@ class AutoScheduler:
|
|||||||
analysis_service,
|
analysis_service,
|
||||||
bot_manager,
|
bot_manager,
|
||||||
retry_manager,
|
retry_manager,
|
||||||
|
report_generator=None,
|
||||||
html_render_func=None,
|
html_render_func=None,
|
||||||
):
|
):
|
||||||
self.config_manager = config_manager
|
self.config_manager = config_manager
|
||||||
self.analysis_service = analysis_service
|
self.analysis_service = analysis_service
|
||||||
self.bot_manager = bot_manager
|
self.bot_manager = bot_manager
|
||||||
self.retry_manager = retry_manager
|
self.retry_manager = retry_manager
|
||||||
|
self.report_generator = report_generator
|
||||||
self.html_render_func = html_render_func
|
self.html_render_func = html_render_func
|
||||||
|
|
||||||
# 初始化核心组件
|
# 初始化核心组件
|
||||||
self.message_sender = MessageSender(bot_manager, config_manager, retry_manager)
|
self.message_sender = MessageSender(bot_manager, config_manager, retry_manager)
|
||||||
self.report_dispatcher = ReportDispatcher(
|
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:
|
if html_render_func:
|
||||||
self.report_dispatcher.set_html_render(html_render_func)
|
self.report_dispatcher.set_html_render(html_render_func)
|
||||||
|
|||||||
Reference in New Issue
Block a user