guanghulab/server/proxy/dashboard/update-dashboard.js

226 lines
8.0 KiB
JavaScript
Raw Normal View History

#!/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);