设计文档:设置弹层模型清单(封面+名称+使用按钮,点选才下载带进度)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@@ -0,0 +1,92 @@
|
||||
# 模型清单(设置弹层内选择/下载/使用模型)
|
||||
|
||||
日期:2026-08-31
|
||||
状态:已确认,待实现
|
||||
|
||||
## 背景与目标
|
||||
|
||||
模型不再打包进 APK,由用户在 app 内自行下载使用。在「打开视野」相机页的设置弹层中增加**模型清单**:展示后端已发布模型的封面缩略图与名称,提供「使用」按钮——未下载则先下载(显示进度)后自动使用,已下载则直接使用。
|
||||
|
||||
现状:
|
||||
- `/api/v1/app/update` 的 models[] 只有 datasetId/名称/版本/labels/大小/sha256/downloadUrl,**无封面字段**;数据集封面文件在管理端鉴权路由后,app 拿不到。
|
||||
- `ModelManager.refresh()` 启动时**自动下载目录里全部模型**,无进度回调。
|
||||
- 设置弹层(`camera_screen.dart` `_openSettings()`)目前只有置信度阈值滑块。
|
||||
- app 无任何网络图片加载代码。
|
||||
|
||||
## 已确认的决策
|
||||
|
||||
1. **多选叠加**:点「使用」的模型都保持生效,多个模型并行推理(沿用现有多模型架构);再次点击取消。
|
||||
2. **仅点选才下载**:启动只拉目录不下载;下载只发生在点「使用」时,且显示进度。
|
||||
3. **缩略图**:后端在 models[] 下发 `coverUrl`(公开路由,无需鉴权)。
|
||||
|
||||
## 后端改动(server/)
|
||||
|
||||
### 1. 公开封面路由
|
||||
|
||||
新增 `GET /api/v1/app/cover?namePrefix=<数据集前缀>`,无鉴权,注册在 main.go 公共区:
|
||||
|
||||
- 按 namePrefix 查数据集(`dao.Dataset`),取其 `Cover` 字段文件名。
|
||||
- 返回该封面文件(复用现有封面文件定位逻辑,参照 `service/dataset.go` 的 `CoverFile`)。
|
||||
- 数据集不存在 / 无封面 / 文件缺失 → 404。
|
||||
- 分层约束:controller(app_version.go 或新增)→ service(查数据集 + 定位文件)→ dao;响应体直接写(文件型响应属「直接写响应体」例外,由 controller 完成,与现有 admin 图片接口一致)。
|
||||
|
||||
### 2. models[] 增加 coverUrl
|
||||
|
||||
`service/model_version.go` `ClientCatalog` 序列化时每个条目增加:
|
||||
|
||||
- `coverUrl`:`/api/v1/app/cover?namePrefix=<NamePrefix>`
|
||||
- DTO:`model/dto/training.go` `ModelCatalogItem` 加 `CoverUrl` 字段(json tag `coverUrl`)。
|
||||
|
||||
app 端拼接方式与 downloadUrl 相同:`$baseUrl + coverUrl`。
|
||||
|
||||
## app 端改动(flutter_app/)
|
||||
|
||||
### 3. ModelManager 改造
|
||||
|
||||
`lib/models/model_manager.dart`:
|
||||
|
||||
- **refresh() 只拉目录**:删除循环 `_ensureLocal` 自动下载;保留目录解析、`_prune` 清理、`_loadBundles`。
|
||||
- 新增 `downloadModel(item, {onProgress})`:流式下载 + sha256 校验 + 写 labels.json/meta.json(复用 `_downloadAndVerify` 逻辑);进度回调 `(receivedBytes, totalBytes)`;失败可重试(保留现有重试一次语义)。
|
||||
- 新增**激活集**:
|
||||
- `Set<int> activeDatasetIds`,持久化到应用目录 `active.json`(启动加载,变化时保存)。
|
||||
- 「使用」= 加入激活集;取消 = 移除。
|
||||
- 重启后已下载的激活模型自动恢复(**不触发下载**)。
|
||||
- `models`(供 worker)**只返回激活且已下载**的 bundles;`modelsLabel` 显示激活模型名。
|
||||
- 下载进度状态:按 datasetId 维护进度映射(供弹层订阅)。
|
||||
|
||||
### 4. 设置弹层 UI
|
||||
|
||||
`lib/camera/camera_screen.dart` `_openSettings()` 的 Column 中,滑块下方新增「模型清单」区块:
|
||||
|
||||
- 区块标题「模型清单」+ 刷新按钮(重拉目录);目录为空显示「暂无已发布模型」;弹层打开时调用一次 `refresh()` 拉最新目录(幂等,与启动时的调用合并为同一次进行中的刷新)。
|
||||
- `GridView` **一行 2 个**(crossAxisCount: 2),shrinkWrap;bottom sheet 整体包 `SingleChildScrollView`。
|
||||
- 卡片:上方缩略图(AspectRatio 固定宽高比 + BoxFit.cover,`Image.network`,loading/errorBuilder,失败显示占位图标)→ 下方数据集名称 + 状态行(版本/大小)。
|
||||
- 按钮三态:
|
||||
- 未下载 →「使用」:点击后按钮位变进度条(LinearProgressIndicator + 百分比,按 sizeBytes 计算);完成自动加入激活集变「已使用」。
|
||||
- 已下载未激活 →「使用」:直接激活变「已使用」。
|
||||
- 已使用 →「已使用」(点击取消激活)。
|
||||
- 下载失败 → 恢复「使用」+ 失败提示,可重试。
|
||||
- 状态来自 ModelManager(catalog / activeIds / 进度映射),弹层用 ListenableBuilder 订阅。
|
||||
|
||||
### 5. 相机联动
|
||||
|
||||
- `camera_screen.dart` `_init()` 中的 `DetectorWorker.create()` 抽成 `_reloadWorker()`;激活集变化时 dispose 旧 worker 重建(识别暂停数百毫秒,UI 不阻塞)。相机未开时无需处理(打开时自然用新激活集)。
|
||||
- 无激活模型 → worker 为 null → 仅预览(现有降级路径,提示照旧)。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 目录拉取失败 → 弹层显示错误 + 重试按钮,不阻塞相机。
|
||||
- 封面图加载失败 → 占位图标。
|
||||
- 模型下载失败(网络/sha256 不符)→ 卡片失败提示,按钮恢复「使用」可重试。
|
||||
|
||||
## 测试
|
||||
|
||||
- ModelManager 单测(已有 rootDir/client 注入):目录拉取不触发下载;downloadModel 进度回调;sha256 失败重试;激活集持久化/恢复;models 只含激活且已下载。
|
||||
- 弹层 widget test:按钮三态、进度显示、取消激活。
|
||||
- 后端:`go build ./...` + 手工验证 `/api/v1/app/cover` 与 app/update 的 coverUrl。
|
||||
|
||||
## 影响面与风险
|
||||
|
||||
- 行为变更:启动不再自动下载模型 → 首次使用需手动点「使用」(已确认)。
|
||||
- 封面 URL 使用 namePrefix 定位数据集:无 namePrefix 的数据集不会出现在目录中(目录只含已训练模型,均有前缀);路由对缺失前缀返回 404,app 显示占位图。
|
||||
- 不新增依赖:图片用内置 `Image.network`。
|
||||
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 786 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1008 KiB |
|
Before Width: | Height: | Size: 176 KiB |