diff --git a/substore/README.md b/substore/README.md index 95e4b10..9fbcd32 100644 --- a/substore/README.md +++ b/substore/README.md @@ -9,7 +9,8 @@ 1. `sntp-rename.js`:放在 SNTP 单订阅里,注入线路标签。 2. `yuetong-rename.js`:只清理 YueTong 的品牌前缀和装饰符号。 3. `peiqian-rename.js`:只清理 PeiQian 的推荐/用途描述。 -4. `rename.js`:整合订阅最终重命名,负责国家短码、国旗归位、订阅后缀和去重。 +4. `alphaair-rename.js`:只处理 Alpha Air 的供应商压缩、倍率排序和连续编号。 +5. `rename.js`:整合订阅最终重命名,负责国家短码、国旗归位、订阅后缀和去重。 如果某个订阅没有对应的专属脚本,直接进入最后的 `rename.js` 即可。 @@ -20,6 +21,7 @@ | `sntp-rename.js` | SNTP 单订阅 | 注入 `[三网]`、`[CF]`、`[直连]`、`[cn2]` 等线路标签 | | `yuetong-rename.js` | YueTong 单订阅 | 去掉 `悦·`、`💎`,把 `AI解锁` 压成 `AI` | | `peiqian-rename.js` | PeiQian 单订阅 | 压短 `电信联通推荐`、`移动联通推荐`、`高速专线`、`hy2`、倍率和用途描述 | +| `alphaair-rename.js` | Alpha Air 单订阅 | 去掉供应商/提供者/三网/家宽文案,保留地区、城市码、倍率并连续编号 | | `rename.js` | 整合订阅 | 统一国家/城市短码,移动国旗到首部,追加 `- _subName`,处理重名编号 | ## SNTP 规则 @@ -88,6 +90,21 @@ 🇼🇸 TW - PeiQian ``` +## Alpha Air 规则 + +`alphaair-rename.js` 建议挂在 Alpha Air 单条订阅里使用。它会把 Alpha Air 的供应商、提供者和线路文案压掉,并在该订阅内部按地区和倍率编号;之后再交给全局 `rename.js` 追加订阅后缀。 + +```text +🇺🇸 4X|美国夏威夷家宽|三网1 +=> 🇺🇸 US HNL 4X 01 + +3X 🇨🇳 台湾家宽Hinet|COP +=> 🇹🇼 TW 3X 01 + +🇳🇱 搬瓦工荷兰|JC +=> 🇳🇱 NL 01 +``` + ## 全局重命名 `rename.js` 适合放在整合订阅最后。它做的事情更通用: @@ -119,4 +136,4 @@ Pro-🇭🇰 Sharon|守夜人 - Alpha 8K Snell - `sntp-rename.js` 不带 `_subName` 判断,适合挂在 SNTP 单订阅里;如果放进整合订阅,可能会误伤其他带 `BGP`、`D`、`CT` 的节点。 - `yuetong-rename.js` 和 `peiqian-rename.js` 也不带 `_subName` 判断,适合挂在对应单订阅里;如果放进整合订阅,需要确认规则不会误伤其他订阅。 - `rename.js` 会剥离并重新追加 `- _subName`,重复运行不会叠加订阅后缀。 -- 如果新增订阅里出现很短、很泛的关键词,例如 `D`、`CT`、`BGP`、`S1`,优先拆成单订阅脚本,不建议放进全局 `rename.js`。 +- 如果新增订阅里出现很短、很泛的关键词,例如 `D`、`CT`、`BGP`、`S1`,或像 Alpha Air 这种强绑定单机场的供应商压缩规则,优先拆成单订阅脚本,不建议放进全局 `rename.js`。 diff --git a/substore/alphaair-rename.js b/substore/alphaair-rename.js new file mode 100644 index 0000000..4e4c8bb --- /dev/null +++ b/substore/alphaair-rename.js @@ -0,0 +1,182 @@ +/** + * 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 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 normalizedFlag = regionCodeToFlag(regionCode); + + return { + proxy, + originalIndex, + countryCode: regionCode, + cityCode, + multiplierValue: multiplier ? Number(multiplier.replace(/X$/i, '')) : Number.NEGATIVE_INFINITY, + normalizedName: [normalizedFlag, regionPrefix, 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.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; + }); +} diff --git a/substore/peiqian-rename.js b/substore/peiqian-rename.js deleted file mode 100644 index 577f992..0000000 --- a/substore/peiqian-rename.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * PeiQian 专用节点清理脚本 - * 负责清理推荐、用途、hy2、倍率等 PeiQian 节点文案。 - */ -function operator(proxies) { - return proxies.map(proxy => { - if (typeof proxy.name !== 'string') { - return proxy; - } - - proxy.name = proxy.name - .replace(/^AWS\s*新加坡\s*/i, '新加坡 AWS ') - .replace(/\s*\|\s*合适下载使用\s*-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x') - .replace(/\s*\|\s*合适下载使用/gi, '') - .replace(/\s*\|\s*避免晚高峰使用/gi, '') - .replace(/\s*\|\s*电信联通移动推荐\s*-\s*hy2/gi, ' [三网 HY2]') - .replace(/\s*\|\s*电信联通移动推荐/gi, ' [三网]') - .replace(/\s*\|\s*电信联通推荐\s*-\s*hy2/gi, ' [电联 HY2]') - .replace(/\s*\|\s*电信联通推荐/gi, ' [电联]') - .replace(/\s*\|\s*移动联通推荐\s*-\s*hy2/gi, ' [移联 HY2]') - .replace(/\s*\|\s*移动联通推荐/gi, ' [移联]') - .replace(/\s*\|\s*高速专线(?:推荐)?\s*-\s*hy2/gi, ' [HY2]') - .replace(/\s*\|\s*高速专线推荐/gi, ' [专线]') - .replace(/\s*\|\s*三网推荐/gi, ' [三网]') - .replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x') - .replace(/(\d+(?:\.\d+)?)倍/gi, '$1x') - .replace(/\s+\[/g, ' [') - .replace(/\s{2,}/g, ' ') - .trim(); - - return proxy; - }); -} diff --git a/substore/rename.js b/substore/rename.js index 6477aeb..11bcd57 100644 --- a/substore/rename.js +++ b/substore/rename.js @@ -7,11 +7,7 @@ function operator(proxies) { const globalCount = Object.create(null); const currentCount = Object.create(null); - const serialCount = Object.create(null); - const currentSerial = Object.create(null); - const countryOrder = Object.create(null); const baseNames = new Array(proxies.length); - const renamedItems = new Array(proxies.length); const suffixRegexCache = Object.create(null); const regionAliases = [ @@ -554,9 +550,6 @@ function operator(proxies) { const knownRegionCodes = new Set( regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0]) ); - const knownRegionPrefixes = new Set( - regionAliases.map(([, replacement]) => replacement) - ); Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code)); const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -580,26 +573,6 @@ function operator(proxies) { .join(''); }; - const regionCodeToFlag = regionCode => { - if (!regionCode) return ''; - - 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 normalizedFlagForCore = (flag, coreName) => { - if (!flag) return ''; - - const regionPrefix = leadingRegionPrefixOf(coreName) || regionPrefixInName(coreName); - const regionCode = regionPrefix.split(/\s+/)[0]; - return regionCodeToFlag(regionCode) || flag; - }; - const stripExistingSuffix = (name, subName) => { if (!subName) return name.trim(); const suffixRegex = suffixRegexCache[subName] || ( @@ -684,158 +657,6 @@ function operator(proxies) { .trim(); }; - const isProviderTag = tag => /^(JC|毕业)$/i.test(tag.trim()); - const isRouteTag = tag => /^(三网\d*|COP|CM)$/i.test(tag.trim()); - - const splitPipeFields = name => name - .split('|') - .map(part => part.trim()) - .filter(Boolean); - - const cleanupPipeFields = name => { - const fields = splitPipeFields(name); - if (fields.length <= 1) return name.trim(); - - const [head, ...tailFields] = fields; - const keptFields = tailFields.filter(field => !isRouteTag(field)); - return [head, ...keptFields].join('|').trim(); - }; - - const leadingRegionPrefixOf = name => { - const match = name.match(/^([A-Z]{2})(?:\s+([A-Z]{3}))?(?=\s|$|-|\d|\|)/); - if (!match) return ''; - - const fullPrefix = match[2] ? `${match[1]} ${match[2]}` : match[1]; - if (match[2] && knownRegionPrefixes.has(fullPrefix)) return fullPrefix; - - return knownRegionCodes.has(match[1]) ? match[1] : ''; - }; - - const hasAliasInName = (name, alias, aliasLower) => { - const nameLower = name.toLowerCase(); - const aliasIndex = nameLower.indexOf(aliasLower); - if (aliasIndex === -1) return false; - - if (/^[A-Za-z]/.test(alias)) { - const before = aliasIndex > 0 ? name[aliasIndex - 1] : ''; - const after = name[aliasIndex + alias.length] || ''; - return !/[A-Za-z]/.test(before) && !/[A-Za-z]/.test(after); - } - - return true; - }; - - const regionPrefixInName = name => { - for (const [alias, replacement, aliasLower] of sortedRegionAliases) { - if (hasAliasInName(name, alias, aliasLower)) return replacement; - } - - return leadingRegionPrefixOf(name); - }; - - const multiplierOf = name => { - const match = name.match(/\b(\d+(?:\.\d+)?X)\b/i); - return match ? match[1].toUpperCase() : ''; - }; - - const compactHomeBroadbandName = name => { - if (!/家宽/.test(name)) return name; - - const regionPrefix = /(?:Hinet|Seednet)/i.test(name) ? 'TW' : regionPrefixInName(name); - const multiplier = multiplierOf(name); - const compactFields = [regionPrefix, multiplier].filter(Boolean); - - return compactFields.join(' ').trim(); - }; - - const compactRegionOnlyName = name => { - const regionPrefix = regionPrefixInName(name); - return regionPrefix || name; - }; - - const compactKnownLineName = name => { - const cleanName = cleanupPipeFields(name); - const compactName = /家宽/.test(cleanName) - ? compactHomeBroadbandName(cleanName) - : compactRegionOnlyName(cleanName); - - return compactName - .replace(/\s*\|\s*/g, '|') - .replace(/\s{2,}/g, ' ') - .trim(); - }; - - const serialGroupOf = coreName => { - const nameWithoutFlag = coreName.replace(leadingFlagRegex, '').trim(); - const regionPrefix = leadingRegionPrefixOf(nameWithoutFlag) || regionPrefixInName(nameWithoutFlag); - if (!regionPrefix) return ''; - - return `${regionPrefix.split(/\s+/)[0]}|region`; - }; - - const appendSerialToCoreName = (coreName, serialNumber) => { - const fields = splitPipeFields(coreName); - const serialText = String(serialNumber).padStart(2, '0'); - - if (fields.length <= 1) return `${coreName} ${serialText}`; - - const providers = []; - const bodyFields = []; - fields.forEach((field, fieldIndex) => { - if (fieldIndex > 0 && isProviderTag(field)) { - providers.push(field); - } else { - bodyFields.push(field); - } - }); - - const body = `${bodyFields.join('|')} ${serialText}`.trim(); - return providers.length ? `${body}|${providers.join('|')}` : body; - }; - - const sortInfoOf = (coreName, originalIndex) => { - const nameWithoutFlag = coreName.replace(leadingFlagRegex, '').trim(); - const regionPrefix = leadingRegionPrefixOf(nameWithoutFlag) || regionPrefixInName(nameWithoutFlag); - const [countryCode, cityCode = ''] = regionPrefix.split(/\s+/); - const multiplierMatch = nameWithoutFlag.match(/\b(\d+(?:\.\d+)?)X\b/i); - - return { - originalIndex, - countryCode: countryCode || '', - cityCode, - regionPrefix, - multiplier: multiplierMatch ? Number(multiplierMatch[1]) : Number.NEGATIVE_INFINITY - }; - }; - - const compareSortInfo = (firstInfo, secondInfo) => { - if (!firstInfo.countryCode || !secondInfo.countryCode) { - return firstInfo.originalIndex - secondInfo.originalIndex; - } - - const firstCountryOrder = countryOrder[firstInfo.countryCode]; - const secondCountryOrder = countryOrder[secondInfo.countryCode]; - if (firstCountryOrder !== secondCountryOrder) { - return firstCountryOrder - secondCountryOrder; - } - - if (firstInfo.multiplier !== secondInfo.multiplier) { - return firstInfo.multiplier - secondInfo.multiplier; - } - - const firstCityRank = firstInfo.cityCode ? 1 : 0; - const secondCityRank = secondInfo.cityCode ? 1 : 0; - if (firstCityRank !== secondCityRank) { - return firstCityRank - secondCityRank; - } - - if (firstInfo.regionPrefix !== secondInfo.regionPrefix) { - return firstInfo.regionPrefix < secondInfo.regionPrefix ? -1 : 1; - } - - return firstInfo.originalIndex - secondInfo.originalIndex; - }; - const normalizeBilingualName = name => { const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/); if (!bilingualMatch) return name; @@ -899,10 +720,9 @@ function operator(proxies) { core = joinAlias(flagRegionCode, core); } - core = compactKnownLineName(simplifyCoreName(core)); + core = simplifyCoreName(core); - const normalizedFlag = normalizedFlagForCore(flag, core); - return normalizedFlag ? `${normalizedFlag} ${core}` : core; + return flag ? `${flag} ${core}` : core; }; proxies.forEach((proxy, proxyIndex) => { @@ -911,53 +731,13 @@ function operator(proxies) { const coreName = stripExistingSuffix(proxyName, subName); const normalizedCoreName = normalizeCoreName(coreName); const baseName = subName ? `${normalizedCoreName} - ${subName}` : normalizedCoreName; - const serialGroup = serialGroupOf(normalizedCoreName); - const scopedSerialGroup = serialGroup ? `${serialGroup}|${subName}` : ''; - const sortInfo = sortInfoOf(normalizedCoreName, proxyIndex); - - if (sortInfo.countryCode && countryOrder[sortInfo.countryCode] === undefined) { - countryOrder[sortInfo.countryCode] = Object.keys(countryOrder).length; - } baseNames[proxyIndex] = baseName; - renamedItems[proxyIndex] = { - normalizedCoreName, - subName, - scopedSerialGroup, - sortInfo - }; globalCount[baseName] = (globalCount[baseName] || 0) + 1; - if (scopedSerialGroup) { - serialCount[scopedSerialGroup] = (serialCount[scopedSerialGroup] || 0) + 1; - } }); - const sortedProxyIndexes = proxies - .map((proxy, proxyIndex) => proxyIndex) - .sort((firstIndex, secondIndex) => { - return compareSortInfo( - renamedItems[firstIndex].sortInfo, - renamedItems[secondIndex].sortInfo - ); - }); - - return sortedProxyIndexes.map(proxyIndex => { - const proxy = proxies[proxyIndex]; + return proxies.map((proxy, proxyIndex) => { const baseName = baseNames[proxyIndex]; - const renamedItem = renamedItems[proxyIndex]; - - if (renamedItem.scopedSerialGroup) { - currentSerial[renamedItem.scopedSerialGroup] = ( - currentSerial[renamedItem.scopedSerialGroup] || 0 - ) + 1; - - const serialCoreName = appendSerialToCoreName( - renamedItem.normalizedCoreName, - currentSerial[renamedItem.scopedSerialGroup] - ); - proxy.name = renamedItem.subName ? `${serialCoreName} - ${renamedItem.subName}` : serialCoreName; - return proxy; - } if (globalCount[baseName] > 1) { currentCount[baseName] = (currentCount[baseName] || 0) + 1; diff --git a/substore/yuetong-rename.js b/substore/yuetong-rename.js deleted file mode 100644 index 96d6717..0000000 --- a/substore/yuetong-rename.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * YueTong 专用节点清理脚本 - * 负责去掉“悦·”品牌前缀、钻石等装饰,以及压短 AI 解锁文案。 - */ -function operator(proxies) { - return proxies.map(proxy => { - if (typeof proxy.name !== 'string') { - return proxy; - } - - proxy.name = proxy.name - .replace(/悦[·・]\s*/g, '') - .replace(/AI解锁/g, 'AI') - .replace(/[💎]/gu, '') - .replace(/\s{2,}/g, ' ') - .trim(); - - return proxy; - }); -}