feat(core): ✨ 新增 Sub-Store 节点命名处理系统
本次变更引入了一套完整的节点自动化重命名方案,旨在提升 Sub-Store 订阅管理的标准化程度: 1. 新增三个机场专属处理脚本:peiqian-rename.js、yuetong-rename.js 及整合版 rename.js,用于精简节点名称和统一格式。 2. 优化 sntp-rename.js:增加了 CT(CF) 和 D(直连) 等线路特征识别。 3. 更新 README.md:详细说明了脚本的链式调用顺序与各自职责。 4. 核心逻辑改进:支持国家/城市短码转换、国旗位置归位及防重复注入机制。
This commit is contained in:
+80
-59
@@ -1,23 +1,37 @@
|
|||||||
# 守候网络节点命名脚本
|
# Sub-Store 节点命名脚本
|
||||||
|
|
||||||
`sntp-rename.js` 是一个专用于守候网络节点命名的 Sub-Store 处理脚本。它会根据节点名称中的线路关键词,自动注入统一的线路标签,让节点列表更容易筛选、分组和识别。
|
这个目录放的是一组 Sub-Store 节点处理脚本。推荐把“单订阅专属规则”和“整合订阅全局规则”分开使用,避免一个机场的关键词误伤其他机场。
|
||||||
|
|
||||||
## 功能
|
## 推荐使用顺序
|
||||||
|
|
||||||
脚本会遍历传入的 `proxies` 数组,检查每个代理节点的 `name` 字段。命中关键词后,会把对应标签追加到节点名中:
|
单订阅脚本先跑,整合订阅最后跑:
|
||||||
|
|
||||||
- 如果节点名带有 `- SNTP...` 后缀,标签会插入到 `- SNTP` 之前。
|
1. `sntp-rename.js`:只给 SNTP 节点注入线路标签。
|
||||||
- 如果节点名没有该后缀,标签会追加到名称末尾。
|
2. `yuetong-rename.js`:只清理 YueTong 的品牌前缀和装饰符号。
|
||||||
- 如果已经包含同样标签,不会重复注入。
|
3. `peiqian-rename.js`:只清理 PeiQian 的推荐/用途描述。
|
||||||
|
4. `rename.js`:整合订阅最终重命名,负责国家短码、国旗归位、订阅后缀和去重。
|
||||||
|
|
||||||
脚本只处理节点名称,不会修改协议、地址、端口或其他连接参数。
|
如果某个订阅没有对应的专属脚本,直接进入最后的 `rename.js` 即可。
|
||||||
|
|
||||||
## 默认规则
|
## 脚本职责
|
||||||
|
|
||||||
|
| 脚本 | 建议配置位置 | 作用 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `sntp-rename.js` | SNTP 单订阅 | 注入 `[三网]`、`[CF]`、`[直连]`、`[cn2]` 等线路标签 |
|
||||||
|
| `yuetong-rename.js` | YueTong 单订阅 | 去掉 `悦·`、`💎`,把 `AI解锁` 压成 `AI` |
|
||||||
|
| `peiqian-rename.js` | PeiQian 单订阅 | 压短 `电信联通推荐`、`移动联通推荐`、`高速专线`、`hy2`、倍率和用途描述 |
|
||||||
|
| `rename.js` | 整合订阅 | 统一国家/城市短码,移动国旗到首部,追加 `- _subName`,处理重名编号 |
|
||||||
|
|
||||||
|
## SNTP 规则
|
||||||
|
|
||||||
|
`sntp-rename.js` 只建议用于 SNTP。像 `BGP`、`D`、`CT` 这类关键词比较短,全局使用容易误伤其他订阅。
|
||||||
|
|
||||||
| 命中关键词 | 注入标签 |
|
| 命中关键词 | 注入标签 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `GTM 0.5x` | `[三网]` |
|
| `GTM 0.5x` | `[三网]` |
|
||||||
| `GTM` | `[三网]` |
|
| `GTM` | `[三网]` |
|
||||||
|
| `CT` | `[CF]` |
|
||||||
|
| `D` | `[直连]` |
|
||||||
| `S1` | `[广移]` |
|
| `S1` | `[广移]` |
|
||||||
| `S2` | `[广电]` |
|
| `S2` | `[广电]` |
|
||||||
| `S3` | `[广移]` |
|
| `S3` | `[广移]` |
|
||||||
@@ -25,75 +39,82 @@
|
|||||||
| `BGP` | `[cn2]` |
|
| `BGP` | `[cn2]` |
|
||||||
| `Anytls` | `[直连]` |
|
| `Anytls` | `[直连]` |
|
||||||
|
|
||||||
## 示例
|
示例:
|
||||||
|
|
||||||
输入:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
香港 GTM 0.5x - SNTP 01
|
🇭🇰 HK-CT-1 0.2x - SNTP
|
||||||
广州 S1 - SNTP 02
|
=> 🇭🇰 HK-CT-1 0.2x [CF] - SNTP
|
||||||
日本 BGP 01
|
|
||||||
深圳 Anytls - SNTP 03
|
🇯🇵 JP-D-1-0.2x - SNTP
|
||||||
普通节点 01
|
=> 🇯🇵 JP-D-1-0.2x [直连] - SNTP
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:
|
## YueTong 规则
|
||||||
|
|
||||||
|
`yuetong-rename.js` 只处理 `_subName === 'YueTong'`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
香港 GTM 0.5x [三网] - SNTP 01
|
🇯🇵 悦·日本专属2💎 - YueTong
|
||||||
广州 S1 [广移] - SNTP 02
|
=> 🇯🇵 日本专属2 - YueTong
|
||||||
日本 BGP 01 [cn2]
|
|
||||||
深圳 Anytls [直连] - SNTP 03
|
|
||||||
普通节点 01
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 在 Sub-Store 中使用
|
再经过 `rename.js` 后:
|
||||||
|
|
||||||
1. 打开 Sub-Store。
|
```text
|
||||||
2. 进入订阅或节点处理配置。
|
🇯🇵 日本专属2 - YueTong
|
||||||
3. 添加 JavaScript 脚本处理器。
|
=> 🇯🇵 JP 专属 2 - YueTong
|
||||||
4. 粘贴 [sntp-rename.js](sntp-rename.js) 内容。
|
|
||||||
5. 保存并刷新订阅。
|
|
||||||
|
|
||||||
脚本暴露的入口函数为:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function operator(proxies) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 修改规则
|
## PeiQian 规则
|
||||||
|
|
||||||
只需要编辑脚本顶部的 `featureMap`。左侧是节点名称中要识别的关键词,右侧是要注入的标签内容:
|
`peiqian-rename.js` 只处理 `_subName === 'PeiQian'`:
|
||||||
|
|
||||||
```javascript
|
```text
|
||||||
const featureMap = {
|
🇯🇵 日本东京03-0.1倍 | 电信联通推荐 - PeiQian
|
||||||
"GTM 0.5x": "三网",
|
=> 🇯🇵 日本东京03 0.1x [电联] - PeiQian
|
||||||
"GTM": "三网",
|
|
||||||
"S1": "广移",
|
🇺🇸 美国01 | 合适下载使用-0.01倍 - PeiQian
|
||||||
"S2": "广电",
|
=> 🇺🇸 美国01 0.01x - PeiQian
|
||||||
"S3": "广移",
|
|
||||||
"S4": "广联",
|
🇼🇸 台湾 | 避免晚高峰使用 - PeiQian
|
||||||
"BGP": "cn2",
|
=> 🇼🇸 台湾 - PeiQian
|
||||||
"Anytls": "直连"
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
新增规则示例:
|
再经过 `rename.js` 后会变成:
|
||||||
|
|
||||||
```javascript
|
```text
|
||||||
"IEPL": "专线"
|
🇯🇵 JP TYO 03 0.1x [电联] - PeiQian
|
||||||
|
🇺🇸 US 01 0.01x - PeiQian
|
||||||
|
🇼🇸 TW - PeiQian
|
||||||
```
|
```
|
||||||
|
|
||||||
## 匹配逻辑
|
## 全局重命名
|
||||||
|
|
||||||
- 关键词匹配不区分大小写。
|
`rename.js` 适合放在整合订阅最后。它做的事情更通用:
|
||||||
- 关键词会按长度从长到短生成正则,避免 `GTM` 先抢走 `GTM 0.5x` 的匹配。
|
|
||||||
- 纯字母数字关键词会自动加单词边界,避免 `S1` 错误匹配到 `US1`。
|
- 国家/地区中文名、英文名转短码,例如 `香港` -> `HK`、`Japan` -> `JP`。
|
||||||
- 映射时会忽略关键词中的空格,例如 `GTM 0.5x` 与 `GTM0.5X` 会映射到同一个标签。
|
- 常见城市转短码,例如 `Los Angeles` -> `LA`、`Seattle` -> `SEA`、`东京` -> `TYO`。
|
||||||
|
- 支持国旗在中间的节点名,例如 `Pro-🇭🇰 Sharon|守夜人` -> `🇭🇰 HK Pro-Sharon|守夜人`。
|
||||||
|
- 清理 `[DIP China-Hongkong]` 为 `[DIP]`。
|
||||||
|
- 清理 `IEPL 专线` 这类冗余描述。
|
||||||
|
- 追加订阅后缀 `- _subName`。
|
||||||
|
- 对最终重名节点追加两位编号。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🇭🇰 Hong Kong 01 [DIP China-Hongkong] - Nexitally
|
||||||
|
=> 🇭🇰 HK 01 [DIP] - Nexitally
|
||||||
|
|
||||||
|
🇭🇰 香港实验性 IEPL 专线 1 - Alpha Flower
|
||||||
|
=> 🇭🇰 HK 实验性 1 - Alpha Flower
|
||||||
|
|
||||||
|
Pro-🇭🇰 Sharon|守夜人 - Alpha 8K Snell
|
||||||
|
=> 🇭🇰 HK Pro-Sharon|守夜人 - Alpha 8K Snell
|
||||||
|
```
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 如果节点命中多个关键词,只会按当前正则排序匹配第一个。
|
- 专属脚本都带 `_subName` 判断,放错位置通常也不会处理其他订阅。
|
||||||
- 如需支持更多后缀位置,可修改脚本中的 `suffixRegex`。
|
- `rename.js` 会剥离并重新追加 `- _subName`,重复运行不会叠加订阅后缀。
|
||||||
|
- 如果新增订阅里出现很短、很泛的关键词,例如 `D`、`CT`、`BGP`、`S1`,优先拆成单订阅脚本,不建议放进全局 `rename.js`。
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* PeiQian 专用节点清理脚本
|
||||||
|
* 只处理 _subName 为 PeiQian 的节点,避免把“推荐/用途/hy2”等文案规则误伤到其他订阅。
|
||||||
|
*/
|
||||||
|
function operator(proxies) {
|
||||||
|
return proxies.map(proxy => {
|
||||||
|
if (proxy._subName !== 'PeiQian' || typeof proxy.name !== 'string') {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.name = proxy.name
|
||||||
|
.replace(/^AWS\s*新加坡\s*/i, '新加坡 AWS ')
|
||||||
|
.replace(/\s*\|\s*合适下载使用\s*-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x')
|
||||||
|
.replace(/\s*\|\s*合适下载使用/gi, '')
|
||||||
|
.replace(/\s*\|\s*避免晚高峰使用/gi, '')
|
||||||
|
.replace(/\s*\|\s*电信联通移动推荐\s*-\s*hy2/gi, ' [三网 HY2]')
|
||||||
|
.replace(/\s*\|\s*电信联通移动推荐/gi, ' [三网]')
|
||||||
|
.replace(/\s*\|\s*电信联通推荐\s*-\s*hy2/gi, ' [电联 HY2]')
|
||||||
|
.replace(/\s*\|\s*电信联通推荐/gi, ' [电联]')
|
||||||
|
.replace(/\s*\|\s*移动联通推荐\s*-\s*hy2/gi, ' [移联 HY2]')
|
||||||
|
.replace(/\s*\|\s*移动联通推荐/gi, ' [移联]')
|
||||||
|
.replace(/\s*\|\s*高速专线(?:推荐)?\s*-\s*hy2/gi, ' [HY2]')
|
||||||
|
.replace(/\s*\|\s*高速专线推荐/gi, ' [专线]')
|
||||||
|
.replace(/\s*\|\s*三网推荐/gi, ' [三网]')
|
||||||
|
.replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x')
|
||||||
|
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1x')
|
||||||
|
.replace(/\s+\[/g, ' [')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* Sub-Store 节点重命名脚本
|
||||||
|
* - 统一节点前缀为地区短码,保留旗帜与订阅后缀
|
||||||
|
* - 清理常见冗余线路描述,例如 DIP 地区说明、IEPL 专线
|
||||||
|
* - 基于归一化后的完整名称做精准去重
|
||||||
|
*/
|
||||||
|
function operator(proxies) {
|
||||||
|
const globalCount = {};
|
||||||
|
const currentCount = {};
|
||||||
|
|
||||||
|
const regionAliases = [
|
||||||
|
['United Arab Emirates', 'AE'],
|
||||||
|
['United States Los Angeles', 'US LA'],
|
||||||
|
['United States San Jose', 'US SJ'],
|
||||||
|
['United States Seattle', 'US SEA'],
|
||||||
|
['Australia Sydney', 'AU SYD'],
|
||||||
|
['Russia St. Petersburg', 'RU LED'],
|
||||||
|
['Russia Moscow', 'RU MOW'],
|
||||||
|
['USA Los Angeles', 'US LA'],
|
||||||
|
['USA San Jose', 'US SJ'],
|
||||||
|
['USA Seattle', 'US SEA'],
|
||||||
|
['United Kingdom', 'UK'],
|
||||||
|
['United States', 'US'],
|
||||||
|
['South Africa', 'ZA'],
|
||||||
|
['Saudi Arabia', 'SA'],
|
||||||
|
['New Zealand', 'NZ'],
|
||||||
|
['Hong Kong', 'HK'],
|
||||||
|
['Netherlands', 'NL'],
|
||||||
|
['Switzerland', 'CH'],
|
||||||
|
['Singapore', 'SG'],
|
||||||
|
['Indonesia', 'ID'],
|
||||||
|
['Philippines', 'PH'],
|
||||||
|
['Azerbaijan', 'AZ'],
|
||||||
|
['Kazakhstan', 'KZ'],
|
||||||
|
['Kyrgyzstan', 'KG'],
|
||||||
|
['Argentina', 'AR'],
|
||||||
|
['Australia', 'AU'],
|
||||||
|
['Macedonia', 'MK'],
|
||||||
|
['Lithuania', 'LT'],
|
||||||
|
['Colombia', 'CO'],
|
||||||
|
['Malaysia', 'MY'],
|
||||||
|
['Thailand', 'TH'],
|
||||||
|
['Vietnam', 'VN'],
|
||||||
|
['Romania', 'RO'],
|
||||||
|
['Nigeria', 'NG'],
|
||||||
|
['Bulgaria', 'BG'],
|
||||||
|
['Germany', 'DE'],
|
||||||
|
['Hungary', 'HU'],
|
||||||
|
['Austria', 'AT'],
|
||||||
|
['Ireland', 'IE'],
|
||||||
|
['Turkey', 'TR'],
|
||||||
|
['Taiwan', 'TW'],
|
||||||
|
['Canada', 'CA'],
|
||||||
|
['Brazil', 'BR'],
|
||||||
|
['Russia', 'RU'],
|
||||||
|
['France', 'FR'],
|
||||||
|
['Sweden', 'SE'],
|
||||||
|
['Belgium', 'BE'],
|
||||||
|
['Denmark', 'DK'],
|
||||||
|
['Iceland', 'IS'],
|
||||||
|
['Norway', 'NO'],
|
||||||
|
['Morocco', 'MA'],
|
||||||
|
['Mexico', 'MX'],
|
||||||
|
['Israel', 'IL'],
|
||||||
|
['Spain', 'ES'],
|
||||||
|
['Italy', 'IT'],
|
||||||
|
['Czech', 'CZ'],
|
||||||
|
['Egypt', 'EG'],
|
||||||
|
['Chile', 'CL'],
|
||||||
|
['India', 'IN'],
|
||||||
|
['Korea', 'KR'],
|
||||||
|
['Japan', 'JP'],
|
||||||
|
['Macau', 'MO'],
|
||||||
|
['Dubai', 'AE DXB'],
|
||||||
|
['Sydney', 'AU SYD'],
|
||||||
|
['Melbourne', 'AU MEL'],
|
||||||
|
['Toronto', 'CA TOR'],
|
||||||
|
['Casablanca', 'MA CMN'],
|
||||||
|
['USA', 'US'],
|
||||||
|
|
||||||
|
['美国圣何塞', 'US SJ'],
|
||||||
|
['美国洛杉矶', 'US LA'],
|
||||||
|
['美国阿什本', 'US IAD'],
|
||||||
|
['日本东京', 'JP TYO'],
|
||||||
|
['韩国首尔', 'KR SEL'],
|
||||||
|
['英国伦敦', 'UK LON'],
|
||||||
|
['瑞士苏黎世', 'CH ZRH'],
|
||||||
|
['印度尼西亚', 'ID'],
|
||||||
|
['所罗门群岛', 'SB'],
|
||||||
|
['阿塞拜疆', 'AZ'],
|
||||||
|
['哈萨克斯坦', 'KZ'],
|
||||||
|
['吉尔吉斯斯坦', 'KG'],
|
||||||
|
['卡萨布兰卡', 'MA CMN'],
|
||||||
|
['澳大利亚', 'AU'],
|
||||||
|
['新西兰', 'NZ'],
|
||||||
|
['马来西亚', 'MY'],
|
||||||
|
['尼日利亚', 'NG'],
|
||||||
|
['摩尔多瓦', 'MD'],
|
||||||
|
['巴基斯坦', 'PK'],
|
||||||
|
['阿根廷', 'AR'],
|
||||||
|
['阿联酋', 'AE'],
|
||||||
|
['柬埔寨', 'KH'],
|
||||||
|
['东帝汶', 'TL'],
|
||||||
|
['梵蒂冈', 'VA'],
|
||||||
|
['百慕大', 'BM'],
|
||||||
|
['格陵兰', 'GL'],
|
||||||
|
['南极洲', 'AQ'],
|
||||||
|
['罗马尼亚', 'RO'],
|
||||||
|
['哥伦比亚', 'CO'],
|
||||||
|
['菲律宾', 'PH'],
|
||||||
|
['以色列', 'IL'],
|
||||||
|
['比利时', 'BE'],
|
||||||
|
['立陶宛', 'LT'],
|
||||||
|
['马其顿', 'MK'],
|
||||||
|
['俄罗斯', 'RU'],
|
||||||
|
['乌克兰', 'UA'],
|
||||||
|
['匈牙利', 'HU'],
|
||||||
|
['西班牙', 'ES'],
|
||||||
|
['土耳其', 'TR'],
|
||||||
|
['加拿大', 'CA'],
|
||||||
|
['意大利', 'IT'],
|
||||||
|
['奥地利', 'AT'],
|
||||||
|
['阿富汗', 'AF'],
|
||||||
|
['卡塔尔', 'QA'],
|
||||||
|
['索马里', 'SO'],
|
||||||
|
['新加坡', 'SG'],
|
||||||
|
['印度', 'IN'],
|
||||||
|
['印尼', 'ID'],
|
||||||
|
['日本', 'JP'],
|
||||||
|
['香港', 'HK'],
|
||||||
|
['台湾', 'TW'],
|
||||||
|
['美国', 'US'],
|
||||||
|
['韩国', 'KR'],
|
||||||
|
['英国', 'UK'],
|
||||||
|
['德国', 'DE'],
|
||||||
|
['荷兰', 'NL'],
|
||||||
|
['法国', 'FR'],
|
||||||
|
['巴西', 'BR'],
|
||||||
|
['智利', 'CL'],
|
||||||
|
['澳门', 'MO'],
|
||||||
|
['泰国', 'TH'],
|
||||||
|
['越南', 'VN'],
|
||||||
|
['瑞典', 'SE'],
|
||||||
|
['丹麦', 'DK'],
|
||||||
|
['多哥', 'TG'],
|
||||||
|
['埃及', 'EG'],
|
||||||
|
['冰岛', 'IS'],
|
||||||
|
['挪威', 'NO'],
|
||||||
|
['南非', 'ZA'],
|
||||||
|
['古巴', 'CU'],
|
||||||
|
['斐济', 'FJ'],
|
||||||
|
['关岛', 'GU'],
|
||||||
|
['缅甸', 'MM'],
|
||||||
|
['蒙古', 'MN'],
|
||||||
|
['老挝', 'LA'],
|
||||||
|
['捷克', 'CZ'],
|
||||||
|
['沙特', 'SA'],
|
||||||
|
['墨西哥', 'MX'],
|
||||||
|
['迪拜', 'AE DXB'],
|
||||||
|
['悉尼', 'AU SYD'],
|
||||||
|
['墨尔本', 'AU MEL'],
|
||||||
|
['多伦多', 'CA TOR']
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortedRegionAliases = regionAliases
|
||||||
|
.slice()
|
||||||
|
.sort((firstAlias, secondAlias) => secondAlias[0].length - firstAlias[0].length);
|
||||||
|
|
||||||
|
const flagCodeOverrides = {
|
||||||
|
'🇬🇧': 'UK',
|
||||||
|
'🇼🇸': 'TW'
|
||||||
|
};
|
||||||
|
|
||||||
|
const knownRegionCodes = new Set(
|
||||||
|
regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0])
|
||||||
|
);
|
||||||
|
Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code));
|
||||||
|
|
||||||
|
const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
const flagToRegionCode = flag => {
|
||||||
|
if (!flag) return '';
|
||||||
|
if (flagCodeOverrides[flag]) return flagCodeOverrides[flag];
|
||||||
|
|
||||||
|
const codePoints = [...flag].map(char => char.codePointAt(0));
|
||||||
|
if (
|
||||||
|
codePoints.length !== 2 ||
|
||||||
|
codePoints.some(codePoint => codePoint < 0x1F1E6 || codePoint > 0x1F1FF)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return codePoints
|
||||||
|
.map(codePoint => String.fromCharCode(codePoint - 0x1F1E6 + 65))
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripExistingSuffix = (name, subName) => {
|
||||||
|
if (!subName) return name.trim();
|
||||||
|
const suffixRegex = new RegExp(`\\s+-\\s*${escapeRegex(subName)}(?:\\s+\\d{2})?\\s*$`, 'i');
|
||||||
|
return name.replace(suffixRegex, '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinAlias = (replacement, rest) => {
|
||||||
|
const cleanRest = rest.trim();
|
||||||
|
if (!cleanRest) return replacement;
|
||||||
|
if (/^[|/\-]/.test(cleanRest)) {
|
||||||
|
return `${replacement}${cleanRest}`;
|
||||||
|
}
|
||||||
|
return `${replacement} ${cleanRest}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeLeadingRegion = name => {
|
||||||
|
const cleanName = name.trim();
|
||||||
|
|
||||||
|
for (const [alias, replacement] of sortedRegionAliases) {
|
||||||
|
if (!cleanName.toLowerCase().startsWith(alias.toLowerCase())) continue;
|
||||||
|
|
||||||
|
const rest = cleanName.slice(alias.length);
|
||||||
|
if (/^[A-Za-z]/.test(alias) && rest && !/^[\s\d[|\-.x]/i.test(rest)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return joinAlias(replacement, rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const leadingCodeOf = name => {
|
||||||
|
const match = name.match(/^([A-Z]{2})(?=\b|\s|-|$|\d)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasKnownRegionCode = name => {
|
||||||
|
const leadingCode = leadingCodeOf(name);
|
||||||
|
if (knownRegionCodes.has(leadingCode)) return true;
|
||||||
|
|
||||||
|
const proMatch = name.match(/^Pro-\s*(.+)$/i);
|
||||||
|
return proMatch ? knownRegionCodes.has(leadingCodeOf(proMatch[1])) : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSpecificRegionCode = (name, regionCode) => {
|
||||||
|
if (!regionCode) return false;
|
||||||
|
if (leadingCodeOf(name) === regionCode) return true;
|
||||||
|
|
||||||
|
const proMatch = name.match(/^Pro-\s*(.+)$/i);
|
||||||
|
return proMatch ? leadingCodeOf(proMatch[1]) === regionCode : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveProRegionToFront = (name, regionCode) => {
|
||||||
|
if (!regionCode) return name;
|
||||||
|
|
||||||
|
const proRegionRegex = new RegExp(`^Pro-\\s*${escapeRegex(regionCode)}(?=\\s|[\\u4e00-\\u9fff]|\\d)(.*)$`, 'i');
|
||||||
|
const proRegionMatch = name.match(proRegionRegex);
|
||||||
|
if (!proRegionMatch) return name;
|
||||||
|
|
||||||
|
const rest = proRegionMatch[1].trim();
|
||||||
|
return rest ? `${regionCode} Pro-${rest}` : regionCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const simplifyCoreName = name => {
|
||||||
|
return name
|
||||||
|
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '')
|
||||||
|
.replace(/\u200d/g, '')
|
||||||
|
.replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x')
|
||||||
|
.replace(/(\d+(?:\.\d+)?)倍/gi, '$1x')
|
||||||
|
.replace(/\b(\d+(?:\.\d+)?)X\b/g, '$1x')
|
||||||
|
.replace(/([A-Z]{2})\s*(专属|家宽|AI|高速|专线)(\d+)/g, '$1 $2 $3')
|
||||||
|
.replace(/\b(AI)(\d+)/g, '$1 $2')
|
||||||
|
.replace(/\b(Pro-)\s+/gi, '$1')
|
||||||
|
.replace(/^((?:Pro-\s*)?)([A-Z]{2})(?=[\u4e00-\u9fff]|\d)/u, '$1$2 ')
|
||||||
|
.replace(/\s+\[/g, ' [')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeBilingualName = name => {
|
||||||
|
const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/);
|
||||||
|
if (!bilingualMatch) return name;
|
||||||
|
|
||||||
|
const leftName = normalizeLeadingRegion(bilingualMatch[1]);
|
||||||
|
const rightName = normalizeLeadingRegion(bilingualMatch[2]);
|
||||||
|
const leftCode = leadingCodeOf(leftName);
|
||||||
|
const rightCode = leadingCodeOf(rightName);
|
||||||
|
|
||||||
|
if (leftCode && rightCode && leftCode === rightCode) {
|
||||||
|
return rightName === rightCode ? leftName : rightName;
|
||||||
|
}
|
||||||
|
if (rightCode) return rightName;
|
||||||
|
if (leftCode) return leftName;
|
||||||
|
|
||||||
|
return name.replace(/\s*|\s*/g, ' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCoreName = name => {
|
||||||
|
const flagPattern = '([\\u{1F1E6}-\\u{1F1FF}]{2}|🏴☠️)';
|
||||||
|
const leadingFlagMatch = name.match(new RegExp(`^${flagPattern}\\s*`, 'u'));
|
||||||
|
const inlineFlagMatch = leadingFlagMatch
|
||||||
|
? null
|
||||||
|
: name.match(new RegExp(`^(.{1,24}?)${flagPattern}\\s*(.*)$`, 'u'));
|
||||||
|
const flag = leadingFlagMatch ? leadingFlagMatch[1] : inlineFlagMatch ? inlineFlagMatch[2] : '';
|
||||||
|
let core = name.trim();
|
||||||
|
|
||||||
|
if (leadingFlagMatch) {
|
||||||
|
core = name.slice(leadingFlagMatch[0].length).trim();
|
||||||
|
} else if (inlineFlagMatch) {
|
||||||
|
const prefix = inlineFlagMatch[1].trim();
|
||||||
|
const rest = inlineFlagMatch[3].trim();
|
||||||
|
core = rest ? `${prefix}${prefix.endsWith('-') ? '' : ' '}${rest}` : prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
core = core
|
||||||
|
.replace(/\[DIP[^\]]*\]/gi, '[DIP]');
|
||||||
|
|
||||||
|
core = normalizeBilingualName(core);
|
||||||
|
core = normalizeLeadingRegion(core);
|
||||||
|
|
||||||
|
if (/^Pro-\s*/i.test(core)) {
|
||||||
|
core = core.replace(/^(Pro-\s*)(.+)$/i, (fullMatch, proPrefix, restName) => {
|
||||||
|
return `${proPrefix}${normalizeLeadingRegion(restName)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
core = core
|
||||||
|
.replace(/^([A-Z]{2})\s*(实验性|标准|高级)\s+IEPL\s+专线\s*(\d+)(.*)$/i, '$1 $2 $3$4')
|
||||||
|
.replace(/搬瓦工荷兰/g, '搬瓦工')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const flagRegionCode = flagToRegionCode(flag);
|
||||||
|
core = moveProRegionToFront(core, flagRegionCode);
|
||||||
|
|
||||||
|
if (
|
||||||
|
flagRegionCode &&
|
||||||
|
!hasSpecificRegionCode(core, flagRegionCode) &&
|
||||||
|
!hasKnownRegionCode(core)
|
||||||
|
) {
|
||||||
|
core = joinAlias(flagRegionCode, core);
|
||||||
|
}
|
||||||
|
|
||||||
|
core = simplifyCoreName(core);
|
||||||
|
|
||||||
|
return flag ? `${flag} ${core}` : core;
|
||||||
|
};
|
||||||
|
|
||||||
|
proxies.forEach(proxy => {
|
||||||
|
const proxyName = typeof proxy.name === 'string' ? proxy.name : '';
|
||||||
|
const subName = typeof proxy._subName === 'string' ? proxy._subName.trim() : '';
|
||||||
|
const coreName = stripExistingSuffix(proxyName, subName);
|
||||||
|
const normalizedCoreName = normalizeCoreName(coreName);
|
||||||
|
const baseName = subName ? `${normalizedCoreName} - ${subName}` : normalizedCoreName;
|
||||||
|
|
||||||
|
proxy._tempBaseName = baseName;
|
||||||
|
globalCount[baseName] = (globalCount[baseName] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return proxies.map(proxy => {
|
||||||
|
const baseName = proxy._tempBaseName;
|
||||||
|
|
||||||
|
if (globalCount[baseName] > 1) {
|
||||||
|
currentCount[baseName] = (currentCount[baseName] || 0) + 1;
|
||||||
|
proxy.name = `${baseName} ${currentCount[baseName].toString().padStart(2, '0')}`;
|
||||||
|
} else {
|
||||||
|
proxy.name = baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete proxy._tempBaseName;
|
||||||
|
return proxy;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,11 +10,13 @@ function operator(proxies) {
|
|||||||
const featureMap = {
|
const featureMap = {
|
||||||
"GTM 0.5x": "三网",
|
"GTM 0.5x": "三网",
|
||||||
"GTM": "三网",
|
"GTM": "三网",
|
||||||
|
"CT": "CF",
|
||||||
"S1": "广移",
|
"S1": "广移",
|
||||||
"S2": "广电",
|
"S2": "广电",
|
||||||
"S3": "广移",
|
"S3": "广移",
|
||||||
"S4": "广联",
|
"S4": "广联",
|
||||||
"BGP": "cn2",
|
"BGP": "cn2",
|
||||||
|
"D": "直连",
|
||||||
"Anytls": "直连" // 日后如果要加 IEPL,只需在这里写 "IEPL": "专线"
|
"Anytls": "直连" // 日后如果要加 IEPL,只需在这里写 "IEPL": "专线"
|
||||||
};
|
};
|
||||||
// ==============================================
|
// ==============================================
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* YueTong 专用节点清理脚本
|
||||||
|
* 只处理 _subName 为 YueTong 的节点,负责去掉“悦·”品牌前缀、钻石等装饰,以及压短 AI 解锁文案。
|
||||||
|
*/
|
||||||
|
function operator(proxies) {
|
||||||
|
return proxies.map(proxy => {
|
||||||
|
if (proxy._subName !== 'YueTong' || typeof proxy.name !== 'string') {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.name = proxy.name
|
||||||
|
.replace(/悦[·・]\s*/g, '')
|
||||||
|
.replace(/AI解锁/g, 'AI')
|
||||||
|
.replace(/[💎]/gu, '')
|
||||||
|
.replace(/\s{2,}/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user