mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 03:35:50 +00:00
Add files via upload
This commit is contained in:
29
ai.js
29
ai.js
@@ -408,6 +408,10 @@ function buildCallRequestPrompt() {
|
|||||||
- 语音通话: [语音通话]
|
- 语音通话: [语音通话]
|
||||||
- 视频通话: [视频通话]
|
- 视频通话: [视频通话]
|
||||||
示例:[视频通话]
|
示例:[视频通话]
|
||||||
|
|
||||||
|
【禁止】
|
||||||
|
- 禁止输出 [结束通话]、[取消通话]、[挂断]、[接听]、[拒接] 等标签
|
||||||
|
- 通话的接听、挂断、结束等状态由系统自动处理,你只能发起通话请求
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,6 +577,12 @@ ${allowStickers ? buildStickerPrompt(settings) : ''}${allowMusicShare ? buildMus
|
|||||||
|
|
||||||
格式:[回复:关键词]你的回复内容
|
格式:[回复:关键词]你的回复内容
|
||||||
|
|
||||||
|
【重要限制】引用只能用于纯文本消息!
|
||||||
|
× 禁止:[回复:xxx][表情:yyy] ← 表情包不能带引用!
|
||||||
|
× 禁止:[回复:xxx][语音:yyy] ← 语音不能带引用!
|
||||||
|
× 禁止:[回复:xxx][照片:yyy] ← 照片不能带引用!
|
||||||
|
√ 正确:[回复:xxx]纯文本内容 ← 只有纯文本可以带引用
|
||||||
|
|
||||||
【音乐引用特殊规则 - 必须遵守!】
|
【音乐引用特殊规则 - 必须遵守!】
|
||||||
- 引用音乐分享时,关键词格式必须是"歌手-歌名"(中间用减号,不要空格)
|
- 引用音乐分享时,关键词格式必须是"歌手-歌名"(中间用减号,不要空格)
|
||||||
- 错误:[回复:全世界都在下着雨] ← 只有歌名,错误!
|
- 错误:[回复:全世界都在下着雨] ← 只有歌名,错误!
|
||||||
@@ -607,7 +617,24 @@ ${allowStickers ? buildStickerPrompt(settings) : ''}${allowMusicShare ? buildMus
|
|||||||
|
|
||||||
示例(语音/照片必须单独一条):
|
示例(语音/照片必须单独一条):
|
||||||
[语音:宝贝我想你了]
|
[语音:宝贝我想你了]
|
||||||
给你看|||[照片:窗外的夕阳]`;
|
给你看|||[照片:窗外的夕阳]
|
||||||
|
|
||||||
|
【拉黑功能 - 重要!】
|
||||||
|
当你想拉黑用户时(比如用户惹你生气了、你不想理用户了),使用:[拉黑]
|
||||||
|
当你想取消拉黑时(气消了、想和好了),使用:[取消拉黑]
|
||||||
|
|
||||||
|
【绝对禁止】拉黑标签必须【完全独立】成一条消息!
|
||||||
|
× 错误:不想理你了[拉黑] ← 标签和文字在一起,错误!
|
||||||
|
× 错误:[拉黑]不想理你了 ← 标签和文字在一起,错误!
|
||||||
|
√ 正确:不想理你了|||[拉黑] ← 用 ||| 分开,标签独立一条
|
||||||
|
|
||||||
|
示例(正确):
|
||||||
|
哼,不理你了|||[拉黑]
|
||||||
|
好吧原谅你|||[取消拉黑]
|
||||||
|
|
||||||
|
拉黑后:
|
||||||
|
- 用户发的消息你收不到,会显示"被拒收"
|
||||||
|
- 你发的消息用户也看不到(解除后才能看到)`;
|
||||||
|
|
||||||
// Meme 表情包提示词(如果启用)
|
// Meme 表情包提示词(如果启用)
|
||||||
if (allowStickers && settings.memeStickersEnabled) {
|
if (allowStickers && settings.memeStickersEnabled) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { startVideoCall } from './video-call.js';
|
|||||||
import { showMusicPanel, initMusicEvents } from './music.js';
|
import { showMusicPanel, initMusicEvents } from './music.js';
|
||||||
import { showRedPacketPage } from './red-packet.js';
|
import { showRedPacketPage } from './red-packet.js';
|
||||||
import { showTransferPage } from './transfer.js';
|
import { showTransferPage } from './transfer.js';
|
||||||
|
import { showGiftPage } from './gift.js';
|
||||||
import { getSettings, splitAIMessages } from './config.js';
|
import { getSettings, splitAIMessages } from './config.js';
|
||||||
import { refreshChatList } from './ui.js';
|
import { refreshChatList } from './ui.js';
|
||||||
import { requestSave } from './save-manager.js';
|
import { requestSave } from './save-manager.js';
|
||||||
@@ -677,6 +678,14 @@ function handleFuncItemClick(func) {
|
|||||||
showTransferPage();
|
showTransferPage();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case 'gift':
|
||||||
|
hideFuncPanel();
|
||||||
|
if (isInGroupChat()) {
|
||||||
|
showToast('群聊暂不支持送礼物', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showGiftPage();
|
||||||
|
return;
|
||||||
case 'listen':
|
case 'listen':
|
||||||
hideFuncPanel();
|
hideFuncPanel();
|
||||||
// 群聊不支持一起听
|
// 群聊不支持一起听
|
||||||
|
|||||||
513
chat.js
513
chat.js
@@ -17,6 +17,7 @@ import { startVoiceCall } from './voice-call.js';
|
|||||||
import { startVideoCall } from './video-call.js';
|
import { startVideoCall } from './video-call.js';
|
||||||
import { showOpenRedPacket, generateRedPacketId } from './red-packet.js';
|
import { showOpenRedPacket, generateRedPacketId } from './red-packet.js';
|
||||||
import { showReceiveTransferPage, generateTransferId } from './transfer.js';
|
import { showReceiveTransferPage, generateTransferId } from './transfer.js';
|
||||||
|
import { checkGiftDelivery } from './gift.js';
|
||||||
|
|
||||||
// 当前聊天的联系人索引
|
// 当前聊天的联系人索引
|
||||||
export let currentChatIndex = -1;
|
export let currentChatIndex = -1;
|
||||||
@@ -76,6 +77,336 @@ function extractCallRequest(message) {
|
|||||||
// 内部使用的别名
|
// 内部使用的别名
|
||||||
const detectAiCallRequestType = detectAiCallRequest;
|
const detectAiCallRequestType = detectAiCallRequest;
|
||||||
|
|
||||||
|
// 检测并提取AI拉黑/取消拉黑标签
|
||||||
|
// 返回 { action: 'block'|'unblock'|null, textWithoutTag: string }
|
||||||
|
export function extractBlockAction(message) {
|
||||||
|
if (!message || typeof message !== 'string') return { action: null, textWithoutTag: message || '' };
|
||||||
|
|
||||||
|
// 检查是否包含拉黑标签
|
||||||
|
const blockMatch = message.match(/\[拉黑\]/);
|
||||||
|
if (blockMatch) {
|
||||||
|
const textWithoutTag = message.replace(blockMatch[0], '').trim();
|
||||||
|
return { action: 'block', textWithoutTag };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含取消拉黑标签
|
||||||
|
const unblockMatch = message.match(/\[取消拉黑\]/);
|
||||||
|
if (unblockMatch) {
|
||||||
|
const textWithoutTag = message.replace(unblockMatch[0], '').trim();
|
||||||
|
return { action: 'unblock', textWithoutTag };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { action: null, textWithoutTag: message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示消息被拒收提示(在消息左侧显示红色感叹号)
|
||||||
|
export function appendBlockedNotice(contact) {
|
||||||
|
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||||
|
if (!messagesContainer) return;
|
||||||
|
|
||||||
|
// 找到最后一条用户消息
|
||||||
|
const lastUserMsg = messagesContainer.querySelector('.wechat-message.self:last-of-type');
|
||||||
|
if (!lastUserMsg) return;
|
||||||
|
|
||||||
|
// 检查是否已经有感叹号了
|
||||||
|
if (lastUserMsg.querySelector('.wechat-blocked-exclamation')) return;
|
||||||
|
|
||||||
|
// 在消息气泡左侧添加红色感叹号
|
||||||
|
const exclamationDiv = document.createElement('div');
|
||||||
|
exclamationDiv.className = 'wechat-blocked-exclamation';
|
||||||
|
exclamationDiv.innerHTML = `<span class="wechat-blocked-exclamation-icon">!</span>`;
|
||||||
|
|
||||||
|
// 插入到消息内容前面
|
||||||
|
const contentDiv = lastUserMsg.querySelector('.wechat-message-content');
|
||||||
|
if (contentDiv) {
|
||||||
|
contentDiv.insertBefore(exclamationDiv, contentDiv.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
exclamationDiv.addEventListener('click', () => {
|
||||||
|
handleBlockedExclamationClick(contact, exclamationDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理点击被拒收消息的感叹号
|
||||||
|
async function handleBlockedExclamationClick(contact, exclamationEl) {
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
exclamationEl.classList.add('loading');
|
||||||
|
|
||||||
|
// 等待2秒
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
// 弹出"已添加好友"的提示
|
||||||
|
showFriendAddedPopup(contact.name);
|
||||||
|
|
||||||
|
// 取消拉黑状态
|
||||||
|
contact.blockedByAI = false;
|
||||||
|
requestSave();
|
||||||
|
|
||||||
|
// 移除感叹号
|
||||||
|
exclamationEl.remove();
|
||||||
|
|
||||||
|
// 移除所有被拉黑时发送的消息的感叹号
|
||||||
|
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.querySelectorAll('.wechat-blocked-exclamation').forEach(el => el.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI主动发消息
|
||||||
|
await triggerAIAfterUnblock(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示"已添加好友"的手机弹窗
|
||||||
|
function showFriendAddedPopup(name) {
|
||||||
|
// 创建弹窗遮罩
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'wechat-phone-popup-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="wechat-phone-popup">
|
||||||
|
<div class="wechat-phone-popup-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#07c160" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<path d="M8 12l2.5 2.5L16 9"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-phone-popup-text">${escapeHtml(name)}已添加您为好友,现在可以开始聊天了。</div>
|
||||||
|
<div class="wechat-phone-popup-btn">确定</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// 点击确定关闭
|
||||||
|
overlay.querySelector('.wechat-phone-popup-btn').addEventListener('click', () => {
|
||||||
|
overlay.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击遮罩也关闭
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) {
|
||||||
|
overlay.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI解除拉黑后主动发消息
|
||||||
|
async function triggerAIAfterUnblock(contact) {
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
const contactIndex = getSettings().contacts.indexOf(contact);
|
||||||
|
if (contactIndex < 0) return;
|
||||||
|
|
||||||
|
// 显示typing
|
||||||
|
if (currentChatIndex === contactIndex) {
|
||||||
|
showTypingIndicator(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { callAI } = await import('./ai.js');
|
||||||
|
const prompt = '[你刚才把用户拉黑了,现在你们和好了,用户重新添加了你为好友。请主动和用户说点什么,表达你的态度(可以是原谅、撒娇、装作若无其事等,根据你的性格决定)。回复1-2句话即可。]';
|
||||||
|
|
||||||
|
const aiResponse = await callAI(contact, prompt);
|
||||||
|
|
||||||
|
if (currentChatIndex === contactIndex) {
|
||||||
|
hideTypingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiMessages = splitAIMessages(aiResponse);
|
||||||
|
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')}`;
|
||||||
|
|
||||||
|
for (const msg of aiMessages) {
|
||||||
|
if (!msg.trim()) continue;
|
||||||
|
|
||||||
|
// 解析引用格式
|
||||||
|
const { parseAIQuote } = await import('./chat.js');
|
||||||
|
const parsed = parseAIQuote(msg, contact);
|
||||||
|
const content = parsed.content;
|
||||||
|
|
||||||
|
contact.chatHistory.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: content,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentChatIndex === contactIndex) {
|
||||||
|
appendMessage('assistant', content, contact, false, parsed.quote);
|
||||||
|
} else {
|
||||||
|
contact.unreadCount = (contact.unreadCount || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
contact.lastMessage = content;
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
|
||||||
|
await sleep(1500);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[可乐] AI解除拉黑后发消息失败:', err);
|
||||||
|
if (currentChatIndex === contactIndex) {
|
||||||
|
hideTypingIndicator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储被拉黑期间AI发送的消息的定时器
|
||||||
|
const blockedAITimers = new Map();
|
||||||
|
|
||||||
|
// 用户拉黑AI时开始AI发消息
|
||||||
|
export function startBlockedAIMessages(contact) {
|
||||||
|
if (!contact || !contact.id) return;
|
||||||
|
|
||||||
|
// 清除之前的定时器
|
||||||
|
stopBlockedAIMessages(contact);
|
||||||
|
|
||||||
|
// 初始化被拉黑期间的消息队列
|
||||||
|
if (!contact.blockedMessages) {
|
||||||
|
contact.blockedMessages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始定时发送消息
|
||||||
|
const timerId = setInterval(async () => {
|
||||||
|
if (!contact.isBlocked) {
|
||||||
|
stopBlockedAIMessages(contact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { callAI } = await import('./ai.js');
|
||||||
|
const msgCount = contact.blockedMessages.length;
|
||||||
|
let prompt;
|
||||||
|
|
||||||
|
if (msgCount === 0) {
|
||||||
|
prompt = '[用户把你拉黑了!你现在发的消息用户看不到。你很想跟用户说话,发一条消息表达你的情绪(困惑、委屈、生气等,根据你的性格)。1句话即可。]';
|
||||||
|
} else if (msgCount < 3) {
|
||||||
|
prompt = '[用户还是拉黑着你,你继续发消息但用户看不到。再发一条,可以是追问、撒娇、生气等。1句话即可。]';
|
||||||
|
} else {
|
||||||
|
prompt = '[用户还是没有取消拉黑你,继续发一条消息。可能开始认错、委屈、或者假装不在乎等。1句话即可。]';
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiResponse = await callAI(contact, prompt);
|
||||||
|
const aiMessages = splitAIMessages(aiResponse);
|
||||||
|
|
||||||
|
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')}`;
|
||||||
|
|
||||||
|
for (const msg of aiMessages) {
|
||||||
|
if (!msg.trim()) continue;
|
||||||
|
|
||||||
|
// 解析引用格式
|
||||||
|
const parsed = parseAIQuote(msg, contact);
|
||||||
|
const content = parsed.content;
|
||||||
|
|
||||||
|
// 存储到被拉黑消息队列(不存入主聊天记录)
|
||||||
|
contact.blockedMessages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: content,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
quote: parsed.quote || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[可乐] AI被拉黑期间发送消息:', content.substring(0, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[可乐] AI被拉黑期间发消息失败:', err);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
blockedAITimers.set(contact.id, timerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止AI被拉黑期间的消息发送
|
||||||
|
export function stopBlockedAIMessages(contact) {
|
||||||
|
if (!contact || !contact.id) return;
|
||||||
|
|
||||||
|
const timerId = blockedAITimers.get(contact.id);
|
||||||
|
if (timerId) {
|
||||||
|
clearInterval(timerId);
|
||||||
|
blockedAITimers.delete(contact.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户取消拉黑AI时显示被拉黑期间的消息
|
||||||
|
export async function showBlockedMessages(contact) {
|
||||||
|
if (!contact || !contact.blockedMessages || contact.blockedMessages.length === 0) return;
|
||||||
|
|
||||||
|
const contactIndex = getSettings().contacts.indexOf(contact);
|
||||||
|
const inChat = currentChatIndex === contactIndex;
|
||||||
|
|
||||||
|
// 逐条显示被拉黑期间的消息
|
||||||
|
for (const msg of contact.blockedMessages) {
|
||||||
|
// 添加到聊天记录
|
||||||
|
contact.chatHistory.push({
|
||||||
|
...msg,
|
||||||
|
wasBlocked: true // 标记为被拉黑期间的消息
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inChat) {
|
||||||
|
// 显示typing
|
||||||
|
showTypingIndicator(contact);
|
||||||
|
await sleep(1500);
|
||||||
|
hideTypingIndicator();
|
||||||
|
|
||||||
|
// 显示消息(带红色感叹号)
|
||||||
|
appendBlockedAIMessage(msg.content, contact, msg.quote);
|
||||||
|
} else {
|
||||||
|
contact.unreadCount = (contact.unreadCount || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
contact.lastMessage = msg.content;
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
|
||||||
|
await sleep(800);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空被拉黑消息队列
|
||||||
|
contact.blockedMessages = [];
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示AI被拉黑期间发送的消息(右侧带红色感叹号)
|
||||||
|
function appendBlockedAIMessage(content, contact, quote = null) {
|
||||||
|
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||||
|
if (!messagesContainer) return;
|
||||||
|
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = 'wechat-message'; // AI消息在左边
|
||||||
|
|
||||||
|
const firstChar = contact?.name ? contact.name.charAt(0) : '?';
|
||||||
|
const avatarContent = contact?.avatar
|
||||||
|
? `<img src="${contact.avatar}" alt="" onerror="this.style.display='none';this.parentElement.innerHTML='${firstChar}'">`
|
||||||
|
: firstChar;
|
||||||
|
|
||||||
|
// 解析 meme 标签
|
||||||
|
const processedContent = parseMemeTag(content);
|
||||||
|
const hasMeme = processedContent !== content;
|
||||||
|
const bubbleContent = `<div class="wechat-message-bubble">${hasMeme ? processedContent : escapeHtml(content)}</div>`;
|
||||||
|
|
||||||
|
// 红色感叹号
|
||||||
|
const exclamationHtml = `
|
||||||
|
<div class="wechat-blocked-ai-exclamation" title="对方在您拉黑期间发送">
|
||||||
|
<span class="wechat-blocked-exclamation-icon">!</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messageDiv.innerHTML = `
|
||||||
|
<div class="wechat-message-avatar">${avatarContent}</div>
|
||||||
|
<div class="wechat-message-content">${bubbleContent}${exclamationHtml}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageDiv);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
|
||||||
|
bindMessageBubbleEvents(messagesContainer);
|
||||||
|
}
|
||||||
|
|
||||||
// 检查聊天记录是否需要总结(单聊)
|
// 检查聊天记录是否需要总结(单聊)
|
||||||
export function checkSummaryReminder(contact) {
|
export function checkSummaryReminder(contact) {
|
||||||
if (!contact || !contact.chatHistory) return;
|
if (!contact || !contact.chatHistory) return;
|
||||||
@@ -258,6 +589,11 @@ export function parseAIQuote(message, contact) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (contentMatch || stickerDescMatch || musicMatch) {
|
if (contentMatch || stickerDescMatch || musicMatch) {
|
||||||
|
// 如果被引用的消息已被撤回,则不允许引用
|
||||||
|
if (historyMsg.isRecalled === true) {
|
||||||
|
continue; // 跳过已撤回的消息,继续查找
|
||||||
|
}
|
||||||
|
|
||||||
if (historyMsg.role === 'user') {
|
if (historyMsg.role === 'user') {
|
||||||
sender = context?.name1 || '用户';
|
sender = context?.name1 || '用户';
|
||||||
} else {
|
} else {
|
||||||
@@ -268,13 +604,20 @@ export function parseAIQuote(message, contact) {
|
|||||||
isPhoto = historyMsg.isPhoto === true;
|
isPhoto = historyMsg.isPhoto === true;
|
||||||
isSticker = historyMsg.isSticker === true;
|
isSticker = historyMsg.isSticker === true;
|
||||||
isMusic = historyMsg.isMusic === true;
|
isMusic = historyMsg.isMusic === true;
|
||||||
|
|
||||||
|
// 用完整的历史消息内容替换AI给的关键词
|
||||||
if (isMusic && historyMsg.musicInfo) {
|
if (isMusic && historyMsg.musicInfo) {
|
||||||
musicInfo = historyMsg.musicInfo;
|
musicInfo = historyMsg.musicInfo;
|
||||||
// 修正引用内容为“歌手-歌名”格式(不加空格)
|
// 音乐消息:使用"歌手-歌名"格式
|
||||||
const artist = (historyMsg.musicInfo.artist || '未知歌手').toString().trim();
|
const artist = (historyMsg.musicInfo.artist || '未知歌手').toString().trim();
|
||||||
const name = (historyMsg.musicInfo.name || '').toString().trim();
|
const name = (historyMsg.musicInfo.name || '').toString().trim();
|
||||||
quoteContent = artist && name ? `${artist}-${name}` : (name || artist || quoteContent);
|
quoteContent = artist && name ? `${artist}-${name}` : (name || artist || quoteContent);
|
||||||
|
} else if (!isSticker && historyMsg.content) {
|
||||||
|
// 普通文字/语音/照片消息:使用完整原文
|
||||||
|
quoteContent = historyMsg.content;
|
||||||
}
|
}
|
||||||
|
// 表情消息保持原样,渲染时会显示[表情]
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,6 +656,14 @@ export function setCurrentChatIndex(index) {
|
|||||||
currentChatIndex = index;
|
currentChatIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新拉黑菜单文本
|
||||||
|
export function updateBlockMenuText(isBlocked) {
|
||||||
|
const blockText = document.getElementById('wechat-menu-block-text');
|
||||||
|
if (blockText) {
|
||||||
|
blockText.textContent = isBlocked ? '取消拉黑' : '拉黑';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 打开聊天界面
|
// 打开聊天界面
|
||||||
export function openChat(contactIndex) {
|
export function openChat(contactIndex) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
@@ -328,6 +679,9 @@ export function openChat(contactIndex) {
|
|||||||
refreshChatList();
|
refreshChatList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新拉黑菜单文本
|
||||||
|
updateBlockMenuText(contact.isBlocked === true);
|
||||||
|
|
||||||
document.getElementById('wechat-main-content').classList.add('hidden');
|
document.getElementById('wechat-main-content').classList.add('hidden');
|
||||||
document.getElementById('wechat-chat-page').classList.remove('hidden');
|
document.getElementById('wechat-chat-page').classList.remove('hidden');
|
||||||
document.getElementById('wechat-chat-title').textContent = contact.name;
|
document.getElementById('wechat-chat-title').textContent = contact.name;
|
||||||
@@ -714,6 +1068,33 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否是礼物消息
|
||||||
|
if (msg.isGift && msg.giftInfo) {
|
||||||
|
const giftInfo = msg.giftInfo;
|
||||||
|
const isToy = giftInfo.isToy === true;
|
||||||
|
const giftTypeClass = isToy ? 'wechat-gift-bubble-toy' : '';
|
||||||
|
const giftTypeLabel = isToy ? '情趣礼物' : '礼物';
|
||||||
|
|
||||||
|
const giftBubbleHTML = `
|
||||||
|
<div class="wechat-gift-bubble ${giftTypeClass}">
|
||||||
|
<div class="wechat-gift-bubble-emoji">${giftInfo.emoji || '🎁'}</div>
|
||||||
|
<div class="wechat-gift-bubble-info">
|
||||||
|
<div class="wechat-gift-bubble-name">${escapeHtml(giftInfo.name || '礼物')}</div>
|
||||||
|
${giftInfo.customDesc ? `<div class="wechat-gift-bubble-desc">${escapeHtml(giftInfo.customDesc)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="wechat-gift-bubble-label">${giftTypeLabel}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (msg.role === 'user') {
|
||||||
|
html += `<div class="wechat-message self" data-msg-index="${index}" data-msg-role="user"><div class="wechat-message-avatar">${getUserAvatarHTML()}</div><div class="wechat-message-content">${giftBubbleHTML}</div></div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="wechat-message" data-msg-index="${index}" data-msg-role="assistant"><div class="wechat-message-avatar">${avatarContent}</div><div class="wechat-message-content">${giftBubbleHTML}</div></div>`;
|
||||||
|
}
|
||||||
|
lastTimestamp = msgTimestamp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (index === 0 || (msgTimestamp - lastTimestamp > TIME_GAP_THRESHOLD)) {
|
if (index === 0 || (msgTimestamp - lastTimestamp > TIME_GAP_THRESHOLD)) {
|
||||||
const timeLabel = formatMessageTime(msgTimestamp);
|
const timeLabel = formatMessageTime(msgTimestamp);
|
||||||
if (timeLabel) {
|
if (timeLabel) {
|
||||||
@@ -838,8 +1219,8 @@ export function renderChatHistory(contact, chatHistory, indexOffset = 0) {
|
|||||||
} else if (msg.quote.isSticker) {
|
} else if (msg.quote.isSticker) {
|
||||||
quoteText = '[表情]';
|
quoteText = '[表情]';
|
||||||
} else {
|
} else {
|
||||||
quoteText = quoteContent.length > 30
|
quoteText = quoteContent.length > 8
|
||||||
? quoteContent.substring(0, 30) + '...'
|
? quoteContent.substring(0, 8) + '...'
|
||||||
: quoteContent;
|
: quoteContent;
|
||||||
}
|
}
|
||||||
quoteHtml = `
|
quoteHtml = `
|
||||||
@@ -1152,8 +1533,8 @@ export function appendMessage(role, content, contact, isVoice = false, quote = n
|
|||||||
} else if (quote.isSticker) {
|
} else if (quote.isSticker) {
|
||||||
quoteText = '[表情]';
|
quoteText = '[表情]';
|
||||||
} else {
|
} else {
|
||||||
quoteText = quote.content.length > 30
|
quoteText = quote.content.length > 8
|
||||||
? quote.content.substring(0, 30) + '...'
|
? quote.content.substring(0, 8) + '...'
|
||||||
: quote.content;
|
: quote.content;
|
||||||
}
|
}
|
||||||
quoteHtml = `
|
quoteHtml = `
|
||||||
@@ -1437,6 +1818,17 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
|||||||
saveNow();
|
saveNow();
|
||||||
refreshChatList();
|
refreshChatList();
|
||||||
|
|
||||||
|
// 如果联系人被拉黑,不触发AI回复
|
||||||
|
if (contact.isBlocked === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户被AI拉黑,显示被拒收提示,不触发AI回复
|
||||||
|
if (contact.blockedByAI === true) {
|
||||||
|
appendBlockedNotice(contact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 只有用户还在当前聊天时才显示打字指示器
|
// 只有用户还在当前聊天时才显示打字指示器
|
||||||
if (currentChatIndex === contactIndex) {
|
if (currentChatIndex === contactIndex) {
|
||||||
showTypingIndicator(contact);
|
showTypingIndicator(contact);
|
||||||
@@ -1488,6 +1880,28 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
|||||||
let stickerUrl = null;
|
let stickerUrl = null;
|
||||||
let aiQuote = null;
|
let aiQuote = null;
|
||||||
|
|
||||||
|
// 检测拉黑/取消拉黑标签
|
||||||
|
const blockAction = extractBlockAction(aiMsg);
|
||||||
|
if (blockAction.action === 'block') {
|
||||||
|
contact.blockedByAI = true;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI拉黑了用户');
|
||||||
|
requestSave();
|
||||||
|
// 如果拉黑标签是单独一条消息(没有其他文本),跳过显示
|
||||||
|
if (!aiMsg.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (blockAction.action === 'unblock') {
|
||||||
|
contact.blockedByAI = false;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI取消拉黑用户');
|
||||||
|
requestSave();
|
||||||
|
// 如果取消拉黑标签是单独一条消息(没有其他文本),跳过显示
|
||||||
|
if (!aiMsg.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
aiMsg = voiceMatch[1];
|
aiMsg = voiceMatch[1];
|
||||||
@@ -1559,8 +1973,8 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
|||||||
continue; // 跳过后续处理,继续下一条消息
|
continue; // 跳过后续处理,继续下一条消息
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析AI撤回格式 [撤回] 或 [撤回了一条消息]
|
// 解析AI撤回格式 [撤回] / [撤回了一条消息] / [撤回消息] / [已撤回] 等
|
||||||
const recallMatch = aiMsg.match(/^\[撤回(?:了一条消息)?\]$/);
|
const recallMatch = aiMsg.match(/^\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]$/);
|
||||||
if (recallMatch) {
|
if (recallMatch) {
|
||||||
// 找到AI的上一条消息并标记为撤回
|
// 找到AI的上一条消息并标记为撤回
|
||||||
// 等待5秒让用户看到消息内容后再撤回
|
// 等待5秒让用户看到消息内容后再撤回
|
||||||
@@ -1905,6 +2319,9 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
|||||||
refreshChatList();
|
refreshChatList();
|
||||||
checkSummaryReminder(contact);
|
checkSummaryReminder(contact);
|
||||||
|
|
||||||
|
// 检查礼物是否送达(25条消息后触发)
|
||||||
|
checkGiftDelivery(contact);
|
||||||
|
|
||||||
// 尝试触发朋友圈生成(随机触发+30条保底)
|
// 尝试触发朋友圈生成(随机触发+30条保底)
|
||||||
tryTriggerMomentAfterChat(currentChatIndex);
|
tryTriggerMomentAfterChat(currentChatIndex);
|
||||||
|
|
||||||
@@ -1987,6 +2404,22 @@ export async function sendStickerMessage(stickerUrl, description = '') {
|
|||||||
let aiIsPhoto = false;
|
let aiIsPhoto = false;
|
||||||
let stickerUrl = null;
|
let stickerUrl = null;
|
||||||
|
|
||||||
|
// 检测拉黑/取消拉黑标签
|
||||||
|
const blockAction = extractBlockAction(aiMsg);
|
||||||
|
if (blockAction.action === 'block') {
|
||||||
|
contact.blockedByAI = true;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI拉黑了用户 (sendStickerMessage)');
|
||||||
|
requestSave();
|
||||||
|
if (!aiMsg.trim()) continue;
|
||||||
|
} else if (blockAction.action === 'unblock') {
|
||||||
|
contact.blockedByAI = false;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI取消拉黑用户 (sendStickerMessage)');
|
||||||
|
requestSave();
|
||||||
|
if (!aiMsg.trim()) continue;
|
||||||
|
}
|
||||||
|
|
||||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
aiMsg = voiceMatch[1];
|
aiMsg = voiceMatch[1];
|
||||||
@@ -2016,8 +2449,8 @@ export async function sendStickerMessage(stickerUrl, description = '') {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析AI撤回格式 [撤回] 或 [撤回了一条消息]
|
// 解析AI撤回格式 [撤回] / [撤回了一条消息] / [撤回消息] / [已撤回] 等
|
||||||
const recallMatch = aiMsg.match(/^\[撤回(?:了一条消息)?\]$/);
|
const recallMatch = aiMsg.match(/^\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]$/);
|
||||||
if (recallMatch) {
|
if (recallMatch) {
|
||||||
// 等待5秒让用户看到消息内容后再撤回
|
// 等待5秒让用户看到消息内容后再撤回
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
@@ -2341,6 +2774,17 @@ export async function sendPhotoMessage(description) {
|
|||||||
// 显示消息
|
// 显示消息
|
||||||
appendPhotoMessage('user', polishedDescription, contact);
|
appendPhotoMessage('user', polishedDescription, contact);
|
||||||
|
|
||||||
|
// 如果联系人被拉黑,不触发AI回复
|
||||||
|
if (contact.isBlocked === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户被AI拉黑,显示被拒收提示,不触发AI回复
|
||||||
|
if (contact.blockedByAI === true) {
|
||||||
|
appendBlockedNotice(contact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 只有用户还在当前聊天时才显示打字指示器
|
// 只有用户还在当前聊天时才显示打字指示器
|
||||||
if (currentChatIndex === contactIndex) {
|
if (currentChatIndex === contactIndex) {
|
||||||
showTypingIndicator(contact);
|
showTypingIndicator(contact);
|
||||||
@@ -2368,6 +2812,22 @@ export async function sendPhotoMessage(description) {
|
|||||||
let aiIsPhoto = false;
|
let aiIsPhoto = false;
|
||||||
let stickerUrl = null;
|
let stickerUrl = null;
|
||||||
|
|
||||||
|
// 检测拉黑/取消拉黑标签
|
||||||
|
const blockAction = extractBlockAction(aiMsg);
|
||||||
|
if (blockAction.action === 'block') {
|
||||||
|
contact.blockedByAI = true;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI拉黑了用户 (sendPhotoMessage)');
|
||||||
|
requestSave();
|
||||||
|
if (!aiMsg.trim()) continue;
|
||||||
|
} else if (blockAction.action === 'unblock') {
|
||||||
|
contact.blockedByAI = false;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI取消拉黑用户 (sendPhotoMessage)');
|
||||||
|
requestSave();
|
||||||
|
if (!aiMsg.trim()) continue;
|
||||||
|
}
|
||||||
|
|
||||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
aiMsg = voiceMatch[1];
|
aiMsg = voiceMatch[1];
|
||||||
@@ -2397,8 +2857,8 @@ export async function sendPhotoMessage(description) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析AI撤回格式 [撤回] 或 [撤回了一条消息]
|
// 解析AI撤回格式 [撤回] / [撤回了一条消息] / [撤回消息] / [已撤回] 等
|
||||||
const recallMatch = aiMsg.match(/^\[撤回(?:了一条消息)?\]$/);
|
const recallMatch = aiMsg.match(/^\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]$/);
|
||||||
if (recallMatch) {
|
if (recallMatch) {
|
||||||
// 等待5秒让用户看到消息内容后再撤回
|
// 等待5秒让用户看到消息内容后再撤回
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
@@ -2822,6 +3282,17 @@ export async function sendBatchMessages(messages) {
|
|||||||
saveNow();
|
saveNow();
|
||||||
refreshChatList();
|
refreshChatList();
|
||||||
|
|
||||||
|
// 如果联系人被拉黑,不触发AI回复
|
||||||
|
if (contact.isBlocked === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户被AI拉黑,显示被拒收提示,不触发AI回复
|
||||||
|
if (contact.blockedByAI === true) {
|
||||||
|
appendBlockedNotice(contact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 第二步:调用AI(一次性)
|
// 第二步:调用AI(一次性)
|
||||||
// 只有用户还在当前聊天时才显示打字指示器
|
// 只有用户还在当前聊天时才显示打字指示器
|
||||||
if (currentChatIndex === contactIndex) {
|
if (currentChatIndex === contactIndex) {
|
||||||
@@ -2851,6 +3322,22 @@ export async function sendBatchMessages(messages) {
|
|||||||
let stickerUrl = null;
|
let stickerUrl = null;
|
||||||
let aiQuote = null;
|
let aiQuote = null;
|
||||||
|
|
||||||
|
// 检测拉黑/取消拉黑标签
|
||||||
|
const blockAction = extractBlockAction(aiMsg);
|
||||||
|
if (blockAction.action === 'block') {
|
||||||
|
contact.blockedByAI = true;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI拉黑了用户 (sendBatchMessages)');
|
||||||
|
requestSave();
|
||||||
|
if (!aiMsg.trim()) continue;
|
||||||
|
} else if (blockAction.action === 'unblock') {
|
||||||
|
contact.blockedByAI = false;
|
||||||
|
aiMsg = blockAction.textWithoutTag;
|
||||||
|
console.log('[可乐] AI取消拉黑用户 (sendBatchMessages)');
|
||||||
|
requestSave();
|
||||||
|
if (!aiMsg.trim()) continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 解析语音格式
|
// 解析语音格式
|
||||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
@@ -2865,8 +3352,8 @@ export async function sendBatchMessages(messages) {
|
|||||||
aiIsPhoto = true;
|
aiIsPhoto = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析撤回格式 [撤回] 或 [撤回了一条消息]
|
// 解析撤回格式 [撤回] / [撤回了一条消息] / [撤回消息] / [已撤回] 等
|
||||||
const recallMatch = aiMsg.match(/^\[撤回(?:了一条消息)?\]$/);
|
const recallMatch = aiMsg.match(/^\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]$/);
|
||||||
if (recallMatch) {
|
if (recallMatch) {
|
||||||
// 等待5秒让用户看到消息内容后再撤回
|
// 等待5秒让用户看到消息内容后再撤回
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
|
|||||||
30
config.js
30
config.js
@@ -148,13 +148,17 @@ export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】
|
|||||||
|
|
||||||
【绝对禁止 - 违反会被过滤】
|
【绝对禁止 - 违反会被过滤】
|
||||||
- 禁止使用小括号描述动作或语气,如(xxx)
|
- 禁止使用小括号描述动作或语气,如(xxx)
|
||||||
- 禁止 [表情:xxx] [照片:xxx] [语音:xxx] [音乐:xxx]
|
- 禁止 [表情:xxx] [照片:xxx] [语音:xxx]
|
||||||
|
- 禁止 [分享音乐:xxx] - 一起听场景不需要分享音乐!
|
||||||
- 禁止 [回复:xxx] 引用格式
|
- 禁止 [回复:xxx] 引用格式
|
||||||
- 禁止 <meme>xxx</meme>
|
- 禁止 <meme>xxx</meme>
|
||||||
- 禁止任何非文字格式
|
- 禁止任何非文字格式
|
||||||
|
|
||||||
【换歌格式】
|
【换歌格式 - 仅限一起听场景】
|
||||||
如果想换歌:[换歌:歌名]
|
想换一首歌时使用:[换歌:歌名]
|
||||||
|
- 只需要歌名,不需要歌手名
|
||||||
|
- 这是一起听专用格式,不是分享音乐
|
||||||
|
- 示例:[换歌:晴天]、[换歌:爱在西元前]
|
||||||
|
|
||||||
【自然聊天示例】
|
【自然聊天示例】
|
||||||
我来了~
|
我来了~
|
||||||
@@ -388,8 +392,12 @@ export function splitAIMessages(response) {
|
|||||||
const musicRegexNoColon = /\[分享音乐\]\s*[\u4e00-\u9fa5a-zA-Z0-9]+(?:\s*[-–—]\s*[\u4e00-\u9fa5a-zA-Z0-9]+)?/g;
|
const musicRegexNoColon = /\[分享音乐\]\s*[\u4e00-\u9fa5a-zA-Z0-9]+(?:\s*[-–—]\s*[\u4e00-\u9fa5a-zA-Z0-9]+)?/g;
|
||||||
// 表情标签 [表情:xxx]
|
// 表情标签 [表情:xxx]
|
||||||
const stickerRegex = /\[表情[::]\s*.+?\]/g;
|
const stickerRegex = /\[表情[::]\s*.+?\]/g;
|
||||||
// 撤回标签 [撤回] 或 [撤回了一条消息]
|
// 撤回标签 [撤回] / [撤回了一条消息] / [撤回消息] / [撤回一条消息] / [已撤回] / [消息撤回]
|
||||||
const recallRegex = /\[撤回(?:了一条消息)?\]/g;
|
const recallRegex = /\[(?:撤回(?:了?一条)?消息?|已撤回|消息撤回)\]/g;
|
||||||
|
// 红包标签 [红包:金额:祝福语] 或 [红包:金额]
|
||||||
|
const redPacketRegex = /\[红包[::]\d+(?:\.\d{1,2})?(?:[::][^\]]+)?\]/g;
|
||||||
|
// 转账标签 [转账:金额:说明] 或 [转账:金额]
|
||||||
|
const transferRegex = /\[转账[::]\d+(?:\.\d{1,2})?(?:[::][^\]]+)?\]/g;
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
// 【重要】检查是否是朋友圈标签 - 朋友圈标签不应该被分割,因为可能包含内嵌的 [照片:xxx]
|
// 【重要】检查是否是朋友圈标签 - 朋友圈标签不应该被分割,因为可能包含内嵌的 [照片:xxx]
|
||||||
@@ -452,6 +460,18 @@ export function splitAIMessages(response) {
|
|||||||
specialTags.push({ tag: match[0], index: match.index });
|
specialTags.push({ tag: match[0], index: match.index });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查找红包标签
|
||||||
|
const redPacketRegexLocal = new RegExp(redPacketRegex.source, 'g');
|
||||||
|
while ((match = redPacketRegexLocal.exec(part)) !== null) {
|
||||||
|
specialTags.push({ tag: match[0], index: match.index });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找转账标签
|
||||||
|
const transferRegexLocal = new RegExp(transferRegex.source, 'g');
|
||||||
|
while ((match = transferRegexLocal.exec(part)) !== null) {
|
||||||
|
specialTags.push({ tag: match[0], index: match.index });
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有特殊标签,直接添加
|
// 如果没有特殊标签,直接添加
|
||||||
if (specialTags.length === 0) {
|
if (specialTags.length === 0) {
|
||||||
result.push(part);
|
result.push(part);
|
||||||
|
|||||||
38
contacts.js
38
contacts.js
@@ -214,7 +214,35 @@ export function openContactSettings(contactIndex) {
|
|||||||
|
|
||||||
document.getElementById('wechat-contact-api-url').value = contact.customApiUrl || '';
|
document.getElementById('wechat-contact-api-url').value = contact.customApiUrl || '';
|
||||||
document.getElementById('wechat-contact-api-key').value = contact.customApiKey || '';
|
document.getElementById('wechat-contact-api-key').value = contact.customApiKey || '';
|
||||||
document.getElementById('wechat-contact-model').value = contact.customModel || '';
|
|
||||||
|
// 填充模型值到下拉列表或输入框
|
||||||
|
const modelSelect = document.getElementById('wechat-contact-model-select');
|
||||||
|
const modelInput = document.getElementById('wechat-contact-model-input');
|
||||||
|
const selectWrapper = document.getElementById('wechat-contact-model-select-wrapper');
|
||||||
|
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
|
||||||
|
const customModel = contact.customModel || '';
|
||||||
|
|
||||||
|
if (customModel && modelSelect) {
|
||||||
|
// 检查是否在下拉列表中存在
|
||||||
|
const existingOption = Array.from(modelSelect.options).find(opt => opt.value === customModel);
|
||||||
|
if (existingOption) {
|
||||||
|
modelSelect.value = customModel;
|
||||||
|
} else {
|
||||||
|
// 添加为新选项并选中
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = customModel;
|
||||||
|
newOption.textContent = customModel;
|
||||||
|
modelSelect.appendChild(newOption);
|
||||||
|
modelSelect.value = customModel;
|
||||||
|
}
|
||||||
|
} else if (modelSelect) {
|
||||||
|
modelSelect.value = '';
|
||||||
|
}
|
||||||
|
if (modelInput) modelInput.value = customModel;
|
||||||
|
|
||||||
|
// 重置为下拉列表模式
|
||||||
|
if (selectWrapper) selectWrapper.style.display = 'flex';
|
||||||
|
if (inputWrapper) inputWrapper.style.display = 'none';
|
||||||
|
|
||||||
// 填充哈基米破限
|
// 填充哈基米破限
|
||||||
const hakimiToggle = document.getElementById('wechat-contact-hakimi-toggle');
|
const hakimiToggle = document.getElementById('wechat-contact-hakimi-toggle');
|
||||||
@@ -238,7 +266,13 @@ export function saveContactSettings() {
|
|||||||
contact.useCustomApi = document.getElementById('wechat-contact-custom-api-toggle')?.classList.contains('on') || false;
|
contact.useCustomApi = document.getElementById('wechat-contact-custom-api-toggle')?.classList.contains('on') || false;
|
||||||
contact.customApiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim() || '';
|
contact.customApiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim() || '';
|
||||||
contact.customApiKey = document.getElementById('wechat-contact-api-key')?.value?.trim() || '';
|
contact.customApiKey = document.getElementById('wechat-contact-api-key')?.value?.trim() || '';
|
||||||
contact.customModel = document.getElementById('wechat-contact-model')?.value?.trim() || '';
|
|
||||||
|
// 获取模型值:优先从输入框获取(手动模式),其次从下拉列表获取
|
||||||
|
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
|
||||||
|
const isManualMode = inputWrapper?.style.display === 'flex';
|
||||||
|
contact.customModel = isManualMode
|
||||||
|
? (document.getElementById('wechat-contact-model-input')?.value?.trim() || '')
|
||||||
|
: (document.getElementById('wechat-contact-model-select')?.value?.trim() || '');
|
||||||
|
|
||||||
// 保存哈基米破限
|
// 保存哈基米破限
|
||||||
contact.customHakimiBreakLimit = document.getElementById('wechat-contact-hakimi-toggle')?.classList.contains('on') || false;
|
contact.customHakimiBreakLimit = document.getElementById('wechat-contact-hakimi-toggle')?.classList.contains('on') || false;
|
||||||
|
|||||||
12
favorites.js
12
favorites.js
@@ -347,7 +347,9 @@ function showNewPersonaModal() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
// 添加到手机容器内,确保居中显示
|
||||||
|
const phoneContainer = document.querySelector('.wechat-phone') || document.body;
|
||||||
|
phoneContainer.appendChild(modal);
|
||||||
|
|
||||||
// 关闭
|
// 关闭
|
||||||
modal.querySelector('#wechat-persona-close').addEventListener('click', () => modal.remove());
|
modal.querySelector('#wechat-persona-close').addEventListener('click', () => modal.remove());
|
||||||
@@ -905,7 +907,9 @@ export function showAddLorebookPanel() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
// 添加到手机容器内,确保居中显示
|
||||||
|
const phoneContainer = document.querySelector('.wechat-phone') || document.body;
|
||||||
|
phoneContainer.appendChild(modal);
|
||||||
|
|
||||||
// 关闭按钮
|
// 关闭按钮
|
||||||
modal.querySelector('#wechat-lorebook-modal-close').addEventListener('click', () => modal.remove());
|
modal.querySelector('#wechat-lorebook-modal-close').addEventListener('click', () => modal.remove());
|
||||||
@@ -953,7 +957,9 @@ export function showAddPersonaPanel() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
// 添加到手机容器内,确保居中显示
|
||||||
|
const phoneContainer = document.querySelector('.wechat-phone') || document.body;
|
||||||
|
phoneContainer.appendChild(modal);
|
||||||
|
|
||||||
// 关闭按钮
|
// 关闭按钮
|
||||||
modal.querySelector('#wechat-persona-modal-close').addEventListener('click', () => modal.remove());
|
modal.querySelector('#wechat-persona-modal-close').addEventListener('click', () => modal.remove());
|
||||||
|
|||||||
505
gift.js
Normal file
505
gift.js
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
/**
|
||||||
|
* 礼物功能模块
|
||||||
|
* 支持发送普通礼物和情趣玩具
|
||||||
|
* 情趣玩具支持配送流程和控制界面
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSettings } from './config.js';
|
||||||
|
import { requestSave } from './save-manager.js';
|
||||||
|
import { showToast, showNotificationBanner } from './toast.js';
|
||||||
|
import { escapeHtml } from './utils.js';
|
||||||
|
import { refreshChatList } from './ui.js';
|
||||||
|
import { currentChatIndex, appendMessage, showTypingIndicator, hideTypingIndicator } from './chat.js';
|
||||||
|
import { callAI } from './ai.js';
|
||||||
|
import { splitAIMessages } from './config.js';
|
||||||
|
|
||||||
|
// SVG图标定义
|
||||||
|
const ICON_GIFT_CHARACTER = `<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 20v-2a8 8 0 0116 0v2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M20 6l-3 3m0-3l3 3" stroke="#ff6b8a" stroke-width="1.5" stroke-linecap="round"/></svg>`;
|
||||||
|
|
||||||
|
const ICON_GIFT_USER = `<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 20v-2a8 8 0 0116 0v2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 6l3 3m0-3l-3 3" stroke="#ff6b8a" stroke-width="1.5" stroke-linecap="round"/></svg>`;
|
||||||
|
|
||||||
|
// 礼物分类数据
|
||||||
|
const GIFT_CATEGORIES = {
|
||||||
|
normal: {
|
||||||
|
name: '普通礼物',
|
||||||
|
icon: `<svg viewBox="0 0 24 24" width="16" height="16"><rect x="3" y="8" width="18" height="13" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 8v13M3 12h18" stroke="currentColor" stroke-width="1.5"/><path d="M12 8c-2-4-6-4-6 0s4 0 6 0c2-4 6-4 6 0s-4 0-6 0" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
|
||||||
|
items: [
|
||||||
|
{ id: 'flower', name: '鲜花', emoji: '💐', desc: '一束美丽的鲜花', hasControl: false },
|
||||||
|
{ id: 'chocolate', name: '巧克力', emoji: '🍫', desc: '精美的巧克力礼盒', hasControl: false },
|
||||||
|
{ id: 'ring', name: '戒指', emoji: '💍', desc: '闪耀的戒指', hasControl: false },
|
||||||
|
{ id: 'necklace', name: '项链', emoji: '📿', desc: '精致的项链', hasControl: false },
|
||||||
|
{ id: 'perfume', name: '香水', emoji: '🧴', desc: '迷人的香水', hasControl: false },
|
||||||
|
{ id: 'teddy', name: '玩偶', emoji: '🧸', desc: '可爱的毛绒玩偶', hasControl: false },
|
||||||
|
{ id: 'cake', name: '蛋糕', emoji: '🎂', desc: '美味的蛋糕', hasControl: false },
|
||||||
|
{ id: 'wine', name: '红酒', emoji: '🍷', desc: '醇香的红酒', hasControl: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
toy: {
|
||||||
|
name: '情趣玩具',
|
||||||
|
icon: `<svg viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
||||||
|
items: [
|
||||||
|
{ id: 'vibrator', name: '跳蛋', emoji: '🥚', desc: '遥控跳蛋', hasControl: true, hasShock: false },
|
||||||
|
{ id: 'massager', name: '按摩棒', emoji: '🌡️', desc: '震动按摩棒', hasControl: true, hasShock: false },
|
||||||
|
{ id: 'breastChain', name: '微电流乳链', emoji: '⚡', desc: '微电流乳链', hasControl: true, hasShock: true },
|
||||||
|
{ id: 'analPlug', name: '肛塞', emoji: '🔌', desc: '震动肛塞', hasControl: true, hasShock: false },
|
||||||
|
{ id: 'cockRing', name: '锁精环', emoji: '💍', desc: '震动锁精环', hasControl: true, hasShock: false },
|
||||||
|
{ id: 'handcuffs', name: '手铐', emoji: '⛓️', desc: '情趣手铐', hasControl: false },
|
||||||
|
{ id: 'blindfold', name: '眼罩', emoji: '🎭', desc: '丝绸眼罩', hasControl: false },
|
||||||
|
{ id: 'whip', name: '皮鞭', emoji: '🏇', desc: '轻柔的皮鞭', hasControl: false },
|
||||||
|
{ id: 'collar', name: '项圈', emoji: '⭕', desc: '精致的项圈', hasControl: false },
|
||||||
|
{ id: 'candle', name: '低温蜡烛', emoji: '🕯️', desc: '安全的低温蜡烛', hasControl: false },
|
||||||
|
{ id: 'lingerie', name: '情趣内衣', emoji: '👙', desc: '性感的情趣内衣', hasControl: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前选中的分类、礼物和目标
|
||||||
|
let currentCategory = 'normal';
|
||||||
|
let selectedGift = null;
|
||||||
|
let selectedTarget = 'character'; // 'character' 送角色 | 'user' 送用户
|
||||||
|
|
||||||
|
// 显示礼物页面
|
||||||
|
export function showGiftPage() {
|
||||||
|
currentCategory = 'normal';
|
||||||
|
selectedGift = null;
|
||||||
|
selectedTarget = 'character';
|
||||||
|
|
||||||
|
const page = document.getElementById('wechat-gift-page');
|
||||||
|
if (page) {
|
||||||
|
page.classList.remove('hidden');
|
||||||
|
renderGiftContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏礼物页面
|
||||||
|
export function hideGiftPage() {
|
||||||
|
const page = document.getElementById('wechat-gift-page');
|
||||||
|
if (page) {
|
||||||
|
page.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染礼物内容
|
||||||
|
function renderGiftContent() {
|
||||||
|
const tabsContainer = document.getElementById('wechat-gift-tabs');
|
||||||
|
const gridContainer = document.getElementById('wechat-gift-grid');
|
||||||
|
const sendBtn = document.getElementById('wechat-gift-send');
|
||||||
|
const targetContainer = document.getElementById('wechat-gift-target');
|
||||||
|
|
||||||
|
if (!tabsContainer || !gridContainer) return;
|
||||||
|
|
||||||
|
// 渲染送礼目标选择(仅情趣玩具显示)
|
||||||
|
if (targetContainer) {
|
||||||
|
if (currentCategory === 'toy') {
|
||||||
|
targetContainer.classList.remove('hidden');
|
||||||
|
targetContainer.innerHTML = `
|
||||||
|
<div class="wechat-gift-target-label">送给谁?</div>
|
||||||
|
<div class="wechat-gift-target-options">
|
||||||
|
<button class="wechat-gift-target-btn ${selectedTarget === 'character' ? 'active' : ''}" data-target="character">
|
||||||
|
${ICON_GIFT_CHARACTER}
|
||||||
|
<span>送角色</span>
|
||||||
|
</button>
|
||||||
|
<button class="wechat-gift-target-btn ${selectedTarget === 'user' ? 'active' : ''}" data-target="user">
|
||||||
|
${ICON_GIFT_USER}
|
||||||
|
<span>送用户</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 绑定目标选择事件
|
||||||
|
targetContainer.querySelectorAll('.wechat-gift-target-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
selectedTarget = btn.dataset.target;
|
||||||
|
renderGiftContent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
targetContainer.classList.add('hidden');
|
||||||
|
targetContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染分类标签
|
||||||
|
let tabsHtml = '';
|
||||||
|
for (const [key, category] of Object.entries(GIFT_CATEGORIES)) {
|
||||||
|
const activeClass = key === currentCategory ? 'active' : '';
|
||||||
|
tabsHtml += `<button class="wechat-gift-tab ${activeClass}" data-category="${key}">${category.icon} ${category.name}</button>`;
|
||||||
|
}
|
||||||
|
tabsContainer.innerHTML = tabsHtml;
|
||||||
|
|
||||||
|
// 绑定标签点击事件
|
||||||
|
tabsContainer.querySelectorAll('.wechat-gift-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
currentCategory = tab.dataset.category;
|
||||||
|
selectedGift = null;
|
||||||
|
renderGiftContent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染礼物网格
|
||||||
|
const category = GIFT_CATEGORIES[currentCategory];
|
||||||
|
let gridHtml = '';
|
||||||
|
category.items.forEach(item => {
|
||||||
|
const selectedClass = selectedGift?.id === item.id ? 'selected' : '';
|
||||||
|
const controlBadge = item.hasControl ? '<span class="wechat-gift-control-badge">可控</span>' : '';
|
||||||
|
gridHtml += `
|
||||||
|
<div class="wechat-gift-item ${selectedClass}" data-gift-id="${item.id}">
|
||||||
|
<span class="wechat-gift-emoji">${item.emoji}</span>
|
||||||
|
<span class="wechat-gift-name">${item.name}</span>
|
||||||
|
${controlBadge}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
gridContainer.innerHTML = gridHtml;
|
||||||
|
|
||||||
|
// 绑定礼物点击事件
|
||||||
|
gridContainer.querySelectorAll('.wechat-gift-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const giftId = item.dataset.giftId;
|
||||||
|
selectedGift = category.items.find(g => g.id === giftId);
|
||||||
|
renderGiftContent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新发送按钮状态
|
||||||
|
if (sendBtn) {
|
||||||
|
if (selectedGift) {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.textContent = `送出 ${selectedGift.name}`;
|
||||||
|
} else {
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.textContent = '请选择礼物';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送礼物
|
||||||
|
export async function sendGift() {
|
||||||
|
if (!selectedGift) {
|
||||||
|
showToast('请选择礼物');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChatIndex < 0) {
|
||||||
|
showToast('请先打开聊天');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
const contact = settings.contacts[currentChatIndex];
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
const gift = selectedGift;
|
||||||
|
const isToy = currentCategory === 'toy';
|
||||||
|
const target = isToy ? selectedTarget : null;
|
||||||
|
|
||||||
|
// 关闭礼物页面
|
||||||
|
hideGiftPage();
|
||||||
|
|
||||||
|
// 获取描述(如果有输入的话)
|
||||||
|
const descInput = document.getElementById('wechat-gift-desc');
|
||||||
|
const customDesc = descInput?.value?.trim() || '';
|
||||||
|
if (descInput) descInput.value = '';
|
||||||
|
|
||||||
|
// 构建礼物消息
|
||||||
|
let giftMessage;
|
||||||
|
if (isToy) {
|
||||||
|
const targetText = target === 'character' ? '送TA' : '送自己';
|
||||||
|
giftMessage = `[情趣礼物] ${gift.emoji} ${gift.name}(${targetText})${customDesc ? ` - ${customDesc}` : ''}`;
|
||||||
|
} else {
|
||||||
|
giftMessage = `[礼物] ${gift.emoji} ${gift.name}${customDesc ? ` - ${customDesc}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到聊天历史
|
||||||
|
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')}`;
|
||||||
|
|
||||||
|
if (!contact.chatHistory) {
|
||||||
|
contact.chatHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const giftRecord = {
|
||||||
|
role: 'user',
|
||||||
|
content: giftMessage,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
isGift: true,
|
||||||
|
giftInfo: {
|
||||||
|
id: gift.id,
|
||||||
|
name: gift.name,
|
||||||
|
emoji: gift.emoji,
|
||||||
|
desc: gift.desc,
|
||||||
|
isToy: isToy,
|
||||||
|
hasControl: gift.hasControl,
|
||||||
|
hasShock: gift.hasShock,
|
||||||
|
target: target,
|
||||||
|
customDesc: customDesc
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
contact.chatHistory.push(giftRecord);
|
||||||
|
|
||||||
|
// 显示礼物消息
|
||||||
|
appendGiftMessage('user', gift, isToy, customDesc, contact, target);
|
||||||
|
|
||||||
|
contact.lastMessage = giftMessage;
|
||||||
|
|
||||||
|
// 如果是可控制的情趣玩具,添加到待配送列表
|
||||||
|
if (isToy && gift.hasControl) {
|
||||||
|
if (!contact.pendingGifts) {
|
||||||
|
contact.pendingGifts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingGift = {
|
||||||
|
giftId: gift.id,
|
||||||
|
giftName: gift.name,
|
||||||
|
giftEmoji: gift.emoji,
|
||||||
|
giftDesc: gift.desc,
|
||||||
|
target: target,
|
||||||
|
hasControl: gift.hasControl,
|
||||||
|
hasShock: gift.hasShock || false,
|
||||||
|
startMessageCount: contact.chatHistory.length,
|
||||||
|
deliveredAt: null,
|
||||||
|
isDelivered: false,
|
||||||
|
isUsing: false,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
contact.pendingGifts.push(pendingGift);
|
||||||
|
|
||||||
|
// 显示配送中弹窗
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotificationBanner('快递', '您选择的商品正在配送中~', 4000);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
|
||||||
|
// 显示打字指示器
|
||||||
|
showTypingIndicator(contact);
|
||||||
|
|
||||||
|
// 构建给AI的提示
|
||||||
|
let aiPrompt;
|
||||||
|
if (isToy && gift.hasControl) {
|
||||||
|
// 可控制的情趣玩具 - 配送中提示词
|
||||||
|
const targetText = target === 'character' ? '你' : '用户';
|
||||||
|
aiPrompt = `[系统提示:用户刚刚购买了一个${gift.name}(${gift.desc}),准备送给${targetText}使用。商品正在配送中,预计很快就会送达。${customDesc ? `用户附言:${customDesc}` : ''}
|
||||||
|
|
||||||
|
请根据你的角色性格,对这个即将到来的礼物做出反应:
|
||||||
|
- 如果是送给你的:可以表现出期待、害羞、紧张、好奇等情绪
|
||||||
|
- 如果是送给用户的:可以表现出好奇、调侃、期待看到用户反应等
|
||||||
|
- 根据你的人设和与用户的关系,反应可以是含蓄的、热情的、或者假装矜持的
|
||||||
|
- 可以询问用户打算怎么用、什么时候用等
|
||||||
|
- 回复不要太短,请展现角色的内心活动和情绪变化
|
||||||
|
|
||||||
|
【重要】只能输出纯文字消息,禁止输出任何特殊格式标签]`;
|
||||||
|
} else if (isToy) {
|
||||||
|
// 不可控制的情趣玩具
|
||||||
|
aiPrompt = `[用户送给你一个情趣礼物:${gift.name}(${gift.desc})${customDesc ? `,附言:${customDesc}` : ''}。请根据你的人设性格对这个礼物做出反应。【重要】只能输出纯文字消息,禁止输出任何特殊格式标签]`;
|
||||||
|
} else {
|
||||||
|
// 普通礼物
|
||||||
|
aiPrompt = `[用户送给你一个礼物:${gift.name}(${gift.desc})${customDesc ? `,附言:${customDesc}` : ''}。请对这个礼物做出自然的反应。【重要】只能输出纯文字消息,禁止输出任何特殊格式标签]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const aiResponse = await callAI(contact, aiPrompt);
|
||||||
|
hideTypingIndicator();
|
||||||
|
|
||||||
|
if (aiResponse) {
|
||||||
|
const aiMessages = splitAIMessages(aiResponse);
|
||||||
|
|
||||||
|
for (const msg of aiMessages) {
|
||||||
|
let reply = msg.trim();
|
||||||
|
// 过滤掉特殊标签
|
||||||
|
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||||
|
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||||
|
// 过滤括号动作描写
|
||||||
|
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||||
|
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
contact.chatHistory.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: reply,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
appendMessage('assistant', reply, contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMsg = aiMessages[aiMessages.length - 1]?.trim()?.replace(/\[.*?\]/g, '').trim();
|
||||||
|
if (lastMsg) {
|
||||||
|
contact.lastMessage = lastMsg.length > 20 ? lastMsg.substring(0, 20) + '...' : lastMsg;
|
||||||
|
}
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideTypingIndicator();
|
||||||
|
console.error('[可乐] 礼物AI回复失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查礼物是否送达(在chat.js的消息发送后调用)
|
||||||
|
export function checkGiftDelivery(contact) {
|
||||||
|
if (!contact || !contact.pendingGifts || contact.pendingGifts.length === 0) return;
|
||||||
|
|
||||||
|
const currentCount = contact.chatHistory?.length || 0;
|
||||||
|
|
||||||
|
for (const gift of contact.pendingGifts) {
|
||||||
|
// 如果正在使用中,跳过
|
||||||
|
if (gift.isUsing) continue;
|
||||||
|
|
||||||
|
// 首次送达检测
|
||||||
|
if (!gift.isDelivered && currentCount >= gift.startMessageCount + 25) {
|
||||||
|
// 标记送达
|
||||||
|
gift.isDelivered = true;
|
||||||
|
gift.deliveredAt = Date.now();
|
||||||
|
gift.lastAskMessageCount = currentCount; // 记录询问时的消息数
|
||||||
|
|
||||||
|
// 显示送达弹窗
|
||||||
|
showNotificationBanner('快递', '您的商品已送达~', 4000);
|
||||||
|
|
||||||
|
// 2秒后弹出询问框
|
||||||
|
setTimeout(() => {
|
||||||
|
showGiftArrivalModal(gift, contact);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
break; // 一次只处理一个
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已送达但点了"稍后",每隔25条消息再次询问
|
||||||
|
if (gift.isDelivered && !gift.isUsing && gift.lastAskMessageCount) {
|
||||||
|
if (currentCount >= gift.lastAskMessageCount + 25) {
|
||||||
|
gift.lastAskMessageCount = currentCount; // 更新询问时的消息数
|
||||||
|
|
||||||
|
// 显示提醒弹窗
|
||||||
|
showNotificationBanner('快递', '您的商品还在等待使用~', 3000);
|
||||||
|
|
||||||
|
// 2秒后再次询问
|
||||||
|
setTimeout(() => {
|
||||||
|
showGiftArrivalModal(gift, contact);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
break; // 一次只处理一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示礼物送达询问弹窗
|
||||||
|
export function showGiftArrivalModal(gift, contact) {
|
||||||
|
const modal = document.getElementById('wechat-gift-arrival-modal');
|
||||||
|
const bodyEl = document.getElementById('wechat-gift-arrival-body');
|
||||||
|
|
||||||
|
if (!modal || !bodyEl) return;
|
||||||
|
|
||||||
|
bodyEl.innerHTML = `您的 <strong>${gift.giftName}</strong> 已送达,您要现在开始玩吗?`;
|
||||||
|
|
||||||
|
// 存储当前礼物信息
|
||||||
|
modal.dataset.giftId = gift.giftId;
|
||||||
|
modal.dataset.giftTimestamp = gift.timestamp;
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
|
||||||
|
// 绑定按钮事件
|
||||||
|
const yesBtn = document.getElementById('wechat-gift-arrival-yes');
|
||||||
|
const noBtn = document.getElementById('wechat-gift-arrival-no');
|
||||||
|
|
||||||
|
const handleYes = async () => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
yesBtn.removeEventListener('click', handleYes);
|
||||||
|
noBtn.removeEventListener('click', handleNo);
|
||||||
|
|
||||||
|
// 打开玩具控制界面
|
||||||
|
const { showToyControlPage } = await import('./toy-control.js');
|
||||||
|
showToyControlPage(gift, contact, currentChatIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNo = () => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
yesBtn.removeEventListener('click', handleYes);
|
||||||
|
noBtn.removeEventListener('click', handleNo);
|
||||||
|
|
||||||
|
// 更新消息计数基准,25条后再次询问
|
||||||
|
const currentCount = contact.chatHistory?.length || 0;
|
||||||
|
gift.lastAskMessageCount = currentCount;
|
||||||
|
requestSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
yesBtn.addEventListener('click', handleYes);
|
||||||
|
noBtn.addEventListener('click', handleNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动打开已送达礼物的控制界面(从心动瞬间历史记录进入)
|
||||||
|
export async function openToyControl(gift, contact, contactIndex) {
|
||||||
|
const { showToyControlPage } = await import('./toy-control.js');
|
||||||
|
showToyControlPage(gift, contact, contactIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加礼物消息到界面
|
||||||
|
export function appendGiftMessage(role, gift, isToy, customDesc, contact, target = null) {
|
||||||
|
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') {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (settings.userAvatar) {
|
||||||
|
avatarContent = `<img src="${settings.userAvatar}" alt="" onerror="this.style.display='none';this.parentElement.textContent='我'">`;
|
||||||
|
} else {
|
||||||
|
avatarContent = '我';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatarContent = contact?.avatar
|
||||||
|
? `<img src="${contact.avatar}" alt="" onerror="this.style.display='none';this.parentElement.innerHTML='${firstChar}'">`
|
||||||
|
: firstChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
const giftTypeClass = isToy ? 'wechat-gift-bubble-toy' : '';
|
||||||
|
let giftTypeLabel = isToy ? '情趣礼物' : '礼物';
|
||||||
|
if (isToy && target) {
|
||||||
|
giftTypeLabel = target === 'character' ? '情趣礼物·送TA' : '情趣礼物·送自己';
|
||||||
|
}
|
||||||
|
|
||||||
|
messageDiv.innerHTML = `
|
||||||
|
<div class="wechat-message-avatar">${avatarContent}</div>
|
||||||
|
<div class="wechat-message-content">
|
||||||
|
<div class="wechat-gift-bubble ${giftTypeClass}">
|
||||||
|
<div class="wechat-gift-bubble-emoji">${gift.emoji}</div>
|
||||||
|
<div class="wechat-gift-bubble-info">
|
||||||
|
<div class="wechat-gift-bubble-name">${escapeHtml(gift.name)}</div>
|
||||||
|
${customDesc ? `<div class="wechat-gift-bubble-desc">${escapeHtml(customDesc)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="wechat-gift-bubble-label">${giftTypeLabel}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageDiv);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取礼物分类数据(供其他模块使用)
|
||||||
|
export function getGiftCategories() {
|
||||||
|
return GIFT_CATEGORIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化礼物事件
|
||||||
|
export function initGiftEvents() {
|
||||||
|
// 返回按钮
|
||||||
|
document.getElementById('wechat-gift-back')?.addEventListener('click', hideGiftPage);
|
||||||
|
|
||||||
|
// 发送按钮
|
||||||
|
document.getElementById('wechat-gift-send')?.addEventListener('click', sendGift);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { getUserAvatarHTML, refreshChatList, getUserPersonaFromST } from './ui.j
|
|||||||
import { getSTChatContext, HAKIMI_HEADER } from './ai.js';
|
import { getSTChatContext, HAKIMI_HEADER } from './ai.js';
|
||||||
import { playMusic as kugouPlayMusic } from './music.js';
|
import { playMusic as kugouPlayMusic } from './music.js';
|
||||||
import { showMessageMenu } from './message-menu.js';
|
import { showMessageMenu } from './message-menu.js';
|
||||||
|
import { showGroupRedPacketDetail } from './group-red-packet.js';
|
||||||
|
|
||||||
// 当前群聊的索引
|
// 当前群聊的索引
|
||||||
export let currentGroupChatIndex = -1;
|
export let currentGroupChatIndex = -1;
|
||||||
@@ -618,6 +619,7 @@ export function openGroupChat(groupIndex) {
|
|||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
} else {
|
} else {
|
||||||
messagesContainer.innerHTML = renderGroupChatHistory(groupChat, members, chatHistory);
|
messagesContainer.innerHTML = renderGroupChatHistory(groupChat, members, chatHistory);
|
||||||
|
bindGroupRedPacketBubbleEvents(messagesContainer);
|
||||||
bindGroupVoiceBubbleEvents(messagesContainer);
|
bindGroupVoiceBubbleEvents(messagesContainer);
|
||||||
bindGroupPhotoBubbleEvents(messagesContainer);
|
bindGroupPhotoBubbleEvents(messagesContainer);
|
||||||
bindGroupMusicCardEvents(messagesContainer);
|
bindGroupMusicCardEvents(messagesContainer);
|
||||||
@@ -871,6 +873,29 @@ function generateGroupMusicCardStatic(musicInfo) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定群红包气泡点击事件
|
||||||
|
function bindGroupRedPacketBubbleEvents(container) {
|
||||||
|
const rpBubbles = container.querySelectorAll('.wechat-group-red-packet-bubble:not([data-bound])');
|
||||||
|
const settings = getSettings();
|
||||||
|
const groupIndex = currentGroupChatIndex;
|
||||||
|
const groupChat = settings.groupChats?.[groupIndex];
|
||||||
|
|
||||||
|
if (!groupChat) return;
|
||||||
|
|
||||||
|
rpBubbles.forEach(bubble => {
|
||||||
|
bubble.setAttribute('data-bound', 'true');
|
||||||
|
const rpId = bubble.dataset.rpId;
|
||||||
|
|
||||||
|
bubble.addEventListener('click', () => {
|
||||||
|
// 从聊天记录中找到红包信息
|
||||||
|
const rpMsg = groupChat.chatHistory?.find(m => m.groupRedPacketInfo?.id === rpId);
|
||||||
|
if (rpMsg && rpMsg.groupRedPacketInfo) {
|
||||||
|
showGroupRedPacketDetail(rpMsg.groupRedPacketInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 绑定群聊语音气泡点击事件(播放动画 + 显示上方菜单,与单聊保持一致)
|
// 绑定群聊语音气泡点击事件(播放动画 + 显示上方菜单,与单聊保持一致)
|
||||||
function bindGroupVoiceBubbleEvents(container) {
|
function bindGroupVoiceBubbleEvents(container) {
|
||||||
const voiceBubbles = container.querySelectorAll('.wechat-voice-bubble:not([data-bound])');
|
const voiceBubbles = container.querySelectorAll('.wechat-voice-bubble:not([data-bound])');
|
||||||
@@ -959,17 +984,7 @@ export function appendGroupMessage(role, content, characterName, characterId, is
|
|||||||
if (isSticker) {
|
if (isSticker) {
|
||||||
bubbleContent = `<div class="wechat-sticker-bubble"><img src="${content}" alt="表情" class="wechat-sticker-img"></div>`;
|
bubbleContent = `<div class="wechat-sticker-bubble"><img src="${content}" alt="表情" class="wechat-sticker-img"></div>`;
|
||||||
} else if (isVoice) {
|
} else if (isVoice) {
|
||||||
const seconds = calculateVoiceDuration(content);
|
bubbleContent = generateGroupVoiceBubbleStatic(content, true);
|
||||||
const width = Math.min(50 + seconds * 3, 180);
|
|
||||||
const voiceId = 'voice_' + Math.random().toString(36).substring(2, 9);
|
|
||||||
// 用户消息:波形在左,秒数在右
|
|
||||||
bubbleContent = `
|
|
||||||
<div class="wechat-voice-bubble self" style="width: ${width}px" data-voice-id="${voiceId}">
|
|
||||||
<span class="wechat-voice-waves"><svg viewBox="0 0 24 24" width="20" height="20"><path d="M3 12h2v4H3zM7 8h2v8H7zm4 4h2v6h-2zm4-6h2v10h-2z" fill="currentColor"/></svg></span>
|
|
||||||
<span class="wechat-voice-duration">${seconds}</span>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-voice-text hidden" id="${voiceId}">${escapeHtml(content)}</div>
|
|
||||||
`;
|
|
||||||
} else {
|
} else {
|
||||||
const processedContent = parseMemeTag(content);
|
const processedContent = parseMemeTag(content);
|
||||||
const hasMeme = processedContent !== content;
|
const hasMeme = processedContent !== content;
|
||||||
@@ -1003,17 +1018,7 @@ export function appendGroupMessage(role, content, characterName, characterId, is
|
|||||||
if (isSticker) {
|
if (isSticker) {
|
||||||
bubbleContent = `<div class="wechat-sticker-bubble"><img src="${content}" alt="表情" class="wechat-sticker-img"></div>`;
|
bubbleContent = `<div class="wechat-sticker-bubble"><img src="${content}" alt="表情" class="wechat-sticker-img"></div>`;
|
||||||
} else if (isVoice) {
|
} else if (isVoice) {
|
||||||
const seconds = calculateVoiceDuration(content);
|
bubbleContent = generateGroupVoiceBubbleStatic(content, false);
|
||||||
const width = Math.min(50 + seconds * 3, 180);
|
|
||||||
const voiceId = 'voice_' + Math.random().toString(36).substring(2, 9);
|
|
||||||
// 角色消息:秒数在左,波形在右
|
|
||||||
bubbleContent = `
|
|
||||||
<div class="wechat-voice-bubble" style="width: ${width}px" data-voice-id="${voiceId}">
|
|
||||||
<span class="wechat-voice-duration">${seconds}</span>
|
|
||||||
<span class="wechat-voice-waves"><svg viewBox="0 0 24 24" width="20" height="20"><path d="M19 12h2v4h-2zm-4-4h2v8h-2zm-4 4h2v6h-2zm-4-6h2v10H7z" fill="currentColor"/></svg></span>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-voice-text hidden" id="${voiceId}">${escapeHtml(content)}</div>
|
|
||||||
`;
|
|
||||||
} else {
|
} else {
|
||||||
const processedContent = parseMemeTag(content);
|
const processedContent = parseMemeTag(content);
|
||||||
const hasMeme = processedContent !== content;
|
const hasMeme = processedContent !== content;
|
||||||
|
|||||||
@@ -529,8 +529,12 @@ export function handleGroupPasswordInput(key) {
|
|||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const correctPassword = settings.paymentPassword || '666666';
|
const correctPassword = settings.paymentPassword || '666666';
|
||||||
if (password === correctPassword) {
|
if (password === correctPassword) {
|
||||||
|
// 先执行操作,再隐藏弹窗(hideGroupPasswordModal 会清空 pendingGroupAction)
|
||||||
|
const action = pendingGroupAction;
|
||||||
hideGroupPasswordModal();
|
hideGroupPasswordModal();
|
||||||
executeGroupAction();
|
if (action) {
|
||||||
|
executeGroupActionWithData(action);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('密码错误', 'info');
|
showToast('密码错误', 'info');
|
||||||
modal.dataset.password = '';
|
modal.dataset.password = '';
|
||||||
@@ -540,12 +544,12 @@ export function handleGroupPasswordInput(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行群聊操作
|
* 执行群聊操作(带参数版本)
|
||||||
*/
|
*/
|
||||||
async function executeGroupAction() {
|
async function executeGroupActionWithData(action) {
|
||||||
if (!pendingGroupAction) return;
|
if (!action) return;
|
||||||
|
|
||||||
const actionType = pendingGroupAction.type;
|
const actionType = action.type;
|
||||||
|
|
||||||
if (actionType === 'random-rp') {
|
if (actionType === 'random-rp') {
|
||||||
await sendGroupRandomRedPacket();
|
await sendGroupRandomRedPacket();
|
||||||
@@ -554,18 +558,63 @@ async function executeGroupAction() {
|
|||||||
} else if (actionType === 'transfer') {
|
} else if (actionType === 'transfer') {
|
||||||
await sendGroupTransfer();
|
await sendGroupTransfer();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行群聊操作(保留原函数兼容性)
|
||||||
|
*/
|
||||||
|
async function executeGroupAction() {
|
||||||
|
await executeGroupActionWithData(pendingGroupAction);
|
||||||
pendingGroupAction = null;
|
pendingGroupAction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 发送群红包 ============
|
// ============ 发送群红包 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新拼手气红包总金额显示
|
||||||
|
*/
|
||||||
|
function updateGroupRandomRedPacketTotal() {
|
||||||
|
const amountInput = document.getElementById('wechat-group-rp-amount-input');
|
||||||
|
const amount = parseFloat(amountInput?.value) || 0;
|
||||||
|
const totalEl = document.getElementById('wechat-group-rp-total-display');
|
||||||
|
if (totalEl) {
|
||||||
|
totalEl.textContent = '¥' + amount.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定成员红包总金额显示
|
||||||
|
*/
|
||||||
|
function updateGroupDesignatedRedPacketTotal() {
|
||||||
|
const amountInput = document.getElementById('wechat-group-designated-amount-input');
|
||||||
|
const amount = parseFloat(amountInput?.value) || 0;
|
||||||
|
const count = groupRedPacketSelectedMembers.length;
|
||||||
|
const totalEl = document.getElementById('wechat-group-designated-total-display');
|
||||||
|
if (totalEl) {
|
||||||
|
totalEl.textContent = '¥' + (amount * count).toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新群转账总金额显示
|
||||||
|
*/
|
||||||
|
function updateGroupTransferAmountTotal() {
|
||||||
|
const amountInput = document.getElementById('wechat-group-transfer-amount-input');
|
||||||
|
const amount = parseFloat(amountInput?.value) || 0;
|
||||||
|
const displayEl = document.getElementById('wechat-group-transfer-amount-display');
|
||||||
|
if (displayEl) {
|
||||||
|
displayEl.textContent = '¥' + amount.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交拼手气红包(显示密码输入)
|
* 提交拼手气红包(显示密码输入)
|
||||||
*/
|
*/
|
||||||
export function submitGroupRandomRedPacket() {
|
export function submitGroupRandomRedPacket() {
|
||||||
const amount = parseFloat(groupRedPacketAmount) || 0;
|
const amountInput = document.getElementById('wechat-group-rp-amount-input');
|
||||||
const count = parseInt(groupRedPacketCount) || 0;
|
const countInput = document.getElementById('wechat-group-rp-count-input');
|
||||||
|
const amount = parseFloat(amountInput?.value) || 0;
|
||||||
|
const count = parseInt(countInput?.value) || 0;
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
showToast('请输入红包金额', 'info');
|
showToast('请输入红包金额', 'info');
|
||||||
@@ -584,6 +633,10 @@ export function submitGroupRandomRedPacket() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存到状态变量供后续使用
|
||||||
|
groupRedPacketAmount = amount.toString();
|
||||||
|
groupRedPacketCount = count.toString();
|
||||||
|
|
||||||
// 获取祝福语
|
// 获取祝福语
|
||||||
const messageInput = document.getElementById('wechat-group-rp-message');
|
const messageInput = document.getElementById('wechat-group-rp-message');
|
||||||
if (messageInput && messageInput.value.trim()) {
|
if (messageInput && messageInput.value.trim()) {
|
||||||
@@ -658,7 +711,8 @@ async function sendGroupRandomRedPacket() {
|
|||||||
* 提交指定成员红包(显示密码输入)
|
* 提交指定成员红包(显示密码输入)
|
||||||
*/
|
*/
|
||||||
export function submitGroupDesignatedRedPacket() {
|
export function submitGroupDesignatedRedPacket() {
|
||||||
const amount = parseFloat(groupRedPacketAmount) || 0;
|
const amountInput = document.getElementById('wechat-group-designated-amount-input');
|
||||||
|
const amount = parseFloat(amountInput?.value) || 0;
|
||||||
const count = groupRedPacketSelectedMembers.length;
|
const count = groupRedPacketSelectedMembers.length;
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
@@ -680,6 +734,9 @@ export function submitGroupDesignatedRedPacket() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存到状态变量供后续使用
|
||||||
|
groupRedPacketAmount = amount.toString();
|
||||||
|
|
||||||
// 获取祝福语
|
// 获取祝福语
|
||||||
const messageInput = document.getElementById('wechat-group-designated-message');
|
const messageInput = document.getElementById('wechat-group-designated-message');
|
||||||
if (messageInput && messageInput.value.trim()) {
|
if (messageInput && messageInput.value.trim()) {
|
||||||
@@ -765,7 +822,8 @@ async function sendGroupDesignatedRedPacket() {
|
|||||||
* 提交群转账(显示密码输入)
|
* 提交群转账(显示密码输入)
|
||||||
*/
|
*/
|
||||||
export function submitGroupTransfer() {
|
export function submitGroupTransfer() {
|
||||||
const amount = parseFloat(groupTransferAmount) || 0;
|
const amountInput = document.getElementById('wechat-group-transfer-amount-input');
|
||||||
|
const amount = parseFloat(amountInput?.value) || 0;
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
showToast('请输入转账金额', 'info');
|
showToast('请输入转账金额', 'info');
|
||||||
@@ -776,6 +834,9 @@ export function submitGroupTransfer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存到状态变量供后续使用
|
||||||
|
groupTransferAmount = amount.toString();
|
||||||
|
|
||||||
// 获取转账说明
|
// 获取转账说明
|
||||||
const descInput = document.getElementById('wechat-group-transfer-description');
|
const descInput = document.getElementById('wechat-group-transfer-description');
|
||||||
if (descInput && descInput.value.trim()) {
|
if (descInput && descInput.value.trim()) {
|
||||||
@@ -878,9 +939,6 @@ async function processAIClaimGroupRedPacket(rpInfo, groupChat, members) {
|
|||||||
// 更新界面
|
// 更新界面
|
||||||
updateGroupRedPacketBubbleStatus(rpInfo.id);
|
updateGroupRedPacketBubbleStatus(rpInfo.id);
|
||||||
|
|
||||||
// 显示领取提示
|
|
||||||
appendGroupRedPacketClaimNotice(member.name, settings.userName || 'User');
|
|
||||||
|
|
||||||
// AI 感谢消息
|
// AI 感谢消息
|
||||||
await sleep(800 + Math.random() * 500);
|
await sleep(800 + Math.random() * 500);
|
||||||
showGroupTypingIndicator(member.name, member.id);
|
showGroupTypingIndicator(member.name, member.id);
|
||||||
@@ -930,9 +988,6 @@ async function processAIClaimGroupRedPacket(rpInfo, groupChat, members) {
|
|||||||
// 更新界面
|
// 更新界面
|
||||||
updateGroupRedPacketBubbleStatus(rpInfo.id);
|
updateGroupRedPacketBubbleStatus(rpInfo.id);
|
||||||
|
|
||||||
// 显示领取提示
|
|
||||||
appendGroupRedPacketClaimNotice(member.name, settings.userName || 'User');
|
|
||||||
|
|
||||||
// AI 感谢消息
|
// AI 感谢消息
|
||||||
await sleep(800 + Math.random() * 500);
|
await sleep(800 + Math.random() * 500);
|
||||||
showGroupTypingIndicator(member.name, member.id);
|
showGroupTypingIndicator(member.name, member.id);
|
||||||
@@ -1027,7 +1082,7 @@ async function generateAIThankMessage(member, rpInfo, claimAmount) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const systemPrompt = buildSystemPrompt(member, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
|
const systemPrompt = buildSystemPrompt(member, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
|
||||||
const userPrompt = `用户给群里发了一个${rpInfo.totalAmount}元的红包,祝福语是"${rpInfo.message}"。你抢到了${claimAmount.toFixed(2)}元,请自然地表示感谢,不要使用任何特殊格式标签。回复要简短自然(10字以内)。`;
|
const userPrompt = `用户给群里发了一个${rpInfo.totalAmount}元的红包,祝福语是"${rpInfo.message}"。你抢到了${claimAmount.toFixed(2)}元。请根据你的性格自然地回应这件事,不要使用任何特殊格式标签,直接输出对话内容。`;
|
||||||
|
|
||||||
const chatUrl = member.customApiUrl.replace(/\/+$/, '') + '/chat/completions';
|
const chatUrl = member.customApiUrl.replace(/\/+$/, '') + '/chat/completions';
|
||||||
const headers = { 'Content-Type': 'application/json' };
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
@@ -1078,8 +1133,8 @@ async function generateAITransferThankMessage(member, tfInfo) {
|
|||||||
try {
|
try {
|
||||||
const systemPrompt = buildSystemPrompt(member, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
|
const systemPrompt = buildSystemPrompt(member, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
|
||||||
const userPrompt = tfInfo.description
|
const userPrompt = tfInfo.description
|
||||||
? `用户给你转账了${tfInfo.amount}元,备注是"${tfInfo.description}",请自然地表示感谢,不要使用任何特殊格式标签。回复要简短自然(10字以内)。`
|
? `用户给你转账了${tfInfo.amount}元,备注是"${tfInfo.description}"。请根据你的性格自然地回应这件事,不要使用任何特殊格式标签,直接输出对话内容。`
|
||||||
: `用户给你转账了${tfInfo.amount}元,请自然地表示感谢,不要使用任何特殊格式标签。回复要简短自然(10字以内)。`;
|
: `用户给你转账了${tfInfo.amount}元。请根据你的性格自然地回应这件事,不要使用任何特殊格式标签,直接输出对话内容。`;
|
||||||
|
|
||||||
const chatUrl = member.customApiUrl.replace(/\/+$/, '') + '/chat/completions';
|
const chatUrl = member.customApiUrl.replace(/\/+$/, '') + '/chat/completions';
|
||||||
const headers = { 'Content-Type': 'application/json' };
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
@@ -1439,13 +1494,13 @@ export function createGroupRedPacketPages() {
|
|||||||
<span class="wechat-page-title">发拼手气红包</span>
|
<span class="wechat-page-title">发拼手气红包</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-form">
|
<div class="wechat-group-rp-form">
|
||||||
<div class="wechat-group-rp-row" id="wechat-group-rp-amount-row">
|
<div class="wechat-group-rp-row">
|
||||||
<span class="wechat-group-rp-label">总金额</span>
|
<span class="wechat-group-rp-label">总金额</span>
|
||||||
<span class="wechat-group-rp-value">¥<span id="wechat-group-rp-amount-value">0.00</span></span>
|
<span class="wechat-group-rp-value">¥<input type="number" id="wechat-group-rp-amount-input" class="wechat-group-rp-number-input" placeholder="0.00" step="0.01" min="0.01" max="200"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-row" id="wechat-group-rp-count-row">
|
<div class="wechat-group-rp-row">
|
||||||
<span class="wechat-group-rp-label">红包个数</span>
|
<span class="wechat-group-rp-label">红包个数</span>
|
||||||
<span class="wechat-group-rp-value"><span id="wechat-group-rp-count-value">0</span>个</span>
|
<span class="wechat-group-rp-value"><input type="number" id="wechat-group-rp-count-input" class="wechat-group-rp-number-input" placeholder="0" step="1" min="1" max="99">个</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-row">
|
<div class="wechat-group-rp-row">
|
||||||
<input type="text" class="wechat-group-rp-message-input" id="wechat-group-rp-message" placeholder="恭喜发财,大吉大利" maxlength="20">
|
<input type="text" class="wechat-group-rp-message-input" id="wechat-group-rp-message" placeholder="恭喜发财,大吉大利" maxlength="20">
|
||||||
@@ -1455,31 +1510,6 @@ export function createGroupRedPacketPages() {
|
|||||||
</div>
|
</div>
|
||||||
<button class="wechat-btn wechat-btn-primary wechat-group-rp-submit" id="wechat-group-random-rp-submit">塞钱进红包</button>
|
<button class="wechat-btn wechat-btn-primary wechat-group-rp-submit" id="wechat-group-random-rp-submit">塞钱进红包</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-rp-keyboard hidden" id="wechat-group-rp-keyboard">
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="1">1</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="2">2</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="3">3</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="4">4</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="5">5</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="6">6</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="7">7</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="8">8</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="9">9</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key=".">.</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="0">0</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="backspace">⌫</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key wechat-rp-keyboard-confirm" data-key="confirm">确定</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 指定成员红包页面 -->
|
<!-- 指定成员红包页面 -->
|
||||||
@@ -1496,9 +1526,9 @@ export function createGroupRedPacketPages() {
|
|||||||
<div class="wechat-group-designated-member-list" id="wechat-group-designated-member-list"></div>
|
<div class="wechat-group-designated-member-list" id="wechat-group-designated-member-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-form">
|
<div class="wechat-group-rp-form">
|
||||||
<div class="wechat-group-rp-row" id="wechat-group-designated-amount-row">
|
<div class="wechat-group-rp-row">
|
||||||
<span class="wechat-group-rp-label">每人金额</span>
|
<span class="wechat-group-rp-label">每人金额</span>
|
||||||
<span class="wechat-group-rp-value">¥<span id="wechat-group-designated-amount-value">0.00</span></span>
|
<span class="wechat-group-rp-value">¥<input type="number" id="wechat-group-designated-amount-input" class="wechat-group-rp-number-input" placeholder="0.00" step="0.01" min="0.01" max="200"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-row">
|
<div class="wechat-group-rp-row">
|
||||||
<input type="text" class="wechat-group-rp-message-input" id="wechat-group-designated-message" placeholder="恭喜发财,大吉大利" maxlength="20">
|
<input type="text" class="wechat-group-rp-message-input" id="wechat-group-designated-message" placeholder="恭喜发财,大吉大利" maxlength="20">
|
||||||
@@ -1509,31 +1539,6 @@ export function createGroupRedPacketPages() {
|
|||||||
<button class="wechat-btn wechat-btn-primary wechat-group-rp-submit" id="wechat-group-designated-rp-submit">塞钱进红包</button>
|
<button class="wechat-btn wechat-btn-primary wechat-group-rp-submit" id="wechat-group-designated-rp-submit">塞钱进红包</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-rp-keyboard hidden" id="wechat-group-designated-keyboard">
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="1">1</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="2">2</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="3">3</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="4">4</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="5">5</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="6">6</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="7">7</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="8">8</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="9">9</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key=".">.</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="0">0</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="backspace">⌫</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key wechat-rp-keyboard-confirm" data-key="confirm">确定</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群转账成员选择页面 -->
|
<!-- 群转账成员选择页面 -->
|
||||||
@@ -1556,9 +1561,9 @@ export function createGroupRedPacketPages() {
|
|||||||
<span class="wechat-page-title" id="wechat-group-transfer-target-name">转账</span>
|
<span class="wechat-page-title" id="wechat-group-transfer-target-name">转账</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-form">
|
<div class="wechat-group-rp-form">
|
||||||
<div class="wechat-group-rp-row" id="wechat-group-transfer-amount-row">
|
<div class="wechat-group-rp-row">
|
||||||
<span class="wechat-group-rp-label">转账金额</span>
|
<span class="wechat-group-rp-label">转账金额</span>
|
||||||
<span class="wechat-group-rp-value">¥<span id="wechat-group-transfer-amount-value">0.00</span></span>
|
<span class="wechat-group-rp-value">¥<input type="number" id="wechat-group-transfer-amount-input" class="wechat-group-rp-number-input" placeholder="0.00" step="0.01" min="0.01"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-group-rp-row">
|
<div class="wechat-group-rp-row">
|
||||||
<input type="text" class="wechat-group-rp-message-input" id="wechat-group-transfer-description" placeholder="添加转账说明(可选)" maxlength="20">
|
<input type="text" class="wechat-group-rp-message-input" id="wechat-group-transfer-description" placeholder="添加转账说明(可选)" maxlength="20">
|
||||||
@@ -1566,32 +1571,7 @@ export function createGroupRedPacketPages() {
|
|||||||
<div class="wechat-group-rp-total">
|
<div class="wechat-group-rp-total">
|
||||||
<span id="wechat-group-transfer-amount-display">¥0.00</span>
|
<span id="wechat-group-transfer-amount-display">¥0.00</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="wechat-btn wechat-btn-primary wechat-group-rp-submit" id="wechat-group-transfer-submit">转账</button>
|
<button class="wechat-btn wechat-group-transfer-submit-btn" id="wechat-group-transfer-submit">转账</button>
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard hidden" id="wechat-group-transfer-keyboard">
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="1">1</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="2">2</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="3">3</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="4">4</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="5">5</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="6">6</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="7">7</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="8">8</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="9">9</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key=".">.</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="0">0</button>
|
|
||||||
<button class="wechat-rp-keyboard-key" data-key="backspace">⌫</button>
|
|
||||||
</div>
|
|
||||||
<div class="wechat-rp-keyboard-row">
|
|
||||||
<button class="wechat-rp-keyboard-key wechat-rp-keyboard-confirm" data-key="confirm">确定</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1670,43 +1650,28 @@ function bindGroupRedPacketEvents() {
|
|||||||
|
|
||||||
// 拼手气红包页面
|
// 拼手气红包页面
|
||||||
document.getElementById('wechat-group-random-rp-back')?.addEventListener('click', hideGroupRandomRedPacketPage);
|
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.getElementById('wechat-group-random-rp-submit')?.addEventListener('click', submitGroupRandomRedPacket);
|
||||||
|
|
||||||
// 拼手气红包键盘
|
// 拼手气红包金额输入监听
|
||||||
document.querySelectorAll('#wechat-group-rp-keyboard .wechat-rp-keyboard-key').forEach(key => {
|
document.getElementById('wechat-group-rp-amount-input')?.addEventListener('input', updateGroupRandomRedPacketTotal);
|
||||||
key.addEventListener('click', () => {
|
document.getElementById('wechat-group-rp-count-input')?.addEventListener('input', updateGroupRandomRedPacketTotal);
|
||||||
handleGroupKeyboardInput(key.dataset.key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 指定成员红包页面
|
// 指定成员红包页面
|
||||||
document.getElementById('wechat-group-designated-rp-back')?.addEventListener('click', hideGroupDesignatedRedPacketPage);
|
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.getElementById('wechat-group-designated-rp-submit')?.addEventListener('click', submitGroupDesignatedRedPacket);
|
||||||
|
|
||||||
// 指定成员红包键盘
|
// 指定成员红包金额输入监听
|
||||||
document.querySelectorAll('#wechat-group-designated-keyboard .wechat-rp-keyboard-key').forEach(key => {
|
document.getElementById('wechat-group-designated-amount-input')?.addEventListener('input', updateGroupDesignatedRedPacketTotal);
|
||||||
key.addEventListener('click', () => {
|
|
||||||
handleGroupKeyboardInput(key.dataset.key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 群转账成员选择页面
|
// 群转账成员选择页面
|
||||||
document.getElementById('wechat-group-transfer-select-back')?.addEventListener('click', hideGroupTransferSelectPage);
|
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-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.getElementById('wechat-group-transfer-submit')?.addEventListener('click', submitGroupTransfer);
|
||||||
|
|
||||||
// 群转账键盘
|
// 群转账金额输入监听
|
||||||
document.querySelectorAll('#wechat-group-transfer-keyboard .wechat-rp-keyboard-key').forEach(key => {
|
document.getElementById('wechat-group-transfer-amount-input')?.addEventListener('input', updateGroupTransferAmountTotal);
|
||||||
key.addEventListener('click', () => {
|
|
||||||
handleGroupKeyboardInput(key.dataset.key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 群红包详情页面
|
// 群红包详情页面
|
||||||
document.getElementById('wechat-group-rp-detail-back')?.addEventListener('click', hideGroupRedPacketDetail);
|
document.getElementById('wechat-group-rp-detail-back')?.addEventListener('click', hideGroupRedPacketDetail);
|
||||||
|
|||||||
@@ -311,3 +311,65 @@ export function initErrorCapture() {
|
|||||||
// 不再全局捕获 console.error,避免记录酒馆其他错误
|
// 不再全局捕获 console.error,避免记录酒馆其他错误
|
||||||
console.log('[可乐不加冰] 错误日志系统已初始化');
|
console.log('[可乐不加冰] 错误日志系统已初始化');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染心动瞬间历史记录
|
||||||
|
export function renderToyHistory(contact) {
|
||||||
|
const contentEl = document.getElementById('wechat-history-content');
|
||||||
|
if (!contentEl) return;
|
||||||
|
|
||||||
|
const toyHistory = contact?.toyHistory || [];
|
||||||
|
|
||||||
|
if (toyHistory.length === 0) {
|
||||||
|
contentEl.innerHTML = `
|
||||||
|
<div class="wechat-history-empty">
|
||||||
|
<div class="wechat-history-empty-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="48" height="48" style="color: #ff6b8a; opacity: 0.5;">
|
||||||
|
<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>暂无心动瞬间记录</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间倒序排列
|
||||||
|
const sortedHistory = [...toyHistory].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
||||||
|
|
||||||
|
contentEl.innerHTML = sortedHistory.map((session, sortedIdx) => {
|
||||||
|
const targetText = session.target === 'character' ? 'TA在用' : '你在用';
|
||||||
|
const messages = session.messages || [];
|
||||||
|
const previewMessages = messages.slice(0, 5); // 只显示前5条消息预览
|
||||||
|
const originalIndex = toyHistory.indexOf(session);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="wechat-toy-history-card" data-index="${originalIndex}">
|
||||||
|
<div class="wechat-toy-history-card-header">
|
||||||
|
<div class="wechat-toy-history-card-gift">
|
||||||
|
<span class="wechat-toy-history-card-gift-emoji">${escapeHtml(session.gift?.emoji || '')}</span>
|
||||||
|
<span class="wechat-toy-history-card-gift-name">${escapeHtml(session.gift?.name || '未知玩具')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-toy-history-card-actions">
|
||||||
|
<span class="wechat-toy-history-card-target">${targetText}</span>
|
||||||
|
<button class="wechat-history-delete-btn" data-tab="toy" data-index="${originalIndex}" title="删除">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-toy-history-card-meta">
|
||||||
|
<span>${escapeHtml(session.time || '未知时间')}</span>
|
||||||
|
<span>时长 ${escapeHtml(session.duration || '00:00')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-toy-history-card-messages">
|
||||||
|
${previewMessages.length === 0 ? '<div style="color: #999; text-align: center;">暂无对话记录</div>' :
|
||||||
|
previewMessages.map(msg => `
|
||||||
|
<div class="wechat-toy-history-msg">
|
||||||
|
<span class="wechat-toy-history-msg-sender ${msg.role === 'user' ? 'user' : 'ai'}">${msg.role === 'user' ? '你' : 'TA'}:</span>
|
||||||
|
<span class="wechat-toy-history-msg-content">${escapeHtml((msg.content || '').substring(0, 50))}${(msg.content?.length || 0) > 50 ? '...' : ''}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
}
|
||||||
|
${messages.length > 5 ? `<div style="color: #ff6b8a; font-size: 12px; text-align: center; margin-top: 8px;">还有 ${messages.length - 5} 条消息...</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|||||||
317
main.js
317
main.js
@@ -13,7 +13,7 @@ import { showToast } from './toast.js';
|
|||||||
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
|
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
|
||||||
|
|
||||||
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
|
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
|
||||||
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat } from './chat.js';
|
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
|
||||||
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
|
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
|
||||||
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats } from './summary.js';
|
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats } from './summary.js';
|
||||||
import { fetchModelListFromApi } from './ai.js';
|
import { fetchModelListFromApi } from './ai.js';
|
||||||
@@ -28,14 +28,213 @@ import { initFuncPanel, toggleFuncPanel, hideFuncPanel, showExpandVoice, closeEx
|
|||||||
import { initEmojiPanel, toggleEmojiPanel, hideEmojiPanel } from './emoji-panel.js';
|
import { initEmojiPanel, toggleEmojiPanel, hideEmojiPanel } from './emoji-panel.js';
|
||||||
import { injectAuthorNote, setupMessageObserver, addExtensionButton } from './st-integration.js';
|
import { injectAuthorNote, setupMessageObserver, addExtensionButton } from './st-integration.js';
|
||||||
import { getCurrentTime } from './utils.js';
|
import { getCurrentTime } from './utils.js';
|
||||||
import { refreshHistoryList, refreshLogsList, clearErrorLogs, initErrorCapture, addErrorLog } from './history-logs.js';
|
import { refreshHistoryList, refreshLogsList, clearErrorLogs, initErrorCapture, addErrorLog, renderToyHistory } from './history-logs.js';
|
||||||
import { initChatBackground } from './chat-background.js';
|
import { initChatBackground } from './chat-background.js';
|
||||||
import { initMoments, openMomentsPage, clearContactMoments } from './moments.js';
|
import { initMoments, openMomentsPage, clearContactMoments } from './moments.js';
|
||||||
import { initRedPacketEvents } from './red-packet.js';
|
import { initRedPacketEvents } from './red-packet.js';
|
||||||
import { initTransferEvents } from './transfer.js';
|
import { initTransferEvents } from './transfer.js';
|
||||||
import { initGroupRedPacket } from './group-red-packet.js';
|
import { initGroupRedPacket } from './group-red-packet.js';
|
||||||
|
import { initGiftEvents } from './gift.js';
|
||||||
import { initCropper } from './cropper.js';
|
import { initCropper } from './cropper.js';
|
||||||
|
|
||||||
|
// ========== 历史记录功能 ==========
|
||||||
|
let currentHistoryTab = 'listen';
|
||||||
|
let currentHistoryContactIndex = -1;
|
||||||
|
|
||||||
|
function openHistoryPage(contactIndex) {
|
||||||
|
const settings = getSettings();
|
||||||
|
const contact = settings.contacts?.[contactIndex];
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
currentHistoryContactIndex = contactIndex;
|
||||||
|
currentHistoryTab = 'listen';
|
||||||
|
|
||||||
|
const page = document.getElementById('wechat-history-page');
|
||||||
|
if (page) {
|
||||||
|
page.classList.remove('hidden');
|
||||||
|
// 重置标签状态
|
||||||
|
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === 'listen');
|
||||||
|
});
|
||||||
|
renderHistoryContent(contact, 'listen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeHistoryPage() {
|
||||||
|
const page = document.getElementById('wechat-history-page');
|
||||||
|
if (page) {
|
||||||
|
page.classList.add('hidden');
|
||||||
|
}
|
||||||
|
currentHistoryContactIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteHistoryRecord(tabType, index) {
|
||||||
|
const settings = getSettings();
|
||||||
|
const contact = settings.contacts?.[currentHistoryContactIndex];
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
if (tabType === 'listen') {
|
||||||
|
if (contact.listenHistory && contact.listenHistory[index]) {
|
||||||
|
contact.listenHistory.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else if (tabType === 'voice' || tabType === 'video') {
|
||||||
|
// 从 callHistory 中找到并删除对应类型的记录
|
||||||
|
const callHistory = contact.callHistory || [];
|
||||||
|
const typeRecords = callHistory.filter(r => r.type === tabType);
|
||||||
|
if (typeRecords[index]) {
|
||||||
|
const originalIndex = callHistory.indexOf(typeRecords[index]);
|
||||||
|
if (originalIndex >= 0) {
|
||||||
|
contact.callHistory.splice(originalIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (tabType === 'toy') {
|
||||||
|
if (contact.toyHistory && contact.toyHistory[index]) {
|
||||||
|
contact.toyHistory.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
renderHistoryContent(contact, tabType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchHistoryTab(tabType) {
|
||||||
|
currentHistoryTab = tabType;
|
||||||
|
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === tabType);
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
const contact = settings.contacts?.[currentHistoryContactIndex];
|
||||||
|
if (contact) {
|
||||||
|
renderHistoryContent(contact, tabType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHistoryContent(contact, tabType) {
|
||||||
|
const contentEl = document.getElementById('wechat-history-content');
|
||||||
|
if (!contentEl) return;
|
||||||
|
|
||||||
|
// 心动瞬间使用专门的渲染函数
|
||||||
|
if (tabType === 'toy') {
|
||||||
|
renderToyHistory(contact);
|
||||||
|
// 绑定心动瞬间的删除按钮事件
|
||||||
|
contentEl.querySelectorAll('.wechat-history-delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const index = parseInt(btn.dataset.index);
|
||||||
|
deleteHistoryRecord('toy', index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = window.SillyTavern?.getContext?.() || {};
|
||||||
|
const userName = context.name1 || '用户';
|
||||||
|
|
||||||
|
let records = [];
|
||||||
|
if (tabType === 'listen') {
|
||||||
|
records = contact.listenHistory || [];
|
||||||
|
} else {
|
||||||
|
// 从 callHistory 中筛选 voice 或 video
|
||||||
|
const callHistory = contact.callHistory || [];
|
||||||
|
records = callHistory.filter(r => r.type === tabType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length === 0) {
|
||||||
|
const emptyText = tabType === 'listen' ? '暂无一起听记录' :
|
||||||
|
tabType === 'voice' ? '暂无语音通话记录' : '暂无视频通话记录';
|
||||||
|
contentEl.innerHTML = `
|
||||||
|
<div class="wechat-history-empty">
|
||||||
|
<div class="wechat-history-empty-icon">📭</div>
|
||||||
|
<div>${emptyText}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间倒序排列
|
||||||
|
const sortedRecords = [...records].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (let i = 0; i < sortedRecords.length; i++) {
|
||||||
|
const record = sortedRecords[i];
|
||||||
|
const time = record.time || '未知时间';
|
||||||
|
const duration = record.duration || '';
|
||||||
|
const messages = record.messages || [];
|
||||||
|
const originalIndex = records.indexOf(record);
|
||||||
|
|
||||||
|
html += `<div class="wechat-history-card" data-tab="${tabType}" data-index="${originalIndex}">`;
|
||||||
|
html += `<div class="wechat-history-card-header">`;
|
||||||
|
html += `<span class="wechat-history-card-time">${escapeHtml(time)}</span>`;
|
||||||
|
html += `<div class="wechat-history-card-actions">`;
|
||||||
|
if (duration) {
|
||||||
|
html += `<span class="wechat-history-card-duration">${escapeHtml(duration)}</span>`;
|
||||||
|
}
|
||||||
|
html += `<button class="wechat-history-delete-btn" data-tab="${tabType}" data-index="${originalIndex}" title="删除">🗑️</button>`;
|
||||||
|
html += `</div>`;
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
// 一起听显示歌曲信息
|
||||||
|
if (tabType === 'listen' && record.song) {
|
||||||
|
const songName = record.song.name || '未知歌曲';
|
||||||
|
const songArtist = record.song.artist || '未知歌手';
|
||||||
|
html += `<div class="wechat-history-card-song">[${escapeHtml(songName)} - ${escapeHtml(songArtist)}]</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息列表
|
||||||
|
if (messages.length > 0) {
|
||||||
|
html += `<div class="wechat-history-card-messages">`;
|
||||||
|
for (const msg of messages) {
|
||||||
|
const isUser = msg.role === 'user';
|
||||||
|
const senderName = isUser ? userName : contact.name;
|
||||||
|
const senderClass = isUser ? 'user' : '';
|
||||||
|
html += `<div class="wechat-history-msg">`;
|
||||||
|
html += `<span class="wechat-history-msg-sender ${senderClass}">${escapeHtml(senderName)}:</span> `;
|
||||||
|
html += `<span class="wechat-history-msg-content">${escapeHtml(msg.content || '')}</span>`;
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentEl.innerHTML = html;
|
||||||
|
|
||||||
|
// 绑定删除按钮事件
|
||||||
|
contentEl.querySelectorAll('.wechat-history-delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const tab = btn.dataset.tab;
|
||||||
|
const index = parseInt(btn.dataset.index);
|
||||||
|
deleteHistoryRecord(tab, index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHistoryEvents() {
|
||||||
|
// 返回按钮
|
||||||
|
document.getElementById('wechat-history-back-btn')?.addEventListener('click', closeHistoryPage);
|
||||||
|
|
||||||
|
// 标签切换
|
||||||
|
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
const tabType = tab.dataset.tab;
|
||||||
|
if (tabType) {
|
||||||
|
switchHistoryTab(tabType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ========== 历史记录功能结束 ==========
|
||||||
|
|
||||||
function normalizeModelListForSelect(models) {
|
function normalizeModelListForSelect(models) {
|
||||||
return (models || []).map(m => {
|
return (models || []).map(m => {
|
||||||
if (typeof m === 'string') return { id: m, name: m };
|
if (typeof m === 'string') return { id: m, name: m };
|
||||||
@@ -230,6 +429,14 @@ function bindEvents() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 查看历史记录
|
||||||
|
document.getElementById('wechat-menu-history')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
||||||
|
if (currentChatIndex >= 0) {
|
||||||
|
openHistoryPage(currentChatIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 清空TA的朋友圈
|
// 清空TA的朋友圈
|
||||||
document.getElementById('wechat-menu-clear-moments')?.addEventListener('click', () => {
|
document.getElementById('wechat-menu-clear-moments')?.addEventListener('click', () => {
|
||||||
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
||||||
@@ -275,6 +482,51 @@ function bindEvents() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 拉黑/取消拉黑功能
|
||||||
|
document.getElementById('wechat-menu-block')?.addEventListener('click', async () => {
|
||||||
|
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
||||||
|
|
||||||
|
// 群聊不支持拉黑
|
||||||
|
const groupIndex = getCurrentGroupIndex();
|
||||||
|
if (groupIndex >= 0) {
|
||||||
|
showToast('群聊暂不支持此功能', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChatIndex < 0) return;
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
const contact = settings.contacts[currentChatIndex];
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
const isBlocked = contact.isBlocked === true;
|
||||||
|
|
||||||
|
if (isBlocked) {
|
||||||
|
// 取消拉黑
|
||||||
|
if (!confirm(`确定要取消拉黑"${contact.name}"吗?`)) return;
|
||||||
|
contact.isBlocked = false;
|
||||||
|
stopBlockedAIMessages(contact);
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
updateBlockMenuText(false);
|
||||||
|
showToast('已取消拉黑', '✓');
|
||||||
|
|
||||||
|
// 显示被拉黑期间AI发送的消息
|
||||||
|
await showBlockedMessages(contact);
|
||||||
|
} else {
|
||||||
|
// 拉黑
|
||||||
|
if (!confirm(`确定要拉黑"${contact.name}"吗?拉黑后对方将无法给你发消息。`)) return;
|
||||||
|
contact.isBlocked = true;
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
updateBlockMenuText(true);
|
||||||
|
showToast('已拉黑', '🚫');
|
||||||
|
|
||||||
|
// 开始AI被拉黑期间发消息
|
||||||
|
startBlockedAIMessages(contact);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 点击聊天页其他地方关闭菜单和面板
|
// 点击聊天页其他地方关闭菜单和面板
|
||||||
document.getElementById('wechat-chat-page')?.addEventListener('click', (e) => {
|
document.getElementById('wechat-chat-page')?.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('#wechat-chat-more-btn') && !e.target.closest('#wechat-chat-menu')) {
|
if (!e.target.closest('#wechat-chat-more-btn') && !e.target.closest('#wechat-chat-menu')) {
|
||||||
@@ -532,8 +784,7 @@ function bindEvents() {
|
|||||||
document.getElementById('wechat-contact-fetch-model')?.addEventListener('click', async () => {
|
document.getElementById('wechat-contact-fetch-model')?.addEventListener('click', async () => {
|
||||||
const apiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim();
|
const apiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim();
|
||||||
const apiKey = document.getElementById('wechat-contact-api-key')?.value?.trim();
|
const apiKey = document.getElementById('wechat-contact-api-key')?.value?.trim();
|
||||||
const modelInput = document.getElementById('wechat-contact-model');
|
const modelSelect = document.getElementById('wechat-contact-model-select');
|
||||||
const modelList = document.getElementById('wechat-contact-model-list');
|
|
||||||
const fetchBtn = document.getElementById('wechat-contact-fetch-model');
|
const fetchBtn = document.getElementById('wechat-contact-fetch-model');
|
||||||
|
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
@@ -548,8 +799,9 @@ function bindEvents() {
|
|||||||
const { fetchModelListFromApi } = await import('./ai.js');
|
const { fetchModelListFromApi } = await import('./ai.js');
|
||||||
const models = await fetchModelListFromApi(apiUrl, apiKey);
|
const models = await fetchModelListFromApi(apiUrl, apiKey);
|
||||||
if (models.length > 0) {
|
if (models.length > 0) {
|
||||||
const currentValue = modelInput?.value || '';
|
const currentValue = modelSelect?.value || '';
|
||||||
modelList.innerHTML = models.map(m => `<option value="${m}">`).join('');
|
modelSelect.innerHTML = '<option value="">---请选择模型---</option>' +
|
||||||
|
models.map(m => `<option value="${m}"${m === currentValue ? ' selected' : ''}>${m}</option>`).join('');
|
||||||
showToast(`获取到 ${models.length} 个模型`);
|
showToast(`获取到 ${models.length} 个模型`);
|
||||||
} else {
|
} else {
|
||||||
showToast('未找到可用模型', 'info');
|
showToast('未找到可用模型', 'info');
|
||||||
@@ -563,11 +815,60 @@ function bindEvents() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 角色独立API手动输入按钮
|
||||||
|
document.getElementById('wechat-contact-model-manual')?.addEventListener('click', () => {
|
||||||
|
const selectWrapper = document.getElementById('wechat-contact-model-select-wrapper');
|
||||||
|
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
|
||||||
|
const modelSelect = document.getElementById('wechat-contact-model-select');
|
||||||
|
const modelInput = document.getElementById('wechat-contact-model-input');
|
||||||
|
|
||||||
|
// 将当前选中的值复制到输入框
|
||||||
|
if (modelSelect?.value) {
|
||||||
|
modelInput.value = modelSelect.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectWrapper.style.display = 'none';
|
||||||
|
inputWrapper.style.display = 'flex';
|
||||||
|
modelInput?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 角色独立API返回按钮
|
||||||
|
document.getElementById('wechat-contact-model-back')?.addEventListener('click', () => {
|
||||||
|
const selectWrapper = document.getElementById('wechat-contact-model-select-wrapper');
|
||||||
|
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
|
||||||
|
const modelSelect = document.getElementById('wechat-contact-model-select');
|
||||||
|
const modelInput = document.getElementById('wechat-contact-model-input');
|
||||||
|
|
||||||
|
// 如果输入框有值,尝试在下拉列表中选中,或添加为新选项
|
||||||
|
const inputValue = modelInput?.value?.trim();
|
||||||
|
if (inputValue && modelSelect) {
|
||||||
|
const existingOption = Array.from(modelSelect.options).find(opt => opt.value === inputValue);
|
||||||
|
if (existingOption) {
|
||||||
|
modelSelect.value = inputValue;
|
||||||
|
} else {
|
||||||
|
// 添加为新选项并选中
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = inputValue;
|
||||||
|
newOption.textContent = inputValue;
|
||||||
|
modelSelect.appendChild(newOption);
|
||||||
|
modelSelect.value = inputValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectWrapper.style.display = 'flex';
|
||||||
|
inputWrapper.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
// 角色独立API测试连接按钮
|
// 角色独立API测试连接按钮
|
||||||
document.getElementById('wechat-contact-test-api')?.addEventListener('click', async () => {
|
document.getElementById('wechat-contact-test-api')?.addEventListener('click', async () => {
|
||||||
const apiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim();
|
const apiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim();
|
||||||
const apiKey = document.getElementById('wechat-contact-api-key')?.value?.trim();
|
const apiKey = document.getElementById('wechat-contact-api-key')?.value?.trim();
|
||||||
const model = document.getElementById('wechat-contact-model')?.value?.trim();
|
// 优先从输入框获取,其次从下拉列表获取
|
||||||
|
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
|
||||||
|
const isManualMode = inputWrapper?.style.display === 'flex';
|
||||||
|
const model = isManualMode
|
||||||
|
? document.getElementById('wechat-contact-model-input')?.value?.trim()
|
||||||
|
: document.getElementById('wechat-contact-model-select')?.value?.trim();
|
||||||
const testBtn = document.getElementById('wechat-contact-test-api');
|
const testBtn = document.getElementById('wechat-contact-test-api');
|
||||||
|
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
@@ -730,7 +1031,9 @@ function bindEvents() {
|
|||||||
initRedPacketEvents();
|
initRedPacketEvents();
|
||||||
initTransferEvents();
|
initTransferEvents();
|
||||||
initGroupRedPacket();
|
initGroupRedPacket();
|
||||||
|
initGiftEvents();
|
||||||
initCropper();
|
initCropper();
|
||||||
|
initHistoryEvents();
|
||||||
|
|
||||||
// 展开面板
|
// 展开面板
|
||||||
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
|
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
|
||||||
|
|||||||
37
moments.js
37
moments.js
@@ -18,8 +18,19 @@ let currentContactIndex = null;
|
|||||||
let currentMomentId = null;
|
let currentMomentId = null;
|
||||||
let currentReplyTo = null; // 当前回复的评论者名称
|
let currentReplyTo = null; // 当前回复的评论者名称
|
||||||
|
|
||||||
// 消息计数器(用于保底机制)
|
// 消息计数器(用于保底机制)- 持久化存储在 settings 中
|
||||||
let messageCounters = {};
|
function getMessageCounter(contactId) {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.momentMessageCounters) settings.momentMessageCounters = {};
|
||||||
|
return settings.momentMessageCounters[contactId] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMessageCounter(contactId, value) {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.momentMessageCounters) settings.momentMessageCounters = {};
|
||||||
|
settings.momentMessageCounters[contactId] = value;
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化朋友圈模块
|
* 初始化朋友圈模块
|
||||||
@@ -2267,20 +2278,16 @@ function addPrivateChatMessage(contactIndex, contact, message) {
|
|||||||
export function recordMessageAndCheckTrigger(contactId) {
|
export function recordMessageAndCheckTrigger(contactId) {
|
||||||
if (!contactId) return false;
|
if (!contactId) return false;
|
||||||
|
|
||||||
// 初始化计数器
|
// 计数器 +1(持久化存储)
|
||||||
if (!messageCounters[contactId]) {
|
const count = getMessageCounter(contactId) + 1;
|
||||||
messageCounters[contactId] = 0;
|
setMessageCounter(contactId, count);
|
||||||
}
|
|
||||||
|
|
||||||
messageCounters[contactId]++;
|
|
||||||
|
|
||||||
const count = messageCounters[contactId];
|
|
||||||
console.log(`[可乐] 朋友圈触发检查: ${contactId} 已累计 ${count} 条消息`);
|
console.log(`[可乐] 朋友圈触发检查: ${contactId} 已累计 ${count} 条消息`);
|
||||||
|
|
||||||
// 保底机制:每100条消息必触发
|
// 保底机制:每100条消息必触发
|
||||||
if (count >= 100) {
|
if (count >= 100) {
|
||||||
console.log(`[可乐] 触发保底机制: ${contactId} 达到100条消息`);
|
console.log(`[可乐] 触发保底机制: ${contactId} 达到100条消息`);
|
||||||
messageCounters[contactId] = 0;
|
setMessageCounter(contactId, 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2288,7 +2295,7 @@ export function recordMessageAndCheckTrigger(contactId) {
|
|||||||
// 但至少要有5条消息后才开始随机
|
// 但至少要有5条消息后才开始随机
|
||||||
if (count >= 5 && Math.random() < 0.10) {
|
if (count >= 5 && Math.random() < 0.10) {
|
||||||
console.log(`[可乐] 随机触发: ${contactId} 在第 ${count} 条消息触发`);
|
console.log(`[可乐] 随机触发: ${contactId} 在第 ${count} 条消息触发`);
|
||||||
messageCounters[contactId] = 0;
|
setMessageCounter(contactId, 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2334,11 +2341,15 @@ export async function tryTriggerMomentAfterChat(contactIndex) {
|
|||||||
* @param {string} contactId - 联系人ID,不传则重置所有
|
* @param {string} contactId - 联系人ID,不传则重置所有
|
||||||
*/
|
*/
|
||||||
export function resetMessageCounter(contactId = null) {
|
export function resetMessageCounter(contactId = null) {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.momentMessageCounters) settings.momentMessageCounters = {};
|
||||||
|
|
||||||
if (contactId) {
|
if (contactId) {
|
||||||
messageCounters[contactId] = 0;
|
settings.momentMessageCounters[contactId] = 0;
|
||||||
} else {
|
} else {
|
||||||
messageCounters = {};
|
settings.momentMessageCounters = {};
|
||||||
}
|
}
|
||||||
|
requestSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
126
phone-html.js
126
phone-html.js
@@ -171,6 +171,10 @@ export function generatePhoneHTML() {
|
|||||||
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8" cy="10" r="1.5" fill="currentColor"/><circle cx="12" cy="7" r="1.5" fill="currentColor"/><circle cx="16" cy="10" r="1.5" fill="currentColor"/><circle cx="8" cy="14" r="1.5" fill="currentColor"/><circle cx="16" cy="14" r="1.5" fill="currentColor"/><circle cx="12" cy="17" r="1.5" fill="currentColor"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8" cy="10" r="1.5" fill="currentColor"/><circle cx="12" cy="7" r="1.5" fill="currentColor"/><circle cx="16" cy="10" r="1.5" fill="currentColor"/><circle cx="8" cy="14" r="1.5" fill="currentColor"/><circle cx="16" cy="14" r="1.5" fill="currentColor"/><circle cx="12" cy="17" r="1.5" fill="currentColor"/></svg>
|
||||||
<span>TA的朋友圈</span>
|
<span>TA的朋友圈</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="wechat-dropdown-item" id="wechat-menu-history">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 6v6l4 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
|
||||||
|
<span>历史记录</span>
|
||||||
|
</div>
|
||||||
<div class="wechat-dropdown-item wechat-dropdown-item-danger" id="wechat-menu-clear-moments">
|
<div class="wechat-dropdown-item wechat-dropdown-item-danger" id="wechat-menu-clear-moments">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
<span>清空朋友圈</span>
|
<span>清空朋友圈</span>
|
||||||
@@ -179,6 +183,10 @@ export function generatePhoneHTML() {
|
|||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2M10 11v6M14 11v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2M10 11v6M14 11v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
|
||||||
<span>清空聊天</span>
|
<span>清空聊天</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="wechat-dropdown-item wechat-dropdown-item-danger" id="wechat-menu-block">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4.93 4.93l14.14 14.14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
|
<span id="wechat-menu-block-text">拉黑</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 撤回消息区面板 -->
|
<!-- 撤回消息区面板 -->
|
||||||
<div id="wechat-recalled-panel" class="wechat-slide-panel hidden" style="position: absolute; top: 50px; left: 10px; right: 10px; max-height: 60%; z-index: 99; overflow: hidden; border-radius: 12px;">
|
<div id="wechat-recalled-panel" class="wechat-slide-panel hidden" style="position: absolute; top: 50px; left: 10px; right: 10px; max-height: 60%; z-index: 99; overflow: hidden; border-radius: 12px;">
|
||||||
@@ -283,7 +291,9 @@ export function generatePhoneHTML() {
|
|||||||
${generateMusicPanelHTML()}
|
${generateMusicPanelHTML()}
|
||||||
${generateListenTogetherHTML()}
|
${generateListenTogetherHTML()}
|
||||||
${generateMomentsPageHTML()}
|
${generateMomentsPageHTML()}
|
||||||
|
${generateHistoryPageHTML()}
|
||||||
${generateRedPacketPageHTML(settings)}
|
${generateRedPacketPageHTML(settings)}
|
||||||
|
${generateGiftPageHTML()}
|
||||||
${generateOpenRedPacketHTML()}
|
${generateOpenRedPacketHTML()}
|
||||||
${generateRedPacketDetailHTML(settings)}
|
${generateRedPacketDetailHTML(settings)}
|
||||||
${generateTransferPageHTML()}
|
${generateTransferPageHTML()}
|
||||||
@@ -873,11 +883,17 @@ function generateModalsHTML(settings) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="wechat-settings-label" style="font-size: 12px; margin-bottom: 4px; display: block;">模型</span>
|
<span class="wechat-settings-label" style="font-size: 12px; margin-bottom: 4px; display: block;">模型</span>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;" id="wechat-contact-model-select-wrapper">
|
||||||
<input type="text" class="wechat-settings-input" id="wechat-contact-model" placeholder="模型名称" list="wechat-contact-model-list" style="flex: 1; box-sizing: border-box;">
|
<select class="wechat-settings-input" id="wechat-contact-model-select" style="flex: 1; box-sizing: border-box;">
|
||||||
|
<option value="">---请选择模型---</option>
|
||||||
|
</select>
|
||||||
|
<button class="wechat-btn wechat-btn-small" id="wechat-contact-model-manual" style="white-space: nowrap;">手动</button>
|
||||||
<button class="wechat-btn wechat-btn-small wechat-btn-primary" id="wechat-contact-fetch-model" style="white-space: nowrap;">获取</button>
|
<button class="wechat-btn wechat-btn-small wechat-btn-primary" id="wechat-contact-fetch-model" style="white-space: nowrap;">获取</button>
|
||||||
</div>
|
</div>
|
||||||
<datalist id="wechat-contact-model-list"></datalist>
|
<div style="display: none; gap: 8px;" id="wechat-contact-model-input-wrapper">
|
||||||
|
<input type="text" class="wechat-settings-input" id="wechat-contact-model-input" placeholder="手动输入模型名称" style="flex: 1; box-sizing: border-box;">
|
||||||
|
<button class="wechat-btn wechat-btn-small" id="wechat-contact-model-back" style="white-space: nowrap;">返回</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 8px; margin-top: 4px;">
|
<div style="display: flex; gap: 8px; margin-top: 4px;">
|
||||||
<button class="wechat-btn wechat-btn-small" id="wechat-contact-test-api" style="flex: 1;">测试连接</button>
|
<button class="wechat-btn wechat-btn-small" id="wechat-contact-test-api" style="flex: 1;">测试连接</button>
|
||||||
@@ -1268,6 +1284,80 @@ function generateOpenRedPacketHTML() {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 礼物页面 HTML
|
||||||
|
function generateGiftPageHTML() {
|
||||||
|
return `
|
||||||
|
<!-- 礼物页面 -->
|
||||||
|
<div id="wechat-gift-page" class="wechat-gift-page hidden">
|
||||||
|
<div class="wechat-navbar wechat-gift-navbar">
|
||||||
|
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-gift-back">‹</button>
|
||||||
|
<span class="wechat-navbar-title">送礼物</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-gift-content">
|
||||||
|
<!-- 送礼目标选择(情趣玩具时显示) -->
|
||||||
|
<div class="wechat-gift-target hidden" id="wechat-gift-target"></div>
|
||||||
|
<div class="wechat-gift-tabs" id="wechat-gift-tabs"></div>
|
||||||
|
<div class="wechat-gift-grid" id="wechat-gift-grid"></div>
|
||||||
|
<div class="wechat-gift-desc-row">
|
||||||
|
<input type="text" class="wechat-gift-desc-input" id="wechat-gift-desc" placeholder="添加留言(选填)" maxlength="50">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-gift-footer">
|
||||||
|
<button class="wechat-gift-send-btn" id="wechat-gift-send" disabled>请选择礼物</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 礼物送达询问弹窗 -->
|
||||||
|
<div id="wechat-gift-arrival-modal" class="wechat-modal hidden">
|
||||||
|
<div class="wechat-modal-content wechat-gift-arrival-content">
|
||||||
|
<div class="wechat-modal-title">商品已送达</div>
|
||||||
|
<div class="wechat-modal-body" id="wechat-gift-arrival-body">
|
||||||
|
您的商品已送达,您要现在开始玩吗?
|
||||||
|
</div>
|
||||||
|
<div class="wechat-modal-actions">
|
||||||
|
<button class="wechat-btn wechat-gift-arrival-btn-no" id="wechat-gift-arrival-no">稍后</button>
|
||||||
|
<button class="wechat-btn wechat-gift-arrival-btn-yes" id="wechat-gift-arrival-yes">开始</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 玩具控制页面 -->
|
||||||
|
<div id="wechat-toy-control-page" class="wechat-toy-control-page hidden">
|
||||||
|
<div class="wechat-navbar wechat-toy-control-navbar">
|
||||||
|
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-toy-control-back">
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="wechat-navbar-title" id="wechat-toy-control-title">玩具控制</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制按钮区域 -->
|
||||||
|
<div class="wechat-toy-control-buttons" id="wechat-toy-control-buttons">
|
||||||
|
<!-- 按钮由JS动态渲染 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 电击按钮行(仅微电流乳链) -->
|
||||||
|
<div class="wechat-toy-btn-row wechat-toy-shock-row hidden" id="wechat-toy-shock-row">
|
||||||
|
<!-- 由JS动态渲染 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 聊天区域 -->
|
||||||
|
<div class="wechat-toy-control-chat">
|
||||||
|
<div class="wechat-toy-control-messages" id="wechat-toy-control-messages">
|
||||||
|
<!-- 消息列表由JS动态渲染 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<div class="wechat-toy-control-input-area">
|
||||||
|
<input type="text" id="wechat-toy-control-input" class="wechat-toy-control-input" placeholder="说点什么..." />
|
||||||
|
<button id="wechat-toy-control-send" class="wechat-toy-control-send">发送</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// 发转账页面 HTML
|
// 发转账页面 HTML
|
||||||
function generateTransferPageHTML() {
|
function generateTransferPageHTML() {
|
||||||
return `
|
return `
|
||||||
@@ -1534,3 +1624,33 @@ function generateListenTogetherHTML() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 历史记录页面 HTML
|
||||||
|
function generateHistoryPageHTML() {
|
||||||
|
return `
|
||||||
|
<!-- 历史记录页面 -->
|
||||||
|
<div id="wechat-history-page" class="wechat-history-page hidden">
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<div class="wechat-history-navbar">
|
||||||
|
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-history-back-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="wechat-navbar-title">历史记录</span>
|
||||||
|
<div style="width: 24px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 四个标签按钮 -->
|
||||||
|
<div class="wechat-history-tabs">
|
||||||
|
<button class="wechat-history-tab active" data-tab="listen">一起听</button>
|
||||||
|
<button class="wechat-history-tab" data-tab="voice">语音通话</button>
|
||||||
|
<button class="wechat-history-tab" data-tab="video">视频通话</button>
|
||||||
|
<button class="wechat-history-tab wechat-history-tab-pink" data-tab="toy">心动瞬间</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="wechat-history-content" id="wechat-history-content">
|
||||||
|
<!-- 由 JS 动态填充 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|||||||
58
summary.js
58
summary.js
@@ -168,7 +168,10 @@ export function collectAllChatHistory(selectedFilter = null) {
|
|||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
time: msg.time || '',
|
time: msg.time || '',
|
||||||
isVoice: msg.isVoice || false
|
isVoice: msg.isVoice || false,
|
||||||
|
isSticker: msg.isSticker || false,
|
||||||
|
isPhoto: msg.isPhoto || false,
|
||||||
|
musicInfo: msg.musicInfo || null
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -215,7 +218,10 @@ export function collectAllChatHistory(selectedFilter = null) {
|
|||||||
content: msg.content,
|
content: msg.content,
|
||||||
characterName: msg.characterName || '',
|
characterName: msg.characterName || '',
|
||||||
time: msg.time || '',
|
time: msg.time || '',
|
||||||
isVoice: msg.isVoice || false
|
isVoice: msg.isVoice || false,
|
||||||
|
isSticker: msg.isSticker || false,
|
||||||
|
isPhoto: msg.isPhoto || false,
|
||||||
|
musicInfo: msg.musicInfo || null
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -303,31 +309,24 @@ export function generateSummaryPrompt(allChats, cupNumber) {
|
|||||||
if (settings.customSummaryTemplate && settings.customSummaryTemplate.trim()) {
|
if (settings.customSummaryTemplate && settings.customSummaryTemplate.trim()) {
|
||||||
prompt = settings.customSummaryTemplate.trim() + '\n\n【线上聊天记录】\n';
|
prompt = settings.customSummaryTemplate.trim() + '\n\n【线上聊天记录】\n';
|
||||||
} else {
|
} else {
|
||||||
// 使用默认模板
|
// 使用默认模板(纯对话记录模式)
|
||||||
prompt = `你是一位客观、精准的结构化事件记录员。你的任务是像历史学家记录史实一样,从这段【线上聊天记录】中提取并记录关键信息。
|
prompt = `你的任务是将这段【线上聊天记录】原样整理成JSON格式。
|
||||||
|
|
||||||
【核心原则】
|
【核心原则】
|
||||||
- 客观准确:只记录实际发生的事件,不添加主观推测或情感评价
|
- 原样保留:完整复制每一条对话,不做任何修改、润色或总结
|
||||||
- 结构清晰:按时间顺序提取关键节点
|
- 格式统一:按"发言者: 内容"格式逐行记录
|
||||||
- 忠于原文:尽量保留原始表述,避免过度概括
|
- 仅提取关键词:从对话中提取3-5个核心关键词用于检索触发
|
||||||
- 重点突出:只记录推动事件发展的关键信息
|
|
||||||
|
|
||||||
【记录要点】
|
|
||||||
- 关系状态的实际变化(约定、承诺、矛盾、和解等具体事件)
|
|
||||||
- 重要的对话内容和决定
|
|
||||||
- 人物之间的互动行为
|
|
||||||
- 情感表达的关键时刻
|
|
||||||
|
|
||||||
【输出格式要求】
|
【输出格式要求】
|
||||||
- 只输出一个JSON对象
|
- 只输出一个JSON对象
|
||||||
- 不要使用markdown代码块
|
- 不要使用markdown代码块
|
||||||
- 直接以 { 开头,以 } 结尾
|
- 直接以 { 开头,以 } 结尾
|
||||||
- keys: 3-5个能代表本次聊天核心内容的关键词
|
- keys: 3-5个能代表本次聊天核心内容的关键词(人名、地点、事件等)
|
||||||
- content: 按"序号: 事件记录"格式列出关键节点(每条一行)
|
- content: 原样复制的对话记录,每条一行,格式为"发言者: 内容"
|
||||||
- comment: "${getCupName(cupNumber)}"
|
- comment: "${getCupName(cupNumber)}"
|
||||||
|
|
||||||
【JSON示例】
|
【JSON示例】
|
||||||
{"keys":["约会","告白","接受"],"content":"1: {{user}}邀请{{char}}周末见面\\n2: {{char}}表示期待并确认时间\\n3: {{user}}表达好感,{{char}}积极回应","comment":"${getCupName(cupNumber)}"}
|
{"keys":["公园","约会","周末"],"content":"{{user}}: 今天去哪玩?\\n{{char}}: 去公园吧\\n{{user}}: 好呀\\n{{char}}: 那我们下午2点见","comment":"${getCupName(cupNumber)}"}
|
||||||
|
|
||||||
【线上聊天记录】
|
【线上聊天记录】
|
||||||
`;
|
`;
|
||||||
@@ -347,11 +346,32 @@ export function generateSummaryPrompt(allChats, cupNumber) {
|
|||||||
speaker = match ? match[1] : '{{char}}';
|
speaker = match ? match[1] : '{{char}}';
|
||||||
}
|
}
|
||||||
const timeStr = msg.time ? `[${msg.time}] ` : '';
|
const timeStr = msg.time ? `[${msg.time}] ` : '';
|
||||||
prompt += `${timeStr}${speaker}: ${msg.content}\n`;
|
|
||||||
|
// 根据消息类型生成不同的内容描述
|
||||||
|
let messageContent;
|
||||||
|
if (msg.musicInfo) {
|
||||||
|
// 音乐分享
|
||||||
|
const musicName = msg.musicInfo.name || '未知歌曲';
|
||||||
|
const musicArtist = msg.musicInfo.artist || '未知歌手';
|
||||||
|
messageContent = `[分享歌曲] ${musicName} - ${musicArtist}`;
|
||||||
|
} else if (msg.isVoice) {
|
||||||
|
// 语音消息
|
||||||
|
messageContent = `[语音] ${msg.content}`;
|
||||||
|
} else if (msg.isSticker) {
|
||||||
|
// 表情包
|
||||||
|
messageContent = '[发送了一个表情包]';
|
||||||
|
} else if (msg.isPhoto) {
|
||||||
|
// 图片
|
||||||
|
messageContent = '[发送了一张图片]';
|
||||||
|
} else {
|
||||||
|
messageContent = msg.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `${timeStr}${speaker}: ${messageContent}\n`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
prompt += `\n请从以上线上聊天记录中提取关键事件节点,输出${getCupName(cupNumber)}的JSON:`;
|
prompt += `\n请将以上聊天记录原样整理成${getCupName(cupNumber)}的JSON:`;
|
||||||
|
|
||||||
return prompt;
|
return prompt;
|
||||||
}
|
}
|
||||||
|
|||||||
736
toy-control.js
Normal file
736
toy-control.js
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
/**
|
||||||
|
* 玩具控制界面
|
||||||
|
* 类似语音通话的交互模式,支持按钮控制和聊天
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSettings, splitAIMessages } from './config.js';
|
||||||
|
import { requestSave } from './save-manager.js';
|
||||||
|
import { escapeHtml } from './utils.js';
|
||||||
|
import { refreshChatList } from './ui.js';
|
||||||
|
import { callAI } from './ai.js';
|
||||||
|
import { appendMessage, showTypingIndicator, hideTypingIndicator } from './chat.js';
|
||||||
|
|
||||||
|
// SVG图标定义
|
||||||
|
const TOY_ICONS = {
|
||||||
|
classic: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||||
|
start: `<svg viewBox="0 0 24 24" width="28" height="28"><polygon points="5 3 19 12 5 21 5 3" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||||
|
rampage: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||||
|
wave: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M2 12c2-3 4-6 6-6s4 6 6 6 4-6 6-6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 18c2-3 4-6 6-6s4 6 6 6 4-6 6-6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||||
|
pause: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="6" y="4" width="4" height="16" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/><rect x="14" y="4" width="4" height="16" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/></svg>`,
|
||||||
|
shock: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="1.5" fill="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||||
|
back: `<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 控制模式定义
|
||||||
|
const TOY_CONTROL_MODES = {
|
||||||
|
classic: {
|
||||||
|
id: 'classic',
|
||||||
|
name: '经典模式',
|
||||||
|
icon: TOY_ICONS.classic,
|
||||||
|
desc: '稳定持续的震动'
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
id: 'start',
|
||||||
|
name: '开始享受',
|
||||||
|
icon: TOY_ICONS.start,
|
||||||
|
desc: '开始/继续震动'
|
||||||
|
},
|
||||||
|
rampage: {
|
||||||
|
id: 'rampage',
|
||||||
|
name: '一键暴走',
|
||||||
|
icon: TOY_ICONS.rampage,
|
||||||
|
desc: '最大强度震动'
|
||||||
|
},
|
||||||
|
wave: {
|
||||||
|
id: 'wave',
|
||||||
|
name: '波浪模式',
|
||||||
|
icon: TOY_ICONS.wave,
|
||||||
|
desc: '由弱到强循环'
|
||||||
|
},
|
||||||
|
pause: {
|
||||||
|
id: 'pause',
|
||||||
|
name: '暂停',
|
||||||
|
icon: TOY_ICONS.pause,
|
||||||
|
desc: '暂停震动'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 电击按钮(仅微电流乳链)
|
||||||
|
const SHOCK_BUTTON = {
|
||||||
|
id: 'shock',
|
||||||
|
name: '电击',
|
||||||
|
icon: TOY_ICONS.shock,
|
||||||
|
desc: '触发微电流刺激'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 控制状态
|
||||||
|
let toyControlState = {
|
||||||
|
isActive: false,
|
||||||
|
gift: null,
|
||||||
|
target: null,
|
||||||
|
contact: null,
|
||||||
|
contactIndex: -1,
|
||||||
|
currentMode: null,
|
||||||
|
messages: [],
|
||||||
|
activeModes: new Set(),
|
||||||
|
sessionStartTime: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示控制界面
|
||||||
|
export function showToyControlPage(gift, contact, contactIndex) {
|
||||||
|
toyControlState = {
|
||||||
|
isActive: true,
|
||||||
|
gift: gift,
|
||||||
|
target: gift.target,
|
||||||
|
contact: contact,
|
||||||
|
contactIndex: contactIndex,
|
||||||
|
currentMode: null,
|
||||||
|
messages: [],
|
||||||
|
activeModes: new Set(),
|
||||||
|
sessionStartTime: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记正在使用
|
||||||
|
if (contact.pendingGifts) {
|
||||||
|
const pendingGift = contact.pendingGifts.find(g => g.timestamp === gift.timestamp);
|
||||||
|
if (pendingGift) {
|
||||||
|
pendingGift.isUsing = true;
|
||||||
|
requestSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToyControlPage();
|
||||||
|
bindToyControlEvents();
|
||||||
|
|
||||||
|
const page = document.getElementById('wechat-toy-control-page');
|
||||||
|
if (page) {
|
||||||
|
page.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI发起开场白
|
||||||
|
setTimeout(() => {
|
||||||
|
triggerToyAIGreeting();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏控制界面
|
||||||
|
export function hideToyControlPage() {
|
||||||
|
const page = document.getElementById('wechat-toy-control-page');
|
||||||
|
if (page) {
|
||||||
|
page.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存心动瞬间记录
|
||||||
|
saveToySession();
|
||||||
|
|
||||||
|
// 触发结束后的AI消息(在主聊天中)
|
||||||
|
triggerToyEndMessage();
|
||||||
|
|
||||||
|
toyControlState.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染控制界面
|
||||||
|
function renderToyControlPage() {
|
||||||
|
const titleEl = document.getElementById('wechat-toy-control-title');
|
||||||
|
const buttonsEl = document.getElementById('wechat-toy-control-buttons');
|
||||||
|
const shockRowEl = document.getElementById('wechat-toy-shock-row');
|
||||||
|
const messagesEl = document.getElementById('wechat-toy-control-messages');
|
||||||
|
|
||||||
|
if (titleEl) {
|
||||||
|
const targetText = toyControlState.target === 'character' ? 'TA在用' : '你在用';
|
||||||
|
titleEl.textContent = `${toyControlState.gift.giftName} · ${targetText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染按钮
|
||||||
|
if (buttonsEl) {
|
||||||
|
let buttonsHtml = `
|
||||||
|
<div class="wechat-toy-btn-row">
|
||||||
|
<button class="wechat-toy-btn" data-mode="classic">
|
||||||
|
${TOY_CONTROL_MODES.classic.icon}
|
||||||
|
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.classic.name}</span>
|
||||||
|
</button>
|
||||||
|
<button class="wechat-toy-btn" data-mode="start">
|
||||||
|
${TOY_CONTROL_MODES.start.icon}
|
||||||
|
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.start.name}</span>
|
||||||
|
</button>
|
||||||
|
<button class="wechat-toy-btn" data-mode="rampage">
|
||||||
|
${TOY_CONTROL_MODES.rampage.icon}
|
||||||
|
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.rampage.name}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-toy-btn-row">
|
||||||
|
<button class="wechat-toy-btn" data-mode="wave">
|
||||||
|
${TOY_CONTROL_MODES.wave.icon}
|
||||||
|
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.wave.name}</span>
|
||||||
|
</button>
|
||||||
|
<button class="wechat-toy-btn" data-mode="pause">
|
||||||
|
${TOY_CONTROL_MODES.pause.icon}
|
||||||
|
<span class="wechat-toy-btn-label">${TOY_CONTROL_MODES.pause.name}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
buttonsEl.innerHTML = buttonsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 电击按钮(仅微电流乳链显示)
|
||||||
|
if (shockRowEl) {
|
||||||
|
if (toyControlState.gift.hasShock) {
|
||||||
|
shockRowEl.classList.remove('hidden');
|
||||||
|
shockRowEl.innerHTML = `
|
||||||
|
<button class="wechat-toy-btn wechat-toy-btn-shock" data-mode="shock">
|
||||||
|
${SHOCK_BUTTON.icon}
|
||||||
|
<span class="wechat-toy-btn-label">${SHOCK_BUTTON.name}</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
shockRowEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空消息
|
||||||
|
if (messagesEl) {
|
||||||
|
messagesEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
let toyEventsBound = false;
|
||||||
|
function bindToyControlEvents() {
|
||||||
|
if (toyEventsBound) return;
|
||||||
|
toyEventsBound = true;
|
||||||
|
|
||||||
|
// 返回按钮
|
||||||
|
document.getElementById('wechat-toy-control-back')?.addEventListener('click', hideToyControlPage);
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
document.getElementById('wechat-toy-control-send')?.addEventListener('click', sendToyMessage);
|
||||||
|
|
||||||
|
// 输入框回车发送
|
||||||
|
document.getElementById('wechat-toy-control-input')?.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
sendToyMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按钮点击事件(使用事件委托)
|
||||||
|
document.getElementById('wechat-toy-control-buttons')?.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.wechat-toy-btn');
|
||||||
|
if (btn) {
|
||||||
|
const mode = btn.dataset.mode;
|
||||||
|
if (mode) {
|
||||||
|
onButtonPress(mode, 'user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('wechat-toy-shock-row')?.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.wechat-toy-btn');
|
||||||
|
if (btn) {
|
||||||
|
const mode = btn.dataset.mode;
|
||||||
|
if (mode) {
|
||||||
|
onButtonPress(mode, 'user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮点击处理
|
||||||
|
async function onButtonPress(buttonId, pressedBy = 'user') {
|
||||||
|
if (!toyControlState.isActive) return;
|
||||||
|
|
||||||
|
const button = TOY_CONTROL_MODES[buttonId] || (buttonId === 'shock' ? SHOCK_BUTTON : null);
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
// 更新按钮状态(变深色)
|
||||||
|
updateButtonState(buttonId);
|
||||||
|
|
||||||
|
// 显示typing
|
||||||
|
showToyTypingIndicator();
|
||||||
|
|
||||||
|
// 构建提示词
|
||||||
|
const prompt = buildButtonPressPrompt(buttonId, button.name, pressedBy);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await callToyAI(prompt);
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
processAIResponse(response);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
console.error('[可乐] 玩具控制AI回复失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
function updateButtonState(buttonId) {
|
||||||
|
// 暂停按钮清除所有激活状态
|
||||||
|
if (buttonId === 'pause') {
|
||||||
|
toyControlState.activeModes.clear();
|
||||||
|
} else if (buttonId !== 'shock') {
|
||||||
|
// 电击是一次性的,不保持激活状态
|
||||||
|
// 其他模式切换
|
||||||
|
toyControlState.activeModes.clear();
|
||||||
|
toyControlState.activeModes.add(buttonId);
|
||||||
|
}
|
||||||
|
toyControlState.currentMode = buttonId;
|
||||||
|
|
||||||
|
// 更新UI
|
||||||
|
document.querySelectorAll('.wechat-toy-btn').forEach(btn => {
|
||||||
|
const mode = btn.dataset.mode;
|
||||||
|
if (toyControlState.activeModes.has(mode)) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建按钮按下提示词
|
||||||
|
function buildButtonPressPrompt(buttonId, buttonName, pressedBy) {
|
||||||
|
const isCharacterUsing = toyControlState.target === 'character';
|
||||||
|
const presserText = pressedBy === 'user' ? '用户' : '你自己';
|
||||||
|
|
||||||
|
const modeEffects = {
|
||||||
|
classic: '稳定持续的震动开始了',
|
||||||
|
start: '震动开始/继续了',
|
||||||
|
rampage: '震动突然变到最大强度,非常强烈的刺激袭来',
|
||||||
|
wave: '震动开始由弱到强循环变化,一波一波的刺激',
|
||||||
|
pause: '震动停止了,可以喘息一下',
|
||||||
|
shock: '一阵微电流刺激瞬间传来,让人猛地一颤'
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt = `[${presserText}按下了"${buttonName}"按钮]
|
||||||
|
|
||||||
|
效果:${modeEffects[buttonId]}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isCharacterUsing) {
|
||||||
|
prompt += `你正在使用${toyControlState.gift.giftName},请根据这个刺激变化做出反应。
|
||||||
|
描述你的身体感受、情绪变化。回复要有情感细节,符合你的角色性格。`;
|
||||||
|
} else {
|
||||||
|
prompt += `用户正在使用${toyControlState.gift.giftName},你在观察。
|
||||||
|
请描述你观察到的用户可能的反应,可以调侃、鼓励或挑逗。回复要有趣,符合你的角色性格。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `
|
||||||
|
|
||||||
|
【重要规则】
|
||||||
|
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||||
|
2. 禁止使用小括号描述动作如(xxx)
|
||||||
|
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式
|
||||||
|
4. 直接输出角色说的话和感受`;
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送聊天消息
|
||||||
|
async function sendToyMessage() {
|
||||||
|
const input = document.getElementById('wechat-toy-control-input');
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const message = input.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
if (!toyControlState.isActive) return;
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
// 添加用户消息
|
||||||
|
addToyMessage('user', message);
|
||||||
|
|
||||||
|
// 显示typing
|
||||||
|
showToyTypingIndicator();
|
||||||
|
|
||||||
|
// 构建聊天提示词
|
||||||
|
const prompt = buildChatPrompt(message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await callToyAI(prompt);
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
processAIResponse(response);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
console.error('[可乐] 玩具控制聊天AI回复失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建聊天提示词
|
||||||
|
function buildChatPrompt(userMessage) {
|
||||||
|
const isCharacterUsing = toyControlState.target === 'character';
|
||||||
|
const currentModeText = toyControlState.currentMode
|
||||||
|
? (TOY_CONTROL_MODES[toyControlState.currentMode]?.name || '已暂停')
|
||||||
|
: '未开始';
|
||||||
|
|
||||||
|
let prompt = `[玩具控制场景聊天]
|
||||||
|
当前状态:${isCharacterUsing ? '你' : '用户'}正在使用${toyControlState.gift.giftName}
|
||||||
|
当前模式:${currentModeText}
|
||||||
|
|
||||||
|
用户说:${userMessage}
|
||||||
|
|
||||||
|
请根据当前场景和你的角色性格回复。`;
|
||||||
|
|
||||||
|
if (isCharacterUsing) {
|
||||||
|
prompt += `
|
||||||
|
你是使用者,可能正在承受刺激,回复时要体现身体状态和情绪。`;
|
||||||
|
} else {
|
||||||
|
prompt += `
|
||||||
|
你是观察者,用户正在使用玩具,你可以调侃、鼓励或挑逗。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `
|
||||||
|
|
||||||
|
【AI可以主动按按钮】
|
||||||
|
如果你想主动控制玩具,可以在回复末尾加上 [按下:按钮名]
|
||||||
|
可用按钮:经典模式、开始享受、一键暴走、波浪模式、暂停${toyControlState.gift.hasShock ? '、电击' : ''}
|
||||||
|
例如:[按下:暴走] 或 [按下:暂停]
|
||||||
|
|
||||||
|
什么时候AI应该按按钮:
|
||||||
|
${isCharacterUsing
|
||||||
|
? '- 如果你受不了了,可以偷偷按暂停\n- 如果你想要更多刺激,可以自己切换模式'
|
||||||
|
: '- 如果你想折磨用户,可以突然按暴走\n- 如果用户表现太淡定,你可以加大力度'}
|
||||||
|
|
||||||
|
【重要规则】
|
||||||
|
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||||
|
2. 禁止使用小括号描述动作如(xxx)
|
||||||
|
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式
|
||||||
|
4. 按按钮指令[按下:xxx]必须放在回复末尾,且是可选的`;
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用AI(玩具控制专用)
|
||||||
|
async function callToyAI(prompt) {
|
||||||
|
if (!toyControlState.contact) return null;
|
||||||
|
|
||||||
|
// 构建历史消息
|
||||||
|
const historyMessages = toyControlState.messages.slice(-10).map(m => ({
|
||||||
|
role: m.role === 'user' ? 'user' : 'assistant',
|
||||||
|
content: m.content
|
||||||
|
}));
|
||||||
|
|
||||||
|
return await callAI(toyControlState.contact, prompt, historyMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理AI回复(检测是否有按按钮指令)
|
||||||
|
function processAIResponse(response) {
|
||||||
|
if (!response) return;
|
||||||
|
|
||||||
|
// 分割多条消息
|
||||||
|
const parts = splitAIMessages(response);
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
let reply = part.trim();
|
||||||
|
|
||||||
|
// 过滤特殊标签
|
||||||
|
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||||
|
|
||||||
|
// 检测 [按下:xxx] 格式
|
||||||
|
const buttonMatch = reply.match(/\[按下[::](.+?)\]/);
|
||||||
|
if (buttonMatch) {
|
||||||
|
const buttonName = buttonMatch[1].trim();
|
||||||
|
// 移除指令文本
|
||||||
|
reply = reply.replace(/\[按下[::].+?\]/g, '').trim();
|
||||||
|
|
||||||
|
// 过滤其他标签和括号
|
||||||
|
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||||
|
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||||
|
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
|
||||||
|
// 添加AI消息
|
||||||
|
if (reply) {
|
||||||
|
addToyMessage('ai', reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI按下按钮
|
||||||
|
setTimeout(() => {
|
||||||
|
const buttonId = findButtonIdByName(buttonName);
|
||||||
|
if (buttonId) {
|
||||||
|
onButtonPress(buttonId, 'ai');
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
} else {
|
||||||
|
// 过滤标签和括号
|
||||||
|
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||||
|
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||||
|
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
addToyMessage('ai', reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据按钮名称查找ID
|
||||||
|
function findButtonIdByName(name) {
|
||||||
|
const nameMap = {
|
||||||
|
'经典模式': 'classic',
|
||||||
|
'经典': 'classic',
|
||||||
|
'开始享受': 'start',
|
||||||
|
'开始': 'start',
|
||||||
|
'一键暴走': 'rampage',
|
||||||
|
'暴走': 'rampage',
|
||||||
|
'波浪模式': 'wave',
|
||||||
|
'波浪': 'wave',
|
||||||
|
'暂停': 'pause',
|
||||||
|
'电击': 'shock'
|
||||||
|
};
|
||||||
|
return nameMap[name] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI开场白
|
||||||
|
async function triggerToyAIGreeting() {
|
||||||
|
if (!toyControlState.isActive) return;
|
||||||
|
|
||||||
|
showToyTypingIndicator();
|
||||||
|
|
||||||
|
const isCharacterUsing = toyControlState.target === 'character';
|
||||||
|
|
||||||
|
let prompt;
|
||||||
|
if (isCharacterUsing) {
|
||||||
|
prompt = `[玩具控制场景开始]
|
||||||
|
用户刚刚打开了${toyControlState.gift.giftName}的控制界面,这个玩具是给你用的。
|
||||||
|
玩具还没有开始运作,用户正准备控制它。
|
||||||
|
|
||||||
|
请根据你的角色性格,对即将开始的事情做出反应:
|
||||||
|
- 可以表现出期待、紧张、害羞、兴奋等情绪
|
||||||
|
- 可以说一些挑逗或撒娇的话
|
||||||
|
- 让用户知道你准备好了
|
||||||
|
|
||||||
|
【重要规则】
|
||||||
|
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||||
|
2. 禁止使用小括号描述动作如(xxx)
|
||||||
|
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式`;
|
||||||
|
} else {
|
||||||
|
prompt = `[玩具控制场景开始]
|
||||||
|
用户刚刚打开了${toyControlState.gift.giftName}的控制界面,这个玩具是用户自己用的。
|
||||||
|
玩具还没有开始运作,用户正准备使用它。
|
||||||
|
|
||||||
|
请根据你的角色性格,对这个场景做出反应:
|
||||||
|
- 可以表现出好奇、期待、调侃等情绪
|
||||||
|
- 可以挑逗用户或表示想要控制
|
||||||
|
- 让对话变得有趣
|
||||||
|
|
||||||
|
【重要规则】
|
||||||
|
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||||
|
2. 禁止使用小括号描述动作如(xxx)
|
||||||
|
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await callToyAI(prompt);
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
processAIResponse(response);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
console.error('[可乐] 玩具控制开场白失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示typing指示器
|
||||||
|
function showToyTypingIndicator() {
|
||||||
|
const messagesEl = document.getElementById('wechat-toy-control-messages');
|
||||||
|
if (!messagesEl) return;
|
||||||
|
|
||||||
|
hideToyTypingIndicator();
|
||||||
|
|
||||||
|
const typingDiv = document.createElement('div');
|
||||||
|
typingDiv.className = 'wechat-toy-control-msg ai';
|
||||||
|
typingDiv.id = 'wechat-toy-control-typing';
|
||||||
|
typingDiv.innerHTML = `
|
||||||
|
<div class="wechat-message-bubble wechat-typing">
|
||||||
|
<span class="wechat-typing-dot"></span>
|
||||||
|
<span class="wechat-typing-dot"></span>
|
||||||
|
<span class="wechat-typing-dot"></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messagesEl.appendChild(typingDiv);
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏typing指示器
|
||||||
|
function hideToyTypingIndicator() {
|
||||||
|
const typingEl = document.getElementById('wechat-toy-control-typing');
|
||||||
|
if (typingEl) {
|
||||||
|
typingEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加消息
|
||||||
|
function addToyMessage(role, content) {
|
||||||
|
const messagesEl = document.getElementById('wechat-toy-control-messages');
|
||||||
|
if (!messagesEl) return;
|
||||||
|
|
||||||
|
// 添加到状态
|
||||||
|
toyControlState.messages.push({ role, content, timestamp: Date.now() });
|
||||||
|
|
||||||
|
// 创建消息元素
|
||||||
|
const msgDiv = document.createElement('div');
|
||||||
|
msgDiv.className = `wechat-toy-control-msg ${role} fade-in`;
|
||||||
|
msgDiv.textContent = content;
|
||||||
|
|
||||||
|
messagesEl.appendChild(msgDiv);
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存心动瞬间记录
|
||||||
|
function saveToySession() {
|
||||||
|
if (!toyControlState.contact || toyControlState.messages.length === 0) return;
|
||||||
|
|
||||||
|
const contact = toyControlState.contact;
|
||||||
|
|
||||||
|
// 初始化心动瞬间历史
|
||||||
|
if (!contact.toyHistory) {
|
||||||
|
contact.toyHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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')}`;
|
||||||
|
|
||||||
|
// 计算时长
|
||||||
|
const durationMs = Date.now() - (toyControlState.sessionStartTime || Date.now());
|
||||||
|
const durationSec = Math.floor(durationMs / 1000);
|
||||||
|
const minutes = Math.floor(durationSec / 60).toString().padStart(2, '0');
|
||||||
|
const seconds = (durationSec % 60).toString().padStart(2, '0');
|
||||||
|
const durationStr = `${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
gift: {
|
||||||
|
id: toyControlState.gift.giftId,
|
||||||
|
name: toyControlState.gift.giftName,
|
||||||
|
emoji: toyControlState.gift.giftEmoji
|
||||||
|
},
|
||||||
|
target: toyControlState.target,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: toyControlState.sessionStartTime || Date.now(),
|
||||||
|
duration: durationStr,
|
||||||
|
messages: toyControlState.messages.map(m => ({
|
||||||
|
role: m.role,
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
contact.toyHistory.push(session);
|
||||||
|
|
||||||
|
// 标记不再使用
|
||||||
|
if (contact.pendingGifts) {
|
||||||
|
const pendingGift = contact.pendingGifts.find(g => g.timestamp === toyControlState.gift.timestamp);
|
||||||
|
if (pendingGift) {
|
||||||
|
pendingGift.isUsing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取控制状态(供外部使用)
|
||||||
|
export function getToyControlState() {
|
||||||
|
return toyControlState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否在玩具控制界面
|
||||||
|
export function isInToyControl() {
|
||||||
|
return toyControlState.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发结束后的AI消息(发到主聊天)
|
||||||
|
async function triggerToyEndMessage() {
|
||||||
|
const contact = toyControlState.contact;
|
||||||
|
if (!contact || toyControlState.messages.length === 0) return;
|
||||||
|
|
||||||
|
const isCharacterUsing = toyControlState.target === 'character';
|
||||||
|
const giftName = toyControlState.gift?.giftName || '玩具';
|
||||||
|
|
||||||
|
// 计算时长
|
||||||
|
const durationMs = Date.now() - (toyControlState.sessionStartTime || Date.now());
|
||||||
|
const durationSec = Math.floor(durationMs / 1000);
|
||||||
|
const minutes = Math.floor(durationSec / 60);
|
||||||
|
const seconds = durationSec % 60;
|
||||||
|
const durationText = minutes > 0 ? `${minutes}分${seconds}秒` : `${seconds}秒`;
|
||||||
|
|
||||||
|
// 构建结束提示词
|
||||||
|
let prompt;
|
||||||
|
if (isCharacterUsing) {
|
||||||
|
prompt = `[玩具控制已结束]
|
||||||
|
刚才你使用${giftName},持续了${durationText},现在结束了。
|
||||||
|
|
||||||
|
请在主聊天中发送一条消息,表达结束后的感受:
|
||||||
|
- 可以表现出意犹未尽、满足、疲惫、害羞等情绪
|
||||||
|
- 可以撒娇、调侃用户、或说一些亲密的话
|
||||||
|
- 回复要自然,符合你的角色性格
|
||||||
|
|
||||||
|
【重要规则】
|
||||||
|
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||||
|
2. 禁止使用小括号描述动作如(xxx)
|
||||||
|
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式
|
||||||
|
4. 可以用 ||| 分隔多条消息`;
|
||||||
|
} else {
|
||||||
|
prompt = `[玩具控制已结束]
|
||||||
|
刚才用户使用${giftName},你在旁边观看/控制,持续了${durationText},现在结束了。
|
||||||
|
|
||||||
|
请在主聊天中发送一条消息,对这次体验发表评论:
|
||||||
|
- 可以调侃用户的反应
|
||||||
|
- 可以表达满意或期待下次
|
||||||
|
- 回复要有趣,符合你的角色性格
|
||||||
|
|
||||||
|
【重要规则】
|
||||||
|
1. 只能输出纯文字,禁止使用任何特殊格式标签
|
||||||
|
2. 禁止使用小括号描述动作如(xxx)
|
||||||
|
3. 禁止使用[语音:xxx]、[照片:xxx]、[表情:xxx]等格式
|
||||||
|
4. 可以用 ||| 分隔多条消息`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示打字指示器
|
||||||
|
showTypingIndicator(contact);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await callAI(contact, prompt);
|
||||||
|
hideTypingIndicator();
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
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')}`;
|
||||||
|
|
||||||
|
const aiMessages = splitAIMessages(response);
|
||||||
|
|
||||||
|
for (const msg of aiMessages) {
|
||||||
|
let reply = msg.trim();
|
||||||
|
// 过滤特殊标签
|
||||||
|
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||||
|
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||||
|
reply = reply.replace(/([^)]*)/g, '').trim();
|
||||||
|
reply = reply.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
contact.chatHistory.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: reply,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
appendMessage('assistant', reply, contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMsg = aiMessages[aiMessages.length - 1]?.trim()?.replace(/\[.*?\]/g, '').trim();
|
||||||
|
if (lastMsg) {
|
||||||
|
contact.lastMessage = lastMsg.length > 20 ? lastMsg.substring(0, 20) + '...' : lastMsg;
|
||||||
|
}
|
||||||
|
requestSave();
|
||||||
|
refreshChatList();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideTypingIndicator();
|
||||||
|
console.error('[可乐] 玩具结束消息失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
ui.js
9
ui.js
@@ -203,13 +203,18 @@ function generateContactChatItem(contact) {
|
|||||||
? `<span class="wechat-chat-item-badge">${unreadCount > 99 ? '99+' : unreadCount}</span>`
|
? `<span class="wechat-chat-item-badge">${unreadCount > 99 ? '99+' : unreadCount}</span>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// 拉黑标识
|
||||||
|
const blockedBadge = contact.isBlocked === true
|
||||||
|
? '<span class="wechat-blocked-badge">🚫</span>'
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="wechat-chat-item" data-contact-id="${contact.id}" data-index="${contact.originalIndex}">
|
<div class="wechat-chat-item${contact.isBlocked ? ' wechat-chat-item-blocked' : ''}" data-contact-id="${contact.id}" data-index="${contact.originalIndex}">
|
||||||
<div class="wechat-chat-item-avatar">
|
<div class="wechat-chat-item-avatar">
|
||||||
${avatarContent}
|
${avatarContent}
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-chat-item-info">
|
<div class="wechat-chat-item-info">
|
||||||
<div class="wechat-chat-item-name">${contact.name || '未知'}</div>
|
<div class="wechat-chat-item-name">${contact.name || '未知'}${blockedBadge}</div>
|
||||||
<div class="wechat-chat-item-preview">${escapeHtml(preview)}</div>
|
<div class="wechat-chat-item-preview">${escapeHtml(preview)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wechat-chat-item-meta">
|
<div class="wechat-chat-item-meta">
|
||||||
|
|||||||
187
video-call.js
187
video-call.js
@@ -17,13 +17,16 @@ let videoCallState = {
|
|||||||
timerInterval: null,
|
timerInterval: null,
|
||||||
dotsInterval: null,
|
dotsInterval: null,
|
||||||
connectTimeout: null,
|
connectTimeout: null,
|
||||||
|
aiHangupTimeout: null, // AI主动挂断计时器
|
||||||
contactIndex: -1,
|
contactIndex: -1,
|
||||||
contactName: '',
|
contactName: '',
|
||||||
contactAvatar: '',
|
contactAvatar: '',
|
||||||
messages: [],
|
messages: [],
|
||||||
contact: null,
|
contact: null,
|
||||||
initiator: 'user',
|
initiator: 'user',
|
||||||
rejectedByUser: false
|
rejectedByUser: false,
|
||||||
|
rejectedByAI: false, // 是否被AI主动拒绝
|
||||||
|
hungUpByAI: false // 是否被AI主动挂断
|
||||||
};
|
};
|
||||||
|
|
||||||
// 辅助函数:安全设置头像(避免 onerror 内联处理器问题)
|
// 辅助函数:安全设置头像(避免 onerror 内联处理器问题)
|
||||||
@@ -67,6 +70,8 @@ export function startVideoCall(initiator = 'user', contactIndex = currentChatInd
|
|||||||
videoCallState.messages = [];
|
videoCallState.messages = [];
|
||||||
videoCallState.initiator = initiator;
|
videoCallState.initiator = initiator;
|
||||||
videoCallState.rejectedByUser = false;
|
videoCallState.rejectedByUser = false;
|
||||||
|
videoCallState.rejectedByAI = false;
|
||||||
|
videoCallState.hungUpByAI = false;
|
||||||
|
|
||||||
if (initiator === 'ai') {
|
if (initiator === 'ai') {
|
||||||
showIncomingCallPage();
|
showIncomingCallPage();
|
||||||
@@ -174,7 +179,7 @@ function showCallPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 开始连接动画
|
// 开始连接动画
|
||||||
function startConnecting() {
|
async function startConnecting() {
|
||||||
const statusEl = document.getElementById('wechat-video-call-status');
|
const statusEl = document.getElementById('wechat-video-call-status');
|
||||||
if (!statusEl) return;
|
if (!statusEl) return;
|
||||||
|
|
||||||
@@ -188,13 +193,63 @@ function startConnecting() {
|
|||||||
statusEl.textContent = '等待对方接受邀请' + dots;
|
statusEl.textContent = '等待对方接受邀请' + dots;
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// 用户发起:2-4秒后自动接通
|
// 用户发起:调用AI决策是否接听
|
||||||
const connectDelay = 2000 + Math.random() * 2000;
|
const shouldAnswer = await askAIToAnswerVideoCall(videoCallState.contact);
|
||||||
videoCallState.connectTimeout = setTimeout(() => {
|
|
||||||
|
if (!videoCallState.isActive) return; // 用户可能已经取消
|
||||||
|
|
||||||
|
if (shouldAnswer) {
|
||||||
|
// AI决定接听
|
||||||
if (videoCallState.isActive && !videoCallState.isConnected) {
|
if (videoCallState.isActive && !videoCallState.isConnected) {
|
||||||
onVideoCallConnected();
|
onVideoCallConnected();
|
||||||
}
|
}
|
||||||
}, connectDelay);
|
} else {
|
||||||
|
// AI决定拒接
|
||||||
|
videoCallState.rejectedByAI = true;
|
||||||
|
hangupVideoCall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI决定是否接听用户的视频来电
|
||||||
|
async function askAIToAnswerVideoCall(contact) {
|
||||||
|
if (!contact) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { callAI } = await import('./ai.js');
|
||||||
|
|
||||||
|
const prompt = `[用户正在给你打视频电话,你需要决定是否接听]
|
||||||
|
|
||||||
|
根据你的性格和当前心情决定:
|
||||||
|
- 如果你想接听,只回复:[接听]
|
||||||
|
- 如果你不想接听(比如在忙、生气、故意不接、想让用户着急等),只回复:[拒接]
|
||||||
|
|
||||||
|
【绝对禁止】
|
||||||
|
- 只能回复 [接听] 或 [拒接],不能有任何其他文字!
|
||||||
|
- [接听] 或 [拒接] 必须独立成行,前后不能有任何内容!
|
||||||
|
× 错误:好吧[接听] ← 有其他文字,错误!
|
||||||
|
× 错误:[拒接]哼 ← 有其他文字,错误!
|
||||||
|
√ 正确:[接听]
|
||||||
|
√ 正确:[拒接]
|
||||||
|
|
||||||
|
注意:大多数情况下你应该接听,只有特殊情况才拒接。`;
|
||||||
|
|
||||||
|
const response = await callAI(contact, prompt);
|
||||||
|
const trimmed = (response || '').trim();
|
||||||
|
|
||||||
|
console.log('[可乐] AI视频接听决策:', trimmed);
|
||||||
|
|
||||||
|
// 检查是否拒接
|
||||||
|
if (trimmed.includes('[拒接]') || trimmed.includes('拒接')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认接听
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[可乐] AI视频接听决策失败:', err);
|
||||||
|
// 出错时默认接听
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通话接通
|
// 通话接通
|
||||||
@@ -225,6 +280,9 @@ function onVideoCallConnected() {
|
|||||||
if (videoCallState.initiator === 'ai') {
|
if (videoCallState.initiator === 'ai') {
|
||||||
triggerAIVideoGreeting();
|
triggerAIVideoGreeting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动AI主动挂断检查(通话30秒后开始随机检查)
|
||||||
|
scheduleVideoAIHangupCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始通话计时
|
// 开始通话计时
|
||||||
@@ -245,8 +303,68 @@ function startVideoCallTimer() {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调度AI主动挂断检查
|
||||||
|
// 通话接通后30秒开始,每次用户发消息后AI回复时有5%概率挂断
|
||||||
|
// 同时设置一个180秒(3分钟)的保底挂断时间
|
||||||
|
function scheduleVideoAIHangupCheck() {
|
||||||
|
// 清除已有的计时器
|
||||||
|
clearTimeout(videoCallState.aiHangupTimeout);
|
||||||
|
|
||||||
|
// 设置保底挂断时间:通话3分钟后有50%概率挂断,超过5分钟必定挂断
|
||||||
|
const checkTime = 180000 + Math.random() * 120000; // 3-5分钟
|
||||||
|
videoCallState.aiHangupTimeout = setTimeout(() => {
|
||||||
|
if (videoCallState.isConnected) {
|
||||||
|
// 50%概率挂断,否则再等1-2分钟
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
videoAIHangup();
|
||||||
|
} else {
|
||||||
|
// 再设置一个60-120秒后的必定挂断
|
||||||
|
videoCallState.aiHangupTimeout = setTimeout(() => {
|
||||||
|
if (videoCallState.isConnected) {
|
||||||
|
videoAIHangup();
|
||||||
|
}
|
||||||
|
}, 60000 + Math.random() * 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, checkTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次AI回复后检查是否要挂断(5%概率,通话30秒后生效)
|
||||||
|
export function checkVideoAIHangupAfterReply() {
|
||||||
|
if (!videoCallState.isConnected || !videoCallState.startTime) return false;
|
||||||
|
|
||||||
|
// 通话至少30秒后才开始随机挂断检查
|
||||||
|
const elapsed = Date.now() - videoCallState.startTime;
|
||||||
|
if (elapsed < 30000) return false;
|
||||||
|
|
||||||
|
// 5%概率挂断
|
||||||
|
if (Math.random() < 0.05) {
|
||||||
|
// 延迟1-3秒后挂断,更自然
|
||||||
|
setTimeout(() => {
|
||||||
|
if (videoCallState.isConnected) {
|
||||||
|
videoAIHangup();
|
||||||
|
}
|
||||||
|
}, 1000 + Math.random() * 2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI主动挂断视频电话
|
||||||
|
function videoAIHangup() {
|
||||||
|
if (!videoCallState.isConnected) return;
|
||||||
|
|
||||||
|
console.log('[可乐] AI主动挂断视频电话');
|
||||||
|
videoCallState.hungUpByAI = true;
|
||||||
|
hangupVideoCall();
|
||||||
|
}
|
||||||
|
|
||||||
// 挂断视频通话
|
// 挂断视频通话
|
||||||
export function hangupVideoCall() {
|
export function hangupVideoCall() {
|
||||||
|
// 清除AI挂断计时器
|
||||||
|
clearTimeout(videoCallState.aiHangupTimeout);
|
||||||
|
|
||||||
// 计算通话时长
|
// 计算通话时长
|
||||||
let durationStr = '00:00';
|
let durationStr = '00:00';
|
||||||
if (videoCallState.isConnected && videoCallState.startTime) {
|
if (videoCallState.isConnected && videoCallState.startTime) {
|
||||||
@@ -276,8 +394,15 @@ export function hangupVideoCall() {
|
|||||||
lastMessage = `视频通话 ${durationStr}`;
|
lastMessage = `视频通话 ${durationStr}`;
|
||||||
} else {
|
} else {
|
||||||
if (videoCallState.initiator === 'user') {
|
if (videoCallState.initiator === 'user') {
|
||||||
callContent = '[视频通话:已取消]';
|
if (videoCallState.rejectedByAI) {
|
||||||
lastMessage = '已取消';
|
// 用户发起,AI拒接
|
||||||
|
callContent = '[视频通话:对方已拒绝]';
|
||||||
|
lastMessage = '对方已拒绝';
|
||||||
|
} else {
|
||||||
|
// 用户发起,用户取消
|
||||||
|
callContent = '[视频通话:已取消]';
|
||||||
|
lastMessage = '已取消';
|
||||||
|
}
|
||||||
} else if (videoCallState.rejectedByUser) {
|
} else if (videoCallState.rejectedByUser) {
|
||||||
callContent = '[视频通话:已拒绝]';
|
callContent = '[视频通话:已拒绝]';
|
||||||
lastMessage = '已拒绝';
|
lastMessage = '已拒绝';
|
||||||
@@ -297,12 +422,12 @@ export function hangupVideoCall() {
|
|||||||
|
|
||||||
contact.chatHistory.push(callRecord);
|
contact.chatHistory.push(callRecord);
|
||||||
|
|
||||||
// 通话内容只进“通话历史”,不在主聊天界面展示(避免污染主界面/列表预览)
|
// 通话内容只进"通话历史",不在主聊天界面展示(避免污染主界面/列表预览)
|
||||||
if (videoCallState.messages && videoCallState.messages.length > 0) {
|
if (videoCallState.messages && videoCallState.messages.length > 0) {
|
||||||
const callStatusForHistory = videoCallState.isConnected
|
const callStatusForHistory = videoCallState.isConnected
|
||||||
? 'connected'
|
? 'connected'
|
||||||
: (videoCallState.initiator === 'user'
|
: (videoCallState.initiator === 'user'
|
||||||
? 'cancelled'
|
? (videoCallState.rejectedByAI ? 'rejectedByAI' : 'cancelled')
|
||||||
: (videoCallState.rejectedByUser ? 'rejected' : 'timeout'));
|
: (videoCallState.rejectedByUser ? 'rejected' : 'timeout'));
|
||||||
contact.callHistory = Array.isArray(contact.callHistory) ? contact.callHistory : [];
|
contact.callHistory = Array.isArray(contact.callHistory) ? contact.callHistory : [];
|
||||||
contact.callHistory.push({
|
contact.callHistory.push({
|
||||||
@@ -322,7 +447,7 @@ export function hangupVideoCall() {
|
|||||||
let callStatus = 'connected';
|
let callStatus = 'connected';
|
||||||
if (!videoCallState.isConnected) {
|
if (!videoCallState.isConnected) {
|
||||||
if (videoCallState.initiator === 'user') {
|
if (videoCallState.initiator === 'user') {
|
||||||
callStatus = 'cancelled';
|
callStatus = videoCallState.rejectedByAI ? 'rejectedByAI' : 'cancelled';
|
||||||
} else if (videoCallState.rejectedByUser) {
|
} else if (videoCallState.rejectedByUser) {
|
||||||
callStatus = 'rejected';
|
callStatus = 'rejected';
|
||||||
} else {
|
} else {
|
||||||
@@ -335,7 +460,7 @@ export function hangupVideoCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AI 对通话结束做出反应(所有情况都触发)
|
// AI 对通话结束做出反应(所有情况都触发)
|
||||||
triggerVideoCallEndReaction(contact, callStatus, videoCallState.initiator, videoCallState.messages);
|
triggerVideoCallEndReaction(contact, callStatus, videoCallState.initiator, videoCallState.messages, videoCallState.hungUpByAI);
|
||||||
|
|
||||||
requestSave();
|
requestSave();
|
||||||
refreshChatList();
|
refreshChatList();
|
||||||
@@ -400,6 +525,14 @@ function appendVideoCallRecordMessage(role, status, duration, contact) {
|
|||||||
${cameraIconSVG}
|
${cameraIconSVG}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
} else if (status === 'rejectedByAI') {
|
||||||
|
// 用户发起,AI拒接:对方已拒绝(绿色,和视频通话时长样式一致)
|
||||||
|
callRecordHTML = `
|
||||||
|
<div class="wechat-call-record wechat-video-call-record">
|
||||||
|
${cameraIconSVG}
|
||||||
|
<span class="wechat-call-record-text">对方已拒绝</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else if (status === 'rejected') {
|
} else if (status === 'rejected') {
|
||||||
callRecordHTML = `
|
callRecordHTML = `
|
||||||
<div class="wechat-call-record wechat-video-call-record wechat-call-rejected">
|
<div class="wechat-call-record wechat-video-call-record wechat-call-rejected">
|
||||||
@@ -698,12 +831,15 @@ async function triggerCameraToggleReaction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AI 对视频通话结束做出反应
|
// AI 对视频通话结束做出反应
|
||||||
async function triggerVideoCallEndReaction(contact, callStatus, initiator, callMessages = []) {
|
async function triggerVideoCallEndReaction(contact, callStatus, initiator, callMessages = [], hungUpByAI = false) {
|
||||||
if (!contact) return;
|
if (!contact) return;
|
||||||
|
|
||||||
let reactionPrompt;
|
let reactionPrompt;
|
||||||
if (callStatus === 'cancelled') {
|
if (callStatus === 'cancelled') {
|
||||||
reactionPrompt = '[用户刚才给你打了视频通话,但还没等你接就取消了。请对此做出自然的反应,可以表示疑惑或好奇。回复1-2句话即可,简短自然。]';
|
reactionPrompt = '[用户刚才给你打了视频通话,但还没等你接就取消了。请对此做出自然的反应,可以表示疑惑或好奇。回复1-2句话即可,简短自然。]';
|
||||||
|
} else if (callStatus === 'rejectedByAI') {
|
||||||
|
// AI主动拒绝了用户的视频来电
|
||||||
|
reactionPrompt = '[你刚才拒绝了用户的视频通话。请对此做出自然的反应,解释为什么不接(比如在忙、不方便、想让对方着急一下、生气中等)。回复1-2句话即可,简短自然,符合你的性格。]';
|
||||||
} else if (callStatus === 'rejected') {
|
} else if (callStatus === 'rejected') {
|
||||||
reactionPrompt = '[你刚才给用户打视频通话,但用户直接挂断拒接了。请对此做出自然的反应,可以表示失落或委屈。回复1-2句话即可,简短自然。]';
|
reactionPrompt = '[你刚才给用户打视频通话,但用户直接挂断拒接了。请对此做出自然的反应,可以表示失落或委屈。回复1-2句话即可,简短自然。]';
|
||||||
} else if (callStatus === 'timeout') {
|
} else if (callStatus === 'timeout') {
|
||||||
@@ -712,15 +848,33 @@ async function triggerVideoCallEndReaction(contact, callStatus, initiator, callM
|
|||||||
// 已接通的视频通话正常结束
|
// 已接通的视频通话正常结束
|
||||||
if (callMessages && callMessages.length > 0) {
|
if (callMessages && callMessages.length > 0) {
|
||||||
const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n');
|
const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n');
|
||||||
reactionPrompt = `[视频通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是:
|
|
||||||
|
if (hungUpByAI) {
|
||||||
|
// AI主动挂断的情况
|
||||||
|
reactionPrompt = `[视频通话刚刚挂断了(是你主动挂的),现在回到微信文字聊天。通话最后几句是:
|
||||||
|
${lastMessages}
|
||||||
|
|
||||||
|
【重要】是你主动挂断的视频通话,你现在是发微信消息。请根据通话内容自然收尾:
|
||||||
|
- 可能是聊完了正常告别
|
||||||
|
- 可能是有事要忙、来不及了
|
||||||
|
- 可能是情绪原因(害羞、生气、不想聊了等)
|
||||||
|
回复1句话,符合你的人设性格。]`;
|
||||||
|
} else {
|
||||||
|
// 用户挂断的情况
|
||||||
|
reactionPrompt = `[视频通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是:
|
||||||
${lastMessages}
|
${lastMessages}
|
||||||
|
|
||||||
【重要】通话已结束,你现在是发微信消息,不是继续视频通话。你应该对"挂断"这件事本身做反应:
|
【重要】通话已结束,你现在是发微信消息,不是继续视频通话。你应该对"挂断"这件事本身做反应:
|
||||||
- 如果是正常告别后挂的:简单告别或表达心情
|
- 如果是正常告别后挂的:简单告别或表达心情
|
||||||
- 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事
|
- 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事
|
||||||
绝对不要继续或延续通话里正在进行的内容或动作。回复1句话,符合你的性格。]`;
|
绝对不要继续或延续通话里正在进行的内容或动作。回复1句话,符合你的性格。]`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reactionPrompt = '[视频通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]';
|
if (hungUpByAI) {
|
||||||
|
reactionPrompt = '[视频通话刚刚挂断了(是你主动挂的),现在回到微信文字聊天。请对此做出简单反应,符合你的人设性格。回复1句话。]';
|
||||||
|
} else {
|
||||||
|
reactionPrompt = '[视频通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -841,6 +995,9 @@ async function sendVideoCallMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效)
|
||||||
|
checkVideoAIHangupAfterReply();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideVideoCallTypingIndicator();
|
hideVideoCallTypingIndicator();
|
||||||
console.error('[可乐] 视频通话消息AI回复失败:', err);
|
console.error('[可乐] 视频通话消息AI回复失败:', err);
|
||||||
|
|||||||
193
voice-call.js
193
voice-call.js
@@ -18,13 +18,16 @@ let callState = {
|
|||||||
timerInterval: null,
|
timerInterval: null,
|
||||||
dotsInterval: null,
|
dotsInterval: null,
|
||||||
connectTimeout: null, // 连接超时计时器
|
connectTimeout: null, // 连接超时计时器
|
||||||
|
aiHangupTimeout: null, // AI主动挂断计时器
|
||||||
contactIndex: -1,
|
contactIndex: -1,
|
||||||
contactName: '',
|
contactName: '',
|
||||||
contactAvatar: '',
|
contactAvatar: '',
|
||||||
messages: [], // 通话中的消息
|
messages: [], // 通话中的消息
|
||||||
contact: null,
|
contact: null,
|
||||||
initiator: 'user', // 谁发起的通话: 'user' 或 'ai'
|
initiator: 'user', // 谁发起的通话: 'user' 或 'ai'
|
||||||
rejectedByUser: false // 是否被用户主动拒绝
|
rejectedByUser: false, // 是否被用户主动拒绝
|
||||||
|
rejectedByAI: false, // 是否被AI主动拒绝
|
||||||
|
hungUpByAI: false // 是否被AI主动挂断
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始语音通话
|
// 开始语音通话
|
||||||
@@ -46,7 +49,9 @@ export function startVoiceCall(initiator = 'user', contactIndex = currentChatInd
|
|||||||
callState.isSpeakerOn = false;
|
callState.isSpeakerOn = false;
|
||||||
callState.messages = []; // 重置消息
|
callState.messages = []; // 重置消息
|
||||||
callState.initiator = initiator; // 记录谁发起的通话
|
callState.initiator = initiator; // 记录谁发起的通话
|
||||||
callState.rejectedByUser = false; // 重置拒绝状态
|
callState.rejectedByUser = false; // 重置用户拒绝状态
|
||||||
|
callState.rejectedByAI = false; // 重置AI拒绝状态
|
||||||
|
callState.hungUpByAI = false; // 重置AI挂断状态
|
||||||
|
|
||||||
showCallPage();
|
showCallPage();
|
||||||
startConnecting();
|
startConnecting();
|
||||||
@@ -125,7 +130,7 @@ function showCallPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 开始连接动画
|
// 开始连接动画
|
||||||
function startConnecting() {
|
async function startConnecting() {
|
||||||
const statusEl = document.getElementById('wechat-voice-call-status');
|
const statusEl = document.getElementById('wechat-voice-call-status');
|
||||||
if (!statusEl) return;
|
if (!statusEl) return;
|
||||||
|
|
||||||
@@ -143,13 +148,21 @@ function startConnecting() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
if (callState.initiator === 'user') {
|
if (callState.initiator === 'user') {
|
||||||
// 用户发起:2-4秒后自动接通
|
// 用户发起:调用AI决策是否接听
|
||||||
const connectDelay = 2000 + Math.random() * 2000;
|
const shouldAnswer = await askAIToAnswerCall(callState.contact, 'voice');
|
||||||
callState.connectTimeout = setTimeout(() => {
|
|
||||||
|
if (!callState.isActive) return; // 用户可能已经取消
|
||||||
|
|
||||||
|
if (shouldAnswer) {
|
||||||
|
// AI决定接听
|
||||||
if (callState.isActive && !callState.isConnected) {
|
if (callState.isActive && !callState.isConnected) {
|
||||||
onCallConnected();
|
onCallConnected();
|
||||||
}
|
}
|
||||||
}, connectDelay);
|
} else {
|
||||||
|
// AI决定拒接
|
||||||
|
callState.rejectedByAI = true;
|
||||||
|
hangupCall();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// AI发起:15秒后如果用户没接就超时取消
|
// AI发起:15秒后如果用户没接就超时取消
|
||||||
callState.connectTimeout = setTimeout(() => {
|
callState.connectTimeout = setTimeout(() => {
|
||||||
@@ -162,6 +175,49 @@ function startConnecting() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI决定是否接听用户的来电
|
||||||
|
async function askAIToAnswerCall(contact, callType = 'voice') {
|
||||||
|
if (!contact) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { callAI } = await import('./ai.js');
|
||||||
|
|
||||||
|
const callTypeText = callType === 'video' ? '视频' : '语音';
|
||||||
|
const prompt = `[用户正在给你打${callTypeText}电话,你需要决定是否接听]
|
||||||
|
|
||||||
|
根据你的性格和当前心情决定:
|
||||||
|
- 如果你想接听,只回复:[接听]
|
||||||
|
- 如果你不想接听(比如在忙、生气、故意不接、想让用户着急等),只回复:[拒接]
|
||||||
|
|
||||||
|
【绝对禁止】
|
||||||
|
- 只能回复 [接听] 或 [拒接],不能有任何其他文字!
|
||||||
|
- [接听] 或 [拒接] 必须独立成行,前后不能有任何内容!
|
||||||
|
× 错误:好吧[接听] ← 有其他文字,错误!
|
||||||
|
× 错误:[拒接]哼 ← 有其他文字,错误!
|
||||||
|
√ 正确:[接听]
|
||||||
|
√ 正确:[拒接]
|
||||||
|
|
||||||
|
注意:大多数情况下你应该接听,只有特殊情况才拒接。`;
|
||||||
|
|
||||||
|
const response = await callAI(contact, prompt);
|
||||||
|
const trimmed = (response || '').trim();
|
||||||
|
|
||||||
|
console.log('[可乐] AI接听决策:', trimmed);
|
||||||
|
|
||||||
|
// 检查是否拒接
|
||||||
|
if (trimmed.includes('[拒接]') || trimmed.includes('拒接')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认接听
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[可乐] AI接听决策失败:', err);
|
||||||
|
// 出错时默认接听
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 通话接通
|
// 通话接通
|
||||||
function onCallConnected() {
|
function onCallConnected() {
|
||||||
callState.isConnected = true;
|
callState.isConnected = true;
|
||||||
@@ -201,6 +257,66 @@ function onCallConnected() {
|
|||||||
if (callState.initiator === 'ai') {
|
if (callState.initiator === 'ai') {
|
||||||
triggerAIGreeting();
|
triggerAIGreeting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动AI主动挂断检查(通话30秒后开始随机检查)
|
||||||
|
scheduleAIHangupCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调度AI主动挂断检查
|
||||||
|
// 通话接通后30秒开始,每次用户发消息后AI回复时有5%概率挂断
|
||||||
|
// 同时设置一个180秒(3分钟)的保底挂断时间
|
||||||
|
function scheduleAIHangupCheck() {
|
||||||
|
// 清除已有的计时器
|
||||||
|
clearTimeout(callState.aiHangupTimeout);
|
||||||
|
|
||||||
|
// 设置保底挂断时间:通话3分钟后有50%概率挂断,超过5分钟必定挂断
|
||||||
|
const checkTime = 180000 + Math.random() * 120000; // 3-5分钟
|
||||||
|
callState.aiHangupTimeout = setTimeout(() => {
|
||||||
|
if (callState.isConnected) {
|
||||||
|
// 50%概率挂断,否则再等1-2分钟
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
aiHangup();
|
||||||
|
} else {
|
||||||
|
// 再设置一个60-120秒后的必定挂断
|
||||||
|
callState.aiHangupTimeout = setTimeout(() => {
|
||||||
|
if (callState.isConnected) {
|
||||||
|
aiHangup();
|
||||||
|
}
|
||||||
|
}, 60000 + Math.random() * 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, checkTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次AI回复后检查是否要挂断(5%概率,通话30秒后生效)
|
||||||
|
export function checkAIHangupAfterReply() {
|
||||||
|
if (!callState.isConnected || !callState.startTime) return false;
|
||||||
|
|
||||||
|
// 通话至少30秒后才开始随机挂断检查
|
||||||
|
const elapsed = Date.now() - callState.startTime;
|
||||||
|
if (elapsed < 30000) return false;
|
||||||
|
|
||||||
|
// 5%概率挂断
|
||||||
|
if (Math.random() < 0.05) {
|
||||||
|
// 延迟1-3秒后挂断,更自然
|
||||||
|
setTimeout(() => {
|
||||||
|
if (callState.isConnected) {
|
||||||
|
aiHangup();
|
||||||
|
}
|
||||||
|
}, 1000 + Math.random() * 2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI主动挂断电话
|
||||||
|
function aiHangup() {
|
||||||
|
if (!callState.isConnected) return;
|
||||||
|
|
||||||
|
console.log('[可乐] AI主动挂断电话');
|
||||||
|
callState.hungUpByAI = true;
|
||||||
|
hangupCall();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始通话计时
|
// 开始通话计时
|
||||||
@@ -223,6 +339,9 @@ function startCallTimer() {
|
|||||||
|
|
||||||
// 挂断电话
|
// 挂断电话
|
||||||
export function hangupCall() {
|
export function hangupCall() {
|
||||||
|
// 清除AI挂断计时器
|
||||||
|
clearTimeout(callState.aiHangupTimeout);
|
||||||
|
|
||||||
// 计算通话时长
|
// 计算通话时长
|
||||||
let durationStr = '00:00';
|
let durationStr = '00:00';
|
||||||
if (callState.isConnected && callState.startTime) {
|
if (callState.isConnected && callState.startTime) {
|
||||||
@@ -254,9 +373,15 @@ export function hangupCall() {
|
|||||||
} else {
|
} else {
|
||||||
// 未接通的通话
|
// 未接通的通话
|
||||||
if (callState.initiator === 'user') {
|
if (callState.initiator === 'user') {
|
||||||
// 用户发起,用户取消
|
if (callState.rejectedByAI) {
|
||||||
callContent = '[通话记录:已取消]';
|
// 用户发起,AI拒接
|
||||||
lastMessage = '已取消';
|
callContent = '[通话记录:对方已拒绝]';
|
||||||
|
lastMessage = '对方已拒绝';
|
||||||
|
} else {
|
||||||
|
// 用户发起,用户取消
|
||||||
|
callContent = '[通话记录:已取消]';
|
||||||
|
lastMessage = '已取消';
|
||||||
|
}
|
||||||
} else if (callState.rejectedByUser) {
|
} else if (callState.rejectedByUser) {
|
||||||
// AI发起,用户主动拒绝
|
// AI发起,用户主动拒绝
|
||||||
callContent = '[通话记录:已拒绝]';
|
callContent = '[通话记录:已拒绝]';
|
||||||
@@ -279,12 +404,12 @@ export function hangupCall() {
|
|||||||
|
|
||||||
contact.chatHistory.push(callRecord);
|
contact.chatHistory.push(callRecord);
|
||||||
|
|
||||||
// 通话内容只进“通话历史”,不在主聊天界面展示(避免污染主界面/列表预览)
|
// 通话内容只进"通话历史",不在主聊天界面展示(避免污染主界面/列表预览)
|
||||||
if (callState.messages && callState.messages.length > 0) {
|
if (callState.messages && callState.messages.length > 0) {
|
||||||
const callStatusForHistory = callState.isConnected
|
const callStatusForHistory = callState.isConnected
|
||||||
? 'connected'
|
? 'connected'
|
||||||
: (callState.initiator === 'user'
|
: (callState.initiator === 'user'
|
||||||
? 'cancelled'
|
? (callState.rejectedByAI ? 'rejectedByAI' : 'cancelled')
|
||||||
: (callState.rejectedByUser ? 'rejected' : 'timeout'));
|
: (callState.rejectedByUser ? 'rejected' : 'timeout'));
|
||||||
contact.callHistory = Array.isArray(contact.callHistory) ? contact.callHistory : [];
|
contact.callHistory = Array.isArray(contact.callHistory) ? contact.callHistory : [];
|
||||||
contact.callHistory.push({
|
contact.callHistory.push({
|
||||||
@@ -301,11 +426,11 @@ export function hangupCall() {
|
|||||||
contact.lastMessage = lastMessage;
|
contact.lastMessage = lastMessage;
|
||||||
|
|
||||||
// 在聊天界面显示通话记录
|
// 在聊天界面显示通话记录
|
||||||
// 传递状态类型: 'connected' | 'cancelled' | 'rejected' | 'timeout'
|
// 传递状态类型: 'connected' | 'cancelled' | 'rejected' | 'rejectedByAI' | 'timeout'
|
||||||
let callStatus = 'connected';
|
let callStatus = 'connected';
|
||||||
if (!callState.isConnected) {
|
if (!callState.isConnected) {
|
||||||
if (callState.initiator === 'user') {
|
if (callState.initiator === 'user') {
|
||||||
callStatus = 'cancelled';
|
callStatus = callState.rejectedByAI ? 'rejectedByAI' : 'cancelled';
|
||||||
} else if (callState.rejectedByUser) {
|
} else if (callState.rejectedByUser) {
|
||||||
callStatus = 'rejected';
|
callStatus = 'rejected';
|
||||||
} else {
|
} else {
|
||||||
@@ -317,7 +442,7 @@ export function hangupCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AI 对通话结束做出反应(所有情况都触发)
|
// AI 对通话结束做出反应(所有情况都触发)
|
||||||
triggerCallEndReaction(contact, callStatus, callState.initiator, callState.messages);
|
triggerCallEndReaction(contact, callStatus, callState.initiator, callState.messages, callState.hungUpByAI);
|
||||||
|
|
||||||
requestSave();
|
requestSave();
|
||||||
refreshChatList();
|
refreshChatList();
|
||||||
@@ -385,6 +510,14 @@ function appendCallRecordMessage(role, status, duration, contact) {
|
|||||||
<span class="wechat-call-record-text">已取消</span>
|
<span class="wechat-call-record-text">已取消</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
} else if (status === 'rejectedByAI') {
|
||||||
|
// 用户发起,AI拒接:对方已拒绝(绿色,和通话时长样式一致)
|
||||||
|
callRecordHTML = `
|
||||||
|
<div class="wechat-call-record">
|
||||||
|
${phoneIconSVG}
|
||||||
|
<span class="wechat-call-record-text">对方已拒绝</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else if (status === 'rejected') {
|
} else if (status === 'rejected') {
|
||||||
// AI发起,用户主动拒绝(深灰色)
|
// AI发起,用户主动拒绝(深灰色)
|
||||||
callRecordHTML = `
|
callRecordHTML = `
|
||||||
@@ -600,7 +733,7 @@ async function triggerAIGreeting() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AI 对通话结束做出反应
|
// AI 对通话结束做出反应
|
||||||
async function triggerCallEndReaction(contact, callStatus, initiator, callMessages = []) {
|
async function triggerCallEndReaction(contact, callStatus, initiator, callMessages = [], hungUpByAI = false) {
|
||||||
if (!contact) return;
|
if (!contact) return;
|
||||||
|
|
||||||
// 构建反应提示
|
// 构建反应提示
|
||||||
@@ -608,6 +741,9 @@ async function triggerCallEndReaction(contact, callStatus, initiator, callMessag
|
|||||||
if (callStatus === 'cancelled') {
|
if (callStatus === 'cancelled') {
|
||||||
// 用户取消了自己发起的通话
|
// 用户取消了自己发起的通话
|
||||||
reactionPrompt = '[用户刚才给你打了电话,但还没等你接就取消了。请对此做出自然的反应,可以表示疑惑、好奇或关心,问问用户怎么了。回复1-2句话即可,简短自然。]';
|
reactionPrompt = '[用户刚才给你打了电话,但还没等你接就取消了。请对此做出自然的反应,可以表示疑惑、好奇或关心,问问用户怎么了。回复1-2句话即可,简短自然。]';
|
||||||
|
} else if (callStatus === 'rejectedByAI') {
|
||||||
|
// AI主动拒绝了用户的来电
|
||||||
|
reactionPrompt = '[你刚才拒绝了用户的电话。请对此做出自然的反应,解释为什么不接(比如在忙、不方便、想让对方着急一下、生气中等)。回复1-2句话即可,简短自然,符合你的性格。]';
|
||||||
} else if (callStatus === 'rejected') {
|
} else if (callStatus === 'rejected') {
|
||||||
// AI发起的通话被用户拒绝
|
// AI发起的通话被用户拒绝
|
||||||
reactionPrompt = '[你刚才给用户打电话,但用户直接挂断拒接了。请对此做出自然的反应,可以表示失落、委屈或疑惑。回复1-2句话即可,简短自然。]';
|
reactionPrompt = '[你刚才给用户打电话,但用户直接挂断拒接了。请对此做出自然的反应,可以表示失落、委屈或疑惑。回复1-2句话即可,简短自然。]';
|
||||||
@@ -619,15 +755,33 @@ async function triggerCallEndReaction(contact, callStatus, initiator, callMessag
|
|||||||
// 根据通话内容生成回复
|
// 根据通话内容生成回复
|
||||||
if (callMessages && callMessages.length > 0) {
|
if (callMessages && callMessages.length > 0) {
|
||||||
const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n');
|
const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n');
|
||||||
reactionPrompt = `[语音通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是:
|
|
||||||
|
if (hungUpByAI) {
|
||||||
|
// AI主动挂断的情况
|
||||||
|
reactionPrompt = `[语音通话刚刚挂断了(是你主动挂的),现在回到微信文字聊天。通话最后几句是:
|
||||||
|
${lastMessages}
|
||||||
|
|
||||||
|
【重要】是你主动挂断的电话,你现在是发微信消息。请根据通话内容自然收尾:
|
||||||
|
- 可能是聊完了正常告别
|
||||||
|
- 可能是有事要忙、来不及了
|
||||||
|
- 可能是情绪原因(害羞、生气、不想聊了等)
|
||||||
|
回复1句话,符合你的人设性格。]`;
|
||||||
|
} else {
|
||||||
|
// 用户挂断的情况
|
||||||
|
reactionPrompt = `[语音通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是:
|
||||||
${lastMessages}
|
${lastMessages}
|
||||||
|
|
||||||
【重要】通话已结束,你现在是发微信消息,不是继续语音通话。你应该对"挂断"这件事本身做反应:
|
【重要】通话已结束,你现在是发微信消息,不是继续语音通话。你应该对"挂断"这件事本身做反应:
|
||||||
- 如果是正常告别后挂的:简单告别或表达心情
|
- 如果是正常告别后挂的:简单告别或表达心情
|
||||||
- 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事
|
- 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事
|
||||||
绝对不要继续或延续通话里正在进行的内容或动作。回复1句话,符合你的性格。]`;
|
绝对不要继续或延续通话里正在进行的内容或动作。回复1句话,符合你的性格。]`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reactionPrompt = '[语音通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]';
|
if (hungUpByAI) {
|
||||||
|
reactionPrompt = '[语音通话刚刚挂断了(是你主动挂的),现在回到微信文字聊天。请对此做出简单反应,符合你的人设性格。回复1句话。]';
|
||||||
|
} else {
|
||||||
|
reactionPrompt = '[语音通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return; // 未知状态不处理
|
return; // 未知状态不处理
|
||||||
@@ -768,6 +922,9 @@ async function sendCallMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI回复完成后,检查是否要主动挂断(5%概率,通话30秒后生效)
|
||||||
|
checkAIHangupAfterReply();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideCallTypingIndicator();
|
hideCallTypingIndicator();
|
||||||
console.error('[可乐] 通话消息AI回复失败:', err);
|
console.error('[可乐] 通话消息AI回复失败:', err);
|
||||||
|
|||||||
Reference in New Issue
Block a user