// 部署完成后在网址后面加上这个,获取自建节点和机场聚合节点,/?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="粘贴节点配置内容,支持多个节点,每行一个... 支持:VLESS、VMess、Shadowsocks、Trojan等 支持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'
}
});
}