feat: 待办通知 10s检测 + 通知方式(Agent/系统/同时) + 时间本地化修复
This commit is contained in:
parent
e82ab6a70e
commit
f1095c3f60
@ -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",
|
||||||
|
|||||||
@ -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_at(ISO 8601 格式,例如 2026-05-01T14:45:00.000Z)。如果用户只说任务没要求提醒,不设置 remind_at。
|
- 如果用户希望被提醒,设置 remind_at。如果用户只说任务没要求提醒,不设置 remind_at。
|
||||||
- 可以附加 keywords 便于后续搜索。
|
- 可以附加 keywords 便于后续搜索。
|
||||||
|
|
||||||
2) 何时完成待办(todo complete):
|
2) 何时完成待办(todo complete):
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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()
|
// 支持三种格式:带毫秒Z、带Z、不带时区(本地时间)
|
||||||
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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user