新增「我的·模型管理」页模型下载功能设计: - 独立 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>
8.1 KiB
8.1 KiB
模型自动下载功能设计(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="未配置",iconcpu),尚未连接任何界面。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 下载状态模型
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
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 重启也能续):
- 目标
Models/<repo>/<file>已存在且 size 匹配 → 跳过(粗粒度续传)。 - 否则下到
Models/<repo>/<file>.part:已下字节数 =.part当前大小, 发Range: bytes=<已下>-请求,URLSessiondata delegate 流式FileHandle追加写。 - 完成后校验
.part大小 == manifestbytes,原子rename去掉.part后缀。 - 该模型全部文件就位 →
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. 测试策略
- 单元测试(用
URLProtocolmock 网络,不碰真 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-4bit(9 文件,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-4bit(11 文件,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 字节,仅作素材核对参照。