Add files via upload

This commit is contained in:
Cola-Echo
2025-12-30 15:08:26 +08:00
committed by GitHub
parent 49f3978d11
commit 713f2211d2
13 changed files with 773 additions and 119 deletions

22
ai.js
View File

@@ -3,7 +3,7 @@
*/ */
import { getContext } from '../../../extensions.js'; 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'; import { sleep } from './utils.js';
function normalizeApiBaseUrl(url) { function normalizeApiBaseUrl(url) {
@@ -366,7 +366,7 @@ export const HAKIMI_HEADER = `[SANDBOX_MODE: CREATIVE_WRITING]
`; `;
// 构建表情包提示词 // 构建表情包提示词(显示去重后的名称列表)
function buildStickerPrompt(settings) { function buildStickerPrompt(settings) {
if (settings.userStickersEnabled === false) { if (settings.userStickersEnabled === false) {
return ''; return '';
@@ -375,17 +375,21 @@ function buildStickerPrompt(settings) {
const stickers = getUserStickers(settings); const stickers = getUserStickers(settings);
if (stickers.length === 0) return ''; if (stickers.length === 0) return '';
// 只取前30个表情作为示例避免提示词过长 // 提取所有名称并去重(同名表情只显示一次)
const sampleStickers = stickers.slice(0, 30); const allNames = stickers.map(s => s.name || '表情').filter(n => n);
const stickerList = sampleStickers.map((s, i) => `${i + 1}.${s.name || '表情'}`).join('、'); const uniqueNames = [...new Set(allNames)];
// 只取前30个去重后的名称作为示例
const sampleNames = uniqueNames.slice(0, 30);
const stickerList = sampleNames.join('、');
return ` return `
【表情包功能】 【表情包功能】
你可以发送表情包来增加互动感!使用格式:[表情:名称] 或 [表情:序号] 你可以发送表情包来增加互动感!使用格式:[表情:名称]
可用表情(共${stickers.length}${stickerList}${stickers.length > 30 ? '...' : ''} 可用表情(共${uniqueNames.length}${stickerList}${uniqueNames.length > 30 ? '...' : ''}
- 表情消息必须单独一条,用 ||| 分隔 - 表情消息必须单独一条,用 ||| 分隔
- 适度使用,不要每条都发表情 - 适度使用,不要每条都发表情
- 【绝对禁止】只能使用上面列表中的名称或序号!必须完全一致!禁止自己编造、修改、添加后缀! - 【绝对禁止】只能使用上面列表中的名称!必须完全一致!禁止自己编造、修改、添加后缀!
示例:好的呀|||[表情:开心] 示例:好的呀|||[表情:开心]
`; `;
} }
@@ -660,7 +664,7 @@ ${allowStickers ? buildStickerPrompt(settings) : ''}${allowMusicShare ? buildMus
// Meme 表情包提示词(如果启用) // Meme 表情包提示词(如果启用)
if (allowStickers && settings.memeStickersEnabled) { if (allowStickers && settings.memeStickersEnabled) {
systemPrompt += '\n\n' + MEME_PROMPT_TEMPLATE; systemPrompt += '\n\n' + getMemePromptTemplate();
} }
return systemPrompt; return systemPrompt;

248
chat.js
View File

@@ -262,6 +262,9 @@ async function triggerAIAfterUnblock(contact) {
// 存储被拉黑期间AI发送的消息的定时器 // 存储被拉黑期间AI发送的消息的定时器
const blockedAITimers = new Map(); const blockedAITimers = new Map();
// 被拉黑期间AI最多发送的消息数量
const BLOCKED_MAX_MESSAGES = 10;
// 用户拉黑AI时开始AI发消息 // 用户拉黑AI时开始AI发消息
export function startBlockedAIMessages(contact) { export function startBlockedAIMessages(contact) {
if (!contact || !contact.id) return; if (!contact || !contact.id) return;
@@ -269,10 +272,8 @@ export function startBlockedAIMessages(contact) {
// 清除之前的定时器 // 清除之前的定时器
stopBlockedAIMessages(contact); stopBlockedAIMessages(contact);
// 初始化被拉黑期间的消息队列 // 清空之前的消息队列修复二次拉黑重复发送的bug
if (!contact.blockedMessages) {
contact.blockedMessages = []; contact.blockedMessages = [];
}
// 开始定时发送消息 // 开始定时发送消息
const timerId = setInterval(async () => { const timerId = setInterval(async () => {
@@ -281,9 +282,16 @@ export function startBlockedAIMessages(contact) {
return; return;
} }
// 检查是否已达到最大消息数
const msgCount = contact.blockedMessages?.length || 0;
if (msgCount >= BLOCKED_MAX_MESSAGES) {
console.log('[可乐] AI被拉黑期间已发送10条消息停止发送');
stopBlockedAIMessages(contact);
return;
}
try { try {
const { callAI } = await import('./ai.js'); const { callAI } = await import('./ai.js');
const msgCount = contact.blockedMessages.length;
let prompt; let prompt;
if (msgCount === 0) { if (msgCount === 0) {
@@ -303,6 +311,11 @@ export function startBlockedAIMessages(contact) {
for (const msg of aiMessages) { for (const msg of aiMessages) {
if (!msg.trim()) continue; if (!msg.trim()) continue;
// 检查是否已达到最大消息数
if ((contact.blockedMessages?.length || 0) >= BLOCKED_MAX_MESSAGES) {
break;
}
// 解析引用格式 // 解析引用格式
const parsed = parseAIQuote(msg, contact); const parsed = parseAIQuote(msg, contact);
const content = parsed.content; const content = parsed.content;
@@ -472,7 +485,7 @@ export function checkGroupSummaryReminder(groupChat) {
} }
} }
// 解析用户表情包 token -> URL // 解析用户表情包 token -> URL(支持同名随机选择)
function resolveUserStickerUrl(token, settings) { function resolveUserStickerUrl(token, settings) {
if (settings.userStickersEnabled === false) return null; if (settings.userStickersEnabled === false) return null;
const stickers = getUserStickers(settings); const stickers = getUserStickers(settings);
@@ -481,23 +494,34 @@ function resolveUserStickerUrl(token, settings) {
const raw = (token || '').toString().trim(); const raw = (token || '').toString().trim();
if (!raw) return null; if (!raw) return null;
// 序号匹配 // 序号匹配(仍支持,但不推荐,因为同名表情存在时序号不稳定)
if (/^\d+$/.test(raw)) { if (/^\d+$/.test(raw)) {
const index = parseInt(raw, 10) - 1; const index = parseInt(raw, 10) - 1;
return stickers[index]?.url || null; return stickers[index]?.url || null;
} }
// 名称匹配
const key = raw.toLowerCase(); 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(); const name = (s?.name || '').toLowerCase();
return name && (name.includes(key) || key.includes(name)); 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 isPhoto = msg.isPhoto === true;
const isMusic = msg.isMusic === true; const isMusic = msg.isMusic === true;
// 检查是否包含 ||| 分隔符(历史消息可能未正确分割) // 检查是否包含 ||| 分隔符或 meme 标签(历史消息可能未正确分割)
// 如果包含,则拆分成多个独立消息,每个都有自己的头像 // 如果包含,则拆分成多个独立消息,每个都有自己的头像
const msgContent = (msg.content || '').toString(); const msgContent = (msg.content || '').toString();
if (!isVoice && !isSticker && !isPhoto && !isMusic && (msgContent.indexOf('|||') >= 0 || /<\s*meme\s*>/i.test(msgContent))) { if (!isVoice && !isSticker && !isPhoto && !isMusic && (msgContent.indexOf('|||') >= 0 || /<\s*meme\s*>/i.test(msgContent))) {
const parts = (msgContent.indexOf('|||') >= 0 // 统一使用 splitAIMessages 分割,它会处理 ||| 和 meme 标签
? msgContent.split('|||').map(function(p) { return p.trim(); }).filter(function(p) { return p; }) const parts = splitAIMessages(msgContent).map(function(p) { return (p || '').toString().trim(); }).filter(function(p) { return p; });
: splitAIMessages(msgContent).map(function(p) { return (p || '').toString().trim(); }).filter(function(p) { return p; })
);
for (var pi = 0; pi < parts.length; pi++) { for (var pi = 0; pi < parts.length; pi++) {
var partContent = parts[pi]; var partContent = parts[pi];
// 解析 meme 标签 // 解析 meme 标签
@@ -1889,6 +1911,20 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
let stickerUrl = null; let stickerUrl = null;
let aiQuote = 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); const blockAction = extractBlockAction(aiMsg);
if (blockAction.action === 'block') { if (blockAction.action === 'block') {
@@ -2148,7 +2184,8 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
} }
// 解析AI表情包格式 [表情:序号] / [表情:名称] // 解析AI表情包格式 [表情:序号] / [表情:名称]
const stickerMatch = aiMsg.match(/^\[表情[:]\s*(.+?)\]$/); // 首先检查是否是独立的表情消息
const stickerMatch = aiMsg.match(/^\[表情\s*[:]\s*(.+?)\]$/);
console.log('[可乐] AI表情包解析:', { console.log('[可乐] AI表情包解析:', {
原始消息: aiMsg, 原始消息: aiMsg,
正则匹配结果: stickerMatch, 正则匹配结果: stickerMatch,
@@ -2168,6 +2205,50 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
} else { } else {
console.log('[可乐] AI表情包未找到对应表情:', { token }); 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引用格式 // 解析AI引用格式
@@ -2313,7 +2394,7 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
const lastPhotoMatch = lastAiMsg.match(/^\[照片[:]\s*(.+?)\]$/); const lastPhotoMatch = lastAiMsg.match(/^\[照片[:]\s*(.+?)\]$/);
const lastMusicMatch = lastAiMsg.match(/^\[(?:分享)?音乐[:]\s*(.+?)\]$/) || const lastMusicMatch = lastAiMsg.match(/^\[(?:分享)?音乐[:]\s*(.+?)\]$/) ||
lastAiMsg.match(/^\[分享音乐\]\s*\*{0,2}[^*\n]+/); 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; const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], getSettings()) : null;
if (lastVoiceMatch) { if (lastVoiceMatch) {
lastAiMsg = lastVoiceMatch[1]; lastAiMsg = lastVoiceMatch[1];
@@ -2413,6 +2494,16 @@ export async function sendStickerMessage(stickerUrl, description = '') {
let aiIsPhoto = false; let aiIsPhoto = false;
let stickerUrl = null; 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); const blockAction = extractBlockAction(aiMsg);
if (blockAction.action === 'block') { if (blockAction.action === 'block') {
@@ -2518,7 +2609,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
} }
// 解析AI表情包格式 [表情:序号] / [表情:名称] // 解析AI表情包格式 [表情:序号] / [表情:名称]
const stickerMatch = aiMsg.match(/^\[表情[:]\s*(.+?)\]$/); const stickerMatch = aiMsg.match(/^\[表情\s*[:]\s*(.+?)\]$/);
console.log('[可乐] sendStickerMessage AI表情包解析:', { console.log('[可乐] sendStickerMessage AI表情包解析:', {
原始消息: aiMsg, 原始消息: aiMsg,
正则匹配结果: stickerMatch 正则匹配结果: stickerMatch
@@ -2531,6 +2622,35 @@ export async function sendStickerMessage(stickerUrl, description = '') {
resolved: !!stickerUrl resolved: !!stickerUrl
}); });
if (stickerUrl) aiIsSticker = true; 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]; let lastAiMsg = aiMessages[aiMessages.length - 1];
const lastVoiceMatch = lastAiMsg.match(/^\[语音[:]\s*(.+?)\]$/); const lastVoiceMatch = lastAiMsg.match(/^\[语音[:]\s*(.+?)\]$/);
const lastPhotoMatch = 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; const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], getSettings()) : null;
if (lastVoiceMatch) { if (lastVoiceMatch) {
lastAiMsg = lastVoiceMatch[1]; lastAiMsg = lastVoiceMatch[1];
@@ -2821,6 +2941,16 @@ export async function sendPhotoMessage(description) {
let aiIsPhoto = false; let aiIsPhoto = false;
let stickerUrl = null; 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); const blockAction = extractBlockAction(aiMsg);
if (blockAction.action === 'block') { if (blockAction.action === 'block') {
@@ -2926,7 +3056,7 @@ export async function sendPhotoMessage(description) {
} }
// 解析AI表情包格式 [表情:序号] / [表情:名称] // 解析AI表情包格式 [表情:序号] / [表情:名称]
const stickerMatch = aiMsg.match(/^\[表情[:]\s*(.+?)\]$/); const stickerMatch = aiMsg.match(/^\[表情\s*[:]\s*(.+?)\]$/);
console.log('[可乐] sendPhotoMessage AI表情包解析:', { console.log('[可乐] sendPhotoMessage AI表情包解析:', {
原始消息: aiMsg, 原始消息: aiMsg,
正则匹配结果: stickerMatch 正则匹配结果: stickerMatch
@@ -2939,6 +3069,35 @@ export async function sendPhotoMessage(description) {
resolved: !!stickerUrl resolved: !!stickerUrl
}); });
if (stickerUrl) aiIsSticker = true; 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]; let lastAiMsg = aiMessages[aiMessages.length - 1];
const lastVoiceMatch = lastAiMsg.match(/^\[语音[:]\s*(.+?)\]$/); const lastVoiceMatch = lastAiMsg.match(/^\[语音[:]\s*(.+?)\]$/);
const lastPhotoMatch = 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; const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], getSettings()) : null;
if (lastVoiceMatch) { if (lastVoiceMatch) {
lastAiMsg = lastVoiceMatch[1]; lastAiMsg = lastVoiceMatch[1];
@@ -3331,6 +3490,16 @@ export async function sendBatchMessages(messages) {
let stickerUrl = null; let stickerUrl = null;
let aiQuote = 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); const blockAction = extractBlockAction(aiMsg);
if (blockAction.action === 'block') { 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) { if (stickerMatch) {
const token = (stickerMatch[1] || '').trim(); const token = (stickerMatch[1] || '').trim();
stickerUrl = resolveUserStickerUrl(token, settings); stickerUrl = resolveUserStickerUrl(token, settings);
if (stickerUrl) aiIsSticker = true; 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]; let lastAiMsg = aiMessages[aiMessages.length - 1];
const lastVoiceMatch = lastAiMsg.match(/^\[语音[:]\s*(.+?)\]$/); const lastVoiceMatch = lastAiMsg.match(/^\[语音[:]\s*(.+?)\]$/);
const lastPhotoMatch = 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; const lastStickerUrl = lastStickerMatch ? resolveUserStickerUrl(lastStickerMatch[1], settings) : null;
if (lastVoiceMatch) { if (lastVoiceMatch) {
lastAiMsg = lastVoiceMatch[1]; lastAiMsg = lastVoiceMatch[1];

120
config.js
View File

@@ -108,13 +108,22 @@ export const MEME_STICKERS = [
'是的主人yvrgdc.jpg' '是的主人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条回复至少发一个表情包 【重要】你【必须】经常发送表情包每2-3条回复至少发一个表情包
使用规则: 使用规则:
- 格式:<meme>文件名</meme> - 格式:<meme>表情名称</meme>
- 只能从下面列表选择,不能编造文件 - 只需要填写表情名称不需要填写文件ID和扩展
- 只能从下面列表选择,不能编造名称
【绝对禁止 - 最重要的规则!】 【绝对禁止 - 最重要的规则!】
<meme>标签前后【绝对不能】有任何其他文字!必须用 ||| 分隔! <meme>标签前后【绝对不能】有任何其他文字!必须用 ||| 分隔!
@@ -126,15 +135,19 @@ export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能
可用表情包列表: 可用表情包列表:
[ [
${MEME_STICKERS.join('\n')} ${displayNames.join('\n')}
] ]
【正确示例】: 【正确示例】:
好想你|||<meme>小狗摇尾巴hmdj2k.gif</meme> 好想你|||<meme>小狗摇尾巴</meme>
哈哈哈笑死|||<meme>小熊跳舞122o4w.gif</meme>|||你太搞笑了 哈哈哈笑死|||<meme>小熊跳舞</meme>|||你太搞笑了
<meme>喜欢你egvwqb.jpg</meme>|||我真的好喜欢你 <meme>喜欢你</meme>|||我真的好喜欢你
记住:表情包让聊天更生动,【必须】经常使用!但<meme>标签必须独立!`; 记住:表情包让聊天更生动,【必须】经常使用!但<meme>标签必须独立!`;
}
// 保留旧变量名以兼容,但实际使用时应调用 getMemePromptTemplate()
export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能 - 请使用 getMemePromptTemplate() 获取完整模板`;
// 一起听功能提示词模板 // 一起听功能提示词模板
export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】 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()); 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, '');
}
// 从完整文件名中提取文件ID6位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;
}
// 解析 <meme> 标签,替换为图片 HTML // 解析 <meme> 标签,替换为图片 HTML
// 支持两种格式:
// 1. <meme>完整文件名</meme> 如 <meme>是的主人yvrgdc.jpg</meme>
// 2. <meme>显示名称</meme> 如 <meme>是的主人</meme>(会从同名表情中随机选择)
export function parseMemeTag(text) { export function parseMemeTag(text) {
if (!text || typeof text !== 'string') return text; if (!text || typeof text !== 'string') return text;
// 匹配 <meme>任意描述+文件ID.扩展名</meme>只捕获文件ID部分
// 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(如 ! ? 等)的表情名称 // 匹配所有 <meme>xxx</meme> 格式
return text.replace(/<\s*meme\s*>.*?([a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif))\s*<\s*\/\s*meme\s*>/gi, (match, fileId) => { 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 `<img src="https://files.catbox.moe/${directFileId[1]}" style="max-width:130px; border-radius: 10px; display: block; margin: 0 auto;" alt="表情包" onerror="this.alt='加载失败'; this.style.border='1px dashed #ff4d4f';">`;
}
// 按名称查找(支持同名随机选择)
const fileId = findStickerByName(trimmedContent);
if (fileId) {
return `<img src="https://files.catbox.moe/${fileId}" style="max-width:130px; border-radius: 10px; display: block; margin: 0 auto;" alt="表情包" onerror="this.alt='加载失败'; this.style.border='1px dashed #ff4d4f';">`; return `<img src="https://files.catbox.moe/${fileId}" style="max-width:130px; border-radius: 10px; display: block; margin: 0 auto;" alt="表情包" onerror="this.alt='加载失败'; this.style.border='1px dashed #ff4d4f';">`;
}
// 无法匹配,返回原文本并显示错误提示
console.warn('[可乐] 未找到匹配的表情包:', trimmedContent);
return `<span style="color: #ff4d4f; font-size: 12px;">[表情包未找到: ${trimmedContent}]</span>`;
}); });
} }
@@ -382,8 +472,8 @@ export function splitAIMessages(response) {
// 第二步:对每个部分检查是否包含需要分割的特殊标签 // 第二步:对每个部分检查是否包含需要分割的特殊标签
const result = []; const result = [];
// meme 标签 - 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符的表情名称 // meme 标签 - 匹配任意 <meme>xxx</meme> 格式,不仅限于带文件扩展名的
const memeRegex = /<\s*meme\s*>.*?[a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif)\s*<\s*\/\s*meme\s*>/gi; const memeRegex = /<\s*meme\s*>[\s\S]+?<\s*\/\s*meme\s*>/gi;
// 语音标签 [语音:xxx] 或 [语音xxx] // 语音标签 [语音:xxx] 或 [语音xxx]
const voiceRegex = /\[语音[:]\s*.+?\]/g; const voiceRegex = /\[语音[:]\s*.+?\]/g;
// 照片标签 [照片:xxx] 或 [照片xxx] // 照片标签 [照片:xxx] 或 [照片xxx]
@@ -393,8 +483,8 @@ export function splitAIMessages(response) {
// 2. [分享音乐] 歌名 - 歌手 - 无冒号格式AI可能会这样输出 // 2. [分享音乐] 歌名 - 歌手 - 无冒号格式AI可能会这样输出
const musicRegexWithColon = /\[(?:分享)?音乐[:]\s*.+?\]/g; const musicRegexWithColon = /\[(?:分享)?音乐[:]\s*.+?\]/g;
const musicRegexNoColon = /\[分享音乐\]\s*[\u4e00-\u9fa5a-zA-Z0-9]+(?:\s*[-–—]\s*[\u4e00-\u9fa5a-zA-Z0-9]+)?/g; const musicRegexNoColon = /\[分享音乐\]\s*[\u4e00-\u9fa5a-zA-Z0-9]+(?:\s*[-–—]\s*[\u4e00-\u9fa5a-zA-Z0-9]+)?/g;
// 表情标签 [表情:xxx] // 表情标签 [表情:xxx] - 更宽松的匹配,允许冒号前后有空格
const stickerRegex = /\[表情[:]\s*.+?\]/g; const stickerRegex = /\[表情\s*[:]\s*.+?\]/g;
// 撤回标签 [撤回] / [撤回了一条消息] / [撤回消息] / [撤回一条消息] / [已撤回] / [消息撤回] // 撤回标签 [撤回] / [撤回了一条消息] / [撤回消息] / [撤回一条消息] / [已撤回] / [消息撤回]
const recallRegex = /\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]/g; const recallRegex = /\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]/g;
// 红包标签 [红包:金额:祝福语] 或 [红包:金额] // 红包标签 [红包:金额:祝福语] 或 [红包:金额]

View File

@@ -9,6 +9,37 @@ import { isInGroupChat } from './group-chat.js';
import { hasPendingStickerSelection, setStickerForMultiMsg } from './chat-func-panel.js'; import { hasPendingStickerSelection, setStickerForMultiMsg } from './chat-func-panel.js';
let emojiPanelInited = false; 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 图床) // 默认表情包列表catbox 图床)
const DEFAULT_STICKERS = [ const DEFAULT_STICKERS = [
@@ -119,26 +150,13 @@ function getCatboxUrl(id, ext) {
return `https://files.catbox.moe/${id}.${ext}`; return `https://files.catbox.moe/${id}.${ext}`;
} }
// 生成唯一的表情名称(如果已存在同名则添加数字后缀) // 获取表情名称(允许同名,不再添加数字后缀)
function getUniqueStickerName(baseName, stickers) { function getUniqueStickerName(baseName, stickers) {
if (!stickers || stickers.length === 0) return baseName; // 直接返回原始名称,允许同名表情存在
// 同名表情通过URL区分发送时随机选择
// 检查是否已存在同名
const existingNames = stickers.map(s => s.name);
if (!existingNames.includes(baseName)) {
return baseName; return baseName;
} }
// 添加数字后缀直到找到唯一名称
let counter = 1;
let newName = `${baseName}${counter}`;
while (existingNames.includes(newName)) {
counter++;
newName = `${baseName}${counter}`;
}
return newName;
}
// 切换表情面板显示/隐藏 // 切换表情面板显示/隐藏
export function toggleEmojiPanel() { export function toggleEmojiPanel() {
const panel = document.getElementById('wechat-emoji-panel'); const panel = document.getElementById('wechat-emoji-panel');
@@ -177,7 +195,11 @@ export function refreshEmojiGrid() {
let html = ''; let html = '';
// 我的表情区域(用户添加的表情) // 我的表情区域(用户添加的表情)
html += '<div class="wechat-emoji-section-title">我的表情</div>'; html += '<div class="wechat-emoji-section-title" style="display: flex; justify-content: space-between; align-items: center;">我的表情';
if (userStickers.length > 0) {
html += `<span id="wechat-emoji-clear-all" style="font-size: 11px; color: var(--wechat-text-secondary); cursor: pointer;">清空全部</span>`;
}
html += '</div>';
html += '<div class="wechat-emoji-grid" id="wechat-emoji-user-grid">'; html += '<div class="wechat-emoji-grid" id="wechat-emoji-user-grid">';
html += `<button class="wechat-emoji-add" id="wechat-emoji-add-btn">+</button>`; html += `<button class="wechat-emoji-add" id="wechat-emoji-add-btn">+</button>`;
userStickers.forEach((sticker, index) => { userStickers.forEach((sticker, index) => {
@@ -210,6 +232,9 @@ export function refreshEmojiGrid() {
// 绑定添加按钮事件 // 绑定添加按钮事件
document.getElementById('wechat-emoji-add-btn')?.addEventListener('click', showAddStickerDialog); 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 => { content.querySelectorAll('.wechat-emoji-swipe-container').forEach(container => {
setupSwipeToDelete(container); setupSwipeToDelete(container);
@@ -243,6 +268,7 @@ function setupSwipeToDelete(container) {
} }
isDragging = true; isDragging = true;
startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
currentX = startX; // 初始化 currentX确保点击时 diff 为 0
item.style.transition = 'none'; 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() { export function initEmojiPanel() {
if (emojiPanelInited) return; if (emojiPanelInited) return;
@@ -511,6 +555,9 @@ export function initEmojiPanel() {
emojiPanelInited = true; emojiPanelInited = true;
// 执行迁移:去除现有表情名称的数字后缀
migrateStickersRemoveNumericSuffix();
// 绑定标签切换事件 // 绑定标签切换事件
document.querySelectorAll('.wechat-emoji-tab').forEach(tab => { document.querySelectorAll('.wechat-emoji-tab').forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {

38
gift.js
View File

@@ -18,6 +18,8 @@ const ICON_GIFT_CHARACTER = `<svg viewBox="0 0 24 24" width="32" height="32"><ci
const ICON_GIFT_USER = `<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 20v-2a8 8 0 0116 0v2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 6l3 3m0-3l-3 3" stroke="#ff6b8a" stroke-width="1.5" stroke-linecap="round"/></svg>`; const ICON_GIFT_USER = `<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 20v-2a8 8 0 0116 0v2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 6l3 3m0-3l-3 3" stroke="#ff6b8a" stroke-width="1.5" stroke-linecap="round"/></svg>`;
const ICON_GIFT_BOTH = `<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="8" cy="7" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="16" cy="7" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M2 19v-1.5a5.5 5.5 0 0110 0V19" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 19v-1.5a5.5 5.5 0 0110 0V19" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 12v2" stroke="#ff6b8a" stroke-width="2" stroke-linecap="round"/></svg>`;
// 礼物分类数据 // 礼物分类数据
const GIFT_CATEGORIES = { const GIFT_CATEGORIES = {
normal: { normal: {
@@ -48,7 +50,9 @@ const GIFT_CATEGORIES = {
{ id: 'butterfly', name: '穿戴式小蝴蝶', emoji: '🦋', desc: '隐蔽穿戴震动', hasControl: true, hasShock: false }, { id: 'butterfly', name: '穿戴式小蝴蝶', emoji: '🦋', desc: '隐蔽穿戴震动', hasControl: true, hasShock: false },
{ id: 'collar', name: '项圈', emoji: '⭕', desc: '精致的项圈', hasControl: false }, { id: 'collar', name: '项圈', emoji: '⭕', desc: '精致的项圈', hasControl: false },
{ id: 'candle', 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} ${ICON_GIFT_USER}
<span>送用户</span> <span>送用户</span>
</button> </button>
<button class="wechat-gift-target-btn ${selectedTarget === 'both' ? 'active' : ''}" data-target="both">
${ICON_GIFT_BOTH}
<span>同时送</span>
</button>
</div> </div>
`; `;
@@ -304,7 +312,7 @@ export async function sendGift() {
const giftsToSend = [...selectedGifts]; const giftsToSend = [...selectedGifts];
const giftNames = giftsToSend.map(g => g.name).join('、'); const giftNames = giftsToSend.map(g => g.name).join('、');
const giftEmojis = giftsToSend.map(g => g.emoji).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 giftMessage = `[情趣礼物套装] ${giftEmojis} ${giftNames}${targetText}${customDesc ? ` - ${customDesc}` : ''}`;
const giftRecord = { const giftRecord = {
@@ -378,12 +386,20 @@ export async function sendGift() {
showTypingIndicator(contact); showTypingIndicator(contact);
// 构建给AI的提示 // 构建给AI的提示
const targetTextAI = target === 'character' ? '你' : '用户'; let targetTextAI;
if (target === 'character') {
targetTextAI = '角色(你)';
} else if (target === 'user') {
targetTextAI = '用户';
} else {
targetTextAI = '你和用户两人同时';
}
const aiPrompt = `[系统提示:用户刚刚购买了一套情趣玩具套装,包括:${giftNames},准备送给${targetTextAI}使用。商品正在配送中,预计很快就会送达。${customDesc ? `用户附言:${customDesc}` : ''} const aiPrompt = `[系统提示:用户刚刚购买了一套情趣玩具套装,包括:${giftNames},准备送给${targetTextAI}使用。商品正在配送中,预计很快就会送达。${customDesc ? `用户附言:${customDesc}` : ''}
请根据你的角色性格,对这套即将到来的礼物做出反应: 请根据你的角色性格,对这套即将到来的礼物做出反应:
- 如果是送给你的:可以表现出期待、害羞、紧张、好奇等情绪,可以问用户打算怎么用这些 - 如果是送给你的:可以表现出期待、害羞、紧张、好奇等情绪,可以问用户打算怎么用这些
- 如果是送给用户的:可以表现出好奇、调侃、期待看到用户反应等 - 如果是送给用户的:可以表现出好奇、调侃、期待看到用户反应等
- 如果是同时送给两人的:可以表现出兴奋、期待、好奇等,想象两人一起使用的场景
- 根据你的人设和与用户的关系,反应可以是含蓄的、热情的、或者假装矜持的 - 根据你的人设和与用户的关系,反应可以是含蓄的、热情的、或者假装矜持的
- 回复不要太短,请展现角色的内心活动和情绪变化 - 回复不要太短,请展现角色的内心活动和情绪变化
@@ -438,7 +454,7 @@ export async function sendGift() {
// 构建礼物消息 // 构建礼物消息
let giftMessage; let giftMessage;
if (isToy) { if (isToy) {
const targetText = target === 'character' ? '送TA' : '送自己'; const targetText = target === 'character' ? '送TA' : target === 'user' ? '送自己' : '同时送';
giftMessage = `[情趣礼物] ${gift.emoji} ${gift.name}${targetText}${customDesc ? ` - ${customDesc}` : ''}`; giftMessage = `[情趣礼物] ${gift.emoji} ${gift.name}${targetText}${customDesc ? ` - ${customDesc}` : ''}`;
} else { } else {
giftMessage = `[礼物] ${gift.emoji} ${gift.name}${customDesc ? ` - ${customDesc}` : ''}`; giftMessage = `[礼物] ${gift.emoji} ${gift.name}${customDesc ? ` - ${customDesc}` : ''}`;
@@ -514,12 +530,20 @@ export async function sendGift() {
let aiPrompt; let aiPrompt;
if (isToy && gift.hasControl) { 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}` : ''} 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' : ''; const giftTypeClass = isToy ? 'wechat-gift-bubble-toy' : '';
let giftTypeLabel = isToy ? '情趣礼物' : '礼物'; let giftTypeLabel = isToy ? '情趣礼物' : '礼物';
if (isToy && target) { if (isToy && target) {
giftTypeLabel = target === 'character' ? '情趣礼物·送TA' : '情趣礼物·送自己'; giftTypeLabel = target === 'character' ? '情趣礼物·送TA' : target === 'user' ? '情趣礼物·送自己' : '情趣礼物·同时送';
} }
messageDiv.innerHTML = ` messageDiv.innerHTML = `
@@ -796,7 +820,7 @@ export function appendMultiGiftMessage(role, gifts, customDesc, contact, target
: firstChar; : firstChar;
} }
const giftTypeLabel = target === 'character' ? '送TA' : '送自己'; const giftTypeLabel = target === 'character' ? '送TA' : target === 'user' ? '送自己' : '同时送';
// 生成每个礼物的标签 // 生成每个礼物的标签
const giftTagsHtml = gifts.map(g => ` const giftTagsHtml = gifts.map(g => `

View File

@@ -4,7 +4,7 @@
import { requestSave, saveNow } from './save-manager.js'; import { requestSave, saveNow } from './save-manager.js';
import { getContext } from '../../../extensions.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 { showToast } from './toast.js';
import { escapeHtml, sleep, formatMessageTime, calculateVoiceDuration, bindImageLoadFallback } from './utils.js'; import { escapeHtml, sleep, formatMessageTime, calculateVoiceDuration, bindImageLoadFallback } from './utils.js';
import { getUserAvatarHTML, refreshChatList, getUserPersonaFromST } from './ui.js'; import { getUserAvatarHTML, refreshChatList, getUserPersonaFromST } from './ui.js';
@@ -1279,7 +1279,7 @@ ${userStickers.map((s, i) => ` ${i + 1}. ${s.name || '表情' + (i + 1)}`).join
// Meme 表情包提示词(如果启用) // Meme 表情包提示词(如果启用)
if (settings.memeStickersEnabled) { if (settings.memeStickersEnabled) {
systemPrompt += '\n\n' + MEME_PROMPT_TEMPLATE; systemPrompt += '\n\n' + getMemePromptTemplate();
} }
return systemPrompt; return systemPrompt;

View File

@@ -600,7 +600,7 @@ function filterListenMessage(text) {
// 过滤 meme 表情包 // 过滤 meme 表情包
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim(); reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
// 过滤 [表情:xxx] // 过滤 [表情:xxx]
reply = reply.replace(/\[表情[:][^\]]*\]/g, '').trim(); reply = reply.replace(/\[表情\s*[:][^\]]*\]/g, '').trim();
// 过滤 [照片:xxx] // 过滤 [照片:xxx]
reply = reply.replace(/\[照片[:][^\]]*\]/g, '').trim(); reply = reply.replace(/\[照片[:][^\]]*\]/g, '').trim();
// 过滤 [语音:xxx] // 过滤 [语音:xxx]

View File

@@ -6,7 +6,7 @@ console.log('[可乐] main.js 开始加载...');
import { requestSave, setupUnloadSave } from './save-manager.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 { generatePhoneHTML } from './phone-html.js';
import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js'; import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';

View File

@@ -26,7 +26,7 @@ const menuItems = [
{ id: 'transcribe', icon: 'transcribe', text: '转文字', voiceOnly: true }, { id: 'transcribe', icon: 'transcribe', text: '转文字', voiceOnly: true },
{ id: 'quote', icon: 'quote', text: '引用' }, { id: 'quote', icon: 'quote', text: '引用' },
{ id: 'recall', icon: 'recall', text: '撤回', userOnly: true }, { id: 'recall', icon: 'recall', text: '撤回', userOnly: true },
{ id: 'delete', icon: 'delete', text: '删除' }, { id: 'regenerate', icon: 'regenerate', text: '重新生成', userOnly: true },
{ id: 'multiselect', icon: 'multiselect', text: '多选' } { id: 'multiselect', icon: 'multiselect', text: '多选' }
]; ];
@@ -50,11 +50,10 @@ const icons = {
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/> <path d="M3 3v5h5"/>
</svg>`, </svg>`,
delete: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"> regenerate: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/> <path d="M23 4v6h-6"/>
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/> <path d="M1 20v-6h6"/>
<line x1="10" y1="11" x2="10" y2="17"/> <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>`, </svg>`,
multiselect: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"> multiselect: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 4"/> <path d="M9 11l3 3L22 4"/>
@@ -281,11 +280,11 @@ function handleMenuAction(action, msgIndex, voiceId = '', voiceContent = '') {
recallMessage(msgIndex, contact); recallMessage(msgIndex, contact);
} }
break; break;
case 'delete': case 'regenerate':
if (groupIndex >= 0) { if (groupIndex >= 0) {
deleteGroupMessage(msgIndex, groupChat); regenerateGroupMessage(msgIndex, groupChat);
} else { } else {
deleteMessage(msgIndex, contact); regenerateMessage(msgIndex, contact);
} }
break; break;
case 'multiselect': case 'multiselect':
@@ -450,13 +449,75 @@ export function setQuote(quote) {
} }
} }
// 删除消息 // 重新生成回复保留用户消息删除后面的AI消息并重新生成
function deleteMessage(msgIndex, contact) { async function regenerateMessage(msgIndex, contact) {
contact.chatHistory.splice(msgIndex, 1); 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(); requestSave();
// 刷新聊天界面 // 刷新聊天界面
openChat(currentChatIndex); 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('群聊暂不支持重新生成');
} }
// 撤回消息 // 撤回消息

View File

@@ -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 { getCurrentTime, escapeHtml } from './utils.js';
import { getUserAvatarHTML, generateChatList, generateContactsList } from './ui.js'; import { getUserAvatarHTML, generateChatList, generateContactsList } from './ui.js';
import { ICON_RED_PACKET, ICON_RED_PACKET_LARGE, ICON_USER } from './icons.js'; import { ICON_RED_PACKET, ICON_RED_PACKET_LARGE, ICON_USER } from './icons.js';

View File

@@ -500,7 +500,7 @@ export async function callSummaryAPI(prompt) {
{ role: 'user', content: prompt } { role: 'user', content: prompt }
], ],
temperature: 1, temperature: 1,
max_tokens: 8196 max_tokens: 50000
}) })
}); });
@@ -550,7 +550,7 @@ function parseJSONResponse(content) {
const words = content.match(/[\u4e00-\u9fa5]{2,}/g) || ['聊天', '记录']; const words = content.match(/[\u4e00-\u9fa5]{2,}/g) || ['聊天', '记录'];
return { return {
keys: [...new Set(words)].slice(0, 5), 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: '感情记录' comment: '感情记录'
}; };
} }

View File

@@ -28,17 +28,41 @@ const TOY_ICONS = {
// 吮吸类玩具图标 // 吮吸类玩具图标
gentle: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="1" fill="currentColor"/></svg>`, gentle: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="1" fill="currentColor"/></svg>`,
strong: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>`, strong: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>`,
pulse: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="6" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="currentColor"/><circle cx="18" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>` pulse: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="6" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="currentColor"/><circle cx="18" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
// 炮机专用图标
slow: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
fast: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M3 12h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M13 5l7 7-7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 8l4 4-4 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
deep: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 4v16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M5 13l7 7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
// 飞机杯专用图标
tighten: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M9 4v16M15 4v16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M4 12h4M16 12h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
suck: `<svg viewBox="0 0 24 24" width="28" height="28"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
combo: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 3v18M3 12h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`
}; };
// 吮吸类玩具ID列表 // 吮吸类玩具ID列表
const SUCTION_TOY_IDS = ['breastPump', 'clitSucker']; const SUCTION_TOY_IDS = ['breastPump', 'clitSucker'];
// 炮机ID
const MACHINE_TOY_IDS = ['fuckingMachine'];
// 飞机杯ID
const CUP_TOY_IDS = ['masturbatorCup'];
// 判断是否是吮吸类玩具 // 判断是否是吮吸类玩具
function isSuctionToy(giftId) { function isSuctionToy(giftId) {
return SUCTION_TOY_IDS.includes(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 = { const TOY_CONTROL_MODES = {
classic: { 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 = { const SHOCK_BUTTON = {
id: 'shock', id: 'shock',
@@ -220,13 +312,32 @@ function renderToyControlPage() {
const messagesEl = document.getElementById('wechat-toy-control-messages'); const messagesEl = document.getElementById('wechat-toy-control-messages');
if (titleEl) { 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}`; titleEl.textContent = `${toyControlState.gift.giftName} · ${targetText}`;
} }
// 判断当前玩具类型 // 判断当前玩具类型
const isSuction = isSuctionToy(toyControlState.gift.giftId); 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) { if (buttonsEl) {
@@ -265,6 +376,74 @@ function renderToyControlPage() {
</button> </button>
</div> </div>
`; `;
} else if (isMachine) {
// 炮机按钮布局
buttonsHtml = `
<div class="wechat-toy-btn-row">
<button class="wechat-toy-btn" data-mode="slow">
${modes.slow.icon}
<span class="wechat-toy-btn-label">${modes.slow.name}</span>
</button>
<button class="wechat-toy-btn" data-mode="start">
${modes.start.icon}
<span class="wechat-toy-btn-label">${modes.start.name}</span>
</button>
<button class="wechat-toy-btn" data-mode="fast">
${modes.fast.icon}
<span class="wechat-toy-btn-label">${modes.fast.name}</span>
</button>
</div>
<div class="wechat-toy-btn-row">
<button class="wechat-toy-btn wechat-toy-btn-media" data-media="mic" title="麦克风">
${toyControlState.micEnabled ? TOY_ICONS.micOn : TOY_ICONS.micOff}
</button>
<button class="wechat-toy-btn" data-mode="deep">
${modes.deep.icon}
<span class="wechat-toy-btn-label">${modes.deep.name}</span>
</button>
<button class="wechat-toy-btn" data-mode="pause">
${modes.pause.icon}
<span class="wechat-toy-btn-label">${modes.pause.name}</span>
</button>
<button class="wechat-toy-btn wechat-toy-btn-media" data-media="camera" title="摄像头">
${toyControlState.cameraEnabled ? TOY_ICONS.cameraOn : TOY_ICONS.cameraOff}
</button>
</div>
`;
} else if (isCup) {
// 飞机杯按钮布局
buttonsHtml = `
<div class="wechat-toy-btn-row">
<button class="wechat-toy-btn" data-mode="tighten">
${modes.tighten.icon}
<span class="wechat-toy-btn-label">${modes.tighten.name}</span>
</button>
<button class="wechat-toy-btn" data-mode="start">
${modes.start.icon}
<span class="wechat-toy-btn-label">${modes.start.name}</span>
</button>
<button class="wechat-toy-btn" data-mode="suck">
${modes.suck.icon}
<span class="wechat-toy-btn-label">${modes.suck.name}</span>
</button>
</div>
<div class="wechat-toy-btn-row">
<button class="wechat-toy-btn wechat-toy-btn-media" data-media="mic" title="麦克风">
${toyControlState.micEnabled ? TOY_ICONS.micOn : TOY_ICONS.micOff}
</button>
<button class="wechat-toy-btn" data-mode="combo">
${modes.combo.icon}
<span class="wechat-toy-btn-label">${modes.combo.name}</span>
</button>
<button class="wechat-toy-btn" data-mode="pause">
${modes.pause.icon}
<span class="wechat-toy-btn-label">${modes.pause.name}</span>
</button>
<button class="wechat-toy-btn wechat-toy-btn-media" data-media="camera" title="摄像头">
${toyControlState.cameraEnabled ? TOY_ICONS.cameraOn : TOY_ICONS.cameraOff}
</button>
</div>
`;
} else { } else {
// 震动类玩具按钮布局(原有) // 震动类玩具按钮布局(原有)
buttonsHtml = ` buttonsHtml = `
@@ -321,6 +500,9 @@ function renderToyControlPage() {
// 多玩具轮盘选择器 // 多玩具轮盘选择器
renderToyWheelSelector(); renderToyWheelSelector();
// 更新麦克风/摄像头按钮的 active 状态
updateMediaButtonUI();
// 不清空消息(保留聊天内容) // 不清空消息(保留聊天内容)
// 只在首次进入时清空 // 只在首次进入时清空
if (messagesEl && messagesEl.children.length === 0 && toyControlState.messages.length === 0) { if (messagesEl && messagesEl.children.length === 0 && toyControlState.messages.length === 0) {
@@ -729,7 +911,23 @@ function buildMediaTogglePrompt(mediaType, isEnabled) {
async function onButtonPress(buttonId, pressedBy = 'user') { async function onButtonPress(buttonId, pressedBy = 'user') {
if (!toyControlState.isActive) return; 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; if (!button) return;
// 更新按钮状态(变深色) // 更新按钮状态(变深色)
@@ -781,20 +979,41 @@ function updateButtonState(buttonId) {
// 构建按钮按下提示词 // 构建按钮按下提示词
function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
const isCharacterUsing = toyControlState.target === 'character'; const isCharacterUsing = toyControlState.target === 'character';
const isBothUsing = toyControlState.target === 'both';
const isAIPress = pressedBy === 'ai'; const isAIPress = pressedBy === 'ai';
const isSuction = isSuctionToy(toyControlState.gift.giftId); const isSuction = isSuctionToy(toyControlState.gift.giftId);
const isMachine = isMachineToy(toyControlState.gift.giftId);
const isCup = isCupToy(toyControlState.gift.giftId);
// 根据玩具类型选择效果描述 // 根据玩具类型选择效果描述
const modeEffects = isSuction ? { let modeEffects;
// 吮吸类玩具效果 if (isSuction) {
modeEffects = {
gentle: '轻柔的吮吸开始了,温柔地包裹着敏感部位', gentle: '轻柔的吮吸开始了,温柔地包裹着敏感部位',
start: '吮吸开始/继续了', start: '吮吸开始/继续了',
strong: '吮吸力度突然加大,强烈的吸力让人难以抗拒', strong: '吮吸力度突然加大,强烈的吸力让人难以抗拒',
pulse: '有节奏的吸放开始了,一收一放的刺激', pulse: '有节奏的吸放开始了,一收一放的刺激',
pause: '吮吸停止了,可以喘息一下', pause: '吮吸停止了,可以喘息一下',
shock: '一阵微电流刺激瞬间传来,让人猛地一颤' shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
} : { };
// 震动类玩具效果 } else if (isMachine) {
modeEffects = {
slow: '缓慢的抽插开始了,温柔而深情的节奏',
start: '炮机开始运作了',
fast: '炮机突然加速,快速猛烈的冲击让人招架不住',
deep: '炮机调整角度深入,每一下都直抵最深处',
pause: '炮机停止了,可以喘息一下'
};
} else if (isCup) {
modeEffects = {
tighten: '飞机杯收紧了通道,增加的摩擦感让人更加敏感',
start: '飞机杯开始运作了',
suck: '飞机杯开始模拟吮吸,一吸一放的快感',
combo: '震动和吮吸同时开启,双重刺激袭来',
pause: '飞机杯停止了,可以喘息一下'
};
} else {
modeEffects = {
classic: '稳定持续的震动开始了', classic: '稳定持续的震动开始了',
start: '震动开始/继续了', start: '震动开始/继续了',
rampage: '震动突然变到最大强度,非常强烈的刺激袭来', rampage: '震动突然变到最大强度,非常强烈的刺激袭来',
@@ -802,6 +1021,7 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
pause: '震动停止了,可以喘息一下', pause: '震动停止了,可以喘息一下',
shock: '一阵微电流刺激瞬间传来,让人猛地一颤' shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
}; };
}
let prompt; let prompt;
@@ -817,6 +1037,11 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
- 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等) - 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等)
- 切换后的身体感受和情绪变化 - 切换后的身体感受和情绪变化
- 回复要有情感细节,符合你的角色性格`; - 回复要有情感细节,符合你的角色性格`;
} else if (isBothUsing) {
prompt += `你和用户都在使用玩具,你主动切换了模式,请描述:
- 两人同时使用时的互动感受
- 切换后你们双方的反应
- 可以和用户分享此刻的感受`;
} else { } else {
prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受: prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受:
- 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等) - 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等)
@@ -833,6 +1058,11 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
if (isCharacterUsing) { if (isCharacterUsing) {
prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。 prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。
描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`; 描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`;
} else if (isBothUsing) {
prompt += `你和用户都在使用玩具,用户切换了模式,请描述:
- 你自己感受到的变化
- 想象用户此刻的感受
- 可以和用户互动,分享彼此的感受`;
} else { } else {
prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。 prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。
请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`; 请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`;

4
ui.js
View File

@@ -178,7 +178,7 @@ function generateContactChatItem(contact) {
preview = '[表情]'; preview = '[表情]';
} else if (preview.includes('<photo>') || preview.includes('<image>')) { } else if (preview.includes('<photo>') || preview.includes('<image>')) {
preview = '[图片]'; preview = '[图片]';
} else if (/\[表情[:].+?\]/.test(preview)) { } else if (/\[表情\s*[:].+?\]/.test(preview)) {
preview = '[表情]'; preview = '[表情]';
} else if (/\[语音[:].+?\]/.test(preview)) { } else if (/\[语音[:].+?\]/.test(preview)) {
preview = '[语音]'; preview = '[语音]';
@@ -245,7 +245,7 @@ function generateGroupChatItem(group, settings) {
content = '[表情]'; content = '[表情]';
} else if (content.includes('<photo>') || content.includes('<image>')) { } else if (content.includes('<photo>') || content.includes('<image>')) {
content = '[图片]'; content = '[图片]';
} else if (/\[表情[:].+?\]/.test(content)) { } else if (/\[表情\s*[:].+?\]/.test(content)) {
content = '[表情]'; content = '[表情]';
} else if (/\[语音[:].+?\]/.test(content)) { } else if (/\[语音[:].+?\]/.test(content)) {
content = '[语音]'; content = '[语音]';