From 967da1b8899643c081a3c6bd27bd7cf0257ba3fb Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Fri, 1 May 2026 01:19:12 +0800 Subject: [PATCH] =?UTF-8?q?debug:=20=E6=B7=BB=E5=8A=A0=20Reminder=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=97=A5=E5=BF=97=E6=8E=92=E6=9F=A5=20DMG=20?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E6=8F=90=E9=86=92=E5=A4=B1=E6=95=88=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppDelegate: 新增 reminderLog() 写入 ~/Library/Logs/VoiceInput/reminder.log - AppDelegate: 修复 agentLog 路径使用用户 Logs 目录 - AppDelegate: 启动时记录 bundlePath/agentDir/memoryPath 等诊断信息 - AgentBridge: init 不清空历史日志,start() 失败时输出详细错误 - checkReminders/triggerReminder 全链路覆盖文件日志 --- Sources/App/AppDelegate.swift | 227 ++++++++++++++++++++++++++++++++- Sources/Core/AgentBridge.swift | 34 +++-- 2 files changed, 252 insertions(+), 9 deletions(-) diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 7bdd240..03e8398 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -32,6 +32,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel private var agentIsTextMode = false private var agentExitingCleanly = false // 主动关闭标记,避免误报退出错误 private var autoHideWorkItem: DispatchWorkItem? + private var reminderTimer: Timer? + private var triggeredReminderIds = Set() // 防止重复弹窗 override init() { agentBridge = AgentBridge(agentDir: Config.shared.agentDir) @@ -42,6 +44,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) + + // 启动诊断日志 + reminderLog("======== VoiceInput 启动 ========") + reminderLog("bundlePath: \(Bundle.main.bundlePath)") + reminderLog("resourcePath: \(Bundle.main.resourcePath ?? "nil")") + reminderLog("agentDir: \(Config.shared.agentDir)") + reminderLog("memoryPath: \(Config.shared.memoryPath)") + reminderLog("memoryExists: \(FileManager.default.fileExists(atPath: Config.shared.memoryPath))") + reminderLog("logDir: \(logDir)") + reminderLog("agentModelsPath: \(Config.shared.userAgentModelsPath)") + reminderLog("agentModelsExists: \(FileManager.default.fileExists(atPath: Config.shared.userAgentModelsPath))") + + startReminderTimer() beginPermissionFlow() print("[App] Ready") } @@ -440,11 +455,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel case .error(let message): agentLog("[App] handleEvent error message=\(message)") agentTracker.fail(message) + case .reminderTriggered(let memoryId, let content): + agentLog("[App] handleEvent reminderTriggered id=\(memoryId) content=\(content)") + // bridge 快速通道已注入消息到对话,无需额外处理 } } + /// 日志根目录(~/Library/Logs/VoiceInput/) + private var logDir: String { + let lib = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! + let dir = lib.appendingPathComponent("Logs/VoiceInput").path + if !FileManager.default.fileExists(atPath: dir) { + try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) + } + return dir + } + private func agentLog(_ msg: String) { - let logPath = (Bundle.main.bundlePath as NSString).deletingLastPathComponent + "/../../../agent_debug.log" + let logPath = logDir + "/agent_app.log" let ts = ISO8601DateFormatter().string(from: Date()) let line = "[\(ts)] \(msg)\n" if let handle = FileHandle(forWritingAtPath: logPath) { @@ -454,6 +482,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel } } + /// Reminder 专用文件日志,写入 ~/Library/Logs/VoiceInput/reminder.log + private func reminderLog(_ msg: String) { + let logPath = logDir + "/reminder.log" + let ts = ISO8601DateFormatter().string(from: Date()) + let line = "[\(ts)] \(msg)\n" + if let handle = FileHandle(forWritingAtPath: logPath) { + handle.seekToEndOfFile() + handle.write(line.data(using: .utf8)!) + handle.closeFile() + } else { + // 文件不存在时创建 + try? line.write(toFile: logPath, atomically: true, encoding: .utf8) + } + } + private func showAgentNotch() { showAgentNotchInternal(isTextInput: false) } @@ -520,12 +563,194 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel ) agentNotch = DynamicNotch(style: .notch) { view } agentNotch?.show() + // 兜底:强制窗口置前(accessory 应用可能需要) + if let window = agentNotch?.windowController?.window { + window.orderFrontRegardless() + window.makeKey() + print("[Notch] 强制 orderFrontRegardless makeKey=\(window.isKeyWindow)") + } + print("[Notch] show() 完成 window=\(agentNotch?.windowController?.window != nil ? "存在" : "nil")") } private func handleTextInput(_ text: String) { handleAgentMode(text: text) } + // MARK: - Reminders + + private func startReminderTimer() { + reminderLog("startReminderTimer() 启动定时器,间隔30秒") + reminderTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in + self?.checkReminders() + } + // 启动后延迟 3 秒首次检查(等 bridge 就绪) + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + self?.reminderLog("首次延迟检查 (3秒后)") + self?.checkReminders() + } + } + + private func loadMemories() -> [[String: Any]] { + let path = Config.shared.memoryPath + guard FileManager.default.fileExists(atPath: path), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { + return [] + } + return json + } + + private func saveMemories(_ memories: [[String: Any]]) { + let path = Config.shared.memoryPath + let dir = (path as NSString).deletingLastPathComponent + try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) + guard let data = try? JSONSerialization.data(withJSONObject: memories, options: .prettyPrinted) else { return } + try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + } + + private func markMemoryReminded(_ memoryId: String) { + var memories = loadMemories() + guard let idx = memories.firstIndex(where: { ($0["id"] as? String) == memoryId }) else { return } + memories[idx]["reminded"] = true + saveMemories(memories) + } + + private func checkReminders() { + let memories = loadMemories() + let now = Date() + reminderLog("checkReminders() 开始: 共\(memories.count)条记忆, now=\(now)") + + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let isoFormatterNoMs = ISO8601DateFormatter() + isoFormatterNoMs.formatOptions = [.withInternetDateTime] + + var foundDue = false + for mem in memories { + guard let type = mem["type"] as? String, type == "todo", + let remindAtStr = mem["remind_at"] as? String, + let content = mem["content"] as? String, + let memoryId = mem["id"] as? String else { + continue + } + + var remindAt = isoFormatter.date(from: remindAtStr) + if remindAt == nil { + remindAt = isoFormatterNoMs.date(from: remindAtStr) + } + let reminded = (mem["reminded"] as? Bool) ?? false + + if remindAt == nil { + let msg = "⚠️ 无法解析 remind_at: \(remindAtStr)" + print("[Reminder] \(msg)") + reminderLog("checkReminders \(msg)") + continue + } + if remindAt! > now { + print("[Reminder] 未到期 id=\(memoryId) remind=\(remindAtStr) now=\(now)") + continue + } + if reminded { + print("[Reminder] 已提醒 id=\(memoryId)") + continue + } + if triggeredReminderIds.contains(memoryId) { + print("[Reminder] 重复 id=\(memoryId)") + continue + } + + foundDue = true + triggeredReminderIds.insert(memoryId) + + 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)" + + let msg = "✅ 触发提醒 id=\(memoryId)" + print("[Reminder] \(msg)") + reminderLog("checkReminders \(msg)") + triggerReminder(message: reminderMsg, memoryId: memoryId) + } + if !foundDue && !memories.isEmpty { + print("[Reminder] 检查完成,无到期提醒 (共\(memories.count)条)") + } + } + + private func triggerReminder(message: String, memoryId: String) { + print("[Reminder] triggerReminder 开始 id=\(memoryId)") + reminderLog("triggerReminder() 开始 id=\(memoryId) message=\(message.prefix(100))") + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + + let isNotchVisible = self.agentNotch?.windowController?.window?.isVisible ?? false + print("[Reminder] 主线程 isNotchVisible=\(isNotchVisible) agentActive=\(self.agentActive)") + self.reminderLog("triggerReminder 主线程 isNotchVisible=\(isNotchVisible) agentActive=\(self.agentActive)") + + // 销毁旧弹窗(如果存在) + if self.agentNotch != nil { + self.reminderLog("triggerReminder 销毁旧弹窗") + self.agentNotch?.hide() + self.agentNotch = nil + } + + // 完全重置 UI 状态 + self.agentTracker.resetForInput() + self.agentTracker.start() + self.agentTracker.isTextInputMode = false + self.agentTracker.toolsMode = Config.shared.agentToolsMode + self.agentTracker.newConversationMode = Config.shared.agentNewConversationMode + + if !isNotchVisible { + // 弹窗不可见 → 弹出弹窗 + print("[Reminder] 弹窗不可见,弹出") + self.reminderLog("triggerReminder 弹出弹窗") + self.showAgentNotchInternal(isTextInput: false) + print("[Reminder] showAgentNotchInternal 完成") + self.reminderLog("triggerReminder showAgentNotchInternal 完成") + } + + if !self.agentActive { + // 需要启动 bridge + self.reminderLog("triggerReminder agentActive=false,即将启动 bridge") + self.agentBridge.onEvent = { [weak self] event in + DispatchQueue.main.async { self?.handleAgentEvent(event) } + } + self.agentBridge.onExit = { [weak self] code in + DispatchQueue.main.async { + self?.agentActive = false + self?.agentIsTextMode = false + } + } + guard self.agentBridge.start() else { + let errMsg = "❌ agentBridge.start 失败" + print("[Reminder] \(errMsg)") + self.reminderLog("triggerReminder \(errMsg)") + self.agentTracker.fail("无法启动智能体进程") + return + } + print("[Reminder] bridge 已启动") + self.reminderLog("triggerReminder bridge 启动成功") + self.agentActive = true + self.agentBridge.send(message: message, workspace: Config.shared.agentWorkspace, allowMode: Config.shared.agentAllowMode, modelKey: nil) + self.reminderLog("triggerReminder 消息已发送到 bridge") + } else { + print("[Reminder] bridge 已在运行,追加消息") + self.reminderLog("triggerReminder bridge 已在运行,追加消息") + self.agentBridge.sendContinue(message: message) + } + print("[Reminder] 消息已发送") + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.markMemoryReminded(memoryId) + self.reminderLog("triggerReminder 标记 memoryId=\(memoryId) 已提醒") + } + } + } + func applicationWillTerminate(_ notification: Notification) { speechSynthesizer.stopSpeaking() keyMonitor.stop() diff --git a/Sources/Core/AgentBridge.swift b/Sources/Core/AgentBridge.swift index 7ebe489..7846e62 100644 --- a/Sources/Core/AgentBridge.swift +++ b/Sources/Core/AgentBridge.swift @@ -9,6 +9,7 @@ enum AgentEvent { case assistantChunk(content: String) case done(content: String, elapsedMs: Int, totalTokens: Int) case error(message: String) + case reminderTriggered(memoryId: String, content: String) } // MARK: - AgentBridge @@ -33,9 +34,8 @@ final class AgentBridge { try? FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: true) } self.logPath = logDir + "/agent_debug.log" - // 清空旧日志 - try? "".write(toFile: logPath, atomically: true, encoding: .utf8) - log("======== AgentBridge 日志开始 ========") + // 不清空旧日志,保留历史以便排查 + log("======== AgentBridge 新实例 ========") log("agentDir: \(agentDir)") } @@ -53,20 +53,29 @@ final class AgentBridge { /// 启动 Node.js 桥接进程 func start() -> Bool { + log("start() 开始,agentDir=\(agentDir)") + guard let nodePath = findNode() else { - print("[AgentBridge] ❌ Node.js not found") + let msg = "❌ Node.js not found. agentDir=\(agentDir), bundledNode=\(agentDir)/bin/node" + print("[AgentBridge] \(msg)") + log("start() \(msg)") return false } let bridgePath = "\(agentDir)/src/bridge.js" guard FileManager.default.fileExists(atPath: bridgePath) else { - print("[AgentBridge] ❌ bridge.js not found at \(bridgePath)") + let msg = "❌ bridge.js not found at \(bridgePath). agentDir contents: \((try? FileManager.default.contentsOfDirectory(atPath: agentDir)) ?? [])" + print("[AgentBridge] \(msg)") + log("start() \(msg)") return false } + let userConfigPath = Config.shared.userAgentModelsPath + log("start() nodePath=\(nodePath) bridgePath=\(bridgePath) userConfigPath=\(userConfigPath) configExists=\(FileManager.default.fileExists(atPath: userConfigPath))") + let process = Process() process.executableURL = URL(fileURLWithPath: nodePath) - process.arguments = [bridgePath, "--config", Config.shared.userAgentModelsPath] + process.arguments = [bridgePath, "--config", userConfigPath] process.currentDirectoryURL = URL(fileURLWithPath: agentDir) // stdin → 向 bridge 发送输入 @@ -116,10 +125,14 @@ final class AgentBridge { do { try process.run() self.process = process - print("[AgentBridge] ✅ Started (pid \(process.processIdentifier))") + let msg = "✅ Started (pid \(process.processIdentifier))" + print("[AgentBridge] \(msg)") + log("start() \(msg)") return true } catch { - print("[AgentBridge] ❌ Failed to start: \(error)") + let msg = "❌ Failed to start: \(error.localizedDescription)" + print("[AgentBridge] \(msg)") + log("start() \(msg)") return false } } @@ -246,6 +259,11 @@ final class AgentBridge { let message = json["message"] as? String ?? "未知错误" log(" → error message=\(message)") onEvent?(.error(message: message)) + case "reminder_triggered": + let memoryId = json["memory_id"] as? String ?? "" + let content = json["content"] as? String ?? "" + log(" → reminder_triggered id=\(memoryId)") + onEvent?(.reminderTriggered(memoryId: memoryId, content: content)) default: log(" → 未知 type: \(type)") break