diff --git a/main.py b/main.py index 4d539a3..a2ae68e 100644 --- a/main.py +++ b/main.py @@ -18,17 +18,17 @@ from .src.application.services.analysis_application_service import ( AnalysisApplicationService, ) from .src.domain.services.analysis_domain_service import AnalysisDomainService +from .src.domain.services.incremental_merge_service import IncrementalMergeService from .src.domain.services.statistics_service import StatisticsService from .src.infrastructure.analysis.llm_analyzer import LLMAnalyzer from .src.infrastructure.config.config_manager import ConfigManager from .src.infrastructure.persistence.history_manager import HistoryManager +from .src.infrastructure.persistence.incremental_store import IncrementalStore from .src.infrastructure.platform.bot_manager import BotManager from .src.infrastructure.reporting.generators import ReportGenerator from .src.infrastructure.scheduler.auto_scheduler import AutoScheduler from .src.infrastructure.scheduler.retry import RetryManager from .src.utils.pdf_utils import PDFInstaller -from .src.infrastructure.persistence.incremental_store import IncrementalStore -from .src.domain.services.incremental_merge_service import IncrementalMergeService class QQGroupDailyAnalysis(Star): diff --git a/src/domain/entities/incremental_state.py b/src/domain/entities/incremental_state.py index 87aabbf..70d5147 100644 --- a/src/domain/entities/incremental_state.py +++ b/src/domain/entities/incremental_state.py @@ -63,11 +63,13 @@ class IncrementalBatch: golden_quotes: list[dict] = field(default_factory=list) # Token 消耗 - token_usage: dict = field(default_factory=lambda: { - "prompt_tokens": 0, - "completion_tokens": 0, - "total_tokens": 0, - }) + token_usage: dict = field( + default_factory=lambda: { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + } + ) # 增量追踪 last_message_timestamp: int = 0 @@ -107,11 +109,14 @@ class IncrementalBatch: emoji_stats=data.get("emoji_stats", {}), topics=data.get("topics", []), golden_quotes=data.get("golden_quotes", []), - token_usage=data.get("token_usage", { - "prompt_tokens": 0, - "completion_tokens": 0, - "total_tokens": 0, - }), + token_usage=data.get( + "token_usage", + { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + }, + ), last_message_timestamp=data.get("last_message_timestamp", 0), participant_ids=data.get("participant_ids", []), ) @@ -179,11 +184,13 @@ class IncrementalState: total_message_count: int = 0 total_character_count: int = 0 total_analysis_count: int = 0 - total_token_usage: dict = field(default_factory=lambda: { - "prompt_tokens": 0, - "completion_tokens": 0, - "total_tokens": 0, - }) + total_token_usage: dict = field( + default_factory=lambda: { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + } + ) # 增量跟踪 last_analyzed_message_timestamp: int = 0 @@ -237,12 +244,14 @@ class IncrementalState: """ users = [] for user_id, data in self.user_activities.items(): - users.append({ - "user_id": user_id, - "name": data.get("name", user_id), - "message_count": data.get("message_count", 0), - "char_count": data.get("char_count", 0), - }) + users.append( + { + "user_id": user_id, + "name": data.get("name", user_id), + "message_count": data.get("message_count", 0), + "char_count": data.get("char_count", 0), + } + ) users.sort(key=lambda x: x["message_count"], reverse=True) return users[:top_n] diff --git a/src/domain/models/data_models.py b/src/domain/models/data_models.py index b9ddef1..761bcfe 100644 --- a/src/domain/models/data_models.py +++ b/src/domain/models/data_models.py @@ -13,7 +13,9 @@ class SummaryTopic: topic: str contributors: list[str] detail: str - contributor_ids: list[str] = field(default_factory=list) # 贡献者ID列表 (用于显示头像) + contributor_ids: list[str] = field( + default_factory=list + ) # 贡献者ID列表 (用于显示头像) @dataclass diff --git a/src/infrastructure/analysis/analyzers/topic_analyzer.py b/src/infrastructure/analysis/analyzers/topic_analyzer.py index 6eff13a..c5cf115 100644 --- a/src/infrastructure/analysis/analyzers/topic_analyzer.py +++ b/src/infrastructure/analysis/analyzers/topic_analyzer.py @@ -353,12 +353,14 @@ class TopicAnalyzer(BaseAnalyzer): # 后处理:contributors 此时包含的是 ID,需要映射回昵称 for topic in topics: raw_ids = topic.contributors # LLM 返回的是 ID 列表 - + # 填充 contributor_ids # 过滤掉非数字的脏数据 (LLM 偶尔会发疯) - valid_ids = [str(uid).strip() for uid in raw_ids if str(uid).strip().isdigit()] + valid_ids = [ + str(uid).strip() for uid in raw_ids if str(uid).strip().isdigit() + ] topic.contributor_ids = valid_ids - + # 映射回昵称用于显示 resolved_names = [] for uid in valid_ids: @@ -370,9 +372,9 @@ class TopicAnalyzer(BaseAnalyzer): if uid in bot_ids: name = "Bot" else: - name = uid # Fallback to ID + name = uid # Fallback to ID resolved_names.append(name) - + topic.contributors = resolved_names return topics, usage diff --git a/src/infrastructure/persistence/incremental_store.py b/src/infrastructure/persistence/incremental_store.py index b033c2a..e3a979a 100644 --- a/src/infrastructure/persistence/incremental_store.py +++ b/src/infrastructure/persistence/incremental_store.py @@ -13,7 +13,6 @@ KV 键设计: 值: int (epoch timestamp) """ -import time from typing import Any from ...domain.entities.incremental_state import IncrementalBatch @@ -131,10 +130,12 @@ class IncrementalStore: # 2. 更新索引 index = await self._get_index(group_id) - index.append({ - "batch_id": batch.batch_id, - "timestamp": batch.timestamp, - }) + index.append( + { + "batch_id": batch.batch_id, + "timestamp": batch.timestamp, + } + ) await self._save_index(group_id, index) logger.debug( @@ -173,7 +174,8 @@ class IncrementalStore: # 筛选在窗口范围内的批次 matching_entries = [ - entry for entry in index + entry + for entry in index if window_start <= entry.get("timestamp", 0) <= window_end ] @@ -247,22 +249,16 @@ class IncrementalStore: key = self._last_ts_key(group_id) try: await self.plugin.put_kv_data(key, timestamp) - logger.debug( - f"更新最后分析时间戳: 群 {group_id}, ts={timestamp}" - ) + logger.debug(f"更新最后分析时间戳: 群 {group_id}, ts={timestamp}") except Exception as e: - logger.error( - f"更新最后分析时间戳失败 (Key: {key}): {e}", exc_info=True - ) + logger.error(f"更新最后分析时间戳失败 (Key: {key}): {e}", exc_info=True) raise # ================================================================ # 过期批次清理 # ================================================================ - async def cleanup_old_batches( - self, group_id: str, before_timestamp: float - ) -> int: + async def cleanup_old_batches(self, group_id: str, before_timestamp: float) -> int: """ 清理指定群中早于给定时间戳的所有批次。 @@ -337,9 +333,7 @@ class IncrementalStore: index = await self._get_index(group_id) return len(index) - async def get_all_batch_summaries( - self, group_id: str - ) -> list[dict]: + async def get_all_batch_summaries(self, group_id: str) -> list[dict]: """ 获取指定群所有批次的摘要信息(不加载完整数据)。 @@ -355,4 +349,3 @@ class IncrementalStore: # 按时间戳升序排列 index.sort(key=lambda x: x.get("timestamp", 0)) return index - diff --git a/src/infrastructure/platform/adapters/discord_adapter.py b/src/infrastructure/platform/adapters/discord_adapter.py index 6c7e482..b20388a 100644 --- a/src/infrastructure/platform/adapters/discord_adapter.py +++ b/src/infrastructure/platform/adapters/discord_adapter.py @@ -399,8 +399,9 @@ class DiscordAdapter(PlatformAdapter): file_to_send = None if image_path.startswith("base64://"): # Base64 图片:解码 -> 内存 Object -> Discord - from io import BytesIO import base64 # Fix: Ensure base64 is imported + from io import BytesIO + try: base64_data = image_path.split("base64://")[1] image_bytes = base64.b64decode(base64_data) diff --git a/src/infrastructure/platform/adapters/telegram_adapter.py b/src/infrastructure/platform/adapters/telegram_adapter.py index e2ff631..8a6bc48 100644 --- a/src/infrastructure/platform/adapters/telegram_adapter.py +++ b/src/infrastructure/platform/adapters/telegram_adapter.py @@ -52,7 +52,7 @@ class TelegramAdapter(PlatformAdapter): def __init__(self, bot_instance: Any, config: dict | None = None): super().__init__(bot_instance, config) self._cached_client: ExtBot | None = None - self._context: "Context | None" = None + self._context: Context | None = None # 机器人自身 ID(用于消息过滤) self.bot_user_id = str(config.get("bot_user_id", "")) if config else "" @@ -610,15 +610,15 @@ class TelegramAdapter(PlatformAdapter): # 格式: https://api.telegram.org/file/bot/ # python-telegram-bot 的 File.file_path 属性通常只返回路径部分 # 需要手动拼接或使用 instance.file.file_path (取决于版本) - + file_path = file.file_path if file_path.startswith("http"): return file_path - + # 尝试构建完整 URL if hasattr(client, "token"): return f"https://api.telegram.org/file/bot{client.token}/{file_path}" - + # 如果无法获取 token,返回 None return None return None @@ -655,10 +655,10 @@ class TelegramAdapter(PlatformAdapter): file_path = file.file_path if file_path.startswith("http"): return file_path - + if hasattr(client, "token"): return f"https://api.telegram.org/file/bot{client.token}/{file_path}" - + return None return None except Exception as e: diff --git a/src/infrastructure/platform/factory.py b/src/infrastructure/platform/factory.py index d80438b..339e58f 100644 --- a/src/infrastructure/platform/factory.py +++ b/src/infrastructure/platform/factory.py @@ -91,4 +91,3 @@ def _register_adapters(): _register_adapters() - diff --git a/src/infrastructure/reporting/dispatcher.py b/src/infrastructure/reporting/dispatcher.py index 2cacecf..1be4294 100644 --- a/src/infrastructure/reporting/dispatcher.py +++ b/src/infrastructure/reporting/dispatcher.py @@ -79,7 +79,10 @@ class ReportDispatcher: return None image_url, html_content = await self.report_generator.generate_image_report( - analysis_result, group_id, self._html_render_func, avatar_getter=avatar_getter + analysis_result, + group_id, + self._html_render_func, + avatar_getter=avatar_getter, ) except Exception as e: logger.error(f"[{trace_id}] Failed to generate image report: {e}") diff --git a/src/infrastructure/scheduler/retry.py b/src/infrastructure/scheduler/retry.py index 75b804f..c86f17f 100644 --- a/src/infrastructure/scheduler/retry.py +++ b/src/infrastructure/scheduler/retry.py @@ -179,17 +179,26 @@ class RetryManager: # 本地文件路径 try: import os + if os.path.exists(image_data): with open(image_data, "rb") as f: image_data = f.read() - + # 校验文件头 (防御性编程,避免发送错误文本) - if not image_data.startswith(b"\xff\xd8") and not image_data.startswith(b"\x89PNG"): - if len(image_data) < 1024 and (b"Error" in image_data or b"Exception" in image_data): - logger.error(f"[RetryManager] 渲染器生成了错误文件而非图片: {image_data.decode('utf-8', errors='ignore')}") + if not image_data.startswith( + b"\xff\xd8" + ) and not image_data.startswith(b"\x89PNG"): + if len(image_data) < 1024 and ( + b"Error" in image_data or b"Exception" in image_data + ): + logger.error( + f"[RetryManager] 渲染器生成了错误文件而非图片: {image_data.decode('utf-8', errors='ignore')}" + ) return False else: - logger.error(f"[RetryManager] 渲染器返回的路径不存在: {image_data}") + logger.error( + f"[RetryManager] 渲染器返回的路径不存在: {image_data}" + ) image_data = None except Exception as e: logger.error(f"[RetryManager] 读取本地图片失败: {e}")