#!/usr/bin/env node // ═══════════════════════════════════════════════ // 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 // 📜 Copyright: 国作登字-2026-A-00037559 // ═══════════════════════════════════════════════ // server/proxy/service/traffic-monitor.js // 📊 铸渊专线 · 流量监控Agent // // ⚠️ [DEPRECATED · D58] V1节点已停用 // 铸渊专线2.0已测试通过并正式启用,V1节点不再使用。 // V2流量监控: traffic-monitor-v2.js (共享流量池2000GB/月) // 此文件保留作为V1历史参考,不再部署运行。 // // 定期查询Xray Stats API获取流量数据 // 更新quota-status.json供订阅服务和仪表盘使用 // // 运行方式: PM2 managed (zy-proxy-monitor) // 检查间隔: 每5分钟 // ═══════════════════════════════════════════════ 'use strict'; const { execSync, execFileSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const DATA_DIR = process.env.ZY_PROXY_DATA_DIR || '/opt/zhuyuan/proxy/data'; const QUOTA_FILE = path.join(DATA_DIR, 'quota-status.json'); const HISTORY_FILE = path.join(DATA_DIR, 'traffic-history.json'); const XRAY_API_HOST = '127.0.0.1'; const XRAY_API_PORT = 10085; const CHECK_INTERVAL = 5 * 60 * 1000; // 5分钟 const MONTHLY_QUOTA = 500 * 1024 * 1024 * 1024; // 500GB in bytes // ── 确保数据目录存在 ──────────────────────── function ensureDataDir() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } } // ── 查询Xray流量统计 ──────────────────────── function queryXrayStats() { try { const result = execSync( `xray api statsquery --server=${XRAY_API_HOST}:${XRAY_API_PORT} -pattern ""`, { encoding: 'utf8', timeout: 10000 } ); return JSON.parse(result); } catch (err) { console.error('[流量监控] Xray Stats API查询失败:', err.message); return null; } } // ── 解析统计数据 ───────────────────────────── function parseStats(stats) { if (!stats || !stats.stat) return { upload: 0, download: 0 }; let upload = 0; let download = 0; for (const item of stats.stat) { if (item.name && item.name.includes('uplink')) { upload += parseInt(item.value || '0', 10); } if (item.name && item.name.includes('downlink')) { download += parseInt(item.value || '0', 10); } } return { upload, download }; } // ── 读取当前配额状态 ──────────────────────── function readQuotaStatus() { try { return JSON.parse(fs.readFileSync(QUOTA_FILE, 'utf8')); } catch { const now = new Date(); return { total_bytes: MONTHLY_QUOTA, upload_bytes: 0, download_bytes: 0, used_bytes: 0, reset_day: 1, period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, created_at: now.toISOString(), updated_at: now.toISOString(), alerts_sent: { p80: false, p90: false, p100: false } }; } } // ── 检查是否需要月度重置 ───────────────────── function checkMonthlyReset(quota) { const now = new Date(); const currentPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; if (quota.period !== currentPeriod) { console.log(`[流量监控] 月度重置: ${quota.period} → ${currentPeriod}`); // 保存上月记录到历史 appendHistory(quota); // 重置Xray统计 try { execSync( `xray api statsquery --server=${XRAY_API_HOST}:${XRAY_API_PORT} -pattern "" -reset`, { encoding: 'utf8', timeout: 10000 } ); } catch { console.error('[流量监控] Xray统计重置失败'); } return { ...quota, upload_bytes: 0, download_bytes: 0, used_bytes: 0, period: currentPeriod, updated_at: now.toISOString(), alerts_sent: { p80: false, p90: false, p100: false } }; } return quota; } // ── 追加历史记录 ───────────────────────────── function appendHistory(quota) { let history = []; try { history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8')); } catch { /* ignore */ } history.push({ period: quota.period, upload_bytes: quota.upload_bytes, download_bytes: quota.download_bytes, total_used: quota.upload_bytes + quota.download_bytes, total_quota: quota.total_bytes, recorded_at: new Date().toISOString() }); // 只保留最近12个月 if (history.length > 12) { history = history.slice(-12); } fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2)); } // ── 保存配额状态 ───────────────────────────── function saveQuotaStatus(quota) { fs.writeFileSync(QUOTA_FILE, JSON.stringify(quota, null, 2)); } // ── 检查配额告警 ───────────────────────────── function checkAlerts(quota) { const usedPercent = ((quota.upload_bytes + quota.download_bytes) / quota.total_bytes) * 100; const alerts = []; if (usedPercent >= 100 && !quota.alerts_sent.p100) { alerts.push({ level: 'critical', percent: 100, message: '⚠️ 本月500GB配额已用完!等待下月重置。' }); quota.alerts_sent.p100 = true; } else if (usedPercent >= 90 && !quota.alerts_sent.p90) { alerts.push({ level: 'warning', percent: 90, message: '⚠️ 本月配额已使用90%,请注意用量。' }); quota.alerts_sent.p90 = true; } else if (usedPercent >= 80 && !quota.alerts_sent.p80) { alerts.push({ level: 'info', percent: 80, message: '📊 本月配额已使用80%。' }); quota.alerts_sent.p80 = true; } return alerts; } // ── 发送告警邮件 ───────────────────────────── async function sendAlert(alert, quota) { try { const sendScript = path.join(__dirname, 'send-subscription.js'); const usedGB = ((quota.upload_bytes + quota.download_bytes) / (1024 ** 3)).toFixed(2); const totalGB = (quota.total_bytes / (1024 ** 3)).toFixed(0); // 使用execFileSync避免Shell命令注入 (不经过shell解释器) execFileSync('node', [sendScript, 'alert', `${alert.message} 已用 ${usedGB}GB / ${totalGB}GB`], { encoding: 'utf8', timeout: 30000 }); console.log(`[流量监控] 告警邮件已发送: ${alert.level}`); } catch (err) { console.error('[流量监控] 告警邮件发送失败:', err.message); } } // ── 主循环 ─────────────────────────────────── async function monitor() { console.log('[流量监控] 开始检查...'); ensureDataDir(); // 读取当前状态 let quota = readQuotaStatus(); // 检查月度重置 quota = checkMonthlyReset(quota); // 查询Xray统计 const stats = queryXrayStats(); if (stats) { const { upload, download } = parseStats(stats); quota.upload_bytes = upload; quota.download_bytes = download; quota.used_bytes = upload + download; quota.updated_at = new Date().toISOString(); const usedGB = ((upload + download) / (1024 ** 3)).toFixed(2); const totalGB = (quota.total_bytes / (1024 ** 3)).toFixed(0); const pct = ((quota.used_bytes / quota.total_bytes) * 100).toFixed(1); console.log(`[流量监控] 已用: ${usedGB}GB / ${totalGB}GB (${pct}%)`); } else { quota.updated_at = new Date().toISOString(); console.log('[流量监控] Xray统计暂不可用,保持上次数据'); } // 检查告警 const alerts = checkAlerts(quota); for (const alert of alerts) { await sendAlert(alert, quota); } // 保存状态 saveQuotaStatus(quota); console.log('[流量监控] 检查完成'); } // ── 启动监控循环 ───────────────────────────── console.log('📊 铸渊专线流量监控启动'); console.log(` 检查间隔: ${CHECK_INTERVAL / 1000}秒`); console.log(` 月配额: ${MONTHLY_QUOTA / (1024 ** 3)}GB`); console.log(` 数据目录: ${DATA_DIR}`); // 立即执行一次 monitor().catch(console.error); // 定期执行 setInterval(() => { monitor().catch(console.error); }, CHECK_INTERVAL);