// 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)
}
// 检测是否使用了本服务的短链接作为源
const selfShortLinkInfo = await checkSelfShortLink(clashUrl, url.origin)
if (selfShortLinkInfo.isSelfLink) {
return jsonResponse({
error: '不能使用本服务生成的短链接作为订阅源。请使用原始订阅链接,或直接复制现有订阅。',
hint: selfShortLinkInfo.originalUrl ? `原始订阅地址: ${selfShortLinkInfo.originalUrl}` : null
}, 400)
}
// 临时订阅限制有效期
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)
}
}
// 获取单个订阅详情 (排除特殊路径: export, import, clear-all)
if (url.pathname.match(/^\/api\/subscriptions\/[^\/]+$/) && request.method === 'GET') {
const subscriptionId = url.pathname.split('/').pop()
// 跳过特殊路径,让后面的路由处理
if (['export', 'import', 'clear-all'].includes(subscriptionId)) {
// 继续到下一个路由
} else {
try {
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.subscriptionMode && ['permanent', 'temporary'].includes(updates.subscriptionMode)) {
subscription.subscriptionMode = updates.subscriptionMode
}
// 更新有效期
let expirationChanged = false
if (updates.expiration) {
// 临时订阅限制有效期
let finalExpiration = updates.expiration
if (subscription.subscriptionMode === 'temporary') {
if (!['1d', '3d', '7d'].includes(updates.expiration)) {
finalExpiration = '3d'
}
}
subscription.expiration = finalExpiration
const expirationTime = parseExpiration(finalExpiration)
subscription.expiresAt = expirationTime ? Date.now() + expirationTime * 1000 : null
expirationChanged = true
}
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()
// 如果有效期变更,更新所有短链的过期时间
if (expirationChanged && subscription.shortIds) {
const expirationTime = parseExpiration(subscription.expiration)
const newExpiryTime = expirationTime ? Date.now() + expirationTime * 1000 : null
for (const [linkType, shortId] of Object.entries(subscription.shortIds)) {
if (shortId) {
const shortLinkData = {
url: subscription.clashUrl,
type: subscription.subscriptionType || 'clash',
subscriptionId,
outputType: linkType,
subscriptionMode: subscription.subscriptionMode
}
const putOptions = expirationTime
? { expirationTtl: expirationTime, metadata: { updatedAt: Date.now() } }
: { metadata: { updatedAt: Date.now(), permanent: true } }
await SUBSCRIPTION_KV.put(shortId, JSON.stringify(shortLinkData), putOptions)
// 更新订阅中记录的短链过期时间
subscription.shortIdsExpiry = subscription.shortIdsExpiry || {}
subscription.shortIdsExpiry[linkType] = newExpiryTime
}
}
}
// 保存更新后的订阅
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)
}
}
// 导出所有订阅(CSV格式)
if (url.pathname === '/api/subscriptions/export' && request.method === 'GET') {
try {
if (typeof SUBSCRIPTION_KV === 'undefined') {
return jsonResponse({ error: 'KV 未配置' }, 500)
}
const subscriptionIds = await getSubscriptionList()
const subscriptions = []
for (const id of subscriptionIds) {
const data = await SUBSCRIPTION_KV.get(`sub:${id}`)
if (data) {
subscriptions.push(JSON.parse(data))
}
}
// 生成 CSV 内容
const csvHeader = 'name,clashUrl,subscriptionType,subscriptionMode,expiration,createdAt,expiresAt'
const csvRows = subscriptions.map(sub => {
const name = `"${(sub.name || '').replace(/"/g, '""')}"`
const clashUrl = `"${(sub.clashUrl || '').replace(/"/g, '""')}"`
const subscriptionType = sub.subscriptionType || 'unknown'
const subscriptionMode = sub.subscriptionMode || 'permanent'
const expiration = sub.expiration || '7d'
const createdAt = sub.createdAt ? new Date(sub.createdAt).toISOString() : ''
const expiresAt = sub.expiresAt ? new Date(sub.expiresAt).toISOString() : ''
return `${name},${clashUrl},${subscriptionType},${subscriptionMode},${expiration},${createdAt},${expiresAt}`
})
const csvContent = [csvHeader, ...csvRows].join('\n')
return new Response(csvContent, {
headers: {
'Content-Type': 'text/csv;charset=UTF-8',
'Content-Disposition': `attachment; filename="subscriptions_${new Date().toISOString().slice(0,10)}.csv"`
}
})
} catch (error) {
return jsonResponse({ error: error.message }, 500)
}
}
// 导入订阅
if (url.pathname === '/api/subscriptions/import' && request.method === 'POST') {
try {
if (typeof SUBSCRIPTION_KV === 'undefined') {
return jsonResponse({ error: 'KV 未配置' }, 500)
}
const { content, expiration = '7d', subscriptionMode = 'permanent' } = await request.json()
if (!content || !content.trim()) {
return jsonResponse({ error: '导入内容不能为空' }, 400)
}
const importedSubscriptions = []
const errors = []
const lines = content.trim().split('\n')
// 检测格式:CSV 或自定义格式
const firstLine = lines[0].trim()
const isCSV = firstLine.toLowerCase().includes('name,') && firstLine.toLowerCase().includes('clashurl')
if (isCSV) {
// 跳过标题行,解析 CSV
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) continue
try {
// 简单 CSV 解析(支持引号包裹的字段)
const fields = parseCSVLine(line)
if (fields.length >= 2) {
const name = fields[0].trim()
const clashUrl = fields[1].trim()
if (name && clashUrl && (clashUrl.startsWith('http://') || clashUrl.startsWith('https://'))) {
importedSubscriptions.push({ name, clashUrl })
}
}
} catch (e) {
errors.push(`行 ${i + 1}: 解析失败`)
}
}
} else {
// 自定义格式:[订阅名称]: [原始链接] 或 [订阅名称]: \n [原始链接]
// 也支持:[订阅名称] \n [原始链接](无冒号)
let currentName = null
let buffer = ''
let lineNumber = 0
for (const line of lines) {
lineNumber++
const trimmedLine = line.trim()
if (!trimmedLine) {
// 空行,检查是否有待处理的订阅
if (currentName && buffer) {
const clashUrl = buffer.trim()
if (clashUrl.startsWith('http://') || clashUrl.startsWith('https://')) {
importedSubscriptions.push({ name: currentName, clashUrl })
} else {
errors.push(`行 ${lineNumber}: URL 格式无效`)
}
currentName = null
buffer = ''
}
continue
}
// 检查是否是纯 URL 行
if (trimmedLine.startsWith('http://') || trimmedLine.startsWith('https://')) {
if (currentName) {
// 有等待中的名称,这个URL作为它的链接
importedSubscriptions.push({ name: currentName, clashUrl: trimmedLine })
currentName = null
buffer = ''
} else {
// 没有名称,使用 URL 的一部分作为名称
try {
const urlObj = new URL(trimmedLine)
const autoName = urlObj.hostname.replace(/^www\./, '') + ' - ' + lineNumber
importedSubscriptions.push({ name: autoName, clashUrl: trimmedLine })
} catch (e) {
errors.push(`行 ${lineNumber}: URL 解析失败`)
}
}
continue
}
// 检查是否是 "名称: 链接" 或 "名称:" 格式
const colonIndex = trimmedLine.indexOf(':')
if (colonIndex > 0) {
// 先保存之前的订阅
if (currentName && buffer) {
const clashUrl = buffer.trim()
if (clashUrl.startsWith('http://') || clashUrl.startsWith('https://')) {
importedSubscriptions.push({ name: currentName, clashUrl })
}
}
const namePart = trimmedLine.substring(0, colonIndex).trim()
const urlPart = trimmedLine.substring(colonIndex + 1).trim()
if (urlPart && (urlPart.startsWith('http://') || urlPart.startsWith('https://'))) {
// 名称和链接在同一行
importedSubscriptions.push({ name: namePart, clashUrl: urlPart })
currentName = null
buffer = ''
} else {
// 只有名称或冒号后面不是 URL,链接可能在下一行
currentName = namePart
buffer = urlPart || ''
}
} else {
// 没有冒号,可能是名称行
if (currentName && buffer) {
// 保存之前的
const clashUrl = buffer.trim()
if (clashUrl.startsWith('http://') || clashUrl.startsWith('https://')) {
importedSubscriptions.push({ name: currentName, clashUrl })
}
}
// 当作新的名称
currentName = trimmedLine
buffer = ''
}
}
// 处理最后一个订阅
if (currentName && buffer) {
const clashUrl = buffer.trim()
if (clashUrl.startsWith('http://') || clashUrl.startsWith('https://')) {
importedSubscriptions.push({ name: currentName, clashUrl })
}
}
}
if (importedSubscriptions.length === 0) {
return jsonResponse({
error: '没有找到有效的订阅数据。请确保格式正确:每行一个 "名称: URL" 或者使用 CSV 格式。',
errors,
hint: '支持的格式: 1) CSV格式(name,clashUrl,...) 2) 名称: URL 3) 名称换行URL',
debug: {
linesCount: lines.length,
firstLine: lines[0]?.substring(0, 50),
isCSV
}
}, 400)
}
// 获取现有订阅列表用于去重
const existingSubscriptionIds = await getSubscriptionList()
const existingUrls = new Set()
for (const id of existingSubscriptionIds) {
const data = await SUBSCRIPTION_KV.get(`sub:${id}`)
if (data) {
const sub = JSON.parse(data)
if (sub.clashUrl) {
existingUrls.add(sub.clashUrl.trim())
}
}
}
// 创建订阅(带去重)
const created = []
const duplicates = []
const selfLinks = []
for (const { name, clashUrl } of importedSubscriptions) {
// 检查是否重复
if (existingUrls.has(clashUrl.trim())) {
duplicates.push({ name, clashUrl })
continue
}
// 检查是否是自引用短链接
const selfCheck = await checkSelfShortLink(clashUrl, url.origin)
if (selfCheck.isSelfLink) {
selfLinks.push({ name, clashUrl, originalUrl: selfCheck.originalUrl })
continue
}
try {
const subscriptionType = await detectSubscriptionType(clashUrl)
const subscriptionId = generateShortId()
let finalExpiration = expiration
if (subscriptionMode === 'temporary') {
if (!['1d', '3d', '7d'].includes(expiration)) {
finalExpiration = '3d'
}
}
const expirationTime = parseExpiration(finalExpiration)
const subscription = {
id: subscriptionId,
name,
clashUrl,
subscriptionType,
subscriptionMode,
outputType: 'auto',
shortIds: {},
shortIdsExpiry: {},
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)
created.push({ id: subscriptionId, name })
// 添加到已存在集合,防止本次导入内的重复
existingUrls.add(clashUrl.trim())
} catch (e) {
errors.push(`"${name}": ${e.message}`)
}
}
let message = `成功导入 ${created.length} 个订阅`
if (duplicates.length > 0) {
message += `,跳过 ${duplicates.length} 个重复`
}
if (selfLinks.length > 0) {
message += `,跳过 ${selfLinks.length} 个自引用短链接`
}
return jsonResponse({
success: true,
message,
created,
duplicates: duplicates.length > 0 ? duplicates : undefined,
selfLinks: selfLinks.length > 0 ? selfLinks : undefined,
errors: errors.length > 0 ? errors : undefined
})
} 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 })
}
// ========== 辅助函数 ==========
// 检测是否使用了本服务的短链接作为源URL
async function checkSelfShortLink(inputUrl, currentOrigin) {
try {
const parsedUrl = new URL(inputUrl)
// 检查是否是同一个域名
if (parsedUrl.origin !== currentOrigin) {
return { isSelfLink: false }
}
// 检查是否是短链接路径
if (!parsedUrl.pathname.startsWith('/s/')) {
return { isSelfLink: false }
}
// 提取短链接ID
const shortId = parsedUrl.pathname.substring(3)
if (!shortId || typeof SUBSCRIPTION_KV === 'undefined') {
return { isSelfLink: false }
}
// 尝试从KV获取短链接信息
const shortLinkJson = await SUBSCRIPTION_KV.get(shortId)
if (!shortLinkJson) {
// 短链接不存在,但URL格式匹配,仍然阻止
return { isSelfLink: true, originalUrl: null }
}
const shortLinkData = JSON.parse(shortLinkJson)
return {
isSelfLink: true,
originalUrl: shortLinkData.url,
subscriptionId: shortLinkData.subscriptionId,
sourceType: shortLinkData.type
}
} catch (e) {
// 解析失败,说明不是有效URL或不是本服务链接
return { isSelfLink: false }
}
}
// 检测订阅类型
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))
}
// 解析 CSV 行(支持引号包裹的字段)
function parseCSVLine(line) {
const result = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
// 转义的引号
current += '"'
i++
} else {
inQuotes = !inQuotes
}
} else if (char === ',' && !inQuotes) {
result.push(current)
current = ''
} else {
current += char
}
}
result.push(current)
return result
}
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 `
Subscription Manager
👋
Select a subscription to manage
`
}
function getAdminHTML() {
return `
短链管理
短链管理
使用 token 访问本页。可续期、删除现有短链。
默认续期 7 天
提示:当前列表最多显示 50 条,如需更多可使用 cursor 翻页。
`
}