226 lines
8.0 KiB
JavaScript
226 lines
8.0 KiB
JavaScript
#!/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);
|