/** * 实时语音通话功能 * 真正的语音交互:用户说话 → STT → AI → TTS → 播放 */ import { getSettings } from './config.js'; import { currentChatIndex } from './chat.js'; import { requestSave } from './save-manager.js'; import { refreshChatList } from './ui.js'; import { AudioRecorder, speechToText, textToSpeech, playAudio } from './voice-api.js'; import { showToast } from './toast.js'; import { saveVoiceRecordings } from './audio-storage.js'; // 通话状态 let callState = { isActive: false, isConnected: false, isMuted: false, isHangingUp: false, // 是否正在挂断 startTime: null, timerInterval: null, dotsInterval: null, connectTimeout: null, contactIndex: -1, contactName: '', contactAvatar: '', messages: [], // 通话消息记录 contact: null, initiator: 'user', rejectedByUser: false, rejectedByAI: false, isRecording: false, // 是否正在录音 isProcessing: false, // 是否正在处理(STT/AI/TTS) isPlaying: false, // 是否正在播放语音 recorder: null, // 录音器实例 currentAudio: null, // 当前播放的音频 voiceCache: [] // 缓存的 AI 语音 [{text, audioBlob, duration}] }; /** * 开始实时语音通话 */ export function startRealVoiceCall(initiator = 'user', contactIndex = currentChatIndex) { if (callState.isActive) return; if (contactIndex < 0) return; const settings = getSettings(); const contact = settings.contacts[contactIndex]; if (!contact) return; // 检查语音 API 是否配置 if (!settings.sttApiUrl || !settings.sttApiKey) { alert('请先在设置中配置语音识别 (STT) API'); return; } if (!settings.ttsApiUrl || !settings.ttsApiKey) { alert('请先在设置中配置语音合成 (TTS) API'); return; } // 检查浏览器是否支持录音 if (!AudioRecorder.isSupported()) { alert('您的浏览器不支持录音功能'); return; } callState.contactName = contact.name; callState.contactAvatar = contact.avatar; callState.contact = contact; callState.contactIndex = contactIndex; callState.isActive = true; callState.isConnected = false; callState.isMuted = false; callState.messages = []; callState.initiator = initiator; callState.rejectedByUser = false; callState.rejectedByAI = false; callState.isRecording = false; callState.isProcessing = false; callState.isPlaying = false; callState.recorder = new AudioRecorder(); callState.voiceCache = []; // 重置语音缓存 callState.isHangingUp = false; // 重置挂断标志 showCallPage(); startConnecting(); } /** * 显示通话页面 */ function showCallPage() { const page = document.getElementById('wechat-real-voice-call-page'); if (!page) return; // 设置头像 const avatarEl = document.getElementById('wechat-real-voice-call-avatar'); if (avatarEl) { const firstChar = callState.contactName ? callState.contactName.charAt(0) : '?'; if (callState.contactAvatar) { avatarEl.innerHTML = ``; } else { avatarEl.textContent = firstChar; } } // 设置名称 const nameEl = document.getElementById('wechat-real-voice-call-name'); if (nameEl) { nameEl.textContent = callState.contactName; } // 设置状态 const statusEl = document.getElementById('wechat-real-voice-call-status'); if (statusEl) { if (callState.initiator === 'ai') { statusEl.textContent = '邀请你实时语音...'; } else { statusEl.textContent = '等待对方接受邀请'; } statusEl.classList.add('connecting'); } // 重置时间显示 const timeEl = document.getElementById('wechat-real-voice-call-time'); if (timeEl) { timeEl.textContent = '00:00'; timeEl.classList.add('hidden'); } // 隐藏对话区域 const chatEl = document.getElementById('wechat-real-voice-call-chat'); if (chatEl) { chatEl.classList.add('hidden'); } const messagesEl = document.getElementById('wechat-real-voice-call-messages'); if (messagesEl) { messagesEl.innerHTML = ''; } // 隐藏按住说话按钮 const talkBtnArea = document.getElementById('wechat-real-voice-call-talk-area'); if (talkBtnArea) { talkBtnArea.classList.add('hidden'); } // 检测是否支持录音 const supportsRecording = AudioRecorder.isSupported(); const talkBtn = document.getElementById('wechat-real-voice-call-talk-btn'); const talkHint = document.querySelector('.wechat-real-voice-call-talk-hint'); const textInputArea = document.getElementById('wechat-real-voice-call-text-input-area'); // 语音按钮:只有支持录音时显示 if (talkBtn) talkBtn.style.display = supportsRecording ? 'flex' : 'none'; if (talkHint) talkHint.style.display = supportsRecording ? 'block' : 'none'; // 文字输入:始终显示,方便用户选择打字或语音 if (textInputArea) textInputArea.style.display = 'flex'; // 根据发起者显示不同的操作按钮 const incomingActionsEl = document.getElementById('wechat-real-voice-call-incoming-actions'); const callActionsEl = document.getElementById('wechat-real-voice-call-actions'); if (callState.initiator === 'ai') { if (incomingActionsEl) incomingActionsEl.classList.remove('hidden'); if (callActionsEl) callActionsEl.classList.add('hidden'); } else { if (incomingActionsEl) incomingActionsEl.classList.add('hidden'); if (callActionsEl) callActionsEl.classList.remove('hidden'); } page.classList.remove('hidden'); bindCallEvents(); } /** * 开始连接动画 */ async function startConnecting() { const statusEl = document.getElementById('wechat-real-voice-call-status'); if (!statusEl) return; let dotCount = 0; clearInterval(callState.dotsInterval); clearTimeout(callState.connectTimeout); const waitingText = callState.initiator === 'ai' ? '邀请你实时语音' : '等待对方接受邀请'; callState.dotsInterval = setInterval(() => { dotCount = (dotCount + 1) % 4; const dots = '.'.repeat(dotCount); statusEl.textContent = waitingText + dots; }, 500); if (callState.initiator === 'user') { // 用户发起:调用AI决策是否接听 const shouldAnswer = await askAIToAnswerCall(callState.contact); if (!callState.isActive) return; if (shouldAnswer) { if (callState.isActive && !callState.isConnected) { onCallConnected(); } } else { callState.rejectedByAI = true; hangupCall(); } } else { // AI发起:15秒后超时 callState.connectTimeout = setTimeout(() => { if (callState.isActive && !callState.isConnected) { callState.rejectedByUser = false; hangupCall(); } }, 15000); } } /** * AI决定是否接听 */ async function askAIToAnswerCall(contact) { if (!contact) return true; try { const { callAI } = await import('./ai.js'); const prompt = `[用户正在给你打实时语音电话,你需要决定是否接听] 根据你的性格和当前心情决定: - 如果你想接听,只回复:[接听] - 如果你不想接听,只回复:[拒接] 注意:大多数情况下你应该接听,只有特殊情况才拒接。`; const response = await callAI(contact, prompt); const trimmed = (response || '').trim(); console.log('[可乐] 实时语音 AI接听决策:', trimmed); if (trimmed.includes('[拒接]') || trimmed.includes('拒接')) { return false; } return true; } catch (err) { console.error('[可乐] AI接听决策失败:', err); return true; } } /** * 通话接通 */ function onCallConnected() { callState.isConnected = true; callState.startTime = Date.now(); clearInterval(callState.dotsInterval); clearTimeout(callState.connectTimeout); const statusEl = document.getElementById('wechat-real-voice-call-status'); if (statusEl) { statusEl.textContent = '通话中'; statusEl.classList.remove('connecting'); } // 显示计时器 const timeEl = document.getElementById('wechat-real-voice-call-time'); if (timeEl) { timeEl.classList.remove('hidden'); } // 显示对话区域 const chatEl = document.getElementById('wechat-real-voice-call-chat'); if (chatEl) { chatEl.classList.remove('hidden'); } // 显示按住说话按钮 const talkBtnArea = document.getElementById('wechat-real-voice-call-talk-area'); if (talkBtnArea) { talkBtnArea.classList.remove('hidden'); } // 切换到通话中按钮 const incomingActionsEl = document.getElementById('wechat-real-voice-call-incoming-actions'); const callActionsEl = document.getElementById('wechat-real-voice-call-actions'); if (incomingActionsEl) incomingActionsEl.classList.add('hidden'); if (callActionsEl) callActionsEl.classList.remove('hidden'); startCallTimer(); // AI发起的通话,接通后AI先打招呼 if (callState.initiator === 'ai') { triggerAIGreeting(); } } /** * 开始通话计时 */ function startCallTimer() { clearInterval(callState.timerInterval); callState.timerInterval = setInterval(() => { if (!callState.isConnected || !callState.startTime) return; const elapsed = Math.floor((Date.now() - callState.startTime) / 1000); const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); const seconds = (elapsed % 60).toString().padStart(2, '0'); const timeEl = document.getElementById('wechat-real-voice-call-time'); if (timeEl) { timeEl.textContent = `${minutes}:${seconds}`; } }, 1000); } /** * AI主动打招呼(AI发起通话时) */ async function triggerAIGreeting() { if (!callState.isConnected || !callState.contact) return; updateStatus('AI思考中...'); try { const { callRealVoiceAI } = await import('./ai.js'); const aiResponse = await callRealVoiceAI( callState.contact, '[用户接听了实时语音电话]', [], 'ai' ); // 清理回复 let reply = cleanAIReply(aiResponse); if (!reply) return; // 添加消息记录 addCallMessage('ai', reply); // TTS 合成并播放 await speakText(reply); updateStatus('通话中'); } catch (err) { console.error('[可乐] AI打招呼失败:', err); updateStatus('通话中'); } } /** * 清理 AI 回复(移除特殊标签,保留完整内容) */ function cleanAIReply(text) { if (!text) return ''; console.log('[可乐] AI原始回复:', text); let reply = text.trim(); // 移除语音标记 const voiceMatch = reply.match(/^\[语音[::]\s*(.+?)\]$/); if (voiceMatch) { reply = voiceMatch[1]; } // 移除特殊标记 reply = reply.replace(/\[.*?\]/g, '').trim(); reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); // 移除括号描述(中文和英文括号) reply = reply.replace(/([^)]+)/g, '').trim(); reply = reply.replace(/\([^)]+\)/g, '').trim(); // 如果清理后为空,用原始内容去掉标记 if (!reply && text.trim()) { reply = text.trim().replace(/[\[\]()()【】<>]/g, '').trim(); console.log('[可乐] 清理后为空,恢复内容:', reply); } console.log('[可乐] 最终回复:', reply || '(空)'); return reply; } /** * TTS 合成并播放 */ async function speakText(text) { if (!text || callState.isPlaying) return; callState.isPlaying = true; updateStatus('语音合成中...'); try { console.log('[可乐] 开始TTS合成:', text.substring(0, 50)); const audioBlob = await textToSpeech(text, callState.contact); // 检查音频数据 console.log('[可乐] TTS返回音频:', { size: audioBlob?.size, type: audioBlob?.type }); if (!audioBlob || audioBlob.size < 100) { console.error('[可乐] TTS返回的音频数据无效'); updateStatus('语音合成失败'); return; } updateStatus('对方正在说话...'); // 播放音频 const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); // 设置音量 audio.volume = 1.0; let audioDuration = 0; await new Promise((resolve, reject) => { audio.onended = () => { URL.revokeObjectURL(audioUrl); resolve(); }; audio.onerror = (e) => { URL.revokeObjectURL(audioUrl); console.error('[可乐] 音频播放错误:', e); reject(new Error('音频播放失败')); }; audio.oncanplaythrough = () => { audioDuration = audio.duration; console.log('[可乐] 音频可以播放,时长:', audioDuration); }; audio.play().then(() => { console.log('[可乐] 音频开始播放'); }).catch(err => { console.error('[可乐] 音频播放被阻止:', err); reject(err); }); }); // 播放成功后缓存音频(用于通话结束后选择保存) callState.voiceCache.push({ text: text, audioBlob: audioBlob, duration: audioDuration || (audioBlob.size / 16000) // 估算时长 }); console.log('[可乐] 语音已缓存,当前缓存数量:', callState.voiceCache.length); } catch (err) { console.error('[可乐] TTS 播放失败:', err); // 显示错误提示 const errorMsg = err.message || '语音播放失败'; updateStatus('语音失败'); showToast('语音合成失败: ' + errorMsg.substring(0, 30), '⚠️'); await new Promise(r => setTimeout(r, 1500)); } finally { callState.isPlaying = false; if (callState.isConnected) { updateStatus('通话中'); } } } /** * 开始录音(按住说话) */ async function startRecording() { if (!callState.isConnected || callState.isRecording || callState.isProcessing || callState.isPlaying) { return; } try { await callState.recorder.start(); callState.isRecording = true; updateTalkButton(true); updateStatus('正在录音...'); } catch (err) { console.error('[可乐] 开始录音失败:', err); alert(err.message); } } /** * 停止录音并处理 */ async function stopRecording() { if (!callState.isRecording) return; callState.isRecording = false; updateTalkButton(false); try { const audioBlob = await callState.recorder.stop(); if (audioBlob.size < 1000) { console.log('[可乐] 录音太短,忽略'); updateStatus('通话中'); return; } callState.isProcessing = true; updateStatus('识别中...'); // STT 语音转文字 const userText = await speechToText(audioBlob); if (!userText || !userText.trim()) { console.log('[可乐] 未识别到语音'); showToast('未识别到语音内容', 'info'); updateStatus('通话中'); callState.isProcessing = false; return; } console.log('[可乐] 用户说:', userText); // 添加用户消息 addCallMessage('user', userText); // 调用 AI(带超时保护,使用实时语音专用函数) updateStatus('AI思考中...'); const { callRealVoiceAI } = await import('./ai.js'); // 30秒超时 const aiPromise = callRealVoiceAI( callState.contact, userText, callState.messages.slice(0, -1), callState.initiator ); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('AI响应超时')), 30000) ); const aiResponse = await Promise.race([aiPromise, timeoutPromise]); // 清理回复 let reply = cleanAIReply(aiResponse); callState.isProcessing = false; if (!reply) { updateStatus('通话中'); return; } // 添加 AI 消息 addCallMessage('ai', reply); // TTS 并播放 await speakText(reply); // 检查是否要挂断 if (detectHangupIntent(reply)) { setTimeout(() => { if (callState.isConnected) { hangupCall(); } }, 1500); } } catch (err) { console.error('[可乐] 语音处理失败:', err); callState.isProcessing = false; updateStatus('通话中'); // 显示具体错误 const errorMsg = err.message || '处理失败'; showToast('语音处理失败: ' + errorMsg.substring(0, 30), '⚠️'); } } /** * 取消录音 */ function cancelRecording() { if (callState.recorder) { callState.recorder.cancel(); } callState.isRecording = false; updateTalkButton(false); updateStatus('通话中'); } /** * 处理文字输入(不支持录音时的替代方案) */ async function processUserTextInput(userText) { if (!callState.isConnected || callState.isProcessing || callState.isPlaying) { return; } try { console.log('[可乐] 用户输入:', userText); // 添加用户消息 addCallMessage('user', userText); callState.isProcessing = true; // 调用 AI(带超时保护) updateStatus('AI思考中...'); const { callRealVoiceAI } = await import('./ai.js'); // 30秒超时 const aiPromise = callRealVoiceAI( callState.contact, userText, callState.messages.slice(0, -1), callState.initiator ); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('AI响应超时')), 30000) ); const aiResponse = await Promise.race([aiPromise, timeoutPromise]); // 清理回复 let reply = cleanAIReply(aiResponse); callState.isProcessing = false; if (!reply) { updateStatus('通话中'); return; } // 添加 AI 消息 addCallMessage('ai', reply); // TTS 并播放 await speakText(reply); // 检查是否要挂断 if (detectHangupIntent(reply)) { setTimeout(() => { if (callState.isConnected) { hangupCall(); } }, 1500); } } catch (err) { console.error('[可乐] 文字处理失败:', err); callState.isProcessing = false; updateStatus('通话中'); const errorMsg = err.message || '处理失败'; showToast('处理失败: ' + errorMsg.substring(0, 30), '⚠️'); } } /** * 检测挂断意图 */ function detectHangupIntent(text) { if (!text) return false; const hangupPatterns = [ /我(先)?挂了/, /那我挂了/, /先挂(了)?啊?/, /挂了(啊|哈|呀|哦)?$/, /拜拜.*挂/, /再见.*挂/ ]; return hangupPatterns.some(pattern => pattern.test(text)); } /** * 更新状态显示 */ function updateStatus(text) { const statusEl = document.getElementById('wechat-real-voice-call-status'); if (statusEl) { statusEl.textContent = text; } } /** * 更新说话按钮状态 */ function updateTalkButton(isRecording) { const btn = document.getElementById('wechat-real-voice-call-talk-btn'); if (btn) { if (isRecording) { btn.classList.add('recording'); btn.textContent = '点击 发送'; } else { btn.classList.remove('recording'); btn.textContent = '点击 说话'; } } } /** * 添加通话消息 */ function addCallMessage(role, content) { const messagesEl = document.getElementById('wechat-real-voice-call-messages'); if (!messagesEl) return; callState.messages.push({ role, content }); const msgDiv = document.createElement('div'); msgDiv.className = `wechat-real-voice-call-msg ${role} fade-in`; // 显示文字内容 const textSpan = document.createElement('span'); textSpan.className = 'msg-text'; textSpan.textContent = content; msgDiv.appendChild(textSpan); messagesEl.appendChild(msgDiv); messagesEl.scrollTop = messagesEl.scrollHeight; } /** * 挂断电话(用户主动挂断) */ export async function hangupCall() { // 如果已经在挂断中,忽略 if (callState.isHangingUp) return; callState.isHangingUp = true; // 先保存需要的值(后面状态会变) const wasConnected = callState.isConnected; const cachedVoices = callState.voiceCache ? [...callState.voiceCache] : []; const contactIdx = callState.contactIndex; const callTimestamp = callState.startTime || Date.now(); console.log('[可乐] 挂断时状态:', { wasConnected, voiceCacheLength: cachedVoices.length }); // 停止录音 if (callState.recorder) { callState.recorder.cancel(); } // 停止当前播放 if (callState.currentAudio) { callState.currentAudio.pause(); callState.currentAudio = null; } // 如果通话已接通,让 AI 说再见 if (callState.isConnected && !callState.isProcessing) { try { updateStatus('对方正在说话...'); // 调用 AI 生成告别语 const { callRealVoiceAI } = await import('./ai.js'); const goodbyePrompt = '[用户正在挂断电话,请简短地说再见,一句话即可]'; const aiResponse = await Promise.race([ callRealVoiceAI( callState.contact, goodbyePrompt, callState.messages, callState.initiator ), new Promise((_, reject) => setTimeout(() => reject(new Error('超时')), 5000)) ]); const reply = cleanAIReply(aiResponse); if (reply) { addCallMessage('ai', reply); // TTS 播放告别语 await speakText(reply); } } catch (err) { console.log('[可乐] AI告别语生成失败:', err.message); } } // 计算通话时长 let durationStr = '00:00'; if (callState.isConnected && callState.startTime) { const elapsed = Math.floor((Date.now() - callState.startTime) / 1000); const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); const seconds = (elapsed % 60).toString().padStart(2, '0'); durationStr = `${minutes}:${seconds}`; } // 添加通话记录到聊天历史 if (callState.contact) { const contact = callState.contact; if (!contact.chatHistory) { contact.chatHistory = []; } const now = new Date(); const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`; let callContent; let lastMessage; if (callState.isConnected) { callContent = `[实时语音:${durationStr}]`; lastMessage = `实时语音 ${durationStr}`; } else { if (callState.initiator === 'user') { if (callState.rejectedByAI) { callContent = '[实时语音:对方已拒绝]'; lastMessage = '对方已拒绝'; } else { callContent = '[实时语音:已取消]'; lastMessage = '已取消'; } } else if (callState.rejectedByUser) { callContent = '[实时语音:已拒绝]'; lastMessage = '已拒绝'; } else { callContent = '[实时语音:对方已取消]'; lastMessage = '对方已取消'; } } // 通话记录消息 const callRecord = { role: callState.initiator === 'user' ? 'user' : 'assistant', content: callContent, time: timeStr, timestamp: Date.now(), isCallRecord: true, isRealVoice: true }; contact.chatHistory.push(callRecord); // 保存通话历史 if (callState.messages && callState.messages.length > 0) { contact.realVoiceCallHistory = Array.isArray(contact.realVoiceCallHistory) ? contact.realVoiceCallHistory : []; contact.realVoiceCallHistory.push({ type: 'real-voice', initiator: callState.initiator, duration: durationStr, time: timeStr, timestamp: Date.now(), messages: callState.messages.map(m => ({ role: m.role, content: m.content })) }); } contact.lastMessage = lastMessage; // 在聊天界面显示通话记录 if (currentChatIndex === callState.contactIndex) { appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', durationStr, contact); } requestSave(); refreshChatList(); } // 隐藏通话页面 const page = document.getElementById('wechat-real-voice-call-page'); if (page) { page.classList.add('hidden'); } clearInterval(callState.timerInterval); clearInterval(callState.dotsInterval); // 如果有缓存的语音,显示保存弹窗(使用之前保存的变量,因为 callState 可能已被修改) console.log('[可乐] 检查是否显示语音保存弹窗:', { wasConnected, cachedVoicesLength: cachedVoices.length }); if (cachedVoices.length > 0 && wasConnected) { // 重置状态 resetCallState(); // 显示语音保存弹窗 showVoiceSaveModal(cachedVoices, contactIdx, callTimestamp); } else { // 重置状态 resetCallState(); } } /** * 重置通话状态 */ function resetCallState() { callState.isActive = false; callState.isConnected = false; callState.isHangingUp = false; callState.startTime = null; callState.isRecording = false; callState.isProcessing = false; callState.isPlaying = false; callState.voiceCache = []; } /** * 在聊天界面显示通话记录 */ function appendCallRecordMessage(role, duration, contact) { const messagesContainer = document.getElementById('wechat-chat-messages'); if (!messagesContainer) return; const messageDiv = document.createElement('div'); messageDiv.className = `wechat-message ${role === 'user' ? 'self' : ''}`; const firstChar = contact?.name ? contact.name.charAt(0) : '?'; let userAvatarContent = '我'; try { const settings = getSettings(); if (settings.userAvatar) { userAvatarContent = ``; } } catch (e) {} const avatarContent = role === 'user' ? userAvatarContent : (contact?.avatar ? `` : firstChar); // 麦克风图标 const micIconSVG = ` `; messageDiv.innerHTML = `
${avatarContent}
${micIconSVG} 实时语音 ${duration}
`; messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } /** * 接听来电 */ function acceptIncomingCall() { if (!callState.isActive || callState.isConnected) return; onCallConnected(); } /** * 拒绝来电 */ function rejectIncomingCall() { if (!callState.isActive || callState.isConnected) return; callState.rejectedByUser = true; hangupCall(); } /** * 切换静音 */ function toggleMute() { callState.isMuted = !callState.isMuted; const muteBtn = document.getElementById('wechat-real-voice-call-mute'); if (muteBtn) { const btn = muteBtn.querySelector('.wechat-real-voice-call-action-btn'); const label = muteBtn.querySelector('.wechat-real-voice-call-action-label'); if (btn) btn.classList.toggle('muted', callState.isMuted); if (label) label.textContent = callState.isMuted ? '已静音' : '静音'; } } /** * 绑定事件 */ let eventsBound = false; function bindCallEvents() { if (eventsBound) return; eventsBound = true; // 挂断 document.getElementById('wechat-real-voice-call-hangup')?.addEventListener('click', hangupCall); // 静音 document.getElementById('wechat-real-voice-call-mute')?.addEventListener('click', toggleMute); // 最小化 document.getElementById('wechat-real-voice-call-minimize')?.addEventListener('click', hangupCall); // 接听 document.getElementById('wechat-real-voice-call-accept')?.addEventListener('click', acceptIncomingCall); // 拒绝 document.getElementById('wechat-real-voice-call-reject')?.addEventListener('click', rejectIncomingCall); // 说话按钮(点击切换模式:点一次开始录音,再点一次停止录音) const talkBtn = document.getElementById('wechat-real-voice-call-talk-btn'); if (talkBtn) { const toggleRecording = (e) => { e.preventDefault(); if (callState.isRecording) { stopRecording(); } else { startRecording(); } }; // PC 端点击事件 talkBtn.addEventListener('click', toggleRecording); // 移动端触摸事件 talkBtn.addEventListener('touchend', (e) => { e.preventDefault(); toggleRecording(e); }); } // 文字输入发送按钮(不支持录音时使用) const textSendBtn = document.getElementById('wechat-real-voice-call-text-send'); const textInput = document.getElementById('wechat-real-voice-call-text-input'); if (textSendBtn && textInput) { const sendTextMessage = async () => { const text = textInput.value.trim(); if (!text || callState.isProcessing || callState.isPlaying) return; textInput.value = ''; await processUserTextInput(text); }; textSendBtn.addEventListener('click', sendTextMessage); // 回车发送 textInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); sendTextMessage(); } }); } } /** * 显示语音保存弹窗 */ function showVoiceSaveModal(voiceList, contactIndex, callTimestamp) { const modal = document.getElementById('wechat-voice-save-modal'); const listEl = document.getElementById('wechat-voice-save-list'); if (!modal || !listEl) { console.log('[可乐] 语音保存弹窗元素不存在'); return; } // 清空并填充列表 listEl.innerHTML = ''; voiceList.forEach((voice, index) => { const item = document.createElement('div'); item.className = 'wechat-voice-save-item'; item.dataset.index = index; const durationSec = Math.round(voice.duration || 0); const durationStr = durationSec > 0 ? `${durationSec}"` : '?秒'; item.innerHTML = `
${escapeHtml(voice.text.substring(0, 50))}${voice.text.length > 50 ? '...' : ''}
${durationStr}
`; listEl.appendChild(item); }); // 绑定试听按钮 listEl.querySelectorAll('.wechat-voice-save-play').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const idx = parseInt(btn.dataset.index); const voice = voiceList[idx]; if (voice && voice.audioBlob) { try { btn.disabled = true; btn.innerHTML = ''; const audioUrl = URL.createObjectURL(voice.audioBlob); const audio = new Audio(audioUrl); audio.onended = () => { URL.revokeObjectURL(audioUrl); btn.disabled = false; btn.innerHTML = ''; }; audio.onerror = () => { URL.revokeObjectURL(audioUrl); btn.disabled = false; btn.innerHTML = ''; }; await audio.play(); } catch (err) { console.error('[可乐] 试听失败:', err); btn.disabled = false; btn.innerHTML = ''; } } }); }); // 绑定保存按钮 const confirmBtn = document.getElementById('wechat-voice-save-confirm'); const skipBtn = document.getElementById('wechat-voice-save-skip'); const cancelBtn = document.getElementById('wechat-voice-save-cancel'); const closeModal = () => { modal.classList.add('hidden'); }; const handleSave = async () => { // 获取选中的语音 const selectedVoices = []; listEl.querySelectorAll('.wechat-voice-save-item').forEach((item, idx) => { const checkbox = item.querySelector('input[type="checkbox"]'); if (checkbox && checkbox.checked) { selectedVoices.push({ contactIndex: contactIndex, callTimestamp: callTimestamp, text: voiceList[idx].text, audioBlob: voiceList[idx].audioBlob, duration: voiceList[idx].duration }); } }); if (selectedVoices.length > 0) { try { await saveVoiceRecordings(selectedVoices); showToast(`已保存 ${selectedVoices.length} 条语音`, '✓'); } catch (err) { console.error('[可乐] 保存语音失败:', err); showToast('保存失败', '⚠️'); } } closeModal(); }; // 移除旧的事件监听器 const newConfirmBtn = confirmBtn.cloneNode(true); const newSkipBtn = skipBtn.cloneNode(true); confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); skipBtn.parentNode.replaceChild(newSkipBtn, skipBtn); newConfirmBtn.addEventListener('click', handleSave); newSkipBtn.addEventListener('click', closeModal); if (cancelBtn) { const newCancelBtn = cancelBtn.cloneNode(true); cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn); newCancelBtn.addEventListener('click', closeModal); } // 显示弹窗 modal.classList.remove('hidden'); } /** * HTML 转义 */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * 初始化 */ export function initRealVoiceCall() { // 事件绑定将在显示页面时进行 } /** * 获取通话状态 */ export function getRealVoiceCallState() { return { isActive: callState.isActive, isConnected: callState.isConnected, contactIndex: callState.contactIndex }; }