befly 3.52.0 → 3.54.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) {
@@ -52,11 +52,6 @@
52
52
  "name": "字典列表",
53
53
  "path": "/dict",
54
54
  "sort": 2
55
- },
56
- {
57
- "name": "系统配置",
58
- "path": "/system",
59
- "sort": 3
60
55
  }
61
56
  ]
62
57
  },
@@ -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.52.0",
3
+ "version": "3.54.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
+ }
@@ -1,16 +0,0 @@
1
- export default {
2
- name: "获取所有系统配置",
3
- method: "POST",
4
- body: "none",
5
- auth: true,
6
- fields: {},
7
- required: [],
8
- handler: async (befly) => {
9
- const result = await befly.mysql.getAll({
10
- table: "beflySysConfig",
11
- orderBy: ["id#ASC"]
12
- });
13
-
14
- return befly.tool.Yes("操作成功", { lists: result.data.lists });
15
- }
16
- };
@@ -1,36 +0,0 @@
1
- export default {
2
- name: "删除系统配置",
3
- method: "POST",
4
- body: "none",
5
- auth: true,
6
- fields: {
7
- id: { name: "ID", input: "integer", min: 1, max: null }
8
- },
9
- required: ["id"],
10
- handler: async (befly, ctx) => {
11
- try {
12
- const config = await befly.mysql.getOne({
13
- table: "beflySysConfig",
14
- where: { id: ctx.body.id }
15
- });
16
-
17
- if (!config.data?.id) {
18
- return befly.tool.No("配置不存在");
19
- }
20
-
21
- if (config.data.isSystem === 1) {
22
- return befly.tool.No("系统配置不允许删除");
23
- }
24
-
25
- await befly.mysql.delData({
26
- table: "beflySysConfig",
27
- where: { id: ctx.body.id }
28
- });
29
-
30
- return befly.tool.Yes("操作成功");
31
- } catch (error) {
32
- befly.logger.error("删除系统配置失败", error);
33
- return befly.tool.No("操作失败");
34
- }
35
- }
36
- };
@@ -1,37 +0,0 @@
1
- import sysConfigTable from "#befly/tables/sysConfig.json";
2
-
3
- export default {
4
- name: "根据代码获取配置值",
5
- method: "POST",
6
- body: "none",
7
- auth: false,
8
- fields: {
9
- code: sysConfigTable.code
10
- },
11
- required: ["code"],
12
- handler: async (befly, ctx) => {
13
- const config = await befly.mysql.getOne({
14
- table: "beflySysConfig",
15
- where: { code: ctx.body.code }
16
- });
17
-
18
- if (!config.data?.id) {
19
- return befly.tool.No("配置不存在");
20
- }
21
-
22
- let value = config.data.value;
23
- if (config.data.valueType === "number") {
24
- value = Number(config.data.value);
25
- } else if (config.data.valueType === "boolean") {
26
- value = config.data.value === "true" || config.data.value === "1";
27
- } else if (config.data.valueType === "json") {
28
- try {
29
- value = JSON.parse(config.data.value);
30
- } catch {
31
- value = config.data.value;
32
- }
33
- }
34
-
35
- return befly.tool.Yes("操作成功", { code: config.data.code, value: value });
36
- }
37
- };
@@ -1,45 +0,0 @@
1
- import sysConfigTable from "#befly/tables/sysConfig.json";
2
-
3
- export default {
4
- name: "添加系统配置",
5
- method: "POST",
6
- body: "none",
7
- auth: true,
8
- fields: {
9
- name: sysConfigTable.name,
10
- code: sysConfigTable.code,
11
- value: sysConfigTable.value,
12
- valueType: sysConfigTable.valueType,
13
- group: sysConfigTable.group,
14
- sort: sysConfigTable.sort,
15
- isSystem: sysConfigTable.isSystem,
16
- description: sysConfigTable.description
17
- },
18
- required: ["name", "code", "value"],
19
- handler: async (befly, ctx) => {
20
- const existing = await befly.mysql.getOne({
21
- table: "beflySysConfig",
22
- where: { code: ctx.body.code }
23
- });
24
-
25
- if (existing.data?.id) {
26
- return befly.tool.No("配置代码已存在");
27
- }
28
-
29
- const configId = await befly.mysql.insData({
30
- table: "beflySysConfig",
31
- data: {
32
- name: ctx.body.name,
33
- code: ctx.body.code,
34
- value: ctx.body.value,
35
- valueType: ctx.body.valueType === undefined ? "string" : ctx.body.valueType,
36
- group: ctx.body.group === undefined ? "" : ctx.body.group,
37
- sort: ctx.body.sort === undefined ? 0 : ctx.body.sort,
38
- isSystem: ctx.body.isSystem === undefined ? 0 : ctx.body.isSystem,
39
- description: ctx.body.description === undefined ? "" : ctx.body.description
40
- }
41
- });
42
-
43
- return befly.tool.Yes("操作成功", { id: configId.data });
44
- }
45
- };
@@ -1,30 +0,0 @@
1
- import { queryFields } from "#befly/apis/_apis.js";
2
- import sysConfigTable from "#befly/tables/sysConfig.json";
3
-
4
- export default {
5
- name: "获取系统配置列表",
6
- method: "POST",
7
- body: "none",
8
- auth: true,
9
- fields: {
10
- ...queryFields,
11
- state: sysConfigTable.state
12
- },
13
- required: [],
14
- handler: async (befly, ctx) => {
15
- const result = await befly.mysql.getList({
16
- table: "beflySysConfig",
17
- where: {
18
- name$like$or: ctx.body.keyword,
19
- code$like$or: ctx.body.keyword,
20
- group$like$or: ctx.body.keyword,
21
- state: ctx.body.state
22
- },
23
- page: ctx.body.page,
24
- limit: ctx.body.limit,
25
- orderBy: ["group#ASC", "sort#ASC", "id#ASC"]
26
- });
27
-
28
- return befly.tool.Yes("操作成功", result.data);
29
- }
30
- };
@@ -1,57 +0,0 @@
1
- import sysConfigTable from "#befly/tables/sysConfig.json";
2
-
3
- export default {
4
- name: "更新系统配置",
5
- method: "POST",
6
- body: "none",
7
- auth: true,
8
- fields: {
9
- id: sysConfigTable.id,
10
- name: sysConfigTable.name,
11
- code: sysConfigTable.code,
12
- value: sysConfigTable.value,
13
- valueType: sysConfigTable.valueType,
14
- group: sysConfigTable.group,
15
- sort: sysConfigTable.sort,
16
- description: sysConfigTable.description,
17
- state: sysConfigTable.state
18
- },
19
- required: ["id"],
20
- handler: async (befly, ctx) => {
21
- const config = await befly.mysql.getOne({
22
- table: "beflySysConfig",
23
- where: { id: ctx.body.id }
24
- });
25
-
26
- if (!config.data?.id) {
27
- return befly.tool.No("配置不存在");
28
- }
29
-
30
- if (config.data.isSystem === 1) {
31
- await befly.mysql.updData({
32
- table: "beflySysConfig",
33
- data: {
34
- value: ctx.body.value
35
- },
36
- where: { id: ctx.body.id }
37
- });
38
- } else {
39
- await befly.mysql.updData({
40
- table: "beflySysConfig",
41
- data: {
42
- name: ctx.body.name,
43
- code: ctx.body.code,
44
- value: ctx.body.value,
45
- valueType: ctx.body.valueType,
46
- group: ctx.body.group,
47
- sort: ctx.body.sort,
48
- description: ctx.body.description,
49
- state: ctx.body.state
50
- },
51
- where: { id: ctx.body.id }
52
- });
53
- }
54
-
55
- return befly.tool.Yes("操作成功");
56
- }
57
- };
@@ -1,68 +0,0 @@
1
- {
2
- "id": {
3
- "name": "ID",
4
- "input": "integer",
5
- "min": 1,
6
- "max": null
7
- },
8
- "name": {
9
- "name": "配置名称",
10
- "min": 2,
11
- "max": 50,
12
- "input": "string"
13
- },
14
- "code": {
15
- "name": "配置代码",
16
- "input": "regexp",
17
- "check": "@alphanumeric_",
18
- "min": 2,
19
- "max": 100
20
- },
21
- "value": {
22
- "name": "配置值",
23
- "input": "string"
24
- },
25
- "valueType": {
26
- "name": "值类型",
27
- "input": "enum",
28
- "check": "string|number|boolean|json",
29
- "max": 20
30
- },
31
- "group": {
32
- "name": "配置分组",
33
- "input": "string",
34
- "max": 50
35
- },
36
- "sort": {
37
- "name": "排序",
38
- "input": "number",
39
- "max": 9999
40
- },
41
- "isSystem": {
42
- "name": "是否系统配置",
43
- "input": "number",
44
- "max": 1
45
- },
46
- "description": {
47
- "name": "描述说明",
48
- "input": "string",
49
- "max": 500
50
- },
51
- "state": {
52
- "name": "状态(0=已删除,1=正常,2=禁用)",
53
- "input": "enumInteger",
54
- "check": "0|1|2"
55
- },
56
- "createdAt": {
57
- "name": "创建时间",
58
- "input": "number"
59
- },
60
- "updatedAt": {
61
- "name": "更新时间",
62
- "input": "number"
63
- },
64
- "deletedAt": {
65
- "name": "删除时间",
66
- "input": "number"
67
- }
68
- }