📡
选择一个订阅
查看详情或生成转换链接
订阅名称
📝 基本信息
-
-
-
-
🔗 转换链接
--
// 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 `
查看详情或生成转换链接
| ID | 到期时间 | 原始 Clash 链接 | 操作 |
|---|