befly 3.35.0 → 3.37.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/router/api.js CHANGED
@@ -9,7 +9,7 @@ import { Logger } from "../lib/logger.js";
9
9
  // 相对导入
10
10
  import { setCorsOptions } from "../utils/cors.js";
11
11
  import { getClientIp } from "../utils/getClientIp.js";
12
- import { FinalResponse } from "../utils/response.js";
12
+ import { ErrorResponse, FinalResponse } from "../utils/response.js";
13
13
  import { genShortId } from "../utils/util.js";
14
14
 
15
15
  function createExcludeApisLogMatchers(excludeApisLog) {
@@ -41,7 +41,7 @@ export function apiHandler(apis, hooks, context) {
41
41
  const requestId = genShortId();
42
42
  const now = Date.now();
43
43
 
44
- const corsHeaders = setCorsOptions(req, context.config?.cors || {});
44
+ const corsHeaders = setCorsOptions(req, context.config.cors);
45
45
  corsHeaders["X-Request-ID"] = requestId;
46
46
 
47
47
  // 2. OPTIONS 预检请求:直接返回,不走 hooks,不打日志
@@ -95,6 +95,21 @@ export function apiHandler(apis, hooks, context) {
95
95
  apiFile: apiData.filePath
96
96
  };
97
97
 
98
+ if (ctx.apiMethod !== "ALL" && ctx.method !== ctx.apiMethod) {
99
+ return ErrorResponse(
100
+ ctx,
101
+ `请求方法不允许,请使用 ${ctx.apiMethod}`,
102
+ 1,
103
+ null,
104
+ {
105
+ method: ctx.method,
106
+ allowMethod: ctx.apiMethod,
107
+ apiPath: ctx.apiPath
108
+ },
109
+ "method"
110
+ );
111
+ }
112
+
98
113
  try {
99
114
  // 4. 串联执行所有钩子
100
115
  for (const hook of hooks) {
package/router/static.js CHANGED
@@ -4,24 +4,40 @@
4
4
  */
5
5
 
6
6
  // 外部依赖
7
- import { join } from "pathe";
7
+ import { basename, extname, isAbsolute, normalize, relative, resolve } from "pathe";
8
8
 
9
9
  import { Logger } from "../lib/logger.js";
10
10
  // 相对导入
11
11
  import { getAppPublicDir } from "../paths.js";
12
12
  import { setCorsOptions } from "../utils/cors.js";
13
13
 
14
+ function getStaticFilePath(url, baseDir) {
15
+ try {
16
+ const relativePath = normalize(decodeURIComponent(url.pathname.slice("/public/".length)));
17
+ const filePath = resolve(baseDir, relativePath);
18
+ const fileRelativePath = relative(baseDir, filePath);
19
+
20
+ if (fileRelativePath.startsWith("..") || isAbsolute(fileRelativePath)) {
21
+ return "";
22
+ }
23
+
24
+ return filePath;
25
+ } catch {
26
+ return "";
27
+ }
28
+ }
29
+
14
30
  /**
15
31
  * 静态文件处理器工厂
16
32
  */
17
- export function staticHandler(corsConfig = undefined, publicDir = "./public") {
33
+ export function staticHandler(corsConfig, uploadConfig) {
18
34
  return async (req) => {
19
35
  // 设置 CORS 响应头
20
36
  const corsHeaders = setCorsOptions(req, corsConfig);
21
37
 
22
38
  const url = new URL(req.url);
23
- const publicPath = url.pathname.replace(/^\/public/, "") || "/";
24
- const filePath = join(getAppPublicDir(publicDir), publicPath);
39
+ const baseDir = resolve(getAppPublicDir(uploadConfig.publicDir));
40
+ const filePath = getStaticFilePath(url, baseDir);
25
41
 
26
42
  try {
27
43
  // OPTIONS预检请求
@@ -32,6 +48,18 @@ export function staticHandler(corsConfig = undefined, publicDir = "./public") {
32
48
  });
33
49
  }
34
50
 
51
+ if (!filePath) {
52
+ return Response.json(
53
+ {
54
+ code: 1,
55
+ msg: "文件未找到"
56
+ },
57
+ {
58
+ headers: corsHeaders
59
+ }
60
+ );
61
+ }
62
+
35
63
  const file = Bun.file(filePath);
36
64
  if (await file.exists()) {
37
65
  const headers = {};
@@ -41,6 +69,10 @@ export function staticHandler(corsConfig = undefined, publicDir = "./public") {
41
69
  }
42
70
  }
43
71
  headers["Content-Type"] = file.type || "application/octet-stream";
72
+ headers["X-Content-Type-Options"] = "nosniff";
73
+ if (uploadConfig.forceDownloadExtensions.split(",").includes(extname(filePath).toLowerCase())) {
74
+ headers["Content-Disposition"] = `attachment; filename="${basename(filePath).replaceAll('"', "")}"`;
75
+ }
44
76
  return new Response(file, {
45
77
  headers: headers
46
78
  });
package/sql/befly.sql CHANGED
@@ -227,35 +227,3 @@ CREATE TABLE IF NOT EXISTS `befly_error_report` (
227
227
  KEY `idx_befly_error_report_time` (`report_time`),
228
228
  KEY `idx_befly_error_report_bucket_date` (`bucket_date`)
229
229
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
230
-
231
- CREATE TABLE IF NOT EXISTS `befly_info_report` (
232
- `id` BIGINT NOT NULL,
233
- `report_time` BIGINT NOT NULL DEFAULT 0,
234
- `report_date` INT NOT NULL DEFAULT 0,
235
- `member_key` VARCHAR(100) NOT NULL DEFAULT '',
236
- `source` VARCHAR(50) NOT NULL DEFAULT '',
237
- `product_name` VARCHAR(100) NOT NULL DEFAULT '',
238
- `product_code` VARCHAR(100) NOT NULL DEFAULT '',
239
- `product_version` VARCHAR(100) NOT NULL DEFAULT '',
240
- `page_path` VARCHAR(200) NOT NULL DEFAULT '',
241
- `page_name` VARCHAR(100) NOT NULL DEFAULT '',
242
- `detail` TEXT NULL,
243
- `user_agent` VARCHAR(500) NOT NULL DEFAULT '',
244
- `browser_name` VARCHAR(100) NOT NULL DEFAULT '',
245
- `browser_version` VARCHAR(100) NOT NULL DEFAULT '',
246
- `os_name` VARCHAR(100) NOT NULL DEFAULT '',
247
- `os_version` VARCHAR(100) NOT NULL DEFAULT '',
248
- `device_type` VARCHAR(50) NOT NULL DEFAULT '',
249
- `device_vendor` VARCHAR(100) NOT NULL DEFAULT '',
250
- `device_model` VARCHAR(100) NOT NULL DEFAULT '',
251
- `engine_name` VARCHAR(100) NOT NULL DEFAULT '',
252
- `cpu_architecture` VARCHAR(100) NOT NULL DEFAULT '',
253
- `state` TINYINT NOT NULL DEFAULT 1,
254
- `created_at` BIGINT NOT NULL DEFAULT 0,
255
- `updated_at` BIGINT NOT NULL DEFAULT 0,
256
- `deleted_at` BIGINT NULL DEFAULT NULL,
257
- PRIMARY KEY (`id`),
258
- UNIQUE KEY `uk_befly_info_report_date_member` (`report_date`, `member_key`),
259
- KEY `idx_befly_info_report_time` (`report_time`),
260
- KEY `idx_befly_info_report_date` (`report_date`)
261
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -67,7 +67,8 @@
67
67
  },
68
68
  "detail": {
69
69
  "name": "错误详情",
70
- "input": "string"
70
+ "input": "string",
71
+ "max": 5000
71
72
  },
72
73
  "userAgent": {
73
74
  "name": "用户代理",
package/utils/cors.js CHANGED
@@ -1,21 +1,12 @@
1
1
  /**
2
2
  * 设置 CORS 响应头
3
3
  * @param req - 请求对象
4
- * @param config - CORS 配置(可选)
4
+ * @param config - CORS 配置
5
5
  * @returns CORS 响应头对象
6
6
  */
7
- export function setCorsOptions(req, config = {}) {
8
- const defaultConfig = {
9
- origin: "*",
10
- methods: "GET, POST, OPTIONS",
11
- allowedHeaders: "Content-Type, Authorization, authorization, token",
12
- exposedHeaders: "Content-Range, X-Content-Range, Authorization, authorization, token",
13
- maxAge: 86400,
14
- credentials: "true"
15
- };
16
-
17
- const merged = Object.assign({}, defaultConfig, config || {});
18
- const origin = merged.origin;
7
+ export function setCorsOptions(req, config) {
8
+ const origin = config.origin;
9
+ const allowCredentials = config.credentials === true || config.credentials === "true" ? "true" : "false";
19
10
  const requestedHeaders = req.headers.get("Access-Control-Request-Headers") || "";
20
11
 
21
12
  const allowedHeaderItems = [];
@@ -40,7 +31,7 @@ export function setCorsOptions(req, config = {}) {
40
31
  allowedHeaderItems.push(value);
41
32
  };
42
33
 
43
- for (const item of String(merged.allowedHeaders || "").split(",")) {
34
+ for (const item of String(config.allowedHeaders || "").split(",")) {
44
35
  pushHeaderIfNeeded(item);
45
36
  }
46
37
 
@@ -51,11 +42,11 @@ export function setCorsOptions(req, config = {}) {
51
42
  const allowHeaders = allowedHeaderItems.join(", ");
52
43
 
53
44
  return {
54
- "Access-Control-Allow-Origin": origin === "*" ? req.headers.get("origin") || "*" : origin,
55
- "Access-Control-Allow-Methods": merged.methods,
45
+ "Access-Control-Allow-Origin": origin === "*" && allowCredentials === "false" ? req.headers.get("origin") || "*" : origin,
46
+ "Access-Control-Allow-Methods": config.methods,
56
47
  "Access-Control-Allow-Headers": allowHeaders,
57
- "Access-Control-Expose-Headers": merged.exposedHeaders,
58
- "Access-Control-Max-Age": String(merged.maxAge),
59
- "Access-Control-Allow-Credentials": merged.credentials
48
+ "Access-Control-Expose-Headers": config.exposedHeaders,
49
+ "Access-Control-Max-Age": String(config.maxAge),
50
+ "Access-Control-Allow-Credentials": allowCredentials
60
51
  };
61
52
  }
@@ -1,192 +0,0 @@
1
- import { getDateYmdNumber } from "#befly/utils/datetime.js";
2
-
3
- import { getTongJiMonthStartDate, getTongJiNumber, getTongJiWeekStartDate } from "./_tongJi.js";
4
-
5
- const ONLINE_ACTIVE_KEY = "online:active";
6
- const INFO_STATS_FALLBACK_COUNT_KEY = "stats:fallback:infoStats:count";
7
- const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
8
-
9
- function getInfoStatsFallbackDailyCountKey(reportDate) {
10
- return `stats:fallback:infoStats:count:day:${reportDate}`;
11
- }
12
-
13
- function getErrorStatsFallbackDailyCountKey(reportDate) {
14
- return `stats:fallback:errorStats:count:day:${reportDate}`;
15
- }
16
-
17
- function getOnlineReportTimeKey(periodType, periodValue) {
18
- return `online:${periodType}:${periodValue}:reportTime`;
19
- }
20
-
21
- function getInfoReportTimeKey(periodType, periodValue) {
22
- return `info:${periodType}:${periodValue}:reportTime`;
23
- }
24
-
25
- function getErrorCountKey(periodType, periodValue) {
26
- return `error:${periodType}:${periodValue}:count`;
27
- }
28
-
29
- async function getRedisStringNumber(redis, key) {
30
- return getTongJiNumber(await redis.getString(key));
31
- }
32
-
33
- async function getRedisSetCount(redis, key) {
34
- if (typeof redis.scard !== "function") {
35
- return 0;
36
- }
37
-
38
- return getTongJiNumber(await redis.scard(key));
39
- }
40
-
41
- async function getRedisSortedSetCount(redis, key) {
42
- if (typeof redis.zcard !== "function") {
43
- return 0;
44
- }
45
-
46
- return getTongJiNumber(await redis.zcard(key));
47
- }
48
-
49
- async function getRedisConnectivity(redis) {
50
- if (!redis || typeof redis.ping !== "function") {
51
- return {
52
- status: "unavailable",
53
- responseTime: -1
54
- };
55
- }
56
-
57
- try {
58
- const start = Date.now();
59
- await redis.ping();
60
-
61
- return {
62
- status: "running",
63
- responseTime: Date.now() - start
64
- };
65
- } catch {
66
- return {
67
- status: "stopped",
68
- responseTime: -1
69
- };
70
- }
71
- }
72
-
73
- export default {
74
- name: "获取统计缓存健康状态",
75
- method: "POST",
76
- body: "none",
77
- auth: true,
78
- fields: {},
79
- required: [],
80
- handler: async (befly) => {
81
- const now = Date.now();
82
- const reportDate = getDateYmdNumber(now);
83
- const weekStartDate = getTongJiWeekStartDate(now);
84
- const monthStartDate = getTongJiMonthStartDate(now);
85
- const redisConnectivity = await getRedisConnectivity(befly.redis);
86
-
87
- if (!befly.redis) {
88
- return befly.tool.Yes("获取成功", {
89
- queryTime: now,
90
- redis: redisConnectivity,
91
- online: null,
92
- info: null,
93
- error: null,
94
- roleCache: null,
95
- hints: ["Redis 不可用,统计接口将回退到 MySQL 聚合"]
96
- });
97
- }
98
-
99
- const onlineTodayReportTime = await getRedisStringNumber(befly.redis, getOnlineReportTimeKey("day", reportDate));
100
- const onlineWeekReportTime = await getRedisStringNumber(befly.redis, getOnlineReportTimeKey("week", weekStartDate));
101
- const onlineMonthReportTime = await getRedisStringNumber(befly.redis, getOnlineReportTimeKey("month", monthStartDate));
102
-
103
- const infoTodayReportTime = await getRedisStringNumber(befly.redis, getInfoReportTimeKey("day", reportDate));
104
- const infoWeekReportTime = await getRedisStringNumber(befly.redis, getInfoReportTimeKey("week", weekStartDate));
105
- const infoMonthReportTime = await getRedisStringNumber(befly.redis, getInfoReportTimeKey("month", monthStartDate));
106
-
107
- const errorDayCount = await getRedisStringNumber(befly.redis, getErrorCountKey("day", reportDate));
108
- const errorWeekCount = await getRedisStringNumber(befly.redis, getErrorCountKey("week", weekStartDate));
109
- const errorMonthCount = await getRedisStringNumber(befly.redis, getErrorCountKey("month", monthStartDate));
110
-
111
- const roleApisVersion = String((await befly.redis.getString("role:apis:activeVersion")) || "");
112
- const roleMenusVersion = String((await befly.redis.getString("role:menus:activeVersion")) || "");
113
-
114
- const online = {
115
- activeMembers: await getRedisSortedSetCount(befly.redis, ONLINE_ACTIVE_KEY),
116
- reportTime: {
117
- today: onlineTodayReportTime,
118
- week: onlineWeekReportTime,
119
- month: onlineMonthReportTime
120
- },
121
- ready: onlineTodayReportTime > 0 || onlineWeekReportTime > 0 || onlineMonthReportTime > 0
122
- };
123
-
124
- const info = {
125
- reportTime: {
126
- today: infoTodayReportTime,
127
- week: infoWeekReportTime,
128
- month: infoMonthReportTime
129
- },
130
- daySourcesCount: await getRedisSetCount(befly.redis, `info:day:${reportDate}:sources:names`),
131
- ready: infoTodayReportTime > 0 || infoWeekReportTime > 0 || infoMonthReportTime > 0
132
- };
133
-
134
- const error = {
135
- count: {
136
- today: errorDayCount,
137
- week: errorWeekCount,
138
- month: errorMonthCount
139
- },
140
- ready: errorDayCount > 0 || errorWeekCount > 0 || errorMonthCount > 0
141
- };
142
-
143
- const roleCache = {
144
- apis: {
145
- activeVersion: roleApisVersion,
146
- ready: roleApisVersion.length > 0
147
- },
148
- menus: {
149
- activeVersion: roleMenusVersion,
150
- ready: roleMenusVersion.length > 0
151
- }
152
- };
153
-
154
- const fallback = {
155
- infoStats: await getRedisStringNumber(befly.redis, INFO_STATS_FALLBACK_COUNT_KEY),
156
- errorStats: await getRedisStringNumber(befly.redis, ERROR_STATS_FALLBACK_COUNT_KEY),
157
- today: {
158
- infoStats: await getRedisStringNumber(befly.redis, getInfoStatsFallbackDailyCountKey(reportDate)),
159
- errorStats: await getRedisStringNumber(befly.redis, getErrorStatsFallbackDailyCountKey(reportDate))
160
- }
161
- };
162
-
163
- const hints = [];
164
-
165
- if (!online.ready) {
166
- hints.push("online 统计预聚合尚未建立,onlineStats 将返回空或低命中结果");
167
- }
168
- if (!info.ready) {
169
- hints.push("info 统计预聚合尚未建立,infoStats 将回退到 MySQL 聚合");
170
- }
171
- if (!error.ready) {
172
- hints.push("error 统计预聚合尚未建立,errorStats 将回退到 MySQL 聚合");
173
- }
174
- if (!roleCache.apis.ready || !roleCache.menus.ready) {
175
- hints.push("角色权限版本缓存未就绪,建议执行一次 cacheRefresh");
176
- }
177
- if (fallback.infoStats > 0 || fallback.errorStats > 0) {
178
- hints.push("检测到统计查询回退到 MySQL,可关注预聚合写入是否稳定,必要时可执行 tongJi/fallbackReset 重置观测计数");
179
- }
180
-
181
- return befly.tool.Yes("获取成功", {
182
- queryTime: now,
183
- redis: redisConnectivity,
184
- online: online,
185
- info: info,
186
- error: error,
187
- roleCache: roleCache,
188
- fallback: fallback,
189
- hints: hints
190
- });
191
- }
192
- };
@@ -1,61 +0,0 @@
1
- import { getDateYmdNumber } from "#befly/utils/datetime.js";
2
-
3
- import { getTongJiNumber } from "./_tongJi.js";
4
-
5
- const INFO_STATS_FALLBACK_COUNT_KEY = "stats:fallback:infoStats:count";
6
- const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
7
-
8
- function getInfoStatsFallbackDailyCountKey(reportDate) {
9
- return `stats:fallback:infoStats:count:day:${reportDate}`;
10
- }
11
-
12
- function getErrorStatsFallbackDailyCountKey(reportDate) {
13
- return `stats:fallback:errorStats:count:day:${reportDate}`;
14
- }
15
-
16
- export default {
17
- name: "重置统计回退计数",
18
- method: "POST",
19
- body: "none",
20
- auth: true,
21
- fields: {},
22
- required: [],
23
- handler: async (befly) => {
24
- if (!befly.redis) {
25
- return befly.tool.No("Redis 不可用,无法重置", {
26
- reset: false
27
- });
28
- }
29
-
30
- const reportDate = getDateYmdNumber(Date.now());
31
- const infoDailyKey = getInfoStatsFallbackDailyCountKey(reportDate);
32
- const errorDailyKey = getErrorStatsFallbackDailyCountKey(reportDate);
33
- const resetKeys = [INFO_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_COUNT_KEY, infoDailyKey, errorDailyKey];
34
- const before = {
35
- infoStats: getTongJiNumber(await befly.redis.getString(INFO_STATS_FALLBACK_COUNT_KEY)),
36
- errorStats: getTongJiNumber(await befly.redis.getString(ERROR_STATS_FALLBACK_COUNT_KEY)),
37
- today: {
38
- infoStats: getTongJiNumber(await befly.redis.getString(infoDailyKey)),
39
- errorStats: getTongJiNumber(await befly.redis.getString(errorDailyKey))
40
- }
41
- };
42
-
43
- const removedCount = await befly.redis.delBatch(resetKeys);
44
-
45
- return befly.tool.Yes("重置成功", {
46
- reset: true,
47
- reportDate: reportDate,
48
- removedCount: getTongJiNumber(removedCount),
49
- keys: resetKeys,
50
- before: before,
51
- after: {
52
- infoStats: 0,
53
- errorStats: 0,
54
- today: {
55
- infoStats: 0,
56
- errorStats: 0
57
- }
58
- }
59
- });
60
- }
61
- };
@@ -1,123 +0,0 @@
1
- {
2
- "id": {
3
- "name": "ID",
4
- "input": "integer",
5
- "min": 1,
6
- "max": null
7
- },
8
- "reportTime": {
9
- "name": "上报时间",
10
- "input": "number"
11
- },
12
- "reportDate": {
13
- "name": "上报日期",
14
- "input": "number"
15
- },
16
- "memberKey": {
17
- "name": "成员标识",
18
- "input": "string",
19
- "max": 100
20
- },
21
- "source": {
22
- "name": "来源",
23
- "input": "string",
24
- "max": 50
25
- },
26
- "productName": {
27
- "name": "产品名称",
28
- "input": "string",
29
- "max": 100
30
- },
31
- "productCode": {
32
- "name": "产品代号",
33
- "input": "string",
34
- "max": 100
35
- },
36
- "productVersion": {
37
- "name": "产品版本",
38
- "input": "string",
39
- "max": 100
40
- },
41
- "pagePath": {
42
- "name": "页面路径",
43
- "input": "string",
44
- "max": 200
45
- },
46
- "pageName": {
47
- "name": "页面名称",
48
- "input": "string",
49
- "max": 100
50
- },
51
- "detail": {
52
- "name": "扩展详情",
53
- "input": "string"
54
- },
55
- "userAgent": {
56
- "name": "用户代理",
57
- "input": "string",
58
- "max": 500
59
- },
60
- "browserName": {
61
- "name": "浏览器名称",
62
- "input": "string",
63
- "max": 100
64
- },
65
- "browserVersion": {
66
- "name": "浏览器版本",
67
- "input": "string",
68
- "max": 100
69
- },
70
- "osName": {
71
- "name": "操作系统名称",
72
- "input": "string",
73
- "max": 100
74
- },
75
- "osVersion": {
76
- "name": "操作系统版本",
77
- "input": "string",
78
- "max": 100
79
- },
80
- "deviceType": {
81
- "name": "设备类型",
82
- "input": "string",
83
- "max": 50
84
- },
85
- "deviceVendor": {
86
- "name": "设备厂商",
87
- "input": "string",
88
- "max": 100
89
- },
90
- "deviceModel": {
91
- "name": "设备型号",
92
- "input": "string",
93
- "max": 100
94
- },
95
- "engineName": {
96
- "name": "引擎名称",
97
- "input": "string",
98
- "max": 100
99
- },
100
- "cpuArchitecture": {
101
- "name": "CPU架构",
102
- "input": "string",
103
- "max": 100
104
- },
105
- "state": {
106
- "name": "状态",
107
- "input": "integer",
108
- "min": 0,
109
- "max": 2
110
- },
111
- "createdAt": {
112
- "name": "创建时间",
113
- "input": "number"
114
- },
115
- "updatedAt": {
116
- "name": "更新时间",
117
- "input": "number"
118
- },
119
- "deletedAt": {
120
- "name": "删除时间",
121
- "input": "number"
122
- }
123
- }