修复米家会话失效后不弹登录二维码
Docker Build and Push / build-image (push) Successful in 5m46s

根因一: 过期检测形同虚设
- @zythum02/mijia-api 的 refreshToken() 在本地凭证字段齐全时直接返回成功,
  根本不发请求, 因此 checkAuth() 永远判定 ok, 过期后既不出横幅也不出扫码入口
- checkAuth() 改为真打一次 getHomesList() 验证 token; 成功缓存 5 分钟,
  失败只缓存 30 秒(便于网络恢复后自愈); 增加并发探测合并; 状态未变不写库
- getStatus() 以实时探测结果为准, DB 里的 status 仅在探测为 unknown 时兜底,
  避免残留的 ok 遮蔽已失效状态

根因二: 二维码图片 401 导致弹窗空白
- /api/mihome/reauthorize/qr-image 在 authRequired 之后, 而 <img src> 无法携带
  Authorization 头, 图片请求被拒, 用户看到的是空白/裂图
- 新增 fetchMihomeQrImage(): 带 JWT fetch 后转 blob URL 给 <img>, 并在卸载/换码时释放

其他修复
- EnvView 的 load 依赖 [status] 且成功回调又 setStatus, 造成 /status 无限重复请求;
  改为空依赖 + 函数式 setStatus; 首屏与重试走 ?force=1 强查
- 会话失效(非网络类错误)时自动弹出扫码窗, 每次挂载仅弹一次
- 扫码状态机新增 done 标记, 只有真正登录成功才算成功, 避免取消后误判
- 二维码 120s 超时自动续码(最多 2 次), 慢扫不再直接失败
- 取消后不再丢弃可能已成功的扫码结果
- scripts/mihome-login.mjs 适配 done 语义
- 同步更新设计文档 §5.2 / §5.4 / §6

注意: 前端需重新构建发布后生效。
This commit is contained in:
jif2.zhang
2026-10-23 22:21:08 +08:00
parent 7b4e59957b
commit 95b9985e8b
7 changed files with 188 additions and 81 deletions
+19 -11
View File
@@ -205,24 +205,27 @@ server/routes/mihome.js # REST 路由 (authRequired)
### 5.2 认证与「重新授权」
- 启动时 `login()` 自动尝试加载/刷新 `DiskCache` 凭证;成功则 `status=ok`。
- 失败(30 天过期 / 风控)→ `MihomeAccount.status=expired`,前端弹授权横幅。
- 启动时加载 `DiskCache` 凭证,并用一次真实云请求(`getHomesList`)确认 token 仍有效;
只有该请求通过才 `status=ok`。
- 失败(30 天过期 / 风控 / 无凭证)→ `MihomeAccount.status=expired`,前端弹授权横幅 + 扫码窗。
- 重新授权流程(服务端驱动,前端轮询):
```
POST /api/mihome/reauthorize
└─ 若 pendingQr 空闲 => 后台启动 QRlogin 循环(每轮 120s 超时自动续二维码)
└─ 立即返回 { state, qrImageUrl }
└─ 若已有流程在跑 => 直接复用(同一张二维码继续有效)
└─ 否则后台启动 QRlogin 循环(每轮 120s 超时自动续二维码, 最多续 2 次)
└─ 立即返回 { running, done, state, qrImageUrl }
GET /api/mihome/reauthorize/status
└─ 返回 { state: idle|waiting|scanning|ok|failed, qrImageUrl?, lastError? }
└─ 扫码成功(qrlogin 返回) => 保存凭证 => 状态 ok => 触发一次立即同步
└─ 返回 { running, done, state: idle|waiting|failed, qrImageUrl?, lastError? }
└─ done=true 表示扫码成功、凭证已落盘、authCache 已置 ok
POST /api/mihome/reauthorize/cancel # 人工取消, 停掉后台循环
POST /api/mihome/reauthorize/cancel # 人工取消: 只让前端停止轮询, 后台长轮询自然结束
```
- `pendingQr` 为模块级单例,防并发(同一时间只允许一个扫码流程)。
- 前端在弹窗内 `setInterval 3s` 轮询直到 `ok`,成功后关弹窗并刷新列表。
- `qrState` 为模块级单例,防并发(同一时间只允许一个扫码流程)。
- 前端在弹窗内 `setInterval 3s` 轮询,遇 `done=true` 关弹窗并刷新列表;`failed` 则停轮询显示错误。
- 二维码图片接口在 `authRequired` 之后,`<img>` 带不上 JWT,前端必须 `fetch` 后转 `blob:` URL 显示。
### 5.3 采集器 collector
@@ -261,7 +264,7 @@ POST /api/mihome/reauthorize/cancel # 人工取消, 停掉后台循环
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/status` | 授权状态 + 家庭/设备(含别名、**电量、最新样本**、30 天缺口、统计可用性探测结果) |
| GET | `/status` | 授权状态 + 家庭/设备(含别名、**电量、最新样本**、30 天缺口、统计可用性探测结果);`?force=1` 跳过 5 分钟认证缓存强查一次 |
| GET | `/device/:did/history?range=today` | 当天逐小时(见 §5.6);缺省 `range=today` |
| GET | `/device/:did/history?range=month` | 近 30 天逐日(日均 + min/max) |
| GET | `/device/:did/history?range=year` | 近 52 周逐周(周均值 + min/max,服务端按日档聚合) |
@@ -297,7 +300,12 @@ POST /api/mihome/reauthorize/cancel # 人工取消, 停掉后台循环
1. **授权横幅**(`status !== ok`):
`米家授权已失效/待授权` + 【重新授权】按钮 → `Popup` 内 `<img src=qrImageUrl>`
3s 轮询 `/reauthorize/status` → 成功自动关闭并刷新。
3s 轮询 `/reauthorize/status` → `done=true` 自动关闭并刷新。
- `status=expired` 且非网络类错误时,进入页面**自动弹出一次**扫码窗(用户关掉后不再重复弹)。
- 二维码图片接口在 `authRequired` 后面,`<img src>` 带不上 JWT,必须用 `fetch` + `Authorization`
取二进制再转 `blob:` URL(见 `src/api/mihome.js` 的 `fetchMihomeQrImage()`),否则图片 401、弹窗里一片空白。
- 认证探测不能只看 `api.refreshToken()`:该库在本地凭证字段齐全时直接返回成功而不请求云端,
所以 `checkAuth()` 必须真打一次 `getHomesList()` 才能发现 30 天 token 过期。
2. **顶部操作**:`立即同步`(POST `/sync`)。
3. **家庭列表**:`antd-mobile <Collapse accordion>` 按家庭分组;组展开后显示该家庭
下的**环境设备卡片**;无环境设备的家庭 → 折叠标题旁标「无环境设备」占位。