@tencent-connect/openclaw-qqbot 1.5.6 → 1.5.7
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/README.md +46 -146
- package/README.zh.md +46 -146
- package/bin/qqbot-cli.js +6 -6
- package/dist/AI/345/210/233/346/226/260/345/272/224/347/224/250/345/245/226_/347/224/263/346/212/245/344/271/246.md +211 -0
- package/dist/src/gateway.js +109 -92
- package/dist/src/slash-commands.d.ts +48 -0
- package/dist/src/slash-commands.js +212 -0
- package/dist/src/utils/audio-convert.d.ts +0 -6
- package/dist/src/utils/audio-convert.js +0 -89
- package/package.json +1 -1
- package/scripts/{upgrade.sh → cleanup-legacy-plugins.sh} +3 -3
- package/scripts/set-markdown.sh +20 -20
- package/scripts/upgrade-via-npm.sh +204 -0
- package/scripts/{upgrade-and-run.sh → upgrade-via-source.sh} +60 -44
- package/src/api.ts +104 -24
- package/src/channel.ts +2 -1
- package/src/gateway.ts +229 -33
- package/src/image-server.ts +5 -2
- package/src/outbound.ts +32 -26
- package/src/ref-index-store.ts +358 -0
- package/src/types.ts +6 -0
- package/src/utils/platform.ts +16 -2
- package/scripts/draw_arch.py +0 -174
- package/scripts/npm-upgrade.sh +0 -120
- package/scripts/pull-latest.sh +0 -316
package/src/api.ts
CHANGED
|
@@ -12,6 +12,35 @@ const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
|
|
|
12
12
|
// 运行时配置
|
|
13
13
|
let currentMarkdownSupport = false;
|
|
14
14
|
|
|
15
|
+
// 出站消息回调钩子:消息发送成功且回包含 ext_info.ref_idx 时触发
|
|
16
|
+
// 由外层(gateway/outbound)注册,用于统一缓存 bot 出站消息的 refIdx
|
|
17
|
+
|
|
18
|
+
/** 出站消息元信息(结构化存储,不做预格式化) */
|
|
19
|
+
export interface OutboundMeta {
|
|
20
|
+
/** 消息文本内容 */
|
|
21
|
+
text?: string;
|
|
22
|
+
/** 媒体类型 */
|
|
23
|
+
mediaType?: "image" | "voice" | "video" | "file";
|
|
24
|
+
/** 媒体来源:在线 URL */
|
|
25
|
+
mediaUrl?: string;
|
|
26
|
+
/** 媒体来源:本地文件路径或文件名 */
|
|
27
|
+
mediaLocalPath?: string;
|
|
28
|
+
/** TTS 原文本(仅 voice 类型有效,用于保存 TTS 前的文本内容) */
|
|
29
|
+
ttsText?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type OnMessageSentCallback = (refIdx: string, meta: OutboundMeta) => void;
|
|
33
|
+
let onMessageSentHook: OnMessageSentCallback | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 注册出站消息回调
|
|
37
|
+
* 当消息发送成功且 QQ 返回 ref_idx 时,自动回调此函数
|
|
38
|
+
* 用于在最底层统一缓存 bot 出站消息的 refIdx
|
|
39
|
+
*/
|
|
40
|
+
export function onMessageSent(callback: OnMessageSentCallback): void {
|
|
41
|
+
onMessageSentHook = callback;
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
/**
|
|
16
45
|
* 初始化 API 配置
|
|
17
46
|
* @param options.markdownSupport - 是否支持 markdown 消息(默认 false,需要机器人具备该权限才能启用)
|
|
@@ -98,7 +127,8 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
|
|
|
98
127
|
response.headers.forEach((value, key) => {
|
|
99
128
|
responseHeaders[key] = value;
|
|
100
129
|
});
|
|
101
|
-
|
|
130
|
+
const tokenTraceId = response.headers.get("x-tps-trace-id") ?? "";
|
|
131
|
+
console.log(`[qqbot-api:${appId}] <<< Status: ${response.status} ${response.statusText}${tokenTraceId ? ` | TraceId: ${tokenTraceId}` : ""}`);
|
|
102
132
|
|
|
103
133
|
let data: { access_token?: string; expires_in?: number };
|
|
104
134
|
let rawBody: string;
|
|
@@ -214,6 +244,7 @@ export async function apiRequest<T = unknown>(
|
|
|
214
244
|
if (typeof logBody.file_data === "string") {
|
|
215
245
|
logBody.file_data = `<base64 ${(logBody.file_data as string).length} chars>`;
|
|
216
246
|
}
|
|
247
|
+
console.log(`[qqbot-api] >>> Body:`, JSON.stringify(logBody));
|
|
217
248
|
}
|
|
218
249
|
|
|
219
250
|
let res: Response;
|
|
@@ -235,12 +266,14 @@ export async function apiRequest<T = unknown>(
|
|
|
235
266
|
res.headers.forEach((value, key) => {
|
|
236
267
|
responseHeaders[key] = value;
|
|
237
268
|
});
|
|
238
|
-
|
|
269
|
+
const traceId = res.headers.get("x-tps-trace-id") ?? "";
|
|
270
|
+
console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`);
|
|
239
271
|
|
|
240
272
|
let data: T;
|
|
241
273
|
let rawBody: string;
|
|
242
274
|
try {
|
|
243
275
|
rawBody = await res.text();
|
|
276
|
+
console.log(`[qqbot-api] <<< Body:`, rawBody);
|
|
244
277
|
data = JSON.parse(rawBody) as T;
|
|
245
278
|
} catch (err) {
|
|
246
279
|
throw new Error(`Failed to parse response[${path}]: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -303,12 +336,39 @@ export async function getGatewayUrl(accessToken: string): Promise<string> {
|
|
|
303
336
|
export interface MessageResponse {
|
|
304
337
|
id: string;
|
|
305
338
|
timestamp: number | string;
|
|
339
|
+
/** 消息的引用索引信息(出站时由 QQ 服务端返回) */
|
|
340
|
+
ext_info?: {
|
|
341
|
+
ref_idx?: string;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 发送消息并自动触发 refIdx 回调
|
|
347
|
+
* 所有消息发送函数统一经过此处,确保每条出站消息的 refIdx 都被捕获
|
|
348
|
+
*/
|
|
349
|
+
async function sendAndNotify(
|
|
350
|
+
accessToken: string,
|
|
351
|
+
method: string,
|
|
352
|
+
path: string,
|
|
353
|
+
body: unknown,
|
|
354
|
+
meta: OutboundMeta,
|
|
355
|
+
): Promise<MessageResponse> {
|
|
356
|
+
const result = await apiRequest<MessageResponse>(accessToken, method, path, body);
|
|
357
|
+
if (result.ext_info?.ref_idx && onMessageSentHook) {
|
|
358
|
+
try {
|
|
359
|
+
onMessageSentHook(result.ext_info.ref_idx, meta);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(`[qqbot-api] onMessageSent hook error: ${err}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
306
365
|
}
|
|
307
366
|
|
|
308
367
|
function buildMessageBody(
|
|
309
368
|
content: string,
|
|
310
369
|
msgId: string | undefined,
|
|
311
|
-
msgSeq: number
|
|
370
|
+
msgSeq: number,
|
|
371
|
+
messageReference?: string
|
|
312
372
|
): Record<string, unknown> {
|
|
313
373
|
const body: Record<string, unknown> = currentMarkdownSupport
|
|
314
374
|
? {
|
|
@@ -325,6 +385,9 @@ function buildMessageBody(
|
|
|
325
385
|
if (msgId) {
|
|
326
386
|
body.msg_id = msgId;
|
|
327
387
|
}
|
|
388
|
+
if (messageReference && !currentMarkdownSupport) {
|
|
389
|
+
body.message_reference = { message_id: messageReference };
|
|
390
|
+
}
|
|
328
391
|
return body;
|
|
329
392
|
}
|
|
330
393
|
|
|
@@ -332,11 +395,12 @@ export async function sendC2CMessage(
|
|
|
332
395
|
accessToken: string,
|
|
333
396
|
openid: string,
|
|
334
397
|
content: string,
|
|
335
|
-
msgId?: string
|
|
398
|
+
msgId?: string,
|
|
399
|
+
messageReference?: string
|
|
336
400
|
): Promise<MessageResponse> {
|
|
337
401
|
const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
|
|
338
|
-
const body = buildMessageBody(content, msgId, msgSeq);
|
|
339
|
-
return
|
|
402
|
+
const body = buildMessageBody(content, msgId, msgSeq, messageReference);
|
|
403
|
+
return sendAndNotify(accessToken, "POST", `/v2/users/${openid}/messages`, body, { text: content });
|
|
340
404
|
}
|
|
341
405
|
|
|
342
406
|
export async function sendC2CInputNotify(
|
|
@@ -344,7 +408,7 @@ export async function sendC2CInputNotify(
|
|
|
344
408
|
openid: string,
|
|
345
409
|
msgId?: string,
|
|
346
410
|
inputSecond: number = 60
|
|
347
|
-
): Promise<
|
|
411
|
+
): Promise<{ refIdx?: string }> {
|
|
348
412
|
const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
|
|
349
413
|
const body = {
|
|
350
414
|
msg_type: 6,
|
|
@@ -355,7 +419,8 @@ export async function sendC2CInputNotify(
|
|
|
355
419
|
msg_seq: msgSeq,
|
|
356
420
|
...(msgId ? { msg_id: msgId } : {}),
|
|
357
421
|
};
|
|
358
|
-
await apiRequest(accessToken, "POST", `/v2/users/${openid}/messages`, body);
|
|
422
|
+
const response = await apiRequest<{ ext_info?: { ref_idx?: string } }>(accessToken, "POST", `/v2/users/${openid}/messages`, body);
|
|
423
|
+
return { refIdx: response.ext_info?.ref_idx };
|
|
359
424
|
}
|
|
360
425
|
|
|
361
426
|
export async function sendChannelMessage(
|
|
@@ -396,9 +461,9 @@ export async function sendProactiveC2CMessage(
|
|
|
396
461
|
accessToken: string,
|
|
397
462
|
openid: string,
|
|
398
463
|
content: string
|
|
399
|
-
): Promise<
|
|
464
|
+
): Promise<MessageResponse> {
|
|
400
465
|
const body = buildProactiveMessageBody(content);
|
|
401
|
-
return
|
|
466
|
+
return sendAndNotify(accessToken, "POST", `/v2/users/${openid}/messages`, body, { text: content });
|
|
402
467
|
}
|
|
403
468
|
|
|
404
469
|
export async function sendProactiveGroupMessage(
|
|
@@ -501,16 +566,17 @@ export async function sendC2CMediaMessage(
|
|
|
501
566
|
openid: string,
|
|
502
567
|
fileInfo: string,
|
|
503
568
|
msgId?: string,
|
|
504
|
-
content?: string
|
|
505
|
-
|
|
569
|
+
content?: string,
|
|
570
|
+
meta?: OutboundMeta,
|
|
571
|
+
): Promise<MessageResponse> {
|
|
506
572
|
const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
|
|
507
|
-
return
|
|
573
|
+
return sendAndNotify(accessToken, "POST", `/v2/users/${openid}/messages`, {
|
|
508
574
|
msg_type: 7,
|
|
509
575
|
media: { file_info: fileInfo },
|
|
510
576
|
msg_seq: msgSeq,
|
|
511
577
|
...(content ? { content } : {}),
|
|
512
578
|
...(msgId ? { msg_id: msgId } : {}),
|
|
513
|
-
});
|
|
579
|
+
}, meta ?? { text: content });
|
|
514
580
|
}
|
|
515
581
|
|
|
516
582
|
export async function sendGroupMediaMessage(
|
|
@@ -530,21 +596,29 @@ export async function sendGroupMediaMessage(
|
|
|
530
596
|
});
|
|
531
597
|
}
|
|
532
598
|
|
|
533
|
-
export async function sendC2CImageMessage(accessToken: string, openid: string, imageUrl: string, msgId?: string, content?: string): Promise<
|
|
599
|
+
export async function sendC2CImageMessage(accessToken: string, openid: string, imageUrl: string, msgId?: string, content?: string, localPath?: string): Promise<MessageResponse> {
|
|
534
600
|
let uploadResult: UploadMediaResponse;
|
|
535
|
-
|
|
601
|
+
const isBase64 = imageUrl.startsWith("data:");
|
|
602
|
+
if (isBase64) {
|
|
536
603
|
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
537
604
|
if (!matches) throw new Error("Invalid Base64 Data URL format");
|
|
538
605
|
uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.IMAGE, undefined, matches[2], false);
|
|
539
606
|
} else {
|
|
540
607
|
uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.IMAGE, imageUrl, undefined, false);
|
|
541
608
|
}
|
|
542
|
-
|
|
609
|
+
const meta: OutboundMeta = {
|
|
610
|
+
text: content,
|
|
611
|
+
mediaType: "image",
|
|
612
|
+
...(!isBase64 ? { mediaUrl: imageUrl } : {}),
|
|
613
|
+
...(localPath ? { mediaLocalPath: localPath } : {}),
|
|
614
|
+
};
|
|
615
|
+
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, content, meta);
|
|
543
616
|
}
|
|
544
617
|
|
|
545
618
|
export async function sendGroupImageMessage(accessToken: string, groupOpenid: string, imageUrl: string, msgId?: string, content?: string): Promise<{ id: string; timestamp: string }> {
|
|
546
619
|
let uploadResult: UploadMediaResponse;
|
|
547
|
-
|
|
620
|
+
const isBase64 = imageUrl.startsWith("data:");
|
|
621
|
+
if (isBase64) {
|
|
548
622
|
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
549
623
|
if (!matches) throw new Error("Invalid Base64 Data URL format");
|
|
550
624
|
uploadResult = await uploadGroupMedia(accessToken, groupOpenid, MediaFileType.IMAGE, undefined, matches[2], false);
|
|
@@ -554,9 +628,13 @@ export async function sendGroupImageMessage(accessToken: string, groupOpenid: st
|
|
|
554
628
|
return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId, content);
|
|
555
629
|
}
|
|
556
630
|
|
|
557
|
-
export async function sendC2CVoiceMessage(accessToken: string, openid: string, voiceBase64: string, msgId?: string
|
|
631
|
+
export async function sendC2CVoiceMessage(accessToken: string, openid: string, voiceBase64: string, msgId?: string, ttsText?: string, filePath?: string): Promise<MessageResponse> {
|
|
558
632
|
const uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.VOICE, undefined, voiceBase64, false);
|
|
559
|
-
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId
|
|
633
|
+
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, undefined, {
|
|
634
|
+
mediaType: "voice",
|
|
635
|
+
...(ttsText ? { ttsText } : {}),
|
|
636
|
+
...(filePath ? { mediaLocalPath: filePath } : {})
|
|
637
|
+
});
|
|
560
638
|
}
|
|
561
639
|
|
|
562
640
|
export async function sendGroupVoiceMessage(accessToken: string, groupOpenid: string, voiceBase64: string, msgId?: string): Promise<{ id: string; timestamp: string }> {
|
|
@@ -564,9 +642,10 @@ export async function sendGroupVoiceMessage(accessToken: string, groupOpenid: st
|
|
|
564
642
|
return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId);
|
|
565
643
|
}
|
|
566
644
|
|
|
567
|
-
export async function sendC2CFileMessage(accessToken: string, openid: string, fileBase64?: string, fileUrl?: string, msgId?: string, fileName?: string): Promise<
|
|
645
|
+
export async function sendC2CFileMessage(accessToken: string, openid: string, fileBase64?: string, fileUrl?: string, msgId?: string, fileName?: string, localFilePath?: string): Promise<MessageResponse> {
|
|
568
646
|
const uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.FILE, fileUrl, fileBase64, false, fileName);
|
|
569
|
-
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId
|
|
647
|
+
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, undefined,
|
|
648
|
+
{ mediaType: "file", mediaUrl: fileUrl, mediaLocalPath: localFilePath ?? fileName });
|
|
570
649
|
}
|
|
571
650
|
|
|
572
651
|
export async function sendGroupFileMessage(accessToken: string, groupOpenid: string, fileBase64?: string, fileUrl?: string, msgId?: string, fileName?: string): Promise<{ id: string; timestamp: string }> {
|
|
@@ -574,9 +653,10 @@ export async function sendGroupFileMessage(accessToken: string, groupOpenid: str
|
|
|
574
653
|
return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId);
|
|
575
654
|
}
|
|
576
655
|
|
|
577
|
-
export async function sendC2CVideoMessage(accessToken: string, openid: string, videoUrl?: string, videoBase64?: string, msgId?: string, content?: string): Promise<
|
|
656
|
+
export async function sendC2CVideoMessage(accessToken: string, openid: string, videoUrl?: string, videoBase64?: string, msgId?: string, content?: string, localPath?: string): Promise<MessageResponse> {
|
|
578
657
|
const uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.VIDEO, videoUrl, videoBase64, false);
|
|
579
|
-
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, content
|
|
658
|
+
return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, content,
|
|
659
|
+
{ text: content, mediaType: "video", ...(videoUrl ? { mediaUrl: videoUrl } : {}), ...(localPath ? { mediaLocalPath: localPath } : {}) });
|
|
580
660
|
}
|
|
581
661
|
|
|
582
662
|
export async function sendGroupVideoMessage(accessToken: string, groupOpenid: string, videoUrl?: string, videoBase64?: string, msgId?: string, content?: string): Promise<{ id: string; timestamp: string }> {
|
package/src/channel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ChannelPlugin,
|
|
3
3
|
type OpenClawConfig,
|
|
4
|
+
type NormalizeTargetResult,
|
|
4
5
|
applyAccountNameToChannelSection,
|
|
5
6
|
deleteAccountFromConfigSection,
|
|
6
7
|
setAccountEnabledInConfigSection,
|
|
@@ -167,7 +168,7 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
|
|
|
167
168
|
* - channel:channelid -> 频道
|
|
168
169
|
* - 纯 openid(32位十六进制)-> 私聊
|
|
169
170
|
*/
|
|
170
|
-
normalizeTarget: (target: string) => {
|
|
171
|
+
normalizeTarget: (target: string): NormalizeTargetResult => {
|
|
171
172
|
// 去掉 qqbot: 前缀(如果有)
|
|
172
173
|
const id = target.replace(/^qqbot:/i, "");
|
|
173
174
|
|