From 5aca5c5f2835bc51bd9391d3ef8685be86d9ac9e Mon Sep 17 00:00:00 2001 From: Liu Date: Fri, 1 May 2026 12:45:35 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=E4=BF=AE=E5=A4=8D=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E8=B6=85=E6=97=B6=E6=8B=96=E6=85=A2=E4=B8=BB?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E9=97=AE=E9=A2=98"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 1b6da6db1f10d049998bac5595d1ab707da03157. --- admin/dashboard/blueprints/plugin_routes.py | 1 - plugins/robot_menu/config.toml | 6 +----- plugins/robot_menu/main.py | 13 ------------ plugins/robot_menu/menu_render_tool.py | 19 +++-------------- utils/markdown_to_image.py | 23 +++++++-------------- 5 files changed, 11 insertions(+), 51 deletions(-) diff --git a/admin/dashboard/blueprints/plugin_routes.py b/admin/dashboard/blueprints/plugin_routes.py index a270c16..40d3dde 100644 --- a/admin/dashboard/blueprints/plugin_routes.py +++ b/admin/dashboard/blueprints/plugin_routes.py @@ -20,7 +20,6 @@ _command_catalog_tool = RobotMenuRenderTool( image_fallback_to_text=True, image_render_timeout_seconds=30, image_render_retries=1, - sync_send_timeout_seconds=10, image_template_path="plugins/robot_menu/templates/menu_cards.html", log=LOG, ) diff --git a/plugins/robot_menu/config.toml b/plugins/robot_menu/config.toml index 8301164..0e08ac3 100644 --- a/plugins/robot_menu/config.toml +++ b/plugins/robot_menu/config.toml @@ -8,11 +8,7 @@ output_mode = "image" # 图片生成失败时是否回退文本菜单: # - false:严格按图片模式,不发送完整菜单文本 # - true:优先保证可达,失败后改发文本 -image_fallback_to_text = true -# 菜单命令是即时交互,不允许长时间占住主消息链路: -# - 这里控制“同步等待图片发送完成”的最长时长; -# - 超过后会尽快回退文本或失败提示,避免把整个插件处理流程拖慢。 -sync_send_timeout_seconds = 18 +image_fallback_to_text = false # md2image 渲染参数:可按服务器性能调整 image_render_timeout_seconds = 45 image_render_retries = 1 diff --git a/plugins/robot_menu/main.py b/plugins/robot_menu/main.py index 66b4cfe..ee9a880 100644 --- a/plugins/robot_menu/main.py +++ b/plugins/robot_menu/main.py @@ -87,18 +87,6 @@ class RobotMenuPlugin(MessagePluginInterface): self.image_render_retries = int( self._config.get("RobotMenu", {}).get("image_render_retries", 1) ) - # 菜单命令属于强交互型消息: - # 1. 用户输入“菜单”后,不能允许单次渲染长期霸占消息处理协程; - # 2. 因此这里单独定义“主链路同步等待预算”,超出后立即由渲染工具降级; - # 3. 该预算默认比底层图片渲染超时短很多,优先保障机器人整体吞吐稳定。 - self.sync_send_timeout_seconds = int( - self._config.get("RobotMenu", {}).get("sync_send_timeout_seconds", 18) - ) - # 对外层插件保护显式声明一个更合适的总超时: - # 1. 内层菜单发送会在 sync_send_timeout_seconds 内决定“成功发图 / 回退文本 / 返回失败提示”; - # 2. 外层 wait_for 必须比内层稍长,给降级发送文本留出缓冲; - # 3. 这样可以避免过去“内外层都卡在 55 秒”时,外层先打断,导致降级逻辑来不及执行。 - self.plugin_process_timeout_seconds = max(12, self.sync_send_timeout_seconds + 8) # 菜单图片模板文件路径(相对仓库根目录): # 调整样式和布局时只改模板,不改 Python 逻辑。 self.image_template_path = str( @@ -113,7 +101,6 @@ class RobotMenuPlugin(MessagePluginInterface): image_fallback_to_text=self.image_fallback_to_text, image_render_timeout_seconds=self.image_render_timeout_seconds, image_render_retries=self.image_render_retries, - sync_send_timeout_seconds=self.sync_send_timeout_seconds, image_template_path=self.image_template_path, log=self.LOG, ) diff --git a/plugins/robot_menu/menu_render_tool.py b/plugins/robot_menu/menu_render_tool.py index 2e8bfab..b6286c6 100644 --- a/plugins/robot_menu/menu_render_tool.py +++ b/plugins/robot_menu/menu_render_tool.py @@ -30,7 +30,6 @@ class RobotMenuRenderTool: image_fallback_to_text: bool, image_render_timeout_seconds: int, image_render_retries: int, - sync_send_timeout_seconds: int, image_template_path: str, log=default_logger, ): @@ -41,11 +40,6 @@ class RobotMenuRenderTool: # 渲染超时与重试参数,统一集中在工具层处理。 self.image_render_timeout_seconds = int(image_render_timeout_seconds) self.image_render_retries = int(image_render_retries) - # 同步发送超时预算: - # 1. “菜单”属于即时交互命令,不能像离线任务一样长时间占住消息处理协程; - # 2. 因此这里单独维护一个“主链路最多等多久”的预算,超时后立即进入降级逻辑; - # 3. 图片渲染器即便本身还能继续尝试,也不允许把主消息链路拖成几十秒假死。 - self.sync_send_timeout_seconds = max(8, int(sync_send_timeout_seconds or 18)) # 注入日志对象,便于主插件统一控制日志风格与输出目标。 self.log = log or default_logger # 菜单图片模板路径(相对仓库根目录),支持仅改模板文件完成 UI 更新。 @@ -493,14 +487,7 @@ class RobotMenuRenderTool: md_content = (markdown_content or "").strip() or f"```text\n{text_content}\n```" output_image = f"robot_menu_{int(time.time() * 1000)}.png" try: - # 这里故意不再使用“渲染超时 * 重试次数 + 缓冲”的长预算: - # 1. 菜单是即时命令,用户更在意“尽快拿到结果或降级结果”,而不是死等图片; - # 2. 若沿用 45~55 秒预算,多个菜单命令会持续占住机器人并发槽位,放大成“整条插件链路卡住”; - # 3. 因此统一按 sync_send_timeout_seconds 控制主链路等待时间,超时后快速回退。 - total_timeout = max(8, int(self.sync_send_timeout_seconds or 18)) - # Markdown 转图内部也要用更短预算,避免内外层超时完全重合,导致降级逻辑来不及执行。 - html_budget_seconds = max(5, min(10, total_timeout - 4)) - render_budget_seconds = max(6, total_timeout - 2) + total_timeout = max(30, self.image_render_timeout_seconds * max(1, self.image_render_retries) + 10) output_dir = Path(os.getcwd()) / "temp" / "md2image" output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / output_image @@ -516,8 +503,8 @@ class RobotMenuRenderTool: md_content, output_image, max_retries=max(1, self.image_render_retries), - render_timeout_seconds=render_budget_seconds, - html_timeout_seconds=html_budget_seconds, + render_timeout_seconds=max(10, self.image_render_timeout_seconds), + html_timeout_seconds=min(30, max(10, self.image_render_timeout_seconds)), ), timeout=total_timeout, ) diff --git a/utils/markdown_to_image.py b/utils/markdown_to_image.py index b10c16c..9185109 100644 --- a/utils/markdown_to_image.py +++ b/utils/markdown_to_image.py @@ -864,22 +864,13 @@ async def _run_in_md2img_runtime(coro, timeout_seconds: Optional[int] = None): future = runtime.submit(coro) awaitable_future = asyncio.wrap_future(future) - try: - if timeout_seconds is not None: - return await asyncio.wait_for(awaitable_future, timeout=max(1, int(timeout_seconds))) - # 关键修复: - # 之前这里直接 return Future 对象,调用方 await 后只拿到 Future 本身, - # 导致业务层误以为截图已完成,实际截图仍在后台执行,出现“先判失败后截图”的时序错乱。 - # 这里必须等待 Future 完成并返回真实结果,保证调用链严格串行。 - return await awaitable_future - except (asyncio.TimeoutError, asyncio.CancelledError): - # 这里要把“当前调用方已放弃等待”的状态显式同步给 md2img 专用事件循环: - # 1. 菜单、总结这类交互命令一旦在主链路超时,若不取消专用 loop 里的任务, - # 浏览器截图仍可能继续跑完,白白占着浏览器与事件循环资源; - # 2. 多次超时后,残留任务会在后台堆积,看起来像“插件流程整体卡住”; - # 3. 因此这里在超时/取消时主动 future.cancel(),让下游尽快停止当前截图任务。 - future.cancel() - raise + if timeout_seconds is not None: + return await asyncio.wait_for(awaitable_future, timeout=max(1, int(timeout_seconds))) + # 关键修复: + # 之前这里直接 return Future 对象,调用方 await 后只拿到 Future 本身, + # 导致业务层误以为截图已完成,实际截图仍在后台执行,出现“先判失败后截图”的时序错乱。 + # 这里必须等待 Future 完成并返回真实结果,保证调用链严格串行。 + return await awaitable_future def _get_browser_manager() -> _PersistentBrowser: