feat(qqofficial): allow QQ Official Webhook adapters to proactively send group messages without requiring a cached msg_id (#8841)

* feat(qqofficial): support webhook QR setup

* docs(qqofficial): simplify webhook QR setup step
This commit is contained in:
Weilong Liao
2026-06-17 21:13:05 +08:00
committed by GitHub
parent 30ae18a8f0
commit 55af880369
9 changed files with 198 additions and 65 deletions

View File

@@ -413,7 +413,7 @@ class QQOfficialPlatformAdapter(Platform):
sent_message_id = self._extract_message_id(ret)
if sent_message_id:
self.remember_session_message_id(session.session_id, sent_message_id)
await super().send_by_session(session, message_chain)
await Platform.send_by_session(self, session, message_chain)
def remember_session_message_id(self, session_id: str, message_id: str) -> None:
if not session_id or not message_id:

View File

@@ -123,7 +123,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
self.webhook_helper = None
self._session_last_message_id: dict[str, str] = {}
self._session_scene: dict[str, str] = {}
self._allow_group_proactive_send = False
self._allow_group_proactive_send = True
async def send_by_session(
self,

View File

@@ -99,7 +99,7 @@ class PlatformService:
)
if platform_type == "dingtalk":
return await self._handle_dingtalk_registration(action, payload)
if platform_type == "qq_official":
if platform_type in {"qq_official", "qq_official_webhook"}:
return await self._handle_qqofficial_registration(
action,
payload,

View File

@@ -1045,7 +1045,9 @@ export default {
return this.selectedPlatformConfig?.type === "dingtalk";
},
isQqOfficialPlatform() {
return this.selectedPlatformConfig?.type === "qq_official";
return ["qq_official", "qq_official_webhook"].includes(
this.selectedPlatformConfig?.type,
);
},
},
watch: {

View File

@@ -77,6 +77,13 @@ const REGISTRATION_ACTIONS = {
successKey: 'registrationAction.qqOfficial.created',
statusKeyPrefix: 'registrationAction.qqOfficial.status',
},
qq_official_webhook: {
icon: 'mdi-qrcode',
titleKey: 'registrationAction.qqOfficial.title',
scanTitleKey: 'registrationAction.qqOfficial.scanTitle',
successKey: 'registrationAction.qqOfficial.created',
statusKeyPrefix: 'registrationAction.qqOfficial.status',
},
};
export default {

View File

@@ -2,8 +2,8 @@
> [!WARNING]
> 1. QQ Official Bot currently requires an IP whitelist.
> 2. It supports group chat, private chat, channel chat, and channel private chat.
> 3. You need a server with a public IP and a domain.
> 2. Webhook mode requires a server with a public IP, a domain, and HTTPS access.
> 3. It supports group chat, private chat, channel chat, and channel private chat.
## Supported Basic Message Types
@@ -19,7 +19,43 @@
Proactive message push: Supported.
## Apply for a Bot
## Create a QQ Bot in AstrBot with One-click QR Setup (Recommended)
### Setup Flow
1. In AstrBot WebUI, click `Bots` in the left sidebar, then click `+ Create Bot`.
2. Select `QQ Official Bot (Webhook)`.
3. Under `Choose setup method`, select `One-click QR setup`, click start, then scan the QR code with mobile QQ.
4. After confirming the QR binding, click `Save`.
5. Configure DNS and a reverse proxy for your server so HTTPS requests are forwarded to AstrBot's `6185` port.
6. Go back to the QQ Open Platform bot management page, open `Development -> Callback Configuration`, and enter the Webhook callback URL generated by AstrBot.
7. Select the callback events you need. To receive full group messages, make sure the group event `GROUP_MESSAGE_CREATE` is selected.
8. Save the callback configuration, then restart AstrBot.
> [!TIP]
> With `Unified Webhook Mode`, AstrBot generates a unique Webhook callback URL automatically. You can find it in the logs or on the bot card in WebUI.
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
### Use in Group Chats
#### Add to a Group Chat
Open the created QQ bot profile page (mobile QQ -> Contacts -> Bots tab). You can find `Add to group chat` near the bottom. Currently, the bot can only be added to groups where you are the group owner.
#### Set Message Access Scope and Proactive Speaking
In mobile QQ group settings, open the bot settings page. We recommend setting `Messages the bot can access` to `All group messages`, and enabling `Allow the bot to proactively speak in the group`.
With this configuration, the bot can receive full group messages and proactively push messages to the group, such as scheduled task notifications and plugin notifications.
Webhook mode also requires selecting the group event `GROUP_MESSAGE_CREATE` in QQ Open Platform callback configuration. Otherwise, AstrBot cannot receive full group message events.
![QQ Official Bot recommended group chat settings](/qqofficial-group-recommended-config.png)
## Manually Apply for a QQ Bot (Not Recommended)
### Apply for a Bot
Open [QQ Official Bot](https://q.qq.com) and sign in.
@@ -29,7 +65,7 @@ Open the created bot to enter its management page:
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## Allow Bot in Channel / Group / Private Chat
### Allow Bot in Channel / Group / Private Chat
Open `Sandbox Configuration` to set a sandbox channel / QQ group / QQ private chat (up to 20 members).
@@ -37,49 +73,62 @@ Then configure QQ groups, private chat QQ accounts, and QQ channels as needed.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## Recommended Group Chat Settings
In mobile QQ group settings, open the bot settings page. We recommend setting `Messages the bot can access` to `All group messages`, and enabling `Allow the bot to proactively speak in the group`.
With this configuration, the bot can receive full group messages and proactively push messages to the group, such as scheduled task notifications and plugin notifications.
![QQ Official Bot recommended group chat settings](/qqofficial-group-recommended-config.png)
## Get `appid` and `secret`
### Get `appid` and `secret`
After adding the bot where you need it, open `Development -> Development Settings`, then copy `appid` and `secret`.
## Add IP Whitelist
If you use AstrBot WebUI's `One-click QR setup`, you can skip this step. AstrBot fills in `appid` and `secret` automatically after QR binding succeeds.
### Add IP Whitelist
Open `Development -> Development Settings`, find IP whitelist, and add your server IP.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png)
## Configure in AstrBot
> [!TIP]
> If you do not know your server IP, run `curl ifconfig.me` or check [ip138.com](https://ip138.com/).
>
> In NAT environments without a public IP, the observed IP may change depending on your carrier. Use proxy/tunnel if needed.
### Configure in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `qq_official_webhook`.
4. Select `QQ Official Bot (Webhook)`.
Fill in:
Recommended: use `One-click QR setup`.
1. Under `Choose setup method`, select `One-click QR setup`.
2. Click start, then scan and confirm the QR code with mobile QQ.
3. Wait until the page shows binding success. AstrBot fills in `appid` and `secret` automatically.
4. Keep `Unified Webhook Mode` enabled, adjust `ID` and other options as needed, then click `Save`.
If QR setup is unavailable, choose `Manual setup` and fill in:
- ID (`id`): any unique identifier.
- Enable (`enable`): checked.
- `appid`: from QQ Official Bot platform.
- `secret`: from QQ Official Bot platform.
- Unified Webhook Mode (`unified_webhook_mode`): keep enabled.
Click `Save`.
## Configure Callback URL
### Configure Reverse Proxy
In `Development -> Callback Configuration`, configure callback URL.
After saving, configure DNS and reverse proxy for your server. Forward requests to AstrBot's `6185` port. If `Unified Webhook Mode` is disabled, forward requests to the port configured in the previous step instead.
Set request URL to `<your-domain>/astrbot-qo-webhook/callback`.
The Webhook callback URL must be reachable from QQ Open Platform over the public internet and must use HTTPS.
Your domain should reverse-proxy traffic to AstrBot port `6196` using `Caddy`, `Nginx`, or `Apache`.
### Configure Callback URL and Events
Then add callback events and select all four event categories (private, group, channel, etc.).
Open `Development -> Callback Configuration`.
After you save the bot in AstrBot, AstrBot generates a unique Webhook callback URL. You can find it in the logs or on the bot card in WebUI.
Use that URL as the request URL.
Then add callback events. To receive full group messages, select the group event `GROUP_MESSAGE_CREATE`; also select private chat events, channel events, and other events as needed.
![image](https://files.astrbot.app/docs/source/images/webhook/image.png)
@@ -87,10 +136,6 @@ After entering values, move focus out of the input box to trigger validation. If
Then restart AstrBot.
## Done
AstrBot should now be connected. If messages do not respond immediately, wait 1-2 minutes, restart AstrBot, and test again.
## Appendix: Reverse Proxy Setup
If you are new to reverse proxy, Caddy is recommended:

View File

@@ -1,12 +1,10 @@
# 通过 QQ官方机器人 接入 QQ (Webhook)
> [!WARNING]
>
> 1. 截至目前QQ 官方机器人需要设置 IP 白名单。
> 2. 支持群聊、私聊、频道聊天、频道私聊
>
> **需要**一台带有公网 IP 的服务器和域名(如果没备案,需要服务器在海外或者中国港澳台地区)
> 2. Webhook 模式需要一台带公网 IP 的服务器、域名和 HTTPS 访问能力
> 3. 支持群聊、私聊、频道聊天、频道私聊。
## 支持的基本消息类型
@@ -22,7 +20,43 @@
主动消息推送:支持。
## 申请一个机器人
## 在 AstrBot 中扫码一键创建 QQ 机器人(推荐)
### 配置流程
1. 进入 AstrBot 的 WebUI点击左边栏 `机器人`,然后点击 `+ 创建机器人`
2. 选择 `QQ 官方机器人Webhook`
3.`选择创建方式` 中选择 `扫码一键创建`,点击开始创建后,用手机 QQ 扫描页面中的二维码。
4. 扫码确认后,点击 `保存`
5. 根据服务器环境配置域名 DNS 解析和反向代理,将 HTTPS 请求转发到 AstrBot 所在服务器的 `6185` 端口。
6. 回到 QQ 开放平台的机器人管理页,在 `开发 -> 回调配置` 中填写 AstrBot 生成的 Webhook 回调地址。
7. 在回调事件中勾选需要接收的事件。如果需要接收群聊全量消息,请确保勾选群事件 `GROUP_MESSAGE_CREATE`
8. 保存回调配置后,重启 AstrBot。
> [!TIP]
> 使用 `统一 Webhook 模式` 时AstrBot 会自动生成唯一的 Webhook 回调链接。你可以在日志中,或者 WebUI 的机器人卡片上找到该链接。
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
### 在群聊中使用
#### 添加到群聊
进入创建的 QQ 机器人的资料页手机QQ -> 联系人 -> 机器人页签),在下方可以找到 “添加到群聊”。目前只能添加到自己为群主的群聊。
#### 设置机器人可获取的群聊消息范围和主动发言
在手机 QQ 的群聊设置中打开机器人设置,推荐将 `机器人可获取的群聊消息范围` 设置为 `获取群内全部消息`,并开启 `机器人主动在群聊内发言`
这样机器人可以接收群聊全量消息,也可以在群聊中主动推送消息,例如定时任务推送、插件主动通知等。
Webhook 模式还需要在 QQ 开放平台的回调配置中勾选群事件 `GROUP_MESSAGE_CREATE`,否则 AstrBot 无法收到群聊全量消息事件。
![QQ 官方机器人推荐群聊配置](/qqofficial-group-recommended-config.png)
## 手动申请 QQ 机器人(不推荐)
### 申请一个机器人
首先,打开 [QQ官方机器人](https://q.qq.com) 并登录。
@@ -32,7 +66,7 @@
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## 允许机器人加入频道/群/私聊
### 允许机器人加入频道/群/私聊
点击`沙箱配置`,这允许你立即设置一个沙箱频道/QQ群/QQ私聊用于拉入机器人需要小于等于20个人
@@ -40,34 +74,40 @@
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## 推荐群聊配置
在手机 QQ 的群聊设置中打开机器人设置,推荐将 `机器人可获取的群聊消息范围` 设置为 `获取群内全部消息`,并开启 `机器人主动在群聊内发言`
这样机器人可以接收群聊全量消息,也可以在群聊中主动推送消息,例如定时任务推送、插件主动通知等。
![QQ 官方机器人推荐群聊配置](/qqofficial-group-recommended-config.png)
## 获取 appid、secret
### 获取 appid、secret
添加机器人到你想用的地方后。
点击 `开发->开发设置`,找到 appid、secret。复制并保存它们。
## 添加 IP 白名单
如果你使用 AstrBot WebUI 的 `扫码一键创建`这一步可以跳过。扫码绑定成功后AstrBot 会自动填入 `appid``secret`
### 添加 IP 白名单
点击 `开发->开发设置`,找到 IP 白名单。添加你的服务器 IP 地址。
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png)
## 在 AstrBot 配置
> [!TIP]
> 如果你不知道你的服务器 IP 地址,可以在终端中输入 `curl ifconfig.me` 来获取。或者登录 [ip138.com](https://ip138.com/) 查看。
>
> 如果你在没有公网 IP 的环境下,你看到的 IP 是运营商 NAT 的 IP这个 IP 根据你的运营商的情况可能会随时变化。如有必要,可以配置代理。
### 在 AstrBot 配置
1. 进入 AstrBot 的管理面板
2. 点击左边栏 `机器人`
3. 然后在右边的界面中,点击 `+ 创建机器人`
4. 选择 `qq_official_webhook`
4. 选择 `QQ 官方机器人Webhook`
弹出的配置项填写
推荐使用 `扫码一键创建`
1.`选择创建方式` 中选择 `扫码一键创建`
2. 点击开始创建,用手机 QQ 扫描二维码并确认。
3. 等待页面显示绑定成功。AstrBot 会自动填入 `appid``secret`
4. 保持 `统一 Webhook 模式` 开启,根据需要调整 `ID` 等配置,然后点击 `保存`
如果扫码不可用,也可以选择 `手动创建`。弹出的配置项填写:
- ID(id):随意填写,用于区分不同的消息平台实例。
- 启用(enable): 勾选。
@@ -77,24 +117,21 @@
点击 `保存`
## 反向代理
### 配置反向代理
保存之后,请根据你的服务器环境,配置域名 DNS 解析和反向代理,将请求转发到 AstrBot 所在服务器的 `6185` 端口 (如果没有开启统一 Webhook 模式,将请求转发到上一步配置指定的端口)
保存之后,请根据你的服务器环境,配置域名 DNS 解析和反向代理,将请求转发到 AstrBot 所在服务器的 `6185` 端口如果没有开启统一 Webhook 模式,将请求转发到上一步配置指定的端口
## 设置回调地址
Webhook 回调地址必须可以被 QQ 开放平台公网访问,并且需要使用 HTTPS。
`开发->回调配置` 处,配置回调地址。
### 设置回调地址和事件
`开发 -> 回调配置` 处,配置回调地址。
上一步点击保存之后AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到。
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
将请求地址填写为该地址。
> [!TIP]
> v4.8.0 之前没有 `统一 Webhook 模式`,则请求地址填写 `<你的域名>/astrbot-qo-webhook/callback`。
填写好之后,添加事件,四个事件类型都全选:单聊事件、群事件、频道事件等,如下图。
填写好之后,添加事件。需要接收群聊全量消息时,请勾选群事件 `GROUP_MESSAGE_CREATE`;同时按需勾选单聊事件、频道事件等。
![image](https://files.astrbot.app/docs/source/images/webhook/image.png)
@@ -102,10 +139,6 @@
接着重启 AstrBot。
## 🎉 大功告成
此时,你的 AstrBot 应该已经连接成功。如果发送消息没有反应,请等待一两分钟后重启 AstrBot 再进行确认(测试时发现回调地址不会立即生效)。
## 附录:如何配置反向代理
如果你还没有相关经验,这里推荐使用 Caddy 作为反向代理的工具,请参考:

View File

@@ -255,7 +255,7 @@ async def test_ws_group_send_by_session_with_cached_msg_id_still_omits_msg_id():
@pytest.mark.asyncio
async def test_webhook_group_send_by_session_without_cached_msg_id_skips_send():
async def test_webhook_group_send_by_session_without_cached_msg_id_omits_msg_id():
adapter = QQOfficialWebhookPlatformAdapter(
{
"id": "qq-official-webhook-test",
@@ -276,7 +276,13 @@ async def test_webhook_group_send_by_session_without_cached_msg_id_skips_send():
MessageChain(chain=[Plain("webhook proactive hello")]),
)
adapter.client.api.post_group_message.assert_not_awaited()
adapter.client.api.post_group_message.assert_awaited_once()
kwargs = adapter.client.api.post_group_message.await_args.kwargs
assert kwargs["group_openid"] == "group-1"
assert kwargs["content"] == "webhook proactive hello"
assert "msg_id" not in kwargs
assert "msg_seq" in kwargs
assert adapter._session_last_message_id["group-1"] == "sent-1"
def test_qqofficial_ws_is_not_excluded_from_segmented_reply():

View File

@@ -1,4 +1,5 @@
import base64
from types import SimpleNamespace
import pytest
from Crypto.Cipher import AES
@@ -11,6 +12,8 @@ from astrbot.core.platform.sources.qqofficial.login_registration import (
generate_qqofficial_bind_key,
qqofficial_login_result,
)
from astrbot.dashboard.services import platform_service
from astrbot.dashboard.services.platform_service import PlatformService
def test_generate_qqofficial_bind_key_returns_base64_aes_key():
@@ -69,3 +72,40 @@ def test_decrypt_qqofficial_secret_rejects_invalid_payload():
with pytest.raises(ValueError):
decrypt_qqofficial_secret("invalid", bind_key)
@pytest.mark.asyncio
async def test_qqofficial_webhook_registration_reuses_qr_binding(monkeypatch):
async def fake_request_qqofficial_login_qr(platform_config: dict):
assert platform_config["type"] == "qq_official_webhook"
return SimpleNamespace(
task_id="task-1",
bind_key="bind-key",
qrcode="qr-content",
interval=3,
)
monkeypatch.setattr(
platform_service,
"request_qqofficial_login_qr",
fake_request_qqofficial_login_qr,
)
service = PlatformService.__new__(PlatformService)
result = await service.handle_platform_registration(
"qq_official_webhook",
{
"action": "start",
"platform_config": {"type": "qq_official_webhook"},
},
)
assert result == {
"status": "pending",
"registration_code": "task-1",
"task_id": "task-1",
"bind_key": "bind-key",
"qrcode": "qr-content",
"qrcode_img_content": "qr-content",
"interval": 3,
}