/** * Alpha Air 专用节点清理脚本 * - 压掉提供者、三网、家宽等 Alpha Air 专属文案 * - 普通节点只保留地区,非倍率高级节点保留供应商,倍率节点保留倍率 * - 按地区内“普通 -> 倍率 -> 高级”排序并连续编号 * - 不追加订阅后缀,整合订阅最后仍交给 rename.js 处理 */ function operator(proxies) { const regionAliases = [ ['美国夏威夷', 'US HNL'], ['家宽夏威夷', 'US HNL'], ['夏威夷', 'US HNL'], ['Hawaii', 'US HNL'], ['United States', 'US'], ['America', 'US'], ['USA', 'US'], ['美国', 'US'], ['美西', 'US'], ['Japan', 'JP'], ['日本', 'JP'], ['Taiwan', 'TW'], ['台湾', 'TW'], ['Hong Kong', 'HK'], ['香港', 'HK'], ['Singapore', 'SG'], ['新加坡', 'SG'], ['Netherlands', 'NL'], ['荷兰', 'NL'], ['Korea', 'KR'], ['韩国', 'KR'], ['Germany', 'DE'], ['德国', 'DE'], ['United Kingdom', 'UK'], ['英国', 'UK'] ]; 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 inlineFlagRegex = /^(.{1,24}?)([\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 regionCodeToFlag = regionCode => { const flagCode = regionCode === 'UK' ? 'GB' : regionCode; if (!/^[A-Z]{2}$/.test(flagCode)) return ''; return flagCode .split('') .map(char => String.fromCodePoint(char.charCodeAt(0) - 65 + 0x1F1E6)) .join(''); }; const stripExistingSuffix = name => name .replace(/\s+-\s*Alpha Air(?:\s+\d{2})?\s*$/i, '') .trim(); const stripFlag = name => { const leadingFlagMatch = name.match(leadingFlagRegex); if (leadingFlagMatch) { return { flag: leadingFlagMatch[1], text: name.slice(leadingFlagMatch[0].length).trim() }; } const inlineFlagMatch = name.match(inlineFlagRegex); if (!inlineFlagMatch) return { flag: '', text: name.trim() }; const prefix = inlineFlagMatch[1].trim(); const rest = inlineFlagMatch[3].trim(); return { flag: inlineFlagMatch[2], text: rest ? `${prefix} ${rest}`.trim() : prefix }; }; const multiplierOf = name => { const match = name.match(/\b(\d+(?:\.\d+)?)(?:\s*[xX]|倍)\b/i); return match ? `${match[1]}X` : ''; }; const providerOf = name => { if (/SkyStroll/i.test(name)) return 'SkyStroll'; if (/isif/i.test(name)) return 'ISIF'; if (/(?:搬瓦工|Bandwagon|BWH|BWG)/i.test(name)) return 'BWH'; return ''; }; const leadingRegionPrefixOf = name => { const match = name.match(/^([A-Z]{2})(?:\s+([A-Z]{3}))?(?=\s|$|-|\d|\|)/); if (!match) return ''; return match[2] ? `${match[1]} ${match[2]}` : match[1]; }; const aliasRegionPrefixOf = name => { const cleanNameLower = name.toLowerCase(); for (const [alias, replacement, aliasLower] of sortedRegionAliases) { if (cleanNameLower.includes(aliasLower)) return replacement; } if (/\bhk\b/i.test(name)) return 'HK'; if (/\bsg\b/i.test(name)) return 'SG'; if (/\bjp\b/i.test(name)) return 'JP'; if (/\btw\b/i.test(name)) return 'TW'; if (/\bus\b/i.test(name)) return 'US'; return ''; }; const regionPrefixOf = (name, flagRegionCode) => { if (/(?:Hinet|Seednet)/i.test(name)) return 'TW'; return aliasRegionPrefixOf(name) || leadingRegionPrefixOf(name) || flagRegionCode || ''; }; const normalizeProxy = (proxy, originalIndex) => { const rawName = typeof proxy.name === 'string' ? proxy.name : ''; const cleanName = stripExistingSuffix(rawName); const { flag, text } = stripFlag(cleanName); const flagRegionCode = flagToRegionCode(flag); const regionPrefix = regionPrefixOf(text, flagRegionCode); const regionCode = regionPrefix.split(/\s+/)[0]; const cityCode = regionPrefix.split(/\s+/)[1] || ''; const multiplier = multiplierOf(text); const provider = multiplier ? '' : providerOf(text); const normalizedFlag = regionCodeToFlag(regionCode); return { proxy, originalIndex, countryCode: regionCode, cityCode, provider, providerRank: provider ? 1 : 0, multiplierValue: multiplier ? Number(multiplier.replace(/X$/i, '')) : Number.NEGATIVE_INFINITY, normalizedName: [normalizedFlag, regionPrefix, provider, multiplier].filter(Boolean).join(' ').trim() }; }; const countryOrder = Object.create(null); const normalizedItems = proxies.map((proxy, proxyIndex) => { const normalizedItem = normalizeProxy(proxy, proxyIndex); if ( normalizedItem.countryCode && countryOrder[normalizedItem.countryCode] === undefined ) { countryOrder[normalizedItem.countryCode] = Object.keys(countryOrder).length; } return normalizedItem; }); normalizedItems.sort((firstItem, secondItem) => { const firstOrder = countryOrder[firstItem.countryCode]; const secondOrder = countryOrder[secondItem.countryCode]; if (firstOrder !== secondOrder) return firstOrder - secondOrder; if (firstItem.providerRank !== secondItem.providerRank) { return firstItem.providerRank - secondItem.providerRank; } if (firstItem.multiplierValue !== secondItem.multiplierValue) { return firstItem.multiplierValue - secondItem.multiplierValue; } const firstCityRank = firstItem.cityCode ? 1 : 0; const secondCityRank = secondItem.cityCode ? 1 : 0; if (firstCityRank !== secondCityRank) return firstCityRank - secondCityRank; return firstItem.originalIndex - secondItem.originalIndex; }); const currentCount = Object.create(null); return normalizedItems.map(item => { if (!item.normalizedName || !item.countryCode) return item.proxy; currentCount[item.countryCode] = (currentCount[item.countryCode] || 0) + 1; item.proxy.name = `${item.normalizedName} ${String(currentCount[item.countryCode]).padStart(2, '0')}`; return item.proxy; }); }