befly 3.74.2 → 3.75.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.
Files changed (65) hide show
  1. package/apis/dashboard/systemResources.js +3 -1
  2. package/apis/dict/_dict.js +1 -1
  3. package/apis/role/_role.js +2 -2
  4. package/apis/tongJi/dailyStats.js +2 -2
  5. package/apis/tongJi/dailyStatsDistribution.js +3 -3
  6. package/apis/tongJi/errorList.js +2 -2
  7. package/apis/tongJi/errorStats.js +6 -6
  8. package/checks/api.js +11 -6
  9. package/checks/items.js +56 -0
  10. package/checks/menu.js +1 -1
  11. package/checks/table.js +4 -5
  12. package/configs/beflyConfig.json +5 -10
  13. package/configs/constConfig.js +44 -3
  14. package/hooks/auth.js +3 -2
  15. package/hooks/parser.js +15 -4
  16. package/hooks/permission.js +3 -2
  17. package/hooks/rateLimit.js +16 -3
  18. package/hooks/validator.js +2 -1
  19. package/index.js +53 -24
  20. package/lib/cacheHelper.js +83 -229
  21. package/lib/connect.js +3 -13
  22. package/lib/dbHelper.js +57 -142
  23. package/lib/dbParse.js +65 -248
  24. package/lib/dbUtil.js +19 -63
  25. package/lib/logger.js +45 -22
  26. package/lib/redisHelper.js +186 -420
  27. package/lib/smtpText.js +7 -18
  28. package/lib/sqlBuilder.js +22 -51
  29. package/lib/xmlParse.js +15 -59
  30. package/package.json +1 -1
  31. package/plugins/cache.js +2 -3
  32. package/plugins/config.js +3 -1
  33. package/plugins/cron.js +2 -2
  34. package/plugins/email.js +3 -1
  35. package/plugins/logger.js +2 -2
  36. package/plugins/mysql.js +5 -10
  37. package/plugins/redis.js +4 -11
  38. package/plugins/tool.js +3 -3
  39. package/plugins/xmlParse.js +3 -1
  40. package/router/api.js +2 -6
  41. package/router/static.js +9 -10
  42. package/schemas/config.js +4 -4
  43. package/scripts/syncDb/context.js +4 -7
  44. package/scripts/syncDb/diff.js +2 -1
  45. package/scripts/syncDb/index.js +2 -6
  46. package/sync/api.js +7 -8
  47. package/sync/cache.js +3 -1
  48. package/sync/dev.js +28 -65
  49. package/sync/menu.js +5 -6
  50. package/sync/syncUtil.js +25 -0
  51. package/utils/calcPerfTime.js +2 -3
  52. package/utils/crypto.js +0 -40
  53. package/utils/deepMerge.js +2 -6
  54. package/utils/error.js +64 -0
  55. package/utils/fieldClear.js +13 -56
  56. package/utils/importDefault.js +11 -12
  57. package/utils/is.js +5 -182
  58. package/utils/loggerUtils.js +3 -1
  59. package/utils/response.js +12 -8
  60. package/utils/scanFiles.js +2 -53
  61. package/utils/scanSources.js +9 -0
  62. package/utils/util.js +25 -222
  63. package/checks/hook.js +0 -39
  64. package/checks/plugin.js +0 -39
  65. package/utils/processInfo.js +0 -39
package/utils/is.js CHANGED
@@ -1,11 +1,5 @@
1
- import { statSync } from "node:fs";
2
-
3
- import { join } from "pathe";
4
-
5
- import { DECIMAL_KIND_TYPES, ENUM_KIND_TYPES, FLOAT_KIND_TYPES, INT_KIND_TYPES, JSON_KIND_TYPES, STRING_KIND_TYPES, TEXT_KIND_TYPES } from "../configs/constConfig.js";
6
-
7
1
  /**
8
- * 判断值是否为 plain object(原型为 Object.prototype 或 null)123。
2
+ * 判断值是否为 plain object(原型为 Object.prototype 或 null)。
9
3
  */
10
4
  export function isPlainObject(value) {
11
5
  if (typeof value !== "object" || value === null) {
@@ -26,61 +20,6 @@ export function isJsonPrimitive(value) {
26
20
  return typeof value === "string" || isNumber(value) || isBoolean(value);
27
21
  }
28
22
 
29
- /**
30
- * 判断值是否为 JSON 对象(plain object,且非数组/非 Date)。
31
- */
32
- export function isJsonObject(value) {
33
- if (!value || typeof value !== "object") {
34
- return false;
35
- }
36
- if (value instanceof Date) {
37
- return false;
38
- }
39
- if (Array.isArray(value)) {
40
- return false;
41
- }
42
- return true;
43
- }
44
-
45
- /**
46
- * 判断值是否为 JSON 结构(数组或对象)。
47
- */
48
- export function isJsonStructure(value) {
49
- return Array.isArray(value) || isJsonObject(value);
50
- }
51
-
52
- /**
53
- * 判断值是否为可 JSON 序列化结构(递归)。
54
- */
55
- export function isJsonValue(value) {
56
- if (isJsonPrimitive(value)) {
57
- return true;
58
- }
59
-
60
- if (Array.isArray(value)) {
61
- for (const item of value) {
62
- if (!isJsonValue(item)) {
63
- return false;
64
- }
65
- }
66
- return true;
67
- }
68
-
69
- if (isJsonObject(value)) {
70
- for (const v of Object.values(value)) {
71
- if (v === undefined) {
72
- continue;
73
- }
74
- if (!isJsonValue(v)) {
75
- return false;
76
- }
77
- }
78
- return true;
79
- }
80
-
81
- return false;
82
- }
83
-
84
23
  /**
85
24
  * 判断值是否为有限数值。
86
25
  */
@@ -173,12 +112,12 @@ export function isNoTrimStringAllowEmpty(value) {
173
112
  return true;
174
113
  }
175
114
 
176
- const firstCharCode = value.charCodeAt(0);
115
+ const firstCharCode = value.codePointAt(0);
177
116
  if (isWhitespaceCharCode(firstCharCode)) {
178
117
  return false;
179
118
  }
180
119
 
181
- const lastCharCode = value.charCodeAt(value.length - 1);
120
+ const lastCharCode = value.codePointAt(value.length - 1);
182
121
  if (isWhitespaceCharCode(lastCharCode)) {
183
122
  return false;
184
123
  }
@@ -290,131 +229,15 @@ export function isRegexInput(input) {
290
229
  return isRegexLiteral(trimmed) || trimmed.startsWith("@");
291
230
  }
292
231
 
293
- /**
294
- * 判断输入是否为枚举规则(首字符非 / 且包含 |)。
295
- */
296
- export function isEnumInput(input) {
297
- const trimmed = String(input || "").trim();
298
- if (trimmed.length === 0) {
299
- return false;
300
- }
301
- return !trimmed.startsWith("/") && trimmed.includes("|");
302
- }
303
-
304
- /**
305
- * 判断输入是否为规则别名(@ 开头)。
306
- */
307
- export function isAliasInput(input) {
308
- return String(input || "")
309
- .trim()
310
- .startsWith("@");
311
- }
312
-
313
- /**
314
- * 激进空值判断(项目约定)。
315
- */
316
- export function isEmpty(value) {
317
- if (isNullable(value)) {
318
- return true;
319
- }
320
-
321
- if (typeof value === "string") {
322
- return value.trim().length === 0;
323
- }
324
-
325
- if (isNumber(value)) {
326
- return value === 0 || Number.isNaN(value);
327
- }
328
-
329
- if (isBoolean(value)) {
330
- return value === false;
331
- }
332
-
333
- if (Array.isArray(value)) {
334
- return value.length === 0;
335
- }
336
-
337
- if (value instanceof Map || value instanceof Set) {
338
- return value.size === 0;
339
- }
340
-
341
- if (isPlainObject(value)) {
342
- return Object.keys(value).length === 0;
343
- }
344
-
345
- return false;
346
- }
347
-
348
232
  /**
349
233
  * 判断当前进程是否为主进程。
350
- * PM2 cluster 的实例 0 是主进程;未由 PM2 启动时视为主进程。
234
+ * bm2 的实例 0 是主进程;未由 bm2 启动时视为主进程。
351
235
  */
352
236
  export function isPrimaryProcess() {
353
- const instance = Bun.env.NODE_APP_INSTANCE;
237
+ const instance = Bun.env.BM2_APP_INSTANCE;
354
238
  return instance === "0" || instance === 0 || instance === undefined;
355
239
  }
356
240
 
357
- export const isDirentDirectory = (parentDir, entry) => {
358
- if (entry.isDirectory()) {
359
- return true;
360
- }
361
-
362
- // 兼容 Windows 下的 junction / workspace link:Dirent.isDirectory() 可能为 false,但它实际指向目录。
363
- if (!entry.isSymbolicLink()) {
364
- return false;
365
- }
366
-
367
- try {
368
- const stats = statSync(join(parentDir, entry.name));
369
- return stats.isDirectory();
370
- } catch {
371
- return false;
372
- }
373
- };
374
-
375
- /**
376
- * 数据库类型判定
377
- */
378
- export function isStringDbType(dbType) {
379
- return STRING_KIND_TYPES.includes(dbType);
380
- }
381
-
382
- export function isTextDbType(dbType) {
383
- return TEXT_KIND_TYPES.includes(dbType);
384
- }
385
-
386
- export function isIntDbType(dbType) {
387
- return INT_KIND_TYPES.includes(dbType);
388
- }
389
-
390
- export function isDecimalDbType(dbType) {
391
- return DECIMAL_KIND_TYPES.includes(dbType);
392
- }
393
-
394
- export function isFloatDbType(dbType) {
395
- return FLOAT_KIND_TYPES.includes(dbType);
396
- }
397
-
398
- export function isJsonDbType(dbType) {
399
- return JSON_KIND_TYPES.includes(dbType);
400
- }
401
-
402
- export function isEnumDbType(dbType) {
403
- return ENUM_KIND_TYPES.includes(dbType);
404
- }
405
-
406
- export function isNumericDbType(dbType) {
407
- return INT_KIND_TYPES.includes(dbType) || DECIMAL_KIND_TYPES.includes(dbType) || FLOAT_KIND_TYPES.includes(dbType);
408
- }
409
-
410
- /**
411
- * SQL Value 判定
412
- */
413
- export function isSqlValue(value) {
414
- if (value instanceof Date) return true;
415
- return isJsonValue(value);
416
- }
417
-
418
241
  /**
419
242
  * 日志敏感键判定
420
243
  */
@@ -1,3 +1,4 @@
1
+ import { errorChainSummary } from "./error.js";
1
2
  import { isBoolean, isNullable, isNumber, isPlainObject, isSensitiveKey, isString } from "./is.js";
2
3
 
3
4
  export function buildSensitiveKeyMatcher(options) {
@@ -34,7 +35,8 @@ export function buildSensitiveKeyMatcher(options) {
34
35
  function sanitizeErrorValue(err, options) {
35
36
  const out = {
36
37
  name: err.name || "Error",
37
- message: err.message || ""
38
+ message: err.message || "",
39
+ chain: errorChainSummary(err)
38
40
  };
39
41
  const state = { nodes: 0 };
40
42
  const visited = new WeakSet();
package/utils/response.js CHANGED
@@ -13,11 +13,16 @@ import { isString } from "./is.js";
13
13
  * @returns Response 对象
14
14
  */
15
15
  export function ErrorResponse(ctx, msg, code = 1, data = null, detail = null, reason = null) {
16
- // 记录拦截日志
16
+ // 记录拦截日志(带请求上下文,便于排查被拦截请求)
17
17
  if (ctx.requestId) {
18
- // requestId/apiPath/user/duration 等字段由调用方显式写入日志上下文,避免在 msg 中重复拼接
19
18
  Logger.info("请求已拦截", {
20
19
  event: "request_blocked",
20
+ requestId: ctx.requestId,
21
+ apiPath: ctx.apiPath,
22
+ apiName: ctx.apiName,
23
+ ip: ctx.ip,
24
+ userId: ctx.userId,
25
+ roleCode: ctx.roleCode,
21
26
  msg: msg,
22
27
  reason: reason,
23
28
  code: code,
@@ -62,8 +67,8 @@ export function FinalResponse(ctx) {
62
67
  msg: result
63
68
  };
64
69
  }
65
- // 如果是对象,自动补充 code: 0
66
- else if (result && typeof result === "object") {
70
+ // 如果是对象,自动补充 code: 0(数组保持原样)
71
+ else if (result && typeof result === "object" && !Array.isArray(result)) {
67
72
  if (!("code" in result)) {
68
73
  const merged = { code: 0 };
69
74
  for (const key of Object.keys(result)) {
@@ -87,11 +92,10 @@ export function FinalResponse(ctx) {
87
92
  return new Response(jsonString, {
88
93
  headers: headers
89
94
  });
90
- } else {
91
- return Response.json(result, {
92
- headers: ctx.corsHeaders
93
- });
94
95
  }
96
+ return Response.json(result, {
97
+ headers: ctx.corsHeaders
98
+ });
95
99
  }
96
100
 
97
101
  // 3. 默认响应:没有生成响应
@@ -2,58 +2,10 @@ import { existsSync } from "node:fs";
2
2
 
3
3
  import { relative, normalize, parse, join } from "pathe";
4
4
 
5
+ import { createError } from "./error.js";
5
6
  import { importDefault } from "./importDefault.js";
6
7
  import { isPlainObject } from "./is.js";
7
8
 
8
- // [ hooks ]-112 {
9
- // source: "core",
10
- // type: "hook",
11
- // filePath: "D:/codes/befly/packages/core/hooks/auth.js",
12
- // relativePath: "auth",
13
- // fileName: "auth",
14
- // apiPath: "/api/core/auth",
15
- // name: "auth",
16
- // order: 1,
17
- // handler: [AsyncFunction: handler],
18
- // }
19
- // 🔥[ plugins ]-112 {
20
- // source: "core",
21
- // type: "plugin",
22
- // filePath: "D:/codes/befly/packages/core/plugins/cache.js",
23
- // relativePath: "cache",
24
- // fileName: "cache",
25
- // apiPath: "/api/core/cache",
26
- // name: "cache",
27
- // order: 1,
28
- // handler: [AsyncFunction: handler],
29
- // }
30
- // 🔥[ tables ]-112 {
31
- // source: "core",
32
- // type: "table",
33
- // filePath: "D:/codes/befly/packages/core/tables/admin.json",
34
- // relativePath: "admin",
35
- // fileName: "admin",
36
- // apiPath: "/api/core/admin",
37
- // "lastLoginTime", "lastLoginIp"
38
- // ],
39
- // name: "admin",
40
- // }
41
- // 🔥[ apis ]-112 {
42
- // source: "core",
43
- // type: "api",
44
- // filePath: "D:/codes/befly/packages/core/apis/sysConfig/all.js",
45
- // relativePath: "sysConfig/all",
46
- // fileName: "all",
47
- // apiPath: "/api/core/sysConfig/all",
48
- // name: "all",
49
- // method: "POST",
50
- // body: "none",
51
- // auth: true,
52
- // fields: {},
53
- // required: [],
54
- // handler: [AsyncFunction: handler],
55
- // }
56
-
57
9
  /**
58
10
  * 扫描指定目录下的文件
59
11
  * @param dir 目录路径
@@ -116,9 +68,6 @@ export async function scanFiles(dir, source, type, pattern) {
116
68
  }
117
69
  return results;
118
70
  } catch (error) {
119
- throw new Error(`扫描失败: source=${source} type=${type} dir=${dir} pattern=${pattern}`, {
120
- cause: error,
121
- code: "runtime"
122
- });
71
+ throw createError(`扫描失败: source=${source} type=${type} dir=${dir} pattern=${pattern}`, { cause: error, code: "runtime" });
123
72
  }
124
73
  }
@@ -9,6 +9,7 @@ import {
9
9
  coreApiDir,
10
10
  appApiDir
11
11
  } from "../paths.js";
12
+ import { createError } from "./error.js";
12
13
  import { scanFiles } from "./scanFiles.js";
13
14
 
14
15
  export const scanSources = async () => {
@@ -54,6 +55,14 @@ export const scanSources = async () => {
54
55
  }
55
56
  const allAppApis = await scanFiles(appApiDir, "app", "api", "**/*.js");
56
57
  for (const item of allAppApis) {
58
+ // 禁止 app 接口目录下的 core 一级目录:会与框架内置 /api/core 命名空间冲突,从源头保证 apiPath 唯一
59
+ if (item.relativePath.split("/")[0] === "core") {
60
+ throw createError(`app 接口目录下禁止存在 core 一级目录:${item.filePath}`, {
61
+ code: "policy",
62
+ subsystem: "scan",
63
+ operation: "scanSources"
64
+ });
65
+ }
57
66
  apis.push(item);
58
67
  }
59
68
 
package/utils/util.js CHANGED
@@ -4,7 +4,26 @@
4
4
  * - 仅在 packages/core 内部使用;core 对外不承诺这些路径导出。
5
5
  */
6
6
 
7
- import { isBoolean, isNullable, isNumber, isPlainObject } from "./is.js";
7
+ import { createError } from "./error.js";
8
+ import { isPlainObject } from "./is.js";
9
+
10
+ /**
11
+ * 通用等待工具:轮询直到条件满足或超时(while + sleep 实现,无定时器句柄,无泄漏风险)
12
+ * @param {string} label - 等待说明(用于超时错误信息)
13
+ * @param {Function} check - 异步条件函数,返回 true 表示就绪
14
+ * @param {number} [intervalMs=200] - 轮询间隔(毫秒)
15
+ * @param {number} [timeoutMs=60000] - 超时时间(毫秒)
16
+ */
17
+ export async function waitFor({ label, check, intervalMs = 200, timeoutMs = 60000 }) {
18
+ const deadline = Date.now() + timeoutMs;
19
+ while (Date.now() < deadline) {
20
+ if (await check()) {
21
+ return;
22
+ }
23
+ await Bun.sleep(intervalMs);
24
+ }
25
+ throw createError(`等待${label}超时`, { code: "runtime", subsystem: "start", operation: "waitFor" });
26
+ }
8
27
 
9
28
  /**
10
29
  * 判断 bigint 是否可安全转换为 number。
@@ -32,43 +51,6 @@ export function canConvertToNumber(value) {
32
51
  return asNumber;
33
52
  }
34
53
 
35
- /**
36
- * 安全格式化值用于日志预览(截断长文本,避免抛错)。
37
- */
38
- export function formatValuePreview(value) {
39
- if (value === null) {
40
- return "null";
41
- }
42
- if (value === undefined) {
43
- return "undefined";
44
- }
45
-
46
- if (typeof value === "string") {
47
- const s = value.length > 80 ? `${value.slice(0, 80)}...` : value;
48
- return JSON.stringify(s);
49
- }
50
-
51
- if (isNumber(value) || isBoolean(value) || typeof value === "bigint") {
52
- return String(value);
53
- }
54
-
55
- if (typeof value === "function") {
56
- return "[Function]";
57
- }
58
-
59
- try {
60
- const s = JSON.stringify(value);
61
- if (typeof s === "string") {
62
- return s.length > 120 ? `${s.slice(0, 120)}...` : s;
63
- }
64
- } catch {
65
- // ignore
66
- }
67
-
68
- const s = String(value);
69
- return s.length > 120 ? `${s.slice(0, 120)}...` : s;
70
- }
71
-
72
54
  /**
73
55
  * 获取值的类型标识(数组/对象等做细分)。
74
56
  */
@@ -88,16 +70,6 @@ export function getTypeTag(value) {
88
70
  return "object";
89
71
  }
90
72
 
91
- // 正则字符转义列表
92
- const REGEXP_SPECIAL = /[\^$.*+?()[\]{}|]/g;
93
-
94
- /**
95
- * 转义字符串中的正则特殊字符。
96
- */
97
- export function escapeRegExp(input) {
98
- return String(input).replace(REGEXP_SPECIAL, "\\$&");
99
- }
100
-
101
73
  /**
102
74
  * 归一化正整数:非法值回退,超出范围裁剪。
103
75
  */
@@ -109,24 +81,6 @@ export function normalizePositiveInt(value, fallback, min, max) {
109
81
  return v;
110
82
  }
111
83
 
112
- /**
113
- * 遍历 plain object 的自有字段。
114
- */
115
- export function forOwn(obj, iteratee) {
116
- if (typeof iteratee !== "function") {
117
- return;
118
- }
119
-
120
- if (!isPlainObject(obj)) {
121
- return;
122
- }
123
-
124
- const record = obj;
125
- for (const key of Object.keys(record)) {
126
- iteratee(record[key], key);
127
- }
128
- }
129
-
130
84
  // 将字符串拆分为词段(用于 camelCase/snakeCase)
131
85
  function toWordParts(input) {
132
86
  const normalized = String(input)
@@ -170,19 +124,6 @@ export function camelCase(input) {
170
124
  return [first].concat(rest).join("");
171
125
  }
172
126
 
173
- /**
174
- * 字段名专用小驼峰转换。
175
- * - 只要原始值包含下划线,就直接保留原字段名。
176
- * - 其余场景继续走普通小驼峰转换。
177
- */
178
- export function camelCaseKeepUnderscore(input) {
179
- if (input.includes("_")) {
180
- return input;
181
- }
182
-
183
- return camelCase(input);
184
- }
185
-
186
127
  /**
187
128
  * 把字符串转为 snake_case。
188
129
  * - 主要用于表名/字段名(例如 userId -> user_id)。
@@ -196,22 +137,6 @@ export function snakeCase(input) {
196
137
  return parts.map((p) => p.toLowerCase()).join("_");
197
138
  }
198
139
 
199
- /**
200
- * 对象字段名转小驼峰
201
- * @param obj - 源对象
202
- * @returns 字段名转为小驼峰格式的新对象
203
- */
204
- export const keysToCamel = (obj) => {
205
- if (!obj || !isPlainObject(obj)) return obj;
206
-
207
- const result = {};
208
- for (const [key, value] of Object.entries(obj)) {
209
- const camelKey = camelCase(key);
210
- result[camelKey] = value;
211
- }
212
- return result;
213
- };
214
-
215
140
  /**
216
141
  * 对象字段名转下划线
217
142
  * @param obj - 源对象
@@ -229,134 +154,12 @@ export const keysToSnake = (obj) => {
229
154
  };
230
155
 
231
156
  /**
232
- * 数组对象字段名批量转小驼峰
233
- * @param arr - 源数组
234
- * @returns 字段名转为小驼峰格式的新数组
235
- */
236
- export const arrayKeysToCamel = (arr) => {
237
- if (!arr || !Array.isArray(arr)) return arr;
238
- return arr.map((item) => keysToCamel(item));
239
- };
240
-
241
- /**
242
- * 根据点路径读取对象字段(不存在则返回 undefined)。
243
- */
244
- export function getByPath(obj, path) {
245
- if (!path) {
246
- return obj;
247
- }
248
-
249
- const parts = path.split(".");
250
- let cur = obj;
251
-
252
- for (const part of parts) {
253
- if (isNullable(cur)) {
254
- return undefined;
255
- }
256
- if (typeof cur !== "object") {
257
- return undefined;
258
- }
259
- const record = cur;
260
- cur = record[part];
261
- }
262
-
263
- return cur;
264
- }
265
-
266
- /**
267
- * 根据点路径写入对象字段(中间层自动补空对象)。
268
- */
269
- export function setByPath(target, path, value) {
270
- const parts = path.split(".");
271
- // 避免无效 path(如 a..b)导致部分写入
272
- for (const part of parts) {
273
- if (!part) {
274
- return;
275
- }
276
- }
277
- let cur = target;
278
-
279
- for (let i = 0; i < parts.length; i++) {
280
- const key = parts[i];
281
- if (!key) {
282
- return;
283
- }
284
-
285
- const isLast = i === parts.length - 1;
286
- if (isLast) {
287
- cur[key] = value;
288
- return;
289
- }
290
-
291
- const nextVal = cur[key];
292
- if (!isPlainObject(nextVal)) {
293
- cur[key] = {};
294
- }
295
-
296
- cur = cur[key];
297
- }
298
- }
299
-
300
- /**
301
- * 生成移除指定字段后的浅拷贝。
302
- */
303
- export function omit(obj, keys) {
304
- if (!isPlainObject(obj)) {
305
- return {};
306
- }
307
-
308
- const keySet = new Set(Array.isArray(keys) ? keys : []);
309
- const out = {};
310
-
311
- for (const [k, v] of Object.entries(obj)) {
312
- if (keySet.has(k)) {
313
- continue;
314
- }
315
- out[k] = v;
316
- }
317
-
318
- return out;
319
- }
320
-
321
- /**
322
- * 从对象中挑选指定字段。
323
- */
324
- export const pickFields = (obj, keys) => {
325
- if (!obj || (!isPlainObject(obj) && !Array.isArray(obj))) {
326
- return {};
327
- }
328
-
329
- const record = obj;
330
- const result = {};
331
-
332
- for (const key of keys) {
333
- if (key in record) {
334
- result[key] = record[key];
335
- }
336
- }
337
-
338
- return result;
339
- };
340
-
341
- /**
342
- * 根据 key 生成对象映射。
157
+ * 获取运行模式(开发/正式的统一判断,仅以 NODE_ENV 为准)
158
+ * - 线上部署(bm2 等)注入 NODE_ENV=production
159
+ * - 本地开发未设置 NODE_ENV,默认 "development"
343
160
  */
344
- export function keyBy(items, getKey) {
345
- const out = {};
346
-
347
- if (!Array.isArray(items) || typeof getKey !== "function") {
348
- return out;
349
- }
350
-
351
- for (const item of items) {
352
- const key = getKey(item);
353
- if (typeof key !== "string" || key === "") {
354
- continue;
355
- }
356
- out[key] = item;
357
- }
358
-
359
- return out;
161
+ export function getRunMode() {
162
+ return Bun.env.NODE_ENV || "development";
360
163
  }
361
164
 
362
165
  /**
package/checks/hook.js DELETED
@@ -1,39 +0,0 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
- import { isFiniteNumber, isFunction, isPlainObject, isString } from "../utils/is.js";
4
-
5
- const hookKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
6
-
7
- function checkHookItem(hook) {
8
- const issues = [];
9
- if (!isPlainObject(hook)) {
10
- issues.push({ path: [], message: "必须是对象格式" });
11
- return issues;
12
- }
13
-
14
- for (const key of Object.keys(hook)) {
15
- if (!hookKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
16
- }
17
- for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
18
- if (!isString(hook[key]) || hook[key].length === 0 || hook[key].trim() !== hook[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
19
- }
20
- if (!isFunction(hook.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
21
- if (!isFiniteNumber(hook.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
22
- return issues;
23
- }
24
-
25
- export async function checkHook(hooks) {
26
- if (!Array.isArray(hooks)) {
27
- Logger.warn("钩子校验失败", { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: hooks, itemLabel: "hook" }) }, false);
28
- return true;
29
- }
30
-
31
- let hasError = false;
32
- for (const hook of hooks) {
33
- const issues = checkHookItem(hook);
34
- if (issues.length === 0) continue;
35
- Logger.warn("钩子校验失败", { errors: formatValidationIssues(issues, { item: hook, itemLabel: "hook" }) }, false);
36
- hasError = true;
37
- }
38
- return hasError;
39
- }