mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-05 23:25:51 +00:00
Add files via upload
This commit is contained in:
22
ai.js
22
ai.js
@@ -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;
|
||||||
|
|||||||
250
chat.js
250
chat.js
@@ -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];
|
||||||
|
|||||||
122
config.js
122
config.js
@@ -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, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从完整文件名中提取文件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;
|
||||||
|
}
|
||||||
|
|
||||||
// 解析 <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) => {
|
||||||
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';">`;
|
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';">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无法匹配,返回原文本并显示错误提示
|
||||||
|
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;
|
||||||
// 红包标签 [红包:金额:祝福语] 或 [红包:金额]
|
// 红包标签 [红包:金额:祝福语] 或 [红包:金额]
|
||||||
|
|||||||
@@ -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,24 +150,11 @@ 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区分,发送时随机选择
|
||||||
// 检查是否已存在同名
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换表情面板显示/隐藏
|
// 切换表情面板显示/隐藏
|
||||||
@@ -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
38
gift.js
@@ -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 => `
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
2
main.js
2
main.js
@@ -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';
|
||||||
|
|||||||
@@ -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('群聊暂不支持重新生成');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 撤回消息
|
// 撤回消息
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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: '感情记录'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
272
toy-control.js
272
toy-control.js
@@ -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,27 +979,49 @@ 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) {
|
||||||
gentle: '轻柔的吮吸开始了,温柔地包裹着敏感部位',
|
modeEffects = {
|
||||||
start: '吮吸开始/继续了',
|
gentle: '轻柔的吮吸开始了,温柔地包裹着敏感部位',
|
||||||
strong: '吮吸力度突然加大,强烈的吸力让人难以抗拒',
|
start: '吮吸开始/继续了',
|
||||||
pulse: '有节奏的吸放开始了,一收一放的刺激',
|
strong: '吮吸力度突然加大,强烈的吸力让人难以抗拒',
|
||||||
pause: '吮吸停止了,可以喘息一下',
|
pulse: '有节奏的吸放开始了,一收一放的刺激',
|
||||||
shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
|
pause: '吮吸停止了,可以喘息一下',
|
||||||
} : {
|
shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
|
||||||
// 震动类玩具效果
|
};
|
||||||
classic: '稳定持续的震动开始了',
|
} else if (isMachine) {
|
||||||
start: '震动开始/继续了',
|
modeEffects = {
|
||||||
rampage: '震动突然变到最大强度,非常强烈的刺激袭来',
|
slow: '缓慢的抽插开始了,温柔而深情的节奏',
|
||||||
wave: '震动开始由弱到强循环变化,一波一波的刺激',
|
start: '炮机开始运作了',
|
||||||
pause: '震动停止了,可以喘息一下',
|
fast: '炮机突然加速,快速猛烈的冲击让人招架不住',
|
||||||
shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
|
deep: '炮机调整角度深入,每一下都直抵最深处',
|
||||||
};
|
pause: '炮机停止了,可以喘息一下'
|
||||||
|
};
|
||||||
|
} else if (isCup) {
|
||||||
|
modeEffects = {
|
||||||
|
tighten: '飞机杯收紧了通道,增加的摩擦感让人更加敏感',
|
||||||
|
start: '飞机杯开始运作了',
|
||||||
|
suck: '飞机杯开始模拟吮吸,一吸一放的快感',
|
||||||
|
combo: '震动和吮吸同时开启,双重刺激袭来',
|
||||||
|
pause: '飞机杯停止了,可以喘息一下'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
modeEffects = {
|
||||||
|
classic: '稳定持续的震动开始了',
|
||||||
|
start: '震动开始/继续了',
|
||||||
|
rampage: '震动突然变到最大强度,非常强烈的刺激袭来',
|
||||||
|
wave: '震动开始由弱到强循环变化,一波一波的刺激',
|
||||||
|
pause: '震动停止了,可以喘息一下',
|
||||||
|
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
4
ui.js
@@ -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 = '[语音]';
|
||||||
|
|||||||
Reference in New Issue
Block a user