// ==UserScript== // @name 锐捷睿易路由器增强 Reyee 网页 增强 脚本 插件 网速 流量 连接数 延迟 数学统计 分析 // @name:en Bro-Stat_Ruijie-Reyee-Stat_Max // @namespace ucxn // @version 5.9.9.R // @description 适用于锐捷(睿易 Reyee)路由器的逐设备网速流量统计、连接数、外网延迟曲线、流量一键导出的路由器Web网页后台增强利器 // @copyright 哥哥科技. All Rights Reserved. 使用必要条件之一:不得删除源码、用户界面、文本中的任何“哥哥”;传播官方链接不限;除安装自用外任何使用传输均需查看许可证。 // @description:en A web admin enhancement for Ruijie / Reyee routers: per-device speed and traffic statistics, connection counts, WAN latency charting and one-click traffic export. // @author 哥哥科技 space.bilibili.com/501430041 // @noframes // @tag 路由器 锐捷 睿易 Reyee 网络 监控 统计 数据 可视化 极客 增强 UI HA 智能 定时 后台 消费级 硬路由 家用 流量 RSSI WiFi 测控 算法 // @icon https://scriptcat.org/api/v2/resource/image/duygQktL5QjWtkLc // @match *://*/cgi-bin/luci* // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @storageName GBNPA_Storage // @source https://github.com/ucxn/Bro-Stat // @supportURL https://www.bilibili.com/video/BV19ZpB64E6D // @license 哥哥软件-显著署名-哥哥科技-严格 + (完整使用-署名保留-完全 OR (部分摘录-禁止演绎-署名保护-显著-哥哥科技 OR 二次开发-署名保留/保护-显著署名-署名哥哥-署名追加) AND 相同方式共享) // @note 发行版作者哥哥科技保留所有权利,源码本身公开,请前往 GitHub Source 区域获得授权部分 // @run-at document-start // ==/UserScript== (function () { 'use strict'; if (!/^\/cgi-bin\/luci/.test(location.pathname)) return; const _w = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; console.log("🚀 哥哥科技 V5.9.9 引擎已装载..."); // ======== [0] 用户极客环境变量配置区 ======== const CONFIG = { readSaveData: 1, // 【历史记录】 1: 从路由器后台读档 | 0: 新局模式 | 2: 从本地长期历史读档 | 3: 启动时用后台临时值充当积分 uiLayout: 1, // 【面板拓扑结构】 0: 经典版 | 1: 详细紧凑版(驾驶舱美学) | 2: 详细平铺版(报表流美学) calcMode: 1, // 1: 上行/下行倍数模式, 0: 上行占总和比例模式 ratioExtremeUp: 10, // 极端上传判定阈值 (> 1000%) ratioWarnUp: 0.07, // 重度上传警告阈值 (> 7%) ratioExtremeDown: 0.01, // 极端下载判定阈值 (< 1%) ratioThreshold: 7, // (仅calcMode=0时有效) 上传占比报警阈值(%) lanRefreshInterval: 1, // LAN口刷新时间(秒)。锐捷后台每 1 秒刷新一次,更快只会拿到重复值 wanRefreshInterval: 1, // WAN口刷新时间(秒),同上 宽带最大外网上行速率: 3e8, 宽带最大外网下行速率: 24e8, // 配置外网最大上传|下载比特(bit/bps)速率,请略微大于真实值;500兆为5e8,一千兆1e9 盲漫游: undefined, //也就是无线交换机(AP/有线桥接)模式,无线设备被主路由识别为有线设备则设置1 周期类型: 'M', // 'M'(每月), 'W'(每周), 'D'(固定天数), 其它任意字符:不开启周期重置+自动导出功能 周_天设置: 1, // M: 1~31号; W: 0~6(周日~周六); D: 间隔天数(如 7) 基准日期: '2026-11-16', // 原点时间(仅 D 模式有效) 任意一个历史周期的零点 报告时间: -540, // 提示时间:相对周期0点的偏移分钟数。(如 -4320 代表提前 3 天) 设置相对指定日期的下个周期起点的时间偏移量 自动导出: 0, // 强制导出:相对周期0点的偏移分钟数。(如 W模式+锚点6(周六)+偏移-180 = 周五 21:00 强制导出清零) 时区补偿: 28800000, // 默认 UTC+8 时区补偿量。 portMap: { "有线": "有线", "2.4G": "2.4G", "5G": "5.2G", "6G": "6G", "MLO": "MLO" } }; const S = { wInstUp: 0, wInstDn: 0, wTotUp: 0, wTotDn: 0, cls: {}, isPinned: !0, w2U: 0, w2D: 0, w2TotUp: 0, w2TotDn: 0, w2LT: undefined, hasW2: !1, fI: 0, _domRebuilt: !1, _lastPanelState: null, oDC: null, Warn_MS: 0, Force_MS: 0, _RST: !1, aWu: 0, aWd: 0, lwTU: 0, lwTD: 0, cSnap: null, lTotUp: 0, lTotDn: 0, lInstU: 0, lInstD: 0, lLT: undefined, 总上行图: new Float64Array(8192), 总下行图: new Float64Array(8192), 总图点数: 0, wMaxU: 0, wMaxD: 0, wMinU: Infinity, wMinD: Infinity, 图表拖: null, 图表待画: 0 }; // 锐捷专属状态:auth 与 ping 均由页面自身请求被动截获;计数器按字节自行累计 const 锐 = { auth: null, ov: null, 错误: '', 计数: Object.create(null), 连接比: 0, 射频: null, 射频取中: !1, 信道文: '', 信道24: 0, 信道5: 0, 衰减: 0, 延迟: null, 丢包: 0, 延迟时刻: 0, 延迟图: new Float64Array(1024), 延迟点数: 0 }; // 信道变化时一次性算好:5G 面板文案(≥149 为 5.8G,否则 5.2G)与 2.4G 的 RSSI 频率修正(dB) function 锐应用信道(c24, c5) { if (c24 === 锐.信道24 && c5 === 锐.信道5) return; 锐.信道24 = c24; 锐.信道5 = c5; 锐.衰减 = 20 * Math.log10((5000 + 5 * c5) / (2407 + 5 * c24)); CONFIG.portMap['5G'] = c5 > 148 ? '5.8G' : '5.2G'; window.gegeForceUIRedraw = !0; } 锐应用信道(6, 60); // 读到真实信道之前的占位 // 状态行「射频」里 5G 频段的显示文案:按信道查表(仅此处使用;面板分组标题仍走 portMap 的 5.2G/5.8G 二分) const 锐频段 = {32:'5.1G',36:'5.2G',40:'5.2G',44:'5.2G',48:'5.2G',52:'5.2G',56:'5.3G',60:'5.3G',64:'5.3G',100:'5.5G',104:'5.5G',108:'5.5G',112:'5.5G',116:'5.5G',120:'5.6G',124:'5.6G',128:'5.6G',132:'5.6G',136:'5.6G',140:'5.7G',144:'5.7G',149:'5.8G',153:'5.8G',157:'5.8G',161:'5.8G',165:'5.8G',184:'4.9G',188:'4.9G',192:'4.9G',196:'4.9G'}; // ======== [1] 被动截获:auth(任意 /api/ 请求)与外网延迟(概览页每秒一次的 ping) ======== function 收延迟(txt) { const j = JSON.parse(txt), d = typeof j.data === 'string' ? JSON.parse(j.data) : j.data, p = d && d.ping; if (!p || !isFinite(+p.avg)) return; const v = +p.avg; if (p.time && p.time === 锐.延迟时刻 && 锐.延迟点数 > 0) 锐.延迟图[(锐.延迟点数 - 1) & 1023] = v; // 同一秒重复上报:覆盖不追加 else { 锐.延迟图[锐.延迟点数 & 1023] = v; 锐.延迟点数++; } 锐.延迟 = v; 锐.丢包 = +p.loss || 0; 锐.延迟时刻 = p.time || 0; const ol = document.getElementById('gege-global-overlay'); if (ol && ol.style.display === 'block') 画锐行(); } (() => { const XP = _w.XMLHttpRequest && _w.XMLHttpRequest.prototype; if (!XP) return; const oOpen = XP.open, oSend = XP.send; XP.open = function (m, u) { try { const s = String(u), k = /[?&]auth=([0-9a-f]+)/.exec(s); if (k) { const 首次 = !锐.auth; 锐.auth = k[1]; if (首次) setTimeout(锐取射频); } // 网页打开后读取一次射频信道 this.__gegeU = s; } catch (e) { console.warn(e); } return oOpen.apply(this, arguments); }; XP.send = function (b) { if (typeof b === 'string' && b.indexOf('"method":"ping"') !== -1 && (this.__gegeU || '').indexOf('/api/system') !== -1) { this.addEventListener('load', function () { try { 收延迟(this.responseText); } catch (e) { console.warn('[Ruijie] ping 响应无效', e); } }); } return oSend.apply(this, arguments); }; })(); // ======== [2] 主动取数:请求体与签名必须逐字节一致(签名只与请求体有关,与会话无关) ======== const 锐请求 = { ov: ['{"method":"devSta.get","params":{"module":"overview","noParse":false,"async":null,"remoteIp":false,"device":"pc"}}', '0a02ec86281388dc9c23987f67609885', 'e29be7fc26168f4c0addfa6146da910d'], ul: ['{"method":"cmdArr","params":{"device":"pc","params":[{"method":"devSta.get","params":{"module":"user_list","noParse":true,"async":null,"remoteIp":false,"data":{"devType":"all","dataType":"timely"}}},{"method":"acConfig.get","params":{"module":"wirelessMacFilter","noParse":false,"async":null,"remoteIp":false}}]}}', 'b3011dc5550b351d6ef7935602aa6b40', '23241979c5f2f63c22b3d59ec531e44e'], // 官方页面「设备列表」同款请求:user_list+flow+port_status+ap_list,仅取第 4 项 ap_list(射频信道/占用/底噪),用不到的几项不解析 ap: ['{"method":"cmdArr","params":{"device":"pc","params":[{"method":"devSta.get","params":{"module":"user_list","noParse":true,"async":null,"remoteIp":false,"data":{"devType":"all","dataType":"timely"}}},{"method":"devSta.get","params":{"module":"flow","noParse":true,"async":null,"remoteIp":false,"data":{"func":"interface_info"}}},{"method":"devSta.get","params":{"module":"port_status","noParse":true,"async":null,"remoteIp":false}},{"method":"devSta.get","params":{"module":"ap_list","noParse":true,"async":null,"remoteIp":false}}]}}', '0cfc3dc6fb02046b214b396eb0b20b71', 'a6daf4986b6f0869804ee5587d7e6d2b'] }; async function 锐取(k) { const [body, ca, csa] = 锐请求[k]; const r = await fetch(`/cgi-bin/luci/api/cmd?auth=${锐.auth}`, { method: 'POST', credentials: 'same-origin', body, headers: { 'Content-Type': 'application/json;charset=UTF-8', 'Content-Accept': ca, 'Contents-Accept': csa } }); if (!r.ok) throw new Error('HTTP ' + r.status); const j = await r.json(); if (j.code !== 0) throw new Error('code ' + j.code); return j.data; } // 射频信道:只在「网页打开后」与「每次点开面板」各读一次,不进 1 秒循环。channel 形如 "auto-6,auto-60",与 band/chutil/channf 按逗号逐项对应 async function 锐取射频() { if (!锐.auth || 锐.射频取中) return; 锐.射频取中 = !0; try { const a = (await 锐取('ap'))[3]?.list?.[0]; if (!a) return; const 拆 = s => String(s || '').split(','), 取信道 = s => +s.slice(s.lastIndexOf('-') + 1), // "auto-60"→60;"36"→36 ch = 拆(a.channel), u = 拆(a.chutil), r = {}; 拆(a.band).forEach((k, i) => { r[k] = { ch: 取信道(ch[i]), util: +u[i] }; }); 锐应用信道(r['2.4G']?.ch || 锐.信道24, r['5G']?.ch || 锐.信道5); 锐.射频 = r; 锐.信道文 = Object.entries(r).map(([k, v]) => `${(k === '5G' && 锐频段[v.ch]) || CONFIG.portMap[k] || k} 信道${v.ch} 占用${v.util}%`).join('|'); } catch (e) { console.warn('[Ruijie] 读取射频信道失败,沿用客户端信道', e); } finally { 锐.射频取中 = !1; } } // 锐捷设备计数器(字节,字符串):绝对值偶发整 ±2^32 跳变(低 32 位被当作有符号数扩展),只信相邻差值。 // 差值先模 2^32 再折算到 ±2^31;负值或超出物理上限视为掉线重连/归零,交给计数回退逻辑处理。 function 锐计数(key, s, now) { let v; try { v = BigInt(s || 0); } catch (e) { return 锐.计数[key]?.acc ?? 0; } const 绝对 = () => { let x = v >= 9223372036854775808n ? v - 18446744073709551616n : v; if (x < 0n) x += 4294967296n; return Number(x) * 8; }; const c = 锐.计数[key]; if (!c) return (锐.计数[key] = { v, acc: 绝对(), t: now }).acc; if (v === c.v) return c.acc; let d = Number((v - c.v) & 0xFFFFFFFFn); if (d >= 2147483648) d -= 4294967296; if (d < 0 || d > 3.125e8 * Math.max(1, (now - c.t) * 0.001) + 67108864) c.acc = 绝对(); // 上限:2.5Gbps × 间隔 + 64MiB else c.acc += d * 8; c.v = v; c.t = now; return c.acc; } function 锐在线秒(t) { if (!t) return 0; const ms = new Date(String(t).replace(' ', 'T')).getTime(); return isFinite(ms) ? Math.max(0, (Date.now() - ms) / 1000) : 0; } S.calcTime = (L) => { S.Force_MS = (CONFIG.周期类型 === 'M' ? Date.UTC(new Date(L).getUTCFullYear(), new Date(L).getUTCMonth() + (L >= Date.UTC(new Date(L).getUTCFullYear(), new Date(L).getUTCMonth(), CONFIG.周_天设置) ? 1 : 0), CONFIG.周_天设置) : (CONFIG.周期类型 === 'W' ? Date.UTC(new Date(L).getUTCFullYear(), new Date(L).getUTCMonth(), new Date(L).getUTCDate()) + ((CONFIG.周_天设置 - new Date(L).getUTCDay() > 0 ? CONFIG.周_天设置 - new Date(L).getUTCDay() : CONFIG.周_天设置 - new Date(L).getUTCDay() + 7) * 86400000) : (CONFIG.周期类型 === 'D' ? Date.parse(CONFIG.基准日期 + 'T00:00:00Z') + (Math.floor((L - Date.parse(CONFIG.基准日期 + 'T00:00:00Z')) / (CONFIG.周_天设置 * 86400000)) + 1) * CONFIG.周_天设置 * 86400000 : Infinity))) - CONFIG.时区补偿; S.Warn_MS = S.Force_MS + CONFIG.报告时间 * 60000; S.Force_MS += CONFIG.自动导出 * 60000; };S.calcTime((typeof GM_getValue !== 'undefined' && GM_getValue('gege_reset_ms')) ? (GM_getValue('gege_reset_ms') + CONFIG.时区补偿) : Date.now() + CONFIG.时区补偿); if (typeof GM_getValue !== 'undefined' && GM_getValue('gege_reset_ms') >= Math.min(S.Warn_MS, S.Force_MS)) S.calcTime(S.Force_MS - CONFIG.自动导出 * 60000 + 1000 + CONFIG.时区补偿); const ESC_MAP = { '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }; function escapeHTML(str) { return str ? String(str).replace(/[&<>'"]/g, m => ESC_MAP[m]) : ''; } let isF = !1; const 版本号 = (typeof GM_info !== 'undefined' && GM_info.script?.version) || '环境不支持获取版本号'; // 锐捷版:最小单位固定为 kbps / kB/s(不降到 bps),小数位数不设上限、取到除尽为止。 // 整数 bit/s 除以 1000 或 8000 必是有限小数(≤3 / ≤6 位):单次除法是 IEEE 正确舍入,Number 转字符串又是“最短往返表示”,有限小数会原样打印、尾零自然消失。 // 注意必须用除法:0.000125 在二进制里存不准,乘出来差 1ulp,会打印成 0.0011250000000000001(穷举 0~8388607 有 14% 中招)。 function fB(bps) { if (bps > 1e9) return `${(bps * 1e-6).toFixed(1)} Mbit/s`; if (bps > 1e6) return `${(bps * 1e-6).toFixed(2)} Mbps`; return `${Math.round(bps) / 1000} kbps`; // 先取整:均值等非整数 bit/s 否则会打印出一长串 } function fBy(bps) { if (bps === 0.1) return '智能拦截中...'; if (bps === 0.2) return '漫游中...'; if (bps === 0.3) return '异常网速!'; // 必须大于0的数字,否则数据异常会影响积分计算和数值类型 return bps > 8388607 ? `${(bps * 1.1920928955078125e-7).toFixed(2)} MiB/s` : `${Math.round(bps) / 8000} kB/s`; } function fV(bits) { if (bits > 8589934592) return `${(bits / 8589934592).toFixed(4)} GiB`; if (bits > 8388608) return `${(bits / 8388608).toFixed(4)} MiB`; if (bits > 8192) return `${(bits / 8192).toFixed(2)} KiB`; return `${Math.round(bits / 8)} B`; } function fVD(bitsIntegral, bitsOfficial) { if (bitsIntegral > 8796093022208) return `${(bitsOfficial / 8796093022208).toFixed(4)} | ${(bitsIntegral / 8796093022208).toFixed(4)} TiB`; if (bitsIntegral >= 83886080000) return `${(bitsOfficial / 8589934592).toPrecision(5)} | ${(bitsIntegral / 8589934592).toPrecision(5)} GiB`; if (bitsIntegral > 8388608000) return `${(bitsOfficial / 8388608).toFixed(3)} | ${(bitsIntegral / 8388608).toFixed(3)} MiB`; if (bitsIntegral > 8388608) return `${(bitsOfficial / 8388608).toFixed(3)} | ${(bitsIntegral / 8388608).toFixed(3)} MiB`; if (bitsIntegral > 8192) return `${(bitsOfficial / 8192).toFixed(2)} | ${(bitsIntegral / 8192).toFixed(2)} KiB`; return `${Math.round(bitsOfficial / 8)} | ${Math.round(bitsIntegral / 8)} B`;} function fSV(bits) { if (bits >= 84607500288) return `${(bits / 8589934592).toPrecision(4)}G`; if (bits > 8388608000) return `${Math.round(bits / 8388608)}M`; if (bits > 8388608) return `${(bits / 8388608).toPrecision(4)}M`; if (bits >= 8192) return `${(bits / 8192).toFixed(1)}K`; return `${Math.round(bits / 8)}B`;} function fOT(totalSec) { totalSec = totalSec | 0; if (totalSec < 0) return ""; const d = (totalSec / 86400) | 0; let r = totalSec - d * 86400; const h = (r / 3600) | 0; r = r - h * 3600; const m = (r / 60) | 0; const s = r - m * 60; return d > 0 ? `${d}天${h}时${m}分${s}秒` : `${h}小时${m}分${s}秒`;} function nM(m) { return m ? m.toLowerCase().replace(/-/g, ':').replace(/\s/g, '') : ''; } function 挂样式() { const st = document.createElement('style'); st.innerHTML = `.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:20px;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:system-ui, sans-serif;}.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:system-ui, sans-serif;} .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;}#config-list.gege-list-container{contain:content!important;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{contain:layout style;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;}#gege-global-overlay #zte-geek-board.geek-frozen-pane{position:sticky!important;top:0px!important;z-index:100!important;background-color:#f3f4f5!important;margin-top:0!important;padding-top:15px!important;box-shadow:0 10px 15px -3px rgba(0,0,0,0.05)!important;border-radius:0 0 8px 8px!important;}.gege-pin{cursor:pointer;font-size:11px;filter:grayscale(100%);opacity:0.5;transition:transform 0.2s;margin-left:2px;} .gege-pin.active{filter:none;opacity:1;transform:scale(1.1);}#gege-global-overlay{position:fixed;top:7.5%;right:0;bottom:0;background:#f3f4f5;z-index:9999;overflow-y:auto;padding-bottom:50px;left:0!important;border-radius:16px 16px 0 0;box-shadow:0 -5px 25px rgba(0,0,0,0.15);transition:top 0.3s ease;}@media (max-width: 768px){.geek-right-box:has(#gb-wan-zero-up),.geek-right-box:has(#gb-cur-up-vol){display:none!important}.gege-list-item{padding:12px 10px!important;position:relative!important}.config-item-box{flex-direction:column!important;padding-bottom:0!important}.config-item .info,.config-item .logo,.config-item .speed{width:100%!important;border:none!important;padding:0!important;position:static!important}.config-item .dev-intro{min-height:auto!important;justify-content:center!important;padding-right:90px!important}.config-item .logo{padding-bottom:4px!important}.config-item .info{flex-direction:column!important;margin:0 0 6px 0!important;gap:2px!important}.dev-ip{position:absolute!important;top:12px!important;right:10px!important;font-size:11px!important;background:rgba(0,89,250,0.08);color:#0059fa!important;padding:2px 6px!important;border-radius:4px;font-weight:bold;line-height:1.2;z-index:10;width:auto!important}.dev-number{width:auto!important;margin:0!important;font-size:11px!important}.gege-ratio-box{width:100%!important;margin-top:2px!important;margin-bottom:0!important}.gege-down-box{width:100%!important;margin-top:2px!important}#zte-geek-board{padding:8px!important;gap:0!important;font-size:11.5px!important}.geek-row{height:auto!important;flex-wrap:wrap!important;margin-bottom:4px!important;justify-content:flex-start!important;gap:2px 6px!important;line-height:1.3!important}.geek-label{width:auto!important;min-width:60px!important;font-size:11.5px!important;flex:0 0 auto!important}.geek-val-box{width:auto!important;flex:1 1 0%!important;display:flex!important;flex-wrap:wrap!important;margin-left:0!important;gap:2px 6px!important}.geek-fixed-width{width:auto!important}.geek-right-box{width:100%!important;flex:0 0 100%!important;text-align:left!important;font-size:11.5px!important;margin-top:2px!important;margin-left:0!important}.gege-list-container{padding:8px!important}.zte-enhance-speed{gap:4px!important}}#zte-geek-board{position:relative!important;overflow:visible!important;}#gege-speed-chart{position:absolute;z-index:25;box-sizing:border-box;cursor:move;user-select:none;touch-action:none;container-type:inline-size;}#gege-speed-chart canvas{display:block;width:100%;height:100%;}.gege-chart-head,.gege-chart-foot{position:absolute;left:clamp(28px,8%,36px);right:8px;display:flex;align-items:center;pointer-events:none;font:bold clamp(9px,2.1cqw,11px) system-ui,sans-serif;white-space:nowrap;overflow:hidden;}.gege-chart-head{top:2px;color:#111;gap:8px;}.gege-chart-head [data-gc="range"]{color:#666;font-weight:normal;overflow:hidden;text-overflow:ellipsis;margin-left:auto;}.gege-chart-foot{bottom:4px;justify-content:flex-start;gap:clamp(3px,1.1cqw,12px);}.gege-chart-foot span{min-width:0;overflow:hidden;text-overflow:ellipsis;}.gc-up{color:#ff4c00;flex:0 1 auto;}.gc-down{color:#0b5;flex:0 1 auto;}.gc-extra{color:#666;text-align:right;margin-left:auto;flex:1 1 0;min-width:0;}#gege-speed-chart .gege-chart-resize{position:absolute;right:-4px;bottom:-4px;width:13px;height:13px;border-right:3px solid #0059fa;border-bottom:3px solid #0059fa;cursor:nwse-resize;border-radius:2px;} .t-row,.zte-enhance-speed{font-family:Consolas;} @media (max-width:768px){.config-item-box{position:relative!important}.dev-ip{top:0!important;right:0!important}} #gege-global-overlay{color:#333;} .gege-conn-box{width:95%;margin-top:6px;}.gege-conn-box .gege-ratio-top{color:#7c3aed;}.gege-conn-bar{width:100%;height:4px;background:rgba(124,58,237,.14);border-radius:2px;overflow:hidden;}.gege-conn-bar-inner{height:100%;background:#7c3aed;transition:width 0.5s ease-out;} #gb-rj-row .geek-val-box{gap:14px;align-items:center;flex-wrap:wrap;font-weight:normal;} `;(document.head || document.documentElement).appendChild(st); } window.gegeRenderedMacs = new Set(); async function rSD(ovD, ulD, now) { if (isF) return; isF = !0; try { const wanNow = now, lanNow = now; const wanValid = !!ovD && typeof ovD === 'object' && 'up_rate' in ovD && 'down_rate' in ovD; let cWU = S.wInstUp, cWD = S.wInstDn, cI = Object.create(null); if (wanValid) { 锐.ov = ovD; S.hasW2 = !1; cWU = +ovD.up_rate || 0; // 锐捷 overview 速率单位即 bit/s cWD = +ovD.down_rate || 0; 记总速率图(cWU, cWD); } let cSU = 0, cSD = 0, cTot = 0; const hostList = Array.isArray(ulD) ? ulD[0]?.list : null, lanValid = Array.isArray(hostList); if (lanValid) { if (!锐.射频) { // ap_list 尚未读到时,用客户端自带的信道顶替 let c24 = 锐.信道24, c5 = 锐.信道5; for (const d of hostList) if (+d.channel) { if (d.band === '5G') c5 = +d.channel; else if (d.band === '2.4G') c24 = +d.channel; } 锐应用信道(c24, c5); } hostList.forEach(d => { let m = nM(d.mac); if (!m) return; const 有线 = d.connectType !== 'wireless', u = +d.flowUp || 0, // flowUp/flowDown 单位 bit/s,逐秒等于计数器增量 ×8 dn = +d.flowDown || 0; let hn = d.hostName && d.hostName !== '*' ? d.hostName : ''; cI[m] = { upRate: u, dnRate: dn, iface: 有线 ? '有线' : (d.mlo ? 'MLO' : (d.band || '无线')), offUp: 锐计数(m + 'u', d.up, lanNow), offDn: 锐计数(m + 'd', d.down, lanNow), onSec: 锐在线秒(d.onlinetime), name: d.name || hn || ("未知设备·" + m.slice(-5)), ip: d.userIp || "", rssi: 有线 ? 0 : (+d.rssi || 0), rate: parseInt(d.rxrate) || 0, conn: +d.flow_cnt || 0, ssid: d.ssid || "" }; cSU += u; cSD += dn; cTot += cI[m].conn; }); 锐.连接比 = cTot > 0 ? 100 / cTot : 0; // 每台设备连接数占全部设备连接数之和的百分比系数 } if (lanValid) S.lastCI = cI; else { cI = S.lastCI || Object.create(null); for (const d of Object.values(cI)) { cSU += d.upRate || 0; cSD += d.dnRate || 0; } } let ol = document.getElementById('gege-global-overlay'), cM = Object.keys( cI), iD = lanValid && (window.gegeForceUIRedraw || (cM.length !== window.gegeRenderedMacs.size)); if (!iD && cM.length > 0) { for (let i = 0; i < cM.length; i++) { if (!window.gegeRenderedMacs.has(cM[i])) { iD = !0; break; } } } if (iD) { for (let m in S.cls) if (!cI[m]) { let ms = lanNow - S.cls[m].lUT; if (ms > CONFIG.lanRefreshInterval * 1000) ms = CONFIG.lanRefreshInterval * 1000; S.cls[m].intUp += S.cls[m].upR * ms * 0.0005; S.cls[m].intDn += S.cls[m].dnR * ms * 0.0005; S.cls[m].upR = S.cls[m].dnR = 0; } } if (ol && ol.style.display === 'block' && (iD || !ol.querySelector('.gege-list-item'))) { bVD(ol, cI); // [解耦] 将洗净去重后的 cI 字典传给画板 window.gegeRenderedMacs = new Set( cM); window.gegeForceUIRedraw = !1; } if (wanValid && S.wLT === undefined) { S.wLT = wanNow; } else if (wanValid && (cWU !== S.wInstUp || cWD !== S.wInstDn)) { const wDt = wanNow - S.wLT; if (S.wInstUp > 0) { S.wTotUp += (S.wInstUp + cWU) * wDt * 0.0005; } else if (cWU > 0) { const wEU = cWU * 0.5 * CONFIG.wanRefreshInterval; S.wTotUp += wEU; S.wZEU = (S.wZEU || 0) + wEU; S.wZEUC = (S.wZEUC || 0) + 1; } if (S.wInstDn > 0) { S.wTotDn += (S.wInstDn + cWD) * wDt * 0.0005; } else if (cWD > 0) { const wED = cWD * 0.5 * CONFIG.wanRefreshInterval; S.wTotDn += wED; S.wZED = (S.wZED || 0) + wED; S.wZEDC = (S.wZEDC || 0) + 1; } S.wLT = wanNow; } if (CONFIG.readSaveData === 2 && !S.snapLoaded) { try { let sp = typeof GM_getValue !== 'undefined' ? GM_getValue('ha_snapshot') : null; S.snap = sp && sp.timestamp > (typeof GM_getValue !== 'undefined' ? (GM_getValue('gege_reset_ms', 0) || 0) : 0) ? sp : {}; if(S.snap.global) { S.wTotUp = S.wTotUp === 0 ? S.snap.global.wan_up || 0 : S.wTotUp; S.wTotDn = S.wTotDn === 0 ? S.snap.global.wan_down || 0 : S.wTotDn; S.lTotUp = S.lTotUp === 0 ? S.snap.global.lan_integral_up || 0 : S.lTotUp; S.lTotDn = S.lTotDn === 0 ? S.snap.global.lan_integral_down || 0 : S.lTotDn; } } catch(e){console.warn(e);} S.snapLoaded = !0; } for (const m in cI) { const cC = cI[m]; const spD = (CONFIG.readSaveData === 2 && S.snap && S.snap.devices && S.snap.devices[m]) || null; S.cls[m] ??= { upR: cC.upRate, dnR: cC.dnRate, lUT: lanNow, intUp: CONFIG.readSaveData === 3 ? cC.offUp : (spD ? (spD.integral_up || 0) : 0), intDn: CONFIG.readSaveData === 3 ? cC.offDn : (spD ? (spD.integral_down || 0) : 0), uB: CONFIG.readSaveData & 1 ? 0 : (spD ? cC.offUp - (spD.up || 0) : cC.offUp), dB: CONFIG.readSaveData & 1 ? 0 : (spD ? cC.offDn - (spD.down || 0) : cC.offDn), oU: cC.offUp, oD: cC.offDn, name: spD?.name || cC.name || m, hU: new Float64Array(64), hD: new Float64Array(64), hIdx: 0, ifc: cC.iface }; let cS = S.cls[m], dU = cC.offUp - cS.lU, dD = cC.offDn - cS.lD; if (dU < 0 || dD < 0) { if (dU < 0) { cS.uB += dU; cS.oU += dU; cS.dpU = cS.lU; } if (dD < 0) { cS.dB += dD; cS.oD += dD; cS.dpD = cS.lD; } cS.aR = 3; } else if (cS.aR === 3) { if (dU > 0 || dD > 0) { if ( cS.dpU && dU > cS.dpU * 0.975 && cS.dpD && dD > cS.dpD * 0.975 ) { if (dU < cS.dpU * 1.1 || dU < cS.dpU + CONFIG.宽带最大外网上行速率 * CONFIG.lanRefreshInterval) { cS.uB += cS.dpU; cS.oU += cS.dpU; } else { cS.uB += dU; cS.oU += dU; } if (dD < cS.dpD * 1.1 || dD < cS.dpD + CONFIG.宽带最大外网下行速率 * CONFIG.lanRefreshInterval) { cS.dB += cS.dpD; cS.oD += cS.dpD; } else { cS.dB += dD; cS.oD += dD; } cS.aR = 2; if (CONFIG.盲漫游 === undefined) CONFIG.盲漫游 = 1; } else { cS.aR = 0; } cS.dpU = 0; cS.dpD = 0; } } else if (cS.ifc !== cC.iface) { if (CONFIG.盲漫游 !== 0) cS.aR = 1; if (CONFIG.盲漫游 === 1) cS.aR = 2;} else if (cS.aR > 0) { cS.aR--; } cS.ifc = cC.iface; if (cC.upRate > 6e8) { cSU -= cC.upRate; cC.upRate = 0.3; } else if (cS.aR === 2) { cSU -= cC.upRate; cC.upRate = 0.2; } else if (cS.aR === 1 && cC.upRate > CONFIG.宽带最大外网上行速率 * 1.2 && cC.upRate > 32e7) { cSU -= cC.upRate; cC.upRate = 0.1; } if (cC.dnRate > 24e8) { cSD -= cC.dnRate; cC.dnRate = 0.3; } else if (cS.aR === 2) { cSD -= cC.dnRate; cC.dnRate = 0.2; } else if (cS.aR === 1 && (cC.dnRate > CONFIG.宽带最大外网下行速率 || cC.dnRate > 36e7)) { cSD -= cC.dnRate; cC.dnRate = 0.1; } if (cC.upRate !== cS.upR || cC.dnRate !== cS.dnR || cC.offUp !== cS.lU || cC.offDn !== cS.lD) { cS.onS = cC.onSec; if (cC.upRate !== cS.upR || cC.dnRate !== cS.dnR) { const ms = lanNow - cS.lUT; if (cS.upR > 0) { cS.intUp += (cS.upR + cC.upRate) * ms * 0.0005; } else if (cC.upRate > 0) { const eU = cC.upRate * CONFIG.lanRefreshInterval * 0.5; cS.intUp += eU; cS.zEU = (cS.zEU || 0) + eU; cS.zUC = (cS.zUC || 0) + 1; } if (cS.dnR > 0) { cS.intDn += (cS.dnR + cC.dnRate) * ms * 0.0005; } else if (cC.dnRate > 0) { const eD = cC.dnRate * CONFIG.lanRefreshInterval * 0.5; cS.intDn += eD; cS.zED = (cS.zED || 0) + eD; cS.zDC = (cS.zDC || 0) + 1; } cS.upR = cC.upRate; cS.dnR = cC.dnRate; cS.lUT = lanNow; } } if (cC.name && !cC.name.startsWith('未知设备')) cS.name = cC.name; cS.lU = cC.offUp; cS.lD = cC.offDn; } if (wanValid) { S.wInstUp = cWU; S.wInstDn = cWD; } if (lanValid) { if (S.lLT === undefined) S.lLT = lanNow; else if (cSU !== S.lInstU || cSD !== S.lInstD) { const lDt = lanNow - S.lLT; if (S.lInstU > 0) S.lTotUp += (S.lInstU + cSU) * lDt * 0.0005; else if (cSU > 0) S.lTotUp += cSU * 0.5 * CONFIG.lanRefreshInterval; if (S.lInstD > 0) S.lTotDn += (S.lInstD + cSD) * lDt * 0.0005; else if (cSD > 0) S.lTotDn += cSD * 0.5 * CONFIG.lanRefreshInterval; S.lLT = lanNow; } S.lInstU = cSU; S.lInstD = cSD; } rUI(cWU, cWD, cSU, cSD, cI); } catch (e) { console.error("[哥哥科技] 周期采样中断:", e); } finally { isF = !1; } } function buildCSV() { const csvQ = s => `"${s.replaceAll('"', '""')}"`; // RFC 4180:字段整体加引号,内部双引号翻倍 const csvE = v => { let s = String(v ?? ''); if (typeof v === 'string' && /^(?:\s*[=+\-@=+-@]|[\t\r\n])/.test(s)) s = "'" + s; // 防表格公式注入 return csvQ(s); }; const csvRow = a => a.map(csvE).join(','); const csvRowT = a => a.map(v => csvQ(String(v ?? ''))).join(','); // 仅限脚本自带可信常量 return ((sp, now, start) => '\uFEFF' + [ csvRow([`哥哥科技 硬路由 NPU 增强系列:专用组件 ${版本号} 生成`]), csvRow([`统计周期:${new Date(start + CONFIG.时区补偿).toISOString().replace('T', ' ').slice(0, 19)} 至 ${new Date(now + CONFIG.时区补偿).toISOString().replace('T', ' ').slice(0, 19)} (UTC${CONFIG.时区补偿 > 0 ? '+' : ''}${CONFIG.时区补偿 / 3600000})${CONFIG.readSaveData === 1 ? ' (含路由器后台读档)' : CONFIG.readSaveData === 3 ? ' (积分以路由器后台计数为起点)' : ''}`]), csvRowT(['--- [全局统计] ---']), csvRow(['WAN总上传(bit)','WAN总下载(b)','高精全局上行(b)','高精全局下行(b)','LAN积分总上行(b)','LAN积分总下行(b)','本次在线总上行(b)','本次在线总下行(b)']), csvRow([Math.round(sp.global?.wan_up||0),Math.round(sp.global?.wan_down||0),Math.round(sp.global?.lan_high_up||0),Math.round(sp.global?.lan_high_down||0),Math.round(sp.global?.lan_integral_up||0),Math.round(sp.global?.lan_integral_down||0),Math.round(sp.global?.lan_off_up||0),Math.round(sp.global?.lan_off_down||0)]), '', csvRowT(['--- [设备明细] ---']), csvRow(['设备名称','MAC地址','IP地址','状态/接口','高精上行','高精下行','积分上行','积分下行','官方上行','官方下行']), ...Object.entries(sp.devices || {}).map(d => csvRow([d[1].name,d[0],d[1].ip,d[1].status,Math.round(d[1].up||0),Math.round(d[1].down||0),Math.round(d[1].integral_up||0),Math.round(d[1].integral_down||0),Math.round(d[1].raw_up||0),Math.round(d[1].raw_down||0)])), '', csvRow(['Bro-Stat@哥哥科技 https://space.bilibili.com/501430041']), csvRow(['项目主页: https://github.com/ucxn/Bro-Stat']), csvRow(['脚本下载: https://scriptcat.org/users/203510']) ].join('\r\n'))( S.cSnap || {}, S.cSnap?.timestamp || Date.now(), (typeof GM_getValue !== 'undefined' ? GM_getValue('gege_reset_ms', null) : null) || performance.timeOrigin || Date.now() ); } function doSettle(nowMs) { S._RST = !0; // 防重入锁 const csv = buildCSV(), a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([csv], {type: 'text/csv;charset=utf-8;'})); a.download = `哥哥科技_路由器统计数据导出_${new Date(nowMs + CONFIG.时区补偿).toISOString().slice(2, 19).replace(/[-:]/g, '').replace('T', '_')}_${nowMs}.csv`; a.click(); // 文件 const w = window.open('about:blank', '_blank'); if (w) { w.document.write(`
哥哥科技提示您:请点击下方下载按钮将 CSV 报表保存到本地。
若下载失败,请点击复制按钮,新建文本文档粘贴后将拓展名改为 .csv 即可。