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 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
"""中转管理端加"批量删除"(重写版)。
上一版的坑:
1. JS 里的换行我写成 \\n,但 relay.py 里是 f-string,
\\n 会被还原成真换行 -> JS 字符串跨行 -> 语法错误。
正确写法是补丁里用 \\\\n,这样 relay.py 源码里是 \\n,
f-string 渲染后 JS 拿到的是 反斜杠+n 两个字符。
2. rows.append 的锚点带太多上下文,容易被空格差异搞挂。
换成最小锚点。
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py")
# ---------------------------------------------------------------- 后端
BACKEND_ANCHOR = ''' if path in ("/v1/admin/delete", "/v1/admin/delete_all"):'''
BACKEND_NEW = ''' if path == "/v1/admin/delete_many":
# 批量删除:勾几台删几台。
# 和"删除全部"的区别:不动没勾的,也不清孤儿绑定。
body = self._json_body()
wanted = body.get("deviceIds") or []
if not isinstance(wanted, list):
self._json(400, {"ok": False, "error": "deviceIds 要是数组"})
return
wanted = [str(item).strip() for item in wanted
if str(item).strip()]
if not wanted:
self._json(400, {"ok": False, "error": "没有勾选任何设备"})
return
results = [RELAY.delete_device(device_id=item)
for item in wanted]
done = [r for r in results if r.get("ok")]
failed = [r for r in results if not r.get("ok")]
log(f"管理操作:批量删除 {len(done)}/{len(wanted)} 台设备")
self._json(200, {
"ok": True,
"requested": len(wanted),
"deleted": len(done),
"failed": len(failed),
"devices": done,
"errors": failed[:10],
})
return
''' + BACKEND_ANCHOR
# ---------------------------------------------------------------- 行里加复选框(最小锚点)
ROW_ANCHOR = ''' rows.append(
"<tr>"
'''
ROW_NEW = ''' rows.append(
"<tr>"
# 批量删除用的复选框。value 放 deviceId。
"<td class=pickcell>"
f'<input type=checkbox class=pick value="{_esc(device["deviceId"])}">'
"</td>"
'''
# ---------------------------------------------------------------- 表头全选
HEADER_ANCHOR = "<table><tr><th>设备</th>"
HEADER_NEW = ("<table><tr><th class=pickcell>"
'<input type=checkbox id=pickAll title="全选"></th>'
"<th>设备</th>")
# ---------------------------------------------------------------- 顶部按钮
BAR_ANCHOR = " <button class=del id=delAll>删除全部设备</button>"
BAR_NEW = (" <button class=del id=delPicked>删除选中</button>\n"
" <button class=del id=delAll>删除全部设备</button>")
# ---------------------------------------------------------------- 样式
CSS_ANCHOR = (" button.del:hover{{background:#5a1c22;border-color:#a33;"
"color:#ffa198}}")
CSS_NEW = CSS_ANCHOR + """
button.del:disabled{{opacity:.45;cursor:not-allowed}}
.pickcell{{width:34px;text-align:center;padding-left:10px;padding-right:0}}
input[type=checkbox]{{width:15px;height:15px;cursor:pointer;accent-color:#2f81f7}}
tr.picked td{{background:#1f2733}}"""
# ---------------------------------------------------------------- 前端 JS
# 注意:这里写 \\\\n,落到 relay.py 里是 \\n,
# 再经 f-string 渲染,JS 拿到的才是 反斜杠+n。
JS_ANCHOR = '''document.querySelectorAll('button.del[data-device]').forEach(b => {{
b.onclick = () => del(b.dataset.device, b.dataset.name, b.dataset.code);
}});'''
JS_NEW = JS_ANCHOR + """
// ---------------- 批量删除 ----------------
const picks = () => Array.from(document.querySelectorAll('input.pick'));
const picked = () => picks().filter(c => c.checked).map(c => c.value);
function refreshPickState() {{
const chosen = picked().length;
const btn = document.getElementById('delPicked');
if (btn) {{
btn.disabled = chosen === 0;
btn.textContent = chosen ? ('删除选中 (' + chosen + ')') : '删除选中';
}}
picks().forEach(c => {{
const tr = c.closest('tr');
if (tr) tr.classList.toggle('picked', c.checked);
}});
const all = document.getElementById('pickAll');
if (all) {{
const total = picks().length;
all.checked = total > 0 && chosen === total;
all.indeterminate = chosen > 0 && chosen < total;
}}
}}
picks().forEach(c => c.addEventListener('change', refreshPickState));
const pickAll = document.getElementById('pickAll');
if (pickAll) pickAll.addEventListener('change', () => {{
picks().forEach(c => {{ c.checked = pickAll.checked; }});
refreshPickState();
}});
refreshPickState();
const delPicked = document.getElementById('delPicked');
if (delPicked) delPicked.onclick = async () => {{
const ids = picked();
if (!ids.length) return;
if (!confirm('删除选中的 ' + ids.length + ' 台设备?\\\\n\\\\n'
+ '会断开它们现在的手表连接,并清掉它们占的配对码。'))
return;
delPicked.disabled = true;
try {{
const r = await fetch('/v1/admin/delete_many', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{deviceIds: ids}})
}});
const j = await r.json();
if (j.ok) {{
alert('已删除 ' + j.deleted + ' 台设备'
+ (j.failed ? (',' + j.failed + ' 台失败') : ''));
location.reload();
}} else {{
alert('删除失败:' + (j.error || '未知错误'));
delPicked.disabled = false;
}}
}} catch (e) {{
alert('请求出错:' + e);
delPicked.disabled = false;
}}
}};"""
PAIRS = [
(BACKEND_ANCHOR, BACKEND_NEW),
(ROW_ANCHOR, ROW_NEW),
(HEADER_ANCHOR, HEADER_NEW),
(BAR_ANCHOR, BAR_NEW),
(CSS_ANCHOR, CSS_NEW),
(JS_ANCHOR, JS_NEW),
]
def main() -> int:
text = RELAY.read_text(encoding="utf-8")
if "delete_many" in text:
print(" 已经加过了")
return 0
ok = True
for old, new in PAIRS:
if old not in text:
print(f" [失败] 锚点没找到 -> {old.strip()[:70]!r}")
ok = False
continue
if text.count(old) != 1:
print(f" [警告] 锚点出现 {text.count(old)} 次,只替换第一处")
text = text.replace(old, new, 1)
RELAY.write_text(text, encoding="utf-8", newline="\n")
if ok:
print(" 已加:/v1/admin/delete_many + 复选框 + 全选 + 删除选中")
try:
ast.parse(RELAY.read_text(encoding="utf-8"))
print(" relay.py 语法 OK")
except SyntaxError as exc:
print(f" relay.py 语法错误: 行 {exc.lineno}: {exc.msg}")
ok = False
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())