// ==UserScript== // @name 中兴路由器(ZTE) 增强 // @name:en ZTE-Stat_Max // @namespace http://tampermonkey.net/ // @version 5.8.2 // @description QQ群 680464365 // @description:en https://github.com/ucxn/ZTE-Stat_Max // @author 哥哥科技 // @noframes // @include /^https?:\/\/10(\.[0-9]{1,3}){3}(:\d+)?\/.*$/ // @match http://192.168.5.1 // @include http://192.168.* // @include https://192.168.* // @include http://172.16.* // @include http://zte.home* // @grant none // @license GPL-3.0-or-later // @downloadURL https://update.greasyfork.icu/scripts/576199/%E4%B8%AD%E5%85%B4%E8%B7%AF%E7%94%B1%E5%99%A8%28ZTE%29%20%E5%A2%9E%E5%BC%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/576199/%E4%B8%AD%E5%85%B4%E8%B7%AF%E7%94%B1%E5%99%A8%28ZTE%29%20%E5%A2%9E%E5%BC%BA.meta.js // ==/UserScript== (function() { 'use strict'; function escapeHTML(str) { if (!str) return ''; return String(str).replace(/[&<>'"]/g, function(match) { return { '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[match]; }); } // ======== [0] 用户极客环境变量配置区 ======== const CONFIG = { routerIP: "192.168.5.1", // 路由器内网 IP,用于防断线保活模块的后台寻址 calcMode: 1, // 1: 上行/下行倍数模式, 0: 上行占总和比例模式 ratioExtremeUp: 10,// 极端上传判定阈值 (> 1000%) ratioWarnUp: 0.07,// 重度上传警告阈值 (> 7%) ratioExtremeDown: 0.01, // 极端下载判定阈值 (< 1%) ratioThreshold: 7, // (仅calcMode=0时有效) 上传占比报警阈值(%) portMap: { "eth1": "网口 1", "eth2": "网口 2", "eth3": "网口 3", "eth4": "网口 4", "wl0": "2.4G", "wl1": "5.2G", "wl2": "5.8G" } }; const State = { lastTime: 0, wanUpSpeed: 0, wanDownSpeed: 0, wanUpTraffic: 0, wanDownTraffic: 0, clients: {} }; let isFetching = false; const parser = new DOMParser(); // ======== [1] 换算引擎 (1000进制 vs 1024进制) ======== function speedToBps(speedStr) { if (!speedStr) return 0; let match = speedStr.match(/([\d.]+)\s*(G|M|K)?bps/i); if (!match) return 0; let val = parseFloat(match[1]); let unit = (match[2] || "").toUpperCase(); if (unit === 'G') return val * 1000000000; if (unit === 'M') return val * 1000000; if (unit === 'K') return val * 1000; return val; } function formatBps(bps) { if (bps >= 1000000) return (bps / 1000000).toFixed(3) + ' Mbps'; if (bps >= 1000) return (bps / 1000).toFixed(2) + ' Kbps'; return Math.round(bps) + ' bps'; } function formatBytes(bps) { let bytesPerSec = bps / 8; if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(3) + ' MiB/s'; if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(2) + ' KiB/s'; return Math.round(bytesPerSec) + ' B/s'; } function formatVolume(bits) { let bytes = bits / 8; if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(3) + ' GiB'; if (bytes >= 1048576) return (bytes / 1048576).toFixed(2) + ' MiB'; if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KiB'; return Math.round(bytes) + ' B'; } // 新增:双轨制流量锚定渲染 (强行将官方数据约束在我方单位下) function formatVolumeDual(bitsIntegral, bitsOfficial) { let bytes = bitsIntegral / 8; let bytesOff = bitsOfficial / 8; if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(3) + ' | ' + (bytesOff / 1073741824).toFixed(4) + ' GiB'; if (bytes >= 1048576) return (bytes / 1048576).toFixed(2) + ' | ' + (bytesOff / 1048576).toFixed(3) + ' MiB'; if (bytes >= 1024) return (bytes / 1024).toFixed(2) + ' | ' + (bytesOff / 1024).toFixed(1) + ' KiB'; return Math.round(bytes) + ' | ' + Math.round(bytesOff) + ' B';} // 需求 2.2:区间流量单字母简写引擎 (放弃对齐换取空间) function formatShortVolume(bits) { let bytes = bits / 8; if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(3) + 'G'; if (bytes >= 1048576) return (bytes / 1048576).toFixed(2) + 'M'; if (bytes >= 1024) return (bytes / 1024).toFixed(1) + 'K'; return Math.round(bytes) + 'B'; } // 核心修复点 1:使用相邻节点配对遍历,防范因缺少 ParaValue 标签导致的数组下标错位错乱 function normalizeMac(mac) { if (!mac) return ''; return mac.toLowerCase().replace(/-/g, ':').replace(/\s/g, ''); } function parseInstance(instanceNode) { let obj = Object.create(null); // 防止原型链污染 let children = instanceNode.children; for (let i = 0; i < children.length; i++) { if (children[i].tagName === "ParaName") { let key = children[i].textContent; let val = ""; let j = i + 1; while (j < children.length && children[j].tagName !== "ParaName") { if (children[j].tagName === "ParaValue") { val = children[j].textContent; i = j; // 游标直接跳跃到值的位置,提升遍历性能 break; } j++; } obj[key] = val; } } return obj; } // ======== [2] 注入极客 CSS (实现 PS 级的底部对齐) ======== const style = document.createElement('style'); style.innerHTML = ` /* 清除浮动,启用 Flex 布局控制 */ .config-item { clear: both; } .config-item-box { display: flex !important; align-items: stretch !important; padding-bottom: 12px !important; } /* 列分配与对齐 */ .config-item .logo { width: 33% !important; float: none !important; display: flex !important; flex-direction: row; } .config-item .dev-intro { flex: 1; display: flex !important; flex-direction: column; justify-content: flex-start; min-height: 100px; padding-bottom: 0 !important; margin-bottom: 0 !important; } .config-item .info { width: 27% !important; float: none !important; display: flex !important; flex-direction: column; justify-content: flex-start; padding: 0 10px !important; border-right: 1px solid #eee; } .config-item .speed { width: 40% !important; float: none !important; display: flex !important; flex-direction: column; justify-content: center; padding: 0 10px !important; } .geek-row { display: flex; justify-content: space-between; align-items: center; white-space: nowrap; height: 20px; } .geek-label { width: 110px; color: #333; font-weight: bold; } .geek-val-box { flex: 1; display: flex; gap: 15px; margin-left: 10px; } .geek-fixed-width { display: inline-block; width: 120px; } .geek-right-box { text-align: right; min-width: 220px; font-weight: bold; } .c-up { color: #ff4c00; } /* 温和红 */ .c-down { color: #0059fa; } /* 标准蓝 */ /* 核心对齐法:推到底部 */ .gege-up-box, .gege-down-box { margin-top: auto !important; margin-bottom: 0 !important; width: 95%; } .gege-ratio-box { margin-top: 10px; width: 95%; margin-bottom: 5px; } /* 细条组件 */ .t-row { font-size: 12px; font-weight: bold; margin-bottom: 2px; display: flex; justify-content: space-between; font-family: Consolas; } .zte-thin-bar { width: 100%; height: 3px; background: rgba(0,0,0,0.05); border-radius: 1.5px; overflow: hidden; } .zte-thin-bar-inner { height: 100%; transition: width 0.5s ease-out; } .zte-thin-bar-inner.up { background: #ff4c00; } .zte-thin-bar-inner.down { background: #0059fa; } /* 雷达条:左红右蓝 */ .gege-ratio-top { display: flex; justify-content: space-between; font-size: 12px; font-weight: bold; margin-bottom: 2px; } .gege-ratio-bar { width: 100%; height: 4px; background: #0059fa; border-radius: 2px; overflow: hidden; } .gege-ratio-bar-inner { height: 100%; background: #ff4c00; transition: width 0.5s ease-out; } /* 网速进度条 */ .zte-enhance-speed { display: flex; flex-direction: column; gap: 6px; width: 100%; font-family: Consolas; } .zte-bar-wrap { position: relative; width: 100%; border-radius: 4px; border: 1px solid; font-size: 13px; font-weight: bold; overflow: hidden; padding: 3px 8px; display: flex; justify-content: space-between; align-items: center; z-index: 1; box-sizing: border-box; } .zte-bar-wrap span { font-size: inherit; font-weight: inherit; } .zte-bar-up { color: #ff4c00; border-color: rgba(255, 76, 0, 0.3); } .zte-bar-down { color: #0059fa; border-color: rgba(0, 89, 250, 0.3); } .zte-bar-up::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; z-index: -1; background: rgba(255, 76, 0, 0.12); width: var(--p-up, 0%); transition: width 0.5s; } .zte-bar-down::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; z-index: -1; background: rgba(0, 89, 250, 0.12); width: var(--p-down, 0%); transition: width 0.5s; } /* 面板UI重构:消灭变色龙现象,复刻官方白底圆角灰相框 */ #config-list.gege-list-container { background-color: #ffffff !important; border-radius: 8px !important; border: 1px solid #e0e0e0 !important; padding: 20px 30px !important; box-shadow: 0 2px 10px rgba(0,0,0,0.02) !important; margin-top: 10px !important; } .gege-section { margin-bottom: 10px; } .gege-section:last-child { margin-bottom: 0; } .gege-list-container .config-title { font-size: 16px !important; font-weight: bold !important; color: #333 !important; margin: 15px 0 10px 0 !important; padding-bottom: 5px !important; } .gege-list-container .gege-section:first-child .config-title { margin-top: 0 !important; } .gege-empty-state { color: #999 !important; font-size: 14px !important; padding: 0 0 15px 5px !important; border-bottom: 1px solid #f0f0f0 !important; margin-bottom: 5px !important; } /* 内部设备条目背景改透明,解决灰白相间的问题 */ .gege-list-item { background-color: transparent !important; border-bottom: 1px solid #f0f0f0 !important; padding: 15px 10px !important; margin-bottom: 0 !important; border-radius: 0 !important; } .gege-list-item:last-child { border-bottom: none !important; } /* 看板样式完美融入到白框内部 */ #zte-geek-board { background-color: transparent !important; border-left: 4px solid #0059fa !important; border-radius: 0 !important; padding: 5px 0 5px 15px !important; margin: 10px 0 15px 0 !important; box-shadow: none !important; border-bottom: 1px solid #f0f0f0 !important; font-size: 14px; display: flex; flex-direction: column; gap: 6px; padding-bottom: 15px !important; } `; document.head.appendChild(style); // ======== [3] 核心拉取引擎 (采样) ======== async function refreshSpeedData() { if (isFetching) return; isFetching = true; try { const timestamp = new Date().getTime(); const now = performance.now(); const [wanRes, clientRes] = await Promise.all([ fetch(`/?_type=vueData&_tag=vue_home_device_data_no_update_sess&IF_OP=refresh&_=${timestamp}`), fetch(`/?_type=vueData&_tag=vue_client_data&_=${timestamp}`) ]); if (!wanRes.ok || !clientRes.ok) { console.warn("[哥哥科技] API 请求异常,跳过本次积分更新", wanRes.status, clientRes.status); return;} // 如果路由器抽风返回了非 200 的状态码(比如掉线、重启),直接放弃这秒的更新,防止脏数据污染积分 const wanXml = parser.parseFromString(await wanRes.text(), "text/xml"); const clientXml = parser.parseFromString(await clientRes.text(), "text/xml"); let wanInfo = Object.create(null); const basicInfoNode = wanXml.querySelector("OBJ_HOME_BASICINFO_ID Instance"); if (basicInfoNode) wanInfo = parseInstance(basicInfoNode); let curWanUp = speedToBps(wanInfo.WANUpRate); let curWanDown = speedToBps(wanInfo.WANDownRate); let clientsInfo = Object.create(null); let curSumUp = 0; let curSumDown = 0; const clientNodes = clientXml.querySelectorAll("OBJ_CLIENTS_ID Instance"); clientNodes.forEach(node => { let dev = parseInstance(node); if (dev.MACAddress) { let mac = normalizeMac(dev.MACAddress); let up = speedToBps(dev.UpRate); let down = speedToBps(dev.DownRate); // 同步捕获官方底层累积流量 (单位转为 bits 以统一基准) let upTp = parseFloat(dev.UpThroughput || 0) * 8000; let downTp = parseFloat(dev.DownThroughput || 0) * 8000; clientsInfo[mac] = { up: up, down: down, interface: dev.Interface || "", upTp: upTp, downTp: downTp }; curSumUp += up; curSumDown += down; } }); // [Fix 2] MAC Fingerprint Logic: Replace simple count comparison let currentMacFingerprint = Object.keys(clientsInfo).sort().join(','); let overlay = document.getElementById('gege-global-overlay'); let oldMacFingerprint = overlay ? (overlay.getAttribute('data-mac-fingerprint') || '') : ''; // Trigger reload only when physical topology actually changes if (overlay && overlay.style.display === 'block' && currentMacFingerprint !== oldMacFingerprint) { console.log(`[Gege Tech] Network topology changed, hot reloading...`); overlay.setAttribute('data-mac-fingerprint', currentMacFingerprint); buildVirtualDOM(overlay); } // 梯形积分 if (State.lastTime !== 0) { let dt = (now - State.lastTime) / 1000; State.wanUpTraffic += ((State.wanUpSpeed + curWanUp) / 2) * dt; State.wanDownTraffic += ((State.wanDownSpeed + curWanDown) / 2) * dt; for (let mac in clientsInfo) { if (!State.clients[mac]) { State.clients[mac] = { upSpeed: 0, downSpeed: 0, upTraffic: 0, downTraffic: 0, // [Fix 1] Snapshot Mode: Initialize baseline to 0 to inherit router history upBaseline: 0, downBaseline: 0, lastUpTp: clientsInfo[mac].upTp, lastDownTp: clientsInfo[mac].downTp }; } let cS = State.clients[mac]; let cC = clientsInfo[mac]; // 需求 2.3:负数基线防回流机制 (核心物理法则:只要当前值掉底,立刻吃掉区间差额转为负数基线) if (cC.upTp < cS.lastUpTp) { let intervalUp = cS.lastUpTp - cS.upBaseline; cS.upBaseline = cC.upTp - intervalUp; } if (cC.downTp < cS.lastDownTp) { let intervalDown = cS.lastDownTp - cS.downBaseline; cS.downBaseline = cC.downTp - intervalDown; } cS.lastUpTp = cC.upTp; cS.lastDownTp = cC.downTp; cS.upTraffic += ((cS.upSpeed + cC.up) / 2) * dt; cS.downTraffic += ((cS.downSpeed + cC.down) / 2) * dt; cS.upSpeed = cC.up; cS.downSpeed = cC.down; } } State.lastTime = now; State.wanUpSpeed = curWanUp; State.wanDownSpeed = curWanDown; let lanUpVol = 0, lanDownVol = 0; for (let mac in State.clients) { lanUpVol += State.clients[mac].upTraffic; lanDownVol += State.clients[mac].downTraffic; } renderUI(curWanUp, curWanDown, curSumUp, curSumDown, lanUpVol, lanDownVol, clientsInfo); } catch (e) { console.error(e); } finally { isFetching = false; } } // ======== [4] 渲染层 (完美对齐逻辑) ======== function renderUI(wanUp, wanDown, sumUp, sumDown, lanUpVol, lanDownVol, clientsInfo) { // [新增] 预先计算官方高精流量的全局统计值 (供后续所有计算做分母使用) let totalIntUp = 0, totalIntDown = 0; let totalAbsUp = 0, totalAbsDown = 0; for (let m in clientsInfo) { let cC = clientsInfo[m]; let cState = State.clients[m]; let baseU = cState ? (cState.upBaseline || 0) : 0; let baseD = cState ? (cState.downBaseline || 0) : 0; totalIntUp += Math.max(0, (cC.upTp || 0) - baseU); totalIntDown += Math.max(0, (cC.downTp || 0) - baseD); totalAbsUp += (cC.upTp || 0); totalAbsDown += (cC.downTp || 0); } // 看板渲染 let main = document.querySelector('.el-table') || document.querySelector('.config-item')?.closest('div') || document.querySelector('.main-content'); if (main) { let board = document.getElementById('zte-geek-board'); if (!board) { board = document.createElement('div'); board.id = 'zte-geek-board'; board.innerHTML = `