@karinjs/adapter-qqbot 1.2.10 → 2.0.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.
Files changed (65) hide show
  1. package/README.md +326 -85
  2. package/dist/apps/login.d.ts +20 -0
  3. package/dist/apps/login.js +57 -0
  4. package/dist/config-H7LrvlNU.js +197 -0
  5. package/dist/index.d.ts +670 -0
  6. package/dist/index.js +2847 -0
  7. package/dist/onboard-BX8g8Wik.js +249 -0
  8. package/dist/utils-DO4gEkVN.js +3 -0
  9. package/dist/web.config.d.ts +22 -0
  10. package/dist/web.config.js +173 -0
  11. package/package.json +34 -21
  12. package/config/comment/config.json +0 -3
  13. package/config/config/config.yaml +0 -53
  14. package/lib/cli/pr.d.ts +0 -2
  15. package/lib/cli/pr.js +0 -73
  16. package/lib/core/adapter/adapter.d.ts +0 -91
  17. package/lib/core/adapter/adapter.js +0 -144
  18. package/lib/core/adapter/handler.d.ts +0 -49
  19. package/lib/core/adapter/handler.js +0 -140
  20. package/lib/core/adapter/markdown.d.ts +0 -38
  21. package/lib/core/adapter/markdown.js +0 -220
  22. package/lib/core/adapter/normal.d.ts +0 -30
  23. package/lib/core/adapter/normal.js +0 -263
  24. package/lib/core/api/index.d.ts +0 -167
  25. package/lib/core/api/index.js +0 -180
  26. package/lib/core/api/sign.d.ts +0 -22
  27. package/lib/core/api/sign.js +0 -40
  28. package/lib/core/api/types.d.ts +0 -236
  29. package/lib/core/api/types.js +0 -1
  30. package/lib/core/event/conver.d.ts +0 -10
  31. package/lib/core/event/conver.js +0 -57
  32. package/lib/core/event/index.d.ts +0 -9
  33. package/lib/core/event/index.js +0 -98
  34. package/lib/core/event/message.d.ts +0 -27
  35. package/lib/core/event/message.js +0 -95
  36. package/lib/core/event/notice.d.ts +0 -50
  37. package/lib/core/event/notice.js +0 -133
  38. package/lib/core/event/types.d.ts +0 -473
  39. package/lib/core/event/types.js +0 -1
  40. package/lib/core/internal/axios.d.ts +0 -25
  41. package/lib/core/internal/axios.js +0 -46
  42. package/lib/core/internal/sign.d.ts +0 -22
  43. package/lib/core/internal/sign.js +0 -40
  44. package/lib/core/webhook/c2c.d.ts +0 -2
  45. package/lib/core/webhook/c2c.js +0 -14
  46. package/lib/core/webhook/http.d.ts +0 -2
  47. package/lib/core/webhook/http.js +0 -35
  48. package/lib/core/webhook/index.d.ts +0 -17
  49. package/lib/core/webhook/index.js +0 -87
  50. package/lib/core/webhook/ws.d.ts +0 -2
  51. package/lib/core/webhook/ws.js +0 -79
  52. package/lib/index.d.ts +0 -1
  53. package/lib/index.js +0 -5
  54. package/lib/root.d.ts +0 -5
  55. package/lib/root.js +0 -9
  56. package/lib/types/config.d.ts +0 -53
  57. package/lib/types/config.js +0 -1
  58. package/lib/utils/common.d.ts +0 -80
  59. package/lib/utils/common.js +0 -198
  60. package/lib/utils/config.d.ts +0 -9
  61. package/lib/utils/config.js +0 -54
  62. package/lib/utils/dir.d.ts +0 -1
  63. package/lib/utils/dir.js +0 -1
  64. package/lib/utils/index.d.ts +0 -4
  65. package/lib/utils/index.js +0 -4
package/dist/index.js ADDED
@@ -0,0 +1,2847 @@
1
+ import { a as pkg, c as getImageSize, f as log, l as handleUrl, n as config, p as basename, r as getConfig, s as fakeEvent, t as bindHandlers, u as random } from "./config-H7LrvlNU.js";
2
+ import "./utils-DO4gEkVN.js";
3
+ import { t as needQrOnboard } from "./onboard-BX8g8Wik.js";
4
+ import { AdapterBase, app, buttonHandle, createDirectMessage, createFriendDecreaseNotice, createFriendIncreaseNotice, createFriendMessage, createGroupMemberAddNotice, createGroupMemberDelNotice, createGroupMessage, createGuildMessage, fileToUrl, karin, karinToQQBot, logger, registerBot, segment, unregisterBot } from "node-karin";
5
+ import { URL } from "url";
6
+ import lodash from "node-karin/lodash";
7
+ import FormData from "form-data";
8
+ import axios from "node-karin/axios";
9
+ import EventEmitter from "node:events";
10
+ import os from "node:os";
11
+ import { WebSocket } from "node-karin/ws";
12
+ import express from "node-karin/express";
13
+ import nacl from "tweetnacl";
14
+
15
+ //#region src/core/api/error.ts
16
+ /**
17
+ * OpenAPI 错误码映射表
18
+ * 来源: api-v2/openapi/error/error.md
19
+ *
20
+ * 规则:
21
+ * - 区间码 (如 301000~301099) 用范围匹配
22
+ * - 精确码直接查表
23
+ */
24
+ /** HTTP 状态码映射 */
25
+ const httpStatusMap = {
26
+ 200: "成功",
27
+ 204: "成功,但是无包体,一般用于删除操作",
28
+ 201: "异步操作成功,虽然说成功,但是会返回一个 error body,需要特殊处理",
29
+ 202: "异步操作成功,虽然说成功,但是会返回一个 error body,需要特殊处理",
30
+ 401: "认证失败",
31
+ 404: "未找到 API",
32
+ 405: "http method 不允许",
33
+ 429: "频率限制",
34
+ 500: "处理失败",
35
+ 504: "处理失败"
36
+ };
37
+ /** 业务错误码(code)映射 */
38
+ const errorCodeMap = {
39
+ 10001: "UnknownAccount 账号异常",
40
+ 10003: "UnknownChannel 子频道异常",
41
+ 10004: "UnknownGuild 频道异常",
42
+ 11281: "ErrorCheckAdminFailed 检查是否是管理员失败,系统错误,一般重试一次会好,最多只能重试一次",
43
+ 11282: "ErrorCheckAdminNotPass 检查是否是管理员未通过,该接口需要管理员权限,但是用户在添加机器人的时候并未授予该权限,属于逻辑错误,可以提示用户进行授权",
44
+ 11251: "ErrorWrongAppid 参数中的 appid 错误,开发者填的 token 错误,appid 无法识别",
45
+ 11252: "ErrorCheckAppPrivilegeFailed 检查应用权限失败,系统错误,一般重试一次会好,最多只能重试一次",
46
+ 11253: "ErrorCheckAppPrivilegeNotPass 检查应用权限不通过,该机器人应用未获得调用该接口的权限,需要向平台申请",
47
+ 11254: "ErrorInterfaceForbidden 应用接口被封禁,该机器人虽然获得了该接口权限,但是被封禁了",
48
+ 11261: "ErrorWrongAppid 参数中缺少 appid,同 11251",
49
+ 11262: "ErrorCheckRobot 当前接口不支持使用机器人 Bot Token 调用",
50
+ 11263: "ErrorCheckGuildAuth 检查频道权限失败,系统错误,一般重试一次会好,最多只能重试一次",
51
+ 11264: "ErrorGuildAuthNotPass 检查小站权限未通过,管理员添加机器人的时候未授予该接口权限,属于逻辑错误,可提示用户进行授权,如果已经给予授权,请检查传递的 guild id 是否正确",
52
+ 11265: "ErrorRobotHasBaned 机器人已经被封禁",
53
+ 11241: "ErrorWrongToken 参数中缺少 token",
54
+ 11242: "ErrorCheckTokenFailed 校验 token 失败,系统错误,一般重试一次会好,最多只能重试一次",
55
+ 11243: "ErrorCheckTokenNotPass 校验 token 未通过,用户填充的 token 错误,需要开发者进行检查",
56
+ 11273: "ErrorCheckUserAuth 检查用户权限失败,当前接口不支持使用 Bearer Token 调用",
57
+ 11274: "ErrorUserAuthNotPass 检查用户权限未通过,用户 OAuth 授权时未给与该接口权限,可提示用户重新进行授权",
58
+ 11275: "ErrorWrongAppid 无 appid,同 11251",
59
+ 11301: "ErrorGetHTTPHeader HTTP Header 无效",
60
+ 11302: "ErrorGetHeaderUIN HTTP Header 无效",
61
+ 11303: "ErrorGetNick 获取昵称失败",
62
+ 11304: "ErrorGetAvatar 获取头像失败",
63
+ 11305: "ErrorGetGuildID 获取频道 ID 失败",
64
+ 11306: "ErrorGetGuildInfo 获取频道信息失败",
65
+ 12001: "ReplaceIDFailed 替换 id 失败",
66
+ 12002: "RequestInvalid 请求体错误",
67
+ 12003: "ResponseInvalid 回包错误",
68
+ 20028: "ChannelHitWriteRateLimit 子频道消息触发限频",
69
+ 50006: "CannotSendEmptyMessage 消息为空",
70
+ 50035: "InvalidFormBody form-data 内容异常",
71
+ 50037: "带有 markdown 消息只支持 markdown 或者 keyboard 组合",
72
+ 50038: "非同频道同子频道",
73
+ 50039: "获取消息失败",
74
+ 50040: "消息模版类型错误",
75
+ 50041: "markdown 有空值",
76
+ 50042: "markdown 列表长达最大值",
77
+ 50043: "guild_id 转换失败",
78
+ 50045: "不能回复机器人自己产生的消息",
79
+ 50046: "非 at 机器人消息",
80
+ 50047: "非机器人产生的消息 或者 at 机器人消息",
81
+ 50048: "message id 不能为空",
82
+ 50049: "只能修改含有 keyboard 元素的消息",
83
+ 50050: "修改消息时,keyboard 元素不能为空",
84
+ 50051: "只能修改机器人自己发送的消息",
85
+ 50053: "修改消息错误",
86
+ 50054: "markdown 模版参数错误",
87
+ 50055: "无效的 markdown content",
88
+ 50056: "不允许发送 markdown content",
89
+ 50057: "markdown 参数只支持原生语法或者模版二选一",
90
+ 301e3: "参数错误",
91
+ 301001: "查询频道信息错误",
92
+ 301002: "查询子频道权限错误",
93
+ 301003: "修改子频道权限错误",
94
+ 301004: "私密子频道关联的人数到达上限",
95
+ 301005: "调用 Rpc 服务失败",
96
+ 301006: "非群成员没有查询权限",
97
+ 301007: "参数超过数量限制",
98
+ 302e3: "参数错误",
99
+ 302001: "查询频道信息错误",
100
+ 302002: "查询日程列表失败",
101
+ 302003: "查询日程失败",
102
+ 302004: "修改日程失败",
103
+ 302005: "删除日程失败",
104
+ 302006: "创建日程失败",
105
+ 302007: "获取创建者信息失败",
106
+ 302008: "子频道 ID 不能为空",
107
+ 302009: "频道系统错误,请联系客服",
108
+ 302010: "暂无修改日程权限",
109
+ 302011: "日程活动已被删除",
110
+ 302012: "每天只能创建 10 个日程,明天再来吧!",
111
+ 302013: "创建日程触发安全打击",
112
+ 302014: "日程持续时间超过 7 天,请重新选择",
113
+ 302015: "开始时间不能早于当前时间",
114
+ 302016: "结束时间不能早于开始时间",
115
+ 302017: "Schedule 对象为空",
116
+ 302018: "参数类型转换失败",
117
+ 302019: "调用下游失败,请联系客服",
118
+ 302020: "日程内容违规、账号违规",
119
+ 302021: "频道内当日新增活动达上限",
120
+ 302022: "不能绑定非当前频道的子频道",
121
+ 302023: "开始时跳转不可绑定日程子频道",
122
+ 302024: "绑定的子频道不存在",
123
+ 304003: "URL_NOT_ALLOWED url 未报备",
124
+ 304004: "ARK_NOT_ALLOWED 没有发 ark 消息权限",
125
+ 304005: "EMBED_LIMIT embed 长度超限",
126
+ 304006: "SERVER_CONFIG 后台配置错误",
127
+ 304007: "GET_GUILD 查询频道异常",
128
+ 304008: "GET_BOT 查询机器人异常",
129
+ 304009: "GET_CHENNAL 查询子频道异常",
130
+ 304010: "CHANGE_IMAGE_URL 图片转存错误",
131
+ 304011: "NO_TEMPLATE 模板不存在",
132
+ 304012: "GET_TEMPLATE 取模板错误",
133
+ 304014: "TEMPLATE_PRIVILEGE 没有模板权限",
134
+ 304016: "SEND_ERROR 发消息错误",
135
+ 304017: "UPLOAD_IMAGE 图片上传错误",
136
+ 304018: "SESSION_NOT_EXIST 机器人没连上 gateway",
137
+ 304019: "AT_EVERYONE_TIMES @全体成员次数超限",
138
+ 304020: "FILE_SIZE 文件大小超限",
139
+ 304021: "GET_FILE 下载文件错误",
140
+ 304022: "PUSH_TIME 推送消息时间限制",
141
+ 304023: "PUSH_MSG_ASYNC_OK 推送消息异步调用成功,等待人工审核",
142
+ 304024: "REPLY_MSG_ASYNC_OK 回复消息异步调用成功,等待人工审核",
143
+ 304025: "BEAT 消息被打击",
144
+ 304026: "MSG_ID 回复的消息 id 错误",
145
+ 304027: "MSG_EXPIRE 回复的消息过期",
146
+ 304028: "MSG_PROTECT 非 At 当前用户的消息不允许回复",
147
+ 304029: "CORPUS_ERROR 调语料服务错误",
148
+ 304030: "CORPUS_NOT_MATCH 语料不匹配",
149
+ 304031: "私信已关闭",
150
+ 304032: "私信不存在",
151
+ 304033: "拉私信错误",
152
+ 304034: "不是私信成员",
153
+ 304035: "推送消息超过子频道数量限制",
154
+ 304036: "没有 markdown 模板的权限",
155
+ 304037: "没有发消息按钮组件的权限",
156
+ 304038: "消息按钮组件不存在",
157
+ 304039: "消息按钮组件解析错误",
158
+ 304040: "消息按钮组件消息内容错误",
159
+ 304044: "取消息设置错误",
160
+ 304045: "子频道主动消息数限频",
161
+ 304046: "不允许在此子频道发主动消息",
162
+ 304047: "主动消息推送超过限制的子频道数",
163
+ 304048: "不允许在此频道发主动消息",
164
+ 304049: "私信主动消息数限频",
165
+ 304050: "私信主动消息总量限频",
166
+ 304051: "消息设置引导请求构造错误",
167
+ 304052: "发消息设置引导超频",
168
+ 306001: "param invalid 撤回消息参数错误",
169
+ 306002: "msgid error 消息 id 错误",
170
+ 306003: "fail to get message 获取消息错误(可重试)",
171
+ 306004: "no permission to delete message 没有撤回此消息的权限",
172
+ 306005: "retract message error 消息撤回失败(可重试)",
173
+ 306006: "fail to get channel 获取子频道失败(可重试)",
174
+ 501001: "参数校验失败",
175
+ 501002: "创建子频道公告失败(可重试)",
176
+ 501003: "删除子频道公告失败(可重试)",
177
+ 501004: "获取频道信息失败(可重试)",
178
+ 501005: "MessageID 错误",
179
+ 501006: "创建频道全局公告失败(可重试)",
180
+ 501007: "删除频道全局公告失败(可重试)",
181
+ 501008: "MessageID 不存在",
182
+ 501009: "MessageID 解析失败",
183
+ 501010: "此条消息非子频道内消息",
184
+ 501011: "创建精华消息失败(可重试)",
185
+ 501012: "删除精华消息失败(可重试)",
186
+ 501013: "精华消息超过最大数量",
187
+ 501014: "安全打击",
188
+ 501015: "此消息不允许设置",
189
+ 501016: "频道公告子频道推荐超过最大数量",
190
+ 501017: "非频道主或管理员",
191
+ 501018: "推荐子频道 ID 无效",
192
+ 501019: "公告类型错误",
193
+ 501020: "创建推荐子频道类型频道公告失败",
194
+ 502001: "频道 id 无效",
195
+ 502002: "频道 id 为空",
196
+ 502003: "用户 id 无效",
197
+ 502004: "用户 id 为空",
198
+ 502005: "timestamp 不合法",
199
+ 502006: "timestamp 无效",
200
+ 502007: "参数转换错误",
201
+ 502008: "rpc 调用失败",
202
+ 502009: "安全打击",
203
+ 502010: "请求头错误",
204
+ 503001: "频道 id 无效",
205
+ 503002: "频道 id 为空",
206
+ 503003: "获取子频道信息失败",
207
+ 503004: "超出发布帖子的频次限制",
208
+ 503005: "帖子标题为空",
209
+ 503006: "帖子内容为空",
210
+ 503007: "帖子 ID 为空",
211
+ 503008: "获取 X-Uin 失败",
212
+ 503009: "帖子 ID 无效或不合法",
213
+ 503010: "通过 Uin 获取 TinyID 失败",
214
+ 503011: "帖子 ID 里面的时间戳无效或不合法",
215
+ 503012: "帖子不存在或已删除",
216
+ 503013: "服务器内部错误",
217
+ 503014: "帖子 JSON 内容解析失败",
218
+ 503015: "帖子内容转换失败",
219
+ 503016: "链接数量超过限制",
220
+ 503017: "字数超过限制",
221
+ 503018: "图片数量超过限制",
222
+ 503019: "视频数量超过限制",
223
+ 503020: "标题长度超过限制",
224
+ 504001: "请求参数无效错误",
225
+ 504002: "获取 HTTP 头失败",
226
+ 504003: "获取 BOT UIN 错误",
227
+ 504004: "获取消息频率设置信息错误",
228
+ 610001: "获取频道 ID 失败",
229
+ 610002: "获取 HTTP 头失败",
230
+ 610003: "获取机器人号码失败",
231
+ 610004: "获取机器人角色失败",
232
+ 610005: "获取机器人角色内部错误",
233
+ 610006: "拉取机器人权限列表失败",
234
+ 610007: "机器人不在频道内",
235
+ 610008: "无效参数",
236
+ 610009: "获取 API 接口详情失败",
237
+ 610010: "API 接口已授权",
238
+ 610011: "获取机器人信息失败",
239
+ 610012: "限频失败",
240
+ 610013: "已限频",
241
+ 610014: "api 授权链接发送失败",
242
+ 620001: "表情表态无效参数",
243
+ 620002: "已经达到表情反应的类型数量上限",
244
+ 620003: "已经设置过该表情表态",
245
+ 620004: "没有设置过该表情表态",
246
+ 620005: "没有权限设置表情表态",
247
+ 620006: "操作限频",
248
+ 620007: "表情表态操作失败,请重试",
249
+ 630001: "互动回调数据更新无效参数",
250
+ 630002: "互动回调数据更新获取 AppID 失败",
251
+ 630003: "互动回调数据 AppID 不匹配",
252
+ 630004: "互动回调数据更新内部存储错误",
253
+ 630005: "互动回调数据更新内部存储读取错误",
254
+ 630006: "互动回调数据更新读取请求 AppID 失败",
255
+ 630007: "互动回调数据太大",
256
+ 1100100: "安全打击:消息被限频",
257
+ 1100101: "安全打击:内容涉及敏感,请返回修改",
258
+ 1100102: "安全打击:抱歉,暂未获得新功能体验资格",
259
+ 1100103: "安全打击",
260
+ 1100104: "安全打击:该群已失效或当前群已不存在",
261
+ 1100300: "系统内部错误",
262
+ 1100301: "调用方不是群成员",
263
+ 1100302: "获取指定频道名称失败",
264
+ 1100303: "主页频道非管理员不允许发消息",
265
+ 1100304: "@次数鉴权失败",
266
+ 1100305: "TinyId 转换 Uin 失败",
267
+ 1100306: "非私有频道成员",
268
+ 1100307: "非白名单应用子频道",
269
+ 1100308: "触发频道内限频",
270
+ 1100499: "其他错误",
271
+ 3300006: "安全打击"
272
+ };
273
+ const rangeDefs = [
274
+ {
275
+ min: 301e3,
276
+ max: 301099,
277
+ message: "子频道权限错误"
278
+ },
279
+ {
280
+ min: 501e3,
281
+ max: 501999,
282
+ message: "公告错误"
283
+ },
284
+ {
285
+ min: 502e3,
286
+ max: 502099,
287
+ message: "禁言相关错误"
288
+ },
289
+ {
290
+ min: 504e3,
291
+ max: 504999,
292
+ message: "消息频率相关错误"
293
+ },
294
+ {
295
+ min: 61e4,
296
+ max: 619999,
297
+ message: "频道权限错误"
298
+ },
299
+ {
300
+ min: 620001,
301
+ max: 629999,
302
+ message: "表情表态错误"
303
+ },
304
+ {
305
+ min: 630001,
306
+ max: 639999,
307
+ message: "互动回调数据更新错误"
308
+ },
309
+ {
310
+ min: 1e6,
311
+ max: 2999999,
312
+ message: "发消息错误"
313
+ },
314
+ {
315
+ min: 3e6,
316
+ max: 3999999,
317
+ message: "编辑消息错误"
318
+ }
319
+ ];
320
+ /**
321
+ * 根据 code 查找对应的中文错误描述
322
+ * @param code - 业务错误码
323
+ * @returns 中文错误描述,未找到则返回 undefined
324
+ */
325
+ const getErrorMessage = (code) => {
326
+ const exact = errorCodeMap[code];
327
+ if (exact) return exact;
328
+ for (const range of rangeDefs) if (code >= range.min && code <= range.max) return `${range.message} (code: ${code})`;
329
+ };
330
+ /**
331
+ * 格式化 OpenAPI 错误为可读字符串
332
+ * @param status - HTTP 状态码
333
+ * @param code - 业务错误码
334
+ * @param message - 原始消息
335
+ * @returns 格式化后的错误字符串
336
+ */
337
+ const formatOpenAPIError = (status, code, message) => {
338
+ const parts = [];
339
+ const statusText = httpStatusMap[status];
340
+ if (statusText) parts.push(`[HTTP ${status}] ${statusText}`);
341
+ else parts.push(`[HTTP ${status}]`);
342
+ if (code !== void 0) {
343
+ const codeText = getErrorMessage(code);
344
+ if (codeText) parts.push(`[Code ${code}] ${codeText}`);
345
+ else parts.push(`[Code ${code}]`);
346
+ }
347
+ if (message) parts.push(message);
348
+ return parts.join(" | ");
349
+ };
350
+
351
+ //#endregion
352
+ //#region src/core/api/http.ts
353
+ /**
354
+ * 包装 axios 错误为可读多行 message
355
+ * 如果响应中包含 QQ 官方错误码(code),会自动映射为中文描述
356
+ */
357
+ const formatError = (path, options, err) => {
358
+ if (axios.isAxiosError(err)) {
359
+ const response = err.response;
360
+ const status = response?.status ?? 0;
361
+ const data = response?.data;
362
+ const code = typeof data?.code === "number" ? data.code : void 0;
363
+ const msg = typeof data?.message === "string" ? data.message : void 0;
364
+ const lines = [];
365
+ lines.push("[axios] 请求失败");
366
+ lines.push(`请求路径: ${path}`);
367
+ lines.push(`请求数据: ${lodash.truncate(JSON.stringify(options), { length: 500 })}`);
368
+ if (code !== void 0 || status > 0) lines.push(`错误详情: ${formatOpenAPIError(status, code, msg)}`);
369
+ lines.push(`响应数据: ${JSON.stringify(data)}`);
370
+ return new Error(lines.join("\n"));
371
+ }
372
+ if (err instanceof Error) return err;
373
+ return new Error(typeof err === "string" ? err : JSON.stringify(err));
374
+ };
375
+ /**
376
+ * Http 基础类,子模块继承此类共享 axios 实例与错误格式化
377
+ */
378
+ var Http = class {
379
+ axios;
380
+ constructor(axios) {
381
+ this.axios = axios;
382
+ }
383
+ async get(path) {
384
+ try {
385
+ const { data } = await this.axios.get(path);
386
+ return data;
387
+ } catch (err) {
388
+ throw formatError(path, void 0, err);
389
+ }
390
+ }
391
+ async post(path, body = {}, headers) {
392
+ try {
393
+ const { data } = await this.axios.post(path, body, { headers });
394
+ return data;
395
+ } catch (err) {
396
+ throw formatError(path, body, err);
397
+ }
398
+ }
399
+ async put(path, body = {}, headers) {
400
+ try {
401
+ const { data } = await this.axios.put(path, body, { headers });
402
+ return data;
403
+ } catch (err) {
404
+ throw formatError(path, body, err);
405
+ }
406
+ }
407
+ async delete(path) {
408
+ try {
409
+ const { data } = await this.axios.delete(path);
410
+ return data;
411
+ } catch (err) {
412
+ throw formatError(path, void 0, err);
413
+ }
414
+ }
415
+ };
416
+
417
+ //#endregion
418
+ //#region src/core/api/messages.ts
419
+ /**
420
+ * 消息发送 + 撤回
421
+ */
422
+ var MessagesApi = class extends Http {
423
+ /** 单聊消息 */
424
+ sendFriendMsg(openid, body) {
425
+ return this.post(`/v2/users/${openid}/messages`, body);
426
+ }
427
+ /** 群聊消息 */
428
+ sendGroupMsg(groupOpenid, body) {
429
+ return this.post(`/v2/groups/${groupOpenid}/messages`, body);
430
+ }
431
+ /** 文字子频道消息 */
432
+ sendChannelMsg(channelId, body) {
433
+ const headers = body instanceof FormData ? body.getHeaders() : void 0;
434
+ return this.post(`/channels/${channelId}/messages`, body, headers);
435
+ }
436
+ async sendDmsMsg(a, b, c) {
437
+ if (typeof b === "string") {
438
+ const dms = await this.post("/users/@me/dms", {
439
+ recipient_id: a,
440
+ source_guild_id: b
441
+ });
442
+ const headers = c instanceof FormData ? c.getHeaders() : void 0;
443
+ return this.post(`/dms/${dms.guild_id}/messages`, c, headers);
444
+ }
445
+ const headers = b instanceof FormData ? b.getHeaders() : void 0;
446
+ return this.post(`/dms/${a}/messages`, b, headers);
447
+ }
448
+ async recall(type, peer, messageId, hidetip = false) {
449
+ const path = type === "user" || type === "group" ? `/v2/${type}s/${peer}/messages/${messageId}` : `/${type}/${peer}/messages/${messageId}?hidetip=${hidetip}`;
450
+ await this.delete(path);
451
+ return true;
452
+ }
453
+ };
454
+
455
+ //#endregion
456
+ //#region src/core/api/media.ts
457
+ const FILE_TYPE = {
458
+ image: 1,
459
+ video: 2,
460
+ record: 3,
461
+ file: 4
462
+ };
463
+ /**
464
+ * 富媒体上传
465
+ */
466
+ var MediaApi = class extends Http {
467
+ /**
468
+ * @param scene user(单聊) / group(群聊)
469
+ * @param peer openid 或 group_openid
470
+ * @param type 文件类型
471
+ * @param source 必须是 http url 或 base64 字符串
472
+ * @param srvSendMsg 上传后是否由服务端直接发送
473
+ */
474
+ upload(scene, peer, type, source, srvSendMsg = false) {
475
+ const body = {
476
+ file_type: FILE_TYPE[type],
477
+ srv_send_msg: srvSendMsg
478
+ };
479
+ if (source.startsWith("http")) body.url = source;
480
+ else body.file_data = source.replace(/^data:[\w/+-]+;base64,|^base64:\/\//, "");
481
+ return this.post(`/v2/${scene}s/${peer}/files`, body);
482
+ }
483
+ };
484
+
485
+ //#endregion
486
+ //#region src/core/api/interaction.ts
487
+ /**
488
+ * INTERACTION_CREATE 回应
489
+ */
490
+ var InteractionApi = class extends Http {
491
+ /**
492
+ * @param interactionId INTERACTION_CREATE 事件中的 id
493
+ * @param code 0 成功 / 1 操作失败 / 2 操作频繁 / 3 重复操作 / 4 无权限 / 5 仅管理员
494
+ */
495
+ async ack(interactionId, code = 0) {
496
+ await this.put(`/interactions/${interactionId}`, { code });
497
+ }
498
+ };
499
+
500
+ //#endregion
501
+ //#region src/core/api/meta.ts
502
+ /**
503
+ * 元信息接口:@me / dms / gateway
504
+ */
505
+ var MetaApi = class extends Http {
506
+ /** 当前机器人详情 */
507
+ getMe() {
508
+ return this.get("/users/@me");
509
+ }
510
+ /** 创建频道私信会话 */
511
+ createDms(recipientId, srcGuildId) {
512
+ return this.post("/users/@me/dms", {
513
+ recipient_id: recipientId,
514
+ source_guild_id: srcGuildId
515
+ });
516
+ }
517
+ /** 获取通用 WSS 地址 */
518
+ getGateway() {
519
+ return this.get("/gateway");
520
+ }
521
+ };
522
+
523
+ //#endregion
524
+ //#region src/core/api/builders.ts
525
+ /**
526
+ * 构造 QQ 单聊 / 群聊请求体
527
+ */
528
+ const buildQQMsg = {
529
+ text(content) {
530
+ return {
531
+ msg_type: 0,
532
+ content
533
+ };
534
+ },
535
+ markdown(markdown, keyboard) {
536
+ return {
537
+ msg_type: 2,
538
+ markdown,
539
+ keyboard
540
+ };
541
+ },
542
+ ark(ark) {
543
+ return {
544
+ msg_type: 3,
545
+ ark
546
+ };
547
+ },
548
+ /** msg_type=7 时 content 字段必填,传一个空格占位 */
549
+ media(fileInfo) {
550
+ return {
551
+ msg_type: 7,
552
+ media: { file_info: fileInfo },
553
+ content: " "
554
+ };
555
+ }
556
+ };
557
+ /**
558
+ * 构造频道请求体
559
+ */
560
+ const buildGuildMsg = {
561
+ text(content, image) {
562
+ return {
563
+ type: "text",
564
+ content,
565
+ image
566
+ };
567
+ },
568
+ image(image) {
569
+ return {
570
+ type: "image",
571
+ image
572
+ };
573
+ },
574
+ embed(embed) {
575
+ return {
576
+ type: "embed",
577
+ embed
578
+ };
579
+ },
580
+ ark(ark) {
581
+ return {
582
+ type: "ark",
583
+ ark
584
+ };
585
+ },
586
+ markdown(markdown, keyboard) {
587
+ return {
588
+ type: "markdown",
589
+ markdown,
590
+ keyboard
591
+ };
592
+ }
593
+ };
594
+
595
+ //#endregion
596
+ //#region src/core/api/index.ts
597
+ /**
598
+ * QQBot 官方 API 门面
599
+ *
600
+ * 子模块按职能拆分:
601
+ * - messages:发送 / 撤回
602
+ * - media:富媒体上传
603
+ * - interaction:按钮回调 ack
604
+ * - meta:@me / dms / gateway
605
+ *
606
+ * 请求体构造器 {@link buildQQMsg} / {@link buildGuildMsg} 独立导出,避免与 HTTP 调用耦合
607
+ */
608
+ var QQBotApi = class {
609
+ axios;
610
+ messages;
611
+ media;
612
+ interaction;
613
+ meta;
614
+ /** 请求体构造器(静态便利) */
615
+ qq = buildQQMsg;
616
+ guild = buildGuildMsg;
617
+ constructor(axios) {
618
+ this.axios = axios;
619
+ this.messages = new MessagesApi(axios);
620
+ this.media = new MediaApi(axios);
621
+ this.interaction = new InteractionApi(axios);
622
+ this.meta = new MetaApi(axios);
623
+ }
624
+ };
625
+
626
+ //#endregion
627
+ //#region src/core/internal/axios.ts
628
+ const tokens = /* @__PURE__ */ new Map();
629
+ /**
630
+ * 拉取 access_token 并安排下一次刷新
631
+ * - 提前 50s 刷新,防止边界过期
632
+ * - 同一 appId 旧定时器会被清理,避免并发刷新链
633
+ */
634
+ const getAccessToken = async (url, appId, secret) => {
635
+ let res;
636
+ try {
637
+ res = await axios.post(url, {
638
+ appId: String(appId),
639
+ clientSecret: secret
640
+ });
641
+ } catch (err) {
642
+ if (axios.isAxiosError(err)) {
643
+ const response = err.response;
644
+ const status = response?.status ?? 0;
645
+ const data = response?.data;
646
+ log("error", `[${appId}] 获取 access_token 失败 | ${formatOpenAPIError(status, typeof data?.code === "number" ? data.code : void 0, typeof data?.message === "string" ? data.message : void 0)}`);
647
+ } else log("error", `[${appId}] 获取 access_token 失败:`, err);
648
+ throw err;
649
+ }
650
+ const accessToken = res.data?.access_token;
651
+ const expiresIn = Number(res.data?.expires_in);
652
+ if (!accessToken || !expiresIn || Number.isNaN(expiresIn)) throw new Error(`获取 access_token 失败: ${JSON.stringify(res.data)}`);
653
+ const prev = tokens.get(appId);
654
+ if (prev?.refreshTimer) clearTimeout(prev.refreshTimer);
655
+ const state = { accessToken };
656
+ tokens.set(appId, state);
657
+ const delay = Math.max(5e3, expiresIn * 1e3 - 5e4);
658
+ state.refreshTimer = setTimeout(() => {
659
+ getAccessToken(url, appId, secret).catch(() => {});
660
+ }, delay);
661
+ return {
662
+ accessToken,
663
+ expiresIn
664
+ };
665
+ };
666
+ /**
667
+ * 读取已缓存的 access_token
668
+ */
669
+ const getBotAccessToken = (appId) => {
670
+ return tokens.get(appId)?.accessToken;
671
+ };
672
+ /**
673
+ * 停止某个 bot 的 token 刷新(销毁 bot 时调用)
674
+ */
675
+ const stopTokenRefresh = (appId) => {
676
+ const state = tokens.get(appId);
677
+ if (!state) return;
678
+ if (state.refreshTimer) clearTimeout(state.refreshTimer);
679
+ tokens.delete(appId);
680
+ };
681
+ /**
682
+ * 创建 axios 实例,自动从 tokens map 取最新 access_token
683
+ */
684
+ const createAxiosInstance = (baseURL, appId) => {
685
+ const instance = axios.create({
686
+ baseURL,
687
+ headers: { "Content-Type": "application/json" },
688
+ timeout: 5500
689
+ });
690
+ instance.interceptors.request.use((config) => {
691
+ const token = tokens.get(appId)?.accessToken;
692
+ if (token) config.headers.Authorization = `QQBot ${token}`;
693
+ return config;
694
+ });
695
+ return instance;
696
+ };
697
+
698
+ //#endregion
699
+ //#region src/core/adapter/grouping.ts
700
+ /**
701
+ * 创建空容器
702
+ */
703
+ const createGrouping = () => ({
704
+ text: [],
705
+ qqImages: [],
706
+ guildImageUrls: [],
707
+ guildImageFiles: [],
708
+ buttons: [],
709
+ keyboards: [],
710
+ markdowns: [],
711
+ media: [],
712
+ reply: { messageId: "" },
713
+ pasmsg: {
714
+ type: "msg",
715
+ id: "",
716
+ seq: random(1, 9999999)
717
+ },
718
+ faces: []
719
+ });
720
+ /**
721
+ * 将 karin elements 归类到容器
722
+ */
723
+ const groupElements = (scene, elements) => {
724
+ const g = createGrouping();
725
+ const isQQ = scene === "group" || scene === "friend";
726
+ for (const v of elements) switch (v.type) {
727
+ case "text":
728
+ g.text.push(v.text);
729
+ break;
730
+ case "image":
731
+ if (isQQ) g.qqImages.push(v.file);
732
+ else v.file.startsWith("http") ? g.guildImageUrls.push(v.file) : g.guildImageFiles.push(v.file);
733
+ break;
734
+ case "at":
735
+ if (scene === "friend") break;
736
+ if (v.targetId === "all") g.text.push("<qqbot-at-everyone />");
737
+ else g.text.push(`<qqbot-at-user id="${v.targetId}" />`);
738
+ break;
739
+ case "face":
740
+ if (!isQQ) {
741
+ g.text.push(`<emoji:${v.id}>`);
742
+ g.faces.push(Number(v.id));
743
+ }
744
+ break;
745
+ case "reply":
746
+ g.reply.messageId = v.messageId;
747
+ break;
748
+ case "pasmsg":
749
+ if (v.source === "event") g.pasmsg.type = "event";
750
+ g.pasmsg.id = v.id;
751
+ break;
752
+ case "button":
753
+ g.buttons.push(v);
754
+ break;
755
+ case "keyboard":
756
+ g.keyboards.push(v);
757
+ break;
758
+ case "markdown":
759
+ g.markdowns.push(v);
760
+ break;
761
+ case "video":
762
+ g.media.push({
763
+ kind: "video",
764
+ source: v.file
765
+ });
766
+ break;
767
+ case "record":
768
+ g.media.push({
769
+ kind: "record",
770
+ source: v.file
771
+ });
772
+ break;
773
+ case "file":
774
+ g.media.push({
775
+ kind: "file",
776
+ source: v.file
777
+ });
778
+ break;
779
+ default: break;
780
+ }
781
+ return g;
782
+ };
783
+
784
+ //#endregion
785
+ //#region src/core/adapter/text-to-md.ts
786
+ const MAX_BUTTONS = 5;
787
+ /**
788
+ * 提取文本中的 URL,将其替换为 `[按钮 N]` 占位文本并返回 button 列表
789
+ *
790
+ * @param text 文本
791
+ * @param friendly 是否为单聊(true 时清除残余的 at 标签)
792
+ */
793
+ const extractUrlButtons = (text, friendly = false) => {
794
+ let str = text.replace(/@everyone/g, "everyone").replace(/\n/g, "\r");
795
+ if (friendly) str = str.replace(/<qqbot-at-user id="[^"]+"\s*\/?>/g, "").replace(/<@!\d+>/g, "");
796
+ const urls = handleUrl(str);
797
+ if (urls.length === 0) return {
798
+ text: str,
799
+ buttons: []
800
+ };
801
+ const buttons = [];
802
+ urls.forEach((url, i) => {
803
+ str = str.replace(new RegExp(url, "g"), `[请点击 按钮${i} 查看]`);
804
+ buttons.push(segment.button({
805
+ text: `${i}. ${url}`,
806
+ link: url
807
+ }));
808
+ });
809
+ return {
810
+ text: str,
811
+ buttons: buttons.slice(0, MAX_BUTTONS)
812
+ };
813
+ };
814
+ /**
815
+ * 将图片列表合并到一段 markdown 文本内(每张图占一行)
816
+ */
817
+ const imagesToMarkdown = async (urls) => {
818
+ return Promise.all(urls.map(async (file) => {
819
+ if (file.startsWith("http")) {
820
+ const { width, height } = await getImageSize(file);
821
+ return `![karin #${width}px #${height}px](${file})`;
822
+ }
823
+ const { url, width, height } = await fileToUrl("image", file, "image.jpg");
824
+ return `![karin #${width}px #${height}px](${url})`;
825
+ }));
826
+ };
827
+
828
+ //#endregion
829
+ //#region src/core/adapter/pipeline-qq.ts
830
+ /** 群聊 event_id 白名单 */
831
+ const GROUP_EVENT_WHITELIST = new Set([
832
+ "INTERACTION_CREATE",
833
+ "GROUP_ADD_ROBOT",
834
+ "GROUP_MSG_RECEIVE"
835
+ ]);
836
+ /** 单聊 event_id 白名单 */
837
+ const FRIEND_EVENT_WHITELIST = new Set([
838
+ "INTERACTION_CREATE",
839
+ "C2C_MSG_RECEIVE",
840
+ "FRIEND_ADD"
841
+ ]);
842
+ /**
843
+ * 处理 QQ 场景(单聊 + 群聊)的发送
844
+ */
845
+ const sendQQ = async (ctx, contact, elements) => {
846
+ const target = contact.scene === "friend" ? "user" : "group";
847
+ const grouping = groupElements(contact.scene, elements);
848
+ if (ctx.cfg.keyboard.enable && grouping.text.length) {
849
+ const { text, buttons } = extractUrlButtons(grouping.text.join(""), contact.scene === "friend");
850
+ grouping.text = [text];
851
+ grouping.buttons.push(...buttons);
852
+ }
853
+ /**
854
+ * 永远走 markdown 通道:
855
+ * text / at / image 通过 markdown content 渲染(at 用内嵌标签);
856
+ * 视频 / 语音 / 文件由 sendQQMarkdown 内部以 msg_type=7 并发补发。
857
+ * Markdown / Keyboard 已全量开放,无需再走老的 msg_type=0 文本通道。
858
+ */
859
+ return sendQQMarkdown(ctx, contact, grouping, target);
860
+ };
861
+ /**
862
+ * Markdown 通道:合成一条 msg_type=2 + 可选 keyboard
863
+ */
864
+ const sendQQMarkdown = async (ctx, contact, grouping, target) => {
865
+ const items = [];
866
+ const lines = [];
867
+ if (grouping.text.length) lines.push(grouping.text.join(""));
868
+ if (grouping.qqImages.length) {
869
+ const urlImages = [];
870
+ for (const file of grouping.qqImages) if (file.startsWith("http")) urlImages.push(file);
871
+ else {
872
+ const { url } = await fileToUrl("image", file, "image.jpg");
873
+ urlImages.push(url);
874
+ }
875
+ const mdImages = await imagesToMarkdown(urlImages);
876
+ lines.push(...mdImages);
877
+ }
878
+ if (grouping.markdowns.length) grouping.markdowns.forEach((m) => lines.push(m.markdown));
879
+ if (lines.length || grouping.buttons.length || grouping.keyboards.length) {
880
+ const content = lines.join("\n");
881
+ const keyboard = buildKeyboard$1(grouping);
882
+ items.push(ctx.super.qq.markdown({ content }, keyboard));
883
+ }
884
+ for (const m of grouping.media) {
885
+ let url = m.source;
886
+ if (!url.startsWith("http")) {
887
+ const ext = m.kind === "record" ? "mp3" : m.kind === "video" ? "mp4" : "bin";
888
+ url = (await fileToUrl(m.kind, url, `${m.kind}.${ext}`)).url;
889
+ }
890
+ const res = await ctx.super.media.upload(target, contact.peer, m.kind, url, false);
891
+ items.push(ctx.super.qq.media(res.file_info));
892
+ }
893
+ if (!items.length) items.push(ctx.super.qq.text("不支持发送的消息类型"));
894
+ return flushQQ(ctx, contact, grouping, items);
895
+ };
896
+ /**
897
+ * 构造 keyboard 字段(buttons + keyboards 合并)
898
+ */
899
+ const buildKeyboard$1 = (grouping) => {
900
+ const rows = [];
901
+ grouping.buttons.forEach((b) => rows.push(...karinToQQBot(b)));
902
+ grouping.keyboards.forEach((k) => rows.push(...karinToQQBot(k)));
903
+ if (!rows.length) return void 0;
904
+ return { content: { rows } };
905
+ };
906
+ /**
907
+ * 逐条发送 + 被动消息附加 + 引用回复
908
+ */
909
+ const flushQQ = async (ctx, contact, grouping, items) => {
910
+ const result = ctx.initSendMsgResults();
911
+ const passive = buildPassiveQQ(contact.scene, grouping);
912
+ const send = contact.scene === "friend" ? (peer, item) => ctx.super.messages.sendFriendMsg(peer, item) : (peer, item) => ctx.super.messages.sendGroupMsg(peer, item);
913
+ let replyHandled = false;
914
+ for (const item of items) {
915
+ passive(item);
916
+ if (!replyHandled && grouping.reply.messageId) {
917
+ item.message_reference = { message_id: grouping.reply.messageId };
918
+ replyHandled = true;
919
+ }
920
+ const res = await send(contact.peer, item);
921
+ result.rawData.push(res);
922
+ }
923
+ return ctx.handleResponse(result);
924
+ };
925
+ /**
926
+ * 构造被动消息附加器(含白名单校验)
927
+ */
928
+ const buildPassiveQQ = (scene, grouping) => {
929
+ if (!grouping.pasmsg.id) return (_item) => void 0;
930
+ const whitelist = scene === "friend" ? FRIEND_EVENT_WHITELIST : GROUP_EVENT_WHITELIST;
931
+ if (grouping.pasmsg.type === "event") {
932
+ const eventName = grouping.pasmsg.id.split(":")[0];
933
+ if (eventName && !whitelist.has(eventName)) {}
934
+ return (item) => {
935
+ grouping.pasmsg.seq++;
936
+ item.msg_seq = grouping.pasmsg.seq;
937
+ item.event_id = grouping.pasmsg.id;
938
+ };
939
+ }
940
+ return (item) => {
941
+ grouping.pasmsg.seq++;
942
+ item.msg_seq = grouping.pasmsg.seq;
943
+ item.msg_id = grouping.pasmsg.id;
944
+ };
945
+ };
946
+
947
+ //#endregion
948
+ //#region src/core/adapter/pipeline-guild.ts
949
+ /**
950
+ * 处理频道场景(子频道 + 私信)的发送
951
+ *
952
+ * 永远走 markdown 通道(msg_type=2),文本 / at / 图片均通过 markdown
953
+ * 渲染。Markdown / Keyboard 已全量开放,老的 type=text/image 通道不再使用。
954
+ */
955
+ const sendGuild = async (ctx, contact, elements) => {
956
+ const grouping = groupElements(contact.scene, elements);
957
+ if (ctx.cfg.keyboard.enable && grouping.text.length) {
958
+ const { text, buttons } = extractUrlButtons(grouping.text.join(""), false);
959
+ grouping.text = [text];
960
+ grouping.buttons.push(...buttons);
961
+ }
962
+ return sendGuildMarkdown(ctx, contact, grouping);
963
+ };
964
+ /**
965
+ * Markdown 通道
966
+ */
967
+ const sendGuildMarkdown = async (ctx, contact, grouping) => {
968
+ const items = [];
969
+ const lines = [];
970
+ if (grouping.text.length) lines.push(grouping.text.join(""));
971
+ const allImages = [...grouping.guildImageUrls];
972
+ for (const file of grouping.guildImageFiles) {
973
+ const { url } = await fileToUrl("image", file, "image.jpg");
974
+ allImages.push(url);
975
+ }
976
+ if (allImages.length) {
977
+ const mdImages = await imagesToMarkdown(allImages);
978
+ lines.push(...mdImages);
979
+ }
980
+ grouping.markdowns.forEach((m) => lines.push(m.markdown));
981
+ if (lines.length || grouping.buttons.length || grouping.keyboards.length) {
982
+ const keyboard = buildKeyboard(grouping);
983
+ items.push(ctx.super.guild.markdown({ content: lines.join("\n") }, keyboard));
984
+ }
985
+ if (!items.length) items.push(ctx.super.guild.text("不支持发送的消息类型"));
986
+ return flushGuild(ctx, contact, grouping, items);
987
+ };
988
+ const buildKeyboard = (grouping) => {
989
+ const rows = [];
990
+ grouping.buttons.forEach((b) => rows.push(...karinToQQBot(b)));
991
+ grouping.keyboards.forEach((k) => rows.push(...karinToQQBot(k)));
992
+ if (!rows.length) return void 0;
993
+ return { content: { rows } };
994
+ };
995
+ const flushGuild = async (ctx, contact, grouping, items) => {
996
+ const result = ctx.initSendMsgResults();
997
+ const passive = buildPassiveGuild(grouping);
998
+ const send = contact.scene === "guild" ? (peer, subPeer, item) => ctx.super.messages.sendChannelMsg(subPeer, item) : (peer, _subPeer, item) => ctx.super.messages.sendDmsMsg(peer, item);
999
+ let replyHandled = false;
1000
+ for (const item of items) {
1001
+ passive(item);
1002
+ if (!replyHandled && grouping.reply.messageId && !(item instanceof FormData)) {
1003
+ item.message_reference = { message_id: grouping.reply.messageId };
1004
+ replyHandled = true;
1005
+ }
1006
+ const res = await send(contact.peer, contact.subPeer, item);
1007
+ result.rawData.push(res);
1008
+ }
1009
+ return ctx.handleResponse(result);
1010
+ };
1011
+ const buildPassiveGuild = (grouping) => {
1012
+ if (!grouping.pasmsg.id) return (_item) => void 0;
1013
+ if (grouping.pasmsg.type === "event") return (item) => {
1014
+ if (item instanceof FormData) item.append("event_id", grouping.pasmsg.id);
1015
+ else item.event_id = grouping.pasmsg.id;
1016
+ };
1017
+ return (item) => {
1018
+ if (item instanceof FormData) item.append("msg_id", grouping.pasmsg.id);
1019
+ else item.msg_id = grouping.pasmsg.id;
1020
+ };
1021
+ };
1022
+
1023
+ //#endregion
1024
+ //#region src/core/adapter/base.ts
1025
+ /**
1026
+ * QQ Official Bot 适配器
1027
+ *
1028
+ * 全场景统一走 msg_type=2 Markdown 通道(详见 pipeline-qq / pipeline-guild)。
1029
+ * 视频 / 语音 / 文件由 pipeline 内部以 msg_type=7 紧随主消息补发。
1030
+ */
1031
+ var AdapterQQBot = class extends AdapterBase {
1032
+ /** 与官方 API 交互 */
1033
+ super;
1034
+ /** 当前 bot 配置 */
1035
+ cfg;
1036
+ /** openId / member_openid -> 昵称 缓存 */
1037
+ nicknameCache = /* @__PURE__ */ new Map();
1038
+ constructor(cfg, api) {
1039
+ super();
1040
+ this.cfg = cfg;
1041
+ this.super = api;
1042
+ this.adapter.name = "QQ Official Bot";
1043
+ this.adapter.protocol = "qqbot";
1044
+ this.adapter.platform = "qq";
1045
+ this.adapter.standard = "other";
1046
+ }
1047
+ logger(level, ...args) {
1048
+ logger.bot(level, this.selfId, ...args);
1049
+ }
1050
+ /**
1051
+ * 主消息回复入口(karin 调用)
1052
+ */
1053
+ async srcReply(e, elements) {
1054
+ const extra = this.cfg.keyboard.enable ? await buttonHandle(e.msg, { e }) : [];
1055
+ return this.sendMsg(e.contact, [...elements, ...extra]);
1056
+ }
1057
+ /**
1058
+ * 发送消息
1059
+ */
1060
+ async sendMsg(contact, elements, _retryCount) {
1061
+ if (contact.scene === "direct" || contact.scene === "guild") return sendGuild(this, contact, elements);
1062
+ if (contact.scene === "group" || contact.scene === "friend") return sendQQ(this, contact, elements);
1063
+ throw new Error(`不支持的消息场景: ${contact.scene}`);
1064
+ }
1065
+ /**
1066
+ * 撤回消息
1067
+ */
1068
+ async recallMsg(contact, messageId) {
1069
+ try {
1070
+ if (contact.scene === "friend") await this.super.messages.recall("user", contact.peer, messageId);
1071
+ else if (contact.scene === "group") await this.super.messages.recall("group", contact.peer, messageId);
1072
+ else if (contact.scene === "direct") await this.super.messages.recall("dms", contact.peer, messageId);
1073
+ else if (contact.scene === "guild") await this.super.messages.recall("channels", contact.peer, messageId);
1074
+ } catch (err) {
1075
+ logger.error("撤回消息失败:", err);
1076
+ }
1077
+ }
1078
+ /**
1079
+ * 构造空发送结果,pipeline 内填充 rawData
1080
+ */
1081
+ initSendMsgResults() {
1082
+ return {
1083
+ messageId: "",
1084
+ message_id: "",
1085
+ time: 0,
1086
+ messageTime: 0,
1087
+ rawData: []
1088
+ };
1089
+ }
1090
+ /**
1091
+ * 将 rawData[0].id / timestamp 同步到顶层字段
1092
+ */
1093
+ handleResponse(data) {
1094
+ const first = data.rawData[0];
1095
+ if (!first) return data;
1096
+ const { id, timestamp } = first;
1097
+ data.messageId = id;
1098
+ data.message_id = id;
1099
+ data.time = typeof timestamp === "string" ? new Date(timestamp).getTime() : timestamp;
1100
+ data.messageTime = data.time;
1101
+ return data;
1102
+ }
1103
+ /**
1104
+ * 发送长消息
1105
+ */
1106
+ async sendLongMsg(_contact, _resId) {
1107
+ this.logger("warn", "[sendLongMsg] QQ Official Bot API 暂不支持长消息发送");
1108
+ return this.initSendMsgResults();
1109
+ }
1110
+ /**
1111
+ * 发送合并转发消息
1112
+ *
1113
+ * 将 NodeElement 拆解为文本 + 图片等元素,拼接成 markdown 发送。
1114
+ * 节点之间仅用换行隔开,不添加任何前缀提示文本。
1115
+ */
1116
+ async sendForwardMsg(contact, elements, _options) {
1117
+ const parts = [];
1118
+ const mediaElements = [];
1119
+ for (const node of elements) {
1120
+ if (node.subType === "messageID") continue;
1121
+ const lines = [];
1122
+ for (const el of node.message) {
1123
+ if (typeof el === "string") {
1124
+ lines.push(el);
1125
+ continue;
1126
+ }
1127
+ switch (el.type) {
1128
+ case "text":
1129
+ lines.push(el.text);
1130
+ break;
1131
+ case "image":
1132
+ if (el.file.startsWith("http")) try {
1133
+ const { width, height } = await getImageSize(el.file);
1134
+ lines.push(`![${el.name || "图片"} #${width}px #${height}px](${el.file})`);
1135
+ } catch {
1136
+ lines.push(`![${el.name || "图片"} #300px #300px](${el.file})`);
1137
+ }
1138
+ else try {
1139
+ const { url, width, height } = await fileToUrl("image", el.file, el.name || "image.jpg");
1140
+ lines.push(`![${el.name || "图片"} #${width}px #${height}px](${url})`);
1141
+ } catch {
1142
+ lines.push(`![${el.name || "图片"} #300px #300px]()`);
1143
+ }
1144
+ break;
1145
+ case "at":
1146
+ if (contact.scene === "friend" || contact.scene === "direct") break;
1147
+ if (el.targetId === "all") lines.push("<qqbot-at-everyone />");
1148
+ else lines.push(`<qqbot-at-user id="${el.targetId}" />`);
1149
+ break;
1150
+ case "markdown":
1151
+ lines.push(el.markdown);
1152
+ break;
1153
+ case "video":
1154
+ case "record":
1155
+ case "file":
1156
+ mediaElements.push(el);
1157
+ break;
1158
+ case "face":
1159
+ case "reply":
1160
+ case "basketball":
1161
+ case "dice":
1162
+ case "rps": break;
1163
+ default: break;
1164
+ }
1165
+ }
1166
+ const block = lines.join("\n");
1167
+ if (block) parts.push(block);
1168
+ }
1169
+ const content = parts.join("\r\r\n\n");
1170
+ const result = await this.sendMsg(contact, [segment.markdown(content)]);
1171
+ if (mediaElements.length) await this.sendMsg(contact, mediaElements);
1172
+ return {
1173
+ messageId: result.messageId,
1174
+ forwardId: ""
1175
+ };
1176
+ }
1177
+ /**
1178
+ * 获取头像 url
1179
+ */
1180
+ async getAvatarUrl(_userId, _size = 0) {
1181
+ return `https://thirdqq.qlogo.cn/qqapp/${this.cfg.appId}/${_userId}/${_size}`;
1182
+ }
1183
+ /**
1184
+ * 获取群头像 url
1185
+ */
1186
+ async getGroupAvatarUrl(_groupId, _size = 0, _history) {
1187
+ return `https://p.qlogo.cn/gh/${_groupId}/${_groupId}/${_size || 100}`;
1188
+ }
1189
+ /**
1190
+ * 获取消息
1191
+ */
1192
+ async getMsg(_contact, _messageId) {
1193
+ this.logger("warn", "[getMsg] QQ Official Bot API 暂不支持获取消息内容");
1194
+ return {
1195
+ time: 0,
1196
+ messageId: _messageId || "",
1197
+ messageSeq: 0,
1198
+ contact: typeof _contact === "string" ? {
1199
+ scene: "group",
1200
+ peer: _contact,
1201
+ subPeer: "",
1202
+ name: ""
1203
+ } : _contact,
1204
+ sender: {
1205
+ userId: "",
1206
+ nick: "",
1207
+ name: "",
1208
+ role: "member"
1209
+ },
1210
+ elements: []
1211
+ };
1212
+ }
1213
+ /** @internal */
1214
+ async getHistoryMsg(_contact, _start, _count) {
1215
+ this.logger("warn", "[getHistoryMsg] QQ Official Bot API 暂不支持获取历史消息");
1216
+ return [];
1217
+ }
1218
+ /**
1219
+ * 获取合并转发消息
1220
+ */
1221
+ async getForwardMsg(_resId) {
1222
+ this.logger("warn", "[getForwardMsg] QQ Official Bot API 暂不支持获取合并转发消息");
1223
+ return [];
1224
+ }
1225
+ /**
1226
+ * 获取精华消息
1227
+ */
1228
+ async getGroupHighlights(_groupId, _page, _pageSize) {
1229
+ this.logger("warn", "[getGroupHighlights] QQ Official Bot API 暂不支持获取精华消息");
1230
+ return [];
1231
+ }
1232
+ /**
1233
+ * 构造资源 ID
1234
+ */
1235
+ async createResId(_contact, _elements) {
1236
+ this.logger("warn", "[createResId] QQ Official Bot API 暂不支持构造资源 ID");
1237
+ return "";
1238
+ }
1239
+ /**
1240
+ * 设置、取消群精华消息
1241
+ */
1242
+ async setGroupHighlights(_groupId, _messageId, _create) {
1243
+ this.logger("warn", "[setGroupHighlights] QQ Official Bot API 暂不支持设置精华消息");
1244
+ }
1245
+ /**
1246
+ * 发送好友赞
1247
+ */
1248
+ async sendLike(_targetId, _count) {
1249
+ this.logger("warn", "[sendLike] QQ Official Bot API 暂不支持发送好友赞");
1250
+ }
1251
+ /**
1252
+ * 群踢人
1253
+ */
1254
+ async groupKickMember(_groupId, _targetId, _rejectAddRequest, _kickReason) {
1255
+ this.logger("warn", "[groupKickMember] QQ Official Bot API 暂不支持群踢人");
1256
+ }
1257
+ /**
1258
+ * 禁言群成员
1259
+ */
1260
+ async setGroupMute(_groupId, _targetId, _duration) {
1261
+ this.logger("warn", "[setGroupMute] QQ Official Bot API 暂不支持禁言群成员");
1262
+ }
1263
+ /**
1264
+ * 群全员禁言
1265
+ */
1266
+ async setGroupAllMute(_groupId, _isBan) {
1267
+ this.logger("warn", "[setGroupAllMute] QQ Official Bot API 暂不支持群全员禁言");
1268
+ }
1269
+ /**
1270
+ * 设置群管理员
1271
+ */
1272
+ async setGroupAdmin(_groupId, _targetId, _isAdmin) {
1273
+ this.logger("warn", "[setGroupAdmin] QQ Official Bot API 暂不支持设置群管理员");
1274
+ }
1275
+ /**
1276
+ * 设置群名片
1277
+ */
1278
+ async setGroupMemberCard(_groupId, _targetId, _card) {
1279
+ this.logger("warn", "[setGroupMemberCard] QQ Official Bot API 暂不支持设置群名片");
1280
+ }
1281
+ /**
1282
+ * 设置群名
1283
+ */
1284
+ async setGroupName(_groupId, _groupName) {
1285
+ this.logger("warn", "[setGroupName] QQ Official Bot API 暂不支持设置群名");
1286
+ }
1287
+ /**
1288
+ * 退出群组
1289
+ */
1290
+ async setGroupQuit(_groupId, _isDismiss) {
1291
+ this.logger("warn", "[setGroupQuit] QQ Official Bot API 暂不支持退出群组");
1292
+ }
1293
+ /**
1294
+ * 设置群专属头衔
1295
+ */
1296
+ async setGroupMemberTitle(_groupId, _targetId, _title) {
1297
+ this.logger("warn", "[setGroupMemberTitle] QQ Official Bot API 暂不支持设置群专属头衔");
1298
+ }
1299
+ /**
1300
+ * 获取陌生人信息
1301
+ */
1302
+ async getStrangerInfo(_targetId) {
1303
+ this.logger("warn", "[getStrangerInfo] QQ Official Bot API 暂不支持获取陌生人信息");
1304
+ return {
1305
+ userId: _targetId,
1306
+ nick: ""
1307
+ };
1308
+ }
1309
+ /**
1310
+ * 获取好友列表
1311
+ */
1312
+ async getFriendList(_refresh) {
1313
+ this.logger("warn", "[getFriendList] QQ Official Bot API 暂不支持获取好友列表");
1314
+ return [];
1315
+ }
1316
+ /**
1317
+ * 获取群信息
1318
+ */
1319
+ async getGroupInfo(_groupId, _noCache) {
1320
+ this.logger("warn", "[getGroupInfo] QQ Official Bot API 暂不支持获取群信息");
1321
+ return {
1322
+ groupId: _groupId,
1323
+ groupName: "",
1324
+ owner: "",
1325
+ groupRemark: "",
1326
+ admins: [],
1327
+ maxMemberCount: 0,
1328
+ memberCount: 0,
1329
+ groupDesc: "",
1330
+ avatar: ""
1331
+ };
1332
+ }
1333
+ /**
1334
+ * 获取群列表
1335
+ */
1336
+ async getGroupList(_refresh) {
1337
+ this.logger("warn", "[getGroupList] QQ Official Bot API 暂不支持获取群列表");
1338
+ return [];
1339
+ }
1340
+ /**
1341
+ * 获取群成员信息
1342
+ */
1343
+ async getGroupMemberInfo(_groupId, _targetId, _refresh) {
1344
+ this.logger("warn", "[getGroupMemberInfo] QQ Official Bot API 暂不支持获取群成员信息");
1345
+ return {
1346
+ userId: _targetId,
1347
+ role: "member"
1348
+ };
1349
+ }
1350
+ /**
1351
+ * 获取群成员列表
1352
+ */
1353
+ async getGroupMemberList(_groupId, _refresh) {
1354
+ this.logger("warn", "[getGroupMemberList] QQ Official Bot API 暂不支持获取群成员列表");
1355
+ return [];
1356
+ }
1357
+ /**
1358
+ * 获取群荣誉信息
1359
+ */
1360
+ async getGroupHonor(_groupId) {
1361
+ this.logger("warn", "[getGroupHonor] QQ Official Bot API 暂不支持获取群荣誉信息");
1362
+ return [];
1363
+ }
1364
+ /**
1365
+ * 设置好友请求结果
1366
+ */
1367
+ async setFriendApplyResult(_requestId, _isApprove, _remark) {
1368
+ this.logger("warn", "[setFriendApplyResult] QQ Official Bot API 暂不支持设置好友请求结果");
1369
+ }
1370
+ /**
1371
+ * 设置申请加入群请求结果
1372
+ */
1373
+ async setGroupApplyResult(_requestId, _isApprove, _denyReason) {
1374
+ this.logger("warn", "[setGroupApplyResult] QQ Official Bot API 暂不支持设置群申请结果");
1375
+ }
1376
+ /**
1377
+ * 设置邀请加入群请求结果
1378
+ */
1379
+ async setInvitedJoinGroupResult(_requestId, _isApprove) {
1380
+ this.logger("warn", "[setInvitedJoinGroupResult] QQ Official Bot API 暂不支持设置邀请入群结果");
1381
+ }
1382
+ /**
1383
+ * 设置消息表情回应
1384
+ */
1385
+ async setMsgReaction(_contact, _messageId, _faceId, _isSet) {
1386
+ this.logger("warn", "[setMsgReaction] QQ Official Bot API 暂不支持设置消息表情回应");
1387
+ }
1388
+ /**
1389
+ * 上传群文件、私聊文件
1390
+ */
1391
+ async uploadFile(_contact, _file, _name, _folder) {
1392
+ this.logger("warn", "[uploadFile] QQ Official Bot API 暂不支持上传文件");
1393
+ }
1394
+ /**
1395
+ * 下载文件到协议端本地
1396
+ */
1397
+ async downloadFile(_options) {
1398
+ this.logger("warn", "[downloadFile] QQ Official Bot API 暂不支持下载文件到本地");
1399
+ return { filePath: "" };
1400
+ }
1401
+ /**
1402
+ * 创建群文件夹
1403
+ */
1404
+ async createGroupFolder(_groupId, _name) {
1405
+ this.logger("warn", "[createGroupFolder] QQ Official Bot API 暂不支持创建群文件夹");
1406
+ return {
1407
+ id: "",
1408
+ usedSpace: "0"
1409
+ };
1410
+ }
1411
+ /**
1412
+ * 重命名群文件夹
1413
+ */
1414
+ async renameGroupFolder(_groupId, _folderId, _name) {
1415
+ this.logger("warn", "[renameGroupFolder] QQ Official Bot API 暂不支持重命名群文件夹");
1416
+ return false;
1417
+ }
1418
+ /**
1419
+ * 删除群文件夹
1420
+ */
1421
+ async delGroupFolder(_groupId, _folderId) {
1422
+ this.logger("warn", "[delGroupFolder] QQ Official Bot API 暂不支持删除群文件夹");
1423
+ return false;
1424
+ }
1425
+ /**
1426
+ * 上传群文件
1427
+ */
1428
+ async uploadGroupFile(_groupId, _file, _name) {
1429
+ this.logger("warn", "[uploadGroupFile] QQ Official Bot API 暂不支持上传群文件");
1430
+ return false;
1431
+ }
1432
+ /**
1433
+ * 获取文件 url
1434
+ */
1435
+ async getFileUrl(_contact, _fileId) {
1436
+ this.logger("warn", "[getFileUrl] QQ Official Bot API 暂不支持获取文件 url");
1437
+ return "";
1438
+ }
1439
+ /**
1440
+ * 删除群文件
1441
+ */
1442
+ async delGroupFile(_groupId, _fileId, _busId) {
1443
+ this.logger("warn", "[delGroupFile] QQ Official Bot API 暂不支持删除群文件");
1444
+ return false;
1445
+ }
1446
+ /**
1447
+ * 获取群文件系统信息
1448
+ */
1449
+ async getGroupFileSystemInfo(_groupId) {
1450
+ this.logger("warn", "[getGroupFileSystemInfo] QQ Official Bot API 暂不支持获取群文件系统信息");
1451
+ return {
1452
+ fileCount: 0,
1453
+ limitCount: 0,
1454
+ usedSpace: 0,
1455
+ totalSpace: 0
1456
+ };
1457
+ }
1458
+ /**
1459
+ * 获取群文件夹下文件列表
1460
+ */
1461
+ async getGroupFileList(_groupId, _folderId) {
1462
+ this.logger("warn", "[getGroupFileList] QQ Official Bot API 暂不支持获取群文件列表");
1463
+ return {
1464
+ files: [],
1465
+ folders: []
1466
+ };
1467
+ }
1468
+ /**
1469
+ * 设置群备注
1470
+ */
1471
+ async setGroupRemark(_groupId, _remark) {
1472
+ this.logger("warn", "[setGroupRemark] QQ Official Bot API 暂不支持设置群备注");
1473
+ return false;
1474
+ }
1475
+ /**
1476
+ * 获取陌生群信息
1477
+ */
1478
+ async getNotJoinedGroupInfo(_groupId) {
1479
+ this.logger("warn", "[getNotJoinedGroupInfo] QQ Official Bot API 暂不支持获取陌生群信息");
1480
+ return {
1481
+ groupId: _groupId,
1482
+ groupName: "",
1483
+ owner: "",
1484
+ groupRemark: "",
1485
+ admins: [],
1486
+ maxMemberCount: 0,
1487
+ memberCount: 0,
1488
+ groupDesc: "",
1489
+ avatar: ""
1490
+ };
1491
+ }
1492
+ /**
1493
+ * 获取艾特全体成员剩余次数
1494
+ */
1495
+ async getAtAllCount(_groupId) {
1496
+ this.logger("warn", "[getAtAllCount] QQ Official Bot API 暂不支持获取艾特全体成员剩余次数");
1497
+ return {
1498
+ accessAtAll: false,
1499
+ groupRemainCount: 0,
1500
+ userRremainCount: 0
1501
+ };
1502
+ }
1503
+ /**
1504
+ * 获取群被禁言用户列表
1505
+ */
1506
+ async getGroupMuteList(_groupId) {
1507
+ this.logger("warn", "[getGroupMuteList] QQ Official Bot API 暂不支持获取群被禁言用户列表");
1508
+ return [];
1509
+ }
1510
+ /**
1511
+ * 戳一戳用户
1512
+ */
1513
+ async pokeUser(_contact, _targetId, _count) {
1514
+ this.logger("warn", "[pokeUser] QQ Official Bot API 暂不支持戳一戳用户");
1515
+ return false;
1516
+ }
1517
+ /**
1518
+ * 获取 Cookies
1519
+ */
1520
+ async getCookies(_domain) {
1521
+ this.logger("warn", "[getCookies] QQ Official Bot API 暂不支持获取 Cookies");
1522
+ return { cookie: "" };
1523
+ }
1524
+ /**
1525
+ * 获取 QQ 相关接口凭证
1526
+ */
1527
+ async getCredentials(_domain) {
1528
+ this.logger("warn", "[getCredentials] QQ Official Bot API 暂不支持获取 Credentials");
1529
+ return {
1530
+ cookies: "",
1531
+ csrf_token: 0
1532
+ };
1533
+ }
1534
+ /**
1535
+ * 获取 CSRF Token
1536
+ */
1537
+ async getCSRFToken() {
1538
+ this.logger("warn", "[getCSRFToken] QQ Official Bot API 暂不支持获取 CSRF Token");
1539
+ return { token: 0 };
1540
+ }
1541
+ /**
1542
+ * 设置头像
1543
+ */
1544
+ async setAvatar(_file) {
1545
+ this.logger("warn", "[setAvatar] QQ Official Bot API 暂不支持设置头像");
1546
+ }
1547
+ /**
1548
+ * 获取 rkey
1549
+ */
1550
+ async getRkey() {
1551
+ this.logger("warn", "[getRkey] QQ Official Bot API 暂不支持获取 rkey");
1552
+ return [];
1553
+ }
1554
+ /**
1555
+ * 获取群 Ai 语音可用声色列表
1556
+ */
1557
+ async getAiCharacters() {
1558
+ this.logger("warn", "[getAiCharacters] QQ Official Bot API 暂不支持获取群 Ai 语音可用声色列表");
1559
+ return [];
1560
+ }
1561
+ /**
1562
+ * 设置群 Ai 语音声色
1563
+ */
1564
+ async sendAiCharacter(_groupId, _characterId, _text) {
1565
+ this.logger("warn", "[sendAiCharacter] QQ Official Bot API 暂不支持设置群 Ai 语音声色");
1566
+ return { messageId: "" };
1567
+ }
1568
+ };
1569
+
1570
+ //#endregion
1571
+ //#region src/core/event/conver.ts
1572
+ /** 反转义 HTML 实体 */
1573
+ const unescape = (text) => text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
1574
+ /**
1575
+ * QQ Bot 报文 → karin elements
1576
+ *
1577
+ * @param appid 应用 ID(与机器人 sub_id 同一值会被替换为 appid)
1578
+ * @param ev 事件
1579
+ * @param selfSubId 机器人 user id(非 appid)
1580
+ * @param options
1581
+ */
1582
+ const convertToKarin = (appid, ev, selfSubId, options = {}) => {
1583
+ const elements = [];
1584
+ const data = ev.d;
1585
+ /** mentions 索引 id -> displayName */
1586
+ const mentionMap = /* @__PURE__ */ new Map();
1587
+ if (ev.t === "MESSAGE_CREATE" || ev.t === "AT_MESSAGE_CREATE") ev.d.mentions?.forEach((v) => {
1588
+ mentionMap.set(v.id, v.username);
1589
+ });
1590
+ else if (ev.t === "GROUP_MESSAGE_CREATE" || ev.t === "GROUP_AT_MESSAGE_CREATE") ev.d.mentions?.forEach((v) => {
1591
+ mentionMap.set(v.id, v.username);
1592
+ mentionMap.set(v.member_openid, v.username);
1593
+ });
1594
+ /** 附件 */
1595
+ for (const v of data.attachments || []) {
1596
+ const url = v.url.startsWith("http") ? v.url : `https://${v.url}`;
1597
+ if (v.content_type.startsWith("image/")) elements.push(segment.image(url, {
1598
+ subType: v.content_type.split("/")[1],
1599
+ name: v.filename,
1600
+ width: v.width,
1601
+ height: v.height
1602
+ }));
1603
+ else if (v.content_type === "video/mp4") elements.push(segment.video(url));
1604
+ else if (v.content_type === "voice") elements.push(segment.record(v.voice_wav_url || url));
1605
+ else if (v.content_type === "file") elements.push(segment.json(JSON.stringify({
1606
+ type: "file",
1607
+ attachment: v
1608
+ })));
1609
+ }
1610
+ const tokens = data?.content?.match(/<faceType=\d+,faceId="\d+",ext="[^"]+">|<@!\d+>|<qqbot-at-user id="[^"]+"\s*\/?>|<qqbot-at-everyone\s*\/?>|[^<]+/g) || [];
1611
+ const cfg = getConfig(appid);
1612
+ for (const tok of tokens) {
1613
+ if (tok.startsWith("<faceType=")) {
1614
+ const m = tok.match(/faceId="(\d+)"/);
1615
+ if (m) elements.push(segment.face(Number(m[1])));
1616
+ continue;
1617
+ }
1618
+ if (tok.startsWith("<@!")) {
1619
+ const id = tok.replace(/^<@!|>$/g, "");
1620
+ const name = mentionMap.get(id) || "";
1621
+ elements.push(segment.at(id === selfSubId ? appid : id, name));
1622
+ continue;
1623
+ }
1624
+ if (tok.startsWith("<qqbot-at-user")) {
1625
+ const id = tok.match(/id="([^"]+)"/)?.[1] || "";
1626
+ const name = mentionMap.get(id) || "";
1627
+ elements.push(segment.at(id === selfSubId ? appid : id, name));
1628
+ continue;
1629
+ }
1630
+ if (tok.startsWith("<qqbot-at-everyone")) {
1631
+ elements.push(segment.at("all"));
1632
+ continue;
1633
+ }
1634
+ let text = unescape(tok);
1635
+ if (cfg?.regex) for (const r of cfg.regex) {
1636
+ const reg = r.reg instanceof RegExp ? r.reg : new RegExp(r.reg);
1637
+ text = text.trim().replace(reg, r.rep);
1638
+ }
1639
+ elements.push(segment.text(text));
1640
+ }
1641
+ /** 群消息是否需要在头部补 self at */
1642
+ if (ev.t === "GROUP_AT_MESSAGE_CREATE" || ev.t === "GROUP_MESSAGE_CREATE") {
1643
+ const youMention = (ev.d.mentions || []).find((m) => m.is_you);
1644
+ if (options.forceAtSelf || !!youMention) {
1645
+ if (!elements.some((e) => e.type === "at" && (e.targetId === appid || e.targetId === selfSubId))) elements.unshift(segment.at(appid, youMention?.username || ""));
1646
+ }
1647
+ }
1648
+ return elements;
1649
+ };
1650
+
1651
+ //#endregion
1652
+ //#region src/core/event/message.ts
1653
+ /**
1654
+ * 群消息(@机器人 / 普通消息合并入口)
1655
+ */
1656
+ const onGroupMsg = (client, ev, opts) => {
1657
+ const selfId = client.selfId;
1658
+ const userId = ev.d.author.member_openid || ev.d.author.id;
1659
+ const username = ev.d.author.username || "";
1660
+ if (userId && username) client.nicknameCache.set(userId, username);
1661
+ ev.d.mentions?.forEach((m) => {
1662
+ if (m.member_openid && m.username) client.nicknameCache.set(m.member_openid, m.username);
1663
+ });
1664
+ const contact = karin.contactGroup(ev.d.group_openid || ev.d.group_id);
1665
+ const sender = karin.groupSender(userId, "member", username);
1666
+ const e = createGroupMessage({
1667
+ bot: client,
1668
+ elements: convertToKarin(selfId, ev, client.selfSubId("id"), { forceAtSelf: opts.forceAtSelf }),
1669
+ eventId: ev.id,
1670
+ messageId: ev.d.id,
1671
+ messageSeq: 0,
1672
+ rawEvent: ev,
1673
+ time: Number(new Date(ev.d.timestamp).getTime() || ev.d.timestamp),
1674
+ contact,
1675
+ sender,
1676
+ srcReply: (elements) => client.srcReply(e, [...elements, segment.pasmsg(ev.d.id)])
1677
+ });
1678
+ return e;
1679
+ };
1680
+ /**
1681
+ * 单聊消息
1682
+ */
1683
+ const onFriendMsg = (client, ev) => {
1684
+ const selfId = client.selfId;
1685
+ const userId = ev.d.author.user_openid || ev.d.author.id;
1686
+ const username = ev.d.author.username || "";
1687
+ if (userId && username) client.nicknameCache.set(userId, username);
1688
+ const contact = karin.contactFriend(userId);
1689
+ const sender = karin.friendSender(userId, username);
1690
+ const e = createFriendMessage({
1691
+ bot: client,
1692
+ elements: convertToKarin(selfId, ev, client.selfSubId("id")),
1693
+ eventId: ev.id,
1694
+ messageId: ev.d.id,
1695
+ messageSeq: 0,
1696
+ rawEvent: ev,
1697
+ time: Number(new Date(ev.d.timestamp).getTime() || ev.d.timestamp),
1698
+ contact,
1699
+ sender,
1700
+ srcReply: (elements) => client.srcReply(e, [...elements, segment.pasmsg(ev.d.id)])
1701
+ });
1702
+ return e;
1703
+ };
1704
+ /**
1705
+ * 频道消息
1706
+ */
1707
+ const onChannelMsg = (client, ev) => {
1708
+ const selfId = client.selfId;
1709
+ const userId = ev.d.author.id;
1710
+ const contact = karin.contact("guild", ev.d.guild_id, ev.d.channel_id);
1711
+ const sender = karin.groupSender(userId, "unknown", ev.d.author.username || "");
1712
+ const e = createGuildMessage({
1713
+ bot: client,
1714
+ elements: convertToKarin(selfId, ev, client.selfSubId("id")),
1715
+ eventId: ev.id,
1716
+ messageId: ev.d.id,
1717
+ messageSeq: 0,
1718
+ rawEvent: ev,
1719
+ time: Number(new Date(ev.d.timestamp).getTime() || ev.d.timestamp),
1720
+ contact,
1721
+ sender,
1722
+ srcReply: (elements) => client.srcReply(e, [...elements, segment.pasmsg(ev.d.id)])
1723
+ });
1724
+ return e;
1725
+ };
1726
+ /**
1727
+ * 频道私信
1728
+ */
1729
+ const onDirectMsg = (client, ev) => {
1730
+ const selfId = client.selfId;
1731
+ const userId = ev.d.author.id;
1732
+ const contact = karin.contact("direct", ev.d.guild_id, ev.d.channel_id);
1733
+ const sender = karin.friendSender(userId, ev.d.author.username || "");
1734
+ const e = createDirectMessage({
1735
+ bot: client,
1736
+ elements: convertToKarin(selfId, ev, client.selfSubId("id")),
1737
+ eventId: ev.id,
1738
+ messageId: ev.d.id,
1739
+ messageSeq: 0,
1740
+ rawEvent: ev,
1741
+ time: Number(new Date(ev.d.timestamp).getTime() || ev.d.timestamp),
1742
+ contact,
1743
+ sender,
1744
+ srcGuildId: ev.d.src_guild_id,
1745
+ srcReply: (elements) => client.srcReply(e, [...elements, segment.pasmsg(ev.d.id)])
1746
+ });
1747
+ return e;
1748
+ };
1749
+
1750
+ //#endregion
1751
+ //#region src/core/event/notice.ts
1752
+ /**
1753
+ * 机器人加入群聊事件
1754
+ * @param client 机器人实例
1755
+ * @param event 事件
1756
+ */
1757
+ const onGroupAddRobot = (client, event) => {
1758
+ const selfId = client.selfId;
1759
+ const userId = event.d.op_member_openid;
1760
+ const groupId = event.d.group_openid;
1761
+ const contact = karin.contactGroup(groupId);
1762
+ const sender = karin.groupSender(userId, "unknown");
1763
+ const eventId = event.id;
1764
+ const srcReply = (elements) => client.sendMsg(contact, [...elements, segment.pasmsg(eventId, "event")]);
1765
+ createGroupMemberAddNotice({
1766
+ contact,
1767
+ eventId,
1768
+ sender,
1769
+ srcReply,
1770
+ bot: client,
1771
+ rawEvent: event,
1772
+ time: event.d.timestamp,
1773
+ content: {
1774
+ operatorId: userId,
1775
+ targetId: selfId,
1776
+ type: "invite"
1777
+ }
1778
+ });
1779
+ };
1780
+ /**
1781
+ * 机器人退出群聊事件
1782
+ * @param client 机器人实例
1783
+ * @param event 事件
1784
+ */
1785
+ const onGroupDelRobot = (client, event) => {
1786
+ const selfId = client.selfId;
1787
+ const userId = event.d.op_member_openid;
1788
+ const groupId = event.d.group_openid;
1789
+ const contact = karin.contactGroup(groupId);
1790
+ const sender = karin.groupSender(userId, "unknown");
1791
+ const eventId = event.id;
1792
+ const srcReply = (elements) => client.sendMsg(contact, [...elements, segment.pasmsg(eventId, "event")]);
1793
+ createGroupMemberDelNotice({
1794
+ contact,
1795
+ eventId,
1796
+ sender,
1797
+ srcReply,
1798
+ bot: client,
1799
+ rawEvent: event,
1800
+ time: event.d.timestamp,
1801
+ content: {
1802
+ operatorId: userId,
1803
+ targetId: selfId,
1804
+ type: "kick"
1805
+ }
1806
+ });
1807
+ };
1808
+ /**
1809
+ * 群聊接受机器人主动消息
1810
+ * @param client 机器人实例
1811
+ * @param event 事件
1812
+ */
1813
+ const onGroupMsgReceive = (client, event) => {
1814
+ const groupId = event.d.group_openid;
1815
+ const userId = event.d.op_member_openid;
1816
+ client.logger("mark", `群聊 ${groupId} 已允许机器人主动发送消息 操作人: ${userId}`);
1817
+ };
1818
+ /**
1819
+ * 群聊拒绝机器人主动消息
1820
+ * @param client 机器人实例
1821
+ * @param event 事件
1822
+ */
1823
+ const onGroupMsgReject = (client, event) => {
1824
+ const groupId = event.d.group_openid;
1825
+ const userId = event.d.op_member_openid;
1826
+ client.logger("mark", `群聊 ${groupId} 已拒绝机器人主动发送消息 操作人: ${userId}`);
1827
+ };
1828
+ /**
1829
+ * 好友添加事件
1830
+ * @param client 机器人实例
1831
+ * @param event 事件
1832
+ */
1833
+ const onFriendAdd = (client, event) => {
1834
+ const selfId = client.selfId;
1835
+ const userId = event.d.openid;
1836
+ const contact = karin.contactFriend(userId);
1837
+ const sender = karin.friendSender(userId, "");
1838
+ const eventId = event.id;
1839
+ const srcReply = (elements) => client.sendMsg(contact, [...elements, segment.pasmsg(eventId, "event")]);
1840
+ createFriendIncreaseNotice({
1841
+ contact,
1842
+ eventId,
1843
+ sender,
1844
+ srcReply,
1845
+ bot: client,
1846
+ rawEvent: event,
1847
+ time: event.d.timestamp,
1848
+ content: { targetId: selfId }
1849
+ });
1850
+ };
1851
+ /**
1852
+ * 好友删除事件
1853
+ * @param client 机器人实例
1854
+ * @param event 事件
1855
+ */
1856
+ const onFriendDel = (client, event) => {
1857
+ const selfId = client.selfId;
1858
+ const userId = event.d.openid;
1859
+ const contact = karin.contactFriend(userId);
1860
+ const sender = karin.friendSender(userId, "");
1861
+ const eventId = event.id;
1862
+ const srcReply = (elements) => client.sendMsg(contact, [...elements, segment.pasmsg(eventId, "event")]);
1863
+ createFriendDecreaseNotice({
1864
+ contact,
1865
+ eventId,
1866
+ sender,
1867
+ srcReply,
1868
+ bot: client,
1869
+ rawEvent: event,
1870
+ time: event.d.timestamp,
1871
+ content: { targetId: selfId }
1872
+ });
1873
+ };
1874
+ /**
1875
+ * 好友接受机器人主动消息
1876
+ * @param client 机器人实例
1877
+ * @param event 事件
1878
+ */
1879
+ const onC2CMsgReceive = (client, event) => {
1880
+ const userId = event.d.openid;
1881
+ client.logger("mark", `好友 ${userId} 已允许机器人主动发送消息`);
1882
+ };
1883
+ /**
1884
+ * 好友拒绝机器人主动消息
1885
+ * @param client 机器人实例
1886
+ * @param event 事件
1887
+ */
1888
+ const onC2CMsgReject = (client, event) => {
1889
+ const userId = event.d.openid;
1890
+ client.logger("mark", `好友 ${userId} 已拒绝机器人主动发送消息`);
1891
+ };
1892
+
1893
+ //#endregion
1894
+ //#region src/core/event/interaction.ts
1895
+ /**
1896
+ * INTERACTION_CREATE 处理
1897
+ *
1898
+ * - 先 ack 客户端(必须在 3s 内),失败仅 warn
1899
+ * - 借用普通消息事件投递给业务层,业务层通过 e.rawEvent.t === 'INTERACTION_CREATE' 区分
1900
+ * - elements: [self at] + text(button_data) + json(meta) + pasmsg(event_id, 'event')
1901
+ */
1902
+ const onInteraction = (client, ev) => {
1903
+ client.super.interaction.ack(ev.id, 0).catch((err) => {
1904
+ client.logger("warn", "[INTERACTION] ack 失败:", err);
1905
+ });
1906
+ const selfId = client.selfId;
1907
+ const buttonData = ev.d.data?.resolved?.button_data || "";
1908
+ const meta = segment.json(JSON.stringify({
1909
+ tag: "qqbot-button-click",
1910
+ type: ev.d.type,
1911
+ chat_type: ev.d.chat_type,
1912
+ scene: ev.d.scene,
1913
+ button_id: ev.d.data?.resolved?.button_id,
1914
+ button_data: ev.d.data?.resolved?.button_data,
1915
+ feature_id: ev.d.data?.resolved?.feature_id,
1916
+ message_id: ev.d.data?.resolved?.message_id,
1917
+ user_id: ev.d.data?.resolved?.user_id,
1918
+ version: ev.d.version
1919
+ }));
1920
+ const time = Number(new Date(ev.d.timestamp).getTime() || Date.now());
1921
+ if (ev.d.chat_type === 1 && ev.d.group_openid) {
1922
+ const userId = ev.d.group_member_openid || "";
1923
+ const contact = karin.contactGroup(ev.d.group_openid);
1924
+ const sender = karin.groupSender(userId, "unknown");
1925
+ const e = createGroupMessage({
1926
+ bot: client,
1927
+ elements: [
1928
+ segment.at(selfId),
1929
+ segment.text(buttonData),
1930
+ meta
1931
+ ],
1932
+ eventId: ev.id,
1933
+ messageId: ev.id,
1934
+ messageSeq: 0,
1935
+ rawEvent: ev,
1936
+ time,
1937
+ contact,
1938
+ sender,
1939
+ srcReply: (els) => client.srcReply(e, [...els, segment.pasmsg(ev.id, "event")])
1940
+ });
1941
+ return;
1942
+ }
1943
+ if (ev.d.chat_type === 2 && ev.d.user_openid) {
1944
+ const userId = ev.d.user_openid;
1945
+ const contact = karin.contactFriend(userId);
1946
+ const sender = karin.friendSender(userId, "");
1947
+ const e = createFriendMessage({
1948
+ bot: client,
1949
+ elements: [
1950
+ segment.at(selfId),
1951
+ segment.text(buttonData),
1952
+ meta
1953
+ ],
1954
+ eventId: ev.id,
1955
+ messageId: ev.id,
1956
+ messageSeq: 0,
1957
+ rawEvent: ev,
1958
+ time,
1959
+ contact,
1960
+ sender,
1961
+ srcReply: (els) => client.srcReply(e, [...els, segment.pasmsg(ev.id, "event")])
1962
+ });
1963
+ return;
1964
+ }
1965
+ if (ev.d.chat_type === 0 && ev.d.guild_id && ev.d.channel_id) {
1966
+ const userId = ev.d.data?.resolved?.user_id || "";
1967
+ const contact = karin.contact("guild", ev.d.guild_id, ev.d.channel_id);
1968
+ const sender = karin.groupSender(userId, "unknown");
1969
+ const e = createGuildMessage({
1970
+ bot: client,
1971
+ elements: [segment.text(buttonData), meta],
1972
+ eventId: ev.id,
1973
+ messageId: ev.id,
1974
+ messageSeq: 0,
1975
+ rawEvent: ev,
1976
+ time,
1977
+ contact,
1978
+ sender,
1979
+ srcReply: (els) => client.srcReply(e, [...els, segment.pasmsg(ev.id, "event")])
1980
+ });
1981
+ return;
1982
+ }
1983
+ client.logger("debug", `[INTERACTION] 未识别的 chat_type=${ev.d.chat_type}`);
1984
+ };
1985
+
1986
+ //#endregion
1987
+ //#region src/core/event/dispatcher.ts
1988
+ /** 事件名 → 中文标签 */
1989
+ const eventLabel = {
1990
+ ["READY"]: "就绪",
1991
+ ["RESUMED"]: "连接恢复",
1992
+ ["GROUP_AT_MESSAGE_CREATE"]: "群聊@消息",
1993
+ ["GROUP_MESSAGE_CREATE"]: "群聊消息",
1994
+ ["C2C_MESSAGE_CREATE"]: "好友消息",
1995
+ ["MESSAGE_CREATE"]: "频道消息",
1996
+ ["AT_MESSAGE_CREATE"]: "频道@消息",
1997
+ ["DIRECT_MESSAGE_CREATE"]: "频道私信",
1998
+ ["GROUP_ADD_ROBOT"]: "机器人入群",
1999
+ ["GROUP_DEL_ROBOT"]: "机器人退群",
2000
+ ["GROUP_MSG_RECEIVE"]: "群聊开启通知",
2001
+ ["GROUP_MSG_REJECT"]: "群聊关闭通知",
2002
+ ["FRIEND_ADD"]: "添加好友",
2003
+ ["FRIEND_DEL"]: "删除好友",
2004
+ ["C2C_MSG_RECEIVE"]: "单聊开启通知",
2005
+ ["C2C_MSG_REJECT"]: "单聊关闭通知",
2006
+ ["INTERACTION_CREATE"]: "互动事件",
2007
+ ["MESSAGE_AUDIT_PASS"]: "消息审核通过",
2008
+ ["MESSAGE_AUDIT_REJECT"]: "消息审核不通过"
2009
+ };
2010
+ /**
2011
+ * 事件分发
2012
+ */
2013
+ const dispatch$1 = (client, ev) => {
2014
+ const label = eventLabel[ev.t] || ev.t;
2015
+ client.logger("debug", `[事件][${label}]`);
2016
+ log("debug", `${client.selfId}: dispatcher routing t=${ev.t} -> ${label}`);
2017
+ switch (ev.t) {
2018
+ case "READY": return client.logger("info", `WebSocket 就绪: ${ev.d?.user?.username || ""}`);
2019
+ case "RESUMED": return client.logger("info", "WebSocket 连接已恢复");
2020
+ case "GROUP_AT_MESSAGE_CREATE":
2021
+ onGroupMsg(client, ev, { forceAtSelf: true });
2022
+ return;
2023
+ case "GROUP_MESSAGE_CREATE":
2024
+ onGroupMsg(client, ev, { forceAtSelf: false });
2025
+ return;
2026
+ case "C2C_MESSAGE_CREATE":
2027
+ onFriendMsg(client, ev);
2028
+ return;
2029
+ case "MESSAGE_CREATE":
2030
+ case "AT_MESSAGE_CREATE":
2031
+ onChannelMsg(client, ev);
2032
+ return;
2033
+ case "DIRECT_MESSAGE_CREATE":
2034
+ onDirectMsg(client, ev);
2035
+ return;
2036
+ case "GROUP_ADD_ROBOT":
2037
+ onGroupAddRobot(client, ev);
2038
+ return;
2039
+ case "GROUP_DEL_ROBOT":
2040
+ onGroupDelRobot(client, ev);
2041
+ return;
2042
+ case "GROUP_MSG_RECEIVE":
2043
+ onGroupMsgReceive(client, ev);
2044
+ return;
2045
+ case "GROUP_MSG_REJECT":
2046
+ onGroupMsgReject(client, ev);
2047
+ return;
2048
+ case "FRIEND_ADD":
2049
+ onFriendAdd(client, ev);
2050
+ return;
2051
+ case "FRIEND_DEL":
2052
+ onFriendDel(client, ev);
2053
+ return;
2054
+ case "C2C_MSG_RECEIVE":
2055
+ onC2CMsgReceive(client, ev);
2056
+ return;
2057
+ case "C2C_MSG_REJECT":
2058
+ onC2CMsgReject(client, ev);
2059
+ return;
2060
+ case "INTERACTION_CREATE":
2061
+ onInteraction(client, ev);
2062
+ return;
2063
+ case "MESSAGE_AUDIT_PASS": return client.logger("debug", `消息审核通过: audit_id=${ev.d.audit_id}`);
2064
+ case "MESSAGE_AUDIT_REJECT": return client.logger("debug", `消息审核不通过: audit_id=${ev.d.audit_id}`);
2065
+ default: logger.debug(`[QQ Official Bot] 未处理事件: ${ev.t}`);
2066
+ }
2067
+ };
2068
+
2069
+ //#endregion
2070
+ //#region src/connection/transport.ts
2071
+ /**
2072
+ * 全局事件总线
2073
+ * 每个 appId 一个 channel,业务层(dispatcher)通过 on(appId, handler) 订阅
2074
+ */
2075
+ const bus = new EventEmitter();
2076
+ bus.setMaxListeners(0);
2077
+ /**
2078
+ * 推送一条事件到业务层
2079
+ */
2080
+ const dispatch = (appId, ev) => {
2081
+ const n = bus.listenerCount(appId);
2082
+ log("debug", `${appId}: bus.dispatch t=${ev.t} listeners=${n}`);
2083
+ if (n === 0) log("warn", `${appId}: 收到事件 ${ev.t} 但没有监听器(dispatcher 未注册或已被移除)`);
2084
+ bus.emit(appId, ev);
2085
+ };
2086
+ /**
2087
+ * 注销某个 appId 的所有监听器
2088
+ * 重连 / 销毁 bot 时调用
2089
+ */
2090
+ const offAll = (appId) => {
2091
+ bus.removeAllListeners(appId);
2092
+ };
2093
+
2094
+ //#endregion
2095
+ //#region src/connection/ws/error.ts
2096
+ /** WebSocket 错误码精确映射 */
2097
+ const wsErrorMap = {
2098
+ 4001: {
2099
+ message: "无效的 opcode",
2100
+ canResume: false,
2101
+ canIdentify: false
2102
+ },
2103
+ 4002: {
2104
+ message: "无效的 payload",
2105
+ canResume: false,
2106
+ canIdentify: false
2107
+ },
2108
+ 4007: {
2109
+ message: "seq 错误",
2110
+ canResume: false,
2111
+ canIdentify: true
2112
+ },
2113
+ 4006: {
2114
+ message: "无效的 session id,无法继续 resume,请 identify",
2115
+ canResume: false,
2116
+ canIdentify: true
2117
+ },
2118
+ 4008: {
2119
+ message: "发送 payload 过快,请重新连接,并遵守连接后返回的频控信息",
2120
+ canResume: true,
2121
+ canIdentify: true
2122
+ },
2123
+ 4009: {
2124
+ message: "连接过期,请重连并执行 resume 进行重新连接",
2125
+ canResume: true,
2126
+ canIdentify: true
2127
+ },
2128
+ 4010: {
2129
+ message: "无效的 shard",
2130
+ canResume: false,
2131
+ canIdentify: false
2132
+ },
2133
+ 4011: {
2134
+ message: "连接需要处理的 guild 过多,请进行合理的分片",
2135
+ canResume: false,
2136
+ canIdentify: false
2137
+ },
2138
+ 4012: {
2139
+ message: "无效的 version",
2140
+ canResume: false,
2141
+ canIdentify: false
2142
+ },
2143
+ 4013: {
2144
+ message: "无效的 intent",
2145
+ canResume: false,
2146
+ canIdentify: false
2147
+ },
2148
+ 4014: {
2149
+ message: "intent 无权限",
2150
+ canResume: false,
2151
+ canIdentify: false
2152
+ },
2153
+ 4914: {
2154
+ message: "机器人已下架,只允许连接沙箱环境,请断开连接,检验当前连接环境",
2155
+ canResume: false,
2156
+ canIdentify: false
2157
+ },
2158
+ 4915: {
2159
+ message: "机器人已封禁,不允许连接,请断开连接,申请解封后再连接",
2160
+ canResume: false,
2161
+ canIdentify: false
2162
+ }
2163
+ };
2164
+ /**
2165
+ * 获取 WebSocket 错误码信息
2166
+ * @param code - WebSocket close code
2167
+ * @returns 错误信息,未找到则返回 undefined
2168
+ */
2169
+ const getWSErrorInfo = (code) => {
2170
+ const exact = wsErrorMap[code];
2171
+ if (exact) return exact;
2172
+ if (code >= 4900 && code <= 4913) return {
2173
+ message: "内部错误,请重连",
2174
+ canResume: false,
2175
+ canIdentify: true
2176
+ };
2177
+ };
2178
+ /**
2179
+ * 格式化 WebSocket 关闭信息为可读字符串
2180
+ * @param code - WebSocket close code
2181
+ * @param reason - 关闭原因(二进制 payload)
2182
+ * @returns 格式化后的错误字符串
2183
+ */
2184
+ const formatWSClose = (code, reason) => {
2185
+ const info = getWSErrorInfo(code);
2186
+ const parts = [];
2187
+ if (info) {
2188
+ parts.push(`[${code}] ${info.message}`);
2189
+ if (info.canResume && info.canIdentify) parts.push("建议: 可尝试 Resume 或重新 Identify");
2190
+ else if (info.canResume) parts.push("建议: 可尝试 Resume");
2191
+ else if (info.canIdentify) parts.push("建议: 可尝试重新 Identify");
2192
+ else parts.push("建议: 不可重试,请检查配置或联系官方");
2193
+ } else {
2194
+ parts.push(`[${code}] 未知错误码`);
2195
+ if (code === 1e3) parts.push("正常关闭");
2196
+ else if (code === 1001) parts.push("终端离开");
2197
+ else if (code === 1006) parts.push("异常关闭(连接异常断开)");
2198
+ else if (code === 1011) parts.push("服务器遇到异常");
2199
+ }
2200
+ if (reason) parts.push(`reason: ${reason}`);
2201
+ return parts.join(" | ");
2202
+ };
2203
+
2204
+ //#endregion
2205
+ //#region src/connection/ws/client.ts
2206
+ /**
2207
+ * 单连接生命周期
2208
+ */
2209
+ var WSClient = class {
2210
+ opts;
2211
+ socket;
2212
+ heartbeatTimer;
2213
+ heartbeatInterval = 45e3;
2214
+ sessionId = "";
2215
+ seq = 0;
2216
+ closed = false;
2217
+ closedReason = "manual";
2218
+ constructor(opts) {
2219
+ this.opts = opts;
2220
+ if (opts.resume) {
2221
+ this.sessionId = opts.resume.sessionId;
2222
+ this.seq = opts.resume.seq;
2223
+ }
2224
+ }
2225
+ /** 连接的快照,用于上层 Resume */
2226
+ snapshot() {
2227
+ return {
2228
+ sessionId: this.sessionId,
2229
+ seq: this.seq
2230
+ };
2231
+ }
2232
+ start() {
2233
+ const ws = new WebSocket(this.opts.gatewayUrl);
2234
+ this.socket = ws;
2235
+ ws.on("open", () => {
2236
+ log("debug", `${this.opts.appId}: WebSocket opened (${this.opts.gatewayUrl})`);
2237
+ });
2238
+ ws.on("close", (code, reason) => {
2239
+ const closeInfo = formatWSClose(code, reason?.toString() || "(empty)");
2240
+ log("error", `${this.opts.appId}: WebSocket 关闭 | ${closeInfo}`);
2241
+ if (this.closed) return;
2242
+ this.close("closed");
2243
+ });
2244
+ ws.on("error", (err) => {
2245
+ log("error", `${this.opts.appId}: WebSocket error:`, err);
2246
+ if (this.closed) return;
2247
+ this.close("error");
2248
+ });
2249
+ ws.on("unexpected-response", (_req, res) => {
2250
+ log("error", `${this.opts.appId}: WebSocket unexpected-response status=${res.statusCode} ${res.statusMessage}`);
2251
+ });
2252
+ ws.on("message", (raw) => this.onMessage(raw));
2253
+ }
2254
+ /**
2255
+ * 主动关闭
2256
+ */
2257
+ stop() {
2258
+ this.close("manual");
2259
+ }
2260
+ close(reason) {
2261
+ if (this.closed) return;
2262
+ this.closed = true;
2263
+ this.closedReason = reason;
2264
+ if (this.heartbeatTimer) {
2265
+ clearInterval(this.heartbeatTimer);
2266
+ this.heartbeatTimer = void 0;
2267
+ }
2268
+ try {
2269
+ this.socket?.removeAllListeners();
2270
+ this.socket?.close();
2271
+ } catch {}
2272
+ this.opts.onClose(reason);
2273
+ }
2274
+ send(payload) {
2275
+ try {
2276
+ this.socket?.send(JSON.stringify(payload));
2277
+ } catch (err) {
2278
+ log("error", `${this.opts.appId}: send failed`, err);
2279
+ }
2280
+ }
2281
+ startHeartbeat() {
2282
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
2283
+ this.heartbeatTimer = setInterval(() => {
2284
+ if (this.closed) return;
2285
+ this.send({
2286
+ op: 1,
2287
+ d: this.seq || null
2288
+ });
2289
+ }, this.heartbeatInterval);
2290
+ }
2291
+ identify() {
2292
+ this.send({
2293
+ op: 2,
2294
+ d: {
2295
+ token: `QQBot ${this.opts.getAccessToken()}`,
2296
+ intents: this.opts.intents,
2297
+ shard: [0, 1],
2298
+ properties: {
2299
+ $os: os.type(),
2300
+ $browser: "@karinjs/adapter-qqbot",
2301
+ $device: "@karinjs/adapter-qqbot"
2302
+ }
2303
+ }
2304
+ });
2305
+ log("debug", `${this.opts.appId}: Identify sent, intents=${this.opts.intents}`);
2306
+ }
2307
+ resume() {
2308
+ this.send({
2309
+ op: 6,
2310
+ d: {
2311
+ token: `QQBot ${this.opts.getAccessToken()}`,
2312
+ session_id: this.sessionId,
2313
+ seq: this.seq
2314
+ }
2315
+ });
2316
+ log("debug", `${this.opts.appId}: Resume sent, session=${this.sessionId}, seq=${this.seq}`);
2317
+ }
2318
+ onMessage(raw) {
2319
+ const text = raw.toString();
2320
+ let msg;
2321
+ try {
2322
+ msg = JSON.parse(text);
2323
+ } catch {
2324
+ log("error", `${this.opts.appId}: non-JSON message: ${text}`);
2325
+ return;
2326
+ }
2327
+ log("debug", `${this.opts.appId}: << op=${msg.op} t=${msg.t ?? "-"} s=${msg.s ?? "-"} body=${text.length > 500 ? text.slice(0, 500) + "..." : text}`);
2328
+ switch (msg.op) {
2329
+ case 10:
2330
+ this.heartbeatInterval = msg.d?.heartbeat_interval || 45e3;
2331
+ log("debug", `${this.opts.appId}: Hello received, heartbeat_interval=${this.heartbeatInterval}ms, session=${this.sessionId || "(none)"} seq=${this.seq}`);
2332
+ this.startHeartbeat();
2333
+ if (this.sessionId && this.seq) this.resume();
2334
+ else this.identify();
2335
+ break;
2336
+ case 11:
2337
+ log("debug", `${this.opts.appId}: HeartbeatACK`);
2338
+ break;
2339
+ case 0:
2340
+ if (typeof msg.s === "number") this.seq = msg.s;
2341
+ if (msg.t === "READY") {
2342
+ this.sessionId = msg.d?.session_id || "";
2343
+ log("debug", `${this.opts.appId}: READY received, session_id=${this.sessionId}`);
2344
+ }
2345
+ try {
2346
+ this.opts.onEvent(msg);
2347
+ } catch (err) {
2348
+ log("error", `${this.opts.appId}: onEvent handler threw:`, err);
2349
+ }
2350
+ break;
2351
+ case 7:
2352
+ log("debug", `${this.opts.appId}: server asked reconnect (op:7)`);
2353
+ this.close("reconnect");
2354
+ break;
2355
+ case 9:
2356
+ if (this.sessionId) {
2357
+ log("debug", `${this.opts.appId}: invalid session, will retry with fresh identify`);
2358
+ this.sessionId = "";
2359
+ this.seq = 0;
2360
+ this.close("session_lost");
2361
+ } else {
2362
+ log("error", `${this.opts.appId}: auth_fail (op:9 with no prior session)`);
2363
+ this.close("auth_fail");
2364
+ }
2365
+ break;
2366
+ default: log("debug", `${this.opts.appId}: unhandled opcode ${msg.op}`);
2367
+ }
2368
+ }
2369
+ };
2370
+
2371
+ //#endregion
2372
+ //#region src/connection/ws/intents.ts
2373
+ /**
2374
+ * intent bit -> 名称(中英双语)
2375
+ */
2376
+ const intentBitMap = {
2377
+ [1]: "GUILDS (频道生命周期)",
2378
+ [2]: "GUILD_MEMBERS (频道成员变更)",
2379
+ [512]: "GUILD_MESSAGES (频道消息-私域)",
2380
+ [4096]: "DIRECT_MESSAGE (频道私信)",
2381
+ [1 << 25]: "GROUP_AND_C2C_EVENT (群聊/单聊消息)",
2382
+ [1 << 30]: "PUBLIC_GUILD_MESSAGES (频道消息-公域)"
2383
+ };
2384
+ /** 优先级从高到低,回退时从最前面剥离 */
2385
+ const priorityIntents = [
2386
+ 2,
2387
+ 512,
2388
+ 1 << 30,
2389
+ 4096,
2390
+ 1 << 25,
2391
+ 1
2392
+ ];
2393
+ /**
2394
+ * 计算最大 intents(全部启用)
2395
+ */
2396
+ const computeMax = () => priorityIntents.reduce((acc, bit) => acc | bit, 0);
2397
+ /**
2398
+ * 在当前 intents 基础上去掉最高优先级的一位,得到回退值
2399
+ * 返回 0 表示已无可用 intent
2400
+ */
2401
+ const computeFallback = (current) => {
2402
+ for (const bit of priorityIntents) if (current & bit) return current & ~bit;
2403
+ return 0;
2404
+ };
2405
+ /**
2406
+ * 将 intents 数值解析为可读名称列表
2407
+ */
2408
+ const formatIntentNames = (intents, prefix = "") => {
2409
+ const names = [];
2410
+ for (const [bit, name] of Object.entries(intentBitMap)) if (intents & Number(bit)) names.push(prefix + name);
2411
+ return names.join("\n");
2412
+ };
2413
+
2414
+ //#endregion
2415
+ //#region src/connection/ws/manager.ts
2416
+ const conns = /* @__PURE__ */ new Map();
2417
+ /** /gateway 接口超时(毫秒) */
2418
+ const GATEWAY_FETCH_TIMEOUT_MS = 5e3;
2419
+ /**
2420
+ * 从 prodApi / sandboxApi 推导兜底的 WSS 地址
2421
+ * 官方实际长期返回的也是 wss://{host}/websocket/,不依赖 /gateway 也能直连
2422
+ */
2423
+ const buildFallbackWssUrl = (cfg) => {
2424
+ return `${(cfg.sandbox ? cfg.sandboxApi : cfg.prodApi).replace(/^https?:\/\//i, "wss://").replace(/\/+$/, "")}/websocket/`;
2425
+ };
2426
+ /**
2427
+ * 获取网关地址:优先调 /gateway;失败 / 超时 / 限频 → 直接 fallback 到硬编码
2428
+ */
2429
+ const fetchGateway = async (cfg) => {
2430
+ const apiUrl = cfg.sandbox ? cfg.sandboxApi : cfg.prodApi;
2431
+ const fallback = buildFallbackWssUrl(cfg);
2432
+ const accessToken = getBotAccessToken(cfg.appId);
2433
+ if (!accessToken) {
2434
+ log("warn", `${cfg.appId}: 无 access_token,使用硬编码 WSS ${fallback}`);
2435
+ return fallback;
2436
+ }
2437
+ try {
2438
+ const { data } = await axios.get(`${apiUrl}/gateway`, {
2439
+ headers: { Authorization: `QQBot ${accessToken}` },
2440
+ timeout: GATEWAY_FETCH_TIMEOUT_MS
2441
+ });
2442
+ if (data?.url) return data.url;
2443
+ log("warn", `${cfg.appId}: /gateway 返回为空,使用硬编码 WSS ${fallback}`);
2444
+ return fallback;
2445
+ } catch (err) {
2446
+ if (axios.isAxiosError(err)) {
2447
+ const response = err.response;
2448
+ const status = response?.status ?? 0;
2449
+ const data = response?.data;
2450
+ const detail = formatOpenAPIError(status, typeof data?.code === "number" ? data.code : void 0, typeof data?.message === "string" ? data.message : void 0);
2451
+ log("warn", `${cfg.appId}: /gateway 调用失败 | ${detail},使用硬编码 WSS ${fallback}`);
2452
+ } else {
2453
+ const reason = err?.message || "unknown";
2454
+ log("warn", `${cfg.appId}: /gateway 调用失败 (${reason}),使用硬编码 WSS ${fallback}`);
2455
+ }
2456
+ return fallback;
2457
+ }
2458
+ };
2459
+ /** 重连退避(毫秒) */
2460
+ const backoff = (attempt) => Math.min(3e4, 1500 * 2 ** Math.min(attempt, 5)) + random(0, 1500);
2461
+ /**
2462
+ * 打印 intents 探测汇总
2463
+ */
2464
+ const summarize = (appId, finalIntents, success) => {
2465
+ const conn = conns.get(appId);
2466
+ if (!conn || conn.intentHistory.length <= 1) return;
2467
+ const initial = conn.intentHistory[0];
2468
+ const removed = [];
2469
+ for (let i = 1; i < conn.intentHistory.length; i++) {
2470
+ const diff = conn.intentHistory[i - 1] ^ conn.intentHistory[i];
2471
+ for (const [bit, name] of Object.entries(intentBitMap)) if (diff & Number(bit)) removed.push(name.trim());
2472
+ }
2473
+ const removedBlock = removed.length ? " └─ 被移除 / Removed:\n" + removed.map((n) => ` ${n.trim()}`).join("\n") : " └─ 被移除 / Removed: 无";
2474
+ log("debug", [
2475
+ `${appId}: intents ${success ? "探测完成 | Probe Done" : "探测失败 | Probe Failed"}`,
2476
+ ` ├─ 初始 / Initial: ${initial}`,
2477
+ ` ├─ 最终 / Final : ${finalIntents}`,
2478
+ formatIntentNames(finalIntents, " │ "),
2479
+ removedBlock
2480
+ ].join("\n"));
2481
+ conn.intentHistory = [];
2482
+ };
2483
+ /**
2484
+ * 启动连接
2485
+ */
2486
+ const start = async (cfg) => {
2487
+ if (cfg.event.type !== 2) return;
2488
+ if (!cfg.appId) return;
2489
+ if (conns.has(cfg.appId)) stop(cfg.appId);
2490
+ const intents = computeMax();
2491
+ conns.set(cfg.appId, {
2492
+ client: null,
2493
+ cfg,
2494
+ intents,
2495
+ intentHistory: [intents],
2496
+ aborted: false
2497
+ });
2498
+ await connect(cfg, intents, 0);
2499
+ };
2500
+ /**
2501
+ * 调度一次重试(统一封装,便于 stop 取消)
2502
+ */
2503
+ const schedule = (appId, delay, fn) => {
2504
+ const conn = conns.get(appId);
2505
+ if (!conn || conn.aborted) return;
2506
+ if (conn.pendingTimer) clearTimeout(conn.pendingTimer);
2507
+ conn.pendingTimer = setTimeout(() => {
2508
+ if (conn.aborted) return;
2509
+ conn.pendingTimer = void 0;
2510
+ fn();
2511
+ }, delay);
2512
+ };
2513
+ /**
2514
+ * 内部连接 + 重试
2515
+ */
2516
+ const connect = async (cfg, intents, attempt, resume) => {
2517
+ let conn = conns.get(cfg.appId);
2518
+ if (!conn || conn.aborted) return;
2519
+ const gatewayUrl = await fetchGateway(cfg);
2520
+ conn = conns.get(cfg.appId);
2521
+ if (!conn || conn.aborted) return;
2522
+ const client = new WSClient({
2523
+ appId: cfg.appId,
2524
+ gatewayUrl,
2525
+ getAccessToken: () => getBotAccessToken(cfg.appId) || "",
2526
+ intents,
2527
+ resume,
2528
+ onEvent: (ev) => {
2529
+ if (ev.t === "READY") summarize(cfg.appId, intents, true);
2530
+ dispatch(cfg.appId, ev);
2531
+ },
2532
+ onClose: (reason) => onClose(cfg, intents, attempt, reason, client)
2533
+ });
2534
+ conn.client = client;
2535
+ conn.cfg = cfg;
2536
+ conn.intents = intents;
2537
+ if (conn.intentHistory[conn.intentHistory.length - 1] !== intents) conn.intentHistory.push(intents);
2538
+ client.start();
2539
+ };
2540
+ /**
2541
+ * 关闭回调处理
2542
+ */
2543
+ const onClose = (cfg, intents, attempt, reason, client) => {
2544
+ if (reason === "manual") {
2545
+ log("debug", `${cfg.appId}: WebSocket 已主动关闭`);
2546
+ return;
2547
+ }
2548
+ const conn = conns.get(cfg.appId);
2549
+ if (!conn || conn.aborted) return;
2550
+ if (reason === "auth_fail") {
2551
+ const fallback = computeFallback(intents);
2552
+ if (fallback && fallback !== intents) {
2553
+ schedule(cfg.appId, 1e3, () => connect(cfg, fallback, 0));
2554
+ return;
2555
+ }
2556
+ summarize(cfg.appId, 0, false);
2557
+ log("error", `${cfg.appId}: 鉴权失败,所有 intents 均不可用,请检查机器人是否已上线`);
2558
+ conn.aborted = true;
2559
+ conns.delete(cfg.appId);
2560
+ return;
2561
+ }
2562
+ const snap = client.snapshot();
2563
+ const resume = reason === "session_lost" ? void 0 : snap.sessionId ? snap : void 0;
2564
+ const delay = backoff(attempt);
2565
+ log("error", `${cfg.appId}: WebSocket 断开 (${reason}),${delay}ms 后重连...`);
2566
+ schedule(cfg.appId, delay, () => connect(cfg, intents, attempt + 1, resume));
2567
+ };
2568
+ /**
2569
+ * 停止连接(同步:标记 abort + 清 pendingTimer + 关 client)
2570
+ */
2571
+ const stop = (appId) => {
2572
+ const conn = conns.get(appId);
2573
+ if (!conn) return false;
2574
+ conn.aborted = true;
2575
+ if (conn.pendingTimer) {
2576
+ clearTimeout(conn.pendingTimer);
2577
+ conn.pendingTimer = void 0;
2578
+ }
2579
+ try {
2580
+ conn.client?.stop();
2581
+ } catch {}
2582
+ conns.delete(appId);
2583
+ return true;
2584
+ };
2585
+
2586
+ //#endregion
2587
+ //#region src/core/index.ts
2588
+ /**
2589
+ * 初始化所有 Bot
2590
+ */
2591
+ const initQQBotAdapter = async () => {
2592
+ const cfg = config();
2593
+ logger.debug(`[QQ Official Bot] 开始初始化 ${cfg.length} 个 Bot...`);
2594
+ for (const bot of cfg) try {
2595
+ await createBot(bot);
2596
+ } catch (err) {
2597
+ logger.debug(`[QQ Official Bot] 初始化 Bot 失败 ${bot.appId || "(无 appId)"}:`, err);
2598
+ }
2599
+ };
2600
+ /**
2601
+ * 初始化状态(防止并发触发重复连接)
2602
+ */
2603
+ const initState = /* @__PURE__ */ new Map();
2604
+ const hashConfig = (bot) => JSON.stringify({
2605
+ appId: bot.appId,
2606
+ secret: bot.secret,
2607
+ tokenApi: bot.tokenApi,
2608
+ prodApi: bot.prodApi,
2609
+ sandboxApi: bot.sandboxApi,
2610
+ sandbox: bot.sandbox,
2611
+ event: bot.event,
2612
+ keyboard: bot.keyboard
2613
+ });
2614
+ /**
2615
+ * 创建单个 Bot 实例
2616
+ */
2617
+ const createBot = async (bot) => {
2618
+ const appId = String(bot.appId);
2619
+ if (!appId) {
2620
+ logger.warn("[QQ Official Bot] 配置缺少 appId,跳过");
2621
+ return;
2622
+ }
2623
+ if (bot.event.type === 0) {
2624
+ logger.info(`[QQ Official Bot][${appId}] 事件接收方式为"关闭",跳过`);
2625
+ return;
2626
+ }
2627
+ const hash = hashConfig(bot);
2628
+ const existing = initState.get(appId);
2629
+ if (existing) {
2630
+ if (existing.hash === hash) return existing.promise;
2631
+ await existing.promise.catch(() => {});
2632
+ }
2633
+ const promise = (async () => {
2634
+ unregisterBot("selfId", appId);
2635
+ stop(appId);
2636
+ offAll(appId);
2637
+ stopTokenRefresh(appId);
2638
+ logger.info(`[QQ Official Bot][${appId}] 获取 access_token...`);
2639
+ await getAccessToken(bot.tokenApi, appId, bot.secret);
2640
+ const baseUrl = bot.sandbox ? bot.sandboxApi : bot.prodApi;
2641
+ const api = new QQBotApi(createAxiosInstance(baseUrl, appId));
2642
+ const me = await api.meta.getMe();
2643
+ const client = new AdapterQQBot(bot, api);
2644
+ client.account.name = me.username;
2645
+ client.account.avatar = me.avatar;
2646
+ client.account.selfId = appId;
2647
+ const qq = new URL(me.share_url).searchParams.get("robot_uin");
2648
+ client.account.subId.id = me.id;
2649
+ client.account.subId.qq = qq;
2650
+ client.account.subId.appid = appId;
2651
+ client.account.subId.union_openid = me.union_openid;
2652
+ bus.on(appId, (ev) => dispatch$1(client, ev));
2653
+ client.adapter.address = baseUrl;
2654
+ client.adapter.secret = bot.secret;
2655
+ client.adapter.version = pkg().version;
2656
+ if (bot.event.type === 2) {
2657
+ client.adapter.index = registerBot("webSocketClient", client);
2658
+ logger.debug(`[QQ Official Bot][${appId}] 已注册为 webSocketClient`);
2659
+ await start(bot);
2660
+ } else if (bot.event.type === 1) {
2661
+ client.adapter.index = registerBot("http", client);
2662
+ logger.debug(`[QQ Official Bot][${appId}] 已注册为 http`);
2663
+ }
2664
+ })();
2665
+ initState.set(appId, {
2666
+ hash,
2667
+ promise
2668
+ });
2669
+ promise.finally(() => {
2670
+ if (initState.get(appId)?.hash === hash) initState.delete(appId);
2671
+ });
2672
+ return promise;
2673
+ };
2674
+ /**
2675
+ * 销毁 bot:注销 + 关 WS + 取消监听 + 停止 token 刷新
2676
+ */
2677
+ const destroyBot = (appId) => {
2678
+ unregisterBot("selfId", appId);
2679
+ stop(appId);
2680
+ offAll(appId);
2681
+ stopTokenRefresh(appId);
2682
+ };
2683
+ bindHandlers(createBot, destroyBot);
2684
+
2685
+ //#endregion
2686
+ //#region src/core/api/sign.ts
2687
+ /**
2688
+ * 从 secret 生成密钥对(不足 32 字节循环填充)
2689
+ */
2690
+ const generateKeyPair = (seed) => {
2691
+ let finalSeed = seed;
2692
+ while (finalSeed.length < 32) finalSeed = finalSeed.repeat(2);
2693
+ finalSeed = finalSeed.slice(0, 32);
2694
+ const seedBuffer = Buffer.from(finalSeed, "utf-8");
2695
+ return nacl.sign.keyPair.fromSeed(seedBuffer);
2696
+ };
2697
+ /**
2698
+ * 用 secret 对 eventTs + plainToken 签名(hex 编码)
2699
+ */
2700
+ const sign = (secret, eventTs, plainToken) => {
2701
+ const { secretKey } = generateKeyPair(secret);
2702
+ const messageBytes = Buffer.from(eventTs + plainToken, "utf-8");
2703
+ const signature = nacl.sign.detached(messageBytes, secretKey);
2704
+ return Buffer.from(signature).toString("hex");
2705
+ };
2706
+ /**
2707
+ * 验证 webhook 推送签名(ed25519)
2708
+ * @param secret 机器人密钥
2709
+ * @param timestamp x-signature-timestamp 头
2710
+ * @param rawBody 原始 body 字符串
2711
+ * @param ed25519Hex x-signature-ed25519 头
2712
+ */
2713
+ const verifySignature = (secret, timestamp, rawBody, ed25519Hex) => {
2714
+ return sign(secret, timestamp, rawBody) === ed25519Hex;
2715
+ };
2716
+
2717
+ //#endregion
2718
+ //#region src/connection/webhook.ts
2719
+ /**
2720
+ * 解析请求:读 rawBody + JSON + 字段校验
2721
+ * 失败返回 null,并已写入日志
2722
+ */
2723
+ const parseRequest = async (req) => {
2724
+ const ip = req.socket.remoteAddress;
2725
+ const rawBody = await new Promise((resolve) => {
2726
+ const chunks = [];
2727
+ req.on("data", (chunk) => chunks.push(chunk));
2728
+ req.on("end", () => resolve(chunks.join("")));
2729
+ });
2730
+ const appid = req.headers["x-bot-appid"];
2731
+ if (!appid) {
2732
+ fakeEvent(`未找到 x-bot-appid: ${ip} body: ${rawBody}`);
2733
+ return null;
2734
+ }
2735
+ if (req.headers["user-agent"] !== "QQBot-Callback") {
2736
+ fakeEvent(`User-Agent 非 QQBot-Callback: ${ip} body: ${rawBody}`);
2737
+ return null;
2738
+ }
2739
+ let body;
2740
+ try {
2741
+ body = JSON.parse(rawBody) || {};
2742
+ } catch {
2743
+ fakeEvent(`非法 JSON: ${ip} body: ${rawBody}`);
2744
+ return null;
2745
+ }
2746
+ if (typeof body.op !== "number") {
2747
+ fakeEvent(`缺少 op 字段: ${ip} body: ${rawBody}`);
2748
+ return null;
2749
+ }
2750
+ return {
2751
+ appid,
2752
+ rawBody,
2753
+ body
2754
+ };
2755
+ };
2756
+ /**
2757
+ * QQ 官方 webhook 路由
2758
+ * POST /qqbot/webhook
2759
+ */
2760
+ const webhookRouting = async (req, res) => {
2761
+ const parsed = await parseRequest(req);
2762
+ if (!parsed) {
2763
+ res.status(200).end();
2764
+ return;
2765
+ }
2766
+ const cfg = getConfig(parsed.appid);
2767
+ if (!cfg) {
2768
+ log("error", `[webhook][${parsed.appid}] 配置文件中未找到对应 appId`);
2769
+ res.status(200).end();
2770
+ return;
2771
+ }
2772
+ if (cfg.event.type !== 1) {
2773
+ log("error", `[webhook][${parsed.appid}] webhook 未启用`);
2774
+ res.status(200).end();
2775
+ return;
2776
+ }
2777
+ if (parsed.body.op === 13) {
2778
+ const eventTs = parsed.body?.d?.event_ts;
2779
+ const plainToken = parsed.body?.d?.plain_token;
2780
+ if (!eventTs || !plainToken) {
2781
+ log("error", `[webhook][${parsed.appid}] 缺少 event_ts/plain_token: ${parsed.rawBody}`);
2782
+ res.status(200).end();
2783
+ return;
2784
+ }
2785
+ const signature = sign(cfg.secret, eventTs, plainToken);
2786
+ log("mark", `[webhook][${parsed.appid}] sign=${signature}`);
2787
+ res.setHeader("Content-Type", "application/json");
2788
+ res.status(200).end(JSON.stringify({
2789
+ plain_token: plainToken,
2790
+ signature
2791
+ }));
2792
+ return;
2793
+ }
2794
+ const ed25519 = req.headers["x-signature-ed25519"];
2795
+ const timestamp = req.headers["x-signature-timestamp"];
2796
+ if (!verifySignature(cfg.secret, timestamp, parsed.rawBody, ed25519)) {
2797
+ fakeEvent(`签名验证失败 appid=${parsed.appid} headerSig=${ed25519} body=${parsed.rawBody}`);
2798
+ res.status(200).end();
2799
+ return;
2800
+ }
2801
+ res.status(200).end();
2802
+ dispatch(parsed.appid, parsed.body);
2803
+ };
2804
+
2805
+ //#endregion
2806
+ //#region src/connection/routing.ts
2807
+ /**
2808
+ * 初始化路由
2809
+ * 创建 /qqbot/webhook 路由用于接收QQ机器人事件
2810
+ */
2811
+ const createRouting = () => {
2812
+ const router = express.Router();
2813
+ router.use("/webhook", webhookRouting);
2814
+ app.use("/qqbot", router);
2815
+ };
2816
+
2817
+ //#endregion
2818
+ //#region src/index.ts
2819
+ logger.info(`${logger.violet(`[插件:${pkg().version}]`)} ${logger.green(basename)} 初始化完成~`);
2820
+ createRouting();
2821
+ const printOnboardBanner = () => {
2822
+ console.log();
2823
+ console.log(" ==========================================");
2824
+ console.log(" 未检测到 QQBot 配置");
2825
+ console.log(" 请使用任意可用 bot(如 OneBot / Console 适配器)");
2826
+ console.log(" 对机器人发送指令: #QQ登录");
2827
+ console.log(" 扫码授权完成后会自动写入配置并完成初始化");
2828
+ console.log();
2829
+ console.log(" ⚠ 注意:扫码授权会刷新该机器人的 secret,旧 secret 立即失效");
2830
+ console.log(" ==========================================");
2831
+ console.log();
2832
+ };
2833
+ const bootstrap = async () => {
2834
+ if (needQrOnboard()) {
2835
+ printOnboardBanner();
2836
+ return;
2837
+ }
2838
+ try {
2839
+ await initQQBotAdapter();
2840
+ } catch (err) {
2841
+ log("error", "初始化适配器失败:", err);
2842
+ }
2843
+ };
2844
+ bootstrap();
2845
+
2846
+ //#endregion
2847
+ export { };