import Cocoa /// 应用主控制器,协调 Fn 键(语音输入)和 Control 键(AI 助手)两种模式 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 键和 Control 键。 系统设置已自动打开,请: 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 } // Fn 键:根据当前模式执行不同功能 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() // 根据模式显示不同提示 let promptText = Config.shared.triggerMode == .input ? "Listening..." : "Ask AI..." recordingPanel.updateText(promptText) 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 if nsError.domain == "kAFAssistantErrorDomain" && nsError.code == 1110 { return } print("[App] Speech error: \(error)") } } } private func stopRecording() { guard isRecording else { return } isRecording = false audioMonitor.stop() _ = speechRecognizer.stop() let text = finalTranscription.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { recordingPanel.hideAnimated() finalTranscription = "" return } // 根据 triggerMode 选择配置和逻辑 let mode = Config.shared.triggerMode let config = mode == .input ? Config.shared.inputLLM : Config.shared.assistLLM switch mode { case .input: handleInputMode(text: text, config: config) case .assist: handleAssistMode(text: text, config: config) } finalTranscription = "" } // MARK: - Input Mode (Fn) private func handleInputMode(text: String, config: LLMConfig) { guard config.enabled && config.isConfigured else { recordingPanel.hideAnimated { TextInjector.inject(text) } return } recordingPanel.showRefining() Task { @MainActor in do { let result = try await llmRefiner.refine(text: text, config: config) recordingPanel.hideAnimated { TextInjector.inject(result.text) } } catch { print("[App] LLM refine error: \(error)") recordingPanel.hideAnimated { TextInjector.inject(text) } } } } // MARK: - Assist Mode (Control) private func handleAssistMode(text: String, config: LLMConfig) { guard config.enabled && config.isConfigured else { recordingPanel.updateText("AI 助手未配置") DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.recordingPanel.hideAnimated() } return } recordingPanel.showThinking() Task { @MainActor in do { let result = try await llmRefiner.chat(text: text, config: config) recordingPanel.hideAnimated { TextInjector.inject(result.text) } } catch { print("[App] LLM chat error: \(error)") recordingPanel.updateText("请求失败,请检查配置") DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.recordingPanel.hideAnimated() } } } } private func cancelRecording() { audioMonitor.stop() speechRecognizer.cancel() recordingPanel.hideAnimated() isRecording = false finalTranscription = "" } func applicationWillTerminate(_ notification: Notification) { keyMonitor.stop() audioMonitor.stop() speechRecognizer.cancel() recordingPanel.close() } }