/** * 语音通话功能 */ import { getSettings, splitAIMessages } from './config.js'; import { currentChatIndex } from './chat.js'; import { requestSave } from './save-manager.js'; import { refreshChatList } from './ui.js'; import { escapeHtml } from './utils.js'; // 通话状态 let callState = { isActive: false, isConnected: false, isMuted: false, isSpeakerOn: false, startTime: null, timerInterval: null, dotsInterval: null, connectTimeout: null, // 连接超时计时器 aiHangupTimeout: null, // AI主动挂断计时器 contactIndex: -1, contactName: '', contactAvatar: '', messages: [], // 通话中的消息 contact: null, initiator: 'user', // 谁发起的通话: 'user' 或 'ai' rejectedByUser: false, // 是否被用户主动拒绝 rejectedByAI: false, // 是否被AI主动拒绝 hungUpByAI: false // 是否被AI主动挂断 }; // 开始语音通话 export function startVoiceCall(initiator = 'user', contactIndex = currentChatIndex) { if (callState.isActive) return; if (contactIndex < 0) return; const settings = getSettings(); const contact = settings.contacts[contactIndex]; if (!contact) 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.isSpeakerOn = false; callState.messages = []; // 重置消息 callState.initiator = initiator; // 记录谁发起的通话 callState.rejectedByUser = false; // 重置用户拒绝状态 callState.rejectedByAI = false; // 重置AI拒绝状态 callState.hungUpByAI = false; // 重置AI挂断状态 showCallPage(); startConnecting(); } // 显示通话页面 function showCallPage() { const page = document.getElementById('wechat-voice-call-page'); if (!page) return; // 设置头像 const avatarEl = document.getElementById('wechat-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-voice-call-name'); if (nameEl) { nameEl.textContent = callState.contactName; } // 设置状态 - 根据发起者显示不同文案 const statusEl = document.getElementById('wechat-voice-call-status'); if (statusEl) { if (callState.initiator === 'ai') { statusEl.textContent = '邀请你语音通话...'; } else { statusEl.textContent = '等待对方接受邀请'; } statusEl.classList.add('connecting'); } // 重置时间显示 - 等待时隐藏 const timeEl = document.getElementById('wechat-voice-call-time'); if (timeEl) { timeEl.textContent = '00:00'; timeEl.classList.add('hidden'); } // 重置按钮状态 updateMuteButton(); updateSpeakerButton(); // 隐藏对话框并清空消息 const chatEl = document.getElementById('wechat-voice-call-chat'); if (chatEl) { chatEl.classList.add('hidden'); } const messagesEl = document.getElementById('wechat-voice-call-messages'); if (messagesEl) { messagesEl.innerHTML = ''; } // 根据发起者显示不同的操作按钮 const incomingActionsEl = document.getElementById('wechat-voice-call-incoming-actions'); const callActionsEl = document.getElementById('wechat-voice-call-actions'); if (callState.initiator === 'ai') { // 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-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, 'voice'); if (!callState.isActive) return; // 用户可能已经取消 if (shouldAnswer) { // AI决定接听 if (callState.isActive && !callState.isConnected) { onCallConnected(); } } else { // AI决定拒接 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, callType = 'voice') { if (!contact) return true; try { const { callAI } = await import('./ai.js'); const callTypeText = callType === 'video' ? '视频' : '语音'; const prompt = `[用户正在给你打${callTypeText}电话,你需要决定是否接听] 根据你的性格和当前心情决定: - 如果你想接听,只回复:[接听] - 如果你不想接听(比如在忙、生气、故意不接、想让用户着急等),只回复:[拒接] 【绝对禁止】 - 只能回复 [接听] 或 [拒接],不能有任何其他文字! - [接听] 或 [拒接] 必须独立成行,前后不能有任何内容! × 错误:好吧[接听] ← 有其他文字,错误! × 错误:[拒接]哼 ← 有其他文字,错误! √ 正确:[接听] √ 正确:[拒接] 注意:大多数情况下你应该接听,只有特殊情况才拒接。`; 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-voice-call-status'); if (statusEl) { statusEl.textContent = '通话中'; statusEl.classList.remove('connecting'); } // 显示计时器 const timeEl = document.getElementById('wechat-voice-call-time'); if (timeEl) { timeEl.classList.remove('hidden'); } // 显示对话框 const chatEl = document.getElementById('wechat-voice-call-chat'); if (chatEl) { chatEl.classList.remove('hidden'); } // 切换到通话中按钮(隐藏来电按钮,显示通话控制按钮) const incomingActionsEl = document.getElementById('wechat-voice-call-incoming-actions'); const callActionsEl = document.getElementById('wechat-voice-call-actions'); if (incomingActionsEl) incomingActionsEl.classList.add('hidden'); if (callActionsEl) callActionsEl.classList.remove('hidden'); // 开始计时 startCallTimer(); // 如果是AI发起的通话,接通后AI自动发送第一条消息 if (callState.initiator === 'ai') { triggerAIGreeting(); } // 启动AI主动挂断检查(通话30秒后开始随机检查) scheduleAIHangupCheck(); } // 调度AI主动挂断检查 // 通话接通后30秒开始,每次用户发消息后AI回复时有5%概率挂断 // 同时设置一个180秒(3分钟)的保底挂断时间 function scheduleAIHangupCheck() { // 清除已有的计时器 clearTimeout(callState.aiHangupTimeout); // 设置保底挂断时间:通话3分钟后有50%概率挂断,超过5分钟必定挂断 const checkTime = 180000 + Math.random() * 120000; // 3-5分钟 callState.aiHangupTimeout = setTimeout(() => { if (callState.isConnected) { // 50%概率挂断,否则再等1-2分钟 if (Math.random() < 0.5) { aiHangup(); } else { // 再设置一个60-120秒后的必定挂断 callState.aiHangupTimeout = setTimeout(() => { if (callState.isConnected) { aiHangup(); } }, 60000 + Math.random() * 60000); } } }, checkTime); } // 每次AI回复后检查是否要挂断(5%概率,通话30秒后生效) export function checkAIHangupAfterReply() { if (!callState.isConnected || !callState.startTime) return false; // 通话至少30秒后才开始随机挂断检查 const elapsed = Date.now() - callState.startTime; if (elapsed < 30000) return false; // 5%概率挂断 if (Math.random() < 0.05) { // 延迟1-3秒后挂断,更自然 setTimeout(() => { if (callState.isConnected) { aiHangup(); } }, 1000 + Math.random() * 2000); return true; } return false; } // AI主动挂断电话 function aiHangup() { if (!callState.isConnected) return; console.log('[可乐] AI主动挂断电话'); callState.hungUpByAI = true; hangupCall(); } // 开始通话计时 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-voice-call-time'); if (timeEl) { timeEl.textContent = `${minutes}:${seconds}`; } }, 1000); } // 挂断电话 export function hangupCall() { // 清除AI挂断计时器 clearTimeout(callState.aiHangupTimeout); // 计算通话时长 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 settings = getSettings(); 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) { // 用户发起,AI拒接 callContent = '[通话记录:对方已拒绝]'; lastMessage = '对方已拒绝'; } else { // 用户发起,用户取消 callContent = '[通话记录:已取消]'; lastMessage = '已取消'; } } else if (callState.rejectedByUser) { // AI发起,用户主动拒绝 callContent = '[通话记录:已拒绝]'; lastMessage = '已拒绝'; } else { // AI发起,超时未接(对方取消) callContent = '[通话记录:对方已取消]'; lastMessage = '对方已取消'; } } // 通话记录消息 const callRecord = { role: callState.initiator === 'user' ? 'user' : 'assistant', content: callContent, time: timeStr, timestamp: Date.now(), isCallRecord: true }; contact.chatHistory.push(callRecord); // 通话内容只进"通话历史",不在主聊天界面展示(避免污染主界面/列表预览) if (callState.messages && callState.messages.length > 0) { const callStatusForHistory = callState.isConnected ? 'connected' : (callState.initiator === 'user' ? (callState.rejectedByAI ? 'rejectedByAI' : 'cancelled') : (callState.rejectedByUser ? 'rejected' : 'timeout')); contact.callHistory = Array.isArray(contact.callHistory) ? contact.callHistory : []; contact.callHistory.push({ type: 'voice', initiator: callState.initiator, status: callStatusForHistory, duration: durationStr, time: timeStr, timestamp: Date.now(), messages: callState.messages.map(m => ({ role: m.role, content: m.content })) }); } contact.lastMessage = lastMessage; // 在聊天界面显示通话记录 // 传递状态类型: 'connected' | 'cancelled' | 'rejected' | 'rejectedByAI' | 'timeout' let callStatus = 'connected'; if (!callState.isConnected) { if (callState.initiator === 'user') { callStatus = callState.rejectedByAI ? 'rejectedByAI' : 'cancelled'; } else if (callState.rejectedByUser) { callStatus = 'rejected'; } else { callStatus = 'timeout'; } } if (currentChatIndex === callState.contactIndex) { appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', callStatus, durationStr, contact); } // AI 对通话结束做出反应(所有情况都触发) triggerCallEndReaction(contact, callStatus, callState.initiator, callState.messages, callState.hungUpByAI); requestSave(); refreshChatList(); } callState.isActive = false; callState.isConnected = false; callState.startTime = null; clearInterval(callState.timerInterval); clearInterval(callState.dotsInterval); const page = document.getElementById('wechat-voice-call-page'); if (page) { page.classList.add('hidden'); } } // 在聊天界面显示通话记录消息 // status: 'connected' | 'cancelled' | 'rejected' | 'timeout' function appendCallRecordMessage(role, status, 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 phoneIconSVG = ` `; let callRecordHTML; if (status === 'connected') { // 已接通:显示通话时长 callRecordHTML = `
${phoneIconSVG} 通话时长 ${duration}
`; } else if (status === 'cancelled') { // 用户发起未接通:已取消(绿色) callRecordHTML = `
${phoneIconSVG} 已取消
`; } else if (status === 'rejectedByAI') { // 用户发起,AI拒接:对方已拒绝(绿色,和通话时长样式一致) callRecordHTML = `
${phoneIconSVG} 对方已拒绝
`; } else if (status === 'rejected') { // AI发起,用户主动拒绝(深灰色) callRecordHTML = `
${phoneIconSVG} 已拒绝
`; } else { // AI发起,超时未接:对方已取消(绿色) callRecordHTML = `
${phoneIconSVG} 对方已取消
`; } messageDiv.innerHTML = `
${avatarContent}
${callRecordHTML}
`; messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 切换静音 function toggleMute() { callState.isMuted = !callState.isMuted; updateMuteButton(); } // 更新静音按钮状态 function updateMuteButton() { const muteAction = document.getElementById('wechat-voice-call-mute'); if (!muteAction) return; const btn = muteAction.querySelector('.wechat-voice-call-action-btn'); const label = muteAction.querySelector('.wechat-voice-call-action-label'); if (btn) { if (callState.isMuted) { btn.classList.add('muted'); } else { btn.classList.remove('muted'); } } if (label) { label.textContent = callState.isMuted ? '麦克风已关' : '麦克风已开'; } } // 切换扬声器 function toggleSpeaker() { callState.isSpeakerOn = !callState.isSpeakerOn; updateSpeakerButton(); } // 更新扬声器按钮状态 function updateSpeakerButton() { const speakerAction = document.getElementById('wechat-voice-call-speaker'); if (!speakerAction) return; const btn = speakerAction.querySelector('.wechat-voice-call-action-btn'); const label = speakerAction.querySelector('.wechat-voice-call-action-label'); if (btn) { if (callState.isSpeakerOn) { btn.classList.add('active'); } else { btn.classList.remove('active'); } } if (label) { label.textContent = callState.isSpeakerOn ? '扬声器已开' : '扬声器已关'; } } // 绑定事件 let eventsBound = false; function bindCallEvents() { if (eventsBound) return; eventsBound = true; // 挂断(用户主动点击) document.getElementById('wechat-voice-call-hangup')?.addEventListener('click', userHangup); // 静音 document.getElementById('wechat-voice-call-mute')?.addEventListener('click', toggleMute); // 扬声器 document.getElementById('wechat-voice-call-speaker')?.addEventListener('click', toggleSpeaker); // 最小化(暂时也是挂断) document.getElementById('wechat-voice-call-minimize')?.addEventListener('click', userHangup); // 来电接听按钮 document.getElementById('wechat-voice-call-accept')?.addEventListener('click', acceptIncomingCall); // 来电拒绝按钮 document.getElementById('wechat-voice-call-reject')?.addEventListener('click', rejectIncomingCall); // 发送消息 document.getElementById('wechat-voice-call-send')?.addEventListener('click', sendCallMessage); // 输入框回车发送 document.getElementById('wechat-voice-call-input')?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendCallMessage(); } }); } // 接听来电 function acceptIncomingCall() { if (!callState.isActive || callState.isConnected) return; onCallConnected(); } // 拒绝来电 function rejectIncomingCall() { if (!callState.isActive || callState.isConnected) return; callState.rejectedByUser = true; hangupCall(); } // 用户主动挂断 function userHangup() { // 如果是AI发起且未接通,标记为用户主动拒绝 if (callState.initiator === 'ai' && !callState.isConnected) { callState.rejectedByUser = true; } hangupCall(); } // AI发起通话时的开场白 async function triggerAIGreeting() { if (!callState.isConnected || !callState.contact) return; // 显示typing指示器 showCallTypingIndicator(); try { const { callVoiceAI } = await import('./ai.js'); // AI主动打电话,发送一个触发消息让AI开场 const aiResponse = await callVoiceAI( callState.contact, '[用户接听了电话]', [], 'ai' ); // 隐藏typing指示器 hideCallTypingIndicator(); // 按 ||| 分割,并将特殊标签与文本分离,避免"文字+表情包"混在同一条 const parts = splitAIMessages(aiResponse); for (const part of parts) { if (!callState.isConnected) break; let reply = part.trim(); // 通话中禁用表情包/图片/音乐等富媒体(兜底过滤) reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); if (!reply) continue; if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue; // 移除语音标记 const voiceMatch = reply.match(/^\[语音[::]\s*(.+?)\]$/); if (voiceMatch) { reply = voiceMatch[1]; } // 移除其他特殊标记 reply = reply.replace(/\[.*?\]/g, '').trim(); if (reply) { // 分离小括号内容和说话内容 // 提取所有括号内的语气描述 const moodMatches = reply.match(/([^)]+)/g); // 移除所有括号内容得到说话部分 const speech = reply.replace(/([^)]+)/g, '').trim(); // 先发送说话内容 if (speech) { showCallTypingIndicator(); await new Promise(r => setTimeout(r, 400 + Math.random() * 400)); hideCallTypingIndicator(); if (callState.isConnected) addCallMessage('ai', speech); } // 再发送语气描述(合并所有语气) if (moodMatches && moodMatches.length > 0) { const combinedMood = moodMatches.join('').replace(/)(/g, ','); showCallTypingIndicator(); await new Promise(r => setTimeout(r, 300 + Math.random() * 300)); hideCallTypingIndicator(); if (callState.isConnected) addCallMessage('ai', combinedMood); } // 如果没有括号,直接发送 if (!moodMatches && !speech) { showCallTypingIndicator(); await new Promise(r => setTimeout(r, 500 + Math.random() * 400)); hideCallTypingIndicator(); if (callState.isConnected) addCallMessage('ai', reply); } } } } catch (err) { hideCallTypingIndicator(); console.error('[可乐] AI通话开场白失败:', err); } } // AI 对通话结束做出反应 async function triggerCallEndReaction(contact, callStatus, initiator, callMessages = [], hungUpByAI = false) { if (!contact) return; // 构建反应提示 let reactionPrompt; if (callStatus === 'cancelled') { // 用户取消了自己发起的通话 reactionPrompt = '[用户刚才给你打了电话,但还没等你接就取消了。请对此做出自然的反应,可以表示疑惑、好奇或关心,问问用户怎么了。回复1-2句话即可,简短自然。]'; } else if (callStatus === 'rejectedByAI') { // AI主动拒绝了用户的来电 reactionPrompt = '[你刚才拒绝了用户的电话。请对此做出自然的反应,解释为什么不接(比如在忙、不方便、想让对方着急一下、生气中等)。回复1-2句话即可,简短自然,符合你的性格。]'; } else if (callStatus === 'rejected') { // AI发起的通话被用户拒绝 reactionPrompt = '[你刚才给用户打电话,但用户直接挂断拒接了。请对此做出自然的反应,可以表示失落、委屈或疑惑。回复1-2句话即可,简短自然。]'; } else if (callStatus === 'timeout') { // AI发起的通话超时未接 reactionPrompt = '[你刚才给用户打电话,但用户没有接听。请对此做出自然的反应,可以表示担心、疑惑或轻微失落。回复1-2句话即可,简短自然。]'; } else if (callStatus === 'connected') { // 已接通的通话正常结束 // 根据通话内容生成回复 if (callMessages && callMessages.length > 0) { const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n'); if (hungUpByAI) { // AI主动挂断的情况 reactionPrompt = `[语音通话刚刚挂断了(是你主动挂的),现在回到微信文字聊天。通话最后几句是: ${lastMessages} 【重要】是你主动挂断的电话,你现在是发微信消息。请根据通话内容自然收尾: - 可能是聊完了正常告别 - 可能是有事要忙、来不及了 - 可能是情绪原因(害羞、生气、不想聊了等) 回复1句话,符合你的人设性格。]`; } else { // 用户挂断的情况 reactionPrompt = `[语音通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是: ${lastMessages} 【重要】通话已结束,你现在是发微信消息,不是继续语音通话。你应该对"挂断"这件事本身做反应: - 如果是正常告别后挂的:简单告别或表达心情 - 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事 绝对不要继续或延续通话里正在进行的内容或动作。回复1句话,符合你的性格。]`; } } else { if (hungUpByAI) { reactionPrompt = '[语音通话刚刚挂断了(是你主动挂的),现在回到微信文字聊天。请对此做出简单反应,符合你的人设性格。回复1句话。]'; } else { reactionPrompt = '[语音通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]'; } } } else { return; // 未知状态不处理 } try { const { callAI } = await import('./ai.js'); const { appendMessage, showTypingIndicator, hideTypingIndicator } = await import('./chat.js'); const shouldRenderInChat = currentChatIndex === callState.contactIndex; // 只在当前聊天界面显示 typing/气泡,避免串到别的聊天 if (shouldRenderInChat) { showTypingIndicator(contact); } const aiResponse = await callAI(contact, reactionPrompt); if (shouldRenderInChat) { hideTypingIndicator(); } // 按 ||| 分割,并将特殊标签与文本分离,避免“文字+表情包”混在同一条 const parts = splitAIMessages(aiResponse); for (const part of parts) { let reply = part.trim(); // 通话中禁用表情包/图片/音乐等富媒体(兜底过滤) reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); if (!reply) continue; if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue; // 移除可能的特殊标记 reply = reply.replace(/\[.*?\]/g, '').trim(); if (reply) { // 保存到聊天历史 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')}`; if (!contact.chatHistory) contact.chatHistory = []; contact.chatHistory.push({ role: 'assistant', content: reply, time: timeStr, timestamp: Date.now() }); contact.lastMessage = reply; if (shouldRenderInChat) { // 显示到UI appendMessage('assistant', reply, contact); } else { contact.unreadCount = (contact.unreadCount || 0) + 1; } // 每条消息之间稍微延迟 await new Promise(r => setTimeout(r, 600 + Math.random() * 400)); } } requestSave(); refreshChatList(); } catch (err) { console.error('[可乐] AI通话结束反应失败:', err); } } // 发送通话中消息 async function sendCallMessage() { const input = document.getElementById('wechat-voice-call-input'); if (!input) return; const message = input.value.trim(); if (!message) return; if (!callState.isConnected) return; input.value = ''; // 添加用户消息 addCallMessage('user', message); // 显示typing指示器 showCallTypingIndicator(); // 调用通话专用AI try { const { callVoiceAI } = await import('./ai.js'); // 传入通话中的历史消息(不包含刚添加的用户消息) const historyMessages = callState.messages.slice(0, -1); // 传递通话发起者信息 const aiResponse = await callVoiceAI(callState.contact, message, historyMessages, callState.initiator); // 隐藏typing指示器 hideCallTypingIndicator(); // 按 ||| 分割成多条消息 const parts = aiResponse.split(/\s*\|\|\|\s*/).filter(Boolean); for (const part of parts) { if (!callState.isConnected) break; // 提取回复 let reply = part.trim(); // 移除语音标记 const voiceMatch = reply.match(/^\[语音[::]\s*(.+?)\]$/); if (voiceMatch) { reply = voiceMatch[1]; } // 移除其他特殊标记 reply = reply.replace(/\[.*?\]/g, '').trim(); if (reply) { // 分离小括号内容和说话内容 // 提取所有括号内的语气描述 const moodMatches = reply.match(/([^)]+)/g); // 移除所有括号内容得到说话部分 const speech = reply.replace(/([^)]+)/g, '').trim(); // 先发送说话内容 if (speech) { // 显示typing,模拟打字延迟 showCallTypingIndicator(); await new Promise(r => setTimeout(r, 300 + Math.random() * 400)); hideCallTypingIndicator(); if (callState.isConnected) addCallMessage('ai', speech); } // 再发送语气描述(合并所有语气) if (moodMatches && moodMatches.length > 0) { const combinedMood = moodMatches.join('').replace(/)(/g, ','); showCallTypingIndicator(); await new Promise(r => setTimeout(r, 200 + Math.random() * 300)); hideCallTypingIndicator(); if (callState.isConnected) addCallMessage('ai', combinedMood); } // 如果没有括号,直接发送 if (!moodMatches && !speech) { showCallTypingIndicator(); await new Promise(r => setTimeout(r, 300 + Math.random() * 500)); hideCallTypingIndicator(); if (callState.isConnected) addCallMessage('ai', reply); } } } // AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效) checkAIHangupAfterReply(); } catch (err) { hideCallTypingIndicator(); console.error('[可乐] 通话消息AI回复失败:', err); } } // 显示通话中的typing指示器 function showCallTypingIndicator() { const messagesEl = document.getElementById('wechat-voice-call-messages'); if (!messagesEl) return; // 移除已有的typing指示器 hideCallTypingIndicator(); const typingDiv = document.createElement('div'); typingDiv.className = 'wechat-voice-call-msg ai'; typingDiv.id = 'wechat-voice-call-typing'; typingDiv.innerHTML = `
`; messagesEl.appendChild(typingDiv); messagesEl.scrollTop = messagesEl.scrollHeight; } // 隐藏通话中的typing指示器 function hideCallTypingIndicator() { const typingEl = document.getElementById('wechat-voice-call-typing'); if (typingEl) { typingEl.remove(); } } // 添加通话消息(带渐入动画,可滚动查看所有记录) function addCallMessage(role, content) { const messagesEl = document.getElementById('wechat-voice-call-messages'); if (!messagesEl) return; // 添加到状态 callState.messages.push({ role, content }); // 创建新消息元素 const msgDiv = document.createElement('div'); msgDiv.className = `wechat-voice-call-msg ${role} fade-in`; msgDiv.textContent = content; // 添加新消息 messagesEl.appendChild(msgDiv); // 滚动到底部 messagesEl.scrollTop = messagesEl.scrollHeight; } // 渲染通话消息(初始化用) function renderCallMessages() { const messagesEl = document.getElementById('wechat-voice-call-messages'); if (!messagesEl) return; messagesEl.innerHTML = callState.messages.map(msg => `
${escapeHtml(msg.content)}
`).join(''); // 滚动到底部 messagesEl.scrollTop = messagesEl.scrollHeight; } // 初始化 export function initVoiceCall() { // 事件绑定将在显示页面时进行 }