// 语音输入功能 document.addEventListener('DOMContentLoaded', function() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const agentInput = document.getElementById('agentInput'); const sendAgentBtn = document.getElementById('sendAgentBtn'); const status = document.getElementById('status'); if (!agentInput || !sendAgentBtn || !sendAgentBtn.parentNode) { return; } const voiceBtn = document.createElement('button'); voiceBtn.className = 'btn btn-voice'; voiceBtn.id = 'voiceBtn'; voiceBtn.type = 'button'; voiceBtn.textContent = '🎤'; voiceBtn.title = '按住说话,松开发送'; sendAgentBtn.parentNode.insertBefore(voiceBtn, sendAgentBtn); let recognition = null; let isRecordingVoice = false; let microphoneReady = false; let spacePressed = false; let shouldAutoSend = false; function setStatusMessage(message) { if (status) { status.innerHTML = `

${message}

`; } } function updateVoiceButton(recording) { voiceBtn.classList.toggle('recording', recording); voiceBtn.textContent = recording ? '🔴' : '🎤'; } function hasSecureVoiceContext() { return window.isSecureContext; } function getSecureContextMessage() { return '语音输入需要在 HTTPS 或 localhost 环境下使用;当前地址无法申请麦克风权限。'; } function getRecognitionErrorMessage(error) { switch (error) { case 'not-allowed': case 'service-not-allowed': return '浏览器已阻止麦克风权限,请在地址栏站点设置中允许麦克风后刷新页面。'; case 'audio-capture': return '没有检测到可用麦克风,请检查设备或系统录音权限。'; case 'network': return '语音识别服务连接失败,请检查网络后重试。'; case 'no-speech': return '没有识别到语音,请按住按钮后再说话。'; case 'aborted': return '语音识别已取消。'; default: return `语音识别失败:${error}`; } } function handlePermissionError(error) { const permissionDeniedNames = ['NotAllowedError', 'PermissionDeniedError', 'SecurityError']; const deviceMissingNames = ['NotFoundError', 'DevicesNotFoundError']; if (permissionDeniedNames.includes(error.name)) { setStatusMessage('浏览器未授予麦克风权限,请在站点权限中允许麦克风后重试。'); return; } if (deviceMissingNames.includes(error.name)) { setStatusMessage('没有找到可用的麦克风设备,请检查设备连接。'); return; } setStatusMessage(`麦克风初始化失败:${error.message || error.name}`); } async function ensureMicrophonePermission() { if (microphoneReady) { return true; } if (!hasSecureVoiceContext()) { setStatusMessage(getSecureContextMessage()); return false; } if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { // 某些支持 SpeechRecognition 的浏览器不会暴露 getUserMedia; // 这种情况下交给 recognition.start() 自己触发权限请求。 microphoneReady = true; return true; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach((track) => track.stop()); microphoneReady = true; return true; } catch (error) { console.error('麦克风权限申请失败:', error); handlePermissionError(error); return false; } } function initSpeechRecognition() { if (!SpeechRecognition) { return; } recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = true; recognition.lang = 'zh-CN'; recognition.onstart = function() { isRecordingVoice = true; updateVoiceButton(true); setStatusMessage('正在聆听,请开始说话...'); }; recognition.onresult = function(event) { let finalTranscript = ''; let interimTranscript = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } if (finalTranscript) { agentInput.value = finalTranscript.trim(); } else if (interimTranscript) { agentInput.value = interimTranscript.trim(); } }; recognition.onerror = function(event) { console.error('语音识别错误:', event.error); isRecordingVoice = false; updateVoiceButton(false); if (event.error !== 'aborted') { setStatusMessage(getRecognitionErrorMessage(event.error)); } }; recognition.onend = function() { const hasText = Boolean(agentInput.value.trim()); isRecordingVoice = false; updateVoiceButton(false); if (hasText && shouldAutoSend) { window.setTimeout(function() { sendAgentBtn.click(); }, 300); } else if (!hasText) { setStatusMessage('语音识别已结束。'); } shouldAutoSend = false; }; } async function startVoiceRecognition() { if (!SpeechRecognition) { setStatusMessage('当前浏览器不支持语音识别,请使用最新版 Chrome 或 Edge。'); return; } if (isRecordingVoice) { return; } if (!recognition) { initSpeechRecognition(); } const permissionReady = await ensureMicrophonePermission(); if (!permissionReady || !recognition) { return; } shouldAutoSend = true; try { recognition.start(); } catch (error) { console.error('启动语音识别失败:', error); if (error.name === 'InvalidStateError') { try { recognition.stop(); } catch (stopError) { console.error('停止语音识别失败:', stopError); } return; } setStatusMessage(`启动语音识别失败:${error.message || error.name}`); } } function stopVoiceRecognition() { if (recognition && isRecordingVoice) { recognition.stop(); } } if (!SpeechRecognition) { voiceBtn.disabled = true; voiceBtn.title = '当前浏览器不支持语音识别'; setStatusMessage('当前浏览器不支持语音识别,请使用最新版 Chrome 或 Edge。'); return; } if (!hasSecureVoiceContext()) { voiceBtn.title = '当前页面不是 HTTPS 或 localhost,浏览器不会授予麦克风权限'; setStatusMessage(getSecureContextMessage()); } voiceBtn.addEventListener('pointerdown', async function(event) { if (event.pointerType === 'mouse' && event.button !== 0) { return; } event.preventDefault(); shouldAutoSend = true; if (voiceBtn.setPointerCapture) { try { voiceBtn.setPointerCapture(event.pointerId); } catch (error) { console.debug('setPointerCapture skipped:', error); } } await startVoiceRecognition(); }); voiceBtn.addEventListener('pointerup', function(event) { event.preventDefault(); stopVoiceRecognition(); }); voiceBtn.addEventListener('pointercancel', function() { stopVoiceRecognition(); }); voiceBtn.addEventListener('lostpointercapture', function() { stopVoiceRecognition(); }); voiceBtn.addEventListener('contextmenu', function(event) { event.preventDefault(); }); document.addEventListener('keydown', function(event) { if (event.repeat) { return; } if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') { event.preventDefault(); if (!spacePressed) { spacePressed = true; shouldAutoSend = true; startVoiceRecognition(); } } }); document.addEventListener('keyup', function(event) { if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') { event.preventDefault(); spacePressed = false; stopVoiceRecognition(); } }); });