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:
@@ -12,6 +12,100 @@ from web.auth import perm_required, PERM_DEVICES
|
||||
_log = get_logger("web")
|
||||
bp = Blueprint("devices", __name__)
|
||||
|
||||
|
||||
def _lan_ip():
|
||||
"""挑一个手机连得上的本机地址:优先 192.168.x,其次 10.x。
|
||||
|
||||
不能用 gethostbyname(hostname) —— 这台机器上它可能返回 Tailscale 的
|
||||
100.x(CGNAT 地址,手机不在 tailnet 里根本连不上),或 docker 的 172.17。
|
||||
"""
|
||||
import socket
|
||||
candidates = []
|
||||
try:
|
||||
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
||||
ip = info[4][0]
|
||||
if ip.startswith("127.") or ip.startswith("169.254."):
|
||||
continue
|
||||
candidates.append(ip)
|
||||
except Exception:
|
||||
pass
|
||||
for ip in candidates:
|
||||
if ip.startswith("192.168."):
|
||||
return ip
|
||||
for ip in candidates:
|
||||
if ip.startswith("10."):
|
||||
return ip
|
||||
return candidates[0] if candidates else "本机IP"
|
||||
|
||||
|
||||
@bp.route("/api/devices/qrcode")
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_qrcode():
|
||||
"""给某台设备生成「配置二维码」——设备端 Agent 扫一下就把自己配好。
|
||||
|
||||
二维码里是一段 JSON(平台地址 + 设备令牌 + 这台设备的指纹),扫码即完成:
|
||||
填 server/token/指纹 三件事,不用在手机上打字,也不用连 adb。
|
||||
|
||||
⚠️ 两条使用前提(都会在返回里给提示,不静默失败):
|
||||
1. 平台要先在「应用管理 → 设备端应用商店」启用(令牌是从那儿来的)
|
||||
2. **要用局域网地址打开本页面**(二维码里的地址取自当前访问地址;
|
||||
如果你是用 localhost 打开的,手机会连不上,这里会明确告诉你)
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
import json as _json
|
||||
|
||||
serial = (request.args.get("serial") or "").strip()
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
|
||||
dev = None
|
||||
for d in device_pool.list_devices():
|
||||
if d.get("serial") == serial:
|
||||
dev = d
|
||||
break
|
||||
if dev is None:
|
||||
return jsonify({"ok": False, "error": "设备不在池里"}), 404
|
||||
|
||||
# 令牌必须先启用设备端商店才有
|
||||
from core.db_config import meta_get
|
||||
from web.device_agent_api import store_enabled, ensure_token
|
||||
if not store_enabled():
|
||||
return jsonify({"ok": False,
|
||||
"error": "还没启用设备端商店:先到「应用管理 → 设备端应用商店」勾选启用"}), 400
|
||||
token = ensure_token()
|
||||
|
||||
server = request.host_url.rstrip("/")
|
||||
warn = ""
|
||||
if "127.0.0.1" in server or "localhost" in server:
|
||||
warn = ("你现在是用本机地址打开平台的,二维码里的地址手机会连不上 —— "
|
||||
"请改用局域网地址(如 http://%s:18050)重新打开本页再生成。"
|
||||
% _lan_ip())
|
||||
|
||||
fp = dev.get("fingerprint") or ""
|
||||
if not fp:
|
||||
warn = (warn + " " if warn else "") + \
|
||||
"这台设备还没采集到指纹,扫码后平台认不出它是哪一台(设备端会显示「未登记」);" \
|
||||
"建议先让它在线并刷新一次型号/指纹,再生成二维码。"
|
||||
|
||||
payload = {"v": 1, "server": server, "token": token,
|
||||
"fingerprint": fp, "name": dev.get("name") or ""}
|
||||
text = _json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
try:
|
||||
import qrcode
|
||||
img = qrcode.make(text)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
png = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
||||
except Exception as e:
|
||||
_log.error(f"生成二维码失败: {e}")
|
||||
return jsonify({"ok": False, "error": f"生成二维码失败: {e}"}), 500
|
||||
|
||||
_log.info(f"生成设备配置二维码: {dev.get('name') or serial}")
|
||||
return jsonify({"ok": True, "serial": serial, "name": dev.get("name") or "",
|
||||
"fingerprint": fp, "png": png, "payload": text, "warn": warn})
|
||||
|
||||
@bp.route("/api/devices/pool", methods=["GET"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_list():
|
||||
|
||||
Reference in New Issue
Block a user