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:
2026-09-11 11:03:38 +08:00
parent 24d57d3b96
commit 0a1b4d6122
12 changed files with 595 additions and 70 deletions
+41 -12
View File
@@ -143,8 +143,10 @@
| 方法 | 路径 | 鉴权 | 功能 |
|------|------|------|------|
| GET | `/api/devices/pool` | D | 设备池清单(附实时在线状态) |
| POST | `/api/devices/pool/add` | D | 添加/更新设备 |
| GET | `/api/devices/pool` | D | 设备池清单(附实时在线状态 + 设备指纹) |
| POST | `/api/devices/pool/add` | D | 添加/更新设备(**名称必填唯一**,按指纹自动认领换 IP 的设备) |
| POST | `/api/devices/pool/rename` | D | 重命名设备(名称唯一) |
| POST | `/api/devices/pool/relocate` | D | **人工认领**:把记录迁到新地址并同步分组/任务引用 |
| POST | `/api/devices/pool/remove` | D | 从池中删除 |
| POST | `/api/devices/pool/toggle` | D | 启用/停用(停用不参与调度) |
| POST | `/api/devices/pool/reconnect` | D | 一键重连全部池内网络设备 |
@@ -395,24 +397,51 @@
## 6. 设备池与自动发现
### 6.1 设备身份:名称 + 指纹
| 概念 | 说明 |
|------|------|
| **名称 `name`** | **必填且唯一**,设备在平台里的人可读标识(分组/任务/日志都按它认设备) |
| **serial** | 设备的**当前连接地址**(`IP:5555` 或 USB 序列号)——**可变** |
| **指纹 `fingerprint`** | `ro.serialno`,识别"同一台物理设备"的稳定标识(只对网络设备采集;USB 的 serial 本身已稳定) |
**为什么需要**:设备池原先拿 serial(IP)当身份,设备一换 IP 就变成"陌生新设备",
旧记录永远连不上,分组与 serial 模式的任务还吊着死地址。现在:
- 添加/确认设备时读取指纹 → 若命中池中已有设备(**同一台换了地址**)→ **自动认领**
- 认领 = 迁移原记录(名称/型号/备注/启用状态/添加时间全保留)+ 把 `device_group.serials`
与 `task_job.target.serial` 里的旧地址**同步换成新地址**(库里与内存一起改)
- 旧地址已断联、指纹也没采过时(自动认领无从匹配)→ 用 `pool/relocate` **人工认领**
> 指纹在设备在线时自动采集(添加/确认/启动刷新/「采集型号」按钮都会补);
> 设备列表的「指纹」列:🔑 = 已采集,— = 尚未采集(离线设备采不到)。
### 6.2 设备池
| 接口 | 请求 | 说明 |
|------|------|------|
| `GET /api/devices/pool` | — | 池内设备 + 实时 `online` 状态 |
| `POST /api/devices/pool/add` | `{"serial","name?","note?"}` | upsert;`IP:5555` 立即尝试 connect;后台采型号 |
| `GET /api/devices/pool` | — | 池内设备 + 实时 `online` + `fingerprint` |
| `POST /api/devices/pool/add` | `{"serial","name","note"?}` | **name 必填**(空 → 400)、**唯一**(重名 → 400)。IP:5555 会轻量 `adb connect` 并读指纹,命中则**自动认领**(响应 `claimed=true` + `old_serial`) |
| `POST /api/devices/pool/rename` | `{"serial","name"}` | 改名(唯一校验;不存在 → 404) |
| `POST /api/devices/pool/relocate` | `{"old_serial","new_serial"}` | 人工认领:迁移记录 + 同步引用;旧地址不在池 → 400,新地址已在池 → 400 |
| `POST /api/devices/pool/remove` | `{"serial"}` | 不存在 → 404 |
| `POST /api/devices/pool/toggle` | `{"serial","enabled"}` | 停用则不参与调度 |
| `POST /api/devices/pool/reconnect` | — | 后台并发 10 线程重连全部网络设备 |
| `POST /api/devices/pool/refresh_models` | — | 后台批量采型号 |
| `GET /api/devices/discovery` | — | 发现状态 + 待连接 + 池内断联(前端 10s 轮询) |
| `POST /api/devices/discovery/scan` | — | 后台扫描一轮(约 5-30s);已有扫描 → **409** |
| `POST /api/devices/discovery/confirm` | `{"serial"}` | 待连接 → 设备池 + connect + 采型号 |
| `POST /api/devices/pool/reconnect` | — | 后台并发重连全部网络设备 |
| `POST /api/devices/pool/refresh_models` | — | 后台批量采型号 **+ 补齐缺失指纹** |
### 6.3 自动发现
| 接口 | 请求 | 说明 |
|------|------|------|
| `GET /api/devices/discovery` | — | 发现状态 + 待连接 + 池内断联(前端 10s 轮询)。**待连接项带 `fingerprint` 与 `match`**(`{"serial","name"}` = 指纹命中的池内设备) |
| `POST /api/devices/discovery/scan` | — | 后台扫描一轮;已有扫描 → **409** |
| `POST /api/devices/discovery/confirm` | `{"serial","name"?}` | 确认入池。**新设备必须有名称**(空 → 400);**指纹命中已有设备时不需要名称**——保留原记录与原名,直接认领到新地址 |
| `POST /api/devices/discovery/ignore` | `{"serial"}` | 从待连接删除 |
| `POST /api/devices/discovery/reconnect` | `{"serial"}` | 只接受池内设备,否则 404 |
| `POST /api/devices/discovery/settings` | `{"enabled","subnets","interval","port"}` | 部分更新,存 `app_meta` |
> 自动发现**只做 socket 探测 + 只读校验**,不会把设备直接拉进设备池(必须人工确认)。
---
> 扫描只做 socket 探测 + 只读校验,**不会把设备直接拉进设备池**(必须人工确认)。
> 扫描时会顺带读一遍候选设备的指纹(写入待连接池),用于提示"这台是已有设备换了地址"。
## 7. 远程看屏与设备操作