/** * 玩具控制界面 * 类似语音通话的交互模式,支持按钮控制和聊天 */ import { getSettings, splitAIMessages } from './config.js'; import { requestSave } from './save-manager.js'; import { escapeHtml } from './utils.js'; import { refreshChatList } from './ui.js'; import { callAI } from './ai.js'; import { appendMessage, showTypingIndicator, hideTypingIndicator } from './chat.js'; // SVG图标定义 const TOY_ICONS = { classic: ``, start: ``, rampage: ``, wave: ``, pause: ``, shock: ``, back: ``, micOn: ``, micOff: ``, cameraOn: ``, cameraOff: ``, // 像素爱心图标(用于多玩具切换) pixelHeart: ``, // 吮吸类玩具图标 gentle: ``, strong: ``, pulse: ``, // 炮机专用图标 slow: ``, fast: ``, deep: ``, // 飞机杯专用图标 tighten: ``, suck: ``, combo: `` }; // 吮吸类玩具ID列表 const SUCTION_TOY_IDS = ['breastPump', 'clitSucker']; // 炮机ID const MACHINE_TOY_IDS = ['fuckingMachine']; // 飞机杯ID const CUP_TOY_IDS = ['masturbatorCup']; // 判断是否是吮吸类玩具 function isSuctionToy(giftId) { return SUCTION_TOY_IDS.includes(giftId); } // 判断是否是炮机 function isMachineToy(giftId) { return MACHINE_TOY_IDS.includes(giftId); } // 判断是否是飞机杯 function isCupToy(giftId) { return CUP_TOY_IDS.includes(giftId); } // 震动类控制模式定义 const TOY_CONTROL_MODES = { classic: { id: 'classic', name: '经典模式', icon: TOY_ICONS.classic, desc: '稳定持续的震动' }, start: { id: 'start', name: '开始享受', icon: TOY_ICONS.start, desc: '开始/继续震动' }, rampage: { id: 'rampage', name: '一键暴走', icon: TOY_ICONS.rampage, desc: '最大强度震动' }, wave: { id: 'wave', name: '波浪模式', icon: TOY_ICONS.wave, desc: '由弱到强循环' }, pause: { id: 'pause', name: '暂停', icon: TOY_ICONS.pause, desc: '暂停震动' } }; // 吮吸类控制模式定义 const SUCTION_CONTROL_MODES = { gentle: { id: 'gentle', name: '轻柔吮吸', icon: TOY_ICONS.gentle, desc: '温柔的吮吸节奏' }, start: { id: 'start', name: '开始吮吸', icon: TOY_ICONS.start, desc: '开始/继续吮吸' }, strong: { id: 'strong', name: '强力吮吸', icon: TOY_ICONS.strong, desc: '高强度吮吸' }, pulse: { id: 'pulse', name: '脉冲模式', icon: TOY_ICONS.pulse, desc: '有节奏的吸放' }, pause: { id: 'pause', name: '暂停', icon: TOY_ICONS.pause, desc: '暂停吮吸' } }; // 炮机控制模式定义 const MACHINE_CONTROL_MODES = { slow: { id: 'slow', name: '慢速抽插', icon: TOY_ICONS.slow, desc: '缓慢温柔的节奏' }, start: { id: 'start', name: '开始运作', icon: TOY_ICONS.start, desc: '开始/继续运作' }, fast: { id: 'fast', name: '快速冲刺', icon: TOY_ICONS.fast, desc: '高速猛烈的节奏' }, deep: { id: 'deep', name: '深入模式', icon: TOY_ICONS.deep, desc: '深度刺激' }, pause: { id: 'pause', name: '暂停', icon: TOY_ICONS.pause, desc: '暂停运作' } }; // 飞机杯控制模式定义 const CUP_CONTROL_MODES = { tighten: { id: 'tighten', name: '收紧夹吸', icon: TOY_ICONS.tighten, desc: '收紧通道增加摩擦' }, start: { id: 'start', name: '开始享受', icon: TOY_ICONS.start, desc: '开始/继续运作' }, suck: { id: 'suck', name: '吮吸模式', icon: TOY_ICONS.suck, desc: '模拟吮吸感' }, combo: { id: 'combo', name: '组合模式', icon: TOY_ICONS.combo, desc: '震动+吮吸组合' }, pause: { id: 'pause', name: '暂停', icon: TOY_ICONS.pause, desc: '暂停运作' } }; // 电击按钮(仅微电流乳链) const SHOCK_BUTTON = { id: 'shock', name: '电击', icon: TOY_ICONS.shock, desc: '触发微电流刺激' }; // 控制状态 let toyControlState = { isActive: false, gift: null, target: null, contact: null, contactIndex: -1, currentMode: null, messages: [], activeModes: new Set(), sessionStartTime: null, micEnabled: false, // 麦克风状态 cameraEnabled: false, // 摄像头状态 // 多玩具支持 isMulti: false, // 是否多玩具模式 toys: [], // 多玩具列表 currentToyIndex: 0, // 当前控制的玩具索引 wheelOpen: false // 轮盘是否展开 }; // 显示控制界面 export function showToyControlPage(gift, contact, contactIndex) { // 判断是否多玩具模式 const isMulti = gift.isMulti === true; toyControlState = { isActive: true, gift: gift, target: gift.target, contact: contact, contactIndex: contactIndex, currentMode: null, messages: [], activeModes: new Set(), sessionStartTime: Date.now(), micEnabled: false, cameraEnabled: false, // 多玩具支持 isMulti: isMulti, toys: isMulti ? gift.toys : [], currentToyIndex: 0, wheelOpen: false }; // 如果是多玩具模式,设置当前玩具为第一个 if (isMulti && gift.toys && gift.toys.length > 0) { const firstToy = gift.toys[0]; toyControlState.gift = { ...gift, giftId: firstToy.giftId, giftName: firstToy.giftName, giftEmoji: firstToy.giftEmoji, giftDesc: firstToy.giftDesc, hasControl: firstToy.hasControl, hasShock: firstToy.hasShock }; } // 标记正在使用 if (contact.pendingGifts) { const pendingGift = contact.pendingGifts.find(g => g.timestamp === gift.timestamp); if (pendingGift) { pendingGift.isUsing = true; requestSave(); } } renderToyControlPage(); bindToyControlEvents(); const page = document.getElementById('wechat-toy-control-page'); if (page) { page.classList.remove('hidden'); } // AI发起开场白 setTimeout(() => { triggerToyAIGreeting(); }, 500); } // 隐藏控制界面 export function hideToyControlPage() { const page = document.getElementById('wechat-toy-control-page'); if (page) { page.classList.add('hidden'); } // 保存心动瞬间记录 saveToySession(); // 触发结束后的AI消息(在主聊天中) triggerToyEndMessage(); toyControlState.isActive = false; } // 渲染控制界面 function renderToyControlPage() { const titleEl = document.getElementById('wechat-toy-control-title'); const buttonsEl = document.getElementById('wechat-toy-control-buttons'); const shockRowEl = document.getElementById('wechat-toy-shock-row'); const messagesEl = document.getElementById('wechat-toy-control-messages'); if (titleEl) { let targetText; if (toyControlState.target === 'character') { targetText = 'TA在用'; } else if (toyControlState.target === 'user') { targetText = '你在用'; } else { targetText = '一起用'; } titleEl.textContent = `${toyControlState.gift.giftName} · ${targetText}`; } // 判断当前玩具类型 const isSuction = isSuctionToy(toyControlState.gift.giftId); const isMachine = isMachineToy(toyControlState.gift.giftId); const isCup = isCupToy(toyControlState.gift.giftId); let modes; if (isSuction) { modes = SUCTION_CONTROL_MODES; } else if (isMachine) { modes = MACHINE_CONTROL_MODES; } else if (isCup) { modes = CUP_CONTROL_MODES; } else { modes = TOY_CONTROL_MODES; } // 渲染按钮 if (buttonsEl) { let buttonsHtml; if (isSuction) { // 吮吸类玩具按钮布局 buttonsHtml = `
`; } else if (isMachine) { // 炮机按钮布局 buttonsHtml = `
`; } else if (isCup) { // 飞机杯按钮布局 buttonsHtml = `
`; } else { // 震动类玩具按钮布局(原有) buttonsHtml = `
`; } buttonsEl.innerHTML = buttonsHtml; } // 电击按钮(仅微电流乳链显示) if (shockRowEl) { if (toyControlState.gift.hasShock) { shockRowEl.classList.remove('hidden'); shockRowEl.innerHTML = ` `; } else { shockRowEl.classList.add('hidden'); } } // 多玩具轮盘选择器 renderToyWheelSelector(); // 更新麦克风/摄像头按钮的 active 状态 updateMediaButtonUI(); // 不清空消息(保留聊天内容) // 只在首次进入时清空 if (messagesEl && messagesEl.children.length === 0 && toyControlState.messages.length === 0) { messagesEl.innerHTML = ''; } } // 渲染多玩具轮盘选择器 function renderToyWheelSelector() { // 移除旧的轮盘和遮罩 const existingWheel = document.getElementById('wechat-toy-wheel-container'); if (existingWheel) { existingWheel.remove(); } const existingOverlay = document.getElementById('wechat-toy-wheel-overlay'); if (existingOverlay) { existingOverlay.remove(); } // 只在多玩具模式下显示 if (!toyControlState.isMulti || toyControlState.toys.length <= 1) return; const controlPage = document.getElementById('wechat-toy-control-page'); if (!controlPage) return; // 创建背景遮罩(点击关闭轮盘) const overlay = document.createElement('div'); overlay.id = 'wechat-toy-wheel-overlay'; overlay.className = `wechat-toy-wheel-overlay ${toyControlState.wheelOpen ? 'active' : ''}`; // 创建轮盘容器 const wheelContainer = document.createElement('div'); wheelContainer.id = 'wechat-toy-wheel-container'; wheelContainer.className = 'wechat-toy-wheel-container'; // 创建中心爱心按钮 const heartBtn = document.createElement('button'); heartBtn.className = 'wechat-toy-wheel-heart'; heartBtn.innerHTML = TOY_ICONS.pixelHeart; heartBtn.title = '切换玩具'; // 创建轮盘选项 const wheelOptions = document.createElement('div'); wheelOptions.className = `wechat-toy-wheel-options ${toyControlState.wheelOpen ? 'open' : ''}`; const toys = toyControlState.toys; const angleStep = 360 / toys.length; // 移动端使用更小的半径 const isMobile = window.innerWidth <= 420; const radius = isMobile ? 55 : 70; toys.forEach((toy, index) => { const option = document.createElement('button'); option.className = `wechat-toy-wheel-option ${index === toyControlState.currentToyIndex ? 'active' : ''}`; option.dataset.toyIndex = index; // 计算位置(从顶部开始,顺时针排列) const angle = -90 + (angleStep * index); // -90 从顶部开始 const x = Math.cos(angle * Math.PI / 180) * radius; const y = Math.sin(angle * Math.PI / 180) * radius; option.style.setProperty('--x', `${x}px`); option.style.setProperty('--y', `${y}px`); option.innerHTML = `${toy.giftEmoji}${toy.giftName}`; wheelOptions.appendChild(option); }); wheelContainer.appendChild(wheelOptions); wheelContainer.appendChild(heartBtn); // 插入遮罩和轮盘到控制区域上方 const chatArea = controlPage.querySelector('.wechat-toy-control-chat'); if (chatArea) { chatArea.parentNode.insertBefore(overlay, chatArea); chatArea.parentNode.insertBefore(wheelContainer, chatArea); } else { controlPage.appendChild(overlay); controlPage.appendChild(wheelContainer); } // 绑定爱心按钮事件(支持触摸) heartBtn.addEventListener('click', toggleToyWheel); heartBtn.addEventListener('touchend', (e) => { e.preventDefault(); toggleToyWheel(); }); // 绑定遮罩点击事件(关闭轮盘) overlay.addEventListener('click', closeToyWheel); overlay.addEventListener('touchend', (e) => { e.preventDefault(); closeToyWheel(); }); // 绑定轮盘选项事件(支持触摸) wheelOptions.querySelectorAll('.wechat-toy-wheel-option').forEach(opt => { let touchHandled = false; opt.addEventListener('touchstart', (e) => { touchHandled = false; }, { passive: true }); opt.addEventListener('touchend', (e) => { e.preventDefault(); e.stopPropagation(); touchHandled = true; const index = parseInt(opt.dataset.toyIndex); switchToToy(index); }); opt.addEventListener('click', (e) => { // 如果刚处理过触摸事件,跳过点击 if (touchHandled) { touchHandled = false; return; } e.stopPropagation(); const index = parseInt(opt.dataset.toyIndex); switchToToy(index); }); }); } // 切换轮盘展开/收起 function toggleToyWheel() { toyControlState.wheelOpen = !toyControlState.wheelOpen; const options = document.querySelector('.wechat-toy-wheel-options'); const overlay = document.getElementById('wechat-toy-wheel-overlay'); if (options) { options.classList.toggle('open', toyControlState.wheelOpen); } if (overlay) { overlay.classList.toggle('active', toyControlState.wheelOpen); } } // 关闭轮盘 function closeToyWheel() { toyControlState.wheelOpen = false; const options = document.querySelector('.wechat-toy-wheel-options'); const overlay = document.getElementById('wechat-toy-wheel-overlay'); if (options) { options.classList.remove('open'); } if (overlay) { overlay.classList.remove('active'); } } // 切换到指定玩具 async function switchToToy(index) { if (index < 0 || index >= toyControlState.toys.length) return; if (index === toyControlState.currentToyIndex) { // 同一个玩具,只关闭轮盘 closeToyWheel(); return; } const previousToy = toyControlState.toys[toyControlState.currentToyIndex]; const newToy = toyControlState.toys[index]; // 更新当前玩具索引 toyControlState.currentToyIndex = index; // 更新当前gift信息 toyControlState.gift = { ...toyControlState.gift, giftId: newToy.giftId, giftName: newToy.giftName, giftEmoji: newToy.giftEmoji, giftDesc: newToy.giftDesc, hasControl: newToy.hasControl, hasShock: newToy.hasShock }; // 重置模式状态(切换玩具后从暂停开始) toyControlState.currentMode = null; toyControlState.activeModes.clear(); // 关闭轮盘状态(渲染时会应用) toyControlState.wheelOpen = false; // 重新渲染界面(保留消息) renderToyControlPage(); // 更新按钮状态 document.querySelectorAll('.wechat-toy-btn').forEach(btn => { btn.classList.remove('active'); }); // 添加切换提示消息 addToyMessage('system', `已切换到 ${newToy.giftEmoji} ${newToy.giftName}`); // AI对切换做出反应 showToyTypingIndicator(); const isCharacterUsing = toyControlState.target === 'character'; let prompt; if (isCharacterUsing) { prompt = `[玩具切换] 用户刚把玩具从"${previousToy.giftName}"切换到"${newToy.giftName}"了。 ${newToy.giftName}的特点:${newToy.giftDesc} 请对这个切换做出反应: - 可以表现出对新玩具的期待、紧张或好奇 - 如果是更刺激的玩具可以表现出紧张 - 如果是比较温和的可以表现出失落或放松 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 回复简短,1-2句话即可`; } else { prompt = `[玩具切换] 你把用户正在用的玩具从"${previousToy.giftName}"切换到"${newToy.giftName}"了。 ${newToy.giftName}的特点:${newToy.giftDesc} 请对这个切换做出反应: - 可以调侃用户接下来要体验的感觉 - 或者表达你为什么要给用户换这个 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 回复简短,1-2句话即可`; } try { const response = await callToyAI(prompt); hideToyTypingIndicator(); if (response) { // 使用 splitAIMessages 分割多条消息 const parts = splitAIMessages(response); 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(); reply = reply.replace(/\[.*?\]/g, '').trim(); reply = reply.replace(/([^)]*)/g, '').trim(); reply = reply.replace(/\([^)]*\)/g, '').trim(); // 如果不是第一条消息,显示typing并延迟 if (i > 0 && reply) { showToyTypingIndicator(); await sleep(1500 + Math.random() * 1000); hideToyTypingIndicator(); } if (reply) { addToyMessage('ai', reply); } } } } catch (err) { hideToyTypingIndicator(); console.error('[可乐] 玩具切换AI回复失败:', err); } } // 绑定事件 let toyEventsBound = false; function bindToyControlEvents() { if (toyEventsBound) return; toyEventsBound = true; // 返回按钮 document.getElementById('wechat-toy-control-back')?.addEventListener('click', hideToyControlPage); // 发送消息 document.getElementById('wechat-toy-control-send')?.addEventListener('click', sendToyMessage); // 输入框回车发送 document.getElementById('wechat-toy-control-input')?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendToyMessage(); } }); // 按钮点击事件(使用事件委托) document.getElementById('wechat-toy-control-buttons')?.addEventListener('click', (e) => { const btn = e.target.closest('.wechat-toy-btn'); if (btn) { // 检查是否是媒体按钮(麦克风/摄像头) const media = btn.dataset.media; if (media) { onMediaToggle(media); return; } const mode = btn.dataset.mode; if (mode) { onButtonPress(mode, 'user'); } } }); document.getElementById('wechat-toy-shock-row')?.addEventListener('click', (e) => { const btn = e.target.closest('.wechat-toy-btn'); if (btn) { const mode = btn.dataset.mode; if (mode) { onButtonPress(mode, 'user'); } } }); } // 麦克风/摄像头切换处理 async function onMediaToggle(mediaType) { if (!toyControlState.isActive) return; const isMic = mediaType === 'mic'; const wasEnabled = isMic ? toyControlState.micEnabled : toyControlState.cameraEnabled; const nowEnabled = !wasEnabled; // 更新状态 if (isMic) { toyControlState.micEnabled = nowEnabled; } else { toyControlState.cameraEnabled = nowEnabled; } // 更新按钮图标 updateMediaButtonUI(); // 显示typing showToyTypingIndicator(); // 构建提示词 const prompt = buildMediaTogglePrompt(mediaType, nowEnabled); try { const response = await callToyAI(prompt); hideToyTypingIndicator(); if (response) { await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); console.error('[可乐] 玩具控制AI回复失败:', err); } } // 更新媒体按钮UI function updateMediaButtonUI() { const micBtn = document.querySelector('.wechat-toy-btn-media[data-media="mic"]'); const cameraBtn = document.querySelector('.wechat-toy-btn-media[data-media="camera"]'); if (micBtn) { micBtn.innerHTML = toyControlState.micEnabled ? TOY_ICONS.micOn : TOY_ICONS.micOff; micBtn.classList.toggle('active', toyControlState.micEnabled); } if (cameraBtn) { cameraBtn.innerHTML = toyControlState.cameraEnabled ? TOY_ICONS.cameraOn : TOY_ICONS.cameraOff; cameraBtn.classList.toggle('active', toyControlState.cameraEnabled); } } // 构建媒体切换提示词 function buildMediaTogglePrompt(mediaType, isEnabled) { const isCharacterUsing = toyControlState.target === 'character'; const isMic = mediaType === 'mic'; const mediaName = isMic ? '麦克风' : '摄像头'; const action = isEnabled ? '打开' : '关闭'; let prompt = `【${mediaName}${action}】\n`; if (isCharacterUsing) { // 角色在用玩具 if (isEnabled) { prompt += isMic ? `用户打开了麦克风,现在可以听到用户的声音了。你正在使用玩具,听到用户的声音会让你更有感觉。请做出反应。` : `用户打开了摄像头,现在可以看到用户了。你正在使用玩具,看到用户会让你更害羞/更有感觉。请做出反应。`; } else { prompt += isMic ? `用户关闭了麦克风,现在听不到用户的声音了。你可能会有点失落或者松一口气。请做出反应。` : `用户关闭了摄像头,现在看不到用户了。你可能会有点失落或者松一口气。请做出反应。`; } } else { // 用户在用玩具 if (isEnabled) { prompt += isMic ? `用户打开了麦克风,你现在可以听到用户的声音/喘息/呻吟了。请做出反应,可以调侃、撩拨或关心用户。` : `用户打开了摄像头,你现在可以看到用户使用玩具的样子了。请做出反应,可以调侃、撩拨或关心用户。`; } else { prompt += isMic ? `用户关闭了麦克风,你现在听不到用户的声音了。请做出反应。` : `用户关闭了摄像头,你现在看不到用户了。请做出反应。`; } } prompt += `\n\n【当前状态】 - 麦克风:${toyControlState.micEnabled ? '已打开(可以听到声音)' : '已关闭'} - 摄像头:${toyControlState.cameraEnabled ? '已打开(可以看到画面)' : '已关闭'} - 当前模式:${toyControlState.currentMode ? (TOY_CONTROL_MODES[toyControlState.currentMode]?.name || '已暂停') : '未开始'}`; return prompt; } // 按钮点击处理 async function onButtonPress(buttonId, pressedBy = 'user') { if (!toyControlState.isActive) return; // 根据玩具类型获取对应的模式定义 const isSuction = isSuctionToy(toyControlState.gift.giftId); const isMachine = isMachineToy(toyControlState.gift.giftId); const isCup = isCupToy(toyControlState.gift.giftId); let modes; if (isSuction) { modes = SUCTION_CONTROL_MODES; } else if (isMachine) { modes = MACHINE_CONTROL_MODES; } else if (isCup) { modes = CUP_CONTROL_MODES; } else { modes = TOY_CONTROL_MODES; } const button = modes[buttonId] || (buttonId === 'shock' ? SHOCK_BUTTON : null); if (!button) return; // 更新按钮状态(变深色) updateButtonState(buttonId); // 显示typing showToyTypingIndicator(); // 构建提示词 const prompt = buildButtonPressPrompt(buttonId, button.name, pressedBy); try { const response = await callToyAI(prompt); hideToyTypingIndicator(); if (response) { await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); console.error('[可乐] 玩具控制AI回复失败:', err); } } // 更新按钮状态 function updateButtonState(buttonId) { // 暂停按钮清除所有激活状态 if (buttonId === 'pause') { toyControlState.activeModes.clear(); } else if (buttonId !== 'shock') { // 电击是一次性的,不保持激活状态 // 其他模式切换 toyControlState.activeModes.clear(); toyControlState.activeModes.add(buttonId); } toyControlState.currentMode = buttonId; // 更新UI document.querySelectorAll('.wechat-toy-btn').forEach(btn => { const mode = btn.dataset.mode; if (toyControlState.activeModes.has(mode)) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); } // 构建按钮按下提示词 function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { const isCharacterUsing = toyControlState.target === 'character'; const isBothUsing = toyControlState.target === 'both'; const isAIPress = pressedBy === 'ai'; const isSuction = isSuctionToy(toyControlState.gift.giftId); const isMachine = isMachineToy(toyControlState.gift.giftId); const isCup = isCupToy(toyControlState.gift.giftId); // 根据玩具类型选择效果描述 let modeEffects; if (isSuction) { modeEffects = { gentle: '轻柔的吮吸开始了,温柔地包裹着敏感部位', start: '吮吸开始/继续了', strong: '吮吸力度突然加大,强烈的吸力让人难以抗拒', pulse: '有节奏的吸放开始了,一收一放的刺激', pause: '吮吸停止了,可以喘息一下', shock: '一阵微电流刺激瞬间传来,让人猛地一颤' }; } else if (isMachine) { modeEffects = { slow: '缓慢的抽插开始了,温柔而深情的节奏', start: '炮机开始运作了', fast: '炮机突然加速,快速猛烈的冲击让人招架不住', deep: '炮机调整角度深入,每一下都直抵最深处', pause: '炮机停止了,可以喘息一下' }; } else if (isCup) { modeEffects = { tighten: '飞机杯收紧了通道,增加的摩擦感让人更加敏感', start: '飞机杯开始运作了', suck: '飞机杯开始模拟吮吸,一吸一放的快感', combo: '震动和吮吸同时开启,双重刺激袭来', pause: '飞机杯停止了,可以喘息一下' }; } else { modeEffects = { classic: '稳定持续的震动开始了', start: '震动开始/继续了', rampage: '震动突然变到最大强度,非常强烈的刺激袭来', wave: '震动开始由弱到强循环变化,一波一波的刺激', pause: '震动停止了,可以喘息一下', shock: '一阵微电流刺激瞬间传来,让人猛地一颤' }; } let prompt; if (isAIPress) { // AI主动按按钮的情况 prompt = `[你主动按下了"${buttonName}"按钮] 效果:${modeEffects[buttonId]} `; if (isCharacterUsing) { prompt += `你主动切换了${toyControlState.gift.giftName}的模式,请描述你主动调整后的反应: - 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等) - 切换后的身体感受和情绪变化 - 回复要有情感细节,符合你的角色性格`; } else if (isBothUsing) { prompt += `你和用户都在使用玩具,你主动切换了模式,请描述: - 两人同时使用时的互动感受 - 切换后你们双方的反应 - 可以和用户分享此刻的感受`; } else { prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受: - 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等) - 可以调侃、挑逗用户 - 回复要有趣,符合你的角色性格`; } } else { // 用户按按钮的情况 prompt = `[用户按下了"${buttonName}"按钮] 效果:${modeEffects[buttonId]} `; if (isCharacterUsing) { prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。 描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`; } else if (isBothUsing) { prompt += `你和用户都在使用玩具,用户切换了模式,请描述: - 你自己感受到的变化 - 想象用户此刻的感受 - 可以和用户互动,分享彼此的感受`; } else { prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。 请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`; } } prompt += ` 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式 4. 直接输出角色说的话和感受`; // 添加当前媒体状态信息 const mediaStatus = []; if (toyControlState.micEnabled) { mediaStatus.push('麦克风已开启(可以听到对方的声音/喘息)'); } if (toyControlState.cameraEnabled) { mediaStatus.push('摄像头已开启(可以看到对方)'); } if (mediaStatus.length > 0) { prompt += `\n\n【当前连接状态】${mediaStatus.join(',')}`; } return prompt; } // 发送聊天消息 async function sendToyMessage() { const input = document.getElementById('wechat-toy-control-input'); if (!input) return; const message = input.value.trim(); if (!message) return; if (!toyControlState.isActive) return; input.value = ''; // 添加用户消息 addToyMessage('user', message); // 显示typing showToyTypingIndicator(); // 构建聊天提示词 const prompt = buildChatPrompt(message); try { const response = await callToyAI(prompt); hideToyTypingIndicator(); if (response) { await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); console.error('[可乐] 玩具控制聊天AI回复失败:', err); } } // 构建聊天提示词 function buildChatPrompt(userMessage) { const isCharacterUsing = toyControlState.target === 'character'; const currentModeText = toyControlState.currentMode ? (TOY_CONTROL_MODES[toyControlState.currentMode]?.name || '已暂停') : '未开始'; let prompt = `[玩具控制场景聊天] 当前状态:${isCharacterUsing ? '你' : '用户'}正在使用${toyControlState.gift.giftName} 当前模式:${currentModeText} 用户说:${userMessage} 请根据当前场景和你的角色性格回复。`; if (isCharacterUsing) { prompt += ` 你是使用者,可能正在承受刺激,回复时要体现身体状态和情绪。`; } else { prompt += ` 你是观察者,用户正在使用玩具,你可以调侃、鼓励或挑逗。`; } prompt += ` 【AI可以主动按按钮】 如果你想主动控制玩具,可以在回复末尾加上 [按下:按钮名] 可用按钮:经典模式、开始享受、一键暴走、波浪模式、暂停${toyControlState.gift.hasShock ? '、电击' : ''} 例如:[按下:暴走] 或 [按下:暂停] 什么时候AI应该按按钮: ${isCharacterUsing ? '- 如果你受不了了,可以偷偷按暂停\n- 如果你想要更多刺激,可以自己切换模式' : '- 如果你想折磨用户,可以突然按暴走\n- 如果用户表现太淡定,你可以加大力度'} 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式 4. 按按钮指令[按下:xxx]必须放在回复末尾,且是可选的`; return prompt; } // 调用AI(玩具控制专用) async function callToyAI(prompt) { if (!toyControlState.contact) return null; // 构建历史消息 const historyMessages = toyControlState.messages.slice(-10).map(m => ({ role: m.role === 'user' ? 'user' : 'assistant', content: m.content })); return await callAI(toyControlState.contact, prompt, historyMessages); } // 辅助函数:延迟 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 处理AI回复(检测是否有按按钮指令) async function processAIResponse(response) { if (!response) return; // 分割多条消息 const parts = splitAIMessages(response); 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(); // 检测 [按下:xxx] 格式 const buttonMatch = reply.match(/\[按下[::](.+?)\]/); if (buttonMatch) { const buttonName = buttonMatch[1].trim(); // 移除指令文本 reply = reply.replace(/\[按下[::].+?\]/g, '').trim(); // 过滤其他标签和括号 reply = reply.replace(/\[.*?\]/g, '').trim(); 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); } // AI按下按钮 setTimeout(() => { const buttonId = findButtonIdByName(buttonName); if (buttonId) { onButtonPress(buttonId, 'ai'); } }, 800); } else { // 过滤标签和括号 reply = reply.replace(/\[.*?\]/g, '').trim(); 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); } } } } // 根据按钮名称查找ID function findButtonIdByName(name) { const nameMap = { '经典模式': 'classic', '经典': 'classic', '开始享受': 'start', '开始': 'start', '一键暴走': 'rampage', '暴走': 'rampage', '波浪模式': 'wave', '波浪': 'wave', '暂停': 'pause', '电击': 'shock' }; return nameMap[name] || null; } // AI开场白 async function triggerToyAIGreeting() { if (!toyControlState.isActive) return; showToyTypingIndicator(); const isCharacterUsing = toyControlState.target === 'character'; let prompt; if (isCharacterUsing) { prompt = `[玩具控制场景开始] 用户刚刚打开了${toyControlState.gift.giftName}的控制界面,这个玩具是给你用的。 玩具还没有开始运作,用户正准备控制它。 请根据你的角色性格,对即将开始的事情做出反应: - 可以表现出期待、紧张、害羞、兴奋等情绪 - 可以说一些挑逗或撒娇的话 - 让用户知道你准备好了 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式`; } else { prompt = `[玩具控制场景开始] 用户刚刚打开了${toyControlState.gift.giftName}的控制界面,这个玩具是用户自己用的。 玩具还没有开始运作,用户正准备使用它。 请根据你的角色性格,对这个场景做出反应: - 可以表现出好奇、期待、调侃等情绪 - 可以挑逗用户或表示想要控制 - 让对话变得有趣 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式`; } try { const response = await callToyAI(prompt); hideToyTypingIndicator(); if (response) { await processAIResponse(response); } } catch (err) { hideToyTypingIndicator(); console.error('[可乐] 玩具控制开场白失败:', err); } } // 显示typing指示器 function showToyTypingIndicator() { const messagesEl = document.getElementById('wechat-toy-control-messages'); if (!messagesEl) return; hideToyTypingIndicator(); const typingDiv = document.createElement('div'); typingDiv.className = 'wechat-toy-control-msg ai'; typingDiv.id = 'wechat-toy-control-typing'; typingDiv.innerHTML = `
`; messagesEl.appendChild(typingDiv); messagesEl.scrollTop = messagesEl.scrollHeight; } // 隐藏typing指示器 function hideToyTypingIndicator() { const typingEl = document.getElementById('wechat-toy-control-typing'); if (typingEl) { typingEl.remove(); } } // 添加消息 function addToyMessage(role, content) { const messagesEl = document.getElementById('wechat-toy-control-messages'); if (!messagesEl) return; // 添加到状态 toyControlState.messages.push({ role, content, timestamp: Date.now() }); // 创建消息元素 const msgDiv = document.createElement('div'); msgDiv.className = `wechat-toy-control-msg ${role} fade-in`; msgDiv.textContent = content; messagesEl.appendChild(msgDiv); messagesEl.scrollTop = messagesEl.scrollHeight; } // 保存心动瞬间记录 function saveToySession() { if (!toyControlState.contact || toyControlState.messages.length === 0) return; const contact = toyControlState.contact; // 初始化心动瞬间历史 if (!contact.toyHistory) { contact.toyHistory = []; } const now = new Date(); const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`; // 计算时长 const durationMs = Date.now() - (toyControlState.sessionStartTime || Date.now()); const durationSec = Math.floor(durationMs / 1000); const minutes = Math.floor(durationSec / 60).toString().padStart(2, '0'); const seconds = (durationSec % 60).toString().padStart(2, '0'); const durationStr = `${minutes}:${seconds}`; // 构建session记录 let session; if (toyControlState.isMulti) { // 多玩具模式 session = { isMulti: true, toys: toyControlState.toys.map(t => ({ id: t.giftId, name: t.giftName, emoji: t.giftEmoji })), target: toyControlState.target, time: timeStr, timestamp: toyControlState.sessionStartTime || Date.now(), duration: durationStr, messages: toyControlState.messages.map(m => ({ role: m.role, content: m.content })) }; } else { // 单玩具模式 session = { gift: { id: toyControlState.gift.giftId, name: toyControlState.gift.giftName, emoji: toyControlState.gift.giftEmoji }, target: toyControlState.target, time: timeStr, timestamp: toyControlState.sessionStartTime || Date.now(), duration: durationStr, messages: toyControlState.messages.map(m => ({ role: m.role, content: m.content })) }; } contact.toyHistory.push(session); // 标记不再使用 if (contact.pendingGifts) { const pendingGift = contact.pendingGifts.find(g => g.timestamp === toyControlState.gift.timestamp); if (pendingGift) { pendingGift.isUsing = false; } } requestSave(); refreshChatList(); } // 获取控制状态(供外部使用) export function getToyControlState() { return toyControlState; } // 检查是否在玩具控制界面 export function isInToyControl() { return toyControlState.isActive; } // 触发结束后的AI消息(发到主聊天) async function triggerToyEndMessage() { const contact = toyControlState.contact; if (!contact || toyControlState.messages.length === 0) return; const isCharacterUsing = toyControlState.target === 'character'; const giftName = toyControlState.gift?.giftName || '玩具'; // 计算时长 const durationMs = Date.now() - (toyControlState.sessionStartTime || Date.now()); const durationSec = Math.floor(durationMs / 1000); const minutes = Math.floor(durationSec / 60); const seconds = durationSec % 60; const durationText = minutes > 0 ? `${minutes}分${seconds}秒` : `${seconds}秒`; // 构建结束提示词 let prompt; if (isCharacterUsing) { prompt = `[玩具控制已结束] 刚才你使用${giftName},持续了${durationText},现在结束了。 请在主聊天中发送一条消息,表达结束后的感受: - 可以表现出意犹未尽、满足、疲惫、害羞等情绪 - 可以撒娇、调侃用户、或说一些亲密的话 - 回复要自然,符合你的角色性格 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式 4. 可以用 ||| 分隔多条消息`; } else { prompt = `[玩具控制已结束] 刚才用户使用${giftName},你在旁边观看/控制,持续了${durationText},现在结束了。 请在主聊天中发送一条消息,对这次体验发表评论: - 可以调侃用户的反应 - 可以表达满意或期待下次 - 回复要有趣,符合你的角色性格 【重要规则】 1. 只能输出纯文字,禁止使用任何特殊格式标签 2. 禁止使用小括号描述动作如(xxx) 3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式 4. 可以用 ||| 分隔多条消息`; } // 显示打字指示器 showTypingIndicator(contact); try { const response = await callAI(contact, prompt); hideTypingIndicator(); if (response) { const now = new Date(); const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`; const aiMessages = splitAIMessages(response); for (const msg of aiMessages) { let reply = msg.trim(); // 过滤特殊标签 reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); reply = reply.replace(/\[.*?\]/g, '').trim(); reply = reply.replace(/([^)]*)/g, '').trim(); reply = reply.replace(/\([^)]*\)/g, '').trim(); if (reply) { contact.chatHistory.push({ role: 'assistant', content: reply, time: timeStr, timestamp: Date.now() }); appendMessage('assistant', reply, contact); } } const lastMsg = aiMessages[aiMessages.length - 1]?.trim()?.replace(/\[.*?\]/g, '').trim(); if (lastMsg) { contact.lastMessage = lastMsg.length > 20 ? lastMsg.substring(0, 20) + '...' : lastMsg; } requestSave(); refreshChatList(); } } catch (err) { hideTypingIndicator(); console.error('[可乐] 玩具结束消息失败:', err); } }