@ryantest/openclaw-qqbot 0.0.3 → 1.6.6-alpha.0
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 +2 -15
- package/README.zh.md +3 -16
- package/dist/src/admin-resolver.d.ts +12 -6
- package/dist/src/admin-resolver.js +69 -34
- package/dist/src/api.d.ts +105 -1
- package/dist/src/api.js +164 -15
- package/dist/src/channel.js +13 -0
- package/dist/src/config.js +3 -10
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.js +450 -248
- package/dist/src/image-server.d.ts +27 -8
- package/dist/src/image-server.js +179 -71
- package/dist/src/inbound-attachments.d.ts +3 -1
- package/dist/src/inbound-attachments.js +28 -14
- package/dist/src/outbound-deliver.js +77 -148
- package/dist/src/outbound.d.ts +6 -4
- package/dist/src/outbound.js +266 -442
- package/dist/src/reply-dispatcher.js +4 -4
- package/dist/src/request-context.d.ts +18 -0
- package/dist/src/request-context.js +30 -0
- package/dist/src/slash-commands.js +277 -32
- package/dist/src/startup-greeting.d.ts +5 -5
- package/dist/src/startup-greeting.js +32 -13
- package/dist/src/streaming.d.ts +244 -0
- package/dist/src/streaming.js +907 -0
- package/dist/src/tools/remind.js +11 -10
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.js +17 -1
- package/dist/src/update-checker.js +2 -8
- package/dist/src/utils/audio-convert.d.ts +9 -0
- package/dist/src/utils/audio-convert.js +51 -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 +7 -1
- package/dist/src/utils/file-utils.js +24 -2
- package/dist/src/utils/media-send.d.ts +147 -0
- package/dist/src/utils/media-send.js +434 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/node_modules/ws/index.js +15 -6
- package/node_modules/ws/lib/permessage-deflate.js +6 -6
- package/node_modules/ws/lib/websocket-server.js +5 -5
- package/node_modules/ws/lib/websocket.js +6 -6
- package/node_modules/ws/package.json +4 -3
- package/node_modules/ws/wrapper.mjs +14 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +11 -22
- package/scripts/postinstall-link-sdk.js +113 -0
- package/scripts/upgrade-via-npm.ps1 +161 -6
- package/scripts/upgrade-via-npm.sh +311 -104
- package/scripts/upgrade-via-source.sh +117 -0
- package/skills/qqbot-media/SKILL.md +9 -5
- package/skills/qqbot-remind/SKILL.md +3 -3
- package/src/admin-resolver.ts +76 -35
- package/src/api.ts +284 -12
- package/src/channel.ts +12 -0
- package/src/config.ts +3 -10
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +277 -67
- package/src/image-server.ts +213 -77
- package/src/inbound-attachments.ts +32 -15
- package/src/outbound-deliver.ts +77 -157
- package/src/outbound.ts +304 -451
- package/src/reply-dispatcher.ts +4 -4
- package/src/request-context.ts +39 -0
- package/src/slash-commands.ts +303 -33
- package/src/startup-greeting.ts +35 -13
- package/src/streaming.ts +1096 -0
- package/src/tools/remind.ts +15 -11
- package/src/types.ts +111 -0
- package/src/update-checker.ts +2 -7
- package/src/utils/audio-convert.ts +56 -0
- package/src/utils/chunked-upload.ts +419 -0
- package/src/utils/file-utils.ts +28 -2
- package/src/utils/media-send.ts +563 -0
- package/src/utils/pkg-version.ts +54 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/clawdbot.plugin.json +0 -16
- package/dist/src/user-messages.d.ts +0 -8
- package/dist/src/user-messages.js +0 -8
- package/moltbot.plugin.json +0 -16
- package/scripts/upgrade-via-alt-pkg.sh +0 -307
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +0 -46
- package/src/gateway.log +0 -43
- package/src/openclaw-2026-03-21.log +0 -3729
- package/src/user-messages.ts +0 -7
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* 2. sendPlainReply — 处理不含媒体标签的普通回复(markdown 图片/纯文本+图片)
|
|
7
7
|
*/
|
|
8
8
|
import { sendC2CMessage, sendGroupMessage, sendChannelMessage, sendC2CImageMessage, sendGroupImageMessage } from "./api.js";
|
|
9
|
-
import { sendPhoto,
|
|
9
|
+
import { sendPhoto, sendMedia as sendMediaAuto } from "./outbound.js";
|
|
10
10
|
import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
|
|
11
11
|
import { getQQBotRuntime } from "./runtime.js";
|
|
12
12
|
import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "./utils/image-size.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { parseMediaTagsToSendQueue, executeSendQueue } from "./utils/media-send.js";
|
|
14
|
+
import { isLocalPath as isLocalFilePath } from "./utils/platform.js";
|
|
15
15
|
import { filterInternalMarkers } from "./utils/text-parsing.js";
|
|
16
16
|
// ============ 1. 媒体标签解析 + 发送 ============
|
|
17
17
|
/**
|
|
@@ -22,42 +22,13 @@ import { filterInternalMarkers } from "./utils/text-parsing.js";
|
|
|
22
22
|
export async function parseAndSendMediaTags(replyText, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
23
23
|
const { account, log } = actx;
|
|
24
24
|
const prefix = `[qqbot:${account.accountId}]`;
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (mediaTagMatches.length === 0) {
|
|
30
|
-
return { handled: false, normalizedText: text };
|
|
31
|
-
}
|
|
32
|
-
const tagCounts = mediaTagMatches.reduce((acc, m) => { const t = m[1].toLowerCase(); acc[t] = (acc[t] ?? 0) + 1; return acc; }, {});
|
|
33
|
-
log?.info(`${prefix} Detected media tags: ${Object.entries(tagCounts).map(([k, v]) => `${v} <${k}>`).join(", ")}`);
|
|
34
|
-
const sendQueue = [];
|
|
35
|
-
let lastIndex = 0;
|
|
36
|
-
const regex2 = /<(qqimg|qqvoice|qqvideo|qqfile|qqmedia)>([^<>]+)<\/(?:qqimg|qqvoice|qqvideo|qqfile|qqmedia|img)>/gi;
|
|
37
|
-
let match;
|
|
38
|
-
while ((match = regex2.exec(text)) !== null) {
|
|
39
|
-
const textBefore = text.slice(lastIndex, match.index).replace(/\n{3,}/g, "\n\n").trim();
|
|
40
|
-
if (textBefore) {
|
|
41
|
-
sendQueue.push({ type: "text", content: filterInternalMarkers(textBefore) });
|
|
42
|
-
}
|
|
43
|
-
const tagName = match[1].toLowerCase();
|
|
44
|
-
let mediaPath = decodeMediaPath(match[2]?.trim() ?? "", log, prefix);
|
|
45
|
-
if (mediaPath) {
|
|
46
|
-
const typeMap = {
|
|
47
|
-
qqmedia: "media", qqvoice: "voice", qqvideo: "video", qqfile: "file",
|
|
48
|
-
};
|
|
49
|
-
const itemType = typeMap[tagName] ?? "image";
|
|
50
|
-
sendQueue.push({ type: itemType, content: mediaPath });
|
|
51
|
-
log?.info(`${prefix} Found ${itemType} in <${tagName}>: ${mediaPath}`);
|
|
52
|
-
}
|
|
53
|
-
lastIndex = match.index + match[0].length;
|
|
54
|
-
}
|
|
55
|
-
const textAfter = text.slice(lastIndex).replace(/\n{3,}/g, "\n\n").trim();
|
|
56
|
-
if (textAfter) {
|
|
57
|
-
sendQueue.push({ type: "text", content: filterInternalMarkers(textAfter) });
|
|
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 };
|
|
58
29
|
}
|
|
59
30
|
log?.info(`${prefix} Send queue: ${sendQueue.map(item => item.type).join(" -> ")}`);
|
|
60
|
-
//
|
|
31
|
+
// 构建统一的媒体发送上下文
|
|
61
32
|
const mediaTarget = {
|
|
62
33
|
targetType: event.type === "c2c" ? "c2c" : event.type === "group" ? "group" : "channel",
|
|
63
34
|
targetId: event.type === "c2c" ? event.senderId : event.type === "group" ? event.groupOpenid : event.channelId,
|
|
@@ -65,42 +36,20 @@ export async function parseAndSendMediaTags(replyText, event, actx, sendWithRetr
|
|
|
65
36
|
replyToId: event.messageId,
|
|
66
37
|
logPrefix: prefix,
|
|
67
38
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (result.error)
|
|
83
|
-
log?.error(`${prefix} sendVideoMsg error: ${result.error}`);
|
|
84
|
-
}
|
|
85
|
-
else if (item.type === "file") {
|
|
86
|
-
const result = await sendDocument(mediaTarget, item.content);
|
|
87
|
-
if (result.error)
|
|
88
|
-
log?.error(`${prefix} sendDocument error: ${result.error}`);
|
|
89
|
-
}
|
|
90
|
-
else if (item.type === "media") {
|
|
91
|
-
const result = await sendMediaAuto({
|
|
92
|
-
to: actx.qualifiedTarget,
|
|
93
|
-
text: "",
|
|
94
|
-
mediaUrl: item.content,
|
|
95
|
-
accountId: account.accountId,
|
|
96
|
-
replyToId: event.messageId,
|
|
97
|
-
account,
|
|
98
|
-
});
|
|
99
|
-
if (result.error)
|
|
100
|
-
log?.error(`${prefix} sendMedia(auto) error: ${result.error}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return { handled: true, normalizedText: text };
|
|
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 };
|
|
104
53
|
}
|
|
105
54
|
/**
|
|
106
55
|
* 发送不含媒体标签的普通回复。
|
|
@@ -109,6 +58,25 @@ export async function parseAndSendMediaTags(replyText, event, actx, sendWithRetr
|
|
|
109
58
|
export async function sendPlainReply(payload, replyText, event, actx, sendWithRetry, consumeQuoteRef, toolMediaUrls) {
|
|
110
59
|
const { account, qualifiedTarget, log } = actx;
|
|
111
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
|
+
}
|
|
112
80
|
const collectedImageUrls = [];
|
|
113
81
|
const localMediaToSend = [];
|
|
114
82
|
const collectImageUrl = (url) => {
|
|
@@ -184,78 +152,52 @@ export async function sendPlainReply(payload, replyText, event, actx, sendWithRe
|
|
|
184
152
|
to: qualifiedTarget, text: "", mediaUrl: mediaPath,
|
|
185
153
|
accountId: account.accountId, replyToId: event.messageId, account,
|
|
186
154
|
});
|
|
187
|
-
if (result.error)
|
|
155
|
+
if (result.error) {
|
|
188
156
|
log?.error(`${prefix} sendMedia(auto) error for ${mediaPath}: ${result.error}`);
|
|
189
|
-
|
|
157
|
+
await sendTextChunks(result.error, event, actx, sendWithRetry, consumeQuoteRef);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
190
160
|
log?.info(`${prefix} Sent local media: ${mediaPath}`);
|
|
161
|
+
}
|
|
191
162
|
}
|
|
192
163
|
catch (err) {
|
|
193
164
|
log?.error(`${prefix} sendMedia(auto) failed for ${mediaPath}: ${err}`);
|
|
165
|
+
await sendTextChunks(`发送媒体失败:${err}`, event, actx, sendWithRetry, consumeQuoteRef);
|
|
194
166
|
}
|
|
195
167
|
}
|
|
196
168
|
}
|
|
197
|
-
// 转发 tool
|
|
169
|
+
// 转发 tool 阶段收集的媒体(去重:跳过已在 localMediaToSend 或 collectedImageUrls 中发送过的路径)
|
|
198
170
|
if (toolMediaUrls.length > 0) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
to: qualifiedTarget, text: "", mediaUrl,
|
|
204
|
-
accountId: account.accountId, replyToId: event.messageId, account,
|
|
205
|
-
});
|
|
206
|
-
if (result.error)
|
|
207
|
-
log?.error(`${prefix} Tool media forward error: ${result.error}`);
|
|
208
|
-
else
|
|
209
|
-
log?.info(`${prefix} Forwarded tool media: ${mediaUrl.slice(0, 80)}...`);
|
|
210
|
-
}
|
|
211
|
-
catch (err) {
|
|
212
|
-
log?.error(`${prefix} Tool media forward failed: ${err}`);
|
|
213
|
-
}
|
|
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)`);
|
|
214
175
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (hasOctal || hasNonASCII) {
|
|
231
|
-
log?.debug?.(`${prefix} Decoding path with mixed encoding: ${mediaPath}`);
|
|
232
|
-
let decoded = mediaPath.replace(/\\([0-7]{1,3})/g, (_, octal) => {
|
|
233
|
-
return String.fromCharCode(parseInt(octal, 8));
|
|
234
|
-
});
|
|
235
|
-
const bytes = [];
|
|
236
|
-
for (let i = 0; i < decoded.length; i++) {
|
|
237
|
-
const code = decoded.charCodeAt(i);
|
|
238
|
-
if (code <= 0xFF) {
|
|
239
|
-
bytes.push(code);
|
|
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(result.error, event, actx, sendWithRetry, consumeQuoteRef);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
log?.info(`${prefix} Forwarded tool media: ${mediaUrl.slice(0, 80)}...`);
|
|
190
|
+
}
|
|
240
191
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
bytes.push(...charBytes);
|
|
192
|
+
catch (err) {
|
|
193
|
+
log?.error(`${prefix} Tool media forward failed: ${err}`);
|
|
244
194
|
}
|
|
245
195
|
}
|
|
246
|
-
const buffer = Buffer.from(bytes);
|
|
247
|
-
const utf8Decoded = buffer.toString("utf8");
|
|
248
|
-
if (!utf8Decoded.includes("\uFFFD") || utf8Decoded.length < decoded.length) {
|
|
249
|
-
mediaPath = utf8Decoded;
|
|
250
|
-
log?.debug?.(`${prefix} Successfully decoded path: ${mediaPath}`);
|
|
251
|
-
}
|
|
252
196
|
}
|
|
197
|
+
toolMediaUrls.length = 0;
|
|
253
198
|
}
|
|
254
|
-
catch (decodeErr) {
|
|
255
|
-
log?.error(`${prefix} Path decode error: ${decodeErr}`);
|
|
256
|
-
}
|
|
257
|
-
return mediaPath;
|
|
258
199
|
}
|
|
200
|
+
// ============ 内部辅助函数 ============
|
|
259
201
|
/** 发送文本分块(共用逻辑) */
|
|
260
202
|
async function sendTextChunks(text, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
261
203
|
const { account, log } = actx;
|
|
@@ -282,23 +224,6 @@ async function sendTextChunks(text, event, actx, sendWithRetry, consumeQuoteRef)
|
|
|
282
224
|
}
|
|
283
225
|
}
|
|
284
226
|
}
|
|
285
|
-
/** 语音发送(带 45s 超时保护) */
|
|
286
|
-
async function sendVoiceWithTimeout(target, voicePath, account, log, prefix) {
|
|
287
|
-
const uploadFormats = account.config?.audioFormatPolicy?.uploadDirectFormats ?? account.config?.voiceDirectUploadFormats;
|
|
288
|
-
const transcodeEnabled = account.config?.audioFormatPolicy?.transcodeEnabled !== false;
|
|
289
|
-
const voiceTimeout = 45000;
|
|
290
|
-
try {
|
|
291
|
-
const result = await Promise.race([
|
|
292
|
-
sendVoice(target, voicePath, uploadFormats, transcodeEnabled),
|
|
293
|
-
new Promise((resolve) => setTimeout(() => resolve({ channel: "qqbot", error: "语音发送超时,已跳过" }), voiceTimeout)),
|
|
294
|
-
]);
|
|
295
|
-
if (result.error)
|
|
296
|
-
log?.error(`${prefix} sendVoice error: ${result.error}`);
|
|
297
|
-
}
|
|
298
|
-
catch (err) {
|
|
299
|
-
log?.error(`${prefix} sendVoice unexpected error: ${err}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
227
|
/** Markdown 模式发送 */
|
|
303
228
|
async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef) {
|
|
304
229
|
const { account, log } = actx;
|
|
@@ -428,13 +353,17 @@ async function sendPlainTextReply(textWithoutImages, imageUrls, mdMatches, bareU
|
|
|
428
353
|
for (const imageUrl of imageUrls) {
|
|
429
354
|
try {
|
|
430
355
|
const imgResult = await sendPhoto(imgMediaTarget, imageUrl);
|
|
431
|
-
if (imgResult.error)
|
|
356
|
+
if (imgResult.error) {
|
|
432
357
|
log?.error(`${prefix} Failed to send image: ${imgResult.error}`);
|
|
433
|
-
|
|
358
|
+
await sendTextChunks(`发送图片失败:${imgResult.error}`, event, actx, sendWithRetry, consumeQuoteRef);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
434
361
|
log?.info(`${prefix} Sent image via sendPhoto: ${imageUrl.slice(0, 80)}...`);
|
|
362
|
+
}
|
|
435
363
|
}
|
|
436
364
|
catch (imgErr) {
|
|
437
365
|
log?.error(`${prefix} Failed to send image: ${imgErr}`);
|
|
366
|
+
await sendTextChunks(`发送图片失败:${imgErr}`, event, actx, sendWithRetry, consumeQuoteRef);
|
|
438
367
|
}
|
|
439
368
|
}
|
|
440
369
|
if (result.trim()) {
|
package/dist/src/outbound.d.ts
CHANGED
|
@@ -78,11 +78,13 @@ export interface MediaTargetContext {
|
|
|
78
78
|
* sendPhoto — 发送图片消息(对齐 Telegram sendPhoto)
|
|
79
79
|
*
|
|
80
80
|
* 支持三种来源:
|
|
81
|
-
* -
|
|
82
|
-
* - 公网 HTTP/HTTPS URL
|
|
83
|
-
* - Base64 Data URL
|
|
81
|
+
* - 本地文件路径 → 分片上传
|
|
82
|
+
* - 公网 HTTP/HTTPS URL → 下载到本地 → 分片上传(失败发文本链接兜底)
|
|
83
|
+
* - Base64 Data URL → 直传 QQ API
|
|
84
84
|
*/
|
|
85
|
-
export declare function sendPhoto(ctx: MediaTargetContext, imagePath: string
|
|
85
|
+
export declare function sendPhoto(ctx: MediaTargetContext, imagePath: string,
|
|
86
|
+
/** 原始来源 URL(仅 fallback 路径使用,记录到引用索引) */
|
|
87
|
+
sourceUrl?: string): Promise<OutboundResult>;
|
|
86
88
|
/**
|
|
87
89
|
* sendVoice — 发送语音消息(对齐 Telegram sendVoice)
|
|
88
90
|
*
|