befly 3.18.3 → 3.18.4

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.
@@ -1,7 +1,7 @@
1
1
  import { UAParser } from "ua-parser-js";
2
2
 
3
3
  import adminTable from "../../tables/admin.json";
4
- import { toSessionTtlSeconds } from "../../utils/toSessionTtlSeconds.js";
4
+ import { toSessionTtlSeconds } from "../../utils/util.js";
5
5
 
6
6
  export default {
7
7
  name: "管理员登录",
package/checks/hook.js CHANGED
@@ -17,7 +17,7 @@ const hookSchema = z
17
17
  fileName: noTrimString.min(1),
18
18
  apiPath: noTrimString.min(1),
19
19
  handler: z.custom((value) => typeof value === "function"),
20
- deps: z.array(noTrimString)
20
+ order: z.number()
21
21
  })
22
22
  .strict();
23
23
 
package/checks/plugin.js CHANGED
@@ -17,7 +17,7 @@ const pluginSchema = z
17
17
  fileName: noTrimString.min(1),
18
18
  apiPath: noTrimString.min(1),
19
19
  handler: z.custom((value) => typeof value === "function"),
20
- deps: z.array(noTrimString)
20
+ order: z.number()
21
21
  })
22
22
  .strict();
23
23
 
package/hooks/auth.js CHANGED
@@ -1,7 +1,7 @@
1
- import { toSessionTtlSeconds } from "../utils/toSessionTtlSeconds.js";
1
+ import { toSessionTtlSeconds } from "../utils/util.js";
2
2
 
3
3
  export default {
4
- deps: [],
4
+ order: 1,
5
5
  handler: async (befly, ctx) => {
6
6
  const authHeader = ctx.headers.get("authorization");
7
7
  const ttlSeconds = toSessionTtlSeconds(befly.config?.session?.expireDays);
package/hooks/parser.js CHANGED
@@ -13,7 +13,7 @@ const xmlParser = new XMLParser();
13
13
  * - body: "raw" 时跳过解析,由 handler 自行处理原始请求
14
14
  */
15
15
  export default {
16
- deps: ["auth"],
16
+ order: 2,
17
17
  handler: async (befly, ctx) => {
18
18
  // apiBody=raw 模式:跳过解析,保留原始请求供 handler 自行处理
19
19
  // 适用于:微信回调、支付回调、webhook 等需要手动解密/验签的场景
@@ -12,7 +12,7 @@ import { isValidPositiveInt } from "../utils/is.js";
12
12
  * - 其他角色:检查 Redis 中的角色权限集合
13
13
  */
14
14
  export default {
15
- deps: ["validator"],
15
+ order: 4,
16
16
  handler: async (befly, ctx) => {
17
17
  // 1. 接口无需权限
18
18
  if (ctx.apiAuth === false) {
@@ -9,7 +9,7 @@ import { snakeCase } from "../utils/util.js";
9
9
  * 根据 API 定义的 fields 和 required 验证请求参数
10
10
  */
11
11
  export default {
12
- deps: ["parser"],
12
+ order: 3,
13
13
  handler: async (befly, ctx) => {
14
14
  // 仅保留 fields 中声明的字段,并支持 snake_case 入参回退(例如 agent_id -> agentId)
15
15
  const rawBody = isPlainObject(ctx.body) ? ctx.body : {};
package/index.js CHANGED
@@ -32,7 +32,6 @@ import { calcPerfTime } from "./utils/calcPerfTime.js";
32
32
  import { scanSources } from "./utils/scanSources.js";
33
33
  import { isPrimaryProcess } from "./utils/is.js";
34
34
  import { deepMerge } from "./utils/deepMerge.js";
35
- import { sortModules } from "./utils/sortModules.js";
36
35
 
37
36
  function prefixMenuPaths(menus, prefix) {
38
37
  const output = [];
@@ -95,164 +94,113 @@ async function ensureSyncPrerequisites(ctx) {
95
94
  }
96
95
  }
97
96
 
98
- /**
99
- * Befly 框架核心类
100
- * 职责:管理应用上下文和生命周期
101
- */
102
- export class Befly {
103
- /** 应用上下文 */
104
- context = {
105
- env: {},
106
- config: {}
107
- };
108
-
109
- // 菜单配置
110
- menus = [];
111
-
112
- /** 插件列表 */
113
- plugins = [];
114
-
115
- /** 钩子列表 */
116
- hooks = [];
117
-
118
- /** API 路由映射表 */
119
- apis = {};
97
+ export async function dbCheck(mysqlConfig) {
98
+ return syncDbCheck(mysqlConfig);
99
+ }
120
100
 
121
- /** create 阶段扫描得到的 API 列表 */
122
- scanApis = [];
101
+ export async function dbApply(mysqlConfig) {
102
+ return syncDbApply(mysqlConfig);
103
+ }
123
104
 
124
- /** create 阶段扫描得到的插件列表 */
125
- scanPlugins = [];
105
+ export async function createBefly(env = {}, config = {}, menus = []) {
106
+ const mergedConfig = deepMerge(beflyConfig, config);
107
+ const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
126
108
 
127
- /** create 阶段扫描得到的钩子列表 */
128
- scanHooks = [];
109
+ const configHasError = await checkConfig(mergedConfig);
110
+ const { apis, tables, plugins, hooks } = await scanSources();
129
111
 
130
- /** 是否由工厂创建 */
131
- createdByFactory = false;
112
+ const apiHasError = await checkApi(apis);
113
+ const tableHasError = await checkTable(tables);
114
+ const pluginHasError = await checkPlugin(plugins);
115
+ const hookHasError = await checkHook(hooks);
116
+ const menuHasError = await checkMenu(mergedMenus);
132
117
 
133
- /**
134
- * A 方案:异步工厂,先做预检,失败则不返回实例。
135
- */
136
- static async create(env = {}, config = {}, menus = []) {
137
- const instance = new Befly();
138
- instance.context.env = env;
139
- instance.context.config = deepMerge(beflyConfig, config);
140
- instance.menus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
141
- instance.createdByFactory = true;
142
-
143
- const configHasError = await checkConfig(instance.context.config);
144
- const { apis, tables, plugins, hooks } = await scanSources();
145
-
146
- const apiHasError = await checkApi(apis);
147
- const tableHasError = await checkTable(tables);
148
- const pluginHasError = await checkPlugin(plugins);
149
- const hookHasError = await checkHook(hooks);
150
- const menuHasError = await checkMenu(instance.menus);
151
-
152
- if (configHasError || apiHasError || tableHasError || pluginHasError || hookHasError || menuHasError) {
153
- throw new Error("检查失败:存在配置/结构问题", {
154
- cause: null,
155
- code: "policy",
156
- subsystem: "checks",
157
- operation: "checkAll"
158
- });
159
- }
118
+ if (configHasError || apiHasError || tableHasError || pluginHasError || hookHasError || menuHasError) {
119
+ throw new Error("检查失败:存在配置/结构问题", {
120
+ cause: null,
121
+ code: "policy",
122
+ subsystem: "checks",
123
+ operation: "checkAll"
124
+ });
125
+ }
160
126
 
161
- instance.scanApis = apis;
162
- instance.scanPlugins = plugins;
163
- instance.scanHooks = hooks;
127
+ return new Befly({
128
+ env: env,
129
+ config: mergedConfig,
130
+ menus: mergedMenus,
131
+ apis: apis,
132
+ hooks: hooks,
133
+ plugins: plugins,
134
+ createdByFactory: true
135
+ });
136
+ }
164
137
 
165
- return instance;
138
+ /**
139
+ * Befly 框架核心类
140
+ * 职责:管理应用上下文和生命周期
141
+ */
142
+ export class Befly {
143
+ constructor(init = {}) {
144
+ this.context = {
145
+ env: init.env || {},
146
+ config: init.config || {}
147
+ };
148
+ this.menus = Array.isArray(init.menus) ? init.menus : [];
149
+ this.hooks = Array.isArray(init.hooks) ? init.hooks : [];
150
+ this.apis = Array.isArray(init.apis) ? init.apis : [];
151
+ this.plugins = Array.isArray(init.plugins) ? init.plugins : [];
152
+ this.createdByFactory = init.createdByFactory === true;
166
153
  }
167
154
 
168
- ensureCreatedByFactory(methodName) {
155
+ ensureCreatedByFactory() {
169
156
  if (this.createdByFactory) {
170
157
  return;
171
158
  }
172
159
 
173
- throw new Error(`请使用 Befly.create(...) 创建实例后再调用 ${methodName}`, {
160
+ throw new Error("请使用 createBefly(...) 创建实例后再调用 start", {
174
161
  cause: null,
175
162
  code: "policy",
176
- subsystem: methodName === "start" ? "start" : "syncDb",
163
+ subsystem: "start",
177
164
  operation: "factoryGuard"
178
165
  });
179
166
  }
180
167
 
181
- async dbCheck() {
182
- this.ensureCreatedByFactory("dbCheck");
183
-
184
- return syncDbCheck(this.context.config.mysql);
185
- }
186
-
187
- async dbApply() {
188
- this.ensureCreatedByFactory("dbApply");
189
-
190
- return syncDbApply(this.context.config.mysql);
191
- }
192
-
193
168
  /**
194
169
  * 启动完整的生命周期流程
195
170
  * @returns HTTP 服务器实例
196
171
  */
197
172
  async start() {
198
173
  try {
199
- this.ensureCreatedByFactory("start");
174
+ this.ensureCreatedByFactory();
200
175
 
201
176
  const serverStartTime = Bun.nanoseconds();
202
- if (!Array.isArray(this.scanApis) || !Array.isArray(this.scanPlugins) || !Array.isArray(this.scanHooks)) {
203
- throw new Error("请使用 Befly.create(...) 完成预检后再调用 start", {
204
- cause: null,
205
- code: "policy",
206
- subsystem: "start",
207
- operation: "preflightGuard"
208
- });
209
- }
210
177
 
211
- // 1. 启动期建立基础连接(SQL + Redis)
178
+ // 启动期建立基础连接(SQL + Redis)
212
179
  // 说明:连接职责收敛到启动期单点;插件只消费已连接实例(Connect.getSql/getRedis)。
213
180
  await Connect.connectRedis(this.context.config.redis);
214
181
  await Connect.connectMysql(this.context.config.mysql);
215
182
 
216
- // 2. 加载插件
217
- const sortedPlugins = sortModules(this.scanPlugins);
218
- if (sortedPlugins) {
219
- for (const item of sortedPlugins) {
220
- const pluginInstance = await item.handler(this.context);
221
- this.context[item.fileName] = pluginInstance;
222
- }
223
- } else {
224
- throw new Error("插件依赖关系错误", {
225
- cause: null,
226
- code: "policy"
227
- });
183
+ // 加载插件
184
+ for (const item of this.plugins.sort((a, b) => a.order - b.order)) {
185
+ this.context[item.fileName] = await item.handler(this.context);
228
186
  }
229
187
  await ensureSyncPrerequisites(this.context);
230
188
 
231
- // 5. 自动同步(PM2 cluster:主进程执行,其它进程等待同步完成)
189
+ // 自动同步(PM2 cluster:主进程执行,其它进程等待同步完成)
232
190
  if (isPrimaryProcess(this.context.env)) {
233
- await syncApi(this.context, this.scanApis);
191
+ await syncApi(this.context, this.apis);
234
192
  await syncMenu(this.context, this.menus);
235
193
  await syncDev(this.context);
236
194
  await syncCache(this.context);
237
195
  }
238
196
 
239
- // 3. 加载钩子
240
- const sortedHooks = sortModules(this.scanHooks);
241
- if (sortedHooks) {
242
- this.hooks = sortedHooks;
243
- } else {
244
- throw new Error("钩子依赖关系错误", {
245
- cause: null,
246
- code: "policy"
247
- });
248
- }
197
+ // 加载钩子
198
+ this.hooks = this.hooks.sort((a, b) => a.order - b.order);
249
199
 
250
- // 4. 加载所有 API
251
- for (const api of this.scanApis) {
252
- this.apis[api.apiPath] = api;
253
- }
200
+ // 加载所有 API
201
+ this.apis = Object.fromEntries(this.apis.map((api) => [api.apiPath, api]));
254
202
 
255
- // 6. 启动 HTTP服务器
203
+ // 启动 HTTP服务器
256
204
  const apiFetch = apiHandler(this.apis, this.hooks, this.context);
257
205
  const staticFetch = staticHandler(this.context.config.cors);
258
206
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.18.3",
4
- "gitHead": "0f432ea71a42ecbd584ca6baa7e57a00b902891e",
3
+ "version": "3.18.4",
4
+ "gitHead": "403482610311798cc566fb6b550c069740fa413c",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
7
7
  "keywords": [
package/plugins/cache.js CHANGED
@@ -9,7 +9,7 @@ import { CacheHelper } from "../lib/cacheHelper.js";
9
9
  * 缓存插件
10
10
  */
11
11
  export default {
12
- deps: ["logger", "redis", "mysql"],
12
+ order: 6,
13
13
  async handler(befly) {
14
14
  return new CacheHelper({ mysql: befly.mysql, redis: befly.redis });
15
15
  }
package/plugins/config.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  export default {
7
- deps: [],
7
+ order: 1,
8
8
  handler: (befly) => {
9
9
  return befly.config;
10
10
  }
package/plugins/email.js CHANGED
@@ -16,7 +16,7 @@ const defaultConfig = {
16
16
  };
17
17
 
18
18
  export default {
19
- deps: ["mysql", "logger", "config"],
19
+ order: 7,
20
20
  async handler(befly) {
21
21
  // 从 befly.config.email 获取配置
22
22
  const emailConfigRaw = (befly.config ? befly.config["email"] : undefined) || {};
package/plugins/logger.js CHANGED
@@ -9,7 +9,7 @@ import { Logger } from "../lib/logger.js";
9
9
  * 日志插件
10
10
  */
11
11
  export default {
12
- deps: [],
12
+ order: 2,
13
13
  async handler(befly) {
14
14
  // 配置 Logger
15
15
  if (befly.config && befly.config.logger) {
package/plugins/mysql.js CHANGED
@@ -11,7 +11,7 @@ import { Logger } from "../lib/logger.js";
11
11
  * 数据库插件
12
12
  */
13
13
  export default {
14
- deps: ["logger", "redis"],
14
+ order: 5,
15
15
  async handler(befly) {
16
16
  try {
17
17
  // 创建数据库管理器实例
package/plugins/redis.js CHANGED
@@ -11,7 +11,7 @@ import { RedisHelper } from "../lib/redisHelper.js";
11
11
  * Redis 插件
12
12
  */
13
13
  export default {
14
- deps: ["logger"],
14
+ order: 4,
15
15
  async handler(befly) {
16
16
  const env = befly.config?.runMode;
17
17
  const redisPrefix = befly.config?.redis?.prefix;
package/plugins/tool.js CHANGED
@@ -142,7 +142,7 @@ export function sha256(value) {
142
142
  }
143
143
 
144
144
  export default {
145
- deps: [],
145
+ order: 3,
146
146
  handler: () => {
147
147
  return {
148
148
  Yes: Yes,
@@ -13,7 +13,7 @@ import { isPlainObject } from "./is.js";
13
13
  // fileName: "auth",
14
14
  // apiPath: "/api/core/auth",
15
15
  // name: "auth",
16
- // deps: [ "cors" ],
16
+ // order: 1,
17
17
  // handler: [AsyncFunction: handler],
18
18
  // }
19
19
  // 🔥[ plugins ]-112 {
@@ -24,7 +24,7 @@ import { isPlainObject } from "./is.js";
24
24
  // fileName: "cache",
25
25
  // apiPath: "/api/core/cache",
26
26
  // name: "cache",
27
- // deps: [ "logger", "redis", "mysql" ],
27
+ // order: 1,
28
28
  // handler: [AsyncFunction: handler],
29
29
  // }
30
30
  // 🔥[ tables ]-112 {
package/utils/util.js CHANGED
@@ -357,6 +357,24 @@ export function keyBy(items, getKey) {
357
357
  return out;
358
358
  }
359
359
 
360
+ /**
361
+ * 将 session 有效天数转换为秒。
362
+ */
363
+ export function toSessionTtlSeconds(ttlDays) {
364
+ if (typeof ttlDays === "number" && Number.isFinite(ttlDays) && ttlDays > 0) {
365
+ return Math.floor(ttlDays * 24 * 60 * 60);
366
+ }
367
+
368
+ if (typeof ttlDays === "string") {
369
+ const parsed = Number(ttlDays.trim());
370
+ if (Number.isFinite(parsed) && parsed > 0) {
371
+ return Math.floor(parsed * 24 * 60 * 60);
372
+ }
373
+ }
374
+
375
+ return 7 * 24 * 60 * 60;
376
+ }
377
+
360
378
  /**
361
379
  * 生成短 ID
362
380
  * 由时间戳(base36)+ 随机字符组成,约 13 位
@@ -1,75 +0,0 @@
1
- import { Logger } from "../lib/logger.js";
2
-
3
- /**
4
- * 按 deps 拓扑排序 scanSources 扫描得到的插件/钩子。
5
- *
6
- * 说明:
7
- * - 输入为 scanSources/scanFiles 的条目数组:每个条目包含 fileName 与 deps。
8
- * - deps 里的字符串会与 fileName 匹配。
9
- * - 若出现:重复 name、缺失依赖、循环依赖,则返回 false。
10
- */
11
- export function sortModules(items) {
12
- const result = [];
13
- const visited = new Set();
14
- const visiting = new Set();
15
-
16
- const nameToItem = {};
17
- let isPass = true;
18
-
19
- // 1) 建表 + 重名检查
20
- for (const item of items) {
21
- if (nameToItem[item.fileName]) {
22
- Logger.error("模块 名称重复,无法根据 deps 唯一定位", null, {
23
- name: item.fileName,
24
- first: nameToItem[item.fileName],
25
- second: item
26
- });
27
- isPass = false;
28
- continue;
29
- }
30
-
31
- nameToItem[item.fileName] = item;
32
- }
33
-
34
- if (!isPass) return false;
35
-
36
- // 2) 依赖存在性检查
37
- for (const item of items) {
38
- for (const dep of item.deps) {
39
- if (!nameToItem[dep]) {
40
- Logger.error("模块 依赖未找到", null, { module: item.fileName, dependency: dep });
41
- isPass = false;
42
- }
43
- }
44
- }
45
-
46
- if (!isPass) return false;
47
-
48
- // 3) 拓扑排序(DFS)
49
- const visit = (name) => {
50
- if (visited.has(name)) return;
51
- if (visiting.has(name)) {
52
- Logger.error("模块 循环依赖", null, { module: name });
53
- isPass = false;
54
- return;
55
- }
56
-
57
- const item = nameToItem[name];
58
- if (!item) return;
59
-
60
- visiting.add(name);
61
- for (const dep of item.deps) {
62
- visit(dep);
63
- }
64
- visiting.delete(name);
65
-
66
- visited.add(name);
67
- result.push(item);
68
- };
69
-
70
- for (const item of items) {
71
- visit(item.fileName);
72
- }
73
-
74
- return isPass ? result : false;
75
- }
@@ -1,14 +0,0 @@
1
- export function toSessionTtlSeconds(ttlDays) {
2
- if (typeof ttlDays === "number" && Number.isFinite(ttlDays) && ttlDays > 0) {
3
- return Math.floor(ttlDays * 24 * 60 * 60);
4
- }
5
-
6
- if (typeof ttlDays === "string") {
7
- const parsed = Number(ttlDays.trim());
8
- if (Number.isFinite(parsed) && parsed > 0) {
9
- return Math.floor(parsed * 24 * 60 * 60);
10
- }
11
- }
12
-
13
- return 7 * 24 * 60 * 60;
14
- }