Skip to content

fix: qq_official适配器使用SessionController(会话控制)功能时机器人回复消息无法发送到聊天平台 #1709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

shuiping233
Copy link
Contributor

@shuiping233 shuiping233 commented Jun 2, 2025

这个问题类似 #807

Motivation

如题,使用会话控制时,参照AstrBot文档-会话控制的描述,会话控制器内机器人发送消息是调用await event.send(message_result) 方法的,但是实际qqofficial_message_event里的send方法缺少了_post_send的调用,所以消息发不出去

实例代码

@filter.command("test")
    async def test(self, event: AstrMessageEvent):
        try:
            yield event.plain_result("请发送一个成语~")
            # 注册一个会话控制器,设置超时时间为 60 秒,不记录历史消息链
            @session_waiter(timeout=60, record_history_chains=False)  # type: ignore
            async def empty_mention_waiter(
                controller: SessionController, event: AstrMessageEvent
            ):
                message_result = event.make_result()
                message_result.message(
                    f"{randint(10**10, 20**10)}"
                )
                await event.send(message_result)
                controller.keep(timeout=60, reset_timeout=True)
            try:
                await empty_mention_waiter(event)
            except TimeoutError as _:  # 当超时后,会话控制器会抛出 TimeoutError
                yield event.plain_result("你超时了!")
            except Exception as e:
                yield event.plain_result(
                    "发生错误,请联系管理员: "
                    + str(e)
                )
            finally:
                event.stop_event()
        except Exception as e:
            logger.error("handle_empty_mention error: " + str(e))

Modifications

修改了 qqofficial_message_event.QQOfficialMessageEvent的send方法,使其能调用self._post_send方法,能将消息发送至聊天平台

Check

我的commit message写的可能有亿点点随意了(

  • 😊 我的 Commit Message 符合良好的规范
  • 👀 我的更改经过良好的测试
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。
  • 😮 我的更改没有引入恶意代码

好的,这是翻译成中文的 pull request 摘要:

Sourcery 总结

Bug 修复:

  • 在 QQOfficialMessageEvent.send 中添加 await self._post_send(),以便消息能够真正发送到聊天平台。
Original summary in English

好的,这是将 pull request 总结翻译成中文的结果:

Sourcery 总结

Bug 修复:

  • 在 respond 阶段添加一个特殊分支,以便为 qq_official 事件调用 event._post_send,这样基于会话的回复才能真正被分发。
Original summary in English

Summary by Sourcery

Bug Fixes:

  • Add a special branch in the respond stage to call event._post_send for qq_official events so session-based replies are actually dispatched

Copy link
Contributor

sourcery-ai bot commented Jun 2, 2025

审查者指南

确保通过 SessionController 使用 QQ Official 适配器发送的消息能够实际到达聊天平台,方法是在 respond 阶段调用适配器的 post-send hook。

序列图:修复 QQ Official 适配器在 Respond 阶段的消息发送问题

sequenceDiagram
    actor User
    participant Plugin_SessionController as PluginSC
    participant QQOfficialMessageEvent as Event
    participant RespondStage
    participant QQChatPlatform

    User->>PluginSC: 发送命令 (例如,/test)
    PluginSC->>Event: event.send(message_result)
    Note over PluginSC, Event: 使用 SessionController 的插件尝试发送回复

    %% 消息处理管道继续到 RespondStage %%
    RespondStage->>RespondStage: process(event, result)
    alt If (result 为 null OR result.action != Action.POST_SEND)
        RespondStage->>Event: get_platform_name()
        Event-->>RespondStage: "qq_official"
        opt if platform_name == "qq_official"
            Note over RespondStage: 针对使用 SessionController 的 qq_official 适配器的特殊处理
            RespondStage->>Event: _post_send()
            activate Event
            Event->>QQChatPlatform: 传输消息内容
            QQChatPlatform-->>Event: Ack (消息已发送)
            deactivate Event
        end
    else result.action == Action.POST_SEND
        RespondStage->>RespondStage: 记录消息已处理并发送
    end
Loading

类图:QQOfficialMessageEvent 的相关方面

classDiagram
  class QQOfficialMessageEvent {
    +get_platform_name() String
    +send(message_result) await
    #_post_send() await
  }

  note "PR 描述和 Sourcery 摘要表明 QQOfficialMessageEvent.send() 已被修改为调用 _post_send()。_post_send() 方法也由 RespondStage 中的新逻辑直接调用(来自 diff)。"
Loading

文件级别变更

变更 详情 文件
在 respond 管道中添加针对 qq_official 事件的特殊处理,以触发适配器的 post-send 例程。
  • 在日志记录分支后引入了一个 else 块,以隔离非日志记录路径
  • 检查 event.get_platform_name() 是否等于 "qq_official"
  • 调用 await event._post_send() 以将消息转发到聊天平台
astrbot/core/pipeline/respond/stage.py

提示和命令

与 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

Ensure that messages sent via SessionController using the QQ Official adapter actually reach the chat platform by invoking the adapter’s post-send hook in the respond stage.

Sequence Diagram: Message Sending Fix in Respond Stage for QQ Official Adapter

sequenceDiagram
    actor User
    participant Plugin_SessionController as PluginSC
    participant QQOfficialMessageEvent as Event
    participant RespondStage
    participant QQChatPlatform

    User->>PluginSC: Sends command (e.g., /test)
    PluginSC->>Event: event.send(message_result)
    Note over PluginSC, Event: Plugin using SessionController attempts to send reply

    %% Message processing pipeline continues to RespondStage %%
    RespondStage->>RespondStage: process(event, result)
    alt If (result is null OR result.action != Action.POST_SEND)
        RespondStage->>Event: get_platform_name()
        Event-->>RespondStage: "qq_official"
        opt if platform_name == "qq_official"
            Note over RespondStage: Special handling for qq_official adapter with SessionController
            RespondStage->>Event: _post_send()
            activate Event
            Event->>QQChatPlatform: Transmit message content
            QQChatPlatform-->>Event: Ack (Message Sent)
            deactivate Event
        end
    else result.action == Action.POST_SEND
        RespondStage->>RespondStage: Log message already processed for sending
    end
Loading

Class Diagram: Relevant Aspects of QQOfficialMessageEvent

classDiagram
  class QQOfficialMessageEvent {
    +get_platform_name() String
    +send(message_result) await
    #_post_send() await
  }

  note "The PR description and Sourcery summary suggest QQOfficialMessageEvent.send() was modified to call _post_send(). The _post_send() method is also directly called by the new logic in RespondStage (from diff)."
Loading

File-Level Changes

Change Details Files
Add special handling in the respond pipeline for qq_official events to trigger the adapter’s post-send routine.
  • Introduced an else block after the logging branch to isolate non-logging paths
  • Checked if event.get_platform_name() equals "qq_official"
  • Called await event._post_send() to forward the message to the chat platform
astrbot/core/pipeline/respond/stage.py

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

@shuiping233 - 我已经查看了你的更改,它们看起来很棒!

以下是我在审查期间查看的内容
  • 🟢 通用问题:一切看起来都不错
  • 🟢 安全性:一切看起来都不错
  • 🟢 测试:一切看起来都不错
  • 🟢 复杂性:一切看起来都不错
  • 🟢 文档:一切看起来都不错

Sourcery 对于开源是免费的 - 如果你喜欢我们的评论,请考虑分享它们✨
帮助我变得更有用!请点击每个评论上的👍或👎,我将使用反馈来改进你的评论。
Original comment in English

Hey @shuiping233 - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

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.

@Soulter
Copy link
Member

Soulter commented Jun 4, 2025

这个其实会影响到分段回复,_post_send 包括 _pre_send 的引入是处理分段回复时对 QQ 官方机器人接口的 workaround(只支持一问一答),具体可以参考:https://vscode.dev/github/Soulter/AstrBot/blob/master/astrbot/core/pipeline/respond/stage.py#L123-L212

我觉得一个可行的解决方案是直接抛弃掉 _pre_send() -> send(), _post_send() 这个模式,然后对 QQ 官方机器人接口的特殊处理:直接在 astrbot/core/pipeline/respond/stage.py 中去解决(通过获取平台名)

@shuiping233
Copy link
Contributor Author

我对Astrbot的内部结构了解的还不深入,所以我这个pr也是“见招拆招”的处理,我可能暂时没法提供其他建议了,不过我还是愿意提交代码pr来帮助解决这个问题的,只是我需要知道大概要做什么,能否在详细讲讲我这边具体能帮忙实现内容什么呢(也就是后续我要做什么事情)

@Soulter
Copy link
Member

Soulter commented Jun 4, 2025

我对Astrbot的内部结构了解的还不深入,所以我这个pr也是“见招拆招”的处理,我可能暂时没法提供其他建议了,不过我还是愿意提交代码pr来帮助解决这个问题的,只是我需要知道大概要做什么,能否在详细讲讲我这边具体能帮忙实现内容什么呢(也就是后续我要做什么事情)

感谢!现在 AstrMessageEvent 中实现了这两个方法:

    async def _pre_send(self):
        """调度器会在执行 send() 前调用该方法"""

    async def _post_send(self):
        """调度器会在执行 send() 后调用该方法"""

其实都只是针对 QQ 官方机器人接口只能一问一答的临时解决方案。具体的逻辑就在 https://vscode.dev/github/Soulter/AstrBot/blob/master/astrbot/core/pipeline/respond/stage.py#L123-L212 这里,然后 astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py 中的 send() 方法会将消息段全部append到一个buffer中,在 post_send 的时候一次性发送出去。然后为了解决这个 PR 遇到的问题,我觉得可以对 qqofficial_message_event 单独处理,在 astrbot/core/pipeline/respond/stage.py 这个文件这里的分段回复逻辑的地方,对 qqofficial 单独进行处理,也就是不分段回复了

@shuiping233
Copy link
Contributor Author

好的,我这周末会花点时间研究一下再写写看的

@shuiping233 shuiping233 force-pushed the fix-qq-offical-session-bug branch from d7d64c1 to eb62128 Compare June 7, 2025 01:15
@shuiping233
Copy link
Contributor Author

我实际调试了一下,发现在会话控制器返回result后,stage.py的process函数里,event.get_result() 返回了一个chain为空列表的MessageEventResult,从而导致 无法执行 elif len(result.chain) > 0: 条件分支里的 await event._post_send(),但是这里的event却有一个qqoffical_message_event.py里有的send_buffer,send_buffer里携带了会话控制器正要返回的的消息链内容
image
image

考虑到修改这个process函数会影响其他的平台适配器,我没有条件去覆盖测试新增代码对其他平台的影响,所以我现在还不敢在上面加东西.

而最重要的问题是, event.get_result() 返回了一个chain为空列表的MessageEventResult"这个现象在使用会话控制器时是否是符合预期的,因为我现在不知道我是要怎么通过哪种方法来解决这个问题

  • 是要从event.send_buffer里直接判断非空和qqoffical平台然后直接_post_send ?
  • 还是说去"修好"这个chain为空列表的问题让正确的MessageEventResult进入 elif len(result.chain) > 0: 分支 然后_post_send ?

单步调试仔细发现会话管理器的handle处理完成后,确实没有将消息链赋值到event._result的逻辑,qqoffical_message_event.pysend方法只是将消息链赋值到send_buffer上了,而后续event.stop_event()的判断_resultNone,所以就塞一个空的MessageEventResult进去,这应该就是event.get_result()返回值为None的原因了
image
image
image

@shuiping233 shuiping233 force-pushed the fix-qq-offical-session-bug branch from cdda797 to 1ce95c4 Compare June 8, 2025 02:20
@@ -211,6 +211,10 @@ async def process(
logger.info(
f"AstrBot -> {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}"
)
else:
# 对使用 qq_official 适配器的会话控制器发送消息的特殊处理
if event.get_platform_name() == "qq_official":
Copy link
Contributor Author

Choose a reason for hiding this comment

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

想了想core目录的代码不应该依赖外层api级别的类,所以我就没用from astrbot.api.event import filter filter.PlatformAdapterType来判断平台类型

会话控制器处理发往qq_official适配器消息的时候才会进入这个这个else,因为文档里会话控制器发送消息的示例代码写法是await event.send(message_result) ,执行完会话控制器的handler之后不会再进一步处理event了,也就是没有执行event.set_result(),所以event._result.chain是空列表,而process方法并没有处理len(result.chain) > 0之外的情况,没有调用event._post_event(),导致了消息发不出去

@shuiping233
Copy link
Contributor Author

@sourcery-ai review

Copy link
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.

@shuiping233 - 我已经查看了你的更改,它们看起来很棒!

以下是我在审查期间查看的内容
  • 🟢 一般问题:一切看起来都不错
  • 🟢 安全性:一切看起来都不错
  • 🟢 测试:一切看起来都不错
  • 🟢 复杂性:一切看起来都不错
  • 🟢 文档:一切看起来都不错

Sourcery 对开源是免费的 - 如果你喜欢我们的评论,请考虑分享它们✨
帮助我更有用!请点击每个评论上的👍或👎,我将使用反馈来改进您的评论。
Original comment in English

Hey @shuiping233 - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants