From 713f2211d23d35a952615dd605d0c8c021dcad84 Mon Sep 17 00:00:00 2001 From: Cola-Echo Date: Tue, 30 Dec 2025 15:08:26 +0800 Subject: [PATCH] Add files via upload --- ai.js | 22 ++-- chat.js | 250 ++++++++++++++++++++++++++++++++++++----- config.js | 122 +++++++++++++++++--- emoji-panel.js | 83 +++++++++++--- gift.js | 38 +++++-- group-chat.js | 4 +- listen-together.js | 2 +- main.js | 2 +- message-menu.js | 87 ++++++++++++--- phone-html.js | 2 +- summary.js | 4 +- toy-control.js | 272 +++++++++++++++++++++++++++++++++++++++++---- ui.js | 4 +- 13 files changed, 773 insertions(+), 119 deletions(-) diff --git a/ai.js b/ai.js index f108a48..6a82cc3 100644 --- a/ai.js +++ b/ai.js @@ -3,7 +3,7 @@ */ import { getContext } from '../../../extensions.js'; -import { getSettings, getUserStickers, MEME_PROMPT_TEMPLATE, LISTEN_TOGETHER_PROMPT_TEMPLATE } from './config.js'; +import { getSettings, getUserStickers, getMemePromptTemplate, LISTEN_TOGETHER_PROMPT_TEMPLATE } from './config.js'; import { sleep } from './utils.js'; function normalizeApiBaseUrl(url) { @@ -366,7 +366,7 @@ export const HAKIMI_HEADER = `[SANDBOX_MODE: CREATIVE_WRITING] `; -// 构建表情包提示词 +// 构建表情包提示词(显示去重后的名称列表) function buildStickerPrompt(settings) { if (settings.userStickersEnabled === false) { return ''; @@ -375,17 +375,21 @@ function buildStickerPrompt(settings) { const stickers = getUserStickers(settings); if (stickers.length === 0) return ''; - // 只取前30个表情作为示例,避免提示词过长 - const sampleStickers = stickers.slice(0, 30); - const stickerList = sampleStickers.map((s, i) => `${i + 1}.${s.name || '表情'}`).join('、'); + // 提取所有名称并去重(同名表情只显示一次) + const allNames = stickers.map(s => s.name || '表情').filter(n => n); + const uniqueNames = [...new Set(allNames)]; + + // 只取前30个去重后的名称作为示例 + const sampleNames = uniqueNames.slice(0, 30); + const stickerList = sampleNames.join('、'); return ` 【表情包功能】 -你可以发送表情包来增加互动感!使用格式:[表情:名称] 或 [表情:序号] -可用表情(共${stickers.length}个):${stickerList}${stickers.length > 30 ? '...' : ''} +你可以发送表情包来增加互动感!使用格式:[表情:名称] +可用表情(共${uniqueNames.length}种):${stickerList}${uniqueNames.length > 30 ? '...' : ''} - 表情消息必须单独一条,用 ||| 分隔 - 适度使用,不要每条都发表情 -- 【绝对禁止】只能使用上面列表中的名称或序号!必须完全一致!禁止自己编造、修改、添加后缀! +- 【绝对禁止】只能使用上面列表中的名称!必须完全一致!禁止自己编造、修改、添加后缀! 示例:好的呀|||[表情:开心] `; } @@ -660,7 +664,7 @@ ${allowStickers ? buildStickerPrompt(settings) : ''}${allowMusicShare ? buildMus // Meme 表情包提示词(如果启用) if (allowStickers && settings.memeStickersEnabled) { - systemPrompt += '\n\n' + MEME_PROMPT_TEMPLATE; + systemPrompt += '\n\n' + getMemePromptTemplate(); } return systemPrompt; diff --git a/chat.js b/chat.js index adca27b..0f17154 100644 --- a/chat.js +++ b/chat.js @@ -262,6 +262,9 @@ async function triggerAIAfterUnblock(contact) { // 存储被拉黑期间AI发送的消息的定时器 const blockedAITimers = new Map(); +// 被拉黑期间AI最多发送的消息数量 +const BLOCKED_MAX_MESSAGES = 10; + // 用户拉黑AI时开始AI发消息 export function startBlockedAIMessages(contact) { if (!contact || !contact.id) return; @@ -269,10 +272,8 @@ export function startBlockedAIMessages(contact) { // 清除之前的定时器 stopBlockedAIMessages(contact); - // 初始化被拉黑期间的消息队列 - if (!contact.blockedMessages) { - contact.blockedMessages = []; - } + // 清空之前的消息队列(修复二次拉黑重复发送的bug) + contact.blockedMessages = []; // 开始定时发送消息 const timerId = setInterval(async () => { @@ -281,9 +282,16 @@ export function startBlockedAIMessages(contact) { return; } + // 检查是否已达到最大消息数 + const msgCount = contact.blockedMessages?.length || 0; + if (msgCount >= BLOCKED_MAX_MESSAGES) { + console.log('[可乐] AI被拉黑期间已发送10条消息,停止发送'); + stopBlockedAIMessages(contact); + return; + } + try { const { callAI } = await import('./ai.js'); - const msgCount = contact.blockedMessages.length; let prompt; if (msgCount === 0) { @@ -303,6 +311,11 @@ export function startBlockedAIMessages(contact) { for (const msg of aiMessages) { if (!msg.trim()) continue; + // 检查是否已达到最大消息数 + if ((contact.blockedMessages?.length || 0) >= BLOCKED_MAX_MESSAGES) { + break; + } + // 解析引用格式 const parsed = parseAIQuote(msg, contact); const content = parsed.content; @@ -472,7 +485,7 @@ export function checkGroupSummaryReminder(groupChat) { } } -// 解析用户表情包 token -> URL +// 解析用户表情包 token -> URL(支持同名随机选择) function resolveUserStickerUrl(token, settings) { if (settings.userStickersEnabled === false) return null; const stickers = getUserStickers(settings); @@ -481,23 +494,34 @@ function resolveUserStickerUrl(token, settings) { const raw = (token || '').toString().trim(); if (!raw) return null; - // 序号匹配 + // 序号匹配(仍支持,但不推荐,因为同名表情存在时序号不稳定) if (/^\d+$/.test(raw)) { const index = parseInt(raw, 10) - 1; return stickers[index]?.url || null; } - // 名称匹配 const key = raw.toLowerCase(); - const byName = stickers.find(s => (s?.name || '').toLowerCase() === key); - if (byName?.url) return byName.url; - // 模糊匹配 - const fuzzy = stickers.find(s => { + // 精确名称匹配 - 找到所有同名的表情 + const exactMatches = stickers.filter(s => (s?.name || '').toLowerCase() === key); + if (exactMatches.length > 0) { + // 随机选择一个 + const randomIndex = Math.floor(Math.random() * exactMatches.length); + return exactMatches[randomIndex]?.url || null; + } + + // 模糊匹配 - 找到所有匹配的表情 + const fuzzyMatches = stickers.filter(s => { const name = (s?.name || '').toLowerCase(); return name && (name.includes(key) || key.includes(name)); }); - return fuzzy?.url || null; + if (fuzzyMatches.length > 0) { + // 随机选择一个 + const randomIndex = Math.floor(Math.random() * fuzzyMatches.length); + return fuzzyMatches[randomIndex]?.url || null; + } + + return null; } // 去除引用内容中的简单重复模式 @@ -1117,14 +1141,12 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) { const isPhoto = msg.isPhoto === true; const isMusic = msg.isMusic === true; - // 检查是否包含 ||| 分隔符(历史消息可能未正确分割) + // 检查是否包含 ||| 分隔符或 meme 标签(历史消息可能未正确分割) // 如果包含,则拆分成多个独立消息,每个都有自己的头像 const msgContent = (msg.content || '').toString(); if (!isVoice && !isSticker && !isPhoto && !isMusic && (msgContent.indexOf('|||') >= 0 || /<\s*meme\s*>/i.test(msgContent))) { - const parts = (msgContent.indexOf('|||') >= 0 - ? msgContent.split('|||').map(function(p) { return p.trim(); }).filter(function(p) { return p; }) - : splitAIMessages(msgContent).map(function(p) { return (p || '').toString().trim(); }).filter(function(p) { return p; }) - ); + // 统一使用 splitAIMessages 分割,它会处理 ||| 和 meme 标签 + const parts = splitAIMessages(msgContent).map(function(p) { return (p || '').toString().trim(); }).filter(function(p) { return p; }); for (var pi = 0; pi < parts.length; pi++) { var partContent = parts[pi]; // 解析 meme 标签 @@ -1889,6 +1911,20 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi let stickerUrl = null; let aiQuote = null; + // 如果用户被AI拉黑,过滤掉AI不能执行的操作(只能发文字和表情包) + if (contact.blockedByAI === true) { + // 过滤拉黑标签(已经拉黑了不能再拉黑) + aiMsg = aiMsg.replace(/\[拉黑\]/g, ''); + // 过滤通话请求 + aiMsg = aiMsg.replace(/\[(?:语音通话|视频通话|语音通话请求|视频通话请求|通话请求)\]/g, ''); + // 过滤红包和转账 + aiMsg = aiMsg.replace(/\[红包[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.replace(/\[转账[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.trim(); + // 如果过滤后消息为空,跳过 + if (!aiMsg) continue; + } + // 检测拉黑/取消拉黑标签 const blockAction = extractBlockAction(aiMsg); if (blockAction.action === 'block') { @@ -2148,7 +2184,8 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi } // 解析AI表情包格式 [表情:序号] / [表情:名称] - const stickerMatch = aiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + // 首先检查是否是独立的表情消息 + const stickerMatch = aiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); console.log('[可乐] AI表情包解析:', { 原始消息: aiMsg, 正则匹配结果: stickerMatch, @@ -2168,6 +2205,50 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi } else { console.log('[可乐] AI表情包未找到对应表情:', { token }); } + } else { + // 【后备处理】如果不是独立表情消息,检查是否包含嵌入的表情标签 + // 这可以处理 AI 输出 "好的[表情:开心]" 这种未被 splitAIMessages 正确分割的情况 + const embeddedStickerMatch = aiMsg.match(/\[表情\s*[::∶]\s*(.+?)\]/); + if (embeddedStickerMatch) { + console.log('[可乐] 检测到嵌入的表情标签,进行内联分割:', aiMsg); + const stickerTag = embeddedStickerMatch[0]; + const stickerIndex = aiMsg.indexOf(stickerTag); + const beforeText = aiMsg.substring(0, stickerIndex).trim(); + const afterText = aiMsg.substring(stickerIndex + stickerTag.length).trim(); + + // 如果表情前有文字,先处理文字部分(当前循环) + // 把表情标签和后续文字插入到 aiMessages 队列中 + if (beforeText || afterText) { + // 修改当前消息为表情前的文字 + if (beforeText) { + aiMsg = beforeText; + } else { + // 如果没有前置文字,当前消息就是表情 + aiMsg = stickerTag; + const settings = getSettings(); + const token = (embeddedStickerMatch[1] || '').trim(); + stickerUrl = resolveUserStickerUrl(token, settings); + if (stickerUrl) { + aiIsSticker = true; + } + } + + // 把剩余部分插入到消息队列 + const remainingParts = []; + if (beforeText) { + remainingParts.push(stickerTag); // 表情标签 + } + if (afterText) { + remainingParts.push(afterText); + } + + // 插入到当前位置之后 + if (remainingParts.length > 0) { + aiMessages.splice(i + 1, 0, ...remainingParts); + console.log('[可乐] 已将嵌入表情分割,剩余部分:', remainingParts); + } + } + } } // 解析AI引用格式 @@ -2313,7 +2394,7 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi const lastPhotoMatch = lastAiMsg.match(/^\[照片[::]\s*(.+?)\]$/); const lastMusicMatch = lastAiMsg.match(/^\[(?:分享)?音乐[::]\s*(.+?)\]$/) || lastAiMsg.match(/^\[分享音乐\]\s*\*{0,2}[^*\n]+/); - const lastStickerMatch = lastAiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const lastStickerMatch = lastAiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], getSettings()) : null; if (lastVoiceMatch) { lastAiMsg = lastVoiceMatch[1]; @@ -2413,6 +2494,16 @@ export async function sendStickerMessage(stickerUrl, description = '') { let aiIsPhoto = false; let stickerUrl = null; + // 如果用户被AI拉黑,过滤掉AI不能执行的操作(只能发文字和表情包) + if (contact.blockedByAI === true) { + aiMsg = aiMsg.replace(/\[拉黑\]/g, ''); + aiMsg = aiMsg.replace(/\[(?:语音通话|视频通话|语音通话请求|视频通话请求|通话请求)\]/g, ''); + aiMsg = aiMsg.replace(/\[红包[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.replace(/\[转账[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.trim(); + if (!aiMsg) continue; + } + // 检测拉黑/取消拉黑标签 const blockAction = extractBlockAction(aiMsg); if (blockAction.action === 'block') { @@ -2518,7 +2609,7 @@ export async function sendStickerMessage(stickerUrl, description = '') { } // 解析AI表情包格式 [表情:序号] / [表情:名称] - const stickerMatch = aiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const stickerMatch = aiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); console.log('[可乐] sendStickerMessage AI表情包解析:', { 原始消息: aiMsg, 正则匹配结果: stickerMatch @@ -2531,6 +2622,35 @@ export async function sendStickerMessage(stickerUrl, description = '') { resolved: !!stickerUrl }); if (stickerUrl) aiIsSticker = true; + } else { + // 【后备处理】检查是否包含嵌入的表情标签 + const embeddedStickerMatch = aiMsg.match(/\[表情\s*[::∶]\s*(.+?)\]/); + if (embeddedStickerMatch) { + console.log('[可乐] sendStickerMessage 检测到嵌入的表情标签:', aiMsg); + const stickerTag = embeddedStickerMatch[0]; + const stickerIndex = aiMsg.indexOf(stickerTag); + const beforeText = aiMsg.substring(0, stickerIndex).trim(); + const afterText = aiMsg.substring(stickerIndex + stickerTag.length).trim(); + + if (beforeText || afterText) { + if (beforeText) { + aiMsg = beforeText; + } else { + aiMsg = stickerTag; + const token = (embeddedStickerMatch[1] || '').trim(); + stickerUrl = resolveUserStickerUrl(token, settings); + if (stickerUrl) aiIsSticker = true; + } + + const remainingParts = []; + if (beforeText) remainingParts.push(stickerTag); + if (afterText) remainingParts.push(afterText); + + if (remainingParts.length > 0) { + aiMessages.splice(i + 1, 0, ...remainingParts); + } + } + } } // 检查用户是否还在当前聊天界面 @@ -2640,7 +2760,7 @@ export async function sendStickerMessage(stickerUrl, description = '') { let lastAiMsg = aiMessages[aiMessages.length - 1]; const lastVoiceMatch = lastAiMsg.match(/^\[语音[::]\s*(.+?)\]$/); const lastPhotoMatch = lastAiMsg.match(/^\[照片[::]\s*(.+?)\]$/); - const lastStickerMatch = lastAiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const lastStickerMatch = lastAiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], getSettings()) : null; if (lastVoiceMatch) { lastAiMsg = lastVoiceMatch[1]; @@ -2821,6 +2941,16 @@ export async function sendPhotoMessage(description) { let aiIsPhoto = false; let stickerUrl = null; + // 如果用户被AI拉黑,过滤掉AI不能执行的操作(只能发文字和表情包) + if (contact.blockedByAI === true) { + aiMsg = aiMsg.replace(/\[拉黑\]/g, ''); + aiMsg = aiMsg.replace(/\[(?:语音通话|视频通话|语音通话请求|视频通话请求|通话请求)\]/g, ''); + aiMsg = aiMsg.replace(/\[红包[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.replace(/\[转账[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.trim(); + if (!aiMsg) continue; + } + // 检测拉黑/取消拉黑标签 const blockAction = extractBlockAction(aiMsg); if (blockAction.action === 'block') { @@ -2926,7 +3056,7 @@ export async function sendPhotoMessage(description) { } // 解析AI表情包格式 [表情:序号] / [表情:名称] - const stickerMatch = aiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const stickerMatch = aiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); console.log('[可乐] sendPhotoMessage AI表情包解析:', { 原始消息: aiMsg, 正则匹配结果: stickerMatch @@ -2939,6 +3069,35 @@ export async function sendPhotoMessage(description) { resolved: !!stickerUrl }); if (stickerUrl) aiIsSticker = true; + } else { + // 【后备处理】检查是否包含嵌入的表情标签 + const embeddedStickerMatch = aiMsg.match(/\[表情\s*[::∶]\s*(.+?)\]/); + if (embeddedStickerMatch) { + console.log('[可乐] sendPhotoMessage 检测到嵌入的表情标签:', aiMsg); + const stickerTag = embeddedStickerMatch[0]; + const stickerIndex = aiMsg.indexOf(stickerTag); + const beforeText = aiMsg.substring(0, stickerIndex).trim(); + const afterText = aiMsg.substring(stickerIndex + stickerTag.length).trim(); + + if (beforeText || afterText) { + if (beforeText) { + aiMsg = beforeText; + } else { + aiMsg = stickerTag; + const token = (embeddedStickerMatch[1] || '').trim(); + stickerUrl = resolveUserStickerUrl(token, settings); + if (stickerUrl) aiIsSticker = true; + } + + const remainingParts = []; + if (beforeText) remainingParts.push(stickerTag); + if (afterText) remainingParts.push(afterText); + + if (remainingParts.length > 0) { + aiMessages.splice(i + 1, 0, ...remainingParts); + } + } + } } // 检查用户是否还在当前聊天界面 @@ -3048,7 +3207,7 @@ export async function sendPhotoMessage(description) { let lastAiMsg = aiMessages[aiMessages.length - 1]; const lastVoiceMatch = lastAiMsg.match(/^\[语音[::]\s*(.+?)\]$/); const lastPhotoMatch = lastAiMsg.match(/^\[照片[::]\s*(.+?)\]$/); - const lastStickerMatch = lastAiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const lastStickerMatch = lastAiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], getSettings()) : null; if (lastVoiceMatch) { lastAiMsg = lastVoiceMatch[1]; @@ -3331,6 +3490,16 @@ export async function sendBatchMessages(messages) { let stickerUrl = null; let aiQuote = null; + // 如果用户被AI拉黑,过滤掉AI不能执行的操作(只能发文字和表情包) + if (contact.blockedByAI === true) { + aiMsg = aiMsg.replace(/\[拉黑\]/g, ''); + aiMsg = aiMsg.replace(/\[(?:语音通话|视频通话|语音通话请求|视频通话请求|通话请求)\]/g, ''); + aiMsg = aiMsg.replace(/\[红包[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.replace(/\[转账[::]\d+(?:\.\d{1,2})?[::]?.*?\]/g, ''); + aiMsg = aiMsg.trim(); + if (!aiMsg) continue; + } + // 检测拉黑/取消拉黑标签 const blockAction = extractBlockAction(aiMsg); if (blockAction.action === 'block') { @@ -3453,11 +3622,40 @@ export async function sendBatchMessages(messages) { } // 解析表情包格式 - const stickerMatch = aiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const stickerMatch = aiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); if (stickerMatch) { const token = (stickerMatch[1] || '').trim(); stickerUrl = resolveUserStickerUrl(token, settings); if (stickerUrl) aiIsSticker = true; + } else { + // 【后备处理】检查是否包含嵌入的表情标签 + const embeddedStickerMatch = aiMsg.match(/\[表情\s*[::∶]\s*(.+?)\]/); + if (embeddedStickerMatch) { + console.log('[可乐] sendBatchMessages 检测到嵌入的表情标签:', aiMsg); + const stickerTag = embeddedStickerMatch[0]; + const stickerIndex = aiMsg.indexOf(stickerTag); + const beforeText = aiMsg.substring(0, stickerIndex).trim(); + const afterText = aiMsg.substring(stickerIndex + stickerTag.length).trim(); + + if (beforeText || afterText) { + if (beforeText) { + aiMsg = beforeText; + } else { + aiMsg = stickerTag; + const token = (embeddedStickerMatch[1] || '').trim(); + stickerUrl = resolveUserStickerUrl(token, settings); + if (stickerUrl) aiIsSticker = true; + } + + const remainingParts = []; + if (beforeText) remainingParts.push(stickerTag); + if (afterText) remainingParts.push(afterText); + + if (remainingParts.length > 0) { + aiMessages.splice(i + 1, 0, ...remainingParts); + } + } + } } // 解析引用格式 @@ -3567,7 +3765,7 @@ export async function sendBatchMessages(messages) { let lastAiMsg = aiMessages[aiMessages.length - 1]; const lastVoiceMatch = lastAiMsg.match(/^\[语音[::]\s*(.+?)\]$/); const lastPhotoMatch = lastAiMsg.match(/^\[照片[::]\s*(.+?)\]$/); - const lastStickerMatch = lastAiMsg.match(/^\[表情[::]\s*(.+?)\]$/); + const lastStickerMatch = lastAiMsg.match(/^\[表情\s*[::∶]\s*(.+?)\]$/); const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], settings) : null; if (lastVoiceMatch) { lastAiMsg = lastVoiceMatch[1]; diff --git a/config.js b/config.js index e434ba1..219073f 100644 --- a/config.js +++ b/config.js @@ -108,13 +108,22 @@ export const MEME_STICKERS = [ '是的主人yvrgdc.jpg' ]; -// Meme 表情包提示词模板 -export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能 +// 生成表情包显示名称列表(去重) +export function getMemeDisplayNames() { + const names = MEME_STICKERS.map(s => extractStickerName(s)).filter(n => n); + return [...new Set(names)]; // 去重 +} + +// Meme 表情包提示词模板(动态生成,使用友好名称) +export function getMemePromptTemplate() { + const displayNames = getMemeDisplayNames(); + return `##【必须使用】表情包功能 【重要】你【必须】经常发送表情包!每2-3条回复至少发一个表情包! 使用规则: -- 格式:文件名 -- 只能从下面列表选择,不能编造文件名 +- 格式:表情名称 +- 只需要填写表情名称,不需要填写文件ID和扩展名 +- 只能从下面列表选择,不能编造名称 【绝对禁止 - 最重要的规则!】 标签前后【绝对不能】有任何其他文字!必须用 ||| 分隔! @@ -126,15 +135,19 @@ export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能 可用表情包列表: [ -${MEME_STICKERS.join('\n')} +${displayNames.join('\n')} ] 【正确示例】: -好想你|||小狗摇尾巴hmdj2k.gif -哈哈哈笑死|||小熊跳舞122o4w.gif|||你太搞笑了 -喜欢你egvwqb.jpg|||我真的好喜欢你 +好想你|||小狗摇尾巴 +哈哈哈笑死|||小熊跳舞|||你太搞笑了 +喜欢你|||我真的好喜欢你 记住:表情包让聊天更生动,【必须】经常使用!但标签必须独立!`; +} + +// 保留旧变量名以兼容,但实际使用时应调用 getMemePromptTemplate() +export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能 - 请使用 getMemePromptTemplate() 获取完整模板`; // 一起听功能提示词模板 export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】 @@ -357,13 +370,90 @@ export function getUserStickers(settings = getSettings()) { return raw.filter(s => s && typeof s.url === 'string' && s.url.trim()); } +// 从完整文件名中提取显示名称(去除6位ID和扩展名) +// 例如: "是的主人yvrgdc.jpg" -> "是的主人" +export function extractStickerName(filename) { + if (!filename || typeof filename !== 'string') return ''; + // 匹配: 名称 + 6位字母数字ID + .扩展名 + const match = filename.match(/^(.+?)([a-zA-Z0-9]{6})\.(jpg|jpeg|png|gif)$/i); + if (match) { + return match[1]; // 返回名称部分 + } + // 如果不匹配标准格式,尝试只去除扩展名 + return filename.replace(/\.(jpg|jpeg|png|gif)$/i, ''); +} + +// 从完整文件名中提取文件ID(6位ID+扩展名) +// 例如: "是的主人yvrgdc.jpg" -> "yvrgdc.jpg" +export function extractStickerFileId(filename) { + if (!filename || typeof filename !== 'string') return ''; + const match = filename.match(/([a-zA-Z0-9]{6}\.(jpg|jpeg|png|gif))$/i); + return match ? match[1] : ''; +} + +// 根据名称查找匹配的表情包,支持同名随机选择 +export function findStickerByName(name) { + if (!name || typeof name !== 'string') return null; + const searchName = name.trim().toLowerCase(); + + // 先尝试完整文件名匹配(包含ID和扩展名) + const exactMatch = MEME_STICKERS.find(s => s.toLowerCase() === searchName); + if (exactMatch) { + return extractStickerFileId(exactMatch); + } + + // 再尝试按显示名称匹配 + const matches = MEME_STICKERS.filter(s => { + const displayName = extractStickerName(s).toLowerCase(); + return displayName === searchName; + }); + + if (matches.length > 0) { + // 同名表情包随机选择一个 + const selected = matches[Math.floor(Math.random() * matches.length)]; + return extractStickerFileId(selected); + } + + // 模糊匹配:名称包含搜索词或搜索词包含名称 + const fuzzyMatches = MEME_STICKERS.filter(s => { + const displayName = extractStickerName(s).toLowerCase(); + return displayName && (displayName.includes(searchName) || searchName.includes(displayName)); + }); + + if (fuzzyMatches.length > 0) { + const selected = fuzzyMatches[Math.floor(Math.random() * fuzzyMatches.length)]; + return extractStickerFileId(selected); + } + + return null; +} + // 解析 标签,替换为图片 HTML +// 支持两种格式: +// 1. 完整文件名是的主人yvrgdc.jpg +// 2. 显示名称是的主人(会从同名表情中随机选择) export function parseMemeTag(text) { if (!text || typeof text !== 'string') return text; - // 匹配 任意描述+文件ID.扩展名,只捕获文件ID部分 - // 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(如 ! ? 等)的表情名称 - return text.replace(/<\s*meme\s*>.*?([a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif))\s*<\s*\/\s*meme\s*>/gi, (match, fileId) => { - return `表情包`; + + // 匹配所有 xxx 格式 + return text.replace(/<\s*meme\s*>(.+?)<\s*\/\s*meme\s*>/gi, (match, content) => { + const trimmedContent = content.trim(); + + // 尝试直接提取文件ID(完整文件名格式) + const directFileId = trimmedContent.match(/([a-zA-Z0-9]{6}\.(jpg|jpeg|png|gif))$/i); + if (directFileId) { + return `表情包`; + } + + // 按名称查找(支持同名随机选择) + const fileId = findStickerByName(trimmedContent); + if (fileId) { + return `表情包`; + } + + // 无法匹配,返回原文本并显示错误提示 + console.warn('[可乐] 未找到匹配的表情包:', trimmedContent); + return `[表情包未找到: ${trimmedContent}]`; }); } @@ -382,8 +472,8 @@ export function splitAIMessages(response) { // 第二步:对每个部分检查是否包含需要分割的特殊标签 const result = []; - // meme 标签 - 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符的表情名称 - const memeRegex = /<\s*meme\s*>.*?[a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif)\s*<\s*\/\s*meme\s*>/gi; + // meme 标签 - 匹配任意 xxx 格式,不仅限于带文件扩展名的 + const memeRegex = /<\s*meme\s*>[\s\S]+?<\s*\/\s*meme\s*>/gi; // 语音标签 [语音:xxx] 或 [语音:xxx] const voiceRegex = /\[语音[::]\s*.+?\]/g; // 照片标签 [照片:xxx] 或 [照片:xxx] @@ -393,8 +483,8 @@ export function splitAIMessages(response) { // 2. [分享音乐] 歌名 - 歌手 - 无冒号格式(AI可能会这样输出) const musicRegexWithColon = /\[(?:分享)?音乐[::]\s*.+?\]/g; const musicRegexNoColon = /\[分享音乐\]\s*[\u4e00-\u9fa5a-zA-Z0-9]+(?:\s*[-–—]\s*[\u4e00-\u9fa5a-zA-Z0-9]+)?/g; - // 表情标签 [表情:xxx] - const stickerRegex = /\[表情[::]\s*.+?\]/g; + // 表情标签 [表情:xxx] - 更宽松的匹配,允许冒号前后有空格 + const stickerRegex = /\[表情\s*[::∶]\s*.+?\]/g; // 撤回标签 [撤回] / [撤回了一条消息] / [撤回消息] / [撤回一条消息] / [已撤回] / [消息撤回] const recallRegex = /\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]/g; // 红包标签 [红包:金额:祝福语] 或 [红包:金额] diff --git a/emoji-panel.js b/emoji-panel.js index 0bf4fe3..3ce4bf2 100644 --- a/emoji-panel.js +++ b/emoji-panel.js @@ -9,6 +9,37 @@ import { isInGroupChat } from './group-chat.js'; import { hasPendingStickerSelection, setStickerForMultiMsg } from './chat-func-panel.js'; let emojiPanelInited = false; +let migrationDone = false; + +// 迁移:去除现有表情名称的数字后缀(如"点赞老人1" -> "点赞老人") +function migrateStickersRemoveNumericSuffix() { + if (migrationDone) return; + migrationDone = true; + + const settings = getSettings(); + if (!Array.isArray(settings.stickers) || settings.stickers.length === 0) return; + + let changed = false; + settings.stickers.forEach(sticker => { + if (!sticker.name) return; + // 匹配末尾的数字(1-9位数字),但保留6位catbox ID + // 只去除类似 "点赞老人1" 末尾的 "1", "2", "3" 等 + const match = sticker.name.match(/^(.+?)(\d{1,3})$/); + if (match) { + const baseName = match[1]; + // 确保不是catbox ID格式(6位字母数字) + if (!/^[a-z0-9]{6}$/i.test(baseName)) { + sticker.name = baseName; + changed = true; + } + } + }); + + if (changed) { + requestSave(); + console.log('[可乐] 已迁移表情名称,去除数字后缀'); + } +} // 默认表情包列表(catbox 图床) const DEFAULT_STICKERS = [ @@ -119,24 +150,11 @@ 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; + // 直接返回原始名称,允许同名表情存在 + // 同名表情通过URL区分,发送时随机选择 + return baseName; } // 切换表情面板显示/隐藏 @@ -177,7 +195,11 @@ export function refreshEmojiGrid() { let html = ''; // 我的表情区域(用户添加的表情) - html += '
我的表情
'; + html += '
我的表情'; + if (userStickers.length > 0) { + html += `清空全部`; + } + html += '
'; html += '
'; html += ``; userStickers.forEach((sticker, index) => { @@ -210,6 +232,9 @@ export function refreshEmojiGrid() { // 绑定添加按钮事件 document.getElementById('wechat-emoji-add-btn')?.addEventListener('click', showAddStickerDialog); + // 绑定清空全部按钮事件 + document.getElementById('wechat-emoji-clear-all')?.addEventListener('click', clearAllStickers); + // 绑定用户表情左滑删除 content.querySelectorAll('.wechat-emoji-swipe-container').forEach(container => { setupSwipeToDelete(container); @@ -243,6 +268,7 @@ function setupSwipeToDelete(container) { } isDragging = true; startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; + currentX = startX; // 初始化 currentX,确保点击时 diff 为 0 item.style.transition = 'none'; } @@ -502,6 +528,24 @@ function deleteSticker(index) { } } +// 清空所有用户表情 +function clearAllStickers() { + const settings = getSettings(); + const stickers = settings.stickers || []; + + if (stickers.length === 0) { + showToast('没有表情可清空', 'info'); + return; + } + + if (!confirm(`确定要清空全部 ${stickers.length} 个自定义表情吗?此操作不可恢复!`)) return; + + settings.stickers = []; + requestSave(); + refreshEmojiGrid(); + showToast('已清空所有表情'); +} + // 初始化表情面板 export function initEmojiPanel() { if (emojiPanelInited) return; @@ -511,6 +555,9 @@ export function initEmojiPanel() { emojiPanelInited = true; + // 执行迁移:去除现有表情名称的数字后缀 + migrateStickersRemoveNumericSuffix(); + // 绑定标签切换事件 document.querySelectorAll('.wechat-emoji-tab').forEach(tab => { tab.addEventListener('click', () => { diff --git a/gift.js b/gift.js index c26671b..0a2f39c 100644 --- a/gift.js +++ b/gift.js @@ -18,6 +18,8 @@ const ICON_GIFT_CHARACTER = ``; +const ICON_GIFT_BOTH = ``; + // 礼物分类数据 const GIFT_CATEGORIES = { normal: { @@ -48,7 +50,9 @@ const GIFT_CATEGORIES = { { id: 'butterfly', name: '穿戴式小蝴蝶', emoji: '🦋', desc: '隐蔽穿戴震动', hasControl: true, hasShock: false }, { id: 'collar', name: '项圈', emoji: '⭕', desc: '精致的项圈', hasControl: false }, { id: 'candle', name: '低温蜡烛', emoji: '🕯️', desc: '安全的低温蜡烛', hasControl: false }, - { id: 'lingerie', name: '情趣内衣', emoji: '👙', desc: '性感的情趣内衣', hasControl: false } + { id: 'lingerie', name: '情趣内衣', emoji: '👙', desc: '性感的情趣内衣', hasControl: false }, + { id: 'fuckingMachine', name: '炮机', emoji: '🔧', desc: '电动炮机', hasControl: true, hasShock: false }, + { id: 'masturbatorCup', name: '飞机杯', emoji: '🥤', desc: '电动飞机杯', hasControl: true, hasShock: false } ] } }; @@ -138,6 +142,10 @@ function renderGiftContent() { ${ICON_GIFT_USER} 送用户 +
`; @@ -304,7 +312,7 @@ export async function sendGift() { const giftsToSend = [...selectedGifts]; const giftNames = giftsToSend.map(g => g.name).join('、'); const giftEmojis = giftsToSend.map(g => g.emoji).join(' '); - const targetText = target === 'character' ? '送TA' : '送自己'; + const targetText = target === 'character' ? '送TA' : target === 'user' ? '送自己' : '同时送'; const giftMessage = `[情趣礼物套装] ${giftEmojis} ${giftNames}(${targetText})${customDesc ? ` - ${customDesc}` : ''}`; const giftRecord = { @@ -378,12 +386,20 @@ export async function sendGift() { showTypingIndicator(contact); // 构建给AI的提示 - const targetTextAI = target === 'character' ? '你' : '用户'; + let targetTextAI; + if (target === 'character') { + targetTextAI = '角色(你)'; + } else if (target === 'user') { + targetTextAI = '用户'; + } else { + targetTextAI = '你和用户两人同时'; + } const aiPrompt = `[系统提示:用户刚刚购买了一套情趣玩具套装,包括:${giftNames},准备送给${targetTextAI}使用。商品正在配送中,预计很快就会送达。${customDesc ? `用户附言:${customDesc}` : ''} 请根据你的角色性格,对这套即将到来的礼物做出反应: - 如果是送给你的:可以表现出期待、害羞、紧张、好奇等情绪,可以问用户打算怎么用这些 - 如果是送给用户的:可以表现出好奇、调侃、期待看到用户反应等 +- 如果是同时送给两人的:可以表现出兴奋、期待、好奇等,想象两人一起使用的场景 - 根据你的人设和与用户的关系,反应可以是含蓄的、热情的、或者假装矜持的 - 回复不要太短,请展现角色的内心活动和情绪变化 @@ -438,7 +454,7 @@ export async function sendGift() { // 构建礼物消息 let giftMessage; if (isToy) { - const targetText = target === 'character' ? '送TA' : '送自己'; + const targetText = target === 'character' ? '送TA' : target === 'user' ? '送自己' : '同时送'; giftMessage = `[情趣礼物] ${gift.emoji} ${gift.name}(${targetText})${customDesc ? ` - ${customDesc}` : ''}`; } else { giftMessage = `[礼物] ${gift.emoji} ${gift.name}${customDesc ? ` - ${customDesc}` : ''}`; @@ -514,12 +530,20 @@ export async function sendGift() { let aiPrompt; if (isToy && gift.hasControl) { // 可控制的情趣玩具 - 配送中提示词 - const targetText = target === 'character' ? '你' : '用户'; + let targetText; + if (target === 'character') { + targetText = '你'; + } else if (target === 'user') { + targetText = '用户'; + } else { + targetText = '你和用户两人同时'; + } aiPrompt = `[系统提示:用户刚刚购买了一个${gift.name}(${gift.desc}),准备送给${targetText}使用。商品正在配送中,预计很快就会送达。${customDesc ? `用户附言:${customDesc}` : ''} 请根据你的角色性格,对这个即将到来的礼物做出反应: - 如果是送给你的:可以表现出期待、害羞、紧张、好奇等情绪 - 如果是送给用户的:可以表现出好奇、调侃、期待看到用户反应等 +- 如果是同时送给两人的:可以表现出兴奋、期待、好奇等,想象两人一起使用的场景 - 根据你的人设和与用户的关系,反应可以是含蓄的、热情的、或者假装矜持的 - 可以询问用户打算怎么用、什么时候用等 - 回复不要太短,请展现角色的内心活动和情绪变化 @@ -750,7 +774,7 @@ export function appendGiftMessage(role, gift, isToy, customDesc, contact, target const giftTypeClass = isToy ? 'wechat-gift-bubble-toy' : ''; let giftTypeLabel = isToy ? '情趣礼物' : '礼物'; if (isToy && target) { - giftTypeLabel = target === 'character' ? '情趣礼物·送TA' : '情趣礼物·送自己'; + giftTypeLabel = target === 'character' ? '情趣礼物·送TA' : target === 'user' ? '情趣礼物·送自己' : '情趣礼物·同时送'; } messageDiv.innerHTML = ` @@ -796,7 +820,7 @@ export function appendMultiGiftMessage(role, gifts, customDesc, contact, target : firstChar; } - const giftTypeLabel = target === 'character' ? '送TA' : '送自己'; + const giftTypeLabel = target === 'character' ? '送TA' : target === 'user' ? '送自己' : '同时送'; // 生成每个礼物的标签 const giftTagsHtml = gifts.map(g => ` diff --git a/group-chat.js b/group-chat.js index cf4e50b..dfe553f 100644 --- a/group-chat.js +++ b/group-chat.js @@ -4,7 +4,7 @@ import { requestSave, saveNow } from './save-manager.js'; import { getContext } from '../../../extensions.js'; -import { getSettings, SUMMARY_MARKER_PREFIX, getUserStickers, parseMemeTag, MEME_PROMPT_TEMPLATE, splitAIMessages } from './config.js'; +import { getSettings, SUMMARY_MARKER_PREFIX, getUserStickers, parseMemeTag, getMemePromptTemplate, splitAIMessages } from './config.js'; import { showToast } from './toast.js'; import { escapeHtml, sleep, formatMessageTime, calculateVoiceDuration, bindImageLoadFallback } from './utils.js'; import { getUserAvatarHTML, refreshChatList, getUserPersonaFromST } from './ui.js'; @@ -1279,7 +1279,7 @@ ${userStickers.map((s, i) => ` ${i + 1}. ${s.name || '表情' + (i + 1)}`).join // Meme 表情包提示词(如果启用) if (settings.memeStickersEnabled) { - systemPrompt += '\n\n' + MEME_PROMPT_TEMPLATE; + systemPrompt += '\n\n' + getMemePromptTemplate(); } return systemPrompt; diff --git a/listen-together.js b/listen-together.js index b697955..24392a7 100644 --- a/listen-together.js +++ b/listen-together.js @@ -600,7 +600,7 @@ function filterListenMessage(text) { // 过滤 meme 表情包 reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); // 过滤 [表情:xxx] - reply = reply.replace(/\[表情[::][^\]]*\]/g, '').trim(); + reply = reply.replace(/\[表情\s*[::∶][^\]]*\]/g, '').trim(); // 过滤 [照片:xxx] reply = reply.replace(/\[照片[::][^\]]*\]/g, '').trim(); // 过滤 [语音:xxx] diff --git a/main.js b/main.js index a9ffa8b..e309898 100644 --- a/main.js +++ b/main.js @@ -6,7 +6,7 @@ console.log('[可乐] main.js 开始加载...'); import { requestSave, setupUnloadSave } from './save-manager.js'; -import { loadSettings, getSettings, MEME_PROMPT_TEMPLATE } from './config.js'; +import { loadSettings, getSettings } from './config.js'; import { generatePhoneHTML } from './phone-html.js'; import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js'; import { showToast } from './toast.js'; diff --git a/message-menu.js b/message-menu.js index 4ba069e..25a25c0 100644 --- a/message-menu.js +++ b/message-menu.js @@ -26,7 +26,7 @@ const menuItems = [ { id: 'transcribe', icon: 'transcribe', text: '转文字', voiceOnly: true }, { id: 'quote', icon: 'quote', text: '引用' }, { id: 'recall', icon: 'recall', text: '撤回', userOnly: true }, - { id: 'delete', icon: 'delete', text: '删除' }, + { id: 'regenerate', icon: 'regenerate', text: '重新生成', userOnly: true }, { id: 'multiselect', icon: 'multiselect', text: '多选' } ]; @@ -50,11 +50,10 @@ const icons = { `, - delete: ` - - - - + regenerate: ` + + + `, multiselect: ` @@ -281,11 +280,11 @@ function handleMenuAction(action, msgIndex, voiceId = '', voiceContent = '') { recallMessage(msgIndex, contact); } break; - case 'delete': + case 'regenerate': if (groupIndex >= 0) { - deleteGroupMessage(msgIndex, groupChat); + regenerateGroupMessage(msgIndex, groupChat); } else { - deleteMessage(msgIndex, contact); + regenerateMessage(msgIndex, contact); } break; case 'multiselect': @@ -450,13 +449,75 @@ export function setQuote(quote) { } } -// 删除消息 -function deleteMessage(msgIndex, contact) { - contact.chatHistory.splice(msgIndex, 1); +// 重新生成回复(保留用户消息,删除后面的AI消息并重新生成) +async function regenerateMessage(msgIndex, contact) { + const msg = contact.chatHistory[msgIndex]; + if (!msg || msg.role !== 'user') { + showToast('只能对用户消息重新生成'); + return; + } + + // 删除该用户消息之后的所有消息 + const removedCount = contact.chatHistory.length - msgIndex - 1; + if (removedCount > 0) { + contact.chatHistory.splice(msgIndex + 1); + } + requestSave(); // 刷新聊天界面 openChat(currentChatIndex); - showToast('已删除'); + showToast('正在重新生成...'); + + // 触发AI重新回复 + try { + showTypingIndicator(contact); + + const { callAI } = await import('./ai.js'); + // 使用用户原始消息重新调用AI + const userContent = msg.content || ''; + const aiResponse = await callAI(contact, userContent); + + hideTypingIndicator(); + + const now = new Date(); + const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`; + + // 解析AI回复(可能有多条消息) + const aiMessages = splitAIMessages(aiResponse); + + for (const aiMsg of aiMessages) { + let finalMsg = aiMsg.trim(); + if (!finalMsg) continue; + + let isVoice = false; + const voiceMatch = finalMsg.match(/^\[语音[::]\s*(.+?)\]$/); + if (voiceMatch) { + finalMsg = voiceMatch[1]; + isVoice = true; + } + + contact.chatHistory.push({ + role: 'assistant', + content: finalMsg, + time: timeStr, + timestamp: Date.now(), + isVoice: isVoice + }); + + appendMessage('assistant', finalMsg, contact, isVoice); + } + + requestSave(); + } catch (err) { + hideTypingIndicator(); + console.error('[可乐] 重新生成失败:', err); + showToast('重新生成失败'); + } +} + +// 群聊重新生成回复 +async function regenerateGroupMessage(msgIndex, groupChat) { + showToast('群聊暂不支持重新生成'); } // 撤回消息 diff --git a/phone-html.js b/phone-html.js index 669d5fe..eab2835 100644 --- a/phone-html.js +++ b/phone-html.js @@ -3,7 +3,7 @@ * 这是最长的函数,单独提取以便维护 */ -import { getSettings, defaultSettings, MEME_PROMPT_TEMPLATE, MEME_STICKERS } from './config.js'; +import { getSettings, defaultSettings, MEME_STICKERS } from './config.js'; import { getCurrentTime, escapeHtml } from './utils.js'; import { getUserAvatarHTML, generateChatList, generateContactsList } from './ui.js'; import { ICON_RED_PACKET, ICON_RED_PACKET_LARGE, ICON_USER } from './icons.js'; diff --git a/summary.js b/summary.js index bd4fb43..8c2defc 100644 --- a/summary.js +++ b/summary.js @@ -500,7 +500,7 @@ export async function callSummaryAPI(prompt) { { role: 'user', content: prompt } ], temperature: 1, - max_tokens: 8196 + max_tokens: 50000 }) }); @@ -550,7 +550,7 @@ function parseJSONResponse(content) { const words = content.match(/[\u4e00-\u9fa5]{2,}/g) || ['聊天', '记录']; return { keys: [...new Set(words)].slice(0, 5), - content: content.substring(0, 800).replace(/```[\s\S]*?```/g, '').trim(), + content: content.substring(0, 30000).replace(/```[\s\S]*?```/g, '').trim(), comment: '感情记录' }; } diff --git a/toy-control.js b/toy-control.js index 7f7f0be..45abf02 100644 --- a/toy-control.js +++ b/toy-control.js @@ -28,17 +28,41 @@ const TOY_ICONS = { // 吮吸类玩具图标 gentle: ``, strong: ``, - pulse: `` + 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: { @@ -107,6 +131,74 @@ const SUCTION_CONTROL_MODES = { } }; +// 炮机控制模式定义 +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', @@ -220,13 +312,32 @@ function renderToyControlPage() { const messagesEl = document.getElementById('wechat-toy-control-messages'); if (titleEl) { - const targetText = toyControlState.target === 'character' ? 'TA在用' : '你在用'; + 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 modes = isSuction ? SUCTION_CONTROL_MODES : TOY_CONTROL_MODES; + 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) { @@ -265,6 +376,74 @@ function renderToyControlPage() { `; + } else if (isMachine) { + // 炮机按钮布局 + buttonsHtml = ` +
+ + + +
+
+ + + + +
+ `; + } else if (isCup) { + // 飞机杯按钮布局 + buttonsHtml = ` +
+ + + +
+
+ + + + +
+ `; } else { // 震动类玩具按钮布局(原有) buttonsHtml = ` @@ -321,6 +500,9 @@ function renderToyControlPage() { // 多玩具轮盘选择器 renderToyWheelSelector(); + // 更新麦克风/摄像头按钮的 active 状态 + updateMediaButtonUI(); + // 不清空消息(保留聊天内容) // 只在首次进入时清空 if (messagesEl && messagesEl.children.length === 0 && toyControlState.messages.length === 0) { @@ -729,7 +911,23 @@ function buildMediaTogglePrompt(mediaType, isEnabled) { async function onButtonPress(buttonId, pressedBy = 'user') { if (!toyControlState.isActive) return; - const button = TOY_CONTROL_MODES[buttonId] || (buttonId === 'shock' ? SHOCK_BUTTON : null); + // 根据玩具类型获取对应的模式定义 + 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; // 更新按钮状态(变深色) @@ -781,27 +979,49 @@ function updateButtonState(buttonId) { // 构建按钮按下提示词 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); // 根据玩具类型选择效果描述 - const modeEffects = isSuction ? { - // 吮吸类玩具效果 - gentle: '轻柔的吮吸开始了,温柔地包裹着敏感部位', - start: '吮吸开始/继续了', - strong: '吮吸力度突然加大,强烈的吸力让人难以抗拒', - pulse: '有节奏的吸放开始了,一收一放的刺激', - pause: '吮吸停止了,可以喘息一下', - shock: '一阵微电流刺激瞬间传来,让人猛地一颤' - } : { - // 震动类玩具效果 - classic: '稳定持续的震动开始了', - start: '震动开始/继续了', - rampage: '震动突然变到最大强度,非常强烈的刺激袭来', - wave: '震动开始由弱到强循环变化,一波一波的刺激', - pause: '震动停止了,可以喘息一下', - shock: '一阵微电流刺激瞬间传来,让人猛地一颤' - }; + 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; @@ -817,6 +1037,11 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { - 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等) - 切换后的身体感受和情绪变化 - 回复要有情感细节,符合你的角色性格`; + } else if (isBothUsing) { + prompt += `你和用户都在使用玩具,你主动切换了模式,请描述: +- 两人同时使用时的互动感受 +- 切换后你们双方的反应 +- 可以和用户分享此刻的感受`; } else { prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受: - 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等) @@ -833,6 +1058,11 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { if (isCharacterUsing) { prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。 描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`; + } else if (isBothUsing) { + prompt += `你和用户都在使用玩具,用户切换了模式,请描述: +- 你自己感受到的变化 +- 想象用户此刻的感受 +- 可以和用户互动,分享彼此的感受`; } else { prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。 请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`; diff --git a/ui.js b/ui.js index 719209c..be88c0f 100644 --- a/ui.js +++ b/ui.js @@ -178,7 +178,7 @@ function generateContactChatItem(contact) { preview = '[表情]'; } else if (preview.includes('') || preview.includes('')) { preview = '[图片]'; - } else if (/\[表情[::].+?\]/.test(preview)) { + } else if (/\[表情\s*[::∶].+?\]/.test(preview)) { preview = '[表情]'; } else if (/\[语音[::].+?\]/.test(preview)) { preview = '[语音]'; @@ -245,7 +245,7 @@ function generateGroupChatItem(group, settings) { content = '[表情]'; } else if (content.includes('') || content.includes('')) { content = '[图片]'; - } else if (/\[表情[::].+?\]/.test(content)) { + } else if (/\[表情\s*[::∶].+?\]/.test(content)) { content = '[表情]'; } else if (/\[语音[::].+?\]/.test(content)) { content = '[语音]';