befly 3.13.9 → 3.14.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.
@@ -14,28 +14,28 @@ export async function checkApi(apis) {
14
14
  hasError = true;
15
15
  continue;
16
16
  }
17
- // routePath / routePrefix 由 scanFiles 系统生成:必须是严格的 pathname
18
- if (typeof api?.routePath !== "string" || api.routePath.trim() === "") {
19
- Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 属性必须是非空字符串(由系统生成)" }));
17
+ // path / routePrefix 由 scanFiles 系统生成:必须是严格的 pathname
18
+ if (typeof api?.path !== "string" || api.path.trim() === "") {
19
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 path 属性必须是非空字符串(由系统生成)" }));
20
20
  hasError = true;
21
21
  }
22
22
  else {
23
- const routePath = api.routePath.trim();
23
+ const path = api.path.trim();
24
24
  // 不允许出现 "POST/api/..." 等 method 前缀
25
- if (/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i.test(routePath)) {
26
- Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)" }));
25
+ if (/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i.test(path)) {
26
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 path 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)" }));
27
27
  hasError = true;
28
28
  }
29
- if (!routePath.startsWith("/api/")) {
30
- Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 必须以 /api/ 开头" }));
29
+ if (!path.startsWith("/api/")) {
30
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 path 必须以 /api/ 开头" }));
31
31
  hasError = true;
32
32
  }
33
- if (routePath.includes(" ")) {
34
- Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许包含空格" }));
33
+ if (path.includes(" ")) {
34
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 path 不允许包含空格" }));
35
35
  hasError = true;
36
36
  }
37
- if (routePath.includes("/api//")) {
38
- Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许出现 /api//(重复斜杠)" }));
37
+ if (path.includes("/api//")) {
38
+ Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 path 不允许出现 /api//(重复斜杠)" }));
39
39
  hasError = true;
40
40
  }
41
41
  }
package/dist/index.js CHANGED
@@ -48,16 +48,15 @@ export class Befly {
48
48
  * 启动完整的生命周期流程
49
49
  * @returns HTTP 服务器实例
50
50
  */
51
- async start(env) {
51
+ async start(env = {}) {
52
52
  try {
53
53
  const serverStartTime = Bun.nanoseconds();
54
- const runtimeEnv = env || {};
55
54
  // 0. 加载配置
56
- this.config = await loadBeflyConfig(runtimeEnv.NODE_ENV);
55
+ this.config = await loadBeflyConfig(env.NODE_ENV || "development");
57
56
  // 将配置注入到 ctx,供插件/Hook/sync 等按需读取
58
57
  this.context.config = this.config;
59
58
  // 给插件/Hook/sync 一个统一读取 env 的入口(只从 start 入参注入)
60
- this.context.env = runtimeEnv;
59
+ this.context.env = env;
61
60
  const { apis, tables, plugins, hooks, addons } = await scanSources();
62
61
  // 让后续 syncMenu 能拿到 addon 的 views 路径等信息
63
62
  this.context.addons = addons;
@@ -131,7 +130,7 @@ export class Befly {
131
130
  }
132
131
  });
133
132
  const finalStartupTime = calcPerfTime(serverStartTime);
134
- const processRole = getProcessRole(runtimeEnv);
133
+ const processRole = getProcessRole(env);
135
134
  const roleLabel = processRole.role === "primary" ? "主进程" : `工作进程 #${processRole.instanceId}`;
136
135
  const envLabel = processRole.env === "standalone" ? "" : ` [${processRole.env}]`;
137
136
  Logger.info(`${this.config.appName} 启动成功! (${roleLabel}${envLabel})`);
@@ -12,17 +12,16 @@ import { processAtSymbol } from "../utils/processAtSymbol";
12
12
  export async function loadApis(apis) {
13
13
  const apisMap = new Map();
14
14
  for (const api of apis) {
15
- const apiType = api.type;
16
15
  // 兼容:scanFiles 的结果或测试构造数据可能缺少 type 字段;缺少时默认按 API 处理。
17
16
  // 仅在 type 显式存在且不等于 "api" 时跳过,避免错误过滤。
18
- if (apiType && apiType !== "api") {
17
+ if (api.type !== "api") {
19
18
  continue;
20
19
  }
21
20
  try {
22
21
  const apiRoute = api;
23
22
  // 处理字段定义,将 @ 引用替换为实际字段定义
24
- apiRoute.fields = processAtSymbol(apiRoute.fields || {}, apiRoute.name, apiRoute.routePath);
25
- apisMap.set(apiRoute.routePath, apiRoute);
23
+ apiRoute.fields = processAtSymbol(apiRoute.fields || {}, apiRoute.name, apiRoute.path);
24
+ apisMap.set(apiRoute.path, apiRoute);
26
25
  }
27
26
  catch (error) {
28
27
  Logger.error({ err: error, api: api.relativePath, file: api.filePath, msg: "接口加载失败" });
@@ -21,7 +21,7 @@ export function apiHandler(apis, hooks, context) {
21
21
  // 2. 创建请求上下文
22
22
  const url = new URL(req.url);
23
23
  // 只用接口路径做存在性判断与匹配:不要把 method 拼进 key
24
- // 说明:apisMap 的 key 来源于 scanFiles/loadApis 生成的 routePath(例如 /api/core/xxx)
24
+ // 说明:apisMap 的 key 来源于 scanFiles/loadApis 生成的 path(例如 /api/core/xxx)
25
25
  const apiPath = url.pathname || "/";
26
26
  const clientIp = getClientIp(req, server);
27
27
  const now = Date.now();
@@ -1,5 +1,20 @@
1
1
  import { Logger } from "../lib/logger";
2
2
  import { keyBy } from "../utils/util";
3
+ const getApiParentPath = (apiPath) => {
4
+ const segments = apiPath
5
+ .split("/")
6
+ .map((s) => s.trim())
7
+ .filter((s) => s.length > 0);
8
+ // segments 示例:
9
+ // - /api/addon/admin/sysConfig/list -> ["api","addon","admin","sysConfig","list"]
10
+ // - /api/app/test/hi -> ["api","app","test","hi"]
11
+ const seg2 = segments[1] || "";
12
+ const take = seg2 === "addon" ? 4 : 3;
13
+ const parentSegments = segments.slice(0, Math.min(take, segments.length));
14
+ if (parentSegments.length === 0)
15
+ return "";
16
+ return `/${parentSegments.join("/")}`;
17
+ };
3
18
  export async function syncApi(ctx, apis) {
4
19
  const tableName = "addon_admin_api";
5
20
  if (!ctx.db) {
@@ -14,61 +29,68 @@ export async function syncApi(ctx, apis) {
14
29
  }
15
30
  const allDbApis = await ctx.db.getAll({
16
31
  table: tableName,
17
- fields: ["id", "routePath", "name", "addonName", "auth", "state"],
32
+ fields: ["id", "path", "parentPath", "name", "addonName", "auth", "state"],
18
33
  where: { state$gte: 0 }
19
34
  });
20
35
  const dbLists = allDbApis.data.lists || [];
21
- const allDbApiMap = keyBy(dbLists, (item) => item.routePath);
36
+ const allDbApiMap = keyBy(dbLists, (item) => item.path);
22
37
  const insData = [];
23
38
  const updData = [];
24
39
  const delData = [];
25
- // 1) 先构建当前扫描到的 routePath 集合(用于删除差集)
40
+ // 1) 先构建当前扫描到的 path 集合(用于删除差集)
26
41
  const apiRouteKeys = new Set();
27
- // 2) 插入 / 更新(存在不一定更新:仅当 name/routePath/addonName/auth 任一不匹配时更新)
42
+ // 2) 插入 / 更新(存在不一定更新:仅当 name/path/addonName/auth 任一不匹配时更新)
28
43
  for (const api of apis) {
29
- const apiType = api.type;
30
- // 兼容:历史/测试构造的数据可能没有 type 字段;此时应按 API 处理。
31
- // 因此仅当 type **显式存在** 且不为 "api" 时才跳过,避免误把真实 API 条目过滤掉。
32
- if (apiType && apiType !== "api") {
44
+ // 仅当 type **显式存在** 且不为 "api" 时才跳过,避免误把真实 API 条目过滤掉。
45
+ if (api.type !== "api") {
33
46
  continue;
34
47
  }
35
- const normalizedAuth = api.auth === 0 || api.auth === false ? 0 : 1;
36
- const normalizedApi = {
37
- name: api.name,
38
- routePath: api.routePath,
39
- addonName: api.addonName,
40
- auth: normalizedAuth
41
- };
42
- const routePath = normalizedApi.routePath;
43
- apiRouteKeys.add(routePath);
44
- const item = allDbApiMap[routePath];
48
+ // auth:运行时 API 定义使用 boolean;DB 字段使用 0/1
49
+ // 统一在 syncApi 写库前做归一化,避免类型不一致导致每次启动都触发更新。
50
+ const auth = api.auth === false || api.auth === 0 ? 0 : 1;
51
+ const parentPath = getApiParentPath(api.path);
52
+ apiRouteKeys.add(api.path);
53
+ const item = allDbApiMap[api.path];
45
54
  if (item) {
46
- const dbAuth = item.auth === 0 || item.auth === false ? 0 : 1;
47
- const shouldUpdate = normalizedApi.name !== item.name || normalizedApi.routePath !== item.routePath || normalizedApi.addonName !== item.addonName || normalizedAuth !== dbAuth;
55
+ const shouldUpdate = api.name !== item.name || api.path !== item.path || api.addonName !== item.addonName || parentPath !== item.parentPath || auth !== item.auth;
48
56
  if (shouldUpdate) {
49
- updData.push({ id: item.id, api: normalizedApi });
57
+ updData.push({
58
+ id: item.id,
59
+ name: api.name,
60
+ path: api.path,
61
+ parentPath: parentPath,
62
+ addonName: api.addonName,
63
+ auth: auth
64
+ });
50
65
  }
51
66
  }
52
67
  else {
53
- insData.push(normalizedApi);
68
+ insData.push({
69
+ name: api.name,
70
+ path: api.path,
71
+ parentPath: parentPath,
72
+ addonName: api.addonName,
73
+ auth: auth
74
+ });
54
75
  }
55
76
  }
56
77
  // 3) 删除:用差集(DB - 当前扫描)得到要删除的 id
57
78
  for (const record of dbLists) {
58
- if (!apiRouteKeys.has(record.routePath)) {
79
+ if (!apiRouteKeys.has(record.path)) {
59
80
  delData.push(record.id);
60
81
  }
61
82
  }
62
83
  if (updData.length > 0) {
63
84
  try {
64
- await ctx.db.updBatch(tableName, updData.map((item) => {
85
+ await ctx.db.updBatch(tableName, updData.map((api) => {
65
86
  return {
66
- id: item.id,
87
+ id: api.id,
67
88
  data: {
68
- name: item.api.name,
69
- routePath: item.api.routePath,
70
- addonName: item.api.addonName,
71
- auth: item.api.auth === 0 || item.api.auth === false ? 0 : 1
89
+ name: api.name,
90
+ path: api.path,
91
+ parentPath: api.parentPath,
92
+ addonName: api.addonName,
93
+ auth: api.auth
72
94
  }
73
95
  };
74
96
  }));
@@ -82,9 +104,10 @@ export async function syncApi(ctx, apis) {
82
104
  await ctx.db.insBatch(tableName, insData.map((api) => {
83
105
  return {
84
106
  name: api.name,
85
- routePath: api.routePath,
107
+ path: api.path,
108
+ parentPath: api.parentPath,
86
109
  addonName: api.addonName,
87
- auth: api.auth === 0 || api.auth === false ? 0 : 1
110
+ auth: api.auth
88
111
  };
89
112
  }));
90
113
  }
@@ -4,7 +4,6 @@ export async function syncDev(ctx, config = {}) {
4
4
  if (!config.devPassword) {
5
5
  return;
6
6
  }
7
- const devEmail = typeof config.devEmail === "string" && config.devEmail.length > 0 ? config.devEmail : "dev@qq.com";
8
7
  if (!ctx.db) {
9
8
  throw new Error("syncDev: ctx.db 未初始化(Db 插件未加载或注入失败)");
10
9
  }
@@ -23,140 +22,132 @@ export async function syncDev(ctx, config = {}) {
23
22
  Logger.debug(`addon_admin_menu 表不存在`);
24
23
  return;
25
24
  }
25
+ if (!(await ctx.db.tableExists("addon_admin_api")).data) {
26
+ Logger.debug(`addon_admin_api 表不存在`);
27
+ return;
28
+ }
26
29
  const allMenus = await ctx.db.getAll({
27
30
  table: "addon_admin_menu",
28
31
  fields: ["path"],
29
32
  where: { state$gte: 0 },
30
33
  orderBy: ["id#ASC"]
31
34
  });
32
- const menuPaths = Array.from(new Set((allMenus.data.lists || []).map((m) => (typeof m?.path === "string" ? m.path.trim() : "")).filter((p) => p.length > 0)));
33
- const existApi = await ctx.db.tableExists("addon_admin_api");
34
- let apiPaths = [];
35
- if (existApi.data) {
36
- const allApis = await ctx.db.getAll({
37
- table: "addon_admin_api",
38
- fields: ["routePath"],
39
- where: { state$gte: 0 },
40
- orderBy: ["id#ASC"]
41
- });
42
- apiPaths = Array.from(new Set((allApis.data.lists || []).map((a) => {
43
- if (typeof a?.routePath !== "string") {
44
- throw new Error("syncDev: addon_admin_api.routePath 必须是字符串");
45
- }
46
- const routePath = a.routePath.trim();
47
- if (routePath.length === 0) {
48
- throw new Error("syncDev: addon_admin_api.routePath 不允许为空字符串");
35
+ const allApis = await ctx.db.getAll({
36
+ table: "addon_admin_api",
37
+ fields: ["path"],
38
+ where: { state$gte: 0 },
39
+ orderBy: ["id#ASC"]
40
+ });
41
+ const devRole = await ctx.db.getOne({
42
+ table: "addon_admin_role",
43
+ where: { code: "dev" }
44
+ });
45
+ const devRoleData = {
46
+ code: "dev",
47
+ name: "开发者角色",
48
+ description: "拥有所有菜单和接口权限的开发者角色",
49
+ menus: allMenus.data.lists.map((item) => item.path).filter((v) => v),
50
+ apis: allApis.data.lists.map((item) => item.path).filter((v) => v),
51
+ sort: 0
52
+ };
53
+ if (devRole.data) {
54
+ await ctx.db.updData({
55
+ table: "addon_admin_role",
56
+ where: { code: "dev" },
57
+ data: {
58
+ name: devRoleData.name,
59
+ description: devRoleData.description,
60
+ menus: devRoleData.menus,
61
+ apis: devRoleData.apis,
62
+ sort: devRoleData.sort
49
63
  }
50
- if (!routePath.startsWith("/")) {
51
- throw new Error(`syncDev: addon_admin_api.routePath 必须是 pathname(以 / 开头),当前值=${routePath}`);
64
+ });
65
+ }
66
+ else {
67
+ await ctx.db.insData({
68
+ table: "addon_admin_role",
69
+ data: devRoleData
70
+ });
71
+ }
72
+ const devAdminData = {
73
+ nickname: "开发者",
74
+ email: config.devEmail || "dev@qq.com",
75
+ username: "dev",
76
+ password: await Cipher.hashPassword(Cipher.sha256(config.devPassword + "befly")),
77
+ roleCode: "dev",
78
+ roleType: "admin"
79
+ };
80
+ const devAdmin = await ctx.db.getOne({
81
+ table: "addon_admin_admin",
82
+ where: { username: "dev" }
83
+ });
84
+ if (devAdmin.data) {
85
+ await ctx.db.updData({
86
+ table: "addon_admin_admin",
87
+ where: { username: "dev" },
88
+ data: {
89
+ nickname: devAdminData.nickname,
90
+ email: devAdminData.email,
91
+ username: devAdminData.username,
92
+ password: devAdminData.password,
93
+ roleCode: devAdminData.roleCode,
94
+ roleType: devAdminData.roleType
52
95
  }
53
- return routePath;
54
- })));
96
+ });
97
+ }
98
+ else {
99
+ await ctx.db.insData({
100
+ table: "addon_admin_admin",
101
+ data: devAdminData
102
+ });
55
103
  }
56
104
  const roles = [
57
- {
58
- code: "dev",
59
- name: "开发者角色",
60
- description: "拥有所有菜单和接口权限的开发者角色",
61
- menus: menuPaths,
62
- apis: apiPaths,
63
- sort: 0
64
- },
65
105
  {
66
106
  code: "user",
67
107
  name: "用户角色",
68
108
  description: "普通用户角色",
69
- menus: [],
70
- apis: [],
71
109
  sort: 1
72
110
  },
73
111
  {
74
112
  code: "admin",
75
113
  name: "管理员角色",
76
114
  description: "管理员角色",
77
- menus: [],
78
- apis: [],
79
115
  sort: 2
80
116
  },
81
117
  {
82
118
  code: "guest",
83
119
  name: "访客角色",
84
120
  description: "访客角色",
85
- menus: [],
86
- apis: [],
87
121
  sort: 3
88
122
  }
89
123
  ];
90
- let devRole = null;
91
124
  for (const roleConfig of roles) {
92
125
  const existingRole = await ctx.db.getOne({
93
126
  table: "addon_admin_role",
94
127
  where: { code: roleConfig.code }
95
128
  });
96
- if (existingRole.data) {
97
- const nextMenus = roleConfig.menus;
98
- const nextApis = roleConfig.apis;
99
- const existingMenus = Array.isArray(existingRole.data.menus) ? existingRole.data.menus : [];
100
- const existingApis = Array.isArray(existingRole.data.apis) ? existingRole.data.apis : [];
101
- const menusChanged = existingMenus.length !== nextMenus.length || existingMenus.some((v, i) => v !== nextMenus[i]);
102
- const apisChanged = existingApis.length !== nextApis.length || existingApis.some((v, i) => v !== nextApis[i]);
103
- const hasChanges = existingRole.data.name !== roleConfig.name || existingRole.data.description !== roleConfig.description || menusChanged || apisChanged || existingRole.data.sort !== roleConfig.sort;
104
- if (hasChanges) {
105
- await ctx.db.updData({
106
- table: "addon_admin_role",
107
- where: { code: roleConfig.code },
108
- data: {
109
- name: roleConfig.name,
110
- description: roleConfig.description,
111
- menus: roleConfig.menus,
112
- apis: roleConfig.apis,
113
- sort: roleConfig.sort
114
- }
115
- });
116
- }
117
- if (roleConfig.code === "dev") {
118
- devRole = existingRole.data;
119
- }
129
+ if (existingRole.data?.id) {
130
+ // 角色存在则强制更新(不做差异判断)
131
+ await ctx.db.updData({
132
+ table: "addon_admin_role",
133
+ where: { code: roleConfig.code },
134
+ data: {
135
+ name: roleConfig.name,
136
+ description: roleConfig.description,
137
+ sort: roleConfig.sort
138
+ }
139
+ });
120
140
  }
121
141
  else {
122
- const roleId = await ctx.db.insData({
142
+ await ctx.db.insData({
123
143
  table: "addon_admin_role",
124
- data: roleConfig
144
+ data: {
145
+ code: roleConfig.code,
146
+ name: roleConfig.name,
147
+ description: roleConfig.description,
148
+ sort: roleConfig.sort
149
+ }
125
150
  });
126
- if (roleConfig.code === "dev") {
127
- devRole = { id: roleId.data };
128
- }
129
151
  }
130
152
  }
131
- if (!devRole) {
132
- Logger.error("dev 角色不存在,无法创建开发者账号");
133
- return;
134
- }
135
- const sha256Hashed = Cipher.sha256(config.devPassword + "befly");
136
- const hashed = await Cipher.hashPassword(sha256Hashed);
137
- const devData = {
138
- nickname: "开发者",
139
- email: devEmail,
140
- username: "dev",
141
- password: hashed,
142
- roleCode: "dev",
143
- roleType: "admin"
144
- };
145
- const existing = await ctx.db.getOne({
146
- table: "addon_admin_admin",
147
- where: { email: devEmail }
148
- });
149
- if (existing.data) {
150
- await ctx.db.updData({
151
- table: "addon_admin_admin",
152
- where: { email: devEmail },
153
- data: devData
154
- });
155
- }
156
- else {
157
- await ctx.db.insData({
158
- table: "addon_admin_admin",
159
- data: devData
160
- });
161
- }
162
153
  }
@@ -61,7 +61,8 @@ export interface SyncApiItem {
61
61
  name: string;
62
62
  /** 是否需要登录:0=免登录,1=需登录(同步时会归一化为 0/1) */
63
63
  auth?: 0 | 1 | boolean;
64
- routePath: string;
64
+ path: string;
65
+ parentPath: string;
65
66
  addonName: string;
66
67
  [key: string]: any;
67
68
  }
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * 处理字段定义:将 @ 符号引用替换为实际字段定义
3
3
  */
4
- export declare function processAtSymbol(fields: Record<string, any>, apiName: string, routePath: string): Record<string, any>;
4
+ export declare function processAtSymbol(fields: Record<string, any>, apiName: string, path: string): Record<string, any>;
@@ -2,7 +2,7 @@ import { presetFields } from "../configs/presetFields";
2
2
  /**
3
3
  * 处理字段定义:将 @ 符号引用替换为实际字段定义
4
4
  */
5
- export function processAtSymbol(fields, apiName, routePath) {
5
+ export function processAtSymbol(fields, apiName, path) {
6
6
  if (!fields || typeof fields !== "object")
7
7
  return fields;
8
8
  const processed = {};
@@ -13,7 +13,7 @@ export function processAtSymbol(fields, apiName, routePath) {
13
13
  continue;
14
14
  }
15
15
  const validKeys = Object.keys(presetFields).join(", ");
16
- throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
16
+ throw new Error(`API [${apiName}] (${path}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
17
17
  }
18
18
  processed[key] = value;
19
19
  }
@@ -102,6 +102,10 @@ export async function scanFiles(dir, source, type, pattern) {
102
102
  continue;
103
103
  }
104
104
  if (type === "api") {
105
+ // 运行时 auth 必须是 boolean:
106
+ // - checkApi 会校验 auth 类型
107
+ // - permission hook 以 ctx.api.auth === false 判断公开接口
108
+ // DB 存储的 0/1 由 syncApi 负责转换写入。
105
109
  base.auth = true;
106
110
  base.rawBody = false;
107
111
  base.method = "POST";
@@ -118,7 +122,7 @@ export async function scanFiles(dir, source, type, pattern) {
118
122
  });
119
123
  if (type === "api") {
120
124
  base.routePrefix = source === "app" ? "/app" : `/addon/${addonName}`;
121
- base.routePath = `/api${base.routePrefix}/${relativePath}`;
125
+ base.path = `/api${base.routePrefix}/${relativePath}`;
122
126
  }
123
127
  results.push(base);
124
128
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.13.9",
4
- "gitHead": "990c7108a9a791b9816c447309ec586d08b96ab4",
3
+ "version": "3.14.0",
4
+ "gitHead": "0d60e323a83a1fc53822147de877af19cdb972c0",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
7
7
  "keywords": [