/** * 多人群聊模块 * 特点:无头像,名字+气泡,左对齐,世界观注入 */ import { requestSave, saveNow } from './save-manager.js'; import { getSettings } from './config.js'; import { showToast } from './toast.js'; import { escapeHtml, sleep, formatMessageTime } from './utils.js'; import { refreshChatList } from './ui.js'; // 当前多人群聊索引 export let currentMultiPersonChatIndex = -1; // 设置当前多人群聊索引 export function setCurrentMultiPersonChatIndex(index) { currentMultiPersonChatIndex = index; } // 打开多人群聊 export function openMultiPersonChat(chatIndex) { console.log('[可乐] openMultiPersonChat 被调用, chatIndex:', chatIndex); const settings = getSettings(); const chat = settings.multiPersonChats?.[chatIndex]; if (!chat) return; currentMultiPersonChatIndex = chatIndex; // 确保 chatHistory 存在 if (!chat.chatHistory) chat.chatHistory = []; // 隐藏主页,显示聊天页 document.getElementById('wechat-main-content')?.classList.add('hidden'); document.getElementById('wechat-chat-page')?.classList.remove('hidden'); document.getElementById('wechat-chat-title').textContent = `${chat.name}(${chat.members.length})`; const messagesContainer = document.getElementById('wechat-chat-messages'); const chatHistory = chat.chatHistory; if (chatHistory.length === 0) { messagesContainer.innerHTML = ''; } else { messagesContainer.innerHTML = renderMultiPersonChatHistory(chat, chatHistory); } messagesContainer.scrollTop = messagesContainer.scrollHeight; // 标记当前是多人群聊模式 messagesContainer.dataset.isMultiPerson = 'true'; messagesContainer.dataset.multiPersonIndex = chatIndex; messagesContainer.dataset.isGroup = 'false'; // 区别于普通群聊 } // 渲染多人群聊历史记录 function renderMultiPersonChatHistory(chat, chatHistory) { let html = ''; let lastTimestamp = 0; const TIME_GAP_THRESHOLD = 5 * 60 * 1000; chatHistory.forEach((msg, index) => { const msgTimestamp = msg.timestamp || 0; // 时间戳显示 if (index === 0 || (msgTimestamp - lastTimestamp > TIME_GAP_THRESHOLD)) { const timeLabel = formatMessageTime(msgTimestamp); if (timeLabel) { html += `
${timeLabel}
`; } } lastTimestamp = msgTimestamp; if (msg.role === 'user') { // 用户消息:右对齐,有气泡 html += `
${escapeHtml(msg.content)}
`; } else { // 角色消息:无头像,名字+气泡,左对齐 const charName = msg.characterName || '未知'; html += `
${escapeHtml(charName)}
${escapeHtml(msg.content)}
`; } }); return html; } // 追加多人群聊消息到界面 export function appendMultiPersonMessage(role, content, characterName = null) { const messagesContainer = document.getElementById('wechat-chat-messages'); if (!messagesContainer) return; const messageDiv = document.createElement('div'); if (role === 'user') { messageDiv.className = 'wechat-message self'; messageDiv.innerHTML = `
${escapeHtml(content)}
`; } else { messageDiv.className = 'wechat-message wechat-mp-message'; messageDiv.innerHTML = `
${escapeHtml(characterName || '未知')}
${escapeHtml(content)}
`; } messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 显示多人群聊打字指示器 export function showMultiPersonTypingIndicator(characterName) { const messagesContainer = document.getElementById('wechat-chat-messages'); if (!messagesContainer) return; hideMultiPersonTypingIndicator(); const typingDiv = document.createElement('div'); typingDiv.className = 'wechat-message wechat-mp-message wechat-typing-wrapper'; typingDiv.id = 'wechat-mp-typing-indicator'; typingDiv.innerHTML = `
${escapeHtml(characterName || '...')}
`; messagesContainer.appendChild(typingDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 隐藏多人群聊打字指示器 export function hideMultiPersonTypingIndicator() { const indicator = document.getElementById('wechat-mp-typing-indicator'); if (indicator) indicator.remove(); } // 构建多人群聊系统提示词 function buildMultiPersonSystemPrompt(chat, respondingMembers) { const settings = getSettings(); let systemPrompt = ''; // 世界观(必读) if (chat.worldView) { systemPrompt += `【世界观设定】\n${chat.worldView}\n\n`; } // 参与角色信息 systemPrompt += `【参与角色】\n`; systemPrompt += `这是一个包含 ${chat.members.length} 位角色的多人对话场景。\n\n`; chat.members.forEach((member, idx) => { systemPrompt += `角色 ${idx + 1}: ${member.name}\n`; if (member.gender) systemPrompt += ` 性别: ${member.gender}\n`; if (member.age) systemPrompt += ` 年龄: ${member.age}\n`; if (member.description) systemPrompt += ` 描述: ${member.description}\n`; systemPrompt += '\n'; }); // 本轮回复的角色 if (respondingMembers && respondingMembers.length > 0) { systemPrompt += `【本轮发言角色】\n`; systemPrompt += `本轮需要以下角色发言:${respondingMembers.map(m => m.name).join('、')}\n\n`; } // 回复格式说明 systemPrompt += `【回复格式】 你需要模拟多位角色的对话。请按以下格式回复: [角色名]: 对话内容 如果有多个角色发言,请用 ||| 分隔每条消息。 示例: [${chat.members[0]?.name || '角色A'}]: 你好啊 ||| [${chat.members[1]?.name || '角色B'}]: 嗨,好久不见 规则: 1. 每个角色保持自己的性格特点 2. 对话要自然流畅,像真实聊天 3. 每条消息简短自然(1-3句话) 4. 可以使用表情符号 5. 角色之间可以互相回应、互动 `; return systemPrompt; } // 选择本轮回复的角色(3-5人) function selectRespondingMembers(chat, userMessage) { const members = chat.members || []; const totalMembers = members.length; // 根据群成员数量决定每轮回复人数 let respondCount; if (totalMembers <= 5) { // 5人及以下,全部回复 respondCount = totalMembers; } else if (totalMembers <= 10) { // 6-10人,每轮3-5人 respondCount = Math.min(5, Math.max(3, Math.floor(totalMembers * 0.5))); } else { // 10人以上,每轮5人 respondCount = 5; } // 随机打乱成员顺序 const shuffled = [...members].sort(() => Math.random() - 0.5); // 取前 respondCount 个 return shuffled.slice(0, respondCount); } // 调用多人群聊 AI async function callMultiPersonAI(chat, userMessage, respondingMembers) { const settings = getSettings(); // 使用全局 API 配置 const apiUrl = settings.apiUrl; const apiKey = settings.apiKey; const apiModel = settings.selectedModel; if (!apiUrl || !apiModel) { throw new Error('请先配置 AI 接口'); } const systemPrompt = buildMultiPersonSystemPrompt(chat, respondingMembers); const messages = [{ role: 'system', content: systemPrompt }]; // 添加历史消息 const chatHistory = chat.chatHistory || []; const recentHistory = chatHistory.slice(-50); recentHistory.forEach(msg => { if (msg.role === 'user') { messages.push({ role: 'user', content: msg.content }); } else { const formattedContent = msg.characterName ? `[${msg.characterName}]: ${msg.content}` : msg.content; messages.push({ role: 'assistant', content: formattedContent }); } }); messages.push({ role: 'user', content: userMessage }); const chatUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions'; const headers = { 'Content-Type': 'application/json' }; if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } const response = await fetch(chatUrl, { method: 'POST', headers, body: JSON.stringify({ model: apiModel, messages, temperature: 1, max_tokens: 4096 }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API 错误 (${response.status}): ${errorText.substring(0, 100)}`); } const data = await response.json(); const rawResponse = data.choices?.[0]?.message?.content || ''; return parseMultiPersonResponse(rawResponse, chat.members); } // 解析多人群聊 AI 回复 function parseMultiPersonResponse(response, members) { const results = []; // 按 ||| 分隔多条消息 const parts = response.split('|||').map(p => p.trim()).filter(p => p); parts.forEach(part => { // 匹配 [角色名]: 内容 格式 const match = part.match(/^\[(.+?)\][::]\s*(.+)$/s); if (match) { const charName = match[1].trim(); const content = match[2].trim(); // 查找对应的成员 const member = members.find(m => m.name === charName); results.push({ characterName: member?.name || charName, content: content }); } else { // 无法解析格式时,作为第一个角色的消息 if (members.length > 0 && part.trim()) { results.push({ characterName: members[0].name, content: part.trim() }); } } }); return results; } // 发送多人群聊消息 export async function sendMultiPersonMessage(messageText) { console.log('[可乐] sendMultiPersonMessage 被调用', { messageText, currentMultiPersonChatIndex }); if (currentMultiPersonChatIndex < 0) return; const settings = getSettings(); const chat = settings.multiPersonChats?.[currentMultiPersonChatIndex]; if (!chat) return; 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')}`; const msgTimestamp = Date.now(); // 清空输入框 const input = document.getElementById('wechat-input'); if (input) input.value = ''; window.updateSendButtonState?.(); // 显示用户消息 appendMultiPersonMessage('user', messageText); // 确保 chatHistory 存在 if (!chat.chatHistory) chat.chatHistory = []; // 添加到历史 chat.chatHistory.push({ role: 'user', content: messageText, time: timeStr, timestamp: msgTimestamp }); // 立即保存 saveNow(); // 选择本轮回复的角色 const respondingMembers = selectRespondingMembers(chat, messageText); // 显示第一个角色的打字指示器 showMultiPersonTypingIndicator(respondingMembers[0]?.name); try { // 调用 AI const responses = await callMultiPersonAI(chat, messageText, respondingMembers); hideMultiPersonTypingIndicator(); // 逐条显示 AI 回复,带 typing 效果 for (let i = 0; i < responses.length; i++) { const resp = responses[i]; // 显示 typing 指示器 showMultiPersonTypingIndicator(resp.characterName); await sleep(600 + Math.random() * 400); // 0.6-1秒 hideMultiPersonTypingIndicator(); // 添加到历史 chat.chatHistory.push({ role: 'assistant', content: resp.content, characterName: resp.characterName, time: timeStr, timestamp: Date.now() }); // 显示消息 appendMultiPersonMessage('assistant', resp.content, resp.characterName); } // 更新最后消息 if (responses.length > 0) { const lastResp = responses[responses.length - 1]; chat.lastMessage = `[${lastResp.characterName}]: ${lastResp.content}`; } chat.lastMessageTime = Date.now(); requestSave(); refreshChatList(); } catch (err) { hideMultiPersonTypingIndicator(); console.error('[可乐] 多人群聊 AI 调用失败:', err); appendMultiPersonMessage('assistant', `⚠️ ${err.message}`, '系统'); requestSave(); } } // 判断当前是否在多人群聊 export function isInMultiPersonChat() { const messagesContainer = document.getElementById('wechat-chat-messages'); return messagesContainer?.dataset.isMultiPerson === 'true'; } // 获取当前多人群聊索引 export function getCurrentMultiPersonIndex() { const messagesContainer = document.getElementById('wechat-chat-messages'); if (messagesContainer?.dataset.isMultiPerson === 'true') { const index = parseInt(messagesContainer.dataset.multiPersonIndex); return isNaN(index) ? -1 : index; } return -1; }