// ==UserScript== // @name TP路由器增强 // @name:en Bro-Stat-TP // @namespace ucxn // @version 5.9.9.lab // @description 适用于TP-LINK普联+几乎所有家用消费级硬路由的网速显示和流量微积分统计工具 // @description:en A tool for displaying internet speeds and tracking data usage statistics, compatible with nearly all TP-LINK + Multi brand home gateways // @tag TP TP-link 普联 路由器 WiFi 微积分 高数 统计 流量 网速 事件驱动 大屏 铺平 测控 累加 统计 监控 网络 宽带 P2P // @author 哥哥科技 space.bilibili.com/501430041 // @noframes // @icon https://scriptcat.org/api/v2/resource/image/duygQktL5QjWtkLc // @include http://10.*.*.* // @include http://192.168.*.* // @include /^https?:\/\/tplogin\.[a-z]+\/.*$/ // @include http://172.16.* // @include https://10.*.*.* // @include https://192.168.*.* // @match *://tplogin.cn/* // @include https://172.16.* // @run-at document-start // @grant GM_setValue // @grant GM_getValue // @storageName GBNPA_Storage // @source https://github.com/ucxn/Bro-Stat // @supportURL https://www.bilibili.com/video/BV1LZ6yBXESq // @license 哥哥软件-显著署名-哥哥科技-严格 + (完整使用-署名保留-完全 OR (部分摘录-禁止演绎-署名保护-显著-哥哥科技 OR 二次开发-署名保留/保护-显著署名-署名哥哥-署名追加) AND 相同方式共享) // @note 发行版作者哥哥科技保留所有权利,源码本身公开,请前往 GitHub Source 区域获得授权部分 // ==/UserScript== (function () { 'use strict'; console.log("🚀 哥哥科技 V5.9.9 引擎已装载..."); // ======== [0] 用户极客环境变量配置区 ======== const CONFIG = { readSaveData: 1, // 【历史记录】 从本地长期历史读档 [自动保存!] | 0: 新局模式 forceMeshMode: 0, uiLayout: 2,//【面板拓扑结构】 0: 经典版 | 1: 详细紧凑版(驾驶舱美学) | 2: 详细平铺版(报表流美学) injectMode: 3, //1: 优先,10秒悬浮舱(D)| 3:强制模式 calcMode: 1,// 1: 上行/下行倍数模式, 0: 上行占总和比例模式 ratioExtremeUp: 10, // 极端上传判定阈值 (> 1000%) ratioWarnUp: 0.07, // 重度上传警告阈值 (> 7%) ratioExtremeDown: 0.01, // 极端下载判定阈值 (< 1%) ratioThreshold: 7, // (仅calcMode=0时有效) 上传占比报警阈值(%) lanRefreshInterval: 3, // LAN口刷新时间(秒),用于精准补偿0到唤醒时的瞬时流量 wanRefreshInterval: 3, // WAN口刷新时间(秒),用于精准补偿0到唤醒时的瞬时流量 宽带最大外网上行速率: 3e8, 宽带最大外网下行速率: 24e8, // 配置外网最大上传|下载比特(bit/bps)速率,请略微大于真实值;500兆为5e8,一千兆1e9 周期类型: 'M', // 'M'(每月), 'W'(每周), 'D'(固定天数), 其它任意字符:不开启周期重置+自动导出功能 周_天设置: 1, // M: 1~31号; W: 0~6(周日~周六); D: 间隔天数(如 7) 基准日期: '2026-06-20', // 原点时间(仅 D 模式有效) 任意一个历史周期的零点 报告时间: -1080, // 提示时间:相对周期0点的偏移分钟数。(如 -4320 代表提前 3 天) 设置相对指定日期的下个周期起点的时间偏移量 自动导出: 0, // 强制导出:相对周期0点的偏移分钟数。(如 W模式+锚点6(周六)+偏移-180 = 周五 21:00 强制导出清零) 时区补偿: 28800000, // 默认 UTC+8 时区补偿量。 盲漫游: undefined, //是否拦截漫游可能产生的异常高网速 portMap: { "eth1": "网口 1", "eth2": "网口 2", "eth3": "网口 3", "eth4": "网口 4", "wl0": "2.4G", "wl1": "5.2G", "wl2": "5.8G" } }; const ESC_MAP = { '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }; function escapeHTML(str) { return str ? String(str).replace(/[&<>'"]/g, m => ESC_MAP[m]) : ''; } const __origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { let m = String(url).match(/stok=([a-zA-Z0-9]+)/i); if (m) window.__tpStok = m[1]; return __origOpen.apply(this, arguments); }; let _saved = null; if (CONFIG.readSaveData === 1 && typeof GM_getValue !== 'undefined') { try { let sp = GM_getValue('ha_snapshot', null); _saved = sp && sp.timestamp > (GM_getValue('gege_reset_ms', 0) || 0) ? sp : null; } catch(e) {console.warn(e);} } const S = { lt: 0, wInstUp: 0, wInstDn: 0, // [修改] 启动时将 WAN口 总量继承为存档数据 wTotUp: _saved?.global?.wan_up || 0, wTotDn: _saved?.global?.wan_down || 0, cls: {}, isPinned: !0, w2U: 0, w2D: 0, w2TotUp: 0, w2TotDn: 0, w2LT: undefined, hasW2: !1, is5G_149: !1, Warn_MS: 0, Force_MS: 0, _RST: !1, aWu: 0, aWd: 0, lwTU: 0, lwTD: 0, cSnap: null, 总上行图: new Float64Array(8192), 总下行图: new Float64Array(8192), 总图点数: 0, wMaxU: 0, wMaxD: 0, wMinU: Infinity, wMinD: Infinity, 图表拖: null, 图表待画: 0 }; const _w = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; 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 版本号 = (typeof GM_info !== 'undefined' && GM_info.script?.version) || '环境不支持获取版本号'; function fB(bps) { if (bps > 1e9) return `${(bps * 1e-6).toFixed(1)} Mbit/s`; if (bps > 1e6) return `${(bps * 1e-6).toFixed(2)} Mbps`; if (bps > 1e3) return `${(bps * 1e-3).toFixed(1)} kbps`; return `${Math.round(bps)} bps`; } const F_ARR_8 = ['0', '[1/8]', '[2/8]', '[3/8]', '[4/8]', '[5/8]', '[6/8]', '[7/8]', '[1]']; function fBy(bps) { if (bps === 0) return '0 B'; if (bps > 8388608) return `${(bps / 8388608).toFixed(2)} MiB/s`; if (bps === 0.5) return `拦截中…`; return bps < 8602 ? ((bps * 0.001 | 0) === bps * 0.001 ? `${F_ARR_8[bps * 0.001]} kB/s` : `${(bps * 0.000125).toFixed(2)} kB/s`) : `${(bps / 8192).toFixed(1)} K/s`; } function fV(bits) { if (bits > 83886080000) return `${(bits / 8589934592).toFixed(4)} G`; if (bits > 8388608000) return `${(bits / 8388608).toFixed(1)} M`; if (bits > 8388608) return `${(bits / 8388608).toFixed(4)} M`; if (bits > 8192) return `${(bits / 8192).toFixed(3)} K`; return `${Math.round(bits / 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).toFixed(2)}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 dN(s) { if (!s) return ''; s = String(s); if (!/%[0-9a-fA-F]{2}/.test(s)) return s; try { return decodeURIComponent(s); } catch (_) { return s; } } 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:50px;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;}@media (max-width:768px){} `; document.head. appendChild(st); window.gegeRenderedMacs = new Set(); const TP_WAN_REQUEST = { network: { name: ["wan_status", "wan_status_2"] }, method: "get" }; const TP_HOST_REQUEST = { hosts_info: { table: "online_host" }, method: "get" }; const TP_COMBINED_REQUEST = { network: { name: ["wan_status", "wan_status_2"] }, hosts_info: { table: "online_host" }, method: "get" }; async function tpDsPost(apiUrl, body) { const r = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return r.ok ? r.json() : null; } let readTpStats = async function (apiUrl) { try { const both = await tpDsPost(apiUrl, TP_COMBINED_REQUEST); if (both?.error_code === 0 && both.network?.wan_status && Array.isArray(both.hosts_info?.online_host)) { readTpStats = async url => { const data = await tpDsPost(url, TP_COMBINED_REQUEST); return [data, data]; }; return [both, both]; } } catch (e) { console.warn("[TP] 混合包探测失败,退回双包", e); } readTpStats = async url => { let wan = null, hosts = null; try { wan = await tpDsPost(url, TP_WAN_REQUEST); } catch (e) { console.warn("[TP] WAN口拉取失败", e); } try { hosts = await tpDsPost(url, TP_HOST_REQUEST); } catch (e) { console.warn("[TP] 设备列表拉取失败", e); } return [wan, hosts]; }; return readTpStats(apiUrl); }; async function rSD() { if (window.__gIsF) return; window.__gIsF = !0; try { let stk = window.__tpStok; if (!stk) { window.__gIsF = !1; return; // 还没到就等下一秒再试 } let apiUrl = `/stok=${stk}/ds`; let dW = null, dL = null, wanNow, lanNow; [dW, dL] = await readTpStats(apiUrl); wanNow = performance.now(); lanNow = performance.now(); let wanValid = dW?.error_code === 0 && !!dW.network?.wan_status; let lanValid = dL?.error_code === 0 && Array.isArray(dL.hosts_info?.online_host); let cWU = S.wInstUp, cWD = S.wInstDn; if (wanValid) { let curW = dW.network.wan_status; cWU = (+curW.up_speed || 0) * 8000; // TP 的主、副 WAN 都是 kByte/s,转成 bps 需要 * 8000 cWD = (+curW.down_speed || 0) * 8000; 记总速率图(cWU, cWD); } if (wanValid) S.hasW2 = !!(dW.network.wan_status_2?.ipaddr && dW.network.wan_status_2.ipaddr !== "0.0.0.0"); if (wanValid && S.hasW2) { let u2 = (+dW.network.wan_status_2.up_speed || 0) * 8000, d2 = (+dW.network.wan_status_2.down_speed || 0) * 8000; if (S.w2LT === undefined) S.w2LT = wanNow; else if (S.w2U !== u2 || S.w2D !== d2) { let dt = wanNow - S.w2LT; if (S.w2U > 0) S.w2TotUp += (S.w2U + u2) * dt * 0.0005; else if (u2 > 0) S.w2TotUp += u2 * 0.5 * CONFIG.wanRefreshInterval; if (S.w2D > 0) S.w2TotDn += (S.w2D + d2) * dt * 0.0005; else if (d2 > 0) S.w2TotDn += d2 * 0.5 * CONFIG.wanRefreshInterval; S.w2LT = wanNow; } S.w2U = u2; S.w2D = d2; } let cSU = 0, cSD = 0, cI = Object.create(null); (lanValid ? dL.hosts_info.online_host : []).forEach(obj => { let i = obj[Object.keys(obj)[0]]; if (i && i.mac) { let m = nM(i.mac), u = (+i.up_speed || 0) * 8, dn = (+i.down_speed || 0) * 8; cI[m] = { upRate: u, dnRate: dn, iface: i.type === '0' ? 'eth1' : (i.phy_mode === '7' ? 'wl1' : 'wl0'), offUp: 0, offDn: 0, onSec: +(i.online_time || 0), name: dN(i.hostname) || "未知设备", ip: i.ip || "" }; cSU += u; cSD += dn; } }); 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 cS = S.cls[m], ms = lanNow - cS.lUT; if (ms > CONFIG.lanRefreshInterval * 1000) ms = CONFIG.lanRefreshInterval * 1000; cS.intUp += cS.upR * ms * 0.0005; cS.intDn += cS.dnR * ms * 0.0005; cS.upR = cS.dnR = 0; }} if (ol && ol.style.display === 'block' && (iD || !ol.querySelector('.gege-list-item'))) { bVD(ol, cI); window.gegeRenderedMacs = new Set(cM); window.gegeForceUIRedraw = !1; } const gDt = (S.lt !== 0) ? (lanNow - S.lt) * 0.001 : 0; 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; } const 本轮刷新接口 = new Set(); for (const m in cI) { const cC = cI[m]; let cS = S.cls[m]; if (!cS) { cS = S.cls[m] = { upR: cC.upRate, dnR: cC.dnRate, lUT: lanNow, aR: 0, intUp: _saved?.devices?.[m]?.integral_up || 0, intDn: _saved?.devices?.[m]?.integral_down || 0, onS: cC.onSec, lOS: cC.onSec, name: _saved?.devices?.[m]?.name || cC.name || m, hU: new Float64Array(128), hD: new Float64Array(128), hIdx: 0, ifc: cC.iface }; } else { let dU = cC.offUp - cS.lU, dD = cC.offDn - cS.lD; if (cS.ifc !== cC.iface) { if (CONFIG.盲漫游 != 0) cS.aR = 1; if (CONFIG.盲漫游 === 1) cS.aR = 2; cS.ifc = cC.iface; } 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 && cS.dpD && dD >= cS.dpD) { 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; cS.dB += dD; cS.oD += dD; } 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.aR > 0) { cS.aR--; } if (cS.aR === 2 || (cS.aR == 1 && cC.upRate > CONFIG.宽带最大外网上行速率 * 0.6) || cC.upRate > 6e8) { cSU -= cC.upRate; cC.upRate = 0.5; } if (cS.aR === 2 || (cS.aR == 1 && cC.dnRate > CONFIG.宽带最大外网下行速率 * 0.6) || cC.dnRate > 24e8) { cSD -= cC.dnRate; cC.dnRate = 0.5; } if (cS.aR === 0 && (cC.upRate !== cS.upR || cC.dnRate !== cS.dnR)) 本轮刷新接口.add(cC.iface); if (cS.lOS !== cC.onSec) { cS.onS = cC.onSec; cS.lOS = cC.onSec; } else { cS.onS = (cS.onS || cC.onSec || 0) + gDt; } } if (cC.name && cC.name !== '未知设备') cS.name = cC.name; cS.lU = cC.offUp; cS.lD = cC.offDn; } for (const m in cI) { const cC = cI[m]; let cS = S.cls[m]; if (cC.upRate !== cS.upR || cC.dnRate !== cS.dnR || cS.aR === 0 && 本轮刷新接口.has(cC.iface)) { 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; } } S.lt = lanNow; if (wanValid) { S.wInstUp = cWU; S.wInstDn = cWD; } rUI(cWU, cWD, cSU, cSD, cI); } catch (e) { console.error("[哥哥科技] 周期采样中断:", e); } finally { window.__gIsF = !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 ? ' (含本地历史读档)' : ''}`]), 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(), (CONFIG.readSaveData !== 0 && typeof GM_getValue !== 'undefined' ? GM_getValue('gege_reset_ms', null) : null) || performance.timeOrigin || Date.now() ); } function doSettle(nowMs) { S._RST = !0; // 防重入锁 let 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(); // 文件 let w = window.open('about:blank', '_blank'); if (w) { w.document.write(`
哥哥科技提示您:请点击下方下载按钮将 CSV 报表保存到本地。
若下载失败,请点击复制按钮,新建文本文档粘贴后将拓展名改为 .csv 即可。