wx2tg-puppet 3.0.7 → 3.0.9

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/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # wx2tg-puppet
2
+
3
+ 微信机器人核心库。通过已有的 `WxBot` 配置服务端地址、设备类型和本地数据库。
4
+
5
+ ## 3.0.9:文件下载兼容
6
+
7
+ - 支持服务端 v6.0.10 的 `DownloadFile` 二进制 `200/206` 响应,同时兼容旧版 JSON/Base64 响应。
8
+ - 现有 `toolsApi.DownloadFile()` 仍返回 `Data.data.buffer`(Base64)和 `Data.BaseResponse.ret`,调用方无需为响应格式切换而修改解析逻辑。
9
+ - `toolsApi.DownloadFileBinary()` 返回单个分片的 `Buffer`,保留 HTTP 状态与响应头。
10
+ - `toolsApi.DownloadFileBuffer()` 下载完整文件,默认每次请求 512 KiB,按实际返回字节推进偏移;校验范围、总长度、非空数据和旧版业务状态。
11
+ - `Message.toFileBox()` 的普通文件分片路径使用新下载器;原有超过 80 MiB 的 `DownloadFileMessage` 路径保留。图片、视频和其他 API 的 JSON 解析保持原样。
12
+
13
+ ```ts
14
+ import {toolsApi} from 'wx2tg-puppet/dist/api/ToolsApi';
15
+
16
+ // 使用当前登录账号及原始文件消息中的真实元数据;不要修改文件总长度。
17
+ const buffer = await toolsApi.DownloadFileBuffer({
18
+ Wxid: 'your_logged_in_account',
19
+ UserName: 'original_conversation',
20
+ AttachId: 'original_attachment_id',
21
+ DataLen: originalFileSize,
22
+ });
23
+ ```
24
+
25
+ CDN 回退的首片可能需要等待整份文件接收完成,因此文件分片请求单独使用 180 秒超时;不会修改全局 Axios 配置。完整下载仅对临时网络错误及 HTTP 429/502/503/504 做有限重试,最多三次;业务拒绝、空内容和范围错误立即失败。
26
+
27
+ 失败会抛出 `FileDownloadError`,包含可用的 `httpStatus`、`businessCode` 和 `retryable`。不要把 HTTP 200 或外层 `Success=true` 当作文件下载成功;SDK 会继续检查原生返回码和内容。下载异常不会附带原始请求配置或附件密钥。
28
+
29
+ ## 验证与发布
30
+
31
+ ```sh
32
+ npm run test:download
33
+ npm pack --dry-run
34
+ npm publish
35
+ ```
36
+
37
+ 回归使用本地模拟 HTTP 服务,包括二进制短分片、旧版 JSON、约 26 MiB 文件、范围与业务错误以及 `Message.toFileBox()`,不登录微信或访问真实文件。上述验证不代表真实 CDN 或 home 部署已通过。发布包仅包含 `dist`、README 和 npm 必需元数据。
@@ -1,4 +1,4 @@
1
1
  import { ResponseMsg } from "../core/Message";
2
2
  export declare const say: (content: any, toWxid: string, ats: any) => Promise<ResponseMsg | null | undefined>;
3
3
  export declare const quote: (obj: any, toWxid: string) => Promise<ResponseMsg | null>;
4
- export declare const revoke: (content: any) => Promise<import("axios").AxiosResponse<import("../types/ApiResponse").ApiResponse, any, {}>>;
4
+ export declare const revoke: (content: any) => Promise<import("axios").AxiosResponse<import("../types/ApiResponse").ApiResponse, any>>;
@@ -7,8 +7,12 @@ declare class ToolsApi {
7
7
  GetRealNameInfo(wxid: string): Promise<AxiosResponse<ApiResponse>>;
8
8
  /** 抢红包 */
9
9
  OpenHongBao(body: any): Promise<AxiosResponse<ApiResponse>>;
10
- /** 文件下载 */
10
+ /** 文件分片:兼容旧 JSON 和 v6.0.10 二进制响应,保留 Data.data.buffer。 */
11
11
  DownloadFile(body: any): Promise<AxiosResponse<ApiResponse>>;
12
+ /** 获取原始二进制分片,HTTP 206 的范围会先被校验。 */
13
+ DownloadFileBinary(body: any): Promise<AxiosResponse<Buffer>>;
14
+ /** 按实际返回字节推进分片并拼接完整文件,不需要传 Section。 */
15
+ DownloadFileBuffer(body: any): Promise<Buffer>;
12
16
  /** 文件消息下载 */
13
17
  DownloadFileMessage(body: any): Promise<AxiosResponse<ApiResponse>>;
14
18
  /** 高清图片下载 */
@@ -1,7 +1,17 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.toolsApi = void 0;
4
13
  const axiosInstance_1 = require("../utils/axiosInstance");
14
+ const fileDownload_1 = require("../utils/fileDownload");
5
15
  class ToolsApi {
6
16
  /** 生成支付二维码 */
7
17
  GeneratePayQCode(wxid) {
@@ -15,9 +25,22 @@ class ToolsApi {
15
25
  OpenHongBao(body) {
16
26
  return axiosInstance_1.axiosInstance.post('/TenPay/OpenHongBao', body);
17
27
  }
18
- /** 文件下载 */
28
+ /** 文件分片:兼容旧 JSON 和 v6.0.10 二进制响应,保留 Data.data.buffer。 */
19
29
  DownloadFile(body) {
20
- return axiosInstance_1.axiosInstance.post('/Tools/DownloadFile', body);
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ return (0, fileDownload_1.legacyFileSection)(yield (0, fileDownload_1.fetchFileSection)(body), body);
32
+ });
33
+ }
34
+ /** 获取原始二进制分片,HTTP 206 的范围会先被校验。 */
35
+ DownloadFileBinary(body) {
36
+ return __awaiter(this, void 0, void 0, function* () {
37
+ const section = yield (0, fileDownload_1.fetchFileSection)(body);
38
+ return Object.assign(Object.assign({}, section.response), { data: section.buffer });
39
+ });
40
+ }
41
+ /** 按实际返回字节推进分片并拼接完整文件,不需要传 Section。 */
42
+ DownloadFileBuffer(body) {
43
+ return (0, fileDownload_1.downloadFileBuffer)(body);
21
44
  }
22
45
  /** 文件消息下载 */
23
46
  DownloadFileMessage(body) {
@@ -67,7 +67,7 @@ export declare class Message {
67
67
  * Get the room from the message. Returns null if not in a room.
68
68
  */
69
69
  room(): Promise<Room | null>;
70
- static revoke(obj: any): Promise<Axios.AxiosResponse<import("../types/ApiResponse").ApiResponse, any, {}>>;
70
+ static revoke(obj: any): Promise<Axios.AxiosResponse<import("../types/ApiResponse").ApiResponse, any>>;
71
71
  static forwardTo(text: any, to: any, type: any): void;
72
72
  /**
73
73
  * Get the text content of the message.
@@ -559,37 +559,13 @@ class Message {
559
559
  if (totalSize > LARGE_FILE_THRESHOLD) {
560
560
  return yield this.downloadFileMessage();
561
561
  }
562
- // Get initial chunk size from first request
563
- const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
564
- if (!firstChunk)
565
- return null;
566
- // @ts-ignore - Using any type for response due to mixed axios versions
567
- const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
568
- "AppID": json.msg.appmsg.appid,
569
- "AttachId": json.msg.appmsg.appattach.attachid,
570
- "DataLen": totalSize,
571
- "Section": firstChunk,
572
- "UserName": this.msg.FromUserName,
573
- "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
574
- }));
575
- const downloadedLength = firstResp.data.Data.data.iLen;
576
- const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
577
- let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
578
- // Start from second chunk since we already have the first one
579
- for (let i = 1; i < chunks.length; i++) {
580
- const chunk = chunks[i];
581
- // @ts-ignore - Using any type for response due to mixed axios versions
582
- const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
583
- "AppID": json.msg.appmsg.appid,
584
- "AttachId": json.msg.appmsg.appattach.attachid,
585
- "DataLen": totalSize,
586
- "Section": chunk,
587
- "UserName": this.msg.FromUserName,
588
- "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
589
- }));
590
- const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
591
- completeBuffer = Buffer.concat([completeBuffer, chunkData]);
592
- }
562
+ const completeBuffer = yield ToolsApi_1.toolsApi.DownloadFileBuffer({
563
+ AppID: json.msg.appmsg.appid,
564
+ AttachId: json.msg.appmsg.appattach.attachid,
565
+ DataLen: totalSize,
566
+ UserName: this.msg.FromUserName,
567
+ Wxid: (config === null || config === void 0 ? void 0 : config.wxid) || ''
568
+ });
593
569
  const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, json.msg.appmsg.title);
594
570
  return fileBox;
595
571
  }
package/dist/index.d.ts CHANGED
@@ -38,6 +38,8 @@ export declare class WxBot extends EventEmitter {
38
38
  private httpServer;
39
39
  private loggingOut;
40
40
  private loggingIn;
41
+ private syncInProgress;
42
+ private loginRecoveryInProgress;
41
43
  constructor({ baseUrl, databasePath, deviceType, mode, callbackPort, ProxyIp, ProxyPassword, ProxyUser }: WxBotOptions);
42
44
  qrcode(style?: number): Promise<{
43
45
  qrCode: any;
@@ -59,8 +61,10 @@ export declare class WxBot extends EventEmitter {
59
61
  private handleCallbackMessage;
60
62
  private startPollingMode;
61
63
  private checkLoginAfterDelay;
64
+ private recoverLogin;
62
65
  private checkSyncLoginStatus;
63
66
  private handleMsg;
67
+ private handleMsgOnce;
64
68
  private logOut;
65
69
  /**
66
70
  * 清空登录信息
package/dist/index.js CHANGED
@@ -86,6 +86,8 @@ class WxBot extends events_1.EventEmitter {
86
86
  this.httpServer = null; // HTTP回调服务器
87
87
  this.loggingOut = false; // Flag to prevent concurrent logOut calls
88
88
  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
89
91
  this.baseUrl = baseUrl;
90
92
  this.databasePath = databasePath;
91
93
  this.deviceType = deviceType;
@@ -502,6 +504,27 @@ class WxBot extends events_1.EventEmitter {
502
504
  return true;
503
505
  });
504
506
  }
507
+ recoverLogin(userName) {
508
+ return __awaiter(this, void 0, void 0, function* () {
509
+ if (this.loginRecoveryInProgress) {
510
+ return false;
511
+ }
512
+ this.loginRecoveryInProgress = true;
513
+ try {
514
+ yield LoginApi_1.loginApi.LoginTwiceAutoAuth(userName);
515
+ if (yield this.checkLoginAfterDelay(userName)) {
516
+ this.syncFailureCount = 0;
517
+ return true;
518
+ }
519
+ yield this.logOut();
520
+ yield this.login();
521
+ return false;
522
+ }
523
+ finally {
524
+ this.loginRecoveryInProgress = false;
525
+ }
526
+ });
527
+ }
505
528
  checkSyncLoginStatus(data, userName) {
506
529
  return __awaiter(this, void 0, void 0, function* () {
507
530
  if (data.Code !== 0) {
@@ -511,30 +534,13 @@ class WxBot extends events_1.EventEmitter {
511
534
  if (this.syncFailureCount >= 10) {
512
535
  // 连续失败10次,退出登录
513
536
  this.syncFailureCount = 0; // 重置计数器
514
- // 先尝试二次登录
515
- yield LoginApi_1.loginApi.LoginTwiceAutoAuth(userName);
516
- // 等待60秒后再次检查
517
- if (yield this.checkLoginAfterDelay(userName)) {
518
- return true;
519
- }
520
- yield this.logOut();
521
- yield this.login();
522
- return false;
537
+ return yield this.recoverLogin(userName);
523
538
  }
524
539
  // 失败次数不足3次,继续等待下次sync
525
540
  return false;
526
541
  }
527
542
  if (data.Message === '用户可能退出') {
528
- // 先尝试二次登录
529
- yield LoginApi_1.loginApi.LoginTwiceAutoAuth(userName);
530
- // 等待60秒后再次检查
531
- if (yield this.checkLoginAfterDelay(userName)) {
532
- return true;
533
- }
534
- // 用户可能退出,立即退出登录
535
- yield this.logOut();
536
- yield this.login();
537
- return false;
543
+ return yield this.recoverLogin(userName);
538
544
  }
539
545
  // 成功情况,重置失败计数器
540
546
  this.syncFailureCount = 0;
@@ -542,6 +548,20 @@ class WxBot extends events_1.EventEmitter {
542
548
  });
543
549
  }
544
550
  handleMsg(userName) {
551
+ return __awaiter(this, void 0, void 0, function* () {
552
+ if (this.syncInProgress) {
553
+ return;
554
+ }
555
+ this.syncInProgress = true;
556
+ try {
557
+ yield this.handleMsgOnce(userName);
558
+ }
559
+ finally {
560
+ this.syncInProgress = false;
561
+ }
562
+ });
563
+ }
564
+ handleMsgOnce(userName) {
545
565
  return __awaiter(this, void 0, void 0, function* () {
546
566
  var _a;
547
567
  const resp = yield MsgApi_1.msgApi.Sync({
@@ -0,0 +1,20 @@
1
+ import { AxiosResponse } from 'axios';
2
+ import { ApiResponse } from '../types/ApiResponse';
3
+ export declare class FileDownloadError extends Error {
4
+ httpStatus?: number | undefined;
5
+ businessCode?: number | undefined;
6
+ retryable: boolean;
7
+ constructor(message: string, httpStatus?: number | undefined, businessCode?: number | undefined, retryable?: boolean);
8
+ }
9
+ interface FileSection {
10
+ response: AxiosResponse;
11
+ buffer: Buffer;
12
+ legacy?: ApiResponse;
13
+ }
14
+ /** Read bytes without invoking the shared JSON-only Axios transformer. */
15
+ export declare function fetchFileSection(body: any): Promise<FileSection>;
16
+ /** Preserve the existing SDK envelope for consumers using Data.data.buffer. */
17
+ export declare function legacyFileSection(section: FileSection, body: any): AxiosResponse<ApiResponse>;
18
+ /** Advance by actual received bytes; a successful short section is not EOF. */
19
+ export declare function downloadFileBuffer(body: any): Promise<Buffer>;
20
+ export {};
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.FileDownloadError = void 0;
16
+ exports.fetchFileSection = fetchFileSection;
17
+ exports.legacyFileSection = legacyFileSection;
18
+ exports.downloadFileBuffer = downloadFileBuffer;
19
+ const json_bigint_1 = __importDefault(require("json-bigint"));
20
+ const axiosInstance_1 = require("./axiosInstance");
21
+ const parseJSON = (0, json_bigint_1.default)({ storeAsString: true });
22
+ const FILE_TIMEOUT = 180000;
23
+ const CHUNK_SIZE = 512 * 1024;
24
+ class FileDownloadError extends Error {
25
+ constructor(message, httpStatus, businessCode, retryable = false) {
26
+ super(message);
27
+ this.httpStatus = httpStatus;
28
+ this.businessCode = businessCode;
29
+ this.retryable = retryable;
30
+ this.name = 'FileDownloadError';
31
+ }
32
+ }
33
+ exports.FileDownloadError = FileDownloadError;
34
+ function requestRange(body) {
35
+ var _a, _b;
36
+ const total = body === null || body === void 0 ? void 0 : body.DataLen, start = (_a = body === null || body === void 0 ? void 0 : body.Section) === null || _a === void 0 ? void 0 : _a.StartPos, length = (_b = body === null || body === void 0 ? void 0 : body.Section) === null || _b === void 0 ? void 0 : _b.DataLen;
37
+ if (![total, start, length].every(Number.isSafeInteger) || total <= 0 || total > 0xffffffff || start < 0 || start >= total || length <= 0 || length > 0xffffffff) {
38
+ throw new FileDownloadError('Invalid file size or section range');
39
+ }
40
+ return { total, start, length: Math.min(length, total - start) };
41
+ }
42
+ function header(response, name) {
43
+ var _a, _b;
44
+ return String((_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a[name]) !== null && _b !== void 0 ? _b : '').trim();
45
+ }
46
+ function safeMessage(value, body) {
47
+ if (typeof value !== 'string')
48
+ return '';
49
+ let message = value;
50
+ for (const key of ['AttachId', 'Wxid', 'UserName', 'AppID']) {
51
+ const secret = body === null || body === void 0 ? void 0 : body[key];
52
+ if (typeof secret === 'string' && secret.length > 3)
53
+ message = message.split(secret).join('[redacted]');
54
+ }
55
+ return message.replace(/@cdn_\S+/g, '[redacted]')
56
+ .replace(/https?:\/\/\S+/gi, '[url redacted]')
57
+ .replace(/\b[a-f\d]{24,}\b/gi, '[redacted]')
58
+ .replace(/[A-Za-z\d+/=]{100,}/g, '[data redacted]')
59
+ .replace(/[\r\n]+/g, ' ').slice(0, 400);
60
+ }
61
+ function invalid(message) {
62
+ throw new FileDownloadError(message);
63
+ }
64
+ function decodeLegacy(data, body) {
65
+ var _a, _b;
66
+ const { total, start, length } = requestRange(body);
67
+ const payload = data === null || data === void 0 ? void 0 : data.Data, native = (_a = payload === null || payload === void 0 ? void 0 : payload.BaseResponse) !== null && _a !== void 0 ? _a : payload === null || payload === void 0 ? void 0 : payload.baseResponse;
68
+ if ((data === null || data === void 0 ? void 0 : data.Success) !== true || (data === null || data === void 0 ? void 0 : data.Code) !== 0 || ((native === null || native === void 0 ? void 0 : native.ret) !== undefined && native.ret !== 0)) {
69
+ const code = typeof (data === null || data === void 0 ? void 0 : data.Code) === 'number' ? data.Code : undefined;
70
+ throw new FileDownloadError(`File download rejected${code !== undefined ? ` (Code=${code})` : ''}${(native === null || native === void 0 ? void 0 : native.ret) !== undefined ? ` (ret=${native.ret})` : ''}: ${safeMessage(data === null || data === void 0 ? void 0 : data.Message, body)}`, 200, code);
71
+ }
72
+ const encoded = (_b = payload === null || payload === void 0 ? void 0 : payload.data) === null || _b === void 0 ? void 0 : _b.buffer;
73
+ if (typeof encoded !== 'string' || !encoded.length || !/^[A-Za-z\d+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1) {
74
+ return invalid('File download returned an empty or invalid Base64 section');
75
+ }
76
+ const bytes = Buffer.from(encoded, 'base64');
77
+ if (!bytes.length || bytes.length > length || bytes.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) {
78
+ return invalid('File download returned an invalid section length');
79
+ }
80
+ for (const [value, expected] of [[payload.data.iLen, bytes.length], [payload.dataLen, bytes.length], [payload.totalLen, total], [payload.startPos, start]]) {
81
+ if (value !== undefined && value !== expected)
82
+ return invalid('File download section metadata does not match the requested range');
83
+ }
84
+ return bytes;
85
+ }
86
+ /** Read bytes without invoking the shared JSON-only Axios transformer. */
87
+ function fetchFileSection(body) {
88
+ return __awaiter(this, void 0, void 0, function* () {
89
+ const { total, start, length } = requestRange(body);
90
+ let response;
91
+ try {
92
+ response = yield axiosInstance_1.axiosInstance.post('/Tools/DownloadFile', body, {
93
+ responseType: 'arraybuffer',
94
+ transformResponse: [data => data],
95
+ validateStatus: () => true,
96
+ timeout: FILE_TIMEOUT,
97
+ maxContentLength: Math.ceil(length * 4 / 3) + 64 * 1024,
98
+ });
99
+ }
100
+ catch (error) {
101
+ const errorCode = error === null || error === void 0 ? void 0 : error.code;
102
+ const code = typeof errorCode === 'string' && /^[A-Z_\d]+$/.test(errorCode) ? errorCode : 'NETWORK_ERROR';
103
+ throw new FileDownloadError(`File section request failed (${code})`, undefined, undefined, ['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ECONNREFUSED', 'NETWORK_ERROR'].includes(code));
104
+ }
105
+ const contentType = header(response, 'content-type').split(';')[0].toLowerCase();
106
+ const raw = response.data;
107
+ const bytes = Buffer.isBuffer(raw) ? raw : raw instanceof ArrayBuffer ? Buffer.from(raw) :
108
+ ArrayBuffer.isView(raw) ? Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength) :
109
+ typeof raw === 'string' && contentType === 'application/json' ? Buffer.from(raw) : invalid('File download returned an unsupported response');
110
+ let json;
111
+ if (contentType === 'application/json' || (!contentType && bytes[0] === 123)) {
112
+ try {
113
+ json = parseJSON.parse(bytes.toString('utf8'));
114
+ }
115
+ catch (_a) {
116
+ return invalid(`File download returned invalid JSON (HTTP ${response.status})`);
117
+ }
118
+ }
119
+ if (response.status !== 200 && response.status !== 206) {
120
+ const code = typeof (json === null || json === void 0 ? void 0 : json.Code) === 'number' ? json.Code : undefined;
121
+ throw new FileDownloadError(`File download failed (HTTP ${response.status}${code !== undefined ? `, Code=${code}` : ''}): ${safeMessage(json === null || json === void 0 ? void 0 : json.Message, body)}`, response.status, code, [429, 502, 503, 504].includes(response.status));
122
+ }
123
+ if (json)
124
+ return { response, buffer: decodeLegacy(json, body), legacy: json };
125
+ if (contentType !== 'application/octet-stream' || !bytes.length || bytes.length > length) {
126
+ return invalid('File download returned an empty or invalid binary section');
127
+ }
128
+ const declared = header(response, 'content-length');
129
+ if (declared && (!/^\d+$/.test(declared) || Number(declared) !== bytes.length)) {
130
+ return invalid('File download Content-Length does not match received bytes');
131
+ }
132
+ if (response.status === 206) {
133
+ const range = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(header(response, 'content-range'));
134
+ if (!range || Number(range[1]) !== start || Number(range[2]) !== start + bytes.length - 1 || Number(range[3]) !== total) {
135
+ return invalid('File download Content-Range does not match the requested section');
136
+ }
137
+ }
138
+ else if (start !== 0 || bytes.length !== total) {
139
+ return invalid('File download returned HTTP 200 for an incomplete file');
140
+ }
141
+ return { response, buffer: bytes };
142
+ });
143
+ }
144
+ /** Preserve the existing SDK envelope for consumers using Data.data.buffer. */
145
+ function legacyFileSection(section, body) {
146
+ var _a;
147
+ const { response, buffer, legacy } = section;
148
+ return Object.assign(Object.assign({}, response), { data: Object.assign(Object.assign({}, legacy), { Code: 0, Success: true, Message: (legacy === null || legacy === void 0 ? void 0 : legacy.Message) || '成功', Data: Object.assign(Object.assign({}, legacy === null || legacy === void 0 ? void 0 : legacy.Data), { BaseResponse: { ret: 0 }, totalLen: body.DataLen, startPos: body.Section.StartPos, dataLen: buffer.length, data: Object.assign(Object.assign({}, (_a = legacy === null || legacy === void 0 ? void 0 : legacy.Data) === null || _a === void 0 ? void 0 : _a.data), { iLen: buffer.length, buffer: buffer.toString('base64') }) }) }) });
149
+ }
150
+ /** Advance by actual received bytes; a successful short section is not EOF. */
151
+ function downloadFileBuffer(body) {
152
+ return __awaiter(this, void 0, void 0, function* () {
153
+ const total = body === null || body === void 0 ? void 0 : body.DataLen;
154
+ requestRange(Object.assign(Object.assign({}, body), { Section: { StartPos: 0, DataLen: CHUNK_SIZE } }));
155
+ const chunks = [];
156
+ let offset = 0;
157
+ while (offset < total) {
158
+ const request = Object.assign(Object.assign({}, body), { Section: { StartPos: offset, DataLen: Math.min(CHUNK_SIZE, total - offset) } });
159
+ let section;
160
+ for (let attempt = 0;; attempt++) {
161
+ try {
162
+ section = yield fetchFileSection(request);
163
+ break;
164
+ }
165
+ catch (error) {
166
+ if (!(error instanceof FileDownloadError) || !error.retryable || attempt >= 2)
167
+ throw error;
168
+ yield new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
169
+ }
170
+ }
171
+ chunks.push(section.buffer);
172
+ offset += section.buffer.length;
173
+ }
174
+ return Buffer.concat(chunks, total);
175
+ });
176
+ }
package/package.json CHANGED
@@ -1,42 +1,43 @@
1
- {
2
- "name": "wx2tg-puppet",
3
- "version": "3.0.7",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "files": [
7
- "dist",
8
- "README.md"
9
- ],
10
- "scripts": {
11
- "build": "tsc",
12
- "start": "node dist/index.js",
13
- "dev": "ts-node src/index.ts",
14
- "test": "ts-node src/test.ts",
15
- "prepublishOnly": "npm run build"
16
- },
17
- "keywords": [
18
- "wechat",
19
- "bot",
20
- "puppet"
21
- ],
22
- "author": "",
23
- "license": "ISC",
24
- "description": "微信机器人核心库,可作为依赖被其他项目引入。",
25
- "devDependencies": {
26
- "@types/json-bigint": "^1.0.4",
27
- "@types/qrcode-terminal": "^0.12.2",
28
- "ts-node": "^10.9.2",
29
- "typescript": "^5.8.3"
30
- },
31
- "dependencies": {
32
- "axios": "^1.9.0",
33
- "fast-xml-parser": "^5.2.3",
34
- "file-box": "^1.4.15",
35
- "json-bigint": "^1.0.0",
36
- "qrcode-terminal": "^0.12.0",
37
- "reflect-metadata": "^0.2.2",
38
- "sqlite3": "^5.1.7",
39
- "typeorm": "^0.3.23",
40
- "wx-voice": "^0.2.1"
41
- }
42
- }
1
+ {
2
+ "name": "wx2tg-puppet",
3
+ "version": "3.0.9",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test:download": "npm run build && node --test tests/file-download.test.cjs",
13
+ "start": "node dist/index.js",
14
+ "dev": "ts-node src/index.ts",
15
+ "test": "ts-node src/test.ts",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "wechat",
20
+ "bot",
21
+ "puppet"
22
+ ],
23
+ "author": "",
24
+ "license": "ISC",
25
+ "description": "微信机器人核心库,可作为依赖被其他项目引入。",
26
+ "devDependencies": {
27
+ "@types/json-bigint": "^1.0.4",
28
+ "@types/qrcode-terminal": "^0.12.2",
29
+ "ts-node": "^10.9.2",
30
+ "typescript": "^5.8.3"
31
+ },
32
+ "dependencies": {
33
+ "axios": "^1.9.0",
34
+ "fast-xml-parser": "^5.2.3",
35
+ "file-box": "^1.4.15",
36
+ "json-bigint": "^1.0.0",
37
+ "qrcode-terminal": "^0.12.0",
38
+ "reflect-metadata": "^0.2.2",
39
+ "sqlite3": "^5.1.7",
40
+ "typeorm": "^0.3.23",
41
+ "wx-voice": "^0.2.1"
42
+ }
43
+ }