Files
ykxmz/_tests/acceptance.py
T
xmz7891 566d9e384f 阶段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
2026-10-06 23:53:19 +08:00

213 lines
9.1 KiB
Python
Raw Permalink 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 '@REVAedu120830'}")
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())