feat: 设备配置二维码——设备池里出码,Agent 扫一下配好
配合设备端 Agent 新增的「扫码配置」:人在机架前、设备还没接进平台时,
不用插线也不用在手机上打字输地址/令牌(还容易漏指纹)。
- `GET /api/devices/qrcode?serial=`:生成配置二维码(PNG data-url),内容为
{"v":1,"server":…,"token":…,"fingerprint":…,"name":…}
* 前置:设备端商店已启用(令牌从那儿来),否则 400 并说明去哪儿开
* `warn` 非空必须显示:①用 localhost 打开平台时,二维码里的地址手机会连不上
(提示改用局域网地址,_lan_ip() 优先挑 192.168/10.x,**不用** gethostbyname ——
那会返回 Tailscale 的 100.x 或 docker 的 172.17,手机根本连不上)
②设备还没采到指纹 → 扫完平台认不出是哪台
- 设备池每行加「二维码」按钮 → 弹窗显示二维码 + 设备名/地址 + 可展开看原始内容
- requirements 加 qrcode(纯 Python,配合已有的 Pillow 出 PNG,不引前端二维码库)
- 文档:API.md §6.2.2 新增、§6.2.1 名称呈现表补齐(剪贴板/安装弹窗/版本管理/
安装进度四处);DEVICE_AGENT.md §5.3 新增二维码内容格式(设备端契约)
验证:二维码用 cv2.QRCodeDetector 解回来与 payload 完全一致;手机上实测——
扫 A07 的码 → 相机启动 → 解码 → 保存 → 立刻拉清单,平台认出「A07」。
This commit is contained in:
@@ -498,6 +498,7 @@ async function loadDevPool(){
|
||||
'<td style="color:var(--text-light)">'+esc(d.created_at||'')+'</td>'+
|
||||
'<td style="white-space:nowrap">'+
|
||||
'<button class="btn btn-xs" onclick="renamePoolDev(\''+esc(d.serial)+'\')">改名</button> '+
|
||||
'<button class="btn btn-xs" title="生成配置二维码:设备端 Agent 扫一下即配好(平台地址/令牌/指纹)" onclick="showDevQrcode(\''+esc(d.serial)+'\')">二维码</button> '+
|
||||
(d.online ? '' :
|
||||
'<button class="btn btn-xs" title="设备换了 IP、旧地址连不上时:把这条记录迁到新地址(名称与分组/任务引用自动保留)" onclick="relocatePoolDev(\''+esc(d.serial)+'\')">换地址</button> ') +
|
||||
'<button class="btn btn-xs" onclick="togglePoolDev(\''+esc(d.serial)+'\','+(!d.enabled)+')">'+(d.enabled?'停用':'启用')+'</button> '+
|
||||
@@ -572,3 +573,37 @@ async function refreshPoolModels(){
|
||||
if(r.ok){showToast(r.msg,'success');setTimeout(loadDevPool,12000);}
|
||||
else showToast(r.error,'error');
|
||||
}
|
||||
|
||||
// ================== 设备配置二维码(设备端 Agent 扫码即配好) ==================
|
||||
// 二维码内容:平台地址 + 设备令牌 + 这台设备的指纹(JSON)。
|
||||
// 手机上的 Agent 扫一下 → 保存这三项 → 不用在手机上打字、也不用连 adb。
|
||||
async function showDevQrcode(serial){
|
||||
document.getElementById('modal-title').textContent='设备配置二维码';
|
||||
document.getElementById('modal-body').innerHTML='<p class="text-muted">生成中...</p>';
|
||||
document.getElementById('modal-footer').innerHTML=
|
||||
'<button class="btn" onclick="closeModal()">关闭</button>';
|
||||
showModal();
|
||||
const r=await apiGet('/api/devices/qrcode?serial='+encodeURIComponent(serial));
|
||||
const box=document.getElementById('modal-body');
|
||||
if(!r||!r.ok){
|
||||
box.innerHTML='<div style="background:#fdecea;border:1px solid #f5c6cb;color:#b42318;'+
|
||||
'padding:10px 12px;border-radius:6px">'+esc((r&&r.error)||'生成失败')+'</div>';
|
||||
return;
|
||||
}
|
||||
const warn=r.warn?'<div style="background:#fff3cd;border:1px solid #ffda6a;color:#7a5b00;'+
|
||||
'padding:8px 12px;border-radius:6px;margin-bottom:10px;font-size:13px">⚠️ '+esc(r.warn)+'</div>':'';
|
||||
box.innerHTML=
|
||||
warn+
|
||||
'<div style="text-align:center;padding:8px 0">'+
|
||||
'<img src="'+r.png+'" style="width:240px;height:240px;image-rendering:pixelated;'+
|
||||
'background:#fff;padding:8px;border:1px solid #e5e7eb;border-radius:8px" alt="配置二维码">'+
|
||||
'<div style="margin-top:10px;font-size:15px"><strong>'+esc(r.name||'(未命名)')+
|
||||
'</strong> <span style="font-family:monospace;color:#6b7280">'+esc(r.serial)+'</span></div>'+
|
||||
'</div>'+
|
||||
'<div class="help" style="margin-top:6px">在设备上打开 <strong>设备 Agent</strong> → 点「扫码配置」→ 扫这个二维码,'+
|
||||
'平台地址、令牌、指纹一次写入,不用在手机上打字。</div>'+
|
||||
'<details style="margin-top:8px"><summary style="cursor:pointer;color:#6b7280;font-size:12px">'+
|
||||
'查看二维码内容(排查用)</summary>'+
|
||||
'<div style="font-family:monospace;font-size:11px;color:#64748b;word-break:break-all;'+
|
||||
'background:#f8fafc;padding:8px;border-radius:6px;margin-top:6px">'+esc(r.payload)+'</div></details>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user