mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
2.1.7:手表快捷栏 + 鼠标触摸板 + 弹窗可配 + 后台远程更新
用户这次提了一整套升级,这是最后一批。
== 一、手表端顶部快捷栏 ==
用户原话:"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页
自定义,最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"。
("返回桌面"经确认 = **让被控电脑显示桌面**,即 Win+D。)
* 一条常驻的栏,贴在屏幕顶部,切页面时不动
* **4 个格子**:3 个可自定义 + **1 个固定的「显示桌面」(改不了)**
* 7 个可选动作:传文件 / 屏幕镜像 / 鼠标控制 / 音效 / 关机 /
语音播报 / 投图到电脑
* 设置页里点一下换;也能把某一格留空
* 没连上电脑时按钮置灰(不是藏起来 —— "一直在但暂时不能用"
比"时有时无"更好理解)
实现上的几个要点:
* 快捷栏 addView 在 5 个面板**之后** —— FrameLayout 里后加的在上层,
加在前面会被面板(MATCH_PARENT)整个盖住
* 面板加了 topMargin 给栏让位,集中在 applyPanelTopMargin 里改
(每个面板各写一遍很容易漏一个,表现是"就某一页顶部被切掉")
* 第 4 格在代码里**写死** desktop,而且 QUICK_ACTIONS 里**没有**它 ——
这样它既不会出现在可选列表里,也不可能被改成别的
* 存储用 SharedPreferences。读取时**过滤不认识的 id + 补齐到 3 个**:
以后删了某个动作,老配置也不会让栏变空或下标越界
== 二、远程鼠标控制(手表上的触摸板)==
* 拖动 = 移动光标,单击 = 左键,双击 = 右键
* 下面还有专门的 左键 / 右键 / 滚轮上 / 滚轮下 按钮
* 电脑端用 SendInput 注入(见下)
== 三、弹文字窗口的位置和大小可配 ==
用户原话:"弹文字窗口 …… 位置和大小都能自定义"。
* 位置:右下(默认,保持老习惯)/ 右上 / 左上 / 左下 /
屏幕正中间 / 自定义坐标
* 字号:8~32(夹住,免得设成 200 一个通知占满屏幕)
* 宽度:可指定,0 = 按内容自适应
* 三处窗口定位统一到一个 _notice_geometry() —— 以前三处各写死一遍
右下角,改一次要改三个地方,漏一个用户就看得见
== 四、屏幕被观看时的提示条改成默认不显示 ==
用户原话:"以后不需要弹,啥窗户都没有,就可以像 UU 远程一样"。
* 默认**连窗口都不建**
* **但保留成开关**,设置里可以打开
关于这条:那条提示原来是**故意加**的(有人在看屏幕时,电脑前面的人
应该知道)。用户要求去掉,这是他自己的电脑、自己的教室,
属于正当的远程管理需求。所以能力保留、默认关闭 ——
这软件应该始终有一个"让本机用户看得见正在被控制"的选项可查。
== 五、中转后台能看到版本并远程点更新 ==
* 设备表新增「版本」列
* 每行一个「更新」按钮:点一下让那台电脑自己静默升级
* 离线设备的按钮**置灰并说明原因**(命令只会挂在队列里等它上线,
那时用户早忘了自己点过,突然重启很莫名其妙)
* 和手表那边走**同一条命令**(cmd=update, action=start),
不给"后台触发"单独写一套逻辑 —— 少一套就少一处不一致
== 六、手表端可开关「允许新设备连接」 ==
* 关掉后只有配对过的电脑能连,教室里防别人乱连
* 电脑端新增 _do_allow_new_pairing 这个 feature
* 只影响**配对新设备**,不踢现有会话
(否则老师一点"禁止",自己正连着的那台也被踢掉,成了"自杀开关")
== 修掉的几个自己造的坑 ==
1. **管理页 JS 裸换行** —— 最严重的一个。
ADMIN_JS 是 Python 三引号字符串,我在里面写 `\n` 拼多行文案,
结果是**真的换行**插进 JS,字符串字面量跨行 -> 语法非法 ->
**整个 script 块解析失败 -> 管理页所有按钮全死**(改码/删除/踢下线/全选)。
改成 String.fromCharCode(10),不依赖任何转义层。
2. **路由落进了 do_GET** —— 我拿 "没有这个管理接口" 当锚点,
但那个字符串在文件里有**两处**(do_GET 和 _route_post 各一个),
于是路由加到了 GET 块里,管理页的 POST 打不到它。
表现是"点了更新没反应"。
3. **isConnected() 根本不存在** —— 写快捷栏时凭直觉调了个方法名,
编译才发现。顺手包了一层并加上说明。
4. **两个不存在的方法名** —— onCreate 里写了
buildMousePanelIfNeeded / mousePanelHolder,编译报 cannot find symbol。
5. **未调用方法扫描的假阳性** —— GestureDetector 的
onSingleTapConfirmed / onDoubleTap / onScroll 是框架回调,
扫描把"名字只出现 1 次"当成死代码。改成**跳过 @Override 的方法**。
这种假阳性最坏的结果是有人为了让它变绿把回调删了,手势就失灵了。
6. **测试断言太严导致误报** —— "不能有写死的 240",
但 helper 里 `auto = max(240, ...)` 是自适应的兜底,故意的。
7. **屏幕共享提示那个测试断言的是旧行为** —— 没有简单删掉,
改成验两个方向:开关打开时提示条正常(位置/尺寸/出现/消失),
关闭时默认值必须是 False 且代码路径上不建窗口。
== 验证 ==
全量回归(19 关)全部 PASS,零失败
新增关卡:3a10 静默更新 / 3a11 鼠标控制 / 3a12 后台更新+弹窗 /
3a13 手表快捷栏
Java 编译通过(android-34, --release 8)
APK / 安装包 / 服务端包 全部重建,Win7 兼容校验通过
623 个 Python 文件语法通过
This commit is contained in:
@@ -20,5 +20,11 @@
|
||||
"download_dir": "received",
|
||||
"screen_max_width": 480,
|
||||
"screen_quality": 72,
|
||||
"screen_fps": 12
|
||||
"screen_fps": 12,
|
||||
"notice_position": "bottom-right",
|
||||
"notice_x": 0,
|
||||
"notice_y": 0,
|
||||
"notice_width": 0,
|
||||
"notice_font_size": 11,
|
||||
"show_screen_notice": false
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''验「后台点一下让电脑自己更新」这条链路。
|
||||
|
||||
== 链路 ==
|
||||
|
||||
管理页点「更新」
|
||||
-> POST /v1/admin/update_device {deviceId}
|
||||
-> Relay.request_update() 把 {"cmd":"update","action":"start"}
|
||||
塞进该设备的 to_agent 队列
|
||||
-> 被控端下一次长轮询拿到它
|
||||
-> server.py 的 _do_update:查最新版 -> 下载 -> 校验 -> 静默装 -> 重启
|
||||
|
||||
== 为什么要专门测 ==
|
||||
|
||||
这条链路横跨三端(管理页 JS -> 中转 -> 被控端),任何一环写错,
|
||||
表现都是"我点了更新,电脑没反应" —— 而用户在办公室里没法排查。
|
||||
|
||||
而且它**会重启别人的电脑**,所以几个边界必须卡死:
|
||||
|
||||
* 设备不存在 -> 明确说没有,不能静默成功
|
||||
* 设备离线 -> 直接拒。命令挂在队列里等它上线才执行,
|
||||
那时用户早忘了,突然重启很莫名其妙
|
||||
* 重复点 -> 不重复塞(队列会堆积)
|
||||
* 在线 -> 真的塞进队列,而且内容就是那条更新命令
|
||||
|
||||
== 上一版测试自己写错的地方 ==
|
||||
|
||||
我用了个 FakeDevice,`__init__` 里默认 `_on = True`,
|
||||
结果"离线设备"那条用例测的其实是**在线**设备 —— 假绿。
|
||||
现在 FakeDevice 显式接收 online 参数,不再有隐藏默认值。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "relay"))
|
||||
|
||||
FAILS: list = []
|
||||
|
||||
|
||||
def check(name: str, ok: bool, why: str = "") -> None:
|
||||
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name,
|
||||
(" —— " + why) if why else ""))
|
||||
if not ok:
|
||||
FAILS.append(name)
|
||||
|
||||
|
||||
class FakeDevice:
|
||||
"""假的被控端。online 必须显式给,不给就报错 ——
|
||||
第一版给了个 True 的默认值,导致"离线"用例永远测不到离线。"""
|
||||
|
||||
def __init__(self, online: bool):
|
||||
self.to_agent = bytearray()
|
||||
self._online = bool(online)
|
||||
self.lan_ips: list = []
|
||||
self.port = 0
|
||||
|
||||
def online(self) -> bool:
|
||||
return self._online
|
||||
|
||||
def push_to_agent(self, data: bytes) -> None:
|
||||
self.to_agent.extend(data)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 58)
|
||||
print(" 后台「更新」按钮链路")
|
||||
print("=" * 58)
|
||||
print()
|
||||
|
||||
import relay as R
|
||||
|
||||
print("一、Relay.request_update 的边界")
|
||||
r = R.Relay.__new__(R.Relay)
|
||||
r.lock = threading.RLock()
|
||||
r.devices = {}
|
||||
|
||||
res = r.request_update("不存在的设备")
|
||||
check("设备不存在 -> 拒绝", res.get("ok") is False,
|
||||
"拿到 %r" % res)
|
||||
check(" 错误信息说清楚是没这台", "没有这台设备" in str(res.get("error")),
|
||||
"拿到 %r" % res.get("error"))
|
||||
|
||||
check("设备 id 为空也当不存在",
|
||||
r.request_update("").get("ok") is False)
|
||||
check(" None 也不炸", r.request_update(None).get("ok") is False)
|
||||
|
||||
off = FakeDevice(online=False)
|
||||
r.devices["off1"] = off
|
||||
res = r.request_update("off1")
|
||||
check("离线设备 -> 拒绝", res.get("ok") is False, "拿到 %r" % res)
|
||||
check(" 理由提到离线", "离线" in str(res.get("error")),
|
||||
"拿到 %r" % res.get("error"))
|
||||
check(" 离线时**没往队列里塞东西**", len(off.to_agent) == 0,
|
||||
"队列里有 %d 字节 —— 离线时不该堆命令"
|
||||
% len(off.to_agent))
|
||||
|
||||
on = FakeDevice(online=True)
|
||||
r.devices["on1"] = on
|
||||
res = r.request_update("on1")
|
||||
check("在线设备 -> 成功", res.get("ok") is True, "拿到 %r" % res)
|
||||
check(" 队列里真的放了东西", len(on.to_agent) > 0)
|
||||
raw = bytes(on.to_agent).decode("utf-8")
|
||||
check(" 是换行结尾的 JSON(协议要求)", raw.endswith("\n"),
|
||||
"尾部是 %r" % raw[-6:])
|
||||
try:
|
||||
msg = json.loads(raw.strip())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
check(" 内容是合法 JSON", False, str(exc))
|
||||
msg = {}
|
||||
check(" 内容是合法 JSON", bool(msg))
|
||||
check(" cmd == update", msg.get("cmd") == "update", "拿到 %r" % msg)
|
||||
check(" action == start", msg.get("action") == "start",
|
||||
"拿到 %r" % msg)
|
||||
|
||||
n_before = len(on.to_agent)
|
||||
res = r.request_update("on1")
|
||||
check("重复点 -> 不重复塞", len(on.to_agent) == n_before,
|
||||
"队列从 %d 涨到 %d" % (n_before, len(on.to_agent)))
|
||||
check(" 但仍然返回 ok(用户看到的是「已经在排队」)",
|
||||
res.get("ok") is True and "排队" in str(res.get("message")),
|
||||
"拿到 %r" % res)
|
||||
|
||||
print()
|
||||
print("二、HTTP 接口接到了 Relay 方法上")
|
||||
src = (ROOT / "relay" / "relay.py").read_text(encoding="utf-8")
|
||||
check("有 /v1/admin/update_device 路由",
|
||||
'"/v1/admin/update_device"' in src)
|
||||
m = re.search(r'if path == "/v1/admin/update_device":(.{0,300})',
|
||||
src, re.S)
|
||||
body = m.group(1) if m else ""
|
||||
check(" 它调的是 RELAY.request_update", "RELAY.request_update(" in body,
|
||||
"应该走 Relay 的方法,而不是在 HTTP 处理里伸手掏 RELAY.devices")
|
||||
check(" 它把 deviceId 传下去了", 'get("deviceId")' in body)
|
||||
check("有 Relay.request_update 方法", "def request_update(self" in src)
|
||||
|
||||
print()
|
||||
print("三、管理页那边")
|
||||
adm = (ROOT / "relay" / "admin.py").read_text(encoding="utf-8")
|
||||
check("设备行有「更新」按钮", 'data-update="%s"' in adm)
|
||||
check("JS 里有 data-update 的处理",
|
||||
"querySelectorAll('button[data-update]')" in adm)
|
||||
check("JS 会二次确认(别误点就重启别人电脑)",
|
||||
"confirm(" in adm and "更新到最新版" in adm)
|
||||
check("离线设备的按钮带 disabled",
|
||||
'disabled title="设备离线' in adm)
|
||||
check("设备行显示了版本", 'data-l="版本"' in adm)
|
||||
check("表头也有版本列", "<th>版本</th>" in adm)
|
||||
# 空表提示的 colspan 要跟列数对上(加了版本列之后是 9)
|
||||
check("空表 colspan 是 9(跟列数对上)", 'colspan="9"' in adm,
|
||||
"加列之后忘了改 colspan,空表时表格会错位")
|
||||
|
||||
print()
|
||||
print("=" * 58)
|
||||
if FAILS:
|
||||
print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS)))
|
||||
return 1
|
||||
print(" 全部通过")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -23,7 +23,7 @@ def _latest_apk() -> Path:
|
||||
(ROOT / "dist").glob("*watch*.apk"),
|
||||
key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
return candidates[0] if candidates else \
|
||||
ROOT / "dist" / "ykxmz-watch-2.1.6.apk"
|
||||
ROOT / "dist" / "ykxmz-watch-2.1.7.apk"
|
||||
|
||||
|
||||
APK = _latest_apk()
|
||||
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
@@ -128,9 +129,21 @@ class Talk:
|
||||
|
||||
|
||||
def seed(data_dir: Path) -> None:
|
||||
"""准备好数据目录。
|
||||
|
||||
⚠ 这里**故意把 show_screen_notice 设成 True**。
|
||||
|
||||
为什么:这个测试验的是"提示条显示出来的时候位置和尺寸对不对",
|
||||
而 2.1.7 起**默认是不显示的**(用户要求"像 UU 远程一样啥窗口都没有")。
|
||||
开着开关跑,才能继续验到提示条本身还在正常工作 ——
|
||||
否则这个能力哪天被改坏了也没人知道。
|
||||
|
||||
"默认不显示"这件事由下面的第六节单独验(不重启进程,直接读源码/默认值)。
|
||||
"""
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
(data_dir / "settings.json").write_text(
|
||||
json.dumps({"setup_done": True}, ensure_ascii=False), encoding="utf-8")
|
||||
json.dumps({"setup_done": True, "show_screen_notice": True},
|
||||
ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -237,6 +250,34 @@ def main() -> int:
|
||||
talk.call({"cmd": "screen_stop"}, timeout=15)
|
||||
check("再次停止后消失", wait_notice(proc.pid, False))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
print()
|
||||
print("六、默认必须是**不显示**(用户 2.1.7 的要求)")
|
||||
#
|
||||
# 用户原话:"现在不是要会弹一个屏幕已被共享吗?以后不需要弹,
|
||||
# 啥窗户都没有,就可以像 UU 远程一样"
|
||||
#
|
||||
# 这一节**不重启进程**(重启一次要十几秒),
|
||||
# 直接查默认值 —— 被控端读取设置时,没写这一项就用默认值,
|
||||
# 所以默认值是 False 就意味着"新装的机器不会弹提示条"。
|
||||
sys.path.insert(0, str(ROOT / "windows"))
|
||||
from lanremote.config import DEFAULT_SETTINGS
|
||||
check("★ 默认 show_screen_notice == False",
|
||||
DEFAULT_SETTINGS.get("show_screen_notice") is False,
|
||||
"拿到 %r —— 不是 False 的话新装机还会弹提示条"
|
||||
% DEFAULT_SETTINGS.get("show_screen_notice"))
|
||||
|
||||
# 而且设置关着的时候,代码路径上**连窗口都不该建**
|
||||
headless_src = (ROOT / "windows" / "lanremote"
|
||||
/ "headless.py").read_text(encoding="utf-8")
|
||||
m = re.search(r"def set_screen_sharing\(self.*?(?=\n def )",
|
||||
headless_src, re.S)
|
||||
body = m.group(0) if m else ""
|
||||
check("★ 关着时直接 return,不建窗口",
|
||||
'store.get("show_screen_notice")' in body
|
||||
and "return" in body,
|
||||
"开关没接进 set_screen_sharing")
|
||||
|
||||
talk.close()
|
||||
|
||||
finally:
|
||||
|
||||
@@ -95,6 +95,23 @@ def check(name, ok, detail=""):
|
||||
|
||||
|
||||
def defined_methods(text: str) -> list[str]:
|
||||
"""找出主程序里定义的方法名。
|
||||
|
||||
⚠ **带 `@Override` 的跳过**。
|
||||
|
||||
为什么:这些是**父类/接口回调**,由框架调用,源码里当然只出现一次 ——
|
||||
而这个扫描的判据正是"名字只出现 1 次 = 没人调"。
|
||||
|
||||
踩到的实例:手表端的 `GestureDetector.SimpleOnGestureListener`
|
||||
里有 onSingleTapConfirmed / onDoubleTap / onScroll,
|
||||
它们由 Android 的触摸框架回调,源码里各只出现一次。
|
||||
不跳过的话扫描会把它们报成"未知的未调用方法"(假阳性)——
|
||||
而这种假阳性最坏的结果是**有人为了让它变绿,把回调删了**,
|
||||
于是手势就失灵了。
|
||||
|
||||
同类还会遇到的:View.OnClickListener.onClick、
|
||||
DialogInterface.OnClickListener.onClick、Runnable.run 等。
|
||||
"""
|
||||
pattern = re.compile(
|
||||
r"(?:private|public|protected|static|final|\s)+"
|
||||
r"[\w<>\[\],.\s]+\s+(\w+)\s*\([^)]*\)\s*(?:throws [\w.,\s]+)?\{")
|
||||
@@ -104,6 +121,11 @@ def defined_methods(text: str) -> list[str]:
|
||||
if name in ("if", "for", "while", "switch", "catch", "synchronized",
|
||||
"try", "return", "new"):
|
||||
continue
|
||||
# 往上看几行,如果有 @Override 就跳过
|
||||
head = text[max(0, match.start() - 200):match.start()]
|
||||
tail_lines = head.splitlines()[-3:]
|
||||
if any("@Override" in ln for ln in tail_lines):
|
||||
continue
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''验「弹窗位置/大小可配置」和「屏幕提示条默认关」。
|
||||
|
||||
== 用户的要求 ==
|
||||
|
||||
1. 弹文字窗口:位置和大小都能自定义(原来写死右下角)
|
||||
2. 屏幕共享提示条:以后不要弹,像 UU 远程一样啥窗口都没有
|
||||
|
||||
== 验什么 ==
|
||||
|
||||
一、设置项都在,而且默认值符合要求
|
||||
(位置默认右下角 = 不改变老用户习惯;
|
||||
但 show_screen_notice 默认必须是 False)
|
||||
二、_notice_geometry 对 6 种位置算出来的坐标对不对
|
||||
三、三处写死的 geometry 都改成了调 helper(不能再有硬编码的右下角)
|
||||
四、字号和宽度是可配置的
|
||||
五、屏幕提示条真的会读那个开关
|
||||
|
||||
== 为什么第三层重要 ==
|
||||
|
||||
这次改了 3 处 geometry、5 处字号、2 处宽度。
|
||||
如果漏掉一处,表现就是"有一个窗口还在右下角" ——
|
||||
而用户只会看到"你改了没用"。所以必须断言**没有残留的硬编码**。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "windows"))
|
||||
|
||||
FAILS: list = []
|
||||
|
||||
|
||||
def check(name: str, ok: bool, why: str = "") -> None:
|
||||
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name,
|
||||
(" —— " + why) if why else ""))
|
||||
if not ok:
|
||||
FAILS.append(name)
|
||||
|
||||
|
||||
HEADLESS = ROOT / "windows" / "lanremote" / "headless.py"
|
||||
CONFIG = ROOT / "windows" / "lanremote" / "config.py"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 58)
|
||||
print(" 弹窗位置/大小可配置 + 屏幕提示条默认关")
|
||||
print("=" * 58)
|
||||
print()
|
||||
|
||||
src = HEADLESS.read_text(encoding="utf-8")
|
||||
cfg_src = CONFIG.read_text(encoding="utf-8")
|
||||
|
||||
# ---------------- 一、设置项 ----------------
|
||||
print("一、设置项")
|
||||
from lanremote.config import DEFAULT_SETTINGS as D
|
||||
for key in ("notice_position", "notice_x", "notice_y",
|
||||
"notice_width", "notice_font_size", "show_screen_notice"):
|
||||
check(" %s 存在" % key, key in D)
|
||||
|
||||
check("位置默认右下角(不改变老用户习惯)",
|
||||
D.get("notice_position") == "bottom-right",
|
||||
"拿到 %r" % D.get("notice_position"))
|
||||
check("**屏幕提示条默认关**(用户明确要求)",
|
||||
D.get("show_screen_notice") is False,
|
||||
"拿到 %r —— 这个默认必须是 False" % D.get("show_screen_notice"))
|
||||
check("字号默认 11(原来的值)", D.get("notice_font_size") == 11)
|
||||
check("宽度默认 0(自适应)", D.get("notice_width") == 0)
|
||||
|
||||
# ---------------- 二、_notice_geometry 算得对不对 ----------------
|
||||
print()
|
||||
print("二、_notice_geometry 的坐标计算")
|
||||
|
||||
# 直接复刻逻辑来验(不启动 Tk)—— 和源码里的算法必须一致
|
||||
def geom(pos, w, h, screen=(1920, 1080), cx=0, cy=0):
|
||||
screen_w, screen_h = screen
|
||||
pad, pad_bottom = 24, 72
|
||||
if pos == "custom":
|
||||
x = max(0, min(max(0, screen_w - w), cx))
|
||||
y = max(0, min(max(0, screen_h - h), cy))
|
||||
return (x, y)
|
||||
if pos == "center":
|
||||
return (max(0, (screen_w - w) // 2), max(0, (screen_h - h) // 2))
|
||||
left = pos in ("top-left", "bottom-left")
|
||||
top = pos in ("top-left", "top-right")
|
||||
x = pad if left else screen_w - w - pad
|
||||
y = pad if top else screen_h - h - pad_bottom
|
||||
return (max(0, x), max(0, y))
|
||||
|
||||
cases = [
|
||||
("bottom-right", (1920 - 300 - 24, 1080 - 100 - 72), "右下角"),
|
||||
("bottom-left", (24, 1080 - 100 - 72), "左下角"),
|
||||
("top-right", (1920 - 300 - 24, 24), "右上角"),
|
||||
("top-left", (24, 24), "左上角"),
|
||||
("center", ((1920 - 300) // 2, (1080 - 100) // 2), "屏幕正中间"),
|
||||
]
|
||||
for pos, want, label in cases:
|
||||
got = geom(pos, 300, 100)
|
||||
check(" %s(%s)" % (pos, label), got == want,
|
||||
"期望 %s,拿到 %s" % (want, got))
|
||||
|
||||
# custom:正常值
|
||||
check(" custom 用指定坐标", geom("custom", 300, 100, cx=500, cy=400)
|
||||
== (500, 400))
|
||||
# custom:超范围要夹回来
|
||||
check(" custom 超右边界被夹住",
|
||||
geom("custom", 300, 100, cx=99999, cy=0)
|
||||
== (1920 - 300, 0),
|
||||
"拿到 %s" % (geom("custom", 300, 100, cx=99999, cy=0),))
|
||||
check(" custom 负坐标被夹到 0",
|
||||
geom("custom", 300, 100, cx=-50, cy=-50) == (0, 0))
|
||||
|
||||
# ---------------- 三、没有残留的硬编码定位 ----------------
|
||||
print()
|
||||
print("三、源码里不能有残留的硬编码(漏一处用户就看得见)")
|
||||
check("没有写死的右下角 geometry",
|
||||
"screen_h - height - 72" not in src,
|
||||
"还有窗口固定放右下角")
|
||||
check("没有写死的字号 11",
|
||||
'"Microsoft YaHei UI", 11, "bold"' not in src,
|
||||
"还有窗口用写死的字号")
|
||||
# ⚠ 这里要排除 helper 自己那处 —— _notice_width 里
|
||||
# uto = max(240, win.winfo_reqwidth()) 是**自适应的兜底值**,
|
||||
# 属于故意保留的。第一版断言太粗,把这处也算成写死了。
|
||||
hard = [ln for ln in src.splitlines()
|
||||
if "max(240, win.winfo_reqwidth())" in ln
|
||||
and "auto =" not in ln]
|
||||
check("没有别处写死的宽度 240", not hard,
|
||||
"还有窗口用写死的宽度:%s" % hard)
|
||||
|
||||
n_geom = src.count("self._notice_geometry(width, height)")
|
||||
check("三处定位都改调 helper(实际 %d 处)" % n_geom, n_geom == 3,
|
||||
"应该是 3 处(通知×2 + 屏幕提示×1)")
|
||||
check("有 _notice_geometry 方法", "def _notice_geometry(self" in src)
|
||||
check("有 _notice_font 方法", "def _notice_font(self" in src)
|
||||
check("有 _notice_width 方法", "def _notice_width(self" in src)
|
||||
|
||||
# ---------------- 四、屏幕提示条读开关 ----------------
|
||||
print()
|
||||
print("四、屏幕提示条")
|
||||
m = re.search(r"def set_screen_sharing\(self.*?(?=\n def )", src, re.S)
|
||||
body = m.group(0) if m else ""
|
||||
check("找到了 set_screen_sharing", bool(body))
|
||||
check("它读了 show_screen_notice",
|
||||
'store.get("show_screen_notice")' in body,
|
||||
"没读开关的话提示条还是会强制弹出来")
|
||||
# 读开关的判断必须在 _show_notice 之前
|
||||
i_check = body.find('store.get("show_screen_notice")')
|
||||
i_show = body.find("_show_notice(")
|
||||
check("判断在弹窗之前(关着就不会弹)",
|
||||
0 <= i_check < i_show,
|
||||
"顺序反了的话会先弹再判断")
|
||||
|
||||
# ---------------- 五、设置界面里能不能改 ----------------
|
||||
print()
|
||||
print("五、设置界面")
|
||||
ui_src = (ROOT / "windows" / "lanremote" / "ui.py").read_text(
|
||||
encoding="utf-8")
|
||||
has_any = any(k in ui_src for k in
|
||||
("notice_position", "notice_width", "notice_font_size",
|
||||
"show_screen_notice"))
|
||||
check("设置界面里有这些项", has_any,
|
||||
"配置项加了但界面没入口的话,用户根本改不了")
|
||||
|
||||
print()
|
||||
print("=" * 58)
|
||||
if FAILS:
|
||||
print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS)))
|
||||
return 1
|
||||
print(" 全部通过")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
# 被控端 -> 一个 zip
|
||||
# 服务器端 -> 一个 zip
|
||||
# 控制端 -> 直接给 APK(不打 zip)
|
||||
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.6.zip"
|
||||
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.6.zip"
|
||||
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.6.apk"
|
||||
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.7.zip"
|
||||
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.7.zip"
|
||||
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.7.apk"
|
||||
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
|
||||
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
|
||||
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
|
||||
@@ -89,7 +89,7 @@ def main() -> int:
|
||||
for label, path in (("被控端", agent_dir / "README-agent.txt"),
|
||||
("服务器端", relay_dir / "README-server.txt"),
|
||||
("控制端(旁边那份)",
|
||||
ROOT / "dist" / "ykxmz-watch-2.1.6-说明.txt")):
|
||||
ROOT / "dist" / "ykxmz-watch-2.1.7-说明.txt")):
|
||||
raw = path.read_bytes() if path.is_file() else b""
|
||||
check(f"{label}说明有 UTF-8 BOM",
|
||||
raw[:3] == b"\xef\xbb\xbf", raw[:3])
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''验「手表快捷栏」和「允许新设备连接」。
|
||||
|
||||
== 验什么 ==
|
||||
|
||||
一、手表端源码
|
||||
* 快捷栏建出来了,4 个格子(3 自定义 + 1 固定显示桌面)
|
||||
* 固定那个**不给改**(用户明确要求"其中有一个按钮不能改")
|
||||
* 挂进了 onCreate(光有方法不 addView 是白搭 —— 项目里踩过)
|
||||
* 加进了 showOnly 的数组(漏了的话它会永远压在最上面)
|
||||
* 7 个动作都在
|
||||
* 设置页里有入口(没入口用户改不了)
|
||||
* 鼠标触摸板在
|
||||
|
||||
二、电脑端 allow_new_pairing 这个 feature
|
||||
* 能查、能关、能开
|
||||
* 存储里真的变了
|
||||
* 只影响新配对,不踢现有会话(注释里写了理由)
|
||||
|
||||
== 为什么专门验"固定那个不给改" ==
|
||||
|
||||
用户原话:"最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"。
|
||||
|
||||
如果哪天有人"顺手"把它也做成可配置的,用户点了半天发现
|
||||
"显示桌面"没了 —— 而这种回归不会报错,只会让人困惑。
|
||||
所以这里断言:第 4 格永远是 desktop,且 QUICK_ACTIONS 里**没有**它。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "windows"))
|
||||
|
||||
FAILS: list = []
|
||||
|
||||
|
||||
def check(name: str, ok: bool, why: str = "") -> None:
|
||||
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name,
|
||||
(" —— " + why) if why else ""))
|
||||
if not ok:
|
||||
FAILS.append(name)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 58)
|
||||
print(" 手表快捷栏 + 允许新设备连接")
|
||||
print("=" * 58)
|
||||
print()
|
||||
|
||||
main_java = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
|
||||
/ "MainActivity.java").read_text(encoding="utf-8")
|
||||
|
||||
# ---------------- 一、快捷栏 ----------------
|
||||
print("一、快捷栏(手表端)")
|
||||
for token, label in [
|
||||
("private void buildQuickBar()", "有 buildQuickBar"),
|
||||
("private void refreshQuickBar()", "有 refreshQuickBar"),
|
||||
("private void onQuickClick(int slot)", "有点击分发 onQuickClick"),
|
||||
("private void runQuickAction(String id)", "有动作执行 runQuickAction"),
|
||||
("private void loadQuickSlots()", "有读取设置"),
|
||||
("private void saveQuickSlots()", "有保存设置"),
|
||||
]:
|
||||
check(label, token in main_java)
|
||||
|
||||
# 4 个格子
|
||||
check("格子数 = 3 自定义 + 1 固定",
|
||||
"QUICK_CUSTOM_SLOTS + 1" in main_java
|
||||
and "QUICK_CUSTOM_SLOTS = 3" in main_java)
|
||||
|
||||
# 固定那个
|
||||
m = re.search(r'String id = \(slot < QUICK_CUSTOM_SLOTS\) \? quickSlots\[slot\]'
|
||||
r' : "desktop"', main_java)
|
||||
check("★ 第 4 格固定是 desktop(不给改)", m is not None,
|
||||
"第 4 格不是写死的 desktop —— 用户要求它不能改")
|
||||
check("★ QUICK_ACTIONS 里没有 desktop(否则它会出现在可选列表里)",
|
||||
not re.search(r'\{"desktop"', main_java),
|
||||
"desktop 出现在可选动作里了,用户就能把它改掉")
|
||||
check("desktop 走的是 show_desktop 命令",
|
||||
'"cmd", "show_desktop"' in main_java,
|
||||
"固定按钮应该让电脑 Win+D")
|
||||
|
||||
# 7 个动作
|
||||
print()
|
||||
print(" 7 个可选动作:")
|
||||
for aid, label in [("file", "传文件"), ("mirror", "屏幕镜像"),
|
||||
("mouse", "鼠标控制"), ("sfx", "音效"),
|
||||
("power", "关机"), ("tts", "语音播报"),
|
||||
("image", "投图到电脑")]:
|
||||
ok = ('{"%s", "%s"}' % (aid, label)) in main_java
|
||||
check(" %s(%s)" % (aid, label), ok)
|
||||
|
||||
# 挂接
|
||||
print()
|
||||
print("二、挂接(漏了就白写)")
|
||||
oncreate = main_java[main_java.find("protected void onCreate"):
|
||||
main_java.find("protected void onCreate") + 6000]
|
||||
check("★ onCreate 里调了 buildQuickBar()",
|
||||
"buildQuickBar();" in oncreate,
|
||||
"没挂上去的话栏根本不显示")
|
||||
check("★ onCreate 里调了 loadQuickSlots()",
|
||||
"loadQuickSlots();" in oncreate)
|
||||
check("★ 快捷栏**最后** addView(保证在最上层)",
|
||||
oncreate.find("root.addView(quickBar") > oncreate.find("root.addView(mousePanel"),
|
||||
"加在面板之前会被面板整个盖住")
|
||||
check("★ showOnly 的数组里有 mousePanel",
|
||||
"settingsPanel, mousePanel};" in main_java,
|
||||
"漏了的话鼠标面板永远不隐藏")
|
||||
check("有 applyPanelTopMargin(面板给栏让位)",
|
||||
"private void applyPanelTopMargin" in main_java)
|
||||
|
||||
# 设置页入口
|
||||
print()
|
||||
print("三、设置页入口(没入口用户改不了)")
|
||||
check("有 fillSettingsExtra", "private void fillSettingsExtra()" in main_java)
|
||||
check("有 pickQuickSlot(选动作)",
|
||||
"private void pickQuickSlot(final int slot)" in main_java)
|
||||
check("设置页里加了「快捷栏」小节",
|
||||
'sectionTitle("快捷栏")' in main_java)
|
||||
check("设置页里加了「允许新设备连接」小节",
|
||||
'sectionTitle("允许新设备连接")' in main_java)
|
||||
check("界面上说明了第 4 格改不了",
|
||||
"固定的、改不了" in main_java,
|
||||
"不说明的话用户会一直找怎么改第四个")
|
||||
|
||||
# 鼠标触摸板
|
||||
print()
|
||||
print("四、鼠标触摸板")
|
||||
check("有 buildMousePanel", "private void buildMousePanel()" in main_java)
|
||||
check("用 GestureDetector(单击/双击/拖动)",
|
||||
"GestureDetector" in main_java
|
||||
and "onSingleTapConfirmed" in main_java
|
||||
and "onDoubleTap" in main_java
|
||||
and "onScroll" in main_java)
|
||||
check("单击 -> 左键", 'sendMouse("click", "left", 0)' in main_java)
|
||||
check("双击 -> 右键", 'sendMouse("click", "right", 0)' in main_java)
|
||||
check("滚轮上/下", 'sendMouse("scroll", "", 3)' in main_java
|
||||
and 'sendMouse("scroll", "", -3)' in main_java)
|
||||
check("拖动 -> moveby", '"moveby"' in main_java)
|
||||
check("isConnected() 存在(我第一版漏了,编译才发现)",
|
||||
"private boolean isConnected()" in main_java)
|
||||
|
||||
# ---------------- 五、电脑端 allow_new_pairing ----------------
|
||||
print()
|
||||
print("五、电脑端 allow_new_pairing feature")
|
||||
from lanremote.config import Store
|
||||
from lanremote.features import Features
|
||||
|
||||
class FakeActions:
|
||||
"""Features 要的是 actions 对象,不是 store(第一版就是这里传错了)。"""
|
||||
def __init__(self, store):
|
||||
self.store = store
|
||||
self.ui = None
|
||||
|
||||
d = Path(tempfile.mkdtemp())
|
||||
st = Store(d)
|
||||
feats = Features(FakeActions(st))
|
||||
|
||||
check("能查到当前状态",
|
||||
"允许" in feats.run("allow_new_pairing", {}))
|
||||
out = feats.run("allow_new_pairing", {"allow": False})
|
||||
check("能关掉", "禁止" in out, "拿到 %r" % out)
|
||||
check(" ★ 存储里真的变了",
|
||||
st.get("allow_new_pairing") is False,
|
||||
"拿到 %r" % st.get("allow_new_pairing"))
|
||||
check(" 关掉之后再查也是关",
|
||||
"不允许" in feats.run("allow_new_pairing", {}))
|
||||
out = feats.run("allow_new_pairing", {"allow": True})
|
||||
check("能打开", "已允许" in out, "拿到 %r" % out)
|
||||
check(" 存储里也变了", st.get("allow_new_pairing") is True)
|
||||
|
||||
# 不存在名字要报错(别静默成功)
|
||||
try:
|
||||
feats.run("allow_new_pairing_typo", {})
|
||||
check("不认识的名字会报错", False, "它竟然没报错")
|
||||
except ValueError:
|
||||
check("不认识的名字会报错", True)
|
||||
|
||||
print()
|
||||
print("=" * 58)
|
||||
if FAILS:
|
||||
print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS)))
|
||||
return 1
|
||||
print(" 全部通过")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+39
-10
@@ -430,7 +430,7 @@ $stageOut = & python (Join-Path $root "_tests\test_lan_path.py") 2>&1
|
||||
Record "局域网直连路径" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a2
|
||||
Banner "3a2/7 局域网优化(网段掩码 / 发现重发 / 回切)"
|
||||
Banner "3a2/9 局域网优化(网段掩码 / 发现重发 / 回切)"
|
||||
# 盯四件事:
|
||||
# 1. 网段判断按网卡真实掩码,不再写死 /24(/16 网络下能直连却去走中转)
|
||||
# 2. 广播发现重发 + 多网段(原来只发一次、只探一个网段)
|
||||
@@ -441,7 +441,7 @@ $stageOut = & python (Join-Path $root "_tests\test_lan_optimize.py") 2>&1
|
||||
Record "局域网优化" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a3
|
||||
Banner "3a3/7 手表局域网切换逻辑(源码断言)"
|
||||
Banner "3a3/9 手表局域网切换逻辑(源码断言)"
|
||||
$stageOut = & python (Join-Path $root "_tests\test_watch_lan_switch.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "手表局域网切换" ($LASTEXITCODE -eq 0)
|
||||
@@ -459,7 +459,7 @@ $stageOut = & python (Join-Path $root "_tests\test_rescue_section.py") 2>&1
|
||||
Record "抢救报告分区" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a4
|
||||
Banner "3a4/7 编码与换行(.java/.py 必须 UTF-8 无 BOM + LF)"
|
||||
Banner "3a4/9 编码与换行(.java/.py 必须 UTF-8 无 BOM + LF)"
|
||||
# 踩过的坑:edit 工具会把行尾写成 CRLF,而仓库约定是 LF。
|
||||
# 这个检查专门盯住,免得改动悄悄把整个文件的行尾换掉。
|
||||
$stageOut = & python (Join-Path $root "_tests\check_encoding.py") 2>&1
|
||||
@@ -467,7 +467,7 @@ $stageOut = & python (Join-Path $root "_tests\check_encoding.py") 2>&1
|
||||
Record "编码与换行" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a7
|
||||
Banner "3a7/7 Python 语法(必须用 compile,不能用 ast.parse)"
|
||||
Banner "3a7/9 Python 语法(必须用 compile,不能用 ast.parse)"
|
||||
# 这一轮被同一件事咬了三次:补丁脚本把 import 插到 from __future__
|
||||
# 前面,整个文件就废了。而更糟的是 ast.parse 查不出来(它只解析,
|
||||
# 不做编译期检查)—— 前几次「全仓语法检查 0 问题」都是假绿。
|
||||
@@ -476,13 +476,13 @@ $stageOut = & python (Join-Path $root "_tests\check_python_syntax.py") 2>&1
|
||||
Record "Python 语法检查" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a8
|
||||
Banner "3a8/7 版本号一致性(防漏改)"
|
||||
Banner "3a8/9 版本号一致性(防漏改)"
|
||||
$stageOut = & python (Join-Path $root "_tests\check_version_consistency.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "版本号一致性" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a9
|
||||
Banner "3a9/7 Windows 7 兼容(学校电脑是 Win7)"
|
||||
Banner "3a9/9 Windows 7 兼容(学校电脑是 Win7)"
|
||||
# 用户报:学校电脑是 Windows 7,装完报"dll 缺失"。
|
||||
#
|
||||
# 根因不是漏打包,是 **Python 版本越界**:
|
||||
@@ -503,7 +503,7 @@ $stageOut = & python (Join-Path $root "_build\check_win7_compat.py") 2>&1
|
||||
Record "Windows 7 兼容" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a10
|
||||
Banner "3a10/7 静默自动更新链路"
|
||||
Banner "3a10/9 静默自动更新链路"
|
||||
# 用户要求:"手表端点击一个更新按钮,电脑端就可以自动进行静默更新,
|
||||
# 其他什么设置都不变 …… 不弹任何窗口出来"
|
||||
# 这条链是:中转 /v1/update/latest -> 被控端 updater.py 下载校验
|
||||
@@ -516,7 +516,7 @@ $stageOut = & python (Join-Path $root "_tests\test_update_flow.py") 2>&1
|
||||
Record "静默自动更新" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a11
|
||||
Banner "3a11/7 远程鼠标 + 一键显示桌面"
|
||||
Banner "3a11/9 远程鼠标 + 一键显示桌面"
|
||||
# 鼠标注入是最"有力量"的功能,参数校验写错的话表现是"手表点了没反应",
|
||||
# 教室里没法排查。这里盯坐标归一化(除以 宽-1 那条规则)、
|
||||
# 滚轮夹取、Win+D 的事件顺序、以及非法参数必须被拒。
|
||||
@@ -525,8 +525,37 @@ $stageOut = & python (Join-Path $root "_tests\test_mouse_control.py") 2>&1
|
||||
Record "远程鼠标控制" ($LASTEXITCODE -eq 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 3a12
|
||||
Banner "3a12/9 后台更新按钮 + 弹窗位置/大小"
|
||||
# 用户要求:后台能点一下让电脑自己更新;弹文字窗口位置和大小可自定义。
|
||||
# 后台那条链路横跨三端(JS -> 中转 -> 被控端),错了的表现是
|
||||
# "我点了更新,电脑没反应",所以几个边界必须卡死:
|
||||
# 设备不存在 / 离线 / 重复点 / 在线。
|
||||
$stageOut = & python (Join-Path $root "_tests\test_admin_update.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "后台更新按钮" ($LASTEXITCODE -eq 0)
|
||||
|
||||
$stageOut = & python (Join-Path $root "_tests\test_notice_options.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "弹窗位置和大小" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a13
|
||||
Banner "3a13/9 手表快捷栏 + 允许新设备连接"
|
||||
# 用户要求:"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页
|
||||
# 自定义,最多可以加 4 个,其中有一个按钮不能改,
|
||||
# 功能就是一键返回桌面"。
|
||||
#
|
||||
# 四个地方最容易漏,漏了都不会报错、只会让人困惑:
|
||||
# 1. buildQuickBar 没挂进 onCreate -> 栏根本不显示
|
||||
# 2. 没加进 showOnly 的数组 -> 面板永远压在上面
|
||||
# 3. 第 4 格做成了可配置 -> 用户找半天"显示桌面"没了
|
||||
# 4. 设置页没有入口 -> 说是能自定义其实改不了
|
||||
$stageOut = & python (Join-Path $root "_tests\test_quickbar.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "手表快捷栏" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a5
|
||||
Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)"
|
||||
Banner "3a5/9 局域网/中转对比基准(防测试自己骗人)"
|
||||
# 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」——
|
||||
# 实际是基准自己有两个 bug:两条路用了同一个配对码、
|
||||
# 以及 attach 用了旧码(打到已经死掉的设备上)。
|
||||
@@ -537,7 +566,7 @@ $stageOut = & python (Join-Path $root "_tests\test_lan_bench_guard.py") 2>&1
|
||||
Record "局域网对比基准" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a6
|
||||
Banner "3a6/7 管理页 JS 语法(防按钮全死)"
|
||||
Banner "3a6/9 管理页 JS 语法(防按钮全死)"
|
||||
# 踩过两次的坑:admin.py 的 ADMIN_JS 是普通三引号字符串,
|
||||
# 里面写 \n 会被 Python 变成真实换行;而 JS 的行注释 // 不能跨行,
|
||||
# 一断行整段 <script> 语法错误 -> 页面上所有按钮失效。
|
||||
|
||||
Reference in New Issue
Block a user