From e1cfb27e1690505a7c85dd348ada375b830ed363 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sat, 25 Apr 2026 11:37:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E6=9D=83=E9=99=90?= =?UTF-8?q?=E5=BC=95=E5=AF=BC=20=E2=80=94=20=E8=BE=85=E5=8A=A9=E5=8A=9F?= =?UTF-8?q?=E8=83=BD+=E8=BE=93=E5=85=A5=E7=9B=91=E6=8E=A7+=E9=BA=A6?= =?UTF-8?q?=E5=85=8B=E9=A3=8E+=E8=AF=AD=E9=9F=B3=E8=AF=86=E5=88=AB?= =?UTF-8?q?=EF=BC=8C=E7=BB=9F=E4=B8=80=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/App/AppDelegate.swift | 168 ++++++++++++++++++++++++++-------- 1 file changed, 131 insertions(+), 37 deletions(-) diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index d201e67..a5ecf8b 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -1,4 +1,6 @@ import Cocoa +import AVFoundation +import Speech /// 应用主控制器,协调 Fn 键(语音输入)和 Control 键(AI 助手)两种模式 final class AppDelegate: NSObject, NSApplicationDelegate { @@ -15,57 +17,118 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) - - // 检查辅助功能权限 - if !CGPreflightListenEventAccess() { - requestAccessibilityPermission() - } else { - startKeyMonitor() - } - + beginPermissionFlow() print("[App] Ready") } - // MARK: - Permissions + // MARK: - Permissions — 顺序引导 - private func requestAccessibilityPermission() { - NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!) + private func beginPermissionFlow() { + // 步骤1:辅助功能 + guard CGPreflightListenEventAccess() else { + openPrefsAndWait(panel: "Privacy_Accessibility", label: "辅助功能") { [weak self] in + self?.beginPermissionFlow() + } + return + } + // 步骤2:输入监控 + guard canCreateHIDEventTap() else { + openPrefsAndWait(panel: "Privacy_InputMonitoring", label: "输入监控") { [weak self] in + self?.beginPermissionFlow() + } + return + } + // 全部就绪 + startKeyMonitor() + showReadyAlert() + } + + /// 打开系统设置面板,轮询等待权限,完成后回调 + private func openPrefsAndWait(panel: String, label: String, onGranted: @escaping () -> Void) { + NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.preference.security?\(panel)")! + ) let alert = NSAlert() - alert.messageText = "需要辅助功能权限" - alert.informativeText = """ - Voice Input 需要「辅助功能」权限才能监听 Fn 键和 Control 键。 - - 系统设置已自动打开,请: - 1. 点击左下角的锁并输入密码 - 2. 在列表中找到并勾选「VoiceInput」 - 3. 返回本应用 - - 点击「继续」后,应用将每 2 秒检查一次权限状态。 - """ + alert.messageText = "需要\(label)权限" + alert.informativeText = "点击 + 号,选择 VoiceInput 应用,点击打开。" 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 { + guard alert.runModal() == .alertFirstButtonReturn else { NSApp.terminate(nil) + return + } + + // 轮询检查 + let check: () -> Bool = { + if panel == "Privacy_Accessibility" { + return CGPreflightListenEventAccess() + } else { + return self.canCreateHIDEventTap() + } + } + + Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in + guard check() else { return } + timer.invalidate() + DispatchQueue.main.async { + print("[Permission] ✅ \(label) granted") + onGranted() + } } } + /// 检查输入监控权限 + private func canCreateHIDEventTap() -> Bool { + let eventMask = (1 << CGEventType.flagsChanged.rawValue) + guard let tap = CGEvent.tapCreate( + tap: .cghidEventTap, place: .headInsertEventTap, + options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), + callback: { _, _, _, _ in return nil }, + userInfo: nil + ) else { return false } + CFMachPortInvalidate(tap) + return true + } + + private func showReadyAlert() { + let alert = NSAlert() + alert.messageText = "权限已就绪" + alert.informativeText = "Voice Input 正在运行。首次使用时还需授权麦克风和语音识别。" + alert.alertStyle = .informational + alert.runModal() + } + + // MARK: - 麦克风 / 语音识别权限 + + private func showMicrophonePermissionAlert() { + let alert = NSAlert() + alert.messageText = "需要麦克风权限" + alert.informativeText = "请前往 系统设置 > 隐私与安全性 > 麦克风,开启 VoiceInput 的权限。" + alert.alertStyle = .warning + alert.addButton(withTitle: "打开系统设置") + alert.addButton(withTitle: "取消") + if alert.runModal() == .alertFirstButtonReturn { + NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")!) + } + } + + private func showSpeechPermissionAlert() { + let alert = NSAlert() + alert.messageText = "需要语音识别权限" + alert.informativeText = "请前往 系统设置 > 隐私与安全性 > 语音识别,开启 VoiceInput 的权限。" + alert.alertStyle = .warning + alert.addButton(withTitle: "打开系统设置") + alert.addButton(withTitle: "取消") + if alert.runModal() == .alertFirstButtonReturn { + NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition")!) + } + } + + // MARK: - Key Monitor + private func startKeyMonitor() { let started = keyMonitor.start() if !started { @@ -101,6 +164,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate { isRecording = true finalTranscription = "" + // 检查麦克风权限 + let micStatus = AVCaptureDevice.authorizationStatus(for: .audio) + if micStatus == .denied || micStatus == .restricted { + showMicrophonePermissionAlert() + isRecording = false + return + } + + // 检查语音识别权限 + let speechStatus = SFSpeechRecognizer.authorizationStatus() + if speechStatus == .denied || speechStatus == .restricted { + showSpeechPermissionAlert() + isRecording = false + return + } + if speechStatus == .notDetermined { + // 触发系统弹窗 + SFSpeechRecognizer.requestAuthorization { [weak self] status in + DispatchQueue.main.async { + self?.isRecording = false + guard status == .authorized else { + self?.showSpeechPermissionAlert() + return + } + // 重新触发 + self?.startRecording() + } + } + return + } + recordingPanel.showAnimated() // 根据模式显示不同提示