♻️ refactor(substore): 提取重构公共工具 common.js 并新增 liangxin 脚本

This commit is contained in:
Orion
2026-06-03 12:56:18 +08:00
parent d60d1f2709
commit ce9fac0504
8 changed files with 460 additions and 192 deletions
+27 -2
View File
@@ -2,6 +2,12 @@
这个目录放的是一组 Sub-Store 节点处理脚本。推荐把“单订阅专属规则”和“整合订阅全局规则”分开使用,避免一个机场的关键词误伤其他机场。
`common.js` 是公共工具库,各处理脚本会按下面的 raw 地址自动加载:
```text
https://git.orionc.me/orion/script/raw/branch/main/substore/common.js
```
## 推荐使用顺序
单订阅脚本先跑,整合订阅最后跑:
@@ -10,7 +16,8 @@
2. `yuetong-rename.js`:只清理 YueTong 的品牌前缀和装饰符号。
3. `peiqian-rename.js`:只清理 PeiQian 的推荐/用途描述。
4. `alphaair-rename.js`:只处理 Alpha Air 的供应商压缩、倍率排序和连续编号。
5. `rename.js`整合订阅最终重命名,负责国家短码、国旗归位、订阅后缀和去重
5. `liangxin-rename.js`只清理 LiangXin 的高速、BGP、CMCU 等线路描述
6. `rename.js`:整合订阅最终重命名,负责国家短码、国旗归位、订阅后缀和去重。
如果某个订阅没有对应的专属脚本,直接进入最后的 `rename.js` 即可。
@@ -22,7 +29,9 @@
| `yuetong-rename.js` | YueTong 单订阅 | 去掉 `悦·``💎`,把 `AI解锁` 压成 `AI` |
| `peiqian-rename.js` | PeiQian 单订阅 | 压短 `电信联通推荐``移动联通推荐``高速专线``hy2`、倍率和用途描述 |
| `alphaair-rename.js` | Alpha Air 单订阅 | 去掉提供者/三网/家宽文案,普通节点保留地区,高级非倍率节点保留供应商,倍率节点保留倍率并连续编号 |
| `liangxin-rename.js` | LiangXin 单订阅 | 去掉 `高速``BGP``CMCU``CM``CU``CT` 等线路描述 |
| `rename.js` | 整合订阅 | 统一国家/城市短码,移动国旗到首部,追加 `- _subName`,处理重名编号 |
| `common.js` | 公共依赖 | 被各处理脚本远程加载,不需要单独配置为处理器 |
## SNTP 规则
@@ -110,6 +119,22 @@
=> 🇭🇰 HK 01
```
## LiangXin 规则
`liangxin-rename.js` 建议挂在 LiangXin 单条订阅里使用。它会删除内嵌的线路描述,以及 `|` 分隔出来的纯线路标签:
```text
🇭🇰香港高速01|BGP|CMCU
=> 🇭🇰 香港01
```
再经过 `rename.js` 后:
```text
🇭🇰 香港01
=> 🇭🇰 HK 01 - LiangXin
```
## 全局重命名
`rename.js` 适合放在整合订阅最后。它做的事情更通用:
@@ -140,6 +165,6 @@ Pro-🇭🇰 Sharon|守夜人 - Alpha 8K Snell
## 注意事项
- `sntp-rename.js` 不带 `_subName` 判断,适合挂在 SNTP 单订阅里;如果放进整合订阅,可能会误伤其他带 `BGP``D``CT` 的节点。
- `yuetong-rename.js``peiqian-rename.js` 也不带 `_subName` 判断,适合挂在对应单订阅里;如果放进整合订阅,需要确认规则不会误伤其他订阅。
- `yuetong-rename.js``peiqian-rename.js``liangxin-rename.js` 也不带 `_subName` 判断,适合挂在对应单订阅里;如果放进整合订阅,需要确认规则不会误伤其他订阅。
- `rename.js` 会剥离并重新追加 `- _subName`,重复运行不会叠加订阅后缀。
- 如果新增订阅里出现很短、很泛的关键词,例如 `D``CT``BGP``S1`,或像 Alpha Air 这种强绑定单机场的供应商压缩规则,优先拆成单订阅脚本,不建议放进全局 `rename.js`
+28 -63
View File
@@ -6,7 +6,27 @@
* - 不追加订阅后缀,整合订阅最后仍交给 rename.js 处理
*/
// noinspection JSUnusedGlobalSymbols
function operator(proxies) {
const COMMON_SCRIPT_URL = 'https://git.orionc.me/orion/script/raw/branch/main/substore/common.js';
async function loadRenameUtils() {
if (typeof globalThis !== 'undefined' && globalThis.SubStoreRenameUtils) {
return globalThis.SubStoreRenameUtils;
}
const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null;
let script = cache && cache.get(COMMON_SCRIPT_URL);
if (!script) {
const response = await $substore.http.get({ url: COMMON_SCRIPT_URL });
script = response && (response.body || response.data || response);
if (cache) cache.set(COMMON_SCRIPT_URL, script);
}
new Function(String(script))();
return globalThis.SubStoreRenameUtils;
}
async function operator(proxies) {
const utils = await loadRenameUtils();
const regionAliases = [
['美国夏威夷', 'US HNL'],
['家宽夏威夷', 'US HNL'],
@@ -34,62 +54,12 @@ function operator(proxies) {
['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 regionResolver = utils.createRegionResolver(regionAliases);
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` : '';
@@ -108,25 +78,20 @@ function operator(proxies) {
return match[2] ? `${match[1]} ${match[2]}` : match[1];
};
const aliasRegionPrefixOf = name => {
const cleanNameLower = name.toLowerCase();
for (const [, replacement, aliasLower] of sortedRegionAliases) {
if (cleanNameLower.includes(aliasLower)) return replacement;
}
const alphaAirRegionPrefixOf = name => {
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 '';
return regionResolver.aliasRegionPrefixOf(name);
};
const regionPrefixOf = (name, flagRegionCode) => {
if (/Hinet|Seednet/i.test(name)) return 'TW';
return aliasRegionPrefixOf(name) ||
return alphaAirRegionPrefixOf(name) ||
leadingRegionPrefixOf(name) ||
flagRegionCode ||
'';
@@ -135,14 +100,14 @@ function operator(proxies) {
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 { flag, text } = utils.splitLeadingOrInlineFlag(cleanName);
const flagRegionCode = utils.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);
const normalizedFlag = utils.regionCodeToFlag(regionCode);
return {
proxy,
+236
View File
@@ -0,0 +1,236 @@
/**
* Sub-Store 节点命名公共工具
*
* 远程引用地址:
* https://git.orionc.me/orion/script/raw/branch/main/substore/common.js
*/
(function (root) {
const regexSpecialChars = new Set(['.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']);
const leadingFlagRegex = /^([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*/u;
const inlineFlagRegex = /^(.{1,24}?)([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*(.*)$/u;
const escapeRegex = value => [...String(value)]
.map(char => regexSpecialChars.has(char) ? `\\${char}` : char)
.join('');
const normalizeWhitespace = value => String(value)
.replace(/\s{2,}/g, ' ')
.trim();
const normalizeLeadingFlagSpacing = value => String(value)
.replace(leadingFlagRegex, '$1 ')
.replace(/\s{2,}/g, ' ')
.trim();
const flagToRegionCode = (flag, overrides) => {
if (!flag) return '';
if (overrides && overrides[flag]) return overrides[flag];
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 splitLeadingFlag = name => {
const leadingFlagMatch = String(name).match(leadingFlagRegex);
if (!leadingFlagMatch) return { flag: '', text: String(name).trim() };
return {
flag: leadingFlagMatch[1],
text: String(name).slice(leadingFlagMatch[0].length).trim()
};
};
const splitLeadingOrInlineFlag = name => {
const cleanName = String(name);
const leadingFlagMatch = cleanName.match(leadingFlagRegex);
if (leadingFlagMatch) {
return {
flag: leadingFlagMatch[1],
text: cleanName.slice(leadingFlagMatch[0].length).trim()
};
}
const inlineFlagMatch = cleanName.match(inlineFlagRegex);
if (!inlineFlagMatch) return { flag: '', text: cleanName.trim() };
const prefix = inlineFlagMatch[1].trim();
const rest = inlineFlagMatch[3].trim();
return {
flag: inlineFlagMatch[2],
text: rest ? `${prefix} ${rest}`.trim() : prefix
};
};
const sortRegionAliases = aliases => aliases
.map(([alias, replacement]) => [alias, replacement, String(alias).toLowerCase()])
.sort((firstAlias, secondAlias) => secondAlias[0].length - firstAlias[0].length);
const createRegionResolver = aliases => {
const sortedAliases = sortRegionAliases(aliases);
const regionCodeOf = name => {
const cleanName = String(name);
const leadingFlagMatch = cleanName.match(leadingFlagRegex);
if (leadingFlagMatch) return flagToRegionCode(leadingFlagMatch[1]);
const cleanNameLower = cleanName.toLowerCase();
for (const [alias, replacement, aliasLower] of sortedAliases) {
if (/^[A-Z]{2}$/.test(alias)) {
const codeMatch = cleanName.match(new RegExp(`(^|[^A-Za-z])${escapeRegex(alias)}([^A-Za-z]|$)`, 'i'));
if (codeMatch) return replacement;
continue;
}
if (cleanNameLower.includes(aliasLower)) return replacement;
}
return '';
};
const aliasRegionPrefixOf = name => {
const cleanNameLower = String(name).toLowerCase();
for (const [, replacement, aliasLower] of sortedAliases) {
if (cleanNameLower.includes(aliasLower)) return replacement;
}
return '';
};
const normalizeLeadingRegion = name => {
const cleanName = String(name).trim();
const cleanNameLower = cleanName.toLowerCase();
for (const [alias, replacement, aliasLower] of sortedAliases) {
if (!cleanNameLower.startsWith(aliasLower)) 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;
};
return {
sortedAliases,
regionCodeOf,
aliasRegionPrefixOf,
normalizeLeadingRegion
};
};
const joinAlias = (replacement, rest) => {
const cleanRest = String(rest || '').trim();
if (!cleanRest) return replacement;
if (/^[|/\-]/.test(cleanRest)) {
return `${replacement}${cleanRest}`;
}
return `${replacement} ${cleanRest}`;
};
const normalizeMultiplierText = value => String(value)
.replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1X')
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1X')
.replace(/\b(\d+(?:\.\d+)?)x\b/gi, '$1X');
const moveMultiplierToEnd = name => {
const multipliers = [];
const nameWithoutMultiplier = String(name)
.replace(/\b(\d+(?:\.\d+)?X)\b/gi, multiplier => {
multipliers.push(multiplier.toUpperCase());
return ' ';
})
.replace(/\s+\[/g, ' [')
.replace(/\s{2,}/g, ' ')
.trim();
if (!multipliers.length) return name;
return `${nameWithoutMultiplier} ${multipliers.join(' ')}`.trim();
};
const normalizeRegionSequence = (name, options) => {
const allowCityCode = Boolean(options && options.allowCityCode);
const regionPattern = allowCityCode
? '[A-Z]{2}(?:\\s+[A-Z]{3})?'
: '[A-Z]{2}';
return String(name)
.replace(new RegExp(`(^|\\s)(${regionPattern})[\\s|\\-]*(\\d{1,2})(?=$|[\\s|\\-[\\]])`, 'g'), (
match,
prefix,
regionCode,
sequenceNumber
) => {
return `${prefix}${regionCode} ${sequenceNumber.padStart(2, '0')}`;
})
.replace(new RegExp(`(\\b${regionPattern} \\d{2})-\\s*(?=\\d+(?:\\.\\d+)?X\\b)`, 'g'), '$1 ')
.replace(/\s{2,}/g, ' ')
.trim();
};
const stripLineDescriptors = (name, descriptorKeywords) => {
const descriptorPattern = descriptorKeywords.map(escapeRegex).join('|');
const descriptorOnlyRegex = new RegExp(`^(?:${descriptorPattern})+$`, 'i');
const embeddedDescriptorRegex = new RegExp(`\\s*(?:${descriptorPattern})(?=\\s|\\d|$)`, 'gi');
const bracketDescriptorRegex = new RegExp(`\\s*\\[(?:${descriptorPattern})(?:\\s+(?:${descriptorPattern}))*]\\s*`, 'gi');
const [head, ...segments] = String(name).split('|');
const keptSegments = segments
.map(segment => segment.trim())
.filter(segment => segment && !descriptorOnlyRegex.test(segment));
const cleanHead = head
.replace(bracketDescriptorRegex, ' ')
.replace(embeddedDescriptorRegex, '')
.replace(leadingFlagRegex, '$1 ')
.replace(/\s{2,}/g, ' ')
.trim();
return [cleanHead, ...keptSegments]
.filter(Boolean)
.join('|')
.replace(/\s+\|/g, '|')
.replace(/\|\s+/g, '|')
.trim();
};
root.SubStoreRenameUtils = {
createRegionResolver,
escapeRegex,
flagToRegionCode,
joinAlias,
moveMultiplierToEnd,
normalizeLeadingFlagSpacing,
normalizeMultiplierText,
normalizeRegionSequence,
normalizeWhitespace,
regionCodeToFlag,
sortRegionAliases,
splitLeadingFlag,
splitLeadingOrInlineFlag,
stripLineDescriptors
};
})(typeof globalThis !== 'undefined' ? globalThis : this);
+65
View File
@@ -0,0 +1,65 @@
/**
* LiangXin 专用节点清理脚本
* 负责去掉高速、BGP、CMCU 等 LiangXin 线路描述,保留地区、序号和倍率。
*/
// noinspection JSUnusedGlobalSymbols
const COMMON_SCRIPT_URL = 'https://git.orionc.me/orion/script/raw/branch/main/substore/common.js';
async function loadRenameUtils() {
if (typeof globalThis !== 'undefined' && globalThis.SubStoreRenameUtils) {
return globalThis.SubStoreRenameUtils;
}
const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null;
let script = cache && cache.get(COMMON_SCRIPT_URL);
if (!script) {
const response = await $substore.http.get({ url: COMMON_SCRIPT_URL });
script = response && (response.body || response.data || response);
if (cache) cache.set(COMMON_SCRIPT_URL, script);
}
new Function(String(script))();
return globalThis.SubStoreRenameUtils;
}
async function operator(proxies) {
const utils = await loadRenameUtils();
const lineDescriptorKeywords = [
'CMCU',
'CUCM',
'CMCT',
'CTCM',
'CUCT',
'CTCU',
'BGP',
'CMI',
'CUG',
'CN2',
'IEPL',
'IPLC',
'9929',
'4837',
'CM',
'CU',
'CT',
'高速',
'极速',
'专线',
'中转',
'直连',
'隧道',
'三网',
'移动',
'联通',
'电信'
];
return proxies.map(proxy => {
if (typeof proxy.name !== 'string') {
return proxy;
}
proxy.name = utils.stripLineDescriptors(proxy.name, lineDescriptorKeywords);
return proxy;
});
}
+24 -4
View File
@@ -3,7 +3,28 @@
* 负责清理推荐、用途、hy2、倍率、线路特征和提供商等 PeiQian 节点文案。
*/
// noinspection JSUnusedGlobalSymbols
function operator(proxies) {
const COMMON_SCRIPT_URL = 'https://git.orionc.me/orion/script/raw/branch/main/substore/common.js';
async function loadRenameUtils() {
if (typeof globalThis !== 'undefined' && globalThis.SubStoreRenameUtils) {
return globalThis.SubStoreRenameUtils;
}
const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null;
let script = cache && cache.get(COMMON_SCRIPT_URL);
if (!script) {
const response = await $substore.http.get({ url: COMMON_SCRIPT_URL });
script = response && (response.body || response.data || response);
if (cache) cache.set(COMMON_SCRIPT_URL, script);
}
new Function(String(script))();
return globalThis.SubStoreRenameUtils;
}
async function operator(proxies) {
const utils = await loadRenameUtils();
return proxies.map(proxy => {
if (typeof proxy.name !== 'string') {
return proxy;
@@ -11,7 +32,6 @@ function operator(proxies) {
proxy.name = proxy.name
.replace(/AWS\s*(?=新加坡)/gi, '')
.replace(/^([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*/u, '$1 ')
.replace(/\s*\|\s*合适下载使用\s*-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x')
.replace(/\s*\|\s*合适下载使用/gi, '')
.replace(/\s*\|\s*避免晚高峰使用/gi, '')
@@ -21,8 +41,8 @@ function operator(proxies) {
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1x')
.replace(/\s*\[(?:专线|移联|电联|三网|HY2)(?:\s+(?:专线|移联|电联|三网|HY2))*\]/gi, '')
.replace(/\s+\[/g, ' [')
.replace(/\s{2,}/g, ' ')
.trim();
.replace(/\s{2,}/g, ' ');
proxy.name = utils.normalizeLeadingFlagSpacing(proxy.name);
return proxy;
});
+26 -55
View File
@@ -5,7 +5,27 @@
* - 基于归一化后的完整名称做精准去重
*/
// noinspection JSUnusedGlobalSymbols
function operator(proxies) {
const COMMON_SCRIPT_URL = 'https://git.orionc.me/orion/script/raw/branch/main/substore/common.js';
async function loadRenameUtils() {
if (typeof globalThis !== 'undefined' && globalThis.SubStoreRenameUtils) {
return globalThis.SubStoreRenameUtils;
}
const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null;
let script = cache && cache.get(COMMON_SCRIPT_URL);
if (!script) {
const response = await $substore.http.get({ url: COMMON_SCRIPT_URL });
script = response && (response.body || response.data || response);
if (cache) cache.set(COMMON_SCRIPT_URL, script);
}
new Function(String(script))();
return globalThis.SubStoreRenameUtils;
}
async function operator(proxies) {
const utils = await loadRenameUtils();
const globalCount = Object.create(null);
const currentCount = Object.create(null);
const baseNames = new Array(proxies.length);
@@ -557,29 +577,11 @@ function operator(proxies) {
);
Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code));
const regexSpecialChars = new Set(['.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']);
const escapeRegex = value => [...value]
.map(char => regexSpecialChars.has(char) ? `\\${char}` : char)
.join('');
const escapeRegex = utils.escapeRegex;
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 '';
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 flagToRegionCode = flag => utils.flagToRegionCode(flag, flagCodeOverrides);
const stripExistingSuffix = (name, subName) => {
if (!subName) return name.trim();
@@ -589,14 +591,7 @@ function operator(proxies) {
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 joinAlias = utils.joinAlias;
const normalizeLeadingRegion = name => {
const cleanName = name.trim();
@@ -665,33 +660,9 @@ function operator(proxies) {
.trim();
};
const moveMultiplierToEnd = name => {
const multipliers = [];
const nameWithoutMultiplier = name
.replace(/\b(\d+(?:\.\d+)?X)\b/gi, multiplier => {
multipliers.push(multiplier.toUpperCase());
return ' ';
})
.replace(/\s+\[/g, ' [')
.replace(/\s{2,}/g, ' ')
.trim();
const moveMultiplierToEnd = utils.moveMultiplierToEnd;
if (!multipliers.length) return name;
return `${nameWithoutMultiplier} ${multipliers.join(' ')}`.trim();
};
const normalizeRegionSequence = name => name
.replace(/(^|\s)([A-Z]{2}(?:\s+[A-Z]{3})?)[\s|\-]*(\d{1,2})(?=$|[\s|\-[\]])/g, (
match,
prefix,
regionCode,
sequenceNumber
) => {
return `${prefix}${regionCode} ${sequenceNumber.padStart(2, '0')}`;
})
.replace(/(\b[A-Z]{2}(?:\s+[A-Z]{3})? \d{2})-\s*(?=\d+(?:\.\d+)?X\b)/g, '$1 ')
.replace(/\s{2,}/g, ' ')
.trim();
const normalizeRegionSequence = name => utils.normalizeRegionSequence(name, { allowCityCode: true });
const regionSequenceOf = name => {
const sequenceMatch = name.match(/^((?:[\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*)?([A-Z]{2}(?:\s+[A-Z]{3})?)(?:\s+(\d{2}))?(\s.*)?$/u);
+27 -13
View File
@@ -4,7 +4,28 @@
* 维护说明:仅需修改 featureMap 字典即可,底部逻辑永远无需改动。
*/
// noinspection JSUnusedGlobalSymbols
function operator(proxies) {
const COMMON_SCRIPT_URL = 'https://git.orionc.me/orion/script/raw/branch/main/substore/common.js';
async function loadRenameUtils() {
if (typeof globalThis !== 'undefined' && globalThis.SubStoreRenameUtils) {
return globalThis.SubStoreRenameUtils;
}
const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null;
let script = cache && cache.get(COMMON_SCRIPT_URL);
if (!script) {
const response = await $substore.http.get({ url: COMMON_SCRIPT_URL });
script = response && (response.body || response.data || response);
if (cache) cache.set(COMMON_SCRIPT_URL, script);
}
new Function(String(script))();
return globalThis.SubStoreRenameUtils;
}
async function operator(proxies) {
const utils = await loadRenameUtils();
// ================= 唯一配置区 =================
// 在这里新增或修改关键词。格式:"节点关键词": "想要注入的标签"
// 支持大小写混合,支持包含空格或特殊符号。
@@ -38,16 +59,12 @@ function operator(proxies) {
const featureKeywords = Object.keys(featureMap);
const normalizeFeatureKeyword = keyword => keyword.toUpperCase().replace(/\s+/g, '');
const normalizeHiddenFeatureKeyword = keyword => keyword.trim().replace(/\s+\d+(?:\.\d+)?x$/i, '');
const regexSpecialChars = new Set(['.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']);
const escapeRegexKeyword = keyword => [...keyword]
.map(char => regexSpecialChars.has(char) ? `\\${char}` : char)
.join('');
for (const featureKeyword of featureKeywords) {
normalizedFeatureMap[normalizeFeatureKeyword(featureKeyword)] = featureMap[featureKeyword];
}
const buildKeywordPattern = keyword => {
const keywordPattern = keyword.trim().split(/\s+/).filter(Boolean).map(escapeRegexKeyword).join('\\s*');
const keywordPattern = keyword.trim().split(/\s+/).filter(Boolean).map(utils.escapeRegex).join('\\s*');
const trimmedKeyword = keyword.trim();
if (/^[A-Za-z0-9]/.test(trimmedKeyword) && /[A-Za-z0-9]$/.test(trimmedKeyword)) {
return `\\b${keywordPattern}\\b`;
@@ -68,18 +85,15 @@ function operator(proxies) {
.replace(/^-+|-+$/g, '')
.trim();
const normalizeRegionSequence = proxyName => proxyName
.replace(/(^|\s)([A-Z]{2})[-\s]?(\d{1,2})(?=$|[\s-])/g, (match, prefix, regionCode, sequenceNumber) => {
return `${prefix}${regionCode} ${sequenceNumber.padStart(2, '0')}`;
})
.replace(/(\b[A-Z]{2} \d{2})-(?=\d+(?:\.\d+)?x\b)/g, '$1 ');
const abbreviateDedicatedLineProvider = proxyName => proxyName.replace(
/\b([A-Za-z])[A-Za-z]*\s*专线/g,
(match, providerInitial) => `${providerInitial.toUpperCase()}专线`
);
const normalizeDisplayName = proxyName => abbreviateDedicatedLineProvider(normalizeRegionSequence(proxyName));
const normalizeDisplayName = proxyName => abbreviateDedicatedLineProvider(
utils.normalizeRegionSequence(proxyName.replace(/\b(\d+(?:\.\d+)?)x\b/gi, '$1X'))
.replace(/\b(\d+(?:\.\d+)?)X\b/g, '$1x')
);
// 2. 动态构建复合正则表达式 (核心引擎)
// 按字符串长度降序排序,彻底解决 "短路匹配" (Short-Circuit) 问题
+27 -55
View File
@@ -4,7 +4,27 @@
* 按地区稳定归并后重新连续编号,避免专属/普通节点撞号。
*/
// noinspection JSUnusedGlobalSymbols
function operator(proxies) {
const COMMON_SCRIPT_URL = 'https://git.orionc.me/orion/script/raw/branch/main/substore/common.js';
async function loadRenameUtils() {
if (typeof globalThis !== 'undefined' && globalThis.SubStoreRenameUtils) {
return globalThis.SubStoreRenameUtils;
}
const cache = typeof scriptResourceCache !== 'undefined' ? scriptResourceCache : null;
let script = cache && cache.get(COMMON_SCRIPT_URL);
if (!script) {
const response = await $substore.http.get({ url: COMMON_SCRIPT_URL });
script = response && (response.body || response.data || response);
if (cache) cache.set(COMMON_SCRIPT_URL, script);
}
new Function(String(script))();
return globalThis.SubStoreRenameUtils;
}
async function operator(proxies) {
const utils = await loadRenameUtils();
const regionAliases = [
['Hong Kong', 'HK'],
['香港', 'HK'],
@@ -47,57 +67,10 @@ function operator(proxies) {
['荷蘭', 'NL'],
['NL', 'NL']
];
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 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 regionCodeOf = name => {
const leadingFlagMatch = name.match(leadingFlagRegex);
if (leadingFlagMatch) return flagToRegionCode(leadingFlagMatch[1]);
const nameLower = name.toLowerCase();
for (const [alias, replacement, aliasLower] of sortedRegionAliases) {
if (/^[A-Z]{2}$/.test(alias)) {
const codeMatch = name.match(new RegExp(`(^|[^A-Za-z])${alias}([^A-Za-z]|$)`, 'i'));
if (codeMatch) return replacement;
continue;
}
if (nameLower.includes(aliasLower)) return replacement;
}
return '';
};
const splitLeadingFlag = name => {
const leadingFlagMatch = name.match(leadingFlagRegex);
if (!leadingFlagMatch) return { flag: '', text: name.trim() };
return {
flag: leadingFlagMatch[1],
text: name.slice(leadingFlagMatch[0].length).trim()
};
};
const { regionCodeOf } = utils.createRegionResolver(regionAliases);
const withFreshRegionSequence = (name, sequenceNumber) => {
const { flag, text } = splitLeadingFlag(name);
const { flag, text } = utils.splitLeadingFlag(name);
const sequenceText = String(sequenceNumber);
let core = text.trim();
let feature = '';
@@ -115,10 +88,9 @@ function operator(proxies) {
core = core
.replace(/\s*\d+(?!\d)(?!\s*(?:[xX倍]|\.\d))(?=\s|$)/, '')
.replace(/\s{2,}/g, ' ')
.trim();
.replace(/\s{2,}/g, ' ');
return `${flag}${core}${sequenceText}${feature ? ` ${feature}` : ''}`.trim();
return utils.normalizeWhitespace(`${flag}${core}${sequenceText}${feature ? ` ${feature}` : ''}`);
};
const normalizedItems = proxies.map((proxy, originalIndex) => {
@@ -136,8 +108,8 @@ function operator(proxies) {
.replace(/\s*(?:专属|家宽|高速|专线)(?:\s*(\d+)(?!\s*(?:[xX倍]|\.\d)))?/g, '$1')
.replace(/💎/gu, '')
.replace(/([\u4e00-\u9fff])AI\b/g, '$1 AI')
.replace(/\s{2,}/g, ' ')
.trim();
.replace(/\s{2,}/g, ' ');
proxy.name = utils.normalizeWhitespace(proxy.name);
return {
proxy,