feat(core): 新增 alphaair 重命名脚本并移除旧节点清理脚本

This commit is contained in:
Orion
2026-06-03 09:38:03 +08:00
parent 0dafef595a
commit 3f3e4731c4
5 changed files with 204 additions and 278 deletions
+19 -2
View File
@@ -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`
+182
View File
@@ -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;
});
}
-33
View File
@@ -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;
});
}
+3 -223
View File
@@ -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;
-20
View File
@@ -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;
});
}