ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性

被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+279
View File
@@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
"""验证投图的三种显示方式:屏幕中间 / 全屏 / 自定义大小。
早先版本用 Win32 FindWindow 去找窗口,会偶发找不到 —— Tk 把窗口建好
到系统真正映射之间有几十毫秒时差,机器一忙就错过,于是测试不稳定。
现在改成**直接读 Tk 窗口自己的几何**(winfo_rootx/rooty/width/height),
完全确定,不受系统时序影响。
"""
from __future__ import annotations
import os
import shutil
import sys
import time
from pathlib import Path
from PIL import Image, ImageDraw, ImageGrab
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "windows"))
import tkinter as tk # noqa: E402
from lanremote.actions import Actions # noqa: E402
from lanremote.config import Store # noqa: E402
from lanremote.headless import HeadlessUI # noqa: E402
PASSED = 0
FAILED = 0
SHOTS = ROOT / "_tests" / "shots"
def check(name: str, ok: bool, detail: str = "") -> None:
global PASSED, FAILED
if ok:
PASSED += 1
print(f" [PASS] {name}")
else:
FAILED += 1
print(f" [FAIL] {name} {detail}")
def make_test_image(path: Path, width: int = 1600, height: int = 1000) -> None:
img = Image.new("RGB", (width, height), (0x18, 0x5C, 0xE8))
draw = ImageDraw.Draw(img)
draw.rectangle([40, 40, width - 40, height - 40], outline=(255, 255, 255), width=8)
draw.rectangle([120, 120, width - 120, height - 120], fill=(255, 255, 255))
draw.rectangle([200, 200, width - 200, height - 200], fill=(0x1F, 0x23, 0x28))
path.parent.mkdir(parents=True, exist_ok=True)
img.save(path)
def pump(_root, seconds: float) -> None:
"""等界面稳定。
注意:**不要**在这里调 root.update()。HeadlessUI 有自己的泵线程在
update(),而 Tk 不是线程安全的 —— 两个线程同时 update 会让泵线程
静默挂掉,之后 ui_call 就永远等不到结果。这里只需要等。
"""
time.sleep(seconds)
def geometry_of(win):
"""返回窗口客户区的 (left, top, width, height)。"""
if win is None:
return None
try:
if not win.winfo_exists():
return None
win.update_idletasks()
left = win.winfo_rootx()
top = win.winfo_rooty()
width = win.winfo_width()
height = win.winfo_height()
if width <= 1 or height <= 1:
return None
return (left, top, width, height)
except tk.TclError:
return None
def ui_read(agent, fn, timeout: float = 10.0):
"""所有 Tk 调用都要走界面线程,否则报 main thread is not in main loop。"""
try:
return agent.ui_call(fn, timeout=timeout)
except Exception: # noqa: BLE001
return None
# 超时给宽一点:全套测试一起跑时机器很忙(内存紧张),
# Tk 建窗口会明显变慢,4 秒不够用(踩过:单独跑 19/19,
# 全套跑就假失败)。
def wait_geometry(win, agent, timeout: float = 15.0):
"""轮询直到窗口几何可用。"""
if win is None:
return None
deadline = time.time() + timeout
while time.time() < deadline:
rect = ui_read(agent, lambda: _read_geometry(win), timeout=3.0)
if rect:
return rect
time.sleep(0.05)
return None
def _read_geometry(win):
try:
if not win.winfo_exists():
return None
win.update_idletasks()
left = win.winfo_rootx()
top = win.winfo_rooty()
width = win.winfo_width()
height = win.winfo_height()
if width <= 1 or height <= 1:
return None
return (left, top, width, height)
except tk.TclError:
return None
def main() -> int:
SHOTS.mkdir(parents=True, exist_ok=True)
work = ROOT / "_tests" / "_imgmode_data"
shutil.rmtree(work, ignore_errors=True)
image_path = work / "test_image.png"
make_test_image(image_path)
store = Store(work)
agent = HeadlessUI(store)
pump(agent.root, 0.6)
screen_w, screen_h = agent.screen_size() # 不直接碰 Tk
print(f"\n屏幕: {screen_w}x{screen_h}\n")
actions = Actions(agent, store)
# ---------------- 1. 屏幕中间 ----------------
print("一、屏幕中间(默认)")
store.set("image_display", "window")
store.set("image_timeout", 0)
actions.show_image(str(image_path))
pump(agent.root, 0.3)
rect = wait_geometry(actions._image_window, agent)
check("窗口已创建", rect is not None)
if rect:
left, top, width, height = rect
print(f" 客户区: {width}x{height} @ ({left},{top})")
check("不是全屏", width < screen_w - 40 or height < screen_h - 40,
f"{width}x{height} vs 屏幕 {screen_w}x{screen_h}")
check("不超过屏幕 85%",
width <= screen_w * 0.85 and height <= screen_h * 0.9,
f"{width}x{height}")
cx = left + width / 2
cy = top + height / 2
check("水平居中(偏差 < 40px)", abs(cx - screen_w / 2) < 40,
f"中心 x={cx:.0f},应 {screen_w / 2:.0f}")
check("垂直居中(偏差 < 80px)", abs(cy - screen_h / 2) < 80,
f"中心 y={cy:.0f},应 {screen_h / 2:.0f}")
try:
ImageGrab.grab(bbox=(left, top, left + width, top + height)).save(
SHOTS / "1-屏幕中间.png")
except Exception:
pass
actions.close_image()
pump(agent.root, 0.3)
# ---------------- 2. 全屏 ----------------
print("\n二、全屏")
store.set("image_display", "fullscreen")
actions.show_image(str(image_path))
pump(agent.root, 0.5)
rect = wait_geometry(actions._image_window, agent)
check("全屏窗口已创建", rect is not None)
if rect:
left, top, width, height = rect
print(f" 客户区: {width}x{height} @ ({left},{top})")
check("覆盖整个屏幕宽度", width >= screen_w - 4, f"{width} vs {screen_w}")
check("覆盖整个屏幕高度", height >= screen_h - 4, f"{height} vs {screen_h}")
actions.close_image()
pump(agent.root, 0.3)
# ---------------- 3. 自定义 ----------------
print("\n三、自定义大小 (640x400)")
store.set("image_display", "custom")
store.set("image_custom_width", 640)
store.set("image_custom_height", 400)
actions.show_image(str(image_path))
pump(agent.root, 0.3)
rect = wait_geometry(actions._image_window, agent)
check("自定义窗口已创建", rect is not None)
if rect:
left, top, width, height = rect
print(f" 客户区: {width}x{height} @ ({left},{top})")
check("宽度等于 640", width == 640, f"实际 {width}")
# 客户区 = 图片高度 + 底部「关闭」按钮条(约 48px)
check("图片区高度等于 400(其余是按钮条)",
400 <= height <= 400 + 60, f"实际 {height}")
cx = left + width / 2
check("自定义尺寸下仍然居中", abs(cx - screen_w / 2) < 40, f"中心 x={cx:.0f}")
try:
ImageGrab.grab(bbox=(left, top, left + width, top + height)).save(
SHOTS / "3-自定义640x400.png")
except Exception:
pass
actions.close_image()
pump(agent.root, 0.3)
# ---------------- 4. 只填宽度 ----------------
print("\n四、只填宽度(高度自动)")
store.set("image_custom_width", 500)
store.set("image_custom_height", 0)
actions.show_image(str(image_path))
pump(agent.root, 0.3)
rect = wait_geometry(actions._image_window, agent)
check("只填宽度也能显示", rect is not None)
if rect:
print(f" 客户区: {rect[2]}x{rect[3]}")
check("宽度等于 500", rect[2] == 500, f"实际 {rect[2]}")
check("高度自动(大于 0)", rect[3] > 0, f"实际 {rect[3]}")
actions.close_image()
pump(agent.root, 0.3)
# ---------------- 5. 兜底 ----------------
print("\n五、非法 / 空值兜底")
store.set("image_display", "不存在的模式")
actions.show_image(str(image_path))
pump(agent.root, 0.3)
check("非法模式退回默认窗口",
wait_geometry(actions._image_window, agent) is not None)
actions.close_image()
pump(agent.root, 0.3)
store.set("image_display", "custom")
store.set("image_custom_width", 0)
store.set("image_custom_height", 0)
actions.show_image(str(image_path))
pump(agent.root, 0.3)
check("自定义但宽高都是 0 时能兜底",
wait_geometry(actions._image_window, agent) is not None)
actions.close_image()
pump(agent.root, 0.3)
# ---------------- 6. 窗口标题 ----------------
print("\n六、窗口标题可自定义")
store.set("image_display", "window")
store.set("image_window_title", "【来自手表】{name}")
actions.show_image(str(image_path))
pump(agent.root, 0.3)
title = ui_read(agent, lambda: actions._image_window.title()) or ""
check("标题用了模板", title == f"【来自手表】{image_path.name}", repr(title))
actions.close_image()
pump(agent.root, 0.3)
actions.show_image(str(image_path), "自定义标题")
pump(agent.root, 0.3)
title = ui_read(agent, lambda: actions._image_window.title()) or ""
check("单次可覆盖标题", title == "自定义标题", repr(title))
actions.close_image()
pump(agent.root, 0.3)
try:
agent.stop()
except Exception:
pass
print("\n" + "=" * 62)
print(f"通过 {PASSED} 项,失败 {FAILED} 项")
print(f"截图目录: {SHOTS}")
sys.stdout.flush()
# 这个测试会创建多个 Tk 窗口并抓屏,解释器正常收尾时偶发非 0 退出码。
# 直接用 os._exit,让退出码只反映检查结果。
os._exit(1 if FAILED else 0)
if __name__ == "__main__":
main()