mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
203 lines
7.1 KiB
Python
203 lines
7.1 KiB
Python
# -*- 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())
|