优化一下总结生成。加入重试机制

This commit is contained in:
liuwei
2025-09-22 10:43:25 +08:00
parent 6eb6b70591
commit 31c9fc64ad
2 changed files with 147 additions and 33 deletions

View File

@@ -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

View File

@@ -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():