├── README.md ├── _worker.js ├── base.js ├── clash.js ├── htmlBuilder.js ├── parser.js ├── rules.js ├── singbox.js ├── style.js ├── template_list ├── Private_Line_Groups.txt ├── Universal_Country_Groups.txt └── key_words.txt ├── tempmanager.js └── wrangler.toml /README.md: -------------------------------------------------------------------------------- 1 | # SubHub 2 | 3 | 一个功能强大的订阅转换平台,支持多种代理协议转换和配置模板管理。 4 | 5 | ## 快速开始 6 | 7 | - 🎥 视频教程:[YouTube - SubHub 使用教程](https://www.youtube.com/watch?v=wS1bxcGmZIU) 8 | - 🌐 示例网站:[SubHub Demo](http://subhub-test.698910.xyz) 9 | 10 | ## 功能特点 11 | 12 | - 支持多种代理协议转换: 13 | - VMess 14 | - VLESS (支持 Reality) 15 | - Trojan (没有测试) 16 | - Shadowsocks (没有测试) 17 | - ShadowsocksR (没有测试) 18 | - Hysteria (没有测试) 19 | - Hysteria2 (没有测试) 20 | - TUIC (没有测试) 21 | 22 | - 支持多种输出格式: 23 | - 通用订阅链接 24 | - Clash 配置 25 | - SingBox 配置 26 | 27 | ## 使用说明 28 | 29 | ### 1. 输入类型 30 | - **独立订阅链接**: 直接转换订阅链接,不会保存节点信息,无过期时间 31 | - **多条节点**: 支持批量输入多个节点链接,会保存在 KV 中,24小时后过期 32 | 33 | ### 2. 链接说明 34 | - **通用订阅链接**: `/base?url=` - 返回 base64 编码的原始节点信息 35 | - **Clash 订阅**: `/clash?url=` - 返回 Clash 配置文件 36 | - **SingBox 订阅**: `/singbox?url=` - 返回 SingBox 配置文件 37 | 38 | 所有链接都支持添加 `&template=` 参数指定配置模板 39 | 40 | ### 3. 配置模板说明 41 | 42 | 模板使用类 Clash 的语法格式,主要包含三个部分: 43 | 44 | 1. 规则集定义 45 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/LocalAreaNetwork.list 46 | ruleset=�� 广告拦截,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanAD.list 47 | ruleset=🚀 节点选择,[]MATCH 48 | 49 | 3. 节点分组配置 50 | custom_proxy_group=🚀 节点选择select. 51 | custom_proxy_group=♻️ 自动选择url-test.http://www.gstatic.com/generate_204300,,50 52 | custom_proxy_group=🇭🇰 香港节点url-test(港|HK|Hong Kong)http://www.gstatic.com/generate_204300,,50 53 | 54 | 55 | 4. 节点筛选规则 56 | - 正则匹配: `(港|HK|Hong Kong)` 57 | - 反向匹配: `^(?!.*(美|US|States)).*$` 58 | - 组合匹配: `^(?!.*(美|US|States)).*$(港|HK|Hong Kong)` 59 | 60 | #### 节点分组类型 61 | - `select`: 手动选择节点 62 | - `url-test`: 自动测速选择,可配置测试间隔和延迟阈值 63 | 64 | #### 内置规则 65 | - `[]GEOIP,CN`: GeoIP 规则 66 | - `[]MATCH`: 最终规则 67 | - `[]DIRECT`: 直连规则 68 | - `[]REJECT`: 拒绝规则 69 | 70 | ## 部署说明 71 | 72 | 1. 创建 Cloudflare Worker 73 | 2. 创建 KV 命名空间: 74 | - SUBLINK_KV: 用于存储节点信息(多条节点模式使用) 75 | - TEMPLATE_CONFIG: 用于存储配置模板 76 | 3. 绑定环境变量: 77 | - TEMPLATE_PASSWORD: 模板管理密码 78 | - DEFAULT_TEMPLATE_URL: 默认配置模板链接(可选,默认使用内置链接) 79 | 80 | ### 环境变量说明 81 | 82 | 1. **TEMPLATE_PASSWORD** 83 | - 必需 84 | - 用于模板管理页面的访问控制 85 | - 建议使用强密码 86 | 87 | 2. **DEFAULT_TEMPLATE_URL** 88 | - 可选 89 | - 默认值: `https://raw.githubusercontent.com/Troywww/singbox_conf/refs/heads/main/singbox_clash_conf.txt` 90 | - 用于设置默认的配置模板链接 91 | 92 | 3. **KV 命名空间** 93 | - SUBLINK_KV: 用于存储多条节点信息,24小时自动过期 94 | - TEMPLATE_CONFIG: 用于存储用户自定义的配置模板 95 | 96 | ### 部署方式 97 | 98 | 1. **通过 Cloudflare Dashboard** 99 | - 创建 Worker 100 | - 绑定 KV 命名空间 101 | - 设置环境变量 102 | - 部署代码 103 | 104 | 2. **通过 Cloudflare Pages** 105 | - 连接 GitHub 仓库 106 | - 配置构建设置 107 | - 绑定 KV 和环境变量 108 | - 自动部署 109 | 110 | ## 技术栈 111 | 112 | - 前端: 113 | - React 17 114 | - TailwindCSS 115 | - 原生 JavaScript 116 | 117 | - 后端: 118 | - Cloudflare Workers 119 | - KV 存储 120 | 121 | ## 项目结构 122 | ├── base.js # 基础转换功能 123 | ├── clash.js # Clash 配置生成 124 | ├── singbox.js # SingBox 配置生成 125 | ├── parser.js # 协议解析器 126 | ├── rules.js # 规则管理 127 | ├── tempmanager.js # 模板管理 128 | ├── worker.js # 主入口文件 129 | ├── htmlBuilder.js # 前端页面生成 130 | └── style.js # 样式定义 131 | 132 | 133 | ## 注意事项 134 | 135 | 1. 节点信息安全: 136 | - 独立订阅模式不保存节点信息 137 | - 多条节点模式的信息会在 24 小时后自动删除 138 | - 建议敏感信息使用独立订阅模式 139 | 140 | 2. 配置模板使用: 141 | - 规则集 URL 必须是 HTTPS 142 | - 规则集内容需要符合 Clash 规则格式 143 | - 建议使用 CDN 托管规则文件 144 | - 模板修改需要密码验证 145 | 146 | 3. 性能优化: 147 | - 单个规则集大小建议不超过 1MB 148 | - 规则数量建议控制在合理范围内 149 | - 节点数量过多时建议使用分组筛选 150 | 151 | ## 免责声明 152 | 153 | 本项目仅供学习交流使用,请遵守当地法律法规。 154 | 155 | -------------------------------------------------------------------------------- /_worker.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** 3 | * KV 绑定声明 4 | * @typedef {Object} Env 5 | * @property {KVNamespace} SUBLINK_KV - 用于存储节点信息的短链接 6 | * @property {KVNamespace} TEMPLATE_KV - 用于存储配置模板 7 | * @property {string} TEMPLATE_PASSWORD - 模板管理密码 8 | * @property {string} DEFAULT_TEMPLATE_URL - 默认配置模板链接 9 | * 10 | */ 11 | 12 | import { handleConvertRequest } from './base.js'; 13 | import { handleClashRequest } from './clash.js'; 14 | import { handleSingboxRequest } from './singbox.js'; 15 | import { generateHtml } from './htmlBuilder.js'; 16 | import { 17 | handleGenerateConfig, 18 | handleGetTemplate, 19 | handleListTemplates, 20 | handleDeleteTemplate, 21 | generateTemplateManagerHTML 22 | } from './tempmanager.js'; 23 | /** 24 | * 处理请求 25 | * @param {Request} request 26 | * @param {Env} env 27 | */ 28 | async function handleRequest(request, env) { 29 | const url = new URL(request.url); 30 | const path = url.pathname; 31 | 32 | // 处理页面请求 33 | if (request.method === 'GET' && path === '/') { 34 | return new Response(generateHtml(), { 35 | headers: { 'Content-Type': 'text/html' } 36 | }); 37 | } 38 | 39 | // 处理节点保存请求 40 | if (path === '/save' && request.method === 'POST') { 41 | return handleSaveRequest(request, env); 42 | } 43 | 44 | // 处理转换请求 45 | if (path === '/base') { 46 | return await handleConvertRequest(request, env); 47 | } 48 | 49 | // 处理 SingBox 请求 50 | if (path === '/singbox') { 51 | return await handleSingboxRequest(request, env); 52 | } 53 | 54 | // 处理 Clash 请求 55 | if (path === '/clash') { 56 | return await handleClashRequest(request, env); 57 | } 58 | 59 | // 获取模板列表 60 | if (request.method === 'GET' && path === '/peizhi/api/templates') { 61 | return handleListTemplates(request, env); 62 | } 63 | 64 | // 删除模板 65 | if (request.method === 'DELETE' && path.startsWith('/peizhi/api/templates/')) { 66 | return handleDeleteTemplate(request, url, env); 67 | } 68 | 69 | // 生成配置 70 | if (request.method === 'POST' && path === '/peizhi/api/generate') { 71 | return handleGenerateConfig(request, env); 72 | } 73 | 74 | // 获取模板 75 | if (path.startsWith('/peizhi/template/')) { 76 | const response = await handleGetTemplate(request, url, env); 77 | // 添加 CORS 头 78 | const corsHeaders = { 79 | 'Access-Control-Allow-Origin': '*', 80 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 81 | 'Access-Control-Allow-Headers': 'Content-Type' 82 | }; 83 | 84 | // ���理 OPTIONS 请求 85 | if (request.method === 'OPTIONS') { 86 | return new Response(null, { headers: corsHeaders }); 87 | } 88 | 89 | // 为其他请求添加 CORS 头 90 | Object.entries(corsHeaders).forEach(([key, value]) => { 91 | response.headers.set(key, value); 92 | }); 93 | return response; 94 | } 95 | 96 | // 处理模板管理的主页 97 | if (path === '/peizhi') { 98 | return new Response(generateTemplateManagerHTML(), { 99 | headers: { 100 | 'content-type': 'text/html;charset=UTF-8', 101 | }, 102 | }); 103 | } 104 | 105 | return new Response('Not Found', { status: 404 }); 106 | } 107 | 108 | /** 109 | * 处理节点保存请求 110 | * @param {Request} request 111 | * @param {Env} env 112 | */ 113 | async function handleSaveRequest(request, env) { 114 | if (request.method !== 'POST') { 115 | return new Response('Method not allowed', { status: 405 }); 116 | } 117 | 118 | try { 119 | const { nodes } = await request.json(); 120 | if (!nodes) { 121 | return new Response('No nodes provided', { status: 400 }); 122 | } 123 | 124 | const id = crypto.randomUUID(); 125 | 126 | await env.SUBLINK_KV.put(id, nodes, { 127 | expirationTtl: 86400 // 24小时过期 128 | }); 129 | 130 | return new Response(JSON.stringify({ id }), { 131 | headers: { 'Content-Type': 'application/json' } 132 | }); 133 | } catch (error) { 134 | // 保留错误日志,这对于排查问题很重要 135 | console.error('Save nodes error:', error); 136 | return new Response(`Internal Server Error: ${error.message}`, { 137 | status: 500, 138 | headers: { 'Content-Type': 'text/plain' } 139 | }); 140 | } 141 | } 142 | 143 | export default { 144 | async fetch(request, env, ctx) { 145 | try { 146 | return await handleRequest(request, env); 147 | } catch (error) { 148 | return new Response(JSON.stringify({ 149 | error: error.message || 'Internal Server Error' 150 | }), { 151 | status: 500, 152 | headers: { 'Content-Type': 'application/json' } 153 | }); 154 | } 155 | } 156 | }; -------------------------------------------------------------------------------- /base.js: -------------------------------------------------------------------------------- 1 | import Parser from './parser.js'; 2 | 3 | // @ts-nocheck 4 | /** 5 | * 处理基础转换请求 6 | * @param {Request} request 7 | */ 8 | export async function handleConvertRequest(request, env) { 9 | try { 10 | const url = new URL(request.url); 11 | const sourceUrl = url.searchParams.get('url'); 12 | 13 | if (!sourceUrl) { 14 | return new Response('Missing url parameter', { status: 400 }); 15 | } 16 | 17 | const nodes = await Parser.parse(sourceUrl, env); 18 | 19 | if (!nodes || nodes.length === 0) { 20 | return new Response('No valid nodes found', { status: 400 }); 21 | } 22 | 23 | const convertedNodes = nodes.map(node => { 24 | return convertToLink(node); 25 | }).filter(Boolean); 26 | 27 | const result = convertedNodes.join('\n'); 28 | 29 | return new Response(btoa(result), { 30 | headers: { 31 | 'Content-Type': 'text/plain', 32 | 'Access-Control-Allow-Origin': '*' 33 | } 34 | }); 35 | } catch (error) { 36 | console.error('Convert request error:', error); 37 | return new Response(`Error: ${error.message}`, { 38 | status: 500, 39 | headers: { 'Content-Type': 'text/plain' } 40 | }); 41 | } 42 | } 43 | 44 | function convertToLink(node) { 45 | try { 46 | switch (node.type) { 47 | case 'vmess': 48 | return generateVmessLink(node); 49 | case 'vless': 50 | return generateVlessLink(node); 51 | case 'trojan': 52 | return generateTrojanLink(node); 53 | case 'ss': 54 | return generateSSLink(node); 55 | case 'ssr': 56 | return generateSSRLink(node); 57 | case 'hysteria': 58 | return generateHysteriaLink(node); 59 | case 'hysteria2': 60 | return generateHysteria2Link(node); 61 | case 'tuic': 62 | return generateTuicLink(node); 63 | default: 64 | return null; 65 | } 66 | } catch (error) { 67 | console.error('Error converting node:', error); 68 | return null; 69 | } 70 | } 71 | 72 | // 生成 VMess 链接 73 | function generateVmessLink(node) { 74 | try { 75 | const config = { 76 | v: '2', 77 | ps: node.name, 78 | add: node.server, 79 | port: node.port, 80 | id: node.settings.id, 81 | aid: node.settings.aid || 0, 82 | net: node.settings.net || 'tcp', 83 | type: node.settings.type || 'none', 84 | host: node.settings.host || '', 85 | path: node.settings.path || '', 86 | tls: node.settings.tls || '', 87 | sni: node.settings.sni || '', 88 | alpn: node.settings.alpn || '' 89 | }; 90 | 91 | // 先将配置转换为 UTF-8 编码的字符串 92 | const jsonString = JSON.stringify(config); 93 | const encoder = new TextEncoder(); 94 | const utf8Bytes = encoder.encode(jsonString); 95 | 96 | // 将 UTF-8 字节转换为 base64 97 | return 'vmess://' + btoa(String.fromCharCode.apply(null, utf8Bytes)); 98 | } catch (error) { 99 | console.error('生成 VMess 链接错误:', error); 100 | return null; 101 | } 102 | } 103 | 104 | function generateVlessLink(node) { 105 | try { 106 | const params = new URLSearchParams(); 107 | const { settings } = node; 108 | 109 | if (settings.type) params.set('type', settings.type); 110 | if (settings.security) params.set('security', settings.security); 111 | if (settings.flow) params.set('flow', settings.flow); 112 | if (settings.encryption) params.set('encryption', settings.encryption); 113 | 114 | // Reality 特有参数 115 | if (settings.security === 'reality') { 116 | if (settings.pbk) params.set('pbk', settings.pbk); 117 | if (settings.fp) params.set('fp', settings.fp); 118 | if (settings.sid) params.set('sid', settings.sid); 119 | if (settings.spx) params.set('spx', settings.spx); 120 | } 121 | 122 | // 通用参数 123 | if (settings.path) params.set('path', settings.path); 124 | if (settings.host) params.set('host', settings.host); 125 | if (settings.sni) params.set('sni', settings.sni); 126 | if (settings.alpn) params.set('alpn', settings.alpn); 127 | 128 | const url = `vless://${settings.id}@${node.server}:${node.port}`; 129 | const query = params.toString(); 130 | const hash = node.name ? `#${encodeURIComponent(node.name)}` : ''; 131 | 132 | return `${url}${query ? '?' + query : ''}${hash}`; 133 | } catch (error) { 134 | console.error('Generate VLESS link error:', error); 135 | return null; 136 | } 137 | } 138 | 139 | function generateTrojanLink(node) { 140 | try { 141 | const params = new URLSearchParams(); 142 | const { settings } = node; 143 | 144 | if (settings.type) params.set('type', settings.type); 145 | if (settings.security) params.set('security', settings.security); 146 | if (settings.path) params.set('path', settings.path); 147 | if (settings.host) params.set('host', settings.host); 148 | if (settings.sni) params.set('sni', settings.sni); 149 | if (settings.alpn) params.set('alpn', settings.alpn); 150 | 151 | const url = `trojan://${settings.password}@${node.server}:${node.port}`; 152 | const query = params.toString(); 153 | const hash = node.name ? `#${encodeURIComponent(node.name)}` : ''; 154 | 155 | return `${url}${query ? '?' + query : ''}${hash}`; 156 | } catch (error) { 157 | console.error('Generate Trojan link error:', error); 158 | return null; 159 | } 160 | } 161 | 162 | function generateSSLink(node) { 163 | try { 164 | const userinfo = btoa(`${node.settings.method}:${node.settings.password}`); 165 | const url = `ss://${userinfo}@${node.server}:${node.port}`; 166 | const hash = node.name ? `#${encodeURIComponent(node.name)}` : ''; 167 | return url + hash; 168 | } catch (error) { 169 | console.error('Generate SS link error:', error); 170 | return null; 171 | } 172 | } 173 | 174 | // 生成 ShadowsocksR 链接 175 | function generateSSRLink(node) { 176 | try { 177 | const { settings } = node; 178 | const baseConfig = [ 179 | node.server, 180 | node.port, 181 | settings.protocol, 182 | settings.method, 183 | settings.obfs, 184 | safeBase64Encode(settings.password) 185 | ].join(':'); 186 | 187 | const params = new URLSearchParams(); 188 | if (settings.protocolParam) params.set('protoparam', safeBase64Encode(settings.protocolParam)); 189 | if (settings.obfsParam) params.set('obfsparam', safeBase64Encode(settings.obfsParam)); 190 | if (node.name) params.set('remarks', safeBase64Encode(node.name)); 191 | 192 | const query = params.toString(); 193 | const config = baseConfig + '/?' + query; 194 | return 'ssr://' + safeBase64Encode(config); 195 | } catch (error) { 196 | console.error('生成 SSR 链接错误:', error); 197 | return null; 198 | } 199 | } 200 | 201 | function generateHysteriaLink(node) { 202 | try { 203 | const params = new URLSearchParams(); 204 | const { settings } = node; 205 | 206 | if (settings.protocol) params.set('protocol', settings.protocol); 207 | if (settings.up) params.set('up', settings.up); 208 | if (settings.down) params.set('down', settings.down); 209 | if (settings.alpn) params.set('alpn', settings.alpn); 210 | if (settings.obfs) params.set('obfs', settings.obfs); 211 | if (settings.sni) params.set('sni', settings.sni); 212 | 213 | const url = `hysteria://${node.server}:${node.port}`; 214 | const query = params.toString(); 215 | const hash = node.name ? `#${encodeURIComponent(node.name)}` : ''; 216 | 217 | return `${url}${query ? '?' + query : ''}${hash}`; 218 | } catch (error) { 219 | console.error('Generate Hysteria link error:', error); 220 | return null; 221 | } 222 | } 223 | 224 | function generateHysteria2Link(node) { 225 | try { 226 | const params = new URLSearchParams(); 227 | const { settings } = node; 228 | 229 | if (settings.sni) params.set('sni', settings.sni); 230 | if (settings.obfs) params.set('obfs', settings.obfs); 231 | if (settings.obfsParam) params.set('obfs-password', settings.obfsParam); 232 | 233 | const url = `hysteria2://${settings.auth}@${node.server}:${node.port}`; 234 | const query = params.toString(); 235 | const hash = node.name ? `#${encodeURIComponent(node.name)}` : ''; 236 | 237 | return `${url}${query ? '?' + query : ''}${hash}`; 238 | } catch (error) { 239 | console.error('Generate Hysteria2 link error:', error); 240 | return null; 241 | } 242 | } 243 | 244 | function generateTuicLink(node) { 245 | try { 246 | const { settings } = node; 247 | const params = new URLSearchParams(); 248 | 249 | if (settings.congestion_control) params.set('congestion_control', settings.congestion_control); 250 | if (settings.udp_relay_mode) params.set('udp_relay_mode', settings.udp_relay_mode); 251 | if (settings.alpn && settings.alpn.length) params.set('alpn', settings.alpn.join(',')); 252 | if (settings.reduce_rtt) params.set('reduce_rtt', '1'); 253 | if (settings.sni) params.set('sni', settings.sni); 254 | if (settings.disable_sni) params.set('disable_sni', '1'); 255 | 256 | const url = `tuic://${settings.uuid}:${settings.password}@${node.server}:${node.port}`; 257 | const query = params.toString(); 258 | const hash = node.name ? `#${encodeURIComponent(node.name)}` : ''; 259 | 260 | return `${url}${query ? '?' + query : ''}${hash}`; 261 | } catch (error) { 262 | console.error('Generate TUIC link error:', error); 263 | return null; 264 | } 265 | } 266 | 267 | function safeBase64Encode(str) { 268 | try { 269 | const encoder = new TextEncoder(); 270 | const utf8Bytes = encoder.encode(str); 271 | return btoa(String.fromCharCode.apply(null, utf8Bytes)); 272 | } catch (error) { 273 | console.error('Base64 编码错误:', error); 274 | return ''; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /clash.js: -------------------------------------------------------------------------------- 1 | import Parser from './parser.js'; 2 | 3 | // 定义节点协议列表 4 | const NODE_PROTOCOLS = ['vless:', 'vmess:', 'trojan:', 'ss:', 'ssr:', 'hysteria:', 'tuic:', 'hy2:', 'hysteria2:']; 5 | 6 | // 基础配置 7 | const BASE_CONFIG = `port: 7890 8 | socks-port: 7891 9 | allow-lan: true 10 | mode: rule 11 | log-level: info 12 | external-controller: :9090 13 | dns: 14 | enable: true 15 | enhanced-mode: fake-ip 16 | fake-ip-range: 198.18.0.1/16 17 | nameserver: 18 | - 223.5.5.5 19 | - 119.29.29.29 20 | fallback: 21 | - 8.8.8.8 22 | - 8.8.4.4 23 | default-nameserver: 24 | - 223.5.5.5 25 | - 119.29.29.29 26 | fake-ip-filter: 27 | - '*.lan' 28 | - localhost.ptlogin2.qq.com 29 | - '+.srv.nintendo.net' 30 | - '+.stun.playstation.net' 31 | - '+.msftconnecttest.com' 32 | - '+.msftncsi.com' 33 | - '+.xboxlive.com' 34 | - 'msftconnecttest.com' 35 | - 'xbox.*.microsoft.com' 36 | - '*.battlenet.com.cn' 37 | - '*.battlenet.com' 38 | - '*.blzstatic.cn' 39 | - '*.battle.net' 40 | `; 41 | 42 | // 设置默认模板URL和环境变量处理 43 | const getTemplateUrl = (env) => { 44 | return env?.DEFAULT_TEMPLATE_URL || 'https://raw.githubusercontent.com/Troywww/singbox_conf/refs/heads/main/singbox_clash_conf.txt'; 45 | }; 46 | 47 | export async function handleClashRequest(request, env) { 48 | try { 49 | const url = new URL(request.url); 50 | const directUrl = url.searchParams.get('url'); 51 | const templateUrl = url.searchParams.get('template') || getTemplateUrl(env); 52 | console.log('Fetching template from:', templateUrl); 53 | 54 | // 检查必需的URL参数 55 | let nodes = []; 56 | if (directUrl) { 57 | nodes = await Parser.parse(directUrl, env); 58 | } else { 59 | return new Response('Missing required parameters', { status: 400 }); 60 | } 61 | 62 | if (!nodes || nodes.length === 0) { 63 | return new Response('No valid nodes found', { status: 400 }); 64 | } 65 | 66 | // 获取模板配置 67 | const templateResponse = await fetch(templateUrl); 68 | console.log('Template response:', { 69 | status: templateResponse.status, 70 | contentType: templateResponse.headers.get('content-type'), 71 | url: templateUrl 72 | }); 73 | 74 | // 检查是否是内部模板URL 75 | let templateContent; 76 | if (templateUrl.startsWith('https://inner.template.secret/id-')) { 77 | const templateId = templateUrl.replace('https://inner.template.secret/id-', ''); 78 | const templateData = await env.TEMPLATE_CONFIG.get(templateId); 79 | if (!templateData) { 80 | return new Response('Template not found', { status: 404 }); 81 | } 82 | const templateInfo = JSON.parse(templateData); 83 | templateContent = templateInfo.content; 84 | } else { 85 | if (!templateResponse.ok) { 86 | return new Response('Failed to fetch template', { status: 500 }); 87 | } 88 | templateContent = await templateResponse.text(); 89 | } 90 | 91 | // 生成完整的 Clash 配置 92 | const config = await generateClashConfig(templateContent, nodes); 93 | 94 | return new Response(config, { 95 | headers: { 96 | 'Content-Type': 'text/yaml', 97 | 'Content-Disposition': 'attachment; filename=config.yaml' 98 | } 99 | }); 100 | } catch (error) { 101 | console.error('Clash convert error:', error); 102 | return new Response('Internal Server Error: ' + error.message, { status: 500 }); 103 | } 104 | } 105 | 106 | async function generateClashConfig(templateContent, nodes) { 107 | let config = BASE_CONFIG + '\n'; 108 | 109 | // 添加代理节点 110 | config += 'proxies:\n'; 111 | 112 | const proxies = nodes.map(node => { 113 | const converted = convertNodeToClash(node); 114 | return converted; 115 | }).filter(Boolean); 116 | 117 | proxies.forEach(proxy => { 118 | config += ' -'; 119 | function writeValue(obj, indent = 4) { 120 | Object.entries(obj).forEach(([key, value]) => { 121 | if (value === undefined || value === null) { 122 | return; 123 | } 124 | 125 | const spaces = ' '.repeat(indent); 126 | if (typeof value === 'object') { 127 | config += `\n${spaces}${key}:`; 128 | writeValue(value, indent + 2); 129 | } else { 130 | const formattedValue = typeof value === 'boolean' || typeof value === 'number' 131 | ? value 132 | : `"${value}"`; 133 | config += `\n${spaces}${key}: ${formattedValue}`; 134 | } 135 | }); 136 | } 137 | writeValue(proxy); 138 | config += '\n'; 139 | }); 140 | 141 | // 处理分组 142 | config += '\nproxy-groups:\n'; 143 | const groupLines = templateContent.split('\n') 144 | .filter(line => line.startsWith('custom_proxy_group=')); 145 | 146 | groupLines.forEach(line => { 147 | const [groupName, ...rest] = line.slice('custom_proxy_group='.length).split('`'); 148 | const groupType = rest[0]; 149 | const options = rest.slice(1); 150 | 151 | config += ` - name: "${groupName}"\n`; 152 | config += ` type: ${groupType === 'url-test' ? 'url-test' : 'select'}\n`; 153 | 154 | // 处理 url-test 类型的特殊配置 155 | if (groupType === 'url-test') { 156 | const testUrl = options.find(opt => opt.startsWith('http')) || 'http://www.gstatic.com/generate_204'; 157 | const interval = 300; 158 | const tolerance = groupName.includes('欧美') ? 150 : 50; 159 | 160 | config += ` url: ${testUrl}\n`; 161 | config += ` interval: ${interval}\n`; 162 | config += ` tolerance: ${tolerance}\n`; 163 | } 164 | 165 | config += ' proxies:\n'; 166 | let hasProxies = false; 167 | 168 | // 处理分组选项 169 | options.forEach(option => { 170 | if (option.startsWith('[]')) { 171 | hasProxies = true; 172 | const groupRef = option.slice(2); 173 | config += ` - ${groupRef}\n`; 174 | } else if (option === 'DIRECT' || option === 'REJECT') { 175 | hasProxies = true; 176 | config += ` - ${option}\n`; 177 | } else if (!option.startsWith('http')) { 178 | try { 179 | let matchedCount = 0; 180 | // 处理正则表达式过滤 181 | let pattern = option; 182 | 183 | // 处理否定查找 184 | if (pattern.includes('(?!')) { 185 | const [excludePattern, includePattern] = pattern.split(')).*$'); 186 | const exclude = excludePattern.substring(excludePattern.indexOf('.*(') + 3).split('|'); 187 | const include = includePattern ? includePattern.slice(1).split('|') : []; 188 | 189 | // 添加调试日志 190 | console.log('Pattern processing:', { 191 | original: pattern, 192 | exclude, 193 | include, 194 | includePattern 195 | }); 196 | 197 | const matchedProxies = proxies.filter(proxy => { 198 | const isExcluded = exclude.some(keyword => 199 | proxy.name.includes(keyword) 200 | ); 201 | if (isExcluded) return false; 202 | 203 | // 如果没有包含模式,则返回所有未被排除的节点 204 | if (!includePattern || include.length === 0) { 205 | return true; 206 | } 207 | // 如果有包含模式,则需要匹配包含模式 208 | return include.some(keyword => 209 | proxy.name.includes(keyword) 210 | ); 211 | }); 212 | 213 | matchedProxies.forEach(proxy => { 214 | hasProxies = true; 215 | matchedCount++; 216 | config += ` - ${proxy.name}\n`; 217 | }); 218 | } else { 219 | const filter = new RegExp(pattern); 220 | const matchedProxies = proxies.filter(proxy => 221 | filter.test(proxy.name) 222 | ); 223 | matchedProxies.forEach(proxy => { 224 | hasProxies = true; 225 | matchedCount++; 226 | config += ` - ${proxy.name}\n`; 227 | }); 228 | } 229 | } catch (error) { 230 | console.error('Error processing proxy group option:', error); 231 | } 232 | } 233 | }); 234 | 235 | // 如果分组没有任何节点,添加 DIRECT 236 | if (!hasProxies) { 237 | config += ' - "DIRECT"\n'; 238 | } 239 | }); 240 | 241 | // 处理规则 242 | config += '\nrules:\n'; 243 | const ruleLines = templateContent.split('\n') 244 | .filter(line => line.startsWith('ruleset=')) 245 | .map(line => line.trim()); 246 | 247 | // 获取并解析所有规则列表 248 | for (const line of ruleLines) { 249 | const groupEndIndex = line.indexOf(','); 250 | const group = line.substring('ruleset='.length, groupEndIndex); 251 | const url = line.substring(groupEndIndex + 1); 252 | 253 | if (url.startsWith('[]')) { 254 | // 处理内置规则 255 | const ruleContent = url.slice(2); 256 | 257 | if (ruleContent === 'MATCH' || ruleContent === 'FINAL') { 258 | config += ` - MATCH,${group}\n`; 259 | } else if (ruleContent.startsWith('GEOIP,')) { 260 | config += ` - ${ruleContent},${group}\n`; 261 | } else { 262 | config += ` - ${ruleContent},${group}\n`; 263 | } 264 | } else { 265 | try { 266 | // 获取规则列表内容 267 | const response = await fetch(url); 268 | if (!response.ok) { 269 | console.error(`Failed to fetch rules from ${url}: ${response.status}`); 270 | continue; 271 | } 272 | 273 | const ruleContent = await response.text(); 274 | const rules = ruleContent.split('\n') 275 | .map(rule => rule.trim()) 276 | .filter(rule => rule && !rule.startsWith('#')); 277 | 278 | // 添加解析后的规则 279 | rules.forEach(rule => { 280 | if (rule.includes(',')) { 281 | const parts = rule.split(','); 282 | const ruleType = parts[0]; 283 | const ruleValue = parts[1]; 284 | 285 | // 跳过 USER-AGENT 和 URL-REGEX 规则 286 | if (ruleType === 'USER-AGENT' || ruleType === 'URL-REGEX') { 287 | return; 288 | } 289 | 290 | // 处理规则 291 | if (ruleType === 'IP-CIDR' || ruleType === 'IP-CIDR6') { 292 | config += ` - ${ruleType},${ruleValue},${group},no-resolve\n`; 293 | } else if (ruleType === 'FINAL') { 294 | config += ` - MATCH,${group}\n`; 295 | } else { 296 | config += ` - ${ruleType},${ruleValue},${group}\n`; 297 | } 298 | } 299 | }); 300 | } catch (error) { 301 | console.error(`Error processing rule list ${url}:`, error); 302 | } 303 | } 304 | } 305 | 306 | return config; 307 | } 308 | 309 | function convertNodeToClash(node) { 310 | switch (node.type) { 311 | case 'vmess': 312 | return convertVmess(node); 313 | case 'vless': 314 | return convertVless(node); 315 | case 'trojan': 316 | return convertTrojan(node); 317 | case 'ss': 318 | return convertShadowsocks(node); 319 | case 'ssr': 320 | return convertShadowsocksR(node); 321 | case 'hysteria': 322 | return convertHysteria(node); 323 | case 'hysteria2': 324 | return convertHysteria2(node); 325 | case 'tuic': 326 | return convertTuic(node); 327 | default: 328 | return null; 329 | } 330 | } 331 | 332 | function convertVmess(node) { 333 | // 基础配置 334 | const config = { 335 | name: node.name, 336 | type: 'vmess', 337 | server: node.server, 338 | port: node.port, 339 | uuid: node.settings.id, 340 | alterId: node.settings.aid || 0, 341 | cipher: 'auto', 342 | udp: true 343 | }; 344 | 345 | // 网络设置 346 | if (node.settings.net) { 347 | config.network = node.settings.net; 348 | 349 | // ws 配置 350 | if (node.settings.net === 'ws') { 351 | config['ws-opts'] = { 352 | path: node.settings.path || '/', 353 | headers: { 354 | Host: node.settings.host || '' 355 | } 356 | }; 357 | } 358 | } 359 | 360 | // TLS 设置 361 | if (node.settings.tls === 'tls') { 362 | config.tls = true; 363 | if (node.settings.sni) { 364 | config.servername = node.settings.sni; 365 | } 366 | } 367 | 368 | return config; 369 | } 370 | 371 | function convertVless(node) { 372 | const config = { 373 | name: node.name, 374 | type: 'vless', 375 | server: node.server, 376 | port: node.port, 377 | uuid: node.settings.id, 378 | network: node.settings.type || node.settings.net || 'tcp', 379 | 'skip-cert-verify': false, 380 | tls: true 381 | }; 382 | 383 | // 基本配置 384 | if (node.settings.flow) { 385 | config.flow = node.settings.flow; 386 | } 387 | 388 | if (node.settings.sni || node.settings.host) { 389 | config.servername = node.settings.sni || node.settings.host; 390 | } 391 | 392 | // Reality 配置 393 | if (node.settings.security === 'reality') { 394 | config.flow = 'xtls-rprx-vision'; 395 | config['reality-opts'] = { 396 | 'public-key': node.settings.pbk 397 | }; 398 | config['client-fingerprint'] = node.settings.fp || 'chrome'; 399 | } 400 | 401 | // WebSocket 配置 402 | if (node.settings.type === 'ws' || node.settings.net === 'ws') { 403 | config['ws-opts'] = { 404 | path: node.settings.path || '/', 405 | headers: { 406 | Host: node.settings.host || node.settings.sni || node.server 407 | } 408 | }; 409 | } 410 | 411 | return config; 412 | } 413 | 414 | function convertTrojan(node) { 415 | return { 416 | name: node.name, 417 | type: 'trojan', 418 | server: node.server, 419 | port: node.port, 420 | password: node.settings.password, 421 | udp: true, 422 | 'skip-cert-verify': true, 423 | network: node.settings.type || 'tcp', 424 | 'ws-opts': node.settings.type === 'ws' ? { 425 | path: node.settings.path, 426 | headers: { Host: node.settings.host } 427 | } : undefined, 428 | sni: node.settings.sni || undefined, 429 | alpn: node.settings.alpn ? [node.settings.alpn] : undefined 430 | }; 431 | } 432 | 433 | function convertShadowsocks(node) { 434 | return { 435 | name: node.name, 436 | type: 'ss', 437 | server: node.server, 438 | port: node.port, 439 | cipher: node.settings.method, 440 | password: node.settings.password, 441 | udp: true 442 | }; 443 | } 444 | 445 | function convertShadowsocksR(node) { 446 | return { 447 | name: node.name, 448 | type: 'ssr', 449 | server: node.server, 450 | port: node.port, 451 | cipher: node.settings.method, 452 | password: node.settings.password, 453 | protocol: node.settings.protocol, 454 | 'protocol-param': node.settings.protocolParam, 455 | obfs: node.settings.obfs, 456 | 'obfs-param': node.settings.obfsParam, 457 | udp: true 458 | }; 459 | } 460 | 461 | function convertHysteria(node) { 462 | return { 463 | name: node.name, 464 | type: 'hysteria', 465 | server: node.server, 466 | port: node.port, 467 | auth_str: node.settings.auth, 468 | up: node.settings.up, 469 | down: node.settings.down, 470 | 'skip-cert-verify': true, 471 | sni: node.settings.sni, 472 | alpn: node.settings.alpn ? [node.settings.alpn] : undefined, 473 | obfs: node.settings.obfs 474 | }; 475 | } 476 | 477 | function convertHysteria2(node) { 478 | return { 479 | name: node.name, 480 | type: 'hysteria2', 481 | server: node.server, 482 | port: node.port, 483 | password: node.settings.auth, 484 | 'skip-cert-verify': true, 485 | sni: node.settings.sni, 486 | obfs: node.settings.obfs, 487 | 'obfs-password': node.settings.obfsParam 488 | }; 489 | 490 | } 491 | 492 | // 添加新的转换函数 493 | function convertTuic(node) { 494 | return { 495 | name: node.name, 496 | type: 'tuic', 497 | server: node.server, 498 | port: node.port, 499 | uuid: node.settings.uuid, 500 | password: node.settings.password, 501 | 'congestion-controller': node.settings.congestion_control || 'bbr', 502 | 'udp-relay-mode': node.settings.udp_relay_mode || 'native', 503 | 'reduce-rtt': node.settings.reduce_rtt || false, 504 | 'skip-cert-verify': true, 505 | sni: node.settings.sni || undefined, 506 | alpn: node.settings.alpn ? [node.settings.alpn] : undefined 507 | }; 508 | } 509 | -------------------------------------------------------------------------------- /htmlBuilder.js: -------------------------------------------------------------------------------- 1 | import { styleCSS } from './style.js'; 2 | 3 | // 修改 API 路径 4 | const API_BASE = '/template-manager'; 5 | 6 | // 修改 HTML 部分 7 | const configSection = ` 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 23 |
24 |
25 |
加载中...
26 |
27 |
28 |
29 | `; 30 | 31 | // 修改 JavaScript 部分 32 | const templateManagerScript = ` 33 | class TemplateManager { 34 | constructor() { 35 | this.loadTemplates(); 36 | this.initManageButton(); 37 | } 38 | 39 | initManageButton() { 40 | const manageBtn = document.getElementById('manageTemplateBtn'); 41 | if (manageBtn) { 42 | manageBtn.addEventListener('click', () => { 43 | window.open('/peizhi', '_blank'); 44 | }); 45 | } 46 | } 47 | 48 | async loadTemplates() { 49 | try { 50 | const response = await fetch('/peizhi/api/templates'); 51 | if (!response.ok) throw new Error('Failed to load templates'); 52 | 53 | const templates = await response.json(); 54 | this.renderTemplates(templates); 55 | } catch (error) { 56 | console.error('Load templates error:', error); 57 | document.getElementById('templateList').innerHTML = 58 | '
加载模板失败,请刷新重试
'; 59 | } 60 | } 61 | 62 | renderTemplates(templates) { 63 | const templateList = document.getElementById('templateList'); 64 | if (!templates.length) { 65 | templateList.innerHTML = '
暂无保存的模板
'; 66 | return; 67 | } 68 | 69 | templateList.innerHTML = templates 70 | .sort((a, b) => new Date(b.createTime) - new Date(a.createTime)) 71 | .map(template => { 72 | const createTime = new Date(template.createTime).toLocaleString('zh-CN', { 73 | year: 'numeric', 74 | month: '2-digit', 75 | day: '2-digit', 76 | hour: '2-digit', 77 | minute: '2-digit' 78 | }); 79 | 80 | // 生成内部模板URL 81 | const internalTemplateUrl = 'https://inner.template.secret/id-' + template.id; 82 | 83 | return \` 84 |
87 |
88 |
89 |
90 | 91 | 92 | 93 | \${template.name} 94 |
95 | 101 |
102 |
103 | 104 | 105 | 106 | \${createTime} 107 |
108 |
109 |
110 | \`; 111 | }) 112 | .join(''); 113 | 114 | // 添加点击事件 115 | templateList.querySelectorAll('.template-item').forEach(item => { 116 | item.addEventListener('click', (e) => { 117 | // 如果点击的是查看按钮,不触发选择事件 118 | if (e.target.closest('.view-btn')) return; 119 | 120 | const templateUrl = item.dataset.url; 121 | document.getElementById('templateUrl').value = templateUrl; 122 | 123 | // 更新选中状态 124 | templateList.querySelectorAll('.template-item').forEach(i => 125 | i.classList.remove('selected')); 126 | item.classList.add('selected'); 127 | }); 128 | }); 129 | } 130 | } 131 | 132 | // 初始化模板管理器 133 | new TemplateManager(); 134 | `; 135 | 136 | // ���改 generateHtml 函数 137 | export function generateHtml() { 138 | return ` 139 | 140 | 141 | SubHub 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
150 |

SubHub订阅转换

151 | 152 |
153 | 154 |
155 |

输入类型

156 |
157 | 161 | 165 | 166 |
167 |
168 | 169 | 170 |
171 | 172 | 177 |
178 | 179 | 180 |
181 |
182 | 183 | 189 |
190 | 191 |
192 |
193 | 194 | 200 |
201 | 202 |
203 |
加载中...
204 |
205 |
206 |
207 | 208 | 209 |
210 | 217 |
218 | 219 | 220 |
221 |
222 |
223 | 224 | 508 | 509 | `; 510 | } -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | // 添加名称解码函数 4 | function decodeNodeName(encodedName, fallback = 'Unnamed') { 5 | if (!encodedName) return fallback; 6 | 7 | try { 8 | let decoded = encodedName; 9 | 10 | // 1. 第一次 URL 解码 11 | try { 12 | const urlDecoded = decodeURIComponent(decoded); 13 | decoded = urlDecoded; 14 | } catch (e) {} 15 | 16 | // 2. 第二次 URL 解码(处理双重编码) 17 | try { 18 | const urlDecoded2 = decodeURIComponent(decoded); 19 | decoded = urlDecoded2; 20 | } catch (e) {} 21 | 22 | // 3. 如果看起来是 Base64,尝试 Base64 解码 23 | if (/^[A-Za-z0-9+/=]+$/.test(decoded)) { 24 | try { 25 | const base64Decoded = atob(decoded); 26 | const bytes = new Uint8Array(base64Decoded.length); 27 | for (let i = 0; i < base64Decoded.length; i++) { 28 | bytes[i] = base64Decoded.charCodeAt(i); 29 | } 30 | const text = new TextDecoder('utf-8').decode(bytes); 31 | if (/^[\x20-\x7E\u4E00-\u9FFF]+$/.test(text)) { 32 | decoded = text; 33 | } 34 | } catch (e) {} 35 | } 36 | 37 | // 4. 尝试 UTF-8 解码 38 | try { 39 | const utf8Decoded = decodeURIComponent(escape(decoded)); 40 | if (utf8Decoded !== decoded) { 41 | decoded = utf8Decoded; 42 | } 43 | } catch (e) {} 44 | 45 | return decoded; 46 | } catch (e) { 47 | return encodedName || fallback; 48 | } 49 | } 50 | 51 | export default class Parser { 52 | /** 53 | * 解析订阅内容 54 | * @param {string} url - 订阅链接或短链ID 55 | * @param {Env} [env] - KV 环境变量 56 | */ 57 | static async parse(url, env) { 58 | try { 59 | // 检查是否为内部URL格式 60 | if (url.startsWith('http://inner.nodes.secret/id-')) { 61 | const kvId = url.replace('http://inner.nodes.secret/id-', ''); 62 | // 从KV读取节点信息 63 | const nodesData = await env.SUBLINK_KV.get(kvId); 64 | if (!nodesData) { 65 | throw new Error('Nodes not found in KV storage'); 66 | } 67 | 68 | let nodes = []; 69 | // 分割多行内容 70 | const lines = nodesData.split('\n').filter(line => line.trim()); 71 | 72 | for (const line of lines) { 73 | if (line.startsWith('http')) { 74 | // 如果是URL,解析订阅内容 75 | const subNodes = await this.parse(line, env); 76 | nodes = nodes.concat(subNodes); 77 | } else { 78 | // 如果是节点配置,直接解析 79 | const node = this.parseLine(line.trim()); 80 | if (node) { 81 | nodes.push(node); 82 | } 83 | } 84 | } 85 | 86 | return nodes; 87 | } 88 | 89 | // 处理普通URL 90 | const response = await fetch(url); 91 | if (!response.ok) { 92 | throw new Error(`HTTP error! status: ${response.status}`); 93 | } 94 | const content = await response.text(); 95 | return this.parseContent(content, env); 96 | } catch (error) { 97 | throw error; 98 | } 99 | } 100 | 101 | /** 102 | * 解析订阅内容 103 | * @param {string} content 104 | * @returns {Promise} 节点列表 105 | */ 106 | static async parseContent(content, env) { 107 | try { 108 | if (!content) return []; 109 | 110 | // 尝试 Base64 解码 111 | let decodedContent = this.tryBase64Decode(content); 112 | 113 | // 分割成行 114 | const lines = decodedContent.split(/[\n\s]+/).filter(line => line.trim()); 115 | 116 | let nodes = []; 117 | for (const line of lines) { 118 | if (this.isSubscriptionUrl(line)) { 119 | // 如果是订阅链接,递归解析 120 | const subNodes = await this.parse(line, env); 121 | nodes = nodes.concat(subNodes); 122 | } else { 123 | // 解析单个节点 124 | const node = this.parseLine(line.trim()); 125 | if (node) { 126 | nodes.push(node); 127 | } 128 | } 129 | } 130 | 131 | return nodes; 132 | } catch (error) { 133 | console.error('Parse error:', error); 134 | return []; 135 | } 136 | } 137 | 138 | /** 139 | * 判断是否为订阅链接 140 | * @param {string} line 141 | * @returns {boolean} 142 | */ 143 | static isSubscriptionUrl(line) { 144 | try { 145 | // 1. 检查是否是 UUID 格式(跳过 UUID 格式的字符串) 146 | if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(line)) { 147 | return false; 148 | } 149 | 150 | // 2. 检查是否是有效的URL 151 | const url = new URL(line); 152 | 153 | // 3. 必须是 http 或 https 协议 154 | if (url.protocol !== 'http:' && url.protocol !== 'https:') { 155 | return false; 156 | } 157 | 158 | // 4. 排除已知的节点链接协议 159 | const nodeProtocols = ['vmess://', 'vless://', 'trojan://', 'ss://', 'ssr://', 'hysteria://', 'hysteria2://', 'tuic://']; 160 | if (nodeProtocols.some(protocol => line.toLowerCase().startsWith(protocol))) { 161 | return false; 162 | } 163 | 164 | return true; 165 | } catch (error) { 166 | return false; 167 | } 168 | } 169 | 170 | /** 171 | * 尝试 Base64 解码 172 | * @param {string} content 173 | * @returns {string} 174 | */ 175 | static tryBase64Decode(content) { 176 | try { 177 | // 1. 检查是否看起来像 Base64 178 | if (!/^[A-Za-z0-9+/=]+$/.test(content.trim())) { 179 | return content; 180 | } 181 | 182 | // 2. 尝试 Base64 解码 183 | const decoded = atob(content); 184 | 185 | // 3. 验证解码结果是否包含有效的协议前缀 186 | const validProtocols = ['vmess://', 'vless://', 'trojan://', 'ss://', 'ssr://']; 187 | if (validProtocols.some(protocol => decoded.includes(protocol))) { 188 | return decoded; 189 | } 190 | 191 | // 4. 如果解码结果不包含有效协议,返回原内容 192 | return content; 193 | } catch { 194 | // 5. 如果解码失败,返回原内容 195 | return content; 196 | } 197 | } 198 | 199 | /** 200 | * 解析单行内容 201 | * @param {string} line 202 | * @returns {Object|null} 203 | */ 204 | static parseLine(line) { 205 | if (!line) return null; 206 | 207 | try { 208 | // 解析不同类型的节点 209 | if (line.startsWith('vmess://')) { 210 | return this.parseVmess(line); 211 | } else if (line.startsWith('vless://')) { 212 | return this.parseVless(line); 213 | } else if (line.startsWith('trojan://')) { 214 | return this.parseTrojan(line); 215 | } else if (line.startsWith('ss://')) { 216 | return this.parseSS(line); 217 | } else if (line.startsWith('ssr://')) { 218 | return this.parseSSR(line); 219 | } else if (line.startsWith('hysteria://')) { 220 | return this.parseHysteria(line); 221 | } else if (line.startsWith('hysteria2://')) { 222 | return this.parseHysteria2(line); 223 | } else if (line.startsWith('tuic://')) { 224 | return this.parseTuic(line); 225 | } 226 | return null; 227 | } catch (error) { 228 | return null; 229 | } 230 | } 231 | 232 | /** 233 | * 解析 VMess 节点 234 | * @param {string} line 235 | * @returns {Object|null} 236 | */ 237 | static parseVmess(line) { 238 | try { 239 | const content = line.slice(8); // 移除 "vmess://" 240 | // 将 URL 安全的 base64 转换为标准 base64 241 | const safeContent = content 242 | .replace(/-/g, '+') 243 | .replace(/_/g, '/') 244 | .replace(/\s+/g, ''); 245 | 246 | // 添加适当的填充 247 | let paddedContent = safeContent; 248 | const mod4 = safeContent.length % 4; 249 | if (mod4) { 250 | paddedContent += '='.repeat(4 - mod4); 251 | } 252 | 253 | const config = JSON.parse(atob(paddedContent)); 254 | return { 255 | type: 'vmess', 256 | name: decodeNodeName(config.ps || 'Unnamed'), 257 | server: config.add, 258 | port: parseInt(config.port), 259 | settings: { 260 | id: config.id, 261 | aid: parseInt(config.aid), 262 | net: config.net, 263 | type: config.type, 264 | host: config.host, 265 | path: config.path, 266 | tls: config.tls, 267 | sni: config.sni, 268 | alpn: config.alpn 269 | } 270 | }; 271 | } catch (error) { 272 | console.error('Parse VMess error:', error); 273 | return null; 274 | } 275 | } 276 | 277 | /** 278 | * 解析 VLESS 节点 279 | * @param {string} line 280 | * @returns {Object|null} 281 | */ 282 | static parseVless(line) { 283 | try { 284 | const url = new URL(line); 285 | const params = new URLSearchParams(url.search); 286 | return { 287 | type: 'vless', 288 | name: decodeNodeName(url.hash.slice(1)), 289 | server: url.hostname, 290 | port: parseInt(url.port), 291 | settings: { 292 | id: url.username, 293 | flow: params.get('flow') || '', 294 | encryption: params.get('encryption') || 'none', 295 | type: params.get('type') || 'tcp', 296 | security: params.get('security') || '', 297 | path: params.get('path') || '', 298 | host: params.get('host') || '', 299 | sni: params.get('sni') || '', 300 | alpn: params.get('alpn') || '', 301 | pbk: params.get('pbk') || '', 302 | fp: params.get('fp') || '', 303 | sid: params.get('sid') || '', 304 | spx: params.get('spx') || '' 305 | } 306 | }; 307 | } catch (error) { 308 | console.error('Parse VLESS error:', error); 309 | return null; 310 | } 311 | } 312 | 313 | /** 314 | * 解析 Trojan 节点 315 | * @param {string} line 316 | * @returns {Object|null} 317 | */ 318 | static parseTrojan(line) { 319 | try { 320 | const url = new URL(line); 321 | const params = new URLSearchParams(url.search); 322 | return { 323 | type: 'trojan', 324 | name: decodeNodeName(params.get('remarks') || '') || decodeNodeName(url.hash.slice(1)), 325 | server: url.hostname, 326 | port: parseInt(url.port), 327 | settings: { 328 | password: url.username, 329 | type: params.get('type') || 'tcp', 330 | security: params.get('security') || 'tls', 331 | path: params.get('path') || '', 332 | host: params.get('host') || '', 333 | sni: params.get('sni') || '', 334 | alpn: params.get('alpn') || '' 335 | } 336 | }; 337 | } catch (error) { 338 | console.error('Parse Trojan error:', error); 339 | return null; 340 | } 341 | } 342 | 343 | /** 344 | * 解析 Shadowsocks 节点 345 | * @param {string} line 346 | * @returns {Object|null} 347 | */ 348 | static parseSS(line) { 349 | try { 350 | const content = line.slice(5); // 移除 "ss://" 351 | const [userinfo, serverInfo] = content.split('@'); 352 | const [method, password] = atob(userinfo).split(':'); 353 | const [server, port] = serverInfo.split(':'); 354 | return { 355 | type: 'ss', 356 | name: decodeNodeName(serverInfo || 'Unnamed'), 357 | server, 358 | port: parseInt(port), 359 | settings: { 360 | method, 361 | password 362 | } 363 | }; 364 | } catch (error) { 365 | console.error('Parse Shadowsocks error:', error); 366 | return null; 367 | } 368 | } 369 | 370 | /** 371 | * 解析 ShadowsocksR 节点 372 | * @param {string} line 373 | * @returns {Object|null} 374 | */ 375 | static parseSSR(line) { 376 | try { 377 | const content = line.slice(6); // 移除 "ssr://" 378 | const decoded = this.tryBase64Decode(content); 379 | const [baseConfig, query] = decoded.split('/?'); 380 | const [server, port, protocol, method, obfs, password] = baseConfig.split(':'); 381 | const params = new URLSearchParams(query); 382 | return { 383 | type: 'ssr', 384 | name: decodeNodeName(params.get('remarks') || ''), 385 | server, 386 | port: parseInt(port), 387 | settings: { 388 | protocol, 389 | method, 390 | obfs, 391 | password: atob(password), 392 | protocolParam: atob(params.get('protoparam') || ''), 393 | obfsParam: atob(params.get('obfsparam') || '') 394 | } 395 | }; 396 | } catch (error) { 397 | console.error('Parse ShadowsocksR error:', error); 398 | return null; 399 | } 400 | } 401 | 402 | /** 403 | * 解析 Hysteria 节点 404 | * @param {string} line 405 | * @returns {Object|null} 406 | */ 407 | static parseHysteria(line) { 408 | try { 409 | const url = new URL(line); 410 | const params = new URLSearchParams(url.search); 411 | return { 412 | type: 'hysteria', 413 | name: decodeNodeName(params.get('remarks') || '') || decodeNodeName(url.hash.slice(1)), 414 | server: url.hostname, 415 | port: parseInt(url.port), 416 | settings: { 417 | auth: url.username, 418 | protocol: params.get('protocol') || '', 419 | up: params.get('up') || '', 420 | down: params.get('down') || '', 421 | alpn: params.get('alpn') || '', 422 | obfs: params.get('obfs') || '', 423 | sni: params.get('sni') || '' 424 | } 425 | }; 426 | } catch (error) { 427 | console.error('Parse Hysteria error:', error); 428 | return null; 429 | } 430 | } 431 | 432 | /** 433 | * 解析 Hysteria2 节点 434 | * @param {string} line 435 | * @returns {Object|null} 436 | */ 437 | static parseHysteria2(line) { 438 | try { 439 | const url = new URL(line); 440 | const params = new URLSearchParams(url.search); 441 | return { 442 | type: 'hysteria2', 443 | name: decodeNodeName(params.get('remarks') || '') || decodeNodeName(url.hash.slice(1)), 444 | server: url.hostname, 445 | port: parseInt(url.port), 446 | settings: { 447 | auth: url.username, 448 | sni: params.get('sni') || '', 449 | obfs: params.get('obfs') || '', 450 | obfsParam: params.get('obfs-password') || '' 451 | } 452 | }; 453 | } catch (error) { 454 | console.error('Parse Hysteria2 error:', error); 455 | return null; 456 | } 457 | } 458 | 459 | /** 460 | * 解析 TUIC 节点 461 | * @param {string} line 462 | * @returns {Object|null} 463 | */ 464 | static parseTuic(line) { 465 | try { 466 | const url = new URL(line); 467 | const params = new URLSearchParams(url.search); 468 | return { 469 | type: 'tuic', 470 | name: decodeNodeName(url.hash.slice(1)), 471 | server: url.hostname, 472 | port: parseInt(url.port), 473 | settings: { 474 | uuid: url.username, 475 | password: url.password, 476 | congestion_control: params.get('congestion_control') || 'bbr', 477 | udp_relay_mode: params.get('udp_relay_mode') || 'native', 478 | alpn: (params.get('alpn') || '').split(',').filter(Boolean), 479 | reduce_rtt: params.get('reduce_rtt') === '1', 480 | sni: params.get('sni') || '', 481 | disable_sni: params.get('disable_sni') === '1' 482 | } 483 | }; 484 | } catch (error) { 485 | console.error('Parse TUIC error:', error); 486 | return null; 487 | } 488 | } 489 | } -------------------------------------------------------------------------------- /rules.js: -------------------------------------------------------------------------------- 1 | // 预设规则列表 2 | export const DEFAULT_RULES = ` 3 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/LocalAreaNetwork.list 4 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/UnBan.list 5 | ruleset=🛑 广告拦截,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanAD.list 6 | ruleset=🍃 应用净化,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanProgramAD.list 7 | ruleset=🆎 AdBlock,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyList.list 8 | ruleset=🆎 AdBlock,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyListChina.list 9 | ruleset=🛡️ 隐私防护,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyPrivacy.list 10 | ruleset=📢 谷歌FCM,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/GoogleFCM.list 11 | ruleset=💰 加密货币,https://raw.githubusercontent.com/Troywww/singbox_conf/refs/heads/main/rules_list/crypto.list 12 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/GoogleCN.list 13 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/SteamCN.list 14 | ruleset=Ⓜ️ 微软Bing,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Bing.list 15 | ruleset=Ⓜ️ 微软云盘,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/OneDrive.list 16 | ruleset=Ⓜ️ 微软服务,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Microsoft.list 17 | ruleset=🍎 苹果服务,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Apple.list 18 | ruleset=📲 电报消息,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Telegram.list 19 | ruleset=💬 OpenAi,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/OpenAi.list 20 | ruleset=🎶 网易音乐,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/NetEaseMusic.list 21 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Epic.list 22 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Origin.list 23 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Sony.list 24 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Steam.list 25 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Nintendo.list 26 | ruleset=📹 油管视频,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/YouTube.list 27 | ruleset=🎥 奈飞视频,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Netflix.list 28 | ruleset=📺 巴哈姆特,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Bahamut.list 29 | ruleset=📺 哔哩哔哩,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/BilibiliHMT.list 30 | ruleset=📺 哔哩哔哩,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Bilibili.list 31 | ruleset=🌏 国内媒体,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaMedia.list 32 | ruleset=🌍 国外媒体,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyMedia.list 33 | ruleset=🚀 节点选择,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyGFWlist.list 34 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaDomain.list 35 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaCompanyIp.list 36 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Download.list 37 | ruleset=🎯 全球直连,[]GEOIP,CN 38 | ruleset=🐟 漏网之鱼,[]MATCH 39 | `.trim(); 40 | 41 | // 解析规则字符串为数组 42 | export function parseRules(rulesStr) { 43 | return rulesStr 44 | .split('\n') 45 | .filter(line => line && !line.startsWith(';')) // 过滤空行和注释 46 | .map(line => { 47 | const [_, name, url] = line.match(/ruleset=([^,]+),(.+)/); 48 | // 处理特殊格式的规则 49 | if (url.startsWith('[]')) { 50 | const ruleName = url.substring(2); // 去掉开头的 [] 51 | return { 52 | name, 53 | url: url, 54 | displayName: ruleName // 使用 [] 后面的部分作为显示名称 55 | }; 56 | } 57 | // 普通 URL 规则 58 | return { 59 | name, 60 | url, 61 | displayName: url.split('/').pop() // 使用 URL 的最后一部分作为显示名称 62 | }; 63 | }); 64 | } 65 | 66 | // 分流规则排序配置 67 | export const ROUTING_ORDER = [ 68 | "💰 加密货币", 69 | "📲 电报消息", 70 | "💬 OpenAi", 71 | "📹 油管视频", 72 | "🎥 奈飞视频", 73 | "🌍 国外媒体", 74 | "🌏 国内媒体", 75 | "📢 谷歌FCM", 76 | "Ⓜ️ 微软云盘", 77 | "Ⓜ️ 微软服务", 78 | "🎮 游戏平台", 79 | "🎯 全球直连", 80 | "🛑 广告拦截", 81 | "🍃 应用净化", 82 | "🆎 AdBlock", 83 | "🐟 漏网之鱼" 84 | ]; 85 | 86 | // 排序函数 87 | export function sortRouting(routing) { 88 | return [...routing].sort((a, b) => { 89 | const aIndex = ROUTING_ORDER.indexOf(a.name); 90 | const bIndex = ROUTING_ORDER.indexOf(b.name); 91 | 92 | if (aIndex !== -1 && bIndex !== -1) { 93 | return aIndex - bIndex; 94 | } 95 | 96 | if (aIndex !== -1) return 1; 97 | if (bIndex !== -1) return -1; 98 | 99 | return 0; 100 | }); 101 | } -------------------------------------------------------------------------------- /singbox.js: -------------------------------------------------------------------------------- 1 | import Parser from './parser.js'; 2 | 3 | // 在文件顶部添加规则类型定义 4 | const RULE_TYPES = { 5 | CLASH_MODE: 'clash_mode', 6 | GEOIP: 'geoip', 7 | FINAL: 'final', 8 | PROTOCOL: 'protocol' 9 | }; 10 | 11 | // 在文件顶部添加常量配置 12 | const URL_TEST_CONFIG = { 13 | TEST_URL: 'http://www.gstatic.com/generate_204', 14 | BACKUP_TEST_URL: 'https://cp.cloudflare.com/generate_204', 15 | INTERVAL: '300s', 16 | TOLERANCE: 50, 17 | }; 18 | 19 | // 基础配置 20 | const BASE_CONFIG = { 21 | dns: { 22 | servers: [ 23 | { 24 | tag: "dns_proxy", 25 | address: "https://1.1.1.1/dns-query", 26 | detour: "proxy" 27 | }, 28 | { 29 | tag: "dns_direct", 30 | address: "https://223.5.5.5/dns-query", 31 | detour: "direct" 32 | }, 33 | { 34 | tag: "dns_block", 35 | address: "rcode://success" 36 | }, 37 | { 38 | tag: "dns_fakeip", 39 | address: "fakeip" 40 | } 41 | ], 42 | rules: [ 43 | { 44 | geosite: ["category-ads-all"], 45 | server: "dns_block", 46 | disable_cache: true 47 | }, 48 | { 49 | geosite: ["geolocation-!cn"], 50 | query_type: ["A", "AAAA"], 51 | server: "dns_fakeip" 52 | }, 53 | { 54 | geosite: ["geolocation-!cn"], 55 | server: "dns_proxy" 56 | } 57 | ], 58 | final: "dns_direct", 59 | independent_cache: true, 60 | fakeip: { 61 | enabled: true, 62 | inet4_range: "198.18.0.0/15" 63 | } 64 | }, 65 | ntp: { 66 | enabled: true, 67 | server: "time.apple.com", 68 | server_port: 123, 69 | interval: "30m", 70 | detour: "direct" 71 | }, 72 | inbounds: [ 73 | { 74 | type: "mixed", 75 | tag: "mixed-in", 76 | listen: "0.0.0.0", 77 | listen_port: 2080 78 | }, 79 | { 80 | type: "tun", 81 | tag: "tun-in", 82 | inet4_address: "172.19.0.1/30", 83 | auto_route: true, 84 | strict_route: true, 85 | stack: "system", 86 | sniff: true 87 | } 88 | ] 89 | }; 90 | 91 | // 设置默认模板URL和环境变量处理 92 | const getTemplateUrl = (env) => { 93 | return env?.DEFAULT_TEMPLATE_URL || 'https://raw.githubusercontent.com/Troywww/singbox_conf/refs/heads/main/singbox_clash_conf.txt'; 94 | }; 95 | 96 | export async function handleSingboxRequest(request, env) { 97 | try { 98 | const url = new URL(request.url); 99 | const directUrl = url.searchParams.get('url'); 100 | const templateUrl = url.searchParams.get('template') || getTemplateUrl(env); 101 | 102 | // 检测用户平台 103 | const userAgent = request.headers.get('User-Agent') || ''; 104 | const isApplePlatform = userAgent.includes('iPhone') || 105 | userAgent.includes('iPad') || 106 | userAgent.includes('Macintosh') || 107 | userAgent.includes('SFI/'); 108 | 109 | // 检查必需的URL参数 110 | let nodes = []; 111 | if (directUrl) { 112 | nodes = await Parser.parse(directUrl, env); 113 | } else { 114 | return new Response('Missing required parameters', { status: 400 }); 115 | } 116 | 117 | if (!nodes || nodes.length === 0) { 118 | return new Response('No valid nodes found', { status: 400 }); 119 | } 120 | 121 | // 获取模板配置 122 | const templateResponse = await fetch(templateUrl); 123 | 124 | // 检查是否是内部模板URL 125 | let templateContent; 126 | if (templateUrl.startsWith('https://inner.template.secret/id-')) { 127 | const templateId = templateUrl.replace('https://inner.template.secret/id-', ''); 128 | const templateData = await env.TEMPLATE_CONFIG.get(templateId); 129 | if (!templateData) { 130 | return new Response('Template not found', { status: 404 }); 131 | } 132 | const templateInfo = JSON.parse(templateData); 133 | templateContent = templateInfo.content; 134 | } else { 135 | if (!templateResponse.ok) { 136 | return new Response('Failed to fetch template', { status: 500 }); 137 | } 138 | templateContent = await templateResponse.text(); 139 | } 140 | 141 | // 生成完整的 Singbox 配置 142 | const config = await generateSingboxConfig(templateContent, nodes, isApplePlatform); 143 | 144 | return new Response(JSON.stringify(config, null, 2), { 145 | headers: { 'Content-Type': 'application/json' } 146 | }); 147 | } catch (error) { 148 | console.error('Singbox convert error:', error); 149 | return new Response('Internal Server Error: ' + error.message, { status: 500 }); 150 | } 151 | } 152 | 153 | // 修改 generateSingboxConfig 函数以支持苹果平台参数 154 | async function generateSingboxConfig(templateContent, proxies, isApplePlatform) { 155 | // 首先将节点转换为 Singbox 格式 156 | const singboxNodes = proxies.map(node => ({ 157 | ...convertNodeToSingbox(node), 158 | tag: node.name // 确保保留原始名称作为tag 159 | })); 160 | 161 | // 解析分组规则 162 | const groups = parseGroups(templateContent); 163 | 164 | // 创建分组映射 165 | const groupOutbounds = {}; 166 | 167 | // 使用基础配置模板 168 | const config = { 169 | ...BASE_CONFIG, // 展开基础配置 170 | outbounds: [ 171 | ...singboxNodes, // 直接使用转换好的节点 172 | ...Object.entries(groups).map(([name, group]) => { 173 | const outboundsList = []; 174 | 175 | // 处理分组选项 176 | group.patterns.forEach(option => { 177 | if (option.startsWith('[]')) { 178 | const groupRef = option.slice(2); 179 | if (groupRef !== name) { 180 | outboundsList.push(groupRef); 181 | } 182 | } else if (option === 'DIRECT') { 183 | outboundsList.push('direct'); 184 | } else if (option === 'REJECT') { 185 | outboundsList.push('block'); 186 | } else if (!option.includes('http')) { 187 | const matchedNodes = matchProxies(singboxNodes, option); 188 | outboundsList.push(...matchedNodes.map(p => p.tag)); 189 | } 190 | }); 191 | 192 | return generateGroupOutbound(name, group, outboundsList); 193 | }), 194 | { 195 | type: 'direct', 196 | tag: 'direct' 197 | }, 198 | { 199 | type: 'block', 200 | tag: 'block' 201 | }, 202 | { 203 | type: 'dns', 204 | tag: 'dns-out' 205 | } 206 | ], 207 | route: {}, 208 | experimental: {}, 209 | }; 210 | 211 | const { rules, finalOutbound } = await generateRules(templateContent, groupOutbounds, isApplePlatform); 212 | config.route = { 213 | rules: rules, 214 | auto_detect_interface: true, 215 | final: finalOutbound 216 | }; 217 | config.experimental = {}; 218 | 219 | return config; 220 | } 221 | 222 | // 解析分组规则 223 | function parseGroups(template) { 224 | const groups = {}; 225 | const lines = template.split('\n'); 226 | 227 | for (const line of lines) { 228 | if (line.startsWith('custom_proxy_group=')) { 229 | const [name, ...parts] = line.slice('custom_proxy_group='.length).split('`'); 230 | const type = parts[0]; 231 | const patterns = parts.slice(1).filter(p => p && !p.includes('http')); 232 | 233 | groups[name] = { 234 | type, 235 | patterns, 236 | filter: patterns.map(pattern => { 237 | if (pattern === 'DIRECT') return null; 238 | if (pattern.startsWith('^') && pattern.endsWith('$')) { 239 | return new RegExp(pattern); 240 | } 241 | if (pattern.startsWith('(') && pattern.endsWith(')')) { 242 | return new RegExp(pattern.slice(1, -1)); 243 | } 244 | return pattern; 245 | }).filter(Boolean) 246 | }; 247 | } 248 | } 249 | 250 | return groups; 251 | } 252 | 253 | // 对代理进行分组 254 | function groupProxies(proxies, groups) { 255 | if (!proxies || !Array.isArray(proxies)) { 256 | return {}; 257 | } 258 | 259 | if (!groups || typeof groups !== 'object') { 260 | return {}; 261 | } 262 | 263 | const result = {}; 264 | 265 | for (const [name, group] of Object.entries(groups)) { 266 | if (!group || !Array.isArray(group.filter)) { 267 | result[name] = []; 268 | continue; 269 | } 270 | 271 | result[name] = proxies.filter(proxy => { 272 | if (!proxy || typeof proxy.tag !== 'string') { 273 | return false; 274 | } 275 | 276 | return group.filter.some(pattern => { 277 | if (!pattern) { 278 | return false; 279 | } 280 | 281 | if (pattern instanceof RegExp) { 282 | return pattern.test(proxy.tag); 283 | } 284 | 285 | if (typeof pattern === 'string') { 286 | return proxy.tag.includes(pattern); 287 | } 288 | 289 | return false; 290 | }); 291 | }); 292 | } 293 | 294 | return result; 295 | } 296 | 297 | // 匹配代理节点 298 | function matchProxies(proxies, pattern) { 299 | 300 | 301 | // 安全检查 302 | if (!proxies || !pattern || pattern === 'DIRECT' || pattern.startsWith('[]')) { 303 | return []; 304 | } 305 | 306 | // 确保 proxies 是数组 307 | if (!Array.isArray(proxies)) { 308 | return []; 309 | } 310 | 311 | // 过滤掉无效的代理节点 312 | const validProxies = proxies.filter(proxy => proxy && proxy.tag); 313 | 314 | // 处理否定查找模式 (?!...) 315 | if (pattern.includes('(?!')) { 316 | const [excludePattern, includePattern] = pattern.split(')).*$'); 317 | const exclude = excludePattern.substring(excludePattern.indexOf('.*(') + 3).split('|'); 318 | const include = includePattern ? includePattern.slice(1).split('|') : []; 319 | const result = validProxies.filter(proxy => { 320 | const isExcluded = exclude.some(keyword => { 321 | if (!keyword) return false; 322 | return proxy.tag.indexOf(keyword) !== -1; 323 | }); 324 | if (isExcluded) return false; 325 | 326 | return include.length === 0 || include.some(keyword => { 327 | if (!keyword) return false; 328 | return proxy.tag.indexOf(keyword) !== -1; 329 | }); 330 | }); 331 | return result; 332 | } 333 | // 处理普通正则表达式模式 334 | else if (pattern.startsWith('(') && pattern.endsWith(')')) { 335 | const keywords = pattern.slice(1, -1).split('|'); 336 | const result = validProxies.filter(proxy => 337 | keywords.some(keyword => proxy.tag.indexOf(keyword) !== -1) 338 | ); 339 | return result; 340 | } 341 | // 处理完整正则表达式 342 | else if (pattern.startsWith('^') || pattern.endsWith('$')) { 343 | try { 344 | const regex = new RegExp(pattern, 'i'); 345 | const result = validProxies.filter(proxy => regex.test(proxy.tag)); 346 | return result; 347 | } catch (e) { 348 | console.log('Regex error:', e.message); 349 | return []; 350 | } 351 | } 352 | // 普通字符串匹配 353 | else { 354 | const result = validProxies.filter(proxy => proxy.tag.indexOf(pattern) !== -1); 355 | return result; 356 | } 357 | } 358 | 359 | // 修改 generateGroupOutbound 360 | function generateGroupOutbound(name, group, outbounds) { 361 | // 如果 outbounds 为空,添加 direct 362 | if (outbounds.length === 0) { 363 | outbounds.push('direct'); 364 | } 365 | 366 | // 转换所有出站引用为小写 367 | const normalizedOutbounds = outbounds.map(out => { 368 | if (out === 'DIRECT') return 'direct'; 369 | if (out === 'REJECT') return 'block'; 370 | return out; 371 | }); 372 | 373 | const groupConfig = { 374 | type: group.type === 'url-test' ? 'urltest' : 'selector', 375 | tag: name, 376 | outbounds: normalizedOutbounds 377 | }; 378 | 379 | // 如果是 url-test 类型,添优化的测试配置 380 | if (group.type === 'url-test') { 381 | Object.assign(groupConfig, { 382 | url: URL_TEST_CONFIG.TEST_URL, 383 | interval: URL_TEST_CONFIG.INTERVAL, 384 | tolerance: URL_TEST_CONFIG.TOLERANCE, 385 | idle_timeout: URL_TEST_CONFIG.IDLE_TIMEOUT, 386 | interrupt_exist_connections: true 387 | }); 388 | } 389 | 390 | return groupConfig; 391 | } 392 | 393 | // 修改节点转换函数 394 | function convertNodeToSingbox(node) { 395 | const tag = node.name || `${node.type}-${node.server}:${node.port}`; 396 | 397 | switch (node.type) { 398 | case 'vmess': 399 | return { 400 | type: 'vmess', 401 | tag, 402 | server: node.server, 403 | server_port: node.port, 404 | uuid: node.settings.id, 405 | security: 'auto', 406 | alter_id: node.settings.aid || 0, 407 | global_padding: false, 408 | authenticated_length: true, 409 | multiplex: { 410 | enabled: false, 411 | protocol: 'smux', 412 | max_streams: 32 413 | }, 414 | tls: { 415 | enabled: !!node.settings.tls, 416 | server_name: node.settings.sni || node.settings.host || node.server, 417 | insecure: true, 418 | alpn: node.settings.alpn ? node.settings.alpn.split(',') : undefined 419 | }, 420 | transport: node.settings.net ? { 421 | type: node.settings.net, 422 | path: node.settings.path || '/', 423 | headers: node.settings.host ? { Host: node.settings.host } : undefined 424 | } : undefined 425 | }; 426 | 427 | case 'vless': 428 | const tlsEnabled = node.settings.security === 'tls' || node.settings.tls === 'tls'; 429 | return { 430 | type: 'vless', 431 | tag, 432 | server: node.server, 433 | server_port: node.port, 434 | uuid: node.settings.id, 435 | flow: node.settings.flow || '', 436 | tls: node.settings.security === 'reality' ? { 437 | enabled: true, 438 | server_name: node.settings.sni, 439 | reality: { 440 | enabled: true, 441 | public_key: node.settings.pbk, 442 | short_id: node.settings.sid || '', 443 | }, 444 | utls: { 445 | enabled: true, 446 | fingerprint: node.settings.fp || 'chrome' 447 | } 448 | } : tlsEnabled ? { 449 | enabled: true, 450 | server_name: node.settings.sni || node.settings.host || node.server, 451 | insecure: false, 452 | utls: { 453 | enabled: true, 454 | fingerprint: node.settings.fp || 'random' 455 | } 456 | } : undefined, 457 | transport: node.settings.type !== 'tcp' ? { 458 | type: node.settings.type || node.settings.net, 459 | path: node.settings.path || '/', 460 | headers: node.settings.host ? { Host: node.settings.host } : undefined 461 | } : undefined 462 | }; 463 | 464 | case 'trojan': 465 | return { 466 | tag: node.name, 467 | type: 'trojan', 468 | server: node.server, 469 | server_port: node.port, 470 | password: node.settings.password, 471 | transport: { 472 | type: node.settings.type || 'tcp', 473 | path: node.settings.path, 474 | headers: node.settings.host ? { Host: node.settings.host } : undefined 475 | }, 476 | tls: { 477 | enabled: true, 478 | server_name: node.settings.sni, 479 | insecure: true 480 | } 481 | }; 482 | 483 | case 'ss': 484 | return { 485 | tag: node.name, 486 | type: 'shadowsocks', 487 | server: node.server, 488 | server_port: node.port, 489 | method: node.settings.method, 490 | password: node.settings.password 491 | }; 492 | 493 | case 'ssr': 494 | return { 495 | tag: node.name, 496 | type: 'shadowsocksr', 497 | server: node.server, 498 | server_port: node.port, 499 | method: node.settings.method, 500 | password: node.settings.password, 501 | protocol: node.settings.protocol, 502 | protocol_param: node.settings.protocolParam, 503 | obfs: node.settings.obfs, 504 | obfs_param: node.settings.obfsParam 505 | }; 506 | 507 | case 'hysteria': 508 | return { 509 | tag: node.name, 510 | type: 'hysteria', 511 | server: node.server, 512 | server_port: node.port, 513 | auth_str: node.settings.auth, 514 | up_mbps: parseInt(node.settings.up), 515 | down_mbps: parseInt(node.settings.down), 516 | tls: { 517 | enabled: true, 518 | server_name: node.settings.sni, 519 | insecure: true, 520 | alpn: node.settings.alpn ? [node.settings.alpn] : undefined 521 | }, 522 | obfs: node.settings.obfs 523 | }; 524 | 525 | case 'hysteria2': 526 | return { 527 | tag: node.name, 528 | type: 'hysteria2', 529 | server: node.server, 530 | server_port: node.port, 531 | password: node.settings.auth, 532 | tls: { 533 | enabled: true, 534 | server_name: node.settings.sni, 535 | insecure: true 536 | }, 537 | obfs: { 538 | type: node.settings.obfs, 539 | password: node.settings.obfsParam 540 | } 541 | }; 542 | 543 | case 'tuic': 544 | return { 545 | type: 'tuic', 546 | tag, 547 | server: node.server, 548 | server_port: node.port, 549 | uuid: node.settings.uuid, 550 | password: node.settings.password, 551 | congestion_control: node.settings.congestion_control || 'bbr', 552 | udp_relay_mode: node.settings.udp_relay_mode || 'native', 553 | zero_rtt_handshake: node.settings.reduce_rtt || false, 554 | tls: { 555 | enabled: true, 556 | server_name: node.settings.sni || node.server, 557 | alpn: node.settings.alpn || ['h3'], 558 | disable_sni: node.settings.disable_sni || false 559 | } 560 | }; 561 | 562 | default: 563 | return null; 564 | } 565 | } 566 | 567 | // 修改 generateRules 函数 568 | async function generateRules(template, groupOutbounds, isApplePlatform) { 569 | // 首先检查参数 570 | if (!template) { 571 | return { rules: [], finalOutbound: 'direct' }; 572 | } 573 | 574 | const rules = [ 575 | { 576 | [RULE_TYPES.CLASH_MODE]: "Global", 577 | outbound: "🚀 节点选择" 578 | }, 579 | { 580 | [RULE_TYPES.CLASH_MODE]: "Direct", 581 | outbound: "direct" 582 | }, 583 | { 584 | [RULE_TYPES.PROTOCOL]: "dns", 585 | outbound: "dns-out" 586 | } 587 | ]; 588 | 589 | let finalOutbound = 'direct'; 590 | 591 | // 确保模板内容是字符串并且包含规则 592 | const ruleLines = template.split('\n') 593 | .filter(line => line && typeof line === 'string' && line.startsWith('ruleset=')) 594 | .map(line => line.trim()); 595 | 596 | for (const line of ruleLines) { 597 | // 确保规则行格式正确 598 | if (!line.includes(',')) { 599 | continue; 600 | } 601 | 602 | const [group, ...urlParts] = line.slice('ruleset='.length).split(','); 603 | const url = urlParts.join(','); 604 | 605 | // 确保 group 存在 606 | if (!group) { 607 | continue; 608 | } 609 | 610 | const outbound = group === 'DIRECT' ? 'direct' : 611 | group === 'REJECT' ? 'block' : 612 | group; 613 | 614 | if (url.startsWith('[]')) { 615 | const ruleContent = url.slice(2); 616 | if (!ruleContent) { 617 | continue; 618 | } 619 | 620 | if (ruleContent.startsWith('GEOIP,')) { 621 | const [, geoipValue] = ruleContent.split(','); 622 | if (geoipValue) { 623 | rules.push({ 624 | geoip: [geoipValue.toLowerCase()], 625 | outbound: outbound 626 | }); 627 | } 628 | } else if (ruleContent === 'MATCH' || ruleContent === 'FINAL') { 629 | finalOutbound = outbound; 630 | } 631 | } else { 632 | try { 633 | const rulesByType = { 634 | domain: new Set(), 635 | domain_suffix: new Set(), 636 | domain_keyword: new Set(), 637 | ip_cidr: new Set(), 638 | ...(isApplePlatform ? {} : { process_name: new Set() }) 639 | }; 640 | 641 | const response = await fetch(url); 642 | if (!response.ok) { 643 | continue; 644 | } 645 | 646 | const ruleContent = await response.text(); 647 | 648 | ruleContent.split('\n') 649 | .map(rule => rule && rule.trim()) 650 | .filter(rule => rule && !rule.startsWith('#')) 651 | .forEach(rule => { 652 | const [type, ...valueParts] = rule.split(','); 653 | const value = valueParts.join(','); 654 | 655 | if (!type || !value) { 656 | return; 657 | } 658 | 659 | switch (type) { 660 | case 'DOMAIN-SUFFIX': 661 | rulesByType.domain_suffix.add(value); 662 | break; 663 | case 'DOMAIN': 664 | rulesByType.domain.add(value); 665 | break; 666 | case 'DOMAIN-KEYWORD': 667 | rulesByType.domain_keyword.add(value); 668 | break; 669 | case 'IP-CIDR': 670 | case 'IP-CIDR6': 671 | rulesByType.ip_cidr.add(value.split(',')[0]); 672 | break; 673 | case 'PROCESS-NAME': 674 | if (!isApplePlatform && rulesByType.process_name) { 675 | rulesByType.process_name.add(value); 676 | } 677 | break; 678 | } 679 | }); 680 | 681 | for (const [type, values] of Object.entries(rulesByType)) { 682 | if (values.size > 0) { 683 | rules.push({ 684 | [type]: Array.from(values), 685 | outbound 686 | }); 687 | } 688 | } 689 | } catch (error) { 690 | console.error(`Error processing rule list ${url}:`, error); 691 | } 692 | } 693 | } 694 | 695 | return { rules, finalOutbound }; 696 | } -------------------------------------------------------------------------------- /style.js: -------------------------------------------------------------------------------- 1 | export const styleCSS = ` 2 | /* 模板列表特定样式 */ 3 | .template-items { 4 | max-height: 200px; 5 | overflow-y: auto; 6 | } 7 | 8 | .template-item { 9 | cursor: pointer; 10 | } 11 | 12 | .template-item.selected { 13 | background-color: #e3f2fd !important; 14 | } 15 | 16 | /* 错误和加载状态样式 */ 17 | .error { 18 | color: #c62828; 19 | text-align: center; 20 | padding: 1rem; 21 | } 22 | 23 | .loading { 24 | text-align: center; 25 | color: #666; 26 | padding: 1rem; 27 | } 28 | 29 | .empty { 30 | color: #666; 31 | text-align: center; 32 | padding: 1rem; 33 | } 34 | 35 | /* 结果区域样式 */ 36 | .result { 37 | display: none; 38 | } 39 | 40 | /* 确保链接可以换行 */ 41 | .link-url { 42 | word-break: break-all; 43 | } 44 | `; -------------------------------------------------------------------------------- /template_list/Private_Line_Groups.txt: -------------------------------------------------------------------------------- 1 | ;设置规则标志位 2 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/LocalAreaNetwork.list 3 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/UnBan.list 4 | ruleset=🛑 广告拦截,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanAD.list 5 | ruleset=🍃 应用净化,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanProgramAD.list 6 | ruleset=🆎 AdBlock,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyList.list 7 | ruleset=🆎 AdBlock,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyListChina.list 8 | ;ruleset=🛡️ 隐私防护,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyPrivacy.list 9 | ruleset=📢 谷歌FCM,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/GoogleFCM.list 10 | ruleset=💰 加密货币,https://raw.githubusercontent.com/Troywww/singbox_conf/refs/heads/main/rules_list/crypto.list 11 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/GoogleCN.list 12 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/SteamCN.list 13 | ruleset=Ⓜ️ 微软Bing,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Bing.list 14 | ruleset=Ⓜ️ 微软云盘,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/OneDrive.list 15 | ruleset=Ⓜ️ 微软服务,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Microsoft.list 16 | ruleset=🍎 苹果服务,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Apple.list 17 | ruleset=📲 电报消息,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Telegram.list 18 | ruleset=💬 OpenAi,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/OpenAi.list 19 | ;ruleset=🎶 网易音乐,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/NetEaseMusic.list 20 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Epic.list 21 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Origin.list 22 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Sony.list 23 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Steam.list 24 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Nintendo.list 25 | ruleset=📹 油管视频,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/YouTube.list 26 | ruleset=🎥 奈飞视频,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Netflix.list 27 | ;ruleset=📺 巴哈姆特,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Bahamut.list 28 | ;ruleset=📺 哔哩哔哩,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/BilibiliHMT.list 29 | ;ruleset=📺 哔哩哔哩,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Bilibili.list 30 | ruleset=🌏 国内媒体,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaMedia.list 31 | ruleset=🌍 国外媒体,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyMedia.list 32 | ruleset=🚀 节点选择,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyGFWlist.list 33 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaDomain.list 34 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaCompanyIp.list 35 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Download.list 36 | ;ruleset=🎯 全球直连,[]GEOIP,LAN 37 | ruleset=🎯 全球直连,[]GEOIP,CN 38 | ruleset=🐟 漏网之鱼,[]MATCH 39 | ;设置规则标志位 40 | 41 | ;设置分组标志位 42 | custom_proxy_group=🚀 节点选择`select`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 43 | custom_proxy_group=✈️ 专线自动`url-test`(专线|特线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路)`http://www.gstatic.com/generate_204`300,,50 44 | custom_proxy_group=📡 专线手动`select`(专线|特线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路) 45 | custom_proxy_group=🌐 临时自动`url-test`^(?!.*(特线|专线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路)).*$`http://www.gstatic.com/generate_204`300,,50 46 | custom_proxy_group=🔄 临时手动`select`^(?!.*(特线|专线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路)).*$ 47 | custom_proxy_group=🌏 亚洲节点`url-test`^(?!.*(特线|专线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路)).*$(港|HK|Hong Kong|日本|川日|东京|大阪|泉日|埼玉|沪日|深日|JP|Japan|台|新北|彰化|TW|Taiwan|新加坡|坡|狮城|SG|Singapore|KR|Korea|KOR|首尔|韩|韓)`http://www.gstatic.com/generate_204`300,,50 48 | custom_proxy_group=🌍 欧美节点`url-test`^(?!.*(特线|专线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路)).*$(美|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥|Europe|US|United States|DE|德|德国|FR|法|法国|GB|英|英国)`http://www.gstatic.com/generate_204`300,,150 49 | custom_proxy_group=🌎 未知区域`select`^(?!.*(特线|专线|IPLC|IEPL|BGP|CN2|AIA|CTM|CEN|CMI|AZURE|Azure|专项|普通|普线|普通线路|港|HK|Hong Kong|日本|川日|东京|大阪|泉日|埼玉|沪日|深日|JP|Japan|台|新北|彰化|TW|Taiwan|新加坡|坡|狮城|SG|Singapore|KR|Korea|KOR|首尔|韩|韓|美|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥|US|United States|DE|德|德国|FR|法|法国|Europe|GB|英|英国)).*$ 50 | custom_proxy_group=💰 加密货币`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 51 | custom_proxy_group=📲 电报消息`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 52 | custom_proxy_group=💬 OpenAi`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 53 | custom_proxy_group=📹 油管视频`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 54 | custom_proxy_group=🎥 奈飞视频`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 55 | ;custom_proxy_group=📺 巴哈姆特`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 56 | ;custom_proxy_group=📺 哔哩哔哩`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 57 | custom_proxy_group=🌍 国外媒体`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 58 | custom_proxy_group=🌏 国内媒体`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 59 | custom_proxy_group=📢 谷歌FCM`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 60 | custom_proxy_group=Ⓜ️ 微软Bing`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 61 | custom_proxy_group=Ⓜ️ 微软云盘`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 62 | custom_proxy_group=Ⓜ️ 微软服务`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 63 | custom_proxy_group=🍎 苹果服务`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 64 | custom_proxy_group=🎮 游戏平台`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 65 | ;custom_proxy_group=🎶 网易音乐`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`(网易|音乐|解锁|Music|NetEase) 66 | custom_proxy_group=🎯 全球直连`select`[]DIRECT`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域 67 | custom_proxy_group=🛑 广告拦截`select`[]REJECT`[]DIRECT 68 | custom_proxy_group=🍃 应用净化`select`[]REJECT`[]DIRECT 69 | custom_proxy_group=🆎 AdBlock`select`[]REJECT`[]DIRECT 70 | ;custom_proxy_group=🛡️ 隐私防护`select`[]REJECT`[]DIRECT 71 | custom_proxy_group=🐟 漏网之鱼`select`[]🚀 节点选择`[]✈️ 专线自动`[]📡 专线手动`[]🌐 临时自动`[]🔄 临时手动`[]🌏 亚洲节点`[]🌍 欧美节点`[]🌎 未知区域`[]DIRECT 72 | ;设置分组标志位 73 | -------------------------------------------------------------------------------- /template_list/Universal_Country_Groups.txt: -------------------------------------------------------------------------------- 1 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/LocalAreaNetwork.list 2 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/UnBan.list 3 | ruleset=🛑 广告拦截,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanAD.list 4 | ruleset=💰 加密货币,https://raw.githubusercontent.com/Troywww/singbox_conf/refs/heads/main/rules_list/crypto.list 5 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/GoogleCN.list 6 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/SteamCN.list 7 | ruleset=Ⓜ️ 微软Bing,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Bing.list 8 | ruleset=Ⓜ️ 微软云盘,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/OneDrive.list 9 | ruleset=Ⓜ️ 微软服务,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Microsoft.list 10 | ruleset=🍎 苹果服务,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Apple.list 11 | ruleset=📲 电报消息,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Telegram.list 12 | ruleset=💬 OpenAi,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/OpenAi.list 13 | ruleset=🎶 网易音乐,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/NetEaseMusic.list 14 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Epic.list 15 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Origin.list 16 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Sony.list 17 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Steam.list 18 | ruleset=🎮 游戏平台,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Nintendo.list 19 | ruleset=📹 油管视频,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/YouTube.list 20 | ruleset=🎥 奈飞视频,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Netflix.list 21 | ruleset=📺 巴哈姆特,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Bahamut.list 22 | ruleset=📺 哔哩哔哩,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/BilibiliHMT.list 23 | ruleset=📺 哔哩哔哩,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/Bilibili.list 24 | ruleset=🌏 国内媒体,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaMedia.list 25 | ruleset=🌍 国外媒体,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyMedia.list 26 | ruleset=🚀 节点选择,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyGFWlist.list 27 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaDomain.list 28 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaCompanyIp.list 29 | ruleset=🎯 全球直连,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Download.list 30 | ruleset=🎯 全球直连,[]GEOIP,CN 31 | ruleset=🐟 漏网之鱼,[]MATCH 32 | ruleset=📢 谷歌FCM,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Ruleset/GoogleFCM.list 33 | ruleset=🆎 AdBlock,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyListChina.list 34 | 35 | custom_proxy_group=🚀 节点选择`select`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 36 | custom_proxy_group=🖱️ 手动选择`select`() 37 | custom_proxy_group=🤖 自动选择`select`() 38 | custom_proxy_group=🌏 东南亚`url-test`(Thailand|Singapore|Malaysia|Indonesia|Vietnam|Philippines|Japan|Korea|TH|SG|MY|ID|VN|PH|JP|KR|泰国|新加坡|马来西亚|印度尼西亚|越南|菲律宾|日本|韩国)`http://www.gstatic.com/generate_204`300,,50 39 | custom_proxy_group=🏮 香港`url-test`(Hong Kong|HongKong|HK|香港)`http://www.gstatic.com/generate_204`300,,50 40 | custom_proxy_group=🗽 美国`url-test`(United States|USA|US|America|美国|美利坚)`http://www.gstatic.com/generate_204`300,,50 41 | custom_proxy_group=🏰 欧洲`url-test`(Germany|France|UK|Italy|Spain|Netherlands|Switzerland|Sweden|Norway|Denmark|Finland|Belgium|Austria|Ireland|Portugal|Greece|Poland|Czech Republic|Hungary|Romania|DE|FR|GB|IT|ES|NL|CH|SE|NO|DK|FI|BE|AT|IE|PT|GR|PL|CZ|HU|RO|德国|法国|英国|意大利|西班牙|荷兰|瑞士|瑞典|挪威|丹麦|芬兰|比利时|奥地利|爱尔兰|葡萄牙|希腊|波兰|捷克|匈牙利|罗马尼亚)`http://www.gstatic.com/generate_204`300,,50 42 | custom_proxy_group=❓ 未知区域`select`^(?!.*(Thailand|Singapore|Malaysia|Indonesia|Vietnam|Philippines|Japan|Korea|TH|SG|MY|ID|VN|PH|JP|KR|泰国|新加坡|马来西亚|印度尼西亚|越南|菲律宾|日本|韩国|Hong Kong|HongKong|HK|香港|United States|USA|US|America|美国|美利坚|Germany|France|UK|Italy|Spain|Netherlands|Switzerland|Sweden|Norway|Denmark|Finland|Belgium|Austria|Ireland|Portugal|Greece|Poland|Czech Republic|Hungary|Romania|DE|FR|GB|IT|ES|NL|CH|SE|NO|DK|FI|BE|AT|IE|PT|GR|PL|CZ|HU|RO|德国|法国|英国|意大利|西班牙|荷兰|瑞士|瑞典|挪威|丹麦|芬兰|比利时|奥地利|爱尔兰|葡萄牙|希腊|波兰|捷克|匈牙利|罗马尼亚)).*$ 43 | 44 | custom_proxy_group=Ⓜ️ 微软Bing`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 45 | custom_proxy_group=🍎 苹果服务`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 46 | custom_proxy_group=🎶 网易音乐`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 47 | custom_proxy_group=📺 巴哈姆特`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 48 | custom_proxy_group=📺 哔哩哔哩`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 49 | custom_proxy_group=💰 加密货币`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 50 | custom_proxy_group=📲 电报消息`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 51 | custom_proxy_group=💬 OpenAi`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 52 | custom_proxy_group=📹 油管视频`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 53 | custom_proxy_group=🎥 奈飞视频`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 54 | custom_proxy_group=🌍 国外媒体`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 55 | custom_proxy_group=🌏 国内媒体`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 56 | custom_proxy_group=📢 谷歌FCM`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 57 | custom_proxy_group=Ⓜ️ 微软云盘`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 58 | custom_proxy_group=Ⓜ️ 微软服务`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 59 | custom_proxy_group=🎮 游戏平台`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 60 | custom_proxy_group=🎯 全球直连`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT 61 | custom_proxy_group=🛑 广告拦截`select`[]REJECT`[]DIRECT 62 | custom_proxy_group=🆎 AdBlock`select`[]REJECT`[]DIRECT 63 | custom_proxy_group=🐟 漏网之鱼`select`[]🚀 节点选择`[]🖱️ 手动选择`[]🤖 自动选择`[]🌏 东南亚`[]🏮 香港`[]🗽 美国`[]🏰 欧洲`[]❓ 未知区域`[]DIRECT -------------------------------------------------------------------------------- /template_list/key_words.txt: -------------------------------------------------------------------------------- 1 | #常用关键词筛选 2 | 3 | # 选择模式 4 | 🤖 自动选择 (Auto Select) 5 | 🖱️ 手动选择 (Manual Select) 6 | 7 | # 地区分组关键词 8 | 9 | 🌏 东南亚:Thailand|Singapore|Malaysia|Indonesia|Vietnam|Philippines|Japan|Korea|TH|SG|MY|ID|VN|PH|JP|KR|泰国|新加坡|马来西亚|印度尼西亚|越南|菲律宾|日本|韩国|新日本|日本高速|新加坡高速|狮城|韩国高速|新韩国 10 | 11 | 🏮 香港:Hong Kong|HongKong|HK|香港|HKBGP|HKT|HKBN|WTT|CMI|港|深港|沪港|京港 12 | 13 | 🇨🇳 台湾:Taiwan|TW|台湾|台北|台中|高雄|台|新台|TWN 14 | 15 | 🇯🇵 日本:Japan|JP|JPN|日本|东京|大阪|东京|京都|名古屋|Tokyo|Osaka|Kyoto|Nagoya|日|日特|日高|新日本|JP|日本高速 16 | 17 | 🇰🇷 韩国:Korea|South Korea|KR|KOR|韩国|首尔|釜山|仁川|Seoul|Busan|Incheon|韩|韩特|韩高|新韩国|南韩|KR|韩国高速 18 | 19 | 🇸🇬 新加坡:Singapore|SG|SGP|新加坡|狮城|新加坡高速|新|新特|新高|SG|新加坡特快|新加坡IEPL 20 | 21 | 🇹🇭 泰国:Thailand|TH|THA|泰国|曼谷|清迈|普吉|Bangkok|Chiangmai|Phuket|泰|泰特|泰高 22 | 23 | 🇲🇾 马来西亚:Malaysia|MY|MYS|马来西亚|吉隆坡|马|马特|马高|Kuala Lumpur|KUL 24 | 25 | 🇻🇳 越南:Vietnam|VN|VNM|越南|胡志明市|河内|Hanoi|Ho Chi Minh|越|越特|越高 26 | 27 | 🇵🇭 菲律宾:Philippines|PH|PHL|菲律宾|马尼拉|宿务|Manila|Cebu|菲|菲特|菲高 28 | 29 | 🇮🇩 印尼:Indonesia|ID|IDN|印度尼西亚|印尼|雅加达|泗水|Jakarta|Surabaya|印|印特|印高 30 | 31 | 🌏 亚洲整体:Asia|亚洲|亚洲高速|Japan|Korea|Singapore|Thailand|Malaysia|Vietnam|Philippines|Indonesia|JP|KR|SG|TH|MY|VN|PH|ID|日本|韩国|新加坡|泰国|马来西亚|越南|菲律宾|印度尼西亚|狮城|东京|首尔|曼谷|吉隆坡 32 | 33 | 🗽 美国:United States|USA|US|America|美国|美利坚|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|华盛顿|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥|US|USD|美|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|华盛顿|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥 34 | 35 | 🏰 欧洲:Germany|France|UK|Italy|Spain|Netherlands|Switzerland|Sweden|Norway|Denmark|Finland|Belgium|Austria|Ireland|Portugal|Greece|Poland|Czech Republic|Hungary|Romania|DE|FR|GB|IT|ES|NL|CH|SE|NO|DK|FI|BE|AT|IE|PT|GR|PL|CZ|HU|RO|德国|法国|英国|意大利|西班牙|荷兰|瑞士|瑞典|挪威|丹麦|芬兰|比利时|奥地利|爱尔兰|葡萄牙|希腊|波兰|捷克|匈牙利|罗马尼亚|伦敦|巴黎|法兰克福|阿姆斯特丹 36 | 37 | 🇬🇧 英国:United Kingdom|UK|Britain|England|GB|GBR|英国|英格兰|伦敦|曼彻斯特|利物浦|LONDON|伦敦高速 38 | 39 | 🇩🇪 德国:Germany|DE|DEU|德国|法兰克福|柏林|慕尼黑|德|新德|Frankfurt|Berlin|Munich|德特|德高|德法 40 | 41 | 🇫🇷 法国:France|FR|FRA|法国|巴黎|里昂|马赛|Paris|法|巴黎高速 42 | 43 | 🇮🇹 意大利:Italy|IT|ITA|意大利|罗马|米兰|威尼斯|Rome|Milan|意|意高速 44 | 45 | 🇳🇱 荷兰:Netherlands|NL|NLD|荷兰|阿姆斯特丹|鹿特丹|Amsterdam|Rotterdam|荷兰高速 46 | 47 | 🇨🇭 瑞士:Switzerland|CH|CHE|瑞士|苏黎世|日内瓦|伯尔尼|Zurich|Geneva|瑞士高速 48 | 49 | 🏰 欧洲整体:Europe|EU|欧洲|欧盟|Europe|欧洲高速|Germany|France|UK|Italy|Spain|Netherlands|Switzerland|Sweden|Norway|Denmark|Finland|Belgium|Austria|Ireland|Portugal|Greece|Poland|Czech Republic|Hungary|Romania|DE|FR|GB|IT|ES|NL|CH|SE|NO|DK|FI|BE|AT|IE|PT|GR|PL|CZ|HU|RO|德国|法国|英国|意大利|西班牙|荷兰|瑞士|瑞典|挪威|丹麦|芬兰|比利时|奥地利|爱尔兰|葡萄牙|希腊|波兰|捷克|匈牙利|罗马尼亚 50 | 51 | 🇨🇦 加拿大:Canada|CA|CAN|加拿大|蒙特利尔|温哥华|多伦多 52 | 53 | 🇦🇺 澳大利亚:Australia|AU|AUS|澳大利亚|澳洲|悉尼|墨尔本|布里斯班 54 | 55 | 🇮🇳 印度:India|IN|IND|印度|孟买|新德里|加尔各答 56 | 57 | 🇷🇺 俄罗斯:Russia|RU|RUS|俄罗斯|莫斯科|圣彼得堡 58 | 59 | 🌍 中东:Dubai|United Arab Emirates|UAE|Saudi Arabia|Qatar|迪拜|阿联酋|沙特|卡塔尔 60 | 61 | 🇧🇷 南美:Brazil|Argentina|Chile|巴西|阿根廷|智利|圣保罗|布宜诺斯艾利斯 62 | 63 | 🇿🇦 非洲:South Africa|Nigeria|Egypt|南非|约翰内斯堡|开普敦|开罗 64 | 65 | # 特殊类型 66 | ⚡ 专线:IEPL|IPLC|EIP|CN2|BGP|GIA|AIA|CTM|CEN|CMI|NTT|专线|高速|高级|精品|特殊 67 | 68 | 🚀 游戏:Game|Gaming|游戏|游戏专用|GAME|GAMING 69 | 70 | 🎬 流媒体:Netflix|Disney|Stream|Streaming|Media|流媒体|串流|解锁 71 | 72 | ❓ 未知区域:[以上所有关键词的否定形式] -------------------------------------------------------------------------------- /tempmanager.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_RULES, ROUTING_ORDER, sortRouting, parseRules } from './rules.js'; 2 | 3 | // 生成配置处理 4 | async function handleGenerateConfig(request, env) { 5 | try { 6 | // 检查密码 7 | const data = await request.json(); 8 | if (!data.password || data.password !== env.TEMPLATE_PASSWORD) { 9 | return new Response(JSON.stringify({ 10 | success: false, 11 | message: '密码错误或未提供密码' 12 | }), { 13 | headers: { 'content-type': 'application/json' }, 14 | status: 403 15 | }); 16 | } 17 | 18 | // 检查 KV 绑定 19 | if (!env) { 20 | console.error('env object is undefined'); 21 | return new Response(JSON.stringify({ 22 | success: false, 23 | message: 'Worker 环境变量未正确配置' 24 | }), { 25 | headers: { 'content-type': 'application/json' }, 26 | status: 500 27 | }); 28 | } 29 | 30 | if (!env.TEMPLATE_CONFIG) { 31 | console.error('TEMPLATE_CONFIG binding is missing'); 32 | console.log('Available bindings:', Object.keys(env)); 33 | return new Response(JSON.stringify({ 34 | success: false, 35 | message: 'KV 存储未正确绑定' 36 | }), { 37 | headers: { 'content-type': 'application/json' }, 38 | status: 500 39 | }); 40 | } 41 | 42 | const { password, ...configData } = data; 43 | console.log('Received data:', configData); 44 | 45 | if (!configData.templateName) { 46 | return new Response(JSON.stringify({ 47 | success: false, 48 | message: '模板名称不能为空' 49 | }), { 50 | headers: { 51 | 'content-type': 'application/json', 52 | }, 53 | status: 400 54 | }); 55 | } 56 | 57 | const template = generateTemplate(configData); 58 | 59 | const templateId = crypto.randomUUID(); 60 | const templateInfo = { 61 | name: configData.templateName, 62 | content: template, 63 | createTime: new Date().toISOString() 64 | }; 65 | 66 | // 使用 TextEncoder 确保正确的 UTF-8 编码 67 | const encoder = new TextEncoder(); 68 | const encodedContent = encoder.encode(JSON.stringify(templateInfo)); 69 | 70 | await env.TEMPLATE_CONFIG.put(templateId, encodedContent, { 71 | expirationTtl: undefined, 72 | metadata: { encoding: 'utf-8' } 73 | }); 74 | 75 | return new Response(JSON.stringify({ 76 | success: true, 77 | templateId, 78 | url: `/peizhi/template/${templateId}` 79 | }), { 80 | headers: { 81 | 'content-type': 'application/json', 82 | } 83 | }); 84 | } catch (error) { 85 | console.error('Generate config error:', error); 86 | return new Response(JSON.stringify({ 87 | success: false, 88 | message: `生成配置失败: ${error.message}`, 89 | debug: { 90 | hasEnv: !!env, 91 | hasTemplateConfig: !!env?.TEMPLATE_CONFIG, 92 | availableBindings: env ? Object.keys(env) : [] 93 | } 94 | }), { 95 | headers: { 'content-type': 'application/json' }, 96 | status: 500 97 | }); 98 | } 99 | } 100 | 101 | // 获取模板处理 102 | async function handleGetTemplate(request, url, env) { 103 | if (!env || !env.TEMPLATE_CONFIG) { 104 | return new Response(JSON.stringify({ 105 | success: false, 106 | message: 'KV storage not configured' 107 | }), { 108 | status: 500, 109 | headers: { 110 | 'content-type': 'application/json; charset=utf-8', 111 | 'access-control-allow-origin': '*' 112 | } 113 | }); 114 | } 115 | 116 | const templateId = url.pathname.split('/')[3]; 117 | const encodedContent = await env.TEMPLATE_CONFIG.get(templateId, 'arrayBuffer'); 118 | 119 | if (!encodedContent) { 120 | return new Response(JSON.stringify({ 121 | success: false, 122 | message: 'Template not found' 123 | }), { 124 | status: 404, 125 | headers: { 126 | 'content-type': 'application/json; charset=utf-8', 127 | 'access-control-allow-origin': '*' 128 | } 129 | }); 130 | } 131 | 132 | // 使用 TextDecoder 解码 133 | const decoder = new TextDecoder('utf-8'); 134 | const templateInfoStr = decoder.decode(encodedContent); 135 | const templateInfo = JSON.parse(templateInfoStr); 136 | 137 | return new Response(templateInfo.content, { 138 | headers: { 139 | 'content-type': 'text/plain; charset=utf-8', 140 | 'x-template-name': templateInfo.name, 141 | 'x-template-create-time': templateInfo.createTime, 142 | 'access-control-allow-origin': '*' 143 | }, 144 | }); 145 | } 146 | 147 | // 添加获取模板列表的功能 148 | async function handleListTemplates(request, env) { 149 | if (!env || !env.TEMPLATE_CONFIG) { 150 | return new Response('KV storage not configured', { status: 500 }); 151 | } 152 | 153 | const { keys } = await env.TEMPLATE_CONFIG.list(); 154 | const templates = []; 155 | const decoder = new TextDecoder('utf-8'); 156 | 157 | for (const key of keys) { 158 | const encodedContent = await env.TEMPLATE_CONFIG.get(key.name, 'arrayBuffer'); 159 | const templateInfoStr = decoder.decode(encodedContent); 160 | const templateInfo = JSON.parse(templateInfoStr); 161 | templates.push({ 162 | id: key.name, 163 | name: templateInfo.name, 164 | createTime: templateInfo.createTime 165 | }); 166 | } 167 | 168 | return new Response(JSON.stringify(templates), { 169 | headers: { 170 | 'content-type': 'application/json; charset=utf-8', 171 | 'access-control-allow-origin': '*', 172 | 'access-control-allow-methods': 'GET, OPTIONS', 173 | 'access-control-allow-headers': 'Content-Type' 174 | }, 175 | }); 176 | } 177 | 178 | // 添加删除模板的处理函数 179 | async function handleDeleteTemplate(request, url, env) { 180 | try { 181 | // 检查密码 182 | const { password } = await request.json(); 183 | if (!password || password !== env.TEMPLATE_PASSWORD) { 184 | return new Response(JSON.stringify({ 185 | success: false, 186 | message: '密码错误或未提供密码' 187 | }), { 188 | headers: { 'content-type': 'application/json' }, 189 | status: 403 190 | }); 191 | } 192 | 193 | if (!env || !env.TEMPLATE_CONFIG) { 194 | return new Response('KV storage not configured', { 195 | status: 500, 196 | headers: { 'content-type': 'application/json' } 197 | }); 198 | } 199 | 200 | const templateId = url.pathname.split('/')[4]; 201 | if (!templateId) { 202 | return new Response(JSON.stringify({ 203 | success: false, 204 | message: 'Invalid template ID' 205 | }), { 206 | status: 400, 207 | headers: { 'content-type': 'application/json' } 208 | }); 209 | } 210 | 211 | await env.TEMPLATE_CONFIG.delete(templateId); 212 | 213 | return new Response(JSON.stringify({ 214 | success: true, 215 | message: 'Template deleted successfully' 216 | }), { 217 | headers: { 'content-type': 'application/json' } 218 | }); 219 | } catch (error) { 220 | console.error('Delete template error:', error); 221 | return new Response(JSON.stringify({ 222 | success: false, 223 | message: 'Failed to delete template' 224 | }), { 225 | status: 500, 226 | headers: { 'content-type': 'application/json' } 227 | }); 228 | } 229 | } 230 | 231 | // 生成配置模板 232 | function generateTemplate(config) { 233 | const { rules, proxyGroups, routing } = config; 234 | let template = ''; 235 | 236 | // 1. 生成规则部分 237 | template += rules.map(rule => 238 | `ruleset=${rule.name},${rule.url}` 239 | ).join('\n') + '\n\n'; 240 | 241 | // 2. 生成节点分组部分 242 | const processedGroups = new Set(); 243 | 244 | template += proxyGroups.map(group => { 245 | if (group.name === '🚀 节点选择' && processedGroups.has(group.name)) { 246 | return ''; 247 | } 248 | processedGroups.add(group.name); 249 | 250 | const type = group.type === 'auto' ? 'url-test' : 'select'; 251 | 252 | if (group.isDefault) { 253 | const otherGroups = proxyGroups 254 | .filter(g => !g.isDefault) 255 | .map(g => `[]${g.name}`) 256 | .join('`'); 257 | return `custom_proxy_group=${group.name}\`${type}\`${otherGroups}\`[]DIRECT`; 258 | } 259 | 260 | // 生成过滤规则 261 | let filter; 262 | if (group.filterType === 'regex') { 263 | filter = `(${group.keywords})`; 264 | } else if (group.filterType === 'inverse') { 265 | filter = `^(?!.*(${group.keywords})).*$`; 266 | } else if (group.filterType === 'both') { 267 | // 组合模式:反则在前,正则在后 268 | const [excludeKeywords, includeKeywords] = group.keywords.split(';;'); 269 | filter = `^(?!.*(${excludeKeywords})).*$(${includeKeywords})`; 270 | } 271 | 272 | let groupStr = `custom_proxy_group=${group.name}\`${type}`; 273 | 274 | if (type === 'url-test') { 275 | groupStr += `\`${filter}\`http://www.gstatic.com/generate_204\`300,,50`; 276 | } else { 277 | groupStr += `\`${filter}`; 278 | } 279 | 280 | return groupStr; 281 | }).filter(Boolean).join('\n') + '\n\n'; 282 | 283 | // 3. 生成分流部分 284 | const sortedRouting = sortRouting(routing); 285 | const proxyGroupsStr = proxyGroups.map(g => `[]${g.name}`).join('`'); 286 | 287 | template += sortedRouting.map(route => { 288 | if (proxyGroups.some(group => group.name === route.name)) { 289 | return ''; 290 | } 291 | 292 | if (route.isReject) { 293 | return `custom_proxy_group=${route.name}\`select\`[]REJECT\`[]DIRECT`; 294 | } 295 | return `custom_proxy_group=${route.name}\`select\`${proxyGroupsStr}\`[]DIRECT`; 296 | }).filter(Boolean).join('\n'); 297 | 298 | return template; 299 | } 300 | 301 | // 生成 HTML 302 | function generateTemplateManagerHTML() { 303 | return ` 304 | 305 | 306 | 307 | 308 | 309 | 配置生成器 310 | 311 | 312 | 313 | 314 | 331 | 332 | 333 |
334 | 337 | 338 | 339 | `; 340 | } 341 | 342 | // 生成 React 组件代码 343 | function generateReactComponents() { 344 | return ` 345 | // 传递所有必要的配置到前端 346 | const DEFAULT_RULES = ${JSON.stringify(DEFAULT_RULES)}; 347 | const parseRules = ${parseRules.toString()}; 348 | const ROUTING_ORDER = ${JSON.stringify(ROUTING_ORDER)}; 349 | const sortRouting = ${sortRouting.toString()}; 350 | 351 | // 添加 TemplateListSection 组件 352 | function TemplateListSection({ onNew }) { 353 | const [templates, setTemplates] = React.useState([]); 354 | const [loading, setLoading] = React.useState(true); 355 | const [error, setError] = React.useState(null); 356 | 357 | React.useEffect(() => { 358 | fetchTemplates(); 359 | }, []); 360 | 361 | const fetchTemplates = async () => { 362 | setLoading(true); 363 | setError(null); 364 | try { 365 | const response = await fetch('/peizhi/api/templates'); 366 | if (!response.ok) throw new Error('Failed to load templates'); 367 | const data = await response.json(); 368 | setTemplates(data.sort((a, b) => 369 | new Date(b.createTime) - new Date(a.createTime) 370 | )); 371 | } catch (err) { 372 | console.error('Load templates error:', err); 373 | setError('加载失败,请稍后重试'); 374 | } finally { 375 | setLoading(false); 376 | } 377 | }; 378 | 379 | const handleDelete = async (id, name) => { 380 | const password = prompt('请输入管理密码以删除模板:'); 381 | if (!password) return; 382 | 383 | if (!confirm(\`确定要删除模板 "\${name}" 吗?\`)) return; 384 | 385 | try { 386 | const response = await fetch(\`/peizhi/api/templates/\${id}\`, { 387 | method: 'DELETE', 388 | headers: { 389 | 'Content-Type': 'application/json' 390 | }, 391 | body: JSON.stringify({ password }) 392 | }); 393 | 394 | if (!response.ok) { 395 | const data = await response.json(); 396 | throw new Error(data.message || 'Delete failed'); 397 | } 398 | 399 | const result = await response.json(); 400 | if (result.success) { 401 | setTemplates(prev => prev.filter(t => t.id !== id)); 402 | } else { 403 | throw new Error(result.message || 'Delete failed'); 404 | } 405 | } catch (err) { 406 | console.error('Delete error:', err); 407 | alert(err.message || '删除失败,请稍后重试'); 408 | } 409 | }; 410 | 411 | const copyToClipboard = async (id) => { 412 | const url = \`\${window.location.origin}/peizhi/template/\${id}\`; 413 | try { 414 | await navigator.clipboard.writeText(url); 415 | alert('链接已复制到剪贴板'); 416 | } catch (err) { 417 | // 降级处理:创建临时输入框 418 | const input = document.createElement('input'); 419 | input.value = url; 420 | document.body.appendChild(input); 421 | input.select(); 422 | document.execCommand('copy'); 423 | document.body.removeChild(input); 424 | alert('链接已复制到剪贴板'); 425 | } 426 | }; 427 | 428 | if (loading) { 429 | return
加载中...
; 430 | } 431 | 432 | return ( 433 |
434 |
435 |

配置模板列表

436 | 442 |
443 | 444 | {error ? ( 445 |
446 |
{error}
447 | 453 |
454 | ) : templates.length === 0 ? ( 455 |
456 | 暂无模板,点击右上角创建新模板 457 |
458 | ) : ( 459 |
460 | {templates.map(template => ( 461 |
462 |
463 |
464 |
{template.name}
465 |
466 | 创建时间: {new Date(template.createTime).toLocaleString()} 467 |
468 |
469 |
470 | 476 | 482 | 488 |
489 |
490 |
491 | ))} 492 |
493 | )} 494 |
495 | ); 496 | } 497 | 498 | // 修改 App 组件 499 | function App() { 500 | const [currentStep, setCurrentStep] = React.useState(0); 501 | const [config, setConfig] = React.useState({ 502 | rules: [], 503 | proxyGroups: [ 504 | { 505 | name: "🚀 节点选择", 506 | type: "select", 507 | isDefault: true 508 | } 509 | ], 510 | routing: [] 511 | }); 512 | 513 | // 当进入第三步时,自动生成分流配置 514 | React.useEffect(() => { 515 | if (currentStep === 3) { 516 | const uniqueRuleNames = [...new Set(config.rules.map(rule => { 517 | const baseName = rule.name.split(' - ')[0]; 518 | return baseName; 519 | }))]; 520 | 521 | const initialRouting = uniqueRuleNames.map(name => ({ 522 | name, 523 | isReject: name.includes('广告') || 524 | name.includes('净化') || 525 | name.includes('AdBlock') 526 | })); 527 | 528 | setConfig(prev => ({ 529 | ...prev, 530 | routing: sortRouting(initialRouting) 531 | })); 532 | } 533 | }, [currentStep]); 534 | 535 | const handleGenerate = async (templateName, password) => { 536 | try { 537 | const response = await fetch('/peizhi/api/generate', { 538 | method: 'POST', 539 | headers: { 540 | 'Content-Type': 'application/json', 541 | }, 542 | body: JSON.stringify({ 543 | ...config, 544 | templateName, 545 | password 546 | }) 547 | }); 548 | 549 | if (!response.ok) { 550 | const data = await response.json(); 551 | throw new Error(data.message || '生成失败'); 552 | } 553 | 554 | const data = await response.json(); 555 | if (data.success) { 556 | window.open(\`/peizhi/template/\${data.templateId}\`, '_blank'); 557 | setCurrentStep(0); // 生成成功后返回模板列表 558 | } else { 559 | alert('生成配置失败:' + (data.message || '未知错误')); 560 | } 561 | } catch (error) { 562 | console.error('生成配置失败:', error); 563 | alert('生成配置失败:' + error.message); 564 | } 565 | }; 566 | 567 | const handleBackToTemplates = () => { 568 | if (confirm('确定要返回模板管理吗?当前修改将不会保存。')) { 569 | setCurrentStep(0); 570 | setConfig({ 571 | rules: [], 572 | proxyGroups: [ 573 | { 574 | name: "🚀 节点选择", 575 | type: "select", 576 | isDefault: true 577 | } 578 | ], 579 | routing: [] 580 | }); 581 | } 582 | }; 583 | 584 | return ( 585 |
586 |
587 |

配置生成器

588 | {currentStep > 0 && ( 589 | 595 | )} 596 |
597 | 598 |
599 |
模板管理
600 |
规则配置
601 |
节点分组
602 |
分流配置
603 |
604 | 605 | {currentStep === 0 && ( 606 | setCurrentStep(1)} 608 | /> 609 | )} 610 | 611 | {currentStep === 1 && ( 612 | setConfig({...config, rules})} 615 | onNext={() => setCurrentStep(2)} 616 | /> 617 | )} 618 | 619 | {currentStep === 2 && ( 620 | setConfig({...config, proxyGroups})} 623 | onBack={() => setCurrentStep(1)} 624 | onNext={() => setCurrentStep(3)} 625 | /> 626 | )} 627 | 628 | {currentStep === 3 && ( 629 | setConfig({...config, routing})} 633 | onBack={() => setCurrentStep(2)} 634 | onGenerate={handleGenerate} 635 | /> 636 | )} 637 |
638 | ); 639 | } 640 | 641 | // RuleSection 组件 642 | function RuleSection({ rules, onChange, onNext, onBack }) { 643 | const [selectedRules, setSelectedRules] = React.useState(new Set()); 644 | const [customRules, setCustomRules] = React.useState([{ name: '', url: '' }]); 645 | const availableRules = React.useMemo(() => parseRules(DEFAULT_RULES), []); 646 | 647 | // 组件加载时自动选择所有预设规则 648 | React.useEffect(() => { 649 | setSelectedRules(new Set(availableRules)); 650 | onChange(availableRules); 651 | }, []); 652 | 653 | const handleRuleToggle = (rule, checked) => { 654 | const newSelected = new Set(selectedRules); 655 | if (checked) { 656 | newSelected.add(rule); 657 | } else { 658 | newSelected.delete(rule); 659 | } 660 | setSelectedRules(newSelected); 661 | 662 | // 并预设规则和自定义规则 663 | const selectedPresetRules = Array.from(newSelected); 664 | const validCustomRules = customRules.filter(rule => rule.name && rule.url); 665 | onChange([...selectedPresetRules, ...validCustomRules]); 666 | }; 667 | 668 | const handleCustomRuleChange = (index, field, value) => { 669 | const newCustomRules = [...customRules]; 670 | newCustomRules[index][field] = value; 671 | setCustomRules(newCustomRules); 672 | 673 | // 更新所有规则 674 | const validCustomRules = newCustomRules.filter(rule => rule.name && rule.url); 675 | const selectedPresetRules = Array.from(selectedRules); 676 | onChange([...selectedPresetRules, ...validCustomRules]); 677 | }; 678 | 679 | // 添加拖拽排序功能 680 | const handleDragStart = (e, index) => { 681 | e.dataTransfer.setData('text/plain', index); 682 | }; 683 | 684 | const handleDragOver = (e) => { 685 | e.preventDefault(); 686 | }; 687 | 688 | const handleDrop = (e, dropIndex) => { 689 | e.preventDefault(); 690 | const dragIndex = parseInt(e.dataTransfer.getData('text/plain')); 691 | if (dragIndex === dropIndex) return; 692 | 693 | const newRules = [...rules]; 694 | const [movedRule] = newRules.splice(dragIndex, 1); 695 | newRules.splice(dropIndex, 0, movedRule); 696 | onChange(newRules); 697 | }; 698 | 699 | return ( 700 |
701 | {/* 预设规则部分 - 三列布局 */} 702 |
703 |

预设规则

704 |
705 | {availableRules.map((rule, index) => ( 706 |
707 | handleRuleToggle(rule, e.target.checked)} 712 | className="mr-2" 713 | /> 714 | 720 |
721 | ))} 722 |
723 |
724 | 725 | {/* 自定义规则部分 */} 726 |
727 |

自定义规则

728 | {customRules.map((rule, index) => ( 729 |
730 | handleCustomRuleChange(index, 'name', e.target.value)} 735 | className="w-1/3 p-2 border rounded" 736 | /> 737 | handleCustomRuleChange(index, 'url', e.target.value)} 742 | className="flex-1 p-2 border rounded" 743 | /> 744 | 753 |
754 | ))} 755 | 761 |
762 | 763 | {/* 已选规则排序部分 - 三列布局 */} 764 | {rules.length > 0 && ( 765 |
766 |

规则排序(拖动调整,从左到右,从上到下,按循序优先)

767 |
768 | {rules.map((rule, index) => ( 769 |
handleDragStart(e, index)} 773 | onDragOver={handleDragOver} 774 | onDrop={(e) => handleDrop(e, index)} 775 | className="flex items-center p-2 bg-white border rounded cursor-move hover:bg-gray-100 text-sm" 776 | > 777 |
778 | {rule.name} 779 | 780 | {rule.displayName} 781 | 782 |
783 | 793 |
794 | ))} 795 |
796 |
797 | )} 798 | 799 |
800 | 806 |
807 |
808 | ); 809 | } 810 | 811 | // ProxyGroupSection 组件 812 | ${ProxyGroupSectionComponent} 813 | 814 | // RoutingSection 组件 815 | function RoutingSection({ routing, proxyGroups, onChange, onBack, onGenerate }) { 816 | const [templateName, setTemplateName] = React.useState(''); 817 | const [password, setPassword] = React.useState(''); 818 | 819 | const handleGenerate = () => { 820 | if (!templateName.trim()) { 821 | alert('请输入模板名称'); 822 | return; 823 | } 824 | if (!password.trim()) { 825 | alert('请输入管理密码'); 826 | return; 827 | } 828 | onGenerate(templateName, password); 829 | }; 830 | 831 | return ( 832 |
833 |

分流配置

834 |

835 | 以下是根据您选择的规则自动生成的分流配置: 836 |

837 | 838 |
839 | {routing.map((route, index) => ( 840 |
841 |
{route.name}
842 |
843 | {route.isReject ? ( 844 | '拦截规则:REJECT, DIRECT' 845 | ) : ( 846 |
847 |
代理规则,可选节点:
848 |
849 | {proxyGroups.map(g => ( 850 |
{g.name}
851 | ))} 852 |
DIRECT
853 |
854 |
855 | )} 856 |
857 |
858 | ))} 859 |
860 | 861 |
862 |

保存模板

863 |
864 |
865 | setTemplateName(e.target.value)} 870 | className="flex-1 p-2 border rounded" 871 | /> 872 |
873 |
874 | setPassword(e.target.value)} 879 | className="flex-1 p-2 border rounded" 880 | /> 881 |
882 |
883 |
884 | 885 |
886 | 892 | 898 |
899 |
900 | ); 901 | } 902 | 903 | // 渲染应用 904 | ReactDOM.render(, document.getElementById('root')); 905 | `; 906 | } 907 | 908 | // ProxyGroupSection 组件代码 909 | const ProxyGroupSectionComponent = ` 910 | function ProxyGroupSection({ proxyGroups, onChange, onBack, onNext }) { 911 | const addProxyGroup = () => { 912 | onChange([...proxyGroups, { 913 | name: '', 914 | type: 'select', 915 | filterType: 'regex', 916 | keywords: '' 917 | }]); 918 | }; 919 | 920 | const removeProxyGroup = (index) => { 921 | if (proxyGroups[index].isDefault) return; 922 | const newGroups = proxyGroups.filter((_, i) => i !== index); 923 | onChange(newGroups); 924 | }; 925 | 926 | const updateProxyGroup = (index, field, value) => { 927 | const newGroups = [...proxyGroups]; 928 | newGroups[index][field] = value; 929 | onChange(newGroups); 930 | }; 931 | 932 | return ( 933 |
934 |

节点分组配置

935 | 936 | {proxyGroups.map((group, index) => ( 937 |
938 |
939 | updateProxyGroup(index, 'name', e.target.value)} 944 | disabled={group.isDefault} 945 | className="flex-1 p-2 border rounded" 946 | /> 947 | 955 | {!group.isDefault && ( 956 | 962 | )} 963 |
964 | 965 | {!group.isDefault && ( 966 |
967 | 976 | updateProxyGroup(index, 'keywords', e.target.value)} 985 | className="flex-1 p-2 border rounded" 986 | /> 987 |
988 | )} 989 |
990 | ))} 991 | 992 | 998 | 999 |
1000 | 1006 | 1012 |
1013 |
1014 | ); 1015 | } 1016 | `; 1017 | 1018 | // 替换为新的导出格式 1019 | export { 1020 | handleGenerateConfig, 1021 | handleGetTemplate, 1022 | handleListTemplates, 1023 | handleDeleteTemplate, 1024 | generateTemplate, // 如果其他地方需要使用这个函数 1025 | generateTemplateManagerHTML, // 如果其他地方需要使用这个函数 1026 | }; -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "subhub" 2 | # main = "_worker.js" # Pages 不需要这行 3 | compatibility_date = "2024-01-01" 4 | 5 | [build] 6 | command = "" # 不需要构建命令 7 | [build.upload] 8 | format = "modules" # 使用 ES modules 格式 --------------------------------------------------------------------------------