/** * Sub-Store 节点重命名脚本 * - 统一节点前缀为地区短码,保留旗帜与订阅后缀 * - 清理常见冗余线路描述,例如 DIP 地区说明、IEPL 专线 * - 基于归一化后的完整名称做精准去重 */ function operator(proxies) { const globalCount = {}; const currentCount = {}; const regionAliases = [ ['United Arab Emirates', 'AE'], ['United States Los Angeles', 'US LA'], ['United States San Jose', 'US SJ'], ['United States Seattle', 'US SEA'], ['Australia Sydney', 'AU SYD'], ['Russia St. Petersburg', 'RU LED'], ['Russia Moscow', 'RU MOW'], ['USA Los Angeles', 'US LA'], ['USA San Jose', 'US SJ'], ['USA Seattle', 'US SEA'], ['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'], ['Toronto', 'CA TOR'], ['Casablanca', 'MA CMN'], ['USA', 'US'], ['美国圣何塞', 'US SJ'], ['美国洛杉矶', 'US LA'], ['美国阿什本', 'US IAD'], ['日本东京', 'JP TYO'], ['韩国首尔', 'KR SEL'], ['英国伦敦', 'UK LON'], ['瑞士苏黎世', 'CH ZRH'], ['印度尼西亚', 'ID'], ['所罗门群岛', 'SB'], ['阿塞拜疆', 'AZ'], ['哈萨克斯坦', 'KZ'], ['吉尔吉斯斯坦', 'KG'], ['卡萨布兰卡', 'MA CMN'], ['澳大利亚', 'AU'], ['新西兰', 'NZ'], ['马来西亚', 'MY'], ['尼日利亚', 'NG'], ['摩尔多瓦', 'MD'], ['巴基斯坦', 'PK'], ['阿根廷', 'AR'], ['阿联酋', 'AE'], ['柬埔寨', 'KH'], ['东帝汶', 'TL'], ['梵蒂冈', 'VA'], ['百慕大', 'BM'], ['格陵兰', 'GL'], ['南极洲', 'AQ'], ['罗马尼亚', 'RO'], ['哥伦比亚', 'CO'], ['菲律宾', 'PH'], ['以色列', 'IL'], ['比利时', 'BE'], ['立陶宛', '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'], ['巴西', 'BR'], ['智利', 'CL'], ['澳门', 'MO'], ['泰国', 'TH'], ['越南', 'VN'], ['瑞典', 'SE'], ['丹麦', 'DK'], ['多哥', 'TG'], ['埃及', 'EG'], ['冰岛', 'IS'], ['挪威', 'NO'], ['南非', 'ZA'], ['古巴', 'CU'], ['斐济', 'FJ'], ['关岛', 'GU'], ['缅甸', 'MM'], ['蒙古', 'MN'], ['老挝', 'LA'], ['捷克', 'CZ'], ['沙特', 'SA'], ['墨西哥', 'MX'], ['迪拜', 'AE DXB'], ['悉尼', 'AU SYD'], ['墨尔本', 'AU MEL'], ['多伦多', 'CA TOR'] ]; const sortedRegionAliases = regionAliases .slice() .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 escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const flagToRegionCode = flag => { if (!flag) return ''; if (flagCodeOverrides[flag]) return flagCodeOverrides[flag]; const codePoints = [...flag].map(char => char.codePointAt(0)); if ( codePoints.length !== 2 || codePoints.some(codePoint => codePoint < 0x1F1E6 || codePoint > 0x1F1FF) ) { return ''; } return codePoints .map(codePoint => String.fromCharCode(codePoint - 0x1F1E6 + 65)) .join(''); }; const stripExistingSuffix = (name, subName) => { if (!subName) return name.trim(); const suffixRegex = new RegExp(`\\s+-\\s*${escapeRegex(subName)}(?:\\s+\\d{2})?\\s*$`, 'i'); return name.replace(suffixRegex, '').trim(); }; const joinAlias = (replacement, rest) => { const cleanRest = rest.trim(); if (!cleanRest) return replacement; if (/^[|/\-]/.test(cleanRest)) { return `${replacement}${cleanRest}`; } return `${replacement} ${cleanRest}`; }; const normalizeLeadingRegion = name => { const cleanName = name.trim(); for (const [alias, replacement] of sortedRegionAliases) { if (!cleanName.toLowerCase().startsWith(alias.toLowerCase())) 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 proRegionRegex = new RegExp(`^Pro-\\s*${escapeRegex(regionCode)}(?=\\s|[\\u4e00-\\u9fff]|\\d)(.*)$`, 'i'); const proRegionMatch = name.match(proRegionRegex); if (!proRegionMatch) return name; const rest = proRegionMatch[1].trim(); return rest ? `${regionCode} Pro-${rest}` : regionCode; }; const simplifyCoreName = name => { return name .replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '') .replace(/\u200d/g, '') .replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x') .replace(/(\d+(?:\.\d+)?)倍/gi, '$1x') .replace(/\b(\d+(?:\.\d+)?)X\b/g, '$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+\[/g, ' [') .replace(/\s{2,}/g, ' ') .trim(); }; 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 => { const flagPattern = '([\\u{1F1E6}-\\u{1F1FF}]{2}|🏴‍☠️)'; const leadingFlagMatch = name.match(new RegExp(`^${flagPattern}\\s*`, 'u')); const inlineFlagMatch = leadingFlagMatch ? null : name.match(new RegExp(`^(.{1,24}?)${flagPattern}\\s*(.*)$`, 'u')); 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)) { core = core.replace(/^(Pro-\s*)(.+)$/i, (fullMatch, proPrefix, restName) => { return `${proPrefix}${normalizeLeadingRegion(restName)}`; }); } core = core .replace(/^([A-Z]{2})\s*(实验性|标准|高级)\s+IEPL\s+专线\s*(\d+)(.*)$/i, '$1 $2 $3$4') .replace(/搬瓦工荷兰/g, '搬瓦工') .replace(/\s{2,}/g, ' ') .trim(); const flagRegionCode = flagToRegionCode(flag); core = moveProRegionToFront(core, flagRegionCode); if ( flagRegionCode && !hasSpecificRegionCode(core, flagRegionCode) && !hasKnownRegionCode(core) ) { core = joinAlias(flagRegionCode, core); } core = simplifyCoreName(core); return flag ? `${flag} ${core}` : core; }; proxies.forEach(proxy => { 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 baseName = subName ? `${normalizedCoreName} - ${subName}` : normalizedCoreName; proxy._tempBaseName = baseName; globalCount[baseName] = (globalCount[baseName] || 0) + 1; }); return proxies.map(proxy => { const baseName = proxy._tempBaseName; if (globalCount[baseName] > 1) { currentCount[baseName] = (currentCount[baseName] || 0) + 1; proxy.name = `${baseName} ${currentCount[baseName].toString().padStart(2, '0')}`; } else { proxy.name = baseName; } delete proxy._tempBaseName; return proxy; }); }