feat: 设备身份改为「名称 + 指纹」——更换 IP 自动认领,分组/任务引用自动同步
背景:设备池原先拿 serial(IP)当身份。设备一换 IP(DHCP 重新分配)旧记录就成了连不上的
僵尸条目(表现为"断联·自动重连中"但设备并没关机),分组与 serial 模式的任务还吊着死地址。
2026-09-11 实际发生:.70 变成 .71、.72 消失,平台两个条目永远连不上。
实现
- **设备名称必填且唯一**:加入设备必须填名称;库层面用部分唯一索引兜底
(ux_device_name / ux_device_fingerprint,WHERE 非空 → 兼容历史空值),管理页可改名
- **设备指纹**(ro.serialno):网络设备添加/确认/扫描/采集型号时自动读取;
身份三层拆分——名称(人可读,稳定)、指纹(机器识别,稳定)、serial(当前地址,可变)
- **自动认领**:添加或确认设备时指纹命中池中已有记录 → 迁移原记录到新地址
(名称/型号/备注/启用状态/添加时间全保留),不新增条目
- **人工认领** `POST /api/devices/pool/relocate`:旧地址已断联、指纹没采过时的兜底——
人工指认"这条就是那台,现在在 X",迁移并同步引用
- **引用同步**:认领/迁址时把 device_group.serials 与 task_job.target.serial 的旧地址
换成新地址。⚠️ 必须同时改**内存**:分组/任务在 TaskManager 里另有内存副本且调度用内存对象,
只改库不重启不生效 → device_pool 迁址后回调 TaskManager.sync_device_serial
(装配层用 set_move_hook 注册;device_pool 不能反向 import task_manager,会循环依赖)
- **前端**:待连接池新增「识别」列(指纹命中时提示"≈ 名称(原 IP)",按钮变「认领为 X」);
设备池新增「名称/指纹」列与「改名/换地址」操作;断联设备表也加「换地址」(用户看到断联就在这里)
- 添加设备接口改用 adb_connect_light(单次短超时),避免不可达 IP 让请求卡 30s+;重名校验提前到 adb 之前
文档:API.md §6 重写(设备身份/认领/新接口)、DATA_MODEL.md(新列 + v5/v6 迁移 + 唯一索引)、
ARCHITECTURE.md §4.0(身份三层与引用同步的内存坑)、DEPLOY.md 排查表加"断联但没关机"条目
自测(全通过):名称必填/唯一/改名/重名拒绝(含库层面约束);指纹采集(真实读到 .71 的
gy7lskwkkvj7c6b6);自动认领(指纹命中→迁址+保留名称+带指纹+未命中不误判);人工认领
(迁址+名称保留+分组与任务引用同步);浏览器验证设备池/断联表/待连接池三个界面
This commit is contained in:
@@ -228,18 +228,25 @@ def scan_once(manual=False):
|
||||
# 4. state==device 过滤(排除 unauthorized/offline)
|
||||
verified = _parse_adb_devices(_adb("devices")) & candidates
|
||||
|
||||
# 5. 写 pending:新 → 插入;已有 → 更新 last_seen
|
||||
# 5. 写 pending:新 → 插入;已有 → 更新 last_seen(顺带刷新指纹)
|
||||
# 指纹用于认出"这台其实是设备池里某台设备换了 IP",见 list_pending 的 match
|
||||
now = _fmt()
|
||||
existing = {p.serial for p in PendingDevice.query.all()}
|
||||
added = 0
|
||||
from core import device_pool
|
||||
for serial in verified:
|
||||
source = "tailscale" if serial.split(":")[0].startswith("100.") else "lan"
|
||||
try:
|
||||
fp = device_pool.read_fingerprint(serial, timeout=4)
|
||||
except Exception:
|
||||
fp = ""
|
||||
if serial in existing:
|
||||
PendingDevice.query.filter_by(serial=serial).update(
|
||||
{"last_seen": now})
|
||||
{"last_seen": now, "fingerprint": fp})
|
||||
else:
|
||||
db.session.add(PendingDevice(serial=serial, source=source,
|
||||
first_seen=now, last_seen=now))
|
||||
first_seen=now, last_seen=now,
|
||||
fingerprint=fp))
|
||||
added += 1
|
||||
db.session.commit()
|
||||
# 6. 正式池断联设备自动重连:adb connect 会因 WiFi 波动/设备重启/
|
||||
@@ -371,20 +378,47 @@ def list_pending():
|
||||
with _ctx():
|
||||
rows = [p.to_dict() for p in PendingDevice.query.order_by(
|
||||
PendingDevice.first_seen.desc()).all()]
|
||||
return [dict(r, online=True) for r in rows if r["serial"] in online]
|
||||
out = []
|
||||
for r in rows:
|
||||
if r["serial"] not in online:
|
||||
continue
|
||||
r["online"] = True
|
||||
# 指纹匹配:这台其实就是设备池里某台设备换了地址(前端据此提示"认领")
|
||||
r["match"] = None
|
||||
if r.get("fingerprint"):
|
||||
try:
|
||||
from core import device_pool
|
||||
m = device_pool.find_by_fingerprint(r["fingerprint"])
|
||||
if m and m["serial"] != r["serial"]:
|
||||
r["match"] = {"serial": m["serial"], "name": m.get("name") or ""}
|
||||
except Exception:
|
||||
pass
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def confirm_pending(serial, name=""):
|
||||
def confirm_pending(serial, name="", fingerprint=""):
|
||||
"""确认连接:pending 行 → 正式设备池(add_device upsert)→ 删 pending。
|
||||
|
||||
返回 (ok, msg, is_new)。adb_connect + 采型号由 API 层后台线程做。
|
||||
先按指纹尝试**认领**:同一台物理设备换了地址时,把池中旧记录迁到新 serial,
|
||||
并同步分组/任务里的引用(名称等信息全部保留),而不是新增一条。
|
||||
|
||||
返回 (ok, msg, is_new)。adb_connect + 采型号/指纹由调用方(API 层后台线程)做。
|
||||
"""
|
||||
with _ctx():
|
||||
row = PendingDevice.query.get(serial)
|
||||
if not row:
|
||||
return False, "设备不在待连接列表", False
|
||||
from core import device_pool
|
||||
is_new = device_pool.add_device(serial, name=name or "")
|
||||
fp = (fingerprint or row.fingerprint or "").strip()
|
||||
claimed_old, claimed_name = device_pool.claim_device(serial, fp)
|
||||
if claimed_old:
|
||||
device_pool.add_device(serial, name=claimed_name or name, fingerprint=fp)
|
||||
PendingDevice.query.filter_by(serial=serial).delete()
|
||||
db.session.commit()
|
||||
return True, (f"已认领为『{claimed_name or claimed_old}』"
|
||||
f"(原地址 {claimed_old},分组/任务的引用已同步)"), False
|
||||
is_new = device_pool.add_device(serial, name=name or "", fingerprint=fp)
|
||||
PendingDevice.query.filter_by(serial=serial).delete()
|
||||
db.session.commit()
|
||||
return True, "已加入设备池" + ("" if is_new else "(已存在,信息已更新)"), is_new
|
||||
|
||||
Reference in New Issue
Block a user