优化一下总结生成。加入重试机制
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user