feat(ai): add ModelStore with path management and bundle seed

按 W2 plan Task 4 落地模型路径管理:
- ModelKind enum: llm (Qwen3-1.7B-MLX-4bit) / vl (Qwen2.5-VL-3B-MLX-4bit)
- 用 config.json 作为 sentinel 判定模型是否就绪
- isReady / localURL / totalBytes 三个查询接口
- seedFromBundle(_:) 占位:Demo 现场预装模型旁路(W6 启用)
- shared 单例用 Application Support/Models/

测试 3 条:fresh / mark-ready / totalBytes,均用临时目录隔离 + defer cleanup。

注:.swift 文件需用户在 Xcode 拖入 target,⌘U 确认绿后 amend build commit。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
link2026
2026-05-25 15:09:51 +08:00
parent 0739ccea2b
commit ad6fb660f0
2 changed files with 122 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
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 {
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)
}
}