├── .github
└── workflows
│ ├── test.yml
│ └── obfuscate.yml
├── README.md
├── فارسی.md
└── snippets
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Worker Script
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*' # 当任何标签被推送时触发
7 |
8 | jobs:
9 | deploy-worker:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Get tag name
14 | id: get_tag
15 | run: |
16 | TAG_NAME=${GITHUB_REF#refs/tags/}
17 | echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
18 | echo "当前标签: $TAG_NAME"
19 |
20 | - name: Checkout repository
21 | uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0 # 需要完整的Git历史来获取标签信息
24 |
25 | - name: Check if source file exists
26 | run: |
27 | if [ ! -f "少年你相信光吗" ]; then
28 | echo "错误:在项目根目录未找到 '少年你相信光吗' 文件。"
29 | exit 1
30 | fi
31 | echo "成功找到源文件 '少年你相信光吗'"
32 |
33 | - name: Rename file to _worker.js
34 | run: |
35 | cp "少年你相信光吗" "_worker.js"
36 | echo "成功将 '少年你相信光吗' 复制为 '_worker.js'"
37 | echo "注意:文件不会提交到仓库,仅用于 Release"
38 |
39 | - name: Create zip file
40 | run: |
41 | zip Pages.zip _worker.js
42 | echo "成功将 '_worker.js' 压缩为 'Pages.zip'"
43 |
44 | - name: Create GitHub Release
45 | uses: softprops/action-gh-release@v1
46 | with:
47 | tag_name: ${{ steps.get_tag.outputs.tag_name }}
48 | name: Pages ${{ steps.get_tag.outputs.tag_name }}
49 | body: |
50 | ## 部署信息
51 |
52 | - **源文件**: 少年你相信光吗
53 | - **目标文件**: _worker.js
54 | - **压缩文件**: Pages.zip
55 | - **标签**: ${{ steps.get_tag.outputs.tag_name }}
56 | - **部署时间**: ${{ github.event.head_commit.timestamp }}
57 |
58 | ## 文件变更
59 |
60 | 已从 `少年你相信光吗` 生成 `_worker.js` 文件,并压缩为 `Pages.zip`。
61 | draft: false
62 | prerelease: false
63 | files: Pages.zip
64 | env:
65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66 |
67 | - name: Output summary
68 | run: |
69 | echo "## 部署完成" >> $GITHUB_STEP_SUMMARY
70 | echo "- 源文件: 少年你相信光吗" >> $GITHUB_STEP_SUMMARY
71 | echo "- 目标文件: _worker.js" >> $GITHUB_STEP_SUMMARY
72 | echo "- 压缩文件: Pages.zip" >> $GITHUB_STEP_SUMMARY
73 | echo "- 标签: ${{ steps.get_tag.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
74 | echo "- GitHub Release 已创建" >> $GITHUB_STEP_SUMMARY
75 |
--------------------------------------------------------------------------------
/.github/workflows/obfuscate.yml:
--------------------------------------------------------------------------------
1 | name: Generate and Obfuscate Worker Script
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - '**' # 仅匹配分支推送,排除标签推送
8 | paths:
9 | - '明文源吗'
10 |
11 | jobs:
12 | build-and-obfuscate:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up Node.js
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: '18'
23 |
24 | - name: Install Obfuscator
25 | run: npm install javascript-obfuscator
26 |
27 | - name: Obfuscate from local source file
28 | run: |
29 | cat > obfuscate.js << 'EOF'
30 | const JavaScriptObfuscator = require('javascript-obfuscator');
31 | const fs = require('fs');
32 | const path = require('path');
33 |
34 | const sourceFileName = '明文源吗';
35 | const outputFileName = '少年你相信光吗';
36 | const sourceFilePath = path.join(process.cwd(), sourceFileName);
37 |
38 | if (!fs.existsSync(sourceFilePath)) {
39 | console.error('错误:在路径 \'' + sourceFilePath + '\' 未找到源文件。请确保您的仓库根目录有名为 \'明文源吗\' 的文件。');
40 | process.exit(1);
41 | }
42 |
43 | const originalCode = fs.readFileSync(sourceFilePath, 'utf8');
44 |
45 | if (!originalCode || originalCode.trim().length === 0) {
46 | console.error('错误:源文件 ' + sourceFileName + ' 为空。');
47 | process.exit(1);
48 | }
49 |
50 | const obfuscationOptions = {
51 | compact: true,
52 | controlFlowFlattening: false,
53 | controlFlowFlatteningThreshold: 0,
54 | deadCodeInjection: false,
55 | stringArray: true,
56 | stringArrayEncoding: ['base64'],
57 | stringArrayThreshold: 1.0,
58 | stringArrayRotate: true,
59 | stringArrayShuffle: true,
60 | stringArrayWrappersCount: 2,
61 | stringArrayWrappersChainedCalls: false,
62 | stringArrayWrappersParametersMaxCount: 3,
63 | renameGlobals: true,
64 | identifierNamesGenerator: 'mangled-shuffled',
65 | identifierNamesCache: null,
66 | identifiersPrefix: '',
67 | renameProperties: false,
68 | renamePropertiesMode: 'safe',
69 | ignoreImports: false,
70 | target: 'browser',
71 | numbersToExpressions: false,
72 | simplify: false,
73 | splitStrings: true,
74 | splitStringsChunkLength: 1,
75 | transformObjectKeys: false,
76 | unicodeEscapeSequence: true,
77 | selfDefending: false,
78 | debugProtection: false,
79 | debugProtectionInterval: 0,
80 | disableConsoleOutput: true,
81 | domainLock: []
82 | };
83 |
84 | const obfuscatedCode = JavaScriptObfuscator.obfuscate(originalCode, obfuscationOptions).getObfuscatedCode();
85 |
86 | fs.writeFileSync(path.join(process.cwd(), outputFileName), obfuscatedCode, 'utf8');
87 | console.log('成功将 \'' + sourceFileName + '\' 混淆并保存至 \'' + outputFileName + '\'。');
88 | EOF
89 | node obfuscate.js
90 |
91 | - name: Commit and push the obfuscated file
92 | run: |
93 | git config --global user.name 'GitHub Actions Bot'
94 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
95 | git add '少年你相信光吗'
96 | if git diff --staged --quiet; then
97 | echo "No changes to commit, the obfuscated file is already up-to-date."
98 | else
99 | git commit -m "部署用这个"
100 | git push
101 | fi
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CFnew - 终端 v2.8
2 |
3 |
4 |
5 | **语言 / زبان:** [🇨🇳 中文](README.md) | [🇮🇷 فارسی](فارسی.md)
6 |
7 |
8 |
9 |
10 |
11 | **多协议支持 · 自定义路径 · 延迟测试优选**
12 |
13 | [](https://t.me/+ft-zI76oovgwNmRh)
14 | []()
15 | []()
16 |
17 |
18 |
19 | ## ✨ v2.8 核心特性
20 |
21 | - 🎭 **多协议支持** - VLESS + Trojan + xhttp,自由切换
22 | - 🛤️ **自定义路径** - 告别UUID路径,自定义访问地址,支持多级路径
23 | - ⚡ **延迟测试** - 内置IP延迟测试,自动获取机场码,多线程并发测试
24 | - 🔄 **订阅转换** - 自定义转换服务,优选类型细粒度控制
25 | - 🎯 **图形化管理** - KV存储,实时配置,无需重新部署
26 | - 🚀 **API管理** - RESTful API,批量操作,动态更新
27 | - 📱 **多客户端支持** - CLASH、SURGE、SING-BOX、LOON、QUANTUMULT X、V2RAY、Shadowrocket、STASH、NEKORAY、V2RAYNG
28 | - 🚀 **应用唤醒** - 点击按钮自动唤醒对应客户端应用
29 | - 🔍 **自动识别** - 根据User-Agent自动识别并返回对应格式
30 | - 🌐 **多语言支持** - 支持中文和波斯语(伊朗语),根据浏览器语言自动切换
31 |
32 | ---
33 |
34 | ## 🆕 v2.8 更新内容
35 |
36 | - ⚙️ **在线优选展示优化** - 延迟测试结果列表只展示测速成功的优选节点,失败/超时自动隐藏,方便一键添加优选。
37 | - 🌍 **地区/机场国旗显示** - Cloudflare 数据中心映射名称前统一加上对应国家/地区 emoji 国旗,例如 `🇺🇸 圣何塞`、`🇭🇰 香港`。
38 | - 🌐 **URL 获取增强** - “URL获取” 支持逗号分隔多个 URL,并自动解析远程内容中逗号分隔的 IP/节点,统一填入测试列表。
39 | - 🎛️ **界面特效收敛** - 默认关闭矩阵雨、光效闪烁等前端特效,界面更简洁稳定,仍保留绿色终端风格。
40 | - 🧩 **细节与文案优化** - 版本号更新为 v2.8,并对多语言文案和若干内部逻辑做小幅优化。
41 |
42 | ---
43 |
44 | ### 配套工具
45 |
46 | | 类型 | 描述 | 链接 |
47 | | :--- | :--- | :--- |
48 | | **优选工具** | 根据自己的网络环境选择最适合的IP | https://github.com/byJoey/yx-tools/releases |
49 | | **文字教程** | 详细的部署与使用说明博客文章 | [https://joeyblog.net/yuanchuang/1146.html](https://joeyblog.net/yuanchuang/1146.html) |
50 | | **Workers视频教程** | 直观的操作演示和功能讲解 | https://www.youtube.com/watch?v=aYzTr8FafN4 |
51 | | **Pages视频教程** | 直观的操作演示和功能讲解 | https://www.youtube.com/watch?v=JhVxJChDL-E |
52 | | **Snippets视频教程** | 直观的操作演示和功能讲解 | https://www.youtube.com/watch?v=xeFeH3Akcu8 |
53 |
54 | ### 部署
55 |
56 | 加入了千呼万唤的订阅每15分钟自动优选一次
57 |
58 | #### 🔧 基础配置
59 | | 变量名 | 值 | 说明 |
60 | | :--- | :--- | :--- |
61 | | `u` | `你的 UUID` | **必需**。用于访问订阅和配置管理界面 |
62 | | `p` | `proxyip` | **可选**。自定义ProxyIP地址和端口 |
63 | | `s` | `你的SOCKS5地址` | **可选**。用于将所有出站流量通过 SOCKS5 代理转发,格式为 `user:pass@host:port` 或 `host:port` |
64 | | `d` | `自定义路径` | **可选**。自定义订阅访问路径,支持多级路径,如 `/mypath` 或 `/path/to/sub`,不填则使用 UUID 路径。如果路径没有以 `/` 开头,会自动补上 |
65 | | `wk` | `地区代码` | **可选**。手动指定Worker地区,如:`SG`、`HK`、`US`、`JP`等 |
66 |
67 | #### 🎭 协议配置
68 | | 变量名 | 值 | 说明 |
69 | | :--- | :--- | :--- |
70 | | `ev` | `yes/no` | **可选**。启用VLESS协议(默认启用) |
71 | | `et` | `yes/no` | **可选**。启用Trojan协议(默认禁用) |
72 | | `ex` | `yes/no` | **可选**。启用xhttp协议(默认禁用) |
73 | | `tp` | `自定义密码` | **可选**。Trojan协议密码,留空则使用UUID |
74 |
75 | #### 🎯 图形化配置(推荐)
76 | - **KV存储配置**:在Workers中创建KV命名空间,绑定环境变量 `C`
77 | - **访问界面**:部署后访问 `/{你的UUID}` 即可使用图形化配置管理
78 | - **实时生效**:通过界面修改配置无需重新部署,立即生效
79 |
80 | #### 🔧 高级控制
81 | | 变量名 | 值 | 说明 |
82 | | :--- | :--- | :--- |
83 | | `yx` | `自定义优选IP/域名` | **可选**。支持节点命名,格式:`1.1.1.1:443#香港节点,8.8.8.8:53#Google DNS` |
84 | | `yxURL` | `优选IP来源URL` | **可选**。自定义优选IP列表来源URL,留空则使用默认地址 |
85 | | `scu` | `订阅转换地址` | **可选**。自定义订阅转换服务URL,默认:`https://url.v1.mk/sub` |
86 | | `epd` | `yes/no` | **可选**。启用优选域名(默认启用) |
87 | | `epi` | `yes/no` | **可选**。启用优选IP(默认启用) |
88 | | `egi` | `yes/no` | **可选**。启用GitHub默认优选(默认启用) |
89 | | `qj` | `no` | **可选**。降级控制,设置为`no`时启用降级模式:CF直连失败→SOCKS5连接→fallback地址 |
90 | | `dkby` | `yes` | **可选**。TLS控制,设置为`yes`时只生成TLS节点,不生成非TLS节点(如80端口) |
91 | | `yxby` | `yes` | **可选**。优选控制,设置为`yes`时关闭所有优选功能,只使用原生地址,不生成优选IP和域名节点 |
92 | | `rm` | `no` | **可选**。地区匹配控制,设置为`no`时关闭地区智能匹配 |
93 | | `ae` | `yes` | **可选**。API管理开关,设置为`yes`时允许通过API动态管理优选IP(默认关闭) |
94 |
95 | #### 📦 KV存储设置(可选但推荐)
96 | 1. 在Cloudflare Workers中创建KV命名空间
97 | 2. 在Workers设置中绑定KV命名空间,变量名设为 `C`
98 | 3. 重新部署Workers
99 | 4. 访问 `/{你的UUID}` 即可使用图形化配置管理
100 |
101 | #### 🔑 API快速开始
102 | 1. https://github.com/byJoey/yx-tools/releases 优选软件
103 | 2. **开启API功能**:访问 `/{UUID}` 或 `/{自定义路径}` → 找到"允许API管理"→ 选择"开启API管理"→ 保存
104 | 3. **添加单个IP**:
105 | ```bash
106 | # 使用UUID路径
107 | curl -X POST "https://your-worker.workers.dev/{UUID}/api/preferred-ips" \
108 | -H "Content-Type: application/json" \
109 | -d '{"ip": "1.2.3.4", "port": 443, "name": "香港节点"}'
110 |
111 | # 使用自定义路径(如果设置了d变量)
112 | curl -X POST "https://your-worker.workers.dev/{自定义路径}/api/preferred-ips" \
113 | -H "Content-Type: application/json" \
114 | -d '{"ip": "1.2.3.4", "port": 443, "name": "香港节点"}'
115 | ```
116 | 4. **批量添加IP**:
117 | ```bash
118 | curl -X POST "https://your-worker.workers.dev/{UUID或自定义路径}/api/preferred-ips" \
119 | -H "Content-Type: application/json" \
120 | -d '[
121 | {"ip": "1.2.3.4", "port": 443, "name": "节点1"},
122 | {"ip": "5.6.7.8", "port": 8443, "name": "节点2"}
123 | ]'
124 | ```
125 | 5. **一键清空**:
126 | ```bash
127 | curl -X DELETE "https://your-worker.workers.dev/{UUID或自定义路径}/api/preferred-ips" \
128 | -H "Content-Type: application/json" \
129 | -d '{"all": true}'
130 | ```
131 |
132 | ### 新功能
133 |
134 | #### ⚡ 延迟测试功能(v2.7 起提供,v2.8 优化展示)
135 | - **内置测试工具**:无需外部软件,直接在配置页面测试IP延迟
136 | - **多种IP来源**:
137 | - 手动输入:直接输入IP或域名,支持批量(逗号分隔)
138 | - CF随机IP:从Cloudflare IP段随机生成指定数量的IP
139 | - URL获取:从远程URL获取IP列表进行测试
140 | - **多线程并发**:支持1-50线程并发测试,默认5线程
141 | - **自动获取机场码**:测试成功后自动获取CF机场码(如SJC、LAX等)
142 | - **中文机场名映射**:自动将机场码映射为中文名称(如SJC→圣何塞)
143 | - **真实延迟计算**:自动扣除DNS+TLS握手时间,显示纯网络延迟
144 | - **浏览器记忆**:所有设置(端口、线程数、IP来源、URL等)自动保存到浏览器
145 | - **一键添加优选**:选中测试结果后一键替换优选列表并自动保存到KV
146 |
147 | #### 🎭 多协议支持
148 | - **VLESS协议**:默认启用,完整的VLESS支持
149 | - **Trojan协议**:支持Trojan-WS-TLS,自定义密码或使用UUID
150 | - **xhttp协议**:基于HTTP POST的伪装协议
151 | - **协议混用**:支持同时启用多个协议,客户端自动识别
152 | - **协议切换**:图形化界面一键启用/禁用协议
153 | - **独立保存**:协议配置区域新增独立保存按钮,更方便快捷
154 | - **密码管理**:Trojan支持自定义密码,留空自动使用UUID
155 |
156 | #### 🛤️ 自定义路径(d变量)
157 | - **路径自定义**:不再使用UUID作为路径,设置你喜欢的路径名
158 | - **多级路径支持**:支持多级路径,如 `/path/to/sub`,灵活组织访问结构
159 | - **自动补全**:如果路径没有以 `/` 开头,系统会自动补上
160 | - **安全增强**:自定义路径后UUID路径自动禁用,提高隐蔽性
161 | - **灵活访问**:`https://worker.dev/mypath/sub` 或 `https://worker.dev/path/to/sub/sub` 更简洁易记
162 | - **动态切换**:通过图形界面随时修改访问路径
163 | - **状态显示**:配置页面实时显示当前使用的路径类型
164 |
165 | #### 🎯 图形化配置管理
166 | - **KV存储支持**:使用Cloudflare KV存储持久化配置
167 | - **图形化界面**:访问 `/{你的UUID}` 或 `/{自定义路径}` 即可使用配置管理界面
168 | - **实时配置**:无需重新部署,配置立即生效
169 | - **配置优先级**:KV配置 > 环境变量 > 默认值
170 |
171 | #### 🌐 多语言支持
172 | - **自动语言检测**:根据浏览器语言自动选择中文或波斯语
173 | - **手动语言切换**:页面右上角提供语言选择器,可手动切换
174 | - **持久化存储**:用户选择的语言会保存到浏览器,下次访问自动应用
175 | - **RTL支持**:波斯语版本自动启用从右到左(RTL)布局
176 | - **完整翻译**:所有界面文本、提示信息、按钮标签都已翻译
177 |
178 | #### 🔄 订阅转换控制
179 | - **自定义转换服务**:支持配置自定义订阅转换URL
180 | - **优选类型控制**:细粒度控制优选域名、优选IP、GitHub优选
181 | - **独立开关**:每种优选类型可单独启用/禁用
182 | - **默认设置**:内置优选域名、优选IP、GitHub默认优选全部启用
183 | - **实时更新**:修改配置后立即生效,无需重新部署
184 |
185 | #### 🚀 API动态管理
186 | - **API管理**:通过RESTful API动态管理优选IP,无需修改代码
187 | - **批量上报**:支持一次性批量添加多个优选IP
188 | - **一键清空**:支持清空所有优选IP,快速更新列表
189 | - **安全开关**:默认关闭,需在图形界面手动开启API功能
190 | - **自动合并**:API添加的IP与手动配置的yx变量自动合并
191 | - **实时同步**:API添加的IP立即在配置页面显示
192 | - **API端点**:
193 | - `GET /{UUID或路径}/api/preferred-ips` - 查询优选IP列表
194 | - `POST /{UUID或路径}/api/preferred-ips` - 添加优选IP(支持单个/批量)
195 | - `DELETE /{UUID或路径}/api/preferred-ips` - 删除优选IP(支持单个/全部)
196 |
197 | #### 🌍 手动指定地区
198 | - **地区选择**:支持手动指定Worker地区,覆盖自动检测
199 | - **设置方式**:`wk=SG` 或通过图形化界面选择
200 | - **支持地区**:US、SG、JP、HK、KR、DE、SE、NL、FI、GB
201 | - **智能显示**:系统状态会显示"手动指定地区"而非"自动检测"
202 |
203 | #### 🏷️ 优选节点命名
204 | - **自定义名称**:支持为优选节点设置自定义名称
205 | - **格式支持**:`IP:端口#节点名称` 或 `IP:端口`(使用默认名称)
206 | - **示例**:`1.1.1.1:443#香港节点,8.8.8.8:53#Google DNS`
207 | - **默认格式**:未设置名称时自动生成 `自定义优选-IP:端口`
208 |
209 | #### 📊 系统状态监控
210 | - **实时检测**:显示Worker地区、检测方式、ProxyIP状态
211 | - **智能匹配**:同地区 → 邻近地区 → 其他地区的选择逻辑
212 | - **状态指示**:可视化显示系统运行状态和配置信息
213 |
214 | #### 🔧 高级控制选项
215 | - **地区匹配控制**:`rm=no` 关闭地区智能匹配
216 | - **降级控制**:`qj=no` 启用降级模式(CF直连失败→SOCKS5→fallback)
217 | - **TLS控制**:`dkby=yes` 只生成TLS节点,不生成非TLS节点
218 | - **优选控制**:`yxby=yes` 关闭所有优选功能
219 |
220 | #### 🎨 多客户端支持
221 | - **10种客户端**:支持 CLASH、SURGE、SING-BOX、LOON、QUANTUMULT X、V2RAY、Shadowrocket、STASH、NEKORAY、V2RAYNG
222 | - **自动转换**:根据客户端类型自动生成对应配置
223 | - **一键获取**:图形化界面一键生成订阅链接
224 | - **应用唤醒**:点击客户端按钮自动唤醒对应应用,无需手动复制链接
225 | - **自动识别**:根据User-Agent自动识别客户端类型并返回对应格式
226 | - **协议适配**:不同客户端自动适配最佳协议组合
227 | - **ALPN支持**:所有TLS链接自动包含 `h3,h2,http/1.1` 协议协商
228 |
229 | #### ⚡ 性能优化
230 | - **智能优选**:每15分钟自动优选一次,保持最佳性能
231 | - **容错机制**:多重备用方案,确保服务稳定性
232 | - **缓存优化**:智能缓存机制,减少重复计算
233 |
234 | ### 致谢
235 |
236 | * 本项目基于 [zizifn/edgetunnel](https://github.com/zizifn/edgetunnel) 修改,感谢原作者的贡献。
237 | * 本项目内置ProxyIP部分 来自CM [[cmliu](https://github.com/cmliu)) ,感谢作者的贡献。
238 | * 本项目反代IP来着前端独苗kejiland [[qwer-search](https://github.com/qwer-search)) ,感谢作者的贡献。
239 | * 本项目在线优选接口来自 [[白嫖哥](https://t.me/bestcfipas)) ,感谢作者的贡献。
240 |
241 |
242 | ## Star History
243 |
244 | [](https://www.star-history.com/#byJoey/cfnew&Timeline&LogScale)
245 |
--------------------------------------------------------------------------------
/فارسی.md:
--------------------------------------------------------------------------------
1 | # CFnew - ترمینال v2.2
2 |
3 |
4 |
5 | **زبان / 语言:** [🇨🇳 中文](README.md) | [🇮🇷 فارسی](فارسی.md)
6 |
7 |
8 |
9 |
10 |
11 | **پشتیبانی چند پروتکل · مسیر سفارشی · بهبود اختلاط کد**
12 |
13 | [](https://t.me/+ft-zI76oovgwNmRh)
14 | []()
15 | []()
16 |
17 |
18 |
19 | ## ✨ ویژگیهای اصلی v2.1
20 |
21 | - 🎭 **پشتیبانی چند پروتکل** - VLESS + Trojan + xhttp، تعویض آزاد
22 | - 🛤️ **مسیر سفارشی** - خداحافظی با مسیر UUID، آدرس دسترسی سفارشی، پشتیبانی از مسیرهای چند سطحی
23 | - 🔐 **اختلاط کد** - رمزگذاری کلمات کلیدی، پنهانسازی ویژگیها، افزایش امنیت
24 | - 🔄 **تبدیل اشتراک** - سرویس تبدیل سفارشی، کنترل دقیق نوع ترجیحی
25 | - 🎯 **مدیریت گرافیکی** - ذخیرهسازی KV، پیکربندی بلادرنگ، بدون نیاز به استقرار مجدد
26 | - 🚀 **مدیریت API** - RESTful API، عملیات دستهای، بهروزرسانی پویا
27 | - 📱 **پشتیبانی چند کلاینت** - CLASH، SURGE، SING-BOX، LOON، QUANTUMULT X، V2RAY، Shadowrocket، STASH، NEKORAY، V2RAYNG
28 | - 🚀 **بیدار کردن برنامه** - با کلیک روی دکمه به طور خودکار برنامه کلاینت مربوطه را بیدار میکند
29 | - 🔍 **تشخیص خودکار** - بر اساس User-Agent به طور خودکار تشخیص داده و قالب مربوطه را برمیگرداند
30 |
31 | ---
32 |
33 | ## 🆕 محتوای بهروزرسانی v2.1
34 |
35 | - ✨ اضافه شدن پشتیبانی از کلاینتهای Shadowrocket، STASH، NEKORAY، V2RAYNG
36 | - ✨ اضافه شدن عملکرد بیدار کردن خودکار برنامه، باز کردن کلاینت با یک کلیک
37 | - ✨ اضافه شدن عملکرد تشخیص خودکار، بازگشت هوشمند قالب کلاینت
38 | - ✨ بهینهسازی مسیر سفارشی، پشتیبانی از مسیرهای چند سطحی (مثل `/path/to/sub`)
39 | - ✨ تکمیل پیکربندی ALPN، تمام لینکهای TLS به طور خودکار شامل `h3,h2,http/1.1` میشوند
40 | - ✨ اضافه شدن دکمه ذخیره مستقل پیکربندی پروتکل، مدیریت پیکربندی راحتتر
41 | - 🔧 رفع منطق پردازش مسیر، تکمیل خودکار اسلش ابتدایی
42 |
43 | ---
44 |
45 | ### ابزارهای همراه
46 |
47 | | نوع | توضیحات | لینک |
48 | | :--- | :--- | :--- |
49 | | **ابزار ترجیحی** | بر اساس محیط شبکه خود مناسبترین IP را انتخاب کنید | https://github.com/byJoey/yx-tools/releases |
50 | | **آموزش متنی** | مقاله وبلاگ با توضیحات دقیق استقرار و استفاده | [https://joeyblog.net/yuanchuang/1146.html](https://joeyblog.net/yuanchuang/1146.html) |
51 | | **آموزش ویدیویی Workers** | نمایش عملی و توضیح عملکرد | https://www.youtube.com/watch?v=aYzTr8FafN4 |
52 | | **آموزش ویدیویی Pages** | نمایش عملی و توضیح عملکرد | https://www.youtube.com/watch?v=JhVxJChDL-E |
53 | | **آموزش ویدیویی Snippets** | نمایش عملی و توضیح عملکرد | https://www.youtube.com/watch?v=xeFeH3Akcu8 |
54 |
55 | ### استقرار
56 |
57 | اضافه شدن انتخاب خودکار اشتراک هر 15 دقیقه یکبار که هزاران بار درخواست شده بود
58 |
59 | #### 🔧 پیکربندی پایه
60 | | نام متغیر | مقدار | توضیحات |
61 | | :--- | :--- | :--- |
62 | | `u` | `UUID شما` | **الزامی**. برای دسترسی به اشتراک و رابط مدیریت پیکربندی |
63 | | `p` | `proxyip` | **اختیاری**. آدرس و پورت ProxyIP سفارشی |
64 | | `s` | `آدرس SOCKS5 شما` | **اختیاری**. برای ارسال تمام ترافیک خروجی از طریق پروکسی SOCKS5، فرمت: `user:pass@host:port` یا `host:port` |
65 | | `d` | `مسیر سفارشی` | **اختیاری**. مسیر دسترسی اشتراک سفارشی، پشتیبانی از مسیرهای چند سطحی، مثل `/mypath` یا `/path/to/sub`، اگر خالی بگذارید از مسیر UUID استفاده میشود. اگر مسیر با `/` شروع نشود، به طور خودکار اضافه میشود |
66 | | `wk` | `کد منطقه` | **اختیاری**. تعیین دستی منطقه Worker، مثل: `SG`، `HK`، `US`، `JP` و غیره |
67 |
68 | #### 🎭 پیکربندی پروتکل (جدید)
69 | | نام متغیر | مقدار | توضیحات |
70 | | :--- | :--- | :--- |
71 | | `ev` | `yes/no` | **اختیاری**. فعالسازی پروتکل VLESS (به طور پیشفرض فعال است) |
72 | | `et` | `yes/no` | **اختیاری**. فعالسازی پروتکل Trojan (به طور پیشفرض غیرفعال است) |
73 | | `ex` | `yes/no` | **اختیاری**. فعالسازی پروتکل xhttp (به طور پیشفرض غیرفعال است) |
74 | | `tp` | `رمز عبور سفارشی` | **اختیاری**. رمز عبور پروتکل Trojan، اگر خالی بگذارید از UUID استفاده میشود |
75 |
76 | #### 🎯 پیکربندی گرافیکی (توصیه میشود)
77 | - **پیکربندی ذخیرهسازی KV**: در Workers یک فضای نام KV ایجاد کنید، متغیر محیطی `C` را متصل کنید
78 | - **دسترسی به رابط**: پس از استقرار به `/{UUID شما}` دسترسی پیدا کنید تا از مدیریت پیکربندی گرافیکی استفاده کنید
79 | - **اعمال بلادرنگ**: تغییر پیکربندی از طریق رابط بدون نیاز به استقرار مجدد، بلافاصله اعمال میشود
80 |
81 | #### 🔧 کنترل پیشرفته
82 | | نام متغیر | مقدار | توضیحات |
83 | | :--- | :--- | :--- |
84 | | `yx` | `IP/دامنه ترجیحی سفارشی` | **اختیاری**. پشتیبانی از نامگذاری گره، فرمت: `1.1.1.1:443#گره هنگکنگ,8.8.8.8:53#Google DNS` |
85 | | `yxURL` | `URL منبع IP ترجیحی` | **اختیاری**. URL منبع لیست IP ترجیحی سفارشی، اگر خالی بگذارید از آدرس پیشفرض استفاده میشود |
86 | | `scu` | `آدرس تبدیل اشتراک` | **اختیاری**. URL سرویس تبدیل اشتراک سفارشی، پیشفرض: `https://url.v1.mk/sub` |
87 | | `epd` | `yes/no` | **اختیاری**. فعالسازی دامنه ترجیحی (به طور پیشفرض فعال است) |
88 | | `epi` | `yes/no` | **اختیاری**. فعالسازی IP ترجیحی (به طور پیشفرض فعال است) |
89 | | `egi` | `yes/no` | **اختیاری**. فعالسازی ترجیح پیشفرض GitHub (به طور پیشفرض فعال است) |
90 | | `qj` | `no` | **اختیاری**. کنترل کاهش سطح، وقتی `no` تنظیم شود حالت کاهش سطح فعال میشود: اتصال مستقیم CF ناموفق → اتصال SOCKS5 → آدرس fallback |
91 | | `dkby` | `yes` | **اختیاری**. کنترل TLS، وقتی `yes` تنظیم شود فقط گرههای TLS تولید میشوند، گرههای غیرTLS تولید نمیشوند (مثل پورت 80) |
92 | | `yxby` | `yes` | **اختیاری**. کنترل ترجیحی، وقتی `yes` تنظیم شود تمام عملکردهای ترجیحی خاموش میشوند، فقط از آدرس اصلی استفاده میشود، گرههای IP و دامنه ترجیحی تولید نمیشوند |
93 | | `rm` | `no` | **اختیاری**. کنترل تطبیق منطقه، وقتی `no` تنظیم شود تطبیق هوشمند منطقه خاموش میشود |
94 | | `ae` | `yes` | **اختیاری**. کلید مدیریت API، وقتی `yes` تنظیم شود اجازه مدیریت پویای IP ترجیحی از طریق API داده میشود (به طور پیشفرض خاموش است) |
95 |
96 | #### 📦 تنظیمات ذخیرهسازی KV (اختیاری اما توصیه میشود)
97 | 1. در Cloudflare Workers یک فضای نام KV ایجاد کنید
98 | 2. در تنظیمات Workers فضای نام KV را متصل کنید، نام متغیر را `C` تنظیم کنید
99 | 3. Workers را دوباره استقرار دهید
100 | 4. به `/{UUID شما}` دسترسی پیدا کنید تا از مدیریت پیکربندی گرافیکی استفاده کنید
101 |
102 | #### 🔑 شروع سریع API
103 | 1. https://github.com/byJoey/yx-tools/releases نرمافزار ترجیحی
104 | 2. **فعالسازی عملکرد API**: به `/{UUID}` یا `/{مسیر سفارشی}` دسترسی پیدا کنید → "اجازه مدیریت API" را پیدا کنید → "فعالسازی مدیریت API" را انتخاب کنید → ذخیره
105 | 3. **افزودن IP تک**:
106 | ```bash
107 | # استفاده از مسیر UUID
108 | curl -X POST "https://your-worker.workers.dev/{UUID}/api/preferred-ips" \
109 | -H "Content-Type: application/json" \
110 | -d '{"ip": "1.2.3.4", "port": 443, "name": "گره هنگکنگ"}'
111 |
112 | # استفاده از مسیر سفارشی (اگر متغیر d تنظیم شده باشد)
113 | curl -X POST "https://your-worker.workers.dev/{مسیر سفارشی}/api/preferred-ips" \
114 | -H "Content-Type: application/json" \
115 | -d '{"ip": "1.2.3.4", "port": 443, "name": "گره هنگکنگ"}'
116 | ```
117 | 4. **افزودن دستهای IP**:
118 | ```bash
119 | curl -X POST "https://your-worker.workers.dev/{UUID یا مسیر سفارشی}/api/preferred-ips" \
120 | -H "Content-Type: application/json" \
121 | -d '[
122 | {"ip": "1.2.3.4", "port": 443, "name": "گره 1"},
123 | {"ip": "5.6.7.8", "port": 8443, "name": "گره 2"}
124 | ]'
125 | ```
126 | 5. **پاک کردن یک کلیکی**:
127 | ```bash
128 | curl -X DELETE "https://your-worker.workers.dev/{UUID یا مسیر سفارشی}/api/preferred-ips" \
129 | -H "Content-Type: application/json" \
130 | -d '{"all": true}'
131 | ```
132 |
133 | ### عملکرد جدید
134 |
135 | #### 🎭 پشتیبانی چند پروتکل (تقویت شده در v2.1)
136 | - **پروتکل VLESS**: به طور پیشفرض فعال، پشتیبانی کامل VLESS
137 | - **پروتکل Trojan**: پشتیبانی از Trojan-WS-TLS، رمز عبور سفارشی یا استفاده از UUID
138 | - **پروتکل xhttp**: پروتکل استتار مبتنی بر HTTP POST
139 | - **استفاده ترکیبی پروتکل**: پشتیبانی از فعالسازی همزمان چند پروتکل، کلاینت به طور خودکار تشخیص میدهد
140 | - **تعویض پروتکل**: فعال/غیرفعال کردن پروتکل با یک کلیک در رابط گرافیکی
141 | - **ذخیره مستقل**: منطقه پیکربندی پروتکل دکمه ذخیره مستقل جدید اضافه شده، راحتتر و سریعتر (جدید در v2.1)
142 | - **مدیریت رمز عبور**: Trojan از رمز عبور سفارشی پشتیبانی میکند، اگر خالی بگذارید به طور خودکار از UUID استفاده میشود
143 |
144 | #### 🛤️ مسیر سفارشی (متغیر d) (تقویت شده در v2.1)
145 | - **سفارشیسازی مسیر**: دیگر از UUID به عنوان مسیر استفاده نمیشود، نام مسیر مورد علاقه خود را تنظیم کنید
146 | - **پشتیبانی از مسیرهای چند سطحی**: پشتیبانی از مسیرهای چند سطحی، مثل `/path/to/sub`، سازماندهی انعطافپذیر ساختار دسترسی (جدید در v2.1)
147 | - **تکمیل خودکار**: اگر مسیر با `/` شروع نشود، سیستم به طور خودکار اضافه میکند (جدید در v2.1)
148 | - **افزایش امنیت**: پس از مسیر سفارشی مسیر UUID به طور خودکار غیرفعال میشود، افزایش پنهانکاری
149 | - **دسترسی انعطافپذیر**: `https://worker.dev/mypath/sub` یا `https://worker.dev/path/to/sub/sub` سادهتر و راحتتر برای یادآوری
150 | - **تعویض پویا**: از طریق رابط گرافیکی هر زمان مسیر دسترسی را تغییر دهید
151 | - **نمایش وضعیت**: صفحه پیکربندی وضعیت نوع مسیر فعلی را به صورت بلادرنگ نمایش میدهد
152 |
153 | #### 🎯 مدیریت پیکربندی گرافیکی
154 | - **پشتیبانی ذخیرهسازی KV**: استفاده از ذخیرهسازی Cloudflare KV برای پیکربندی پایدار
155 | - **رابط گرافیکی**: به `/{UUID شما}` یا `/{مسیر سفارشی}` دسترسی پیدا کنید تا از رابط مدیریت پیکربندی استفاده کنید
156 | - **پیکربندی بلادرنگ**: بدون نیاز به استقرار مجدد، پیکربندی بلافاصله اعمال میشود
157 | - **اولویت پیکربندی**: پیکربندی KV > متغیر محیطی > مقدار پیشفرض
158 |
159 | #### 🔄 کنترل تبدیل اشتراک (جدید در v2.0)
160 | - **سرویس تبدیل سفارشی**: پشتیبانی از پیکربندی URL تبدیل اشتراک سفارشی
161 | - **کنترل نوع ترجیحی**: کنترل دقیق دامنه ترجیحی، IP ترجیحی، ترجیح GitHub
162 | - **کلید مستقل**: هر نوع ترجیحی میتواند به طور جداگانه فعال/غیرفعال شود
163 | - **تنظیمات پیشفرض**: دامنه ترجیحی داخلی، IP ترجیحی، ترجیح پیشفرض GitHub همه فعال هستند
164 | - **بهروزرسانی بلادرنگ**: پس از تغییر پیکربندی بلافاصله اعمال میشود، بدون نیاز به استقرار مجدد
165 |
166 | #### 🔐 بهبود اختلاط کد (جدید در v2.0)
167 | - **رمزگذاری کلمات کلیدی**: کلمات کلیدی حساس مثل websocket، clash، v2ray با رمزگذاری Base64
168 | - **اختلاط نام متغیر**: تمام نامهای متغیر پیکربندی به طور یکنواخت از نامهای کوتاه استفاده میکنند (ev/et/ex/tp/scu و غیره)
169 | - **پاکسازی دیباگ**: حذف تمام دستورات دیباگ console
170 | - **پنهانسازی ویژگیها**: کاهش 95%+ خطر تطبیق کلمات کلیدی
171 | - **دوستدار کاربر**: رابط HTML واضح و قابل خواندن باقی میماند
172 | - **ابزار خودکار**: اسکریپت اختلاط ارائه میشود، برای نگهداری بعدی راحت است
173 |
174 | #### 🚀 مدیریت پویای API
175 | - **مدیریت API**: مدیریت پویای IP ترجیحی از طریق RESTful API، بدون نیاز به تغییر کد
176 | - **گزارش دستهای**: پشتیبانی از افزودن دستهای چند IP ترجیحی
177 | - **پاک کردن یک کلیکی**: پشتیبانی از پاک کردن تمام IP ترجیحی، بهروزرسانی سریع لیست
178 | - **کلید امنیتی**: به طور پیشفرض خاموش است، باید عملکرد API را به صورت دستی در رابط گرافیکی فعال کنید
179 | - **ادغام خودکار**: IP های اضافه شده از طریق API با متغیر yx پیکربندی دستی به طور خودکار ادغام میشوند
180 | - **همگامسازی بلادرنگ**: IP های اضافه شده از طریق API بلافاصله در صفحه پیکربندی نمایش داده میشوند
181 | - **نقاط پایانی API**:
182 | - `GET /{UUID یا مسیر}/api/preferred-ips` - پرسوجوی لیست IP ترجیحی
183 | - `POST /{UUID یا مسیر}/api/preferred-ips` - افزودن IP ترجیحی (پشتیبانی از تک/دستهای)
184 | - `DELETE /{UUID یا مسیر}/api/preferred-ips` - حذف IP ترجیحی (پشتیبانی از تک/همه)
185 |
186 |
187 | #### 🌍 تعیین دستی منطقه
188 | - **انتخاب منطقه**: پشتیبانی از تعیین دستی منطقه Worker، بازنویسی تشخیص خودکار
189 | - **روش تنظیم**: `wk=SG` یا از طریق رابط گرافیکی انتخاب کنید
190 | - **مناطق پشتیبانی شده**: US، SG، JP، HK، KR، DE، SE، NL، FI، GB
191 | - **نمایش هوشمند**: وضعیت سیستم "تعیین دستی منطقه" را نمایش میدهد نه "تشخیص خودکار"
192 |
193 | #### 🏷️ نامگذاری گره ترجیحی
194 | - **نام سفارشی**: پشتیبانی از تنظیم نام سفارشی برای گره ترجیحی
195 | - **پشتیبانی فرمت**: `IP:پورت#نام گره` یا `IP:پورت` (استفاده از نام پیشفرض)
196 | - **مثال**: `1.1.1.1:443#گره هنگکنگ,8.8.8.8:53#Google DNS`
197 | - **فرمت پیشفرض**: وقتی نام تنظیم نشود به طور خودکار `ترجیح سفارشی-IP:پورت` تولید میشود
198 |
199 | #### 📊 نظارت بر وضعیت سیستم
200 | - **تشخیص بلادرنگ**: نمایش منطقه Worker، روش تشخیص، وضعیت ProxyIP
201 | - **تطبیق هوشمند**: منطق انتخاب هممنطقه → منطقه مجاور → سایر مناطق
202 | - **نمایش وضعیت**: نمایش بصری وضعیت اجرای سیستم و اطلاعات پیکربندی
203 |
204 | #### 🔧 گزینههای کنترل پیشرفته
205 | - **کنترل تطبیق منطقه**: `rm=no` خاموش کردن تطبیق هوشمند منطقه
206 | - **کنترل کاهش سطح**: `qj=no` فعالسازی حالت کاهش سطح (اتصال مستقیم CF ناموفق → SOCKS5 → fallback)
207 | - **کنترل TLS**: `dkby=yes` فقط تولید گرههای TLS، تولید گرههای غیرTLS نمیشود
208 | - **کنترل ترجیحی**: `yxby=yes` خاموش کردن تمام عملکردهای ترجیحی
209 |
210 | #### 🎨 پشتیبانی چند کلاینت (تقویت شده در v2.1)
211 | - **10 نوع کلاینت**: پشتیبانی از CLASH، SURGE، SING-BOX، LOON، QUANTUMULT X، V2RAY، Shadowrocket، STASH، NEKORAY، V2RAYNG (4 کلاینت جدید در v2.1)
212 | - **تبدیل خودکار**: بر اساس نوع کلاینت به طور خودکار پیکربندی مربوطه تولید میشود
213 | - **دریافت یک کلیکی**: تولید یک کلیکی لینک اشتراک در رابط گرافیکی
214 | - **بیدار کردن برنامه**: کلیک روی دکمه کلاینت به طور خودکار برنامه مربوطه را بیدار میکند، بدون نیاز به کپی دستی لینک (جدید در v2.1)
215 | - **تشخیص خودکار**: بر اساس User-Agent به طور خودکار نوع کلاینت را تشخیص داده و قالب مربوطه را برمیگرداند (جدید در v2.1)
216 | - **سازگاری پروتکل**: کلاینتهای مختلف به طور خودکار با بهترین ترکیب پروتکل سازگار میشوند
217 | - **پشتیبانی ALPN**: تمام لینکهای TLS به طور خودکار شامل مذاکره پروتکل `h3,h2,http/1.1` میشوند (جدید در v2.1)
218 |
219 | #### ⚡ بهینهسازی عملکرد
220 | - **انتخاب هوشمند**: هر 15 دقیقه یکبار به طور خودکار انتخاب میشود، حفظ بهترین عملکرد
221 | - **مکانیسم تحمل خطا**: چندین طرح پشتیبان، تضمین ثبات سرویس
222 | - **بهینهسازی کش**: مکانیسم کش هوشمند، کاهش محاسبات تکراری
223 |
224 | ### تشکر
225 |
226 | * این پروژه بر اساس [zizifn/edgetunnel](https://github.com/zizifn/edgetunnel) اصلاح شده است، از مشارکت نویسنده اصلی سپاسگزاریم.
227 | * ProxyIP داخلی این پروژه از CM [[cmliu](https://github.com/cmliu)) است، از مشارکت نویسنده سپاسگزاریم.
228 | * IP پروکسی معکوس این پروژه از kejiland جلوی [[qwer-search](https://github.com/qwer-search)) است، از مشارکت نویسنده سپاسگزاریم.
229 | ## تاریخچه ستاره
230 |
231 | [](https://www.star-history.com/#byJoey/cfnew&Timeline&LogScale)
232 |
233 |
234 |
--------------------------------------------------------------------------------
/snippets:
--------------------------------------------------------------------------------
1 | import { connect } from 'cloudflare:sockets';
2 |
3 | // --- 硬编码配置 ---
4 | const authToken = 'f64bdc57-0f54-4705-bf75-cfd646d98c06';
5 | let fallbackAddress = '';
6 | const socks5Config = '';
7 | // 手动指定地区(留空则自动检测,可选值:US、SG、JP、HK、KR、DE、SE、NL、FI、GB)
8 | const manualWorkerRegion = '';
9 | // D短地址(自定义路径,留空则使用UUID路径,支持多级路径如:mypath 或 path/to/sub)
10 | const customPath = '';
11 | // GitHub订阅URL(硬编码)
12 | const githubPreferredURL = 'https://raw.githubusercontent.com/qwer-search/bestip/refs/heads/main/kejilandbestip.txt';
13 | // 启用GitHub优选IP(true启用,false禁用)
14 | const enableGitHubPreferred = true;
15 | // 启用其他优选(域名优选,true启用,false禁用)
16 | const enableOtherPreferred = true;
17 | // API地址配置(订阅转换服务)
18 | const apiBaseUrl = 'https://url.v1.mk/sub';
19 |
20 | const directDomains = [
21 | { name: "cloudflare.182682.xyz", domain: "cloudflare.182682.xyz" },
22 | { name: "speed.marisalnc.com", domain: "speed.marisalnc.com" },
23 | { domain: "freeyx.cloudflare88.eu.org" }, { domain: "bestcf.top" },
24 | { domain: "cdn.2020111.xyz" }, { domain: "cfip.cfcdn.vip" },
25 | { domain: "cf.0sm.com" }, { domain: "cf.090227.xyz" },
26 | { domain: "cf.zhetengsha.eu.org" }, { domain: "cloudflare.9jy.cc" },
27 | { domain: "cf.zerone-cdn.pp.ua" }, { domain: "cfip.1323123.xyz" },
28 | { domain: "cnamefuckxxs.yuchen.icu" }, { domain: "cloudflare-ip.mofashi.ltd" },
29 | { domain: "115155.xyz" }, { domain: "cname.xirancdn.us" },
30 | { domain: "f3058171cad.002404.xyz" }, { domain: "8.889288.xyz" },
31 | { domain: "cdn.tzpro.xyz" }, { domain: "cf.877771.xyz" },
32 | { domain: "xn--b6gac.eu.org" }
33 | ];
34 |
35 | const parsedSocks5Config = {};
36 | const isSocksEnabled = false;
37 |
38 | let enableRegionMatching = true;
39 | let currentWorkerRegion = '';
40 |
41 | const backupIPs = [
42 | { domain: 'ProxyIP.US.CMLiussss.net', region: 'US', regionCode: 'US', port: 443 },
43 | { domain: 'ProxyIP.SG.CMLiussss.net', region: 'SG', regionCode: 'SG', port: 443 },
44 | { domain: 'ProxyIP.JP.CMLiussss.net', region: 'JP', regionCode: 'JP', port: 443 },
45 | { domain: 'ProxyIP.HK.CMLiussss.net', region: 'HK', regionCode: 'HK', port: 443 },
46 | { domain: 'ProxyIP.KR.CMLiussss.net', region: 'KR', regionCode: 'KR', port: 443 },
47 | { domain: 'ProxyIP.DE.CMLiussss.net', region: 'DE', regionCode: 'DE', port: 443 },
48 | { domain: 'ProxyIP.SE.CMLiussss.net', region: 'SE', regionCode: 'SE', port: 443 },
49 | { domain: 'ProxyIP.NL.CMLiussss.net', region: 'NL', regionCode: 'NL', port: 443 },
50 | { domain: 'ProxyIP.FI.CMLiussss.net', region: 'FI', regionCode: 'FI', port: 443 },
51 | { domain: 'ProxyIP.GB.CMLiussss.net', region: 'GB', regionCode: 'GB', port: 443 }
52 | ];
53 |
54 | const E_INVALID_DATA = atob('aW52YWxpZCBkYXRh');
55 | const E_INVALID_USER = atob('aW52YWxpZCB1c2Vy');
56 | const E_UNSUPPORTED_CMD = atob('Y29tbWFuZCBpcyBub3Qgc3VwcG9ydGVk');
57 | const E_UDP_DNS_ONLY = atob('VURQIHByb3h5IG9ubHkgZW5hYmxlIGZvciBETlMgd2hpY2ggaXMgcG9ydCA1Mw==');
58 | const E_INVALID_ADDR_TYPE = atob('aW52YWxpZCBhZGRyZXNzVHlwZQ==');
59 | const E_EMPTY_ADDR = atob('YWRkcmVzc1ZhbHVlIGlzIGVtcHR5');
60 | const E_WS_NOT_OPEN = atob('d2ViU29ja2V0LmVhZHlTdGF0ZSBpcyBub3Qgb3Blbg==');
61 | const E_INVALID_ID_STR = atob('U3RyaW5naWZpZWQgaWRlbnRpZmllciBpcyBpbnZhbGlk');
62 | const E_INVALID_SOCKS_ADDR = atob('SW52YWxpZCBTT0NLUyBhZGRyZXNzIGZvcm1hdA==');
63 | const E_SOCKS_NO_METHOD = atob('bm8gYWNjZXB0YWJsZSBtZXRob2Rz');
64 | const E_SOCKS_AUTH_NEEDED = atob('c29ja3Mgc2VydmVyIG5lZWRzIGF1dGg=');
65 | const E_SOCKS_AUTH_FAIL = atob('ZmFpbCB0byBhdXRoIHNvY2tzIHNlcnZlcg==');
66 | const E_SOCKS_CONN_FAIL = atob('ZmFpbCB0byBvcGVuIHNvY2tzIGNvbm5lY3Rpb24=');
67 |
68 | const ADDRESS_TYPE_IPV4 = 1;
69 | const ADDRESS_TYPE_URL = 2;
70 | const ADDRESS_TYPE_IPV6 = 3;
71 |
72 | async function detectWorkerRegion(request) {
73 | try {
74 | const cfCountry = request.cf?.country;
75 | if (cfCountry) {
76 | const countryToRegion = {
77 | 'US': 'US', 'SG': 'SG', 'JP': 'JP', 'HK': 'HK', 'KR': 'KR',
78 | 'DE': 'DE', 'SE': 'SE', 'NL': 'NL', 'FI': 'FI', 'GB': 'GB',
79 | 'CN': 'HK', 'TW': 'HK', 'AU': 'SG', 'CA': 'US',
80 | 'FR': 'DE', 'IT': 'DE', 'ES': 'DE', 'CH': 'DE',
81 | 'AT': 'DE', 'BE': 'NL', 'DK': 'SE', 'NO': 'SE', 'IE': 'GB'
82 | };
83 | if (countryToRegion[cfCountry]) return countryToRegion[cfCountry];
84 | }
85 | return 'HK';
86 | } catch (error) {
87 | return 'HK';
88 | }
89 | }
90 |
91 | function getNearbyRegions(region) {
92 | const nearbyMap = {
93 | 'US': ['SG', 'JP', 'HK', 'KR'],
94 | 'SG': ['JP', 'HK', 'KR', 'US'],
95 | 'JP': ['SG', 'HK', 'KR', 'US'],
96 | 'HK': ['SG', 'JP', 'KR', 'US'],
97 | 'KR': ['JP', 'HK', 'SG', 'US'],
98 | 'DE': ['NL', 'GB', 'SE', 'FI'],
99 | 'SE': ['DE', 'NL', 'FI', 'GB'],
100 | 'NL': ['DE', 'GB', 'SE', 'FI'],
101 | 'FI': ['SE', 'DE', 'NL', 'GB'],
102 | 'GB': ['DE', 'NL', 'SE', 'FI']
103 | };
104 | return nearbyMap[region] || [];
105 | }
106 |
107 | function getAllRegionsByPriority(region) {
108 | const nearbyRegions = getNearbyRegions(region);
109 | const allRegions = ['US', 'SG', 'JP', 'HK', 'KR', 'DE', 'SE', 'NL', 'FI', 'GB'];
110 | return [region, ...nearbyRegions, ...allRegions.filter(r => r !== region && !nearbyRegions.includes(r))];
111 | }
112 |
113 | function getSmartRegionSelection(workerRegion, availableIPs) {
114 | if (!enableRegionMatching || !workerRegion) return availableIPs;
115 | const priorityRegions = getAllRegionsByPriority(workerRegion);
116 | const sortedIPs = [];
117 | for (const region of priorityRegions) {
118 | const regionIPs = availableIPs.filter(ip => ip.regionCode === region);
119 | sortedIPs.push(...regionIPs);
120 | }
121 | return sortedIPs;
122 | }
123 |
124 | async function getBestBackupIP(workerRegion = '') {
125 | if (backupIPs.length === 0) return null;
126 | const availableIPs = backupIPs.map(ip => ({ ...ip, available: true }));
127 | if (enableRegionMatching && workerRegion) {
128 | const sortedIPs = getSmartRegionSelection(workerRegion, availableIPs);
129 | if (sortedIPs.length > 0) return sortedIPs[0];
130 | }
131 | return availableIPs[0];
132 | }
133 |
134 | function parseAddressAndPort(input) {
135 | if (!input) return { address: '', port: null };
136 | if (input.includes('[') && input.includes(']')) {
137 | const match = input.match(/^\[([^\]]+)\](?::(\d+))?$/);
138 | if (match) {
139 | return { address: match[1], port: match[2] ? parseInt(match[2], 10) : null };
140 | }
141 | }
142 | const lastColonIndex = input.lastIndexOf(':');
143 | if (lastColonIndex > 0) {
144 | const address = input.substring(0, lastColonIndex);
145 | const portStr = input.substring(lastColonIndex + 1);
146 | const port = parseInt(portStr, 10);
147 | if (!isNaN(port) && port > 0 && port <= 65535) return { address, port };
148 | }
149 | return { address: input, port: null };
150 | }
151 |
152 | export default {
153 | async fetch(request, env, ctx) {
154 | try {
155 | const url = new URL(request.url);
156 |
157 | if (manualWorkerRegion && manualWorkerRegion.trim()) {
158 | currentWorkerRegion = manualWorkerRegion.trim().toUpperCase();
159 | } else {
160 | currentWorkerRegion = await detectWorkerRegion(request);
161 | }
162 |
163 | let currentFallbackAddress = fallbackAddress;
164 | if (!currentFallbackAddress && currentWorkerRegion) {
165 | const bestBackupIP = await getBestBackupIP(currentWorkerRegion);
166 | if (bestBackupIP) currentFallbackAddress = bestBackupIP.domain + ':' + bestBackupIP.port;
167 | }
168 |
169 | if (request.headers.get('Upgrade') === 'websocket') {
170 | return await handleWsRequest(request, currentFallbackAddress);
171 | } else if (request.method === 'GET') {
172 | if (url.pathname === '/') {
173 | const successHtml = `服务正常`;
174 | return new Response(successHtml, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
175 | }
176 |
177 | if (customPath && customPath.trim()) {
178 | const cleanCustomPath = customPath.trim().startsWith('/') ? customPath.trim() : '/' + customPath.trim();
179 | const normalizedCustomPath = cleanCustomPath.endsWith('/') && cleanCustomPath.length > 1 ? cleanCustomPath.slice(0, -1) : cleanCustomPath;
180 | const normalizedPath = url.pathname.endsWith('/') && url.pathname.length > 1 ? url.pathname.slice(0, -1) : url.pathname;
181 |
182 | if (normalizedPath === normalizedCustomPath) {
183 | return await handleSubscriptionPage(request, authToken);
184 | }
185 |
186 | if (normalizedPath === normalizedCustomPath + '/sub') {
187 | return await handleSubscriptionRequest(request, authToken, url);
188 | }
189 |
190 | if (url.pathname.length > 1 && url.pathname !== '/') {
191 | const user = url.pathname.replace(/\/$/, '').replace('/sub', '').substring(1);
192 | if (isValidFormat(user)) {
193 | return new Response(JSON.stringify({
194 | error: '访问被拒绝',
195 | message: '当前 Worker 已启用自定义路径模式,UUID 访问已禁用'
196 | }), {
197 | status: 403,
198 | headers: { 'Content-Type': 'application/json; charset=utf-8' }
199 | });
200 | }
201 | }
202 | } else {
203 | if (url.pathname.length > 1 && url.pathname !== '/' && !url.pathname.includes('/sub')) {
204 | const uuid = url.pathname.replace(/\/$/, '').substring(1);
205 | if (isValidFormat(uuid)) {
206 | if (uuid === authToken) return await handleSubscriptionPage(request, uuid);
207 | return new Response('UUID错误', { status: 403 });
208 | }
209 | }
210 |
211 | if (url.pathname.includes('/sub')) {
212 | const pathParts = url.pathname.split('/');
213 | if (pathParts.length === 2 && pathParts[1] === 'sub') {
214 | const uuid = pathParts[0].substring(1);
215 | if (isValidFormat(uuid)) {
216 | if (uuid === authToken) return await handleSubscriptionRequest(request, uuid, url);
217 | return new Response('UUID错误', { status: 403 });
218 | }
219 | }
220 | }
221 |
222 | if (url.pathname.toLowerCase().includes(`/${authToken}`)) {
223 | return await handleSubscriptionRequest(request, authToken);
224 | }
225 | }
226 | }
227 | return new Response('Not Found', { status: 404 });
228 | } catch (err) {
229 | return new Response(err.toString(), { status: 500 });
230 | }
231 | },
232 | };
233 |
234 | async function handleSubscriptionPage(request, uuid = null) {
235 | if (!uuid) uuid = authToken;
236 |
237 | const pageHtml = `
238 |
239 |
240 |
241 |
242 | 订阅中心
243 |
268 |
269 |
270 |
271 |
272 | 代理订阅中心精简版 v2.1
273 |
274 |
278 |
279 |
[ 选择客户端 ]
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
[ 快速获取 ]
296 |
297 |
298 |
299 |
300 |
[ 相关链接 ]
301 |
305 |
306 |
307 |
413 |
414 | `;
415 |
416 | return new Response(pageHtml, {
417 | status: 200,
418 | headers: { 'Content-Type': 'text/html; charset=utf-8' }
419 | });
420 | }
421 |
422 | async function handleSubscriptionRequest(request, uuid, url = null) {
423 | if (!url) url = new URL(request.url);
424 |
425 | const finalLinks = [];
426 | const workerDomain = url.hostname;
427 |
428 | const nativeList = [{ ip: workerDomain, isp: '原生地址' }];
429 | finalLinks.push(...generateLinksFromSource(nativeList, uuid, workerDomain));
430 |
431 | if (enableOtherPreferred) {
432 | const domainList = directDomains.map(d => ({ ip: d.domain, isp: d.name || d.domain }));
433 | finalLinks.push(...generateLinksFromSource(domainList, uuid, workerDomain));
434 | }
435 |
436 | if (enableGitHubPreferred) {
437 | const newIPList = await fetchAndParseNewIPs();
438 | if (newIPList.length > 0) finalLinks.push(...generateLinksFromNewIPs(newIPList, uuid, workerDomain));
439 | }
440 |
441 | if (finalLinks.length === 0) {
442 | const errorRemark = "所有节点获取失败";
443 | const errorLink = `vless://00000000-0000-0000-0000-000000000000@127.0.0.1:80?encryption=none&security=none&type=ws&host=error.com&path=%2F#${encodeURIComponent(errorRemark)}`;
444 | finalLinks.push(errorLink);
445 | }
446 |
447 | const subscriptionContent = btoa(finalLinks.join('\n'));
448 |
449 | return new Response(subscriptionContent, {
450 | headers: {
451 | 'Content-Type': 'text/plain; charset=utf-8',
452 | 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
453 | },
454 | });
455 | }
456 |
457 | function generateLinksFromSource(list, uuid, workerDomain) {
458 | const httpsPorts = [443];
459 | const links = [];
460 | const wsPath = '/?ed=2048';
461 | const proto = 'vless';
462 |
463 | list.forEach(item => {
464 | const nodeNameBase = item.isp.replace(/\s/g, '_');
465 | const safeIP = item.ip.includes(':') ? `[${item.ip}]` : item.ip;
466 |
467 | httpsPorts.forEach(port => {
468 | const wsNodeName = `${nodeNameBase}-${port}-WS-TLS`;
469 | const wsParams = new URLSearchParams({
470 | encryption: 'none',
471 | security: 'tls',
472 | sni: workerDomain,
473 | fp: 'randomized',
474 | type: 'ws',
475 | host: workerDomain,
476 | path: wsPath
477 | });
478 | links.push(`${proto}://${uuid}@${safeIP}:${port}?${wsParams.toString()}#${encodeURIComponent(wsNodeName)}`);
479 | });
480 | });
481 |
482 | return links;
483 | }
484 |
485 | async function fetchAndParseNewIPs() {
486 | const url = githubPreferredURL;
487 | try {
488 | const response = await fetch(url);
489 | if (!response.ok) return [];
490 | const text = await response.text();
491 | const results = [];
492 | const lines = text.trim().replace(/\r/g, "").split('\n');
493 | const regex = /^([^:]+):(\d+)#(.*)$/;
494 |
495 | for (const line of lines) {
496 | const trimmedLine = line.trim();
497 | if (!trimmedLine) continue;
498 | const match = trimmedLine.match(regex);
499 | if (match) {
500 | results.push({
501 | ip: match[1],
502 | port: parseInt(match[2], 10),
503 | name: match[3].trim() || match[1]
504 | });
505 | }
506 | }
507 | return results;
508 | } catch (error) {
509 | return [];
510 | }
511 | }
512 |
513 | function generateLinksFromNewIPs(list, uuid, workerDomain) {
514 | const links = [];
515 | const wsPath = '/?ed=2048';
516 | const proto = 'vless';
517 |
518 | list.forEach(item => {
519 | const nodeName = item.name;
520 | const safeIP = item.ip.includes(':') ? `[${item.ip}]` : item.ip;
521 | const params = {
522 | encryption: 'none',
523 | security: 'tls',
524 | sni: workerDomain,
525 | fp: 'randomized',
526 | type: 'ws',
527 | host: workerDomain,
528 | path: wsPath
529 | };
530 | const wsParams = new URLSearchParams(params);
531 | links.push(`${proto}://${uuid}@${safeIP}:${item.port}?${wsParams.toString()}#${encodeURIComponent(nodeName)}`);
532 | });
533 |
534 | return links;
535 | }
536 |
537 | async function handleWsRequest(request, currentFallbackAddress = null) {
538 | const wsPair = new WebSocketPair();
539 | const [clientSock, serverSock] = Object.values(wsPair);
540 | serverSock.accept();
541 |
542 | let remoteConnWrapper = { socket: null };
543 | let isDnsQuery = false;
544 |
545 | const fbAddr = currentFallbackAddress || fallbackAddress;
546 |
547 | const earlyData = request.headers.get('sec-websocket-protocol') || '';
548 | const readable = makeReadableStream(serverSock, earlyData);
549 |
550 | readable.pipeTo(new WritableStream({
551 | async write(chunk) {
552 | if (isDnsQuery) return await forwardUDP(chunk, serverSock, null);
553 | if (remoteConnWrapper.socket) {
554 | const writer = remoteConnWrapper.socket.writable.getWriter();
555 | await writer.write(chunk);
556 | writer.releaseLock();
557 | return;
558 | }
559 | const { hasError, message, addressType, port, hostname, rawIndex, version, isUDP } = parseWsPacketHeader(chunk, authToken);
560 | if (hasError) throw new Error(message);
561 |
562 | if (isUDP) {
563 | if (port === 53) isDnsQuery = true;
564 | else throw new Error(E_UDP_DNS_ONLY);
565 | }
566 | const respHeader = new Uint8Array([version[0], 0]);
567 | const rawData = chunk.slice(rawIndex);
568 |
569 | if (isDnsQuery) return forwardUDP(rawData, serverSock, respHeader);
570 | await forwardTCP(addressType, hostname, port, rawData, serverSock, respHeader, remoteConnWrapper, fbAddr);
571 | },
572 | })).catch((err) => { console.log('WS Stream Error:', err); });
573 |
574 | return new Response(null, { status: 101, webSocket: clientSock });
575 | }
576 |
577 | async function forwardTCP(addrType, host, portNum, rawData, ws, respHeader, remoteConnWrapper, fbAddr = null) {
578 | async function connectAndSend(address, port) {
579 | const remoteSock = connect({ hostname: address, port: port });
580 | const writer = remoteSock.writable.getWriter();
581 | await writer.write(rawData);
582 | writer.releaseLock();
583 | return remoteSock;
584 | }
585 |
586 | async function retryConnection() {
587 | let fallbackHost, fallbackPort;
588 | if (fbAddr && fbAddr.trim()) {
589 | const parsed = parseAddressAndPort(fbAddr);
590 | fallbackHost = parsed.address;
591 | fallbackPort = parsed.port || portNum;
592 | } else if (fallbackAddress && fallbackAddress.trim()) {
593 | const parsed = parseAddressAndPort(fallbackAddress);
594 | fallbackHost = parsed.address;
595 | fallbackPort = parsed.port || portNum;
596 | } else {
597 | const bestBackupIP = await getBestBackupIP(currentWorkerRegion);
598 | fallbackHost = bestBackupIP ? bestBackupIP.domain : host;
599 | fallbackPort = bestBackupIP ? bestBackupIP.port : portNum;
600 | }
601 | const newSocket = await connectAndSend(fallbackHost || host, fallbackPort);
602 | remoteConnWrapper.socket = newSocket;
603 | newSocket.closed.catch(() => { }).finally(() => closeSocketQuietly(ws));
604 | connectStreams(newSocket, ws, respHeader, null);
605 | }
606 |
607 | try {
608 | const initialSocket = await connectAndSend(host, portNum);
609 | remoteConnWrapper.socket = initialSocket;
610 | connectStreams(initialSocket, ws, respHeader, retryConnection);
611 | } catch (err) {
612 | console.log('Initial connection failed, trying fallback:', err);
613 | retryConnection();
614 | }
615 | }
616 |
617 | function parseWsPacketHeader(chunk, token) {
618 | if (chunk.byteLength < 24) return { hasError: true, message: E_INVALID_DATA };
619 | const version = new Uint8Array(chunk.slice(0, 1));
620 | if (formatIdentifier(new Uint8Array(chunk.slice(1, 17))) !== token) return { hasError: true, message: E_INVALID_USER };
621 | const optLen = new Uint8Array(chunk.slice(17, 18))[0];
622 | const cmd = new Uint8Array(chunk.slice(18 + optLen, 19 + optLen))[0];
623 | let isUDP = false;
624 | if (cmd === 1) { }
625 | else if (cmd === 2) { isUDP = true; }
626 | else { return { hasError: true, message: E_UNSUPPORTED_CMD }; }
627 |
628 | const portIdx = 19 + optLen;
629 | const port = new DataView(chunk.slice(portIdx, portIdx + 2)).getUint16(0);
630 | let addrIdx = portIdx + 2, addrLen = 0, addrValIdx = addrIdx + 1, hostname = '';
631 | const addressType = new Uint8Array(chunk.slice(addrIdx, addrValIdx))[0];
632 |
633 | switch (addressType) {
634 | case ADDRESS_TYPE_IPV4:
635 | addrLen = 4;
636 | hostname = new Uint8Array(chunk.slice(addrValIdx, addrValIdx + addrLen)).join('.');
637 | break;
638 | case ADDRESS_TYPE_URL:
639 | addrLen = new Uint8Array(chunk.slice(addrValIdx, addrValIdx + 1))[0];
640 | addrValIdx += 1;
641 | hostname = new TextDecoder().decode(chunk.slice(addrValIdx, addrValIdx + addrLen));
642 | break;
643 | case ADDRESS_TYPE_IPV6:
644 | addrLen = 16;
645 | const ipv6 = [];
646 | const ipv6View = new DataView(chunk.slice(addrValIdx, addrValIdx + addrLen));
647 | for (let i = 0; i < 8; i++) ipv6.push(ipv6View.getUint16(i * 2).toString(16));
648 | hostname = ipv6.join(':');
649 | break;
650 | default:
651 | return { hasError: true, message: `${E_INVALID_ADDR_TYPE}: ${addressType}` };
652 | }
653 |
654 | if (!hostname) return { hasError: true, message: `${E_EMPTY_ADDR}: ${addressType}` };
655 | return { hasError: false, addressType, port, hostname, isUDP, rawIndex: addrValIdx + addrLen, version };
656 | }
657 |
658 | function makeReadableStream(socket, earlyDataHeader) {
659 | let cancelled = false;
660 | return new ReadableStream({
661 | start(controller) {
662 | socket.addEventListener('message', (event) => { if (!cancelled) controller.enqueue(event.data); });
663 | socket.addEventListener('close', () => { if (!cancelled) { closeSocketQuietly(socket); controller.close(); } });
664 | socket.addEventListener('error', (err) => controller.error(err));
665 | const { earlyData, error } = base64ToArray(earlyDataHeader);
666 | if (error) controller.error(error);
667 | else if (earlyData) controller.enqueue(earlyData);
668 | },
669 | cancel() { cancelled = true; closeSocketQuietly(socket); }
670 | });
671 | }
672 |
673 | async function connectStreams(remoteSocket, webSocket, headerData, retryFunc) {
674 | let header = headerData, hasData = false;
675 | await remoteSocket.readable.pipeTo(
676 | new WritableStream({
677 | async write(chunk, controller) {
678 | hasData = true;
679 | if (webSocket.readyState !== 1) controller.error(E_WS_NOT_OPEN);
680 | if (header) {
681 | webSocket.send(await new Blob([header, chunk]).arrayBuffer());
682 | header = null;
683 | } else {
684 | webSocket.send(chunk);
685 | }
686 | },
687 | abort(reason) { console.error("Readable aborted:", reason); },
688 | })
689 | ).catch((error) => { console.error("Stream connection error:", error); closeSocketQuietly(webSocket); });
690 |
691 | if (!hasData && retryFunc) retryFunc();
692 | }
693 |
694 | async function forwardUDP(udpChunk, webSocket, respHeader) {
695 | try {
696 | const tcpSocket = connect({ hostname: '8.8.4.4', port: 53 });
697 | let vlessHeader = respHeader;
698 | const writer = tcpSocket.writable.getWriter();
699 | await writer.write(udpChunk);
700 | writer.releaseLock();
701 | await tcpSocket.readable.pipeTo(new WritableStream({
702 | async write(chunk) {
703 | if (webSocket.readyState === 1) {
704 | if (vlessHeader) {
705 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
706 | vlessHeader = null;
707 | } else {
708 | webSocket.send(chunk);
709 | }
710 | }
711 | },
712 | }));
713 | } catch (error) {
714 | console.error(`DNS forward error: ${error.message}`);
715 | }
716 | }
717 |
718 | function base64ToArray(b64Str) {
719 | if (!b64Str) return { error: null };
720 | try {
721 | b64Str = b64Str.replace(/-/g, '+').replace(/_/g, '/');
722 | return { earlyData: Uint8Array.from(atob(b64Str), (c) => c.charCodeAt(0)).buffer, error: null };
723 | } catch (error) {
724 | return { error };
725 | }
726 | }
727 |
728 | function isValidFormat(uuid) {
729 | return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid);
730 | }
731 |
732 | function closeSocketQuietly(socket) {
733 | try { if (socket.readyState === 1 || socket.readyState === 2) socket.close(); }
734 | catch (error) { }
735 | }
736 |
737 | const hexTable = Array.from({ length: 256 }, (v, i) => (i + 256).toString(16).slice(1));
738 |
739 | function formatIdentifier(arr, offset = 0) {
740 | const id = (
741 | hexTable[arr[offset]] + hexTable[arr[offset + 1]] + hexTable[arr[offset + 2]] + hexTable[arr[offset + 3]] + "-" +
742 | hexTable[arr[offset + 4]] + hexTable[arr[offset + 5]] + "-" +
743 | hexTable[arr[offset + 6]] + hexTable[arr[offset + 7]] + "-" +
744 | hexTable[arr[offset + 8]] + hexTable[arr[offset + 9]] + "-" +
745 | hexTable[arr[offset + 10]] + hexTable[arr[offset + 11]] + hexTable[arr[offset + 12]] + hexTable[arr[offset + 13]] +
746 | hexTable[arr[offset + 14]] + hexTable[arr[offset + 15]]
747 | ).toLowerCase();
748 | if (!isValidFormat(id)) throw new TypeError(E_INVALID_ID_STR);
749 | return id;
750 | }
751 |
--------------------------------------------------------------------------------