├── 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 |
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 |
93 |
\${template.name}
94 |
95 |
101 |
102 |
103 |
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 |
168 |
169 |
170 |
171 |
172 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
189 |
190 |
191 |
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 |
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 |
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 |
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 格式
--------------------------------------------------------------------------------