debug: 添加 Reminder 文件日志排查 DMG 定时提醒失效问题

- AppDelegate: 新增 reminderLog() 写入 ~/Library/Logs/VoiceInput/reminder.log
- AppDelegate: 修复 agentLog 路径使用用户 Logs 目录
- AppDelegate: 启动时记录 bundlePath/agentDir/memoryPath 等诊断信息
- AgentBridge: init 不清空历史日志,start() 失败时输出详细错误
- checkReminders/triggerReminder 全链路覆盖文件日志
This commit is contained in:
JOJO 2026-05-01 01:19:12 +08:00
parent f07a72de2b
commit 967da1b889
2 changed files with 252 additions and 9 deletions

View File

@ -32,6 +32,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
private var agentIsTextMode = false private var agentIsTextMode = false
private var agentExitingCleanly = false // 退 private var agentExitingCleanly = false // 退
private var autoHideWorkItem: DispatchWorkItem? private var autoHideWorkItem: DispatchWorkItem?
private var reminderTimer: Timer?
private var triggeredReminderIds = Set<String>() //
override init() { override init() {
agentBridge = AgentBridge(agentDir: Config.shared.agentDir) agentBridge = AgentBridge(agentDir: Config.shared.agentDir)
@ -42,6 +44,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory) 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() beginPermissionFlow()
print("[App] Ready") print("[App] Ready")
} }
@ -440,11 +455,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
case .error(let message): case .error(let message):
agentLog("[App] handleEvent error message=\(message)") agentLog("[App] handleEvent error message=\(message)")
agentTracker.fail(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) { 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 ts = ISO8601DateFormatter().string(from: Date())
let line = "[\(ts)] \(msg)\n" let line = "[\(ts)] \(msg)\n"
if let handle = FileHandle(forWritingAtPath: logPath) { 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() { private func showAgentNotch() {
showAgentNotchInternal(isTextInput: false) showAgentNotchInternal(isTextInput: false)
} }
@ -520,12 +563,194 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSSpeechSynthesizerDel
) )
agentNotch = DynamicNotch(style: .notch) { view } agentNotch = DynamicNotch(style: .notch) { view }
agentNotch?.show() 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) { private func handleTextInput(_ text: String) {
handleAgentMode(text: text) 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) { func applicationWillTerminate(_ notification: Notification) {
speechSynthesizer.stopSpeaking() speechSynthesizer.stopSpeaking()
keyMonitor.stop() keyMonitor.stop()

View File

@ -9,6 +9,7 @@ enum AgentEvent {
case assistantChunk(content: String) case assistantChunk(content: String)
case done(content: String, elapsedMs: Int, totalTokens: Int) case done(content: String, elapsedMs: Int, totalTokens: Int)
case error(message: String) case error(message: String)
case reminderTriggered(memoryId: String, content: String)
} }
// MARK: - AgentBridge // MARK: - AgentBridge
@ -33,9 +34,8 @@ final class AgentBridge {
try? FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: true) try? FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: true)
} }
self.logPath = logDir + "/agent_debug.log" self.logPath = logDir + "/agent_debug.log"
// // 便
try? "".write(toFile: logPath, atomically: true, encoding: .utf8) log("======== AgentBridge 新实例 ========")
log("======== AgentBridge 日志开始 ========")
log("agentDir: \(agentDir)") log("agentDir: \(agentDir)")
} }
@ -53,20 +53,29 @@ final class AgentBridge {
/// Node.js /// Node.js
func start() -> Bool { func start() -> Bool {
log("start() 开始agentDir=\(agentDir)")
guard let nodePath = findNode() else { 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 return false
} }
let bridgePath = "\(agentDir)/src/bridge.js" let bridgePath = "\(agentDir)/src/bridge.js"
guard FileManager.default.fileExists(atPath: bridgePath) else { 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 return false
} }
let userConfigPath = Config.shared.userAgentModelsPath
log("start() nodePath=\(nodePath) bridgePath=\(bridgePath) userConfigPath=\(userConfigPath) configExists=\(FileManager.default.fileExists(atPath: userConfigPath))")
let process = Process() let process = Process()
process.executableURL = URL(fileURLWithPath: nodePath) process.executableURL = URL(fileURLWithPath: nodePath)
process.arguments = [bridgePath, "--config", Config.shared.userAgentModelsPath] process.arguments = [bridgePath, "--config", userConfigPath]
process.currentDirectoryURL = URL(fileURLWithPath: agentDir) process.currentDirectoryURL = URL(fileURLWithPath: agentDir)
// stdin bridge // stdin bridge
@ -116,10 +125,14 @@ final class AgentBridge {
do { do {
try process.run() try process.run()
self.process = process self.process = process
print("[AgentBridge] ✅ Started (pid \(process.processIdentifier))") let msg = "✅ Started (pid \(process.processIdentifier))"
print("[AgentBridge] \(msg)")
log("start() \(msg)")
return true return true
} catch { } catch {
print("[AgentBridge] ❌ Failed to start: \(error)") let msg = "❌ Failed to start: \(error.localizedDescription)"
print("[AgentBridge] \(msg)")
log("start() \(msg)")
return false return false
} }
} }
@ -246,6 +259,11 @@ final class AgentBridge {
let message = json["message"] as? String ?? "未知错误" let message = json["message"] as? String ?? "未知错误"
log(" → error message=\(message)") log(" → error message=\(message)")
onEvent?(.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: default:
log(" → 未知 type: \(type)") log(" → 未知 type: \(type)")
break break