fix(windows): 校准麦克风权限探测 (#53)

Co-authored-by: Cooper-X-Oak <Cooper-X-Oak@users.noreply.github.com>
This commit is contained in:
Cooper OakandCooper-X-Oak authored and GitHub committed 2026-04-30 14:17:42 +08:00
1 parent bb7300ee85
commit ce0f00fd54
2 files changed
+68 -9

No files matched your search

@@ -264,6 +264,11 @@ mod platform {
/// Windows 的麦克风权限走系统设置 → 隐私 → 麦克风;
/// 这里用 cpal 建立一次短生命周期输入流,避免只查设备格式时误报已授权。
pub fn check_microphone() -> PermissionStatus {
if windows_microphone_registry_denied() {
log::warn!("[mic] Windows microphone privacy registry is denied");
return PermissionStatus::Denied;
}
let host = cpal::default_host();
let Some(device) = host.default_input_device() else {
log::warn!("[mic] no default input device");
@@ -334,6 +339,51 @@ mod platform {
drop(stream);
Ok(())
}
fn windows_microphone_registry_denied() -> bool {
candidate_microphone_registry_paths()
.into_iter()
.any(|path| registry_value_is_deny(&path))
}
fn candidate_microphone_registry_paths() -> Vec<String> {
let mut paths = vec![
r"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone".to_string(),
r"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged".to_string(),
r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone".to_string(),
];
if let Ok(exe) = std::env::current_exe() {
if let Some(encoded) = exe.to_str().map(|path| path.replace('\\', "#")) {
paths.push(format!(
r"HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged\{encoded}"
));
}
}
paths
}
fn registry_value_is_deny(path: &str) -> bool {
let output = match std::process::Command::new("reg")
.args(["query", path, "/v", "Value"])
.output()
{
Ok(output) => output,
Err(err) => {
log::warn!("[mic] reg query failed for {path}: {err}");
return false;
}
};
if !output.status.success() {
return false;
}
String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| line.contains("REG_SZ") && line.split_whitespace().any(|part| part == "Deny"))
}
}
pub use platform::{
+18 -9
View File
@@ -565,37 +565,46 @@ function PermissionsSection() {
const [hotkey, setHotkey] = useState<HotkeyStatus | null>(null);
const { capability } = useHotkeySettings();
const refresh = async () => {
const refreshPermissions = async () => {
const [a, m] = await Promise.all([
checkAccessibilityPermission(),
checkMicrophonePermission(),
]);
setAccessibility(a);
setMicrophone(m);
};
const refreshHotkey = async () => {
setHotkey(await getHotkeyStatus());
};
useEffect(() => {
refresh();
const id = window.setInterval(refresh, 1000);
// 用户从系统设置切回来时立刻刷新(不等下一个 1s tick)
const onFocus = () => refresh();
refreshPermissions();
refreshHotkey();
const hotkeyId = window.setInterval(refreshHotkey, 1000);
// 麦克风检查会短暂打开输入流,避免每秒探测导致隐私指示器频繁闪烁。
const permissionId = window.setInterval(refreshPermissions, 10000);
const onFocus = () => {
refreshPermissions();
refreshHotkey();
};
window.addEventListener('focus', onFocus);
return () => {
window.clearInterval(id);
window.clearInterval(hotkeyId);
window.clearInterval(permissionId);
window.removeEventListener('focus', onFocus);
};
}, []);
const reRequestAccessibility = async () => {
await requestAccessibilityPermission();
refresh();
refreshPermissions();
};
const reRequestMicrophone = async () => {
if (microphone === 'denied' || microphone === 'restricted') {
await openSystemSettings('microphone');
refresh();
refreshPermissions();
return;
}
const status = await requestMicrophonePermission();
@@ -603,7 +612,7 @@ function PermissionsSection() {
if (status === 'denied' || status === 'restricted') {
await openSystemSettings('microphone');
}
refresh();
refreshPermissions();
};
const desc = capability?.requiresAccessibilityPermission