befly 3.51.0 → 3.53.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.
@@ -1,6 +1,5 @@
1
- import { UAParser } from "ua-parser-js";
2
-
3
1
  import adminTable from "#befly/tables/admin.json";
2
+ import { parseUserAgent } from "#befly/utils/userAgent.js";
4
3
  import { toSessionTtlSeconds } from "#befly/utils/util.js";
5
4
 
6
5
  export default {
@@ -27,7 +26,7 @@ export default {
27
26
  required: ["account", "password", "loginType"],
28
27
  handler: async (befly, ctx) => {
29
28
  const userAgent = ctx.req.headers.get("user-agent") || "";
30
- const uaResult = UAParser(userAgent);
29
+ const uaData = parseUserAgent(userAgent);
31
30
 
32
31
  const logData = {
33
32
  adminId: 0,
@@ -35,15 +34,15 @@ export default {
35
34
  nickname: "",
36
35
  ip: ctx.ip || "",
37
36
  userAgent: userAgent.substring(0, 500),
38
- browserName: uaResult.browser.name || "",
39
- browserVersion: uaResult.browser.version || "",
40
- osName: uaResult.os.name || "",
41
- osVersion: uaResult.os.version || "",
42
- deviceType: uaResult.device.type || "desktop",
43
- deviceVendor: uaResult.device.vendor || "",
44
- deviceModel: uaResult.device.model || "",
45
- engineName: uaResult.engine.name || "",
46
- cpuArchitecture: uaResult.cpu.architecture || "",
37
+ browserName: uaData.browserName,
38
+ browserVersion: uaData.browserVersion,
39
+ osName: uaData.osName,
40
+ osVersion: uaData.osVersion,
41
+ deviceType: uaData.deviceType,
42
+ deviceVendor: uaData.deviceVendor,
43
+ deviceModel: uaData.deviceModel,
44
+ engineName: uaData.engineName,
45
+ cpuArchitecture: uaData.cpuArchitecture,
47
46
  loginTime: Date.now(),
48
47
  loginResult: 0,
49
48
  failReason: ""
@@ -11,13 +11,7 @@ export default {
11
11
  subject: emailLogTable.subject,
12
12
  content: emailLogTable.content,
13
13
  cc: emailLogTable.ccEmail,
14
- bcc: emailLogTable.bccEmail,
15
- isHtml: {
16
- name: "是否HTML",
17
- min: null,
18
- max: null,
19
- input: "string"
20
- }
14
+ bcc: emailLogTable.bccEmail
21
15
  },
22
16
  required: ["to", "subject", "content"],
23
17
  handler: async (befly, ctx) => {
@@ -36,8 +30,7 @@ export default {
36
30
  const result = await befly.email.sendMail({
37
31
  to: ctx.body.to,
38
32
  subject: ctx.body.subject,
39
- html: ctx.body.isHtml ? ctx.body.content : undefined,
40
- text: ctx.body.isHtml ? undefined : ctx.body.content,
33
+ text: ctx.body.content,
41
34
  cc: ctx.body.cc,
42
35
  bcc: ctx.body.bcc
43
36
  });
@@ -1,7 +1,6 @@
1
- import { UAParser } from "ua-parser-js";
2
-
3
1
  import errorReportTable from "#befly/tables/errorReport.json";
4
2
  import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
3
+ import { parseUserAgent } from "#befly/utils/userAgent.js";
5
4
 
6
5
  import { expireTongJiRedisKeys, getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
7
6
 
@@ -17,20 +16,7 @@ function getErrorReportUaData(ctx) {
17
16
  userAgent = ctx.headers.get("user-agent") || "";
18
17
  }
19
18
 
20
- const uaResult = UAParser(userAgent);
21
-
22
- return {
23
- userAgent: userAgent,
24
- browserName: uaResult.browser.name || "",
25
- browserVersion: uaResult.browser.version || "",
26
- osName: uaResult.os.name || "",
27
- osVersion: uaResult.os.version || "",
28
- deviceType: uaResult.device.type || "desktop",
29
- deviceVendor: uaResult.device.vendor || "",
30
- deviceModel: uaResult.device.model || "",
31
- engineName: uaResult.engine.name || "",
32
- cpuArchitecture: uaResult.cpu.architecture || ""
33
- };
19
+ return parseUserAgent(userAgent);
34
20
  }
35
21
 
36
22
  async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorType) {
@@ -0,0 +1,283 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ function encodeBase64(value) {
4
+ return Buffer.from(String(value || ""), "utf8").toString("base64");
5
+ }
6
+
7
+ function sanitizeHeaderValue(value) {
8
+ return String(value || "")
9
+ .replace(/[\r\n]+/g, " ")
10
+ .trim();
11
+ }
12
+
13
+ function encodeHeaderValue(value) {
14
+ const text = sanitizeHeaderValue(value);
15
+ if (/^[\x20-\x7E]*$/.test(text)) {
16
+ return text;
17
+ }
18
+ return `=?UTF-8?B?${encodeBase64(text)}?=`;
19
+ }
20
+
21
+ function normalizeSecureValue(value) {
22
+ return value === true || value === 1;
23
+ }
24
+
25
+ function parseAddressList(value) {
26
+ return String(value || "")
27
+ .split(/[;,]/)
28
+ .map(function (item) {
29
+ const trimmed = item.trim();
30
+ const matched = /<([^<>]+)>/.exec(trimmed);
31
+ return matched ? matched[1].trim() : trimmed;
32
+ })
33
+ .filter(function (item) {
34
+ return item.length > 0;
35
+ });
36
+ }
37
+
38
+ function buildAddressHeader(label, email) {
39
+ const cleanEmail = sanitizeHeaderValue(email);
40
+ if (!label) {
41
+ return cleanEmail;
42
+ }
43
+ return `${encodeHeaderValue(label)} <${cleanEmail}>`;
44
+ }
45
+
46
+ function dotStuffText(value) {
47
+ return String(value || "")
48
+ .replace(/\r\n/g, "\n")
49
+ .replace(/\r/g, "\n")
50
+ .split("\n")
51
+ .map(function (line) {
52
+ return line.startsWith(".") ? `.${line}` : line;
53
+ })
54
+ .join("\r\n");
55
+ }
56
+
57
+ function createMessageId(config) {
58
+ return `<${Date.now()}.${Math.random().toString(36).slice(2)}@${String(config.host || "localhost").replace(/[^a-zA-Z0-9.-]/g, "")}>`;
59
+ }
60
+
61
+ export function createSmtpTextMessage(config, options) {
62
+ const toList = parseAddressList(options.to);
63
+ const ccList = parseAddressList(options.cc);
64
+ const bccList = parseAddressList(options.bcc);
65
+ const messageId = createMessageId(config);
66
+ const headers = [
67
+ `From: ${buildAddressHeader(config.label, config.user)}`,
68
+ `To: ${toList.join(", ")}`,
69
+ ccList.length > 0 ? `Cc: ${ccList.join(", ")}` : "",
70
+ `Subject: ${encodeHeaderValue(options.subject)}`,
71
+ `Message-ID: ${messageId}`,
72
+ "MIME-Version: 1.0",
73
+ "Content-Type: text/plain; charset=utf-8",
74
+ "Content-Transfer-Encoding: 8bit"
75
+ ].filter(function (item) {
76
+ return item.length > 0;
77
+ });
78
+
79
+ return {
80
+ messageId: messageId,
81
+ recipients: toList.concat(ccList, bccList),
82
+ message: `${headers.join("\r\n")}\r\n\r\n${dotStuffText(options.text)}`
83
+ };
84
+ }
85
+
86
+ function createResponseReader() {
87
+ const decoder = new TextDecoder();
88
+ const waiters = [];
89
+ let buffer = "";
90
+ let closedError = null;
91
+
92
+ function readCompleteResponse() {
93
+ const lines = buffer.split(/\r?\n/);
94
+ const completeLines = lines.slice(0, -1);
95
+ if (completeLines.length === 0) {
96
+ return null;
97
+ }
98
+
99
+ const responseLines = [];
100
+ let consumedCount = 0;
101
+ let code = 0;
102
+ for (const line of completeLines) {
103
+ consumedCount = consumedCount + 1;
104
+ responseLines.push(line);
105
+ const matched = /^(\d{3})([ -])/.exec(line);
106
+ if (!matched) {
107
+ continue;
108
+ }
109
+ code = Number(matched[1]);
110
+ if (matched[2] === " ") {
111
+ buffer = lines.slice(consumedCount).join("\n");
112
+ return {
113
+ code: code,
114
+ lines: responseLines,
115
+ text: responseLines.join("\n")
116
+ };
117
+ }
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ function flushWaiters() {
124
+ while (waiters.length > 0) {
125
+ if (closedError) {
126
+ waiters.shift().reject(closedError);
127
+ continue;
128
+ }
129
+
130
+ const response = readCompleteResponse();
131
+ if (!response) {
132
+ return;
133
+ }
134
+ waiters.shift().resolve(response);
135
+ }
136
+ }
137
+
138
+ return {
139
+ append: function (data) {
140
+ buffer += decoder.decode(data, { stream: true });
141
+ flushWaiters();
142
+ },
143
+ close: function (error) {
144
+ closedError =
145
+ error ||
146
+ new Error("SMTP 连接已关闭", {
147
+ cause: null,
148
+ code: "runtime",
149
+ subsystem: "smtp",
150
+ operation: "readResponse"
151
+ });
152
+ flushWaiters();
153
+ },
154
+ read: function () {
155
+ return new Promise(function (resolve, reject) {
156
+ waiters.push({
157
+ resolve: resolve,
158
+ reject: reject
159
+ });
160
+ flushWaiters();
161
+ });
162
+ }
163
+ };
164
+ }
165
+
166
+ async function readResponseWithTimeout(reader, timeout) {
167
+ let timer = null;
168
+ try {
169
+ return await Promise.race([
170
+ reader.read(),
171
+ new Promise(function (_resolve, reject) {
172
+ timer = setTimeout(function () {
173
+ reject(
174
+ new Error("SMTP 响应超时", {
175
+ cause: null,
176
+ code: "runtime",
177
+ subsystem: "smtp",
178
+ operation: "readResponse"
179
+ })
180
+ );
181
+ }, timeout);
182
+ })
183
+ ]);
184
+ } finally {
185
+ clearTimeout(timer);
186
+ }
187
+ }
188
+
189
+ async function expectResponse(reader, expectedCodes, commandName, timeout) {
190
+ const response = await readResponseWithTimeout(reader, timeout);
191
+ if (!expectedCodes.includes(response.code)) {
192
+ throw new Error(`SMTP ${commandName} 响应异常: ${response.text}`, {
193
+ cause: null,
194
+ code: "runtime",
195
+ subsystem: "smtp",
196
+ operation: commandName,
197
+ response: response.text
198
+ });
199
+ }
200
+ return response;
201
+ }
202
+
203
+ function writeCommand(socket, command) {
204
+ socket.write(`${command}\r\n`);
205
+ }
206
+
207
+ export async function sendSmtpTextMail(config, options) {
208
+ const mail = createSmtpTextMessage(config, options);
209
+ if (mail.recipients.length === 0) {
210
+ throw new Error("邮件收件人不能为空", {
211
+ cause: null,
212
+ code: "validation",
213
+ subsystem: "smtp",
214
+ operation: "sendSmtpTextMail"
215
+ });
216
+ }
217
+
218
+ const reader = createResponseReader();
219
+ const socket = await Bun.connect({
220
+ hostname: config.host,
221
+ port: config.port || 25,
222
+ tls: normalizeSecureValue(config.secure),
223
+ socket: {
224
+ data: function (_socket, data) {
225
+ reader.append(data);
226
+ },
227
+ close: function (_socket, error) {
228
+ reader.close(error || null);
229
+ },
230
+ error: function (_socket, error) {
231
+ reader.close(error);
232
+ },
233
+ connectError: function (_socket, error) {
234
+ reader.close(error);
235
+ },
236
+ end: function () {
237
+ reader.close(null);
238
+ }
239
+ }
240
+ });
241
+
242
+ try {
243
+ const timeout = 10000;
244
+ await expectResponse(reader, [220], "connect", timeout);
245
+
246
+ writeCommand(socket, "EHLO localhost");
247
+ await expectResponse(reader, [250], "ehlo", timeout);
248
+
249
+ writeCommand(socket, "AUTH LOGIN");
250
+ await expectResponse(reader, [334], "authUser", timeout);
251
+
252
+ writeCommand(socket, encodeBase64(config.user));
253
+ await expectResponse(reader, [334], "authPassword", timeout);
254
+
255
+ writeCommand(socket, encodeBase64(config.pass));
256
+ await expectResponse(reader, [235], "auth", timeout);
257
+
258
+ writeCommand(socket, `MAIL FROM:<${config.user}>`);
259
+ await expectResponse(reader, [250], "mailFrom", timeout);
260
+
261
+ for (const recipient of mail.recipients) {
262
+ writeCommand(socket, `RCPT TO:<${recipient}>`);
263
+ await expectResponse(reader, [250, 251], "rcptTo", timeout);
264
+ }
265
+
266
+ writeCommand(socket, "DATA");
267
+ await expectResponse(reader, [354], "data", timeout);
268
+
269
+ socket.write(`${mail.message}\r\n.\r\n`);
270
+ const response = await expectResponse(reader, [250], "message", timeout);
271
+
272
+ writeCommand(socket, "QUIT");
273
+ socket.end();
274
+
275
+ return {
276
+ messageId: mail.messageId,
277
+ response: response.text
278
+ };
279
+ } catch (error) {
280
+ socket.end();
281
+ throw error;
282
+ }
283
+ }
package/lib/xmlParse.js CHANGED
@@ -1,21 +1,170 @@
1
- import { XMLParser } from "fast-xml-parser";
1
+ import { isString } from "#befly/utils/is.js";
2
2
 
3
- import { isPlainObject, isString } from "#befly/utils/is.js";
3
+ function decodeXmlText(value) {
4
+ return String(value)
5
+ .replace(/&lt;/g, "<")
6
+ .replace(/&gt;/g, ">")
7
+ .replace(/&amp;/g, "&")
8
+ .replace(/&apos;/g, "'")
9
+ .replace(/&quot;/g, '"');
10
+ }
4
11
 
5
- const xmlParser = new XMLParser();
12
+ function normalizeXmlValue(value) {
13
+ const text = decodeXmlText(value.trim());
14
+ if (text === "") {
15
+ return "";
16
+ }
17
+ const numberValue = Number(text);
18
+ if (Number.isFinite(numberValue) && String(numberValue) === text) {
19
+ return numberValue;
20
+ }
21
+ return text;
22
+ }
6
23
 
7
- function normalizeXmlBody(parsed) {
8
- if (!isPlainObject(parsed)) {
9
- return parsed;
24
+ function assignNodeValue(target, key, value) {
25
+ if (Object.hasOwn(target, key)) {
26
+ if (Array.isArray(target[key])) {
27
+ target[key].push(value);
28
+ return;
29
+ }
30
+ target[key] = [target[key], value];
31
+ return;
10
32
  }
11
33
 
12
- const rootKeys = Object.keys(parsed).filter((key) => !key.startsWith("?"));
13
- if (rootKeys.length !== 1) {
14
- return parsed;
34
+ target[key] = value;
35
+ }
36
+
37
+ function skipIgnoredMarkup(rawXml, startIndex) {
38
+ let index = startIndex;
39
+
40
+ while (index < rawXml.length) {
41
+ while (/\s/.test(rawXml[index])) {
42
+ index += 1;
43
+ }
44
+
45
+ if (rawXml.startsWith("<?", index)) {
46
+ const declarationEnd = rawXml.indexOf("?>", index);
47
+ if (declarationEnd < 0) {
48
+ throw new Error("XML 声明未闭合", {
49
+ cause: null,
50
+ code: "validation",
51
+ subsystem: "xml",
52
+ operation: "skipIgnoredMarkup"
53
+ });
54
+ }
55
+ index = declarationEnd + 2;
56
+ continue;
57
+ }
58
+
59
+ if (rawXml.startsWith("<!--", index)) {
60
+ const commentEnd = rawXml.indexOf("-->", index);
61
+ if (commentEnd < 0) {
62
+ throw new Error("XML 注释未闭合", {
63
+ cause: null,
64
+ code: "validation",
65
+ subsystem: "xml",
66
+ operation: "skipIgnoredMarkup"
67
+ });
68
+ }
69
+ index = commentEnd + 3;
70
+ continue;
71
+ }
72
+
73
+ return index;
15
74
  }
16
75
 
17
- const rootKey = rootKeys[0];
18
- return isPlainObject(parsed[rootKey]) ? parsed[rootKey] : parsed;
76
+ return index;
77
+ }
78
+
79
+ function parseNode(rawXml, startIndex) {
80
+ const openEnd = rawXml.indexOf(">", startIndex);
81
+ if (openEnd < 0) {
82
+ throw new Error("XML 开始标签不完整", {
83
+ cause: null,
84
+ code: "validation",
85
+ subsystem: "xml",
86
+ operation: "parseNode"
87
+ });
88
+ }
89
+
90
+ const openContent = rawXml.slice(startIndex + 1, openEnd).trim();
91
+ const isSelfClosing = openContent.endsWith("/");
92
+ const tagName = (isSelfClosing ? openContent.slice(0, -1).trim() : openContent).split(/\s+/)[0];
93
+ if (!tagName || tagName.startsWith("/")) {
94
+ throw new Error("XML 标签格式不正确", {
95
+ cause: null,
96
+ code: "validation",
97
+ subsystem: "xml",
98
+ operation: "parseNode"
99
+ });
100
+ }
101
+ if (isSelfClosing) {
102
+ return {
103
+ key: tagName,
104
+ value: "",
105
+ index: openEnd + 1
106
+ };
107
+ }
108
+
109
+ let index = openEnd + 1;
110
+ let text = "";
111
+ const children = {};
112
+ let hasChild = false;
113
+
114
+ while (index < rawXml.length) {
115
+ index = skipIgnoredMarkup(rawXml, index);
116
+
117
+ if (rawXml.startsWith("<![CDATA[", index)) {
118
+ const cdataEnd = rawXml.indexOf("]]>", index);
119
+ if (cdataEnd < 0) {
120
+ throw new Error("XML CDATA 未闭合", {
121
+ cause: null,
122
+ code: "validation",
123
+ subsystem: "xml",
124
+ operation: "parseNode"
125
+ });
126
+ }
127
+ text += rawXml.slice(index + 9, cdataEnd);
128
+ index = cdataEnd + 3;
129
+ continue;
130
+ }
131
+
132
+ if (rawXml.startsWith(`</${tagName}>`, index)) {
133
+ index += tagName.length + 3;
134
+ return {
135
+ key: tagName,
136
+ value: hasChild ? children : normalizeXmlValue(text),
137
+ index: index
138
+ };
139
+ }
140
+
141
+ if (rawXml[index] === "<") {
142
+ const child = parseNode(rawXml, index);
143
+ hasChild = true;
144
+ assignNodeValue(children, child.key, child.value);
145
+ index = child.index;
146
+ continue;
147
+ }
148
+
149
+ const nextTagIndex = rawXml.indexOf("<", index);
150
+ if (nextTagIndex < 0) {
151
+ throw new Error("XML 结束标签缺失", {
152
+ cause: null,
153
+ code: "validation",
154
+ subsystem: "xml",
155
+ operation: "parseNode"
156
+ });
157
+ }
158
+ text += rawXml.slice(index, nextTagIndex);
159
+ index = nextTagIndex;
160
+ }
161
+
162
+ throw new Error("XML 标签未闭合", {
163
+ cause: null,
164
+ code: "validation",
165
+ subsystem: "xml",
166
+ operation: "parseNode"
167
+ });
19
168
  }
20
169
 
21
170
  export function xmlParse(value) {
@@ -28,5 +177,17 @@ export function xmlParse(value) {
28
177
  });
29
178
  }
30
179
 
31
- return normalizeXmlBody(xmlParser.parse(value));
180
+ const normalized = value.trim();
181
+ const startIndex = skipIgnoredMarkup(normalized, 0);
182
+ if (normalized[startIndex] !== "<") {
183
+ throw new Error("XML 根节点缺失", {
184
+ cause: null,
185
+ code: "validation",
186
+ subsystem: "xml",
187
+ operation: "xmlParse"
188
+ });
189
+ }
190
+
191
+ const node = parseNode(normalized, startIndex);
192
+ return node.key === "xml" && Object.prototype.toString.call(node.value) === "[object Object]" ? node.value : { [node.key]: node.value };
32
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.51.0",
3
+ "version": "3.53.0",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
@@ -55,11 +55,8 @@
55
55
  "test": "bun test"
56
56
  },
57
57
  "dependencies": {
58
- "fast-xml-parser": "^5.8.0",
59
- "nodemailer": "^8.0.10",
60
58
  "pathe": "^2.0.3",
61
59
  "picomatch": "^4.0.4",
62
- "ua-parser-js": "^2.0.10",
63
60
  "zod": "^4.4.3"
64
61
  },
65
62
  "engines": {
package/plugins/email.js CHANGED
@@ -1,38 +1,15 @@
1
- /**
2
- * 邮件插件
3
- * 提供邮件发送功能,支持 SMTP 配置
4
- */
5
-
6
- import nodemailer from "nodemailer";
7
-
8
1
  import { Logger } from "#befly/lib/logger.js";
2
+ import { sendSmtpTextMail } from "#befly/lib/smtpText.js";
9
3
  import { hasEmailConfig } from "#befly/utils/email.js";
10
4
 
11
5
  export default {
12
6
  order: 7,
13
7
  handler: async function (befly) {
14
8
  const config = befly?.config?.email || {};
15
- let transporter = null;
16
-
17
- if (hasEmailConfig(config)) {
18
- try {
19
- transporter = nodemailer.createTransport({
20
- host: config.host,
21
- port: config.port || 25,
22
- secure: config.secure ?? config.ssl,
23
- auth: {
24
- user: config.user,
25
- pass: config.pass
26
- },
27
- connectionTimeout: config.timeout || 10000
28
- });
29
- } catch (error) {
30
- Logger.warn("邮件服务初始化失败", { err: error });
31
- }
32
- }
9
+ const enabled = hasEmailConfig(config);
33
10
 
34
11
  const sendMail = async function (options) {
35
- if (!transporter) {
12
+ if (!enabled) {
36
13
  Logger.warn("邮件未配置,已禁用发送");
37
14
  return {
38
15
  success: false,
@@ -41,15 +18,12 @@ export default {
41
18
  }
42
19
 
43
20
  try {
44
- const info = await transporter.sendMail({
45
- text: options.text,
46
- html: options.html,
47
- from: options.from || (config.label ? `${config.label} <${config.user}>` : config.user),
21
+ const info = await sendSmtpTextMail(config, {
48
22
  to: options.to,
49
23
  cc: options.cc,
50
24
  bcc: options.bcc,
51
25
  subject: options.subject,
52
- attachments: options.attachments
26
+ text: options.text || ""
53
27
  });
54
28
  return {
55
29
  success: true,
@@ -1,6 +1,6 @@
1
1
  import { DECIMAL_KIND_TYPES, ENUM_KIND_TYPES, FLOAT_KIND_TYPES, INT_KIND_TYPES, JSON_KIND_TYPES, STRING_KIND_TYPES, TEXT_KIND_TYPES } from "#befly/configs/constConfig.js";
2
2
  import { isNonEmptyString, isNumber } from "#befly/utils/is.js";
3
- import { camelCaseKeepUnderscore } from "#befly/utils/util.js";
3
+ import { camelCase, snakeCase } from "#befly/utils/util.js";
4
4
 
5
5
  function resolveSyncDbEnumInput(columnType) {
6
6
  const matched = /^enum\((.*)\)$/.exec(columnType);
@@ -90,7 +90,9 @@ function resolveSyncDbMaxByColumn(columnMeta) {
90
90
  }
91
91
 
92
92
  export function toSyncDbFieldDef(columnMeta) {
93
- const fieldName = camelCaseKeepUnderscore(columnMeta.columnName);
93
+ const camelFieldName = camelCase(columnMeta.columnName);
94
+ const columnName = String(columnMeta.columnName || "");
95
+ const fieldName = snakeCase(camelFieldName) === columnName ? camelFieldName : columnName;
94
96
  const fieldDisplayName = isNonEmptyString(columnMeta.columnComment) ? String(columnMeta.columnComment).trim() : fieldName;
95
97
  const fieldInput = resolveSyncDbInputByColumn(columnMeta);
96
98
  const fieldMax = resolveSyncDbMaxByColumn(columnMeta);
@@ -0,0 +1,172 @@
1
+ const WINDOWS_VERSION_MAP = {
2
+ "10.0": "10",
3
+ 6.3: "8.1",
4
+ 6.2: "8",
5
+ 6.1: "7"
6
+ };
7
+
8
+ function getMatchedVersion(userAgent, regexp) {
9
+ const matched = regexp.exec(userAgent);
10
+ return matched ? matched[1].replace(/_/g, ".") : "";
11
+ }
12
+
13
+ function getBrowserData(userAgent) {
14
+ const browserRules = [
15
+ ["WeChat", /MicroMessenger\/([\d.]+)/],
16
+ ["Edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
17
+ ["Opera", /OPR\/([\d.]+)/],
18
+ ["Firefox", /FxiOS\/([\d.]+)|Firefox\/([\d.]+)/],
19
+ ["Chrome", /CriOS\/([\d.]+)|Chrome\/([\d.]+)/],
20
+ ["Safari", /Version\/([\d.]+).*Safari/],
21
+ ["IE", /MSIE\s([\d.]+)|Trident\/.*rv:([\d.]+)/]
22
+ ];
23
+
24
+ for (const [name, regexp] of browserRules) {
25
+ const matched = regexp.exec(userAgent);
26
+ if (matched) {
27
+ return {
28
+ browserName: name === "Safari" && /iPhone|iPad|iPod/i.test(userAgent) ? "Mobile Safari" : name,
29
+ browserVersion: matched[1] || matched[2] || ""
30
+ };
31
+ }
32
+ }
33
+
34
+ return {
35
+ browserName: "",
36
+ browserVersion: ""
37
+ };
38
+ }
39
+
40
+ function getOsData(userAgent) {
41
+ const androidVersion = getMatchedVersion(userAgent, /Android\s([\d.]+)/);
42
+ if (androidVersion) {
43
+ return {
44
+ osName: "Android",
45
+ osVersion: androidVersion
46
+ };
47
+ }
48
+
49
+ const iosVersion = getMatchedVersion(userAgent, /(?:iPhone|iPad|iPod).*OS\s([\d_]+)/);
50
+ if (iosVersion) {
51
+ return {
52
+ osName: "iOS",
53
+ osVersion: iosVersion
54
+ };
55
+ }
56
+
57
+ const windowsVersion = getMatchedVersion(userAgent, /Windows NT\s([\d.]+)/);
58
+ if (windowsVersion) {
59
+ return {
60
+ osName: "Windows",
61
+ osVersion: WINDOWS_VERSION_MAP[windowsVersion] || windowsVersion
62
+ };
63
+ }
64
+
65
+ const macVersion = getMatchedVersion(userAgent, /Mac OS X\s([\d_]+)/);
66
+ if (macVersion) {
67
+ return {
68
+ osName: "macOS",
69
+ osVersion: macVersion
70
+ };
71
+ }
72
+
73
+ if (/Linux/i.test(userAgent)) {
74
+ return {
75
+ osName: "Linux",
76
+ osVersion: ""
77
+ };
78
+ }
79
+
80
+ return {
81
+ osName: "",
82
+ osVersion: ""
83
+ };
84
+ }
85
+
86
+ function getDeviceType(userAgent) {
87
+ if (/iPad|Tablet|PlayBook|Silk/i.test(userAgent) || (/Android/i.test(userAgent) && !/Mobile/i.test(userAgent))) {
88
+ return "tablet";
89
+ }
90
+ if (/Mobi|iPhone|iPod|Android|Windows Phone/i.test(userAgent)) {
91
+ return "mobile";
92
+ }
93
+ return "desktop";
94
+ }
95
+
96
+ function getAndroidDeviceModel(userAgent) {
97
+ const matched = /Android[\d.\s]*;\s*([^;)]+?)(?:\s+Build\/|;|\))/i.exec(userAgent);
98
+ return matched ? matched[1].trim() : "";
99
+ }
100
+
101
+ function getDeviceData(userAgent) {
102
+ const deviceType = getDeviceType(userAgent);
103
+ if (/iPhone/i.test(userAgent)) {
104
+ return {
105
+ deviceType: deviceType,
106
+ deviceVendor: "Apple",
107
+ deviceModel: "iPhone"
108
+ };
109
+ }
110
+ if (/iPad/i.test(userAgent)) {
111
+ return {
112
+ deviceType: deviceType,
113
+ deviceVendor: "Apple",
114
+ deviceModel: "iPad"
115
+ };
116
+ }
117
+
118
+ return {
119
+ deviceType: deviceType,
120
+ deviceVendor: "",
121
+ deviceModel: getAndroidDeviceModel(userAgent)
122
+ };
123
+ }
124
+
125
+ function getEngineName(userAgent) {
126
+ if (/Chrome|CriOS|Edg|OPR/i.test(userAgent)) {
127
+ return "Blink";
128
+ }
129
+ if (/Firefox|FxiOS/i.test(userAgent)) {
130
+ return "Gecko";
131
+ }
132
+ if (/AppleWebKit|Safari/i.test(userAgent)) {
133
+ return "WebKit";
134
+ }
135
+ if (/Trident|MSIE/i.test(userAgent)) {
136
+ return "Trident";
137
+ }
138
+ return "";
139
+ }
140
+
141
+ function getCpuArchitecture(userAgent) {
142
+ if (/x86_64|Win64|x64|amd64/i.test(userAgent)) {
143
+ return "amd64";
144
+ }
145
+ if (/arm64|aarch64/i.test(userAgent)) {
146
+ return "arm64";
147
+ }
148
+ if (/arm/i.test(userAgent)) {
149
+ return "arm";
150
+ }
151
+ return "";
152
+ }
153
+
154
+ export function parseUserAgent(userAgent) {
155
+ const text = String(userAgent || "");
156
+ const browserData = getBrowserData(text);
157
+ const osData = getOsData(text);
158
+ const deviceData = getDeviceData(text);
159
+
160
+ return {
161
+ userAgent: text,
162
+ browserName: browserData.browserName,
163
+ browserVersion: browserData.browserVersion,
164
+ osName: osData.osName,
165
+ osVersion: osData.osVersion,
166
+ deviceType: deviceData.deviceType,
167
+ deviceVendor: deviceData.deviceVendor,
168
+ deviceModel: deviceData.deviceModel,
169
+ engineName: getEngineName(text),
170
+ cpuArchitecture: getCpuArchitecture(text)
171
+ };
172
+ }