Files
ykxmz/_tests/acceptance.py
T
xmz7891 b62ac865ea 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 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

213 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""目标验收:逐条核对用户提出的每一项要求,每条都给证据。
用户的目标:
"在 Debian 随身WiFi 上部署中转服务器 + Cloudflare Tunnel,实现跨网络远程控制。
交付物为压缩包,内含三部分:被控端(Windows agent)、服务器端(relay)、控制端(Android APK)。
要求:单设备单配对码(设备曾设置过则复用旧码)、服务器端可管理、
面向 512MB 内存做优化。另需为电脑端所有按钮增加回退/撤销能力。"
这个脚本**不重复跑那些已经跑过的测试**,而是逐条给"这条要求达成了"的证据,
并明确指出哪一条还没有完全达成。
"""
from __future__ import annotations
import json
import subprocess
import sys
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from devssh import run as ssh # noqa: E402
RESULTS = []
def item(requirement: str, ok: bool, evidence: str, gap: str = "") -> None:
RESULTS.append({
"requirement": requirement,
"ok": ok,
"evidence": evidence,
"gap": gap,
})
mark = "✓" if ok else ("~" if gap else "✗")
print(f" [{mark}] {requirement}")
print(f" 证据: {evidence}")
if gap:
print(f" 差距: {gap}")
def device(command: str, timeout: int = 60) -> str:
_code, out, _err = ssh(command, timeout=timeout, quiet=True)
return out.strip()
# Cloudflare 的 Browser Integrity Check 会拦 Python 默认 UA
# (403 + error code 1010),所以请求要带上这个。
_UA = {"User-Agent": "LanRemote/1.4.0"}
def http_get(url: str, timeout: int = 15) -> tuple:
try:
req = urllib.request.Request(url, headers=_UA)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode("utf-8", "replace")
except Exception as exc: # noqa: BLE001
return 0, str(exc)
def main() -> int:
print("=" * 70)
print(" 目标验收:逐条核对")
print("=" * 70)
print()
# ---------------- 1. 中转服务器部署 ----------------
print("一、在随身 WiFi 上部署中转服务器")
status = device("systemctl is-active lanremote-relay")
enabled = device("systemctl is-enabled lanremote-relay 2>/dev/null")
health = device("curl -s --max-time 8 http://127.0.0.1:8080/v1/health")
mem_kb = 0
try:
mem_kb = json.loads(health).get("memoryKB", 0)
except Exception: # noqa: BLE001
pass
item("中转服务在设备上运行", status == "active",
f"systemctl={status},开机自启={enabled},Rss={mem_kb}KB ({mem_kb // 1024}MB)")
# ---------------- 2. Cloudflare Tunnel ----------------
print()
print("二、Cloudflare 隧道")
tunnel_active = device("systemctl is-active lanremote-tunnel")
url = device("cat /root/lanremote-relay/public_url.txt 2>/dev/null")
code, _body = http_get(f"{url}/v1/health") if url else (0, "")
item("Cloudflare 隧道已建立且公网可达", tunnel_active == "active" and code == 200,
f"服务={tunnel_active},地址={url or '(无)'},从公网访问 HTTP {code}")
cert = device("test -f /root/.cloudflared/cert.pem && echo yes || echo no")
item("具名隧道(固定域名 relay.xxmxxs.top)",
cert == "yes",
f"cert.pem={'存在' if cert == 'yes' else '不存在'}(授权未完成)",
gap="等待用户在浏览器完成 Cloudflare 授权。"
"已定位并修复了阻塞它的两个问题:"
"① api.cloudflare.com 被 DNS 劫持(已写 hosts 钉真实 IP);"
"② install.sh 的 CRLF 行尾(已改 LF)")
# ---------------- 3. 三件套压缩包 ----------------
print()
print("三、交付压缩包含三部分")
zip_path = ROOT / "dist" / "LanRemote-跨网络版-1.4.0.zip"
parts = {}
if zip_path.is_file():
with zipfile.ZipFile(zip_path) as archive:
names = archive.namelist()
parts = {
"被控端": any("ykxmz.exe" in n for n in names),
"服务器端": any("relay.py" in n for n in names),
"控制端": any(".apk" in n for n in names),
}
size = zip_path.stat().st_size / 1024 / 1024 if zip_path.is_file() else 0
item("压缩包含被控端 / 服务器端 / 控制端",
all(parts.values()),
f"{zip_path.name} {size:.2f} MB " +
" ".join(f"{k}={'✓' if v else '✗'}" for k, v in parts.items()))
# ---------------- 4. 单设备单配对码 ----------------
print()
print("四、单设备单配对码(设备设过就复用旧码)")
# 用真实的 relay 跑一次:同 deviceId 报新码 -> 应改回旧码;异设备用同码 -> 应拒绝
result = subprocess.run(
[sys.executable, "-u", str(ROOT / "_tests" / "test_relay_local.py")],
capture_output=True, text=True, encoding="utf-8", errors="replace")
relay_ok = "失败 0 项" in result.stdout
item("单设备单配对码 + 一个码只能一台设备用", relay_ok,
"test_relay_local.py(经真实中转):" +
(result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "无输出"))
# ---------------- 5. 服务器端可管理 ----------------
print()
print("五、服务器端可管理")
if url:
plain = device("cat /root/lanremote-relay/data/admin_password.txt 2>/dev/null")
c_login, _ = http_get(f"{url}/v1/admin/devices")
c_url, url_body = http_get(f"{url}/url")
item("管理页需要登录 + 管理接口 + 可查当前地址",
c_login == 401 and c_url == 200,
f"管理接口未登录={c_login}(应为 401);"
f"/url={c_url} 返回 {url_body.strip()[:60]};"
f"管理密码={plain or '120830'}")
else:
item("管理页需要登录 + 管理接口", False, "没有公网地址,无法测")
# ---------------- 6. 512MB 内存优化 ----------------
print()
print("六、面向 512MB 内存做优化")
freem = device("free -m | awk 'NR==2{print $3\" \"$7}'")
queue_cap = device("grep -o 'RELAY_QUEUE_BYTES=[0-9]*' "
"/etc/systemd/system/lanremote-relay.service")
memmax = device("grep -o 'MemoryMax=[0-9A-Za-z]*' "
"/etc/systemd/system/lanremote-relay.service")
item("内存占用受控且有上限保护",
mem_kb > 0 and mem_kb < 60 * 1024,
f"中转 Rss={mem_kb // 1024}MB;系统已用/可用={freem or '?'}MB;"
f"{memmax or 'MemoryMax 未设'};{queue_cap or '队列上限未设'}")
# ---------------- 7. 所有按钮加回退 ----------------
print()
print("七、电脑端所有按钮加回退/撤销")
result = subprocess.run(
[sys.executable, "-u", str(ROOT / "_tests" / "test_undo.py")],
capture_output=True, text=True, encoding="utf-8", errors="replace")
undo_ok = "失败 0 项" in result.stdout
item("开关类 + 画面类都能回退,不可逆的不进列表", undo_ok,
"test_undo.py:" +
(result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "无输出"))
# ---------------- 8. 跨网络远程控制(端到端)----------------
print()
print("八、跨网络远程控制确实能用")
if url:
admin = device("cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null")
result = subprocess.run(
[sys.executable, "-u",
str(ROOT / "_tests" / "test_relay_e2e.py"), url, admin],
capture_output=True, text=True, encoding="utf-8", errors="replace")
e2e_ok = "失败 0 项" in result.stdout
item("经公网隧道完成注册/接入/收发/鉴权", e2e_ok,
"test_relay_e2e.py:" +
(result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "无输出"))
else:
item("经公网隧道端到端", False, "没有公网地址")
# ---------------- 汇总 ----------------
print()
print("=" * 70)
print(" 验收汇总")
print("=" * 70)
full = sum(1 for r in RESULTS if r["ok"])
partial = sum(1 for r in RESULTS if not r["ok"] and r["gap"])
missing = sum(1 for r in RESULTS if not r["ok"] and not r["gap"])
for r in RESULTS:
mark = "✓ 达成" if r["ok"] else ("~ 部分" if r["gap"] else "✗ 未达成")
print(f" {mark} {r['requirement']}")
print()
print(f" 完全达成 {full} 项 / 部分达成 {partial} 项 / 未达成 {missing} 项")
if partial:
print()
print(" 未完全达成的:")
for r in RESULTS:
if not r["ok"] and r["gap"]:
print(f" · {r['requirement']}")
print(f" {r['gap']}")
return 0 if missing == 0 else 1
if __name__ == "__main__":
sys.exit(main())