Files
ykxmz/_build/add_admin_ui.py
T
xmz7891 d4f335b8f9 管理页新增:允许/禁止新设备接入 + 修改设备配对码
== 功能一:允许新设备接入开关 ==

关掉之后只有已经登记过的电脑能继续上线,没见过的新设备一律拒绝。
状态存在 relay_state.json 里,重启不丢。

判断新设备用的是 deviceId,不是配对码 ——
这一点很关键:管理员给老设备改过码之后,
那台设备必须仍然能进来;按码判断的话它会被自己的开关挡在外面。
测试里专门有一条 A3b 守这个行为。

被拒时回 blocked=True,让客户端能区分被管理员挡了
和配对码冲突,而不是去匹配中文错误文案(文案一改就坏)。

== 功能二:修改任意设备的配对码 ==

管理页每台设备一个「改码」按钮,填 6 位数字即生效。
要点:
  * 新旧码冲突检查
  * 旧码映射必须删掉,否则变成孤儿条目,
    管理页配对码占用的数字会虚高
  * 改码踢掉该设备当前会话(会话绑在码上)
  * 立刻持久化

== 顺手修掉两个真 bug ==

1. 改码功能一上线就 500:Session 用了 __slots__,
   根本没有 session_id 属性(会话是以 token 为 key 存在 dict 里的),
   写 s.session_id 直接 AttributeError。
   改成遍历 sessions.items() 拿 key。

2. snapshot() 里有同样的错(清理超时设备那段):
   平时 stale 列表是空的所以从没炸过,一旦真有设备超过 6 小时
   没上报就会 500。这次一起修了。

== 管理页 UI ==
  * 设备小节加了「允许新设备接入」勾选框
  * 每行加「改码」按钮(走 prompt,不引入弹窗层)
  * 改码按钮用 data-recode 而不是 data-device ——
    现有单台删除按 button.danger[data-device] 绑,
    带上 data-device 会被当成删除按钮,点改码变删设备。
    测试 C6 专门断言这一点。

== 测试 ==
新增 _tests/test_admin_device_control.py(23 项),
已注册进 _tests/verify_all.ps1 第 3e 阶段。
2026-10-07 17:01:14 +08:00

184 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''管理页 UI:加「允许新设备接入」开关 + 每台设备的「改码」按钮。
设计上遵守用户的"所有东西都要白色简洁":
* 开关用系统原生 checkbox + 极简样式,不引入花哨组件
* 改码走 prompt(),不加弹窗层 —— 管理页是自用的,
少一层 DOM 就少一处会坏的地方
* 按钮沿用现有的 .ghost 样式(不抢 .danger 的视觉重量)
⚠ 一个容易踩的坑:
现有 JS 用 `button.danger[data-device]` 绑定"单台删除"。
新加的"改码"按钮**故意不带 data-device 而用 data-recode**,
否则会被删除逻辑一起选中,点改码变成删设备。
'''
from __future__ import annotations
import sys
from pathlib import Path
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay\admin.py")
def patch(text: str, old: str, new: str, label: str) -> tuple:
if old not in text:
print(" ✗ 没找到:%s" % label)
return text, False
if text.count(old) != 1:
print(" ! 出现 %d 次:%s" % (text.count(old), label))
return text, False
print(" ✓ %s" % label)
return text.replace(old, new, 1), True
def main() -> int:
text = SRC.read_text(encoding="utf-8")
ok_all = True
# ------------------------------------------------ 1. 行里加"改码"按钮
old = """ '<td class="ops"><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,
"on" if online else "off", "在线" if online else "离线",
device["idleSeconds"],
device["queuedToClient"], device["queuedToAgent"],
_kb(device["bytesIn"]), _kb(device["bytesOut"]),
did, name, code)
)"""
new = """ '<td class="ops">'
# ⚠ 这个按钮用 data-recode 而不是 data-device ——
# 现有"单台删除"是按 button.danger[data-device] 绑的,
# 这里要是也带 data-device 就会被当成删除按钮。
'<button class="ghost sm" data-recode="%s" '
'data-name="%s" data-code="%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,
"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)
)"""
text, ok = patch(text, old, new, "设备行加「改码」按钮")
ok_all = ok_all and ok
# ------------------------------------------------ 2. 开关放进"设备"小节
old = """ <div class="sec">
<h3>设备</h3>
<div class="right">
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
<input type="checkbox" id="autoRef" checked> 自动刷新
</label>"""
new = """ <div class="sec">
<h3>设备</h3>
<div class="right">
<label class="hint" id="allowWrap"
style="cursor:pointer;display:flex;align-items:center;gap:7px;margin:0 18px 0 0">
<input type="checkbox" id="allowNew"%(allownew)s>
<span>允许新设备接入</span>
</label>
<label class="hint" style="cursor:pointer;display:flex;align-items:center;gap:6px;margin:0">
<input type="checkbox" id="autoRef" checked> 自动刷新
</label>"""
text, ok = patch(text, old, new, "设备小节加开关")
ok_all = ok_all and ok
# ------------------------------------------------ 3. 传入开关状态
old = """ sessions = snap.get("sessions", 0)"""
new = """ sessions = snap.get("sessions", 0)
# 开关状态:允许新设备接入时勾上
allow_new = bool(snap.get("allowNew", True))"""
text, ok = patch(text, old, new, "读 allowNew 状态")
ok_all = ok_all and ok
old = """ "rows": _device_rows(devices),
"dl": dl_rows,"""
new = """ "rows": _device_rows(devices),
"allownew": " checked" if allow_new else "",
"dl": dl_rows,"""
text, ok = patch(text, old, new, "把状态传进模板")
ok_all = ok_all and ok
# ------------------------------------------------ 4. JS
old = """// ---------------- 单台删除 ----------------"""
new = """// ---------------- 允许/禁止新设备接入 ----------------
// 关掉之后,只有**已经登记过**的电脑能继续上线,
// 没见过的新设备一律拒绝。
const allowNew = document.getElementById('allowNew');
if (allowNew) allowNew.onchange = async () => {
const want = allowNew.checked;
allowNew.disabled = true;
try {
const j = await post('/v1/admin/allow_new', {allow: want});
if (j.ok) {
toast(j.allowNew ? '已允许新设备接入' : '已禁止新设备接入', 'ok');
} else {
toast('设置失败:' + (j.error || '未知错误'), 'err');
allowNew.checked = !want;
}
} catch (e) {
toast('请求出错:' + e, 'err');
allowNew.checked = !want;
}
allowNew.disabled = false;
};
// ---------------- 改配对码 ----------------
document.querySelectorAll('button[data-recode]').forEach(b => {
b.onclick = async () => {
const name = b.dataset.name, old = b.dataset.code;
const typed = prompt('把「' + name + '」的配对码 ' + old
+ ' 改成:\\n\\n(必须是 6 位数字)', '');
if (typed === null) return;
const code = (typed || '').trim();
if (!/^[0-9]{6}$/.test(code)) {
toast('配对码必须是 6 位数字', 'err');
return;
}
b.disabled = true;
try {
const j = await post('/v1/admin/set_code',
{deviceId: b.dataset.recode, code: code});
if (j.ok) {
toast(j.unchanged ? '配对码没有变化'
: ('配对码已改成 ' + j.code
+ (j.kicked ? ',踢掉 ' + j.kicked + ' 个会话' : '')),
'ok');
setTimeout(() => location.reload(), 900);
} else {
toast('改码失败:' + (j.error || '未知错误'), 'err');
b.disabled = false;
}
} catch (e) {
toast('请求出错:' + e, 'err');
b.disabled = false;
}
};
});
// ---------------- 单台删除 ----------------"""
text, ok = patch(text, old, new, "加开关和改码的 JS")
ok_all = ok_all and ok
if not ok_all:
print()
print(" 有锚点没匹配,没有写回")
return 1
SRC.write_text(text, encoding="utf-8", newline="\n")
print()
print(" admin.py 已写回")
return 0
if __name__ == "__main__":
sys.exit(main())