Files
kangkang/docs/superpowers/specs/2026-05-29-model-download-design.md
link2026 6ccbe4ac55 docs(spec): 模型自动下载功能设计(2026-05-29)
新增「我的·模型管理」页模型下载功能设计:
- 独立 ModelDownloadService + ModelStore 保持纯存储(§3.1)
- HTTPS 断点续传(Range+追加写)、分模型卡片进度、大小校验
- 旁路文件导入兜底(补 VL)、AI 入口未就绪「前往下载」引导
- base URL https://file.myv0.com/,含精确 24 文件清单

并加 .gitignore 忽略本地模型素材目录 /Models/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 22:12:19 +08:00

181 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模型自动下载功能设计2026-05-29
> 让用户在「我的 · 模型管理」页一键从自建 HTTPS 服务下载两个 MLX 模型,支持断点续传、
> 进度展示和现场重装的旁路导入兜底。对应 CLAUDE.md §4「模型分发」与 W6「首启动下载流程」的核心部分。
## 1. 背景与现状
- 模型加载链路已通:`LLMSession`/`VLSession``ModelConfiguration(directory:)` 从沙盒
`Application Support/Models/<repo>/` 读取,`AIRuntime.prepare()/prepareVL()`
`ModelStore.isReady()` 为假时抛 `notReady`
- **缺口**:没有任何下载实现。`ModelStore` 只有 `isReady()` 判定 + `seedFromBundle()` 占位;
唯一能装模型的路径是 DEBUG-only 的 `DebugAIRunner` 手动 `fileImporter`(且只导 LLM漏 VL
- `MeView` 已预留「模型管理」卡片(`detail="未配置"`icon `cpu`),尚未连接任何界面。
- `HealthExportService` 的未就绪文案已写「请先到『我的 · 模型管理』下载」,落点早有预期。
- 无 Onboarding / 首启动流程。
## 2. 服务器素材(已就绪并验证)
- 自建 Caddy 静态文件服务,文件根 `/srv/models/`
- base URL**`https://file.myv0.com/`**(用户自建反代,标准 HTTPS
- 备选:`http://101.132.124.52:5244/`(纯 IP需 App 端 ATS 例外;域名挂了时用)。
- 已验证:`config.json` 返回 200两个 `model.safetensors` 均支持 Range`206` + `Accept-Ranges: bytes`
反代返回的总大小与本机精确一致LLM 968080210、VL 3073720461未截断大文件。
- 服务器 24 个真实文件字节数与本机逐一匹配LLM 984015687、VL 3089713215
## 3. 范围
**做**模型管理页分模型卡片、HTTPS 断点续传下载、大小校验、蜂窝网络提示、
旁路文件导入LLM + VL、MeView 接入、AI 入口未就绪「前往下载」引导。
**不做YAGNI**:首启动 Onboarding、启动自动后台下载、哈希校验大小校验够
Live Activity 下载进度Live Activity 是推理时的 tok/s单独功能、并行多文件下载。
## 4. 架构(方案 A独立 Service + ModelStore 保持纯存储)
```
ModelManagementView (UI)
→ ModelDownloadService (@MainActor @Observable下载编排 + 进度状态)
→ ModelStore (文件路径 / 就绪判定 / 旁路导入)
→ URLSession (HTTPS 分块下载)
```
- 符合 §3.1 模块边界UI 不直接碰 `URLSession`,只观察 Service 发布的状态。
- `ModelDownloadService` 与现有 `CaptureService`/`AskService` 并列。
- `ModelStore` 继续只管「模型在哪 / 是否就绪 / 旁路拷入」,不引入网络职责。
### 4.1 下载状态模型
```swift
enum DownloadPhase: Equatable {
case idle //
case downloading //
case verifying //
case ready //
case failed(String) // ·
}
struct DownloadState: Equatable {
var phase: DownloadPhase
var receivedBytes: Int
var totalBytes: Int
var bytesPerSecond: Double
var fraction: Double { totalBytes > 0 ? Double(receivedBytes) / Double(totalBytes) : 0 }
}
```
`ModelDownloadService` 持有 `var states: [ModelKind: DownloadState]``@MainActor` 更新UI 观察。
## 5. 数据:硬编码 manifest
```swift
struct ModelFile { let path: String; let bytes: Int } // path
enum ModelManifest {
static let baseURL = URL(string: "https://file.myv0.com/")!
static func files(for kind: ModelKind) -> [ModelFile]
static func totalBytes(for kind: ModelKind) -> Int // files.reduce
}
```
- 只列**加载必需**的功能文件,排除纯文档 `README.md` / `.gitattributes`(省下载)。
- 文件 URL = `baseURL / kind.rawValue / file.path`
- `bytes` 用于总进度计算与下载后**逐文件大小校验**。
- 精确清单见附录 A。
## 6. 下载流程(断点续传,应对 3GB 单文件)
逐文件**串行**下载,单文件级续传用 **HTTP Range + 追加写**(比 `URLSession.resumeData` 更可控,
app 重启也能续):
1. 目标 `Models/<repo>/<file>` 已存在且 size 匹配 → 跳过(粗粒度续传)。
2. 否则下到 `Models/<repo>/<file>.part`:已下字节数 = `.part` 当前大小,
`Range: bytes=<已下>-` 请求,`URLSession` data delegate 流式 `FileHandle` 追加写。
3. 完成后校验 `.part` 大小 == manifest `bytes`,原子 `rename` 去掉 `.part` 后缀。
4. 该模型全部文件就位 → `ModelStore.isReady` 自然为真。
- 串行(一次一个文件):不抢 MLX 资源、进度计算清晰。
- 总进度 = 已完成字节 / `totalBytes(for:)`;速度用滑动窗口算 bytes/s。
- 支持「暂停」:取消当前 task`.part` 保留,下次从断点续。
## 7. UI
### 7.1 `ModelManagementView`(分模型卡片)
- 两张卡:
- **Qwen3-1.7B · 文本解读**(约 939 MB
- **Qwen2.5-VL-3B · 拍照识别**(约 2.9 GB
- 每张卡显示:状态 `待下载 / 下载中 xx% · x.x MB/s / 校验中 / 已就绪 ✅ / 失败 · 重试`
+ 进度条(原生 `ProgressView` + `Tj.Palette`+ 大小。
- 顶部总操作 `下载全部模型``TjPrimaryButton`);下载中切为 `暂停`
- **蜂窝网络提示**`NWPathMonitor` 检测到非 WiFi开下前弹确认"约 3.9GB,建议 WiFi 下载")。
- 底部 `从文件导入``TjGhostButton`)→ 旁路导入。
- 复用 `.tjCard` / `TjBadge` / `TjLockChip`,不新增设计 token§9
### 7.2 旁路导入(现场重装兜底)
`DebugAIRunner``fileImporter` 逻辑转正进 Service / `ModelStore`
- 选文件夹 → 校验含 `config.json` → 拷入 `Models/<repo>/`
- **补上 VL**(现在 DEBUG 只导 LLM
- 按所选文件夹名匹配 `ModelKind.rawValue` 自动识别是 LLM 还是 VL不匹配时提示选择。
## 8. 接入点
- `MeView` 「模型管理」卡片 → `NavigationLink``ModelManagementView`
`detail` 动态显示 `已就绪 / 未下载 / 下载中 xx%`
- **AI 入口未就绪引导**§4 要求):`DiaryQuickSheet``UnifiedCaptureFlow``HealthExport`
的「模型未就绪」错误态补 `前往下载` 按钮,跳 `ModelManagementView`
## 9. 错误处理
- 网络中断 → 卡片转 `失败 · 重试`,保留 `.part` 供下次续传,不卡死、不删已下数据。
- 校验失败size 不符)→ 删该文件重下。
- 旁路导入选错文件夹(无 `config.json`)→ 提示,不写入。
- base URL 不可达 → 失败态,文案提示检查网络。
## 10. 测试策略
- 单元测试(用 `URLProtocol` mock 网络,不碰真 MLX / SwiftData
- `ModelManifest.totalBytes` 计算正确。
- 续传偏移计算:`.part` 已有 N 字节时请求 `Range: bytes=N-`
- 大小校验size 不符判失败。
- `DownloadState.fraction` 边界totalBytes=0
- `ModelStore.isReady` 在文件齐全 / 缺失时的判定。
- UI 手动验证:模拟器跑下载流程(指向真实 base URL 或 mock
## 附录 A精确文件清单功能文件排除 README/.gitattributes
### Qwen3-1.7B-4bit9 文件984,013,244 字节≈939 MB
| path | bytes |
|---|---|
| config.json | 937 |
| model.safetensors | 968080210 |
| model.safetensors.index.json | 49731 |
| tokenizer.json | 11422654 |
| tokenizer_config.json | 9706 |
| vocab.json | 2776833 |
| merges.txt | 1671853 |
| special_tokens_map.json | 613 |
| added_tokens.json | 707 |
### Qwen2.5-VL-3B-Instruct-4bit11 文件3,089,710,883 字节≈2.9 GB
| path | bytes |
|---|---|
| config.json | 1659 |
| model.safetensors | 3073720461 |
| model.safetensors.index.json | 108307 |
| tokenizer.json | 11421896 |
| tokenizer_config.json | 7256 |
| vocab.json | 2776833 |
| merges.txt | 1671853 |
| special_tokens_map.json | 613 |
| added_tokens.json | 605 |
| chat_template.json | 1050 |
| preprocessor_config.json | 350 |
> 注:进度分母 = 本表功能文件 `bytes` 之和(已排除 README/.gitattributes
> 服务器上含 README/.gitattributes 的全量为 LLM 984,015,687 / VL 3,089,713,215 字节,仅作素材核对参照。