// 部署完成后在网址后面加上这个,获取自建节点和机场聚合节点,/?token=xxoo&tag=9527abc-jichang
// 新增:仅获取机场聚合节点,/?token=xxoo&tag=9527abc-jichang-only
// 默认节点信息,聚合订阅地址: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 SUBSCRIPTION_TAGS = {
COMBINED: '9527abc-jichang', // 自建 + 机场
AIRPORT_ONLY: '9527abc-jichang-only' // 仅机场
};
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>节点管理后台 v2.0</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-pause-btn" class="btn btn-sm" onclick="batchPauseNodes()" style="background: #ffc107; color: white; margin-right: 10px;" disabled>暂停选中</button>
<button id="batch-enable-btn" class="btn btn-sm" onclick="batchEnableNodes()" style="background: #28a745; color: white; margin-right: 10px;" disabled>启用选中</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-status" style="width: 100px; text-align: center;">状态</th>
<th class="node-config">节点配置</th>
<th class="node-actions">操作</th>
</tr>
</thead>
<tbody id="custom-list">
<tr>
<td colspan="6" 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">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3>机场订阅列表</h3>
<div>
<button id="select-all-sub-btn" class="btn btn-sm" onclick="toggleSelectAllSub()" style="margin-right: 10px;">全选</button>
<button id="batch-pause-sub-btn" class="btn btn-sm" onclick="batchPauseSubscriptions()" style="background: #ffc107; color: white; margin-right: 10px;" disabled>暂停选中</button>
<button id="batch-enable-sub-btn" class="btn btn-sm" onclick="batchEnableSubscriptions()" style="background: #28a745; color: white; margin-right: 10px;" disabled>启用选中</button>
<button id="batch-delete-sub-btn" class="btn btn-danger btn-sm" onclick="batchDeleteSubscriptions()" disabled>批量删除</button>
</div>
</div>
<div class="table-container">
<table class="subscription-table">
<thead>
<tr>
<th class="sub-checkbox" style="width: 50px;">
<input type="checkbox" id="select-all-sub-checkbox" onchange="handleSelectAllSubChange()">
</th>
<th class="sub-index">序号</th>
<th class="sub-name">订阅名称</th>
<th class="sub-status" style="width: 100px; text-align: center;">状态</th>
<th class="sub-url">订阅链接</th>
<th class="sub-actions">操作</th>
</tr>
</thead>
<tbody id="subscription-list">
<tr>
<td colspan="6" 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="6" style="text-align: center; padding: 20px; color: #666;">暂无自建节点</td></tr>';
return;
}
tbody.innerHTML = data.map((node, index) => {
const isEnabled = node.enabled !== false; // 默认为启用(向后兼容)
const statusClass = isEnabled ? 'success' : 'warning';
const statusText = isEnabled ? '启用' : '暂停';
return \`
<tr style="\${!isEnabled ? 'opacity: 0.6;' : ''}">
<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-status" style="text-align: center;">
<span class="status \${statusClass}">\${statusText}</span>
</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="6" 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="6" style="text-align: center; padding: 20px; color: #666;">暂无机场订阅</td></tr>';
return;
}
tbody.innerHTML = data.map((sub, index) => {
const isEnabled = sub.enabled !== false; // 默认为启用(向后兼容)
const statusClass = isEnabled ? 'success' : 'warning';
const statusText = isEnabled ? '启用' : '暂停';
return \`
<tr style="\${!isEnabled ? 'opacity: 0.6;' : ''}">
<td class="sub-checkbox" style="text-align: center;">
<input type="checkbox" class="sub-checkbox-input" value="\${sub.id}" onchange="handleSubCheckboxChange()">
</td>
<td class="sub-index">\${index + 1}</td>
<td class="sub-name" title="\${sub.name}">\${truncateText(sub.name, 20)}</td>
<td class="sub-status" style="text-align: center;">
<span class="status \${statusClass}">\${statusText}</span>
</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('');
// 更新批量操作按钮状态
updateBatchSubButton();
} catch (error) {
console.error('Load subscriptions error:', error);
document.getElementById('subscription-list').innerHTML = '<tr><td colspan="6" 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');
const batchPauseBtn = document.getElementById('batch-pause-btn');
const batchEnableBtn = document.getElementById('batch-enable-btn');
if (selectedCheckboxes.length > 0) {
batchDeleteBtn.disabled = false;
batchDeleteBtn.textContent = '批量删除 (' + selectedCheckboxes.length + ')';
batchPauseBtn.disabled = false;
batchPauseBtn.textContent = '暂停选中 (' + selectedCheckboxes.length + ')';
batchEnableBtn.disabled = false;
batchEnableBtn.textContent = '启用选中 (' + selectedCheckboxes.length + ')';
} else {
batchDeleteBtn.disabled = true;
batchDeleteBtn.textContent = '批量删除';
batchPauseBtn.disabled = true;
batchPauseBtn.textContent = '暂停选中';
batchEnableBtn.disabled = true;
batchEnableBtn.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 batchPauseNodes() {
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-toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: selectedIds, enabled: false })
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('成功暂停 ' + result.updatedCount + ' 个节点', 'success');
loadCustomNodes(); // 重新加载节点列表
} else {
showStatus(result.error || '批量暂停失败', 'error');
console.error('Batch pause error:', result);
}
} catch (error) {
showStatus('网络错误: ' + error.message, 'error');
console.error('Network error:', error);
}
}
// 批量启用节点
async function batchEnableNodes() {
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-toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: selectedIds, enabled: true })
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('成功启用 ' + result.updatedCount + ' 个节点', 'success');
loadCustomNodes(); // 重新加载节点列表
} else {
showStatus(result.error || '批量启用失败', 'error');
console.error('Batch enable 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);
}
}
// 全选/取消全选订阅功能
function toggleSelectAllSub() {
const selectAllCheckbox = document.getElementById('select-all-sub-checkbox');
const subCheckboxes = document.querySelectorAll('.sub-checkbox-input');
// 检查是否所有订阅都被选中
const allChecked = Array.from(subCheckboxes).every(checkbox => checkbox.checked);
// 如果全部选中,则取消全选;否则全选
const shouldCheck = !allChecked;
subCheckboxes.forEach(checkbox => {
checkbox.checked = shouldCheck;
});
// 更新全选复选框状态
selectAllCheckbox.checked = shouldCheck;
selectAllCheckbox.indeterminate = false;
updateBatchSubButton();
}
// 处理全选订阅复选框变化
function handleSelectAllSubChange() {
const selectAllCheckbox = document.getElementById('select-all-sub-checkbox');
const subCheckboxes = document.querySelectorAll('.sub-checkbox-input');
// 根据全选复选框的状态来设置所有订阅复选框
const isChecked = selectAllCheckbox.checked;
subCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
});
// 清除indeterminate状态
selectAllCheckbox.indeterminate = false;
updateBatchSubButton();
}
// 处理单个订阅复选框变化
function handleSubCheckboxChange() {
const selectAllCheckbox = document.getElementById('select-all-sub-checkbox');
const subCheckboxes = document.querySelectorAll('.sub-checkbox-input');
// 检查是否所有订阅都被选中
const allChecked = Array.from(subCheckboxes).every(checkbox => checkbox.checked);
const someChecked = Array.from(subCheckboxes).some(checkbox => checkbox.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
updateBatchSubButton();
}
// 更新批量操作订阅按钮状态
function updateBatchSubButton() {
const selectedCheckboxes = document.querySelectorAll('.sub-checkbox-input:checked');
const batchDeleteBtn = document.getElementById('batch-delete-sub-btn');
const batchPauseBtn = document.getElementById('batch-pause-sub-btn');
const batchEnableBtn = document.getElementById('batch-enable-sub-btn');
if (selectedCheckboxes.length > 0) {
batchDeleteBtn.disabled = false;
batchDeleteBtn.textContent = '批量删除 (' + selectedCheckboxes.length + ')';
batchPauseBtn.disabled = false;
batchPauseBtn.textContent = '暂停选中 (' + selectedCheckboxes.length + ')';
batchEnableBtn.disabled = false;
batchEnableBtn.textContent = '启用选中 (' + selectedCheckboxes.length + ')';
} else {
batchDeleteBtn.disabled = true;
batchDeleteBtn.textContent = '批量删除';
batchPauseBtn.disabled = true;
batchPauseBtn.textContent = '暂停选中';
batchEnableBtn.disabled = true;
batchEnableBtn.textContent = '启用选中';
}
}
// 批量删除订阅
async function batchDeleteSubscriptions() {
const selectedCheckboxes = document.querySelectorAll('.sub-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/subscriptions/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');
loadSubscriptions(); // 重新加载订阅列表
} else {
showStatus(result.error || '批量删除失败', 'error');
console.error('Batch delete subscriptions error:', result);
}
} catch (error) {
showStatus('网络错误: ' + error.message, 'error');
console.error('Network error:', error);
}
}
// 批量暂停订阅
async function batchPauseSubscriptions() {
const selectedCheckboxes = document.querySelectorAll('.sub-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/subscriptions/batch-toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: selectedIds, enabled: false })
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('成功暂停 ' + result.updatedCount + ' 个订阅', 'success');
loadSubscriptions(); // 重新加载订阅列表
} else {
showStatus(result.error || '批量暂停失败', 'error');
console.error('Batch pause subscriptions error:', result);
}
} catch (error) {
showStatus('网络错误: ' + error.message, 'error');
console.error('Network error:', error);
}
}
// 批量启用订阅
async function batchEnableSubscriptions() {
const selectedCheckboxes = document.querySelectorAll('.sub-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/subscriptions/batch-toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: selectedIds, enabled: true })
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('成功启用 ' + result.updatedCount + ' 个订阅', 'success');
loadSubscriptions(); // 重新加载订阅列表
} else {
showStatus(result.error || '批量启用失败', 'error');
console.error('Batch enable subscriptions 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/custom-nodes/batch-toggle' && method === 'POST') {
const data = await request.json();
return batchToggleCustomNodes(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/subscriptions/batch-delete' && method === 'POST') {
const data = await request.json();
return batchDeleteSubscriptions(data);
}
// 批量暂停/启用机场订阅API
if (pathname === '/api/subscriptions/batch-toggle' && method === 'POST') {
const data = await request.json();
return batchToggleSubscriptions(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,
enabled: true, // 默认启用
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 batchToggleCustomNodes(data) {
try {
console.log('Batch toggling 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' }
});
}
if (data.enabled === undefined) {
return new Response(JSON.stringify({
success: false,
error: '必须指定enabled状态(true或false)'
}), {
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 toggle:', data.ids);
console.log('New enabled state:', data.enabled);
let updatedCount = 0;
// 更新选中节点的enabled状态
nodes.forEach(node => {
if (data.ids.includes(node.id)) {
node.enabled = data.enabled;
node.updatedAt = new Date().toISOString();
updatedCount++;
}
});
console.log('Updated count:', updatedCount);
if (updatedCount === 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(nodes));
const action = data.enabled ? '启用' : '暂停';
return new Response(JSON.stringify({
success: true,
updatedCount: updatedCount,
message: `成功${action} ${updatedCount} 个节点`
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Batch toggle 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,
enabled: true, // 默认启用
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 batchDeleteSubscriptions(data) {
try {
console.log('Batch deleting subscriptions:', 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.SUBSCRIPTION_URLS);
const subscriptions = existingData ? JSON.parse(existingData) : [];
console.log('Current subscriptions count:', subscriptions.length);
console.log('Subscriptions to delete:', data.ids);
const originalLength = subscriptions.length;
const filteredSubscriptions = subscriptions.filter(sub => !data.ids.includes(sub.id));
const deletedCount = originalLength - filteredSubscriptions.length;
console.log('Filtered subscriptions count:', filteredSubscriptions.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.SUBSCRIPTION_URLS, JSON.stringify(filteredSubscriptions));
return new Response(JSON.stringify({
success: true,
deletedCount: deletedCount,
message: `成功删除 ${deletedCount} 个订阅`
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Batch delete subscriptions error:', error);
return new Response(JSON.stringify({
success: false,
error: error.message,
details: '批量删除订阅时发生错误'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
// 批量暂停/启用机场订阅
async function batchToggleSubscriptions(data) {
try {
console.log('Batch toggling subscriptions:', 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' }
});
}
if (data.enabled === undefined) {
return new Response(JSON.stringify({
success: false,
error: '必须指定enabled状态(true或false)'
}), {
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);
console.log('Subscriptions to toggle:', data.ids);
console.log('New enabled state:', data.enabled);
let updatedCount = 0;
// 更新选中订阅的enabled状态
subscriptions.forEach(sub => {
if (data.ids.includes(sub.id)) {
sub.enabled = data.enabled;
sub.updatedAt = new Date().toISOString();
updatedCount++;
}
});
console.log('Updated count:', updatedCount);
if (updatedCount === 0) {
return new Response(JSON.stringify({
success: false,
error: '未找到要更新的订阅'
}), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
await NODES_KV.put(KV_KEYS.SUBSCRIPTION_URLS, JSON.stringify(subscriptions));
const action = data.enabled ? '启用' : '暂停';
return new Response(JSON.stringify({
success: true,
updatedCount: updatedCount,
message: `成功${action} ${updatedCount} 个订阅`
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Batch toggle subscriptions 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 = "";
const includeCustomNodes = tag !== SUBSCRIPTION_TAGS.AIRPORT_ONLY;
const includeSubscriptions = tag === SUBSCRIPTION_TAGS.COMBINED || tag === SUBSCRIPTION_TAGS.AIRPORT_ONLY;
if (includeCustomNodes) {
try {
const customNodesData = await NODES_KV.get(KV_KEYS.CUSTOM_NODES);
if (customNodesData) {
const customNodes = JSON.parse(customNodesData);
// 只输出启用的节点(enabled为true或未定义,保持向后兼容)
customNodes.forEach(node => {
if (node.enabled !== false) {
req_data += node.config + "\n";
}
});
}
} catch (error) {
console.error('获取自建节点失败:', error);
}
}
if (includeSubscriptions) {
try {
const subscriptionsData = await NODES_KV.get(KV_KEYS.SUBSCRIPTION_URLS);
if (subscriptionsData) {
const subscriptions = JSON.parse(subscriptionsData);
// 只获取启用的订阅(enabled为true或未定义,保持向后兼容)
const enabledSubscriptions = subscriptions.filter(sub => sub.enabled !== false);
const urls = enabledSubscriptions.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 // 新增:仅获取机场聚合节点,/?token=xxoo&tag=9527abc-jichang-only // 默认节点信息,聚合订阅地址: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 SUBSCRIPTION_TAGS = { COMBINED: '9527abc-jichang', // 自建 + 机场 AIRPORT_ONLY: '9527abc-jichang-only' // 仅机场 }; 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>节点管理后台 v2.0</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-pause-btn" class="btn btn-sm" onclick="batchPauseNodes()" style="background: #ffc107; color: white; margin-right: 10px;" disabled>暂停选中</button> <button id="batch-enable-btn" class="btn btn-sm" onclick="batchEnableNodes()" style="background: #28a745; color: white; margin-right: 10px;" disabled>启用选中</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-status" style="width: 100px; text-align: center;">状态</th> <th class="node-config">节点配置</th> <th class="node-actions">操作</th> </tr> </thead> <tbody id="custom-list"> <tr> <td colspan="6" 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"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> <h3>机场订阅列表</h3> <div> <button id="select-all-sub-btn" class="btn btn-sm" onclick="toggleSelectAllSub()" style="margin-right: 10px;">全选</button> <button id="batch-pause-sub-btn" class="btn btn-sm" onclick="batchPauseSubscriptions()" style="background: #ffc107; color: white; margin-right: 10px;" disabled>暂停选中</button> <button id="batch-enable-sub-btn" class="btn btn-sm" onclick="batchEnableSubscriptions()" style="background: #28a745; color: white; margin-right: 10px;" disabled>启用选中</button> <button id="batch-delete-sub-btn" class="btn btn-danger btn-sm" onclick="batchDeleteSubscriptions()" disabled>批量删除</button> </div> </div> <div class="table-container"> <table class="subscription-table"> <thead> <tr> <th class="sub-checkbox" style="width: 50px;"> <input type="checkbox" id="select-all-sub-checkbox" onchange="handleSelectAllSubChange()"> </th> <th class="sub-index">序号</th> <th class="sub-name">订阅名称</th> <th class="sub-status" style="width: 100px; text-align: center;">状态</th> <th class="sub-url">订阅链接</th> <th class="sub-actions">操作</th> </tr> </thead> <tbody id="subscription-list"> <tr> <td colspan="6" 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="6" style="text-align: center; padding: 20px; color: #666;">暂无自建节点</td></tr>'; return; }
tbody.innerHTML = data.map((node, index) => {
const isEnabled = node.enabled !== false; // 默认为启用(向后兼容)
const statusClass = isEnabled ? 'success' : 'warning';
const statusText = isEnabled ? '启用' : '暂停';
return \
<tr style="\${!isEnabled ? 'opacity: 0.6;' : ''}">
<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-status" style="text-align: center;">
<span class="status \${statusClass}">\${statusText}</span>
</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="6" 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="6" style="text-align: center; padding: 20px; color: #666;">暂无机场订阅</td></tr>'; return; }
tbody.innerHTML = data.map((sub, index) => {
const isEnabled = sub.enabled !== false; // 默认为启用(向后兼容)
const statusClass = isEnabled ? 'success' : 'warning';
const statusText = isEnabled ? '启用' : '暂停';
return \
<tr style="\${!isEnabled ? 'opacity: 0.6;' : ''}">
<td class="sub-checkbox" style="text-align: center;">
<input type="checkbox" class="sub-checkbox-input" value="\${sub.id}" onchange="handleSubCheckboxChange()">
</td>
<td class="sub-index">\${index + 1}</td>
<td class="sub-name" title="\${sub.name}">\${truncateText(sub.name, 20)}</td>
<td class="sub-status" style="text-align: center;">
<span class="status \${statusClass}">\${statusText}</span>
</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('');
// 更新批量操作按钮状态 updateBatchSubButton(); } catch (error) { console.error('Load subscriptions error:', error); document.getElementById('subscription-list').innerHTML = '<tr><td colspan="6" 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'); const batchPauseBtn = document.getElementById('batch-pause-btn'); const batchEnableBtn = document.getElementById('batch-enable-btn');
if (selectedCheckboxes.length > 0) { batchDeleteBtn.disabled = false; batchDeleteBtn.textContent = '批量删除 (' + selectedCheckboxes.length + ')'; batchPauseBtn.disabled = false; batchPauseBtn.textContent = '暂停选中 (' + selectedCheckboxes.length + ')'; batchEnableBtn.disabled = false; batchEnableBtn.textContent = '启用选中 (' + selectedCheckboxes.length + ')'; } else { batchDeleteBtn.disabled = true; batchDeleteBtn.textContent = '批量删除'; batchPauseBtn.disabled = true; batchPauseBtn.textContent = '暂停选中'; batchEnableBtn.disabled = true; batchEnableBtn.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 batchPauseNodes() { 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-toggle', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: selectedIds, enabled: false }) });
const result = await response.json();
if (response.ok && result.success) { showStatus('成功暂停 ' + result.updatedCount + ' 个节点', 'success'); loadCustomNodes(); // 重新加载节点列表 } else { showStatus(result.error || '批量暂停失败', 'error'); console.error('Batch pause error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } }
// 批量启用节点 async function batchEnableNodes() { 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-toggle', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: selectedIds, enabled: true }) });
const result = await response.json();
if (response.ok && result.success) { showStatus('成功启用 ' + result.updatedCount + ' 个节点', 'success'); loadCustomNodes(); // 重新加载节点列表 } else { showStatus(result.error || '批量启用失败', 'error'); console.error('Batch enable 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); } }
// 全选/取消全选订阅功能 function toggleSelectAllSub() { const selectAllCheckbox = document.getElementById('select-all-sub-checkbox'); const subCheckboxes = document.querySelectorAll('.sub-checkbox-input');
// 检查是否所有订阅都被选中 const allChecked = Array.from(subCheckboxes).every(checkbox => checkbox.checked);
// 如果全部选中,则取消全选;否则全选 const shouldCheck = !allChecked;
subCheckboxes.forEach(checkbox => { checkbox.checked = shouldCheck; });
// 更新全选复选框状态 selectAllCheckbox.checked = shouldCheck; selectAllCheckbox.indeterminate = false;
updateBatchSubButton(); }
// 处理全选订阅复选框变化 function handleSelectAllSubChange() { const selectAllCheckbox = document.getElementById('select-all-sub-checkbox'); const subCheckboxes = document.querySelectorAll('.sub-checkbox-input');
// 根据全选复选框的状态来设置所有订阅复选框 const isChecked = selectAllCheckbox.checked; subCheckboxes.forEach(checkbox => { checkbox.checked = isChecked; });
// 清除indeterminate状态 selectAllCheckbox.indeterminate = false;
updateBatchSubButton(); }
// 处理单个订阅复选框变化 function handleSubCheckboxChange() { const selectAllCheckbox = document.getElementById('select-all-sub-checkbox'); const subCheckboxes = document.querySelectorAll('.sub-checkbox-input');
// 检查是否所有订阅都被选中 const allChecked = Array.from(subCheckboxes).every(checkbox => checkbox.checked); const someChecked = Array.from(subCheckboxes).some(checkbox => checkbox.checked);
selectAllCheckbox.checked = allChecked; selectAllCheckbox.indeterminate = someChecked && !allChecked;
updateBatchSubButton(); }
// 更新批量操作订阅按钮状态 function updateBatchSubButton() { const selectedCheckboxes = document.querySelectorAll('.sub-checkbox-input:checked'); const batchDeleteBtn = document.getElementById('batch-delete-sub-btn'); const batchPauseBtn = document.getElementById('batch-pause-sub-btn'); const batchEnableBtn = document.getElementById('batch-enable-sub-btn');
if (selectedCheckboxes.length > 0) { batchDeleteBtn.disabled = false; batchDeleteBtn.textContent = '批量删除 (' + selectedCheckboxes.length + ')'; batchPauseBtn.disabled = false; batchPauseBtn.textContent = '暂停选中 (' + selectedCheckboxes.length + ')'; batchEnableBtn.disabled = false; batchEnableBtn.textContent = '启用选中 (' + selectedCheckboxes.length + ')'; } else { batchDeleteBtn.disabled = true; batchDeleteBtn.textContent = '批量删除'; batchPauseBtn.disabled = true; batchPauseBtn.textContent = '暂停选中'; batchEnableBtn.disabled = true; batchEnableBtn.textContent = '启用选中'; } }
// 批量删除订阅 async function batchDeleteSubscriptions() { const selectedCheckboxes = document.querySelectorAll('.sub-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/subscriptions/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'); loadSubscriptions(); // 重新加载订阅列表 } else { showStatus(result.error || '批量删除失败', 'error'); console.error('Batch delete subscriptions error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } }
// 批量暂停订阅 async function batchPauseSubscriptions() { const selectedCheckboxes = document.querySelectorAll('.sub-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/subscriptions/batch-toggle', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: selectedIds, enabled: false }) });
const result = await response.json();
if (response.ok && result.success) { showStatus('成功暂停 ' + result.updatedCount + ' 个订阅', 'success'); loadSubscriptions(); // 重新加载订阅列表 } else { showStatus(result.error || '批量暂停失败', 'error'); console.error('Batch pause subscriptions error:', result); } } catch (error) { showStatus('网络错误: ' + error.message, 'error'); console.error('Network error:', error); } }
// 批量启用订阅 async function batchEnableSubscriptions() { const selectedCheckboxes = document.querySelectorAll('.sub-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/subscriptions/batch-toggle', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: selectedIds, enabled: true }) });
const result = await response.json();
if (response.ok && result.success) { showStatus('成功启用 ' + result.updatedCount + ' 个订阅', 'success'); loadSubscriptions(); // 重新加载订阅列表 } else { showStatus(result.error || '批量启用失败', 'error'); console.error('Batch enable subscriptions 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/custom-nodes/batch-toggle' && method === 'POST') { const data = await request.json(); return batchToggleCustomNodes(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/subscriptions/batch-delete' && method === 'POST') { const data = await request.json(); return batchDeleteSubscriptions(data); }
// 批量暂停/启用机场订阅API if (pathname === '/api/subscriptions/batch-toggle' && method === 'POST') { const data = await request.json(); return batchToggleSubscriptions(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, enabled: true, // 默认启用 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 batchToggleCustomNodes(data) { try { console.log('Batch toggling 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' } }); }
if (data.enabled === undefined) { return new Response(JSON.stringify({ success: false, error: '必须指定enabled状态(true或false)' }), { 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 toggle:', data.ids); console.log('New enabled state:', data.enabled);
let updatedCount = 0;
// 更新选中节点的enabled状态 nodes.forEach(node => { if (data.ids.includes(node.id)) { node.enabled = data.enabled; node.updatedAt = new Date().toISOString(); updatedCount++; } });
console.log('Updated count:', updatedCount);
if (updatedCount === 0) { return new Response(JSON.stringify({ success: false, error: '未找到要更新的节点' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); }
await NODESKV.put(KVKEYS.CUSTOM_NODES, JSON.stringify(nodes));
const action = data.enabled ? '启用' : '暂停';
return new Response(JSON.stringify({
success: true,
updatedCount: updatedCount,
message: 成功${action} ${updatedCount} 个节点
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Batch toggle 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, enabled: true, // 默认启用 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 batchDeleteSubscriptions(data) { try { console.log('Batch deleting subscriptions:', 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.SUBSCRIPTION_URLS); const subscriptions = existingData ? JSON.parse(existingData) : [];
console.log('Current subscriptions count:', subscriptions.length); console.log('Subscriptions to delete:', data.ids);
const originalLength = subscriptions.length; const filteredSubscriptions = subscriptions.filter(sub => !data.ids.includes(sub.id)); const deletedCount = originalLength - filteredSubscriptions.length;
console.log('Filtered subscriptions count:', filteredSubscriptions.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.SUBSCRIPTION_URLS, JSON.stringify(filteredSubscriptions));
return new Response(JSON.stringify({
success: true,
deletedCount: deletedCount,
message: 成功删除 ${deletedCount} 个订阅
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Batch delete subscriptions error:', error);
return new Response(JSON.stringify({
success: false,
error: error.message,
details: '批量删除订阅时发生错误'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
// 批量暂停/启用机场订阅 async function batchToggleSubscriptions(data) { try { console.log('Batch toggling subscriptions:', 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' } }); }
if (data.enabled === undefined) { return new Response(JSON.stringify({ success: false, error: '必须指定enabled状态(true或false)' }), { 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); console.log('Subscriptions to toggle:', data.ids); console.log('New enabled state:', data.enabled);
let updatedCount = 0;
// 更新选中订阅的enabled状态 subscriptions.forEach(sub => { if (data.ids.includes(sub.id)) { sub.enabled = data.enabled; sub.updatedAt = new Date().toISOString(); updatedCount++; } });
console.log('Updated count:', updatedCount);
if (updatedCount === 0) { return new Response(JSON.stringify({ success: false, error: '未找到要更新的订阅' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); }
await NODESKV.put(KVKEYS.SUBSCRIPTION_URLS, JSON.stringify(subscriptions));
const action = data.enabled ? '启用' : '暂停';
return new Response(JSON.stringify({
success: true,
updatedCount: updatedCount,
message: 成功${action} ${updatedCount} 个订阅
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Batch toggle subscriptions 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 = "";
const includeCustomNodes = tag !== SUBSCRIPTIONTAGS.AIRPORTONLY; const includeSubscriptions = tag === SUBSCRIPTIONTAGS.COMBINED || tag === SUBSCRIPTIONTAGS.AIRPORT_ONLY;
if (includeCustomNodes) { try { const customNodesData = await NODESKV.get(KVKEYS.CUSTOM_NODES); if (customNodesData) { const customNodes = JSON.parse(customNodesData); // 只输出启用的节点(enabled为true或未定义,保持向后兼容) customNodes.forEach(node => { if (node.enabled !== false) { req_data += node.config + "\n"; } }); } } catch (error) { console.error('获取自建节点失败:', error); } }
if (includeSubscriptions) { try { const subscriptionsData = await NODESKV.get(KVKEYS.SUBSCRIPTION_URLS); if (subscriptionsData) { const subscriptions = JSON.parse(subscriptionsData); // 只获取启用的订阅(enabled为true或未定义,保持向后兼容) const enabledSubscriptions = subscriptions.filter(sub => sub.enabled !== false); const urls = enabledSubscriptions.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' } }); }