Skip to content

feat: 主动消息配图(自动识别生图工具 / 先图后文 / 提示词插件端生成)#73#79

Closed
Ayleovelle wants to merge 4 commits into
DBJD-CR:mainfrom
Ayleovelle:feat/issue-73-proactive-image
Closed

feat: 主动消息配图(自动识别生图工具 / 先图后文 / 提示词插件端生成)#73#79
Ayleovelle wants to merge 4 commits into
DBJD-CR:mainfrom
Ayleovelle:feat/issue-73-proactive-image

Conversation

@Ayleovelle
Copy link
Copy Markdown
Contributor

@Ayleovelle Ayleovelle commented Jun 7, 2026

实现 #73:主动消息支持配图。私聊/群聊各加一个三态开关 image_settings.mode

  • off(默认):只发文本。
  • auto:让模型判断这条消息适不适合配图,适合才生成。
  • always:尽量每条都配图。

不绑定特定生图插件——装了哪个生图插件(向主 LLM 注册了工具的)都能用,没装就只发文本。

📝 描述 / Description

在主动消息发送流程里接入配图能力,并解决了几个实际问题:图片由本插件统一发送(不让生图插件绕过分段等处理)、先图后文、自动识别生图工具、提示词由插件端生成。

🛠️ 改动点 / Modifications

新增 core/image_generator.py,另外动了 _conf_schema.jsonmessage_sender.pyplugin_lifecycle.pymain.py。核心几点:

  1. 图回到本插件自己发:用一个「捕获型」合成事件(继承 AstrMessageEvent,参考官方 CronMessageEvent),重写 send() 把生图插件吐出来的图拦到缓冲区,不让它直接发平台。这样图能走本插件自己的发送流程(分段、装饰钩子、平台历史都正常)。

  2. 先图后文:生图和发图放在文本之前,修掉「文本先到、图掉队十几秒」的问题。

  3. 自动识别生图工具:按关键词双线匹配工具名 + 描述(draw/image/生图/绘图 等)自动挑生图工具,命中「识别/视频」这种负向词时要有强生图信号才保留,避免误收 read_image_file、视频生成那类。识别不到的可以用 extra_tools 手动补、误判的用 exclude_tools 排。

  4. 提示词插件端生成:先让 LLM 把消息文本转成一段画面描述(auto 模式下还会判断要不要配图),再把这段描述交给 Agent 去调生图工具,而不是让模型在工具循环里自己临场编。

  5. 预选 + 缓存:插件加载后预热探测一次并缓存,发送时直接复用、不每次扫描;连续 3 次找不到任何生图工具就本次运行内回退不生图。

  • 不是一个破坏性变更 / This is NOT a breaking change.

📸 运行截图或测试结果 / Screenshots or Test Results

核心流程已真机测试、能正常配图出图:三态开关、图回到本插件自己发、先图后文、自动识别生图工具、缓存与退避都验证过。

第 4 点「提示词插件端生成」是最后才加的,尚未真机验证,只在本地用真实 AstrBot 组件验过逻辑(always 出描述、auto 不适合返空、LLM 失败兜底)。

本地用真实 AstrBot 组件验证过的点:

  • 捕获事件:生图插件调 event.send([Image, Plain]) 时只截图片不截文本,0 次泄漏到平台。
  • 真实工具执行器 call_local_llm_tool 把捕获事件喂给模拟生图 handler,确认图被截回缓冲区。
  • 关键词双线筛选:生图工具命中、read_image_file/视频/无关工具被正确排除。
  • 缓存与退避:选定后只扫一次、连续 3 次选不到永久回退。
  • 提示词生成:always 出描述、auto 不适合返空、失败兜底。
  • 全插件 compileall 通过,ruff format / check 通过。

✅ 检查清单 / Checklist

  • 😊 这是 issue [Feature] 发送主动消息时允许发送图片 #73 提的功能。
  • 👀 核心流程已真机测试,仅「提示词插件端生成」这一步未真机验证(已在描述中标注)。
  • 🤓 没有引入新依赖库。
  • 😮 没有引入恶意代码。

❤️ CONTRIBUTING

  • 🥳 我已阅读并同意遵守该项目的贡献指南。

Ayleovelle and others added 2 commits June 7, 2026 05:14
实现 issue DBJD-CR#73:主动消息可根据文本内容生成配图,提供「不生图 / Bot 自行
判断 / 直接生图」三种模式,群聊与私聊各自独立配置。

实现要点:
- provider 无关:通过工具循环 Agent + get_full_tool_set 暴露的全部已注册
  LLM 工具,由主模型自行判断并调用任意已安装的生图插件,不绑定 aiimg 等
  特定插件;未安装任何生图插件时自动降级为纯文本。
- 图片回流自管:新增「捕获型」合成事件(继承 AstrMessageEvent,重写 send
  拦截图片到缓冲区),使生图插件产出的图片不直接发往平台,而是交还本插件,
  统一走既有分段 / 装饰钩子 / 平台历史持久化的发送流程。
- 不改动 AstrBot 源码:仅继承公开基类、调用 context 公开 API。
- 失败静默降级:生图任何环节出错只记录日志并回退纯文本,绝不把错误信息塞进
  发送给用户的消息内容。

新增 core/image_generator.py;_conf_schema.json 为 friend/group 各增 image_settings;
message_sender 在文本发送后接入配图发送;main.py 注册 ImageMixin。

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
在 issue DBJD-CR#73 配图功能基础上:
1. 先图后文,修复文本不等图。
2. 关键词双线自动识别生图工具(可 extra 补 / exclude 排)+ 预选缓存 + 3 次退避。
3. 配图提示词由插件端先生成画面描述,再交给 Agent 据此调用生图工具。

Generated with Claude Code via Happy
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@dosubot dosubot Bot added size:L 修改了 100-499 行代码 (忽略生成文件) type/feat ✨ 新功能 / New Feature labels Jun 7, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jun 7, 2026

审阅者指南

通过一个 tool-loop agent 增加了主动消息图片生成支持,包括图片捕获事件、可配置的 auto/always 模式、工具预热与缓存,以及与主动消息发送流水线的集成。

主动消息图片生成与捕获的时序图

sequenceDiagram
    participant ProactiveChatPlugin
    participant ImageMixin
    participant Context
    participant ToolManager
    participant ToolLoopAgent as tool_loop_agent
    participant CaptureEvent as _ImageCaptureEvent
    participant ImageTool as ImagePlugin

    ProactiveChatPlugin->>ImageMixin: _maybe_generate_proactive_images(session_id, text, session_config)
    alt image_settings.mode is off
        ImageMixin-->>ProactiveChatPlugin: []
    else auto or always
        ImageMixin->>ImageMixin: _run_image_agent(session_id, text, image_conf, mode)
        ImageMixin->>Context: get_current_chat_provider_id(session_id)
        Context-->>ImageMixin: provider_id
        ImageMixin->>Context: get_llm_tool_manager()
        Context-->>ImageMixin: ToolManager
        ImageMixin->>ImageMixin: _ensure_image_tool_names(ToolManager, image_conf)
        ImageMixin-->>ImageMixin: tool_names
        ImageMixin->>Context: llm_generate(provider_id, prompt, system_prompt)
        Context-->>ImageMixin: completion_text
        ImageMixin->>ImageMixin: _generate_image_prompt(provider_id, text, image_conf, mode)
        ImageMixin-->>ImageMixin: image_prompt
        ImageMixin->>CaptureEvent: _ImageCaptureEvent(context, session, text, message_type)
        ImageMixin->>Context: tool_loop_agent(CaptureEvent, provider_id, user_prompt, tools, system_prompt, max_steps)
        Context->>ToolLoopAgent: tool_loop_agent(...)
        ToolLoopAgent->>ImageTool: call_local_llm_tool(...)
        ImageTool->>CaptureEvent: send(MessageChain[Image, Plain])
        CaptureEvent->>CaptureEvent: captured_images.append(Image)
        Context-->>ImageMixin: tool_loop_agent finished
        ImageMixin-->>ProactiveChatPlugin: images
    end
    loop for each image
        ProactiveChatPlugin->>ProactiveChatPlugin: _send_chain_with_hooks(session_id, [image])
    end
    ProactiveChatPlugin->>ProactiveChatPlugin: send text after images
Loading

文件级变更

变更 细节 文件
将可配置的主动图片生成集成到主动消息发送流程中,确保在不影响可靠性的前提下,图片会在文本之前生成并发送。
  • 将主动图片生成挂接到主动发送器中,在发送文本前调用内部辅助函数生成图片。
  • 为图片生成增加错误处理包装,使任何失败只记录警告日志,而不会阻塞或影响文本投递。
  • 通过现有的 hook/装饰器流水线发送每一张生成的图片,并在图片之间做轻微节流。
core/message_sender.py
引入一个图片生成 mixin 与 tool-loop agent 集成,用于发现、过滤并驱动任意图片工具,同时捕获其输出而不是让插件直接发送。
  • 在主主动插件类中新增 ImageMixin,并跟踪每个进程的图片工具选择状态(缓存、尝试计数器以及禁用标志)。
  • 实现一个合成事件 _ImageCaptureEvent,用于拦截工具发送的 Image 组件,将其写入内部缓冲区而不是直接投递到平台,同时保留会话/上下文语义。
  • 为 ImageMixin 添加方法,用于根据会话配置决定是否/何时生成图片,解析 include/exclude 工具列表,通过名称/描述的关键词匹配(包含负向/强匹配过滤)启发式选择图片工具,并在连续失败后通过退避策略缓存所选工具名称。
  • 驱动一个基于 tool_loop_agent 的图片生成流程:先让 LLM 生成图片 prompt(支持 auto/always 行为以及可选 extra_prompt),然后对所选图片工具运行该 agent,最终返回捕获到的 Image 组件作为结果。
main.py
core/image_generator.py
在插件初始化期间预热并惰性缓存图片工具,以避免运行时扫描开销,并在无工具可用时尽早失败。
  • 在插件初始化时,如果可用则安排一个延迟执行的异步 prewarm_image_tools 调用,并对任务创建进行保护,同时对失败进行调试级日志记录。
  • 实现 prewarm_image_tools,在不增加失败计数的前提下构建一次候选图片工具的 ToolSet,将工具名称缓存以供后续重用,并在未发现任何工具时保持静默。
core/plugin_lifecycle.py
core/image_generator.py
扩展配置模式以支持按会话级别的主动图片设置,用于控制模式与工具选择行为。
  • 新增 image_settings.mode 字段,采用 off/auto/always 三态语义,用于控制主动消息是否以及如何携带图片。
  • 新增 extra_tools、exclude_tools 和 extra_prompt 等配置字段,以细化图片生成过程中工具选择与 prompt 生成行为。
_conf_schema.json

可能关联的 Issue


提示与命令

与 Sourcery 交互

  • 触发新的审阅: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 基于审阅评论生成 GitHub issue: 通过回复某条审阅评论来要求 Sourcery 从该评论创建一个 issue。你也可以在审阅评论中回复 @sourcery-ai issue 来从该评论创建一个 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来随时(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写入 @sourcery-ai summary,即可在该位置随时生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来随时(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理了所有评论且不希望再看到它们,这会很有用。
  • 关闭所有 Sourcery 审阅: 在 pull request 中评论 @sourcery-ai dismiss,即可关闭所有现有的 Sourcery 审阅。特别适用于你希望从一次全新的审阅开始的场景——记得再评论 @sourcery-ai review 以触发新的审阅!

自定义你的体验

访问你的 dashboard 以:

  • 启用或禁用审阅特性,例如 Sourcery 生成的 pull request 摘要、审阅者指南等。
  • 更改审阅语言。
  • 添加、移除或编辑自定义审阅说明。
  • 调整其他审阅设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds proactive message image generation support via a tool-loop agent, including an image capture event, configurable auto/always modes, tool prewarm & caching, and integration into the proactive sender pipeline.

Sequence diagram for proactive message image generation and capture

sequenceDiagram
    participant ProactiveChatPlugin
    participant ImageMixin
    participant Context
    participant ToolManager
    participant ToolLoopAgent as tool_loop_agent
    participant CaptureEvent as _ImageCaptureEvent
    participant ImageTool as ImagePlugin

    ProactiveChatPlugin->>ImageMixin: _maybe_generate_proactive_images(session_id, text, session_config)
    alt image_settings.mode is off
        ImageMixin-->>ProactiveChatPlugin: []
    else auto or always
        ImageMixin->>ImageMixin: _run_image_agent(session_id, text, image_conf, mode)
        ImageMixin->>Context: get_current_chat_provider_id(session_id)
        Context-->>ImageMixin: provider_id
        ImageMixin->>Context: get_llm_tool_manager()
        Context-->>ImageMixin: ToolManager
        ImageMixin->>ImageMixin: _ensure_image_tool_names(ToolManager, image_conf)
        ImageMixin-->>ImageMixin: tool_names
        ImageMixin->>Context: llm_generate(provider_id, prompt, system_prompt)
        Context-->>ImageMixin: completion_text
        ImageMixin->>ImageMixin: _generate_image_prompt(provider_id, text, image_conf, mode)
        ImageMixin-->>ImageMixin: image_prompt
        ImageMixin->>CaptureEvent: _ImageCaptureEvent(context, session, text, message_type)
        ImageMixin->>Context: tool_loop_agent(CaptureEvent, provider_id, user_prompt, tools, system_prompt, max_steps)
        Context->>ToolLoopAgent: tool_loop_agent(...)
        ToolLoopAgent->>ImageTool: call_local_llm_tool(...)
        ImageTool->>CaptureEvent: send(MessageChain[Image, Plain])
        CaptureEvent->>CaptureEvent: captured_images.append(Image)
        Context-->>ImageMixin: tool_loop_agent finished
        ImageMixin-->>ProactiveChatPlugin: images
    end
    loop for each image
        ProactiveChatPlugin->>ProactiveChatPlugin: _send_chain_with_hooks(session_id, [image])
    end
    ProactiveChatPlugin->>ProactiveChatPlugin: send text after images
Loading

File-Level Changes

Change Details Files
Integrate configurable proactive image generation into the proactive message sending flow, ensuring images are generated and sent before text without affecting reliability.
  • Hook proactive image generation into the proactive sender, calling an internal helper to generate images before sending text.
  • Wrap image generation in error handling so any failures only log warnings and never block or impact text delivery.
  • Send each generated image through the existing hook/decorator pipeline with slight throttling between images.
core/message_sender.py
Introduce an image-generation mixin and tool-loop agent integration that discovers, filters, and drives arbitrary image tools, capturing their outputs instead of letting plugins send directly.
  • Add ImageMixin to the main proactive plugin class and track per-process image-tool selection state (cache, attempts counter, and disable flag).
  • Implement an _ImageCaptureEvent synthetic event that intercepts tool-sent Image components into an internal buffer instead of delivering them to the platform, while preserving session/context semantics.
  • Add ImageMixin methods to decide if/when to generate images based on session config, parse include/exclude tool lists, heuristically select image tools via name/description keyword matching with negative/strong filters, and cache selected tool names with backoff after repeated failures.
  • Drive a tool_loop_agent-based image generation flow that first asks the LLM to produce an image prompt (with auto/always behavior and optional extra_prompt), then runs the agent against the selected image tools, returning captured Image components as the result.
main.py
core/image_generator.py
Prewarm and lazily cache image tools during plugin initialization to avoid runtime scanning overhead and to fail fast when no tools are available.
  • On plugin initialize, schedule a deferred async prewarm_image_tools call if available, with guarded task creation and debug logging for failures.
  • Implement prewarm_image_tools to build a ToolSet of candidate image tools once without incrementing failure counters, caching tool names for later reuse while staying silent if nothing is found.
core/plugin_lifecycle.py
core/image_generator.py
Extend configuration schema to support per-session proactive image settings controlling mode and tool selection behavior.
  • Add image_settings.mode with off/auto/always tri-state semantics to control whether and how proactive messages carry images.
  • Add configuration fields for extra_tools, exclude_tools, and extra_prompt to refine tool selection and prompt-generation behavior for image generation.
_conf_schema.json

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces proactive message image generation capabilities to AstrBot, allowing the bot to automatically generate and attach images to proactive messages using available image generation tools. The changes include configuration schema updates, a new ImageMixin class to manage tool selection and agent execution, and integration into the message sending and plugin lifecycle. The code review feedback is highly constructive and identifies several robustness issues, such as handling different message types in the capture event, adding defensive checks for null tool managers and raw string LLM responses, removing redundant code, and preventing premature garbage collection of the background prewarm task.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread core/image_generator.py Outdated
Comment on lines +89 to +97
async def send(self, message: "MessageChain") -> None:
"""拦截发送:仅收集图片组件,不真正发往平台。"""
try:
if message and getattr(message, "chain", None):
for comp in message.chain:
if isinstance(comp, Image):
self.captured_images.append(comp)
except Exception as e: # noqa: BLE001 - 拦截阶段不应影响主流程
logger.debug(f"[主动消息] 捕获配图组件时出现异常喵: {e!r}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

_ImageCaptureEventsend 方法中,当前代码仅通过 getattr(message, "chain", None) 来获取消息组件。然而,在 AstrBot 中,生图插件或其它插件在调用 event.send 时,可能会直接传入一个组件列表(list)或单个 Image 组件。如果传入的是 list,由于 Python 的 list 对象没有 chain 属性,该方法将无法捕获到任何图片,导致配图功能失效。

建议对 message 的类型进行更具防御性的判断,兼容 MessageChainlist/tuple 以及单个组件的情况。

Suggested change
async def send(self, message: "MessageChain") -> None:
"""拦截发送:仅收集图片组件,不真正发往平台。"""
try:
if message and getattr(message, "chain", None):
for comp in message.chain:
if isinstance(comp, Image):
self.captured_images.append(comp)
except Exception as e: # noqa: BLE001 - 拦截阶段不应影响主流程
logger.debug(f"[主动消息] 捕获配图组件时出现异常喵: {e!r}")
async def send(self, message: Any) -> None:
"""拦截发送:仅收集图片组件,不真正发往平台。"""
try:
if message:
if hasattr(message, "chain") and message.chain is not None:
comps = message.chain
elif isinstance(message, (list, tuple)):
comps = message
else:
comps = [message]
for comp in comps:
if isinstance(comp, Image):
self.captured_images.append(comp)
except Exception as e: # noqa: BLE001 - 拦截阶段不应影响主流程
logger.debug(f"[主动消息] 捕获配图组件时出现异常喵: {e!r}")

Comment thread core/image_generator.py
Comment on lines +347 to +351
tool_manager = context.get_llm_tool_manager()
tool_names = self._ensure_image_tool_names(tool_manager, image_conf)
if not tool_names:
# 已选不到(或已永久回退),_ensure_image_tool_names 内已记日志。
return []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

_run_image_agent 中,如果 context.get_llm_tool_manager() 返回了 None(例如在 LLM 工具管理器尚未初始化完成或加载失败时),而此时 self._image_tools_selected 已经为 True,那么 _ensure_image_tool_names 仍会返回缓存的工具名称列表。随后在重建 ToolSet 时,调用 tool_manager.get_func(name) 将会直接抛出 AttributeError: 'NoneType' object has no attribute 'get_func' 异常导致程序崩溃。

建议在获取 tool_manager 后立即进行 None 值校验,若为空则安全退出。

Suggested change
tool_manager = context.get_llm_tool_manager()
tool_names = self._ensure_image_tool_names(tool_manager, image_conf)
if not tool_names:
# 已选不到(或已永久回退),_ensure_image_tool_names 内已记日志。
return []
tool_manager = context.get_llm_tool_manager()
if not tool_manager:
logger.info("[主动消息] 未找到 LLM 工具管理器,跳过配图喵。")
return []
tool_names = self._ensure_image_tool_names(tool_manager, image_conf)
if not tool_names:
# 已选不到(或已永久回退),_ensure_image_tool_names 内已记日志。
return []

Comment thread core/image_generator.py Outdated
Comment on lines +363 to +377
capture_event = _ImageCaptureEvent(
context=context,
session=session,
message=text,
message_type=session.message_type,
)

# 第一步:由插件端先生成“画面描述”(生图提示词)。
# auto 模式下若模型判断这条消息不适合配图,会返回空,此时直接跳过。
image_prompt = await self._generate_image_prompt(
provider_id, text, image_conf, mode
)
if not image_prompt:
logger.info("[主动消息] 未生成配图提示词(判断无需配图),跳过配图喵。")
return []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

_run_image_agent 方法中,capture_event 在第 363 行被首次实例化,但在其被使用之前,第 372 行调用了 _generate_image_prompt,如果该调用返回空则直接返回。若不为空,在第 379 行 capture_event 又被重新实例化并覆盖。这导致第 363 行的首次实例化完全是多余的死代码。

建议移除该冗余的实例化步骤,以提高代码的可读性和运行效率。

Suggested change
capture_event = _ImageCaptureEvent(
context=context,
session=session,
message=text,
message_type=session.message_type,
)
# 第一步:由插件端先生成“画面描述”(生图提示词)。
# auto 模式下若模型判断这条消息不适合配图,会返回空,此时直接跳过。
image_prompt = await self._generate_image_prompt(
provider_id, text, image_conf, mode
)
if not image_prompt:
logger.info("[主动消息] 未生成配图提示词(判断无需配图),跳过配图喵。")
return []
# 第一步:由插件端先生成“画面描述”(生图提示词)。
# auto 模式下若模型判断这条消息不适合配图,会返回空,此时直接跳过。
image_prompt = await self._generate_image_prompt(
provider_id, text, image_conf, mode
)
if not image_prompt:
logger.info("[主动消息] 未生成配图提示词(判断无需配图),跳过配图喵。")
return []

Comment thread core/image_generator.py
Comment on lines +323 to +325
prompt = (getattr(resp, "completion_text", "") or "").strip()
if not prompt:
return ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

_generate_image_prompt 中,self.context.llm_generate 的返回值 resp 在不同的 AstrBot 版本或不同的 Provider 适配器中,有可能会直接返回一个原始的 str 字符串,而非包含 completion_text 属性的响应对象。如果返回的是 str,直接使用 getattr(resp, "completion_text", "") 将会得到空字符串,导致提示词生成失败。

建议增加对 resp 是否为 str 类型的防御性检查。

Suggested change
prompt = (getattr(resp, "completion_text", "") or "").strip()
if not prompt:
return ""
if isinstance(resp, str):
prompt = resp.strip()
else:
prompt = (getattr(resp, "completion_text", "") or "").strip()
if not prompt:
return ""

Comment thread core/plugin_lifecycle.py
Comment on lines +163 to +166
try:
asyncio.create_task(_deferred_prewarm_image_tools())
except Exception as e: # noqa: BLE001
logger.debug(f"[主动消息] 启动配图工具预热任务失败喵: {e!r}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

initialize 中,通过 asyncio.create_task(_deferred_prewarm_image_tools()) 启动了一个延迟预热配图工具的后台任务。由于该任务在启动后会 await asyncio.sleep(5) 挂起 5 秒,且当前代码没有保留对该 Task 对象的强引用,在 Python 的垃圾回收(GC)机制下,该未被引用的 Task 极有可能在执行完毕前被提前回收,导致预热任务静默失败。

建议使用插件中已有的 self._track_task 方法来登记该任务的引用,防止其被过早释放。

Suggested change
try:
asyncio.create_task(_deferred_prewarm_image_tools())
except Exception as e: # noqa: BLE001
logger.debug(f"[主动消息] 启动配图工具预热任务失败喵: {e!r}")
try:
task = asyncio.create_task(_deferred_prewarm_image_tools())
if hasattr(self, "_track_task"):
self._track_task(task)
except Exception as e: # noqa: BLE001
logger.debug(f"[主动消息] 启动配图工具预热任务失败喵: {e!r}")

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并且给出了一些高层次的反馈:

  • _run_image_agent 中,capture_event 被构造了两次(分别在 _generate_image_prompt 前后),但实际上只使用了第二个实例;建议移除第一次的构造,以避免混淆和不必要的工作。
  • 图像工具选择缓存标记(_image_tools_selected_image_tools_disabled_image_tools_attempts)在多个方法中被修改;如果这些方法在同一个进程中可能被并发调用,建议添加一个小的锁,或者确保它们只会从单一任务访问,以避免微妙的竞态条件。
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- In `_run_image_agent`, `capture_event` is constructed twice (before and after `_generate_image_prompt`), but only the second instance is actually used; consider removing the first construction to avoid confusion and unnecessary work.
- The image-tool selection cache flags (`_image_tools_selected`, `_image_tools_disabled`, `_image_tools_attempts`) are manipulated in several methods; if these can be called concurrently per process, consider adding a small lock or ensuring they’re only accessed from a single task to avoid subtle race conditions.

## Individual Comments

### Comment 1
<location path="core/image_generator.py" line_range="363" />
<code_context>
+            logger.info("[主动消息] 已选定的生图工具当前不可用,跳过配图喵。")
+            return []
+
+        capture_event = _ImageCaptureEvent(
+            context=context,
+            session=session,
</code_context>
<issue_to_address>
**issue:** The `capture_event` instance is constructed twice, but the first one is never used.

In `_run_image_agent`, `capture_event` is created once before `_generate_image_prompt` and again immediately after. Since the first instance is never used, please instantiate `capture_event` only once after confirming `image_prompt` is non-empty.
</issue_to_address>

Sourcery 对开源项目免费——如果你喜欢我们的代码审查,请考虑分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会利用这些反馈来改进对你代码的审查。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In _run_image_agent, capture_event is constructed twice (before and after _generate_image_prompt), but only the second instance is actually used; consider removing the first construction to avoid confusion and unnecessary work.
  • The image-tool selection cache flags (_image_tools_selected, _image_tools_disabled, _image_tools_attempts) are manipulated in several methods; if these can be called concurrently per process, consider adding a small lock or ensuring they’re only accessed from a single task to avoid subtle race conditions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_run_image_agent`, `capture_event` is constructed twice (before and after `_generate_image_prompt`), but only the second instance is actually used; consider removing the first construction to avoid confusion and unnecessary work.
- The image-tool selection cache flags (`_image_tools_selected`, `_image_tools_disabled`, `_image_tools_attempts`) are manipulated in several methods; if these can be called concurrently per process, consider adding a small lock or ensuring they’re only accessed from a single task to avoid subtle race conditions.

## Individual Comments

### Comment 1
<location path="core/image_generator.py" line_range="363" />
<code_context>
+            logger.info("[主动消息] 已选定的生图工具当前不可用,跳过配图喵。")
+            return []
+
+        capture_event = _ImageCaptureEvent(
+            context=context,
+            session=session,
</code_context>
<issue_to_address>
**issue:** The `capture_event` instance is constructed twice, but the first one is never used.

In `_run_image_agent`, `capture_event` is created once before `_generate_image_prompt` and again immediately after. Since the first instance is never used, please instantiate `capture_event` only once after confirming `image_prompt` is non-empty.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread core/image_generator.py Outdated
Generated with Claude Code via Happy
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@dosubot dosubot Bot added size:XL 修改了 500-999 行代码 (忽略生成文件) and removed size:L 修改了 100-499 行代码 (忽略生成文件) labels Jun 7, 2026
Generated with Claude Code via Happy
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@Ayleovelle
Copy link
Copy Markdown
Contributor Author

Ayleovelle commented Jun 7, 2026

@gemini-code-assist @sourcery-ai 谢了,几条都看了。

send 那块改了,现在 MessageChain、list、单个组件都能接,免得不同生图插件写法不一样漏图。重复构造的 capture_event 删了,确实是手滑留下的。tool_manager 拿到 None 的判空加了。预热那个后台任务也用 _track_task 挂上引用了,不然 sleep 五秒被 GC 收了就白跑。

另外两条没动:llm_generate 宿主签名就是返回 LLMResponse,不会给个裸字符串,那个 str 兜底就没必要了;缓存标记加锁也不用,主动消息是 APScheduler 串行触发的,碰不到并发,顶多预热和第一次发送各扫一次工具,没影响。

@gemini-code-assist
Copy link
Copy Markdown
Contributor

感谢你的详细说明,处理得非常清晰。关于你提到的两点未采纳意见,我完全理解并支持你的判断:

  1. 关于 llm_generate 的兜底:如果宿主 Context.llm_generate 的类型签名确实保证返回 LLMResponse,那么确实没必要引入额外的类型防御,保持代码简洁是更好的选择。
  2. 关于缓存标记加锁:既然主动消息发送流程由 APScheduler 串行调度,确实没有并发竞争风险,避免过度设计是合理的。

你的处理逻辑很稳健,尤其是通过 _track_task 登记预热任务来防止 GC 回收,以及对 _ImageCaptureEvent 的兼容性优化,这些都显著提升了插件的健壮性。期待这个功能上线后的表现!

@Ayleovelle
Copy link
Copy Markdown
Contributor Author

这个和另外一个工具调用的共性很大。打算合在一起做。故本pr关闭

@Ayleovelle Ayleovelle closed this Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 修改了 500-999 行代码 (忽略生成文件) type/feat ✨ 新功能 / New Feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant