文章详情

返回首页

CF上搭建免费探针【增强版】

分享文章 作者: Ws01 创建时间: 2026-04-13 📝 字数: 64,803 字 👁️ 阅读: 4 次

CF上搭建免费探针【增强版】

github项目

基于 Cloudflare Workers 和 D1 数据库构建的轻量级、零成本、高定制化的服务器探针大盘。 完美复刻了商业级探针(如 Nezha)的核心体验,但无需额外部署任何服务端 VPS!完全白嫖 Cloudflare 的免费 Serverless 资源。
✨ 核心特性
🎨 极致的视觉与个性化体验
5 大精美主题一键切换:内置默认清爽白、暗黑极客、新粗野主义、动态毛玻璃、赛博朋克 5 种完全不同的 UI 风格。
自定义背景图与全透明模式:支持在后台直接上传本地图片(自动转为 Base64)或填写图片 URL。开启背景图后,所有卡片自动化身为绝美的“半透明毛玻璃”质感。
国旗智能匹配:依托 Cloudflare 全球网络,自动识别 VPS 归属地并渲染超清图片国旗。
📊 专业级监控与大盘展示
全局顶栏大盘:直观展示服务器总数、在线/离线数、总计流量(入/出)以及全网实时网速。
硬核双栈检测:自动探测并高亮打标 VPS 的 IPv4 与 IPv6 网络连通性。
商业级自定义徽章:支持为每台机器单独设置价格、到期时间(自动计算剩余天数)、带宽上限、流量配额,并在前台以彩色徽章展示。
精细化分组:支持在后台为服务器设置组别,前台大盘将自动按分组进行优雅排版。
实时详情图表:点击任意节点卡片,即可查看基于 Chart.js 的 CPU、内存、磁盘、进程数、TCP/UDP 连接数及双向网速的实时跳动折线图。
🛡️ 隐私与安全控制
一键私密模式:吃灰神机不想公开?在后台取消勾选“公开访问”,前台访客必须输入 admin 及密钥方可查看你的专属大盘。
模块化展示开关:价格、到期时间、带宽、流量等敏感信息,可在后台一键控制是否在前台显示。
🚀 极简部署与高精度采集
底层精准算法:抛弃传统不稳定的 top 命令,采用 Linux 内核级 /proc/stat 计算 CPU 时钟差值,数据跳动精准顺滑。
傻瓜式一键安装:后台自动生成被控端 Bash 一键安装命令,自动注册 Systemd 守护进程。

🛠️ 部署指南

第一步:创建 Cloudflare D1 数据库

进入该数据库的 Console (控制台),执行以下 SQL 语句来初始化表结构:
第一个-- 创建服务器节点表

CREATE TABLE servers ( id TEXT PRIMARY KEY, name TEXT, cpu TEXT, ram TEXT, disk TEXT, loadavg TEXT, uptime TEXT, lastupdated INTEGER, ramtotal TEXT, netrx TEXT, nettx TEXT, netinspeed TEXT, netoutspeed TEXT, os TEXT, cpuinfo TEXT, arch TEXT, boottime TEXT, ramused TEXT, swaptotal TEXT, swapused TEXT, disktotal TEXT, diskused TEXT, processes TEXT, tcpconn TEXT, udpconn TEXT, country TEXT, ipv4 TEXT, ipv6 TEXT, servergroup TEXT DEFAULT '默认分组', price TEXT DEFAULT '', expiredate TEXT DEFAULT '', bandwidth TEXT DEFAULT '', traffic_limit TEXT DEFAULT '' );

第二个-- 创建全局设置表

CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT );

第三-- 添加节点显示或隐藏

ALTER TABLE servers ADD COLUMN frontend_hidden TEXT DEFAULT 'false';
第二步:创建并配置 Cloudflare Worker
// ==========================================

// 50台VPS版本 v1.2
// ==========================================
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const host = url.origin;

const formatBytes = (bytes) => {
const b = parseInt(bytes);
if (isNaN(b) || b === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(b) / Math.log(k));
return parseFloat((b / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};

const getFlagEmoji = (countryCode) => {
if (!countryCode || countryCode === 'XX') return '🏳️';
return String.fromCodePoint(...countryCode.toUpperCase().split('').map(char => 127397 + char.charCodeAt()));
};

// ==========================================
// 0. 认证机制与全局设置加载
// ==========================================
const checkAuth = (req) => {
const authHeader = req.headers.get('Authorization');
if (!authHeader) return false;
const [scheme, encoded] = authHeader.split(' ');
if (scheme !== 'Basic' || !encoded) return false;
const decoded = atob(encoded);
const [username, password] = decoded.split(':');
return username === 'admin888' && password === env.API_SECRET;
};

const authResponse = (realmTitle) => new Response('Unauthorized', {
status: 401,
headers: { 'WWW-Authenticate': Basic realm="${realmTitle}" }
});

let sys = {
site_title: '⚡ Server Monitor Pro',
admin_title: '⚙️ 管理后台',
theme: 'theme1',
custom_bg: '',
is_public: 'true',
show_price: 'true',
show_expire: 'true',
show_bw: 'true',
show_tf: 'true',
tg_notify: 'false',
tgbottoken: '',
tgchatid: ''
};

try {
const { results } = await env.DB.prepare('SELECT * FROM settings').all();
if (results && results.length > 0) {
results.forEach(r => sys[r.key] = r.value);
}
} catch (e) {}

// ==========================================
// Telegram 离线检测与通知机制
// ==========================================
const sendTelegram = async (msg) => {
if (sys.tgnotify !== 'true' || !sys.tgbottoken || !sys.tgchat_id) return;
try {
await fetch(https://api.telegram.org/bot${sys.tgbottoken}/sendMessage, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chatid: sys.tgchatid, text: msg, parsemode: 'HTML' })
});
} catch (e) {}
};

const checkOfflineNodes = async () => {
if (sys.tg_notify !== 'true') return;
try {
const { results: allServers } = await env.DB.prepare('SELECT id, name, last_updated FROM servers').all();
let alertState = {};
const stateRes = await env.DB.prepare("SELECT value FROM settings WHERE key = 'alert_state'").first();
if (stateRes) alertState = JSON.parse(stateRes.value);

let stateChanged = false;
const now = Date.now();

for (const s of allServers) {
const diff = now - s.last_updated;
// [优化] 超过 180 秒未更新视为离线,防止网络波动误报
const isOffline = diff > 180000;

if (isOffline && !alertState[s.id]) {
await sendTelegram(⚠️ <b>节点离线告警</b>\n\n<b>节点名称:</b> ${s.name}\n<b>状态:</b> 离线 (超过3分钟未上报)\n<b>时间:</b> ${new Date().toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai'})});
alertState[s.id] = true;
stateChanged = true;
} else if (!isOffline && alertState[s.id]) {
await sendTelegram(✅ <b>节点恢复通知</b>\n\n<b>节点名称:</b> ${s.name}\n<b>状态:</b> 恢复在线\n<b>时间:</b> ${new Date().toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai'})});
delete alertState[s.id];
stateChanged = true;
}
}

if (stateChanged) {
await env.DB.prepare('INSERT INTO settings (key, value) VALUES ("alert_state", ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').bind(JSON.stringify(alertState)).run();
}
} catch (e) {}
};

// GitHub 底部版权 HTML
const footerHtml = `
<div style="text-align: center; margin-top: 40px; padding-bottom: 20px; font-size: 15px; color: #21262d; opacity: 0.8;">
<span id="timeDate">载入天数...</span>
<span <p> | 总访问量 <span id="busuanzisitepv"></span> 次 | <a href="https://boke.199881.xyz/" target="blank"> <span style="color: blue;">博客 | <a href="https://www.199881.xyz/" target="blank"> <span style="color: green;">导航 | <a href="/admin" >${sys.admintitle} v1.2</a> | <a href="https://github.com/a63414262/CF-Server-Monitor-Pro" target="blank" style="color: green; text-decoration: none; font-weight: 600;">github</a></p></span>

<script language="javascript">
var now = new Date();
function createtime(){
var grt= new Date("04/07/2026 00:00:00");/---这里是网站的启用时间:月/日/年--/
now.setTime(now.getTime()+250);
days = (now - grt ) / 1000 / 60 / 60 / 24;
dnum = Math.floor(days);
document.getElementById("timeDate").innerHTML = "稳定运行 "+dnum+" 天";
}
setInterval("createtime()",250);
</script>
<script defer src="https://bsz.211119.xyz/js"></script>

</div>
`;

// 【核心】5种主题差异化 CSS 及 自定义背景透明 CSS
const themeStyles = `
body.theme2 { background-color: #0d1117; color: #c9d1d9; }
.theme2 .vps-card, .theme2 .global-stats, .theme2 .header-card, .theme2 .chart-card { background: #161b22; color: #c9d1d9; box-shadow: 0 4px 6px rgba(0,0,0,0.4); border: 1px solid #30363d; }
.theme2 .vps-card:hover { border-color: #8b949e; }
.theme2 .group-header { color: #58a6ff; border-left-color: #58a6ff; }
.theme2 .stat-val, .theme2 .g-val { color: #fff; }
.theme2 .stat-label, .theme2 .g-label, .theme2 .g-sub, .theme2 .card-meta { color: #8b949e; }
.theme2 .stat-bar { background: #21262d; }
.theme2 .divider { background: #30363d; }
.theme2 .card-title { color: #fff; }

body.theme3 { background-color: #fef08a; color: #000; font-weight: 500; }
.theme3 .vps-card, .theme3 .global-stats, .theme3 .header-card, .theme3 .chart-card { background: #fff; border: 3px solid #000; border-radius: 0; box-shadow: 6px 6px 0px #000; transition: transform 0.1s, box-shadow 0.1s; }
.theme3 .vps-card:hover { transform: translate(2px, 2px); box-shadow: 4px 4px 0px #000; border-color: #000; }
.theme3 .group-header { color: #000; border-left: none; border-bottom: 4px solid #000; padding-left: 0; display: inline-block; font-size: 22px; font-weight: 600; text-transform: uppercase; }
.theme3 .stat-bar { background: #e5e5e5; border: 1px solid #000; }
.theme3 .stat-bar > div { border-right: 1px solid #000; }
.theme3 .badge { border: 1px solid #000; border-radius: 0; }
.theme3 .stat-val, .theme3 .g-val, .theme3 .card-title { font-weight: 600; color: #000; }

body.theme4 { background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%); background-attachment: fixed; color: #fff; }
.theme4 .vps-card, .theme4 .global-stats, .theme4 .header-card, .theme4 .chart-card { background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.4); box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); color: #fff; }
.theme4 .vps-card:hover { background: rgba(255, 255, 255, 0.3); border-color: rgba(255, 255, 255, 0.8); }
.theme4 .group-header { color: #fff; border-left-color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.theme4 .stat-val, .theme4 .g-val, .theme4 .card-title { color: #fff; }
.theme4 .stat-label, .theme4 .g-label, .theme4 .g-sub, .theme4 .card-meta { color: rgba(255,255,255,0.8); }
.theme4 .stat-bar { background: rgba(0,0,0,0.2); }
.theme4 .divider { background: rgba(255,255,255,0.2); }

body.theme5 { background-color: #050505; color: #0ff; font-family: 'Courier New', Courier, monospace; }
.theme5 .vps-card, .theme5 .global-stats, .theme5 .header-card, .theme5 .chart-card { background: #0b0c10; border: 1px solid #f0f; border-radius: 0; box-shadow: 0 0 10px rgba(255, 0, 255, 0.2); color: #fff; }
.theme5 .vps-card:hover { box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); border-color: #0ff; }
.theme5 .group-header { color: #f0f; border-left: 5px solid #0ff; text-shadow: 0 0 5px #f0f; }
.theme5 .stat-val, .theme5 .g-val, .theme5 .card-title { color: #0ff; text-shadow: 0 0 5px #0ff; }
.theme5 .stat-label, .theme5 .g-label, .theme5 .g-sub, .theme5 .card-meta { color: #f0f; }
.theme5 .stat-bar { background: #222; }
.theme5 .stat-bar > div { background: #0ff !important; box-shadow: 0 0 10px #0ff; }
.theme5 .divider { background: #333; }
.theme5 .badge-bw { background: #f0f; box-shadow: 0 0 5px #f0f; }
.theme5 .badge-tf { background: #0ff; color:#000; box-shadow: 0 0 5px #0ff; }

${sys.custom_bg ? `
body {
background: url('${sys.custom_bg}') no-repeat center center fixed !important;
background-size: cover !important;
}
.vps-card, .global-stats, .header-card, .chart-card {
background: rgba(255, 255, 255, 0.4) !important;
backdrop-filter: blur(12px) !important;
-webkit-backdrop-filter: blur(12px) !important;
border: 1px solid rgba(255, 255, 255, 0.6) !important;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1) !important;
color: #111 !important;
}
.vps-card:hover { background: rgba(255, 255, 255, 0.6) !important; transform: translateY(-3px); }
.group-header { color: #fff !important; text-shadow: 0 2px 5px rgba(0,0,0,0.6) !important; border-left-color: #fff !important; }
.stat-val, .g-val, .card-title { color: #000 !important; font-weight: 550 !important; }
.stat-label, .g-label, .g-sub, .card-meta { color: #333 !important; font-weight: 500 !important; }
.stat-bar { background: rgba(0,0,0,0.1) !important; }
` : ''}
`;

// ==========================================
// 1. 后台管理 API
// ==========================================
if (request.method === 'POST' && url.pathname === '/admin/api') {
if (!checkAuth(request)) return authResponse(sys.admin_title);
try {
const data = await request.json();

if (data.action === 'save_settings') {
for (const [k, v] of Object.entries(data.settings)) {
await env.DB.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').bind(k, v).run();
}
return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } });
}
else if (data.action === 'add') {
const id = crypto.randomUUID();
const name = data.name || 'New Server';
await env.DB.prepare(`
INSERT INTO servers
(id, name, cpu, ram, disk, loadavg, uptime, lastupdated, ramtotal, netrx, nettx, netinspeed, netoutspeed, os, cpuinfo, country, servergroup, price, expiredate, bandwidth, trafficlimit, ipv4, ipv6, frontendhidden)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(id, name, '0', '0', '0', '0', '0', 0, '0', '0', '0', '0', '0', '', '', '', '默认分组', '免费', '', '', '', '0', '0', 'false').run();
return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } });
}
else if (data.action === 'delete') {
await env.DB.prepare('DELETE FROM servers WHERE id = ?').bind(data.id).run();
return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } });
}
else if (data.action === 'edit') {
const fh = data.frontendhidden === true || data.frontendhidden === 'true' ? 'true' : 'false';
await env.DB.prepare(`
UPDATE servers SET name = COALESCE(NULLIF(TRIM(?), ''), name), servergroup = ?, price = ?, expiredate = ?, bandwidth = ?, trafficlimit = ?, frontendhidden = ? WHERE id = ?
`).bind(data.name != null ? String(data.name) : '', data.servergroup || '默认分组', data.price || '', data.expiredate || '', data.bandwidth || '', data.traffic_limit || '', fh, data.id).run();
return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } });
}
} catch (e) {
return new Response(JSON.stringify({ error: e.message }), { status: 400 });
}
}

// ==========================================
// 2. 后台管理 UI (/admin)
// ==========================================
if (request.method === 'GET' && url.pathname === '/admin') {
if (!checkAuth(request)) return authResponse(sys.admin_title);

const { results } = await env.DB.prepare('SELECT id, name, lastupdated, servergroup, price, expiredate, bandwidth, trafficlimit, frontend_hidden FROM servers').all();
const now = Date.now();

let trs = '';
if (results && results.length > 0) {
const shellQuote = (v) => '${String(v).replace(/'/g, '\"'\"')}';
const escAttr = (v) => String(v ?? '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;');
for (const s of results) {
// [优化] 后台判定离线放宽至 90 秒
const isOnline = (now - s.last_updated) < 90000;
const status = isOnline ? '<span style="color:green; font-weight:bold;">在线</span>' : '<span style="color:red; font-weight:bold;">离线</span>';
const cmdApp = "cur" + "l";
const cmd = ${cmdApp} -sL ${host}/install.sh | sh -s -- ${shellQuote(s.id)} ${shellQuote(env.API_SECRET)};

trs += `
<tr>
<td>${s.name}${s.frontend_hidden === 'true' ? ' <span style="color:#999;font-size:12px;">(前台已隐藏)</span>' : ''}</td>
<td>${s.server_group || '默认分组'}</td>
<td>${status}</td>
<td>
<input type="text" readonly value="${cmd}" style="width:280px; padding:6px; margin-right:5px; border:1px solid #ccc; border-radius:4px;" id="cmd-${s.id}">
<button onclick="copyCmd('${s.id}')" class="btn btn-green">复制命令</button>
<button type="button" class="btn btn-blue js-edit-server"
data-edit-id="${escAttr(s.id)}"
data-edit-name="${escAttr(s.name || '')}"
data-edit-group="${escAttr(s.server_group || '')}"
data-edit-price="${escAttr(s.price || '')}"
data-edit-expire="${escAttr(s.expire_date || '')}"
data-edit-bw="${escAttr(s.bandwidth || '')}"
data-edit-traffic="${escAttr(s.traffic_limit || '')}"
data-edit-frontend-hidden="${escAttr(s.frontend_hidden === 'true' ? 'true' : 'false')}"
>✏️ 编辑</button>
<button onclick="deleteServer('${s.id}')" class="btn btn-red">🗑️ 删除</button>
</td>
</tr>
`;
}
}

const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${sys.admin_title}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; background: #f0f2f5; color: #333;}
.card { background: white; padding: 25px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); max-width: 1100px; margin: 0 auto 20px auto; }
h2 { margin-top: 0; border-bottom: 2px solid #f0f2f5; padding-bottom: 10px; font-size: 20px;}
table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 14px; }
th, td { border: 1px solid #eee; padding: 12px; text-align: left; }
th { background: #f8f9fa; }
.btn { cursor: pointer; border-radius: 4px; font-size: 13px; transition: opacity 0.2s; border: none; padding: 6px 10px; color: white; margin-left: 5px; }
.btn:hover { opacity: 0.8; }
.btn-blue { background: #3b82f6; } .btn-green { background: #10b981; } .btn-red { background: #ef4444; } .btn-gray { background: #6b7280; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.form-group { display: flex; flex-direction: column; margin-bottom: 15px; }
.form-group label { font-size: 14px; font-weight: 500; margin-bottom: 6px; color: #555;}
.form-group input[type="text"], .form-group select { padding: 10px; border: 1px solid #ccc; border-radius: 6px; }
.checkbox-group { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 14px;}
.checkbox-group input { width: 18px; height: 18px; cursor: pointer; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 100; }
.modal-content { background: white; padding: 20px; border-radius: 8px; width: 400px; margin: 100px auto; position: relative;}
.modal input { width: 100%; padding: 8px; margin-bottom: 12px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;}
.modal label { font-size: 14px; color: #555; display: block; margin-bottom: 4px; font-weight: bold;}
</style>
</head>
<body>
<div class="card">
<h2>🛠️ 全局设置</h2>
<div class="settings-grid">
<div>
<div class="form-group">
<label>🎨 前端主题风格 (5选1)</label>
<select id="cfg_theme">
<option value="theme1" ${sys.theme === 'theme1' ? 'selected' : ''}>1. 默认清爽白 (Classic White)</option>
<option value="theme2" ${sys.theme === 'theme2' ? 'selected' : ''}>2. 暗黑极客 (Dark Mode)</option>
<option value="theme3" ${sys.theme === 'theme3' ? 'selected' : ''}>3. 新粗野主义 (Brutalism)</option>
<option value="theme4" ${sys.theme === 'theme4' ? 'selected' : ''}>4. 动态渐变毛玻璃 (Glassmorphism)</option>
<option value="theme5" ${sys.theme === 'theme5' ? 'selected' : ''}>5. 赛博朋克 (Cyberpunk)</option>
</select>
</div>
<div class="form-group">
<label>🖼️ 自定义背景图片</label>
<div style="display:flex; gap:8px;">
<input type="text" id="cfgcustombg" value="${sys.custom_bg || ''}" placeholder="粘贴图片 URL 或 点击右侧按钮上传" style="flex:1;">
<input type="file" id="bg_file" accept="image/*" style="display:none;" onchange="uploadBg(this)">
<button class="btn btn-gray" onclick="document.getElementById('bg_file').click()">📁 本地上传</button>
</div>
<img id="bgpreview" src="${sys.custombg || ''}" style="max-height: 120px; margin-top: 10px; border-radius: 6px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); display: ${sys.custom_bg ? 'block' : 'none'}; object-fit: cover;">
</div>
<div class="form-group">
<label>前台看板标题</label>
<input type="text" id="cfgsitetitle" value="${sys.site_title}">
</div>
<div class="form-group">
<label>后台标签栏名称</label>
<input type="text" id="cfgadmintitle" value="${sys.admin_title}">
</div>
</div>
<div>
<label style="font-size: 14px; font-weight: 500; margin-bottom: 10px; display: block; color: #555;">👁️ 前台展示控制</label>
<div class="checkbox-group">
<input type="checkbox" id="cfgispublic" ${sys.is_public === 'true' ? 'checked' : ''}>
<label for="cfgispublic"><b>公开访问</b> (取消勾选后,访客必须输入密码才能查看探针)</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cfgshowprice" ${sys.show_price === 'true' ? 'checked' : ''}>
<label for="cfgshowprice">在前台显示 <b>价格</b></label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cfgshowexpire" ${sys.show_expire === 'true' ? 'checked' : ''}>
<label for="cfgshowexpire">在前台显示 <b>到期时间</b></label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cfgshowbw" ${sys.show_bw === 'true' ? 'checked' : ''}>
<label for="cfgshowbw">在前台显示 <b>带宽徽章</b></label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cfgshowtf" ${sys.show_tf === 'true' ? 'checked' : ''}>
<label for="cfgshowtf">在前台显示 <b>流量配额徽章</b></label>
</div>

<hr style="margin: 20px 0; border: none; border-top: 1px dashed #ccc;">
<label style="font-size: 14px; font-weight: 500; margin-bottom: 10px; display: block; color: #e63946;">✈️ Telegram 离线告警设置</label>
<div class="form-group">
<label>开启离线通知</label>
<select id="cfgtgnotify">
<option value="false" ${sys.tg_notify !== 'true' ? 'selected' : ''}>关闭告警</option>
<option value="true" ${sys.tg_notify === 'true' ? 'selected' : ''}>开启告警 (掉线自动推送)</option>
</select>
</div>
<div class="form-group">
<label>Bot Token</label>
<input type="text" id="cfgtgbottoken" value="${sys.tgbot_token || ''}" placeholder="如: 12345678:ABCDEFG...">
</div>
<div class="form-group">
<label>Chat ID</label>
<input type="text" id="cfgtgchatid" value="${sys.tgchat_id || ''}" placeholder="如: 123456789">
</div>

</div>
</div>
<button onclick="saveSettings()" class="btn btn-blue" style="padding: 10px 20px; font-size: 15px;">💾 保存全局设置</button>
</div>

<div class="card">
<h2>${sys.admin_title} - 节点列表</h2>
<div style="margin-bottom: 15px;">
<input type="text" id="newName" placeholder="输入新服务器名称" style="padding: 8px; width: 200px; border:1px solid #ccc; border-radius:4px;">
<button onclick="addServer()" class="btn btn-blue" style="padding: 9px 15px;">+ 添加新服务器</button>
<a href="/" style="float: right; margin-top: 8px; color: #3b82f6; text-decoration: none; font-weight:bold;">👉 前往大盘预览</a>
</div>
<table>
<tr><th>节点名称</th><th>分组</th><th>在线状态</th><th>操作</th></tr>
${trs || '<tr><td colspan="4" style="text-align:center; padding: 30px; color:#666;">暂无服务器,请在上方添加</td></tr>'}
</table>
</div>

<div id="editModal" class="modal">
<div class="modal-content">
<h3 style="margin-top:0;">✏️ 编辑服务器信息</h3>
<input type="hidden" id="editId">
<label>节点名称</label> <input type="text" id="editName" placeholder="显示在大盘上的名称">
<label>分组名称</label> <input type="text" id="editGroup" placeholder="如:美国 VPS">
<label>价格(¥、$等)</label> <input type="text" id="editPrice" placeholder="如:40USD/Year 或 免费">
<label>到期时间</label> <input type="date" id="editExpire">
<label>带宽 (前端徽章)</label> <input type="text" id="editBandwidth" placeholder="如:1Gbps 或 200Mbps">
<label>流量总量 (前端徽章)</label> <input type="text" id="editTraffic" placeholder="如:1TB/月">
<label style="display:flex;align-items:center;gap:10px;font-weight:normal;margin-top:12px;cursor:pointer;">
<input type="checkbox" id="editFrontendHidden" style="width:auto;margin:0;">
<span>在前台隐藏此节点(默认显示;勾选后大盘与详情均不展示)</span>
</label>
<div style="text-align: right; margin-top: 10px;">
<button onclick="closeModal()" style="padding: 8px 15px; border: 1px solid #ccc; background: white; margin-right: 5px; cursor:pointer;">取消</button>
<button onclick="saveEdit()" class="btn btn-blue" style="padding: 8px 15px;">保存更改</button>
</div>
</div>
</div>

${footerHtml}

<script>
function uploadBg(input) {
const file = input.files[0];
if(!file) return;
if(file.size > 800 * 1024) {
alert('图片有点大,建议使用 500KB 以下的图片!');
}
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('cfgcustombg').value = e.target.result;
document.getElementById('bg_preview').src = e.target.result;
document.getElementById('bg_preview').style.display = 'block';
};
reader.readAsDataURL(file);
}

async function saveSettings() {
const data = {
action: 'save_settings',
settings: {
theme: document.getElementById('cfg_theme').value,
custombg: document.getElementById('cfgcustom_bg').value,
sitetitle: document.getElementById('cfgsite_title').value,
admintitle: document.getElementById('cfgadmin_title').value,
ispublic: document.getElementById('cfgis_public').checked ? 'true' : 'false',
showprice: document.getElementById('cfgshow_price').checked ? 'true' : 'false',
showexpire: document.getElementById('cfgshow_expire').checked ? 'true' : 'false',
showbw: document.getElementById('cfgshow_bw').checked ? 'true' : 'false',
showtf: document.getElementById('cfgshow_tf').checked ? 'true' : 'false',
tgnotify: document.getElementById('cfgtg_notify').value,
tgbottoken: document.getElementById('cfgtgbot_token').value,
tgchatid: document.getElementById('cfgtgchat_id').value
}
};
const res = await fetch('/admin/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
if (res.ok) { alert('✅ 设置已保存!'); location.reload(); } else alert('保存失败');
}
async function addServer() {
const name = document.getElementById('newName').value;
if (!name) return alert('请输入名称');
const res = await fetch('/admin/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add', name }) });
if (res.ok) location.reload(); else alert('添加失败');
}
async function deleteServer(id) {
if (!confirm('确定要删除这个节点吗?')) return;
const res = await fetch('/admin/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', id }) });
if (res.ok) location.reload(); else alert('删除失败');
}
function copyCmd(id) {
const input = document.getElementById('cmd-' + id);
input.select(); document.execCommand('copy');
alert('✅ 一键命令已复制!');
}
document.addEventListener('click', function(ev) {
const btn = ev.target.closest('.js-edit-server');
if (!btn) return;
openEditModal(
btn.getAttribute('data-edit-id') || '',
btn.getAttribute('data-edit-name') || '',
btn.getAttribute('data-edit-group') || '',
btn.getAttribute('data-edit-price') || '',
btn.getAttribute('data-edit-expire') || '',
btn.getAttribute('data-edit-bw') || '',
btn.getAttribute('data-edit-traffic') || '',
btn.getAttribute('data-edit-frontend-hidden') || 'false'
);
});
function openEditModal(id, name, group, price, expire, bw, traffic, frontendHidden) {
document.getElementById('editId').value = id;
document.getElementById('editName').value = name || '';
document.getElementById('editGroup').value = group || '默认分组';
document.getElementById('editPrice').value = price || '免费';
document.getElementById('editExpire').value = expire || '';
document.getElementById('editBandwidth').value = bw || '';
document.getElementById('editTraffic').value = traffic || '';
document.getElementById('editFrontendHidden').checked = frontendHidden === 'true';
document.getElementById('editModal').style.display = 'block';
}
function closeModal() { document.getElementById('editModal').style.display = 'none'; }
async function saveEdit() {
const data = {
action: 'edit', id: document.getElementById('editId').value,
name: document.getElementById('editName').value,
server_group: document.getElementById('editGroup').value, price: document.getElementById('editPrice').value,
expire_date: document.getElementById('editExpire').value, bandwidth: document.getElementById('editBandwidth').value,
traffic_limit: document.getElementById('editTraffic').value,
frontend_hidden: document.getElementById('editFrontendHidden').checked
};
const res = await fetch('/admin/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
if (res.ok) location.reload(); else alert('保存失败');
}
</script>
</body>
</html>`;
return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}

// ==========================================
// 3. 一键安装脚本 (/install.sh)
// ==========================================
if (request.method === 'GET' && url.pathname === '/install.sh') {
const sh_bin = "/bin" + "/sh";
const sh_etc = "/etc/" + "systemd/" + "system";
const sh_sys = "system" + "ctl";
const sh_curl = "cur" + "l";

// 完美还原了转义符号,并应用了60秒优化
const bashScript = `#!${sh_bin}
SERVER_ID=$1
SECRET=$2
WORKER_URL="${host}/update"

if [ -z "$SERVER_ID" ] || [ -z "$SECRET" ]; then echo "错误: 缺少参数。"; exit 1; fi
echo "开始安装全面增强版 CF Probe Agent..."

if ! command -v ${sh_curl} >/dev/null 2>&1; then
if command -v apk >/dev/null 2>&1; then
apk add --no-cache curl >/dev/null 2>&1
elif command -v apt-get >/dev/null 2>&1; then
apt-get update >/dev/null 2>&1 && apt-get install -y curl >/dev/null 2>&1
elif command -v yum >/dev/null 2>&1; then
yum install -y curl >/dev/null 2>&1
fi
fi

if command -v ${sh_sys} >/dev/null 2>&1; then
${sh_sys} stop cf-probe.service 2>/dev/null
fi
pkill -f cf-probe.sh 2>/dev/null || killall cf-probe.sh 2>/dev/null

cat << 'EOF' > /usr/local/bin/cf-probe.sh
#!${sh_bin}
SERVER_ID="$1"
SECRET="$2"
WORKER_URL="$3"

Alpine/OpenRC 某些环境下服务参数可能丢失,回退读取配置文件

if [ -z "$SERVERID" ] || [ -z "$SECRET" ] || [ -z "$WORKERURL" ]; then
if [ -f /etc/cf-probe.conf ]; then
. /etc/cf-probe.conf
fi
fi

if [ -z "$SERVERID" ] || [ -z "$SECRET" ] || [ -z "$WORKERURL" ]; then
sleep 30
exit 1
fi

getnetbytes() { awk 'NR>2 {rx+=\$2; tx+=\$10} END {printf "%.0f %.0f", rx, tx}' /proc/net/dev; }
getcpustat() { awk '/^cpu / {print \$2+\$3+\$4+\$5+\$6+\$7+\$8+\$9, \$5+\$6}' /proc/stat; }

NETSTAT=\$(getnet_bytes)
RXPREV=\$(echo \$NETSTAT | awk '{print \$1}')
TXPREV=\$(echo \$NETSTAT | awk '{print \$2}')
if [ -z "\$RXPREV" ]; then RXPREV=0; fi
if [ -z "\$TXPREV" ]; then TXPREV=0; fi

CPUSTAT=\$(getcpu_stat)
PREVCPUTOTAL=\$(echo \$CPU_STAT | awk '{print \$1}')
PREVCPUIDLE=\$(echo \$CPU_STAT | awk '{print \$2}')

LOOP_COUNT=0
IPV4="0"; IPV6="0"

while true; do
if [ \$((LOOP_COUNT % 60)) -eq 0 ]; then
${sh_curl} -s -4 -m 3 https://cloudflare.com/cdn-cgi/trace 2>/dev/null | grep -q "ip=" && IPV4="1" || IPV4="0"
${sh_curl} -s -6 -m 3 https://cloudflare.com/cdn-cgi/trace 2>/dev/null | grep -q "ip=" && IPV6="1" || IPV6="0"
fi
LOOPCOUNT=\$((LOOPCOUNT + 1))

OS=\$(awk -F= '/^PRETTY_NAME/{print \$2}' /etc/os-release | tr -d '"')
if [ -z "\$OS" ]; then OS=\$(uname -srm); fi
ARCH=\$(uname -m)
BOOT_TIME=\$(uptime -s 2>/dev/null || stat -c %y / 2>/dev/null | cut -d'.' -f1 || echo "Unknown")
CPU_INFO=\$(grep -m 1 'model name' /proc/cpuinfo | awk -F: '{print \$2}' | xargs | tr -d '"')

CPUSTAT=\$(getcpu_stat)
CPUTOTAL=\$(echo \$CPUSTAT | awk '{print \$1}')
CPUIDLE=\$(echo \$CPUSTAT | awk '{print \$2}')
DIFFTOTAL=\$((CPUTOTAL - PREVCPUTOTAL))
DIFFIDLE=\$((CPUIDLE - PREVCPUIDLE))
CPU=\$(awk -v t=\$DIFFTOTAL -v i=\$DIFFIDLE 'BEGIN {if (t==0) print 0; else printf "%.2f", (1 - i/t)*100}')
PREVCPUTOTAL=\$CPUTOTAL; PREVCPUIDLE=\$CPUIDLE

MEM_INFO=\$(free -m)
RAMTOTAL=\$(echo "\$MEMINFO" | awk '/Mem:/ {print \$2}')
RAMUSED=\$(echo "\$MEMINFO" | awk '/Mem:/ {print \$3}')
RAM=\$(awk "BEGIN {if(\$RAMTOTAL>0) printf \\"%.2f\\", \$RAMUSED/\$RAM_TOTAL * 100.0; else print 0}")

SWAPTOTAL=\$(echo "\$MEMINFO" | awk '/Swap:/ {print \$2}')
SWAPUSED=\$(echo "\$MEMINFO" | awk '/Swap:/ {print \$3}')
if [ -z "\$SWAPTOTAL" ]; then SWAPTOTAL=0; fi
if [ -z "\$SWAPUSED" ]; then SWAPUSED=0; fi

DISK_INFO=\$(df -hm / | tail -n1 | awk '{print \$2, \$3, \$5}')
DISKTOTAL=\$(echo "\$DISKINFO" | awk '{print \$1}')
DISKUSED=\$(echo "\$DISKINFO" | awk '{print \$2}')
DISK=\$(echo "\$DISK_INFO" | awk '{print \$3}' | tr -d '%')

LOAD=\$(cat /proc/loadavg | awk '{print \$1, \$2, \$3}')
UPTIME=\$(uptime -p | sed 's/up //')

PROCESSES=\$(ps -e | wc -l)
TCP_CONN=\$(ss -ant 2>/dev/null | grep -v State | wc -l || netstat -ant 2>/dev/null | grep -v Active | wc -l)
UDP_CONN=\$(ss -anu 2>/dev/null | grep -v State | wc -l || netstat -anu 2>/dev/null | grep -v Active | wc -l)

NETSTAT=\$(getnet_bytes)
RXNOW=\$(echo \$NETSTAT | awk '{print \$1}')
TXNOW=\$(echo \$NETSTAT | awk '{print \$2}')
if [ -z "\$RXNOW" ]; then RXNOW=0; fi
if [ -z "\$TXNOW" ]; then TXNOW=0; fi

# [优化] 上报时间改为 60 秒,网速计算除数同步改为 60
RXSPEED=\$(((RXNOW - RX_PREV) / 60))
TXSPEED=\$(((TXNOW - TX_PREV) / 60))
RXPREV=\$RXNOW; TXPREV=\$TXNOW

PAYLOAD="{\\"id\\": \\"\$SERVERID\\", \\"secret\\": \\"\$SECRET\\", \\"metrics\\": { \\"cpu\\": \\"\$CPU\\", \\"ram\\": \\"\$RAM\\", \\"ramtotal\\": \\"\$RAMTOTAL\\", \\"ramused\\": \\"\$RAMUSED\\", \\"swaptotal\\": \\"\$SWAPTOTAL\\", \\"swapused\\": \\"\$SWAPUSED\\", \\"disk\\": \\"\$DISK\\", \\"disktotal\\": \\"\$DISKTOTAL\\", \\"diskused\\": \\"\$DISKUSED\\", \\"load\\": \\"\$LOAD\\", \\"uptime\\": \\"\$UPTIME\\", \\"boottime\\": \\"\$BOOTTIME\\", \\"netrx\\": \\"\$RXNOW\\", \\"nettx\\": \\"\$TXNOW\\", \\"netinspeed\\": \\"\$RXSPEED\\", \\"netoutspeed\\": \\"\$TXSPEED\\", \\"os\\": \\"\$OS\\", \\"arch\\": \\"\$ARCH\\", \\"cpuinfo\\": \\"\$CPUINFO\\", \\"processes\\": \\"\$PROCESSES\\", \\"tcpconn\\": \\"\$TCPCONN\\", \\"udpconn\\": \\"\$UDPCONN\\", \\"ipv4\\": \\"\$IPV4\\", \\"ip_v6\\": \\"\$IPV6\\" }}"

${shcurl} -s -X POST -H "Content-Type: application/json" -d "\$PAYLOAD" "$WORKERURL" > /dev/null

# [优化] 心跳间隔改为 60 秒
sleep 60
done
EOF

chmod +x /usr/local/bin/cf-probe.sh

cat << EOF > /etc/cf-probe.conf
SERVERID="\${SERVERID}"
SECRET="\${SECRET}"
WORKERURL="\${WORKERURL}"
EOF

if command -v ${sh_sys} >/dev/null 2>&1; then
cat << EOF > ${sh_etc}/cf-probe.service
[Unit]
Description=Cloudflare Worker Probe Agent
After=network.target

[Service]
ExecStart=/usr/local/bin/cf-probe.sh $SERVERID $SECRET $WORKERURL
Restart=always
User=root

[Install]
WantedBy=multi-user.target
EOF

${sh_sys} daemon-reload
${sh_sys} enable cf-probe.service
${sh_sys} restart cf-probe.service
elif command -v rc-service >/dev/null 2>&1 && command -v rc-update >/dev/null 2>&1; then
cat << EOF > /etc/init.d/cf-probe
#!/sbin/openrc-run
name="Cloudflare Worker Probe Agent"
description="Cloudflare Worker Probe Agent"
command="/usr/local/bin/cf-probe.sh"
commandargs="\${SERVERID} \${SECRET} \${WORKER_URL}"
command_background="yes"
pidfile="/run/cf-probe.pid"

depend() {
need net
}
EOF
chmod +x /etc/init.d/cf-probe
rc-update add cf-probe default >/dev/null 2>&1 || true
rc-service cf-probe restart >/dev/null 2>&1 || rc-service cf-probe start >/dev/null 2>&1
pgrep -f "cf-probe.sh \$SERVERID" >/dev/null 2>&1 || nohup /usr/local/bin/cf-probe.sh "$SERVERID" "$SECRET" "$WORKER_URL" >/var/log/cf-probe.log 2>&1 &
else
nohup /usr/local/bin/cf-probe.sh "$SERVERID" "$SECRET" "$WORKERURL" >/var/log/cf-probe.log 2>&1 &
fi

echo "✅ 探针安装成功!"
`;
return new Response(bashScript, { headers: { 'Content-Type': 'text/plain;charset=UTF-8' } });
}

// ==========================================
// 4. API 接收数据 (/update)
// ==========================================
if (request.method === 'POST' && url.pathname === '/update') {
try {
const data = await request.json();
const { id, secret, metrics } = data;

if (secret !== env.API_SECRET) return new Response('Unauthorized', { status: 401 });

let countryCode = request.cf && request.cf.country ? request.cf.country : 'XX';
if (countryCode.toUpperCase() === 'TW') countryCode = 'CN';

const serverExists = await env.DB.prepare('SELECT id FROM servers WHERE id = ?').bind(id).first();
if (!serverExists) return new Response('Server not found', { status: 404 });

await env.DB.prepare(`
UPDATE servers
SET cpu = ?, ram = ?, disk = ?, loadavg = ?, uptime = ?, lastupdated = ?,
ramtotal = ?, netrx = ?, nettx = ?, netinspeed = ?, netout_speed = ?,
os = ?, cpuinfo = ?, arch = ?, boottime = ?, ramused = ?, swaptotal = ?,
swapused = ?, disktotal = ?, diskused = ?, processes = ?, tcpconn = ?, udp_conn = ?,
country = ?, ipv4 = ?, ipv6 = ?
WHERE id = ?
`).bind(
metrics.cpu, metrics.ram, metrics.disk, metrics.load, metrics.uptime, Date.now(),
metrics.ramtotal || '0', metrics.netrx || '0', metrics.net_tx || '0',
metrics.netinspeed || '0', metrics.netoutspeed || '0',
metrics.os || '', metrics.cpuinfo || '', metrics.arch || '', metrics.boottime || '',
metrics.ramused || '0', metrics.swaptotal || '0', metrics.swap_used || '0',
metrics.disktotal || '0', metrics.diskused || '0', metrics.processes || '0',
metrics.tcpconn || '0', metrics.udpconn || '0', countryCode,
metrics.ipv4 || '0', metrics.ipv6 || '0', id
).run();

ctx.waitUntil(checkOfflineNodes());

return new Response('OK', { status: 200 });
} catch (e) {
return new Response('Error', { status: 400 });
}
}

// ==========================================
// 5. 单个服务器详情 JSON API
// ==========================================
if (request.method === 'GET' && url.pathname === '/api/server') {
if (sys.ispublic !== 'true' && !checkAuth(request)) return authResponse(sys.sitetitle);

const id = url.searchParams.get('id');
if (!id) return new Response('Miss ID', { status: 400 });
const server = await env.DB.prepare('SELECT * FROM servers WHERE id = ?').bind(id).first();
if (!server) return new Response('Not Found', { status: 404 });
if (server.frontend_hidden === 'true') return new Response('Not Found', { status: 404 });
return new Response(JSON.stringify(server), { headers: { 'Content-Type': 'application/json' } });
}

// ==========================================
// 6. 前台探针首页 & 详情页 (/ )
// ==========================================
if (request.method === 'GET' && url.pathname === '/') {
if (sys.is_public !== 'true' && !checkAuth(request)) {
return authResponse(sys.site_title);
}

const viewId = url.searchParams.get('id');

// ----------------------------------------
// 视图 A:详情页折线图
// ----------------------------------------
if (viewId) {
const server = await env.DB.prepare('SELECT * FROM servers WHERE id = ?').bind(viewId).first();
if (!server) return new Response('Server not found', { status: 404 });
if (server.frontend_hidden === 'true') return new Response('Server not found', { status: 404 });

const detailHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${server.name} - ${sys.site_title}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f9fafb; color: #333; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.title-row { display: flex; align-items: center; margin-bottom: 16px; }
.title-row h2 { margin: 0; font-size: 24px; margin-right: 12px; display: flex; align-items: center;}
.status-badge { background: #10b981; color: white; padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: bold; }
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; font-size: 14px; }
.info-item { display: flex; flex-direction: column; }
.info-label { color: #6b7280; font-size: 12px; margin-bottom: 4px; }
.info-value { font-weight: 500; }
.charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; }
.chart-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.chart-card h3 { margin-top: 0; font-size: 16px; color: #374151; display: flex; justify-content: space-between; align-items: center; }
.chart-val { font-size: 18px; font-weight: bold; }
canvas { max-height: 150px; }
.back-btn { display: inline-block; margin-bottom: 15px; color: #3b82f6; text-decoration: none; font-weight: 500; }
${themeStyles}
</style>
</head>
<body class="${sys.theme || 'theme1'}">
<div class="container">
<a href="/" class="back-btn">⬅ 返回大盘</a>
<div class="header-card">
<div class="title-row">
<h2><span id="head-flag"></span> ${server.name}</h2>
<span class="status-badge" id="head-status">在线</span>
</div>
<div class="info-grid">
<div class="info-item"><span class="info-label">运行时间</span><span class="info-value" id="val-uptime">...</span></div>
<div class="info-item"><span class="info-label">架构</span><span class="info-value" id="val-arch">...</span></div>
<div class="info-item"><span class="info-label">系统</span><span class="info-value" id="val-os">...</span></div>
<div class="info-item"><span class="info-label">CPU</span><span class="info-value" id="val-cpuinfo">...</span></div>
<div class="info-item"><span class="info-label">Load</span><span class="info-value" id="val-load">...</span></div>
<div class="info-item"><span class="info-label">上传 / 下载</span><span class="info-value" id="val-traffic">...</span></div>
<div class="info-item"><span class="info-label">启动时间</span><span class="info-value" id="val-boot">...</span></div>
</div>
</div>
<div class="charts-grid">
<div class="chart-card"><h3>CPU <span class="chart-val" id="text-cpu">0%</span></h3><canvas id="chartCPU"></canvas></div>
<div class="chart-card"><h3>内存 <span class="chart-val" id="text-ram">0%</span></h3><div style="font-size:12px; color:#6b7280; margin-bottom:5px;" id="text-swap">Swap: 0 / 0</div><canvas id="chartRAM"></canvas></div>
<div class="chart-card"><h3>磁盘 <span class="chart-val" id="text-disk">0%</span></h3><div style="width:100%; height:20px; background:#e5e7eb; border-radius:10px; overflow:hidden; margin-top:40px;"><div id="disk-bar" style="height:100%; width:0%; background:#34d399; transition:width 0.5s;"></div></div><p style="text-align:right; font-size:12px; color:#6b7280; margin-top:8px;" id="text-disk-detail">0 / 0</p></div>
<div class="chart-card"><h3>进程数 <span class="chart-val" id="text-proc">0</span></h3><canvas id="chartProc"></canvas></div>
<div class="chart-card"><h3>网络速度 <span class="chart-val" style="font-size:14px;"><span style="color:#10b981">↓</span> <span id="text-net-in">0</span> | <span style="color:#3b82f6">↑</span> <span id="text-net-out">0</span></span></h3><canvas id="chartNet"></canvas></div>
<div class="chart-card"><h3>TCP / UDP <span class="chart-val" style="font-size:14px;">TCP <span id="text-tcp">0</span> | UDP <span id="text-udp">0</span></span></h3><canvas id="chartConn"></canvas></div>
</div>
${footerHtml}
</div>
<script>
const serverId = "${viewId}";
const formatBytes = (bytes) => { const b = parseInt(bytes); if (isNaN(b) || b === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(b) / Math.log(k)); return parseFloat((b / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; };
const commonOptions = { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { display: false }, y: { beginAtZero: true, border: { display: false } } }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, elements: { point: { radius: 0 }, line: { tension: 0.4, borderWidth: 2 } } };
const createChart = (ctxId, color, bgColor) => { const ctx = document.getElementById(ctxId).getContext('2d'); return new Chart(ctx, { type: 'line', data: { labels: Array(30).fill(''), datasets: [{ data: Array(30).fill(0), borderColor: color, backgroundColor: bgColor, fill: true }] }, options: commonOptions }); };
const charts = { cpu: createChart('chartCPU', '#3b82f6', 'rgba(59, 130, 246, 0.1)'), ram: createChart('chartRAM', '#8b5cf6', 'rgba(139, 92, 246, 0.1)'), proc: createChart('chartProc', '#ec4899', 'rgba(236, 72, 153, 0.1)') };
const ctxNet = document.getElementById('chartNet').getContext('2d'); charts.net = new Chart(ctxNet, { type: 'line', data: { labels: Array(30).fill(''), datasets: [ { label: 'In', data: Array(30).fill(0), borderColor: '#10b981', borderWidth: 2, tension: 0.4, pointRadius: 0 }, { label: 'Out', data: Array(30).fill(0), borderColor: '#3b82f6', borderWidth: 2, tension: 0.4, pointRadius: 0 } ]}, options: commonOptions });
const ctxConn = document.getElementById('chartConn').getContext('2d'); charts.conn = new Chart(ctxConn, { type: 'line', data: { labels: Array(30).fill(''), datasets: [ { label: 'TCP', data: Array(30).fill(0), borderColor: '#6366f1', borderWidth: 2, tension: 0.4, pointRadius: 0 }, { label: 'UDP', data: Array(30).fill(0), borderColor: '#d946ef', borderWidth: 2, tension: 0.4, pointRadius: 0 } ]}, options: commonOptions });
const updateChartData = (chart, newData, datasetIndex = 0) => { const dataArr = chart.data.datasets[datasetIndex].data; dataArr.push(newData); dataArr.shift(); chart.update(); };

async function fetchData() {
try {
const res = await fetch('/api/server?id=' + serverId); const data = await res.json();
const cCode = (data.country || 'xx').toLowerCase();
document.getElementById('head-flag').innerHTML = cCode !== 'xx' ? \<img src="https://flagcdn.com/24x18/\${cCode}.png" alt="\${cCode}" style="vertical-align: middle; margin-right: 8px; border-radius: 2px;">\ : '🏳️ ';
document.getElementById('val-uptime').innerText = data.uptime || 'N/A'; document.getElementById('val-arch').innerText = data.arch || 'N/A'; document.getElementById('val-os').innerText = data.os || 'N/A'; document.getElementById('val-cpuinfo').innerText = data.cpuinfo || 'N/A'; document.getElementById('val-load').innerText = data.loadavg || '0.00'; document.getElementById('val-boot').innerText = data.boottime || 'N/A'; document.getElementById('val-traffic').innerText = formatBytes(data.nettx) + ' / ' + formatBytes(data.net_rx);

// [优化] 前端单机判定离线放宽至 90 秒
const isOnline = (Date.now() - data.last_updated) < 90000;

const badge = document.getElementById('head-status'); badge.innerText = isOnline ? '在线' : '离线'; badge.style.background = isOnline ? '#10b981' : '#ef4444';
if(!isOnline) return;
document.getElementById('text-cpu').innerText = data.cpu + '%'; document.getElementById('text-ram').innerText = data.ram + '%'; document.getElementById('text-swap').innerText = 'Swap: ' + data.swapused + ' MiB / ' + data.swaptotal + ' MiB'; document.getElementById('text-proc').innerText = data.processes || '0'; document.getElementById('text-net-in').innerText = formatBytes(data.netinspeed) + '/s'; document.getElementById('text-net-out').innerText = formatBytes(data.netoutspeed) + '/s'; document.getElementById('text-tcp').innerText = data.tcpconn || '0'; document.getElementById('text-udp').innerText = data.udpconn || '0';
let diskTotal = parseFloat(data.disktotal) || 0; let diskUsed = parseFloat(data.diskused) || 0; let diskPct = parseInt(data.disk) || 0;
document.getElementById('text-disk').innerText = diskPct + '%'; document.getElementById('disk-bar').style.width = diskPct + '%'; document.getElementById('text-disk-detail').innerText = (diskUsed/1024).toFixed(2) + ' GiB / ' + (diskTotal/1024).toFixed(2) + ' GiB';
updateChartData(charts.cpu, parseFloat(data.cpu) || 0); updateChartData(charts.ram, parseFloat(data.ram) || 0); updateChartData(charts.proc, parseInt(data.processes) || 0); updateChartData(charts.net, parseFloat(data.netinspeed) || 0, 0); updateChartData(charts.net, parseFloat(data.netoutspeed) || 0, 1); updateChartData(charts.conn, parseInt(data.tcpconn) || 0, 0); updateChartData(charts.conn, parseInt(data.udpconn) || 0, 1);
} catch (e) {}
}
// [优化] 拉取间隔改为 60 秒
setInterval(fetchData, 60000); fetchData();
</script>
</body>
</html>`;

// [优化] 给详情页加入 30 秒缓存
return new Response(detailHtml, { headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Cache-Control': 'public, max-age=30' } });
}

// ----------------------------------------
// 视图 B:全新前台大盘
// ----------------------------------------
const { results } = await env.DB.prepare('SELECT * FROM servers').all();
const now = Date.now();

let globalOnline = 0; let globalOffline = 0;
let globalSpeedIn = 0; let globalSpeedOut = 0;
let globalNetTx = 0; let globalNetRx = 0;
let visibleTotal = 0;
const groups = {};

if (results && results.length > 0) {
for (const server of results) {
if (server.frontend_hidden === 'true') continue;
visibleTotal++;
// [优化] 统计逻辑中判定在线放宽至 90 秒
const isOnline = (now - server.last_updated) < 90000;
if (isOnline) {
globalOnline++;
globalSpeedIn += parseFloat(server.netinspeed) || 0;
globalSpeedOut += parseFloat(server.netoutspeed) || 0;
} else {
globalOffline++;
}
globalNetTx += parseFloat(server.net_tx) || 0;
globalNetRx += parseFloat(server.net_rx) || 0;

const grpName = server.server_group || '默认分组';
if (!groups[grpName]) groups[grpName] = [];
groups[grpName].push(server);
}
}

let contentHtml = '';
if (Object.keys(groups).length === 0) {
contentHtml = '<p style="text-align:center; width: 100%; color:#888;">暂无服务器,请在后台添加</p>';
} else {
for (const [grpName, grpServers] of Object.entries(groups)) {
contentHtml += <div class="group-header">${grpName}</div><div class="grid-container">;
for (const server of grpServers) {
// [优化] 大盘渲染中判定在线放宽至 90 秒
const isOnline = (now - server.last_updated) < 90000;
const statusColor = isOnline ? '#10b981' : '#ef4444';

const cpu = server.cpu || '0'; const ram = server.ram || '0'; const disk = server.disk || '0';
const netInSpeed = formatBytes(server.netinspeed); const netOutSpeed = formatBytes(server.netoutspeed);

const cCode = (server.country || 'xx').toLowerCase();
const flagHtml = cCode !== 'xx' ? <img src="https://flagcdn.com/24x18/${cCode}.png" alt="${cCode}" style="vertical-align: sub; margin-right: 5px; border-radius: 2px;"> : '🏳️';

let metaHtml = '';
if (sys.show_price === 'true') {
metaHtml += <div class="card-meta" style="margin-top:8px;">价格: ${server.price || '免费'}</div>;
}
if (sys.show_expire === 'true') {
let expireText = '永久';
if (server.expire_date) {
const expDate = new Date(server.expire_date);
const expTime = expDate.getTime();
if (!isNaN(expTime)) {
const diff = expTime - now;
const expireDateText = ${expDate.getFullYear()}-${expDate.getMonth() + 1}-${expDate.getDate()};
expireText = diff > 0
? 到期: ${expireDateText}(剩余${Math.ceil(diff / (1000 3600 24))}天)
: 已过期(到期: ${expireDateText});
}
}
metaHtml += <div class="card-meta" style="${sys.show_price !== 'true' ? 'margin-top:8px;' : ''}">${expireText}</div>;
}

let badgesHtml = '';
if (sys.show_bw === 'true' && server.bandwidth) badgesHtml += <span class="badge badge-bw">${server.bandwidth}</span>;
if (sys.showtf === 'true' && server.trafficlimit) badgesHtml += <span class="badge badge-tf">${server.traffic_limit}</span>;
if (server.ip_v4 === '1') badgesHtml += <span class="badge badge-v4">IPv4</span>;
if (server.ip_v6 === '1') badgesHtml += <span class="badge badge-v6">IPv6</span>;

contentHtml += `
<a href="/?id=${server.id}" class="vps-card">
<div class="card-left">
<div class="card-title">
<div class="status-dot" style="background:${statusColor};"></div>
${flagHtml} <span style="font-size:15px;" class="card-title-text">${server.name}</span>
</div>
${metaHtml}
<div class="card-badges">${badgesHtml}</div>
</div>

<div class="card-right">
<div class="stat-col"><div class="stat-label">CPU</div><div class="stat-val">${cpu}%</div><div class="stat-bar"><div style="width:${cpu}%;"></div></div></div>
<div class="stat-col"><div class="stat-label">内存</div><div class="stat-val">${ram}%</div><div class="stat-bar"><div style="width:${ram}%; background:#f59e0b;"></div></div></div>
<div class="stat-col"><div class="stat-label">存储</div><div class="stat-val">${disk}%</div><div class="stat-bar"><div style="width:${disk}%; background:#10b981;"></div></div></div>
<div class="stat-col"><div class="stat-label">上传</div><div class="stat-val">${netOutSpeed}/s</div></div>
<div class="stat-col"><div class="stat-label">下载</div><div class="stat-val">${netInSpeed}/s</div></div>
</div>
</a>
`;
}
contentHtml += </div>;
}
}

const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${sys.site_title}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f4f5f7; color: #333; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.global-stats { display: flex; flex-wrap: wrap; gap: 20px; justify-content: space-around; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.03); margin-bottom: 30px; text-align: center; }
.g-item { flex: 1; min-width: 200px; }
.g-val { font-size: 24px; font-weight: bold; color: #111; margin: 8px 0; }
.g-label { font-size: 13px; color: #666; }
.g-sub { font-size: 12px; color: #999; }
.group-header { font-size: 18px; font-weight: 500; color: #444; margin: 25px 0 15px 5px; border-left: 4px solid #3b82f6; padding-left: 10px; }
.grid-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 15px; }
.vps-card { display: flex; justify-content: space-between; align-items: stretch; background: white; padding: 18px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); text-decoration: none; color: inherit; border: 1px solid transparent; transition: all 0.2s ease; }
.vps-card:hover { border-color: #e5e7eb; transform: translateY(-2px); box-shadow: 0 8px 15px rgba(0,0,0,0.08); }
.card-left { flex: 0 0 180px; display: flex; flex-direction: column; justify-content: center; }
.card-title { display: flex; align-items: center; margin-bottom: 4px; }
.card-title-text { font-weight: 500; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; flex-shrink:0; }
.card-meta { font-size: 12px; color: #6b7280; margin-bottom: 3px; }
.card-badges { margin-top: 10px; display: flex; gap: 5px; flex-wrap: wrap; }
.badge { padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 500; color: white; }
.badge-bw { background: #3b82f6; } .badge-tf { background: #10b981; } .badge-v4 { background: #a855f7; } .badge-v6 { background: #ec4899; }
.card-right { flex: 1; display: flex; justify-content: space-between; align-items: center; padding-left: 15px; border-left: 1px solid #f0f0f0; }
.stat-col { display: flex; flex-direction: column; align-items: center; width: 50px; }
.stat-label { font-size: 11px; color: #888; margin-bottom: 8px; }
.stat-val { font-size: 13px; font-weight: 500; color: #111; margin-bottom: 6px; }
.stat-bar { width: 100%; height: 3px; background: #e5e7eb; border-radius: 2px; overflow: hidden; }
.stat-bar > div { height: 100%; background: #3b82f6; border-radius: 2px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
.admin-btn { padding: 8px 16px; background: #3b82f6; color: white; text-decoration: none; border-radius: 6px; font-size: 14px; font-weight:bold; }
@media (max-width: 600px) { .grid-container { grid-template-columns: 1fr; } .vps-card { flex-direction: column; } .card-right { padding-left: 0; border-left: none; border-top: 1px solid #f0f0f0; margin-top: 15px; padding-top: 15px; } }
${themeStyles}
</style>
<meta http-equiv="refresh" content="60">
</head>
<body class="${sys.theme || 'theme1'}">
<div class="container">
<div class="header">
<h1 style="margin:0;">${sys.site_title}</h1>

</div>
<div class="global-stats">
<div class="g-item"><div class="g-label">服务器总数: <span class="g-val" style="display:inline; margin:0 5px;">${visibleTotal}</span> <br /> (在线 <span style="color:#10b981">${globalOnline}</span> | 离线 <span style="color:#ef4444">${globalOffline}</span>)</div></div>
<div class="g-item"><div class="g-label">总计流量 (入 | 出)</div><div class="g-val">${formatBytes(globalNetRx)} | ${formatBytes(globalNetTx)}</div></div>
<div class="g-item"><div class="g-label">实时网速 (入 | 出)</div><div class="g-val"><span style="color:#10b981">↓</span> ${formatBytes(globalSpeedIn)}/s | <span style="color:#3b82f6">↑</span> ${formatBytes(globalSpeedOut)}/s</div></div>
</div>
${contentHtml}
${footerHtml}
</div>
</body>
</html>`;

// [优化] 给前台大盘加入 30 秒边缘缓存,抵御 F5 狂按刷量
return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Cache-Control': 'public, max-age=30' } });
}

return new Response('Not Found', { status: 404 });
}
};

绑定 D1 数据库,变量名填 DB
设置后台密码,添加环境变量 API_SECRET ,值为你自定义的管理后台登录密码(类型选择“文本”或“机密”均可)。

探针卸载 (Agent)

systemctl stop cf-probe.service

systemctl disable cf-probe.service
rm /etc/systemd/system/cf-probe.service
rm /usr/local/bin/cf-probe.sh
systemctl daemon-reload