chanjs 2.0.4 → 2.0.5
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/common/api.js +20 -0
- package/common/code.js +20 -0
- package/extend/api.js +21 -0
- package/extend/file.js +92 -0
- package/extend/upload.js +80 -0
- package/extend/utils.js +342 -0
- package/global/env.js +5 -0
- package/global/global.js +27 -0
- package/global/import.js +46 -0
- package/global/index.js +3 -0
- package/helper/api.js +14 -0
- package/helper/bind.js +21 -0
- package/helper/dataparse.js +24 -0
- package/helper/db.js +75 -0
- package/helper/file.js +202 -0
- package/helper/html.js +19 -0
- package/helper/index.js +25 -0
- package/helper/ip.js +36 -0
- package/helper/jwt.js +41 -0
- package/helper/loader.js +65 -0
- package/helper/response.js +20 -0
- package/helper/safe.js +263 -0
- package/helper/time.js +28 -0
- package/helper/tree.js +32 -0
- package/index.js +9 -3
- package/middleware/safe.js +144 -0
- package/middleware/waf.js +197 -0
- package/package.json +1 -1
- package/utils/dataparse.js +24 -0
- package/utils/loader.js +23 -24
@@ -0,0 +1,24 @@
|
|
1
|
+
/**
|
2
|
+
* 将数组转换为键值对对象
|
3
|
+
* @param {Array} arr - 包含键值对的数组
|
4
|
+
* @param {string} keyField - 作为键的字段名
|
5
|
+
* @param {string} valueField - 作为值的字段名
|
6
|
+
* @returns {Object} 转换后的对象
|
7
|
+
*/
|
8
|
+
export const arrToObj = (arr, keyField = 'config_key', valueField = 'config_value') => {
|
9
|
+
// 输入验证
|
10
|
+
if (!Array.isArray(arr)) {
|
11
|
+
throw new Error('arrToObj 期望接收数组作为第一个参数');
|
12
|
+
}
|
13
|
+
|
14
|
+
return arr.reduce((result, item) => {
|
15
|
+
if (item && typeof item === 'object') {
|
16
|
+
const key = item[keyField];
|
17
|
+
const value = item[valueField];
|
18
|
+
if (key !== undefined && value !== undefined) {
|
19
|
+
result[key] = value;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
return result;
|
23
|
+
}, {});
|
24
|
+
};
|
package/helper/db.js
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
import knex from "knex";
|
2
|
+
|
3
|
+
const errCode = {
|
4
|
+
ECONNREFUSED: "数据库连接被拒绝,请检查数据库服务是否正常运行。",
|
5
|
+
ER_ACCESS_DENIED_ERROR: "无权限访问,账号或密码错误。",
|
6
|
+
ER_ROW_IS_REFERENCED_2: "无法删除或更新记录,存在关联数据。",
|
7
|
+
ER_BAD_FIELD_ERROR: "SQL语句中包含无效字段,请检查查询条件或列名。",
|
8
|
+
ER_DUP_ENTRY: "插入失败:数据重复,违反唯一性约束。",
|
9
|
+
ER_NO_SUCH_TABLE: "操作失败:目标表不存在。",
|
10
|
+
ETIMEOUT: "数据库操作超时,请稍后再试。",
|
11
|
+
};
|
12
|
+
const getDefaultErrorMessage = (error) => {
|
13
|
+
if (error.message.includes("syntax") || error.message.includes("SQL")) {
|
14
|
+
return "数据库语法错误,请检查您的查询语句。";
|
15
|
+
} else if (error.message.includes("Connection closed")) {
|
16
|
+
return "数据库连接已关闭,请重试。";
|
17
|
+
} else if (error.message.includes("permission")) {
|
18
|
+
return "数据库权限不足,请检查配置。";
|
19
|
+
}
|
20
|
+
return "数据库发生未知错误,请稍后重试。";
|
21
|
+
};
|
22
|
+
export const db =({
|
23
|
+
client = "mysql2",
|
24
|
+
host = "127.0.0.1",
|
25
|
+
user = "root",
|
26
|
+
password = "123456",
|
27
|
+
database = "test",
|
28
|
+
port = 3306,
|
29
|
+
debug = true,
|
30
|
+
charset = "utf8mb4",
|
31
|
+
min = 0,
|
32
|
+
max = 2,
|
33
|
+
}) =>{
|
34
|
+
let config = {
|
35
|
+
client,
|
36
|
+
connection: {
|
37
|
+
host,
|
38
|
+
port,
|
39
|
+
user,
|
40
|
+
password,
|
41
|
+
database,
|
42
|
+
charset,
|
43
|
+
},
|
44
|
+
debug,
|
45
|
+
pool: {
|
46
|
+
//默认为{min: 2, max: 10},连接池配置
|
47
|
+
min,
|
48
|
+
max,
|
49
|
+
},
|
50
|
+
log: {
|
51
|
+
warn(message) {
|
52
|
+
console.error("[knex warn]", message);
|
53
|
+
},
|
54
|
+
error(message) {
|
55
|
+
console.error("[knex error]", message);
|
56
|
+
},
|
57
|
+
debug(message) {
|
58
|
+
console.log("[knex debug]", message);
|
59
|
+
},
|
60
|
+
deprecate(message) {
|
61
|
+
console.warn("[knex deprecate]", message);
|
62
|
+
},
|
63
|
+
trace(message) {
|
64
|
+
console.log("[knex trace]", message);
|
65
|
+
},
|
66
|
+
log(message) {
|
67
|
+
console.log("[knex log]", message);
|
68
|
+
},
|
69
|
+
info(message) {
|
70
|
+
console.log("[knex info]", message);
|
71
|
+
},
|
72
|
+
},
|
73
|
+
};
|
74
|
+
return knex(config);
|
75
|
+
};
|
package/helper/file.js
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
import fs from 'fs/promises';
|
2
|
+
import { accessSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
3
|
+
import path from 'path';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* 获取指定路径的文件树结构
|
7
|
+
*
|
8
|
+
* @param {string} basePath - 要扫描的基础路径
|
9
|
+
* @param {boolean} [deep=true] - 是否深度遍历子目录
|
10
|
+
* @returns {Promise<Array<Object>>} 文件树数组,每个元素包含文件/目录信息
|
11
|
+
* @returns {string} return[].name - 名称
|
12
|
+
* @returns {string} return[].path - 完整路径
|
13
|
+
* @returns {string} return[].relativePath - 相对于APP_PATH的路径
|
14
|
+
* @returns {'directory'|'file'} return[].type - 类型(目录或文件)
|
15
|
+
* @returns {number} return[].size - 大小(字节)
|
16
|
+
* @returns {Date} return[].modified - 最后修改时间
|
17
|
+
* @returns {number} return[].depth - 深度
|
18
|
+
* @returns {Array<Object>} [return[].children] - 子目录内容(仅目录有)
|
19
|
+
* @throws {Error} 当路径不存在或没有访问权限时抛出错误
|
20
|
+
*/
|
21
|
+
export const getFileTree = async (basePath, deep = true) => {
|
22
|
+
try {
|
23
|
+
const stats = await fs.stat(basePath);
|
24
|
+
if (!stats.isDirectory()) {
|
25
|
+
return [];
|
26
|
+
}
|
27
|
+
|
28
|
+
const items = await fs.readdir(basePath);
|
29
|
+
const tree = [];
|
30
|
+
|
31
|
+
for (const item of items) {
|
32
|
+
const itemPath = path.join(basePath, item);
|
33
|
+
const itemStats = await fs.stat(itemPath);
|
34
|
+
|
35
|
+
const treeItem = {
|
36
|
+
name: item,
|
37
|
+
path: itemPath,
|
38
|
+
relativePath: path.relative(APP_PATH, itemPath),
|
39
|
+
type: itemStats.isDirectory() ? 'directory' : 'file',
|
40
|
+
size: itemStats.size,
|
41
|
+
modified: itemStats.mtime,
|
42
|
+
depth: itemPath.split(path.sep).length - path.resolve(APP_PATH).split(path.sep).length
|
43
|
+
};
|
44
|
+
|
45
|
+
if (treeItem.type === 'directory' && deep) {
|
46
|
+
treeItem.children = await getFileTree(itemPath, deep);
|
47
|
+
}
|
48
|
+
|
49
|
+
tree.push(treeItem);
|
50
|
+
}
|
51
|
+
|
52
|
+
// 排序:文件夹在前,文件在后,名称按字母顺序排列
|
53
|
+
return tree.sort((a, b) => {
|
54
|
+
if (a.type === b.type) {
|
55
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
56
|
+
}
|
57
|
+
return a.type === 'directory' ? -1 : 1;
|
58
|
+
});
|
59
|
+
} catch (error) {
|
60
|
+
console.error(`获取文件树失败: ${basePath}`, error);
|
61
|
+
throw error;
|
62
|
+
}
|
63
|
+
};
|
64
|
+
|
65
|
+
/**
|
66
|
+
* 读取文件内容(UTF-8编码)
|
67
|
+
*
|
68
|
+
* @param {string} filePath - 要读取的文件路径
|
69
|
+
* @returns {Promise<string>} 文件内容字符串
|
70
|
+
* @throws {Error} 当文件不存在、无法读取或不是文件时抛出错误
|
71
|
+
*/
|
72
|
+
export const readFileContent = async (filePath) => {
|
73
|
+
try {
|
74
|
+
// 先检查路径是否安全
|
75
|
+
if (!isPathSafe(filePath, APP_PATH)) {
|
76
|
+
throw new Error(`路径不安全: ${filePath}`);
|
77
|
+
}
|
78
|
+
|
79
|
+
// 检查是否为文件
|
80
|
+
const stats = await fs.stat(filePath);
|
81
|
+
if (!stats.isFile()) {
|
82
|
+
throw new Error(`不是文件: ${filePath}`);
|
83
|
+
}
|
84
|
+
|
85
|
+
return await fs.readFile(filePath, 'utf8');
|
86
|
+
} catch (error) {
|
87
|
+
console.error(`读取文件失败: ${filePath}`, error);
|
88
|
+
throw error;
|
89
|
+
}
|
90
|
+
};
|
91
|
+
|
92
|
+
/**
|
93
|
+
* 保存内容到文件(UTF-8编码)
|
94
|
+
*
|
95
|
+
* @param {string} filePath - 要保存的文件路径
|
96
|
+
* @param {string} content - 要写入的内容
|
97
|
+
* @returns {Promise<void>} 无返回值
|
98
|
+
* @throws {Error} 当路径不安全、无法写入或发生其他错误时抛出错误
|
99
|
+
*/
|
100
|
+
export const saveFileContent = async (filePath, content) => {
|
101
|
+
try {
|
102
|
+
// 先检查路径是否安全
|
103
|
+
if (!isPathSafe(filePath, APP_PATH)) {
|
104
|
+
throw new Error(`路径不安全: ${filePath}`);
|
105
|
+
}
|
106
|
+
|
107
|
+
// 确保目录存在
|
108
|
+
const dirname = path.dirname(filePath);
|
109
|
+
await fs.mkdir(dirname, { recursive: true });
|
110
|
+
|
111
|
+
// 写入文件
|
112
|
+
await fs.writeFile(filePath, content, 'utf8');
|
113
|
+
} catch (error) {
|
114
|
+
console.error(`保存文件失败: ${filePath}`, error);
|
115
|
+
throw error;
|
116
|
+
}
|
117
|
+
};
|
118
|
+
|
119
|
+
/**
|
120
|
+
* 验证路径是否在基础路径范围内(防止路径遍历攻击)
|
121
|
+
*
|
122
|
+
* @param {string} requestedPath - 要验证的路径
|
123
|
+
* @param {string} basePath - 基础路径,requestedPath必须在此路径下
|
124
|
+
* @returns {boolean} 如果路径安全则返回true,否则返回false
|
125
|
+
*/
|
126
|
+
export const isPathSafe = (requestedPath, basePath) => {
|
127
|
+
const resolvedRequestedPath = path.resolve(requestedPath);
|
128
|
+
const resolvedBasePath = path.resolve(basePath);
|
129
|
+
|
130
|
+
// 检查请求的路径是否以基础路径为前缀
|
131
|
+
return resolvedRequestedPath.startsWith(resolvedBasePath);
|
132
|
+
};
|
133
|
+
|
134
|
+
/**
|
135
|
+
* 删除指定路径的图片文件
|
136
|
+
*
|
137
|
+
* @param {string} link - 图片文件的路径
|
138
|
+
* @returns {boolean} 成功删除返回true,否则返回false
|
139
|
+
* @description 同步操作,会检查文件是否存在后再删除
|
140
|
+
*/
|
141
|
+
export function delImg(link) {
|
142
|
+
try {
|
143
|
+
// 先检查路径是否安全
|
144
|
+
if (!isPathSafe(link, APP_PATH)) {
|
145
|
+
console.error(`路径不安全: ${link}`);
|
146
|
+
return false;
|
147
|
+
}
|
148
|
+
|
149
|
+
// 检查文件是否存在
|
150
|
+
accessSync(link);
|
151
|
+
|
152
|
+
// 删除文件
|
153
|
+
unlinkSync(link);
|
154
|
+
return true;
|
155
|
+
} catch (err) {
|
156
|
+
console.error(`删除图片失败: ${link}`, err);
|
157
|
+
return false;
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* 递归创建目录(同步操作)
|
163
|
+
*
|
164
|
+
* @param {string} dirname - 要创建的目录路径
|
165
|
+
* @returns {boolean} 成功创建或目录已存在返回true,否则返回false
|
166
|
+
* @description 类似于mkdir -p命令,会创建所有不存在的父目录
|
167
|
+
*/
|
168
|
+
export function mkdirsSync(dirname) {
|
169
|
+
try {
|
170
|
+
// 先检查路径是否安全
|
171
|
+
if (!isPathSafe(dirname, APP_PATH)) {
|
172
|
+
console.error(`路径不安全: ${dirname}`);
|
173
|
+
return false;
|
174
|
+
}
|
175
|
+
|
176
|
+
if (existsSync(dirname)) {
|
177
|
+
return true;
|
178
|
+
}
|
179
|
+
|
180
|
+
// 递归创建父目录
|
181
|
+
const parentDir = path.dirname(dirname);
|
182
|
+
if (mkdirsSync(parentDir)) {
|
183
|
+
mkdirSync(dirname);
|
184
|
+
return true;
|
185
|
+
}
|
186
|
+
|
187
|
+
return false;
|
188
|
+
} catch (err) {
|
189
|
+
console.error(`创建目录失败: ${dirname}`, err);
|
190
|
+
return false;
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
|
195
|
+
export default {
|
196
|
+
mkdirsSync,
|
197
|
+
delImg,
|
198
|
+
isPathSafe,
|
199
|
+
saveFileContent,
|
200
|
+
readFileContent,
|
201
|
+
getFileTree,
|
202
|
+
}
|
package/helper/html.js
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
/**
|
2
|
+
* 处理最常用的HTML特殊字符实体
|
3
|
+
* @param {string} str - 需要解码的字符串
|
4
|
+
* @returns {string} 解码后的字符串
|
5
|
+
*/
|
6
|
+
export const htmlDecode = (str) => {
|
7
|
+
// 非字符串直接返回,避免报错
|
8
|
+
if (typeof str !== 'string') return str;
|
9
|
+
|
10
|
+
// 一次性替换所有常见实体,减少函数调用
|
11
|
+
return str.replace(/&|<|>| |'|"/g, match => ({
|
12
|
+
'&': '&',
|
13
|
+
'<': '<',
|
14
|
+
'>': '>',
|
15
|
+
' ': ' ',
|
16
|
+
''': "'",
|
17
|
+
'"': '"'
|
18
|
+
}[match]));
|
19
|
+
}
|
package/helper/index.js
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
import {
|
2
|
+
getFileTree,
|
3
|
+
readFileContent,
|
4
|
+
saveFileContent,
|
5
|
+
isPathSafe,
|
6
|
+
} from "./file.js";
|
7
|
+
import { setToken, getToken } from "./token.js";
|
8
|
+
import { formatDay, fromatTime } from "./time.js";
|
9
|
+
import { buildTree } from "./tree.js";
|
10
|
+
import { htmlDecode } from "./html.js";
|
11
|
+
import { getIp } from "./ip.js";
|
12
|
+
|
13
|
+
export default {
|
14
|
+
getFileTree,
|
15
|
+
readFileContent,
|
16
|
+
saveFileContent,
|
17
|
+
isPathSafe,
|
18
|
+
setToken,
|
19
|
+
getToken,
|
20
|
+
formatDay,
|
21
|
+
fromatTime,
|
22
|
+
buildTree,
|
23
|
+
htmlDecode,
|
24
|
+
getIp
|
25
|
+
};
|
package/helper/ip.js
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
/**
|
2
|
+
* 获取用户登录IP地址
|
3
|
+
* @param {Object} req - Express请求对象
|
4
|
+
* @returns {string} 格式化后的IP地址,默认返回空字符串
|
5
|
+
*/
|
6
|
+
export const getIp = (req) =>{
|
7
|
+
|
8
|
+
// 优先级从高到低获取可能的IP来源
|
9
|
+
const ipSources = [
|
10
|
+
req.headers['x-forwarded-for'], // 代理场景下的真实IP(可能多个,逗号分隔)
|
11
|
+
req.headers['x-real-ip'], // 部分代理服务器使用
|
12
|
+
req.ip, // Express内置的IP获取(已处理代理)
|
13
|
+
req.connection?.remoteAddress, // 底层连接的远程地址
|
14
|
+
req.socket?.remoteAddress, // 套接字的远程地址
|
15
|
+
req.connection?.socket?.remoteAddress // 连接套接字的远程地址
|
16
|
+
];
|
17
|
+
|
18
|
+
// 从来源中找到第一个有效IP字符串
|
19
|
+
let ip = ipSources.find(source => typeof source === 'string' && source.trim() !== '');
|
20
|
+
|
21
|
+
// 若未找到有效IP,直接返回空
|
22
|
+
if (!ip) return '';
|
23
|
+
|
24
|
+
// 处理多IP情况(取第一个)
|
25
|
+
ip = ip.split(',').shift().trim();
|
26
|
+
|
27
|
+
// IPv6转IPv4处理
|
28
|
+
if (ip === '::1') {
|
29
|
+
return '127.0.0.1'; // 本地环回地址
|
30
|
+
}
|
31
|
+
if (ip.startsWith('::ffff:')) {
|
32
|
+
return ip.slice(7); // 去除IPv6映射的IPv4前缀
|
33
|
+
}
|
34
|
+
|
35
|
+
return ip;
|
36
|
+
}
|
package/helper/jwt.js
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
import jwt from "jsonwebtoken";
|
2
|
+
|
3
|
+
|
4
|
+
/**
|
5
|
+
*
|
6
|
+
* @param {Object} data - 要存储在令牌中的数据(不建议存放敏感信息)
|
7
|
+
* @param {string} secretKey - 用于签名的密钥
|
8
|
+
* @param {string} time - 令牌过期时间,如 '1h'、'7d' 或秒数
|
9
|
+
* @returns {string} 生成的JWT令牌
|
10
|
+
*/
|
11
|
+
export function setToken(data={}, secretKey='chancms', time='7d') {
|
12
|
+
try {
|
13
|
+
return jwt.sign(data, secretKey, {
|
14
|
+
expiresIn: time,
|
15
|
+
algorithm: 'HS256',
|
16
|
+
});
|
17
|
+
} catch (error) {
|
18
|
+
console.error('令牌生成失败:', error.message);
|
19
|
+
throw new Error(`生成令牌失败: ${error.message}`);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
// 获取token
|
24
|
+
export async function getToken(token, secretKey='chancms') {
|
25
|
+
return new Promise((resolve, reject) => {
|
26
|
+
jwt.verify(token, secretKey,(err, decode) => {
|
27
|
+
if (err) {
|
28
|
+
let errorMessage = '令牌验证失败';
|
29
|
+
if (err.name === 'TokenExpiredError') {
|
30
|
+
errorMessage = '令牌已过期';
|
31
|
+
} else if (err.name === 'JsonWebTokenError') {
|
32
|
+
errorMessage = '无效的令牌';
|
33
|
+
}
|
34
|
+
console.error(errorMessage, '令牌异常信息:', err.message);
|
35
|
+
return reject(new Error(errorMessage));
|
36
|
+
} else {
|
37
|
+
resolve(decode);
|
38
|
+
}
|
39
|
+
});
|
40
|
+
});
|
41
|
+
}
|
package/helper/loader.js
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
import fs from "fs";
|
2
|
+
import path from "path";
|
3
|
+
import { bindClass } from "./bind.js";
|
4
|
+
|
5
|
+
/**
|
6
|
+
*
|
7
|
+
* @param {*} module 模块目录
|
8
|
+
* @returns Array
|
9
|
+
* @description 将web模块放到最后加载
|
10
|
+
*/
|
11
|
+
export const loaderSort = (modules = []) => {
|
12
|
+
const index = modules.indexOf("web");
|
13
|
+
if (index !== -1) {
|
14
|
+
const web = modules.splice(index, 1);
|
15
|
+
modules.push(web[0]);
|
16
|
+
}
|
17
|
+
return modules;
|
18
|
+
};
|
19
|
+
|
20
|
+
export const getPackage = async function () {
|
21
|
+
let pkg = await importFile("package.json");
|
22
|
+
return pkg;
|
23
|
+
};
|
24
|
+
|
25
|
+
export const loadConfig = async function () {
|
26
|
+
let config = await importFile("config/index.js");
|
27
|
+
console.log("config", config);
|
28
|
+
return config;
|
29
|
+
};
|
30
|
+
|
31
|
+
/**
|
32
|
+
* 加载指定模块名下的所有控制器文件
|
33
|
+
* @param {string} moduleName - 模块名称
|
34
|
+
* @returns {Promise<Object>} - 控制器对象
|
35
|
+
*/
|
36
|
+
export const loadController = async function (moduleName) {
|
37
|
+
const controller = {};
|
38
|
+
|
39
|
+
const dir = path.join(MODULES_PATH, moduleName, "controller");
|
40
|
+
|
41
|
+
if (!fs.existsSync(dir)) {
|
42
|
+
console.warn(`模块路径不存在,跳过加载控制器: ${dir}`);
|
43
|
+
return controller;
|
44
|
+
}
|
45
|
+
|
46
|
+
const files = fs.readdirSync(dir).filter((file) => file.endsWith(".js"));
|
47
|
+
|
48
|
+
for (const file of files) {
|
49
|
+
const filePath = path.join(dir, file);
|
50
|
+
const name = file.replace(/\.js$/i, ""); // 安全处理 .js 后缀
|
51
|
+
|
52
|
+
try {
|
53
|
+
const module = await importFile(filePath);
|
54
|
+
let obj = module.default || module;
|
55
|
+
|
56
|
+
controller[name] = obj;
|
57
|
+
} catch (e) {
|
58
|
+
console.error(`加载控制器失败: ${filePath}`, e);
|
59
|
+
// 可选:抛出错误或继续加载其他文件
|
60
|
+
// throw e;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
return controller;
|
65
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
export function parseJsonFields(obj) {
|
2
|
+
const result = {};
|
3
|
+
for (const key in obj) {
|
4
|
+
if (!obj.hasOwnProperty(key)) continue;
|
5
|
+
const value = obj[key];
|
6
|
+
// 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
|
7
|
+
if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
|
8
|
+
try {
|
9
|
+
result[key] = JSON.parse(value);
|
10
|
+
} catch (e) {
|
11
|
+
console.warn(`JSON parse failed for field: ${key}`, e);
|
12
|
+
result[key] = value; // 保留原始值
|
13
|
+
}
|
14
|
+
} else {
|
15
|
+
result[key] = value;
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
return result;
|
20
|
+
}
|