mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-19 11:50: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:
+51
-2
@@ -268,7 +268,7 @@ def _device_rows(devices: list) -> str:
|
||||
if not devices:
|
||||
# 图标用内联 SVG 而不是 emoji ——
|
||||
# emoji 依赖系统字体,无头浏览器/精简系统里会渲染成方框。
|
||||
return ('<tr><td colspan="8" class="empty">'
|
||||
return ('<tr><td colspan="9" class="empty">'
|
||||
'<svg width="34" height="34" viewBox="0 0 24 24" fill="none" '
|
||||
'stroke="currentColor" stroke-width="1.5" '
|
||||
'stroke-linecap="round" stroke-linejoin="round" '
|
||||
@@ -296,6 +296,9 @@ def _device_rows(devices: list) -> str:
|
||||
'<span class="dot %s"></span>%s</div>'
|
||||
'<div class="devid">%s</div></td>'
|
||||
'<td data-l="配对码"><code>%s</code></td>'
|
||||
# 版本列:用户要求"手表端连接那个电脑端也可以查看电脑端
|
||||
# 目前所处的版本",后台这边同理 —— 一眼看出哪台该升级。
|
||||
'<td data-l="版本"><span class="mono">%s</span></td>'
|
||||
'<td data-l="状态"><span class="pill %s">%s</span></td>'
|
||||
'<td data-l="空闲"><span class="mono">%ss</span></td>'
|
||||
'<td data-l="待发 表/机"><span class="mono">%d / %d</span></td>'
|
||||
@@ -306,17 +309,24 @@ def _device_rows(devices: list) -> str:
|
||||
# 这里要是也带 data-device 就会被当成删除按钮。
|
||||
'<button class="ghost sm" data-recode="%s" '
|
||||
'data-name="%s" data-code="%s">改码</button>'
|
||||
# 「更新」只对在线设备有意义(要给它发命令)。
|
||||
# 离线的话按钮置灰并说明原因,而不是点了没反应。
|
||||
'<button class="ghost sm" data-update="%s" '
|
||||
'data-name="%s" data-code="%s"%s>更新</button>'
|
||||
'<button class="danger sm" '
|
||||
'data-device="%s" data-name="%s" data-code="%s">删除</button></td>'
|
||||
'</tr>' % (
|
||||
did, name,
|
||||
"on" if online else "off", name, did,
|
||||
code,
|
||||
_esc(device.get("version") or "—"),
|
||||
"on" if online else "off", "在线" if online else "离线",
|
||||
device["idleSeconds"],
|
||||
device["queuedToClient"], device["queuedToAgent"],
|
||||
_kb(device["bytesIn"]), _kb(device["bytesOut"]),
|
||||
did, name, code,
|
||||
did, name, code,
|
||||
"" if online else ' disabled title="设备离线,连不上"',
|
||||
did, name, code)
|
||||
)
|
||||
return "".join(out)
|
||||
@@ -407,6 +417,45 @@ document.querySelectorAll('button[data-recode]').forEach(b => {
|
||||
});
|
||||
|
||||
// ---------------- 单台删除 ----------------
|
||||
// ---- 「更新」按钮:让那台电脑自己静默升级 ----
|
||||
//
|
||||
// 和手表那边走的是同一条命令,只是入口不同。
|
||||
// 离线设备的按钮在 HTML 里就带 disabled 了,所以这里不用再判。
|
||||
document.querySelectorAll('button[data-update]').forEach((btn) => {
|
||||
btn.onclick = async () => {
|
||||
const name = btn.dataset.name || '这台电脑';
|
||||
// ⚠ 这里**不能用反斜杠 n** 拼多行文案。
|
||||
// ADMIN_JS 是 Python 三引号字符串,源码里写反斜杠 n 会变成
|
||||
// **真的换行**插进 JS,于是字符串字面量跨行 —— JS 语法非法,
|
||||
// 整个 script 块解析失败,管理页所有按钮全死。
|
||||
// (回归里的「管理页 JS 合法性」就是专门抓这个的。)
|
||||
//
|
||||
// 用 String.fromCharCode(10) 完全不依赖转义层,
|
||||
// 以后谁改这段都不会再踩。
|
||||
const NL = String.fromCharCode(10);
|
||||
const msg = ['让「' + name + '」现在更新到最新版?', '',
|
||||
'它会自己下载、静默安装并重启,全程不弹窗口,',
|
||||
'中转地址、配对码和各项设置都不变。'].join(NL);
|
||||
if (!confirm(msg)) {
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
const old = btn.textContent;
|
||||
btn.textContent = '下发中…';
|
||||
try {
|
||||
const r = await post('/v1/admin/update_device',
|
||||
{ deviceId: btn.dataset.update });
|
||||
toast((r && r.message) || '已下发更新命令', 'ok');
|
||||
// 更新要跑一阵子,不立刻刷新列表(刷新了也看不出变化)
|
||||
} catch (e) {
|
||||
toast('下发失败:' + (e && e.message ? e.message : e), 'err');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = old;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
document.querySelectorAll('button.danger[data-device]').forEach(b => {
|
||||
b.onclick = async () => {
|
||||
const name = b.dataset.name, code = b.dataset.code;
|
||||
@@ -729,7 +778,7 @@ def render_admin(snap: dict, public: str, admin_path: str, codes: int,
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:38px"><input type="checkbox" id="pickAll" title="全选"></th>
|
||||
<th>设备</th><th>配对码</th><th>状态</th>
|
||||
<th>设备</th><th>配对码</th><th>版本</th><th>状态</th>
|
||||
<th>空闲</th><th>待发 表/机</th><th>流量 入/出</th><th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
|
||||
+53
-1
@@ -60,7 +60,7 @@ from collections import deque
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
VERSION = "2.1.6"
|
||||
VERSION = "2.1.7"
|
||||
|
||||
# ---------------- 参数(都能用环境变量覆盖)----------------
|
||||
|
||||
@@ -833,6 +833,40 @@ class Relay:
|
||||
return {"ok": True, "code": new_code,
|
||||
"oldCode": old_code, "kicked": kicked}
|
||||
|
||||
def request_update(self, device_id: str) -> dict:
|
||||
"""让某台被控端**现在自己静默升级**。
|
||||
|
||||
做两件事:把 {cmd:update, action:start} 塞进它的下发队列,
|
||||
于是它下一次长轮询就会拿到并开始更新。
|
||||
|
||||
和手表那边走的是**同一条命令**,只是入口不同 ——
|
||||
不给后台触发单独写一套逻辑,少一套就少一处不一致。
|
||||
|
||||
几个判断:
|
||||
* 设备不存在 -> 明确说没有(而不是静默成功)
|
||||
* 设备离线 -> 直接拒。命令只会挂在队列里等它上线,
|
||||
而那时用户早忘了自己点过,突然重启很莫名其妙。
|
||||
* 队列里已经有更新命令 -> 不重复塞(重复点会堆积)
|
||||
"""
|
||||
device_id = str(device_id or "").strip()
|
||||
payload = b'{"cmd":"update","action":"start"}' + b"\n"
|
||||
with self.lock:
|
||||
device = self.devices.get(device_id)
|
||||
if device is None:
|
||||
return {"ok": False, "error": "没有这台设备(可能已下线)"}
|
||||
if not device.online():
|
||||
return {"ok": False, "error": "设备离线,连不上它"}
|
||||
if payload in bytes(device.to_agent):
|
||||
return {"ok": True,
|
||||
"message": "这台电脑的更新已经在排队了"}
|
||||
try:
|
||||
device.push_to_agent(payload)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"ok": False, "error": "下发失败:%s" % exc}
|
||||
return {"ok": True,
|
||||
"message": "已让这台电脑开始更新:它会自己下载、"
|
||||
"静默安装并重启,设置和配对码都不变"}
|
||||
|
||||
def device_by_token(self, token: str) -> Device | None:
|
||||
if not token:
|
||||
return None
|
||||
@@ -1838,6 +1872,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if path == "/v1/admin/devices":
|
||||
self._json(200, RELAY.snapshot())
|
||||
return
|
||||
|
||||
self._json(404, {"ok": False, "error": "没有这个管理接口"})
|
||||
return
|
||||
|
||||
@@ -2098,6 +2133,23 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._json(200 if result.get("ok") else 400, result)
|
||||
return
|
||||
|
||||
# ---- 让某台电脑自己静默升级 ----
|
||||
#
|
||||
# ⚠ 这个必须放在 **_route_post** 里(不是 do_GET)——
|
||||
# 管理页的 post() 助手发的是 POST。第一版我按
|
||||
# "没有这个管理接口" 这个字符串找锚点,结果匹配到了
|
||||
# do_GET 里那个同名的 404 兜底,路由落错块里了:
|
||||
# curl 直接 404,管理页点了也没反应。
|
||||
#
|
||||
# 教训:这个文件里有**两处**一模一样的 404 兜底字符串,
|
||||
# 拿它当锚点是不安全的。改成按后面的 set_code 定位。
|
||||
if path == "/v1/admin/update_device":
|
||||
body = self._json_body()
|
||||
result = RELAY.request_update(
|
||||
str(body.get("deviceId") or ""))
|
||||
self._json(200 if result.get("ok") else 400, result)
|
||||
return
|
||||
|
||||
self._json(404, {"ok": False, "error": "没有这个管理接口"})
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user