feat(Capture): 归档后后台预生成大白话摘要,详情页秒开 + 兜底重试

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
link2026
2026-06-10 07:12:48 +08:00
parent 0a824610cf
commit 43cdde9bab
5 changed files with 184 additions and 8 deletions

View File

@@ -0,0 +1,61 @@
import Foundation
import SwiftData
/// (§3.1: AIRuntime,UI )
/// :();
/// : summary VL
@MainActor
final class ReportInsightService {
static let shared = ReportInsightService()
private init() {}
/// ID,
private var inFlight: Set<String> = []
func pregenerateIfNeeded(report: Report, in ctx: ModelContext) async {
guard (report.summary ?? "").isEmpty, !report.indicators.isEmpty else { return }
let key = String(describing: report.persistentModelID)
guard !inFlight.contains(key) else { return }
inFlight.insert(key)
defer { inFlight.remove(key) }
do {
try await AIRuntime.shared.prepare()
} catch {
return // :,
}
let prompt = InsightPrompts.reportPlainSummary(
title: report.title,
typeLabel: report.type.label,
indicatorLines: Self.indicatorLines(for: report.indicators)
)
var collected = ""
do {
let stream = await AIRuntime.shared.generate(
prompt: prompt, maxTokens: 200, priority: .background)
for try await chunk in stream { collected += chunk.text }
} catch {
return // (CancellationError):,
}
let text = HealthExportService.stripThinkBlocks(collected)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, (report.summary ?? "").isEmpty else { return }
report.summary = text
try? ctx.save()
}
/// ( range)status;, 15 prompt
static func indicatorLines(for indicators: [Indicator]) -> String {
let sorted = indicators.sorted {
($0.status == .normal ? 1 : 0) < ($1.status == .normal ? 1 : 0)
}
return sorted.prefix(15).map { i in
var line = "\(i.name) \(i.value)"
if !i.unit.isEmpty { line += " \(i.unit)" }
if !i.range.isEmpty { line += "(参考 \(i.range))" }
line += " \(i.status.rawValue)"
return line
}.joined(separator: "\n")
}
}