befly 3.76.7 → 3.77.1

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 (72) hide show
  1. package/Befly.js +97 -34
  2. package/apis/admin/_meta.js +3 -0
  3. package/apis/admin/delete.js +1 -1
  4. package/apis/admin/detail.js +1 -0
  5. package/apis/admin/insert.js +1 -1
  6. package/apis/admin/update.js +1 -1
  7. package/apis/api/_meta.js +3 -0
  8. package/apis/auth/_meta.js +3 -0
  9. package/apis/auth/login.js +3 -2
  10. package/apis/dashboard/_meta.js +3 -0
  11. package/apis/dashboard/systemResources.js +12 -1
  12. package/apis/dict/_meta.js +3 -0
  13. package/apis/dict/detail.js +1 -0
  14. package/apis/dictType/_meta.js +3 -0
  15. package/apis/dictType/detail.js +1 -0
  16. package/apis/email/_meta.js +3 -0
  17. package/apis/email/config.js +1 -1
  18. package/apis/loginLog/_meta.js +3 -0
  19. package/apis/menu/_meta.js +3 -0
  20. package/apis/operateLog/_meta.js +3 -0
  21. package/apis/role/_meta.js +3 -0
  22. package/apis/role/apiSave.js +14 -1
  23. package/apis/role/detail.js +1 -0
  24. package/apis/role/menuSave.js +1 -1
  25. package/apis/source/_meta.js +3 -0
  26. package/apis/tongJi/_meta.js +3 -0
  27. package/apis/tongJi/_tongJi.js +16 -0
  28. package/apis/tongJi/dailyReport.js +5 -1
  29. package/apis/tongJi/dailyStatsDistribution.js +5 -4
  30. package/apis/tongJi/errorReport.js +8 -2
  31. package/apis/tongJi/todayOnline.js +2 -5
  32. package/apis/upload/_meta.js +3 -0
  33. package/checks/api.js +2 -6
  34. package/checks/field.js +30 -6
  35. package/checks/menu.js +2 -1
  36. package/checks/table.js +1 -1
  37. package/configs/beflyConfig.json +7 -3
  38. package/exports.js +1 -0
  39. package/hooks/auth.js +7 -1
  40. package/hooks/permission.js +22 -1
  41. package/hooks/rateLimit.js +36 -5
  42. package/hooks/validator.js +5 -16
  43. package/index.js +46 -38
  44. package/libs/cacheHelper.js +13 -18
  45. package/libs/logger/logger.js +103 -24
  46. package/libs/logger/sanitize.js +40 -15
  47. package/libs/memCache.js +53 -0
  48. package/libs/mysql/dbHelper.js +39 -71
  49. package/libs/mysql/dbParse.js +5 -1
  50. package/libs/mysql/sql/sqlBuilder.js +9 -0
  51. package/libs/redis/redis.js +44 -57
  52. package/libs/smtpText.js +41 -22
  53. package/libs/validator/compiler.js +3 -2
  54. package/libs/validator/parser.js +11 -5
  55. package/libs/validator/util.js +1 -5
  56. package/package.json +1 -1
  57. package/paths.js +23 -10
  58. package/router/static.js +13 -6
  59. package/schemas/api.json +10 -0
  60. package/schemas/config.json +38 -0
  61. package/sync/api.js +18 -7
  62. package/sync/dev.js +27 -3
  63. package/sync/menu.js +3 -3
  64. package/sync/syncUtil.js +12 -6
  65. package/tables/api.json +21 -0
  66. package/tables/emailLog.json +6 -2
  67. package/tables/menu.json +3 -0
  68. package/utils/is.js +7 -0
  69. package/utils/prettyError.js +102 -0
  70. package/utils/scanFiles.js +85 -46
  71. package/utils/scanSources.js +56 -4
  72. package/utils/util.js +5 -1
@@ -5,7 +5,7 @@ function addIssue(issues, path, code, message) {
5
5
  }
6
6
 
7
7
  function typeMessage(kind) {
8
- if (kind === "boolean") return "必须是 boolean";
8
+ if (kind === "boolean") return "必须是布尔值";
9
9
  if (kind === "integer") return "必须是安全整数";
10
10
  if (kind === "number") return "必须是有限数字";
11
11
  if (kind === "string") return "必须是字符串";
@@ -44,11 +44,13 @@ export function parseNode(value, node, path, useDefault = true) {
44
44
  if (value === null) return failure([{ path: path, code: "null", message: "不允许为 null" }]);
45
45
 
46
46
  if (node.kind === "anyOf") {
47
+ const branchMessages = [];
47
48
  for (const branch of node.branches) {
48
49
  const result = parseNode(value, branch, path);
49
50
  if (result.ok) return result;
51
+ branchMessages.push(result.issues[0]?.message || typeMessage(branch.kind));
50
52
  }
51
- return failure([{ path: path, code: "anyOf", message: "不符合任一允许类型" }]);
53
+ return failure([{ path: path, code: "anyOf", message: `不符合任一允许类型:${branchMessages.join("")}` }]);
52
54
  }
53
55
 
54
56
  if (node.kind === "any") return success(value);
@@ -79,8 +81,9 @@ export function parseNode(value, node, path, useDefault = true) {
79
81
  }
80
82
 
81
83
  if (node.kind === "array") {
82
- if (node.minItem !== undefined && value.length < node.minItem) addIssue(issues, path, "minItem", `至少需要${node.minItem}项`);
83
- if (node.maxItem !== undefined && value.length > node.maxItem) addIssue(issues, path, "maxItem", `最多允许${node.maxItem}项`);
84
+ // 数量违规提前返回,避免后续对超大数组的遍历与拷贝
85
+ if (node.minItem !== undefined && value.length < node.minItem) return failure([{ path: path, code: "minItem", message: `至少需要${node.minItem}项` }]);
86
+ if (node.maxItem !== undefined && value.length > node.maxItem) return failure([{ path: path, code: "maxItem", message: `最多允许${node.maxItem}项` }]);
84
87
  if (node.unique && new Set(value).size !== value.length) addIssue(issues, path, "unique", "数组项不能重复");
85
88
  if (node.uniqueBy) {
86
89
  const seen = new Set();
@@ -97,7 +100,10 @@ export function parseNode(value, node, path, useDefault = true) {
97
100
  if (result.ok) data.push(result.data);
98
101
  else issues.push(...result.issues);
99
102
  });
100
- } else data.push(...value);
103
+ } else {
104
+ // 逐项拷贝而非 spread,避免超大数组触发调用栈溢出
105
+ for (const item of value) data.push(item);
106
+ }
101
107
  return issues.length > 0 ? failure(issues) : success(data);
102
108
  }
103
109
 
@@ -1,5 +1 @@
1
- export function isPlainObject(value) {
2
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3
- const prototype = Object.getPrototypeOf(value);
4
- return prototype === Object.prototype || prototype === null;
5
- }
1
+ export { isPlainObject } from "../../utils/is.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.76.7",
3
+ "version": "3.77.1",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
package/paths.js CHANGED
@@ -10,7 +10,8 @@
10
10
  *
11
11
  */
12
12
 
13
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
13
+ import { existsSync } from "node:fs";
14
+ import { dirname, isAbsolute, join, resolve } from "node:path";
14
15
  import { fileURLToPath } from "node:url";
15
16
 
16
17
  // 当前文件的路径信息
@@ -25,12 +26,6 @@ const moduleDir = dirname(moduleFilePath);
25
26
  */
26
27
  export const coreDir = moduleDir;
27
28
 
28
- /**
29
- * Core 框架 dist 目录
30
- * @description 源码态为 packages/core/dist;dist 运行态为 packages/core/dist
31
- */
32
- export const coreDistDir = basename(moduleDir) === "dist" ? moduleDir : join(moduleDir, "dist");
33
-
34
29
  /**
35
30
  * Core 框架检查目录
36
31
  * @description packages/core/checks/
@@ -75,12 +70,30 @@ export const coreTableDir = join(moduleDir, "tables");
75
70
 
76
71
  // ==================== 用户项目路径 ====================
77
72
 
73
+ /**
74
+ * 定位项目根目录:从 cwd 逐级向上查找 bm2.toml(项目根标识文件)。
75
+ * 找不到时回退 cwd——此时项目源码目录(apis/tables 等)将扫描不到,
76
+ * 由 scanSources 在启动期对"app 源码目录全部缺失"的场景统一告警。
77
+ */
78
+ function findAppRoot() {
79
+ let dir = process.cwd();
80
+ for (;;) {
81
+ if (existsSync(join(dir, "bm2.toml"))) {
82
+ return dir;
83
+ }
84
+ const parent = dirname(dir);
85
+ if (parent === dir) {
86
+ return process.cwd();
87
+ }
88
+ dir = parent;
89
+ }
90
+ }
91
+
78
92
  /**
79
93
  * 项目根目录
80
- * @description process.cwd()
81
- * @usage 用户项目的根目录
94
+ * @description 含 bm2.toml 的目录
82
95
  */
83
- export const appDir = process.cwd();
96
+ export const appDir = findAppRoot();
84
97
 
85
98
  /**
86
99
  * 项目检查目录
package/router/static.js CHANGED
@@ -62,14 +62,21 @@ export function staticHandler(corsConfig, uploadConfig) {
62
62
 
63
63
  const file = Bun.file(filePath);
64
64
  if (await file.exists()) {
65
- const headers = {};
66
- if (corsHeaders && typeof corsHeaders === "object") {
67
- for (const key of Object.keys(corsHeaders)) {
68
- headers[key] = corsHeaders[key];
69
- }
70
- }
65
+ const headers = { ...corsHeaders };
71
66
  headers["Content-Type"] = file.type || "application/octet-stream";
72
67
  headers["X-Content-Type-Options"] = "nosniff";
68
+
69
+ // ETag 协商缓存:命中 If-None-Match 直接返回 304,避免重复传输文件体
70
+ const etag = `"${file.size}-${file.lastModified}"`;
71
+ headers["ETag"] = etag;
72
+ headers["Cache-Control"] = "public, max-age=86400";
73
+ if (req.headers.get("if-none-match") === etag) {
74
+ return new Response(null, {
75
+ status: 304,
76
+ headers: headers
77
+ });
78
+ }
79
+
73
80
  if (uploadConfig.forceDownloadExtensions.split(",").includes(extname(filePath).toLowerCase())) {
74
81
  headers["Content-Disposition"] = `attachment; filename="${basename(filePath).replaceAll('"', "")}"`;
75
82
  }
package/schemas/api.json CHANGED
@@ -19,6 +19,12 @@
19
19
  "paramType": "string",
20
20
  "minValue": 1
21
21
  },
22
+ "parentTitle": {
23
+ "paramType": "string",
24
+ "minValue": 0,
25
+ "maxValue": 500,
26
+ "optional": true
27
+ },
22
28
  "apiPath": {
23
29
  "paramType": "string",
24
30
  "minValue": 1,
@@ -52,6 +58,10 @@
52
58
  "paramType": "string",
53
59
  "minValue": 1
54
60
  }
61
+ },
62
+ {
63
+ "paramType": "string",
64
+ "enum": ["dev"]
55
65
  }
56
66
  ]
57
67
  },
@@ -78,6 +78,24 @@
78
78
  "items": {
79
79
  "paramType": "string"
80
80
  }
81
+ },
82
+ "truncateStringLength": {
83
+ "paramType": "integer",
84
+ "minValue": 32,
85
+ "maxValue": 65536,
86
+ "optional": true
87
+ },
88
+ "truncateArrayLength": {
89
+ "paramType": "integer",
90
+ "minValue": 1,
91
+ "maxValue": 1000,
92
+ "optional": true
93
+ },
94
+ "truncatePreviewLength": {
95
+ "paramType": "integer",
96
+ "minValue": 32,
97
+ "maxValue": 65536,
98
+ "optional": true
81
99
  }
82
100
  }
83
101
  },
@@ -248,6 +266,26 @@
248
266
  "items": {
249
267
  "paramType": "string"
250
268
  }
269
+ },
270
+ "rules": {
271
+ "paramType": "array",
272
+ "items": {
273
+ "paramType": "object",
274
+ "fields": {
275
+ "path": {
276
+ "paramType": "string",
277
+ "minValue": 1
278
+ },
279
+ "limit": {
280
+ "paramType": "integer",
281
+ "minValue": 1
282
+ },
283
+ "window": {
284
+ "paramType": "integer",
285
+ "minValue": 1
286
+ }
287
+ }
288
+ }
251
289
  }
252
290
  }
253
291
  }
package/sync/api.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { BEFLY_API_TABLE } from "../configs/constConfig.js";
2
2
  import { buildPathSyncLists, serializeAuth } from "./syncUtil.js";
3
3
 
4
+ // insBatch 单批上限 1000 行,超出分批写入
5
+ async function insBatchChunked(mysql, table, list) {
6
+ for (let start = 0; start < list.length; start += 1000) {
7
+ await mysql.insBatch(table, list.slice(start, start + 1000));
8
+ }
9
+ }
10
+
4
11
  const getApiParentPath = (apiPath) => {
5
12
  const segments = apiPath
6
13
  .split("/")
@@ -19,7 +26,7 @@ const getApiParentPath = (apiPath) => {
19
26
  export async function syncApi(ctx, apis) {
20
27
  const allDbApis = await ctx.mysql.getAll({
21
28
  table: BEFLY_API_TABLE,
22
- fields: ["id", "path", "parentPath", "name", "auth", "state"],
29
+ fields: ["id", "path", "parentPath", "parentTitle", "name", "method", "auth", "category", "state"],
23
30
  where: { state$gte: 0 }
24
31
  });
25
32
 
@@ -30,7 +37,9 @@ export async function syncApi(ctx, apis) {
30
37
  path: api.apiPath,
31
38
  method: api.method,
32
39
  parentPath: getApiParentPath(api.apiPath),
33
- auth: serializeAuth(api.auth)
40
+ parentTitle: api.parentTitle || "",
41
+ auth: serializeAuth(api.auth),
42
+ category: api.category || "other"
34
43
  });
35
44
  }
36
45
 
@@ -42,14 +51,18 @@ export async function syncApi(ctx, apis) {
42
51
  path: def.path,
43
52
  method: def.method,
44
53
  parentPath: def.parentPath,
45
- auth: def.auth
54
+ parentTitle: def.parentTitle,
55
+ auth: def.auth,
56
+ category: def.category
46
57
  }),
47
58
  (def) => ({
48
59
  name: def.name,
49
60
  path: def.path,
50
61
  method: def.method,
51
62
  parentPath: def.parentPath,
52
- auth: def.auth
63
+ parentTitle: def.parentTitle,
64
+ auth: def.auth,
65
+ category: def.category
53
66
  })
54
67
  );
55
68
 
@@ -57,9 +70,7 @@ export async function syncApi(ctx, apis) {
57
70
  await ctx.mysql.updBatch(BEFLY_API_TABLE, syncLists.updList);
58
71
  }
59
72
 
60
- if (syncLists.insList.length > 0) {
61
- await ctx.mysql.insBatch(BEFLY_API_TABLE, syncLists.insList);
62
- }
73
+ await insBatchChunked(ctx.mysql, BEFLY_API_TABLE, syncLists.insList);
63
74
 
64
75
  if (syncLists.delIds.length > 0) {
65
76
  await ctx.mysql.delForceBatch(BEFLY_API_TABLE, syncLists.delIds);
package/sync/dev.js CHANGED
@@ -38,6 +38,12 @@ export async function syncDev(ctx) {
38
38
  where: { username: "dev", state$gte: 0 }
39
39
  });
40
40
 
41
+ const adminAccount = await ctx.mysql.getOne({
42
+ table: BEFLY_ADMIN_TABLE,
43
+ fields: ["id"],
44
+ where: { username: "admin", state$gte: 0 }
45
+ });
46
+
41
47
  const menuPaths = [];
42
48
  for (const item of allMenus.data.lists) {
43
49
  menuPaths.push(item.path);
@@ -72,7 +78,6 @@ export async function syncDev(ctx) {
72
78
 
73
79
  // devPassword 为必填项(校验见函数开头),此处直接使用
74
80
  const password = await hashPassword(ctx.config.devPassword);
75
- // 【临时诊断】hashPassword 后连接预检
76
81
 
77
82
  const devAdminData = {
78
83
  nickname: "开发者",
@@ -92,7 +97,24 @@ export async function syncDev(ctx) {
92
97
  } else {
93
98
  await ctx.mysql.insData({
94
99
  table: BEFLY_ADMIN_TABLE,
95
- data: { ...devAdminData, state: 1 }
100
+ data: devAdminData
101
+ });
102
+ }
103
+
104
+ // admin 管理员账号:仅首次创建,已存在则不覆盖(密码与资料以库内为准,改密后重启不受影响)
105
+ // 初始密码为固定默认值 admin123456,首次登录后应立即修改
106
+ if (!isNumber(adminAccount.data.id)) {
107
+ await ctx.mysql.insData({
108
+ table: BEFLY_ADMIN_TABLE,
109
+ data: {
110
+ nickname: "管理员",
111
+ email: "admin@qq.com",
112
+ username: "admin",
113
+ password: await hashPassword("admin123456"),
114
+ roleCode: "admin",
115
+ roleType: "admin",
116
+ state: 1
117
+ }
96
118
  });
97
119
  }
98
120
 
@@ -141,6 +163,8 @@ export async function syncDev(ctx) {
141
163
 
142
164
  const mergedApis = [];
143
165
  const mergedApiSet = new Set();
166
+ // 仅保留当前仍存在的接口路径,已删除/改名的旧 path 不再永久残留
167
+ const validApiSet = new Set(allApis.data.lists.map((api) => api.path));
144
168
 
145
169
  if (Array.isArray(existingRole.data?.apis)) {
146
170
  for (const apiPath of existingRole.data.apis) {
@@ -148,7 +172,7 @@ export async function syncDev(ctx) {
148
172
  continue;
149
173
  }
150
174
  const value = apiPath.trim();
151
- if (value.length === 0 || mergedApiSet.has(value)) {
175
+ if (value.length === 0 || !validApiSet.has(value) || mergedApiSet.has(value)) {
152
176
  continue;
153
177
  }
154
178
  mergedApiSet.add(value);
package/sync/menu.js CHANGED
@@ -40,7 +40,7 @@ export function flattenMenusToDefMap(menus) {
40
40
  export async function syncMenu(ctx, menus) {
41
41
  const menuDefMap = flattenMenusToDefMap(menus);
42
42
 
43
- // 2) 批量同步(事务内):按 path diff 执行批量 insert/update/delete
43
+ // 2) 批量同步:按 path diff 执行批量 insert/update/delete(幂等收敛,非原子事务)
44
44
  // 读取全部菜单
45
45
  const allExistingMenus = await ctx.mysql.getAll({
46
46
  table: BEFLY_MENU_TABLE,
@@ -69,8 +69,8 @@ export async function syncMenu(ctx, menus) {
69
69
  await ctx.mysql.updBatch(BEFLY_MENU_TABLE, syncLists.updList);
70
70
  }
71
71
 
72
- if (syncLists.insList.length > 0) {
73
- await ctx.mysql.insBatch(BEFLY_MENU_TABLE, syncLists.insList);
72
+ for (let start = 0; start < syncLists.insList.length; start += 1000) {
73
+ await ctx.mysql.insBatch(BEFLY_MENU_TABLE, syncLists.insList.slice(start, start + 1000));
74
74
  }
75
75
 
76
76
  // 3) 删除差集(DB - 配置)
package/sync/syncUtil.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // 接口 auth 的存储协议(serializeAuth 与 authAllowsRole 配套使用):
2
2
  // - true -> 1(需登录)
3
3
  // - false -> 0(免登录)
4
+ // - "dev" -> "dev"(超级管理员专属:管理员表中 roleCode 为 dev/admin 的账号,不参与角色授权)
4
5
  // - string[] -> "a,b,c"(角色类型白名单,如 admin/user)
5
6
  export function serializeAuth(auth) {
6
- return auth === false ? 0 : Array.isArray(auth) ? auth.join(",") : 1;
7
+ return auth === false ? 0 : auth === "dev" ? "dev" : Array.isArray(auth) ? auth.join(",") : 1;
7
8
  }
8
9
 
9
10
  /**
@@ -17,6 +18,10 @@ export function authAllowsRole(auth, roleCode) {
17
18
  return false;
18
19
  }
19
20
 
21
+ function isSameRecord(data, existing) {
22
+ return Object.entries(data).every(([key, value]) => existing[key] === value);
23
+ }
24
+
20
25
  export function buildPathSyncLists(existingList, defList, buildUpdateData, buildInsertData) {
21
26
  const existingMap = new Map();
22
27
  const delIdSet = new Set();
@@ -24,8 +29,9 @@ export function buildPathSyncLists(existingList, defList, buildUpdateData, build
24
29
  for (const record of existingList) {
25
30
  if (!existingMap.has(record.path)) {
26
31
  existingMap.set(record.path, record);
27
- delIdSet.add(record.id);
28
32
  }
33
+ // 全部行先进删除集,命中配置的再移出;同 path 重复行(历史并发同步产物)因此按孤儿清理
34
+ delIdSet.add(record.id);
29
35
  }
30
36
 
31
37
  const updList = [];
@@ -36,10 +42,10 @@ export function buildPathSyncLists(existingList, defList, buildUpdateData, build
36
42
 
37
43
  if (existing) {
38
44
  delIdSet.delete(existing.id);
39
- updList.push({
40
- id: existing.id,
41
- data: buildUpdateData(def, existing)
42
- });
45
+ const data = buildUpdateData(def, existing);
46
+ if (!isSameRecord(data, existing)) {
47
+ updList.push({ id: existing.id, data: data });
48
+ }
43
49
  continue;
44
50
  }
45
51
 
package/tables/api.json CHANGED
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "$tableName": "接口表",
3
+ "$tableIndexes": {
4
+ "uk_befly_api_path": "unique#path"
5
+ },
3
6
  "name": {
4
7
  "name": "接口名称",
5
8
  "minValue": 2,
@@ -17,6 +20,15 @@
17
20
  "fieldType": "varchar",
18
21
  "fieldDefault": ""
19
22
  },
23
+ "category": {
24
+ "name": "接口分类",
25
+ "detail": "select=查询类,other=其他",
26
+ "minValue": 1,
27
+ "maxValue": 50,
28
+ "paramType": "string",
29
+ "fieldType": "varchar",
30
+ "fieldDefault": "other"
31
+ },
20
32
  "method": {
21
33
  "name": "请求方式",
22
34
  "minValue": 1,
@@ -40,5 +52,14 @@
40
52
  "paramType": "string",
41
53
  "fieldType": "varchar",
42
54
  "fieldDefault": ""
55
+ },
56
+ "parentTitle": {
57
+ "name": "目录标题",
58
+ "detail": "目录标题路径:每级有 _meta.js 标题用标题,没有用目录名",
59
+ "minValue": 0,
60
+ "maxValue": 100,
61
+ "paramType": "string",
62
+ "fieldType": "varchar",
63
+ "fieldDefault": ""
43
64
  }
44
65
  }
@@ -48,14 +48,18 @@
48
48
  "maxValue": 500,
49
49
  "paramType": "string",
50
50
  "fieldType": "varchar",
51
- "fieldDefault": ""
51
+ "fieldDefault": "",
52
+ "pattern": "^(|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}([;,][a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})*)$",
53
+ "minValue": 0
52
54
  },
53
55
  "bccEmail": {
54
56
  "name": "密送邮箱",
55
57
  "maxValue": 500,
56
58
  "paramType": "string",
57
59
  "fieldType": "varchar",
58
- "fieldDefault": ""
60
+ "fieldDefault": "",
61
+ "pattern": "^(|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}([;,][a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})*)$",
62
+ "minValue": 0
59
63
  },
60
64
  "sendTime": {
61
65
  "name": "发送时间",
package/tables/menu.json CHANGED
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "$tableName": "菜单表",
3
+ "$tableIndexes": {
4
+ "uk_befly_menu_path": "unique#path"
5
+ },
3
6
  "name": {
4
7
  "name": "菜单名称",
5
8
  "minValue": 2,
package/utils/is.js CHANGED
@@ -28,3 +28,10 @@ export function isPrimaryProcess() {
28
28
  const instance = Bun.env.BM2_APP_INSTANCE;
29
29
  return instance === "0" || instance === 0 || instance === undefined;
30
30
  }
31
+
32
+ // 超级管理员判定(auth: "dev" 接口的准入口径):
33
+ // 必须是管理员表账号(roleType=admin)且角色代号为 dev 或 admin;
34
+ // 用户表账号 roleType 固定为 user,即使 roleCode=admin 也不具备超管接口权限
35
+ export function isSuperAdminContext(ctx) {
36
+ return ctx?.roleType === "admin" && (ctx?.roleCode === "dev" || ctx?.roleCode === "admin");
37
+ }
@@ -0,0 +1,102 @@
1
+ import { relative } from "node:path";
2
+
3
+ // 摘要中每组错误最多列出的文件数,其余以计数收尾(完整清单在日志里)
4
+ const MAX_FILES_PER_GROUP = 3;
5
+
6
+ // 高频错误的修复提示(按内容模式匹配,只保留最有价值的少数几条)
7
+ const HINT_RULES = [
8
+ {
9
+ match: (text) => text.includes("不允许出现"),
10
+ hint: "疑似新增字段未在对应 schema 中声明"
11
+ },
12
+ {
13
+ match: (text) => text.includes("必须是") || text.includes("必填"),
14
+ hint: "检查对应字段的类型与取值是否符合 schema 声明"
15
+ }
16
+ ];
17
+
18
+ function shortPath(file) {
19
+ const text = String(file || "");
20
+ if (!text) return "(未知来源)";
21
+ const related = relative(process.cwd(), text);
22
+ return (related.length < text.length ? related : text).replaceAll("\\", "/");
23
+ }
24
+
25
+ // 兼容字符串化的错误项(经 sanitize 深度降级后会变成 JSON 字符串)
26
+ function toErrorItems(errors) {
27
+ if (!Array.isArray(errors)) return [];
28
+ return errors.map((item) => {
29
+ if (item && typeof item === "object") return item;
30
+ if (typeof item === "string" && item.startsWith("{")) {
31
+ try {
32
+ return JSON.parse(item);
33
+ } catch {
34
+ return { expected: item };
35
+ }
36
+ }
37
+ return { expected: String(item) };
38
+ });
39
+ }
40
+
41
+ function collectHints(texts) {
42
+ const hints = [];
43
+ for (const rule of HINT_RULES) {
44
+ if (hints.length >= 2) break;
45
+ if (texts.some((text) => rule.match(text)) && !hints.includes(rule.hint)) {
46
+ hints.push(rule.hint);
47
+ }
48
+ }
49
+ return hints;
50
+ }
51
+
52
+ /**
53
+ * 将结构化错误(含 checkAll 的 errors 分组)格式化为人类可读的多行摘要。
54
+ * 用于启动失败等关键错误的终端输出;日志仍记录完整结构化信息。
55
+ */
56
+ export function formatErrorSummary(error, options = {}) {
57
+ const title = options.title || "启动失败";
58
+ const lines = [`✗ ${title}:${error?.message || error}`];
59
+
60
+ const groups = Array.isArray(error?.errors) ? error.errors : [];
61
+ const allTexts = [];
62
+ for (const group of groups) {
63
+ const items = toErrorItems(group?.errors ?? group);
64
+ if (items.length === 0) continue;
65
+
66
+ // 按原因聚合:同因错误合并,文件清单截断展示
67
+ const byReason = new Map();
68
+ for (const item of items) {
69
+ const reason = item.expected || item.message || "(无原因)";
70
+ if (!byReason.has(reason)) byReason.set(reason, []);
71
+ byReason.get(reason).push(shortPath(item.file));
72
+ }
73
+ allTexts.push(...byReason.keys());
74
+
75
+ lines.push("");
76
+ lines.push(`[${group?.check || "错误"}] 共 ${items.length} 处:`);
77
+ for (const [reason, files] of byReason) {
78
+ lines.push(` ${reason}`);
79
+ const shown = files.slice(0, MAX_FILES_PER_GROUP);
80
+ for (const file of shown) {
81
+ lines.push(` ${file}`);
82
+ }
83
+ if (files.length > shown.length) {
84
+ lines.push(` … 其余 ${files.length - shown.length} 个`);
85
+ }
86
+ }
87
+ }
88
+
89
+ const hints = collectHints(allTexts);
90
+ if (hints.length > 0) {
91
+ lines.push("");
92
+ for (const hint of hints) {
93
+ lines.push(`提示:${hint}`);
94
+ }
95
+ }
96
+
97
+ return lines.join("\n");
98
+ }
99
+
100
+ export function printErrorSummary(error, options = {}) {
101
+ Bun.stderr.write(`${formatErrorSummary(error, options)}\n`);
102
+ }