缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。

当您提供代码差异后,我将按照以下格式生成:

```
<type>(<scope>): <subject>

<body>
```

其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
link2026
2026-06-07 14:17:18 +08:00
parent 074d99715d
commit 77a4ee1c37
66 changed files with 2676 additions and 548 deletions

View File

@@ -135,6 +135,13 @@ struct HealthExportService {
throw ServiceError.generationFailed("模型未输出任何内容")
}
// ( LLM,)
let trendBlock = Self.trendSection(snapshot.trends)
if !trendBlock.isEmpty {
generated += trendBlock
continuation.yield(.token(TokenChunk(text: trendBlock, decodeRate: 0)))
}
// Phase 4:
let export = HealthExport(
prompt: prompt,
@@ -170,6 +177,146 @@ struct HealthExportService {
}
}
/// ,
func answer(question: String,
conversation: [HealthExportDialogueTurn],
in modelContext: ModelContext) -> AsyncThrowingStream<TokenChunk, Error> {
AsyncThrowingStream { continuation in
let task = Task { @MainActor in
do {
do {
try await AIRuntime.shared.prepare()
} catch {
throw ServiceError.modelNotReady
}
let snapshot = Self.retrieveDialogueSnapshot(ctx: modelContext)
let dataJSON = Self.serializeData(snapshot: snapshot)
let transcript = HealthExportDialogueTurn.transcript(from: conversation)
let prompt = HealthExportPrompts.dialogueAnswer(
latestQuestion: question,
transcript: transcript,
dataJSON: dataJSON
)
var displayed = ""
var rawAccum = ""
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 480)
for try await chunk in stream {
try Task.checkCancellation()
rawAccum += chunk.text
let clean = Self.stripThinkBlocks(rawAccum)
if clean.count > displayed.count, clean.hasPrefix(displayed) {
let delta = String(clean.dropFirst(displayed.count))
displayed = clean
continuation.yield(TokenChunk(text: delta, decodeRate: chunk.decodeRate))
} else if clean != displayed {
displayed = clean
}
}
guard !displayed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw ServiceError.generationFailed("模型未输出任何内容")
}
continuation.finish()
} catch is CancellationError {
continuation.finish(throwing: ServiceError.cancelled)
} catch let e as ServiceError {
continuation.finish(throwing: e)
} catch {
continuation.finish(throwing: ServiceError.generationFailed("\(error)"))
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
/// HealthExport
func export(conversation: [HealthExportDialogueTurn],
in modelContext: ModelContext) -> AsyncThrowingStream<Event, Error> {
AsyncThrowingStream { continuation in
let task = Task { @MainActor in
do {
do {
try await AIRuntime.shared.prepare()
} catch {
throw ServiceError.modelNotReady
}
continuation.yield(.phaseChanged(.retrieving))
let snapshot = Self.retrieveDialogueSnapshot(ctx: modelContext)
let dataJSON = Self.serializeData(snapshot: snapshot)
let transcript = HealthExportDialogueTurn.transcript(from: conversation)
try Task.checkCancellation()
continuation.yield(.phaseChanged(.generating))
let genPrompt = HealthExportPrompts.dialogueReportGeneration(
transcript: transcript,
dataJSON: dataJSON
)
var generated = ""
var rawAccum = ""
var lastRate: Double = 0
let stream = await AIRuntime.shared.generate(prompt: genPrompt, maxTokens: 1200)
for try await chunk in stream {
try Task.checkCancellation()
if chunk.decodeRate > 0 { lastRate = chunk.decodeRate }
rawAccum += chunk.text
let clean = Self.stripThinkBlocks(rawAccum)
if clean.count > generated.count, clean.hasPrefix(generated) {
let delta = String(clean.dropFirst(generated.count))
generated = clean
continuation.yield(.token(TokenChunk(text: delta, decodeRate: chunk.decodeRate)))
} else if clean != generated {
generated = clean
}
}
guard !generated.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw ServiceError.generationFailed("模型未输出任何内容")
}
// ( LLM,)
let trendBlock = Self.trendSection(snapshot.trends)
if !trendBlock.isEmpty {
generated += trendBlock
continuation.yield(.token(TokenChunk(text: trendBlock, decodeRate: 0)))
}
let export = HealthExport(
prompt: transcript,
content: generated,
referencedIndicatorIDs: snapshot.indicators.map { Self.idString($0.persistentModelID) },
referencedReportIDs: [],
referencedSymptomIDs: [],
referencedDiaryIDs: snapshot.diaries.map { Self.idString($0.persistentModelID) },
inferredTimeFromDate: snapshot.fromDate,
inferredTimeToDate: snapshot.toDate,
inferredIntent: "dialogue_export",
inferredLabelCN: "对话整理",
modelTag: ModelKind.llm.rawValue,
decodeRate: lastRate
)
modelContext.insert(export)
do { try modelContext.save() } catch {
print("[HealthExportService] save failed: \(error)")
}
continuation.yield(.phaseChanged(.completed))
continuation.yield(.completed(persistentID: export.persistentModelID))
continuation.finish()
} catch is CancellationError {
continuation.finish(throwing: ServiceError.cancelled)
} catch let e as ServiceError {
continuation.finish(throwing: e)
} catch {
continuation.finish(throwing: ServiceError.generationFailed("\(error)"))
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
// MARK: - Phase 1: intent extraction
struct Intent: Sendable {
@@ -251,6 +398,8 @@ struct HealthExportService {
var reports: [Report]
var diaries: [DiaryEntry]
var profile: UserProfile
/// (, LLM) ##
var trends: [ExportTrend] = []
}
/// SwiftData @MainActor
@@ -265,7 +414,8 @@ struct HealthExportService {
predicate: #Predicate { $0.capturedAt >= fromDate && $0.capturedAt <= toDate },
sortBy: [SortDescriptor(\.capturedAt, order: .reverse)]
)
var indicators = (try? ctx.fetch(indDesc)) ?? []
let allInWindow = (try? ctx.fetch(indDesc)) ?? []
var indicators = allInWindow
if !intent.keywords.isEmpty {
let filtered = indicators.filter { ind in
intent.keywords.contains { kw in
@@ -328,6 +478,14 @@ struct HealthExportService {
// Profile()
let profile = UserProfileStore.loadOrCreate(in: ctx)
// (, LLM)
// in-window ; indicators series
let trends = ExportTrendBuilder.build(
allInWindow: allInWindow,
relevant: indicators,
profile: profile
)
return Snapshot(
fromDate: fromDate,
toDate: toDate,
@@ -335,8 +493,44 @@ struct HealthExportService {
symptoms: symptoms,
reports: reports,
diaries: diaries,
profile: profile,
trends: trends
)
}
/// 使 + prompt ,
static func retrieveDialogueSnapshot(ctx: ModelContext) -> Snapshot {
let indicatorDesc = FetchDescriptor<Indicator>(
sortBy: [SortDescriptor(\.capturedAt, order: .reverse)]
)
let diaryDesc = FetchDescriptor<DiaryEntry>(
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
let indicators = (try? ctx.fetch(indicatorDesc)) ?? []
let diaries = (try? ctx.fetch(diaryDesc)) ?? []
let profile = UserProfileStore.loadOrCreate(in: ctx)
let dates = indicators.map(\.capturedAt) + diaries.map(\.createdAt)
let fromDate = dates.min() ?? Date()
let toDate = dates.max() ?? Date()
// ,
let trends = ExportTrendBuilder.build(
allInWindow: indicators,
relevant: indicators,
profile: profile
)
return Snapshot(
fromDate: fromDate,
toDate: toDate,
indicators: indicators,
symptoms: [],
reports: [],
diaries: diaries,
profile: profile,
trends: trends
)
}
// MARK: - Phase 3: serialize data for prompt
@@ -480,6 +674,12 @@ struct HealthExportService {
"""
}
/// LLM ## ()
static func trendSection(_ trends: [ExportTrend]) -> String {
guard !trends.isEmpty else { return "" }
return "\n\n## 指标趋势\n" + trends.map { $0.line() }.joined(separator: "\n")
}
// MARK: - Helpers
/// SwiftData persistentModelID