import Cocoa /// 应用主控制器,协调按键监听、语音识别、LLM 纠错和文本注入 final class AppDelegate: NSObject, NSApplicationDelegate { private let keyMonitor = KeyMonitor() private let speechRecognizer = SpeechRecognizer() private let audioMonitor = AudioLevelMonitor() private let recordingPanel = RecordingPanel() private let menuBarController = MenuBarController() private let llmRefiner = LLMRefiner() private var isRecording = false private var finalTranscription: String = "" func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) // 检查辅助功能权限 if !CGPreflightListenEventAccess() { requestAccessibilityPermission() } else { startKeyMonitor() } print("[App] Ready") } // MARK: - Permissions private func requestAccessibilityPermission() { // 自动打开系统设置 NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!) // 显示提示 let alert = NSAlert() alert.messageText = "需要辅助功能权限" alert.informativeText = """ Voice Input 需要「辅助功能」权限才能监听 Fn 键。 系统设置已自动打开,请: 1. 点击左下角的锁并输入密码 2. 在列表中找到并勾选「VoiceInput」 3. 返回本应用 点击「继续」后,应用将每 2 秒检查一次权限状态。 """ alert.alertStyle = .informational alert.addButton(withTitle: "继续") alert.addButton(withTitle: "退出") if alert.runModal() == .alertFirstButtonReturn { // 轮询检查权限 Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] timer in if CGPreflightListenEventAccess() { timer.invalidate() DispatchQueue.main.async { self?.startKeyMonitor() // 显示成功提示 let successAlert = NSAlert() successAlert.messageText = "权限已授权" successAlert.informativeText = "辅助功能权限已开启,Voice Input 正在运行。" successAlert.alertStyle = .informational successAlert.runModal() } } } } else { NSApp.terminate(nil) } } private func startKeyMonitor() { let started = keyMonitor.start() if !started { print("[App] Failed to start key monitor") return } keyMonitor.onFnDown = { [weak self] in self?.startRecording() } keyMonitor.onFnUp = { [weak self] in self?.stopRecording() } } // MARK: - Recording private func startRecording() { guard !isRecording else { return } isRecording = true finalTranscription = "" recordingPanel.showAnimated() recordingPanel.updateText("Listening...") let language = Config.shared.language.rawValue Task { @MainActor in // 启动音频电平监测 let audioOK = audioMonitor.start() if audioOK { audioMonitor.onLevelUpdate = { [weak self] level in self?.recordingPanel.waveformView.updateLevel(level) } } // 启动语音识别 let speechOK = await speechRecognizer.start(language: language) if !speechOK { recordingPanel.updateText("Recognition error") DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.cancelRecording() } return } speechRecognizer.onResult = { [weak self] text in guard let self = self, self.isRecording else { return } self.finalTranscription = text self.recordingPanel.updateText(text) } speechRecognizer.onError = { error in let nsError = error as NSError // kAFAssistantErrorDomain code 1110 = 未检测到语音,属正常情况,不报错 if nsError.domain == "kAFAssistantErrorDomain" && nsError.code == 1110 { return } print("[App] Speech error: \(error)") } speechRecognizer.onStateChange = { [weak self] isActive in if !isActive, self?.isRecording == true { // 识别意外停止 } } } } private func stopRecording() { guard isRecording else { return } isRecording = false // 停止音频和识别 audioMonitor.stop() _ = speechRecognizer.stop() let text = finalTranscription.trimmingCharacters(in: .whitespacesAndNewlines) // 如果 LLM 启用且已配置 if Config.shared.llmEnabled && Config.shared.isLLMConfigured && !text.isEmpty { recordingPanel.showRefining() Task { @MainActor in do { let result = try await llmRefiner.refine(text: text, config: Config.shared) recordingPanel.hideAnimated { TextInjector.inject(result.refinedText) } } catch { print("[App] LLM error: \(error)") // LLM 失败,回退到原始文本 recordingPanel.hideAnimated { TextInjector.inject(text) } } } } else if !text.isEmpty { recordingPanel.hideAnimated { TextInjector.inject(text) } } else { recordingPanel.hideAnimated() } finalTranscription = "" } private func cancelRecording() { audioMonitor.stop() speechRecognizer.cancel() recordingPanel.hideAnimated() isRecording = false finalTranscription = "" } func applicationWillTerminate(_ notification: Notification) { keyMonitor.stop() audioMonitor.stop() speechRecognizer.cancel() recordingPanel.close() } }