feat(Capture): 归档后后台预生成大白话摘要,详情页秒开 + 兜底重试
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
61
康康/Services/ReportInsightService.swift
Normal file
61
康康/Services/ReportInsightService.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user