Files
Cola/voice-call.js
2026-01-02 02:26:21 +08:00

1082 lines
37 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.
/**
* 语音通话功能
*/
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 = `<img src="${callState.contactAvatar}" alt="" onerror="this.style.display='none';this.parentElement.textContent='${firstChar}'">`;
} 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 inputAreaEl = document.getElementById('wechat-voice-call-input-area');
if (inputAreaEl) {
inputAreaEl.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 inputAreaEl = document.getElementById('wechat-voice-call-input-area');
if (inputAreaEl) {
inputAreaEl.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 detectHangupIntent(text) {
if (!text) return false;
// 常见的挂断表达
const hangupPatterns = [
/我(先)?挂了/,
/那我挂了/,
/先挂(了)?啊?/,
/挂了(啊|哈|呀|哦)?$/,
/我(要)?挂(电话|断)了/,
/拜拜.*挂/,
/挂.*拜拜/,
/再见.*挂/,
/不聊了.*挂/,
/不说了.*挂/,
/那就这样.*挂/,
/就这样吧.*挂/
];
return hangupPatterns.some(pattern => pattern.test(text));
}
// 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 = `<img src="${settings.userAvatar}" alt="" onerror="this.style.display='none';this.parentElement.textContent='我'">`;
}
} catch (e) {}
const avatarContent = role === 'user'
? userAvatarContent
: (contact?.avatar
? `<img src="${contact.avatar}" alt="" onerror="this.style.display='none';this.parentElement.innerHTML='${firstChar}'">`
: firstChar);
// 通话记录卡片内容
// 线条电话图标
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>`;
let callRecordHTML;
if (status === 'connected') {
// 已接通:显示通话时长
callRecordHTML = `
<div class="wechat-call-record">
${phoneIconSVG}
<span class="wechat-call-record-text">通话时长 ${duration}</span>
</div>
`;
} else if (status === 'cancelled') {
// 用户发起未接通:已取消(绿色)
callRecordHTML = `
<div class="wechat-call-record">
${phoneIconSVG}
<span class="wechat-call-record-text">已取消</span>
</div>
`;
} else if (status === 'rejectedByAI') {
// 用户发起AI拒接对方已拒绝绿色和通话时长样式一致
callRecordHTML = `
<div class="wechat-call-record">
${phoneIconSVG}
<span class="wechat-call-record-text">对方已拒绝</span>
</div>
`;
} else if (status === 'rejected') {
// AI发起用户主动拒绝深灰色
callRecordHTML = `
<div class="wechat-call-record wechat-call-rejected">
${phoneIconSVG}
<span class="wechat-call-record-text">已拒绝</span>
</div>
`;
} else {
// AI发起超时未接对方已取消绿色
callRecordHTML = `
<div class="wechat-call-record">
${phoneIconSVG}
<span class="wechat-call-record-text">对方已取消</span>
</div>
`;
}
messageDiv.innerHTML = `
<div class="wechat-message-avatar">${avatarContent}</div>
<div class="wechat-message-content"><div class="wechat-bubble wechat-call-record-bubble">${callRecordHTML}</div></div>
`;
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();
}
});
// 移动端键盘收起后重置滚动位置
document.getElementById('wechat-voice-call-input')?.addEventListener('blur', () => {
// 延迟执行以等待键盘完全收起
setTimeout(() => {
window.scrollTo(0, 0);
// 重置可能被移动的元素
const page = document.getElementById('wechat-voice-call-page');
if (page) {
page.style.transform = '';
page.style.top = '0';
}
}, 100);
});
}
// 接听来电
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*meme\s*>/gi, '').trim();
reply = reply.replace(/<meme>[^<]*<\/meme>/gi, '').trim();
reply = reply.replace(/<\/?meme>/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*meme\s*>/gi, '').trim();
reply = reply.replace(/<meme>[^<]*<\/meme>/gi, '').trim();
reply = reply.replace(/<\/?meme>/gi, '').trim();
if (!reply) continue;
if (/^\[(?:表情|照片|分享音乐|音乐)[:]/.test(reply)) continue;
// 移除可能的特殊标记
reply = reply.replace(/\[.*?\]/g, '').trim();
// 过滤掉泄露的提示词或内部指令
// 1. 以负数开头的行(如 -5000
reply = reply.replace(/^-\d+\s*.*/gm, '').trim();
// 2. 包含"我需要"+"回复/做出/扮演"等指令性内容
if (/我需要.*(回复|做出|扮演|以.*身份)/.test(reply)) {
// 尝试提取 --- 后面的实际内容
const dashMatch = reply.match(/---+\s*(.+)$/);
if (dashMatch) {
reply = dashMatch[1].trim();
} else {
continue; // 跳过这条消息
}
}
// 3. 移除 --- 分隔符前面的内容(如果有的话)
if (reply.includes('---')) {
const parts2 = reply.split(/---+/);
reply = parts2[parts2.length - 1].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回复完成后检查是否要主动挂断
// 1. 检测AI的挂断意图如"我挂了"、"先挂了"等)
const fullReply = parts.join(' ');
if (detectHangupIntent(fullReply)) {
console.log('[可乐] 检测到AI挂断意图:', fullReply);
setTimeout(() => {
if (callState.isConnected) {
aiHangup();
}
}, 1500 + Math.random() * 1000);
return;
}
// 2. 随机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 = `
<div class="wechat-message-bubble wechat-typing">
<span class="wechat-typing-dot"></span>
<span class="wechat-typing-dot"></span>
<span class="wechat-typing-dot"></span>
</div>
`;
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 => `
<div class="wechat-voice-call-msg ${msg.role}">${escapeHtml(msg.content)}</div>
`).join('');
// 滚动到底部
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// 初始化
export function initVoiceCall() {
// 事件绑定将在显示页面时进行
}