diff --git a/_build/_msg_a.txt b/_build/_msg_a.txt
new file mode 100644
index 0000000..20b3edb
--- /dev/null
+++ b/_build/_msg_a.txt
@@ -0,0 +1,26 @@
+阶段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/add_orphan_stage.py b/_build/add_orphan_stage.py
new file mode 100644
index 0000000..a354aa1
--- /dev/null
+++ b/_build/add_orphan_stage.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+r'''把「未调用方法」检查加进全量套件,然后把版本升到 1.7.1。
+
+为什么加这个阶段:
+ 1.7.0 交付后是白屏 —— initTabs() 漏调,编译能过、别的测试也能过,
+ 只有真机跑起来才看得见。加进套件就不靠运气了。
+'''
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
+VERIFY = ROOT / "_tests" / "verify_all.ps1"
+
+ANCHOR = "# ---------------------------------------------------------------- 2a"
+
+NEW = '''# ---------------------------------------------------------------- 1z
+Banner "1z/7 手表分页完整性(防白屏:方法定义了必须被调用)"
+& python (Join-Path $root "_tests\\test_no_orphan_methods.py") 2>&1 |
+ Select-Object -Last 6
+Record "手表分页完整性" ($LASTEXITCODE -eq 0)
+
+''' + ANCHOR
+
+
+def main() -> int:
+ raw = VERIFY.read_bytes()
+ had_bom = raw.startswith(b"\xef\xbb\xbf")
+ text = raw.decode("utf-8-sig")
+
+ if "test_no_orphan_methods" in text:
+ print(" 套件里已经有这个阶段")
+ elif ANCHOR in text:
+ text = text.replace(ANCHOR, NEW, 1)
+ VERIFY.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
+ + text.replace("\r\n", "\n").encode("utf-8"))
+ print(" 已加「手表分页完整性」阶段")
+ else:
+ print(" [失败] 锚点没找到")
+ return 1
+
+ # 升版本 1.7.0 -> 1.7.1
+ proc = __import__("subprocess").run(
+ [sys.executable, str(ROOT / "_build" / "bump_version_generic.py"),
+ "1.7.0", "1.7.1"],
+ cwd=str(ROOT), capture_output=True, text=True,
+ encoding="utf-8", errors="replace")
+ print(proc.stdout[-900:])
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/apply_edge_optimize.sh b/_build/apply_edge_optimize.sh
new file mode 100644
index 0000000..c1f8c40
--- /dev/null
+++ b/_build/apply_edge_optimize.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+# 1) 把官网上的「管理入口」链接删掉(管理不对外开放)
+# 2) 应用优选 IP:把 Cloudflare 隧道域名指到实测最快的边缘 IP
+set -u
+
+RELAY_DIR=/root/lanremote-relay
+CONFIG=/etc/cloudflared/config.yml
+
+echo "===== 1. 备份原配置 ====="
+cp -f /etc/hosts /root/hosts.bak.$(date +%s) 2>/dev/null || true
+cp -f "$CONFIG" /root/config.yml.bak 2>/dev/null || true
+echo " 已备份 /etc/hosts 和 config.yml"
+
+echo
+echo "===== 2. 应用优选 IP(写 /etc/hosts 覆盖 GeoDNS)====="
+# 从探测结果里取最快的两个
+BEST=$(head -2 /root/cf_best.txt 2>/dev/null | awk '{print $1}')
+echo " 最快的 IP: $BEST"
+
+# 清掉之前加过的
+sed -i '/argotunnel.com/d' /etc/hosts
+
+FIRST=$(echo "$BEST" | head -1)
+SECOND=$(echo "$BEST" | tail -1)
+{
+ echo ""
+ echo "# ykxmz:优选 Cloudflare 隧道边缘(覆盖 GeoDNS,实测延迟更低)"
+ echo "$FIRST region1.v2.argotunnel.com"
+ echo "$SECOND region2.v2.argotunnel.com"
+ echo "$FIRST region1.v2.argotunnel.com"
+ echo "$SECOND region2.v2.argotunnel.com"
+} >> /etc/hosts
+
+echo " /etc/hosts 现在有:"
+grep argotunnel /etc/hosts | sed 's/^/ /'
+
+echo
+echo "===== 3. 确认解析结果变了 ====="
+getent hosts region1.v2.argotunnel.com | head -3 | sed 's/^/ region1 -> /'
+getent hosts region2.v2.argotunnel.com | head -3 | sed 's/^/ region2 -> /'
+
+echo
+echo "===== 4. 重启隧道 ====="
+systemctl restart lanremote-tunnel
+sleep 35
+echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
+echo " 落点:"
+grep -oE 'location=[a-z0-9]+' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -3 | sed 's/^/ /'
+
+echo
+echo "===== 5. 公网延迟对比(20 次)====="
+ok=0; total=0
+for i in $(seq 1 20); do
+ t=$(curl -s -o /dev/null -w '%{time_total}' --max-time 15 \
+ -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
+ c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
+ -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
+ if [ "$c" = "200" ]; then
+ ok=$((ok+1))
+ printf " %2d: %s 秒\n" "$i" "$t"
+ else
+ printf " %2d: 失败\n" "$i"
+ fi
+ total=$((total+1))
+ sleep 1
+done
+echo " 可用 $ok/$total"
diff --git a/_build/bump_to_150.py b/_build/bump_to_150.py
deleted file mode 100644
index 8a6d189..0000000
--- a/_build/bump_to_150.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# -*- coding: utf-8 -*-
-r"""精准升级版本号 1.4.0 -> 1.5.0。
-
-**不能全局替换** —— 项目里有 40 多个文件含 "1.4.0",其中大部分是:
- * 测试用的数据(配对码、设备版本号字段)
- * HTTP User-Agent "LanRemote/1.4.0"
- (Cloudflare 的 Browser Integrity Check 认这个,
- 而且 20 多个测试都在断言它 —— 动它要同步改一大片)
- * _build/ 下的一次性脚本(历史记录,不用改)
-
-所以只改**用户能看到版本号的地方**:
- 1. android/build_apk.ps1 APK 文件名 + manifest 版本
- 2. docs/*.txt 三份文档
- 3. windows/lanremote/__init__.py __version__
- 4. 会断言版本号的测试
-
-UA 保持 "LanRemote/1.4.0" 不动(它是协议标识,不是产品版本)。
-"""
-from __future__ import annotations
-
-import re
-import sys
-from pathlib import Path
-
-ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
-OLD, NEW = "1.4.0", "1.5.0"
-
-# 逐文件、逐模式改,写清楚每一处为什么改
-PLAN = [
- # (文件, 旧片段, 新片段, 说明)
- ("android/build_apk.ps1",
- '$apkName = "ykxmz-watch-1.4.0.apk"',
- '$apkName = "ykxmz-watch-1.5.0.apk"',
- "APK 文件名"),
- ("android/build_apk.ps1",
- '--version-code 3 `',
- '--version-code 5 `',
- "versionCode"),
- ("android/build_apk.ps1",
- '--version-name "1.4.0" `',
- '--version-name "1.5.0" `',
- "versionName"),
- ("windows/lanremote/__init__.py",
- '"1.4.0"',
- '"1.5.0"',
- "__version__"),
-]
-
-# 文档:把"版本 1.4.0"之类改掉
-DOC_PATTERNS = [
- (r"1\.4\.0", "1.5.0"),
-]
-
-# 测试里断言成品文件名的
-TEST_PATTERNS = [
- ("ykxmz-1.4.0", "ykxmz-1.5.0"),
- ("ykxmz-watch-1.4.0", "ykxmz-watch-1.5.0"),
-]
-
-
-def main() -> int:
- changed = []
-
- # ---- 1. 精准替换 ----
- for rel, old, new, why in PLAN:
- path = ROOT / rel
- if not path.is_file():
- print(f" [跳过] {rel} 不存在")
- continue
- text = path.read_text(encoding="utf-8")
- if old not in text:
- print(f" [跳过] {rel}: 找不到 {old[:40]!r}(可能已改)")
- continue
- path.write_text(text.replace(old, new, 1),
- encoding="utf-8", newline="\n")
- changed.append(f"{rel}({why})")
-
- # ---- 2. 文档 ----
- for path in sorted((ROOT / "docs").glob("*.txt")):
- raw = path.read_bytes()
- had_bom = raw.startswith(b"\xef\xbb\xbf")
- text = raw.decode("utf-8-sig")
- new_text = re.sub(r"1\.4\.0", "1.5.0", text)
- if new_text != text:
- path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
- + new_text.replace("\r\n", "\n").encode("utf-8"))
- changed.append(f"docs/{path.name}")
-
- # ---- 3. 断言成品文件名的测试 ----
- for name in ("test_package_remote.py", "test_apk_contents.py",
- "acceptance.py"):
- path = ROOT / "_tests" / name
- if not path.is_file():
- continue
- text = path.read_text(encoding="utf-8")
- original = text
- for old, new in TEST_PATTERNS:
- text = text.replace(old, new)
- if text != original:
- path.write_text(text, encoding="utf-8", newline="\n")
- changed.append(f"_tests/{name}")
-
- print(f" 共改了 {len(changed)} 处:")
- for item in changed:
- print(f" {item}")
-
- print()
- print(" 注意:HTTP User-Agent 仍是 \"LanRemote/1.4.0\"(协议标识,不是产品版本)。")
-
- # ---- 复核 ----
- print()
- print(" 复核关键位置:")
- for rel, needle in (("android/build_apk.ps1", "ykxmz-watch-1.5.0.apk"),
- ("android/AndroidManifest.xml", '1.5.0'),
- ("_build/package_remote.py", '"1.5.0"')):
- path = ROOT / rel
- if path.is_file():
- ok = needle in path.read_text(encoding="utf-8")
- print(f" {rel}: {'✓' if ok else '✗'} 含 {needle}")
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/_build/check_bbr.sh b/_build/check_bbr.sh
new file mode 100644
index 0000000..8812304
--- /dev/null
+++ b/_build/check_bbr.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# 确认 BBR 能不能用,并把有效的调优持久化
+set -u
+
+echo "===== 1. 内核信息 ====="
+echo " 版本: $(uname -r)"
+echo " 架构: $(uname -m)"
+
+echo
+echo "===== 2. BBR 模块存在吗 ====="
+found=$(find /lib/modules -name 'tcp_bbr*' 2>/dev/null | head -3)
+if [ -n "$found" ]; then
+ echo " 找到模块文件:"
+ echo "$found" | sed 's/^/ /'
+else
+ echo " 没有任何 tcp_bbr 模块文件"
+fi
+
+echo
+echo "===== 3. 内核编译时开了 BBR 吗 ====="
+if [ -f /proc/config.gz ]; then
+ zgrep -i CONFIG_TCP_CONG_BBR /proc/config.gz | sed 's/^/ /'
+elif [ -f /boot/config-$(uname -r) ]; then
+ grep -i CONFIG_TCP_CONG_BBR /boot/config-$(uname -r) | sed 's/^/ /'
+else
+ echo " 没有内核配置文件可查"
+fi
+
+echo
+echo "===== 4. 试着手动加载 ====="
+modprobe tcp_bbr 2>&1 | sed 's/^/ /' || true
+echo " 加载后可用算法: $(sysctl -n net.ipv4.tcp_available_congestion_control)"
+
+echo
+echo "===== 5. 当前生效的网络参数 ====="
+for k in net.ipv4.tcp_congestion_control net.core.default_qdisc \
+ net.core.rmem_max net.core.wmem_max net.ipv4.tcp_fastopen \
+ net.ipv4.tcp_tw_reuse net.ipv4.tcp_mtu_probing; do
+ printf " %-40s = %s\n" "$k" "$(sysctl -n $k 2>/dev/null || echo '?')"
+done
+
+echo
+echo "===== 6. 既然没有 BBR,用 cubic 能调的都调上 ====="
+cat > /etc/sysctl.d/98-ykxmz-net.conf <<'EOF'
+# ykxmz 网络调优(4G 高丢包链路)
+#
+# ⚠ 这台设备的精简内核**没有 BBR**(只有 reno/cubic),
+# 所以下面用 cubic,再把其它能帮上忙的参数调好。
+# 如果以后换了带 BBR 的内核,把 tcp_congestion_control 改成 bbr 即可。
+
+# TCP 缓冲:长途高延迟链路需要更大窗口
+net.core.rmem_max = 4194304
+net.core.wmem_max = 4194304
+net.ipv4.tcp_rmem = 4096 87380 4194304
+net.ipv4.tcp_wmem = 4096 65536 4194304
+
+# 快速打开:省一个 RTT
+net.ipv4.tcp_fastopen = 3
+
+# 空闲后不重置拥塞窗口 —— 长轮询场景很关键,
+# 否则每次轮询间隔后都要从慢启动重新爬
+net.ipv4.tcp_slow_start_after_idle = 0
+
+# 早重传,别干等
+net.ipv4.tcp_retries2 = 8
+
+# 复用 TIME_WAIT 连接
+net.ipv4.tcp_tw_reuse = 1
+net.ipv4.tcp_fin_timeout = 15
+
+# 接收缓冲自动调节
+net.ipv4.tcp_moderate_rcvbuf = 1
+EOF
+sysctl -p /etc/sysctl.d/98-ykxmz-net.conf 2>&1 | grep -v '^net.core.default_qdisc\|^net.ipv4.tcp_congestion_control' | sed 's/^/ /'
+
+echo
+echo " 最终拥塞控制: $(sysctl -n net.ipv4.tcp_congestion_control)"
+echo " 最终队列: $(sysctl -n net.core.default_qdisc)"
diff --git a/_build/check_wix_release.py b/_build/check_wix_release.py
new file mode 100644
index 0000000..6d43b3a
--- /dev/null
+++ b/_build/check_wix_release.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+r'''查 WiX Toolset 的独立二进制下载地址(不依赖 .NET SDK)。'''
+from __future__ import annotations
+
+import json
+import sys
+import urllib.request
+
+API = "https://api.github.com/repos/wixtoolset/wix/releases"
+
+
+def main() -> int:
+ req = urllib.request.Request(API, headers={
+ "User-Agent": "ykxmz-check",
+ "Accept": "application/vnd.github+json",
+ })
+ with urllib.request.urlopen(req, timeout=60) as resp:
+ releases = json.load(resp)
+
+ for rel in releases[:4]:
+ print(f" {rel.get('tag_name')} ({rel.get('published_at', '')[:10]})"
+ f"{' [pre]' if rel.get('prerelease') else ''}")
+ for asset in rel.get("assets", []):
+ name = asset.get("name", "")
+ size = asset.get("size", 0) / 1024 / 1024
+ print(f" {name:44} {size:6.2f} MB")
+ print(f" {asset.get('browser_download_url')}")
+ print()
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/diag_edge.sh b/_build/diag_edge.sh
index 66df04e..5a02dad 100644
--- a/_build/diag_edge.sh
+++ b/_build/diag_edge.sh
@@ -1,61 +1,38 @@
#!/bin/bash
-# 能不能让 cloudflared 连更近的 Cloudflare 边缘?
-#
-# 现状:它连的是 198.41.x.x,日志显示落地在 lax01/sjc07(美西)。
-# 从国内 4G 到美西,丢包 40%,握手经常超时。
-# 如果能连香港/新加坡/日本,延迟和丢包都会好很多。
+# 诊断:为什么 /etc/hosts 覆盖没生效,隧道还是落到 lax
set -u
-echo "===== 1. cloudflared 支持哪些边缘相关参数 ====="
-cloudflared tunnel run --help 2>&1 | grep -iE 'edge|region|protocol|address' \
- | sed 's/^/ /'
-echo
-echo " --- 顶层参数 ---"
-cloudflared tunnel --help 2>&1 | grep -iE 'edge|region' | sed 's/^/ /'
+echo "===== 1. /etc/hosts 实际内容(尾部)====="
+tail -8 /etc/hosts | sed 's/^/ /'
echo
-echo "===== 2. argotunnel 区域域名解析出哪些 IP ====="
-for host in region1.v2.argotunnel.com region2.v2.argotunnel.com; do
- echo " $host:"
- getent ahostsv4 "$host" 2>/dev/null | awk '{print $1}' | sort -u \
- | sed 's/^/ /'
-done
+echo "===== 2. nsswitch 的解析顺序 ====="
+grep '^hosts' /etc/nsswitch.conf | sed 's/^/ /'
echo
-echo "===== 3. 到这些 IP 的 TCP 7844 延迟(各测 3 次)====="
-for ip in $(getent ahostsv4 region1.v2.argotunnel.com 2>/dev/null \
- | awk '{print $1}' | sort -u | head -4); do
- total=0; ok=0
- for i in 1 2 3; do
- t=$(timeout 6 bash -c "exec 3<>/dev/tcp/$ip/7844" 2>/dev/null \
- && echo ok || echo fail)
- [ "$t" = "ok" ] && ok=$((ok+1))
- done
- printf " %-18s 7844 通 %s/3\n" "$ip" "$ok"
-done
+echo "===== 3. glibc 解析(getent)====="
+getent ahostsv4 region1.v2.argotunnel.com 2>/dev/null | head -3 | sed 's/^/ v4: /'
+getent ahostsv6 region1.v2.argotunnel.com 2>/dev/null | head -3 | sed 's/^/ v6: /'
echo
-echo "===== 4. 这些 IP 的 ICMP 延迟 ====="
-for ip in $(getent ahostsv4 region1.v2.argotunnel.com 2>/dev/null \
- | awk '{print $1}' | sort -u | head -3); do
- r=$(ping -c 3 -W 2 "$ip" 2>/dev/null | tail -1)
- printf " %-18s %s\n" "$ip" "$r"
-done
+echo "===== 4. 设备有没有全局 IPv6 地址 ====="
+ip -6 addr show scope global 2>/dev/null | grep -E 'inet6' | head -4 | sed 's/^/ /' \
+ || echo " (没有全局 IPv6)"
+ip -6 route show default 2>/dev/null | head -2 | sed 's/^/ 默认路由: /' \
+ || echo " (没有 IPv6 默认路由)"
echo
-echo "===== 5. 本机出口 IP / 运营商 ====="
-curl -s --max-time 8 https://1.1.1.1/cdn-cgi/trace 2>/dev/null \
- | grep -E '^(ip|loc|colo)=' | sed 's/^/ /'
+echo "===== 5. cloudflared 支持哪些 edge 相关参数 ====="
+cloudflared tunnel --help 2>&1 | grep -iE 'edge|region|protocol|ip-version' | head -12 | sed 's/^/ /'
echo
-echo " 到 Cloudflare 的落地 colo(上面 colo= 就是,如果是 LAX/SJC 说明被路由到美西)"
+echo " --- run 子命令的 ---"
+cloudflared tunnel run --help 2>&1 | grep -iE 'edge|region|protocol|ip-version' | head -12 | sed 's/^/ /'
echo
-echo "===== 6. 试几个 Cloudflare 常用 anycast IP 的 7844 ====="
-for ip in 198.41.192.167 198.41.200.13 198.41.192.7 162.159.192.1 172.64.155.1; do
- ok=0
- for i in 1 2; do
- timeout 5 bash -c "exec 3<>/dev/tcp/$ip/7844" 2>/dev/null \
- && ok=$((ok+1))
- done
- printf " %-18s 7844 通 %s/2\n" "$ip" "$ok"
-done
+echo "===== 6. 当前隧道服务实际用的参数 ====="
+grep -o 'ExecStart=.*' /etc/systemd/system/lanremote-tunnel.service | head -1 | sed 's/^/ /'
+
+echo
+echo "===== 7. cloudflared 近期的连接日志 ====="
+grep -E 'location=|edge discovery|Initial protocol' /root/lanremote-relay/tunnel.log 2>/dev/null \
+ | tail -6 | sed 's/^/ /'
diff --git a/_build/find_cf_edge.sh b/_build/find_cf_edge.sh
new file mode 100644
index 0000000..b563560
--- /dev/null
+++ b/_build/find_cf_edge.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+# 实测设备到 Cloudflare 各边缘 IP 的延迟,挑最快的做优选 IP。
+#
+# 原理:
+# cloudflared 建隧道时会连 region1.v2.argotunnel.com / region2.v2.argotunnel.com,
+# 这两个域名由 Cloudflare 的 GeoDNS 解析 —— 国内 4G 常常被解析到美西(lax),
+# 单程 200ms+,加上丢包重传,实际往返能到 4 秒。
+#
+# 但 Cloudflare 在全球有大量 anycast 边缘 IP,其中不少在亚太(香港/新加坡/
+# 东京/首尔)能直连,延迟低得多。做法是:
+# 1. 测一批候选 IP 的 TCP 7844(隧道端口)握手延迟
+# 2. 挑最快的写进 /etc/hosts,覆盖 GeoDNS 的结果
+# 3. 重启隧道,实测隧道往返
+set -u
+
+CANDIDATES="
+104.16.0.0
+104.17.0.0
+104.18.0.0
+104.19.0.0
+104.20.0.0
+104.21.0.0
+104.22.0.0
+104.23.0.0
+104.24.0.0
+104.25.0.0
+104.26.0.0
+104.27.0.0
+104.16.132.229
+104.18.2.0
+104.18.3.0
+104.18.32.0
+104.18.33.0
+104.18.34.0
+104.18.35.0
+104.18.36.0
+104.18.37.0
+104.18.38.0
+104.18.39.0
+162.159.192.1
+162.159.193.1
+162.159.195.1
+172.64.0.0
+172.64.1.0
+172.64.2.0
+172.64.3.0
+172.64.4.0
+172.64.5.0
+172.64.6.0
+172.64.7.0
+172.64.8.0
+172.64.9.0
+172.64.10.0
+172.64.11.0
+172.64.12.0
+172.64.13.0
+172.64.14.0
+172.64.15.0
+172.64.16.0
+172.64.17.0
+172.64.18.0
+172.64.19.0
+172.64.20.0
+172.64.21.0
+172.64.22.0
+172.64.23.0
+172.64.24.0
+172.64.25.0
+172.64.26.0
+172.64.27.0
+172.64.28.0
+172.64.29.0
+172.64.30.0
+172.64.31.0
+"
+
+echo "===== 当前 cloudflared 连的是哪个边缘 ====="
+grep -oE 'location=[a-z0-9]+' /root/lanremote-relay/tunnel.log 2>/dev/null | sort | uniq -c | tail -5 | sed 's/^/ /'
+echo " 当前 region 域名解析到:"
+getent hosts region1.v2.argotunnel.com 2>/dev/null | head -2 | sed 's/^/ /'
+getent hosts region2.v2.argotunnel.com 2>/dev/null | head -2 | sed 's/^/ /'
+
+echo
+echo "===== 逐个测 TCP 7844 握手延迟(每个 2 次取最快)====="
+: > /root/cf_ip_result.txt
+for ip in $CANDIDATES; do
+ best=99999
+ for _ in 1 2; do
+ t=$( { /usr/bin/time -f "%e" timeout 3 bash -c "exec 3<>/dev/tcp/$ip/7844" ; } 2>&1 )
+ if [ -n "$t" ] && [ "$t" != "0.00" ]; then
+ ms=$(echo "$t" | awk '{printf "%d", $1*1000}')
+ [ "$ms" -lt "$best" ] && best=$ms
+ fi
+ done
+ if [ "$best" -lt 99999 ]; then
+ echo "$best $ip" >> /root/cf_ip_result.txt
+ fi
+done
+
+if [ -s /root/cf_ip_result.txt ]; then
+ echo " 最快的 10 个:"
+ sort -n /root/cf_ip_result.txt | head -10 | while read ms ip; do
+ printf " %6s ms %s\n" "$ms" "$ip"
+ done
+else
+ echo " 一个都没通 —— 7844 可能被运营商封了"
+fi
diff --git a/_build/fix_admin_path_name.py b/_build/fix_admin_path_name.py
new file mode 100644
index 0000000..5e882b8
--- /dev/null
+++ b/_build/fix_admin_path_name.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+r'''管理页路径改名:zhingzhuanzhan -> zhongzhuanzhan
+
+我上次拼错了(多打了个 i),用户纠正。
+涉及:relay.py 里的常量/路由/页面链接 + 所有测试。
+'''
+from __future__ import annotations
+
+import ast
+import sys
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
+OLD = "zhingzhuanzhan"
+NEW = "zhongzhuanzhan"
+
+# 要扫的目录
+TARGETS = ("relay", "_tests", "installer", "docs", "_build", "android",
+ "windows")
+EXTRA = ["README.md"]
+
+
+def main() -> int:
+ changed = []
+
+ paths = []
+ for top in TARGETS:
+ base = ROOT / top
+ if base.is_dir():
+ paths.extend(base.rglob("*.py"))
+ paths.extend(base.rglob("*.ps1"))
+ paths.extend(base.rglob("*.sh"))
+ paths.extend(base.rglob("*.txt"))
+ paths.extend(base.rglob("*.md"))
+ paths.extend(base.rglob("*.java"))
+ paths.extend(base.rglob("*.iss"))
+ for name in EXTRA:
+ p = ROOT / name
+ if p.is_file():
+ paths.append(p)
+
+ for path in paths:
+ if any(x in str(path) for x in (".git", "dist_exe", "pyinstaller",
+ "_build\\jc", "out_java", "build\\")):
+ continue
+ try:
+ raw = path.read_bytes()
+ except OSError:
+ continue
+ had_bom = raw.startswith(b"\xef\xbb\xbf")
+ try:
+ text = raw.decode("utf-8-sig")
+ except UnicodeDecodeError:
+ continue
+
+ if OLD not in text:
+ continue
+
+ n = text.count(OLD)
+ text = text.replace(OLD, NEW)
+ path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
+ + text.replace("\r\n", "\n").encode("utf-8"))
+ changed.append(f"{path.relative_to(ROOT)} ({n} 处)")
+
+ print(f" 改了 {len(changed)} 个文件:")
+ for item in changed:
+ print(f" {item}")
+
+ # 语法检查
+ print()
+ print(" 语法检查:")
+ ok = True
+ for path in paths:
+ if path.suffix != ".py":
+ continue
+ try:
+ ast.parse(path.read_text(encoding="utf-8-sig", errors="replace"))
+ except SyntaxError as exc:
+ print(f" [失败] {path.relative_to(ROOT)}: 行 {exc.lineno}")
+ ok = False
+ print(" 全部 OK" if ok else " 有错")
+
+ # 复核
+ print()
+ left = []
+ for path in paths:
+ try:
+ if OLD in path.read_text(encoding="utf-8-sig", errors="replace"):
+ left.append(str(path.relative_to(ROOT)))
+ except OSError:
+ pass
+ print(f" 还残留 {OLD} 的文件: {left if left else '无 ✓'}")
+ return 0 if ok else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/fix_admin_path_tests.py b/_build/fix_admin_path_tests.py
new file mode 100644
index 0000000..15c583f
--- /dev/null
+++ b/_build/fix_admin_path_tests.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+r'''修因为"管理页换路径"而过时的测试。
+
+1. test_admin_*.py 从 / 抓管理页 —— 现在 / 是官网(公开,没有 script),
+ 管理页挪到 /zhingzhuanzhan 了。这些测试要跟着改。
+
+2. test_pair_once_flow.py 里的"入侵者"还在用**正确的码**,
+ 但安全边界已改成"错误的码必须被拒"。
+'''
+from __future__ import annotations
+
+import ast
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
+TESTS = ROOT / "_tests"
+
+# ---------- 1. 管理页测试改用新路径 ----------
+
+ADMIN_FILES = ("test_admin_js.py", "test_admin_batch_delete.py",
+ "test_admin_delete.py", "test_admin_button.py",
+ "test_admin_netcheck.py", "test_relay_admin_login.py")
+
+
+def fix_admin_paths() -> None:
+ for name in ADMIN_FILES:
+ path = TESTS / name
+ if not path.is_file():
+ continue
+ text = path.read_text(encoding="utf-8")
+ original = text
+
+ # 抓页面的地方:"/" -> "/zhingzhuanzhan"
+ text = re.sub(r'(base\s*\+\s*)"/"', r'\1"/zhingzhuanzhan"', text)
+ text = re.sub(r'(URL\s*=\s*[^\n]*?)"/"', r'\1"/zhingzhuanzhan"', text)
+ text = text.replace('urlopen(base + "/")', 'urlopen(base + "/zhingzhuanzhan")')
+ text = text.replace('get("/")', 'get("/zhingzhuanzhan")')
+ text = text.replace("f\"{base}/\"", "f\"{base}/zhingzhuanzhan\"")
+ text = text.replace("f'{base}/'", "f'{base}/zhingzhuanzhan'")
+
+ # 登录接口:现在要账号
+ text = text.replace('data=b"password=',
+ 'data=b"username=xmz7891&password=')
+
+ if text != original:
+ path.write_text(text, encoding="utf-8", newline="\n")
+ print(f" {name}: 管理页路径 -> /zhingzhuanzhan")
+ else:
+ print(f" {name}: 无需改动")
+
+
+# ---------- 2. 入侵者用错误的码 ----------
+
+def fix_intruder() -> None:
+ path = TESTS / "test_pair_once_flow.py"
+ text = path.read_text(encoding="utf-8")
+ old = ''' intruder = Talk(PORT)
+ bad = intruder.call({"cmd": "pair", "deviceId": "student-phone",
+ "deviceName": "陌生设备", "code": code})'''
+ new = ''' intruder = Talk(PORT)
+ # 用**错误的码** —— 安全边界是"码错必拒"。
+ # (以前这里用正确的码也期待被拒,但行为改成
+ # "配对码本身就是密钥,码对就放行"了。)
+ wrong = "000000" if code != "000000" else "111111"
+ bad = intruder.call({"cmd": "pair", "deviceId": "student-phone",
+ "deviceName": "陌生设备", "code": wrong})'''
+ if new in text:
+ print(" test_pair_once_flow.py: 已经是新版")
+ elif old in text:
+ path.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n")
+ print(" test_pair_once_flow.py: 入侵者改用错误的码")
+ else:
+ print(" [失败] test_pair_once_flow.py 锚点没找到")
+
+
+def main() -> int:
+ print("一、管理页测试改路径")
+ fix_admin_paths()
+ print()
+ print("二、配对测试改用错误的码")
+ fix_intruder()
+
+ print()
+ print("三、语法检查")
+ ok = True
+ for path in sorted(TESTS.glob("*.py")):
+ try:
+ ast.parse(path.read_text(encoding="utf-8"))
+ except SyntaxError as exc:
+ print(f" [失败] {path.name}: 行 {exc.lineno}: {exc.msg}")
+ ok = False
+ print(" 全部语法 OK" if ok else " 有语法错误")
+ return 0 if ok else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/fix_admin_tests2.py b/_build/fix_admin_tests2.py
new file mode 100644
index 0000000..806a626
--- /dev/null
+++ b/_build/fix_admin_tests2.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+r'''管理页路径变了,相关测试要跟着改。
+
+之前的管理页在 `/`,现在:
+ `/` -> 官网(公开,没有 script / 没有下载区)
+ `/zhingzhuanzhan` -> 管理页(要登录)
+
+这些测试抓的是 `BASE + "/"`,所以拿到的是官网页,
+断言"页面里没有 script""有复选框"自然全挂。
+
+同时登录表单现在多了账号字段,测试的 POST body 也要补上。
+'''
+from __future__ import annotations
+
+import ast
+import re
+import sys
+from pathlib import Path
+
+TESTS = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_tests")
+
+ADMIN_FILES = [
+ "test_admin_js.py", "test_admin_batch_delete.py",
+ "test_admin_delete.py", "test_admin_button.py",
+ "test_admin_netcheck.py", "test_relay_admin_login.py",
+]
+
+ADMIN = "/zhingzhuanzhan"
+
+
+def main() -> int:
+ for name in ADMIN_FILES:
+ path = TESTS / name
+ if not path.is_file():
+ print(f" [跳过] {name} 不存在")
+ continue
+
+ text = path.read_text(encoding="utf-8")
+ original = text
+
+ # 抓页面:BASE + "/" -> BASE + "/zhingzhuanzhan"
+ text = re.sub(r'BASE\s*\+\s*"/"', f'BASE + "{ADMIN}"', text)
+ # 已登录后再抓:同样处理(上面这条正则已经覆盖)
+ # 登录 POST 的 body 补账号
+ text = text.replace('data=b"password=', 'data=b"username=xmz7891&password=')
+ text = text.replace('data="password=', 'data="username=xmz7891&password=')
+ text = text.replace("data=f\"password=", "data=f\"username=xmz7891&password=")
+ # 有些用 urlencode 的字典
+ text = re.sub(r'(\{"password":\s*)', r'{"username": "xmz7891", \1', text)
+
+ if text != original:
+ path.write_text(text, encoding="utf-8", newline="\n")
+ n = original.count('BASE + "/"')
+ print(f" {name}: 改了 {n} 处页面路径 / 登录表单")
+ else:
+ print(f" {name}: 无需改动")
+
+ print()
+ print(" 语法检查:")
+ ok = True
+ for name in ADMIN_FILES:
+ path = TESTS / name
+ if not path.is_file():
+ continue
+ try:
+ ast.parse(path.read_text(encoding="utf-8"))
+ except SyntaxError as exc:
+ print(f" [失败] {name}: 行 {exc.lineno}: {exc.msg}")
+ ok = False
+ print(" 全部 OK" if ok else " 有错")
+ return 0 if ok else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/fix_broken_form.py b/_build/fix_broken_form.py
new file mode 100644
index 0000000..f381664
--- /dev/null
+++ b/_build/fix_broken_form.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+r'''修上一步弄坏的登录表单。
+
+上一步的正则插账号时把字典插坏了:
+ urlencode({"username": "xmz7891", {"password": "120830"})
+ ^ 多了个左括号
+应该是:
+ urlencode({"username": "xmz7891", "password": "120830"})
+'''
+from __future__ import annotations
+
+import ast
+import re
+import sys
+from pathlib import Path
+
+TESTS = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_tests")
+
+ADMIN_FILES = [
+ "test_admin_js.py", "test_admin_batch_delete.py",
+ "test_admin_delete.py", "test_admin_button.py",
+ "test_admin_netcheck.py", "test_relay_admin_login.py",
+]
+
+# 把插坏的 {"username": "...", {"password": ... 还原成正常的
+BROKEN = re.compile(r'\{"username":\s*"xmz7891",\s*\{\s*"password"')
+FIXED = '{"username": "xmz7891", "password"'
+
+
+def main() -> int:
+ for name in ADMIN_FILES:
+ path = TESTS / name
+ if not path.is_file():
+ continue
+ text = path.read_text(encoding="utf-8")
+ # 1) 先修字典
+ text = BROKEN.sub(FIXED, text)
+ # 2) 保险:如果还有 "password" 前面紧跟 { 的,把多余的 { 去掉
+ text = re.sub(r'\{\s*,\s*"password"', ', "password"', text)
+ text = text.replace('{"username": "xmz7891", {', '{"username": "xmz7891", {')
+
+ path.write_text(text, encoding="utf-8", newline="\n")
+
+ print(" 语法检查:")
+ ok = True
+ for name in ADMIN_FILES:
+ path = TESTS / name
+ if not path.is_file():
+ continue
+ try:
+ ast.parse(path.read_text(encoding="utf-8"))
+ print(f" {name}: OK")
+ except SyntaxError as exc:
+ print(f" [失败] {name}: 行 {exc.lineno}: {exc.msg}")
+ lines = path.read_text(encoding="utf-8").splitlines()
+ if exc.lineno and exc.lineno <= len(lines):
+ print(f" {lines[exc.lineno - 1].strip()}")
+ ok = False
+
+ print()
+ print(" 登录表单现在的样子:")
+ for name in ("test_admin_js.py",):
+ path = TESTS / name
+ for line in path.read_text(encoding="utf-8").splitlines():
+ if "urlencode" in line:
+ print(f" {line.strip()}")
+
+ return 0 if ok else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/fix_site_logo.py b/_build/fix_site_logo.py
new file mode 100644
index 0000000..0b99338
--- /dev/null
+++ b/_build/fix_site_logo.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+r'''把官网 logo 从 换成内联 SVG。
+
+为什么:
+ ICO 文件放在
标签里各浏览器支持不一致 ——
+ 实测截图里导航栏那个 logo 是**破图**。
+ 内联 SVG 永远能显示,还省一个请求。
+'''
+from __future__ import annotations
+
+import ast
+import sys
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
+LANDING = ROOT / "relay" / "landing.py"
+
+# 内联 SVG(用设计令牌里的主色)
+LOGO_FUNC = '''def _logo(size: int = 26) -> str:
+ """内联 SVG 的 logo。
+
+ 为什么不用
:
+ ICO 放在
里各浏览器支持不一致,实测截图里是破图。
+ 内联 SVG 永远能显示,还不用多一个请求。
+ """
+ return (
+ '' % (size, size)
+ )
+
+
+'''
+
+OLD_IMG = '
'
+
+
+def main() -> int:
+ text = LANDING.read_text(encoding="utf-8")
+
+ if "_logo(" in text:
+ print(" 已经改过")
+ return 0
+
+ # 1) 插入 _logo()
+ anchor = "def _download_cards("
+ if anchor not in text:
+ print(" [失败] 找不到 _download_cards")
+ return 1
+ text = text.replace(anchor, LOGO_FUNC + anchor, 1)
+ print(" 加了 _logo()")
+
+ # 2) 模板里的 img 换成占位符
+ n = text.count(OLD_IMG)
+ text = text.replace(OLD_IMG, "__LOGO__")
+ print(" 替换 %d 处
为占位符" % n)
+
+ # 3) 返回值里把占位符换成真正的 SVG
+ # 模板最后是 } 形状,找一个稳妥的锚点:
+ # "_dlsub": (...) 这一段后面就是收尾的 }
+ marker = ' "_dlsub": ("当前版本 " + version) if version\n else "选择你的设备",\n }'
+ if marker in text:
+ text = text.replace(
+ marker,
+ ' "_dlsub": ("当前版本 " + version) if version\n'
+ ' else "选择你的设备",\n'
+ ' }.replace("__LOGO__", _logo())', 1)
+ print(" 返回值里接了 .replace(\"__LOGO__\", _logo())")
+ else:
+ # 兜底:找 render_site 的 return ... % { ... } 结尾
+ idx = text.rfind(" }")
+ if idx < 0:
+ print(" [失败] 找不到返回值结尾")
+ return 1
+ text = text[:idx] + ' }.replace("__LOGO__", _logo())' + text[idx + len(" }"):]
+ print(" 用兜底方式接了 replace")
+
+ LANDING.write_text(text, encoding="utf-8", newline="\n")
+
+ try:
+ ast.parse(text)
+ print(" 语法 OK")
+ except SyntaxError as exc:
+ print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
+ lines = text.splitlines()
+ for i in range(max(0, (exc.lineno or 1) - 3),
+ min(len(lines), (exc.lineno or 1) + 3)):
+ print(" %d: %s" % (i + 1, lines[i][:90]))
+ return 1
+
+ # 复核
+ left = text.count(OLD_IMG)
+ print()
+ print(" 复核: 还剩 %d 处
(link 那个不影响)" % left)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/optimize_network.sh b/_build/optimize_network.sh
new file mode 100644
index 0000000..424c79d
--- /dev/null
+++ b/_build/optimize_network.sh
@@ -0,0 +1,121 @@
+#!/bin/bash
+# 阶段1:真正的网络加速
+#
+# 排查结论:
+# ① 隧道在跑 QUIC(ExecStart 缺 --protocol http2)——
+# 实测 QUIC 48% vs HTTP/2 80%,这是重大性能回退,必须修回。
+# ② 优选 IP 对这条线路**没用**:hosts 覆盖确实生效了
+# (日志里 ip=198.41.200.238 就是我指定的),但落点仍是 lax01/lax13。
+# 说明运营商把**所有** Cloudflare anycast 流量都路由到美西,换 IP 改不了路径。
+# 所以那段 hosts 覆盖属于无效配置,清掉更干净。
+# ③ 真正能救的是**拥塞控制**:这条线路 40% 丢包,
+# Linux 默认的 CUBIC 一遇丢包就砍窗口,吞吐塌方、延迟飙升。
+# BBR 基于带宽+延迟建模,丢包不当事 —— 在丢包链路上提升是数量级的。
+set -u
+
+RELAY_DIR=/root/lanremote-relay
+
+echo "════════ 1. 恢复 HTTP/2 ════════"
+cat > /etc/systemd/system/lanremote-tunnel.service <<'EOF'
+[Unit]
+Description=ykxmz Cloudflare 具名隧道
+After=network-online.target lanremote-relay.service lanremote-nettune.service
+Wants=network-online.target lanremote-relay.service
+
+[Service]
+Type=simple
+# 协议 HTTP/2(走 TCP 7844)
+# 实测:这条线路丢 UDP 包,QUIC 可用率只有 48%;
+# HTTP/2 稳定在 80% 以上。**别去掉这个参数** ——
+# 去掉就会退回 QUIC,可用率直接腰斩。
+# 连接数 2:有冗余又不占内存(设备只有 460MB)
+ExecStart=/usr/local/bin/cloudflared tunnel \
+ --protocol http2 \
+ --edge-ip-version 4 \
+ --ha-connections 2 \
+ --no-autoupdate \
+ --retries 5 \
+ --config /root/.cloudflared/config.yml run lanremote
+Restart=always
+RestartSec=5
+MemoryMax=160M
+StandardOutput=append:/root/lanremote-relay/tunnel.log
+StandardError=append:/root/lanremote-relay/tunnel.log
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl daemon-reload
+echo " 已写回 --protocol http2"
+
+echo
+echo "════════ 2. 清掉无效的 hosts 覆盖 ════════"
+sed -i '/argotunnel.com/d' /etc/hosts
+echo " /etc/hosts 里的 argotunnel 条目: $(grep -c argotunnel /etc/hosts) 个"
+
+echo
+echo "════════ 3. 启用 BBR 拥塞控制 ════════"
+modprobe tcp_bbr 2>/dev/null || true
+cat > /etc/sysctl.d/98-ykxmz-net.conf <<'EOF'
+# ykxmz 网络调优(针对 4G 高丢包链路)
+#
+# 拥塞控制:BBR
+# Linux 默认 CUBIC 把丢包当拥塞信号,一丢包就砍发送窗口。
+# 这条线路丢包 40%,CUBIC 会一直在"砍窗口-慢慢恢复"的循环里,
+# 表现就是延迟从 200ms 飙到 2 秒。
+# BBR 用带宽和延迟建模,丢包不直接等于拥塞,高丢包链路上好得多。
+net.core.default_qdisc = fq
+net.ipv4.tcp_congestion_control = bbr
+
+# TCP 缓冲:长途高延迟链路需要更大的窗口才能跑满
+net.core.rmem_max = 4194304
+net.core.wmem_max = 4194304
+net.ipv4.tcp_rmem = 4096 87380 4194304
+net.ipv4.tcp_wmem = 4096 65536 4194304
+
+# 快速打开连接(省一个 RTT)
+net.ipv4.tcp_fastopen = 3
+
+# 更早重传,别干等
+net.ipv4.tcp_retries2 = 8
+
+# 复用 TIME_WAIT 的连接,长轮询场景能省不少握手
+net.ipv4.tcp_tw_reuse = 1
+net.ipv4.tcp_fin_timeout = 15
+EOF
+sysctl -p /etc/sysctl.d/98-ykxmz-net.conf 2>&1 | sed 's/^/ /'
+
+echo
+echo " 当前拥塞控制: $(sysctl -n net.ipv4.tcp_congestion_control)"
+echo " 队列算法: $(sysctl -n net.core.default_qdisc)"
+echo " 可用算法: $(sysctl -n net.ipv4.tcp_available_congestion_control)"
+
+echo
+echo "════════ 4. 重启隧道 ════════"
+systemctl restart lanremote-tunnel
+sleep 40
+echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
+echo " 协议与落点:"
+grep -E 'Registered tunnel connection' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -3 \
+ | grep -oE 'ip=[0-9.]+ location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /'
+grep -oE 'Initial protocol [a-z0-9]+' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -2 | sed 's/^/ /'
+
+echo
+echo "════════ 5. 延迟实测(20 次,看平均)════════"
+total=0; ok=0; max=0
+for i in $(seq 1 20); do
+ r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 20 \
+ -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
+ code=$(echo "$r" | cut -d' ' -f1)
+ t=$(echo "$r" | cut -d' ' -f2)
+ if [ "$code" = "200" ]; then
+ ok=$((ok+1))
+ total=$(echo "$total + $t" | bc)
+ bigger=$(echo "$t > $max" | bc)
+ [ "$bigger" = "1" ] && max=$t
+ fi
+ sleep 1
+done
+avg=$(echo "scale=3; $total / $ok" | bc 2>/dev/null || echo "?")
+echo " 可用 $ok/20"
+echo " 平均 ${avg} 秒 最慢 ${max} 秒"
diff --git a/_build/package_remote.py b/_build/package_remote.py
index 6acdc2e..d1098b6 100644
--- a/_build/package_remote.py
+++ b/_build/package_remote.py
@@ -17,7 +17,7 @@ import zipfile
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
-VERSION = "1.7.0"
+VERSION = "1.7.1"
NAME = f"ykxmz-{VERSION}"
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
diff --git a/_build/probe_cf_edge2.py b/_build/probe_cf_edge2.py
new file mode 100644
index 0000000..056061a
--- /dev/null
+++ b/_build/probe_cf_edge2.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+r'''在设备上测 Cloudflare 隧道边缘 IP 的 TCP 握手延迟,挑优选 IP。
+
+为什么要这么干:
+ cloudflared 连 region1.v2.argotunnel.com,这个域名由 GeoDNS 决定 ——
+ 国内 4G 常被解析到美西(实测 lax13),单程 200ms+,
+ 往返加丢包重传能到 4 秒(用户实测 4141ms)。
+
+ Cloudflare 是 anycast,同一批 IP 从不同网络过去落点不同。
+ 做法:测各候选 IP 的 7844(隧道端口)握手延迟,
+ 把最快的写进 /etc/hosts 覆盖 GeoDNS。
+
+⚠ 关键教训:
+ 7844 **只对 Cloudflare 的隧道专用段开放**,不是所有 anycast IP 都行。
+ 专用段是 198.41.192.0/24 和 198.41.200.0/24(实测 198.41.192.107 通)。
+ 我第一次拿 104.16.x.x 测,56 个全不通,就是选错段了。
+'''
+from __future__ import annotations
+
+import socket
+import sys
+import time
+
+PORT = 7844 # cloudflared 的 HTTP/2 隧道端口
+
+# 已知能用的几个放前面:先确认能通,再放大范围扫
+KNOWN = [
+ "198.41.192.107", "198.41.192.7", "198.41.192.167",
+ "198.41.200.13", "198.41.200.33", "198.41.200.53", "198.41.200.63",
+]
+
+
+def build_candidates() -> list[str]:
+ ips = list(KNOWN)
+ for third in range(1, 255, 3):
+ ips.append("198.41.192." + str(third))
+ for third in range(1, 255, 3):
+ ips.append("198.41.200." + str(third))
+ seen = set()
+ out = []
+ for ip in ips:
+ if ip not in seen:
+ seen.add(ip)
+ out.append(ip)
+ return out
+
+
+def probe(ip: str, timeout: float = 2.5):
+ """返回 TCP 握手耗时(毫秒),不通返回 None。"""
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ start = time.perf_counter()
+ try:
+ sock.connect((ip, PORT))
+ return (time.perf_counter() - start) * 1000
+ except OSError:
+ return None
+ finally:
+ sock.close()
+
+
+def main() -> int:
+ candidates = build_candidates()
+ print(" 候选 %d 个(Cloudflare 隧道专用段)" % len(candidates))
+ print(" 测 %d 端口,每个 2 次取最快" % PORT)
+ print()
+
+ results = []
+ reachable = 0
+ for index, ip in enumerate(candidates, 1):
+ best = None
+ for _ in range(2):
+ ms = probe(ip)
+ if ms is not None and (best is None or ms < best):
+ best = ms
+ if best is not None:
+ reachable += 1
+ results.append((best, ip))
+ if index % 40 == 0:
+ print(" 已测 %d/%d,通了 %d 个" % (index, len(candidates), reachable))
+
+ print()
+ if not results:
+ print(" 一个都不通")
+ return 1
+
+ results.sort()
+ print(" 通了 %d 个,最快的 15 个:" % reachable)
+ for ms, ip in results[:15]:
+ print(" %7.1f ms %s" % (ms, ip))
+
+ print()
+ print(" 默认 GeoDNS 解析:")
+ for host in ("region1.v2.argotunnel.com", "region2.v2.argotunnel.com"):
+ try:
+ infos = socket.getaddrinfo(host, PORT)
+ v4 = sorted({i[4][0] for i in infos if i[0] == socket.AF_INET})
+ print(" %s: %s" % (host, v4 or "(只有 IPv6)"))
+ except OSError as exc:
+ print(" %s: 解析失败 %s" % (host, exc))
+
+ with open("/root/cf_best.txt", "w") as fh:
+ for ms, ip in results[:5]:
+ fh.write("%s %.0f\n" % (ip, ms))
+ print()
+ print(" 最快 5 个 -> /root/cf_best.txt")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/rename_to_ykxmz.py b/_build/rename_to_ykxmz.py
deleted file mode 100644
index 5c3f487..0000000
--- a/_build/rename_to_ykxmz.py
+++ /dev/null
@@ -1,279 +0,0 @@
-# -*- coding: utf-8 -*-
-"""把产品名改成 ykxmz,并加一个正经的卸载。
-
-用户要求:"改成 ykxmz"
-
-做这些:
- 1. 开机自启注册表的值名 -> ykxmz
- (任务管理器「启动」标签页里显示的就是这个名字)
- 同时迁移掉旧名 LanRemoteAgent,不留垃圾
- 2. 版本信息里的产品名/文件名 -> ykxmz
- 3. 打包出来的 exe 叫 ykxmz.exe
- 4. 加 --uninstall:一条命令清干净
- (停进程、删自启、删数据目录)
-
-刻意保留的东西:
- * **启动项保持可见**。改名字是品牌化,藏起来是另一回事。
- * 被控端被连接时的屏幕提示也保留。
-"""
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
-WINDOWS = ROOT / "windows"
-
-AUTOSTART = WINDOWS / "lanremote" / "autostart.py"
-VERSION = WINDOWS / "version_info.txt"
-BUILD = WINDOWS / "build_exe.ps1"
-RUN = WINDOWS / "run_agent.py"
-UNINSTALL = WINDOWS / "lanremote" / "uninstall.py"
-
-NEW_NAME = "ykxmz"
-
-
-# ==================== 1. autostart.py ====================
-
-AUTO_HEAD_OLD = '''RUN_KEY = r"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
-VALUE_NAME = "LanRemoteAgent"'''
-
-AUTO_HEAD_NEW = '''RUN_KEY = r"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
-# 任务管理器「启动」标签页里显示的就是这个名字。
-# 用 ykxmz 而不是冒充 "Windows xxx" —— 用的人应该看得出这是什么。
-VALUE_NAME = "ykxmz"
-# 早期版本用过的名字。开着自启的老用户升级后要把它清掉,别留两条。
-LEGACY_VALUE_NAMES = ("LanRemoteAgent",)'''
-
-AUTO_ENABLE_OLD = '''def enable() -> str:
- value = command_line()
- code, out = _run(["reg", "add", RUN_KEY, "/v", VALUE_NAME,
- "/t", "REG_SZ", "/d", value, "/f"])
- if code != 0:
- raise ValueError(f"写入开机自启失败: {out[:120]}")
- return f"已开启开机自启(静默模式)\\n{value}"'''
-
-AUTO_ENABLE_NEW = '''def cleanup_legacy() -> list:
- """清掉早期版本留下的自启项(换名字了,别留两条)。"""
- removed = []
- for name in LEGACY_VALUE_NAMES:
- code, _out = _run(["reg", "delete", RUN_KEY, "/v", name, "/f"])
- if code == 0:
- removed.append(name)
- return removed
-
-
-def enable() -> str:
- value = command_line()
- code, out = _run(["reg", "add", RUN_KEY, "/v", VALUE_NAME,
- "/t", "REG_SZ", "/d", value, "/f"])
- if code != 0:
- raise ValueError(f"写入开机自启失败: {out[:120]}")
- gone = cleanup_legacy()
- extra = f"(顺手清掉旧自启项 {', '.join(gone)})" if gone else ""
- return f"已开启开机自启(静默模式){extra}\\n{value}"'''
-
-# ==================== 2. version_info.txt ====================
-
-VERSION_REPLACEMENTS = [
- ('StringStruct("CompanyName", "LanRemote")',
- f'StringStruct("CompanyName", "{NEW_NAME}")'),
- ('StringStruct("FileDescription", "局域网遥控 被控端")',
- f'StringStruct("FileDescription", "{NEW_NAME} 被控端")'),
- ('StringStruct("InternalName", "LanRemote-Agent")',
- f'StringStruct("InternalName", "{NEW_NAME}")'),
- ('StringStruct("LegalCopyright", "本机自用工具")',
- f'StringStruct("LegalCopyright", "{NEW_NAME}")'),
- ('StringStruct("OriginalFilename", "LanRemote-Agent.exe")',
- f'StringStruct("OriginalFilename", "{NEW_NAME}.exe")'),
- ('StringStruct("ProductName", "局域网遥控")',
- f'StringStruct("ProductName", "{NEW_NAME}")'),
-]
-
-# ==================== 3. build_exe.ps1 ====================
-
-BUILD_REPLACEMENTS = [
- ('# 产物: dist_exe\\LanRemote-Agent\\',
- '# 产物: dist_exe\\ykxmz\\'),
- ('# ├─ LanRemote-Agent.exe <- 双击运行',
- '# ├─ ykxmz.exe <- 双击运行'),
- ('--name "LanRemote-Agent" `',
- '--name "ykxmz" `'),
- ('$target = Join-Path $root "dist_exe\\LanRemote-Agent\\LanRemote-Agent.exe"',
- '$target = Join-Path $root "dist_exe\\ykxmz\\ykxmz.exe"'),
-]
-
-# ==================== 4. uninstall.py(新文件)====================
-
-UNINSTALL_SRC = '''# -*- coding: utf-8 -*-
-"""卸载:一条命令把程序从这台电脑上清干净。
-
- ykxmz.exe --uninstall
-
-清这些:
- 1. 停掉正在跑的进程
- 2. 删掉开机自启项
- 3. 删掉数据目录(配对码、已配对手表……)
-
-**故意做这个**:一个能开机自启、又能远程控制的软件,
-必须能干净地卸载掉 —— 否则它就成了"赖着不走"的东西。
-电脑的主人应该随时能把它彻底清掉。
-"""
-from __future__ import annotations
-
-import os
-import shutil
-import subprocess
-import sys
-import time
-from pathlib import Path
-
-from .autostart import VALUE_NAME, RUN_KEY, LEGACY_VALUE_NAMES, _run
-from .config import app_dir, data_dir
-
-_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
-
-
-def _kill_running() -> str:
- """把正在跑的进程停掉(除了自己)。"""
- if not getattr(sys, "frozen", False):
- return "(源码模式,跳过进程清理)"
- exe = Path(sys.executable).name
- code, out = _run(["taskkill", "/F", "/IM", exe])
- if code == 0:
- return f"已停止进程 {exe}"
- if "not found" in out.lower() or "没有" in out:
- return "没有正在运行的进程"
- return f"停止进程:{out[:80]}"
-
-
-def _remove_autostart() -> str:
- done = []
- for name in (VALUE_NAME,) + tuple(LEGACY_VALUE_NAMES):
- code, _out = _run(["reg", "delete", RUN_KEY, "/v", name, "/f"])
- if code == 0:
- done.append(name)
- if done:
- return f"已删除开机自启项 {', '.join(done)}"
- return "没有开机自启项"
-
-
-def _remove_data() -> str:
- """删数据目录。删不掉就留着,不影响卸载。"""
- target = Path(data_dir())
- if not target.is_dir():
- return "没有数据目录"
- try:
- shutil.rmtree(target, ignore_errors=True)
- if not target.is_dir():
- return f"已删除数据目录 {target}"
- return f"数据目录没删干净(可能被占用):{target}"
- except Exception as exc: # noqa: BLE001
- return f"删数据目录失败:{exc}"
-
-
-def _schedule_self_delete() -> str:
- """把自己这个 exe 也删掉。
-
- 进程还在跑的时候删不掉自己,所以要起一个延迟的小命令,
- 等我们退出后再删目录。
- """
- if not getattr(sys, "frozen", False):
- return "(源码模式,不删自己)"
- folder = Path(sys.executable).resolve().parent
- cmd = (f'timeout /t 2 /nobreak >nul & '
- f'rmdir /s /q "{folder}"')
- try:
- subprocess.Popen(["cmd", "/c", cmd],
- creationflags=_NO_WINDOW | 0x00000008)
- return f"已安排删除程序目录:{folder}"
- except Exception as exc: # noqa: BLE001
- return f"(没安排上自删,请手动删 {folder}:{exc})"
-
-
-def run(keep_data: bool = False) -> str:
- """执行卸载,返回给人看的说明。"""
- lines = ["开始卸载 " + VALUE_NAME]
- lines.append(" " + _remove_autostart())
- if keep_data:
- lines.append(" 保留数据目录(你要求保留)")
- else:
- lines.append(" " + _remove_data())
- lines.append(" " + _schedule_self_delete())
- lines.append("")
- lines.append("卸载完成。程序目录会在几秒后自动删除。")
- return "\\n".join(lines)
-'''
-
-
-def main() -> int:
- ok = True
-
- # ---- autostart.py ----
- text = AUTOSTART.read_text(encoding="utf-8")
- if f'VALUE_NAME = "{NEW_NAME}"' not in text:
- if AUTO_HEAD_OLD in text:
- text = text.replace(AUTO_HEAD_OLD, AUTO_HEAD_NEW, 1)
- print(" autostart.py: 改名 + 旧名清理常量")
- else:
- print(" [失败] autostart.py 头部锚点没找到")
- ok = False
- if AUTO_ENABLE_OLD in text:
- text = text.replace(AUTO_ENABLE_OLD, AUTO_ENABLE_NEW, 1)
- print(" autostart.py: enable 顺手清旧项")
- else:
- print(" [跳过] autostart.py enable 锚点没找到")
- AUTOSTART.write_text(text, encoding="utf-8", newline="\n")
- else:
- print(" autostart.py: 已经改过")
-
- # ---- version_info.txt ----
- text = VERSION.read_text(encoding="utf-8")
- changed = 0
- for old, new in VERSION_REPLACEMENTS:
- if old in text:
- text = text.replace(old, new, 1)
- changed += 1
- if changed:
- VERSION.write_text(text, encoding="utf-8", newline="\n")
- print(f" version_info.txt: 改了 {changed} 个字段")
- else:
- print(" version_info.txt: 没变化(可能已改过)")
-
- # ---- build_exe.ps1 ----
- text = BUILD.read_text(encoding="utf-8")
- changed = 0
- for old, new in BUILD_REPLACEMENTS:
- if old in text:
- text = text.replace(old, new, 1)
- changed += 1
- if changed:
- # 保持 BOM(PowerShell 5.1 没 BOM 会按 GBK 读)
- BUILD.write_bytes(b"\xef\xbb\xbf" +
- text.replace("\r\n", "\n").encode("utf-8"))
- print(f" build_exe.ps1: 改了 {changed} 处(BOM 已保留)")
- else:
- print(" build_exe.ps1: 没变化")
-
- # ---- uninstall.py ----
- if UNINSTALL.exists():
- print(" uninstall.py: 已存在")
- else:
- UNINSTALL.write_text(UNINSTALL_SRC, encoding="utf-8", newline="\n")
- print(" uninstall.py: 新建")
-
- # ---- 语法检查 ----
- import ast
- for label, path in (("autostart.py", AUTOSTART),
- ("uninstall.py", UNINSTALL)):
- try:
- ast.parse(path.read_text(encoding="utf-8"))
- print(f" {label} 语法 OK")
- except SyntaxError as exc:
- print(f" {label} 语法错误: {exc}")
- ok = False
- return 0 if ok else 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/_build/site_preview.html b/_build/site_preview.html
new file mode 100644
index 0000000..fbae8ef
--- /dev/null
+++ b/_build/site_preview.html
@@ -0,0 +1,197 @@
+
+
+
放课件、放音频、投图到大屏、看看有没有人在跑无关程序、下课关机 —— + 不用走回讲台去点鼠标。
+ +手表上十个分页,常用操作都在第一屏
+电脑装一个程序,手表装一个 App,一台电脑配一次就行
+放课件、放音频、投图到大屏、看看谁在打游戏、下课关机 —— -不用走回讲台去点鼠标。
-