befly 3.16.5 → 3.16.6

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/dist/befly.js CHANGED
@@ -9833,6 +9833,7 @@ async function syncCache(ctx) {
9833
9833
  await ctx.cache.cacheApis();
9834
9834
  await ctx.cache.cacheMenus();
9835
9835
  await ctx.cache.rebuildRoleApiPermissions();
9836
+ await ctx.cache.rebuildRoleMenuPermissions();
9836
9837
  }
9837
9838
 
9838
9839
  // lib/cipher.ts
@@ -11482,32 +11483,6 @@ var calcPerfTime = (startTime, endTime = Bun.nanoseconds()) => {
11482
11483
  }
11483
11484
  };
11484
11485
 
11485
- // utils/processInfo.ts
11486
- function getProcessRole(env) {
11487
- const runtimeEnv = env || {};
11488
- const bunWorkerId = runtimeEnv["BUN_WORKER_ID"];
11489
- const pm2InstanceId = runtimeEnv["PM2_INSTANCE_ID"];
11490
- if (bunWorkerId !== undefined) {
11491
- return {
11492
- role: bunWorkerId === "" ? "primary" : "worker",
11493
- instanceId: bunWorkerId || "0",
11494
- env: "bun-cluster"
11495
- };
11496
- }
11497
- if (pm2InstanceId !== undefined) {
11498
- return {
11499
- role: pm2InstanceId === "0" ? "primary" : "worker",
11500
- instanceId: pm2InstanceId,
11501
- env: "pm2-cluster"
11502
- };
11503
- }
11504
- return {
11505
- role: "primary",
11506
- instanceId: null,
11507
- env: "standalone"
11508
- };
11509
- }
11510
-
11511
11486
  // utils/scanSources.ts
11512
11487
  init_dist();
11513
11488
 
@@ -13109,6 +13084,9 @@ class CacheKeys {
13109
13084
  static roleInfo(roleCode) {
13110
13085
  return `role:info:${roleCode}`;
13111
13086
  }
13087
+ static roleMenus(roleCode) {
13088
+ return `role:menus:${roleCode}`;
13089
+ }
13112
13090
  static roleApis(roleCode) {
13113
13091
  return `role:apis:${roleCode}`;
13114
13092
  }
@@ -13694,6 +13672,25 @@ class CacheHelper {
13694
13672
  }
13695
13673
  return trimmed;
13696
13674
  }
13675
+ assertMenuPathname(value, errorPrefix) {
13676
+ if (typeof value !== "string") {
13677
+ throw new Error(`${errorPrefix} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
13678
+ }
13679
+ const trimmed = value.trim();
13680
+ if (!trimmed) {
13681
+ throw new Error(`${errorPrefix} \u4E0D\u5141\u8BB8\u4E3A\u7A7A\u5B57\u7B26\u4E32`);
13682
+ }
13683
+ if (!trimmed.startsWith("/")) {
13684
+ throw new Error(`${errorPrefix} \u5FC5\u987B\u662F pathname\uFF08\u4EE5 / \u5F00\u5934\uFF09`);
13685
+ }
13686
+ if (trimmed.includes(" ")) {
13687
+ throw new Error(`${errorPrefix} \u4E0D\u5141\u8BB8\u5305\u542B\u7A7A\u683C`);
13688
+ }
13689
+ if (trimmed.length > 1 && trimmed.endsWith("/")) {
13690
+ throw new Error(`${errorPrefix} \u4E0D\u5141\u8BB8\u4EE5 / \u7ED3\u5C3E`);
13691
+ }
13692
+ return trimmed;
13693
+ }
13697
13694
  assertApiPathList(value, roleCode) {
13698
13695
  if (value === null || value === undefined)
13699
13696
  return [];
@@ -13721,6 +13718,33 @@ class CacheHelper {
13721
13718
  }
13722
13719
  return out;
13723
13720
  }
13721
+ assertMenuPathList(value, roleCode) {
13722
+ if (value === null || value === undefined)
13723
+ return [];
13724
+ let list = value;
13725
+ if (typeof list === "string") {
13726
+ const trimmed = list.trim();
13727
+ if (trimmed === "" || trimmed === "null") {
13728
+ return [];
13729
+ }
13730
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
13731
+ try {
13732
+ list = JSON.parse(trimmed);
13733
+ } catch {
13734
+ throw new Error(`\u89D2\u8272\u83DC\u5355\u6743\u9650\u6570\u636E\u4E0D\u5408\u6CD5\uFF1Aaddon_admin_role.menus JSON \u89E3\u6790\u5931\u8D25\uFF0CroleCode=${roleCode}`);
13735
+ }
13736
+ }
13737
+ }
13738
+ if (!Array.isArray(list)) {
13739
+ const typeLabel = typeof list;
13740
+ throw new Error(`\u89D2\u8272\u83DC\u5355\u6743\u9650\u6570\u636E\u4E0D\u5408\u6CD5\uFF1Aaddon_admin_role.menus \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\u6216 JSON \u6570\u7EC4\u5B57\u7B26\u4E32\uFF0CroleCode=${roleCode}\uFF0Ctype=${typeLabel}`);
13741
+ }
13742
+ const out = [];
13743
+ for (const item of list) {
13744
+ out.push(this.assertMenuPathname(item, `\u89D2\u8272\u83DC\u5355\u6743\u9650\u6570\u636E\u4E0D\u5408\u6CD5\uFF1Aaddon_admin_role.menus \u5143\u7D20\uFF0CroleCode=${roleCode}`));
13745
+ }
13746
+ return out;
13747
+ }
13724
13748
  async cacheApis() {
13725
13749
  try {
13726
13750
  const tableExists = await this.db.tableExists("addon_admin_api");
@@ -13807,6 +13831,56 @@ class CacheHelper {
13807
13831
  });
13808
13832
  }
13809
13833
  }
13834
+ async rebuildRoleMenuPermissions() {
13835
+ try {
13836
+ const roleTableExists = await this.db.tableExists("addon_admin_role");
13837
+ if (!roleTableExists.data) {
13838
+ Logger.warn("\u26A0\uFE0F \u89D2\u8272\u8868\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u89D2\u8272\u83DC\u5355\u6743\u9650\u7F13\u5B58");
13839
+ return;
13840
+ }
13841
+ const roles = await this.db.getAll({
13842
+ table: "addon_admin_role",
13843
+ fields: ["code", "menus"]
13844
+ });
13845
+ const roleMenuPathsMap = new Map;
13846
+ for (const role of roles.data.lists) {
13847
+ if (!role?.code)
13848
+ continue;
13849
+ const menuPaths = this.assertMenuPathList(role.menus, role.code);
13850
+ roleMenuPathsMap.set(role.code, menuPaths);
13851
+ }
13852
+ const roleCodes = Array.from(roleMenuPathsMap.keys());
13853
+ if (roleCodes.length === 0) {
13854
+ Logger.info("\u2705 \u6CA1\u6709\u9700\u8981\u7F13\u5B58\u7684\u89D2\u8272\u83DC\u5355\u6743\u9650");
13855
+ return;
13856
+ }
13857
+ const roleKeys = roleCodes.map((code) => CacheKeys.roleMenus(code));
13858
+ await this.redis.delBatch(roleKeys);
13859
+ const items = [];
13860
+ for (const roleCode of roleCodes) {
13861
+ const menuPaths = roleMenuPathsMap.get(roleCode) || [];
13862
+ const members = Array.from(new Set(menuPaths)).sort();
13863
+ if (members.length > 0) {
13864
+ items.push({ key: CacheKeys.roleMenus(roleCode), members });
13865
+ }
13866
+ }
13867
+ if (items.length > 0) {
13868
+ await this.redis.saddBatch(items);
13869
+ }
13870
+ } catch (error) {
13871
+ Logger.error({ err: error, msg: "\u26A0\uFE0F \u89D2\u8272\u83DC\u5355\u6743\u9650\u7F13\u5B58\u5F02\u5E38\uFF08\u5C06\u963B\u65AD\u542F\u52A8\uFF09" });
13872
+ throw new CoreError({
13873
+ kind: "runtime",
13874
+ message: "\u26A0\uFE0F \u89D2\u8272\u83DC\u5355\u6743\u9650\u7F13\u5B58\u5F02\u5E38\uFF08\u5C06\u963B\u65AD\u542F\u52A8\uFF09",
13875
+ logged: true,
13876
+ cause: error,
13877
+ meta: {
13878
+ subsystem: "cache",
13879
+ operation: "rebuildRoleMenuPermissions"
13880
+ }
13881
+ });
13882
+ }
13883
+ }
13810
13884
  async refreshRoleApiPermissions(roleCode, apiPaths) {
13811
13885
  if (!roleCode || typeof roleCode !== "string") {
13812
13886
  throw new Error("roleCode \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
@@ -13826,10 +13900,30 @@ class CacheHelper {
13826
13900
  await this.redis.sadd(roleKey, members);
13827
13901
  }
13828
13902
  }
13903
+ async refreshRoleMenuPermissions(roleCode, menuPaths) {
13904
+ if (!roleCode || typeof roleCode !== "string") {
13905
+ throw new Error("roleCode \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
13906
+ }
13907
+ if (!Array.isArray(menuPaths)) {
13908
+ throw new Error("menuPaths \u5FC5\u987B\u662F\u6570\u7EC4");
13909
+ }
13910
+ const normalizedPaths = menuPaths.map((p) => this.assertMenuPathname(p, `refreshRoleMenuPermissions: menuPaths \u5143\u7D20\uFF0CroleCode=${roleCode}`));
13911
+ const roleKey = CacheKeys.roleMenus(roleCode);
13912
+ if (normalizedPaths.length === 0) {
13913
+ await this.redis.del(roleKey);
13914
+ return;
13915
+ }
13916
+ const members = Array.from(new Set(normalizedPaths));
13917
+ await this.redis.del(roleKey);
13918
+ if (members.length > 0) {
13919
+ await this.redis.sadd(roleKey, members);
13920
+ }
13921
+ }
13829
13922
  async cacheAll() {
13830
13923
  await this.cacheApis();
13831
13924
  await this.cacheMenus();
13832
13925
  await this.rebuildRoleApiPermissions();
13926
+ await this.rebuildRoleMenuPermissions();
13833
13927
  }
13834
13928
  async getApis() {
13835
13929
  try {
@@ -13858,6 +13952,15 @@ class CacheHelper {
13858
13952
  return [];
13859
13953
  }
13860
13954
  }
13955
+ async getRoleMenuPermissions(roleCode) {
13956
+ try {
13957
+ const permissions = await this.redis.smembers(CacheKeys.roleMenus(roleCode));
13958
+ return permissions || [];
13959
+ } catch (error) {
13960
+ Logger.error({ err: error, roleCode, msg: "\u83B7\u53D6\u89D2\u8272\u83DC\u5355\u6743\u9650\u7F13\u5B58\u5931\u8D25" });
13961
+ return [];
13962
+ }
13963
+ }
13861
13964
  async checkRolePermission(roleCode, apiPath) {
13862
13965
  try {
13863
13966
  const pathname = this.assertApiPathname(apiPath, "checkRolePermission: apiPath");
@@ -13867,6 +13970,15 @@ class CacheHelper {
13867
13970
  return false;
13868
13971
  }
13869
13972
  }
13973
+ async checkRoleMenuPermission(roleCode, menuPath) {
13974
+ try {
13975
+ const pathname = this.assertMenuPathname(menuPath, "checkRoleMenuPermission: menuPath");
13976
+ return await this.redis.sismember(CacheKeys.roleMenus(roleCode), pathname);
13977
+ } catch (error) {
13978
+ Logger.error({ err: error, roleCode, msg: "\u68C0\u67E5\u89D2\u8272\u83DC\u5355\u6743\u9650\u5931\u8D25" });
13979
+ return false;
13980
+ }
13981
+ }
13870
13982
  async deleteRolePermissions(roleCode) {
13871
13983
  try {
13872
13984
  const result = await this.redis.del(CacheKeys.roleApis(roleCode));
@@ -13880,6 +13992,19 @@ class CacheHelper {
13880
13992
  return false;
13881
13993
  }
13882
13994
  }
13995
+ async deleteRoleMenuPermissions(roleCode) {
13996
+ try {
13997
+ const result = await this.redis.del(CacheKeys.roleMenus(roleCode));
13998
+ if (result > 0) {
13999
+ Logger.info(`\u2705 \u5DF2\u5220\u9664\u89D2\u8272 ${roleCode} \u7684\u83DC\u5355\u6743\u9650\u7F13\u5B58`);
14000
+ return true;
14001
+ }
14002
+ return false;
14003
+ } catch (error) {
14004
+ Logger.error({ err: error, roleCode, msg: "\u5220\u9664\u89D2\u8272\u83DC\u5355\u6743\u9650\u7F13\u5B58\u5931\u8D25" });
14005
+ return false;
14006
+ }
14007
+ }
13883
14008
  }
13884
14009
 
13885
14010
  // plugins/cache.ts
@@ -16293,6 +16418,52 @@ class RedisHelper {
16293
16418
  return null;
16294
16419
  }
16295
16420
  }
16421
+ async tryAcquireLock(key, token, ttlMs) {
16422
+ try {
16423
+ if (!key || typeof key !== "string") {
16424
+ throw new Error("tryAcquireLock: key \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
16425
+ }
16426
+ if (!token || typeof token !== "string") {
16427
+ throw new Error("tryAcquireLock: token \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
16428
+ }
16429
+ if (!(typeof ttlMs === "number" && Number.isFinite(ttlMs) && ttlMs > 0)) {
16430
+ throw new Error("tryAcquireLock: ttlMs \u5FC5\u987B\u662F\u6B63\u6570");
16431
+ }
16432
+ const pkey = `${this.prefix}${key}`;
16433
+ const startTime = Date.now();
16434
+ const ttlMsString = String(Math.floor(ttlMs));
16435
+ const res = await this.client.set(pkey, token, "NX", "PX", ttlMsString);
16436
+ const duration = Date.now() - startTime;
16437
+ this.logSlow("SET NX PX", pkey, duration, { ttlMs });
16438
+ return res === "OK";
16439
+ } catch (error) {
16440
+ Logger.error({ err: error, msg: "Redis tryAcquireLock \u9519\u8BEF" });
16441
+ return false;
16442
+ }
16443
+ }
16444
+ async releaseLock(key, token) {
16445
+ try {
16446
+ if (!key || typeof key !== "string") {
16447
+ throw new Error("releaseLock: key \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
16448
+ }
16449
+ if (!token || typeof token !== "string") {
16450
+ throw new Error("releaseLock: token \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
16451
+ }
16452
+ const pkey = `${this.prefix}${key}`;
16453
+ const startTime = Date.now();
16454
+ const current = await this.client.get(pkey);
16455
+ if (current !== token) {
16456
+ return false;
16457
+ }
16458
+ const deleted = await this.client.del(pkey);
16459
+ const duration = Date.now() - startTime;
16460
+ this.logSlow("GET+DEL", pkey, duration);
16461
+ return deleted > 0;
16462
+ } catch (error) {
16463
+ Logger.error({ err: error, msg: "Redis releaseLock \u9519\u8BEF" });
16464
+ return false;
16465
+ }
16466
+ }
16296
16467
  async getString(key) {
16297
16468
  try {
16298
16469
  const pkey = `${this.prefix}${key}`;
@@ -16884,17 +17055,57 @@ class Befly {
16884
17055
  });
16885
17056
  this.plugins = await loadPlugins(plugins, this.context);
16886
17057
  this.assertStartContextReady();
16887
- await new SyncTable(this.context).run(tables);
16888
- await syncApi(this.context, apis);
16889
- await syncMenu(this.context, checkedMenus);
16890
- const devEmail = this.config.devEmail;
16891
- const devPassword = this.config.devPassword;
16892
- if (typeof devEmail === "string" && devEmail.length > 0 && typeof devPassword === "string" && devPassword.length > 0) {
16893
- await syncDev(this.context, { devEmail, devPassword });
17058
+ const syncLockKey = "sync:lock";
17059
+ const syncReadyKey = "sync:ready";
17060
+ const syncLockTtlMs = 10 * 60 * 1000;
17061
+ const syncWaitReadyMaxMs = 120 * 1000;
17062
+ const syncWaitPollMs = 250;
17063
+ const syncToken = `${process.pid}:${Bun.nanoseconds()}`;
17064
+ const ctx = this.context;
17065
+ const acquired = await ctx.redis.tryAcquireLock(syncLockKey, syncToken, syncLockTtlMs);
17066
+ if (acquired) {
17067
+ Logger.info({ key: syncLockKey, msg: "\u2705 \u5DF2\u83B7\u53D6\u542F\u52A8\u540C\u6B65\u9501\uFF0C\u5C06\u6267\u884C\u81EA\u52A8\u540C\u6B65" });
17068
+ await ctx.redis.del(syncReadyKey);
17069
+ try {
17070
+ await new SyncTable(ctx).run(tables);
17071
+ await syncApi(ctx, apis);
17072
+ await syncMenu(ctx, checkedMenus);
17073
+ const devEmail = this.config.devEmail;
17074
+ const devPassword = this.config.devPassword;
17075
+ if (typeof devEmail === "string" && devEmail.length > 0 && typeof devPassword === "string" && devPassword.length > 0) {
17076
+ await syncDev(ctx, { devEmail, devPassword });
17077
+ } else {
17078
+ Logger.debug("\u8DF3\u8FC7 syncDev\uFF1A\u672A\u914D\u7F6E devEmail/devPassword");
17079
+ }
17080
+ await syncCache(ctx);
17081
+ await ctx.redis.setString(syncReadyKey, String(Date.now()), 60 * 60);
17082
+ } finally {
17083
+ const released = await ctx.redis.releaseLock(syncLockKey, syncToken);
17084
+ if (!released) {
17085
+ Logger.warn({ key: syncLockKey, msg: "\u540C\u6B65\u9501\u672A\u80FD\u4E3B\u52A8\u91CA\u653E\uFF08\u5C06\u4F9D\u8D56 TTL \u81EA\u52A8\u91CA\u653E\uFF09" });
17086
+ }
17087
+ }
16894
17088
  } else {
16895
- Logger.debug("\u8DF3\u8FC7 syncDev\uFF1A\u672A\u914D\u7F6E devEmail/devPassword");
17089
+ Logger.info({ key: syncLockKey, msg: "\u542F\u52A8\u540C\u6B65\u9501\u88AB\u5360\u7528\uFF1A\u7B49\u5F85\u540C\u6B65\u5B8C\u6210\u6807\u8BB0" });
17090
+ const waitStart = Date.now();
17091
+ while (true) {
17092
+ const ready = await ctx.redis.getString(syncReadyKey);
17093
+ if (typeof ready === "string" && ready.length > 0) {
17094
+ Logger.info({ key: syncReadyKey, msg: "\u2705 \u68C0\u6D4B\u5230\u540C\u6B65\u5B8C\u6210\u6807\u8BB0\uFF0C\u5C06\u8DF3\u8FC7\u81EA\u52A8\u540C\u6B65" });
17095
+ break;
17096
+ }
17097
+ const elapsed = Date.now() - waitStart;
17098
+ if (elapsed >= syncWaitReadyMaxMs) {
17099
+ throw new CoreError({
17100
+ kind: "runtime",
17101
+ message: `\u542F\u52A8\u7B49\u5F85\u540C\u6B65\u5B8C\u6210\u8D85\u65F6\uFF08${syncWaitReadyMaxMs}ms\uFF09\uFF1A\u8BF7\u68C0\u67E5\u662F\u5426\u6709\u5B9E\u4F8B\u5361\u5728\u540C\u6B65\u9636\u6BB5\u6216 Redis \u9501 TTL \u8FC7\u957F`,
17102
+ logged: true,
17103
+ meta: { subsystem: "start", operation: "waitSyncReady" }
17104
+ });
17105
+ }
17106
+ await Bun.sleep(syncWaitPollMs);
17107
+ }
16896
17108
  }
16897
- await syncCache(this.context);
16898
17109
  this.hooks = await loadHooks(hooks);
16899
17110
  this.apis = await loadApis(apis);
16900
17111
  const apiFetch = apiHandler(this.apis, this.hooks, this.context);
@@ -16930,10 +17141,7 @@ class Befly {
16930
17141
  }
16931
17142
  });
16932
17143
  const finalStartupTime = calcPerfTime(serverStartTime);
16933
- const processRole = getProcessRole(env);
16934
- const roleLabel = processRole.role === "primary" ? "\u4E3B\u8FDB\u7A0B" : `\u5DE5\u4F5C\u8FDB\u7A0B #${processRole.instanceId}`;
16935
- const envLabel = processRole.env === "standalone" ? "" : ` [${processRole.env}]`;
16936
- Logger.info(`${this.config.appName} \u542F\u52A8\u6210\u529F! (${roleLabel}${envLabel})`);
17144
+ Logger.info(`${this.config.appName} \u542F\u52A8\u6210\u529F!`);
16937
17145
  Logger.info(`\u670D\u52A1\u5668\u542F\u52A8\u8017\u65F6: ${finalStartupTime}`);
16938
17146
  Logger.info(`\u670D\u52A1\u5668\u76D1\u542C\u5730\u5740: ${server.url}`);
16939
17147
  return server;