wx2tg-puppet 1.0.6 → 1.0.8

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.
@@ -59,6 +59,32 @@ const audioUtil_1 = require("../utils/audioUtil");
59
59
  const path_1 = __importDefault(require("path"));
60
60
  const common_2 = require("../action/common");
61
61
  const fs = __importStar(require("fs"));
62
+ const FileChunkHelper_1 = require("../utils/FileChunkHelper");
63
+ const MAX_RETRIES = 3;
64
+ const RETRY_DELAY = 1000; // 1 second
65
+ // @ts-ignore - Ignore type errors for the entire function since we're dealing with a mix of old and new axios types
66
+ function retryDownload(downloadFn) {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ var _a, _b, _c, _d, _e, _f;
69
+ let lastError;
70
+ for (let i = 0; i < MAX_RETRIES; i++) {
71
+ try {
72
+ const response = yield downloadFn();
73
+ if (((_c = (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.Data) === null || _b === void 0 ? void 0 : _b.BaseResponse) === null || _c === void 0 ? void 0 : _c.ret) === 0) {
74
+ return response;
75
+ }
76
+ lastError = new Error(`Download failed with ret code: ${(_f = (_e = (_d = response === null || response === void 0 ? void 0 : response.data) === null || _d === void 0 ? void 0 : _d.Data) === null || _e === void 0 ? void 0 : _e.BaseResponse) === null || _f === void 0 ? void 0 : _f.ret}`);
77
+ }
78
+ catch (error) {
79
+ lastError = error;
80
+ }
81
+ if (i < MAX_RETRIES - 1) {
82
+ yield new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
83
+ }
84
+ }
85
+ throw new Error(`Download failed after ${MAX_RETRIES} attempts. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
86
+ });
87
+ }
62
88
  /**
63
89
  * Message class for handling and interacting with messages in cache.
64
90
  */
@@ -319,39 +345,117 @@ class Message {
319
345
  const xml = this.msg.Content;
320
346
  const json = Message.getXmlToJson(xml);
321
347
  const config = yield config_service_1.configService.get();
322
- const resp = yield ToolsApi_1.toolsApi.DownloadImg({
348
+ const totalSize = parseInt(json.msg.img.length);
349
+ // Get initial chunk size from first request
350
+ const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
351
+ if (!firstChunk)
352
+ return null;
353
+ // @ts-ignore - Using any type for response due to mixed axios versions
354
+ const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadImg({
323
355
  "CompressType": 0,
324
- "DataLen": parseInt(json.msg.img.length),
356
+ "DataLen": totalSize,
325
357
  "MsgId": this.msg.MsgId,
326
- "Section": {
327
- "DataLen": parseInt(json.msg.img.length),
328
- "StartPos": 0
329
- },
358
+ "Section": firstChunk,
330
359
  "ToWxid": this.msg.ToUserName,
331
360
  "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
332
- });
333
- const base64Data = resp.data.Data.data.buffer;
334
- // 你可以给图片起个名字,比如用 MsgId
335
- const fileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.png`);
361
+ }));
362
+ const downloadedLength = firstResp.data.Data.data.iLen;
363
+ const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
364
+ let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
365
+ // Start from second chunk since we already have the first one
366
+ for (let i = 1; i < chunks.length; i++) {
367
+ const chunk = chunks[i];
368
+ // @ts-ignore - Using any type for response due to mixed axios versions
369
+ const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadImg({
370
+ "CompressType": 0,
371
+ "DataLen": totalSize,
372
+ "MsgId": this.msg.MsgId,
373
+ "Section": chunk,
374
+ "ToWxid": this.msg.ToUserName,
375
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
376
+ }));
377
+ const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
378
+ completeBuffer = Buffer.concat([completeBuffer, chunkData]);
379
+ }
380
+ const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, `${this.msg.MsgId}.png`);
336
381
  return fileBox;
337
382
  }
338
383
  else if (this.type() === MessageType_1.MessageType.Video) {
339
384
  const xml = this.msg.Content;
340
385
  const json = Message.getXmlToJson(xml);
341
386
  const config = yield config_service_1.configService.get();
342
- const resp = yield ToolsApi_1.toolsApi.DownloadVideo({
387
+ const totalSize = parseInt(json.msg.videomsg.length);
388
+ // Get initial chunk size from first request
389
+ const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
390
+ if (!firstChunk)
391
+ return null;
392
+ // @ts-ignore - Using any type for response due to mixed axios versions
393
+ const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadVideo({
343
394
  "CompressType": 0,
344
- "DataLen": parseInt(json.msg.videomsg.length),
395
+ "DataLen": totalSize,
345
396
  "MsgId": this.msg.MsgId,
346
- "Section": {
347
- "DataLen": parseInt(json.msg.videomsg.length),
348
- "StartPos": 0
349
- },
397
+ "Section": firstChunk,
350
398
  "ToWxid": this.msg.ToUserName,
351
399
  "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
352
- });
353
- const base64Data = resp.data.Data.data.buffer;
354
- const fileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.mp4`);
400
+ }));
401
+ const downloadedLength = firstResp.data.Data.data.iLen;
402
+ const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
403
+ let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
404
+ // Start from second chunk since we already have the first one
405
+ for (let i = 1; i < chunks.length; i++) {
406
+ const chunk = chunks[i];
407
+ // @ts-ignore - Using any type for response due to mixed axios versions
408
+ const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadVideo({
409
+ "CompressType": 0,
410
+ "DataLen": totalSize,
411
+ "MsgId": this.msg.MsgId,
412
+ "Section": chunk,
413
+ "ToWxid": this.msg.ToUserName,
414
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
415
+ }));
416
+ const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
417
+ completeBuffer = Buffer.concat([completeBuffer, chunkData]);
418
+ }
419
+ const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, `${this.msg.MsgId}.mp4`);
420
+ return fileBox;
421
+ }
422
+ else if (this.type() === MessageType_1.MessageType.File) {
423
+ const xml = this.msg.Content;
424
+ const json = Message.getXmlToJson(xml);
425
+ const config = yield config_service_1.configService.get();
426
+ const totalSize = parseInt(json.msg.appmsg.appattach.totallen);
427
+ // Get initial chunk size from first request
428
+ const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
429
+ if (!firstChunk)
430
+ return null;
431
+ // @ts-ignore - Using any type for response due to mixed axios versions
432
+ const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
433
+ "AppID": json.msg.appmsg.appid,
434
+ "AttachId": json.msg.appmsg.appattach.attachid,
435
+ "DataLen": totalSize,
436
+ "Section": firstChunk,
437
+ "UserName": this.msg.FromUserName,
438
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
439
+ }));
440
+ const downloadedLength = firstResp.data.Data.data.iLen;
441
+ const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
442
+ let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
443
+ // Start from second chunk since we already have the first one
444
+ for (let i = 1; i < chunks.length; i++) {
445
+ const chunk = chunks[i];
446
+ // @ts-ignore - Using any type for response due to mixed axios versions
447
+ const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
448
+ "AppID": json.msg.appmsg.appid,
449
+ "AttachId": json.msg.appmsg.appattach.attachid,
450
+ "DataLen": totalSize,
451
+ "Section": chunk,
452
+ "UserName": this.msg.FromUserName,
453
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
454
+ }));
455
+ const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
456
+ completeBuffer = Buffer.concat([completeBuffer, chunkData]);
457
+ }
458
+ const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, json.msg.appmsg.title);
355
459
  return fileBox;
356
460
  }
357
461
  else if (this.type() === MessageType_1.MessageType.Voice) {
@@ -369,35 +473,12 @@ class Message {
369
473
  const silkFileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.silk`);
370
474
  const silkFilePath = path_1.default.resolve(__dirname, `${this.msg.MsgId}.silk`);
371
475
  const mp3FilePath = path_1.default.resolve(__dirname, `${this.msg.MsgId}.mp3`);
372
- // 保存 Silk 文件到本地
373
476
  yield silkFileBox.toFile(silkFilePath);
374
- // 使用 SilkToMp3Converter 转换为 MP3
375
477
  const converter = new audioUtil_1.SilkToMp3Converter();
376
478
  yield converter.convertToMp3(silkFilePath, mp3FilePath);
377
- // 删除 Silk 文件,只保留 MP3
378
479
  fs.unlinkSync(silkFilePath);
379
- // 返回 MP3 文件的 FileBox
380
480
  return file_box_1.FileBox.fromFile(mp3FilePath);
381
481
  }
382
- else if (this.type() === MessageType_1.MessageType.File) {
383
- const xml = this.msg.Content;
384
- const json = Message.getXmlToJson(xml);
385
- const config = yield config_service_1.configService.get();
386
- const resp = yield ToolsApi_1.toolsApi.DownloadFile({
387
- "AppID": json.msg.appmsg.appid,
388
- "AttachId": json.msg.appmsg.appattach.attachid,
389
- "DataLen": json.msg.appmsg.appattach.totallen,
390
- "Section": {
391
- "DataLen": json.msg.appmsg.appattach.totallen,
392
- "StartPos": 0
393
- },
394
- "UserName": this.msg.FromUserName,
395
- "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
396
- });
397
- const base64Data = resp.data.Data.data.buffer;
398
- const fileBox = file_box_1.FileBox.fromBase64(base64Data, json.msg.appmsg.title);
399
- return fileBox;
400
- }
401
482
  else if (this.type() === MessageType_1.MessageType.Emoji) {
402
483
  const xml = this.msg.Content;
403
484
  const json = Message.getXmlToJson(xml);
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare class WxBot extends EventEmitter {
20
20
  private interval;
21
21
  private heartbeatInterval;
22
22
  private msgInterval;
23
+ private recentMsgIds;
23
24
  constructor({ baseUrl, databasePath }: WxBotOptions);
24
25
  private fetchAndShowQRCode;
25
26
  private setupHeartbeat;
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ class WxBot extends events_1.EventEmitter {
43
43
  this.interval = null;
44
44
  this.heartbeatInterval = null;
45
45
  this.msgInterval = null;
46
+ this.recentMsgIds = []; // Store recent message IDs
46
47
  this.baseUrl = baseUrl;
47
48
  this.databasePath = databasePath;
48
49
  (0, axiosInstance_1.setBaseUrl)(baseUrl);
@@ -191,6 +192,16 @@ class WxBot extends events_1.EventEmitter {
191
192
  // 更新联系人或者群组信息
192
193
  // 新增:触发 message 事件
193
194
  for (const msg of data.Data.AddMsgs) {
195
+ // Check for duplicate messages
196
+ const newMsgId = BigInt(msg.NewMsgId);
197
+ if (this.recentMsgIds.includes(newMsgId)) {
198
+ continue; // Skip duplicate message
199
+ }
200
+ // Add to recent messages and maintain only last 10
201
+ this.recentMsgIds.push(newMsgId);
202
+ if (this.recentMsgIds.length > 10) {
203
+ this.recentMsgIds.shift(); // Remove oldest message ID
204
+ }
194
205
  // 将msg转换为Message类型
195
206
  const message = new Message_1.Message({
196
207
  MsgId: msg.MsgId,
@@ -0,0 +1,30 @@
1
+ export interface FileChunk {
2
+ DataLen: number;
3
+ StartPos: number;
4
+ }
5
+ export declare class FileChunkHelper {
6
+ private static readonly INITIAL_CHUNK_SIZE;
7
+ private static lastDownloadedLength;
8
+ /**
9
+ * Calculate file chunks based on total file size
10
+ * @param totalSize Total file size in bytes
11
+ * @param downloadedLength Optional length from previous download (iLen)
12
+ * @returns Array of chunks with DataLen and StartPos
13
+ */
14
+ static calculateChunks(totalSize: number, downloadedLength?: number): FileChunk[];
15
+ /**
16
+ * Get a specific chunk information
17
+ * @param totalSize Total file size in bytes
18
+ * @param chunkIndex Chunk index (0-based)
19
+ * @param downloadedLength Optional length from previous download (iLen)
20
+ * @returns Chunk information or null if index is out of range
21
+ */
22
+ static getChunk(totalSize: number, chunkIndex: number, downloadedLength?: number): FileChunk | null;
23
+ /**
24
+ * Get total number of chunks needed for a file
25
+ * @param totalSize Total file size in bytes
26
+ * @param downloadedLength Optional length from previous download (iLen)
27
+ * @returns Total number of chunks
28
+ */
29
+ static getTotalChunks(totalSize: number, downloadedLength?: number): number;
30
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileChunkHelper = void 0;
4
+ class FileChunkHelper {
5
+ /**
6
+ * Calculate file chunks based on total file size
7
+ * @param totalSize Total file size in bytes
8
+ * @param downloadedLength Optional length from previous download (iLen)
9
+ * @returns Array of chunks with DataLen and StartPos
10
+ */
11
+ static calculateChunks(totalSize, downloadedLength) {
12
+ const chunks = [];
13
+ let position = 0;
14
+ // Use consistent chunk size throughout
15
+ const chunkSize = downloadedLength || this.INITIAL_CHUNK_SIZE;
16
+ while (position < totalSize) {
17
+ const remainingSize = totalSize - position;
18
+ const currentChunkSize = Math.min(chunkSize, remainingSize);
19
+ chunks.push({
20
+ DataLen: currentChunkSize,
21
+ StartPos: position
22
+ });
23
+ position += currentChunkSize;
24
+ }
25
+ return chunks;
26
+ }
27
+ /**
28
+ * Get a specific chunk information
29
+ * @param totalSize Total file size in bytes
30
+ * @param chunkIndex Chunk index (0-based)
31
+ * @param downloadedLength Optional length from previous download (iLen)
32
+ * @returns Chunk information or null if index is out of range
33
+ */
34
+ static getChunk(totalSize, chunkIndex, downloadedLength) {
35
+ const chunks = this.calculateChunks(totalSize, downloadedLength);
36
+ return chunks[chunkIndex] || null;
37
+ }
38
+ /**
39
+ * Get total number of chunks needed for a file
40
+ * @param totalSize Total file size in bytes
41
+ * @param downloadedLength Optional length from previous download (iLen)
42
+ * @returns Total number of chunks
43
+ */
44
+ static getTotalChunks(totalSize, downloadedLength) {
45
+ return this.calculateChunks(totalSize, downloadedLength).length;
46
+ }
47
+ }
48
+ exports.FileChunkHelper = FileChunkHelper;
49
+ FileChunkHelper.INITIAL_CHUNK_SIZE = 1024; // Initial chunk size for first request
50
+ FileChunkHelper.lastDownloadedLength = null; // Store the last downloaded length
@@ -6,7 +6,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.axiosInstance = void 0;
7
7
  exports.setBaseUrl = setBaseUrl;
8
8
  const axios_1 = __importDefault(require("axios"));
9
- exports.axiosInstance = axios_1.default.create({});
9
+ const json_bigint_1 = __importDefault(require("json-bigint"));
10
+ const JSONbigString = (0, json_bigint_1.default)({
11
+ storeAsString: true,
12
+ alwaysParseAsBig: false,
13
+ useNativeBigInt: false
14
+ });
15
+ exports.axiosInstance = axios_1.default.create({
16
+ transformResponse: data => {
17
+ try {
18
+ return JSONbigString.parse(data + '');
19
+ }
20
+ catch (err) {
21
+ console.log(err);
22
+ return JSON.parse(data + '');
23
+ }
24
+ }
25
+ });
10
26
  function setBaseUrl(baseUrl) {
11
27
  exports.axiosInstance.defaults.baseURL = baseUrl;
12
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wx2tg-puppet",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
@@ -24,6 +24,7 @@
24
24
  "description": "微信机器人核心库,可作为依赖被其他项目引入。",
25
25
  "devDependencies": {
26
26
  "@types/axios": "^0.9.36",
27
+ "@types/json-bigint": "^1.0.4",
27
28
  "@types/qrcode-terminal": "^0.12.2",
28
29
  "ts-node": "^10.9.2",
29
30
  "typescript": "^5.8.3"
@@ -32,6 +33,7 @@
32
33
  "axios": "^1.9.0",
33
34
  "fast-xml-parser": "^5.2.3",
34
35
  "file-box": "^1.4.15",
36
+ "json-bigint": "^1.0.0",
35
37
  "qrcode-terminal": "^0.12.0",
36
38
  "reflect-metadata": "^0.2.2",
37
39
  "sqlite3": "^5.1.7",