@cxyhhhhh/openclaw-qqbot 1.6.7-alpha.1
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/LICENSE +22 -0
- package/README.md +470 -0
- package/README.zh.md +465 -0
- package/bin/qqbot-cli.js +243 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +26 -0
- package/dist/src/admin-resolver.d.ts +33 -0
- package/dist/src/admin-resolver.js +157 -0
- package/dist/src/api.d.ts +264 -0
- package/dist/src/api.js +777 -0
- package/dist/src/channel.d.ts +29 -0
- package/dist/src/channel.js +452 -0
- package/dist/src/config.d.ts +56 -0
- package/dist/src/config.js +278 -0
- package/dist/src/credential-backup.d.ts +31 -0
- package/dist/src/credential-backup.js +66 -0
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.d.ts +18 -0
- package/dist/src/gateway.js +2021 -0
- package/dist/src/group-history.d.ts +136 -0
- package/dist/src/group-history.js +226 -0
- package/dist/src/image-server.d.ts +87 -0
- package/dist/src/image-server.js +570 -0
- package/dist/src/inbound-attachments.d.ts +60 -0
- package/dist/src/inbound-attachments.js +248 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +263 -0
- package/dist/src/message-gating.d.ts +53 -0
- package/dist/src/message-gating.js +107 -0
- package/dist/src/message-queue.d.ts +86 -0
- package/dist/src/message-queue.js +257 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +203 -0
- package/dist/src/outbound-deliver.d.ts +48 -0
- package/dist/src/outbound-deliver.js +392 -0
- package/dist/src/outbound.d.ts +205 -0
- package/dist/src/outbound.js +926 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +399 -0
- package/dist/src/ref-index-store.d.ts +70 -0
- package/dist/src/ref-index-store.js +250 -0
- package/dist/src/reply-dispatcher.d.ts +35 -0
- package/dist/src/reply-dispatcher.js +311 -0
- package/dist/src/request-context.d.ts +18 -0
- package/dist/src/request-context.js +30 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +52 -0
- package/dist/src/session-store.js +254 -0
- package/dist/src/slash-commands.d.ts +77 -0
- package/dist/src/slash-commands.js +1461 -0
- package/dist/src/startup-greeting.d.ts +30 -0
- package/dist/src/startup-greeting.js +97 -0
- package/dist/src/streaming.d.ts +250 -0
- package/dist/src/streaming.js +914 -0
- package/dist/src/stt.d.ts +21 -0
- package/dist/src/stt.js +70 -0
- package/dist/src/tools/channel.d.ts +16 -0
- package/dist/src/tools/channel.js +234 -0
- package/dist/src/tools/remind.d.ts +2 -0
- package/dist/src/tools/remind.js +248 -0
- package/dist/src/types.d.ts +364 -0
- package/dist/src/types.js +17 -0
- package/dist/src/typing-keepalive.d.ts +27 -0
- package/dist/src/typing-keepalive.js +64 -0
- package/dist/src/update-checker.d.ts +34 -0
- package/dist/src/update-checker.js +160 -0
- package/dist/src/utils/audio-convert.d.ts +98 -0
- package/dist/src/utils/audio-convert.js +755 -0
- package/dist/src/utils/chunked-upload.d.ts +59 -0
- package/dist/src/utils/chunked-upload.js +289 -0
- package/dist/src/utils/file-utils.d.ts +61 -0
- package/dist/src/utils/file-utils.js +172 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/media-send.d.ts +148 -0
- package/dist/src/utils/media-send.js +456 -0
- package/dist/src/utils/media-tags.d.ts +14 -0
- package/dist/src/utils/media-tags.js +164 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/dist/src/utils/platform.d.ts +137 -0
- package/dist/src/utils/platform.js +390 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/dist/src/utils/text-parsing.d.ts +32 -0
- package/dist/src/utils/text-parsing.js +69 -0
- package/dist/src/utils/upload-cache.d.ts +34 -0
- package/dist/src/utils/upload-cache.js +93 -0
- package/index.ts +31 -0
- package/node_modules/@eshaz/web-worker/LICENSE +201 -0
- package/node_modules/@eshaz/web-worker/README.md +134 -0
- package/node_modules/@eshaz/web-worker/browser.js +17 -0
- package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
- package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
- package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
- package/node_modules/@eshaz/web-worker/node.js +223 -0
- package/node_modules/@eshaz/web-worker/package.json +54 -0
- package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
- package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
- package/node_modules/mpg123-decoder/README.md +265 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
- package/node_modules/mpg123-decoder/index.js +8 -0
- package/node_modules/mpg123-decoder/package.json +58 -0
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
- package/node_modules/mpg123-decoder/types.d.ts +30 -0
- package/node_modules/silk-wasm/LICENSE +21 -0
- package/node_modules/silk-wasm/README.md +85 -0
- package/node_modules/silk-wasm/lib/index.cjs +16 -0
- package/node_modules/silk-wasm/lib/index.d.ts +70 -0
- package/node_modules/silk-wasm/lib/index.mjs +16 -0
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
- package/node_modules/silk-wasm/package.json +39 -0
- package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
- package/node_modules/simple-yenc/.prettierignore +1 -0
- package/node_modules/simple-yenc/LICENSE +7 -0
- package/node_modules/simple-yenc/README.md +163 -0
- package/node_modules/simple-yenc/dist/esm.js +1 -0
- package/node_modules/simple-yenc/dist/index.js +1 -0
- package/node_modules/simple-yenc/package.json +50 -0
- package/node_modules/simple-yenc/rollup.config.js +27 -0
- package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +13 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +69 -0
- package/node_modules/ws/wrapper.mjs +8 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +67 -0
- package/preload.cjs +33 -0
- package/scripts/cleanup-legacy-plugins.sh +124 -0
- package/scripts/link-sdk-core.cjs +185 -0
- package/scripts/postinstall-link-sdk.js +113 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-npm.ps1 +451 -0
- package/scripts/upgrade-via-npm.sh +528 -0
- package/scripts/upgrade-via-source.sh +916 -0
- package/skills/qqbot-channel/SKILL.md +263 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +60 -0
- package/skills/qqbot-remind/SKILL.md +149 -0
- package/src/admin-resolver.ts +181 -0
- package/src/api.ts +1138 -0
- package/src/channel.ts +477 -0
- package/src/config.ts +347 -0
- package/src/credential-backup.ts +72 -0
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +2257 -0
- package/src/group-history.ts +328 -0
- package/src/image-server.ts +675 -0
- package/src/inbound-attachments.ts +321 -0
- package/src/known-users.ts +353 -0
- package/src/message-gating.ts +190 -0
- package/src/message-queue.ts +349 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-plugin-sdk.d.ts +587 -0
- package/src/outbound-deliver.ts +473 -0
- package/src/outbound.ts +1119 -0
- package/src/proactive.ts +530 -0
- package/src/ref-index-store.ts +335 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/request-context.ts +39 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/slash-commands.ts +1615 -0
- package/src/startup-greeting.ts +120 -0
- package/src/streaming.ts +1102 -0
- package/src/stt.ts +86 -0
- package/src/tools/channel.ts +281 -0
- package/src/tools/remind.ts +300 -0
- package/src/types.ts +386 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/update-checker.ts +174 -0
- package/src/utils/audio-convert.ts +859 -0
- package/src/utils/chunked-upload.ts +419 -0
- package/src/utils/file-utils.ts +193 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-send.ts +585 -0
- package/src/utils/media-tags.ts +182 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/pkg-version.ts +54 -0
- package/src/utils/platform.ts +435 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/src/utils/text-parsing.ts +75 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出站消息投递模块
|
|
3
|
+
*
|
|
4
|
+
* 从 gateway deliver 回调中提取的两大发送管线:
|
|
5
|
+
* 1. parseAndSendMediaTags — 解析 <qqimg/qqvoice/qqvideo/qqfile/qqmedia> 标签并按顺序发送
|
|
6
|
+
* 2. sendPlainReply — 处理不含媒体标签的普通回复(markdown 图片/纯文本+图片)
|
|
7
|
+
*/
|
|
8
|
+
import type { ResolvedQQBotAccount } from "./types.js";
|
|
9
|
+
export interface DeliverEventContext {
|
|
10
|
+
type: "c2c" | "guild" | "dm" | "group";
|
|
11
|
+
senderId: string;
|
|
12
|
+
messageId: string;
|
|
13
|
+
channelId?: string;
|
|
14
|
+
groupOpenid?: string;
|
|
15
|
+
msgIdx?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DeliverAccountContext {
|
|
18
|
+
account: ResolvedQQBotAccount;
|
|
19
|
+
qualifiedTarget: string;
|
|
20
|
+
log?: {
|
|
21
|
+
info: (msg: string) => void;
|
|
22
|
+
error: (msg: string) => void;
|
|
23
|
+
debug?: (msg: string) => void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** token 重试包装 */
|
|
27
|
+
export type SendWithRetryFn = <T>(sendFn: (token: string) => Promise<T>) => Promise<T>;
|
|
28
|
+
/** 一次性消费引用 ref */
|
|
29
|
+
export type ConsumeQuoteRefFn = () => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* 解析回复文本中的媒体标签并按顺序发送。
|
|
32
|
+
*
|
|
33
|
+
* @returns true 如果检测到媒体标签并已处理;false 表示无媒体标签,调用方继续走普通文本管线
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseAndSendMediaTags(replyText: string, event: DeliverEventContext, actx: DeliverAccountContext, sendWithRetry: SendWithRetryFn, consumeQuoteRef: ConsumeQuoteRefFn): Promise<{
|
|
36
|
+
handled: boolean;
|
|
37
|
+
normalizedText: string;
|
|
38
|
+
}>;
|
|
39
|
+
export interface PlainReplyPayload {
|
|
40
|
+
text?: string;
|
|
41
|
+
mediaUrls?: string[];
|
|
42
|
+
mediaUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 发送不含媒体标签的普通回复。
|
|
46
|
+
* 处理 markdown 图片嵌入、Base64 富媒体、纯文本分块、本地媒体自动路由。
|
|
47
|
+
*/
|
|
48
|
+
export declare function sendPlainReply(payload: PlainReplyPayload, replyText: string, event: DeliverEventContext, actx: DeliverAccountContext, sendWithRetry: SendWithRetryFn, consumeQuoteRef: ConsumeQuoteRefFn, toolMediaUrls: string[]): Promise<void>;
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出站消息投递模块
|
|
3
|
+
*
|
|
4
|
+
* 从 gateway deliver 回调中提取的两大发送管线:
|
|
5
|
+
* 1. parseAndSendMediaTags — 解析 <qqimg/qqvoice/qqvideo/qqfile/qqmedia> 标签并按顺序发送
|
|
6
|
+
* 2. sendPlainReply — 处理不含媒体标签的普通回复(markdown 图片/纯文本+图片)
|
|
7
|
+
*/
|
|
8
|
+
import { sendC2CMessage, sendGroupMessage, sendChannelMessage, sendC2CImageMessage, sendGroupImageMessage } from "./api.js";
|
|
9
|
+
import { sendPhoto, sendMedia as sendMediaAuto } from "./outbound.js";
|
|
10
|
+
import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
|
|
11
|
+
import { getQQBotRuntime } from "./runtime.js";
|
|
12
|
+
import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "./utils/image-size.js";
|
|
13
|
+
import { parseMediaTagsToSendQueue, executeSendQueue } from "./utils/media-send.js";
|
|
14
|
+
import { isLocalPath as isLocalFilePath } from "./utils/platform.js";
|
|
15
|
+
import { filterInternalMarkers } from "./utils/text-parsing.js";
|
|
16
|
+
// ============ 1. 媒体标签解析 + 发送 ============
|
|
17
|
+
/**
|
|
18
|
+
* 解析回复文本中的媒体标签并按顺序发送。
|
|
19
|
+
*
|
|
20
|
+
* @returns true 如果检测到媒体标签并已处理;false 表示无媒体标签,调用方继续走普通文本管线
|
|
21
|
+
*/
|
|
22
|
+
export async function parseAndSendMediaTags(replyText, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
23
|
+
const { account, log } = actx;
|
|
24
|
+
const prefix = `[qqbot:${account.accountId}]`;
|
|
25
|
+
// 使用 media-send.ts 的统一解析器(内含 normalizeMediaTags + 路径编码修复)
|
|
26
|
+
const { hasMediaTags: hasMedia, sendQueue } = parseMediaTagsToSendQueue(replyText, log);
|
|
27
|
+
if (!hasMedia || sendQueue.length === 0) {
|
|
28
|
+
return { handled: false, normalizedText: replyText };
|
|
29
|
+
}
|
|
30
|
+
log?.info(`${prefix} Send queue: ${sendQueue.map(item => item.type).join(" -> ")}`);
|
|
31
|
+
// 构建统一的媒体发送上下文
|
|
32
|
+
const mediaTarget = {
|
|
33
|
+
targetType: event.type === "c2c" ? "c2c" : event.type === "group" ? "group" : "channel",
|
|
34
|
+
targetId: event.type === "c2c" ? event.senderId : event.type === "group" ? event.groupOpenid : event.channelId,
|
|
35
|
+
account,
|
|
36
|
+
replyToId: event.messageId,
|
|
37
|
+
logPrefix: prefix,
|
|
38
|
+
};
|
|
39
|
+
const mediaSendCtx = {
|
|
40
|
+
mediaTarget,
|
|
41
|
+
qualifiedTarget: actx.qualifiedTarget,
|
|
42
|
+
account,
|
|
43
|
+
replyToId: event.messageId,
|
|
44
|
+
log,
|
|
45
|
+
};
|
|
46
|
+
// 使用 media-send.ts 的统一执行器
|
|
47
|
+
await executeSendQueue(sendQueue, mediaSendCtx, {
|
|
48
|
+
onSendText: async (textContent) => {
|
|
49
|
+
await sendTextChunks(filterInternalMarkers(textContent), event, actx, sendWithRetry, consumeQuoteRef);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return { handled: true, normalizedText: replyText };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 发送不含媒体标签的普通回复。
|
|
56
|
+
* 处理 markdown 图片嵌入、Base64 富媒体、纯文本分块、本地媒体自动路由。
|
|
57
|
+
*/
|
|
58
|
+
export async function sendPlainReply(payload, replyText, event, actx, sendWithRetry, consumeQuoteRef, toolMediaUrls) {
|
|
59
|
+
const { account, qualifiedTarget, log } = actx;
|
|
60
|
+
const prefix = `[qqbot:${account.accountId}]`;
|
|
61
|
+
// 预去重:把 payload 自带的媒体 URL 从 toolMediaUrls 中移除,
|
|
62
|
+
// 防止同一个文件既被 payload.mediaUrl/mediaUrls 发送,又被 toolMediaUrls 重复发送
|
|
63
|
+
if (toolMediaUrls.length > 0) {
|
|
64
|
+
const payloadUrls = new Set();
|
|
65
|
+
if (payload.mediaUrl)
|
|
66
|
+
payloadUrls.add(payload.mediaUrl);
|
|
67
|
+
if (payload.mediaUrls)
|
|
68
|
+
for (const u of payload.mediaUrls)
|
|
69
|
+
payloadUrls.add(u);
|
|
70
|
+
if (payloadUrls.size > 0) {
|
|
71
|
+
const before = toolMediaUrls.length;
|
|
72
|
+
const filtered = toolMediaUrls.filter(url => !payloadUrls.has(url));
|
|
73
|
+
if (filtered.length < before) {
|
|
74
|
+
log?.info(`${prefix} Pre-dedup: removed ${before - filtered.length} payload media URL(s) from toolMediaUrls`);
|
|
75
|
+
toolMediaUrls.length = 0;
|
|
76
|
+
toolMediaUrls.push(...filtered);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const collectedImageUrls = [];
|
|
81
|
+
const localMediaToSend = [];
|
|
82
|
+
const collectImageUrl = (url) => {
|
|
83
|
+
if (!url)
|
|
84
|
+
return false;
|
|
85
|
+
const isHttpUrl = url.startsWith("http://") || url.startsWith("https://");
|
|
86
|
+
const isDataUrl = url.startsWith("data:image/");
|
|
87
|
+
if (isHttpUrl || isDataUrl) {
|
|
88
|
+
if (!collectedImageUrls.includes(url)) {
|
|
89
|
+
collectedImageUrls.push(url);
|
|
90
|
+
log?.info(`${prefix} Collected ${isDataUrl ? "Base64" : "media URL"}: ${isDataUrl ? `(length: ${url.length})` : url.slice(0, 80) + "..."}`);
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (isLocalFilePath(url)) {
|
|
95
|
+
if (!localMediaToSend.includes(url)) {
|
|
96
|
+
localMediaToSend.push(url);
|
|
97
|
+
log?.info(`${prefix} Collected local media for auto-routing: ${url}`);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
if (payload.mediaUrls?.length) {
|
|
104
|
+
for (const url of payload.mediaUrls)
|
|
105
|
+
collectImageUrl(url);
|
|
106
|
+
}
|
|
107
|
+
if (payload.mediaUrl)
|
|
108
|
+
collectImageUrl(payload.mediaUrl);
|
|
109
|
+
// 提取 markdown 图片
|
|
110
|
+
const mdImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/gi;
|
|
111
|
+
const mdMatches = [...replyText.matchAll(mdImageRegex)];
|
|
112
|
+
for (const m of mdMatches) {
|
|
113
|
+
const url = m[2]?.trim();
|
|
114
|
+
if (url && !collectedImageUrls.includes(url)) {
|
|
115
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
116
|
+
collectedImageUrls.push(url);
|
|
117
|
+
log?.info(`${prefix} Extracted HTTP image from markdown: ${url.slice(0, 80)}...`);
|
|
118
|
+
}
|
|
119
|
+
else if (isLocalFilePath(url)) {
|
|
120
|
+
if (!localMediaToSend.includes(url)) {
|
|
121
|
+
localMediaToSend.push(url);
|
|
122
|
+
log?.info(`${prefix} Collected local media from markdown for auto-routing: ${url}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 提取裸 URL 图片
|
|
128
|
+
const bareUrlRegex = /(?<![(\["'])(https?:\/\/[^\s)"'<>]+\.(?:png|jpg|jpeg|gif|webp)(?:\?[^\s"'<>]*)?)/gi;
|
|
129
|
+
const bareUrlMatches = [...replyText.matchAll(bareUrlRegex)];
|
|
130
|
+
for (const m of bareUrlMatches) {
|
|
131
|
+
const url = m[1];
|
|
132
|
+
if (url && !collectedImageUrls.includes(url)) {
|
|
133
|
+
collectedImageUrls.push(url);
|
|
134
|
+
log?.info(`${prefix} Extracted bare image URL: ${url.slice(0, 80)}...`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const useMarkdown = account.markdownSupport === true;
|
|
138
|
+
log?.info(`${prefix} Markdown mode: ${useMarkdown}, images: ${collectedImageUrls.length}`);
|
|
139
|
+
let textWithoutImages = filterInternalMarkers(replyText);
|
|
140
|
+
if (useMarkdown) {
|
|
141
|
+
await sendMarkdownReply(textWithoutImages, collectedImageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
await sendPlainTextReply(textWithoutImages, collectedImageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef);
|
|
145
|
+
}
|
|
146
|
+
// 发送本地媒体(由 payload.mediaUrl 或 markdown 本地路径触发)
|
|
147
|
+
if (localMediaToSend.length > 0) {
|
|
148
|
+
log?.info(`${prefix} Sending ${localMediaToSend.length} local media via sendMedia auto-routing`);
|
|
149
|
+
for (const mediaPath of localMediaToSend) {
|
|
150
|
+
try {
|
|
151
|
+
const result = await sendMediaAuto({
|
|
152
|
+
to: qualifiedTarget, text: "", mediaUrl: mediaPath,
|
|
153
|
+
accountId: account.accountId, replyToId: event.messageId, account,
|
|
154
|
+
});
|
|
155
|
+
if (result.error) {
|
|
156
|
+
log?.error(`${prefix} sendMedia(auto) error for ${mediaPath}: ${result.error}`);
|
|
157
|
+
await sendTextChunks("发送失败,请稍后重试。", event, actx, sendWithRetry, consumeQuoteRef);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
log?.info(`${prefix} Sent local media: ${mediaPath}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
log?.error(`${prefix} sendMedia(auto) failed for ${mediaPath}: ${err}`);
|
|
165
|
+
await sendTextChunks("发送失败,请稍后重试。", event, actx, sendWithRetry, consumeQuoteRef);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 转发 tool 阶段收集的媒体(去重:跳过已在 localMediaToSend 或 collectedImageUrls 中发送过的路径)
|
|
170
|
+
if (toolMediaUrls.length > 0) {
|
|
171
|
+
const alreadySent = new Set([...localMediaToSend, ...collectedImageUrls]);
|
|
172
|
+
const dedupedToolMedia = toolMediaUrls.filter(url => !alreadySent.has(url));
|
|
173
|
+
if (dedupedToolMedia.length < toolMediaUrls.length) {
|
|
174
|
+
log?.info(`${prefix} Deduped tool media: ${toolMediaUrls.length} → ${dedupedToolMedia.length} (skipped ${toolMediaUrls.length - dedupedToolMedia.length} already sent via localMedia/collectedImages)`);
|
|
175
|
+
}
|
|
176
|
+
if (dedupedToolMedia.length > 0) {
|
|
177
|
+
log?.info(`${prefix} Forwarding ${dedupedToolMedia.length} tool-collected media URL(s) after block deliver`);
|
|
178
|
+
for (const mediaUrl of dedupedToolMedia) {
|
|
179
|
+
try {
|
|
180
|
+
const result = await sendMediaAuto({
|
|
181
|
+
to: qualifiedTarget, text: "", mediaUrl,
|
|
182
|
+
accountId: account.accountId, replyToId: event.messageId, account,
|
|
183
|
+
});
|
|
184
|
+
if (result.error) {
|
|
185
|
+
log?.error(`${prefix} Tool media forward error: ${result.error}`);
|
|
186
|
+
await sendTextChunks("发送失败,请稍后重试。", event, actx, sendWithRetry, consumeQuoteRef);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
log?.info(`${prefix} Forwarded tool media: ${mediaUrl.slice(0, 80)}...`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
log?.error(`${prefix} Tool media forward failed: ${err}`);
|
|
194
|
+
await sendTextChunks("发送失败,请稍后重试。", event, actx, sendWithRetry, consumeQuoteRef);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
toolMediaUrls.length = 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ============ 内部辅助函数 ============
|
|
202
|
+
/** 发送文本分块(共用逻辑) */
|
|
203
|
+
async function sendTextChunks(text, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
204
|
+
const { account, log } = actx;
|
|
205
|
+
const prefix = `[qqbot:${account.accountId}]`;
|
|
206
|
+
const chunks = getQQBotRuntime().channel.text.chunkMarkdownText(text, TEXT_CHUNK_LIMIT);
|
|
207
|
+
for (const chunk of chunks) {
|
|
208
|
+
try {
|
|
209
|
+
await sendWithRetry(async (token) => {
|
|
210
|
+
const ref = consumeQuoteRef();
|
|
211
|
+
if (event.type === "c2c") {
|
|
212
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
213
|
+
}
|
|
214
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
215
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
216
|
+
}
|
|
217
|
+
else if (event.channelId) {
|
|
218
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
log?.info(`${prefix} Sent text chunk (${chunk.length}/${text.length} chars): ${chunk.slice(0, 50)}...`);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
log?.error(`${prefix} Failed to send text chunk: ${err}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Markdown 模式发送 */
|
|
229
|
+
async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
230
|
+
const { account, log } = actx;
|
|
231
|
+
const prefix = `[qqbot:${account.accountId}]`;
|
|
232
|
+
// 分离图片:公网 URL vs Base64
|
|
233
|
+
const httpImageUrls = [];
|
|
234
|
+
const base64ImageUrls = [];
|
|
235
|
+
for (const url of imageUrls) {
|
|
236
|
+
if (url.startsWith("data:image/"))
|
|
237
|
+
base64ImageUrls.push(url);
|
|
238
|
+
else if (url.startsWith("http://") || url.startsWith("https://"))
|
|
239
|
+
httpImageUrls.push(url);
|
|
240
|
+
}
|
|
241
|
+
log?.info(`${prefix} Image classification: httpUrls=${httpImageUrls.length}, base64=${base64ImageUrls.length}`);
|
|
242
|
+
// 发送 Base64 图片
|
|
243
|
+
if (base64ImageUrls.length > 0) {
|
|
244
|
+
log?.info(`${prefix} Sending ${base64ImageUrls.length} image(s) via Rich Media API...`);
|
|
245
|
+
for (const imageUrl of base64ImageUrls) {
|
|
246
|
+
try {
|
|
247
|
+
await sendWithRetry(async (token) => {
|
|
248
|
+
if (event.type === "c2c") {
|
|
249
|
+
await sendC2CImageMessage(token, event.senderId, imageUrl, event.messageId);
|
|
250
|
+
}
|
|
251
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
252
|
+
await sendGroupImageMessage(token, event.groupOpenid, imageUrl, event.messageId);
|
|
253
|
+
}
|
|
254
|
+
else if (event.channelId) {
|
|
255
|
+
log?.info(`${prefix} Channel does not support rich media, skipping Base64 image`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
log?.info(`${prefix} Sent Base64 image via Rich Media API (size: ${imageUrl.length} chars)`);
|
|
259
|
+
}
|
|
260
|
+
catch (imgErr) {
|
|
261
|
+
log?.error(`${prefix} Failed to send Base64 image via Rich Media API: ${imgErr}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// 处理公网 URL 图片
|
|
266
|
+
const existingMdUrls = new Set(mdMatches.map((m) => m[2]));
|
|
267
|
+
const imagesToAppend = [];
|
|
268
|
+
for (const url of httpImageUrls) {
|
|
269
|
+
if (!existingMdUrls.has(url)) {
|
|
270
|
+
try {
|
|
271
|
+
const size = await getImageSize(url);
|
|
272
|
+
imagesToAppend.push(formatQQBotMarkdownImage(url, size));
|
|
273
|
+
log?.info(`${prefix} Formatted HTTP image: ${size ? `${size.width}x${size.height}` : "default size"} - ${url.slice(0, 60)}...`);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
log?.info(`${prefix} Failed to get image size, using default: ${err}`);
|
|
277
|
+
imagesToAppend.push(formatQQBotMarkdownImage(url, null));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// 补充已有 markdown 图片的尺寸信息
|
|
282
|
+
let result = textWithoutImages;
|
|
283
|
+
for (const m of mdMatches) {
|
|
284
|
+
const fullMatch = m[0];
|
|
285
|
+
const imgUrl = m[2];
|
|
286
|
+
const isHttpUrl = imgUrl.startsWith("http://") || imgUrl.startsWith("https://");
|
|
287
|
+
if (isHttpUrl && !hasQQBotImageSize(fullMatch)) {
|
|
288
|
+
try {
|
|
289
|
+
const size = await getImageSize(imgUrl);
|
|
290
|
+
result = result.replace(fullMatch, formatQQBotMarkdownImage(imgUrl, size));
|
|
291
|
+
log?.info(`${prefix} Updated image with size: ${size ? `${size.width}x${size.height}` : "default"} - ${imgUrl.slice(0, 60)}...`);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
log?.info(`${prefix} Failed to get image size for existing md, using default: ${err}`);
|
|
295
|
+
result = result.replace(fullMatch, formatQQBotMarkdownImage(imgUrl, null));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// 移除裸 URL 图片
|
|
300
|
+
for (const m of bareUrlMatches) {
|
|
301
|
+
result = result.replace(m[0], "").trim();
|
|
302
|
+
}
|
|
303
|
+
// 追加图片
|
|
304
|
+
if (imagesToAppend.length > 0) {
|
|
305
|
+
result = result.trim();
|
|
306
|
+
result = result ? result + "\n\n" + imagesToAppend.join("\n") : imagesToAppend.join("\n");
|
|
307
|
+
}
|
|
308
|
+
// 发送 markdown 文本
|
|
309
|
+
if (result.trim()) {
|
|
310
|
+
const mdChunks = chunkText(result, TEXT_CHUNK_LIMIT);
|
|
311
|
+
for (const chunk of mdChunks) {
|
|
312
|
+
try {
|
|
313
|
+
await sendWithRetry(async (token) => {
|
|
314
|
+
const ref = consumeQuoteRef();
|
|
315
|
+
if (event.type === "c2c") {
|
|
316
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
317
|
+
}
|
|
318
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
319
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
320
|
+
}
|
|
321
|
+
else if (event.channelId) {
|
|
322
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
log?.info(`${prefix} Sent markdown chunk (${chunk.length}/${result.length} chars) with ${httpImageUrls.length} HTTP images (${event.type})`);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
log?.error(`${prefix} Failed to send markdown message chunk: ${err}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** 普通文本模式发送 */
|
|
334
|
+
async function sendPlainTextReply(textWithoutImages, imageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
335
|
+
const { account, log } = actx;
|
|
336
|
+
const prefix = `[qqbot:${account.accountId}]`;
|
|
337
|
+
const imgMediaTarget = {
|
|
338
|
+
targetType: event.type === "c2c" ? "c2c" : event.type === "group" ? "group" : "channel",
|
|
339
|
+
targetId: event.type === "c2c" ? event.senderId : event.type === "group" ? event.groupOpenid : event.channelId,
|
|
340
|
+
account,
|
|
341
|
+
replyToId: event.messageId,
|
|
342
|
+
logPrefix: prefix,
|
|
343
|
+
};
|
|
344
|
+
let result = textWithoutImages;
|
|
345
|
+
for (const m of mdMatches)
|
|
346
|
+
result = result.replace(m[0], "").trim();
|
|
347
|
+
for (const m of bareUrlMatches)
|
|
348
|
+
result = result.replace(m[0], "").trim();
|
|
349
|
+
// 群聊 URL 点号过滤
|
|
350
|
+
if (result && event.type !== "c2c") {
|
|
351
|
+
result = result.replace(/([a-zA-Z0-9])\.([a-zA-Z0-9])/g, "$1_$2");
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
for (const imageUrl of imageUrls) {
|
|
355
|
+
try {
|
|
356
|
+
const imgResult = await sendPhoto(imgMediaTarget, imageUrl);
|
|
357
|
+
if (imgResult.error) {
|
|
358
|
+
log?.error(`${prefix} Failed to send image: ${imgResult.error}`);
|
|
359
|
+
await sendTextChunks(`发送图片失败:${imgResult.error}`, event, actx, sendWithRetry, consumeQuoteRef);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
log?.info(`${prefix} Sent image via sendPhoto: ${imageUrl.slice(0, 80)}...`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (imgErr) {
|
|
366
|
+
log?.error(`${prefix} Failed to send image: ${imgErr}`);
|
|
367
|
+
await sendTextChunks(`发送图片失败:${imgErr}`, event, actx, sendWithRetry, consumeQuoteRef);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (result.trim()) {
|
|
371
|
+
const plainChunks = chunkText(result, TEXT_CHUNK_LIMIT);
|
|
372
|
+
for (const chunk of plainChunks) {
|
|
373
|
+
await sendWithRetry(async (token) => {
|
|
374
|
+
const ref = consumeQuoteRef();
|
|
375
|
+
if (event.type === "c2c") {
|
|
376
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
377
|
+
}
|
|
378
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
379
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
380
|
+
}
|
|
381
|
+
else if (event.channelId) {
|
|
382
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
log?.info(`${prefix} Sent text chunk (${chunk.length}/${result.length} chars) (${event.type})`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
log?.error(`${prefix} Send failed: ${err}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot 消息发送模块
|
|
3
|
+
*/
|
|
4
|
+
import type { ResolvedQQBotAccount } from "./types.js";
|
|
5
|
+
/** 限流检查结果 */
|
|
6
|
+
export interface ReplyLimitResult {
|
|
7
|
+
/** 是否允许被动回复 */
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
/** 剩余被动回复次数 */
|
|
10
|
+
remaining: number;
|
|
11
|
+
/** 是否需要降级为主动消息(超期或超过次数) */
|
|
12
|
+
shouldFallbackToProactive: boolean;
|
|
13
|
+
/** 降级原因 */
|
|
14
|
+
fallbackReason?: "expired" | "limit_exceeded";
|
|
15
|
+
/** 提示消息 */
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 检查是否可以回复该消息(限流检查)
|
|
20
|
+
* @param messageId 消息ID
|
|
21
|
+
* @returns ReplyLimitResult 限流检查结果
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkMessageReplyLimit(messageId: string): ReplyLimitResult;
|
|
24
|
+
/**
|
|
25
|
+
* 记录一次消息回复
|
|
26
|
+
* @param messageId 消息ID
|
|
27
|
+
*/
|
|
28
|
+
export declare function recordMessageReply(messageId: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* 获取消息回复统计信息
|
|
31
|
+
*/
|
|
32
|
+
export declare function getMessageReplyStats(): {
|
|
33
|
+
trackedMessages: number;
|
|
34
|
+
totalReplies: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* 获取消息回复限制配置(供外部查询)
|
|
38
|
+
*/
|
|
39
|
+
export declare function getMessageReplyConfig(): {
|
|
40
|
+
limit: number;
|
|
41
|
+
ttlMs: number;
|
|
42
|
+
ttlHours: number;
|
|
43
|
+
};
|
|
44
|
+
export interface OutboundContext {
|
|
45
|
+
to: string;
|
|
46
|
+
text: string;
|
|
47
|
+
accountId?: string | null;
|
|
48
|
+
replyToId?: string | null;
|
|
49
|
+
account: ResolvedQQBotAccount;
|
|
50
|
+
}
|
|
51
|
+
export interface MediaOutboundContext extends OutboundContext {
|
|
52
|
+
mediaUrl: string;
|
|
53
|
+
/** 可选的 MIME 类型,优先于扩展名判断媒体类型 */
|
|
54
|
+
mimeType?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface OutboundResult {
|
|
57
|
+
channel: string;
|
|
58
|
+
messageId?: string;
|
|
59
|
+
timestamp?: string | number;
|
|
60
|
+
error?: string;
|
|
61
|
+
/** 出站消息的引用索引(ext_info.ref_idx),供引用消息缓存使用 */
|
|
62
|
+
refIdx?: string;
|
|
63
|
+
}
|
|
64
|
+
/** 媒体发送的目标上下文(从 deliver 回调或 sendText 中提取) */
|
|
65
|
+
export interface MediaTargetContext {
|
|
66
|
+
/** 目标类型 */
|
|
67
|
+
targetType: "c2c" | "group" | "channel";
|
|
68
|
+
/** 目标 ID */
|
|
69
|
+
targetId: string;
|
|
70
|
+
/** QQ Bot 账户配置 */
|
|
71
|
+
account: ResolvedQQBotAccount;
|
|
72
|
+
/** 被动回复消息 ID(可选) */
|
|
73
|
+
replyToId?: string;
|
|
74
|
+
/** 日志前缀(可选,用于区分调用来源) */
|
|
75
|
+
logPrefix?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* sendPhoto — 发送图片消息
|
|
79
|
+
*
|
|
80
|
+
* 支持三种来源:
|
|
81
|
+
* - 本地文件路径 → 分片上传
|
|
82
|
+
* - 公网 HTTP/HTTPS URL → 下载到本地 → 分片上传(失败发文本链接兜底)
|
|
83
|
+
* - Base64 Data URL → 直传 QQ API
|
|
84
|
+
*/
|
|
85
|
+
export declare function sendPhoto(ctx: MediaTargetContext, imagePath: string,
|
|
86
|
+
/** 原始来源 URL(仅 fallback 路径使用,记录到引用索引) */
|
|
87
|
+
sourceUrl?: string): Promise<OutboundResult>;
|
|
88
|
+
/**
|
|
89
|
+
* sendVoice — 发送语音消息
|
|
90
|
+
*
|
|
91
|
+
* 支持本地音频文件和公网 URL:
|
|
92
|
+
* - urlDirectUpload=true + 公网URL:先直传平台,失败后下载到本地再转码重试
|
|
93
|
+
* - urlDirectUpload=false + 公网URL:直接下载到本地再转码发送
|
|
94
|
+
* - 本地文件:自动转换为 SILK 格式后上传
|
|
95
|
+
*
|
|
96
|
+
* 支持 transcodeEnabled 配置:禁用时非原生格式 fallback 到文件发送。
|
|
97
|
+
*/
|
|
98
|
+
export declare function sendVoice(ctx: MediaTargetContext, voicePath: string,
|
|
99
|
+
/** 直传格式列表(跳过 SILK 转换),可选 */
|
|
100
|
+
directUploadFormats?: string[],
|
|
101
|
+
/** 是否启用转码(默认 true),false 时非原生格式直接返回错误 */
|
|
102
|
+
transcodeEnabled?: boolean): Promise<OutboundResult>;
|
|
103
|
+
/**
|
|
104
|
+
* sendVideoMsg — 发送视频消息
|
|
105
|
+
*
|
|
106
|
+
* 支持公网 URL(urlDirectUpload 控制直传或下载,失败自动 fallback)和本地文件路径。
|
|
107
|
+
*/
|
|
108
|
+
export declare function sendVideoMsg(ctx: MediaTargetContext, videoPath: string): Promise<OutboundResult>;
|
|
109
|
+
/**
|
|
110
|
+
* sendDocument — 发送文件消息
|
|
111
|
+
*
|
|
112
|
+
* 支持本地文件路径和公网 URL(urlDirectUpload 控制直传或下载,失败自动 fallback)。
|
|
113
|
+
*/
|
|
114
|
+
export declare function sendDocument(ctx: MediaTargetContext, filePath: string): Promise<OutboundResult>;
|
|
115
|
+
/**
|
|
116
|
+
* 发送文本消息
|
|
117
|
+
* - 有 replyToId: 被动回复,1小时内最多回复4次
|
|
118
|
+
* - 无 replyToId: 主动发送,有配额限制(每月4条/用户/群)
|
|
119
|
+
*
|
|
120
|
+
* 注意:
|
|
121
|
+
* 1. 主动消息(无 replyToId)必须有消息内容,不支持流式发送
|
|
122
|
+
* 2. 当被动回复不可用(超期或超过次数)时,自动降级为主动消息
|
|
123
|
+
* 3. 支持 <qqimg>路径</qqimg> 或 <qqimg>路径</img> 格式发送图片
|
|
124
|
+
*/
|
|
125
|
+
export declare function sendText(ctx: OutboundContext): Promise<OutboundResult>;
|
|
126
|
+
/**
|
|
127
|
+
* 主动发送消息(不需要 replyToId,有配额限制:每月 4 条/用户/群)
|
|
128
|
+
*
|
|
129
|
+
* @param account - 账户配置
|
|
130
|
+
* @param to - 目标地址,格式:openid(单聊)或 group:xxx(群聊)
|
|
131
|
+
* @param text - 消息内容
|
|
132
|
+
*/
|
|
133
|
+
export declare function sendProactiveMessage(account: ResolvedQQBotAccount, to: string, text: string): Promise<OutboundResult>;
|
|
134
|
+
/**
|
|
135
|
+
* 发送富媒体消息(图片)
|
|
136
|
+
*
|
|
137
|
+
* 支持以下 mediaUrl 格式:
|
|
138
|
+
* - 公网 URL: https://example.com/image.png
|
|
139
|
+
* - Base64 Data URL: data:image/png;base64,xxxxx
|
|
140
|
+
* - 本地文件路径: /path/to/image.png(自动读取并转换为 Base64)
|
|
141
|
+
*
|
|
142
|
+
* @param ctx - 发送上下文,包含 mediaUrl
|
|
143
|
+
* @returns 发送结果
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* // 发送网络图片
|
|
148
|
+
* const result = await sendMedia({
|
|
149
|
+
* to: "group:xxx",
|
|
150
|
+
* text: "这是图片说明",
|
|
151
|
+
* mediaUrl: "https://example.com/image.png",
|
|
152
|
+
* account,
|
|
153
|
+
* replyToId: msgId,
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* // 发送 Base64 图片
|
|
157
|
+
* const result = await sendMedia({
|
|
158
|
+
* to: "group:xxx",
|
|
159
|
+
* text: "这是图片说明",
|
|
160
|
+
* mediaUrl: "data:image/png;base64,iVBORw0KGgo...",
|
|
161
|
+
* account,
|
|
162
|
+
* replyToId: msgId,
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* // 发送本地文件(自动读取并转换为 Base64)
|
|
166
|
+
* const result = await sendMedia({
|
|
167
|
+
* to: "group:xxx",
|
|
168
|
+
* text: "这是图片说明",
|
|
169
|
+
* mediaUrl: "/tmp/generated-chart.png",
|
|
170
|
+
* account,
|
|
171
|
+
* replyToId: msgId,
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export declare function sendMedia(ctx: MediaOutboundContext): Promise<OutboundResult>;
|
|
176
|
+
/**
|
|
177
|
+
* 发送 Cron 触发的消息
|
|
178
|
+
*
|
|
179
|
+
* 当 OpenClaw cron 任务触发时,消息内容可能是:
|
|
180
|
+
* 1. QQBOT_CRON:{base64} 格式的结构化载荷 - 解码后根据 targetType 和 targetAddress 发送
|
|
181
|
+
* 2. 普通文本 - 直接发送到指定目标
|
|
182
|
+
*
|
|
183
|
+
* @param account - 账户配置
|
|
184
|
+
* @param to - 目标地址(作为后备,如果载荷中没有指定)
|
|
185
|
+
* @param message - 消息内容(可能是 QQBOT_CRON: 格式或普通文本)
|
|
186
|
+
* @returns 发送结果
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* // 处理结构化载荷
|
|
191
|
+
* const result = await sendCronMessage(
|
|
192
|
+
* account,
|
|
193
|
+
* "user_openid", // 后备地址
|
|
194
|
+
* "QQBOT_CRON:eyJ0eXBlIjoiY3Jvbl9yZW1pbmRlciIs..." // Base64 编码的载荷
|
|
195
|
+
* );
|
|
196
|
+
*
|
|
197
|
+
* // 处理普通文本
|
|
198
|
+
* const result = await sendCronMessage(
|
|
199
|
+
* account,
|
|
200
|
+
* "user_openid",
|
|
201
|
+
* "这是一条普通的提醒消息"
|
|
202
|
+
* );
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export declare function sendCronMessage(account: ResolvedQQBotAccount, to: string, message: string): Promise<OutboundResult>;
|