diff --git a/patch_v2/meow_netmgrd/meow_netmgrd.c b/patch_v2/meow_netmgrd/meow_netmgrd.c index 3555499..c89cd24 100644 --- a/patch_v2/meow_netmgrd/meow_netmgrd.c +++ b/patch_v2/meow_netmgrd/meow_netmgrd.c @@ -98,6 +98,13 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET #define EVT_DUP_MAX 64 /* 事件日志 1 分钟去重表 */ #define DNS_RESOLV_CHECK_SEC 5 /* resolv.conf 自检周期 */ +/* ---- 期 5 常量(公网流量统计:netlink sock_diag 按 TCP 连接累计字节;静态表,禁 malloc) ---- */ +#define FLOW_LAST_MAX 128 /* inode→上拍累计 跟踪表(仅公网/内网 TCP 流) */ +#define PUB_FLOW_MAX 16 /* 公网字节 top 流表(内存累加用) */ +#define PUB_SERVE_ENT 6 /* status JSON 序列化的 top 实体数(4096 帧预算内) */ +#define PUB_SERVE_FLOW 6 /* status JSON 序列化的 top 流数 */ +#define TCP_INFO_MIN_LEN 136 /* tcp_info 载荷≥此值才含 tcpi_bytes_received(off128+8) */ + #define DNS_PORT_DEF 53 /* 监听端口(MEOW_DNS_PORT 覆盖;宿主测试用高位规避 <1024 权限) */ #define DNS_SELF_IP "127.0.0.1" @@ -402,30 +409,19 @@ static int netdev_iface(const char *ifname, long *rx, long *tx) return found; } -/* 全局 /proc/net/dev RX/TX 累计(所有接口求和)。 */ -static void netdev_totals(long *rx, long *tx) +/* 上行接口累计字节(/proc/net/dev)。仅取「非回环且 up」接口求和——本设备即 wlan0;去掉 lo 对 + * 实时速率的虚高污染。依赖调用方先 enum_ifaces(0) 刷新 g_ifaces[] 再取数(daemon_tick 节拍内如此)。 + * 无任何 up 非回环接口 → rx 置 -1(速率未知/无连接,不谎报 0)。 */ +static void uplink_totals(long *rx, long *tx) { - long trx = 0, ttx = 0; - FILE *f = fopen(g_netdev, "r"); - if (f) { - char line[256]; - while (fgets(line, sizeof line, f)) { - char *c = strchr(line, ':'); - if (!c) continue; - char *save = NULL; - char *tk = strtok_r(c + 1, " \t", &save); - long vr = 0; - if (tk) { - vr = strtol(tk, NULL, 10); - for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save); - if (tk) ttx += strtol(tk, NULL, 10); - } - trx += vr; - } - fclose(f); + long trx = 0, ttx = 0, seen = 0; + for (int i = 0; i < g_iface_n; i++) { + if (iface_parse_kind(g_ifaces[i].name) == 0) continue; /* lo */ + if (!g_ifaces[i].up) continue; /* 只取 up */ + trx += g_ifaces[i].rx; ttx += g_ifaces[i].tx; seen = 1; } - if (rx) *rx = trx; - if (tx) *tx = ttx; + if (rx) *rx = seen ? trx : -1; + if (tx) *tx = seen ? ttx : 0; } /* iface IP:优先 `ip -o -4 addr show dev `;ip 缺失/无输出 → fib_trie 兜底(作用于主口)。 @@ -842,6 +838,24 @@ static char g_prev_if[IFACE_MAX][32]; static int g_prev_if_up[IFACE_MAX]; static int g_prev_if_n = 0; +/* ---- 期 5:公网/内网流量统计(netlink sock_diag,按 TCP 连接累计字节;UDP 无字节计数,诚实标注)。 + * 全部静态表(禁 malloc);每 2s 节拍在 census_run 内刷新,build_status_json 只读序列化。 ---- */ +typedef struct { char entity[48]; uint64_t pub_rx, pub_tx; /* 远端为公网的 TCP 累计 */ + uint64_t lan_rx, lan_tx; } pub_ent_t; /* 远端为内网/本链路的 TCP 累计 */ +static pub_ent_t g_pub_ent[ENTITY_MAX]; +static int g_pub_ent_n = 0; +typedef struct { uint32_t inode; uint64_t last_rx, last_tx; long seen_gen; } flow_last_t; /* inode→上拍累计(增量用) */ +static flow_last_t g_flow_last[FLOW_LAST_MAX]; +static int g_flow_last_n = 0; +static long g_flow_gen = 0; /* 每拍代际(剔陈旧 inode 用) */ +typedef struct { uint32_t remote_ip; uint16_t remote_port; char entity[48]; char domain[48]; + uint64_t rx, tx; long last_seen; } pub_flow_t; /* 公网字节 top 流 */ +static pub_flow_t g_pub_flow[PUB_FLOW_MAX]; +static int g_pub_flow_n = 0; +static uint64_t g_pub_rx = 0, g_pub_tx = 0; /* 公网总收发累计(TCP) */ +static int g_diag_fd = -1; /* NETLINK_SOCK_DIAG(懒开) */ +static int g_diag_warned = 0; /* 打开失败仅告警一次 */ + /* ---------------- JSON 构建:status/iface ---------------- */ static void iface_json_obj(jw_t *w, const iface_t *f) { @@ -852,6 +866,80 @@ static void iface_json_obj(jw_t *w, const iface_t *f) jadd(w, "\"rx_bytes\":%ld,\"tx_bytes\":%ld}", f->rx, f->tx); } +/* status JSON 里的公网/内网流量子块。TOP-K 序列化保住 4096 单帧预算;数据侧仍全量累计。 + * 口径:netlink sock_diag 按 TCP 连接(远端分公网/内网)累计字节;UDP/ARP/mDNS 无字节计数,标 tcp_only。 + * 注意:本函数只闭合 pub_bytes 对象(}, 不闭合外层 status 对象,由调用方收尾)。 */ +static void pub_bytes_json(jw_t *w) +{ + jadd(w, ",\"pub_bytes\":{"); + jadd(w, "\"source\":\"sock_diag_tcp\",\"tcp_only\":true,"); + jadd(w, "\"total\":{\"rx\":%llu,\"tx\":%llu},", (unsigned long long)g_pub_rx, (unsigned long long)g_pub_tx); + + /* by_entity:按 pub+lan total 取 TOP-K(量小,线性选择)。 */ + jadd(w, "\"by_entity\":["); + { + int sel[PUB_SERVE_ENT], seln = 0; + for (int i = 0; i < g_pub_ent_n; i++) { + uint64_t s = g_pub_ent[i].pub_rx + g_pub_ent[i].pub_tx + + g_pub_ent[i].lan_rx + g_pub_ent[i].lan_tx; + if (seln < PUB_SERVE_ENT) { sel[seln++] = i; continue; } + int mi = 0; + for (int j = 1; j < seln; j++) { + pub_ent_t *a = &g_pub_ent[sel[j]]; + pub_ent_t *b = &g_pub_ent[sel[mi]]; + if (a->pub_rx + a->pub_tx + a->lan_rx + a->lan_tx < + b->pub_rx + b->pub_tx + b->lan_rx + b->lan_tx) mi = j; + } + pub_ent_t *m = &g_pub_ent[sel[mi]]; + if (m->pub_rx + m->pub_tx + m->lan_rx + m->lan_tx < s) sel[mi] = i; + } + for (int i = 0; i < seln; i++) for (int j = i + 1; j < seln; j++) { + pub_ent_t *a = &g_pub_ent[sel[i]], *b = &g_pub_ent[sel[j]]; + if (a->pub_rx + a->pub_tx + a->lan_rx + a->lan_tx < + b->pub_rx + b->pub_tx + b->lan_rx + b->lan_tx) { int t = sel[i]; sel[i] = sel[j]; sel[j] = t; } + } + for (int k = 0; k < seln; k++) { + pub_ent_t *e = &g_pub_ent[sel[k]]; + if (k) jadd(w, ","); + jadd(w, "{\"entity\":\"%s\",\"pub\":[%llu,%llu],\"lan\":[%llu,%llu]}", + e->entity, (unsigned long long)e->pub_rx, (unsigned long long)e->pub_tx, + (unsigned long long)e->lan_rx, (unsigned long long)e->lan_tx); + } + } + jadd(w, "],"); + + /* by_flow:仅公网 top 流。 */ + jadd(w, "\"by_flow\":["); + { + int sel[PUB_SERVE_FLOW], seln = 0; + for (int i = 0; i < g_pub_flow_n; i++) { + uint64_t s = g_pub_flow[i].rx + g_pub_flow[i].tx; + if (seln < PUB_SERVE_FLOW) { sel[seln++] = i; continue; } + int mi = 0; + for (int j = 1; j < seln; j++) + if (g_pub_flow[sel[j]].rx + g_pub_flow[sel[j]].tx < + g_pub_flow[sel[mi]].rx + g_pub_flow[sel[mi]].tx) mi = j; + if (g_pub_flow[sel[mi]].rx + g_pub_flow[sel[mi]].tx < s) sel[mi] = i; + } + for (int i = 0; i < seln; i++) for (int j = i + 1; j < seln; j++) + if (g_pub_flow[sel[i]].rx + g_pub_flow[sel[i]].tx < + g_pub_flow[sel[j]].rx + g_pub_flow[sel[j]].tx) { int t = sel[i]; sel[i] = sel[j]; sel[j] = t; } + for (int k = 0; k < seln; k++) { + pub_flow_t *f = &g_pub_flow[sel[k]]; + char ip[32]; + snprintf(ip, sizeof ip, "%u.%u.%u.%u", + (f->remote_ip >> 24) & 0xff, (f->remote_ip >> 16) & 0xff, + (f->remote_ip >> 8) & 0xff, f->remote_ip & 0xff); + if (k) jadd(w, ","); + jadd(w, "{\"ip\":\"%s\",\"port\":%u,\"entity\":\"%s\",\"domain\":\"%s\",\"rx\":%llu,\"tx\":%llu}", + ip, (unsigned)f->remote_port, f->entity, f->domain, + (unsigned long long)f->rx, (unsigned long long)f->tx); + } + } + jadd(w, "]"); + jadd(w, "}"); +} + static void build_status_json(char *out, size_t cap) { jw_t w = { out, cap, 0 }; @@ -918,7 +1006,10 @@ static void build_status_json(char *out, size_t cap) jadd(&w, "{\"entity\":\"%s\",\"rx\":%u,\"tx\":%u}", g_ent_bytes[i].entity, g_ent_bytes[i].rx, g_ent_bytes[i].tx); } - jadd(&w, "]}"); + jadd(&w, "]"); + /* 期5:公网/内网流量(netlink sock_diag)——只闭合 pub_bytes 对象,外层 status 由下方 `}` 收尾。 */ + pub_bytes_json(&w); + jadd(&w, "}"); } /* 单/全接口详情 JSON。name 空=全部(数组);否则单对象。返回 MEOW_OK / MEOW_ERR_NOENT。 */ @@ -1549,6 +1640,170 @@ static void evt_conn_log(const char *entity, const char *dst, const char *what) net_log("info", "conn", "%s entity=%s dst=%s", what, entity, dst); } +/* ---- 期5:公网/内网流量计量(netlink sock_diag,per-TCP 连接累计字节) ---- + * 口径:每个 TCP 连接 tcp_info 的 tcpi_bytes_received(off128)/tcpi_bytes_acked(off120) 是连接 + * 生命周期累计;按拍差分(首见=基线 0,防一次性计入存量),远端分公网/内网(RFC1918/回环/链路本地/ + * 组播都归内网)。UDP 无字节计数(DNS/流媒体 UDP 不计量),故整体标 tcp_only。内核 4.4.94 实测返回 + * tcp_info plen=144,bytes_* 有效;所有表静态,禁 malloc。 */ +static int ip_is_public(uint32_t ip) +{ + uint8_t b0 = (uint8_t)(ip >> 24), b1 = (uint8_t)(ip >> 16); + if (b0 == 0 || b0 == 127) return 0; /* 保留/回环 */ + if (b0 == 10) return 0; /* RFC1918 10/8 */ + if (b0 == 172 && b1 >= 16 && b1 <= 31) return 0; /* RFC1918 172.16/12 */ + if (b0 == 192 && b1 == 168) return 0; /* RFC1918 192.168/16 */ + if (b0 == 169 && b1 == 254) return 0; /* 链路本地 */ + if (b0 >= 224) return 0; /* 组播/保留 224/4+240/4 */ + return 1; +} +static flow_last_t *flow_last_get(uint32_t inode) +{ + for (int i = 0; i < g_flow_last_n; i++) + if (g_flow_last[i].inode == inode) return &g_flow_last[i]; + if (g_flow_last_n < FLOW_LAST_MAX) return &g_flow_last[g_flow_last_n++]; + return &g_flow_last[0]; /* 全满:复用首槽(本设备并发 TCP 极少,防御性兜底) */ +} +static pub_ent_t *pub_ent_get(const char *entity) +{ + for (int i = 0; i < g_pub_ent_n; i++) + if (strcmp(g_pub_ent[i].entity, entity) == 0) return &g_pub_ent[i]; + if (g_pub_ent_n < ENTITY_MAX) { + pub_ent_t *p = &g_pub_ent[g_pub_ent_n++]; + memset(p, 0, sizeof *p); + snprintf(p->entity, sizeof p->entity, "%s", entity); + return p; + } + return &g_pub_ent[0]; /* 满:并入首槽(保稳定,不新键) */ +} +static void pub_flow_add(uint32_t rip, uint16_t rport, const char *entity, + const char *domain, uint64_t drx, uint64_t dtx) +{ + if (drx == 0 && dtx == 0) return; + pub_flow_t *f = NULL; + for (int i = 0; i < g_pub_flow_n; i++) + if (g_pub_flow[i].remote_ip == rip && g_pub_flow[i].remote_port == rport && + strcmp(g_pub_flow[i].entity, entity) == 0) { f = &g_pub_flow[i]; break; } + if (!f) { + if (g_pub_flow_n < PUB_FLOW_MAX) f = &g_pub_flow[g_pub_flow_n++]; + else { + f = &g_pub_flow[0]; + for (int i = 1; i < PUB_FLOW_MAX; i++) + if (g_pub_flow[i].rx + g_pub_flow[i].tx < f->rx + f->tx) f = &g_pub_flow[i]; + } + memset(f, 0, sizeof *f); + f->remote_ip = rip; f->remote_port = rport; + snprintf(f->entity, sizeof f->entity, "%s", entity); + snprintf(f->domain, sizeof f->domain, "%s", domain ? domain : ""); + } + f->rx += drx; f->tx += dtx; f->last_seen = mono_now(); +} +static void pub_bytes_run(const conn_t *conns, int n) +{ + /* 懒开 NETLINK_SOCK_DIAG。 */ + if (g_diag_fd < 0) { + g_diag_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_SOCK_DIAG); + if (g_diag_fd < 0) { + if (!g_diag_warned) { g_diag_warned = 1; net_log("warn", "pub", "sock_diag open fail errno=%d", errno); } + return; + } + struct timeval tv = { 0, 120000 }; /* 120ms 收超时,防单拍卡死(本设备连接极少,秒回) */ + setsockopt(g_diag_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + struct sockaddr_nl sa; memset(&sa, 0, sizeof sa); + sa.nl_family = AF_NETLINK; + bind(g_diag_fd, (struct sockaddr *)&sa, sizeof sa); + } + + struct { struct nlmsghdr nlh; struct inet_diag_req_v2 req; } req; + memset(&req, 0, sizeof req); + req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.req)); + req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; + req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + req.req.sdiag_family = AF_INET; + req.req.sdiag_protocol = IPPROTO_TCP; + req.req.idiag_ext = (1u << (INET_DIAG_INFO - 1)); /* 请求 tcp_info 扩展 */ + req.req.idiag_states = 0xFFFFFFFF; + + struct sockaddr_nl kern; memset(&kern, 0, sizeof kern); + kern.nl_family = AF_NETLINK; + if (sendto(g_diag_fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&kern, sizeof kern) < 0) { + if (!g_diag_warned) { g_diag_warned = 1; net_log("warn", "pub", "sock_diag send fail errno=%d", errno); } + return; + } + + g_flow_gen++; + char buf[16384]; + int done = 0; + for (int round = 0; round < 8 && !done; round++) { /* 有界收割 */ + ssize_t r = recv(g_diag_fd, buf, sizeof buf, 0); + if (r < 0) break; /* 超时/错误 → 结束本拍 */ + for (struct nlmsghdr *nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned)r); + nlh = NLMSG_NEXT(nlh, r)) { + if (nlh->nlmsg_type == NLMSG_DONE || nlh->nlmsg_type == NLMSG_ERROR) { done = 1; break; } + struct inet_diag_msg *dm = (struct inet_diag_msg *)NLMSG_DATA(nlh); + uint32_t inode = dm->idiag_inode; + uint32_t rnet = ntohl(dm->id.idiag_dst[0]); /* 远端(host 序) */ + uint16_t rport = ntohs(dm->id.idiag_dport); + if (rnet == 0 || rport == 0 || inode == 0) continue; /* 监听口/未定 → 跳过 */ + int msglen = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct inet_diag_msg))); + if (msglen <= 0) continue; + /* 解析 INET_DIAG_INFO(tcp_info),取 bytes_received(off128)/bytes_acked(off120)。 */ + uint64_t brx = 0, btx = 0; + int have = 0; + struct nlattr *at = (struct nlattr *)((char *)dm + sizeof(struct inet_diag_msg)); + int rem = msglen; + while (rem >= (int)sizeof(struct nlattr) && at->nla_len >= sizeof(struct nlattr) && + at->nla_len <= (unsigned)rem) { + int type = at->nla_type & NLA_TYPE_MASK; + int plen = (int)at->nla_len - NLA_HDRLEN; + if (type == INET_DIAG_INFO && plen >= TCP_INFO_MIN_LEN) { + unsigned char *pay = (unsigned char *)at + NLA_HDRLEN; + memcpy(&brx, pay + 128, 8); /* tcpi_bytes_received */ + memcpy(&btx, pay + 120, 8); /* tcpi_bytes_acked */ + have = 1; + break; + } + int alen = NLA_ALIGN((int)at->nla_len); + rem -= alen; + at = (struct nlattr *)((char *)at + alen); + } + if (!have) continue; + /* 匹配连接表 → 实体 + 域名;未归因 → 跳过(保归因干净)。 */ + const conn_t *c = NULL; + for (int i = 0; i < n; i++) if (conns[i].inode == inode) { c = &conns[i]; break; } + if (!c) continue; + flow_last_t *fl = flow_last_get(inode); + uint64_t drx, dtx; + if (fl->inode != inode) { /* 首见/复槽 → 基线扣 0,防一次性计入存量 */ + fl->inode = inode; + fl->last_rx = brx; fl->last_tx = btx; + fl->seen_gen = g_flow_gen; + drx = 0; dtx = 0; + } else { + drx = brx >= fl->last_rx ? (brx - fl->last_rx) : brx; + dtx = btx >= fl->last_tx ? (btx - fl->last_tx) : btx; + fl->last_rx = brx; fl->last_tx = btx; + fl->seen_gen = g_flow_gen; + } + if (drx == 0 && dtx == 0) continue; + pub_ent_t *pe = pub_ent_get(c->entity); + if (ip_is_public(rnet)) { + pe->pub_rx += drx; pe->pub_tx += dtx; + g_pub_rx += drx; g_pub_tx += dtx; + pub_flow_add(rnet, rport, c->entity, c->domain, drx, dtx); + } else { + pe->lan_rx += drx; pe->lan_tx += dtx; + } + } + } + /* 剔掉本拍未出现的 inode(已关闭/变监听口),防表膨胀。 */ + if (g_flow_last_n) { + int w = 0; + for (int i = 0; i < g_flow_last_n; i++) + if (g_flow_last[i].seen_gen == g_flow_gen) g_flow_last[w++] = g_flow_last[i]; + g_flow_last_n = w; + } +} + /* ---- 连接普查(每 2s):/proc/net/{tcp,udp} → inode→pid→实体 → 连接表 + 事件 ---- */ static void census_run(void) { @@ -1602,6 +1857,8 @@ static void census_run(void) evt_conn_log(g_conn[j].entity[0] ? g_conn[j].entity : "proc:unknown", dst, "close"); } } + /* 期5:公网/内网流量计量(netlink sock_diag,基于本拍连接表归因)。 */ + pub_bytes_run(g_conn, g_conn_n); } /* ---- DNS 代理(§5;三条兜底:双行 resolv + atexit 恢复 + 5s 自检) ---- */ @@ -1976,9 +2233,9 @@ static void daemon_tick(void) } g_prev_if_n = cur; - /* 全局速率差分(2s 拍):/proc/net/dev 累计。 */ + /* 全局速率差分(2s 拍):仅上行接口累计(去 lo)。 */ long rx = -1, tx = 0; - netdev_totals(&rx, &tx); + uplink_totals(&rx, &tx); if (rx >= 0) { long now = mono_now(); if (g_net_last_rx >= 0 && now > g_net_last_mono) { diff --git a/patch_v2/meowpanel/web/meowpanel.js b/patch_v2/meowpanel/web/meowpanel.js index 08d00c8..8331991 100644 --- a/patch_v2/meowpanel/web/meowpanel.js +++ b/patch_v2/meowpanel/web/meowpanel.js @@ -747,6 +747,12 @@ } else { rows += homeKv('DNS', '—'); } + const pb = (nd && nd.pub_bytes) || null; + if (pb && pb.total) { + rows += homeKv('公网流量', '↓ ' + fmtBytes(pb.total.rx || 0) + ' · ↑ ' + fmtBytes(pb.total.tx || 0) + '(TCP)'); + } else { + rows += homeKv('公网流量', '—'); + } return homeCard('WiFi / DNS', '
' + rows + '
'); } function healthCard() { @@ -1269,6 +1275,7 @@ meta += (ifd.rssi != null) ? '信号 ' + esc(ifd.rssi) + ' dBm' : '信号 —'; if (ifd.dns && ifd.dns.length) meta += ' · DNS ' + esc(ifd.dns.join(' / ')); meta += ''; + h += '

RX / TX 为接口累计字节(含局域网/面板轮询/DNS 等内网往返),非手机热点公网消耗。

'; h += meta; h += ''; if (!ifs.length) h += ''; @@ -1289,11 +1296,46 @@ h += ''; return h; } + /* 公网流量卡(netmgrd pub_bytes:netlink sock_diag 按 TCP 连接累积,远端公网/内网分流)。 + * 口径诚实:仅 TCP;UDP/DNS/mDNS/ARP 不计;内网本链路(面板轮询等)归 lan。 */ + function pubBytesCard(pb) { + if (!pb) return ''; + const t = pb.total || {}; + let h = '

公网流量

'; + h += '

按 TCP 连接计量(远端公网才计入);UDP/DNS/mDNS/ARP 不计。接口累计含内网往返,非公网消耗。

'; + h += '
公网累计:↓ ' + fmtBytes(t.rx || 0) + ' · ↑ ' + fmtBytes(t.tx || 0) + '
'; + const ents = pb.by_entity || []; + if (ents.length) { + h += '
接口类型状态IP 地址网关RX / TX
无接口数据
'; + ents.forEach(e => { + h += '' + + '' + + ''; + }); + h += '
实体公网 ↓ / ↑内网 ↓ / ↑
' + netBadge(e.entity) + '' + fmtBytes((e.pub && e.pub[0]) || 0) + ' / ' + fmtBytes((e.pub && e.pub[1]) || 0) + '' + fmtBytes((e.lan && e.lan[0]) || 0) + ' / ' + fmtBytes((e.lan && e.lan[1]) || 0) + '
'; + } + const flows = pb.by_flow || []; + if (flows.length) { + h += '
公网流 Top
'; + h += ''; + flows.forEach(f => { + h += '' + + '' + + ''; + }); + h += '
远端实体↓ / ↑
' + esc(f.ip || '') + ':' + esc('' + f.port) + + (f.domain ? ' ' + esc(f.domain) + '' : '') + '' + netBadge(f.entity) + '' + fmtBytes(f.rx || 0) + ' / ' + fmtBytes(f.tx || 0) + '
'; + } + h += ''; + return h; + } + /* WiFi / 蓝牙摘要(/api/net/status:E0 概览,含实时速率)。接口细分已迁 netIfaceCards。 */ function netStatusCards(d) { let h = ''; if (d && d.rate && (d.rate.rx_bps != null || d.rate.tx_bps != null)) - h += '

实时速率:↓ ' + fmtBytes(d.rate.rx_bps) + '/s · ↑ ' + fmtBytes(d.rate.tx_bps) + '/s

'; + h += '

实时速率(上行接口,不含回环):↓ ' + fmtBytes(d.rate.rx_bps) + '/s · ↑ ' + fmtBytes(d.rate.tx_bps) + '/s

'; + if (d && d.pub_bytes) h += pubBytesCard(d.pub_bytes); // WiFi 摘要 const w = (d && d.wifi) || {}; h += '

WiFi

';