fix(core): 代码审查修复 AI 并发/隐私/解析等多处缺陷

- AIRuntime 加 actor 内串行推理闸门,封死 LLM/VL in-flight 并发解码窄口(jetsam OOM 根因)
- prepare 的 .loading 改轮询等待消除假就绪竞态;就绪判据 isReady→isComplete 防半下载崩溃
- applyReanalyzed 重新解读时 unlink 旧 Asset,消除 Vault 孤儿图片(§6 隐私承诺)
- parseReportJSON 改 extractBalancedJSON + 裸数组兜底,防 VL 多项输出被静默截断丢指标
- 临时文件改 completeUnlessOpen 修锁屏写失败;parseDate 支持多格式防归档年份错位
- TimelineEntry/DayDetailSheet 修「偏高」文案与血压箭头方向(偏低指标不再显示相反结论)
- FileVault.wipe 容错;HealthExportSheet 异常关键词排除否定句;modelTag 取实际枚举值
- 删除 B1-B5 + ArchiveFlow 死代码(含违反 §6 的 AES 加密文案)
- 补 3 个回归测试,编译 + 测试全部通过
This commit is contained in:
link2026
2026-06-01 07:43:49 +08:00
parent 32e7c25ed7
commit bff7cfd4b6
16 changed files with 185 additions and 1204 deletions

View File

@@ -32,6 +32,40 @@ actor AIRuntime {
private var llmSession: LLMSession?
private var vlSession: VLSession?
// MARK: - (§3.1 OOM )
//
// actor , generate() Task;
// analyzeReport await actor,LLM VL,
// GPU App jetsam
//(MEMORY in-flight )
//
// actor (count = 1):( + )
// await acquireGate(), releaseGate()actor
// gateBusy / gateWaiters
private var gateBusy = false
private var gateWaiters: [CheckedContinuation<Void, Never>] = []
private func acquireGate() async {
if !gateBusy {
gateBusy = true
return
}
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
gateWaiters.append(cont)
}
// releaseGate (gateBusy true)
}
private func releaseGate() {
if gateWaiters.isEmpty {
gateBusy = false
} else {
// ,gateBusy true,
let next = gateWaiters.removeFirst()
next.resume()
}
}
private init() {}
/// App : MLX GPU , reuse cache
@@ -46,25 +80,30 @@ actor AIRuntime {
/// ,
func prepare() async throws {
switch status {
case .ready:
return
case .loading:
// ; prepare ,
// await prepare() status, / UI
// W3 prepare
return
case .error, .notReady:
break
// ,
// return: ready, generate
// `guard status == .ready` ()
while status == .loading {
try await Task.sleep(nanoseconds: 80_000_000)
}
if status == .ready { return }
guard ModelStore.shared.isReady(.llm) else {
// isComplete() isReady( config.json):config.json ,
// isReady true safetensors ModelDownloadService
// ( isComplete)
guard ModelStore.shared.isComplete(for: .llm) else {
status = .error("LLM 模型未就绪")
throw AIRuntimeError.notReady
}
// :( VL ), VL + LLM,
// VL + LLM OOM
await acquireGate()
defer { releaseGate() }
// :, load
if status == .ready { return }
// OOM (§3.1):LLM(~1GB) VL(~3GB), App jetsam
// LLM VL, ModelContainer + MLX
unloadVL()
status = .loading
@@ -93,6 +132,8 @@ actor AIRuntime {
continuation.finish(throwing: AIRuntimeError.notReady)
return
}
// : LLM VL / ,
await self.acquireGate()
do {
// session.generate actor , await
let stream = await session.generate(prompt: prompt, maxTokens: maxTokens)
@@ -109,6 +150,9 @@ actor AIRuntime {
} catch {
continuation.finish(throwing: AIRuntimeError.inferenceFailed("\(error)"))
}
// / / (checkCancellation catch ),
// ,
self.releaseGate()
}
// / Task( LLMSession / HealthExportService )
continuation.onTermination = { _ in task.cancel() }
@@ -123,20 +167,24 @@ actor AIRuntime {
/// VL , load
func prepareVL() async throws {
switch vlStatus {
case .ready, .loading:
return
case .error, .notReady:
break
while vlStatus == .loading {
try await Task.sleep(nanoseconds: 80_000_000)
}
if vlStatus == .ready { return }
guard ModelStore.shared.isReady(.vl) else {
// prepare(): isComplete (),
guard ModelStore.shared.isComplete(for: .vl) else {
vlStatus = .error("VL 模型未就绪")
throw AIRuntimeError.notReady
}
// OOM (§3.1): VL(~3GB) LLM(~1GB), jetsam
// App 退
// :( LLM ), LLM + VL
// App 退
await acquireGate()
defer { releaseGate() }
if vlStatus == .ready { return }
// OOM (§3.1): VL(~3GB) LLM(~1GB), jetsam
unloadLLM()
vlStatus = .loading
@@ -155,8 +203,7 @@ actor AIRuntime {
// MARK: - (OOM )
/// LLM, ModelContainer MLX
/// : generate() , session ,;
/// /,
/// :(prepareVL ), LLM ,
private func unloadLLM() {
guard llmSession != nil else { return }
llmSession = nil
@@ -174,13 +221,15 @@ actor AIRuntime {
/// JSON ( VLPrompts.reportExtraction )
/// + 退(§3.2)
/// AIRuntime actor, LLM.generate() , OOM
/// LLM.generate() , OOM
func analyzeReport(imageURLs: [URL],
prompt: String,
maxTokens: Int = 512) async throws -> String {
guard vlStatus == .ready, let session = vlSession else {
throw AIRuntimeError.notReady
}
await acquireGate()
defer { releaseGate() }
do {
return try await session.analyze(
imageURLs: imageURLs,