feat: 实现群聊分析任务锁及全局资源并发限流控制

1. 任务锁定机制 (Group Task Locking):
   - 在应用服务层实现基于群组和任务类型的异步锁,使用 asynccontextmanager 生成器函数确保同一群聊在同一时间只能执行一个分析任务。
   - 使用弱引用字典 (WeakValueDictionary) 管理锁实例,防止长期的内存占用。
   - 在指令入口增加对 CancelledError 的捕获,当任务冲突时向用户反馈“任务正在执行中”的友好提示。

2. 全局并发限流重构 (Resource-level Throttling):
   - 资源层限流下沉:将并发控制中心从调度层迁移至具体的资源处理层(T2I/LLM)。
   - T2I 渲染保护:在报告生成器中引入全局信号量,严格控制 T2I 引擎的瞬时负载。
   - LLM API 保护:在应用服务层引入全局信号量,平滑所有分析请求对 LLM 提供商接口的请求频率。
   - 调度逻辑简化:移除调度器内部冗余的信号量,使其仅专注于分发逻辑与 Stagger(交错启动)平滑。

3. 调度与反馈优化:
   - 自动分析调度器增加对“跳过”任务的独立统计,并在执行报告中清晰展示“成功、跳过、失败”的具体数量。
   - 优化了管理测试指令和增量分析指令的异常处理逻辑。

4. 消息重复发送防护:
   - 在重试组件中增加基于消息历史记录的探活逻辑。在重试发送前先核实记录,确认是否已因网络抖动导致“假失败”但已实达,有效拦截重复消息。
This commit is contained in:
SXP-Simon
2026-03-03 21:07:51 +08:00
committed by Helian Nuits
parent 3956939837
commit ee0bf037b8
8 changed files with 742 additions and 667 deletions
@@ -7,7 +7,9 @@
import asyncio
import datetime as dt
import time as time_mod
import weakref
from collections import defaultdict
from contextlib import asynccontextmanager
from typing import Any
from ...domain.entities.incremental_state import IncrementalBatch
@@ -46,6 +48,37 @@ class AnalysisApplicationService:
self.analysis_domain_service = analysis_domain_service
self.incremental_store = incremental_store
self.incremental_merge_service = incremental_merge_service
self._locks = weakref.WeakValueDictionary()
# 全局 LLM 分析信号量,控制对外 API 的并发压力
max_concurrent = self.config_manager.get_max_concurrent_tasks()
self.llm_semaphore = asyncio.Semaphore(max_concurrent)
@asynccontextmanager
async def group_lock(self, group_id: str, task_type: str = "analysis"):
"""
同一时间、同一个群、同一种任务只能有一个在执行
锁将在退出上下文时自动释放。
"""
lock_key = f"{task_type}:{group_id}"
# 获取或创建该群专属的异步锁
lock = self._locks.get(lock_key)
if lock is None:
lock = asyncio.Lock()
self._locks[lock_key] = lock
# 检查是否已经锁定(防止并发)
if lock.locked():
logger.warning(f"{group_id}{task_type} 任务已在运行,跳过本次请求")
# 这里抛出异常以便上层识别并优雅跳过
raise asyncio.CancelledError(f"Duplicate task for {lock_key}")
async with lock:
logger.debug(f"[Lock] 已获取群 {group_id}{task_type} 排他锁")
try:
yield
finally:
logger.debug(f"[Lock] 已释放群 {group_id}{task_type} 排他锁")
async def execute_daily_analysis(
self, group_id: str, platform_id: str | None = None, manual: bool = False
@@ -63,121 +96,129 @@ class AnalysisApplicationService:
7. 持久化摘要 (Persistence)
8. 返回结果
"""
logger.info(f"开始执行分析用例: 群 {group_id}, 平台 {platform_id or '默认'}")
# 1. 获取适配器
adapter = self.bot_manager.get_adapter(platform_id)
if not adapter:
raise ValueError(f"未找到平台 {platform_id} 的适配器")
# 2. 拉取消息
days = self.config_manager.get_analysis_days()
max_count = self.config_manager.get_max_messages()
raw_messages = await adapter.fetch_messages(
group_id=group_id, days=days, max_count=max_count
)
if not raw_messages:
logger.warning(f"{group_id} 在最近 {days} 天内无消息或无法获取")
return {"success": False, "reason": "no_messages"}
# 3. 清理消息 (Filter commands, bot messages, noise)
from ...domain.services.message_cleaner_service import MessageCleanerService
cleaner = MessageCleanerService()
bot_self_ids = self.config_manager.get_bot_self_ids()
# 对于自动任务,强制过滤指令;对于手动任务,也建议过滤以保持报告纯净
unified_messages = cleaner.clean_messages(
raw_messages, bot_self_ids=bot_self_ids, filter_commands=True
)
# 4. 检查最小消息阈值 (在清理后进行)
threshold = self.config_manager.get_min_messages_threshold()
if len(unified_messages) < threshold and not manual:
async with self.group_lock(group_id, "daily"):
logger.info(
f"{group_id} 有效消息数 ({len(unified_messages)}) 未达到自动分析阈值 ({threshold})"
)
return {"success": False, "reason": "below_threshold"}
# 5. 基础统计 (Domain Service)
statistics = await asyncio.to_thread(
self.statistics_service.calculate_group_statistics, unified_messages
)
# 4. 用户分析 (Domain Service)
bot_self_ids = self.config_manager.get_bot_self_ids()
user_activity = await asyncio.to_thread(
self.analysis_domain_service.analyze_user_activity,
unified_messages,
bot_self_ids,
)
max_user_titles = self.config_manager.get_max_user_titles()
top_users = self.analysis_domain_service.get_top_users(
user_activity, limit=max_user_titles
)
# 5. LLM 语义分析 (为了保持兼容,目前直接传 UnifiedMessage,后续如需传 raw dict 再加转换)
# LLMAnalyzer 内部可能已经处理了转换(见之前代码)
topic_enabled = self.config_manager.get_topic_analysis_enabled()
user_title_enabled = self.config_manager.get_user_title_analysis_enabled()
golden_quote_enabled = self.config_manager.get_golden_quote_analysis_enabled()
topics = []
user_titles = []
golden_quotes = []
total_token_usage = TokenUsage()
# Note: LLMAnalyzer 目前可能只接收 legacy 格式或特定的 UnifiedMessage 适配
# 暂时转换回 legacy 格式以确保稳定性,直到 LLMAnalyzer 被重构
legacy_messages = self.statistics_service._convert_to_legacy_dict(
unified_messages
)
unified_msg_origin = (
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
)
if topic_enabled or user_title_enabled or golden_quote_enabled:
(
topics,
user_titles,
golden_quotes,
total_token_usage,
) = await self.llm_analyzer.analyze_all_concurrent(
legacy_messages,
user_activity,
umo=unified_msg_origin,
top_users=top_users,
topic_enabled=topic_enabled,
user_title_enabled=user_title_enabled,
golden_quote_enabled=golden_quote_enabled,
f"开始执行分析用例: {group_id}, platform_id={platform_id or '默认'}"
)
# 回填结果
statistics.golden_quotes = golden_quotes
statistics.token_usage = total_token_usage
# 1. 获取适配器
adapter = self.bot_manager.get_adapter(platform_id)
if not adapter:
raise ValueError(f"未找到平台 {platform_id} 的适配器")
analysis_result = {
"statistics": statistics,
"topics": topics,
"user_titles": user_titles,
"user_analysis": user_activity,
}
# 2. 拉取消息
days = self.config_manager.get_analysis_days()
max_count = self.config_manager.get_max_messages()
# 6. 持久化摘要 (Persistence)
await self.history_manager.save_analysis(group_id, analysis_result)
raw_messages = await adapter.fetch_messages(
group_id=group_id, days=days, max_count=max_count
)
# 7. 生成报告并发送 (应用层编排发送动作)
# 这里由调用方处理发送,本服务只返回分析结果和可能的视觉产物
return {
"success": True,
"analysis_result": analysis_result,
"messages_count": len(unified_messages),
"adapter": adapter,
}
if not raw_messages:
logger.warning(f"{group_id} 在最近 {days} 天内无消息或无法获取")
return {"success": False, "reason": "no_messages"}
# 3. 清理消息 (Filter commands, bot messages, noise)
from ...domain.services.message_cleaner_service import MessageCleanerService
cleaner = MessageCleanerService()
bot_self_ids = self.config_manager.get_bot_self_ids()
# 对于自动任务,强制过滤指令;对于手动任务,也建议过滤以保持报告纯净
unified_messages = cleaner.clean_messages(
raw_messages, bot_self_ids=bot_self_ids, filter_commands=True
)
# 4. 检查最小消息阈值 (在清理后进行)
threshold = self.config_manager.get_min_messages_threshold()
if len(unified_messages) < threshold and not manual:
logger.info(
f"{group_id} 有效消息数 ({len(unified_messages)}) 未达到自动分析阈值 ({threshold})"
)
return {"success": False, "reason": "below_threshold"}
# 5. 基础统计 (Domain Service)
statistics = await asyncio.to_thread(
self.statistics_service.calculate_group_statistics, unified_messages
)
# 4. 用户分析 (Domain Service)
bot_self_ids = self.config_manager.get_bot_self_ids()
user_activity = await asyncio.to_thread(
self.analysis_domain_service.analyze_user_activity,
unified_messages,
bot_self_ids,
)
max_user_titles = self.config_manager.get_max_user_titles()
top_users = self.analysis_domain_service.get_top_users(
user_activity, limit=max_user_titles
)
# 5. LLM 语义分析 (为了保持兼容,目前直接传 UnifiedMessage,后续如需传 raw dict 再加转换)
# LLMAnalyzer 内部可能已经处理了转换(见之前代码)
topic_enabled = self.config_manager.get_topic_analysis_enabled()
user_title_enabled = self.config_manager.get_user_title_analysis_enabled()
golden_quote_enabled = (
self.config_manager.get_golden_quote_analysis_enabled()
)
topics = []
user_titles = []
golden_quotes = []
total_token_usage = TokenUsage()
# Note: LLMAnalyzer 目前可能只接收 legacy 格式或特定的 UnifiedMessage 适配
# 暂时转换回 legacy 格式以确保稳定性,直到 LLMAnalyzer 被重构
legacy_messages = self.statistics_service._convert_to_legacy_dict(
unified_messages
)
unified_msg_origin = (
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
)
if topic_enabled or user_title_enabled or golden_quote_enabled:
async with self.llm_semaphore:
logger.debug(f"[LLM] 已进入分析队列 (群: {group_id})")
(
topics,
user_titles,
golden_quotes,
total_token_usage,
) = await self.llm_analyzer.analyze_all_concurrent(
legacy_messages,
user_activity,
umo=unified_msg_origin,
top_users=top_users,
topic_enabled=topic_enabled,
user_title_enabled=user_title_enabled,
golden_quote_enabled=golden_quote_enabled,
)
# 回填结果
statistics.golden_quotes = golden_quotes
statistics.token_usage = total_token_usage
analysis_result = {
"statistics": statistics,
"topics": topics,
"user_titles": user_titles,
"user_analysis": user_activity,
}
# 6. 持久化摘要 (Persistence)
await self.history_manager.save_analysis(group_id, analysis_result)
# 7. 生成报告并发送 (应用层编排发送动作)
# 这里由调用方处理发送,本服务只返回分析结果和可能的视觉产物
return {
"success": True,
"analysis_result": analysis_result,
"messages_count": len(unified_messages),
"adapter": adapter,
}
# ----------------------------------------------------------------
# 增量分析用例
@@ -212,188 +253,195 @@ class AnalysisApplicationService:
Returns:
dict: 包含 success、batch_summary 等信息
"""
if not self.incremental_store:
raise RuntimeError("增量分析未初始化:缺少 IncrementalStore")
async with self.group_lock(group_id, "incremental"):
if not self.incremental_store:
raise RuntimeError("增量分析未初始化:缺少 IncrementalStore")
logger.info(f"开始增量分析用例: 群 {group_id}, 平台 {platform_id or '默认'}")
logger.info(
f"开始增量分析用例: 群 {group_id}, 平台 {platform_id or '默认'}"
)
# 1. 获取适配器
adapter = self.bot_manager.get_adapter(platform_id)
if not adapter:
raise ValueError(f"未找到平台 {platform_id} 的适配器")
# 1. 获取适配器
adapter = self.bot_manager.get_adapter(platform_id)
if not adapter:
raise ValueError(f"未找到平台 {platform_id} 的适配器")
# 2. 拉取消息(使用增量配置的消息数量上限)
days = self.config_manager.get_analysis_days()
max_count = self.config_manager.get_incremental_max_messages()
# 2. 拉取消息(使用增量配置的消息数量上限)
days = self.config_manager.get_analysis_days()
max_count = self.config_manager.get_incremental_max_messages()
raw_messages = await adapter.fetch_messages(
group_id=group_id, days=days, max_count=max_count
)
raw_messages = await adapter.fetch_messages(
group_id=group_id, days=days, max_count=max_count
)
if not raw_messages:
logger.warning(f"{group_id} 增量分析:无法获取消息")
return {"success": False, "reason": "no_messages"}
if not raw_messages:
logger.warning(f"{group_id} 增量分析:无法获取消息")
return {"success": False, "reason": "no_messages"}
# 3. 清理消息
from ...domain.services.message_cleaner_service import MessageCleanerService
# 3. 清理消息
from ...domain.services.message_cleaner_service import MessageCleanerService
cleaner = MessageCleanerService()
bot_self_ids = self.config_manager.get_bot_self_ids()
unified_messages = cleaner.clean_messages(
raw_messages, bot_self_ids=bot_self_ids, filter_commands=True
)
cleaner = MessageCleanerService()
bot_self_ids = self.config_manager.get_bot_self_ids()
unified_messages = cleaner.clean_messages(
raw_messages, bot_self_ids=bot_self_ids, filter_commands=True
)
# 4. 按时间戳去重:获取最后分析消息时间戳
last_analyzed_ts = await self.incremental_store.get_last_analyzed_timestamp(
group_id
)
# 4. 按时间戳去重:获取最后分析消息时间戳
last_analyzed_ts = await self.incremental_store.get_last_analyzed_timestamp(
group_id
)
if last_analyzed_ts > 0:
unified_messages = [
msg for msg in unified_messages if msg.timestamp > last_analyzed_ts
if last_analyzed_ts > 0:
unified_messages = [
msg for msg in unified_messages if msg.timestamp > last_analyzed_ts
]
# 5. 检查最小消息阈值
min_messages = self.config_manager.get_incremental_min_messages()
if len(unified_messages) < min_messages:
logger.info(
f"{group_id} 增量分析:新消息数 ({len(unified_messages)}) "
f"未达到阈值 ({min_messages}),跳过本次分析"
)
return {"success": False, "reason": "below_threshold"}
# 6. 计算基础统计
statistics = await asyncio.to_thread(
self.statistics_service.calculate_group_statistics, unified_messages
)
user_activity = await asyncio.to_thread(
self.analysis_domain_service.analyze_user_activity,
unified_messages,
bot_self_ids,
)
# 计算本批次的小时分布
hourly_msg_counts, hourly_char_counts = self._compute_hourly_counts(
unified_messages
)
# 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()
)
# 需要将 UnifiedMessage 转换为 legacy 格式供 LLM 分析器使用
legacy_messages = self.statistics_service._convert_to_legacy_dict(
unified_messages
)
unified_msg_origin = (
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
)
async with self.llm_semaphore:
logger.debug(f"[LLM] 已进入增量分析队列 (群: {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,
)
# 8. 构建 IncrementalBatch
# 8a. 转换话题: SummaryTopic -> dict
new_topics = [
{
"topic": t.topic,
"contributors": t.contributors,
"detail": t.detail,
"contributor_ids": t.contributor_ids,
}
for t in topics
]
# 5. 检查最小消息阈值
min_messages = self.config_manager.get_incremental_min_messages()
if len(unified_messages) < min_messages:
logger.info(
f"{group_id} 增量分析:新消息数 ({len(unified_messages)}) "
f"未达到阈值 ({min_messages}),跳过本次分析"
# 8b. 转换金句: GoldenQuote -> dict
new_quotes = [
{
"content": q.content,
"sender": q.sender,
"reason": q.reason,
"user_id": q.user_id,
}
for q in golden_quotes
]
# 8c. 转换 token 消耗: TokenUsage -> dict
token_usage_dict = {
"prompt_tokens": token_usage.prompt_tokens,
"completion_tokens": token_usage.completion_tokens,
"total_tokens": token_usage.total_tokens,
}
# 8d. 转换用户统计: AnalysisDomainService 格式 -> IncrementalBatch 格式
user_stats = self._convert_user_activity_for_merge(
user_activity, unified_messages
)
return {"success": False, "reason": "below_threshold"}
# 6. 计算基础统计
statistics = await asyncio.to_thread(
self.statistics_service.calculate_group_statistics, unified_messages
)
user_activity = await asyncio.to_thread(
self.analysis_domain_service.analyze_user_activity,
unified_messages,
bot_self_ids,
)
# 计算本批次的小时分布
hourly_msg_counts, hourly_char_counts = self._compute_hourly_counts(
unified_messages
)
# 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()
# 需要将 UnifiedMessage 转换为 legacy 格式供 LLM 分析器使用
legacy_messages = self.statistics_service._convert_to_legacy_dict(
unified_messages
)
unified_msg_origin = (
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,
)
# 8. 构建 IncrementalBatch
# 8a. 转换话题: SummaryTopic -> dict
new_topics = [
{
"topic": t.topic,
"contributors": t.contributors,
"detail": t.detail,
"contributor_ids": t.contributor_ids,
# 8e. 转换表情统计: EmojiStatistics -> dict
emoji_stats = {
"face_count": statistics.emoji_statistics.face_count,
"mface_count": statistics.emoji_statistics.mface_count,
"bface_count": statistics.emoji_statistics.bface_count,
"sface_count": statistics.emoji_statistics.sface_count,
"other_emoji_count": statistics.emoji_statistics.other_emoji_count,
"face_details": statistics.emoji_statistics.face_details,
}
for t in topics
]
# 8b. 转换金句: GoldenQuote -> dict
new_quotes = [
{
"content": q.content,
"sender": q.sender,
"reason": q.reason,
"user_id": q.user_id,
# 8f. 获取参与者 ID 和最后消息时间戳
participant_ids = list({msg.sender_id for msg in unified_messages})
last_message_timestamp = max(
(msg.timestamp for msg in unified_messages), default=0
)
# 8g. 计算本批次总字符数
characters_count = sum(msg.get_text_length() for msg in unified_messages)
# 构建批次对象
batch = IncrementalBatch(
group_id=group_id,
timestamp=time_mod.time(),
messages_count=len(unified_messages),
characters_count=characters_count,
hourly_msg_counts={str(k): v for k, v in hourly_msg_counts.items()},
hourly_char_counts={str(k): v for k, v in hourly_char_counts.items()},
user_stats=user_stats,
emoji_stats=emoji_stats,
topics=new_topics,
golden_quotes=new_quotes,
token_usage=token_usage_dict,
last_message_timestamp=last_message_timestamp,
participant_ids=participant_ids,
)
# 9. 保存批次并更新最后分析时间戳
await self.incremental_store.save_batch(batch)
await self.incremental_store.update_last_analyzed_timestamp(
group_id, last_message_timestamp
)
logger.info(
f"{group_id} 增量分析完成: "
f"本批次消息={len(unified_messages)}, "
f"新话题={len(new_topics)}, 新金句={len(new_quotes)}"
)
return {
"success": True,
"batch_summary": batch.get_summary(),
"messages_count": len(unified_messages),
}
for q in golden_quotes
]
# 8c. 转换 token 消耗: TokenUsage -> dict
token_usage_dict = {
"prompt_tokens": token_usage.prompt_tokens,
"completion_tokens": token_usage.completion_tokens,
"total_tokens": token_usage.total_tokens,
}
# 8d. 转换用户统计: AnalysisDomainService 格式 -> IncrementalBatch 格式
user_stats = self._convert_user_activity_for_merge(
user_activity, unified_messages
)
# 8e. 转换表情统计: EmojiStatistics -> dict
emoji_stats = {
"face_count": statistics.emoji_statistics.face_count,
"mface_count": statistics.emoji_statistics.mface_count,
"bface_count": statistics.emoji_statistics.bface_count,
"sface_count": statistics.emoji_statistics.sface_count,
"other_emoji_count": statistics.emoji_statistics.other_emoji_count,
"face_details": statistics.emoji_statistics.face_details,
}
# 8f. 获取参与者 ID 和最后消息时间戳
participant_ids = list({msg.sender_id for msg in unified_messages})
last_message_timestamp = max(
(msg.timestamp for msg in unified_messages), default=0
)
# 8g. 计算本批次总字符数
characters_count = sum(msg.get_text_length() for msg in unified_messages)
# 构建批次对象
batch = IncrementalBatch(
group_id=group_id,
timestamp=time_mod.time(),
messages_count=len(unified_messages),
characters_count=characters_count,
hourly_msg_counts={str(k): v for k, v in hourly_msg_counts.items()},
hourly_char_counts={str(k): v for k, v in hourly_char_counts.items()},
user_stats=user_stats,
emoji_stats=emoji_stats,
topics=new_topics,
golden_quotes=new_quotes,
token_usage=token_usage_dict,
last_message_timestamp=last_message_timestamp,
participant_ids=participant_ids,
)
# 9. 保存批次并更新最后分析时间戳
await self.incremental_store.save_batch(batch)
await self.incremental_store.update_last_analyzed_timestamp(
group_id, last_message_timestamp
)
logger.info(
f"{group_id} 增量分析完成: "
f"本批次消息={len(unified_messages)}, "
f"新话题={len(new_topics)}, 新金句={len(new_quotes)}"
)
return {
"success": True,
"batch_summary": batch.get_summary(),
"messages_count": len(unified_messages),
}
async def execute_incremental_final_report(
self, group_id: str, platform_id: str | None = None
@@ -422,101 +470,110 @@ class AnalysisApplicationService:
Returns:
dict: 包含 success、analysis_result、adapter 等信息
"""
if not self.incremental_store or not self.incremental_merge_service:
raise RuntimeError(
"增量分析未初始化:缺少 IncrementalStore 或 IncrementalMergeService"
async with self.group_lock(group_id, "final"):
if not self.incremental_store or not self.incremental_merge_service:
raise RuntimeError(
"增量分析未初始化:缺少 IncrementalStore 或 IncrementalMergeService"
)
logger.info(
f"开始增量最终报告: 群 {group_id}, 平台 {platform_id or '默认'}"
)
logger.info(f"开始增量最终报告: 群 {group_id}, 平台 {platform_id or '默认'}")
# 1. 计算滑动窗口范围
analysis_days = self.config_manager.get_analysis_days()
window_end = time_mod.time()
window_start = window_end - (analysis_days * 24 * 3600)
# 1. 计算滑动窗口范围
analysis_days = self.config_manager.get_analysis_days()
window_end = time_mod.time()
window_start = window_end - (analysis_days * 24 * 3600)
# 2. 查询窗口内的所有批次
batches = await self.incremental_store.query_batches(
group_id, window_start, window_end
)
# 3. 检查批次有效性
if not batches:
logger.warning(f"{group_id} 滑动窗口内无增量分析数据,无法生成最终报告")
return {"success": False, "reason": "no_incremental_data"}
# 4. 合并批次为 IncrementalState
state = self.incremental_merge_service.merge_batches(
batches, window_start, window_end
)
# 5. 获取适配器(报告发送需要)
adapter = self.bot_manager.get_adapter(platform_id)
if not adapter:
raise ValueError(f"未找到平台 {platform_id} 的适配器")
# 6. 执行用户称号 LLM 分析
user_titles = []
user_title_enabled = self.config_manager.get_user_title_analysis_enabled()
if user_title_enabled and state.user_activities:
max_user_titles = self.config_manager.get_max_user_titles()
# 从合并后的 user_activities 中取出 top 用户
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
# 2. 查询窗口内的所有批次
batches = await self.incremental_store.query_batches(
group_id, window_start, window_end
)
try:
(
user_titles_result,
title_token_usage,
) = await self.llm_analyzer.analyze_user_titles(
messages=[], # 增量模式下不传原始消息
user_activity=state.user_activities,
umo=unified_msg_origin,
top_users=top_users,
# 3. 检查批次有效性
if not batches:
logger.warning(
f"{group_id} 滑动窗口内无增量分析数据,无法生成最终报告"
)
user_titles = user_titles_result
return {"success": False, "reason": "no_incremental_data"}
# 将称号分析的 token 消耗追加到状态中
state.total_token_usage["prompt_tokens"] = (
state.total_token_usage.get("prompt_tokens", 0)
+ title_token_usage.prompt_tokens
# 4. 合并批次为 IncrementalState
state = self.incremental_merge_service.merge_batches(
batches, window_start, window_end
)
# 5. 获取适配器(报告发送需要)
adapter = self.bot_manager.get_adapter(platform_id)
if not adapter:
raise ValueError(f"未找到平台 {platform_id} 的适配器")
# 6. 执行用户称号 LLM 分析
user_titles = []
user_title_enabled = self.config_manager.get_user_title_analysis_enabled()
if user_title_enabled and state.user_activities:
max_user_titles = self.config_manager.get_max_user_titles()
# 从合并后的 user_activities 中取出 top 用户
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
)
state.total_token_usage["completion_tokens"] = (
state.total_token_usage.get("completion_tokens", 0)
+ title_token_usage.completion_tokens
)
state.total_token_usage["total_tokens"] = (
state.total_token_usage.get("total_tokens", 0)
+ title_token_usage.total_tokens
)
except Exception as e:
logger.error(f"增量最终报告用户称号分析失败: {e}", exc_info=True)
# 7. 构建 analysis_result
analysis_result = self.incremental_merge_service.build_analysis_result(
state, user_titles
)
try:
async with self.llm_semaphore:
logger.debug(f"[LLM] 已进入称号分析队列 (群: {group_id})")
(
user_titles_result,
title_token_usage,
) = await self.llm_analyzer.analyze_user_titles(
messages=[], # 增量模式下不传原始消息
user_activity=state.user_activities,
umo=unified_msg_origin,
top_users=top_users,
)
user_titles = user_titles_result
# 8. 持久化到 history_manager
await self.history_manager.save_analysis(group_id, analysis_result)
# 将称号分析的 token 消耗追加到状态中
state.total_token_usage["prompt_tokens"] = (
state.total_token_usage.get("prompt_tokens", 0)
+ title_token_usage.prompt_tokens
)
state.total_token_usage["completion_tokens"] = (
state.total_token_usage.get("completion_tokens", 0)
+ title_token_usage.completion_tokens
)
state.total_token_usage["total_tokens"] = (
state.total_token_usage.get("total_tokens", 0)
+ title_token_usage.total_tokens
)
except Exception as e:
logger.error(f"增量最终报告用户称号分析失败: {e}", exc_info=True)
logger.info(
f"{group_id} 增量最终报告完成: "
f"窗口={state.get_window_date_str()}, "
f"累计消息={state.total_message_count}, "
f"话题={len(state.topics)}, 金句={len(state.golden_quotes)}, "
f"批次={state.total_analysis_count}"
)
# 7. 构建 analysis_result
analysis_result = self.incremental_merge_service.build_analysis_result(
state, user_titles
)
return {
"success": True,
"analysis_result": analysis_result,
"messages_count": state.total_message_count,
"adapter": adapter,
}
# 8. 持久化到 history_manager
await self.history_manager.save_analysis(group_id, analysis_result)
logger.info(
f"{group_id} 增量最终报告完成: "
f"窗口={state.get_window_date_str()}, "
f"累计消息={state.total_message_count}, "
f"话题={len(state.topics)}, 金句={len(state.golden_quotes)}, "
f"批次={state.total_analysis_count}"
)
return {
"success": True,
"analysis_result": analysis_result,
"messages_count": state.total_message_count,
"adapter": adapter,
}
# ----------------------------------------------------------------
# 辅助方法