hifun-tools 1.3.28 → 1.3.30

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/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./request";
2
2
  export * from "./init";
3
3
  export * from "./utils";
4
+ export * from "./msg";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./request";
2
2
  export * from "./init";
3
3
  export * from "./utils";
4
+ export * from "./msg";
@@ -1,42 +1,57 @@
1
- import { filterSmartLines, getOptimalDecodedString, isDomainMatch, matchBrowser, TenantDict, toStandardUrl } from "./utils";
1
+ import { isDomainMatch, filterSmartLines, getOptimalDecodedString, matchBrowser, TenantDict, toStandardUrl } from "./utils";
2
2
  import { AesDecrypt, AesEncrypt } from "./ende";
3
- type TenantConfig = {
4
- PUBLIC_TITLE: string;
5
- SITE_TITLE: string;
6
- SITE_URL: string;
7
- USER_TENANT: string;
8
- };
9
3
  declare class InitCls {
4
+ AesDecrypt: typeof AesDecrypt;
5
+ AesEncrypt: typeof AesEncrypt;
6
+ private appLine;
7
+ private appTenant;
8
+ private defaultBaseUrl;
10
9
  private domainBaseUrl;
10
+ private initialized;
11
+ private localPath;
12
+ private tenant;
11
13
  private tenantConfig;
12
14
  private tenantDict;
13
15
  private tenantDictList;
14
- private initialized;
15
- private tenant;
16
- private defaultBaseUrl;
17
- private localPath;
18
- getBaseUrl(): string;
19
- /** 获取配置 */
16
+ /** 初始化配置入口 */
17
+ InitConfig(options: {
18
+ fileType?: string[];
19
+ defaultBaseUrl?: string;
20
+ localPath?: string;
21
+ appLine?: string;
22
+ appTenant?: string;
23
+ }): Promise<(string | null)[]>;
24
+ /** 是否 App 环境 */
25
+ getIsApp(): boolean;
26
+ /** 获取租户名 */
27
+ getTenant(): string;
28
+ /** 获取租户配置 */
20
29
  getTenantConfig(): TenantConfig | null;
30
+ /** 获取租户字典 */
21
31
  getTenantDict(): TenantDict | null;
32
+ /** 获取租户字典列表 */
22
33
  getTenantDictList(): TenantDict[];
23
- /** 获取租户名 */
24
- getTenant(): string;
34
+ /** 获取基础 URL */
35
+ getBaseUrl(): string;
36
+ /** 获取图片路径 */
25
37
  getImgPath(imgName: string): string;
38
+ /** 严格初始化租户信息 */
39
+ private _initializeTenant;
26
40
  /** 严格同步获取租户信息 */
27
- private getTenantInfoStrictSync;
28
- /** 严格初始化(必须在应用启动时调用) */
29
- private initialize;
30
- InitConfig({ fileType, defaultBaseUrl, localPath, appDomain, appTenant, }: {
31
- fileType?: string[];
32
- defaultBaseUrl?: string;
33
- localPath?: string;
34
- appDomain?: string;
35
- appTenant?: string;
36
- }): Promise<any[]>;
37
- private LoadGatewayConfig;
38
- AesEncrypt: typeof AesEncrypt;
39
- AesDecrypt: typeof AesDecrypt;
41
+ private _getTenantInfoStrictSync;
42
+ /** 匹配默认租户 */
43
+ private _matchDefaultTenant;
44
+ refreshHttp(): void;
45
+ /** 获取 lineDict.txt 内容 */
46
+ private _fetchLineDict;
47
+ /** 获取并处理 lineAddress.txt */
48
+ private _loadGatewayConfig;
40
49
  }
41
50
  declare const HF: InitCls;
51
+ export type TenantConfig = {
52
+ PUBLIC_TITLE: string;
53
+ SITE_TITLE: string;
54
+ SITE_URL: string;
55
+ USER_TENANT: string;
56
+ };
42
57
  export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt, filterSmartLines, matchBrowser, getOptimalDecodedString, };
@@ -1,188 +1,216 @@
1
+ // index.ts
1
2
  import { getResource } from "./getResource";
2
- import { filterSmartLines, getOptimalDecodedString, isDomainMatch, matchBrowser, toStandardUrl, } from "./utils";
3
+ import { isDomainMatch, filterSmartLines, getOptimalDecodedString, matchBrowser, toStandardUrl, difArr, } from "./utils";
3
4
  import { AesDecrypt, AesEncrypt } from "./ende";
5
+ import { rewardMsg } from "../msg";
4
6
  class InitCls {
7
+ AesDecrypt = AesDecrypt;
8
+ AesEncrypt = AesEncrypt;
9
+ appLine = "";
10
+ appTenant = "";
11
+ defaultBaseUrl = "";
5
12
  domainBaseUrl = "";
13
+ initialized = false;
14
+ localPath = "";
15
+ tenant = "";
6
16
  tenantConfig = null;
7
17
  tenantDict = null;
8
18
  tenantDictList = [];
9
- initialized = false;
10
- tenant = "";
11
- defaultBaseUrl = "";
12
- localPath = "";
13
- getBaseUrl() {
14
- if (!this.domainBaseUrl) {
15
- throw new Error("域名尚未初始化或初始化失败");
19
+ /** 初始化配置入口 */
20
+ async InitConfig(options) {
21
+ const { fileType, defaultBaseUrl, localPath, appLine, appTenant } = options;
22
+ if (defaultBaseUrl)
23
+ this.defaultBaseUrl = defaultBaseUrl;
24
+ if (localPath)
25
+ this.localPath = localPath;
26
+ if (appLine && appTenant) {
27
+ this.appLine = appLine;
28
+ this.appTenant = appTenant;
29
+ console.info("📱 appLine:", appLine, "appTenant:", appTenant);
16
30
  }
17
- return this.domainBaseUrl;
31
+ console.info("📝 加载类型:", fileType);
32
+ const results = [];
33
+ if (fileType?.includes("lineDict")) {
34
+ console.info("初始化 lineDict...");
35
+ results.push(await this._initializeTenant());
36
+ }
37
+ if (fileType?.includes("lineAddress")) {
38
+ console.info("初始化 lineAddress...");
39
+ results.push(await this._loadGatewayConfig());
40
+ }
41
+ console.info("✅ 所有初始化完成:", results);
42
+ return results;
18
43
  }
19
- /** 获取配置 */
44
+ /** 是否 App 环境 */
45
+ getIsApp() {
46
+ return !!this.appTenant && !!this.appLine;
47
+ }
48
+ /** 获取租户名 */
49
+ getTenant() {
50
+ return this.tenant;
51
+ }
52
+ /** 获取租户配置 */
20
53
  getTenantConfig() {
21
54
  if (!this.initialized || !this.tenant) {
22
- throw new Error("租户尚未初始化或初始化失败getTenantConfig");
55
+ throw new Error("租户尚未初始化");
23
56
  }
24
57
  return this.tenantConfig;
25
58
  }
59
+ /** 获取租户字典 */
26
60
  getTenantDict() {
27
61
  if (!this.initialized || !this.tenant) {
28
- throw new Error("租户尚未初始化或初始化失败getTenantDict");
62
+ throw new Error("租户尚未初始化");
29
63
  }
30
64
  return this.tenantDict;
31
65
  }
66
+ /** 获取租户字典列表 */
32
67
  getTenantDictList() {
33
68
  if (!this.initialized || !this.tenant) {
34
- throw new Error("租户尚未初始化或初始化失败getTenantDictList");
69
+ throw new Error("租户尚未初始化");
35
70
  }
36
71
  return this.tenantDictList;
37
72
  }
38
- /** 获取租户名 */
39
- getTenant() {
40
- return this.tenant;
73
+ /** 获取基础 URL */
74
+ getBaseUrl() {
75
+ if (!this.domainBaseUrl)
76
+ throw new Error("域名尚未初始化");
77
+ return this.domainBaseUrl;
41
78
  }
79
+ /** 获取图片路径 */
42
80
  getImgPath(imgName) {
43
81
  if (!this.initialized || !this.tenant) {
44
- throw new Error("租户尚未初始化或初始化失败getImgPath");
82
+ throw new Error("租户尚未初始化");
45
83
  }
46
84
  return getResource(`${this.tenant}/image/${imgName}`);
47
85
  }
48
- /** 严格同步获取租户信息 */
49
- async getTenantInfoStrictSync() {
50
- try {
51
- const response = await fetch(`/lineDict.txt?t=${Date.now()}`, {
52
- method: "GET",
53
- cache: "no-store",
54
- });
55
- if (!response.ok) {
56
- throw new Error(`HTTP 状态码错误: ${response.status}`);
57
- }
58
- const rawText = await response.text();
59
- const rawData = AesDecrypt(rawText);
60
- const data = JSON.parse(rawData);
61
- const host = location.host;
62
- const matchedEntry = data.find((item) => {
63
- return isDomainMatch([item.line], host);
64
- });
65
- this.tenantDictList = data;
66
- if (matchedEntry) {
67
- this.tenantDict = matchedEntry;
68
- console.info(`🏠 匹配租户: ${matchedEntry.tenant}`);
69
- return { tenant: matchedEntry.tenant };
70
- }
71
- // 默认租户匹配
72
- if (host.includes("iggame") || host.includes("localhost")) {
73
- console.info(`🏠 iggame匹配到默认租户2`);
74
- return { tenant: "iggame" };
75
- }
76
- throw new Error("未找到匹配租户");
77
- }
78
- catch (error) {
79
- console.error("解析 lineDict.txt 失败:", error);
80
- const host = location.host;
81
- if (host.includes("iggame") ||
82
- host.includes("localhost") ||
83
- (!!this.localPath &&
84
- location.origin.includes(this.localPath.replace(/\/+$/, "")))) {
85
- console.info(`🏠 iggame匹配到默认租户1`);
86
- return { tenant: "iggame" };
87
- }
88
- throw new Error("无法获取有效的租户信息");
89
- }
90
- }
91
- /** 严格初始化(必须在应用启动时调用) */
92
- async initialize(tenant) {
86
+ /** 严格初始化租户信息 */
87
+ async _initializeTenant() {
93
88
  if (this.initialized)
94
89
  return this.tenant;
95
90
  try {
96
- if (tenant)
97
- this.tenant = tenant;
98
- else {
99
- const tenantInfo = await this.getTenantInfoStrictSync();
100
- this.tenant = tenantInfo.tenant;
101
- }
91
+ const tenantInfo = await this._getTenantInfoStrictSync();
92
+ this.tenant = tenantInfo.tenant;
102
93
  this.tenantConfig = getResource(`${this.tenant}/config.json`);
103
94
  this.initialized = true;
104
- console.info(`🏢 严格加载租户成功: ${this.tenant}`);
95
+ console.info("🏢 租户初始化成功:", this.tenant);
105
96
  return this.tenant;
106
97
  }
107
- catch (error) {
108
- console.error("严格租户加载失败:", error);
109
- throw error;
98
+ catch (err) {
99
+ console.error("❌ 租户初始化失败:", err);
100
+ throw err;
110
101
  }
111
102
  }
112
- async InitConfig({ fileType, defaultBaseUrl, localPath, appDomain, appTenant, }) {
113
- if (appDomain && appTenant) {
114
- this.initialize(appTenant);
115
- this.domainBaseUrl = appDomain;
116
- const results = [this.tenant, this.domainBaseUrl];
117
- console.log("所有初始化完成:", results);
118
- return results;
119
- }
120
- if (defaultBaseUrl) {
121
- this.defaultBaseUrl = defaultBaseUrl;
103
+ /** 严格同步获取租户信息 */
104
+ async _getTenantInfoStrictSync() {
105
+ try {
106
+ const rawText = await this._fetchLineDict();
107
+ const data = JSON.parse(AesDecrypt(rawText));
108
+ const host = location.host;
109
+ this.tenantDictList = [...data];
110
+ if (this.getIsApp()) {
111
+ this.tenantDict = {
112
+ browserCheck: [],
113
+ line: host,
114
+ lineGroup: this.appLine,
115
+ tenant: this.appTenant,
116
+ };
117
+ this.tenantDictList.push(this.tenantDict);
118
+ console.info("🏠 匹配 App 租户:", this.tenantDict);
119
+ return { tenant: this.appTenant };
120
+ }
121
+ const matched = data.find((item) => isDomainMatch([item.line], host));
122
+ if (matched) {
123
+ this.tenantDict = matched;
124
+ console.info("🏠 匹配租户:", matched.tenant);
125
+ return { tenant: matched.tenant };
126
+ }
127
+ return this._matchDefaultTenant();
122
128
  }
123
- if (localPath) {
124
- this.localPath = localPath;
129
+ catch (err) {
130
+ console.warn("⚠️ 解析 lineDict.txt 失败:", err);
131
+ return this._matchDefaultTenant();
125
132
  }
126
- console.log("加载类型:", fileType);
127
- const results = [];
128
- if (fileType?.includes("lineDict")) {
129
- console.log("初始化 lineDict...");
130
- const res1 = await this.initialize();
131
- results.push(res1);
133
+ }
134
+ /** 匹配默认租户 */
135
+ _matchDefaultTenant() {
136
+ const host = location.host;
137
+ if (host.includes("iggame") ||
138
+ host.includes("localhost") ||
139
+ (!!this.localPath &&
140
+ location.origin.includes(this.localPath.replace(/\/+$/, "")))) {
141
+ console.info("🏠 匹配默认租户 iggame");
142
+ return { tenant: "iggame" };
143
+ }
144
+ throw new Error("无法获取有效的租户信息");
145
+ }
146
+ refreshHttp() {
147
+ rewardMsg({
148
+ title: "提示",
149
+ text: "网络环境异常,刷新页面以重置",
150
+ onSubmit() {
151
+ location.reload();
152
+ },
153
+ });
154
+ try {
155
+ getOptimalDecodedString([this.getBaseUrl()]);
132
156
  }
133
- if (fileType?.includes("lineAddress")) {
134
- console.log("初始化 lineAddress...");
135
- const res2 = await this.LoadGatewayConfig();
136
- results.push(res2);
157
+ catch (error) {
158
+ console.log(error);
137
159
  }
138
- console.log("所有初始化完成:", results);
139
- return results;
140
160
  }
141
- async LoadGatewayConfig() {
161
+ /** 获取 lineDict.txt 内容 */
162
+ async _fetchLineDict() {
163
+ const response = await fetch(`/lineDict.txt?t=${Date.now()}`, {
164
+ method: "GET",
165
+ cache: "no-store",
166
+ });
167
+ if (!response.ok)
168
+ throw new Error(`HTTP 错误: ${response.status}`);
169
+ return response.text();
170
+ }
171
+ /** 获取并处理 lineAddress.txt */
172
+ async _loadGatewayConfig() {
142
173
  if (this.domainBaseUrl)
143
174
  return this.domainBaseUrl;
144
175
  try {
145
176
  const response = await fetch(`/lineAddress.txt?t=${Date.now()}`);
146
177
  const configText = await response.text();
147
- let baseUrl = JSON.parse(AesDecrypt(configText));
148
- const { lineGroup } = this.getTenantDict();
178
+ const OriginBaseUrl = JSON.parse(AesDecrypt(configText));
149
179
  const dictList = this.getTenantDictList();
150
- baseUrl = filterSmartLines(dictList, baseUrl, lineGroup);
151
- if (Array.isArray(baseUrl)) {
180
+ const lineGroup = this.getTenantDict()?.lineGroup;
181
+ let baseUrl = filterSmartLines(dictList, OriginBaseUrl, lineGroup);
182
+ try {
183
+ this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(baseUrl));
184
+ console.info("✅ 成功加载生产环境配置:", this.domainBaseUrl);
185
+ }
186
+ catch {
187
+ // 备用策略
188
+ let allBaseUrl = filterSmartLines(dictList, OriginBaseUrl);
152
189
  try {
153
- this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(baseUrl));
154
- console.log("✅ 成功加载生产环境配置(测速选择):", this.domainBaseUrl);
190
+ this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(difArr(allBaseUrl, baseUrl)));
191
+ console.info("✅ 备用配置成功:", this.domainBaseUrl);
155
192
  }
156
- catch (error) {
157
- this.domainBaseUrl = toStandardUrl(baseUrl[0]);
158
- console.warn("⚠️ 测速失败,使用第一个配置:", this.domainBaseUrl, error);
193
+ catch {
194
+ this.domainBaseUrl = toStandardUrl(allBaseUrl[0]);
195
+ console.warn("⚠️ 备选测速失败,使用备用配置:", allBaseUrl);
159
196
  }
160
197
  }
161
- else {
162
- this.domainBaseUrl = toStandardUrl(baseUrl);
163
- console.log("✅ 成功加载生产环境配置:", this.domainBaseUrl);
164
- }
165
198
  return this.domainBaseUrl;
166
199
  }
167
- catch (error) {
168
- console.error(error);
200
+ catch (err) {
201
+ console.error("⚠️ 加载 lineAddress.txt 失败:", err);
169
202
  if ((this.defaultBaseUrl &&
170
203
  (location.host.includes("iggame") ||
171
204
  location.host.includes("localhost"))) ||
172
205
  (!!this.localPath &&
173
206
  location.origin.includes(this.localPath.replace(/\/+$/, "")))) {
174
- console.info(`🏠 iggame匹配到默认链接`, this.defaultBaseUrl);
175
207
  this.domainBaseUrl = toStandardUrl(this.defaultBaseUrl);
208
+ console.info("🏠 使用默认 BaseUrl:", this.domainBaseUrl);
176
209
  return this.domainBaseUrl;
177
210
  }
178
- else {
179
- console.warn("⚠️ 加载 lineAddress.txt 失败:", error);
180
- return null;
181
- }
211
+ return null;
182
212
  }
183
213
  }
184
- AesEncrypt = AesEncrypt;
185
- AesDecrypt = AesDecrypt;
186
214
  }
187
215
  const HF = new InitCls();
188
216
  export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt, filterSmartLines, matchBrowser, getOptimalDecodedString, };
@@ -4,9 +4,28 @@ export type TenantDict = {
4
4
  lineGroup: string;
5
5
  tenant: string;
6
6
  };
7
- export declare function isIPv4(str: string): boolean;
8
- export declare function getOptimalDecodedString(encodedArray: string[]): Promise<string>;
7
+ /** 保留端口,去掉协议 */
8
+ export declare function normalizeHost(hostname: string): string;
9
+ /** 域名严格匹配,允许 www 特例 */
10
+ export declare function isDomainStrictEqual(a: string, b: string): boolean;
9
11
  export declare function isDomainMatch(list: string[], target: string): boolean;
10
- export declare function toStandardUrl(input: string): string;
12
+ /** 检查是否 IPv4 */
13
+ export declare function isIPv4(str: string): boolean;
14
+ export declare function detectBrowser(): string;
15
+ /** 匹配浏览器 */
11
16
  export declare function matchBrowser(checkItem: string, currentBrowser: string): boolean;
12
- export declare function filterSmartLines(list1: TenantDict[], list2: string[], groupType?: string, tenant?: string): string[];
17
+ /** 标准化 URL */
18
+ export declare function toStandardUrl(input: string): string;
19
+ /** 根据浏览器和租户过滤线路 */
20
+ export declare function filterSmartLines(tenantList: TenantDict[], lineList: string[], groupType?: string, tenant?: string): string[];
21
+ /** 并发测速获取最佳 URL(限制超时和并发) */
22
+ export declare function getOptimalDecodedString(arr: string[], options?: {
23
+ error?: (arr: string[]) => void;
24
+ filterIp?: boolean;
25
+ }): Promise<string>;
26
+ /**
27
+ * 返回两个数组的差集(对称差集)
28
+ * @param arr1 第一个数组
29
+ * @param arr2 第二个数组
30
+ */
31
+ export declare function difArr(arr1: string[], arr2: string[]): string[];
@@ -1,131 +1,45 @@
1
- export function isIPv4(str) {
2
- if (str.startsWith("http://") || str.startsWith("https://")) {
3
- try {
4
- const url = new URL(str);
5
- str = url.hostname;
6
- }
7
- catch {
8
- return false;
9
- }
10
- }
11
- if (str === "localhost")
12
- return false;
13
- const ipv4Regex = /^(25[0-5]|2[0-4]\d|1?\d{1,2})(\.(25[0-5]|2[0-4]\d|1?\d{1,2})){3}$/;
14
- return ipv4Regex.test(str);
15
- }
16
- export async function getOptimalDecodedString(encodedArray) {
17
- if (!encodedArray?.length)
18
- throw new Error("输入数组为空或无效");
19
- const hostname = window.location.hostname;
20
- const isIp = isIPv4(hostname);
21
- const decodedList = encodedArray
22
- .filter(Boolean)
23
- .map((v) => v.trim());
24
- const ipList = decodedList.filter((v) => isIPv4(v));
25
- const domainList = decodedList.filter((v) => !isIPv4(v));
26
- const getOptimal = (arr) => new Promise((resolve) => {
27
- if (!arr.length) {
28
- resolve("");
29
- return;
30
- }
31
- const currentProtocol = window.location.protocol;
32
- const filteredArr = arr.filter((url) => url.startsWith("http") ? url.startsWith(currentProtocol) : true);
33
- const targetArr = filteredArr.length ? filteredArr : arr;
34
- if (targetArr.length === 1)
35
- return resolve(targetArr[0]);
36
- let hasResolved = false;
37
- targetArr.forEach((url) => {
38
- const testUrl = `${toStandardUrl(url)}/health?t=${Date.now()}`;
39
- const controller = new AbortController();
40
- const timeoutId = setTimeout(() => {
41
- controller.abort();
42
- }, 3000);
43
- fetch(testUrl, { method: "GET", signal: controller.signal })
44
- .then(() => {
45
- clearTimeout(timeoutId);
46
- if (!hasResolved) {
47
- hasResolved = true;
48
- resolve(url);
49
- }
50
- })
51
- .catch(() => clearTimeout(timeoutId));
52
- });
53
- setTimeout(() => {
54
- if (!hasResolved) {
55
- hasResolved = true;
56
- resolve(targetArr[0]);
57
- }
58
- }, 3500);
59
- });
60
- return isIp ? await getOptimal(ipList) : await getOptimal(domainList);
61
- }
62
- /**
63
- * normalizeHost:保留端口,只清理协议
64
- */
65
- function normalizeHost(hostname) {
1
+ /** 浏览器识别(结果缓存) */
2
+ let cachedBrowser = null;
3
+ /** 保留端口,去掉协议 */
4
+ export function normalizeHost(hostname) {
66
5
  hostname = hostname.trim().toLowerCase();
67
- // 如果没有协议,加上 http:// 方便 URL 解析
68
- let urlStr = hostname;
69
- if (!/^https?:\/\//i.test(hostname)) {
70
- urlStr = "http://" + hostname;
71
- }
6
+ const urlStr = /^https?:\/\//i.test(hostname)
7
+ ? hostname
8
+ : `http://${hostname}`;
72
9
  try {
73
10
  const url = new URL(urlStr);
74
- // 保留 hostname + port
75
11
  return url.port ? `${url.hostname}:${url.port}` : url.hostname;
76
12
  }
77
- catch (_) {
78
- return hostname; // 保留原始字符串
13
+ catch {
14
+ return hostname;
79
15
  }
80
16
  }
81
- /**
82
- * 域名严格匹配,允许 www 特例
83
- */
84
- function isDomainStrictEqual(a, b) {
85
- a = normalizeHost(a);
86
- b = normalizeHost(b);
87
- if (a === b)
88
- return true;
17
+ /** 域名严格匹配,允许 www 特例 */
18
+ export function isDomainStrictEqual(a, b) {
89
19
  const stripWww = (h) => (h.startsWith("www.") ? h.slice(4) : h);
90
- return stripWww(a) === stripWww(b);
20
+ return stripWww(normalizeHost(a)) === stripWww(normalizeHost(b));
91
21
  }
92
22
  export function isDomainMatch(list, target) {
93
23
  const t = normalizeHost(target);
94
- return list.some((item) => {
95
- const h = normalizeHost(item);
96
- return isDomainStrictEqual(h, t);
97
- });
24
+ return list.some((item) => isDomainStrictEqual(item, t));
98
25
  }
99
- export function toStandardUrl(input) {
100
- try {
101
- if (!input)
102
- return "";
103
- input = input.trim();
104
- let protocol = window?.location?.protocol || "https:";
105
- if (!/^https?:\/\//i.test(input)) {
106
- input = protocol + "//" + input;
26
+ /** 检查是否 IPv4 */
27
+ export function isIPv4(str) {
28
+ if (str.startsWith("http")) {
29
+ try {
30
+ str = new URL(str).hostname;
107
31
  }
108
- const url = new URL(input);
109
- const hostname = url.hostname;
110
- const port = url.port;
111
- const isHostnameValid = hostname === "localhost" ||
112
- (/^[a-zA-Z0-9.-]+$/.test(hostname) &&
113
- (hostname.includes(".") || isIPv4(hostname)));
114
- if (!isHostnameValid) {
115
- return "";
32
+ catch {
33
+ return false;
116
34
  }
117
- return port
118
- ? `${url.protocol}//${hostname}:${port}`
119
- : `${url.protocol}//${hostname}`;
120
- }
121
- catch (e) {
122
- return "";
123
35
  }
36
+ if (str === "localhost")
37
+ return false;
38
+ return /^(25[0-5]|2[0-4]\d|1?\d{1,2})(\.(25[0-5]|2[0-4]\d|1?\d{1,2})){3}$/.test(str);
124
39
  }
125
- /**
126
- * 浏览器识别
127
- */
128
- function detectBrowser() {
40
+ export function detectBrowser() {
41
+ if (cachedBrowser)
42
+ return cachedBrowser;
129
43
  const ua = navigator.userAgent.toLowerCase();
130
44
  const rules = [
131
45
  { key: "baidu", regex: /baidubrowser|baiduboxapp|baidu/i },
@@ -148,43 +62,128 @@ function detectBrowser() {
148
62
  { key: "alipay", regex: /alipayclient/i },
149
63
  { key: "douyin", regex: /aweme|douyin/i },
150
64
  ];
151
- for (const item of rules) {
152
- if (item.exclude && item.exclude.test(ua))
65
+ for (const { key, regex, exclude } of rules) {
66
+ if (exclude && exclude.test(ua))
153
67
  continue;
154
- if (item.regex.test(ua))
155
- return item.key;
68
+ if (regex.test(ua))
69
+ return (cachedBrowser = key);
156
70
  }
157
- return "unknown";
71
+ return (cachedBrowser = "unknown");
158
72
  }
73
+ /** 匹配浏览器 */
159
74
  export function matchBrowser(checkItem, currentBrowser) {
160
75
  if (!checkItem || !currentBrowser)
161
76
  return false;
162
- const normalized = checkItem.toString().toLowerCase();
77
+ const normalized = checkItem.toLowerCase();
163
78
  const browser = currentBrowser.toLowerCase();
164
79
  if (normalized.startsWith("/") && normalized.endsWith("/")) {
165
80
  try {
166
- const body = normalized.slice(1, -1);
167
- const regex = new RegExp(body, "i");
168
- return regex.test(browser);
81
+ return new RegExp(normalized.slice(1, -1), "i").test(browser);
169
82
  }
170
- catch (_) { }
83
+ catch { }
171
84
  }
172
85
  return browser.includes(normalized);
173
86
  }
174
- export function filterSmartLines(list1, list2, groupType, tenant) {
175
- if (!Array.isArray(list1) || !Array.isArray(list2)) {
176
- throw new Error("前两个参数必须是数组");
87
+ /** 标准化 URL */
88
+ export function toStandardUrl(input) {
89
+ if (!input)
90
+ return "";
91
+ input = input.trim();
92
+ const protocol = window?.location?.protocol || "https:";
93
+ if (!/^https?:\/\//i.test(input))
94
+ input = `${protocol}//${input}`;
95
+ try {
96
+ const url = new URL(input);
97
+ const hostname = url.hostname;
98
+ const port = url.port;
99
+ if (hostname !== "localhost" &&
100
+ !(/^[a-zA-Z0-9.-]+$/.test(hostname) &&
101
+ (hostname.includes(".") || isIPv4(hostname)))) {
102
+ return "";
103
+ }
104
+ return port
105
+ ? `${url.protocol}//${hostname}:${port}`
106
+ : `${url.protocol}//${hostname}`;
177
107
  }
108
+ catch {
109
+ return "";
110
+ }
111
+ }
112
+ /** 根据浏览器和租户过滤线路 */
113
+ export function filterSmartLines(tenantList, lineList, groupType, tenant) {
114
+ if (!Array.isArray(tenantList) || !Array.isArray(lineList))
115
+ throw new Error("参数必须是数组");
178
116
  const currentBrowser = detectBrowser();
179
- let result = list1.filter((item) => {
180
- const checks = item.browserCheck || [];
181
- const blocked = checks.some((checkItem) => matchBrowser(checkItem, currentBrowser));
182
- return !blocked;
183
- });
117
+ let filtered = tenantList.filter((item) => !(item.browserCheck || []).some((check) => matchBrowser(check, currentBrowser)));
184
118
  if (tenant)
185
- result = result.filter((item) => item.tenant === tenant);
119
+ filtered = filtered.filter((item) => item.tenant === tenant);
186
120
  if (groupType)
187
- result = result.filter((item) => item.lineGroup === groupType);
188
- const lines = result.map((item) => toStandardUrl(item.line));
189
- return list2.map(toStandardUrl).filter((v) => isDomainMatch(lines, v));
121
+ filtered = filtered.filter((item) => item.lineGroup === groupType);
122
+ const lines = filtered.map((item) => toStandardUrl(item.line));
123
+ return lineList.map(toStandardUrl).filter((v) => isDomainMatch(lines, v));
124
+ }
125
+ /** 并发测速获取最佳 URL(限制超时和并发) */
126
+ export async function getOptimalDecodedString(arr, options) {
127
+ if (!arr?.length)
128
+ throw new Error("输入数组为空");
129
+ const { error, filterIp = true } = options || {};
130
+ const hostname = window.location.hostname;
131
+ const isIp = isIPv4(hostname);
132
+ // 过滤逻辑
133
+ let targetArr = arr.filter(Boolean).map((v) => v.trim());
134
+ if (filterIp) {
135
+ targetArr = isIp
136
+ ? targetArr.filter(isIPv4)
137
+ : targetArr.filter((v) => !isIPv4(v));
138
+ }
139
+ if (!targetArr.length)
140
+ throw new Error("无可用地址");
141
+ const failedUrls = [];
142
+ const controllerMap = {};
143
+ return new Promise((resolve, reject) => {
144
+ let resolved = false;
145
+ targetArr.forEach((url) => {
146
+ const controller = new AbortController();
147
+ controllerMap[url] = controller;
148
+ const timeoutId = setTimeout(() => {
149
+ controller.abort();
150
+ failedUrls.push(url);
151
+ }, 3000);
152
+ fetch(`${toStandardUrl(url)}/health?t=${Date.now()}`, {
153
+ signal: controller.signal,
154
+ })
155
+ .then(() => {
156
+ clearTimeout(timeoutId);
157
+ if (!resolved) {
158
+ resolved = true;
159
+ resolve(url);
160
+ }
161
+ })
162
+ .catch(() => {
163
+ clearTimeout(timeoutId);
164
+ failedUrls.push(url);
165
+ });
166
+ });
167
+ // 等待所有请求完成
168
+ setTimeout(() => {
169
+ if (failedUrls.length && error) {
170
+ error(failedUrls);
171
+ }
172
+ if (!resolved) {
173
+ reject({ message: "测速全部失败", failedUrls });
174
+ }
175
+ }, 3500);
176
+ });
177
+ }
178
+ /**
179
+ * 返回两个数组的差集(对称差集)
180
+ * @param arr1 第一个数组
181
+ * @param arr2 第二个数组
182
+ */
183
+ export function difArr(arr1, arr2) {
184
+ const set1 = new Set(arr1.map((v) => v.trim()));
185
+ const set2 = new Set(arr2.map((v) => v.trim()));
186
+ const diff1 = [...set1].filter((v) => !set2.has(v));
187
+ const diff2 = [...set2].filter((v) => !set1.has(v));
188
+ return [...diff1, ...diff2];
190
189
  }
@@ -0,0 +1,18 @@
1
+ export interface MsgOptions {
2
+ type?: "info" | "success" | "warning" | "error";
3
+ duration?: number;
4
+ closable?: boolean;
5
+ onClose?: () => void;
6
+ }
7
+ export declare function msg(message: string, options?: MsgOptions): void;
8
+ export interface RewardOptions {
9
+ title?: string;
10
+ text?: string;
11
+ topImg?: string;
12
+ submitText?: string;
13
+ cancelText?: string;
14
+ onSubmit?: () => void;
15
+ onCancel?: () => void;
16
+ onClose?: () => void;
17
+ }
18
+ export declare function rewardMsg(options: RewardOptions): void;
@@ -0,0 +1,262 @@
1
+ // --------------------------- 普通消息 ---------------------------
2
+ let messageStackContainer = null;
3
+ // --------------------------- reward 弹窗 ---------------------------
4
+ let rewardMask = null;
5
+ let rewardStackContainer = null;
6
+ function getMessageStackContainer() {
7
+ if (!messageStackContainer) {
8
+ messageStackContainer = document.createElement("div");
9
+ Object.assign(messageStackContainer.style, {
10
+ position: "fixed",
11
+ bottom: "10vh",
12
+ left: "50%",
13
+ transform: "translateX(-50%)",
14
+ display: "flex",
15
+ flexDirection: "column",
16
+ alignItems: "center",
17
+ gap: "10px",
18
+ zIndex: "9999",
19
+ pointerEvents: "none",
20
+ width: "100%",
21
+ maxWidth: "calc(100% - 40px)",
22
+ });
23
+ document.body.appendChild(messageStackContainer);
24
+ }
25
+ return messageStackContainer;
26
+ }
27
+ function hideMsg(element, callback) {
28
+ element.style.opacity = "0";
29
+ element.style.transform += " translateY(20px)";
30
+ setTimeout(() => {
31
+ element.parentNode?.removeChild(element);
32
+ callback?.();
33
+ }, 300);
34
+ }
35
+ function getColorByType(type) {
36
+ switch (type) {
37
+ case "success":
38
+ return "#67C23A";
39
+ case "warning":
40
+ return "#E6A23C";
41
+ case "error":
42
+ return "#F56C6C";
43
+ case "info":
44
+ return "#409EFF";
45
+ default:
46
+ return "#409EFF";
47
+ }
48
+ }
49
+ function getRewardMask() {
50
+ if (!rewardMask) {
51
+ rewardMask = document.createElement("div");
52
+ Object.assign(rewardMask.style, {
53
+ position: "fixed",
54
+ top: "0",
55
+ left: "0",
56
+ width: "100vw",
57
+ height: "100vh",
58
+ background: "rgba(0,0,0,0.5)", // 使用 rgba,蒙层透明不影响弹窗
59
+ zIndex: "100000",
60
+ pointerEvents: "auto",
61
+ display: "flex",
62
+ justifyContent: "center",
63
+ alignItems: "center",
64
+ });
65
+ document.body.appendChild(rewardMask);
66
+ }
67
+ return rewardMask;
68
+ }
69
+ function getRewardStackContainer() {
70
+ if (!rewardStackContainer) {
71
+ rewardStackContainer = document.createElement("div");
72
+ Object.assign(rewardStackContainer.style, {
73
+ position: "relative",
74
+ display: "flex",
75
+ flexDirection: "column",
76
+ alignItems: "center",
77
+ gap: "12px",
78
+ width: "100%",
79
+ maxWidth: "calc(100% - 40px)",
80
+ });
81
+ getRewardMask().appendChild(rewardStackContainer);
82
+ }
83
+ return rewardStackContainer;
84
+ }
85
+ export function msg(message, options = {}) {
86
+ const { type = "info", duration = 3000, closable = false, onClose } = options;
87
+ const container = document.createElement("div");
88
+ Object.assign(container.style, {
89
+ backgroundColor: getColorByType(type),
90
+ color: "#fff",
91
+ borderRadius: "12px",
92
+ fontSize: "12px",
93
+ fontWeight: "500",
94
+ padding: "10px 16px",
95
+ maxWidth: "400px",
96
+ minWidth: "120px",
97
+ textAlign: "center",
98
+ display: "-webkit-box",
99
+ WebkitLineClamp: "2",
100
+ WebkitBoxOrient: "vertical",
101
+ overflow: "hidden",
102
+ textOverflow: "ellipsis",
103
+ wordWrap: "break-word",
104
+ boxShadow: "0 6px 18px rgba(0,0,0,0.12)",
105
+ opacity: "0",
106
+ transform: "translateY(20px)",
107
+ transition: "all 0.3s cubic-bezier(0.4,0,0.2,1)",
108
+ pointerEvents: "auto",
109
+ position: "relative",
110
+ });
111
+ container.textContent = message;
112
+ if (closable) {
113
+ const closeBtn = document.createElement("span");
114
+ closeBtn.innerHTML = "&times;";
115
+ Object.assign(closeBtn.style, {
116
+ position: "absolute",
117
+ top: "50%",
118
+ right: "10px",
119
+ transform: "translateY(-50%)",
120
+ cursor: "pointer",
121
+ fontSize: "14px",
122
+ fontWeight: "bold",
123
+ });
124
+ closeBtn.onclick = () => hideMsg(container, onClose);
125
+ container.appendChild(closeBtn);
126
+ container.style.paddingRight = "30px";
127
+ }
128
+ const stack = getMessageStackContainer();
129
+ stack.appendChild(container);
130
+ requestAnimationFrame(() => {
131
+ container.style.opacity = "1";
132
+ container.style.transform = "translateY(0)";
133
+ });
134
+ if (duration > 0) {
135
+ setTimeout(() => hideMsg(container, onClose), duration);
136
+ }
137
+ }
138
+ export function rewardMsg(options) {
139
+ const { title, text, topImg, submitText, cancelText, onSubmit, onCancel, onClose, } = options;
140
+ const mask = getRewardMask();
141
+ const originalOverflow = document.body.style.overflow;
142
+ document.body.style.overflow = "hidden"; // 禁止下方滚动
143
+ const container = document.createElement("div");
144
+ container.className = "rewardWindow";
145
+ Object.assign(container.style, {
146
+ position: "relative",
147
+ width: "calc(100% - 60px)",
148
+ maxWidth: "370px",
149
+ paddingTop: topImg ? "88px" : "23px",
150
+ backgroundColor: "#fff",
151
+ borderRadius: "12px",
152
+ boxSizing: "border-box",
153
+ display: "flex",
154
+ flexDirection: "column",
155
+ alignItems: "center",
156
+ textAlign: "center",
157
+ opacity: "0",
158
+ transform: "translateY(-20px) scale(0.9)",
159
+ transition: "all 0.3s cubic-bezier(0.4,0,0.2,1)",
160
+ overflow: "hidden",
161
+ zIndex: "100001",
162
+ });
163
+ // 标题
164
+ if (title) {
165
+ const titleEl = document.createElement("div");
166
+ titleEl.textContent = title;
167
+ Object.assign(titleEl.style, {
168
+ fontSize: "16px",
169
+ fontWeight: "600",
170
+ color: "#303442",
171
+ });
172
+ container.appendChild(titleEl);
173
+ }
174
+ // 顶部图片
175
+ if (topImg) {
176
+ const imgEl = document.createElement("img");
177
+ imgEl.src = topImg;
178
+ Object.assign(imgEl.style, {
179
+ position: "absolute",
180
+ top: "-68px",
181
+ height: "150px",
182
+ width: "150px",
183
+ });
184
+ container.appendChild(imgEl);
185
+ }
186
+ // 文本内容
187
+ if (text) {
188
+ const textEl = document.createElement("div");
189
+ textEl.innerHTML = text;
190
+ Object.assign(textEl.style, {
191
+ padding: "14px 22px 12px",
192
+ fontSize: "12px",
193
+ fontWeight: "500",
194
+ lineHeight: "16px",
195
+ textAlign: "center",
196
+ width: "100%",
197
+ overflow: "hidden",
198
+ wordBreak: "break-word",
199
+ color: "#7981A4",
200
+ });
201
+ container.appendChild(textEl);
202
+ }
203
+ // 按钮容器
204
+ const btnContainer = document.createElement("div");
205
+ Object.assign(btnContainer.style, {
206
+ display: "flex",
207
+ width: "100%",
208
+ height: "44px",
209
+ borderTop: "1px solid #f2f2f6",
210
+ padding: "8px 0",
211
+ });
212
+ const btnStyle = {
213
+ flex: "1",
214
+ border: "none",
215
+ background: "transparent",
216
+ cursor: "pointer",
217
+ fontSize: "14px",
218
+ fontWeight: "500",
219
+ };
220
+ // 仅当传入 cancelText 才显示取消按钮
221
+ if (cancelText) {
222
+ const cancelBtn = document.createElement("button");
223
+ cancelBtn.textContent = cancelText;
224
+ Object.assign(cancelBtn.style, {
225
+ ...btnStyle,
226
+ borderRight: "1px solid #f2f2f6",
227
+ color: "#303442",
228
+ });
229
+ cancelBtn.onclick = () => {
230
+ hideMsg(container, onCancel ?? onClose);
231
+ // 如果这是最后一个弹窗,隐藏蒙层并恢复滚动
232
+ if (rewardStackContainer?.childElementCount === 1) {
233
+ hideMsg(mask);
234
+ document.body.style.overflow = originalOverflow;
235
+ }
236
+ };
237
+ btnContainer.appendChild(cancelBtn);
238
+ }
239
+ // 确认按钮始终显示
240
+ const confirmBtn = document.createElement("button");
241
+ confirmBtn.textContent = submitText || "确认";
242
+ Object.assign(confirmBtn.style, {
243
+ ...btnStyle,
244
+ color: "#179CFF",
245
+ });
246
+ confirmBtn.onclick = () => {
247
+ onSubmit?.();
248
+ hideMsg(container, onClose);
249
+ if (rewardStackContainer?.childElementCount === 1) {
250
+ hideMsg(mask);
251
+ document.body.style.overflow = originalOverflow;
252
+ }
253
+ };
254
+ btnContainer.appendChild(confirmBtn);
255
+ container.appendChild(btnContainer);
256
+ const stack = getRewardStackContainer();
257
+ stack.appendChild(container);
258
+ requestAnimationFrame(() => {
259
+ container.style.opacity = "1";
260
+ container.style.transform = "translateY(0) scale(1)";
261
+ });
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hifun-tools",
3
- "version": "1.3.28",
3
+ "version": "1.3.30",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,9 +17,13 @@
17
17
  "dist"
18
18
  ],
19
19
  "devDependencies": {
20
+ "@babel/core": "^7.28.5",
21
+ "@babel/preset-typescript": "^7.28.5",
20
22
  "@types/jest": "^30.0.0",
21
23
  "jest": "^30.2.0",
22
24
  "jest-environment-jsdom": "^30.2.0",
25
+ "jscodeshift": "^17.3.0",
26
+ "prettier": "^3.6.2",
23
27
  "ts-jest": "^29.4.5",
24
28
  "typescript": "^5.6.3"
25
29
  },