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

8.1 KiB
Raw Permalink Blame History

模型自动下载功能设计2026-05-29

让用户在「我的 · 模型管理」页一键从自建 HTTPS 服务下载两个 MLX 模型,支持断点续传、 进度展示和现场重装的旁路导入兜底。对应 CLAUDE.md §4「模型分发」与 W6「首启动下载流程」的核心部分。

1. 背景与现状

  • 模型加载链路已通:LLMSession/VLSessionModelConfiguration(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 URLhttps://file.myv0.com/(用户自建反代,标准 HTTPS
    • 备选:http://101.132.124.52:5244/(纯 IP需 App 端 ATS 例外;域名挂了时用)。
  • 已验证:config.json 返回 200两个 model.safetensors 均支持 Range206 + 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 重启也能续):

  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 旁路导入(现场重装兜底)

DebugAIRunnerfileImporter 逻辑转正进 Service / ModelStore

  • 选文件夹 → 校验含 config.json → 拷入 Models/<repo>/
  • 补上 VL(现在 DEBUG 只导 LLM
  • 按所选文件夹名匹配 ModelKind.rawValue 自动识别是 LLM 还是 VL不匹配时提示选择。

8. 接入点

  • MeView 「模型管理」卡片 → NavigationLinkModelManagementView detail 动态显示 已就绪 / 未下载 / 下载中 xx%
  • AI 入口未就绪引导§4 要求):DiaryQuickSheetUnifiedCaptureFlowHealthExport 的「模型未就绪」错误态补 前往下载 按钮,跳 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 字节,仅作素材核对参照。