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);
|