Files
kangkang/体己/AI/ModelStore.swift
link2026 acfdaa1f4f fix(concurrency): nonisolated(unsafe) static shared + 修同 actor 内冗余 await
项目开启了 -default-isolation=MainActor upcoming feature,导致:

1. static let shared 默认被视为 MainActor 隔离,即使 class 标了
   @unchecked Sendable,从其他 actor(如 AIRuntime)同步访问仍报
   "Expression is 'async' but is not marked with 'await'".

   修法:ModelStore.shared 和 FileVault.shared 都加 nonisolated(unsafe)
   修饰,明确"任何隔离上下文都可同步访问"。

2. AIRuntime.generate() 内的 Task { ... } 继承 AIRuntime actor 隔离,
   self.recordRate 是同 actor 内部调用,不需要 await,否则报
   "No 'async' operations occur within 'await' expression".

   修法:去掉冗余的 await。

** BUILD SUCCEEDED ** 已验证。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:00:30 +08:00

88 lines
3.1 KiB
Swift

import Foundation
enum ModelKind: String, CaseIterable {
/// HuggingFace mlx-community , Models/
case llm = "Qwen3-1.7B-4bit"
case vl = "Qwen2.5-VL-3B-Instruct-4bit"
var displayName: String {
switch self {
case .llm: return "Qwen3-1.7B"
case .vl: return "Qwen2.5-VL-3B"
}
}
/// HuggingFace ID(org/name),
var huggingFaceRepo: String { "mlx-community/\(rawValue)" }
///
var sentinelFilename: String { "config.json" }
}
/// `@unchecked Sendable`:rootURL let, filesystem(线),
/// actor / Task 访
/// `nonisolated(unsafe) shared`: `-default-isolation=MainActor`
/// static MainActor , actor 访 await opt-out
final class ModelStore: @unchecked Sendable {
nonisolated(unsafe) 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)
}
}