befly-weixin-utils 1.1.0
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 +9 -0
- package/aesgcm.js +11 -0
- package/cache.js +3 -0
- package/config.js +18 -0
- package/hash.js +5 -0
- package/http.js +123 -0
- package/index.js +10 -0
- package/nonce.js +10 -0
- package/package.json +84 -0
- package/rsa.js +13 -0
- package/transform.js +72 -0
- package/url.js +9 -0
- package/xml.js +215 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# befly-weixin-utils
|
|
2
|
+
|
|
3
|
+
微信通用底层能力包,提供 HTTP 请求、RSA 签名验签、AES-GCM 解密、字段转换、基础 hash/nonce/url/cache 工具和微信消息 XML 解析/生成。
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { createNonceStr, parseWeixinXml, sha1Hex, signSha256WithRsa } from "befly-weixin-utils";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
XML 工具只覆盖微信消息 XML 场景,不实现完整 XML 标准。
|
package/aesgcm.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function decryptAes256Gcm(options) {
|
|
4
|
+
const ciphertextBuffer = Buffer.from(options.ciphertext, "base64");
|
|
5
|
+
const authTag = ciphertextBuffer.subarray(ciphertextBuffer.length - 16);
|
|
6
|
+
const data = ciphertextBuffer.subarray(0, ciphertextBuffer.length - 16);
|
|
7
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", options.key, options.nonce);
|
|
8
|
+
decipher.setAuthTag(authTag);
|
|
9
|
+
decipher.setAAD(Buffer.from(options.associatedData));
|
|
10
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8");
|
|
11
|
+
}
|
package/cache.js
ADDED
package/config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function validateConfigFields(config, fields, moduleName) {
|
|
2
|
+
const missingFields = [];
|
|
3
|
+
|
|
4
|
+
for (const field of fields) {
|
|
5
|
+
if (!config[field]) {
|
|
6
|
+
missingFields.push(field);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (missingFields.length > 0) {
|
|
11
|
+
throw new Error(`${moduleName}配置缺少必填字段: ${missingFields.join(", ")}`, {
|
|
12
|
+
cause: null,
|
|
13
|
+
code: "validation",
|
|
14
|
+
subsystem: "weixin",
|
|
15
|
+
operation: "validateConfigFields"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
package/hash.js
ADDED
package/http.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
function sleep(ms) {
|
|
2
|
+
return new Promise(function (resolve) {
|
|
3
|
+
setTimeout(resolve, ms);
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function shouldRetry(response) {
|
|
8
|
+
return [408, 429, 500, 502, 503, 504].includes(response.status);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeHeaders(headers) {
|
|
12
|
+
const result = {};
|
|
13
|
+
headers.forEach(function (value, key) {
|
|
14
|
+
result[String(key).toLowerCase()] = value;
|
|
15
|
+
});
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchWithRetry(url, requestOptions, options = {}) {
|
|
20
|
+
const retryCount = options.retryCount ?? 3;
|
|
21
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
22
|
+
let lastError = null;
|
|
23
|
+
let lastResponse = null;
|
|
24
|
+
|
|
25
|
+
for (let attempt = 1; attempt <= retryCount; attempt += 1) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timeoutId = setTimeout(function () {
|
|
28
|
+
controller.abort();
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const finalOptions = Object.assign({}, requestOptions, {
|
|
33
|
+
signal: controller.signal
|
|
34
|
+
});
|
|
35
|
+
const response = await fetch(url, finalOptions);
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
|
|
38
|
+
if (response.ok || !shouldRetry(response)) {
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lastResponse = response;
|
|
43
|
+
if (attempt < retryCount) {
|
|
44
|
+
const delay = 1000 * attempt;
|
|
45
|
+
options.logger?.warn?.({
|
|
46
|
+
msg: "请求返回可重试状态码,准备重试",
|
|
47
|
+
url: url,
|
|
48
|
+
status: response.status,
|
|
49
|
+
attempt: attempt,
|
|
50
|
+
maxRetries: retryCount,
|
|
51
|
+
delayMs: delay
|
|
52
|
+
});
|
|
53
|
+
await sleep(delay);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
clearTimeout(timeoutId);
|
|
57
|
+
lastError = error;
|
|
58
|
+
if (attempt < retryCount) {
|
|
59
|
+
const delay = 1000 * attempt;
|
|
60
|
+
options.logger?.warn?.({
|
|
61
|
+
msg: error.name === "AbortError" ? "请求超时,准备重试" : "请求失败,准备重试",
|
|
62
|
+
url: url,
|
|
63
|
+
attempt: attempt,
|
|
64
|
+
maxRetries: retryCount,
|
|
65
|
+
delayMs: delay,
|
|
66
|
+
error: error.message
|
|
67
|
+
});
|
|
68
|
+
await sleep(delay);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (lastResponse) {
|
|
74
|
+
return lastResponse;
|
|
75
|
+
}
|
|
76
|
+
throw (
|
|
77
|
+
lastError ||
|
|
78
|
+
new Error("请求失败,已达最大重试次数", {
|
|
79
|
+
cause: null,
|
|
80
|
+
code: "runtime",
|
|
81
|
+
subsystem: "weixin",
|
|
82
|
+
operation: "fetchWithRetry"
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function requestJson(url, options, logger) {
|
|
88
|
+
const response = await fetchWithRetry(url, options, {
|
|
89
|
+
logger: logger
|
|
90
|
+
});
|
|
91
|
+
const responseHeaders = normalizeHeaders(response.headers);
|
|
92
|
+
|
|
93
|
+
if (response.status === 204) {
|
|
94
|
+
return {
|
|
95
|
+
status: 204,
|
|
96
|
+
ok: true,
|
|
97
|
+
data: {},
|
|
98
|
+
rawBody: "",
|
|
99
|
+
responseHeaders: responseHeaders
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rawBody = await response.text();
|
|
104
|
+
return {
|
|
105
|
+
status: response.status,
|
|
106
|
+
ok: response.ok,
|
|
107
|
+
data: rawBody ? JSON.parse(rawBody) : {},
|
|
108
|
+
rawBody: rawBody,
|
|
109
|
+
responseHeaders: responseHeaders
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function requestArrayBuffer(url, options, logger) {
|
|
114
|
+
const response = await fetchWithRetry(url, options, {
|
|
115
|
+
logger: logger
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
status: response.status,
|
|
119
|
+
ok: response.ok,
|
|
120
|
+
data: await response.arrayBuffer(),
|
|
121
|
+
responseHeaders: normalizeHeaders(response.headers)
|
|
122
|
+
};
|
|
123
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { decryptAes256Gcm } from "./aesgcm.js";
|
|
2
|
+
export { getSafeExpiresIn } from "./cache.js";
|
|
3
|
+
export { validateConfigFields } from "./config.js";
|
|
4
|
+
export { sha1Hex } from "./hash.js";
|
|
5
|
+
export { requestJson, requestArrayBuffer } from "./http.js";
|
|
6
|
+
export { createNonceStr } from "./nonce.js";
|
|
7
|
+
export { signSha256WithRsa, verifySha256WithRsa, buildWechatpayAuthorization } from "./rsa.js";
|
|
8
|
+
export { deepTransformKeys } from "./transform.js";
|
|
9
|
+
export { normalizeUrlWithoutHash } from "./url.js";
|
|
10
|
+
export { parseWeixinXml, buildWeixinXml, escapeXml, cdata } from "./xml.js";
|
package/nonce.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function createNonceStr(length = 16) {
|
|
2
|
+
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
3
|
+
let result = "";
|
|
4
|
+
|
|
5
|
+
for (let index = 0; index < length; index += 1) {
|
|
6
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return result;
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "befly-weixin-utils",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Befly Weixin Utils - 微信通用工具能力",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"befly",
|
|
8
|
+
"utils",
|
|
9
|
+
"wechat",
|
|
10
|
+
"weixin"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://chensuiyi.me",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"author": "chensuiyi <bimostyle@qq.com>",
|
|
15
|
+
"files": [
|
|
16
|
+
"aesgcm.js",
|
|
17
|
+
"cache.js",
|
|
18
|
+
"config.js",
|
|
19
|
+
"hash.js",
|
|
20
|
+
"http.js",
|
|
21
|
+
"index.js",
|
|
22
|
+
"nonce.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"rsa.js",
|
|
25
|
+
"transform.js",
|
|
26
|
+
"url.js",
|
|
27
|
+
"xml.js",
|
|
28
|
+
"package.json"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": "./index.js",
|
|
34
|
+
"default": "./index.js"
|
|
35
|
+
},
|
|
36
|
+
"./aesgcm": {
|
|
37
|
+
"import": "./aesgcm.js",
|
|
38
|
+
"default": "./aesgcm.js"
|
|
39
|
+
},
|
|
40
|
+
"./config": {
|
|
41
|
+
"import": "./config.js",
|
|
42
|
+
"default": "./config.js"
|
|
43
|
+
},
|
|
44
|
+
"./cache": {
|
|
45
|
+
"import": "./cache.js",
|
|
46
|
+
"default": "./cache.js"
|
|
47
|
+
},
|
|
48
|
+
"./hash": {
|
|
49
|
+
"import": "./hash.js",
|
|
50
|
+
"default": "./hash.js"
|
|
51
|
+
},
|
|
52
|
+
"./http": {
|
|
53
|
+
"import": "./http.js",
|
|
54
|
+
"default": "./http.js"
|
|
55
|
+
},
|
|
56
|
+
"./nonce": {
|
|
57
|
+
"import": "./nonce.js",
|
|
58
|
+
"default": "./nonce.js"
|
|
59
|
+
},
|
|
60
|
+
"./rsa": {
|
|
61
|
+
"import": "./rsa.js",
|
|
62
|
+
"default": "./rsa.js"
|
|
63
|
+
},
|
|
64
|
+
"./transform": {
|
|
65
|
+
"import": "./transform.js",
|
|
66
|
+
"default": "./transform.js"
|
|
67
|
+
},
|
|
68
|
+
"./url": {
|
|
69
|
+
"import": "./url.js",
|
|
70
|
+
"default": "./url.js"
|
|
71
|
+
},
|
|
72
|
+
"./xml": {
|
|
73
|
+
"import": "./xml.js",
|
|
74
|
+
"default": "./xml.js"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public",
|
|
79
|
+
"registry": "https://registry.npmjs.org"
|
|
80
|
+
},
|
|
81
|
+
"engines": {
|
|
82
|
+
"bun": ">=1.3.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/rsa.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function signSha256WithRsa(content, privateKey) {
|
|
4
|
+
return crypto.createSign("RSA-SHA256").update(content).sign(privateKey, "base64");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function verifySha256WithRsa(content, signature, publicKey) {
|
|
8
|
+
return crypto.createVerify("RSA-SHA256").update(content).verify(publicKey, signature, "base64");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildWechatpayAuthorization(options) {
|
|
12
|
+
return `WECHATPAY2-SHA256-RSA2048 mchid="${options.mchId}",nonce_str="${options.nonceStr}",timestamp="${options.timestamp}",signature="${options.signature}",serial_no="${options.serialNo}"`;
|
|
13
|
+
}
|
package/transform.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function camelCase(input) {
|
|
2
|
+
const normalized = String(input)
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
4
|
+
.replace(/[_-]+/g, " ")
|
|
5
|
+
.replace(/\s+/g, " ")
|
|
6
|
+
.trim();
|
|
7
|
+
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const words = normalized.split(" ").map(function (part) {
|
|
13
|
+
return part.toLowerCase();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
words[0] +
|
|
18
|
+
words
|
|
19
|
+
.slice(1)
|
|
20
|
+
.map(function (part) {
|
|
21
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
22
|
+
})
|
|
23
|
+
.join("")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function deepTransformKeys(data, mode = "camel") {
|
|
32
|
+
const transformKey =
|
|
33
|
+
mode === "camel"
|
|
34
|
+
? camelCase
|
|
35
|
+
: function (key) {
|
|
36
|
+
return key;
|
|
37
|
+
};
|
|
38
|
+
const visited = new WeakSet();
|
|
39
|
+
|
|
40
|
+
function transform(value) {
|
|
41
|
+
if (value === null || value === undefined) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
if (visited.has(value)) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
visited.add(value);
|
|
50
|
+
const result = value.map(transform);
|
|
51
|
+
visited.delete(value);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isPlainObject(value)) {
|
|
56
|
+
if (visited.has(value)) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
visited.add(value);
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const [key, val] of Object.entries(value)) {
|
|
62
|
+
result[transformKey(key)] = transform(val);
|
|
63
|
+
}
|
|
64
|
+
visited.delete(value);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return transform(data);
|
|
72
|
+
}
|
package/url.js
ADDED
package/xml.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
function decodeXmlText(value) {
|
|
2
|
+
return String(value)
|
|
3
|
+
.replace(/</g, "<")
|
|
4
|
+
.replace(/>/g, ">")
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/'/g, "'")
|
|
7
|
+
.replace(/"/g, '"');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function assignNodeValue(target, key, value) {
|
|
11
|
+
if (Object.hasOwn(target, key)) {
|
|
12
|
+
if (Array.isArray(target[key])) {
|
|
13
|
+
target[key].push(value);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
target[key] = [target[key], value];
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
target[key] = value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function skipIgnoredMarkup(rawXml, startIndex) {
|
|
24
|
+
let index = startIndex;
|
|
25
|
+
|
|
26
|
+
while (index < rawXml.length) {
|
|
27
|
+
while (/\s/.test(rawXml[index])) {
|
|
28
|
+
index += 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (rawXml.startsWith("<?", index)) {
|
|
32
|
+
const declarationEnd = rawXml.indexOf("?>", index);
|
|
33
|
+
if (declarationEnd < 0) {
|
|
34
|
+
throw new Error("XML 声明未闭合", {
|
|
35
|
+
cause: null,
|
|
36
|
+
code: "validation",
|
|
37
|
+
subsystem: "weixinXml",
|
|
38
|
+
operation: "skipIgnoredMarkup"
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
index = declarationEnd + 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (rawXml.startsWith("<!--", index)) {
|
|
46
|
+
const commentEnd = rawXml.indexOf("-->", index);
|
|
47
|
+
if (commentEnd < 0) {
|
|
48
|
+
throw new Error("XML 注释未闭合", {
|
|
49
|
+
cause: null,
|
|
50
|
+
code: "validation",
|
|
51
|
+
subsystem: "weixinXml",
|
|
52
|
+
operation: "skipIgnoredMarkup"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
index = commentEnd + 3;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return index;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseNode(rawXml, startIndex) {
|
|
66
|
+
const openEnd = rawXml.indexOf(">", startIndex);
|
|
67
|
+
if (openEnd < 0) {
|
|
68
|
+
throw new Error("XML 开始标签不完整", {
|
|
69
|
+
cause: null,
|
|
70
|
+
code: "validation",
|
|
71
|
+
subsystem: "weixinXml",
|
|
72
|
+
operation: "parseNode"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const tagName = rawXml
|
|
77
|
+
.slice(startIndex + 1, openEnd)
|
|
78
|
+
.trim()
|
|
79
|
+
.split(/\s+/)[0];
|
|
80
|
+
if (!tagName || tagName.startsWith("/")) {
|
|
81
|
+
throw new Error("XML 标签格式不正确", {
|
|
82
|
+
cause: null,
|
|
83
|
+
code: "validation",
|
|
84
|
+
subsystem: "weixinXml",
|
|
85
|
+
operation: "parseNode"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let index = openEnd + 1;
|
|
90
|
+
let text = "";
|
|
91
|
+
const children = {};
|
|
92
|
+
let hasChild = false;
|
|
93
|
+
|
|
94
|
+
while (index < rawXml.length) {
|
|
95
|
+
index = skipIgnoredMarkup(rawXml, index);
|
|
96
|
+
|
|
97
|
+
if (rawXml.startsWith("<![CDATA[", index)) {
|
|
98
|
+
const cdataEnd = rawXml.indexOf("]]>", index);
|
|
99
|
+
if (cdataEnd < 0) {
|
|
100
|
+
throw new Error("XML CDATA 未闭合", {
|
|
101
|
+
cause: null,
|
|
102
|
+
code: "validation",
|
|
103
|
+
subsystem: "weixinXml",
|
|
104
|
+
operation: "parseNode"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
text += rawXml.slice(index + 9, cdataEnd);
|
|
108
|
+
index = cdataEnd + 3;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (rawXml.startsWith(`</${tagName}>`, index)) {
|
|
113
|
+
index += tagName.length + 3;
|
|
114
|
+
return {
|
|
115
|
+
key: tagName,
|
|
116
|
+
value: hasChild ? children : decodeXmlText(text.trim()),
|
|
117
|
+
index: index
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (rawXml[index] === "<") {
|
|
122
|
+
const child = parseNode(rawXml, index);
|
|
123
|
+
hasChild = true;
|
|
124
|
+
assignNodeValue(children, child.key, child.value);
|
|
125
|
+
index = child.index;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nextTagIndex = rawXml.indexOf("<", index);
|
|
130
|
+
if (nextTagIndex < 0) {
|
|
131
|
+
throw new Error("XML 结束标签缺失", {
|
|
132
|
+
cause: null,
|
|
133
|
+
code: "validation",
|
|
134
|
+
subsystem: "weixinXml",
|
|
135
|
+
operation: "parseNode"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
text += rawXml.slice(index, nextTagIndex);
|
|
139
|
+
index = nextTagIndex;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error("XML 标签未闭合", {
|
|
143
|
+
cause: null,
|
|
144
|
+
code: "validation",
|
|
145
|
+
subsystem: "weixinXml",
|
|
146
|
+
operation: "parseNode"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseWeixinXml(rawXml) {
|
|
151
|
+
const normalized = String(rawXml || "").trim();
|
|
152
|
+
if (!normalized) {
|
|
153
|
+
throw new Error("parseWeixinXml 输入必须是非空字符串", {
|
|
154
|
+
cause: null,
|
|
155
|
+
code: "validation",
|
|
156
|
+
subsystem: "weixinXml",
|
|
157
|
+
operation: "parseWeixinXml"
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const startIndex = skipIgnoredMarkup(normalized, 0);
|
|
162
|
+
if (normalized[startIndex] !== "<") {
|
|
163
|
+
throw new Error("XML 根节点缺失", {
|
|
164
|
+
cause: null,
|
|
165
|
+
code: "validation",
|
|
166
|
+
subsystem: "weixinXml",
|
|
167
|
+
operation: "parseWeixinXml"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const node = parseNode(normalized, startIndex);
|
|
171
|
+
return node.key === "xml" && Object.prototype.toString.call(node.value) === "[object Object]" ? node.value : node.value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function escapeXml(value) {
|
|
175
|
+
return String(value ?? "")
|
|
176
|
+
.replace(/&/g, "&")
|
|
177
|
+
.replace(/</g, "<")
|
|
178
|
+
.replace(/>/g, ">")
|
|
179
|
+
.replace(/"/g, """)
|
|
180
|
+
.replace(/'/g, "'");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function cdata(value) {
|
|
184
|
+
return `<![CDATA[${String(value ?? "").replace(/\]\]>/g, "]]]]><![CDATA[>")}]]>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildNode(key, value) {
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return value
|
|
190
|
+
.map(function (item) {
|
|
191
|
+
return buildNode(key, item);
|
|
192
|
+
})
|
|
193
|
+
.join("");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (value && Object.prototype.toString.call(value) === "[object Object]") {
|
|
197
|
+
const body = Object.entries(value)
|
|
198
|
+
.map(function ([childKey, childValue]) {
|
|
199
|
+
return buildNode(childKey, childValue);
|
|
200
|
+
})
|
|
201
|
+
.join("");
|
|
202
|
+
return `<${key}>${body}</${key}>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return `<${key}>${cdata(value)}</${key}>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function buildWeixinXml(data) {
|
|
209
|
+
const body = Object.entries(data || {})
|
|
210
|
+
.map(function ([key, value]) {
|
|
211
|
+
return buildNode(key, value);
|
|
212
|
+
})
|
|
213
|
+
.join("");
|
|
214
|
+
return `<xml>${body}</xml>`;
|
|
215
|
+
}
|