From 433649fee12cca4162628949f82ad5751fcc218a Mon Sep 17 00:00:00 2001 From: Orion Date: Fri, 31 Jul 2026 08:59:20 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(core):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=8A=82=E7=82=B9=E9=87=8D=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=B9=B6=E7=AE=80=E5=8C=96=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- substore/README.md | 38 +- substore/rename.js | 956 ++------------------------------------------- 2 files changed, 51 insertions(+), 943 deletions(-) diff --git a/substore/README.md b/substore/README.md index 61e4183..26d730a 100644 --- a/substore/README.md +++ b/substore/README.md @@ -19,7 +19,7 @@ https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=20260702 5. `iriscloud-rename.js`:只处理 Iris Cloud 的入口/落地地区冲突和供应商特征。 6. `liangxin-rename.js`:只清理 LiangXin 的高速、BGP、CMCU 等线路描述。 7. `yinyun-rename.js`:只处理 YinYun 的中文地区名,并按地区连续编号。 -8. `rename.js`:整合订阅最终重命名,负责国家短码、国旗归位、订阅后缀和去重。 +8. `rename.js`:整合订阅最后追加订阅后缀,并对仍然重名的节点编号。 如果某个订阅没有对应的专属脚本,直接进入最后的 `rename.js` 即可。 @@ -34,7 +34,7 @@ https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=20260702 | `iriscloud-rename.js` | Iris Cloud 单订阅 | 按 `SG`、`TYO`/`JP`、其余香港的专属规则重判落地地区,去掉入口线路和供应商文案并保留倍率 | | `liangxin-rename.js` | LiangXin 单订阅 | 去掉 `高速`、`BGP`、`CMCU`、`CM`、`CU`、`CT` 等线路描述 | | `yinyun-rename.js` | YinYun 单订阅 | 中文国家/地区名转短码,忽略 `优选`、原始编号和 `(T)`/`(R)`,按地区连续编号 | -| `rename.js` | 整合订阅 | 统一国家/城市短码,移动国旗到首部,追加 `- _subName`,处理重名编号 | +| `rename.js` | 整合订阅 | 保留原节点名,追加 `- _subName`,处理追加后仍然重名的节点 | | `common.js` | 公共依赖 | 被各处理脚本远程加载,不需要单独配置为处理器 | ## SNTP 规则 @@ -187,37 +187,23 @@ https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=20260702 => 🇹🇭 TH 01 - YinYun ``` -## 全局重命名 +## 全局后缀 -`rename.js` 适合放在整合订阅最后。它做的事情更通用: +`rename.js` 适合放在整合订阅最后。它不会再修改节点原名、地区/线路标签或节点顺序,只处理订阅后缀和重名兜底: -全局规则只按节点名结构、地区词、线路词和通用格式匹配,不按机场名或 `_subName` 分支处理;以后修改某个订阅的 `_subName` 不应影响全局规则命中。 - -- 国家/地区中文名、英文名转短码,例如 `香港` -> `HK`、`Japan` -> `JP`。 -- 常见城市转短码,例如 `Los Angeles` -> `LAX`、`San Jose` -> `SJC`、`Seattle` -> `SEA`、`东京` -> `TYO`。 -- 国家/地区别名表覆盖常见英文、中文国家名;城市尽量使用三字 IATA/城市码,避免 `LA` 这类两字码被旗帜操作误识别为国家。 -- 支持国旗在中间的节点名,例如 `Pro-🇭🇰 Sharon|守夜人` -> `🇭🇰 HK Pro-Sharon|守夜人`。 -- 支持保留连字符命名里的入口/落地特征,例如 `HK-Go-05-HostHatch` -> `HK 05 GO-HostHatch`。 -- 将倍率移动到节点核心末尾、订阅后缀之前,例如 `US 2X 02 - Example` -> `US 02 2X - Example`。 -- 清理 `[DIP China-Hongkong]` 为 `[DIP]`。 -- 清理 `IEPL 专线 3`、`标准 1`、`高级 1` 这类冗余线路尾号。 -- 追加订阅后缀 `- _subName`。 -- 对最终重名节点追加两位编号。 +- 先移除本次订阅已经存在的 `- _subName`(含重跑时的两位编号),再追加一次,避免重复运行叠加后缀。 +- 不同 `_subName` 的订阅即使原节点名相同,追加后缀后也会自然区分,不会互相编号。 +- 只有追加后仍然完全重名的节点,才在订阅后缀前追加两位编号。 示例: ```text -🇭🇰 Hong Kong 01 [DIP China-Hongkong] - Example -=> 🇭🇰 HK 01 [DIP] - Example +Hong Kong 01 +=> Hong Kong 01 - Example -🇯🇵 日本高级 IEPL 专线 3 - Example -=> 🇯🇵 JP 01 高级 - Example - -Pro-🇭🇰 Sharon|守夜人 - Example -=> 🇭🇰 HK Pro-Sharon|守夜人 - Example - -🇭🇰 HK-Go-05-HostHatch - Example -=> 🇭🇰 HK 05 GO-HostHatch - Example +重复节点 +=> 重复节点 01 - Example +=> 重复节点 02 - Example ``` ## 注意事项 diff --git a/substore/rename.js b/substore/rename.js index c779f63..f23d395 100644 --- a/substore/rename.js +++ b/substore/rename.js @@ -1,8 +1,8 @@ /** - * Sub-Store 节点重命名脚本 - * - 统一节点前缀为地区短码,保留旗帜与订阅后缀 - * - 清理常见冗余线路描述,例如 DIP 地区说明、IEPL 专线 - * - 基于归一化后的完整名称做精准去重 + * Sub-Store 整合订阅后缀脚本 + * - 保留节点原名,只追加当前订阅的 `_subName` + * - 重复执行不会叠加相同的订阅后缀 + * - 同名节点追加两位编号,避免节点名重复 */ // noinspection JSUnusedGlobalSymbols const COMMON_SCRIPT_VERSION = '20260702-1'; @@ -33,931 +33,53 @@ async function loadRenameUtils() { async function operator(proxies) { const utils = await loadRenameUtils(); - const normalizeHyphenatedRegionFeatures = utils.normalizeHyphenatedRegionFeatures || (name => name); - const globalCount = Object.create(null); - const currentCount = Object.create(null); + const stripSubscriptionSuffix = utils.stripSubscriptionSuffix || ((name, subName) => { + const cleanSubName = String(subName || '').trim(); + if (!cleanSubName) return String(name); + + const escapeRegex = utils.escapeRegex || (value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + return String(name) + .replace(new RegExp(`\\s+-\\s*${escapeRegex(cleanSubName)}(?:\\s+\\d{2})?\\s*$`, 'i'), '') + .trim(); + }); + const nameCounts = Object.create(null); + const currentCounts = Object.create(null); const baseNames = new Array(proxies.length); - const finalCoreNames = new Array(proxies.length); - const normalizedCoreNames = new Array(proxies.length); + const coreNames = new Array(proxies.length); const subNames = new Array(proxies.length); - const sequenceInfos = new Array(proxies.length); - const suffixRegexCache = Object.create(null); - - const regionAliases = [ - ['United Arab Emirates', 'AE'], - ['United States Los Angeles', 'US LAX'], - ['United States San Jose', 'US SJC'], - ['United States San Francisco', 'US SFO'], - ['United States New York', 'US NYC'], - ['United States Newark', 'US EWR'], - ['United States Washington', 'US IAD'], - ['United States Ashburn', 'US IAD'], - ['United States Dallas', 'US DFW'], - ['United States Chicago', 'US ORD'], - ['United States Miami', 'US MIA'], - ['United States Atlanta', 'US ATL'], - ['United States Phoenix', 'US PHX'], - ['United States Denver', 'US DEN'], - ['United States Las Vegas', 'US LAS'], - ['United States Portland', 'US PDX'], - ['United States Boston', 'US BOS'], - ['United States Seattle', 'US SEA'], - ['United States Hawaii', 'US HNL'], - ['Australia Sydney', 'AU SYD'], - ['Australia Melbourne', 'AU MEL'], - ['Australia Brisbane', 'AU BNE'], - ['Australia Perth', 'AU PER'], - ['Canada Toronto', 'CA TOR'], - ['Canada Vancouver', 'CA YVR'], - ['Canada Montreal', 'CA YUL'], - ['Canada Calgary', 'CA YYC'], - ['Japan Tokyo', 'JP TYO'], - ['Japan Osaka', 'JP OSA'], - ['Japan Nagoya', 'JP NGO'], - ['South Korea Seoul', 'KR SEL'], - ['Korea Seoul', 'KR SEL'], - ['United Kingdom London', 'UK LON'], - ['Germany Frankfurt', 'DE FRA'], - ['Germany Berlin', 'DE BER'], - ['Germany Munich', 'DE MUC'], - ['France Paris', 'FR PAR'], - ['Netherlands Amsterdam', 'NL AMS'], - ['Switzerland Zurich', 'CH ZRH'], - ['Sweden Stockholm', 'SE STO'], - ['Norway Oslo', 'NO OSL'], - ['Denmark Copenhagen', 'DK CPH'], - ['Finland Helsinki', 'FI HEL'], - ['Poland Warsaw', 'PL WAW'], - ['Czech Prague', 'CZ PRG'], - ['Austria Vienna', 'AT VIE'], - ['Hungary Budapest', 'HU BUD'], - ['Italy Milan', 'IT MIL'], - ['Italy Rome', 'IT ROM'], - ['Spain Madrid', 'ES MAD'], - ['Spain Barcelona', 'ES BCN'], - ['Ireland Dublin', 'IE DUB'], - ['Portugal Lisbon', 'PT LIS'], - ['Greece Athens', 'GR ATH'], - ['Turkey Istanbul', 'TR IST'], - ['Brazil Sao Paulo', 'BR SAO'], - ['Brazil Rio de Janeiro', 'BR RIO'], - ['Argentina Buenos Aires', 'AR BUE'], - ['Chile Santiago', 'CL SCL'], - ['Mexico Mexico City', 'MX MEX'], - ['South Africa Johannesburg', 'ZA JNB'], - ['South Africa Cape Town', 'ZA CPT'], - ['UAE Dubai', 'AE DXB'], - ['Russia St. Petersburg', 'RU LED'], - ['Russia Moscow', 'RU MOW'], - ['USA Los Angeles', 'US LAX'], - ['USA San Jose', 'US SJC'], - ['USA San Francisco', 'US SFO'], - ['USA New York', 'US NYC'], - ['USA Newark', 'US EWR'], - ['USA Washington', 'US IAD'], - ['USA Ashburn', 'US IAD'], - ['USA Dallas', 'US DFW'], - ['USA Chicago', 'US ORD'], - ['USA Miami', 'US MIA'], - ['USA Atlanta', 'US ATL'], - ['USA Phoenix', 'US PHX'], - ['USA Denver', 'US DEN'], - ['USA Las Vegas', 'US LAS'], - ['USA Portland', 'US PDX'], - ['USA Boston', 'US BOS'], - ['USA Seattle', 'US SEA'], - ['USA Hawaii', 'US HNL'], - ['United Kingdom', 'UK'], - ['United States', 'US'], - ['South Africa', 'ZA'], - ['Saudi Arabia', 'SA'], - ['New Zealand', 'NZ'], - ['Hong Kong', 'HK'], - ['Netherlands', 'NL'], - ['Switzerland', 'CH'], - ['Singapore', 'SG'], - ['Indonesia', 'ID'], - ['Philippines', 'PH'], - ['Azerbaijan', 'AZ'], - ['Kazakhstan', 'KZ'], - ['Kyrgyzstan', 'KG'], - ['Argentina', 'AR'], - ['Australia', 'AU'], - ['Macedonia', 'MK'], - ['Lithuania', 'LT'], - ['Colombia', 'CO'], - ['Malaysia', 'MY'], - ['Thailand', 'TH'], - ['Vietnam', 'VN'], - ['Romania', 'RO'], - ['Nigeria', 'NG'], - ['Bulgaria', 'BG'], - ['Germany', 'DE'], - ['Hungary', 'HU'], - ['Austria', 'AT'], - ['Ireland', 'IE'], - ['Turkey', 'TR'], - ['Taiwan', 'TW'], - ['Canada', 'CA'], - ['Brazil', 'BR'], - ['Russia', 'RU'], - ['France', 'FR'], - ['Sweden', 'SE'], - ['Belgium', 'BE'], - ['Denmark', 'DK'], - ['Iceland', 'IS'], - ['Norway', 'NO'], - ['Morocco', 'MA'], - ['Mexico', 'MX'], - ['Israel', 'IL'], - ['Spain', 'ES'], - ['Italy', 'IT'], - ['Czech', 'CZ'], - ['Egypt', 'EG'], - ['Chile', 'CL'], - ['India', 'IN'], - ['Korea', 'KR'], - ['Japan', 'JP'], - ['Macau', 'MO'], - ['Dubai', 'AE DXB'], - ['Sydney', 'AU SYD'], - ['Melbourne', 'AU MEL'], - ['Brisbane', 'AU BNE'], - ['Perth', 'AU PER'], - ['Toronto', 'CA TOR'], - ['Vancouver', 'CA YVR'], - ['Montreal', 'CA YUL'], - ['Calgary', 'CA YYC'], - ['Casablanca', 'MA CMN'], - ['Tokyo', 'JP TYO'], - ['Osaka', 'JP OSA'], - ['Nagoya', 'JP NGO'], - ['Seoul', 'KR SEL'], - ['London', 'UK LON'], - ['Frankfurt', 'DE FRA'], - ['Berlin', 'DE BER'], - ['Munich', 'DE MUC'], - ['Paris', 'FR PAR'], - ['Amsterdam', 'NL AMS'], - ['Zurich', 'CH ZRH'], - ['Stockholm', 'SE STO'], - ['Oslo', 'NO OSL'], - ['Copenhagen', 'DK CPH'], - ['Helsinki', 'FI HEL'], - ['Warsaw', 'PL WAW'], - ['Prague', 'CZ PRG'], - ['Vienna', 'AT VIE'], - ['Budapest', 'HU BUD'], - ['Milan', 'IT MIL'], - ['Rome', 'IT ROM'], - ['Madrid', 'ES MAD'], - ['Barcelona', 'ES BCN'], - ['Dublin', 'IE DUB'], - ['Lisbon', 'PT LIS'], - ['Athens', 'GR ATH'], - ['Istanbul', 'TR IST'], - ['Sao Paulo', 'BR SAO'], - ['Rio de Janeiro', 'BR RIO'], - ['Buenos Aires', 'AR BUE'], - ['Santiago', 'CL SCL'], - ['Mexico City', 'MX MEX'], - ['Johannesburg', 'ZA JNB'], - ['Cape Town', 'ZA CPT'], - ['USA', 'US'], - - ['Bosnia and Herzegovina', 'BA'], - ['Trinidad and Tobago', 'TT'], - ['Saint Vincent and the Grenadines', 'VC'], - ['Antigua and Barbuda', 'AG'], - ['Saint Kitts and Nevis', 'KN'], - ['Sao Tome and Principe', 'ST'], - ['Central African Republic', 'CF'], - ['Democratic Republic of the Congo', 'CD'], - ['Republic of the Congo', 'CG'], - ['Dominican Republic', 'DO'], - ['Papua New Guinea', 'PG'], - ['Equatorial Guinea', 'GQ'], - ['Cote d Ivoire', 'CI'], - ['Cote d’Ivoire', 'CI'], - ['Ivory Coast', 'CI'], - ['Czech Republic', 'CZ'], - ['North Macedonia', 'MK'], - ['Cayman Islands', 'KY'], - ['French Guiana', 'GF'], - ['French Polynesia', 'PF'], - ['New Caledonia', 'NC'], - ['Puerto Rico', 'PR'], - ['Virgin Islands', 'VI'], - ['British Virgin Islands', 'VG'], - ['Faroe Islands', 'FO'], - ['Isle of Man', 'IM'], - ['Greenland', 'GL'], - ['Guam', 'GU'], - ['Bermuda', 'BM'], - ['Armenia', 'AM'], - ['Albania', 'AL'], - ['Andorra', 'AD'], - ['Belarus', 'BY'], - ['Croatia', 'HR'], - ['Cyprus', 'CY'], - ['Estonia', 'EE'], - ['Finland', 'FI'], - ['Greece', 'GR'], - ['Latvia', 'LV'], - ['Luxembourg', 'LU'], - ['Malta', 'MT'], - ['Moldova', 'MD'], - ['Montenegro', 'ME'], - ['Poland', 'PL'], - ['Portugal', 'PT'], - ['Serbia', 'RS'], - ['Slovakia', 'SK'], - ['Slovenia', 'SI'], - ['Ukraine', 'UA'], - ['Romania', 'RO'], - ['UAE', 'AE'], - ['Qatar', 'QA'], - ['Kuwait', 'KW'], - ['Bahrain', 'BH'], - ['Oman', 'OM'], - ['Jordan', 'JO'], - ['Lebanon', 'LB'], - ['Palestine', 'PS'], - ['Pakistan', 'PK'], - ['Bangladesh', 'BD'], - ['Sri Lanka', 'LK'], - ['Nepal', 'NP'], - ['Maldives', 'MV'], - ['Kazakhstan', 'KZ'], - ['Uzbekistan', 'UZ'], - ['Tajikistan', 'TJ'], - ['Turkmenistan', 'TM'], - ['Afghanistan', 'AF'], - ['Cambodia', 'KH'], - ['Myanmar', 'MM'], - ['Laos', 'LA'], - ['Brunei', 'BN'], - ['Mongolia', 'MN'], - ['Ethiopia', 'ET'], - ['Kenya', 'KE'], - ['Tanzania', 'TZ'], - ['Uganda', 'UG'], - ['Ghana', 'GH'], - ['Senegal', 'SN'], - ['Cameroon', 'CM'], - ['Angola', 'AO'], - ['Algeria', 'DZ'], - ['Tunisia', 'TN'], - ['Libya', 'LY'], - ['Zimbabwe', 'ZW'], - ['Zambia', 'ZM'], - ['Botswana', 'BW'], - ['Mauritius', 'MU'], - ['Seychelles', 'SC'], - ['Madagascar', 'MG'], - ['Panama', 'PA'], - ['Costa Rica', 'CR'], - ['Guatemala', 'GT'], - ['Honduras', 'HN'], - ['El Salvador', 'SV'], - ['Nicaragua', 'NI'], - ['Jamaica', 'JM'], - ['Bahamas', 'BS'], - ['Barbados', 'BB'], - ['Uruguay', 'UY'], - ['Paraguay', 'PY'], - ['Bolivia', 'BO'], - ['Peru', 'PE'], - ['Ecuador', 'EC'], - ['Venezuela', 'VE'], - ['Suriname', 'SR'], - ['Guyana', 'GY'], - - ['美国圣何塞', 'US SJC'], - ['美国洛杉矶', 'US LAX'], - ['美国旧金山', 'US SFO'], - ['美国纽约', 'US NYC'], - ['美国纽瓦克', 'US EWR'], - ['美国华盛顿', 'US IAD'], - ['美国阿什本', 'US IAD'], - ['美国达拉斯', 'US DFW'], - ['美国芝加哥', 'US ORD'], - ['美国迈阿密', 'US MIA'], - ['美国亚特兰大', 'US ATL'], - ['美国凤凰城', 'US PHX'], - ['美国丹佛', 'US DEN'], - ['美国拉斯维加斯', 'US LAS'], - ['美国波特兰', 'US PDX'], - ['美国波士顿', 'US BOS'], - ['美国夏威夷', 'US HNL'], - ['日本东京', 'JP TYO'], - ['日本大阪', 'JP OSA'], - ['日本名古屋', 'JP NGO'], - ['韩国首尔', 'KR SEL'], - ['英国伦敦', 'UK LON'], - ['瑞士苏黎世', 'CH ZRH'], - ['德国法兰克福', 'DE FRA'], - ['德国柏林', 'DE BER'], - ['德国慕尼黑', 'DE MUC'], - ['法国巴黎', 'FR PAR'], - ['荷兰阿姆斯特丹', 'NL AMS'], - ['瑞典斯德哥尔摩', 'SE STO'], - ['挪威奥斯陆', 'NO OSL'], - ['丹麦哥本哈根', 'DK CPH'], - ['芬兰赫尔辛基', 'FI HEL'], - ['波兰华沙', 'PL WAW'], - ['捷克布拉格', 'CZ PRG'], - ['奥地利维也纳', 'AT VIE'], - ['匈牙利布达佩斯', 'HU BUD'], - ['意大利米兰', 'IT MIL'], - ['意大利罗马', 'IT ROM'], - ['西班牙马德里', 'ES MAD'], - ['西班牙巴塞罗那', 'ES BCN'], - ['爱尔兰都柏林', 'IE DUB'], - ['葡萄牙里斯本', 'PT LIS'], - ['希腊雅典', 'GR ATH'], - ['土耳其伊斯坦布尔', 'TR IST'], - ['巴西圣保罗', 'BR SAO'], - ['巴西里约热内卢', 'BR RIO'], - ['阿根廷布宜诺斯艾利斯', 'AR BUE'], - ['智利圣地亚哥', 'CL SCL'], - ['墨西哥墨西哥城', 'MX MEX'], - ['南非约翰内斯堡', 'ZA JNB'], - ['南非开普敦', 'ZA CPT'], - ['印度尼西亚', 'ID'], - ['所罗门群岛', 'SB'], - ['波斯尼亚和黑塞哥维那', 'BA'], - ['特立尼达和多巴哥', 'TT'], - ['圣文森特和格林纳丁斯', 'VC'], - ['安提瓜和巴布达', 'AG'], - ['圣基茨和尼维斯', 'KN'], - ['圣多美和普林西比', 'ST'], - ['中非共和国', 'CF'], - ['刚果民主共和国', 'CD'], - ['刚果共和国', 'CG'], - ['多米尼加共和国', 'DO'], - ['巴布亚新几内亚', 'PG'], - ['赤道几内亚', 'GQ'], - ['沙特阿拉伯', 'SA'], - ['捷克共和国', 'CZ'], - ['北马其顿', 'MK'], - ['开曼群岛', 'KY'], - ['法属圭亚那', 'GF'], - ['法属波利尼西亚', 'PF'], - ['新喀里多尼亚', 'NC'], - ['波多黎各', 'PR'], - ['美属维尔京群岛', 'VI'], - ['英属维尔京群岛', 'VG'], - ['阿塞拜疆', 'AZ'], - ['哈萨克斯坦', 'KZ'], - ['吉尔吉斯斯坦', 'KG'], - ['卡萨布兰卡', 'MA CMN'], - ['澳大利亚', 'AU'], - ['新西兰', 'NZ'], - ['马来西亚', 'MY'], - ['尼日利亚', 'NG'], - ['摩尔多瓦', 'MD'], - ['巴基斯坦', 'PK'], - ['阿根廷', 'AR'], - ['阿联酋', 'AE'], - ['柬埔寨', 'KH'], - ['东帝汶', 'TL'], - ['梵蒂冈', 'VA'], - ['百慕大', 'BM'], - ['格陵兰', 'GL'], - ['南极洲', 'AQ'], - ['罗马尼亚', 'RO'], - ['哥伦比亚', 'CO'], - ['菲律宾', 'PH'], - ['以色列', 'IL'], - ['比利时', 'BE'], - ['葡萄牙', 'PT'], - ['波兰', 'PL'], - ['希腊', 'GR'], - ['芬兰', 'FI'], - ['爱尔兰', 'IE'], - ['克罗地亚', 'HR'], - ['斯洛伐克', 'SK'], - ['斯洛文尼亚', 'SI'], - ['塞尔维亚', 'RS'], - ['保加利亚', 'BG'], - ['白俄罗斯', 'BY'], - ['爱沙尼亚', 'EE'], - ['拉脱维亚', 'LV'], - ['卢森堡', 'LU'], - ['塞浦路斯', 'CY'], - ['马耳他', 'MT'], - ['阿尔巴尼亚', 'AL'], - ['黑山', 'ME'], - ['安道尔', 'AD'], - ['列支敦士登', 'LI'], - ['摩纳哥', 'MC'], - ['圣马力诺', 'SM'], - ['直布罗陀', 'GI'], - ['立陶宛', 'LT'], - ['马其顿', 'MK'], - ['俄罗斯', 'RU'], - ['乌克兰', 'UA'], - ['匈牙利', 'HU'], - ['西班牙', 'ES'], - ['土耳其', 'TR'], - ['加拿大', 'CA'], - ['意大利', 'IT'], - ['奥地利', 'AT'], - ['阿富汗', 'AF'], - ['卡塔尔', 'QA'], - ['索马里', 'SO'], - ['新加坡', 'SG'], - ['印度', 'IN'], - ['印尼', 'ID'], - ['日本', 'JP'], - ['香港', 'HK'], - ['台湾', 'TW'], - ['美国', 'US'], - ['韩国', 'KR'], - ['英国', 'UK'], - ['德国', 'DE'], - ['荷兰', 'NL'], - ['法国', 'FR'], - ['美西', 'US'], - ['巴西', 'BR'], - ['智利', 'CL'], - ['澳门', 'MO'], - ['泰国', 'TH'], - ['越南', 'VN'], - ['瑞典', 'SE'], - ['丹麦', 'DK'], - ['多哥', 'TG'], - ['埃及', 'EG'], - ['冰岛', 'IS'], - ['挪威', 'NO'], - ['南非', 'ZA'], - ['古巴', 'CU'], - ['斐济', 'FJ'], - ['关岛', 'GU'], - ['缅甸', 'MM'], - ['蒙古', 'MN'], - ['老挝', 'LA'], - ['捷克', 'CZ'], - ['沙特', 'SA'], - ['墨西哥', 'MX'], - ['夏威夷', 'US HNL'], - ['迪拜', 'AE DXB'], - ['悉尼', 'AU SYD'], - ['墨尔本', 'AU MEL'], - ['布里斯班', 'AU BNE'], - ['珀斯', 'AU PER'], - ['多伦多', 'CA TOR'], - ['温哥华', 'CA YVR'], - ['蒙特利尔', 'CA YUL'], - ['卡尔加里', 'CA YYC'], - ['埃塞俄比亚', 'ET'], - ['肯尼亚', 'KE'], - ['坦桑尼亚', 'TZ'], - ['乌干达', 'UG'], - ['加纳', 'GH'], - ['科特迪瓦', 'CI'], - ['塞内加尔', 'SN'], - ['喀麦隆', 'CM'], - ['安哥拉', 'AO'], - ['苏丹', 'SD'], - ['阿尔及利亚', 'DZ'], - ['突尼斯', 'TN'], - ['利比亚', 'LY'], - ['津巴布韦', 'ZW'], - ['赞比亚', 'ZM'], - ['博茨瓦纳', 'BW'], - ['毛里求斯', 'MU'], - ['塞舌尔', 'SC'], - ['马达加斯加', 'MG'], - ['尼泊尔', 'NP'], - ['孟加拉国', 'BD'], - ['斯里兰卡', 'LK'], - ['哈萨克斯坦', 'KZ'], - ['乌兹别克斯坦', 'UZ'], - ['塔吉克斯坦', 'TJ'], - ['土库曼斯坦', 'TM'], - ['伊朗', 'IR'], - ['伊拉克', 'IQ'], - ['约旦', 'JO'], - ['黎巴嫩', 'LB'], - ['巴勒斯坦', 'PS'], - ['科威特', 'KW'], - ['巴林', 'BH'], - ['阿曼', 'OM'], - ['也门', 'YE'], - ['叙利亚', 'SY'], - ['卡塔尔', 'QA'], - ['马尔代夫', 'MV'], - ['文莱', 'BN'], - ['新喀里多尼亚', 'NC'], - ['巴拿马', 'PA'], - ['哥斯达黎加', 'CR'], - ['危地马拉', 'GT'], - ['洪都拉斯', 'HN'], - ['萨尔瓦多', 'SV'], - ['尼加拉瓜', 'NI'], - ['牙买加', 'JM'], - ['海地', 'HT'], - ['巴哈马', 'BS'], - ['巴巴多斯', 'BB'], - ['乌拉圭', 'UY'], - ['巴拉圭', 'PY'], - ['玻利维亚', 'BO'], - ['秘鲁', 'PE'], - ['厄瓜多尔', 'EC'], - ['委内瑞拉', 'VE'], - ['苏里南', 'SR'], - ['圭亚那', 'GY'] - ]; - - const sortedRegionAliases = regionAliases - .map(([alias, replacement]) => [alias, replacement, alias.toLowerCase()]) - .sort((firstAlias, secondAlias) => secondAlias[0].length - firstAlias[0].length); - - const flagCodeOverrides = { - '🇬🇧': 'UK', - '🇼🇸': 'TW' - }; - - const knownRegionCodes = new Set( - regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0]) - ); - Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code)); - const knownCityCodes = new Set( - regionAliases - .map(([, replacement]) => replacement.split(/\s+/)[1]) - .filter(code => /^[A-Z]{3}$/.test(code || '')) - ); - - const escapeRegex = utils.escapeRegex; - const leadingFlagRegex = /^([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*/u; - const inlineFlagRegex = /^(.{1,24}?)([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*(.*)$/u; - const normalizeLeadingMojibakeFlag = utils.normalizeLeadingMojibakeFlag - || utils.stripLeadingMojibakeFlag - || (name => String(name).trim()); - - const flagToRegionCode = flag => utils.flagToRegionCode(flag, flagCodeOverrides); - - const providerAliases = [ - ['搬瓦工', 'BWH'], - ['Bandwagon', 'BWH'], - ['BWG', 'BWH'], - ['BWH', 'BWH'], - ['SkyStroll', 'SkyStroll'], - ['ISIF', 'ISIF'], - ['RFC', 'RFC'] - ]; - const providerAliasMatchers = providerAliases.map(([alias, provider]) => [ - provider, - /^[A-Za-z0-9.]+$/.test(alias) - ? new RegExp(`(^|[^A-Za-z0-9])${escapeRegex(alias)}([^A-Za-z0-9]|$)`, 'i') - : new RegExp(escapeRegex(alias), 'i') - ]); - const embeddedRegionAliasMatchers = sortedRegionAliases.map(([alias, replacement]) => [ - replacement, - /^[A-Za-z0-9 ]+$/.test(alias) - ? new RegExp(`(^|[^A-Za-z])${escapeRegex(alias)}(?=$|[^A-Za-z])`, 'i') - : new RegExp(escapeRegex(alias), 'i') - ]); - - const stripExistingSuffix = (name, subName) => { - if (!subName) return name.trim(); - const suffixRegex = suffixRegexCache[subName] || ( - suffixRegexCache[subName] = new RegExp(`\\s+-\\s*${escapeRegex(subName)}(?:\\s+\\d{2})?\\s*$`, 'i') - ); - return name.replace(suffixRegex, '').trim(); - }; - - const joinAlias = utils.joinAlias; - - const normalizeLeadingRegion = name => { - const cleanName = name.trim(); - const cleanNameLower = cleanName.toLowerCase(); - - for (const [alias, replacement, aliasLower] of sortedRegionAliases) { - if (!cleanNameLower.startsWith(aliasLower)) continue; - - const rest = cleanName.slice(alias.length); - if (/^[A-Za-z]/.test(alias) && rest && !/^[\s\d[|\-.x]/i.test(rest)) { - continue; - } - - return joinAlias(replacement, rest); - } - - return cleanName; - }; - - const leadingCodeOf = name => { - const match = name.match(/^([A-Z]{2})(?=\b|\s|-|$|\d)/); - return match ? match[1] : ''; - }; - - const hasKnownRegionCode = name => { - const leadingCode = leadingCodeOf(name); - if (knownRegionCodes.has(leadingCode)) return true; - - const proMatch = name.match(/^Pro-\s*(.+)$/i); - return proMatch ? knownRegionCodes.has(leadingCodeOf(proMatch[1])) : false; - }; - - const hasSpecificRegionCode = (name, regionCode) => { - if (!regionCode) return false; - if (leadingCodeOf(name) === regionCode) return true; - - const proMatch = name.match(/^Pro-\s*(.+)$/i); - return proMatch ? leadingCodeOf(proMatch[1]) === regionCode : false; - }; - - const moveProRegionToFront = (name, regionCode) => { - if (!regionCode) return name; - - const proRegionMatch = name.match(/^Pro-\s*([A-Z]{2})(.*)$/i); - if (!proRegionMatch || proRegionMatch[1].toUpperCase() !== regionCode) return name; - - const rest = proRegionMatch[2].trim(); - return rest ? `${regionCode} Pro-${rest}` : regionCode; - }; - - const providerOf = name => { - const cleanName = String(name); - for (const [provider, aliasRegex] of providerAliasMatchers) { - if (aliasRegex.test(cleanName)) return provider; - } - return ''; - }; - - const embeddedRegionPrefixOf = (name, fallbackRegionCode) => { - if (fallbackRegionCode) return fallbackRegionCode; - - const cleanName = String(name); - let bestIndex = Number.POSITIVE_INFINITY; - let bestRegion = ''; - - for (const [replacement, aliasRegex] of embeddedRegionAliasMatchers) { - const match = cleanName.match(aliasRegex); - if (!match) continue; - - const matchIndex = match.index + (match[1] ? match[1].length : 0); - if (matchIndex < bestIndex) { - bestIndex = matchIndex; - bestRegion = replacement; - } - } - - for (const match of cleanName.matchAll(/(^|[^A-Za-z])([A-Z]{2})(?:\d{1,2})?(?=$|[^A-Za-z])/g)) { - const regionCode = match[2].toUpperCase(); - if (!knownRegionCodes.has(regionCode)) continue; - - const matchIndex = match.index + (match[1] ? match[1].length : 0); - if (matchIndex < bestIndex) { - bestIndex = matchIndex; - bestRegion = regionCode; - } - } - - return bestRegion; - }; - - const normalizeProviderRegionName = (name, fallbackRegionCode) => { - if (/^[A-Z]{2}(?:\s+[A-Z]{3})?\s+\d{2}(?=$|\s)/.test(name)) return name; - - const provider = providerOf(name); - if (!provider) return name; - - const regionPrefix = embeddedRegionPrefixOf(name, fallbackRegionCode); - return regionPrefix ? `${regionPrefix} ${provider}` : name; - }; - - const simplifyCoreName = name => { - return name - .replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '') - .replace(/\u200d/g, '') - .replace(/\s*\|\s*流媒体/gi, '') - .replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1X') - .replace(/(\d+(?:\.\d+)?)倍/gi, '$1X') - .replace(/\b(\d+(?:\.\d+)?)x\b/gi, '$1X') - .replace(/([A-Z]{2})\s*(专属|家宽|AI|高速|专线)(\d+)/g, '$1 $2 $3') - .replace(/\b(AI)(\d+)/g, '$1 $2') - .replace(/\b(Pro-)\s+/gi, '$1') - .replace(/^((?:Pro-\s*)?)([A-Z]{2})(?=[\u4e00-\u9fff]|\d)/u, '$1$2 ') - .replace(/\s*\|\s*/g, '|') - .replace(/\s+\[/g, ' [') - .replace(/\s{2,}/g, ' ') - .trim(); - }; - - const moveMultiplierToEnd = utils.moveMultiplierToEnd; - - const normalizeRegionSequence = name => utils.normalizeRegionSequence(name, { allowCityCode: true }); - - const stripTrailingFeatureSequence = name => String(name) - .replace(/\bHY\s+(\d{1,2})(?=-[A-Z]{2}\d{2}\b)/i, (match, version) => `HY${Number(version)}`) - .replace(/^([A-Z]{2}(?:\s+[A-Z]{3})?)\s+([A-Z][A-Z0-9.]{1,15})\s+\d{1,2}$/i, (match, regionPrefix, featureName) => ( - /^[A-Z]{2}$/.test(regionPrefix) && knownCityCodes.has(featureName.toUpperCase()) - ? match - : `${regionPrefix} ${featureName}` - )) - .replace(/\s{2,}/g, ' ') - .trim(); - - const regionSequenceOf = name => { - const sequenceMatch = name.match(/^((?:[\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*)?([A-Z]{2}(?:\s+[A-Z]{3})?)(?:\s+(\d{2}))?(\s.*)?$/u); - if (!sequenceMatch) return null; - - const prefix = sequenceMatch[1] || ''; - let regionCode = sequenceMatch[2]; - const sequenceNumber = sequenceMatch[3] || ''; - let rest = sequenceMatch[4] || ''; - const cityCodeMatch = regionCode.match(/^([A-Z]{2})\s+([A-Z]{3})$/); - if (cityCodeMatch && !knownCityCodes.has(cityCodeMatch[2])) { - regionCode = cityCodeMatch[1]; - rest = ` ${cityCodeMatch[2]}${rest}`; - } - - return { - prefix, - regionCode, - sequenceNumber, - rest - }; - }; - - const withRegionSequence = (sequenceInfo, sequenceNumber) => { - return `${sequenceInfo.prefix}${sequenceInfo.regionCode} ${sequenceNumber}${sequenceInfo.rest}` - .replace(/\s{2,}/g, ' ') - .trim(); - }; - - const withInferredRegionSequence = sequenceInfo => { - const featureSequenceMatch = sequenceInfo.rest.match(/^\s+(专属|家宽|AI|高速|专线)\s+(\d{1,2})(?!\d)(?!\s*(?:[xX倍]|\.\d))(.*)$/i); - if (!featureSequenceMatch) return withRegionSequence(sequenceInfo, '01'); - - const featureName = featureSequenceMatch[1].toUpperCase() === 'AI' - ? 'AI' - : featureSequenceMatch[1]; - const inferredSequence = featureSequenceMatch[2].padStart(2, '0'); - const remainingRest = featureSequenceMatch[3] || ''; - - return withRegionSequence( - { - ...sequenceInfo, - rest: ` ${featureName}${remainingRest}` - }, - inferredSequence - ); - }; - - const compactRegionSequences = () => { - const indexesByRegion = Object.create(null); - - sequenceInfos.forEach((sequenceInfo, proxyIndex) => { - if (!sequenceInfo) return; - - const groupKey = `${subNames[proxyIndex]}\u0000${sequenceInfo.prefix}|${sequenceInfo.regionCode}`; - const regionIndexes = indexesByRegion[groupKey] || ( - indexesByRegion[groupKey] = [] - ); - regionIndexes.push(proxyIndex); - }); - - Object.keys(indexesByRegion).forEach(groupKey => { - const regionIndexes = indexesByRegion[groupKey]; - regionIndexes.forEach((proxyIndex, sequenceIndex) => { - const sequenceInfo = sequenceInfos[proxyIndex]; - const compactSequenceNumber = (sequenceIndex + 1) - .toString() - .padStart(2, '0'); - const compactSequenceInfo = { - ...sequenceInfo, - sequenceNumber: compactSequenceNumber - }; - - sequenceInfos[proxyIndex] = compactSequenceInfo; - normalizedCoreNames[proxyIndex] = withRegionSequence(compactSequenceInfo, compactSequenceNumber); - }); - }); - }; - - const normalizeBilingualName = name => { - const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/); - if (!bilingualMatch) return name; - - const leftName = normalizeLeadingRegion(bilingualMatch[1]); - const rightName = normalizeLeadingRegion(bilingualMatch[2]); - const leftCode = leadingCodeOf(leftName); - const rightCode = leadingCodeOf(rightName); - - if (leftCode && rightCode && leftCode === rightCode) { - return rightName === rightCode ? leftName : rightName; - } - if (rightCode) return rightName; - if (leftCode) return leftName; - - return name.replace(/\s*|\s*/g, ' '); - }; - - const normalizeCoreName = name => { - name = normalizeLeadingMojibakeFlag(name); - const leadingFlagMatch = name.match(leadingFlagRegex); - const inlineFlagMatch = leadingFlagMatch - ? null - : name.match(inlineFlagRegex); - const flag = leadingFlagMatch ? leadingFlagMatch[1] : inlineFlagMatch ? inlineFlagMatch[2] : ''; - let core = name.trim(); - - if (leadingFlagMatch) { - core = name.slice(leadingFlagMatch[0].length).trim(); - } else if (inlineFlagMatch) { - const prefix = inlineFlagMatch[1].trim(); - const rest = inlineFlagMatch[3].trim(); - core = rest ? `${prefix}${prefix.endsWith('-') ? '' : ' '}${rest}` : prefix; - } - - core = core - .replace(/\[DIP.*?]/gi, '[DIP]'); - - core = normalizeBilingualName(core); - core = normalizeLeadingRegion(core); - - if (/^Pro-\s*/i.test(core)) { - const proMatch = core.match(/^(Pro-\s*)(.+)$/i); - if (proMatch) { - core = `${proMatch[1]}${normalizeLeadingRegion(proMatch[2])}`; - } - } - - core = core - .replace(/^([A-Z]{2})\s*(实验性|标准|高级)\s+IEPL\s+专线\s*\d+(.*)$/i, '$1 $2$3') - .replace(/\s{2,}/g, ' ') - .trim(); - - const flagRegionCode = flagToRegionCode(flag); - core = normalizeProviderRegionName(core, flagRegionCode); - - core = moveProRegionToFront(core, flagRegionCode); - - if ( - flagRegionCode && - !hasSpecificRegionCode(core, flagRegionCode) && - !hasKnownRegionCode(core) - ) { - core = joinAlias(flagRegionCode, core); - } - - core = stripTrailingFeatureSequence( - normalizeRegionSequence(normalizeHyphenatedRegionFeatures(moveMultiplierToEnd(simplifyCoreName(core)))) - ) - .replace(/^([A-Z]{2}(?:\s+[A-Z]{3})?\s+\d{2}\s+(?:标准|高级))\s+1(?=$|\s)/i, '$1'); - - const coreRegionCode = leadingCodeOf(core); - const outputFlag = flag && knownRegionCodes.has(coreRegionCode) - ? utils.regionCodeToFlag(coreRegionCode) || flag - : flag; - - return outputFlag ? `${outputFlag} ${core}` : core; - }; proxies.forEach((proxy, proxyIndex) => { - const proxyName = typeof proxy.name === 'string' ? proxy.name : ''; - const subName = typeof proxy._subName === 'string' ? proxy._subName.trim() : ''; - const coreName = stripExistingSuffix(proxyName, subName); - const normalizedCoreName = normalizeCoreName(coreName); - const sequenceInfo = regionSequenceOf(normalizedCoreName); + // 非对象或非字符串名称不参与处理,避免改动无效节点数据。 + if (!proxy || typeof proxy !== 'object' || typeof proxy.name !== 'string') { + baseNames[proxyIndex] = null; + return; + } - normalizedCoreNames[proxyIndex] = normalizedCoreName; - subNames[proxyIndex] = subName; - sequenceInfos[proxyIndex] = sequenceInfo; - }); + const proxyName = proxy.name; + const subName = proxy && typeof proxy._subName === 'string' + ? proxy._subName.trim() + : ''; + // 没有订阅名时完全保留原字符串;公共工具的兼容实现会顺手 trim。 + const coreName = subName ? stripSubscriptionSuffix(proxyName, subName) : proxyName; + const baseName = subName + ? (coreName ? `${coreName} - ${subName}` : subName) + : coreName; - compactRegionSequences(); - - normalizedCoreNames.forEach((normalizedCoreName, proxyIndex) => { - const subName = subNames[proxyIndex]; - const sequenceInfo = sequenceInfos[proxyIndex]; - const needsLeadingSequence = sequenceInfo && !sequenceInfo.sequenceNumber; - const coreName = needsLeadingSequence - ? withInferredRegionSequence(sequenceInfo) - : normalizedCoreName; - const baseName = subName ? `${coreName} - ${subName}` : coreName; - - finalCoreNames[proxyIndex] = coreName; baseNames[proxyIndex] = baseName; - globalCount[baseName] = (globalCount[baseName] || 0) + 1; + coreNames[proxyIndex] = coreName; + subNames[proxyIndex] = subName; + nameCounts[baseName] = (nameCounts[baseName] || 0) + 1; }); return proxies.map((proxy, proxyIndex) => { const baseName = baseNames[proxyIndex]; - const coreName = finalCoreNames[proxyIndex]; - const subName = subNames[proxyIndex]; - const sequenceInfo = sequenceInfos[proxyIndex]; + if (baseName === null || !proxy || typeof proxy !== 'object') return proxy; - if (globalCount[baseName] > 1 && !sequenceInfo) { - currentCount[baseName] = (currentCount[baseName] || 0) + 1; - const duplicateNumber = currentCount[baseName].toString().padStart(2, '0'); - proxy.name = subName - ? `${coreName} ${duplicateNumber} - ${subName}` + if (nameCounts[baseName] > 1) { + currentCounts[baseName] = (currentCounts[baseName] || 0) + 1; + const duplicateNumber = String(currentCounts[baseName]).padStart(2, '0'); + proxy.name = subNames[proxyIndex] + ? `${coreNames[proxyIndex]} ${duplicateNumber} - ${subNames[proxyIndex]}` : `${baseName} ${duplicateNumber}`; } else { proxy.name = baseName;