wx2tg-puppet 3.0.9 → 6.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.UrlLink = exports.WeVideo = exports.Voice = exports.Emoji = exports.Message = exports.Room = exports.Contact = exports.WxBot = exports.AppDataSource = void 0;
45
+ exports.UrlLink = exports.WeVideo = exports.Voice = exports.Emoji = exports.isOfficialAccountVerifyFlag = exports.normalizeContactVerifyFlag = exports.Message = exports.Room = exports.Contact = exports.WxBot = exports.AppDataSource = void 0;
46
46
  require("reflect-metadata");
47
47
  const axiosInstance_1 = require("./utils/axiosInstance");
48
48
  const data_source_1 = require("./database/data-source");
@@ -73,6 +73,7 @@ const ContactEntity_1 = require("./entities/ContactEntity");
73
73
  const room_service_1 = require("./services/room.service");
74
74
  const message_service_1 = require("./services/message.service");
75
75
  const http = __importStar(require("http"));
76
+ const CallbackPayload_1 = require("./utils/CallbackPayload");
76
77
  class WxBot extends events_1.EventEmitter {
77
78
  constructor({ baseUrl, databasePath, deviceType = 'ipad', mode = 'polling', callbackPort = 8056, ProxyIp, ProxyPassword, ProxyUser }) {
78
79
  super();
@@ -81,13 +82,15 @@ class WxBot extends events_1.EventEmitter {
81
82
  this.heartbeatInterval = null;
82
83
  this.callbackInterval = null;
83
84
  this.msgInterval = null;
84
- this.recentMsgIds = []; // Store recent message IDs
85
+ this.recentMsgIds = new Map();
86
+ this.messageQueue = Promise.resolve();
87
+ this.syncTask = null;
88
+ this.syncRequested = false;
89
+ this.loginRecoveryInProgress = false;
85
90
  this.syncFailureCount = 0; // Sync failure counter
86
91
  this.httpServer = null; // HTTP回调服务器
87
92
  this.loggingOut = false; // Flag to prevent concurrent logOut calls
88
93
  this.loggingIn = false; // Flag to prevent concurrent login calls
89
- this.syncInProgress = false; // Prevent overlapping message sync calls
90
- this.loginRecoveryInProgress = false; // Allow only one login recovery flow
91
94
  this.baseUrl = baseUrl;
92
95
  this.databasePath = databasePath;
93
96
  this.deviceType = deviceType;
@@ -383,95 +386,103 @@ class WxBot extends events_1.EventEmitter {
383
386
  DeviceType: this.deviceType,
384
387
  });
385
388
  }
386
- // 设置心跳包
387
- this.setupHeartbeat(userName);
388
- // 根据模式选择消息监听方式
389
+ // 先监听再启动心跳;不再通过 Sync 清空并丢弃登录期间的消息。
389
390
  if (this.mode === 'callback') {
390
- // 循环调用至无新消息
391
- while (true) {
392
- const resp = yield MsgApi_1.msgApi.Sync({
393
- "Wxid": userName,
394
- "Scene": 0,
395
- "Synckey": ""
396
- });
397
- if (!(yield this.checkSyncLoginStatus(resp.data, userName))) {
398
- return;
399
- }
400
- if (resp.data.Success) {
401
- if (resp.data.Message === '当前未有新消息') {
402
- break;
403
- }
404
- }
405
- }
391
+ yield this.startCallbackServer(userName);
392
+ this.emit('login', userName);
406
393
  this.callbackInterval = setInterval(() => {
407
- this.handleCallbackMessage(userName);
394
+ void this.handleMsg(userName).catch(() => console.error('回调兜底同步失败'));
408
395
  }, 1000 * 60 * 5);
409
- this.startCallbackServer(userName);
410
396
  }
411
397
  else {
412
398
  this.startPollingMode(userName);
413
399
  }
400
+ yield this.setupHeartbeat(userName);
414
401
  });
415
402
  }
416
403
  startCallbackServer(userName) {
417
- if (this.httpServer) {
418
- this.httpServer.close();
419
- this.httpServer = null;
420
- }
421
- this.httpServer = http.createServer((req, res) => {
422
- // 设置CORS头
423
- res.setHeader('Access-Control-Allow-Origin', '*');
424
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
425
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
426
- if (req.method === 'OPTIONS') {
427
- res.writeHead(200);
428
- res.end();
429
- return;
430
- }
431
- const url = req.url || '';
432
- const callbackPattern = `/message/callback/${userName}`;
433
- if (req.method === 'POST' && url === callbackPattern) {
434
- let body = '';
404
+ return __awaiter(this, void 0, void 0, function* () {
405
+ if (this.httpServer) {
406
+ const previous = this.httpServer;
407
+ this.httpServer = null;
408
+ yield new Promise(resolve => previous.close(() => resolve()));
409
+ }
410
+ const server = http.createServer((req, res) => {
411
+ const reply = (status, data) => {
412
+ if (!res.writableEnded && !res.destroyed) {
413
+ res.writeHead(status, { 'Content-Type': 'application/json' });
414
+ res.end(JSON.stringify(data));
415
+ }
416
+ };
417
+ if (req.method !== 'POST' || req.url !== `/message/callback/${userName}`) {
418
+ reply(404, { error: 'Not Found' });
419
+ req.resume();
420
+ return;
421
+ }
422
+ let size = 0;
423
+ let oversized = false;
435
424
  let chunks = [];
436
425
  req.on('data', (chunk) => {
437
- chunks.push(chunk);
438
- body += chunk.toString('utf8');
426
+ size += chunk.length;
427
+ if (size > CallbackPayload_1.MAX_CALLBACK_BYTES) {
428
+ oversized = true;
429
+ chunks = [];
430
+ reply(413, { error: 'Callback body too large' });
431
+ }
432
+ else if (!oversized) {
433
+ chunks.push(chunk);
434
+ }
439
435
  });
440
- req.on('end', () => {
436
+ req.on('end', () => __awaiter(this, void 0, void 0, function* () {
437
+ if (oversized || res.destroyed)
438
+ return;
439
+ let data;
441
440
  try {
442
- this.handleCallbackMessage(userName);
443
- res.writeHead(200, { 'Content-Type': 'application/json' });
444
- res.end(JSON.stringify({ success: true }));
441
+ // Decode once, after joining chunks, so split UTF-8 characters are preserved.
442
+ data = (0, CallbackPayload_1.parseCallbackPayload)(Buffer.concat(chunks).toString('utf8'));
443
+ chunks = [];
445
444
  }
446
- catch (error) {
447
- console.error('JSON解析错误:', error);
448
- console.error('原始内容:', body);
449
- res.writeHead(400, { 'Content-Type': 'application/json' });
450
- res.end(JSON.stringify({ error: 'Invalid JSON', body: body }));
445
+ catch (_a) {
446
+ reply(400, { error: 'Invalid callback payload' });
447
+ return;
451
448
  }
449
+ try {
450
+ yield this.handleCallbackMessage(userName, data);
451
+ reply(200, { success: true });
452
+ }
453
+ catch (_b) {
454
+ console.error('回调消息处理失败');
455
+ reply(500, { error: 'Callback processing failed' });
456
+ }
457
+ }));
458
+ req.on('error', () => reply(400, { error: 'Request error' }));
459
+ });
460
+ this.httpServer = server;
461
+ server.requestTimeout = 30000;
462
+ yield new Promise((resolve, reject) => {
463
+ const failed = (error) => {
464
+ if (this.httpServer === server)
465
+ this.httpServer = null;
466
+ reject(error);
467
+ };
468
+ server.once('error', failed);
469
+ server.listen(this.callbackPort, '0.0.0.0', () => {
470
+ server.removeListener('error', failed);
471
+ server.on('error', () => console.error('回调服务器错误'));
472
+ resolve();
452
473
  });
453
- req.on('error', (error) => {
454
- console.error('请求处理错误:', error);
455
- res.writeHead(500, { 'Content-Type': 'application/json' });
456
- res.end(JSON.stringify({ error: 'Request error' }));
457
- });
474
+ });
475
+ });
476
+ }
477
+ handleCallbackMessage(userName_1) {
478
+ return __awaiter(this, arguments, void 0, function* (userName, data = null) {
479
+ if (data === null) {
480
+ yield this.handleMsg(userName);
458
481
  }
459
482
  else {
460
- res.writeHead(404, { 'Content-Type': 'application/json' });
461
- res.end(JSON.stringify({ error: 'Not Found' }));
483
+ yield this.enqueueMessages(userName, data);
462
484
  }
463
485
  });
464
- this.httpServer.listen(this.callbackPort, '0.0.0.0', () => {
465
- console.log(`回调服务器启动成功,监听地址: http://0.0.0.0:${this.callbackPort}${`/message/callback/${userName}`}`);
466
- });
467
- this.httpServer.on('error', (error) => {
468
- console.error('回调服务器启动失败:', error);
469
- });
470
- }
471
- handleCallbackMessage(userName) {
472
- return __awaiter(this, void 0, void 0, function* () {
473
- yield this.handleMsg(userName);
474
- });
475
486
  }
476
487
  startPollingMode(userName) {
477
488
  // 防止重复定时
@@ -480,9 +491,9 @@ class WxBot extends events_1.EventEmitter {
480
491
  this.msgInterval = null;
481
492
  }
482
493
  // 监听消息,每秒掉一次
483
- this.msgInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
484
- yield this.handleMsg(userName);
485
- }), 1000);
494
+ this.msgInterval = setInterval(() => {
495
+ void this.handleMsg(userName).catch(() => console.error('消息轮询失败'));
496
+ }, 1000);
486
497
  this.emit('login', userName);
487
498
  }
488
499
  checkLoginAfterDelay(userName) {
@@ -501,14 +512,17 @@ class WxBot extends events_1.EventEmitter {
501
512
  if (resp.data.Message === '用户可能退出') {
502
513
  return false;
503
514
  }
515
+ // Recovery checks also consume Sync; preserve messages returned by that check.
516
+ (0, CallbackPayload_1.validateSyncResponse)(resp.data);
517
+ yield this.processMessages(userName, resp.data);
504
518
  return true;
505
519
  });
506
520
  }
521
+ // Preserve the single-flight login recovery fix from published puppet 3.0.8.
507
522
  recoverLogin(userName) {
508
523
  return __awaiter(this, void 0, void 0, function* () {
509
- if (this.loginRecoveryInProgress) {
524
+ if (this.loginRecoveryInProgress)
510
525
  return false;
511
- }
512
526
  this.loginRecoveryInProgress = true;
513
527
  try {
514
528
  yield LoginApi_1.loginApi.LoginTwiceAutoAuth(userName);
@@ -549,99 +563,130 @@ class WxBot extends events_1.EventEmitter {
549
563
  }
550
564
  handleMsg(userName) {
551
565
  return __awaiter(this, void 0, void 0, function* () {
552
- if (this.syncInProgress) {
553
- return;
554
- }
555
- this.syncInProgress = true;
566
+ this.syncRequested = true;
567
+ if (this.syncTask)
568
+ return this.syncTask;
569
+ this.syncTask = (() => __awaiter(this, void 0, void 0, function* () {
570
+ do {
571
+ this.syncRequested = false;
572
+ const resp = yield MsgApi_1.msgApi.Sync({ Wxid: userName, Scene: 0, Synckey: '' });
573
+ yield this.enqueueMessages(userName, resp.data);
574
+ } while (this.syncRequested);
575
+ }))();
556
576
  try {
557
- yield this.handleMsgOnce(userName);
577
+ yield this.syncTask;
558
578
  }
559
579
  finally {
560
- this.syncInProgress = false;
580
+ this.syncTask = null;
561
581
  }
562
582
  });
563
583
  }
564
- handleMsgOnce(userName) {
584
+ enqueueMessages(userName, data) {
585
+ (0, CallbackPayload_1.validateSyncResponse)(data);
586
+ const task = this.messageQueue.then(() => this.processMessages(userName, data));
587
+ // Both polling and callbacks use one parser/queue. A failed batch must not poison later ones.
588
+ this.messageQueue = task.catch(() => { });
589
+ return task;
590
+ }
591
+ processMessages(userName, data) {
565
592
  return __awaiter(this, void 0, void 0, function* () {
566
593
  var _a;
567
- const resp = yield MsgApi_1.msgApi.Sync({
568
- "Wxid": userName,
569
- "Scene": 0,
570
- "Synckey": ""
571
- });
572
- const data = resp.data;
573
- if (!(yield this.checkSyncLoginStatus(data, userName))) {
594
+ if (!(yield this.checkSyncLoginStatus(data, userName)))
574
595
  return;
575
- }
576
- if (data.Success) {
577
- if (data.Message === '当前未有新消息') {
578
- return;
579
- }
580
- if (!data.Data.AddMsgs) {
581
- return;
582
- }
583
- // 更新联系人或者群组信息
584
- // 新增:触发 message 事件
585
- for (const msg of data.Data.AddMsgs) {
586
- // Check for duplicate messages
587
- const newMsgId = BigInt(msg.NewMsgId);
588
- if (this.recentMsgIds.includes(newMsgId)) {
589
- continue; // Skip duplicate message
590
- }
591
- // Add to recent messages and maintain only last 40
592
- this.recentMsgIds.push(newMsgId);
593
- if (this.recentMsgIds.length > 40) {
594
- this.recentMsgIds.shift(); // Remove oldest message ID
595
- }
596
- // 将msg转换为Message类型
597
- const message = new Message_1.Message({
598
- MsgId: msg.MsgId,
599
- FromUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n')[0] : msg.FromUserName.string,
600
- ToUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : msg.ToUserName.string,
601
- MsgType: msg.MsgType,
602
- Content: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n').slice(1).join(':\n') : msg.Content.string,
603
- Status: msg.Status,
604
- ImgStatus: msg.ImgStatus,
605
- ImgBuf: msg.ImgBuf.iLen,
606
- CreateTime: msg.CreateTime,
607
- MsgSource: msg.MsgSource,
608
- NewMsgId: msg.NewMsgId,
609
- MsgSeq: msg.MsgSeq,
610
- RoomId: (msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : '') || (msg.ToUserName.string.endsWith('@chatroom') ? msg.ToUserName.string : ''),
611
- IsSelf: msg.FromUserName.string === userName,
612
- xml: msg.Content.string
613
- });
614
- // 处理企业微信
615
- if ((_a = message.fromId) === null || _a === void 0 ? void 0 : _a.includes('@openim')) {
616
- let contactE = yield contact_service_1.contactService.getByUserName(message.fromId);
617
- if (!contactE) {
618
- contactE = new ContactEntity_1.ContactEntity();
596
+ if (!data.Success || !((_a = data.Data) === null || _a === void 0 ? void 0 : _a.AddMsgs))
597
+ return;
598
+ let failed = false;
599
+ for (const msg of data.Data.AddMsgs) {
600
+ try {
601
+ // One malformed entry must not discard unrelated messages in this consumed batch.
602
+ (0, CallbackPayload_1.validateRawMessage)(msg);
603
+ for (let attempt = 0; attempt < 3; attempt++) {
604
+ try {
605
+ yield this.processMessage(userName, msg);
606
+ break;
607
+ }
608
+ catch (error) {
609
+ if (attempt === 2)
610
+ throw error;
611
+ yield new Promise(resolve => setTimeout(resolve, 100));
619
612
  }
620
- contactE.nickName = msg.PushContent.split(':')[0].slice(0, -1);
621
- contactE.userName = message.fromId;
622
- contactE.smallHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
623
- contactE.bigHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
624
- contact_service_1.contactService.upsertByUsername(contactE.userName, contactE);
625
- }
626
- if (message.type() === MessageType_1.MessageType.AddFriend) { // 好友请求
627
- const obj = Message_1.Message.getXmlToJson(message.text());
628
- console.log(obj);
629
- const friendship = new Friendship_1.Friendship({
630
- fromnickname: obj.msg.fromnickname,
631
- v3: obj.msg.encryptusername,
632
- v4: obj.msg.ticket,
633
- scene: obj.msg.scene,
634
- fromusername: obj.msg.fromusername,
635
- bigHeadImgUrl: obj.msg.bigHeadImgUrl,
636
- smallHeadImgUrl: obj.msg.smallHeadImgUrl,
637
- content: obj.msg.content
638
- });
639
- this.emit('friendship', friendship);
640
- }
641
- else {
642
- this.emit('message', message);
643
613
  }
644
614
  }
615
+ catch (_b) {
616
+ failed = true;
617
+ console.error('单条微信消息处理失败,其余消息继续处理');
618
+ }
619
+ }
620
+ if (failed)
621
+ throw new Error('Some messages could not be processed');
622
+ });
623
+ }
624
+ processMessage(userName, msg) {
625
+ return __awaiter(this, void 0, void 0, function* () {
626
+ var _a, _b;
627
+ const now = Date.now();
628
+ for (const [key, seenAt] of this.recentMsgIds) {
629
+ if (now - seenAt < 120000)
630
+ break;
631
+ this.recentMsgIds.delete(key);
632
+ }
633
+ const newMsgId = String(msg.NewMsgId);
634
+ const identity = newMsgId !== '0' ? newMsgId
635
+ : `${msg.MsgId}:${msg.FromUserName.string}:${msg.ToUserName.string}:${msg.CreateTime}`;
636
+ const key = `${userName}:${identity}`;
637
+ if (this.recentMsgIds.has(key))
638
+ return;
639
+ // 将msg转换为Message类型
640
+ const message = new Message_1.Message({
641
+ MsgId: msg.MsgId,
642
+ FromUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n')[0] : msg.FromUserName.string,
643
+ ToUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : msg.ToUserName.string,
644
+ MsgType: msg.MsgType,
645
+ Content: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n').slice(1).join(':\n') : msg.Content.string,
646
+ Status: msg.Status,
647
+ ImgStatus: msg.ImgStatus,
648
+ ImgBuf: ((_a = msg.ImgBuf) === null || _a === void 0 ? void 0 : _a.iLen) || 0,
649
+ CreateTime: msg.CreateTime,
650
+ MsgSource: msg.MsgSource,
651
+ NewMsgId: newMsgId,
652
+ MsgSeq: msg.MsgSeq,
653
+ RoomId: (msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : '') || (msg.ToUserName.string.endsWith('@chatroom') ? msg.ToUserName.string : ''),
654
+ IsSelf: msg.FromUserName.string === userName,
655
+ xml: msg.Content.string
656
+ });
657
+ // 处理企业微信
658
+ if ((_b = message.fromId) === null || _b === void 0 ? void 0 : _b.includes('@openim')) {
659
+ let contactE = yield contact_service_1.contactService.getByUserName(message.fromId);
660
+ if (!contactE) {
661
+ contactE = new ContactEntity_1.ContactEntity();
662
+ }
663
+ contactE.nickName = msg.PushContent.split(':')[0].slice(0, -1);
664
+ contactE.userName = message.fromId;
665
+ contactE.smallHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
666
+ contactE.bigHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
667
+ yield contact_service_1.contactService.upsertByUsername(contactE.userName, contactE);
668
+ }
669
+ if (message.type() === MessageType_1.MessageType.AddFriend) { // 好友请求
670
+ const obj = Message_1.Message.getXmlToJson(message.text());
671
+ const friendship = new Friendship_1.Friendship({
672
+ fromnickname: obj.msg.fromnickname,
673
+ v3: obj.msg.encryptusername,
674
+ v4: obj.msg.ticket,
675
+ scene: obj.msg.scene,
676
+ fromusername: obj.msg.fromusername,
677
+ bigHeadImgUrl: obj.msg.bigHeadImgUrl,
678
+ smallHeadImgUrl: obj.msg.smallHeadImgUrl,
679
+ content: obj.msg.content
680
+ });
681
+ this.emit('friendship', friendship);
682
+ }
683
+ else {
684
+ this.emit('message', message);
685
+ }
686
+ // Mark only after successful dispatch so a synchronous failure can be retried.
687
+ this.recentMsgIds.set(key, now);
688
+ if (this.recentMsgIds.size > 10000) {
689
+ this.recentMsgIds.delete(this.recentMsgIds.keys().next().value);
645
690
  }
646
691
  });
647
692
  }
@@ -713,6 +758,7 @@ class WxBot extends events_1.EventEmitter {
713
758
  });
714
759
  if (resp.data.Code === 0) {
715
760
  yield this.loginSuccess(config.wxid);
761
+ yield this.enqueueMessages(config.wxid, resp.data);
716
762
  }
717
763
  else {
718
764
  yield this.login();
@@ -742,6 +788,7 @@ class WxBot extends events_1.EventEmitter {
742
788
  yield LoginApi_1.loginApi.LoginTwiceAutoAuth(config === null || config === void 0 ? void 0 : config.wxid);
743
789
  if (yield this.checkSyncLoginStatus(resp.data, config === null || config === void 0 ? void 0 : config.wxid)) {
744
790
  yield this.loginSuccess(config.wxid);
791
+ yield this.enqueueMessages(config.wxid, resp.data);
745
792
  return true;
746
793
  }
747
794
  else {
@@ -769,3 +816,6 @@ class WxBot extends events_1.EventEmitter {
769
816
  }
770
817
  }
771
818
  exports.WxBot = WxBot;
819
+ var ContactClassification_1 = require("./utils/ContactClassification");
820
+ Object.defineProperty(exports, "normalizeContactVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.normalizeContactVerifyFlag; } });
821
+ Object.defineProperty(exports, "isOfficialAccountVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.isOfficialAccountVerifyFlag; } });
@@ -35,11 +35,11 @@ class ContactService {
35
35
  }
36
36
  update(userName, update) {
37
37
  return __awaiter(this, void 0, void 0, function* () {
38
- const entity = yield this.getByUserName(userName);
39
- if (!entity)
40
- return null;
41
- Object.assign(entity, update);
42
- return this.repo.save(entity);
38
+ // Update only supplied columns, not a stale snapshot of the whole row.
39
+ if (Object.values(update).some(value => value !== undefined)) {
40
+ yield this.repo.update({ userName }, update);
41
+ }
42
+ return this.getByUserName(userName);
43
43
  });
44
44
  }
45
45
  clearRoom() {
@@ -56,14 +56,10 @@ class ContactService {
56
56
  }
57
57
  upsertByUsername(userName, data) {
58
58
  return __awaiter(this, void 0, void 0, function* () {
59
- let contact = yield this.getByUserName(userName);
60
- if (contact) {
61
- Object.assign(contact, data);
62
- }
63
- else {
64
- contact = this.repo.create(Object.assign({ userName }, data));
65
- }
66
- return this.repo.save(contact);
59
+ // INSERT ... ON CONFLICT avoids concurrent read-then-insert UNIQUE failures.
60
+ // TypeORM excludes undefined columns from the conflict update.
61
+ yield this.repo.upsert(Object.assign(Object.assign({}, data), { userName }), ['userName']);
62
+ return this.repo.findOneByOrFail({ userName });
67
63
  });
68
64
  }
69
65
  delete(userName) {
@@ -15,6 +15,7 @@ export declare const MessageType: {
15
15
  Link: string;
16
16
  AddFriend: string;
17
17
  Quote: string;
18
+ Solitaire: string;
18
19
  Transfer: string;
19
20
  RedPacket: string;
20
21
  VideoAccount: string;
@@ -18,6 +18,7 @@ exports.MessageType = {
18
18
  Link: 'link', // 公众号链接
19
19
  AddFriend: 'add_friend', // 添加好友通知
20
20
  Quote: 'quote', // 引用消息
21
+ Solitaire: 'solitaire', // 接龙
21
22
  Transfer: 'transfer', //转账
22
23
  RedPacket: 'red_packet', //红包
23
24
  VideoAccount: 'video_account', // 视频号消息
@@ -0,0 +1,4 @@
1
+ export declare const MAX_CALLBACK_BYTES: number;
2
+ export declare function validateSyncResponse(data: any): void;
3
+ export declare function validateRawMessage(msg: any): void;
4
+ export declare function parseCallbackPayload(body: string): any | null;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_CALLBACK_BYTES = void 0;
7
+ exports.validateSyncResponse = validateSyncResponse;
8
+ exports.validateRawMessage = validateRawMessage;
9
+ exports.parseCallbackPayload = parseCallbackPayload;
10
+ const json_bigint_1 = __importDefault(require("json-bigint"));
11
+ const json = (0, json_bigint_1.default)({ storeAsString: true });
12
+ exports.MAX_CALLBACK_BYTES = 8 * 1024 * 1024;
13
+ // Go time.Time.String(), including its optional monotonic-clock suffix.
14
+ const timestampNotification = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4} \S+(?: m=[+-]\d+(?:\.\d+)?)?$/;
15
+ function validateSyncResponse(data) {
16
+ var _a;
17
+ if (!data || typeof data !== 'object' || !Number.isInteger(data.Code) || typeof data.Success !== 'boolean') {
18
+ throw new Error('Invalid sync envelope');
19
+ }
20
+ if (data.Code !== 0 || !data.Success)
21
+ return;
22
+ if (!data.Data || typeof data.Data !== 'object')
23
+ throw new Error('Invalid sync data');
24
+ const messages = data.Data.AddMsgs;
25
+ if (messages == null) {
26
+ // The v6 server retains AddMsgs. Do not silently acknowledge an unknown message format.
27
+ if ((_a = data.Data.Messages) === null || _a === void 0 ? void 0 : _a.length)
28
+ throw new Error('Missing raw messages');
29
+ return;
30
+ }
31
+ if (!Array.isArray(messages))
32
+ throw new Error('Invalid message list');
33
+ }
34
+ function validateRawMessage(msg) {
35
+ var _a, _b, _c;
36
+ if (!msg || typeof ((_a = msg.FromUserName) === null || _a === void 0 ? void 0 : _a.string) !== 'string' || typeof ((_b = msg.ToUserName) === null || _b === void 0 ? void 0 : _b.string) !== 'string'
37
+ || typeof ((_c = msg.Content) === null || _c === void 0 ? void 0 : _c.string) !== 'string' || !Number.isInteger(msg.MsgType)
38
+ || !Number.isInteger(msg.CreateTime)
39
+ || !((typeof msg.NewMsgId === 'string' && /^\d+$/.test(msg.NewMsgId))
40
+ || (typeof msg.NewMsgId === 'number' && Number.isSafeInteger(msg.NewMsgId) && msg.NewMsgId >= 0))) {
41
+ throw new Error('Invalid raw message');
42
+ }
43
+ }
44
+ // null denotes a legacy wake-up notification, not a batch with an empty message list.
45
+ function parseCallbackPayload(body) {
46
+ const text = body.trim();
47
+ if (!text || timestampNotification.test(text))
48
+ return null;
49
+ const data = json.parse(text);
50
+ validateSyncResponse(data);
51
+ return data;
52
+ }
@@ -0,0 +1,4 @@
1
+ /** Normalize an API VerifyFlag without turning missing/invalid data into personal contacts. */
2
+ export declare function normalizeContactVerifyFlag(value: unknown): number | null;
3
+ /** MM_USERATTRVERIFYFALG_BIZ_BRAND (8), not a username convention or any nonzero flag. */
4
+ export declare function isOfficialAccountVerifyFlag(value: unknown): boolean | undefined;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeContactVerifyFlag = normalizeContactVerifyFlag;
4
+ exports.isOfficialAccountVerifyFlag = isOfficialAccountVerifyFlag;
5
+ /** Normalize an API VerifyFlag without turning missing/invalid data into personal contacts. */
6
+ function normalizeContactVerifyFlag(value) {
7
+ if (typeof value === 'string') {
8
+ const parsed = Number(value);
9
+ // uint32 numbers stringify as canonical decimal (no whitespace, signs or leading zeros).
10
+ if (String(parsed) !== value)
11
+ return null;
12
+ value = parsed;
13
+ }
14
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff
15
+ ? value : null;
16
+ }
17
+ /** MM_USERATTRVERIFYFALG_BIZ_BRAND (8), not a username convention or any nonzero flag. */
18
+ function isOfficialAccountVerifyFlag(value) {
19
+ const flag = normalizeContactVerifyFlag(value);
20
+ return flag === null ? undefined : (flag & 8) !== 0;
21
+ }
@@ -0,0 +1,7 @@
1
+ /** Only static diagnostics and numeric status codes belong in download errors. */
2
+ export declare class FileDownloadError extends Error {
3
+ httpStatus?: number | undefined;
4
+ businessCode?: number | undefined;
5
+ retryable: boolean;
6
+ constructor(message: string, httpStatus?: number | undefined, businessCode?: number | undefined, retryable?: boolean);
7
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileDownloadError = void 0;
4
+ /** Only static diagnostics and numeric status codes belong in download errors. */
5
+ class FileDownloadError extends Error {
6
+ constructor(message, httpStatus, businessCode, retryable = false) {
7
+ super(`DownloadFile: ${message}`);
8
+ this.httpStatus = httpStatus;
9
+ this.businessCode = businessCode;
10
+ this.retryable = retryable;
11
+ this.name = 'FileDownloadError';
12
+ }
13
+ }
14
+ exports.FileDownloadError = FileDownloadError;
@@ -0,0 +1,5 @@
1
+ export declare const SOLITAIRE_FALLBACK = "[\u63A5\u9F99] \u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u89E3\u6790\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u67E5\u770B\u3002";
2
+ /** Bound solitaire display parsing; do not impose this size limit on other app types. */
3
+ export declare function isBoundedAppMessageXml(value: string): boolean;
4
+ /** Only appmsg.title is display text; solitaire_info is deliberately never interpreted. */
5
+ export declare function getSolitaireDisplayText(payload: string): string;