hifun-tools 1.3.15 → 1.3.17

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.
@@ -35,15 +35,18 @@ describe("extractRootDomain", () => {
35
35
  // --------------------- isDomainMatch -------------------------
36
36
  describe("isDomainMatch", () => {
37
37
  test("匹配根域", () => {
38
- const list = ["a.example.com", "b.test.co.uk"];
39
- expect(isDomainMatch(list, "https://api.example.com")).toBe(true);
38
+ const list = [
39
+ " localhost:3000/asdnajkd ",
40
+ " http://192.168.101.85:3000/asdasdasd ",
41
+ ];
42
+ expect(isDomainMatch(list, "localhost:3000")).toBe(true);
40
43
  });
41
44
  test("多级后缀匹配", () => {
42
- const list = ["node.test.co.uk"];
45
+ const list = [" node.test.co.uk "];
43
46
  expect(isDomainMatch(list, "api.test.co.uk")).toBe(true);
44
47
  });
45
48
  test("不匹配", () => {
46
- const list = ["a.example.com"];
49
+ const list = [" a.example.com "];
47
50
  expect(isDomainMatch(list, "another.com")).toBe(false);
48
51
  });
49
52
  });
@@ -1,4 +1,4 @@
1
- import { isDomainMatch, toStandardUrl } from "./utils";
1
+ import { filterSmartLines, isDomainMatch, matchBrowser, TenantDict, toStandardUrl } from "./utils";
2
2
  import { AesDecrypt, AesEncrypt } from "./ende";
3
3
  type TenantConfig = {
4
4
  PUBLIC_TITLE: string;
@@ -9,6 +9,8 @@ type TenantConfig = {
9
9
  declare class InitCls {
10
10
  private domainBaseUrl;
11
11
  private tenantConfig;
12
+ private tenantDict;
13
+ private tenantDictList;
12
14
  private initialized;
13
15
  private tenant;
14
16
  private defaultBaseUrl;
@@ -16,6 +18,8 @@ declare class InitCls {
16
18
  getBaseUrl(): string;
17
19
  /** 获取配置 */
18
20
  getTenantConfig(): TenantConfig | null;
21
+ getTenantDict(): TenantDict | null;
22
+ getTenantDictList(): TenantDict[];
19
23
  /** 获取租户名 */
20
24
  getTenant(): string;
21
25
  getImgPath(imgName: string): string;
@@ -33,4 +37,4 @@ declare class InitCls {
33
37
  AesDecrypt: typeof AesDecrypt;
34
38
  }
35
39
  declare const HF: InitCls;
36
- export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt };
40
+ export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt, filterSmartLines, matchBrowser, };
@@ -1,9 +1,11 @@
1
1
  import { getResource } from "./getResource";
2
- import { getOptimalDecodedString, isDomainMatch, toStandardUrl } from "./utils";
2
+ import { filterSmartLines, getOptimalDecodedString, isDomainMatch, matchBrowser, toStandardUrl, } from "./utils";
3
3
  import { AesDecrypt, AesEncrypt } from "./ende";
4
4
  class InitCls {
5
5
  domainBaseUrl = "";
6
6
  tenantConfig = null;
7
+ tenantDict = null;
8
+ tenantDictList = [];
7
9
  initialized = false;
8
10
  tenant = "";
9
11
  defaultBaseUrl = "";
@@ -21,6 +23,18 @@ class InitCls {
21
23
  }
22
24
  return this.tenantConfig;
23
25
  }
26
+ getTenantDict() {
27
+ if (!this.initialized || !this.tenant) {
28
+ throw new Error("租户尚未初始化或初始化失败getTenantDict");
29
+ }
30
+ return this.tenantDict;
31
+ }
32
+ getTenantDictList() {
33
+ if (!this.initialized || !this.tenant) {
34
+ throw new Error("租户尚未初始化或初始化失败getTenantDictList");
35
+ }
36
+ return this.tenantDictList;
37
+ }
24
38
  /** 获取租户名 */
25
39
  getTenant() {
26
40
  return this.tenant;
@@ -34,7 +48,7 @@ class InitCls {
34
48
  /** 严格同步获取租户信息 */
35
49
  async getTenantInfoStrictSync() {
36
50
  try {
37
- const response = await fetch(`/lineTenants.txt?t=${Date.now()}`, {
51
+ const response = await fetch(`/lineDict.txt?t=${Date.now()}`, {
38
52
  method: "GET",
39
53
  cache: "no-store",
40
54
  });
@@ -45,15 +59,14 @@ class InitCls {
45
59
  const rawData = AesDecrypt(rawText);
46
60
  const data = JSON.parse(rawData);
47
61
  const host = location.host;
48
- const matchedEntry = Object.entries(data).find(([key]) => {
49
- return isDomainMatch([key], host);
62
+ const matchedEntry = data.find((item) => {
63
+ return isDomainMatch([item.line], host);
50
64
  });
65
+ this.tenantDictList = data;
51
66
  if (matchedEntry) {
52
- const matched = matchedEntry[1];
53
- if (data && matched) {
54
- console.info(`🏠 匹配租户: ${matched}`);
55
- return { tenant: matched };
56
- }
67
+ this.tenantDict = matchedEntry;
68
+ console.info(`🏠 匹配租户: ${matchedEntry.tenant}`);
69
+ return { tenant: matchedEntry.tenant };
57
70
  }
58
71
  // 默认租户匹配
59
72
  if (host.includes("iggame") || host.includes("localhost")) {
@@ -63,7 +76,7 @@ class InitCls {
63
76
  throw new Error("未找到匹配租户");
64
77
  }
65
78
  catch (error) {
66
- console.error("解析 lineTenants.txt 失败:", error);
79
+ console.error("解析 lineDict.txt 失败:", error);
67
80
  const host = location.host;
68
81
  if (host.includes("iggame") ||
69
82
  host.includes("localhost") ||
@@ -99,17 +112,17 @@ class InitCls {
99
112
  this.localPath = localPath;
100
113
  }
101
114
  console.log("加载类型:", fileType);
102
- const tasks = [];
115
+ const results = [];
116
+ if (fileType.includes("lineDict")) {
117
+ console.log("初始化 lineDict...");
118
+ const res1 = await this.initialize();
119
+ results.push(res1);
120
+ }
103
121
  if (fileType.includes("lineAddress")) {
104
122
  console.log("初始化 lineAddress...");
105
- tasks.push(this.LoadGatewayConfig());
106
- }
107
- if (fileType.includes("lineTenants")) {
108
- console.log("初始化 lineTenants...");
109
- tasks.push(this.initialize());
123
+ const res2 = await this.LoadGatewayConfig();
124
+ results.push(res2);
110
125
  }
111
- // 等待所有异步任务完成
112
- const results = await Promise.all(tasks);
113
126
  console.log("所有初始化完成:", results);
114
127
  return results;
115
128
  }
@@ -119,7 +132,10 @@ class InitCls {
119
132
  try {
120
133
  const response = await fetch(`/lineAddress.txt?t=${Date.now()}`);
121
134
  const configText = await response.text();
122
- const baseUrl = JSON.parse(AesDecrypt(configText));
135
+ let baseUrl = JSON.parse(AesDecrypt(configText));
136
+ const dict = this.getTenantDict();
137
+ const dictList = this.getTenantDictList();
138
+ baseUrl = filterSmartLines(dictList, baseUrl, dict?.lineGroup);
123
139
  if (Array.isArray(baseUrl)) {
124
140
  try {
125
141
  this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(baseUrl));
@@ -155,4 +171,4 @@ class InitCls {
155
171
  AesDecrypt = AesDecrypt;
156
172
  }
157
173
  const HF = new InitCls();
158
- export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt };
174
+ export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt, filterSmartLines, matchBrowser, };
@@ -1,3 +1,9 @@
1
+ export type TenantDict = {
2
+ browserCheck: string[];
3
+ line: string;
4
+ lineGroup: string;
5
+ tenant: string;
6
+ };
1
7
  export declare function isIPv4(str: string): boolean;
2
8
  export declare function getOptimalDecodedString(encodedArray: string[]): Promise<string>;
3
9
  /**
@@ -9,3 +15,14 @@ export declare function extractRootDomain(url: string): string;
9
15
  */
10
16
  export declare function isDomainMatch(list: string[], target: string): boolean;
11
17
  export declare function toStandardUrl(input: string): string;
18
+ /**
19
+ * 匹配规则:模糊 + 正则 + 不区分大小写
20
+ */
21
+ export declare function matchBrowser(checkItem: string, currentBrowser: string): boolean;
22
+ /**
23
+ * 1. 过滤 browserCheck
24
+ * 2. 按 tenant 过滤
25
+ * 3. 按 groupType(可为空)过滤
26
+ * 4. 与 list2 求交集
27
+ */
28
+ export declare function filterSmartLines(list1: TenantDict[], list2: string[], groupType?: string, tenant?: string): string[];
@@ -18,8 +18,10 @@ export async function getOptimalDecodedString(encodedArray) {
18
18
  throw new Error("输入数组为空或无效");
19
19
  const hostname = window.location.hostname;
20
20
  const isIp = isIPv4(hostname);
21
- // 修正:Base64 解码后再过滤
22
- const decodedList = encodedArray.filter(Boolean);
21
+ // 关键增强:先 trim 再过滤
22
+ const decodedList = encodedArray
23
+ .filter(Boolean)
24
+ .map((v) => v.trim());
23
25
  const ipList = decodedList.filter((v) => isIPv4(v));
24
26
  const domainList = decodedList.filter((v) => !isIPv4(v));
25
27
  const getOptimal = (arr) => new Promise((resolve) => {
@@ -28,6 +30,7 @@ export async function getOptimalDecodedString(encodedArray) {
28
30
  return;
29
31
  }
30
32
  const currentProtocol = window.location.protocol;
33
+ // 过滤协议
31
34
  const filteredArr = arr.filter((url) => url.startsWith("http") ? url.startsWith(currentProtocol) : true);
32
35
  const targetArr = filteredArr.length ? filteredArr : arr;
33
36
  if (targetArr.length === 1)
@@ -86,6 +89,7 @@ const MULTI_LEVEL_TLDS = new Set([
86
89
  */
87
90
  export function extractRootDomain(url) {
88
91
  try {
92
+ url = url.trim(); // ✨ 增加
89
93
  if (!/^https?:\/\//i.test(url)) {
90
94
  url = "http://" + url;
91
95
  }
@@ -94,13 +98,11 @@ export function extractRootDomain(url) {
94
98
  if (parts.length < 2)
95
99
  return hostname;
96
100
  const last2 = parts.slice(-2).join(".");
97
- // 匹配多级 TLD(如 example.com.cn → example.com.cn)
98
101
  if (MULTI_LEVEL_TLDS.has(last2)) {
99
102
  if (parts.length >= 3) {
100
103
  return parts.slice(-3).join(".");
101
104
  }
102
105
  }
103
- // 通用规则:主域 = 最后两段
104
106
  return last2;
105
107
  }
106
108
  catch (e) {
@@ -117,16 +119,102 @@ export function isDomainMatch(list, target) {
117
119
  }
118
120
  export function toStandardUrl(input) {
119
121
  try {
122
+ input = input.trim(); // ✨ 增加
120
123
  let protocol = window?.location?.protocol || "https:";
121
- // 自动补协议
122
124
  if (!/^https?:\/\//i.test(input)) {
123
125
  input = protocol + "//" + input;
124
126
  }
125
127
  const url = new URL(input);
126
- // 输出 protocol://hostname
127
128
  return `${url.protocol}//${url.hostname}`;
128
129
  }
129
130
  catch (e) {
130
- return input; // 当成普通字符串返回
131
+ return input;
131
132
  }
132
133
  }
134
+ /**
135
+ * 浏览器识别:返回一个归一化的浏览器类型字符串
136
+ */
137
+ function detectBrowser() {
138
+ const ua = navigator.userAgent.toLowerCase();
139
+ const rules = [
140
+ { key: "baidu", regex: /baidubrowser|baiduboxapp|baidu/i },
141
+ { key: "uc", regex: /ucbrowser|ucweb/i },
142
+ { key: "qq", regex: /mqqbrowser|qqbrowser/i },
143
+ { key: "quark", regex: /quarkbrowser|quark/i },
144
+ { key: "sougou", regex: /se 2\.x|sogoumobilebrowser|sogou/i },
145
+ { key: "360", regex: /360browser|qhbrowser|360se|360ee/i },
146
+ { key: "xiaomi", regex: /miuibrowser/i },
147
+ { key: "huawei", regex: /huaweibrowser|hwbrowser/i },
148
+ { key: "oppo", regex: /oppobrowser/i },
149
+ { key: "vivo", regex: /vivobrowser/i },
150
+ { key: "samsung", regex: /samsungbrowser/i },
151
+ { key: "edge", regex: /edg|edge/i },
152
+ { key: "firefox", regex: /firefox/i },
153
+ { key: "opera", regex: /opr|opera/i },
154
+ { key: "safari", regex: /safari/i, exclude: /chrome|crios|android/i },
155
+ { key: "chrome", regex: /chrome|crios/i },
156
+ { key: "weixin", regex: /micromessenger/i },
157
+ { key: "alipay", regex: /alipayclient/i },
158
+ { key: "douyin", regex: /aweme|douyin/i },
159
+ ];
160
+ for (const item of rules) {
161
+ if (item.exclude && item.exclude.test(ua))
162
+ continue;
163
+ if (item.regex.test(ua))
164
+ return item.key;
165
+ }
166
+ return "unknown";
167
+ }
168
+ /**
169
+ * 匹配规则:模糊 + 正则 + 不区分大小写
170
+ */
171
+ export function matchBrowser(checkItem, currentBrowser) {
172
+ if (!checkItem)
173
+ return false;
174
+ if (!currentBrowser)
175
+ return false;
176
+ const normalized = checkItem.toString().toLowerCase();
177
+ const browser = currentBrowser.toLowerCase();
178
+ // 正则格式,如 "/uc/i"
179
+ if (normalized.startsWith("/") && normalized.endsWith("/")) {
180
+ try {
181
+ const body = normalized.slice(1, -1);
182
+ const regex = new RegExp(body, "i");
183
+ return regex.test(browser);
184
+ }
185
+ catch (_) { }
186
+ }
187
+ // 字符串模糊匹配
188
+ return browser.includes(normalized);
189
+ }
190
+ /**
191
+ * 1. 过滤 browserCheck
192
+ * 2. 按 tenant 过滤
193
+ * 3. 按 groupType(可为空)过滤
194
+ * 4. 与 list2 求交集
195
+ */
196
+ export function filterSmartLines(list1, list2, groupType, tenant) {
197
+ if (!Array.isArray(list1) || !Array.isArray(list2)) {
198
+ throw new Error("前两个参数必须是数组");
199
+ }
200
+ const currentBrowser = detectBrowser();
201
+ // 1. 浏览器过滤
202
+ let result = list1.filter((item) => {
203
+ const checks = item.browserCheck || [];
204
+ const blocked = checks.some((checkItem) => matchBrowser(checkItem, currentBrowser));
205
+ return !blocked;
206
+ });
207
+ // 2. tenant 过滤(如果有传值)
208
+ if (tenant) {
209
+ result = result.filter((item) => item.tenant === tenant);
210
+ }
211
+ // 3. 按 groupType 过滤
212
+ if (groupType) {
213
+ result = result.filter((item) => item.lineGroup === groupType);
214
+ }
215
+ // 4. 提取 lines
216
+ const lines = result.map((item) => toStandardUrl(item.line));
217
+ // 5. 与 list2 求交集
218
+ const set = new Set(lines);
219
+ return list2.filter((v) => set.has(toStandardUrl(v))).map(toStandardUrl);
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hifun-tools",
3
- "version": "1.3.15",
3
+ "version": "1.3.17",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",