2.3.0:手表界面精简 + 播放器 + 鼠标重做

手表端
- 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源)
- 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、
  剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等
- 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘
- 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮
  滚轮点按=中键、上下滑=滚动
- 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、
  点画面播放暂停、上一/下一、播完自动下一首

电脑端
- 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身
- 新增 media_list / media_pull(播放器数据源)
  安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
This commit is contained in:
xmz7891
2026-10-18 11:39:14 +08:00
parent 1d368979aa
commit 2061f9525f
50 changed files with 13812 additions and 584 deletions
+192
View File
@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
"""中继站:管理员改码 + 关机超 TTL 后不能丢(2.3.0)。
要钉死的行为:
① 管理员在后台把配对码从 111111 改成 222222
② 那台电脑关机(或断网)超过 DEVICE_TTL(真实 6 小时)
③ snapshot() 把设备记录和"孤儿"码绑定都清掉
④ 电脑开机,报**自己本地的旧码** 111111
⑤ 中转应该回它 222222,并让它写回本地设置
—— 改动之前这里会收下 111111,管理员的改码白做
另外顺带验证:
* 老格式 relay_state.json(knownDevices 是纯 ID 列表)能平滑升级
* 「删除设备」之后历史被忘干净(开关语义不能被绕)
* 「抹码」之后历史里记的码也清空(否则抹码白做)
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PORT = 8151
BASE = f"http://127.0.0.1:{PORT}"
_UA = {"User-Agent": "LanRemote/2.3.0"}
ADMIN_KEY = "recode-ttl-key"
TTL = 2.0
passed = 0
failed = 0
def check(name: str, ok: bool, detail: str = "") -> bool:
global passed, failed
if ok:
passed += 1
print(" [PASS] " + name)
else:
failed += 1
print(" [FAIL] " + name + ((" —— " + str(detail)) if detail else ""))
return ok
def main() -> int:
work = Path(tempfile.mkdtemp(prefix="recode_ttl_"))
data = work / "d"
data.mkdir(parents=True, exist_ok=True)
env = dict(os.environ)
env.update({
"RELAY_PORT": str(PORT), "RELAY_BIND": "127.0.0.1",
"RELAY_DATA": str(data), "RELAY_DEVICE_TTL": str(TTL),
"RELAY_ADMIN_KEY": ADMIN_KEY, "RELAY_POLL": "2",
"PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1",
})
relay = None
def start() -> subprocess.Popen:
p = subprocess.Popen(
[sys.executable, "-u", str(ROOT / "relay" / "relay.py")], env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(80):
time.sleep(0.4)
try:
health()
return p
except Exception: # noqa: BLE001
continue
return p
def health() -> dict:
r = urllib.request.Request(BASE + "/v1/health", headers=_UA)
with urllib.request.urlopen(r, timeout=5) as resp:
return json.loads(resp.read().decode("utf-8"))
def post(path: str, payload: dict, admin: bool = False) -> dict:
body = json.dumps(payload).encode()
headers = {**_UA, "Content-Type": "application/json"}
if admin:
headers["X-Admin"] = ADMIN_KEY
req = urllib.request.Request(BASE + path, data=body, method="POST",
headers=headers)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
try:
return json.loads(exc.read().decode("utf-8"))
except Exception: # noqa: BLE001
return {}
def reg(dev: str, code: str, name: str = "教室电脑") -> dict:
return post("/v1/agent/register",
{"deviceId": dev, "code": code, "name": name,
"version": "2.3.0"})
try:
relay = start()
dev = "classroom-PC-AAAA1111"
old_code = "111111"
new_code = "222222"
print()
print("═══ 1. 登记 + 管理员改码 ═══")
r = reg(dev, old_code)
check("① 首次登记成功", r.get("ok") is True, r)
check("② 登记返回的码是它上报的", r.get("code") == old_code, r)
res = post("/v1/admin/set_code",
{"deviceId": dev, "code": new_code}, admin=True)
check("③ 管理员改码成功", res.get("ok") is True, res)
r2 = reg(dev, old_code)
check("④ 在线时改码生效(回的是新码)",
r2.get("ok") is True and r2.get("code") == new_code, r2)
print()
print(f"═══ 2. 电脑关机,等 {TTL + 2:.0f} 秒超过 TTL ═══")
time.sleep(TTL + 2)
h = health()
check("⑤ 设备记录被清掉(设计中如此)",
dev not in [d["deviceId"] for d in h["devices"]])
print(f" 清理后 codeCount={h.get('codeCount')}")
print()
print("═══ 3. 电脑开机,报本地旧码 —— 应该被纠正成新码 ═══")
r3 = reg(dev, old_code)
check("⑥ 开机登记成功", r3.get("ok") is True, r3)
check("⑦ **写回的是新码 222222,不是它报的旧码**",
r3.get("code") == new_code,
f"拿到 {r3.get('code')!r},期望 {new_code!r}")
check("⑧ 有 note 说明改码原因", bool(r3.get("note")), r3.get("note"))
h2 = health()
codes_now = [d.get("code") for d in h2["devices"]]
check("⑨ /v1/health 里这台设备显示的也是新码",
new_code in codes_now, codes_now)
print()
print("═══ 4. 重启中转 —— 新码要能持久化 ═══")
relay.terminate()
relay.wait(timeout=15)
relay = start()
r4 = reg(dev, old_code)
check("⑩ 重启后仍然纠正成新码",
r4.get("ok") is True and r4.get("code") == new_code, r4)
print()
print("═══ 5. 「删除设备」要忘干净(否���开关被绕过)═══")
post("/v1/admin/delete", {"deviceId": dev}, admin=True)
post("/v1/admin/allow_new", {"allow": False}, admin=True)
r5 = reg(dev, new_code)
check("⑪ 删除后设备被当成新设备,且开关关着 -> 拒绝",
r5.get("ok") is False and r5.get("blocked") is True, r5)
post("/v1/admin/allow_new", {"allow": True}, admin=True)
print()
print("═══ 6. 「抹码」之后历史里的码也要清空 ═══")
dev2 = "classroom-PC-BBBB2222"
reg(dev2, "333333")
post("/v1/admin/forget", {"code": "333333"}, admin=True)
r6 = reg(dev2, "444444")
check("⑫ 抹码后可以换新码(历史没把旧码塞回来)",
r6.get("ok") is True and r6.get("code") == "444444", r6)
r7 = reg(dev2, "555555")
check("⑬ 但换过之后又锁定单码制(回 444444)",
r7.get("ok") is True and r7.get("code") == "444444", r7)
print()
print("=" * 62)
print(f" 通过 {passed} 项,失败 {failed} 项")
print("=" * 62)
return 0 if failed == 0 else 1
finally:
if relay is not None and relay.poll() is None:
relay.terminate()
try:
relay.wait(timeout=10)
except Exception: # noqa: BLE001
relay.kill()
if __name__ == "__main__":
sys.exit(main())