diff --git a/substore/rename.js b/substore/rename.js index 74ee2c7..b9c3ae3 100644 --- a/substore/rename.js +++ b/substore/rename.js @@ -716,6 +716,25 @@ function operator(proxies) { .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 normalizeBilingualName = name => { const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/); if (!bilingualMatch) return name; @@ -802,7 +821,7 @@ function operator(proxies) { const sequenceInfo = sequenceInfos[proxyIndex]; const needsLeadingSequence = sequenceInfo && !sequenceInfo.sequenceNumber; const coreName = needsLeadingSequence - ? withRegionSequence(sequenceInfo, '01') + ? withInferredRegionSequence(sequenceInfo) : normalizedCoreName; const baseName = subName ? `${coreName} - ${subName}` : coreName; diff --git a/substore/yuetong-rename.js b/substore/yuetong-rename.js index 97ceb4f..4f007e5 100644 --- a/substore/yuetong-rename.js +++ b/substore/yuetong-rename.js @@ -1,21 +1,131 @@ /** * YueTong 专用节点清理脚本 - * 负责去掉“悦·”品牌前缀、钻石等装饰,以及压短 AI 解锁文案。 + * 负责去掉“悦·”品牌前缀、钻石、专属/家宽等线路标识,以及压短 AI 解锁文案。 + * 保留线路标识后面的序号,并按地区稳定归并。 */ // noinspection JSUnusedGlobalSymbols function operator(proxies) { - return proxies.map(proxy => { + const regionAliases = [ + ['Hong Kong', 'HK'], + ['香港', 'HK'], + ['HK', 'HK'], + ['Japan', 'JP'], + ['日本', 'JP'], + ['JP', 'JP'], + ['Singapore', 'SG'], + ['新加坡', 'SG'], + ['狮城', 'SG'], + ['SG', 'SG'], + ['Taiwan', 'TW'], + ['台湾', 'TW'], + ['台灣', 'TW'], + ['TW', 'TW'], + ['United States', 'US'], + ['America', 'US'], + ['USA', 'US'], + ['美国', 'US'], + ['美國', 'US'], + ['美西', 'US'], + ['美东', 'US'], + ['美東', 'US'], + ['US', 'US'], + ['Korea', 'KR'], + ['韩国', 'KR'], + ['韓國', 'KR'], + ['KR', 'KR'], + ['United Kingdom', 'UK'], + ['Britain', 'UK'], + ['英国', 'UK'], + ['英國', 'UK'], + ['UK', 'UK'], + ['Germany', 'DE'], + ['德国', 'DE'], + ['德國', 'DE'], + ['DE', 'DE'], + ['Netherlands', 'NL'], + ['荷兰', 'NL'], + ['荷蘭', 'NL'], + ['NL', 'NL'] + ]; + const sortedRegionAliases = regionAliases + .map(([alias, replacement]) => [alias, replacement, alias.toLowerCase()]) + .sort((firstAlias, secondAlias) => secondAlias[0].length - firstAlias[0].length); + const leadingFlagRegex = /^([\u{1F1E6}-\u{1F1FF}]{2})\s*/u; + + const flagToRegionCode = flag => { + if (!flag) return ''; + const codePoints = [...flag].map(char => char.codePointAt(0)); + if ( + codePoints.length !== 2 || + codePoints.some(codePoint => codePoint < 0x1F1E6 || codePoint > 0x1F1FF) + ) { + return ''; + } + + const regionCode = codePoints + .map(codePoint => String.fromCharCode(codePoint - 0x1F1E6 + 65)) + .join(''); + return regionCode === 'GB' ? 'UK' : regionCode; + }; + + const regionCodeOf = name => { + const leadingFlagMatch = name.match(leadingFlagRegex); + if (leadingFlagMatch) return flagToRegionCode(leadingFlagMatch[1]); + + const nameLower = name.toLowerCase(); + for (const [alias, replacement, aliasLower] of sortedRegionAliases) { + if (/^[A-Z]{2}$/.test(alias)) { + const codeMatch = name.match(new RegExp(`(^|[^A-Za-z])${alias}([^A-Za-z]|$)`, 'i')); + if (codeMatch) return replacement; + continue; + } + + if (nameLower.includes(aliasLower)) return replacement; + } + + return ''; + }; + + const normalizedItems = proxies.map((proxy, originalIndex) => { if (typeof proxy.name !== 'string') { - return proxy; + return { + proxy, + originalIndex, + regionCode: '' + }; } proxy.name = proxy.name .replace(/悦[·・]\s*/g, '') .replace(/AI解锁/g, 'AI') + .replace(/\s*(?:专属|家宽|高速|专线)(?:\s*(\d+)(?!\s*(?:[xX倍]|\.\d)))?/g, '$1') .replace(/💎/gu, '') + .replace(/([\u4e00-\u9fff])AI\b/g, '$1 AI') .replace(/\s{2,}/g, ' ') .trim(); - return proxy; + return { + proxy, + originalIndex, + regionCode: regionCodeOf(proxy.name) + }; }); + + const regionOrder = Object.create(null); + const groupOrder = normalizedItems.map(item => { + if (!item.regionCode) return item.originalIndex; + if (regionOrder[item.regionCode] === undefined) { + regionOrder[item.regionCode] = item.originalIndex; + } + return regionOrder[item.regionCode]; + }); + + normalizedItems.sort((firstItem, secondItem) => { + const firstGroupOrder = groupOrder[firstItem.originalIndex]; + const secondGroupOrder = groupOrder[secondItem.originalIndex]; + if (firstGroupOrder !== secondGroupOrder) return firstGroupOrder - secondGroupOrder; + return firstItem.originalIndex - secondItem.originalIndex; + }); + + return normalizedItems.map(item => item.proxy); }