/** * Sub-Store 节点重命名脚本 * - 统一节点前缀为地区短码,保留旗帜与订阅后缀 * - 清理常见冗余线路描述,例如 DIP 地区说明、IEPL 专线 * - 基于归一化后的完整名称做精准去重 */ // noinspection JSUnusedGlobalSymbols const COMMON_SCRIPT_VERSION = '20260702-1'; const COMMON_SCRIPT_URL = `https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=${COMMON_SCRIPT_VERSION}`; const hasCurrentRenameUtils = utils => Boolean( utils && utils.version === COMMON_SCRIPT_VERSION ); async function loadRenameUtils() { if (typeof globalThis !== 'undefined' && hasCurrentRenameUtils(globalThis.SubStoreRenameUtils)) { return globalThis.SubStoreRenameUtils; } const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null; let script = cache && cache.get(COMMON_SCRIPT_URL); if (!script) { const response = await $substore.http.get({ url: COMMON_SCRIPT_URL }); script = response && (response.body || response.data || response); if (cache) cache.set(COMMON_SCRIPT_URL, script); } new Function(String(script))(); if (!hasCurrentRenameUtils(globalThis.SubStoreRenameUtils)) { throw new Error('Loaded common.js is missing current rename utilities'); } return globalThis.SubStoreRenameUtils; } 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 baseNames = new Array(proxies.length); const finalCoreNames = new Array(proxies.length); const normalizedCoreNames = 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); normalizedCoreNames[proxyIndex] = normalizedCoreName; subNames[proxyIndex] = subName; sequenceInfos[proxyIndex] = sequenceInfo; }); 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; }); return proxies.map((proxy, proxyIndex) => { const baseName = baseNames[proxyIndex]; const coreName = finalCoreNames[proxyIndex]; const subName = subNames[proxyIndex]; const sequenceInfo = sequenceInfos[proxyIndex]; 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}` : `${baseName} ${duplicateNumber}`; } else { proxy.name = baseName; } return proxy; }); }