@karinjs/adapter-qqbot 3.0.7 → 3.1.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/dist/index.d.ts +945 -14
- package/dist/index.js +740 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import karin, { AdapterBase, app, buttonHandle, common, createDirectMessage, cre
|
|
|
6
6
|
import { URL } from "url";
|
|
7
7
|
import lodash from "node-karin/lodash";
|
|
8
8
|
import { createReadStream } from "node:fs";
|
|
9
|
-
import FormData from "form-data";
|
|
10
9
|
import axios from "node-karin/axios";
|
|
10
|
+
import FormData from "form-data";
|
|
11
11
|
import crypto from "node:crypto";
|
|
12
12
|
import { open, readFile, stat } from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
@@ -274,7 +274,25 @@ const errorCodeMap = {
|
|
|
274
274
|
1100307: "非白名单应用子频道",
|
|
275
275
|
1100308: "触发频道内限频",
|
|
276
276
|
1100499: "其他错误",
|
|
277
|
-
3300006: "安全打击"
|
|
277
|
+
3300006: "安全打击",
|
|
278
|
+
40030001: "参数错误,请检查请求参数是否正确",
|
|
279
|
+
40030006: "指令面板不存在,请确认 panel_id 是否正确",
|
|
280
|
+
40030008: "URL 格式错误,请确认 URL 以 https:// 开头",
|
|
281
|
+
40030009: "指令面板操作进行中,存在并发操作冲突,请稍后重试",
|
|
282
|
+
40030011: "生效场景不合法,scope 仅支持 c2c/group/channel/dm",
|
|
283
|
+
40030012: "生效范围不合法,target_type 仅支持 all/specific,channel/dm 场景仅支持 all",
|
|
284
|
+
40030013: "超出数量限制,具体限制值见返回信息中的 limit",
|
|
285
|
+
40030014: "菜单类型不合法,menu.type 仅支持 switch/send_message/link/menu",
|
|
286
|
+
40030015: "面板元素类型不合法,panel_item.type 仅支持 command/link",
|
|
287
|
+
40030016: "必填字段缺失,请检查必填字段是否全部正确传入",
|
|
288
|
+
40030017: "操作类型不合法,op 仅支持 add/del",
|
|
289
|
+
40030018: "当前场景不支持此操作,请检查 scope 是否支持当前操作",
|
|
290
|
+
40030020: "内容存在安全风险,请检查菜单/面板内容是否包含敏感信息",
|
|
291
|
+
40030021: "全局面板不支持添加指定关联对象,请使用 specific 模式",
|
|
292
|
+
40061001: "请求参数无效,请检查请求参数格式",
|
|
293
|
+
40062003: "无操作权限,请检查机器人是否为群管理员,或发消息的用户是否为普通用户",
|
|
294
|
+
40064004: "已超出消息撤回时限,消息发送超过 2 分钟后不可撤回",
|
|
295
|
+
50065001: "消息撤回失败,请稍后重试"
|
|
278
296
|
};
|
|
279
297
|
const rangeDefs = [
|
|
280
298
|
{
|
|
@@ -357,6 +375,23 @@ const formatOpenAPIError = (status, code, message) => {
|
|
|
357
375
|
//#endregion
|
|
358
376
|
//#region src/core/api/http.ts
|
|
359
377
|
/**
|
|
378
|
+
* 把 {@link RequestOptions} 转为 axios 请求配置。
|
|
379
|
+
*
|
|
380
|
+
* @param options 请求可选配置。
|
|
381
|
+
* @returns axios 请求配置。
|
|
382
|
+
*/
|
|
383
|
+
const toAxiosConfig = (options) => {
|
|
384
|
+
if (!options) return {};
|
|
385
|
+
const { headers, timeout, params, signal, responseType } = options;
|
|
386
|
+
return {
|
|
387
|
+
headers,
|
|
388
|
+
timeout,
|
|
389
|
+
params,
|
|
390
|
+
signal,
|
|
391
|
+
responseType
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
360
395
|
* 包装 axios 错误为可读多行 message
|
|
361
396
|
* 如果响应中包含 QQ 官方错误码(err_code),会自动映射为中文描述
|
|
362
397
|
*/
|
|
@@ -400,50 +435,195 @@ const redactRequestData = (value) => {
|
|
|
400
435
|
};
|
|
401
436
|
/**
|
|
402
437
|
* Http 基础类,子模块继承此类共享 axios 实例与错误格式化
|
|
438
|
+
*
|
|
439
|
+
* 各请求方法均为 `protected`,只供各领域 API 子类内部使用;
|
|
440
|
+
* 需要给下游插件开放的裸请求入口见 {@link RequestApi}。
|
|
403
441
|
*/
|
|
404
442
|
var Http = class {
|
|
405
443
|
axios;
|
|
406
444
|
constructor(axios) {
|
|
407
445
|
this.axios = axios;
|
|
408
446
|
}
|
|
409
|
-
async get(path) {
|
|
447
|
+
async get(path, options) {
|
|
410
448
|
try {
|
|
411
|
-
const { data } = await this.axios.get(path);
|
|
449
|
+
const { data } = await this.axios.get(path, toAxiosConfig(options));
|
|
412
450
|
return data;
|
|
413
451
|
} catch (err) {
|
|
414
452
|
throw formatError(path, void 0, err);
|
|
415
453
|
}
|
|
416
454
|
}
|
|
417
|
-
async post(path, body = {},
|
|
455
|
+
async post(path, body = {}, options) {
|
|
418
456
|
try {
|
|
419
|
-
const { data } = await this.axios.post(path, body,
|
|
420
|
-
headers,
|
|
421
|
-
timeout
|
|
422
|
-
});
|
|
457
|
+
const { data } = await this.axios.post(path, body, toAxiosConfig(options));
|
|
423
458
|
return data;
|
|
424
459
|
} catch (err) {
|
|
425
460
|
throw formatError(path, body, err);
|
|
426
461
|
}
|
|
427
462
|
}
|
|
428
|
-
async put(path, body = {},
|
|
463
|
+
async put(path, body = {}, options) {
|
|
429
464
|
try {
|
|
430
|
-
const { data } = await this.axios.put(path, body,
|
|
431
|
-
headers,
|
|
432
|
-
timeout
|
|
433
|
-
});
|
|
465
|
+
const { data } = await this.axios.put(path, body, toAxiosConfig(options));
|
|
434
466
|
return data;
|
|
435
467
|
} catch (err) {
|
|
436
468
|
throw formatError(path, body, err);
|
|
437
469
|
}
|
|
438
470
|
}
|
|
439
|
-
async
|
|
471
|
+
async patch(path, body = {}, options) {
|
|
440
472
|
try {
|
|
441
|
-
const { data } = await this.axios.
|
|
473
|
+
const { data } = await this.axios.patch(path, body, toAxiosConfig(options));
|
|
474
|
+
return data;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
throw formatError(path, body, err);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async delete(path, options) {
|
|
480
|
+
try {
|
|
481
|
+
const { data } = await this.axios.delete(path, toAxiosConfig(options));
|
|
442
482
|
return data;
|
|
443
483
|
} catch (err) {
|
|
444
484
|
throw formatError(path, void 0, err);
|
|
445
485
|
}
|
|
446
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* 任意 method 的裸请求,返回响应体。
|
|
489
|
+
*
|
|
490
|
+
* @param config axios 请求配置,`url` 相对 `baseURL`。
|
|
491
|
+
* @returns 响应体 `data`。
|
|
492
|
+
*/
|
|
493
|
+
async request(config) {
|
|
494
|
+
const label = `${String(config.method ?? "GET").toUpperCase()} ${config.url ?? ""}`;
|
|
495
|
+
try {
|
|
496
|
+
const { data } = await this.axios.request(config);
|
|
497
|
+
return data;
|
|
498
|
+
} catch (err) {
|
|
499
|
+
throw formatError(label, config.data, err);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* 任意 method 的裸请求,返回完整 axios 响应。
|
|
504
|
+
*
|
|
505
|
+
* 需要读取响应头或状态码时使用。
|
|
506
|
+
*
|
|
507
|
+
* @param config axios 请求配置,`url` 相对 `baseURL`。
|
|
508
|
+
* @returns 完整 axios 响应。
|
|
509
|
+
*/
|
|
510
|
+
async requestRaw(config) {
|
|
511
|
+
const label = `${String(config.method ?? "GET").toUpperCase()} ${config.url ?? ""}`;
|
|
512
|
+
try {
|
|
513
|
+
return await this.axios.request(config);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
throw formatError(label, config.data, err);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/core/api/request.ts
|
|
522
|
+
/**
|
|
523
|
+
* 裸请求入口,供下游插件直接调用官方 OpenAPI。
|
|
524
|
+
*
|
|
525
|
+
* {@link Http} 的请求方法是 `protected`,只供各领域 API 子类内部使用;
|
|
526
|
+
* 本类把同一套方法以 `public` 暴露出来,用于官方新接口尚未被适配器封装、
|
|
527
|
+
* 或需要传自定义参数的场景:
|
|
528
|
+
*
|
|
529
|
+
* ```ts
|
|
530
|
+
* const bot = e.bot as AdapterQQBot
|
|
531
|
+
*
|
|
532
|
+
* // GET,query 走 params
|
|
533
|
+
* const menu = await bot.super.request.get('/v2/menu')
|
|
534
|
+
* const list = await bot.super.request.get('/v2/panels', { params: { scope: 'group', limit: 20 } })
|
|
535
|
+
*
|
|
536
|
+
* // POST / PUT / PATCH / DELETE
|
|
537
|
+
* await bot.super.request.post(`/v2/groups/${groupId}/messages`, { content: 'hi', msg_type: 0 })
|
|
538
|
+
* await bot.super.request.put('/v2/menu', { menu })
|
|
539
|
+
*
|
|
540
|
+
* // 任意 method
|
|
541
|
+
* await bot.super.request.request({ method: 'POST', url: '/v2/xxx', data: {} })
|
|
542
|
+
* ```
|
|
543
|
+
*
|
|
544
|
+
* 与直接用 `bot.super.axios` 的区别:
|
|
545
|
+
* - 自动解包 `response.data`;
|
|
546
|
+
* - 失败时抛出的 Error 已带请求路径、脱敏请求体、QQ 错误码中文描述和 TraceID。
|
|
547
|
+
*
|
|
548
|
+
* 需要读响应头或状态码时用 {@link RequestApi.requestRaw};
|
|
549
|
+
* 需要完全裸的 axios 时用 `bot.super.axios`。
|
|
550
|
+
*
|
|
551
|
+
* 路径相对官方 API baseURL,鉴权头由拦截器自动注入,无需自己传 Authorization。
|
|
552
|
+
*/
|
|
553
|
+
var RequestApi = class extends Http {
|
|
554
|
+
/**
|
|
555
|
+
* GET 请求。
|
|
556
|
+
*
|
|
557
|
+
* @param path 接口路径,相对 baseURL。
|
|
558
|
+
* @param options 可选配置(params / headers / timeout 等)。
|
|
559
|
+
* @returns 响应体。
|
|
560
|
+
*/
|
|
561
|
+
get(path, options) {
|
|
562
|
+
return super.get(path, options);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* POST 请求。
|
|
566
|
+
*
|
|
567
|
+
* @param path 接口路径,相对 baseURL。
|
|
568
|
+
* @param body 请求体,默认空对象。
|
|
569
|
+
* @param options 可选配置(params / headers / timeout 等)。
|
|
570
|
+
* @returns 响应体。
|
|
571
|
+
*/
|
|
572
|
+
post(path, body, options) {
|
|
573
|
+
return super.post(path, body, options);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* PUT 请求。
|
|
577
|
+
*
|
|
578
|
+
* @param path 接口路径,相对 baseURL。
|
|
579
|
+
* @param body 请求体,默认空对象。
|
|
580
|
+
* @param options 可选配置(params / headers / timeout 等)。
|
|
581
|
+
* @returns 响应体。
|
|
582
|
+
*/
|
|
583
|
+
put(path, body, options) {
|
|
584
|
+
return super.put(path, body, options);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* PATCH 请求。
|
|
588
|
+
*
|
|
589
|
+
* @param path 接口路径,相对 baseURL。
|
|
590
|
+
* @param body 请求体,默认空对象。
|
|
591
|
+
* @param options 可选配置(params / headers / timeout 等)。
|
|
592
|
+
* @returns 响应体。
|
|
593
|
+
*/
|
|
594
|
+
patch(path, body, options) {
|
|
595
|
+
return super.patch(path, body, options);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* DELETE 请求。
|
|
599
|
+
*
|
|
600
|
+
* @param path 接口路径,相对 baseURL。
|
|
601
|
+
* @param options 可选配置(params / headers / timeout 等)。
|
|
602
|
+
* @returns 响应体。
|
|
603
|
+
*/
|
|
604
|
+
delete(path, options) {
|
|
605
|
+
return super.delete(path, options);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* 任意 method 的请求,返回响应体。
|
|
609
|
+
*
|
|
610
|
+
* @param config axios 请求配置,`url` 相对 baseURL。
|
|
611
|
+
* @returns 响应体。
|
|
612
|
+
*/
|
|
613
|
+
request(config) {
|
|
614
|
+
return super.request(config);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 任意 method 的请求,返回完整 axios 响应。
|
|
618
|
+
*
|
|
619
|
+
* 需要读取响应头或状态码时使用。
|
|
620
|
+
*
|
|
621
|
+
* @param config axios 请求配置,`url` 相对 baseURL。
|
|
622
|
+
* @returns 完整 axios 响应。
|
|
623
|
+
*/
|
|
624
|
+
requestRaw(config) {
|
|
625
|
+
return super.requestRaw(config);
|
|
626
|
+
}
|
|
447
627
|
};
|
|
448
628
|
|
|
449
629
|
//#endregion
|
|
@@ -492,7 +672,7 @@ var MessagesApi = class extends Http {
|
|
|
492
672
|
/** 文字子频道消息 */
|
|
493
673
|
sendChannelMsg(channelId, body) {
|
|
494
674
|
const headers = body instanceof FormData ? body.getHeaders() : void 0;
|
|
495
|
-
return this.post(`/channels/${channelId}/messages`, body, headers);
|
|
675
|
+
return this.post(`/channels/${channelId}/messages`, body, { headers });
|
|
496
676
|
}
|
|
497
677
|
async sendDmsMsg(a, b, c) {
|
|
498
678
|
if (typeof b === "string") {
|
|
@@ -501,10 +681,10 @@ var MessagesApi = class extends Http {
|
|
|
501
681
|
source_guild_id: b
|
|
502
682
|
});
|
|
503
683
|
const headers = c instanceof FormData ? c.getHeaders() : void 0;
|
|
504
|
-
return this.post(`/dms/${dms.guild_id}/messages`, c, headers);
|
|
684
|
+
return this.post(`/dms/${dms.guild_id}/messages`, c, { headers });
|
|
505
685
|
}
|
|
506
686
|
const headers = b instanceof FormData ? b.getHeaders() : void 0;
|
|
507
|
-
return this.post(`/dms/${a}/messages`, b, headers);
|
|
687
|
+
return this.post(`/dms/${a}/messages`, b, { headers });
|
|
508
688
|
}
|
|
509
689
|
async recall(type, peer, messageId, hidetip = false) {
|
|
510
690
|
const path = type === "user" || type === "group" ? `/v2/${type}s/${peer}/messages/${messageId}` : `/${type}/${peer}/messages/${messageId}?hidetip=${hidetip}`;
|
|
@@ -531,7 +711,7 @@ const MIN_EFFECTIVE_TTL_SECONDS = 10;
|
|
|
531
711
|
* @param contentHash 文件内容 hash。
|
|
532
712
|
* @returns 缓存键。
|
|
533
713
|
*/
|
|
534
|
-
const cacheKey = (scene, peer, type, contentHash) => [
|
|
714
|
+
const cacheKey$1 = (scene, peer, type, contentHash) => [
|
|
535
715
|
scene,
|
|
536
716
|
peer,
|
|
537
717
|
type,
|
|
@@ -551,7 +731,7 @@ const cacheKey = (scene, peer, type, contentHash) => [
|
|
|
551
731
|
*/
|
|
552
732
|
const getCachedUpload = async (scene, peer, type, contentHash) => {
|
|
553
733
|
try {
|
|
554
|
-
const raw = await getMessageStore().getUploadCache(cacheKey(scene, peer, type, contentHash));
|
|
734
|
+
const raw = await getMessageStore().getUploadCache(cacheKey$1(scene, peer, type, contentHash));
|
|
555
735
|
if (!raw) return null;
|
|
556
736
|
return JSON.parse(raw);
|
|
557
737
|
} catch {
|
|
@@ -572,7 +752,7 @@ const setCachedUpload = async (scene, peer, type, contentHash, response) => {
|
|
|
572
752
|
const ttl = Number(response.ttl);
|
|
573
753
|
const expiresAt = ttl > 0 ? Date.now() + Math.max(ttl - TTL_SAFETY_MARGIN_SECONDS, MIN_EFFECTIVE_TTL_SECONDS) * 1e3 : 0;
|
|
574
754
|
try {
|
|
575
|
-
await getMessageStore().setUploadCache(cacheKey(scene, peer, type, contentHash), JSON.stringify(response), expiresAt);
|
|
755
|
+
await getMessageStore().setUploadCache(cacheKey$1(scene, peer, type, contentHash), JSON.stringify(response), expiresAt);
|
|
576
756
|
} catch {}
|
|
577
757
|
};
|
|
578
758
|
|
|
@@ -960,7 +1140,7 @@ var MediaApi = class extends Http {
|
|
|
960
1140
|
if (type === "file" && fileName) body.file_name = fileName;
|
|
961
1141
|
const path = `/v2/${scene}s/${peer}/files`;
|
|
962
1142
|
try {
|
|
963
|
-
return await this.post(path, body,
|
|
1143
|
+
return await this.post(path, body, { timeout: MEDIA_UPLOAD_TIMEOUT });
|
|
964
1144
|
} catch (err) {
|
|
965
1145
|
if (!sourceBody.url) throw err;
|
|
966
1146
|
const fallbackBody = {
|
|
@@ -968,7 +1148,7 @@ var MediaApi = class extends Http {
|
|
|
968
1148
|
file_data: await downloadRemoteAsBase64(sourceBody.url)
|
|
969
1149
|
};
|
|
970
1150
|
delete fallbackBody.url;
|
|
971
|
-
return this.post(path, fallbackBody,
|
|
1151
|
+
return this.post(path, fallbackBody, { timeout: MEDIA_UPLOAD_TIMEOUT });
|
|
972
1152
|
}
|
|
973
1153
|
}
|
|
974
1154
|
/**
|
|
@@ -1004,7 +1184,7 @@ var MediaApi = class extends Http {
|
|
|
1004
1184
|
file_data: await readFallbackSourceAsBase64(fallbackSource)
|
|
1005
1185
|
};
|
|
1006
1186
|
if (type === "file") body.file_name = fallbackSource.fileName;
|
|
1007
|
-
const response = await this.post(`/v2/${scene}s/${peer}/files`, body,
|
|
1187
|
+
const response = await this.post(`/v2/${scene}s/${peer}/files`, body, { timeout: MEDIA_UPLOAD_TIMEOUT });
|
|
1008
1188
|
if (hashes) await setCachedUpload(scene, peer, type, hashes.md5, response);
|
|
1009
1189
|
return response;
|
|
1010
1190
|
}
|
|
@@ -1022,7 +1202,7 @@ var MediaApi = class extends Http {
|
|
|
1022
1202
|
async finishUploadPart(scene, peer, body, retryTimeout) {
|
|
1023
1203
|
let lastError;
|
|
1024
1204
|
for (let attempt = 0; attempt <= PART_FINISH_RETRIES; attempt++) try {
|
|
1025
|
-
await this.post(`/v2/${scene}s/${peer}/upload_part_finish`, body,
|
|
1205
|
+
await this.post(`/v2/${scene}s/${peer}/upload_part_finish`, body, { timeout: MEDIA_UPLOAD_TIMEOUT });
|
|
1026
1206
|
return;
|
|
1027
1207
|
} catch (err) {
|
|
1028
1208
|
if (shouldRetryPartFinishPersistently(err)) {
|
|
@@ -1048,7 +1228,7 @@ var MediaApi = class extends Http {
|
|
|
1048
1228
|
let attempts = 0;
|
|
1049
1229
|
let lastError;
|
|
1050
1230
|
while (Date.now() < deadline) try {
|
|
1051
|
-
await this.post(`/v2/${scene}s/${peer}/upload_part_finish`, body,
|
|
1231
|
+
await this.post(`/v2/${scene}s/${peer}/upload_part_finish`, body, { timeout: MEDIA_UPLOAD_TIMEOUT });
|
|
1052
1232
|
return;
|
|
1053
1233
|
} catch (err) {
|
|
1054
1234
|
lastError = err;
|
|
@@ -1070,7 +1250,7 @@ var MediaApi = class extends Http {
|
|
|
1070
1250
|
let lastError;
|
|
1071
1251
|
const body = { upload_id: uploadId };
|
|
1072
1252
|
for (let attempt = 0; attempt <= COMPLETE_UPLOAD_RETRIES; attempt++) try {
|
|
1073
|
-
return await this.post(`/v2/${scene}s/${peer}/files`, body,
|
|
1253
|
+
return await this.post(`/v2/${scene}s/${peer}/files`, body, { timeout: MEDIA_UPLOAD_TIMEOUT });
|
|
1074
1254
|
} catch (err) {
|
|
1075
1255
|
lastError = err;
|
|
1076
1256
|
if (attempt < COMPLETE_UPLOAD_RETRIES) await sleep(COMPLETE_UPLOAD_BASE_DELAY * 2 ** attempt);
|
|
@@ -1099,7 +1279,7 @@ var MediaApi = class extends Http {
|
|
|
1099
1279
|
sha1: hashes.sha1
|
|
1100
1280
|
};
|
|
1101
1281
|
prepareBody["md5_10m"] = hashes.md510m;
|
|
1102
|
-
const prepare = await this.post(`/v2/${scene}s/${peer}/upload_prepare`, prepareBody,
|
|
1282
|
+
const prepare = await this.post(`/v2/${scene}s/${peer}/upload_prepare`, prepareBody, { timeout: MEDIA_UPLOAD_TIMEOUT }).catch((err) => {
|
|
1103
1283
|
if (openApiCode(err) === UPLOAD_PREPARE_DAILY_LIMIT_CODE) throw new Error(`QQ 大文件上传额度已达上限,${MEDIA_TYPE_LABEL[type]}暂时无法通过 fallback 直传:${source.fileName} (${formatBytes(source.size)})`);
|
|
1104
1284
|
return throwError(err);
|
|
1105
1285
|
});
|
|
@@ -1141,14 +1321,14 @@ var InteractionApi = class extends Http {
|
|
|
1141
1321
|
* @param code 0 成功 / 1 操作失败 / 2 操作频繁 / 3 重复操作 / 4 无权限 / 5 仅管理员
|
|
1142
1322
|
*/
|
|
1143
1323
|
async ack(interactionId, code = 0) {
|
|
1144
|
-
await this.put(`/interactions/${interactionId}`, { code },
|
|
1324
|
+
await this.put(`/interactions/${interactionId}`, { code }, { timeout: INTERACTION_ACK_TIMEOUT });
|
|
1145
1325
|
}
|
|
1146
1326
|
};
|
|
1147
1327
|
|
|
1148
1328
|
//#endregion
|
|
1149
1329
|
//#region src/core/api/meta.ts
|
|
1150
1330
|
/**
|
|
1151
|
-
* 元信息接口:@me / dms / gateway
|
|
1331
|
+
* 元信息接口:@me / dms / gateway / 分享链接
|
|
1152
1332
|
*/
|
|
1153
1333
|
var MetaApi = class extends Http {
|
|
1154
1334
|
/** 当前机器人详情 */
|
|
@@ -1166,6 +1346,17 @@ var MetaApi = class extends Http {
|
|
|
1166
1346
|
getGateway() {
|
|
1167
1347
|
return this.get("/gateway");
|
|
1168
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* 生成机器人分享链接
|
|
1351
|
+
*
|
|
1352
|
+
* 用于邀请用户添加机器人为好友;`callback_data`(最长 32 字符)会在用户通过该
|
|
1353
|
+
* 链接添加机器人时透传到 `FRIEND_ADD` 事件的 `scene_param`。
|
|
1354
|
+
*
|
|
1355
|
+
* @param body 分享链接参数,均为可选
|
|
1356
|
+
*/
|
|
1357
|
+
generateUrlLink(body = {}) {
|
|
1358
|
+
return this.post("/v2/generate_url_link", body);
|
|
1359
|
+
}
|
|
1169
1360
|
};
|
|
1170
1361
|
|
|
1171
1362
|
//#endregion
|
|
@@ -1202,18 +1393,329 @@ var GroupsApi = class extends Http {
|
|
|
1202
1393
|
}
|
|
1203
1394
|
/**
|
|
1204
1395
|
* 设置群成员禁言
|
|
1396
|
+
*
|
|
1397
|
+
* 机器人需拥有群管理员身份,平台限制最大禁言时长为 30 天。
|
|
1398
|
+
*
|
|
1205
1399
|
* @param groupOpenid 群 openid
|
|
1206
1400
|
* @param memberOpenid 群成员 openid
|
|
1207
1401
|
* @param op 操作: add 增加禁言,update 更新禁言到期时间,del 解除禁言
|
|
1208
1402
|
* @param duration 禁言时间使用RFC3339 格式
|
|
1209
1403
|
*/
|
|
1210
|
-
|
|
1404
|
+
setGroupMute(groupOpenid, memberOpenid, op, duration) {
|
|
1211
1405
|
return this.post(`/v2/groups/${groupOpenid}/restrict_chat_setting`, { members: [{
|
|
1212
1406
|
op,
|
|
1213
1407
|
member_openid: memberOpenid,
|
|
1214
1408
|
mute_expire_at: duration
|
|
1215
1409
|
}] });
|
|
1216
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* 设置群成员禁言
|
|
1413
|
+
* @deprecated 方法名拼写错误,请使用 {@link GroupsApi.setGroupMute}
|
|
1414
|
+
*/
|
|
1415
|
+
setGroupMetu(groupOpenid, memberOpenid, op, duration) {
|
|
1416
|
+
return this.setGroupMute(groupOpenid, memberOpenid, op, duration);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* 查询群禁言状态
|
|
1420
|
+
*
|
|
1421
|
+
* 返回全员禁言模式(含定时 / 周期规则)与当前禁言中的成员列表(不含已过期)。
|
|
1422
|
+
* 机器人需拥有群管理员身份。
|
|
1423
|
+
*
|
|
1424
|
+
* @param groupOpenid 群 openid
|
|
1425
|
+
*/
|
|
1426
|
+
getGroupMuteSetting(groupOpenid) {
|
|
1427
|
+
return this.get(`/v2/groups/${groupOpenid}/restrict_chat_setting`);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* 拉取入群申请列表
|
|
1431
|
+
*
|
|
1432
|
+
* 机器人需拥有群管理员身份。
|
|
1433
|
+
*
|
|
1434
|
+
* @param groupOpenid 群 openid
|
|
1435
|
+
* @param params 分页参数,`limit` 默认 20、最大 100
|
|
1436
|
+
*/
|
|
1437
|
+
getJoinRequestList(groupOpenid, params = {}) {
|
|
1438
|
+
const query = new URLSearchParams();
|
|
1439
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
1440
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
1441
|
+
const search = query.toString();
|
|
1442
|
+
return this.get(`/v2/groups/${groupOpenid}/join_request_list${search ? `?${search}` : ""}`);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* 审批入群申请
|
|
1446
|
+
*
|
|
1447
|
+
* 机器人需拥有群管理员身份。
|
|
1448
|
+
*
|
|
1449
|
+
* @param groupOpenid 群 openid
|
|
1450
|
+
* @param memberOpenid 申请人 openid
|
|
1451
|
+
* @param body 审批动作,`op=decline` 时可附带拒绝理由与拉黑标记
|
|
1452
|
+
*/
|
|
1453
|
+
approvalJoinRequest(groupOpenid, memberOpenid, body) {
|
|
1454
|
+
return this.post(`/v2/groups/${groupOpenid}/approval_join_request/${memberOpenid}`, body);
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
//#endregion
|
|
1459
|
+
//#region src/core/api/guilds.ts
|
|
1460
|
+
/**
|
|
1461
|
+
* 频道 / 子频道接口
|
|
1462
|
+
*
|
|
1463
|
+
* 子频道的创建、修改、删除均需要管理员权限且属于私域接口,
|
|
1464
|
+
* 操作成功后会触发 `CHANNEL_CREATE` / `CHANNEL_UPDATE` / `CHANNEL_DELETE` 事件。
|
|
1465
|
+
*/
|
|
1466
|
+
var GuildsApi = class extends Http {
|
|
1467
|
+
/**
|
|
1468
|
+
* 获取机器人加入的频道列表
|
|
1469
|
+
*
|
|
1470
|
+
* 官方文档响应体标注为 `{ guilds: [] }`,但示例与实际返回均为裸数组;
|
|
1471
|
+
* 这里对两种形态都做兼容。
|
|
1472
|
+
*
|
|
1473
|
+
* @param params 分页参数,`limit` 默认 100、最大 100
|
|
1474
|
+
*/
|
|
1475
|
+
async getGuildList(params = {}) {
|
|
1476
|
+
const query = new URLSearchParams();
|
|
1477
|
+
if (params.before) query.set("before", params.before);
|
|
1478
|
+
if (params.after) query.set("after", params.after);
|
|
1479
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
1480
|
+
const search = query.toString();
|
|
1481
|
+
const data = await this.get(`/users/@me/guilds${search ? `?${search}` : ""}`);
|
|
1482
|
+
if (Array.isArray(data)) return data;
|
|
1483
|
+
return data?.guilds ?? [];
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* 获取频道详情
|
|
1487
|
+
* @param guildId 频道 ID
|
|
1488
|
+
*/
|
|
1489
|
+
getGuild(guildId) {
|
|
1490
|
+
return this.get(`/guilds/${guildId}`);
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* 获取子频道列表
|
|
1494
|
+
*
|
|
1495
|
+
* 官方文档响应体标注为 `{ channels: [] }`,但示例与实际返回均为裸数组;
|
|
1496
|
+
* 这里对两种形态都做兼容。
|
|
1497
|
+
*
|
|
1498
|
+
* @param guildId 频道 ID
|
|
1499
|
+
*/
|
|
1500
|
+
async getChannelList(guildId) {
|
|
1501
|
+
const data = await this.get(`/guilds/${guildId}/channels`);
|
|
1502
|
+
if (Array.isArray(data)) return data;
|
|
1503
|
+
return data?.channels ?? [];
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* 创建子频道(需要管理员权限,私域接口)
|
|
1507
|
+
* @param guildId 频道 ID
|
|
1508
|
+
* @param body 子频道配置
|
|
1509
|
+
*/
|
|
1510
|
+
createChannel(guildId, body) {
|
|
1511
|
+
return this.post(`/guilds/${guildId}/channels`, body);
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* 获取子频道详情
|
|
1515
|
+
* @param channelId 子频道 ID
|
|
1516
|
+
*/
|
|
1517
|
+
getChannel(channelId) {
|
|
1518
|
+
return this.get(`/channels/${channelId}`);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* 修改子频道(需要管理员权限,私域接口)
|
|
1522
|
+
*
|
|
1523
|
+
* 只需传入要修改的字段。
|
|
1524
|
+
* @param channelId 子频道 ID
|
|
1525
|
+
* @param body 待修改的字段
|
|
1526
|
+
*/
|
|
1527
|
+
updateChannel(channelId, body) {
|
|
1528
|
+
return this.patch(`/channels/${channelId}`, body);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* 删除子频道(需要管理员权限,私域接口)
|
|
1532
|
+
*
|
|
1533
|
+
* 子频道删除后无法恢复。
|
|
1534
|
+
* @param channelId 子频道 ID
|
|
1535
|
+
*/
|
|
1536
|
+
deleteChannel(channelId) {
|
|
1537
|
+
return this.delete(`/channels/${channelId}`);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
//#endregion
|
|
1542
|
+
//#region src/core/api/menu.ts
|
|
1543
|
+
/**
|
|
1544
|
+
* 全局自定义菜单接口
|
|
1545
|
+
*
|
|
1546
|
+
* 自定义菜单展示在机器人单聊窗口底部,仅支持 C2C(单聊)场景,
|
|
1547
|
+
* 设置后对所有用户生效,不支持按用户维度区分。
|
|
1548
|
+
*
|
|
1549
|
+
* {@link https://bot.q.qq.com/wiki/develop/api-v2/server-inter/menu-panel/}
|
|
1550
|
+
*/
|
|
1551
|
+
var MenuApi = class extends Http {
|
|
1552
|
+
/**
|
|
1553
|
+
* 查询全局自定义菜单
|
|
1554
|
+
*
|
|
1555
|
+
* 未设置过菜单时响应的 `menu` 字段为空。
|
|
1556
|
+
* @returns 当前生效的菜单配置与版本号
|
|
1557
|
+
*/
|
|
1558
|
+
getMenu() {
|
|
1559
|
+
return this.get("/v2/menu");
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* 修改全局自定义菜单
|
|
1563
|
+
*
|
|
1564
|
+
* 传入后会覆盖原有的完整菜单配置;不传 `menu` 表示清空菜单。
|
|
1565
|
+
* @param menu 菜单配置,最多 10 个一级菜单项
|
|
1566
|
+
* @returns 本次修改后的菜单版本号
|
|
1567
|
+
*/
|
|
1568
|
+
updateMenu(menu) {
|
|
1569
|
+
return this.put("/v2/menu", menu ? { menu } : {});
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
//#endregion
|
|
1574
|
+
//#region src/core/api/panels.ts
|
|
1575
|
+
/**
|
|
1576
|
+
* 指令面板接口
|
|
1577
|
+
*
|
|
1578
|
+
* 指令面板以面板形式展示指令或链接,支持 c2c(单聊)、group(群聊)、
|
|
1579
|
+
* channel(文字子频道)、dm(频道私信)四种场景;其中 c2c / group 支持
|
|
1580
|
+
* 按指定用户或群生效(target_type=specific),channel / dm 仅支持全局配置。
|
|
1581
|
+
*
|
|
1582
|
+
* 一个机器人最多创建 20 个指令面板。
|
|
1583
|
+
*
|
|
1584
|
+
* {@link https://bot.q.qq.com/wiki/develop/api-v2/server-inter/menu-panel/}
|
|
1585
|
+
*/
|
|
1586
|
+
var PanelsApi = class extends Http {
|
|
1587
|
+
/**
|
|
1588
|
+
* 查询指令面板列表
|
|
1589
|
+
*
|
|
1590
|
+
* 分页拉取指定场景下已生效的面板,按设置时间倒序排列。
|
|
1591
|
+
* @param params 查询参数,`scope` 必填
|
|
1592
|
+
* @returns 面板记录列表与分页游标
|
|
1593
|
+
*/
|
|
1594
|
+
getPanelList(params) {
|
|
1595
|
+
const query = new URLSearchParams({ scope: params.scope });
|
|
1596
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
1597
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
1598
|
+
return this.get(`/v2/panels?${query.toString()}`);
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* 创建指令面板
|
|
1602
|
+
* @param body 面板场景、作用范围与配置内容
|
|
1603
|
+
* @returns 新创建的面板 ID
|
|
1604
|
+
*/
|
|
1605
|
+
createPanel(body) {
|
|
1606
|
+
return this.post("/v2/panels", body);
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* 查询指令面板详情
|
|
1610
|
+
*
|
|
1611
|
+
* 返回面板完整配置,target_type=specific 时同时返回关联的用户 / 群 openid 列表。
|
|
1612
|
+
* @param panelId 面板 ID
|
|
1613
|
+
*/
|
|
1614
|
+
getPanel(panelId) {
|
|
1615
|
+
return this.get(`/v2/panels/${panelId}`);
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* 修改指令面板
|
|
1619
|
+
*
|
|
1620
|
+
* 传入后会覆盖原有的面板元素列表和备注,不影响已关联的用户 / 群列表。
|
|
1621
|
+
* @param panelId 面板 ID
|
|
1622
|
+
* @param panel 面板配置内容
|
|
1623
|
+
* @returns 本次修改后的面板版本号
|
|
1624
|
+
*/
|
|
1625
|
+
updatePanel(panelId, panel) {
|
|
1626
|
+
return this.put(`/v2/panels/${panelId}`, { panel });
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* 删除指令面板
|
|
1630
|
+
*
|
|
1631
|
+
* 删除后该面板不再对任何用户或群生效。
|
|
1632
|
+
* @param panelId 面板 ID
|
|
1633
|
+
*/
|
|
1634
|
+
deletePanel(panelId) {
|
|
1635
|
+
return this.delete(`/v2/panels/${panelId}`);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* 修改指令面板关联对象
|
|
1639
|
+
*
|
|
1640
|
+
* c2c 场景操作 `user_openids`,group 场景操作 `group_openids`,一次最多 20 个;
|
|
1641
|
+
* channel / dm 场景为全局配置,不支持此操作。
|
|
1642
|
+
* @param panelId 面板 ID
|
|
1643
|
+
* @param body 操作类型与关联对象列表
|
|
1644
|
+
*/
|
|
1645
|
+
updatePanelTarget(panelId, body) {
|
|
1646
|
+
return this.put(`/v2/panels/${panelId}/target`, body);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/core/api/join-approval.ts
|
|
1652
|
+
/**
|
|
1653
|
+
* 入群自动审批策略接口
|
|
1654
|
+
*
|
|
1655
|
+
* 策略把「关联群 + 白名单 QQ 号码」绑在一起,命中白名单的入群申请会自动审批通过。
|
|
1656
|
+
* 规则仅在机器人拥有群管理员身份时才会实际运行;一个机器人最多 20 个策略。
|
|
1657
|
+
*/
|
|
1658
|
+
var JoinApprovalApi = class extends Http {
|
|
1659
|
+
/**
|
|
1660
|
+
* 查询入群自动审批策略列表
|
|
1661
|
+
*
|
|
1662
|
+
* 按创建时间倒序,支持分页。
|
|
1663
|
+
* @param params 分页参数,`limit` 默认 20、最大 100
|
|
1664
|
+
*/
|
|
1665
|
+
getStrategyList(params = {}) {
|
|
1666
|
+
const query = new URLSearchParams();
|
|
1667
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
1668
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
1669
|
+
const search = query.toString();
|
|
1670
|
+
return this.get(`/v2/groups/join_approval_strategy${search ? `?${search}` : ""}`);
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* 创建入群自动审批策略
|
|
1674
|
+
*
|
|
1675
|
+
* `group_openids` 与 `group_ids` 二选一必填,同时传入或均未传入都会报错。
|
|
1676
|
+
* @param body 策略配置
|
|
1677
|
+
*/
|
|
1678
|
+
createStrategy(body) {
|
|
1679
|
+
return this.post("/v2/groups/join_approval_strategy", body);
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* 修改入群自动审批策略
|
|
1683
|
+
*
|
|
1684
|
+
* 可修改生效状态、失效时间、备注,或增删关联群;
|
|
1685
|
+
* 增删关联群时的群标识形式必须与创建时一致。
|
|
1686
|
+
* @param strategyId 策略 ID
|
|
1687
|
+
* @param body 待修改的字段
|
|
1688
|
+
*/
|
|
1689
|
+
updateStrategy(strategyId, body) {
|
|
1690
|
+
return this.patch(`/v2/groups/join_approval_strategy/${strategyId}`, body);
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* 删除入群自动审批策略
|
|
1694
|
+
* @param strategyId 策略 ID
|
|
1695
|
+
*/
|
|
1696
|
+
deleteStrategy(strategyId) {
|
|
1697
|
+
return this.delete(`/v2/groups/join_approval_strategy/${strategyId}`);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* 执行入群自动审批策略
|
|
1701
|
+
*
|
|
1702
|
+
* 对策略关联的全部群发起全量扫描,命中白名单号码的入群申请自动审批通过。
|
|
1703
|
+
* 异步执行,约 10 分钟完成。
|
|
1704
|
+
* @param strategyId 策略 ID
|
|
1705
|
+
*/
|
|
1706
|
+
executeStrategy(strategyId) {
|
|
1707
|
+
return this.post(`/v2/groups/join_approval_strategy/${strategyId}/execute`);
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* 修改入群自动审批策略的白名单号码
|
|
1711
|
+
*
|
|
1712
|
+
* 单次最多 10000 个,策略号码上限 10W。
|
|
1713
|
+
* @param strategyId 策略 ID
|
|
1714
|
+
* @param body 操作类型与号码列表
|
|
1715
|
+
*/
|
|
1716
|
+
updateWhitelistUsers(strategyId, body) {
|
|
1717
|
+
return this.post(`/v2/groups/join_approval_strategy/${strategyId}/whitelist_users`, body);
|
|
1718
|
+
}
|
|
1217
1719
|
};
|
|
1218
1720
|
|
|
1219
1721
|
//#endregion
|
|
@@ -1296,28 +1798,56 @@ const buildGuildMsg = {
|
|
|
1296
1798
|
* - messages:发送 / 撤回
|
|
1297
1799
|
* - media:富媒体上传
|
|
1298
1800
|
* - interaction:按钮回调 ack
|
|
1299
|
-
* - meta:@me / dms / gateway
|
|
1300
|
-
* - groups:群基础信息 /
|
|
1801
|
+
* - meta:@me / dms / gateway / 分享链接
|
|
1802
|
+
* - groups:群基础信息 / 机器人群内状态 / 禁言 / 入群申请(部分为白名单接口)
|
|
1803
|
+
* - guilds:频道与子频道的查询和增删改
|
|
1804
|
+
* - menu:全局自定义菜单(仅单聊)
|
|
1805
|
+
* - panels:指令面板
|
|
1806
|
+
* - joinApproval:入群自动审批策略
|
|
1807
|
+
* - request:裸请求入口,用于调用尚未封装的官方接口
|
|
1301
1808
|
*
|
|
1302
1809
|
* 请求体构造器 {@link buildQQMsg} / {@link buildGuildMsg} 独立导出,避免与 HTTP 调用耦合
|
|
1303
1810
|
*/
|
|
1304
1811
|
var QQBotApi = class {
|
|
1305
1812
|
axios;
|
|
1813
|
+
/**
|
|
1814
|
+
* 裸请求入口,直接调用官方 OpenAPI。
|
|
1815
|
+
*
|
|
1816
|
+
* 官方新接口还没被适配器封装时可用它兜底,会自动带鉴权、解包 `data`、
|
|
1817
|
+
* 格式化错误码:
|
|
1818
|
+
*
|
|
1819
|
+
* ```ts
|
|
1820
|
+
* await bot.super.request.get('/v2/menu')
|
|
1821
|
+
* await bot.super.request.post('/v2/xxx', { foo: 1 })
|
|
1822
|
+
* ```
|
|
1823
|
+
*
|
|
1824
|
+
* @see {@link RequestApi}
|
|
1825
|
+
*/
|
|
1826
|
+
request;
|
|
1306
1827
|
messages;
|
|
1307
1828
|
media;
|
|
1308
1829
|
interaction;
|
|
1309
1830
|
meta;
|
|
1310
1831
|
groups;
|
|
1832
|
+
guilds;
|
|
1833
|
+
menu;
|
|
1834
|
+
panels;
|
|
1835
|
+
joinApproval;
|
|
1311
1836
|
/** 请求体构造器(静态便利) */
|
|
1312
1837
|
qq = buildQQMsg;
|
|
1313
1838
|
guild = buildGuildMsg;
|
|
1314
1839
|
constructor(axios) {
|
|
1315
1840
|
this.axios = axios;
|
|
1841
|
+
this.request = new RequestApi(axios);
|
|
1316
1842
|
this.messages = new MessagesApi(axios);
|
|
1317
1843
|
this.media = new MediaApi(axios);
|
|
1318
1844
|
this.interaction = new InteractionApi(axios);
|
|
1319
1845
|
this.meta = new MetaApi(axios);
|
|
1320
1846
|
this.groups = new GroupsApi(axios);
|
|
1847
|
+
this.guilds = new GuildsApi(axios);
|
|
1848
|
+
this.menu = new MenuApi(axios);
|
|
1849
|
+
this.panels = new PanelsApi(axios);
|
|
1850
|
+
this.joinApproval = new JoinApprovalApi(axios);
|
|
1321
1851
|
}
|
|
1322
1852
|
};
|
|
1323
1853
|
|
|
@@ -2414,11 +2944,79 @@ const responseTime = (timestamp) => {
|
|
|
2414
2944
|
return Date.now();
|
|
2415
2945
|
};
|
|
2416
2946
|
|
|
2947
|
+
//#endregion
|
|
2948
|
+
//#region src/core/adapter/join-request-cache.ts
|
|
2949
|
+
/** 入群申请索引缓存上限,仅用于把 Karin 的 flag 还原成群 / 成员 openid */
|
|
2950
|
+
const JOIN_REQUEST_CACHE_MAX = 1e3;
|
|
2951
|
+
/**
|
|
2952
|
+
* `appId:join_request_id` -> 群 / 申请人 openid。
|
|
2953
|
+
*
|
|
2954
|
+
* Karin 的 `setGroupApplyResult` / `setInvitedJoinGroupResult` 只透传 flag
|
|
2955
|
+
* (即 `join_request_id`),而官方审批接口的路径需要 group_openid 与
|
|
2956
|
+
* member_openid,因此在收到入群申请事件时先把对应关系记下来。
|
|
2957
|
+
*
|
|
2958
|
+
* 按 appId 加前缀,避免多 bot 共用进程时互相覆盖。
|
|
2959
|
+
*/
|
|
2960
|
+
const joinRequests = /* @__PURE__ */ new Map();
|
|
2961
|
+
/**
|
|
2962
|
+
* 拼接缓存键。
|
|
2963
|
+
* @param appId 机器人 appId。
|
|
2964
|
+
* @param joinRequestId 申请 ID。
|
|
2965
|
+
*/
|
|
2966
|
+
const cacheKey = (appId, joinRequestId) => `${appId}:${joinRequestId}`;
|
|
2967
|
+
/**
|
|
2968
|
+
* 记录入群申请索引,供后续审批接口还原群 / 申请人 openid。
|
|
2969
|
+
*
|
|
2970
|
+
* 超出上限时按写入顺序淘汰最旧的一条。
|
|
2971
|
+
*
|
|
2972
|
+
* @param ctx 当前 QQBot 适配器实例。
|
|
2973
|
+
* @param joinRequestId 申请 ID,同时是 Karin 请求事件的 flag。
|
|
2974
|
+
* @param target 该申请对应的群与申请人 openid。
|
|
2975
|
+
*/
|
|
2976
|
+
const cacheJoinRequest = (ctx, joinRequestId, target) => {
|
|
2977
|
+
if (!joinRequestId) return;
|
|
2978
|
+
if (joinRequests.size >= JOIN_REQUEST_CACHE_MAX) {
|
|
2979
|
+
const oldest = joinRequests.keys().next().value;
|
|
2980
|
+
if (oldest !== void 0) joinRequests.delete(oldest);
|
|
2981
|
+
}
|
|
2982
|
+
joinRequests.set(cacheKey(String(ctx.cfg.appId), joinRequestId), target);
|
|
2983
|
+
};
|
|
2984
|
+
/**
|
|
2985
|
+
* 读取入群申请索引。
|
|
2986
|
+
*
|
|
2987
|
+
* @param ctx 当前 QQBot 适配器实例。
|
|
2988
|
+
* @param joinRequestId 申请 ID。
|
|
2989
|
+
* @returns 命中的群 / 申请人 openid;未命中返回 undefined。
|
|
2990
|
+
*/
|
|
2991
|
+
const getJoinRequest = (ctx, joinRequestId) => {
|
|
2992
|
+
return joinRequests.get(cacheKey(String(ctx.cfg.appId), joinRequestId));
|
|
2993
|
+
};
|
|
2994
|
+
/**
|
|
2995
|
+
* 审批完成后移除索引。
|
|
2996
|
+
*
|
|
2997
|
+
* @param ctx 当前 QQBot 适配器实例。
|
|
2998
|
+
* @param joinRequestId 申请 ID。
|
|
2999
|
+
*/
|
|
3000
|
+
const removeJoinRequest = (ctx, joinRequestId) => {
|
|
3001
|
+
joinRequests.delete(cacheKey(String(ctx.cfg.appId), joinRequestId));
|
|
3002
|
+
};
|
|
3003
|
+
/**
|
|
3004
|
+
* 清理某个 bot 的全部入群申请索引,销毁 bot 时调用。
|
|
3005
|
+
*
|
|
3006
|
+
* @param appId 机器人 appId。
|
|
3007
|
+
*/
|
|
3008
|
+
const clearJoinRequests = (appId) => {
|
|
3009
|
+
const prefix = `${appId}:`;
|
|
3010
|
+
for (const key of joinRequests.keys()) if (key.startsWith(prefix)) joinRequests.delete(key);
|
|
3011
|
+
};
|
|
3012
|
+
|
|
2417
3013
|
//#endregion
|
|
2418
3014
|
//#region src/core/adapter/base.ts
|
|
2419
3015
|
const adapterConfigStore = /* @__PURE__ */ new WeakMap();
|
|
2420
3016
|
/** HTTP(S) URL 可直接交给 QQ 平台拉取上传 */
|
|
2421
3017
|
const HTTP_URL_RE = /^https?:\/\//i;
|
|
3018
|
+
/** 平台限制的最大禁言时长:30 天 */
|
|
3019
|
+
const MAX_GROUP_MUTE_SECONDS = 720 * 60 * 60;
|
|
2422
3020
|
/**
|
|
2423
3021
|
* QQ Official Bot 适配器
|
|
2424
3022
|
*
|
|
@@ -2729,20 +3327,31 @@ var AdapterQQBot = class extends AdapterBase {
|
|
|
2729
3327
|
}
|
|
2730
3328
|
/**
|
|
2731
3329
|
* 禁言群成员
|
|
3330
|
+
*
|
|
3331
|
+
* 机器人需拥有群管理员身份;平台限制最大禁言时长为 30 天,
|
|
3332
|
+
* 超出部分会被截断到 30 天而不是直接失败。
|
|
3333
|
+
*
|
|
3334
|
+
* @param _groupId 群 openid
|
|
3335
|
+
* @param _targetId 群成员 openid
|
|
3336
|
+
* @param duration 禁言时长(秒),传 0 或负数表示解除禁言
|
|
2732
3337
|
*/
|
|
2733
3338
|
async setGroupMute(_groupId, _targetId, duration) {
|
|
2734
3339
|
let rfc3339 = "";
|
|
2735
3340
|
if (duration > 0) {
|
|
2736
|
-
|
|
2737
|
-
|
|
3341
|
+
let seconds = duration;
|
|
3342
|
+
if (seconds > MAX_GROUP_MUTE_SECONDS) {
|
|
3343
|
+
this.logger("warn", `[setGroupMute] 禁言时长 ${seconds}s 超出平台上限 30 天,已截断为 ${MAX_GROUP_MUTE_SECONDS}s`);
|
|
3344
|
+
seconds = MAX_GROUP_MUTE_SECONDS;
|
|
3345
|
+
}
|
|
3346
|
+
rfc3339 = new Date(Date.now() + seconds * 1e3).toISOString();
|
|
2738
3347
|
}
|
|
2739
|
-
this.super.groups.
|
|
3348
|
+
await this.super.groups.setGroupMute(_groupId, _targetId, rfc3339 ? "add" : "del", rfc3339);
|
|
2740
3349
|
}
|
|
2741
3350
|
/**
|
|
2742
3351
|
* 群全员禁言
|
|
2743
3352
|
*/
|
|
2744
3353
|
async setGroupAllMute(_groupId, _isBan) {
|
|
2745
|
-
this.logger("warn", "[setGroupAllMute] QQ Official Bot API
|
|
3354
|
+
this.logger("warn", "[setGroupAllMute] QQ Official Bot API 暂不支持设置群全员禁言,仅可通过 bot.super.groups.getGroupMuteSetting() 查询 global_rule");
|
|
2746
3355
|
}
|
|
2747
3356
|
/**
|
|
2748
3357
|
* 设置群管理员
|
|
@@ -2794,8 +3403,11 @@ var AdapterQQBot = class extends AdapterBase {
|
|
|
2794
3403
|
/**
|
|
2795
3404
|
* 获取群信息
|
|
2796
3405
|
*
|
|
2797
|
-
*
|
|
2798
|
-
*
|
|
3406
|
+
* QQ 群走官方 `GET /v2/groups/{group_openid}/info`(白名单接口);传入的是频道
|
|
3407
|
+
* ID 时退回 `GET /guilds/{guild_id}`,这样频道场景下的插件调用也能拿到名称和
|
|
3408
|
+
* 人数。两者都不可用(如白名单未开通返回 11253)时降级为空结构并告警。
|
|
3409
|
+
*
|
|
3410
|
+
* @param _groupId 群 openid 或频道 ID。
|
|
2799
3411
|
*/
|
|
2800
3412
|
async getGroupInfo(_groupId, _noCache) {
|
|
2801
3413
|
const empty = {
|
|
@@ -2819,15 +3431,28 @@ var AdapterQQBot = class extends AdapterBase {
|
|
|
2819
3431
|
avatar: await this.getGroupAvatarUrl(_groupId)
|
|
2820
3432
|
};
|
|
2821
3433
|
} catch (err) {
|
|
3434
|
+
const guild = await this.super.guilds.getGuild(_groupId).catch(() => null);
|
|
3435
|
+
if (guild) return {
|
|
3436
|
+
...empty,
|
|
3437
|
+
groupName: guild.name || "",
|
|
3438
|
+
owner: guild.owner_id || "",
|
|
3439
|
+
memberCount: guild.member_count ?? 0,
|
|
3440
|
+
maxMemberCount: guild.max_members ?? 0,
|
|
3441
|
+
groupDesc: guild.description || "",
|
|
3442
|
+
avatar: guild.icon || ""
|
|
3443
|
+
};
|
|
2822
3444
|
this.logger("warn", `[getGroupInfo] 获取群信息失败: ${err instanceof Error ? err.message : err}`);
|
|
2823
3445
|
return empty;
|
|
2824
3446
|
}
|
|
2825
3447
|
}
|
|
2826
3448
|
/**
|
|
2827
3449
|
* 获取群列表
|
|
3450
|
+
*
|
|
3451
|
+
* 官方没有 QQ 群列表接口。频道列表可用 `bot.super.guilds.getGuildList()` 获取,
|
|
3452
|
+
* 但频道在 Karin 里属于 guild 场景,与 QQ 群不是同一类会话,因此不在此处混用。
|
|
2828
3453
|
*/
|
|
2829
3454
|
async getGroupList(_refresh) {
|
|
2830
|
-
this.logger("warn", "[getGroupList] QQ Official Bot API
|
|
3455
|
+
this.logger("warn", "[getGroupList] QQ Official Bot API 暂不支持获取群列表,频道列表请用 bot.super.guilds.getGuildList()");
|
|
2831
3456
|
return [];
|
|
2832
3457
|
}
|
|
2833
3458
|
/**
|
|
@@ -2891,16 +3516,46 @@ var AdapterQQBot = class extends AdapterBase {
|
|
|
2891
3516
|
this.logger("warn", "[setFriendApplyResult] QQ Official Bot API 暂不支持设置好友请求结果");
|
|
2892
3517
|
}
|
|
2893
3518
|
/**
|
|
2894
|
-
*
|
|
3519
|
+
* 审批入群申请。
|
|
3520
|
+
*
|
|
3521
|
+
* 走官方 `POST /v2/groups/{group_openid}/approval_join_request/{member_openid}`,
|
|
3522
|
+
* 机器人需拥有群管理员身份。Karin 只透传 flag(`join_request_id`),群与申请人
|
|
3523
|
+
* openid 由 `join-request-cache` 在收到申请事件时记录,因此仅支持审批本次运行
|
|
3524
|
+
* 期间收到过的申请;历史申请请改用
|
|
3525
|
+
* `bot.super.groups.getJoinRequestList()` + `bot.super.groups.approvalJoinRequest()`。
|
|
3526
|
+
*
|
|
3527
|
+
* @param requestId 申请 ID(Karin 请求事件的 flag)。
|
|
3528
|
+
* @param isApprove 是否通过。
|
|
3529
|
+
* @param denyReason 拒绝理由,仅拒绝时生效。
|
|
2895
3530
|
*/
|
|
2896
|
-
async setGroupApplyResult(
|
|
2897
|
-
|
|
3531
|
+
async setGroupApplyResult(requestId, isApprove, denyReason) {
|
|
3532
|
+
const cached = getJoinRequest(this, requestId);
|
|
3533
|
+
if (!cached) {
|
|
3534
|
+
this.logger("warn", `[setGroupApplyResult] 未找到入群申请索引,无法还原群/申请人 openid: ${requestId || "(empty)"}`);
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
try {
|
|
3538
|
+
await this.super.groups.approvalJoinRequest(cached.groupId, cached.memberId, {
|
|
3539
|
+
op: isApprove ? "approve" : "decline",
|
|
3540
|
+
join_request_id: requestId,
|
|
3541
|
+
...isApprove ? {} : { reject_reason: denyReason }
|
|
3542
|
+
});
|
|
3543
|
+
removeJoinRequest(this, requestId);
|
|
3544
|
+
} catch (err) {
|
|
3545
|
+
this.logger("error", "[setGroupApplyResult] 审批入群申请失败:", err);
|
|
3546
|
+
}
|
|
2898
3547
|
}
|
|
2899
3548
|
/**
|
|
2900
|
-
*
|
|
3549
|
+
* 设置邀请加入群请求结果。
|
|
3550
|
+
*
|
|
3551
|
+
* 机器人被邀请入群与用户主动申请入群在官方侧是同一条 `GROUP_JOIN_REQUEST`
|
|
3552
|
+
* 事件,审批走同一个接口,因此直接复用 {@link AdapterQQBot.setGroupApplyResult}。
|
|
3553
|
+
*
|
|
3554
|
+
* @param requestId 申请 ID(Karin 请求事件的 flag)。
|
|
3555
|
+
* @param isApprove 是否通过。
|
|
2901
3556
|
*/
|
|
2902
|
-
async setInvitedJoinGroupResult(
|
|
2903
|
-
this.
|
|
3557
|
+
async setInvitedJoinGroupResult(requestId, isApprove) {
|
|
3558
|
+
await this.setGroupApplyResult(requestId, isApprove);
|
|
2904
3559
|
}
|
|
2905
3560
|
/**
|
|
2906
3561
|
* 设置消息表情回应
|
|
@@ -3043,10 +3698,30 @@ var AdapterQQBot = class extends AdapterBase {
|
|
|
3043
3698
|
}
|
|
3044
3699
|
/**
|
|
3045
3700
|
* 获取群被禁言用户列表
|
|
3701
|
+
*
|
|
3702
|
+
* 走官方 `GET /v2/groups/{group_openid}/restrict_chat_setting`,只返回仍在
|
|
3703
|
+
* 禁言中的成员(不含已过期)。机器人需拥有群管理员身份,无权限或查询失败时
|
|
3704
|
+
* 降级为空数组并告警。
|
|
3705
|
+
*
|
|
3706
|
+
* `muteTime` 为禁言到期时间的 Unix 秒,而非剩余时长;全员禁言规则不在该返回
|
|
3707
|
+
* 结构内,需要时请用 `bot.super.groups.getGroupMuteSetting()` 读取 `global_rule`。
|
|
3708
|
+
*
|
|
3709
|
+
* @param _groupId 群 openid
|
|
3046
3710
|
*/
|
|
3047
3711
|
async getGroupMuteList(_groupId) {
|
|
3048
|
-
|
|
3049
|
-
|
|
3712
|
+
try {
|
|
3713
|
+
return ((await this.super.groups.getGroupMuteSetting(_groupId)).members ?? []).map((member) => {
|
|
3714
|
+
if (member.username) this.nicknameCache.set(member.member_openid, member.username);
|
|
3715
|
+
const expireAt = member.mute_expire_at ? Date.parse(member.mute_expire_at) : NaN;
|
|
3716
|
+
return {
|
|
3717
|
+
userId: member.member_openid,
|
|
3718
|
+
muteTime: Number.isNaN(expireAt) ? 0 : Math.floor(expireAt / 1e3)
|
|
3719
|
+
};
|
|
3720
|
+
});
|
|
3721
|
+
} catch (err) {
|
|
3722
|
+
this.logger("warn", `[getGroupMuteList] 获取群禁言列表失败(需机器人具备群管理员身份): ${err instanceof Error ? err.message : err}`);
|
|
3723
|
+
return [];
|
|
3724
|
+
}
|
|
3050
3725
|
}
|
|
3051
3726
|
/**
|
|
3052
3727
|
* 戳一戳用户
|
|
@@ -3768,6 +4443,12 @@ const onGroupJoinRequest = (client, event) => {
|
|
|
3768
4443
|
const contact = karin$1.contactGroup(event.d.group_openid);
|
|
3769
4444
|
const srcReply = (elements) => client.sendMsg(contact, [...elements, segment.pasmsg(eventId, "event")]);
|
|
3770
4445
|
const time = Date.parse(event.d.apply_at);
|
|
4446
|
+
/** 审批接口需要群 / 申请人 openid,而 Karin 只透传 join_request_id */
|
|
4447
|
+
cacheJoinRequest(client, event.d.join_request_id, {
|
|
4448
|
+
groupId: event.d.group_openid,
|
|
4449
|
+
memberId: event.d.member_openid
|
|
4450
|
+
});
|
|
4451
|
+
if (event.d.username) client.nicknameCache.set(event.d.member_openid, event.d.username);
|
|
3771
4452
|
if (event.d.member_openid === client.selfId) createGroupInviteRequest({
|
|
3772
4453
|
bot: client,
|
|
3773
4454
|
time,
|
|
@@ -3813,12 +4494,16 @@ const onGroupJoinRequest = (client, event) => {
|
|
|
3813
4494
|
* 取得互动事件真正用于 ACK 的平台 ID。
|
|
3814
4495
|
*
|
|
3815
4496
|
* WebSocket Dispatch 外层也有一个 `id`,形如 `INTERACTION_CREATE:uuid`;按钮 ACK
|
|
3816
|
-
* 接口需要的是事件体内的 `d.id`,也就是不带事件名前缀的 uuid
|
|
4497
|
+
* 接口需要的是事件体内的 `d.id`,也就是不带事件名前缀的 uuid。缺少 `d.id` 时
|
|
4498
|
+
* 退回外层 `id`,并按官方要求剥掉 `INTERACTION_CREATE:` 前缀。
|
|
3817
4499
|
*
|
|
3818
4500
|
* @param ev QQBot 原始互动事件。
|
|
3819
4501
|
* @returns 可传给 `/interactions/{interaction_id}` 的 ID。
|
|
3820
4502
|
*/
|
|
3821
|
-
const getInteractionId = (ev) =>
|
|
4503
|
+
const getInteractionId = (ev) => {
|
|
4504
|
+
if (ev.d.id) return ev.d.id;
|
|
4505
|
+
return ev.id.replace(/^INTERACTION_CREATE:/, "");
|
|
4506
|
+
};
|
|
3822
4507
|
/**
|
|
3823
4508
|
* 构造按钮回调附加给 Karin 消息的元信息。
|
|
3824
4509
|
*
|
|
@@ -4448,14 +5133,13 @@ const QUICK_DISCONNECT_DELAY_MS = 6e4;
|
|
|
4448
5133
|
* 获取网关地址:优先调 /gateway;失败 / 超时 / 限频 → 直接 fallback 到硬编码
|
|
4449
5134
|
*/
|
|
4450
5135
|
const fetchGateway = async (cfg) => {
|
|
4451
|
-
const apiUrl = "https://api.bot.qq.com";
|
|
4452
5136
|
const accessToken = getBotAccessToken(cfg.appId);
|
|
4453
5137
|
if (!accessToken) {
|
|
4454
5138
|
log("warn", `${cfg.appId}: 无 access_token,使用硬编码 WS`);
|
|
4455
5139
|
return "wss://api.bot.qq.com/websocket";
|
|
4456
5140
|
}
|
|
4457
5141
|
try {
|
|
4458
|
-
const { data } = await axios.get(
|
|
5142
|
+
const { data } = await axios.get(`${QQAPIBASEURL}/gateway`, {
|
|
4459
5143
|
headers: {
|
|
4460
5144
|
Authorization: `QQBot ${accessToken}`,
|
|
4461
5145
|
"User-Agent": getUserAgent()
|
|
@@ -4754,13 +5438,14 @@ const createBot = async (input) => {
|
|
|
4754
5438
|
return promise;
|
|
4755
5439
|
};
|
|
4756
5440
|
/**
|
|
4757
|
-
* 销毁 bot:注销 + 关 WS + 取消监听 + 停止 token 刷新
|
|
5441
|
+
* 销毁 bot:注销 + 关 WS + 取消监听 + 停止 token 刷新 + 清理入群申请索引
|
|
4758
5442
|
*/
|
|
4759
5443
|
const destroyBot = (appId) => {
|
|
4760
5444
|
unregisterBot("selfId", appId);
|
|
4761
5445
|
stop(appId);
|
|
4762
5446
|
offAll(appId);
|
|
4763
5447
|
stopTokenRefresh(appId);
|
|
5448
|
+
clearJoinRequests(appId);
|
|
4764
5449
|
};
|
|
4765
5450
|
bindHandlers(createBot, destroyBot);
|
|
4766
5451
|
|
|
@@ -4910,4 +5595,4 @@ const bootstrap = async () => {
|
|
|
4910
5595
|
bootstrap();
|
|
4911
5596
|
|
|
4912
5597
|
//#endregion
|
|
4913
|
-
export {
|
|
5598
|
+
export { Http, QQBotApi, RequestApi };
|