diff --git a/README.md b/README.md
index 8b74b23..3e09bf0 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,146 @@
-# Cloudflare Workers Subscription Converter & SOCKS5 Chain Proxy (cf-cub)
+# 🚀 多客户端订阅转换器与链式代理(`cf-sub`)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> ⚡ 一个多客户端机场订阅转换工具与家宽链式代理节点注入工具
-一个部署在 **Cloudflare Workers** 的开源机场订阅转换服务与 SOCKS5 链式代理 Exit 节点注入工具。
+---
-[](https://deploy.workers.cloudflare.com/?url=https://github.com/Mareixcode/cf-cub)
-[](LICENSE)
-[](https://www.typescriptlang.org/)
-[](https://workers.cloudflare.com/)
+## 📖 项目简介
----
+`cf-sub` 可以将标准的机场节点订阅转化为支持多客户端格式(**Clash / Mihomo**、**Sing-box**、**Surge**、**Quantumult X**、**Shadowrocket**)的配置。为机场节点自动附加后置家宽代理(支持 SOCKS5, HTTP, HTTPS, SS, Trojan, VLESS 出口),以家宽 IP 作为终点出口,同时保留机场原生节点与原有的分流策略。
-## ⚡ 30 秒极简部署
+---
-选择以下 **任意一种** 方式即可完成部署:
+## 🌟 核心特性
-### 方式一:一键网页部署 (推荐,无需本地环境)
+- 🌐 **多客户端**:
+ - **Clash / Mihomo** (YAML) -> 利用 `dialer-proxy` 链式代理
+ - **Sing-box** (JSON 1.8+) -> 利用 `detour` 链式代理
+ - **Surge** (.conf) -> 利用 `under-proxy` 链式代理
+ - **Quantumult X** -> 策略组与节点转换
+ - **Shadowrocket / 通用节点** -> Base64 编码与单行 URI 列表
+- 🛡️ **家宽敏感信息零暴露**:支持在 Workers 环境变量中存储家宽 IP、端口与密码。
+- 📊 **剩余流量与到期时间显示**:在客户端卡片及 Web UI 中直观展示已用流量、剩余流量及到期倒计时。
+- 🔗 **一键客户端导入**:可一键生成并调用 `clash://`, `sing-box://`, `surge://`, `sub://` 客户端快捷导入链接。
+---
-点击下方按钮,登录 Cloudflare 网页上一键自动完成部署:
+## 🏗️ 工作原理
-[](https://deploy.workers.cloudflare.com/?url=https://github.com/Mareixcode/cf-cub)
+```text
+[ 用户客户端 (Clash / Sing-box / Surge) ]
+ │
+ ▼ (访问目标网站)
+ [ 前置节点 (机场节点) ] ── (中继流量) ──► [ 出口节点 (家宽出口) ] ──► [ 目标网站 ]
+```
---
-### 方式二:命令行 3 步部署
+## ⚡ 部署指南
+
+### 方式一:网页一键部署 (推荐)
+1、Fork 本仓库
+
+2、点击下方按钮,登录 Cloudflare 账号
+
+[](https://deploy.workers.cloudflare.com/?url=https://github.com/Mareixcode/cf-sub)
-复制粘贴以下 3 行命令:
+3、在 Cloudflare 中连接你 Fork 的仓库
+
+---
+
+### 方式二:命令行部署 (使用Wrangler CLI)
```bash
-git clone https://github.com/Mareixcode/cf-cub.git
-cd cf-cub && npm install
+# 1. 克隆项目仓库
+git clone https://github.com/Mareixcode/cf-sub.git
+cd cf-sub
+
+# 2. 安装依赖
+npm install
+
+# 3. 部署至 Cloudflare Workers
npx wrangler deploy
```
---
-## ⚙️ 环境变量配置 (可选)
+## ⚙️ 配置说明
+
+### 方法 A:使用 Web UI 界面
+
+填入机场订阅,选择目标客户端与家宽出口协议(若家宽信息已配置在环境变量中,可保持留空),点击**生成订阅链接**或**一键导入客户端**。
+
+### 方法 B:Cloudflare 后台环境变量配置 (推荐)
-部署成功后,配置您的家宽 SOCKS5 出口参数(两种):
+前往 Cloudflare Dashboard -> **Workers & Pages** -> 选择您的 Worker -> **Settings** -> **Variables**,添加以下环境变量:
-- **方法 A (Web UI 界面直接填)**:直接在浏览器打开部署好的 Worker 网址,在网页界面上直接填入 SOCKS5 IP、端口、账号和密码。
-- **方法 B (Cloudflare 环境变量)**:在 Cloudflare Dashboard -> Worker -> **Settings** -> **Variables** 中添加环境变量:
- - `SOCKS_SERVER`: SOCKS5 服务器 IP/域名
- - `SOCKS_PORT`: SOCKS5 端口
- - `SOCKS_USERNAME`: 认证用户名
- - `SOCKS_PASSWORD`: 认证密码
+| 变量名 | 说明 | 示例 |
+| :--- | :--- | :--- |
+| `SOCKS_TYPE` | 出口协议类型 (`socks5` \| `http` \| `https` \| `ss` \| `trojan` \| `vless`) | `socks5` |
+| `SOCKS_SERVER` | 家宽出口服务器 IP 或域名 | `1.2.3.4` 或 `exit.example.com` |
+| `SOCKS_PORT` | 出口服务端口 | `1080` |
+| `SOCKS_USERNAME` | 认证用户名 (可选) | `user` |
+| `SOCKS_PASSWORD` | 认证密码 (可选) | `pass` |
---
-## 🌟 核心特性
+## 📡 API 路由与动态参数
+
+- **订阅转换端点**:`GET /sub`
+ - `url`: 原始机场订阅 URL(需 URL 编码)
+ - `target`: 目标客户端 (`clash`, `singbox`, `surge`, `quanx`, `shadowrocket`)
+ - `socks_type`: 出口协议类型 (`socks5`, `http`, `https`, `ss`, `trojan`, `vless`)
+ - `socks_server`: 家宽出口 IP/域名 (可选)
+ - `socks_port`: 家宽出口端口 (可选)
+
+---
+
+## 🙏 致谢
+
+- **[tindy2013/subconverter](https://github.com/tindy2013/subconverter)** -订阅转换工具与客户端转换
+- **[Metacubex/mihomo](https://github.com/Metacubex/mihomo)** - Clash Meta 内核
+- **[SagerNet/sing-box](https://github.com/SagerNet/sing-box)** - 网络代理工具
+- **[Loyalsoldier/clash-rules](https://github.com/Loyalsoldier/clash-rules)** - 分流规则集
+- **[ACL4SSR/ACL4SSR](https://github.com/ACL4SSR/ACL4SSR)** - 策略组与规则定义
+
+---
+
+## ⚠️ 免责声明 (Disclaimer)
+
+1. **仅供学习交流**:本项目(`cf-sub`)仅供网络技术研究、开源代码学习及个人测试使用,请勿用于任何违反所在国家或地区法律法规的用途。
+2. **风险自负**:使用者在部署和使用本项目时,应自行承担相关风险。开发者不对因使用、复制、修改或分发本软件而产生的任何直接或间接损失、法律纠纷、设备故障或安全后果承担任何责任。
+3. **第三方服务**:本项目依赖 Cloudflare Workers 及第三方订阅节点,第三方服务的稳定性、安全性及合法性由对应服务提供方负责,本项目不做任何形式的保证或担保。
+4. **合规使用**:请使用者严格遵守所在国家/地区的法律法规及网络管理规定,如因违规使用产生任何法律后果,均由使用者自行承担,与本项目开发者无关。
+
+---
+
+## 📬 联系方式
+
+* **开发者**: MareixHunk
+* **Email**: [ceohunk@gmail.com](mailto:ceohunk@gmail.com)
+* **GitHub**: [MareixHunk](https://github.com/Mareixcode)
+
+---
-- **Module Worker & Wrangler JSONC 架构**:采用最新标准 TypeScript Module Worker 导出与 `wrangler.jsonc` 配置文件。
-- **SOCKS5 链式代理 (Exit Proxy)**:将机场节点作为前置,家宽 SOCKS5 作为后置 Exit 出口 (`dialer-proxy: 前置节点`),不改变原本的分流规则与策略组。
-- **严格隐私安全**:代码与配置文件零硬编码隐私信息,支持环境变量与 URL 动态查询参数。
-- **智能订阅格式解析**:支持自动检测解析 Base64 节点列表、Clash YAML 配置以及单行节点 URI 列表 (SS / Vmess / Trojan / Vless / Hysteria2)。
-- **yantao.wiki 极客控制台**:集成科技风 Terminal 调试控制台,支持在线一键解析测试与 Clash 客户端快速导入。
-- **Cloudflare Cache API**:针对 `/sub` 自动边缘缓存 300 秒,减少对机场源站的频繁请求。
+## 📄 开源协议
-## 📄 License
+本项目采用 [MIT License](LICENSE) 协议开源。
-[MIT License](LICENSE) © 2026 [Mareixcode](https://github.com/Mareixcode)
+感谢 LINUX DO 社区开发者对本项目的支持 [**LINUX.DO**](https://linux.do)
\ No newline at end of file
diff --git a/package.json b/package.json
index f078b17..d2ae9f4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "cf-cub",
+ "name": "cf-sub",
"version": "1.0.0",
- "description": "Cloudflare Workers Subscription Converter & SOCKS5 Chain Proxy Exit",
+ "description": "Cloudflare Workers Subscription Converter & SOCKS5 Chain Proxy",
"main": "src/index.ts",
"type": "module",
"scripts": {
diff --git a/src/output/base64.ts b/src/output/base64.ts
new file mode 100644
index 0000000..454611b
--- /dev/null
+++ b/src/output/base64.ts
@@ -0,0 +1,73 @@
+import { ClashConfig, ClashProxy } from '../types';
+
+export function formatBase64Sub(config: ClashConfig): string {
+ const proxies = config.proxies || [];
+ const lines: string[] = [];
+
+ for (const proxy of proxies) {
+ const link = proxyToUri(proxy);
+ if (link) {
+ lines.push(link);
+ }
+ }
+
+ const rawText = lines.join('\n');
+ return btoa(unescape(encodeURIComponent(rawText)));
+}
+
+export function formatRawTextSub(config: ClashConfig): string {
+ const proxies = config.proxies || [];
+ const lines: string[] = [];
+
+ for (const proxy of proxies) {
+ const link = proxyToUri(proxy);
+ if (link) {
+ lines.push(link);
+ }
+ }
+
+ return lines.join('\n');
+}
+
+function proxyToUri(proxy: ClashProxy): string | null {
+ if (!proxy || !proxy.name) return null;
+
+ const name = encodeURIComponent(proxy.name);
+ const server = proxy.server;
+ const port = proxy.port;
+ const type = (proxy.type || '').toLowerCase();
+
+ if (type === 'socks5' || type === 'socks') {
+ let auth = '';
+ if (proxy.username && proxy.password) {
+ auth = `${encodeURIComponent(String(proxy.username))}:${encodeURIComponent(String(proxy.password))}@`;
+ }
+ return `socks5://${auth}${server}:${port}#${name}`;
+ } else if (type === 'ss') {
+ const userInfo = btoa(`${proxy.cipher}:${proxy.password}`);
+ return `ss://${userInfo}@${server}:${port}#${name}`;
+ } else if (type === 'trojan') {
+ return `trojan://${encodeURIComponent(String(proxy.password))}@${server}:${port}#${name}`;
+ } else if (type === 'vless') {
+ return `vless://${proxy.uuid}@${server}:${port}?encryption=none#${name}`;
+ } else if (type === 'vmess') {
+ const vmessObj = {
+ v: '2',
+ ps: proxy.name,
+ add: server,
+ port: String(port),
+ id: proxy.uuid,
+ aid: String(proxy.alterId || '0'),
+ scy: proxy.cipher || 'auto',
+ net: proxy.network || 'tcp',
+ type: 'none',
+ host: proxy.servername || '',
+ tls: proxy.tls ? 'tls' : '',
+ };
+ const jsonStr = JSON.stringify(vmessObj);
+ const base64Str = btoa(unescape(encodeURIComponent(jsonStr)));
+ return `vmess://${base64Str}`;
+ }
+
+ return null;
+}
diff --git a/src/output/quanx.ts b/src/output/quanx.ts
new file mode 100644
index 0000000..303572b
--- /dev/null
+++ b/src/output/quanx.ts
@@ -0,0 +1,76 @@
+import { ClashConfig, ClashProxy } from '../types';
+
+export function formatQuanxConfig(config: ClashConfig): string {
+ const proxies = config.proxies || [];
+ const groups = config['proxy-groups'] || [];
+
+ const nodeLines: string[] = [];
+ for (const proxy of proxies) {
+ const line = transformProxyToQuanx(proxy);
+ if (line) {
+ nodeLines.push(line);
+ }
+ }
+
+ const policyLines: string[] = [];
+ for (const group of groups) {
+ const members = (group.proxies || []).join(', ');
+ policyLines.push(`static=${group.name}, ${members}, img-url=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Proxy.png`);
+ }
+
+ return `[general]
+profile_img_url=https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Server.png
+
+[server_local]
+${nodeLines.join('\n')}
+
+[policy]
+${policyLines.join('\n')}
+
+[filter_local]
+FINAL, ⚡ 最终出口
+`;
+}
+
+function transformProxyToQuanx(proxy: ClashProxy): string | null {
+ if (!proxy || !proxy.name) return null;
+
+ const name = proxy.name;
+ const server = proxy.server;
+ const port = proxy.port;
+ const type = (proxy.type || '').toLowerCase();
+
+ let line = '';
+ if (type === 'socks5' || type === 'socks') {
+ const auth = (proxy.username && proxy.password)
+ ? `, fast-open=false, udp-relay=true, user=${proxy.username}, password=${proxy.password}`
+ : ', fast-open=false, udp-relay=true';
+ line = `socks5=${server}:${port}${auth}, tag=${name}`;
+ } else if (type === 'http' || type === 'https') {
+ const tls = type === 'https' ? ', over-tls=true' : '';
+ const auth = (proxy.username && proxy.password)
+ ? `, user=${proxy.username}, password=${proxy.password}`
+ : '';
+ line = `http=${server}:${port}${auth}${tls}, tag=${name}`;
+ } else if (type === 'ss') {
+ // QX Shadowsocks 格式
+ const cipher = proxy.cipher || 'chacha20-ietf-poly1305';
+ line = `shadowsocks=${server}:${port}, method=${cipher}, password=${proxy.password}, fast-open=false, udp-relay=true, tag=${name}`;
+ } else if (type === 'trojan') {
+ line = `trojan=${server}:${port}, password=${proxy.password}, over-tls=true, tls-verification=true, tag=${name}`;
+ } else if (type === 'vmess') {
+ // QX VMess: method 使用节点实际加密方式而非固定 none
+ const cipher = (proxy.cipher && proxy.cipher !== 'auto') ? String(proxy.cipher) : 'chacha20-ietf-poly1305';
+ const tls = proxy.tls ? ', obfs=over-tls' : '';
+ line = `vmess=${server}:${port}, method=${cipher}, password=${proxy.uuid}${tls}, tag=${name}`;
+ } else if (type === 'vless') {
+ // QX VLESS (需要 QX >= 1.0.30)
+ line = `vless=${server}:${port}, password=${proxy.uuid}, over-tls=true, tag=${name}`;
+ } else {
+ // 不支持的类型跳过
+ return null;
+ }
+
+ return line;
+}
+
diff --git a/src/output/singbox.ts b/src/output/singbox.ts
new file mode 100644
index 0000000..f94b4c3
--- /dev/null
+++ b/src/output/singbox.ts
@@ -0,0 +1,140 @@
+import { ClashConfig, ClashProxy } from '../types';
+
+export function formatSingboxJson(config: ClashConfig): string {
+ const clashProxies = config.proxies || [];
+ const clashGroups = config['proxy-groups'] || [];
+
+ const outbounds: Record[] = [];
+
+ // 1. 转换基础节点
+ for (const proxy of clashProxies) {
+ const sbOutbound = transformProxyToSingbox(proxy);
+ if (sbOutbound) {
+ outbounds.push(sbOutbound);
+ }
+ }
+
+ // 2. 转换策略组为 selector
+ for (const group of clashGroups) {
+ outbounds.push({
+ type: 'selector',
+ tag: group.name,
+ outbounds: group.proxies || [],
+ });
+ }
+
+ // 3. 补全内置出站
+ outbounds.push(
+ { type: 'direct', tag: 'DIRECT' },
+ { type: 'block', tag: 'REJECT' },
+ { type: 'dns', tag: 'dns-out' }
+ );
+
+ // Sing-box 1.8+ 规范:dns.servers 不放 detour,通过 dns.rules 路由
+ const singboxConfig = {
+ log: {
+ level: 'info',
+ timestamp: true,
+ },
+ dns: {
+ servers: [
+ { tag: 'remote', address: 'tls://8.8.8.8', detour: '⚡ 最终出口' },
+ { tag: 'local', address: '223.5.5.5' },
+ { tag: 'block', address: 'rcode://success' },
+ ],
+ rules: [
+ { geosite: ['cn'], server: 'local' },
+ { geosite: ['category-ads-all'], server: 'block', disable_cache: true },
+ ],
+ final: 'remote',
+ strategy: 'prefer_ipv4',
+ },
+ inbounds: [
+ {
+ type: 'tun',
+ tag: 'tun-in',
+ address: ['172.19.0.1/30', 'fdfe:dcba:9876::1/126'],
+ auto_route: true,
+ strict_route: true,
+ sniff: true,
+ },
+ ],
+ outbounds,
+ route: {
+ auto_detect_interface: true,
+ final: '⚡ 最终出口',
+ },
+ };
+
+ return JSON.stringify(singboxConfig, null, 2);
+}
+
+
+function transformProxyToSingbox(proxy: ClashProxy): Record | null {
+ if (!proxy || !proxy.name) return null;
+
+ const tag = proxy.name;
+ const server = proxy.server;
+ const port = proxy.port;
+ const dialerProxy = proxy['dialer-proxy'] as string | undefined;
+
+ const type = (proxy.type || '').toLowerCase();
+
+ let outbound: Record = {
+ tag,
+ server,
+ server_port: port,
+ };
+
+ if (dialerProxy) {
+ outbound.detour = dialerProxy;
+ }
+
+ if (type === 'socks5' || type === 'socks') {
+ outbound.type = 'socks';
+ outbound.version = '5';
+ if (proxy.username) outbound.username = proxy.username;
+ if (proxy.password) outbound.password = proxy.password;
+ } else if (type === 'http') {
+ outbound.type = 'http';
+ if (proxy.username) outbound.username = proxy.username;
+ if (proxy.password) outbound.password = proxy.password;
+ } else if (type === 'https') {
+ // Sing-box 中 https 代理用 http + tls 实现
+ outbound.type = 'http';
+ if (proxy.username) outbound.username = proxy.username;
+ if (proxy.password) outbound.password = proxy.password;
+ outbound.tls = { enabled: true, server_name: proxy.sni || server };
+ } else if (type === 'ss') {
+ outbound.type = 'shadowsocks';
+ outbound.method = proxy.cipher || 'chacha20-ietf-poly1305';
+ outbound.password = proxy.password;
+ } else if (type === 'vmess') {
+ outbound.type = 'vmess';
+ outbound.uuid = proxy.uuid;
+ outbound.security = proxy.cipher || 'auto';
+ outbound.alter_id = proxy.alterId || 0;
+ if (proxy.tls) {
+ outbound.tls = { enabled: true, server_name: proxy.servername || server };
+ }
+ } else if (type === 'vless') {
+ outbound.type = 'vless';
+ outbound.uuid = proxy.uuid;
+ if (proxy.flow) outbound.flow = proxy.flow;
+ // VLESS 通常需要 TLS(Reality 或 TLS)
+ outbound.tls = { enabled: true, server_name: proxy.sni || proxy.servername || server };
+ } else if (type === 'trojan') {
+ outbound.type = 'trojan';
+ outbound.password = proxy.password;
+ outbound.tls = { enabled: true, server_name: proxy.sni || server };
+ } else if (type === 'hysteria2' || type === 'hy2') {
+ outbound.type = 'hysteria2';
+ outbound.password = proxy.auth;
+ outbound.tls = { enabled: true, server_name: proxy.sni || server };
+ } else {
+ // 不支持的类型直接跳过,避免生成无效配置
+ return null;
+ }
+
+ return outbound;
+}
diff --git a/src/output/surge.ts b/src/output/surge.ts
new file mode 100644
index 0000000..8d588f3
--- /dev/null
+++ b/src/output/surge.ts
@@ -0,0 +1,97 @@
+import { ClashConfig, ClashProxy } from '../types';
+
+export function formatSurgeConf(config: ClashConfig): string {
+ const proxies = config.proxies || [];
+ const groups = config['proxy-groups'] || [];
+
+ const proxyLines: string[] = [];
+ for (const proxy of proxies) {
+ const line = transformProxyToSurge(proxy);
+ if (line) {
+ proxyLines.push(line);
+ }
+ }
+
+ const groupLines: string[] = [];
+ for (const group of groups) {
+ const members = (group.proxies || []).join(', ');
+ groupLines.push(`${group.name} = select, ${members}`);
+ }
+
+ return `[General]
+loglevel = notify
+bypass-system = true
+ipv6 = false
+
+[Proxy]
+${proxyLines.join('\n')}
+
+[Proxy Group]
+${groupLines.join('\n')}
+
+[Rule]
+FINAL, ⚡ 最终出口
+`;
+}
+
+function transformProxyToSurge(proxy: ClashProxy): string | null {
+ if (!proxy || !proxy.name) return null;
+
+ const name = proxy.name;
+ const server = proxy.server;
+ const port = proxy.port;
+ const dialerProxy = proxy['dialer-proxy'] as string | undefined;
+
+ const type = (proxy.type || '').toLowerCase();
+ const parts: string[] = [];
+
+ if (type === 'socks5' || type === 'socks') {
+ // Surge SOCKS5: name = socks5, server, port, username=x, password=x
+ parts.push('socks5', server, String(port));
+ if (proxy.username) parts.push(`username=${proxy.username}`);
+ if (proxy.password) parts.push(`password=${proxy.password}`);
+ parts.push('udp-relay=true');
+ } else if (type === 'http') {
+ parts.push('http', server, String(port));
+ if (proxy.username) parts.push(`username=${proxy.username}`);
+ if (proxy.password) parts.push(`password=${proxy.password}`);
+ } else if (type === 'https') {
+ // Surge: https 用 http 类型加 tls=true
+ parts.push('http', server, String(port));
+ if (proxy.username) parts.push(`username=${proxy.username}`);
+ if (proxy.password) parts.push(`password=${proxy.password}`);
+ parts.push('tls=true');
+ if (proxy.sni) parts.push(`sni=${proxy.sni}`);
+ } else if (type === 'ss') {
+ // Surge 4+ 标准 Shadowsocks 格式
+ parts.push('ss', server, String(port));
+ parts.push(`encrypt-method=${proxy.cipher}`);
+ parts.push(`password=${proxy.password}`);
+ parts.push('udp-relay=true');
+ } else if (type === 'trojan') {
+ // Surge Trojan 格式
+ parts.push('trojan', server, String(port));
+ parts.push(`password=${proxy.password}`);
+ if (proxy.sni) parts.push(`sni=${proxy.sni}`);
+ parts.push('udp-relay=true');
+ } else if (type === 'vmess') {
+ // Surge VMess 格式
+ parts.push('vmess', server, String(port));
+ parts.push(`username=${proxy.uuid}`);
+ if (proxy.cipher) parts.push(`encrypt-method=${proxy.cipher}`);
+ if (proxy.tls) {
+ parts.push('tls=true');
+ if (proxy.servername) parts.push(`sni=${proxy.servername}`);
+ }
+ } else {
+ // 降级回 socks5
+ parts.push('socks5', server, String(port));
+ }
+
+ if (dialerProxy) {
+ parts.push(`under-proxy=${dialerProxy}`);
+ }
+
+ return `${name} = ${parts.join(', ')}`;
+}
+
diff --git a/src/router.ts b/src/router.ts
index de539e2..cceefea 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -1,4 +1,4 @@
-import { Env, DynamicSocksConfig } from './types';
+import { Env, DynamicSocksConfig, TargetClient } from './types';
import { fetchSubscription } from './utils/http';
import { transformSubscription } from './transform';
import { renderWebUI } from './ui/html';
@@ -7,10 +7,10 @@ export async function handleRequest(request: Request, env: Env): Promise
+ // 4. GET /sub?url=<机场订阅链接>&target=
if (pathname === '/sub') {
const subUrl = url.searchParams.get('url');
if (!subUrl) {
@@ -72,28 +71,53 @@ export async function handleRequest(request: Request, env: Env): Promise User-Agent > Default clash)
+ let target: TargetClient = 'clash';
+ const queryTarget = (url.searchParams.get('target') || '').toLowerCase();
+
+ if (['clash', 'singbox', 'surge', 'quanx', 'shadowrocket', 'base64'].includes(queryTarget)) {
+ target = queryTarget as TargetClient;
+ } else {
+ const uaLower = userAgent.toLowerCase();
+ if (uaLower.includes('sing-box') || uaLower.includes('singbox')) {
+ target = 'singbox';
+ } else if (uaLower.includes('surge')) {
+ target = 'surge';
+ } else if (uaLower.includes('quantumult')) {
+ target = 'quanx';
+ } else if (uaLower.includes('shadowrocket')) {
+ target = 'shadowrocket';
+ }
+ }
+
+ // 解析出口代理参数
+ const socksType = url.searchParams.get('socks_type') || url.searchParams.get('type');
const socksServer = url.searchParams.get('socks_server') || url.searchParams.get('server');
const socksPort = url.searchParams.get('socks_port') || url.searchParams.get('port');
const socksUser = url.searchParams.get('socks_user') || url.searchParams.get('username');
const socksPass = url.searchParams.get('socks_pass') || url.searchParams.get('password');
+ const socksCipher = url.searchParams.get('socks_cipher') || url.searchParams.get('cipher');
+ const socksUuid = url.searchParams.get('socks_uuid') || url.searchParams.get('uuid');
+ const socksSni = url.searchParams.get('socks_sni') || url.searchParams.get('sni');
let customSocks: DynamicSocksConfig | undefined = undefined;
- if (socksServer || socksPort || socksUser || socksPass) {
+ if (socksType || socksServer || socksPort || socksUser || socksPass || socksCipher || socksUuid || socksSni) {
customSocks = {
+ type: socksType || undefined,
server: socksServer || undefined,
port: socksPort || undefined,
username: socksUser || undefined,
password: socksPass || undefined,
+ cipher: socksCipher || undefined,
+ uuid: socksUuid || undefined,
+ sni: socksSni || undefined,
};
}
- const userAgent = request.headers.get('User-Agent');
-
// 下载订阅
- let rawContent: string;
+ let subResult;
try {
- rawContent = await fetchSubscription(subUrl, userAgent);
+ subResult = await fetchSubscription(subUrl, userAgent);
} catch {
return new Response(
JSON.stringify({
@@ -109,14 +133,35 @@ export async function handleRequest(request: Request, env: Env): Promise = {
+ 'Content-Type': transformResult.contentType,
+ 'Cache-Control': 'public, max-age=300',
+ // 允许浏览器跨域读取(UI 调试页面 fetch 同源,但明确声明更安全)
+ 'Access-Control-Allow-Origin': '*',
+ // 必须显式暴露自定义响应头,否则浏览器 JS 无法读取
+ 'Access-Control-Expose-Headers': 'subscription-userinfo, Subscription-Userinfo, profile-update-interval, Profile-Update-Interval',
+ };
+
+ // 原样透传剩余流量与更新间隔 Response Headers
+ if (subResult.userinfo) {
+ headers['subscription-userinfo'] = subResult.userinfo;
+ }
+
+ if (subResult.profileUpdateInterval) {
+ headers['profile-update-interval'] = subResult.profileUpdateInterval;
+ }
+
+ return new Response(transformResult.content, {
status: 200,
- headers: {
- 'Content-Type': 'text/yaml; charset=utf-8',
- 'Cache-Control': 'public, max-age=300',
- },
+ headers,
});
} catch (err) {
const errorMsg = (err as Error).message || '';
@@ -135,7 +180,7 @@ export async function handleRequest(request: Request, env: Env): Promise
- (codePoint >= 0x1F000 && codePoint <= 0x1FAFF) ||
- (codePoint >= 0x2600 && codePoint <= 0x27BF) ||
- codePoint === 0xFE0F ||
- codePoint === 0x200D;
-
- const stripEmoji = text => Array.from(String(text || ""))
- .filter(character => !isEmojiCodePoint(character.codePointAt(0)))
- .join("")
- .replace(/\s+/g, " ")
- .trim();
+ // 保留原始 Emoji 图标,仅修剪头尾多余空格
+ const formatNodeName = text => String(text || "").replace(/\s+/g, " ").trim();
const usedSourceNames = new Map();
const sourceProxies = [];
config.proxies.forEach((proxy, index) => {
if (!proxy || !proxy.name) return;
- if (proxy.name === homeExitName) return;
- if (proxy.name === "动态跟随-家宽链") return;
+ if (proxy.name === homeExitName || proxy.name.includes("家宽出口")) return;
- const baseName = stripEmoji(proxy.name) || `节点 ${index + 1}`;
+ const baseName = formatNodeName(proxy.name) || `节点 ${index + 1}`;
const duplicateIndex = (usedSourceNames.get(baseName) || 0) + 1;
usedSourceNames.set(baseName, duplicateIndex);
@@ -94,32 +83,63 @@ function main(config, profileName) {
config.proxies = sourceProxies;
const sourceProxyNames = sourceProxies.map(proxy => proxy.name);
- // 读取动态参数 -> 环境变量 -> 通用示例降级值 (无隐私硬编码)
+ // 读取动态参数 -> 环境变量 -> 默认值
const customSocks = (typeof CUSTOM_SOCKS !== 'undefined' && CUSTOM_SOCKS) ? CUSTOM_SOCKS : {};
+ const socksType = (customSocks.type || (typeof SOCKS_TYPE !== 'undefined' ? SOCKS_TYPE : "socks5")).toLowerCase();
const socksServer = customSocks.server || ((typeof SOCKS_SERVER !== 'undefined' && SOCKS_SERVER) ? SOCKS_SERVER : "127.0.0.1");
const socksPort = customSocks.port ? Number(customSocks.port) : ((typeof SOCKS_PORT !== 'undefined' && SOCKS_PORT) ? Number(SOCKS_PORT) : 1080);
const socksUsername = customSocks.username || ((typeof SOCKS_USERNAME !== 'undefined' && SOCKS_USERNAME) ? SOCKS_USERNAME : "");
const socksPassword = customSocks.password || ((typeof SOCKS_PASSWORD !== 'undefined' && SOCKS_PASSWORD) ? SOCKS_PASSWORD : "");
+ const socksCipher = customSocks.cipher || ((typeof SOCKS_CIPHER !== 'undefined' && SOCKS_CIPHER) ? SOCKS_CIPHER : "chacha20-ietf-poly1305");
+ const socksUuid = customSocks.uuid || ((typeof SOCKS_UUID !== 'undefined' && SOCKS_UUID) ? SOCKS_UUID : "");
+ const socksSni = customSocks.sni || ((typeof SOCKS_SNI !== 'undefined' && SOCKS_SNI) ? SOCKS_SNI : "");
const socksNodeName = customSocks.name || homeExitName;
- // 将家宽节点插入并指向前置组,实现链式代理
- const homeExitProxy = {
+ // 根据不同出口协议组装家宽代理节点
+ // Clash 中 http/https 出口均使用 type=http,https 通过 tls=true 区分
+ const clashProxyType = (socksType === 'https') ? 'http' : socksType;
+
+ let homeExitProxy = {
name: socksNodeName,
- type: "socks5",
+ type: clashProxyType,
server: socksServer,
port: socksPort,
- username: socksUsername,
- password: socksPassword,
- udp: true,
"dialer-proxy": frontGroupName
};
+ if (socksType === 'socks5') {
+ if (socksUsername) homeExitProxy.username = socksUsername;
+ if (socksPassword) homeExitProxy.password = socksPassword;
+ homeExitProxy.udp = true;
+ } else if (socksType === 'http') {
+ if (socksUsername) homeExitProxy.username = socksUsername;
+ if (socksPassword) homeExitProxy.password = socksPassword;
+ } else if (socksType === 'https') {
+ if (socksUsername) homeExitProxy.username = socksUsername;
+ if (socksPassword) homeExitProxy.password = socksPassword;
+ homeExitProxy.tls = true;
+ if (socksSni) homeExitProxy.sni = socksSni;
+ } else if (socksType === 'ss') {
+ homeExitProxy.cipher = socksCipher;
+ homeExitProxy.password = socksPassword;
+ homeExitProxy.udp = true;
+ } else if (socksType === 'trojan') {
+ homeExitProxy.password = socksPassword;
+ homeExitProxy.tls = true;
+ if (socksSni) homeExitProxy.sni = socksSni;
+ homeExitProxy.udp = true;
+ } else if (socksType === 'vless') {
+ homeExitProxy.uuid = socksUuid;
+ homeExitProxy.cipher = 'auto';
+ homeExitProxy.udp = true;
+ }
+
config.proxies.push(homeExitProxy);
const frontProxyNames = unique(sourceProxyNames);
- // 策略组
+ // 策略组定义(带 Emoji 图标)
config["proxy-groups"] = [
{
name: frontGroupName,
@@ -239,3 +259,4 @@ function main(config, profileName) {
export default main;
export { main };
+
diff --git a/src/transform.ts b/src/transform.ts
index e6442d6..2476224 100644
--- a/src/transform.ts
+++ b/src/transform.ts
@@ -1,19 +1,33 @@
-import { Env, ClashConfig, DynamicSocksConfig } from './types';
+import { Env, ClashConfig, DynamicSocksConfig, TargetClient } from './types';
import { parseSubscription } from './parser/clash';
import { formatClashYaml } from './output/clash';
+import { formatSingboxJson } from './output/singbox';
+import { formatSurgeConf } from './output/surge';
+import { formatQuanxConfig } from './output/quanx';
+import { formatBase64Sub } from './output/base64';
import main from './script.js';
+export interface TransformResult {
+ content: string;
+ contentType: string;
+}
+
export function transformSubscription(
rawContent: string,
env: Env,
+ target: TargetClient = 'clash',
profileName: string = 'CF-Sub',
customSocks?: DynamicSocksConfig
-): string {
+): TransformResult {
// 1. 将 env 及 customSocks 注入全局
if (env.SOCKS_SERVER) (globalThis as unknown as Record).SOCKS_SERVER = env.SOCKS_SERVER;
if (env.SOCKS_PORT) (globalThis as unknown as Record).SOCKS_PORT = env.SOCKS_PORT;
if (env.SOCKS_USERNAME) (globalThis as unknown as Record).SOCKS_USERNAME = env.SOCKS_USERNAME;
if (env.SOCKS_PASSWORD) (globalThis as unknown as Record).SOCKS_PASSWORD = env.SOCKS_PASSWORD;
+ if (env.SOCKS_TYPE) (globalThis as unknown as Record).SOCKS_TYPE = env.SOCKS_TYPE;
+ if (env.SOCKS_CIPHER) (globalThis as unknown as Record).SOCKS_CIPHER = env.SOCKS_CIPHER;
+ if (env.SOCKS_UUID) (globalThis as unknown as Record).SOCKS_UUID = env.SOCKS_UUID;
+ if (env.SOCKS_SNI) (globalThis as unknown as Record).SOCKS_SNI = env.SOCKS_SNI;
if (customSocks) {
(globalThis as unknown as Record).CUSTOM_SOCKS = customSocks;
@@ -32,6 +46,33 @@ export function transformSubscription(
throw new Error(`Script Error: ${(err as Error).message}`);
}
- // 4. 输出 YAML
- return formatClashYaml(newConfig);
+ // 4. 根据目标客户端按需导出格式
+ if (target === 'singbox') {
+ return {
+ content: formatSingboxJson(newConfig),
+ contentType: 'application/json; charset=utf-8',
+ };
+ } else if (target === 'surge') {
+ return {
+ content: formatSurgeConf(newConfig),
+ contentType: 'text/plain; charset=utf-8',
+ };
+ } else if (target === 'quanx') {
+ return {
+ content: formatQuanxConfig(newConfig),
+ contentType: 'text/plain; charset=utf-8',
+ };
+ } else if (target === 'shadowrocket' || target === 'base64') {
+ return {
+ content: formatBase64Sub(newConfig),
+ contentType: 'text/plain; charset=utf-8',
+ };
+ }
+
+ // 默认 Clash / Mihomo
+ return {
+ content: formatClashYaml(newConfig),
+ contentType: 'text/yaml; charset=utf-8',
+ };
}
+
diff --git a/src/types.ts b/src/types.ts
index 54c3362..eef5e2b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -3,14 +3,23 @@ export interface Env {
SOCKS_PORT?: string | number;
SOCKS_USERNAME?: string;
SOCKS_PASSWORD?: string;
+ SOCKS_TYPE?: string;
+ SOCKS_CIPHER?: string;
+ SOCKS_UUID?: string;
+ SOCKS_SNI?: string;
}
export interface DynamicSocksConfig {
+ type?: string; // 'socks5' | 'http' | 'https' | 'ss' | 'trojan' | 'vless'
server?: string;
port?: number | string;
username?: string;
password?: string;
+ cipher?: string;
+ uuid?: string;
+ sni?: string;
name?: string;
+ tls?: boolean;
}
export interface ClashProxy {
@@ -36,7 +45,16 @@ export interface ClashConfig {
[key: string]: unknown;
}
+export type TargetClient = 'clash' | 'singbox' | 'surge' | 'quanx' | 'shadowrocket' | 'base64';
+
+export interface FetchSubResult {
+ content: string;
+ userinfo?: string;
+ profileUpdateInterval?: string;
+}
+
export interface ErrorResponse {
success: false;
message: string;
}
+
diff --git a/src/ui/html.ts b/src/ui/html.ts
index b41ddaa..e0ab299 100644
--- a/src/ui/html.ts
+++ b/src/ui/html.ts
@@ -4,233 +4,285 @@ export function renderWebUI(): string {
- Almighty.CF-Sub - Cloudflare Worker 订阅转换器
-
+ CF-Sub - 机场订阅转换 & 链式代理
-
+
+
+
-
-
-
-
-
-
-