♻️ refactor(substore): 提取重构公共工具 common.js 并新增 liangxin 脚本
This commit is contained in:
+27
-2
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user