/** * (Legacy)可乐不加冰 v1.0.0 - SillyTavern 插件 * 模拟微信界面,支持导入角色卡 */ import { saveSettingsDebounced, getRequestHeaders } from '../../../../script.js'; import { getContext, extension_settings, renderExtensionTemplateAsync } from '../../../extensions.js'; import { world_names, loadWorldInfo, saveWorldInfo, createNewWorldInfo } from '../../../world-info.js'; // 插件名称 const extensionName = 'wechat-simulator'; // 默认设置 const defaultSettings = { darkMode: true, // 默认开启深色模式 autoInjectPrompt: true, contacts: [], // 存储导入的角色卡 phoneVisible: false, userAvatar: '', // 用户自定义头像 // API 配置 apiUrl: '', apiKey: '', selectedModel: '', // 选中的模型 modelList: [], // 缓存的模型列表 // 总结功能 API 配置 summaryApiUrl: '', summaryApiKey: '', summarySelectedModel: '', summaryModelList: [], // 上下文设置 contextEnabled: false, // 上下文开关(需要主界面有聊天时才启用) contextLevel: 5, // 0-5层,参考酒馆主聊天 contextTags: [], // 自定义提取标签,如 ['content', 'scene', 'action'] walletAmount: '5773.89', // 钱包金额 }; // 作者注释模板 const authorNoteTemplate = `[微信消息格式指南] 当角色想要通过手机微信发送消息时,请使用以下格式: - 普通消息:[微信: 消息内容] - 语音消息:[语音: 秒数] 例如 [语音: 5秒] - 图片消息:[图片: 图片描述] - 朋友圈:[朋友圈: 内容 | 图片描述] - 表情:[表情: 表情描述] - 撤回消息:[撤回] - 红包:[红包: 祝福语] - 转账:[转账: 金额] 示例: [微信: 你在干嘛呢?] [语音: 10秒] [微信: 刚录了条语音给你听~]`; // 初始化设置 function loadSettings() { extension_settings[extensionName] = extension_settings[extensionName] || {}; if (Object.keys(extension_settings[extensionName]).length === 0) { Object.assign(extension_settings[extensionName], defaultSettings); } } // 获取当前时间字符串 function getCurrentTime() { const now = new Date(); return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; } // 获取用户头像HTML function getUserAvatarHTML() { const settings = extension_settings[extensionName]; const context = getContext(); const userName = context?.name1 || 'User'; const firstChar = userName.charAt(0); // 优先使用自定义头像 if (settings.userAvatar) { return ``; } // 其次尝试从 SillyTavern 获取 const userAvatar = context?.user_avatar; if (userAvatar) { // 尝试多种路径格式 const avatarPaths = [ `/User Avatars/${userAvatar}`, `/characters/${userAvatar}`, userAvatar ]; // 使用第一个路径,onerror 时会显示首字母 return ``; } // 尝试从 getUserPersonaFromST 获取 const stPersona = getUserPersonaFromST(); if (stPersona?.avatar) { return ``; } // 默认显示首字母 return firstChar; } // 生成手机界面 HTML function generatePhoneHTML() { const settings = extension_settings[extensionName]; const darkClass = settings.darkMode ? 'wechat-dark' : ''; const hiddenClass = settings.phoneVisible ? '' : 'hidden'; return `
${getCurrentTime()}
微信
${generateChatList()}
`; } // 生成聊天列表 HTML(微信主页列表样式) function generateChatList() { const settings = extension_settings[extensionName]; const contacts = settings.contacts || []; if (contacts.length === 0) { return `
💬
暂无聊天记录
添加好友开始聊天吧
`; } // 获取有聊天记录的联系人,按最后消息时间排序 const contactsWithChat = contacts.map((contact, index) => { const chatHistory = contact.chatHistory || []; const lastMsg = chatHistory.length > 0 ? chatHistory[chatHistory.length - 1] : null; // 使用 timestamp 或从 time 字符串解析时间 const lastMsgTime = lastMsg ? (lastMsg.timestamp || new Date(lastMsg.time).getTime() || 0) : 0; // 确保有 ID,没有则使用索引 const contactId = contact.id || `idx_${index}`; return { ...contact, id: contactId, originalIndex: index, lastMsg, lastMsgTime }; }).filter(c => c.lastMsg).sort((a, b) => b.lastMsgTime - a.lastMsgTime); if (contactsWithChat.length === 0) { return `
💬
暂无聊天记录
点击通讯录选择好友开始聊天
`; } return contactsWithChat.map(contact => { const lastMsg = contact.lastMsg; let preview = ''; if (lastMsg.type === 'voice' || lastMsg.isVoice) { preview = '[语音消息]'; } else if (lastMsg.type === 'image') { preview = '[图片]'; } else { preview = lastMsg.content || ''; if (preview.length > 20) preview = preview.substring(0, 20) + '...'; } // 格式化时间 const msgTime = contact.lastMsgTime ? formatChatTime(contact.lastMsgTime) : ''; const avatarContent = contact.avatar ? `${contact.name}` : `${contact.name?.charAt(0) || '?'}`; return `
${avatarContent}
${contact.name || '未知'}
${preview}
${msgTime}
`; }).join(''); } // 格式化聊天时间 function formatChatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; const oneDay = 24 * 60 * 60 * 1000; if (diff < oneDay && date.getDate() === now.getDate()) { // 今天,显示时:分 return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }); } else if (diff < 2 * oneDay && date.getDate() === now.getDate() - 1) { return '昨天'; } else if (diff < 7 * oneDay) { const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; return days[date.getDay()]; } else { return `${date.getMonth() + 1}/${date.getDate()}`; } } // 刷新聊天列表 function refreshChatList() { const chatListEl = document.getElementById('wechat-chat-list'); if (chatListEl) { chatListEl.innerHTML = generateChatList(); } } // 通过联系人ID打开聊天 function openChatByContactId(contactId, index) { const settings = extension_settings[extensionName]; const contacts = settings.contacts || []; // 先尝试通过 ID 查找 let contactIndex = contacts.findIndex(c => c.id === contactId); // 如果找不到,尝试使用索引(兼容 idx_N 格式) if (contactIndex === -1 && contactId.startsWith('idx_')) { contactIndex = parseInt(contactId.replace('idx_', '')); } // 如果还是找不到,使用传入的 index if (contactIndex === -1 && typeof index === 'number') { contactIndex = index; } if (contactIndex >= 0 && contactIndex < contacts.length) { openChat(contactIndex); } } // 生成联系人列表 HTML(图片网格样式) function generateContactsList() { const settings = extension_settings[extensionName]; const contacts = settings.contacts || []; if (contacts.length === 0) { return `
💬
暂无聊天
点击右上角 + 导入角色卡
`; } return `
` + contacts.map((contact, index) => { const firstChar = contact.name ? contact.name.charAt(0) : '?'; const avatarContent = contact.avatar ? `` : ''; return `
${avatarContent}
${firstChar}
${contact.name}
删除
`}).join('') + `
`; } // 从 PNG 提取角色卡数据 (V2 格式) async function extractCharacterFromPNG(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async function(e) { try { const arrayBuffer = e.target.result; const dataView = new DataView(arrayBuffer); // 检查 PNG 签名 const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10]; for (let i = 0; i < 8; i++) { if (dataView.getUint8(i) !== pngSignature[i]) { throw new Error('不是有效的 PNG 文件'); } } // 遍历 PNG chunks 寻找 tEXt 或 iTXt chunk let offset = 8; while (offset < arrayBuffer.byteLength) { const length = dataView.getUint32(offset); const type = String.fromCharCode( dataView.getUint8(offset + 4), dataView.getUint8(offset + 5), dataView.getUint8(offset + 6), dataView.getUint8(offset + 7) ); if (type === 'tEXt' || type === 'iTXt') { const chunkData = new Uint8Array(arrayBuffer, offset + 8, length); const text = new TextDecoder('utf-8').decode(chunkData); // 检查是否是角色卡数据 if (text.startsWith('chara\0')) { const base64Data = text.substring(6); // 正确处理 UTF-8 编码的 Base64 解码 const binaryStr = atob(base64Data); const bytes = new Uint8Array(binaryStr.length); for (let i = 0; i < binaryStr.length; i++) { bytes[i] = binaryStr.charCodeAt(i); } const jsonStr = new TextDecoder('utf-8').decode(bytes); const charData = JSON.parse(jsonStr); // 获取图片作为头像 (转为base64以便持久化存储) const uint8Array = new Uint8Array(arrayBuffer); let binary = ''; for (let i = 0; i < uint8Array.length; i++) { binary += String.fromCharCode(uint8Array[i]); } const avatarBase64 = 'data:image/png;base64,' + btoa(binary); resolve({ name: charData.name || charData.data?.name || '未知角色', description: charData.description || charData.data?.description || '', avatar: avatarBase64, rawData: charData }); return; } } offset += 12 + length; // 4 (length) + 4 (type) + length + 4 (CRC) } throw new Error('PNG 文件中未找到角色卡数据'); } catch (err) { reject(err); } }; reader.onerror = () => reject(new Error('文件读取失败')); reader.readAsArrayBuffer(file); }); } // 从 JSON 导入角色卡 async function extractCharacterFromJSON(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { try { const charData = JSON.parse(e.target.result); resolve({ name: charData.name || charData.data?.name || '未知角色', description: charData.description || charData.data?.description || charData.personality || '', avatar: charData.avatar || null, rawData: charData }); } catch (err) { reject(new Error('JSON 解析失败')); } }; reader.onerror = () => reject(new Error('文件读取失败')); reader.readAsText(file); }); } // 导入角色卡到 SillyTavern async function importCharacterToST(characterData) { try { const context = getContext(); // 创建一个格式化的角色卡对象 const formData = new FormData(); // 如果有原始文件数据,使用它 if (characterData.file) { formData.append('avatar', characterData.file); } // 调用 SillyTavern 的角色导入 API const response = await fetch('/api/characters/import', { method: 'POST', headers: getRequestHeaders(), body: formData }); if (!response.ok) { throw new Error('导入失败'); } return await response.json(); } catch (err) { console.error('导入角色卡失败:', err); throw err; } } // 添加联系人 function addContact(characterData) { const settings = extension_settings[extensionName]; const now = new Date(); const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; // 检查是否已存在 const exists = settings.contacts.some(c => c.name === characterData.name); if (exists) { showToast('该角色已在联系人列表中', '⚠️'); return false; } settings.contacts.push({ id: 'contact_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9), name: characterData.name, description: characterData.description?.substring(0, 50) + '...' || '', avatar: characterData.avatar, importTime: timeStr, rawData: characterData.rawData }); saveSettingsDebounced(); refreshContactsList(); return true; } // 刷新联系人列表 function refreshContactsList() { const contactsContainer = document.getElementById('wechat-contacts'); if (contactsContainer) { contactsContainer.innerHTML = generateContactsList(); bindContactsEvents(); } } // 绑定联系人点击事件 function bindContactsEvents() { // 单击卡片进入聊天(点击头像除外) document.querySelectorAll('.wechat-card-content').forEach(card => { card.addEventListener('click', function(e) { // 如果点击的是头像,不进入聊天(用于换头像) if (e.target.closest('.wechat-card-avatar')) return; const cardEl = this.closest('.wechat-contact-card'); const index = parseInt(cardEl.dataset.index); openChat(index); }); }); // 单击头像更换角色头像 document.querySelectorAll('.wechat-card-avatar').forEach(avatar => { avatar.addEventListener('click', function(e) { e.stopPropagation(); const index = parseInt(this.dataset.index); changeContactAvatar(index); }); }); // 删除按钮点击 document.querySelectorAll('.wechat-card-delete').forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const index = parseInt(this.dataset.index); deleteContact(index); }); }); // 初始化滑动删除功能(支持触摸和鼠标) initSwipeToDelete(); } // 删除联系人 function deleteContact(index) { const settings = extension_settings[extensionName]; const contact = settings.contacts[index]; if (!contact) return; if (confirm(`确定要删除 ${contact.name} 吗?`)) { settings.contacts.splice(index, 1); saveSettingsDebounced(); refreshContactsList(); } } // 初始化滑动删除功能 function initSwipeToDelete() { const cards = document.querySelectorAll('.wechat-contact-card'); cards.forEach(card => { const wrapper = card.querySelector('.wechat-card-swipe-wrapper'); if (!wrapper || wrapper.dataset.swipeInit) return; wrapper.dataset.swipeInit = 'true'; let startX = 0; let currentX = 0; let isDragging = false; let isOpen = false; const deleteWidth = 70; // 删除按钮宽度 // 触摸开始 / 鼠标按下 const handleStart = (e) => { // 如果点击的是头像,不触发滑动 if (e.target.closest('.wechat-card-avatar')) return; isDragging = true; startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; wrapper.style.transition = 'none'; }; // 触摸移动 / 鼠标移动 const handleMove = (e) => { if (!isDragging) return; const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; const diff = clientX - startX; // 计算新位置 let newX; if (isOpen) { newX = -deleteWidth + diff; } else { newX = diff; } // 限制滑动范围 newX = Math.max(-deleteWidth, Math.min(0, newX)); currentX = newX; wrapper.style.transform = `translateX(${newX}px)`; }; // 触摸结束 / 鼠标松开 const handleEnd = () => { if (!isDragging) return; isDragging = false; wrapper.style.transition = 'transform 0.3s ease'; // 判断是否打开或关闭 if (currentX < -deleteWidth / 2) { // 打开删除按钮 wrapper.style.transform = `translateX(-${deleteWidth}px)`; isOpen = true; } else { // 关闭删除按钮 wrapper.style.transform = 'translateX(0)'; isOpen = false; } }; // 关闭其他卡片的删除按钮 const closeOthers = () => { cards.forEach(otherCard => { if (otherCard !== card) { const otherWrapper = otherCard.querySelector('.wechat-card-swipe-wrapper'); if (otherWrapper) { otherWrapper.style.transition = 'transform 0.3s ease'; otherWrapper.style.transform = 'translateX(0)'; } } }); }; // 触摸事件 wrapper.addEventListener('touchstart', (e) => { closeOthers(); handleStart(e); }, { passive: true }); wrapper.addEventListener('touchmove', handleMove, { passive: true }); wrapper.addEventListener('touchend', handleEnd); // 鼠标事件(电脑端支持) const onMouseMove = (e) => handleMove(e); const onMouseUp = () => { handleEnd(); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; wrapper.addEventListener('mousedown', (e) => { // 如果点击的是头像,不触发滑动 if (e.target.closest('.wechat-card-avatar')) return; closeOthers(); handleStart(e); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); e.preventDefault(); }); }); } // 更换角色头像 let pendingAvatarContactIndex = -1; function changeContactAvatar(contactIndex) { pendingAvatarContactIndex = contactIndex; // 使用动态创建的 input let input = document.getElementById('wechat-contact-avatar-input'); if (!input) { input = document.createElement('input'); input.type = 'file'; input.id = 'wechat-contact-avatar-input'; input.accept = 'image/*'; input.style.display = 'none'; document.body.appendChild(input); input.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file || pendingAvatarContactIndex < 0) return; try { const reader = new FileReader(); reader.onload = function(event) { const settings = extension_settings[extensionName]; if (settings.contacts[pendingAvatarContactIndex]) { settings.contacts[pendingAvatarContactIndex].avatar = event.target.result; saveSettingsDebounced(); refreshContactsList(); showToast('角色头像已更换'); } }; reader.readAsDataURL(file); } catch (err) { console.error('更换角色头像失败:', err); showToast('更换头像失败: ' + err.message, '❌'); } e.target.value = ''; pendingAvatarContactIndex = -1; }); } input.click(); } // 当前聊天的联系人索引 let currentChatIndex = -1; // 打开聊天界面 function openChat(contactIndex) { const settings = extension_settings[extensionName]; const contact = settings.contacts[contactIndex]; if (!contact) return; currentChatIndex = contactIndex; // 隐藏主页面,显示聊天页面 document.getElementById('wechat-main-content').classList.add('hidden'); document.getElementById('wechat-chat-page').classList.remove('hidden'); // 设置标题 document.getElementById('wechat-chat-title').textContent = contact.name; // 显示聊天历史或空白 const messagesContainer = document.getElementById('wechat-chat-messages'); const chatHistory = contact.chatHistory || []; if (chatHistory.length === 0) { // 空白聊天界面 messagesContainer.innerHTML = ''; } else { // 渲染聊天历史 messagesContainer.innerHTML = renderChatHistory(contact, chatHistory); // 绑定历史语音消息的点击事件 bindVoiceBubbleEvents(messagesContainer); } // 滚动到底部 messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 绑定语音消息点击事件 function bindVoiceBubbleEvents(container) { const voiceBubbles = container.querySelectorAll('.wechat-voice-bubble:not([data-bound])'); voiceBubbles.forEach(bubble => { bubble.setAttribute('data-bound', 'true'); bubble.addEventListener('click', () => { const voiceId = bubble.dataset.voiceId; const textEl = document.getElementById(voiceId); if (textEl) { textEl.classList.toggle('hidden'); bubble.classList.toggle('expanded'); } }); }); } // 切换页面显示 function showPage(pageId) { ['wechat-main-content', 'wechat-add-page', 'wechat-chat-page', 'wechat-settings-page', 'wechat-me-page', 'wechat-favorites-page', 'wechat-service-page'].forEach(id => { const el = document.getElementById(id); if (el) { el.classList.toggle('hidden', id !== pageId); } }); // 如果进入"我"页面,更新用户信息 if (pageId === 'wechat-me-page') { updateMePageInfo(); } // 如果进入收藏页面,刷新列表 if (pageId === 'wechat-favorites-page') { refreshFavoritesList(); } // 如果进入服务页面,更新钱包金额显示 if (pageId === 'wechat-service-page') { const settings = extension_settings[extensionName]; const amountEl = document.getElementById('wechat-wallet-amount'); if (amountEl) { const amount = settings.walletAmount || '5773.89'; amountEl.textContent = amount.startsWith('¥') ? amount : `¥${amount}`; } } } // 更新"我"页面用户信息 function updateMePageInfo() { try { const context = getContext(); if (context) { const userName = context.name1 || 'User'; const nameEl = document.getElementById('wechat-me-name'); const avatarEl = document.getElementById('wechat-me-avatar'); if (nameEl) nameEl.textContent = userName; if (avatarEl) { avatarEl.innerHTML = getUserAvatarHTML(); } } } catch (err) { console.error('更新用户信息失败:', err); } } // 刷新收藏/世界书列表 function refreshFavoritesList(filter = 'all') { const settings = extension_settings[extensionName]; const listEl = document.getElementById('wechat-favorites-list'); if (!listEl) return; // 关闭所有展开的面板 closeUserPersonaPanel(); closeEntryPanel(); const items = []; // 收集用户设定 - 支持多条目 if (filter === 'all' || filter === 'user') { // 初始化用户设定数组 if (!settings.userPersonas) { settings.userPersonas = []; // 迁移旧数据 if (settings.userPersona) { settings.userPersonas.push({ id: Date.now(), name: settings.userPersona.name || '用户设定', content: settings.userPersona.customContent || settings.userPersona.content || '', enabled: settings.userPersona.enabled !== false }); } } // 从酒馆读取用户设定(作为默认项,如果没有自定义的话) const stPersona = getUserPersonaFromST(); if (stPersona && settings.userPersonas.length === 0) { settings.userPersonas.push({ id: Date.now(), name: stPersona.name || '用户', content: stPersona.description || '', enabled: true, fromST: true }); } // 添加所有用户设定条目 settings.userPersonas.forEach((persona, idx) => { items.push({ type: 'user-entry', personaIdx: idx, id: persona.id, name: persona.name || '用户设定', content: persona.content || '', enabled: persona.enabled !== false }); }); } // 收集角色卡的世界书条目 - 按角色分组 if (filter === 'all' || filter === 'character') { settings.contacts.forEach((contact, contactIdx) => { if (contact.rawData?.data?.character_book?.entries?.length > 0) { const entries = contact.rawData.data.character_book.entries; // 先添加角色卡头部 items.push({ type: 'character-header', source: contact.name, contactIdx: contactIdx, entriesCount: entries.length, collapsed: contact.lorebookCollapsed !== false // 默认折叠 }); // 再添加条目(如果未折叠) if (contact.lorebookCollapsed === false) { entries.forEach((entry, idx) => { items.push({ type: 'character', source: contact.name, contactIdx: contactIdx, entryIdx: idx, title: entry.comment || entry.keys?.[0] || `条目 ${idx + 1}`, content: entry.content || '', keys: entry.keys || [], enabled: entry.enabled !== false }); }); } } }); } // 收集选择的世界书条目(全局世界书) if (filter === 'all' || filter === 'global') { (settings.selectedLorebooks || []).forEach((lb, lbIdx) => { // 跳过角色卡自带的世界书 if (lb.fromCharacter) return; // 显示世界书本身 items.push({ type: 'global-header', source: lb.name, lorebookIdx: lbIdx, title: lb.name, date: lb.addedTime || '', entriesCount: (lb.entries || []).length, enabled: lb.enabled !== false }); // 显示世界书下的条目 (lb.entries || []).forEach((entry, entryIdx) => { items.push({ type: 'global', source: lb.name, lorebookIdx: lbIdx, entryIdx: entryIdx, title: entry.comment || entry.keys?.[0] || entry.key?.[0] || `条目 ${entryIdx + 1}`, content: entry.content || '', keys: entry.keys || entry.key || [], enabled: entry.enabled !== false && entry.disable !== true }); }); }); } if (items.length === 0) { const emptyMsg = filter === 'user' ? '暂无用户设定
请在酒馆中设置用户人格' : '暂无收藏
导入角色卡或添加世界书'; listEl.innerHTML = `
📚
${emptyMsg}
`; return; } listEl.innerHTML = items.map((item, idx) => { if (item.type === 'user-entry') { // 用户设定条目(带展开面板容器) const isEnabled = item.enabled !== false; const previewText = (item.content || '').substring(0, 40) + ((item.content || '').length > 40 ? '...' : ''); return `
👤
`; } else if (item.type === 'character-header') { // 角色卡世界书标题行(可折叠) const collapseIcon = item.collapsed ? '▶' : '▼'; return `
${collapseIcon}
📝
${escapeHtml(item.source)} ${item.entriesCount} 个条目
`; } else if (item.type === 'global-header') { // 全局世界书标题行 const isEnabled = item.enabled !== false; return `
🌍
${item.title} ${item.entriesCount} 个条目
`; } else { // 条目行(细条)- 带展开面板容器 const enabledClass = item.enabled ? '' : 'disabled'; const typeTag = item.type === 'character' ? '角色' : '全局'; const entryId = `entry-${item.type}-${item.contactIdx ?? 'lb'}-${item.lorebookIdx ?? ''}-${item.entryIdx}`; return `
`; } }).join(''); // 如果是用户标签,在底部添加"新建"按钮 if (filter === 'user') { listEl.innerHTML += ` `; } // 绑定用户设定条目点击事件(展开面板) listEl.querySelectorAll('.wechat-favorites-user-entry').forEach(entry => { entry.addEventListener('click', (e) => { if (e.target.closest('.wechat-toggle')) return; const personaIdx = parseInt(entry.dataset.personaIdx); toggleUserPersonaPanel(personaIdx); }); }); // 绑定用户设定开关 listEl.querySelectorAll('.wechat-favorites-user-entry .wechat-toggle').forEach(toggle => { toggle.addEventListener('click', (e) => { e.stopPropagation(); }); const checkbox = toggle.querySelector('input[type="checkbox"]'); checkbox?.addEventListener('change', (e) => { const personaIdx = parseInt(toggle.dataset.personaIdx); if (settings.userPersonas && settings.userPersonas[personaIdx]) { settings.userPersonas[personaIdx].enabled = e.target.checked; saveSettingsDebounced(); } }); }); // 绑定新建按钮(新建使用弹窗) document.getElementById('wechat-add-persona-btn')?.addEventListener('click', () => { showNewPersonaModal(); // 新建使用弹窗 }); // 绑定角色卡世界书头部点击(展开/折叠) listEl.querySelectorAll('.wechat-favorites-character-header').forEach(header => { header.addEventListener('click', () => { const contactIdx = parseInt(header.dataset.contactIdx); const contact = settings.contacts[contactIdx]; if (contact) { // 切换折叠状态 contact.lorebookCollapsed = contact.lorebookCollapsed === false ? true : false; saveSettingsDebounced(); refreshFavoritesList(filter); } }); }); // 绑定条目点击事件(点击非toggle区域展开面板) listEl.querySelectorAll('.wechat-favorites-entry:not(.wechat-favorites-user-entry)').forEach(entry => { entry.addEventListener('click', (e) => { // 如果点击的是toggle,不展开面板 if (e.target.closest('.wechat-toggle')) return; const type = entry.dataset.type; const entryIdx = parseInt(entry.dataset.entryIdx); const entryId = entry.dataset.entryId; if (type === 'character') { const contactIdx = parseInt(entry.dataset.contactIdx); toggleEntryPanel(type, contactIdx, null, entryIdx, entryId); } else if (type === 'global') { const lbIdx = parseInt(entry.dataset.lbIdx); toggleEntryPanel(type, null, lbIdx, entryIdx, entryId); } }); }); // 绑定删除按钮 listEl.querySelectorAll('.wechat-favorites-delete-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const lbIdx = parseInt(btn.dataset.lbIdx); if (confirm('确定要删除这个世界书吗?')) { settings.selectedLorebooks.splice(lbIdx, 1); saveSettingsDebounced(); refreshFavoritesList(filter); } }); }); // 绑定启用/禁用开关(世界书整体开关) listEl.querySelectorAll('.wechat-favorites-header .wechat-toggle').forEach(toggle => { toggle.addEventListener('click', (e) => { e.stopPropagation(); }); const checkbox = toggle.querySelector('input[type="checkbox"]'); checkbox?.addEventListener('change', (e) => { const lbIdx = parseInt(toggle.dataset.lbIdx); if (settings.selectedLorebooks[lbIdx]) { settings.selectedLorebooks[lbIdx].enabled = e.target.checked; saveSettingsDebounced(); } }); }); // 绑定条目开关 listEl.querySelectorAll('.wechat-favorites-entry .wechat-toggle').forEach(toggle => { toggle.addEventListener('click', (e) => { e.stopPropagation(); }); const checkbox = toggle.querySelector('input[type="checkbox"]'); checkbox?.addEventListener('change', (e) => { const type = toggle.dataset.type; const entryIdx = parseInt(toggle.dataset.entryIdx); if (type === 'character') { const contactIdx = parseInt(toggle.dataset.contactIdx); const contact = settings.contacts[contactIdx]; if (contact?.rawData?.data?.character_book?.entries?.[entryIdx]) { contact.rawData.data.character_book.entries[entryIdx].enabled = e.target.checked; saveSettingsDebounced(); } } else if (type === 'global') { const lbIdx = parseInt(toggle.dataset.lbIdx); if (settings.selectedLorebooks[lbIdx]?.entries?.[entryIdx]) { settings.selectedLorebooks[lbIdx].entries[entryIdx].enabled = e.target.checked; saveSettingsDebounced(); } } // 更新条目样式 const entryEl = toggle.closest('.wechat-favorites-entry'); if (entryEl) { entryEl.classList.toggle('disabled', !e.target.checked); } }); }); } // 当前展开的条目ID let currentExpandedEntryId = null; // 切换条目展开面板 function toggleEntryPanel(type, contactIdx, lbIdx, entryIdx, entryId) { const settings = extension_settings[extensionName]; const panel = document.getElementById(`${entryId}-panel`); const entryEl = document.querySelector(`.wechat-favorites-entry[data-entry-id="${entryId}"]`); if (!panel) return; let entry, source; if (type === 'character') { const contact = settings.contacts[contactIdx]; entry = contact?.rawData?.data?.character_book?.entries?.[entryIdx]; source = contact?.name || '未知角色'; } else { const lb = settings.selectedLorebooks[lbIdx]; entry = lb?.entries?.[entryIdx]; source = lb?.name || '未知世界书'; } if (!entry) { showToast('无法找到条目', '❌'); return; } // 如果已经展开,则收起 if (currentExpandedEntryId === entryId) { closeEntryPanel(); return; } // 先关闭其他展开的面板 if (currentExpandedEntryId) { closeEntryPanel(); } currentExpandedEntryId = entryId; // 填充面板内容 panel.innerHTML = `
${entry.comment || entry.keys?.[0] || '条目详情'}
${entry.enabled !== false && entry.disable !== true ? '✅ 启用' : '❌ 禁用'}
`; // 显示面板 panel.classList.add('wechat-lorebook-panel-show'); entryEl?.classList.add('wechat-favorites-item-expanded'); // 绑定事件 bindEntryPanelEvents(type, contactIdx, lbIdx, entryIdx, entryId); } // 关闭条目展开面板 function closeEntryPanel() { if (!currentExpandedEntryId) return; const panel = document.getElementById(`${currentExpandedEntryId}-panel`); const entryEl = document.querySelector(`.wechat-favorites-entry[data-entry-id="${currentExpandedEntryId}"]`); if (panel) { panel.classList.remove('wechat-lorebook-panel-show'); panel.innerHTML = ''; } entryEl?.classList.remove('wechat-favorites-item-expanded'); currentExpandedEntryId = null; } // 绑定条目面板事件 function bindEntryPanelEvents(type, contactIdx, lbIdx, entryIdx, entryId) { const settings = extension_settings[extensionName]; // 收起按钮 document.getElementById('wechat-entry-panel-close')?.addEventListener('click', () => { closeEntryPanel(); }); // 同步到酒馆 document.getElementById('wechat-entry-sync-btn')?.addEventListener('click', async () => { const keys = document.getElementById('wechat-entry-edit-keys')?.value.trim(); const comment = document.getElementById('wechat-entry-edit-comment')?.value.trim(); const content = document.getElementById('wechat-entry-edit-content')?.value.trim(); if (!content) { showToast('请先填写内容', '⚠️'); return; } try { if (type === 'global') { const lb = settings.selectedLorebooks[lbIdx]; if (lb && lb.name) { await syncLorebookEntryToTavern(lb.name, entryIdx, { keys: keys.split(/[,,]/).map(k => k.trim()).filter(k => k), comment: comment, content: content }); showToast('已同步到酒馆'); } } else { showToast('角色卡条目暂不支持同步', '⚠️'); } } catch (err) { console.error('同步失败:', err); showToast('同步失败: ' + err.message, '❌'); } }); // 保存 document.getElementById('wechat-entry-save-btn')?.addEventListener('click', () => { const keys = document.getElementById('wechat-entry-edit-keys')?.value.trim(); const comment = document.getElementById('wechat-entry-edit-comment')?.value.trim(); const content = document.getElementById('wechat-entry-edit-content')?.value.trim(); let entry; if (type === 'character') { const contact = settings.contacts[contactIdx]; entry = contact?.rawData?.data?.character_book?.entries?.[entryIdx]; } else { entry = settings.selectedLorebooks[lbIdx]?.entries?.[entryIdx]; } if (entry) { entry.keys = keys.split(/[,,]/).map(k => k.trim()).filter(k => k); entry.key = entry.keys; // 兼容两种格式 entry.comment = comment; entry.content = content; saveSettingsDebounced(); showToast('已保存'); closeEntryPanel(); // 刷新列表 const activeTab = document.querySelector('.wechat-favorites-tab.active'); refreshFavoritesList(activeTab?.dataset.tab || 'all'); } }); } // 同步世界书条目到酒馆 async function syncLorebookEntryToTavern(lorebookName, entryIdx, entryData) { try { if (typeof loadWorldInfo !== 'function' || typeof saveWorldInfo !== 'function') { throw new Error('世界书API不可用'); } const worldData = await loadWorldInfo(lorebookName); if (!worldData?.entries) { throw new Error('无法加载世界书数据'); } // 更新条目 if (worldData.entries[entryIdx]) { worldData.entries[entryIdx].key = entryData.keys; worldData.entries[entryIdx].comment = entryData.comment; worldData.entries[entryIdx].content = entryData.content; await saveWorldInfo(lorebookName, worldData); } else { throw new Error('找不到对应的条目'); } } catch (err) { console.error('同步世界书条目失败:', err); throw err; } } // 从酒馆获取用户设定 function getUserPersonaFromST() { try { // SillyTavern 暴露的全局变量 let name = ''; let description = ''; let avatar = ''; // 方法1: 从 getContext 获取 const context = getContext(); if (context) { name = context.name1 || ''; avatar = context.user_avatar || ''; } // 方法2: 从 name1 全局变量获取 if (!name && typeof name1 !== 'undefined') { name = name1; } // 方法3: 从 power_user.persona_description 获取描述 if (typeof power_user !== 'undefined') { if (power_user.persona_description) { description = power_user.persona_description; } // 从 personas 系统获取当前 persona if (power_user.personas && power_user.default_persona) { const currentPersona = power_user.default_persona; if (power_user.personas[currentPersona]) { description = power_user.personas[currentPersona]; if (!name) name = currentPersona; } } } // 方法4: 尝试从 user_avatar 获取名字 if (!name && typeof user_avatar !== 'undefined') { name = user_avatar.replace(/\.[^/.]+$/, ''); // 去掉扩展名 } // 方法5: 从 DOM 获取当前 persona 描述 if (!description) { const personaDescEl = document.querySelector('#persona_description'); if (personaDescEl && personaDescEl.value) { description = personaDescEl.value; } } if (name || description) { return { name, description, avatar }; } } catch (err) { console.error('获取用户设定失败:', err); } return null; } // 当前展开的用户设定索引 let currentExpandedPersonaIdx = -1; // 切换用户设定展开面板 function toggleUserPersonaPanel(personaIdx) { const settings = extension_settings[extensionName]; const panel = document.getElementById(`wechat-persona-panel-${personaIdx}`); const entryEl = document.querySelector(`.wechat-favorites-user-entry[data-persona-idx="${personaIdx}"]`); if (!panel || !settings.userPersonas?.[personaIdx]) return; // 如果已经展开,则收起 if (currentExpandedPersonaIdx === personaIdx) { closeUserPersonaPanel(); return; } // 先关闭其他展开的面板 if (currentExpandedPersonaIdx >= 0) { closeUserPersonaPanel(); } currentExpandedPersonaIdx = personaIdx; const persona = settings.userPersonas[personaIdx]; // 填充面板内容 panel.innerHTML = `
编辑用户设定
💡 启用的设定会作为用户背景发送给AI
`; // 显示面板 panel.classList.add('wechat-lorebook-panel-show'); entryEl?.classList.add('wechat-favorites-item-expanded'); // 绑定事件 bindPersonaPanelEvents(personaIdx); } // 关闭用户设定展开面板 function closeUserPersonaPanel() { if (currentExpandedPersonaIdx < 0) return; const panel = document.getElementById(`wechat-persona-panel-${currentExpandedPersonaIdx}`); const entryEl = document.querySelector(`.wechat-favorites-user-entry[data-persona-idx="${currentExpandedPersonaIdx}"]`); if (panel) { panel.classList.remove('wechat-lorebook-panel-show'); panel.innerHTML = ''; } entryEl?.classList.remove('wechat-favorites-item-expanded'); currentExpandedPersonaIdx = -1; } // 绑定用户设定面板事件 function bindPersonaPanelEvents(personaIdx) { const settings = extension_settings[extensionName]; // 收起按钮 document.getElementById('wechat-persona-panel-close')?.addEventListener('click', () => { closeUserPersonaPanel(); }); // 从酒馆导入 document.getElementById('wechat-persona-import-btn')?.addEventListener('click', () => { const stPersona = getUserPersonaFromST(); if (stPersona) { const nameInput = document.getElementById('wechat-persona-edit-name'); const contentInput = document.getElementById('wechat-persona-edit-content'); if (nameInput) nameInput.value = stPersona.name || ''; if (contentInput) contentInput.value = stPersona.description || ''; showToast('已从酒馆导入用户设定'); } else { showToast('未找到酒馆用户设定', '⚠️'); } }); // 同步到酒馆 document.getElementById('wechat-persona-sync-btn')?.addEventListener('click', () => { const name = document.getElementById('wechat-persona-edit-name')?.value.trim(); const content = document.getElementById('wechat-persona-edit-content')?.value.trim(); if (!content) { showToast('请先填写内容', '⚠️'); return; } syncPersonaToTavern(name, content); }); // 删除 document.getElementById('wechat-persona-delete-btn')?.addEventListener('click', () => { if (confirm('确定要删除这个用户设定吗?')) { settings.userPersonas.splice(personaIdx, 1); saveSettingsDebounced(); closeUserPersonaPanel(); refreshFavoritesList('user'); } }); // 保存 document.getElementById('wechat-persona-save-btn')?.addEventListener('click', () => { const name = document.getElementById('wechat-persona-edit-name')?.value.trim(); const content = document.getElementById('wechat-persona-edit-content')?.value.trim(); if (!name) { showToast('请输入名称', '⚠️'); return; } settings.userPersonas[personaIdx].name = name; settings.userPersonas[personaIdx].content = content; saveSettingsDebounced(); showToast('已保存'); closeUserPersonaPanel(); refreshFavoritesList('user'); }); } // 同步用户设定到酒馆 function syncPersonaToTavern(name, content) { try { // 检查 power_user 是否可用 if (typeof power_user === 'undefined') { showToast('无法访问酒馆设置', '❌'); return; } // 更新 persona_description power_user.persona_description = content; // 如果有 name 且 personas 系统可用,也更新它 if (name && power_user.personas && power_user.default_persona) { power_user.personas[power_user.default_persona] = content; } // 更新 DOM 中的输入框(如果存在) const personaDescEl = document.querySelector('#persona_description'); if (personaDescEl) { personaDescEl.value = content; personaDescEl.dispatchEvent(new Event('input', { bubbles: true })); } // 触发酒馆保存 if (typeof SillyTavern !== 'undefined' && SillyTavern.saveSettingsDebounced) { SillyTavern.saveSettingsDebounced(); } else if (typeof saveSettingsDebounced !== 'undefined') { saveSettingsDebounced(); } showToast('已同步到酒馆'); } catch (err) { console.error('同步到酒馆失败:', err); showToast('同步失败: ' + err.message, '❌'); } } // 显示新建用户设定弹窗 function showNewPersonaModal() { const settings = extension_settings[extensionName]; // 初始化数组 if (!settings.userPersonas) { settings.userPersonas = []; } const modal = document.createElement('div'); modal.className = 'wechat-modal'; modal.id = 'wechat-user-persona-modal'; modal.innerHTML = `
新建用户设定
名称
内容
💡 启用的设定会作为用户背景发送给AI
`; document.body.appendChild(modal); // 取消 modal.querySelector('#wechat-user-persona-cancel').addEventListener('click', () => { modal.remove(); }); // 从酒馆导入 modal.querySelector('#wechat-user-persona-import').addEventListener('click', () => { const stPersona = getUserPersonaFromST(); if (stPersona) { document.getElementById('wechat-user-persona-name').value = stPersona.name || ''; document.getElementById('wechat-user-persona-content').value = stPersona.description || ''; showToast('已从酒馆导入用户设定'); } else { showToast('未找到酒馆用户设定', '⚠️'); } }); // 保存 modal.querySelector('#wechat-user-persona-save').addEventListener('click', () => { const name = document.getElementById('wechat-user-persona-name').value.trim(); const content = document.getElementById('wechat-user-persona-content').value.trim(); if (!name) { showToast('请输入名称', '⚠️'); return; } // 新建 settings.userPersonas.push({ id: Date.now(), name: name, content: content, enabled: true }); saveSettingsDebounced(); refreshFavoritesList('user'); modal.remove(); }); // 点击背景关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } // 获取酒馆世界书列表 async function getLorebooksList() { try { const response = await fetch('/api/worldinfo/get', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({}) }); if (response.ok) { return await response.json(); } } catch (err) { console.error('获取世界书列表失败:', err); } return []; } // 显示世界书选择弹窗 async function showLorebookModal() { const modal = document.getElementById('wechat-lorebook-modal'); const listEl = document.getElementById('wechat-lorebook-list'); listEl.innerHTML = '
加载中...
'; modal.classList.remove('hidden'); try { let lorebooks = []; // SillyTavern 在前端暴露了 world_names 全局变量 if (typeof world_names !== 'undefined' && Array.isArray(world_names)) { lorebooks = [...world_names]; } if (lorebooks.length === 0) { listEl.innerHTML = `
暂无世界书
请在酒馆中创建世界书后刷新
`; return; } // 过滤重复和空值 lorebooks = [...new Set(lorebooks.filter(Boolean))]; listEl.innerHTML = lorebooks.map(name => `
${name}
`).join(''); // 绑定点击事件 listEl.querySelectorAll('.wechat-lorebook-item').forEach(item => { item.addEventListener('click', async () => { const name = item.dataset.name; await loadLorebookEntries(name); modal.classList.add('hidden'); }); }); } catch (err) { console.error('获取世界书失败:', err); listEl.innerHTML = '
加载失败: ' + err.message + '
'; } } // 加载世界书条目 async function loadLorebookEntries(lorebookName) { const settings = extension_settings[extensionName]; if (!settings.selectedLorebooks) { settings.selectedLorebooks = []; } // 检查是否已添加 if (settings.selectedLorebooks.some(lb => lb.name === lorebookName)) { showToast('该世界书已在收藏中', '⚠️'); return; } let entries = []; try { // 使用 SillyTavern 的 loadWorldInfo 函数加载世界书数据 const data = await loadWorldInfo(lorebookName); if (data && data.entries) { entries = Object.values(data.entries); } } catch (err) { console.error('加载世界书条目失败:', err); } const now = new Date(); const timeStr = `${(now.getMonth() + 1)}月${now.getDate()}日`; settings.selectedLorebooks.push({ name: lorebookName, addedTime: timeStr, entries: entries }); saveSettingsDebounced(); refreshFavoritesList(); if (entries.length > 0) { showToast(`已添加: ${lorebookName} (${entries.length}条)`); } else { showToast(`已添加: ${lorebookName}`); } } // 添加世界书到收藏 function addLorebookToFavorites(name) { const settings = extension_settings[extensionName]; if (!settings.selectedLorebooks) { settings.selectedLorebooks = []; } // 检查是否已添加 if (settings.selectedLorebooks.some(lb => lb.name === name)) { showToast('该世界书已在收藏中', '⚠️'); return; } const now = new Date(); const timeStr = `${(now.getMonth() + 1)}月${now.getDate()}日`; settings.selectedLorebooks.push({ name: name, addedTime: timeStr }); saveSettingsDebounced(); refreshFavoritesList(); showToast(`已添加: ${name}`); } // ========== 总结功能相关函数 ========== // 世界书名称(固定) const LOREBOOK_NAME = '【可乐】聊天记录'; // 获取当前应该是第几杯 function getNextCupNumber() { const settings = extension_settings[extensionName]; const selectedLorebooks = settings.selectedLorebooks || []; // 查找【可乐】聊天记录世界书 const lorebook = selectedLorebooks.find(lb => lb.name === LOREBOOK_NAME); if (lorebook && lorebook.entries) { return lorebook.entries.length + 1; } return 1; } // 标记前缀 const SUMMARY_MARKER_PREFIX = '🧊 可乐已加冰_'; // 收集所有联系人的聊天记录(只收集最后一个标记之后的内容) function collectAllChatHistory() { const settings = extension_settings[extensionName]; const contacts = settings.contacts || []; const allChats = []; contacts.forEach(contact => { const chatHistory = contact.chatHistory || []; if (chatHistory.length === 0) return; // 查找最后一个标记的位置 let lastMarkerIndex = -1; for (let i = chatHistory.length - 1; i >= 0; i--) { if (chatHistory[i].content?.startsWith(SUMMARY_MARKER_PREFIX)) { lastMarkerIndex = i; break; } } // 只收集标记之后的消息 const startIndex = lastMarkerIndex + 1; const newMessages = chatHistory.slice(startIndex); // 过滤掉系统标记消息,只保留真实对话 const realMessages = newMessages.filter(msg => !msg.content?.startsWith(SUMMARY_MARKER_PREFIX) ); if (realMessages.length > 0) { allChats.push({ contactName: contact.name, contactDescription: contact.description || '', messages: realMessages.map(msg => ({ role: msg.role, content: msg.content, time: msg.time || '', isVoice: msg.isVoice || false })) }); } }); return allChats; } // 在所有联系人的聊天记录中插入标记 function insertSummaryMarker(cupNumber) { const settings = extension_settings[extensionName]; const contacts = settings.contacts || []; const marker = `${SUMMARY_MARKER_PREFIX}${cupNumber}`; 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')}`; contacts.forEach(contact => { if (!contact.chatHistory) contact.chatHistory = []; // 检查该联系人是否有未总结的消息 let hasNewMessages = false; for (let i = contact.chatHistory.length - 1; i >= 0; i--) { const msg = contact.chatHistory[i]; if (msg.content?.startsWith(SUMMARY_MARKER_PREFIX)) { break; // 找到标记,停止 } if (!msg.content?.startsWith(SUMMARY_MARKER_PREFIX)) { hasNewMessages = true; break; } } // 只有有新消息的联系人才插入标记 if (hasNewMessages || contact.chatHistory.length === 0) { // 如果最后一条消息不是标记,才插入 const lastMsg = contact.chatHistory[contact.chatHistory.length - 1]; if (!lastMsg?.content?.startsWith(SUMMARY_MARKER_PREFIX)) { contact.chatHistory.push({ role: 'system', content: marker, time: timeStr, timestamp: Date.now(), isMarker: true }); } } }); saveSettingsDebounced(); } // 生成总结提示词(每次只生成一杯,记录感情变化) function generateSummaryPrompt(allChats, cupNumber) { let prompt = `分析以下微信聊天记录,记录感情关系的变化。 【任务】 这是第${cupNumber}杯记录。请总结这段对话中感情关系的发展和变化。 【记录要点】 - 感情状态的变化(亲密度、信任度、态度转变等) - 关系中的重要事件(约定、承诺、矛盾、和解等) - 双方互动的关键内容 - 只记录事实,不做主观评价 【输出要求】 - 只输出一个条目的JSON - 不要使用markdown代码块 - 直接以 { 开头,以 } 结尾 【JSON格式】 {"keys":["关键词1","关键词2"],"content":"感情变化记录","comment":"第${cupNumber}杯"} 【示例】 {"keys":["表白","确认关系"],"content":"小美向用户表白,用户接受。两人确认恋爱关系,约定周末见面。小美表现得很开心,多次说想用户。","comment":"第1杯"} 【聊天记录】 `; allChats.forEach(chat => { prompt += `\n[与${chat.contactName}的对话]\n`; chat.messages.slice(-300).forEach(msg => { // 取最近300条消息 const speaker = msg.role === 'user' ? '用户' : chat.contactName; prompt += `${speaker}: ${msg.content}\n`; }); }); prompt += `\n总结这段对话中的感情变化,输出第${cupNumber}杯的JSON:`; return prompt; } // 调用总结API async function callSummaryAPI(prompt) { const settings = extension_settings[extensionName]; const apiUrl = settings.summaryApiUrl; const apiKey = settings.summaryApiKey; const model = settings.summarySelectedModel; if (!apiUrl || !apiKey || !model) { throw new Error('请先配置总结API(URL、密钥和模型)'); } const chatUrl = apiUrl.replace(/\/$/, '') + '/chat/completions'; const response = await fetch(chatUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model, messages: [ { role: 'system', content: '你是一个专业的内容分析师,擅长从对话中提取关键信息并生成结构化的世界书条目。' }, { role: 'user', content: prompt } ], temperature: 1, max_tokens: 8196 }) }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error?.message || `HTTP ${response.status}`); } const data = await response.json(); const content = data.choices?.[0]?.message?.content || ''; console.log('[可乐不加冰] AI原始响应:', content); // 尝试解析JSON(多种方式) const parseJSON = (str) => { // 方法1: 直接解析 try { const result = JSON.parse(str); console.log('[可乐不加冰] 方法1成功: 直接解析'); return result; } catch (e) {} // 方法2: 移除 markdown 代码块后解析 try { const cleaned = str.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim(); const result = JSON.parse(cleaned); console.log('[可乐不加冰] 方法2成功: 移除markdown'); return result; } catch (e) {} // 方法3: 从文本中提取 JSON 对象(找第一个 { 到最后一个 }) try { const firstBrace = str.indexOf('{'); const lastBrace = str.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace > firstBrace) { const jsonPart = str.substring(firstBrace, lastBrace + 1); const result = JSON.parse(jsonPart); console.log('[可乐不加冰] 方法3成功: 提取JSON部分'); return result; } } catch (e) {} // 方法4: 尝试匹配 entries 数组(更宽松) try { // 找到 "entries" 后的数组内容 const match = str.match(/"entries"\s*:\s*\[/); if (match) { const startIdx = str.indexOf('[', match.index); let bracketCount = 1; let endIdx = startIdx + 1; while (endIdx < str.length && bracketCount > 0) { if (str[endIdx] === '[') bracketCount++; if (str[endIdx] === ']') bracketCount--; endIdx++; } const arrayContent = str.substring(startIdx, endIdx); const result = JSON.parse(`{"entries":${arrayContent}}`); console.log('[可乐不加冰] 方法4成功: 提取entries数组'); return result; } } catch (e) {} // 方法5: 尝试修复常见JSON错误 try { let fixed = str .replace(/,\s*}/g, '}') .replace(/,\s*]/g, ']') .replace(/[\u201c\u201d]/g, '"') // 中文引号 .replace(/'/g, '"'); const firstBrace = fixed.indexOf('{'); const lastBrace = fixed.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace > firstBrace) { const result = JSON.parse(fixed.substring(firstBrace, lastBrace + 1)); console.log('[可乐不加冰] 方法5成功: 修复JSON格式'); return result; } } catch (e) {} // 方法6: 从非JSON文本中提取结构化信息 try { const entries = []; const blocks = str.split(/\n\n+|\d+\.\s+/); for (const block of blocks) { if (!block.trim()) continue; let keys = []; let content = ''; let comment = ''; // 尝试提取关键词 const keyMatch = block.match(/[关键词keys]+[::\s]+([^\n]+)/i); if (keyMatch) keys = keyMatch[1].split(/[,,、]/g).map(k => k.trim()).filter(k => k); // 尝试提取内容 const contentMatch = block.match(/[内容content]+[::\s]+([^\n]+)/i); if (contentMatch) content = contentMatch[1].trim(); // 尝试提取标题 const titleMatch = block.match(/[标题title评论comment]+[::\s]+([^\n]+)/i); if (titleMatch) comment = titleMatch[1].trim(); // 如果有足够信息,创建条目 if ((keys.length > 0 || comment) && content) { entries.push({ keys: keys.length > 0 ? keys : [comment || '关键词'], content: content, comment: comment || keys[0] || '条目' }); } } if (entries.length > 0) { console.log('[可乐不加冰] 方法6成功: 从文本提取'); return { entries }; } } catch (e) {} return null; }; const parsed = parseJSON(content); if (parsed) { // 现在返回单个条目格式(不是 entries 数组) // 如果解析结果有 keys 和 content,说明是单条目 if (parsed.keys && parsed.content) { console.log('[可乐不加冰] 解析成功: 单条目格式'); return parsed; } // 兼容旧的 entries 数组格式(取第一个) if (parsed.entries && parsed.entries.length > 0) { console.log('[可乐不加冰] 解析成功: entries数组格式,取第一个'); return parsed.entries[0]; } // 如果是数组,取第一个 if (Array.isArray(parsed) && parsed.length > 0) { console.log('[可乐不加冰] 解析成功: 数组格式,取第一个'); return parsed[0]; } } // 最终降级:如果内容不为空,创建一个基本条目 console.error('[可乐不加冰] 所有解析方法失败,原始内容:', content); if (content && content.trim().length > 20) { console.log('[可乐不加冰] 使用降级方案:创建基本条目'); // 提取有意义的文本片段作为关键词 const words = content.match(/[\u4e00-\u9fa5]{2,}/g) || ['聊天', '记录']; const uniqueWords = [...new Set(words)].slice(0, 5); return { keys: uniqueWords.length > 0 ? uniqueWords : ['聊天记录'], content: content.substring(0, 800).replace(/```[\s\S]*?```/g, '').trim(), comment: '感情记录' }; } throw new Error('AI返回内容为空或无法解析'); } // 保存单个条目到收藏(追加到已有世界书) function saveEntryToFavorites(entry, cupNumber) { const settings = extension_settings[extensionName]; if (!settings.selectedLorebooks) { settings.selectedLorebooks = []; } 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 lorebook = settings.selectedLorebooks.find(lb => lb.name === LOREBOOK_NAME); if (!lorebook) { // 不存在则创建新的 lorebook = { name: LOREBOOK_NAME, addedTime: timeStr, entries: [], enabled: true, fromSummary: true }; settings.selectedLorebooks.push(lorebook); } // 格式化新条目 const newEntry = { uid: cupNumber - 1, keys: entry.keys || [], content: entry.content || '', comment: entry.comment || `第${cupNumber}杯`, enabled: true, case_sensitive: false, priority: 10, id: cupNumber - 1, addedTime: timeStr }; // 追加条目 lorebook.entries.push(newEntry); lorebook.lastUpdated = timeStr; saveSettingsDebounced(); return lorebook; } // 同步单个条目到酒馆世界书(追加模式) async function syncEntryToSillyTavern(entry, cupNumber) { try { const name = LOREBOOK_NAME; // 构建单个条目格式 const newEntry = { uid: cupNumber - 1, key: entry.keys || [], keysecondary: [], comment: entry.comment || `第${cupNumber}杯`, content: entry.content || '', constant: false, vectorized: false, selective: true, selectiveLogic: 0, addMemo: true, order: 100, position: 0, disable: false, excludeRecursion: false, preventRecursion: false, delayUntilRecursion: false, probability: 100, useProbability: true, depth: 4, group: '', groupOverride: false, groupWeight: 100, scanDepth: null, caseSensitive: false, matchWholeWords: null, useGroupScoring: null, automationId: '', role: 0, sticky: null, cooldown: null, delay: null }; console.log('[可乐不加冰] 准备同步第', cupNumber, '杯到酒馆'); // 检查世界书是否已存在 const worldExists = typeof world_names !== 'undefined' && Array.isArray(world_names) && world_names.includes(name); if (!worldExists) { // 世界书不存在,创建新的 console.log('[可乐不加冰] 世界书不存在,创建新的...'); if (typeof createNewWorldInfo === 'function') { await createNewWorldInfo(name); await sleep(500); } } // 加载现有世界书数据 let worldInfo = { entries: {} }; if (typeof loadWorldInfo === 'function') { const existingData = await loadWorldInfo(name); if (existingData && existingData.entries) { worldInfo = existingData; } } // 追加新条目(使用 cupNumber-1 作为 key,确保不会覆盖) const entryKey = cupNumber - 1; worldInfo.entries[entryKey] = newEntry; console.log('[可乐不加冰] 当前条目数:', Object.keys(worldInfo.entries).length); // 保存世界书 if (typeof saveWorldInfo === 'function') { await saveWorldInfo(name, worldInfo); console.log('[可乐不加冰] 保存完成'); // 验证 await sleep(300); const verifyData = await loadWorldInfo(name); const savedCount = verifyData?.entries ? Object.keys(verifyData.entries).length : 0; console.log('[可乐不加冰] 验证: 条目数 =', savedCount); return true; } throw new Error('saveWorldInfo 函数不可用'); } catch (err) { console.error('[可乐不加冰] 同步到酒馆失败:', err); throw err; } } // 执行总结主函数 async function executeSummary() { const progressEl = document.getElementById('wechat-summary-progress'); const executeBtn = document.getElementById('wechat-summary-execute'); const updateProgress = (msg) => { if (progressEl) progressEl.textContent = msg; }; // 禁用按钮 if (executeBtn) { executeBtn.disabled = true; executeBtn.textContent = '⏳ 处理中...'; } try { // 步骤1: 收集聊天记录 updateProgress('📋 收集聊天记录...'); const allChats = collectAllChatHistory(); if (allChats.length === 0) { throw new Error('没有新的聊天记录需要总结'); } const totalMessages = allChats.reduce((sum, chat) => sum + chat.messages.length, 0); updateProgress(`📋 收集到 ${allChats.length} 个对话,共 ${totalMessages} 条消息`); await sleep(500); // 步骤2: 获取当前杯数 const cupNumber = getNextCupNumber(); updateProgress(`🍵 准备生成第${cupNumber}杯...`); await sleep(300); // 步骤3: 生成提示词并调用API updateProgress('🤖 调用AI分析感情变化...'); const prompt = generateSummaryPrompt(allChats, cupNumber); const entry = await callSummaryAPI(prompt); updateProgress(`✨ 已生成第${cupNumber}杯记录`); await sleep(500); // 步骤4: 保存到收藏(追加到【可乐】聊天记录世界书) updateProgress('💾 保存到收藏...'); saveEntryToFavorites(entry, cupNumber); await sleep(300); // 步骤5: 同步到酒馆(可选,失败不影响使用) updateProgress('📤 尝试同步到酒馆...'); try { await syncEntryToSillyTavern(entry, cupNumber); updateProgress(`✅ 完成!第${cupNumber}杯已保存`); } catch (syncErr) { // 同步失败但本地保存成功,这是可以接受的 console.error('同步到酒馆失败:', syncErr); updateProgress(`✅ 第${cupNumber}杯已保存到收藏!(酒馆同步暂不可用)`); } // 步骤6: 插入标记,防止下次重复总结 insertSummaryMarker(cupNumber); // 刷新收藏列表 refreshFavoritesList(); } catch (err) { console.error('执行总结失败:', err); updateProgress(`❌ 失败: ${err.message}`); } finally { // 恢复按钮 if (executeBtn) { executeBtn.disabled = false; executeBtn.textContent = '执行总结'; } } } // 回退总结(删除最后一杯) async function rollbackSummary() { const settings = extension_settings[extensionName]; const progressEl = document.getElementById('wechat-summary-progress'); const updateProgress = (msg) => { if (progressEl) progressEl.textContent = msg; }; // 查找【可乐】聊天记录世界书 const selectedLorebooks = settings.selectedLorebooks || []; const lorebookIdx = selectedLorebooks.findIndex(lb => lb.name === LOREBOOK_NAME); if (lorebookIdx < 0 || !selectedLorebooks[lorebookIdx].entries?.length) { updateProgress('❌ 没有可回退的总结'); return; } const lorebook = selectedLorebooks[lorebookIdx]; const cupNumber = lorebook.entries.length; // 当前是第几杯 if (!confirm(`确定要回退第${cupNumber}杯总结吗?\n\n这将删除:\n1. 世界书中的第${cupNumber}杯条目\n2. 所有聊天记录中的"${SUMMARY_MARKER_PREFIX}${cupNumber}"标记`)) { return; } updateProgress(`🔄 正在回退第${cupNumber}杯...`); try { // 1. 从收藏中删除最后一个条目 lorebook.entries.pop(); updateProgress('📋 已删除收藏中的条目...'); // 2. 从所有联系人聊天记录中删除对应标记 const markerToRemove = `${SUMMARY_MARKER_PREFIX}${cupNumber}`; const contacts = settings.contacts || []; let removedCount = 0; contacts.forEach(contact => { if (!contact.chatHistory) return; // 从后往前遍历,删除匹配的标记 for (let i = contact.chatHistory.length - 1; i >= 0; i--) { const msg = contact.chatHistory[i]; if (msg.content === markerToRemove || (msg.isMarker && msg.content?.startsWith(SUMMARY_MARKER_PREFIX + cupNumber))) { contact.chatHistory.splice(i, 1); removedCount++; } } }); updateProgress(`📋 已删除 ${removedCount} 个聊天标记...`); // 3. 尝试从酒馆世界书中删除 try { if (typeof loadWorldInfo === 'function' && typeof saveWorldInfo === 'function') { const worldData = await loadWorldInfo(LOREBOOK_NAME); if (worldData?.entries) { // 删除对应的条目(key 是 cupNumber - 1) const entryKey = cupNumber - 1; if (worldData.entries[entryKey]) { delete worldData.entries[entryKey]; await saveWorldInfo(LOREBOOK_NAME, worldData); updateProgress('📤 已同步删除酒馆世界书条目...'); } } } } catch (syncErr) { console.error('同步删除酒馆条目失败:', syncErr); // 不影响本地回退 } // 4. 保存设置 saveSettingsDebounced(); // 5. 刷新界面 refreshFavoritesList(); refreshChatList(); // 如果当前在聊天页面,刷新聊天历史显示 if (currentChatIndex >= 0) { const contact = settings.contacts[currentChatIndex]; if (contact) { const messagesContainer = document.getElementById('wechat-chat-messages'); if (messagesContainer) { messagesContainer.innerHTML = renderChatHistory(contact, contact.chatHistory || []); bindVoiceBubbleEvents(messagesContainer); } } } updateProgress(`✅ 已回退第${cupNumber}杯,当前剩余 ${lorebook.entries.length} 杯`); } catch (err) { console.error('回退总结失败:', err); updateProgress(`❌ 回退失败: ${err.message}`); } } // 测试 API 连接 async function testApiConnection(apiUrl, apiKey) { try { // 尝试请求 /models 端点(OpenAI 兼容格式) const modelsUrl = apiUrl.replace(/\/+$/, '') + '/models'; const headers = { 'Content-Type': 'application/json', }; if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } const response = await fetch(modelsUrl, { method: 'GET', headers: headers, }); if (response.ok) { const data = await response.json(); const modelCount = data.data?.length || 0; return { success: true, message: `发现 ${modelCount} 个可用模型` }; } else { const errorText = await response.text(); return { success: false, message: `HTTP ${response.status}: ${errorText.substring(0, 100)}` }; } } catch (err) { return { success: false, message: err.message }; } } // 获取 API 配置 function getApiConfig() { const settings = extension_settings[extensionName]; return { url: settings.apiUrl || '', key: settings.apiKey || '', model: settings.selectedModel || 'gpt-3.5-turbo' }; } // 获取模型列表 async function fetchModelList() { const apiUrl = document.getElementById('wechat-api-url')?.value.trim(); const apiKey = document.getElementById('wechat-api-key')?.value.trim(); if (!apiUrl) { showToast('请先填写 API 地址', '⚠️'); return []; } const modelsUrl = apiUrl.replace(/\/+$/, '') + '/models'; const headers = { 'Content-Type': 'application/json', }; if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } try { const response = await fetch(modelsUrl, { method: 'GET', headers: headers, }); if (response.ok) { const data = await response.json(); // 兼容 OpenAI 格式和其他格式 let models = []; if (data.data && Array.isArray(data.data)) { // OpenAI 格式 models = data.data.map(m => ({ id: m.id, name: m.id })); } else if (Array.isArray(data)) { // 直接数组格式 models = data.map(m => ({ id: typeof m === 'string' ? m : m.id, name: typeof m === 'string' ? m : (m.name || m.id) })); } return models; } else { const errorText = await response.text(); showToast(`获取模型列表失败: HTTP ${response.status}`, '❌'); return []; } } catch (err) { showToast(`获取模型列表失败: ${err.message}`, '❌'); return []; } } // 刷新模型下拉列表 async function refreshModelSelect() { const select = document.getElementById('wechat-model-select'); const refreshBtn = document.getElementById('wechat-refresh-models'); if (!select) return; // 显示加载状态 const originalText = refreshBtn?.textContent; if (refreshBtn) { refreshBtn.textContent = '加载中...'; refreshBtn.disabled = true; } const models = await fetchModelList(); const settings = extension_settings[extensionName]; // 清空现有选项 select.innerHTML = ''; if (models.length === 0) { select.innerHTML = ''; } else { select.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name; if (model.id === settings.selectedModel) { option.selected = true; } select.appendChild(option); }); // 缓存模型列表 settings.modelList = models; saveSettingsDebounced(); } // 恢复按钮状态 if (refreshBtn) { refreshBtn.textContent = originalText; refreshBtn.disabled = false; } } // 从缓存恢复模型列表 function restoreModelSelect() { const select = document.getElementById('wechat-model-select'); if (!select) return; const settings = extension_settings[extensionName]; const models = settings.modelList || []; if (models.length > 0) { select.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name; if (model.id === settings.selectedModel) { option.selected = true; } select.appendChild(option); }); } } // 渲染聊天历史 function renderChatHistory(contact, chatHistory) { const firstChar = contact.name ? contact.name.charAt(0) : '?'; const avatarContent = contact.avatar ? `` : firstChar; let html = ''; let lastTimestamp = 0; const TIME_GAP_THRESHOLD = 5 * 60 * 1000; // 5分钟间隔显示时间 chatHistory.forEach((msg, index) => { // 获取消息时间戳 const msgTimestamp = msg.timestamp || new Date(msg.time).getTime() || 0; // 检查是否是总结标记消息 if (msg.isMarker || msg.content?.startsWith(SUMMARY_MARKER_PREFIX)) { // 像时间戳一样居中显示标记 const markerText = msg.content || '可乐已加冰'; html += `
${escapeHtml(markerText)}
`; lastTimestamp = msgTimestamp; return; // 跳过后续的普通消息渲染 } // 判断是否需要显示时间标签(间隔超过5分钟或第一条消息) if (index === 0 || (msgTimestamp - lastTimestamp > TIME_GAP_THRESHOLD)) { const timeLabel = formatMessageTime(msgTimestamp); if (timeLabel) { html += `
${timeLabel}
`; } } lastTimestamp = msgTimestamp; // 判断是否是语音消息 const isVoice = msg.isVoice === true; let bubbleContent; if (isVoice) { bubbleContent = generateVoiceBubbleStatic(msg.content, msg.role === 'user'); } else { bubbleContent = `
${escapeHtml(msg.content)}
`; } if (msg.role === 'user') { // 用户消息(右侧) html += `
${getUserAvatarHTML()}
${bubbleContent}
`; } else { // AI/角色消息(左侧) html += `
${avatarContent}
${bubbleContent}
`; } }); return html; } // 格式化消息时间标签(微信风格) function formatMessageTime(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp); const now = new Date(); const diff = now - date; const oneDay = 24 * 60 * 60 * 1000; const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); const timeStr = `${hours}:${minutes}`; // 今天:只显示时间 if (diff < oneDay && date.getDate() === now.getDate()) { return timeStr; } // 昨天 const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); if (date.getDate() === yesterday.getDate() && date.getMonth() === yesterday.getMonth() && date.getFullYear() === yesterday.getFullYear()) { return `昨天 ${timeStr}`; } // 一周内:显示星期几 if (diff < 7 * oneDay) { const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; return `${days[date.getDay()]} ${timeStr}`; } // 更早:显示日期 return `${date.getMonth() + 1}月${date.getDate()}日 ${timeStr}`; } // 生成静态语音消息HTML(用于历史记录,带唯一ID) function generateVoiceBubbleStatic(content, isSelf) { const duration = calculateVoiceDuration(content); const width = Math.min(200, Math.max(80, 60 + duration * 3)); const uniqueId = 'voice-hist-' + Math.random().toString(36).substring(2, 11); // 语音图标SVG - 三条弧线样式(微信风格) // 发送消息(右侧绿色气泡):弧线朝左 ((( // 接收消息(左侧白色气泡):弧线朝右 ))) const voiceIconSvg = isSelf ? ` ` : ` `; return `
${duration}" ${voiceIconSvg}
`; } // HTML 转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 显示Toast提示 function showToast(message, icon = '✅') { const phone = document.getElementById('wechat-phone'); if (!phone) return; // 移除已有的toast const existingToast = phone.querySelector('.wechat-toast'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.className = 'wechat-toast'; toast.innerHTML = `${icon}${escapeHtml(message)}`; phone.appendChild(toast); // 动画结束后移除 setTimeout(() => { toast.remove(); }, 2000); } // 根据内容长度计算语音秒数 function calculateVoiceDuration(content) { // 大约每3个字符1秒,最少2秒,最多60秒 const seconds = Math.max(2, Math.min(60, Math.ceil(content.length / 3))); return seconds; } // 生成语音消息HTML function generateVoiceBubble(content, isSelf) { const duration = calculateVoiceDuration(content); // 语音条宽度根据秒数变化,最小80px,最大200px const width = Math.min(200, Math.max(80, 60 + duration * 3)); const uniqueId = 'voice-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11); // 语音图标SVG - 三条弧线样式(微信风格) // 发送消息(右侧绿色气泡):弧线朝左 ((( // 接收消息(左侧白色气泡):弧线朝右 ))) const voiceIconSvg = isSelf ? ` ` : ` `; return `
${duration}" ${voiceIconSvg}
`; } // 添加消息到聊天界面(支持语音消息) function appendMessage(role, content, contact, isVoice = false) { const messagesContainer = document.getElementById('wechat-chat-messages'); if (!messagesContainer) return; const firstChar = contact?.name ? contact.name.charAt(0) : '?'; const avatarContent = contact?.avatar ? `` : firstChar; let bubbleContent; if (isVoice) { bubbleContent = generateVoiceBubble(content, role === 'user'); } else { bubbleContent = `
${escapeHtml(content)}
`; } let messageHtml = ''; if (role === 'user') { messageHtml = `
${getUserAvatarHTML()}
${bubbleContent}
`; } else { messageHtml = `
${avatarContent}
${bubbleContent}
`; } messagesContainer.insertAdjacentHTML('beforeend', messageHtml); messagesContainer.scrollTop = messagesContainer.scrollHeight; // 绑定语音点击事件 if (isVoice) { bindVoiceBubbleEvents(messagesContainer); } } // 显示打字中状态 function showTypingIndicator(contact) { const messagesContainer = document.getElementById('wechat-chat-messages'); if (!messagesContainer) return; const firstChar = contact?.name ? contact.name.charAt(0) : '?'; const avatarContent = contact?.avatar ? `` : firstChar; const typingHtml = `
${avatarContent}
`; messagesContainer.insertAdjacentHTML('beforeend', typingHtml); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 隐藏打字中状态 function hideTypingIndicator() { const indicator = document.querySelector('.wechat-typing-indicator'); if (indicator) { indicator.remove(); } } // 从消息中提取指定标签内容(支持多个标签) function extractCustomTags(message, tags) { if (!tags || tags.length === 0) return ''; const results = []; tags.forEach(tag => { // 构建正则表达式,匹配 内容 const regex = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'gi'); const matches = message.match(regex); if (matches) { matches.forEach(m => { const content = m.replace(new RegExp(`<\\/?${tag}>`, 'gi'), '').trim(); if (content) { results.push(content); } }); } }); return results.join('\n'); } // 从主界面消息中提取时间 function extractTimeFromSTChat() { const settings = extension_settings[extensionName]; try { const context = getContext(); const chat = context.chat || []; if (chat.length === 0) return null; // 从最近的消息中查找时间标签(取最近5条) const recentChat = chat.slice(-5); // 时间标签列表(优先级从高到低) const defaultTimeTags = ['time', 'timestamp', '时间', 'datetime', 'date', 'now']; // 合并用户配置的标签中可能包含时间的标签 const customTags = settings.contextTags || []; const timeRelatedCustomTags = customTags.filter(tag => tag.toLowerCase().includes('time') || tag.includes('时间') || tag.includes('日期') ); const allTimeTags = [...defaultTimeTags, ...timeRelatedCustomTags]; // 从最新消息向前搜索 for (let i = recentChat.length - 1; i >= 0; i--) { const msg = recentChat[i]; const content = msg.mes || ''; // 尝试从标签中提取时间 for (const tag of allTimeTags) { const regex = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'i'); const match = content.match(regex); if (match && match[1]) { const timeStr = match[1].trim(); const parsedTime = parseTimeString(timeStr); if (parsedTime) { console.log(`[可乐不加冰] 从主界面提取到时间: ${timeStr} -> ${new Date(parsedTime).toLocaleString()}`); return parsedTime; } } } } return null; } catch (err) { console.error('提取时间失败:', err); return null; } } // 解析时间字符串为时间戳 function parseTimeString(timeStr) { if (!timeStr) return null; // 格式1: HH:MM 或 H:MM(纯时间,使用今天日期) const timeOnlyMatch = timeStr.match(/^(\d{1,2}):(\d{2})$/); if (timeOnlyMatch) { const now = new Date(); const hours = parseInt(timeOnlyMatch[1]); const minutes = parseInt(timeOnlyMatch[2]); if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { now.setHours(hours, minutes, 0, 0); return now.getTime(); } } // 格式2: YYYY-MM-DD HH:MM:SS 或 YYYY/MM/DD HH:MM:SS const fullDateMatch = timeStr.match(/(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?/); if (fullDateMatch) { const date = new Date( parseInt(fullDateMatch[1]), parseInt(fullDateMatch[2]) - 1, parseInt(fullDateMatch[3]), parseInt(fullDateMatch[4]), parseInt(fullDateMatch[5]), parseInt(fullDateMatch[6] || '0') ); return date.getTime(); } // 格式3: MM-DD HH:MM 或 M月D日 HH:MM(使用今年) const dateTimeMatch = timeStr.match(/(\d{1,2})[-月](\d{1,2})[日]?\s+(\d{1,2}):(\d{2})/); if (dateTimeMatch) { const now = new Date(); const date = new Date( now.getFullYear(), parseInt(dateTimeMatch[1]) - 1, parseInt(dateTimeMatch[2]), parseInt(dateTimeMatch[3]), parseInt(dateTimeMatch[4]) ); return date.getTime(); } // 格式4: 中文描述如"上午10:30"、"下午3:45"、"凌晨2:00" const chineseTimeMatch = timeStr.match(/(上午|下午|凌晨|中午|晚上|早上)?(\d{1,2}):(\d{2})/); if (chineseTimeMatch) { const now = new Date(); let hours = parseInt(chineseTimeMatch[2]); const minutes = parseInt(chineseTimeMatch[3]); const period = chineseTimeMatch[1]; if (period === '下午' || period === '晚上') { if (hours < 12) hours += 12; } else if ((period === '上午' || period === '凌晨' || period === '早上') && hours === 12) { hours = 0; } now.setHours(hours, minutes, 0, 0); return now.getTime(); } // 格式5: 纯数字时间戳 if (/^\d{10,13}$/.test(timeStr)) { const ts = parseInt(timeStr); // 如果是10位(秒),转换为毫秒 return ts < 10000000000 ? ts * 1000 : ts; } // 格式6: 尝试 Date.parse(最后手段) const parsed = Date.parse(timeStr); if (!isNaN(parsed)) { return parsed; } return null; } // 获取酒馆主聊天的上下文 function getSTChatContext(layers) { const settings = extension_settings[extensionName]; // 检查开关 if (!settings.contextEnabled) return ''; if (layers <= 0) return ''; const tags = settings.contextTags || []; if (tags.length === 0) return ''; try { const context = getContext(); const chat = context.chat || []; if (chat.length === 0) return ''; // 取最近 N 条消息 const recentChat = chat.slice(-layers); // 提取标签内容 const contents = []; recentChat.forEach(msg => { const extracted = extractCustomTags(msg.mes || '', tags); if (extracted) { const role = msg.is_user ? '用户' : (msg.name || '角色'); contents.push(`[${role}]: ${extracted}`); } }); if (contents.length === 0) return ''; return `【剧情上下文】\n${contents.join('\n')}\n`; } catch (err) { console.error('获取酒馆上下文失败:', err); return ''; } } // 刷新上下文标签显示 function refreshContextTags() { const settings = extension_settings[extensionName]; const tagsContainer = document.getElementById('wechat-context-tags'); if (!tagsContainer) return; const tags = settings.contextTags || []; // 标签 + 添加按钮,按钮始终在最后 tagsContainer.innerHTML = tags.map((tag, i) => `
<${tag}>
`).join('') + ''; } // 构建 AI 请求的系统提示 function buildSystemPrompt(contact) { const settings = extension_settings[extensionName]; const rawData = contact.rawData || {}; const charData = rawData.data || rawData; let systemPrompt = ''; // 酒馆主聊天上下文(根据层数设置) const contextLevel = settings.contextLevel ?? 5; const stContext = getSTChatContext(contextLevel); if (stContext) { systemPrompt += stContext + '\n'; } // 用户设定(收集所有启用的设定) const userPersonas = settings.userPersonas || []; const enabledPersonas = userPersonas.filter(p => p.enabled !== false); if (enabledPersonas.length > 0) { systemPrompt += `【用户设定】\n`; enabledPersonas.forEach(persona => { if (persona.name) { systemPrompt += `[${persona.name}]\n`; } if (persona.content) { systemPrompt += `${persona.content}\n`; } }); systemPrompt += '\n'; } // 角色名 if (charData.name) { systemPrompt += `你是 ${charData.name}。\n\n`; } // 角色描述 if (charData.description) { systemPrompt += `【角色描述】\n${charData.description}\n\n`; } // 角色性格 if (charData.personality) { systemPrompt += `【性格】\n${charData.personality}\n\n`; } // 场景 if (charData.scenario) { systemPrompt += `【场景】\n${charData.scenario}\n\n`; } // 示例对话 if (charData.mes_example) { systemPrompt += `【示例对话】\n${charData.mes_example}\n\n`; } // 世界书/角色书条目 - 只包含启用的条目 if (charData.character_book?.entries?.length > 0) { const enabledEntries = charData.character_book.entries.filter(entry => entry.enabled !== false && entry.disable !== true ); if (enabledEntries.length > 0) { systemPrompt += `【世界观设定】\n`; enabledEntries.forEach(entry => { if (entry.content) { systemPrompt += `- ${entry.content}\n`; } }); systemPrompt += '\n'; } } // 选择的世界书条目 - 只包含启用的 const selectedLorebooks = settings.selectedLorebooks || []; const enabledLorebookEntries = []; selectedLorebooks.forEach(lb => { if (lb.enabled === false) return; // 整本世界书禁用 (lb.entries || []).forEach(entry => { if (entry.enabled !== false && entry.disable !== true && entry.content) { enabledLorebookEntries.push(entry.content); } }); }); if (enabledLorebookEntries.length > 0) { systemPrompt += `【世界书设定】\n`; enabledLorebookEntries.forEach(content => { systemPrompt += `- ${content}\n`; }); systemPrompt += '\n'; } // 添加微信对话格式提示 systemPrompt += `【回复格式】 你正在通过微信与用户聊天。请用简短、自然的口语化方式回复,就像真实的微信聊天一样。 - 你可以发送多条消息,每条消息之间用 ||| 分隔 - 每条消息不要太长,控制在1-2句话 - 可以使用表情符号 - 回复要符合角色性格 - 不要使用任何格式标记,直接输出对话内容 - 如果想发送语音消息,使用格式:[语音:语音内容] 示例(多条消息): 你在干嘛|||想你了|||今天工作好累啊 示例(包含语音): [语音:宝贝我想你了,今天怎么没给我发消息啊]|||你是不是把我忘了`; return systemPrompt; } // 构建消息历史 function buildMessages(contact, userMessage) { const systemPrompt = buildSystemPrompt(contact); const chatHistory = contact.chatHistory || []; const messages = [ { role: 'system', content: systemPrompt } ]; // 添加历史消息(最多保留300条) // 注意:调用此函数时,当前用户消息还未加入 chatHistory,所以不会重复 const recentHistory = chatHistory.slice(-300); recentHistory.forEach(msg => { messages.push({ role: msg.role === 'user' ? 'user' : 'assistant', content: msg.content }); }); // 添加当前用户的最新消息 messages.push({ role: 'user', content: userMessage }); return messages; } // 调用 AI API async function callAI(contact, userMessage) { const apiConfig = getApiConfig(); if (!apiConfig.url) { throw new Error('请先在设置中配置 API 地址'); } if (!apiConfig.model) { throw new Error('请先在设置中选择模型'); } const messages = buildMessages(contact, userMessage); const chatUrl = apiConfig.url.replace(/\/+$/, '') + '/chat/completions'; const headers = { 'Content-Type': 'application/json', }; if (apiConfig.key) { headers['Authorization'] = `Bearer ${apiConfig.key}`; } const response = await fetch(chatUrl, { method: 'POST', headers: headers, body: JSON.stringify({ model: apiConfig.model, messages: messages, temperature: 1, max_tokens: 8196 }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API 错误 (${response.status}): ${errorText.substring(0, 100)}`); } const data = await response.json(); return data.choices?.[0]?.message?.content || '...'; } // 发送消息(支持多条消息数组和语音消息) async function sendMessage(messageText, isMultipleMessages = false, isVoice = false) { if (currentChatIndex < 0) return; const settings = extension_settings[extensionName]; const contact = settings.contacts[currentChatIndex]; if (!contact) return; // 初始化聊天历史 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 messagesToSend = []; if (isMultipleMessages && Array.isArray(messageText)) { messagesToSend = messageText.filter(m => m.trim()); } else if (typeof messageText === 'string' && messageText.trim()) { messagesToSend = [messageText.trim()]; } if (messagesToSend.length === 0) return; // 清空输入框 const input = document.getElementById('wechat-input'); if (input) input.value = ''; // 从主界面提取时间,如果没有则使用系统时间 const extractedTime = extractTimeFromSTChat(); const msgTimestamp = extractedTime || Date.now(); // 先在界面上显示用户消息(但暂不加入历史) for (let i = 0; i < messagesToSend.length; i++) { const msg = messagesToSend[i]; appendMessage('user', msg, contact, isVoice); if (i < messagesToSend.length - 1) { await sleep(300); } } // 更新最后一条消息预览 contact.lastMessage = isVoice ? '[语音消息]' : messagesToSend[messagesToSend.length - 1]; // 显示打字中状态 showTypingIndicator(contact); try { // 调用 AI - 此时 chatHistory 还不包含当前用户消息,所以不会重复 const combinedMessage = isVoice ? `[用户发送了语音消息,内容是:${messagesToSend.join('\n')}]` : messagesToSend.join('\n'); const aiResponse = await callAI(contact, combinedMessage); // 隐藏打字中状态 hideTypingIndicator(); // AI 调用成功后,才把用户消息加入历史(使用提取的时间或系统时间) for (const msg of messagesToSend) { contact.chatHistory.push({ role: 'user', content: msg, time: timeStr, timestamp: msgTimestamp, isVoice: isVoice }); } // 解析 AI 回复(支持多条消息,用 ||| 分隔,支持语音格式 [语音:内容]) const aiMessages = aiResponse.split('|||').map(m => m.trim()).filter(m => m); // 依次显示 AI 的多条回复 for (let i = 0; i < aiMessages.length; i++) { let aiMsg = aiMessages[i]; let aiIsVoice = false; // 检查是否是语音消息格式 [语音:内容] 或 [语音:内容] const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/); if (voiceMatch) { aiMsg = voiceMatch[1]; aiIsVoice = true; } // 添加 AI 回复到历史 contact.chatHistory.push({ role: 'assistant', content: aiMsg, time: timeStr, timestamp: Date.now(), isVoice: aiIsVoice }); // 显示 AI 回复 appendMessage('assistant', aiMsg, contact, aiIsVoice); // 如果不是最后一条,显示打字中并添加延迟 if (i < aiMessages.length - 1) { showTypingIndicator(contact); await sleep(800 + Math.random() * 400); // 随机延迟 800-1200ms hideTypingIndicator(); } } // 更新最后一条消息预览 const lastAiMsg = aiMessages[aiMessages.length - 1]; const lastVoiceMatch = lastAiMsg.match(/^\[语音[::]\s*(.+?)\]$/); contact.lastMessage = lastVoiceMatch ? '[语音消息]' : lastAiMsg; saveSettingsDebounced(); refreshChatList(); // 刷新聊天列表显示最新消息 } catch (err) { hideTypingIndicator(); console.error('AI 调用失败:', err); // 即使失败,也要把用户消息加入历史(使用提取的时间或系统时间) for (const msg of messagesToSend) { contact.chatHistory.push({ role: 'user', content: msg, time: timeStr, timestamp: msgTimestamp, isVoice: isVoice }); } saveSettingsDebounced(); refreshChatList(); // 刷新聊天列表 // 显示错误消息 appendMessage('assistant', `⚠️ ${err.message}`, contact); } } // 睡眠函数 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 注入作者注释 function injectAuthorNote() { try { const context = getContext(); if (context && context.setExtensionPrompt) { context.setExtensionPrompt(extensionName, authorNoteTemplate, 1, 0); showToast('微信格式提示已注入'); } else { // 备用方案:尝试直接修改 const authorNoteTextarea = document.querySelector('#author_note_text'); if (authorNoteTextarea) { authorNoteTextarea.value = authorNoteTemplate; authorNoteTextarea.dispatchEvent(new Event('input')); showToast('微信格式提示已注入'); } else { showToast('无法找到作者注释区域', '⚠️'); console.log('作者注释模板:', authorNoteTemplate); } } } catch (err) { console.error('注入作者注释失败:', err); showToast('注入失败,请手动添加', '❌'); } } let phoneAutoCenteringBound = false; let phoneManuallyPositioned = false; // 用户是否手动拖拽过 function centerPhoneInViewport({ force = false } = {}) { const phone = document.getElementById('wechat-phone'); if (!phone) return; if (!force && phone.classList.contains('hidden')) return; // 如果用户手动拖拽过,不自动居中(除非是首次显示) const settings = extension_settings[extensionName]; if (phoneManuallyPositioned && settings.phonePosition && !force) { return; } // 如果有保存的位置,使用保存的位置 if (settings.phonePosition && !force) { phone.style.setProperty('left', `${settings.phonePosition.x}px`, 'important'); phone.style.setProperty('top', `${settings.phonePosition.y}px`, 'important'); phoneManuallyPositioned = true; return; } const viewport = window.visualViewport; const rawViewportWidth = viewport?.width ?? window.innerWidth; const rawViewportHeight = viewport?.height ?? window.innerHeight; const viewportWidth = rawViewportWidth >= 100 ? rawViewportWidth : window.innerWidth; const viewportHeight = rawViewportHeight >= 100 ? rawViewportHeight : window.innerHeight; const viewportLeft = viewport?.offsetLeft ?? 0; const viewportTop = viewport?.offsetTop ?? 0; const isCoarsePointer = window.matchMedia?.('(pointer: coarse)')?.matches ?? false; const maxWidth = isCoarsePointer ? 360 : 375; const maxHeight = isCoarsePointer ? 700 : 667; const margin = isCoarsePointer ? 8 : 12; const availableWidth = Math.max(0, Math.floor(viewportWidth - margin * 2)); const availableHeight = Math.max(0, Math.floor(viewportHeight - margin * 2)); const targetWidth = Math.min(maxWidth, availableWidth); const targetHeight = Math.min(maxHeight, availableHeight); if (targetWidth > 0) phone.style.setProperty('width', `${targetWidth}px`, 'important'); if (targetHeight > 0) phone.style.setProperty('height', `${targetHeight}px`, 'important'); phone.style.setProperty('max-width', 'none', 'important'); phone.style.setProperty('max-height', 'none', 'important'); const effectiveWidth = targetWidth > 0 ? targetWidth : phone.getBoundingClientRect().width; const effectiveHeight = targetHeight > 0 ? targetHeight : phone.getBoundingClientRect().height; const unclampedCenterX = viewportLeft + viewportWidth / 2; const unclampedCenterY = viewportTop + viewportHeight / 2; const minCenterX = viewportLeft + margin + effectiveWidth / 2; const maxCenterX = viewportLeft + viewportWidth - margin - effectiveWidth / 2; const minCenterY = viewportTop + margin + effectiveHeight / 2; const maxCenterY = viewportTop + viewportHeight - margin - effectiveHeight / 2; const centerX = Math.round(Math.min(Math.max(unclampedCenterX, minCenterX), maxCenterX)); const centerY = Math.round(Math.min(Math.max(unclampedCenterY, minCenterY), maxCenterY)); phone.style.setProperty('left', `${centerX}px`, 'important'); phone.style.setProperty('top', `${centerY}px`, 'important'); phone.style.setProperty('right', 'auto', 'important'); phone.style.setProperty('bottom', 'auto', 'important'); } // 设置手机拖拽功能 function setupPhoneDrag() { const phone = document.getElementById('wechat-phone'); if (!phone) return; let isDragging = false; let startX = 0; let startY = 0; let initialX = 0; let initialY = 0; // 拖拽手柄:状态栏区域 const statusbar = phone.querySelector('.wechat-statusbar'); if (!statusbar) return; // 添加拖拽提示样式 statusbar.style.cursor = 'grab'; statusbar.title = '拖拽移动手机位置'; const handleStart = (e) => { // 排除按钮点击 if (e.target.closest('button') || e.target.closest('a')) return; isDragging = true; statusbar.style.cursor = 'grabbing'; const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX; const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY; startX = clientX; startY = clientY; const rect = phone.getBoundingClientRect(); initialX = rect.left + rect.width / 2; initialY = rect.top + rect.height / 2; e.preventDefault(); }; const handleMove = (e) => { if (!isDragging) return; const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX; const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY; const deltaX = clientX - startX; const deltaY = clientY - startY; const newX = initialX + deltaX; const newY = initialY + deltaY; phone.style.setProperty('left', `${newX}px`, 'important'); phone.style.setProperty('top', `${newY}px`, 'important'); e.preventDefault(); }; const handleEnd = () => { if (!isDragging) return; isDragging = false; statusbar.style.cursor = 'grab'; phoneManuallyPositioned = true; // 保存位置到设置 const rect = phone.getBoundingClientRect(); const settings = extension_settings[extensionName]; settings.phonePosition = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; saveSettingsDebounced(); }; // 鼠标事件 statusbar.addEventListener('mousedown', handleStart); document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); // 触摸事件 statusbar.addEventListener('touchstart', handleStart, { passive: false }); document.addEventListener('touchmove', handleMove, { passive: false }); document.addEventListener('touchend', handleEnd); // 双击状态栏重置位置到中心 statusbar.addEventListener('dblclick', () => { phoneManuallyPositioned = false; const settings = extension_settings[extensionName]; delete settings.phonePosition; saveSettingsDebounced(); centerPhoneInViewport({ force: true }); }); } function setupPhoneAutoCentering() { if (phoneAutoCenteringBound) return; phoneAutoCenteringBound = true; let rafPending = false; const handler = () => { if (rafPending) return; rafPending = true; requestAnimationFrame(() => { rafPending = false; centerPhoneInViewport(); }); }; window.addEventListener('resize', handler); window.addEventListener('orientationchange', handler); if (window.visualViewport) { window.visualViewport.addEventListener('resize', handler); window.visualViewport.addEventListener('scroll', handler); } const phone = document.getElementById('wechat-phone'); phone?.addEventListener('focusin', () => { centerPhoneInViewport({ force: true }); setTimeout(() => centerPhoneInViewport({ force: true }), 250); if (document.activeElement?.id === 'wechat-input') { const messages = document.getElementById('wechat-chat-messages'); if (messages) messages.scrollTop = messages.scrollHeight; } }); phone?.addEventListener('focusout', () => { setTimeout(() => centerPhoneInViewport({ force: true }), 250); }); setTimeout(() => centerPhoneInViewport({ force: true }), 0); } // 切换手机显示 function togglePhone() { const phone = document.getElementById('wechat-phone'); const settings = extension_settings[extensionName]; phone.classList.toggle('hidden'); settings.phoneVisible = !phone.classList.contains('hidden'); saveSettingsDebounced(); // 更新时间 if (settings.phoneVisible) { document.querySelector('.wechat-statusbar-time').textContent = getCurrentTime(); centerPhoneInViewport(); setTimeout(() => centerPhoneInViewport({ force: true }), 150); } } // 切换深色模式 function toggleDarkMode() { const phone = document.getElementById('wechat-phone'); const toggle = document.getElementById('wechat-dark-toggle'); const settings = extension_settings[extensionName]; settings.darkMode = !settings.darkMode; phone.classList.toggle('wechat-dark', settings.darkMode); toggle.classList.toggle('on', settings.darkMode); saveSettingsDebounced(); } // 解析聊天消息中的微信格式 function parseWeChatMessage(text) { const patterns = [ { regex: /\[微信:\s*(.+?)\]/g, type: 'text' }, { regex: /\[语音:\s*(\d+)秒?\]/g, type: 'voice' }, { regex: /\[图片:\s*(.+?)\]/g, type: 'image' }, { regex: /\[表情:\s*(.+?)\]/g, type: 'emoji' }, { regex: /\[红包:\s*(.+?)\]/g, type: 'redpacket' }, { regex: /\[转账:\s*(.+?)\]/g, type: 'transfer' }, { regex: /\[撤回\]/g, type: 'recall' }, ]; const messages = []; let lastIndex = 0; let match; // 合并所有匹配 const allMatches = []; for (const pattern of patterns) { pattern.regex.lastIndex = 0; while ((match = pattern.regex.exec(text)) !== null) { allMatches.push({ index: match.index, length: match[0].length, type: pattern.type, content: match[1] || '' }); } } // 按位置排序 allMatches.sort((a, b) => a.index - b.index); return allMatches; } // 展开面板相关 let expandMode = null; // 'voice' 或 'multi' let expandMsgItems = ['']; // 显示展开面板 - 语音模式 function showExpandVoice() { expandMode = 'voice'; const panel = document.getElementById('wechat-expand-input'); const title = document.getElementById('wechat-expand-title'); const body = document.getElementById('wechat-expand-body'); title.textContent = '语音消息'; body.innerHTML = `
输入语音内容,系统会根据字数计算时长
预计时长: 0"
`; panel.classList.remove('hidden'); // 绑定输入事件更新时长 const textarea = document.getElementById('wechat-expand-voice-text'); textarea.addEventListener('input', updateExpandVoiceDuration); setTimeout(() => textarea.focus(), 50); } // 更新语音时长预览 function updateExpandVoiceDuration() { const textarea = document.getElementById('wechat-expand-voice-text'); const durationEl = document.getElementById('wechat-expand-voice-duration'); if (textarea && durationEl) { const content = textarea.value.trim(); const duration = content ? calculateVoiceDuration(content) : 0; durationEl.textContent = duration + '"'; } } // 显示展开面板 - 多条消息模式 function showExpandMulti() { expandMode = 'multi'; expandMsgItems = ['']; const panel = document.getElementById('wechat-expand-input'); const title = document.getElementById('wechat-expand-title'); title.textContent = '多条消息'; renderExpandMsgList(); panel.classList.remove('hidden'); // 聚焦第一个输入框 setTimeout(() => { const firstInput = document.querySelector('.wechat-expand-msg-input'); if (firstInput) firstInput.focus(); }, 50); } // 渲染多条消息列表 function renderExpandMsgList() { const body = document.getElementById('wechat-expand-body'); let html = '
'; expandMsgItems.forEach((msg, index) => { html += `
${index + 1} ${expandMsgItems.length > 1 ? `` : ''}
`; }); html += '
'; html += ''; body.innerHTML = html; // 绑定事件 document.querySelectorAll('.wechat-expand-msg-input').forEach(input => { input.addEventListener('input', (e) => { expandMsgItems[parseInt(e.target.dataset.index)] = e.target.value; }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addExpandMsgItem(); } }); }); document.querySelectorAll('.wechat-expand-msg-del').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index); expandMsgItems.splice(index, 1); renderExpandMsgList(); }); }); document.getElementById('wechat-expand-add-msg')?.addEventListener('click', addExpandMsgItem); } // 添加一条消息 function addExpandMsgItem() { expandMsgItems.push(''); renderExpandMsgList(); // 聚焦新输入框 setTimeout(() => { const inputs = document.querySelectorAll('.wechat-expand-msg-input'); const lastInput = inputs[inputs.length - 1]; if (lastInput) lastInput.focus(); }, 50); } // 关闭展开面板 function closeExpandPanel() { const panel = document.getElementById('wechat-expand-input'); panel.classList.add('hidden'); expandMode = null; } // 功能面板相关 let funcPanelPage = 0; function toggleFuncPanel() { const panel = document.getElementById('wechat-func-panel'); const expandPanel = document.getElementById('wechat-expand-input'); // 如果语音/多条消息面板打开,先关闭它 if (!expandPanel.classList.contains('hidden')) { expandPanel.classList.add('hidden'); expandMode = null; } panel.classList.toggle('hidden'); } function hideFuncPanel() { const panel = document.getElementById('wechat-func-panel'); panel.classList.add('hidden'); } function showFuncPanel() { const panel = document.getElementById('wechat-func-panel'); panel.classList.remove('hidden'); } function setFuncPanelPage(pageIndex) { funcPanelPage = pageIndex; const pages = document.getElementById('wechat-func-pages'); const dots = document.querySelectorAll('.wechat-func-dot'); if (pages) { pages.style.transform = `translateX(-${pageIndex * 100}%)`; } dots.forEach((dot, idx) => { dot.classList.toggle('active', idx === pageIndex); }); } function initFuncPanel() { const pages = document.getElementById('wechat-func-pages'); if (!pages) return; let startX = 0; let currentX = 0; let isDragging = false; // 开始拖拽(触摸/鼠标) const handleStart = (e) => { startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; currentX = startX; isDragging = true; pages.style.transition = 'none'; }; // 拖拽中(触摸/鼠标) const handleMove = (e) => { if (!isDragging) return; currentX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; }; // 结束拖拽(触摸/鼠标) const handleEnd = () => { if (!isDragging) return; isDragging = false; pages.style.transition = 'transform 0.3s ease'; const diff = startX - currentX; if (Math.abs(diff) > 50) { if (diff > 0 && funcPanelPage < 1) { setFuncPanelPage(1); } else if (diff < 0 && funcPanelPage > 0) { setFuncPanelPage(0); } } }; // 触摸事件 pages.addEventListener('touchstart', handleStart, { passive: true }); pages.addEventListener('touchmove', handleMove, { passive: true }); pages.addEventListener('touchend', handleEnd); // 鼠标事件(电脑端支持) pages.addEventListener('mousedown', (e) => { handleStart(e); e.preventDefault(); }); pages.addEventListener('mousemove', handleMove); pages.addEventListener('mouseup', handleEnd); pages.addEventListener('mouseleave', handleEnd); // 点击指示点切换页面 document.querySelectorAll('.wechat-func-dot').forEach(dot => { dot.addEventListener('click', () => { const page = parseInt(dot.dataset.page); setFuncPanelPage(page); }); }); // 功能项点击 document.querySelectorAll('.wechat-func-item').forEach(item => { item.addEventListener('click', () => { const func = item.dataset.func; handleFuncItemClick(func); }); }); } function handleFuncItemClick(func) { switch (func) { case 'voice': hideFuncPanel(); showExpandVoice(); break; case 'multi': hideFuncPanel(); showExpandMulti(); break; case 'photo': case 'camera': case 'videocall': case 'location': case 'redpacket': case 'gift': case 'transfer': case 'favorites': case 'contact': case 'file': case 'card': case 'music': // 暂时只提示功能开发中 showToast('该功能开发中...', '🚧'); break; } } // 发送展开面板的内容 function sendExpandContent() { if (expandMode === 'voice') { const textarea = document.getElementById('wechat-expand-voice-text'); const content = textarea?.value.trim(); if (!content) { showToast('请输入语音内容', '⚠️'); return; } closeExpandPanel(); sendMessage(content, false, true); } else if (expandMode === 'multi') { const validMessages = expandMsgItems.filter(m => m.trim()); if (validMessages.length === 0) { showToast('请至少输入一条消息', '⚠️'); return; } closeExpandPanel(); sendMessage(validMessages, true); } } // 绑定事件 function bindEvents() { // 添加按钮 - 显示下拉菜单 document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => { e.stopPropagation(); const dropdown = document.getElementById('wechat-dropdown-menu'); dropdown.classList.toggle('hidden'); }); // 点击其他地方关闭下拉菜单 document.getElementById('wechat-phone')?.addEventListener('click', (e) => { if (!e.target.closest('#wechat-add-btn') && !e.target.closest('#wechat-dropdown-menu')) { document.getElementById('wechat-dropdown-menu')?.classList.add('hidden'); } }); // 通讯录页面的添加按钮 - 直接进入添加朋友页面 document.getElementById('wechat-contacts-add-btn')?.addEventListener('click', () => { showPage('wechat-add-page'); }); // 下拉菜单 - 添加朋友 document.getElementById('wechat-menu-add-friend')?.addEventListener('click', () => { document.getElementById('wechat-dropdown-menu').classList.add('hidden'); showPage('wechat-add-page'); }); // 下拉菜单 - 其他选项(暂时只关闭菜单) document.getElementById('wechat-menu-group')?.addEventListener('click', () => { document.getElementById('wechat-dropdown-menu').classList.add('hidden'); }); document.getElementById('wechat-menu-scan')?.addEventListener('click', () => { document.getElementById('wechat-dropdown-menu').classList.add('hidden'); }); document.getElementById('wechat-menu-pay')?.addEventListener('click', () => { document.getElementById('wechat-dropdown-menu').classList.add('hidden'); }); // 返回按钮 document.getElementById('wechat-back-btn')?.addEventListener('click', () => { showPage('wechat-main-content'); }); document.getElementById('wechat-chat-back-btn')?.addEventListener('click', () => { currentChatIndex = -1; showPage('wechat-main-content'); refreshContactsList(); // 刷新列表显示最新消息 }); document.getElementById('wechat-settings-back-btn')?.addEventListener('click', () => { showPage('wechat-me-page'); }); document.getElementById('wechat-favorites-back-btn')?.addEventListener('click', () => { showPage('wechat-me-page'); }); // 导入 PNG document.getElementById('wechat-import-png')?.addEventListener('click', () => { document.getElementById('wechat-file-png').click(); }); // 导入 JSON document.getElementById('wechat-import-json')?.addEventListener('click', () => { document.getElementById('wechat-file-json').click(); }); // PNG 文件选择 document.getElementById('wechat-file-png')?.addEventListener('change', async function(e) { const file = e.target.files[0]; if (!file) return; try { const charData = await extractCharacterFromPNG(file); charData.file = file; // 直接添加联系人,不显示确认弹窗 if (addContact(charData)) { showToast('导入成功', '✅'); // 尝试导入到 SillyTavern(静默失败) try { await importCharacterToST(charData); } catch (err) { console.log('导入到酒馆失败(可忽略):', err.message); } showPage('wechat-main-content'); } } catch (err) { showToast(err.message, '❌'); } this.value = ''; }); // JSON 文件选择 document.getElementById('wechat-file-json')?.addEventListener('change', async function(e) { const file = e.target.files[0]; if (!file) return; try { const charData = await extractCharacterFromJSON(file); charData.file = file; // 直接添加联系人,不显示确认弹窗 if (addContact(charData)) { showToast('导入成功', '✅'); // 尝试导入到 SillyTavern(静默失败) try { await importCharacterToST(charData); } catch (err) { console.log('导入到酒馆失败(可忽略):', err.message); } showPage('wechat-main-content'); } } catch (err) { showToast(err.message, '❌'); } this.value = ''; }); // 深色模式切换 document.getElementById('wechat-dark-toggle')?.addEventListener('click', toggleDarkMode); // 聊天输入框发送消息 const chatInput = document.getElementById('wechat-input'); if (chatInput) { // 按回车发送 chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(chatInput.value); } }); } // 点击 + 按钮切换功能面板 document.querySelector('.wechat-chat-input-more')?.addEventListener('click', () => { toggleFuncPanel(); }); // 语音按钮 - 快捷方式直接打开语音输入 document.querySelector('.wechat-chat-input-voice')?.addEventListener('click', () => { hideFuncPanel(); showExpandVoice(); }); // 功能面板滑动和点击 initFuncPanel(); // 展开面板 - 关闭按钮 document.getElementById('wechat-expand-close')?.addEventListener('click', () => { closeExpandPanel(); }); // 展开面板 - 发送按钮 document.getElementById('wechat-expand-send')?.addEventListener('click', () => { sendExpandContent(); }); // 标签栏切换(处理所有标签栏,包括主页面和"我"页面) document.querySelectorAll('.wechat-tab').forEach(tab => { tab.addEventListener('click', function() { // 更新所有标签栏的状态 document.querySelectorAll('.wechat-tab').forEach(t => { if (t.dataset.tab === this.dataset.tab) { t.classList.add('active'); } else { t.classList.remove('active'); } }); const tabName = this.dataset.tab; if (tabName === 'me') { showPage('wechat-me-page'); } else if (tabName === 'chat') { showPage('wechat-main-content'); // 显示微信聊天列表,隐藏通讯录 document.getElementById('wechat-chat-tab-content')?.classList.remove('hidden'); document.getElementById('wechat-contacts-tab-content')?.classList.add('hidden'); // 刷新聊天列表 refreshChatList(); } else if (tabName === 'contacts') { showPage('wechat-main-content'); // 显示通讯录,隐藏微信聊天列表 document.getElementById('wechat-chat-tab-content')?.classList.add('hidden'); document.getElementById('wechat-contacts-tab-content')?.classList.remove('hidden'); } else { // 其他标签暂时也显示主页面 showPage('wechat-main-content'); } }); }); // 聊天列表项点击 - 进入聊天 document.getElementById('wechat-chat-list')?.addEventListener('click', (e) => { const chatItem = e.target.closest('.wechat-chat-item'); if (chatItem) { const contactId = chatItem.dataset.contactId; const index = parseInt(chatItem.dataset.index); if (contactId) { openChatByContactId(contactId, index); } } }); // "我"页面菜单 document.getElementById('wechat-menu-favorites')?.addEventListener('click', () => { showPage('wechat-favorites-page'); }); document.getElementById('wechat-menu-settings')?.addEventListener('click', () => { showPage('wechat-settings-page'); }); // 服务页面 document.getElementById('wechat-menu-service')?.addEventListener('click', () => { showPage('wechat-service-page'); }); document.getElementById('wechat-service-back-btn')?.addEventListener('click', () => { showPage('wechat-me-page'); }); // 服务页面 - 钱包点击切换滑出面板 document.getElementById('wechat-service-wallet')?.addEventListener('click', () => { const walletPanel = document.getElementById('wechat-wallet-panel'); const contextPanel = document.getElementById('wechat-context-panel'); // 关闭另一个面板 contextPanel?.classList.add('hidden'); // 切换当前面板 walletPanel?.classList.toggle('hidden'); }); // 服务页面 - 上下文设置点击切换滑出面板 document.getElementById('wechat-service-context')?.addEventListener('click', () => { const contextPanel = document.getElementById('wechat-context-panel'); const walletPanel = document.getElementById('wechat-wallet-panel'); // 关闭另一个面板 walletPanel?.classList.add('hidden'); // 切换当前面板 contextPanel?.classList.toggle('hidden'); }); // 上下文开关变化 document.getElementById('wechat-context-enabled')?.addEventListener('change', (e) => { const enabled = e.target.checked; const settings = extension_settings[extensionName]; settings.contextEnabled = enabled; saveSettingsDebounced(); // 更新显示 document.getElementById('wechat-context-level-display').textContent = enabled ? '已开启' : '已关闭'; // 切换设置区域状态 const settingsSection = document.getElementById('wechat-context-settings'); if (settingsSection) { settingsSection.style.opacity = enabled ? '1' : '0.5'; settingsSection.style.pointerEvents = enabled ? 'auto' : 'none'; } }); // 上下文滑块变化 document.getElementById('wechat-context-slider')?.addEventListener('input', (e) => { const value = e.target.value; const settings = extension_settings[extensionName]; settings.contextLevel = parseInt(value); saveSettingsDebounced(); // 更新显示 document.getElementById('wechat-context-value').textContent = value; }); // 标签容器事件委托(添加和删除) document.getElementById('wechat-context-tags')?.addEventListener('click', (e) => { // 删除标签 if (e.target.classList.contains('wechat-tag-del-btn')) { const index = parseInt(e.target.dataset.index); const settings = extension_settings[extensionName]; if (settings.contextTags && index >= 0 && index < settings.contextTags.length) { settings.contextTags.splice(index, 1); saveSettingsDebounced(); refreshContextTags(); } } // 添加标签 if (e.target.classList.contains('wechat-tag-add-btn')) { const tagName = prompt('输入标签名(如 content、scene):'); if (tagName && tagName.trim()) { const settings = extension_settings[extensionName]; if (!settings.contextTags) settings.contextTags = []; if (!settings.contextTags.includes(tagName.trim())) { settings.contextTags.push(tagName.trim()); saveSettingsDebounced(); refreshContextTags(); } } } }); // 钱包金额保存(滑出面板) document.getElementById('wechat-wallet-save-slide')?.addEventListener('click', () => { const input = document.getElementById('wechat-wallet-input-slide'); const amount = input?.value || '0.00'; const settings = extension_settings[extensionName]; settings.walletAmount = amount; saveSettingsDebounced(); // 更新显示 document.getElementById('wechat-wallet-amount').textContent = '¥' + amount; // 关闭面板 document.getElementById('wechat-wallet-panel')?.classList.add('hidden'); }); // 总结API配置 - 密码显示切换 document.getElementById('wechat-summary-key-toggle')?.addEventListener('click', () => { const input = document.getElementById('wechat-summary-key'); if (input) { input.type = input.type === 'password' ? 'text' : 'password'; } }); // 总结API配置 - 获取模型列表 document.getElementById('wechat-summary-fetch-models')?.addEventListener('click', async () => { const statusEl = document.getElementById('wechat-summary-status'); const urlInput = document.getElementById('wechat-summary-url'); const keyInput = document.getElementById('wechat-summary-key'); const modelSelect = document.getElementById('wechat-summary-model'); const url = urlInput?.value?.trim(); const key = keyInput?.value?.trim(); if (!url || !key) { if (statusEl) statusEl.textContent = '❌ 请先填写 URL 和 Key'; return; } if (statusEl) statusEl.textContent = '⏳ 正在获取模型列表...'; try { const modelsUrl = url.replace(/\/$/, '') + '/models'; const response = await fetch(modelsUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const models = (data.data || data || []) .map(m => m.id || m.name || m) .filter(m => typeof m === 'string') .sort(); if (models.length === 0) { if (statusEl) statusEl.textContent = '⚠️ 未找到可用模型'; return; } // 更新下拉列表 if (modelSelect) { modelSelect.innerHTML = '' + models.map(m => ``).join(''); } // 保存到设置 const settings = extension_settings[extensionName]; settings.summaryModelList = models; saveSettingsDebounced(); if (statusEl) statusEl.textContent = `✅ 获取到 ${models.length} 个模型`; } catch (err) { console.error('获取模型列表失败:', err); if (statusEl) statusEl.textContent = `❌ 获取失败: ${err.message}`; } }); // 总结API配置 - 测试连接 document.getElementById('wechat-summary-test')?.addEventListener('click', async () => { const statusEl = document.getElementById('wechat-summary-status'); const urlInput = document.getElementById('wechat-summary-url'); const keyInput = document.getElementById('wechat-summary-key'); const modelSelect = document.getElementById('wechat-summary-model'); const url = urlInput?.value?.trim(); const key = keyInput?.value?.trim(); const model = modelSelect?.value; if (!url || !key) { if (statusEl) statusEl.textContent = '❌ 请先填写 URL 和 Key'; return; } if (!model) { if (statusEl) statusEl.textContent = '❌ 请先选择模型'; return; } if (statusEl) statusEl.textContent = '⏳ 正在测试连接...'; try { const chatUrl = url.replace(/\/$/, '') + '/chat/completions'; const response = await fetch(chatUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model, messages: [{ role: 'user', content: 'Hi' }], max_tokens: 5 }) }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error?.message || `HTTP ${response.status}`); } if (statusEl) statusEl.textContent = '✅ 连接成功!'; } catch (err) { console.error('测试连接失败:', err); if (statusEl) statusEl.textContent = `❌ 连接失败: ${err.message}`; } }); // 总结API配置 - 保存配置 document.getElementById('wechat-summary-save')?.addEventListener('click', () => { const statusEl = document.getElementById('wechat-summary-status'); const urlInput = document.getElementById('wechat-summary-url'); const keyInput = document.getElementById('wechat-summary-key'); const modelSelect = document.getElementById('wechat-summary-model'); const settings = extension_settings[extensionName]; settings.summaryApiUrl = urlInput?.value?.trim() || ''; settings.summaryApiKey = keyInput?.value?.trim() || ''; settings.summarySelectedModel = modelSelect?.value || ''; saveSettingsDebounced(); if (statusEl) statusEl.textContent = '✅ 配置已保存'; // 2秒后关闭面板 setTimeout(() => { document.getElementById('wechat-summary-panel')?.classList.add('hidden'); }, 1500); }); // 总结API配置 - 模型选择变化 document.getElementById('wechat-summary-model')?.addEventListener('change', (e) => { const settings = extension_settings[extensionName]; settings.summarySelectedModel = e.target.value; saveSettingsDebounced(); }); // 总结API配置 - 执行总结 document.getElementById('wechat-summary-execute')?.addEventListener('click', () => { executeSummary(); }); // 总结API配置 - 回退总结 document.getElementById('wechat-summary-rollback')?.addEventListener('click', () => { rollbackSummary(); }); // 总结面板 - 关闭按钮 document.getElementById('wechat-summary-close')?.addEventListener('click', () => { document.getElementById('wechat-summary-panel')?.classList.add('hidden'); }); // 服务页面 - 服务项点击 document.querySelectorAll('.wechat-service-item').forEach(item => { item.addEventListener('click', () => { const service = item.dataset.service; // 总结功能 - 打开配置面板 if (service === 'summary') { const panel = document.getElementById('wechat-summary-panel'); if (panel) { // 关闭其他面板 document.getElementById('wechat-context-panel')?.classList.add('hidden'); document.getElementById('wechat-wallet-panel')?.classList.add('hidden'); // 切换当前面板 panel.classList.toggle('hidden'); } return; } // 其他功能暂未实现 showToast(`"${item.querySelector('span').textContent}" 功能开发中...`, '🚧'); }); }); // 收藏页面 - 添加世界书按钮 document.getElementById('wechat-favorites-add-btn')?.addEventListener('click', () => { showLorebookModal(); }); // 世界书选择弹窗取消 document.getElementById('wechat-lorebook-cancel')?.addEventListener('click', () => { document.getElementById('wechat-lorebook-modal').classList.add('hidden'); }); // 收藏页面标签切换 document.querySelectorAll('.wechat-favorites-tab').forEach(tab => { tab.addEventListener('click', function() { document.querySelectorAll('.wechat-favorites-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); refreshFavoritesList(this.dataset.tab); }); }); // 清空联系人 document.getElementById('wechat-clear-contacts')?.addEventListener('click', () => { if (confirm('确定要清空所有联系人吗?')) { extension_settings[extensionName].contacts = []; saveSettingsDebounced(); refreshContactsList(); showToast('已清空所有联系人'); } }); // 用户头像点击更换 document.getElementById('wechat-me-avatar')?.addEventListener('click', () => { document.getElementById('wechat-user-avatar-input')?.click(); }); // 用户头像文件选择 document.getElementById('wechat-user-avatar-input')?.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; try { const reader = new FileReader(); reader.onload = function(event) { const settings = extension_settings[extensionName]; settings.userAvatar = event.target.result; saveSettingsDebounced(); updateMePageInfo(); showToast('头像已更换'); }; reader.readAsDataURL(file); } catch (err) { console.error('更换头像失败:', err); showToast('更换头像失败: ' + err.message, '❌'); } e.target.value = ''; // 清空以便重复选择同一文件 }); // API 配置相关事件 // 切换密钥可见性 document.getElementById('wechat-toggle-key-visibility')?.addEventListener('click', () => { const keyInput = document.getElementById('wechat-api-key'); const eyeBtn = document.getElementById('wechat-toggle-key-visibility'); if (keyInput.type === 'password') { keyInput.type = 'text'; eyeBtn.innerHTML = ''; } else { keyInput.type = 'password'; eyeBtn.innerHTML = ''; } }); // 保存 API 配置 document.getElementById('wechat-save-api')?.addEventListener('click', () => { const apiUrl = document.getElementById('wechat-api-url').value.trim(); const apiKey = document.getElementById('wechat-api-key').value.trim(); const selectedModel = document.getElementById('wechat-model-select')?.value || ''; extension_settings[extensionName].apiUrl = apiUrl; extension_settings[extensionName].apiKey = apiKey; extension_settings[extensionName].selectedModel = selectedModel; saveSettingsDebounced(); showToast('API 配置已保存'); }); // 刷新模型列表 document.getElementById('wechat-refresh-models')?.addEventListener('click', () => { refreshModelSelect(); }); // 模型选择变化 document.getElementById('wechat-model-select')?.addEventListener('change', (e) => { extension_settings[extensionName].selectedModel = e.target.value; saveSettingsDebounced(); }); // 测试 API 连接 document.getElementById('wechat-test-api')?.addEventListener('click', async () => { const apiUrl = document.getElementById('wechat-api-url').value.trim(); const apiKey = document.getElementById('wechat-api-key').value.trim(); if (!apiUrl) { showToast('请先填写 API 地址', '⚠️'); return; } const testBtn = document.getElementById('wechat-test-api'); const originalText = testBtn.textContent; testBtn.textContent = '测试中...'; testBtn.disabled = true; try { const result = await testApiConnection(apiUrl, apiKey); if (result.success) { showToast('连接成功'); } else { showToast('连接失败:' + (result.message || '未知错误'), '❌'); } } catch (err) { showToast('连接失败:' + err.message, '❌'); } finally { testBtn.textContent = originalText; testBtn.disabled = false; } }); // 弹窗取消 document.getElementById('wechat-import-cancel')?.addEventListener('click', () => { document.getElementById('wechat-import-modal').classList.add('hidden'); pendingImport = null; }); // 弹窗确认 document.getElementById('wechat-import-confirm')?.addEventListener('click', async () => { if (pendingImport) { try { // 添加到联系人 if (addContact(pendingImport)) { // 尝试导入到 SillyTavern try { await importCharacterToST(pendingImport); showToast(`${pendingImport.name} 已添加`); } catch (err) { showToast(`${pendingImport.name} 已添加,导入酒馆失败`, '⚠️'); } } } catch (err) { showToast('添加失败:' + err.message, '❌'); } document.getElementById('wechat-import-modal').classList.add('hidden'); pendingImport = null; showPage('wechat-main-content'); } }); // 绑定联系人点击 bindContactsEvents(); } // 待导入的角色数据 let pendingImport = null; // 显示导入确认弹窗 function showImportModal(charData) { pendingImport = charData; const preview = document.getElementById('wechat-card-preview'); preview.innerHTML = `
${charData.avatar ? `` : charData.name.charAt(0)}
${charData.name}
${charData.description?.substring(0, 200) || '暂无简介'}
`; document.getElementById('wechat-import-modal').classList.remove('hidden'); } // 监听聊天消息更新 function setupMessageObserver() { const context = getContext(); if (!context) return; // 监听新消息 const chatContainer = document.getElementById('chat'); if (chatContainer) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1 && node.classList?.contains('mes')) { // 检查是否包含微信格式 const mesText = node.querySelector('.mes_text'); if (mesText) { const wechatMessages = parseWeChatMessage(mesText.textContent); if (wechatMessages.length > 0) { // 可以在这里添加微信消息的特殊显示 console.log('检测到微信格式消息:', wechatMessages); } } } }); }); }); observer.observe(chatContainer, { childList: true, subtree: true }); } } // 添加扩展按钮到酒馆魔法棒菜单 function addExtensionButton() { // 添加到扩展菜单 (extensionsMenu) const extensionsMenu = document.getElementById('extensionsMenu'); if (extensionsMenu && !document.getElementById('wechat-extension-menu-item')) { const menuItem = document.createElement('div'); menuItem.id = 'wechat-extension-menu-item'; menuItem.className = 'list-group-item flex-container flexGap5'; menuItem.innerHTML = ` 可乐 `; menuItem.style.cursor = 'pointer'; menuItem.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); togglePhone(); // 关闭扩展菜单 const menu = document.getElementById('extensionsMenu'); if (menu) menu.style.display = 'none'; }); extensionsMenu.appendChild(menuItem); } } // 初始化插件 jQuery(async () => { loadSettings(); // 添加 HTML 到页面 const phoneHTML = generatePhoneHTML(); $('body').append(phoneHTML); setupPhoneAutoCentering(); setupPhoneDrag(); // 绑定事件 bindEvents(); // 恢复模型列表 restoreModelSelect(); // 设置消息监听 setupMessageObserver(); // 添加扩展按钮到酒馆魔法棒菜单 addExtensionButton(); // 更新时间 setInterval(() => { const timeEl = document.querySelector('.wechat-statusbar-time'); if (timeEl && !document.getElementById('wechat-phone').classList.contains('hidden')) { timeEl.textContent = getCurrentTime(); } }, 60000); console.log('✅ 可乐不加冰 v1.0.0 已加载'); });