import Foundation import Observation /// 模型下载编排:遍历 ModelManifest 逐文件串行下载,聚合进度,支持暂停/重试/旁路导入。 /// UI 只观察 `states`,不直接碰 URLSession(§3.1 模块边界)。 /// 核心下载/校验逻辑在 `FileDownloader`,文件路径/就绪判定在 `ModelStore`。 @MainActor @Observable final class ModelDownloadService { static let shared = ModelDownloadService() private(set) var states: [ModelKind: DownloadState] = [:] private let store: ModelStore private var tasks: [ModelKind: Task] = [:] private var lastSampleTime: [ModelKind: Date] = [:] private var lastSampleBytes: [ModelKind: Int] = [:] init(store: ModelStore = .shared) { self.store = store refreshStates() } /// 根据沙盒现状刷新每个模型的状态(已完整→ready,否则 idle)。 func refreshStates() { for kind in ModelKind.allCases { let total = ModelManifest.totalBytes(for: kind) if store.isComplete(for: kind) { states[kind] = DownloadState(phase: .ready, receivedBytes: total, totalBytes: total, bytesPerSecond: 0) } else if states[kind]?.phase == .downloading { continue // 不打断进行中的下载 } else { states[kind] = DownloadState(phase: .idle, receivedBytes: completedBytes(for: kind), totalBytes: total, bytesPerSecond: 0) } } } var isAnyDownloading: Bool { states.values.contains { $0.phase == .downloading } } /// 下载某个模型。幂等:已在下载或已就绪则忽略。 func download(_ kind: ModelKind) { guard tasks[kind] == nil, states[kind]?.phase != .ready else { return } let total = ModelManifest.totalBytes(for: kind) states[kind] = DownloadState(phase: .downloading, receivedBytes: completedBytes(for: kind), totalBytes: total, bytesPerSecond: 0) lastSampleTime[kind] = Date() lastSampleBytes[kind] = completedBytes(for: kind) let task = Task { [weak self] in guard let self else { return } await self.run(kind) } tasks[kind] = task } func downloadAll() { for kind in ModelKind.allCases { download(kind) } } /// 暂停下载。已下载的 .part 保留,下次从断点续传。 func cancel(_ kind: ModelKind) { tasks[kind]?.cancel() tasks[kind] = nil let total = ModelManifest.totalBytes(for: kind) states[kind] = DownloadState(phase: .idle, receivedBytes: completedBytes(for: kind), totalBytes: total, bytesPerSecond: 0) } /// 旁路导入:从用户选择的文件夹拷入模型(现场重装兜底)。 func importModel(_ kind: ModelKind, from folder: URL) throws { try store.importModel(kind, from: folder) refreshStates() } // MARK: - 内部 private func run(_ kind: ModelKind) async { let files = ModelManifest.files(for: kind) let downloader = FileDownloader() var completedBefore = 0 do { for file in files { if Task.isCancelled { return } let destination = store.fileURL(for: kind, relativePath: file.path) let base = completedBefore try await downloader.download( from: ModelManifest.fileURL(for: kind, file: file), to: destination, expectedBytes: file.bytes, onProgress: { [weak self] received in Task { @MainActor in self?.applyProgress(kind, currentTotal: base + received) } } ) completedBefore += file.bytes } finish(kind, success: true, message: nil) } catch { if Task.isCancelled { // cancel() 已设置 idle 状态 } else { finish(kind, success: false, message: error.localizedDescription) } } } private func applyProgress(_ kind: ModelKind, currentTotal: Int) { guard var state = states[kind], state.phase == .downloading else { return } let now = Date() if let lastTime = lastSampleTime[kind], let lastBytes = lastSampleBytes[kind] { let dt = now.timeIntervalSince(lastTime) if dt >= 0.5 { state.bytesPerSecond = Double(currentTotal - lastBytes) / dt lastSampleTime[kind] = now lastSampleBytes[kind] = currentTotal } } state.receivedBytes = currentTotal states[kind] = state } private func finish(_ kind: ModelKind, success: Bool, message: String?) { tasks[kind] = nil let total = ModelManifest.totalBytes(for: kind) if success { states[kind] = DownloadState(phase: .ready, receivedBytes: total, totalBytes: total, bytesPerSecond: 0) } else { states[kind] = DownloadState(phase: .failed(message ?? "下载失败"), receivedBytes: completedBytes(for: kind), totalBytes: total, bytesPerSecond: 0) } } /// 已完整下载的文件字节之和(用于续传时的起始进度)。 private func completedBytes(for kind: ModelKind) -> Int { ModelManifest.files(for: kind).reduce(0) { sum, file in store.localBytes(for: kind, relativePath: file.path) == file.bytes ? sum + file.bytes : sum } } }