hifun-tools 1.3.29 → 1.3.31

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,215 @@
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;
43
+ }
44
+ /** 是否 App 环境 */
45
+ getIsApp() {
46
+ return !!this.appTenant && !!this.appLine;
18
47
  }
19
- /** 获取配置 */
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
+ try {
148
+ getOptimalDecodedString([this.getBaseUrl()]);
132
149
  }
133
- if (fileType?.includes("lineAddress")) {
134
- console.log("初始化 lineAddress...");
135
- const res2 = await this.LoadGatewayConfig();
136
- results.push(res2);
150
+ catch (error) {
151
+ rewardMsg({
152
+ title: "提示",
153
+ text: "网络环境异常,刷新页面以重置",
154
+ onSubmit() {
155
+ location.reload();
156
+ },
157
+ });
137
158
  }
138
- console.log("所有初始化完成:", results);
139
- return results;
140
159
  }
141
- async LoadGatewayConfig() {
160
+ /** 获取 lineDict.txt 内容 */
161
+ async _fetchLineDict() {
162
+ const response = await fetch(`/lineDict.txt?t=${Date.now()}`, {
163
+ method: "GET",
164
+ cache: "no-store",
165
+ });
166
+ if (!response.ok)
167
+ throw new Error(`HTTP 错误: ${response.status}`);
168
+ return response.text();
169
+ }
170
+ /** 获取并处理 lineAddress.txt */
171
+ async _loadGatewayConfig() {
142
172
  if (this.domainBaseUrl)
143
173
  return this.domainBaseUrl;
144
174
  try {
145
175
  const response = await fetch(`/lineAddress.txt?t=${Date.now()}`);
146
176
  const configText = await response.text();
147
- let baseUrl = JSON.parse(AesDecrypt(configText));
148
- const { lineGroup } = this.getTenantDict();
177
+ const OriginBaseUrl = JSON.parse(AesDecrypt(configText));
149
178
  const dictList = this.getTenantDictList();
150
- baseUrl = filterSmartLines(dictList, baseUrl, lineGroup);
151
- if (Array.isArray(baseUrl)) {
179
+ const lineGroup = this.getTenantDict()?.lineGroup;
180
+ let baseUrl = filterSmartLines(dictList, OriginBaseUrl, lineGroup);
181
+ try {
182
+ this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(baseUrl));
183
+ console.info("✅ 成功加载生产环境配置:", this.domainBaseUrl);
184
+ }
185
+ catch {
186
+ // 备用策略
187
+ let allBaseUrl = filterSmartLines(dictList, OriginBaseUrl);
152
188
  try {
153
- this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(baseUrl));
154
- console.log("✅ 成功加载生产环境配置(测速选择):", this.domainBaseUrl);
189
+ this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(difArr(allBaseUrl, baseUrl)));
190
+ console.info("✅ 备用配置成功:", this.domainBaseUrl);
155
191
  }
156
- catch (error) {
157
- this.domainBaseUrl = toStandardUrl(baseUrl[0]);
158
- console.warn("⚠️ 测速失败,使用第一个配置:", this.domainBaseUrl, error);
192
+ catch {
193
+ this.domainBaseUrl = toStandardUrl(allBaseUrl[0]);
194
+ console.warn("⚠️ 备选测速失败,使用备用配置:", allBaseUrl);
159
195
  }
160
196
  }
161
- else {
162
- this.domainBaseUrl = toStandardUrl(baseUrl);
163
- console.log("✅ 成功加载生产环境配置:", this.domainBaseUrl);
164
- }
165
197
  return this.domainBaseUrl;
166
198
  }
167
- catch (error) {
168
- console.error(error);
199
+ catch (err) {
200
+ console.error("⚠️ 加载 lineAddress.txt 失败:", err);
169
201
  if ((this.defaultBaseUrl &&
170
202
  (location.host.includes("iggame") ||
171
203
  location.host.includes("localhost"))) ||
172
204
  (!!this.localPath &&
173
205
  location.origin.includes(this.localPath.replace(/\/+$/, "")))) {
174
- console.info(`🏠 iggame匹配到默认链接`, this.defaultBaseUrl);
175
206
  this.domainBaseUrl = toStandardUrl(this.defaultBaseUrl);
207
+ console.info("🏠 使用默认 BaseUrl:", this.domainBaseUrl);
176
208
  return this.domainBaseUrl;
177
209
  }
178
- else {
179
- console.warn("⚠️ 加载 lineAddress.txt 失败:", error);
180
- return null;
181
- }
210
+ return null;
182
211
  }
183
212
  }
184
- AesEncrypt = AesEncrypt;
185
- AesDecrypt = AesDecrypt;
186
213
  }
187
214
  const HF = new InitCls();
188
215
  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[], filterIp?: boolean): 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,135 +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, filterIp = true) {
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 !filterIp
61
- ? await getOptimal(decodedList)
62
- : isIp
63
- ? await getOptimal(ipList)
64
- : await getOptimal(domainList);
65
- }
66
- /**
67
- * normalizeHost:保留端口,只清理协议
68
- */
69
- function normalizeHost(hostname) {
1
+ /** 浏览器识别(结果缓存) */
2
+ let cachedBrowser = null;
3
+ /** 保留端口,去掉协议 */
4
+ export function normalizeHost(hostname) {
70
5
  hostname = hostname.trim().toLowerCase();
71
- // 如果没有协议,加上 http:// 方便 URL 解析
72
- let urlStr = hostname;
73
- if (!/^https?:\/\//i.test(hostname)) {
74
- urlStr = "http://" + hostname;
75
- }
6
+ const urlStr = /^https?:\/\//i.test(hostname)
7
+ ? hostname
8
+ : `http://${hostname}`;
76
9
  try {
77
10
  const url = new URL(urlStr);
78
- // 保留 hostname + port
79
11
  return url.port ? `${url.hostname}:${url.port}` : url.hostname;
80
12
  }
81
- catch (_) {
82
- return hostname; // 保留原始字符串
13
+ catch {
14
+ return hostname;
83
15
  }
84
16
  }
85
- /**
86
- * 域名严格匹配,允许 www 特例
87
- */
88
- function isDomainStrictEqual(a, b) {
89
- a = normalizeHost(a);
90
- b = normalizeHost(b);
91
- if (a === b)
92
- return true;
17
+ /** 域名严格匹配,允许 www 特例 */
18
+ export function isDomainStrictEqual(a, b) {
93
19
  const stripWww = (h) => (h.startsWith("www.") ? h.slice(4) : h);
94
- return stripWww(a) === stripWww(b);
20
+ return stripWww(normalizeHost(a)) === stripWww(normalizeHost(b));
95
21
  }
96
22
  export function isDomainMatch(list, target) {
97
23
  const t = normalizeHost(target);
98
- return list.some((item) => {
99
- const h = normalizeHost(item);
100
- return isDomainStrictEqual(h, t);
101
- });
24
+ return list.some((item) => isDomainStrictEqual(item, t));
102
25
  }
103
- export function toStandardUrl(input) {
104
- try {
105
- if (!input)
106
- return "";
107
- input = input.trim();
108
- let protocol = window?.location?.protocol || "https:";
109
- if (!/^https?:\/\//i.test(input)) {
110
- input = protocol + "//" + input;
26
+ /** 检查是否 IPv4 */
27
+ export function isIPv4(str) {
28
+ if (str.startsWith("http")) {
29
+ try {
30
+ str = new URL(str).hostname;
111
31
  }
112
- const url = new URL(input);
113
- const hostname = url.hostname;
114
- const port = url.port;
115
- const isHostnameValid = hostname === "localhost" ||
116
- (/^[a-zA-Z0-9.-]+$/.test(hostname) &&
117
- (hostname.includes(".") || isIPv4(hostname)));
118
- if (!isHostnameValid) {
119
- return "";
32
+ catch {
33
+ return false;
120
34
  }
121
- return port
122
- ? `${url.protocol}//${hostname}:${port}`
123
- : `${url.protocol}//${hostname}`;
124
- }
125
- catch (e) {
126
- return "";
127
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);
128
39
  }
129
- /**
130
- * 浏览器识别
131
- */
132
- function detectBrowser() {
40
+ export function detectBrowser() {
41
+ if (cachedBrowser)
42
+ return cachedBrowser;
133
43
  const ua = navigator.userAgent.toLowerCase();
134
44
  const rules = [
135
45
  { key: "baidu", regex: /baidubrowser|baiduboxapp|baidu/i },
@@ -152,43 +62,128 @@ function detectBrowser() {
152
62
  { key: "alipay", regex: /alipayclient/i },
153
63
  { key: "douyin", regex: /aweme|douyin/i },
154
64
  ];
155
- for (const item of rules) {
156
- if (item.exclude && item.exclude.test(ua))
65
+ for (const { key, regex, exclude } of rules) {
66
+ if (exclude && exclude.test(ua))
157
67
  continue;
158
- if (item.regex.test(ua))
159
- return item.key;
68
+ if (regex.test(ua))
69
+ return (cachedBrowser = key);
160
70
  }
161
- return "unknown";
71
+ return (cachedBrowser = "unknown");
162
72
  }
73
+ /** 匹配浏览器 */
163
74
  export function matchBrowser(checkItem, currentBrowser) {
164
75
  if (!checkItem || !currentBrowser)
165
76
  return false;
166
- const normalized = checkItem.toString().toLowerCase();
77
+ const normalized = checkItem.toLowerCase();
167
78
  const browser = currentBrowser.toLowerCase();
168
79
  if (normalized.startsWith("/") && normalized.endsWith("/")) {
169
80
  try {
170
- const body = normalized.slice(1, -1);
171
- const regex = new RegExp(body, "i");
172
- return regex.test(browser);
81
+ return new RegExp(normalized.slice(1, -1), "i").test(browser);
173
82
  }
174
- catch (_) { }
83
+ catch { }
175
84
  }
176
85
  return browser.includes(normalized);
177
86
  }
178
- export function filterSmartLines(list1, list2, groupType, tenant) {
179
- if (!Array.isArray(list1) || !Array.isArray(list2)) {
180
- 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}`;
181
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("参数必须是数组");
182
116
  const currentBrowser = detectBrowser();
183
- let result = list1.filter((item) => {
184
- const checks = item.browserCheck || [];
185
- const blocked = checks.some((checkItem) => matchBrowser(checkItem, currentBrowser));
186
- return !blocked;
187
- });
117
+ let filtered = tenantList.filter((item) => !(item.browserCheck || []).some((check) => matchBrowser(check, currentBrowser)));
188
118
  if (tenant)
189
- result = result.filter((item) => item.tenant === tenant);
119
+ filtered = filtered.filter((item) => item.tenant === tenant);
190
120
  if (groupType)
191
- result = result.filter((item) => item.lineGroup === groupType);
192
- const lines = result.map((item) => toStandardUrl(item.line));
193
- 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];
194
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,270 @@
1
+ let isRewardShowing = false;
2
+ // --------------------------- 普通消息 ---------------------------
3
+ let messageStackContainer = null;
4
+ // --------------------------- reward 弹窗 ---------------------------
5
+ let rewardMask = null;
6
+ const rewardQueue = [];
7
+ let rewardStackContainer = null;
8
+ function getMessageStackContainer() {
9
+ if (!messageStackContainer) {
10
+ messageStackContainer = document.createElement("div");
11
+ Object.assign(messageStackContainer.style, {
12
+ position: "fixed",
13
+ bottom: "10vh",
14
+ left: "50%",
15
+ transform: "translateX(-50%)",
16
+ display: "flex",
17
+ flexDirection: "column",
18
+ alignItems: "center",
19
+ gap: "10px",
20
+ zIndex: "9999",
21
+ pointerEvents: "none",
22
+ width: "100%",
23
+ maxWidth: "calc(100% - 40px)",
24
+ });
25
+ document.body.appendChild(messageStackContainer);
26
+ }
27
+ return messageStackContainer;
28
+ }
29
+ function hideMsg(element, callback) {
30
+ element.style.opacity = "0";
31
+ element.style.transform += " translateY(20px)";
32
+ setTimeout(() => {
33
+ element.parentNode?.removeChild(element);
34
+ callback?.();
35
+ }, 300);
36
+ }
37
+ function getColorByType(type) {
38
+ switch (type) {
39
+ case "success":
40
+ return "#67C23A";
41
+ case "warning":
42
+ return "#E6A23C";
43
+ case "error":
44
+ return "#F56C6C";
45
+ case "info":
46
+ return "#409EFF";
47
+ default:
48
+ return "#409EFF";
49
+ }
50
+ }
51
+ function getRewardMask() {
52
+ if (!rewardMask) {
53
+ rewardMask = document.createElement("div");
54
+ Object.assign(rewardMask.style, {
55
+ position: "fixed",
56
+ top: "0",
57
+ left: "0",
58
+ width: "100vw",
59
+ height: "100vh",
60
+ background: "rgba(0,0,0,0.5)",
61
+ zIndex: "100000",
62
+ pointerEvents: "auto",
63
+ display: "flex",
64
+ justifyContent: "center",
65
+ alignItems: "center",
66
+ });
67
+ document.body.appendChild(rewardMask);
68
+ }
69
+ return rewardMask;
70
+ }
71
+ function getRewardStackContainer() {
72
+ if (!rewardStackContainer) {
73
+ rewardStackContainer = document.createElement("div");
74
+ Object.assign(rewardStackContainer.style, {
75
+ position: "relative",
76
+ display: "flex",
77
+ flexDirection: "column",
78
+ alignItems: "center",
79
+ gap: "12px",
80
+ width: "100%",
81
+ maxWidth: "calc(100% - 40px)",
82
+ });
83
+ getRewardMask().appendChild(rewardStackContainer);
84
+ }
85
+ return rewardStackContainer;
86
+ }
87
+ function showNextReward() {
88
+ if (isRewardShowing || rewardQueue.length === 0)
89
+ return;
90
+ const options = rewardQueue.shift();
91
+ isRewardShowing = true;
92
+ const mask = getRewardMask();
93
+ const originalOverflow = document.body.style.overflow;
94
+ document.body.style.overflow = "hidden";
95
+ const container = document.createElement("div");
96
+ container.className = "rewardWindow";
97
+ Object.assign(container.style, {
98
+ position: "relative",
99
+ width: "calc(100% - 60px)",
100
+ maxWidth: "370px",
101
+ paddingTop: options.topImg ? "88px" : "23px",
102
+ backgroundColor: "#fff",
103
+ borderRadius: "12px",
104
+ boxSizing: "border-box",
105
+ display: "flex",
106
+ flexDirection: "column",
107
+ alignItems: "center",
108
+ textAlign: "center",
109
+ opacity: "0",
110
+ transform: "scale(0.9)",
111
+ transition: "all 0.3s cubic-bezier(0.4,0,0.2,1)",
112
+ overflow: "hidden",
113
+ zIndex: "100001",
114
+ });
115
+ if (options.title) {
116
+ const titleEl = document.createElement("div");
117
+ titleEl.textContent = options.title;
118
+ Object.assign(titleEl.style, {
119
+ fontSize: "16px",
120
+ fontWeight: "600",
121
+ color: "#303442",
122
+ });
123
+ container.appendChild(titleEl);
124
+ }
125
+ if (options.topImg) {
126
+ const imgEl = document.createElement("img");
127
+ imgEl.src = options.topImg;
128
+ Object.assign(imgEl.style, {
129
+ position: "absolute",
130
+ top: "-68px",
131
+ height: "150px",
132
+ width: "150px",
133
+ });
134
+ container.appendChild(imgEl);
135
+ }
136
+ if (options.text) {
137
+ const textEl = document.createElement("div");
138
+ textEl.innerHTML = options.text;
139
+ Object.assign(textEl.style, {
140
+ padding: "14px 22px 12px",
141
+ fontSize: "12px",
142
+ fontWeight: "500",
143
+ lineHeight: "16px",
144
+ textAlign: "center",
145
+ width: "100%",
146
+ overflow: "hidden",
147
+ wordBreak: "break-word",
148
+ color: "#7981A4",
149
+ });
150
+ container.appendChild(textEl);
151
+ }
152
+ const btnContainer = document.createElement("div");
153
+ Object.assign(btnContainer.style, {
154
+ display: "flex",
155
+ width: "100%",
156
+ height: "44px",
157
+ borderTop: "1px solid #f2f2f6",
158
+ padding: "8px 0",
159
+ });
160
+ const btnStyle = {
161
+ flex: "1",
162
+ border: "none",
163
+ background: "transparent",
164
+ cursor: "pointer",
165
+ fontSize: "14px",
166
+ fontWeight: "500",
167
+ };
168
+ if (options.cancelText) {
169
+ const cancelBtn = document.createElement("button");
170
+ cancelBtn.textContent = options.cancelText;
171
+ Object.assign(cancelBtn.style, {
172
+ ...btnStyle,
173
+ borderRight: "1px solid #f2f2f6",
174
+ color: "#303442",
175
+ });
176
+ cancelBtn.onclick = () => closeReward(container, options.onCancel ?? options.onClose, originalOverflow);
177
+ btnContainer.appendChild(cancelBtn);
178
+ }
179
+ const confirmBtn = document.createElement("button");
180
+ confirmBtn.textContent = options.submitText || "确认";
181
+ Object.assign(confirmBtn.style, {
182
+ ...btnStyle,
183
+ color: "#179CFF",
184
+ });
185
+ confirmBtn.onclick = () => {
186
+ options.onSubmit?.();
187
+ closeReward(container, options.onClose, originalOverflow);
188
+ };
189
+ btnContainer.appendChild(confirmBtn);
190
+ container.appendChild(btnContainer);
191
+ const stack = getRewardStackContainer();
192
+ stack.appendChild(container);
193
+ requestAnimationFrame(() => {
194
+ container.style.opacity = "1";
195
+ container.style.transform = "scale(1)";
196
+ });
197
+ }
198
+ function closeReward(container, callback, originalOverflow) {
199
+ container.style.opacity = "0";
200
+ container.style.transform = "scale(0.9)";
201
+ setTimeout(() => {
202
+ container.parentNode?.removeChild(container);
203
+ callback?.();
204
+ if (rewardStackContainer?.childElementCount === 0) {
205
+ rewardMask?.remove();
206
+ rewardMask = null;
207
+ rewardStackContainer = null;
208
+ document.body.style.overflow = originalOverflow ?? "";
209
+ }
210
+ isRewardShowing = false;
211
+ showNextReward(); // 显示下一个弹窗
212
+ }, 300);
213
+ }
214
+ export function msg(message, options = {}) {
215
+ const { type = "info", duration = 3000, closable = false, onClose } = options;
216
+ const container = document.createElement("div");
217
+ Object.assign(container.style, {
218
+ backgroundColor: getColorByType(type),
219
+ color: "#fff",
220
+ borderRadius: "12px",
221
+ fontSize: "12px",
222
+ fontWeight: "500",
223
+ padding: "10px 16px",
224
+ maxWidth: "400px",
225
+ minWidth: "120px",
226
+ textAlign: "center",
227
+ display: "-webkit-box",
228
+ WebkitLineClamp: "2",
229
+ WebkitBoxOrient: "vertical",
230
+ overflow: "hidden",
231
+ textOverflow: "ellipsis",
232
+ wordWrap: "break-word",
233
+ boxShadow: "0 6px 18px rgba(0,0,0,0.12)",
234
+ opacity: "0",
235
+ transform: "translateY(20px)",
236
+ transition: "all 0.3s cubic-bezier(0.4,0,0.2,1)",
237
+ pointerEvents: "auto",
238
+ position: "relative",
239
+ });
240
+ container.textContent = message;
241
+ if (closable) {
242
+ const closeBtn = document.createElement("span");
243
+ closeBtn.innerHTML = "&times;";
244
+ Object.assign(closeBtn.style, {
245
+ position: "absolute",
246
+ top: "50%",
247
+ right: "10px",
248
+ transform: "translateY(-50%)",
249
+ cursor: "pointer",
250
+ fontSize: "14px",
251
+ fontWeight: "bold",
252
+ });
253
+ closeBtn.onclick = () => hideMsg(container, onClose);
254
+ container.appendChild(closeBtn);
255
+ container.style.paddingRight = "30px";
256
+ }
257
+ const stack = getMessageStackContainer();
258
+ stack.appendChild(container);
259
+ requestAnimationFrame(() => {
260
+ container.style.opacity = "1";
261
+ container.style.transform = "translateY(0)";
262
+ });
263
+ if (duration > 0) {
264
+ setTimeout(() => hideMsg(container, onClose), duration);
265
+ }
266
+ }
267
+ export function rewardMsg(options) {
268
+ rewardQueue.push(options);
269
+ showNextReward();
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hifun-tools",
3
- "version": "1.3.29",
3
+ "version": "1.3.31",
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
  },