fix: 完善权限引导 — 辅助功能+输入监控+麦克风+语音识别,统一文案
This commit is contained in:
parent
ad72907e89
commit
e1cfb27e16
@ -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() {
|
||||
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 {
|
||||
self?.startKeyMonitor()
|
||||
let successAlert = NSAlert()
|
||||
successAlert.messageText = "权限已授权"
|
||||
successAlert.informativeText = "辅助功能权限已开启,Voice Input 正在运行。"
|
||||
successAlert.alertStyle = .informational
|
||||
successAlert.runModal()
|
||||
print("[Permission] ✅ \(label) granted")
|
||||
onGranted()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSApp.terminate(nil)
|
||||
|
||||
/// 检查输入监控权限
|
||||
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()
|
||||
|
||||
// 根据模式显示不同提示
|
||||
|
||||
Loading…
Reference in New Issue
Block a user