diff --git a/clash-config-subscriptions.yaml b/clash-config-subscriptions.yaml
index d671f04..077122b 100644
--- a/clash-config-subscriptions.yaml
+++ b/clash-config-subscriptions.yaml
@@ -1425,6 +1425,24 @@ proxy-groups:
- 🇯🇵 JP节点
- 🇬🇧 EUR节点
- DIRECT
+ - name: 🎥 YouTube
+ type: select
+ proxies:
+ - 🚀 节点选择
+ - 🎯 全球直连
+ - ♻️ 自动选择
+ - 🚀 手动选择
+ - 🇺🇸 incogNET-US节点
+ - 🇺🇸 ColoCrossing-US节点
+ - 🇺🇸 VeloxMediaApps节点-US
+ - 🇬🇧 VeloxMediaTools节点-UK
+ - 🇺🇸 ShareColoCrossing-US节点
+ - 🇺🇸 US节点
+ - 🇹🇷 TR节点
+ - 🇸🇬 SG节点
+ - 🇯🇵 JP节点
+ - 🇬🇧 EUR节点
+ - DIRECT
- name: 🌍 Copilot
type: select
proxies:
@@ -1785,24 +1803,6 @@ proxy-groups:
- 🇯🇵 vless-JP04
- 🇯🇵 vless-JP05
- DIRECT
- - name: 🎥 YouTube
- type: select
- proxies:
- - 🚀 节点选择
- - 🎯 全球直连
- - ♻️ 自动选择
- - 🚀 手动选择
- - 🇺🇸 incogNET-US节点
- - 🇺🇸 ColoCrossing-US节点
- - 🇺🇸 VeloxMediaApps节点-US
- - 🇬🇧 VeloxMediaTools节点-UK
- - 🇺🇸 ShareColoCrossing-US节点
- - 🇺🇸 US节点
- - 🇹🇷 TR节点
- - 🇸🇬 SG节点
- - 🇯🇵 JP节点
- - 🇬🇧 EUR节点
- - DIRECT
- name: Ⓜ️ Facebook
type: select
proxies:
diff --git a/tools/SubConverter/DEPLOYMENT_GUIDE.md b/tools/SubConverter/DEPLOYMENT_GUIDE.md
new file mode 100644
index 0000000..44c12f7
--- /dev/null
+++ b/tools/SubConverter/DEPLOYMENT_GUIDE.md
@@ -0,0 +1,1402 @@
+# Cloudflare Worker 部署与使用指南
+
+## 📦 文件说明
+
+本工具只需要 **2 个文件**:
+
+1. **cf_worker_v2ray_converter.js** (~88KB) - Worker 主文件
+ - 包含所有功能代码和完整的 HTML/CSS/JavaScript 界面
+ - **这是唯一需要部署到 Cloudflare 的文件**
+
+2. **DEPLOYMENT_GUIDE.md** - 本文件,部署和使用指南
+
+---
+
+## 🎯 功能说明
+
+### v4.1 核心功能 ⭐ 最新版本
+
+#### 📡 订阅管理中心(需要配置 KV)
+
+全新的订阅管理界面,专注于订阅的长期管理:
+
+- ✅ **左右分栏布局**:左侧订阅列表,右侧详情操作
+- ✅ **订阅分类管理**:
+ - **正式订阅**:支持 7天、1个月、3个月、1年、3年、长期(默认1年)
+ - **临时订阅**:限定1天/3天/7天有效期,到期自动删除
+- ✅ **智能类型识别**:自动检测 Clash 或 V2Ray 格式
+- ✅ **双向格式转换**:Clash ⇄ V2Ray 任意转换
+- ✅ **短链生命周期管理**:续期、换新链接、删除
+- ✅ **状态标记**:快过期(30天内)和已过期订阅有明显标记
+- ✅ **自定义对话框**:美观的模态确认框替代浏览器默认弹窗
+
+---
+
+## 🚀 快速部署(5分钟)
+
+### 方式 1: 通过 Cloudflare Dashboard(推荐新手)
+
+#### 步骤 1:登录 Cloudflare
+
+访问 并登录(没有账号需先注册)
+
+#### 步骤 2:创建 Worker
+
+1. 点击左侧菜单 **Workers & Pages**
+2. 点击 **Create application** → **Create Worker**
+3. 输入名称(如:`subscription-manager`)
+ - 这将成为你的访问域名:`subscription-manager.你的账号.workers.dev`
+4. 点击 **Deploy**
+
+#### 步骤 3:部署代码
+
+1. 点击 **Quick edit** 打开代码编辑器
+2. **删除**编辑器中的所有默认代码
+3. 打开本地的 `cf_worker_v2ray_converter.js`
+4. **全选复制**所有内容(Ctrl+A / Cmd+A,然后 Ctrl+C / Cmd+C)
+ - ⚠️ 重要:必须复制全部 ~88KB 内容
+5. **粘贴**到编辑器(Ctrl+V / Cmd+V)
+6. 点击右上角 **Save and deploy**
+
+#### 步骤 4:验证部署
+
+1. 访问 `https://你的worker名称.你的账号.workers.dev/`
+2. 应该看到紫色渐变背景的「📡 订阅管理中心」页面
+3. 提示需要配置 KV 命名空间
+4. 继续下一步配置 KV
+
+✅ **基础部署完成!**
+
+---
+
+### 方式 2: 使用 Wrangler CLI(推荐开发者)
+
+```bash
+# 1. 安装 wrangler
+npm install -g wrangler
+
+# 2. 登录
+wrangler login
+
+# 3. 创建项目
+mkdir subscription-manager && cd subscription-manager
+wrangler init
+
+# 4. 复制代码到 src/index.js
+cp /path/to/cf_worker_v2ray_converter.js src/index.js
+
+# 5. 部署
+wrangler deploy
+
+# 6. 查看日志(可选)
+wrangler tail
+```
+
+---
+
+## 🔧 必需配置:启用订阅管理
+
+### 为什么需要配置 KV?
+
+订阅管理功能依赖 Cloudflare KV 存储数据:
+
+**不配置 KV:**
+
+- ✅ 快速转换可用
+- ❌ 订阅管理不可用
+- ❌ 短链接不可用
+
+**配置 KV:**
+
+- ✅ 全部功能可用
+- ✅ 可保存多个订阅
+- ✅ 数据持久化
+
+**强烈建议配置 KV!** 以下是详细步骤:
+
+---
+
+### 第一步:创建 KV 命名空间
+
+#### 通过 Dashboard 创建
+
+1. 在 Cloudflare Dashboard 中,点击 **Workers & Pages**
+2. 点击顶部的 **KV** 标签
+3. 点击 **Create a namespace**
+4. 输入名称:`SUBSCRIPTION_STORE`
+5. 点击 **Add**
+
+#### 通过 CLI 创建(可选)
+
+```bash
+wrangler kv:namespace create "SUBSCRIPTION_STORE"
+# 记录输出的 namespace id
+```
+
+---
+
+### 第二步:绑定 KV 到 Worker
+
+#### 通过 Dashboard 绑定
+
+1. 进入你的 Worker 页面
+2. 点击 **Settings** 标签
+3. 选择左侧的 **Variables**
+4. 滚动到 **KV Namespace Bindings** 部分
+5. 点击 **Add binding**
+6. 填写:
+ - **Variable name**: `SUBSCRIPTION_KV` ⚠️ **必须用这个名字**
+ - **KV namespace**: 选择 `SUBSCRIPTION_STORE`
+7. 点击 **Save and deploy**
+
+#### 通过 Wrangler 绑定(可选)
+
+在 `wrangler.toml` 中添加:
+
+```toml
+[[kv_namespaces]]
+binding = "SUBSCRIPTION_KV"
+id = "your_namespace_id"
+```
+
+重新部署:
+
+```bash
+wrangler deploy
+```
+
+---
+
+### 第三步:验证配置
+
+1. 访问你的 Worker URL
+2. 切换到「订阅管理」标签
+3. 如果看到「+ 添加新订阅」按钮
+4. 说明配置成功!
+
+**如果显示"KV 未配置":**
+
+- 检查变量名是否为 `SUBSCRIPTION_KV`
+- 检查是否点击了 Save and deploy
+- 强制刷新页面(Ctrl+Shift+R)
+
+---
+
+## 💾 订阅数据存储机制
+
+### 存储架构概览
+
+本工具使用 Cloudflare KV 的**三层存储结构**来管理订阅数据:
+
+```
+┌─────────────────────────────────────────────────┐
+│ Cloudflare KV Storage │
+├─────────────────────────────────────────────────┤
+│ │
+│ 1️⃣ 订阅配置存储 │
+│ Key: sub:{subscriptionId} │
+│ Value: {...订阅详细信息...} │
+│ 生命周期: 永久 │
+│ │
+│ 2️⃣ 短链接映射存储 │
+│ Key: {shortId} │
+│ Value: clashUrl │
+│ 生命周期: 7-365天(可配置) │
+│ │
+│ 3️⃣ 订阅列表索引 │
+│ Key: subscriptions:list │
+│ Value: [id1, id2, id3, ...] │
+│ 生命周期: 永久 │
+│ │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+### 详细存储说明
+
+#### 1️⃣ 订阅配置存储
+
+**键格式**: `sub:{subscriptionId}`
+**值类型**: JSON 对象
+**生命周期**: 永久(手动删除才会消失)
+
+**存储内容**:
+
+```json
+{
+ "id": "abc12345", // 订阅唯一ID(8位随机字符)
+ "name": "我的订阅", // 用户设置的订阅名称
+ "clashUrl": "https://...", // 原始订阅链接(Clash或V2Ray)
+ "subscriptionType": "clash", // 订阅源类型:clash/v2ray
+ "subscriptionMode": "permanent", // 订阅分类:permanent/temporary
+ "outputType": "v2ray", // 输出类型:auto/clash/v2ray
+ "shortIds": {}, // 短链ID对象(按需生成)
+ "shortIdsExpiry": {}, // 短链过期时间对象
+ "createdAt": 1703001234567, // 创建时间(Unix时间戳)
+ "expiresAt": 1703606034567, // 订阅过期时间(null表示无限期)
+ "expiration": "7d", // 有效期配置(如:7d, 30d, 3m, 1y, unlimited)
+ "updatedAt": 1703001234567 // 最后更新时间
+}
+```
+
+**实际例子**:
+
+```json
+Key: sub:aB7cD9eF
+Value: {
+ "id": "aB7cD9eF",
+ "name": "公司网络",
+ "clashUrl": "https://...",
+ "subscriptionType": "clash",
+ "subscriptionMode": "permanent",
+ "outputType": "v2ray",
+ "shortIds": {},
+ "shortIdsExpiry": {},
+ "createdAt": 1734672000000,
+ "expiresAt": 1737264000000,
+ "expiration": "30d",
+ "updatedAt": 1734672000000
+}
+```
+
+---
+
+#### 2️⃣ 短链接映射存储
+
+**键格式**: `{shortId}`(直接使用短ID)
+**值类型**: JSON 对象(包含 url、type、subscriptionId、outputType)
+**生命周期**: 有过期时间(TTL机制)
+
+**存储内容**:
+
+```json
+Key: xyz98765
+Value: {
+ "url": "https://example.com/clash-subscription",
+ "type": "clash",
+ "subscriptionId": "abc12345",
+ "outputType": "v2ray"
+}
+TTL: 604800 秒(7天)
+```
+
+**TTL 机制**:
+
+- 创建时设置过期时间(expirationTtl)
+- 到期后 KV 自动删除
+- 订阅配置仍保留(可续期或换新链接)
+
+**实际例子**:
+
+```json
+Key: x3Y8z2W
+Value: {
+ "url": "https://sub.example.com/api/v1/client/subscribe?token=abc123",
+ "type": "clash",
+ "subscriptionId": "aB7cD9eF",
+ "outputType": "v2ray"
+}
+TTL: 2592000 秒(30天)
+```
+
+---
+
+#### 3️⃣ 订阅列表索引
+
+**键格式**: `subscriptions:list`(固定键名)
+**值类型**: JSON 数组
+**生命周期**: 永久
+
+**存储内容**:
+
+```json
+["aB7cD9eF", "gH4iJ6kL", "mN1oP8qR"]
+```
+
+**作用**:
+
+- 快速获取所有订阅ID
+- 避免遍历整个 KV 空间
+- 支持高效的列表查询
+
+---
+
+### 数据操作流程
+
+#### 📝 创建订阅
+
+```
+用户输入 → 生成ID → 写入3个地方
+```
+
+详细步骤:
+
+1. 用户提交:名称、Clash URL、有效期
+2. 生成 `subscriptionId`(8位随机字符)
+3. 生成 `shortId`(8位随机字符)
+4. **写入操作**:
+
+ ```javascript
+ // ① 创建短链数据(按需生成,不在创建时预先生成)
+ const shortLinkData = {
+ url: clashUrl,
+ type: subscriptionType,
+ subscriptionId: subscriptionId,
+ outputType: outputType
+ }
+
+ // ② 写入订阅配置
+ await KV.put(`sub:${subscriptionId}`, JSON.stringify(subscription))
+
+ // ③ 更新订阅列表
+ const list = await getList()
+ list.push(subscriptionId)
+ await KV.put('subscriptions:list', JSON.stringify(list))
+ ```
+
+---
+
+#### 📖 获取订阅列表
+
+```
+读取索引 → 遍历读取 → 检查状态 → 返回列表
+```
+
+详细步骤:
+
+1. 读取 `subscriptions:list` 获取所有ID
+2. 循环读取每个 `sub:{id}`
+3. 检查 `expiresAt` 判断是否过期
+4. 组装并返回完整列表
+
+**性能**:
+
+- 假设有 10 个订阅
+- 需要 11 次 KV 读取(1次索引 + 10次配置)
+- 免费版每天 100K 读取,完全够用
+
+---
+
+#### 🔄 续期短链
+
+```
+读取配置 → 更新TTL → 更新配置
+```
+
+详细步骤:
+
+1. 读取 `sub:{id}` 获取 shortIds 和订阅信息
+2. **重新写入短链**(更新 TTL):
+
+ ```javascript
+ const shortLinkData = {
+ url: subscription.clashUrl,
+ type: subscription.subscriptionType,
+ subscriptionId: id,
+ outputType: subscription.outputType
+ }
+ await KV.put(shortId, JSON.stringify(shortLinkData), {
+ expirationTtl: newDays * 86400
+ })
+ ```
+
+3. **更新订阅配置**:
+
+ ```javascript
+ subscription.shortIdsExpiry[type] = Date.now() + newTTL * 1000
+ subscription.updatedAt = Date.now()
+ await KV.put(`sub:${id}`, JSON.stringify(subscription))
+ ```
+
+**优势**:
+
+- 短链接URL不变
+- v2rayN客户端无需重新配置
+- 只需2次KV操作
+
+---
+
+#### 🔁 换新链接
+
+```
+删除旧链 → 生成新ID → 写入新链 → 更新配置
+```
+
+详细步骤:
+
+1. 读取订阅配置,获取旧的 shortIds
+2. **删除旧短链**:
+
+ ```javascript
+ if (subscription.shortIds[type]) {
+ await KV.delete(subscription.shortIds[type])
+ }
+ ```
+
+3. 生成新的 shortId
+4. **写入新短链**:
+
+ ```javascript
+ const shortLinkData = {
+ url: subscription.clashUrl,
+ type: subscription.subscriptionType,
+ subscriptionId: id,
+ outputType: subscription.outputType
+ }
+ await KV.put(newShortId, JSON.stringify(shortLinkData), {
+ expirationTtl: days * 86400
+ })
+ ```
+
+5. **更新订阅配置**:
+
+ ```javascript
+ subscription.shortIds[type] = newShortId
+ subscription.shortIdsExpiry[type] = Date.now() + newTTL * 1000
+ subscription.updatedAt = Date.now()
+ await KV.put(`sub:${id}`, JSON.stringify(subscription))
+ ```
+
+**注意**:
+
+- 旧短链立即失效
+- 需要在v2rayN中更新订阅链接
+- 用于应对链接泄露的情况
+
+---
+
+#### 🗑️ 删除订阅
+
+```
+删除短链 → 删除配置 → 更新索引
+```
+
+详细步骤:
+
+1. 读取配置获取 shortIds 对象
+2. **删除所有短链映射**:
+
+ ```javascript
+ for (const type in subscription.shortIds) {
+ await KV.delete(subscription.shortIds[type])
+ }
+ ```
+
+3. **删除订阅配置**:`await KV.delete(`sub:${id}`)`
+4. **更新订阅列表**:
+
+ ```javascript
+ const list = await getList()
+ const newList = list.filter(x => x !== id)
+ await KV.put('subscriptions:list', JSON.stringify(newList))
+ ```
+
+---
+
+### 存储容量和限制
+
+#### Cloudflare KV 免费版配额
+
+| 指标 | 限制 | 说明 |
+|------|------|------|
+| 存储空间 | 1 GB | 可存储约100万个订阅 |
+| 读取操作 | 100,000 次/天 | 获取订阅、访问短链 |
+| 写入操作 | 1,000 次/天 | 创建、更新、删除订阅 |
+| 删除操作 | 1,000 次/天 | 包含在写入配额中 |
+| 列表操作 | 1,000 次/天 | 遍历键名 |
+
+#### 操作消耗计算
+
+**创建1个订阅**:
+
+- 2次写入(配置 + 索引)
+- 消耗配额:2/1000
+- 注:短链按需生成,不在创建时预先生成
+
+**获取订阅列表**(假设10个订阅):
+
+- 11次读取(1次索引 + 10次配置)
+- 消耗配额:11/100000
+
+**续期1个订阅**:
+
+- 1次读取 + 2次写入
+- 消耗配额:1/100000 + 2/1000
+
+**换新链接**:
+
+- 1次读取 + 1次删除 + 2次写入
+- 消耗配额:1/100000 + 3/1000
+
+---
+
+#### 实际使用场景
+
+**个人使用(推荐)**:
+
+```
+订阅数量:10-50 个
+短链有效期:30-90 天
+每天操作:
+ - 查看列表 5 次 → 55 次读取
+ - 访问短链 50 次 → 50 次读取
+ - 创建订阅 1 次 → 2 次写入
+ - 续期订阅 1 次 → 2 次写入
+
+总消耗:
+ - 读取:105 / 100,000 = 0.1%
+ - 写入:4 / 1,000 = 0.4%
+
+结论:完全在免费额度内 ✅
+```
+
+**小团队使用**:
+
+```
+订阅数量:100-200 个
+短链有效期:60-180 天
+每天操作:
+ - 查看列表 20 次 → 4,020 次读取
+ - 访问短链 500 次 → 500 次读取
+ - 创建订阅 5 次 → 10 次写入
+ - 续期订阅 5 次 → 10 次写入
+
+总消耗:
+ - 读取:4,520 / 100,000 = 4.5%
+ - 写入:20 / 1,000 = 2.0%
+
+结论:免费额度足够 ✅
+```
+
+**高频使用(需付费)**:
+
+```
+- 写入 > 1,000 次/天
+- 读取 > 100,000 次/天
+建议:升级到付费版
+```
+
+---
+
+### 数据持久性
+
+#### 永久保存的数据
+
+- ✅ 订阅配置(`sub:{id}`)
+- ✅ 订阅列表索引(`subscriptions:list`)
+- ✅ 不会因为 Worker 重新部署而丢失
+
+#### 有生命周期的数据
+
+- ⏰ 短链接(`{shortId}`)
+- ⏰ 创建时设置 TTL
+- ⏰ 到期自动删除
+- ⏰ 但可以续期或换新
+
+#### 数据丢失风险
+
+- ❌ 删除 KV 命名空间 → 所有数据丢失
+- ❌ 解绑 KV → 无法访问数据
+- ✅ Worker 重新部署 → 数据不受影响
+- ✅ 修改代码 → 数据不受影响
+
+---
+
+### 数据安全性
+
+#### 1. 数据隔离
+
+- 每个 Worker 绑定独立的 KV 命名空间
+- 其他人无法访问你的 KV 数据
+- Cloudflare 账号权限控制
+
+#### 2. 访问控制
+
+- 订阅管理功能在前端,无内置权限控制
+- **建议**:不要公开分享 Worker URL
+- **可选**:配置 `ADMIN_TOKEN` 环境变量启用管理员页面
+
+配置管理员 Token:
+
+```
+Worker Settings → Variables → Environment Variables
+Name: ADMIN_TOKEN
+Value: your-secret-token
+```
+
+访问管理页面:
+
+```
+https://your-worker.workers.dev/admin?token=your-secret-token
+```
+
+#### 3. 数据备份
+
+- KV 数据由 Cloudflare 自动备份
+- **建议**:定期导出重要订阅的 Clash URL
+- 可通过 API 或管理页面查看所有订阅
+
+#### 4. 隐私保护
+
+- Clash URL 存储在 KV 中(加密存储)
+- 短链接隐藏了原始 URL
+- **建议**:使用短链接分享,不要直接分享 Clash URL
+
+---
+
+## 📱 使用指南
+
+### 场景 1:快速转换(临时使用)
+
+**适合**:偶尔需要转换、不想保存
+
+**步骤**:
+
+1. 访问 Worker URL
+2. 在「快速转换」标签
+3. 粘贴 Clash 订阅链接
+4. 点击「生成在线链接」或「下载订阅」
+5. 使用生成的链接或文件
+
+---
+
+### 场景 2:保存订阅(长期管理)⭐ 推荐
+
+**适合**:经常使用、需要管理多个订阅
+
+#### 首次添加订阅
+
+1. 切换到「订阅管理」标签
+2. 点击「+ 添加新订阅」
+3. 填写信息:
+
+ ```
+ 订阅名称:公司网络
+ Clash 订阅链接:https://...
+ 短链有效期:30 天
+ ```
+
+4. 点击「保存」
+
+#### 使用订阅
+
+1. 找到订阅卡片
+2. 点击「复制链接」
+3. 在 v2rayN 中:
+
+ ```
+ 订阅 → 订阅设置
+ 添加订阅地址
+ 粘贴链接 → 确定
+ 更新订阅
+ ```
+
+#### 管理订阅
+
+- **续期**:短链快过期 → 点击「续期」→ 设置天数
+ - 优点:链接不变,客户端无需更新
+
+- **换新链接**:担心链接泄露 → 点击「换新链接」→ 设置天数
+ - 注意:旧链接失效,需要更新客户端
+
+- **编辑**:Clash URL 变了 → 点击「编辑」→ 修改并保存
+
+- **删除**:不再需要 → 点击「删除」→ 确认
+
+---
+
+### 场景 3:管理多个订阅
+
+**适合**:多个机场、多个环境
+
+**示例**:
+
+```
+订阅列表:
+├─ 机场A - 美国节点(30天)
+├─ 机场B - 香港节点(60天)
+├─ 公司专线(90天)
+└─ 备用订阅(7天)
+```
+
+**操作**:
+
+1. 分别添加所有订阅
+2. 设置不同的名称和有效期
+3. 在 v2rayN 中添加多个订阅源
+4. 定期检查过期状态
+5. 需要时单独续期或换链接
+
+---
+
+## 🔌 API 接口说明
+
+### 基础接口
+
+#### 1. 网页界面
+
+```http
+GET /
+```
+
+返回 HTML 页面
+
+#### 2. 快速转换(POST)
+
+```http
+POST /convert
+Content-Type: application/json
+
+{
+ "clashUrl": "https://..."
+}
+```
+
+返回 Base64 编码的 v2rayN 订阅
+
+#### 3. 快速转换(GET)
+
+```http
+GET /convert?url=https://...
+```
+
+直接返回订阅内容,用于在线订阅
+
+#### 4. 生成短链
+
+```http
+POST /shorten
+Content-Type: application/json
+
+{
+ "clashUrl": "https://..."
+}
+```
+
+#### 5. 访问短链
+
+```http
+GET /s/{shortId}
+```
+
+---
+
+### 订阅管理 API
+
+#### 创建订阅
+
+```http
+POST /api/subscriptions
+Content-Type: application/json
+
+{
+ "name": "我的订阅",
+ "clashUrl": "https://...",
+ "expirationDays": 30
+}
+```
+
+#### 获取所有订阅
+
+```http
+GET /api/subscriptions
+```
+
+#### 获取单个订阅
+
+```http
+GET /api/subscriptions/{id}
+```
+
+#### 更新订阅
+
+```http
+PUT /api/subscriptions/{id}
+Content-Type: application/json
+
+{
+ "name": "新名称",
+ "clashUrl": "https://..."
+}
+```
+
+#### 删除订阅
+
+```http
+DELETE /api/subscriptions/{id}
+```
+
+#### 续期短链
+
+```http
+POST /api/subscriptions/{id}/renew
+Content-Type: application/json
+
+{
+ "days": 30
+}
+```
+
+#### 换新链接
+
+```http
+POST /api/subscriptions/{id}/regenerate
+Content-Type: application/json
+
+{
+ "days": 30
+}
+```
+
+---
+
+## ❓ 常见问题
+
+### Q1: 必须配置 KV 吗?
+
+**答**:不是必须,但强烈建议
+
+- 不配置:只能用快速转换
+- 配置后:完整功能,可保存订阅
+
+### Q2: 免费版够用吗?
+
+**答**:个人使用完全够用
+
+- 存储:1GB(数万个订阅)
+- 读取:10万次/天
+- 写入:1000次/天
+
+### Q3: 订阅会丢失吗?
+
+**答**:不会
+
+- 订阅配置永久保存
+- 短链接有过期时间
+- Worker 重新部署不影响数据
+
+### Q4: 短链过期了怎么办?
+
+**答**:两种方式
+
+1. **续期**:延长有效期,链接不变
+2. **换新链接**:生成新链接,旧链失效
+
+推荐续期,无需更新客户端。
+
+### Q5: 如何备份数据?
+
+**答**:建议
+
+1. 定期导出订阅列表(API 或页面)
+2. 保存重要的 Clash URL
+3. KV 数据 Cloudflare 自动备份
+
+### Q6: 支持多少个订阅?
+
+**答**:理论上无限
+
+- 免费版存储 1GB
+- 每个订阅约 1KB
+- 可存储约 100 万个
+
+实际建议:10-100个
+
+### Q7: 如何查看日志?
+
+**答**:使用 wrangler
+
+```bash
+wrangler tail
+```
+
+### Q8: 可以自定义域名吗?
+
+**答**:可以
+
+在 Worker 设置中添加自定义域名
+
+### Q9: 数据安全吗?
+
+**答**:安全
+
+- KV 数据加密存储
+- 账号权限隔离
+- 建议不公开分享 Worker URL
+
+### Q10: 转换失败怎么办?
+
+**答**:检查
+
+1. Clash URL 是否有效
+2. 浏览器控制台错误
+3. Worker 日志
+
+---
+
+## 🔄 更新日志
+
+### v3.1 (2024-12-20) 🆕
+
+**订阅类型支持**:
+
+- ✨ 自动识别订阅类型(Clash / V2Ray)
+- ✨ 双向转换:Clash ⇄ V2Ray
+- ✨ 自定义输出类型(保存时选择)
+- ✨ 短链支持类型参数 `?type=clash` 或 `?type=v2ray`
+
+**灵活的有效期管理**:
+
+- ✨ 支持多种时间单位:天(d)、月(m)、年(y)
+- ✨ 支持无限期订阅
+- ✨ 示例:`7d`(7天)、`3m`(3个月)、`1y`(1年)、`unlimited`(无限期)
+
+**UI 改进**:
+
+- ✨ 订阅卡片显示类型标签(Clash/V2Ray)
+- ✨ 显示输出类型和转换方向
+- ✨ 续期/换链接使用模态框界面(替代 prompt)
+- ✨ 时间单位下拉选择(更友好)
+
+**数据结构升级**:
+
+- 📝 订阅对象新增 `subscriptionType` 和 `outputType` 字段
+- 📝 短链存储改为 JSON 对象(包含类型信息)
+- 📝 过期时间格式改为字符串(如 `7d`)
+
+### v4.1 (2025-01)
+
+**有效期选项简化**:
+
+- ✨ 正式订阅有效期简化为:7天、1个月、3个月、1年、3年、长期
+- ✨ 默认选择1年(更适合长期使用场景)
+- ✨ 移除过于细分的有效期选项
+
+**状态标记增强**:
+
+- ✨ 快过期订阅(30天内到期)显示「快过期」橙色标记
+- ✨ 已过期订阅显示「已过期」红色标记
+- ✨ 便于用户及时续期或清理
+
+**UI/UX 改进**:
+
+- ✨ 自定义对话框替代浏览器默认 alert/confirm 弹窗
+- ✨ 统一的模态框风格,更美观
+- ✨ 清空订阅列表功能完善
+
+**代码优化**:
+
+- 📝 移除本地 KV 模拟代码(Wrangler 自带模拟)
+- 📝 优化 API 路由顺序
+- 📝 简化 clearAllSubscriptions 函数
+
+### v3.0 (2024-12-20)
+
+- ✨ 多订阅管理
+- ✨ 短链生命周期管理
+- ✨ 双标签界面
+- ✨ 12个新API端点
+
+### v2.0 (2025-12-19)
+
+- ✨ 在线链接生成
+- ✨ KV 短链接支持
+
+### v1.0 (2025-12-19)
+
+- 🎉 初始版本
+- ✅ 基础转换功能
+
+---
+
+## 📞 技术支持
+
+遇到问题?
+
+1. 查看本文档的常见问题
+2. 检查浏览器控制台
+3. 查看 Worker 日志
+
+**当前版本**: v3.1
+**更新时间**: 2024-12-20
+
+---
+
+## 🆕 v3.1 新功能详细说明
+
+### 1. 订阅类型自动识别
+
+**功能说明**:
+添加订阅时,系统会自动检测订阅链接的类型:
+
+- Clash 格式:YAML 配置文件
+- V2Ray 格式:Base64 编码的节点列表
+
+**识别逻辑**:
+
+1. 尝试下载订阅内容
+2. 检查是否包含 "proxies:" → Clash
+3. 尝试 Base64 解码 → V2Ray
+4. 检查是否包含 vmess:// 等协议 → V2Ray
+
+**使用场景**:
+
+- 无需手动选择类型
+- 支持混合使用多种订阅源
+- 自动标记订阅类型
+
+### 2. 双向转换功能
+
+**支持的转换方向**:
+
+- ✅ Clash → V2Ray(原有功能)
+- ✅ V2Ray → Clash(新增功能)
+- ✅ Clash → Clash(直接透传)
+- ✅ V2Ray → V2Ray(直接透传)
+
+**短链动态转换**:
+
+```
+短链:https://worker.com/s/abc123
+- 默认:https://worker.com/s/abc123(使用配置的输出类型)
+- Clash:https://worker.com/s/abc123?type=clash
+- V2Ray:https://worker.com/s/abc123?type=v2ray
+```
+
+### 3. 灵活的有效期设置
+
+**支持的格式**:
+
+| 格式 | 说明 | 实际时长 |
+|------|------|---------|
+| `7` 或 `7d` | 7天 | 604,800 秒 |
+| `30d` | 30天 | 2,592,000 秒 |
+| `3m` | 3个月 | ~7,776,000 秒 |
+| `1y` | 1年 | ~31,536,000 秒 |
+| `unlimited` | 无限期 | 永不过期 |
+
+### 4. API 变更说明
+
+**创建订阅 API**:
+
+```http
+POST /api/subscriptions
+{
+ "name": "订阅名",
+ "clashUrl": "https://...", // 支持 Clash 或 V2Ray
+ "expiration": "7d", // 支持 d/m/y/unlimited
+ "outputType": "auto" // auto/v2ray/clash
+}
+```
+
+**续期/换链接 API**:
+
+```http
+POST /api/subscriptions/{id}/renew
+POST /api/subscriptions/{id}/regenerate
+{
+ "expiration": "7d", // 支持 d/m/y/unlimited
+ "outputType": "v2ray" // 可选:更改输出类型
+}
+```
+
+---
+
+## 📝 v4.0 新特性详解
+
+### 1. 全新的左右分栏界面
+
+**界面结构**:
+
+```
+┌─────────────────────────────────────────────┐
+│ 📡 订阅管理中心 │
+├─────────────┬───────────────────────────────┤
+│ 订阅列表 │ 订阅详情 │
+│ [+新建] │ │
+│ │ 订阅名称 [正式] [V2Ray] │
+│ > 我的订阅 │ ─────────────────────── │
+│ 机场A │ 原始链接: https://... │
+│ 测试订阅 │ 输出类型: V2Ray │
+│ │ 创建时间: 2025-01-01 │
+│ │ 有效期: 2025-02-01 │
+│ │ │
+│ │ 短链接: https://... │
+│ │ [复制] ✅ 正常使用中 │
+│ │ │
+│ │ [编辑] [续期] [换链接] [删除] │
+└─────────────┴───────────────────────────────┘
+```
+
+**交互流程**:
+
+1. 左侧列表点击订阅 → 右侧显示详情
+2. 点击「新建」→ 弹出添加表单
+3. 点击「编辑」→ 弹出编辑表单
+4. 点击「续期」→ 弹出续期表单
+5. 点击「换链接」→ 弹出换链接表单
+
+### 2. 订阅分类:正式 vs 临时
+
+#### 正式订阅(Permanent)
+
+**特点**:
+
+- ✅ 完整的功能和灵活性
+- ✅ 支持全部有效期选项:1天-无限期
+- ✅ 支持所有单位:天(d)、月(m)、年(y)、无限期
+- ✅ 适合长期使用的订阅
+
+**使用场景**:
+
+- 主力机场订阅
+- 公司专线
+- 长期测试环境
+- 需要永久保存的配置
+
+#### 临时订阅(Temporary)
+
+**特点**:
+
+- ⚡ 限制有效期:只能选择 1天/3天/7天
+- ⚡ 到期自动删除:短链过期后订阅配置也会被清理
+- ⚡ 适合短期临时使用
+
+**使用场景**:
+
+- 临时测试链接
+- 短期分享给朋友
+- 一次性测试任务
+- 避免数据积累
+
+**对比表格**:
+
+| 特性 | 正式订阅 | 临时订阅 |
+|------|---------|---------|
+| 有效期选项 | 1天-无限期 | 仅1天/3天/7天 |
+| 自动删除 | ❌ 不删除 | ✅ 过期自动删除 |
+| 续期 | ✅ 支持任意期限 | ✅ 仅支持1/3/7天 |
+| 换链接 | ✅ 支持任意期限 | ✅ 仅支持1/3/7天 |
+| 用途 | 长期使用 | 短期临时 |
+
+### 3. 智能类型识别与双向转换
+
+**自动识别逻辑**:
+
+```javascript
+// 添加订阅时自动检测
+const url = "https://example.com/subscription"
+// 系统会自动判断:
+// - Clash: 包含 proxies, proxy-groups, rules 等字段
+// - V2Ray: base64 编码的 vmess:// 或 vless:// 链接列表
+```
+
+**转换支持**:
+
+| 原始类型 | 目标类型 | 操作 | 说明 |
+|---------|---------|------|------|
+| Clash | V2Ray | ✅ 转换 | 提取节点转为 V2Ray 格式 |
+| V2Ray | Clash | ✅ 转换 | 解析节点生成 Clash 配置 |
+| Clash | Clash | ⏩ 直传 | 不做转换,直接返回 |
+| V2Ray | V2Ray | ⏩ 直传 | 不做转换,直接返回 |
+
+**短链动态切换**:
+
+```
+短链地址:https://worker.com/s/abc123
+
+可选参数:
+- 默认(自动):https://worker.com/s/abc123
+- 强制 Clash:https://worker.com/s/abc123?type=clash
+- 强制 V2Ray:https://worker.com/s/abc123?type=v2ray
+```
+
+### 4. 多单位有效期系统
+
+**支持的格式**:
+
+| 输入格式 | 说明 | 实际时长(秒) |
+|---------|------|---------------|
+| `7` 或 `7d` | 7天 | 604,800 |
+| `30d` | 30天 | 2,592,000 |
+| `3m` | 3个月 | ~7,776,000 |
+| `6m` | 6个月 | ~15,552,000 |
+| `1y` | 1年 | ~31,536,000 |
+| `unlimited` | 无限期 | null(永不过期) |
+
+**解析规则**:
+
+```javascript
+// parseExpiration() 函数
+"7d" → 7 * 86400 = 604800 秒
+"3m" → 3 * 30 * 86400 = 7776000 秒
+"1y" → 1 * 365 * 86400 = 31536000 秒
+"unlimited" → null(不设置TTL)
+```
+
+**临时订阅限制**:
+
+```javascript
+// 临时订阅只接受以下值:
+✅ "1d" - 1天
+✅ "3d" - 3天
+✅ "7d" - 7天
+❌ "30d", "1m", "1y", "unlimited" - 不允许
+```
+
+### 5. API 完整说明
+
+#### 创建订阅(新增 subscriptionMode)
+
+```http
+POST /api/subscriptions
+Content-Type: application/json
+
+{
+ "name": "订阅名称",
+ "clashUrl": "https://...", // 支持 Clash 或 V2Ray
+ "subscriptionMode": "permanent", // 新增:permanent/temporary
+ "expiration": "7d", // d/m/y/unlimited
+ "outputType": "auto" // auto/v2ray/clash
+}
+```
+
+**Response**:
+
+```json
+{
+ "success": true,
+ "subscription": {
+ "id": "abc123xy",
+ "name": "我的订阅",
+ "clashUrl": "https://...",
+ "subscriptionType": "clash", // 自动识别的类型
+ "subscriptionMode": "permanent", // 订阅分类
+ "outputType": "v2ray",
+ "shortIds": {}, // 短链ID对象(按需生成)
+ "shortIdsExpiry": {}, // 短链过期时间对象
+ "createdAt": 1704067200000,
+ "expiresAt": 1704672000000,
+ "expiration": "7d",
+ "updatedAt": 1704067200000,
+ "isExpired": false
+ }
+}
+```
+
+#### 续期订阅(支持临时订阅限制)
+
+```http
+POST /api/subscriptions/{id}/renew
+
+{
+ "expiration": "7d", // 临时订阅只能 1d/3d/7d
+ "outputType": "clash" // 可选:更改输出类型
+}
+```
+
+#### 换新链接(支持临时订阅限制)
+
+```http
+POST /api/subscriptions/{id}/regenerate
+
+{
+ "expiration": "30d", // 临时订阅只能 1d/3d/7d
+ "outputType": "v2ray" // 可选:更改输出类型
+}
+```
+
+**临时订阅限制示例**:
+
+```javascript
+// 如果是临时订阅 + 传入不合法的期限
+{
+ "subscriptionMode": "temporary",
+ "expiration": "30d" // ❌ 不允许
+}
+// 服务器会自动调整为默认值 "3d"
+```
+
+### 6. 临时订阅自动清理机制
+
+**工作原理**:
+
+1. 临时订阅的短链设置 KV TTL(Time To Live)
+2. 到期后 Cloudflare 自动删除短链 KV 记录
+3. 用户访问订阅列表时,检测到短链已失效
+4. **未来增强**:可以添加定时任务删除过期的订阅配置记录
+
+**当前状态**:
+
+```javascript
+// 短链过期后自动删除(由 KV TTL 机制保证)
+const shortLinkData = {
+ url: subscription.clashUrl,
+ type: subscription.subscriptionType,
+ subscriptionId: id,
+ outputType: subscription.outputType
+}
+await KV.put(shortId, JSON.stringify(shortLinkData), {
+ expirationTtl: days * 86400 // 自动过期
+})
+
+// 订阅配置需要手动删除(访问时检测)
+// TODO: 未来可添加 Cron Trigger 定期清理
+```
+
+---
+
+## 🆕 版本历史
+
+### v4.0(2024-12-20)⭐ 当前版本
+
+**主要更新**:
+
+- 🎨 全新左右分栏 UI 设计
+- 📂 订阅分类:正式订阅 vs 临时订阅
+- ⏱️ 临时订阅自动过期删除
+- 🔧 临时订阅有效期限制(1/3/7天)
+- 🏷️ 订阅列表显示分类标签
+- 🎯 简化用户操作流程
+- 📡 工具更名为「订阅管理中心」
+
+### v3.1(2024-12-20)
+
+**主要更新**:
+
+- ✨ 智能订阅类型识别(Clash/V2Ray)
+- 🔄 双向格式转换(Clash ⇄ V2Ray)
+- 📅 灵活有效期单位(天/月/年/无限期)
+- 🏷️ 订阅类型标签显示
+- 🎨 改进的 UI 反馈
+
+### v3.0(2024-11-XX)
+
+**主要更新**:
+
+- 📋 多订阅管理功能
+- 🔗 短链生命周期管理
+- 💾 KV 存储集成
+- 📊 订阅列表视图
+- ⚙️ 编辑/续期/换链接功能
+
+### v2.0(2024-10-XX)
+
+**主要更新**:
+
+- 🌐 Web UI 界面
+- ⚡ 快速转换模式
+- 📥 直接下载功能
+
+### v1.0(2024-09-XX)
+
+**初始版本**:
+
+- 🔧 基础 Clash → V2Ray 转换
+- 📝 命令行工具
+
+---
+
+**当前版本**: v4.0
+**更新时间**: 2024-12-20
diff --git a/tools/SubConverter/cf_worker_v2ray_converter.js b/tools/SubConverter/cf_worker_v2ray_converter.js
new file mode 100644
index 0000000..1de58b3
--- /dev/null
+++ b/tools/SubConverter/cf_worker_v2ray_converter.js
@@ -0,0 +1,2771 @@
+// Cloudflare Worker: 订阅管理与转换工具 (Subscription Manager & Converter)
+// 部署说明:复制此代码到 Cloudflare Workers 编辑器中
+// 版本:v4.1 - 智能订阅管理系统,支持 Clash/V2Ray 双向转换
+
+// KV 存储结构说明:
+// 1. 短链接键:{shortId} -> JSON {url, type, subscriptionId, outputType} (JSON对象)
+// 2. 订阅配置键:sub:{subscriptionId} -> subscription object (JSON)
+// 3. 订阅索引键:subscriptions:list -> array of subscription IDs (JSON)
+// 4. 订阅类型:permanent (正式) / temporary (临时)
+
+addEventListener('fetch', event => {
+ event.respondWith(handleRequest(event.request))
+})
+
+async function handleRequest(request) {
+ const url = new URL(request.url)
+
+ // 处理根路径 - 返回网页界面
+ if (url.pathname === '/' && request.method === 'GET') {
+ return new Response(getHTML(), {
+ headers: { 'Content-Type': 'text/html;charset=UTF-8' }
+ })
+ }
+
+ // 处理转换请求
+ if (url.pathname === '/convert' && request.method === 'POST') {
+ try {
+ const { clashUrl } = await request.json()
+
+ if (!clashUrl) {
+ return jsonResponse({ error: '请提供 Clash 订阅链接' }, 400)
+ }
+
+ const v2raySubscription = await convertClashToV2ray(clashUrl)
+
+ return new Response(v2raySubscription, {
+ headers: {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'Content-Disposition': 'attachment; filename="v2ray_subscription.txt"'
+ }
+ })
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 处理直接URL转换(用于在线订阅链接)
+ if (url.pathname === '/convert' && request.method === 'GET') {
+ const clashUrl = url.searchParams.get('url')
+
+ if (!clashUrl) {
+ return jsonResponse({ error: '请提供 url 参数' }, 400)
+ }
+
+ try {
+ const v2raySubscription = await convertClashToV2ray(clashUrl)
+ return new Response(v2raySubscription, {
+ headers: {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ 'Pragma': 'no-cache',
+ 'Expires': '0'
+ }
+ })
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 处理生成短链接(如果配置了 KV)
+ if (url.pathname === '/shorten' && request.method === 'POST') {
+ try {
+ const { clashUrl } = await request.json()
+
+ if (!clashUrl) {
+ return jsonResponse({ error: '请提供 Clash 订阅链接' }, 400)
+ }
+
+ // 检查是否配置了 KV
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ // 如果没有配置 KV,直接返回长链接
+ const longUrl = `${url.origin}/convert?url=${encodeURIComponent(clashUrl)}`
+ return jsonResponse({ url: longUrl, type: 'direct' })
+ }
+
+ // 使用 KV 生成短链接
+ const shortId = generateShortId()
+ const expirationTime = 7 * 24 * 60 * 60 // 7天过期
+
+ const metadata = {
+ clashUrl,
+ createdAt: Date.now()
+ }
+ await SUBSCRIPTION_KV.put(shortId, clashUrl, {
+ expirationTtl: expirationTime,
+ metadata
+ })
+
+ const shortUrl = `${url.origin}/s/${shortId}`
+ return jsonResponse({ url: shortUrl, type: 'short', expiresIn: expirationTime })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 处理短链接访问
+ if (url.pathname.startsWith('/s/')) {
+ const shortId = url.pathname.substring(3)
+ const requestedType = url.searchParams.get('type') // clash or v2ray
+
+ try {
+ // 检查是否配置了 KV
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return new Response('短链接功能未启用', { status: 404 })
+ }
+
+ const shortLinkJson = await SUBSCRIPTION_KV.get(shortId)
+
+ if (!shortLinkJson) {
+ return new Response('链接不存在或已过期', { status: 404 })
+ }
+
+ const shortLinkData = JSON.parse(shortLinkJson)
+
+ const sourceUrl = shortLinkData.url
+ const sourceType = shortLinkData.type || 'clash'
+ const defaultOutputType = shortLinkData.outputType || 'v2ray'
+ const targetType = requestedType || defaultOutputType
+
+ let result
+
+ // 根据源类型和目标类型进行转换
+ if (sourceType === 'clash' && targetType === 'v2ray') {
+ result = await convertClashToV2ray(sourceUrl)
+ } else if (sourceType === 'v2ray' && targetType === 'clash') {
+ result = await convertV2rayToClash(sourceUrl)
+ } else if (sourceType === 'clash' && targetType === 'clash') {
+ // 直接返回 clash 订阅
+ const response = await fetch(sourceUrl, {
+ headers: { 'User-Agent': 'ClashForWindows/0.20.39' }
+ })
+ result = await response.text()
+ } else if (sourceType === 'v2ray' && targetType === 'v2ray') {
+ // 直接返回 v2ray 订阅
+ const response = await fetch(sourceUrl, {
+ headers: { 'User-Agent': 'v2rayN/6.0' }
+ })
+ result = await response.text()
+ } else {
+ // 默认:clash to v2ray
+ result = await convertClashToV2ray(sourceUrl)
+ }
+
+ return new Response(result, {
+ headers: {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ 'Pragma': 'no-cache',
+ 'Expires': '0'
+ }
+ })
+ } catch (error) {
+ return new Response(`转换失败: ${error.message}`, { status: 500 })
+ }
+ }
+
+ // ========== 订阅管理 API ==========
+
+ // 创建新订阅
+ if (url.pathname === '/api/subscriptions' && request.method === 'POST') {
+ try {
+ const { name, clashUrl, expiration = '7d', outputType = 'auto', subscriptionMode = 'permanent' } = await request.json()
+
+ if (!name || !clashUrl) {
+ return jsonResponse({ error: '缺少必需参数 name 或 clashUrl' }, 400)
+ }
+
+ // 检查是否配置了 KV
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ // 临时订阅限制有效期
+ let finalExpiration = expiration
+ if (subscriptionMode === 'temporary') {
+ // 临时订阅只能选择 1天、3天、7天
+ if (!['1d', '3d', '7d'].includes(expiration)) {
+ finalExpiration = '3d' // 默认3天
+ }
+ }
+
+ // 自动检测订阅类型
+ const subscriptionType = await detectSubscriptionType(clashUrl)
+
+ // 生成订阅ID
+ const subscriptionId = generateShortId()
+
+ const expirationTime = parseExpiration(finalExpiration)
+
+ // 创建订阅对象(先创建,短链按需生成)
+ const subscription = {
+ id: subscriptionId,
+ name,
+ clashUrl,
+ subscriptionType,
+ subscriptionMode,
+ outputType,
+ shortIds: {}, // 改为对象,存储不同类型的短链: {clash: 'xxx', v2ray: 'yyy'}
+ shortIdsExpiry: {}, // 存储每个短链的过期时间: {clash: timestamp, v2ray: timestamp}
+ createdAt: Date.now(),
+ expiresAt: expirationTime ? Date.now() + expirationTime * 1000 : null,
+ expiration: finalExpiration,
+ updatedAt: Date.now()
+ }
+
+ // 保存订阅
+ await SUBSCRIPTION_KV.put(`sub:${subscriptionId}`, JSON.stringify(subscription))
+
+ // 更新订阅列表索引
+ await addToSubscriptionList(subscriptionId)
+
+ // 短链按需生成,不在这里预先创建
+ subscription.isExpired = expirationTime ? Date.now() > subscription.expiresAt : false
+
+ return jsonResponse({ success: true, subscription })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 获取所有订阅列表
+ if (url.pathname === '/api/subscriptions' && request.method === 'GET') {
+ try {
+ const subscriptionIds = await getSubscriptionList()
+ const subscriptions = []
+
+ for (const id of subscriptionIds) {
+ const data = await SUBSCRIPTION_KV.get(`sub:${id}`)
+ if (data) {
+ const sub = JSON.parse(data)
+ sub.shortIds = sub.shortIds || {}
+ sub.shortIdsExpiry = sub.shortIdsExpiry || {}
+ // 检查是否过期
+ sub.isExpired = sub.expiresAt ? Date.now() > sub.expiresAt : false
+ subscriptions.push(sub)
+ }
+ }
+
+ return jsonResponse({ success: true, subscriptions })
+
+ } catch (error) {
+ console.error('获取订阅列表失败:', error)
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 获取单个订阅详情
+ if (url.pathname.match(/^\/api\/subscriptions\/[^\/]+$/) && request.method === 'GET') {
+ try {
+ const subscriptionId = url.pathname.split('/').pop()
+
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ const data = await SUBSCRIPTION_KV.get(`sub:${subscriptionId}`)
+ if (!data) {
+ return jsonResponse({ error: '订阅不存在' }, 404)
+ }
+
+ const subscription = JSON.parse(data)
+ subscription.shortIds = subscription.shortIds || {}
+ subscription.shortIdsExpiry = subscription.shortIdsExpiry || {}
+ subscription.isExpired = subscription.expiresAt ? Date.now() > subscription.expiresAt : false
+
+ return jsonResponse({ success: true, subscription })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 更新订阅
+ if (url.pathname.match(/^\/api\/subscriptions\/[^\/]+$/) && request.method === 'PUT') {
+ try {
+ const subscriptionId = url.pathname.split('/').pop()
+ const updates = await request.json()
+
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ const data = await SUBSCRIPTION_KV.get(`sub:${subscriptionId}`)
+ if (!data) {
+ return jsonResponse({ error: '订阅不存在' }, 404)
+ }
+
+ const subscription = JSON.parse(data)
+
+ // 更新允许的字段
+ if (updates.name) subscription.name = updates.name
+ if (updates.outputType) subscription.outputType = updates.outputType
+
+ if (updates.clashUrl && updates.clashUrl !== subscription.clashUrl) {
+ // 如果更新了 clashUrl,重新检测类型并更新所有已存在的短链接
+ subscription.clashUrl = updates.clashUrl
+ subscription.subscriptionType = await detectSubscriptionType(updates.clashUrl)
+
+ // 更新所有已存在的短链接数据
+ const ttl = subscription.expiresAt ? Math.floor((subscription.expiresAt - Date.now()) / 1000) : null
+ const putOptions = ttl && ttl > 0
+ ? { expirationTtl: ttl, metadata: { updatedAt: Date.now() } }
+ : { metadata: { updatedAt: Date.now(), permanent: true } }
+
+ subscription.shortIds = subscription.shortIds || {}
+ for (const [linkType, shortId] of Object.entries(subscription.shortIds)) {
+ if (shortId) {
+ const shortLinkData = {
+ url: updates.clashUrl,
+ type: subscription.subscriptionType,
+ subscriptionId,
+ outputType: linkType
+ }
+ await SUBSCRIPTION_KV.put(shortId, JSON.stringify(shortLinkData), putOptions)
+ }
+ }
+ }
+
+ subscription.updatedAt = Date.now()
+
+ // 保存更新后的订阅
+ await SUBSCRIPTION_KV.put(`sub:${subscriptionId}`, JSON.stringify(subscription))
+
+ subscription.shortIds = subscription.shortIds || {}
+ subscription.shortIdsExpiry = subscription.shortIdsExpiry || {}
+ subscription.isExpired = subscription.expiresAt ? Date.now() > subscription.expiresAt : false
+
+ return jsonResponse({ success: true, subscription })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 清空所有KV数据 (必须放在删除订阅路由之前,否则会被正则匹配拦截)
+ if (url.pathname === '/api/subscriptions/clear-all' && request.method === 'POST') {
+ try {
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ // 列出所有 KV 键并逐一删除
+ let cursor = undefined
+ let deletedCount = 0
+
+ do {
+ const listResult = await SUBSCRIPTION_KV.list({ limit: 1000, cursor })
+
+ for (const key of listResult.keys) {
+ await SUBSCRIPTION_KV.delete(key.name)
+ deletedCount++
+ }
+
+ cursor = listResult.list_complete ? undefined : listResult.cursor
+ } while (cursor)
+
+ return jsonResponse({ success: true, message: `已清空所有数据,共删除 ${deletedCount} 条记录` })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 删除订阅
+ if (url.pathname.match(/^\/api\/subscriptions\/[^\/]+$/) && request.method === 'DELETE') {
+ try {
+ const subscriptionId = url.pathname.split('/').pop()
+
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ const data = await SUBSCRIPTION_KV.get(`sub:${subscriptionId}`)
+ if (!data) {
+ return jsonResponse({ error: '订阅不存在' }, 404)
+ }
+
+ const subscription = JSON.parse(data)
+
+ // 删除所有短链接
+ if (subscription.shortIds) {
+ for (const shortId of Object.values(subscription.shortIds)) {
+ if (shortId) {
+ await SUBSCRIPTION_KV.delete(shortId)
+ }
+ }
+ }
+
+ // 删除订阅
+ await SUBSCRIPTION_KV.delete(`sub:${subscriptionId}`)
+
+ // 从订阅列表中移除
+ await removeFromSubscriptionList(subscriptionId)
+
+ return jsonResponse({ success: true, message: '订阅已删除' })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 续期订阅短链
+ if (url.pathname.match(/^\/api\/subscriptions\/[^\/]+\/renew$/) && request.method === 'POST') {
+ try {
+ const subscriptionId = url.pathname.split('/')[3]
+ const { expiration = '7d', outputType, linkType = 'auto' } = await request.json()
+
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ const data = await SUBSCRIPTION_KV.get(`sub:${subscriptionId}`)
+ if (!data) {
+ return jsonResponse({ error: '订阅不存在' }, 404)
+ }
+
+ const subscription = JSON.parse(data)
+
+ subscription.shortIds = subscription.shortIds || {}
+ subscription.shortIdsExpiry = subscription.shortIdsExpiry || {}
+
+ // 临时订阅限制有效期
+ let finalExpiration = expiration
+ if (subscription.subscriptionMode === 'temporary') {
+ if (!['1d', '3d', '7d'].includes(expiration)) {
+ finalExpiration = '3d'
+ }
+ }
+
+ const expirationTime = parseExpiration(finalExpiration)
+ const newExpiryTime = expirationTime ? Date.now() + expirationTime * 1000 : null
+
+ // 更新输出类型(如果提供)
+ if (outputType) {
+ subscription.outputType = outputType
+ }
+
+ // 检查是否已有该类型的短链,如有则续期,没有则创建
+ let targetShortId = subscription.shortIds[linkType]
+
+ if (!targetShortId) {
+ // 创建新的短链
+ targetShortId = generateShortId()
+ subscription.shortIds[linkType] = targetShortId
+ }
+
+ // 更新短链接过期时间
+ const shortLinkData = {
+ url: subscription.clashUrl,
+ type: subscription.subscriptionType || 'clash',
+ subscriptionId,
+ outputType: linkType,
+ subscriptionMode: subscription.subscriptionMode
+ }
+
+ const putOptions = expirationTime
+ ? { expirationTtl: expirationTime, metadata: { renewedAt: Date.now() } }
+ : { metadata: { renewedAt: Date.now(), permanent: true } }
+
+ await SUBSCRIPTION_KV.put(targetShortId, JSON.stringify(shortLinkData), putOptions)
+
+ // 保存该短链的过期时间
+ subscription.shortIdsExpiry[linkType] = newExpiryTime
+
+ // 更新订阅过期时间(取所有短链中最晚的)
+ const allExpiries = Object.values(subscription.shortIdsExpiry).filter(t => t !== null)
+ subscription.expiresAt = allExpiries.length > 0 ? Math.max(...allExpiries) : null
+ subscription.expiration = finalExpiration
+ subscription.updatedAt = Date.now()
+
+ await SUBSCRIPTION_KV.put(`sub:${subscriptionId}`, JSON.stringify(subscription))
+
+ subscription.isExpired = false
+
+ return jsonResponse({
+ success: true,
+ subscription,
+ message: `已续期: ${finalExpiration}`,
+ shortId: targetShortId,
+ shortUrl: `${url.origin}/s/${targetShortId}`
+ })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // 重新生成短链
+ if (url.pathname.match(/^\/api\/subscriptions\/[^\/]+\/regenerate$/) && request.method === 'POST') {
+ try {
+ const subscriptionId = url.pathname.split('/')[3]
+ const { expiration = '7d', outputType, linkType = 'auto' } = await request.json()
+
+ if (typeof SUBSCRIPTION_KV === 'undefined') {
+ return jsonResponse({ error: 'KV 未配置' }, 500)
+ }
+
+ const data = await SUBSCRIPTION_KV.get(`sub:${subscriptionId}`)
+ if (!data) {
+ return jsonResponse({ error: '订阅不存在' }, 404)
+ }
+
+ const subscription = JSON.parse(data)
+
+ subscription.shortIds = subscription.shortIds || {}
+ subscription.shortIdsExpiry = subscription.shortIdsExpiry || {}
+
+ // 临时订阅限制有效期
+ let finalExpiration = expiration
+ if (subscription.subscriptionMode === 'temporary') {
+ if (!['1d', '3d', '7d'].includes(expiration)) {
+ finalExpiration = '3d'
+ }
+ }
+
+ // 删除该类型的旧短链接(如果存在)
+ const oldShortId = subscription.shortIds[linkType]
+ if (oldShortId) {
+ await SUBSCRIPTION_KV.delete(oldShortId)
+ }
+
+ // 生成新短链接
+ const newShortId = generateShortId()
+ const expirationTime = parseExpiration(finalExpiration)
+ const newExpiryTime = expirationTime ? Date.now() + expirationTime * 1000 : null
+
+ // 更新输出类型(如果提供)
+ if (outputType) {
+ subscription.outputType = outputType
+ }
+
+ const shortLinkData = {
+ url: subscription.clashUrl,
+ type: subscription.subscriptionType || 'clash',
+ subscriptionId,
+ outputType: linkType,
+ subscriptionMode: subscription.subscriptionMode
+ }
+
+ const putOptions = expirationTime
+ ? { expirationTtl: expirationTime, metadata: { regeneratedAt: Date.now() } }
+ : { metadata: { regeneratedAt: Date.now(), permanent: true } }
+
+ await SUBSCRIPTION_KV.put(newShortId, JSON.stringify(shortLinkData), putOptions)
+
+ // 更新订阅中该类型的短链
+ subscription.shortIds[linkType] = newShortId
+ subscription.shortIdsExpiry[linkType] = newExpiryTime
+
+ // 更新订阅过期时间(取所有短链中最晚的)
+ const allExpiries = Object.values(subscription.shortIdsExpiry).filter(t => t !== null)
+ subscription.expiresAt = allExpiries.length > 0 ? Math.max(...allExpiries) : null
+ subscription.expiration = finalExpiration
+ subscription.updatedAt = Date.now()
+
+ await SUBSCRIPTION_KV.put(`sub:${subscriptionId}`, JSON.stringify(subscription))
+
+ subscription.isExpired = false
+
+ return jsonResponse({
+ success: true,
+ subscription,
+ message: '已生成新短链',
+ shortId: newShortId,
+ shortUrl: `${url.origin}/s/${newShortId}`
+ })
+
+ } catch (error) {
+ return jsonResponse({ error: error.message }, 500)
+ }
+ }
+
+ // ========== 管理页和API ==========
+
+
+ // 管理页(需要 ADMIN_TOKEN)
+ if (url.pathname === '/admin' && request.method === 'GET') {
+ const adminToken = getAdminToken()
+ if (!adminToken) return new Response('管理功能未启用', { status: 404 })
+ const requestToken = request.headers.get('x-admin-token') || url.searchParams.get('token')
+ if (requestToken !== adminToken) return new Response('未授权', { status: 401 })
+ return new Response(getAdminHTML(), {
+ headers: { 'Content-Type': 'text/html;charset=UTF-8' }
+ })
+ }
+
+ // 列出短链
+ if (url.pathname === '/admin/list' && request.method === 'GET') {
+ const adminToken = getAdminToken()
+ if (!adminToken) return new Response('管理功能未启用', { status: 404 })
+ const requestToken = request.headers.get('x-admin-token') || url.searchParams.get('token')
+ if (requestToken !== adminToken) return new Response('未授权', { status: 401 })
+ if (typeof SUBSCRIPTION_KV === 'undefined') return new Response('KV 未配置', { status: 500 })
+
+ const cursor = url.searchParams.get('cursor') || undefined
+ const listResult = await SUBSCRIPTION_KV.list({ limit: 50, cursor })
+
+ const keys = listResult.keys.map(key => ({
+ id: key.name,
+ expiration: key.expiration || null,
+ metadata: key.metadata || null
+ }))
+
+ return jsonResponse({
+ keys,
+ cursor: listResult.cursor || null,
+ list_complete: listResult.list_complete
+ })
+ }
+
+ // 续期短链
+ if (url.pathname === '/admin/renew' && request.method === 'POST') {
+ const adminToken = getAdminToken()
+ if (!adminToken) return new Response('管理功能未启用', { status: 404 })
+ const requestToken = request.headers.get('x-admin-token') || url.searchParams.get('token')
+ if (requestToken !== adminToken) return new Response('未授权', { status: 401 })
+ if (typeof SUBSCRIPTION_KV === 'undefined') return new Response('KV 未配置', { status: 500 })
+
+ const { id, days = 7 } = await request.json()
+ if (!id) return jsonResponse({ error: '缺少短链 id' }, 400)
+
+ const ttlDays = Math.max(1, parseInt(days, 10) || 7)
+ const expirationTime = ttlDays * 24 * 60 * 60
+
+ const { value, metadata } = await SUBSCRIPTION_KV.getWithMetadata(id, { type: 'text' })
+ if (!value) return jsonResponse({ error: '短链不存在或已过期' }, 404)
+
+ const newMetadata = Object.assign({}, metadata || {}, { renewedAt: Date.now() })
+ await SUBSCRIPTION_KV.put(id, value, {
+ expirationTtl: expirationTime,
+ metadata: newMetadata
+ })
+
+ return jsonResponse({ ok: true, expiresIn: expirationTime })
+ }
+
+ // 删除短链
+ if (url.pathname === '/admin/delete' && request.method === 'POST') {
+ const adminToken = getAdminToken()
+ if (!adminToken) return new Response('管理功能未启用', { status: 404 })
+ const requestToken = request.headers.get('x-admin-token') || url.searchParams.get('token')
+ if (requestToken !== adminToken) return new Response('未授权', { status: 401 })
+ if (typeof SUBSCRIPTION_KV === 'undefined') return new Response('KV 未配置', { status: 500 })
+
+ const { id } = await request.json()
+ if (!id) return jsonResponse({ error: '缺少短链 id' }, 400)
+
+ await SUBSCRIPTION_KV.delete(id)
+ return jsonResponse({ ok: true })
+ }
+
+ return new Response('Not Found', { status: 404 })
+}
+
+// ========== 辅助函数 ==========
+
+// 检测订阅类型
+async function detectSubscriptionType(subscriptionUrl) {
+ try {
+ const response = await fetch(subscriptionUrl, {
+ headers: { 'User-Agent': 'ClashForWindows/0.20.39' },
+ cf: { cacheTtl: 0, cacheEverything: false }
+ })
+
+ if (!response.ok) return 'unknown'
+
+ const text = await response.text()
+
+ // 尝试 Base64 解码检测 v2ray
+ try {
+ const decoded = atob(text.trim())
+ if (decoded.includes('vmess://') || decoded.includes('ss://') ||
+ decoded.includes('trojan://') || decoded.includes('vless://')) {
+ return 'v2ray'
+ }
+ } catch (e) {
+ // 不是 base64,继续检测
+ }
+
+ // 检测是否是 Clash YAML 格式
+ if (text.includes('proxies:') || text.includes('proxy-groups:')) {
+ return 'clash'
+ }
+
+ // 直接包含协议链接(未编码的 v2ray)
+ if (text.includes('vmess://') || text.includes('ss://') ||
+ text.includes('trojan://') || text.includes('vless://')) {
+ return 'v2ray'
+ }
+
+ return 'unknown'
+ } catch (error) {
+ console.error('检测订阅类型失败:', error)
+ return 'unknown'
+ }
+}
+
+// 解析过期时间(支持:7, 30d, 3m, 1y, unlimited)
+function parseExpiration(input) {
+ if (!input || input === 'unlimited' || input === 'never') {
+ return null // 无限期
+ }
+
+ const str = String(input).trim().toLowerCase()
+
+ // 纯数字,默认天
+ if (/^\d+$/.test(str)) {
+ return parseInt(str) * 24 * 60 * 60
+ }
+
+ // 带单位
+ const match = str.match(/^(\d+)([dmy])$/)
+ if (match) {
+ const value = parseInt(match[1])
+ const unit = match[2]
+
+ if (unit === 'd') return value * 24 * 60 * 60
+ if (unit === 'm') return value * 30 * 24 * 60 * 60
+ if (unit === 'y') return value * 365 * 24 * 60 * 60
+ }
+
+ // 默认 7 天
+ return 7 * 24 * 60 * 60
+}
+
+// V2Ray 转 Clash(简化版,支持基本协议)
+async function convertV2rayToClash(v2rayUrl) {
+ const response = await fetch(v2rayUrl, {
+ headers: { 'User-Agent': 'v2rayN/6.0' },
+ cf: { cacheTtl: 0, cacheEverything: false }
+ })
+
+ if (!response.ok) {
+ throw new Error(`下载订阅失败: ${response.status}`)
+ }
+
+ let text = await response.text()
+
+ // 尝试 Base64 解码
+ try {
+ text = atob(text.trim())
+ } catch (e) {
+ // 已经是解码状态
+ }
+
+ const lines = text.split('\n').filter(l => l.trim())
+ const proxies = []
+
+ for (const line of lines) {
+ try {
+ if (line.startsWith('vmess://')) {
+ proxies.push(parseVmessToClash(line))
+ } else if (line.startsWith('ss://')) {
+ proxies.push(parseShadowsocksToClash(line))
+ } else if (line.startsWith('trojan://')) {
+ proxies.push(parseTrojanToClash(line))
+ }
+ } catch (e) {
+ console.error('解析节点失败:', e)
+ }
+ }
+
+ if (proxies.length === 0) {
+ throw new Error('没有找到有效的节点')
+ }
+
+ // 生成 Clash YAML
+ const yaml = generateClashYAML(proxies)
+ return yaml
+}
+
+function parseVmessToClash(vmessLink) {
+ const json = JSON.parse(atob(vmessLink.substring(8)))
+ return {
+ name: json.ps || 'VMess',
+ type: 'vmess',
+ server: json.add,
+ port: parseInt(json.port),
+ uuid: json.id,
+ alterId: parseInt(json.aid || 0),
+ cipher: json.scy || 'auto',
+ network: json.net || 'tcp',
+ tls: json.tls === 'tls',
+ 'skip-cert-verify': true
+ }
+}
+
+function parseShadowsocksToClash(ssLink) {
+ const url = new URL(ssLink)
+ const userinfo = atob(url.username)
+ const [cipher, password] = userinfo.split(':')
+
+ return {
+ name: decodeURIComponent(url.hash.substring(1)) || 'SS',
+ type: 'ss',
+ server: url.hostname,
+ port: parseInt(url.port),
+ cipher: cipher,
+ password: password
+ }
+}
+
+function parseTrojanToClash(trojanLink) {
+ const url = new URL(trojanLink)
+
+ return {
+ name: decodeURIComponent(url.hash.substring(1)) || 'Trojan',
+ type: 'trojan',
+ server: url.hostname,
+ port: parseInt(url.port),
+ password: url.username,
+ 'skip-cert-verify': true
+ }
+}
+
+function generateClashYAML(proxies) {
+ let yaml = 'proxies:\n'
+ for (const proxy of proxies) {
+ yaml += ` - name: "${proxy.name}"\n`
+ yaml += ` type: ${proxy.type}\n`
+ yaml += ` server: ${proxy.server}\n`
+ yaml += ` port: ${proxy.port}\n`
+
+ if (proxy.type === 'vmess') {
+ yaml += ` uuid: ${proxy.uuid}\n`
+ yaml += ` alterId: ${proxy.alterId}\n`
+ yaml += ` cipher: ${proxy.cipher}\n`
+ if (proxy.tls) yaml += ` tls: true\n`
+ } else if (proxy.type === 'ss') {
+ yaml += ` cipher: ${proxy.cipher}\n`
+ yaml += ` password: ${proxy.password}\n`
+ } else if (proxy.type === 'trojan') {
+ yaml += ` password: ${proxy.password}\n`
+ yaml += ` skip-cert-verify: true\n`
+ }
+ }
+ return yaml
+}
+
+async function convertClashToV2ray(clashUrl) {
+ // 1. 下载 Clash 订阅配置
+ const response = await fetch(clashUrl, {
+ headers: {
+ 'User-Agent': 'ClashForWindows/0.20.39'
+ },
+ cf: {
+ cacheTtl: 0,
+ cacheEverything: false
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`下载订阅失败: ${response.status} ${response.statusText}`)
+ }
+
+ const text = await response.text()
+
+ // 2. 解析 YAML (简化版解析器)
+ const config = parseSimpleYAML(text)
+
+ if (!config.proxies || !Array.isArray(config.proxies)) {
+ throw new Error('订阅格式错误: 找不到 proxies 节点')
+ }
+
+ // 3. 转换每个代理节点
+ const v2rayLinks = []
+
+ for (const proxy of config.proxies) {
+ try {
+ const link = convertProxyToV2ray(proxy)
+ if (link) {
+ v2rayLinks.push(link)
+ }
+ } catch (error) {
+ console.error(`转换节点失败: ${proxy.name}`, error)
+ // 继续处理其他节点
+ }
+ }
+
+ if (v2rayLinks.length === 0) {
+ throw new Error('没有成功转换的节点')
+ }
+
+ // 4. 生成最终订阅 (Base64 编码)
+ const allLinks = v2rayLinks.join('\n')
+ return btoa(unescape(encodeURIComponent(allLinks)))
+}
+
+function convertProxyToV2ray(proxy) {
+ const name = encodeURIComponent(proxy.name || 'node')
+ const sniValue = proxy.sni || proxy.servername || ''
+ const fingerprintValue = proxy.fingerprint || proxy['client-fingerprint'] || ''
+
+ // Shadowsocks
+ if (proxy.type === 'ss') {
+ const userInfo = `${proxy.cipher}:${proxy.password}`
+ const userInfoB64 = btoa(userInfo)
+
+ // 处理插件和插件选项
+ let plugin = ''
+ if (proxy.plugin) {
+ plugin = `?plugin=${encodeURIComponent(proxy.plugin)}`
+ if (proxy['plugin-opts']) {
+ const opts = Object.entries(proxy['plugin-opts'])
+ .map(([k, v]) => `${k}=${v}`)
+ .join(';')
+ if (opts) {
+ plugin += encodeURIComponent(';' + opts)
+ }
+ }
+ }
+
+ return `ss://${userInfoB64}@${proxy.server}:${proxy.port}${plugin}#${name}`
+ }
+
+ // VMess
+ if (proxy.type === 'vmess') {
+ const net = proxy.network || 'tcp'
+ const vmessData = {
+ v: '2',
+ ps: proxy.name || 'node',
+ add: proxy.server,
+ port: String(proxy.port),
+ id: proxy.uuid,
+ aid: String(proxy.alterId || 0),
+ scy: proxy.cipher || 'auto',
+ net: net,
+ type: 'none', // 伪装类型,默认none
+ host: '',
+ path: '',
+ tls: proxy.tls ? 'tls' : '',
+ sni: sniValue,
+ alpn: ''
+ }
+
+ if (proxy['skip-cert-verify']) {
+ vmessData.allowInsecure = true
+ }
+
+ // 处理 WebSocket
+ if (net === 'ws' && proxy['ws-opts']) {
+ vmessData.path = proxy['ws-opts'].path || '/'
+ vmessData.host = proxy['ws-opts'].headers?.Host || sniValue || ''
+ }
+
+ // 处理 HTTP/2
+ if (net === 'h2' && proxy['h2-opts']) {
+ vmessData.path = proxy['h2-opts'].path || '/'
+ vmessData.host = Array.isArray(proxy['h2-opts'].host)
+ ? proxy['h2-opts'].host.join(',')
+ : (proxy['h2-opts'].host || '')
+ }
+
+ // 处理 gRPC
+ if (net === 'grpc' && proxy['grpc-opts']) {
+ vmessData.path = proxy['grpc-opts']['grpc-service-name'] || ''
+ vmessData.type = 'gun' // gRPC 的 type
+ }
+
+ // 处理 ALPN
+ if (proxy.alpn) {
+ vmessData.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn.join(',')
+ : proxy.alpn
+ }
+
+ // 处理 fingerprint (TLS)
+ if (fingerprintValue) {
+ vmessData.fp = fingerprintValue
+ }
+
+ const vmessJson = JSON.stringify(vmessData)
+ const vmessB64 = btoa(unescape(encodeURIComponent(vmessJson)))
+ return `vmess://${vmessB64}`
+ }
+
+ // Trojan
+ if (proxy.type === 'trojan') {
+ const params = new URLSearchParams()
+
+ if (sniValue) params.set('sni', sniValue)
+ if (proxy.alpn) {
+ const alpn = Array.isArray(proxy.alpn) ? proxy.alpn.join(',') : proxy.alpn
+ params.set('alpn', alpn)
+ }
+ if (proxy['skip-cert-verify']) params.set('allowInsecure', '1')
+
+ // WebSocket 传输
+ if (proxy.network === 'ws' || proxy['ws-opts']) {
+ params.set('type', 'ws')
+ if (proxy['ws-opts']?.path) params.set('path', proxy['ws-opts'].path)
+ if (proxy['ws-opts']?.headers?.Host) params.set('host', proxy['ws-opts'].headers.Host)
+ }
+
+ // gRPC 传输
+ if (proxy.network === 'grpc' || proxy['grpc-opts']) {
+ params.set('type', 'grpc')
+ if (proxy['grpc-opts']?.['grpc-service-name']) {
+ params.set('serviceName', proxy['grpc-opts']['grpc-service-name'])
+ }
+ if (proxy['grpc-opts']?.mode) {
+ params.set('mode', proxy['grpc-opts'].mode)
+ }
+ }
+
+ const query = params.toString() ? `?${params.toString()}` : ''
+ return `trojan://${proxy.password}@${proxy.server}:${proxy.port}${query}#${name}`
+ }
+
+ // VLESS (部分 Clash 版本支持)
+ if (proxy.type === 'vless') {
+ const params = new URLSearchParams()
+
+ params.set('encryption', proxy.cipher || 'none')
+ if (proxy.flow) params.set('flow', proxy.flow)
+
+ // TLS/Reality 设置
+ if (proxy.tls) {
+ params.set('security', 'tls')
+ if (sniValue) params.set('sni', sniValue)
+ if (proxy['skip-cert-verify']) params.set('allowInsecure', '1')
+ if (fingerprintValue) params.set('fp', fingerprintValue)
+ }
+
+ // Reality 协议支持
+ if (proxy['reality-opts']) {
+ params.set('security', 'reality')
+ if (proxy['reality-opts']['server-name'] && !sniValue) {
+ params.set('sni', proxy['reality-opts']['server-name'])
+ } else if (sniValue) {
+ params.set('sni', sniValue)
+ }
+ if (proxy['reality-opts']['public-key']) {
+ params.set('pbk', proxy['reality-opts']['public-key'])
+ }
+ if (proxy['reality-opts']['short-id']) {
+ params.set('sid', proxy['reality-opts']['short-id'])
+ }
+ if (proxy['reality-opts']['fingerprint']) {
+ params.set('fp', proxy['reality-opts']['fingerprint'])
+ } else if (fingerprintValue) {
+ params.set('fp', fingerprintValue)
+ }
+ if (proxy['reality-opts'].dest) {
+ params.set('dest', proxy['reality-opts'].dest)
+ }
+ if (proxy['reality-opts'].spiderx || proxy['reality-opts'].spiderX) {
+ params.set('spx', proxy['reality-opts'].spiderx || proxy['reality-opts'].spiderX)
+ }
+ }
+
+ // 传输协议
+ const network = proxy.network || 'tcp'
+ params.set('type', network)
+
+ if (network === 'ws' && proxy['ws-opts']) {
+ if (proxy['ws-opts'].path) params.set('path', proxy['ws-opts'].path)
+ const wsHost = proxy['ws-opts'].headers?.Host || sniValue
+ if (wsHost) params.set('host', wsHost)
+ }
+
+ // XHTTP (httpupgrade) 传输
+ if (network === 'xhttp' && proxy['xhttp-opts']) {
+ if (proxy['xhttp-opts'].path) params.set('path', proxy['xhttp-opts'].path)
+ const xhHost = proxy['xhttp-opts'].host || sniValue
+ if (xhHost) params.set('host', xhHost)
+ if (proxy['xhttp-opts'].mode) params.set('mode', proxy['xhttp-opts'].mode)
+ }
+
+ if (network === 'grpc' && proxy['grpc-opts']) {
+ params.set('serviceName', proxy['grpc-opts']['grpc-service-name'] || '')
+ if (proxy['grpc-opts'].mode) params.set('mode', proxy['grpc-opts'].mode)
+ }
+
+ if (network === 'h2' && proxy['h2-opts']) {
+ if (proxy['h2-opts'].path) params.set('path', proxy['h2-opts'].path)
+ if (proxy['h2-opts'].host) {
+ const host = Array.isArray(proxy['h2-opts'].host)
+ ? proxy['h2-opts'].host.join(',')
+ : proxy['h2-opts'].host
+ params.set('host', host)
+ } else if (sniValue) {
+ params.set('host', sniValue)
+ }
+ }
+
+ // ALPN
+ if (proxy.alpn) {
+ const alpn = Array.isArray(proxy.alpn) ? proxy.alpn.join(',') : proxy.alpn
+ params.set('alpn', alpn)
+ }
+
+ return `vless://${proxy.uuid}@${proxy.server}:${proxy.port}?${params.toString()}#${name}`
+ }
+
+ // Hysteria (v1)
+ if (proxy.type === 'hysteria') {
+ const params = new URLSearchParams()
+
+ if (proxy.protocol) params.set('protocol', proxy.protocol)
+ if (proxy.up) params.set('upmbps', proxy.up)
+ if (proxy.down) params.set('downmbps', proxy.down)
+ if (proxy.obfs) params.set('obfs', proxy.obfs)
+ if (proxy['obfs-password']) params.set('obfsParam', proxy['obfs-password'])
+ if (proxy.alpn) {
+ const alpn = Array.isArray(proxy.alpn) ? proxy.alpn.join(',') : proxy.alpn
+ params.set('alpn', alpn)
+ }
+ if (proxy['skip-cert-verify']) params.set('insecure', '1')
+ if (sniValue) params.set('peer', sniValue)
+
+ const auth = proxy.auth || proxy.password || ''
+ return `hysteria://${proxy.server}:${proxy.port}?auth=${encodeURIComponent(auth)}&${params.toString()}#${name}`
+ }
+
+ // Hysteria2
+ if (proxy.type === 'hysteria2' || proxy.type === 'hy2') {
+ const params = new URLSearchParams()
+
+ if (proxy.password) params.set('password', proxy.password)
+ if (proxy.obfs) {
+ params.set('obfs', proxy.obfs)
+ if (proxy['obfs-password']) params.set('obfs-password', proxy['obfs-password'])
+ }
+ if (sniValue) params.set('sni', sniValue)
+ if (proxy['skip-cert-verify']) params.set('insecure', '1')
+ if (fingerprintValue) params.set('pinSHA256', fingerprintValue)
+
+ return `hysteria2://${proxy.password}@${proxy.server}:${proxy.port}?${params.toString()}#${name}`
+ }
+
+ console.log(`不支持的协议类型: ${proxy.type}`)
+ return null
+}
+
+// 增强的 YAML 解析器(支持嵌套对象)
+function parseSimpleYAML(yamlText) {
+ const config = { proxies: [] }
+ const lines = yamlText.split('\n')
+ let inProxies = false
+ let currentProxy = null
+ let currentNested = null
+ let nestedKey = null
+
+ for (let line of lines) {
+ const trimmed = line.trim()
+
+ if (trimmed === 'proxies:') {
+ inProxies = true
+ continue
+ }
+
+ if (!inProxies) continue
+
+ // 检测新的代理节点 ( - name: xxx)
+ if (line.match(/^ - name:/)) {
+ if (currentProxy) {
+ config.proxies.push(currentProxy)
+ }
+ currentProxy = {}
+ currentNested = null
+ nestedKey = null
+ const match = line.match(/name:\s*(.+)/)
+ if (match) currentProxy.name = match[1].replace(/['"]/g, '').trim()
+ continue
+ }
+
+ // 检测嵌套对象开始 ( xxx-opts:) 或数组开始
+ if (currentProxy && line.match(/^ [\w-]+:/)) {
+ const match = line.match(/^ ([\w-]+):\s*(.*)$/)
+ if (match) {
+ const key = match[1]
+ let value = match[2].trim()
+
+ // 如果值为空,表示这是一个嵌套对象或数组的开始
+ if (!value || value === '') {
+ nestedKey = key
+ // 检查下一行是否是数组项(以 - 开头)
+ currentNested = {}
+ currentProxy[key] = currentNested
+ } else {
+ // 普通键值对
+ value = parseValue(value)
+ currentProxy[key] = value
+ currentNested = null
+ nestedKey = null
+ }
+ }
+ continue
+ }
+
+ // 处理数组项 ( - value)
+ if (currentProxy && line.match(/^ - /)) {
+ const match = line.match(/^ - (.+)/)
+ if (match && nestedKey) {
+ const value = parseValue(match[1])
+ // 如果还不是数组,转换为数组
+ if (!Array.isArray(currentProxy[nestedKey])) {
+ currentProxy[nestedKey] = []
+ }
+ currentProxy[nestedKey].push(value)
+ }
+ continue
+ }
+
+ // 解析嵌套对象的属性 ( key: value)
+ if (currentNested && line.match(/^ [\w-]+:/)) {
+ const match = line.match(/^ ([\w-]+):\s*(.+)/)
+ if (match) {
+ const key = match[1]
+ let value = parseValue(match[2])
+
+ // 特殊处理 headers
+ if ((nestedKey === 'ws-opts' || nestedKey === 'h2-opts') && key === 'headers') {
+ currentNested.headers = {}
+ continue
+ }
+
+ currentNested[key] = value
+ }
+ continue
+ }
+
+ // 解析更深层嵌套 ( Host: xxx)
+ if (currentNested && line.match(/^ [\w-]+:/)) {
+ const match = line.match(/^ ([\w-]+):\s*(.+)/)
+ if (match) {
+ const key = match[1]
+ const value = parseValue(match[2])
+
+ if (currentNested.headers) {
+ currentNested.headers[key] = value
+ }
+ }
+ continue
+ }
+
+ // 检测新的 section(退出 proxies)
+ if (inProxies && line.match(/^[\w-]+:/) && !line.match(/^ /)) {
+ if (currentProxy) {
+ config.proxies.push(currentProxy)
+ }
+ break
+ }
+ }
+
+ if (currentProxy) {
+ config.proxies.push(currentProxy)
+ }
+
+ return config
+}
+
+// 解析 YAML 值
+function parseValue(str) {
+ // 去掉行内注释(# 之后的内容)
+ const noComment = str.split('#')[0]
+ let value = noComment.replace(/['"]/g, '').trim()
+
+ // 数字
+ if (/^\d+$/.test(value)) {
+ return parseInt(value)
+ }
+ // 布尔值
+ if (value === 'true') return true
+ if (value === 'false') return false
+
+ // 数组 [a, b, c]
+ if (value.startsWith('[') && value.endsWith(']')) {
+ return value.slice(1, -1).split(',').map(v => v.trim())
+ }
+
+ return value
+}
+
+// 生成短ID
+function generateShortId() {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ let result = ''
+ for (let i = 0; i < 8; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
+ }
+ return result
+}
+
+// 获取订阅列表
+async function getSubscriptionList() {
+ if (typeof SUBSCRIPTION_KV === 'undefined') return []
+ const data = await SUBSCRIPTION_KV.get('subscriptions:list')
+ return data ? JSON.parse(data) : []
+}
+
+// 添加到订阅列表
+async function addToSubscriptionList(subscriptionId) {
+ if (typeof SUBSCRIPTION_KV === 'undefined') return
+ const list = await getSubscriptionList()
+ if (!list.includes(subscriptionId)) {
+ list.push(subscriptionId)
+ await SUBSCRIPTION_KV.put('subscriptions:list', JSON.stringify(list))
+ }
+}
+
+// 从订阅列表中移除
+async function removeFromSubscriptionList(subscriptionId) {
+ if (typeof SUBSCRIPTION_KV === 'undefined') return
+ const list = await getSubscriptionList()
+ const newList = list.filter(id => id !== subscriptionId)
+ await SUBSCRIPTION_KV.put('subscriptions:list', JSON.stringify(newList))
+}
+
+function getAdminToken() {
+ if (typeof ADMIN_TOKEN === 'undefined') return null
+ return ADMIN_TOKEN || null
+}
+
+function jsonResponse(data, status = 200) {
+ return new Response(JSON.stringify(data), {
+ status,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+
+function getHTML() {
+ return `
+
+
+
+
+ 订阅管理中心
+
+
+
+
+
+
+
+
+
+
+
+
📡
+
选择一个订阅
+
查看详情或生成转换链接
+
+
+
+
+
+
+
+
+
🔗 转换链接
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+}
+
+function getAdminHTML() {
+ return `
+
+
+
+
+ 短链管理
+
+
+
+
+
短链管理
+
使用 token 访问本页。可续期、删除现有短链。
+
+
+ 默认续期 7 天
+
+
+
+
+
+ | ID |
+ 到期时间 |
+ 原始 Clash 链接 |
+ 操作 |
+
+
+
+
+
+
提示:当前列表最多显示 50 条,如需更多可使用 cursor 翻页。
+
+
+
+
+`
+}