@onebots/adapter-icqq 1.0.0 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/adapter.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * ICQQ 适配器
3
- * 继承 Adapter 基类,实现 ICQQ 平台功能
4
- */
5
1
  import { Account } from "onebots";
6
2
  import { Adapter } from "onebots";
7
3
  import { BaseApp } from "onebots";
@@ -74,6 +70,18 @@ export declare class ICQQAdapter extends Adapter<ICQQBot, "icqq"> {
74
70
  */
75
71
  getStatus(uin: string): Promise<Adapter.StatusInfo>;
76
72
  createAccount(config: Account.Config<'icqq'>): Account<'icqq', ICQQBot>;
73
+ /**
74
+ * Web 验证提交:将前端提交的滑块 ticket 或短信验证码转交给 ICQQ Bot
75
+ * 支持 data.ticket / data.code(兼容)或通用 data.value
76
+ */
77
+ submitVerification(accountId: string, type: string, data: Record<string, unknown>): void;
78
+ /** 请求向密保手机发送短信验证码(设备锁时用户选短信验证前调用) */
79
+ requestSmsCode(accountId: string): Promise<void>;
80
+ /**
81
+ * 处理 base64:// 前缀的文件数据
82
+ * 如果是 base64 格式,转换为 Buffer;否则返回原始数据
83
+ */
84
+ private processFileData;
77
85
  /**
78
86
  * 构建 ICQQ 消息
79
87
  */
package/lib/adapter.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * ICQQ 适配器
3
3
  * 继承 Adapter 基类,实现 ICQQ 平台功能
4
4
  */
5
- import { Account, AdapterRegistry, AccountStatus } from "onebots";
5
+ import { Buffer } from "node:buffer";
6
+ import { Account, AdapterRegistry, AccountStatus, unixSecondsToEventMs } from "onebots";
6
7
  import { Adapter } from "onebots";
7
8
  import { ICQQBot, segment } from "./bot.js";
8
9
  export class ICQQAdapter extends Adapter {
@@ -21,10 +22,11 @@ export class ICQQAdapter extends Adapter {
21
22
  if (!account)
22
23
  throw new Error(`Account ${uin} not found`);
23
24
  const bot = account.client;
24
- const { scene_id, scene_type, message } = params;
25
+ const { scene_type, message } = params;
26
+ const sceneId = this.coerceId(params.scene_id);
25
27
  // 转换消息格式
26
28
  const icqqMessage = this.buildICQQMessage(message);
27
- const targetId = parseInt(scene_id.string);
29
+ const targetId = parseInt(sceneId.string);
28
30
  let result;
29
31
  if (scene_type === 'private') {
30
32
  result = await bot.sendPrivateMessage(targetId, icqqMessage);
@@ -47,7 +49,7 @@ export class ICQQAdapter extends Adapter {
47
49
  if (!account)
48
50
  throw new Error(`Account ${uin} not found`);
49
51
  const bot = account.client;
50
- await bot.recallMessage(params.message_id.string);
52
+ await bot.recallMessage(this.coerceId(params.message_id).string);
51
53
  }
52
54
  /**
53
55
  * 获取消息
@@ -57,11 +59,12 @@ export class ICQQAdapter extends Adapter {
57
59
  if (!account)
58
60
  throw new Error(`Account ${uin} not found`);
59
61
  const bot = account.client;
60
- const msg = await bot.getMessage(params.message_id.string);
62
+ const msg = await bot.getMessage(this.coerceId(params.message_id).string);
61
63
  const isGroup = !!msg.group_id;
62
64
  return {
63
65
  message_id: this.createId(msg.message_id),
64
- time: msg.time * 1000,
66
+ // MessageInfo.time 约定为 Unix 秒(与 OneBot get_msg 等一致)
67
+ time: msg.time,
65
68
  sender: {
66
69
  scene_type: isGroup ? 'group' : 'private',
67
70
  sender_id: this.createId(msg.user_id.toString()),
@@ -303,18 +306,61 @@ export class ICQQAdapter extends Adapter {
303
306
  });
304
307
  bot.on('qrcode', (event) => {
305
308
  this.logger.info(`ICQQ 请扫描二维码登录`);
306
- // 可以通过事件通知前端显示二维码
307
309
  this.emit('qrcode', { account_id: config.account_id, image: event.image });
310
+ const imageBase64 = event.image instanceof Buffer ? event.image.toString('base64') : event.image;
311
+ this.emit('verification:request', {
312
+ platform: 'icqq',
313
+ account_id: config.account_id,
314
+ type: 'qrcode',
315
+ hint: '请使用手机 QQ 扫描下方二维码登录',
316
+ options: { blocks: [{ type: 'image', base64: imageBase64, alt: '登录二维码' }] },
317
+ });
308
318
  });
309
319
  bot.on('slider', (event) => {
310
320
  this.logger.info(`ICQQ 需要滑块验证: ${event.url}`);
311
- // 可以通过事件通知前端进行滑块验证
312
321
  this.emit('slider', { account_id: config.account_id, url: event.url });
322
+ this.emit('verification:request', {
323
+ platform: 'icqq',
324
+ account_id: config.account_id,
325
+ type: 'slider',
326
+ hint: '请在浏览器中打开下方链接完成滑块验证,完成后将获取的 ticket 填入并提交',
327
+ options: {
328
+ blocks: [
329
+ { type: 'link', url: event.url, label: event.url },
330
+ { type: 'input', key: 'ticket', placeholder: '粘贴 ticket' },
331
+ ],
332
+ },
333
+ });
313
334
  });
314
335
  bot.on('device', (event) => {
315
336
  this.logger.info(`ICQQ 需要设备锁验证: ${event.url}`);
316
- // 可以通过事件通知前端进行设备锁验证
317
337
  this.emit('device', { account_id: config.account_id, url: event.url, phone: event.phone });
338
+ const blocks = [
339
+ { type: 'link', url: event.url, label: event.url },
340
+ ];
341
+ if (event.phone)
342
+ blocks.push({ type: 'text', content: `手机号:${event.phone}` });
343
+ this.emit('verification:request', {
344
+ platform: 'icqq',
345
+ account_id: config.account_id,
346
+ type: 'device',
347
+ hint: '请在浏览器中打开下方链接完成设备锁验证',
348
+ options: { blocks },
349
+ });
350
+ if (event.phone) {
351
+ this.emit('verification:request', {
352
+ platform: 'icqq',
353
+ account_id: config.account_id,
354
+ type: 'sms',
355
+ hint: '使用短信验证:请先点击「发送验证码」,收到后填入 6 位验证码并提交',
356
+ requestSmsAvailable: true,
357
+ options: {
358
+ blocks: [
359
+ { type: 'input', key: 'code', placeholder: '6 位短信验证码', maxLength: 6 },
360
+ ],
361
+ },
362
+ });
363
+ }
318
364
  });
319
365
  bot.on('login_error', (event) => {
320
366
  this.logger.error(`ICQQ 登录失败:`, event);
@@ -331,7 +377,7 @@ export class ICQQAdapter extends Adapter {
331
377
  // 转换为 CommonEvent 格式
332
378
  const commonEvent = {
333
379
  id: this.createId(event.message_id),
334
- timestamp: event.time * 1000,
380
+ timestamp: unixSecondsToEventMs(event.time),
335
381
  platform: 'icqq',
336
382
  bot_id: this.createId(config.account_id),
337
383
  type: 'message',
@@ -359,7 +405,7 @@ export class ICQQAdapter extends Adapter {
359
405
  // 转换为 CommonEvent 格式
360
406
  const commonEvent = {
361
407
  id: this.createId(event.message_id),
362
- timestamp: event.time * 1000,
408
+ timestamp: unixSecondsToEventMs(event.time),
363
409
  platform: 'icqq',
364
410
  bot_id: this.createId(config.account_id),
365
411
  type: 'message',
@@ -384,7 +430,7 @@ export class ICQQAdapter extends Adapter {
384
430
  bot.on('group_increase', (event) => {
385
431
  const noticeEvent = {
386
432
  id: this.createId(`${event.group_id}_${event.user_id}_${event.time}`),
387
- timestamp: event.time * 1000,
433
+ timestamp: unixSecondsToEventMs(event.time),
388
434
  platform: 'icqq',
389
435
  bot_id: this.createId(config.account_id),
390
436
  type: 'notice',
@@ -406,7 +452,7 @@ export class ICQQAdapter extends Adapter {
406
452
  bot.on('group_decrease', (event) => {
407
453
  const noticeEvent = {
408
454
  id: this.createId(`${event.group_id}_${event.user_id}_${event.time}`),
409
- timestamp: event.time * 1000,
455
+ timestamp: unixSecondsToEventMs(event.time),
410
456
  platform: 'icqq',
411
457
  bot_id: this.createId(config.account_id),
412
458
  type: 'notice',
@@ -440,9 +486,68 @@ export class ICQQAdapter extends Adapter {
440
486
  });
441
487
  return account;
442
488
  }
489
+ /**
490
+ * Web 验证提交:将前端提交的滑块 ticket 或短信验证码转交给 ICQQ Bot
491
+ * 支持 data.ticket / data.code(兼容)或通用 data.value
492
+ */
493
+ submitVerification(accountId, type, data) {
494
+ const account = this.getAccount(accountId);
495
+ if (!account) {
496
+ this.logger.warn(`submitVerification: 账号不存在 ${accountId}`);
497
+ return;
498
+ }
499
+ const bot = account.client;
500
+ const value = typeof data.value === 'string' ? data.value : undefined;
501
+ if (type === 'slider') {
502
+ const ticket = (data.ticket ?? value);
503
+ if (typeof ticket === 'string')
504
+ bot.submitSlider(ticket);
505
+ }
506
+ else if (type === 'sms') {
507
+ const code = (data.code ?? value);
508
+ if (typeof code === 'string')
509
+ bot.submitSmsCode(code);
510
+ }
511
+ else {
512
+ this.logger.debug(`submitVerification: 忽略类型 ${type} 或缺少参数`);
513
+ }
514
+ }
515
+ /** 请求向密保手机发送短信验证码(设备锁时用户选短信验证前调用) */
516
+ requestSmsCode(accountId) {
517
+ const account = this.getAccount(accountId);
518
+ if (!account) {
519
+ this.logger.warn(`requestSmsCode: 账号不存在 ${accountId}`);
520
+ return Promise.resolve();
521
+ }
522
+ return account.client.sendSmsCode();
523
+ }
443
524
  // ============================================
444
525
  // 消息转换
445
526
  // ============================================
527
+ /**
528
+ * 处理 base64:// 前缀的文件数据
529
+ * 如果是 base64 格式,转换为 Buffer;否则返回原始数据
530
+ */
531
+ processFileData(file) {
532
+ if (typeof file === 'string' && file.startsWith('base64://')) {
533
+ const base64Data = file.replace(/^base64:\/\//, '');
534
+ // Strip whitespace (RFC 4648 allows whitespace in base64)
535
+ const cleanedData = base64Data.replace(/\s/g, '');
536
+ // Validate base64 format (basic validation)
537
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanedData)) {
538
+ this.logger.warn(`Invalid base64 data format (length: ${cleanedData.length})`);
539
+ return file; // Return original if invalid
540
+ }
541
+ try {
542
+ return Buffer.from(cleanedData, 'base64');
543
+ }
544
+ catch (error) {
545
+ this.logger.error(`Failed to convert base64 to Buffer:`, error);
546
+ return file; // Return original on error
547
+ }
548
+ }
549
+ return file;
550
+ }
446
551
  /**
447
552
  * 构建 ICQQ 消息
448
553
  */
@@ -467,7 +572,7 @@ export class ICQQAdapter extends Adapter {
467
572
  else if (seg.type === 'image') {
468
573
  const file = seg.data.url || seg.data.file;
469
574
  if (file) {
470
- result.push(segment.image(file));
575
+ result.push(segment.image(this.processFileData(file)));
471
576
  }
472
577
  }
473
578
  else if (seg.type === 'face') {
@@ -479,13 +584,13 @@ export class ICQQAdapter extends Adapter {
479
584
  else if (seg.type === 'record' || seg.type === 'audio') {
480
585
  const file = seg.data.url || seg.data.file;
481
586
  if (file) {
482
- result.push(segment.record(file));
587
+ result.push(segment.record(this.processFileData(file)));
483
588
  }
484
589
  }
485
590
  else if (seg.type === 'video') {
486
591
  const file = seg.data.url || seg.data.file;
487
592
  if (file) {
488
- result.push(segment.video(file));
593
+ result.push(segment.video(this.processFileData(file)));
489
594
  }
490
595
  }
491
596
  else if (seg.type === 'reply') {
package/lib/bot.d.ts CHANGED
@@ -135,6 +135,10 @@ export declare class ICQQBot extends EventEmitter {
135
135
  * 提交滑块 ticket
136
136
  */
137
137
  submitSlider(ticket: string): void;
138
+ /**
139
+ * 请求发送短信验证码(设备锁时可选,先调用此方法再提交验证码)
140
+ */
141
+ sendSmsCode(): Promise<void>;
138
142
  /**
139
143
  * 提交短信验证码
140
144
  */
package/lib/bot.js CHANGED
@@ -565,6 +565,14 @@ export class ICQQBot extends EventEmitter {
565
565
  throw new Error('Bot not connected');
566
566
  this.client.submitSlider(ticket);
567
567
  }
568
+ /**
569
+ * 请求发送短信验证码(设备锁时可选,先调用此方法再提交验证码)
570
+ */
571
+ sendSmsCode() {
572
+ if (!this.client)
573
+ throw new Error('Bot not connected');
574
+ return this.client.sendSmsCode();
575
+ }
568
576
  /**
569
577
  * 提交短信验证码
570
578
  */
package/lib/index.js CHANGED
@@ -1,3 +1,23 @@
1
+ import { AdapterRegistry } from 'onebots';
1
2
  export * from './adapter.js';
2
3
  export * from './bot.js';
4
+ const icqqSchema = {
5
+ account_id: { type: 'string', required: true, label: 'QQ 号' },
6
+ password: { type: 'string', label: '密码(可选/支持扫码)' },
7
+ protocol: {
8
+ platform: { type: 'number', enum: [1, 2, 3, 4, 5, 6], default: 2, label: '登录平台' },
9
+ ver: { type: 'string', label: 'APK 版本' },
10
+ sign_api_addr: { type: 'string', label: '签名服务器地址' },
11
+ data_dir: { type: 'string', label: '数据目录' },
12
+ log_config: { type: 'object', label: 'log4js 配置' },
13
+ ignore_self: { type: 'boolean', default: true, label: '过滤自己消息' },
14
+ resend: { type: 'boolean', default: true, label: '风控分片发送' },
15
+ reconn_interval: { type: 'number', default: 5, label: '重连间隔(秒)' },
16
+ cache_group_member: { type: 'boolean', default: true, label: '缓存群员列表' },
17
+ auto_server: { type: 'boolean', default: true, label: '自动选择服务器' },
18
+ ffmpeg_path: { type: 'string', label: 'ffmpeg 路径' },
19
+ ffprobe_path: { type: 'string', label: 'ffprobe 路径' },
20
+ },
21
+ };
22
+ AdapterRegistry.registerSchema('icqq', icqqSchema);
3
23
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebots/adapter-icqq",
3
- "version": "1.0.0",
3
+ "version": "1.0.5",
4
4
  "description": "onebots ICQQ 适配器 - 基于 ICQQ 协议的 QQ 机器人",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -27,11 +27,15 @@
27
27
  "typescript": "latest"
28
28
  },
29
29
  "peerDependencies": {
30
- "onebots": "1.0.0"
30
+ "onebots": "1.0.5"
31
31
  },
32
32
  "dependencies": {
33
33
  "@icqqjs/icqq": "^1.10.18"
34
34
  },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/lc-cn/onebots.git"
38
+ },
35
39
  "scripts": {
36
40
  "build": "rm -f *.tsbuildinfo && tsc --project tsconfig.json && tsc-alias -p tsconfig.json",
37
41
  "clean": "rm -rf lib *.tsbuildinfo"