文章详情

返回首页

CF搭建节点管理

分享文章 作者: Ws01 创建时间: 2025-11-24 📝 字数: 80,655 字 👁️ 阅读: 10 次
// 部署完成后在网址后面加上这个,获取自建节点和机场聚合节点,/?token=xxoo&tag=9527abc-jichang // 默认节点信息,聚合订阅地址:https://域名/?token=5758bf7a-87ad-4b69-a48c-c9c0bd4cfc1f&tag=9527abc-jichang // 部署完成后在网址后面加上这个,只获取自建节点,/?token=xxoo // 登录管理页面:https://域名/9527kkk/login // CF的kv数据库邦定名称NODES_KV const mytoken = '5758bf7a-87ad-4b69-a48c-c9c0bd4cfc1f'; //可以随便取,或者uuid生成,https://1024tools.com/uuid const tgbottoken =''; //可以为空,或者@BotFather中输入/start,/newbot,并关注机器人 const tgchatid =''; //可以为空,或者@userinfobot中获取,/start // 登录认证配置 const LOGIN_USERNAME = 'admin'; // 默认用户名,设置中修改,添加变量名USERNAME const LOGIN_PASSWORD = 'admin888'; // 默认密码,设置中修改,添加变量名PASSWORD const LOGIN_PATH = '/9527kkk/login'; // 登录页面路径 const ADMIN_PATH = '/9527kkk/'; // 管理页面路径 // 从环境变量获取登录凭据(如果设置了的话) const ENV_USERNAME = typeof USERNAME !== 'undefined' ? USERNAME : LOGIN_USERNAME; const ENV_PASSWORD = typeof PASSWORD !== 'undefined' ? PASSWORD : LOGIN_PASSWORD; // KV存储键名 const KV_KEYS = { CUSTOM_NODES: 'custom_nodes', SUBSCRIPTION_URLS: 'subscription_urls', AUTH_SESSIONS: 'auth_sessions' }; // 内存存储作为fallback let memoryStorage = { custom_nodes: [], subscription_urls: [], auth_sessions: {} }; // 创建fallback存储对象 let fallbackStorage = { async get(key) { console.log(`KV Get (fallback): ${key}`); return memoryStorage[key] ? JSON.stringify(memoryStorage[key]) : null; }, async put(key, value) { console.log(`KV Put (fallback): ${key} = ${value}`); try { memoryStorage[key] = JSON.parse(value); return true; } catch (error) { console.error('Memory storage error:', error); return false; } }, async delete(key) { console.log(`KV Delete (fallback): ${key}`); delete memoryStorage[key]; return true; } }; // 检查KV绑定状态 console.log('检查KV绑定状态...'); // 检查是否已经有KV绑定(Cloudflare会自动注入绑定的变量) let usingRealKV = false; // 方法1: 检查全局变量NODES_KV是否被Cloudflare注入 if (typeof NODES_KV !== 'undefined' && NODES_KV !== fallbackStorage) { usingRealKV = true; console.log('✅ 检测到KV绑定 (方法1) - 数据将持久保存'); } // 方法2: 检查是否有KV绑定对象 if (!usingRealKV && typeof NODES_KV_BINDING !== 'undefined') { NODES_KV = NODES_KV_BINDING; usingRealKV = true; console.log('✅ 检测到KV绑定 (方法2) - 数据将持久保存'); } // 方法3: 尝试直接访问绑定的变量 if (!usingRealKV) { try { // 在Cloudflare Workers中,绑定的变量会直接可用 if (typeof NODES_KV !== 'undefined' && NODES_KV && typeof NODES_KV.get === 'function') { usingRealKV = true; console.log('✅ 检测到KV绑定 (方法3) - 数据将持久保存'); } } catch (error) { console.log('KV检测方法3失败:', error); } } if (!usingRealKV) { NODES_KV = fallbackStorage; console.log('⚠️ 使用内存存储fallback - 数据在Worker重启后会丢失'); console.log('请确保在Worker设置中正确绑定了KV存储,变量名为: NODES_KV'); console.log('当前NODES_KV类型:', typeof NODES_KV); console.log('当前NODES_KV值:', NODES_KV); } addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) async function handleRequest(request) { const url = new URL(request.url); const pathname = url.pathname; const token = url.searchParams.get('token'); const tag = url.searchParams.get('tag'); // 处理静态资源请求(不需要token验证) if (pathname === '/favicon.ico' || pathname.startsWith('/static/') || pathname.endsWith('.css') || pathname.endsWith('.js')) { return new Response('', { status: 404 }); } // 登录页面路由 if (pathname === LOGIN_PATH) { return handleLoginPage(request); } // 登录API路由 if (pathname === LOGIN_PATH + '/auth') { return handleLoginAuth(request); } // 登出API路由 if (pathname === LOGIN_PATH + '/logout') { return handleLogout(request); } // 管理页面路由(需要登录验证) if (pathname === ADMIN_PATH) { return handleAdminPageWithAuth(request); } // 旧的管理页面路由(保持兼容性,但需要token验证) if (pathname === '/admin') { if (token !== mytoken) { return new Response('Invalid token???', { status: 403 }); } return handleAdminPage(request); } // API路由(需要登录验证) if (pathname.startsWith('/api/')) { return handleAPIWithAuth(request); } // 原有的节点订阅逻辑(保持原有token验证) if (token !== mytoken) { return new Response('Invalid token???', { status: 403 }); } return handleSubscription(request, tag); } async function handleAdminPage(request) { const html = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>节点管理后台</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚙️</text></svg>"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } .header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: #333; } .form-group input, .form-group textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .form-group textarea { height: 100px; resize: vertical; } .btn { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; margin-right: 10px; } .btn:hover { background: #0056b3; } .btn-danger { background: #dc3545; } .btn-danger:hover { background: #c82333; } .btn-success { background: #28a745; } .btn-success:hover { background: #218838; } .list-item { background: #f8f9fa; padding: 15px; margin-bottom: 10px; border-radius: 4px; border-left: 4px solid #007bff; } .list-item h4 { margin-bottom: 5px; color: #333; } .list-item p { color: #666; font-size: 14px; margin-bottom: 10px; } .actions { display: flex; gap: 10px; } /* 表格样式 */ .table-container { overflow-x: auto; margin-top: 20px; } .nodes-table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .nodes-table th { background: #f8f9fa; color: #333; font-weight: 600; padding: 12px 15px; text-align: left; border-bottom: 2px solid #dee2e6; } .nodes-table td { padding: 12px 15px; border-bottom: 1px solid #dee2e6; vertical-align: top; } .nodes-table tr:hover { background: #f8f9fa; } .nodes-table tr:last-child td { border-bottom: none; } .node-index { width: 100px; text-align: center; font-weight: 600; color: #007bff; } .node-name { width: 200px; font-weight: 500; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .node-config { font-family: monospace; font-size: 12px; color: #666; word-break: break-all; max-width: 400px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .node-actions { width: 220px; text-align: center; } .btn-sm { padding: 6px 12px; font-size: 12px; } /* 机场订阅表格样式 */ .subscription-table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .subscription-table th { background: #f8f9fa; color: #333; font-weight: 600; padding: 12px 15px; text-align: left; border-bottom: 2px solid #dee2e6; } .subscription-table td { padding: 12px 15px; border-bottom: 1px solid #dee2e6; vertical-align: top; } .subscription-table tr:hover { background: #f8f9fa; } .subscription-table tr:last-child td { border-bottom: none; } .sub-index { width: 100px; text-align: center; font-weight: 600; color: #007bff; } .sub-name { width: 200px; font-weight: 500; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sub-url { font-family: monospace; font-size: 12px; color: #666; word-break: break-all; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sub-actions { width: 220px; text-align: center; } .status { padding: 5px 10px; border-radius: 4px; font-size: 12px; font-weight: 500; } .status.success { background: #d4edda; color: #155724; } .status.error { background: #f8d7da; color: #721c24; } .status.warning { background: #fff3cd; color: #856404; } .tabs { display: flex; margin-bottom: 20px; } .tab { padding: 10px 20px; background: #e9ecef; border: none; cursor: pointer; border-radius: 4px 4px 0 0; margin-right: 5px; } .tab.active { background: #007bff; color: white; } .tab-content { display: none; } .tab-content.active { display: block; } </style> </head> <body> <div class="container"> <div class="header"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div> <h1>节点管理后台</h1> <p>管理自建节点和机场订阅链接</p> </div> <div style="text-align: right;"> <span id="user-info" style="color: #666; font-size: 14px;">欢迎,管理员</span> <br> <button onclick="logout()" class="btn btn-danger btn-sm" style="margin-top: 5px;">登出</button> </div> </div> <div id="storage-status" style="margin-top: 10px; padding: 8px; border-radius: 4px; font-size: 14px;"> <span id="status-text">检查存储状态中...</span> </div> </div> <div class="tabs"> <button class="tab active" onclick="switchTab('custom')">自建节点</button> <button class="tab" onclick="switchTab('subscription')">机场订阅</button> </div> <!-- 自建节点管理 --> <div id="custom-tab" class="tab-content active"> <div class="card"> <h3>添加自建节点</h3> <form id="custom-form"> <div class="form-group"> <label>节点配置 (支持多个节点,每行一个)</label> <textarea id="custom-config" placeholder="粘贴节点配置内容,支持多个节点,每行一个...&#10;支持:VLESS、VMess、Shadowsocks、Trojan等&#10;支持Base64编码的节点配置" required></textarea> </div> <button type="submit" class="btn btn-success">添加节点</button> </form> </div> <div class="card"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> <h3>自建节点列表</h3> <div> <button id="select-all-btn" class="btn btn-sm" onclick="toggleSelectAll()" style="margin-right: 10px;">全选</button> <button id="batch-delete-btn" class="btn btn-danger btn-sm" onclick="batchDeleteNodes()" disabled>批量删除</button> </div> </div> <div class="table-container"> <table class="nodes-table"> <thead> <tr> <th class="node-checkbox" style="width: 50px;"> <input type="checkbox" id="select-all-checkbox" onchange="handleSelectAllChange()"> </th> <th class="node-index">序号</th> <th class="node-name">节点名称</th> <th class="node-config">节点配置</th> <th class="node-actions">操作</th> </tr> </thead> <tbody id="custom-list"> <tr> <td colspan="5" style="text-align: center; padding: 20px; color: #666;">加载中...</td> </tr> </tbody> </table> </div> </div> </div> <!-- 机场订阅管理 --> <div id="subscription-tab" class="tab-content"> <div class="card"> <h3>添加机场订阅</h3> <form id="subscription-form"> <div class="form-group"> <label>订阅名称</label> <input type="text" id="subscription-name" placeholder="例如:机场A" required> </div> <div class="form-group"> <label>订阅链接</label> <input type="url" id="subscription-url" placeholder="https://example.com/subscription" required> </div> <button type="submit" class="btn btn-success">添加订阅</button> </form> </div> <div class="card"> <h3>机场订阅列表</h3> <div class="table-container"> <table class="subscription-table"> <thead> <tr> <th class="sub-index">序号</th> <th class="sub-name">订阅名称</th> <th class="sub-url">订阅链接</th> <th class="sub-actions">操作</th> </tr> </thead> <tbody id="subscription-list"> <tr> <td colspan="4" style="text-align: center; padding: 20px; color: #666;">加载中...</td> </tr> </tbody> </table> </div> </div> </div> </div> <script> let currentTab = 'custom'; function switchTab(tab) { // 隐藏所有标签页 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // 显示选中的标签页 document.getElementById(tab + '-tab').classList.add('active'); event.target.classList.add('active'); currentTab = tab; } // 加载数据 async function loadData() { await loadCustomNodes(); await loadSubscriptions(); } // 加载自建节点 async function loadCustomNodes() { try { const response = await fetch('/api/custom-nodes'); const data = await response.json(); const tbody = document.getElementById('custom-list'); if (data.length === 0) { tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 20px; color: #666;">暂无自建节点</td></tr>'; return; } tbody.innerHTML = data.map((node, index) => \` <tr> <td class="node-checkbox" style="text-align: center;"> <input type="checkbox" class="node-checkbox-input" value="\${node.id}" onchange="handleNodeCheckboxChange()"> </td> <td class="node-index">\${index + 1}</td> <td class="node-name" title="\${node.name}">\${truncateText(node.name, 20)}</td> <td class="node-config" title="\${node.config}">\${truncateText(node.config, 50)}</td> <td class="node-actions"> <button class="btn btn-sm" onclick="editCustomNode('\${node.id}')" style="background: #28a745; color: white; margin-right: 5px;">编辑</button> <button class="btn btn-sm" onclick="copyCustomNode('\${node.id}')" style="background: #17a2b8; color: white; margin-right: 5px;">复制</button> <button class="btn btn-danger btn-sm" onclick="deleteCustomNode('\${node.id}')">删除</button> </td> </tr> \`).join(''); } catch (error) { console.error('Load custom nodes error:', error); document.getElementById('custom-list').innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 20px; color: #dc3545;">加载失败: ' + error.message + '</td></tr>'; } } // 加载机场订阅 async function loadSubscriptions() { try { const response = await fetch('/api/subscriptions'); const data = await response.json(); const tbody = document.getElementById('subscription-list'); if (data.length === 0) { tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 20px; color: #666;">暂无机场订阅</td></tr>'; return; } tbody.innerHTML = data.map((sub, index) => \` <tr> <td class="sub-index">\${index + 1}</td> <td class="sub-name" title="\${sub.name}">\${truncateText(sub.name, 20)}</td> <td class="sub-url" title="\${sub.url}">\${truncateText(sub.url, 50)}</td> <td class="sub-actions"> <button class="btn btn-sm" onclick="editSubscription('\${sub.id}')" style="background: #28a745; color: white; margin-right: 5px;">编辑</button> <button class="btn btn-sm" onclick="copySubscription('\${sub.id}')" style="background: #17a2b8; color: white; margin-right: 5px;">复制</button> <button class="btn btn-danger btn-sm" onclick="deleteSubscription('\${sub.id}')">删除</button> </td> </tr> \`).join(''); } catch (error) { console.error('Load subscriptions error:', error); document.getElementById('subscription-list').innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 20px; color: #dc3545;">加载失败: ' + error.message + '</td></tr>'; } } // 添加自建节点 document.getElementById('custom-form').addEventListener('submit', async (e) => { e.preventDefault(); const config = document.getElementById('custom-config').value.trim(); if (!config) { showStatus('请输入节点配置', 'error'); return; } try { const response = await fetch('/api/custom-nodes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config }) }); const result = await response.json(); if (response.ok && result.success) { // 显示详细的添加结果 if (result.duplicateCount > 0) { showStatus(result.message, 'warning'); console.log('重复的节点:', result.duplicates); } else { showStatus(result.message, 'success'); } document.getElementById('custom-form').reset(); loadCustomNodes(); } else { showStatus(result.error || '添加失败', 'error'); console.error('Add custom node error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } }); // 添加机场订阅 document.getElementById('subscription-form').addEventListener('submit', async (e) => { e.preventDefault(); const name = document.getElementById('subscription-name').value; const url = document.getElementById('subscription-url').value; try { const response = await fetch('/api/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, url }) }); const result = await response.json(); if (response.ok && result.success) { showStatus(result.message || '添加成功', 'success'); document.getElementById('subscription-form').reset(); loadSubscriptions(); } else { showStatus(result.error || '添加失败', 'error'); console.error('Add subscription error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } }); // 删除自建节点 async function deleteCustomNode(id) { if (!confirm('确定要删除这个节点吗?')) return; try { const response = await fetch(\`/api/custom-nodes/\${id}\`, { method: 'DELETE' }); const result = await response.json(); if (response.ok && result.success) { showStatus('删除成功', 'success'); loadCustomNodes(); } else { showStatus(result.error || '删除失败', 'error'); console.error('Delete custom node error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } } // 删除机场订阅 async function deleteSubscription(id) { if (!confirm('确定要删除这个订阅吗?')) return; try { const response = await fetch(\`/api/subscriptions/\${id}\`, { method: 'DELETE' }); const result = await response.json(); if (response.ok && result.success) { showStatus('删除成功', 'success'); loadSubscriptions(); } else { showStatus(result.error || '删除失败', 'error'); console.error('Delete subscription error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } } // 截断文本显示 function truncateText(text, maxLength) { if (text.length <= maxLength) { return text; } return text.substring(0, maxLength) + '...'; } // 显示状态消息 function showStatus(message, type) { const status = document.createElement('div'); status.className = \`status \${type}\`; status.textContent = message; status.style.position = 'fixed'; status.style.top = '20px'; status.style.right = '20px'; status.style.zIndex = '1000'; document.body.appendChild(status); setTimeout(() => { status.remove(); }, 3000); } // 检查存储状态 async function checkStorageStatus() { try { const response = await fetch('/api/storage-status'); const result = await response.json(); const statusDiv = document.getElementById('storage-status'); const statusText = document.getElementById('status-text'); if (result.usingKV) { statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.style.border = '1px solid #c3e6cb'; statusText.textContent = '✅ 使用KV存储 - 数据将持久保存'; } else { statusDiv.style.background = '#fff3cd'; statusDiv.style.color = '#856404'; statusDiv.style.border = '1px solid #ffeaa7'; statusText.innerHTML = '⚠️ 使用内存存储 - 数据在Worker重启后会丢失<br><small>请按照KV配置指南正确绑定KV存储</small>'; } } catch (error) { const statusDiv = document.getElementById('storage-status'); const statusText = document.getElementById('status-text'); statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.style.border = '1px solid #f5c6cb'; statusText.textContent = '❌ 无法检查存储状态'; } } // 全选/取消全选功能 function toggleSelectAll() { const selectAllCheckbox = document.getElementById('select-all-checkbox'); const nodeCheckboxes = document.querySelectorAll('.node-checkbox-input'); // 检查是否所有节点都被选中 const allChecked = Array.from(nodeCheckboxes).every(checkbox => checkbox.checked); // 如果全部选中,则取消全选;否则全选 const shouldCheck = !allChecked; nodeCheckboxes.forEach(checkbox => { checkbox.checked = shouldCheck; }); // 更新全选复选框状态 selectAllCheckbox.checked = shouldCheck; selectAllCheckbox.indeterminate = false; updateBatchDeleteButton(); } // 处理全选复选框变化 function handleSelectAllChange() { const selectAllCheckbox = document.getElementById('select-all-checkbox'); const nodeCheckboxes = document.querySelectorAll('.node-checkbox-input'); // 根据全选复选框的状态来设置所有节点复选框 const isChecked = selectAllCheckbox.checked; nodeCheckboxes.forEach(checkbox => { checkbox.checked = isChecked; }); // 清除indeterminate状态 selectAllCheckbox.indeterminate = false; updateBatchDeleteButton(); } // 处理单个节点复选框变化 function handleNodeCheckboxChange() { const selectAllCheckbox = document.getElementById('select-all-checkbox'); const nodeCheckboxes = document.querySelectorAll('.node-checkbox-input'); // 检查是否所有节点都被选中 const allChecked = Array.from(nodeCheckboxes).every(checkbox => checkbox.checked); const someChecked = Array.from(nodeCheckboxes).some(checkbox => checkbox.checked); selectAllCheckbox.checked = allChecked; selectAllCheckbox.indeterminate = someChecked && !allChecked; updateBatchDeleteButton(); } // 更新批量删除按钮状态 function updateBatchDeleteButton() { const selectedCheckboxes = document.querySelectorAll('.node-checkbox-input:checked'); const batchDeleteBtn = document.getElementById('batch-delete-btn'); if (selectedCheckboxes.length > 0) { batchDeleteBtn.disabled = false; batchDeleteBtn.textContent = '批量删除 (' + selectedCheckboxes.length + ')'; } else { batchDeleteBtn.disabled = true; batchDeleteBtn.textContent = '批量删除'; } } // 批量删除节点 async function batchDeleteNodes() { const selectedCheckboxes = document.querySelectorAll('.node-checkbox-input:checked'); const selectedIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.value); if (selectedIds.length === 0) { showStatus('请先选择要删除的节点', 'error'); return; } if (!confirm('确定要删除选中的 ' + selectedIds.length + ' 个节点吗?')) { return; } try { // 批量删除请求 const response = await fetch('/api/custom-nodes/batch-delete', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: selectedIds }) }); const result = await response.json(); if (response.ok && result.success) { showStatus('成功删除 ' + result.deletedCount + ' 个节点', 'success'); loadCustomNodes(); // 重新加载节点列表 } else { showStatus(result.error || '批量删除失败', 'error'); console.error('Batch delete error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } } // 复制节点配置 async function copyCustomNode(nodeId) { try { // 获取节点数据 const response = await fetch('/api/custom-nodes'); const nodes = await response.json(); const node = nodes.find(n => n.id === nodeId); if (!node) { showStatus('未找到要复制的节点', 'error'); return; } // 复制到剪贴板 await navigator.clipboard.writeText(node.config); showStatus('节点配置已复制到剪贴板', 'success'); } catch (error) { // 如果剪贴板API不可用,使用传统方法 try { const response = await fetch('/api/custom-nodes'); const nodes = await response.json(); const node = nodes.find(n => n.id === nodeId); if (node) { // 创建临时文本区域 const textArea = document.createElement('textarea'); textArea.value = node.config; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showStatus('节点配置已复制到剪贴板', 'success'); } else { showStatus('未找到要复制的节点', 'error'); } } catch (fallbackError) { showStatus('复制失败: ' + error.message, 'error'); console.error('Copy error:', error); } } } // 编辑节点配置 async function editCustomNode(nodeId) { try { // 获取节点数据 const response = await fetch('/api/custom-nodes'); const nodes = await response.json(); const node = nodes.find(n => n.id === nodeId); if (!node) { showStatus('未找到要编辑的节点', 'error'); return; } // 显示编辑对话框 showEditModal(node); } catch (error) { showStatus('获取节点信息失败: ' + error.message, 'error'); console.error('Get node error:', error); } } // 显示编辑模态框 function showEditModal(node) { // 创建模态框HTML const modalHtml = \` <div id="editModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center;"> <div style="background: white; padding: 30px; border-radius: 8px; width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto;"> <h3 style="margin-bottom: 20px; color: #333;">编辑节点配置</h3> <div class="form-group"> <label>节点名称</label> <input type="text" id="edit-node-name" value="\${node.name}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 15px;"> </div> <div class="form-group"> <label>节点配置</label> <textarea id="edit-node-config" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; height: 200px; font-family: monospace; resize: vertical;">\${node.config}</textarea> </div> <div style="text-align: right; margin-top: 20px;"> <button onclick="closeEditModal()" class="btn" style="margin-right: 10px; background: #6c757d; color: white;">取消</button> <button onclick="saveEditedNode('\${node.id}')" class="btn btn-success">保存</button> </div> </div> </div> \`; // 添加到页面 document.body.insertAdjacentHTML('beforeend', modalHtml); } // 关闭编辑模态框 function closeEditModal() { const modal = document.getElementById('editModal'); if (modal) { modal.remove(); } } // 保存编辑的节点 async function saveEditedNode(nodeId) { const name = document.getElementById('edit-node-name').value.trim(); const config = document.getElementById('edit-node-config').value.trim(); if (!name || !config) { showStatus('节点名称和配置不能为空', 'error'); return; } try { const response = await fetch('/api/custom-nodes/' + nodeId, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name, config }) }); const result = await response.json(); if (response.ok && result.success) { showStatus('节点更新成功', 'success'); closeEditModal(); loadCustomNodes(); // 重新加载节点列表 } else { showStatus(result.error || '更新失败', 'error'); console.error('Update node error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } } // 复制机场订阅 async function copySubscription(subscriptionId) { try { // 获取订阅数据 const response = await fetch('/api/subscriptions'); const subscriptions = await response.json(); const subscription = subscriptions.find(s => s.id === subscriptionId); if (!subscription) { showStatus('未找到要复制的订阅', 'error'); return; } // 复制到剪贴板 await navigator.clipboard.writeText(subscription.url); showStatus('订阅链接已复制到剪贴板', 'success'); } catch (error) { // 如果剪贴板API不可用,使用传统方法 try { const response = await fetch('/api/subscriptions'); const subscriptions = await response.json(); const subscription = subscriptions.find(s => s.id === subscriptionId); if (subscription) { // 创建临时文本区域 const textArea = document.createElement('textarea'); textArea.value = subscription.url; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showStatus('订阅链接已复制到剪贴板', 'success'); } else { showStatus('未找到要复制的订阅', 'error'); } } catch (fallbackError) { showStatus('复制失败: ' + error.message, 'error'); console.error('Copy subscription error:', error); } } } // 编辑机场订阅 async function editSubscription(subscriptionId) { try { // 获取订阅数据 const response = await fetch('/api/subscriptions'); const subscriptions = await response.json(); const subscription = subscriptions.find(s => s.id === subscriptionId); if (!subscription) { showStatus('未找到要编辑的订阅', 'error'); return; } // 显示编辑对话框 showSubscriptionEditModal(subscription); } catch (error) { showStatus('获取订阅信息失败: ' + error.message, 'error'); console.error('Get subscription error:', error); } } // 显示订阅编辑模态框 function showSubscriptionEditModal(subscription) { // 创建模态框HTML const modalHtml = \` <div id="subscriptionEditModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center;"> <div style="background: white; padding: 30px; border-radius: 8px; width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto;"> <h3 style="margin-bottom: 20px; color: #333;">编辑机场订阅</h3> <div class="form-group"> <label>订阅名称</label> <input type="text" id="edit-subscription-name" value="\${subscription.name}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 15px;"> </div> <div class="form-group"> <label>订阅链接</label> <textarea id="edit-subscription-url" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; height: 120px; font-family: monospace; resize: vertical;">\${subscription.url}</textarea> </div> <div style="text-align: right; margin-top: 20px;"> <button onclick="closeSubscriptionEditModal()" class="btn" style="margin-right: 10px; background: #6c757d; color: white;">取消</button> <button onclick="saveEditedSubscription('\${subscription.id}')" class="btn btn-success">保存</button> </div> </div> </div> \`; // 添加到页面 document.body.insertAdjacentHTML('beforeend', modalHtml); } // 关闭订阅编辑模态框 function closeSubscriptionEditModal() { const modal = document.getElementById('subscriptionEditModal'); if (modal) { modal.remove(); } } // 保存编辑的订阅 async function saveEditedSubscription(subscriptionId) { const name = document.getElementById('edit-subscription-name').value.trim(); const url = document.getElementById('edit-subscription-url').value.trim(); if (!name || !url) { showStatus('订阅名称和链接不能为空', 'error'); return; } // 验证URL格式 try { new URL(url); } catch (urlError) { showStatus('订阅链接格式不正确', 'error'); return; } try { const response = await fetch('/api/subscriptions/' + subscriptionId, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name, url }) }); const result = await response.json(); if (response.ok && result.success) { showStatus('订阅更新成功', 'success'); closeSubscriptionEditModal(); loadSubscriptions(); // 重新加载订阅列表 } else { showStatus(result.error || '更新失败', 'error'); console.error('Update subscription error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } } // 登出功能 async function logout() { if (!confirm('确定要登出吗?')) return; try { const response = await fetch('${LOGIN_PATH}/logout', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const result = await response.json(); if (response.ok && result.success) { // 登出成功,跳转到登录页面 window.location.href = '${LOGIN_PATH}'; } else { showStatus('登出失败: ' + (result.error || '未知错误'), 'error'); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Logout error:', error); } } // 页面加载时初始化 loadData(); checkStorageStatus(); </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // 登录页面处理函数 async function handleLoginPage(request) { const html = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>节点管理后台 - 登录</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔐</text></svg>"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } .login-container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 15px 35px rgba(0,0,0,0.1); width: 100%; max-width: 400px; } .login-header { text-align: center; margin-bottom: 30px; } .login-header h1 { color: #333; margin-bottom: 10px; font-size: 28px; } .login-header p { color: #666; font-size: 14px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #333; } .form-group input { width: 100%; padding: 12px 16px; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 16px; transition: border-color 0.3s ease; } .form-group input:focus { outline: none; border-color: #667eea; } .login-btn { width: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px; border-radius: 8px; font-size: 16px; font-weight: 500; cursor: pointer; transition: transform 0.2s ease; } .login-btn:hover { transform: translateY(-2px); } .login-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .error-message { background: #fee; color: #c33; padding: 10px; border-radius: 6px; margin-bottom: 20px; font-size: 14px; display: none; } .loading { display: none; text-align: center; margin-top: 10px; } .spinner { border: 2px solid #f3f3f3; border-top: 2px solid #667eea; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <div class="login-container"> <div class="login-header"> <h1>🔐 登录管理后台</h1> <p>请输入您的登录凭据</p> </div> <div class="error-message" id="errorMessage"></div> <form id="loginForm"> <div class="form-group"> <label for="username">用户名</label> <input type="text" id="username" name="username" required autocomplete="username"> </div> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" name="password" required autocomplete="current-password"> </div> <button type="submit" class="login-btn" id="loginBtn"> 登录 </button> </form> <div class="loading" id="loading"> <div class="spinner"></div> <p>正在验证...</p> </div> </div> <script> document.getElementById('loginForm').addEventListener('submit', async function(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; const errorDiv = document.getElementById('errorMessage'); const loginBtn = document.getElementById('loginBtn'); const loading = document.getElementById('loading'); // 隐藏错误信息 errorDiv.style.display = 'none'; // 显示加载状态 loginBtn.disabled = true; loading.style.display = 'block'; try { const response = await fetch('${LOGIN_PATH}/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }) }); const result = await response.json(); if (response.ok && result.success) { // 登录成功,跳转到管理页面 window.location.href = '${ADMIN_PATH}'; } else { // 显示错误信息 errorDiv.textContent = result.error || '登录失败,请检查用户名和密码'; errorDiv.style.display = 'block'; } } catch (error) { errorDiv.textContent = '网络错误,请稍后重试'; errorDiv.style.display = 'block'; } finally { // 隐藏加载状态 loginBtn.disabled = false; loading.style.display = 'none'; } }); // 自动聚焦到用户名输入框 document.getElementById('username').focus(); </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // 登录认证API async function handleLoginAuth(request) { try { const { username, password } = await request.json(); // 验证用户名和密码 if (username === ENV_USERNAME && password === ENV_PASSWORD) { // 生成会话ID const sessionId = generateSessionId(); const sessionData = { username: username, loginTime: Date.now(), expires: Date.now() + (24 * 60 * 60 * 1000) // 24小时过期 }; // 存储会话到KV await NODES_KV.put(`session_${sessionId}`, JSON.stringify(sessionData)); // 设置Cookie const response = new Response(JSON.stringify({ success: true, message: '登录成功' }), { headers: { 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; Max-Age=86400; SameSite=Strict` } }); return response; } else { return new Response(JSON.stringify({ success: false, error: '用户名或密码错误' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } } catch (error) { return new Response(JSON.stringify({ success: false, error: '登录验证失败' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 登出API async function handleLogout(request) { try { const sessionId = getSessionIdFromRequest(request); if (sessionId) { // 删除会话 await NODES_KV.delete(`session_${sessionId}`); } return new Response(JSON.stringify({ success: true, message: '登出成功' }), { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'session=; Path=/; HttpOnly; Max-Age=0; SameSite=Strict' } }); } catch (error) { return new Response(JSON.stringify({ success: false, error: '登出失败' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 生成会话ID function generateSessionId() { return 'sess_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } // 从请求中获取会话ID function getSessionIdFromRequest(request) { const cookieHeader = request.headers.get('Cookie'); if (!cookieHeader) return null; const cookies = cookieHeader.split(';').map(c => c.trim()); const sessionCookie = cookies.find(c => c.startsWith('session=')); if (sessionCookie) { return sessionCookie.split('=')[1]; } return null; } // 验证会话 async function validateSession(request) { try { const sessionId = getSessionIdFromRequest(request); if (!sessionId) { return { valid: false, reason: 'No session' }; } const sessionData = await NODES_KV.get(`session_${sessionId}`); if (!sessionData) { return { valid: false, reason: 'Session not found' }; } const session = JSON.parse(sessionData); // 检查会话是否过期 if (Date.now() > session.expires) { // 删除过期会话 await NODES_KV.delete(`session_${sessionId}`); return { valid: false, reason: 'Session expired' }; } return { valid: true, session: session }; } catch (error) { return { valid: false, reason: 'Session validation error' }; } } // 带认证的管理页面处理函数 async function handleAdminPageWithAuth(request) { // 验证登录状态 const sessionValidation = await validateSession(request); if (!sessionValidation.valid) { // 未登录,重定向到登录页面 return new Response(null, { status: 302, headers: { 'Location': LOGIN_PATH } }); } // 已登录,显示管理页面 return handleAdminPage(request); } // 带认证的API处理函数 async function handleAPIWithAuth(request) { // 验证登录状态 const sessionValidation = await validateSession(request); if (!sessionValidation.valid) { return new Response(JSON.stringify({ success: false, error: '未登录或会话已过期,请重新登录' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 已登录,处理API请求 return handleAPI(request); } // API处理函数 async function handleAPI(request) { const url = new URL(request.url); const pathname = url.pathname; const method = request.method; // 自建节点API if (pathname === '/api/custom-nodes') { if (method === 'GET') { return getCustomNodes(); } else if (method === 'POST') { const data = await request.json(); return addCustomNode(data); } } // 删除自建节点API if (pathname.startsWith('/api/custom-nodes/') && method === 'DELETE') { const id = pathname.split('/')[3]; return deleteCustomNode(id); } // 更新自建节点API if (pathname.startsWith('/api/custom-nodes/') && method === 'PUT') { const id = pathname.split('/')[3]; const data = await request.json(); return updateCustomNode(id, data); } // 批量删除自建节点API if (pathname === '/api/custom-nodes/batch-delete' && method === 'POST') { const data = await request.json(); return batchDeleteCustomNodes(data); } // 机场订阅API if (pathname === '/api/subscriptions') { if (method === 'GET') { return getSubscriptions(); } else if (method === 'POST') { const data = await request.json(); return addSubscription(data); } } // 删除机场订阅API if (pathname.startsWith('/api/subscriptions/') && method === 'DELETE') { const id = pathname.split('/')[3]; return deleteSubscription(id); } // 更新机场订阅API if (pathname.startsWith('/api/subscriptions/') && method === 'PUT') { const id = pathname.split('/')[3]; const data = await request.json(); return updateSubscription(id, data); } // 存储状态检查API if (pathname === '/api/storage-status') { return checkStorageStatus(); } // KV测试API if (pathname === '/api/kv-test') { return testKVConnection(); } // 节点名称解码测试API if (pathname === '/api/decode-test') { return testNodeNameDecoding(); } // Base64解码测试API if (pathname === '/api/base64-test') { return testBase64Decoding(); } return new Response('Not Found', { status: 404 }); } // 检查存储状态 async function checkStorageStatus() { // 检查KV是否被正确绑定 let usingKV = false; let storageType = '内存存储'; let message = '数据在Worker重启后会丢失'; // 检查是否使用了真实的KV存储 if (NODES_KV !== fallbackStorage) { usingKV = true; storageType = 'KV存储'; message = '数据将持久保存'; } return new Response(JSON.stringify({ usingKV: usingKV, storageType: storageType, message: message, debug: { hasNODES_KV: typeof NODES_KV !== 'undefined', isFallbackStorage: NODES_KV === fallbackStorage, hasNODES_KV_BINDING: typeof NODES_KV_BINDING !== 'undefined', NODES_KV_type: typeof NODES_KV } }), { headers: { 'Content-Type': 'application/json' } }); } // 测试KV连接 async function testKVConnection() { const testKey = 'kv_test_' + Date.now(); const testValue = 'test_value_' + Math.random(); try { // 尝试写入测试数据 await NODES_KV.put(testKey, testValue); // 尝试读取测试数据 const retrievedValue = await NODES_KV.get(testKey); // 清理测试数据 try { await NODES_KV.delete(testKey); } catch (deleteError) { console.log('清理测试数据失败:', deleteError); } const isKVWorking = retrievedValue === testValue; return new Response(JSON.stringify({ success: true, kvWorking: isKVWorking, testKey: testKey, testValue: testValue, retrievedValue: retrievedValue, storageType: isKVWorking ? 'KV存储' : '内存存储', message: isKVWorking ? 'KV存储工作正常' : 'KV存储未正确配置', debug: { NODES_KV_type: typeof NODES_KV, NODES_KV_constructor: NODES_KV?.constructor?.name, hasGet: typeof NODES_KV?.get === 'function', hasPut: typeof NODES_KV?.put === 'function', hasDelete: typeof NODES_KV?.delete === 'function' } }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ success: false, error: error.message, kvWorking: false, storageType: '内存存储', message: 'KV测试失败: ' + error.message, debug: { NODES_KV_type: typeof NODES_KV, error_stack: error.stack } }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 测试节点名称解码 async function testNodeNameDecoding() { const testConfig = 'vless://7a169e43-ff85-4572-9843-ba7207d07319@192.9.162.122:1443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=swdist.apple.com&fp=qq&pbk=ZIBYUH_qQSeI1T6xImXG6MEZXP2yZW3NqGa8W69Cfyk&sid=dde50f55d81116&spx=%2F&type=tcp&headerType=none#%E6%82%89%E5%B0%BC%E5%A4%A7%E9%99%86%E4%BC%98%E5%8C%96BGP%E7%BA%BF%E8%B7%AF'; let nodeName = ''; if (testConfig.includes('#')) { const namePart = testConfig.split('#').pop().trim(); try { nodeName = decodeURIComponent(namePart); } catch (e) { nodeName = namePart; } } return new Response(JSON.stringify({ success: true, originalConfig: testConfig, encodedName: testConfig.split('#').pop(), decodedName: nodeName, testResult: nodeName === '悉尼大陆优化BGP线路' }), { headers: { 'Content-Type': 'application/json' } }); } // 测试Base64解码 async function testBase64Decoding() { const testBase64 = 'aHlzdGVyaWEyOi8vNzljNGZlMTEtOTc4Ny00MDZiLWJmOTQtYzFjMWRiZjU5ZTI4QDc3LjIyMy4yMTQuMTkzOjMxNDY4P3NuaT13d3cuYmluZy5jb20maW5zZWN1cmU9MSNpbG92ZXlvdSUyMC0lMjAlRjAlOUYlOTIlOEUlREElQTklRDglQTclRDklODYlRDklODElREIlOEMlREElQUYlMjAlRDklODclRDglQTclREIlOEMlMjAlRDglQTglREIlOEMlRDglQjQlRDglQUElRDglQjElMjAlRDglQUYlRDglQjElMjAlREElODYlRDklODYlRDklODQlMjAlRDglQUElRDklODQlREElQUYlRDglQjElRDglQTcuLi4NCg=='; try { const decodedConfig = atob(testBase64); console.log('Base64解码测试:', decodedConfig); // 解析解码后的配置 const lines = decodedConfig.split('\n').map(line => line.trim()).filter(line => line); const nodes = []; for (const line of lines) { if (line) { const node = processNodeConfig(line, [], nodes); if (node) { nodes.push(node); } } } return new Response(JSON.stringify({ success: true, originalBase64: testBase64, decodedConfig: decodedConfig, parsedNodes: nodes, nodeCount: nodes.length, isBase64Detected: isBase64Encoded(testBase64) }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ success: false, error: error.message, originalBase64: testBase64, isBase64Detected: isBase64Encoded(testBase64) }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 获取自建节点 async function getCustomNodes() { try { const data = await NODES_KV.get(KV_KEYS.CUSTOM_NODES); const nodes = data ? JSON.parse(data) : []; return new Response(JSON.stringify(nodes), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' } }); } } // 添加自建节点 async function addCustomNode(data) { try { console.log('Adding custom nodes:', data); // 验证输入数据 if (!data.config) { return new Response(JSON.stringify({ success: false, error: '节点配置不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.CUSTOM_NODES); console.log('Existing data:', existingData); const nodes = existingData ? JSON.parse(existingData) : []; console.log('Current nodes count:', nodes.length); // 解析多个节点配置 const configLines = data.config.split('\n').map(line => line.trim()).filter(line => line); const newNodes = []; const duplicateNodes = []; for (let i = 0; i < configLines.length; i++) { let config = configLines[i]; // 检测并解码Base64编码的节点配置 if (isBase64Encoded(config)) { try { const decodedConfig = atob(config); console.log('Base64解码前:', config); console.log('Base64解码后:', decodedConfig); // 如果解码后包含多个节点(用换行分隔),分别处理 const decodedLines = decodedConfig.split('\n').map(line => line.trim()).filter(line => line); for (const decodedLine of decodedLines) { if (decodedLine) { const node = processNodeConfig(decodedLine, nodes, newNodes); if (node) { newNodes.push(node); } else { // 记录重复的节点 duplicateNodes.push(decodedLine); } } } continue; // 跳过下面的单个节点处理 } catch (error) { console.error('Base64解码失败:', error); // 如果解码失败,继续按普通配置处理 } } // 处理普通节点配置 const node = processNodeConfig(config, nodes, newNodes); if (node) { newNodes.push(node); } else { // 记录重复的节点 duplicateNodes.push(config); } } // 添加新节点到现有列表 nodes.push(...newNodes); console.log('New nodes count:', nodes.length); console.log('Added nodes:', newNodes.length); console.log('Duplicate nodes:', duplicateNodes.length); const putResult = await NODES_KV.put(KV_KEYS.CUSTOM_NODES, JSON.stringify(nodes)); console.log('Put result:', putResult); // 构建响应消息 let message = ''; if (newNodes.length > 0 && duplicateNodes.length > 0) { message = `成功添加 ${newNodes.length} 个节点,跳过 ${duplicateNodes.length} 个重复节点`; } else if (newNodes.length > 0) { message = `成功添加 ${newNodes.length} 个节点`; } else if (duplicateNodes.length > 0) { message = `所有 ${duplicateNodes.length} 个节点都已存在,未添加任何新节点`; } else { message = '没有有效的节点配置'; } return new Response(JSON.stringify({ success: true, addedCount: newNodes.length, duplicateCount: duplicateNodes.length, message: message, duplicates: duplicateNodes }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Add custom node error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '添加节点时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 检测是否为Base64编码 function isBase64Encoded(str) { // Base64字符串通常只包含A-Z, a-z, 0-9, +, /, = 字符 const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; // 长度必须是4的倍数 return base64Regex.test(str) && str.length % 4 === 0 && str.length > 20; } // 检测节点是否重复 function isNodeDuplicate(config, existingNodes) { // 标准化配置字符串进行比较 const normalizeConfig = (config) => { // 移除可能的空白字符和换行符 return config.trim().replace(/\s+/g, ''); }; const normalizedNewConfig = normalizeConfig(config); // 检查是否与现有节点重复 return existingNodes.some(node => { const normalizedExistingConfig = normalizeConfig(node.config); return normalizedExistingConfig === normalizedNewConfig; }); } // 处理单个节点配置 function processNodeConfig(config, existingNodes, newNodes) { // 检查是否重复 if (isNodeDuplicate(config, existingNodes)) { console.log('Duplicate node detected:', config); return null; // 返回null表示跳过重复节点 } // 提取节点名称(从#后面或配置中提取) let nodeName = ''; if (config.includes('#')) { const namePart = config.split('#').pop().trim(); // 解码URL编码的中文字符 try { nodeName = decodeURIComponent(namePart); } catch (e) { nodeName = namePart; // 如果解码失败,使用原始字符串 } } else if (config.includes('ps=')) { // 对于vmess链接,尝试从ps参数提取名称 const psMatch = config.match(/ps=([^&]+)/); if (psMatch) { try { nodeName = decodeURIComponent(psMatch[1]); } catch (e) { nodeName = psMatch[1]; // 如果解码失败,使用原始字符串 } } } else if (config.includes('remarks=')) { // 对于其他协议,尝试从remarks参数提取名称 const remarksMatch = config.match(/remarks=([^&]+)/); if (remarksMatch) { try { nodeName = decodeURIComponent(remarksMatch[1]); } catch (e) { nodeName = remarksMatch[1]; // 如果解码失败,使用原始字符串 } } } // 如果没有提取到名称,使用默认名称 if (!nodeName) { nodeName = `节点 ${existingNodes.length + newNodes.length + 1}`; } const newNode = { id: (Date.now() + Math.random()).toString(), name: nodeName, config: config, createdAt: new Date().toISOString() }; return newNode; } // 删除自建节点 async function deleteCustomNode(id) { try { console.log('Deleting custom node:', id); if (!id) { return new Response(JSON.stringify({ success: false, error: '节点ID不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.CUSTOM_NODES); const nodes = existingData ? JSON.parse(existingData) : []; console.log('Current nodes count:', nodes.length); const originalLength = nodes.length; const filteredNodes = nodes.filter(node => node.id !== id); if (filteredNodes.length === originalLength) { return new Response(JSON.stringify({ success: false, error: '未找到要删除的节点' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } console.log('Filtered nodes count:', filteredNodes.length); await NODES_KV.put(KV_KEYS.CUSTOM_NODES, JSON.stringify(filteredNodes)); return new Response(JSON.stringify({ success: true, message: '节点删除成功' }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Delete custom node error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '删除节点时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 更新自建节点 async function updateCustomNode(id, data) { try { console.log('Updating custom node:', id, data); // 验证输入数据 if (!data.name || !data.config) { return new Response(JSON.stringify({ success: false, error: '节点名称和配置不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.CUSTOM_NODES); const nodes = existingData ? JSON.parse(existingData) : []; console.log('Current nodes count:', nodes.length); // 查找要更新的节点 const nodeIndex = nodes.findIndex(node => node.id === id); if (nodeIndex === -1) { return new Response(JSON.stringify({ success: false, error: '未找到要更新的节点' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 更新节点信息 nodes[nodeIndex].name = data.name; nodes[nodeIndex].config = data.config; nodes[nodeIndex].updatedAt = new Date().toISOString(); console.log('Updated node:', nodes[nodeIndex]); await NODES_KV.put(KV_KEYS.CUSTOM_NODES, JSON.stringify(nodes)); return new Response(JSON.stringify({ success: true, message: '节点更新成功' }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Update custom node error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '更新节点时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 批量删除自建节点 async function batchDeleteCustomNodes(data) { try { console.log('Batch deleting custom nodes:', data); // 验证输入数据 if (!data.ids || !Array.isArray(data.ids) || data.ids.length === 0) { return new Response(JSON.stringify({ success: false, error: '节点ID列表不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.CUSTOM_NODES); const nodes = existingData ? JSON.parse(existingData) : []; console.log('Current nodes count:', nodes.length); console.log('Nodes to delete:', data.ids); const originalLength = nodes.length; const filteredNodes = nodes.filter(node => !data.ids.includes(node.id)); const deletedCount = originalLength - filteredNodes.length; console.log('Filtered nodes count:', filteredNodes.length); console.log('Deleted count:', deletedCount); if (deletedCount === 0) { return new Response(JSON.stringify({ success: false, error: '未找到要删除的节点' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } await NODES_KV.put(KV_KEYS.CUSTOM_NODES, JSON.stringify(filteredNodes)); return new Response(JSON.stringify({ success: true, deletedCount: deletedCount, message: `成功删除 ${deletedCount} 个节点` }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Batch delete custom nodes error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '批量删除节点时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 获取机场订阅 async function getSubscriptions() { try { const data = await NODES_KV.get(KV_KEYS.SUBSCRIPTION_URLS); const subscriptions = data ? JSON.parse(data) : []; return new Response(JSON.stringify(subscriptions), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' } }); } } // 添加机场订阅 async function addSubscription(data) { try { console.log('Adding subscription:', data); // 验证输入数据 if (!data.name || !data.url) { return new Response(JSON.stringify({ success: false, error: '订阅名称和链接不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 验证URL格式 try { new URL(data.url); } catch (urlError) { return new Response(JSON.stringify({ success: false, error: '订阅链接格式不正确' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.SUBSCRIPTION_URLS); console.log('Existing subscriptions:', existingData); const subscriptions = existingData ? JSON.parse(existingData) : []; console.log('Current subscriptions count:', subscriptions.length); const newSubscription = { id: Date.now().toString(), name: data.name, url: data.url, createdAt: new Date().toISOString() }; subscriptions.push(newSubscription); console.log('New subscriptions count:', subscriptions.length); const putResult = await NODES_KV.put(KV_KEYS.SUBSCRIPTION_URLS, JSON.stringify(subscriptions)); console.log('Put result:', putResult); return new Response(JSON.stringify({ success: true, id: newSubscription.id, message: '订阅添加成功' }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Add subscription error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '添加订阅时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 删除机场订阅 async function deleteSubscription(id) { try { console.log('Deleting subscription:', id); if (!id) { return new Response(JSON.stringify({ success: false, error: '订阅ID不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.SUBSCRIPTION_URLS); const subscriptions = existingData ? JSON.parse(existingData) : []; console.log('Current subscriptions count:', subscriptions.length); const originalLength = subscriptions.length; const filteredSubscriptions = subscriptions.filter(sub => sub.id !== id); if (filteredSubscriptions.length === originalLength) { return new Response(JSON.stringify({ success: false, error: '未找到要删除的订阅' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } console.log('Filtered subscriptions count:', filteredSubscriptions.length); await NODES_KV.put(KV_KEYS.SUBSCRIPTION_URLS, JSON.stringify(filteredSubscriptions)); return new Response(JSON.stringify({ success: true, message: '订阅删除成功' }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Delete subscription error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '删除订阅时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 更新机场订阅 async function updateSubscription(id, data) { try { console.log('Updating subscription:', id, data); // 验证输入数据 if (!data.name || !data.url) { return new Response(JSON.stringify({ success: false, error: '订阅名称和链接不能为空' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 验证URL格式 try { new URL(data.url); } catch (urlError) { return new Response(JSON.stringify({ success: false, error: '订阅链接格式不正确' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const existingData = await NODES_KV.get(KV_KEYS.SUBSCRIPTION_URLS); const subscriptions = existingData ? JSON.parse(existingData) : []; console.log('Current subscriptions count:', subscriptions.length); // 查找要更新的订阅 const subscriptionIndex = subscriptions.findIndex(sub => sub.id === id); if (subscriptionIndex === -1) { return new Response(JSON.stringify({ success: false, error: '未找到要更新的订阅' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 更新订阅信息 subscriptions[subscriptionIndex].name = data.name; subscriptions[subscriptionIndex].url = data.url; subscriptions[subscriptionIndex].updatedAt = new Date().toISOString(); console.log('Updated subscription:', subscriptions[subscriptionIndex]); await NODES_KV.put(KV_KEYS.SUBSCRIPTION_URLS, JSON.stringify(subscriptions)); return new Response(JSON.stringify({ success: true, message: '订阅更新成功' }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Update subscription error:', error); return new Response(JSON.stringify({ success: false, error: error.message, details: '更新订阅时发生错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 修改后的订阅处理函数 async function handleSubscription(request, tag) { let req_data = ""; // 从KV获取自建节点 try { const customNodesData = await NODES_KV.get(KV_KEYS.CUSTOM_NODES); if (customNodesData) { const customNodes = JSON.parse(customNodesData); customNodes.forEach(node => { req_data += node.config + "\n"; }); } } catch (error) { console.error('获取自建节点失败:', error); } // 如果请求包含机场标签,获取机场订阅 if (tag === '9527abc-jichang') { try { const subscriptionsData = await NODES_KV.get(KV_KEYS.SUBSCRIPTION_URLS); if (subscriptionsData) { const subscriptions = JSON.parse(subscriptionsData); const urls = subscriptions.map(sub => sub.url); const responses = await Promise.all(urls.map(url => fetch(url))); for (const response of responses) { if (response.ok) { const content = await response.text(); req_data += atob(content); } } } } catch (error) { console.error('获取机场订阅失败:', error); } } await sendMessage("#访问信息", request.headers.get('CF-Connecting-IP'), `Tag: ${tag}`); return new Response(btoa(req_data)); } // 代码参考: async function sendMessage(type, ip, add_data = "") { const OPT = { BotToken: tgbottoken, // Telegram Bot API ChatID: tgchatid, // User 或者 ChatID,电报用户名 } let msg = ""; const response = await fetch(`http://ip-api.com/json/${ip}`); if (response.status == 200) { // 查询 IP 来源信息,使用方法参考:https://ip-api.com/docs/api:json const ipInfo = await response.json(); msg = `${type}\nIP: ${ip}\nCountry: ${ipInfo.country}\nCity: ${ipInfo.city}\n${add_data}`; } else { msg = `${type}\nIP: ${ip}\n${add_data}`; } let url = "https://api.telegram.org/"; url += "bot" + OPT.BotToken + "/sendMessage?"; url += "chat_id=" + OPT.ChatID + "&"; url += "text=" + encodeURIComponent(msg); return fetch(url, { method: 'get', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;', 'Accept-Encoding': 'gzip, deflate, br', 'User-Agent': 'Mozilla/5.0 Chrome/90.0.4430.72' } }); }