文章详情

返回首页

CF搭建节点管理

分享文章 作者: Ws01 创建时间: 2025-11-24 📝 字数: 80,655 字 👁️ 阅读: 12 次

原始 Markdown

// 部署完成后在网址后面加上这个,获取自建节点和机场聚合节点,/?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'
    }
  });
}

预览

// 部署完成后在网址后面加上这个,获取自建节点和机场聚合节点,/?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 ENVUSERNAME = typeof USERNAME !== 'undefined' ? USERNAME : LOGINUSERNAME; const ENVPASSWORD = typeof PASSWORD !== 'undefined' ? PASSWORD : LOGINPASSWORD;

// KV存储键名 const KV_KEYS = { CUSTOMNODES: 'customnodes', SUBSCRIPTIONURLS: 'subscriptionurls', AUTHSESSIONS: 'authsessions' };

// 内存存储作为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 NODESKV !== 'undefined' && NODESKV !== fallbackStorage) { usingRealKV = true; console.log('✅ 检测到KV绑定 (方法1) - 数据将持久保存'); }

// 方法2: 检查是否有KV绑定对象 if (!usingRealKV && typeof NODESKVBINDING !== 'undefined') { NODESKV = NODESKV_BINDING; usingRealKV = true; console.log('✅ 检测到KV绑定 (方法2) - 数据将持久保存'); }

// 方法3: 尝试直接访问绑定的变量 if (!usingRealKV) { try { // 在Cloudflare Workers中,绑定的变量会直接可用 if (typeof NODESKV !== 'undefined' && NODESKV && 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('当前NODESKV类型:', typeof NODESKV); console.log('当前NODESKV值:', NODESKV); }

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 === ENVUSERNAME && password === ENVPASSWORD) { // 生成会话ID const sessionId = generateSessionId(); const sessionData = { username: username, loginTime: Date.now(), expires: Date.now() + (24 60 60 * 1000) // 24小时过期 };

// 存储会话到KV await NODESKV.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 NODESKV.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 NODESKV.get(session${sessionId});

if (!sessionData) { return { valid: false, reason: 'Session not found' }; }

const session = JSON.parse(sessionData);

// 检查会话是否过期 if (Date.now() > session.expires) { // 删除过期会话 await NODESKV.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: { hasNODESKV: typeof NODESKV !== 'undefined', isFallbackStorage: NODES_KV === fallbackStorage, hasNODESKVBINDING: typeof NODESKVBINDING !== 'undefined', NODESKVtype: typeof NODES_KV } }), { headers: { 'Content-Type': 'application/json' } }); }

// 测试KV连接 async function testKVConnection() { const testKey = 'kvtest' + Date.now(); const testValue = 'testvalue' + 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: { NODESKVtype: typeof NODES_KV, NODESKVconstructor: 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: { NODESKVtype: 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 NODESKV.get(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.put(KVKEYS.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 NODESKV.get(KVKEYS.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 NODESKV.get(KVKEYS.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' } }); }