import Foundation enum ModelKind: String, CaseIterable { case llm = "Qwen3-1.7B-MLX-4bit" case vl = "Qwen2.5-VL-3B-MLX-4bit" var displayName: String { switch self { case .llm: return "Qwen3-1.7B" case .vl: return "Qwen2.5-VL-3B" } } /// 用于判定该模型是否已就绪的最小标志文件 var sentinelFilename: String { "config.json" } } final class ModelStore { static let shared: ModelStore = { do { let appSupport = try FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) let root = appSupport.appendingPathComponent("Models", isDirectory: true) return try ModelStore(rootURL: root) } catch { fatalError("ModelStore.shared init failed: \(error)") } }() let rootURL: URL init(rootURL: URL) throws { self.rootURL = rootURL try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true) } func localURL(for kind: ModelKind) -> URL { rootURL.appendingPathComponent(kind.rawValue, isDirectory: true) } func isReady(_ kind: ModelKind) -> Bool { let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename) return FileManager.default.fileExists(atPath: sentinel.path) } func totalBytes(for kind: ModelKind) -> Int { let folder = localURL(for: kind) guard let enumerator = FileManager.default.enumerator( at: folder, includingPropertiesForKeys: [.fileSizeKey] ) else { return 0 } var sum = 0 for case let url as URL in enumerator { if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize { sum += size } } return sum } /// Demo 现场旁路:从 Bundle 拷贝预装模型(W6 才真正使用,本周占位) func seedFromBundle(_ kind: ModelKind) throws { guard let bundleURL = Bundle.main.url(forResource: kind.rawValue, withExtension: nil) else { #if DEBUG assertionFailure("Bundle 缺少 \(kind.rawValue),检查资源是否加入 target") #endif return } let target = localURL(for: kind) if FileManager.default.fileExists(atPath: target.path) { try FileManager.default.removeItem(at: target) } try FileManager.default.copyItem(at: bundleURL, to: target) } }