@tencent-connect/openclaw-qqbot 1.5.7 → 1.6.0-alpha.2

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.
Files changed (63) hide show
  1. package/README.md +9 -2
  2. package/README.zh.md +7 -2
  3. package/package.json +1 -1
  4. package/scripts/upgrade-via-npm.sh +85 -115
  5. package/scripts/upgrade-via-source.sh +203 -35
  6. package/skills/qqbot-cron/SKILL.md +46 -423
  7. package/skills/qqbot-media/SKILL.md +29 -182
  8. package/src/api.ts +16 -5
  9. package/src/channel.ts +6 -7
  10. package/src/gateway.ts +510 -525
  11. package/src/image-server.ts +72 -10
  12. package/src/openclaw-plugin-sdk.d.ts +1 -1
  13. package/src/outbound.ts +571 -611
  14. package/src/ref-index-store.ts +1 -1
  15. package/src/slash-commands.ts +425 -0
  16. package/src/types.ts +18 -1
  17. package/src/update-checker.ts +102 -0
  18. package/src/user-messages.ts +73 -0
  19. package/src/utils/audio-convert.ts +69 -4
  20. package/src/utils/media-tags.ts +46 -4
  21. 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 +0 -211
  22. package/dist/index.d.ts +0 -17
  23. package/dist/index.js +0 -22
  24. package/dist/src/api.d.ts +0 -138
  25. package/dist/src/api.js +0 -525
  26. package/dist/src/channel.d.ts +0 -3
  27. package/dist/src/channel.js +0 -337
  28. package/dist/src/config.d.ts +0 -25
  29. package/dist/src/config.js +0 -161
  30. package/dist/src/gateway.d.ts +0 -18
  31. package/dist/src/gateway.js +0 -2468
  32. package/dist/src/image-server.d.ts +0 -62
  33. package/dist/src/image-server.js +0 -401
  34. package/dist/src/known-users.d.ts +0 -100
  35. package/dist/src/known-users.js +0 -263
  36. package/dist/src/onboarding.d.ts +0 -10
  37. package/dist/src/onboarding.js +0 -203
  38. package/dist/src/outbound.d.ts +0 -150
  39. package/dist/src/outbound.js +0 -1175
  40. package/dist/src/proactive.d.ts +0 -170
  41. package/dist/src/proactive.js +0 -399
  42. package/dist/src/runtime.d.ts +0 -3
  43. package/dist/src/runtime.js +0 -10
  44. package/dist/src/session-store.d.ts +0 -52
  45. package/dist/src/session-store.js +0 -254
  46. package/dist/src/slash-commands.d.ts +0 -48
  47. package/dist/src/slash-commands.js +0 -212
  48. package/dist/src/types.d.ts +0 -146
  49. package/dist/src/types.js +0 -1
  50. package/dist/src/utils/audio-convert.d.ts +0 -73
  51. package/dist/src/utils/audio-convert.js +0 -645
  52. package/dist/src/utils/file-utils.d.ts +0 -46
  53. package/dist/src/utils/file-utils.js +0 -107
  54. package/dist/src/utils/image-size.d.ts +0 -51
  55. package/dist/src/utils/image-size.js +0 -234
  56. package/dist/src/utils/media-tags.d.ts +0 -14
  57. package/dist/src/utils/media-tags.js +0 -120
  58. package/dist/src/utils/payload.d.ts +0 -112
  59. package/dist/src/utils/payload.js +0 -186
  60. package/dist/src/utils/platform.d.ts +0 -126
  61. package/dist/src/utils/platform.js +0 -358
  62. package/dist/src/utils/upload-cache.d.ts +0 -34
  63. package/dist/src/utils/upload-cache.js +0 -93
package/dist/src/api.js DELETED
@@ -1,525 +0,0 @@
1
- /**
2
- * QQ Bot API 鉴权和请求封装
3
- * [修复版] 已重构为支持多实例并发,消除全局变量冲突
4
- */
5
- import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
6
- import { sanitizeFileName } from "./utils/platform.js";
7
- const API_BASE = "https://api.sgroup.qq.com";
8
- const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
9
- // 运行时配置
10
- let currentMarkdownSupport = false;
11
- /**
12
- * 初始化 API 配置
13
- * @param options.markdownSupport - 是否支持 markdown 消息(默认 false,需要机器人具备该权限才能启用)
14
- */
15
- export function initApiConfig(options) {
16
- currentMarkdownSupport = options.markdownSupport === true;
17
- }
18
- /**
19
- * 获取当前是否支持 markdown
20
- */
21
- export function isMarkdownSupport() {
22
- return currentMarkdownSupport;
23
- }
24
- // =========================================================================
25
- // 🚀 [核心修复] 将全局状态改为 Map,按 appId 隔离,彻底解决多账号串号问题
26
- // =========================================================================
27
- const tokenCacheMap = new Map();
28
- const tokenFetchPromises = new Map();
29
- /**
30
- * 获取 AccessToken(带缓存 + singleflight 并发安全)
31
- *
32
- * 使用 singleflight 模式:当多个请求同时发现 Token 过期时,
33
- * 只有第一个请求会真正去获取新 Token,其他请求复用同一个 Promise。
34
- *
35
- * 按 appId 隔离,支持多机器人并发请求。
36
- */
37
- export async function getAccessToken(appId, clientSecret) {
38
- const normalizedAppId = String(appId).trim();
39
- const cachedToken = tokenCacheMap.get(normalizedAppId);
40
- // 检查缓存:未过期 且 appId 未变化 时复用
41
- if (cachedToken && Date.now() < cachedToken.expiresAt - 5 * 60 * 1000) {
42
- return cachedToken.token;
43
- }
44
- // Singleflight: 如果当前 appId 已有进行中的 Token 获取请求,复用它
45
- let fetchPromise = tokenFetchPromises.get(normalizedAppId);
46
- if (fetchPromise) {
47
- console.log(`[qqbot-api:${normalizedAppId}] Token fetch in progress, waiting for existing request...`);
48
- return fetchPromise;
49
- }
50
- // 创建新的 Token 获取 Promise(singleflight 入口)
51
- fetchPromise = (async () => {
52
- try {
53
- return await doFetchToken(normalizedAppId, clientSecret);
54
- }
55
- finally {
56
- // 无论成功失败,都清除 Promise 缓存
57
- tokenFetchPromises.delete(normalizedAppId);
58
- }
59
- })();
60
- tokenFetchPromises.set(normalizedAppId, fetchPromise);
61
- return fetchPromise;
62
- }
63
- /**
64
- * 实际执行 Token 获取的内部函数
65
- */
66
- async function doFetchToken(appId, clientSecret) {
67
- const requestBody = { appId, clientSecret };
68
- const requestHeaders = { "Content-Type": "application/json" };
69
- // 打印请求信息(隐藏敏感信息)
70
- console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL}`);
71
- let response;
72
- try {
73
- response = await fetch(TOKEN_URL, {
74
- method: "POST",
75
- headers: requestHeaders,
76
- body: JSON.stringify(requestBody),
77
- });
78
- }
79
- catch (err) {
80
- console.error(`[qqbot-api:${appId}] <<< Network error:`, err);
81
- throw new Error(`Network error getting access_token: ${err instanceof Error ? err.message : String(err)}`);
82
- }
83
- // 打印响应头
84
- const responseHeaders = {};
85
- response.headers.forEach((value, key) => {
86
- responseHeaders[key] = value;
87
- });
88
- console.log(`[qqbot-api:${appId}] <<< Status: ${response.status} ${response.statusText}`);
89
- let data;
90
- let rawBody;
91
- try {
92
- rawBody = await response.text();
93
- // 隐藏 token 值
94
- const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token": "***"');
95
- console.log(`[qqbot-api:${appId}] <<< Body:`, logBody);
96
- data = JSON.parse(rawBody);
97
- }
98
- catch (err) {
99
- console.error(`[qqbot-api:${appId}] <<< Parse error:`, err);
100
- throw new Error(`Failed to parse access_token response: ${err instanceof Error ? err.message : String(err)}`);
101
- }
102
- if (!data.access_token) {
103
- throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
104
- }
105
- const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1000;
106
- tokenCacheMap.set(appId, {
107
- token: data.access_token,
108
- expiresAt,
109
- appId,
110
- });
111
- console.log(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`);
112
- return data.access_token;
113
- }
114
- /**
115
- * 清除 Token 缓存
116
- * @param appId 选填。如果有,只清空特定账号的缓存;如果没有,清空所有账号。
117
- */
118
- export function clearTokenCache(appId) {
119
- if (appId) {
120
- const normalizedAppId = String(appId).trim();
121
- tokenCacheMap.delete(normalizedAppId);
122
- console.log(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
123
- }
124
- else {
125
- tokenCacheMap.clear();
126
- console.log(`[qqbot-api] All token caches cleared.`);
127
- }
128
- }
129
- /**
130
- * 获取 Token 缓存状态(用于监控)
131
- */
132
- export function getTokenStatus(appId) {
133
- if (tokenFetchPromises.has(appId)) {
134
- return { status: "refreshing", expiresAt: tokenCacheMap.get(appId)?.expiresAt ?? null };
135
- }
136
- const cached = tokenCacheMap.get(appId);
137
- if (!cached) {
138
- return { status: "none", expiresAt: null };
139
- }
140
- const isValid = Date.now() < cached.expiresAt - 5 * 60 * 1000;
141
- return { status: isValid ? "valid" : "expired", expiresAt: cached.expiresAt };
142
- }
143
- /**
144
- * 获取全局唯一的消息序号(范围 0 ~ 65535)
145
- * 使用毫秒级时间戳低位 + 随机数异或混合,无状态,避免碰撞
146
- */
147
- export function getNextMsgSeq(_msgId) {
148
- const timePart = Date.now() % 100000000; // 毫秒时间戳后8位
149
- const random = Math.floor(Math.random() * 65536); // 0~65535
150
- return (timePart ^ random) % 65536; // 异或混合后限制在 0~65535
151
- }
152
- // API 请求超时配置(毫秒)
153
- const DEFAULT_API_TIMEOUT = 30000; // 默认 30 秒
154
- const FILE_UPLOAD_TIMEOUT = 120000; // 文件上传 120 秒
155
- /**
156
- * API 请求封装
157
- */
158
- export async function apiRequest(accessToken, method, path, body, timeoutMs) {
159
- const url = `${API_BASE}${path}`;
160
- const headers = {
161
- Authorization: `QQBot ${accessToken}`,
162
- "Content-Type": "application/json",
163
- };
164
- const isFileUpload = path.includes("/files");
165
- const timeout = timeoutMs ?? (isFileUpload ? FILE_UPLOAD_TIMEOUT : DEFAULT_API_TIMEOUT);
166
- const controller = new AbortController();
167
- const timeoutId = setTimeout(() => {
168
- controller.abort();
169
- }, timeout);
170
- const options = {
171
- method,
172
- headers,
173
- signal: controller.signal,
174
- };
175
- if (body) {
176
- options.body = JSON.stringify(body);
177
- }
178
- // 打印请求信息
179
- console.log(`[qqbot-api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
180
- if (body) {
181
- const logBody = { ...body };
182
- if (typeof logBody.file_data === "string") {
183
- logBody.file_data = `<base64 ${logBody.file_data.length} chars>`;
184
- }
185
- }
186
- let res;
187
- try {
188
- res = await fetch(url, options);
189
- }
190
- catch (err) {
191
- clearTimeout(timeoutId);
192
- if (err instanceof Error && err.name === "AbortError") {
193
- console.error(`[qqbot-api] <<< Request timeout after ${timeout}ms`);
194
- throw new Error(`Request timeout[${path}]: exceeded ${timeout}ms`);
195
- }
196
- console.error(`[qqbot-api] <<< Network error:`, err);
197
- throw new Error(`Network error [${path}]: ${err instanceof Error ? err.message : String(err)}`);
198
- }
199
- finally {
200
- clearTimeout(timeoutId);
201
- }
202
- const responseHeaders = {};
203
- res.headers.forEach((value, key) => {
204
- responseHeaders[key] = value;
205
- });
206
- console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}`);
207
- let data;
208
- let rawBody;
209
- try {
210
- rawBody = await res.text();
211
- data = JSON.parse(rawBody);
212
- }
213
- catch (err) {
214
- throw new Error(`Failed to parse response[${path}]: ${err instanceof Error ? err.message : String(err)}`);
215
- }
216
- if (!res.ok) {
217
- const error = data;
218
- throw new Error(`API Error [${path}]: ${error.message ?? JSON.stringify(data)}`);
219
- }
220
- return data;
221
- }
222
- // ============ 上传重试(指数退避) ============
223
- const UPLOAD_MAX_RETRIES = 2;
224
- const UPLOAD_BASE_DELAY_MS = 1000;
225
- async function apiRequestWithRetry(accessToken, method, path, body, maxRetries = UPLOAD_MAX_RETRIES) {
226
- let lastError = null;
227
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
228
- try {
229
- return await apiRequest(accessToken, method, path, body);
230
- }
231
- catch (err) {
232
- lastError = err instanceof Error ? err : new Error(String(err));
233
- const errMsg = lastError.message;
234
- if (errMsg.includes("400") || errMsg.includes("401") || errMsg.includes("Invalid") ||
235
- errMsg.includes("上传超时") || errMsg.includes("timeout") || errMsg.includes("Timeout")) {
236
- throw lastError;
237
- }
238
- if (attempt < maxRetries) {
239
- const delay = UPLOAD_BASE_DELAY_MS * Math.pow(2, attempt);
240
- console.log(`[qqbot-api] Upload attempt ${attempt + 1} failed, retrying in ${delay}ms: ${errMsg.slice(0, 100)}`);
241
- await new Promise(resolve => setTimeout(resolve, delay));
242
- }
243
- }
244
- }
245
- throw lastError;
246
- }
247
- export async function getGatewayUrl(accessToken) {
248
- const data = await apiRequest(accessToken, "GET", "/gateway");
249
- return data.url;
250
- }
251
- function buildMessageBody(content, msgId, msgSeq) {
252
- const body = currentMarkdownSupport
253
- ? {
254
- markdown: { content },
255
- msg_type: 2,
256
- msg_seq: msgSeq,
257
- }
258
- : {
259
- content,
260
- msg_type: 0,
261
- msg_seq: msgSeq,
262
- };
263
- if (msgId) {
264
- body.msg_id = msgId;
265
- }
266
- return body;
267
- }
268
- export async function sendC2CMessage(accessToken, openid, content, msgId) {
269
- const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
270
- const body = buildMessageBody(content, msgId, msgSeq);
271
- return apiRequest(accessToken, "POST", `/v2/users/${openid}/messages`, body);
272
- }
273
- export async function sendC2CInputNotify(accessToken, openid, msgId, inputSecond = 60) {
274
- const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
275
- const body = {
276
- msg_type: 6,
277
- input_notify: {
278
- input_type: 1,
279
- input_second: inputSecond,
280
- },
281
- msg_seq: msgSeq,
282
- ...(msgId ? { msg_id: msgId } : {}),
283
- };
284
- await apiRequest(accessToken, "POST", `/v2/users/${openid}/messages`, body);
285
- }
286
- export async function sendChannelMessage(accessToken, channelId, content, msgId) {
287
- return apiRequest(accessToken, "POST", `/channels/${channelId}/messages`, {
288
- content,
289
- ...(msgId ? { msg_id: msgId } : {}),
290
- });
291
- }
292
- export async function sendGroupMessage(accessToken, groupOpenid, content, msgId) {
293
- const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
294
- const body = buildMessageBody(content, msgId, msgSeq);
295
- return apiRequest(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body);
296
- }
297
- function buildProactiveMessageBody(content) {
298
- if (!content || content.trim().length === 0) {
299
- throw new Error("主动消息内容不能为空 (markdown.content is empty)");
300
- }
301
- if (currentMarkdownSupport) {
302
- return { markdown: { content }, msg_type: 2 };
303
- }
304
- else {
305
- return { content, msg_type: 0 };
306
- }
307
- }
308
- export async function sendProactiveC2CMessage(accessToken, openid, content) {
309
- const body = buildProactiveMessageBody(content);
310
- return apiRequest(accessToken, "POST", `/v2/users/${openid}/messages`, body);
311
- }
312
- export async function sendProactiveGroupMessage(accessToken, groupOpenid, content) {
313
- const body = buildProactiveMessageBody(content);
314
- return apiRequest(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body);
315
- }
316
- // ============ 富媒体消息支持 ============
317
- export var MediaFileType;
318
- (function (MediaFileType) {
319
- MediaFileType[MediaFileType["IMAGE"] = 1] = "IMAGE";
320
- MediaFileType[MediaFileType["VIDEO"] = 2] = "VIDEO";
321
- MediaFileType[MediaFileType["VOICE"] = 3] = "VOICE";
322
- MediaFileType[MediaFileType["FILE"] = 4] = "FILE";
323
- })(MediaFileType || (MediaFileType = {}));
324
- export async function uploadC2CMedia(accessToken, openid, fileType, url, fileData, srvSendMsg = false, fileName) {
325
- if (!url && !fileData)
326
- throw new Error("uploadC2CMedia: url or fileData is required");
327
- if (fileData) {
328
- const contentHash = computeFileHash(fileData);
329
- const cachedInfo = getCachedFileInfo(contentHash, "c2c", openid, fileType);
330
- if (cachedInfo) {
331
- return { file_uuid: "", file_info: cachedInfo, ttl: 0 };
332
- }
333
- }
334
- const body = { file_type: fileType, srv_send_msg: srvSendMsg };
335
- if (url)
336
- body.url = url;
337
- else if (fileData)
338
- body.file_data = fileData;
339
- if (fileType === MediaFileType.FILE && fileName)
340
- body.file_name = sanitizeFileName(fileName);
341
- const result = await apiRequestWithRetry(accessToken, "POST", `/v2/users/${openid}/files`, body);
342
- if (fileData && result.file_info && result.ttl > 0) {
343
- const contentHash = computeFileHash(fileData);
344
- setCachedFileInfo(contentHash, "c2c", openid, fileType, result.file_info, result.file_uuid, result.ttl);
345
- }
346
- return result;
347
- }
348
- export async function uploadGroupMedia(accessToken, groupOpenid, fileType, url, fileData, srvSendMsg = false, fileName) {
349
- if (!url && !fileData)
350
- throw new Error("uploadGroupMedia: url or fileData is required");
351
- if (fileData) {
352
- const contentHash = computeFileHash(fileData);
353
- const cachedInfo = getCachedFileInfo(contentHash, "group", groupOpenid, fileType);
354
- if (cachedInfo) {
355
- return { file_uuid: "", file_info: cachedInfo, ttl: 0 };
356
- }
357
- }
358
- const body = { file_type: fileType, srv_send_msg: srvSendMsg };
359
- if (url)
360
- body.url = url;
361
- else if (fileData)
362
- body.file_data = fileData;
363
- if (fileType === MediaFileType.FILE && fileName)
364
- body.file_name = sanitizeFileName(fileName);
365
- const result = await apiRequestWithRetry(accessToken, "POST", `/v2/groups/${groupOpenid}/files`, body);
366
- if (fileData && result.file_info && result.ttl > 0) {
367
- const contentHash = computeFileHash(fileData);
368
- setCachedFileInfo(contentHash, "group", groupOpenid, fileType, result.file_info, result.file_uuid, result.ttl);
369
- }
370
- return result;
371
- }
372
- export async function sendC2CMediaMessage(accessToken, openid, fileInfo, msgId, content) {
373
- const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
374
- return apiRequest(accessToken, "POST", `/v2/users/${openid}/messages`, {
375
- msg_type: 7,
376
- media: { file_info: fileInfo },
377
- msg_seq: msgSeq,
378
- ...(content ? { content } : {}),
379
- ...(msgId ? { msg_id: msgId } : {}),
380
- });
381
- }
382
- export async function sendGroupMediaMessage(accessToken, groupOpenid, fileInfo, msgId, content) {
383
- const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
384
- return apiRequest(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, {
385
- msg_type: 7,
386
- media: { file_info: fileInfo },
387
- msg_seq: msgSeq,
388
- ...(content ? { content } : {}),
389
- ...(msgId ? { msg_id: msgId } : {}),
390
- });
391
- }
392
- export async function sendC2CImageMessage(accessToken, openid, imageUrl, msgId, content) {
393
- let uploadResult;
394
- if (imageUrl.startsWith("data:")) {
395
- const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
396
- if (!matches)
397
- throw new Error("Invalid Base64 Data URL format");
398
- uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.IMAGE, undefined, matches[2], false);
399
- }
400
- else {
401
- uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.IMAGE, imageUrl, undefined, false);
402
- }
403
- return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, content);
404
- }
405
- export async function sendGroupImageMessage(accessToken, groupOpenid, imageUrl, msgId, content) {
406
- let uploadResult;
407
- if (imageUrl.startsWith("data:")) {
408
- const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
409
- if (!matches)
410
- throw new Error("Invalid Base64 Data URL format");
411
- uploadResult = await uploadGroupMedia(accessToken, groupOpenid, MediaFileType.IMAGE, undefined, matches[2], false);
412
- }
413
- else {
414
- uploadResult = await uploadGroupMedia(accessToken, groupOpenid, MediaFileType.IMAGE, imageUrl, undefined, false);
415
- }
416
- return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId, content);
417
- }
418
- export async function sendC2CVoiceMessage(accessToken, openid, voiceBase64, msgId) {
419
- const uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.VOICE, undefined, voiceBase64, false);
420
- return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId);
421
- }
422
- export async function sendGroupVoiceMessage(accessToken, groupOpenid, voiceBase64, msgId) {
423
- const uploadResult = await uploadGroupMedia(accessToken, groupOpenid, MediaFileType.VOICE, undefined, voiceBase64, false);
424
- return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId);
425
- }
426
- export async function sendC2CFileMessage(accessToken, openid, fileBase64, fileUrl, msgId, fileName) {
427
- const uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.FILE, fileUrl, fileBase64, false, fileName);
428
- return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId);
429
- }
430
- export async function sendGroupFileMessage(accessToken, groupOpenid, fileBase64, fileUrl, msgId, fileName) {
431
- const uploadResult = await uploadGroupMedia(accessToken, groupOpenid, MediaFileType.FILE, fileUrl, fileBase64, false, fileName);
432
- return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId);
433
- }
434
- export async function sendC2CVideoMessage(accessToken, openid, videoUrl, videoBase64, msgId, content) {
435
- const uploadResult = await uploadC2CMedia(accessToken, openid, MediaFileType.VIDEO, videoUrl, videoBase64, false);
436
- return sendC2CMediaMessage(accessToken, openid, uploadResult.file_info, msgId, content);
437
- }
438
- export async function sendGroupVideoMessage(accessToken, groupOpenid, videoUrl, videoBase64, msgId, content) {
439
- const uploadResult = await uploadGroupMedia(accessToken, groupOpenid, MediaFileType.VIDEO, videoUrl, videoBase64, false);
440
- return sendGroupMediaMessage(accessToken, groupOpenid, uploadResult.file_info, msgId, content);
441
- }
442
- const backgroundRefreshControllers = new Map();
443
- export function startBackgroundTokenRefresh(appId, clientSecret, options) {
444
- if (backgroundRefreshControllers.has(appId)) {
445
- console.log(`[qqbot-api:${appId}] Background token refresh already running`);
446
- return;
447
- }
448
- const { refreshAheadMs = 5 * 60 * 1000, randomOffsetMs = 30 * 1000, minRefreshIntervalMs = 60 * 1000, retryDelayMs = 5 * 1000, log, } = options ?? {};
449
- const controller = new AbortController();
450
- backgroundRefreshControllers.set(appId, controller);
451
- const signal = controller.signal;
452
- const refreshLoop = async () => {
453
- log?.info?.(`[qqbot-api:${appId}] Background token refresh started`);
454
- while (!signal.aborted) {
455
- try {
456
- await getAccessToken(appId, clientSecret);
457
- const cached = tokenCacheMap.get(appId);
458
- if (cached) {
459
- const expiresIn = cached.expiresAt - Date.now();
460
- const randomOffset = Math.random() * randomOffsetMs;
461
- const refreshIn = Math.max(expiresIn - refreshAheadMs - randomOffset, minRefreshIntervalMs);
462
- log?.debug?.(`[qqbot-api:${appId}] Token valid, next refresh in ${Math.round(refreshIn / 1000)}s`);
463
- await sleep(refreshIn, signal);
464
- }
465
- else {
466
- log?.debug?.(`[qqbot-api:${appId}] No cached token, retrying soon`);
467
- await sleep(minRefreshIntervalMs, signal);
468
- }
469
- }
470
- catch (err) {
471
- if (signal.aborted)
472
- break;
473
- log?.error?.(`[qqbot-api:${appId}] Background token refresh failed: ${err}`);
474
- await sleep(retryDelayMs, signal);
475
- }
476
- }
477
- backgroundRefreshControllers.delete(appId);
478
- log?.info?.(`[qqbot-api:${appId}] Background token refresh stopped`);
479
- };
480
- refreshLoop().catch((err) => {
481
- backgroundRefreshControllers.delete(appId);
482
- log?.error?.(`[qqbot-api:${appId}] Background token refresh crashed: ${err}`);
483
- });
484
- }
485
- /**
486
- * 停止后台 Token 刷新
487
- * @param appId 选填。如果有,仅停止该账号的定时刷新。
488
- */
489
- export function stopBackgroundTokenRefresh(appId) {
490
- if (appId) {
491
- const controller = backgroundRefreshControllers.get(appId);
492
- if (controller) {
493
- controller.abort();
494
- backgroundRefreshControllers.delete(appId);
495
- }
496
- }
497
- else {
498
- for (const controller of backgroundRefreshControllers.values()) {
499
- controller.abort();
500
- }
501
- backgroundRefreshControllers.clear();
502
- }
503
- }
504
- export function isBackgroundTokenRefreshRunning(appId) {
505
- if (appId)
506
- return backgroundRefreshControllers.has(appId);
507
- return backgroundRefreshControllers.size > 0;
508
- }
509
- async function sleep(ms, signal) {
510
- return new Promise((resolve, reject) => {
511
- const timer = setTimeout(resolve, ms);
512
- if (signal) {
513
- if (signal.aborted) {
514
- clearTimeout(timer);
515
- reject(new Error("Aborted"));
516
- return;
517
- }
518
- const onAbort = () => {
519
- clearTimeout(timer);
520
- reject(new Error("Aborted"));
521
- };
522
- signal.addEventListener("abort", onAbort, { once: true });
523
- }
524
- });
525
- }
@@ -1,3 +0,0 @@
1
- import { type ChannelPlugin } from "openclaw/plugin-sdk";
2
- import type { ResolvedQQBotAccount } from "./types.js";
3
- export declare const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount>;