Files
kangkang/康康/Services/DiaryAssistService.swift
link2026 b3777d508d 根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:
```
chore(project): 更新项目配置文件

移除未使用的依赖项并优化构建配置,
提升项目整体性能和可维护性。
```
2026-06-16 00:01:48 +08:00

159 lines
6.8 KiB
Swift

import Foundation
/// AI : LLM 3-4
///
/// HealthExportService ,(< 400 token),
/// await
///
/// :DiaryQuickSheet
@MainActor
struct DiaryAssistService {
static let shared = DiaryAssistService()
private init() {}
/// fill ,
/// `dim` ( `DiaryAssistPrompts.dimensions`),
/// `adopted` UI ;`round` UI append ,
struct Question: Identifiable, Hashable {
let id: UUID
let q: String
let fill: String
let dim: String
var adopted: Bool
var round: Int
init(id: UUID = UUID(),
q: String,
fill: String,
dim: String = "",
adopted: Bool = false,
round: Int = 0) {
self.id = id
self.q = q
self.fill = fill
self.dim = dim
self.adopted = adopted
self.round = round
}
}
enum AssistError: Error, LocalizedError {
case modelNotReady
case empty
case parseFailed(String)
var errorDescription: String? {
switch self {
case .modelNotReady: return String(appLoc: "AI 模型尚未准备好")
case .empty: return String(appLoc: "AI 没有给出建议,请稍后重试")
case .parseFailed(let m): return String(appLoc: "结果解析失败:\(m)")
}
}
}
/// 3-4
/// - coveredDimensions: ,( question.dim),
/// prompt
/// : AIRuntime actor , Capture / Export GPU
func suggest(content: String,
coveredDimensions: [String] = []) async throws -> (questions: [Question], decodeRate: Double) {
do {
try await AIRuntime.shared.prepare()
} catch {
throw AssistError.modelNotReady
}
let prompt = DiaryAssistPrompts.suggest(content: content, coveredDimensions: coveredDimensions)
// MNN JSON / {"questions":} ( MNN MLX )
// , §10.5退, AI
var lastRate: Double = 0
var parsedButEmpty = false
var lastRaw = ""
for _ in 0..<2 {
try Task.checkCancellation()
var collected = ""
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 400)
for try await chunk in stream {
collected += chunk.text
if chunk.decodeRate > 0 { lastRate = chunk.decodeRate }
}
lastRaw = collected
if let questions = Self.parseQuestions(from: collected) {
if !questions.isEmpty {
return (Array(questions.prefix(4)), lastRate)
}
parsedButEmpty = true // JSON :, .empty
}
}
// ,,便
#if DEBUG
print("[DiaryAssistService] 解析失败,原始输出 = \(lastRaw)")
#endif
throw parsedButEmpty ? AssistError.empty : AssistError.parseFailed("非 JSON 输出")
}
/// ( §3.2 退):
/// `<think>` JSON `{"questions":[]}`,
/// 退 `[{}]`(MNN ) nil(/)
/// `[]`( nil : .empty .parseFailed)
static func parseQuestions(from raw: String) -> [Question]? {
let stripped = HealthExportService.stripThinkBlocks(raw)
var rawQuestions: [[String: Any]]?
// {"questions":[]}
let objStr = CaptureService.repairJSON(CaptureService.extractJSONObject(from: stripped))
if let data = objStr.data(using: .utf8),
let dict = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let arr = dict["questions"] as? [[String: Any]] {
rawQuestions = arr
}
// 退:, [{},{}]
if rawQuestions == nil {
let arrStr = CaptureService.repairJSON(CaptureService.extractBalancedJSON(from: stripped))
if let data = arrStr.data(using: .utf8),
let arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]] {
rawQuestions = arr
}
}
guard let rawQuestions else { return nil }
return rawQuestions.compactMap { d -> Question? in
guard let q = (d["q"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines), !q.isEmpty else {
return nil
}
let fill = (d["fill"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let dim = (d["dim"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return Question(q: q, fill: fill, dim: dim)
}
}
/// 稿稿(spec 2026-06-10-voice-diary)
/// ( / ),退使,
/// suggest AIRuntime actor ,/
func organize(transcript: String) async throws -> (text: String, decodeRate: Double) {
do {
try await AIRuntime.shared.prepare()
} catch {
throw AssistError.modelNotReady
}
let prompt = DiaryAssistPrompts.organize(transcript: transcript)
var collected = ""
var lastRate: Double = 0
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 400)
for try await chunk in stream {
collected += chunk.text
if chunk.decodeRate > 0 { lastRate = chunk.decodeRate }
}
let text = HealthExportService.stripThinkBlocks(collected)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { throw AssistError.empty }
return (text, lastRate)
}
}