QQ 开放平台机器人 API 通信协议规范:从认证鉴权到消息收发的完整实现指南
QQ 开放平台机器人 API 是腾讯面向 QQ 机器人开发者提供的 HTTP/JSON 通信协议,它统一了 QQ 单聊(C2C)、群聊、文字子频道与频道私信(DMS)四种消息场景,同时支持 WebSocket 与 HTTP Webhook 两种事件接收方式。本协议规范依据 QQ 官方 v2 API、社区 SDK(botpy、botgo、bot-node-sdk)实现经验整理,并沉淀于 protocol-spec.md。LobeHub 仓库中的 @lobechat/chat-adapter-qq 与 apps/server 的 QQ bot 平台适配器即为该协议的落地实现,可作为开发 QQ Bot、SDK 或网关时对照参考。
读完本文你将掌握:AccessToken 获取与平滑刷新、QQBot 鉴权头构造、四种场景下主动/被动消息的发送约束、msg_id+msg_seq 去重机制、Webhook 回调的 Ed25519 签名验证全流程,以及基于仓库源码实现的可直接复用的协议层细节。
1. 协议概览:核心特征与关键地址
QQ 开放平台机器人 API 作为 HTTP/JSON 协议,有四个核心特征:
- 认证方式:使用
appId+clientSecret调用认证服务换取access_token,业务请求鉴权头采用Authorization: QQBot {access_token}格式(前缀是QQBot,不是Bearer)。 - 发送场景:覆盖 QQ 单聊(C2C)、QQ 群聊、文字子频道、频道私信(DMS)四种,接口路径与能力各有差异。
- 接收方式:支持 WebSocket 长连接和 HTTP Webhook 回调两种模式,Webhook 回调地址使用 Ed25519 签名验证。
- 消息类别:分为主动消息与被动回复,两者在频率限制与触发条件上差异显著。
协议涉及的关键地址列表如下:
| 用途 | 地址 |
|---|---|
| 认证服务 | https://bots.qq.com/app/getAppAccessToken |
| API 基座 | https://api.sgroup.qq.com |
| 沙箱 API 基座 | https://sandbox.api.sgroup.qq.com |
仓库中的真实客户端实现与上述地址一一对应,例如 api.ts 中即定义了 AUTH_URL = 'https://bots.qq.com/app/getAppAccessToken' 与 API_BASE_URL = 'https://api.sgroup.qq.com'。
2. 认证流程:获取与使用 AccessToken
2.1 获取 Access Token
Method:POST
URL:https://bots.qq.com/app/getAppAccessToken
Headers
| Header | 是否必需 | 说明 |
|---|---|---|
Content-Type |
是 | 固定 application/json。 |
Request Body
{
"appId": "102012345",
"clientSecret": "aBcDeFgHiJkLmNoPqRsTuVwXyZ123456"
}
字段说明:
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
appId |
string |
是 | 在 QQ 开放平台注册后获得的应用 ID。 |
clientSecret |
string |
是 | 对应的客户端密钥(即 Bot Secret)。 |
Response Body
{
"access_token": "a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK",
"expires_in": "7200"
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
access_token |
string |
访问令牌,后续所有业务 API 请求的鉴权凭证。 |
expires_in |
string |
有效期,单位秒。默认 7200 秒(2 小时)。注意此字段实际返回为字符串类型。 |
curl 示例
curl 'https://bots.qq.com/app/getAppAccessToken' \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{
"appId": "102012345",
"clientSecret": "aBcDeFgHiJkLmNoPqRsTuVwXyZ123456"
}'
工程建议
access_token默认 2 小时过期,不会自动刷新,需开发者手动在过期前重新获取。- 在过期前 60 秒内获取新 token 时,新旧 token 在此 60 秒窗口内均有效,可实现平滑过渡。
- 建议提前 5 分钟刷新 token(即
expires_in - 300秒后刷新),避免临界点竞态。 expires_in虽然语义是数字,实际返回为字符串,解析时注意类型转换。
在仓库的 api.ts 中,QQApiClient.getAccessToken() 正是这套逻辑的落地实现:首次调用时请求认证服务,之后将 token 缓存在 cachedToken 字段,并以 tokenExpiresAt = Date.now() + (data.expires_in - 300) * 1000 记录过期时刻——提前 300 秒(5 分钟)即在缓存失效前重新向认证服务发起请求,与上述工程建议完全一致。
2.2 认证头格式
所有业务 API 请求需携带以下认证头:
Authorization: QQBot {access_token}
注意前缀是 QQBot(不是 Bearer)。旧版 Token 鉴权方式(Bot {token})已废弃,应使用 AccessToken 方式。仓库客户端在每次请求时统一注入该头,见 api.ts 中 call() 方法的 Authorization: \QQBot ${token}`` 拼接。
3. 公共请求规范
3.1 通用请求头
| Header | 示例值 | 是否必需 | 说明 |
|---|---|---|---|
Content-Type |
application/json |
是 | 所有业务接口发 JSON。频道消息接口额外支持 multipart/form-data。 |
Authorization |
QQBot a]kLm4oPqRsT.uVwXyZ_123... |
是 | AccessToken 鉴权。 |
3.2 基座地址
正式环境统一使用 https://api.sgroup.qq.com,拼接方式为 {BASE_URL}{path}。
沙箱环境使用 https://sandbox.api.sgroup.qq.com,接口路径不变。沙箱环境不受频率限制约束,适合调试。
3.3 通用响应格式
成功时 HTTP 状态码为 200(或 201、204),响应体为 JSON。
错误响应结构:
{
"code": 11241,
"message": "invalid appid"
}
关键 HTTP 状态码:
| 状态码 | 含义 | 说明 |
|---|---|---|
200 |
成功 | 正常处理。 |
201/202 |
异步成功 | 需要检查响应体判断是否真正成功。 |
204 |
成功但无响应体 | 常见于删除操作。 |
401 |
认证失败 | Token 无效或过期。 |
404 |
接口不存在 | 检查路径拼写。 |
405 |
方法不允许 | HTTP 方法错误。 |
429 |
频率限制 | 触发速率限制,需退避重试。 |
500/504 |
服务端错误 | 建议有限重试。 |
仓库客户端在响应体无 application/json 时将其按空对象处理,以兼容 204 等无响应体场景(见 api.ts)。
4. 消息发送:四种场景与被动回复约束
QQ 机器人消息发送覆盖四种场景,接口路径和能力略有差异。所有消息接口都要求至少包含一个内容字段(content、markdown、ark、embed、media)。
4.1 消息类型枚举 msg_type
| 值 | 名称 | 说明 |
|---|---|---|
0 |
TEXT | 纯文本消息。对应 content 字段。 |
2 |
MARKDOWN | Markdown 消息。对应 markdown 字段。 |
3 |
ARK | 模板消息(Ark 卡片)。对应 ark 字段。 |
4 |
EMBED | 嵌入消息。对应 embed 字段。仅频道子频道和频道私信支持。 |
7 |
MEDIA | 富媒体消息(图片、视频、语音、文件)。对应 media 字段。 |
仓库在 types.ts 中以 QQ_MSG_TYPE 常量完整复刻了这套枚举(TEXT: 0、MARKDOWN: 2、ARK: 3、EMBED: 4、MEDIA: 7)。
4.2 主动消息与被动回复:决定性约束
QQ 机器人消息分为两类:
被动回复:用户发消息或触发事件后,机器人在限定时间窗口内通过 msg_id 或 event_id 回复。
- 群聊被动回复窗口:5 分钟,每条用户消息最多回复 5 次。
- 单聊被动回复窗口:60 分钟,每条用户消息最多回复 5 次。
- 频道被动回复窗口:5 分钟。
主动消息:机器人在无用户触发的情况下主动下发。受严格频率限制。
- 单聊主动消息:每月每用户 4 条。
- 群聊主动消息:每月每群 4 条。
- 频道主动消息:每日每子频道 20 条,每日最多推送 2 个子频道。
- 频道私信主动消息:每日每用户 2 条,每日累计 200 条。
⚠ 重要限制:主动消息已不可用(2025 年 4 月起)
自 2025 年 4 月 21 日起,QQ 开放平台已全面下线机器人主动消息能力。这意味着:
- 所有消息必须是被动回复:机器人只能在收到用户消息后,通过携带触发消息的
msg_id进行回复。不携带msg_id的发送请求会被服务端以频率限制错误拒绝。- message tool 的
sendMessage操作无法可靠工作:message tool 的执行上下文中不携带触发用户消息的msg_id,因此无法构造合法的被动回复请求。即使 API 调用本身格式正确,也会因缺少msg_id被服务端拒绝。- 实际影响:在服务适配器实现中,
sendMessage已被标记为不支持(unsupported)。所有需要主动发送消息的场景在 QQ 平台上均无法实现。
工程建议
- 优先使用被动回复模式,在收到事件后立即回复。
- 被动回复必须携带
msg_id(回复某条消息)或event_id(回复某个事件),二选一。 - 主动消息不带
msg_id和event_id。 - HTTP 请求超时建议至少设置 5 秒。
4.3 消息去重机制
相同的 msg_id + msg_seq 组合会被服务端视为重复提交并拒绝。msg_seq 用于对同一条用户消息发送多条回复时的序号标识。
- 首条回复
msg_seq可省略(默认1)。 - 多条回复需递增
msg_seq:第 1 条为1,第 2 条为2,依此类推。 - 群聊和单聊场景使用
msg_seq;频道场景不使用。
这一设计意味着当单条回复内容超过长度限制需要分片发送时,多个分片必须以递增的 msg_seq 复用同一个 msg_id,才能被服务端判定为"同一触发消息下的多条回复"。
4.4 发送群聊消息
Method:POST
URL:https://api.sgroup.qq.com/v2/groups/{group_openid}/messages
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
group_openid |
string |
群组的 openid,从 GROUP_AT_MESSAGE_CREATE 事件中获取。 |
Request Body
{
"content": "你好,这是一条群聊消息。",
"msg_id": "xxxxxx",
"msg_seq": 1,
"msg_type": 0
}
字段说明:
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
content |
string |
条件必需 | 文本内容。msg_type=0 时必填。 |
msg_type |
int |
是 | 消息类型,见第 4.1 节。 |
markdown |
object |
条件必需 | Markdown 对象。msg_type=2 时必填。 |
ark |
object |
条件必需 | Ark 模板对象。msg_type=3 时必填。 |
media |
object |
条件必需 | 富媒体 file_info 对象。msg_type=7 时必填。 |
msg_id |
string |
否 | 被动回复时填入触发消息的 ID。 |
event_id |
string |
否 | 被动回复时填入触发事件的 ID。与 msg_id 二选一。 |
msg_seq |
int |
否 | 回复序号,默认 1。多条回复时递增。 |
Response Body
{
"id": "xxxxxx",
"timestamp": "1677000000"
}
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
发送成功后的消息 ID。 |
timestamp |
string |
消息发送时间戳。 |
curl 示例(被动回复文本)
curl 'https://api.sgroup.qq.com/v2/groups/A1B2C3D4E5F6/messages' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"content": "你好,这是一条群聊消息。",
"msg_type": 0,
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_seq": 1
}'
curl 示例(被动回复 Markdown)
curl 'https://api.sgroup.qq.com/v2/groups/A1B2C3D4E5F6/messages' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"msg_type": 2,
"markdown": {
"content": "# 标题\n这是一段 **Markdown** 内容。"
},
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_seq": 1
}'
在仓库的 api.ts 中,sendGroupMessage(groupOpenId, content, options) 接收可选的 eventId、msgId、msgSeq,并按需写入 event_id / msg_id / msg_seq 字段——这正好呼应了协议中"被动回复必须携带触发标识"的要求。
4.5 发送频道子频道消息
Method:POST
URL:https://api.sgroup.qq.com/channels/{channel_id}/messages
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
channel_id |
string |
子频道 ID,从 AT_MESSAGE_CREATE 或 MESSAGE_CREATE 事件中获取。 |
Request Body
{
"content": "你好,这是一条频道消息。",
"msg_id": "xxxxxx"
}
字段说明:
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
content |
string |
条件必需 | 文本内容,支持内嵌格式(@用户、# 子频道、表情)。 |
embed |
MessageEmbed |
条件必需 | 嵌入消息对象。 |
ark |
MessageArk |
条件必需 | Ark 模板对象。 |
markdown |
MessageMarkdown |
条件必需 | Markdown 对象。 |
image |
string |
条件必需 | 图片 URL,平台会自动转存。 |
file_image |
file |
条件必需 | 图片文件,仅 multipart/form-data 格式时可用。 |
message_reference |
MessageReference |
否 | 引用消息对象。 |
msg_id |
string |
否 | 被动回复时填入触发消息 ID。 |
event_id |
string |
否 | 被动回复时填入触发事件 ID。 |
注意:content、embed、ark、image/file_image、markdown 至少需要提供一个。
Response Body
频道消息返回完整的 Message 对象:
{
"author": {
"id": "11586990140073229091",
"username": "mybot",
"avatar": "https://thirdqq.qlogo.cn/g?...",
"bot": true
},
"channel_id": "123456",
"content": "你好,这是一条频道消息。",
"guild_id": "789012",
"id": "xxxxxx",
"timestamp": "2026-03-22T04:25:12+08:00"
}
curl 示例
curl 'https://api.sgroup.qq.com/channels/123456/messages' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"content": "你好,这是一条频道消息。",
"msg_id": "08e09a8305ac3e5d141a1490"
}'
仓库中的 sendGuildMessage(channelId, content, options) 即对应此接口(api.ts),路径为 /channels/${channelId}/messages,注意它是频道体系下的 v1 风格路径,无需 msg_seq。
频道消息审核
频道消息可能触发内容审核,此时返回错误码 304023 或 304024,响应体包含 audit_id 字段。审核通过后消息会自动发出,审核不通过则不会发出。
4.6 发送单聊消息(C2C)
Method:POST
URL:https://api.sgroup.qq.com/v2/users/{openid}/messages
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
openid |
string |
用户的 openid,从 C2C_MESSAGE_CREATE 事件中获取。 |
Request Body
{
"content": "你好,这是一条私聊消息。",
"msg_id": "xxxxxx",
"msg_seq": 1,
"msg_type": 0
}
字段说明与群聊消息一致(见第 4.4 节),额外支持:
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
is_wakeup |
bool |
否 | 互动召回消息标记,仅单聊场景支持。 |
Response Body
{
"id": "xxxxxx",
"timestamp": "1677000000"
}
curl 示例
curl 'https://api.sgroup.qq.com/v2/users/F6A8B9C0D1E2/messages' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"content": "你好,这是一条私聊消息。",
"msg_type": 0,
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_seq": 1
}'
仓库中的 sendC2CMessage(openId, content, options) 支持完整的 msgId / eventId / msgSeq 三个可选参数(api.ts)。
单聊互动召回消息
用户主动对话后 30 天内,机器人可在以下四个阶段各下发 1 条互动召回消息:当天、1-3 天、3-7 天、7-30 天。发送时设置 is_wakeup: true。
4.7 发送频道私信消息(DMS)
频道私信需先通过 POST /users/@me/dms 创建私信会话获取 guild_id,然后发送消息。机器人与用户必须存在共同频道。
4.7.1 创建私信会话
Method:POST
URL:https://api.sgroup.qq.com/users/@me/dms
Request Body
{
"recipient_id": "11586990140073229091",
"source_guild_id": "789012"
}
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
recipient_id |
string |
是 | 目标用户 ID。 |
source_guild_id |
string |
是 | 源频道 ID,机器人与用户的共同频道。 |
Response Body
{
"channel_id": "xxxxxx",
"create_time": "1677000000",
"guild_id": "xxxxxx"
}
4.7.2 发送私信消息
Method:POST
URL:https://api.sgroup.qq.com/dms/{guild_id}/messages
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
guild_id |
string |
创建私信会话返回的 guild_id,也可从 DIRECT_MESSAGE_CREATE 事件中获取。 |
Request Body
{
"content": "你好,这是一条频道私信。",
"msg_id": "xxxxxx"
}
字段与频道消息一致(见第 4.5 节),支持 content、embed、ark、markdown、image、message_reference、msg_id、event_id。
Response Body
返回完整 Message 对象,结构与频道消息响应一致。
curl 示例
curl 'https://api.sgroup.qq.com/dms/DMS_GUILD_ID/messages' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"content": "你好,这是一条频道私信。",
"msg_id": "08e09a8305ac3e5d141a1490"
}'
仓库中的 sendDmsMessage(guildId, content, options) 对应 /dms/${guildId}/messages(api.ts)。需要注意 DMS 与 guild(子频道)不同:DMS 的目标参数是创建私信会话得到的 guild_id。
4.8 文本长度限制与分片策略
协议没有在响应中显式下发"最大文本长度"字段。社区实现和已有代码普遍以 2000 个 Unicode 字符作为保守兼容上限。
在仓库中,这个 2000 字符上限被定义为常量 MAX_TEXT_LENGTH = 2000,发送前由 truncateText() 统一处理——超长时截断为 MAX_TEXT_LENGTH - 3 个字符并追加 ...(见 api.ts 与 api.ts)。这是"发送端兜底截断"的实现;要完整保留内容,仍需在业务层主动分片。
推荐分片算法
- 以 2000 个 Unicode 字符为上限。
- 优先在
\n\n、其次在\n、再次在空格处分片。 - 找不到合适边界时,硬切 2000 字符,末尾附加
...。 - 每个分片单独调用一次消息发送接口。
- 多条回复递增
msg_seq,复用同一个msg_id。
4.9 频率限制汇总
| 场景 | 类型 | 限制 |
|---|---|---|
| 群聊被动回复 | 每条消息 | 5 分钟窗口,最多 5 条 |
| 群聊主动消息 | 每群每月 | 4 条 |
| 单聊被动回复 | 每条消息 | 60 分钟窗口,最多 5 条 |
| 单聊主动消息 | 每用户每月 | 4 条 |
| 频道子频道 | 全局 | 每秒每子频道 5 条 |
| 频道子频道主动消息 | 每日 | 每子频道 20 条,每日最多 2 个子频道 |
| 频道被动回复 | 每条消息 | 5 分钟窗口 |
| 频道私信主动消息 | 每日 | 每用户 2 条,累计 200 条 |
| 频道私信被动回复 | 每条消息 | 5 分钟窗口 |
工程建议
- 沙箱环境不受上述频率限制约束。
- 被动回复场景下,应在收到用户消息后尽快回复,避免超时。
- 需要发送 WebSocket 连接才可以发送频道消息(频道 API 的前提要求)。
5. 获取机器人信息
Method:GET
URL:https://api.sgroup.qq.com/users/@me
Headers:使用第 3 节的通用请求头。
Request Body:无。
Response Body
{
"avatar": "https://thirdqq.qlogo.cn/g?b=oidb&k=M5TibpXicS7Jt4z89BZxiamAA&s=100&t=1641802698",
"id": "11586990140073229091",
"union_openid": "74F138F7F3AF68C4B8E8325013FCA295",
"union_user_account": "",
"username": "gitsub"
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
机器人用户 ID。 |
username |
string |
机器人显示名称。 |
avatar |
string |
头像 URL。 |
union_openid |
string |
特约商户下的用户跨应用唯一标识(非特约商户为空)。 |
union_user_account |
string |
联合用户账号(可能为空)。 |
curl 示例
curl 'https://api.sgroup.qq.com/users/@me' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK'
仓库适配器在初始化时调用该接口获取机器人自身用户名与 ID(见 adapter.ts 中 initialize() 的逻辑:先校验 AccessToken,再尝试拉取 bot 信息以确定 userName 与 botUserId)。getBotInfo() 的具体实现位于 api.ts,它将该接口标为可选能力——即使失败也不阻塞整体初始化。
6. 消息数据结构
6.1 群聊 / 单聊消息事件载荷
群聊和单聊使用 v2 API 事件结构。
GROUP_AT_MESSAGE_CREATE 事件 d 字段:
{
"author": {
"member_openid": "F6A8B9C0D1E2"
},
"content": "/help 查看帮助",
"group_openid": "A1B2C3D4E5F6",
"id": "08e09a8305ac3e5d141a1490",
"timestamp": "2026-03-22T04:25:12+08:00"
}
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
消息 ID,用于被动回复时回传。 |
author.member_openid |
string |
发送者在群内的成员 openid。 |
content |
string |
消息文本内容(@机器人 前缀已去除)。 |
group_openid |
string |
群组 openid。 |
timestamp |
string |
消息创建时间,RFC 3339 格式。 |
C2C_MESSAGE_CREATE 事件 d 字段:
{
"attachments": [],
"author": {
"user_openid": "F6A8B9C0D1E2"
},
"content": "你好",
"id": "08e09a8305ac3e5d141a1490",
"timestamp": "2026-03-22T04:25:12+08:00"
}
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
消息 ID。 |
author.user_openid |
string |
发送者的 openid。 |
content |
string |
消息文本内容。 |
timestamp |
string |
消息创建时间。 |
attachments |
array |
附件数组(图片、语音、视频、文件)。 |
需要注意事件结构与 6.1 节响应中作者字段命名上的差异:事件里的 author 使用 member_openid / user_openid,而响应/频道消息的 author 使用统一 id。这也是 QQ v2 中 openid 体系的直接体现——openid 本身就是发送目标路径参数的来源。
6.2 频道消息事件载荷
频道消息使用 Message 对象结构。
AT_MESSAGE_CREATE / MESSAGE_CREATE 事件 d 字段:
{
"author": {
"id": "11586990140073229091",
"username": "testuser",
"avatar": "https://...",
"bot": false
},
"channel_id": "123456",
"content": "<@!botid> 你好",
"guild_id": "789012",
"id": "xxxxxx",
"member": {
"joined_at": "2025-01-01T00:00:00+08:00",
"roles": ["1"]
},
"mentions": [
{
"id": "botid",
"username": "mybot",
"avatar": "https://...",
"bot": true
}
],
"seq": 123,
"seq_in_channel": "456",
"timestamp": "2026-03-22T04:25:12+08:00"
}
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
消息 ID。 |
channel_id |
string |
子频道 ID。 |
guild_id |
string |
频道 ID。 |
content |
string |
消息内容,@ 使用 <@!userid> 格式。 |
timestamp |
string |
消息创建时间,RFC 3339 格式。 |
author |
object |
发送者信息(id、username、avatar、bot)。 |
member |
object |
成员信息(joined_at、roles)。 |
mentions |
array |
被 @ 的用户数组。 |
seq |
number |
消息序列号,用于排序。 |
seq_in_channel |
string |
频道内序列号。 |
attachments |
array |
附件数组(包含 url、content_type、filename 等)。 |
message_reference |
object |
引用消息对象(包含 message_id)。 |
6.3 附件结构 Attachment
{
"content_type": "image/png",
"filename": "image.png",
"height": 600,
"size": 248736,
"url": "https://multimedia.nt.qq.com.cn/xxx",
"width": 800
}
| 字段 | 类型 | 说明 |
|---|---|---|
url |
string |
附件下载 URL。 |
content_type |
string |
MIME 类型。 |
filename |
string |
文件名。 |
height |
number? |
图片 / 视频高度。 |
width |
number? |
图片 / 视频宽度。 |
size |
number |
文件大小,字节。 |
仓库类型定义 types.ts 中的 QQAttachment 与 QQRawMessage(types.ts)基本逐字段复刻了上述结构。值得注意的工程细节是:QQ 的 content_type 并不总是合法 MIME——C2C 文件附件可能返回粗粒度类别标签 "file",因此在适配器的 resolveMimeType() 中,若 content_type 不含 /,会回退到基于文件名(通过 mime.getType)推断 MIME(见 adapter.ts)。
QQ 附件是各平台中最易处理的一种:它们带公网 CDN URL(如 https://multimedia.nt.qq.com.cn/...),无需鉴权即可直接下载。在 client.ts 的 qqExtractFiles() 中,入站附件被直接提取为 URL 形式的 AttachmentSource,交给下游 ingestAttachment 用普通 fetch() 拉取即可。
7. 消息类型详解
7.1 TEXT(msg_type = 0)
最简单的消息类型,直接在 content 字段填入文本内容。
{
"content": "你好,世界!",
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_type": 0
}
频道消息中的 content 支持内嵌格式:
| 格式 | 说明 | 示例 |
|---|---|---|
<@!user_id> |
@某用户 | <@!11586990140073229091> |
<#channel_id> |
引用子频道 | <#123456> |
<emoji:id> |
表情 | <emoji:4> |
7.2 MARKDOWN(msg_type = 2)
Markdown 消息有两种使用方式:
方式一:自定义 Markdown(原生内容)
{
"markdown": {
"content": "# 标题\n## 简介\n这是一段 **加粗** 和 *斜体* 的文本。\n- 列表项 1\n- 列表项 2"
},
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_type": 2
}
方式二:模板 Markdown
{
"markdown": {
"custom_template_id": "template_id_12345",
"params": [
{ "key": "title", "values": ["我的标题"] },
{ "key": "content", "values": ["正文内容"] }
]
},
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_type": 2
}
MessageMarkdown 结构:
| 字段 | 类型 | 说明 |
|---|---|---|
content |
string? |
原生 Markdown 文本。与 custom_template_id 二选一。 |
custom_template_id |
string? |
模板 ID,在平台申请后获得。 |
params |
array? |
模板变量的键值映射数组,每项包含 key 和 values。 |
支持的 Markdown 语法:
| 语法 | 说明 |
|---|---|
#、## |
标题 |
**text** |
加粗 |
__text__ |
下划线加粗 |
_text_、*text* |
斜体 |
~~text~~ |
删除线 |
text |
链接 |
text #widthpx #heightpx |
图片(需公网可访问 URL) |
1.、- |
有序 / 无序列表 |
> |
引用 |
*** |
分割线 |
工程建议
- 自定义 Markdown 在沙箱环境可直接使用;正式环境需要邀请开通。
- 开通模板 MD 或原生 MD 后,主动 MD 默认生效,被动 MD 仍需单独申请。
- Markdown 内的图片资源必须使用公网可访问的 URL。
7.3 ARK(msg_type = 3)
Ark 是 QQ 机器人的模板卡片消息。平台默认提供三个模板:
| 模板 ID | 说明 |
|---|---|
23 |
链接 + 文本列表布局 |
24 |
文本 + 缩略图布局 |
37 |
大图布局 |
MessageArk 结构:
| 字段 | 类型 | 说明 |
|---|---|---|
template_id |
int |
模板 ID。 |
kv |
array |
模板变量键值对数组。 |
kv 数组元素结构:
| 字段 | 类型 | 说明 |
|---|---|---|
key |
string |
变量名,通常以 # 包围(如 #DESC#)。 |
value |
string? |
变量值(简单类型)。 |
obj |
array? |
变量值(数组类型),元素为 { obj_kv: [{key, value}] }。 |
{
"ark": {
"template_id": 23,
"kv": [
{ "key": "#DESC#", "value": "描述信息" },
{ "key": "#PROMPT#", "value": "通知摘要" },
{
"key": "#LIST#",
"obj": [
{ "obj_kv": [{ "key": "desc", "value": "项目 1" }] },
{ "obj_kv": [{ "key": "desc", "value": "项目 2" }] }
]
}
]
},
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_type": 3
}
工程建议
- 主动 Ark 消息默认可用,被动 Ark 消息需要达到准入条件并向平台申请。
- 自定义 Ark 模板需在管理后台申请。
7.4 EMBED(msg_type = 4)
Embed 是一种特殊的 Ark 消息,仅在频道子频道和频道私信场景下可用。群聊和单聊不支持。
MessageEmbed 结构:
| 字段 | 类型 | 说明 |
|---|---|---|
title |
string |
标题。 |
prompt |
string |
消息通知文本(展示在推送通知中)。 |
thumbnail |
object? |
缩略图对象,包含 url 字段。 |
fields |
array |
内容项数组,每项包含 name 字段。 |
{
"embed": {
"title": "等级通知",
"prompt": "你的等级已更新",
"thumbnail": { "url": "https://example.com/thumb.png" },
"fields": [{ "name": "当前等级:黄金" }, { "name": "之前等级:白银" }, { "name": "继续努力" }]
},
"msg_id": "08e09a8305ac3e5d141a1490"
}
7.5 MEDIA(msg_type = 7)
富媒体消息(图片、视频、语音、文件)需要先上传到 QQ 服务器获取 file_info,再在发送消息时通过 media 字段引用。
上传接口
| 场景 | 接口 |
|---|---|
| 单聊 | POST /v2/users/{openid}/files |
| 群聊 | POST /v2/groups/{group_openid}/files |
Request Body
{
"file_type": 1,
"srv_send_msg": false,
"url": "https://example.com/image.png"
}
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
file_type |
int |
是 | 1 = 图片(png/jpg),2 = 视频(mp4),3 = 语音(silk/wav/mp3/flac),4 = 文件(群聊不可用)。 |
url |
string |
是 | 媒体资源的公网 URL。 |
srv_send_msg |
bool |
是 | true = 上传并直接发送(消耗主动消息配额);false = 仅上传,返回 file_info 供后续引用。 |
file_data |
string? |
否 | base64 编码的文件二进制数据(替代 URL 方式)。 |
Response Body
{
"file_info": "xxxxxx",
"file_uuid": "xxxxxx",
"id": "xxxxxx",
"ttl": 259200
}
| 字段 | 类型 | 说明 |
|---|---|---|
file_uuid |
string |
文件唯一标识符。 |
file_info |
string |
用于消息发送的文件引用信息。 |
ttl |
int |
文件有效期,秒。0 表示长期可用。 |
id |
string? |
消息 ID(仅 srv_send_msg=true 时返回)。 |
仓库中 api.ts 的 uploadGroupRichMedia() 与 uploadC2CRichMedia() 即封装了这两个上传接口,且固定使用 srv_send_msg: false 的"先上传、后发送"策略,符合协议文档"推荐设置 srv_send_msg=false"的工程建议。
引用上传文件发送消息
{
"media": {
"file_info": "xxxxxx"
},
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_seq": 1,
"msg_type": 7
}
curl 示例(上传图片)
curl 'https://api.sgroup.qq.com/v2/groups/A1B2C3D4E5F6/files' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"file_type": 1,
"url": "https://example.com/image.png",
"srv_send_msg": false
}'
curl 示例(发送已上传的图片)
curl 'https://api.sgroup.qq.com/v2/groups/A1B2C3D4E5F6/messages' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: QQBot a]kLm4oPqRsT.uVwXyZ_1234567890ABCdEfGhIjK' \
--data-raw '{
"msg_type": 7,
"media": {
"file_info": "xxxxxx"
},
"msg_id": "08e09a8305ac3e5d141a1490",
"msg_seq": 1
}'
发送富媒体消息对应仓库中的 sendGroupMedia() / sendC2CMedia()(api.ts)。实现细节值得注意:由于同一条消息的 msg_type 要么是 7(MEDIA)要么是 0(TEXT)、不可兼得,调用方需要把文本与媒体分两条消息发送(媒体消息的 content 以 ' ' 占位)。
工程建议
- 通过
/v2/users/{openid}/files上传的文件只能发到单聊,通过/v2/groups/{group_openid}/files上传的文件只能发到群聊。不可跨场景使用。 - 单个
file_info可复用于多个接收者。 - 开发者需自行管理
file_info的过期时间(根据ttl字段)。 - 推荐设置
srv_send_msg=false,先获取file_info,再通过消息发送接口发送。 - 频道消息的图片可直接通过
image字段传入 URL,由平台自动转存,无需单独上传。
在服务端实际发送场景中,sendAttachments.ts 组合了"上传 file_info → 按 MEDIA 发送 + 文本另发"的完整链路,与上述协议流程一致。
7.6 引用消息 MessageReference
仅频道子频道和频道私信支持。
{
"message_reference": {
"message_id": "xxxxxx",
"ignore_get_message_error": true
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
message_id |
string |
被引用的消息 ID。 |
ignore_get_message_error |
bool? |
是否忽略引用消息获取失败错误,默认 false。 |
8. Webhook 事件接收
8.1 通信载荷结构
所有 Webhook 和 WebSocket 消息使用统一格式:
{
"d": {},
"id": "event_id_xxx",
"op": 0,
"s": 42,
"t": "GROUP_AT_MESSAGE_CREATE"
}
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
事件标识符。 |
op |
int |
操作码,见下表。 |
d |
object |
事件数据,结构随事件类型变化。 |
s |
number? |
序列号,用于消息唯一性判断。 |
t |
string? |
事件类型名称,仅 op=0 时存在。 |
8.2 操作码 OpCode
| 代码 | 名称 | 方向 | 说明 |
|---|---|---|---|
0 |
DISPATCH | 服务端 -> 客户端 | 事件推送。 |
12 |
HTTP_CALLBACK_ACK | 客户端 -> 服务端 | 确认收到事件回调。 |
13 |
CALLBACK_VERIFY | 服务端 -> 客户端 | 回调地址验证请求。 |
仓库在 types.ts 中将上述 Webhook OpCode 定义为 QQ_OP_CODES 常量(DISPATCH: 0、HTTP_CALLBACK_ACK: 12、VERIFY: 13),WebSocket 网关则另行维护一套 OpCode(QQ_WS_OP_CODES,含 HEARTBEAT: 1、IDENTIFY: 2、RESUME: 6、RECONNECT: 7、INVALID_SESSION: 9、HELLO: 10、HEARTBEAT_ACK: 11),位于同一文件的 types.ts。
8.3 回调地址验证(op = 13)
配置 Webhook 回调地址时,平台会发送验证请求。
请求 Headers
| Header | 说明 |
|---|---|
User-Agent |
QQBot-Callback |
X-Bot-Appid |
机器人 appId |
请求 Body
{
"d": {
"plain_token": "xxxxxx",
"event_ts": "1677000000"
},
"op": 13
}
响应 Body
机器人服务端需返回:
{
"plain_token": "xxxxxx",
"signature": "hex_encoded_ed25519_signature"
}
签名计算过程:
- 将 Bot Secret 字符串重复填充至 32 字节(Ed25519 seed 长度)。
- 用该 seed 生成 Ed25519 密钥对。
- 拼接签名体:
event_ts + plain_token。 - 用私钥签名,结果转十六进制编码。
工程建议
- 回调地址仅支持 80、443、8080、8443 端口。
- 必须使用 HTTPS。
- 验证请求的
plain_token需原样返回。
8.4 事件签名验证
接收到事件推送后,应验证请求签名:
- 从 HTTP Header 获取
X-Signature-Ed25519(十六进制编码的签名)。 - 从 HTTP Header 获取
X-Signature-Timestamp(时间戳)。 - 拼接签名体:
X-Signature-Timestamp + HTTP Body。 - 用步骤 8.3 中生成的公钥调用
ed25519.Verify验证签名。
8.5 事件回调确认
收到事件后,机器人服务端应返回:
{
"d": 0,
"op": 12
}
HTTP 状态码 200。
8.6 事件类型与 Intent
| 事件类型 | Intent | 说明 |
|---|---|---|
C2C_MESSAGE_CREATE |
GROUP_AND_C2C_EVENT (1 << 25) |
用户单聊消息。 |
GROUP_AT_MESSAGE_CREATE |
GROUP_AND_C2C_EVENT (1 << 25) |
用户在群内 @机器人。 |
AT_MESSAGE_CREATE |
PUBLIC_GUILD_MESSAGES (1 << 30) |
频道内 @机器人(公域)。 |
MESSAGE_CREATE |
GUILD_MESSAGES (1 << 9) |
频道全量消息(私域,需权限)。 |
DIRECT_MESSAGE_CREATE |
DIRECT_MESSAGE (1 << 12) |
频道私信消息。 |
Intent 计算
多个事件组合使用按位或:
intents = (1 << 25) | (1 << 30) | (1 << 12)
仓库在 types.ts 中定义了一整套 QQ_INTENTS 常量,除上述事件外还覆盖 GUILDS(1<<0)、GUILD_MEMBERS(1<<1)、GUILD_MESSAGE_REACTIONS(1<<10)、MESSAGE_AUDIT(1<<27)、INTERACTION(1<<26) 等网关能力位,便于按需组合。
工程建议
- 为保证消息送达,平台可能重复推送相同消息 ID 的事件。开发者应基于消息 ID 做去重,避免重复回复。
GROUP_AND_C2C_EVENT(1 << 25) 是群聊和单聊场景的 Intent。PUBLIC_GUILD_MESSAGES(1 << 30) 是公域频道 @机器人 的 Intent。GUILD_MESSAGES(1 << 9) 需要私域权限,一般不使用。- 未授权的 Intent 会导致连接被断开。
在仓库的适配器中,事件接收与上述协议流程是逐条对应的:handleWebhook()(adapter.ts)首先判断 payload.op——VERIFY(13) 时调用 signWebhookResponse() 完成地址验证;DISPATCH(0) 时仅处理消息类事件(isMessageEvent() 只放行 GROUP_AT_MESSAGE_CREATE / C2C_MESSAGE_CREATE / AT_MESSAGE_CREATE / DIRECT_MESSAGE_CREATE 四类),并将事件内容与附件为空的事件直接跳过。
9. Ed25519 签名算法实现
9.1 Seed 派生
将 Bot Secret 重复拼接直到达到 32 字节:
secret = "naOC0ocQE3shWLAfffVLB1rhYPG7" (28 字节)
seed = "naOC0ocQE3shWLAfffVLB1rhYPG7naOC" (32 字节)
9.2 TypeScript 实现示例
import crypto from 'node:crypto';
function deriveEd25519Seed(secret: string): Buffer {
const seed = Buffer.alloc(32);
const secretBuf = Buffer.from(secret, 'utf8');
for (let i = 0; i < 32; i++) {
seed[i] = secretBuf[i % secretBuf.length];
}
return seed;
}
function signCallback(secret: string, eventTs: string, plainToken: string): string {
const seed = deriveEd25519Seed(secret);
const keyPair = crypto.sign(null, Buffer.from(eventTs + plainToken), {
key: crypto.createPrivateKey({
format: 'der',
type: 'pkcs8',
// Ed25519 PKCS8 DER prefix + seed
key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]),
}),
});
return keyPair.toString('hex');
}
仓库中 crypto.ts 的 signWebhookResponse() 是本算法可运行的完整实现,并与上述示例存在两处等价但略有差异的细节:
- seed 填充策略:将
clientSecret通过repeat(2)不断翻倍直至长度不小于 32,再slice(0, 32)截取——与"逐字节循环填充"的结果一致; - PKCS8 DER 前缀:
ED25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex')被单独提取为常量,其 ASN.1 结构为SEQUENCE { INTEGER 0, SEQUENCE { OID 1.3.101.112 (Ed25519) }, OCTET STRING { OCTET STRING { <32 字节 seed> } } },Node.js 会从该 seed 自动派生出对应的 Ed25519 公钥/私钥,无需显式引入公钥文件。
10. 错误码参考表
10.1 HTTP 层
| 状态码 | 含义 | 处理建议 |
|---|---|---|
200 |
成功 | 正常处理。 |
201/202 |
异步成功 | 检查响应体。 |
204 |
成功无响应体 | 常见于删除操作。 |
401 |
认证失败 | 检查 Token,可能需要刷新。 |
404 |
接口不存在 | 检查 URL 拼写。 |
405 |
方法不允许 | 检查 HTTP 方法。 |
429 |
频率限制 | 退避重试。 |
500/504 |
服务端错误 | 有限重试,建议最多 3 次。 |
10.2 业务层
| 错误码范围 | 类别 | 说明 |
|---|---|---|
10001-11275 |
账号认证 | appId 无效、Token 校验失败等。Token 校验失败一般重试一次即可。 |
20028 |
消息限频 | 超出频率限制。 |
50006 |
消息为空 | 消息体缺少有效内容。 |
50045 |
回复限制 | 不允许回复机器人自己的消息。 |
301000-610014 |
频道权限 | 无效频道 / 子频道 ID、权限不足等。 |
304023/304024 |
内容审核 | 消息进入审核,响应包含 audit_id。 |
620001-639999 |
安全过滤 | 内容包含敏感信息。 |
1100100-1100499 |
内容违规 | 敏感内容检测(如 1100101 表示 "内容涉及敏感")。 |
3000000-3999999 |
消息编辑 | 编辑限制(如不能修改非机器人消息)。 |
工程建议
- 认证失败(
401或10001系列)应触发 Token 刷新,然后重试。 - 频率限制(
429或20028)应按指数退避策略重试。 - 内容审核(
304023/304024)不应重试,等待审核结果即可。 - URL 包含的消息需在管理后台预先配置白名单,否则可能被拦截。
11. 完整消息流程图
从"用户 @机器人"到"机器人回复"的完整时序如下——这也是 QQ Bot 最常见的被动回复闭环,其中 WebSocket/Webhook 两种接收方式会在此汇合:平台负责把网关事件送达,机器人侧统一进入消息处理管线后再调用发送 API 应答:
sequenceDiagram
participant U as QQ 用户
participant Q as QQ 开放平台
participant B as Bot 服务端
participant A as Agent/LLM
Note over B: 启动时获取 AccessToken
B->>Q: POST /app/getAppAccessToken<br/>appId + clientSecret
Q-->>B: access_token, expires_in
U->>Q: 在群内 @机器人 发消息
Q->>B: Webhook POST (op=0)<br/>t=GROUP_AT_MESSAGE_CREATE<br/>d={id, content, group_openid, author}
B->>Q: 返回 {op: 12, d: 0} 确认
B->>A: 转发消息内容给 Agent 处理
A-->>B: 返回回复文本
alt 回复纯文本
B->>Q: POST /v2/groups/{group_openid}/messages<br/>msg_type=0, content, msg_id
else 回复富媒体
B->>Q: POST /v2/groups/{group_openid}/files<br/>file_type=1, url, srv_send_msg=false
Q-->>B: file_info
B->>Q: POST /v2/groups/{group_openid}/messages<br/>msg_type=7, media={file_info}, msg_id
end
Q-->>B: {id, timestamp}
Q-->>U: 用户在群内看到回复
在 LobeHub 网关实现中,这条流程对应 client.ts 的 QQGatewayClient:启动时用 appId + clientSecret 创建 QQAdapter 并建立 Chat 实例,随后 startGatewayListener() 以固定的 8 小时时长(DEFAULT_DURATION_MS = 8 * 60 * 60 * 1000)拉起监听;无自定义 options 时到期会自动重启(this.start())以维持长连接,并同步向 Redis 上报 starting → connected / failed / disconnected 的运行状态。仓库同时支持纯 Webhook 模式的 QQWebhookClient(client.ts),其启动仅做一次 AccessToken 凭据校验。两种模式的取舍通过 settings.connectionMode 决定,默认值见 const.ts 中的 DEFAULT_QQ_CONNECTION_MODE = 'websocket',创建逻辑见 client.ts。
网关的 WebSocket 实现还维持着另一套更底层的 OpCode 交互(HELLO → IDENTIFY → HEARTBEAT/HEARTBEAT_ACK、断线 RESUME/RECONNECT),核心逻辑位于 gateway.ts,并通过 QQ_WS_OP_CODES 常量在 types.ts 中集中定义。
12. 与 Telegram、微信 iLink 的协议对比
| 维度 | Telegram Bot API | 微信 iLink Bot API | QQ 开放平台 Bot API |
|---|---|---|---|
| 收消息方式 | getUpdates 或 Webhook |
getupdates 长轮询 |
WebSocket 或 HTTP Webhook |
| 发送目标 | chat_id |
to_user_id + context_token |
group_openid / openid / channel_id / DMS guild_id |
| 对话关联主键 | chat_id 足够 |
context_token 是关键 |
路径参数中的 openid/channel_id 即可 |
| 主动发消息能力 | 只要知道 chat_id 通常即可 |
依赖最近入站 context_token |
有严格月度 / 日度配额限制 |
| 被动回复窗口 | 无限制 | 无明确公开限制 | 群聊 5 分钟 / 单聊 60 分钟 |
| 消息去重 | message_id 自动处理 |
client_id 避免重复 |
msg_id + msg_seq 去重 |
| 媒体上传 | multipart/form-data | AES-128-ECB + CDN 上传 | URL 转存或 base64 上传,返回 file_info |
| 认证方式 | Bot Token(永久) | 二维码扫码获取 Bot Token | appId + clientSecret 获取 AccessToken(2h 过期) |
| 签名验证 | 无 | 无 | Ed25519 签名 |
| 内容审核 | 无强制 | 无公开审核 | 平台内置审核,可能返回 audit_id |
总结:QQ 开放平台的消息模型比 Telegram 更受限(主动消息有配额、被动回复有时间窗口),但比微信 iLink 更标准化(不需要维护 context_token,使用标准 RESTful 路径参数寻址)。签名验证使用 Ed25519 算法,是三者中安全机制最完整的。开发者应优先使用被动回复模式,合理管理 AccessToken 生命周期,并在消息发送时正确设置 msg_id + msg_seq 以避免去重拒绝。
13. 仓库实测:服务适配器的能力边界
LobeHub 在 service.ts 中实现了 QQMessageService(message tool 的 QQ 服务适配器),并记录了与 QQ 开放平台 API 对接的实测结论。
13.1 验证方式
QQ API 通过代码审查与官方文档对照进行验证,确认各接口的请求 / 响应格式与官方规范一致。
13.2 关键发现
| 发现 | 说明 |
|---|---|
sendMessage 不可用 |
自 2025 年 4 月起,QQ 已下线主动消息能力。所有消息必须作为被动回复发送,需携带触发用户消息的 msg_id。message tool 的执行上下文不携带 msg_id,因此 sendMessage 在服务适配器中被标记为不支持(unsupported)。 |
| API 能力极度受限 | QQ 开放平台仅提供消息发送相关 API,不提供消息历史读取、消息编辑、消息删除、表情回复、频道信息查询、成员信息查询等能力。这使得 message tool 的 17 项操作中几乎全部无法实现。 |
channelId 格式约定 |
服务适配器使用 "type:id" 格式编码 channelId,以区分不同的消息场景。例如:"group:A1B2C3D4E5F6"(群聊)、"guild:789012"(频道)、"c2c:F6A8B9C0D1E2"(单聊)、"dms:DMS_GUILD_ID"(频道私信)。 |
上述 channelId 格式约定在 service.ts 的 parseChannelId() 中被解析回 targetId + threadType,无类型前缀时默认回退为 group。协议侧线程 ID 的完整编码是 qq:{type}:{id}(频道类为 qq:{type}:{id}:{guildId}),见 adapter.ts 的 encodeThreadId() / decodeThreadId()。
13.3 服务适配器操作支持情况
服务适配器支持 0 / 17 项 message tool 操作(sendMessage 因缺少 msg_id 上下文而标记为不支持):
| 操作 | 支持 | 原因 |
|---|---|---|
sendMessage |
No | 需要 msg_id 进行被动回复,tool 上下文不提供 |
readMessages |
No | QQ API 无消息历史读取接口 |
editMessage |
No | QQ API 无消息编辑接口 |
deleteMessage |
No | QQ API 无消息撤回接口(仅频道有撤回,群聊 / 单聊无) |
reactToMessage |
No | QQ API 无表情回复接口(仅频道有表情表态) |
getChannelInfo |
No | QQ API 无通用频道信息查询接口 |
getMemberInfo |
No | QQ API 无成员信息查询接口 |
replyToThread |
No | QQ API 无话题回复机制 |
searchMessages |
No | QQ API 无消息搜索接口 |
getReactions |
No | QQ API 无表情回复查询接口 |
pinMessage |
No | QQ API 无消息置顶接口(仅频道有) |
unpinMessage |
No | QQ API 无取消置顶接口 |
getPinnedMessages |
No | QQ API 无置顶消息查询接口 |
listChannels |
No | QQ API 无通用频道列表接口 |
createThread |
No | QQ API 无话题创建接口 |
listThreads |
No | QQ API 无话题列表接口 |
createPoll |
No | QQ API 无投票接口 |
这一能力边界同样反映在低层 Chat SDK 适配器中:editMessage 退化为"发一条新消息"(QQ 不支持编辑,见 adapter.ts),deleteMessage 记录 warn 日志(adapter.ts),fetchMessages 返回空列表(QQ 无消息历史 API,见 adapter.ts),表情表态与正在输入指示(typing)均为空实现(adapter.ts)。对应地,client.ts 中 messenger 的 editMessage 同样以重发消息兜底。
对服务端工程而言,QQ 平台机器人的正确打开方式是:以被动回复为核心,在收到事件后尽快携带 msg_id/event_id 应答,围绕"消息 ID 去重 + msg_seq 递增分片"处理长文本,并在接入层完整实现 Ed25519 回调签名验证与 AccessToken 提前刷新。协议全文与工程建议沉淀于 protocol-spec.md,对应的可运行实现可对照阅读 packages/chat-adapter-qq 与 apps/server/src/services/bot/platforms/qq/client.ts。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00