Files
kangkang/康康/Services/ReminderService.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

163 lines
6.8 KiB
Swift

import Foundation
import UserNotifications
///
/// `metricId` iOS N weekly-repeats ,id
/// `kangkang.reminder.<metricId>.w<weekday>`,便 weekday cancel
///
/// SwiftData `MetricReminder`;,
/// SwiftData
enum ReminderService {
static let idPrefix = "kangkang.reminder."
static let customIdPrefix = "kangkang.custom."
enum AuthState: String {
case granted, denied, notDetermined, provisional
}
// MARK: - authorization
static func currentAuthState() async -> AuthState {
let settings = await UNUserNotificationCenter.current().notificationSettings()
switch settings.authorizationStatus {
case .authorized: return .granted
case .denied: return .denied
case .provisional: return .provisional
case .ephemeral: return .granted
case .notDetermined: return .notDetermined
@unknown default: return .notDetermined
}
}
/// granted/denied
@discardableResult
static func requestAuthorization() async -> AuthState {
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
if settings.authorizationStatus != .notDetermined {
return await currentAuthState()
}
do {
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
return granted ? .granted : .denied
} catch {
return .denied
}
}
// MARK: - upsert / cancel
/// metric pending , enabled//weekdays
/// `MetricReminder` save
static func sync(_ reminder: MetricReminder) async {
cancel(metricId: reminder.metricId)
guard reminder.enabled else { return }
let slots = reminder.weekdays.map { wd in
Slot(suffix: "w\(wd)",
dc: DateComponents(hour: reminder.hour, minute: reminder.minute, weekday: wd))
}
await schedule(
idBase: "\(idPrefix)\(reminder.metricId)",
title: String(appLoc: "该测\(reminder.displayName)"),
body: String(appLoc: "在「+ 新建 → 指标记录 → \(reminder.displayName)」记录一次"),
thread: "kangkang.reminder.\(reminder.metricId)",
slots: slots
)
}
/// metric pending (7 weekday ,)
static func cancel(metricId: String) {
cancelBase("\(idPrefix)\(metricId)")
}
// MARK: - (CustomReminder)
/// `CustomReminder` save
static func sync(_ reminder: CustomReminder) async {
cancel(customId: reminder.id)
guard reminder.enabled else { return }
let title = reminder.title.trimmingCharacters(in: .whitespacesAndNewlines)
let body = reminder.note.trimmingCharacters(in: .whitespacesAndNewlines)
let h = reminder.hour, m = reminder.minute
// :,(suffix ,)
var slots: [Slot] = []
for f in reminder.frequencies {
switch f {
case .daily:
slots.append(Slot(suffix: "daily", dc: DateComponents(hour: h, minute: m)))
case .weekly:
slots += reminder.weekdays.map { wd in
Slot(suffix: "w\(wd)", dc: DateComponents(hour: h, minute: m, weekday: wd))
}
case .monthly:
slots += reminder.monthlyDays.map { d in
Slot(suffix: "m\(d)", dc: DateComponents(day: d, hour: h, minute: m))
}
case .yearly:
slots.append(Slot(suffix: "yearly",
dc: DateComponents(month: reminder.month, day: reminder.dayOfMonth, hour: h, minute: m)))
}
}
await schedule(
idBase: "\(customIdPrefix)\(reminder.id.uuidString)",
title: title.isEmpty ? String(appLoc: "提醒") : title,
body: body.isEmpty ? String(appLoc: "到点啦,记得完成") : body,
thread: "\(customIdPrefix)\(reminder.id.uuidString)",
slots: slots
)
}
/// pending
static func cancel(customId: UUID) {
cancelBase("\(customIdPrefix)\(customId.uuidString)")
}
/// Me Tab
static func cancelAll() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
// MARK: -
/// :`suffix` id(`<idBase>.<suffix>`,
/// `.daily` / `.w2` / `.monthly` / `.yearly`),`dc`
private struct Slot {
let suffix: String
let dc: DateComponents
}
/// `Slot` N repeats ///
private static func schedule(idBase: String,
title: String,
body: String,
thread: String,
slots: [Slot]) async {
guard !slots.isEmpty else { return }
let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
content.threadIdentifier = thread
for slot in slots {
let trigger = UNCalendarNotificationTrigger(dateMatching: slot.dc, repeats: true)
let request = UNNotificationRequest(identifier: "\(idBase).\(slot.suffix)",
content: content,
trigger: trigger)
try? await center.add(request)
}
}
/// idBase pending ,:
/// daily / yearly / monthly + 7 weekday(w1...w7)+ 31 (m1...m31)
private static func cancelBase(_ idBase: String) {
let center = UNUserNotificationCenter.current()
var ids = ["\(idBase).daily", "\(idBase).monthly", "\(idBase).yearly"]
ids += (1...7).map { "\(idBase).w\($0)" }
ids += (1...31).map { "\(idBase).m\($0)" }
center.removePendingNotificationRequests(withIdentifiers: ids)
}
}