289 lines
9.1 KiB
JavaScript
Raw Normal View History

// 语音输入功能
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 = `<p>${message}</p>`;
}
}
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();
}
});
});