karin-plugin-adapter-all 0.0.0-stage → 1.2.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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -2
  3. package/dist/config-DKcb6lmw.js +54 -0
  4. package/dist/dir.js +49 -0
  5. package/dist/icqq-BR8miqDV.js +1771 -0
  6. package/dist/index.js +10 -0
  7. package/dist/server.js +13577 -0
  8. package/dist/web.config.js +36 -0
  9. package/package.json +69 -4
  10. package/resources/image//345/220/257/347/250/213/345/256/243/345/217/221.png +0 -0
  11. package/resources/reactions.json +50 -0
  12. package/resources/template/test.html +21 -0
  13. package/resources/web/404/index.html +1 -0
  14. package/resources/web/404.html +1 -0
  15. package/resources/web/_next/static/SIPU0JhZAVD0DvlvpoXqx/_buildManifest.js +1 -0
  16. package/resources/web/_next/static/SIPU0JhZAVD0DvlvpoXqx/_ssgManifest.js +1 -0
  17. package/resources/web/_next/static/chunks/2592325e-57fbed9f64536727.js +1 -0
  18. package/resources/web/_next/static/chunks/634-7552fa79f7816d81.js +1 -0
  19. package/resources/web/_next/static/chunks/778-0a7f658317254539.js +1 -0
  20. package/resources/web/_next/static/chunks/78-98f9c6d536c55c8d.js +1 -0
  21. package/resources/web/_next/static/chunks/app/_not-found/page-479700a6cdca1b89.js +1 -0
  22. package/resources/web/_next/static/chunks/app/layout-2fcd97f6eeeb705f.js +1 -0
  23. package/resources/web/_next/static/chunks/app/not-found-479700a6cdca1b89.js +1 -0
  24. package/resources/web/_next/static/chunks/app/page-32d138e107d21bbf.js +1 -0
  25. package/resources/web/_next/static/chunks/framework-939e80bc3ec79f24.js +1 -0
  26. package/resources/web/_next/static/chunks/main-app-9148086af618c8de.js +1 -0
  27. package/resources/web/_next/static/chunks/main-bff001951bd96286.js +1 -0
  28. package/resources/web/_next/static/chunks/pages/_app-04ed26170c78d2cb.js +1 -0
  29. package/resources/web/_next/static/chunks/pages/_error-2a3f5266a349ca5b.js +1 -0
  30. package/resources/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  31. package/resources/web/_next/static/chunks/webpack-eb9f7b784f442cad.js +1 -0
  32. package/resources/web/_next/static/css/5ca80a6ff000abd3.css +1 -0
  33. package/resources/web/index.html +1 -0
  34. package/resources/web/index.txt +21 -0
@@ -0,0 +1,1771 @@
1
+ import { n as emitLoginEvent, r as BaseBot, t as clearLoginState } from "./server.js";
2
+ import path from "node:path";
3
+ import { contactFriend, contactGroup, contactGroupTemp, createFriendMessage, createGroupAdminChangedNotice, createGroupApplyRequest, createGroupInviteRequest, createGroupMemberAddNotice, createGroupMemberBanNotice, createGroupMemberDelNotice, createGroupMessage, createGroupMessageReactionNotice, createGroupPokeNotice, createGroupRecallNotice, createGroupTempMessage, createPrivateApplyRequest, createPrivatePokeNotice, createPrivateRecallNotice, karinPathData, logger, segment, senderFriend, senderGroup, senderGroupTemp } from "node-karin";
4
+ import fs from "node:fs";
5
+ import { createClient, genDmMessageId, genGroupMessageId, parseDmMessageId, parseGroupMessageId, segment as segment$1 } from "icqq";
6
+ import icqqPkg from "icqq/package.json" with { type: "json" };
7
+
8
+ //#region src/adapters/icqq/convert.ts
9
+ /** 从收到的消息构造 karin Contact (供会话级 API 使用, 转发节点转换场景传 ForwardMessage 兜底私聊) */
10
+ function contactOf(message) {
11
+ const msg = message;
12
+ if (msg.message_type === "group") {
13
+ return {
14
+ scene: "group",
15
+ peer: String(message.group_id ?? message.user_id),
16
+ subId: {},
17
+ name: ""
18
+ };
19
+ }
20
+ return {
21
+ scene: "friend",
22
+ peer: String(message.from_id ?? message.user_id),
23
+ subId: {},
24
+ name: ""
25
+ };
26
+ }
27
+ /** resid 归属场景缓存: 合并转发 resid 解包依赖群/私聊上下文, 收侧记录以便 getForwardMsg 选择正确的会话对象 */
28
+ const residScene = new Map();
29
+ /** 记录 resid 场景 (容量 500, 淘汰最旧) */
30
+ function rememberResidScene(resId, contact) {
31
+ if (!resId) return;
32
+ residScene.set(resId, {
33
+ scene: contact.scene === "group" ? "group" : "friend",
34
+ peer: contact.peer
35
+ });
36
+ if (residScene.size > 500) {
37
+ const first = residScene.keys().next().value;
38
+ if (first) residScene.delete(first);
39
+ }
40
+ }
41
+ /** 查询 resid 归属场景 */
42
+ function getResidScene(resId) {
43
+ return residScene.get(resId);
44
+ }
45
+ /**
46
+ * 把 icqq 元素的消息 id(群/私聊 cqhttp 字符串) 解析成可引用的 Quotable
47
+ * 用于 Karin reply 段 → icqq 引用回复
48
+ */
49
+ const parseQuotable = (messageId) => {
50
+ try {
51
+ if (messageId.includes("group")) {
52
+ const info = parseGroupMessageId(messageId);
53
+ return {
54
+ user_id: info.user_id,
55
+ time: info.time,
56
+ seq: info.seq,
57
+ rand: info.rand,
58
+ message: ""
59
+ };
60
+ }
61
+ const info = parseDmMessageId(messageId);
62
+ return {
63
+ user_id: info.user_id,
64
+ time: info.time,
65
+ seq: info.seq,
66
+ rand: info.rand,
67
+ message: ""
68
+ };
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ };
73
+ /** 根据消息上下文 + 引用信息生成 Karin reply 所需的 cqhttp message_id */
74
+ const encodeQuoteMsgId = (message, q) => {
75
+ if (message.message_type === "group") {
76
+ return genGroupMessageId(message.group_id, q.user_id, q.seq, q.rand, q.time);
77
+ }
78
+ return genDmMessageId(q.user_id, q.seq, q.rand, q.time);
79
+ };
80
+ /** icqq 收到的消息 转 Karin Elements */
81
+ async function AdapterConvertKarin$1(message, bot) {
82
+ const elements = [];
83
+ const source = message.source;
84
+ if (source) {
85
+ elements.push(segment.reply(encodeQuoteMsgId(message, source)));
86
+ }
87
+ for (const i of message.message) {
88
+ switch (i.type) {
89
+ case "text":
90
+ elements.push(segment.text(i.text));
91
+ break;
92
+ case "at":
93
+ if (i.qq === "all") {
94
+ elements.push(segment.at("all"));
95
+ } else {
96
+ elements.push(segment.at(String(i.qq), i.text));
97
+ }
98
+ break;
99
+ case "face":
100
+ case "sface":
101
+ elements.push(segment.face(i.id));
102
+ break;
103
+ case "image":
104
+ elements.push(segment.image(i.url || String(i.file), {
105
+ width: i.width,
106
+ height: i.height,
107
+ summary: i.summary
108
+ }));
109
+ break;
110
+ case "record": {
111
+ let url = i.url || "";
112
+ if (!url && i.fid) {
113
+ try {
114
+ url = await bot.getRecordUrl(contactOf(message), i) || "";
115
+ } catch {}
116
+ }
117
+ elements.push(segment.record(url || String(i.file)));
118
+ break;
119
+ }
120
+ case "video": {
121
+ let url = "";
122
+ if (i.fid) {
123
+ try {
124
+ url = await bot.getVideoUrl(contactOf(message), i) || "";
125
+ } catch {}
126
+ }
127
+ elements.push(segment.video(url || String(i.file), {
128
+ width: i.width,
129
+ height: i.height,
130
+ name: i.name
131
+ }));
132
+ break;
133
+ }
134
+ case "flash":
135
+ elements.push(segment.image(i.url || String(i.file)));
136
+ break;
137
+ case "json":
138
+ elements.push(segment.json(typeof i.data === "string" ? i.data : JSON.stringify(i.data)));
139
+ break;
140
+ case "xml":
141
+ elements.push(segment.xml(String(i.data ?? "")));
142
+ break;
143
+ case "markdown":
144
+ elements.push(segment.markdown(i.content));
145
+ break;
146
+ case "reply":
147
+ elements.push(segment.reply(i.id));
148
+ break;
149
+ case "quote": {
150
+ const mid = encodeQuoteMsgId(message, i);
151
+ elements.push(segment.reply(mid));
152
+ break;
153
+ }
154
+ case "file": {
155
+ if (i.fid) {
156
+ const msg = message;
157
+ const scene = msg.message_type === "private" ? "friend" : "group";
158
+ const peer = scene === "friend" ? String(msg.from_id ?? msg.user_id) : String(msg.group_id ?? msg.user_id);
159
+ const contact = scene === "friend" ? {
160
+ scene: "friend",
161
+ peer,
162
+ subId: {},
163
+ name: ""
164
+ } : {
165
+ scene: "group",
166
+ peer,
167
+ subId: {},
168
+ name: ""
169
+ };
170
+ const url = await bot.getFileUrl(contact, String(i.fid));
171
+ elements.push(segment.file(url, {
172
+ fid: String(i.fid),
173
+ name: i.name || "",
174
+ size: i.size
175
+ }));
176
+ } else {
177
+ elements.push(segment.text(`[文件 ${i.name || ""}]`));
178
+ }
179
+ break;
180
+ }
181
+ case "long_msg":
182
+ case "multimsg": {
183
+ const resId = String(i.resid);
184
+ rememberResidScene(resId, contactOf(message));
185
+ elements.push(segment.longMsg(resId));
186
+ break;
187
+ }
188
+ case "poke":
189
+ elements.push(segment.text(`[戳一戳 ${i.text || ""}]`));
190
+ break;
191
+ case "rps":
192
+ elements.push(segment.text("[猜拳]"));
193
+ break;
194
+ case "dice":
195
+ elements.push(segment.text("[骰子]"));
196
+ break;
197
+ case "share":
198
+ elements.push(segment.text(`[分享 ${i.title || ""}]`));
199
+ break;
200
+ case "location":
201
+ elements.push(segment.text(`[位置 ${i.address || ""}]`));
202
+ break;
203
+ case "bface":
204
+ case "mirai":
205
+ case "button":
206
+ case "bubble":
207
+ case "forum":
208
+ default: elements.push(segment.text(JSON.stringify(i)));
209
+ }
210
+ }
211
+ return elements;
212
+ }
213
+ /**
214
+ * data: URL 统一转 `base64://` scheme。
215
+ * icqq 的 uri 仅支持 http(s):// file:// base64:// 三种格式
216
+ */
217
+ const normalizeUri = (uri) => {
218
+ const match = /^data:[^;,]*;base64,(.+)$/s.exec(uri);
219
+ return match ? `base64://${match[1]}` : uri;
220
+ };
221
+ async function KarinConvertAdapter$1(data) {
222
+ const message = [];
223
+ let source;
224
+ for (const i of data) {
225
+ switch (i.type) {
226
+ case "text":
227
+ message.push(i.text);
228
+ break;
229
+ case "at":
230
+ if (i.targetId === "all" || String(i.targetId) === "all") {
231
+ message.push(segment$1.at("all"));
232
+ } else {
233
+ message.push(segment$1.at(Number(i.targetId)));
234
+ }
235
+ break;
236
+ case "face":
237
+ message.push(segment$1.face(i.id));
238
+ break;
239
+ case "reply":
240
+ source = parseQuotable(i.messageId) || source;
241
+ break;
242
+ case "image":
243
+ message.push(segment$1.image(normalizeUri(i.file)));
244
+ break;
245
+ case "record":
246
+ message.push(segment$1.record(normalizeUri(i.file)));
247
+ break;
248
+ case "video":
249
+ message.push(segment$1.video(normalizeUri(i.file)));
250
+ break;
251
+ case "file":
252
+ message.push({
253
+ type: "file",
254
+ file: normalizeUri(i.file),
255
+ name: i.name || ""
256
+ });
257
+ break;
258
+ case "json":
259
+ message.push(segment$1.json(parseJson(i.data)));
260
+ break;
261
+ case "xml":
262
+ message.push(segment$1.xml(i.data));
263
+ break;
264
+ case "markdown":
265
+ message.push(segment$1.markdown(i.markdown));
266
+ break;
267
+ case "marketFace":
268
+ message.push(segment$1.face(Number(i.id)));
269
+ break;
270
+ default: message.push(JSON.stringify(i));
271
+ }
272
+ }
273
+ return {
274
+ message,
275
+ source
276
+ };
277
+ }
278
+ /** icqq 的 json 元素 data 需要对象, Karin 传的是字符串 */
279
+ const parseJson = (data) => {
280
+ try {
281
+ return JSON.parse(data);
282
+ } catch {
283
+ return data;
284
+ }
285
+ };
286
+
287
+ //#endregion
288
+ //#region src/adapters/icqq/event.ts
289
+ /** 消息事件 (client.on('message') 懒加载全部消息) */
290
+ async function dispatchMessage(e, bot) {
291
+ if (e.message_type === "group") {
292
+ await handleGroupMessage(e, bot);
293
+ } else if (e.message_type === "private") {
294
+ await handlePrivateMessage(e, bot);
295
+ } else {
296
+ bot.logger("debug", `[消息] 忽略不支持的消息类型: ${e.message_type}`);
297
+ }
298
+ }
299
+ /** 群消息 → Karin 群消息事件 */
300
+ async function handleGroupMessage(e, bot) {
301
+ const contact = contactGroup(String(e.group_id), e.group_name);
302
+ const s = e.sender;
303
+ const sender = senderGroup(String(s.user_id), s.role, s.nickname, s.sex, s.age, s.card, undefined, s.level, s.title);
304
+ const elements = await AdapterConvertKarin$1(e, bot);
305
+ createGroupMessage({
306
+ time: e.time,
307
+ eventId: e.message_id,
308
+ rawEvent: e,
309
+ srcReply: (element) => bot.sendMsg(contact, element),
310
+ bot,
311
+ messageId: e.message_id,
312
+ messageSeq: e.seq,
313
+ elements,
314
+ contact,
315
+ sender
316
+ });
317
+ }
318
+ /** 私聊消息 → Karin 好友消息 / 群临时会话消息 */
319
+ async function handlePrivateMessage(e, bot) {
320
+ const elements = await AdapterConvertKarin$1(e, bot);
321
+ if (e.sub_type === "friend") {
322
+ const contact = contactFriend(String(e.from_id), e.sender.nickname);
323
+ const sender = senderFriend(String(e.from_id), e.sender.nickname);
324
+ createFriendMessage({
325
+ time: e.time,
326
+ eventId: e.message_id,
327
+ rawEvent: e,
328
+ srcReply: (element) => bot.sendMsg(contact, element),
329
+ bot,
330
+ messageId: e.message_id,
331
+ messageSeq: e.seq,
332
+ elements,
333
+ contact,
334
+ sender
335
+ });
336
+ } else if (e.sub_type === "group" || e.sub_type === "other") {
337
+ const groupId = e.sender.group_id;
338
+ if (!groupId) {
339
+ bot.logger("debug", `[消息] 临时会话缺少群号, 忽略: ${e.message_id}`);
340
+ return;
341
+ }
342
+ const contact = contactGroupTemp(String(groupId), String(e.from_id));
343
+ const sender = senderGroupTemp(String(e.from_id));
344
+ createGroupTempMessage({
345
+ time: e.time,
346
+ eventId: e.message_id,
347
+ rawEvent: e,
348
+ srcReply: (element) => bot.sendMsg(contact, element),
349
+ bot,
350
+ messageId: e.message_id,
351
+ messageSeq: e.seq,
352
+ elements,
353
+ contact,
354
+ sender
355
+ });
356
+ }
357
+ }
358
+ /** 通知事件 (client.on('notice')) */
359
+ async function dispatchNotice(e, bot) {
360
+ const time = Date.now();
361
+ if (e.notice_type === "friend") {
362
+ switch (e.sub_type) {
363
+ case "recall": {
364
+ const ev = e;
365
+ const userId = String(ev.user_id);
366
+ const contact = contactFriend(userId);
367
+ createPrivateRecallNotice({
368
+ time: Date.now(),
369
+ eventId: `notice:${ev.message_id}`,
370
+ rawEvent: e,
371
+ contact,
372
+ sender: senderFriend(userId),
373
+ srcReply: (element) => bot.sendMsg(contact, element),
374
+ bot,
375
+ content: {
376
+ operatorId: String(ev.operator_id),
377
+ messageId: ev.message_id || "",
378
+ tips: ""
379
+ }
380
+ });
381
+ break;
382
+ }
383
+ case "poke": {
384
+ const ev = e;
385
+ const userId = String(ev.operator_id);
386
+ const contact = contactFriend(userId);
387
+ createPrivatePokeNotice({
388
+ time,
389
+ eventId: `notice:${time}`,
390
+ rawEvent: e,
391
+ bot,
392
+ contact,
393
+ sender: senderFriend(userId),
394
+ srcReply: (element) => bot.sendMsg(contact, element),
395
+ content: {
396
+ operatorId: userId,
397
+ targetId: String(ev.target_id),
398
+ action: ev.action,
399
+ actionImage: "",
400
+ suffix: ev.suffix
401
+ }
402
+ });
403
+ break;
404
+ }
405
+ case "increase":
406
+ bot.logger("info", `[通知] 新增好友: ${e.user_id} (${e.nickname})`);
407
+ break;
408
+ case "decrease":
409
+ bot.logger("info", `[通知] 好友减少: ${e.user_id} (${e.nickname})`);
410
+ break;
411
+ default: bot.logger("debug", `[通知] 未知好友通知: ${JSON.stringify(e)}`);
412
+ }
413
+ return;
414
+ }
415
+ if (e.notice_type === "group") {
416
+ const contact = contactGroup(String(e.group_id), e.group?.name);
417
+ switch (e.sub_type) {
418
+ case "increase": {
419
+ const ev = e;
420
+ const userId = String(ev.user_id);
421
+ createGroupMemberAddNotice({
422
+ bot,
423
+ eventId: `notice:${time}`,
424
+ rawEvent: e,
425
+ time,
426
+ contact,
427
+ sender: senderGroup(userId),
428
+ srcReply: (element) => bot.sendMsg(contact, element),
429
+ content: {
430
+ operatorId: String(ev.user_id),
431
+ targetId: userId,
432
+ type: "approve"
433
+ }
434
+ });
435
+ break;
436
+ }
437
+ case "decrease": {
438
+ const ev = e;
439
+ const userId = String(ev.user_id);
440
+ const operatorId = String(ev.operator_id);
441
+ createGroupMemberDelNotice({
442
+ bot,
443
+ eventId: `notice:${time}`,
444
+ rawEvent: e,
445
+ time,
446
+ contact,
447
+ sender: senderGroup(userId),
448
+ srcReply: (element) => bot.sendMsg(contact, element),
449
+ content: {
450
+ operatorId,
451
+ targetId: userId,
452
+ type: operatorId === userId ? "leave" : ev.dismiss ? "kickBot" : "kick"
453
+ }
454
+ });
455
+ break;
456
+ }
457
+ case "recall": {
458
+ const ev = e;
459
+ createGroupRecallNotice({
460
+ time: Date.now(),
461
+ eventId: `notice:${ev.message_id}`,
462
+ rawEvent: e,
463
+ contact,
464
+ sender: senderGroup(String(ev.operator_id)),
465
+ srcReply: (element) => bot.sendMsg(contact, element),
466
+ bot,
467
+ content: {
468
+ operatorId: String(ev.operator_id),
469
+ messageId: ev.message_id || "",
470
+ targetId: String(ev.user_id),
471
+ tip: ""
472
+ }
473
+ });
474
+ break;
475
+ }
476
+ case "admin": {
477
+ const ev = e;
478
+ createGroupAdminChangedNotice({
479
+ bot,
480
+ eventId: `notice:${time}`,
481
+ rawEvent: e,
482
+ time,
483
+ contact,
484
+ sender: senderGroup(String(ev.user_id)),
485
+ srcReply: (element) => bot.sendMsg(contact, element),
486
+ content: {
487
+ targetId: String(ev.user_id),
488
+ isAdmin: ev.set
489
+ }
490
+ });
491
+ break;
492
+ }
493
+ case "ban": {
494
+ const ev = e;
495
+ const userId = String(ev.user_id);
496
+ createGroupMemberBanNotice({
497
+ bot,
498
+ eventId: `notice:${time}`,
499
+ rawEvent: e,
500
+ time,
501
+ contact,
502
+ sender: senderGroup(userId),
503
+ srcReply: (element) => bot.sendMsg(contact, element),
504
+ content: {
505
+ operatorId: String(ev.operator_id),
506
+ targetId: userId,
507
+ duration: ev.duration,
508
+ isBan: ev.duration > 0
509
+ }
510
+ });
511
+ break;
512
+ }
513
+ case "poke": {
514
+ const ev = e;
515
+ const userId = String(ev.operator_id);
516
+ createGroupPokeNotice({
517
+ time,
518
+ eventId: `notice:${time}`,
519
+ rawEvent: e,
520
+ bot,
521
+ contact,
522
+ sender: senderGroup(userId),
523
+ srcReply: (element) => bot.sendMsg(contact, element),
524
+ content: {
525
+ operatorId: userId,
526
+ targetId: String(ev.target_id),
527
+ action: ev.action,
528
+ actionImage: "",
529
+ suffix: ev.suffix
530
+ }
531
+ });
532
+ break;
533
+ }
534
+ case "reaction": {
535
+ const ev = e;
536
+ createGroupMessageReactionNotice({
537
+ bot,
538
+ eventId: `notice:${time}`,
539
+ rawEvent: e,
540
+ time,
541
+ contact,
542
+ sender: senderGroup(String(ev.user_id)),
543
+ srcReply: (element) => bot.sendMsg(contact, element),
544
+ content: {
545
+ count: 1,
546
+ faceId: Number(ev.id),
547
+ isSet: ev.set,
548
+ messageId: bot.encodeQuoteMsgId(String(ev.group_id), ev.seq)
549
+ }
550
+ });
551
+ break;
552
+ }
553
+ case "sign": {
554
+ const ev = e;
555
+ bot.logger("info", `[通知] 群签到: ${ev.nickname} (${ev.sign_text})`);
556
+ break;
557
+ }
558
+ case "transfer": {
559
+ const ev = e;
560
+ bot.logger("info", `[通知] 群转让: ${ev.operator_id} → ${ev.user_id}`);
561
+ break;
562
+ }
563
+ default: bot.logger("debug", `[通知] 未知群通知: ${JSON.stringify(e)}`);
564
+ }
565
+ return;
566
+ }
567
+ bot.logger("debug", `[通知] 未知通知: ${JSON.stringify(e)}`);
568
+ }
569
+ /** 申请事件 (client.on('request')) */
570
+ async function dispatchRequest(e, bot) {
571
+ const time = Date.now();
572
+ if (e.request_type === "friend") {
573
+ const ev = e;
574
+ const userId = String(ev.user_id);
575
+ const contact = contactFriend(userId);
576
+ createPrivateApplyRequest({
577
+ bot,
578
+ time,
579
+ contact,
580
+ rawEvent: e,
581
+ subEvent: "friendApply",
582
+ eventId: `request:${ev.flag}`,
583
+ sender: senderFriend(userId, ev.nickname),
584
+ srcReply: (element) => bot.sendMsg(contact, element),
585
+ content: {
586
+ applierId: userId,
587
+ message: ev.comment,
588
+ flag: ev.flag
589
+ }
590
+ });
591
+ return;
592
+ }
593
+ if (e.request_type === "group") {
594
+ const contact = contactGroup(String(e.group_id), e.group_name);
595
+ if (e.sub_type === "invite") {
596
+ const ev = e;
597
+ const inviterId = String(ev.user_id);
598
+ createGroupInviteRequest({
599
+ bot,
600
+ time,
601
+ contact,
602
+ rawEvent: e,
603
+ subEvent: "groupInvite",
604
+ eventId: `request:${ev.flag}`,
605
+ sender: senderGroup(inviterId, ev.role),
606
+ srcReply: (element) => bot.sendMsg(contact, element),
607
+ content: {
608
+ inviterId,
609
+ flag: ev.flag
610
+ }
611
+ });
612
+ } else {
613
+ const ev = e;
614
+ const applierId = String(ev.user_id);
615
+ createGroupApplyRequest({
616
+ bot,
617
+ time,
618
+ contact,
619
+ rawEvent: e,
620
+ subEvent: "groupApply",
621
+ eventId: `request:${ev.flag}`,
622
+ sender: senderGroup(applierId, "unknown", ev.nickname),
623
+ srcReply: (element) => bot.sendMsg(contact, element),
624
+ content: {
625
+ applierId,
626
+ inviterId: ev.inviter_id ? String(ev.inviter_id) : "",
627
+ reason: ev.comment,
628
+ flag: ev.flag,
629
+ groupId: String(ev.group_id)
630
+ }
631
+ });
632
+ }
633
+ return;
634
+ }
635
+ bot.logger("debug", `[申请] 未知申请: ${JSON.stringify(e)}`);
636
+ }
637
+
638
+ //#endregion
639
+ //#region src/adapters/icqq/index.ts
640
+ /**
641
+ * icqq 协议适配器: 库直连 QQ(安卓/iPad/Watch等) 协议,
642
+ * 支持 fast(快速登录) / password(密码登录) / qrcode(扫码登录, 需 Watch 平台)。
643
+ * 断线由 icqq 内置 reconn_interval 自动重连, 无需适配器自行处理。
644
+ */
645
+ var IcqqBot = class extends BaseBot {
646
+ /** icqq 客户端 */
647
+ super;
648
+ /** 兼容 BaseBot.stop 的 raw (覆写 stop, 不调用其 close) */
649
+ raw;
650
+ /** 是否已绑定事件 (重连时幂等) */
651
+ #eventsBound = false;
652
+ /** 是否已主动停止 (停止后不再自动重连) */
653
+ #stopped = false;
654
+ /** 连接起始时间 */
655
+ #startTime = 0;
656
+ /** 连接时长计时器 */
657
+ #connectTimeTimer = null;
658
+ /** 登录等待 Promise 的 resolver */
659
+ #loginResolve = null;
660
+ #loginReject = null;
661
+ /** 登录结果 (start 中等待) */
662
+ #loginDone = Promise.resolve();
663
+ /** 是否已上线 (用于区分登录期错误与会话期异常) */
664
+ #online = false;
665
+ /** 滑块已触发 (多通道只跑一轮) */
666
+ #sliderDone = false;
667
+ /** 滑块 ticket 已提交 (多通道互斥) */
668
+ #sliderSubmitted = false;
669
+ /** 登录超时计时器 (滑块/设备/登录验证环节会重置) */
670
+ #loginTimer = null;
671
+ /** 被踢下线后的延迟重连计时器 */
672
+ #loginRetryTimer = null;
673
+ /** 最近一次签名服务报错详情 (internal.verbose 暂存, 由登录错误统一打印, 避免日志多行) */
674
+ #lastQsignError = "";
675
+ /** icqq 数据目录: 统一存放在 Karin 数据根 `@karinjs/data/icqq` (不可配置) */
676
+ get #dataDir() {
677
+ return path.join(karinPathData, "icqq");
678
+ }
679
+ constructor(cfg) {
680
+ super(cfg);
681
+ const clientConfig = {
682
+ data_dir: this.#dataDir,
683
+ log_level: "off",
684
+ ignore_self: true,
685
+ reconn_interval: 5
686
+ };
687
+ if (cfg.platform) clientConfig.platform = cfg.platform;
688
+ if (cfg.ver) clientConfig.ver = cfg.ver;
689
+ if (cfg.sign_api_addr) clientConfig.sign_api_addr = cfg.sign_api_addr;
690
+ this.super = createClient(clientConfig);
691
+ this.raw = this.super;
692
+ this.adapter.name = "ICQQ";
693
+ this.adapter.version = icqqPkg.version;
694
+ this.adapter.platform = "qq";
695
+ this.adapter.standard = "icqq";
696
+ this.adapter.protocol = "icqq";
697
+ this.adapter.communication = "webSocketClient";
698
+ }
699
+ /** 幂等的 for setGroupQQ 登录等待用 */
700
+ __registerBot() {
701
+ this.register();
702
+ }
703
+ /** 断开时注销 Bot */
704
+ __unregisterBot() {
705
+ this.unregister();
706
+ }
707
+ /** 打印当前 Bot 专属日志 */
708
+ logger(level, ...args) {
709
+ logger.bot(level, this.account.selfId, ...args);
710
+ }
711
+ /** 上报登录验证事件到 WebUI (SSE 推送到网页面板) */
712
+ #emit(type, title, extra = {}) {
713
+ const uin = String(this.cfg.uin ?? this.super?.uin ?? "");
714
+ emitLoginEvent(uin, {
715
+ type,
716
+ title,
717
+ ...extra
718
+ });
719
+ }
720
+ /** 直通 icqq API */
721
+ async sendApi(action, params) {
722
+ const method = this.super[action];
723
+ if (typeof method === "function") return method.apply(this.super, Array.isArray(params) ? params : [params]);
724
+ throw new Error(`icqq 客户端无该方法: ${action}`);
725
+ }
726
+ /**
727
+ * 设备锁/登录验证完成后的手动续登 (供 server HTTP 端点调用):
728
+ * icqq 的设备锁(system.login.device)没有自动回调, 用户在网页/手机QQ完成验证后
729
+ * 必须重新调用 login() 让服务端重新校验设备, 验证通过则直接上线。
730
+ */
731
+ async continueLogin() {
732
+ return this.#relogin();
733
+ }
734
+ /** 生成群引用消息 id (reaction 等缺少 rand 的场景) */
735
+ encodeQuoteMsgId(groupId, seq) {
736
+ return genGroupMessageId(Number(groupId), 0, seq, 0, Math.floor(Date.now() / 1e3), 0);
737
+ }
738
+ /** 解析消息对象 → Karin MessageResponse */
739
+ async #toMessageResponse(m) {
740
+ const msg = m;
741
+ const userId = String(msg.sender?.user_id ?? msg.from_id ?? m.user_id);
742
+ const nickname = String(msg.sender?.nickname ?? msg.nickname ?? "");
743
+ const isGroup = msg.message_type === "group";
744
+ const contact = isGroup ? {
745
+ scene: "group",
746
+ peer: String(msg.group_id),
747
+ name: msg.group_name,
748
+ subId: {}
749
+ } : {
750
+ scene: "friend",
751
+ peer: String(msg.from_id ?? m.user_id),
752
+ name: "",
753
+ subId: {}
754
+ };
755
+ const sender = {
756
+ role: msg.sender?.role ?? "unknown",
757
+ userId,
758
+ nick: nickname,
759
+ name: nickname,
760
+ card: msg.sender?.card,
761
+ level: msg.sender?.level,
762
+ title: msg.sender?.title,
763
+ sex: msg.sender?.sex
764
+ };
765
+ return {
766
+ time: m.time,
767
+ messageId: msg.message_id || `${m.seq}-${msg.rand ?? 0}`,
768
+ messageSeq: m.seq,
769
+ contact,
770
+ sender,
771
+ elements: await AdapterConvertKarin$1(m, this)
772
+ };
773
+ }
774
+ /** 绑定客户端事件 (幂等) */
775
+ #bindEvents() {
776
+ if (this.#eventsBound) return;
777
+ this.#eventsBound = true;
778
+ const client = this.super;
779
+ client.on("internal.verbose", (msg, level) => {
780
+ if (Number(level) !== 2) return;
781
+ const s = String(msg ?? "");
782
+ if (/\[qsign\]/.test(s) || /签名api异常/.test(s)) {
783
+ this.#lastQsignError = s;
784
+ } else {
785
+ this.logger("error", `[icqq] ${s}`);
786
+ }
787
+ });
788
+ client.on("message", (e) => {
789
+ dispatchMessage(e, this).catch((err) => this.logger("error", `消息处理错误: ${err.message}`));
790
+ });
791
+ client.on("notice", (e) => {
792
+ dispatchNotice(e, this).catch((err) => this.logger("error", `通知处理错误: ${err.message}`));
793
+ });
794
+ client.on("request", (e) => {
795
+ dispatchRequest(e, this).catch((err) => this.logger("error", `申请处理错误: ${err.message}`));
796
+ });
797
+ client.on("system.online", () => this.#onOnline());
798
+ client.on("system.offline", () => this.#onOffline());
799
+ client.on("system.offline.kickoff", (e) => {
800
+ this.#online = false;
801
+ this.logger("warn", `[登出] 被服务器踢下线: ${e?.message || ""}`);
802
+ this.__unregisterBot();
803
+ this.#scheduleRelogin();
804
+ });
805
+ client.on("system.login.qrcode", (e) => this.#onQrcode(e.image));
806
+ client.on("system.login.slider", (e) => {
807
+ this.#resetLoginTimeout();
808
+ this.#onSlider(e?.url);
809
+ });
810
+ client.on("system.login.device", (e) => {
811
+ this.#resetLoginTimeout();
812
+ const nt = Boolean(this.super?.useNTLogin);
813
+ const phone = nt ? "" : e?.phone ? String(e?.phone) : "";
814
+ this.#emit("device", "需要设备锁验证", {
815
+ message: nt ? "NT 协议设备锁验证, 请在浏览器打开链接完成, 完成后点击「我已验证, 继续登录」" : phone ? `手机号: ${phone}` : "请在浏览器打开链接完成新设备验证, 完成后点击「我已验证, 继续登录」",
816
+ url: e?.url,
817
+ phone
818
+ });
819
+ this.logger("warn", `[登录] 需要设备锁验证: ${e?.url}${nt ? " (NT 协议, 仅网页验证, 完成后需手动重新登录)" : phone ? ` (手机号: ${phone})` : " (无可用手机号, 需网页验证, 完成后需手动重新登录)"}`);
820
+ if (!nt && phone) this.logger("warn", `[登录] 可调用 sendApi('sendSmsCode') 发送短信后 sendApi('submitSmsCode', ['验证码']) 继续`);
821
+ });
822
+ client.on("system.login.auth", (e) => this.#onAuth(e));
823
+ client.on("system.login.error", (e) => {
824
+ this.#emit("failed", "登录失败", { message: `${e?.message ?? e?.code}` });
825
+ this.logger("error", `[登录] 登录失败: ${e?.message ?? e?.code}`);
826
+ if (this.#online) {
827
+ this.logger("warn", `[登录] 已在线, 忽略该登录错误 (等待 icqq 自动重连)`);
828
+ return;
829
+ }
830
+ this.#loginReject?.(new Error(`登录失败: ${e?.message ?? e?.code}`));
831
+ });
832
+ }
833
+ /** 上线: 刷新账号信息 + 注册 + 开启连接计时 */
834
+ #onOnline() {
835
+ const uin = String(this.super.uin || this.cfg.uin || "");
836
+ this.#online = true;
837
+ this.account = {
838
+ uin,
839
+ uid: uin,
840
+ selfId: uin,
841
+ name: this.super.nickname || uin,
842
+ avatar: `https://q1.qlogo.cn/g?b=qq&s=0&nk=${uin}`,
843
+ subId: {}
844
+ };
845
+ this.#startTime = Date.now();
846
+ if (this.#connectTimeTimer) clearInterval(this.#connectTimeTimer);
847
+ this.#connectTimeTimer = setInterval(() => {
848
+ this.adapter.connectTime = Date.now() - this.#startTime;
849
+ }, 1e3);
850
+ this.__registerBot();
851
+ this.#emit("online", "登录成功", { message: this.super.nickname || uin });
852
+ this.logger("info", `登录成功: ${uin}`);
853
+ this.#loginResolve?.();
854
+ this.#loginResolve = null;
855
+ }
856
+ /** 掉线: 注销 + 停连接计时 (自动重连由 icqq 内部处理) */
857
+ #onOffline() {
858
+ this.#online = false;
859
+ this.__unregisterBot();
860
+ this.#emit("offline", "连接已断开");
861
+ if (this.#connectTimeTimer) {
862
+ clearInterval(this.#connectTimeTimer);
863
+ this.#connectTimeTimer = null;
864
+ }
865
+ if (!this.#stopped) {
866
+ this.logger("warn", "[连接] 已掉线, icqq 将自动重连");
867
+ }
868
+ }
869
+ /** 收到登录二维码: 写到本地文件方便扫码 */
870
+ #onQrcode(image) {
871
+ const dirPath = this.#dataDir;
872
+ const file = path.join(dirPath, `icqq-qrcode.png`);
873
+ try {
874
+ fs.mkdirSync(dirPath, { recursive: true });
875
+ fs.writeFileSync(file, image);
876
+ this.logger("warn", `[登录] 请用手机QQ扫码登录, 二维码已保存: ${file}`);
877
+ } catch (err) {
878
+ this.logger("error", `[登录] 二维码保存失败: ${err.message}`);
879
+ }
880
+ }
881
+ /**
882
+ * 需要滑动验证: 按配置选择验证方式 (参考 TRSS Yunzai-ICQQ-Plugin 的半自动化方案)
883
+ * - auto: 全通道并行, 任一通道拿到 ticket 即提交
884
+ * - gt / txhelper / manual: 只走指定通道
885
+ */
886
+ #onSlider(url) {
887
+ if (this.#sliderDone) return;
888
+ this.#sliderDone = true;
889
+ const c = this.super;
890
+ if (c.useNTLogin) {
891
+ this.#emit("progress", "正在准备滑动验证...");
892
+ this.logger("warn", `[登录] NT 协议滑动验证: ${url}`);
893
+ this.#sliderViaCapNT(url);
894
+ return;
895
+ }
896
+ const mode = this.cfg.sliderMode || "gt";
897
+ this.#emit("slider", `需要滑动验证 (方式: ${mode})`, { url });
898
+ this.logger("warn", `[登录] 需要滑动验证 (方式: ${mode}): ${url}`);
899
+ switch (mode) {
900
+ case "txhelper":
901
+ this.#sliderViaTxHelper(url);
902
+ return;
903
+ case "manual":
904
+ this.#sliderViaTicketFile();
905
+ return;
906
+ default: this.#sliderViaGT(url);
907
+ }
908
+ }
909
+ /**
910
+ * NT 协议滑动验证专用通道 (对齐 TRSS Yunzai-ICQQ-Plugin):
911
+ * 先检测候选渲染域名可达性 (captcha.928100.xyz 现役 / CapNT.928100.xyz 旧域名),
912
+ * 用首个可达域名渲染腾讯下发的 NT 验证链接完成滑动,
913
+ * 客户端双端点轮询 ticket + randstr 后提交.
914
+ * 注: 渲染站当前把 ticket 提交到 captcha-api-cf.928100.xyz (Cloudflare 后端),
915
+ * captcha-nt-api.928100.xyz (EdgeOne 后端) 为旧渲染站使用, 两片数据不互通, 故全部轮询.
916
+ */
917
+ async #sliderViaCapNT(url) {
918
+ const uin = String(this.cfg.uin ?? "");
919
+ const gf = globalThis.fetch;
920
+ if (typeof gf !== "function") {
921
+ this.logger("warn", `[登录] 请打开链接完成滑动验证: ${url}`);
922
+ return;
923
+ }
924
+ const apiBases = ["https://captcha-api-cf.928100.xyz", "https://captcha-nt-api.928100.xyz"];
925
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
926
+ const getTicket = async () => {
927
+ for (const base of apiBases) {
928
+ try {
929
+ const res = await (await gf(`${base}/?key=${uin}`)).json();
930
+ if (res && typeof res === "object") return res;
931
+ } catch {}
932
+ }
933
+ return null;
934
+ };
935
+ try {
936
+ for (let i = 0; i < 20; i++) {
937
+ const res = await getTicket();
938
+ if (res?.status === "0" || res?.ticket) break;
939
+ await sleep(3e3);
940
+ }
941
+ const pickRenderBase = async () => {
942
+ const hosts = ["https://captcha.928100.xyz", "https://CapNT.928100.xyz"];
943
+ for (const base of hosts) {
944
+ try {
945
+ const ctrl = new AbortController();
946
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
947
+ try {
948
+ const r = await gf(base, {
949
+ method: "GET",
950
+ redirect: "follow",
951
+ signal: ctrl.signal
952
+ });
953
+ if (r && typeof r.status === "number" && r.status >= 200 && r.status < 500) return base;
954
+ } finally {
955
+ clearTimeout(timer);
956
+ }
957
+ } catch {}
958
+ }
959
+ return null;
960
+ };
961
+ let capUrl = url;
962
+ try {
963
+ const base = await pickRenderBase();
964
+ const query = new URL(url).searchParams.toString();
965
+ capUrl = base ? `${base}?${query}` : url;
966
+ if (!base) this.logger("warn", "[登录] CapNT 渲染服务均不可达, 直接使用腾讯下发原链接");
967
+ } catch {}
968
+ this.#emit("slider", "需要滑动验证 (NT 协议)", {
969
+ message: "请点击打开链接,在新窗口完成滑动",
970
+ url: capUrl
971
+ });
972
+ this.logger("warn", `[登录] 请打开链接完成滑动验证: ${capUrl}`);
973
+ let ticket = null;
974
+ for (let i = 0; i < 60; i++) {
975
+ await sleep(3e3);
976
+ const res = await getTicket();
977
+ if (res?.ticket) {
978
+ ticket = res.randstr ? `${res.ticket},${res.randstr}` : res.ticket;
979
+ break;
980
+ }
981
+ }
982
+ if (ticket) await this.#submitSlider(ticket);
983
+ else {
984
+ this.#emit("timeout", "滑动验证超时", { message: "点击「重新上线」可再次尝试" });
985
+ this.logger("warn", "[登录] 滑动验证超时, 可重新上线再试");
986
+ }
987
+ } catch (err) {
988
+ this.#emit("failed", "滑动验证失败", { message: err?.message ?? err });
989
+ this.logger("error", `[登录] NT 滑动验证失败: ${err?.message ?? err}`);
990
+ }
991
+ }
992
+ /** 通道2: 网页反代(WS) — 浏览器页面内的验证码请求经 GT 服务转发到本端代发, 兼容新式验证链接 */
993
+ async #sliderViaGTProxy(api, sliderUrl) {
994
+ let ws;
995
+ try {
996
+ const mod = await import("ws");
997
+ ws = new mod.WebSocket(api);
998
+ } catch {
999
+ return false;
1000
+ }
1001
+ const opened = await new Promise((resolve) => {
1002
+ const timer = setTimeout(() => {
1003
+ try {
1004
+ ws?.terminate?.();
1005
+ } catch {}
1006
+ resolve(false);
1007
+ }, 1e4);
1008
+ ws.on("open", () => {
1009
+ clearTimeout(timer);
1010
+ resolve(true);
1011
+ });
1012
+ ws.on("error", () => {
1013
+ clearTimeout(timer);
1014
+ resolve(false);
1015
+ });
1016
+ ws.on("close", () => {
1017
+ clearTimeout(timer);
1018
+ resolve(false);
1019
+ });
1020
+ });
1021
+ if (!opened) return false;
1022
+ this.logger("warn", `[登录] 请打开完成滑动: ${api}`);
1023
+ this.#emit("slider", "需要滑动验证 (GT 网页反代)", {
1024
+ message: "请点击打开链接,在新窗口完成滑动",
1025
+ url: api
1026
+ });
1027
+ ws.send(JSON.stringify({
1028
+ type: "register",
1029
+ payload: { url: sliderUrl }
1030
+ }));
1031
+ return await new Promise((resolve) => {
1032
+ let done = false;
1033
+ const timeout = setTimeout(() => close(), 18e4);
1034
+ const close = (ok = false) => {
1035
+ if (done) return;
1036
+ done = true;
1037
+ clearTimeout(timeout);
1038
+ try {
1039
+ ws.terminate?.();
1040
+ } catch {}
1041
+ resolve(ok);
1042
+ };
1043
+ ws.on("message", async (raw) => {
1044
+ let data;
1045
+ try {
1046
+ data = JSON.parse(raw.toString());
1047
+ } catch {
1048
+ return;
1049
+ }
1050
+ switch (data?.type) {
1051
+ case "ticket": {
1052
+ const ticket = data.payload?.ticket;
1053
+ if (ticket) {
1054
+ this.logger("info", "[登录] 已获取ticket");
1055
+ await this.#submitSlider(ticket);
1056
+ close(true);
1057
+ }
1058
+ break;
1059
+ }
1060
+ case "handle": {
1061
+ const { url: hUrl, ...opts } = data.payload ?? {};
1062
+ const gf = globalThis.fetch;
1063
+ if (!hUrl || typeof gf !== "function") break;
1064
+ try {
1065
+ const req = await gf(hUrl, opts);
1066
+ data.payload = {
1067
+ result: Buffer.from(await req.arrayBuffer()).toString("base64"),
1068
+ headers: Object.fromEntries(req.headers.entries?.() ?? [])
1069
+ };
1070
+ ws.send(JSON.stringify(data));
1071
+ } catch {}
1072
+ break;
1073
+ }
1074
+ default: break;
1075
+ }
1076
+ });
1077
+ ws.on("close", () => close());
1078
+ ws.on("error", () => close());
1079
+ });
1080
+ }
1081
+ /** 通道2: GT 网页验证 — 注册滑块链接, 浏览器访问服务端页面完成滑动, 轮询获取 ticket */
1082
+ async #sliderViaGT(url) {
1083
+ const gf = globalThis.fetch;
1084
+ if (typeof gf !== "function") return;
1085
+ const uin = String(this.cfg.uin ?? "");
1086
+ const api = `https://GT.928100.xyz/captcha/slider?key=${uin}`;
1087
+ if (await this.#sliderViaGTProxy(api, url)) return;
1088
+ try {
1089
+ await gf(api, {
1090
+ method: "POST",
1091
+ headers: { "Content-Type": "application/json" },
1092
+ body: JSON.stringify({ url })
1093
+ });
1094
+ } catch {
1095
+ return;
1096
+ }
1097
+ this.logger("warn", `[登录] 请打开完成滑动: ${api}`);
1098
+ this.#emit("slider", "需要滑动验证 (GT 网页)", {
1099
+ message: "请点击打开链接,在新窗口完成滑动",
1100
+ url: api
1101
+ });
1102
+ for (let i = 0; i < 60; i++) {
1103
+ await new Promise((r) => setTimeout(r, 3e3));
1104
+ try {
1105
+ const res = await (await gf(api, {
1106
+ method: "POST",
1107
+ headers: { "Content-Type": "application/json" },
1108
+ body: JSON.stringify({ submit: uin })
1109
+ })).json();
1110
+ const body = res?.data && typeof res.data === "object" ? res.data : res;
1111
+ if (body?.ticket) {
1112
+ const ticket = body.randstr ? `${body.ticket},${body.randstr}` : body.ticket;
1113
+ this.logger("info", "[登录] 已获取ticket");
1114
+ return this.#submitSlider(ticket);
1115
+ }
1116
+ } catch {}
1117
+ }
1118
+ }
1119
+ /** 通道1: txhelper 请求码 — 浏览器输入请求码完成滑动, 轮询原链接获取 ticket */
1120
+ async #sliderViaTxHelper(url) {
1121
+ const gf = globalThis.fetch;
1122
+ if (typeof gf !== "function") return;
1123
+ const helpUrl = url.replace("ssl.captcha.qq.com", "txhelper.glitch.me");
1124
+ try {
1125
+ const code = (await (await gf(helpUrl)).text()).trim();
1126
+ if (!code || /^https?:/i.test(code) || code.length > 32) return;
1127
+ this.logger("warn", `[登录] 请在 txhelper.glitch.me 输入请求码: ${code}`);
1128
+ this.#emit("slider", "需要滑动验证 (txhelper)", {
1129
+ message: `请在 txhelper.glitch.me 输入请求码: ${code}`,
1130
+ url: helpUrl
1131
+ });
1132
+ for (let i = 0; i < 60; i++) {
1133
+ await new Promise((r) => setTimeout(r, 3e3));
1134
+ const res = (await (await gf(helpUrl)).text()).trim();
1135
+ if (res && res !== code) return this.#submitSlider(res);
1136
+ }
1137
+ } catch {}
1138
+ }
1139
+ /** 通道3: 手动兜底 — 轮询等待 ticket 文件 (用户完成验证后将 ticket,randstr 写入) */
1140
+ async #sliderViaTicketFile() {
1141
+ const dirPath = this.#dataDir;
1142
+ const file = path.join(dirPath, "slider.ticket");
1143
+ this.logger("warn", `[登录] 完成后将 ticket 写入: ${file}`);
1144
+ this.#emit("slider", "需要滑动验证 (手动)", { message: `验证完成后将 ticket,randstr 写入: ${file}` });
1145
+ for (let i = 0; i < 100; i++) {
1146
+ await new Promise((r) => setTimeout(r, 2e3));
1147
+ try {
1148
+ if (!fs.existsSync(file)) continue;
1149
+ const ticket = fs.readFileSync(file, "utf-8").trim();
1150
+ if (!ticket) continue;
1151
+ fs.rmSync(file, { force: true });
1152
+ return this.#submitSlider(ticket);
1153
+ } catch {}
1154
+ }
1155
+ }
1156
+ /** 重置登录超时计时 (滑块/设备/登录验证环节等待较久时调用, 避免被超时打断) */
1157
+ #resetLoginTimeout(ms = 6e5) {
1158
+ if (this.#loginTimer) clearTimeout(this.#loginTimer);
1159
+ this.#loginTimer = setTimeout(() => {
1160
+ this.#loginTimer = null;
1161
+ if (this.#loginResolve) {
1162
+ this.#loginResolve = null;
1163
+ this.#loginReject?.(new Error("登录超时"));
1164
+ }
1165
+ }, ms).unref?.();
1166
+ }
1167
+ /**
1168
+ * 被踢下线/登录失效后延迟手动重连 (icqq 的 kickoff 不会自动重连):
1169
+ * 延迟避免刚被踢就立刻撞上服务器风控; 重新 login 由 icqq 内部走 token → 密码降级,
1170
+ * 若再次触发设备锁/滑块验证会由对应事件处理并推送到 WebUI 继续引导。
1171
+ */
1172
+ #scheduleRelogin(delayMs = 5e3) {
1173
+ if (this.#stopped) return;
1174
+ if (this.#loginRetryTimer) clearTimeout(this.#loginRetryTimer);
1175
+ this.#loginRetryTimer = setTimeout(() => {
1176
+ this.#loginRetryTimer = null;
1177
+ const c = this.super;
1178
+ if (typeof c.login !== "function" || c.isOnline()) return;
1179
+ const uin = this.cfg.uin !== undefined && this.cfg.uin !== "" ? Number(this.cfg.uin) : undefined;
1180
+ this.logger("warn", `[登录] 被踢下线, ${delayMs / 1e3}秒后尝试重新登录...`);
1181
+ const p = this.cfg.password ? c.login(uin, this.cfg.password) : c.login(uin);
1182
+ if (p?.catch) p.catch((err) => this.logger("error", `[登录] 重新登录失败: ${err?.message ?? err}`));
1183
+ }, delayMs).unref?.();
1184
+ }
1185
+ /** system.login.auth — 设备/登录验证自动化 (流程同 TRSS: 928100.xyz 验证服务) */
1186
+ async #onAuth(e) {
1187
+ this.#resetLoginTimeout();
1188
+ const uin = String(this.cfg.uin ?? "");
1189
+ const gf = globalThis.fetch;
1190
+ if (typeof gf !== "function") {
1191
+ this.logger("warn", `[登录] 需要登录验证: ${e?.url}`);
1192
+ return;
1193
+ }
1194
+ const api = (path) => `https://captcha-api.928100.xyz/${path}?uin=${uin}`;
1195
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1196
+ const authUrl = `https://Auth.928100.xyz?uin=${uin}`;
1197
+ this.#emit("auth", "需要登录验证", {
1198
+ message: "请点击打开链接,在新窗口完成验证",
1199
+ url: authUrl
1200
+ });
1201
+ this.logger("warn", `[登录] 请打开链接完成登录验证: ${authUrl}`);
1202
+ const checkStatus = async () => {
1203
+ for (let i = 0; i < 60; i++) {
1204
+ try {
1205
+ const res = await (await gf(api("get-verify-status"))).json();
1206
+ if (res?.status === 1) return true;
1207
+ if (res?.status !== 0) this.logger("warn", `[登录] 验证错误: ${JSON.stringify(res)}`);
1208
+ } catch (err) {
1209
+ this.logger("error", `[登录] 验证请求错误: ${err?.message ?? err}`);
1210
+ }
1211
+ await sleep(5e3);
1212
+ }
1213
+ return false;
1214
+ };
1215
+ try {
1216
+ const res = await (await gf(api("query-bound-phone"))).json();
1217
+ if (res?.retcode === 0) {
1218
+ this.logger("info", "[登录] 存在设备缓存, 正在验证...");
1219
+ if (await checkStatus()) return this.#relogin();
1220
+ return this.#authFail();
1221
+ }
1222
+ } catch (err) {
1223
+ this.logger("warn", `[登录] 查询设备缓存失败: ${err?.message ?? err}`);
1224
+ }
1225
+ let ticket;
1226
+ for (let i = 0; i < 60; i++) {
1227
+ try {
1228
+ const res = await (await gf(api("get-ticket"))).json();
1229
+ if (res?.status === 0 && res.data?.ticket && res.data?.randstr) {
1230
+ ticket = res.data;
1231
+ break;
1232
+ }
1233
+ if (res?.status !== 1) this.logger("warn", `[登录] ticket 错误: ${JSON.stringify(res)}`);
1234
+ } catch (err) {
1235
+ this.logger("error", `[登录] ticket 请求错误: ${err?.message ?? err}`);
1236
+ }
1237
+ await sleep(5e3);
1238
+ }
1239
+ if (!ticket) return this.#authFail("ticket 验证超时");
1240
+ const sig = decodeURIComponent(String(e?.url ?? "")).match(/sig=([^&]+)/)?.[1];
1241
+ const payload = {
1242
+ version: this.super.apk?.ver,
1243
+ sig,
1244
+ ticket: ticket.ticket,
1245
+ randstr: ticket.randstr,
1246
+ guid: e?.device?.guid,
1247
+ qimei: e?.device?.qimei,
1248
+ appid: e?.device?.subappid
1249
+ };
1250
+ this.logger("info", "[登录] 上传设备信息");
1251
+ for (let i = 0; i < 5; i++) {
1252
+ try {
1253
+ const res = await (await gf(api("upload-qqInfo"), {
1254
+ method: "POST",
1255
+ headers: { "Content-Type": "application/json" },
1256
+ body: JSON.stringify(payload)
1257
+ })).json();
1258
+ if (res?.status === 0) {
1259
+ this.#emit("progress", "设备信息上传成功", { message: "正在等待验证结果..." });
1260
+ this.logger("info", "[登录] 设备信息上传成功, 正在验证...");
1261
+ if (await checkStatus()) return this.#relogin();
1262
+ return this.#authFail();
1263
+ }
1264
+ this.logger("warn", `[登录] 设备信息错误: ${JSON.stringify(res)}`);
1265
+ } catch (err) {
1266
+ this.logger("error", `[登录] 设备信息上传错误: ${err?.message ?? err}`);
1267
+ }
1268
+ await sleep(5e3);
1269
+ }
1270
+ return this.#authFail("设备信息上传超时");
1271
+ }
1272
+ /** 登录验证通过后重新登录 */
1273
+ async #relogin() {
1274
+ const uin = this.cfg.uin !== undefined && this.cfg.uin !== "" ? Number(this.cfg.uin) : undefined;
1275
+ this.#emit("relogin", "验证已通过", { message: "正在重新登录..." });
1276
+ try {
1277
+ await this.super.login(uin, this.cfg.password);
1278
+ this.logger("info", "[登录] 验证已通过, 重新登录中...");
1279
+ } catch (err) {
1280
+ this.logger("warn", `[登录] 重新登录失败: ${err?.message ?? err}`);
1281
+ }
1282
+ }
1283
+ /** 登录验证流程失败 */
1284
+ #authFail(reason = "登录验证超时") {
1285
+ this.#emit("failed", "登录验证失败", { message: reason });
1286
+ this.logger("warn", `[登录] ${reason}`);
1287
+ this.#loginReject?.(new Error(reason));
1288
+ }
1289
+ /** 提交滑块 ticket (多通道互斥, 成功后其余通道自动放弃) */
1290
+ async #submitSlider(ticket) {
1291
+ if (this.#sliderSubmitted) return;
1292
+ this.#sliderSubmitted = true;
1293
+ try {
1294
+ await this.super.submitSlider(ticket);
1295
+ this.#emit("submit", "滑动验证已通过", { message: "正在继续登录..." });
1296
+ this.logger("info", "[登录] 已提交滑动验证, 正在继续登录...");
1297
+ } catch (err) {
1298
+ this.#sliderSubmitted = false;
1299
+ this.logger("error", `[登录] 滑动验证提交失败: ${err?.message ?? err}`);
1300
+ this.#loginReject?.(new Error(`滑动验证提交失败: ${err?.message ?? err}`));
1301
+ }
1302
+ }
1303
+ /** 启动: 绑定事件 + 登录 */
1304
+ async start() {
1305
+ if (this.#stopped) this.#stopped = false;
1306
+ this.#bindEvents();
1307
+ if (this.super.isOnline()) {
1308
+ this.#onOnline();
1309
+ return;
1310
+ }
1311
+ const uin = this.cfg.uin !== undefined && this.cfg.uin !== "" ? Number(this.cfg.uin) : undefined;
1312
+ const password = this.cfg.password;
1313
+ const loginType = this.cfg.loginType || (password ? "password" : uin ? "fast" : "qrcode");
1314
+ this.#loginDone = new Promise((resolve, reject) => {
1315
+ this.#loginResolve = resolve;
1316
+ this.#loginReject = reject;
1317
+ this.#resetLoginTimeout();
1318
+ });
1319
+ try {
1320
+ this.#lastQsignError = "";
1321
+ const loginTask = (async () => {
1322
+ if (loginType === "password") {
1323
+ if (!uin || !password) throw new Error("密码登录需要配置 uin 和 password");
1324
+ await this.super.login(uin, password);
1325
+ } else if (loginType === "fast") {
1326
+ if (!uin) throw new Error("快速登录需要配置 uin");
1327
+ await this.super.login(uin);
1328
+ } else {
1329
+ if (this.cfg.platform && this.cfg.platform !== 3) {
1330
+ this.logger("warn", "[登录] 扫码登录建议使用 Watch 平台 (platform=3)");
1331
+ }
1332
+ await this.super.login();
1333
+ }
1334
+ })();
1335
+ this.logger("info", `[登录] 正在登录 ${uin ?? "(扫码)"} (${loginType})...`);
1336
+ const hangGuard = new Promise((_, reject) => {
1337
+ const timer = setTimeout(() => reject(new Error("登录请求无响应超时")), 6e4);
1338
+ timer.unref?.();
1339
+ });
1340
+ await Promise.race([loginTask, hangGuard]);
1341
+ } catch (err) {
1342
+ if (err?.code === -90 || /签名api/.test(String(err?.message ?? ""))) {
1343
+ const detail = err?.stack ?? (err && typeof err === "object" ? JSON.stringify(err) : String(err ?? ""));
1344
+ const qsign = this.#lastQsignError ? ` | ${this.#lastQsignError}` : "";
1345
+ this.logger("error", `[登录] 签名api异常: ${detail}${qsign}`);
1346
+ }
1347
+ if (this.#loginTimer) {
1348
+ clearTimeout(this.#loginTimer);
1349
+ this.#loginTimer = null;
1350
+ }
1351
+ throw err;
1352
+ }
1353
+ await this.#loginDone;
1354
+ }
1355
+ /** 停止: 登出并注销 (热更新/停用时调用) */
1356
+ async stop() {
1357
+ this.#stopped = true;
1358
+ if (this.#loginRetryTimer) {
1359
+ clearTimeout(this.#loginRetryTimer);
1360
+ this.#loginRetryTimer = null;
1361
+ }
1362
+ this.unregister();
1363
+ clearLoginState(String(this.cfg.uin ?? this.super?.uin ?? ""));
1364
+ if (this.#connectTimeTimer) {
1365
+ clearInterval(this.#connectTimeTimer);
1366
+ this.#connectTimeTimer = null;
1367
+ }
1368
+ try {
1369
+ await this.super.logout();
1370
+ } catch {}
1371
+ }
1372
+ async sendMsg(contact, elements) {
1373
+ const result = {
1374
+ messageId: "",
1375
+ time: 0,
1376
+ rawData: {},
1377
+ message_id: "",
1378
+ messageTime: 0
1379
+ };
1380
+ const { message, source } = await KarinConvertAdapter$1(elements);
1381
+ let ret;
1382
+ if (contact.scene === "group") {
1383
+ ret = await this.super.sendGroupMsg(Number(contact.peer), message, source);
1384
+ } else if (contact.scene === "friend") {
1385
+ ret = await this.super.sendPrivateMsg(Number(contact.peer), message, source);
1386
+ } else if (contact.scene === "groupTemp") {
1387
+ ret = await this.super.sendPrivateMsg(Number(contact.peer), message, source);
1388
+ } else {
1389
+ throw new Error(`不支持的场景: ${contact.scene}`);
1390
+ }
1391
+ result.messageId = ret.message_id;
1392
+ result.time = ret.time;
1393
+ result.rawData = ret;
1394
+ result.message_id = ret.message_id;
1395
+ result.messageTime = ret.time;
1396
+ return result;
1397
+ }
1398
+ async sendForwardMsg(contact, elements, _options) {
1399
+ const userId = Number(this.account.selfId) || 0;
1400
+ const nickname = this.account.name;
1401
+ const nodes = [];
1402
+ for (const v of elements) {
1403
+ const nodeUserId = v.subType === "fake" ? Number(v.userId) : userId;
1404
+ const nodeNickname = v.subType === "fake" ? v.nickname : nickname;
1405
+ if (v.subType === "fake") {
1406
+ const { message } = await KarinConvertAdapter$1(v.message);
1407
+ nodes.push(segment$1.fake(nodeUserId, message, nodeNickname));
1408
+ } else {
1409
+ try {
1410
+ const msgs = await this.getForwardMsg(v.messageId);
1411
+ const first = msgs[0];
1412
+ const { message } = first ? await KarinConvertAdapter$1(first.elements) : { message: [""] };
1413
+ nodes.push(segment$1.fake(nodeUserId, message, nodeNickname));
1414
+ } catch {
1415
+ nodes.push(segment$1.fake(nodeUserId, [""], nodeNickname));
1416
+ }
1417
+ }
1418
+ }
1419
+ if (nodes.length === 0) throw new Error("合并转发内容为空");
1420
+ const result = {
1421
+ messageId: "",
1422
+ forwardId: ""
1423
+ };
1424
+ const ret = contact.scene === "friend" ? await this.super.sendPrivateMsg(Number(contact.peer), nodes) : await this.super.sendGroupMsg(Number(contact.peer), nodes);
1425
+ result.messageId = ret.message_id;
1426
+ return result;
1427
+ }
1428
+ async recallMsg(contact, messageId) {
1429
+ await this.super.deleteMsg(messageId);
1430
+ }
1431
+ async getMsg(Contact, messageId) {
1432
+ const mid = typeof Contact === "string" ? Contact : messageId;
1433
+ const m = await this.super.getMsg(mid);
1434
+ if (!m) throw new Error(`消息不存在: ${mid}`);
1435
+ return await this.#toMessageResponse(m);
1436
+ }
1437
+ async getHistoryMsg(contact, startMsgSeq, count) {
1438
+ const result = [];
1439
+ if (contact.scene === "group") {
1440
+ const seq = typeof startMsgSeq === "string" ? parseSeq(startMsgSeq) : startMsgSeq;
1441
+ const msgs = await this.super.pickGroup(Number(contact.peer)).getChatHistory(seq > 0 ? seq : undefined, count);
1442
+ for (const m of msgs) result.push(await this.#toMessageResponse(m));
1443
+ } else {
1444
+ const seq = typeof startMsgSeq === "string" ? parseSeq(startMsgSeq) : startMsgSeq;
1445
+ const msgs = await this.super.pickFriend(Number(contact.peer)).getChatHistory(seq > 0 ? seq : undefined, count);
1446
+ for (const m of msgs) result.push(await this.#toMessageResponse(m));
1447
+ }
1448
+ return result;
1449
+ }
1450
+ async getForwardMsg(resId) {
1451
+ const scene = getResidScene(resId);
1452
+ const msgs = scene ? await (scene.scene === "group" ? this.super.pickGroup(Number(scene.peer)) : this.super.pickFriend(Number(scene.peer))).getForwardMsg(resId) : await this.super.getForwardMsg(resId);
1453
+ const result = [];
1454
+ for (const m of msgs) {
1455
+ result.push(await this.#toMessageResponse(m));
1456
+ }
1457
+ return result;
1458
+ }
1459
+ async setMsgReaction(contact, messageId, faceId, isSet) {
1460
+ if (contact.scene !== "group") throw new Error("仅支持群聊设置表情回应");
1461
+ const info = parseMessageId(messageId);
1462
+ if (!info) throw new Error(`无法解析消息 id: ${messageId}`);
1463
+ const group = this.super.pickGroup(info.group_id);
1464
+ if (isSet) await group.setReaction(info.seq, String(faceId));
1465
+ else await group.delReaction(info.seq, String(faceId));
1466
+ }
1467
+ async groupKickMember(groupId, targetId, rejectAddRequest, _kickReason) {
1468
+ await this.super.setGroupKick(Number(groupId), Number(targetId), rejectAddRequest);
1469
+ }
1470
+ async setGroupMute(groupId, targetId, duration) {
1471
+ await this.super.setGroupBan(Number(groupId), Number(targetId), duration);
1472
+ }
1473
+ async setGroupAllMute(groupId, isBan) {
1474
+ await this.super.setGroupWholeBan(Number(groupId), isBan);
1475
+ }
1476
+ /** 发送群公告 (karin 无标准接口, icqq SDK Group.announce, 需管理员权限) */
1477
+ async groupAnnounce(groupId, content) {
1478
+ return this.super.pickGroup(Number(groupId)).announce(content);
1479
+ }
1480
+ async setGroupAdmin(groupId, targetId, isAdmin) {
1481
+ await this.super.setGroupAdmin(Number(groupId), Number(targetId), isAdmin);
1482
+ }
1483
+ async setGroupMemberCard(groupId, targetId, card) {
1484
+ await this.super.setGroupCard(Number(groupId), Number(targetId), card);
1485
+ }
1486
+ async setGroupName(groupId, groupName) {
1487
+ await this.super.setGroupName(Number(groupId), groupName);
1488
+ }
1489
+ async setGroupQuit(groupId, isDismiss) {
1490
+ const info = await this.super.getGroupMemberInfo(Number(groupId), Number(this.account.selfId || 0));
1491
+ if (info && info.role === "owner" && !isDismiss) return;
1492
+ await this.super.setGroupLeave(Number(groupId));
1493
+ }
1494
+ async setGroupMemberTitle(groupId, targetId, title) {
1495
+ await this.super.setGroupSpecialTitle(Number(groupId), Number(targetId), title);
1496
+ }
1497
+ async getGroupInfo(groupId, noCache) {
1498
+ const info = await this.super.getGroupInfo(Number(groupId), noCache);
1499
+ let admins = [];
1500
+ try {
1501
+ for (const [uid, m] of await this.super.getGroupMemberList(Number(groupId), noCache)) {
1502
+ if (m.role === "owner" || m.role === "admin") {
1503
+ admins.push({
1504
+ userId: String(uid),
1505
+ name: m.card || m.nickname,
1506
+ role: m.role
1507
+ });
1508
+ }
1509
+ }
1510
+ } catch {}
1511
+ return {
1512
+ groupId: String(info.group_id),
1513
+ groupName: info.group_name,
1514
+ maxMemberCount: info.max_member_count,
1515
+ memberCount: info.member_count,
1516
+ admins,
1517
+ avatar: `https://p.qlogo.cn/gh/${groupId}/${groupId}/640`
1518
+ };
1519
+ }
1520
+ async getGroupList(_refresh) {
1521
+ const groups = [];
1522
+ for (const [gid, info] of this.super.getGroupList()) {
1523
+ groups.push({
1524
+ groupId: String(gid),
1525
+ groupName: info.group_name,
1526
+ maxMemberCount: info.max_member_count,
1527
+ memberCount: info.member_count,
1528
+ admins: [],
1529
+ avatar: `https://p.qlogo.cn/gh/${gid}/${gid}/0`
1530
+ });
1531
+ }
1532
+ return groups;
1533
+ }
1534
+ async getGroupMemberInfo(groupId, targetId, refresh) {
1535
+ const m = await this.super.getGroupMemberInfo(Number(groupId), Number(targetId), refresh);
1536
+ return {
1537
+ userId: String(m.user_id),
1538
+ role: m.role,
1539
+ nick: m.nickname,
1540
+ age: m.age ?? 0,
1541
+ uniqueTitle: m.title,
1542
+ card: m.card,
1543
+ joinTime: m.join_time,
1544
+ lastActiveTime: m.last_sent_time,
1545
+ level: m.level,
1546
+ shutUpTime: m.shutup_time || undefined,
1547
+ sex: m.sex,
1548
+ sender: {
1549
+ userId: String(m.user_id),
1550
+ nick: m.nickname,
1551
+ name: m.nickname,
1552
+ role: m.role,
1553
+ card: m.card,
1554
+ level: m.level,
1555
+ title: m.title
1556
+ }
1557
+ };
1558
+ }
1559
+ async getGroupMemberList(groupId, refresh) {
1560
+ const map = await this.super.getGroupMemberList(Number(groupId), refresh);
1561
+ const info = [];
1562
+ for (const [uid, m] of map) {
1563
+ info.push({
1564
+ userId: String(uid),
1565
+ role: m.role,
1566
+ nick: m.nickname,
1567
+ age: m.age ?? 0,
1568
+ uniqueTitle: m.title,
1569
+ card: m.card,
1570
+ joinTime: m.join_time,
1571
+ lastActiveTime: m.last_sent_time,
1572
+ level: m.level,
1573
+ shutUpTime: m.shutup_time || undefined,
1574
+ sex: m.sex,
1575
+ sender: {
1576
+ userId: String(uid),
1577
+ nick: m.nickname,
1578
+ name: m.nickname,
1579
+ role: m.role,
1580
+ card: m.card,
1581
+ level: m.level,
1582
+ title: m.title
1583
+ }
1584
+ });
1585
+ }
1586
+ return info;
1587
+ }
1588
+ async getGroupHighlights(_groupId, _page, _pageSize) {
1589
+ return [];
1590
+ }
1591
+ async setGroupHighlights(groupId, messageId, create) {
1592
+ if (create) {
1593
+ await this.super.setEssenceMessage(messageId);
1594
+ } else {
1595
+ await this.super.removeEssenceMessage(messageId);
1596
+ }
1597
+ }
1598
+ async getStrangerInfo(targetId) {
1599
+ const info = await this.super.pickUser(Number(targetId)).getSimpleInfo();
1600
+ return {
1601
+ userId: String(info.user_id),
1602
+ nick: info.nickname,
1603
+ qid: "",
1604
+ remark: "",
1605
+ level: 0,
1606
+ age: info.age,
1607
+ sex: info.sex
1608
+ };
1609
+ }
1610
+ async getFriendList(_refresh) {
1611
+ const info = [];
1612
+ for (const [uid, f] of this.super.getFriendList()) {
1613
+ info.push({
1614
+ userId: String(uid),
1615
+ nick: f.nickname,
1616
+ qid: "",
1617
+ remark: f.remark,
1618
+ sex: f.sex
1619
+ });
1620
+ }
1621
+ return info;
1622
+ }
1623
+ async sendLike(targetId, count) {
1624
+ await this.super.sendLike(Number(targetId), count);
1625
+ }
1626
+ async getAvatarUrl(userId, size) {
1627
+ return `https://q1.qlogo.cn/g?b=qq&s=${size || 0}&nk=${userId}`;
1628
+ }
1629
+ async getGroupAvatarUrl(groupId, size, _history) {
1630
+ return `https://p.qlogo.cn/gh/${groupId}/${groupId}/${size || 0}`;
1631
+ }
1632
+ async pokeUser(contact, targetId, count = 1) {
1633
+ let pokeFunc;
1634
+ if (contact.scene === "group") pokeFunc = () => this.super.sendGroupPoke(Number(contact.peer), Number(targetId));
1635
+ if (contact.scene === "friend") pokeFunc = () => this.super.sendGroupPoke(Number(this.super.uin || this.cfg.uin), Number(targetId)).then(() => true);
1636
+ if (!pokeFunc) throw new Error(`不支持的场景: ${contact.scene}`);
1637
+ for (let i = 0; i < count; i++) await pokeFunc();
1638
+ return true;
1639
+ }
1640
+ async setFriendApplyResult(requestId, isApprove, remark) {
1641
+ await this.super.setFriendAddRequest(requestId, isApprove, remark);
1642
+ }
1643
+ async setGroupApplyResult(requestId, isApprove, denyReason) {
1644
+ await this.super.setGroupAddRequest(requestId, isApprove, denyReason);
1645
+ }
1646
+ async setInvitedJoinGroupResult(requestId, isApprove) {
1647
+ await this.super.setGroupAddRequest(requestId, isApprove);
1648
+ }
1649
+ async getFileUrl(contact, fileId) {
1650
+ if (contact.scene === "group") {
1651
+ const res = await this.super.pickGroup(Number(contact.peer)).getFileUrl(fileId);
1652
+ return res;
1653
+ }
1654
+ const res = await this.super.pickFriend(Number(contact.peer)).getFileUrl(fileId);
1655
+ return res;
1656
+ }
1657
+ /** 获取视频播放地址 (NT 视频由会话接口自行解码 protobuf 载荷, 普通视频用 fid+md5) */
1658
+ async getVideoUrl(contact, elem) {
1659
+ if (elem.nt) {
1660
+ const target = contact.scene === "group" ? this.super.pickGroup(Number(contact.peer)) : this.super.pickFriend(Number(contact.peer));
1661
+ return target.getNTVideoUrl(elem);
1662
+ }
1663
+ if (!elem.fid || !elem.md5) return null;
1664
+ return this.super.getVideoUrl(String(elem.fid), Buffer.from(String(elem.md5), "base64"));
1665
+ }
1666
+ /** 获取语音播放地址 (带 fid 走会话 NT 接口, 否则用收侧自带 url) */
1667
+ async getRecordUrl(contact, elem) {
1668
+ const target = contact.scene === "group" ? this.super.pickGroup(Number(contact.peer)) : this.super.pickFriend(Number(contact.peer));
1669
+ return await target.getPttUrl(elem) ?? null;
1670
+ }
1671
+ async uploadFile(contact, file, name, folder) {
1672
+ if (contact.scene === "group") {
1673
+ await this.super.pickGroup(Number(contact.peer)).uploadFile(file, folder ?? "/", name);
1674
+ } else {
1675
+ await this.super.pickFriend(Number(contact.peer)).sendFile(file, name);
1676
+ }
1677
+ }
1678
+ async uploadGroupFile(groupId, file, name) {
1679
+ await this.super.pickGroup(Number(groupId)).uploadFile(file, "/", name || "");
1680
+ return true;
1681
+ }
1682
+ async delGroupFile(groupId, fileId) {
1683
+ await this.super.pickGroup(Number(groupId)).fs.rm(fileId);
1684
+ return true;
1685
+ }
1686
+ async getGroupFileList(groupId, folderId) {
1687
+ const fs = this.super.pickGroup(Number(groupId)).fs;
1688
+ const list = await fs.dir(folderId || "/", 0, 100);
1689
+ return {
1690
+ files: list.filter((v) => !v.is_dir).map((v) => ({
1691
+ fid: v.fid,
1692
+ name: v.name,
1693
+ size: v.size ?? 0,
1694
+ uploadTime: v.create_time,
1695
+ expireTime: v.duration ?? 0,
1696
+ modifyTime: v.modify_time,
1697
+ downloadCount: v.download_times ?? 0,
1698
+ uploadId: String(v.user_id ?? ""),
1699
+ uploadName: "",
1700
+ sha1: v.sha1 ?? "",
1701
+ sha3: "",
1702
+ md5: v.md5 ?? ""
1703
+ })),
1704
+ folders: list.filter((v) => v.is_dir).map((v) => ({
1705
+ id: v.fid,
1706
+ name: v.name,
1707
+ fileCount: v.file_count ?? 0,
1708
+ createTime: v.create_time,
1709
+ creatorId: String(v.user_id ?? ""),
1710
+ creatorName: ""
1711
+ }))
1712
+ };
1713
+ }
1714
+ async createGroupFolder(groupId, name) {
1715
+ const stat = await this.super.pickGroup(Number(groupId)).fs.mkdir(name);
1716
+ return {
1717
+ id: stat.fid,
1718
+ usedSpace: "0"
1719
+ };
1720
+ }
1721
+ async renameGroupFolder(groupId, folderId, name) {
1722
+ await this.super.pickGroup(Number(groupId)).fs.rename(folderId, name);
1723
+ return true;
1724
+ }
1725
+ async delGroupFolder(groupId, folderId) {
1726
+ await this.super.pickGroup(Number(groupId)).fs.rm(folderId);
1727
+ return true;
1728
+ }
1729
+ async getCookies(domain) {
1730
+ return { cookie: this.super.getCookies(domain) };
1731
+ }
1732
+ async getCredentials(domain) {
1733
+ const cookies = (await this.getCookies(domain)).cookie;
1734
+ const token = (await this.getCSRFToken()).token;
1735
+ return {
1736
+ cookies,
1737
+ csrf_token: token
1738
+ };
1739
+ }
1740
+ async getCSRFToken() {
1741
+ const token = this.super.getCsrfToken();
1742
+ return { token: Number.isFinite(token) ? token : 0 };
1743
+ }
1744
+ async setAvatar(uri) {
1745
+ await this.super.setPortrait(uri);
1746
+ }
1747
+ };
1748
+ /** 从 cqhttp message_id 解析 seq */
1749
+ const parseSeq = (messageId) => {
1750
+ const q = parseQuotable(messageId);
1751
+ return q?.seq ?? 0;
1752
+ };
1753
+ /** 解析 cqhttp message_id → { group_id, seq } (仅供群场景) */
1754
+ const parseMessageId = (messageId) => {
1755
+ try {
1756
+ const info = parseGroupMessageId(messageId);
1757
+ return {
1758
+ group_id: info.group_id,
1759
+ seq: info.seq
1760
+ };
1761
+ } catch {
1762
+ return undefined;
1763
+ }
1764
+ };
1765
+ /** icqq 协议 bot 工厂 */
1766
+ const createIcqqBot = (cfg) => {
1767
+ return new IcqqBot(cfg);
1768
+ };
1769
+
1770
+ //#endregion
1771
+ export { createIcqqBot };