befly 3.72.0 → 3.74.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.
package/checks/api.js CHANGED
@@ -1,125 +1,68 @@
1
- import * as z from "zod";
2
-
3
- import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
4
1
  import { Logger } from "../lib/logger.js";
5
- import { formatZodIssues } from "../utils/formatZodIssues.js";
6
- import { isNoTrimStringAllowEmpty, isNullable, isRegexInput } from "../utils/is.js";
7
-
8
- z.config(z.locales.zhCN());
2
+ import { validateFieldRule } from "../lib/validator.js";
3
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
4
+ import { isBoolean, isFunction, isPlainObject, isString } from "../utils/is.js";
9
5
 
10
- const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
11
6
  const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
12
7
  const apiPathRegex = /^\/api\/[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*$/;
8
+ const apiKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "name", "handler", "apiPath", "method", "body", "auth", "fields", "required"]);
13
9
 
14
- function addIssue(context, path, message) {
15
- context.addIssue({
16
- path: path,
17
- message: message
18
- });
10
+ function checkNoTrimString(issues, value, path, min = 0, max = Number.MAX_SAFE_INTEGER) {
11
+ if (!isString(value)) {
12
+ issues.push({ path: path, message: "必须是字符串" });
13
+ return;
14
+ }
15
+ if (value !== "" && value.trim() !== value) issues.push({ path: path, message: "不允许首尾空格" });
16
+ if (value.length < min) issues.push({ path: path, message: `长度不能少于${min}个字符` });
17
+ if (value.length > max) issues.push({ path: path, message: `长度不能超过${max}个字符` });
19
18
  }
20
19
 
21
- const fieldSchema = z
22
- .object({
23
- name: noTrimString.min(1),
24
- input: z.enum(FIELD_RULE_INPUT_TYPES),
25
- check: noTrimString.optional(),
26
- min: z.number().nullable().optional(),
27
- max: z.number().nullable().optional(),
28
- detail: z.string().optional()
29
- })
30
- .strict()
31
- .superRefine((value, context) => {
32
- const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
33
- const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
34
- if (min > max) {
35
- addIssue(context, ["min"], "min 不能大于 max");
36
- }
37
-
38
- const checkValue = value.check;
39
- const input = value.input;
40
-
41
- if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
42
- addIssue(context, ["check"], `${input} 必须提供 check`);
43
- return;
44
- }
45
-
46
- if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
47
- addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
48
- return;
49
- }
50
-
51
- if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
52
- const items = String(checkValue)
53
- .split("|")
54
- .map((item) => item.trim())
55
- .filter((item) => item.length > 0);
56
-
57
- if (items.length === 0) {
58
- addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
59
- return;
60
- }
61
-
62
- if (input === "enumNumber" || input === "enumInteger") {
63
- for (const item of items) {
64
- const num = Number(item);
65
- if (!Number.isFinite(num)) {
66
- addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
67
- return;
68
- }
69
- if (input === "enumInteger" && !Number.isInteger(num)) {
70
- addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
71
- return;
72
- }
73
- }
74
- }
75
- }
76
- });
77
-
78
- const fieldsSchema = z.record(z.string().regex(lowerCamelRegex), fieldSchema);
20
+ function checkApiItem(api, index) {
21
+ const issues = [];
22
+ if (!isPlainObject(api)) {
23
+ issues.push({ path: [index], message: "必须是对象格式" });
24
+ return issues;
25
+ }
79
26
 
80
- const apiSchema = z
81
- .object({
82
- source: noTrimString.min(1),
83
- type: noTrimString.min(1),
84
- filePath: noTrimString.min(1),
85
- relativePath: noTrimString.min(1),
86
- fileName: z.string().min(1).regex(lowerCamelRegex),
87
- name: noTrimString.min(1).max(100),
88
- handler: z.custom((value) => typeof value === "function"),
89
- apiPath: noTrimString.regex(apiPathRegex),
90
- method: z.enum(["GET", "POST", "ALL"]),
91
- body: z.enum(["none", "raw"]),
92
- auth: z.union([z.boolean(), z.array(noTrimString).min(1)]),
93
- fields: fieldsSchema,
94
- required: z.array(noTrimString)
95
- })
96
- .strict()
97
- .superRefine((value, context) => {
98
- if (value.source !== "app") {
99
- return;
100
- }
27
+ for (const key of Object.keys(api)) {
28
+ if (!apiKeys.has(key)) issues.push({ path: [index, key], message: "不允许出现" });
29
+ }
30
+ for (const key of ["source", "type", "filePath", "relativePath"]) checkNoTrimString(issues, api[key], [index, key], 1);
31
+ checkNoTrimString(issues, api.name, [index, "name"], 1, 100);
32
+ if (!isString(api.fileName) || !lowerCamelRegex.test(api.fileName)) issues.push({ path: [index, "fileName"], message: "必须是 lowerCamelCase" });
33
+ if (!isFunction(api.handler)) issues.push({ path: [index, "handler"], message: "必须是函数" });
34
+ if (!isString(api.apiPath) || !apiPathRegex.test(api.apiPath)) issues.push({ path: [index, "apiPath"], message: "路径格式无效" });
35
+ if (!["GET", "POST", "ALL"].includes(api.method)) issues.push({ path: [index, "method"], message: "仅允许 GET|POST|ALL" });
36
+ if (!["none", "raw"].includes(api.body)) issues.push({ path: [index, "body"], message: "仅允许 none|raw" });
37
+
38
+ if (!isBoolean(api.auth) && (!Array.isArray(api.auth) || api.auth.length === 0 || api.auth.some((item) => !isString(item) || (item !== "" && item.trim() !== item)))) {
39
+ issues.push({ path: [index, "auth"], message: "必须是 boolean 或非空字符串数组" });
40
+ }
101
41
 
102
- if (value.relativePath.split("/")[0] !== "core") {
103
- return;
42
+ if (!isPlainObject(api.fields)) {
43
+ issues.push({ path: [index, "fields"], message: "必须是对象格式" });
44
+ } else {
45
+ for (const [fieldName, rule] of Object.entries(api.fields)) {
46
+ if (!lowerCamelRegex.test(fieldName)) issues.push({ path: [index, "fields", fieldName], message: "字段名必须为 lowerCamelCase" });
47
+ for (const issue of validateFieldRule(rule)) issues.push({ path: [index, "fields", fieldName, ...issue.path], message: issue.message });
104
48
  }
49
+ }
105
50
 
106
- addIssue(context, ["relativePath"], "app API 不能使用 core 作为一级目录,避免占用 /api/core 命名空间");
107
- });
51
+ if (!Array.isArray(api.required) || api.required.some((item) => !isString(item) || (item !== "" && item.trim() !== item))) issues.push({ path: [index, "required"], message: "必须是字符串数组" });
52
+ if (api.source === "app" && isString(api.relativePath) && api.relativePath.split("/")[0] === "core") issues.push({ path: [index, "relativePath"], message: "app API 不能使用 core 作为一级目录,避免占用 /api/core 命名空间" });
108
53
 
109
- const apiListSchema = z.array(apiSchema);
54
+ return issues;
55
+ }
110
56
 
111
57
  export async function checkApi(apis) {
112
- const result = apiListSchema.safeParse(apis);
113
- if (result.success) {
114
- return false;
58
+ const issues = [];
59
+ if (!Array.isArray(apis)) {
60
+ issues.push({ path: [], message: "必须是数组" });
61
+ } else {
62
+ apis.forEach((api, index) => issues.push(...checkApiItem(api, index)));
115
63
  }
116
64
 
117
- Logger.warn(
118
- "接口校验失败",
119
- {
120
- errors: formatZodIssues(result.error.issues, { items: apis, itemLabel: "api" })
121
- },
122
- false
123
- );
65
+ if (issues.length === 0) return false;
66
+ Logger.warn("接口校验失败", { errors: formatValidationIssues(issues, { items: apis, itemLabel: "api" }) }, false);
124
67
  return true;
125
68
  }
package/checks/config.js CHANGED
@@ -1,143 +1,15 @@
1
- import * as z from "zod";
2
-
3
- import { RUN_MODE_VALUES } from "../configs/constConfig.js";
4
1
  import { Logger } from "../lib/logger.js";
5
- import { formatZodIssues } from "../utils/formatZodIssues.js";
6
- import { isNoTrimStringAllowEmpty, isValidTimeZone } from "../utils/is.js";
7
-
8
- z.config(z.locales.zhCN());
9
-
10
- const boolIntSchema = z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]);
11
- const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
12
- const beflyModeSchema = z.union([z.literal("manual"), z.literal("auto")]);
13
- const uploadExtensionListSchema = noTrimString.regex(/^\.[a-z0-9]+(?:,\.[a-z0-9]+)*$/);
14
- const uploadMimeTypeListSchema = noTrimString.regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*(?:,[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*)*$/);
15
- const configSchema = z
16
- .object({
17
- runMode: z.enum(RUN_MODE_VALUES),
18
- appName: noTrimString,
19
- appPort: z.int().min(1).max(65535),
20
- appHost: noTrimString,
21
- apiHost: noTrimString,
22
- devEmail: z.union([z.literal(""), z.email()]),
23
- devPassword: z.string().min(6),
24
- bodyLimit: z.int().min(1),
25
- upload: z
26
- .object({
27
- maxSize: z.int().min(1),
28
- publicDir: noTrimString.min(1),
29
- publicHost: noTrimString,
30
- allowedExtensions: uploadExtensionListSchema,
31
- allowedMimeTypes: uploadMimeTypeListSchema,
32
- forceDownloadExtensions: z.union([z.literal(""), uploadExtensionListSchema])
33
- })
34
- .strict(),
35
- tz: z.string().refine((value) => isValidTimeZone(value), "无效的时区"),
36
- excludeApisLog: z.array(noTrimString),
37
-
38
- logger: z
39
- .object({
40
- debug: boolIntSchema,
41
- excludeFields: z.array(noTrimString)
42
- })
43
- .strict(),
44
-
45
- mysql: z
46
- .object({
47
- hostname: noTrimString,
48
- port: z.int().min(1).max(65535),
49
- username: noTrimString,
50
- password: noTrimString,
51
- database: noTrimString,
52
- max: z.number().min(1),
53
- beflyMode: beflyModeSchema.optional(),
54
- idleTimeout: z.number().min(1).optional(),
55
- maxLifetime: z.number().min(0).optional(),
56
- connectionTimeout: z.number().min(1).optional()
57
- })
58
- .strict(),
59
-
60
- redis: z
61
- .object({
62
- hostname: noTrimString,
63
- port: z.int().min(1).max(65535),
64
- username: noTrimString,
65
- password: noTrimString,
66
- db: z.int().min(0).max(16),
67
- prefix: noTrimString.regex(/^[^:]*$/)
68
- })
69
- .strict(),
70
-
71
- session: z
72
- .object({
73
- expireDays: z.int().min(1)
74
- })
75
- .strict(),
76
-
77
- email: z
78
- .object({
79
- host: noTrimString,
80
- port: z.int().min(1).max(65535),
81
- secure: z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]),
82
- user: noTrimString,
83
- pass: noTrimString,
84
- label: noTrimString.optional()
85
- })
86
- .strict()
87
- .optional(),
88
-
89
- cors: z
90
- .object({
91
- origin: noTrimString,
92
- methods: noTrimString,
93
- allowedHeaders: noTrimString,
94
- exposedHeaders: noTrimString,
95
- maxAge: z.int().min(0),
96
- credentials: z.union([z.literal("true"), z.literal("false"), z.literal(true), z.literal(false)])
97
- })
98
- .strict()
99
- .superRefine((cors, ctx) => {
100
- if (cors.origin !== "*" || (cors.credentials !== true && cors.credentials !== "true")) {
101
- return;
102
- }
103
-
104
- ctx.addIssue({
105
- code: "custom",
106
- message: "cors.credentials 为 true 时 cors.origin 不能为 *",
107
- path: ["credentials"]
108
- });
109
- }),
110
-
111
- rateLimit: z
112
- .object({
113
- enable: boolIntSchema,
114
- defaultLimit: z.int().min(1),
115
- defaultWindow: z.int().min(1),
116
- key: noTrimString,
117
- skipRoutes: z.array(noTrimString),
118
- rules: z.array(z.object({}).passthrough())
119
- })
120
- .strict()
121
- })
122
- .strict();
2
+ import { validateConfig } from "../lib/validator.js";
3
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
123
4
 
124
- /**
125
- * 配置检查(启动期强校验)
126
- *
127
- * 说明:
128
- * - checkTable/checkApi 等校验的是“扫描到的源码/表定义”。
129
- * - checkConfig 校验的是“最终合并后的运行时配置对象”,用于阻断错误配置带病启动。
130
- */
131
5
  export async function checkConfig(config) {
132
- const result = configSchema.safeParse(config);
133
- if (result.success) {
134
- return false;
135
- }
6
+ const issues = validateConfig(config);
7
+ if (issues.length === 0) return false;
136
8
 
137
9
  Logger.warn(
138
10
  "配置校验失败",
139
11
  {
140
- errors: formatZodIssues(result.error.issues, { item: config, itemLabel: "config" })
12
+ errors: formatValidationIssues(issues, { item: config, itemLabel: "config" })
141
13
  },
142
14
  false
143
15
  );
package/checks/hook.js CHANGED
@@ -1,42 +1,39 @@
1
- import * as z from "zod";
2
-
3
1
  import { Logger } from "../lib/logger.js";
4
- import { formatZodIssues } from "../utils/formatZodIssues.js";
5
- import { isNoTrimStringAllowEmpty } from "../utils/is.js";
2
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
+ import { isFiniteNumber, isFunction, isPlainObject, isString } from "../utils/is.js";
6
4
 
7
- z.config(z.locales.zhCN());
5
+ const hookKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
8
6
 
9
- const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
7
+ function checkHookItem(hook) {
8
+ const issues = [];
9
+ if (!isPlainObject(hook)) {
10
+ issues.push({ path: [], message: "必须是对象格式" });
11
+ return issues;
12
+ }
10
13
 
11
- const hookSchema = z
12
- .object({
13
- source: noTrimString.min(1),
14
- type: noTrimString.min(1),
15
- filePath: noTrimString.min(1),
16
- relativePath: noTrimString.min(1),
17
- fileName: noTrimString.min(1),
18
- apiPath: noTrimString.min(1),
19
- handler: z.custom((value) => typeof value === "function"),
20
- order: z.number()
21
- })
22
- .strict();
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
+ }
23
24
 
24
25
  export async function checkHook(hooks) {
25
- let hasError = false;
26
+ if (!Array.isArray(hooks)) {
27
+ Logger.warn("钩子校验失败", { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: hooks, itemLabel: "hook" }) }, false);
28
+ return true;
29
+ }
26
30
 
31
+ let hasError = false;
27
32
  for (const hook of hooks) {
28
- const result = hookSchema.safeParse(hook);
29
- if (!result.success) {
30
- Logger.warn(
31
- "钩子校验失败",
32
- {
33
- errors: formatZodIssues(result.error.issues, { item: hook, itemLabel: "hook" })
34
- },
35
- false
36
- );
37
- hasError = true;
38
- }
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;
39
37
  }
40
-
41
38
  return hasError;
42
39
  }
package/checks/menu.js CHANGED
@@ -1,62 +1,48 @@
1
- import * as z from "zod";
2
-
3
1
  import { Logger } from "../lib/logger.js";
4
- import { formatZodIssues } from "../utils/formatZodIssues.js";
5
-
6
- z.config(z.locales.zhCN());
2
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
+ import { isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
7
4
 
8
5
  const menuPathSegmentRegexSource = "[a-z][a-z0-9]*(?:-[a-z0-9]+)*";
9
6
  const fullMenuPathRegex = new RegExp(`^/(?:core(?:/${menuPathSegmentRegexSource})*|(?:${menuPathSegmentRegexSource}(?:/${menuPathSegmentRegexSource})*)?)$`);
10
7
  const childMenuPathRegex = new RegExp(`^/${menuPathSegmentRegexSource}$`);
8
+ const menuKeys = new Set(["path", "name", "sort", "children"]);
11
9
 
12
- const childMenuSchema = z
13
- .object({
14
- path: z.string().regex(childMenuPathRegex),
15
- name: z.string().regex(/^\S(?:.*\S)?$/),
16
- sort: z.number(),
17
- children: z.array(z.any()).max(0).optional()
18
- })
19
- .strict();
20
-
21
- const menuSchema = z
22
- .object({
23
- path: z.string().regex(fullMenuPathRegex),
24
- name: z.string().regex(/^\S(?:.*\S)?$/),
25
- sort: z.number(),
26
- children: z.array(childMenuSchema).optional()
27
- })
28
- .strict();
10
+ function checkMenuItem(issues, menu, path, child = false) {
11
+ if (!isPlainObject(menu)) {
12
+ issues.push({ path: path, message: "必须是对象格式" });
13
+ return;
14
+ }
15
+ for (const key of Object.keys(menu)) {
16
+ if (!menuKeys.has(key)) issues.push({ path: [...path, key], message: "不允许出现" });
17
+ }
18
+ const pathRegex = child ? childMenuPathRegex : fullMenuPathRegex;
19
+ if (!isString(menu.path) || !pathRegex.test(menu.path)) issues.push({ path: [...path, "path"], message: "路径格式无效" });
20
+ if (!isString(menu.name) || menu.name.trim() !== menu.name || menu.name.length === 0) issues.push({ path: [...path, "name"], message: "必须是非空且无首尾空格的字符串" });
21
+ if (!isFiniteNumber(menu.sort)) issues.push({ path: [...path, "sort"], message: "必须是有限数字" });
22
+
23
+ if (child) {
24
+ if (menu.children !== undefined && (!Array.isArray(menu.children) || menu.children.length > 0)) issues.push({ path: [...path, "children"], message: "只能为空数组" });
25
+ return;
26
+ }
29
27
 
30
- const menuListSchema = z.array(menuSchema).superRefine((menuList, refineCtx) => {
31
- for (let menuIndex = 0; menuIndex < menuList.length; menuIndex++) {
32
- const menu = menuList[menuIndex];
33
- const children = Array.isArray(menu.children) ? menu.children : [];
34
- for (let childIndex = 0; childIndex < children.length; childIndex++) {
35
- const child = children[childIndex];
36
- const childFullPath = `${menu.path}${child.path}`;
37
- if (!fullMenuPathRegex.test(childFullPath)) {
38
- refineCtx.addIssue({
39
- code: "custom",
40
- message: "菜单完整路径必须是 /core/xxx 或无前缀路径",
41
- path: [menuIndex, "children", childIndex, "path"]
42
- });
43
- }
28
+ if (menu.children !== undefined && !Array.isArray(menu.children)) {
29
+ issues.push({ path: [...path, "children"], message: "必须是数组" });
30
+ return;
31
+ }
32
+ for (const [index, item] of (menu.children || []).entries()) {
33
+ checkMenuItem(issues, item, [...path, "children", index], true);
34
+ if (isPlainObject(item) && isString(menu.path) && isString(item.path) && !fullMenuPathRegex.test(`${menu.path}${item.path}`)) {
35
+ issues.push({ path: [...path, "children", index, "path"], message: "菜单完整路径必须是 /core/xxx 或无前缀路径" });
44
36
  }
45
37
  }
46
- });
38
+ }
47
39
 
48
- export const checkMenu = async (menus) => {
49
- const result = menuListSchema.safeParse(menus);
50
- if (result.success) {
51
- return false;
52
- }
40
+ export async function checkMenu(menus) {
41
+ const issues = [];
42
+ if (!Array.isArray(menus)) issues.push({ path: [], message: "必须是数组" });
43
+ else menus.forEach((menu, index) => checkMenuItem(issues, menu, [index]));
53
44
 
54
- Logger.warn(
55
- "菜单校验失败",
56
- {
57
- errors: formatZodIssues(result.error.issues, { items: menus, itemLabel: "menus" })
58
- },
59
- false
60
- );
45
+ if (issues.length === 0) return false;
46
+ Logger.warn("菜单校验失败", { errors: formatValidationIssues(issues, { items: menus, itemLabel: "menus" }) }, false);
61
47
  return true;
62
- };
48
+ }
package/checks/plugin.js CHANGED
@@ -1,42 +1,39 @@
1
- import * as z from "zod";
2
-
3
1
  import { Logger } from "../lib/logger.js";
4
- import { formatZodIssues } from "../utils/formatZodIssues.js";
5
- import { isNoTrimStringAllowEmpty } from "../utils/is.js";
2
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
+ import { isFunction, isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
6
4
 
7
- z.config(z.locales.zhCN());
5
+ const pluginKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
8
6
 
9
- const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
7
+ function checkPluginItem(plugin) {
8
+ const issues = [];
9
+ if (!isPlainObject(plugin)) {
10
+ issues.push({ path: [], message: "必须是对象格式" });
11
+ return issues;
12
+ }
10
13
 
11
- const pluginSchema = z
12
- .object({
13
- source: noTrimString.min(1),
14
- type: noTrimString.min(1),
15
- filePath: noTrimString.min(1),
16
- relativePath: noTrimString.min(1),
17
- fileName: noTrimString.min(1),
18
- apiPath: noTrimString.min(1),
19
- handler: z.custom((value) => typeof value === "function"),
20
- order: z.number()
21
- })
22
- .strict();
14
+ for (const key of Object.keys(plugin)) {
15
+ if (!pluginKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
16
+ }
17
+ for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
18
+ if (!isString(plugin[key]) || plugin[key].length === 0 || plugin[key].trim() !== plugin[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
19
+ }
20
+ if (!isFunction(plugin.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
21
+ if (!isFiniteNumber(plugin.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
22
+ return issues;
23
+ }
23
24
 
24
25
  export async function checkPlugin(plugins) {
25
- let hasError = false;
26
+ if (!Array.isArray(plugins)) {
27
+ Logger.warn("插件校验失败", { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: plugins, itemLabel: "plugin" }) }, false);
28
+ return true;
29
+ }
26
30
 
31
+ let hasError = false;
27
32
  for (const plugin of plugins) {
28
- const result = pluginSchema.safeParse(plugin);
29
- if (!result.success) {
30
- Logger.warn(
31
- "插件校验失败",
32
- {
33
- errors: formatZodIssues(result.error.issues, { item: plugin, itemLabel: "plugin" })
34
- },
35
- false
36
- );
37
- hasError = true;
38
- }
33
+ const issues = checkPluginItem(plugin);
34
+ if (issues.length === 0) continue;
35
+ Logger.warn("插件校验失败", { errors: formatValidationIssues(issues, { item: plugin, itemLabel: "plugin" }) }, false);
36
+ hasError = true;
39
37
  }
40
-
41
38
  return hasError;
42
39
  }