Files
kangkang/康康/Models/Models.swift
link2026 9d856fcfc4 ```
feat(AI): 集成MNN推理引擎替换MLX作为主AI运行时

- 引入MNN(alibaba) + Arm SME2 + CPU作为主AI运行时,支持A19/iPhone17的
  SME2和A17的NEON加速
- 添加MLX Swift作为兜底GPU推理方案,实现双后端切换机制
- 使用单一Qwen3.5-2B多模态模型(1.2GB),替代原有的LLM+VL分离架构
- 实现InferenceEngine.current引擎选择逻辑,真机默认MNN,模拟器回退MLX
- 更新AIAgent架构,通过MNNLLMBridge(ObjC++) → MNNBackend进行推理
- 修改队列机制防止并发推理导致OOM,使用信号量闸门控制显存占用
- 更新文档中的技术栈说明、模块边界和周次交付计划
```
2026-06-15 09:24:59 +08:00

542 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import CoreGraphics
import SwiftData
enum IndicatorStatus: String, Codable, CaseIterable {
case high, low, normal
}
/// manual = ;quickCapture = (VL);report =
/// manual()
enum IndicatorSource: String, Codable, CaseIterable {
case manual, quickCapture, report
var label: String {
switch self {
case .manual: return String(appLoc: "手动记录")
case .quickCapture: return String(appLoc: "指标速记")
case .report: return String(appLoc: "报告归档")
}
}
}
enum ReportType: String, Codable, CaseIterable {
case checkup, lab, imaging, prescription, other
var label: String {
switch self {
case .checkup: return String(appLoc: "体检报告")
case .lab: return String(appLoc: "化验单")
case .imaging: return String(appLoc: "影像报告")
case .prescription: return String(appLoc: "处方")
case .other: return String(appLoc: "其他")
}
}
}
@Model
final class Indicator {
var name: String
var value: String
var unit: String
var range: String
var statusRaw: String
var note: String?
var capturedAt: Date
var report: Report?
var asset: Asset?
var pinned: Bool = false
/// key, "bp.systolic" / "glucose.fasting" / "weight"
/// :IndicatorRecordSheet ;VL/Report/ nil
/// :Trends seriesKey ;Timeline ( bp.systolic + bp.diastolic )
var seriesKey: String?
/// (IndicatorSource.rawValue) SwiftData ,
var sourceRaw: String = IndicatorSource.manual.rawValue
/// VL 0-based;box (0...1)
///
var sourcePageIndex: Int?
var sourceBoxX: Double?
var sourceBoxY: Double?
var sourceBoxWidth: Double?
var sourceBoxHeight: Double?
init(name: String,
value: String,
unit: String,
range: String,
status: IndicatorStatus,
note: String? = nil,
capturedAt: Date = .now,
report: Report? = nil,
asset: Asset? = nil,
pinned: Bool = false,
seriesKey: String? = nil,
source: IndicatorSource = .manual,
sourcePageIndex: Int? = nil,
sourceBoxX: Double? = nil,
sourceBoxY: Double? = nil,
sourceBoxWidth: Double? = nil,
sourceBoxHeight: Double? = nil) {
self.name = name
self.value = value
self.unit = unit
self.range = range
self.statusRaw = status.rawValue
self.note = note
self.capturedAt = capturedAt
self.report = report
self.asset = asset
self.pinned = pinned
self.seriesKey = seriesKey
self.sourceRaw = source.rawValue
self.sourcePageIndex = sourcePageIndex
self.sourceBoxX = sourceBoxX
self.sourceBoxY = sourceBoxY
self.sourceBoxWidth = sourceBoxWidth
self.sourceBoxHeight = sourceBoxHeight
}
var status: IndicatorStatus {
IndicatorStatus(rawValue: statusRaw) ?? .normal
}
var source: IndicatorSource {
IndicatorSource(rawValue: sourceRaw) ?? .manual
}
var hasEvidenceBox: Bool {
evidenceRect != nil && sourcePageIndex != nil
}
var evidenceRect: CGRect? {
guard let x = sourceBoxX,
let y = sourceBoxY,
let width = sourceBoxWidth,
let height = sourceBoxHeight,
x >= 0, y >= 0, width > 0, height > 0,
x + width <= 1, y + height <= 1 else {
return nil
}
return CGRect(x: x, y: y, width: width, height: height)
}
}
@Model
final class Report {
var title: String
var typeRaw: String
var reportDate: Date
var institution: String?
var note: String?
var summary: String?
var pageCount: Int
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Indicator.report)
var indicators: [Indicator] = []
@Relationship(deleteRule: .cascade)
var assets: [Asset] = []
init(title: String,
type: ReportType,
reportDate: Date,
institution: String? = nil,
note: String? = nil,
summary: String? = nil,
pageCount: Int = 1,
createdAt: Date = .now) {
self.title = title
self.typeRaw = type.rawValue
self.reportDate = reportDate
self.institution = institution
self.note = note
self.summary = summary
self.pageCount = pageCount
self.createdAt = createdAt
}
var type: ReportType {
ReportType(rawValue: typeRaw) ?? .other
}
}
@Model
final class DiaryEntry {
var content: String
var createdAt: Date
var tags: [String]
/// ( 5 ://)
/// ( swiftdata-rebuild-data-loss)
/// cascade: Asset ;Vault JPEG unlink
@Relationship(deleteRule: .cascade)
var assets: [Asset] = []
init(content: String, createdAt: Date = .now, tags: [String] = []) {
self.content = content
self.createdAt = createdAt
self.tags = tags
}
}
extension DiaryEntry {
/// tag UI ,**** appLoc
/// ()线
static let medicationTag = "用药"
var isMedicationLog: Bool { tags.contains(Self.medicationTag) }
}
@Model
final class Asset {
var relativePath: String
var mimeType: String
var bytes: Int
var createdAt: Date
init(relativePath: String,
mimeType: String = "image/jpeg",
bytes: Int = 0,
createdAt: Date = .now) {
self.relativePath = relativePath
self.mimeType = mimeType
self.bytes = bytes
self.createdAt = createdAt
}
}
/// : master ()
/// 使( `DiaryEntry.medicationTag` , + ):
/// / / / ,
/// @Model SwiftData ( KangkangApp )
@Model
final class Medication {
var name: String // (,), ParsedMedication.name
var strength: String // , "80mg×7"; ""
var usage: String // , ","; ""
var note: String? // ()
var createdAt: Date
var updatedAt: Date
/// ( / / , 5 )
/// cascade: Asset ;Vault JPEG unlink( DiaryEntry.assets )
@Relationship(deleteRule: .cascade)
var assets: [Asset] = []
init(name: String,
strength: String = "",
usage: String = "",
note: String? = nil,
createdAt: Date = .now) {
self.name = name
self.strength = strength
self.usage = usage
self.note = note
self.createdAt = createdAt
self.updatedAt = createdAt
}
/// / :"80mg×7 · "()
var detailLine: String {
[strength, usage]
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
.joined(separator: " · ")
}
}
@Model
final class Symptom {
var name: String
var startedAt: Date
var endedAt: Date?
var note: String?
var severity: Int
var tags: [String]
var createdAt: Date
init(name: String,
startedAt: Date = .now,
endedAt: Date? = nil,
note: String? = nil,
severity: Int = 3,
tags: [String] = [],
createdAt: Date = .now) {
self.name = name
self.startedAt = startedAt
self.endedAt = endedAt
self.note = note
self.severity = max(1, min(5, severity))
self.tags = tags
self.createdAt = createdAt
}
var isOngoing: Bool { endedAt == nil }
var duration: TimeInterval {
(endedAt ?? .now).timeIntervalSince(startedAt)
}
}
///
/// hardcoded `MonitorMetric` IndicatorQuickSheet grid ;
/// `seriesKey` `"custom.<uuid>"`, Indicator
@Model
final class CustomMonitorMetric {
@Attribute(.unique) var seriesKey: String
var name: String
var unit: String
var lowerBound: Double?
var upperBound: Double?
var icon: String
var createdAt: Date
init(name: String,
unit: String,
lowerBound: Double? = nil,
upperBound: Double? = nil,
icon: String = "circle.fill",
createdAt: Date = .now) {
self.seriesKey = "custom.\(UUID().uuidString)"
self.name = name
self.unit = unit
self.lowerBound = lowerBound
self.upperBound = upperBound
self.icon = icon
self.createdAt = createdAt
}
var referenceRange: ClosedRange<Double>? {
guard let lo = lowerBound, let hi = upperBound, lo <= hi else { return nil }
return lo...hi
}
var rangeText: String {
guard let r = referenceRange else { return "" }
return "\(Self.format(r.lowerBound)) - \(Self.format(r.upperBound))"
}
private static func format(_ v: Double) -> String {
v.truncatingRemainder(dividingBy: 1) == 0
? String(format: "%.0f", v)
: String(format: "%.1f", v)
}
}
///
/// metric (`metricId` = `MonitorMetric.rawValue`)
/// `enabled=false`(), `ctx.delete`
@Model
final class MetricReminder {
@Attribute(.unique) var metricId: String
var displayName: String
var enabled: Bool
var hour: Int // 0...23
var minute: Int // 0...59
var weekdays: [Int] // iOS Calendar :1=, 2=, ..., 7= 7 =
var createdAt: Date
var updatedAt: Date
init(metricId: String,
displayName: String,
hour: Int = 8,
minute: Int = 0,
weekdays: [Int] = [1, 2, 3, 4, 5, 6, 7],
enabled: Bool = true,
createdAt: Date = .now) {
self.metricId = metricId
self.displayName = displayName
self.enabled = enabled
self.hour = max(0, min(23, hour))
self.minute = max(0, min(59, minute))
self.weekdays = weekdays
self.createdAt = createdAt
self.updatedAt = createdAt
}
var isEveryDay: Bool { Set(weekdays) == Set(1...7) }
var frequencyLabel: String {
if !enabled { return String(appLoc: "已关闭") }
if isEveryDay { return String(appLoc: "每天") }
if weekdays.isEmpty { return String(appLoc: "未选日") }
let names = [String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: "")]
let sorted = weekdays.sorted()
return String(appLoc: "每周 ") + sorted.map { names[$0 - 1] }.joined()
}
var timeLabel: String {
String(format: "%02d:%02d", hour, minute)
}
/// (weekday , 7 = ); false
///
func occurs(on date: Date, calendar: Calendar = .current) -> Bool {
guard enabled else { return false }
return weekdays.contains(calendar.component(.weekday, from: date))
}
}
/// ( 20:00 5 12:30 2 )
/// `MetricReminder`():,
/// (5 / 2 ) `title`
/// 沿 weekday ( 7 = ); `ReminderService`
@Model
final class CustomReminder {
/// ; weekdays; dayOfMonth; month + dayOfMonth
enum Frequency: String, CaseIterable, Sendable {
case daily, weekly, monthly, yearly
}
@Attribute(.unique) var id: UUID
var title: String // , "5"
var note: String //
var hour: Int // 0...23
var minute: Int // 0...59
var weekdays: [Int] // iOS Calendar :1=, 2=, ..., 7= 7 =
var frequencyRaw: String = "daily" // :; frequenciesRaw
var dayOfMonth: Int = 1 // yearly + monthly ,1...31
var month: Int = 1 // yearly ,1...12
/// (["daily","weekly",...]) = ,退 frequency
var frequenciesRaw: [String] = []
/// (1...31) = ,退 dayOfMonth
var monthDays: [Int] = []
var enabled: Bool
var createdAt: Date
var updatedAt: Date
init(id: UUID = UUID(),
title: String,
note: String = "",
hour: Int = 8,
minute: Int = 0,
weekdays: [Int] = [1, 2, 3, 4, 5, 6, 7],
frequency: Frequency = .daily,
dayOfMonth: Int = 1,
month: Int = 1,
enabled: Bool = true,
createdAt: Date = .now) {
self.id = id
self.title = title
self.note = note
self.hour = max(0, min(23, hour))
self.minute = max(0, min(59, minute))
self.weekdays = weekdays
self.frequencyRaw = frequency.rawValue
self.dayOfMonth = max(1, min(31, dayOfMonth))
self.month = max(1, min(12, month))
self.enabled = enabled
self.createdAt = createdAt
self.updatedAt = createdAt
}
var isEveryDay: Bool { Set(weekdays) == Set(1...7) }
var frequency: Frequency {
get { Frequency(rawValue: frequencyRaw) ?? .daily }
set { frequencyRaw = newValue.rawValue }
}
/// ()frequenciesRaw 退 frequency( / init)
var frequencies: Set<Frequency> {
get {
let parsed = Set(frequenciesRaw.compactMap { Frequency(rawValue: $0) })
return parsed.isEmpty ? [frequency] : parsed
}
set {
frequenciesRaw = newValue.map(\.rawValue).sorted()
// , frequency
if let rep = newValue.map(\.rawValue).sorted().first { frequencyRaw = rep }
}
}
/// (,1...31)monthDays 退 dayOfMonth()
/// : dayOfMonth yearly ,+
var monthlyDays: [Int] {
get { monthDays.isEmpty ? [dayOfMonth] : monthDays.sorted() }
set { monthDays = Set(newValue.map { max(1, min(31, $0)) }).sorted() }
}
/// : · , · 1·15
/// ()
var frequencyLabel: String {
if !enabled { return String(appLoc: "已关闭") }
let active = frequencies
if active.contains(.daily) { return String(appLoc: "每天") }
// weekly 7
if active == [.weekly] && isEveryDay { return String(appLoc: "每天") }
let order: [Frequency] = [.weekly, .monthly, .yearly]
let parts = order.filter { active.contains($0) }.map { freqPartLabel($0) }
return parts.isEmpty ? String(appLoc: "未选日") : parts.joined(separator: " · ")
}
private func freqPartLabel(_ f: Frequency) -> String {
switch f {
case .daily:
return String(appLoc: "每天")
case .weekly:
if isEveryDay { return String(appLoc: "每天") }
if weekdays.isEmpty { return String(appLoc: "未选日") }
let names = [String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: "")]
return String(appLoc: "每周 ") + weekdays.sorted().map { names[$0 - 1] }.joined()
case .monthly:
let days = monthlyDays
if days.isEmpty { return String(appLoc: "未选日") }
return String(appLoc: "每月") + days.map { String($0) }.joined(separator: "·") + String(appLoc: "")
case .yearly:
return String(appLoc: "每年\(month)\(dayOfMonth)")
}
}
var timeLabel: String {
String(format: "%02d:%02d", hour, minute)
}
/// (,); false
/// monthly/yearly ( 31 ) false,
/// iOS
func occurs(on date: Date, calendar: Calendar = .current) -> Bool {
guard enabled else { return false }
let c = calendar.dateComponents([.weekday, .day, .month], from: date)
let wd = c.weekday ?? -1, day = c.day ?? -1, mo = c.month ?? -1
// :
for f in frequencies {
switch f {
case .daily: return true
case .weekly: if weekdays.contains(wd) { return true }
case .monthly: if monthlyDays.contains(day) { return true }
case .yearly: if month == mo && dayOfMonth == day { return true }
}
}
return false
}
}
@Model
final class ChatTurn {
var question: String
var answer: String
var referencedIndicatorIDs: [String]
var referencedReportIDs: [String]
var createdAt: Date
var decodeRate: Double
init(question: String,
answer: String,
referencedIndicatorIDs: [String] = [],
referencedReportIDs: [String] = [],
createdAt: Date = .now,
decodeRate: Double = 0) {
self.question = question
self.answer = answer
self.referencedIndicatorIDs = referencedIndicatorIDs
self.referencedReportIDs = referencedReportIDs
self.createdAt = createdAt
self.decodeRate = decodeRate
}
}