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,使用信号量闸门控制显存占用
- 更新文档中的技术栈说明、模块边界和周次交付计划
```
This commit is contained in:
link2026
2026-06-15 09:24:59 +08:00
parent 6c6a950140
commit 9d856fcfc4
37 changed files with 2605 additions and 430 deletions

View File

@@ -11,8 +11,10 @@ struct DiaryQuickSheet: View {
@State private var content: String = ""
@State private var createdAt: Date = .now
/// :,tag
/// :,
@State private var showMedicationScan = false
/// :( + + ),tag
@State private var showMedicationLog = false
/// : SymptomStartSheet(/,)
@State private var showSymptomStart = false
@@ -98,14 +100,20 @@ struct DiaryQuickSheet: View {
.padding(.horizontal, 20)
.padding(.bottom, 10)
// :()/ ()/ (SymptomStartSheet)
HStack(spacing: 10) {
// (2×2):()/ (MedicationLogSheet,+)/
// ()/ (SymptomStartSheet)
LazyVGrid(columns: [GridItem(.flexible(), spacing: 10),
GridItem(.flexible(), spacing: 10)], spacing: 10) {
modeCard(icon: "pencil", title: String(appLoc: "写日记"),
subtitle: String(appLoc: "文字或语音"), active: true) {
contentFocused = true
}
modeCard(icon: "pills.fill", title: String(appLoc: "拍药盒"),
subtitle: String(appLoc: "识别用药"), active: false) {
modeCard(icon: "pills.fill", title: String(appLoc: "用药"),
subtitle: String(appLoc: "记剂量与时间"), active: false) {
showMedicationLog = true
}
modeCard(icon: "camera.viewfinder", title: String(appLoc: "拍药盒"),
subtitle: String(appLoc: "识别入药品库"), active: false) {
showMedicationScan = true
}
modeCard(icon: "waveform.path.ecg", title: String(appLoc: "记症状"),
@@ -252,9 +260,9 @@ struct DiaryQuickSheet: View {
.presentationCornerRadius(Tj.Radius.xl)
.fullScreenCover(isPresented: $showMedicationScan) {
MedicationScanFlow(
onSave: { entries in
// :(线)+ ·
MedicationArchiver.archive(entries: entries, in: ctx)
onSave: { meds, images in
// (), ·
MedicationArchiver.archive(medications: meds, images: images, in: ctx)
dismiss()
},
onClose: { showMedicationScan = false }
@@ -264,6 +272,10 @@ struct DiaryQuickSheet: View {
// sheet:/;,
SymptomStartSheet()
}
.sheet(isPresented: $showMedicationLog) {
// sheet:/;()
MedicationLogSheet()
}
.onDisappear {
suggestTask?.cancel()
voiceFlowTask?.cancel()

View File

@@ -0,0 +1,133 @@
import SwiftUI
import SwiftData
/// · : ()+ +
/// `DiaryEntry.medicationTag` ,线
///
/// (`Medication`,master ):
/// sheet, / ( `SymptomStartSheet`),
struct MedicationLogSheet: View {
@Environment(\.modelContext) private var ctx
@Environment(\.dismiss) private var dismiss
@Query(sort: \Medication.updatedAt, order: .reverse)
private var library: [Medication]
/// ; nil
@State private var selectedMed: Medication?
/// (,) selectedMed
@State private var manualName = ""
@State private var dosage = ""
@State private var takenAt: Date = .now
private var resolvedName: String {
(selectedMed?.name ?? manualName).trimmingCharacters(in: .whitespacesAndNewlines)
}
private var canSave: Bool { !resolvedName.isEmpty }
var body: some View {
NavigationStack {
Form {
Section {
if library.isEmpty {
TextField(String(appLoc: "药名,如:缬沙坦胶囊"), text: $manualName)
.foregroundStyle(Tj.Palette.text)
} else {
ForEach(library) { m in
Button { select(m) } label: { medRow(m) }
.buttonStyle(.plain)
}
HStack(spacing: 8) {
Image(systemName: "pencil")
.foregroundStyle(Tj.Palette.text3)
TextField(String(appLoc: "或手动输入药名"), text: $manualName)
.foregroundStyle(Tj.Palette.text)
.onChange(of: manualName) { _, v in
if !v.trimmingCharacters(in: .whitespaces).isEmpty {
selectedMed = nil
}
}
}
}
} header: {
Text("吃了哪个药")
} footer: {
if library.isEmpty {
Text("药品库还没有药,可在「记录 · 药品库」拍药盒或手动添加。这里直接手输也行。")
}
}
Section {
TextField(String(appLoc: "剂量,如:1 片 / 80mg"), text: $dosage)
.foregroundStyle(Tj.Palette.text)
} header: {
Text("剂量")
}
Section {
DatePicker(String(appLoc: "时间"), selection: $takenAt, in: ...Date.now)
} header: {
Text("时间")
}
}
.scrollContentBackground(.hidden)
.background(Tj.Palette.sand.ignoresSafeArea())
.navigationTitle("记录用药")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button(String(appLoc: "取消")) { dismiss() }
}
ToolbarItem(placement: .topBarTrailing) {
Button(String(appLoc: "保存")) { save() }
.fontWeight(.semibold)
.disabled(!canSave)
}
}
}
}
private func medRow(_ m: Medication) -> some View {
let on = selectedMed === m
return HStack(spacing: 10) {
Image(systemName: on ? "checkmark.circle.fill" : "circle")
.foregroundStyle(on ? Tj.Palette.ink : Tj.Palette.text3)
VStack(alignment: .leading, spacing: 2) {
Text(m.name)
.foregroundStyle(Tj.Palette.text)
if !m.detailLine.isEmpty {
Text(m.detailLine)
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
}
Spacer(minLength: 0)
}
.contentShape(Rectangle())
}
private func select(_ m: Medication) {
selectedMed = m
manualName = ""
}
private func save() {
guard canSave else { return }
// content : [] · , createdAt
// TimelineEntry.firstLine / TimelineEntryDetailView.medicationLines
var line = resolvedName
if let s = selectedMed?.strength, !s.isEmpty { line += " \(s)" }
let dose = dosage.trimmingCharacters(in: .whitespacesAndNewlines)
if !dose.isEmpty { line += " · \(dose)" }
let entry = DiaryEntry(content: line, createdAt: takenAt, tags: [DiaryEntry.medicationTag])
ctx.insert(entry)
try? ctx.save()
dismiss()
}
}
#Preview {
MedicationLogSheet()
.modelContainer(for: [Medication.self, DiaryEntry.self, Asset.self], inMemory: true)
}