mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
The QQ Bot v2 API supports inline keyboards on outbound messages. When a
user taps a button, the platform dispatches an INTERACTION_CREATE
gateway event; the bot ACKs it via PUT /interactions/{id} and decodes
the button's data payload to route the click.
This commit adds:
New module gateway/platforms/qqbot/keyboards.py
- Inline-keyboard dataclasses (InlineKeyboard, KeyboardRow, KeyboardButton,
KeyboardButtonAction, KeyboardButtonRenderData, KeyboardButtonPermission)
that serialize to the JSON shape the QQ API expects.
- build_approval_keyboard(session_key) — 3-button layout:
✅ 允许一次 / ⭐ 始终允许 / ❌ 拒绝, all sharing group_id='approval'
so clicking one greys out the rest.
- build_update_prompt_keyboard() — Yes/No keyboard for update confirms.
- parse_approval_button_data() / parse_update_prompt_button_data() —
decode the button_data payload from INTERACTION_CREATE.
approve:<session_key>:<decision> (decision = allow-once|allow-always|deny)
update_prompt:<answer> (answer = y|n)
- build_approval_text(ApprovalRequest) — markdown renderer for the
surrounding message body (exec-approval and plugin-approval variants,
with severity icons 🔴/🔵/🟡).
- parse_interaction_event(raw) → InteractionEvent dataclass — normalizes
the nested raw payload (id / scene / openids / button_data / etc.).
Adapter changes (gateway/platforms/qqbot/adapter.py)
- _dispatch_payload routes INTERACTION_CREATE → _on_interaction.
- _on_interaction parses the event, ACKs via PUT /interactions/{id}, then
invokes a user-registered interaction callback. Exceptions from the
callback are caught and logged (never propagate into the WS loop).
- set_interaction_callback(cb) lets gateway wiring register a routing
handler that inspects button_data and resolves the corresponding
pending approval / update prompt.
- _send_c2c_text / _send_group_text now accept an optional keyboard kwarg
and append it to the outbound body.
- send_with_keyboard(chat_id, content, keyboard, reply_to=None) — public
helper that sends a single short message with a keyboard attached.
Does NOT chunk-split (a keyboard message has one interactive surface).
Guild chats are rejected non-retryably — they don't support keyboards.
- send_approval_request(chat_id, ApprovalRequest, reply_to=None) +
send_update_prompt(chat_id, content, reply_to=None) — convenience
wrappers over send_with_keyboard.
Tests
27 new unit tests under TestApprovalButtonData, TestUpdatePromptButtonData,
TestBuildApprovalKeyboard, TestBuildUpdatePromptKeyboard, TestBuildApprovalText,
TestInteractionEventParsing, and TestAdapterInteractionDispatch. Cover:
- Button-data round-trip (build → parse returns original session/decision)
- Keyboard JSON shape + mutual-exclusion group_id
- Exec vs plugin approval text templates + severity icons
- Interaction event parsing (c2c / group / guild scene codes)
- _on_interaction end-to-end: ACK invoked, callback receives parsed event,
callback exceptions are swallowed, missing id skips ACK, no registered
callback is harmless.
Full qqbot suite: 118 passed (72 existing + 19 chunked + 27 keyboards).
Co-authored-by: WideLee <limkuan24@gmail.com>
91 lines
2.5 KiB
Python
91 lines
2.5 KiB
Python
"""
|
|
QQBot platform package.
|
|
|
|
Re-exports the main adapter symbols from ``adapter.py`` (the original
|
|
``qqbot.py``) so that **all existing import paths remain unchanged**::
|
|
|
|
from gateway.platforms.qqbot import QQAdapter # works
|
|
from gateway.platforms.qqbot import check_qq_requirements # works
|
|
|
|
New modules:
|
|
- ``constants`` — shared constants (API URLs, timeouts, message types)
|
|
- ``utils`` — User-Agent builder, config helpers
|
|
- ``crypto`` — AES-256-GCM key generation and decryption
|
|
- ``onboard`` — QR-code scan-to-configure flow
|
|
"""
|
|
|
|
# -- Adapter (original qqbot.py) ------------------------------------------
|
|
from .adapter import ( # noqa: F401
|
|
QQAdapter,
|
|
QQCloseError,
|
|
check_qq_requirements,
|
|
_coerce_list,
|
|
_ssrf_redirect_guard,
|
|
)
|
|
|
|
# -- Onboard (QR-code scan-to-configure) -----------------------------------
|
|
from .onboard import ( # noqa: F401
|
|
BindStatus,
|
|
build_connect_url,
|
|
qr_register,
|
|
)
|
|
from .crypto import decrypt_secret, generate_bind_key # noqa: F401
|
|
|
|
# -- Utils -----------------------------------------------------------------
|
|
from .utils import build_user_agent, get_api_headers, coerce_list # noqa: F401
|
|
|
|
# -- Chunked upload --------------------------------------------------------
|
|
from .chunked_upload import ( # noqa: F401
|
|
ChunkedUploader,
|
|
UploadDailyLimitExceededError,
|
|
UploadFileTooLargeError,
|
|
)
|
|
|
|
# -- Inline keyboards ------------------------------------------------------
|
|
from .keyboards import ( # noqa: F401
|
|
ApprovalRequest,
|
|
ApprovalSender,
|
|
InlineKeyboard,
|
|
InteractionEvent,
|
|
build_approval_keyboard,
|
|
build_approval_text,
|
|
build_update_prompt_keyboard,
|
|
parse_approval_button_data,
|
|
parse_interaction_event,
|
|
parse_update_prompt_button_data,
|
|
)
|
|
|
|
__all__ = [
|
|
# adapter
|
|
"QQAdapter",
|
|
"QQCloseError",
|
|
"check_qq_requirements",
|
|
"_coerce_list",
|
|
"_ssrf_redirect_guard",
|
|
# onboard
|
|
"BindStatus",
|
|
"build_connect_url",
|
|
"qr_register",
|
|
# crypto
|
|
"decrypt_secret",
|
|
"generate_bind_key",
|
|
# utils
|
|
"build_user_agent",
|
|
"get_api_headers",
|
|
"coerce_list",
|
|
# chunked upload
|
|
"ChunkedUploader",
|
|
"UploadDailyLimitExceededError",
|
|
"UploadFileTooLargeError",
|
|
# keyboards
|
|
"ApprovalRequest",
|
|
"ApprovalSender",
|
|
"InlineKeyboard",
|
|
"InteractionEvent",
|
|
"build_approval_keyboard",
|
|
"build_approval_text",
|
|
"build_update_prompt_keyboard",
|
|
"parse_approval_button_data",
|
|
"parse_interaction_event",
|
|
"parse_update_prompt_button_data",
|
|
]
|