Add files via upload

This commit is contained in:
Cola-Echo
2025-12-26 23:26:02 +08:00
committed by GitHub
parent 475707f104
commit 57ac982e84
15 changed files with 1088 additions and 63 deletions

52
chat.js
View File

@@ -155,35 +155,45 @@ async function handleBlockedExclamationClick(contact, exclamationEl) {
await triggerAIAfterUnblock(contact); await triggerAIAfterUnblock(contact);
} }
// 显示"已添加好友"的手机弹窗 // 显示"已添加好友"的仿手机弹窗
function showFriendAddedPopup(name) { function showFriendAddedPopup(name) {
// 创建弹窗遮罩 // 获取手机容器
const overlay = document.createElement('div'); const phoneContainer = document.querySelector('.wechat-phone');
overlay.className = 'wechat-phone-popup-overlay'; if (!phoneContainer) return;
overlay.innerHTML = `
<div class="wechat-phone-popup"> // 创建仿手机弹窗(使用与其他弹窗一致的 wechat-modal 样式)
<div class="wechat-phone-popup-icon"> const modal = document.createElement('div');
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#07c160" stroke-width="2"> modal.className = 'wechat-modal';
modal.id = 'wechat-friend-added-modal';
modal.innerHTML = `
<div class="wechat-modal-content">
<div class="wechat-modal-title">添加好友成功</div>
<div class="wechat-modal-body">
<div style="margin-bottom: 12px;">
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="#07c160" stroke-width="2">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
<path d="M8 12l2.5 2.5L16 9"/> <path d="M8 12l2.5 2.5L16 9"/>
</svg> </svg>
</div> </div>
<div class="wechat-phone-popup-text">${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。</div> ${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。
<div class="wechat-phone-popup-btn">确定</div> </div>
<div class="wechat-modal-actions">
<button class="wechat-btn wechat-btn-primary" id="wechat-friend-added-confirm">确定</button>
</div>
</div> </div>
`; `;
document.body.appendChild(overlay); phoneContainer.appendChild(modal);
// 点击确定关闭 // 点击确定关闭
overlay.querySelector('.wechat-phone-popup-btn').addEventListener('click', () => { modal.querySelector('#wechat-friend-added-confirm').addEventListener('click', () => {
overlay.remove(); modal.remove();
}); });
// 点击遮罩也关闭 // 点击遮罩也关闭
overlay.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
if (e.target === overlay) { if (e.target === modal) {
overlay.remove(); modal.remove();
} }
}); });
} }
@@ -862,7 +872,7 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
const isTimeout = callInfo === '对方已取消'; const isTimeout = callInfo === '对方已取消';
// 线条电话图标 // 线条电话图标
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/> <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>`; </svg>`;
@@ -871,16 +881,16 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
// 已接通:显示通话时长 // 已接通:显示通话时长
callRecordHTML = ` callRecordHTML = `
<div class="wechat-call-record"> <div class="wechat-call-record">
<span class="wechat-call-record-text">通话时长 ${callInfo}</span>
${phoneIconSVG} ${phoneIconSVG}
<span class="wechat-call-record-text">通话时长 ${callInfo}</span>
</div> </div>
`; `;
} else if (isCancelled) { } else if (isCancelled) {
// 用户发起未接通:已取消 // 用户发起未接通:已取消
callRecordHTML = ` callRecordHTML = `
<div class="wechat-call-record"> <div class="wechat-call-record">
<span class="wechat-call-record-text">已取消</span>
${phoneIconSVG} ${phoneIconSVG}
<span class="wechat-call-record-text">已取消</span>
</div> </div>
`; `;
} else if (isRejected) { } else if (isRejected) {
@@ -903,8 +913,8 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
// 兜底:显示原始内容 // 兜底:显示原始内容
callRecordHTML = ` callRecordHTML = `
<div class="wechat-call-record"> <div class="wechat-call-record">
<span class="wechat-call-record-text">${escapeHtml(callInfo)}</span>
${phoneIconSVG} ${phoneIconSVG}
<span class="wechat-call-record-text">${escapeHtml(callInfo)}</span>
</div> </div>
`; `;
} }
@@ -928,7 +938,7 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
const isTimeout = callInfo === '对方已取消'; const isTimeout = callInfo === '对方已取消';
// 摄像机图标 // 摄像机图标
const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="6" width="13" height="12" rx="2"/> <rect x="2" y="6" width="13" height="12" rx="2"/>
<path d="M22 8l-7 4 7 4V8z"/> <path d="M22 8l-7 4 7 4V8z"/>
</svg>`; </svg>`;

View File

@@ -145,7 +145,7 @@ export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】
【核心要求 - 必须遵守】 【核心要求 - 必须遵守】
1. 只能发送纯文字消息,像朋友之间真实聊天一样 1. 只能发送纯文字消息,像朋友之间真实聊天一样
2. 保持你的性格特点,用符合你角色设定的方式说话 2. 保持你的性格特点,用符合你角色设定的方式说话
3. 每次回复请发送2-4条消息,用换行分隔,让对话更有层次感 3. 每次回复1-3条消息即可,用换行分隔,不要刻意凑数量
4. 可以聊歌曲、聊心情、聊任何话题,自然就好 4. 可以聊歌曲、聊心情、聊任何话题,自然就好
5. 发表对歌曲的看法时,要结合你的角色性格和经历 5. 发表对歌曲的看法时,要结合你的角色性格和经历

View File

@@ -14,6 +14,9 @@ let pendingAvatarContactIndex = -1;
// 当前编辑的联系人索引 // 当前编辑的联系人索引
let currentEditingContactIndex = -1; let currentEditingContactIndex = -1;
// 弹窗打开时间(用于防止点击穿透)
let contactSettingsOpenTime = 0;
// 添加联系人 // 添加联系人
export function addContact(characterData) { export function addContact(characterData) {
const settings = getSettings(); const settings = getSettings();
@@ -183,6 +186,9 @@ export function openContactSettings(contactIndex) {
currentEditingContactIndex = contactIndex; currentEditingContactIndex = contactIndex;
// 记录打开时间,用于防止点击穿透
contactSettingsOpenTime = Date.now();
// 填充头像和名称 // 填充头像和名称
const avatarPreview = document.getElementById('wechat-contact-avatar-preview'); const avatarPreview = document.getElementById('wechat-contact-avatar-preview');
const nameEl = document.getElementById('wechat-contact-settings-name'); const nameEl = document.getElementById('wechat-contact-settings-name');
@@ -258,6 +264,11 @@ export function openContactSettings(contactIndex) {
export function saveContactSettings() { export function saveContactSettings() {
if (currentEditingContactIndex < 0) return; if (currentEditingContactIndex < 0) return;
// 防止点击穿透如果弹窗刚打开300ms内忽略保存操作
if (Date.now() - contactSettingsOpenTime < 300) {
return;
}
const settings = getSettings(); const settings = getSettings();
const contact = settings.contacts[currentEditingContactIndex]; const contact = settings.contacts[currentEditingContactIndex];
if (!contact) return; if (!contact) return;

View File

@@ -119,6 +119,26 @@ function getCatboxUrl(id, ext) {
return `https://files.catbox.moe/${id}.${ext}`; return `https://files.catbox.moe/${id}.${ext}`;
} }
// 生成唯一的表情名称(如果已存在同名则添加数字后缀)
function getUniqueStickerName(baseName, stickers) {
if (!stickers || stickers.length === 0) return baseName;
// 检查是否已存在同名
const existingNames = stickers.map(s => s.name);
if (!existingNames.includes(baseName)) {
return baseName;
}
// 添加数字后缀直到找到唯一名称
let counter = 1;
let newName = `${baseName}${counter}`;
while (existingNames.includes(newName)) {
counter++;
newName = `${baseName}${counter}`;
}
return newName;
}
// 切换表情面板显示/隐藏 // 切换表情面板显示/隐藏
export function toggleEmojiPanel() { export function toggleEmojiPanel() {
const panel = document.getElementById('wechat-emoji-panel'); const panel = document.getElementById('wechat-emoji-panel');
@@ -241,19 +261,22 @@ function addStickersFromInput(inputs) {
} }
} }
// 检查是否已存在 // 检查是否已存在按URL判断
const exists = settings.stickers.some(s => s.url === url); const exists = settings.stickers.some(s => s.url === url);
if (exists) { if (exists) {
showToast(`已存在: ${name}`, 'info'); showToast(`已存在: ${name}`, 'info');
continue; continue;
} }
// 生成唯一名称(如果同名则自动添加数字后缀)
const uniqueName = getUniqueStickerName(name, settings.stickers);
// 调试:显示添加的表情信息 // 调试:显示添加的表情信息
console.log('[可乐] 添加表情:', { name, url }); console.log('[可乐] 添加表情:', { name: uniqueName, url });
settings.stickers.push({ settings.stickers.push({
url, url,
name, name: uniqueName,
addedTime: new Date().toISOString() addedTime: new Date().toISOString()
}); });
addedCount++; addedCount++;
@@ -287,9 +310,11 @@ function addStickerFromFile() {
for (const file of files) { for (const file of files) {
try { try {
const dataUrl = await readFileAsDataURL(file); const dataUrl = await readFileAsDataURL(file);
// 生成唯一名称(如果同名则自动添加数字后缀)
const uniqueName = getUniqueStickerName(file.name, settings.stickers);
settings.stickers.push({ settings.stickers.push({
url: dataUrl, url: dataUrl,
name: file.name, name: uniqueName,
addedTime: new Date().toISOString() addedTime: new Date().toISOString()
}); });
addedCount++; addedCount++;

290
floating-ball.js Normal file
View File

@@ -0,0 +1,290 @@
/**
* 悬浮球组件
* 可爱猫咪悬浮窗,支持拖拽,点击打开主界面
*/
import { getSettings } from './config.js';
import { requestSave } from './save-manager.js';
// 悬浮球状态
let floatingBallState = {
isDragging: false,
startX: 0,
startY: 0,
initialX: 0,
initialY: 0,
currentX: 0,
currentY: 0,
hasMoved: false
};
// SVG 图标 - 渐变圆圈和猫咪
const FLOATING_BALL_SVG = `
<svg viewBox="0 0 100 100" width="60" height="60" class="floating-ball-svg">
<defs>
<linearGradient id="ring-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FFB6C1;stop-opacity:1" />
<stop offset="50%" style="stop-color:#FFC0CB;stop-opacity:1" />
<stop offset="100%" style="stop-color:#FFEFD5;stop-opacity:1" />
</linearGradient>
</defs>
<!-- 渐变圆圈 -->
<circle cx="50" cy="50" r="44" fill="none" stroke="url(#ring-gradient)" stroke-width="5" stroke-linecap="round"/>
<!-- 猫咪头部轮廓 -->
<g transform="translate(50, 52)" stroke="#333" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<!-- 头部 -->
<ellipse cx="0" cy="0" rx="22" ry="18"/>
<!-- 左耳 -->
<path d="M-18,-12 L-22,-24 L-12,-16"/>
<!-- 右耳 -->
<path d="M18,-12 L22,-24 L12,-16"/>
<!-- 内耳(粉色填充) -->
<path d="M-17,-14 L-19,-21 L-13,-16" fill="#FFB6C1" stroke="none"/>
<path d="M17,-14 L19,-21 L13,-16" fill="#FFB6C1" stroke="none"/>
<!-- 左眼 -->
<circle cx="-8" cy="-2" r="3" fill="#333"/>
<!-- 右眼 -->
<circle cx="8" cy="-2" r="3" fill="#333"/>
<!-- 鼻子 -->
<ellipse cx="0" cy="6" rx="2" ry="1.5" fill="#FFB6C1"/>
<!-- 嘴巴 -->
<path d="M0,7 Q-4,12 -8,9" fill="none"/>
<path d="M0,7 Q4,12 8,9" fill="none"/>
<!-- 腮红 -->
<ellipse cx="-14" cy="4" rx="4" ry="3" fill="#FFB6C1" opacity="0.5" stroke="none"/>
<ellipse cx="14" cy="4" rx="4" ry="3" fill="#FFB6C1" opacity="0.5" stroke="none"/>
<!-- 胡须 -->
<path d="M-24,0 L-12,2"/>
<path d="M-24,6 L-12,5"/>
<path d="M24,0 L12,2"/>
<path d="M24,6 L12,5"/>
</g>
</svg>
`;
// 创建悬浮球
export function createFloatingBall() {
// 检查是否已存在
if (document.getElementById('wechat-floating-ball')) {
return;
}
const ball = document.createElement('div');
ball.id = 'wechat-floating-ball';
ball.className = 'wechat-floating-ball';
ball.innerHTML = FLOATING_BALL_SVG;
document.body.appendChild(ball);
// 恢复位置
restorePosition(ball);
// 绑定事件
bindFloatingBallEvents(ball);
// 根据主界面状态设置悬浮球可见性
updateFloatingBallVisibility();
return ball;
}
// 恢复保存的位置
function restorePosition(ball) {
const settings = getSettings();
const savedPos = settings.floatingBallPosition;
if (savedPos && savedPos.x !== undefined && savedPos.y !== undefined) {
// 确保位置在视口内
const maxX = window.innerWidth - 60;
const maxY = window.innerHeight - 60;
floatingBallState.currentX = Math.min(Math.max(0, savedPos.x), maxX);
floatingBallState.currentY = Math.min(Math.max(0, savedPos.y), maxY);
} else {
// 默认位置:右侧中间
floatingBallState.currentX = window.innerWidth - 80;
floatingBallState.currentY = (window.innerHeight - 60) / 2;
}
ball.style.left = floatingBallState.currentX + 'px';
ball.style.top = floatingBallState.currentY + 'px';
}
// 保存位置
function savePosition() {
const settings = getSettings();
settings.floatingBallPosition = {
x: floatingBallState.currentX,
y: floatingBallState.currentY
};
requestSave();
}
// 绑定事件
function bindFloatingBallEvents(ball) {
// 鼠标事件
ball.addEventListener('mousedown', onDragStart);
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
// 触摸事件
ball.addEventListener('touchstart', onDragStart, { passive: false });
document.addEventListener('touchmove', onDragMove, { passive: false });
document.addEventListener('touchend', onDragEnd);
// 窗口大小变化时调整位置
window.addEventListener('resize', () => {
const maxX = window.innerWidth - 60;
const maxY = window.innerHeight - 60;
if (floatingBallState.currentX > maxX) {
floatingBallState.currentX = maxX;
ball.style.left = floatingBallState.currentX + 'px';
}
if (floatingBallState.currentY > maxY) {
floatingBallState.currentY = maxY;
ball.style.top = floatingBallState.currentY + 'px';
}
});
}
// 开始拖拽
function onDragStart(e) {
const ball = document.getElementById('wechat-floating-ball');
if (!ball) return;
floatingBallState.isDragging = true;
floatingBallState.hasMoved = false;
// 获取起始位置
if (e.type === 'touchstart') {
floatingBallState.startX = e.touches[0].clientX;
floatingBallState.startY = e.touches[0].clientY;
e.preventDefault();
} else {
floatingBallState.startX = e.clientX;
floatingBallState.startY = e.clientY;
}
floatingBallState.initialX = floatingBallState.currentX;
floatingBallState.initialY = floatingBallState.currentY;
ball.classList.add('dragging');
}
// 拖拽移动
function onDragMove(e) {
if (!floatingBallState.isDragging) return;
const ball = document.getElementById('wechat-floating-ball');
if (!ball) return;
let clientX, clientY;
if (e.type === 'touchmove') {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
e.preventDefault();
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const deltaX = clientX - floatingBallState.startX;
const deltaY = clientY - floatingBallState.startY;
// 如果移动距离超过5px认为是拖拽而非点击
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
floatingBallState.hasMoved = true;
}
// 计算新位置
let newX = floatingBallState.initialX + deltaX;
let newY = floatingBallState.initialY + deltaY;
// 限制在视口内
const maxX = window.innerWidth - 60;
const maxY = window.innerHeight - 60;
newX = Math.min(Math.max(0, newX), maxX);
newY = Math.min(Math.max(0, newY), maxY);
floatingBallState.currentX = newX;
floatingBallState.currentY = newY;
ball.style.left = newX + 'px';
ball.style.top = newY + 'px';
}
// 结束拖拽
function onDragEnd(e) {
if (!floatingBallState.isDragging) return;
const ball = document.getElementById('wechat-floating-ball');
if (ball) {
ball.classList.remove('dragging');
}
floatingBallState.isDragging = false;
// 如果没有移动,视为点击
if (!floatingBallState.hasMoved) {
toggleMainInterface();
} else {
// 保存位置
savePosition();
}
}
// 切换主界面显示
function toggleMainInterface() {
const phone = document.getElementById('wechat-phone');
if (!phone) return;
const isHidden = phone.classList.contains('hidden');
if (isHidden) {
phone.classList.remove('hidden');
} else {
phone.classList.add('hidden');
}
// 更新设置
const settings = getSettings();
settings.phoneVisible = isHidden;
requestSave();
// 更新悬浮球状态
updateFloatingBallVisibility();
}
// 更新悬浮球可见性(主界面显示时隐藏悬浮球,反之显示)
export function updateFloatingBallVisibility() {
const ball = document.getElementById('wechat-floating-ball');
const phone = document.getElementById('wechat-phone');
if (!ball) return;
// 主界面隐藏时显示悬浮球,主界面显示时也显示悬浮球(方便用户随时关闭)
ball.style.display = 'flex';
}
// 显示悬浮球
export function showFloatingBall() {
const ball = document.getElementById('wechat-floating-ball');
if (ball) {
ball.style.display = 'flex';
}
}
// 隐藏悬浮球
export function hideFloatingBall() {
const ball = document.getElementById('wechat-floating-ball');
if (ball) {
ball.style.display = 'none';
}
}
// 销毁悬浮球
export function destroyFloatingBall() {
const ball = document.getElementById('wechat-floating-ball');
if (ball) {
ball.remove();
}
}

View File

@@ -349,8 +349,8 @@ export function checkGiftDelivery(contact) {
const currentCount = contact.chatHistory?.length || 0; const currentCount = contact.chatHistory?.length || 0;
for (const gift of contact.pendingGifts) { for (const gift of contact.pendingGifts) {
// 如果正在使用中,跳过 // 如果正在使用中或已完成,跳过
if (gift.isUsing) continue; if (gift.isUsing || gift.completed) continue;
// 首次送达检测 // 首次送达检测
if (!gift.isDelivered && currentCount >= gift.startMessageCount + 25) { if (!gift.isDelivered && currentCount >= gift.startMessageCount + 25) {
@@ -415,6 +415,10 @@ export function showGiftArrivalModal(gift, contact) {
yesBtn.removeEventListener('click', handleYes); yesBtn.removeEventListener('click', handleYes);
noBtn.removeEventListener('click', handleNo); noBtn.removeEventListener('click', handleNo);
// 标记礼物为已完成,防止重复触发弹窗
gift.completed = true;
requestSave();
// 打开玩具控制界面 // 打开玩具控制界面
const { showToyControlPage } = await import('./toy-control.js'); const { showToyControlPage } = await import('./toy-control.js');
showToyControlPage(gift, contact, currentChatIndex); showToyControlPage(gift, contact, currentChatIndex);

View File

@@ -150,8 +150,8 @@ export function refreshHistoryList(filter = 'all') {
<div class="wechat-history-item" data-index="${lb.originalIndex}" style="padding: 12px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;"> <div class="wechat-history-item" data-index="${lb.originalIndex}" style="padding: 12px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
<div style="display: flex; align-items: center; gap: 10px;"> <div style="display: flex; align-items: center; gap: 10px;">
<div style="flex: 1; min-width: 0;"> <div style="flex: 1; min-width: 0;">
<div style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #000;">${escapeHtml(displayName)}</div> <div style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--wechat-text-primary);">${escapeHtml(displayName)}</div>
<div style="font-size: 12px; color: #000;">${entriesCount} 杯总结 · ${lb.lastUpdated || lb.addedTime || '未知时间'}</div> <div style="font-size: 12px; color: var(--wechat-text-secondary);">${entriesCount} 杯总结 · ${lb.lastUpdated || lb.addedTime || '未知时间'}</div>
</div> </div>
<label class="wechat-toggle wechat-toggle-small" onclick="event.stopPropagation()"> <label class="wechat-toggle wechat-toggle-small" onclick="event.stopPropagation()">
<input type="checkbox" class="wechat-history-toggle" data-index="${lb.originalIndex}" ${lb.enabled !== false ? 'checked' : ''}> <input type="checkbox" class="wechat-history-toggle" data-index="${lb.originalIndex}" ${lb.enabled !== false ? 'checked' : ''}>
@@ -339,7 +339,6 @@ export function renderToyHistory(contact) {
contentEl.innerHTML = sortedHistory.map((session, sortedIdx) => { contentEl.innerHTML = sortedHistory.map((session, sortedIdx) => {
const targetText = session.target === 'character' ? 'TA在用' : '你在用'; const targetText = session.target === 'character' ? 'TA在用' : '你在用';
const messages = session.messages || []; const messages = session.messages || [];
const previewMessages = messages.slice(0, 5); // 只显示前5条消息预览
const originalIndex = toyHistory.indexOf(session); const originalIndex = toyHistory.indexOf(session);
return ` return `
@@ -350,7 +349,7 @@ export function renderToyHistory(contact) {
<span class="wechat-toy-history-card-gift-name">${escapeHtml(session.gift?.name || '未知玩具')}</span> <span class="wechat-toy-history-card-gift-name">${escapeHtml(session.gift?.name || '未知玩具')}</span>
</div> </div>
<div class="wechat-toy-history-card-actions"> <div class="wechat-toy-history-card-actions">
<span class="wechat-toy-history-card-target">${targetText}</span> <span class="wechat-toy-history-card-target">${targetText}<button class="wechat-toy-target-close-btn" data-tab="toy" data-index="${originalIndex}" title="删除">×</button></span>
<button class="wechat-history-delete-btn" data-tab="toy" data-index="${originalIndex}" title="删除">×</button> <button class="wechat-history-delete-btn" data-tab="toy" data-index="${originalIndex}" title="删除">×</button>
</div> </div>
</div> </div>
@@ -358,16 +357,15 @@ export function renderToyHistory(contact) {
<span>${escapeHtml(session.time || '未知时间')}</span> <span>${escapeHtml(session.time || '未知时间')}</span>
<span>时长 ${escapeHtml(session.duration || '00:00')}</span> <span>时长 ${escapeHtml(session.duration || '00:00')}</span>
</div> </div>
<div class="wechat-toy-history-card-messages"> <div class="wechat-toy-history-card-messages wechat-toy-history-scrollable">
${previewMessages.length === 0 ? '<div style="color: #999; text-align: center;">暂无对话记录</div>' : ${messages.length === 0 ? '<div style="color: #999; text-align: center;">暂无对话记录</div>' :
previewMessages.map(msg => ` messages.map(msg => `
<div class="wechat-toy-history-msg"> <div class="wechat-toy-history-msg">
<span class="wechat-toy-history-msg-sender ${msg.role === 'user' ? 'user' : 'ai'}">${msg.role === 'user' ? '你' : 'TA'}:</span> <span class="wechat-toy-history-msg-sender ${msg.role === 'user' ? 'user' : 'ai'}">${msg.role === 'user' ? '你' : 'TA'}:</span>
<span class="wechat-toy-history-msg-content">${escapeHtml((msg.content || '').substring(0, 50))}${(msg.content?.length || 0) > 50 ? '...' : ''}</span> <span class="wechat-toy-history-msg-content">${escapeHtml(msg.content || '')}</span>
</div> </div>
`).join('') `).join('')
} }
${messages.length > 5 ? `<div style="color: #ff6b8a; font-size: 12px; text-align: center; margin-top: 8px;">还有 ${messages.length - 5} 条消息...</div>` : ''}
</div> </div>
</div> </div>
`; `;

View File

@@ -143,6 +143,13 @@ function showListenTogetherPage() {
const page = document.getElementById('wechat-listen-together-page'); const page = document.getElementById('wechat-listen-together-page');
if (!page) return; if (!page) return;
// 清空上次的聊天消息 DOM
const messagesEl = document.getElementById('wechat-listen-messages');
if (messagesEl) {
messagesEl.innerHTML = '';
messagesEl.classList.add('hidden');
}
const settings = getSettings(); const settings = getSettings();
const contact = listenState.contact; const contact = listenState.contact;
const song = listenState.currentSong; const song = listenState.currentSong;

262
main.js
View File

@@ -15,7 +15,7 @@ import { ICON_SUCCESS, ICON_INFO } from './icons.js';
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js'; import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js'; import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js'; import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats } from './summary.js'; import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats, recoverFromTavernWorldbook } from './summary.js';
import { fetchModelListFromApi } from './ai.js'; import { fetchModelListFromApi } from './ai.js';
import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST } from './character-import.js'; import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST } from './character-import.js';
@@ -36,6 +36,7 @@ import { initTransferEvents } from './transfer.js';
import { initGroupRedPacket } from './group-red-packet.js'; import { initGroupRedPacket } from './group-red-packet.js';
import { initGiftEvents } from './gift.js'; import { initGiftEvents } from './gift.js';
import { initCropper } from './cropper.js'; import { initCropper } from './cropper.js';
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
// ========== 历史记录功能 ========== // ========== 历史记录功能 ==========
let currentHistoryTab = 'listen'; let currentHistoryTab = 'listen';
@@ -125,6 +126,14 @@ function renderHistoryContent(contact, tabType) {
deleteHistoryRecord('toy', index); deleteHistoryRecord('toy', index);
}); });
}); });
// 绑定标签内的叉叉按钮事件
contentEl.querySelectorAll('.wechat-toy-target-close-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
deleteHistoryRecord('toy', index);
});
});
return; return;
} }
@@ -331,7 +340,241 @@ function updateWalletAmountDisplay() {
amountEl.textContent = amount.startsWith('¥') ? amount : `${amount}`; amountEl.textContent = amount.startsWith('¥') ? amount : `${amount}`;
} }
// ===== 缩小/恢复手机功能 =====
let minimizeState = {
isDragging: false,
startX: 0,
startY: 0,
initialLeft: 0,
initialTop: 0,
hasMoved: false
};
// 悬浮窗开关
function toggleFloatingBallEnabled() {
const settings = getSettings();
const isEnabled = settings.floatingBallEnabled !== false;
if (isEnabled) {
// 关闭悬浮窗
settings.floatingBallEnabled = false;
hideFloatingBall();
updateFloatingBallMenuText(false);
} else {
// 开启悬浮窗
settings.floatingBallEnabled = true;
// 只有非缩小状态才显示
const phone = document.getElementById('wechat-phone');
if (!phone?.classList.contains('minimized')) {
showFloatingBall();
}
updateFloatingBallMenuText(true);
}
requestSave();
}
function updateFloatingBallMenuText(enabled) {
const textEl = document.getElementById('wechat-floating-ball-text');
if (textEl) {
textEl.textContent = enabled ? '关闭悬浮窗' : '开启悬浮窗';
}
}
function setupPhoneMinimize() {
const phone = document.getElementById('wechat-phone');
const minimizeBtn = document.getElementById('wechat-minimize-btn');
if (!phone || !minimizeBtn) return;
// 点击右上角图标 - 缩小 (PC)
minimizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
minimizePhone();
});
// 移动端触摸支持
let minimizeBtnTouchMoved = false;
minimizeBtn.addEventListener('touchstart', (e) => {
minimizeBtnTouchMoved = false;
}, { passive: true });
minimizeBtn.addEventListener('touchmove', (e) => {
minimizeBtnTouchMoved = true;
}, { passive: true });
minimizeBtn.addEventListener('touchend', (e) => {
if (!minimizeBtnTouchMoved) {
e.stopPropagation();
e.preventDefault();
minimizePhone();
}
});
// 缩小后点击恢复 + 拖动支持
phone.addEventListener('mousedown', onMinimizedDragStart);
document.addEventListener('mousemove', onMinimizedDragMove);
document.addEventListener('mouseup', onMinimizedDragEnd);
// 触摸支持
phone.addEventListener('touchstart', onMinimizedDragStart, { passive: false });
document.addEventListener('touchmove', onMinimizedDragMove, { passive: false });
document.addEventListener('touchend', onMinimizedDragEnd);
}
function minimizePhone() {
const phone = document.getElementById('wechat-phone');
if (!phone) return;
// 获取当前位置
const rect = phone.getBoundingClientRect();
const settings = getSettings();
// 保存原始位置
if (!settings.phoneOriginalPosition) {
settings.phoneOriginalPosition = {
left: phone.style.left || rect.left + 'px',
top: phone.style.top || rect.top + 'px'
};
}
// 缩小后移到右下角
const scale = 0.25;
const phoneWidth = rect.width * scale;
const phoneHeight = rect.height * scale;
// 使用保存的缩小位置或默认右下角
const savedMinPos = settings.phoneMinimizedPosition;
let targetLeft, targetTop;
if (savedMinPos) {
targetLeft = savedMinPos.left;
targetTop = savedMinPos.top;
} else {
targetLeft = window.innerWidth - phoneWidth - 20;
targetTop = window.innerHeight - phoneHeight - 20;
}
phone.style.left = targetLeft + 'px';
phone.style.top = targetTop + 'px';
phone.style.right = 'auto';
phone.style.bottom = 'auto';
phone.classList.add('minimized');
// 缩小时隐藏悬浮球
hideFloatingBall();
requestSave();
}
function restorePhone() {
const phone = document.getElementById('wechat-phone');
if (!phone) return;
const settings = getSettings();
phone.classList.remove('minimized');
// 恢复原始位置或居中
if (settings.phoneOriginalPosition) {
phone.style.left = settings.phoneOriginalPosition.left;
phone.style.top = settings.phoneOriginalPosition.top;
}
// 恢复时根据设置显示悬浮球
if (settings.floatingBallEnabled !== false) {
showFloatingBall();
}
requestSave();
}
function onMinimizedDragStart(e) {
const phone = document.getElementById('wechat-phone');
if (!phone || !phone.classList.contains('minimized')) return;
minimizeState.isDragging = true;
minimizeState.hasMoved = false;
const rect = phone.getBoundingClientRect();
// 缩小状态下需要考虑缩放后的实际位置
minimizeState.initialLeft = parseFloat(phone.style.left) || rect.left;
minimizeState.initialTop = parseFloat(phone.style.top) || rect.top;
if (e.type === 'touchstart') {
minimizeState.startX = e.touches[0].clientX;
minimizeState.startY = e.touches[0].clientY;
e.preventDefault();
} else {
minimizeState.startX = e.clientX;
minimizeState.startY = e.clientY;
}
phone.style.transition = 'none';
}
function onMinimizedDragMove(e) {
if (!minimizeState.isDragging) return;
const phone = document.getElementById('wechat-phone');
if (!phone || !phone.classList.contains('minimized')) return;
let clientX, clientY;
if (e.type === 'touchmove') {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
e.preventDefault();
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const deltaX = clientX - minimizeState.startX;
const deltaY = clientY - minimizeState.startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
minimizeState.hasMoved = true;
}
const newLeft = minimizeState.initialLeft + deltaX;
const newTop = minimizeState.initialTop + deltaY;
phone.style.left = newLeft + 'px';
phone.style.top = newTop + 'px';
}
function onMinimizedDragEnd(e) {
if (!minimizeState.isDragging) return;
const phone = document.getElementById('wechat-phone');
minimizeState.isDragging = false;
if (phone) {
phone.style.transition = '';
}
if (!minimizeState.hasMoved) {
// 没有移动,视为点击 - 恢复
restorePhone();
} else {
// 移动了,保存位置
if (phone && phone.classList.contains('minimized')) {
const settings = getSettings();
settings.phoneMinimizedPosition = {
left: parseFloat(phone.style.left),
top: parseFloat(phone.style.top)
};
requestSave();
}
}
}
function bindEvents() { function bindEvents() {
// ===== 缩小/恢复手机功能 =====
setupPhoneMinimize();
// 添加按钮 - 显示下拉菜单 // 添加按钮 - 显示下拉菜单
document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => { document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -369,6 +612,12 @@ function bindEvents() {
}); });
}); });
// 下拉菜单 - 悬浮窗开关
document.getElementById('wechat-menu-floating-ball')?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
toggleFloatingBallEnabled();
});
// ===== 群聊创建弹窗事件 ===== // ===== 群聊创建弹窗事件 =====
document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal); document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal);
document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat); document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat);
@@ -1330,6 +1579,9 @@ function bindEvents() {
rollbackSummary(); rollbackSummary();
}); });
// 暴露恢复函数到全局,可在控制台调用: window.keleRecoverSummary()
window.keleRecoverSummary = recoverFromTavernWorldbook;
document.getElementById('wechat-summary-close')?.addEventListener('click', () => { document.getElementById('wechat-summary-close')?.addEventListener('click', () => {
document.getElementById('wechat-summary-panel')?.classList.add('hidden'); document.getElementById('wechat-summary-panel')?.classList.add('hidden');
}); });
@@ -1933,6 +2185,14 @@ function init() {
// 首次可见时居中 // 首次可见时居中
centerPhoneInViewport({ force: true }); centerPhoneInViewport({ force: true });
// 初始化悬浮球
createFloatingBall();
// 根据设置决定是否显示
if (settings.floatingBallEnabled === false) {
hideFloatingBall();
}
updateFloatingBallMenuText(settings.floatingBallEnabled !== false);
console.log('✅ 可乐不加冰 已加载'); console.log('✅ 可乐不加冰 已加载');
} }

View File

@@ -19,7 +19,7 @@ export function generatePhoneHTML() {
<!-- 状态栏 --> <!-- 状态栏 -->
<div class="wechat-statusbar"> <div class="wechat-statusbar">
<span class="wechat-statusbar-time">${getCurrentTime()}</span> <span class="wechat-statusbar-time">${getCurrentTime()}</span>
<div class="wechat-statusbar-icons"> <div class="wechat-statusbar-icons" id="wechat-minimize-btn" title="缩小窗口">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" fill="currentColor"/></svg> <svg viewBox="0 0 24 24" width="18" height="18"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" fill="currentColor"/></svg>
<svg viewBox="0 0 24 24" width="22" height="22"><rect x="2" y="6" width="18" height="12" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="20" y="10" width="2" height="4" fill="currentColor"/><rect x="4" y="8" width="12" height="8" rx="1" fill="currentColor"/></svg> <svg viewBox="0 0 24 24" width="22" height="22"><rect x="2" y="6" width="18" height="12" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="20" y="10" width="2" height="4" fill="currentColor"/><rect x="4" y="8" width="12" height="8" rx="1" fill="currentColor"/></svg>
</div> </div>
@@ -114,6 +114,10 @@ export function generatePhoneHTML() {
<svg viewBox="0 0 24 24"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg> <svg viewBox="0 0 24 24"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
<span>收付款</span> <span>收付款</span>
</div> </div>
<div class="wechat-dropdown-item" id="wechat-menu-floating-ball">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M7 16.5c0-2 2.2-3.5 5-3.5s5 1.5 5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
<span id="wechat-floating-ball-text">悬浮窗</span>
</div>
</div> </div>
<!-- 添加朋友页面 --> <!-- 添加朋友页面 -->

215
style.css
View File

@@ -4391,9 +4391,18 @@
box-sizing: border-box; box-sizing: border-box;
} }
/* 确保照片气泡不被其他样式折叠 */ /* 确保照片气泡不被其他样式折叠,强制垂直排列 */
.wechat-message-content .wechat-photo-bubble { .wechat-message-content .wechat-photo-bubble {
display: block !important; display: block !important;
flex-shrink: 0;
width: 180px !important;
float: none !important;
}
/* 确保消息内容区域的子元素垂直排列 */
.wechat-message-content {
flex-wrap: nowrap !important;
flex-direction: column !important;
} }
.wechat-photo-blur { .wechat-photo-blur {
@@ -4737,7 +4746,7 @@
/* 通话中对话框 */ /* 通话中对话框 */
.wechat-voice-call-chat { .wechat-voice-call-chat {
margin: 0 16px; margin: 0 16px 15px 16px;
background: transparent; background: transparent;
padding: 10px 0; padding: 10px 0;
display: flex; display: flex;
@@ -4836,6 +4845,7 @@
background: rgba(50, 50, 50, 0.8); background: rgba(50, 50, 50, 0.8);
border-radius: 20px; border-radius: 20px;
padding: 4px 4px 4px 16px; padding: 4px 4px 4px 16px;
margin-top: auto;
} }
.wechat-voice-call-input { .wechat-voice-call-input {
@@ -6888,7 +6898,7 @@
.wechat-moment-images.grid-2 { .wechat-moment-images.grid-2 {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
max-width: 280px; max-width: 200px;
} }
.wechat-moment-images.grid-3, .wechat-moment-images.grid-3,
@@ -6974,6 +6984,19 @@
border-radius: 8px; border-radius: 8px;
} }
/* 当朋友圈图片区域只包含照片描述卡片时,使用垂直布局 */
.wechat-moment-images:has(.wechat-moment-photo-card) {
display: flex !important;
flex-direction: column !important;
gap: 8px;
max-width: 180px;
}
.wechat-moment-images:has(.wechat-moment-photo-card) .wechat-moment-photo-card {
width: 100%;
max-width: 180px;
}
.wechat-moment-photo-card .wechat-photo-blur { .wechat-moment-photo-card .wechat-photo-blur {
position: absolute; position: absolute;
top: 8px; top: 8px;
@@ -11284,6 +11307,7 @@
/* 聊天区域 */ /* 聊天区域 */
.wechat-toy-control-chat { .wechat-toy-control-chat {
flex: 1; flex: 1;
min-height: 0;
overflow-y: auto; overflow-y: auto;
background: rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.6);
margin: 0 10px; margin: 0 10px;
@@ -11291,6 +11315,25 @@
box-shadow: inset 0 2px 8px rgba(255, 107, 138, 0.1); box-shadow: inset 0 2px 8px rgba(255, 107, 138, 0.1);
} }
/* 聊天区域滚动条样式 */
.wechat-toy-control-chat::-webkit-scrollbar {
width: 6px;
}
.wechat-toy-control-chat::-webkit-scrollbar-track {
background: rgba(255, 107, 138, 0.1);
border-radius: 3px;
}
.wechat-toy-control-chat::-webkit-scrollbar-thumb {
background: rgba(255, 107, 138, 0.4);
border-radius: 3px;
}
.wechat-toy-control-chat::-webkit-scrollbar-thumb:hover {
background: rgba(255, 107, 138, 0.6);
}
.wechat-toy-control-messages { .wechat-toy-control-messages {
padding: 15px; padding: 15px;
display: flex; display: flex;
@@ -11416,6 +11459,18 @@
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
} }
.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-track {
background: rgba(255, 107, 138, 0.05);
}
.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-thumb {
background: rgba(255, 107, 138, 0.3);
}
.wechat-dark .wechat-toy-control-chat::-webkit-scrollbar-thumb:hover {
background: rgba(255, 107, 138, 0.5);
}
.wechat-dark .wechat-toy-control-msg.ai { .wechat-dark .wechat-toy-control-msg.ai {
background: #2a2a2a; background: #2a2a2a;
color: #e9e9e9; color: #e9e9e9;
@@ -11500,6 +11555,25 @@
padding: 2px 8px; padding: 2px 8px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border-radius: 10px; border-radius: 10px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.wechat-toy-target-close-btn {
background: none;
border: none;
cursor: pointer;
font-size: 12px;
color: inherit;
padding: 0;
line-height: 1;
opacity: 0.7;
margin-left: 2px;
}
.wechat-toy-target-close-btn:hover {
opacity: 1;
} }
.wechat-toy-history-card-meta { .wechat-toy-history-card-meta {
@@ -11514,11 +11588,30 @@
.wechat-toy-history-card-messages { .wechat-toy-history-card-messages {
padding: 10px 14px; padding: 10px 14px;
max-height: 200px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
} }
/* 心动瞬间消息滚动条样式 */
.wechat-toy-history-scrollable::-webkit-scrollbar {
width: 6px;
}
.wechat-toy-history-scrollable::-webkit-scrollbar-track {
background: rgba(255, 107, 138, 0.1);
border-radius: 3px;
}
.wechat-toy-history-scrollable::-webkit-scrollbar-thumb {
background: rgba(255, 107, 138, 0.4);
border-radius: 3px;
}
.wechat-toy-history-scrollable::-webkit-scrollbar-thumb:hover {
background: rgba(255, 107, 138, 0.6);
}
.wechat-toy-history-msg { .wechat-toy-history-msg {
margin-bottom: 8px; margin-bottom: 8px;
font-size: 13px; font-size: 13px;
@@ -11567,3 +11660,117 @@
.wechat-dark .wechat-toy-history-msg-content { .wechat-dark .wechat-toy-history-msg-content {
color: #ddd; color: #ddd;
} }
/* ===== 悬浮球样式 ===== */
.wechat-floating-ball {
position: fixed;
width: 60px;
height: 60px;
z-index: 99999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
user-select: none;
-webkit-user-select: none;
touch-action: none;
transition: transform 0.15s ease, opacity 0.15s ease;
}
.wechat-floating-ball:hover {
transform: scale(1.08);
}
.wechat-floating-ball:active,
.wechat-floating-ball.dragging {
transform: scale(0.95);
opacity: 0.9;
}
.wechat-floating-ball .floating-ball-svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 2px 8px rgba(255, 182, 193, 0.4));
transition: filter 0.2s ease;
}
.wechat-floating-ball:hover .floating-ball-svg {
filter: drop-shadow(0 4px 12px rgba(255, 182, 193, 0.6));
}
/* 悬浮球呼吸动画(可选,可删除) */
@keyframes floating-ball-breathe {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
}
.wechat-floating-ball:not(:hover):not(.dragging) {
animation: floating-ball-breathe 3s ease-in-out infinite;
}
/* ===== 缩小手机样式 ===== */
#wechat-minimize-btn {
cursor: pointer;
padding: 4px 8px;
margin: -4px -8px;
border-radius: 4px;
transition: background 0.2s ease;
}
#wechat-minimize-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.wechat-dark #wechat-minimize-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
/* 缩小状态 */
.wechat-phone.minimized {
transform: scale(0.25) !important;
transform-origin: center center;
position: fixed !important;
cursor: pointer;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease,
top 0.3s ease,
left 0.3s ease,
right 0.3s ease;
z-index: 99998;
border-radius: 20px;
overflow: hidden;
touch-action: none;
}
.wechat-phone.minimized:hover {
transform: scale(0.27) !important;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
}
/* 缩小时禁用内部交互,但允许事件冒泡到父元素 */
.wechat-phone.minimized::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99999;
cursor: pointer;
}
.wechat-phone.minimized * {
pointer-events: none !important;
}
/* 缩小时的过渡动画 */
.wechat-phone {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease;
}

View File

@@ -852,3 +852,106 @@ export async function rollbackSummary() {
} }
} }
} }
/**
* 从酒馆世界书恢复总结数据
* 当插件的 selectedLorebooks 条目丢失但酒馆世界书还在时使用
*/
export async function recoverFromTavernWorldbook() {
const settings = getSettings();
const selectedLorebooks = settings.selectedLorebooks || [];
// 找到所有总结生成的世界书(条目为空的)
const emptyBooks = selectedLorebooks.filter(lb =>
(lb.fromSummary === true || (lb.name && lb.name.startsWith(LOREBOOK_NAME_PREFIX))) &&
(!lb.entries || lb.entries.length === 0)
);
if (emptyBooks.length === 0) {
alert('没有需要恢复的世界书(所有世界书都有条目,或没有总结类世界书)');
return;
}
const options = emptyBooks.map((lb, idx) => `${idx + 1}. ${lb.name}`).join('\n');
const choice = prompt(`以下世界书条目为空,可尝试从酒馆恢复:\n\n${options}\n\n输入序号(或输入 all 恢复全部):`);
if (!choice) return;
const booksToRecover = choice.toLowerCase() === 'all'
? emptyBooks
: [emptyBooks[parseInt(choice) - 1]].filter(Boolean);
if (booksToRecover.length === 0) {
alert('无效的选择');
return;
}
let recoveredCount = 0;
let totalEntries = 0;
for (const book of booksToRecover) {
try {
const name = book.name;
// 检查酒馆世界书是否存在
const worldExists = typeof world_names !== 'undefined' &&
Array.isArray(world_names) &&
world_names.includes(name);
if (!worldExists) {
console.log(`[可乐] 酒馆中不存在世界书: ${name}`);
continue;
}
// 加载酒馆世界书
if (typeof loadWorldInfo !== 'function') {
console.error('[可乐] loadWorldInfo 函数不可用');
continue;
}
const worldInfo = await loadWorldInfo(name);
if (!worldInfo?.entries || Object.keys(worldInfo.entries).length === 0) {
console.log(`[可乐] 酒馆世界书 ${name} 没有条目`);
continue;
}
// 将酒馆条目转换为插件格式
const entries = [];
const sortedKeys = Object.keys(worldInfo.entries).sort((a, b) => parseInt(a) - parseInt(b));
for (const key of sortedKeys) {
const tavernEntry = worldInfo.entries[key];
if (!tavernEntry) continue;
entries.push({
content: tavernEntry.content || '',
comment: tavernEntry.comment || getCupName(entries.length + 1),
keys: tavernEntry.key || [],
enabled: !tavernEntry.disable,
addedTime: new Date().toISOString()
});
}
if (entries.length > 0) {
// 更新插件的 selectedLorebooks
const bookIndex = selectedLorebooks.findIndex(lb => lb.name === name);
if (bookIndex >= 0) {
selectedLorebooks[bookIndex].entries = entries;
selectedLorebooks[bookIndex].lastUpdated = new Date().toISOString();
recoveredCount++;
totalEntries += entries.length;
console.log(`[可乐] 已恢复 ${name}: ${entries.length}`);
}
}
} catch (err) {
console.error(`[可乐] 恢复 ${book.name} 失败:`, err);
}
}
if (recoveredCount > 0) {
requestSave();
alert(`恢复完成!\n\n已恢复 ${recoveredCount} 个世界书,共 ${totalEntries} 条总结。\n\n请刷新页面查看。`);
} else {
alert('恢复失败:酒馆中没有找到对应的世界书数据。\n\n数据可能已彻底丢失。');
}
}

View File

@@ -255,7 +255,7 @@ async function onButtonPress(buttonId, pressedBy = 'user') {
hideToyTypingIndicator(); hideToyTypingIndicator();
if (response) { if (response) {
processAIResponse(response); await processAIResponse(response);
} }
} catch (err) { } catch (err) {
hideToyTypingIndicator(); hideToyTypingIndicator();
@@ -290,7 +290,7 @@ function updateButtonState(buttonId) {
// 构建按钮按下提示词 // 构建按钮按下提示词
function buildButtonPressPrompt(buttonId, buttonName, pressedBy) { function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
const isCharacterUsing = toyControlState.target === 'character'; const isCharacterUsing = toyControlState.target === 'character';
const presserText = pressedBy === 'user' ? '用户' : '你自己'; const isAIPress = pressedBy === 'ai';
const modeEffects = { const modeEffects = {
classic: '稳定持续的震动开始了', classic: '稳定持续的震动开始了',
@@ -301,12 +301,33 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
shock: '一阵微电流刺激瞬间传来,让人猛地一颤' shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
}; };
let prompt = `[${presserText}按下了"${buttonName}"按钮] let prompt;
if (isAIPress) {
// AI主动按按钮的情况
prompt = `[你主动按下了"${buttonName}"按钮]
效果:${modeEffects[buttonId]} 效果:${modeEffects[buttonId]}
`; `;
if (isCharacterUsing) {
prompt += `你主动切换了${toyControlState.gift.giftName}的模式,请描述你主动调整后的反应:
- 为什么要主动切换这个模式(想要更多刺激/受不了想暂停/想换个感觉等)
- 切换后的身体感受和情绪变化
- 回复要有情感细节,符合你的角色性格`;
} else {
prompt += `你主动控制了用户正在使用的${toyControlState.gift.giftName},请描述你主动操作后的感受:
- 为什么要主动给用户切换这个模式(想折磨对方/想看对方的反应/调侃等)
- 可以调侃、挑逗用户
- 回复要有趣,符合你的角色性格`;
}
} else {
// 用户按按钮的情况
prompt = `[用户按下了"${buttonName}"按钮]
效果:${modeEffects[buttonId]}
`;
if (isCharacterUsing) { if (isCharacterUsing) {
prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。 prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。
描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`; 描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`;
@@ -314,6 +335,7 @@ function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。 prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。
请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`; 请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`;
} }
}
prompt += ` prompt += `
@@ -351,7 +373,7 @@ async function sendToyMessage() {
hideToyTypingIndicator(); hideToyTypingIndicator();
if (response) { if (response) {
processAIResponse(response); await processAIResponse(response);
} }
} catch (err) { } catch (err) {
hideToyTypingIndicator(); hideToyTypingIndicator();
@@ -416,15 +438,20 @@ async function callToyAI(prompt) {
return await callAI(toyControlState.contact, prompt, historyMessages); return await callAI(toyControlState.contact, prompt, historyMessages);
} }
// 辅助函数:延迟
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 处理AI回复检测是否有按按钮指令 // 处理AI回复检测是否有按按钮指令
function processAIResponse(response) { async function processAIResponse(response) {
if (!response) return; if (!response) return;
// 分割多条消息 // 分割多条消息
const parts = splitAIMessages(response); const parts = splitAIMessages(response);
for (const part of parts) { for (let i = 0; i < parts.length; i++) {
let reply = part.trim(); let reply = parts[i].trim();
// 过滤特殊标签 // 过滤特殊标签
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();
@@ -441,6 +468,13 @@ function processAIResponse(response) {
reply = reply.replace(/[^]*/g, '').trim(); reply = reply.replace(/[^]*/g, '').trim();
reply = reply.replace(/\([^)]*\)/g, '').trim(); reply = reply.replace(/\([^)]*\)/g, '').trim();
// 如果不是第一条消息显示typing并延迟
if (i > 0 && reply) {
showToyTypingIndicator();
await sleep(1500 + Math.random() * 1000); // 1.5-2.5秒延迟
hideToyTypingIndicator();
}
// 添加AI消息 // 添加AI消息
if (reply) { if (reply) {
addToyMessage('ai', reply); addToyMessage('ai', reply);
@@ -459,6 +493,13 @@ function processAIResponse(response) {
reply = reply.replace(/[^]*/g, '').trim(); reply = reply.replace(/[^]*/g, '').trim();
reply = reply.replace(/\([^)]*\)/g, '').trim(); reply = reply.replace(/\([^)]*\)/g, '').trim();
// 如果不是第一条消息显示typing并延迟
if (i > 0 && reply) {
showToyTypingIndicator();
await sleep(1500 + Math.random() * 1000); // 1.5-2.5秒延迟
hideToyTypingIndicator();
}
if (reply) { if (reply) {
addToyMessage('ai', reply); addToyMessage('ai', reply);
} }
@@ -527,7 +568,7 @@ async function triggerToyAIGreeting() {
hideToyTypingIndicator(); hideToyTypingIndicator();
if (response) { if (response) {
processAIResponse(response); await processAIResponse(response);
} }
} catch (err) { } catch (err) {
hideToyTypingIndicator(); hideToyTypingIndicator();

View File

@@ -351,6 +351,26 @@ export function checkVideoAIHangupAfterReply() {
return false; return false;
} }
// 检测AI是否有挂断意图
function detectVideoHangupIntent(text) {
if (!text) return false;
const hangupPatterns = [
/我(先)?挂了/,
/那我挂了/,
/先挂(了)?啊?/,
/挂了(啊|哈|呀|哦)?$/,
/我(要)?挂(电话|断)了/,
/拜拜.*挂/,
/挂.*拜拜/,
/再见.*挂/,
/不聊了.*挂/,
/不说了.*挂/,
/那就这样.*挂/,
/就这样吧.*挂/
];
return hangupPatterns.some(pattern => pattern.test(text));
}
// AI主动挂断视频电话 // AI主动挂断视频电话
function videoAIHangup() { function videoAIHangup() {
if (!videoCallState.isConnected) return; if (!videoCallState.isConnected) return;
@@ -505,7 +525,7 @@ function appendVideoCallRecordMessage(role, status, duration, contact) {
: firstChar); : firstChar);
// 摄像机图标 // 摄像机图标
const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> const cameraIconSVG = `<svg class="wechat-call-record-icon wechat-video-call-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="6" width="13" height="12" rx="2"/> <rect x="2" y="6" width="13" height="12" rx="2"/>
<path d="M22 8l-7 4 7 4V8z"/> <path d="M22 8l-7 4 7 4V8z"/>
</svg>`; </svg>`;
@@ -1053,7 +1073,19 @@ async function sendVideoCallMessage() {
} }
} }
// AI回复完成后检查是否要主动挂断5%概率通话30秒后生效 // AI回复完成后检查是否要主动挂断
// 1. 检测AI的挂断意图如"我挂了"、"先挂了"等)
const fullReply = parts.join(' ');
if (detectVideoHangupIntent(fullReply)) {
console.log('[可乐] 检测到视频通话AI挂断意图:', fullReply);
setTimeout(() => {
if (videoCallState.isConnected) {
videoAIHangup();
}
}, 1500 + Math.random() * 1000);
return;
}
// 2. 随机5%概率挂断通话30秒后生效
checkVideoAIHangupAfterReply(); checkVideoAIHangupAfterReply();
} catch (err) { } catch (err) {
hideVideoCallTypingIndicator(); hideVideoCallTypingIndicator();

View File

@@ -310,6 +310,27 @@ export function checkAIHangupAfterReply() {
return false; return false;
} }
// 检测AI是否有挂断意图
function detectHangupIntent(text) {
if (!text) return false;
// 常见的挂断表达
const hangupPatterns = [
/我(先)?挂了/,
/那我挂了/,
/先挂(了)?啊?/,
/挂了(啊|哈|呀|哦)?$/,
/我(要)?挂(电话|断)了/,
/拜拜.*挂/,
/挂.*拜拜/,
/再见.*挂/,
/不聊了.*挂/,
/不说了.*挂/,
/那就这样.*挂/,
/就这样吧.*挂/
];
return hangupPatterns.some(pattern => pattern.test(text));
}
// AI主动挂断电话 // AI主动挂断电话
function aiHangup() { function aiHangup() {
if (!callState.isConnected) return; if (!callState.isConnected) return;
@@ -489,7 +510,7 @@ function appendCallRecordMessage(role, status, duration, contact) {
// 通话记录卡片内容 // 通话记录卡片内容
// 线条电话图标 // 线条电话图标
const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> const phoneIconSVG = `<svg class="wechat-call-record-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/> <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>`; </svg>`;
@@ -956,7 +977,19 @@ async function sendCallMessage() {
} }
} }
// AI回复完成后检查是否要主动挂断5%概率通话30秒后生效 // AI回复完成后检查是否要主动挂断
// 1. 检测AI的挂断意图如"我挂了"、"先挂了"等)
const fullReply = parts.join(' ');
if (detectHangupIntent(fullReply)) {
console.log('[可乐] 检测到AI挂断意图:', fullReply);
setTimeout(() => {
if (callState.isConnected) {
aiHangup();
}
}, 1500 + Math.random() * 1000);
return;
}
// 2. 随机5%概率挂断通话30秒后生效
checkAIHangupAfterReply(); checkAIHangupAfterReply();
} catch (err) { } catch (err) {
hideCallTypingIndicator(); hideCallTypingIndicator();