diff --git a/sundynix-desktop/frontend/src/lib/voice.ts b/sundynix-desktop/frontend/src/lib/voice.ts index 7508c18..f97d779 100644 --- a/sundynix-desktop/frontend/src/lib/voice.ts +++ b/sundynix-desktop/frontend/src/lib/voice.ts @@ -201,15 +201,42 @@ export class VoiceClient { if (!navigator.mediaDevices?.getUserMedia) { throw new Error("此窗口暂不支持麦克风(原生壳需授权/安全上下文)。用浏览器打开 localhost:5173 可直接说话。"); } - // 先试带回声消除等约束(浏览器更好),WKWebView(原生壳)不认高级约束会抛 "Invalid constraint", - // 退回最简 {audio:true}(最兼容;多数实现默认已开回声消除)。 - let stream: MediaStream; + // 先看有没有麦克风设备:Mac Studio/Mini 等无内置麦克风的机器上,getUserMedia 会以 + // OverconstrainedError "Invalid constraint" 报错(误导),提前给准确提示。 + let devs: MediaDeviceInfo[] = []; try { - stream = await navigator.mediaDevices.getUserMedia({ - audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 }, - }); + devs = await navigator.mediaDevices.enumerateDevices(); } catch { - stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + /* 拿不到设备列表就跳过预检,直接试 getUserMedia */ + } + if (devs.length > 0 && !devs.some((d) => d.kind === "audioinput")) { + throw new Error("没检测到麦克风设备。这台机器(如 Mac Studio/Mini)可能无内置麦克风——插个麦克风(带麦耳机 / USB 麦 / AirPods)再试。"); + } + + // 依次尝试多种约束形式:WKWebView(原生壳)对约束挑剔,不同实现接受的形式不同。 + // 全失败则把**真实错误名**抛出来(区分是"约束不认"Overconstrained 还是"权限被拒"NotAllowed)。 + const tries: MediaStreamConstraints[] = [ + { audio: true }, + { audio: {} }, + { audio: { echoCancellation: true, noiseSuppression: true } }, + ]; + let stream: MediaStream | null = null; + let lastErr: unknown; + for (const c of tries) { + try { + stream = await navigator.mediaDevices.getUserMedia(c); + break; + } catch (e) { + lastErr = e; + } + } + if (!stream) { + const err = lastErr as { name?: string; message?: string }; + const detail = `${err?.name ?? "Error"}: ${err?.message ?? String(lastErr)}`; + if (err?.name === "NotAllowedError" || err?.name === "SecurityError") { + throw new Error("麦克风权限被拒。请到 系统设置 → 隐私与安全性 → 麦克风 里允许本应用。"); + } + throw new Error("麦克风打开失败:" + detail); } this.micStream = stream; const ctx = new AudioContext();