guanghulab/server/proxy/dashboard/update-dashboard.js
2026-05-10 13:12:44 +08:00

226 lines
8.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/dashboard/update-dashboard.js
// 📊 铸渊专线 · README仪表盘更新
//
// 读取服务器上的流量数据更新README.md中的仪表盘区域
// 仅显示用量数字和状态,不暴露任何敏感信息
//
// 用法:
// node update-dashboard.js — 从本地文件读取
// node update-dashboard.js --remote — 从服务器API读取
// node update-dashboard.js --json <f> — 从指定JSON文件读取
//
// 环境变量:
// ZY_SERVER_HOST — 服务器地址 (--remote模式)
// ═══════════════════════════════════════════════
'use strict';
const fs = require('fs');
const path = require('path');
const http = require('http');
const ROOT = path.resolve(__dirname, '..', '..', '..');
const README_PATH = path.join(ROOT, 'README.md');
// ── 默认配额数据 ─────────────────────────────
function getDefaultQuota() {
const now = new Date();
return {
total_gb: 500,
used_gb: 0,
remaining_gb: 500,
percentage_used: 0,
period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`,
reset_day: 1,
updated_at: now.toISOString()
};
}
// ── 从本地文件读取配额 ──────────────────────
function readLocalQuota() {
const quotaFile = '/opt/zhuyuan/proxy/data/quota-status.json';
try {
const data = JSON.parse(fs.readFileSync(quotaFile, 'utf8'));
const totalGB = (data.total_bytes / (1024 ** 3)).toFixed(1);
const usedGB = ((data.upload_bytes + data.download_bytes) / (1024 ** 3)).toFixed(1);
return {
total_gb: parseFloat(totalGB),
used_gb: parseFloat(usedGB),
remaining_gb: parseFloat((totalGB - usedGB).toFixed(1)),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
period: data.period,
reset_day: data.reset_day || 1,
updated_at: data.updated_at
};
} catch {
return null;
}
}
// ── 从服务器API读取配额 ─────────────────────
function readRemoteQuota() {
const host = process.env.ZY_SERVER_HOST || '';
// 验证host格式: 仅允许IP地址或域名
if (!host || !/^[\w.-]+$/.test(host)) {
console.error('⚠️ ZY_SERVER_HOST 未设置或格式无效');
return Promise.resolve(null);
}
return new Promise((resolve) => {
http.get(`http://${host}/api/proxy-sub/quota`, { timeout: 10000 }, (res) => {
let data = '';
res.on('data', (d) => { data += d; });
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch {
resolve(null);
}
});
}).on('error', () => {
resolve(null);
});
});
}
// ── 从JSON文件读取配额 ──────────────────────
function readJsonQuota(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
// ── 获取守护状态 ────────────────────────────
function getGuardianStatus() {
try {
const data = JSON.parse(fs.readFileSync('/opt/zhuyuan/proxy/data/guardian-status.json', 'utf8'));
return data.status === 'healthy' ? '✅ 在线' : '⚠️ 异常';
} catch {
return '⏳ 待部署';
}
}
// ── 生成进度条 ──────────────────────────────
function generateProgressBar(percentage) {
const total = 20;
const filled = Math.round((percentage / 100) * total);
const empty = total - filled;
const bar = '█'.repeat(filled) + '░'.repeat(empty);
let emoji = '🟢';
if (percentage >= 90) emoji = '🔴';
else if (percentage >= 80) emoji = '🟡';
return `${emoji} \`${bar}\` ${percentage}%`;
}
// ── 生成仪表盘Markdown ──────────────────────
function generateDashboard(quota, nodeStatus) {
const now = new Date();
const updatedStr = now.toISOString().slice(0, 10);
// 计算下次重置日期
const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, quota.reset_day || 1);
const daysLeft = Math.ceil((nextReset - now) / (1000 * 60 * 60 * 24));
const nextResetStr = nextReset.toISOString().slice(0, 10);
const progressBar = generateProgressBar(quota.percentage_used);
return `## 🌐 铸渊专线 · ZY-Proxy Dashboard
> 🔒 **安全说明**: 订阅链接通过邮件发送 · 仓库不存储敏感信息
| 指标 | 状态 |
|------|------|
| 📡 **节点状态** | ${nodeStatus} |
| 📊 **本月配额** | ${quota.total_gb} GB |
| 📈 **已使用** | ${quota.used_gb} GB |
| 📉 **剩余** | ${quota.remaining_gb} GB |
| 📅 **下次重置** | ${nextResetStr} (${daysLeft}天后) |
| 🔄 **数据更新** | ${updatedStr} |
${progressBar}
> 📱 支持: Shadowrocket (iOS) · Clash Verge (Mac/Win) · ClashMi (Android)
> 🛡️ 协议: VLESS + Reality · 最高级别反检测`;
}
// ── 更新README.md ────────────────────────────
function updateReadme(dashboard) {
let readme = fs.readFileSync(README_PATH, 'utf8');
const startMarker = '## 🌐 铸渊专线 · ZY-Proxy Dashboard';
const endMarker = '> 🛡️ 协议: VLESS + Reality · 最高级别反检测';
const startIdx = readme.indexOf(startMarker);
if (startIdx !== -1) {
// 替换现有仪表盘
const endIdx = readme.indexOf(endMarker, startIdx);
if (endIdx !== -1) {
const endOfLine = readme.indexOf('\n', endIdx);
readme = readme.slice(0, startIdx) + dashboard + readme.slice(endOfLine !== -1 ? endOfLine : readme.length);
}
} else {
// 在部署状态后插入仪表盘
const insertAfter = '铸渊100%主控恢复';
const insertIdx = readme.indexOf(insertAfter);
if (insertIdx !== -1) {
const nextNewline = readme.indexOf('\n', insertIdx);
readme = readme.slice(0, nextNewline + 1) + '\n---\n\n' + dashboard + '\n' + readme.slice(nextNewline + 1);
} else {
// 在部署状态部分后插入
const deploySection = '## 🚀 部署状态';
const deployIdx = readme.indexOf(deploySection);
if (deployIdx !== -1) {
const nextSection = readme.indexOf('\n---\n', deployIdx + deploySection.length);
if (nextSection !== -1) {
readme = readme.slice(0, nextSection) + '\n\n' + dashboard + '\n\n---' + readme.slice(nextSection + 4);
}
}
}
}
fs.writeFileSync(README_PATH, readme);
console.log('✅ README.md仪表盘已更新');
}
// ── 主入口 ───────────────────────────────────
async function main() {
const args = process.argv.slice(2);
let quota = null;
if (args.includes('--remote')) {
console.log('从服务器API读取配额...');
quota = await readRemoteQuota();
} else if (args.includes('--json')) {
const jsonIdx = args.indexOf('--json');
const jsonPath = args[jsonIdx + 1];
if (jsonPath) {
console.log(`从文件读取: ${jsonPath}`);
quota = readJsonQuota(jsonPath);
}
} else {
console.log('从本地文件读取配额...');
quota = readLocalQuota();
}
if (!quota) {
console.log('无法读取配额数据,使用默认值');
quota = getDefaultQuota();
}
const nodeStatus = args.includes('--remote') ? '✅ 在线' : getGuardianStatus();
const dashboard = generateDashboard(quota, nodeStatus);
updateReadme(dashboard);
}
main().catch(console.error);