diff --git a/chat.js b/chat.js index 92e6e05..adca27b 100644 --- a/chat.js +++ b/chat.js @@ -155,35 +155,45 @@ async function handleBlockedExclamationClick(contact, exclamationEl) { await triggerAIAfterUnblock(contact); } -// 显示"已添加好友"的手机弹窗 +// 显示"已添加好友"的仿手机弹窗 function showFriendAddedPopup(name) { - // 创建弹窗遮罩 - const overlay = document.createElement('div'); - overlay.className = 'wechat-phone-popup-overlay'; - overlay.innerHTML = ` -
-
- - - - + // 获取手机容器 + const phoneContainer = document.querySelector('.wechat-phone'); + if (!phoneContainer) return; + + // 创建仿手机弹窗(使用与其他弹窗一致的 wechat-modal 样式) + const modal = document.createElement('div'); + modal.className = 'wechat-modal'; + modal.id = 'wechat-friend-added-modal'; + modal.innerHTML = ` +
+
添加好友成功
+
+
+ + + + +
+ ${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。 +
+
+
-
${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。
-
确定
`; - document.body.appendChild(overlay); + phoneContainer.appendChild(modal); // 点击确定关闭 - overlay.querySelector('.wechat-phone-popup-btn').addEventListener('click', () => { - overlay.remove(); + modal.querySelector('#wechat-friend-added-confirm').addEventListener('click', () => { + modal.remove(); }); // 点击遮罩也关闭 - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - overlay.remove(); + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); } }); } @@ -862,7 +872,7 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) { const isTimeout = callInfo === '对方已取消'; // 线条电话图标 - const phoneIconSVG = ` + const phoneIconSVG = ` `; @@ -871,16 +881,16 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) { // 已接通:显示通话时长 callRecordHTML = `
- 通话时长 ${callInfo} ${phoneIconSVG} + 通话时长 ${callInfo}
`; } else if (isCancelled) { // 用户发起未接通:已取消 callRecordHTML = `
- 已取消 ${phoneIconSVG} + 已取消
`; } else if (isRejected) { @@ -903,8 +913,8 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) { // 兜底:显示原始内容 callRecordHTML = `
- ${escapeHtml(callInfo)} ${phoneIconSVG} + ${escapeHtml(callInfo)}
`; } @@ -928,7 +938,7 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) { const isTimeout = callInfo === '对方已取消'; // 摄像机图标 - const cameraIconSVG = ` + const cameraIconSVG = ` `; diff --git a/config.js b/config.js index 39b7a28..4e53b8c 100644 --- a/config.js +++ b/config.js @@ -145,7 +145,7 @@ export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】 【核心要求 - 必须遵守】 1. 只能发送纯文字消息,像朋友之间真实聊天一样 2. 保持你的性格特点,用符合你角色设定的方式说话 -3. 每次回复请发送2-4条消息,用换行分隔,让对话更有层次感 +3. 每次回复1-3条消息即可,用换行分隔,不要刻意凑数量 4. 可以聊歌曲、聊心情、聊任何话题,自然就好 5. 发表对歌曲的看法时,要结合你的角色性格和经历 diff --git a/contacts.js b/contacts.js index 50a28b6..0aabc5c 100644 --- a/contacts.js +++ b/contacts.js @@ -14,6 +14,9 @@ let pendingAvatarContactIndex = -1; // 当前编辑的联系人索引 let currentEditingContactIndex = -1; +// 弹窗打开时间(用于防止点击穿透) +let contactSettingsOpenTime = 0; + // 添加联系人 export function addContact(characterData) { const settings = getSettings(); @@ -183,6 +186,9 @@ export function openContactSettings(contactIndex) { currentEditingContactIndex = contactIndex; + // 记录打开时间,用于防止点击穿透 + contactSettingsOpenTime = Date.now(); + // 填充头像和名称 const avatarPreview = document.getElementById('wechat-contact-avatar-preview'); const nameEl = document.getElementById('wechat-contact-settings-name'); @@ -258,6 +264,11 @@ export function openContactSettings(contactIndex) { export function saveContactSettings() { if (currentEditingContactIndex < 0) return; + // 防止点击穿透:如果弹窗刚打开(300ms内),忽略保存操作 + if (Date.now() - contactSettingsOpenTime < 300) { + return; + } + const settings = getSettings(); const contact = settings.contacts[currentEditingContactIndex]; if (!contact) return; diff --git a/emoji-panel.js b/emoji-panel.js index 5f586b5..5aee4b7 100644 --- a/emoji-panel.js +++ b/emoji-panel.js @@ -119,6 +119,26 @@ function getCatboxUrl(id, ext) { return `https://files.catbox.moe/${id}.${ext}`; } +// 生成唯一的表情名称(如果已存在同名则添加数字后缀) +function getUniqueStickerName(baseName, stickers) { + if (!stickers || stickers.length === 0) return baseName; + + // 检查是否已存在同名 + const existingNames = stickers.map(s => s.name); + if (!existingNames.includes(baseName)) { + return baseName; + } + + // 添加数字后缀直到找到唯一名称 + let counter = 1; + let newName = `${baseName}${counter}`; + while (existingNames.includes(newName)) { + counter++; + newName = `${baseName}${counter}`; + } + return newName; +} + // 切换表情面板显示/隐藏 export function toggleEmojiPanel() { const panel = document.getElementById('wechat-emoji-panel'); @@ -241,19 +261,22 @@ function addStickersFromInput(inputs) { } } - // 检查是否已存在 + // 检查是否已存在(按URL判断) const exists = settings.stickers.some(s => s.url === url); if (exists) { showToast(`已存在: ${name}`, 'info'); continue; } + // 生成唯一名称(如果同名则自动添加数字后缀) + const uniqueName = getUniqueStickerName(name, settings.stickers); + // 调试:显示添加的表情信息 - console.log('[可乐] 添加表情:', { name, url }); + console.log('[可乐] 添加表情:', { name: uniqueName, url }); settings.stickers.push({ url, - name, + name: uniqueName, addedTime: new Date().toISOString() }); addedCount++; @@ -287,9 +310,11 @@ function addStickerFromFile() { for (const file of files) { try { const dataUrl = await readFileAsDataURL(file); + // 生成唯一名称(如果同名则自动添加数字后缀) + const uniqueName = getUniqueStickerName(file.name, settings.stickers); settings.stickers.push({ url: dataUrl, - name: file.name, + name: uniqueName, addedTime: new Date().toISOString() }); addedCount++; diff --git a/floating-ball.js b/floating-ball.js new file mode 100644 index 0000000..2211a0c --- /dev/null +++ b/floating-ball.js @@ -0,0 +1,290 @@ +/** + * 悬浮球组件 + * 可爱猫咪悬浮窗,支持拖拽,点击打开主界面 + */ + +import { getSettings } from './config.js'; +import { requestSave } from './save-manager.js'; + +// 悬浮球状态 +let floatingBallState = { + isDragging: false, + startX: 0, + startY: 0, + initialX: 0, + initialY: 0, + currentX: 0, + currentY: 0, + hasMoved: false +}; + +// SVG 图标 - 渐变圆圈和猫咪 +const FLOATING_BALL_SVG = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +// 创建悬浮球 +export function createFloatingBall() { + // 检查是否已存在 + if (document.getElementById('wechat-floating-ball')) { + return; + } + + const ball = document.createElement('div'); + ball.id = 'wechat-floating-ball'; + ball.className = 'wechat-floating-ball'; + ball.innerHTML = FLOATING_BALL_SVG; + + document.body.appendChild(ball); + + // 恢复位置 + restorePosition(ball); + + // 绑定事件 + bindFloatingBallEvents(ball); + + // 根据主界面状态设置悬浮球可见性 + updateFloatingBallVisibility(); + + return ball; +} + +// 恢复保存的位置 +function restorePosition(ball) { + const settings = getSettings(); + const savedPos = settings.floatingBallPosition; + + if (savedPos && savedPos.x !== undefined && savedPos.y !== undefined) { + // 确保位置在视口内 + const maxX = window.innerWidth - 60; + const maxY = window.innerHeight - 60; + floatingBallState.currentX = Math.min(Math.max(0, savedPos.x), maxX); + floatingBallState.currentY = Math.min(Math.max(0, savedPos.y), maxY); + } else { + // 默认位置:右侧中间 + floatingBallState.currentX = window.innerWidth - 80; + floatingBallState.currentY = (window.innerHeight - 60) / 2; + } + + ball.style.left = floatingBallState.currentX + 'px'; + ball.style.top = floatingBallState.currentY + 'px'; +} + +// 保存位置 +function savePosition() { + const settings = getSettings(); + settings.floatingBallPosition = { + x: floatingBallState.currentX, + y: floatingBallState.currentY + }; + requestSave(); +} + +// 绑定事件 +function bindFloatingBallEvents(ball) { + // 鼠标事件 + ball.addEventListener('mousedown', onDragStart); + document.addEventListener('mousemove', onDragMove); + document.addEventListener('mouseup', onDragEnd); + + // 触摸事件 + ball.addEventListener('touchstart', onDragStart, { passive: false }); + document.addEventListener('touchmove', onDragMove, { passive: false }); + document.addEventListener('touchend', onDragEnd); + + // 窗口大小变化时调整位置 + window.addEventListener('resize', () => { + const maxX = window.innerWidth - 60; + const maxY = window.innerHeight - 60; + if (floatingBallState.currentX > maxX) { + floatingBallState.currentX = maxX; + ball.style.left = floatingBallState.currentX + 'px'; + } + if (floatingBallState.currentY > maxY) { + floatingBallState.currentY = maxY; + ball.style.top = floatingBallState.currentY + 'px'; + } + }); +} + +// 开始拖拽 +function onDragStart(e) { + const ball = document.getElementById('wechat-floating-ball'); + if (!ball) return; + + floatingBallState.isDragging = true; + floatingBallState.hasMoved = false; + + // 获取起始位置 + if (e.type === 'touchstart') { + floatingBallState.startX = e.touches[0].clientX; + floatingBallState.startY = e.touches[0].clientY; + e.preventDefault(); + } else { + floatingBallState.startX = e.clientX; + floatingBallState.startY = e.clientY; + } + + floatingBallState.initialX = floatingBallState.currentX; + floatingBallState.initialY = floatingBallState.currentY; + + ball.classList.add('dragging'); +} + +// 拖拽移动 +function onDragMove(e) { + if (!floatingBallState.isDragging) return; + + const ball = document.getElementById('wechat-floating-ball'); + if (!ball) return; + + let clientX, clientY; + if (e.type === 'touchmove') { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + e.preventDefault(); + } else { + clientX = e.clientX; + clientY = e.clientY; + } + + const deltaX = clientX - floatingBallState.startX; + const deltaY = clientY - floatingBallState.startY; + + // 如果移动距离超过5px,认为是拖拽而非点击 + if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { + floatingBallState.hasMoved = true; + } + + // 计算新位置 + let newX = floatingBallState.initialX + deltaX; + let newY = floatingBallState.initialY + deltaY; + + // 限制在视口内 + const maxX = window.innerWidth - 60; + const maxY = window.innerHeight - 60; + newX = Math.min(Math.max(0, newX), maxX); + newY = Math.min(Math.max(0, newY), maxY); + + floatingBallState.currentX = newX; + floatingBallState.currentY = newY; + + ball.style.left = newX + 'px'; + ball.style.top = newY + 'px'; +} + +// 结束拖拽 +function onDragEnd(e) { + if (!floatingBallState.isDragging) return; + + const ball = document.getElementById('wechat-floating-ball'); + if (ball) { + ball.classList.remove('dragging'); + } + + floatingBallState.isDragging = false; + + // 如果没有移动,视为点击 + if (!floatingBallState.hasMoved) { + toggleMainInterface(); + } else { + // 保存位置 + savePosition(); + } +} + +// 切换主界面显示 +function toggleMainInterface() { + const phone = document.getElementById('wechat-phone'); + if (!phone) return; + + const isHidden = phone.classList.contains('hidden'); + + if (isHidden) { + phone.classList.remove('hidden'); + } else { + phone.classList.add('hidden'); + } + + // 更新设置 + const settings = getSettings(); + settings.phoneVisible = isHidden; + requestSave(); + + // 更新悬浮球状态 + updateFloatingBallVisibility(); +} + +// 更新悬浮球可见性(主界面显示时隐藏悬浮球,反之显示) +export function updateFloatingBallVisibility() { + const ball = document.getElementById('wechat-floating-ball'); + const phone = document.getElementById('wechat-phone'); + + if (!ball) return; + + // 主界面隐藏时显示悬浮球,主界面显示时也显示悬浮球(方便用户随时关闭) + ball.style.display = 'flex'; +} + +// 显示悬浮球 +export function showFloatingBall() { + const ball = document.getElementById('wechat-floating-ball'); + if (ball) { + ball.style.display = 'flex'; + } +} + +// 隐藏悬浮球 +export function hideFloatingBall() { + const ball = document.getElementById('wechat-floating-ball'); + if (ball) { + ball.style.display = 'none'; + } +} + +// 销毁悬浮球 +export function destroyFloatingBall() { + const ball = document.getElementById('wechat-floating-ball'); + if (ball) { + ball.remove(); + } +} diff --git a/gift.js b/gift.js index 2d04f02..3246388 100644 --- a/gift.js +++ b/gift.js @@ -349,8 +349,8 @@ export function checkGiftDelivery(contact) { const currentCount = contact.chatHistory?.length || 0; for (const gift of contact.pendingGifts) { - // 如果正在使用中,跳过 - if (gift.isUsing) continue; + // 如果正在使用中或已完成,跳过 + if (gift.isUsing || gift.completed) continue; // 首次送达检测 if (!gift.isDelivered && currentCount >= gift.startMessageCount + 25) { @@ -415,6 +415,10 @@ export function showGiftArrivalModal(gift, contact) { yesBtn.removeEventListener('click', handleYes); noBtn.removeEventListener('click', handleNo); + // 标记礼物为已完成,防止重复触发弹窗 + gift.completed = true; + requestSave(); + // 打开玩具控制界面 const { showToyControlPage } = await import('./toy-control.js'); showToyControlPage(gift, contact, currentChatIndex); diff --git a/history-logs.js b/history-logs.js index a67b60d..f60748f 100644 --- a/history-logs.js +++ b/history-logs.js @@ -150,8 +150,8 @@ export function refreshHistoryList(filter = 'all') {
-
${escapeHtml(displayName)}
-
${entriesCount} 杯总结 · ${lb.lastUpdated || lb.addedTime || '未知时间'}
+
${escapeHtml(displayName)}
+
${entriesCount} 杯总结 · ${lb.lastUpdated || lb.addedTime || '未知时间'}
- ${targetText} + ${targetText}
@@ -358,16 +357,15 @@ export function renderToyHistory(contact) { ${escapeHtml(session.time || '未知时间')} 时长 ${escapeHtml(session.duration || '00:00')}
-
- ${previewMessages.length === 0 ? '
暂无对话记录
' : - previewMessages.map(msg => ` +
+ ${messages.length === 0 ? '
暂无对话记录
' : + messages.map(msg => `
${msg.role === 'user' ? '你' : 'TA'}: - ${escapeHtml((msg.content || '').substring(0, 50))}${(msg.content?.length || 0) > 50 ? '...' : ''} + ${escapeHtml(msg.content || '')}
`).join('') } - ${messages.length > 5 ? `
还有 ${messages.length - 5} 条消息...
` : ''}
`; diff --git a/listen-together.js b/listen-together.js index 141052b..b697955 100644 --- a/listen-together.js +++ b/listen-together.js @@ -143,6 +143,13 @@ function showListenTogetherPage() { const page = document.getElementById('wechat-listen-together-page'); if (!page) return; + // 清空上次的聊天消息 DOM + const messagesEl = document.getElementById('wechat-listen-messages'); + if (messagesEl) { + messagesEl.innerHTML = ''; + messagesEl.classList.add('hidden'); + } + const settings = getSettings(); const contact = listenState.contact; const song = listenState.currentSong; diff --git a/main.js b/main.js index 9542fda..e5933bc 100644 --- a/main.js +++ b/main.js @@ -15,7 +15,7 @@ import { ICON_SUCCESS, ICON_INFO } from './icons.js'; import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js'; import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js'; import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js'; -import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats } from './summary.js'; +import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats, recoverFromTavernWorldbook } from './summary.js'; import { fetchModelListFromApi } from './ai.js'; import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST } from './character-import.js'; @@ -36,6 +36,7 @@ import { initTransferEvents } from './transfer.js'; import { initGroupRedPacket } from './group-red-packet.js'; import { initGiftEvents } from './gift.js'; import { initCropper } from './cropper.js'; +import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js'; // ========== 历史记录功能 ========== let currentHistoryTab = 'listen'; @@ -125,6 +126,14 @@ function renderHistoryContent(contact, tabType) { deleteHistoryRecord('toy', index); }); }); + // 绑定标签内的叉叉按钮事件 + contentEl.querySelectorAll('.wechat-toy-target-close-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const index = parseInt(btn.dataset.index); + deleteHistoryRecord('toy', index); + }); + }); return; } @@ -331,7 +340,241 @@ function updateWalletAmountDisplay() { amountEl.textContent = amount.startsWith('¥') ? amount : `¥${amount}`; } +// ===== 缩小/恢复手机功能 ===== +let minimizeState = { + isDragging: false, + startX: 0, + startY: 0, + initialLeft: 0, + initialTop: 0, + hasMoved: false +}; + +// 悬浮窗开关 +function toggleFloatingBallEnabled() { + const settings = getSettings(); + const isEnabled = settings.floatingBallEnabled !== false; + + if (isEnabled) { + // 关闭悬浮窗 + settings.floatingBallEnabled = false; + hideFloatingBall(); + updateFloatingBallMenuText(false); + } else { + // 开启悬浮窗 + settings.floatingBallEnabled = true; + // 只有非缩小状态才显示 + const phone = document.getElementById('wechat-phone'); + if (!phone?.classList.contains('minimized')) { + showFloatingBall(); + } + updateFloatingBallMenuText(true); + } + + requestSave(); +} + +function updateFloatingBallMenuText(enabled) { + const textEl = document.getElementById('wechat-floating-ball-text'); + if (textEl) { + textEl.textContent = enabled ? '关闭悬浮窗' : '开启悬浮窗'; + } +} + +function setupPhoneMinimize() { + const phone = document.getElementById('wechat-phone'); + const minimizeBtn = document.getElementById('wechat-minimize-btn'); + + if (!phone || !minimizeBtn) return; + + // 点击右上角图标 - 缩小 (PC) + minimizeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + minimizePhone(); + }); + + // 移动端触摸支持 + let minimizeBtnTouchMoved = false; + minimizeBtn.addEventListener('touchstart', (e) => { + minimizeBtnTouchMoved = false; + }, { passive: true }); + + minimizeBtn.addEventListener('touchmove', (e) => { + minimizeBtnTouchMoved = true; + }, { passive: true }); + + minimizeBtn.addEventListener('touchend', (e) => { + if (!minimizeBtnTouchMoved) { + e.stopPropagation(); + e.preventDefault(); + minimizePhone(); + } + }); + + // 缩小后点击恢复 + 拖动支持 + phone.addEventListener('mousedown', onMinimizedDragStart); + document.addEventListener('mousemove', onMinimizedDragMove); + document.addEventListener('mouseup', onMinimizedDragEnd); + + // 触摸支持 + phone.addEventListener('touchstart', onMinimizedDragStart, { passive: false }); + document.addEventListener('touchmove', onMinimizedDragMove, { passive: false }); + document.addEventListener('touchend', onMinimizedDragEnd); +} + +function minimizePhone() { + const phone = document.getElementById('wechat-phone'); + if (!phone) return; + + // 获取当前位置 + const rect = phone.getBoundingClientRect(); + const settings = getSettings(); + + // 保存原始位置 + if (!settings.phoneOriginalPosition) { + settings.phoneOriginalPosition = { + left: phone.style.left || rect.left + 'px', + top: phone.style.top || rect.top + 'px' + }; + } + + // 缩小后移到右下角 + const scale = 0.25; + const phoneWidth = rect.width * scale; + const phoneHeight = rect.height * scale; + + // 使用保存的缩小位置或默认右下角 + const savedMinPos = settings.phoneMinimizedPosition; + let targetLeft, targetTop; + + if (savedMinPos) { + targetLeft = savedMinPos.left; + targetTop = savedMinPos.top; + } else { + targetLeft = window.innerWidth - phoneWidth - 20; + targetTop = window.innerHeight - phoneHeight - 20; + } + + phone.style.left = targetLeft + 'px'; + phone.style.top = targetTop + 'px'; + phone.style.right = 'auto'; + phone.style.bottom = 'auto'; + + phone.classList.add('minimized'); + + // 缩小时隐藏悬浮球 + hideFloatingBall(); + + requestSave(); +} + +function restorePhone() { + const phone = document.getElementById('wechat-phone'); + if (!phone) return; + + const settings = getSettings(); + + phone.classList.remove('minimized'); + + // 恢复原始位置或居中 + if (settings.phoneOriginalPosition) { + phone.style.left = settings.phoneOriginalPosition.left; + phone.style.top = settings.phoneOriginalPosition.top; + } + + // 恢复时根据设置显示悬浮球 + if (settings.floatingBallEnabled !== false) { + showFloatingBall(); + } + + requestSave(); +} + +function onMinimizedDragStart(e) { + const phone = document.getElementById('wechat-phone'); + if (!phone || !phone.classList.contains('minimized')) return; + + minimizeState.isDragging = true; + minimizeState.hasMoved = false; + + const rect = phone.getBoundingClientRect(); + // 缩小状态下需要考虑缩放后的实际位置 + minimizeState.initialLeft = parseFloat(phone.style.left) || rect.left; + minimizeState.initialTop = parseFloat(phone.style.top) || rect.top; + + if (e.type === 'touchstart') { + minimizeState.startX = e.touches[0].clientX; + minimizeState.startY = e.touches[0].clientY; + e.preventDefault(); + } else { + minimizeState.startX = e.clientX; + minimizeState.startY = e.clientY; + } + + phone.style.transition = 'none'; +} + +function onMinimizedDragMove(e) { + if (!minimizeState.isDragging) return; + + const phone = document.getElementById('wechat-phone'); + if (!phone || !phone.classList.contains('minimized')) return; + + let clientX, clientY; + if (e.type === 'touchmove') { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + e.preventDefault(); + } else { + clientX = e.clientX; + clientY = e.clientY; + } + + const deltaX = clientX - minimizeState.startX; + const deltaY = clientY - minimizeState.startY; + + if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { + minimizeState.hasMoved = true; + } + + const newLeft = minimizeState.initialLeft + deltaX; + const newTop = minimizeState.initialTop + deltaY; + + phone.style.left = newLeft + 'px'; + phone.style.top = newTop + 'px'; +} + +function onMinimizedDragEnd(e) { + if (!minimizeState.isDragging) return; + + const phone = document.getElementById('wechat-phone'); + minimizeState.isDragging = false; + + if (phone) { + phone.style.transition = ''; + } + + if (!minimizeState.hasMoved) { + // 没有移动,视为点击 - 恢复 + restorePhone(); + } else { + // 移动了,保存位置 + if (phone && phone.classList.contains('minimized')) { + const settings = getSettings(); + settings.phoneMinimizedPosition = { + left: parseFloat(phone.style.left), + top: parseFloat(phone.style.top) + }; + requestSave(); + } + } +} + function bindEvents() { + // ===== 缩小/恢复手机功能 ===== + setupPhoneMinimize(); + // 添加按钮 - 显示下拉菜单 document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => { e.stopPropagation(); @@ -369,6 +612,12 @@ function bindEvents() { }); }); + // 下拉菜单 - 悬浮窗开关 + document.getElementById('wechat-menu-floating-ball')?.addEventListener('click', () => { + document.getElementById('wechat-dropdown-menu')?.classList.add('hidden'); + toggleFloatingBallEnabled(); + }); + // ===== 群聊创建弹窗事件 ===== document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal); document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat); @@ -1330,6 +1579,9 @@ function bindEvents() { rollbackSummary(); }); + // 暴露恢复函数到全局,可在控制台调用: window.keleRecoverSummary() + window.keleRecoverSummary = recoverFromTavernWorldbook; + document.getElementById('wechat-summary-close')?.addEventListener('click', () => { document.getElementById('wechat-summary-panel')?.classList.add('hidden'); }); @@ -1933,6 +2185,14 @@ function init() { // 首次可见时居中 centerPhoneInViewport({ force: true }); + // 初始化悬浮球 + createFloatingBall(); + // 根据设置决定是否显示 + if (settings.floatingBallEnabled === false) { + hideFloatingBall(); + } + updateFloatingBallMenuText(settings.floatingBallEnabled !== false); + console.log('✅ 可乐不加冰 已加载'); } diff --git a/phone-html.js b/phone-html.js index a16d81d..059e675 100644 --- a/phone-html.js +++ b/phone-html.js @@ -19,7 +19,7 @@ export function generatePhoneHTML() {
${getCurrentTime()} -
+
@@ -114,6 +114,10 @@ export function generatePhoneHTML() { 收付款
+
+ + 悬浮窗 +
diff --git a/style.css b/style.css index 15cfcbf..2f4ca3f 100644 --- a/style.css +++ b/style.css @@ -4391,9 +4391,18 @@ box-sizing: border-box; } -/* 确保照片气泡不被其他样式折叠 */ +/* 确保照片气泡不被其他样式折叠,强制垂直排列 */ .wechat-message-content .wechat-photo-bubble { display: block !important; + flex-shrink: 0; + width: 180px !important; + float: none !important; +} + +/* 确保消息内容区域的子元素垂直排列 */ +.wechat-message-content { + flex-wrap: nowrap !important; + flex-direction: column !important; } .wechat-photo-blur { @@ -4737,7 +4746,7 @@ /* 通话中对话框 */ .wechat-voice-call-chat { - margin: 0 16px; + margin: 0 16px 15px 16px; background: transparent; padding: 10px 0; display: flex; @@ -4836,6 +4845,7 @@ background: rgba(50, 50, 50, 0.8); border-radius: 20px; padding: 4px 4px 4px 16px; + margin-top: auto; } .wechat-voice-call-input { @@ -6888,7 +6898,7 @@ .wechat-moment-images.grid-2 { grid-template-columns: repeat(2, 1fr); - max-width: 280px; + max-width: 200px; } .wechat-moment-images.grid-3, @@ -6974,6 +6984,19 @@ border-radius: 8px; } +/* 当朋友圈图片区域只包含照片描述卡片时,使用垂直布局 */ +.wechat-moment-images:has(.wechat-moment-photo-card) { + display: flex !important; + flex-direction: column !important; + gap: 8px; + max-width: 180px; +} + +.wechat-moment-images:has(.wechat-moment-photo-card) .wechat-moment-photo-card { + width: 100%; + max-width: 180px; +} + .wechat-moment-photo-card .wechat-photo-blur { position: absolute; top: 8px; @@ -11284,6 +11307,7 @@ /* 聊天区域 */ .wechat-toy-control-chat { flex: 1; + min-height: 0; overflow-y: auto; background: rgba(255, 255, 255, 0.6); margin: 0 10px; @@ -11291,6 +11315,25 @@ box-shadow: inset 0 2px 8px rgba(255, 107, 138, 0.1); } +/* 聊天区域滚动条样式 */ +.wechat-toy-control-chat::-webkit-scrollbar { + width: 6px; +} + +.wechat-toy-control-chat::-webkit-scrollbar-track { + background: rgba(255, 107, 138, 0.1); + border-radius: 3px; +} + +.wechat-toy-control-chat::-webkit-scrollbar-thumb { + background: rgba(255, 107, 138, 0.4); + border-radius: 3px; +} + +.wechat-toy-control-chat::-webkit-scrollbar-thumb:hover { + background: rgba(255, 107, 138, 0.6); +} + .wechat-toy-control-messages { padding: 15px; display: flex; @@ -11416,6 +11459,18 @@ background: rgba(0, 0, 0, 0.3); } +.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-track { + background: rgba(255, 107, 138, 0.05); +} + +.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-thumb { + background: rgba(255, 107, 138, 0.3); +} + +.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-thumb:hover { + background: rgba(255, 107, 138, 0.5); +} + .wechat-dark .wechat-toy-control-msg.ai { background: #2a2a2a; color: #e9e9e9; @@ -11500,6 +11555,25 @@ padding: 2px 8px; background: rgba(255, 255, 255, 0.2); border-radius: 10px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.wechat-toy-target-close-btn { + background: none; + border: none; + cursor: pointer; + font-size: 12px; + color: inherit; + padding: 0; + line-height: 1; + opacity: 0.7; + margin-left: 2px; +} + +.wechat-toy-target-close-btn:hover { + opacity: 1; } .wechat-toy-history-card-meta { @@ -11514,11 +11588,30 @@ .wechat-toy-history-card-messages { padding: 10px 14px; - max-height: 200px; + max-height: 300px; overflow-y: auto; background: rgba(255, 255, 255, 0.5); } +/* 心动瞬间消息滚动条样式 */ +.wechat-toy-history-scrollable::-webkit-scrollbar { + width: 6px; +} + +.wechat-toy-history-scrollable::-webkit-scrollbar-track { + background: rgba(255, 107, 138, 0.1); + border-radius: 3px; +} + +.wechat-toy-history-scrollable::-webkit-scrollbar-thumb { + background: rgba(255, 107, 138, 0.4); + border-radius: 3px; +} + +.wechat-toy-history-scrollable::-webkit-scrollbar-thumb:hover { + background: rgba(255, 107, 138, 0.6); +} + .wechat-toy-history-msg { margin-bottom: 8px; font-size: 13px; @@ -11567,3 +11660,117 @@ .wechat-dark .wechat-toy-history-msg-content { color: #ddd; } + +/* ===== 悬浮球样式 ===== */ +.wechat-floating-ball { + position: fixed; + width: 60px; + height: 60px; + z-index: 99999; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + user-select: none; + -webkit-user-select: none; + touch-action: none; + transition: transform 0.15s ease, opacity 0.15s ease; +} + +.wechat-floating-ball:hover { + transform: scale(1.08); +} + +.wechat-floating-ball:active, +.wechat-floating-ball.dragging { + transform: scale(0.95); + opacity: 0.9; +} + +.wechat-floating-ball .floating-ball-svg { + width: 100%; + height: 100%; + filter: drop-shadow(0 2px 8px rgba(255, 182, 193, 0.4)); + transition: filter 0.2s ease; +} + +.wechat-floating-ball:hover .floating-ball-svg { + filter: drop-shadow(0 4px 12px rgba(255, 182, 193, 0.6)); +} + +/* 悬浮球呼吸动画(可选,可删除) */ +@keyframes floating-ball-breathe { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.03); + } +} + +.wechat-floating-ball:not(:hover):not(.dragging) { + animation: floating-ball-breathe 3s ease-in-out infinite; +} + +/* ===== 缩小手机样式 ===== */ +#wechat-minimize-btn { + cursor: pointer; + padding: 4px 8px; + margin: -4px -8px; + border-radius: 4px; + transition: background 0.2s ease; +} + +#wechat-minimize-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +.wechat-dark #wechat-minimize-btn:hover { + background: rgba(255, 255, 255, 0.05); +} + +/* 缩小状态 */ +.wechat-phone.minimized { + transform: scale(0.25) !important; + transform-origin: center center; + position: fixed !important; + cursor: pointer; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s ease, + top 0.3s ease, + left 0.3s ease, + right 0.3s ease; + z-index: 99998; + border-radius: 20px; + overflow: hidden; + touch-action: none; +} + +.wechat-phone.minimized:hover { + transform: scale(0.27) !important; + box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); +} + +/* 缩小时禁用内部交互,但允许事件冒泡到父元素 */ +.wechat-phone.minimized::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 99999; + cursor: pointer; +} + +.wechat-phone.minimized * { + pointer-events: none !important; +} + +/* 缩小时的过渡动画 */ +.wechat-phone { + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s ease; +} diff --git a/summary.js b/summary.js index 28e509f..1f5e357 100644 --- a/summary.js +++ b/summary.js @@ -852,3 +852,106 @@ export async function rollbackSummary() { } } } + +/** + * 从酒馆世界书恢复总结数据 + * 当插件的 selectedLorebooks 条目丢失但酒馆世界书还在时使用 + */ +export async function recoverFromTavernWorldbook() { + const settings = getSettings(); + const selectedLorebooks = settings.selectedLorebooks || []; + + // 找到所有总结生成的世界书(条目为空的) + const emptyBooks = selectedLorebooks.filter(lb => + (lb.fromSummary === true || (lb.name && lb.name.startsWith(LOREBOOK_NAME_PREFIX))) && + (!lb.entries || lb.entries.length === 0) + ); + + if (emptyBooks.length === 0) { + alert('没有需要恢复的世界书(所有世界书都有条目,或没有总结类世界书)'); + return; + } + + const options = emptyBooks.map((lb, idx) => `${idx + 1}. ${lb.name}`).join('\n'); + const choice = prompt(`以下世界书条目为空,可尝试从酒馆恢复:\n\n${options}\n\n输入序号(或输入 all 恢复全部):`); + + if (!choice) return; + + const booksToRecover = choice.toLowerCase() === 'all' + ? emptyBooks + : [emptyBooks[parseInt(choice) - 1]].filter(Boolean); + + if (booksToRecover.length === 0) { + alert('无效的选择'); + return; + } + + let recoveredCount = 0; + let totalEntries = 0; + + for (const book of booksToRecover) { + try { + const name = book.name; + + // 检查酒馆世界书是否存在 + const worldExists = typeof world_names !== 'undefined' && + Array.isArray(world_names) && + world_names.includes(name); + + if (!worldExists) { + console.log(`[可乐] 酒馆中不存在世界书: ${name}`); + continue; + } + + // 加载酒馆世界书 + if (typeof loadWorldInfo !== 'function') { + console.error('[可乐] loadWorldInfo 函数不可用'); + continue; + } + + const worldInfo = await loadWorldInfo(name); + if (!worldInfo?.entries || Object.keys(worldInfo.entries).length === 0) { + console.log(`[可乐] 酒馆世界书 ${name} 没有条目`); + continue; + } + + // 将酒馆条目转换为插件格式 + const entries = []; + const sortedKeys = Object.keys(worldInfo.entries).sort((a, b) => parseInt(a) - parseInt(b)); + + for (const key of sortedKeys) { + const tavernEntry = worldInfo.entries[key]; + if (!tavernEntry) continue; + + entries.push({ + content: tavernEntry.content || '', + comment: tavernEntry.comment || getCupName(entries.length + 1), + keys: tavernEntry.key || [], + enabled: !tavernEntry.disable, + addedTime: new Date().toISOString() + }); + } + + if (entries.length > 0) { + // 更新插件的 selectedLorebooks + const bookIndex = selectedLorebooks.findIndex(lb => lb.name === name); + if (bookIndex >= 0) { + selectedLorebooks[bookIndex].entries = entries; + selectedLorebooks[bookIndex].lastUpdated = new Date().toISOString(); + recoveredCount++; + totalEntries += entries.length; + console.log(`[可乐] 已恢复 ${name}: ${entries.length} 条`); + } + } + } catch (err) { + console.error(`[可乐] 恢复 ${book.name} 失败:`, err); + } + } + + if (recoveredCount > 0) { + requestSave(); + alert(`恢复完成!\n\n已恢复 ${recoveredCount} 个世界书,共 ${totalEntries} 条总结。\n\n请刷新页面查看。`); + } else { + alert('恢复失败:酒馆中没有找到对应的世界书数据。\n\n数据可能已彻底丢失。'); + } +} diff --git a/toy-control.js b/toy-control.js index dd288ce..0585911 100644 --- a/toy-control.js +++ b/toy-control.js @@ -255,7 +255,7 @@ async function onButtonPress(buttonId, pressedBy = 'user') { hideToyTypingIndicator(); if (response) { - processAIResponse(response); + await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); @@ -290,7 +290,7 @@ function updateButtonState(buttonId) { // 构建按钮按下提示词 function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { const isCharacterUsing = toyControlState.target === 'character'; - const presserText = pressedBy === 'user' ? '用户' : '你自己'; + const isAIPress = pressedBy === 'ai'; const modeEffects = { classic: '稳定持续的震动开始了', @@ -301,18 +301,40 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { shock: '一阵微电流刺激瞬间传来,让人猛地一颤' }; - let prompt = `[${presserText}按下了"${buttonName}"按钮] + let prompt; + + if (isAIPress) { + // AI主动按按钮的情况 + prompt = `[你主动按下了"${buttonName}"按钮] 效果:${modeEffects[buttonId]} `; - - if (isCharacterUsing) { - prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。 -描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`; + if (isCharacterUsing) { + prompt += `你主动切换了${toyControlState.gift.giftName}的模式,请描述你主动调整后的反应: +- 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等) +- 切换后的身体感受和情绪变化 +- 回复要有情感细节,符合你的角色性格`; + } else { + prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受: +- 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等) +- 可以调侃、挑逗用户 +- 回复要有趣,符合你的角色性格`; + } } else { - prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。 + // 用户按按钮的情况 + prompt = `[用户按下了"${buttonName}"按钮] + +效果:${modeEffects[buttonId]} + +`; + if (isCharacterUsing) { + prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。 +描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`; + } else { + prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。 请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`; + } } prompt += ` @@ -351,7 +373,7 @@ async function sendToyMessage() { hideToyTypingIndicator(); if (response) { - processAIResponse(response); + await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); @@ -416,15 +438,20 @@ async function callToyAI(prompt) { return await callAI(toyControlState.contact, prompt, historyMessages); } +// 辅助函数:延迟 +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + // 处理AI回复(检测是否有按按钮指令) -function processAIResponse(response) { +async function processAIResponse(response) { if (!response) return; // 分割多条消息 const parts = splitAIMessages(response); - for (const part of parts) { - let reply = part.trim(); + for (let i = 0; i < parts.length; i++) { + let reply = parts[i].trim(); // 过滤特殊标签 reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); @@ -441,6 +468,13 @@ function processAIResponse(response) { reply = reply.replace(/([^)]*)/g, '').trim(); reply = reply.replace(/\([^)]*\)/g, '').trim(); + // 如果不是第一条消息,显示typing并延迟 + if (i > 0 && reply) { + showToyTypingIndicator(); + await sleep(1500 + Math.random() * 1000); // 1.5-2.5秒延迟 + hideToyTypingIndicator(); + } + // 添加AI消息 if (reply) { addToyMessage('ai', reply); @@ -459,6 +493,13 @@ function processAIResponse(response) { reply = reply.replace(/([^)]*)/g, '').trim(); reply = reply.replace(/\([^)]*\)/g, '').trim(); + // 如果不是第一条消息,显示typing并延迟 + if (i > 0 && reply) { + showToyTypingIndicator(); + await sleep(1500 + Math.random() * 1000); // 1.5-2.5秒延迟 + hideToyTypingIndicator(); + } + if (reply) { addToyMessage('ai', reply); } @@ -527,7 +568,7 @@ async function triggerToyAIGreeting() { hideToyTypingIndicator(); if (response) { - processAIResponse(response); + await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); diff --git a/video-call.js b/video-call.js index 38de636..9d3824a 100644 --- a/video-call.js +++ b/video-call.js @@ -351,6 +351,26 @@ export function checkVideoAIHangupAfterReply() { return false; } +// 检测AI是否有挂断意图 +function detectVideoHangupIntent(text) { + if (!text) return false; + const hangupPatterns = [ + /我(先)?挂了/, + /那我挂了/, + /先挂(了)?啊?/, + /挂了(啊|哈|呀|哦)?$/, + /我(要)?挂(电话|断)了/, + /拜拜.*挂/, + /挂.*拜拜/, + /再见.*挂/, + /不聊了.*挂/, + /不说了.*挂/, + /那就这样.*挂/, + /就这样吧.*挂/ + ]; + return hangupPatterns.some(pattern => pattern.test(text)); +} + // AI主动挂断视频电话 function videoAIHangup() { if (!videoCallState.isConnected) return; @@ -505,7 +525,7 @@ function appendVideoCallRecordMessage(role, status, duration, contact) { : firstChar); // 摄像机图标 - const cameraIconSVG = ` + const cameraIconSVG = ` `; @@ -1053,7 +1073,19 @@ async function sendVideoCallMessage() { } } - // AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效) + // AI回复完成后,检查是否要主动挂断 + // 1. 检测AI的挂断意图(如"我挂了"、"先挂了"等) + const fullReply = parts.join(' '); + if (detectVideoHangupIntent(fullReply)) { + console.log('[可乐] 检测到视频通话AI挂断意图:', fullReply); + setTimeout(() => { + if (videoCallState.isConnected) { + videoAIHangup(); + } + }, 1500 + Math.random() * 1000); + return; + } + // 2. 随机5%概率挂断(通话30秒后生效) checkVideoAIHangupAfterReply(); } catch (err) { hideVideoCallTypingIndicator(); diff --git a/voice-call.js b/voice-call.js index 84a62d8..47ad529 100644 --- a/voice-call.js +++ b/voice-call.js @@ -310,6 +310,27 @@ export function checkAIHangupAfterReply() { return false; } +// 检测AI是否有挂断意图 +function detectHangupIntent(text) { + if (!text) return false; + // 常见的挂断表达 + const hangupPatterns = [ + /我(先)?挂了/, + /那我挂了/, + /先挂(了)?啊?/, + /挂了(啊|哈|呀|哦)?$/, + /我(要)?挂(电话|断)了/, + /拜拜.*挂/, + /挂.*拜拜/, + /再见.*挂/, + /不聊了.*挂/, + /不说了.*挂/, + /那就这样.*挂/, + /就这样吧.*挂/ + ]; + return hangupPatterns.some(pattern => pattern.test(text)); +} + // AI主动挂断电话 function aiHangup() { if (!callState.isConnected) return; @@ -489,7 +510,7 @@ function appendCallRecordMessage(role, status, duration, contact) { // 通话记录卡片内容 // 线条电话图标 - const phoneIconSVG = ` + const phoneIconSVG = ` `; @@ -956,7 +977,19 @@ async function sendCallMessage() { } } - // AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效) + // AI回复完成后,检查是否要主动挂断 + // 1. 检测AI的挂断意图(如"我挂了"、"先挂了"等) + const fullReply = parts.join(' '); + if (detectHangupIntent(fullReply)) { + console.log('[可乐] 检测到AI挂断意图:', fullReply); + setTimeout(() => { + if (callState.isConnected) { + aiHangup(); + } + }, 1500 + Math.random() * 1000); + return; + } + // 2. 随机5%概率挂断(通话30秒后生效) checkAIHangupAfterReply(); } catch (err) { hideCallTypingIndicator();