🐛 fix(substore): 修复重命名脚本中的节点序号与基础名称逻辑
This commit is contained in:
+232
-6
@@ -7,7 +7,11 @@
|
|||||||
function operator(proxies) {
|
function operator(proxies) {
|
||||||
const globalCount = Object.create(null);
|
const globalCount = Object.create(null);
|
||||||
const currentCount = 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 baseNames = new Array(proxies.length);
|
||||||
|
const renamedItems = new Array(proxies.length);
|
||||||
const suffixRegexCache = Object.create(null);
|
const suffixRegexCache = Object.create(null);
|
||||||
|
|
||||||
const regionAliases = [
|
const regionAliases = [
|
||||||
@@ -29,6 +33,7 @@ function operator(proxies) {
|
|||||||
['United States Portland', 'US PDX'],
|
['United States Portland', 'US PDX'],
|
||||||
['United States Boston', 'US BOS'],
|
['United States Boston', 'US BOS'],
|
||||||
['United States Seattle', 'US SEA'],
|
['United States Seattle', 'US SEA'],
|
||||||
|
['United States Hawaii', 'US HNL'],
|
||||||
['Australia Sydney', 'AU SYD'],
|
['Australia Sydney', 'AU SYD'],
|
||||||
['Australia Melbourne', 'AU MEL'],
|
['Australia Melbourne', 'AU MEL'],
|
||||||
['Australia Brisbane', 'AU BNE'],
|
['Australia Brisbane', 'AU BNE'],
|
||||||
@@ -92,6 +97,7 @@ function operator(proxies) {
|
|||||||
['USA Portland', 'US PDX'],
|
['USA Portland', 'US PDX'],
|
||||||
['USA Boston', 'US BOS'],
|
['USA Boston', 'US BOS'],
|
||||||
['USA Seattle', 'US SEA'],
|
['USA Seattle', 'US SEA'],
|
||||||
|
['USA Hawaii', 'US HNL'],
|
||||||
['United Kingdom', 'UK'],
|
['United Kingdom', 'UK'],
|
||||||
['United States', 'US'],
|
['United States', 'US'],
|
||||||
['South Africa', 'ZA'],
|
['South Africa', 'ZA'],
|
||||||
@@ -314,6 +320,7 @@ function operator(proxies) {
|
|||||||
['美国拉斯维加斯', 'US LAS'],
|
['美国拉斯维加斯', 'US LAS'],
|
||||||
['美国波特兰', 'US PDX'],
|
['美国波特兰', 'US PDX'],
|
||||||
['美国波士顿', 'US BOS'],
|
['美国波士顿', 'US BOS'],
|
||||||
|
['美国夏威夷', 'US HNL'],
|
||||||
['日本东京', 'JP TYO'],
|
['日本东京', 'JP TYO'],
|
||||||
['日本大阪', 'JP OSA'],
|
['日本大阪', 'JP OSA'],
|
||||||
['日本名古屋', 'JP NGO'],
|
['日本名古屋', 'JP NGO'],
|
||||||
@@ -443,6 +450,7 @@ function operator(proxies) {
|
|||||||
['德国', 'DE'],
|
['德国', 'DE'],
|
||||||
['荷兰', 'NL'],
|
['荷兰', 'NL'],
|
||||||
['法国', 'FR'],
|
['法国', 'FR'],
|
||||||
|
['美西', 'US'],
|
||||||
['巴西', 'BR'],
|
['巴西', 'BR'],
|
||||||
['智利', 'CL'],
|
['智利', 'CL'],
|
||||||
['澳门', 'MO'],
|
['澳门', 'MO'],
|
||||||
@@ -464,6 +472,7 @@ function operator(proxies) {
|
|||||||
['捷克', 'CZ'],
|
['捷克', 'CZ'],
|
||||||
['沙特', 'SA'],
|
['沙特', 'SA'],
|
||||||
['墨西哥', 'MX'],
|
['墨西哥', 'MX'],
|
||||||
|
['夏威夷', 'US HNL'],
|
||||||
['迪拜', 'AE DXB'],
|
['迪拜', 'AE DXB'],
|
||||||
['悉尼', 'AU SYD'],
|
['悉尼', 'AU SYD'],
|
||||||
['墨尔本', 'AU MEL'],
|
['墨尔本', 'AU MEL'],
|
||||||
@@ -545,6 +554,9 @@ function operator(proxies) {
|
|||||||
const knownRegionCodes = new Set(
|
const knownRegionCodes = new Set(
|
||||||
regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0])
|
regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0])
|
||||||
);
|
);
|
||||||
|
const knownRegionPrefixes = new Set(
|
||||||
|
regionAliases.map(([, replacement]) => replacement)
|
||||||
|
);
|
||||||
Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code));
|
Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code));
|
||||||
|
|
||||||
const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
@@ -568,6 +580,26 @@ function operator(proxies) {
|
|||||||
.join('');
|
.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) => {
|
const stripExistingSuffix = (name, subName) => {
|
||||||
if (!subName) return name.trim();
|
if (!subName) return name.trim();
|
||||||
const suffixRegex = suffixRegexCache[subName] || (
|
const suffixRegex = suffixRegexCache[subName] || (
|
||||||
@@ -639,18 +671,171 @@ function operator(proxies) {
|
|||||||
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '')
|
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '')
|
||||||
.replace(/\u200d/g, '')
|
.replace(/\u200d/g, '')
|
||||||
.replace(/\s*\|\s*流媒体/gi, '')
|
.replace(/\s*\|\s*流媒体/gi, '')
|
||||||
.replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x')
|
.replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1X')
|
||||||
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1x')
|
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1X')
|
||||||
.replace(/\b(\d+(?:\.\d+)?)X\b/g, '$1x')
|
.replace(/\b(\d+(?:\.\d+)?)x\b/gi, '$1X')
|
||||||
.replace(/([A-Z]{2})\s*(专属|家宽|AI|高速|专线)(\d+)/g, '$1 $2 $3')
|
.replace(/([A-Z]{2})\s*(专属|家宽|AI|高速|专线)(\d+)/g, '$1 $2 $3')
|
||||||
.replace(/\b(AI)(\d+)/g, '$1 $2')
|
.replace(/\b(AI)(\d+)/g, '$1 $2')
|
||||||
.replace(/\b(Pro-)\s+/gi, '$1')
|
.replace(/\b(Pro-)\s+/gi, '$1')
|
||||||
.replace(/^((?:Pro-\s*)?)([A-Z]{2})(?=[\u4e00-\u9fff]|\d)/u, '$1$2 ')
|
.replace(/^((?:Pro-\s*)?)([A-Z]{2})(?=[\u4e00-\u9fff]|\d)/u, '$1$2 ')
|
||||||
|
.replace(/\s*\|\s*/g, '|')
|
||||||
.replace(/\s+\[/g, ' [')
|
.replace(/\s+\[/g, ' [')
|
||||||
.replace(/\s{2,}/g, ' ')
|
.replace(/\s{2,}/g, ' ')
|
||||||
.trim();
|
.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 normalizeBilingualName = name => {
|
||||||
const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/);
|
const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/);
|
||||||
if (!bilingualMatch) return name;
|
if (!bilingualMatch) return name;
|
||||||
@@ -714,9 +899,10 @@ function operator(proxies) {
|
|||||||
core = joinAlias(flagRegionCode, core);
|
core = joinAlias(flagRegionCode, core);
|
||||||
}
|
}
|
||||||
|
|
||||||
core = simplifyCoreName(core);
|
core = compactKnownLineName(simplifyCoreName(core));
|
||||||
|
|
||||||
return flag ? `${flag} ${core}` : core;
|
const normalizedFlag = normalizedFlagForCore(flag, core);
|
||||||
|
return normalizedFlag ? `${normalizedFlag} ${core}` : core;
|
||||||
};
|
};
|
||||||
|
|
||||||
proxies.forEach((proxy, proxyIndex) => {
|
proxies.forEach((proxy, proxyIndex) => {
|
||||||
@@ -725,13 +911,53 @@ function operator(proxies) {
|
|||||||
const coreName = stripExistingSuffix(proxyName, subName);
|
const coreName = stripExistingSuffix(proxyName, subName);
|
||||||
const normalizedCoreName = normalizeCoreName(coreName);
|
const normalizedCoreName = normalizeCoreName(coreName);
|
||||||
const baseName = subName ? `${normalizedCoreName} - ${subName}` : normalizedCoreName;
|
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;
|
baseNames[proxyIndex] = baseName;
|
||||||
|
renamedItems[proxyIndex] = {
|
||||||
|
normalizedCoreName,
|
||||||
|
subName,
|
||||||
|
scopedSerialGroup,
|
||||||
|
sortInfo
|
||||||
|
};
|
||||||
globalCount[baseName] = (globalCount[baseName] || 0) + 1;
|
globalCount[baseName] = (globalCount[baseName] || 0) + 1;
|
||||||
|
if (scopedSerialGroup) {
|
||||||
|
serialCount[scopedSerialGroup] = (serialCount[scopedSerialGroup] || 0) + 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return proxies.map((proxy, proxyIndex) => {
|
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];
|
||||||
const baseName = baseNames[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) {
|
if (globalCount[baseName] > 1) {
|
||||||
currentCount[baseName] = (currentCount[baseName] || 0) + 1;
|
currentCount[baseName] = (currentCount[baseName] || 0) + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user