/**
* 视频通话功能
*/
import { getSettings, splitAIMessages } from './config.js';
import { currentChatIndex } from './chat.js';
import { requestSave } from './save-manager.js';
import { refreshChatList } from './ui.js';
// 通话状态
let videoCallState = {
isActive: false,
isConnected: false,
isMuted: false,
isCameraOn: true,
startTime: null,
timerInterval: null,
dotsInterval: null,
connectTimeout: null,
aiHangupTimeout: null, // AI主动挂断计时器
contactIndex: -1,
contactName: '',
contactAvatar: '',
messages: [],
contact: null,
initiator: 'user',
rejectedByUser: false,
rejectedByAI: false, // 是否被AI主动拒绝
hungUpByAI: false // 是否被AI主动挂断
};
// 辅助函数:安全设置头像(避免 onerror 内联处理器问题)
function setAvatarSafe(el, avatarUrl, fallbackChar) {
if (!el) return;
el.innerHTML = '';
if (avatarUrl) {
const img = document.createElement('img');
img.src = avatarUrl;
img.alt = '';
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'cover';
img.onerror = () => {
img.remove();
el.textContent = fallbackChar;
};
el.appendChild(img);
} else {
el.textContent = fallbackChar;
}
}
// 开始视频通话
export function startVideoCall(initiator = 'user', contactIndex = currentChatIndex) {
if (videoCallState.isActive) return;
if (contactIndex < 0) return;
const settings = getSettings();
const contact = settings.contacts[contactIndex];
if (!contact) return;
videoCallState.contactName = contact.name;
videoCallState.contactAvatar = contact.avatar;
videoCallState.contact = contact;
videoCallState.contactIndex = contactIndex;
videoCallState.isActive = true;
videoCallState.isConnected = false;
videoCallState.isMuted = false;
videoCallState.isCameraOn = true;
videoCallState.messages = [];
videoCallState.initiator = initiator;
videoCallState.rejectedByUser = false;
videoCallState.rejectedByAI = false;
videoCallState.hungUpByAI = false;
if (initiator === 'ai') {
showIncomingCallPage();
} else {
showCallPage();
startConnecting();
}
}
// 显示AI来电界面
function showIncomingCallPage() {
const page = document.getElementById('wechat-video-call-page');
const incomingEl = document.getElementById('wechat-video-call-incoming');
if (!page || !incomingEl) return;
// 设置头像和名称
const avatarEl = document.getElementById('wechat-video-call-incoming-avatar');
const nameEl = document.getElementById('wechat-video-call-incoming-name');
const firstChar = videoCallState.contactName ? videoCallState.contactName.charAt(0) : '?';
setAvatarSafe(avatarEl, videoCallState.contactAvatar, firstChar);
if (nameEl) {
nameEl.textContent = videoCallState.contactName;
}
// 隐藏主界面元素,显示来电界面
document.getElementById('wechat-video-call-center')?.classList.add('hidden');
document.getElementById('wechat-video-call-chat')?.classList.add('hidden');
document.getElementById('wechat-video-call-input-area')?.classList.add('hidden');
document.getElementById('wechat-video-call-actions')?.classList.add('hidden');
incomingEl.classList.remove('hidden');
// 来电阶段不显示计时
const timeEl = document.getElementById('wechat-video-call-time');
if (timeEl) {
timeEl.textContent = '00:00';
timeEl.classList.add('hidden');
}
page.classList.remove('hidden');
bindVideoCallEvents();
// 5秒后如果用户没接就超时
videoCallState.connectTimeout = setTimeout(() => {
if (videoCallState.isActive && !videoCallState.isConnected) {
videoCallState.rejectedByUser = false;
hangupVideoCall();
}
}, 5000);
}
// 显示通话页面
function showCallPage() {
const page = document.getElementById('wechat-video-call-page');
if (!page) return;
// 隐藏来电界面
document.getElementById('wechat-video-call-incoming')?.classList.add('hidden');
// 设置角色头像(中间圆形)
const avatarEl = document.getElementById('wechat-video-call-avatar');
const firstChar = videoCallState.contactName ? videoCallState.contactName.charAt(0) : '?';
setAvatarSafe(avatarEl, videoCallState.contactAvatar, firstChar);
// 设置用户头像(右上角长方形小窗)
const localAvatarEl = document.getElementById('wechat-video-call-local-avatar');
if (localAvatarEl) {
try {
const settings = getSettings();
setAvatarSafe(localAvatarEl, settings.userAvatar, '我');
} catch (e) {
localAvatarEl.textContent = '我';
}
}
// 设置状态
const statusEl = document.getElementById('wechat-video-call-status');
if (statusEl) {
statusEl.textContent = '等待对方接受邀请';
}
// 显示中间区域
document.getElementById('wechat-video-call-center')?.classList.remove('hidden');
document.getElementById('wechat-video-call-actions')?.classList.remove('hidden');
// 重置时间显示
const timeEl = document.getElementById('wechat-video-call-time');
if (timeEl) {
timeEl.textContent = '00:00';
timeEl.classList.add('hidden'); // 拨打中不显示计时
}
// 隐藏对话框和输入框
document.getElementById('wechat-video-call-chat')?.classList.add('hidden');
document.getElementById('wechat-video-call-input-area')?.classList.add('hidden');
document.getElementById('wechat-video-call-messages')?.innerHTML &&
(document.getElementById('wechat-video-call-messages').innerHTML = '');
// 更新按钮状态
updateCameraButton();
updateMuteButtonVideo();
page.classList.remove('hidden');
bindVideoCallEvents();
}
// 开始连接动画
async function startConnecting() {
const statusEl = document.getElementById('wechat-video-call-status');
if (!statusEl) return;
let dotCount = 0;
clearInterval(videoCallState.dotsInterval);
clearTimeout(videoCallState.connectTimeout);
videoCallState.dotsInterval = setInterval(() => {
dotCount = (dotCount + 1) % 4;
const dots = '.'.repeat(dotCount);
statusEl.textContent = '等待对方接受邀请' + dots;
}, 500);
// 用户发起:调用AI决策是否接听
const shouldAnswer = await askAIToAnswerVideoCall(videoCallState.contact);
if (!videoCallState.isActive) return; // 用户可能已经取消
if (shouldAnswer) {
// AI决定接听
if (videoCallState.isActive && !videoCallState.isConnected) {
onVideoCallConnected();
}
} else {
// AI决定拒接
videoCallState.rejectedByAI = true;
hangupVideoCall();
}
}
// AI决定是否接听用户的视频来电
async function askAIToAnswerVideoCall(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 onVideoCallConnected() {
videoCallState.isConnected = true;
videoCallState.startTime = Date.now();
clearInterval(videoCallState.dotsInterval);
clearTimeout(videoCallState.connectTimeout);
// 隐藏中间区域的状态文字,保留头像
const statusEl = document.getElementById('wechat-video-call-status');
if (statusEl) statusEl.classList.add('hidden');
document.getElementById('wechat-video-call-incoming')?.classList.add('hidden');
document.getElementById('wechat-video-call-actions')?.classList.remove('hidden');
// 显示对话框和输入框
document.getElementById('wechat-video-call-chat')?.classList.remove('hidden');
document.getElementById('wechat-video-call-input-area')?.classList.remove('hidden');
// 接通后才显示计时
const timeEl = document.getElementById('wechat-video-call-time');
timeEl?.classList.remove('hidden');
// 开始计时
startVideoCallTimer();
// 如果是AI发起的通话,接通后AI自动发送第一条消息
if (videoCallState.initiator === 'ai') {
triggerAIVideoGreeting();
}
// 启动AI主动挂断检查(通话30秒后开始随机检查)
scheduleVideoAIHangupCheck();
}
// 开始通话计时
function startVideoCallTimer() {
clearInterval(videoCallState.timerInterval);
videoCallState.timerInterval = setInterval(() => {
if (!videoCallState.isConnected || !videoCallState.startTime) return;
const elapsed = Math.floor((Date.now() - videoCallState.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-video-call-time');
if (timeEl) {
timeEl.textContent = `${minutes}:${seconds}`;
}
}, 1000);
}
// 调度AI主动挂断检查
// 通话接通后30秒开始,每次用户发消息后AI回复时有5%概率挂断
// 同时设置一个180秒(3分钟)的保底挂断时间
function scheduleVideoAIHangupCheck() {
// 清除已有的计时器
clearTimeout(videoCallState.aiHangupTimeout);
// 设置保底挂断时间:通话3分钟后有50%概率挂断,超过5分钟必定挂断
const checkTime = 180000 + Math.random() * 120000; // 3-5分钟
videoCallState.aiHangupTimeout = setTimeout(() => {
if (videoCallState.isConnected) {
// 50%概率挂断,否则再等1-2分钟
if (Math.random() < 0.5) {
videoAIHangup();
} else {
// 再设置一个60-120秒后的必定挂断
videoCallState.aiHangupTimeout = setTimeout(() => {
if (videoCallState.isConnected) {
videoAIHangup();
}
}, 60000 + Math.random() * 60000);
}
}
}, checkTime);
}
// 每次AI回复后检查是否要挂断(5%概率,通话30秒后生效)
export function checkVideoAIHangupAfterReply() {
if (!videoCallState.isConnected || !videoCallState.startTime) return false;
// 通话至少30秒后才开始随机挂断检查
const elapsed = Date.now() - videoCallState.startTime;
if (elapsed < 30000) return false;
// 5%概率挂断
if (Math.random() < 0.05) {
// 延迟1-3秒后挂断,更自然
setTimeout(() => {
if (videoCallState.isConnected) {
videoAIHangup();
}
}, 1000 + Math.random() * 2000);
return true;
}
return false;
}
// 检测AI是否有挂断意图
function detectVideoHangupIntent(text) {
if (!text) return false;
const hangupPatterns = [
/我(先)?挂了/,
/那我挂了/,
/先挂(了)?啊?/,
/挂了(啊|哈|呀|哦)?$/,
/我(要)?挂(电话|断)了/,
/拜拜.*挂/,
/挂.*拜拜/,
/再见.*挂/,
/不聊了.*挂/,
/不说了.*挂/,
/那就这样.*挂/,
/就这样吧.*挂/
];
return hangupPatterns.some(pattern => pattern.test(text));
}
// AI主动挂断视频电话
function videoAIHangup() {
if (!videoCallState.isConnected) return;
console.log('[可乐] AI主动挂断视频电话');
videoCallState.hungUpByAI = true;
hangupVideoCall();
}
// 挂断视频通话
export function hangupVideoCall() {
// 清除AI挂断计时器
clearTimeout(videoCallState.aiHangupTimeout);
// 计算通话时长
let durationStr = '00:00';
if (videoCallState.isConnected && videoCallState.startTime) {
const elapsed = Math.floor((Date.now() - videoCallState.startTime) / 1000);
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0');
const seconds = (elapsed % 60).toString().padStart(2, '0');
durationStr = `${minutes}:${seconds}`;
}
// 添加通话记录到聊天历史
if (videoCallState.contact) {
const settings = getSettings();
const contact = videoCallState.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 (videoCallState.isConnected) {
callContent = `[视频通话:${durationStr}]`;
lastMessage = `视频通话 ${durationStr}`;
} else {
if (videoCallState.initiator === 'user') {
if (videoCallState.rejectedByAI) {
// 用户发起,AI拒接
callContent = '[视频通话:对方已拒绝]';
lastMessage = '对方已拒绝';
} else {
// 用户发起,用户取消
callContent = '[视频通话:已取消]';
lastMessage = '已取消';
}
} else if (videoCallState.rejectedByUser) {
callContent = '[视频通话:已拒绝]';
lastMessage = '已拒绝';
} else {
callContent = '[视频通话:对方已取消]';
lastMessage = '对方已取消';
}
}
const callRecord = {
role: videoCallState.initiator === 'user' ? 'user' : 'assistant',
content: callContent,
time: timeStr,
timestamp: Date.now(),
isVideoCallRecord: true
};
contact.chatHistory.push(callRecord);
// 通话内容只进"通话历史",不在主聊天界面展示(避免污染主界面/列表预览)
if (videoCallState.messages && videoCallState.messages.length > 0) {
const callStatusForHistory = videoCallState.isConnected
? 'connected'
: (videoCallState.initiator === 'user'
? (videoCallState.rejectedByAI ? 'rejectedByAI' : 'cancelled')
: (videoCallState.rejectedByUser ? 'rejected' : 'timeout'));
contact.callHistory = Array.isArray(contact.callHistory) ? contact.callHistory : [];
contact.callHistory.push({
type: 'video',
initiator: videoCallState.initiator,
status: callStatusForHistory,
duration: durationStr,
time: timeStr,
timestamp: Date.now(),
messages: videoCallState.messages.map(m => ({ role: m.role, content: m.content }))
});
}
contact.lastMessage = lastMessage;
// 确定状态类型
let callStatus = 'connected';
if (!videoCallState.isConnected) {
if (videoCallState.initiator === 'user') {
callStatus = videoCallState.rejectedByAI ? 'rejectedByAI' : 'cancelled';
} else if (videoCallState.rejectedByUser) {
callStatus = 'rejected';
} else {
callStatus = 'timeout';
}
}
if (currentChatIndex === videoCallState.contactIndex) {
appendVideoCallRecordMessage(videoCallState.initiator === 'user' ? 'user' : 'assistant', callStatus, durationStr, contact);
}
// AI 对通话结束做出反应(所有情况都触发)
triggerVideoCallEndReaction(contact, callStatus, videoCallState.initiator, videoCallState.messages, videoCallState.hungUpByAI);
requestSave();
refreshChatList();
}
videoCallState.isActive = false;
videoCallState.isConnected = false;
videoCallState.startTime = null;
clearInterval(videoCallState.timerInterval);
clearInterval(videoCallState.dotsInterval);
clearTimeout(videoCallState.connectTimeout);
const page = document.getElementById('wechat-video-call-page');
if (page) {
page.classList.add('hidden');
}
}
// 在聊天界面显示视频通话记录消息
function appendVideoCallRecordMessage(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 cameraIconSVG = ``;
let callRecordHTML;
if (status === 'connected') {
callRecordHTML = `