289 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 语音输入功能
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();
}
});
});