4 Commits
Author SHA1 Message Date
xmz7891 bc289c5c21 remove temp 2026-10-08 20:08:16 +08:00
xmz7891 d5a1943d1f 加两个运维辅助脚本
_build/watch_stability.py   连续打公网统计可用率(验证隧道加固效果)
  _build/desktop_state.py     列出当前可见窗口 + 输入桌面名
                              (排查"桌面消失/一片蓝"这类问题用)

这两个是查 2.1.1 / 2.1.2 那两个问题时写的,以后还会用到。

顺便记一笔当时的排查结论(2026-10-08):
用户报"桌面消失、一片蓝、有个蓝色窗口",查下来:
    * 输入桌面是 Default(正常),explorer 在跑
    * 那 9 个"全屏窗口"全是 Windows 自己的 UWP 组件
      (开始菜单 / 搜索 / 锁屏 / 输入法),程序枚举时会误报全屏
    * 真正开着的是「设置」App 和任务管理器
    * 自启项里没有 ykxmz,也没有残留的隐藏桌面或测试进程
    * 18:42:41 有一次**非正常关机**(Kernel-Power 41 / EventLog 6008),
      19:58 重启。没有任何蓝屏或硬件错误记录 ——
      最符合"掉电或硬挂",不是软件崩溃。
2026-10-08 20:08:16 +08:00
xmz7891 768309c1f2 remove temp 2026-10-08 20:04:35 +08:00
xmz7891 8dc47d0f48 2.1.2:隧道稳定性加固(继续修「网站还是容易挂」)
2.1.1 修掉了 nettune 死锁之后,公网上到 200,但**过一阵又 530**。
这次抓到的是隧道自己:

cloudflared 的 precheck 输出(tunnel.log):

    | UDP Connectivity  region1.v2.argotunnel.com  FAIL  QUIC connection failed                      |
    | TCP Connectivity  region1.v2.argotunnel.com  FAIL  HTTP/2 connection is blocked or unreachable |
    | Cloudflare API    api.cloudflare.com:443     PASS  API is reachable                            |
    | ERROR: Allow outbound QUIC traffic on port 7844 or use HTTP2.                                  |

也就是 **443 通、7844 不通**,而且是**间歇性**的(过几分钟自己又好了,
然后再次断)。Cloudflare 隧道的边缘连接走 7844(QUIC 走 UDP 7844、
HTTP/2 走 TCP 7844)。这条线路对它是间歇性阻断 —— 属于运营商那一侧,
设备改不了,但可以让 cloudflared 自己去适应。

== 两个改动 ==

1. **协议从写死改成 auto**
       lanremote-tunnel      --protocol quic    -> --protocol auto
       lanremote-tunnel-h2   --protocol http2   -> --protocol auto
   原来两条实例各赌一个协议:那一刻 7844 的 UDP 和 TCP 都不通就一起死,
   没有退路(实测连接数掉到 1/0、公网 530)。改成 auto 之后
   cloudflared 自己在 QUIC / HTTP/2 之间挑当前能通的那条,
   10 条连接全都有自适应能力。

2. **--retries 5 -> 1000000**
   原来失败 5 次就退出、靠 systemd Restart=always 拉起来,
   每次退出到重启之间网站都是断的。而这条线路的抖动是分钟级的 ——
   实测隧道每 3~10 分钟就被重启一次(19:37 / 19:40 / 19:43 / 19:50 / 19:52),
   公网跟着一起断。拉大之后抖动期间连接自己恢复,整条隧道不退出。

3. 顺手加日志轮转(tunnel.log 已经 2.98 MB,这台机器硬盘不大)。

== 实测 ==

    改前:连接数 1/0,公网 530
    改后:连接数 7+2(接近满配 10),公网连测 5 次全部 200

== 同步 ==

relay/deploy.sh 里的 MAIN_PROTO / ALT_PROTO 也改成 auto、
TUNNEL_BASE 的 --retries 也拉到 1000000,新机器装出来就是加固过的。

全量回归 46 个阶段全部通过。
2026-10-08 20:04:35 +08:00
20 changed files with 582 additions and 36 deletions
+32
View File
@@ -1,3 +1,35 @@
## 2.1.2
- **隧道稳定性加固**(继续修「网站还是容易挂」)。查到隧道自己的诊断输出:
```
| UDP Connectivity Cloudflare 边缘 FAIL 连不上 |
| TCP Connectivity Cloudflare 边缘 FAIL 连不上或被挡住 |
| Cloudflare API api.cloudflare.com:443 PASS 通的 |
| ERROR: Allow outbound QUIC traffic on port 7844 or use HTTP2. |
```
也就是说:**443 通,7844 不通**,而且是**一阵一阵的**
(过几分钟自己又好了,然后再次断)。Cloudflare 隧道的边缘连接走 7844,
这条线路对它是间歇性阻断 —— 这属于运营商那一侧,设备改不了。
**但可以让它自己去适应**:
- 两条隧道实例原来**各自写死一个协议**(一条只走 QUIC、一条只走 HTTP/2)。
那一刻如果 7844 的 UDP 和 TCP 都不通,两条一起死,没有退路。
现在都改成 **自动选择**,cloudflared 自己在 QUIC / HTTP/2 之间挑
当前能通的那条。等于所有连接都有自适应能力,而不是
"8 条赌 UDP、2 条赌 TCP"。
- 重试次数从 5 提到「一直重试」。原来失败 5 次就退出、靠系统重新拉起,
每次退出到重启之间网站都是断的;而这条线路的抖动是分钟级的,
5 次根本扛不过去(实测隧道每 3~10 分钟就被重启一次,公网跟着 530)。
现在抖动期间连接自己恢复,整条隧道不退出。
**改完实测**:连接数 7+2(接近满配 10),公网连测 5 次全部 200。
- 顺手加了日志轮转(隧道日志已经涨到 3 MB,这台小机器硬盘不大)。
## 2.1.1 ## 2.1.1
- **修一个会让网站整个挂掉的死锁**(这是「网站还是容易挂」的真因)。 - **修一个会让网站整个挂掉的死锁**(这是「网站还是容易挂」的真因)。
+120
View File
@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
r'''看当前桌面/窗口的真实状态 —— 诊断"桌面消失、一片蓝"。
用户报:"我现在桌面消失了,电脑一片蓝,然后还有个蓝色的窗口"。
已知:
18:42:41 机器异常关机(Kernel-Power 41 / EventLog 6008,无蓝屏记录)
19:58:23 重新启动
20:01:37 explorer 起来
自启项里没有 ykxmz,当前也没有 ykxmz 在跑
要查的是:现在屏幕上到底有什么、是不是有全屏窗口盖住了桌面。
'''
from __future__ import annotations
import ctypes
import ctypes.wintypes as wt
import subprocess
user32 = ctypes.WinDLL("user32", use_last_error=True)
EnumWindows = user32.EnumWindows
EnumWindows.argtypes = [ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM),
wt.LPARAM]
class RECT(ctypes.Structure):
_fields_ = [("left", wt.LONG), ("top", wt.LONG),
("right", wt.LONG), ("bottom", wt.LONG)]
def windows() -> list:
out = []
def cb(hwnd, _lp):
if not user32.IsWindowVisible(hwnd):
return True
title = ctypes.create_unicode_buffer(512)
user32.GetWindowTextW(hwnd, title, 512)
cls = ctypes.create_unicode_buffer(256)
user32.GetClassNameW(hwnd, cls, 256)
r = RECT()
user32.GetWindowRect(hwnd, ctypes.byref(r))
pid = wt.DWORD()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
w, h = r.right - r.left, r.bottom - r.top
if w > 80 and h > 80:
out.append({"hwnd": hwnd, "title": title.value, "cls": cls.value,
"pid": pid.value, "w": w, "h": h,
"x": r.left, "y": r.top})
return True
EnumWindows(ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)(cb), 0)
return out
def proc_name(pid: int) -> str:
try:
r = subprocess.run(
["powershell", "-NoProfile", "-Command",
"(Get-Process -Id %d -EA SilentlyContinue).ProcessName" % pid],
capture_output=True, text=True, timeout=8)
return (r.stdout or "").strip() or "?"
except Exception: # noqa: BLE001
return "?"
def main() -> int:
sw = user32.GetSystemMetrics(0)
sh = user32.GetSystemMetrics(1)
print(" 屏幕: %d x %d" % (sw, sh))
print(" 当前输入桌面: %s" % _desktop_name())
print()
ws = windows()
ws.sort(key=lambda w: w["w"] * w["h"], reverse=True)
print(" 可见窗口 %d 个(按面积排序,前 20):" % len(ws))
for w in ws[:20]:
full = w["w"] >= sw - 4 and w["h"] >= sh - 4
pn = proc_name(w["pid"])
print(" %s %-14s pid=%-6d %dx%d @(%d,%d)"
% ("★全屏" if full else " ", pn, w["pid"],
w["w"], w["h"], w["x"], w["y"]))
print(" 类=%-22s 标题=%r" % (w["cls"][:22], w["title"][:60]))
print()
fulls = [w for w in ws if w["w"] >= sw - 4 and w["h"] >= sh - 4]
if fulls:
print(" ⚠ 有 %d 个全屏窗口 —— 下面这些就是盖住桌面的嫌疑:" % len(fulls))
for w in fulls:
print(" pid=%d (%s) 类=%s 标题=%r"
% (w["pid"], proc_name(w["pid"]), w["cls"], w["title"][:50]))
else:
print(" ✓ 没有全屏窗口盖着")
print()
ex = subprocess.run(["powershell", "-NoProfile", "-Command",
"Get-Process explorer -EA SilentlyContinue | "
"Select-Object -First 1 -ExpandProperty Id"],
capture_output=True, text=True, timeout=10)
print(" explorer PID: %s" % ((ex.stdout or "").strip() or "**没在跑**"))
return 0
def _desktop_name() -> str:
"""当前输入桌面的名字。Default = 正常,Winlogon = 锁屏。"""
h = user32.OpenInputDesktop(0, False, 0x0001)
if not h:
return "(拿不到 —— 可能锁屏或没有交互桌面)"
try:
sb = ctypes.create_unicode_buffer(256)
need = wt.DWORD()
ok = user32.GetUserObjectInformationW(h, 2, sb, 512,
ctypes.byref(need))
return sb.value if ok else "(拿不到名字)"
finally:
user32.CloseDesktop(h)
if __name__ == "__main__":
import sys
sys.exit(main())
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
r'''查隧道为什么掉连接(公网 530,连接数 1/0)。
上一轮才刚修掉 nettune 死锁、把公网拉到 200;这次重启 nettune 之后
又变成 530 了。要分清是:
* nettune 又卡了(那个死锁没修干净?)
* 还是隧道自己掉了(cloudflared 的问题)
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from devssh import run # noqa: E402
REMOTE = r'''
echo "════ 1. 服务状态 + 有没有卡住的 ════"
for s in lanremote-relay lanremote-tunnel lanremote-tunnel-h2 lanremote-nettune; do
printf ' %-24s %s\n' "$s" "$(systemctl is-active $s)"
done
echo " --- nettune 卡住吗 ---"
systemctl show -p SubState -p MainPID -p ExecMainStartTimestamp lanremote-nettune 2>&1 | sed 's/^/ /'
echo " --- 有没有卡住的 systemctl ---"
ps -eo pid,etime,cmd 2>/dev/null | grep -E 'systemctl|net-tune' | grep -v grep | sed 's/^/ /'
echo
echo "════ 2. net-tune 的修复在设备上吗 ════"
grep -c NETTUNE_SERVICE_CALL /root/lanremote-relay/net-tune.sh
echo " unit 里有吗: $(grep -c NETTUNE_SERVICE_CALL /etc/systemd/system/lanremote-nettune.service)"
echo
echo "════ 3. 隧道为什么连接数是 0 ════"
journalctl -u lanremote-tunnel --no-pager -n 25 2>/dev/null | tail -25 | sed 's/^/ /'
echo
echo "════ 4. 隧道 unit 长什么样(重启策略 / 协议)════"
systemctl cat lanremote-tunnel 2>/dev/null | grep -vE '^#' | head -25 | sed 's/^/ /'
echo
echo "════ 5. cloudflared 进程 ════"
ps -eo pid,etime,rss,cmd 2>/dev/null | grep cloudflared | grep -v grep | sed 's/^/ /'
'''
def main() -> int:
out = run(REMOTE, timeout=300)
text = out[1] if isinstance(out, tuple) else str(out)
print(text)
return 0
if __name__ == "__main__":
sys.exit(main())
+66
View File
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
r'''看隧道为什么反复掉(公网 530 的真因)。
已知:
nettune 死锁已修(SubState=exited,不再卡)
但隧道每 3~10 分钟被重启一次(部分是我加的预案在救、部分是它自己掉)
公网 530
要查的是 tunnel.log 里 cloudflared 到底在报什么。
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from devssh import run # noqa: E402
REMOTE = r'''
LOG=/root/lanremote-relay/tunnel.log
H2LOG=/root/lanremote-relay/tunnel-h2.log
echo "════ 1. tunnel.log 里的错误行(最近 40 条)════"
grep -iE 'error|fail|warn|timeout|retry|refused|EOF|unregister|connection' "$LOG" 2>/dev/null | tail -40 | sed 's/^/ /'
echo
echo "════ 2. tunnel.log 最后 25 行(原样)════"
tail -25 "$LOG" 2>/dev/null | sed 's/^/ /'
echo
echo "════ 3. h2 那份的日志 ════"
tail -15 "$H2LOG" 2>/dev/null | sed 's/^/ /'
echo
echo "════ 4. 两份日志多大(写爆了会拖慢小机器)════"
ls -la "$LOG" "$H2LOG" 2>/dev/null | sed 's/^/ /'
echo
echo "════ 5. cloudflared 版本 + 配置 ════"
/usr/local/bin/cloudflared --version 2>&1 | sed 's/^/ /'
echo " --- config.yml ---"
cat /root/.cloudflared/config.yml 2>/dev/null | sed 's/^/ /'
echo
echo "════ 6. 现在两条隧道的连接数 ════"
for p in 20241 20242; do
n=$(curl -s --max-time 5 "http://127.0.0.1:$p/metrics" 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1)
echo " $p -> ${n:-拿不到}"
done
echo
echo "════ 7. 公网现在通吗 ════"
echo " $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://relay.xxmxxs.top/v1/health)"
'''
def main() -> int:
out = run(REMOTE, timeout=300)
text = out[1] if isinstance(out, tuple) else str(out)
print(text)
return 0
if __name__ == "__main__":
sys.exit(main())
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
r'''增强隧道稳定性 —— 「网站还是容易挂」的真因修法。
== 查到什么 ==
cloudflared 自己的 precheck 输出(tunnel.log):
| UDP Connectivity region1.v2.argotunnel.com FAIL QUIC connection failed |
| UDP Connectivity region2.v2.argotunnel.com FAIL QUIC connection failed |
| TCP Connectivity region1.v2.argotunnel.com FAIL HTTP/2 connection is blocked or unreachable |
| TCP Connectivity region2.v2.argotunnel.com FAIL HTTP/2 connection is blocked or unreachable |
| Cloudflare API api.cloudflare.com:443 PASS API is reachable |
| ERROR: Allow outbound QUIC traffic on port 7844 or use HTTP2. |
也就是:**443 通,7844 不通**,而且是**间歇性**的
(日志后面又出现 Registered tunnel connection,过一阵又掉)。
Cloudflare 隧道的边缘连接走 7844(QUIC 走 UDP 7844、HTTP/2 走 TCP 7844)。
这条线路对 7844 是间歇性阻断的 —— 这不是设备能修的,
但**可以让 cloudflared 自己去适应**。
== 现在的配置有两个问题 ==
1. **两条实例各写死一个协议**
lanremote-tunnel --protocol quic (只走 UDP 7844)
lanremote-tunnel-h2 --protocol http2 (只走 TCP 7844)
如果那一刻 7844 的 UDP 和 TCP 都不通,两条一起死 —— 没有退路。
实测就是这样:连接数掉到 1/0,公网 530。
2. **--retries 5**
cloudflared 重试 5 次就**退出**,靠 systemd Restart=always 拉起来。
每次退出/重启期间网站都是断的。而这个线路的抖动是分钟级的,
5 次远远不够扛过去。
== 改法 ==
1. `--protocol auto` —— 让 cloudflared **自己**在 QUIC 和 HTTP/2 之间选,
哪条通走哪条。两条实例都改成 auto,等于 10 条连接全都能自适应。
(而不是现在这样"8 条赌 UDP、2 条赌 TCP")
2. `--retries` 提到很大(相当于"一直重试,别退出")——
抖动期间连接会自己恢复,不会整条隧道退出让网站断掉。
3. 顺手加日志轮转:tunnel.log 已经涨到 2.98 MB,
这台机器只有 460MB 内存 + 小硬盘,无限增长不合适。
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from devssh import run # noqa: E402
def patch_unit(name: str, proto: str) -> str:
"""读设备上现有的 unit,改协议和重试次数。
用 sed 现场改而不是整份重写 —— 现有 unit 里还有
metrics 端口、ha-connections、MemoryMax 这些实测调出来的参数,
整份重写容易漏掉。
"""
cmds = [
# 协议
"sed -i 's/--protocol [a-z0-9]*/--protocol %s/' %s" % (proto, name),
# 重试次数:改成很大(一直重试)
"sed -i 's/--retries [0-9]*/--retries 1000000/' %s" % name,
]
return " && ".join(cmds)
def main() -> int:
print("=" * 62)
print(" 隧道稳定性:协议改 auto + 加大重试 + 日志轮转")
print("=" * 62)
UNIT_DIR = "/etc/systemd/system"
print()
print(" ── 1. 两条隧道都改成 --protocol auto ──")
out = run(
"cd %s && "
"sed -i 's/--protocol [a-z0-9]*/--protocol auto/' "
"lanremote-tunnel.service lanremote-tunnel-h2.service && "
"echo ' tunnel:'; grep -o -- '--protocol [a-z0-9]*' "
"lanremote-tunnel.service; "
"echo ' h2:'; grep -o -- '--protocol [a-z0-9]*' "
"lanremote-tunnel-h2.service; "
# 重试次数
"sed -i 's/--retries [0-9]*/--retries 1000000/' "
"lanremote-tunnel.service lanremote-tunnel-h2.service && "
"echo ' 重试:'; grep -o -- '--retries [0-9]*' "
"lanremote-tunnel.service lanremote-tunnel-h2.service"
% UNIT_DIR, timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 2. 日志轮转(tunnel.log 已经 2.98 MB)──")
out = run(
"cat > /etc/logrotate.d/lanremote <<'EOF'\n"
"/root/lanremote-relay/*.log {\n"
" size 5M\n"
" rotate 2\n"
" compress\n"
" missingok\n"
" notifempty\n"
" copytruncate\n"
"}\n"
"EOF\n"
"echo ' logrotate 配置已写'; "
"logrotate --debug /etc/logrotate.d/lanremote 2>&1 | tail -3",
timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 3. 重启两条隧道让新参数生效 ──")
out = run(
"systemctl daemon-reload && "
"systemctl restart lanremote-tunnel lanremote-tunnel-h2 && "
"echo ' 等 40 秒让连接建立…' && sleep 40 && "
"echo \" tunnel: $(systemctl is-active lanremote-tunnel)\"; "
"echo \" h2: $(systemctl is-active lanremote-tunnel-h2)\"",
timeout=300)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 4. 连接数 ──")
out = run(
"for p in 20241 20242; do "
"n=$(curl -s --max-time 5 http://127.0.0.1:$p/metrics 2>/dev/null "
"| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' "
"| head -1); "
"echo \" $p -> ${n:-拿不到}\"; done; "
"echo ' --- 用的什么协议 ---'; "
"grep -o 'protocol=[a-z0-9]*' /root/lanremote-relay/tunnel.log 2>/dev/null "
"| tail -6 | sort | uniq -c | sed 's/^/ /'", timeout=120)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" ── 5. 公网(连测 5 次看稳不稳)──")
out = run(
"for i in 1 2 3 4 5; do "
"c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "
"https://relay.xxmxxs.top/v1/health); "
"echo \" 第 $i 次: $c\"; sleep 3; done", timeout=200)
for ln in out[1].strip().splitlines():
print(" " + ln)
print()
print(" 说明:3844/7844 那条线路封不封是运营商的事,设备改不了。")
print(" 但 --protocol auto 让 cloudflared 自己绕,")
print(" --retries 拉大让它别退出 —— 抖动期间能自己恢复。")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -17,7 +17,7 @@ import zipfile
from pathlib import Path from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
VERSION = "2.1.1" VERSION = "2.1.2"
NAME = f"ykxmz-{VERSION}" NAME = f"ykxmz-{VERSION}"
AGENT_SRC = ROOT / "dist_exe" / "ykxmz" AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
+85
View File
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
r'''稳定性观测:连续打公网,统计可用率。
用途:验证 2.1.2 的隧道加固(协议 auto + 重试拉大)真的有效。
2.1.1 之前是"过一阵就 530",所以只看一次 200 说明不了问题,
要**连续观测一段时间**。
用法:
python _build/watch_stability.py # 默认 20 次、间隔 15 秒
python _build/watch_stability.py 40 20 # 40 次、间隔 20 秒
'''
from __future__ import annotations
import sys
import time
import urllib.error
import urllib.request
URL = "https://relay.xxmxxs.top/v1/health"
def once(timeout: float = 15.0) -> tuple:
"""返回 (状态码文本, 耗时秒)。"""
t0 = time.time()
try:
req = urllib.request.Request(
URL, headers={"Cache-Control": "no-cache",
"User-Agent": "ykxmz-watch"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
resp.read(64)
return str(resp.status), time.time() - t0
except urllib.error.HTTPError as exc:
try:
exc.read(64)
except Exception: # noqa: BLE001
pass
return str(exc.code), time.time() - t0
except Exception as exc: # noqa: BLE001
return "无响应(%s)" % type(exc).__name__, time.time() - t0
def main() -> int:
times = int(sys.argv[1]) if len(sys.argv) > 1 else 20
gap = float(sys.argv[2]) if len(sys.argv) > 2 else 15.0
print("=" * 58)
print(" 公网稳定性观测:%d 次,间隔 %.0f 秒(约 %.0f 分钟)"
% (times, gap, times * gap / 60))
print("=" * 58)
codes = []
durs = []
for i in range(1, times + 1):
code, dur = once()
codes.append(code)
durs.append(dur)
mark = "✓" if code == "200" else "✗"
print(" %s 第 %2d 次: %-8s %.2fs" % (mark, i, code, dur))
if i < times:
time.sleep(gap)
ok = sum(1 for c in codes if c == "200")
print()
print("=" * 58)
print(" 200 的次数: %d / %d" % (ok, times))
print(" 可用率: %.1f%%" % (100.0 * ok / max(1, times)))
if durs:
sd = sorted(durs)
print(" 延迟 中位: %.2fs P90: %.2fs 最大: %.2fs"
% (sd[len(sd) // 2], sd[min(len(sd) - 1, int(len(sd) * 0.9))],
sd[-1]))
print()
if ok == times:
print(" ✅ 全程 200 —— 加固有效")
elif ok >= times * 0.9:
print(" ⚠ 偶尔失败(%d 次)—— 比加固前好,但还没完全稳"
% (times - ok))
else:
print(" ❌ 可用率偏低(%d 次失败)—— 还要继续查"
% (times - ok))
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -23,7 +23,7 @@ def _latest_apk() -> Path:
(ROOT / "dist").glob("*watch*.apk"), (ROOT / "dist").glob("*watch*.apk"),
key=lambda p: p.stat().st_mtime, reverse=True) key=lambda p: p.stat().st_mtime, reverse=True)
return candidates[0] if candidates else \ return candidates[0] if candidates else \
ROOT / "dist" / "ykxmz-watch-2.1.1.apk" ROOT / "dist" / "ykxmz-watch-2.1.2.apk"
APK = _latest_apk() APK = _latest_apk()
+4 -4
View File
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
# 被控端 -> 一个 zip # 被控端 -> 一个 zip
# 服务器端 -> 一个 zip # 服务器端 -> 一个 zip
# 控制端 -> 直接给 APK(不打 zip) # 控制端 -> 直接给 APK(不打 zip)
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.1.zip" AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.2.zip"
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.1.zip" SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.2.zip"
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.1.apk" WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.2.apk"
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain") TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe" JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe" JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
@@ -89,7 +89,7 @@ def main() -> int:
for label, path in (("被控端", agent_dir / "README-agent.txt"), for label, path in (("被控端", agent_dir / "README-agent.txt"),
("服务器端", relay_dir / "README-server.txt"), ("服务器端", relay_dir / "README-server.txt"),
("控制端(旁边那份)", ("控制端(旁边那份)",
ROOT / "dist" / "ykxmz-watch-2.1.1-说明.txt")): ROOT / "dist" / "ykxmz-watch-2.1.2-说明.txt")):
raw = path.read_bytes() if path.is_file() else b"" raw = path.read_bytes() if path.is_file() else b""
check(f"{label}说明有 UTF-8 BOM", check(f"{label}说明有 UTF-8 BOM",
raw[:3] == b"\xef\xbb\xbf", raw[:3]) raw[:3] == b"\xef\xbb\xbf", raw[:3])
+1 -1
View File
@@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lanremote.watch" package="com.lanremote.watch"
android:versionCode="5" android:versionCode="5"
android:versionName="2.1.1"> android:versionName="2.1.2">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+2 -2
View File
@@ -27,7 +27,7 @@ $d8 = Join-Path $bt "d8.bat"
$work = Join-Path $android "build" $work = Join-Path $android "build"
$outDir = Join-Path $root "dist" $outDir = Join-Path $root "dist"
$apkName = "ykxmz-watch-2.1.1.apk" $apkName = "ykxmz-watch-2.1.2.apk"
function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan } function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan }
@@ -65,7 +65,7 @@ $unsigned = Join-Path $work "app-unsigned.apk"
--min-sdk-version 24 ` --min-sdk-version 24 `
--target-sdk-version 34 ` --target-sdk-version 34 `
--version-code 5 ` --version-code 5 `
--version-name "2.1.1" ` --version-name "2.1.2" `
--auto-add-overlay --auto-add-overlay
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" } if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
+3 -3
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.1 版本:2.1.2
作者:小麦子 作者:小麦子
装好之后看这一页:怎么确认它在跑、怎么改设置、 装好之后看这一页:怎么确认它在跑、怎么改设置、
@@ -197,7 +197,7 @@
八、版本记录 八、版本记录
-------------------------------------------------------- --------------------------------------------------------
2.1.1 2.1.2
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍 * 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2, * 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
合计 10 条连接),一种协议犯病另一种还在服务 合计 10 条连接),一种协议犯病另一种还在服务
@@ -218,6 +218,6 @@
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.1 软件:ykxmz 2.1.2
许可条款见《许可协议》(LICENSE.txt) 许可条款见《许可协议》(LICENSE.txt)
-------------------------------------------------------- --------------------------------------------------------
+2 -2
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.1 版本:2.1.2
作者:小麦子 作者:小麦子
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的 请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
@@ -164,6 +164,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.1 软件:ykxmz 2.1.2
继续安装,即表示你已阅读并同意《许可协议》。 继续安装,即表示你已阅读并同意《许可协议》。
-------------------------------------------------------- --------------------------------------------------------
+3 -3
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz(远程管理工具) 软件名称:ykxmz(远程管理工具)
版本:2.1.1 版本:2.1.2
作者:小麦子 作者:小麦子
生效日期:2026 年 10 月 5 日 生效日期:2026 年 10 月 5 日
@@ -16,7 +16,7 @@
> 都不许拿去做下面第三条那些事。** > 都不许拿去做下面第三条那些事。**
> >
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"—— > (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
> 那和 AGPL 冲突,所以 2.1.1 起拆开了:代码授权归 LICENSE, > 那和 AGPL 冲突,所以 2.1.2 起拆开了:代码授权归 LICENSE,
> 使用约束归这份文件。) > 使用约束归这份文件。)
@@ -160,7 +160,7 @@
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.1 软件:ykxmz 2.1.2
如果你不同意本条款的任何一条,请不要安装或使用本软件。 如果你不同意本条款的任何一条,请不要安装或使用本软件。
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。 代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
+8 -8
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz(远程管理工具) 软件名称:ykxmz(远程管理工具)
版本:2.1.1 版本:2.1.2
作者:小麦子 作者:小麦子
生效日期:2026 年 10 月 5 日 生效日期:2026 年 10 月 5 日
@@ -16,7 +16,7 @@
> 都不许拿去做下面第三条那些事。** > 都不许拿去做下面第三条那些事。**
> >
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"—— > (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
> 那和 AGPL 冲突,所以 2.1.1 起拆开了:代码授权归 LICENSE, > 那和 AGPL 冲突,所以 2.1.2 起拆开了:代码授权归 LICENSE,
> 使用约束归这份文件。) > 使用约束归这份文件。)
@@ -160,7 +160,7 @@
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.1 软件:ykxmz 2.1.2
如果你不同意本条款的任何一条,请不要安装或使用本软件。 如果你不同意本条款的任何一条,请不要安装或使用本软件。
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。 代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
@@ -173,7 +173,7 @@
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.1 版本:2.1.2
作者:小麦子 作者:小麦子
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的 请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
@@ -334,7 +334,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.1 软件:ykxmz 2.1.2
继续安装,即表示你已阅读并同意《许可协议》。 继续安装,即表示你已阅读并同意《许可协议》。
-------------------------------------------------------- --------------------------------------------------------
@@ -345,7 +345,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.1 版本:2.1.2
作者:小麦子 作者:小麦子
装好之后看这一页:怎么确认它在跑、怎么改设置、 装好之后看这一页:怎么确认它在跑、怎么改设置、
@@ -539,7 +539,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
八、版本记录 八、版本记录
-------------------------------------------------------- --------------------------------------------------------
2.1.1 2.1.2
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍 * 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2, * 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
合计 10 条连接),一种协议犯病另一种还在服务 合计 10 条连接),一种协议犯病另一种还在服务
@@ -560,6 +560,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.1 软件:ykxmz 2.1.2
许可条款见《许可协议》(LICENSE.txt) 许可条款见《许可协议》(LICENSE.txt)
-------------------------------------------------------- --------------------------------------------------------
+2 -2
View File
@@ -27,7 +27,7 @@
; ============================================================================ ; ============================================================================
#define MyAppName "ykxmz" #define MyAppName "ykxmz"
#define MyAppVersion "2.1.1" #define MyAppVersion "2.1.2"
#define MyAppPublisher "小麦子" #define MyAppPublisher "小麦子"
#define MyAppExeName "ykxmz.exe" #define MyAppExeName "ykxmz.exe"
@@ -163,7 +163,7 @@ Filename: "{app}\{#MyAppExeName}"; Parameters: "--autostart on"; \
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住 ; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
; (PowerShell 的 Start-Process -Wait 就是)。 ; (PowerShell 的 Start-Process -Wait 就是)。
; 学校批量部署请用 cmd: ; 学校批量部署请用 cmd:
; cmd /c ykxmz-setup-2.1.1.exe /VERYSILENT /CODE=123456 ; cmd /c ykxmz-setup-2.1.2.exe /VERYSILENT /CODE=123456
; cmd 只等安装器本身,不等孙子进程,不会卡。 ; cmd 只等安装器本身,不等孙子进程,不会卡。
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \ Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
Description: "立即启动(后台运行,不显示窗口)"; \ Description: "立即启动(后台运行,不显示窗口)"; \
+31 -5
View File
@@ -204,9 +204,26 @@ fi
# #
# (早先这里写的是"两条同时跑会互相争抢",那个结论是错的,已作废。 # (早先这里写的是"两条同时跑会互相争抢",那个结论是错的,已作废。
# 再早还写过"运营商封 UDP",那个测量方法本身是错的,也作废。) # 再早还写过"运营商封 UDP",那个测量方法本身是错的,也作废。)
MAIN_PROTO=quic # ⚠ 用 auto,不要写死 quic / http2。
#
# 实测(2026-10-08)这条线路对 Cloudflare 隧道的 **7844 端口**
# (QUIC 走 UDP 7844、HTTP/2 走 TCP 7844)是**间歇性阻断**的,
# 而 443 一直是通的。cloudflared 自己的 precheck 报得很清楚:
#
# | UDP Connectivity region1.v2.argotunnel.com FAIL QUIC connection failed |
# | TCP Connectivity region1.v2.argotunnel.com FAIL HTTP/2 connection is blocked or unreachable |
# | Cloudflare API api.cloudflare.com:443 PASS API is reachable |
# | ERROR: Allow outbound QUIC traffic on port 7844 or use HTTP2. |
#
# 写死协议的时候,那一刻 7844 的 UDP 和 TCP 都不通 -> 两条实例一起死
# -> 连接数掉到 1/0 -> 公网 530。实测就是这么挂的。
#
# 改成 auto 之后 **cloudflared 自己**在 QUIC / HTTP/2 之间挑
# 当前能通的那条,两条实例都带自适应能力(而不是
# "8 条赌 UDP、2 条赌 TCP")。改完实测连接数 7+2、公网连测 5 次全 200。
MAIN_PROTO=auto
MAIN_CONNS="${QUIC_CONNS:-8}" MAIN_CONNS="${QUIC_CONNS:-8}"
ALT_PROTO=http2 ALT_PROTO=auto
ALT_CONNS="${H2_CONNS:-2}" ALT_CONNS="${H2_CONNS:-2}"
# metrics 端口必须**显式钉死**。 # metrics 端口必须**显式钉死**。
@@ -581,9 +598,18 @@ EOF
# --edge-ip-version 4 这条线路有 IPv6 地址但实际不通, # --edge-ip-version 4 这条线路有 IPv6 地址但实际不通,
# 边缘默认优先 IPv6 会一直超时 # 边缘默认优先 IPv6 会一直超时
# --ha-connections N 每个单元各自多少条连接(默认 quic 8 / h2 2) # --ha-connections N 每个单元各自多少条连接(默认 quic 8 / h2 2)
# --retries 5 连边缘失败时多重试 # --retries N 连边缘失败时重试多少次
# --metrics ADDR:PORT **必须显式钉死**,见下面的注释 #
TUNNEL_BASE="--no-autoupdate --edge-ip-version 4 --retries 5" # ⚠ --retries 从 5 提到 1000000(相当于"一直重试,别退出")。
#
# 原来 5 次就退出,靠 systemd Restart=always 拉起来 ——
# 每次退出到重启之间网站都是断的。而这条线路对 7844 的阻断是
# **分钟级**的,5 次重试远远扛不过去:实测隧道每 3~10 分钟
# 就被重启一次,公网跟着 530。
#
# 拉大之后,抖动期间连接自己恢复,整条隧道不退出,
# 网站也就不会跟着断。
TUNNEL_BASE="--no-autoupdate --edge-ip-version 4 --retries 1000000"
if [ -n "$TOKEN" ]; then if [ -n "$TOKEN" ]; then
# Token 模式:Token 存单独的 600 权限文件,不写进 unit # Token 模式:Token 存单独的 600 权限文件,不写进 unit
+1 -1
View File
@@ -59,7 +59,7 @@ from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
VERSION = "2.1.1" VERSION = "2.1.2"
# ---------------- 参数(都能用环境变量覆盖)---------------- # ---------------- 参数(都能用环境变量覆盖)----------------
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""局域网遥控 · Windows 被控端。""" """局域网遥控 · Windows 被控端。"""
__version__ = "2.1.1" __version__ = "2.1.2"
APP_NAME = "ykxmz" APP_NAME = "ykxmz"
+2 -2
View File
@@ -27,12 +27,12 @@ VSVersionInfo(
[ [
StringStruct("CompanyName", "ykxmz"), StringStruct("CompanyName", "ykxmz"),
StringStruct("FileDescription", "ykxmz"), StringStruct("FileDescription", "ykxmz"),
StringStruct("FileVersion", "2.1.1.0"), StringStruct("FileVersion", "2.1.2.0"),
StringStruct("InternalName", "ykxmz"), StringStruct("InternalName", "ykxmz"),
StringStruct("LegalCopyright", "ykxmz"), StringStruct("LegalCopyright", "ykxmz"),
StringStruct("OriginalFilename", "ykxmz.exe"), StringStruct("OriginalFilename", "ykxmz.exe"),
StringStruct("ProductName", "ykxmz"), StringStruct("ProductName", "ykxmz"),
StringStruct("ProductVersion", "2.1.1.0"), StringStruct("ProductVersion", "2.1.2.0"),
], ],
) )
] ]