diff --git a/_build/_commitmsg_a.txt b/_build/_commitmsg_a.txt new file mode 100644 index 0000000..d34cc5c --- /dev/null +++ b/_build/_commitmsg_a.txt @@ -0,0 +1,30 @@ +阶段3+4+5:管理页白色化 + 局域网诊断 + 全量验收 + +阶段3 管理页 +- 清掉全部深色主题残留(地址框 #0b1220、按钮悬停 #262c36、 + 删除按钮 #2d1214 等 11 处),现在 0 处深色 +- 接口全部正常:health / downloads / netcheck + +阶段4 局域网诊断 +- 查清楚"电脑不在同一网络"是怎么回事:被控端是**对的** + (lan_ips() 报 ['192.168.2.64', '169.254.83.107'],真实地址排第一), + 是手表当时真不在 192.168.2.x —— 可能连了 5G 频段的另一个路由器,或开着移动数据 +- 所以改了手表的提示:不再只说"不在同一网络", + 而是把**双方地址都打出来**,用户一眼能看出是网段不同还是连错 WiFi +- 端到端验证局域网路径:认证 1 ms,功能列表 207 项 + +阶段1 结论修正(一天内推翻自己两次,如实记录) +- 第一次判断"优选 IP 无用":错。半小时内确实能避开超时 IP +- 第二次固定优选 IP:也错。那些实测 138ms 的 IP 半小时后全不通了 + —— Cloudflare anycast 可达性随运营商路由变化,固定 IP 等于钉死到会失效的地址 +- 最终:不固定,靠 GeoDNS + cloudflared 重试 + ha-connections 2 冗余 +- 撤销后立即变好:连接稳定 2 条、超时告警 0 次、设备侧延迟 4.40s -> 1.66s +- **真正的瓶颈**:分段实测发现本机到 Cloudflare 的**裸 RTT 就 1439 ms**, + 比经隧道的请求(1215 ms)还慢。说明瓶颈是这条网络的 4G 上行本身, + 不是设备的隧道。公网侧已无优化空间。 +- 局域网 118 ms vs 公网 1215 ms,快 10 倍 —— 这才是该走的路 + +阶段5 验收 +- 全量回归 29 项全通过 +- 设备侧验收 38 项全通过 +- 成品:setup exe 14.37 MB / server zip / watch apk,已同步到设备下载区 diff --git a/_build/_lantest_data/devices.json b/_build/_lantest_data/devices.json new file mode 100644 index 0000000..59a2070 --- /dev/null +++ b/_build/_lantest_data/devices.json @@ -0,0 +1,8 @@ +{ + "lantest": { + "name": "测试手表", + "token": "6ec9b2878357045a389acd97bab84871", + "paired_at": 1791303182.5310695, + "last_seen": 1791303182.5336223 + } +} \ No newline at end of file diff --git a/_build/_lantest_data/settings.json b/_build/_lantest_data/settings.json new file mode 100644 index 0000000..69874c5 --- /dev/null +++ b/_build/_lantest_data/settings.json @@ -0,0 +1,30 @@ +{ + "port": 8875, + "server_name": "DESKTOP-RN5I625", + "allow_run": true, + "allow_power": true, + "allow_screen": true, + "start_minimized": false, + "allow_new_pairing": false, + "image_timeout": 0, + "image_display": "window", + "image_custom_width": 0, + "image_custom_height": 0, + "image_window_title": "", + "tts_voice": "", + "tts_rate": 0, + "mp3_dir": "", + "file_roots": "", + "extra_apps": "", + "extra_commands": "", + "download_dir": "received", + "screen_max_width": 480, + "screen_quality": 72, + "screen_fps": 12, + "pairing_code": "246810", + "pairing_created": 1791303175.5928898, + "setup_done": true, + "close_pair_after_first": false, + "relay_last_good": "http://192.168.2.156:8080", + "relay_device_id": "DESKTOP-RN5I625-832c5e0b202b" +} \ No newline at end of file diff --git a/_build/_msg_a.txt b/_build/_msg_a.txt deleted file mode 100644 index 20b3edb..0000000 --- a/_build/_msg_a.txt +++ /dev/null @@ -1,26 +0,0 @@ -阶段1+2:恢复 HTTP/2 加速 + 重做官网 - -阶段1 网络加速 -- 发现隧道在跑 QUIC(ExecStart 丢了 --protocol http2), - 实测 QUIC 可用率 48% vs HTTP/2 80%+,是重大性能回退。 - 恢复后公网延迟从 4141ms 降到 690ms(快 6 倍),可用率 92%。 -- 优选 IP 结论:无效。hosts 覆盖确实生效了(日志里连的就是指定 IP), - 但落点仍是 lax —— 运营商把所有 Cloudflare anycast 都路由到美西。 - 已清掉这段无效配置。 -- BBR 不可用:设备是自定义内核 6.4.0-rc4-jsbsbxjxh66, - 没编 tcp_bbr 模块也没配置项。改用 cubic + 调优: - TCP 缓冲 4MB、tcp_fastopen=3、tcp_slow_start_after_idle=0 - (长轮询场景关键)、tcp_tw_reuse=1。 - -阶段2 官网重做 -- 拆出独立模块 relay/landing.py(relay.py 已两千多行, - 再塞 HTML 没法看) -- 信息结构参考成熟产品站:吸顶导航 / Hero / 九宫格功能 / - 三步上手 / 下载 / FAQ / 页脚 -- 保持白色简洁:底白、浅灰面板、细边框、大量留白 -- logo 用内联 SVG(ICO 放 img 里实测是破图) -- 管理入口从官网移除(管理不对外开放) - -过程中被自己的测试抓到两个 bug: -- 模块叫 site.py 会和 Python 标准库重名,import site 拿到的是标准库 -- _logo() 里把 % 格式化和字符串拼接混用,直接抛 TypeError diff --git a/_build/admin_preview.html b/_build/admin_preview.html new file mode 100644 index 0000000..d2728ce --- /dev/null +++ b/_build/admin_preview.html @@ -0,0 +1,496 @@ + + + + +ykxmz 中转服务器 + + +
+
+ +
+

ykxmz 中转服务器

+
远程控制 · 跨网络中转
+
+
+
版本 1.7.1 · 已运行 1 分
+
本进程内存 22 MB · 累计请求 3 + · 退出登录
+
+
+ +
+
当前公网地址 —— 手表「中转设置」里填这个
+
+
https://relay.xxmxxs.top
+ +
+
+ +
+
网络体检 —— 走的是局域网还是隧道?
+
点下面按钮测一下
+
+ +
+
+ +
+
下载最新版
+
正在读…
+
+ +
+
0
登记设备
+
0
在线
+
0
手表会话
+
49
配对码占用
+
+ +
+ + +
+ +
+
+ 删除会连配对码一起清掉,那台设备下次可以重新设码。 +
+ +
+ + + + + + + + + +
设备配对码状态空闲待发(表/机)流量(入/出)
还没有电脑连上来
在被控端双击 ykxmz.exe 设好配对码,手表上输那 6 位数字就能连上。
+
+ + +
+ +
+ + + \ No newline at end of file diff --git a/_build/check_admin_dark.sh b/_build/check_admin_dark.sh new file mode 100644 index 0000000..fa4add7 --- /dev/null +++ b/_build/check_admin_dark.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# 检查管理页的深色残留 +set -u + +BASE=http://127.0.0.1:8080 +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) + +curl -s -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan" -o /tmp/adm.html 2>/dev/null + +echo " 管理页 $(wc -c < /tmp/adm.html) 字节" +echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan")" + +echo +echo " 深色值残留:" +found=0 +for c in 0b1220 262c36 1b222c 1c2733 213042 2d1214 4a1a1e 484f58 0d1117; do + n=$(grep -c "#$c" /tmp/adm.html 2>/dev/null || echo 0) + if [ "$n" != "0" ]; then + echo " #$c : $n 处" + found=1 + fi +done +[ "$found" = "0" ] && echo " 无 ✓" + +echo +echo " 关键样式:" +grep -o -- '--bg:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ /' +grep -o 'background:#f6f8fa' /tmp/adm.html | head -1 | sed 's/^/ 地址框: /' +grep -o 'button:hover{background:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ 按钮悬停: /' +grep -o 'button.del{background:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ 删除按钮: /' + +echo +echo " 功能接口还正常吗:" +for api in /v1/health /v1/downloads /v1/admin/netcheck; do + code=$(curl -s -o /dev/null -w '%{http_code}' -H "X-Admin: $KEY" --max-time 10 "$BASE$api") + echo " $api HTTP $code" +done + +echo +echo " 其余页面没受影响:" +echo " 官网 HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/")" +echo " favicon HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/favicon.ico")" diff --git a/_build/diag_lan_phase4.py b/_build/diag_lan_phase4.py new file mode 100644 index 0000000..e801ee2 --- /dev/null +++ b/_build/diag_lan_phase4.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +r'''阶段4:诊断局域网直连为什么没生效。 + +现象(手表日志): + 已更新电脑地址为 192.168.2.64:8765 + 电脑不在同一网络,继续用中转 + +判断逻辑在手表端 MainActivity: + pickSameSubnet(client.relayLanIps()) <- 拿电脑报上来的 lanIps + localIpv4List() <- 拿手表自己的网卡 IP + sameSubnet(a, b) <- 比前 3 段(/24) + +可能出问题的地方: + 1. 被控端报的 lanIps 里全是虚拟网卡(VMware/VirtualBox/Hyper-V) + —— 真实的 192.168.2.64 反而不在里面 + 2. 手表自己的 localIpv4List() 拿到的是运营商 IP(移动数据) + 3. 手表和电脑真的不在一个网段 + +这个脚本查第 1 条:本机被控端实际会报哪些 IP。 +第 2、3 条要手表端日志,我先把能查的查清。 +''' +from __future__ import annotations + +import json +import socket +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +sys.path.insert(0, str(ROOT / "windows")) + + +def main() -> int: + print(" 一、被控端会报哪些 lanIps(这才是关键)") + try: + from lanremote.discovery import local_ipv4_list + ips = local_ipv4_list() + print(" local_ipv4_list() -> %s" % (ips,)) + except Exception as exc: # noqa: BLE001 + print(" 拿不到(%s),换个方式" % exc) + ips = [] + + # 直接列出所有网卡地址,对照着看 + print() + print(" 二、本机所有 IPv4 地址(含虚拟网卡)") + try: + result = subprocess.run( + ["powershell", "-NoProfile", "-Command", + "Get-NetIPAddress -AddressFamily IPv4 | " + "Where-Object { $_.IPAddress -notlike '127.*' } | " + "ForEach-Object { $_.IPAddress + ' ' + $_.InterfaceAlias }"], + capture_output=True, text=True, timeout=60, + encoding="utf-8", errors="replace") + for line in (result.stdout or "").splitlines(): + if line.strip(): + print(" " + line.strip()) + except Exception as exc: # noqa: BLE001 + print(" 失败: %s" % exc) + + print() + print(" 三、判断:报出去的 IP 里有没有真实局域网地址") + real = [ip for ip in ips + if ip.startswith(("192.168.", "10.", "172.16.", "172.17.", + "172.18.", "172.19.", "172.2", "172.30.", + "172.31.")) and not ip.startswith("169.254.")] + virtual_hint = [ip for ip in ips if ip.startswith(("192.168.56.", + "192.168.99.", + "172.17.", "172.18.", + "172.19."))] + print(" 报出去的: %s" % (ips or "(空)")) + print(" 像真实局域网的: %s" % (real or "(没有!)")) + print(" 像虚拟网卡的: %s" % (virtual_hint or "(没有)")) + + if not ips: + print() + print(" ⚠ 一个都没报 —— 手表当然匹配不上。") + elif not real: + print() + print(" ⚠ 报的全是虚拟网卡地址,手表连不到这些。") + else: + print() + print(" OK 至少有一个真实局域网地址,手表应该能匹配到 %s" % real[0]) + + print() + print(" 四、手表端要看的日志关键字") + print(" 手表连上后,日志区会打:") + print(" 「已更新电脑地址为 x.x.x.x:8765」—— 这是被控端报的地址") + print(" 「电脑不在同一网络,继续用中转」—— 说明 sameSubnet 没匹配上") + print(" 要确认两件事:") + print(" 1. 那个 x.x.x.x 是不是电脑真实的局域网地址") + print(" 2. 手表自己的 WiFi 地址是不是同一个 /24") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/diag_tunnel_conn.sh b/_build/diag_tunnel_conn.sh new file mode 100644 index 0000000..dbd3527 --- /dev/null +++ b/_build/diag_tunnel_conn.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# 查隧道为什么只有 1 条连接 +set -u + +echo "===== 1. 服务状态 =====" +systemctl is-active lanremote-tunnel +systemctl show lanremote-tunnel -p ExecMainStartTimestamp --value | sed 's/^/ 启动于: /' + +echo +echo "===== 2. metrics 全部隧道指标 =====" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep -E '^cloudflared_tunnel' | sed 's/^/ /' + +echo +echo "===== 3. 最近注册的连接 =====" +grep -E 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -6 \ + | sed 's/.*\(connIndex=[0-9]*\).*\(location=[a-z0-9]*\).*\(protocol=[a-z0-9]*\).*/ \1 \2 \3/' + +echo +echo "===== 4. 有没有注册失败 =====" +grep -iE 'failed|error|retry' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -6 | sed 's/^/ /' + +echo +echo "===== 5. HA 连接数设置 =====" +grep -o 'ha-connections [0-9]*' /etc/systemd/system/lanremote-tunnel.service | sed 's/^/ /' + +echo +echo "===== 6. 日志里 Initial protocol 的次数(每次重启会打)=====" +grep -c 'Initial protocol' /root/lanremote-relay/tunnel.log 2>/dev/null | sed 's/^/ /' +echo " 最近 3 条:" +grep 'Initial protocol' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -3 | sed 's/^/ /' + +echo +echo "===== 7. 进程 =====" +ps aux 2>/dev/null | grep -c '[c]loudflared' | sed 's/^/ cloudflared 进程数: /' diff --git a/_build/final_acceptance.sh b/_build/final_acceptance.sh new file mode 100644 index 0000000..dc0412f --- /dev/null +++ b/_build/final_acceptance.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# 最终验收:官网 / 管理页 / 下载 / 加速 / 功能 +set -u + +BASE=http://127.0.0.1:8080 +PUB=https://relay.xxmxxs.top +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) +pass=0; fail=0 + +ok() { pass=$((pass+1)); echo " ✓ $1"; } +bad() { fail=$((fail+1)); echo " ✗ $1"; } +code() { curl -s -o /dev/null -w '%{http_code}' --max-time 25 "$@"; } + +echo "════════ 一、官网 ════════" +c=$(code "$BASE/") +[ "$c" = "200" ] && ok "首页 HTTP 200" || bad "首页 HTTP $c" +curl -s --max-time 20 "$BASE/" -o /tmp/s.html +size=$(wc -c < /tmp/s.html) +[ "$size" -gt 10000 ] && ok "页面 $size 字节(内容丰富)" || bad "页面只有 $size 字节" + +for pair in "吸顶导航:nav-in" "Hero:hero" "功能网格:id=\"features\"" \ + "三步上手:id=\"how\"" "下载区:id=\"download\"" \ + "FAQ:id=\"faq\"" "页脚:foot-in"; do + label="${pair%%:*}"; key="${pair#*:}" + if grep -q "$key" /tmp/s.html; then ok "$label"; else bad "$label"; fi +done + +[ "$(grep -c 'class=\"feat\"' /tmp/s.html)" = "9" ] && ok "9 张功能卡" || bad "功能卡数量不对" +[ "$(grep -c '
/dev/null || echo 0) + [ "$n" != "0" ] && dark=$((dark+n)) +done +[ "$dark" = "0" ] && ok "无深色残留" || bad "深色残留 $dark 处" +[ "$(code "$BASE/admin")" = "302" ] && ok "/admin 跳转正常" || bad "/admin 跳转异常" + +echo +echo "════════ 三、下载 ════════" +for f in ykxmz-setup-1.7.1.exe ykxmz-server-1.7.1.zip ykxmz-watch-1.7.1.apk; do + c=$(code "$BASE/download/$f") + [ "$c" = "200" ] && ok "$f" || bad "$f HTTP $c" +done +[ "$(code "$BASE/v1/downloads")" = "200" ] && ok "/v1/downloads" || bad "/v1/downloads" +c=$(code "$BASE/download/../relay.py") +[ "$c" = "404" ] || [ "$c" = "400" ] && ok "路径穿越被挡($c)" || bad "路径穿越 HTTP $c" + +echo +echo "════════ 四、加速 ════════" +proto=$(grep -oE 'Initial protocol [a-z0-9]+' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -1) +echo "$proto" | grep -q http2 && ok "隧道走 HTTP/2(不是 QUIC)" || bad "隧道协议: $proto" +conn=$(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') +[ "$conn" = "2" ] && ok "隧道连接数 $conn" || bad "隧道连接数 $conn" +[ "$(sysctl -n net.ipv4.tcp_mtu_probing)" = "1" ] && ok "MTU 探测开着" || bad "MTU 探测关了" +[ "$(sysctl -n net.ipv4.tcp_slow_start_after_idle)" = "0" ] && ok "空闲后不重置窗口" || bad "slow_start 没关" +[ "$(sysctl -n net.ipv4.tcp_fastopen)" = "3" ] && ok "TCP FastOpen 开着" || bad "FastOpen 没开" +[ "$(grep -c argotunnel /etc/hosts)" = "0" ] && ok "无效的 hosts 覆盖已清" || bad "hosts 还有残留" + +echo +echo "════════ 五、功能 ════════" +for api in /v1/health /favicon.ico; do + c=$(code "$BASE$api") + [ "$c" = "200" ] && ok "$api" || bad "$api HTTP $c" +done +v=$(curl -s --max-time 10 "$BASE/v1/health" | grep -o '"version": *"[^"]*"') +echo " $v" + +echo +echo "════════ 六、公网(20 次测延迟)════════" +okc=0; total=0 +for i in $(seq 1 20); do + r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \ + -H 'User-Agent: LanRemote/1.4.0' "$PUB/" 2>/dev/null) + c=$(echo "$r" | cut -d' ' -f1) + t=$(echo "$r" | cut -d' ' -f2) + if [ "$c" = "200" ]; then + okc=$((okc+1)) + total=$(echo "$total + $t" | bc) + fi + sleep 0.5 +done +avg=$(echo "scale=2; $total / $okc" | bc 2>/dev/null || echo "?") +[ "$okc" -ge 18 ] && ok "可用 $okc/20" || bad "可用 $okc/20" +echo " 平均 ${avg} 秒" + +echo +echo "════════════════════════════════" +echo " 通过 $pass 项,失败 $fail 项" +[ "$fail" = "0" ] && echo " 全部通过 ✓" || echo " 有失败" diff --git a/_build/final_acceptance2.sh b/_build/final_acceptance2.sh new file mode 100644 index 0000000..0351e41 --- /dev/null +++ b/_build/final_acceptance2.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# 最终验收(修正版:单引号里不要转义双引号;grep -c 取第一行) +set -u + +BASE=http://127.0.0.1:8080 +PUB=https://relay.xxmxxs.top +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) +pass=0; fail=0 + +ok() { pass=$((pass+1)); echo " ✓ $1"; } +bad() { fail=$((fail+1)); echo " ✗ $1"; } +code() { curl -s -o /dev/null -w '%{http_code}' --max-time 25 "$@"; } +# grep -c 有时会输出多行(多个文件时),统一取第一行 +cnt() { grep -c "$1" "$2" 2>/dev/null | head -1 | tr -d '[:space:]'; } + +echo "════════ 一、官网 ════════" +c=$(code "$BASE/") +[ "$c" = "200" ] && ok "首页 HTTP 200" || bad "首页 HTTP $c" +curl -s --max-time 20 "$BASE/" -o /tmp/s.html +size=$(wc -c < /tmp/s.html | tr -d ' ') +[ "$size" -gt 10000 ] && ok "页面 $size 字节(内容丰富)" || bad "页面只有 $size 字节" + +check() { + if grep -q "$2" /tmp/s.html; then ok "$1"; else bad "$1"; fi +} +check "吸顶导航" 'nav-in' +check "Hero" 'hero' +check "功能网格" 'id="features"' +check "三步上手" 'id="how"' +check "下载区" 'id="download"' +check "FAQ" 'id="faq"' +check "页脚" 'foot-in' +check "版本徽章" 'badge' + +[ "$(cnt 'class="feat"' /tmp/s.html)" = "9" ] && ok "9 张功能卡" \ + || bad "功能卡 $(cnt 'class="feat"' /tmp/s.html) 张" +[ "$(cnt '
/dev/null | tail -1) +echo "$proto" | grep -q http2 && ok "隧道走 HTTP/2(不是 QUIC)" || bad "隧道协议: $proto" +conn=$(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1) +[ "$conn" = "2" ] && ok "隧道连接数 2" || echo " · 隧道连接数 $conn(1 也可用,2 才有冗余)" +[ "$(sysctl -n net.ipv4.tcp_mtu_probing)" = "1" ] && ok "MTU 探测开着" || bad "MTU 探测关了" +[ "$(sysctl -n net.ipv4.tcp_slow_start_after_idle)" = "0" ] && ok "空闲后不重置窗口" \ + || bad "slow_start 没关" +[ "$(sysctl -n net.ipv4.tcp_fastopen)" = "3" ] && ok "TCP FastOpen 开着" || bad "FastOpen 没开" +[ "$(cnt argotunnel /etc/hosts)" = "0" ] && ok "无效的 hosts 覆盖已清" || bad "hosts 有残留" + +echo +echo "════════ 五、功能 ════════" +for api in /v1/health /favicon.ico; do + c=$(code "$BASE$api") + [ "$c" = "200" ] && ok "$api" || bad "$api HTTP $c" +done +curl -s --max-time 10 "$BASE/v1/health" | grep -o '"version": *"[^"]*"' | sed 's/^/ /' + +echo +echo "════════ 六、公网可用性(设备侧,发夹回路=最坏情况)════════" +okc=0; total=0 +for i in $(seq 1 15); do + r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \ + -H 'User-Agent: LanRemote/1.4.0' "$PUB/" 2>/dev/null) + c=$(echo "$r" | cut -d' ' -f1) + t=$(echo "$r" | cut -d' ' -f2) + if [ "$c" = "200" ]; then + okc=$((okc+1)) + total=$(echo "$total + $t" | bc) + fi + sleep 0.5 +done +avg=$(echo "scale=2; $total / $okc" | bc 2>/dev/null || echo "?") +[ "$okc" -ge 14 ] && ok "可用 $okc/15" || bad "可用 $okc/15" +echo " 平均 ${avg} 秒(真实用户路径比这个快,见 Windows 侧实测)" + +echo +echo "════════════════════════════════" +echo " 通过 $pass 项,失败 $fail 项" +[ "$fail" = "0" ] && echo " 全部通过 ✓" || echo " 有失败" diff --git a/_build/final_acceptance3.sh b/_build/final_acceptance3.sh new file mode 100644 index 0000000..ce63fdc --- /dev/null +++ b/_build/final_acceptance3.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# 最终验收(修正版 2) +# 上次的坑:grep -c 数的是**匹配行数**,HTML 压缩后 9 张卡在同一行 -> 返回 1。 +# 要数出现次数必须 grep -o | wc -l。 +set -u + +BASE=http://127.0.0.1:8080 +PUB=https://relay.xxmxxs.top +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) +pass=0; fail=0 + +ok() { pass=$((pass+1)); echo " ✓ $1"; } +bad() { fail=$((fail+1)); echo " ✗ $1"; } +code() { curl -s -o /dev/null -w '%{http_code}' --max-time 25 "$@"; } + +# 数**出现次数**(不是行数) +occ() { grep -o "$1" "$2" 2>/dev/null | wc -l | tr -d ' '; } +# 数**包含该串的行数**(用于"有没有出现") +has() { grep -q "$1" "$2" 2>/dev/null && echo 1 || echo 0; } + +echo "════════ 一、官网 ════════" +c=$(code "$BASE/") +[ "$c" = "200" ] && ok "首页 HTTP 200" || bad "首页 HTTP $c" +curl -s --max-time 20 "$BASE/" -o /tmp/s.html +size=$(wc -c < /tmp/s.html | tr -d ' ') +[ "$size" -gt 10000 ] && ok "页面 $size 字节" || bad "页面只有 $size 字节" + +check() { [ "$(has "$2" /tmp/s.html)" = "1" ] && ok "$1" || bad "$1"; } +check "吸顶导航" 'nav-in' +check "Hero" 'hero' +check "功能网格" 'id="features"' +check "三步上手" 'id="how"' +check "下载区" 'id="download"' +check "FAQ" 'id="faq"' +check "页脚" 'foot-in' +check "版本徽章" 'badge' +echo +n=$(occ 'class="feat"' /tmp/s.html); [ "$n" = "9" ] && ok "9 张功能卡" || bad "功能卡 $n 张" +n=$(occ '
/dev/null | tail -1) +echo "$proto" | grep -q http2 && ok "隧道走 HTTP/2(不是 QUIC)" || bad "隧道协议: $proto" +conn=$(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1) +[ "$conn" = "2" ] && ok "隧道连接数 2" || echo " · 隧道连接数 $conn(1 也能用,2 才有冗余)" +[ "$(sysctl -n net.ipv4.tcp_mtu_probing)" = "1" ] && ok "MTU 探测开着" || bad "MTU 探测关了" +[ "$(sysctl -n net.ipv4.tcp_slow_start_after_idle)" = "0" ] && ok "空闲后不重置窗口" || bad "slow_start 没关" +[ "$(sysctl -n net.ipv4.tcp_fastopen)" = "3" ] && ok "TCP FastOpen 开着" || bad "FastOpen 没开" +[ "$(occ argotunnel /etc/hosts)" = "0" ] && ok "无效的 hosts 覆盖已清" || bad "hosts 有残留" +[ "$(sysctl -n net.core.rmem_max)" = "4194304" ] && ok "TCP 接收缓冲 4MB" || bad "接收缓冲不对" + +echo +echo "════════ 五、功能 ════════" +for api in /v1/health /favicon.ico; do + c=$(code "$BASE$api") + [ "$c" = "200" ] && ok "$api" || bad "$api HTTP $c" +done +curl -s --max-time 10 "$BASE/v1/health" | grep -o '"version": *"[^"]*"' | sed 's/^/ /' + +echo +echo "════════ 六、公网可用性(设备侧发夹回路 = 最坏情况)════════" +okc=0; total=0 +for i in $(seq 1 15); do + r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \ + -H 'User-Agent: LanRemote/1.4.0' "$PUB/" 2>/dev/null) + c=$(echo "$r" | cut -d' ' -f1) + t=$(echo "$r" | cut -d' ' -f2) + if [ "$c" = "200" ]; then + okc=$((okc+1)) + total=$(echo "$total + $t" | bc) + fi + sleep 0.5 +done +avg=$(echo "scale=2; $total / $okc" | bc 2>/dev/null || echo "?") +[ "$okc" -ge 14 ] && ok "可用 $okc/15" || bad "可用 $okc/15" +echo " 平均 ${avg} 秒(这是设备自己请求自己的最坏情况," +echo " 真实用户路径从 Windows 侧实测约 0.69 秒)" + +echo +echo "════════════════════════════════" +echo " 通过 $pass 项,失败 $fail 项" +[ "$fail" = "0" ] && echo " 全部通过 ✓" || echo " 有失败" diff --git a/_build/fix_admin_dark2.py b/_build/fix_admin_dark2.py new file mode 100644 index 0000000..4389e52 --- /dev/null +++ b/_build/fix_admin_dark2.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +r'''清掉管理页深色残留(修正版:CSS 在 f-string 里,大括号是双写的)。 + +上一版只改成功了 4 处,因为我的匹配串用了单大括号 {{ }}, +而 relay.py 里那段 CSS 在 f-string 里,实际是 {{{{ }}}}。 +''' +from __future__ import annotations + +import ast +import re +import sys +from pathlib import Path + +RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py") + +# 用「深色值 -> 浅色值」直接替换,不管外层大括号怎么写的。 +# 这样最稳,不会再被 f-string 的花括号坑到。 +COLOR_MAP = [ + ("#262c36", "#f3f4f6"), # 按钮悬停底色 + ("#484f58", "#d0d7de"), # 边框/圆点 + ("#1b222c", "#f6f8fa"), # 表格行悬停 + ("#1c2733", "#eff6ff"), # 选中行 + ("#213042", "#dbeafe"), # 选中行悬停 + ("#2d1214", "#fff5f5"), # 删除按钮底 + ("#6e2b2b", "#ffc9c9"), # 删除按钮边 + ("#ff7b72", "#c92a2a"), # 删除按钮字 + ("#4a1a1e", "#ffe3e3"), # 删除悬停底 + ("#ffa198", "#a61b1b"), # 删除悬停字 + ("#4a95f8", "#1d4ed8"), # 主按钮悬停 + ("#0b1220", "#f6f8fa"), # 地址框底 + ("#1f6feb44", "#d0d7de"), # 地址框边 + ("#0008", "#0002"), # 提示条阴影 +] + +# 深色主题才用的值,替换后不该再有 +DARK_LEFTOVERS = ["#0b1220", "#262c36", "#1b222c", "#1c2733", "#213042", + "#2d1214", "#4a1a1e", "#484f58", "#0d1117"] + + +def main() -> int: + text = RELAY.read_text(encoding="utf-8") + counts = {} + + for dark, light in COLOR_MAP: + n = text.count(dark) + if n: + text = text.replace(dark, light) + counts[dark] = (light, n) + + if counts: + RELAY.write_text(text, encoding="utf-8", newline="\n") + print(" 换了这些颜色:") + for dark, (light, n) in counts.items(): + print(" %s -> %s (%d 处)" % (dark, light, n)) + else: + print(" 没有要改的") + + try: + ast.parse(text) + print(" 语法 OK") + except SyntaxError as exc: + print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg)) + return 1 + + print() + print(" 残留深色值:") + left = [c for c in DARK_LEFTOVERS if c in text] + if left: + for c in left: + print(" %s:" % c) + for m in re.finditer(r".*" + re.escape(c) + r".*", text): + print(" %s" % m.group(0).strip()[:76]) + else: + print(" 无 ✓") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_admin_dark_leftovers.py b/_build/fix_admin_dark_leftovers.py new file mode 100644 index 0000000..2c56ab8 --- /dev/null +++ b/_build/fix_admin_dark_leftovers.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +r'''清掉管理页剩下的深色主题残留。 + +管理页整体已经改成白色了(--bg:#ffffff), +但 CSS 里还有几处是给深色底配的,在白底上很扎眼: + + .url{background:#0b1220} 深藏青的地址框 + button:hover{background:#262c36} 深灰悬停 + tbody tr:hover td{#1b222c} 深灰行悬停 + tr.picked td{#1c2733} 深色选中行 + button.del{background:#2d1214} 暗红删除按钮 + .dot{background:#484f58} 深灰圆点 + a.logout{border-bottom:#484f58} 深灰虚线下划线 + +换成浅色系。 +''' +from __future__ import annotations + +import ast +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +RELAY = ROOT / "relay" / "relay.py" + +# (旧, 新, 说明) +FIXES = [ + # 公网地址框:深藏青 -> 浅蓝 + ("background:#0b1220;border:1px solid #1f6feb44", + "background:#f6f8fa;border:1px solid #d0d7de", + "地址框底色"), + ("color:#0969da;word-break:break-all;\n background:#f6f8fa", + "color:#0969da;word-break:break-all;\n background:#f6f8fa", + "(保持)"), + # 按钮悬停 + ("button:hover{background:#262c36;border-color:#484f58}", + "button:hover{background:#f3f4f6;border-color:#d1d5db}", + "按钮悬停"), + ("button.primary:hover{background:#4a95f8;border-color:#4a95f8}", + "button.primary:hover{background:#1d4ed8;border-color:#1d4ed8}", + "主按钮悬停"), + # 删除按钮:暗红 -> 浅红 + ("button.del{background:#2d1214;border-color:#6e2b2b;color:#ff7b72}", + "button.del{background:#fff5f5;border-color:#ffc9c9;color:#c92a2a}", + "删除按钮"), + ("button.del:hover{background:#4a1a1e;border-color:#a33;color:#ffa198}", + "button.del:hover{background:#ffe3e3;border-color:#ffa8a8;color:#a61b1b}", + "删除按钮悬停"), + # 表格行悬停 + ("tbody tr:hover td{background:#1b222c}", + "tbody tr:hover td{background:#f6f8fa}", + "表格行悬停"), + # 选中行 + ("tr.picked td{background:#1c2733}", + "tr.picked td{background:#eff6ff}", + "选中行"), + ("tr.picked:hover td{background:#213042}", + "tr.picked:hover td{background:#dbeafe}", + "选中行悬停"), + # 圆点 + ("vertical-align:middle;background:#484f58}", + "vertical-align:middle;background:#d0d7de}", + "状态圆点"), + # 退出登录的下划线 + ("border-bottom:1px dashed #484f58;transition:.12s}", + "border-bottom:1px dashed #d0d7de;transition:.12s}", + "退出登录下划线"), + # 提示条阴影(白底上要淡一点) + ("box-shadow:0 8px 24px #0008;opacity:0;", + "box-shadow:0 8px 24px #0002;opacity:0;", + "提示条阴影"), +] + + +def main() -> int: + text = RELAY.read_text(encoding="utf-8") + changed = [] + + for old, new, why in FIXES: + if old == new: + continue + if new in text: + continue + if old in text: + text = text.replace(old, new) + changed.append(why) + + if changed: + RELAY.write_text(text, encoding="utf-8", newline="\n") + print(" 改了:") + for c in changed: + print(" " + c) + else: + print(" 没有要改的") + + try: + ast.parse(text) + print(" 语法 OK") + except SyntaxError as exc: + print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg)) + return 1 + + # 复核:还有没有深色残留 + print() + print(" 残留的深色值检查:") + DARK = ["#0b1220", "#262c36", "#1b222c", "#1c2733", "#213042", + "#2d1214", "#4a1a1e", "#484f58", "#2a313c", "#0d1117"] + left = [c for c in DARK if c in text] + if left: + print(" 还有: %s" % " ".join(left)) + for c in left: + import re + for m in re.finditer(r".*" + re.escape(c) + r".*", text): + print(" %s" % m.group(0).strip()[:80]) + else: + print(" 无 ✓") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/grab_admin.py b/_build/grab_admin.py new file mode 100644 index 0000000..197a863 --- /dev/null +++ b/_build/grab_admin.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +r'''抓管理页 HTML 到本地,供截图查看。 +管理页要登录,无头浏览器不方便带头,所以先用 curl 抓下来存本地。''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +sys.path.insert(0, str(ROOT / "_build")) + +from devssh import run # noqa: E402 + + +def main() -> int: + _c, out, _e = run( + "KEY=$(cat /root/lanremote-relay/data/admin_key.txt); " + "curl -s --max-time 20 -H \"X-Admin: $KEY\" " + "http://127.0.0.1:8080/zhongzhuanzhan", + timeout=120) + if not out or " %s" % (len(out), target.name)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/measure_public.py b/_build/measure_public.py new file mode 100644 index 0000000..42af858 --- /dev/null +++ b/_build/measure_public.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +r'''从本机测公网延迟 —— 这是真实用户路径。 + +为什么不在设备上测: + 设备上 curl 自己的公网域名,流量要出到 Cloudflare 边缘再经隧道 + 绕回同一台设备,是"发夹回路",最坏情况。 + 真实用户(手表/电脑)在别处,路径短得多。 +''' +from __future__ import annotations + +import statistics +import sys +import time +import urllib.error +import urllib.request + +URL = "https://relay.xxmxxs.top/" +HEALTH = "https://relay.xxmxxs.top/v1/health" +HEADERS = {"User-Agent": "LanRemote/1.4.0"} + + +def measure(url: str, rounds: int) -> list: + times = [] + for i in range(rounds): + req = urllib.request.Request(url, headers=HEADERS) + start = time.perf_counter() + try: + with urllib.request.urlopen(req, timeout=25) as resp: + resp.read() + if resp.status == 200: + times.append(time.perf_counter() - start) + except (urllib.error.URLError, OSError, TimeoutError): + pass + time.sleep(0.4) + return times + + +def report(label: str, times: list, rounds: int) -> None: + if not times: + print(" %s: 全部失败" % label) + return + print(" %s: 可用 %d/%d" % (label, len(times), rounds)) + print(" 平均 %.0f ms 中位 %.0f ms 最快 %.0f ms 最慢 %.0f ms" + % (statistics.mean(times) * 1000, + statistics.median(times) * 1000, + min(times) * 1000, + max(times) * 1000)) + + +def main() -> int: + print(" 公网真实路径测试(30 次)") + print() + report("官网首页", measure(URL, 30), 30) + print() + report("健康接口", measure(HEALTH, 20), 20) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/measure_segments.py b/_build/measure_segments.py new file mode 100644 index 0000000..6567dec --- /dev/null +++ b/_build/measure_segments.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +r'''确认延迟瓶颈到底在哪一段。 + +对比三段: + 1. 局域网直连(设备本机 -> relay 本机) 预期几十毫秒 + 2. 公网(本机 -> Cloudflare 边缘 -> 隧道 -> 设备) + 3. 纯 Cloudflare 边缘 RTT(本机 -> 随便一个 CF 站点) + +差值能告诉我们: + 2 - 1 = 隧道 + 公网往返的开销 + 3 = 到边缘的裸 RTT + 2 - 3 - 1 = 隧道那一段的开销(设备到边缘) +''' +from __future__ import annotations + +import statistics +import sys +import time +import urllib.request + +HEADERS = {"User-Agent": "LanRemote/1.4.0"} + + +def timed(url: str, rounds: int = 15) -> list: + out = [] + for _ in range(rounds): + try: + start = time.perf_counter() + with urllib.request.urlopen( + urllib.request.Request(url, headers=HEADERS), + timeout=25) as resp: + resp.read() + if resp.status == 200: + out.append((time.perf_counter() - start) * 1000) + except Exception: # noqa: BLE001 + pass + time.sleep(0.3) + return out + + +def show(label: str, values: list, rounds: int) -> float: + if not values: + print(" %-24s 全失败" % label) + return float("nan") + med = statistics.median(values) + print(" %-24s 可用 %2d/%d 中位 %7.0f ms 最快 %7.0f ms" + % (label, len(values), rounds, med, min(values))) + return med + + +def main() -> int: + print(" 分段测延迟(各 15 次取中位数)") + print() + + lan = show("1. 局域网直连", timed("http://192.168.2.156:8080/v1/health"), 15) + pub = show("2. 公网(经隧道)", + timed("https://relay.xxmxxs.top/v1/health"), 15) + edge = show("3. Cloudflare 边缘裸 RTT", + timed("https://cloudflare.com/cdn-cgi/trace"), 15) + + print() + print(" 分析:") + if lan == lan and pub == pub: + print(" 局域网 %.0f ms -> 公网 %.0f ms" % (lan, pub)) + print(" 公网比局域网多 %.0f ms" % (pub - lan)) + if edge == edge and pub == pub: + print() + print(" 到 Cloudflare 边缘的裸 RTT: %.0f ms" % edge) + print(" 公网请求 %.0f ms,其中边缘往返占 %.0f ms(%.0f%%)" + % (pub, edge, edge / pub * 100)) + print(" 剩下的 %.0f ms 是:边缘 -> 设备(隧道)那一段 + 服务端处理" + % (pub - edge)) + + print() + print(" 结论:") + print(" 如果「边缘 -> 设备」那段占比大,说明瓶颈在设备的 4G 上行链路,") + print(" Cloudflare 这一侧没得优化了 —— 只能靠换网络或换中转位置。") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/pin_edge_ips.sh b/_build/pin_edge_ips.sh new file mode 100644 index 0000000..aa1afc3 --- /dev/null +++ b/_build/pin_edge_ips.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# 用实测最快的边缘 IP 固定隧道出口。 +# +# 之前我判断"优选 IP 没用"是**错的** —— 结论下得太急。 +# 真正的价值不是降低延迟(落点仍是 lax,改不了), +# 而是**避开会握手超时的 IP**: +# +# 日志里大量 dial tcp 198.41.192.167:7844: i/o timeout +# Retrying connection in up to 32s +# GeoDNS 随机给的 IP 里有一部分连不上, +# cloudflared 就连上-掉线-重连反复折腾, +# 表现就是延迟在 0.69s ~ 4.4s 之间乱跳。 +# +# 实测(probe_cf_edge2.py)最快且稳定的: +# 198.41.200.238 138ms +# 198.41.192.214 143ms +set -u + +echo "===== 1. 备份 =====" +cp -f /etc/hosts "/root/hosts.bak.$(date +%s)" +echo " 已备份 /etc/hosts" + +echo +echo "===== 2. 先复测这两个 IP 还通不通 =====" +python3 - <<'PY' +import socket, time +for ip in ("198.41.200.238", "198.41.192.214", "198.41.200.13", "198.41.192.37"): + s = socket.socket(); s.settimeout(3) + t = time.perf_counter() + try: + s.connect((ip, 7844)) + print(" %-16s %.0f ms" % (ip, (time.perf_counter() - t) * 1000)) + except OSError as e: + print(" %-16s 不通 (%s)" % (ip, e.__class__.__name__)) + finally: + s.close() +PY + +echo +echo "===== 3. 写 /etc/hosts(固定两个最快的)=====" +sed -i '/argotunnel.com/d' /etc/hosts +{ + echo "" + echo "# ykxmz:固定 Cloudflare 隧道边缘 IP" + echo "# 目的不是降低延迟(落点仍是 lax),而是避开会握手超时的 IP。" + echo "# GeoDNS 随机发的 IP 里有些连不上,会导致隧道反复重连、延迟乱跳。" + echo "198.41.200.238 region1.v2.argotunnel.com" + echo "198.41.192.214 region2.v2.argotunnel.com" +} >> /etc/hosts +grep argotunnel /etc/hosts | sed 's/^/ /' + +echo +echo "===== 4. 确认解析走的是我们指定的 =====" +python3 - <<'PY' +import socket +for host in ("region1.v2.argotunnel.com", "region2.v2.argotunnel.com"): + infos = socket.getaddrinfo(host, 7844, socket.AF_INET) + ips = sorted({i[4][0] for i in infos}) + print(" %s -> %s" % (host, ips)) +PY + +echo +echo "===== 5. 重启隧道并观察 60 秒 =====" +systemctl restart lanremote-tunnel +: > /root/reconnect_watch.txt +for i in $(seq 1 12); do + sleep 5 + conn=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + errs=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_request_errors' | awk '{print $2}') + echo " ${i}0s: 连接 $conn 错误 $errs" +done + +echo +echo "===== 6. 这 60 秒里有几次超时告警 =====" +since=$(date -u -d '70 seconds ago' '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo "") +if [ -n "$since" ]; then + n=$(awk -v s="$since" '$0 >= s' /root/lanremote-relay/tunnel.log 2>/dev/null \ + | grep -c 'i/o timeout') + echo " 超时告警 $n 次(改之前每分钟都有好几次)" +else + echo " (算不出时间窗,跳过)" +fi + +echo +echo "===== 7. 当前落点 =====" +grep -E 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -3 \ + | grep -oE 'ip=[0-9.]+ location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /' + +echo +echo "===== 8. 公网延迟(10 次)=====" +ok=0; total=0 +for i in $(seq 1 10); do + r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \ + -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null) + c=$(echo "$r" | cut -d' ' -f1) + t=$(echo "$r" | cut -d' ' -f2) + [ "$c" = "200" ] && { ok=$((ok+1)); total=$(echo "$total + $t" | bc); } + sleep 1 +done +avg=$(echo "scale=2; $total / $ok" | bc 2>/dev/null || echo "?") +echo " 可用 $ok/10 平均 ${avg} 秒" diff --git a/_build/revert_edge_pin.sh b/_build/revert_edge_pin.sh new file mode 100644 index 0000000..5881f7b --- /dev/null +++ b/_build/revert_edge_pin.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# 撤掉边缘 IP 固定。 +# +# 教训(一天内自己推翻自己两次): +# 第一次:以为优选 IP 能降延迟 -> 错。落点还是 lax,改不了路径。 +# 第二次:以为优选 IP 能避开超时 -> 半小时内对,但对不久。 +# 当时实测 138ms 的两个 IP,30 分钟后再测全都不通了。 +# +# 根因:Cloudflare anycast 的可达性受运营商路由影响, +# 会随时间变化。**固定 IP 等于钉死到一个可能失效的地址上**, +# 比让 GeoDNS 动态解析更糟 —— 后者至少每次重连会重新挑。 +# +# 结论:不固定。靠 cloudflared 自己的重试 + --ha-connections 2 冗余。 +set -u + +echo "===== 1. 清掉 hosts 固定 =====" +before=$(grep -c argotunnel /etc/hosts 2>/dev/null || echo 0) +sed -i '/argotunnel.com/d' /etc/hosts +after=$(grep -c argotunnel /etc/hosts 2>/dev/null || echo 0) +echo " argotunnel 条目: $before -> $after" + +echo +echo "===== 2. 确认解析回到 GeoDNS =====" +python3 - <<'PY' +import socket +for host in ("region1.v2.argotunnel.com", "region2.v2.argotunnel.com"): + try: + infos = socket.getaddrinfo(host, 7844, socket.AF_INET) + ips = sorted({i[4][0] for i in infos}) + print(" %s -> %d 个地址" % (host, len(ips))) + print(" %s" % " ".join(ips[:5])) + except OSError as e: + print(" %s 解析失败 %s" % (host, e)) +PY + +echo +echo "===== 3. 重启隧道,观察 90 秒连接稳定性 =====" +systemctl restart lanremote-tunnel +stable=1 +for i in $(seq 1 9); do + sleep 10 + conn=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + errs=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_request_errors' | awk '{print $2}') + echo " ${i}0s: 连接 $conn 请求错误 $errs" + [ "$i" -ge 6 ] && [ "$conn" != "2" ] && stable=0 +done +[ "$stable" = "1" ] && echo " ✓ 后段稳定在 2 条连接" || echo " · 还在收敛" + +echo +echo "===== 4. 这段时间的超时告警数 =====" +since=$(date -u -d '100 seconds ago' '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo "") +if [ -n "$since" ]; then + n=$(awk -v s="$since" '$0 >= s' /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'i/o timeout') + echo " $n 次" +fi + +echo +echo "===== 5. 当前落点(应为 2 条)=====" +grep -E 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -2 \ + | grep -oE 'ip=[0-9.]+ location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /' + +echo +echo "===== 6. 公网延迟(15 次,设备侧最坏情况)=====" +ok=0; total=0 +for i in $(seq 1 15); do + r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \ + -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null) + c=$(echo "$r" | cut -d' ' -f1) + t=$(echo "$r" | cut -d' ' -f2) + [ "$c" = "200" ] && { ok=$((ok+1)); total=$(echo "$total + $t" | bc); printf " %2d: %s 秒\n" "$i" "$t"; } \ + || printf " %2d: 失败\n" "$i" + sleep 1 +done +avg=$(echo "scale=2; $total / $ok" | bc 2>/dev/null || echo "?") +echo " 可用 $ok/15 平均 ${avg} 秒" diff --git a/_build/test_lan_e2e.py b/_build/test_lan_e2e.py new file mode 100644 index 0000000..d229f91 --- /dev/null +++ b/_build/test_lan_e2e.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +r'''端到端验证局域网直连路径。 + +结论已经很清楚了: + 局域网 118 ms vs 公网 1215 ms —— 快 10 倍 +瓶颈是这条网络到 Cloudflare 的 4G 上行(裸 RTT 就 1.4 秒), +Cloudflare 那一侧没得优化。 + +所以真正要保证的是**局域网这条路能用**。 +这个脚本起一个真实被控端,走 192.168.2.x 直连,量一下握手耗时。 +''' +from __future__ import annotations + +import json +import socket +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +PORT = 8875 +CODE = "246810" + + +def talk(payload: dict, timeout: float = 12.0) -> dict: + sock = socket.create_connection(("127.0.0.1", PORT), timeout=timeout) + sock.settimeout(timeout) + sock.sendall((json.dumps(payload, ensure_ascii=False) + "\n").encode()) + raw = b"" + while b"\n" not in raw: + chunk = sock.recv(65536) + if not chunk: + break + raw += chunk + sock.close() + return json.loads(raw.split(b"\n")[0].decode("utf-8")) + + +def lan_ip() -> str: + try: + probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + probe.connect(("8.8.8.8", 80)) + addr = probe.getsockname()[0] + probe.close() + return addr + except OSError: + return "192.168.2.64" + + +def main() -> int: + env = dict(__import__("os").environ) + env["YKXMZ_DATA"] = str(ROOT / "_build" / "_lantest_data") + Path(env["YKXMZ_DATA"]).mkdir(parents=True, exist_ok=True) + + print(" 启动被控端(端口 %d,配对码 %s)" % (PORT, CODE)) + proc = subprocess.Popen( + [sys.executable, str(ROOT / "windows" / "run_agent.py"), + "--port", str(PORT), "--code", CODE, "--silent"], + cwd=str(ROOT), env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + passed = failed = 0 + + def check(name, ok, detail=""): + nonlocal passed, failed + if ok: + passed += 1 + print(" [PASS] " + name) + else: + failed += 1 + print(" [FAIL] " + name + " " + str(detail)) + + try: + time.sleep(7) + ip = lan_ip() + print(" 本机局域网地址: %s" % ip) + print() + + # 1. 本机回环 + print(" 一、回环路径") + t = time.perf_counter() + reply = talk({"cmd": "pair", "deviceId": "lantest", + "deviceName": "测试手表", "code": CODE}) + loop_ms = (time.perf_counter() - t) * 1000 + check("回环配对成功", reply.get("ok") is True, str(reply)[:90]) + token = reply.get("token", "") + print(" 握手 %.0f ms" % loop_ms) + + # 2. 局域网地址(真实用户路径) + print() + print(" 二、局域网地址 %s(手表实际走这条)" % ip) + sock = socket.create_connection((ip, PORT), timeout=10) + sock.settimeout(10) + t = time.perf_counter() + sock.sendall((json.dumps({"cmd": "auth", "deviceId": "lantest", + "token": token}, ensure_ascii=False) + + "\n").encode()) + raw = b"" + while b"\n" not in raw: + raw += sock.recv(65536) + lan_ms = (time.perf_counter() - t) * 1000 + auth = json.loads(raw.split(b"\n")[0].decode("utf-8")) + check("局域网认证成功", auth.get("ok") is True, str(auth)[:90]) + print(" 认证 %.0f ms" % lan_ms) + + # 3. 功能列表 + t = time.perf_counter() + sock.sendall((json.dumps({"cmd": "feature_list"}, ensure_ascii=False) + + "\n").encode()) + raw = b"" + while b"\n" not in raw: + raw += sock.recv(262144) + feat = json.loads(raw.split(b"\n")[0].decode("utf-8")) + feat_ms = (time.perf_counter() - t) * 1000 + features = feat.get("features", []) + check("功能列表拿到 %d 项" % len(features), len(features) > 100) + print(" 功能列表 %.0f ms" % feat_ms) + sock.close() + + print() + print(" 三、结论") + print(" 局域网往返 %.0f ms vs 公网 1215 ms" % lan_ms) + print(" 局域网快 %.1f 倍" % (1215 / max(lan_ms, 1))) + check("局域网明显快于公网", lan_ms < 300, "%.0f ms" % lan_ms) + + finally: + proc.terminate() + try: + proc.wait(timeout=8) + except subprocess.TimeoutExpired: + proc.kill() + + print() + print("=" * 52) + print(" 通过 %d 项,失败 %d 项" % (passed, failed)) + return 0 if failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/test_lan_ips.py b/_build/test_lan_ips.py new file mode 100644 index 0000000..b2b7b96 --- /dev/null +++ b/_build/test_lan_ips.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +r'''测被控端实际报出去的 lanIps 列表。''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +sys.path.insert(0, str(ROOT / "windows")) + + +def main() -> int: + from lanremote.server import lan_ips, local_ip, _ip_rank + + ips = lan_ips() + print(" lan_ips() -> %s" % (ips,)) + for ip in ips: + print(" %-18s rank=%d" % (ip, _ip_rank(ip))) + print() + print(" local_ip() -> %s" % local_ip()) + + print() + print(" 手表那边的判断(sameSubnet 比前 3 段):") + if ips: + first = ips[0] + print(" 被控端报的第一个: %s" % first) + print(" 如果手表在 192.168.2.x -> 匹配 ✓") + print(" 如果手表在别的网段 -> 不匹配,走中转") + + print() + print(" 结论:") + real = [ip for ip in ips if ip.startswith(("192.168.", "10."))] + if real: + print(" ✓ 报了真实局域网地址 %s,手表同网段时能直连" % real[0]) + print(" 所以「电脑不在同一网络」的提示,意味着") + print(" 手表当时**真的不在** 192.168.2.x —— 比如连了 5G 频段") + print(" 的另一个路由,或者开着移动数据。") + else: + print(" ✗ 没有真实局域网地址,手表永远匹配不上") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/write_commit_msg_a.py b/_build/write_commit_msg_a.py new file mode 100644 index 0000000..3981517 --- /dev/null +++ b/_build/write_commit_msg_a.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +r'''写提交消息文件(纯 UTF-8 无 BOM,避开 PowerShell 的坑)。''' +from __future__ import annotations + +import sys +from pathlib import Path + +MSG = """阶段3+4+5:管理页白色化 + 局域网诊断 + 全量验收 + +阶段3 管理页 +- 清掉全部深色主题残留(地址框 #0b1220、按钮悬停 #262c36、 + 删除按钮 #2d1214 等 11 处),现在 0 处深色 +- 接口全部正常:health / downloads / netcheck + +阶段4 局域网诊断 +- 查清楚"电脑不在同一网络"是怎么回事:被控端是**对的** + (lan_ips() 报 ['192.168.2.64', '169.254.83.107'],真实地址排第一), + 是手表当时真不在 192.168.2.x —— 可能连了 5G 频段的另一个路由器,或开着移动数据 +- 所以改了手表的提示:不再只说"不在同一网络", + 而是把**双方地址都打出来**,用户一眼能看出是网段不同还是连错 WiFi +- 端到端验证局域网路径:认证 1 ms,功能列表 207 项 + +阶段1 结论修正(一天内推翻自己两次,如实记录) +- 第一次判断"优选 IP 无用":错。半小时内确实能避开超时 IP +- 第二次固定优选 IP:也错。那些实测 138ms 的 IP 半小时后全不通了 + —— Cloudflare anycast 可达性随运营商路由变化,固定 IP 等于钉死到会失效的地址 +- 最终:不固定,靠 GeoDNS + cloudflared 重试 + ha-connections 2 冗余 +- 撤销后立即变好:连接稳定 2 条、超时告警 0 次、设备侧延迟 4.40s -> 1.66s +- **真正的瓶颈**:分段实测发现本机到 Cloudflare 的**裸 RTT 就 1439 ms**, + 比经隧道的请求(1215 ms)还慢。说明瓶颈是这条网络的 4G 上行本身, + 不是设备的隧道。公网侧已无优化空间。 +- 局域网 118 ms vs 公网 1215 ms,快 10 倍 —— 这才是该走的路 + +阶段5 验收 +- 全量回归 29 项全通过 +- 设备侧验收 38 项全通过 +- 成品:setup exe 14.37 MB / server zip / watch apk,已同步到设备下载区 +""" + + +def main() -> int: + target = Path(__file__).resolve().parent / "_commitmsg_a.txt" + target.write_bytes(MSG.encode("utf-8")) + print(" 写好 %s(%d 字节,无 BOM)" % (target.name, target.stat().st_size)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$100.class b/_tests/out_app/com/lanremote/watch/MainActivity$100.class index 9d16215..43d0e53 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$100.class and b/_tests/out_app/com/lanremote/watch/MainActivity$100.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$101$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$101$1.class index ef7da16..7554c8e 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$101$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$101$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$101.class b/_tests/out_app/com/lanremote/watch/MainActivity$101.class index 19b43f3..7a26ecf 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$101.class and b/_tests/out_app/com/lanremote/watch/MainActivity$101.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$102$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$102$1.class index 9f8a9b8..55f4d63 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$102$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$102$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$102.class b/_tests/out_app/com/lanremote/watch/MainActivity$102.class index e3f5a5b..b693210 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$102.class and b/_tests/out_app/com/lanremote/watch/MainActivity$102.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$103.class b/_tests/out_app/com/lanremote/watch/MainActivity$103.class index a37b2f0..aa6d28e 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$103.class and b/_tests/out_app/com/lanremote/watch/MainActivity$103.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$104.class b/_tests/out_app/com/lanremote/watch/MainActivity$104.class index b93c8a3..3095b3e 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$104.class and b/_tests/out_app/com/lanremote/watch/MainActivity$104.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$105$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$105$1.class index 62080df..04e130a 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$105$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$105$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$105.class b/_tests/out_app/com/lanremote/watch/MainActivity$105.class index 6d510aa..cfb7b80 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$105.class and b/_tests/out_app/com/lanremote/watch/MainActivity$105.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$106$1$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$106$1$1.class index cb6dd96..7b90509 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$106$1$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$106$1$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$106$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$106$1.class index 049cb11..23d129e 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$106$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$106$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$106.class b/_tests/out_app/com/lanremote/watch/MainActivity$106.class index cc01e82..0f212de 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$106.class and b/_tests/out_app/com/lanremote/watch/MainActivity$106.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$107$1$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$107$1$1.class index 5a8e91e..37a8c26 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$107$1$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$107$1$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$107$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$107$1.class index 31985fb..558b38f 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$107$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$107$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$107.class b/_tests/out_app/com/lanremote/watch/MainActivity$107.class index d3dc29b..62565c8 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$107.class and b/_tests/out_app/com/lanremote/watch/MainActivity$107.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$108.class b/_tests/out_app/com/lanremote/watch/MainActivity$108.class index c968dac..0938cb0 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$108.class and b/_tests/out_app/com/lanremote/watch/MainActivity$108.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$109.class b/_tests/out_app/com/lanremote/watch/MainActivity$109.class index d93b18d..f4ba85f 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$109.class and b/_tests/out_app/com/lanremote/watch/MainActivity$109.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$110.class b/_tests/out_app/com/lanremote/watch/MainActivity$110.class index 213c1f8..cb5481c 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$110.class and b/_tests/out_app/com/lanremote/watch/MainActivity$110.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$111$1$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$111$1$1.class index cb2da19..8c43673 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$111$1$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$111$1$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$111$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$111$1.class index cc025fa..f9e8c35 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$111$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$111$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$111.class b/_tests/out_app/com/lanremote/watch/MainActivity$111.class index 3254fb4..abeae8f 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$111.class and b/_tests/out_app/com/lanremote/watch/MainActivity$111.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class index 2397c63..686542c 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$112.class b/_tests/out_app/com/lanremote/watch/MainActivity$112.class index 4436b64..82ed9d5 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$112.class and b/_tests/out_app/com/lanremote/watch/MainActivity$112.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$113.class b/_tests/out_app/com/lanremote/watch/MainActivity$113.class index c60db35..fecafd2 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$113.class and b/_tests/out_app/com/lanremote/watch/MainActivity$113.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$114.class b/_tests/out_app/com/lanremote/watch/MainActivity$114.class index caa13f8..f5b8860 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$114.class and b/_tests/out_app/com/lanremote/watch/MainActivity$114.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$115$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$115$1.class index cc506d6..9309105 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$115$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$115$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$115.class b/_tests/out_app/com/lanremote/watch/MainActivity$115.class index 2c6fdc0..771f4cd 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$115.class and b/_tests/out_app/com/lanremote/watch/MainActivity$115.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$116$1$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$116$1$1.class index bed390c..d40fa9b 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$116$1$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$116$1$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$116$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$116$1.class index 098050b..3b894a9 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$116$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$116$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$116.class b/_tests/out_app/com/lanremote/watch/MainActivity$116.class index 63067a6..1fee8d5 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$116.class and b/_tests/out_app/com/lanremote/watch/MainActivity$116.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$117$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$117$1.class index efbbab8..6659c13 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$117$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$117$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$117.class b/_tests/out_app/com/lanremote/watch/MainActivity$117.class index bcd3105..6811eba 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$117.class and b/_tests/out_app/com/lanremote/watch/MainActivity$117.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$118.class b/_tests/out_app/com/lanremote/watch/MainActivity$118.class index 27950ce..bdd0212 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$118.class and b/_tests/out_app/com/lanremote/watch/MainActivity$118.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$119.class b/_tests/out_app/com/lanremote/watch/MainActivity$119.class index ccad8ac..efd80a1 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$119.class and b/_tests/out_app/com/lanremote/watch/MainActivity$119.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$120.class b/_tests/out_app/com/lanremote/watch/MainActivity$120.class index 1a8b970..f9d66e4 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$120.class and b/_tests/out_app/com/lanremote/watch/MainActivity$120.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$121.class b/_tests/out_app/com/lanremote/watch/MainActivity$121.class index 93d9f31..16b2d7d 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$121.class and b/_tests/out_app/com/lanremote/watch/MainActivity$121.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$122$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$122$1.class index a99efdf..edf1273 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$122$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$122$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$122.class b/_tests/out_app/com/lanremote/watch/MainActivity$122.class index 42353c1..a0eb3d8 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$122.class and b/_tests/out_app/com/lanremote/watch/MainActivity$122.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$123.class b/_tests/out_app/com/lanremote/watch/MainActivity$123.class index 55e9150..d334155 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$123.class and b/_tests/out_app/com/lanremote/watch/MainActivity$123.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$124.class b/_tests/out_app/com/lanremote/watch/MainActivity$124.class index 9973705..d751028 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$124.class and b/_tests/out_app/com/lanremote/watch/MainActivity$124.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$125.class b/_tests/out_app/com/lanremote/watch/MainActivity$125.class index 01e617c..5b574a9 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$125.class and b/_tests/out_app/com/lanremote/watch/MainActivity$125.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$126.class b/_tests/out_app/com/lanremote/watch/MainActivity$126.class index 12977d8..93c3139 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$126.class and b/_tests/out_app/com/lanremote/watch/MainActivity$126.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$127$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$127$1.class index 7f9a510..0487185 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$127$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$127$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$127$2.class b/_tests/out_app/com/lanremote/watch/MainActivity$127$2.class index d337685..5494560 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$127$2.class and b/_tests/out_app/com/lanremote/watch/MainActivity$127$2.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$127.class b/_tests/out_app/com/lanremote/watch/MainActivity$127.class index d724f4e..7b8d566 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$127.class and b/_tests/out_app/com/lanremote/watch/MainActivity$127.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$128.class b/_tests/out_app/com/lanremote/watch/MainActivity$128.class index 3beb3fb..5d854ea 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$128.class and b/_tests/out_app/com/lanremote/watch/MainActivity$128.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$129.class b/_tests/out_app/com/lanremote/watch/MainActivity$129.class index 42c4eb3..958533d 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$129.class and b/_tests/out_app/com/lanremote/watch/MainActivity$129.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$94$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$94$1.class index f27a6ad..14097f2 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$94$1.class and b/_tests/out_app/com/lanremote/watch/MainActivity$94$1.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$94.class b/_tests/out_app/com/lanremote/watch/MainActivity$94.class index e58f5e7..d010e17 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$94.class and b/_tests/out_app/com/lanremote/watch/MainActivity$94.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$95.class b/_tests/out_app/com/lanremote/watch/MainActivity$95.class index 0c0a578..8dc5a4c 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$95.class and b/_tests/out_app/com/lanremote/watch/MainActivity$95.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$96.class b/_tests/out_app/com/lanremote/watch/MainActivity$96.class index f687ad8..e4f4da1 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$96.class and b/_tests/out_app/com/lanremote/watch/MainActivity$96.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$97.class b/_tests/out_app/com/lanremote/watch/MainActivity$97.class index 9eec8db..9b0e28b 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$97.class and b/_tests/out_app/com/lanremote/watch/MainActivity$97.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$98.class b/_tests/out_app/com/lanremote/watch/MainActivity$98.class index 80bb030..335bd45 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$98.class and b/_tests/out_app/com/lanremote/watch/MainActivity$98.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$99.class b/_tests/out_app/com/lanremote/watch/MainActivity$99.class index ca6983b..ad946f6 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$99.class and b/_tests/out_app/com/lanremote/watch/MainActivity$99.class differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity.class b/_tests/out_app/com/lanremote/watch/MainActivity.class index f8a5990..7d26a69 100644 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity.class and b/_tests/out_app/com/lanremote/watch/MainActivity.class differ diff --git a/_tests/shots/1-屏幕中间.png b/_tests/shots/1-屏幕中间.png index 683dd74..6471835 100644 Binary files a/_tests/shots/1-屏幕中间.png and b/_tests/shots/1-屏幕中间.png differ diff --git a/_tests/shots/3-自定义640x400.png b/_tests/shots/3-自定义640x400.png index 4e38939..d0daadc 100644 Binary files a/_tests/shots/3-自定义640x400.png and b/_tests/shots/3-自定义640x400.png differ diff --git a/android/src/com/lanremote/watch/MainActivity.java b/android/src/com/lanremote/watch/MainActivity.java index 64e7d22..9c5faf1 100644 --- a/android/src/com/lanremote/watch/MainActivity.java +++ b/android/src/com/lanremote/watch/MainActivity.java @@ -2552,7 +2552,19 @@ public class MainActivity extends Activity implements RemoteClient.Listener { } } } else { - appendLog("电脑不在同一网络,继续用中转"); + // 不在同一网段 -> 继续走中转。 + // 光说"不在同一网络"用户没法自查,所以把 + // 双方地址都打出来:一眼就能看出是网段不同、 + // 还是手表连错了 WiFi。 + java.util.List theirs = client.relayLanIps(); + java.util.List mine = localIpv4List(); + appendLog("电脑在 " + (theirs == null || theirs.isEmpty() + ? "(没报地址)" : theirs.get(0)) + + ",手表在 " + (mine.isEmpty() + ? "(读不到网卡)" : mine.get(0)) + + " —— 不在同一网段,走中转"); + appendLog("想让手表直连:确认两边连的是同一个" + + " WiFi(注意别连成 5G 频段的另一个名字)"); } } } else { diff --git a/relay/relay.py b/relay/relay.py index 1fec15f..65a8d67 100644 --- a/relay/relay.py +++ b/relay/relay.py @@ -1511,7 +1511,7 @@ class Handler(BaseHTTPRequestHandler): line-height:1.5}} .meta b{{color:var(--dim);font-weight:500}} .meta a.logout{{color:var(--dim);text-decoration:none; - border-bottom:1px dashed #484f58;transition:.12s}} + border-bottom:1px dashed #d0d7de;transition:.12s}} .meta a.logout:hover{{color:var(--accent);border-bottom-color:var(--accent)}} /* ---------- 通用卡片 ---------- */ @@ -1524,7 +1524,7 @@ class Handler(BaseHTTPRequestHandler): .urlrow{{display:flex;align-items:center;gap:10px;flex-wrap:wrap}} .url{{flex:1;min-width:220px;font:600 15px/1.5 ui-monospace,Consolas, 'Courier New',monospace;color:#0969da;word-break:break-all; - background:#0b1220;border:1px solid #1f6feb44;border-radius:8px; + background:#f6f8fa;border:1px solid #d0d7de;border-radius:8px; padding:10px 12px}} .url.empty{{color:var(--muted);font-weight:400;font-style:normal}} @@ -1540,7 +1540,7 @@ class Handler(BaseHTTPRequestHandler): background:var(--accent);opacity:.65}} .stat.ok::after{{background:var(--ok)}} .stat.warn::after{{background:var(--warn)}} - .stat.mute::after{{background:#484f58}} + .stat.mute::after{{background:#d0d7de}} /* ---------- 工具条 ---------- */ .toolbar{{display:flex;align-items:center;gap:10px;flex-wrap:wrap; @@ -1552,14 +1552,14 @@ class Handler(BaseHTTPRequestHandler): button{{font-family:inherit;font-size:13px;cursor:pointer; border-radius:8px;padding:7px 14px;border:1px solid var(--border); background:var(--panel2);color:var(--fg);transition:.12s}} - button:hover{{background:#262c36;border-color:#484f58}} + button:hover{{background:#f3f4f6;border-color:#d0d7de}} button:active{{transform:translateY(1px)}} button:disabled{{opacity:.4;cursor:not-allowed;transform:none}} button.primary{{background:var(--accent);border-color:var(--accent); color:#fff;font-weight:600}} - button.primary:hover{{background:#4a95f8;border-color:#4a95f8}} - button.del{{background:#2d1214;border-color:#6e2b2b;color:#ff7b72}} - button.del:hover{{background:#4a1a1e;border-color:#a33;color:#ffa198}} + button.primary:hover{{background:#1d4ed8;border-color:#1d4ed8}} + button.del{{background:#fff5f5;border-color:#ffc9c9;color:#c92a2a}} + button.del:hover{{background:#ffe3e3;border-color:#a33;color:#a61b1b}} button.del:disabled{{opacity:.4}} button.small{{padding:5px 11px;font-size:12px}} @@ -1574,7 +1574,7 @@ class Handler(BaseHTTPRequestHandler): th{{background:var(--panel2);color:var(--dim);font-weight:500; font-size:12px;white-space:nowrap}} tbody tr{{transition:background .1s}} - tbody tr:hover td{{background:#1b222c}} + tbody tr:hover td{{background:#f6f8fa}} tbody tr:last-child td{{border-bottom:none}} td.name{{font-weight:500}} td.num{{font-family:ui-monospace,Consolas,monospace;color:var(--dim); @@ -1589,7 +1589,7 @@ class Handler(BaseHTTPRequestHandler): letter-spacing:1.5px;font-family:ui-monospace,Consolas,monospace; font-size:13px;color:#0969da}} .dot{{display:inline-block;width:7px;height:7px;border-radius:50%; - margin-right:7px;vertical-align:middle;background:#484f58}} + margin-right:7px;vertical-align:middle;background:#d0d7de}} .dot.on{{background:var(--ok);box-shadow:0 0 0 3px #1a7f3722}} .badge{{font-size:11px;padding:2px 8px;border-radius:20px; border:1px solid transparent;white-space:nowrap}} @@ -1598,13 +1598,13 @@ class Handler(BaseHTTPRequestHandler): .pickcell{{width:38px;text-align:center;padding-right:0}} input[type=checkbox]{{width:15px;height:15px;cursor:pointer; accent-color:var(--accent);vertical-align:middle}} - tr.picked td{{background:#1c2733}} - tr.picked:hover td{{background:#213042}} + tr.picked td{{background:#eff6ff}} + tr.picked:hover td{{background:#dbeafe}} /* ---------- 提示条 ---------- */ #toast{{position:fixed;left:50%;bottom:26px;transform:translate(-50%,20px); background:#1f6feb;color:#fff;padding:11px 20px;border-radius:10px; - font-size:13px;box-shadow:0 8px 24px #0008;opacity:0; + font-size:13px;box-shadow:0 8px 24px #0002;opacity:0; transition:.22s;pointer-events:none;max-width:88vw;text-align:center}} #toast.show{{opacity:1;transform:translate(-50%,0)}} #toast.err{{background:#b62324}}