Files
kangkang/体己/Persistence/FileVault.swift
link2026 ef0fbeac97 fix(ai,persistence): ModelStore + FileVault 标 @unchecked Sendable
Xcode 26 默认开启 Swift 6 严格并发检查。AIRuntime(actor)
调用 ModelStore.shared.isReady(...) 跨 actor 边界,因 ModelStore
非 Sendable 而编译报错"Expression is 'async' but is not marked
with 'await'; this is an error in the Swift 6 language mode"。

两个类的内部状态只读(rootURL: let),方法只做线程安全的
filesystem I/O,符合 Sendable 语义,标 @unchecked Sendable
即可,不必加锁或重构。

修复目标错误:
- AIRuntime.swift:48 - guard ModelStore.shared.isReady(.llm) ...
- 后续 CaptureService 调 FileVault.shared.writeJPEG 同样路径

不影响:
- HomeView/B5ResultView 里 Text "+" 的 macOS 26.0 deprecation 是
  warning,不阻塞 build,留待 UI polish 周清理

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

101 lines
3.0 KiB
Swift

import Foundation
import UIKit
enum FileVaultError: Error {
case readFailed
case writeFailed
case removeFailed
case decodeFailed
}
/// `@unchecked Sendable`:rootURL let, I/O (线),
/// actor / Task 访
final class FileVault: @unchecked Sendable {
static let shared: FileVault = {
do {
let appSupport = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let vaultURL = appSupport.appendingPathComponent("Vault", isDirectory: true)
return try FileVault(rootURL: vaultURL)
} catch {
fatalError("FileVault.shared init failed: \(error)")
}
}()
let rootURL: URL
init(rootURL: URL) throws {
self.rootURL = rootURL
try FileManager.default.createDirectory(
at: rootURL,
withIntermediateDirectories: true,
attributes: [.protectionKey: FileProtectionType.complete]
)
}
struct SavedAsset {
let relativePath: String
let bytes: Int
}
// MARK: - Path Safety
private func resolveSafePath(_ relativePath: String) throws -> URL {
guard !relativePath.contains("/"),
!relativePath.contains(".."),
!relativePath.isEmpty else {
throw FileVaultError.readFailed
}
let url = rootURL.appendingPathComponent(relativePath)
guard url.path.hasPrefix(rootURL.path) else {
throw FileVaultError.readFailed
}
return url
}
// MARK: - Public API
func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset {
guard let data = image.jpegData(compressionQuality: quality) else {
throw FileVaultError.writeFailed
}
let filename = "\(UUID().uuidString).jpg"
let url = rootURL.appendingPathComponent(filename)
try data.write(to: url, options: [.atomic, .completeFileProtection])
return SavedAsset(relativePath: filename, bytes: data.count)
}
func loadImage(relativePath: String) throws -> UIImage {
let url = try resolveSafePath(relativePath)
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
throw FileVaultError.readFailed
}
guard let image = UIImage(data: data) else { throw FileVaultError.decodeFailed }
return image
}
func remove(relativePath: String) throws {
let url = try resolveSafePath(relativePath)
do {
try FileManager.default.removeItem(at: url)
} catch {
throw FileVaultError.removeFailed
}
}
func wipe() throws {
let fm = FileManager.default
let contents = try fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil)
for url in contents {
try fm.removeItem(at: url)
}
}
}