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:
parent
f07a72de2b
commit
967da1b889
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user