@leoqlin/openclaw-qqbot 1.6.8-beta.3 → 1.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/api.js CHANGED
@@ -850,7 +850,6 @@ async function sleep(ms, signal) {
850
850
  }
851
851
  });
852
852
  }
853
- import { StreamInputState } from "./types.js";
854
853
  /**
855
854
  * 发送流式消息(C2C 私聊)
856
855
  *
@@ -882,9 +881,5 @@ export async function sendC2CStreamMessage(accessToken, openid, req) {
882
881
  if (req.stream_msg_id) {
883
882
  body.stream_msg_id = req.stream_msg_id;
884
883
  }
885
- // 仅终结分片触发引用回调,中间分片跳过
886
- if (req.input_state === StreamInputState.DONE) {
887
- return sendAndNotify(accessToken, "POST", path, body, { text: req.content_raw });
888
- }
889
884
  return apiRequest(accessToken, "POST", path, body);
890
885
  }
@@ -1,6 +1,7 @@
1
1
  import WebSocket from "ws";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
+ import { MSG_TYPE_QUOTE } from "./types.js";
4
5
  import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh, sendC2CInputNotify, onMessageSent, PLUGIN_USER_AGENT, acknowledgeInteraction, getApiPluginVersion, setApiLogger } from "./api.js";
5
6
  import { loadSession, saveSession, clearSession } from "./session-store.js";
6
7
  import { recordKnownUser, flushKnownUsers } from "./known-users.js";
@@ -699,7 +700,8 @@ export async function startGateway(ctx) {
699
700
  let replyToBody;
700
701
  let replyToSender;
701
702
  let replyToIsQuote = false;
702
- // 引用消息处理:优先使用本地 refIndex 缓存(同步、已处理),缓存未命中时降级到 messageReference
703
+ // 引用消息处理:优先使用本地 refIndex 缓存(同步、已处理),缓存未命中时从 msg_elements[0] 获取
704
+ // refMsgIdx 已由 parseRefIndices 在引用消息类型时合并了 msg_elements[0].msg_idx 的优先级
703
705
  if (event.refMsgIdx) {
704
706
  const refEntry = getRefIndex(event.refMsgIdx);
705
707
  replyToId = event.refMsgIdx;
@@ -710,14 +712,22 @@ export async function startGateway(ctx) {
710
712
  replyToSender = refEntry.senderName ?? refEntry.senderId;
711
713
  log?.info(`[qqbot:${account.accountId}] Quote detected via refMsgIdx cache: refMsgIdx=${event.refMsgIdx}, sender=${replyToSender}, content="${replyToBody.slice(0, 80)}..."`);
712
714
  }
713
- else if (event.messageReference) {
714
- // 缓存未命中,降级到 messageReference:需异步下载附件、语音转录、表情解析
715
- replyToBody = await formatMessageReferenceForAgent(event.messageReference, { appId: account.appId, peerId, cfg, log });
716
- log?.info(`[qqbot:${account.accountId}] Quote detected via message_reference (cache miss): id=${replyToId}, content="${replyToBody.slice(0, 80)}..."`);
715
+ else if (event.msgType === MSG_TYPE_QUOTE) {
716
+ // 缓存未命中且为引用消息类型,从 msg_elements[0] 获取被引用消息内容
717
+ const refElement = event.msgElements?.[0];
718
+ if (refElement) {
719
+ const refData = { content: refElement.content ?? "", attachments: refElement.attachments };
720
+ replyToBody = await formatMessageReferenceForAgent(refData, { appId: account.appId, peerId, cfg, log });
721
+ log?.info(`[qqbot:${account.accountId}] Quote detected via msg_elements[0] (cache miss): id=${replyToId}, sender=${replyToSender ?? "unknown"}, content="${(replyToBody ?? "").slice(0, 80)}..."`);
722
+ }
723
+ else {
724
+ // 引用消息但 msg_elements 为空:AI 只能知道"用户引用了一条消息"
725
+ log?.info(`[qqbot:${account.accountId}] Quote detected (MSG_TYPE_QUOTE) but no msg_elements: refMsgIdx=${event.refMsgIdx}`);
726
+ }
717
727
  }
718
728
  else {
719
- // 缓存未命中且无 messageReference:AI 只能知道"用户引用了一条消息"
720
- log?.info(`[qqbot:${account.accountId}] Quote detected but no cache and no messageReference: refMsgIdx=${event.refMsgIdx}`);
729
+ // 缓存未命中且非引用消息类型:AI 只能知道"用户引用了一条消息"
730
+ log?.info(`[qqbot:${account.accountId}] Quote detected but no cache and msgType=${event.msgType} (not quote): refMsgIdx=${event.refMsgIdx}`);
721
731
  }
722
732
  }
723
733
  // 2. 缓存当前消息自身的 msgIdx(供将来被引用时查找)
@@ -1711,7 +1721,7 @@ export async function startGateway(ctx) {
1711
1721
  accountId: account.accountId,
1712
1722
  });
1713
1723
  // 解析引用索引
1714
- const c2cRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
1724
+ const c2cRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1715
1725
  // 斜杠指令拦截 → 不匹配则入队
1716
1726
  trySlashCommandOrEnqueue({
1717
1727
  type: "c2c",
@@ -1722,7 +1732,8 @@ export async function startGateway(ctx) {
1722
1732
  attachments: event.attachments,
1723
1733
  refMsgIdx: c2cRefs.refMsgIdx,
1724
1734
  msgIdx: c2cRefs.msgIdx,
1725
- messageReference: event.message_reference,
1735
+ msgElements: event.msg_elements,
1736
+ msgType: event.message_type,
1726
1737
  });
1727
1738
  }
1728
1739
  else if (t === "AT_MESSAGE_CREATE") {
@@ -1734,7 +1745,7 @@ export async function startGateway(ctx) {
1734
1745
  nickname: event.author.username,
1735
1746
  accountId: account.accountId,
1736
1747
  });
1737
- const guildRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
1748
+ const guildRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1738
1749
  trySlashCommandOrEnqueue({
1739
1750
  type: "guild",
1740
1751
  senderId: event.author.id,
@@ -1747,6 +1758,7 @@ export async function startGateway(ctx) {
1747
1758
  attachments: event.attachments,
1748
1759
  refMsgIdx: guildRefs.refMsgIdx,
1749
1760
  msgIdx: guildRefs.msgIdx,
1761
+ msgType: event.message_type,
1750
1762
  });
1751
1763
  }
1752
1764
  else if (t === "DIRECT_MESSAGE_CREATE") {
@@ -1758,7 +1770,7 @@ export async function startGateway(ctx) {
1758
1770
  nickname: event.author.username,
1759
1771
  accountId: account.accountId,
1760
1772
  });
1761
- const dmRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
1773
+ const dmRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1762
1774
  trySlashCommandOrEnqueue({
1763
1775
  type: "dm",
1764
1776
  senderId: event.author.id,
@@ -1770,6 +1782,7 @@ export async function startGateway(ctx) {
1770
1782
  attachments: event.attachments,
1771
1783
  refMsgIdx: dmRefs.refMsgIdx,
1772
1784
  msgIdx: dmRefs.msgIdx,
1785
+ msgType: event.message_type,
1773
1786
  });
1774
1787
  }
1775
1788
  else if (t === "GROUP_AT_MESSAGE_CREATE") {
@@ -1782,7 +1795,7 @@ export async function startGateway(ctx) {
1782
1795
  groupOpenid: event.group_openid,
1783
1796
  accountId: account.accountId,
1784
1797
  });
1785
- const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
1798
+ const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1786
1799
  trySlashCommandOrEnqueue({
1787
1800
  type: "group",
1788
1801
  senderId: event.author.member_openid,
@@ -1797,7 +1810,8 @@ export async function startGateway(ctx) {
1797
1810
  eventType: "GROUP_AT_MESSAGE_CREATE",
1798
1811
  mentions: event.mentions,
1799
1812
  messageScene: event.message_scene,
1800
- messageReference: event.message_reference,
1813
+ msgElements: event.msg_elements,
1814
+ msgType: event.message_type,
1801
1815
  });
1802
1816
  }
1803
1817
  else if (t === "GROUP_MESSAGE_CREATE") {
@@ -1809,7 +1823,7 @@ export async function startGateway(ctx) {
1809
1823
  groupOpenid: event.group_openid,
1810
1824
  accountId: account.accountId,
1811
1825
  });
1812
- const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
1826
+ const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1813
1827
  trySlashCommandOrEnqueue({
1814
1828
  type: "group",
1815
1829
  senderId: event.author.member_openid,
@@ -1825,7 +1839,8 @@ export async function startGateway(ctx) {
1825
1839
  eventType: "GROUP_MESSAGE_CREATE",
1826
1840
  mentions: event.mentions,
1827
1841
  messageScene: event.message_scene,
1828
- messageReference: event.message_reference,
1842
+ msgElements: event.msg_elements,
1843
+ msgType: event.message_type,
1829
1844
  });
1830
1845
  }
1831
1846
  else if (t === "GROUP_ADD_ROBOT") {
@@ -1,5 +1,5 @@
1
1
  import type { QueueSnapshot } from "./slash-commands.js";
2
- import type { MessageReference } from "./types.js";
2
+ import type { MsgElement } from "./types.js";
3
3
  /**
4
4
  * 消息队列项类型(用于异步处理消息,防止阻塞心跳)
5
5
  */
@@ -43,8 +43,10 @@ export interface QueuedMessage {
43
43
  source?: string;
44
44
  ext?: string[];
45
45
  };
46
- /** 消息引用结构(QQ 推送事件中携带的被引用原始消息) */
47
- messageReference?: MessageReference;
46
+ /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
47
+ msgElements?: MsgElement[];
48
+ /** 消息类型,参见 MSG_TYPE_* */
49
+ msgType?: number;
48
50
  /** 群消息合并标记:记录合并了多少条原始消息 */
49
51
  _mergedCount?: number;
50
52
  /** 合并前的原始消息列表(用于 gateway 侧逐条格式化信封) */
@@ -1828,7 +1828,23 @@ registerCommand({
1828
1828
  ].join("\n");
1829
1829
  }
1830
1830
  catch (err) {
1831
- return `❌ 更新配置失败:${err}`;
1831
+ const fwVer = getFrameworkVersion();
1832
+ return [
1833
+ `❌ 当前版本不支持该指令`,
1834
+ ``,
1835
+ `🦞框架版本:${fwVer}`,
1836
+ `🤖QQBot 插件版本:v${PLUGIN_VERSION}`,
1837
+ ``,
1838
+ `可通过以下命令手动开启流式消息:`,
1839
+ ``,
1840
+ `\`\`\`shell`,
1841
+ `# 1. 开启流式消息`,
1842
+ `openclaw config set channels.qqbot.streaming true`,
1843
+ ``,
1844
+ `# 2. 重启网关使配置生效`,
1845
+ `openclaw gateway restart`,
1846
+ `\`\`\``,
1847
+ ].join("\n");
1832
1848
  }
1833
1849
  },
1834
1850
  });
@@ -1,3 +1,7 @@
1
+ /** 普通文本消息 */
2
+ export declare const MSG_TYPE_TEXT = 0;
3
+ /** 引用(回复)消息 */
4
+ export declare const MSG_TYPE_QUOTE = 103;
1
5
  /**
2
6
  * QQ Bot 配置类型
3
7
  */
@@ -198,8 +202,10 @@ export interface C2CMessageEvent {
198
202
  ext?: string[];
199
203
  };
200
204
  attachments?: MessageAttachment[];
201
- /** 消息引用,当用户引用某条消息时存在 */
202
- message_reference?: MessageReference;
205
+ /** 消息类型,参见 MSG_TYPE_* */
206
+ message_type?: number;
207
+ /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
208
+ msg_elements?: MsgElement[];
203
209
  }
204
210
  /**
205
211
  * 频道 AT 消息事件
@@ -221,16 +227,18 @@ export interface GuildMessageEvent {
221
227
  };
222
228
  attachments?: MessageAttachment[];
223
229
  }
224
- /**
225
- * 消息引用(被回复的原始消息)
226
- */
227
- export interface MessageReference {
228
- /** 被引用消息的文本内容 */
229
- content: string;
230
- /** 被引用消息的附件列表 */
231
- attachments?: MessageAttachment[];
232
- /** 被引用消息的索引标识 */
230
+ /** 消息元素结点,引用消息时 msg_elements[0] 为被引用的原始消息 */
231
+ export interface MsgElement {
232
+ /** 消息索引标识 */
233
233
  msg_idx?: string;
234
+ /** 消息类型,参见 MSG_TYPE_* 常量 */
235
+ message_type?: number;
236
+ /** 文本内容 */
237
+ content?: string;
238
+ /** 附件列表 */
239
+ attachments?: MessageAttachment[];
240
+ /** 嵌套消息元素(引用消息场景下可能存在) */
241
+ msg_elements?: MsgElement[];
234
242
  }
235
243
  /**
236
244
  * 群聊 AT 消息事件
@@ -263,8 +271,10 @@ export interface GroupMessageEvent {
263
271
  /** 是否 @机器人自身 */
264
272
  is_you?: boolean;
265
273
  }>;
266
- /** 消息引用,当用户引用某条消息时存在 */
267
- message_reference?: MessageReference;
274
+ /** 消息类型,参见 MSG_TYPE_* */
275
+ message_type?: number;
276
+ /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
277
+ msg_elements?: MsgElement[];
268
278
  }
269
279
  /**
270
280
  * 按钮交互事件(INTERACTION_CREATE)
package/dist/src/types.js CHANGED
@@ -1,3 +1,8 @@
1
+ // ── QQ 消息类型常量(message_type 枚举值) ──
2
+ /** 普通文本消息 */
3
+ export const MSG_TYPE_TEXT = 0;
4
+ /** 引用(回复)消息 */
5
+ export const MSG_TYPE_QUOTE = 103;
1
6
  // ---- 流式消息常量 ----
2
7
  /** 流式消息输入模式 */
3
8
  export const StreamInputMode = {
@@ -13,15 +13,10 @@ export declare function parseFaceTags(text: string): string;
13
13
  * 这些标记可能被 AI 错误地学习并输出,需要在发送前移除
14
14
  */
15
15
  export declare function filterInternalMarkers(text: string): string;
16
- /**
17
- * message_scene.ext message_reference 中解析引用索引。
18
- *
19
- * @param ext message_scene.ext 数组,格式示例: ["", "ref_msg_idx=REFIDX_xxx", "msg_idx=REFIDX_yyy"]
20
- * @param msgRef message_reference 对象,其 msg_idx 比 ext 中解析的更权威,有值时优先使用
21
- */
22
- export declare function parseRefIndices(ext?: string[], msgRef?: {
16
+ /** 从 ext 和 msg_elements 中解析引用索引,仅 MSG_TYPE_QUOTE 时取 msg_elements */
17
+ export declare function parseRefIndices(ext?: string[], messageType?: number, msgElements?: Array<{
23
18
  msg_idx?: string;
24
- }): {
19
+ }>): {
25
20
  refMsgIdx?: string;
26
21
  msgIdx?: string;
27
22
  };
@@ -2,6 +2,7 @@
2
2
  * QQ Bot 文本解析工具函数
3
3
  */
4
4
  import { inferAttachmentType } from "../group-history.js";
5
+ import { MSG_TYPE_QUOTE } from "../types.js";
5
6
  /**
6
7
  * 解析 QQ 表情标签,将 <faceType=1,faceId="13",ext="base64..."> 格式
7
8
  * 替换为 【表情: 中文名】 格式
@@ -35,13 +36,8 @@ export function filterInternalMarkers(text) {
35
36
  result = result.replace(/\n{3,}/g, "\n\n").trim();
36
37
  return result;
37
38
  }
38
- /**
39
- * 从 message_scene.ext message_reference 中解析引用索引。
40
- *
41
- * @param ext message_scene.ext 数组,格式示例: ["", "ref_msg_idx=REFIDX_xxx", "msg_idx=REFIDX_yyy"]
42
- * @param msgRef message_reference 对象,其 msg_idx 比 ext 中解析的更权威,有值时优先使用
43
- */
44
- export function parseRefIndices(ext, msgRef) {
39
+ /** 从 ext 和 msg_elements 中解析引用索引,仅 MSG_TYPE_QUOTE 时取 msg_elements */
40
+ export function parseRefIndices(ext, messageType, msgElements) {
45
41
  let refMsgIdx;
46
42
  let msgIdx;
47
43
  if (ext && ext.length > 0) {
@@ -54,9 +50,12 @@ export function parseRefIndices(ext, msgRef) {
54
50
  }
55
51
  }
56
52
  }
57
- // messageReference.msg_idx 更权威,有值时覆盖 ext 解析结果
58
- if (msgRef?.msg_idx) {
59
- refMsgIdx = msgRef.msg_idx;
53
+ // 仅当 message_type=MSG_TYPE_QUOTE(引用消息)时,msg_elements[0].msg_idx 更权威,有值时覆盖 ext 解析结果
54
+ if (messageType === MSG_TYPE_QUOTE) {
55
+ const refElement = msgElements?.[0];
56
+ if (refElement?.msg_idx) {
57
+ refMsgIdx = refElement.msg_idx;
58
+ }
60
59
  }
61
60
  return { refMsgIdx, msgIdx };
62
61
  }
@@ -4,7 +4,7 @@
4
4
  "description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
5
5
  "channels": ["qqbot"],
6
6
  "extensions": ["./preload.cjs"],
7
- "skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media"],
7
+ "skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media", "skills/qqbot-upgrade"],
8
8
  "capabilities": {
9
9
  "proactiveMessaging": true,
10
10
  "cronJobs": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leoqlin/openclaw-qqbot",
3
- "version": "1.6.8-beta.3",
3
+ "version": "1.6.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -80,7 +80,7 @@ trap cleanup_on_exit EXIT
80
80
  # 清理上次升级可能遗留的备份目录(如上次脚本被 kill 等极端情况)
81
81
  find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
82
82
 
83
- PKG_NAME="@tencent-connect/openclaw-qqbot"
83
+ PKG_NAME="@leoqlin/openclaw-qqbot"
84
84
  PLUGIN_ID="openclaw-qqbot"
85
85
  INSTALL_SRC=""
86
86
  TARGET_VERSION=""
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: qqbot-upgrade
3
+ description: 通过官方脚本将 openclaw-qqbot 插件升级到最新 npm 版本。当用户要求更新 QQ 机器人插件、升级 qqbot 扩展或同步官方最新版时使用。
4
+ metadata: {"openclaw":{"emoji":"⬆️","requires":{"config":["channels.qqbot"]}}}
5
+ ---
6
+
7
+ # QQBot 插件升级
8
+
9
+ ## 何时使用
10
+
11
+ 用户表达以下意图时,应在**本机终端**执行升级命令(不要只口头说明「去官网升级」):
12
+
13
+ - 更新 / 升级 `openclaw-qqbot` 或 QQBot 插件
14
+ - 拉取官方最新 npm 包
15
+ - 执行与「upgrade-via-npm」相关的操作
16
+
17
+ ---
18
+
19
+ ## 标准命令
20
+
21
+ 在 **bash** 环境中执行(需已安装 `curl`,且能访问 GitHub):
22
+
23
+ ```bash
24
+ curl -fsSL https://raw.githubusercontent.com/leol1024/openclaw-qqbot/main/scripts/upgrade-via-npm.sh | bash
25
+ ```
26
+
27
+ 说明:
28
+
29
+ - `-f`:HTTP 错误时失败;`-sS`:静默但保留错误输出;`-L`:跟随重定向
30
+ - 脚本由 [leol1024/openclaw-qqbot](https://github.com/leol1024/openclaw-qqbot) 仓库 `feat/stream_skill` 分支提供,通过 npm 完成升级流程(具体步骤以脚本为准)
31
+
32
+ ---
33
+
34
+ ## 代理与前置条件
35
+
36
+ - 若网络无法直连 GitHub,需由用户配置代理或镜像后再执行;代理应作用于当前 shell(如 `HTTPS_PROXY`)
37
+ - 脚本通常会依赖 **Node.js / npm** 环境;若命令失败,根据终端报错检查 PATH、权限与 npm 登录状态
38
+
39
+ ---
40
+
41
+ ## 安全提示
42
+
43
+ 管道执行远程脚本属于**远程代码执行**:仅适用于用户明确请求升级且信任该官方仓库的场景。不要替换为未经验证的 URL。
44
+
45
+ ---
46
+
47
+ ## 执行后
48
+
49
+ 根据脚本退出码与终端输出向用户简要汇报:成功则说明已按官方流程升级;失败则摘录关键错误并提示检查网络、Node/npm 与权限。
package/src/api.ts CHANGED
@@ -1276,9 +1276,5 @@ export async function sendC2CStreamMessage(
1276
1276
  if (req.stream_msg_id) {
1277
1277
  body.stream_msg_id = req.stream_msg_id;
1278
1278
  }
1279
- // 仅终结分片触发引用回调,中间分片跳过
1280
- if (req.input_state === StreamInputState.DONE) {
1281
- return sendAndNotify(accessToken, "POST", path, body, { text: req.content_raw });
1282
- }
1283
1279
  return apiRequest<MessageResponse>(accessToken, "POST", path, body);
1284
1280
  }
package/src/gateway.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import WebSocket from "ws";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
- import type { ResolvedQQBotAccount, WSPayload, C2CMessageEvent, GuildMessageEvent, GroupMessageEvent, InteractionEvent, MessageReference } from "./types.js";
4
+ import type { ResolvedQQBotAccount, WSPayload, C2CMessageEvent, GuildMessageEvent, GroupMessageEvent, InteractionEvent, MsgElement } from "./types.js";
5
+ import { MSG_TYPE_QUOTE } from "./types.js";
5
6
  import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh, sendC2CInputNotify, onMessageSent, PLUGIN_USER_AGENT, sendProactiveGroupMessage, acknowledgeInteraction, getApiPluginVersion, setApiLogger } from "./api.js";
6
7
  import { loadSession, saveSession, clearSession } from "./session-store.js";
7
8
  import { recordKnownUser, flushKnownUsers } from "./known-users.js";
@@ -726,8 +727,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
726
727
  eventType?: string;
727
728
  mentions?: Array<{ scope?: "all" | "single"; id?: string; user_openid?: string; member_openid?: string; username?: string; bot?: boolean; is_you?: boolean }>;
728
729
  messageScene?: { source?: string; ext?: string[] };
729
- /** QQ 推送事件中携带的被引用原始消息结构 */
730
- messageReference?: MessageReference;
730
+ msgElements?: MsgElement[];
731
+ /** 消息类型,参见 MSG_TYPE_* */
732
+ msgType?: number;
731
733
  }) => {
732
734
 
733
735
  log?.debug?.(`[qqbot:${account.accountId}] Received message: ${JSON.stringify(event)}`);
@@ -856,7 +858,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
856
858
  let replyToSender: string | undefined;
857
859
  let replyToIsQuote = false;
858
860
 
859
- // 引用消息处理:优先使用本地 refIndex 缓存(同步、已处理),缓存未命中时降级到 messageReference
861
+ // 引用消息处理:优先使用本地 refIndex 缓存(同步、已处理),缓存未命中时从 msg_elements[0] 获取
862
+ // refMsgIdx 已由 parseRefIndices 在引用消息类型时合并了 msg_elements[0].msg_idx 的优先级
860
863
  if (event.refMsgIdx) {
861
864
  const refEntry = getRefIndex(event.refMsgIdx);
862
865
  replyToId = event.refMsgIdx;
@@ -867,13 +870,20 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
867
870
  replyToBody = formatRefEntryForAgent(refEntry);
868
871
  replyToSender = refEntry.senderName ?? refEntry.senderId;
869
872
  log?.info(`[qqbot:${account.accountId}] Quote detected via refMsgIdx cache: refMsgIdx=${event.refMsgIdx}, sender=${replyToSender}, content="${replyToBody.slice(0, 80)}..."`);
870
- } else if (event.messageReference) {
871
- // 缓存未命中,降级到 messageReference:需异步下载附件、语音转录、表情解析
872
- replyToBody = await formatMessageReferenceForAgent(event.messageReference, { appId: account.appId, peerId, cfg, log });
873
- log?.info(`[qqbot:${account.accountId}] Quote detected via message_reference (cache miss): id=${replyToId}, content="${replyToBody.slice(0, 80)}..."`);
873
+ } else if (event.msgType === MSG_TYPE_QUOTE) {
874
+ // 缓存未命中且为引用消息类型,从 msg_elements[0] 获取被引用消息内容
875
+ const refElement = event.msgElements?.[0];
876
+ if (refElement) {
877
+ const refData = { content: refElement.content ?? "", attachments: refElement.attachments };
878
+ replyToBody = await formatMessageReferenceForAgent(refData, { appId: account.appId, peerId, cfg, log });
879
+ log?.info(`[qqbot:${account.accountId}] Quote detected via msg_elements[0] (cache miss): id=${replyToId}, sender=${replyToSender ?? "unknown"}, content="${(replyToBody ?? "").slice(0, 80)}..."`);
880
+ } else {
881
+ // 引用消息但 msg_elements 为空:AI 只能知道"用户引用了一条消息"
882
+ log?.info(`[qqbot:${account.accountId}] Quote detected (MSG_TYPE_QUOTE) but no msg_elements: refMsgIdx=${event.refMsgIdx}`);
883
+ }
874
884
  } else {
875
- // 缓存未命中且无 messageReference:AI 只能知道"用户引用了一条消息"
876
- log?.info(`[qqbot:${account.accountId}] Quote detected but no cache and no messageReference: refMsgIdx=${event.refMsgIdx}`);
885
+ // 缓存未命中且非引用消息类型:AI 只能知道"用户引用了一条消息"
886
+ log?.info(`[qqbot:${account.accountId}] Quote detected but no cache and msgType=${event.msgType} (not quote): refMsgIdx=${event.refMsgIdx}`);
877
887
  }
878
888
  }
879
889
 
@@ -1945,7 +1955,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1945
1955
  accountId: account.accountId,
1946
1956
  });
1947
1957
  // 解析引用索引
1948
- const c2cRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
1958
+ const c2cRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
1949
1959
  // 斜杠指令拦截 → 不匹配则入队
1950
1960
  trySlashCommandOrEnqueue({
1951
1961
  type: "c2c",
@@ -1956,7 +1966,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1956
1966
  attachments: event.attachments,
1957
1967
  refMsgIdx: c2cRefs.refMsgIdx,
1958
1968
  msgIdx: c2cRefs.msgIdx,
1959
- messageReference: event.message_reference,
1969
+ msgElements: event.msg_elements,
1970
+ msgType: event.message_type,
1960
1971
  });
1961
1972
  } else if (t === "AT_MESSAGE_CREATE") {
1962
1973
  const event = d as GuildMessageEvent;
@@ -1967,7 +1978,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1967
1978
  nickname: event.author.username,
1968
1979
  accountId: account.accountId,
1969
1980
  });
1970
- const guildRefs = parseRefIndices((event as any).message_scene?.ext, (event as any).message_reference);
1981
+ const guildRefs = parseRefIndices((event as any).message_scene?.ext, (event as any).message_type, (event as any).msg_elements);
1971
1982
  trySlashCommandOrEnqueue({
1972
1983
  type: "guild",
1973
1984
  senderId: event.author.id,
@@ -1980,6 +1991,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1980
1991
  attachments: event.attachments,
1981
1992
  refMsgIdx: guildRefs.refMsgIdx,
1982
1993
  msgIdx: guildRefs.msgIdx,
1994
+ msgType: (event as any).message_type,
1983
1995
  });
1984
1996
  } else if (t === "DIRECT_MESSAGE_CREATE") {
1985
1997
  const event = d as GuildMessageEvent;
@@ -1990,7 +2002,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1990
2002
  nickname: event.author.username,
1991
2003
  accountId: account.accountId,
1992
2004
  });
1993
- const dmRefs = parseRefIndices((event as any).message_scene?.ext, (event as any).message_reference);
2005
+ const dmRefs = parseRefIndices((event as any).message_scene?.ext, (event as any).message_type, (event as any).msg_elements);
1994
2006
  trySlashCommandOrEnqueue({
1995
2007
  type: "dm",
1996
2008
  senderId: event.author.id,
@@ -2002,6 +2014,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2002
2014
  attachments: event.attachments,
2003
2015
  refMsgIdx: dmRefs.refMsgIdx,
2004
2016
  msgIdx: dmRefs.msgIdx,
2017
+ msgType: (event as any).message_type,
2005
2018
  });
2006
2019
  } else if (t === "GROUP_AT_MESSAGE_CREATE") {
2007
2020
  const event = d as GroupMessageEvent;
@@ -2013,7 +2026,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2013
2026
  groupOpenid: event.group_openid,
2014
2027
  accountId: account.accountId,
2015
2028
  });
2016
- const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
2029
+ const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
2017
2030
  trySlashCommandOrEnqueue({
2018
2031
  type: "group",
2019
2032
  senderId: event.author.member_openid,
@@ -2028,7 +2041,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2028
2041
  eventType: "GROUP_AT_MESSAGE_CREATE",
2029
2042
  mentions: event.mentions,
2030
2043
  messageScene: event.message_scene,
2031
- messageReference: event.message_reference,
2044
+ msgElements: event.msg_elements,
2045
+ msgType: event.message_type,
2032
2046
  });
2033
2047
  } else if (t === "GROUP_MESSAGE_CREATE") {
2034
2048
  const event = d as GroupMessageEvent;
@@ -2039,7 +2053,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2039
2053
  groupOpenid: event.group_openid,
2040
2054
  accountId: account.accountId,
2041
2055
  });
2042
- const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_reference);
2056
+ const groupRefs = parseRefIndices(event.message_scene?.ext, event.message_type, event.msg_elements);
2043
2057
  trySlashCommandOrEnqueue({
2044
2058
  type: "group",
2045
2059
  senderId: event.author.member_openid,
@@ -2055,7 +2069,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2055
2069
  eventType: "GROUP_MESSAGE_CREATE",
2056
2070
  mentions: event.mentions,
2057
2071
  messageScene: event.message_scene,
2058
- messageReference: event.message_reference,
2072
+ msgElements: event.msg_elements,
2073
+ msgType: event.message_type,
2059
2074
  });
2060
2075
  } else if (t === "GROUP_ADD_ROBOT") {
2061
2076
  const event = d as { timestamp: string; group_openid: string; op_member_openid: string };
@@ -1,5 +1,5 @@
1
1
  import type { QueueSnapshot } from "./slash-commands.js";
2
- import type { MessageReference } from "./types.js";
2
+ import type { MsgElement } from "./types.js";
3
3
 
4
4
  // ── 消息队列默认配置 ──
5
5
  const DEFAULT_GLOBAL_QUEUE_SIZE = 1000;
@@ -33,8 +33,10 @@ export interface QueuedMessage {
33
33
  mentions?: Array<{ scope?: "all" | "single"; id?: string; user_openid?: string; member_openid?: string; username?: string; bot?: boolean; is_you?: boolean }>;
34
34
  /** 消息场景(来源、扩展字段) */
35
35
  messageScene?: { source?: string; ext?: string[] };
36
- /** 消息引用结构(QQ 推送事件中携带的被引用原始消息) */
37
- messageReference?: MessageReference;
36
+ /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
37
+ msgElements?: MsgElement[];
38
+ /** 消息类型,参见 MSG_TYPE_* */
39
+ msgType?: number;
38
40
  /** 群消息合并标记:记录合并了多少条原始消息 */
39
41
  _mergedCount?: number;
40
42
  /** 合并前的原始消息列表(用于 gateway 侧逐条格式化信封) */
@@ -1987,7 +1987,23 @@ registerCommand({
1987
1987
  : `AI 的回复将恢复为完整发送。`,
1988
1988
  ].join("\n");
1989
1989
  } catch (err) {
1990
- return `❌ 更新配置失败:${err}`;
1990
+ const fwVer = getFrameworkVersion();
1991
+ return [
1992
+ `❌ 当前版本不支持该指令`,
1993
+ ``,
1994
+ `🦞框架版本:${fwVer}`,
1995
+ `🤖QQBot 插件版本:v${PLUGIN_VERSION}`,
1996
+ ``,
1997
+ `可通过以下命令手动开启流式消息:`,
1998
+ ``,
1999
+ `\`\`\`shell`,
2000
+ `# 1. 开启流式消息`,
2001
+ `openclaw config set channels.qqbot.streaming true`,
2002
+ ``,
2003
+ `# 2. 重启网关使配置生效`,
2004
+ `openclaw gateway restart`,
2005
+ `\`\`\``,
2006
+ ].join("\n");
1991
2007
  }
1992
2008
  },
1993
2009
  });
package/src/types.ts CHANGED
@@ -1,3 +1,9 @@
1
+ // ── QQ 消息类型常量(message_type 枚举值) ──
2
+ /** 普通文本消息 */
3
+ export const MSG_TYPE_TEXT = 0;
4
+ /** 引用(回复)消息 */
5
+ export const MSG_TYPE_QUOTE = 103;
6
+
1
7
  /**
2
8
  * QQ Bot 配置类型
3
9
  */
@@ -207,8 +213,10 @@ export interface C2CMessageEvent {
207
213
  ext?: string[];
208
214
  };
209
215
  attachments?: MessageAttachment[];
210
- /** 消息引用,当用户引用某条消息时存在 */
211
- message_reference?: MessageReference;
216
+ /** 消息类型,参见 MSG_TYPE_* */
217
+ message_type?: number;
218
+ /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
219
+ msg_elements?: MsgElement[];
212
220
  }
213
221
 
214
222
  /**
@@ -232,16 +240,18 @@ export interface GuildMessageEvent {
232
240
  attachments?: MessageAttachment[];
233
241
  }
234
242
 
235
- /**
236
- * 消息引用(被回复的原始消息)
237
- */
238
- export interface MessageReference {
239
- /** 被引用消息的文本内容 */
240
- content: string;
241
- /** 被引用消息的附件列表 */
242
- attachments?: MessageAttachment[];
243
- /** 被引用消息的索引标识 */
243
+ /** 消息元素结点,引用消息时 msg_elements[0] 为被引用的原始消息 */
244
+ export interface MsgElement {
245
+ /** 消息索引标识 */
244
246
  msg_idx?: string;
247
+ /** 消息类型,参见 MSG_TYPE_* 常量 */
248
+ message_type?: number;
249
+ /** 文本内容 */
250
+ content?: string;
251
+ /** 附件列表 */
252
+ attachments?: MessageAttachment[];
253
+ /** 嵌套消息元素(引用消息场景下可能存在) */
254
+ msg_elements?: MsgElement[];
245
255
  }
246
256
 
247
257
  /**
@@ -275,8 +285,10 @@ export interface GroupMessageEvent {
275
285
  /** 是否 @机器人自身 */
276
286
  is_you?: boolean;
277
287
  }>;
278
- /** 消息引用,当用户引用某条消息时存在 */
279
- message_reference?: MessageReference;
288
+ /** 消息类型,参见 MSG_TYPE_* */
289
+ message_type?: number;
290
+ /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
291
+ msg_elements?: MsgElement[];
280
292
  }
281
293
 
282
294
  /**
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { RefAttachmentSummary } from "../ref-index-store.js";
6
6
  import { inferAttachmentType } from "../group-history.js";
7
+ import { MSG_TYPE_QUOTE } from "../types.js";
7
8
 
8
9
  /**
9
10
  * 解析 QQ 表情标签,将 <faceType=1,faceId="13",ext="base64..."> 格式
@@ -40,15 +41,11 @@ export function filterInternalMarkers(text: string): string {
40
41
  return result;
41
42
  }
42
43
 
43
- /**
44
- * 从 message_scene.ext 和 message_reference 中解析引用索引。
45
- *
46
- * @param ext message_scene.ext 数组,格式示例: ["", "ref_msg_idx=REFIDX_xxx", "msg_idx=REFIDX_yyy"]
47
- * @param msgRef message_reference 对象,其 msg_idx 比 ext 中解析的更权威,有值时优先使用
48
- */
44
+ /** 从 ext 和 msg_elements 中解析引用索引,仅 MSG_TYPE_QUOTE 时取 msg_elements */
49
45
  export function parseRefIndices(
50
46
  ext?: string[],
51
- msgRef?: { msg_idx?: string },
47
+ messageType?: number,
48
+ msgElements?: Array<{ msg_idx?: string }>,
52
49
  ): { refMsgIdx?: string; msgIdx?: string } {
53
50
  let refMsgIdx: string | undefined;
54
51
  let msgIdx: string | undefined;
@@ -61,9 +58,12 @@ export function parseRefIndices(
61
58
  }
62
59
  }
63
60
  }
64
- // messageReference.msg_idx 更权威,有值时覆盖 ext 解析结果
65
- if (msgRef?.msg_idx) {
66
- refMsgIdx = msgRef.msg_idx;
61
+ // 仅当 message_type=MSG_TYPE_QUOTE(引用消息)时,msg_elements[0].msg_idx 更权威,有值时覆盖 ext 解析结果
62
+ if (messageType === MSG_TYPE_QUOTE) {
63
+ const refElement = msgElements?.[0];
64
+ if (refElement?.msg_idx) {
65
+ refMsgIdx = refElement.msg_idx;
66
+ }
67
67
  }
68
68
  return { refMsgIdx, msgIdx };
69
69
  }