Files
script/substore/rename.js
T
orion 0733bdcbe2 refactor(core): ♻️ 移除订阅名依赖并优化节点重命名规则
优化 substore 脚本逻辑,移除 peiqian-rename.js 和 yuetong-rename.js
中对 _subName 的硬编码判断,使其更适合在单条订阅中复用。

同时在 rename.js 中新增流媒体文案清理规则,并同步更新 README.md
文档,说明各脚本在单订阅与整合订阅中的最佳实践建议。
2026-05-19 00:37:11 +08:00

373 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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*\|\s*流媒体/gi, '')
.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;
});
}