Add files via upload

This commit is contained in:
Cola-Echo
2025-12-30 01:25:12 +08:00
committed by GitHub
parent 1b3ec1d43f
commit c97f2958ce
12 changed files with 1340 additions and 106 deletions

View File

@@ -18,7 +18,13 @@ const TOY_ICONS = {
wave: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M2 12c2-3 4-6 6-6s4 6 6 6 4-6 6-6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 18c2-3 4-6 6-6s4 6 6 6 4-6 6-6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
pause: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="6" y="4" width="4" height="16" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/><rect x="14" y="4" width="4" height="16" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/></svg>`,
shock: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="1.5" fill="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
back: `<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`
back: `<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
micOn: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/><line x1="12" y1="19" x2="12" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="8" y1="23" x2="16" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
micOff: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/><line x1="12" y1="19" x2="12" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="8" y1="23" x2="16" y2="23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
cameraOn: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M23 7l-7 5 7 5V7z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
cameraOff: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M23 7l-7 5 7 5V7z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
// 像素爱心图标(用于多玩具切换)
pixelHeart: `<svg viewBox="0 0 16 16" width="24" height="24"><rect x="2" y="4" width="2" height="2" fill="currentColor"/><rect x="4" y="2" width="2" height="2" fill="currentColor"/><rect x="6" y="2" width="2" height="2" fill="currentColor"/><rect x="8" y="2" width="2" height="2" fill="currentColor"/><rect x="10" y="2" width="2" height="2" fill="currentColor"/><rect x="12" y="4" width="2" height="2" fill="currentColor"/><rect x="2" y="6" width="2" height="2" fill="currentColor"/><rect x="4" y="4" width="2" height="2" fill="currentColor"/><rect x="6" y="4" width="2" height="2" fill="currentColor"/><rect x="8" y="4" width="2" height="2" fill="currentColor"/><rect x="10" y="4" width="2" height="2" fill="currentColor"/><rect x="12" y="6" width="2" height="2" fill="currentColor"/><rect x="2" y="8" width="2" height="2" fill="currentColor"/><rect x="4" y="6" width="2" height="2" fill="currentColor"/><rect x="6" y="6" width="2" height="2" fill="currentColor"/><rect x="8" y="6" width="2" height="2" fill="currentColor"/><rect x="10" y="6" width="2" height="2" fill="currentColor"/><rect x="12" y="8" width="2" height="2" fill="currentColor"/><rect x="4" y="8" width="2" height="2" fill="currentColor"/><rect x="6" y="8" width="2" height="2" fill="currentColor"/><rect x="8" y="8" width="2" height="2" fill="currentColor"/><rect x="10" y="8" width="2" height="2" fill="currentColor"/><rect x="6" y="10" width="2" height="2" fill="currentColor"/><rect x="8" y="10" width="2" height="2" fill="currentColor"/><rect x="4" y="10" width="2" height="2" fill="currentColor"/><rect x="10" y="10" width="2" height="2" fill="currentColor"/><rect x="6" y="12" width="4" height="2" fill="currentColor"/></svg>`
};
// 控制模式定义
@@ -73,11 +79,21 @@ let toyControlState = {
currentMode: null,
messages: [],
activeModes: new Set(),
sessionStartTime: null
sessionStartTime: null,
micEnabled: false, // 麦克风状态
cameraEnabled: false, // 摄像头状态
// 多玩具支持
isMulti: false, // 是否多玩具模式
toys: [], // 多玩具列表
currentToyIndex: 0, // 当前控制的玩具索引
wheelOpen: false // 轮盘是否展开
};
// 显示控制界面
export function showToyControlPage(gift, contact, contactIndex) {
// 判断是否多玩具模式
const isMulti = gift.isMulti === true;
toyControlState = {
isActive: true,
gift: gift,
@@ -87,9 +103,30 @@ export function showToyControlPage(gift, contact, contactIndex) {
currentMode: null,
messages: [],
activeModes: new Set(),
sessionStartTime: Date.now()
sessionStartTime: Date.now(),
micEnabled: false,
cameraEnabled: false,
// 多玩具支持
isMulti: isMulti,
toys: isMulti ? gift.toys : [],
currentToyIndex: 0,
wheelOpen: false
};
// 如果是多玩具模式,设置当前玩具为第一个
if (isMulti && gift.toys && gift.toys.length > 0) {
const firstToy = gift.toys[0];
toyControlState.gift = {
...gift,
giftId: firstToy.giftId,
giftName: firstToy.giftName,
giftEmoji: firstToy.giftEmoji,
giftDesc: firstToy.giftDesc,
hasControl: firstToy.hasControl,
hasShock: firstToy.hasShock
};
}
// 标记正在使用
if (contact.pendingGifts) {
const pendingGift = contact.pendingGifts.find(g => g.timestamp === gift.timestamp);
@@ -159,6 +196,9 @@ function renderToyControlPage() {
</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="wave">
${TOY_CONTROL_MODES.wave.icon}
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.wave.name}</span>
@@ -167,6 +207,9 @@ function renderToyControlPage() {
${TOY_CONTROL_MODES.pause.icon}
<span class="wechat-toy-btn-label">${TOY_CONTROL_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>
`;
buttonsEl.innerHTML = buttonsHtml;
@@ -187,12 +230,246 @@ function renderToyControlPage() {
}
}
// 清空消息
if (messagesEl) {
// 多玩具轮盘选择器
renderToyWheelSelector();
// 不清空消息(保留聊天内容)
// 只在首次进入时清空
if (messagesEl && messagesEl.children.length === 0 && toyControlState.messages.length === 0) {
messagesEl.innerHTML = '';
}
}
// 渲染多玩具轮盘选择器
function renderToyWheelSelector() {
// 移除旧的轮盘和遮罩
const existingWheel = document.getElementById('wechat-toy-wheel-container');
if (existingWheel) {
existingWheel.remove();
}
const existingOverlay = document.getElementById('wechat-toy-wheel-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
// 只在多玩具模式下显示
if (!toyControlState.isMulti || toyControlState.toys.length <= 1) return;
const controlPage = document.getElementById('wechat-toy-control-page');
if (!controlPage) return;
// 创建背景遮罩(点击关闭轮盘)
const overlay = document.createElement('div');
overlay.id = 'wechat-toy-wheel-overlay';
overlay.className = `wechat-toy-wheel-overlay ${toyControlState.wheelOpen ? 'active' : ''}`;
// 创建轮盘容器
const wheelContainer = document.createElement('div');
wheelContainer.id = 'wechat-toy-wheel-container';
wheelContainer.className = 'wechat-toy-wheel-container';
// 创建中心爱心按钮
const heartBtn = document.createElement('button');
heartBtn.className = 'wechat-toy-wheel-heart';
heartBtn.innerHTML = TOY_ICONS.pixelHeart;
heartBtn.title = '切换玩具';
// 创建轮盘选项
const wheelOptions = document.createElement('div');
wheelOptions.className = `wechat-toy-wheel-options ${toyControlState.wheelOpen ? 'open' : ''}`;
const toys = toyControlState.toys;
const angleStep = 360 / toys.length;
// 移动端使用更小的半径
const isMobile = window.innerWidth <= 420;
const radius = isMobile ? 55 : 70;
toys.forEach((toy, index) => {
const option = document.createElement('button');
option.className = `wechat-toy-wheel-option ${index === toyControlState.currentToyIndex ? 'active' : ''}`;
option.dataset.toyIndex = index;
// 计算位置(从顶部开始,顺时针排列)
const angle = -90 + (angleStep * index); // -90 从顶部开始
const x = Math.cos(angle * Math.PI / 180) * radius;
const y = Math.sin(angle * Math.PI / 180) * radius;
option.style.setProperty('--x', `${x}px`);
option.style.setProperty('--y', `${y}px`);
option.innerHTML = `<span class="emoji">${toy.giftEmoji}</span><span class="name">${toy.giftName}</span>`;
wheelOptions.appendChild(option);
});
wheelContainer.appendChild(wheelOptions);
wheelContainer.appendChild(heartBtn);
// 插入遮罩和轮盘到控制区域上方
const chatArea = controlPage.querySelector('.wechat-toy-control-chat');
if (chatArea) {
chatArea.parentNode.insertBefore(overlay, chatArea);
chatArea.parentNode.insertBefore(wheelContainer, chatArea);
} else {
controlPage.appendChild(overlay);
controlPage.appendChild(wheelContainer);
}
// 绑定爱心按钮事件(支持触摸)
heartBtn.addEventListener('click', toggleToyWheel);
heartBtn.addEventListener('touchend', (e) => {
e.preventDefault();
toggleToyWheel();
});
// 绑定遮罩点击事件(关闭轮盘)
overlay.addEventListener('click', closeToyWheel);
overlay.addEventListener('touchend', (e) => {
e.preventDefault();
closeToyWheel();
});
// 绑定轮盘选项事件(支持触摸)
wheelOptions.querySelectorAll('.wechat-toy-wheel-option').forEach(opt => {
opt.addEventListener('click', (e) => {
const index = parseInt(opt.dataset.toyIndex);
switchToToy(index);
});
opt.addEventListener('touchend', (e) => {
e.preventDefault();
const index = parseInt(opt.dataset.toyIndex);
switchToToy(index);
});
});
}
// 切换轮盘展开/收起
function toggleToyWheel() {
toyControlState.wheelOpen = !toyControlState.wheelOpen;
const options = document.querySelector('.wechat-toy-wheel-options');
const overlay = document.getElementById('wechat-toy-wheel-overlay');
if (options) {
options.classList.toggle('open', toyControlState.wheelOpen);
}
if (overlay) {
overlay.classList.toggle('active', toyControlState.wheelOpen);
}
}
// 关闭轮盘
function closeToyWheel() {
toyControlState.wheelOpen = false;
const options = document.querySelector('.wechat-toy-wheel-options');
const overlay = document.getElementById('wechat-toy-wheel-overlay');
if (options) {
options.classList.remove('open');
}
if (overlay) {
overlay.classList.remove('active');
}
}
// 切换到指定玩具
async function switchToToy(index) {
if (index < 0 || index >= toyControlState.toys.length) return;
if (index === toyControlState.currentToyIndex) {
// 同一个玩具,只关闭轮盘
closeToyWheel();
return;
}
const previousToy = toyControlState.toys[toyControlState.currentToyIndex];
const newToy = toyControlState.toys[index];
// 更新当前玩具索引
toyControlState.currentToyIndex = index;
// 更新当前gift信息
toyControlState.gift = {
...toyControlState.gift,
giftId: newToy.giftId,
giftName: newToy.giftName,
giftEmoji: newToy.giftEmoji,
giftDesc: newToy.giftDesc,
hasControl: newToy.hasControl,
hasShock: newToy.hasShock
};
// 重置模式状态(切换玩具后从暂停开始)
toyControlState.currentMode = null;
toyControlState.activeModes.clear();
// 关闭轮盘状态(渲染时会应用)
toyControlState.wheelOpen = false;
// 重新渲染界面(保留消息)
renderToyControlPage();
// 更新按钮状态
document.querySelectorAll('.wechat-toy-btn').forEach(btn => {
btn.classList.remove('active');
});
// 添加切换提示消息
addToyMessage('system', `已切换到 ${newToy.giftEmoji} ${newToy.giftName}`);
// AI对切换做出反应
showToyTypingIndicator();
const isCharacterUsing = toyControlState.target === 'character';
let prompt;
if (isCharacterUsing) {
prompt = `[玩具切换]
用户刚把玩具从"${previousToy.giftName}"切换到"${newToy.giftName}"了。
${newToy.giftName}的特点:${newToy.giftDesc}
请对这个切换做出反应:
- 可以表现出对新玩具的期待、紧张或好奇
- 如果是更刺激的玩具可以表现出紧张
- 如果是比较温和的可以表现出失落或放松
【重要规则】
1. 只能输出纯文字,禁止使用任何特殊格式标签
2. 禁止使用小括号描述动作如xxx
3. 回复简短1-2句话即可`;
} else {
prompt = `[玩具切换]
你把用户正在用的玩具从"${previousToy.giftName}"切换到"${newToy.giftName}"了。
${newToy.giftName}的特点:${newToy.giftDesc}
请对这个切换做出反应:
- 可以调侃用户接下来要体验的感觉
- 或者表达你为什么要给用户换这个
【重要规则】
1. 只能输出纯文字,禁止使用任何特殊格式标签
2. 禁止使用小括号描述动作如xxx
3. 回复简短1-2句话即可`;
}
try {
const response = await callToyAI(prompt);
hideToyTypingIndicator();
if (response) {
let reply = response.trim();
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
reply = reply.replace(/\[.*?\]/g, '').trim();
reply = reply.replace(/[^]*/g, '').trim();
reply = reply.replace(/\([^)]*\)/g, '').trim();
if (reply) {
addToyMessage('ai', reply);
}
}
} catch (err) {
hideToyTypingIndicator();
console.error('[可乐] 玩具切换AI回复失败:', err);
}
}
// 绑定事件
let toyEventsBound = false;
function bindToyControlEvents() {
@@ -216,6 +493,12 @@ function bindToyControlEvents() {
document.getElementById('wechat-toy-control-buttons')?.addEventListener('click', (e) => {
const btn = e.target.closest('.wechat-toy-btn');
if (btn) {
// 检查是否是媒体按钮(麦克风/摄像头)
const media = btn.dataset.media;
if (media) {
onMediaToggle(media);
return;
}
const mode = btn.dataset.mode;
if (mode) {
onButtonPress(mode, 'user');
@@ -234,6 +517,99 @@ function bindToyControlEvents() {
});
}
// 麦克风/摄像头切换处理
async function onMediaToggle(mediaType) {
if (!toyControlState.isActive) return;
const isMic = mediaType === 'mic';
const wasEnabled = isMic ? toyControlState.micEnabled : toyControlState.cameraEnabled;
const nowEnabled = !wasEnabled;
// 更新状态
if (isMic) {
toyControlState.micEnabled = nowEnabled;
} else {
toyControlState.cameraEnabled = nowEnabled;
}
// 更新按钮图标
updateMediaButtonUI();
// 显示typing
showToyTypingIndicator();
// 构建提示词
const prompt = buildMediaTogglePrompt(mediaType, nowEnabled);
try {
const response = await callToyAI(prompt);
hideToyTypingIndicator();
if (response) {
await processAIResponse(response);
}
} catch (err) {
hideToyTypingIndicator();
console.error('[可乐] 玩具控制AI回复失败:', err);
}
}
// 更新媒体按钮UI
function updateMediaButtonUI() {
const micBtn = document.querySelector('.wechat-toy-btn-media[data-media="mic"]');
const cameraBtn = document.querySelector('.wechat-toy-btn-media[data-media="camera"]');
if (micBtn) {
micBtn.innerHTML = toyControlState.micEnabled ? TOY_ICONS.micOn : TOY_ICONS.micOff;
micBtn.classList.toggle('active', toyControlState.micEnabled);
}
if (cameraBtn) {
cameraBtn.innerHTML = toyControlState.cameraEnabled ? TOY_ICONS.cameraOn : TOY_ICONS.cameraOff;
cameraBtn.classList.toggle('active', toyControlState.cameraEnabled);
}
}
// 构建媒体切换提示词
function buildMediaTogglePrompt(mediaType, isEnabled) {
const isCharacterUsing = toyControlState.target === 'character';
const isMic = mediaType === 'mic';
const mediaName = isMic ? '麦克风' : '摄像头';
const action = isEnabled ? '打开' : '关闭';
let prompt = `${mediaName}${action}\n`;
if (isCharacterUsing) {
// 角色在用玩具
if (isEnabled) {
prompt += isMic
? `用户打开了麦克风,现在可以听到用户的声音了。你正在使用玩具,听到用户的声音会让你更有感觉。请做出反应。`
: `用户打开了摄像头,现在可以看到用户了。你正在使用玩具,看到用户会让你更害羞/更有感觉。请做出反应。`;
} else {
prompt += isMic
? `用户关闭了麦克风,现在听不到用户的声音了。你可能会有点失落或者松一口气。请做出反应。`
: `用户关闭了摄像头,现在看不到用户了。你可能会有点失落或者松一口气。请做出反应。`;
}
} else {
// 用户在用玩具
if (isEnabled) {
prompt += isMic
? `用户打开了麦克风,你现在可以听到用户的声音/喘息/呻吟了。请做出反应,可以调侃、撩拨或关心用户。`
: `用户打开了摄像头,你现在可以看到用户使用玩具的样子了。请做出反应,可以调侃、撩拨或关心用户。`;
} else {
prompt += isMic
? `用户关闭了麦克风,你现在听不到用户的声音了。请做出反应。`
: `用户关闭了摄像头,你现在看不到用户了。请做出反应。`;
}
}
prompt += `\n\n【当前状态】
- 麦克风:${toyControlState.micEnabled ? '已打开(可以听到声音)' : '已关闭'}
- 摄像头:${toyControlState.cameraEnabled ? '已打开(可以看到画面)' : '已关闭'}
- 当前模式:${toyControlState.currentMode ? (TOY_CONTROL_MODES[toyControlState.currentMode]?.name || '已暂停') : '未开始'}`;
return prompt;
}
// 按钮点击处理
async function onButtonPress(buttonId, pressedBy = 'user') {
if (!toyControlState.isActive) return;
@@ -345,6 +721,18 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式
4. 直接输出角色说的话和感受`;
// 添加当前媒体状态信息
const mediaStatus = [];
if (toyControlState.micEnabled) {
mediaStatus.push('麦克风已开启(可以听到对方的声音/喘息)');
}
if (toyControlState.cameraEnabled) {
mediaStatus.push('摄像头已开启(可以看到对方)');
}
if (mediaStatus.length > 0) {
prompt += `\n\n【当前连接状态】${mediaStatus.join('')}`;
}
return prompt;
}
@@ -644,21 +1032,44 @@ function saveToySession() {
const seconds = (durationSec % 60).toString().padStart(2, '0');
const durationStr = `${minutes}:${seconds}`;
const session = {
gift: {
id: toyControlState.gift.giftId,
name: toyControlState.gift.giftName,
emoji: toyControlState.gift.giftEmoji
},
target: toyControlState.target,
time: timeStr,
timestamp: toyControlState.sessionStartTime || Date.now(),
duration: durationStr,
messages: toyControlState.messages.map(m => ({
role: m.role,
content: m.content
}))
};
// 构建session记录
let session;
if (toyControlState.isMulti) {
// 多玩具模式
session = {
isMulti: true,
toys: toyControlState.toys.map(t => ({
id: t.giftId,
name: t.giftName,
emoji: t.giftEmoji
})),
target: toyControlState.target,
time: timeStr,
timestamp: toyControlState.sessionStartTime || Date.now(),
duration: durationStr,
messages: toyControlState.messages.map(m => ({
role: m.role,
content: m.content
}))
};
} else {
// 单玩具模式
session = {
gift: {
id: toyControlState.gift.giftId,
name: toyControlState.gift.giftName,
emoji: toyControlState.gift.giftEmoji
},
target: toyControlState.target,
time: timeStr,
timestamp: toyControlState.sessionStartTime || Date.now(),
duration: durationStr,
messages: toyControlState.messages.map(m => ({
role: m.role,
content: m.content
}))
};
}
contact.toyHistory.push(session);