wx2tg-puppet 3.0.8 → 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.
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SOLITAIRE_FALLBACK = void 0;
4
+ exports.isBoundedAppMessageXml = isBoundedAppMessageXml;
5
+ exports.getSolitaireDisplayText = getSolitaireDisplayText;
6
+ const fast_xml_parser_1 = require("fast-xml-parser");
7
+ exports.SOLITAIRE_FALLBACK = '[接龙] 内容暂时无法解析,请在微信查看。';
8
+ const MAX_XML_BYTES = 1024 * 1024;
9
+ /** Bound solitaire display parsing; do not impose this size limit on other app types. */
10
+ function isBoundedAppMessageXml(value) {
11
+ return typeof value === 'string' && value.length <= MAX_XML_BYTES &&
12
+ Buffer.byteLength(value, 'utf8') <= MAX_XML_BYTES &&
13
+ !/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(value);
14
+ }
15
+ function decodeXmlText(value) {
16
+ const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
17
+ // A single pass is important: &amp;lt; is visible text "&lt;", not "<".
18
+ return value.replace(/&([^;]*);|&/g, (match, entity) => {
19
+ if (entity && Object.prototype.hasOwnProperty.call(named, entity))
20
+ return named[entity];
21
+ if (!entity || !/^#(?:[0-9]+|x[0-9a-fA-F]+)$/.test(entity)) {
22
+ throw new Error('Invalid XML entity');
23
+ }
24
+ const code = entity[1] === 'x' ? parseInt(entity.slice(2), 16) : Number(entity.slice(1));
25
+ if (!(code === 9 || code === 10 || code === 13 ||
26
+ (code >= 0x20 && code <= 0xd7ff) || (code >= 0xe000 && code <= 0xfffd) ||
27
+ (code >= 0x10000 && code <= 0x10ffff))) {
28
+ throw new Error('Invalid XML character');
29
+ }
30
+ return String.fromCodePoint(code);
31
+ });
32
+ }
33
+ /** Only appmsg.title is display text; solitaire_info is deliberately never interpreted. */
34
+ function getSolitaireDisplayText(payload) {
35
+ var _a;
36
+ try {
37
+ if (!isBoundedAppMessageXml(payload))
38
+ return exports.SOLITAIRE_FALLBACK;
39
+ // Only a single, identifier-shaped WeChat sender line may precede the XML.
40
+ const xml = payload.replace(/^[A-Za-z0-9_.@-]{1,256}:\r?\n/, '').trimStart();
41
+ if (fast_xml_parser_1.XMLValidator.validate(xml) !== true)
42
+ return exports.SOLITAIRE_FALLBACK;
43
+ // Convert CDATA before locating scalar title text, so a literal closing
44
+ // tag inside CDATA cannot end it. Protect CR only inside title text:
45
+ // replacing XML-declaration/root whitespace would create invalid entities.
46
+ const escaped = xml.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'))
47
+ .replace(/(<title\s*>)([\s\S]*?)(<\/title\s*>)/g, (_, open, text, close) => open + text.replace(/\r/g, '&#13;') + close);
48
+ const parsed = new fast_xml_parser_1.XMLParser({
49
+ parseTagValue: false,
50
+ trimValues: false,
51
+ processEntities: false,
52
+ ignoreAttributes: false,
53
+ ignoreDeclaration: true,
54
+ commentPropName: '#comment',
55
+ }).parse(escaped);
56
+ // Require an unambiguous envelope and scalar title (no nested markup/attributes).
57
+ if (Object.keys(parsed).some(key => key !== 'msg'))
58
+ return exports.SOLITAIRE_FALLBACK;
59
+ const app = (_a = parsed.msg) === null || _a === void 0 ? void 0 : _a.appmsg;
60
+ if (!app || Array.isArray(parsed.msg) || Array.isArray(app) ||
61
+ typeof app.type !== 'string' || app.type.trim() !== '53' ||
62
+ typeof app.title !== 'string')
63
+ return exports.SOLITAIRE_FALLBACK;
64
+ const title = decodeXmlText(app.title);
65
+ if (!title.trim() || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/u.test(title)) {
66
+ return exports.SOLITAIRE_FALLBACK;
67
+ }
68
+ for (const point of title) {
69
+ const code = point.codePointAt(0);
70
+ if (code >= 0xd800 && code <= 0xdfff)
71
+ return exports.SOLITAIRE_FALLBACK;
72
+ }
73
+ return title;
74
+ }
75
+ catch (_b) {
76
+ return exports.SOLITAIRE_FALLBACK;
77
+ }
78
+ }
@@ -0,0 +1,11 @@
1
+ import { AxiosResponse } from 'axios';
2
+ import { ApiResponse } from '../types/ApiResponse';
3
+ export interface FileSection {
4
+ response: AxiosResponse;
5
+ buffer: Buffer;
6
+ legacy?: ApiResponse;
7
+ }
8
+ /** The single section engine; DownloadFile alone bypasses the shared JSON transformer. */
9
+ export declare function fetchFileSection(body: any): Promise<FileSection>;
10
+ /** v6 contract: exactly one section, retaining the Axios response and raw bytes. */
11
+ export declare function downloadFileSection(body: any): Promise<AxiosResponse<Buffer>>;
@@ -0,0 +1,153 @@
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.fetchFileSection = fetchFileSection;
16
+ exports.downloadFileSection = downloadFileSection;
17
+ const json_bigint_1 = __importDefault(require("json-bigint"));
18
+ const axiosInstance_1 = require("./axiosInstance");
19
+ const FileDownloadError_1 = require("./FileDownloadError");
20
+ const json = (0, json_bigint_1.default)();
21
+ const responseJson = (0, json_bigint_1.default)({ storeAsString: true });
22
+ const TRANSIENT_CODES = ['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ECONNREFUSED'];
23
+ function businessCode(value) {
24
+ var _a, _b, _c, _d;
25
+ const native = [(_b = (_a = value === null || value === void 0 ? void 0 : value.Data) === null || _a === void 0 ? void 0 : _a.BaseResponse) === null || _b === void 0 ? void 0 : _b.ret, (_d = (_c = value === null || value === void 0 ? void 0 : value.Data) === null || _c === void 0 ? void 0 : _c.baseResponse) === null || _d === void 0 ? void 0 : _d.ret]
26
+ .find(ret => Number.isSafeInteger(ret) && ret !== 0);
27
+ const code = native !== null && native !== void 0 ? native : value === null || value === void 0 ? void 0 : value.Code;
28
+ return Number.isSafeInteger(code) ? code : undefined;
29
+ }
30
+ function header(response, name) {
31
+ const key = Object.keys(response.headers || {}).find(key => key.toLowerCase() === name);
32
+ return key === undefined || response.headers[key] === undefined ? undefined : String(response.headers[key]);
33
+ }
34
+ function decodeLegacy(value, total, start, status) {
35
+ const payload = value === null || value === void 0 ? void 0 : value.Data;
36
+ if ((value === null || value === void 0 ? void 0 : value.Success) !== true || (value === null || value === void 0 ? void 0 : value.Code) !== 0 ||
37
+ [payload === null || payload === void 0 ? void 0 : payload.BaseResponse, payload === null || payload === void 0 ? void 0 : payload.baseResponse].some(native => (native === null || native === void 0 ? void 0 : native.ret) !== undefined && native.ret !== 0)) {
38
+ const code = businessCode(value);
39
+ throw new FileDownloadError_1.FileDownloadError(`legacy response failed or malformed${code === undefined ? '' : ` (code ${code})`}`, status, code);
40
+ }
41
+ const data = payload === null || payload === void 0 ? void 0 : payload.data;
42
+ const encoded = data === null || data === void 0 ? void 0 : data.buffer;
43
+ if (typeof encoded !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) ||
44
+ (encoded.includes('=') && encoded.length % 4 !== 0)) {
45
+ throw new FileDownloadError_1.FileDownloadError('malformed legacy Base64', status);
46
+ }
47
+ const bytes = Buffer.from(encoded, 'base64');
48
+ if (bytes.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) {
49
+ throw new FileDownloadError_1.FileDownloadError('malformed legacy Base64', status);
50
+ }
51
+ for (const [actual, expected] of [[data.iLen, bytes.length], [payload.dataLen, bytes.length],
52
+ [payload.totalLen, total], [payload.startPos, start]]) {
53
+ if (actual !== undefined && actual !== expected) {
54
+ throw new FileDownloadError_1.FileDownloadError('truncated or inconsistent legacy section metadata', status);
55
+ }
56
+ }
57
+ return bytes;
58
+ }
59
+ /** The single section engine; DownloadFile alone bypasses the shared JSON transformer. */
60
+ function fetchFileSection(body) {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ var _a, _b, _c, _d;
63
+ const total = body === null || body === void 0 ? void 0 : body.DataLen;
64
+ const start = (_a = body === null || body === void 0 ? void 0 : body.Section) === null || _a === void 0 ? void 0 : _a.StartPos;
65
+ const length = (_b = body === null || body === void 0 ? void 0 : body.Section) === null || _b === void 0 ? void 0 : _b.DataLen;
66
+ if (!Number.isSafeInteger(total) || total <= 0 || total > 0xffffffff ||
67
+ !Number.isSafeInteger(start) || start < 0 || start >= total ||
68
+ !Number.isSafeInteger(length) || length <= 0 || length > 0xffffffff) {
69
+ throw new FileDownloadError_1.FileDownloadError('invalid total or section length');
70
+ }
71
+ const expected = Math.min(length, total - start);
72
+ // JSONbig keeps uint64 message IDs numeric on the wire without rounding them.
73
+ if (typeof body.NewMsgId === 'number' && !Number.isSafeInteger(body.NewMsgId)) {
74
+ throw new FileDownloadError_1.FileDownloadError('invalid NewMsgId');
75
+ }
76
+ const newMsgId = String((_c = body.NewMsgId) !== null && _c !== void 0 ? _c : 0);
77
+ if (!/^(0|[1-9]\d*)$/.test(newMsgId) || BigInt(newMsgId) > BigInt('18446744073709551615')) {
78
+ throw new FileDownloadError_1.FileDownloadError('invalid NewMsgId');
79
+ }
80
+ let response;
81
+ try {
82
+ response = yield axiosInstance_1.axiosInstance.post('/Tools/DownloadFile', json.stringify(Object.assign(Object.assign({}, body), { NewMsgId: json.parse(newMsgId) })), {
83
+ headers: { 'Content-Type': 'application/json' },
84
+ responseType: 'arraybuffer',
85
+ transformResponse: [data => data],
86
+ validateStatus: () => true,
87
+ // First CDN fallback can consume the server's entire 120-second I/O budget.
88
+ timeout: 180000,
89
+ maxContentLength: Math.ceil(expected / 3) * 4 + 64 * 1024,
90
+ });
91
+ }
92
+ catch (error) {
93
+ const code = error === null || error === void 0 ? void 0 : error.code;
94
+ const timedOut = code === 'ECONNABORTED' || code === 'ETIMEDOUT';
95
+ throw new FileDownloadError_1.FileDownloadError(timedOut ? 'request timed out' :
96
+ 'transport failed (interrupted, truncated or oversized response)', undefined, undefined, typeof code === 'string' && TRANSIENT_CODES.includes(code));
97
+ }
98
+ const contentType = String((_d = header(response, 'content-type')) !== null && _d !== void 0 ? _d : '').split(';')[0].trim().toLowerCase();
99
+ const isJson = /^application\/(?:[\w.+-]+\+)?json$/.test(contentType);
100
+ const raw = response.data;
101
+ const bytes = Buffer.isBuffer(raw) ? raw : raw instanceof ArrayBuffer ? Buffer.from(raw) :
102
+ ArrayBuffer.isView(raw) ? Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength) :
103
+ typeof raw === 'string' && isJson ? Buffer.from(raw, 'utf8') : undefined;
104
+ let value;
105
+ if (bytes && (isJson || !contentType)) {
106
+ try {
107
+ value = responseJson.parse(bytes.toString('utf8'));
108
+ }
109
+ catch ( /* Parser diagnostics can expose response contents. */_e) { /* Parser diagnostics can expose response contents. */ }
110
+ }
111
+ // HTTP status wins over malformed error bodies, including HTML/invalid JSON 503s.
112
+ if (response.status !== 200 && response.status !== 206) {
113
+ const status = Number.isSafeInteger(response.status) ? response.status : undefined;
114
+ const code = businessCode(value);
115
+ throw new FileDownloadError_1.FileDownloadError(`HTTP ${status !== null && status !== void 0 ? status : 0} unexpected status${code === undefined ? '' : ` (code ${code})`}`, status, code, [429, 502, 503, 504].includes(status !== null && status !== void 0 ? status : 0));
116
+ }
117
+ if (!bytes)
118
+ throw new FileDownloadError_1.FileDownloadError('expected binary response body', response.status);
119
+ const declared = header(response, 'content-length');
120
+ if (declared !== undefined && (!/^\d+$/.test(String(declared)) || Number(declared) !== bytes.length)) {
121
+ throw new FileDownloadError_1.FileDownloadError('truncated or inconsistent Content-Length', response.status);
122
+ }
123
+ const legacy = isJson || !contentType;
124
+ if (!legacy && contentType !== 'application/octet-stream') {
125
+ throw new FileDownloadError_1.FileDownloadError('unexpected response Content-Type', response.status);
126
+ }
127
+ const buffer = legacy ? decodeLegacy(value, total, start, response.status) : bytes;
128
+ if (!buffer.length)
129
+ throw new FileDownloadError_1.FileDownloadError('empty section', response.status);
130
+ if (buffer.length > expected)
131
+ throw new FileDownloadError_1.FileDownloadError('oversized section', response.status);
132
+ const range = header(response, 'content-range');
133
+ // Binary 206 requires a range; JSON servers may return HTTP 200 per section.
134
+ if (range !== undefined || (!legacy && response.status === 206)) {
135
+ const match = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(String(range));
136
+ if (!match || Number(match[1]) !== start || Number(match[2]) !== start + buffer.length - 1 ||
137
+ Number(match[3]) !== total) {
138
+ throw new FileDownloadError_1.FileDownloadError('truncated or inconsistent Content-Range', response.status);
139
+ }
140
+ }
141
+ if (!legacy && response.status === 200 && (start !== 0 || buffer.length !== total)) {
142
+ throw new FileDownloadError_1.FileDownloadError('HTTP 200 returned an incomplete file', response.status);
143
+ }
144
+ return Object.assign({ response, buffer }, (legacy ? { legacy: value } : {}));
145
+ });
146
+ }
147
+ /** v6 contract: exactly one section, retaining the Axios response and raw bytes. */
148
+ function downloadFileSection(body) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ const section = yield fetchFileSection(body);
151
+ return Object.assign(Object.assign({}, section.response), { data: section.buffer });
152
+ });
153
+ }
@@ -0,0 +1,9 @@
1
+ import { AxiosResponse } from 'axios';
2
+ import { ApiResponse } from '../types/ApiResponse';
3
+ import { FileSection } from './downloadFileSection';
4
+ export { fetchFileSection, FileSection } from './downloadFileSection';
5
+ export { FileDownloadError } from './FileDownloadError';
6
+ /** Explicit compatibility API for consumers using Data.data.buffer. */
7
+ export declare function legacyFileSection(section: FileSection, body: any): AxiosResponse<ApiResponse>;
8
+ /** Advance by actual bytes, retrying only transient failures at the same offset. */
9
+ export declare function downloadFileBuffer(body: any): Promise<Buffer>;
@@ -0,0 +1,57 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FileDownloadError = exports.fetchFileSection = void 0;
13
+ exports.legacyFileSection = legacyFileSection;
14
+ exports.downloadFileBuffer = downloadFileBuffer;
15
+ const buffer_1 = require("buffer");
16
+ const downloadFileSection_1 = require("./downloadFileSection");
17
+ const FileDownloadError_1 = require("./FileDownloadError");
18
+ var downloadFileSection_2 = require("./downloadFileSection");
19
+ Object.defineProperty(exports, "fetchFileSection", { enumerable: true, get: function () { return downloadFileSection_2.fetchFileSection; } });
20
+ var FileDownloadError_2 = require("./FileDownloadError");
21
+ Object.defineProperty(exports, "FileDownloadError", { enumerable: true, get: function () { return FileDownloadError_2.FileDownloadError; } });
22
+ const CHUNK_SIZE = 512 * 1024;
23
+ /** Explicit compatibility API for consumers using Data.data.buffer. */
24
+ function legacyFileSection(section, body) {
25
+ var _a;
26
+ const { response, buffer, legacy } = section;
27
+ 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') }) }) }) });
28
+ }
29
+ /** Advance by actual bytes, retrying only transient failures at the same offset. */
30
+ function downloadFileBuffer(body) {
31
+ return __awaiter(this, void 0, void 0, function* () {
32
+ const total = body === null || body === void 0 ? void 0 : body.DataLen;
33
+ if (!Number.isSafeInteger(total) || total <= 0 || total > 0xffffffff || total > buffer_1.constants.MAX_LENGTH) {
34
+ throw new FileDownloadError_1.FileDownloadError('invalid total length');
35
+ }
36
+ const chunks = [];
37
+ let offset = 0;
38
+ while (offset < total) {
39
+ const request = Object.assign(Object.assign({}, body), { Section: { StartPos: offset, DataLen: Math.min(CHUNK_SIZE, total - offset) } });
40
+ let section;
41
+ for (let attempt = 0;; attempt++) {
42
+ try {
43
+ section = yield (0, downloadFileSection_1.fetchFileSection)(request);
44
+ break;
45
+ }
46
+ catch (error) {
47
+ if (!(error instanceof FileDownloadError_1.FileDownloadError) || !error.retryable || attempt >= 2)
48
+ throw error;
49
+ yield new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
50
+ }
51
+ }
52
+ chunks.push(section.buffer);
53
+ offset += section.buffer.length;
54
+ }
55
+ return Buffer.concat(chunks, total);
56
+ });
57
+ }
package/package.json CHANGED
@@ -1,42 +1,50 @@
1
- {
2
- "name": "wx2tg-puppet",
3
- "version": "3.0.8",
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": "6.0.16",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist",
8
+ "README.md",
9
+ "VERSION"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "dev": "ts-node src/index.ts",
15
+ "test": "npm run build && node --test test/*.test.cjs tests/*.test.cjs",
16
+ "test:download": "npm run build && node --test test/download.test.cjs tests/file-download.test.cjs",
17
+ "check:version": "node scripts/check-version.cjs",
18
+ "prepack": "npm run check:version && npm run build",
19
+ "prepublishOnly": "npm run check:version && npm test"
20
+ },
21
+ "keywords": [
22
+ "wechat",
23
+ "bot",
24
+ "puppet"
25
+ ],
26
+ "author": "",
27
+ "license": "ISC",
28
+ "description": "微信机器人核心库,可作为依赖被其他项目引入。",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/finalpi/wx2tg-puppet.git"
32
+ },
33
+ "devDependencies": {
34
+ "@types/json-bigint": "^1.0.4",
35
+ "@types/qrcode-terminal": "^0.12.2",
36
+ "ts-node": "^10.9.2",
37
+ "typescript": "^5.8.3"
38
+ },
39
+ "dependencies": {
40
+ "axios": "^1.9.0",
41
+ "fast-xml-parser": "^5.2.3",
42
+ "file-box": "^1.4.15",
43
+ "json-bigint": "^1.0.0",
44
+ "qrcode-terminal": "^0.12.0",
45
+ "reflect-metadata": "^0.2.2",
46
+ "sqlite3": "^5.1.7",
47
+ "typeorm": "^0.3.23",
48
+ "wx-voice": "^0.2.1"
49
+ }
50
+ }