befly 3.62.0 → 3.64.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.
@@ -5,168 +5,293 @@ const WINDOWS_VERSION_MAP = {
5
5
  6.1: "7"
6
6
  };
7
7
 
8
- function getMatchedVersion(userAgent, regexp) {
9
- const matched = regexp.exec(userAgent);
10
- return matched ? matched[1].replace(/_/g, ".") : "";
8
+ function firstMatch(text, regexp) {
9
+ const matched = regexp.exec(text);
10
+
11
+ if (!matched) {
12
+ return null;
13
+ }
14
+
15
+ for (let index = 1; index < matched.length; index++) {
16
+ if (matched[index]) {
17
+ return matched[index];
18
+ }
19
+ }
20
+
21
+ return null;
11
22
  }
12
23
 
13
- function getBrowserData(userAgent) {
14
- const browserRules = [
15
- ["WeChat", /MicroMessenger\/([\d.]+)/],
16
- ["Edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
17
- ["Opera", /OPR\/([\d.]+)/],
18
- ["Firefox", /FxiOS\/([\d.]+)|Firefox\/([\d.]+)/],
19
- ["Chrome", /CriOS\/([\d.]+)|Chrome\/([\d.]+)/],
20
- ["Safari", /Version\/([\d.]+).*Safari/],
21
- ["IE", /MSIE\s([\d.]+)|Trident\/.*rv:([\d.]+)/]
24
+ function firstNonEmptyMatch(text, regexp) {
25
+ const matched = regexp.exec(text);
26
+
27
+ if (!matched) {
28
+ return null;
29
+ }
30
+
31
+ for (let index = 1; index < matched.length; index++) {
32
+ if (matched[index]) {
33
+ return matched[index];
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ function hasMatch(text, regexp) {
41
+ return regexp.test(text) ? "" : null;
42
+ }
43
+
44
+ function normalizeVersion(version) {
45
+ return String(version || "").replace(/_/g, ".");
46
+ }
47
+
48
+ function majorVersion(version) {
49
+ const parts = String(version || "").split(".");
50
+ return parts[0] || "";
51
+ }
52
+
53
+ function matchBrowser(text) {
54
+ const rules = [
55
+ { name: "wxwork", regexp: /wxwork\/([\d.]+)/ },
56
+ { name: "WeChat", regexp: /MicroMessenger\/([\d.]+)/ },
57
+ { name: "DingTalk", regexp: /DingTalk\/([\d.]+)/ },
58
+ { name: "Alipay", regexp: /AlipayClient\/([\d.]+)/ },
59
+ { name: "Taobao", regexp: /(?:TaoBao|AliApp\(TB|WindVane)/, match: hasMatch },
60
+ { name: "Jingdong", regexp: /jdapp/i, match: hasMatch },
61
+ { name: "Weibo", regexp: /Weibo|__weibo__/, match: hasMatch },
62
+ { name: "Douyin", regexp: /(?:aweme|Trill|Douyin)/i, match: hasMatch },
63
+ { name: "Lark", regexp: /(?:Lark|Feishu)\S*?\/([\d.]+)/ },
64
+ { name: "QQ Browser", regexp: /(?:MQQBrowser|QQBrowser)\/([\d.]+)/ },
65
+ { name: "UC Browser", regexp: /UCBrowser\/([\d.]+)/ },
66
+ { name: "Quark", regexp: /Quark\/([\d.]+)/ },
67
+ { name: "Baidu", regexp: /(?:baiduboxapp|baiduapp|BaiduBrowser|FlyFlow)\S*?\/([\d.]+)/i },
68
+ { name: "Sogou", regexp: /(?:SogouMobileBrowser|SogouMSE|MetaSr)\S*?\/([\d.]+)/i },
69
+ { name: "360", regexp: /(?:360SE|360EE|QHBrowser|QihooBrowser)\S*?(?:\s|v|\/)([\d.]+)/i },
70
+ { name: "Huawei Browser", regexp: /HuaweiBrowser\/([\d.]+)/ },
71
+ { name: "MiuiBrowser", regexp: /MiuiBrowser\/([\d.]+)/ },
72
+ { name: "HeyTapBrowser", regexp: /HeyTapBrowser\/([\d.]+)/ },
73
+ { name: "VivoBrowser", regexp: /VivoBrowser\/([\d.]+)/ },
74
+ { name: "Samsung Browser", regexp: /SamsungBrowser\/([\d.]+)/ },
75
+ { name: "Maxthon", regexp: /Maxthon\/([\d.]+)/ },
76
+ { name: "Edge", regexp: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
77
+ { name: "Opera", regexp: /(?:OPR|Opera)\/([\d.]+)/ },
78
+ { name: "Firefox", regexp: /(?:FxiOS|Firefox)\/([\d.]+)/ },
79
+ { name: "Chrome", regexp: /(?:CriOS|Chrome)\/([\d.]+)/ },
80
+ { name: "Safari", regexp: /Version\/([\d.]+).*Safari/ },
81
+ { name: "IE", regexp: /(?:MSIE\s([\d.]+)|Trident\/.*rv:([\d.]+))/, match: firstNonEmptyMatch }
22
82
  ];
23
83
 
24
- for (const [name, regexp] of browserRules) {
25
- const matched = regexp.exec(userAgent);
26
- if (matched) {
27
- return {
28
- browserName: name === "Safari" && /iPhone|iPad|iPod/i.test(userAgent) ? "Mobile Safari" : name,
29
- browserVersion: matched[1] || matched[2] || ""
30
- };
84
+ for (const rule of rules) {
85
+ const matcher = rule.match || firstMatch;
86
+ const version = matcher(text, rule.regexp);
87
+
88
+ if (version !== null) {
89
+ const name = rule.name === "Safari" && /iPhone|iPad|iPod/i.test(text) ? "Mobile Safari" : rule.name;
90
+
91
+ return { name: name, version: version };
31
92
  }
32
93
  }
33
94
 
34
- return {
35
- browserName: "",
36
- browserVersion: ""
37
- };
95
+ return { name: "", version: "" };
96
+ }
97
+
98
+ function matchEngine(text) {
99
+ if (/AppleWebKit/i.test(text) && /iPhone|iPad|iPod/i.test(text)) {
100
+ return { name: "WebKit", version: firstMatch(text, /AppleWebKit\/([\d.]+)/) };
101
+ }
102
+
103
+ const rules = [
104
+ { name: "Blink", regexp: /Chrome\/([\d.]+)/ },
105
+ { name: "Blink", regexp: /CriOS\/([\d.]+)/ },
106
+ { name: "Blink", regexp: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
107
+ { name: "Blink", regexp: /OPR\/([\d.]+)/ },
108
+ { name: "Gecko", regexp: /Firefox\/([\d.]+)/ },
109
+ { name: "Gecko", regexp: /FxiOS\/([\d.]+)/ },
110
+ { name: "WebKit", regexp: /AppleWebKit\/([\d.]+)/ },
111
+ { name: "Trident", regexp: /Trident\/([\d.]+)/ },
112
+ { name: "Trident", regexp: /MSIE\s([\d.]+)/ }
113
+ ];
114
+
115
+ for (const rule of rules) {
116
+ const version = firstMatch(text, rule.regexp);
117
+
118
+ if (version) {
119
+ return { name: rule.name, version: version };
120
+ }
121
+ }
122
+
123
+ return { name: "", version: "" };
38
124
  }
39
125
 
40
- function getOsData(userAgent) {
41
- const androidVersion = getMatchedVersion(userAgent, /Android\s([\d.]+)/);
126
+ function matchOs(text) {
127
+ const androidVersion = firstMatch(text, /Android\s([\d.]+)/);
128
+
42
129
  if (androidVersion) {
43
- return {
44
- osName: "Android",
45
- osVersion: androidVersion
46
- };
130
+ return { name: "Android", version: androidVersion };
47
131
  }
48
132
 
49
- const iosVersion = getMatchedVersion(userAgent, /(?:iPhone|iPad|iPod).*OS\s([\d_]+)/);
133
+ const iosVersion = firstMatch(text, /(?:iPhone|iPad|iPod).*OS\s([\d_]+)/);
134
+
50
135
  if (iosVersion) {
51
- return {
52
- osName: "iOS",
53
- osVersion: iosVersion
54
- };
136
+ return { name: "iOS", version: normalizeVersion(iosVersion) };
55
137
  }
56
138
 
57
- const windowsVersion = getMatchedVersion(userAgent, /Windows NT\s([\d.]+)/);
139
+ const windowsVersion = firstMatch(text, /Windows NT\s([\d.]+)/);
140
+
58
141
  if (windowsVersion) {
59
- return {
60
- osName: "Windows",
61
- osVersion: WINDOWS_VERSION_MAP[windowsVersion] || windowsVersion
62
- };
142
+ return { name: "Windows", version: WINDOWS_VERSION_MAP[windowsVersion] || windowsVersion };
63
143
  }
64
144
 
65
- const macVersion = getMatchedVersion(userAgent, /Mac OS X\s([\d_]+)/);
145
+ const macVersion = firstMatch(text, /Mac OS X\s([\d_]+)/);
146
+
66
147
  if (macVersion) {
67
- return {
68
- osName: "macOS",
69
- osVersion: macVersion
70
- };
148
+ return { name: "macOS", version: normalizeVersion(macVersion) };
71
149
  }
72
150
 
73
- if (/Linux/i.test(userAgent)) {
74
- return {
75
- osName: "Linux",
76
- osVersion: ""
77
- };
151
+ if (/HarmonyOS/i.test(text)) {
152
+ return { name: "HarmonyOS", version: "" };
78
153
  }
79
154
 
80
- return {
81
- osName: "",
82
- osVersion: ""
83
- };
155
+ if (/Linux/i.test(text)) {
156
+ return { name: "Linux", version: "" };
157
+ }
158
+
159
+ if (/CrOS/i.test(text)) {
160
+ return { name: "Chrome OS", version: "" };
161
+ }
162
+
163
+ return { name: "", version: "" };
84
164
  }
85
165
 
86
- function getDeviceType(userAgent) {
87
- if (/iPad|Tablet|PlayBook|Silk/i.test(userAgent) || (/Android/i.test(userAgent) && !/Mobile/i.test(userAgent))) {
166
+ function getDeviceType(text) {
167
+ if (/iPad|Tablet|PlayBook|Silk/i.test(text) || (/Android/i.test(text) && !/Mobile/i.test(text))) {
88
168
  return "tablet";
89
169
  }
90
- if (/Mobi|iPhone|iPod|Android|Windows Phone/i.test(userAgent)) {
170
+
171
+ if (/PlayStation|Xbox|Nintendo|CrKey|Roku|AppleTV|SmartTV|TV/i.test(text)) {
172
+ return "console";
173
+ }
174
+
175
+ if (/bot|crawler|spider|googlebot|bingbot|baiduspider|slurp|yandex|duckduckgo/i.test(text)) {
176
+ return "bot";
177
+ }
178
+
179
+ if (/Mobi|iPhone|iPod|Phone\s*\d|Android.*Mobile|Windows Phone/i.test(text)) {
180
+ return "mobile";
181
+ }
182
+
183
+ if (/HarmonyOS/i.test(text) && !/Pad|Tablet/i.test(text)) {
91
184
  return "mobile";
92
185
  }
186
+
187
+ if (/HarmonyOS/i.test(text)) {
188
+ return "tablet";
189
+ }
190
+
93
191
  return "desktop";
94
192
  }
95
193
 
96
- function getAndroidDeviceModel(userAgent) {
97
- const matched = /Android[\d.\s]*;\s*([^;)]+?)(?:\s+Build\/|;|\))/i.exec(userAgent);
194
+ function matchAndroidModel(text) {
195
+ const matched = /Android[\d.\s]*;\s*([^;)]+?)(?:\s+Build\/|;|\))/i.exec(text);
98
196
  return matched ? matched[1].trim() : "";
99
197
  }
100
198
 
101
- function getDeviceData(userAgent) {
102
- const deviceType = getDeviceType(userAgent);
103
- if (/iPhone/i.test(userAgent)) {
104
- return {
105
- deviceType: deviceType,
106
- deviceVendor: "Apple",
107
- deviceModel: "iPhone"
108
- };
109
- }
110
- if (/iPad/i.test(userAgent)) {
111
- return {
112
- deviceType: deviceType,
113
- deviceVendor: "Apple",
114
- deviceModel: "iPad"
115
- };
199
+ function matchVendor(text) {
200
+ if (/(iPhone|iPad|iPod|Macintosh)\b/i.test(text)) {
201
+ return "Apple";
116
202
  }
117
203
 
118
- return {
119
- deviceType: deviceType,
120
- deviceVendor: "",
121
- deviceModel: getAndroidDeviceModel(userAgent)
122
- };
123
- }
204
+ if (/Samsung|SM-\w+/i.test(text)) {
205
+ return "Samsung";
206
+ }
124
207
 
125
- function getEngineName(userAgent) {
126
- if (/Chrome|CriOS|Edg|OPR/i.test(userAgent)) {
127
- return "Blink";
208
+ if (/Huawei|HUAWEI|Honor|HONOR/i.test(text)) {
209
+ return "Huawei";
128
210
  }
129
- if (/Firefox|FxiOS/i.test(userAgent)) {
130
- return "Gecko";
211
+
212
+ if (/Xiaomi|Redmi|Mi\s|MI\s|M200|M201|M202|M210|M211|M220|M221|M230|M231|M240/i.test(text)) {
213
+ return "Xiaomi";
131
214
  }
132
- if (/AppleWebKit|Safari/i.test(userAgent)) {
133
- return "WebKit";
215
+
216
+ if (/OPPO|PB[A-Z]|PCHM\d+|PCAM\d+/i.test(text)) {
217
+ return "OPPO";
134
218
  }
135
- if (/Trident|MSIE/i.test(userAgent)) {
136
- return "Trident";
219
+
220
+ if (/vivo|V\d{4}[A-Z]/i.test(text)) {
221
+ return "vivo";
137
222
  }
223
+
138
224
  return "";
139
225
  }
140
226
 
141
- function getCpuArchitecture(userAgent) {
142
- if (/x86_64|Win64|x64|amd64/i.test(userAgent)) {
227
+ function matchDevice(text) {
228
+ const type = getDeviceType(text);
229
+
230
+ if (/iPad/i.test(text)) {
231
+ return { vendor: "Apple", model: "iPad", type: type };
232
+ }
233
+
234
+ if (/iPod/i.test(text)) {
235
+ return { vendor: "Apple", model: "iPod", type: type };
236
+ }
237
+
238
+ if (/iPhone/i.test(text)) {
239
+ return { vendor: "Apple", model: "iPhone", type: type };
240
+ }
241
+
242
+ return { vendor: matchVendor(text), model: matchAndroidModel(text), type: type };
243
+ }
244
+
245
+ function matchCpuArchitecture(text) {
246
+ if (/x86_64|Win64|x64|amd64/i.test(text)) {
143
247
  return "amd64";
144
248
  }
145
- if (/arm64|aarch64/i.test(userAgent)) {
249
+
250
+ if (/arm64|aarch64/i.test(text)) {
146
251
  return "arm64";
147
252
  }
148
- if (/arm/i.test(userAgent)) {
253
+
254
+ if (/arm/i.test(text)) {
149
255
  return "arm";
150
256
  }
257
+
151
258
  return "";
152
259
  }
153
260
 
154
261
  export function parseUserAgent(userAgent) {
155
262
  const text = String(userAgent || "");
156
- const browserData = getBrowserData(text);
157
- const osData = getOsData(text);
158
- const deviceData = getDeviceData(text);
263
+ const browser = matchBrowser(text);
264
+ const engine = matchEngine(text);
265
+ const os = matchOs(text);
266
+ const device = matchDevice(text);
267
+ const cpu = { architecture: matchCpuArchitecture(text) };
159
268
 
160
269
  return {
161
270
  userAgent: text,
162
- browserName: browserData.browserName,
163
- browserVersion: browserData.browserVersion,
164
- osName: osData.osName,
165
- osVersion: osData.osVersion,
166
- deviceType: deviceData.deviceType,
167
- deviceVendor: deviceData.deviceVendor,
168
- deviceModel: deviceData.deviceModel,
169
- engineName: getEngineName(text),
170
- cpuArchitecture: getCpuArchitecture(text)
271
+ browserName: browser.name,
272
+ browserVersion: browser.version,
273
+ osName: os.name,
274
+ osVersion: os.version,
275
+ deviceType: device.type,
276
+ deviceVendor: device.vendor,
277
+ deviceModel: device.model,
278
+ engineName: engine.name,
279
+ engineVersion: engine.version,
280
+ cpuArchitecture: cpu.architecture,
281
+ browser: { name: browser.name, version: browser.version, major: majorVersion(browser.version) },
282
+ engine: engine,
283
+ os: os,
284
+ device: device,
285
+ cpu: cpu
171
286
  };
172
287
  }
288
+
289
+ export function getUserAgentFields(userAgent) {
290
+ const text = typeof userAgent === "string" ? userAgent.trim() : "";
291
+
292
+ if (text.length === 0) {
293
+ return null;
294
+ }
295
+
296
+ return parseUserAgent(text);
297
+ }
@@ -1,159 +0,0 @@
1
- import { DAY_MS } from "#befly/utils/datetime.js";
2
-
3
- const DEFAULT_TIME_ZONE = "Asia/Shanghai";
4
-
5
- function getTimeZoneDateParts(timestamp, timeZone) {
6
- const formatter = new Intl.DateTimeFormat("en-CA", {
7
- timeZone: timeZone,
8
- year: "numeric",
9
- month: "2-digit",
10
- day: "2-digit"
11
- });
12
- const parts = formatter.formatToParts(timestamp);
13
- const result = {};
14
-
15
- for (const part of parts) {
16
- if (part.type !== "literal") {
17
- result[part.type] = Number(part.value);
18
- }
19
- }
20
-
21
- return result;
22
- }
23
-
24
- function buildYmdNumber(year, month, day) {
25
- return year * 10000 + month * 100 + day;
26
- }
27
-
28
- function normalizeTimeZone(timeZone) {
29
- return typeof timeZone === "string" && timeZone.trim().length > 0 ? timeZone.trim() : DEFAULT_TIME_ZONE;
30
- }
31
-
32
- function findTimestampInDay(targetYmd, timeZone) {
33
- const year = Math.floor(targetYmd / 10000);
34
- const month = Math.floor((targetYmd % 10000) / 100);
35
- const day = targetYmd % 100;
36
- // 取当天正午作为锚点,再向前/后逐小时修正,确保落在目标日期内
37
- let utc = Date.UTC(year, month - 1, day, 12, 0, 0, 0);
38
- const ymdAtUtc = getDailyDateYmdNumber(utc, timeZone);
39
-
40
- if (ymdAtUtc === targetYmd) {
41
- return utc;
42
- }
43
-
44
- const step = ymdAtUtc < targetYmd ? 3600000 : -3600000;
45
-
46
- do {
47
- utc += step;
48
- } while (getDailyDateYmdNumber(utc, timeZone) !== targetYmd);
49
-
50
- return utc;
51
- }
52
-
53
- function getWeekdayIndex(timestamp, timeZone) {
54
- const formatter = new Intl.DateTimeFormat("en-US", {
55
- timeZone: timeZone,
56
- weekday: "short"
57
- });
58
- const parts = formatter.formatToParts(timestamp);
59
- const shortWeekday = parts.find((item) => item.type === "weekday").value;
60
- const map = {
61
- Sun: 0,
62
- Mon: 1,
63
- Tue: 2,
64
- Wed: 3,
65
- Thu: 4,
66
- Fri: 5,
67
- Sat: 6
68
- };
69
-
70
- return map[shortWeekday] ?? 0;
71
- }
72
-
73
- export function getDailyDateYmdNumber(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
74
- const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
75
-
76
- return buildYmdNumber(parts.year, parts.month, parts.day);
77
- }
78
-
79
- export function getDailyDayStartTime(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
80
- const zone = normalizeTimeZone(timeZone);
81
- const targetYmd = getDailyDateYmdNumber(timestamp, zone);
82
- const timestampInDay = findTimestampInDay(targetYmd, zone);
83
- let low = timestampInDay - DAY_MS;
84
- let high = timestampInDay + DAY_MS;
85
-
86
- while (getDailyDateYmdNumber(low, zone) >= targetYmd) {
87
- low -= DAY_MS;
88
- }
89
-
90
- while (getDailyDateYmdNumber(high, zone) <= targetYmd) {
91
- high += DAY_MS;
92
- }
93
-
94
- while (low + 1 < high) {
95
- const mid = Math.floor((low + high) / 2);
96
-
97
- if (getDailyDateYmdNumber(mid, zone) < targetYmd) {
98
- low = mid;
99
- } else {
100
- high = mid;
101
- }
102
- }
103
-
104
- return high;
105
- }
106
-
107
- export function getDailyMonthStartDate(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
108
- const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
109
-
110
- return buildYmdNumber(parts.year, parts.month, 1);
111
- }
112
-
113
- export function getDailyWeekStartDate(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
114
- const zone = normalizeTimeZone(timeZone);
115
- let dayStart = getDailyDayStartTime(timestamp, zone);
116
-
117
- while (getWeekdayIndex(dayStart, zone) !== 1) {
118
- dayStart = getDailyDayStartTime(dayStart - DAY_MS, zone);
119
- }
120
-
121
- return getDailyDateYmdNumber(dayStart, zone);
122
- }
123
-
124
- export function getDailyRecentDateList(timestamp = Date.now(), limit = 30, timeZone = DEFAULT_TIME_ZONE) {
125
- const zone = normalizeTimeZone(timeZone);
126
- const dayStart = getDailyDayStartTime(timestamp, zone);
127
- const list = [];
128
-
129
- for (let i = limit - 1; i >= 0; i -= 1) {
130
- list.push(getDailyDateYmdNumber(dayStart - i * DAY_MS, zone));
131
- }
132
-
133
- return list;
134
- }
135
-
136
- export function getDailyDayStartTimeForYmd(reportDate, timeZone = DEFAULT_TIME_ZONE) {
137
- return getDailyDayStartTime(findTimestampInDay(reportDate, normalizeTimeZone(timeZone)), timeZone);
138
- }
139
-
140
- export function getDailyDateRangeList(startDateYmd, endDateYmd, timeZone = DEFAULT_TIME_ZONE) {
141
- const zone = normalizeTimeZone(timeZone);
142
- const startDayStart = getDailyDayStartTimeForYmd(startDateYmd, zone);
143
- const endDayStart = getDailyDayStartTimeForYmd(endDateYmd, zone);
144
- const list = [];
145
-
146
- for (let t = startDayStart; t <= endDayStart; t += DAY_MS) {
147
- list.push(getDailyDateYmdNumber(t, zone));
148
- }
149
-
150
- return list;
151
- }
152
-
153
- export function getDailyStatsDayMembersKey(reportDate) {
154
- return `daily:day:${reportDate}:members`;
155
- }
156
-
157
- export function getDailyStatsDayProductMembersKey(reportDate, productCode) {
158
- return `daily:day:${reportDate}:product:${encodeURIComponent(productCode)}:members`;
159
- }