Sub-Hub 基于 Cloudflare Worker+D1数据库的简洁订阅管理器，摒弃第三方订阅转换！
Sub-Hub 基于 Cloudflare Worker+D1数据库的简洁订阅管理器，摒弃第三方订阅转换！ github项目地址 Sub-Hub 是一个基于 Cloudflare Workers 的代理节点订阅管理系统。它提供了一个直观的 Web 界面，让您可以轻松管理多个订阅和节点基于Cloudflare Worker 搭建，不需要借助VPS进行部署支持原始格式，BASE64格式，Surge格式、Clash格式（内置模板，需要使用自己的规则可以按需修改）支持SS、VMess、VLESS（除Surge）、Trojan、SOCKS5、Snell（仅Surge）、Hysteria2、Tuic 格式节点的托管本项目不使用任何第三方订阅转换，所以可能有部分协议转换不完整，目前支持的协议经过测试没发现太大问题基于Cursor纯AI代码 越来越屎山了，有问题可以提，但不一定能解决功能特点 🚀 支持多种代理协议SS（Shadowsocks）SS2022（Shadowsocks 2022）VMessTrojanVLESS（除 Surge 外）SOCKS5Snell（仅 Surge）Hysteria2Tuic订阅管理 创建多个独立订阅自定义订阅路径支持批量导入节点节点拖拽排序多种订阅格式原始格式（适用于大多数客户端）Base64 编码格式（/v2ray 路径）Surge 配置格式（/surge 路径）Clash 配置格式（/clash 路径）(内置Clash模板)安全特性管理面板登录认证会话管理安全的 Cookie 设置🎨 现代化界面响应式设计直观的操作界面支持移动设备{dotted startColor="#ff6c6c" endColor="#1989fa"/}创建项目创建名为“sub-hub”新的 Workers 项目创建名为“sub-hub” 的D1 数据库将D1数据库与Cloudflare Workers绑定变量名称 = "DB" 数据库名称 = "sub-hub"初始化数据库，在名为“sub-hub” 的D1 数据库“控制台中执行如下代码”-- 创建订阅表CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE );-- 创建节点表CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, subscription_id INTEGER NOT NULL, name TEXT NOT NULL, original_link TEXT NOT NULL, node_order INTEGER NOT NULL DEFAULT 0, enabled INTEGER DEFAULT 1, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE );-- 创建会话表CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at INTEGER NOT NULL );-- 创建索引CREATE INDEX IF NOT EXISTS idx_subscriptions_path ON subscriptions(path); CREATE INDEX IF NOT EXISTS idx_nodes_subscription_order ON nodes(subscription_id, node_order); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);下面是原初始化数据库方法-- 创建订阅表CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE );-- 创建节点表CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, subscription_id INTEGER NOT NULL, name TEXT NOT NULL, original_link TEXT NOT NULL, node_order INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE );-- 创建会话表CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at INTEGER NOT NULL );-- 创建索引CREATE INDEX IF NOT EXISTS idx_subscriptions_path ON subscriptions(path); CREATE INDEX IF NOT EXISTS idx_nodes_subscription_order ON nodes(subscription_id, node_order); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);配置环境变量，不变在 Cloudflare Dashboard 中设置以下环境变量：ADMIN_PATH: 管理面板路径（默认：admin）ADMIN_USERNAME: 管理员用户名（默认：admin）ADMIN_PASSWORD: 管理员密码（默认：password）部署代码将 “worker.js” 文件内容复制到Cloudflare Workers保存【20250710更新】新增禁用节点功能，被禁用的节点将不显示在订阅链接中A、此次更新需要添加新字段 enabled 到 nodes 表， 或者重新部署数据库ALTER TABLE nodes ADD COLUMN enabled INTEGER DEFAULT 1;这个命令会在表中添加 enabled 字段，并设置默认值为 1。但是请注意：这个默认值只适用于将来插入的新记录。现有记录不会自动更新。B、更新现有记录的 enabled 值为 1UPDATE nodes SET enabled = 1 WHERE enabled IS NULL;这个命令会将当前表中所有 enabled 为空的记录设置为 1。新文件【20250710更新】// 通用的路径验证和节点名称提取函数 function validateSubscriptionPath(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 节点类型常量定义 const NODE_TYPES = { SS: 'ss://', VMESS: 'vmess://', TROJAN: 'trojan://', VLESS: 'vless://', SOCKS: 'socks://', HYSTERIA2: 'hysteria2://', TUIC: 'tuic://', SNELL: 'snell,' }; function extractNodeName(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes(NODE_TYPES.SNELL)) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith(NODE_TYPES.VMESS)) { try { const config = JSON.parse(safeBase64Decode(nodeLink.substring(8))); if (config.ps) { return safeUtf8Decode(config.ps); } } catch {} return '未命名节点'; } // 处理其他使用哈希标记名称的链接类型（SS、TROJAN、VLESS、SOCKS、Hysteria2、TUIC等） const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } export default { async fetch(request, env) { // 解析请求路径和参数 const url = new URL(request.url); const pathname = url.pathname; const method = request.method; // 检查是否有查询参数 if (url.search && !pathname.startsWith('/admin')) { return new Response('Not Found', { status: 404 }); } // 从环境变量获取配置路径，如果未设置则使用默认值 const adminPath = env.ADMIN_PATH || 'admin'; // 从环境变量获取登录凭据 const adminUsername = env.ADMIN_USERNAME || 'admin'; const adminPassword = env.ADMIN_PASSWORD || 'password'; // 处理登录页面请求 if (pathname === `/${adminPath}/login`) { if (method === "GET") { return serveLoginPage(adminPath); } else if (method === "POST") { return handleLogin(request, env, adminUsername, adminPassword, adminPath); } } // 处理登出请求 if (pathname === `/${adminPath}/logout`) { return handleLogout(request, env, adminPath); } // 处理管理面板请求 if (pathname === `/${adminPath}`) { const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return Response.redirect(`${url.origin}/${adminPath}/login`, 302); } return serveAdminPanel(env, adminPath); } // 处理API请求 if (pathname.startsWith(`/${adminPath}/api/`)) { // 验证会话 const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return new Response(JSON.stringify({ success: false, message: '未授权访问' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 处理节点管理API请求 const nodeApiMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)/nodes(?:/([^/]+|reorder))?$`)); if (nodeApiMatch) { const subscriptionPath = nodeApiMatch[1]; const nodeId = nodeApiMatch[2]; try { // 更新节点顺序 if (nodeId === 'reorder' && method === 'POST') { const { orders } = await request.json(); if (!Array.isArray(orders) || orders.length === 0) { return new Response(JSON.stringify({ success: false, message: '无效的排序数据' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } const subscriptionId = subResults[0].id; // 使用事务来确保数据一致性 const statements = []; // 准备更新语句 for (const { id, order } of orders) { statements.push(env.DB.prepare( "UPDATE nodes SET node_order = ? WHERE id = ? AND subscription_id = ?" ).bind(order, id, subscriptionId)); } // 执行批量更新 const result = await env.DB.batch(statements); return new Response(JSON.stringify({ success: true, message: '节点顺序已更新' }), { headers: { 'Content-Type': 'application/json' } }); } // 获取节点列表 if (!nodeId && method === 'GET') { return handleGetNodes(env, subscriptionPath); } // 创建新节点 if (!nodeId && method === 'POST') { return handleCreateNode(request, env, subscriptionPath); } // 更新节点 if (nodeId && nodeId !== 'reorder' && method === 'PUT') { return handleUpdateNode(request, env, subscriptionPath, nodeId); } // 删除节点 if (nodeId && nodeId !== 'reorder' && method === 'DELETE') { return handleDeleteNode(env, subscriptionPath, nodeId); } // 切换节点状态 if (nodeId && nodeId !== 'reorder' && method === 'PATCH') { return handleToggleNode(env, subscriptionPath, nodeId, request); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('API请求处理失败:', error); return new Response(JSON.stringify({ success: false, message: error.message || '服务器内部错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 处理订阅管理API请求 if (pathname.startsWith(`/${adminPath}/api/subscriptions`)) { // 获取单个订阅内容 const getOneMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (getOneMatch && method === 'GET') { return handleGetSubscription(env, getOneMatch[1]); } // 获取订阅列表 if (pathname === `/${adminPath}/api/subscriptions` && method === 'GET') { return handleGetSubscriptions(env); } // 创建新订阅 if (pathname === `/${adminPath}/api/subscriptions` && method === 'POST') { try { const { name, path } = await request.json(); if (!name || !validateSubscriptionPath(path)) { return createErrorResponse('无效的参数', 400); } // 检查路径是否已存在 const { results } = await env.DB.prepare( "SELECT COUNT(*) as count FROM subscriptions WHERE path = ?" ).bind(path).all(); if (results[0].count > 0) { return createErrorResponse('该路径已被使用', 400); } // 创建订阅 const result = await env.DB.prepare( "INSERT INTO subscriptions (name, path) VALUES (?, ?)" ).bind(name, path).run(); if (!result.success) { throw new Error('创建订阅失败'); } return createSuccessResponse(null, '订阅创建成功'); } catch (error) { console.error('创建订阅失败:', error); return createErrorResponse('创建订阅失败: ' + error.message); } } // 更新订阅信息 const updateMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (updateMatch && method === 'PUT') { const data = await request.json(); return handleUpdateSubscriptionInfo(env, updateMatch[1], data); } // 删除订阅 const deleteMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (deleteMatch && method === 'DELETE') { return handleDeleteSubscription(env, deleteMatch[1]); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ success: false, message: 'Not Found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 处理订阅请求 if (pathname.startsWith('/')) { // 检查路径格式是否合法（只允许一级或两级路径，如 /path 或 /path/surge 或 /path/v2ray 或 /path/clash） const pathParts = pathname.split('/').filter(Boolean); if (pathParts.length > 2) { return new Response('Not Found', { status: 404 }); } if (pathParts.length === 2 && !['surge', 'v2ray', 'clash'].includes(pathParts[1])) { return new Response('Not Found', { status: 404 }); } try { // 获取基本路径 let basePath = pathname; if (pathname.endsWith('/surge')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/surge } else if (pathname.endsWith('/v2ray')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/v2ray } else if (pathname.endsWith('/clash')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/clash } else { basePath = pathname.slice(1); // 只移除开头的/ } // 获取订阅信息 const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(basePath).all(); const subscription = results[0]; if (subscription) { // 生成订阅内容 const content = await generateSubscriptionContent(env, basePath); // 根据请求路径返回不同格式的内容 if (pathname.endsWith('/surge')) { // 返回 Surge 格式 const surgeContent = convertToSurge(content); return new Response(surgeContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/v2ray')) { // 返回 Base64 编码格式，排除 snell 节点，包括 VLESS 节点 const filteredContent = filterSnellNodes(content); const base64Content = safeBase64Encode(filteredContent); return new Response(base64Content, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/clash')) { // 返回 Clash 格式 const clashContent = convertToClash(content); return new Response(clashContent, { headers: { 'Content-Type': 'text/yaml; charset=utf-8' }, }); } // 返回普通订阅内容，排除 snell 节点 const filteredContent = filterSnellNodes(content); return new Response(filteredContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } } catch (error) { console.error('处理订阅请求失败:', error); return new Response('Internal Server Error', { status: 500 }); } // 如果没有找到匹配的订阅，返回404 return new Response('Not Found', { status: 404 }); } // 其他所有路径返回 404 return new Response('Not Found', { status: 404 }); }, }; // 添加获取单个订阅的处理函数 async function handleGetSubscription(env, path) { try { const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(path).all(); if (!results || results.length === 0) { return createErrorResponse('订阅不存在', 404); } return createSuccessResponse(results[0]); } catch (error) { console.error('获取订阅内容失败:', error); return createErrorResponse('获取订阅内容失败: ' + error.message); } } // 提供登录页面HTML function serveLoginPage(adminPath) { const html = ` Sub-Hub - 登录 Sub-Hub 请登录以继续使用 用户名或密码错误 用户名 密码 登录 `; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 验证会话 async function verifySession(request, env) { const sessionId = getSessionFromCookie(request); if (!sessionId) return false; const now = Date.now(); const { results } = await env.DB.prepare(` UPDATE sessions SET expires_at = ? WHERE session_id = ? AND expires_at > ? RETURNING * `).bind(now + 24 * 60 * 60 * 1000, sessionId, now).all(); return results.length > 0; } // 从Cookie中获取会话ID function getSessionFromCookie(request) { const cookieHeader = request.headers.get('Cookie') || ''; const sessionCookie = cookieHeader.split(';') .find(cookie => cookie.trim().startsWith('session=')); return sessionCookie ? sessionCookie.trim().substring(8) : null; } // 生成安全的会话令牌 async function generateSecureSessionToken(username, env) { // 清理过期会话和用户旧会话 const now = Date.now(); await env.DB.batch([ env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(now), env.DB.prepare("DELETE FROM sessions WHERE username = ?").bind(username) ]); const sessionId = crypto.randomUUID(); const expiresAt = now + 24 * 60 * 60 * 1000; // 24小时后过期 await env.DB.prepare(` INSERT INTO sessions (session_id, username, expires_at) VALUES (?, ?, ?) `).bind(sessionId, username, expiresAt).run(); return sessionId; } // 处理登录请求 async function handleLogin(request, env, adminUsername, adminPassword, adminPath) { const { username, password } = await request.json(); if (!username || !password || username !== adminUsername || password !== adminPassword) { return new Response(JSON.stringify({ success: false, message: '用户名或密码错误' }), { headers: { 'Content-Type': 'application/json' }, status: 401 }); } const sessionId = await generateSecureSessionToken(username, env); const headers = new Headers({ 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400; Secure` }); return new Response(JSON.stringify({ success: true, message: '登录成功', redirect: `/${adminPath}` }), { headers }); } // 处理登出请求 async function handleLogout(request, env, adminPath) { const sessionId = getSessionFromCookie(request); if (sessionId) { await env.DB.prepare("DELETE FROM sessions WHERE session_id = ?").bind(sessionId).run(); } const headers = new Headers({ 'Set-Cookie': `session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Secure` }); return Response.redirect(`${new URL(request.url).origin}/${adminPath}/login`, 302); } // 修改管理面板HTML生成函数 function serveAdminPanel(env, adminPath) { const html = ` Sub-Hub Sub-Hub 退出 添加订阅 添加新订阅 订阅名称 * 请输入订阅名称 订阅路径 * 路径只能包含小写字母、数字和连字符，长度在5-50个字符之间 取消 创建 添加节点 节点内容 * 取消 添加 编辑订阅信息 订阅名称 * 请输入订阅名称 订阅路径 * 路径只能包含小写字母、数字和连字符，长度在5-50个字符之间 删除订阅 取消 保存 编辑节点 节点内容 * 取消 保存 操作成功! `; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 通用响应头 const JSON_HEADERS = { 'Content-Type': 'application/json' }; // 通用响应创建函数 function createResponse(success, message, data = null, status = success ? 200 : 500) { return new Response( JSON.stringify({ success, message, ...(data && { data }) }), { headers: JSON_HEADERS, status } ); } // 错误响应函数 const createErrorResponse = (message, status = 500) => createResponse(false, message, null, status); // 成功响应函数 const createSuccessResponse = (data = null, message = '操作成功') => createResponse(true, message, data); // 修改获取订阅列表函数 async function handleGetSubscriptions(env) { const { results } = await env.DB.prepare(` SELECT s.path, s.name, COUNT(n.id) as nodeCount FROM subscriptions s LEFT JOIN nodes n ON s.id = n.subscription_id GROUP BY s.id ORDER BY s.id ASC `).all(); const subscriptions = results.map(item => ({ name: item.name, path: item.path, nodeCount: item.nodeCount || 0 })); return createSuccessResponse(subscriptions); } // 添加获取节点列表的函数 async function handleGetNodes(env, subscriptionPath) { const { results } = await env.DB.prepare(` SELECT n.id, n.name, n.original_link, n.node_order, COALESCE(n.enabled, 1) as enabled FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? ORDER BY n.node_order ASC `).bind(subscriptionPath).all(); return createSuccessResponse(results || []); } // 添加创建节点的函数 async function handleCreateNode(request, env, subscriptionPath) { const nodeData = await request.json(); if (!nodeData.content) { return createErrorResponse('缺少节点内容', 400); } const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.trim(); // 尝试Base64解码 try { const decodedContent = safeBase64Decode(originalLink); // 检查解码后的内容是否是有效的节点链接 if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.trim(); } } catch (e) { // 不是Base64格式，继续使用原始内容 } // 验证节点类型 const lowerContent = originalLink.toLowerCase(); const isSnell = lowerContent.includes('=') && lowerContent.includes('snell,'); if (!['ss://', 'vmess://', 'trojan://', 'vless://', 'socks://', 'hysteria2://', 'tuic://'].some(prefix => lowerContent.startsWith(prefix)) && !isSnell) { return createErrorResponse('不支持的节点格式', 400); } // 从节点链接中提取名称 let nodeName = extractNodeName(originalLink); // 直接使用提供的order值 const nodeOrder = nodeData.order; // 直接插入新节点，不更新其他节点的顺序 await env.DB.prepare(` INSERT INTO nodes (subscription_id, name, original_link, node_order, enabled) VALUES (?, ?, ?, ?, 1) `).bind(subscriptionId, nodeName, originalLink, nodeOrder).run(); return createSuccessResponse(null, '节点创建成功'); } // 添加删除节点的函数 async function handleDeleteNode(env, subscriptionPath, nodeId) { try { const result = await env.DB.prepare(` DELETE FROM nodes WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(nodeId, subscriptionPath).run(); return createSuccessResponse(null, '节点已删除'); } catch (error) { return createErrorResponse('删除节点失败: ' + error.message); } } // 添加切换节点状态的函数 async function handleToggleNode(env, subscriptionPath, nodeId, request) { try { const { enabled } = await request.json(); // 验证enabled值 if (typeof enabled !== 'boolean') { return createErrorResponse('无效的状态值', 400); } // 更新节点状态 const result = await env.DB.prepare(` UPDATE nodes SET enabled = ? WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(enabled ? 1 : 0, nodeId, subscriptionPath).run(); if (result.changes === 0) { return createErrorResponse('节点不存在或更新失败', 404); } return createSuccessResponse(null, '节点已' + (enabled ? '启用' : '禁用')); } catch (error) { return createErrorResponse('切换节点状态失败: ' + error.message); } } // 生成订阅内容 async function generateSubscriptionContent(env, path) { if (!path?.trim()) return ''; const { results } = await env.DB.prepare(` SELECT GROUP_CONCAT(n.original_link, CHAR(10)) as content FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? AND n.original_link IS NOT NULL AND (n.enabled IS NULL OR n.enabled = 1) GROUP BY s.id ORDER BY n.node_order ASC `).bind(path).all(); return results?.[0]?.content || ''; } // 解析SIP002格式 function parseSIP002Format(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; const nodeName = name ? decodeURIComponent(name) : '未命名节点'; return `${nodeName} = ss, ${server}, ${port}, encrypt-method=${method}, password=${password}`; } catch { return null; } } // 解析Vmess链接为Surge格式 function parseVmessLink(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; // 正确处理UTF-8编码的中文字符 const name = config.ps ? safeUtf8Decode(config.ps) : '未命名节点'; const configParts = [ `${name} = vmess`, config.add, config.port, `username=${config.id}`, 'vmess-aead=true', `tls=${config.tls === 'tls'}`, `sni=${config.add}`, 'skip-cert-verify=true', 'tfo=false' ]; if (config.tls === 'tls' && config.alpn) { configParts.push(`alpn=${config.alpn.replace(/,/g, ':')}`); } if (config.net === 'ws') { configParts.push('ws=true'); if (config.path) configParts.push(`ws-path=${config.path}`); configParts.push(`ws-headers=Host:${config.host || config.add}`); } return configParts.join(', '); } catch { return null; } } // 解析Trojan链接为Surge格式 function parseTrojanLink(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const configParts = [ `${nodeName} = trojan`, url.hostname, url.port, `password=${url.username}`, 'tls=true', `sni=${url.hostname}`, 'skip-cert-verify=true', 'tfo=false' ]; const alpn = params.get('alpn'); if (alpn) { configParts.push(`alpn=${safeDecodeURIComponent(alpn).replace(/,/g, ':')}`); } if (params.get('type') === 'ws') { configParts.push('ws=true'); const path = params.get('path'); if (path) { configParts.push(`ws-path=${safeDecodeURIComponent(path)}`); } const host = params.get('host'); configParts.push(`ws-headers=Host:${host ? safeDecodeURIComponent(host) : url.hostname}`); } return configParts.join(', '); } catch { return null; } } // 解析SOCKS链接为Surge格式 function parseSocksLink(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { // 处理标准格式：socks://username:password@server:port#name // 或者 socks://base64encoded@server:port#name const url = new URL(socksLink); if (!url.hostname || !url.port) return null; // 提取节点名称 const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; // 处理认证信息 let username = '', password = ''; // 专门处理 socks://base64auth@server:port 这样的格式 if (url.username) { // 首先对username进行URL解码 let decodedUsername = safeDecodeURIComponent(url.username); // 特殊处理 dXNlcm5hbWUxMjM6cGFzc3dvcmQxMjM= 这样的Base64编码认证信息 try { // 尝试Base64解码 const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { // 成功解码为 username:password 格式 const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } else { username = decodedUsername; } } else { // 不是预期的格式，使用原始值 username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } // 构建Surge格式 const configParts = [ nodeName + " = socks5", url.hostname, url.port ]; // 如果有用户名密码，添加到配置中 if (username) configParts.push(username); if (password) configParts.push(password); return configParts.join(', '); } catch (error) { return null; } } // 添加更新订阅信息的函数 async function handleUpdateSubscriptionInfo(env, path, data) { const name = data.name?.trim(); const newPath = data.path?.trim(); // 基本验证 if (!name) { return createErrorResponse('订阅名称不能为空', 400); } if (!validateSubscriptionPath(newPath)) { return createErrorResponse('无效的订阅路径格式', 400); } try { // 如果路径被修改，检查新路径是否已存在 if (newPath !== path) { const { results } = await env.DB.prepare(` SELECT 1 FROM subscriptions WHERE path = ? LIMIT 1 `).bind(newPath).all(); if (results.length > 0) { return createErrorResponse('该路径已被使用', 400); } } // 使用事务确保数据一致性 const statements = [ env.DB.prepare( "UPDATE subscriptions SET name = ?, path = ? WHERE path = ?" ).bind(name, newPath, path), env.DB.prepare( "SELECT id, name, path FROM subscriptions WHERE path = ?" ).bind(newPath) ]; const [updateResult, { results }] = await env.DB.batch(statements); if (!results?.[0]) { return createErrorResponse('更新失败：找不到订阅', 404); } return createSuccessResponse(results[0], '订阅信息已更新'); } catch (error) { return createErrorResponse('更新订阅信息失败: ' + error.message); } } // 添加删除订阅的处理函数 async function handleDeleteSubscription(env, path) { // 使用事务确保数据一致性 const statements = [ // 首先删除该订阅下的所有节点 env.DB.prepare( "DELETE FROM nodes WHERE subscription_id IN (SELECT id FROM subscriptions WHERE path = ?)" ).bind(path), // 然后删除订阅 env.DB.prepare( "DELETE FROM subscriptions WHERE path = ?" ).bind(path) ]; // 执行事务 await env.DB.batch(statements); return createSuccessResponse(null, '订阅已删除'); } // 添加更新节点的处理函数 async function handleUpdateNode(request, env, subscriptionPath, nodeId) { const nodeData = await request.json(); // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.replace(/[\r\n\s]+$/, ''); // 尝试base64解码 try { const decodedContent = safeBase64Decode(originalLink); if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.replace(/[\r\n\s]+$/, ''); } } catch (e) {} // 不是base64格式，继续使用原始内容 // 使用通用的节点名称提取函数 const nodeName = extractNodeName(originalLink); // 更新节点内容和名称 await env.DB.prepare(` UPDATE nodes SET original_link = ?, name = ? WHERE id = ? AND subscription_id = ? `).bind(originalLink, nodeName || '未命名节点', nodeId, subscriptionId).run(); return createSuccessResponse(null, '节点更新成功'); } // 将订阅内容转换为surge格式 function convertToSurge(content) { if (!content?.trim()) return ''; // 使用Map来映射节点类型和处理函数，提高性能 const nodeParserMap = new Map([ [NODE_TYPES.SS, parseSIP002Format], [NODE_TYPES.VMESS, parseVmessLink], [NODE_TYPES.TROJAN, parseTrojanLink], [NODE_TYPES.SOCKS, parseSocksLink], [NODE_TYPES.HYSTERIA2, parseHysteria2ToSurge], [NODE_TYPES.TUIC, parseTuicToSurge] ]); return content .split(/\r?\n/) .map(line => { const trimmedLine = line.trim(); if (!trimmedLine) return null; // 如果已经是snell格式,格式化并返回 if (trimmedLine.includes(NODE_TYPES.SNELL)) { return formatSnellConfig(trimmedLine); } // 跳过 VLESS 节点 if (trimmedLine.toLowerCase().startsWith(NODE_TYPES.VLESS)) { return null; } // 检查是否有匹配的解析器 for (const [prefix, parser] of nodeParserMap.entries()) { if (trimmedLine.startsWith(prefix)) { return parser(trimmedLine); } } return null; }) .filter(Boolean) .join('\n'); } // 格式化snell配置 function formatSnellConfig(snellConfig) { if (!snellConfig) return null; // 分割配置字符串，保持格式一致 const parts = snellConfig.split(',').map(part => part.trim()); return parts.join(', '); } // 安全的URL解码辅助函数 function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch { return str; } } // 安全的Base64编码辅助函数，支持Unicode字符 function safeBase64Encode(str) { try { return btoa(unescape(encodeURIComponent(str))); } catch (e) { return str; } } // 安全的Base64解码辅助函数 function safeBase64Decode(str) { try { // 方法1：使用atob解码，然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder，使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2：直接使用atob，可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败，返回原字符串 return str; } } } // 安全的UTF-8字符串解码函数 function safeUtf8Decode(str) { if (!str) return str; try { // 方法1：使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2：直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3：使用TextDecoder（如果支持） if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败，返回原始字符串 return str; } return str; } } } // 过滤掉snell节点的函数 function filterSnellNodes(content) { if (!content?.trim()) return ''; return content .split(/\r?\n/) .filter(line => { const trimmedLine = line.trim(); if (!trimmedLine) return false; // 过滤掉snell节点 return !trimmedLine.includes(NODE_TYPES.SNELL); }) .join('\n'); } // 将订阅内容转换为 Clash 格式 function convertToClash(content) { if (!content?.trim()) { return generateEmptyClashConfig(); } const nodes = content .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(parseNodeToClash) .filter(Boolean); return generateClashConfig(nodes); } // 解析单个节点为 Clash 格式 function parseNodeToClash(nodeLink) { if (!nodeLink) return null; const lowerLink = nodeLink.toLowerCase(); // 跳过 snell 节点，Clash 不支持 if (nodeLink.includes(NODE_TYPES.SNELL)) { return null; } // 解析 SS 节点 if (lowerLink.startsWith(NODE_TYPES.SS)) { return parseSSToClash(nodeLink); } // 解析 VMess 节点 if (lowerLink.startsWith(NODE_TYPES.VMESS)) { return parseVmessToClash(nodeLink); } // 解析 Trojan 节点 if (lowerLink.startsWith(NODE_TYPES.TROJAN)) { return parseTrojanToClash(nodeLink); } // 解析 VLESS 节点 if (lowerLink.startsWith(NODE_TYPES.VLESS)) { return parseVlessToClash(nodeLink); } // 解析 SOCKS 节点 if (lowerLink.startsWith(NODE_TYPES.SOCKS)) { return parseSocksToClash(nodeLink); } // 解析 Hysteria2 节点 if (lowerLink.startsWith(NODE_TYPES.HYSTERIA2)) { return parseHysteria2ToClash(nodeLink); } // 解析 TUIC 节点 if (lowerLink.startsWith(NODE_TYPES.TUIC)) { return parseTuicToClash(nodeLink); } return null; } // 解析 SS 节点为 Clash 格式 function parseSSToClash(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; return { name: name ? decodeURIComponent(name) : '未命名节点', type: 'ss', server: server, port: parseInt(port), cipher: method, password: password }; } catch { return null; } } // 解析 VMess 节点为 Clash 格式 function parseVmessToClash(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; const node = { name: config.ps ? safeUtf8Decode(config.ps) : '未命名节点', type: 'vmess', server: config.add, port: parseInt(config.port), uuid: config.id, alterId: parseInt(config.aid) || 0, cipher: 'auto', tls: config.tls === 'tls' }; // 添加网络类型配置 if (config.net === 'ws') { node.network = 'ws'; if (config.path) { node['ws-opts'] = { path: config.path, headers: { Host: config.host || config.add } }; } } else if (config.net === 'grpc') { node.network = 'grpc'; if (config.path) { node['grpc-opts'] = { 'grpc-service-name': config.path }; } } // TLS 配置 if (config.tls === 'tls') { node['skip-cert-verify'] = true; if (config.sni) { node.servername = config.sni; } } return node; } catch { return null; } } // 解析 Trojan 节点为 Clash 格式 function parseTrojanToClash(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'trojan', server: url.hostname, port: parseInt(url.port), password: url.username, 'skip-cert-verify': true }; // 添加网络类型配置 if (params.get('type') === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (params.get('type') === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 VLESS 节点为 Clash 格式 function parseVlessToClash(vlessLink) { if (!vlessLink.startsWith(NODE_TYPES.VLESS)) return null; try { const url = new URL(vlessLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'vless', server: url.hostname, port: parseInt(url.port), uuid: url.username, tls: params.get('security') === 'tls' || params.get('security') === 'reality', 'client-fingerprint': 'chrome', tfo: false, 'skip-cert-verify': false }; // 添加 flow 参数 const flow = params.get('flow'); if (flow) { node.flow = flow; } // Reality 配置 if (params.get('security') === 'reality') { const publicKey = params.get('pbk'); const shortId = params.get('sid'); if (publicKey || shortId) { node['reality-opts'] = {}; if (publicKey) node['reality-opts']['public-key'] = publicKey; if (shortId) node['reality-opts']['short-id'] = shortId; } } // 添加网络类型配置 const type = params.get('type'); if (type === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (type === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } else if (type === 'tcp') { node.network = 'tcp'; } else { // 默认设置为 tcp node.network = 'tcp'; } // SNI 配置 const sni = params.get('sni'); if (sni) { node.servername = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 SOCKS 节点为 Clash 格式 function parseSocksToClash(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { const url = new URL(socksLink); if (!url.hostname || !url.port) return null; const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'socks5', server: url.hostname, port: parseInt(url.port) }; // 处理认证信息 if (url.username) { let username = '', password = ''; let decodedUsername = safeDecodeURIComponent(url.username); try { const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } } else { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } if (username) node.username = username; if (password) node.password = password; } return node; } catch { return null; } } // 生成 Clash 配置文件 function generateClashConfig(proxies) { const proxyNames = proxies.map(proxy => proxy.name); const config = { // 用于下载订阅时指定UA 'global-ua': 'clash', // 全局配置 mode: 'rule', 'mixed-port': 7890, 'allow-lan': true, // 控制面板 'external-controller': '0.0.0.0:9090', // 如果有代理节点，则包含代理节点配置 proxies: proxies.length > 0 ? proxies : [], // 策略组 'proxy-groups': [ { name: '节点选择', type: 'select', proxies: ['DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Proxy.png' }, { name: '媒体服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Netflix.png' }, { name: '微软服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png' }, { name: '苹果服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png' }, { name: 'CDN服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/OneDrive.png' }, { name: 'AI服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png' }, { name: 'Telegram', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png' }, { name: 'Speedtest', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Speedtest.png' }, ], // 分流规则 rules: [ 'RULE-SET,reject_non_ip,REJECT', 'RULE-SET,reject_domainset,REJECT', 'RULE-SET,reject_extra_domainset,REJECT', 'RULE-SET,reject_non_ip_drop,REJECT-DROP', 'RULE-SET,reject_non_ip_no_drop,REJECT', // 域名类规则 'RULE-SET,speedtest,Speedtest', 'RULE-SET,telegram_non_ip,Telegram', 'RULE-SET,apple_cdn,DIRECT', 'RULE-SET,apple_cn_non_ip,DIRECT', 'RULE-SET,microsoft_cdn_non_ip,DIRECT', 'RULE-SET,apple_services,苹果服务', 'RULE-SET,microsoft_non_ip,微软服务', 'RULE-SET,download_domainset,CDN服务', 'RULE-SET,download_non_ip,CDN服务', 'RULE-SET,cdn_domainset,CDN服务', 'RULE-SET,cdn_non_ip,CDN服务', 'RULE-SET,stream_non_ip,媒体服务', 'RULE-SET,ai_non_ip,AI服务', 'RULE-SET,global_non_ip,节点选择', 'RULE-SET,domestic_non_ip,DIRECT', 'RULE-SET,direct_non_ip,DIRECT', 'RULE-SET,lan_non_ip,DIRECT', 'GEOSITE,CN,DIRECT', // IP 类规则 'RULE-SET,reject_ip,REJECT', 'RULE-SET,telegram_ip,Telegram', 'RULE-SET,stream_ip,媒体服务', 'RULE-SET,lan_ip,DIRECT', 'RULE-SET,domestic_ip,DIRECT', 'RULE-SET,china_ip,DIRECT', 'GEOIP,LAN,DIRECT', 'GEOIP,CN,DIRECT', // 兜底规则 'MATCH,节点选择' ], // 规则提供者 'rule-providers': { reject_non_ip_no_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-no-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_no_drop.txt' }, reject_non_ip_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_drop.txt' }, reject_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip.txt' }, reject_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject.txt', path: './rule_set/sukkaw_ruleset/reject_domainset.txt' }, reject_extra_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject_extra.txt', path: './sukkaw_ruleset/reject_domainset_extra.txt' }, reject_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_ip.txt' }, speedtest: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: 'Speedtest', url: 'https://ruleset.skk.moe/Clash/domainset/speedtest.txt', path: './rule_set/sukkaw_ruleset/speedtest.txt' }, cdn_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_domainset.txt' }, cdn_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_non_ip.txt' }, stream_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_non_ip.txt' }, stream_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_ip.txt' }, ai_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/ai.txt', path: './rule_set/sukkaw_ruleset/ai_non_ip.txt' }, telegram_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_non_ip.txt' }, telegram_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_ip.txt' }, apple_cdn: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/apple_cdn.txt', path: './rule_set/sukkaw_ruleset/apple_cdn.txt' }, apple_services: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_services.txt', path: './rule_set/sukkaw_ruleset/apple_services.txt' }, apple_cn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_cn.txt', path: './rule_set/sukkaw_ruleset/apple_cn_non_ip.txt' }, microsoft_cdn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft_cdn.txt', path: './rule_set/sukkaw_ruleset/microsoft_cdn_non_ip.txt' }, microsoft_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft.txt', path: './rule_set/sukkaw_ruleset/microsoft_non_ip.txt' }, download_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/download.txt', path: './rule_set/sukkaw_ruleset/download_domainset.txt' }, download_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/download.txt', path: './rule_set/sukkaw_ruleset/download_non_ip.txt' }, lan_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_non_ip.txt' }, lan_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_ip.txt' }, domestic_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_non_ip.txt' }, direct_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/direct.txt', path: './rule_set/sukkaw_ruleset/direct_non_ip.txt' }, global_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/global.txt', path: './rule_set/sukkaw_ruleset/global_non_ip.txt' }, domestic_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_ip.txt' }, china_ip: { type: 'http', behavior: 'ipcidr', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/china_ip.txt', path: './rule_set/sukkaw_ruleset/china_ip.txt' } } }; return `# Clash 配置文件 - Sub-Hub 自动生成 # 生成时间: ${new Date().toISOString()} ${convertToYaml(config)}`; } // 生成空的 Clash 配置 function generateEmptyClashConfig() { return generateClashConfig([]); } // 简化的对象转 YAML 函数，针对 Clash 配置优化 function convertToYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); let yaml = ''; for (const [key, value] of Object.entries(obj)) { // 处理键名，只对真正需要的情况加引号 let yamlKey = key; if (key.includes(' ') || key.includes('@') || key.includes('&') || key.includes('*') || key.includes('?') || key.includes('>') || key.includes('<') || key.includes('!') || key.includes('%') || key.includes('^') || key.includes('`') || /^\d/.test(key) || key === '' || /^(true|false|null|yes|no|on|off)$/i.test(key)) { yamlKey = `"${key.replace(/"/g, '\\"')}"`; } if (value === null || value === undefined) { yaml += `${spaces}${yamlKey}: null\n`; } else if (typeof value === 'boolean') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'number') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'string') { // 对字符串值更宽松的引号判断，主要针对真正会导致 YAML 解析问题的字符 const needsQuotes = value.includes(':') || value.includes('#') || value.includes('"') || value.includes('\n') || value.includes('&') || value.includes('*') || value.includes('[') || value.includes(']') || value.includes('{') || value.includes('}') || value.includes('@') || value.includes('`') || /^\s/.test(value) || /\s$/.test(value) || value === '' || /^(true|false|null|yes|no|on|off)$/i.test(value) || (/^\d+$/.test(value) && value.length > 1) || (/^\d+\.\d+$/.test(value) && value.length > 1); if (needsQuotes) { const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces}${yamlKey}: "${escapedValue}"\n`; } else { yaml += `${spaces}${yamlKey}: ${value}\n`; } } else if (Array.isArray(value)) { if (value.length === 0) { yaml += `${spaces}${yamlKey}: []\n`; } else { yaml += `${spaces}${yamlKey}:\n`; for (const item of value) { if (typeof item === 'object' && item !== null) { yaml += `${spaces} -\n`; const itemYaml = convertToYaml(item, 0); yaml += itemYaml.split('\n').map(line => line.trim() ? `${spaces} ${line}` : '' ).filter(line => line).join('\n') + '\n'; } else if (typeof item === 'string') { // 对数组中的字符串项（如节点名称）更宽松的引号判断 const needsQuotes = item.includes(':') || item.includes('#') || item.includes('"') || item.includes('\n') || item.includes('&') || item.includes('*') || item.includes('[') || item.includes(']') || item.includes('{') || item.includes('}') || item.includes('@') || item.includes('`') || /^\s/.test(item) || /\s$/.test(item) || item === '' || /^(true|false|null|yes|no|on|off)$/i.test(item) || (/^\d+$/.test(item) && item.length > 1) || (/^\d+\.\d+$/.test(item) && item.length > 1); if (needsQuotes) { const escapedItem = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces} - "${escapedItem}"\n`; } else { yaml += `${spaces} - ${item}\n`; } } else { yaml += `${spaces} - ${item}\n`; } } } } else if (typeof value === 'object' && value !== null) { yaml += `${spaces}${yamlKey}:\n`; yaml += convertToYaml(value, indent + 1); } } return yaml; } // 解析 Hysteria2 节点为 Clash 格式 function parseHysteria2ToClash(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'hysteria2', server: url.hostname, port: parseInt(url.port), password: url.username || params.get('password') || '', 'skip-cert-verify': true }; // 上传和下载速度配置 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) node.up = upMbps; if (downMbps) node.down = downMbps; // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // 混淆配置 const obfs = params.get('obfs'); if (obfs) { node.obfs = safeDecodeURIComponent(obfs); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { node['obfs-password'] = safeDecodeURIComponent(obfsPassword); } } // 拥塞控制算法 const cc = params.get('cc'); if (cc) { node.cc = cc; } return node; } catch { return null; } } // 解析 TUIC 节点为 Clash 格式 function parseTuicToClash(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'tuic', server: url.hostname, port: parseInt(url.port), uuid: url.username || params.get('uuid') || '', password: url.password || params.get('password') || '', 'skip-cert-verify': true }; // TUIC 版本 const version = params.get('version') || params.get('v'); if (version) { node.version = parseInt(version); } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // UDP Relay 模式 const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) { node['udp-relay-mode'] = udpRelayMode; } // 拥塞控制算法 const cc = params.get('congestion_control') || params.get('cc'); if (cc) { node['congestion-control'] = cc; } // 禁用 SNI const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { node['disable-sni'] = true; } // 减少 RTT const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { node['reduce-rtt'] = true; } return node; } catch { return null; } } // 解析 Hysteria2 链接为 Surge 格式 function parseHysteria2ToSurge(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const password = url.username || params.get('password') || ''; // 构建 Surge 格式的 Hysteria2 配置 const configParts = [ `${nodeName} = hysteria2`, url.hostname, url.port, `password=${password}` ]; // 添加可选参数 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) configParts.push(`up=${upMbps}`); if (downMbps) configParts.push(`down=${downMbps}`); const sni = params.get('sni'); if (sni) configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); const obfs = params.get('obfs'); if (obfs) { configParts.push(`obfs=${safeDecodeURIComponent(obfs)}`); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { configParts.push(`obfs-password=${safeDecodeURIComponent(obfsPassword)}`); } } const cc = params.get('cc'); if (cc) configParts.push(`cc=${cc}`); // 默认跳过证书验证 configParts.push('skip-cert-verify=true'); return configParts.join(', '); } catch (error) { return null; } } // 解析 TUIC 链接为 Surge 格式 function parseTuicToSurge(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const uuid = url.username || params.get('uuid') || ''; const password = url.password || params.get('password') || ''; // 构建 Surge 格式的 TUIC 配置 const configParts = [ `${nodeName} = tuic`, url.hostname, url.port, `uuid=${uuid}`, `password=${password}` ]; // TUIC 版本 - 如果没有指定版本，默认使用版本 5 const version = params.get('version') || params.get('v') || '5'; configParts.push(`version=${version}`); // SNI 配置 - 如果没有指定 SNI，使用服务器地址作为 SNI const sni = params.get('sni') || url.hostname; configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); // 处理 allow_insecure 参数 const allowInsecure = params.get('allow_insecure') || params.get('allowInsecure'); if (allowInsecure === 'true' || allowInsecure === '1') { configParts.push('skip-cert-verify=true'); } else { // 如果没有明确设置 allow_insecure，默认为 false configParts.push('skip-cert-verify=false'); } const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) configParts.push(`udp-relay-mode=${udpRelayMode}`); const cc = params.get('congestion_control') || params.get('congestion-control') || params.get('cc'); if (cc) configParts.push(`congestion-control=${cc}`); const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { configParts.push('disable-sni=true'); } const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { configParts.push('reduce-rtt=true'); } return configParts.join(', '); } catch (error) { return null; } }以下是原worker文件【20250529更新】// 通用的路径验证和节点名称提取函数 function validateSubscriptionPath(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 节点类型常量定义 const NODE_TYPES = { SS: 'ss://', VMESS: 'vmess://', TROJAN: 'trojan://', VLESS: 'vless://', SOCKS: 'socks://', HYSTERIA2: 'hysteria2://', TUIC: 'tuic://', SNELL: 'snell,' }; function extractNodeName(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes(NODE_TYPES.SNELL)) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith(NODE_TYPES.VMESS)) { try { const config = JSON.parse(safeBase64Decode(nodeLink.substring(8))); if (config.ps) { return safeUtf8Decode(config.ps); } } catch {} return '未命名节点'; } // 处理其他使用哈希标记名称的链接类型（SS、TROJAN、VLESS、SOCKS、Hysteria2、TUIC等） const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } export default { async fetch(request, env) { // 解析请求路径和参数 const url = new URL(request.url); const pathname = url.pathname; const method = request.method; // 检查是否有查询参数 if (url.search && !pathname.startsWith('/admin')) { return new Response('Not Found', { status: 404 }); } // 从环境变量获取配置路径，如果未设置则使用默认值 const adminPath = env.ADMIN_PATH || 'admin'; // 从环境变量获取登录凭据 const adminUsername = env.ADMIN_USERNAME || 'admin'; const adminPassword = env.ADMIN_PASSWORD || 'password'; // 处理登录页面请求 if (pathname === `/${adminPath}/login`) { if (method === "GET") { return serveLoginPage(adminPath); } else if (method === "POST") { return handleLogin(request, env, adminUsername, adminPassword, adminPath); } } // 处理登出请求 if (pathname === `/${adminPath}/logout`) { return handleLogout(request, env, adminPath); } // 处理管理面板请求 if (pathname === `/${adminPath}`) { const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return Response.redirect(`${url.origin}/${adminPath}/login`, 302); } return serveAdminPanel(env, adminPath); } // 处理API请求 if (pathname.startsWith(`/${adminPath}/api/`)) { // 验证会话 const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return new Response(JSON.stringify({ success: false, message: '未授权访问' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 处理节点管理API请求 const nodeApiMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)/nodes(?:/([^/]+|reorder))?$`)); if (nodeApiMatch) { const subscriptionPath = nodeApiMatch[1]; const nodeId = nodeApiMatch[2]; try { // 更新节点顺序 if (nodeId === 'reorder' && method === 'POST') { const { orders } = await request.json(); if (!Array.isArray(orders) || orders.length === 0) { return new Response(JSON.stringify({ success: false, message: '无效的排序数据' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } const subscriptionId = subResults[0].id; // 使用事务来确保数据一致性 const statements = []; // 准备更新语句 for (const { id, order } of orders) { statements.push(env.DB.prepare( "UPDATE nodes SET node_order = ? WHERE id = ? AND subscription_id = ?" ).bind(order, id, subscriptionId)); } // 执行批量更新 const result = await env.DB.batch(statements); return new Response(JSON.stringify({ success: true, message: '节点顺序已更新' }), { headers: { 'Content-Type': 'application/json' } }); } // 获取节点列表 if (!nodeId && method === 'GET') { return handleGetNodes(env, subscriptionPath); } // 创建新节点 if (!nodeId && method === 'POST') { return handleCreateNode(request, env, subscriptionPath); } // 更新节点 if (nodeId && nodeId !== 'reorder' && method === 'PUT') { return handleUpdateNode(request, env, subscriptionPath, nodeId); } // 删除节点 if (nodeId && nodeId !== 'reorder' && method === 'DELETE') { return handleDeleteNode(env, subscriptionPath, nodeId); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('API请求处理失败:', error); return new Response(JSON.stringify({ success: false, message: error.message || '服务器内部错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 处理订阅管理API请求 if (pathname.startsWith(`/${adminPath}/api/subscriptions`)) { // 获取单个订阅内容 const getOneMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (getOneMatch && method === 'GET') { return handleGetSubscription(env, getOneMatch[1]); } // 获取订阅列表 if (pathname === `/${adminPath}/api/subscriptions` && method === 'GET') { return handleGetSubscriptions(env); } // 创建新订阅 if (pathname === `/${adminPath}/api/subscriptions` && method === 'POST') { try { const { name, path } = await request.json(); if (!name || !validateSubscriptionPath(path)) { return createErrorResponse('无效的参数', 400); } // 检查路径是否已存在 const { results } = await env.DB.prepare( "SELECT COUNT(*) as count FROM subscriptions WHERE path = ?" ).bind(path).all(); if (results[0].count > 0) { return createErrorResponse('该路径已被使用', 400); } // 创建订阅 const result = await env.DB.prepare( "INSERT INTO subscriptions (name, path) VALUES (?, ?)" ).bind(name, path).run(); if (!result.success) { throw new Error('创建订阅失败'); } return createSuccessResponse(null, '订阅创建成功'); } catch (error) { console.error('创建订阅失败:', error); return createErrorResponse('创建订阅失败: ' + error.message); } } // 更新订阅信息 const updateMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (updateMatch && method === 'PUT') { const data = await request.json(); return handleUpdateSubscriptionInfo(env, updateMatch[1], data); } // 删除订阅 const deleteMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (deleteMatch && method === 'DELETE') { return handleDeleteSubscription(env, deleteMatch[1]); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ success: false, message: 'Not Found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 处理订阅请求 if (pathname.startsWith('/')) { // 检查路径格式是否合法（只允许一级或两级路径，如 /path 或 /path/surge 或 /path/v2ray 或 /path/clash） const pathParts = pathname.split('/').filter(Boolean); if (pathParts.length > 2) { return new Response('Not Found', { status: 404 }); } if (pathParts.length === 2 && !['surge', 'v2ray', 'clash'].includes(pathParts[1])) { return new Response('Not Found', { status: 404 }); } try { // 获取基本路径 let basePath = pathname; if (pathname.endsWith('/surge')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/surge } else if (pathname.endsWith('/v2ray')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/v2ray } else if (pathname.endsWith('/clash')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/clash } else { basePath = pathname.slice(1); // 只移除开头的/ } // 获取订阅信息 const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(basePath).all(); const subscription = results[0]; if (subscription) { // 生成订阅内容 const content = await generateSubscriptionContent(env, basePath); // 根据请求路径返回不同格式的内容 if (pathname.endsWith('/surge')) { // 返回 Surge 格式 const surgeContent = convertToSurge(content); return new Response(surgeContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/v2ray')) { // 返回 Base64 编码格式，排除 snell 节点，包括 VLESS 节点 const filteredContent = filterSnellNodes(content); const base64Content = safeBase64Encode(filteredContent); return new Response(base64Content, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/clash')) { // 返回 Clash 格式 const clashContent = convertToClash(content); return new Response(clashContent, { headers: { 'Content-Type': 'text/yaml; charset=utf-8' }, }); } // 返回普通订阅内容，排除 snell 节点 const filteredContent = filterSnellNodes(content); return new Response(filteredContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } } catch (error) { console.error('处理订阅请求失败:', error); return new Response('Internal Server Error', { status: 500 }); } // 如果没有找到匹配的订阅，返回404 return new Response('Not Found', { status: 404 }); } // 其他所有路径返回 404 return new Response('Not Found', { status: 404 }); }, }; // 添加获取单个订阅的处理函数 async function handleGetSubscription(env, path) { try { const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(path).all(); if (!results || results.length === 0) { return createErrorResponse('订阅不存在', 404); } return createSuccessResponse(results[0]); } catch (error) { console.error('获取订阅内容失败:', error); return createErrorResponse('获取订阅内容失败: ' + error.message); } } // 提供登录页面HTML function serveLoginPage(adminPath) { const html = ` Sub-Hub - 登录 Sub-Hub 请登录以继续使用 用户名或密码错误 用户名 密码 登录 `; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 验证会话 async function verifySession(request, env) { const sessionId = getSessionFromCookie(request); if (!sessionId) return false; const now = Date.now(); const { results } = await env.DB.prepare(` UPDATE sessions SET expires_at = ? WHERE session_id = ? AND expires_at > ? RETURNING * `).bind(now + 24 * 60 * 60 * 1000, sessionId, now).all(); return results.length > 0; } // 从Cookie中获取会话ID function getSessionFromCookie(request) { const cookieHeader = request.headers.get('Cookie') || ''; const sessionCookie = cookieHeader.split(';') .find(cookie => cookie.trim().startsWith('session=')); return sessionCookie ? sessionCookie.trim().substring(8) : null; } // 生成安全的会话令牌 async function generateSecureSessionToken(username, env) { // 清理过期会话和用户旧会话 const now = Date.now(); await env.DB.batch([ env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(now), env.DB.prepare("DELETE FROM sessions WHERE username = ?").bind(username) ]); const sessionId = crypto.randomUUID(); const expiresAt = now + 24 * 60 * 60 * 1000; // 24小时后过期 await env.DB.prepare(` INSERT INTO sessions (session_id, username, expires_at) VALUES (?, ?, ?) `).bind(sessionId, username, expiresAt).run(); return sessionId; } // 处理登录请求 async function handleLogin(request, env, adminUsername, adminPassword, adminPath) { const { username, password } = await request.json(); if (!username || !password || username !== adminUsername || password !== adminPassword) { return new Response(JSON.stringify({ success: false, message: '用户名或密码错误' }), { headers: { 'Content-Type': 'application/json' }, status: 401 }); } const sessionId = await generateSecureSessionToken(username, env); const headers = new Headers({ 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400; Secure` }); return new Response(JSON.stringify({ success: true, message: '登录成功', redirect: `/${adminPath}` }), { headers }); } // 处理登出请求 async function handleLogout(request, env, adminPath) { const sessionId = getSessionFromCookie(request); if (sessionId) { await env.DB.prepare("DELETE FROM sessions WHERE session_id = ?").bind(sessionId).run(); } const headers = new Headers({ 'Set-Cookie': `session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Secure` }); return Response.redirect(`${new URL(request.url).origin}/${adminPath}/login`, 302); } // 修改管理面板HTML生成函数 function serveAdminPanel(env, adminPath) { const html = ` Sub-Hub Sub-Hub 退出 添加订阅 添加新订阅 订阅名称 * 请输入订阅名称 订阅路径 * 路径只能包含小写字母、数字和连字符，长度在5-50个字符之间 取消 创建 添加节点 节点内容 * 取消 添加 编辑订阅信息 订阅名称 * 请输入订阅名称 订阅路径 * 路径只能包含小写字母、数字和连字符，长度在5-50个字符之间 删除订阅 取消 保存 编辑节点 节点内容 * 取消 保存 操作成功! `; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 通用响应头 const JSON_HEADERS = { 'Content-Type': 'application/json' }; // 通用响应创建函数 function createResponse(success, message, data = null, status = success ? 200 : 500) { return new Response( JSON.stringify({ success, message, ...(data && { data }) }), { headers: JSON_HEADERS, status } ); } // 错误响应函数 const createErrorResponse = (message, status = 500) => createResponse(false, message, null, status); // 成功响应函数 const createSuccessResponse = (data = null, message = '操作成功') => createResponse(true, message, data); // 修改获取订阅列表函数 async function handleGetSubscriptions(env) { const { results } = await env.DB.prepare(` SELECT s.path, s.name, COUNT(n.id) as nodeCount FROM subscriptions s LEFT JOIN nodes n ON s.id = n.subscription_id GROUP BY s.id ORDER BY s.id ASC `).all(); const subscriptions = results.map(item => ({ name: item.name, path: item.path, nodeCount: item.nodeCount || 0 })); return createSuccessResponse(subscriptions); } // 添加获取节点列表的函数 async function handleGetNodes(env, subscriptionPath) { const { results } = await env.DB.prepare(` SELECT n.id, n.name, n.original_link, n.node_order FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? ORDER BY n.node_order ASC `).bind(subscriptionPath).all(); return createSuccessResponse(results || []); } // 添加创建节点的函数 async function handleCreateNode(request, env, subscriptionPath) { const nodeData = await request.json(); if (!nodeData.content) { return createErrorResponse('缺少节点内容', 400); } const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.trim(); // 尝试Base64解码 try { const decodedContent = safeBase64Decode(originalLink); // 检查解码后的内容是否是有效的节点链接 if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.trim(); } } catch (e) { // 不是Base64格式，继续使用原始内容 } // 验证节点类型 const lowerContent = originalLink.toLowerCase(); const isSnell = lowerContent.includes('=') && lowerContent.includes('snell,'); if (!['ss://', 'vmess://', 'trojan://', 'vless://', 'socks://', 'hysteria2://', 'tuic://'].some(prefix => lowerContent.startsWith(prefix)) && !isSnell) { return createErrorResponse('不支持的节点格式', 400); } // 从节点链接中提取名称 let nodeName = extractNodeName(originalLink); // 直接使用提供的order值 const nodeOrder = nodeData.order; // 直接插入新节点，不更新其他节点的顺序 await env.DB.prepare(` INSERT INTO nodes (subscription_id, name, original_link, node_order) VALUES (?, ?, ?, ?) `).bind(subscriptionId, nodeName, originalLink, nodeOrder).run(); return createSuccessResponse(null, '节点创建成功'); } // 添加删除节点的函数 async function handleDeleteNode(env, subscriptionPath, nodeId) { try { const result = await env.DB.prepare(` DELETE FROM nodes WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(nodeId, subscriptionPath).run(); return createSuccessResponse(null, '节点已删除'); } catch (error) { return createErrorResponse('删除节点失败: ' + error.message); } } // 生成订阅内容 async function generateSubscriptionContent(env, path) { if (!path?.trim()) return ''; const { results } = await env.DB.prepare(` SELECT GROUP_CONCAT(n.original_link, CHAR(10)) as content FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? AND n.original_link IS NOT NULL GROUP BY s.id ORDER BY n.node_order ASC `).bind(path).all(); return results?.[0]?.content || ''; } // 解析SIP002格式 function parseSIP002Format(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; const nodeName = name ? decodeURIComponent(name) : '未命名节点'; return `${nodeName} = ss, ${server}, ${port}, encrypt-method=${method}, password=${password}`; } catch { return null; } } // 解析Vmess链接为Surge格式 function parseVmessLink(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; // 正确处理UTF-8编码的中文字符 const name = config.ps ? safeUtf8Decode(config.ps) : '未命名节点'; const configParts = [ `${name} = vmess`, config.add, config.port, `username=${config.id}`, 'vmess-aead=true', `tls=${config.tls === 'tls'}`, `sni=${config.add}`, 'skip-cert-verify=true', 'tfo=false' ]; if (config.tls === 'tls' && config.alpn) { configParts.push(`alpn=${config.alpn.replace(/,/g, ':')}`); } if (config.net === 'ws') { configParts.push('ws=true'); if (config.path) configParts.push(`ws-path=${config.path}`); configParts.push(`ws-headers=Host:${config.host || config.add}`); } return configParts.join(', '); } catch { return null; } } // 解析Trojan链接为Surge格式 function parseTrojanLink(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const configParts = [ `${nodeName} = trojan`, url.hostname, url.port, `password=${url.username}`, 'tls=true', `sni=${url.hostname}`, 'skip-cert-verify=true', 'tfo=false' ]; const alpn = params.get('alpn'); if (alpn) { configParts.push(`alpn=${safeDecodeURIComponent(alpn).replace(/,/g, ':')}`); } if (params.get('type') === 'ws') { configParts.push('ws=true'); const path = params.get('path'); if (path) { configParts.push(`ws-path=${safeDecodeURIComponent(path)}`); } const host = params.get('host'); configParts.push(`ws-headers=Host:${host ? safeDecodeURIComponent(host) : url.hostname}`); } return configParts.join(', '); } catch { return null; } } // 解析SOCKS链接为Surge格式 function parseSocksLink(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { // 处理标准格式：socks://username:password@server:port#name // 或者 socks://base64encoded@server:port#name const url = new URL(socksLink); if (!url.hostname || !url.port) return null; // 提取节点名称 const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; // 处理认证信息 let username = '', password = ''; // 专门处理 socks://base64auth@server:port 这样的格式 if (url.username) { // 首先对username进行URL解码 let decodedUsername = safeDecodeURIComponent(url.username); // 特殊处理 dXNlcm5hbWUxMjM6cGFzc3dvcmQxMjM= 这样的Base64编码认证信息 try { // 尝试Base64解码 const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { // 成功解码为 username:password 格式 const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } else { username = decodedUsername; } } else { // 不是预期的格式，使用原始值 username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } // 构建Surge格式 const configParts = [ nodeName + " = socks5", url.hostname, url.port ]; // 如果有用户名密码，添加到配置中 if (username) configParts.push(username); if (password) configParts.push(password); return configParts.join(', '); } catch (error) { return null; } } // 添加更新订阅信息的函数 async function handleUpdateSubscriptionInfo(env, path, data) { const name = data.name?.trim(); const newPath = data.path?.trim(); // 基本验证 if (!name) { return createErrorResponse('订阅名称不能为空', 400); } if (!validateSubscriptionPath(newPath)) { return createErrorResponse('无效的订阅路径格式', 400); } try { // 如果路径被修改，检查新路径是否已存在 if (newPath !== path) { const { results } = await env.DB.prepare(` SELECT 1 FROM subscriptions WHERE path = ? LIMIT 1 `).bind(newPath).all(); if (results.length > 0) { return createErrorResponse('该路径已被使用', 400); } } // 使用事务确保数据一致性 const statements = [ env.DB.prepare( "UPDATE subscriptions SET name = ?, path = ? WHERE path = ?" ).bind(name, newPath, path), env.DB.prepare( "SELECT id, name, path FROM subscriptions WHERE path = ?" ).bind(newPath) ]; const [updateResult, { results }] = await env.DB.batch(statements); if (!results?.[0]) { return createErrorResponse('更新失败：找不到订阅', 404); } return createSuccessResponse(results[0], '订阅信息已更新'); } catch (error) { return createErrorResponse('更新订阅信息失败: ' + error.message); } } // 添加删除订阅的处理函数 async function handleDeleteSubscription(env, path) { // 使用事务确保数据一致性 const statements = [ // 首先删除该订阅下的所有节点 env.DB.prepare( "DELETE FROM nodes WHERE subscription_id IN (SELECT id FROM subscriptions WHERE path = ?)" ).bind(path), // 然后删除订阅 env.DB.prepare( "DELETE FROM subscriptions WHERE path = ?" ).bind(path) ]; // 执行事务 await env.DB.batch(statements); return createSuccessResponse(null, '订阅已删除'); } // 添加更新节点的处理函数 async function handleUpdateNode(request, env, subscriptionPath, nodeId) { const nodeData = await request.json(); // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.replace(/[\r\n\s]+$/, ''); // 尝试base64解码 try { const decodedContent = safeBase64Decode(originalLink); if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.replace(/[\r\n\s]+$/, ''); } } catch (e) {} // 不是base64格式，继续使用原始内容 // 使用通用的节点名称提取函数 const nodeName = extractNodeName(originalLink); // 更新节点内容和名称 await env.DB.prepare(` UPDATE nodes SET original_link = ?, name = ? WHERE id = ? AND subscription_id = ? `).bind(originalLink, nodeName || '未命名节点', nodeId, subscriptionId).run(); return createSuccessResponse(null, '节点更新成功'); } // 将订阅内容转换为surge格式 function convertToSurge(content) { if (!content?.trim()) return ''; // 使用Map来映射节点类型和处理函数，提高性能 const nodeParserMap = new Map([ [NODE_TYPES.SS, parseSIP002Format], [NODE_TYPES.VMESS, parseVmessLink], [NODE_TYPES.TROJAN, parseTrojanLink], [NODE_TYPES.SOCKS, parseSocksLink], [NODE_TYPES.HYSTERIA2, parseHysteria2ToSurge], [NODE_TYPES.TUIC, parseTuicToSurge] ]); return content .split(/\r?\n/) .map(line => { const trimmedLine = line.trim(); if (!trimmedLine) return null; // 如果已经是snell格式,格式化并返回 if (trimmedLine.includes(NODE_TYPES.SNELL)) { return formatSnellConfig(trimmedLine); } // 跳过 VLESS 节点 if (trimmedLine.toLowerCase().startsWith(NODE_TYPES.VLESS)) { return null; } // 检查是否有匹配的解析器 for (const [prefix, parser] of nodeParserMap.entries()) { if (trimmedLine.startsWith(prefix)) { return parser(trimmedLine); } } return null; }) .filter(Boolean) .join('\n'); } // 格式化snell配置 function formatSnellConfig(snellConfig) { if (!snellConfig) return null; // 分割配置字符串，保持格式一致 const parts = snellConfig.split(',').map(part => part.trim()); return parts.join(', '); } // 安全的URL解码辅助函数 function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch { return str; } } // 安全的Base64编码辅助函数，支持Unicode字符 function safeBase64Encode(str) { try { return btoa(unescape(encodeURIComponent(str))); } catch (e) { return str; } } // 安全的Base64解码辅助函数 function safeBase64Decode(str) { try { // 方法1：使用atob解码，然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder，使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2：直接使用atob，可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败，返回原字符串 return str; } } } // 安全的UTF-8字符串解码函数 function safeUtf8Decode(str) { if (!str) return str; try { // 方法1：使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2：直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3：使用TextDecoder（如果支持） if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败，返回原始字符串 return str; } return str; } } } // 过滤掉snell节点的函数 function filterSnellNodes(content) { if (!content?.trim()) return ''; return content .split(/\r?\n/) .filter(line => { const trimmedLine = line.trim(); if (!trimmedLine) return false; // 过滤掉snell节点 return !trimmedLine.includes(NODE_TYPES.SNELL); }) .join('\n'); } // 将订阅内容转换为 Clash 格式 function convertToClash(content) { if (!content?.trim()) { return generateEmptyClashConfig(); } const nodes = content .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(parseNodeToClash) .filter(Boolean); return generateClashConfig(nodes); } // 解析单个节点为 Clash 格式 function parseNodeToClash(nodeLink) { if (!nodeLink) return null; const lowerLink = nodeLink.toLowerCase(); // 跳过 snell 节点，Clash 不支持 if (nodeLink.includes(NODE_TYPES.SNELL)) { return null; } // 解析 SS 节点 if (lowerLink.startsWith(NODE_TYPES.SS)) { return parseSSToClash(nodeLink); } // 解析 VMess 节点 if (lowerLink.startsWith(NODE_TYPES.VMESS)) { return parseVmessToClash(nodeLink); } // 解析 Trojan 节点 if (lowerLink.startsWith(NODE_TYPES.TROJAN)) { return parseTrojanToClash(nodeLink); } // 解析 VLESS 节点 if (lowerLink.startsWith(NODE_TYPES.VLESS)) { return parseVlessToClash(nodeLink); } // 解析 SOCKS 节点 if (lowerLink.startsWith(NODE_TYPES.SOCKS)) { return parseSocksToClash(nodeLink); } // 解析 Hysteria2 节点 if (lowerLink.startsWith(NODE_TYPES.HYSTERIA2)) { return parseHysteria2ToClash(nodeLink); } // 解析 TUIC 节点 if (lowerLink.startsWith(NODE_TYPES.TUIC)) { return parseTuicToClash(nodeLink); } return null; } // 解析 SS 节点为 Clash 格式 function parseSSToClash(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; return { name: name ? decodeURIComponent(name) : '未命名节点', type: 'ss', server: server, port: parseInt(port), cipher: method, password: password }; } catch { return null; } } // 解析 VMess 节点为 Clash 格式 function parseVmessToClash(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; const node = { name: config.ps ? safeUtf8Decode(config.ps) : '未命名节点', type: 'vmess', server: config.add, port: parseInt(config.port), uuid: config.id, alterId: parseInt(config.aid) || 0, cipher: 'auto', tls: config.tls === 'tls' }; // 添加网络类型配置 if (config.net === 'ws') { node.network = 'ws'; if (config.path) { node['ws-opts'] = { path: config.path, headers: { Host: config.host || config.add } }; } } else if (config.net === 'grpc') { node.network = 'grpc'; if (config.path) { node['grpc-opts'] = { 'grpc-service-name': config.path }; } } // TLS 配置 if (config.tls === 'tls') { node['skip-cert-verify'] = true; if (config.sni) { node.servername = config.sni; } } return node; } catch { return null; } } // 解析 Trojan 节点为 Clash 格式 function parseTrojanToClash(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'trojan', server: url.hostname, port: parseInt(url.port), password: url.username, 'skip-cert-verify': true }; // 添加网络类型配置 if (params.get('type') === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (params.get('type') === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 VLESS 节点为 Clash 格式 function parseVlessToClash(vlessLink) { if (!vlessLink.startsWith(NODE_TYPES.VLESS)) return null; try { const url = new URL(vlessLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'vless', server: url.hostname, port: parseInt(url.port), uuid: url.username, tls: params.get('security') === 'tls' || params.get('security') === 'reality', 'skip-cert-verify': true, tfo: false, udp: true, 'ip-version': 'dual' }; // 添加网络类型配置 const type = params.get('type'); if (type === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (type === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } else { node.network = 'tcp'; } // Reality 配置 if (params.get('security') === 'reality') { node.reality = true; const publicKey = params.get('pbk'); if (publicKey) { node['reality-opts'] = { 'public-key': publicKey }; } } // 添加 flow 参数 const flow = params.get('flow'); if (flow) { node.flow = flow; } // 添加 client-fingerprint const fp = params.get('fp'); if (fp) { node['client-fingerprint'] = fp; } // SNI 配置 const sni = params.get('sni'); if (sni) { node.servername = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 SOCKS 节点为 Clash 格式 function parseSocksToClash(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { const url = new URL(socksLink); if (!url.hostname || !url.port) return null; const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'socks5', server: url.hostname, port: parseInt(url.port) }; // 处理认证信息 if (url.username) { let username = '', password = ''; let decodedUsername = safeDecodeURIComponent(url.username); try { const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } } else { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } if (username) node.username = username; if (password) node.password = password; } return node; } catch { return null; } } // 生成 Clash 配置文件 function generateClashConfig(proxies) { const proxyNames = proxies.map(proxy => proxy.name); const config = { // 用于下载订阅时指定UA 'global-ua': 'clash', // 全局配置 mode: 'rule', 'mixed-port': 7890, 'allow-lan': true, // 控制面板 'external-controller': '0.0.0.0:9090', // 如果有代理节点，则包含代理节点配置 proxies: proxies.length > 0 ? proxies : [], // 策略组 'proxy-groups': [ { name: '节点选择', type: 'select', proxies: ['DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Proxy.png' }, { name: '媒体服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Netflix.png' }, { name: '微软服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png' }, { name: '苹果服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png' }, { name: 'CDN服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/OneDrive.png' }, { name: 'AI服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png' }, { name: 'Telegram', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png' }, ], // 分流规则 rules: [ 'RULE-SET,reject_non_ip,REJECT', 'RULE-SET,reject_domainset,REJECT', 'RULE-SET,reject_extra_domainset,REJECT', 'RULE-SET,reject_non_ip_drop,REJECT-DROP', 'RULE-SET,reject_non_ip_no_drop,REJECT', // 域名类规则 'RULE-SET,telegram_non_ip,Telegram', 'RULE-SET,apple_cdn,苹果服务', 'RULE-SET,apple_cn_non_ip,苹果服务', 'RULE-SET,microsoft_cdn_non_ip,微软服务', 'RULE-SET,apple_services,苹果服务', 'RULE-SET,microsoft_non_ip,微软服务', 'RULE-SET,download_domainset,CDN服务', 'RULE-SET,download_non_ip,CDN服务', 'RULE-SET,cdn_domainset,CDN服务', 'RULE-SET,cdn_non_ip,CDN服务', 'RULE-SET,stream_non_ip,媒体服务', 'RULE-SET,ai_non_ip,AI服务', 'RULE-SET,global_non_ip,节点选择', 'RULE-SET,domestic_non_ip,DIRECT', 'RULE-SET,direct_non_ip,DIRECT', 'RULE-SET,lan_non_ip,DIRECT', 'GEOSITE,CN,DIRECT', // IP 类规则 'RULE-SET,reject_ip,REJECT', 'RULE-SET,telegram_ip,Telegram', 'RULE-SET,stream_ip,媒体服务', 'RULE-SET,lan_ip,DIRECT', 'RULE-SET,domestic_ip,DIRECT', 'RULE-SET,china_ip,DIRECT', 'GEOIP,LAN,DIRECT', 'GEOIP,CN,DIRECT', // 兜底规则 'MATCH,节点选择' ], // 规则提供者 'rule-providers': { reject_non_ip_no_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-no-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_no_drop.txt' }, reject_non_ip_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_drop.txt' }, reject_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip.txt' }, reject_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject.txt', path: './rule_set/sukkaw_ruleset/reject_domainset.txt' }, reject_extra_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject_extra.txt', path: './sukkaw_ruleset/reject_domainset_extra.txt' }, reject_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_ip.txt' }, cdn_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_domainset.txt' }, cdn_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_non_ip.txt' }, stream_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_non_ip.txt' }, stream_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_ip.txt' }, ai_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/ai.txt', path: './rule_set/sukkaw_ruleset/ai_non_ip.txt' }, telegram_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_non_ip.txt' }, telegram_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_ip.txt' }, apple_cdn: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/apple_cdn.txt', path: './rule_set/sukkaw_ruleset/apple_cdn.txt' }, apple_services: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_services.txt', path: './rule_set/sukkaw_ruleset/apple_services.txt' }, apple_cn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_cn.txt', path: './rule_set/sukkaw_ruleset/apple_cn_non_ip.txt' }, microsoft_cdn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft_cdn.txt', path: './rule_set/sukkaw_ruleset/microsoft_cdn_non_ip.txt' }, microsoft_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft.txt', path: './rule_set/sukkaw_ruleset/microsoft_non_ip.txt' }, download_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/download.txt', path: './rule_set/sukkaw_ruleset/download_domainset.txt' }, download_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/download.txt', path: './rule_set/sukkaw_ruleset/download_non_ip.txt' }, lan_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_non_ip.txt' }, lan_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_ip.txt' }, domestic_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_non_ip.txt' }, direct_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/direct.txt', path: './rule_set/sukkaw_ruleset/direct_non_ip.txt' }, global_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/global.txt', path: './rule_set/sukkaw_ruleset/global_non_ip.txt' }, domestic_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_ip.txt' }, china_ip: { type: 'http', behavior: 'ipcidr', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/china_ip.txt', path: './rule_set/sukkaw_ruleset/china_ip.txt' } } }; return `# Clash 配置文件 - Sub-Hub 自动生成 # 生成时间: ${new Date().toISOString()} ${convertToYaml(config)}`; } // 生成空的 Clash 配置 function generateEmptyClashConfig() { return generateClashConfig([]); } // 简化的对象转 YAML 函数，针对 Clash 配置优化 function convertToYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); let yaml = ''; for (const [key, value] of Object.entries(obj)) { // 处理键名，只对真正需要的情况加引号 let yamlKey = key; if (key.includes(' ') || key.includes('@') || key.includes('&') || key.includes('*') || key.includes('?') || key.includes('>') || key.includes('<') || key.includes('!') || key.includes('%') || key.includes('^') || key.includes('`') || /^\d/.test(key) || key === '' || /^(true|false|null|yes|no|on|off)$/i.test(key)) { yamlKey = `"${key.replace(/"/g, '\\"')}"`; } if (value === null || value === undefined) { yaml += `${spaces}${yamlKey}: null\n`; } else if (typeof value === 'boolean') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'number') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'string') { // 对字符串值更宽松的引号判断，主要针对真正会导致 YAML 解析问题的字符 const needsQuotes = value.includes(':') || value.includes('#') || value.includes('"') || value.includes('\n') || value.includes('&') || value.includes('*') || value.includes('[') || value.includes(']') || value.includes('{') || value.includes('}') || value.includes('@') || value.includes('`') || /^\s/.test(value) || /\s$/.test(value) || value === '' || /^(true|false|null|yes|no|on|off)$/i.test(value) || (/^\d+$/.test(value) && value.length > 1) || (/^\d+\.\d+$/.test(value) && value.length > 1); if (needsQuotes) { const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces}${yamlKey}: "${escapedValue}"\n`; } else { yaml += `${spaces}${yamlKey}: ${value}\n`; } } else if (Array.isArray(value)) { if (value.length === 0) { yaml += `${spaces}${yamlKey}: []\n`; } else { yaml += `${spaces}${yamlKey}:\n`; for (const item of value) { if (typeof item === 'object' && item !== null) { yaml += `${spaces} -\n`; const itemYaml = convertToYaml(item, 0); yaml += itemYaml.split('\n').map(line => line.trim() ? `${spaces} ${line}` : '' ).filter(line => line).join('\n') + '\n'; } else if (typeof item === 'string') { // 对数组中的字符串项（如节点名称）更宽松的引号判断 const needsQuotes = item.includes(':') || item.includes('#') || item.includes('"') || item.includes('\n') || item.includes('&') || item.includes('*') || item.includes('[') || item.includes(']') || item.includes('{') || item.includes('}') || item.includes('@') || item.includes('`') || /^\s/.test(item) || /\s$/.test(item) || item === '' || /^(true|false|null|yes|no|on|off)$/i.test(item) || (/^\d+$/.test(item) && item.length > 1) || (/^\d+\.\d+$/.test(item) && item.length > 1); if (needsQuotes) { const escapedItem = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces} - "${escapedItem}"\n`; } else { yaml += `${spaces} - ${item}\n`; } } else { yaml += `${spaces} - ${item}\n`; } } } } else if (typeof value === 'object' && value !== null) { yaml += `${spaces}${yamlKey}:\n`; yaml += convertToYaml(value, indent + 1); } } return yaml; } // 解析 Hysteria2 节点为 Clash 格式 function parseHysteria2ToClash(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'hysteria2', server: url.hostname, port: parseInt(url.port), password: url.username || params.get('password') || '', 'skip-cert-verify': true }; // 上传和下载速度配置 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) node.up = upMbps; if (downMbps) node.down = downMbps; // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // 混淆配置 const obfs = params.get('obfs'); if (obfs) { node.obfs = safeDecodeURIComponent(obfs); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { node['obfs-password'] = safeDecodeURIComponent(obfsPassword); } } // 拥塞控制算法 const cc = params.get('cc'); if (cc) { node.cc = cc; } return node; } catch { return null; } } // 解析 TUIC 节点为 Clash 格式 function parseTuicToClash(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'tuic', server: url.hostname, port: parseInt(url.port), uuid: url.username || params.get('uuid') || '', password: url.password || params.get('password') || '', 'skip-cert-verify': true }; // TUIC 版本 const version = params.get('version') || params.get('v'); if (version) { node.version = parseInt(version); } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // UDP Relay 模式 const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) { node['udp-relay-mode'] = udpRelayMode; } // 拥塞控制算法 const cc = params.get('congestion_control') || params.get('cc'); if (cc) { node['congestion-control'] = cc; } // 禁用 SNI const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { node['disable-sni'] = true; } // 减少 RTT const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { node['reduce-rtt'] = true; } return node; } catch { return null; } } // 解析 Hysteria2 链接为 Surge 格式 function parseHysteria2ToSurge(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const password = url.username || params.get('password') || ''; // 构建 Surge 格式的 Hysteria2 配置 const configParts = [ `${nodeName} = hysteria2`, url.hostname, url.port, `password=${password}` ]; // 添加可选参数 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) configParts.push(`up=${upMbps}`); if (downMbps) configParts.push(`down=${downMbps}`); const sni = params.get('sni'); if (sni) configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); const obfs = params.get('obfs'); if (obfs) { configParts.push(`obfs=${safeDecodeURIComponent(obfs)}`); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { configParts.push(`obfs-password=${safeDecodeURIComponent(obfsPassword)}`); } } const cc = params.get('cc'); if (cc) configParts.push(`cc=${cc}`); // 默认跳过证书验证 configParts.push('skip-cert-verify=true'); return configParts.join(', '); } catch (error) { return null; } } // 解析 TUIC 链接为 Surge 格式 function parseTuicToSurge(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const uuid = url.username || params.get('uuid') || ''; const password = url.password || params.get('password') || ''; // 构建 Surge 格式的 TUIC 配置 const configParts = [ `${nodeName} = tuic`, url.hostname, url.port, `uuid=${uuid}`, `password=${password}` ]; // TUIC 版本 - 如果没有指定版本，默认使用版本 5 const version = params.get('version') || params.get('v') || '5'; configParts.push(`version=${version}`); // SNI 配置 - 如果没有指定 SNI，使用服务器地址作为 SNI const sni = params.get('sni') || url.hostname; configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); // 处理 allow_insecure 参数 const allowInsecure = params.get('allow_insecure') || params.get('allowInsecure'); if (allowInsecure === 'true' || allowInsecure === '1') { configParts.push('skip-cert-verify=true'); } else { // 如果没有明确设置 allow_insecure，默认为 false configParts.push('skip-cert-verify=false'); } const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) configParts.push(`udp-relay-mode=${udpRelayMode}`); const cc = params.get('congestion_control') || params.get('congestion-control') || params.get('cc'); if (cc) configParts.push(`congestion-control=${cc}`); const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { configParts.push('disable-sni=true'); } const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { configParts.push('reduce-rtt=true'); } return configParts.join(', '); } catch (error) { return null; } } {dotted startColor="#ff6c6c" endColor="#1989fa"/}访问系统访问管理面板：https://你的域名/ADMIN_PATH订阅地址格式：原始格式：https://你的域名/订阅路径Base64 格式：https://你的域名/订阅路径/v2raySurge 格式：https://你的域名/订阅路径/surgeClash 格式：https://你的域名/订阅路径/clash{dotted startColor="#ff6c6c" endColor="#1989fa"/}使用说明创建订阅登录管理面板点击"添加订阅"按钮输入订阅名称和路径（路径只能包含小写字母、数字和连字符）点击"创建"按钮管理节点在订阅列表中找到目标订阅点击"添加节点"按钮添加新节点支持以下格式：单个节点链接多个节点链接（每行一个）Base64 编码的节点列表节点排序点击"节点列表"按钮查看节点拖拽节点行可以调整顺序顺序会自动保存批量操作点击"批量删除"按钮进入批量模式勾选要删除的节点点击"确认删除"执行删除操作注意事项首次部署后请立即修改默认的管理员密码定期备份数据库内容妥善保管管理面板地址和登录信息建议使用强密码提高安全性