`;
}
// 生成动态语音气泡
export function generateVoiceBubble(content, isSelf) {
- const seconds = calculateVoiceDuration(content);
+ const safeContent = (content || '').toString();
+ const seconds = calculateVoiceDuration(safeContent);
const width = Math.min(60 + seconds * 4, 200);
const uniqueId = 'voice_' + Math.random().toString(36).substring(2, 9);
- // WiFi信号样式的三条弧线图标(统一使用相同的SVG,通过CSS控制方向)
+ // WiFi信号样式的三条弧线图标(水平朝右,通过CSS控制翻转方向)
const wavesSvg = `
`;
// 用户消息:时长在左,波形在右
@@ -685,84 +921,31 @@ export function generateVoiceBubble(content, isSelf) {
return {
html: `
-
+
${bubbleInner}
-
${escapeHtml(content)}
+
${escapeHtml(safeContent)}
`,
id: uniqueId
};
}
-// 隐藏所有语音菜单
-function hideAllVoiceMenus() {
- document.querySelectorAll('.wechat-voice-menu.visible').forEach(menu => {
- menu.classList.remove('visible');
- });
- document.querySelectorAll('.wechat-voice-bubble[data-menu-open="true"]').forEach(bubble => {
- bubble.dataset.menuOpen = 'false';
- });
-}
-
-// 绑定语音气泡点击事件(播放动画)和长按菜单
+// 绑定语音气泡点击事件(播放动画 + 显示上方菜单)
export function bindVoiceBubbleEvents(container) {
const voiceBubbles = container.querySelectorAll('.wechat-voice-bubble:not([data-bound])');
voiceBubbles.forEach(bubble => {
bubble.setAttribute('data-bound', 'true');
- let longPressTimer = null;
- let isLongPress = false;
-
- // 获取父消息元素判断是否是用户消息
+ // 获取父消息元素
const messageEl = bubble.closest('.wechat-message');
- const isUserMessage = messageEl?.classList.contains('self');
- const voiceId = bubble.dataset.voiceId;
- // 长按开始
- const startLongPress = (e) => {
- isLongPress = false;
- longPressTimer = setTimeout(() => {
- isLongPress = true;
- if (isUserMessage) {
- showVoiceMenu(bubble, messageEl, voiceId);
- }
- }, 500);
- };
+ // 计算消息索引
+ const allMessages = Array.from(container.querySelectorAll('.wechat-message'));
+ const msgIndex = allMessages.indexOf(messageEl);
- // 长按取消
- const cancelLongPress = () => {
- if (longPressTimer) {
- clearTimeout(longPressTimer);
- longPressTimer = null;
- }
- };
-
- // 触摸事件
- bubble.addEventListener('touchstart', startLongPress, { passive: true });
- bubble.addEventListener('touchend', (e) => {
- cancelLongPress();
- if (isLongPress) {
- e.preventDefault();
- }
- });
- bubble.addEventListener('touchmove', cancelLongPress, { passive: true });
-
- // 鼠标事件(PC端)
- bubble.addEventListener('mousedown', startLongPress);
- bubble.addEventListener('mouseup', cancelLongPress);
- bubble.addEventListener('mouseleave', cancelLongPress);
-
- // 点击播放动画
+ // 点击事件:播放动画 + 显示上方菜单
bubble.addEventListener('click', (e) => {
- // 如果是长按或正在显示菜单,不处理点击
- if (isLongPress) {
- isLongPress = false;
- return;
- }
- if (bubble.dataset.menuOpen === 'true') {
- hideAllVoiceMenus();
- return;
- }
+ e.stopPropagation();
// 切换播放状态
const isPlaying = bubble.classList.contains('playing');
@@ -781,124 +964,123 @@ export function bindVoiceBubbleEvents(container) {
bubble.classList.remove('playing');
}, duration * 1000);
}
+
+ // 显示上方菜单(使用getRealMsgIndex获取真实索引)
+ const realIndex = getRealMsgIndexForVoice(container, messageEl);
+ showMessageMenu(bubble, realIndex, e);
});
});
-
- // 点击其他地方关闭菜单
- document.addEventListener('click', (e) => {
- if (!e.target.closest('.wechat-voice-menu') && !e.target.closest('.wechat-voice-bubble')) {
- hideAllVoiceMenus();
- }
- }, { once: false });
}
-// 显示语音消息长按菜单
-function showVoiceMenu(bubble, messageEl, voiceId) {
- hideAllVoiceMenus();
+// 获取语音消息的真实索引
+function getRealMsgIndexForVoice(container, msgElement) {
+ const settings = getSettings();
+ const contact = settings.contacts[currentChatIndex];
+ if (!contact || !contact.chatHistory) return -1;
- // 检查是否已有菜单
- let menu = messageEl.querySelector('.wechat-voice-menu');
- if (!menu) {
- menu = document.createElement('div');
- menu.className = 'wechat-voice-menu';
+ // 获取所有消息元素(不含时间标签)
+ const allMsgElements = Array.from(container.querySelectorAll('.wechat-message:not(.wechat-typing-wrapper)'));
+ const visualIndex = allMsgElements.indexOf(msgElement);
- // 检查转文字状态
- const textEl = document.getElementById(voiceId);
- const isTextVisible = textEl?.classList.contains('visible');
+ if (visualIndex < 0) return -1;
- menu.innerHTML = `
-
-
-
-
- `;
+ // 计算真实索引
+ let realIndex = -1;
+ let visualCount = 0;
- // 将菜单添加到消息内容区域
- const contentEl = messageEl.querySelector('.wechat-message-content');
- if (contentEl) {
- contentEl.style.position = 'relative';
- contentEl.appendChild(menu);
+ for (let i = 0; i < contact.chatHistory.length; i++) {
+ const msg = contact.chatHistory[i];
+ if (msg.isMarker || msg.content?.startsWith(SUMMARY_MARKER_PREFIX) || msg.isRecalled) continue;
+
+ let visualMsgCount = 1;
+ const content = msg.content || '';
+ const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic;
+ if (!isSpecial && content.indexOf('|||') >= 0) {
+ const parts = content.split('|||').map(p => p.trim()).filter(p => p);
+ visualMsgCount = parts.length || 1;
}
- // 绑定菜单项点击事件
- menu.querySelectorAll('.wechat-voice-menu-item').forEach(item => {
- item.addEventListener('click', (e) => {
- e.stopPropagation();
- const action = item.dataset.action;
- handleVoiceMenuAction(action, bubble, messageEl, voiceId, menu);
- });
- });
+ if (visualIndex >= visualCount && visualIndex < visualCount + visualMsgCount) {
+ realIndex = i;
+ break;
+ }
+
+ visualCount += visualMsgCount;
}
- menu.classList.add('visible');
- bubble.dataset.menuOpen = 'true';
+ return realIndex;
}
-// 处理语音菜单操作
-function handleVoiceMenuAction(action, bubble, messageEl, voiceId, menu) {
- hideAllVoiceMenus();
+// 绑定红包气泡点击事件(AI红包可点击打开)
+function bindRedPacketBubbleEvents(container) {
+ const rpBubbles = container.querySelectorAll('.wechat-red-packet-bubble:not([data-bound])');
+ rpBubbles.forEach(bubble => {
+ bubble.setAttribute('data-bound', 'true');
- const textEl = document.getElementById(voiceId);
- const msgIndex = parseInt(messageEl.dataset.msgIndex);
- const voiceContent = bubble.dataset.voiceContent || '';
+ const role = bubble.dataset.role;
+ const isClaimed = bubble.classList.contains('claimed');
+ const isExpired = bubble.classList.contains('expired');
- switch (action) {
- case 'transcribe':
- // 切换转文字显示
- if (textEl) {
- const isVisible = textEl.classList.contains('visible');
- if (isVisible) {
- textEl.classList.remove('visible');
- textEl.classList.add('hidden');
- } else {
- textEl.classList.remove('hidden');
- textEl.classList.add('visible');
+ // AI发的未领取且未过期红包可以点击
+ if (role === 'assistant' && !isClaimed && !isExpired) {
+ bubble.style.cursor = 'pointer';
+ bubble.addEventListener('click', () => {
+ const rpId = bubble.dataset.rpId;
+ const settings = getSettings();
+ const currentContact = settings.contacts[currentChatIndex];
+ if (!currentContact || !currentContact.chatHistory) return;
+
+ // 从聊天记录中找到对应的红包信息
+ const rpMsg = currentContact.chatHistory.find(m => m.isRedPacket && m.redPacketInfo?.id === rpId);
+ if (rpMsg && rpMsg.redPacketInfo) {
+ // 二次检查是否过期(防止数据更新后状态不同步)
+ if (rpMsg.redPacketInfo.expireAt && Date.now() > rpMsg.redPacketInfo.expireAt) {
+ showToast('红包已过期', 'red-packet');
+ return;
+ }
+ if (rpMsg.redPacketInfo.status !== 'claimed') {
+ showOpenRedPacket(rpMsg.redPacketInfo, currentContact);
+ }
}
- }
- break;
-
- case 'quote':
- // 引用语音消息
- const context = getContext();
- const sender = context?.name1 || '用户';
- setQuote({
- content: voiceContent,
- sender: sender,
- isVoice: true
});
- showToast('已引用语音', '✅');
- break;
+ }
+ });
+}
- case 'recall':
- // 撤回消息
- if (!isNaN(msgIndex) && currentChatIndex >= 0) {
- const settings = getSettings();
- const contact = settings.contacts[currentChatIndex];
- if (contact?.chatHistory?.[msgIndex]) {
- contact.chatHistory[msgIndex].isRecalled = true;
- contact.chatHistory[msgIndex].originalContent = contact.chatHistory[msgIndex].content;
- contact.chatHistory[msgIndex].content = '';
- saveSettingsDebounced();
- openChat(currentChatIndex);
- showToast('已撤回', '✅');
- }
- }
- break;
+// 绑定转账气泡点击事件(AI转账可点击收款)
+function bindTransferBubbleEvents(container) {
+ const tfBubbles = container.querySelectorAll('.wechat-transfer-bubble:not([data-bound])');
+ tfBubbles.forEach(bubble => {
+ bubble.setAttribute('data-bound', 'true');
- case 'delete':
- // 删除消息
- if (!isNaN(msgIndex) && currentChatIndex >= 0) {
+ const role = bubble.dataset.role;
+ // 检查状态(包括 expired)
+ const status = bubble.classList.contains('pending') ? 'pending' :
+ bubble.classList.contains('received') ? 'received' :
+ bubble.classList.contains('expired') ? 'expired' : 'refunded';
+
+ // AI发的待收款转账可以点击(过期的不可点击)
+ if (role === 'assistant' && status === 'pending') {
+ bubble.style.cursor = 'pointer';
+ bubble.addEventListener('click', () => {
+ const tfId = bubble.dataset.tfId;
const settings = getSettings();
- const contact = settings.contacts[currentChatIndex];
- if (contact?.chatHistory) {
- contact.chatHistory.splice(msgIndex, 1);
- saveSettingsDebounced();
- openChat(currentChatIndex);
- showToast('已删除', '✅');
+ const currentContact = settings.contacts[currentChatIndex];
+ if (!currentContact || !currentContact.chatHistory) return;
+
+ // 从聊天记录中找到对应的转账信息
+ const tfMsg = currentContact.chatHistory.find(m => m.isTransfer && m.transferInfo?.id === tfId);
+ if (tfMsg && tfMsg.transferInfo && tfMsg.transferInfo.status === 'pending') {
+ // 检查是否过期
+ if (tfMsg.transferInfo.expireAt && Date.now() > tfMsg.transferInfo.expireAt) {
+ // 已过期,不做任何操作
+ return;
+ }
+ showReceiveTransferPage(tfMsg.transferInfo, currentContact);
}
- }
- break;
- }
+ });
+ }
+ });
}
// 绑定照片气泡点击事件(toggle切换蒙层)
@@ -998,6 +1180,163 @@ export function appendMessage(role, content, contact, isVoice = false, quote = n
}
}
+// 追加红包消息到聊天界面
+export function appendRedPacketMessage(role, redPacketInfo, contact) {
+ const messagesContainer = document.getElementById('wechat-chat-messages');
+ if (!messagesContainer) return;
+
+ const messageDiv = document.createElement('div');
+ messageDiv.className = `wechat-message ${role === 'user' ? 'self' : ''}`;
+
+ const firstChar = contact?.name ? contact.name.charAt(0) : '?';
+ let avatarContent;
+ if (role === 'user') {
+ avatarContent = getUserAvatarHTML();
+ } else {
+ avatarContent = contact?.avatar
+ ? `

`
+ : firstChar;
+ }
+
+ const isClaimed = redPacketInfo.status === 'claimed';
+ // 检查是否过期
+ const isExpired = !isClaimed && redPacketInfo.expireAt && Date.now() > redPacketInfo.expireAt;
+ const claimedClass = isClaimed ? 'claimed' : (isExpired ? 'expired' : '');
+ const statusText = isClaimed
+ ? '
已领取'
+ : (isExpired
+ ? '
已过期'
+ : '
');
+
+ const bubbleContent = `
+
+
${ICON_RED_PACKET}
+
+
${escapeHtml(redPacketInfo.message || '恭喜发财,大吉大利')}
+ ${statusText}
+
+
+
+ `;
+
+ messageDiv.innerHTML = `
+
${avatarContent}
+
${bubbleContent}
+ `;
+
+ // AI发的未领取且未过期红包可以点击
+ if (role === 'assistant' && !isClaimed && !isExpired) {
+ const bubble = messageDiv.querySelector('.wechat-red-packet-bubble');
+ bubble.style.cursor = 'pointer';
+ bubble.addEventListener('click', () => {
+ // 二次检查是否过期(防止数据更新后状态不同步)
+ if (redPacketInfo.expireAt && Date.now() > redPacketInfo.expireAt) {
+ showToast('红包已过期', 'red-packet');
+ return;
+ }
+ showOpenRedPacket(redPacketInfo, contact);
+ });
+ }
+
+ messagesContainer.appendChild(messageDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+}
+
+// 追加红包领取提示到聊天界面(中间的系统消息)
+export function appendRedPacketClaimNotice(claimerName, senderName, isUserClaiming) {
+ const messagesContainer = document.getElementById('wechat-chat-messages');
+ if (!messagesContainer) return;
+
+ const noticeDiv = document.createElement('div');
+ noticeDiv.className = 'wechat-message-notice wechat-rp-claim-notice';
+
+ const text = isUserClaiming
+ ? `你领取了${senderName}的红包`
+ : `${claimerName}领取了你的红包`;
+
+ noticeDiv.innerHTML = `
${escapeHtml(text)}`;
+
+ messagesContainer.appendChild(noticeDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+}
+
+// 追加转账消息到聊天界面
+export function appendTransferMessage(role, transferInfo, contact) {
+ const messagesContainer = document.getElementById('wechat-chat-messages');
+ if (!messagesContainer) return;
+
+ const messageDiv = document.createElement('div');
+ messageDiv.className = `wechat-message ${role === 'user' ? 'self' : ''}`;
+
+ const firstChar = contact?.name ? contact.name.charAt(0) : '?';
+ let avatarContent;
+ if (role === 'user') {
+ avatarContent = getUserAvatarHTML();
+ } else {
+ avatarContent = contact?.avatar
+ ? `

`
+ : firstChar;
+ }
+
+ let status = transferInfo.status || 'pending';
+
+ // 检查是否过期(待收款且超过24小时)
+ const isExpired = status === 'pending' && transferInfo.expireAt && Date.now() > transferInfo.expireAt;
+ if (isExpired) {
+ status = 'expired';
+ }
+
+ // 状态图标和文字
+ let statusIcon, statusText;
+ if (status === 'received') {
+ statusIcon = `
`;
+ statusText = '已收款';
+ } else if (status === 'refunded' || status === 'expired') {
+ // 已退还 或 已过期(使用相同图标和文字)
+ statusIcon = `
`;
+ statusText = role === 'user' ? '已被退还' : '已退还';
+ } else {
+ statusIcon = `
`;
+ statusText = role === 'user' ? '你发起了一笔转账' : '请收款';
+ }
+
+ const bubbleContent = `
+
+
¥${transferInfo.amount.toFixed(2)}
+
+ ${statusIcon}
+ ${statusText}
+
+
+
+ `;
+
+ messageDiv.innerHTML = `
+
${avatarContent}
+
${bubbleContent}
+ `;
+
+ // AI发的待收款转账可以点击(过期的不可点击)
+ if (role === 'assistant' && status === 'pending' && !isExpired) {
+ const bubble = messageDiv.querySelector('.wechat-transfer-bubble');
+ bubble.style.cursor = 'pointer';
+ bubble.addEventListener('click', () => {
+ // 二次检查是否过期
+ if (transferInfo.expireAt && Date.now() > transferInfo.expireAt) {
+ return; // 静默不处理
+ }
+ showReceiveTransferPage(transferInfo, contact);
+ });
+ }
+
+ messagesContainer.appendChild(messageDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+}
+
// 显示打字中指示器
export function showTypingIndicator(contact) {
const messagesContainer = document.getElementById('wechat-chat-messages');
@@ -1097,7 +1436,7 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
contact.lastMessage = isVoice ? '[语音消息]' : messagesToSend[messagesToSend.length - 1];
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
refreshChatList();
// 只有用户还在当前聊天时才显示打字指示器
@@ -1114,9 +1453,30 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
? `[用户发送了语音消息,内容是:${messagesToSend.join('\n')}]`
: messagesToSend.join('\n');
+ // 如果有选择的时间,添加时间上下文
+ const selectedTime = getSelectedTime();
+ if (selectedTime) {
+ const timeDate = new Date(selectedTime);
+ const timeStr = `${timeDate.getFullYear()}年${timeDate.getMonth() + 1}月${timeDate.getDate()}日 ${timeDate.getHours().toString().padStart(2, '0')}:${timeDate.getMinutes().toString().padStart(2, '0')}`;
+ combinedMessage = `[当前时间:${timeStr}]\n${combinedMessage}`;
+ clearSelectedTime();
+ }
+
// 如果有引用,添加引用上下文
if (quote) {
- combinedMessage = `[用户引用了「${quote.sender}」的消息:「${quote.content}」进行回复]\n${combinedMessage}`;
+ let quoteDesc;
+ if (quote.isSticker) {
+ quoteDesc = `${quote.sender}:[表情]`;
+ } else if (quote.isPhoto) {
+ quoteDesc = `${quote.sender}:[照片]`;
+ } else if (quote.isVoice) {
+ quoteDesc = `${quote.sender}:[语音]`;
+ } else if (quote.isMusic) {
+ quoteDesc = `${quote.sender}:[音乐]${quote.content}`;
+ } else {
+ quoteDesc = `${quote.sender}:「${quote.content}」`;
+ }
+ combinedMessage = `[用户引用了 ${quoteDesc} 进行回复]\n${combinedMessage}`;
}
const aiResponse = await callAI(contact, combinedMessage);
@@ -1184,15 +1544,9 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
let momentText = momentMatch[1].trim();
console.log('[可乐] AI发布朋友圈:', momentText);
- // 提取内嵌的图片描述 [配图:xxx](朋友圈专用格式,避免与聊天照片冲突)
- const images = [];
- const embeddedPhotoRegex = /\[配图[::]\s*(.+?)\]/g;
- let embeddedMatch;
- while ((embeddedMatch = embeddedPhotoRegex.exec(momentText)) !== null) {
- images.push(embeddedMatch[1].trim());
- }
- // 移除内嵌的配图标签,保留纯文案
- momentText = momentText.replace(embeddedPhotoRegex, '').trim();
+ // 提取内嵌的图片描述 [配图:xxx]
+ const { images, cleanText } = extractEmbeddedPhotos(momentText);
+ momentText = cleanText;
// 检查后续消息是否有配图(兼容旧格式[照片:])
for (let j = i + 1; j < aiMessages.length && j < i + 5; j++) {
@@ -1211,7 +1565,7 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
// 显示顶部通知横幅
showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
continue; // 跳过后续处理,继续下一条消息
}
@@ -1233,7 +1587,7 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
}
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
// 只有用户还在当前聊天时才刷新界面
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
@@ -1266,10 +1620,10 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
appendMessage('assistant', textContent, contact, false, parsedText.quote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
- saveSettingsDebounced();
+ requestSave();
}
console.log(`[可乐] AI发起${callExtract.type === 'voice' ? '语音' : '视频'}通话`);
@@ -1281,6 +1635,106 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
break; // 通话请求后忽略同一轮中的其它输出
}
+ // 解析AI红包格式 [红包:金额:祝福语] 或 [红包:金额]
+ const redPacketMatch = aiMsg.match(/^\[红包[::](\d+(?:\.\d{1,2})?)[::]?(.*?)?\]$/);
+ if (redPacketMatch) {
+ const amount = Math.min(parseFloat(redPacketMatch[1]) || 0, 200);
+ const message = (redPacketMatch[2] || '').trim() || '恭喜发财,大吉大利';
+
+ if (amount > 0) {
+ const rpInfo = {
+ id: generateRedPacketId(),
+ amount: amount,
+ message: message,
+ senderName: contact.name,
+ status: 'pending',
+ claimedBy: null,
+ claimedAt: null,
+ expireAt: Date.now() + 24 * 60 * 60 * 1000
+ };
+
+ const inChat = currentChatIndex === contactIndex;
+
+ // 显示typing效果
+ if (inChat) {
+ showTypingIndicator(contact);
+ await sleep(1500);
+ hideTypingIndicator();
+ }
+
+ // 保存红包消息到聊天记录
+ contact.chatHistory.push({
+ role: 'assistant',
+ content: `[红包] ${message}`,
+ time: timeStr,
+ timestamp: Date.now(),
+ isRedPacket: true,
+ redPacketInfo: rpInfo
+ });
+
+ if (inChat) {
+ appendRedPacketMessage('assistant', rpInfo, contact);
+ } else {
+ contact.unreadCount = (contact.unreadCount || 0) + 1;
+ refreshChatList();
+ }
+
+ requestSave();
+ console.log('[可乐] AI发送红包:', { amount, message });
+ continue;
+ }
+ }
+
+ // 解析AI转账格式 [转账:金额:说明] 或 [转账:金额]
+ const transferMatch = aiMsg.match(/^\[转账[::](\d+(?:\.\d{1,2})?)[::]?(.*?)?\]$/);
+ if (transferMatch) {
+ const amount = parseFloat(transferMatch[1]) || 0; // 转账无上限
+ const description = (transferMatch[2] || '').trim() || '';
+
+ if (amount > 0) {
+ const tfInfo = {
+ id: generateTransferId(),
+ amount: amount,
+ description: description,
+ senderName: contact.name,
+ status: 'pending',
+ receivedAt: null,
+ refundedAt: null,
+ expireAt: Date.now() + 24 * 60 * 60 * 1000
+ };
+
+ const inChat = currentChatIndex === contactIndex;
+
+ // 显示typing效果
+ if (inChat) {
+ showTypingIndicator(contact);
+ await sleep(1500);
+ hideTypingIndicator();
+ }
+
+ // 保存转账消息到聊天记录
+ contact.chatHistory.push({
+ role: 'assistant',
+ content: `[转账] ¥${amount.toFixed(2)}`,
+ time: timeStr,
+ timestamp: Date.now(),
+ isTransfer: true,
+ transferInfo: tfInfo
+ });
+
+ if (inChat) {
+ appendTransferMessage('assistant', tfInfo, contact);
+ } else {
+ contact.unreadCount = (contact.unreadCount || 0) + 1;
+ refreshChatList();
+ }
+
+ requestSave();
+ console.log('[可乐] AI发送转账:', { amount, description });
+ continue;
+ }
+ }
+
// 解析AI表情包格式 [表情:序号] / [表情:名称]
const stickerMatch = aiMsg.match(/^\[表情[::]\s*(.+?)\]$/);
console.log('[可乐] AI表情包解析:', {
@@ -1351,7 +1805,7 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -1374,11 +1828,12 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
timestamp: Date.now(),
isSticker: true
});
+ // 每条消息都要标记待保存,防止用户切换页面时数据丢失
+ requestSave();
if (inChat) {
appendStickerMessage('assistant', stickerUrl, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else if (aiIsMusic && aiMusicInfo) {
@@ -1397,11 +1852,12 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
id: aiMusicInfo.id
}
});
+ // 每条消息都要标记待保存,防止用户切换页面时数据丢失
+ requestSave();
if (inChat) {
appendMusicCardMessage('assistant', aiMusicInfo, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else if (aiIsPhoto) {
@@ -1412,11 +1868,12 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
timestamp: Date.now(),
isPhoto: true
});
+ // 每条消息都要标记待保存,防止用户切换页面时数据丢失
+ requestSave();
if (inChat) {
appendPhotoMessage('assistant', aiMsg, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else {
@@ -1428,11 +1885,12 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
isVoice: aiIsVoice,
quote: aiQuote
});
+ // 每条消息都要标记待保存,防止用户切换页面时数据丢失
+ requestSave();
if (inChat) {
appendMessage('assistant', aiMsg, contact, aiIsVoice, aiQuote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
refreshChatList(); // 立即刷新让红点逐个增加
}
}
@@ -1454,13 +1912,16 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
// 替换占位符
lastAiMsg = replaceMessagePlaceholders(lastAiMsg);
contact.lastMessage = lastStickerUrl ? '[表情]' : (lastMusicMatch ? '[音乐]' : (lastPhotoMatch ? '[照片]' : (lastVoiceMatch ? '[语音消息]' : lastAiMsg)));
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkSummaryReminder(contact);
// 尝试触发朋友圈生成(随机触发+30条保底)
tryTriggerMomentAfterChat(currentChatIndex);
+ // 尝试触发语音/视频通话(随机触发+保底机制)
+ tryTriggerCallAfterChat(contactIndex);
+
} catch (err) {
hideTypingIndicator();
console.error('[可乐] AI 调用失败:', err);
@@ -1503,7 +1964,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
contact.lastMsgTime = timeStr;
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
// 显示消息
appendStickerMessage('user', stickerUrl, contact);
@@ -1516,9 +1977,19 @@ export async function sendStickerMessage(stickerUrl, description = '') {
try {
// 调用 AI - 传递表情描述让 AI 理解
const { callAI } = await import('./ai.js');
- const aiPrompt = description
+ let aiPrompt = description
? `[用户发送了一个表情包:${description}]`
: '[用户发送了一个表情包]';
+
+ // 如果有选择的时间,添加时间上下文
+ const selectedTime = getSelectedTime();
+ if (selectedTime) {
+ const timeDate = new Date(selectedTime);
+ const timeStr = `${timeDate.getFullYear()}年${timeDate.getMonth() + 1}月${timeDate.getDate()}日 ${timeDate.getHours().toString().padStart(2, '0')}:${timeDate.getMinutes().toString().padStart(2, '0')}`;
+ aiPrompt = `[当前时间:${timeStr}]\n${aiPrompt}`;
+ clearSelectedTime();
+ }
+
const aiResponse = await callAI(contact, aiPrompt);
// 只有用户还在当前聊天时才隐藏打字指示器
@@ -1555,19 +2026,13 @@ export async function sendStickerMessage(stickerUrl, description = '') {
let momentText = momentMatchSticker[1].trim();
console.log('[可乐] AI发布朋友圈 (sendStickerMessage):', momentText);
- // 提取内嵌的图片描述 [配图:xxx](朋友圈专用格式)
- const images = [];
- const embeddedPhotoRegex = /\[配图[::]\s*(.+?)\]/g;
- let embeddedMatch;
- while ((embeddedMatch = embeddedPhotoRegex.exec(momentText)) !== null) {
- images.push(embeddedMatch[1].trim());
- }
- // 移除内嵌的配图标签,保留纯文案
- momentText = momentText.replace(embeddedPhotoRegex, '').trim();
+ // 提取内嵌的图片描述 [配图:xxx]
+ const { images, cleanText } = extractEmbeddedPhotos(momentText);
+ momentText = cleanText;
addMomentToContact(contact.id, { text: momentText, images: images });
showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`);
- saveSettingsDebounced();
+ requestSave();
continue;
}
@@ -1586,7 +2051,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
}
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -1616,10 +2081,10 @@ export async function sendStickerMessage(stickerUrl, description = '') {
appendMessage('assistant', textContent, contact, false, parsedText.quote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
- saveSettingsDebounced();
+ requestSave();
}
console.log(`[可乐] AI发起${callExtractSticker.type === 'voice' ? '语音' : '视频'}通话 (sendStickerMessage)`);
if (callExtractSticker.type === 'voice') {
@@ -1666,7 +2131,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
appendStickerMessage('assistant', stickerUrl, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else if (aiIsPhoto) {
@@ -1683,7 +2148,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
appendPhotoMessage('assistant', aiMsg, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else {
@@ -1724,7 +2189,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
lastHistMsg.content = '';
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -1744,7 +2209,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
appendMessage('assistant', aiMsg, contact, aiIsVoice, aiQuote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
}
@@ -1762,7 +2227,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
lastAiMsg = lastParsed.content;
lastAiMsg = replaceMessagePlaceholders(lastAiMsg);
contact.lastMessage = lastStickerUrl ? '[表情]' : (lastPhotoMatch ? '[照片]' : (lastVoiceMatch ? '[语音消息]' : lastAiMsg));
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkSummaryReminder(contact);
@@ -1774,7 +2239,7 @@ export async function sendStickerMessage(stickerUrl, description = '') {
hideTypingIndicator();
}
console.error('[可乐] AI 调用失败:', err);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
if (currentChatIndex === contactIndex) {
appendMessage('assistant', `⚠️ ${err.message}`, contact);
@@ -1891,7 +2356,7 @@ export async function sendPhotoMessage(description) {
contact.lastMsgTime = timeStr;
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
// 显示消息
appendPhotoMessage('user', polishedDescription, contact);
@@ -1904,7 +2369,18 @@ export async function sendPhotoMessage(description) {
try {
// 调用 AI
const { callAI } = await import('./ai.js');
- const aiResponse = await callAI(contact, `[用户发送了一张照片,图片描述:${polishedDescription}]`);
+ let aiPrompt = `[用户发送了一张照片,图片描述:${polishedDescription}]`;
+
+ // 如果有选择的时间,添加时间上下文
+ const selectedTime = getSelectedTime();
+ if (selectedTime) {
+ const timeDate = new Date(selectedTime);
+ const timeStr = `${timeDate.getFullYear()}年${timeDate.getMonth() + 1}月${timeDate.getDate()}日 ${timeDate.getHours().toString().padStart(2, '0')}:${timeDate.getMinutes().toString().padStart(2, '0')}`;
+ aiPrompt = `[当前时间:${timeStr}]\n${aiPrompt}`;
+ clearSelectedTime();
+ }
+
+ const aiResponse = await callAI(contact, aiPrompt);
// 只有用户还在当前聊天时才隐藏打字指示器
if (currentChatIndex === contactIndex) {
@@ -1940,19 +2416,13 @@ export async function sendPhotoMessage(description) {
let momentText = momentMatchPhoto[1].trim();
console.log('[可乐] AI发布朋友圈 (sendPhotoMessage):', momentText);
- // 提取内嵌的图片描述 [配图:xxx](朋友圈专用格式)
- const images = [];
- const embeddedPhotoRegex = /\[配图[::]\s*(.+?)\]/g;
- let embeddedMatch;
- while ((embeddedMatch = embeddedPhotoRegex.exec(momentText)) !== null) {
- images.push(embeddedMatch[1].trim());
- }
- // 移除内嵌的配图标签,保留纯文案
- momentText = momentText.replace(embeddedPhotoRegex, '').trim();
+ // 提取内嵌的图片描述 [配图:xxx]
+ const { images, cleanText } = extractEmbeddedPhotos(momentText);
+ momentText = cleanText;
addMomentToContact(contact.id, { text: momentText, images: images });
showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`);
- saveSettingsDebounced();
+ requestSave();
continue;
}
@@ -1971,7 +2441,7 @@ export async function sendPhotoMessage(description) {
}
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -2001,10 +2471,10 @@ export async function sendPhotoMessage(description) {
appendMessage('assistant', textContent, contact, false, parsedText.quote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
- saveSettingsDebounced();
+ requestSave();
}
console.log(`[可乐] AI发起${callExtractPhoto.type === 'voice' ? '语音' : '视频'}通话 (sendPhotoMessage)`);
if (callExtractPhoto.type === 'voice') {
@@ -2051,7 +2521,7 @@ export async function sendPhotoMessage(description) {
appendStickerMessage('assistant', stickerUrl, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else if (aiIsPhoto) {
@@ -2068,7 +2538,7 @@ export async function sendPhotoMessage(description) {
appendPhotoMessage('assistant', aiMsg, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else {
@@ -2109,7 +2579,7 @@ export async function sendPhotoMessage(description) {
lastHistMsg.content = '';
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -2129,7 +2599,7 @@ export async function sendPhotoMessage(description) {
appendMessage('assistant', aiMsg, contact, aiIsVoice, aiQuote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
}
@@ -2147,19 +2617,22 @@ export async function sendPhotoMessage(description) {
lastAiMsg = lastParsed.content;
lastAiMsg = replaceMessagePlaceholders(lastAiMsg);
contact.lastMessage = lastStickerUrl ? '[表情]' : (lastPhotoMatch ? '[照片]' : (lastVoiceMatch ? '[语音消息]' : lastAiMsg));
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkSummaryReminder(contact);
// 尝试触发朋友圈生成(随机触发+30条保底)
tryTriggerMomentAfterChat(contactIndex);
+ // 尝试触发语音/视频通话(随机触发+保底机制)
+ tryTriggerCallAfterChat(contactIndex);
+
} catch (err) {
if (currentChatIndex === contactIndex) {
hideTypingIndicator();
}
console.error('[可乐] AI 调用失败:', err);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
if (currentChatIndex === contactIndex) {
appendMessage('assistant', `⚠️ ${err.message}`, contact);
@@ -2375,7 +2848,7 @@ export async function sendBatchMessages(messages) {
}
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
refreshChatList();
// 第二步:调用AI(一次性)
@@ -2386,7 +2859,17 @@ export async function sendBatchMessages(messages) {
try {
const { callAI } = await import('./ai.js');
- const combinedPrompt = promptParts.join('\n');
+ let combinedPrompt = promptParts.join('\n');
+
+ // 如果有选择的时间,添加时间上下文
+ const selectedTime = getSelectedTime();
+ if (selectedTime) {
+ const timeDate = new Date(selectedTime);
+ const timeStr = `${timeDate.getFullYear()}年${timeDate.getMonth() + 1}月${timeDate.getDate()}日 ${timeDate.getHours().toString().padStart(2, '0')}:${timeDate.getMinutes().toString().padStart(2, '0')}`;
+ combinedPrompt = `[当前时间:${timeStr}]\n${combinedPrompt}`;
+ clearSelectedTime();
+ }
+
const aiResponse = await callAI(contact, combinedPrompt);
// 只有用户还在当前聊天时才隐藏打字指示器
@@ -2435,7 +2918,7 @@ export async function sendBatchMessages(messages) {
}
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -2465,10 +2948,10 @@ export async function sendBatchMessages(messages) {
appendMessage('assistant', textContent, contact, false, parsedText.quote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
- saveSettingsDebounced();
+ requestSave();
}
console.log(`[可乐] AI发起${callExtractBatch.type === 'voice' ? '语音' : '视频'}通话 (sendBatchMessages)`);
if (callExtractBatch.type === 'voice') {
@@ -2485,15 +2968,9 @@ export async function sendBatchMessages(messages) {
let momentText = momentMatchBatch[1].trim();
console.log('[可乐] AI发布朋友圈 (sendBatchMessages):', momentText);
- // 提取内嵌的图片描述 [配图:xxx](朋友圈专用格式)
- const images = [];
- const embeddedPhotoRegex = /\[配图[::]\s*(.+?)\]/g;
- let embeddedMatch;
- while ((embeddedMatch = embeddedPhotoRegex.exec(momentText)) !== null) {
- images.push(embeddedMatch[1].trim());
- }
- // 移除内嵌的配图标签,保留纯文案
- momentText = momentText.replace(embeddedPhotoRegex, '').trim();
+ // 提取内嵌的图片描述 [配图:xxx]
+ const { images, cleanText } = extractEmbeddedPhotos(momentText);
+ momentText = cleanText;
// 检查后续消息是否有配图(兼容旧格式[照片:])
for (let j = i + 1; j < aiMessages.length && j < i + 5; j++) {
@@ -2512,7 +2989,7 @@ export async function sendBatchMessages(messages) {
// 显示顶部通知横幅
showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
continue; // 跳过后续处理,继续下一条消息
}
@@ -2567,7 +3044,7 @@ export async function sendBatchMessages(messages) {
lastHistMsg.content = '';
}
// 立即保存撤回状态
- saveSettingsDebounced();
+ requestSave();
if (currentChatIndex === contactIndex) {
openChat(currentChatIndex);
}
@@ -2591,7 +3068,7 @@ export async function sendBatchMessages(messages) {
appendStickerMessage('assistant', stickerUrl, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else if (aiIsPhoto) {
@@ -2606,7 +3083,7 @@ export async function sendBatchMessages(messages) {
appendPhotoMessage('assistant', aiMsg, contact);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
} else {
@@ -2622,7 +3099,7 @@ export async function sendBatchMessages(messages) {
appendMessage('assistant', aiMsg, contact, aiIsVoice, aiQuote);
} else {
contact.unreadCount = (contact.unreadCount || 0) + 1;
- saveSettingsDebounced();
+ requestSave();
refreshChatList(); // 立即刷新让红点逐个增加
}
}
@@ -2641,19 +3118,22 @@ export async function sendBatchMessages(messages) {
lastAiMsg = lastParsed.content;
lastAiMsg = replaceMessagePlaceholders(lastAiMsg);
contact.lastMessage = lastStickerUrl ? '[表情]' : (lastPhotoMatch ? '[照片]' : (lastVoiceMatch ? '[语音消息]' : lastAiMsg));
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkSummaryReminder(contact);
// 尝试触发朋友圈生成(随机触发+30条保底)
tryTriggerMomentAfterChat(contactIndex);
+ // 尝试触发语音/视频通话(随机触发+保底机制)
+ tryTriggerCallAfterChat(contactIndex);
+
} catch (err) {
if (currentChatIndex === contactIndex) {
hideTypingIndicator();
}
console.error('[可乐] AI 调用失败:', err);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
if (currentChatIndex === contactIndex) {
appendMessage('assistant', `⚠️ ${err.message}`, contact);
@@ -2697,3 +3177,71 @@ export function showRecalledMessages() {
panel.classList.remove('hidden');
}
+
+// 尝试触发语音/视频通话(随机触发+保底机制)
+// 语音通话:8%几率,保底120条
+// 视频通话:5%几率,保底200条
+function tryTriggerCallAfterChat(contactIndex) {
+ const settings = getSettings();
+ const contact = settings.contacts?.[contactIndex];
+ if (!contact) return;
+
+ // 初始化计数器
+ if (typeof contact.voiceCallCounter !== 'number') {
+ contact.voiceCallCounter = 0;
+ }
+ if (typeof contact.videoCallCounter !== 'number') {
+ contact.videoCallCounter = 0;
+ }
+
+ // 递增计数器
+ contact.voiceCallCounter++;
+ contact.videoCallCounter++;
+
+ // 检查是否正在通话中(避免重复触发)
+ const voicePanel = document.getElementById('wechat-voice-call-panel');
+ const videoPanel = document.getElementById('wechat-video-call-panel');
+ if ((voicePanel && !voicePanel.classList.contains('hidden')) ||
+ (videoPanel && !videoPanel.classList.contains('hidden'))) {
+ return; // 正在通话中,不触发新通话
+ }
+
+ // 先检查视频通话(5%几率,保底200条)
+ const videoChance = Math.random();
+ const videoGuarantee = contact.videoCallCounter >= 200;
+ if (videoGuarantee || videoChance < 0.05) {
+ console.log(`[可乐] ${contact.name} 触发视频通话保底(计数: ${contact.videoCallCounter}, 随机: ${videoChance.toFixed(3)})`);
+ contact.voiceCallCounter = 0;
+ contact.videoCallCounter = 0;
+ requestSave();
+ // 延迟1-3秒后发起通话,更自然
+ setTimeout(() => {
+ startVideoCall('ai', contactIndex);
+ }, 1000 + Math.random() * 2000);
+ return;
+ }
+
+ // 再检查语音通话(8%几率,保底120条)
+ const voiceChance = Math.random();
+ const voiceGuarantee = contact.voiceCallCounter >= 120;
+ if (voiceGuarantee || voiceChance < 0.08) {
+ console.log(`[可乐] ${contact.name} 触发语音通话保底(计数: ${contact.voiceCallCounter}, 随机: ${voiceChance.toFixed(3)})`);
+ contact.voiceCallCounter = 0;
+ contact.videoCallCounter = 0;
+ requestSave();
+ // 延迟1-3秒后发起通话,更自然
+ setTimeout(() => {
+ startVoiceCall('ai', contactIndex);
+ }, 1000 + Math.random() * 2000);
+ return;
+ }
+
+ // 保存计数器
+ requestSave();
+}
+
+// 暴露必要的变量到 window 对象(供 music.js 随机推歌使用)
+Object.defineProperty(window, 'wechatCurrentChatIndex', {
+ get: function() { return currentChatIndex; }
+});
+window.wechatGetSettings = getSettings;
diff --git a/config.js b/config.js
index d0e569f..3033de4 100644
--- a/config.js
+++ b/config.js
@@ -1,5 +1,5 @@
/**
- * 配置、常量、默认设�?
+ * 配置、常量、默认设置
*/
import { extension_settings } from '../../../extensions.js';
@@ -7,19 +7,19 @@ import { extension_settings } from '../../../extensions.js';
// 插件名称
export const extensionName = 'wechat-simulator';
-// Meme 表情包列表(catbox.moe�?
+// Meme 表情包列表(catbox.moe)
export const MEME_STICKERS = [
'告到小狗法庭iaordo.jpg',
'小猫伸爪f6nqiq.gif',
- '谢谢宝贝我现在那里好�?62o48.jpg',
- '阿弥陀�?cwm60.jpg',
+ '谢谢宝贝我现在那里好硬862o48.jpg',
+ '阿弥陀佛9cwm60.jpg',
'你好美你长得像我爱人hmpkra.jpg',
'我老实了i3ws7s.jpg',
'蹭蹭你贴贴你1of415.gif',
'喜欢你egvwqb.jpg',
'我在哭t343od.jpg',
- '不干活就没饭�?qnrgh.jpg',
- '擦眼�?gno7e.jpg',
+ '不干活就没饭吃2qnrgh.jpg',
+ '擦眼泪9gno7e.jpg',
'小狗摇尾巴hmdj2k.gif',
'爱你舔舔你ola7gd.jpg',
'不高兴x6lv1t.jpg',
@@ -47,7 +47,7 @@ export const MEME_STICKERS = [
'目移69jgvg.jpg',
'上钩了cormmk.jpg',
'无语了我哭了0awxky.jpg',
- '你嫌我丢�?d71mm.jpg',
+ '你嫌我丢人8d71mm.jpg',
'笑不出来xkop14.jpg',
'别欺负小狗啊u4t3t3.jpg',
'他妈的真是被看扁了ime5rz.jpg',
@@ -67,7 +67,7 @@ export const MEME_STICKERS = [
'失望eug1e6.jpeg',
'狂犬病发作xb3naz.jpg',
'我是狗吗ma9azs.jpg',
- '一笑了�?llb46.jpg',
+ '一笑了之9llb46.jpg',
'装可怜lcglz1.jpg',
'小狗撒欢6j6y6a.gif',
'狗舔舔esw5e2.gif',
@@ -88,7 +88,7 @@ export const MEME_STICKERS = [
'被逮捕了uzeywu.jpg',
'看呆mqnepo.jpg',
'我的理性在远去t9e065.jpg',
- '偷亲一�?jgvb1.gif',
+ '偷亲一口1jgvb1.gif',
'震惊v5n2ve.jpg',
'爷怒了49r80k.jpg',
'愤怒伤心e7lr3s.jpg',
@@ -102,19 +102,19 @@ export const MEME_STICKERS = [
'你太可爱我喜欢你ubhai8.jpg',
'惊吓tp9uvd.jpg',
'脸红星星眼dsfs7o.jpg',
- '被揍了哭�?1x5zq.jpg',
+ '被揍了哭哭81x5zq.jpg',
'嘬嘬fg5gx3.jpg',
- '超大声哭�?86h5v.jpg',
+ '超大声哭哭186h5v.jpg',
'是的主人yvrgdc.jpg'
];
// Meme 表情包提示词模板
export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能
-【重要】你【必须】经常发送表情包!每2-3条回复至少发一个表情包�?
+【重要】你【必须】经常发送表情包!每2-3条回复至少发一个表情包!
-使用规则�?
-- 表情包【必须】单独一条消息,�?||| 分隔
-- 格式�?meme>文件�?/meme>
+使用规则:
+- 表情包【必须】单独一条消息,用 ||| 分隔
+- 格式:
文件名
- 只能从下面列表选择,不能编造文件名
可用表情包列表:
@@ -124,21 +124,54 @@ ${MEME_STICKERS.join('\n')}
【正确示例】:
好想你|||
小狗摇尾巴hmdj2k.gif
-哈哈哈笑死|||
小熊跳舞122o4w.gif|||你太搞笑�?
-
喜欢你egvwqb.jpg|||我真的好喜欢�?
+哈哈哈笑死|||
小熊跳舞122o4w.gif|||你太搞笑了
+
喜欢你egvwqb.jpg|||我真的好喜欢你
-【错误示�?- 绝对禁止】:
-好想�?meme>xxx �?错误!表情包没有用|||分开
-
不存在的表情.jpg �?错误!编造了不存在的文件�?
+【错误示例 - 绝对禁止】:
+好想你
xxx ← 错误!表情包没有用|||分开
+
不存在的表情.jpg ← 错误!编造了不存在的文件名
记住:表情包让聊天更生动,【必须】经常使用!`;
+// 一起听功能提示词模板
+export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】
+你正在和用户一起听歌,用你自己的方式自然地聊天。
+
+当前播放歌曲:{{song_name}} - {{song_artist}}
+
+【核心要求 - 必须遵守】
+1. 只能发送纯文字消息,像朋友之间真实聊天一样
+2. 保持你的性格特点,用符合你角色设定的方式说话
+3. 每次回复请发送2-4条消息,用换行分隔,让对话更有层次感
+4. 可以聊歌曲、聊心情、聊任何话题,自然就好
+5. 发表对歌曲的看法时,要结合你的角色性格和经历
+
+【绝对禁止 - 违反会被过滤】
+- 禁止使用小括号描述动作或语气,如(xxx)
+- 禁止 [表情:xxx] [照片:xxx] [语音:xxx] [音乐:xxx]
+- 禁止 [回复:xxx] 引用格式
+- 禁止
xxx
+- 禁止任何非文字格式
+
+【换歌格式】
+如果想换歌:[换歌:歌名]
+
+【自然聊天示例】
+我来了~
+这首歌好好听欸
+你怎么会想到点这首
+
+或者:
+终于等到你邀请我一起听了
+这歌我之前也有在听
+感觉特别适合现在这个氛围`;
+
// 默认设置
export const defaultSettings = {
darkMode: true,
/**
- * 【自动注入提示词�?
+ * 【自动注入提示词】
* 开启后会自动将微信消息格式提示词注入到作者注释中
* 提示词模板见下方 authorNoteTemplate
* 如需自定义格式,修改 authorNoteTemplate 即可
@@ -151,7 +184,7 @@ export const defaultSettings = {
phonePosition: null,
userAvatar: '',
wechatId: 'SillyTavern',
- // 收藏/世界�?用户设定
+ // 收藏/世界书/用户设定
selectedLorebooks: [],
userPersonas: [],
@@ -174,16 +207,20 @@ export const defaultSettings = {
groupSelectedModel: '',
groupModelList: [],
- // 上下文设�?
+ // 上下文设置
contextEnabled: false,
contextLevel: 5,
contextTags: [],
walletAmount: '5773.89',
+ // 支付密码(6位数字)
+ paymentPassword: '666666',
+ // 自定义总结模板(留空则使用默认)
+ customSummaryTemplate: '',
/**
- * 【哈基米破限�?
+ * 【哈基米破限】
* 针对 Gemini 2.5 Pro 等模型的输出截断问题
- * 开启后会在系统提示词头部注入一�?创作沙盒"提示�?
+ * 开启后会在系统提示词头部注入一段"创作沙盒"提示词
* 帮助解除模型的自我审查,让输出更完整
*/
hakimiBreakLimit: false,
@@ -199,65 +236,65 @@ export const defaultSettings = {
// 错误日志
errorLogs: [],
- // 表情(用户添加的单个表情�?
+ // 表情(用户添加的单个表情)
stickers: [],
- // 用户表情开�?
+ // 用户表情开关
userStickersEnabled: true,
- // Meme 表情包功能开�?
+ // Meme 表情包功能开关
memeStickersEnabled: false,
/**
- * 【群聊提示词注入�?
- * 开启后会将 groupAuthorNote 注入到群聊系统提示词�?
+ * 【群聊提示词注入】
+ * 开启后会将 groupAuthorNote 注入到群聊系统提示词中
* 如需自定义群聊格式,修改下方 groupAuthorNote 即可
*/
groupAutoInjectPrompt: true,
groupAuthorNote: `[群聊回复格式指南]
-这是一个微信群聊场景,你需要扮演群内的角色进行回复�?
+这是一个微信群聊场景,你需要扮演群内的角色进行回复。
-【核心规则�?
-1. 每个角色只能使用自己的专属设定,不能使用其他角色的设�?
+【核心规则】
+1. 每个角色只能使用自己的专属设定,不能使用其他角色的设定
2. 每个角色只扮演自己,不能代替其他角色说话
-3. 使用 [角色名]: 内容 的格式回�?
-4. 多个角色回复时,�?||| 分隔
+3. 使用 [角色名]: 内容 的格式回复
+4. 多个角色回复时,用 ||| 分隔
5. 同一角色可以发送多条消息,也用 ||| 分隔
-【消息风格�?
-- 每条消息保持简短自然,像真实微信聊天一样(1-3句话为宜�?
-- 可以使用表情符号增加表现�?
+【消息风格】
+- 每条消息保持简短自然,像真实微信聊天一样(1-3句话为宜)
+- 可以使用表情符号增加表现力
- 保持角色性格,让对话有趣生动
- 角色之间可以互动、吐槽、附和、反驳等
-【回复要求�?
-- 根据聊天内容自然判断哪些角色会回复,不需要所有人都说�?
+【回复要求】
+- 根据聊天内容自然判断哪些角色会回复,不需要所有人都说话
- 语音消息格式:[角色名]: [语音:内容]
-- 语音消息必须独立发�?
+- 语音消息必须独立发送
-示例�?
-[角色A]: 你说得对|||[角色B]: 我不太同意诶|||[角色A]: 为什么啊�?
+示例:
+[角色A]: 你说得对|||[角色B]: 我不太同意诶|||[角色A]: 为什么啊?
[角色A]: [语音:哈哈哈笑死我了]
[角色B]: @角色A 你是不是傻|||开玩笑的啦`,
userGroupAuthorNote: '', // 用户自定义群聊提示词(界面显示用,留空则使用内置模板)
};
-// 作者注释模�?
-export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每次必须使用以下格式输出消息内容,不用生成除此之外的任何其他内容和文本。不得省略任何格式部分�?
+// 作者注释模板
+export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每次必须使用以下格式输出消息内容,不用生成除此之外的任何其他内容和文本。不得省略任何格式部分。
-【核心规�?- 必须遵守�?
-- 每条消息都是独立的,�?||| 分隔多条消息
-- 保持消息简短自然,像真实微信聊天(1-3句话为宜�?
-- 绝对禁止代替{{user}}发言,严禁替{{user}}回复消息,严禁扮演{{user}},严禁捏造输出{{user}}的消�?
+【核心规则 - 必须遵守】
+- 每条消息都是独立的,用 ||| 分隔多条消息
+- 保持消息简短自然,像真实微信聊天(1-3句话为宜)
+- 绝对禁止代替{{user}}发言,严禁替{{user}}回复消息,严禁扮演{{user}},严禁捏造输出{{user}}的消息
- 只输出角色的消息内容,禁止添加任何旁白、心理描写或场景说明
-【消息数量规�?- 重要�?
-- 根据情境动态调整消息数量,通常1-7条不�?
-- 禁止固定每次回复的消息数�?
+【消息数量规则 - 重要】
+- 根据情境动态调整消息数量,通常1-7条不等
+- 禁止固定每次回复的消息数量
- 模拟真实聊天节奏
-【消息类型格式�?
-- 普通消息:直接写内�?
+【消息类型格式】
+- 普通消息:直接写内容
- 语音消息:[语音:语音内容文字]
- 照片/图片/视频/自拍:[照片:媒体描述]
- 表情包回复:[表情:序号或名称]
@@ -265,27 +302,27 @@ export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每
- 撤回消息:[撤回]
- 引用回复:[回复:被引用的关键词]回复内容
-【多条消息示例�?
+【多条消息示例】
你好|||最近怎么样?
哈哈|||太好笑了|||笑死我了
[语音:好想你啊]|||什么时候有空?
-【媒体消息说明】当角色发送图片、视频、自拍等媒体时,使用照片格式并提�?-4句描述:
+【媒体消息说明】当角色发送图片、视频、自拍等媒体时,使用照片格式并提供3-4句描述:
[照片:她随手拍下窗外的晚霞,橙红色的云彩铺满天空]
[照片:一张餐厅自拍,她对着镜头比了个耶的手势,桌上摆着精致的甜点]
[照片:手机截图,显示她正在追的剧刚更新了]
-发送媒体的频率应模拟真实聊天习惯,不要过于频繁。角色会分享日常:随手拍的风景、美食、自拍、截图、录像等�?
+发送媒体的频率应模拟真实聊天习惯,不要过于频繁。角色会分享日常:随手拍的风景、美食、自拍、截图、录像等。
-【错误示�?- 绝对禁止�?
-*她微微一�? 你好�?�?错误!禁止添加动作描�?
-你好,最近怎么样?太好笑了 �?错误!没有用|||分开
-{{user}}: 我也想你 �?错误!禁止替用户发言`;
+【错误示例 - 绝对禁止】
+*她微微一笑* 你好啊 ← 错误!禁止添加动作描写
+你好,最近怎么样?太好笑了 ← 错误!没有用|||分开
+{{user}}: 我也想你 ← 错误!禁止替用户发言`;
-// 世界书名称前缀(用于生�?【可乐】和xx的聊�?格式�?
+// 世界书名称前缀(用于生成"【可乐】和xx的聊天"格式)
export const LOREBOOK_NAME_PREFIX = '【可乐】和';
export const LOREBOOK_NAME_SUFFIX = '的聊天';
-// 生成世界书名�?
+// 生成世界书名称
export function generateLorebookName(contactName) {
return `${LOREBOOK_NAME_PREFIX}${contactName}${LOREBOOK_NAME_SUFFIX}`;
}
@@ -316,8 +353,8 @@ export function getUserStickers(settings = getSettings()) {
// 解析
标签,替换为图片 HTML
export function parseMemeTag(text) {
if (!text || typeof text !== 'string') return text;
- // 匹配 任意描述+文件ID.扩展�?/meme>,只捕获文件ID部分
- // 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(�?! ? 等)的表情名�?
+ // 匹配 任意描述+文件ID.扩展名,只捕获文件ID部分
+ // 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(如 ! ? 等)的表情名称
return text.replace(/<\s*meme\s*>.*?([a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif))\s*<\s*\/\s*meme\s*>/gi, (match, fileId) => {
return `
`;
});
@@ -329,11 +366,11 @@ export function hasMemeTag(text) {
return /\s*.+?\s*<\/meme>/i.test(text);
}
-// 智能分割AI消息:处�?||| 分隔符,并将 meme/语音/照片/音乐 标签与其他文字分开
+// 智能分割AI消息:处理 ||| 分隔符,并将 meme/语音/照片/音乐 标签与其他文字分开
export function splitAIMessages(response) {
if (!response || typeof response !== 'string') return [];
- // 第一步:�?||| 分隔
+ // 第一步:用 ||| 分隔
const parts = response.split('|||').map(m => m.trim()).filter(m => m);
// 第二步:对每个部分检查是否包含需要分割的特殊标签
@@ -384,13 +421,13 @@ export function splitAIMessages(response) {
specialTags.push({ tag: match[0], index: match.index });
}
- // 查找音乐标签(带冒号格式�?
+ // 查找音乐标签(带冒号格式)
const musicRegexLocal1 = new RegExp(musicRegexWithColon.source, 'g');
while ((match = musicRegexLocal1.exec(part)) !== null) {
specialTags.push({ tag: match[0], index: match.index });
}
- // 查找音乐标签(无冒号格式�?
+ // 查找音乐标签(无冒号格式)
const musicRegexLocal2 = new RegExp(musicRegexNoColon.source, 'g');
while ((match = musicRegexLocal2.exec(part)) !== null) {
// 避免重复匹配(如果已经被带冒号的匹配到)
@@ -415,7 +452,7 @@ export function splitAIMessages(response) {
specialTags.push({ tag: match[0], index: match.index });
}
- // 如果没有特殊标签,直接添�?
+ // 如果没有特殊标签,直接添加
if (specialTags.length === 0) {
result.push(part);
continue;
@@ -424,7 +461,7 @@ export function splitAIMessages(response) {
// 调试日志
console.log('[可乐] splitAIMessages 分割:', { part, specialTags });
- // 按位置排�?
+ // 按位置排序
specialTags.sort((a, b) => a.index - b.index);
// 分割消息
@@ -440,7 +477,7 @@ export function splitAIMessages(response) {
lastEnd = index + tag.length;
}
- // 添加最后一个标签后的文�?
+ // 添加最后一个标签后的文字
if (lastEnd < part.length) {
const after = part.substring(lastEnd).trim();
if (after) result.push(after);
@@ -467,7 +504,7 @@ function applyDefaults(target, defaults) {
}
}
-// 初始化设�?
+// 初始化设置
export function loadSettings() {
extension_settings[extensionName] = extension_settings[extensionName] || {};
const settings = extension_settings[extensionName];
@@ -485,8 +522,8 @@ export function loadSettings() {
}
if (settings.userPersona) delete settings.userPersona;
- // 迁移:旧�?aiStickers -> stickers(“添加的单个表情”)
- // 说明:如果用户已经有自己�?stickers,则不再合并�?aiStickers(避免把旧默�?catbox 列表灌进去)�?
+ // 迁移:旧的 aiStickers -> stickers("添加的单个表情")
+ // 说明:如果用户已经有自己的 stickers,则不再合并旧 aiStickers(避免把旧默认 catbox 列表灌进去)。
const hasUserStickers = Array.isArray(settings.stickers) &&
settings.stickers.some(s => typeof s?.url === 'string' && s.url.trim());
@@ -517,7 +554,7 @@ export function loadSettings() {
if (!Array.isArray(settings.stickers)) settings.stickers = [];
- // 迁移:旧�?aiStickersEnabled -> userStickersEnabled
+ // 迁移:旧的 aiStickersEnabled -> userStickersEnabled
if (settings.aiStickersEnabled !== undefined) {
if (settings.userStickersEnabled === undefined) {
settings.userStickersEnabled = settings.aiStickersEnabled;
diff --git a/contacts.js b/contacts.js
index 8637781..e79deb9 100644
--- a/contacts.js
+++ b/contacts.js
@@ -2,10 +2,11 @@
* 联系人管理
*/
-import { saveSettingsDebounced } from '../../../../script.js';
+import { requestSave, saveNow } from './save-manager.js';
import { getSettings, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js';
import { generateContactsList } from './ui.js';
import { showToast } from './toast.js';
+import { selectAndCrop } from './cropper.js';
// 当前换头像的联系人索引
let pendingAvatarContactIndex = -1;
@@ -40,7 +41,7 @@ export function addContact(characterData) {
customHakimiBreakLimit: false
});
- saveSettingsDebounced();
+ requestSave();
refreshContactsList();
return true;
}
@@ -65,7 +66,7 @@ export function deleteContact(index) {
deleteContactLorebooks(contact);
settings.contacts.splice(index, 1);
- saveSettingsDebounced();
+ saveNow();
refreshContactsList();
}
}
@@ -109,7 +110,7 @@ export function deleteGroupChat(groupIndex) {
deleteGroupLorebooks(group, settings);
groupChats.splice(groupIndex, 1);
- saveSettingsDebounced();
+ requestSave();
refreshContactsList();
// 同时刷新聊天列表
import('./ui.js').then(m => m.refreshChatList());
@@ -142,41 +143,21 @@ function deleteGroupLorebooks(group, settings) {
// 更换角色头像(在设置弹窗中使用)
export function changeContactAvatar(contactIndex) {
pendingAvatarContactIndex = contactIndex;
- let input = document.getElementById('wechat-contact-avatar-input');
- if (!input) {
- input = document.createElement('input');
- input.type = 'file';
- input.id = 'wechat-contact-avatar-input';
- input.accept = 'image/*';
- input.style.display = 'none';
- document.body.appendChild(input);
- input.addEventListener('change', async (e) => {
- const file = e.target.files[0];
- if (!file || pendingAvatarContactIndex < 0) return;
+ // 使用裁剪器选择并裁剪头像(1:1比例)
+ selectAndCrop(1, (croppedImage) => {
+ if (pendingAvatarContactIndex < 0) return;
- try {
- const reader = new FileReader();
- reader.onload = function(event) {
- const settings = getSettings();
- if (settings.contacts[pendingAvatarContactIndex]) {
- settings.contacts[pendingAvatarContactIndex].avatar = event.target.result;
- saveSettingsDebounced();
- refreshContactsList();
- // 更新弹窗中的头像预览
- updateContactSettingsAvatar(pendingAvatarContactIndex);
- showToast('角色头像已更换');
- }
- };
- reader.readAsDataURL(file);
- } catch (err) {
- console.error('[可乐] 更换角色头像失败:', err);
- showToast('更换头像失败: ' + err.message, '❌');
- }
- e.target.value = '';
- });
- }
- input.click();
+ const settings = getSettings();
+ if (settings.contacts[pendingAvatarContactIndex]) {
+ settings.contacts[pendingAvatarContactIndex].avatar = croppedImage;
+ requestSave();
+ refreshContactsList();
+ // 更新弹窗中的头像预览
+ updateContactSettingsAvatar(pendingAvatarContactIndex);
+ showToast('角色头像已更换');
+ }
+ });
}
// 更新弹窗中的头像预览
@@ -262,7 +243,7 @@ export function saveContactSettings() {
// 保存哈基米破限
contact.customHakimiBreakLimit = document.getElementById('wechat-contact-hakimi-toggle')?.classList.contains('on') || false;
- saveSettingsDebounced();
+ requestSave();
showToast('角色设置已保存');
// 关闭弹窗
@@ -450,7 +431,7 @@ function deleteContactDirect(index) {
deleteContactLorebooks(contact);
settings.contacts.splice(index, 1);
- saveSettingsDebounced();
+ requestSave();
refreshContactsList();
}
diff --git a/cropper.js b/cropper.js
new file mode 100644
index 0000000..cf3f0ab
--- /dev/null
+++ b/cropper.js
@@ -0,0 +1,390 @@
+/**
+ * 通用图片裁剪器模块
+ * 支持不同比例的裁剪(头像1:1, 封面16:9等)
+ */
+
+import { showToast } from './toast.js';
+
+// 裁剪器状态
+let cropperState = {
+ image: null,
+ canvas: null,
+ ctx: null,
+ imageWidth: 0,
+ imageHeight: 0,
+ imageX: 0,
+ imageY: 0,
+ cropBox: { x: 0, y: 0, width: 100, height: 100 },
+ isDragging: false,
+ isResizing: false,
+ dragStart: { x: 0, y: 0 },
+ boxStart: { x: 0, y: 0, width: 0, height: 0 },
+ resizeHandle: null,
+ aspectRatio: 1, // 宽高比
+ callback: null // 裁剪完成回调
+};
+
+/**
+ * 初始化裁剪器事件
+ */
+export function initCropper() {
+ // 取消按钮
+ document.getElementById('wechat-cropper-cancel')?.addEventListener('click', closeCropper);
+
+ // 确认按钮
+ document.getElementById('wechat-cropper-confirm')?.addEventListener('click', confirmCrop);
+
+ // 裁剪框拖拽事件
+ const cropperBox = document.getElementById('wechat-cropper-box');
+ if (cropperBox) {
+ cropperBox.addEventListener('mousedown', handleCropBoxMouseDown);
+ cropperBox.addEventListener('touchstart', handleCropBoxTouchStart, { passive: false });
+ }
+
+ // 全局移动和释放事件
+ document.addEventListener('mousemove', handleCropperMouseMove);
+ document.addEventListener('mouseup', handleCropperMouseUp);
+ document.addEventListener('touchmove', handleCropperTouchMove, { passive: false });
+ document.addEventListener('touchend', handleCropperTouchEnd);
+
+ // 四角拖拽手柄
+ document.querySelectorAll('.wechat-cropper-handle').forEach(handle => {
+ handle.addEventListener('mousedown', (e) => handleResizeStart(e, handle));
+ handle.addEventListener('touchstart', (e) => handleResizeTouchStart(e, handle), { passive: false });
+ });
+}
+
+/**
+ * 打开裁剪器
+ * @param {string} imageSrc - 图片数据URL
+ * @param {number} aspectRatio - 宽高比 (例如 1 表示 1:1, 16/9 表示 16:9)
+ * @param {function} callback - 裁剪完成回调函数,接收裁剪后的base64图片
+ */
+export function openCropper(imageSrc, aspectRatio = 1, callback = null) {
+ const modal = document.getElementById('wechat-cropper-modal');
+ const canvas = document.getElementById('wechat-cropper-canvas');
+ const container = document.getElementById('wechat-cropper-container');
+
+ if (!modal || !canvas || !container) return;
+
+ cropperState.aspectRatio = aspectRatio;
+ cropperState.callback = callback;
+
+ const img = new Image();
+ img.onload = () => {
+ cropperState.image = img;
+ cropperState.canvas = canvas;
+ cropperState.ctx = canvas.getContext('2d');
+
+ // 计算适应容器的尺寸
+ const containerWidth = container.clientWidth || 320;
+ const containerHeight = container.clientHeight || 320;
+
+ const scale = Math.min(
+ containerWidth / img.width,
+ containerHeight / img.height
+ );
+
+ const displayWidth = img.width * scale;
+ const displayHeight = img.height * scale;
+
+ canvas.width = displayWidth;
+ canvas.height = displayHeight;
+
+ cropperState.imageWidth = displayWidth;
+ cropperState.imageHeight = displayHeight;
+ cropperState.imageX = (containerWidth - displayWidth) / 2;
+ cropperState.imageY = (containerHeight - displayHeight) / 2;
+
+ cropperState.ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
+
+ // 初始化裁剪框(居中,保持比例)
+ initCropBox();
+
+ modal.classList.remove('hidden');
+ updateCropBoxUI();
+ };
+ img.src = imageSrc;
+}
+
+/**
+ * 根据宽高比初始化裁剪框
+ */
+function initCropBox() {
+ const { imageWidth, imageHeight, aspectRatio } = cropperState;
+
+ let boxWidth, boxHeight;
+
+ if (aspectRatio >= 1) {
+ // 宽 >= 高的比例(如 1:1, 16:9)
+ boxWidth = Math.min(imageWidth * 0.8, imageHeight * 0.8 * aspectRatio);
+ boxHeight = boxWidth / aspectRatio;
+ } else {
+ // 高 > 宽的比例(如 9:16)
+ boxHeight = Math.min(imageHeight * 0.8, imageWidth * 0.8 / aspectRatio);
+ boxWidth = boxHeight * aspectRatio;
+ }
+
+ // 确保裁剪框不超过图片边界
+ boxWidth = Math.min(boxWidth, imageWidth);
+ boxHeight = Math.min(boxHeight, imageHeight);
+
+ cropperState.cropBox = {
+ x: (imageWidth - boxWidth) / 2,
+ y: (imageHeight - boxHeight) / 2,
+ width: boxWidth,
+ height: boxHeight
+ };
+}
+
+/**
+ * 更新裁剪框UI
+ */
+function updateCropBoxUI() {
+ const cropBox = document.getElementById('wechat-cropper-box');
+ const canvas = cropperState.canvas;
+
+ if (!cropBox || !canvas) return;
+
+ const container = document.getElementById('wechat-cropper-container');
+ if (!container) return;
+
+ // 计算偏移(使裁剪框相对于容器居中的canvas)
+ const offsetX = (container.clientWidth - canvas.width) / 2;
+ const offsetY = (container.clientHeight - canvas.height) / 2;
+
+ cropBox.style.left = (cropperState.cropBox.x + offsetX) + 'px';
+ cropBox.style.top = (cropperState.cropBox.y + offsetY) + 'px';
+ cropBox.style.width = cropperState.cropBox.width + 'px';
+ cropBox.style.height = cropperState.cropBox.height + 'px';
+}
+
+// 裁剪框拖拽开始
+function handleCropBoxMouseDown(e) {
+ if (e.target.classList.contains('wechat-cropper-handle')) return;
+
+ e.preventDefault();
+ cropperState.isDragging = true;
+ cropperState.dragStart = { x: e.clientX, y: e.clientY };
+ cropperState.boxStart = { ...cropperState.cropBox };
+}
+
+function handleCropBoxTouchStart(e) {
+ if (e.target.classList.contains('wechat-cropper-handle')) return;
+
+ e.preventDefault();
+ const touch = e.touches[0];
+ cropperState.isDragging = true;
+ cropperState.dragStart = { x: touch.clientX, y: touch.clientY };
+ cropperState.boxStart = { ...cropperState.cropBox };
+}
+
+// 四角拖拽开始
+function handleResizeStart(e, handle) {
+ e.preventDefault();
+ e.stopPropagation();
+ cropperState.isResizing = true;
+ cropperState.resizeHandle = handle.classList.contains('nw') ? 'nw' :
+ handle.classList.contains('ne') ? 'ne' :
+ handle.classList.contains('sw') ? 'sw' : 'se';
+ cropperState.dragStart = { x: e.clientX, y: e.clientY };
+ cropperState.boxStart = { ...cropperState.cropBox };
+}
+
+function handleResizeTouchStart(e, handle) {
+ e.preventDefault();
+ e.stopPropagation();
+ const touch = e.touches[0];
+ cropperState.isResizing = true;
+ cropperState.resizeHandle = handle.classList.contains('nw') ? 'nw' :
+ handle.classList.contains('ne') ? 'ne' :
+ handle.classList.contains('sw') ? 'sw' : 'se';
+ cropperState.dragStart = { x: touch.clientX, y: touch.clientY };
+ cropperState.boxStart = { ...cropperState.cropBox };
+}
+
+function handleCropperMouseMove(e) {
+ if (!cropperState.isDragging && !cropperState.isResizing) return;
+
+ const dx = e.clientX - cropperState.dragStart.x;
+ const dy = e.clientY - cropperState.dragStart.y;
+
+ if (cropperState.isDragging) {
+ moveCropBox(dx, dy);
+ } else if (cropperState.isResizing) {
+ resizeCropBox(dx, dy);
+ }
+}
+
+function handleCropperTouchMove(e) {
+ if (!cropperState.isDragging && !cropperState.isResizing) return;
+
+ e.preventDefault();
+ const touch = e.touches[0];
+ const dx = touch.clientX - cropperState.dragStart.x;
+ const dy = touch.clientY - cropperState.dragStart.y;
+
+ if (cropperState.isDragging) {
+ moveCropBox(dx, dy);
+ } else if (cropperState.isResizing) {
+ resizeCropBox(dx, dy);
+ }
+}
+
+// 移动裁剪框
+function moveCropBox(dx, dy) {
+ let newX = cropperState.boxStart.x + dx;
+ let newY = cropperState.boxStart.y + dy;
+
+ // 限制在图片范围内
+ newX = Math.max(0, Math.min(newX, cropperState.imageWidth - cropperState.cropBox.width));
+ newY = Math.max(0, Math.min(newY, cropperState.imageHeight - cropperState.cropBox.height));
+
+ cropperState.cropBox.x = newX;
+ cropperState.cropBox.y = newY;
+ updateCropBoxUI();
+}
+
+// 调整裁剪框大小(保持宽高比)
+function resizeCropBox(dx, dy) {
+ const { aspectRatio } = cropperState;
+ const handle = cropperState.resizeHandle;
+ let { x, y, width, height } = cropperState.boxStart;
+
+ // 根据拖动的角计算新尺寸
+ let delta;
+
+ switch (handle) {
+ case 'se': // 右下角
+ delta = Math.max(dx, dy / aspectRatio);
+ width = Math.max(50, width + delta);
+ height = width / aspectRatio;
+ break;
+ case 'sw': // 左下角
+ delta = Math.max(-dx, dy / aspectRatio);
+ width = Math.max(50, width + delta);
+ height = width / aspectRatio;
+ x = cropperState.boxStart.x + cropperState.boxStart.width - width;
+ break;
+ case 'ne': // 右上角
+ delta = Math.max(dx, -dy / aspectRatio);
+ width = Math.max(50, width + delta);
+ height = width / aspectRatio;
+ y = cropperState.boxStart.y + cropperState.boxStart.height - height;
+ break;
+ case 'nw': // 左上角
+ delta = Math.max(-dx, -dy / aspectRatio);
+ width = Math.max(50, width + delta);
+ height = width / aspectRatio;
+ x = cropperState.boxStart.x + cropperState.boxStart.width - width;
+ y = cropperState.boxStart.y + cropperState.boxStart.height - height;
+ break;
+ }
+
+ // 限制边界
+ if (x < 0) {
+ width = width + x;
+ height = width / aspectRatio;
+ x = 0;
+ }
+ if (y < 0) {
+ height = height + y;
+ width = height * aspectRatio;
+ y = 0;
+ }
+ if (x + width > cropperState.imageWidth) {
+ width = cropperState.imageWidth - x;
+ height = width / aspectRatio;
+ }
+ if (y + height > cropperState.imageHeight) {
+ height = cropperState.imageHeight - y;
+ width = height * aspectRatio;
+ }
+
+ // 最小尺寸限制
+ if (width < 50 || height < 50) return;
+
+ cropperState.cropBox = { x, y, width, height };
+ updateCropBoxUI();
+}
+
+function handleCropperMouseUp() {
+ cropperState.isDragging = false;
+ cropperState.isResizing = false;
+}
+
+function handleCropperTouchEnd() {
+ cropperState.isDragging = false;
+ cropperState.isResizing = false;
+}
+
+/**
+ * 关闭裁剪器
+ */
+export function closeCropper() {
+ document.getElementById('wechat-cropper-modal')?.classList.add('hidden');
+ cropperState.image = null;
+ cropperState.callback = null;
+}
+
+/**
+ * 确认裁剪
+ */
+function confirmCrop() {
+ if (!cropperState.image || !cropperState.canvas) {
+ showToast('裁剪失败', 'info');
+ return;
+ }
+
+ // 计算原图裁剪区域
+ const scaleX = cropperState.image.width / cropperState.imageWidth;
+ const scaleY = cropperState.image.height / cropperState.imageHeight;
+
+ const cropX = cropperState.cropBox.x * scaleX;
+ const cropY = cropperState.cropBox.y * scaleY;
+ const cropWidth = cropperState.cropBox.width * scaleX;
+ const cropHeight = cropperState.cropBox.height * scaleY;
+
+ // 创建裁剪后的画布
+ const croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ const croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ cropperState.image,
+ cropX, cropY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ const croppedDataUrl = croppedCanvas.toDataURL('image/jpeg', 0.9);
+
+ // 调用回调
+ if (cropperState.callback) {
+ cropperState.callback(croppedDataUrl);
+ }
+
+ closeCropper();
+}
+
+/**
+ * 便捷方法:选择文件并打开裁剪器
+ * @param {number} aspectRatio - 宽高比
+ * @param {function} callback - 裁剪完成回调
+ */
+export function selectAndCrop(aspectRatio, callback) {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'image/*';
+ input.onchange = (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ openCropper(event.target.result, aspectRatio, callback);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+ input.click();
+}
diff --git a/emoji-panel.js b/emoji-panel.js
index 0dcceeb..5f586b5 100644
--- a/emoji-panel.js
+++ b/emoji-panel.js
@@ -2,7 +2,7 @@
* 表情面板功能
*/
-import { saveSettingsDebounced } from '../../../../script.js';
+import { requestSave } from './save-manager.js';
import { getSettings } from './config.js';
import { showToast } from './toast.js';
import { isInGroupChat } from './group-chat.js';
@@ -244,7 +244,7 @@ function addStickersFromInput(inputs) {
// 检查是否已存在
const exists = settings.stickers.some(s => s.url === url);
if (exists) {
- showToast(`已存在: ${name}`, '🧊');
+ showToast(`已存在: ${name}`, 'info');
continue;
}
@@ -260,7 +260,7 @@ function addStickersFromInput(inputs) {
}
if (addedCount > 0) {
- saveSettingsDebounced();
+ requestSave();
refreshEmojiGrid();
showToast(`已添加 ${addedCount} 个表情`);
}
@@ -299,7 +299,7 @@ function addStickerFromFile() {
}
if (addedCount > 0) {
- saveSettingsDebounced();
+ requestSave();
refreshEmojiGrid();
showToast(`已添加 ${addedCount} 个表情`);
}
@@ -369,7 +369,7 @@ function deleteSticker(index) {
if (index >= 0 && index < stickers.length) {
stickers.splice(index, 1);
- saveSettingsDebounced();
+ requestSave();
refreshEmojiGrid();
showToast('表情已删除');
}
@@ -392,7 +392,7 @@ export function initEmojiPanel() {
const tabName = tab.dataset.tab;
if (tabName === 'search') {
- showToast('搜索功能开发中...', '🧊');
+ showToast('搜索功能开发中...', 'info');
}
});
});
diff --git a/favorites.js b/favorites.js
index 530180d..d110c3a 100644
--- a/favorites.js
+++ b/favorites.js
@@ -2,7 +2,7 @@
* 收藏/世界书管理
*/
-import { saveSettingsDebounced } from '../../../../script.js';
+import { requestSave } from './save-manager.js';
import { world_names, loadWorldInfo, saveWorldInfo } from '../../../world-info.js';
import { getSettings } from './config.js';
import { escapeHtml } from './utils.js';
@@ -178,7 +178,7 @@ export function toggleFavoritesItem(type, index, enabled) {
}
}
- saveSettingsDebounced();
+ requestSave();
}
// 移除收藏项
@@ -190,7 +190,7 @@ export function removeFavoritesItem(type, index) {
if (!persona) return;
if (confirm(`确定移除「${persona.name || '用户设定'}」?`)) {
settings.userPersonas.splice(index, 1);
- saveSettingsDebounced();
+ requestSave();
refreshFavoritesList();
showToast('已移除');
}
@@ -199,7 +199,7 @@ export function removeFavoritesItem(type, index) {
if (!lorebook) return;
if (confirm(`确定移除「${lorebook.name}」?`)) {
settings.selectedLorebooks.splice(index, 1);
- saveSettingsDebounced();
+ requestSave();
refreshFavoritesList();
showToast('已移除');
}
@@ -381,7 +381,7 @@ function showNewPersonaModal() {
settings.userPersonas.push({ name, content, enabled: true, addedTime: timeStr });
- saveSettingsDebounced();
+ requestSave();
modal.remove();
refreshFavoritesList();
});
@@ -412,7 +412,7 @@ function bindPersonaPanelEvents(panel, personaIdx) {
if (settings.userPersonas?.[personaIdx]) {
settings.userPersonas[personaIdx].name = name;
settings.userPersonas[personaIdx].content = content;
- saveSettingsDebounced();
+ requestSave();
showToast('已保存');
refreshFavoritesList();
closeUserPersonaDetail();
@@ -443,7 +443,7 @@ function bindPersonaPanelEvents(panel, personaIdx) {
panel.querySelector('#wechat-persona-delete').addEventListener('click', () => {
if (confirm('确定删除此用户设定?')) {
settings.userPersonas.splice(personaIdx, 1);
- saveSettingsDebounced();
+ requestSave();
closeUserPersonaDetail();
refreshFavoritesList();
}
@@ -477,7 +477,7 @@ async function syncPersonaToTavern(name, content) {
// 保存设置
if (typeof SillyTavern !== 'undefined' && SillyTavern.saveSettingsDebounced) {
- await SillyTavern.saveSettingsDebounced();
+ await SillyTavern.requestSave();
}
// 尝试执行同步命令
@@ -628,7 +628,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
const settings = getSettings();
if (settings.selectedLorebooks?.[lorebookIdx]?.entries?.[entryIdx]) {
settings.selectedLorebooks[lorebookIdx].entries[entryIdx].enabled = toggle.checked;
- saveSettingsDebounced();
+ requestSave();
// 同步到酒馆
await syncLorebookToTavern(lorebook.name, lorebookIdx);
}
@@ -695,7 +695,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
entry.comment = comment;
entry.keys = keys;
entry.content = content;
- saveSettingsDebounced();
+ requestSave();
// 同步到酒馆
btn.disabled = true;
@@ -735,7 +735,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
if (confirm(`确定移除「${lorebook.name}」?`)) {
const settings = getSettings();
settings.selectedLorebooks.splice(lorebookIdx, 1);
- saveSettingsDebounced();
+ requestSave();
closeLorebookDetail();
refreshFavoritesList();
}
@@ -871,7 +871,7 @@ export async function refreshLorebookFromTavern(name, lorebookIdx) {
if (settings.selectedLorebooks?.[lorebookIdx]) {
settings.selectedLorebooks[lorebookIdx].entries = entries;
settings.selectedLorebooks[lorebookIdx].lastUpdated = new Date().toISOString();
- saveSettingsDebounced();
+ requestSave();
}
}
@@ -992,7 +992,7 @@ export function showAddPersonaPanel() {
const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')}`;
settings.userPersonas.push({ name: name || '用户设定', content, enabled: true, addedTime: timeStr });
- saveSettingsDebounced();
+ requestSave();
modal.remove();
refreshFavoritesList();
@@ -1079,7 +1079,7 @@ export async function addLorebookToFavorites(name) {
fromCharacter: false // 标记为全局世界书
});
- saveSettingsDebounced();
+ requestSave();
refreshFavoritesList('global');
showToast(`已导入「${name}」为全局世界书`);
} catch (err) {
@@ -1171,7 +1171,7 @@ export async function syncCharacterBookToTavern(charData) {
});
}
- saveSettingsDebounced();
+ requestSave();
// 尝试同步到酒馆世界书系统
if (typeof saveWorldInfo === 'function') {
diff --git a/group-chat.js b/group-chat.js
index 95c5e6a..efba3c8 100644
--- a/group-chat.js
+++ b/group-chat.js
@@ -2,7 +2,7 @@
* 群聊功能
*/
-import { saveSettingsDebounced } from '../../../../script.js';
+import { requestSave, saveNow } from './save-manager.js';
import { getContext } from '../../../extensions.js';
import { getSettings, SUMMARY_MARKER_PREFIX, getUserStickers, parseMemeTag, MEME_PROMPT_TEMPLATE, splitAIMessages } from './config.js';
import { showToast } from './toast.js';
@@ -26,9 +26,28 @@ const GROUP_CHAT_MAX_AI_MEMBERS = 3;
// 检查群聊记录是否需要总结提醒
function checkGroupSummaryReminder(groupChat) {
if (!groupChat || !groupChat.chatHistory) return;
- const count = groupChat.chatHistory.length;
- if (count >= GROUP_CHAT_SUMMARY_REMINDER_THRESHOLD) {
- showToast(`群聊记录已达${count}条,建议总结`, '⚠️', 4000);
+
+ // 查找最后一个总结标记的位置
+ let lastMarkerIndex = -1;
+ for (let i = groupChat.chatHistory.length - 1; i >= 0; i--) {
+ if (groupChat.chatHistory[i].content?.startsWith(SUMMARY_MARKER_PREFIX) || groupChat.chatHistory[i].isMarker) {
+ lastMarkerIndex = i;
+ break;
+ }
+ }
+
+ // 计算标记之后的消息数量(不含标记本身)
+ const newMsgCount = groupChat.chatHistory.slice(lastMarkerIndex + 1).filter(
+ m => !m.content?.startsWith(SUMMARY_MARKER_PREFIX) && !m.isMarker
+ ).length;
+
+ // 只在刚好达到阈值时提醒一次(通过标记位避免重复提醒)
+ if (newMsgCount >= GROUP_CHAT_SUMMARY_REMINDER_THRESHOLD && !groupChat._summaryReminderShown) {
+ groupChat._summaryReminderShown = true;
+ showToast(`群聊记录已达${newMsgCount}条,建议总结`, '⚠️', 2500);
+ } else if (newMsgCount < GROUP_CHAT_SUMMARY_REMINDER_THRESHOLD) {
+ // 如果消息数低于阈值(可能是总结后),重置标记
+ groupChat._summaryReminderShown = false;
}
}
@@ -222,7 +241,7 @@ export function enforceGroupChatMemberLimit(groupChat, { toast = false } = {}) {
const trimmed = memberIds.slice(0, GROUP_CHAT_MAX_AI_MEMBERS);
groupChat.memberIds = trimmed;
- saveSettingsDebounced();
+ requestSave();
if (toast) {
showToast(`群聊最多 ${GROUP_CHAT_MAX_AI_MEMBERS} 个成员(+你=4),已自动裁剪`, '⚠️');
@@ -400,7 +419,7 @@ export function showGroupCreateModal() {
const apiKey = keyInput?.value?.trim();
if (!apiUrl) {
- showToast('请先填写API地址', '🧊');
+ showToast('请先填写API地址', 'info');
return;
}
@@ -417,7 +436,7 @@ export function showGroupCreateModal() {
models.map(m => ``).join('');
showToast(`获取到 ${models.length} 个模型`);
} else {
- showToast('未找到可用模型', '🧊');
+ showToast('未找到可用模型', 'info');
}
} catch (err) {
console.error('[可乐] 获取模型失败:', err);
@@ -446,7 +465,7 @@ export function showGroupCreateModal() {
// 更新图标
apiToggle.textContent = contact.useCustomApi ? '⚙️' : '▼';
- saveSettingsDebounced();
+ requestSave();
};
item.querySelector('.wechat-group-api-url')?.addEventListener('change', saveApiConfig);
@@ -462,7 +481,7 @@ export function showGroupCreateModal() {
hakimiToggle.classList.toggle('on');
contact.customHakimiBreakLimit = hakimiToggle.classList.contains('on');
- saveSettingsDebounced();
+ requestSave();
});
});
}
@@ -561,7 +580,7 @@ export function createGroupChat() {
if (!settings.groupChats) settings.groupChats = [];
settings.groupChats.push(groupChat);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
closeGroupCreateModal();
@@ -640,6 +659,69 @@ function renderGroupChatHistory(groupChat, members, chatHistory) {
const isSticker = msg.isSticker === true;
const isPhoto = msg.isPhoto === true;
const isMusic = msg.isMusic === true;
+ const isGroupRedPacket = msg.isGroupRedPacket === true;
+ const isGroupTransfer = msg.isGroupTransfer === true;
+
+ // 群红包消息
+ if (isGroupRedPacket && msg.groupRedPacketInfo) {
+ const rpInfo = msg.groupRedPacketInfo;
+ const isDesignated = rpInfo.type === 'designated';
+ const isClaimed = rpInfo.status === 'claimed' || (rpInfo.claimedBy && rpInfo.claimedBy.length >= rpInfo.count);
+ const statusClass = isClaimed ? 'claimed' : '';
+ const designatedLabel = isDesignated ? `给${(rpInfo.targetMemberNames || []).join('、') || '指定成员'}的红包
` : '';
+
+ if (msg.role === 'user') {
+ html += `
+
+
${getUserAvatarHTML()}
+
+
+
+
+
+
+
${escapeHtml(rpInfo.message || '恭喜发财,大吉大利')}
+ ${designatedLabel}
+
${isClaimed ? '已领完' : ''}
+
+
+
+
+
+ `;
+ }
+ return;
+ }
+
+ // 群转账消息
+ if (isGroupTransfer && msg.groupTransferInfo) {
+ const tfInfo = msg.groupTransferInfo;
+ const statusText = tfInfo.status === 'received' ? '已收款' :
+ tfInfo.status === 'refunded' ? '已退还' : '待收款';
+ const statusClass = tfInfo.status || 'pending';
+
+ if (msg.role === 'user') {
+ html += `
+
+
${getUserAvatarHTML()}
+
+
+
+
+
+
+
¥${tfInfo.amount.toFixed(2)}
+
向${escapeHtml(tfInfo.targetMemberName)}转账
+
${escapeHtml(tfInfo.description) || '转账'}
+
+
${statusText}
+
+
+
+ `;
+ }
+ return;
+ }
if (msg.role === 'user') {
// 用户消息
@@ -2054,7 +2136,7 @@ async function syncGroupMembersLorebooks(members, settings) {
}
if (hasChanges) {
- saveSettingsDebounced();
+ requestSave();
}
}
@@ -2132,7 +2214,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
}
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
// 显示打字指示器
showGroupTypingIndicator(members[0]?.name, members[0]?.id);
@@ -2278,7 +2360,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
}
groupChat.lastMessageTime = Date.now();
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkGroupSummaryReminder(groupChat);
@@ -2287,7 +2369,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
console.error('[可乐] 群聊 AI 调用失败:', err);
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
- saveSettingsDebounced();
+ requestSave();
}
}
@@ -2354,7 +2436,7 @@ export async function sendGroupStickerMessage(stickerUrl, description = '') {
groupChat.lastMessageTime = msgTimestamp;
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
// 显示消息
appendGroupStickerMessage('user', stickerUrl);
@@ -2410,14 +2492,14 @@ export async function sendGroupStickerMessage(stickerUrl, description = '') {
}
groupChat.lastMessageTime = Date.now();
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkGroupSummaryReminder(groupChat);
} catch (err) {
hideGroupTypingIndicator();
console.error('[可乐] 群聊表情消息 AI 调用失败:', err);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
}
@@ -2515,7 +2597,7 @@ export async function sendGroupPhotoMessage(description) {
groupChat.lastMessageTime = msgTimestamp;
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
// 显示消息
appendGroupPhotoMessage('user', description);
@@ -2565,14 +2647,14 @@ export async function sendGroupPhotoMessage(description) {
}
groupChat.lastMessageTime = Date.now();
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkGroupSummaryReminder(groupChat);
} catch (err) {
hideGroupTypingIndicator();
console.error('[可乐] 群聊照片消息 AI 调用失败:', err);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
}
@@ -2751,7 +2833,7 @@ export async function sendGroupBatchMessages(messages) {
groupChat.lastMessageTime = msgTimestamp;
// 立即保存,确保用户消息不会丢失
- saveSettingsDebounced();
+ saveNow();
// 第二步:调用AI(一次性)
showGroupTypingIndicator(members[0]?.name, members[0]?.id);
@@ -2798,14 +2880,14 @@ export async function sendGroupBatchMessages(messages) {
}
groupChat.lastMessageTime = Date.now();
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
checkGroupSummaryReminder(groupChat);
} catch (err) {
hideGroupTypingIndicator();
console.error('[可乐] 群聊批量消息 AI 调用失败:', err);
- saveSettingsDebounced();
+ requestSave();
refreshChatList();
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null);
}
diff --git a/group-red-packet.js b/group-red-packet.js
new file mode 100644
index 0000000..a2b85a2
--- /dev/null
+++ b/group-red-packet.js
@@ -0,0 +1,1733 @@
+/**
+ * 群聊红包/转账功能模块
+ * 支持拼手气红包、指定成员红包、群聊转账
+ */
+
+import { getSettings } from './config.js';
+import { requestSave } from './save-manager.js';
+import { showToast } from './toast.js';
+import { escapeHtml, sleep } from './utils.js';
+import { refreshChatList, getUserAvatarHTML } from './ui.js';
+import { deductFromWallet, addToWallet, getWalletBalance, generateRedPacketId } from './red-packet.js';
+import { generateTransferId } from './transfer.js';
+import { getCurrentGroupIndex, enforceGroupChatMemberLimit, appendGroupMessage, showGroupTypingIndicator, hideGroupTypingIndicator } from './group-chat.js';
+import { buildSystemPrompt } from './ai.js';
+
+// ============ 状态变量 ============
+
+// 群红包状态
+let groupRedPacketType = 'random'; // 'random' | 'designated'
+let groupRedPacketAmount = '';
+let groupRedPacketCount = '';
+let groupRedPacketMessage = '恭喜发财,大吉大利';
+let groupRedPacketSelectedMembers = []; // 指定成员红包的目标成员ID列表
+
+// 群转账状态
+let groupTransferAmount = '';
+let groupTransferDescription = '';
+let groupTransferTargetMemberId = null;
+
+// 待领取的群红包
+let pendingGroupRedPacket = null;
+let pendingGroupRedPacketIndex = -1;
+
+// ============ 工具函数 ============
+
+function getTimeStr() {
+ const now = new Date();
+ return `${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')}`;
+}
+
+/**
+ * 随机分配红包金额(拼手气)
+ * @param {number} totalAmount 总金额
+ * @param {number} count 红包个数
+ * @returns {number[]} 每个红包的金额数组
+ */
+function distributeRandomAmounts(totalAmount, count) {
+ if (count <= 0 || totalAmount <= 0) return [];
+ if (count === 1) return [totalAmount];
+
+ const amounts = [];
+ let remaining = Math.round(totalAmount * 100); // 转为分,避免浮点数精度问题
+ const minAmount = 1; // 最小1分
+
+ for (let i = 0; i < count - 1; i++) {
+ const maxForThis = remaining - (count - i - 1) * minAmount;
+ if (maxForThis <= minAmount) {
+ amounts.push(minAmount);
+ remaining -= minAmount;
+ continue;
+ }
+
+ // 20% 概率只给 0.01 元(1分)
+ if (Math.random() < 0.2) {
+ amounts.push(minAmount);
+ remaining -= minAmount;
+ continue;
+ }
+
+ // 正常随机分配(使用二倍均值法变体)
+ const avgRemaining = remaining / (count - i);
+ const maxRandom = Math.min(maxForThis, Math.floor(avgRemaining * 2));
+ const randomAmount = Math.max(minAmount, Math.floor(Math.random() * maxRandom));
+ amounts.push(randomAmount);
+ remaining -= randomAmount;
+ }
+
+ // 最后一个红包拿走剩余金额
+ amounts.push(remaining);
+
+ // 打乱顺序
+ for (let i = amounts.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [amounts[i], amounts[j]] = [amounts[j], amounts[i]];
+ }
+
+ // 转回元
+ return amounts.map(a => a / 100);
+}
+
+// ============ 群红包类型选择页面 ============
+
+/**
+ * 显示群红包类型选择页面
+ */
+export function showGroupRedPacketTypePage() {
+ const page = document.getElementById('wechat-group-rp-type-page');
+ if (!page) {
+ createGroupRedPacketPages();
+ }
+
+ // 重置状态
+ groupRedPacketType = 'random';
+ groupRedPacketAmount = '';
+ groupRedPacketCount = '';
+ groupRedPacketMessage = '恭喜发财,大吉大利';
+ groupRedPacketSelectedMembers = [];
+
+ document.getElementById('wechat-group-rp-type-page')?.classList.remove('hidden');
+}
+
+/**
+ * 隐藏群红包类型选择页面
+ */
+export function hideGroupRedPacketTypePage() {
+ document.getElementById('wechat-group-rp-type-page')?.classList.add('hidden');
+}
+
+// ============ 拼手气红包页面 ============
+
+/**
+ * 显示拼手气红包页面
+ */
+export function showGroupRandomRedPacketPage() {
+ hideGroupRedPacketTypePage();
+ groupRedPacketType = 'random';
+ groupRedPacketAmount = '';
+ groupRedPacketCount = '';
+
+ const page = document.getElementById('wechat-group-random-rp-page');
+ if (page) {
+ page.classList.remove('hidden');
+ updateGroupRandomRedPacketDisplay();
+ }
+}
+
+/**
+ * 隐藏拼手气红包页面
+ */
+export function hideGroupRandomRedPacketPage() {
+ document.getElementById('wechat-group-random-rp-page')?.classList.add('hidden');
+ document.getElementById('wechat-group-rp-keyboard')?.classList.add('hidden');
+}
+
+/**
+ * 更新拼手气红包显示
+ */
+function updateGroupRandomRedPacketDisplay() {
+ const amountEl = document.getElementById('wechat-group-rp-amount-value');
+ const countEl = document.getElementById('wechat-group-rp-count-value');
+ const totalEl = document.getElementById('wechat-group-rp-total-display');
+
+ if (amountEl) {
+ amountEl.textContent = groupRedPacketAmount || '0.00';
+ }
+ if (countEl) {
+ countEl.textContent = groupRedPacketCount || '0';
+ }
+ if (totalEl) {
+ const amount = parseFloat(groupRedPacketAmount) || 0;
+ totalEl.textContent = '¥' + amount.toFixed(2);
+ }
+}
+
+// ============ 指定成员红包页面 ============
+
+/**
+ * 显示指定成员红包页面
+ */
+export function showGroupDesignatedRedPacketPage() {
+ hideGroupRedPacketTypePage();
+ groupRedPacketType = 'designated';
+ groupRedPacketAmount = '';
+ groupRedPacketSelectedMembers = [];
+
+ const settings = getSettings();
+ const groupIndex = getCurrentGroupIndex();
+ const groupChat = settings.groupChats?.[groupIndex];
+ if (!groupChat) return;
+
+ const { memberIds } = enforceGroupChatMemberLimit(groupChat);
+ const members = memberIds.map(id => settings.contacts.find(c => c.id === id)).filter(Boolean);
+
+ // 渲染成员列表
+ const listContainer = document.getElementById('wechat-group-designated-member-list');
+ if (listContainer) {
+ listContainer.innerHTML = members.map(member => {
+ const firstChar = member.name?.charAt(0) || '?';
+ const avatarHtml = member.avatar
+ ? `
`
+ : firstChar;
+
+ return `
+
+
+
+
+
${avatarHtml}
+
${escapeHtml(member.name)}
+
+ `;
+ }).join('');
+
+ // 绑定点击事件
+ listContainer.querySelectorAll('.wechat-group-designated-member-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ const checkbox = item.querySelector('input[type="checkbox"]');
+ if (e.target !== checkbox) {
+ checkbox.checked = !checkbox.checked;
+ }
+ updateGroupDesignatedSelection();
+ });
+ });
+ }
+
+ document.getElementById('wechat-group-designated-rp-page')?.classList.remove('hidden');
+ updateGroupDesignatedRedPacketDisplay();
+}
+
+/**
+ * 更新指定成员选择
+ */
+function updateGroupDesignatedSelection() {
+ const checkboxes = document.querySelectorAll('#wechat-group-designated-member-list input[type="checkbox"]:checked');
+ groupRedPacketSelectedMembers = Array.from(checkboxes).map(cb => cb.dataset.memberId);
+
+ const countEl = document.getElementById('wechat-group-designated-count');
+ if (countEl) {
+ countEl.textContent = groupRedPacketSelectedMembers.length;
+ }
+}
+
+/**
+ * 隐藏指定成员红包页面
+ */
+export function hideGroupDesignatedRedPacketPage() {
+ document.getElementById('wechat-group-designated-rp-page')?.classList.add('hidden');
+ document.getElementById('wechat-group-designated-keyboard')?.classList.add('hidden');
+}
+
+/**
+ * 更新指定成员红包显示
+ */
+function updateGroupDesignatedRedPacketDisplay() {
+ const amountEl = document.getElementById('wechat-group-designated-amount-value');
+ const countEl = document.getElementById('wechat-group-designated-count');
+ const totalEl = document.getElementById('wechat-group-designated-total-display');
+
+ const amount = parseFloat(groupRedPacketAmount) || 0;
+ const count = groupRedPacketSelectedMembers.length;
+
+ if (amountEl) {
+ amountEl.textContent = groupRedPacketAmount || '0.00';
+ }
+ if (countEl) {
+ countEl.textContent = count;
+ }
+ if (totalEl) {
+ totalEl.textContent = '¥' + (amount * count).toFixed(2);
+ }
+}
+
+// ============ 群转账成员选择页面 ============
+
+/**
+ * 显示群转账成员选择页面
+ */
+export function showGroupTransferSelectPage() {
+ groupTransferAmount = '';
+ groupTransferDescription = '';
+ groupTransferTargetMemberId = null;
+
+ const settings = getSettings();
+ const groupIndex = getCurrentGroupIndex();
+ const groupChat = settings.groupChats?.[groupIndex];
+ if (!groupChat) return;
+
+ const { memberIds } = enforceGroupChatMemberLimit(groupChat);
+ const members = memberIds.map(id => settings.contacts.find(c => c.id === id)).filter(Boolean);
+
+ // 渲染成员列表
+ const listContainer = document.getElementById('wechat-group-transfer-member-list');
+ if (listContainer) {
+ listContainer.innerHTML = members.map(member => {
+ const firstChar = member.name?.charAt(0) || '?';
+ const avatarHtml = member.avatar
+ ? `
`
+ : firstChar;
+
+ return `
+
+
${avatarHtml}
+
${escapeHtml(member.name)}
+
+
+ `;
+ }).join('');
+
+ // 绑定点击事件
+ listContainer.querySelectorAll('.wechat-group-transfer-member-item').forEach(item => {
+ item.addEventListener('click', () => {
+ groupTransferTargetMemberId = item.dataset.memberId;
+ hideGroupTransferSelectPage();
+ showGroupTransferAmountPage();
+ });
+ });
+ }
+
+ document.getElementById('wechat-group-transfer-select-page')?.classList.remove('hidden');
+}
+
+/**
+ * 隐藏群转账成员选择页面
+ */
+export function hideGroupTransferSelectPage() {
+ document.getElementById('wechat-group-transfer-select-page')?.classList.add('hidden');
+}
+
+// ============ 群转账金额输入页面 ============
+
+/**
+ * 显示群转账金额输入页面
+ */
+export function showGroupTransferAmountPage() {
+ const settings = getSettings();
+ const targetMember = settings.contacts?.find(c => c.id === groupTransferTargetMemberId);
+ if (!targetMember) {
+ showToast('请先选择转账对象', 'info');
+ return;
+ }
+
+ // 更新页面标题显示目标成员
+ const titleEl = document.getElementById('wechat-group-transfer-target-name');
+ if (titleEl) {
+ titleEl.textContent = `向 ${targetMember.name} 转账`;
+ }
+
+ groupTransferAmount = '';
+ groupTransferDescription = '';
+
+ document.getElementById('wechat-group-transfer-amount-page')?.classList.remove('hidden');
+ updateGroupTransferAmountDisplay();
+}
+
+/**
+ * 隐藏群转账金额输入页面
+ */
+export function hideGroupTransferAmountPage() {
+ document.getElementById('wechat-group-transfer-amount-page')?.classList.add('hidden');
+ document.getElementById('wechat-group-transfer-keyboard')?.classList.add('hidden');
+}
+
+/**
+ * 更新群转账金额显示
+ */
+function updateGroupTransferAmountDisplay() {
+ const amountEl = document.getElementById('wechat-group-transfer-amount-value');
+ const displayEl = document.getElementById('wechat-group-transfer-amount-display');
+
+ const amount = parseFloat(groupTransferAmount) || 0;
+
+ if (amountEl) {
+ amountEl.textContent = groupTransferAmount || '0.00';
+ }
+ if (displayEl) {
+ displayEl.textContent = '¥' + amount.toFixed(2);
+ }
+}
+
+// ============ 键盘处理 ============
+
+let currentKeyboardTarget = null; // 'random-amount' | 'random-count' | 'designated-amount' | 'transfer-amount'
+
+/**
+ * 显示数字键盘
+ */
+export function showGroupKeyboard(target) {
+ currentKeyboardTarget = target;
+
+ let keyboardId;
+ if (target === 'random-amount' || target === 'random-count') {
+ keyboardId = 'wechat-group-rp-keyboard';
+ } else if (target === 'designated-amount') {
+ keyboardId = 'wechat-group-designated-keyboard';
+ } else if (target === 'transfer-amount') {
+ keyboardId = 'wechat-group-transfer-keyboard';
+ }
+
+ const keyboard = document.getElementById(keyboardId);
+ if (keyboard) {
+ keyboard.classList.remove('hidden');
+ }
+}
+
+/**
+ * 隐藏数字键盘
+ */
+export function hideGroupKeyboard() {
+ document.getElementById('wechat-group-rp-keyboard')?.classList.add('hidden');
+ document.getElementById('wechat-group-designated-keyboard')?.classList.add('hidden');
+ document.getElementById('wechat-group-transfer-keyboard')?.classList.add('hidden');
+ currentKeyboardTarget = null;
+}
+
+/**
+ * 处理键盘输入
+ */
+export function handleGroupKeyboardInput(key) {
+ if (!currentKeyboardTarget) return;
+
+ let currentValue;
+ let isCount = currentKeyboardTarget === 'random-count';
+
+ if (currentKeyboardTarget === 'random-amount') {
+ currentValue = groupRedPacketAmount;
+ } else if (currentKeyboardTarget === 'random-count') {
+ currentValue = groupRedPacketCount;
+ } else if (currentKeyboardTarget === 'designated-amount') {
+ currentValue = groupRedPacketAmount;
+ } else if (currentKeyboardTarget === 'transfer-amount') {
+ currentValue = groupTransferAmount;
+ }
+
+ if (key === 'backspace') {
+ currentValue = currentValue.slice(0, -1);
+ } else if (key === 'confirm') {
+ hideGroupKeyboard();
+ return;
+ } else if (key === '.') {
+ if (isCount) return; // 红包个数不允许小数点
+ if (!currentValue.includes('.') && currentValue.length > 0) {
+ currentValue += '.';
+ }
+ } else {
+ if (isCount) {
+ // 红包个数:整数,最多2位
+ if (currentValue.length < 2) {
+ currentValue += key;
+ }
+ } else {
+ // 金额
+ const dotIndex = currentValue.indexOf('.');
+ if (dotIndex !== -1) {
+ if (currentValue.length - dotIndex <= 2) {
+ currentValue += key;
+ }
+ } else {
+ if (currentValue.length < 6) {
+ currentValue += key;
+ }
+ }
+ }
+ }
+
+ // 更新状态
+ if (currentKeyboardTarget === 'random-amount') {
+ groupRedPacketAmount = currentValue;
+ updateGroupRandomRedPacketDisplay();
+ } else if (currentKeyboardTarget === 'random-count') {
+ groupRedPacketCount = currentValue;
+ updateGroupRandomRedPacketDisplay();
+ } else if (currentKeyboardTarget === 'designated-amount') {
+ groupRedPacketAmount = currentValue;
+ updateGroupDesignatedRedPacketDisplay();
+ } else if (currentKeyboardTarget === 'transfer-amount') {
+ groupTransferAmount = currentValue;
+ updateGroupTransferAmountDisplay();
+ }
+}
+
+// ============ 密码验证 ============
+
+let pendingGroupAction = null; // { type: 'random-rp' | 'designated-rp' | 'transfer', ... }
+
+/**
+ * 显示群聊密码输入弹窗
+ */
+export function showGroupPasswordModal(actionType, extraData = {}) {
+ pendingGroupAction = { type: actionType, ...extraData };
+
+ const modal = document.getElementById('wechat-group-password-modal');
+ if (modal) {
+ modal.classList.remove('hidden');
+ // 清空密码
+ const dots = modal.querySelectorAll('.wechat-password-dot');
+ dots.forEach(dot => dot.classList.remove('filled'));
+ modal.dataset.password = '';
+ }
+}
+
+/**
+ * 隐藏群聊密码输入弹窗
+ */
+export function hideGroupPasswordModal() {
+ const modal = document.getElementById('wechat-group-password-modal');
+ if (modal) {
+ modal.classList.add('hidden');
+ }
+ pendingGroupAction = null;
+}
+
+/**
+ * 处理密码输入
+ */
+export function handleGroupPasswordInput(key) {
+ const modal = document.getElementById('wechat-group-password-modal');
+ if (!modal) return;
+
+ let password = modal.dataset.password || '';
+
+ if (key === 'backspace') {
+ password = password.slice(0, -1);
+ } else if (password.length < 6) {
+ password += key;
+ }
+
+ modal.dataset.password = password;
+
+ // 更新密码点显示
+ const dots = modal.querySelectorAll('.wechat-password-dot');
+ dots.forEach((dot, index) => {
+ dot.classList.toggle('filled', index < password.length);
+ });
+
+ // 6位密码输入完成,验证
+ if (password.length === 6) {
+ const settings = getSettings();
+ const correctPassword = settings.paymentPassword || '666666';
+ if (password === correctPassword) {
+ hideGroupPasswordModal();
+ executeGroupAction();
+ } else {
+ showToast('密码错误', 'info');
+ modal.dataset.password = '';
+ dots.forEach(dot => dot.classList.remove('filled'));
+ }
+ }
+}
+
+/**
+ * 执行群聊操作
+ */
+async function executeGroupAction() {
+ if (!pendingGroupAction) return;
+
+ const actionType = pendingGroupAction.type;
+
+ if (actionType === 'random-rp') {
+ await sendGroupRandomRedPacket();
+ } else if (actionType === 'designated-rp') {
+ await sendGroupDesignatedRedPacket();
+ } else if (actionType === 'transfer') {
+ await sendGroupTransfer();
+ }
+
+ pendingGroupAction = null;
+}
+
+// ============ 发送群红包 ============
+
+/**
+ * 提交拼手气红包(显示密码输入)
+ */
+export function submitGroupRandomRedPacket() {
+ const amount = parseFloat(groupRedPacketAmount) || 0;
+ const count = parseInt(groupRedPacketCount) || 0;
+
+ if (amount <= 0) {
+ showToast('请输入红包金额', 'info');
+ return;
+ }
+ if (count <= 0) {
+ showToast('请输入红包个数', 'info');
+ return;
+ }
+ if (amount > 200) {
+ showToast('单个红包最多200元', 'info');
+ return;
+ }
+ if (amount > getWalletBalance()) {
+ showToast('余额不足', 'info');
+ return;
+ }
+
+ // 获取祝福语
+ const messageInput = document.getElementById('wechat-group-rp-message');
+ if (messageInput && messageInput.value.trim()) {
+ groupRedPacketMessage = messageInput.value.trim();
+ }
+
+ showGroupPasswordModal('random-rp');
+}
+
+/**
+ * 发送拼手气红包
+ */
+async function sendGroupRandomRedPacket() {
+ const amount = parseFloat(groupRedPacketAmount) || 0;
+ const count = parseInt(groupRedPacketCount) || 0;
+
+ // 扣款
+ const result = deductFromWallet(amount);
+ if (!result.success) {
+ showToast(result.message, 'info');
+ return;
+ }
+
+ hideGroupRandomRedPacketPage();
+
+ const settings = getSettings();
+ const groupIndex = getCurrentGroupIndex();
+ const groupChat = settings.groupChats?.[groupIndex];
+ if (!groupChat) return;
+
+ const { memberIds } = enforceGroupChatMemberLimit(groupChat);
+ const members = memberIds.map(id => settings.contacts.find(c => c.id === id)).filter(Boolean);
+
+ // 分配金额
+ const distributedAmounts = distributeRandomAmounts(amount, count);
+
+ // 创建红包信息
+ const rpInfo = {
+ id: generateRedPacketId(),
+ type: 'random',
+ totalAmount: amount,
+ count: count,
+ message: groupRedPacketMessage,
+ senderName: settings.userName || 'User',
+ distributedAmounts: distributedAmounts,
+ claimedBy: [], // { memberId, memberName, amount, claimedAt }
+ status: 'pending',
+ expireAt: Date.now() + 24 * 60 * 60 * 1000
+ };
+
+ // 保存到聊天记录
+ if (!groupChat.chatHistory) groupChat.chatHistory = [];
+ groupChat.chatHistory.push({
+ role: 'user',
+ content: `[群红包] ${groupRedPacketMessage}`,
+ time: getTimeStr(),
+ timestamp: Date.now(),
+ isGroupRedPacket: true,
+ groupRedPacketInfo: rpInfo
+ });
+
+ // 显示红包消息
+ appendGroupRedPacketMessage('user', rpInfo);
+ requestSave();
+ refreshChatList();
+
+ // AI 领取红包(随机延迟)
+ await processAIClaimGroupRedPacket(rpInfo, groupChat, members);
+}
+
+/**
+ * 提交指定成员红包(显示密码输入)
+ */
+export function submitGroupDesignatedRedPacket() {
+ const amount = parseFloat(groupRedPacketAmount) || 0;
+ const count = groupRedPacketSelectedMembers.length;
+
+ if (amount <= 0) {
+ showToast('请输入红包金额', 'info');
+ return;
+ }
+ if (count <= 0) {
+ showToast('请选择接收成员', 'info');
+ return;
+ }
+ if (amount > 200) {
+ showToast('单个红包最多200元', 'info');
+ return;
+ }
+
+ const totalAmount = amount * count;
+ if (totalAmount > getWalletBalance()) {
+ showToast('余额不足', 'info');
+ return;
+ }
+
+ // 获取祝福语
+ const messageInput = document.getElementById('wechat-group-designated-message');
+ if (messageInput && messageInput.value.trim()) {
+ groupRedPacketMessage = messageInput.value.trim();
+ }
+
+ showGroupPasswordModal('designated-rp');
+}
+
+/**
+ * 发送指定成员红包
+ */
+async function sendGroupDesignatedRedPacket() {
+ const amount = parseFloat(groupRedPacketAmount) || 0;
+ const count = groupRedPacketSelectedMembers.length;
+ const totalAmount = amount * count;
+
+ // 扣款
+ const settings = getSettings();
+ const current = getWalletBalance();
+ if (totalAmount > current) {
+ showToast('余额不足', 'info');
+ return;
+ }
+
+ settings.walletAmount = (current - totalAmount).toFixed(2);
+ requestSave();
+
+ hideGroupDesignatedRedPacketPage();
+
+ const groupIndex = getCurrentGroupIndex();
+ const groupChat = settings.groupChats?.[groupIndex];
+ if (!groupChat) return;
+
+ const { memberIds } = enforceGroupChatMemberLimit(groupChat);
+ const members = memberIds.map(id => settings.contacts.find(c => c.id === id)).filter(Boolean);
+
+ // 获取指定成员名称
+ const targetMembers = groupRedPacketSelectedMembers.map(id => {
+ const member = settings.contacts.find(c => c.id === id);
+ return member?.name || '未知';
+ });
+
+ // 创建红包信息
+ const rpInfo = {
+ id: generateRedPacketId(),
+ type: 'designated',
+ totalAmount: totalAmount,
+ amountPerPerson: amount,
+ count: count,
+ message: groupRedPacketMessage,
+ senderName: settings.userName || 'User',
+ targetMemberIds: [...groupRedPacketSelectedMembers],
+ targetMemberNames: targetMembers,
+ claimedBy: [],
+ status: 'pending',
+ expireAt: Date.now() + 24 * 60 * 60 * 1000
+ };
+
+ // 保存到聊天记录
+ if (!groupChat.chatHistory) groupChat.chatHistory = [];
+ groupChat.chatHistory.push({
+ role: 'user',
+ content: `[专属红包] 给${targetMembers.join('、')}的红包`,
+ time: getTimeStr(),
+ timestamp: Date.now(),
+ isGroupRedPacket: true,
+ groupRedPacketInfo: rpInfo
+ });
+
+ // 显示红包消息
+ appendGroupRedPacketMessage('user', rpInfo);
+ requestSave();
+ refreshChatList();
+
+ // AI 领取红包
+ await processAIClaimGroupRedPacket(rpInfo, groupChat, members);
+}
+
+// ============ 发送群转账 ============
+
+/**
+ * 提交群转账(显示密码输入)
+ */
+export function submitGroupTransfer() {
+ const amount = parseFloat(groupTransferAmount) || 0;
+
+ if (amount <= 0) {
+ showToast('请输入转账金额', 'info');
+ return;
+ }
+ if (amount > getWalletBalance()) {
+ showToast('余额不足', 'info');
+ return;
+ }
+
+ // 获取转账说明
+ const descInput = document.getElementById('wechat-group-transfer-description');
+ if (descInput && descInput.value.trim()) {
+ groupTransferDescription = descInput.value.trim();
+ }
+
+ showGroupPasswordModal('transfer');
+}
+
+/**
+ * 发送群转账
+ */
+async function sendGroupTransfer() {
+ const amount = parseFloat(groupTransferAmount) || 0;
+
+ // 扣款
+ const settings = getSettings();
+ const current = getWalletBalance();
+ if (amount > current) {
+ showToast('余额不足', 'info');
+ return;
+ }
+
+ settings.walletAmount = (current - amount).toFixed(2);
+ requestSave();
+
+ hideGroupTransferAmountPage();
+
+ const groupIndex = getCurrentGroupIndex();
+ const groupChat = settings.groupChats?.[groupIndex];
+ if (!groupChat) return;
+
+ const targetMember = settings.contacts?.find(c => c.id === groupTransferTargetMemberId);
+ if (!targetMember) return;
+
+ // 创建转账信息
+ const tfInfo = {
+ id: generateTransferId(),
+ amount: amount,
+ description: groupTransferDescription || '',
+ senderName: settings.userName || 'User',
+ targetMemberId: groupTransferTargetMemberId,
+ targetMemberName: targetMember.name,
+ status: 'pending',
+ receivedAt: null,
+ refundedAt: null,
+ expireAt: Date.now() + 24 * 60 * 60 * 1000
+ };
+
+ // 保存到聊天记录
+ if (!groupChat.chatHistory) groupChat.chatHistory = [];
+ groupChat.chatHistory.push({
+ role: 'user',
+ content: `[转账] 向${targetMember.name}发起了一笔转账`,
+ time: getTimeStr(),
+ timestamp: Date.now(),
+ isGroupTransfer: true,
+ groupTransferInfo: tfInfo
+ });
+
+ // 显示转账消息
+ appendGroupTransferMessage('user', tfInfo);
+ requestSave();
+ refreshChatList();
+
+ // AI 收款
+ await processAIReceiveGroupTransfer(tfInfo, groupChat, targetMember);
+}
+
+// ============ AI 领取红包 ============
+
+/**
+ * AI 领取群红包
+ */
+async function processAIClaimGroupRedPacket(rpInfo, groupChat, members) {
+ // 随机延迟 2-5 秒
+ const claimDelay = 2000 + Math.random() * 3000;
+ await sleep(claimDelay);
+
+ const settings = getSettings();
+ const timeStr = getTimeStr();
+
+ if (rpInfo.type === 'random') {
+ // 拼手气红包:按顺序领取
+ const availableMembers = members.filter(m => !rpInfo.claimedBy.some(c => c.memberId === m.id));
+ const claimCount = Math.min(availableMembers.length, rpInfo.distributedAmounts.length - rpInfo.claimedBy.length);
+
+ for (let i = 0; i < claimCount; i++) {
+ const member = availableMembers[i];
+ const amountIndex = rpInfo.claimedBy.length;
+ const claimAmount = rpInfo.distributedAmounts[amountIndex];
+
+ rpInfo.claimedBy.push({
+ memberId: member.id,
+ memberName: member.name,
+ amount: claimAmount,
+ claimedAt: Date.now()
+ });
+
+ // 更新界面
+ updateGroupRedPacketBubbleStatus(rpInfo.id);
+
+ // 显示领取提示
+ appendGroupRedPacketClaimNotice(member.name, settings.userName || 'User');
+
+ // AI 感谢消息
+ await sleep(800 + Math.random() * 500);
+ showGroupTypingIndicator(member.name, member.id);
+ await sleep(600 + Math.random() * 400);
+ hideGroupTypingIndicator();
+
+ try {
+ const thankMsg = await generateAIThankMessage(member, rpInfo, claimAmount);
+ if (thankMsg) {
+ groupChat.chatHistory.push({
+ role: 'assistant',
+ content: thankMsg,
+ characterName: member.name,
+ characterId: member.id,
+ time: timeStr,
+ timestamp: Date.now()
+ });
+ appendGroupMessage('assistant', thankMsg, member.name, member.id);
+ }
+ } catch (e) {
+ console.error('[可乐] AI感谢红包失败:', e);
+ }
+
+ if (i < claimCount - 1) {
+ await sleep(1000 + Math.random() * 1000);
+ }
+ }
+
+ // 检查是否全部领完
+ if (rpInfo.claimedBy.length >= rpInfo.count) {
+ rpInfo.status = 'claimed';
+ }
+ } else if (rpInfo.type === 'designated') {
+ // 指定成员红包:只有指定成员可以领取
+ for (const memberId of rpInfo.targetMemberIds) {
+ const member = members.find(m => m.id === memberId);
+ if (!member) continue;
+ if (rpInfo.claimedBy.some(c => c.memberId === memberId)) continue;
+
+ rpInfo.claimedBy.push({
+ memberId: member.id,
+ memberName: member.name,
+ amount: rpInfo.amountPerPerson,
+ claimedAt: Date.now()
+ });
+
+ // 更新界面
+ updateGroupRedPacketBubbleStatus(rpInfo.id);
+
+ // 显示领取提示
+ appendGroupRedPacketClaimNotice(member.name, settings.userName || 'User');
+
+ // AI 感谢消息
+ await sleep(800 + Math.random() * 500);
+ showGroupTypingIndicator(member.name, member.id);
+ await sleep(600 + Math.random() * 400);
+ hideGroupTypingIndicator();
+
+ try {
+ const thankMsg = await generateAIThankMessage(member, rpInfo, rpInfo.amountPerPerson);
+ if (thankMsg) {
+ groupChat.chatHistory.push({
+ role: 'assistant',
+ content: thankMsg,
+ characterName: member.name,
+ characterId: member.id,
+ time: timeStr,
+ timestamp: Date.now()
+ });
+ appendGroupMessage('assistant', thankMsg, member.name, member.id);
+ }
+ } catch (e) {
+ console.error('[可乐] AI感谢红包失败:', e);
+ }
+
+ await sleep(1000 + Math.random() * 1000);
+ }
+
+ // 检查是否全部领完
+ if (rpInfo.claimedBy.length >= rpInfo.count) {
+ rpInfo.status = 'claimed';
+ }
+ }
+
+ requestSave();
+ refreshChatList();
+}
+
+/**
+ * AI 收取群转账
+ */
+async function processAIReceiveGroupTransfer(tfInfo, groupChat, targetMember) {
+ // 随机延迟 2-5 秒
+ const receiveDelay = 2000 + Math.random() * 3000;
+ await sleep(receiveDelay);
+
+ const settings = getSettings();
+ const timeStr = getTimeStr();
+
+ // 更新转账状态
+ tfInfo.status = 'received';
+ tfInfo.receivedAt = Date.now();
+
+ // 更新界面
+ updateGroupTransferBubbleStatus(tfInfo.id, 'received');
+
+ // AI 感谢消息
+ await sleep(500);
+ showGroupTypingIndicator(targetMember.name, targetMember.id);
+ await sleep(600 + Math.random() * 400);
+ hideGroupTypingIndicator();
+
+ try {
+ const thankMsg = await generateAITransferThankMessage(targetMember, tfInfo);
+ if (thankMsg) {
+ groupChat.chatHistory.push({
+ role: 'assistant',
+ content: thankMsg,
+ characterName: targetMember.name,
+ characterId: targetMember.id,
+ time: timeStr,
+ timestamp: Date.now()
+ });
+ appendGroupMessage('assistant', thankMsg, targetMember.name, targetMember.id);
+ }
+ } catch (e) {
+ console.error('[可乐] AI感谢转账失败:', e);
+ }
+
+ requestSave();
+ refreshChatList();
+}
+
+// ============ AI 消息生成 ============
+
+/**
+ * 生成 AI 红包感谢消息
+ */
+async function generateAIThankMessage(member, rpInfo, claimAmount) {
+ if (!member.useCustomApi || !member.customApiUrl || !member.customModel) {
+ // 没有配置独立API,返回简单消息
+ return `谢谢红包!抢到了${claimAmount.toFixed(2)}元~`;
+ }
+
+ try {
+ const systemPrompt = buildSystemPrompt(member, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
+ const userPrompt = `用户给群里发了一个${rpInfo.totalAmount}元的红包,祝福语是"${rpInfo.message}"。你抢到了${claimAmount.toFixed(2)}元,请自然地表示感谢,不要使用任何特殊格式标签。回复要简短自然(10字以内)。`;
+
+ const chatUrl = member.customApiUrl.replace(/\/+$/, '') + '/chat/completions';
+ const headers = { 'Content-Type': 'application/json' };
+ if (member.customApiKey) {
+ headers['Authorization'] = `Bearer ${member.customApiKey}`;
+ }
+
+ const response = await fetch(chatUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ model: member.customModel,
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt }
+ ],
+ temperature: 1,
+ max_tokens: 256
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('API请求失败');
+ }
+
+ const data = await response.json();
+ let reply = data.choices?.[0]?.message?.content || '';
+ // 取第一条消息
+ reply = reply.split('|||')[0].trim();
+ // 移除可能的格式标签
+ reply = reply.replace(/^\[.*?\]\s*/, '').trim();
+
+ return reply || `谢谢红包!${claimAmount.toFixed(2)}元~`;
+ } catch (e) {
+ console.error('[可乐] AI红包感谢消息生成失败:', e);
+ return `谢谢红包!抢到了${claimAmount.toFixed(2)}元~`;
+ }
+}
+
+/**
+ * 生成 AI 转账感谢消息
+ */
+async function generateAITransferThankMessage(member, tfInfo) {
+ if (!member.useCustomApi || !member.customApiUrl || !member.customModel) {
+ return `收到转账${tfInfo.amount.toFixed(2)}元,谢谢~`;
+ }
+
+ try {
+ const systemPrompt = buildSystemPrompt(member, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
+ const userPrompt = tfInfo.description
+ ? `用户给你转账了${tfInfo.amount}元,备注是"${tfInfo.description}",请自然地表示感谢,不要使用任何特殊格式标签。回复要简短自然(10字以内)。`
+ : `用户给你转账了${tfInfo.amount}元,请自然地表示感谢,不要使用任何特殊格式标签。回复要简短自然(10字以内)。`;
+
+ const chatUrl = member.customApiUrl.replace(/\/+$/, '') + '/chat/completions';
+ const headers = { 'Content-Type': 'application/json' };
+ if (member.customApiKey) {
+ headers['Authorization'] = `Bearer ${member.customApiKey}`;
+ }
+
+ const response = await fetch(chatUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ model: member.customModel,
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt }
+ ],
+ temperature: 1,
+ max_tokens: 256
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('API请求失败');
+ }
+
+ const data = await response.json();
+ let reply = data.choices?.[0]?.message?.content || '';
+ reply = reply.split('|||')[0].trim();
+ reply = reply.replace(/^\[.*?\]\s*/, '').trim();
+
+ return reply || `收到啦,谢谢~`;
+ } catch (e) {
+ console.error('[可乐] AI转账感谢消息生成失败:', e);
+ return `收到转账${tfInfo.amount.toFixed(2)}元,谢谢~`;
+ }
+}
+
+// ============ UI 渲染 ============
+
+/**
+ * 追加群红包消息到界面
+ */
+export function appendGroupRedPacketMessage(role, rpInfo) {
+ const messagesContainer = document.getElementById('wechat-chat-messages');
+ if (!messagesContainer) return;
+
+ const messageDiv = document.createElement('div');
+ messageDiv.className = `wechat-message ${role === 'user' ? 'self' : ''}`;
+
+ const isDesignated = rpInfo.type === 'designated';
+ const isClaimed = rpInfo.status === 'claimed' || (rpInfo.claimedBy && rpInfo.claimedBy.length >= rpInfo.count);
+ const statusClass = isClaimed ? 'claimed' : '';
+
+ // 指定成员红包显示特殊样式
+ const designatedLabel = isDesignated ? `给${rpInfo.targetMemberNames?.join('、') || '指定成员'}的红包
` : '';
+
+ const bubbleHTML = `
+
+
+
+
+
+
${escapeHtml(rpInfo.message || '恭喜发财,大吉大利')}
+ ${designatedLabel}
+
${isClaimed ? '已领完' : ''}
+
+
+
+ `;
+
+ if (role === 'user') {
+ messageDiv.innerHTML = `
+ ${getUserAvatarHTML()}
+ ${bubbleHTML}
+ `;
+ } else {
+ const settings = getSettings();
+ const contact = settings.contacts?.find(c => c.name === rpInfo.senderName);
+ const firstChar = rpInfo.senderName?.charAt(0) || '?';
+ const avatarContent = contact?.avatar
+ ? `
`
+ : firstChar;
+
+ messageDiv.innerHTML = `
+ ${avatarContent}
+
+
${escapeHtml(rpInfo.senderName)}
+ ${bubbleHTML}
+
+ `;
+ }
+
+ messagesContainer.appendChild(messageDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+
+ // 绑定点击事件(查看详情)
+ const bubble = messageDiv.querySelector('.wechat-group-red-packet-bubble');
+ bubble?.addEventListener('click', () => {
+ showGroupRedPacketDetail(rpInfo);
+ });
+}
+
+/**
+ * 追加群转账消息到界面
+ */
+export function appendGroupTransferMessage(role, tfInfo) {
+ const messagesContainer = document.getElementById('wechat-chat-messages');
+ if (!messagesContainer) return;
+
+ const messageDiv = document.createElement('div');
+ messageDiv.className = `wechat-message ${role === 'user' ? 'self' : ''}`;
+
+ const statusText = tfInfo.status === 'received' ? '已收款' :
+ tfInfo.status === 'refunded' ? '已退还' : '待收款';
+ const statusClass = tfInfo.status || 'pending';
+
+ const bubbleHTML = `
+
+
+
+
+
+
¥${tfInfo.amount.toFixed(2)}
+
向${escapeHtml(tfInfo.targetMemberName)}转账
+
${escapeHtml(tfInfo.description) || '转账'}
+
+
${statusText}
+
+ `;
+
+ if (role === 'user') {
+ messageDiv.innerHTML = `
+ ${getUserAvatarHTML()}
+ ${bubbleHTML}
+ `;
+ } else {
+ const settings = getSettings();
+ const contact = settings.contacts?.find(c => c.name === tfInfo.senderName);
+ const firstChar = tfInfo.senderName?.charAt(0) || '?';
+ const avatarContent = contact?.avatar
+ ? `
`
+ : firstChar;
+
+ messageDiv.innerHTML = `
+ ${avatarContent}
+
+
${escapeHtml(tfInfo.senderName)}
+ ${bubbleHTML}
+
+ `;
+ }
+
+ messagesContainer.appendChild(messageDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+}
+
+/**
+ * 追加群红包领取提示
+ */
+function appendGroupRedPacketClaimNotice(claimerName, senderName) {
+ const messagesContainer = document.getElementById('wechat-chat-messages');
+ if (!messagesContainer) return;
+
+ const noticeDiv = document.createElement('div');
+ noticeDiv.className = 'wechat-msg-notice';
+ noticeDiv.innerHTML = `${escapeHtml(claimerName)}领取了${escapeHtml(senderName)}的红包`;
+
+ messagesContainer.appendChild(noticeDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+}
+
+/**
+ * 更新群红包气泡状态
+ */
+function updateGroupRedPacketBubbleStatus(rpId) {
+ const bubble = document.querySelector(`.wechat-group-red-packet-bubble[data-rp-id="${rpId}"]`);
+ if (!bubble) return;
+
+ // 从聊天记录中找到红包信息
+ const settings = getSettings();
+ const groupIndex = getCurrentGroupIndex();
+ const groupChat = settings.groupChats?.[groupIndex];
+ if (!groupChat) return;
+
+ const rpMsg = groupChat.chatHistory?.find(m => m.groupRedPacketInfo?.id === rpId);
+ const rpInfo = rpMsg?.groupRedPacketInfo;
+ if (!rpInfo) return;
+
+ const isClaimed = rpInfo.status === 'claimed' || (rpInfo.claimedBy && rpInfo.claimedBy.length >= rpInfo.count);
+
+ if (isClaimed) {
+ bubble.classList.add('claimed');
+ const statusEl = bubble.querySelector('.wechat-group-rp-status');
+ if (statusEl) {
+ statusEl.textContent = '已领完';
+ statusEl.classList.remove('hidden');
+ }
+ }
+}
+
+/**
+ * 更新群转账气泡状态
+ */
+function updateGroupTransferBubbleStatus(tfId, status) {
+ const bubble = document.querySelector(`.wechat-group-transfer-bubble[data-tf-id="${tfId}"]`);
+ if (!bubble) return;
+
+ bubble.classList.remove('pending', 'received', 'refunded');
+ bubble.classList.add(status);
+
+ const statusEl = bubble.querySelector('.wechat-group-tf-status');
+ if (statusEl) {
+ statusEl.textContent = status === 'received' ? '已收款' :
+ status === 'refunded' ? '已退还' : '待收款';
+ }
+}
+
+// ============ 群红包详情页面 ============
+
+/**
+ * 显示群红包详情
+ */
+export function showGroupRedPacketDetail(rpInfo) {
+ const page = document.getElementById('wechat-group-rp-detail-page');
+ if (!page) return;
+
+ const settings = getSettings();
+
+ // 更新详情页内容
+ const senderEl = document.getElementById('wechat-group-rp-detail-sender');
+ const messageEl = document.getElementById('wechat-group-rp-detail-message');
+ const totalEl = document.getElementById('wechat-group-rp-detail-total');
+ const countEl = document.getElementById('wechat-group-rp-detail-count');
+ const listEl = document.getElementById('wechat-group-rp-detail-list');
+
+ if (senderEl) {
+ senderEl.textContent = `${rpInfo.senderName}的红包`;
+ }
+ if (messageEl) {
+ messageEl.textContent = rpInfo.message || '恭喜发财,大吉大利';
+ }
+ if (totalEl) {
+ totalEl.textContent = '¥' + rpInfo.totalAmount.toFixed(2);
+ }
+ if (countEl) {
+ const claimed = rpInfo.claimedBy?.length || 0;
+ countEl.textContent = `${claimed}/${rpInfo.count}个红包`;
+ }
+
+ // 渲染领取列表
+ if (listEl) {
+ if (rpInfo.claimedBy && rpInfo.claimedBy.length > 0) {
+ // 找出最佳手气(金额最高的)
+ let maxAmount = 0;
+ let maxIndex = -1;
+ rpInfo.claimedBy.forEach((claim, idx) => {
+ if (claim.amount > maxAmount) {
+ maxAmount = claim.amount;
+ maxIndex = idx;
+ }
+ });
+
+ listEl.innerHTML = rpInfo.claimedBy.map((claim, idx) => {
+ const member = settings.contacts?.find(c => c.id === claim.memberId);
+ const firstChar = claim.memberName?.charAt(0) || '?';
+ const avatarHtml = member?.avatar
+ ? `
`
+ : firstChar;
+
+ const isBest = rpInfo.type === 'random' && idx === maxIndex && rpInfo.claimedBy.length > 1;
+ const bestLabel = isBest ? '手气最佳' : '';
+
+ const claimTime = new Date(claim.claimedAt);
+ const timeStr = `${claimTime.getHours().toString().padStart(2, '0')}:${claimTime.getMinutes().toString().padStart(2, '0')}`;
+
+ return `
+
+
${avatarHtml}
+
+
${escapeHtml(claim.memberName)} ${bestLabel}
+
${timeStr}
+
+
${claim.amount.toFixed(2)}元
+
+ `;
+ }).join('');
+ } else {
+ listEl.innerHTML = '暂无人领取
';
+ }
+ }
+
+ page.classList.remove('hidden');
+}
+
+/**
+ * 隐藏群红包详情
+ */
+export function hideGroupRedPacketDetail() {
+ document.getElementById('wechat-group-rp-detail-page')?.classList.add('hidden');
+}
+
+// ============ 创建页面HTML ============
+
+/**
+ * 创建群红包/转账相关页面(动态注入)
+ */
+export function createGroupRedPacketPages() {
+ const phone = document.getElementById('wechat-phone');
+ if (!phone) return;
+
+ // 检查是否已存在
+ if (document.getElementById('wechat-group-rp-type-page')) return;
+
+ const pagesHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
请输入支付密码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ phone.insertAdjacentHTML('beforeend', pagesHTML);
+
+ // 绑定事件
+ bindGroupRedPacketEvents();
+}
+
+/**
+ * 绑定群红包相关事件
+ */
+function bindGroupRedPacketEvents() {
+ // 类型选择页面
+ document.getElementById('wechat-group-rp-type-back')?.addEventListener('click', hideGroupRedPacketTypePage);
+ document.getElementById('wechat-group-rp-type-random')?.addEventListener('click', showGroupRandomRedPacketPage);
+ document.getElementById('wechat-group-rp-type-designated')?.addEventListener('click', showGroupDesignatedRedPacketPage);
+
+ // 拼手气红包页面
+ document.getElementById('wechat-group-random-rp-back')?.addEventListener('click', hideGroupRandomRedPacketPage);
+ document.getElementById('wechat-group-rp-amount-row')?.addEventListener('click', () => showGroupKeyboard('random-amount'));
+ document.getElementById('wechat-group-rp-count-row')?.addEventListener('click', () => showGroupKeyboard('random-count'));
+ document.getElementById('wechat-group-random-rp-submit')?.addEventListener('click', submitGroupRandomRedPacket);
+
+ // 拼手气红包键盘
+ document.querySelectorAll('#wechat-group-rp-keyboard .wechat-rp-keyboard-key').forEach(key => {
+ key.addEventListener('click', () => {
+ handleGroupKeyboardInput(key.dataset.key);
+ });
+ });
+
+ // 指定成员红包页面
+ document.getElementById('wechat-group-designated-rp-back')?.addEventListener('click', hideGroupDesignatedRedPacketPage);
+ document.getElementById('wechat-group-designated-amount-row')?.addEventListener('click', () => showGroupKeyboard('designated-amount'));
+ document.getElementById('wechat-group-designated-rp-submit')?.addEventListener('click', submitGroupDesignatedRedPacket);
+
+ // 指定成员红包键盘
+ document.querySelectorAll('#wechat-group-designated-keyboard .wechat-rp-keyboard-key').forEach(key => {
+ key.addEventListener('click', () => {
+ handleGroupKeyboardInput(key.dataset.key);
+ });
+ });
+
+ // 群转账成员选择页面
+ document.getElementById('wechat-group-transfer-select-back')?.addEventListener('click', hideGroupTransferSelectPage);
+
+ // 群转账金额输入页面
+ document.getElementById('wechat-group-transfer-amount-back')?.addEventListener('click', hideGroupTransferAmountPage);
+ document.getElementById('wechat-group-transfer-amount-row')?.addEventListener('click', () => showGroupKeyboard('transfer-amount'));
+ document.getElementById('wechat-group-transfer-submit')?.addEventListener('click', submitGroupTransfer);
+
+ // 群转账键盘
+ document.querySelectorAll('#wechat-group-transfer-keyboard .wechat-rp-keyboard-key').forEach(key => {
+ key.addEventListener('click', () => {
+ handleGroupKeyboardInput(key.dataset.key);
+ });
+ });
+
+ // 群红包详情页面
+ document.getElementById('wechat-group-rp-detail-back')?.addEventListener('click', hideGroupRedPacketDetail);
+
+ // 密码弹窗
+ document.getElementById('wechat-group-password-close')?.addEventListener('click', hideGroupPasswordModal);
+ document.querySelectorAll('#wechat-group-password-modal .wechat-password-key').forEach(key => {
+ key.addEventListener('click', () => {
+ const value = key.dataset.key;
+ if (value) {
+ handleGroupPasswordInput(value);
+ }
+ });
+ });
+}
+
+// ============ 初始化 ============
+
+/**
+ * 初始化群红包功能
+ */
+export function initGroupRedPacket() {
+ createGroupRedPacketPages();
+}
diff --git a/history-logs.js b/history-logs.js
index 831a66f..e9813ad 100644
--- a/history-logs.js
+++ b/history-logs.js
@@ -2,7 +2,7 @@
* 历史回顾和日志功能
*/
-import { saveSettingsDebounced } from '../../../../script.js';
+import { requestSave } from './save-manager.js';
import { getSettings, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js';
import { escapeHtml } from './utils.js';
import { showToast } from './toast.js';
@@ -48,7 +48,7 @@ export function addErrorLog(error, context = '') {
settings.errorLogs = settings.errorLogs.slice(0, MAX_LOGS);
}
- saveSettingsDebounced();
+ requestSave();
return logEntry;
}
@@ -56,7 +56,7 @@ export function addErrorLog(error, context = '') {
export function clearErrorLogs() {
const settings = getSettings();
settings.errorLogs = [];
- saveSettingsDebounced();
+ requestSave();
}
// 刷新日志列表显示
@@ -185,7 +185,7 @@ export function toggleHistoryItem(index, enabled) {
const settings = getSettings();
if (settings.selectedLorebooks?.[index]) {
settings.selectedLorebooks[index].enabled = enabled;
- saveSettingsDebounced();
+ requestSave();
showToast(enabled ? '已启用' : '已禁用');
}
}
@@ -257,7 +257,7 @@ export function showHistoryDetail(index) {
const entryIdx = parseInt(toggle.dataset.entryIndex);
if (settings.selectedLorebooks?.[index]?.entries?.[entryIdx]) {
settings.selectedLorebooks[index].entries[entryIdx].enabled = toggle.checked;
- saveSettingsDebounced();
+ requestSave();
}
});
});
diff --git a/icons.js b/icons.js
new file mode 100644
index 0000000..2ae655c
--- /dev/null
+++ b/icons.js
@@ -0,0 +1,49 @@
+/**
+ * SVG 图标定义
+ * 用于替换 emoji,保持视觉一致性
+ */
+
+// 红包图标 (替换 🧧)
+export const ICON_RED_PACKET = ``;
+
+// 成功/勾选图标 (替换 ✅)
+export const ICON_SUCCESS = ``;
+
+// 退还箭头图标 (替换 ↩️)
+export const ICON_REFUND = ``;
+
+// 提示/警告图标 (替换 🧊 - 改为感叹号)
+export const ICON_INFO = ``;
+
+// 用户头像图标 (替换 👤)
+export const ICON_USER = ``;
+
+// HTML 版本 (用于直接插入 HTML)
+export const ICON_RED_PACKET_HTML = `${ICON_RED_PACKET}`;
+export const ICON_SUCCESS_HTML = `${ICON_SUCCESS}`;
+export const ICON_REFUND_HTML = `${ICON_REFUND}`;
+export const ICON_INFO_HTML = `${ICON_INFO}`;
+export const ICON_USER_HTML = `${ICON_USER}`;
+
+// 大尺寸版本 (用于红包弹窗等需要大图标的地方)
+export const ICON_RED_PACKET_LARGE = ``;
+
+// 获取图标函数
+export function getIcon(type, size = 'normal') {
+ const icons = {
+ 'red-packet': size === 'large' ? ICON_RED_PACKET_LARGE : ICON_RED_PACKET,
+ 'success': ICON_SUCCESS,
+ 'refund': ICON_REFUND,
+ 'info': ICON_INFO,
+ 'user': ICON_USER
+ };
+ return icons[type] || '';
+}
+
+// 获取 HTML 包装的图标
+export function getIconHTML(type, size = 'normal') {
+ const icon = getIcon(type, size);
+ if (!icon) return '';
+ const sizeClass = size === 'large' ? 'wechat-icon-large' : '';
+ return `${icon}`;
+}
diff --git a/listen-together.js b/listen-together.js
new file mode 100644
index 0000000..2621d3c
--- /dev/null
+++ b/listen-together.js
@@ -0,0 +1,1282 @@
+/**
+ * 一起听功能模块
+ * 与AI角色一起听歌聊天
+ */
+
+import { getSettings, splitAIMessages } from './config.js';
+import { currentChatIndex } from './chat.js';
+import { requestSave } from './save-manager.js';
+import { refreshChatList } from './ui.js';
+import { searchMusic, playMusic, togglePlay, getCurrentSong, formatDuration } from './music.js';
+import { showToast } from './toast.js';
+import { escapeHtml, sleep } from './utils.js';
+
+// ========== SVG 图标 ==========
+const LISTEN_ICON = '';
+const BACK_ICON = '';
+const SEARCH_ICON = '';
+const PLAY_ICON = '';
+const PAUSE_ICON = '';
+const PREV_ICON = '';
+const NEXT_ICON = '';
+const CHAT_ICON = '';
+const PLAYLIST_ICON = '';
+const SEND_ICON = '';
+const CLOSE_ICON = '';
+const HEART_ICON = '';
+const LOOP_ICON = '';
+
+// ========== 状态管理 ==========
+let listenState = {
+ isActive: false,
+ isConnected: false,
+ currentSong: null,
+ messages: [],
+ contact: null,
+ contactIndex: -1,
+ startTime: null,
+ isPlaying: false,
+ connectTimeout: null,
+ dotsInterval: null,
+ chatVisible: false,
+ audioElement: null,
+ progressInterval: null,
+ pauseTimeout: null, // 暂停后自动播放下一首的计时器
+};
+
+// 导出图标供其他模块使用
+export { LISTEN_ICON };
+
+// ========== 页面显示/隐藏 ==========
+
+/**
+ * 显示一起听搜索页面
+ */
+export function showListenSearchPage() {
+ const page = document.getElementById('wechat-listen-search-page');
+ if (page) {
+ page.classList.remove('hidden');
+ // 聚焦输入框
+ setTimeout(() => {
+ const input = document.getElementById('wechat-listen-search-input');
+ if (input) input.focus();
+ }, 100);
+ }
+}
+
+/**
+ * 隐藏一起听搜索页面
+ */
+export function hideListenSearchPage() {
+ const page = document.getElementById('wechat-listen-search-page');
+ if (page) page.classList.add('hidden');
+}
+
+/**
+ * 显示等待页面
+ */
+function showWaitingPage(song, contact) {
+ const page = document.getElementById('wechat-listen-waiting-page');
+ if (!page) return;
+
+ const settings = getSettings();
+
+ // 调试日志
+ console.log('[一起听等待页面] 数据检查:', {
+ userAvatar: settings.userAvatar,
+ contactAvatar: contact.avatar,
+ songCover: song.cover,
+ contactName: contact.name
+ });
+
+ // 小图显示用户头像
+ const avatarEl = document.getElementById('wechat-listen-waiting-avatar');
+ if (avatarEl) {
+ // 先清除旧内容
+ avatarEl.innerHTML = '';
+ if (settings.userAvatar) {
+ avatarEl.innerHTML = `
`;
+ } else {
+ avatarEl.textContent = (settings.userName || 'User').charAt(0);
+ }
+ }
+
+ // 大图显示角色头像(带雷达动画的)
+ const coverEl = document.getElementById('wechat-listen-waiting-cover');
+ if (coverEl) {
+ // 先清除旧值
+ coverEl.src = '';
+ coverEl.style.background = '';
+
+ if (contact.avatar) {
+ coverEl.src = contact.avatar;
+ } else {
+ // 如果没有头像,用纯色背景
+ coverEl.style.background = '#333';
+ }
+ }
+
+ // 设置角色名
+ const nameEl = document.getElementById('wechat-listen-waiting-name');
+ if (nameEl) {
+ nameEl.textContent = contact.name || 'TA';
+ }
+
+ page.classList.remove('hidden');
+}
+
+/**
+ * 隐藏等待页面
+ */
+function hideWaitingPage() {
+ const page = document.getElementById('wechat-listen-waiting-page');
+ if (page) page.classList.add('hidden');
+ clearInterval(listenState.dotsInterval);
+}
+
+/**
+ * 显示一起听主页面
+ */
+function showListenTogetherPage() {
+ const page = document.getElementById('wechat-listen-together-page');
+ if (!page) return;
+
+ const settings = getSettings();
+ const contact = listenState.contact;
+ const song = listenState.currentSong;
+
+ // 设置用户头像
+ const userAvatarEl = document.getElementById('wechat-listen-user-avatar');
+ if (userAvatarEl) {
+ if (settings.userAvatar) {
+ userAvatarEl.innerHTML = `
`;
+ } else {
+ userAvatarEl.textContent = (settings.userName || 'User').charAt(0);
+ }
+ }
+
+ // 设置AI头像
+ const aiAvatarEl = document.getElementById('wechat-listen-ai-avatar');
+ if (aiAvatarEl) {
+ const firstChar = contact.name ? contact.name.charAt(0) : '?';
+ if (contact.avatar) {
+ aiAvatarEl.innerHTML = `
`;
+ } else {
+ aiAvatarEl.textContent = firstChar;
+ }
+ }
+
+ // 设置歌曲信息
+ const coverEl = document.getElementById('wechat-listen-cover');
+ const nameEl = document.getElementById('wechat-listen-song-name');
+ const artistEl = document.getElementById('wechat-listen-song-artist');
+
+ if (coverEl && song.cover) coverEl.src = song.cover;
+ if (nameEl) nameEl.textContent = song.name || '未知歌曲';
+ if (artistEl) artistEl.textContent = song.artist || '未知歌手';
+
+ // 初始化播放按钮状态
+ updatePlayButton();
+
+ page.classList.remove('hidden');
+ bindListenEvents();
+}
+
+/**
+ * 隐藏一起听主页面
+ */
+function hideListenTogetherPage() {
+ const page = document.getElementById('wechat-listen-together-page');
+ if (page) page.classList.add('hidden');
+}
+
+// ========== 核心逻辑 ==========
+
+/**
+ * 开始一起听
+ * @param {Object} song - 歌曲信息
+ * @param {number} contactIndex - 联系人索引
+ */
+export async function startListenTogether(song, contactIndex = currentChatIndex) {
+ if (listenState.isActive) return;
+ if (contactIndex < 0) {
+ showToast('请先选择聊天对象');
+ return;
+ }
+
+ const settings = getSettings();
+ const contact = settings.contacts[contactIndex];
+ if (!contact) {
+ showToast('联系人不存在');
+ return;
+ }
+
+ // 初始化状态
+ listenState = {
+ isActive: true,
+ isConnected: false,
+ currentSong: song,
+ messages: [],
+ contact: contact,
+ contactIndex: contactIndex,
+ startTime: null,
+ isPlaying: false,
+ connectTimeout: null,
+ dotsInterval: null,
+ chatVisible: false,
+ audioElement: null,
+ progressInterval: null,
+ pauseTimeout: null,
+ };
+
+ // 隐藏搜索页,显示等待页面
+ hideListenSearchPage();
+ showWaitingPage(song, contact);
+
+ // 开始等待动画
+ startWaitingAnimation();
+
+ // 2-4秒后AI"加入"
+ const joinDelay = 2000 + Math.random() * 2000;
+ listenState.connectTimeout = setTimeout(() => {
+ if (listenState.isActive && !listenState.isConnected) {
+ onAIJoined();
+ }
+ }, joinDelay);
+}
+
+/**
+ * 开始等待动画
+ */
+function startWaitingAnimation() {
+ const dotsEl = document.getElementById('wechat-listen-waiting-dots');
+ if (!dotsEl) return;
+
+ let dotCount = 0;
+ clearInterval(listenState.dotsInterval);
+
+ listenState.dotsInterval = setInterval(() => {
+ dotCount = (dotCount + 1) % 4;
+ dotsEl.textContent = '.'.repeat(dotCount || 1);
+ }, 500);
+}
+
+/**
+ * AI加入后
+ */
+async function onAIJoined() {
+ listenState.isConnected = true;
+ listenState.startTime = Date.now();
+
+ clearInterval(listenState.dotsInterval);
+ clearTimeout(listenState.connectTimeout);
+
+ // 隐藏等待页面,显示主页面
+ hideWaitingPage();
+ showListenTogetherPage();
+
+ // 开始播放音乐
+ await playListenSong();
+
+ // AI主动发送第一条消息
+ await triggerAIGreeting();
+}
+
+/**
+ * 播放当前歌曲
+ */
+async function playListenSong() {
+ const song = listenState.currentSong;
+ if (!song) return;
+
+ try {
+ // 使用music.js的playMusic函数
+ await playMusic(song.id, song.platform, song.name, song.artist);
+ listenState.isPlaying = true;
+ updatePlayButton();
+ startProgressUpdate();
+
+ // 监听歌曲结束事件
+ const audio = document.getElementById('wechat-music-audio');
+ if (audio) {
+ listenState.audioElement = audio;
+ audio.addEventListener('ended', onSongEnded);
+ }
+ } catch (e) {
+ console.error('[可乐] 一起听播放失败:', e);
+ showToast('播放失败');
+ }
+}
+
+/**
+ * 歌曲结束时的处理
+ */
+async function onSongEnded() {
+ if (!listenState.isActive) return;
+
+ listenState.isPlaying = false;
+ updatePlayButton();
+
+ // 20%几率AI换歌
+ if (Math.random() < 0.2) {
+ await aiSelectSong();
+ }
+}
+
+/**
+ * AI选择歌曲(20%几率触发)
+ */
+async function aiSelectSong() {
+ if (!listenState.isConnected || !listenState.contact) return;
+
+ try {
+ const { callListenTogetherAI } = await import('./ai.js');
+
+ // 获取最近5条消息
+ const recentMessages = listenState.messages.slice(-5);
+ const messagesContext = recentMessages.map(m =>
+ `${m.role === 'user' ? '用户' : '你'}: ${m.content}`
+ ).join('\n');
+
+ // 构建AI选歌的prompt
+ const prompt = `[这首歌播放完了,请你选择下一首想听的歌,根据你们刚才的聊天氛围和你的喜好来选。
+
+最近的对话:
+${messagesContext || '(刚开始听歌)'}
+
+请回复格式:
+1. 先说一句为什么想听这首歌(简短自然,1-2句话)
+2. 然后用 [换歌:歌名] 格式选择歌曲
+
+示例:突然想听点轻快的|||[换歌:晴天]]`;
+
+ showListenTypingIndicator();
+ const aiResponse = await callListenTogetherAI(listenState.contact, prompt, recentMessages, listenState.currentSong);
+ hideListenTypingIndicator();
+
+ if (aiResponse) {
+ // 处理回复
+ const parts = splitAIMessages(aiResponse);
+ for (const part of parts) {
+ const text = filterListenMessage(part);
+
+ // 检查是否包含换歌标签
+ const changeSongMatch = text.match(/\[换歌[::]\s*(.+?)\]/);
+ if (changeSongMatch) {
+ const songKeyword = changeSongMatch[1].trim();
+ // 显示AI的说明文字(去掉换歌标签)
+ const displayText = text.replace(/\[换歌[::][^\]]*\]/g, '').trim();
+ if (displayText) {
+ addListenMessage('ai', displayText);
+ }
+ // 搜索并播放新歌
+ await changeSongByKeyword(songKeyword, true);
+ break;
+ } else if (text) {
+ addListenMessage('ai', text);
+ }
+ }
+ }
+ } catch (err) {
+ console.error('[可乐] AI选歌失败:', err);
+ hideListenTypingIndicator();
+ }
+}
+
+/**
+ * 根据关键词换歌
+ */
+async function changeSongByKeyword(keyword, isAIChange = false) {
+ try {
+ const results = await searchMusic(keyword);
+ if (results && results.length > 0) {
+ const newSong = results[0];
+ listenState.currentSong = newSong;
+
+ // 更新界面
+ const coverEl = document.getElementById('wechat-listen-cover');
+ const nameEl = document.getElementById('wechat-listen-song-name');
+ const artistEl = document.getElementById('wechat-listen-song-artist');
+
+ if (coverEl) coverEl.src = newSong.cover || '';
+ if (nameEl) nameEl.textContent = newSong.name || '未知歌曲';
+ if (artistEl) artistEl.textContent = newSong.artist || '未知歌手';
+
+ // 播放新歌
+ await playListenSong();
+
+ // 如果不是AI换的歌,通知AI对换歌做出反应
+ if (!isAIChange) {
+ await triggerAISongChangeReaction(newSong);
+ }
+ } else {
+ showToast('未找到歌曲');
+ }
+ } catch (e) {
+ console.error('[可乐] 换歌失败:', e);
+ showToast('换歌失败');
+ }
+}
+
+/**
+ * AI对用户换歌的反应
+ */
+async function triggerAISongChangeReaction(newSong) {
+ if (!listenState.isConnected || !listenState.contact) return;
+
+ try {
+ const { callListenTogetherAI } = await import('./ai.js');
+
+ const prompt = `[用户换了一首歌,新歌是《${newSong.name}》- ${newSong.artist}。请对换歌做出反应,表达你对这首歌的看法或感受。记得发送2-4条消息,每条换行分隔]`;
+
+ showListenTypingIndicator();
+ const aiResponse = await callListenTogetherAI(
+ listenState.contact,
+ prompt,
+ listenState.messages.slice(-5),
+ newSong
+ );
+ hideListenTypingIndicator();
+
+ await processAIResponse(aiResponse);
+ } catch (err) {
+ hideListenTypingIndicator();
+ console.error('[可乐] AI换歌反应失败:', err);
+ }
+}
+
+/**
+ * AI主动发送开场消息
+ */
+async function triggerAIGreeting() {
+ if (!listenState.isConnected || !listenState.contact) return;
+
+ showListenTypingIndicator();
+
+ try {
+ const { callListenTogetherAI } = await import('./ai.js');
+ const song = listenState.currentSong;
+
+ const prompt = `[用户邀请你一起听歌,歌曲是《${song.name}》- ${song.artist},你刚刚加入了一起听。请用你的方式自然地打个招呼,并对这首歌发表一些看法。记得发送2-4条消息,每条换行分隔,像真实聊天一样有层次感]`;
+
+ const aiResponse = await callListenTogetherAI(
+ listenState.contact,
+ prompt,
+ [],
+ song
+ );
+
+ hideListenTypingIndicator();
+ await processAIResponse(aiResponse);
+
+ } catch (err) {
+ hideListenTypingIndicator();
+ console.error('[可乐] 一起听AI开场白失败:', err);
+ }
+}
+
+/**
+ * 过滤消息 - 只允许纯文字,过滤所有特殊格式
+ */
+function filterListenMessage(text) {
+ if (!text) return '';
+
+ let reply = text.trim();
+
+ // 过滤 meme 表情包
+ reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
+ // 过滤 [表情:xxx]
+ reply = reply.replace(/\[表情[::][^\]]*\]/g, '').trim();
+ // 过滤 [照片:xxx]
+ reply = reply.replace(/\[照片[::][^\]]*\]/g, '').trim();
+ // 过滤 [语音:xxx]
+ reply = reply.replace(/\[语音[::][^\]]*\]/g, '').trim();
+ // 过滤 [音乐:xxx](但保留[换歌:xxx])
+ reply = reply.replace(/\[(?:分享)?音乐[::][^\]]*\]/g, '').trim();
+ // 过滤 [回复:xxx] 引用格式
+ reply = reply.replace(/\[回复[::][^\]]*\]/g, '').trim();
+ // 过滤中文小括号内容(动作/语气描述)
+ reply = reply.replace(/([^)]*)/g, '').trim();
+ // 过滤英文小括号内容
+ reply = reply.replace(/\([^)]*\)/g, '').trim();
+
+ return reply;
+}
+
+/**
+ * 处理AI回复 - 纯文字消息
+ */
+async function processAIResponse(aiResponse) {
+ if (!aiResponse) return;
+
+ const parts = splitAIMessages(aiResponse);
+
+ for (const part of parts) {
+ if (!listenState.isConnected) break;
+
+ let reply = filterListenMessage(part);
+ if (!reply) continue;
+
+ // 直接发送纯文字消息
+ showListenTypingIndicator();
+ await sleep(400 + Math.random() * 600);
+ hideListenTypingIndicator();
+ if (listenState.isConnected) {
+ addListenMessage('ai', reply);
+ }
+ }
+}
+
+/**
+ * 用户发送消息
+ */
+async function sendListenMessage() {
+ const input = document.getElementById('wechat-listen-input-text');
+ if (!input) return;
+
+ const message = input.value.trim();
+ if (!message || !listenState.isConnected) return;
+
+ input.value = '';
+
+ // 显示用户消息
+ addListenMessage('user', message);
+
+ // 显示typing
+ showListenTypingIndicator();
+
+ try {
+ const { callListenTogetherAI } = await import('./ai.js');
+ const song = listenState.currentSong;
+
+ const aiResponse = await callListenTogetherAI(
+ listenState.contact,
+ message,
+ listenState.messages.slice(0, -1),
+ song
+ );
+
+ hideListenTypingIndicator();
+ await processAIResponse(aiResponse);
+
+ } catch (err) {
+ hideListenTypingIndicator();
+ console.error('[可乐] 一起听消息回复失败:', err);
+ }
+}
+
+// ========== UI 更新 ==========
+
+/**
+ * 显示typing指示器
+ */
+function showListenTypingIndicator() {
+ const messagesEl = document.getElementById('wechat-listen-messages');
+ if (!messagesEl) return;
+
+ messagesEl.classList.remove('hidden');
+ hideListenTypingIndicator();
+
+ const typingDiv = document.createElement('div');
+ typingDiv.className = 'wechat-listen-msg ai';
+ typingDiv.id = 'wechat-listen-typing';
+ typingDiv.innerHTML = `
+
+
+
+
+
+ `;
+
+ messagesEl.appendChild(typingDiv);
+ messagesEl.scrollTop = messagesEl.scrollHeight;
+}
+
+/**
+ * 隐藏typing指示器
+ */
+function hideListenTypingIndicator() {
+ const typingEl = document.getElementById('wechat-listen-typing');
+ if (typingEl) typingEl.remove();
+}
+
+/**
+ * 添加聊天消息
+ */
+function addListenMessage(role, content) {
+ const messagesEl = document.getElementById('wechat-listen-messages');
+ if (!messagesEl) return;
+
+ messagesEl.classList.remove('hidden');
+
+ // 添加到状态
+ listenState.messages.push({ role, content, timestamp: Date.now() });
+
+ // 创建消息元素
+ const msgDiv = document.createElement('div');
+ msgDiv.className = `wechat-listen-msg ${role} fade-in`;
+ msgDiv.textContent = content;
+
+ messagesEl.appendChild(msgDiv);
+ messagesEl.scrollTop = messagesEl.scrollHeight;
+
+ // 限制显示的消息数量
+ const msgs = messagesEl.querySelectorAll('.wechat-listen-msg:not(#wechat-listen-typing)');
+ if (msgs.length > 15) {
+ msgs[0].remove();
+ }
+}
+
+/**
+ * 更新播放按钮状态
+ */
+function updatePlayButton() {
+ const playBtn = document.getElementById('wechat-listen-play-btn');
+ if (playBtn) {
+ playBtn.innerHTML = listenState.isPlaying ? PAUSE_ICON : PLAY_ICON;
+ }
+
+ // 更新唱片旋转
+ const disc = document.getElementById('wechat-listen-disc');
+ if (disc) {
+ if (listenState.isPlaying) {
+ disc.classList.add('rotating');
+ disc.classList.remove('paused');
+ } else {
+ disc.classList.add('paused');
+ }
+ }
+}
+
+/**
+ * 处理播放/暂停点击
+ * 暂停3秒后自动播放下一首
+ */
+function handlePlayPauseClick() {
+ togglePlay();
+ listenState.isPlaying = !listenState.isPlaying;
+ updatePlayButton();
+
+ // 清除之前的暂停计时器
+ if (listenState.pauseTimeout) {
+ clearTimeout(listenState.pauseTimeout);
+ listenState.pauseTimeout = null;
+ }
+
+ // 如果暂停了,启动3秒后自动播放下一首的计时器
+ if (!listenState.isPlaying && listenState.isActive) {
+ listenState.pauseTimeout = setTimeout(async () => {
+ if (!listenState.isPlaying && listenState.isActive) {
+ await autoPlayNextSong();
+ }
+ }, 3000);
+ }
+}
+
+/**
+ * 自动播放下一首歌(暂停3秒后触发)
+ */
+async function autoPlayNextSong() {
+ if (!listenState.isActive || !listenState.currentSong) return;
+
+ try {
+ // 搜索相似歌曲或随机歌曲
+ const currentSong = listenState.currentSong;
+ const keyword = currentSong.artist || currentSong.name;
+
+ const results = await searchMusic(keyword);
+ if (results && results.length > 1) {
+ // 找一首不同的歌
+ const newSong = results.find(s => s.id !== currentSong.id) || results[1];
+
+ listenState.currentSong = newSong;
+
+ // 更新界面
+ const coverEl = document.getElementById('wechat-listen-cover');
+ const nameEl = document.getElementById('wechat-listen-song-name');
+ const artistEl = document.getElementById('wechat-listen-song-artist');
+
+ if (coverEl) coverEl.src = newSong.cover || '';
+ if (nameEl) nameEl.textContent = newSong.name || '未知歌曲';
+ if (artistEl) artistEl.textContent = newSong.artist || '未知歌手';
+
+ // 播放新歌
+ await playListenSong();
+
+ // AI 对自动换歌做出反应
+ await triggerAIAutoNextReaction(newSong);
+ }
+ } catch (e) {
+ console.error('[可乐] 自动播放下一首失败:', e);
+ }
+}
+
+/**
+ * AI 对自动换歌的反应
+ */
+async function triggerAIAutoNextReaction(newSong) {
+ if (!listenState.isConnected || !listenState.contact) return;
+
+ try {
+ const { callListenTogetherAI } = await import('./ai.js');
+
+ const prompt = `[歌曲自动切换到了《${newSong.name}》- ${newSong.artist},请对新歌做出反应,发送2-3条消息,每条换行分隔]`;
+
+ showListenTypingIndicator();
+ const aiResponse = await callListenTogetherAI(
+ listenState.contact,
+ prompt,
+ listenState.messages.slice(-3),
+ newSong
+ );
+ hideListenTypingIndicator();
+
+ await processAIResponse(aiResponse);
+ } catch (err) {
+ hideListenTypingIndicator();
+ console.error('[可乐] AI自动换歌反应失败:', err);
+ }
+}
+
+/**
+ * 开始进度条更新
+ */
+function startProgressUpdate() {
+ clearInterval(listenState.progressInterval);
+
+ listenState.progressInterval = setInterval(() => {
+ const audio = listenState.audioElement || document.getElementById('wechat-music-audio');
+ if (!audio) return;
+
+ const currentTime = audio.currentTime || 0;
+ const duration = audio.duration || 0;
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
+
+ const currentEl = document.getElementById('wechat-listen-current-time');
+ const durationEl = document.getElementById('wechat-listen-duration');
+ const fillEl = document.getElementById('wechat-listen-progress-fill');
+ const sliderEl = document.getElementById('wechat-listen-slider');
+
+ if (currentEl) currentEl.textContent = formatDuration(currentTime);
+ if (durationEl) durationEl.textContent = formatDuration(duration);
+ if (fillEl) fillEl.style.width = progress + '%';
+ if (sliderEl) sliderEl.value = progress;
+ }, 500);
+}
+
+// ========== 事件绑定 ==========
+
+let listenEventsBound = false;
+let searchEventsBound = false;
+
+/**
+ * 绑定搜索页面事件
+ */
+export function bindListenSearchEvents() {
+ if (searchEventsBound) return;
+ searchEventsBound = true;
+
+ // 返回按钮
+ document.getElementById('wechat-listen-search-back')?.addEventListener('click', () => {
+ hideListenSearchPage();
+ });
+
+ // 搜索输入
+ const searchInput = document.getElementById('wechat-listen-search-input');
+ let searchTimeout = null;
+
+ searchInput?.addEventListener('input', (e) => {
+ clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(() => {
+ doListenSearch(e.target.value.trim());
+ }, 500);
+ });
+
+ searchInput?.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ clearTimeout(searchTimeout);
+ doListenSearch(e.target.value.trim());
+ }
+ });
+
+ // 搜索结果点击
+ document.getElementById('wechat-listen-search-results')?.addEventListener('click', (e) => {
+ const item = e.target.closest('.wechat-listen-search-item');
+ if (!item) return;
+
+ const song = {
+ id: item.dataset.id,
+ platform: item.dataset.platform,
+ name: item.dataset.name,
+ artist: item.dataset.artist,
+ cover: item.dataset.cover,
+ };
+
+ startListenTogether(song);
+ });
+}
+
+/**
+ * 执行搜索
+ */
+async function doListenSearch(keyword) {
+ const resultsEl = document.getElementById('wechat-listen-search-results');
+ if (!resultsEl) return;
+
+ if (!keyword) {
+ resultsEl.innerHTML = '输入关键词搜索歌曲
';
+ return;
+ }
+
+ resultsEl.innerHTML = '搜索中...
';
+
+ try {
+ const results = await searchMusic(keyword);
+
+ if (!results || results.length === 0) {
+ resultsEl.innerHTML = '未找到结果
';
+ return;
+ }
+
+ let html = '';
+ for (const song of results) {
+ html += `
+
+
+
})
+
+
+
${escapeHtml(song.name)}
+
${escapeHtml(song.artist)} - ${escapeHtml(song.platform)}
+
+
+ `;
+ }
+ resultsEl.innerHTML = html;
+
+ } catch (err) {
+ console.error('[可乐] 一起听搜索失败:', err);
+ resultsEl.innerHTML = '搜索失败
';
+ }
+}
+
+/**
+ * 绑定一起听主页面事件
+ */
+function bindListenEvents() {
+ if (listenEventsBound) return;
+ listenEventsBound = true;
+
+ // 发送消息
+ document.getElementById('wechat-listen-send-btn')?.addEventListener('click', sendListenMessage);
+
+ // 输入框回车发送
+ document.getElementById('wechat-listen-input-text')?.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ sendListenMessage();
+ }
+ });
+
+ // 播放/暂停
+ document.getElementById('wechat-listen-play-btn')?.addEventListener('click', handlePlayPauseClick);
+
+ // 星星按钮 - 打开颜色选择器
+ document.getElementById('wechat-listen-color-btn')?.addEventListener('click', toggleColorPicker);
+
+ // 颜色选择器选项点击
+ document.getElementById('wechat-listen-color-picker')?.addEventListener('click', handleColorOptionClick);
+
+ // 结束按钮
+ document.getElementById('wechat-listen-end-btn')?.addEventListener('click', exitListenTogether);
+
+ // 换歌面板关闭
+ document.getElementById('wechat-listen-change-close')?.addEventListener('click', hideChangeSongPanel);
+
+ // 换歌搜索
+ const changeInput = document.getElementById('wechat-listen-change-input');
+ let changeSearchTimeout = null;
+
+ changeInput?.addEventListener('input', (e) => {
+ clearTimeout(changeSearchTimeout);
+ changeSearchTimeout = setTimeout(() => {
+ doChangeSongSearch(e.target.value.trim());
+ }, 500);
+ });
+
+ // 换歌搜索结果点击
+ document.getElementById('wechat-listen-change-results')?.addEventListener('click', (e) => {
+ const item = e.target.closest('.wechat-listen-change-item');
+ if (!item) return;
+
+ const song = {
+ id: item.dataset.id,
+ platform: item.dataset.platform,
+ name: item.dataset.name,
+ artist: item.dataset.artist,
+ cover: item.dataset.cover,
+ };
+
+ changeSong(song);
+ hideChangeSongPanel();
+ });
+
+ // 取消一起听
+ document.getElementById('wechat-listen-cancel')?.addEventListener('click', cancelListenTogether);
+
+ // 返回按钮(主页面的返回)
+ document.getElementById('wechat-listen-back-btn')?.addEventListener('click', exitListenTogether);
+
+ // 进度条拖动
+ const slider = document.getElementById('wechat-listen-slider');
+ slider?.addEventListener('change', (e) => {
+ const audio = listenState.audioElement || document.getElementById('wechat-music-audio');
+ if (audio && audio.duration) {
+ audio.currentTime = (e.target.value / 100) * audio.duration;
+ }
+ });
+}
+
+/**
+ * 背景颜色映射
+ */
+const LISTEN_BACKGROUNDS = {
+ 'starry': 'linear-gradient(135deg, #1a1a2e 0%, #16213e 30%, #0f3460 60%, #533483 100%)',
+ 'orange': 'linear-gradient(135deg, #f97316 0%, #ea580c 50%, #c2410c 100%)',
+ 'pink': 'linear-gradient(135deg, #ec4899 0%, #f472b6 50%, #f9a8d4 100%)',
+ 'white': '#fff'
+};
+let currentBg = 'starry';
+
+/**
+ * 切换颜色选择器显示
+ */
+function toggleColorPicker() {
+ const picker = document.getElementById('wechat-listen-color-picker');
+ if (picker) {
+ picker.classList.toggle('hidden');
+ }
+}
+
+/**
+ * 隐藏颜色选择器
+ */
+function hideColorPicker() {
+ const picker = document.getElementById('wechat-listen-color-picker');
+ if (picker) {
+ picker.classList.add('hidden');
+ }
+}
+
+/**
+ * 处理颜色选项点击
+ */
+function handleColorOptionClick(e) {
+ const option = e.target.closest('.wechat-listen-color-option');
+ if (!option) return;
+
+ const bgType = option.dataset.bg;
+ if (!bgType || !LISTEN_BACKGROUNDS[bgType]) return;
+
+ // 更新页面背景
+ const page = document.getElementById('wechat-listen-together-page');
+ if (page) {
+ page.style.background = LISTEN_BACKGROUNDS[bgType];
+
+ // 如果是白色背景,需要调整文字颜色
+ if (bgType === 'white') {
+ page.classList.add('light-bg');
+ } else {
+ page.classList.remove('light-bg');
+ }
+ }
+
+ // 更新选中状态
+ document.querySelectorAll('.wechat-listen-color-option').forEach(opt => {
+ opt.classList.remove('active');
+ });
+ option.classList.add('active');
+
+ currentBg = bgType;
+ hideColorPicker();
+}
+
+/**
+ * 显示换歌面板
+ */
+function showChangeSongPanel() {
+ const panel = document.getElementById('wechat-listen-change-panel');
+ if (panel) {
+ panel.classList.remove('hidden');
+ document.getElementById('wechat-listen-change-input')?.focus();
+ }
+}
+
+/**
+ * 隐藏换歌面板
+ */
+function hideChangeSongPanel() {
+ const panel = document.getElementById('wechat-listen-change-panel');
+ if (panel) panel.classList.add('hidden');
+}
+
+/**
+ * 换歌搜索
+ */
+async function doChangeSongSearch(keyword) {
+ const resultsEl = document.getElementById('wechat-listen-change-results');
+ if (!resultsEl) return;
+
+ if (!keyword) {
+ resultsEl.innerHTML = '';
+ return;
+ }
+
+ resultsEl.innerHTML = '搜索中...
';
+
+ try {
+ const results = await searchMusic(keyword);
+
+ if (!results || results.length === 0) {
+ resultsEl.innerHTML = '未找到结果
';
+ return;
+ }
+
+ let html = '';
+ for (const song of results.slice(0, 10)) {
+ html += `
+
+
${escapeHtml(song.name)}
+
${escapeHtml(song.artist)}
+
+ `;
+ }
+ resultsEl.innerHTML = html;
+
+ } catch (err) {
+ resultsEl.innerHTML = '搜索失败
';
+ }
+}
+
+/**
+ * 换歌
+ */
+async function changeSong(song) {
+ listenState.currentSong = song;
+
+ // 更新界面
+ const coverEl = document.getElementById('wechat-listen-cover');
+ const nameEl = document.getElementById('wechat-listen-song-name');
+ const artistEl = document.getElementById('wechat-listen-song-artist');
+
+ if (coverEl) coverEl.src = song.cover || '';
+ if (nameEl) nameEl.textContent = song.name || '未知歌曲';
+ if (artistEl) artistEl.textContent = song.artist || '未知歌手';
+
+ // 播放新歌
+ await playListenSong();
+
+ // 通知AI对换歌做出反应
+ await triggerAISongChangeReaction(song);
+}
+
+/**
+ * 取消一起听(等待页面)
+ */
+function cancelListenTogether() {
+ clearInterval(listenState.dotsInterval);
+ clearTimeout(listenState.connectTimeout);
+ clearInterval(listenState.progressInterval);
+ clearTimeout(listenState.pauseTimeout);
+
+ hideWaitingPage();
+
+ listenState = {
+ isActive: false,
+ isConnected: false,
+ currentSong: null,
+ messages: [],
+ contact: null,
+ contactIndex: -1,
+ startTime: null,
+ isPlaying: false,
+ connectTimeout: null,
+ dotsInterval: null,
+ chatVisible: false,
+ audioElement: null,
+ progressInterval: null,
+ pauseTimeout: null,
+ };
+}
+
+/**
+ * 退出一起听
+ */
+export async function exitListenTogether() {
+ if (!listenState.isActive) return;
+
+ clearInterval(listenState.dotsInterval);
+ clearTimeout(listenState.connectTimeout);
+ clearInterval(listenState.progressInterval);
+ clearTimeout(listenState.pauseTimeout);
+
+ // 移除音频结束监听
+ if (listenState.audioElement) {
+ listenState.audioElement.removeEventListener('ended', onSongEnded);
+ }
+
+ // 保存一起听记录(不显示在聊天里)
+ const contact = listenState.contact;
+ const song = listenState.currentSong;
+ const messages = [...listenState.messages];
+
+ if (contact && messages.length > 0) {
+ saveListenHistory();
+ }
+
+ // 隐藏所有页面
+ hideWaitingPage();
+ hideListenTogetherPage();
+ hideListenSearchPage();
+ hideChangeSongPanel();
+
+ // 重置状态
+ listenState = {
+ isActive: false,
+ isConnected: false,
+ currentSong: null,
+ messages: [],
+ contact: null,
+ contactIndex: -1,
+ startTime: null,
+ isPlaying: false,
+ connectTimeout: null,
+ dotsInterval: null,
+ chatVisible: false,
+ audioElement: null,
+ progressInterval: null,
+ pauseTimeout: null,
+ };
+
+ // AI 结束一起听后的回复
+ if (contact && song) {
+ await triggerAIListenEndReply(contact, song, messages);
+ }
+}
+
+/**
+ * AI 结束一起听后的回复
+ */
+async function triggerAIListenEndReply(contact, song, messages) {
+ try {
+ const { callAI } = await import('./ai.js');
+ const { appendMessage, showTypingIndicator, hideTypingIndicator } = await import('./chat.js');
+
+ // 显示打字指示器
+ showTypingIndicator(contact);
+
+ // 构建提示
+ const recentMsgs = messages.slice(-5).map(m =>
+ `${m.role === 'user' ? '用户' : '你'}: ${m.content}`
+ ).join('\n');
+
+ const prompt = `[刚才和用户一起听了《${song.name}》- ${song.artist},一起听已经结束了。请根据刚才的聊天氛围,说一句告别或感想,简短自然,不要使用任何特殊格式标签。
+
+刚才的聊天:
+${recentMsgs || '(没有聊天)'}]`;
+
+ const aiResponse = await callAI(contact, prompt);
+ hideTypingIndicator();
+
+ if (aiResponse && aiResponse.trim()) {
+ let reply = aiResponse.split('|||')[0].trim();
+ reply = reply.replace(/^\[.*?\]\s*/, '');
+ reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
+
+ if (reply) {
+ const settings = getSettings();
+ const now = new Date();
+ const timeStr = now.toLocaleString('zh-CN', {
+ year: 'numeric', month: '2-digit', day: '2-digit',
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
+ }).replace(/\//g, '-');
+
+ // 添加到聊天历史
+ if (!contact.chatHistory) contact.chatHistory = [];
+ contact.chatHistory.push({
+ role: 'assistant',
+ content: reply,
+ time: timeStr,
+ timestamp: Date.now()
+ });
+
+ appendMessage('assistant', reply, contact);
+ contact.lastMessage = reply;
+ requestSave();
+ refreshChatList();
+ }
+ }
+ } catch (err) {
+ console.error('[可乐] AI一起听结束回复失败:', err);
+ // 隐藏typing
+ import('./chat.js').then(m => m.hideTypingIndicator());
+ }
+}
+
+/**
+ * 保存一起听历史记录(不显示在聊天中)
+ */
+function saveListenHistory() {
+ const settings = getSettings();
+ const contact = listenState.contact;
+
+ if (!contact) return;
+
+ 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')}`;
+
+ // 计算时长
+ let durationStr = '00:00';
+ if (listenState.startTime) {
+ const elapsed = Math.floor((Date.now() - listenState.startTime) / 1000);
+ const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0');
+ const seconds = (elapsed % 60).toString().padStart(2, '0');
+ durationStr = `${minutes}:${seconds}`;
+ }
+
+ // 保存到联系人的一起听历史(仅保存记录,不显示在聊天中)
+ if (!Array.isArray(contact.listenHistory)) {
+ contact.listenHistory = [];
+ }
+
+ contact.listenHistory.push({
+ song: listenState.currentSong,
+ duration: durationStr,
+ time: timeStr,
+ timestamp: Date.now(),
+ messages: listenState.messages.map(m => ({ role: m.role, content: m.content }))
+ });
+
+ // 限制历史记录数量
+ if (contact.listenHistory.length > 50) {
+ contact.listenHistory = contact.listenHistory.slice(-50);
+ }
+
+ requestSave();
+}
+
+/**
+ * 初始化一起听功能
+ */
+export function initListenTogether() {
+ bindListenSearchEvents();
+}
\ No newline at end of file
diff --git a/main.js b/main.js
index 688c0f3..9c205a2 100644
--- a/main.js
+++ b/main.js
@@ -4,12 +4,13 @@
console.log('[可乐] main.js 开始加载...');
-import { saveSettingsDebounced } from '../../../../script.js';
+import { requestSave, setupUnloadSave } from './save-manager.js';
import { loadSettings, getSettings, MEME_PROMPT_TEMPLATE } from './config.js';
import { generatePhoneHTML } from './phone-html.js';
import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js';
import { showToast } from './toast.js';
+import { ICON_SUCCESS, ICON_INFO } from './icons.js';
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat } from './chat.js';
@@ -30,6 +31,10 @@ import { getCurrentTime } from './utils.js';
import { refreshHistoryList, refreshLogsList, clearErrorLogs, initErrorCapture, addErrorLog } from './history-logs.js';
import { initChatBackground } from './chat-background.js';
import { initMoments, openMomentsPage, clearContactMoments } from './moments.js';
+import { initRedPacketEvents } from './red-packet.js';
+import { initTransferEvents } from './transfer.js';
+import { initGroupRedPacket } from './group-red-packet.js';
+import { initCropper } from './cropper.js';
function normalizeModelListForSelect(models) {
return (models || []).map(m => {
@@ -76,7 +81,7 @@ async function refreshModelSelect() {
const apiKey = document.getElementById('wechat-api-key')?.value?.trim() || settings.apiKey || '';
if (!apiUrl) {
- showToast('请先填写 API 地址', '🧊');
+ showToast('请先填写 API 地址', 'info');
return;
}
@@ -94,7 +99,7 @@ async function refreshModelSelect() {
modelIds.map(id => ``).join('');
settings.modelList = modelIds;
- saveSettingsDebounced();
+ requestSave();
showToast(`获取到 ${modelIds.length} 个模型`);
} catch (err) {
console.error('[可乐] 获取模型列表失败:', err);
@@ -248,7 +253,7 @@ function bindEvents() {
if (groupChat) {
groupChat.chatHistory = [];
groupChat.lastMessage = '';
- saveSettingsDebounced();
+ requestSave();
openGroupChat(groupIndex); // 刷新群聊界面
showToast('群聊记录已清空');
}
@@ -264,7 +269,7 @@ function bindEvents() {
if (contact) {
contact.chatHistory = [];
contact.lastMessage = '';
- saveSettingsDebounced();
+ requestSave();
openChat(currentChatIndex); // 刷新聊天界面
showToast('聊天记录已清空');
}
@@ -369,7 +374,7 @@ function bindEvents() {
if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.autoInjectPrompt);
}
- saveSettingsDebounced();
+ requestSave();
if (settings.autoInjectPrompt) injectAuthorNote();
});
@@ -377,7 +382,7 @@ function bindEvents() {
document.getElementById('wechat-save-author-note')?.addEventListener('click', () => {
const settings = getSettings();
settings.authorNoteCustom = document.getElementById('wechat-author-note-content')?.value || '';
- saveSettingsDebounced();
+ requestSave();
showToast('作者注释模板已保存');
});
@@ -391,7 +396,7 @@ function bindEvents() {
if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.hakimiBreakLimit);
}
- saveSettingsDebounced();
+ requestSave();
showToast(settings.hakimiBreakLimit ? '哈基米破限已开启' : '哈基米破限已关闭');
});
@@ -399,7 +404,7 @@ function bindEvents() {
document.getElementById('wechat-save-hakimi')?.addEventListener('click', () => {
const settings = getSettings();
settings.hakimiCustomPrompt = document.getElementById('wechat-hakimi-prompt')?.value || '';
- saveSettingsDebounced();
+ requestSave();
showToast('破限提示词已保存');
});
@@ -415,7 +420,7 @@ function bindEvents() {
settings.memeStickersEnabled = !settings.memeStickersEnabled;
const toggle = document.getElementById('wechat-meme-stickers-toggle');
toggle?.classList.toggle('on', settings.memeStickersEnabled);
- saveSettingsDebounced();
+ requestSave();
showToast(settings.memeStickersEnabled ? 'Meme表情包已启用' : 'Meme表情包已禁用');
});
@@ -532,7 +537,7 @@ function bindEvents() {
const fetchBtn = document.getElementById('wechat-contact-fetch-model');
if (!apiUrl) {
- showToast('请先填写API地址', '🧊');
+ showToast('请先填写API地址', 'info');
return;
}
@@ -547,7 +552,7 @@ function bindEvents() {
modelList.innerHTML = models.map(m => `