@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 +0 -5
- package/dist/src/gateway.js +30 -15
- package/dist/src/message-queue.d.ts +5 -3
- package/dist/src/slash-commands.js +17 -1
- package/dist/src/types.d.ts +23 -13
- package/dist/src/types.js +5 -0
- package/dist/src/utils/text-parsing.d.ts +3 -8
- package/dist/src/utils/text-parsing.js +9 -10
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/upgrade-via-npm.sh +1 -1
- package/skills/qqbot-upgrade/SKILL.md +49 -0
- package/src/api.ts +0 -4
- package/src/gateway.ts +33 -18
- package/src/message-queue.ts +5 -3
- package/src/slash-commands.ts +17 -1
- package/src/types.ts +25 -13
- package/src/utils/text-parsing.ts +10 -10
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
|
}
|
package/dist/src/gateway.js
CHANGED
|
@@ -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
|
|
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.
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
//
|
|
720
|
-
log?.info(`[qqbot:${account.accountId}] Quote detected but no cache and
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
47
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
58
|
-
if (
|
|
59
|
-
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
@@ -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="@
|
|
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,
|
|
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
|
-
|
|
730
|
-
|
|
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
|
|
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.
|
|
871
|
-
//
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
//
|
|
876
|
-
log?.info(`[qqbot:${account.accountId}] Quote detected but no cache and
|
|
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.
|
|
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
|
-
|
|
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).
|
|
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).
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 };
|
package/src/message-queue.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QueueSnapshot } from "./slash-commands.js";
|
|
2
|
-
import type {
|
|
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
|
-
/**
|
|
37
|
-
|
|
36
|
+
/** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
|
|
37
|
+
msgElements?: MsgElement[];
|
|
38
|
+
/** 消息类型,参见 MSG_TYPE_* */
|
|
39
|
+
msgType?: number;
|
|
38
40
|
/** 群消息合并标记:记录合并了多少条原始消息 */
|
|
39
41
|
_mergedCount?: number;
|
|
40
42
|
/** 合并前的原始消息列表(用于 gateway 侧逐条格式化信封) */
|
package/src/slash-commands.ts
CHANGED
|
@@ -1987,7 +1987,23 @@ registerCommand({
|
|
|
1987
1987
|
: `AI 的回复将恢复为完整发送。`,
|
|
1988
1988
|
].join("\n");
|
|
1989
1989
|
} catch (err) {
|
|
1990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
65
|
-
if (
|
|
66
|
-
|
|
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
|
}
|