mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 23:00:44 +00:00
== 功能一:允许新设备接入开关 ==
关掉之后只有已经登记过的电脑能继续上线,没见过的新设备一律拒绝。
状态存在 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 阶段。
184 lines
7.0 KiB
Python
184 lines
7.0 KiB
Python
# -*- 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())
|