/** * 消息操作菜单 */ import { getSettings, SUMMARY_MARKER_PREFIX, splitAIMessages } from './config.js'; import { saveSettingsDebounced } from '../../../../script.js'; import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js'; import { showToast } from './toast.js'; import { getContext } from '../../../extensions.js'; import { formatQuoteDate } from './utils.js'; import { isInGroupChat, getCurrentGroupIndex, openGroupChat } from './group-chat.js'; // 当前显示菜单的消息索引 let currentMenuMsgIndex = -1; // 长按计时器 let longPressTimer = null; // 是否正在长按 let isLongPress = false; // 待引用的消息 let pendingQuote = null; // 菜单项配置 const menuItems = [ { id: 'copy', icon: 'copy', text: '复制' }, { id: 'quote', icon: 'quote', text: '引用' }, { id: 'recall', icon: 'recall', text: '撤回', userOnly: true }, { id: 'delete', icon: 'delete', text: '删除' }, { id: 'multiselect', icon: 'multiselect', text: '多选' } ]; // 图标SVG const icons = { copy: ` `, quote: ` `, recall: ` `, delete: ` `, multiselect: ` ` }; // 创建菜单DOM function createMenuElement(isUserMessage = false) { const menu = document.createElement('div'); menu.className = 'wechat-msg-menu hidden'; menu.id = 'wechat-msg-menu'; const menuContent = document.createElement('div'); menuContent.className = 'wechat-msg-menu-content'; menuItems.forEach(item => { // 跳过仅用户可用的菜单项(如果当前不是用户消息) if (item.userOnly && !isUserMessage) return; const menuItem = document.createElement('div'); menuItem.className = 'wechat-msg-menu-item'; menuItem.dataset.action = item.id; menuItem.innerHTML = `
${icons[item.id]}
${item.text}
`; menuContent.appendChild(menuItem); }); menu.appendChild(menuContent); return menu; } // 显示菜单 export function showMessageMenu(msgElement, msgIndex, event) { hideMessageMenu(); currentMenuMsgIndex = msgIndex; // 检查是否为用户消息 const settings = getSettings(); const groupIndex = getCurrentGroupIndex(); let msg; if (groupIndex >= 0) { // 群聊模式 const groupChat = settings.groupChats?.[groupIndex]; msg = groupChat?.chatHistory?.[msgIndex]; } else { // 单聊模式 const contact = settings.contacts[currentChatIndex]; msg = contact?.chatHistory?.[msgIndex]; } // 优先从历史记录判断,其次从元素属性判断(处理分割显示的消息) let isUserMessage = msg?.role === 'user'; if (msg === undefined) { // 如果找不到消息记录,尝试从元素属性获取 const roleAttr = msgElement?.dataset?.msgRole || msgElement?.closest?.('[data-msg-role]')?.dataset?.msgRole; isUserMessage = roleAttr === 'user'; } // 移除旧菜单并创建新菜单(根据消息类型动态生成) let menu = document.getElementById('wechat-msg-menu'); if (menu) { menu.remove(); } menu = createMenuElement(isUserMessage); document.querySelector('.wechat-phone').appendChild(menu); bindMenuEvents(menu); // 计算位置 const msgRect = msgElement.getBoundingClientRect(); const phoneEl = document.querySelector('.wechat-phone'); const phoneRect = phoneEl.getBoundingClientRect(); // 相对于手机容器的位置 const relativeTop = msgRect.top - phoneRect.top; const relativeLeft = msgRect.left - phoneRect.left; menu.classList.remove('hidden'); // 获取菜单尺寸 const menuRect = menu.getBoundingClientRect(); // 默认显示在消息上方 let top = relativeTop - menuRect.height - 8; let left = relativeLeft + (msgRect.width / 2) - (menuRect.width / 2); // 如果上方空间不够,显示在下方 if (top < 50) { top = relativeTop + msgRect.height + 8; } // 左右边界检查 if (left < 10) left = 10; if (left + menuRect.width > phoneRect.width - 10) { left = phoneRect.width - menuRect.width - 10; } menu.style.top = `${top}px`; menu.style.left = `${left}px`; // 点击其他地方关闭菜单 setTimeout(() => { document.addEventListener('click', handleOutsideClick); document.addEventListener('touchstart', handleOutsideClick); }, 10); } // 隐藏菜单 export function hideMessageMenu() { const menu = document.getElementById('wechat-msg-menu'); if (menu) { menu.classList.add('hidden'); } currentMenuMsgIndex = -1; document.removeEventListener('click', handleOutsideClick); document.removeEventListener('touchstart', handleOutsideClick); } // 点击外部关闭 function handleOutsideClick(e) { const menu = document.getElementById('wechat-msg-menu'); if (menu && !menu.contains(e.target)) { hideMessageMenu(); } } // 绑定菜单事件 function bindMenuEvents(menu) { menu.addEventListener('click', (e) => { const menuItem = e.target.closest('.wechat-msg-menu-item'); if (!menuItem) return; const action = menuItem.dataset.action; handleMenuAction(action, currentMenuMsgIndex); hideMessageMenu(); }); } // 处理菜单操作 function handleMenuAction(action, msgIndex) { const settings = getSettings(); const groupIndex = getCurrentGroupIndex(); let chatHistory, contact, groupChat; if (groupIndex >= 0) { // 群聊模式 groupChat = settings.groupChats?.[groupIndex]; if (!groupChat || !groupChat.chatHistory || msgIndex < 0) return; chatHistory = groupChat.chatHistory; } else { // 单聊模式 contact = settings.contacts[currentChatIndex]; if (!contact || !contact.chatHistory || msgIndex < 0) return; chatHistory = contact.chatHistory; } const msg = chatHistory[msgIndex]; if (!msg) return; switch (action) { case 'copy': copyMessage(msg.content); break; case 'quote': quoteMessage(msg, groupIndex >= 0, groupChat); break; case 'recall': if (groupIndex >= 0) { recallGroupMessage(msgIndex, groupChat); } else { recallMessage(msgIndex, contact); } break; case 'delete': if (groupIndex >= 0) { deleteGroupMessage(msgIndex, groupChat); } else { deleteMessage(msgIndex, contact); } break; case 'multiselect': showToast('多选功能开发中'); break; } } // 复制消息 function copyMessage(content) { navigator.clipboard.writeText(content).then(() => { showToast('已复制'); }).catch(() => { // 降级方案 const textarea = document.createElement('textarea'); textarea.value = content; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showToast('已复制'); }); } // 引用消息 - 设置待引用状态 function quoteMessage(msg, isGroupChat = false, groupChat = null) { const settings = getSettings(); const context = getContext(); // 确定发送者名称 let senderName; if (msg.role === 'user') { senderName = context?.name1 || '我'; } else if (isGroupChat) { // 群聊模式:使用消息中存储的角色名 senderName = msg.characterName || '群成员'; } else { // 单聊模式:使用联系人名称 const contact = settings.contacts[currentChatIndex]; senderName = contact?.name || '对方'; } // 格式化日期 const date = formatQuoteDate(msg.timestamp); // 设置待引用消息 const isMusic = msg.isMusic === true; let quoteContent = msg.content; if (isMusic && msg.musicInfo) { const artist = (msg.musicInfo.artist || '').toString().trim(); const name = (msg.musicInfo.name || '').toString().trim(); quoteContent = artist && name ? `${artist}-${name}` : (name || artist || msg.content); } pendingQuote = { content: quoteContent, sender: senderName, date: date, isVoice: msg.isVoice === true, isPhoto: msg.isPhoto === true, isSticker: msg.isSticker === true, isMusic: isMusic }; // 显示引用预览条 showQuotePreview(); // 聚焦输入框 const input = document.getElementById('wechat-input'); if (input) { input.focus(); } } // 显示引用预览条 function showQuotePreview() { if (!pendingQuote) return; // 移除已有的预览条 hideQuotePreview(); const inputArea = document.querySelector('.wechat-chat-input'); if (!inputArea) return; const previewBar = document.createElement('div'); previewBar.className = 'wechat-quote-preview'; previewBar.id = 'wechat-quote-preview'; // 根据消息类型生成显示文本 let contentText; if (pendingQuote.isVoice) { const seconds = Math.max(2, Math.min(60, Math.ceil(pendingQuote.content.length / 3))); contentText = `[语音] ${seconds}"`; } else if (pendingQuote.isPhoto) { contentText = '[照片]'; } else if (pendingQuote.isSticker) { contentText = '[表情]'; } else { contentText = pendingQuote.content.length > 25 ? pendingQuote.content.substring(0, 25) + '...' : pendingQuote.content; } previewBar.innerHTML = `
${pendingQuote.sender}: ${contentText}
`; // 插入到输入框下方 inputArea.parentNode.insertBefore(previewBar, inputArea.nextSibling); // 绑定关闭按钮事件 document.getElementById('wechat-quote-close').addEventListener('click', clearQuote); } // 隐藏引用预览条 function hideQuotePreview() { const preview = document.getElementById('wechat-quote-preview'); if (preview) { preview.remove(); } } // 获取待引用消息 export function getPendingQuote() { return pendingQuote; } // 清除引用 export function clearQuote() { pendingQuote = null; hideQuotePreview(); } // 设置引用(供外部调用) export function setQuote(quote) { if (!quote || !quote.content) return; pendingQuote = { content: quote.content, sender: quote.sender || '用户', date: quote.date || '', isVoice: quote.isVoice === true, isPhoto: quote.isPhoto === true, isSticker: quote.isSticker === true, isMusic: quote.isMusic === true }; showQuotePreview(); // 聚焦输入框 const input = document.getElementById('wechat-input'); if (input) { input.focus(); } } // 删除消息 function deleteMessage(msgIndex, contact) { contact.chatHistory.splice(msgIndex, 1); saveSettingsDebounced(); // 刷新聊天界面 openChat(currentChatIndex); showToast('已删除'); } // 撤回消息 async function recallMessage(msgIndex, contact) { const msg = contact.chatHistory[msgIndex]; if (!msg) return; // 只能撤回自己的消息 if (msg.role !== 'user') { showToast('只能撤回自己的消息'); return; } // 标记为撤回 msg.isRecalled = true; msg.originalContent = msg.content; msg.content = ''; saveSettingsDebounced(); // 刷新聊天界面 openChat(currentChatIndex); showToast('已撤回'); // 触发AI回复 try { showTypingIndicator(contact); const { callAI } = await import('./ai.js'); const aiResponse = await callAI(contact, '[用户撤回了一条消息]'); hideTypingIndicator(); 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')}`; // 解析AI回复(可能有多条消息) const aiMessages = splitAIMessages(aiResponse); for (const aiMsg of aiMessages) { let finalMsg = aiMsg; let isVoice = false; const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/); if (voiceMatch) { finalMsg = voiceMatch[1]; isVoice = true; } contact.chatHistory.push({ role: 'assistant', content: finalMsg, time: timeStr, timestamp: Date.now(), isVoice: isVoice }); appendMessage('assistant', finalMsg, contact, isVoice); } contact.lastMessage = aiMessages[aiMessages.length - 1]; saveSettingsDebounced(); } catch (err) { hideTypingIndicator(); console.error('[可乐] 撤回后AI回复失败:', err); } } // 删除群聊消息 function deleteGroupMessage(msgIndex, groupChat) { const groupIndex = getCurrentGroupIndex(); if (groupIndex < 0) return; groupChat.chatHistory.splice(msgIndex, 1); saveSettingsDebounced(); // 刷新群聊界面 openGroupChat(groupIndex); showToast('已删除'); } // 撤回群聊消息 async function recallGroupMessage(msgIndex, groupChat) { const groupIndex = getCurrentGroupIndex(); if (groupIndex < 0) return; const msg = groupChat.chatHistory[msgIndex]; if (!msg) return; // 只能撤回自己的消息 if (msg.role !== 'user') { showToast('只能撤回自己的消息'); return; } // 标记为撤回 msg.isRecalled = true; msg.originalContent = msg.content; msg.content = ''; saveSettingsDebounced(); // 刷新群聊界面 openGroupChat(groupIndex); showToast('已撤回'); } // 绑定消息气泡事件 export function bindMessageBubbleEvents(container) { const bubbles = container.querySelectorAll('.wechat-message-bubble, .wechat-voice-bubble'); bubbles.forEach((bubble, index) => { if (bubble.dataset.menuBound) return; bubble.dataset.menuBound = 'true'; // 获取真实的消息索引 const msgElement = bubble.closest('.wechat-message'); if (!msgElement) return; // 计算消息索引(跳过时间标签) const allMessages = Array.from(container.querySelectorAll('.wechat-message')); const msgIndex = allMessages.indexOf(msgElement); // PC端:单击 bubble.addEventListener('click', (e) => { if (isLongPress) { isLongPress = false; return; } // 语音气泡点击展开文本,不显示菜单 if (bubble.classList.contains('wechat-voice-bubble')) return; e.stopPropagation(); showMessageMenu(bubble, getRealMsgIndex(container, msgElement), e); }); // 移动端:长按 bubble.addEventListener('touchstart', (e) => { isLongPress = false; longPressTimer = setTimeout(() => { isLongPress = true; e.preventDefault(); showMessageMenu(bubble, getRealMsgIndex(container, msgElement), e); }, 500); }); bubble.addEventListener('touchend', () => { clearTimeout(longPressTimer); }); bubble.addEventListener('touchmove', () => { clearTimeout(longPressTimer); }); }); } // 获取真实的消息索引(排除时间标签等) function getRealMsgIndex(container, msgElement) { const settings = getSettings(); const contact = settings.contacts[currentChatIndex]; if (!contact || !contact.chatHistory) return -1; // 获取所有消息元素(不含时间标签) const allMsgElements = Array.from(container.querySelectorAll('.wechat-message:not(.wechat-typing-wrapper)')); const visualIndex = allMsgElements.indexOf(msgElement); if (visualIndex < 0) return -1; // 需要计算真实索引(chatHistory中可能包含marker消息和撤回消息) // 注意:包含 ||| 的消息在渲染时会被拆分成多条可视消息,需要正确计算 let realIndex = -1; let visualCount = 0; for (let i = 0; i < contact.chatHistory.length; i++) { const msg = contact.chatHistory[i]; // 跳过marker消息和撤回消息 if (msg.isMarker || msg.content?.startsWith(SUMMARY_MARKER_PREFIX) || msg.isRecalled) continue; // 计算这条消息渲染成几个可视消息 let visualMsgCount = 1; const content = msg.content || ''; const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic; if (!isSpecial && content.indexOf('|||') >= 0) { // 按 ||| 分割后有多少个非空部分 const parts = content.split('|||').map(p => p.trim()).filter(p => p); visualMsgCount = parts.length || 1; } // 检查 visualIndex 是否落在这条消息的范围内 if (visualIndex >= visualCount && visualIndex < visualCount + visualMsgCount) { realIndex = i; break; } visualCount += visualMsgCount; } return realIndex; }