diff --git a/src/analysis/analyzers/base_analyzer.py b/src/analysis/analyzers/base_analyzer.py index b42930e..621a79a 100644 --- a/src/analysis/analyzers/base_analyzer.py +++ b/src/analysis/analyzers/base_analyzer.py @@ -153,7 +153,7 @@ class BaseAnalyzer(ABC): return [], token_usage except Exception as e: - logger.error(f"{self.get_data_type()}分析失败: {e}") + logger.error(f"{self.get_data_type()}分析失败: {e}", exc_info=True) return [], TokenUsage() def get_max_tokens(self) -> int: diff --git a/src/analysis/analyzers/topic_analyzer.py b/src/analysis/analyzers/topic_analyzer.py index d8aa417..cf439ad 100644 --- a/src/analysis/analyzers/topic_analyzer.py +++ b/src/analysis/analyzers/topic_analyzer.py @@ -44,32 +44,49 @@ class TopicAnalyzer(BaseAnalyzer): Returns: 提示词字符串 """ + logger.debug(f"build_prompt 开始处理,输入消息数量: {len(messages) if messages else 0}") + logger.debug(f"输入消息类型: {type(messages)}") + + # 验证输入数据格式 + if not isinstance(messages, list): + logger.error(f"build_prompt 期望列表,但收到: {type(messages)}") + return "" + # 提取文本消息 text_messages = [] - for msg in messages: + for i, msg in enumerate(messages): + logger.debug(f"build_prompt 处理第 {i+1} 条消息,类型: {type(msg)}") + # 确保msg是字典类型,避免'str' object has no attribute 'get'错误 if not isinstance(msg, dict): + logger.warning(f"build_prompt 跳过非字典类型的消息: {type(msg)} - {msg}") continue - sender = msg.get("sender", {}) - nickname = sender.get("nickname", "") or sender.get("card", "") - msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M") - - for content in msg.get("message", []): - if content.get("type") == "text": - text = content.get("data", {}).get("text", "").strip() - if text and len(text) > 2 and not text.startswith("/"): - # 清理消息内容 - text = text.replace('“', '"').replace('”', '"') - text = text.replace('‘', "'").replace('’', "'") - text = text.replace('\n', ' ').replace('\r', ' ') - text = text.replace('\t', ' ') - text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text) - text_messages.append({ - "sender": nickname, - "time": msg_time, - "content": text.strip() - }) + try: + sender = msg.get("sender", {}) + nickname = sender.get("nickname", "") or sender.get("card", "") + msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M") + + for content in msg.get("message", []): + if content.get("type") == "text": + text = content.get("data", {}).get("text", "").strip() + if text and len(text) > 2 and not text.startswith("/"): + # 清理消息内容 + text = text.replace('“', '"').replace('”', '"') + text = text.replace('‘', "'").replace('’', "'") + text = text.replace('\n', ' ').replace('\r', ' ') + text = text.replace('\t', ' ') + text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text) + text_messages.append({ + "sender": nickname, + "time": msg_time, + "content": text.strip() + }) + except Exception as e: + logger.error(f"build_prompt 处理第 {i+1} 条消息时出错: {e}", exc_info=True) + continue + + logger.debug(f"build_prompt 提取到 {len(text_messages)} 条文本消息") if not text_messages: return "" @@ -147,43 +164,57 @@ class TopicAnalyzer(BaseAnalyzer): Returns: SummaryTopic对象列表 """ + logger.debug(f"create_data_objects 开始处理,输入数据数量: {len(topics_data) if topics_data else 0}") + logger.debug(f"输入数据类型: {type(topics_data)}") + try: topics = [] max_topics = self.get_max_count() - for topic_data in topics_data[:max_topics]: + logger.debug(f"处理前 {max_topics} 条话题数据") + + for i, topic_data in enumerate(topics_data[:max_topics]): + logger.debug(f"处理第 {i+1} 条话题数据,类型: {type(topic_data)}") + # 确保topic_data是字典类型,避免'str' object has no attribute 'get'错误 if not isinstance(topic_data, dict): logger.warning(f"跳过非字典类型的话题数据: {type(topic_data)} - {topic_data}") continue - # 确保数据格式正确 - topic_name = topic_data.get("topic", "").strip() - contributors = topic_data.get("contributors", []) - detail = topic_data.get("detail", "").strip() - - # 验证必要字段 - if not topic_name or not detail: - logger.warning(f"话题数据格式不完整,跳过: {topic_data}") + try: + # 确保数据格式正确 + topic_name = topic_data.get("topic", "").strip() + contributors = topic_data.get("contributors", []) + detail = topic_data.get("detail", "").strip() + + logger.debug(f"话题数据 - 名称: {topic_name}, 参与者: {contributors}, 详情: {detail[:50]}...") + + # 验证必要字段 + if not topic_name or not detail: + logger.warning(f"话题数据格式不完整,跳过: {topic_data}") + continue + + # 确保参与者列表有效 + if not contributors or not isinstance(contributors, list): + contributors = ["群友"] + else: + # 清理参与者名称 + contributors = [str(c).strip() for c in contributors if c and str(c).strip()] or ["群友"] + + topics.append(SummaryTopic( + topic=topic_name, + contributors=contributors[:5], # 最多5个参与者 + detail=detail + )) + except Exception as e: + logger.error(f"处理第 {i+1} 条话题数据时出错: {e}", exc_info=True) continue - - # 确保参与者列表有效 - if not contributors or not isinstance(contributors, list): - contributors = ["群友"] - else: - # 清理参与者名称 - contributors = [str(c).strip() for c in contributors if c and str(c).strip()] or ["群友"] - - topics.append(SummaryTopic( - topic=topic_name, - contributors=contributors[:5], # 最多5个参与者 - detail=detail - )) + logger.debug(f"create_data_objects 完成,创建了 {len(topics)} 个话题对象") return topics except Exception as e: - logger.error(f"创建话题对象失败: {e}") + logger.error(f"创建话题对象失败: {e}", exc_info=True) return [] def extract_text_messages(self, messages: List[Dict]) -> List[Dict]: @@ -196,32 +227,41 @@ class TopicAnalyzer(BaseAnalyzer): Returns: 提取的文本消息列表 """ + logger.debug(f"extract_text_messages 开始处理,输入消息数量: {len(messages) if messages else 0}") text_messages = [] - for msg in messages: + + for i, msg in enumerate(messages): + logger.debug(f"处理第 {i+1} 条消息,类型: {type(msg)}") # 确保msg是字典类型,避免'str' object has no attribute 'get'错误 if not isinstance(msg, dict): + logger.warning(f"跳过非字典类型的消息: {type(msg)} - {msg}") continue - sender = msg.get("sender", {}) - nickname = sender.get("nickname", "") or sender.get("card", "") - msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M") - - for content in msg.get("message", []): - if content.get("type") == "text": - text = content.get("data", {}).get("text", "").strip() - if text and len(text) > 2 and not text.startswith("/"): - # 清理消息内容 - text = text.replace('""', '"').replace('""', '"') - text = text.replace(''', "'").replace(''', "'") - text = text.replace('\n', ' ').replace('\r', ' ') - text = text.replace('\t', ' ') - text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text) - text_messages.append({ - "sender": nickname, - "time": msg_time, - "content": text.strip() - }) + try: + sender = msg.get("sender", {}) + nickname = sender.get("nickname", "") or sender.get("card", "") + msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M") + + for content in msg.get("message", []): + if content.get("type") == "text": + text = content.get("data", {}).get("text", "").strip() + if text and len(text) > 2 and not text.startswith("/"): + # 清理消息内容 + text = text.replace('""', '"').replace('""', '"') + text = text.replace(''', "'").replace(''', "'") + text = text.replace('\n', ' ').replace('\r', ' ') + text = text.replace('\t', ' ') + text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text) + text_messages.append({ + "sender": nickname, + "time": msg_time, + "content": text.strip() + }) + except Exception as e: + logger.error(f"处理第 {i+1} 条消息时出错: {e}", exc_info=True) + continue + logger.debug(f"extract_text_messages 完成,提取到 {len(text_messages)} 条文本消息") return text_messages async def analyze_topics(self, messages: List[Dict], umo: str = None) -> Tuple[List[SummaryTopic], TokenUsage]: @@ -236,16 +276,28 @@ class TopicAnalyzer(BaseAnalyzer): (话题列表, Token使用统计) """ try: + logger.debug(f"analyze_topics 开始处理,消息数量: {len(messages) if messages else 0}") + logger.debug(f"消息类型: {type(messages)}") + if messages: + logger.debug(f"第一条消息类型: {type(messages[0]) if messages else '无'}") + logger.debug(f"第一条消息内容: {messages[0] if messages else '无'}") + # 提取文本消息 text_messages = self.extract_text_messages(messages) + logger.debug(f"提取到 {len(text_messages)} 条文本消息") if not text_messages: logger.info("没有有效的文本消息,返回空结果") return [], TokenUsage() logger.info(f"开始分析 {len(text_messages)} 条文本消息中的话题") + logger.debug(f"文本消息类型: {type(text_messages)}") + if text_messages: + logger.debug(f"第一条文本消息类型: {type(text_messages[0])}") + logger.debug(f"第一条文本消息内容: {text_messages[0]}") + return await self.analyze(text_messages, umo) except Exception as e: - logger.error(f"话题分析失败: {e}") + logger.error(f"话题分析失败: {e}", exc_info=True) return [], TokenUsage() \ No newline at end of file diff --git a/src/reports/generators.py b/src/reports/generators.py index 0eb2f30..b70ad1c 100644 --- a/src/reports/generators.py +++ b/src/reports/generators.py @@ -298,14 +298,14 @@ class ReportGenerator: # 尝试启动浏览器,如果 Chromium 不存在会自动下载 logger.info("启动浏览器进行 PDF 转换") - # 配置浏览器启动参数,提高稳定性,避免意外关闭 + # 配置浏览器启动参数,解决Docker环境中的沙盒问题 launch_options = { 'headless': True, 'args': [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-gpu', + '--no-sandbox', # Docker环境必需 - 禁用沙盒 + '--disable-setuid-sandbox', # Docker环境必需 - 禁用setuid沙盒 + '--disable-dev-shm-usage', # 避免共享内存问题 + '--disable-gpu', # 禁用GPU加速 '--no-first-run', '--disable-extensions', '--disable-default-apps', @@ -328,18 +328,6 @@ class ReportGenerator: '--disable-web-security', '--disable-features=VizDisplayCompositor', '--disable-blink-features=AutomationControlled', # 隐藏自动化特征 - '--memory-pressure-off', # 禁用内存压力检测 - '--max_old_space_size=4096', # 限制内存使用 - '--disable-background-mode', # 禁用后台模式 - '--disable-ipc-flooding-protection', # 禁用IPC洪水保护 - '--disable-logging', # 禁用日志记录以减少资源使用 - '--disable-permissions-api', # 禁用权限API - '--disable-notifications', # 禁用通知 - '--disable-web-bluetooth', # 禁用蓝牙 - '--disable-web-usb', # 禁用USB - '--disable-webgl', # 禁用WebGL - '--disable-webgl2', # 禁用WebGL2 - '--disable-webrtc', # 禁用WebRTC ] } @@ -373,10 +361,8 @@ class ReportGenerator: ] # 查找可用的浏览器 - logger.info(f"正在检查 {len(chrome_paths)} 个可能的浏览器路径...") found_browser = False for chrome_path in chrome_paths: - logger.debug(f"检查浏览器路径: {chrome_path}") if Path(chrome_path).exists(): launch_options['executablePath'] = chrome_path logger.info(f"使用系统浏览器: {chrome_path}") @@ -384,67 +370,15 @@ class ReportGenerator: break if not found_browser: - logger.warning("未找到系统浏览器,将使用 pyppeteer 默认下载的 Chromium") + logger.info("未找到系统浏览器,将使用 pyppeteer 默认下载的 Chromium") - # 尝试启动浏览器,最多重试3次 - max_retries = 3 - browser = None - for attempt in range(max_retries): - try: - logger.info(f"尝试启动浏览器 (第 {attempt + 1} 次)") - # 添加更多内存友好的启动选项 - launch_options.update({ - 'dumpio': True, # 输出浏览器日志以便调试 - 'autoClose': False, # 防止自动关闭 - 'handleSIGINT': False, - 'handleSIGTERM': False, - 'handleSIGHUP': False - }) - browser = await launch(**launch_options) - logger.info("浏览器启动成功") - break - except Exception as e: - logger.warning(f"第 {attempt + 1} 次启动浏览器失败: {e}", exc_info=True) - if attempt < max_retries - 1: - await asyncio.sleep(3) # 增加等待时间到3秒 - # 尝试减少内存占用的启动选项 - launch_options['args'].extend([ - '--disable-images', - '--disable-javascript', - '--disable-plugins', - '--disable-webgl', - '--disable-threaded-animation', - '--disable-threaded-scrolling', - '--disable-sync', - '--disable-default-apps', - '--mute-audio', - '--no-zygote', - '--disable-gpu-sandbox', - '--disable-software-rasterizer', - '--disable-background-networking', - '--disable-background-timer-throttling', - '--disable-renderer-backgrounding', - '--disable-client-side-phishing-detection', - '--disable-component-extensions-with-background-pages', - '--disable-default-apps', - '--disable-extensions', - '--disable-features=TranslateUI', - '--disable-ipc-flooding-protection', - '--disable-background-mode', - '--disable-logging', - '--disable-permissions-api', - '--disable-web-bluetooth', - '--disable-web-usb', - '--disable-webrtc', - '--max_old_space_size=1024', # 进一步限制内存 - '--memory-pressure-off' - ]) - else: - logger.error(f"多次尝试后浏览器启动失败,无法生成 PDF, {e}", exc_info=True) - return False - - if not browser: - logger.error("浏览器启动失败,无法继续") + # 尝试启动浏览器 + try: + logger.info("正在启动浏览器...") + browser = await launch(**launch_options) + logger.info("浏览器启动成功") + except Exception as e: + logger.error(f"浏览器启动失败: {e}", exc_info=True) return False try: