MiTV OTA 机型库怎么落地:ADB 采集、在线验证和敏感字段加密
不是简单收集电视型号:我实际怎样从 getprop 生成 OTA 参数、用小米 OTA 接口验证贡献,再把 SN / 设备身份加密后用于持续监测。
这篇对应实际项目:
https://github.com/UcnacDx2/mitv-ota-monitor
之前我把它写成了“想做一个电视固件地图”,但那样没有说清楚最有价值的部分:机型数据不是用户填完表单就直接进库,而是先从设备采参数,再拿这组参数去请求小米 OTA,验证它真的能返回固件后才接受。
问题不是少一个“型号”字段
电视商品页上的型号,和 OTA 服务真正使用的设备身份不是一回事。
项目里至少要处理这些值:
displayName
product
codename
device
module
currentVersion
lang
serial
deviceIdentity
其中 serial 和 deviceIdentity 还属于敏感设备信息,不能跟普通机型元数据一样直接公开。
所以我把流程拆成了:
ADB 读取
↓
生成规范化参数
↓
服务端格式校验
↓
真实请求 Xiaomi OTA
↓
确认能拿到版本和 package
↓
机型公开入库
↓
SN / deviceIdentity 加密保存,仅供后续监测
第一步:不是让贡献者手抄十个 getprop
实际代码在:
components/contribution-form.tsx
页面直接生成了一条 ADB 命令,一次输出表单需要的完整字段:
adb shell 'product=$(getprop ro.short_assm_mn); codename=$(getprop ro.product.device); printf "displayName=%s\nproduct=%s\ncodename=%s\ndevice=%s.%s\nmodule=%s.%s.firmware\nminimumKnownVersion=%s\nlang=%s\nserial=%s\ndeviceIdentity=%s\n" "$(getprop ro.product.model)" "$product" "$codename" "$product" "$codename" "$product" "$codename" "$(getprop ro.build.version.incremental)" "$(getprop ro.product.locale)" "$(getprop ro.serialno)" "$(getprop mitv.factory.mac)"'
这条命令实际读取的是:
| 输出字段 | Android 属性 |
|---|---|
displayName |
ro.product.model |
product |
ro.short_assm_mn |
codename |
ro.product.device |
minimumKnownVersion |
ro.build.version.incremental |
lang |
ro.product.locale |
serial |
ro.serialno |
deviceIdentity |
mitv.factory.mac |
另外两个值默认按规则拼出来:
device = product.codename
module = product.codename.firmware
例如我当时实机验证过的组合是:
codename = finch
product = OBPCN1N
重点是:贡献者不用理解 OTA 参数怎么拼,只需要打开 ADB、运行一条只读命令、把输出交给表单。
第二步:表单提交后先做格式校验
提交接口在:
app/api/contribute/route.ts
服务端不会直接信任用户输入。
它先对几个设备标识做正则约束:
const MODEL_VALUE = /^[A-Za-z0-9._-]{2,96}$/;
const VERSION_VALUE = /^[A-Za-z0-9._-]{3,96}$/;
const LANG_VALUE = /^[A-Za-z]{2}_[A-Za-z]{2}$/;
如果用户没有手工覆盖,服务端也会重新生成默认值:
const device = input.device || `${product}.${codename}`;
const moduleName = input.module || `${product}.${codename}.firmware`;
const lang = input.lang || 'zh_CN';
也就是说,前端的默认拼接只是用户体验,真正落库前服务端会再做一次相同约束。
第三步:贡献频率不是靠前端按钮防刷
接口会取:
cf-connecting-ip
user-agent
然后做 SHA-256:
createHash('sha256')
.update(`${ip}\n${userAgent}`)
.digest('hex')
再交给:
claimContributionWindow(...)
做提交窗口限制。
如果提交过快,直接返回:
429
提交过于频繁,请一分钟后再试
这个 fingerprint 不是拿来当用户身份,而只是一个不直接保存 IP 文本的简单限流键。
第四步:最关键的一步——拿贡献参数去真实请求 Xiaomi OTA
服务端先构造公开配置:
const config = {
displayName,
product,
device,
module: moduleName,
lang,
currentVersion,
};
然后把敏感字段只在请求阶段合进去:
const status = await checkXiaomiOta({
...config,
serial,
deviceIdentity,
});
接下来不是“请求没报错就算成功”,而是至少满足:
status.ok
status.latestVersion
status.packages.length > 0
否则接口直接返回 422:
小米 OTA 未确认该组信息可获得更新,未加入机型库
这一步就是整个机型库质量控制的核心。
贡献者写了一个看起来很像真的 product/codename 并没有用;小米 OTA 服务自己必须能认这组身份。
第五步:验证成功后到底写了哪些东西
如果 OTA 验证通过,会继续执行:
await upsertCommunityModel(env.DB, config, status);
await archiveOtaObservation(env.DB, config, status);
也就是:
- 更新社区机型记录;
- 保存这一次 OTA 观察结果。
如果 OTA 返回的最新版本和贡献者当前版本不同,还会记录一次版本对:
if (status.latestVersion !== currentVersion) {
await recordVersionProbe(...)
}
这也是为什么项目后来不仅能展示“最新版本”,还能逐步积累:
某个源版本
→ 被 OTA 服务导向哪个目标版本
这种历史关系。
第六步:SN 和设备身份不能跟机型数据一起公开
OTA 查询又确实需要:
serial
deviceIdentity
所以项目没有把它们放到公开 GitHub 数据里。
接口要求服务器配置 CHECK_TOKEN,然后:
const encrypted = await encryptMonitorCredentials(
env.CHECK_TOKEN,
{ serial, deviceIdentity },
);
await saveMonitorCredentials(
env.DB,
config,
encrypted.iv,
encrypted.ciphertext,
);
也就是说网页可以公开:
这个机型是什么
当前看到什么版本
有哪些 OTA 包
但持续查询需要的设备身份是加密保存的,不显示在网页,也不提交进 Git 仓库。
前端为什么显示“验证并贡献机型”而不是“提交”
ContributionForm 的按钮实际写的是:
验证并贡献机型
因为提交成功的含义不是:
我收到了你的数据。
而是:
我拿你的数据问过 Xiaomi OTA,它确实返回了有效更新信息,然后我才把它纳入机型库。
成功响应里会带:
latestVersion
packageCount
monitoring: true
前端收到后显示:
验证通过,已加入机型库。最新版本:...
这个项目真正解决的是什么
如果只是做一个“电视型号 Wiki”,其实没有必要写这么多后端逻辑。
这个项目更像一条数据验证流水线:
真实设备属性
↓
可复现的 ADB 采集
↓
规范化 OTA 参数
↓
官方 OTA 服务在线验证
↓
公开机型元数据 + OTA 观察历史
↓
敏感设备身份加密后持续监测
所以我后来觉得它最有价值的地方,不是网页上多了多少型号,而是:每一条社区数据都尽可能带着“这组参数真的被 OTA 服务接受过”的证据。