diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py index f22adab..4e2896a 100644 --- a/plugins/message_summary/main.py +++ b/plugins/message_summary/main.py @@ -158,14 +158,29 @@ class MessageSummaryPlugin(MessagePluginInterface): summary, image_path = await self._generate_summary(chat_content, group_name) if image_path: + # 图片生成成功,发送图片 await self.bot.send_image_message(group_id, Path(image_path)) + self.LOG.info(f"成功发送图片总结到群 {group_id}") return True else: - - client_msg_id, create_time, new_msg_id = await self.bot.send_text_message(group_id, - "❌ 生成总结失败,请稍后再试!") - self.revoke.add_message_to_revoke(group_id, client_msg_id, create_time, new_msg_id, 5) - return False + # 图片生成失败,发送文本消息 + if summary and len(summary.strip()) > 0: + # 截断过长的文本 + max_length = 2000 + if len(summary) > max_length: + summary = summary[:max_length] + "\n\n... (内容过长,已截断)" + + client_msg_id, create_time, new_msg_id = await self.bot.send_text_message(group_id, summary) + self.revoke.add_message_to_revoke(group_id, client_msg_id, create_time, new_msg_id, 30) + self.LOG.info(f"图片生成失败,已发送文本总结到群 {group_id}") + return True + else: + # 连文本内容都没有 + client_msg_id, create_time, new_msg_id = await self.bot.send_text_message(group_id, + "❌ 生成总结失败,请稍后再试!") + self.revoke.add_message_to_revoke(group_id, client_msg_id, create_time, new_msg_id, 5) + return False + except Exception as e: self.LOG.error(f"异步生成总结失败: {e}") client_msg_id, create_time, new_msg_id = await self.bot.send_text_message(group_id, @@ -243,11 +258,22 @@ class MessageSummaryPlugin(MessagePluginInterface): timestamp = int(time.time()) output_path = f"summary_{timestamp}.png" # 构建完整的输出路径 + self.LOG.info(f"开始生成图片: {output_path}") spath = await convert_md_str_to_image(answer, output_path) self.LOG.info(f"成功生成图片: {spath}") except Exception as e: - self.LOG.error(f"生成image失败:{e}", exc_info=True) - spath = None + self.LOG.error(f"生成图片失败: {e}", exc_info=True) + # 如果图片生成失败,尝试发送纯文本消息 + try: + # 截断过长的文本,避免消息太长 + max_length = 2000 + if len(answer) > max_length: + answer = answer[:max_length] + "\n\n... (内容过长,已截断)" + self.LOG.info("图片生成失败,将发送文本消息作为备选方案") + spath = None # 设置为None,让调用方知道需要发送文本 + except Exception as fallback_error: + self.LOG.error(f"备选文本发送也失败: {fallback_error}") + spath = None # 返回文本内容和图片路径 return answer, spath diff --git a/utils/markdown_to_image.py b/utils/markdown_to_image.py index a6f2ce6..c20aae0 100644 --- a/utils/markdown_to_image.py +++ b/utils/markdown_to_image.py @@ -198,6 +198,14 @@ async def html_to_image(html_file, output_image): """ 使用 Playwright 加载 HTML 文件并截图(异步)。 """ + # 验证输入文件是否存在 + if not os.path.exists(html_file): + raise FileNotFoundError(f"HTML文件不存在: {html_file}") + + # 验证输入文件是否可读 + if not os.access(html_file, os.R_OK): + raise PermissionError(f"HTML文件不可读: {html_file}") + try: async with async_playwright() as p: browser_path = None @@ -241,34 +249,66 @@ async def html_to_image(html_file, output_image): browser = await p.chromium.launch(executable_path=browser_path) # 业务逻辑不变 + page = None try: page = await browser.new_page() - # 增加超时时间到60秒或更长 - await page.goto(f'file://{os.path.abspath(html_file)}', timeout=60000) + + # 设置更长的超时时间,并添加更好的错误处理 + file_url = f'file://{os.path.abspath(html_file)}' + logger.debug(f"正在加载文件: {file_url}") + + # 使用更长的超时时间和更宽松的等待条件 + await page.goto(file_url, timeout=120000, wait_until='domcontentloaded') + + # 等待页面完全加载 + await page.wait_for_timeout(2000) + + # 设置视口大小 await page.set_viewport_size({"width": 750, "height": 800}) - await page.wait_for_timeout(500) + + # 再次等待确保渲染完成 + await page.wait_for_timeout(1000) + + # 截图 await page.screenshot(path=output_image, full_page=True) + + # 验证图片文件是否成功生成 + if not os.path.exists(output_image): + raise RuntimeError(f"截图失败,输出文件不存在: {output_image}") + + logger.debug(f"截图成功生成: {output_image}") + except Exception as e: - logger.exception(f"截图失败: {e}") + logger.error(f"截图过程中发生错误: {e}") + # 如果截图失败,确保删除可能的不完整文件 + if os.path.exists(output_image): + try: + os.remove(output_image) + logger.debug(f"已删除不完整的截图文件: {output_image}") + except Exception as cleanup_error: + logger.warning(f"清理不完整文件失败: {cleanup_error}") + raise finally: - await page.close() + if page: + await page.close() await browser.close() except Exception as e: - logger.debug(f"浏览器操作失败: {e}") + logger.error(f"浏览器操作失败: {e}") if "Executable doesn't exist" in str(e): - logger.debug("请运行 'playwright install' 命令安装必要的浏览器组件") + logger.error("请运行 'playwright install' 命令安装必要的浏览器组件") raise # 主函数:从字符串转换 Markdown 到图片(异步版) -async def convert_md_str_to_image(md_content: str, output_image: str) -> str: +async def convert_md_str_to_image(md_content: str, output_image: str, max_retries: int = 3) -> str: """ 将 Markdown 字符串转换为图片(异步)。 Args: md_content (str): Markdown 内容字符串 output_image (str): 输出图片的文件名(不含路径) + max_retries (int): 最大重试次数,默认3次 Returns: str: 生成的图片文件的绝对路径 @@ -276,6 +316,7 @@ async def convert_md_str_to_image(md_content: str, output_image: str) -> str: Raises: FileNotFoundError: 如果临时目录无法创建或访问 ValueError: 如果 md_content 为空 + RuntimeError: 如果重试次数耗尽后仍然失败 """ # 验证输入 if not md_content: @@ -302,27 +343,74 @@ async def convert_md_str_to_image(md_content: str, output_image: str) -> str: # 确保输出图片路径的父目录存在 output_image_path.parent.mkdir(parents=True, exist_ok=True) - try: - # 将 Markdown 转换为 HTML - await md_str_to_html(md_content, str(temp_html_path)) - - # 添加更长的等待时间确保文件系统同步 - await asyncio.sleep(1.0) - - # 检查文件是否存在和可读 - if not os.path.exists(str(temp_html_path)): - logger.error(f"HTML文件不存在: {temp_html_path}") - raise FileNotFoundError(f"HTML文件不存在: {temp_html_path}") + last_error = None + + for attempt in range(max_retries): + try: + logger.debug(f"尝试第 {attempt + 1}/{max_retries} 次生成图片") - # 将 HTML 转换为图片 - await html_to_image(str(temp_html_path), str(output_image_path)) + # 清理之前的临时文件(如果存在) + if temp_html_path.exists(): + os.remove(str(temp_html_path)) + if output_image_path.exists(): + os.remove(str(output_image_path)) + + # 将 Markdown 转换为 HTML + await md_str_to_html(md_content, str(temp_html_path)) - logger.debug(f"图片已生成:{output_image_path}") - return str(output_image_path.resolve()) + # 添加更长的等待时间确保文件系统同步 + await asyncio.sleep(1.0) + + # 检查文件是否存在和可读 + if not os.path.exists(str(temp_html_path)): + raise FileNotFoundError(f"HTML文件不存在: {temp_html_path}") + + # 验证HTML文件内容 + with open(str(temp_html_path), 'r', encoding='utf-8') as f: + html_content = f.read() + if len(html_content) < 100: # HTML文件太短,可能有问题 + raise ValueError(f"HTML文件内容异常,长度仅为: {len(html_content)}") + + logger.debug(f"HTML文件验证通过,大小: {len(html_content)} 字符") + + # 将 HTML 转换为图片 + await html_to_image(str(temp_html_path), str(output_image_path)) - except Exception as e: - logger.error(f"Error converting markdown to image: {e}") - raise + # 验证生成的图片文件 + if not os.path.exists(str(output_image_path)): + raise RuntimeError(f"图片文件生成失败,文件不存在: {output_image_path}") + + # 检查图片文件大小 + image_size = os.path.getsize(str(output_image_path)) + if image_size < 1024: # 小于1KB的图片可能有问题 + raise RuntimeError(f"生成的图片文件异常,大小仅为: {image_size} bytes") + + logger.debug(f"图片已成功生成:{output_image_path},大小: {image_size} bytes") + return str(output_image_path.resolve()) + + except Exception as e: + last_error = e + logger.warning(f"第 {attempt + 1} 次尝试失败: {e}") + + # 清理失败的文件 + try: + if temp_html_path.exists(): + os.remove(str(temp_html_path)) + if output_image_path.exists(): + os.remove(str(output_image_path)) + except Exception as cleanup_error: + logger.warning(f"清理临时文件失败: {cleanup_error}") + + # 如果不是最后一次尝试,等待一段时间后重试 + if attempt < max_retries - 1: + wait_time = (attempt + 1) * 2 # 递增等待时间 + logger.debug(f"等待 {wait_time} 秒后重试...") + await asyncio.sleep(wait_time) + + # 所有重试都失败了 + logger.error(f"经过 {max_retries} 次尝试后仍然失败") + raise RuntimeError(f"图片生成失败,已重试 {max_retries} 次。最后错误: {last_error}") + # finally: # # 可选:清理临时 HTML 文件 # if temp_html_path.exists():