feat: 待办通知 10s检测 + 通知方式(Agent/系统/同时) + 时间本地化修复
This commit is contained in:
parent
e82ab6a70e
commit
f1095c3f60
@ -295,7 +295,7 @@
|
||||
},
|
||||
"remind_at": {
|
||||
"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": {
|
||||
"type": "array",
|
||||
|
||||
@ -146,7 +146,7 @@ set_identity 和 remember 的本质区别:
|
||||
用户提到需要完成的任务时,使用 todo add。
|
||||
|
||||
- content 必须包含明确的绝对日期(如「2026年5月1日下午3点开会」),禁止使用「明天」「下周三」「月底」等相对时间。当前时间是 {current_time},请据此将相对时间转换为绝对日期。
|
||||
- 如果用户希望被提醒,设置 remind_at(ISO 8601 格式,例如 2026-05-01T14:45:00.000Z)。如果用户只说任务没要求提醒,不设置 remind_at。
|
||||
- 如果用户希望被提醒,设置 remind_at。如果用户只说任务没要求提醒,不设置 remind_at。
|
||||
- 可以附加 keywords 便于后续搜索。
|
||||
|
||||
2) 何时完成待办(todo complete):
|
||||
|
||||
@ -36,6 +36,11 @@ function generateId() {
|
||||
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) {
|
||||
const { action, content, id, remind_at, keywords } = args;
|
||||
|
||||
@ -50,7 +55,7 @@ function todoTool(args) {
|
||||
}
|
||||
const ts = new Date(remind_at).getTime();
|
||||
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,
|
||||
};
|
||||
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;
|
||||
}
|
||||
memories.push(entry);
|
||||
@ -139,7 +144,7 @@ function todoTool(args) {
|
||||
delete memories[idx].remind_at;
|
||||
delete memories[idx].reminded;
|
||||
} else {
|
||||
memories[idx].remind_at = new Date(remind_at).toISOString();
|
||||
memories[idx].remind_at = formatLocalISO(new Date(remind_at));
|
||||
memories[idx].reminded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,8 @@ let package = Package(
|
||||
.linkedFramework("Carbon"),
|
||||
.linkedFramework("CoreGraphics"),
|
||||
.linkedFramework("AppKit"),
|
||||
.linkedFramework("Foundation")
|
||||
.linkedFramework("Foundation"),
|
||||
.linkedFramework("UserNotifications")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@ -3,6 +3,7 @@ import AVFoundation
|
||||
import Speech
|
||||
import SwiftUI
|
||||
import DynamicNotchKit
|
||||
import UserNotifications
|
||||
|
||||
/// 应用主控制器,协调 Fn 键(长按录音)和 Control 键(双击切换模式)
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDelegate {
|
||||
@ -45,6 +46,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.accessory)
|
||||
startReminderTimer()
|
||||
requestNotificationPermission()
|
||||
beginPermissionFlow()
|
||||
print("[App] Ready")
|
||||
}
|
||||
@ -493,12 +495,34 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
||||
|
||||
// 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() {
|
||||
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()
|
||||
}
|
||||
// 启动后延迟 3 秒首次检查(等 bridge 就绪)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
self?.remLog("⏱ 首次 checkReminders(启动后 3s)")
|
||||
self?.checkReminders()
|
||||
}
|
||||
}
|
||||
@ -522,27 +546,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
||||
}
|
||||
|
||||
private func markTodoCompleted(_ memoryId: String) {
|
||||
remLog("✅ markTodoCompleted id=\(memoryId)")
|
||||
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())
|
||||
memories[idx]["completed"] = true
|
||||
memories[idx]["completed_at"] = nowISO
|
||||
memories[idx].removeValue(forKey: "remind_at")
|
||||
memories[idx].removeValue(forKey: "reminded")
|
||||
saveMemories(memories)
|
||||
remLog("✅ markTodoCompleted 完成 id=\(memoryId)")
|
||||
}
|
||||
|
||||
private func checkReminders() {
|
||||
let memories = loadMemories()
|
||||
let now = Date()
|
||||
remLog("🔍 checkReminders 开始 memoryCount=\(memories.count) triggeredSetSize=\(triggeredReminderIds.count)")
|
||||
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let isoFormatterNoMs = ISO8601DateFormatter()
|
||||
isoFormatterNoMs.formatOptions = [.withInternetDateTime]
|
||||
// 支持三种格式:带毫秒Z、带Z、不带时区(本地时间)
|
||||
let isoFormatterZ = ISO8601DateFormatter()
|
||||
isoFormatterZ.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
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
|
||||
for mem in memories {
|
||||
for (i, mem) in memories.enumerated() {
|
||||
guard let type = mem["type"] as? String, type == "todo",
|
||||
let remindAtStr = mem["remind_at"] as? String,
|
||||
let content = mem["content"] as? String,
|
||||
@ -550,56 +588,121 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
||||
continue
|
||||
}
|
||||
|
||||
var remindAt = isoFormatter.date(from: remindAtStr)
|
||||
if remindAt == nil {
|
||||
remindAt = isoFormatterNoMs.date(from: remindAtStr)
|
||||
let completed = (mem["completed"] as? Bool) ?? false
|
||||
if completed {
|
||||
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
|
||||
|
||||
if remindAt == nil {
|
||||
print("[Reminder] ⚠️ 无法解析 remind_at: \(remindAtStr)")
|
||||
remLog(" ⚠️ [\(i)] 无法解析 remind_at: \(remindAtStr)")
|
||||
continue
|
||||
}
|
||||
if remindAt! > now {
|
||||
print("[Reminder] 未到期 id=\(memoryId) remind=\(remindAtStr) now=\(now)")
|
||||
remLog(" ⏳ [\(i)] 未到期 id=\(memoryId) remind=\(remindAtStr)")
|
||||
continue
|
||||
}
|
||||
if reminded {
|
||||
print("[Reminder] 已提醒 id=\(memoryId)")
|
||||
remLog(" 🔕 [\(i)] 已提醒 id=\(memoryId)")
|
||||
continue
|
||||
}
|
||||
if triggeredReminderIds.contains(memoryId) {
|
||||
print("[Reminder] 重复 id=\(memoryId)")
|
||||
remLog(" 🔁 [\(i)] 重复触发 id=\(memoryId)")
|
||||
continue
|
||||
}
|
||||
|
||||
foundDue = true
|
||||
triggeredReminderIds.insert(memoryId)
|
||||
remLog(" 🎯 [\(i)] 到期! id=\(memoryId) content=\(content)")
|
||||
|
||||
let df = DateFormatter()
|
||||
df.locale = Locale(identifier: "zh_CN")
|
||||
df.dateFormat = "yyyy年M月d日 H:mm"
|
||||
let timeStr = df.string(from: now)
|
||||
let reminderMsg = "[系统自动发送的消息] 现在是 \(timeStr),请提醒用户:\(content)\n(待办ID:\(memoryId))"
|
||||
|
||||
print("[Reminder] ✅ 触发提醒 id=\(memoryId)")
|
||||
triggerReminder(message: reminderMsg, memoryId: memoryId)
|
||||
remLog("🚀 调用 triggerReminder id=\(memoryId)")
|
||||
triggerReminder(content: content, memoryId: memoryId, timeStr: timeStr)
|
||||
}
|
||||
if !foundDue && !memories.isEmpty {
|
||||
print("[Reminder] 检查完成,无到期提醒 (共\(memories.count)条)")
|
||||
remLog("✅ checkReminders 完成,无到期提醒")
|
||||
} else if memories.isEmpty {
|
||||
remLog("✅ checkReminders 完成,无记忆数据")
|
||||
}
|
||||
}
|
||||
|
||||
private func triggerReminder(message: String, memoryId: String) {
|
||||
print("[Reminder] triggerReminder 开始 id=\(memoryId)")
|
||||
private func requestNotificationPermission() {
|
||||
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
|
||||
guard let self = self else { return }
|
||||
guard let self = self else { self?.remLog("❌ triggerReminder self 已释放"); return }
|
||||
|
||||
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 {
|
||||
self.remLog(" 🗑 销毁旧弹窗")
|
||||
self.agentNotch?.hide()
|
||||
self.agentNotch = nil
|
||||
}
|
||||
@ -612,14 +715,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
||||
self.agentTracker.newConversationMode = Config.shared.agentNewConversationMode
|
||||
|
||||
if !isNotchVisible {
|
||||
// 弹窗不可见 → 弹出弹窗
|
||||
print("[Reminder] 弹窗不可见,弹出")
|
||||
self.remLog(" 📤 弹窗不可见,调用 showAgentNotchInternal")
|
||||
self.showAgentNotchInternal(isTextInput: false)
|
||||
print("[Reminder] showAgentNotchInternal 完成")
|
||||
self.remLog(" 📤 showAgentNotchInternal 完成")
|
||||
}
|
||||
|
||||
if !self.agentActive {
|
||||
// 需要启动 bridge
|
||||
self.remLog(" 🔌 bridge 未运行,启动中...")
|
||||
self.agentBridge.onEvent = { [weak self] event in
|
||||
DispatchQueue.main.async { self?.handleAgentEvent(event) }
|
||||
}
|
||||
@ -630,18 +732,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
|
||||
}
|
||||
}
|
||||
guard self.agentBridge.start() else {
|
||||
print("[Reminder] ❌ agentBridge.start 失败")
|
||||
self.remLog("❌ agentBridge.start 失败")
|
||||
self.agentTracker.fail("无法启动智能体进程")
|
||||
return
|
||||
}
|
||||
print("[Reminder] bridge 已启动")
|
||||
self.remLog(" 🔌 bridge 已启动")
|
||||
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 {
|
||||
print("[Reminder] bridge 已在运行,追加消息")
|
||||
self.agentBridge.sendContinue(message: message)
|
||||
self.remLog(" 🔌 bridge 已在运行,追加消息")
|
||||
self.agentBridge.sendContinue(message: reminderMsg)
|
||||
self.remLog(" 📨 bridge.sendContinue 已调用")
|
||||
}
|
||||
print("[Reminder] 消息已发送")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.markTodoCompleted(memoryId)
|
||||
|
||||
@ -25,6 +25,9 @@ final class SettingsWindow: NSWindow {
|
||||
// Agent 模式专属:语音自动发送开关
|
||||
private let voiceAutoSendCheckbox: NSButton
|
||||
|
||||
// Agent 模式专属:通知方式
|
||||
private let reminderModePopup: NSPopUpButton
|
||||
|
||||
// Agent 模式专属:助手名字/语气
|
||||
private let assistantNameField: NSTextField
|
||||
private let assistantTonePopup: NSPopUpButton
|
||||
@ -69,6 +72,7 @@ final class SettingsWindow: NSWindow {
|
||||
speakCheckbox = NSButton(checkboxWithTitle: "朗读 AI 回复", target: nil, action: nil)
|
||||
agentSpeakCheckbox = NSButton(checkboxWithTitle: "任务完成时朗读 AI 最终输出", target: nil, action: nil)
|
||||
voiceAutoSendCheckbox = NSButton(checkboxWithTitle: "语音输入后自动发送", target: nil, action: nil)
|
||||
reminderModePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||||
assistantNameField = NSTextField(frame: .zero)
|
||||
assistantTonePopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||||
userNameField = NSTextField(frame: .zero)
|
||||
@ -87,7 +91,7 @@ final class SettingsWindow: NSWindow {
|
||||
editModelButton = 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 windowRect = NSRect(x: 0, y: 0, width: width, height: height)
|
||||
super.init(
|
||||
@ -424,6 +428,9 @@ final class SettingsWindow: NSWindow {
|
||||
let currentConvMode = Config.shared.agentNewConversationMode
|
||||
newConversationModePopup.selectItem(at: convModeMap.firstIndex(of: currentConvMode) ?? 0)
|
||||
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() {
|
||||
@ -493,6 +500,14 @@ final class SettingsWindow: NSWindow {
|
||||
voiceAutoSendCheckbox.target = self
|
||||
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("助手名字:")
|
||||
assistantNameField.placeholderString = "小助手"
|
||||
@ -615,6 +630,8 @@ final class SettingsWindow: NSWindow {
|
||||
content.addSubview(autoNewTokenThresholdField)
|
||||
content.addSubview(agentSpeakCheckbox)
|
||||
content.addSubview(voiceAutoSendCheckbox)
|
||||
content.addSubview(reminderModeLabel)
|
||||
content.addSubview(reminderModePopup)
|
||||
content.addSubview(assistantNameLabel)
|
||||
content.addSubview(assistantNameField)
|
||||
content.addSubview(userNameLabel)
|
||||
@ -689,9 +706,17 @@ final class SettingsWindow: NSWindow {
|
||||
voiceAutoSendCheckbox.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 113),
|
||||
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.topAnchor.constraint(equalTo: voiceAutoSendCheckbox.bottomAnchor, constant: 14),
|
||||
assistantNameLabel.topAnchor.constraint(equalTo: reminderModeLabel.bottomAnchor, constant: 14),
|
||||
assistantNameLabel.widthAnchor.constraint(equalToConstant: 85),
|
||||
assistantNameField.leadingAnchor.constraint(equalTo: assistantNameLabel.trailingAnchor, constant: 8),
|
||||
assistantNameField.centerYAnchor.constraint(equalTo: assistantNameLabel.centerYAnchor),
|
||||
@ -828,6 +853,12 @@ final class SettingsWindow: NSWindow {
|
||||
// 保存自动新建阈值
|
||||
let thresholdVal = Int(autoNewTokenThresholdField.stringValue) ?? 40000
|
||||
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()
|
||||
statusLabel.stringValue = "✅ Agent 配置已保存"
|
||||
statusLabel.textColor = .systemGreen
|
||||
|
||||
@ -69,6 +69,20 @@ enum TriggerMode: String {
|
||||
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 {
|
||||
case inject = "inject" // 直接输入到输入栏
|
||||
case notch = "notch" // 刘海弹窗展示
|
||||
@ -306,6 +320,18 @@ final class Config {
|
||||
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: - 助手个性化配置
|
||||
|
||||
var assistantName: String {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user