befly 3.12.3 → 3.13.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 (67) hide show
  1. package/dist/befly.config.js +1 -1
  2. package/dist/befly.js +810 -896
  3. package/dist/befly.min.js +15 -18
  4. package/dist/checks/checkApi.js +14 -17
  5. package/dist/checks/checkHook.js +55 -24
  6. package/dist/checks/checkMenu.js +10 -10
  7. package/dist/checks/checkPlugin.js +55 -24
  8. package/dist/checks/checkTable.js +29 -28
  9. package/dist/hooks/auth.d.ts +3 -7
  10. package/dist/hooks/auth.js +2 -1
  11. package/dist/hooks/cors.d.ts +3 -7
  12. package/dist/hooks/cors.js +3 -2
  13. package/dist/hooks/parser.d.ts +3 -7
  14. package/dist/hooks/parser.js +2 -1
  15. package/dist/hooks/permission.d.ts +3 -7
  16. package/dist/hooks/permission.js +5 -3
  17. package/dist/hooks/validator.d.ts +3 -7
  18. package/dist/hooks/validator.js +2 -1
  19. package/dist/index.js +2 -2
  20. package/dist/lib/cacheHelper.js +8 -8
  21. package/dist/lib/connect.js +5 -5
  22. package/dist/lib/dbHelper.js +6 -5
  23. package/dist/lib/logger.d.ts +16 -17
  24. package/dist/lib/logger.js +335 -749
  25. package/dist/lib/redisHelper.js +27 -26
  26. package/dist/loader/loadApis.js +1 -1
  27. package/dist/loader/loadPlugins.js +1 -1
  28. package/dist/plugins/cache.d.ts +3 -9
  29. package/dist/plugins/cache.js +2 -1
  30. package/dist/plugins/cipher.d.ts +3 -8
  31. package/dist/plugins/cipher.js +2 -1
  32. package/dist/plugins/config.d.ts +3 -12
  33. package/dist/plugins/config.js +2 -1
  34. package/dist/plugins/db.d.ts +3 -9
  35. package/dist/plugins/db.js +3 -2
  36. package/dist/plugins/jwt.d.ts +3 -9
  37. package/dist/plugins/jwt.js +2 -1
  38. package/dist/plugins/logger.d.ts +3 -25
  39. package/dist/plugins/logger.js +2 -1
  40. package/dist/plugins/redis.d.ts +3 -9
  41. package/dist/plugins/redis.js +3 -2
  42. package/dist/plugins/tool.d.ts +3 -11
  43. package/dist/plugins/tool.js +2 -1
  44. package/dist/router/api.js +3 -2
  45. package/dist/router/static.js +1 -1
  46. package/dist/sync/syncApi.js +3 -3
  47. package/dist/sync/syncMenu.js +3 -2
  48. package/dist/sync/syncTable.js +2 -2
  49. package/dist/types/hook.d.ts +13 -0
  50. package/dist/types/hook.js +13 -0
  51. package/dist/types/logger.d.ts +20 -6
  52. package/dist/types/plugin.d.ts +12 -1
  53. package/dist/types/plugin.js +12 -1
  54. package/dist/utils/formatYmdHms.d.ts +1 -0
  55. package/dist/utils/formatYmdHms.js +20 -0
  56. package/dist/utils/importDefault.js +1 -1
  57. package/dist/utils/loadMenuConfigs.js +7 -6
  58. package/dist/utils/loggerUtils.d.ts +18 -0
  59. package/dist/utils/loggerUtils.js +167 -0
  60. package/dist/utils/response.js +6 -4
  61. package/dist/utils/scanCoreBuiltins.js +4 -1
  62. package/dist/utils/scanFiles.d.ts +2 -0
  63. package/dist/utils/scanFiles.js +5 -2
  64. package/dist/utils/sortModules.js +8 -7
  65. package/dist/utils/util.d.ts +2 -0
  66. package/dist/utils/util.js +16 -0
  67. package/package.json +2 -2
@@ -5,70 +5,67 @@ export async function checkApi(apis) {
5
5
  for (const api of apis) {
6
6
  try {
7
7
  if (typeof api?.name !== "string" || api.name.trim() === "") {
8
- Logger.warn(omit(api, ["handler"]), "接口的 name 属性必须是非空字符串");
8
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 name 属性必须是非空字符串" }));
9
9
  hasError = true;
10
10
  continue;
11
11
  }
12
12
  if (typeof api?.handler !== "function") {
13
- Logger.warn(omit(api, ["handler"]), "接口的 handler 属性必须是函数");
13
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 handler 属性必须是函数" }));
14
14
  hasError = true;
15
15
  continue;
16
16
  }
17
17
  // routePath / routePrefix 由 scanFiles 系统生成:必须是严格的 pathname
18
18
  if (typeof api?.routePath !== "string" || api.routePath.trim() === "") {
19
- Logger.warn(omit(api, ["handler"]), "接口的 routePath 属性必须是非空字符串(由系统生成)");
19
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 属性必须是非空字符串(由系统生成)" }));
20
20
  hasError = true;
21
21
  }
22
22
  else {
23
23
  const routePath = api.routePath.trim();
24
24
  // 不允许出现 "POST/api/..." 等 method 前缀
25
25
  if (/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i.test(routePath)) {
26
- Logger.warn(omit(api, ["handler"]), "接口的 routePath 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)");
26
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)" }));
27
27
  hasError = true;
28
28
  }
29
29
  if (!routePath.startsWith("/api/")) {
30
- Logger.warn(omit(api, ["handler"]), "接口的 routePath 必须以 /api/ 开头");
30
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 必须以 /api/ 开头" }));
31
31
  hasError = true;
32
32
  }
33
33
  if (routePath.includes(" ")) {
34
- Logger.warn(omit(api, ["handler"]), "接口的 routePath 不允许包含空格");
34
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许包含空格" }));
35
35
  hasError = true;
36
36
  }
37
37
  if (routePath.includes("/api//")) {
38
- Logger.warn(omit(api, ["handler"]), "接口的 routePath 不允许出现 /api//(重复斜杠)");
38
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许出现 /api//(重复斜杠)" }));
39
39
  hasError = true;
40
40
  }
41
41
  }
42
42
  if (typeof api?.routePrefix !== "string" || api.routePrefix.trim() === "") {
43
- Logger.warn(omit(api, ["handler"]), "接口的 routePrefix 属性必须是非空字符串(由系统生成)");
43
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePrefix 属性必须是非空字符串(由系统生成)" }));
44
44
  hasError = true;
45
45
  }
46
46
  if (api.method && !["GET", "POST", "GET,POST", "POST,GET"].includes(String(api.method).toUpperCase())) {
47
- Logger.warn(omit(api, ["handler"]), "接口的 method 属性必须是有效的 HTTP 方法 (GET, POST, GET,POST, POST,GET)");
47
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 method 属性必须是有效的 HTTP 方法 (GET, POST, GET,POST, POST,GET)" }));
48
48
  hasError = true;
49
49
  }
50
50
  if (api.auth !== undefined && typeof api.auth !== "boolean") {
51
- Logger.warn(omit(api, ["handler"]), "接口的 auth 属性必须是布尔值 (true=需登录, false=公开)");
51
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 auth 属性必须是布尔值 (true=需登录, false=公开)" }));
52
52
  hasError = true;
53
53
  }
54
54
  if (api.fields && !isPlainObject(api.fields)) {
55
- Logger.warn(omit(api, ["handler"]), "接口的 fields 属性必须是对象");
55
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 fields 属性必须是对象" }));
56
56
  hasError = true;
57
57
  }
58
58
  if (api.required && !Array.isArray(api.required)) {
59
- Logger.warn(omit(api, ["handler"]), "接口的 required 属性必须是数组");
59
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 required 属性必须是数组" }));
60
60
  hasError = true;
61
61
  }
62
62
  if (api.required && api.required.some((reqItem) => typeof reqItem !== "string")) {
63
- Logger.warn(omit(api, ["handler"]), "接口的 required 属性必须是字符串数组");
63
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 required 属性必须是字符串数组" }));
64
64
  hasError = true;
65
65
  }
66
66
  }
67
67
  catch (error) {
68
- Logger.error({
69
- err: error,
70
- item: api
71
- }, "接口解析失败");
68
+ Logger.error({ err: error, item: api, msg: "接口解析失败" });
72
69
  hasError = true;
73
70
  }
74
71
  }
@@ -1,82 +1,113 @@
1
1
  import { Logger } from "../lib/logger";
2
2
  import { isPlainObject, omit } from "../utils/util";
3
+ const exportKeys = ["name", "enable", "deps", "handler"];
3
4
  export async function checkHook(hooks) {
4
5
  let hasError = false;
6
+ // 说明:hooks 实际是 scanFiles/scanSources 的结果对象(包含元信息字段)。
7
+ // 这里不再对白名单枚举 metaKeys(因为它们是系统生成的),只校验“用户 default export 导出的字段”。
5
8
  const coreBuiltinNameRegexp = /^[a-z]+(?:_[a-z]+)*$/;
6
9
  for (const hook of hooks) {
7
10
  try {
8
11
  if (!isPlainObject(hook)) {
9
- Logger.warn(omit(hook, ["handler"]), "钩子导出必须是对象(export default { deps, handler })");
12
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子导出必须是对象(export default { deps, handler })" }));
10
13
  hasError = true;
11
14
  continue;
12
15
  }
13
16
  // moduleName 必须存在(用于依赖排序与运行时挂载)。
14
17
  if (typeof hook.moduleName !== "string" || hook.moduleName.trim() === "") {
15
- Logger.warn(omit(hook, ["handler"]), "钩子的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)");
18
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)" }));
16
19
  hasError = true;
17
20
  continue;
18
21
  }
19
- // enable 必须显式声明且只能为 boolean(true/false),不允许 0/1 等其他类型。
20
- if (!Object.prototype.hasOwnProperty.call(hook, "enable")) {
21
- Logger.warn(omit(hook, ["handler"]), "钩子的 enable 属性是必填项,且必须显式声明为 true 或 false");
22
+ const customKeys = hook.customKeys;
23
+ if (!Array.isArray(customKeys)) {
24
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子扫描结果缺少 customKeys(无法判断用户导出的字段是否合法)" }));
22
25
  hasError = true;
23
26
  continue;
24
27
  }
25
- if (typeof hook.enable !== "boolean") {
26
- Logger.warn(omit(hook, ["handler"]), "钩子的 enable 属性必须是 boolean(true/false),不允许 0/1 等其他类型");
28
+ if (customKeys.some((k) => typeof k !== "string")) {
29
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 customKeys 必须是 string[](由系统生成)" }));
27
30
  hasError = true;
28
31
  continue;
29
32
  }
33
+ // 严格字段校验:仅检查用户 default export 的字段集合,出现任何未支持字段都应视为错误。
34
+ const unknownCustomKeys = customKeys.filter((k) => !exportKeys.includes(k));
35
+ if (unknownCustomKeys.length > 0) {
36
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: `钩子导出存在不支持的属性:${unknownCustomKeys.join(", ")};仅允许:${exportKeys.join(", ")};当前 customKeys:${customKeys.join(", ")}` }));
37
+ hasError = true;
38
+ continue;
39
+ }
40
+ const hasCustomEnable = customKeys.includes("enable");
41
+ const hasCustomDeps = customKeys.includes("deps");
42
+ // enable 必须显式声明且只能为 boolean(true/false),不允许 0/1 等其他类型。
43
+ // - 允许缺省:由系统在此处补全默认值 true
44
+ // - 若用户显式导出 enable:必须是 boolean
45
+ if (hasCustomEnable) {
46
+ if (typeof hook.enable !== "boolean") {
47
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 enable 属性必须是 boolean(true/false),不允许 0/1 等其他类型" }));
48
+ hasError = true;
49
+ continue;
50
+ }
51
+ }
52
+ else {
53
+ hook.enable = true;
54
+ }
30
55
  // core 内置钩子:必须来自静态注册(filePath 以 core:hook: 开头),且 name 必须显式指定并与 moduleName 一致。
31
56
  if (hook.source === "core") {
32
57
  const name = typeof hook.name === "string" ? hook.name : "";
33
58
  if (name === "") {
34
- Logger.warn(omit(hook, ["handler"]), "core 内置钩子必须显式设置 name(string),用于确定钩子名称");
59
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子必须显式设置 name(string),用于确定钩子名称" }));
35
60
  hasError = true;
36
61
  continue;
37
62
  }
38
63
  // name 必须满足:小写字母 + 下划线(不允许空格、驼峰、数字等)。
39
64
  if (!coreBuiltinNameRegexp.test(name)) {
40
- Logger.warn(omit(hook, ["handler"]), "core 内置钩子的 name 必须满足小写字母+下划线格式(例如 auth / rate_limit),不允许空格、驼峰或其他字符");
65
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子的 name 必须满足小写字母+下划线格式(例如 auth / rate_limit),不允许空格、驼峰或其他字符" }));
41
66
  hasError = true;
42
67
  continue;
43
68
  }
44
69
  if (!coreBuiltinNameRegexp.test(hook.moduleName)) {
45
- Logger.warn(omit(hook, ["handler"]), "core 内置钩子的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)");
70
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)" }));
46
71
  hasError = true;
47
72
  continue;
48
73
  }
49
74
  if (name !== hook.moduleName) {
50
- Logger.warn(omit(hook, ["handler"]), "core 内置钩子的 name 必须与 moduleName 完全一致");
75
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子的 name 必须与 moduleName 完全一致" }));
51
76
  hasError = true;
52
77
  continue;
53
78
  }
54
79
  if (typeof hook.filePath !== "string" || !hook.filePath.startsWith(`core:hook:${name}`)) {
55
- Logger.warn(omit(hook, ["handler"]), "core 内置钩子必须来自静态注册(filePath 必须以 core:hook:<name> 开头),不允许通过扫描目录加载");
80
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子必须来自静态注册(filePath 必须以 core:hook:<name> 开头),不允许通过扫描目录加载" }));
56
81
  hasError = true;
57
82
  continue;
58
83
  }
59
84
  }
60
- if (!Array.isArray(hook.deps)) {
61
- Logger.warn(omit(hook, ["handler"]), "钩子的 deps 属性必须是字符串数组");
62
- hasError = true;
63
- continue;
85
+ // deps:允许缺省(补全为 []),但如果用户显式导出 deps,则必须是 string[]。
86
+ if (hasCustomDeps) {
87
+ if (!Array.isArray(hook.deps)) {
88
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 deps 属性必须是字符串数组" }));
89
+ hasError = true;
90
+ continue;
91
+ }
92
+ if (hook.deps.some((depItem) => typeof depItem !== "string")) {
93
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 deps 属性必须是字符串数组" }));
94
+ hasError = true;
95
+ continue;
96
+ }
64
97
  }
65
- if (hook.deps.some((depItem) => typeof depItem !== "string")) {
66
- Logger.warn(omit(hook, ["handler"]), "钩子的 deps 属性必须是字符串数组");
67
- hasError = true;
98
+ else {
99
+ if (!Array.isArray(hook.deps)) {
100
+ hook.deps = [];
101
+ }
68
102
  }
69
103
  if (typeof hook.handler !== "function") {
70
- Logger.warn(omit(hook, ["handler"]), "钩子的 handler 属性必须是函数");
104
+ Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 handler 属性必须是函数" }));
71
105
  hasError = true;
72
106
  continue;
73
107
  }
74
108
  }
75
109
  catch (error) {
76
- Logger.error({
77
- err: error,
78
- item: hook
79
- }, "钩子解析失败");
110
+ Logger.error({ err: error, item: hook, msg: "钩子解析失败" });
80
111
  hasError = true;
81
112
  }
82
113
  }
@@ -59,24 +59,24 @@ export const checkMenu = async (addons, options = {}) => {
59
59
  const depth = typeof current?.depth === "number" ? current.depth : 0;
60
60
  if (menu === null || typeof menu !== "object") {
61
61
  hasError = true;
62
- Logger.warn({ menu: menu }, "菜单节点必须是对象");
62
+ Logger.warn({ menu: menu, msg: "菜单节点必须是对象" });
63
63
  continue;
64
64
  }
65
65
  if (depth > 3) {
66
66
  hasError = true;
67
- Logger.warn({ path: menu?.path, depth: depth }, "菜单层级超过 3 级(最多三级)");
67
+ Logger.warn({ path: menu?.path, depth: depth, msg: "菜单层级超过 3 级(最多三级)" });
68
68
  continue;
69
69
  }
70
70
  const children = menu.children;
71
71
  if (typeof children !== "undefined" && !Array.isArray(children)) {
72
72
  hasError = true;
73
- Logger.warn({ path: menu?.path, childrenType: typeof children }, "菜单 children 必须是数组");
73
+ Logger.warn({ path: menu?.path, childrenType: typeof children, msg: "菜单 children 必须是数组" });
74
74
  continue;
75
75
  }
76
76
  if (Array.isArray(children) && children.length > 0) {
77
77
  if (depth >= 3) {
78
78
  hasError = true;
79
- Logger.warn({ path: menu?.path, depth: depth }, "菜单层级超过 3 级(最多三级)");
79
+ Logger.warn({ path: menu?.path, depth: depth, msg: "菜单层级超过 3 级(最多三级)" });
80
80
  }
81
81
  else {
82
82
  for (const child of children) {
@@ -99,29 +99,29 @@ export const checkMenu = async (addons, options = {}) => {
99
99
  }
100
100
  if (!path) {
101
101
  hasError = true;
102
- Logger.warn({ menu: menu }, "菜单缺少 path(必须是非空字符串)");
102
+ Logger.warn({ menu: menu, msg: "菜单缺少 path(必须是非空字符串)" });
103
103
  continue;
104
104
  }
105
105
  const pathCheck = isValidMenuPath(path);
106
106
  if (!pathCheck.ok) {
107
107
  hasError = true;
108
- Logger.warn({ path: path, reason: pathCheck.reason }, "菜单 path 不合法");
108
+ Logger.warn({ path: path, reason: pathCheck.reason, msg: "菜单 path 不合法" });
109
109
  }
110
110
  if (!name) {
111
111
  hasError = true;
112
- Logger.warn({ path: path, menu: menu }, "菜单缺少 name(必须是非空字符串)");
112
+ Logger.warn({ path: path, menu: menu, msg: "菜单缺少 name(必须是非空字符串)" });
113
113
  }
114
114
  if (typeof menu.sort !== "undefined" && typeof menu.sort !== "number") {
115
115
  hasError = true;
116
- Logger.warn({ path: path, sort: menu.sort }, "菜单 sort 必须是 number");
116
+ Logger.warn({ path: path, sort: menu.sort, msg: "菜单 sort 必须是 number" });
117
117
  }
118
118
  if (typeof menu.sort === "number" && (!Number.isFinite(menu.sort) || menu.sort < 1)) {
119
119
  hasError = true;
120
- Logger.warn({ path: path, sort: menu.sort }, "菜单 sort 最小值为 1");
120
+ Logger.warn({ path: path, sort: menu.sort, msg: "菜单 sort 最小值为 1" });
121
121
  }
122
122
  if (pathSet.has(path)) {
123
123
  hasError = true;
124
- Logger.warn({ path: path }, "菜单 path 重复(严格模式禁止重复 path)");
124
+ Logger.warn({ path: path, msg: "菜单 path 重复(严格模式禁止重复 path)" });
125
125
  continue;
126
126
  }
127
127
  pathSet.add(path);
@@ -1,82 +1,113 @@
1
1
  import { Logger } from "../lib/logger";
2
2
  import { isPlainObject, omit } from "../utils/util";
3
+ const exportKeys = ["name", "enable", "deps", "handler"];
3
4
  export async function checkPlugin(plugins) {
4
5
  let hasError = false;
6
+ // 说明:plugins 实际是 scanFiles/scanSources 的结果对象(包含元信息字段)。
7
+ // 这里不再对白名单枚举 metaKeys(因为它们是系统生成的),只校验“用户 default export 导出的字段”。
5
8
  const coreBuiltinNameRegexp = /^[a-z]+(?:_[a-z]+)*$/;
6
9
  for (const plugin of plugins) {
7
10
  try {
8
11
  if (!isPlainObject(plugin)) {
9
- Logger.warn(omit(plugin, ["handler"]), "插件导出必须是对象(export default { deps, handler })");
12
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件导出必须是对象(export default { deps, handler })" }));
10
13
  hasError = true;
11
14
  continue;
12
15
  }
13
16
  // moduleName 必须存在(用于依赖排序与运行时挂载)。
14
17
  if (typeof plugin.moduleName !== "string" || plugin.moduleName.trim() === "") {
15
- Logger.warn(omit(plugin, ["handler"]), "插件的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)");
18
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)" }));
16
19
  hasError = true;
17
20
  continue;
18
21
  }
19
- // enable 必须显式声明且只能为 boolean(true/false),不允许 0/1 等其他类型。
20
- if (!Object.prototype.hasOwnProperty.call(plugin, "enable")) {
21
- Logger.warn(omit(plugin, ["handler"]), "插件的 enable 属性是必填项,且必须显式声明为 true 或 false");
22
+ const customKeys = plugin.customKeys;
23
+ if (!Array.isArray(customKeys)) {
24
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件扫描结果缺少 customKeys(无法判断用户导出的字段是否合法)" }));
22
25
  hasError = true;
23
26
  continue;
24
27
  }
25
- if (typeof plugin.enable !== "boolean") {
26
- Logger.warn(omit(plugin, ["handler"]), "插件的 enable 属性必须是 boolean(true/false),不允许 0/1 等其他类型");
28
+ if (customKeys.some((k) => typeof k !== "string")) {
29
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 customKeys 必须是 string[](由系统生成)" }));
27
30
  hasError = true;
28
31
  continue;
29
32
  }
33
+ // 严格字段校验:仅检查用户 default export 的字段集合,出现任何未支持字段都应视为错误。
34
+ const unknownCustomKeys = customKeys.filter((k) => !exportKeys.includes(k));
35
+ if (unknownCustomKeys.length > 0) {
36
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: `插件导出存在不支持的属性:${unknownCustomKeys.join(", ")};仅允许:${exportKeys.join(", ")};当前 customKeys:${customKeys.join(", ")}` }));
37
+ hasError = true;
38
+ continue;
39
+ }
40
+ const hasCustomEnable = customKeys.includes("enable");
41
+ const hasCustomDeps = customKeys.includes("deps");
42
+ // enable 必须显式声明且只能为 boolean(true/false),不允许 0/1 等其他类型。
43
+ // - 允许缺省:由系统在此处补全默认值 true
44
+ // - 若用户显式导出 enable:必须是 boolean
45
+ if (hasCustomEnable) {
46
+ if (typeof plugin.enable !== "boolean") {
47
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 enable 属性必须是 boolean(true/false),不允许 0/1 等其他类型" }));
48
+ hasError = true;
49
+ continue;
50
+ }
51
+ }
52
+ else {
53
+ plugin.enable = true;
54
+ }
30
55
  // core 内置插件:必须来自静态注册(filePath 以 core:plugin: 开头),且 name 必须显式指定并与 moduleName 一致。
31
56
  if (plugin.source === "core") {
32
57
  const name = typeof plugin.name === "string" ? plugin.name : "";
33
58
  if (name === "") {
34
- Logger.warn(omit(plugin, ["handler"]), "core 内置插件必须显式设置 name(string),用于确定插件名称");
59
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件必须显式设置 name(string),用于确定插件名称" }));
35
60
  hasError = true;
36
61
  continue;
37
62
  }
38
63
  // name 必须满足:小写字母 + 下划线(不允许空格、驼峰、数字等)。
39
64
  if (!coreBuiltinNameRegexp.test(name)) {
40
- Logger.warn(omit(plugin, ["handler"]), "core 内置插件的 name 必须满足小写字母+下划线格式(例如 logger / redis_cache),不允许空格、驼峰或其他字符");
65
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件的 name 必须满足小写字母+下划线格式(例如 logger / redis_cache),不允许空格、驼峰或其他字符" }));
41
66
  hasError = true;
42
67
  continue;
43
68
  }
44
69
  if (!coreBuiltinNameRegexp.test(plugin.moduleName)) {
45
- Logger.warn(omit(plugin, ["handler"]), "core 内置插件的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)");
70
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)" }));
46
71
  hasError = true;
47
72
  continue;
48
73
  }
49
74
  if (name !== plugin.moduleName) {
50
- Logger.warn(omit(plugin, ["handler"]), "core 内置插件的 name 必须与 moduleName 完全一致");
75
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件的 name 必须与 moduleName 完全一致" }));
51
76
  hasError = true;
52
77
  continue;
53
78
  }
54
79
  if (typeof plugin.filePath !== "string" || !plugin.filePath.startsWith(`core:plugin:${name}`)) {
55
- Logger.warn(omit(plugin, ["handler"]), "core 内置插件必须来自静态注册(filePath 必须以 core:plugin:<name> 开头),不允许通过扫描目录加载");
80
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件必须来自静态注册(filePath 必须以 core:plugin:<name> 开头),不允许通过扫描目录加载" }));
56
81
  hasError = true;
57
82
  continue;
58
83
  }
59
84
  }
60
- if (!Array.isArray(plugin.deps)) {
61
- Logger.warn(omit(plugin, ["handler"]), "插件的 deps 属性必须是字符串数组");
62
- hasError = true;
63
- continue;
85
+ // deps:允许缺省(补全为 []),但如果用户显式导出 deps,则必须是 string[]。
86
+ if (hasCustomDeps) {
87
+ if (!Array.isArray(plugin.deps)) {
88
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 deps 属性必须是字符串数组" }));
89
+ hasError = true;
90
+ continue;
91
+ }
92
+ if (plugin.deps.some((depItem) => typeof depItem !== "string")) {
93
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 deps 属性必须是字符串数组" }));
94
+ hasError = true;
95
+ continue;
96
+ }
64
97
  }
65
- if (plugin.deps.some((depItem) => typeof depItem !== "string")) {
66
- Logger.warn(omit(plugin, ["handler"]), "插件的 deps 属性必须是字符串数组");
67
- hasError = true;
98
+ else {
99
+ if (!Array.isArray(plugin.deps)) {
100
+ plugin.deps = [];
101
+ }
68
102
  }
69
103
  if (typeof plugin.handler !== "function") {
70
- Logger.warn(omit(plugin, ["handler"]), "插件的 handler 属性必须是函数");
104
+ Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 handler 属性必须是函数" }));
71
105
  hasError = true;
72
106
  continue;
73
107
  }
74
108
  }
75
109
  catch (error) {
76
- Logger.error({
77
- err: error,
78
- item: plugin
79
- }, "插件解析失败");
110
+ Logger.error({ err: error, item: plugin, msg: "插件解析失败" });
80
111
  hasError = true;
81
112
  }
82
113
  }
@@ -40,25 +40,26 @@ export async function checkTable(tables) {
40
40
  continue;
41
41
  }
42
42
  const sourceName = typeof item.sourceName === "string" ? item.sourceName : "";
43
+ const tablePrefix = sourceName ? `${sourceName}表 ` : "表 ";
43
44
  try {
44
45
  const fileName = item.fileName;
45
46
  const table = item.content || {};
46
47
  // 1) 文件名小驼峰校验
47
48
  if (!LOWER_CAMEL_CASE_REGEX.test(fileName)) {
48
- Logger.warn(`${sourceName}${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
49
+ Logger.warn(`${tablePrefix}${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
49
50
  hasError = true;
50
51
  continue;
51
52
  }
52
53
  // 检查 table 中的每个验证规则
53
54
  for (const [colKey, fieldDef] of Object.entries(table)) {
54
55
  if (typeof fieldDef !== "object" || fieldDef === null || Array.isArray(fieldDef)) {
55
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 规则必须为对象`);
56
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 规则必须为对象`);
56
57
  hasError = true;
57
58
  continue;
58
59
  }
59
60
  // 检查是否使用了保留字段
60
61
  if (RESERVED_FIELDS.includes(colKey)) {
61
- Logger.warn(`${sourceName}${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(", ")}`);
62
+ Logger.warn(`${tablePrefix}${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(", ")}`);
62
63
  hasError = true;
63
64
  }
64
65
  // 直接使用字段对象
@@ -67,78 +68,78 @@ export async function checkTable(tables) {
67
68
  const fieldKeys = Object.keys(field);
68
69
  const illegalProps = fieldKeys.filter((key) => !ALLOWED_FIELD_PROPERTIES.includes(key));
69
70
  if (illegalProps.length > 0) {
70
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 包含非法属性: ${illegalProps.join(", ")},` + `允许的属性为: ${ALLOWED_FIELD_PROPERTIES.join(", ")}`);
71
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 包含非法属性: ${illegalProps.join(", ")},` + `允许的属性为: ${ALLOWED_FIELD_PROPERTIES.join(", ")}`);
71
72
  hasError = true;
72
73
  }
73
74
  // 检查必填字段:name, type
74
75
  if (!field.name || typeof field.name !== "string") {
75
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
76
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
76
77
  hasError = true;
77
78
  continue;
78
79
  }
79
80
  if (!field.type || typeof field.type !== "string") {
80
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
81
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
81
82
  hasError = true;
82
83
  continue;
83
84
  }
84
85
  // 检查可选字段的类型
85
86
  if (field.min !== undefined && !(field.min === null || typeof field.min === "number")) {
86
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
87
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
87
88
  hasError = true;
88
89
  }
89
90
  if (field.max !== undefined && !(field.max === null || typeof field.max === "number")) {
90
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
91
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
91
92
  hasError = true;
92
93
  }
93
94
  if (field.detail !== undefined && typeof field.detail !== "string") {
94
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
95
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
95
96
  hasError = true;
96
97
  }
97
98
  if (field.index !== undefined && typeof field.index !== "boolean") {
98
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
99
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
99
100
  hasError = true;
100
101
  }
101
102
  if (field.unique !== undefined && typeof field.unique !== "boolean") {
102
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
103
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
103
104
  hasError = true;
104
105
  }
105
106
  if (field.nullable !== undefined && typeof field.nullable !== "boolean") {
106
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
107
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
107
108
  hasError = true;
108
109
  }
109
110
  if (field.unsigned !== undefined && typeof field.unsigned !== "boolean") {
110
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
111
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
111
112
  hasError = true;
112
113
  }
113
114
  if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== "string") {
114
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
115
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
115
116
  hasError = true;
116
117
  }
117
118
  const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault } = field;
118
119
  // 字段名称必须为中文、数字、字母、下划线、短横线、空格
119
120
  if (!FIELD_NAME_REGEX.test(fieldName)) {
120
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
121
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
121
122
  hasError = true;
122
123
  }
123
124
  // 字段类型必须为string,number,text,array_string,array_text之一
124
125
  if (!FIELD_TYPES.includes(fieldType)) {
125
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join("、")}之一`);
126
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join("、")}之一`);
126
127
  hasError = true;
127
128
  }
128
129
  // unsigned 仅对 number 类型有效(且仅 MySQL 语义上生效)
129
130
  if (fieldType !== "number" && field.unsigned !== undefined) {
130
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 字段类型为 ${fieldType},不允许设置 unsigned(仅 number 类型有效)`);
131
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段类型为 ${fieldType},不允许设置 unsigned(仅 number 类型有效)`);
131
132
  hasError = true;
132
133
  }
133
134
  // 约束:unique 与 index 不能同时为 true(否则会重复索引),必须阻断启动。
134
135
  if (field.unique === true && field.index === true) {
135
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 同时设置了 unique=true 和 index=true,` + `unique 和 index 不能同时设置,请删除其一(否则会创建重复索引)`);
136
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 同时设置了 unique=true 和 index=true,` + `unique 和 index 不能同时设置,请删除其一(否则会创建重复索引)`);
136
137
  hasError = true;
137
138
  }
138
139
  // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
139
140
  if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
140
141
  if (fieldMin > fieldMax) {
141
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
142
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
142
143
  hasError = true;
143
144
  }
144
145
  }
@@ -146,47 +147,47 @@ export async function checkTable(tables) {
146
147
  if (fieldType === "text" || fieldType === "array_text" || fieldType === "array_number_text") {
147
148
  // text / array_text / array_number_text:min/max 必须为 null,默认值必须为 null,且不支持索引/唯一约束
148
149
  if (fieldMin !== undefined && fieldMin !== null) {
149
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 的 ${fieldType} 类型最小值应为 null,当前为 "${fieldMin}"`);
150
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 的 ${fieldType} 类型最小值应为 null,当前为 "${fieldMin}"`);
150
151
  hasError = true;
151
152
  }
152
153
  if (fieldMax !== undefined && fieldMax !== null) {
153
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 的 ${fieldType} 类型最大长度应为 null,当前为 "${fieldMax}"`);
154
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 的 ${fieldType} 类型最大长度应为 null,当前为 "${fieldMax}"`);
154
155
  hasError = true;
155
156
  }
156
157
  if (fieldDefault !== undefined && fieldDefault !== null) {
157
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
158
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
158
159
  hasError = true;
159
160
  }
160
161
  if (field.index === true) {
161
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持创建索引(index=true 无效)`);
162
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持创建索引(index=true 无效)`);
162
163
  hasError = true;
163
164
  }
164
165
  if (field.unique === true) {
165
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持唯一约束(unique=true 无效)`);
166
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持唯一约束(unique=true 无效)`);
166
167
  hasError = true;
167
168
  }
168
169
  }
169
170
  else if (fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
170
171
  if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== "number")) {
171
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
172
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
172
173
  hasError = true;
173
174
  }
174
175
  else if (fieldMax !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
175
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
176
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
176
177
  hasError = true;
177
178
  }
178
179
  }
179
180
  else if (fieldType === "number") {
180
181
  // number 类型:default 如果存在,必须为 null 或 number
181
182
  if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== "number") {
182
- Logger.warn(`${sourceName}${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
183
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
183
184
  hasError = true;
184
185
  }
185
186
  }
186
187
  }
187
188
  }
188
189
  catch (error) {
189
- Logger.error(`${sourceName}${item.fileName} 解析失败`, error);
190
+ Logger.error({ msg: `${tablePrefix}${item.fileName} 解析失败`, err: error });
190
191
  hasError = true;
191
192
  }
192
193
  }