根因一: 过期检测形同虚设 - @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 注意: 前端需重新构建发布后生效。
72 lines
2.6 KiB
JavaScript
72 lines
2.6 KiB
JavaScript
// server/mihome 模块入口: 汇总导出采集/认证/查询, 并提供 /status 聚合
|
|
import { prisma } from '../db.js'
|
|
import * as vendorStats from './vendorStats.js'
|
|
import * as history from './history.js'
|
|
import * as auth from './client.js'
|
|
|
|
export { checkAuth, startQrLogin, cancelQrLogin, getQrState, getQrImageUrl, MihomeAuthError } from './client.js'
|
|
export { syncNow, dailyMaintenance } from './collector.js'
|
|
export { getDevice, todayBuckets, monthBuckets, yearBuckets, deviceOverview } from './history.js'
|
|
export { getSpec, ENV_MODELS } from './specs.js'
|
|
|
|
// GET /api/mihome/status 聚合: 授权 + 家庭(设备) + 统计可用性 + 每设备概览(电量/最新/缺日)
|
|
// force=true 跳过 5 分钟认证缓存强查一次 (用户主动打开页面/刷新时用)
|
|
export async function getStatus(force = false) {
|
|
const authState = await auth.checkAuth(force)
|
|
const [homes, devices, account] = await Promise.all([
|
|
prisma.mihomeHome.findMany({ orderBy: { id: 'asc' } }),
|
|
prisma.mihomeDevice.findMany({ orderBy: { id: 'asc' } }),
|
|
prisma.mihomeAccount.findUnique({ where: { key: 'default' } }),
|
|
])
|
|
|
|
const overviews = await Promise.all(
|
|
devices.map(async (d) => {
|
|
const ov = await history.deviceOverview(d)
|
|
return {
|
|
did: d.did,
|
|
latest: ov.latest,
|
|
missingDays: ov.missingDays,
|
|
}
|
|
})
|
|
)
|
|
const perDevice = new Map(overviews.map((o) => [o.did, o]))
|
|
|
|
// 统计可用性 (探测过的型号才有值, 未探测 undefined)
|
|
const capability = {}
|
|
for (const d of devices) {
|
|
const v = vendorStats.capabilityOf(d.model)
|
|
if (v !== undefined) capability[d.model] = v
|
|
}
|
|
|
|
const homeList = homes.map((h) => ({
|
|
homeId: h.homeId,
|
|
name: h.name,
|
|
devices: devices
|
|
.filter((d) => d.homeId === h.homeId)
|
|
.map((d) => ({
|
|
did: d.did,
|
|
model: d.model,
|
|
name: d.name,
|
|
alias: d.alias,
|
|
title: d.alias || d.name,
|
|
battery: d.battery,
|
|
batteryAt: d.batteryAt,
|
|
lastSyncAt: d.lastSyncAt,
|
|
latest: perDevice.get(d.did)?.latest ?? null,
|
|
missingDays: perDevice.get(d.did)?.missingDays ?? [],
|
|
})),
|
|
}))
|
|
|
|
// 以实时探测结果为准: DB 里的 status 只在实时探测还是 unknown 时兜底,
|
|
// 否则过期后残留的 ok 会把「已失效」遮掉, 前端永远不出重新授权横幅.
|
|
const liveUnknown = !authState.status || authState.status === 'unknown'
|
|
return {
|
|
auth: {
|
|
status: liveUnknown ? account?.status || 'unknown' : authState.status,
|
|
lastError: (liveUnknown ? account?.lastError : authState.lastError) || '',
|
|
checkedAt: authState.checkedAt,
|
|
},
|
|
capability,
|
|
homes: homeList,
|
|
}
|
|
} |