feat: 待办通知 10s检测 + 通知方式(Agent/系统/同时) + 时间本地化修复

This commit is contained in:
JOJO 2026-05-01 05:04:32 +08:00
parent e82ab6a70e
commit f1095c3f60
7 changed files with 206 additions and 40 deletions

View File

@ -295,7 +295,7 @@
}, },
"remind_at": { "remind_at": {
"type": "string", "type": "string",
"description": "提醒触发时间ISO 8601 格式,例如 2026-05-01T14:45:00.000Z。仅 add 和 update 时有效。add 时可选不提供则不设置提醒。update 时传 null 可取消已有提醒。" "description": "提醒触发时间。设置提醒前必须先执行 date '+%Y-%m-%dT%H:%M:%S' 获取当前精确本地时间,再据此计算。使用本地时间不带时区后缀,格式如 2026-05-01T14:45:00禁止使用带 Z 或 +08:00 的格式。仅 add 和 update 时有效。add 时可选不提供则不设置提醒。update 时传 null 可取消已有提醒。"
}, },
"keywords": { "keywords": {
"type": "array", "type": "array",

View File

@ -146,7 +146,7 @@ set_identity 和 remember 的本质区别:
用户提到需要完成的任务时,使用 todo add。 用户提到需要完成的任务时,使用 todo add。
- content 必须包含明确的绝对日期如「2026年5月1日下午3点开会」禁止使用「明天」「下周三」「月底」等相对时间。当前时间是 {current_time},请据此将相对时间转换为绝对日期。 - content 必须包含明确的绝对日期如「2026年5月1日下午3点开会」禁止使用「明天」「下周三」「月底」等相对时间。当前时间是 {current_time},请据此将相对时间转换为绝对日期。
- 如果用户希望被提醒,设置 remind_atISO 8601 格式,例如 2026-05-01T14:45:00.000Z。如果用户只说任务没要求提醒,不设置 remind_at。 - 如果用户希望被提醒,设置 remind_at。如果用户只说任务没要求提醒不设置 remind_at。
- 可以附加 keywords 便于后续搜索。 - 可以附加 keywords 便于后续搜索。
2) 何时完成待办todo complete 2) 何时完成待办todo complete

View File

@ -36,6 +36,11 @@ function generateId() {
return crypto.randomUUID().slice(0, 8); return crypto.randomUUID().slice(0, 8);
} }
function formatLocalISO(d) {
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function todoTool(args) { function todoTool(args) {
const { action, content, id, remind_at, keywords } = args; const { action, content, id, remind_at, keywords } = args;
@ -50,7 +55,7 @@ function todoTool(args) {
} }
const ts = new Date(remind_at).getTime(); const ts = new Date(remind_at).getTime();
if (isNaN(ts)) { if (isNaN(ts)) {
return { success: false, error: 'remind_at 必须是有效的 ISO 8601 时间字符串,例如 2026-05-01T14:45:00.000Z' }; return { success: false, error: 'remind_at 必须是有效的 ISO 8601 时间字符串,例如 2026-05-01T14:45:00' };
} }
} }
@ -73,7 +78,7 @@ function todoTool(args) {
updated_at: now, updated_at: now,
}; };
if (remind_at !== undefined && remind_at !== null) { if (remind_at !== undefined && remind_at !== null) {
entry.remind_at = new Date(remind_at).toISOString(); entry.remind_at = formatLocalISO(new Date(remind_at));
entry.reminded = false; entry.reminded = false;
} }
memories.push(entry); memories.push(entry);
@ -139,7 +144,7 @@ function todoTool(args) {
delete memories[idx].remind_at; delete memories[idx].remind_at;
delete memories[idx].reminded; delete memories[idx].reminded;
} else { } else {
memories[idx].remind_at = new Date(remind_at).toISOString(); memories[idx].remind_at = formatLocalISO(new Date(remind_at));
memories[idx].reminded = false; memories[idx].reminded = false;
} }
} }

View File

@ -18,7 +18,8 @@ let package = Package(
.linkedFramework("Carbon"), .linkedFramework("Carbon"),
.linkedFramework("CoreGraphics"), .linkedFramework("CoreGraphics"),
.linkedFramework("AppKit"), .linkedFramework("AppKit"),
.linkedFramework("Foundation") .linkedFramework("Foundation"),
.linkedFramework("UserNotifications")
] ]
) )
] ]

View File

@ -3,6 +3,7 @@ import AVFoundation
import Speech import Speech
import SwiftUI import SwiftUI
import DynamicNotchKit import DynamicNotchKit
import UserNotifications
/// Fn Control /// Fn Control
final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDelegate { final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDelegate {
@ -45,6 +46,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory) NSApp.setActivationPolicy(.accessory)
startReminderTimer() startReminderTimer()
requestNotificationPermission()
beginPermissionFlow() beginPermissionFlow()
print("[App] Ready") print("[App] Ready")
} }
@ -493,12 +495,34 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
// MARK: - Reminders // MARK: - Reminders
/// agent_debug.log
private func remLog(_ msg: String) {
let ts = ISO8601DateFormatter().string(from: Date())
let line = "[\(ts)] \(msg)\n"
print(line, terminator: "")
//
let logPath = (Config.shared.agentWorkspace as NSString).appendingPathComponent("agent_debug.log")
if let data = line.data(using: .utf8) {
if FileManager.default.fileExists(atPath: logPath) {
if let fh = try? FileHandle(forWritingTo: URL(fileURLWithPath: logPath)) {
fh.seekToEndOfFile()
fh.write(data)
fh.closeFile()
}
} else {
try? data.write(to: URL(fileURLWithPath: logPath), options: .atomic)
}
}
}
private func startReminderTimer() { private func startReminderTimer() {
reminderTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in remLog("⏱ startReminderTimer 启动 10s 定时器")
reminderTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in
self?.checkReminders() self?.checkReminders()
} }
// 3 bridge // 3 bridge
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
self?.remLog("⏱ 首次 checkReminders启动后 3s")
self?.checkReminders() self?.checkReminders()
} }
} }
@ -522,27 +546,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
} }
private func markTodoCompleted(_ memoryId: String) { private func markTodoCompleted(_ memoryId: String) {
remLog("✅ markTodoCompleted id=\(memoryId)")
var memories = loadMemories() var memories = loadMemories()
guard let idx = memories.firstIndex(where: { ($0["id"] as? String) == memoryId }) else { return } guard let idx = memories.firstIndex(where: { ($0["id"] as? String) == memoryId }) else {
remLog("⚠️ markTodoCompleted 未找到 id=\(memoryId)")
return
}
let nowISO = ISO8601DateFormatter().string(from: Date()) let nowISO = ISO8601DateFormatter().string(from: Date())
memories[idx]["completed"] = true memories[idx]["completed"] = true
memories[idx]["completed_at"] = nowISO memories[idx]["completed_at"] = nowISO
memories[idx].removeValue(forKey: "remind_at") memories[idx].removeValue(forKey: "remind_at")
memories[idx].removeValue(forKey: "reminded") memories[idx].removeValue(forKey: "reminded")
saveMemories(memories) saveMemories(memories)
remLog("✅ markTodoCompleted 完成 id=\(memoryId)")
} }
private func checkReminders() { private func checkReminders() {
let memories = loadMemories() let memories = loadMemories()
let now = Date() let now = Date()
remLog("🔍 checkReminders 开始 memoryCount=\(memories.count) triggeredSetSize=\(triggeredReminderIds.count)")
let isoFormatter = ISO8601DateFormatter() // ZZ
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let isoFormatterZ = ISO8601DateFormatter()
let isoFormatterNoMs = ISO8601DateFormatter() isoFormatterZ.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
isoFormatterNoMs.formatOptions = [.withInternetDateTime] let isoFormatterZNoMs = ISO8601DateFormatter()
isoFormatterZNoMs.formatOptions = [.withInternetDateTime]
let localFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
f.timeZone = TimeZone.current
f.locale = Locale(identifier: "en_US_POSIX")
return f
}()
var foundDue = false var foundDue = false
for mem in memories { for (i, mem) in memories.enumerated() {
guard let type = mem["type"] as? String, type == "todo", guard let type = mem["type"] as? String, type == "todo",
let remindAtStr = mem["remind_at"] as? String, let remindAtStr = mem["remind_at"] as? String,
let content = mem["content"] as? String, let content = mem["content"] as? String,
@ -550,56 +588,121 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
continue continue
} }
var remindAt = isoFormatter.date(from: remindAtStr) let completed = (mem["completed"] as? Bool) ?? false
if remindAt == nil { if completed {
remindAt = isoFormatterNoMs.date(from: remindAtStr) remLog(" skip[\(i)] completed=true id=\(memoryId)")
continue
} }
var remindAt = isoFormatterZ.date(from: remindAtStr)
if remindAt == nil { remindAt = isoFormatterZNoMs.date(from: remindAtStr) }
if remindAt == nil { remindAt = localFormatter.date(from: remindAtStr) }
let reminded = (mem["reminded"] as? Bool) ?? false let reminded = (mem["reminded"] as? Bool) ?? false
if remindAt == nil { if remindAt == nil {
print("[Reminder] ⚠️ 无法解析 remind_at: \(remindAtStr)") remLog(" ⚠️ [\(i)] 无法解析 remind_at: \(remindAtStr)")
continue continue
} }
if remindAt! > now { if remindAt! > now {
print("[Reminder] 未到期 id=\(memoryId) remind=\(remindAtStr) now=\(now)") remLog(" ⏳ [\(i)] 未到期 id=\(memoryId) remind=\(remindAtStr)")
continue continue
} }
if reminded { if reminded {
print("[Reminder] 已提醒 id=\(memoryId)") remLog(" 🔕 [\(i)] 已提醒 id=\(memoryId)")
continue continue
} }
if triggeredReminderIds.contains(memoryId) { if triggeredReminderIds.contains(memoryId) {
print("[Reminder] 重复 id=\(memoryId)") remLog(" 🔁 [\(i)] 重复触发 id=\(memoryId)")
continue continue
} }
foundDue = true foundDue = true
triggeredReminderIds.insert(memoryId) triggeredReminderIds.insert(memoryId)
remLog(" 🎯 [\(i)] 到期! id=\(memoryId) content=\(content)")
let df = DateFormatter() let df = DateFormatter()
df.locale = Locale(identifier: "zh_CN") df.locale = Locale(identifier: "zh_CN")
df.dateFormat = "yyyy年M月d日 H:mm" df.dateFormat = "yyyy年M月d日 H:mm"
let timeStr = df.string(from: now) let timeStr = df.string(from: now)
let reminderMsg = "[系统自动发送的消息] 现在是 \(timeStr),请提醒用户:\(content)\n待办ID\(memoryId)"
print("[Reminder] ✅ 触发提醒 id=\(memoryId)") remLog("🚀 调用 triggerReminder id=\(memoryId)")
triggerReminder(message: reminderMsg, memoryId: memoryId) triggerReminder(content: content, memoryId: memoryId, timeStr: timeStr)
} }
if !foundDue && !memories.isEmpty { if !foundDue && !memories.isEmpty {
print("[Reminder] 检查完成,无到期提醒 (共\(memories.count)条)") remLog("✅ checkReminders 完成,无到期提醒")
} else if memories.isEmpty {
remLog("✅ checkReminders 完成,无记忆数据")
} }
} }
private func triggerReminder(message: String, memoryId: String) { private func requestNotificationPermission() {
print("[Reminder] triggerReminder 开始 id=\(memoryId)") remLog("🔔 requestNotificationPermission 开始")
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error {
self.remLog("🔔 通知权限请求失败: \(error.localizedDescription)")
} else {
self.remLog("🔔 通知权限 granted=\(granted)")
}
}
}
private func sendSystemNotification(content: String, memoryId: String) {
remLog("📬 sendSystemNotification id=\(memoryId) content=\(content)")
let notification = UNMutableNotificationContent()
notification.title = "⏰ 待办提醒"
notification.body = content
notification.sound = .default
let request = UNNotificationRequest(
identifier: "reminder-\(memoryId)",
content: notification,
trigger: nil
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
self.remLog("📬 系统通知发送失败: \(error.localizedDescription)")
} else {
self.remLog("📬 系统通知已发送 id=\(memoryId)")
}
}
//
UNUserNotificationCenter.current().getNotificationSettings { settings in
self.remLog("📬 通知设置 authorizationStatus=\(settings.authorizationStatus.rawValue) alert=\(settings.alertSetting.rawValue)")
}
}
private func triggerReminder(content: String, memoryId: String, timeStr: String) {
let mode = Config.shared.agentReminderMode
remLog("🎬 triggerReminder id=\(memoryId) mode=\(mode.rawValue)(\(mode.displayName))")
//
if mode == .system || mode == .both {
remLog(" → 走系统通知分支")
sendSystemNotification(content: content, memoryId: memoryId)
if mode == .system {
remLog(" → 纯系统通知模式1s 后 markTodoCompleted 并 return")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.markTodoCompleted(memoryId)
}
return
}
remLog(" → both 模式,系统通知已发,继续走 Agent 分支")
} else {
remLog(" → 走智能体通知分支")
}
//
let reminderMsg = "[系统自动发送的消息] 现在是 \(timeStr),请提醒用户:\(content)\n待办ID\(memoryId)"
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { self?.remLog("❌ triggerReminder self 已释放"); return }
let isNotchVisible = self.agentNotch?.windowController?.window?.isVisible ?? false let isNotchVisible = self.agentNotch?.windowController?.window?.isVisible ?? false
print("[Reminder] 主线程 isNotchVisible=\(isNotchVisible) agentActive=\(self.agentActive)") self.remLog(" 📍 主线程 isNotchVisible=\(isNotchVisible) agentActive=\(self.agentActive)")
// //
if self.agentNotch != nil { if self.agentNotch != nil {
self.remLog(" 🗑 销毁旧弹窗")
self.agentNotch?.hide() self.agentNotch?.hide()
self.agentNotch = nil self.agentNotch = nil
} }
@ -612,14 +715,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
self.agentTracker.newConversationMode = Config.shared.agentNewConversationMode self.agentTracker.newConversationMode = Config.shared.agentNewConversationMode
if !isNotchVisible { if !isNotchVisible {
// self.remLog(" 📤 弹窗不可见,调用 showAgentNotchInternal")
print("[Reminder] 弹窗不可见,弹出")
self.showAgentNotchInternal(isTextInput: false) self.showAgentNotchInternal(isTextInput: false)
print("[Reminder] showAgentNotchInternal 完成") self.remLog(" 📤 showAgentNotchInternal 完成")
} }
if !self.agentActive { if !self.agentActive {
// bridge self.remLog(" 🔌 bridge 未运行,启动中...")
self.agentBridge.onEvent = { [weak self] event in self.agentBridge.onEvent = { [weak self] event in
DispatchQueue.main.async { self?.handleAgentEvent(event) } DispatchQueue.main.async { self?.handleAgentEvent(event) }
} }
@ -630,18 +732,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
} }
} }
guard self.agentBridge.start() else { guard self.agentBridge.start() else {
print("[Reminder] ❌ agentBridge.start 失败") self.remLog("❌ agentBridge.start 失败")
self.agentTracker.fail("无法启动智能体进程") self.agentTracker.fail("无法启动智能体进程")
return return
} }
print("[Reminder] bridge 已启动") self.remLog(" 🔌 bridge 已启动")
self.agentActive = true self.agentActive = true
self.agentBridge.send(message: message, workspace: Config.shared.agentWorkspace, allowMode: Config.shared.agentAllowMode, modelKey: nil) self.agentBridge.send(message: reminderMsg, workspace: Config.shared.agentWorkspace, allowMode: Config.shared.agentAllowMode, modelKey: nil)
self.remLog(" 📨 bridge.send 已调用")
} else { } else {
print("[Reminder] bridge 已在运行,追加消息") self.remLog(" 🔌 bridge 已在运行,追加消息")
self.agentBridge.sendContinue(message: message) self.agentBridge.sendContinue(message: reminderMsg)
self.remLog(" 📨 bridge.sendContinue 已调用")
} }
print("[Reminder] 消息已发送")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.markTodoCompleted(memoryId) self.markTodoCompleted(memoryId)

View File

@ -25,6 +25,9 @@ final class SettingsWindow: NSWindow {
// Agent // Agent
private let voiceAutoSendCheckbox: NSButton private let voiceAutoSendCheckbox: NSButton
// Agent
private let reminderModePopup: NSPopUpButton
// Agent / // Agent /
private let assistantNameField: NSTextField private let assistantNameField: NSTextField
private let assistantTonePopup: NSPopUpButton private let assistantTonePopup: NSPopUpButton
@ -69,6 +72,7 @@ final class SettingsWindow: NSWindow {
speakCheckbox = NSButton(checkboxWithTitle: "朗读 AI 回复", target: nil, action: nil) speakCheckbox = NSButton(checkboxWithTitle: "朗读 AI 回复", target: nil, action: nil)
agentSpeakCheckbox = NSButton(checkboxWithTitle: "任务完成时朗读 AI 最终输出", target: nil, action: nil) agentSpeakCheckbox = NSButton(checkboxWithTitle: "任务完成时朗读 AI 最终输出", target: nil, action: nil)
voiceAutoSendCheckbox = NSButton(checkboxWithTitle: "语音输入后自动发送", target: nil, action: nil) voiceAutoSendCheckbox = NSButton(checkboxWithTitle: "语音输入后自动发送", target: nil, action: nil)
reminderModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
assistantNameField = NSTextField(frame: .zero) assistantNameField = NSTextField(frame: .zero)
assistantTonePopup = NSPopUpButton(frame: .zero, pullsDown: false) assistantTonePopup = NSPopUpButton(frame: .zero, pullsDown: false)
userNameField = NSTextField(frame: .zero) userNameField = NSTextField(frame: .zero)
@ -87,7 +91,7 @@ final class SettingsWindow: NSWindow {
editModelButton = NSButton(title: "编辑", target: nil, action: nil) editModelButton = NSButton(title: "编辑", target: nil, action: nil)
deleteModelButton = NSButton(title: "删除", target: nil, action: nil) deleteModelButton = NSButton(title: "删除", target: nil, action: nil)
let height: CGFloat = mode == .agent ? 860 : (mode == .assist ? 400 : 320) let height: CGFloat = mode == .agent ? 890 : (mode == .assist ? 400 : 320)
let width: CGFloat = mode == .agent ? 500 : 420 let width: CGFloat = mode == .agent ? 500 : 420
let windowRect = NSRect(x: 0, y: 0, width: width, height: height) let windowRect = NSRect(x: 0, y: 0, width: width, height: height)
super.init( super.init(
@ -424,6 +428,9 @@ final class SettingsWindow: NSWindow {
let currentConvMode = Config.shared.agentNewConversationMode let currentConvMode = Config.shared.agentNewConversationMode
newConversationModePopup.selectItem(at: convModeMap.firstIndex(of: currentConvMode) ?? 0) newConversationModePopup.selectItem(at: convModeMap.firstIndex(of: currentConvMode) ?? 0)
autoNewTokenThresholdField.stringValue = String(Config.shared.agentAutoNewTokenThreshold) autoNewTokenThresholdField.stringValue = String(Config.shared.agentAutoNewTokenThreshold)
let reminderModes = AgentReminderMode.allCases
let currentReminderMode = Config.shared.agentReminderMode
reminderModePopup.selectItem(at: reminderModes.firstIndex(of: currentReminderMode) ?? 0)
} }
private func buildAgentUI() { private func buildAgentUI() {
@ -493,6 +500,14 @@ final class SettingsWindow: NSWindow {
voiceAutoSendCheckbox.target = self voiceAutoSendCheckbox.target = self
voiceAutoSendCheckbox.action = #selector(agentVoiceAutoSendToggled) voiceAutoSendCheckbox.action = #selector(agentVoiceAutoSendToggled)
//
let reminderModeLabel = makeLabel("通知方式:")
reminderModePopup.translatesAutoresizingMaskIntoConstraints = false
reminderModePopup.addItems(withTitles: AgentReminderMode.allCases.map { $0.displayName })
let reminderModes = AgentReminderMode.allCases
let currentReminderMode = Config.shared.agentReminderMode
reminderModePopup.selectItem(at: reminderModes.firstIndex(of: currentReminderMode) ?? 0)
// //
let assistantNameLabel = makeLabel("助手名字:") let assistantNameLabel = makeLabel("助手名字:")
assistantNameField.placeholderString = "小助手" assistantNameField.placeholderString = "小助手"
@ -615,6 +630,8 @@ final class SettingsWindow: NSWindow {
content.addSubview(autoNewTokenThresholdField) content.addSubview(autoNewTokenThresholdField)
content.addSubview(agentSpeakCheckbox) content.addSubview(agentSpeakCheckbox)
content.addSubview(voiceAutoSendCheckbox) content.addSubview(voiceAutoSendCheckbox)
content.addSubview(reminderModeLabel)
content.addSubview(reminderModePopup)
content.addSubview(assistantNameLabel) content.addSubview(assistantNameLabel)
content.addSubview(assistantNameField) content.addSubview(assistantNameField)
content.addSubview(userNameLabel) content.addSubview(userNameLabel)
@ -689,9 +706,17 @@ final class SettingsWindow: NSWindow {
voiceAutoSendCheckbox.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 113), voiceAutoSendCheckbox.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 113),
voiceAutoSendCheckbox.topAnchor.constraint(equalTo: agentSpeakCheckbox.bottomAnchor, constant: 6), voiceAutoSendCheckbox.topAnchor.constraint(equalTo: agentSpeakCheckbox.bottomAnchor, constant: 6),
//
reminderModeLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
reminderModeLabel.topAnchor.constraint(equalTo: voiceAutoSendCheckbox.bottomAnchor, constant: 14),
reminderModeLabel.widthAnchor.constraint(equalToConstant: 85),
reminderModePopup.leadingAnchor.constraint(equalTo: reminderModeLabel.trailingAnchor, constant: 8),
reminderModePopup.centerYAnchor.constraint(equalTo: reminderModeLabel.centerYAnchor),
reminderModePopup.widthAnchor.constraint(equalToConstant: 200),
// //
assistantNameLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20), assistantNameLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
assistantNameLabel.topAnchor.constraint(equalTo: voiceAutoSendCheckbox.bottomAnchor, constant: 14), assistantNameLabel.topAnchor.constraint(equalTo: reminderModeLabel.bottomAnchor, constant: 14),
assistantNameLabel.widthAnchor.constraint(equalToConstant: 85), assistantNameLabel.widthAnchor.constraint(equalToConstant: 85),
assistantNameField.leadingAnchor.constraint(equalTo: assistantNameLabel.trailingAnchor, constant: 8), assistantNameField.leadingAnchor.constraint(equalTo: assistantNameLabel.trailingAnchor, constant: 8),
assistantNameField.centerYAnchor.constraint(equalTo: assistantNameLabel.centerYAnchor), assistantNameField.centerYAnchor.constraint(equalTo: assistantNameLabel.centerYAnchor),
@ -828,6 +853,12 @@ final class SettingsWindow: NSWindow {
// //
let thresholdVal = Int(autoNewTokenThresholdField.stringValue) ?? 40000 let thresholdVal = Int(autoNewTokenThresholdField.stringValue) ?? 40000
Config.shared.agentAutoNewTokenThreshold = max(1000, thresholdVal) Config.shared.agentAutoNewTokenThreshold = max(1000, thresholdVal)
//
let reminderModes = AgentReminderMode.allCases
let reminderIdx = reminderModePopup.indexOfSelectedItem
if reminderIdx >= 0, reminderIdx < reminderModes.count {
Config.shared.agentReminderMode = reminderModes[reminderIdx]
}
refreshAgentModelTable() refreshAgentModelTable()
statusLabel.stringValue = "✅ Agent 配置已保存" statusLabel.stringValue = "✅ Agent 配置已保存"
statusLabel.textColor = .systemGreen statusLabel.textColor = .systemGreen

View File

@ -69,6 +69,20 @@ enum TriggerMode: String {
case agent = "agent" // case agent = "agent" //
} }
enum AgentReminderMode: String, CaseIterable {
case agent = "agent"
case system = "system"
case both = "both"
var displayName: String {
switch self {
case .agent: return "智能体通知"
case .system: return "系统通知"
case .both: return "同时通知"
}
}
}
enum AssistOutputMode: String, CaseIterable { enum AssistOutputMode: String, CaseIterable {
case inject = "inject" // case inject = "inject" //
case notch = "notch" // case notch = "notch" //
@ -306,6 +320,18 @@ final class Config {
set { defaults.set(newValue, forKey: "agent_thinking_mode") } set { defaults.set(newValue, forKey: "agent_thinking_mode") }
} }
// Agent
var agentReminderMode: AgentReminderMode {
get {
guard let raw = defaults.string(forKey: "agent_reminder_mode"),
let mode = AgentReminderMode(rawValue: raw) else {
return .agent
}
return mode
}
set { defaults.set(newValue.rawValue, forKey: "agent_reminder_mode") }
}
// MARK: - // MARK: -
var assistantName: String { var assistantName: String {