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
@@ -12,6 +12,42 @@ const selectFields = {
12
12
  state: { name: "状态", paramType: "integer" }
13
13
  };
14
14
 
15
+ /**
16
+ * 扫描接口目录下的 _meta.js 目录元数据(约定:_ 前缀文件不注册路由)。
17
+ * 返回 Map<目录 apiPath, 中文标题>,键与接口 apiPath 的目录部分对齐,供 scanSources 挂载 parentTitle。
18
+ */
19
+ export async function scanApiMetaTitles(dir, source) {
20
+ const titles = new Map();
21
+ if (!existsSync(dir)) return titles;
22
+
23
+ const glob = new Bun.Glob("**/_meta.js");
24
+ const files = await Array.fromAsync(
25
+ glob.scan({
26
+ cwd: dir,
27
+ onlyFiles: true,
28
+ absolute: true,
29
+ followSymlinks: true
30
+ })
31
+ );
32
+
33
+ for (const file of files) {
34
+ const filePath = normalize(file);
35
+ const meta = await importDefault(filePath, {});
36
+ const record = isPlainObject(meta) ? meta : {};
37
+ const title = record.title;
38
+ if (typeof title !== "string" || title.trim().length === 0 || title.trim().length > 50) {
39
+ throw createError(`目录元数据无效:${filePath} 的 title 必须是 1-50 个字符`, { code: "policy", subsystem: "scan", operation: "scanApiMetaTitles" });
40
+ }
41
+
42
+ const parsedRelativePath = parse(relative(dir, filePath));
43
+ const relativeDir = parsedRelativePath.dir.replaceAll("\\", "/");
44
+ const dirApiPath = source === "core" ? `/api/core/${relativeDir}` : `/api/${relativeDir}`;
45
+ titles.set(dirApiPath, title.trim());
46
+ }
47
+
48
+ return titles;
49
+ }
50
+
15
51
  /**
16
52
  * 扫描指定目录下的文件
17
53
  * @param dir 目录路径
@@ -24,58 +60,61 @@ export async function scanFiles(dir, source, type, pattern) {
24
60
  const glob = new Bun.Glob(pattern);
25
61
  if (!existsSync(dir)) return [];
26
62
 
27
- const results = [];
63
+ const files = await Array.fromAsync(
64
+ glob.scan({
65
+ cwd: dir,
66
+ onlyFiles: true,
67
+ absolute: true,
68
+ followSymlinks: true
69
+ })
70
+ );
28
71
 
29
- const files = await glob.scan({
30
- cwd: dir,
31
- onlyFiles: true,
32
- absolute: true,
33
- followSymlinks: true
34
- });
72
+ // 并行导入源文件,Promise.all 保持与扫描顺序一致的结果顺序
73
+ const results = await Promise.all(
74
+ files.map(async (file) => {
75
+ const filePath = normalize(file);
76
+ const parsedFile = parse(filePath);
35
77
 
36
- for await (const file of files) {
37
- const filePath = normalize(file);
38
- const parsedFile = parse(filePath);
78
+ // 获取文件名(去除扩展名)
79
+ const fileName = parsedFile.name;
39
80
 
40
- // 获取文件名(去除扩展名)
41
- const fileName = parsedFile.name;
81
+ // 计算相对路径(去除扩展名)
82
+ const parsedRelativePath = parse(relative(dir, filePath));
83
+ const relativePath = (parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name).replaceAll("\\", "/");
42
84
 
43
- // 计算相对路径(去除扩展名)
44
- const parsedRelativePath = parse(relative(dir, filePath));
45
- const relativePath = (parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name).replaceAll("\\", "/");
85
+ // 固定默认过滤(不可关闭):忽略下划线开头的文件/目录
86
+ if (relativePath.split("/").some((part) => part.startsWith("_"))) return null;
87
+ const content = await importDefault(filePath, {});
88
+ const contentObj = isPlainObject(content) ? content : {};
46
89
 
47
- // 固定默认过滤(不可关闭):忽略下划线开头的文件/目录
48
- if (relativePath.split("/").some((part) => part.startsWith("_"))) continue;
49
- const content = await importDefault(filePath, {});
50
- const contentObj = isPlainObject(content) ? content : {};
51
-
52
- const base = {
53
- source: source,
54
- type: type,
55
- filePath: filePath,
56
- relativePath: relativePath,
57
- fileName: fileName,
58
- apiPath: source === "core" ? `/api/${source}/${relativePath}` : `/api/${relativePath}`
59
- };
60
- if (type === "table") {
61
- base["fieldsDef"] = contentObj;
62
- }
63
- if (type === "api") {
64
- base["name"] = "";
65
- }
66
- if (type !== "table") {
67
- for (const [key, value] of Object.entries(contentObj)) {
68
- if (base[key]) continue;
69
- base[key] = value;
90
+ const base = {
91
+ source: source,
92
+ type: type,
93
+ filePath: filePath,
94
+ relativePath: relativePath,
95
+ fileName: fileName,
96
+ apiPath: source === "core" ? `/api/${source}/${relativePath}` : `/api/${relativePath}`
97
+ };
98
+ if (type === "table") {
99
+ base["fieldsDef"] = contentObj;
70
100
  }
71
- }
72
- if (type === "api") {
73
- base.category ||= "other";
74
- if (base.category === "select") base.fields = { ...selectFields, ...base.fields };
75
- }
76
- results.push(base);
77
- }
78
- return results;
101
+ if (type === "api") {
102
+ base["name"] = "";
103
+ }
104
+ if (type !== "table") {
105
+ for (const [key, value] of Object.entries(contentObj)) {
106
+ if (base[key]) continue;
107
+ base[key] = value;
108
+ }
109
+ }
110
+ if (type === "api") {
111
+ base.category ||= "other";
112
+ if (base.category === "select") base.fields = { ...selectFields, ...base.fields };
113
+ }
114
+ return base;
115
+ })
116
+ );
117
+ return results.filter((item) => item !== null);
79
118
  } catch (error) {
80
119
  throw createError(`扫描失败: source=${source} type=${type} dir=${dir} pattern=${pattern}`, { cause: error, code: "runtime" });
81
120
  }
@@ -1,3 +1,6 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ import { Logger } from "../libs/logger/index.js";
1
4
  import { buildTableModel } from "../libs/tableModel.js";
2
5
  import {
3
6
  //
@@ -13,7 +16,42 @@ import {
13
16
  appCornDir
14
17
  } from "../paths.js";
15
18
  import { createError } from "./error.js";
16
- import { scanFiles } from "./scanFiles.js";
19
+ import { scanApiMetaTitles, scanFiles } from "./scanFiles.js";
20
+
21
+ // 逐级拼接目录显示标题:每级目录有 _meta.js 标题用标题,没有用目录名,以 / 连接
22
+ // 例:/api/admin/product/insert + 标题映射 -> "后台管理/product"
23
+ export function composeTitlePath(apiPath, metaTitles, missingCollector) {
24
+ const segments = apiPath.split("/").filter(Boolean);
25
+ const start = segments[1] === "core" ? 2 : 1;
26
+ const dirs = segments.slice(start, -1);
27
+ if (dirs.length === 0) return "";
28
+
29
+ const parts = [];
30
+ let current = `/${segments.slice(0, start).join("/")}`;
31
+ for (const dir of dirs) {
32
+ current = `${current}/${dir}`;
33
+ const title = metaTitles.get(current);
34
+ if (title) {
35
+ parts.push(title);
36
+ } else {
37
+ parts.push(dir);
38
+ if (missingCollector) missingCollector.add(current);
39
+ }
40
+ }
41
+ return parts.join("/");
42
+ }
43
+
44
+ function warnWhenAppSourcesMissing() {
45
+ // 全部 app 源码目录都不存在时显式告警,避免"项目源码静默消失、以纯 core 形态运行"的配置事故
46
+ const appDirs = [appApiDir, appTableDir, appPluginDir, appHookDir, appCornDir];
47
+ if (appDirs.every((dir) => !existsSync(dir))) {
48
+ Logger.warn("项目源码目录(apis/tables/plugins/hooks/corns)均不存在,请检查启动目录是否为项目根目录", {
49
+ subsystem: "scan",
50
+ operation: "scanSources",
51
+ appDir: appDirs[0]
52
+ });
53
+ }
54
+ }
17
55
 
18
56
  export function assertUnique(items, key, label) {
19
57
  const values = new Map();
@@ -72,6 +110,8 @@ export const scanSources = async ({ beflyMode = "auto" } = {}) => {
72
110
  const tables = await scanTables({ beflyMode: beflyMode });
73
111
  const corns = [];
74
112
 
113
+ warnWhenAppSourcesMissing();
114
+
75
115
  // 处理插件
76
116
  const allCorePlugins = await scanFiles(corePluginDir, "core", "plugin", "*.js");
77
117
  for (const item of allCorePlugins) {
@@ -102,17 +142,29 @@ export const scanSources = async ({ beflyMode = "auto" } = {}) => {
102
142
  corns.push(item);
103
143
  }
104
144
 
105
- // 处理接口
106
- const allCoreApis = await scanFiles(coreApiDir, "core", "api", "**/*.js");
145
+ // 处理接口(读取目录 _meta.js 标题挂载 parentTitle);
146
+ // 每一级接口目录都必须有中文标题,缺失即启动失败并列出清单
147
+ const [allCoreApis, coreMetaTitles, allAppApis, appMetaTitles] = await Promise.all([scanFiles(coreApiDir, "core", "api", "**/*.js"), scanApiMetaTitles(coreApiDir, "core"), scanFiles(appApiDir, "app", "api", "**/*.js"), scanApiMetaTitles(appApiDir, "app")]);
148
+
149
+ const missingTitleDirs = new Set();
107
150
  for (const item of allCoreApis) {
151
+ item.parentTitle = composeTitlePath(item.apiPath, coreMetaTitles, missingTitleDirs);
108
152
  apis.push(item);
109
153
  }
110
- const allAppApis = await scanFiles(appApiDir, "app", "api", "**/*.js");
111
154
  for (const item of allAppApis) {
112
155
  assertAppApiNamespace(item);
156
+ item.parentTitle = composeTitlePath(item.apiPath, appMetaTitles, missingTitleDirs);
113
157
  apis.push(item);
114
158
  }
115
159
 
160
+ if (missingTitleDirs.size > 0) {
161
+ throw createError(`以下接口目录缺少 _meta.js 中文标题:${Array.from(missingTitleDirs).toSorted().join("、")}`, {
162
+ code: "policy",
163
+ subsystem: "scan",
164
+ operation: "scanSources"
165
+ });
166
+ }
167
+
116
168
  assertUnique(plugins, "fileName", "Plugin 文件名");
117
169
  assertUnique(hooks, "fileName", "Hook 文件名");
118
170
  assertUnique(corns, "fileName", "Cron 文件名");
package/utils/util.js CHANGED
@@ -37,7 +37,11 @@ export function getRunMode() {
37
37
  }
38
38
 
39
39
  export function toSessionTtlSeconds(ttlDays) {
40
- return ttlDays * 24 * 60 * 60;
40
+ const parsed = typeof ttlDays === "number" ? ttlDays : Number(String(ttlDays ?? "").trim());
41
+ if (Number.isFinite(parsed) && parsed > 0) {
42
+ return Math.floor(parsed * 24 * 60 * 60);
43
+ }
44
+ return 7 * 24 * 60 * 60;
41
45
  }
42
46
 
43
47
  export function genShortId() {