diff --git a/substore/README.md b/substore/README.md index f18f4af..625a034 100644 --- a/substore/README.md +++ b/substore/README.md @@ -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`。 diff --git a/substore/alphaair-rename.js b/substore/alphaair-rename.js index 5f76ef6..7daa922 100644 --- a/substore/alphaair-rename.js +++ b/substore/alphaair-rename.js @@ -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, diff --git a/substore/common.js b/substore/common.js new file mode 100644 index 0000000..b4c7e84 --- /dev/null +++ b/substore/common.js @@ -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); diff --git a/substore/liangxin-rename.js b/substore/liangxin-rename.js new file mode 100644 index 0000000..c98d90a --- /dev/null +++ b/substore/liangxin-rename.js @@ -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; + }); +} diff --git a/substore/peiqian-rename.js b/substore/peiqian-rename.js index bef0cda..ae37050 100644 --- a/substore/peiqian-rename.js +++ b/substore/peiqian-rename.js @@ -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; }); diff --git a/substore/rename.js b/substore/rename.js index b9c3ae3..c3a8646 100644 --- a/substore/rename.js +++ b/substore/rename.js @@ -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); diff --git a/substore/sntp-rename.js b/substore/sntp-rename.js index 4b5c838..03c1248 100644 --- a/substore/sntp-rename.js +++ b/substore/sntp-rename.js @@ -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) 问题 diff --git a/substore/yuetong-rename.js b/substore/yuetong-rename.js index bbef1e0..6740ad3 100644 --- a/substore/yuetong-rename.js +++ b/substore/yuetong-rename.js @@ -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,