Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions astrbot/api/event/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
)
from astrbot.core.platform import AstrMessageEvent

from .qqofficial_interaction import QQOfficialInteractionResultCode

__all__ = [
"AstrMessageEvent",
"QQOfficialInteractionResultCode",
"CommandResult",
"EventResultType",
"MessageChain",
Expand Down
4 changes: 4 additions & 0 deletions astrbot/api/event/filter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
from astrbot.core.star.register import register_on_plugin_error as on_plugin_error
from astrbot.core.star.register import register_on_plugin_loaded as on_plugin_loaded
from astrbot.core.star.register import register_on_plugin_unloaded as on_plugin_unloaded
from astrbot.core.star.register import (
register_on_qqofficial_interaction as on_qqofficial_interaction,
)
from astrbot.core.star.register import register_on_using_llm_tool as on_using_llm_tool
from astrbot.core.star.register import (
register_on_waiting_llm_request as on_waiting_llm_request,
Expand Down Expand Up @@ -63,6 +66,7 @@
"on_plugin_loaded",
"on_plugin_unloaded",
"on_platform_loaded",
"on_qqofficial_interaction",
"on_waiting_llm_request",
"permission_type",
"platform_adapter_type",
Expand Down
14 changes: 14 additions & 0 deletions astrbot/api/event/qqofficial_interaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Public types for QQ Official callback-button interactions."""

from enum import IntEnum


class QQOfficialInteractionResultCode(IntEnum):
"""QQ Official callback-button acknowledgement result codes."""

SUCCESS = 0
FAILED = 1
RATE_LIMITED = 2
DUPLICATE = 3
FORBIDDEN = 4
ADMIN_ONLY = 5
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from botpy.gateway import BotWebSocket

from astrbot import logger
from astrbot.api.event import MessageChain
from astrbot.api.event import MessageChain, QQOfficialInteractionResultCode
from astrbot.api.message_components import At, File, Image, Plain, Record, Reply, Video
from astrbot.api.platform import (
AstrBotMessage,
Expand All @@ -27,6 +27,7 @@
)
from astrbot.core.message.components import BaseMessageComponent
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.star.star_handler import EventType, star_handlers_registry
from astrbot.core.utils.media_utils import MediaResolver

from ...register import register_platform_adapter
Expand Down Expand Up @@ -254,6 +255,10 @@ def _commit(self, abm: AstrBotMessage) -> None:
self.platform.remember_session_message_id(abm.session_id, abm.message_id)
self.platform.commit_event(self.platform.create_event(abm))

async def on_interaction_create(self, interaction: Any) -> None:
"""Forward QQ Official callback-button interactions to the platform."""
await self.platform.dispatch_interaction(interaction)

async def bot_connect(self, session) -> None:
logger.info("[QQOfficial] Websocket session starting.")

Expand Down Expand Up @@ -299,11 +304,13 @@ def __init__(
public_messages=True,
public_guild_messages=True,
direct_message=guild_dm,
interaction=True,
)
else:
self.intents = botpy.Intents(
public_guild_messages=True,
direct_message=guild_dm,
interaction=True,
)
self.client = botClient(
intents=self.intents,
Expand All @@ -321,6 +328,81 @@ def __init__(

self.test_mode = os.environ.get("TEST_MODE", "off") == "on"

@staticmethod
def _normalize_interaction_result_code(
result: Any,
handler_name: str | None = None,
) -> QQOfficialInteractionResultCode | None:
"""Convert a plugin result into a valid QQ Official acknowledgement code.

Args:
result: The result returned by an interaction handler.
handler_name: Fully qualified name of the handler that returned the result.

Returns:
A valid QQ Official result code, or None when the result is absent or invalid.
"""
if result is None:
return None
if isinstance(result, int) and not isinstance(result, bool):
try:
return QQOfficialInteractionResultCode(result)
except ValueError:
pass
logger.debug(
"QQ Official interaction handler returned an invalid result code "
f"from {handler_name or '<unknown>'}: {result!r}"
)
return None

async def _acknowledge_interaction(
self,
interaction: Any,
result_code: QQOfficialInteractionResultCode,
) -> None:
"""Acknowledge a QQ Official interaction.

Args:
interaction: The botpy interaction object received from the gateway.
result_code: The result code returned to QQ.
"""
interaction_id = str(getattr(interaction, "id", ""))
if not interaction_id:
logger.warning("QQ Official interaction has no id; cannot acknowledge it.")
return
try:
await self.client.api.on_interaction_result(interaction_id, result_code)
except Exception:
logger.exception(
f"Failed to acknowledge QQ Official interaction: {interaction_id}"
)

async def dispatch_interaction(self, interaction: Any) -> None:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
"""Dispatch a QQ Official callback-button interaction and acknowledge it.

Args:
interaction: The botpy interaction object received from the gateway.
"""
result_code = QQOfficialInteractionResultCode.FAILED
for handler in star_handlers_registry.get_handlers_by_event_type(
EventType.OnQQOfficialInteractionEvent
):
try:
result = await handler.handler(interaction)
except Exception:
logger.exception(
f"QQ Official interaction handler failed: {handler.handler_full_name}"
)
continue
normalized_result = self._normalize_interaction_result_code(
result,
getattr(handler, "handler_full_name", None),
)
if normalized_result is not None:
result_code = normalized_result
break
await self._acknowledge_interaction(interaction, result_code)

async def send_by_session(
self,
session: MessageSesion,
Expand Down
2 changes: 2 additions & 0 deletions astrbot/core/star/register/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
register_on_plugin_error,
register_on_plugin_loaded,
register_on_plugin_unloaded,
register_on_qqofficial_interaction,
register_on_using_llm_tool,
register_on_waiting_llm_request,
register_permission_type,
Expand All @@ -43,6 +44,7 @@
"register_on_plugin_loaded",
"register_on_plugin_unloaded",
"register_on_platform_loaded",
"register_on_qqofficial_interaction",
"register_on_waiting_llm_request",
"register_permission_type",
"register_platform_adapter_type",
Expand Down
17 changes: 17 additions & 0 deletions astrbot/core/star/register/star_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,23 @@ def decorator(awaitable):
return decorator


def register_on_qqofficial_interaction(**kwargs):
"""Register a QQ Official callback-button interaction handler.

The handler receives a botpy interaction object and should return an
QQOfficialInteractionResultCode or an integer from 0 to 5. Return None when the interaction is
not handled so later handlers can inspect it.
"""

def decorator(awaitable):
_ = get_handler_or_create(
awaitable, EventType.OnQQOfficialInteractionEvent, **kwargs
)
return awaitable

return decorator


def register_on_plugin_error(**kwargs):
"""当插件处理消息异常时触发。

Expand Down
1 change: 1 addition & 0 deletions astrbot/core/star/star_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ class EventType(enum.Enum):

OnAstrBotLoadedEvent = enum.auto() # AstrBot 加载完成
OnPlatformLoadedEvent = enum.auto() # 平台加载完成
OnQQOfficialInteractionEvent = enum.auto() # QQ Official callback button

AdapterMessageEvent = enum.auto() # 收到适配器发来的消息
OnWaitingLLMRequestEvent = enum.auto() # 等待调用 LLM(在获取锁之前,仅通知)
Expand Down
27 changes: 27 additions & 0 deletions docs/en/dev/star/guides/listen-message-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,30 @@ async def check_ok(self, event: AstrMessageEvent):
When event propagation is stopped, all subsequent steps will not be executed.

Assuming there's a plugin A, after A terminates event propagation, all subsequent operations will not be executed, such as executing other plugins' handlers or requesting the LLM.

## QQ Official Button Interactions

QQ Official callback buttons (`action.type = 1`) are delivered as WebSocket interaction events. They do not enter the normal message, command, or LLM pipeline. Plugins can handle them with `on_qqofficial_interaction`.

A handler receives the qq-botpy interaction object. Return `QQOfficialInteractionResultCode` or an integer from `0` to `5` as the QQ acknowledgement code, or return `None` when the click does not belong to the plugin so later enabled handlers can inspect it. The first valid result is acknowledged to QQ; an unhandled interaction receives the `FAILED` code.

```python
from typing import Any

from astrbot.api.event import QQOfficialInteractionResultCode, filter


@filter.on_qqofficial_interaction()
async def on_qqofficial_interaction(
self, interaction: Any
) -> QQOfficialInteractionResultCode | None:
resolved = getattr(getattr(interaction, "data", None), "resolved", {})
button_data = getattr(resolved, "button_data", None)
if button_data != "demo:confirm":
return None

# Start or schedule the real operation here. Return promptly so QQ can stop loading.
return QQOfficialInteractionResultCode.SUCCESS
```

Acknowledgement codes use `QQOfficialInteractionResultCode`: `SUCCESS` (0), `FAILED` (1), `RATE_LIMITED` (2), `DUPLICATE` (3), `FORBIDDEN` (4), and `ADMIN_ONLY` (5). This feature is available only on the QQ Official WebSocket adapter; the QQ Official Webhook adapter does not currently dispatch interaction events and is not supported.
27 changes: 27 additions & 0 deletions docs/zh/dev/star/guides/listen-message-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,30 @@ async def check_ok(self, event: AstrMessageEvent):
当事件停止传播,后续所有步骤将不会被执行。

假设有一个插件 A,A 终止事件传播之后所有后续操作都不会执行,比如执行其它插件的 handler、请求 LLM。

## QQ 官方按钮回调

QQ 官方机器人的回调按钮(`action.type = 1`)会通过 WebSocket 发送 interaction 事件,不会作为普通消息进入 LLM 或命令管线。插件可使用 `on_qqofficial_interaction` 处理点击事件。

处理器接收 qq-botpy 的 interaction 对象。返回 `QQOfficialInteractionResultCode` 或 `0` 到 `5` 的整数作为 QQ 回执码;返回 `None` 表示当前插件不处理该点击,后续已启用的处理器可以继续匹配。第一个有效返回值会结束处理并回执 QQ;没有插件处理时返回失败码 `FAILED`。

```python
from typing import Any

from astrbot.api.event import QQOfficialInteractionResultCode, filter


@filter.on_qqofficial_interaction()
async def on_qqofficial_interaction(
self, interaction: Any
) -> QQOfficialInteractionResultCode | None:
resolved = getattr(getattr(interaction, "data", None), "resolved", {})
button_data = getattr(resolved, "button_data", None)
if button_data != "demo:confirm":
return None

# Start or schedule the real operation here. Return promptly so QQ can stop loading.
return QQOfficialInteractionResultCode.SUCCESS
```

回执码使用 `QQOfficialInteractionResultCode`:`SUCCESS`(0)、`FAILED`(1)、`RATE_LIMITED`(2)、`DUPLICATE`(3)、`FORBIDDEN`(4)和 `ADMIN_ONLY`(5)。该功能仅适用于 QQ 官方 WebSocket 适配器;QQ 官方 Webhook 适配器目前不会分发 interaction 事件,暂不支持此功能。
Loading