mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 11:55:51 +00:00
Compare commits
5 Commits
40526f614d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262611c736 | ||
|
|
4a097a613b | ||
|
|
8595a7c48d | ||
|
|
f07e0914f0 | ||
|
|
5068b46702 |
11
ai.js
11
ai.js
@@ -258,6 +258,17 @@ export async function testApiConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试指定 API 连接(接受参数)
|
||||
export async function testConnection(apiUrl, apiKey, model) {
|
||||
if (!apiUrl) {
|
||||
throw new Error('请先配置 API 地址');
|
||||
}
|
||||
|
||||
// 尝试获取模型列表来验证连接
|
||||
await fetchModelListFromApi(apiUrl, apiKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取模型列表
|
||||
export async function fetchModelList() {
|
||||
const config = getApiConfig();
|
||||
|
||||
@@ -275,3 +275,51 @@ export async function getStorageStats() {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有语音记录,按联系人分组
|
||||
* @returns {Promise<Object>} { contactIndex: { count, totalDuration } }
|
||||
*/
|
||||
export async function getAllVoiceRecordingsGroupedByContact() {
|
||||
await initAudioDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const records = request.result;
|
||||
const grouped = {};
|
||||
|
||||
records.forEach(record => {
|
||||
const idx = record.contactIndex;
|
||||
if (!grouped[idx]) {
|
||||
grouped[idx] = { count: 0, totalDuration: 0 };
|
||||
}
|
||||
grouped[idx].count++;
|
||||
grouped[idx].totalDuration += record.duration || 0;
|
||||
});
|
||||
|
||||
resolve(grouped);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定联系人的所有语音记录
|
||||
* @param {number} contactIndex - 联系人索引
|
||||
* @returns {Promise<number>} 删除的记录数量
|
||||
*/
|
||||
export async function deleteVoiceRecordingsByContact(contactIndex) {
|
||||
const records = await getVoiceRecordingsByContact(contactIndex);
|
||||
for (const record of records) {
|
||||
await deleteVoiceRecording(record.id);
|
||||
}
|
||||
console.log(`[可乐] 已删除联系人 ${contactIndex} 的 ${records.length} 条语音记录`);
|
||||
return records.length;
|
||||
}
|
||||
|
||||
27
chat.js
27
chat.js
@@ -753,6 +753,11 @@ export function openChat(contactIndex) {
|
||||
|
||||
// 加载联系人的聊天背景
|
||||
loadContactBackground(contactIndex);
|
||||
|
||||
// 隐藏群聊专属菜单项,显示单聊专属菜单项
|
||||
document.getElementById('wechat-menu-invite-member')?.classList.add('hidden');
|
||||
document.getElementById('wechat-menu-block')?.classList.remove('hidden');
|
||||
document.getElementById('wechat-menu-moments')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 通过联系人ID打开聊天
|
||||
@@ -1471,7 +1476,8 @@ function getRealMsgIndexForVoice(container, msgElement) {
|
||||
let visualMsgCount = 1;
|
||||
const content = msg.content || '';
|
||||
const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic;
|
||||
if (!isSpecial && content.indexOf('|||') >= 0) {
|
||||
// 只有 assistant 消息才会被 ||| 分割显示
|
||||
if (msg.role === 'assistant' && !isSpecial && content.indexOf('|||') >= 0) {
|
||||
const parts = content.split('|||').map(p => p.trim()).filter(p => p);
|
||||
visualMsgCount = parts.length || 1;
|
||||
}
|
||||
@@ -2017,14 +2023,14 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
||||
}
|
||||
}
|
||||
|
||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||
const voiceMatch = aiMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||
if (voiceMatch) {
|
||||
aiMsg = voiceMatch[1];
|
||||
aiIsVoice = true;
|
||||
}
|
||||
|
||||
// 解析AI照片格式 [照片:描述]
|
||||
const photoMatch = aiMsg.match(/^\[照片[::]\s*(.+?)\]$/);
|
||||
const photoMatch = aiMsg.match(/^\s*\[照片[::]\s*(.+?)\]\s*$/);
|
||||
if (photoMatch) {
|
||||
aiMsg = photoMatch[1];
|
||||
aiIsPhoto = true;
|
||||
@@ -2488,6 +2494,13 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
||||
// 尝试触发语音/视频通话(随机触发+保底机制)
|
||||
tryTriggerCallAfterChat(contactIndex);
|
||||
|
||||
// 检查其他联系人是否要主动发消息
|
||||
import('./proactive-message.js').then(m => {
|
||||
m.checkOtherContactsProactive(contact.id);
|
||||
}).catch(err => {
|
||||
console.error('[可乐] 主动消息检查失败:', err);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
hideTypingIndicator();
|
||||
console.error('[可乐] AI 调用失败:', err);
|
||||
@@ -2590,14 +2603,14 @@ export async function sendStickerMessage(stickerUrl, description = '') {
|
||||
if (!aiMsg.trim()) continue;
|
||||
}
|
||||
|
||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||
const voiceMatch = aiMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||
if (voiceMatch) {
|
||||
aiMsg = voiceMatch[1];
|
||||
aiIsVoice = true;
|
||||
}
|
||||
|
||||
// 解析AI照片格式 [照片:描述]
|
||||
const photoMatch = aiMsg.match(/^\[照片[::]\s*(.+?)\]$/);
|
||||
const photoMatch = aiMsg.match(/^\s*\[照片[::]\s*(.+?)\]\s*$/);
|
||||
if (photoMatch) {
|
||||
aiMsg = photoMatch[1];
|
||||
aiIsPhoto = true;
|
||||
@@ -3037,14 +3050,14 @@ export async function sendPhotoMessage(description) {
|
||||
if (!aiMsg.trim()) continue;
|
||||
}
|
||||
|
||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||
const voiceMatch = aiMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||
if (voiceMatch) {
|
||||
aiMsg = voiceMatch[1];
|
||||
aiIsVoice = true;
|
||||
}
|
||||
|
||||
// 解析AI照片格式 [照片:描述]
|
||||
const photoMatch = aiMsg.match(/^\[照片[::]\s*(.+?)\]$/);
|
||||
const photoMatch = aiMsg.match(/^\s*\[照片[::]\s*(.+?)\]\s*$/);
|
||||
if (photoMatch) {
|
||||
aiMsg = photoMatch[1];
|
||||
aiIsPhoto = true;
|
||||
|
||||
173
contacts.js
173
contacts.js
@@ -143,6 +143,148 @@ function deleteGroupLorebooks(group, settings) {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除多人群聊
|
||||
export function deleteMultiPersonChat(mpIndex) {
|
||||
const settings = getSettings();
|
||||
const multiPersonChats = settings.multiPersonChats || [];
|
||||
const mpChat = multiPersonChats[mpIndex];
|
||||
if (!mpChat) return;
|
||||
|
||||
if (confirm(`确定要删除「${mpChat.name || '多人群聊'}」吗?`)) {
|
||||
multiPersonChats.splice(mpIndex, 1);
|
||||
requestSave();
|
||||
refreshContactsList();
|
||||
// 同时刷新聊天列表
|
||||
import('./ui.js').then(m => m.refreshChatList());
|
||||
showToast('多人群聊已删除');
|
||||
}
|
||||
}
|
||||
|
||||
// 当前正在编辑的多人群聊索引
|
||||
let currentEditingMpIndex = -1;
|
||||
let pendingMpAvatar = null; // 待保存的头像
|
||||
|
||||
// 打开多人群聊配置弹窗
|
||||
export function openMpApiSettings(mpIndex) {
|
||||
const settings = getSettings();
|
||||
const mpChat = settings.multiPersonChats?.[mpIndex];
|
||||
if (!mpChat) return;
|
||||
|
||||
currentEditingMpIndex = mpIndex;
|
||||
pendingMpAvatar = null;
|
||||
|
||||
// 填充头像预览
|
||||
const avatarPreview = document.getElementById('wechat-mp-avatar-preview');
|
||||
if (avatarPreview) {
|
||||
if (mpChat.avatar) {
|
||||
avatarPreview.innerHTML = `<img src="${mpChat.avatar}" style="width: 100%; height: 100%; object-fit: cover;">`;
|
||||
} else {
|
||||
avatarPreview.innerHTML = '群';
|
||||
}
|
||||
}
|
||||
|
||||
// 填充群名
|
||||
const nameInput = document.getElementById('wechat-mp-name-input');
|
||||
if (nameInput) {
|
||||
nameInput.value = mpChat.name || '群聊';
|
||||
}
|
||||
|
||||
// 填充API配置
|
||||
const useCustomApi = mpChat.useCustomApi || false;
|
||||
const customSwitch = document.getElementById('wechat-mp-use-custom-api');
|
||||
if (customSwitch) {
|
||||
customSwitch.classList.toggle('on', useCustomApi);
|
||||
}
|
||||
|
||||
const apiConfigSection = document.getElementById('wechat-mp-api-config');
|
||||
const globalTip = document.getElementById('wechat-mp-global-tip');
|
||||
if (apiConfigSection) apiConfigSection.classList.toggle('hidden', !useCustomApi);
|
||||
if (globalTip) globalTip.classList.toggle('hidden', useCustomApi);
|
||||
|
||||
document.getElementById('wechat-mp-api-url').value = mpChat.customApiUrl || '';
|
||||
document.getElementById('wechat-mp-api-key').value = mpChat.customApiKey || '';
|
||||
|
||||
// 模型选择
|
||||
const modelSelect = document.getElementById('wechat-mp-model-select');
|
||||
if (modelSelect) {
|
||||
modelSelect.innerHTML = '<option value="">--请选择模型--</option>';
|
||||
if (mpChat.customModel) {
|
||||
modelSelect.innerHTML += `<option value="${mpChat.customModel}" selected>${mpChat.customModel}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示弹窗
|
||||
document.getElementById('wechat-mp-api-modal')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 保存多人群聊配置
|
||||
export function saveMpApiSettings() {
|
||||
if (currentEditingMpIndex < 0) return;
|
||||
|
||||
const settings = getSettings();
|
||||
const mpChat = settings.multiPersonChats?.[currentEditingMpIndex];
|
||||
if (!mpChat) return;
|
||||
|
||||
// 保存群名
|
||||
const nameInput = document.getElementById('wechat-mp-name-input');
|
||||
if (nameInput) {
|
||||
mpChat.name = nameInput.value.trim() || '群聊';
|
||||
}
|
||||
|
||||
// 保存头像
|
||||
if (pendingMpAvatar) {
|
||||
mpChat.avatar = pendingMpAvatar;
|
||||
}
|
||||
|
||||
// 保存API配置
|
||||
const useCustomApi = document.getElementById('wechat-mp-use-custom-api')?.classList.contains('on') || false;
|
||||
mpChat.useCustomApi = useCustomApi;
|
||||
|
||||
if (useCustomApi) {
|
||||
mpChat.customApiUrl = document.getElementById('wechat-mp-api-url')?.value?.trim() || '';
|
||||
mpChat.customApiKey = document.getElementById('wechat-mp-api-key')?.value?.trim() || '';
|
||||
|
||||
// 获取模型值
|
||||
const inputWrapper = document.getElementById('wechat-mp-model-input-wrapper');
|
||||
const isManualMode = inputWrapper?.style.display === 'flex';
|
||||
mpChat.customModel = isManualMode
|
||||
? (document.getElementById('wechat-mp-model-input')?.value?.trim() || '')
|
||||
: (document.getElementById('wechat-mp-model-select')?.value?.trim() || '');
|
||||
}
|
||||
|
||||
requestSave();
|
||||
showToast('设置已保存');
|
||||
refreshContactsList();
|
||||
// 同时刷新聊天列表
|
||||
import('./ui.js').then(m => m.refreshChatList());
|
||||
|
||||
// 关闭弹窗
|
||||
document.getElementById('wechat-mp-api-modal')?.classList.add('hidden');
|
||||
currentEditingMpIndex = -1;
|
||||
pendingMpAvatar = null;
|
||||
}
|
||||
|
||||
// 处理多人群聊头像选择
|
||||
export function handleMpAvatarChange(file) {
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
pendingMpAvatar = e.target.result;
|
||||
const avatarPreview = document.getElementById('wechat-mp-avatar-preview');
|
||||
if (avatarPreview) {
|
||||
avatarPreview.innerHTML = `<img src="${pendingMpAvatar}" style="width: 100%; height: 100%; object-fit: cover;">`;
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// 关闭多人群聊API配置弹窗
|
||||
export function closeMpApiSettings() {
|
||||
document.getElementById('wechat-mp-api-modal')?.classList.add('hidden');
|
||||
currentEditingMpIndex = -1;
|
||||
}
|
||||
|
||||
// 更换角色头像(在设置弹窗中使用)
|
||||
export function changeContactAvatar(contactIndex) {
|
||||
pendingAvatarContactIndex = contactIndex;
|
||||
@@ -351,6 +493,37 @@ export function bindContactsEvents() {
|
||||
});
|
||||
});
|
||||
|
||||
// 多人群聊卡片点击进入聊天
|
||||
import('./multi-person-chat.js').then(mpModule => {
|
||||
document.querySelectorAll('.wechat-mp-card .wechat-mp-card-content').forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
// 如果点击的是头像,不进入聊天(由头像自己的事件处理)
|
||||
if (e.target.closest('.wechat-mp-avatar')) return;
|
||||
const cardEl = this.closest('.wechat-mp-card');
|
||||
const mpIndex = parseInt(cardEl.dataset.mpIndex);
|
||||
mpModule.openMultiPersonChat(mpIndex);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 多人群聊头像点击配置API
|
||||
document.querySelectorAll('.wechat-mp-avatar').forEach(avatar => {
|
||||
avatar.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const mpIndex = parseInt(this.dataset.mpIndex);
|
||||
openMpApiSettings(mpIndex);
|
||||
});
|
||||
});
|
||||
|
||||
// 多人群聊删除按钮
|
||||
document.querySelectorAll('.wechat-mp-delete').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const mpIndex = parseInt(this.dataset.mpIndex);
|
||||
deleteMultiPersonChat(mpIndex);
|
||||
});
|
||||
});
|
||||
|
||||
// 头像事件绑定(长按删除 + 单击打开设置)
|
||||
document.querySelectorAll('.wechat-card-avatar').forEach(avatar => {
|
||||
let pressTimer = null;
|
||||
|
||||
201
group-chat.js
201
group-chat.js
@@ -635,6 +635,11 @@ export function openGroupChat(groupIndex) {
|
||||
|
||||
// 加载群聊背景
|
||||
loadGroupBackground(groupIndex);
|
||||
|
||||
// 显示群聊专属菜单项,隐藏单聊专属菜单项
|
||||
document.getElementById('wechat-menu-invite-member')?.classList.remove('hidden');
|
||||
document.getElementById('wechat-menu-block')?.classList.add('hidden');
|
||||
document.getElementById('wechat-menu-moments')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 渲染群聊历史
|
||||
@@ -2404,6 +2409,11 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
|
||||
refreshChatList();
|
||||
checkGroupSummaryReminder(groupChat);
|
||||
|
||||
// 检测群聊中的负面情绪,可能触发私聊
|
||||
// 传递群聊上下文(最近40条消息)
|
||||
const groupContext = getGroupChatHistoryForApi(groupChat.chatHistory, 40);
|
||||
detectGroupEmotionAndTriggerPrivate(responses, members, groupContext);
|
||||
|
||||
} catch (err) {
|
||||
hideGroupTypingIndicator();
|
||||
console.error('[可乐] 群聊 AI 调用失败:', err);
|
||||
@@ -2932,3 +2942,194 @@ export async function sendGroupBatchMessages(messages) {
|
||||
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示邀请成员弹窗
|
||||
*/
|
||||
export function showInviteMemberModal() {
|
||||
const settings = getSettings();
|
||||
const groupChat = settings.groupChats?.[currentGroupChatIndex];
|
||||
if (!groupChat) {
|
||||
showToast('请先打开一个群聊', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMemberIds = groupChat.memberIds || [];
|
||||
|
||||
// 检查是否已满
|
||||
if (currentMemberIds.length >= GROUP_CHAT_MAX_AI_MEMBERS) {
|
||||
showToast(`群聊已满(最多${GROUP_CHAT_MAX_AI_MEMBERS}人)`, '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取可邀请的联系人(不在群里的、配置了独立API的)
|
||||
const availableContacts = settings.contacts.filter(c =>
|
||||
!currentMemberIds.includes(c.id) &&
|
||||
c.useCustomApi &&
|
||||
c.customApiUrl &&
|
||||
c.customModel
|
||||
);
|
||||
|
||||
if (availableContacts.length === 0) {
|
||||
showToast('没有可邀请的联系人\n(需配置独立API)', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取手机容器
|
||||
const phoneContainer = document.querySelector('.wechat-phone');
|
||||
if (!phoneContainer) return;
|
||||
|
||||
// 构建弹窗HTML
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'wechat-modal';
|
||||
modal.id = 'wechat-invite-member-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="wechat-modal-content" style="max-height: 70vh; overflow-y: auto;">
|
||||
<div class="wechat-modal-title">邀请成员</div>
|
||||
<div class="wechat-modal-body">
|
||||
<div style="margin-bottom: 12px; color: #888; font-size: 12px;">
|
||||
当前 ${currentMemberIds.length}/${GROUP_CHAT_MAX_AI_MEMBERS} 人
|
||||
</div>
|
||||
<div id="wechat-invite-contact-list" style="max-height: 300px; overflow-y: auto;">
|
||||
${availableContacts.map(c => `
|
||||
<div class="wechat-invite-contact-item" data-contact-id="${c.id}"
|
||||
style="display: flex; align-items: center; padding: 10px; cursor: pointer; border-bottom: 1px solid #eee; transition: background 0.2s;">
|
||||
<div style="width: 40px; height: 40px; border-radius: 4px; background: #07c160; color: white;
|
||||
display: flex; align-items: center; justify-content: center; margin-right: 10px; overflow: hidden;">
|
||||
${c.avatar ? `<img src="${c.avatar}" style="width: 100%; height: 100%; object-fit: cover;">` : escapeHtml(c.name.charAt(0))}
|
||||
</div>
|
||||
<span>${escapeHtml(c.name)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wechat-modal-actions">
|
||||
<button class="wechat-btn" id="wechat-invite-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
phoneContainer.appendChild(modal);
|
||||
|
||||
// 添加hover效果
|
||||
modal.querySelectorAll('.wechat-invite-contact-item').forEach(item => {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = '#f5f5f5';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = '';
|
||||
});
|
||||
|
||||
// 点击联系人邀请
|
||||
item.addEventListener('click', () => {
|
||||
const contactId = item.dataset.contactId;
|
||||
addMemberToGroup(currentGroupChatIndex, contactId);
|
||||
modal.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// 取消按钮
|
||||
modal.querySelector('#wechat-invite-cancel')?.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
// 点击遮罩关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加成员到群聊
|
||||
*/
|
||||
export function addMemberToGroup(groupIndex, contactId) {
|
||||
const settings = getSettings();
|
||||
const groupChat = settings.groupChats?.[groupIndex];
|
||||
const contact = settings.contacts.find(c => c.id === contactId);
|
||||
|
||||
if (!groupChat || !contact) return;
|
||||
|
||||
// 检查是否已满
|
||||
if (groupChat.memberIds.length >= GROUP_CHAT_MAX_AI_MEMBERS) {
|
||||
showToast('群聊已满', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if (groupChat.memberIds.includes(contactId)) {
|
||||
showToast('该成员已在群聊中', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
groupChat.memberIds.push(contactId);
|
||||
|
||||
// 添加系统消息
|
||||
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')}`;
|
||||
|
||||
groupChat.chatHistory.push({
|
||||
role: 'system',
|
||||
content: `${contact.name} 加入了群聊`,
|
||||
isSystemNotice: true,
|
||||
time: timeStr,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 更新群名(如果是默认名)
|
||||
if (!groupChat.customName) {
|
||||
const memberNames = groupChat.memberIds
|
||||
.map(id => settings.contacts.find(c => c.id === id)?.name)
|
||||
.filter(Boolean);
|
||||
groupChat.name = memberNames.join('、');
|
||||
}
|
||||
|
||||
requestSave();
|
||||
|
||||
// 刷新界面
|
||||
openGroupChat(groupIndex);
|
||||
refreshChatList();
|
||||
|
||||
showToast(`${contact.name} 已加入群聊`);
|
||||
|
||||
console.log(`[可乐] ${contact.name} 加入群聊:`, groupChat.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测群聊中的负面情绪,可能触发私聊
|
||||
* @param {Array} responses - AI回复数组
|
||||
* @param {Array} members - 群成员数组
|
||||
* @param {Array} groupContext - 群聊上下文(最近40条消息)
|
||||
*/
|
||||
function detectGroupEmotionAndTriggerPrivate(responses, members, groupContext = []) {
|
||||
if (!responses || responses.length === 0) return;
|
||||
|
||||
// 负面情绪关键词
|
||||
const NEGATIVE_KEYWORDS = [
|
||||
'生气', '讨厌', '烦', '不理你', '哼', '算了', '随便',
|
||||
'滚', '走开', '别说了', '不想理', '烦死了', '气死',
|
||||
'委屈', '难过', '伤心', '失望'
|
||||
];
|
||||
|
||||
for (const resp of responses) {
|
||||
const content = resp.content || '';
|
||||
const characterId = resp.characterId;
|
||||
|
||||
if (!characterId) continue;
|
||||
|
||||
// 检测负面情绪
|
||||
const hasNegativeEmotion = NEGATIVE_KEYWORDS.some(kw => content.includes(kw));
|
||||
|
||||
if (hasNegativeEmotion) {
|
||||
console.log(`[可乐] 群聊检测到 ${resp.characterName} 的负面情绪:`, content.substring(0, 30));
|
||||
|
||||
// 触发私聊(延迟执行,有概率触发)
|
||||
// 传递群聊上下文,让私聊时AI知道群里发生了什么
|
||||
import('./proactive-message.js').then(m => {
|
||||
m.triggerProactiveFromGroup(characterId, 'negative', groupContext);
|
||||
}).catch(err => {
|
||||
console.error('[可乐] 群聊情绪触发私聊失败:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
508
main.js
508
main.js
@@ -12,7 +12,7 @@ import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, upda
|
||||
import { showToast } from './toast.js';
|
||||
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
|
||||
|
||||
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
|
||||
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex, closeMpApiSettings, saveMpApiSettings, handleMpAvatarChange } from './contacts.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 { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats, recoverFromTavernWorldbook } from './summary.js';
|
||||
@@ -23,6 +23,7 @@ import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST
|
||||
import { setupPhoneAutoCentering, setupPhoneDrag, centerPhoneInViewport } from './phone.js';
|
||||
|
||||
import { showGroupCreateModal, closeGroupCreateModal, createGroupChat, sendGroupMessage, isInGroupChat, setCurrentGroupChatIndex, getCurrentGroupIndex, openGroupChat } from './group-chat.js';
|
||||
import { isInMultiPersonChat, sendMultiPersonMessage, setCurrentMultiPersonChatIndex } from './multi-person-chat.js';
|
||||
import { toggleDarkMode, refreshContextTags } from './settings-ui.js';
|
||||
import { initFuncPanel, toggleFuncPanel, hideFuncPanel, showExpandVoice, closeExpandPanel, sendExpandContent } from './chat-func-panel.js';
|
||||
import { initEmojiPanel, toggleEmojiPanel, hideEmojiPanel } from './emoji-panel.js';
|
||||
@@ -38,7 +39,8 @@ import { initGiftEvents } from './gift.js';
|
||||
import { initCropper } from './cropper.js';
|
||||
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
|
||||
import { testSttApi, testTtsApi } from './voice-api.js';
|
||||
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording } from './audio-storage.js';
|
||||
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording, getAllVoiceRecordingsGroupedByContact, deleteVoiceRecordingsByContact } from './audio-storage.js';
|
||||
import { initMultiCharImport, openMultiImportModal, getMultiCharImportModalHtml, getCharSelectModalHtml, getCharOtherEditModalHtml } from './multi-char-import.js';
|
||||
|
||||
// ========== 历史记录功能 ==========
|
||||
let currentHistoryTab = 'listen';
|
||||
@@ -71,7 +73,7 @@ function closeHistoryPage() {
|
||||
currentHistoryContactIndex = -1;
|
||||
}
|
||||
|
||||
function deleteHistoryRecord(tabType, index) {
|
||||
function deleteHistoryRecord(tabType, index, isRealVoice = false) {
|
||||
const settings = getSettings();
|
||||
const contact = settings.contacts?.[currentHistoryContactIndex];
|
||||
if (!contact) return;
|
||||
@@ -80,8 +82,32 @@ function deleteHistoryRecord(tabType, index) {
|
||||
if (contact.listenHistory && contact.listenHistory[index]) {
|
||||
contact.listenHistory.splice(index, 1);
|
||||
}
|
||||
} else if (tabType === 'voice' || tabType === 'video') {
|
||||
// 从 callHistory 中找到并删除对应类型的记录
|
||||
} else if (tabType === 'voice') {
|
||||
if (isRealVoice) {
|
||||
// 删除实时语音记录
|
||||
if (contact.realVoiceCallHistory && contact.realVoiceCallHistory.length > 0) {
|
||||
// 找到实时语音记录在合并数组中的索引对应的原始索引
|
||||
const realVoiceRecords = contact.realVoiceCallHistory;
|
||||
const callHistory = contact.callHistory || [];
|
||||
const voiceRecords = callHistory.filter(r => r.type === 'voice');
|
||||
// index 是在合并数组中的位置,需要计算在 realVoiceCallHistory 中的实际位置
|
||||
const realVoiceIndex = index - voiceRecords.length;
|
||||
if (realVoiceIndex >= 0 && realVoiceIndex < realVoiceRecords.length) {
|
||||
contact.realVoiceCallHistory.splice(realVoiceIndex, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 删除普通语音通话记录
|
||||
const callHistory = contact.callHistory || [];
|
||||
const typeRecords = callHistory.filter(r => r.type === 'voice');
|
||||
if (typeRecords[index]) {
|
||||
const originalIndex = callHistory.indexOf(typeRecords[index]);
|
||||
if (originalIndex >= 0) {
|
||||
contact.callHistory.splice(originalIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (tabType === 'video') {
|
||||
const callHistory = contact.callHistory || [];
|
||||
const typeRecords = callHistory.filter(r => r.type === tabType);
|
||||
if (typeRecords[index]) {
|
||||
@@ -151,8 +177,17 @@ function renderHistoryContent(contact, tabType) {
|
||||
let records = [];
|
||||
if (tabType === 'listen') {
|
||||
records = contact.listenHistory || [];
|
||||
} else if (tabType === 'voice') {
|
||||
// 语音通话:合并普通语音通话和实时语音通话
|
||||
const callHistory = contact.callHistory || [];
|
||||
const voiceRecords = callHistory.filter(r => r.type === 'voice');
|
||||
const realVoiceRecords = (contact.realVoiceCallHistory || []).map(r => ({
|
||||
...r,
|
||||
isRealVoice: true // 标记为实时语音
|
||||
}));
|
||||
records = [...voiceRecords, ...realVoiceRecords];
|
||||
} else {
|
||||
// 从 callHistory 中筛选 voice 或 video
|
||||
// 从 callHistory 中筛选 video
|
||||
const callHistory = contact.callHistory || [];
|
||||
records = callHistory.filter(r => r.type === tabType);
|
||||
}
|
||||
@@ -179,12 +214,13 @@ function renderHistoryContent(contact, tabType) {
|
||||
const duration = record.duration || '';
|
||||
const messages = record.messages || [];
|
||||
const originalIndex = records.indexOf(record);
|
||||
const isRealVoice = record.isRealVoice ? 'true' : 'false';
|
||||
|
||||
html += `<div class="wechat-history-card" data-tab="${tabType}" data-index="${originalIndex}">`;
|
||||
html += `<div class="wechat-history-card" data-tab="${tabType}" data-index="${originalIndex}" data-real-voice="${isRealVoice}">`;
|
||||
html += `<div class="wechat-history-card-header">`;
|
||||
html += `<span class="wechat-history-card-time">${escapeHtml(time)}</span>`;
|
||||
html += `<span class="wechat-history-card-time">${escapeHtml(time)}${record.isRealVoice ? ' <span style="color: #07c160; font-size: 12px;">[实时语音]</span>' : ''}</span>`;
|
||||
html += `<div class="wechat-history-card-actions">`;
|
||||
html += `<button class="wechat-history-delete-btn" data-tab="${tabType}" data-index="${originalIndex}" title="删除">×</button>`;
|
||||
html += `<button class="wechat-history-delete-btn" data-tab="${tabType}" data-index="${originalIndex}" data-real-voice="${isRealVoice}" title="删除">×</button>`;
|
||||
if (duration) {
|
||||
html += `<span class="wechat-history-card-duration">${escapeHtml(duration)}</span>`;
|
||||
}
|
||||
@@ -224,7 +260,8 @@ function renderHistoryContent(contact, tabType) {
|
||||
e.stopPropagation();
|
||||
const tab = btn.dataset.tab;
|
||||
const index = parseInt(btn.dataset.index);
|
||||
deleteHistoryRecord(tab, index);
|
||||
const isRealVoice = btn.dataset.realVoice === 'true';
|
||||
deleteHistoryRecord(tab, index, isRealVoice);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -513,6 +550,211 @@ function updateFloatingBallMenuText(enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
// 清除孤立缓存(已删除联系人/群聊的总结历史和语音记录)
|
||||
async function clearOrphanedCache() {
|
||||
const settings = getSettings();
|
||||
const contacts = settings.contacts || [];
|
||||
const groupChats = settings.groupChats || [];
|
||||
const lorebooks = settings.selectedLorebooks || [];
|
||||
|
||||
// 获取当前有效的联系人ID和名称
|
||||
const validContactIds = new Set(contacts.map(c => c.id).filter(id => id));
|
||||
const validContactNames = new Set(contacts.map(c => c.name).filter(n => n));
|
||||
const validGroupNames = new Set(groupChats.map(g => g.name).filter(n => n));
|
||||
const validContactIndexes = new Set(contacts.map((_, idx) => idx));
|
||||
|
||||
// 查找孤立的总结世界书
|
||||
const orphanedSummaries = [];
|
||||
|
||||
lorebooks.forEach((lb, idx) => {
|
||||
const isSummaryBook = lb.fromSummary === true ||
|
||||
(lb.name?.startsWith('【可乐】和') && lb.name?.endsWith('的聊天'));
|
||||
|
||||
if (isSummaryBook) {
|
||||
const nameMatch = lb.name?.match(/^【可乐】和(.+)的聊天$/);
|
||||
const linkedName = nameMatch ? nameMatch[1] : null;
|
||||
|
||||
const contactExists = linkedName && validContactNames.has(linkedName);
|
||||
const groupExists = linkedName && validGroupNames.has(linkedName);
|
||||
const linkedById = lb.characterId && validContactIds.has(lb.characterId);
|
||||
|
||||
if (!contactExists && !groupExists && !linkedById) {
|
||||
const cupCount = lb.entries?.length || 0;
|
||||
orphanedSummaries.push({
|
||||
type: 'summary',
|
||||
name: lb.name,
|
||||
linkedName: linkedName || '未知',
|
||||
index: idx,
|
||||
cupCount
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 查找孤立的语音记录
|
||||
const orphanedVoices = [];
|
||||
try {
|
||||
const voiceGroups = await getAllVoiceRecordingsGroupedByContact();
|
||||
for (const [contactIdxStr, data] of Object.entries(voiceGroups)) {
|
||||
const contactIdx = parseInt(contactIdxStr);
|
||||
// 如果索引超出当前联系人范围,则为孤立数据
|
||||
if (!validContactIndexes.has(contactIdx)) {
|
||||
orphanedVoices.push({
|
||||
type: 'voice',
|
||||
contactIndex: contactIdx,
|
||||
count: data.count,
|
||||
totalDuration: data.totalDuration
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[可乐] 获取语音记录失败:', err);
|
||||
}
|
||||
|
||||
// 如果没有孤立数据
|
||||
if (orphanedSummaries.length === 0 && orphanedVoices.length === 0) {
|
||||
showToast('没有发现需要清理的缓存数据');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示选择弹窗
|
||||
showCacheCleanupModal(orphanedSummaries, orphanedVoices);
|
||||
}
|
||||
|
||||
// 显示缓存清理选择弹窗
|
||||
function showCacheCleanupModal(orphanedSummaries, orphanedVoices) {
|
||||
document.getElementById('wechat-cache-cleanup-modal')?.remove();
|
||||
|
||||
const hasSummaries = orphanedSummaries.length > 0;
|
||||
const hasVoices = orphanedVoices.length > 0;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'wechat-modal';
|
||||
modal.id = 'wechat-cache-cleanup-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="wechat-modal-content" style="position: relative; max-width: 380px; max-height: 80vh; margin: auto;">
|
||||
<button class="wechat-modal-close-x" id="wechat-cache-modal-close">×</button>
|
||||
<div class="wechat-modal-title">清除缓存</div>
|
||||
<div style="font-size: 12px; color: var(--wechat-text-secondary); margin-bottom: 12px; padding: 0 4px;">
|
||||
勾选要清除的项目:
|
||||
</div>
|
||||
<div style="max-height: 45vh; overflow-y: auto; margin-bottom: 15px;">
|
||||
${hasSummaries ? `
|
||||
<div style="font-size: 12px; color: var(--wechat-text-secondary); padding: 8px; background: var(--wechat-bg-secondary); border-radius: 4px; margin-bottom: 8px;">
|
||||
📝 总结历史(已删除联系人/群聊)
|
||||
</div>
|
||||
${orphanedSummaries.map(item => `
|
||||
<label class="wechat-cache-item" style="display: flex; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
|
||||
<input type="checkbox" class="wechat-cache-checkbox" data-type="summary" data-index="${item.index}" checked style="margin-right: 10px; width: 18px; height: 18px;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${item.linkedName}</div>
|
||||
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${item.cupCount} 杯总结记录</div>
|
||||
</div>
|
||||
</label>
|
||||
`).join('')}
|
||||
` : ''}
|
||||
${hasVoices ? `
|
||||
<div style="font-size: 12px; color: var(--wechat-text-secondary); padding: 8px; background: var(--wechat-bg-secondary); border-radius: 4px; margin-bottom: 8px; ${hasSummaries ? 'margin-top: 12px;' : ''}">
|
||||
🎙️ 语音通话记录(孤立数据)
|
||||
</div>
|
||||
${orphanedVoices.map(item => `
|
||||
<label class="wechat-cache-item" style="display: flex; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
|
||||
<input type="checkbox" class="wechat-cache-checkbox" data-type="voice" data-contact-index="${item.contactIndex}" checked style="margin-right: 10px; width: 18px; height: 18px;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-size: 14px; font-weight: 500;">联系人 #${item.contactIndex}</div>
|
||||
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${item.count} 条语音,共 ${Math.round(item.totalDuration)} 秒</div>
|
||||
</div>
|
||||
</label>
|
||||
`).join('')}
|
||||
` : ''}
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; justify-content: space-between; align-items: center;">
|
||||
<label style="display: flex; align-items: center; font-size: 13px; color: var(--wechat-text-secondary); cursor: pointer;">
|
||||
<input type="checkbox" id="wechat-cache-select-all" checked style="margin-right: 6px;">
|
||||
全选
|
||||
</label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="wechat-btn wechat-btn-secondary" id="wechat-cache-cancel">取消</button>
|
||||
<button class="wechat-btn wechat-btn-danger" id="wechat-cache-confirm">清除选中</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const phoneContainer = document.querySelector('.wechat-phone') || document.body;
|
||||
phoneContainer.appendChild(modal);
|
||||
|
||||
// 关闭按钮
|
||||
modal.querySelector('#wechat-cache-modal-close').addEventListener('click', () => modal.remove());
|
||||
modal.querySelector('#wechat-cache-cancel').addEventListener('click', () => modal.remove());
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
});
|
||||
|
||||
// 全选/取消全选
|
||||
const selectAllCheckbox = modal.querySelector('#wechat-cache-select-all');
|
||||
const itemCheckboxes = modal.querySelectorAll('.wechat-cache-checkbox');
|
||||
|
||||
selectAllCheckbox.addEventListener('change', () => {
|
||||
itemCheckboxes.forEach(cb => cb.checked = selectAllCheckbox.checked);
|
||||
});
|
||||
|
||||
itemCheckboxes.forEach(cb => {
|
||||
cb.addEventListener('change', () => {
|
||||
const allChecked = Array.from(itemCheckboxes).every(c => c.checked);
|
||||
const noneChecked = Array.from(itemCheckboxes).every(c => !c.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
selectAllCheckbox.indeterminate = !allChecked && !noneChecked;
|
||||
});
|
||||
});
|
||||
|
||||
// 确认清除
|
||||
modal.querySelector('#wechat-cache-confirm').addEventListener('click', async () => {
|
||||
const selectedSummaryIndexes = new Set();
|
||||
const selectedVoiceIndexes = [];
|
||||
|
||||
itemCheckboxes.forEach(cb => {
|
||||
if (cb.checked) {
|
||||
if (cb.dataset.type === 'summary') {
|
||||
selectedSummaryIndexes.add(parseInt(cb.dataset.index));
|
||||
} else if (cb.dataset.type === 'voice') {
|
||||
selectedVoiceIndexes.push(parseInt(cb.dataset.contactIndex));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedSummaryIndexes.size === 0 && selectedVoiceIndexes.length === 0) {
|
||||
showToast('请至少选择一项');
|
||||
return;
|
||||
}
|
||||
|
||||
let clearedCount = 0;
|
||||
|
||||
// 清除总结缓存
|
||||
if (selectedSummaryIndexes.size > 0) {
|
||||
const settings = getSettings();
|
||||
settings.selectedLorebooks = settings.selectedLorebooks.filter((_, idx) => !selectedSummaryIndexes.has(idx));
|
||||
requestSave();
|
||||
clearedCount += selectedSummaryIndexes.size;
|
||||
}
|
||||
|
||||
// 清除语音缓存
|
||||
for (const contactIdx of selectedVoiceIndexes) {
|
||||
try {
|
||||
await deleteVoiceRecordingsByContact(contactIdx);
|
||||
clearedCount++;
|
||||
} catch (err) {
|
||||
console.error('[可乐] 删除语音记录失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
modal.remove();
|
||||
showToast(`已清除 ${clearedCount} 项缓存数据`);
|
||||
console.log('[可乐] 已清除缓存,总结:', selectedSummaryIndexes.size, '语音:', selectedVoiceIndexes.length);
|
||||
});
|
||||
}
|
||||
|
||||
function setupPhoneMinimize() {
|
||||
const phone = document.getElementById('wechat-phone');
|
||||
const minimizeBtn = document.getElementById('wechat-minimize-btn');
|
||||
@@ -609,11 +851,11 @@ function restorePhone() {
|
||||
|
||||
phone.classList.remove('minimized');
|
||||
|
||||
// 恢复原始位置或居中
|
||||
if (settings.phoneOriginalPosition) {
|
||||
phone.style.left = settings.phoneOriginalPosition.left;
|
||||
phone.style.top = settings.phoneOriginalPosition.top;
|
||||
}
|
||||
// 清除缩小前保存的位置,让居中函数重新计算
|
||||
delete settings.phoneOriginalPosition;
|
||||
|
||||
// 恢复到屏幕中央
|
||||
centerPhoneInViewport({ force: true });
|
||||
|
||||
// 恢复时根据设置显示悬浮球
|
||||
if (settings.floatingBallEnabled !== false) {
|
||||
@@ -763,6 +1005,12 @@ function bindEvents() {
|
||||
toggleFloatingBallEnabled();
|
||||
});
|
||||
|
||||
// 下拉菜单 - 清除缓存
|
||||
document.getElementById('wechat-menu-clear-cache')?.addEventListener('click', () => {
|
||||
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
|
||||
clearOrphanedCache();
|
||||
});
|
||||
|
||||
// ===== 群聊创建弹窗事件 =====
|
||||
document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal);
|
||||
document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat);
|
||||
@@ -775,11 +1023,14 @@ function bindEvents() {
|
||||
document.getElementById('wechat-chat-back-btn')?.addEventListener('click', () => {
|
||||
setCurrentChatIndex(-1);
|
||||
setCurrentGroupChatIndex(-1);
|
||||
// 清除群聊标记
|
||||
setCurrentMultiPersonChatIndex(-1);
|
||||
// 清除群聊和多人群聊标记
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
if (messagesContainer) {
|
||||
messagesContainer.dataset.isGroup = 'false';
|
||||
messagesContainer.dataset.groupIndex = '-1';
|
||||
messagesContainer.dataset.isMultiPerson = 'false';
|
||||
messagesContainer.dataset.multiPersonIndex = '-1';
|
||||
// 清除背景
|
||||
messagesContainer.style.backgroundImage = '';
|
||||
}
|
||||
@@ -815,6 +1066,12 @@ function bindEvents() {
|
||||
document.getElementById('wechat-recalled-panel')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 邀请成员(群聊)
|
||||
document.getElementById('wechat-menu-invite-member')?.addEventListener('click', () => {
|
||||
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
||||
import('./group-chat.js').then(m => m.showInviteMemberModal());
|
||||
});
|
||||
|
||||
// 查看TA的朋友圈
|
||||
document.getElementById('wechat-menu-moments')?.addEventListener('click', () => {
|
||||
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
||||
@@ -1007,6 +1264,11 @@ function bindEvents() {
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
// 导入多人卡
|
||||
document.getElementById('wechat-import-multi-card')?.addEventListener('click', () => {
|
||||
openMultiImportModal();
|
||||
});
|
||||
|
||||
// 深色模式切换
|
||||
document.getElementById('wechat-dark-toggle')?.addEventListener('click', toggleDarkMode);
|
||||
|
||||
@@ -1311,6 +1573,186 @@ function bindEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 多人群聊配置弹窗事件 =====
|
||||
// 关闭按钮
|
||||
document.getElementById('wechat-mp-api-close')?.addEventListener('click', closeMpApiSettings);
|
||||
|
||||
// 保存按钮
|
||||
document.getElementById('wechat-mp-api-save')?.addEventListener('click', saveMpApiSettings);
|
||||
|
||||
// 更换头像按钮
|
||||
document.getElementById('wechat-mp-change-avatar')?.addEventListener('click', () => {
|
||||
document.getElementById('wechat-mp-avatar-file')?.click();
|
||||
});
|
||||
|
||||
// 头像预览点击也可以更换
|
||||
document.getElementById('wechat-mp-avatar-preview')?.addEventListener('click', () => {
|
||||
document.getElementById('wechat-mp-avatar-file')?.click();
|
||||
});
|
||||
|
||||
// 头像文件选择
|
||||
document.getElementById('wechat-mp-avatar-file')?.addEventListener('change', (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
handleMpAvatarChange(file);
|
||||
}
|
||||
e.target.value = ''; // 清空以便重复选择同一文件
|
||||
});
|
||||
|
||||
// 独立API开关
|
||||
document.getElementById('wechat-mp-use-custom-api')?.addEventListener('click', () => {
|
||||
const toggle = document.getElementById('wechat-mp-use-custom-api');
|
||||
const apiConfigDiv = document.getElementById('wechat-mp-api-config');
|
||||
const globalTip = document.getElementById('wechat-mp-global-tip');
|
||||
toggle?.classList.toggle('on');
|
||||
const isOn = toggle?.classList.contains('on');
|
||||
if (apiConfigDiv) {
|
||||
if (isOn) {
|
||||
apiConfigDiv.classList.remove('hidden');
|
||||
apiConfigDiv.style.display = 'flex';
|
||||
} else {
|
||||
apiConfigDiv.classList.add('hidden');
|
||||
apiConfigDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
if (globalTip) {
|
||||
globalTip.classList.toggle('hidden', isOn);
|
||||
}
|
||||
});
|
||||
|
||||
// 多人群聊API获取模型按钮
|
||||
document.getElementById('wechat-mp-fetch-model')?.addEventListener('click', async () => {
|
||||
const apiUrl = document.getElementById('wechat-mp-api-url')?.value?.trim();
|
||||
const apiKey = document.getElementById('wechat-mp-api-key')?.value?.trim();
|
||||
const modelSelect = document.getElementById('wechat-mp-model-select');
|
||||
const fetchBtn = document.getElementById('wechat-mp-fetch-model');
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写API地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
fetchBtn.textContent = '...';
|
||||
fetchBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const { fetchModelListFromApi } = await import('./ai.js');
|
||||
const models = await fetchModelListFromApi(apiUrl, apiKey);
|
||||
if (models.length > 0) {
|
||||
const currentValue = modelSelect?.value || '';
|
||||
modelSelect.innerHTML = '<option value="">---请选择模型---</option>' +
|
||||
models.map(m => `<option value="${m}"${m === currentValue ? ' selected' : ''}>${m}</option>`).join('');
|
||||
showToast(`获取到 ${models.length} 个模型`);
|
||||
} else {
|
||||
showToast('未找到可用模型', 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[可乐] 获取模型失败:', err);
|
||||
showToast('获取失败: ' + err.message, '⚠️');
|
||||
} finally {
|
||||
fetchBtn.textContent = '获取';
|
||||
fetchBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 多人群聊API手动输入按钮
|
||||
document.getElementById('wechat-mp-model-manual')?.addEventListener('click', () => {
|
||||
const selectWrapper = document.getElementById('wechat-mp-model-select-wrapper');
|
||||
const inputWrapper = document.getElementById('wechat-mp-model-input-wrapper');
|
||||
const modelSelect = document.getElementById('wechat-mp-model-select');
|
||||
const modelInput = document.getElementById('wechat-mp-model-input');
|
||||
|
||||
if (modelSelect?.value) {
|
||||
modelInput.value = modelSelect.value;
|
||||
}
|
||||
|
||||
selectWrapper.style.display = 'none';
|
||||
inputWrapper.style.display = 'flex';
|
||||
modelInput?.focus();
|
||||
});
|
||||
|
||||
// 多人群聊API返回按钮
|
||||
document.getElementById('wechat-mp-model-back')?.addEventListener('click', () => {
|
||||
const selectWrapper = document.getElementById('wechat-mp-model-select-wrapper');
|
||||
const inputWrapper = document.getElementById('wechat-mp-model-input-wrapper');
|
||||
const modelSelect = document.getElementById('wechat-mp-model-select');
|
||||
const modelInput = document.getElementById('wechat-mp-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测试连接按钮
|
||||
document.getElementById('wechat-mp-test-api')?.addEventListener('click', async () => {
|
||||
const apiUrl = document.getElementById('wechat-mp-api-url')?.value?.trim();
|
||||
const apiKey = document.getElementById('wechat-mp-api-key')?.value?.trim();
|
||||
const inputWrapper = document.getElementById('wechat-mp-model-input-wrapper');
|
||||
const isManualMode = inputWrapper?.style.display === 'flex';
|
||||
const model = isManualMode
|
||||
? document.getElementById('wechat-mp-model-input')?.value?.trim()
|
||||
: document.getElementById('wechat-mp-model-select')?.value?.trim();
|
||||
const testBtn = document.getElementById('wechat-mp-test-api');
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写API地址', 'info');
|
||||
return;
|
||||
}
|
||||
if (!model) {
|
||||
showToast('请先填写或选择模型', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
testBtn.textContent = '测试中...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const chatUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(chatUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [{ role: 'user', content: '请回复"连接成功"' }],
|
||||
max_tokens: 50
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`${response.status}: ${errorText.substring(0, 100)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const reply = data.choices?.[0]?.message?.content || '';
|
||||
showToast(`连接成功!回复: ${reply.substring(0, 20)}...`, 'success');
|
||||
} catch (err) {
|
||||
console.error('[可乐] 测试连接失败:', err);
|
||||
showToast('❌ 连接失败: ' + err.message, '⚠️');
|
||||
} finally {
|
||||
testBtn.textContent = '测试连接';
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 群聊设置事件 =====
|
||||
// 群聊提示词注入开关
|
||||
document.getElementById('wechat-group-inject-toggle')?.addEventListener('click', () => {
|
||||
@@ -1373,10 +1815,15 @@ function bindEvents() {
|
||||
text: text.substring(0, 20),
|
||||
isGroup: messagesContainer?.dataset?.isGroup,
|
||||
groupIndex: messagesContainer?.dataset?.groupIndex,
|
||||
isInGroupChatResult: isInGroupChat()
|
||||
isMultiPerson: messagesContainer?.dataset?.isMultiPerson,
|
||||
isInGroupChatResult: isInGroupChat(),
|
||||
isInMultiPersonChatResult: isInMultiPersonChat()
|
||||
});
|
||||
|
||||
if (isInGroupChat()) {
|
||||
if (isInMultiPersonChat()) {
|
||||
console.log('[可乐] 调用 sendMultiPersonMessage');
|
||||
sendMultiPersonMessage(text);
|
||||
} else if (isInGroupChat()) {
|
||||
console.log('[可乐] 调用 sendGroupMessage');
|
||||
sendGroupMessage(text);
|
||||
} else {
|
||||
@@ -1394,7 +1841,9 @@ function bindEvents() {
|
||||
const text = chatInput?.value?.trim();
|
||||
if (text) {
|
||||
// 有文字时发送消息
|
||||
if (isInGroupChat()) {
|
||||
if (isInMultiPersonChat()) {
|
||||
sendMultiPersonMessage(text);
|
||||
} else if (isInGroupChat()) {
|
||||
sendGroupMessage(text);
|
||||
} else {
|
||||
sendMessage(text);
|
||||
@@ -1428,6 +1877,7 @@ function bindEvents() {
|
||||
initGiftEvents();
|
||||
initCropper();
|
||||
initHistoryEvents();
|
||||
initMultiCharImport();
|
||||
|
||||
// 展开面板
|
||||
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
|
||||
@@ -1471,7 +1921,7 @@ function bindEvents() {
|
||||
});
|
||||
});
|
||||
|
||||
// 聊天列表项点击(支持单聊和群聊)
|
||||
// 聊天列表项点击(支持单聊、群聊和多人群聊)
|
||||
document.getElementById('wechat-chat-list')?.addEventListener('click', (e) => {
|
||||
const chatItem = e.target.closest('.wechat-chat-item');
|
||||
if (!chatItem) return;
|
||||
@@ -1482,6 +1932,12 @@ function bindEvents() {
|
||||
if (!isNaN(groupIndex)) {
|
||||
import('./group-chat.js').then(m => m.openGroupChat(groupIndex));
|
||||
}
|
||||
} else if (chatItem.classList.contains('wechat-chat-item-mp')) {
|
||||
// 多人群聊
|
||||
const mpIndex = parseInt(chatItem.dataset.mpIndex);
|
||||
if (!isNaN(mpIndex)) {
|
||||
import('./multi-person-chat.js').then(m => m.openMultiPersonChat(mpIndex));
|
||||
}
|
||||
} else {
|
||||
// 单聊
|
||||
const contactId = chatItem.dataset.contactId;
|
||||
@@ -1818,6 +2274,13 @@ function bindEvents() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (service === 'multi-char-table') {
|
||||
// 切换角色表格区域的显示/隐藏
|
||||
const section = document.getElementById('wechat-char-tables-section');
|
||||
section?.classList.toggle('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const label = item.querySelector('span')?.textContent || '该';
|
||||
showToast(`"${label}" 功能开发中...`, 'info');
|
||||
});
|
||||
@@ -2420,13 +2883,18 @@ function bindEvents() {
|
||||
}
|
||||
|
||||
function init() {
|
||||
console.log('[可乐] init() 开始');
|
||||
loadSettings();
|
||||
console.log('[可乐] loadSettings 调用完成,开始 getSettings');
|
||||
const settings = getSettings();
|
||||
console.log('[可乐] getSettings 完成,开始 seedDefaultUserPersonaFromST');
|
||||
if (seedDefaultUserPersonaFromST(settings)) {
|
||||
requestSave();
|
||||
}
|
||||
console.log('[可乐] seedDefaultUserPersonaFromST 完成,开始 generatePhoneHTML');
|
||||
|
||||
const phoneHTML = generatePhoneHTML();
|
||||
console.log('[可乐] generatePhoneHTML 完成');
|
||||
document.body.insertAdjacentHTML('beforeend', phoneHTML);
|
||||
|
||||
setupPhoneAutoCentering();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { requestSave } from './save-manager.js';
|
||||
import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { getContext } from '../../../extensions.js';
|
||||
import { formatQuoteDate } from './utils.js';
|
||||
import { formatQuoteDate, sleep } from './utils.js';
|
||||
import { isInGroupChat, getCurrentGroupIndex, openGroupChat } from './group-chat.js';
|
||||
|
||||
// 当前显示菜单的消息索引
|
||||
@@ -118,12 +118,24 @@ export function showMessageMenu(msgElement, msgIndex, event) {
|
||||
msg = contact?.chatHistory?.[msgIndex];
|
||||
}
|
||||
|
||||
// 优先从历史记录判断,其次从元素属性判断(处理分割显示的消息)
|
||||
let isUserMessage = msg?.role === 'user';
|
||||
if (msg === undefined) {
|
||||
// 如果找不到消息记录,尝试从元素属性获取
|
||||
const roleAttr = msgElement?.dataset?.msgRole || msgElement?.closest?.('[data-msg-role]')?.dataset?.msgRole;
|
||||
isUserMessage = roleAttr === 'user';
|
||||
// 从元素或其父元素获取 role 属性
|
||||
let roleAttr = msgElement?.dataset?.msgRole;
|
||||
if (!roleAttr) {
|
||||
// 尝试从父元素获取(气泡元素在 .wechat-message 内部)
|
||||
const parentMsg = msgElement?.closest?.('.wechat-message') || msgElement?.parentElement?.closest?.('.wechat-message');
|
||||
roleAttr = parentMsg?.dataset?.msgRole;
|
||||
}
|
||||
let isUserMessage = roleAttr === 'user';
|
||||
|
||||
// 如果元素属性不存在,回退到历史记录判断
|
||||
if (!roleAttr && msg) {
|
||||
isUserMessage = msg.role === 'user';
|
||||
}
|
||||
|
||||
// 最后检查:通过元素类名判断(self 类表示用户消息)
|
||||
if (!roleAttr && !msg) {
|
||||
const parentMsg = msgElement?.closest?.('.wechat-message');
|
||||
isUserMessage = parentMsg?.classList?.contains('self') || false;
|
||||
}
|
||||
|
||||
// 检测是否是语音消息
|
||||
@@ -470,6 +482,8 @@ async function regenerateMessage(msgIndex, contact) {
|
||||
|
||||
// 触发AI重新回复
|
||||
try {
|
||||
// 等待 DOM 更新后再显示 typing 指示器
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
showTypingIndicator(contact);
|
||||
|
||||
const { callAI } = await import('./ai.js');
|
||||
@@ -490,12 +504,17 @@ async function regenerateMessage(msgIndex, contact) {
|
||||
if (!finalMsg) continue;
|
||||
|
||||
let isVoice = false;
|
||||
const voiceMatch = finalMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
||||
const voiceMatch = finalMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||
if (voiceMatch) {
|
||||
finalMsg = voiceMatch[1];
|
||||
isVoice = true;
|
||||
}
|
||||
|
||||
// 每条消息都要有typing效果和2-2.5秒延迟(与普通回复一致)
|
||||
showTypingIndicator(contact);
|
||||
await sleep(2000 + Math.random() * 500);
|
||||
hideTypingIndicator();
|
||||
|
||||
contact.chatHistory.push({
|
||||
role: 'assistant',
|
||||
content: finalMsg,
|
||||
@@ -721,7 +740,8 @@ function getRealMsgIndex(container, msgElement) {
|
||||
const content = msg.content || '';
|
||||
const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic;
|
||||
// 检查是否包含 ||| 或 <meme> 标签(这些会导致消息被分割显示)
|
||||
if (!isSpecial && (content.indexOf('|||') >= 0 || /<\s*meme\s*>/i.test(content))) {
|
||||
// 注意:只有 assistant 消息才会被分割,用户消息不会分割
|
||||
if (msg.role === 'assistant' && !isSpecial && (content.indexOf('|||') >= 0 || /<\s*meme\s*>/i.test(content))) {
|
||||
// 使用 splitAIMessages 计算实际分割数量
|
||||
const parts = splitAIMessages(content).filter(p => p && p.trim());
|
||||
visualMsgCount = parts.length || 1;
|
||||
|
||||
1501
multi-char-import.js
Normal file
1501
multi-char-import.js
Normal file
File diff suppressed because it is too large
Load Diff
433
multi-person-chat.js
Normal file
433
multi-person-chat.js
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* 多人群聊模块
|
||||
* 特点:无头像,名字+气泡,左对齐,世界观注入
|
||||
*/
|
||||
|
||||
import { requestSave, saveNow } from './save-manager.js';
|
||||
import { getSettings } from './config.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { escapeHtml, sleep, formatMessageTime } from './utils.js';
|
||||
import { refreshChatList } from './ui.js';
|
||||
|
||||
// 当前多人群聊索引
|
||||
export let currentMultiPersonChatIndex = -1;
|
||||
|
||||
// 设置当前多人群聊索引
|
||||
export function setCurrentMultiPersonChatIndex(index) {
|
||||
currentMultiPersonChatIndex = index;
|
||||
}
|
||||
|
||||
// 打开多人群聊
|
||||
export function openMultiPersonChat(chatIndex) {
|
||||
console.log('[可乐] openMultiPersonChat 被调用, chatIndex:', chatIndex);
|
||||
const settings = getSettings();
|
||||
const chat = settings.multiPersonChats?.[chatIndex];
|
||||
if (!chat) return;
|
||||
|
||||
currentMultiPersonChatIndex = chatIndex;
|
||||
|
||||
// 确保 chatHistory 存在
|
||||
if (!chat.chatHistory) chat.chatHistory = [];
|
||||
|
||||
// 隐藏主页,显示聊天页
|
||||
document.getElementById('wechat-main-content')?.classList.add('hidden');
|
||||
document.getElementById('wechat-chat-page')?.classList.remove('hidden');
|
||||
document.getElementById('wechat-chat-title').textContent = `${chat.name}(${chat.members.length})`;
|
||||
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
const chatHistory = chat.chatHistory;
|
||||
|
||||
if (chatHistory.length === 0) {
|
||||
messagesContainer.innerHTML = '';
|
||||
} else {
|
||||
messagesContainer.innerHTML = renderMultiPersonChatHistory(chat, chatHistory);
|
||||
}
|
||||
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
// 标记当前是多人群聊模式
|
||||
messagesContainer.dataset.isMultiPerson = 'true';
|
||||
messagesContainer.dataset.multiPersonIndex = chatIndex;
|
||||
messagesContainer.dataset.isGroup = 'false'; // 区别于普通群聊
|
||||
}
|
||||
|
||||
// 渲染多人群聊历史记录
|
||||
function renderMultiPersonChatHistory(chat, chatHistory) {
|
||||
let html = '';
|
||||
let lastTimestamp = 0;
|
||||
const TIME_GAP_THRESHOLD = 5 * 60 * 1000;
|
||||
|
||||
chatHistory.forEach((msg, index) => {
|
||||
const msgTimestamp = msg.timestamp || 0;
|
||||
|
||||
// 时间戳显示
|
||||
if (index === 0 || (msgTimestamp - lastTimestamp > TIME_GAP_THRESHOLD)) {
|
||||
const timeLabel = formatMessageTime(msgTimestamp);
|
||||
if (timeLabel) {
|
||||
html += `<div class="wechat-msg-time">${timeLabel}</div>`;
|
||||
}
|
||||
}
|
||||
lastTimestamp = msgTimestamp;
|
||||
|
||||
if (msg.role === 'user') {
|
||||
// 用户消息:右对齐,有气泡
|
||||
html += `
|
||||
<div class="wechat-message self">
|
||||
<div class="wechat-message-content">
|
||||
<div class="wechat-message-bubble">${escapeHtml(msg.content)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// 角色消息:无头像,名字+气泡,左对齐
|
||||
const charName = msg.characterName || '未知';
|
||||
html += `
|
||||
<div class="wechat-message wechat-mp-message">
|
||||
<div class="wechat-message-content">
|
||||
<div class="wechat-mp-sender">${escapeHtml(charName)}</div>
|
||||
<div class="wechat-message-bubble">${escapeHtml(msg.content)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 追加多人群聊消息到界面
|
||||
export function appendMultiPersonMessage(role, content, characterName = null) {
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
|
||||
if (role === 'user') {
|
||||
messageDiv.className = 'wechat-message self';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="wechat-message-content">
|
||||
<div class="wechat-message-bubble">${escapeHtml(content)}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
messageDiv.className = 'wechat-message wechat-mp-message';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="wechat-message-content">
|
||||
<div class="wechat-mp-sender">${escapeHtml(characterName || '未知')}</div>
|
||||
<div class="wechat-message-bubble">${escapeHtml(content)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 显示多人群聊打字指示器
|
||||
export function showMultiPersonTypingIndicator(characterName) {
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
hideMultiPersonTypingIndicator();
|
||||
|
||||
const typingDiv = document.createElement('div');
|
||||
typingDiv.className = 'wechat-message wechat-mp-message wechat-typing-wrapper';
|
||||
typingDiv.id = 'wechat-mp-typing-indicator';
|
||||
|
||||
typingDiv.innerHTML = `
|
||||
<div class="wechat-message-content">
|
||||
<div class="wechat-mp-sender">${escapeHtml(characterName || '...')}</div>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(typingDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 隐藏多人群聊打字指示器
|
||||
export function hideMultiPersonTypingIndicator() {
|
||||
const indicator = document.getElementById('wechat-mp-typing-indicator');
|
||||
if (indicator) indicator.remove();
|
||||
}
|
||||
|
||||
// 构建多人群聊系统提示词
|
||||
function buildMultiPersonSystemPrompt(chat, respondingMembers) {
|
||||
const settings = getSettings();
|
||||
let systemPrompt = '';
|
||||
|
||||
// 世界观(必读)
|
||||
if (chat.worldView) {
|
||||
systemPrompt += `【世界观设定】\n${chat.worldView}\n\n`;
|
||||
}
|
||||
|
||||
// 参与角色信息
|
||||
systemPrompt += `【参与角色】\n`;
|
||||
systemPrompt += `这是一个包含 ${chat.members.length} 位角色的多人对话场景。\n\n`;
|
||||
|
||||
chat.members.forEach((member, idx) => {
|
||||
systemPrompt += `角色 ${idx + 1}: ${member.name}\n`;
|
||||
if (member.gender) systemPrompt += ` 性别: ${member.gender}\n`;
|
||||
if (member.age) systemPrompt += ` 年龄: ${member.age}\n`;
|
||||
if (member.description) systemPrompt += ` 描述: ${member.description}\n`;
|
||||
systemPrompt += '\n';
|
||||
});
|
||||
|
||||
// 本轮回复的角色
|
||||
if (respondingMembers && respondingMembers.length > 0) {
|
||||
systemPrompt += `【本轮发言角色】\n`;
|
||||
systemPrompt += `本轮需要以下角色发言:${respondingMembers.map(m => m.name).join('、')}\n\n`;
|
||||
}
|
||||
|
||||
// 回复格式说明
|
||||
systemPrompt += `【回复格式】
|
||||
你需要模拟多位角色的对话。请按以下格式回复:
|
||||
|
||||
[角色名]: 对话内容
|
||||
|
||||
如果有多个角色发言,请用 ||| 分隔每条消息。
|
||||
|
||||
示例:
|
||||
[${chat.members[0]?.name || '角色A'}]: 你好啊 ||| [${chat.members[1]?.name || '角色B'}]: 嗨,好久不见
|
||||
|
||||
规则:
|
||||
1. 每个角色保持自己的性格特点
|
||||
2. 对话要自然流畅,像真实聊天
|
||||
3. 每条消息简短自然(1-3句话)
|
||||
4. 可以使用表情符号
|
||||
5. 角色之间可以互相回应、互动
|
||||
`;
|
||||
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
// 选择本轮回复的角色(3-5人)
|
||||
function selectRespondingMembers(chat, userMessage) {
|
||||
const members = chat.members || [];
|
||||
const totalMembers = members.length;
|
||||
|
||||
// 根据群成员数量决定每轮回复人数
|
||||
let respondCount;
|
||||
if (totalMembers <= 5) {
|
||||
// 5人及以下,全部回复
|
||||
respondCount = totalMembers;
|
||||
} else if (totalMembers <= 10) {
|
||||
// 6-10人,每轮3-5人
|
||||
respondCount = Math.min(5, Math.max(3, Math.floor(totalMembers * 0.5)));
|
||||
} else {
|
||||
// 10人以上,每轮5人
|
||||
respondCount = 5;
|
||||
}
|
||||
|
||||
// 随机打乱成员顺序
|
||||
const shuffled = [...members].sort(() => Math.random() - 0.5);
|
||||
|
||||
// 取前 respondCount 个
|
||||
return shuffled.slice(0, respondCount);
|
||||
}
|
||||
|
||||
// 调用多人群聊 AI
|
||||
async function callMultiPersonAI(chat, userMessage, respondingMembers) {
|
||||
const settings = getSettings();
|
||||
|
||||
// 使用全局 API 配置
|
||||
const apiUrl = settings.apiUrl;
|
||||
const apiKey = settings.apiKey;
|
||||
const apiModel = settings.selectedModel;
|
||||
|
||||
if (!apiUrl || !apiModel) {
|
||||
throw new Error('请先配置 AI 接口');
|
||||
}
|
||||
|
||||
const systemPrompt = buildMultiPersonSystemPrompt(chat, respondingMembers);
|
||||
|
||||
const messages = [{ role: 'system', content: systemPrompt }];
|
||||
|
||||
// 添加历史消息
|
||||
const chatHistory = chat.chatHistory || [];
|
||||
const recentHistory = chatHistory.slice(-50);
|
||||
recentHistory.forEach(msg => {
|
||||
if (msg.role === 'user') {
|
||||
messages.push({ role: 'user', content: msg.content });
|
||||
} else {
|
||||
const formattedContent = msg.characterName
|
||||
? `[${msg.characterName}]: ${msg.content}`
|
||||
: msg.content;
|
||||
messages.push({ role: 'assistant', content: formattedContent });
|
||||
}
|
||||
});
|
||||
|
||||
messages.push({ role: 'user', content: userMessage });
|
||||
|
||||
const chatUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(chatUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: apiModel,
|
||||
messages,
|
||||
temperature: 1,
|
||||
max_tokens: 4096
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API 错误 (${response.status}): ${errorText.substring(0, 100)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const rawResponse = data.choices?.[0]?.message?.content || '';
|
||||
|
||||
return parseMultiPersonResponse(rawResponse, chat.members);
|
||||
}
|
||||
|
||||
// 解析多人群聊 AI 回复
|
||||
function parseMultiPersonResponse(response, members) {
|
||||
const results = [];
|
||||
|
||||
// 按 ||| 分隔多条消息
|
||||
const parts = response.split('|||').map(p => p.trim()).filter(p => p);
|
||||
|
||||
parts.forEach(part => {
|
||||
// 匹配 [角色名]: 内容 格式
|
||||
const match = part.match(/^\[(.+?)\][::]\s*(.+)$/s);
|
||||
|
||||
if (match) {
|
||||
const charName = match[1].trim();
|
||||
const content = match[2].trim();
|
||||
|
||||
// 查找对应的成员
|
||||
const member = members.find(m => m.name === charName);
|
||||
|
||||
results.push({
|
||||
characterName: member?.name || charName,
|
||||
content: content
|
||||
});
|
||||
} else {
|
||||
// 无法解析格式时,作为第一个角色的消息
|
||||
if (members.length > 0 && part.trim()) {
|
||||
results.push({
|
||||
characterName: members[0].name,
|
||||
content: part.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// 发送多人群聊消息
|
||||
export async function sendMultiPersonMessage(messageText) {
|
||||
console.log('[可乐] sendMultiPersonMessage 被调用', { messageText, currentMultiPersonChatIndex });
|
||||
|
||||
if (currentMultiPersonChatIndex < 0) return;
|
||||
|
||||
const settings = getSettings();
|
||||
const chat = settings.multiPersonChats?.[currentMultiPersonChatIndex];
|
||||
if (!chat) return;
|
||||
|
||||
const now = new Date();
|
||||
const timeStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
||||
const msgTimestamp = Date.now();
|
||||
|
||||
// 清空输入框
|
||||
const input = document.getElementById('wechat-input');
|
||||
if (input) input.value = '';
|
||||
window.updateSendButtonState?.();
|
||||
|
||||
// 显示用户消息
|
||||
appendMultiPersonMessage('user', messageText);
|
||||
|
||||
// 确保 chatHistory 存在
|
||||
if (!chat.chatHistory) chat.chatHistory = [];
|
||||
|
||||
// 添加到历史
|
||||
chat.chatHistory.push({
|
||||
role: 'user',
|
||||
content: messageText,
|
||||
time: timeStr,
|
||||
timestamp: msgTimestamp
|
||||
});
|
||||
|
||||
// 立即保存
|
||||
saveNow();
|
||||
|
||||
// 选择本轮回复的角色
|
||||
const respondingMembers = selectRespondingMembers(chat, messageText);
|
||||
|
||||
// 显示第一个角色的打字指示器
|
||||
showMultiPersonTypingIndicator(respondingMembers[0]?.name);
|
||||
|
||||
try {
|
||||
// 调用 AI
|
||||
const responses = await callMultiPersonAI(chat, messageText, respondingMembers);
|
||||
|
||||
hideMultiPersonTypingIndicator();
|
||||
|
||||
// 逐条显示 AI 回复,带 typing 效果
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const resp = responses[i];
|
||||
|
||||
// 显示 typing 指示器
|
||||
showMultiPersonTypingIndicator(resp.characterName);
|
||||
await sleep(600 + Math.random() * 400); // 0.6-1秒
|
||||
hideMultiPersonTypingIndicator();
|
||||
|
||||
// 添加到历史
|
||||
chat.chatHistory.push({
|
||||
role: 'assistant',
|
||||
content: resp.content,
|
||||
characterName: resp.characterName,
|
||||
time: timeStr,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 显示消息
|
||||
appendMultiPersonMessage('assistant', resp.content, resp.characterName);
|
||||
}
|
||||
|
||||
// 更新最后消息
|
||||
if (responses.length > 0) {
|
||||
const lastResp = responses[responses.length - 1];
|
||||
chat.lastMessage = `[${lastResp.characterName}]: ${lastResp.content}`;
|
||||
}
|
||||
chat.lastMessageTime = Date.now();
|
||||
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
|
||||
} catch (err) {
|
||||
hideMultiPersonTypingIndicator();
|
||||
console.error('[可乐] 多人群聊 AI 调用失败:', err);
|
||||
|
||||
appendMultiPersonMessage('assistant', `⚠️ ${err.message}`, '系统');
|
||||
requestSave();
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前是否在多人群聊
|
||||
export function isInMultiPersonChat() {
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
return messagesContainer?.dataset.isMultiPerson === 'true';
|
||||
}
|
||||
|
||||
// 获取当前多人群聊索引
|
||||
export function getCurrentMultiPersonIndex() {
|
||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||
if (messagesContainer?.dataset.isMultiPerson === 'true') {
|
||||
const index = parseInt(messagesContainer.dataset.multiPersonIndex);
|
||||
return isNaN(index) ? -1 : index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
249
phone-html.js
249
phone-html.js
@@ -118,6 +118,10 @@ export function generatePhoneHTML() {
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M7 16.5c0-2 2.2-3.5 5-3.5s5 1.5 5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
|
||||
<span id="wechat-floating-ball-text">悬浮窗</span>
|
||||
</div>
|
||||
<div class="wechat-dropdown-item" id="wechat-menu-clear-cache">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
|
||||
<span>清除缓存</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加朋友页面 -->
|
||||
@@ -150,6 +154,13 @@ export function generatePhoneHTML() {
|
||||
<div class="wechat-add-option-text">导入角色卡 (JSON)</div>
|
||||
<span class="wechat-add-option-arrow">›</span>
|
||||
</div>
|
||||
<div class="wechat-add-option" id="wechat-import-multi-card">
|
||||
<div class="wechat-add-option-icon">
|
||||
<svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
||||
</div>
|
||||
<div class="wechat-add-option-text">导入多人卡</div>
|
||||
<span class="wechat-add-option-arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,6 +178,14 @@ export function generatePhoneHTML() {
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
||||
<span>撤回消息</span>
|
||||
</div>
|
||||
<div class="wechat-dropdown-item hidden" id="wechat-menu-invite-member">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||
<path d="M3 21v-2a4 4 0 014-4h4a4 4 0 014 4v2" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||
<path d="M19 8v6M16 11h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>邀请成员</span>
|
||||
</div>
|
||||
<div class="wechat-dropdown-item" id="wechat-menu-chat-bg">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"/><path d="M21 15l-5-5L5 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
|
||||
<span>聊天背景</span>
|
||||
@@ -770,6 +789,14 @@ function generateServicePageHTML(settings) {
|
||||
<div class="wechat-service-grid">
|
||||
<div class="wechat-service-item" data-service="meme-stickers"><div class="wechat-service-icon purple" style="background: linear-gradient(135deg, #9c27b0, #e91e63);"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="9" cy="9" r="1.5" fill="currentColor"/><circle cx="15" cy="9" r="1.5" fill="currentColor"/><path d="M7 14c1.5 3 4 4 5 4s3.5-1 5-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>Meme表情</span></div>
|
||||
<div class="wechat-service-item" data-service="voice-api"><div class="wechat-service-icon" style="background: linear-gradient(135deg, #00bcd4, #009688);"><svg viewBox="0 0 24 24"><path d="M12 1a4 4 0 00-4 4v7a4 4 0 008 0V5a4 4 0 00-4-4z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2M12 19v4M8 23h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>语音API</span></div>
|
||||
<div class="wechat-service-item" data-service="multi-char-table"><div class="wechat-service-icon" style="background: linear-gradient(135deg, #3f51b5, #7986cb);"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>多人卡表格</span></div>
|
||||
</div>
|
||||
<!-- 角色表格容器(可折叠) -->
|
||||
<div id="wechat-char-tables-section" class="hidden">
|
||||
<div class="wechat-service-section-title" style="margin-top: 16px;">已解析的角色表格</div>
|
||||
<div id="wechat-char-tables-container">
|
||||
<!-- 角色表格由 JS 动态填充 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wechat-service-section">
|
||||
@@ -1010,7 +1037,7 @@ function generateModalsHTML(settings) {
|
||||
<div id="wechat-group-contacts-list" style="max-height: 300px; overflow-y: auto; border: 1px solid var(--wechat-border); border-radius: 8px; padding: 8px;"></div>
|
||||
|
||||
<div style="margin-top: 12px; text-align: center; color: var(--wechat-text-secondary); font-size: 13px;">
|
||||
已选择 <span id="wechat-group-selected-count" style="color: var(--wechat-primary); font-weight: 500;">0</span> 人
|
||||
已选择 <span id="wechat-group-selected-count" style="color: var(--wechat-green); font-weight: 500;">0</span> 人
|
||||
</div>
|
||||
|
||||
<div class="wechat-modal-actions" style="margin-top: 16px;">
|
||||
@@ -1035,6 +1062,71 @@ function generateModalsHTML(settings) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 多人群聊配置弹窗 -->
|
||||
<div id="wechat-mp-api-modal" class="wechat-modal hidden">
|
||||
<div class="wechat-modal-content" style="position: relative; max-width: 380px; max-height: 85vh; overflow-y: auto;">
|
||||
<button class="wechat-modal-close-x" id="wechat-mp-api-close">×</button>
|
||||
<div class="wechat-modal-title">群聊设置</div>
|
||||
|
||||
<!-- 头像和群名编辑区 -->
|
||||
<div class="wechat-settings-group" style="padding: 12px; background: var(--wechat-bg-secondary); border-radius: 8px; margin-bottom: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
||||
<div id="wechat-mp-avatar-preview" style="width: 60px; height: 60px; border-radius: 8px; overflow: hidden; background: #fff; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; color: #000; cursor: pointer; border: 1px solid #ddd;" title="点击更换头像">群</div>
|
||||
<div style="flex: 1;">
|
||||
<span class="wechat-settings-label" style="font-size: 12px; margin-bottom: 4px; display: block;">群聊名称</span>
|
||||
<input type="text" class="wechat-settings-input" id="wechat-mp-name-input" placeholder="群聊" style="width: 100%; box-sizing: border-box;">
|
||||
</div>
|
||||
</div>
|
||||
<button class="wechat-btn wechat-btn-small" id="wechat-mp-change-avatar" style="width: 100%;">更换头像</button>
|
||||
<input type="file" id="wechat-mp-avatar-file" accept="image/*" style="display: none;">
|
||||
</div>
|
||||
|
||||
<!-- API配置区 -->
|
||||
<div class="wechat-settings-group" style="padding: 12px; background: var(--wechat-bg-secondary); border-radius: 8px; margin-bottom: 12px;">
|
||||
<div class="wechat-settings-item" style="margin-bottom: 12px;">
|
||||
<span class="wechat-settings-label">使用独立API</span>
|
||||
<div class="wechat-switch" id="wechat-mp-use-custom-api"></div>
|
||||
</div>
|
||||
|
||||
<div id="wechat-mp-global-tip" style="font-size: 12px; color: var(--wechat-text-secondary);">
|
||||
将使用全局 AI 配置
|
||||
</div>
|
||||
|
||||
<div id="wechat-mp-api-config" class="hidden" style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div>
|
||||
<span class="wechat-settings-label" style="font-size: 12px; margin-bottom: 4px; display: block;">API 地址</span>
|
||||
<input type="text" class="wechat-settings-input" id="wechat-mp-api-url" placeholder="https://api.example.com/v1" style="width: 100%; box-sizing: border-box;">
|
||||
</div>
|
||||
<div>
|
||||
<span class="wechat-settings-label" style="font-size: 12px; margin-bottom: 4px; display: block;">API 密钥</span>
|
||||
<input type="password" class="wechat-settings-input" id="wechat-mp-api-key" placeholder="sk-xxx" style="width: 100%; box-sizing: border-box;">
|
||||
</div>
|
||||
<div>
|
||||
<span class="wechat-settings-label" style="font-size: 12px; margin-bottom: 4px; display: block;">模型</span>
|
||||
<div style="display: flex; gap: 8px;" id="wechat-mp-model-select-wrapper">
|
||||
<select class="wechat-settings-input" id="wechat-mp-model-select" style="flex: 1; box-sizing: border-box;">
|
||||
<option value="">---请选择模型---</option>
|
||||
</select>
|
||||
<button class="wechat-btn wechat-btn-small" id="wechat-mp-model-manual" style="white-space: nowrap;">手动</button>
|
||||
<button class="wechat-btn wechat-btn-small wechat-btn-primary" id="wechat-mp-fetch-model" style="white-space: nowrap;">获取</button>
|
||||
</div>
|
||||
<div style="display: none; gap: 8px;" id="wechat-mp-model-input-wrapper">
|
||||
<input type="text" class="wechat-settings-input" id="wechat-mp-model-input" placeholder="手动输入模型名称" style="flex: 1; box-sizing: border-box;">
|
||||
<button class="wechat-btn wechat-btn-small" id="wechat-mp-model-back" style="white-space: nowrap;">返回</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-top: 4px;">
|
||||
<button class="wechat-btn wechat-btn-small" id="wechat-mp-test-api" style="flex: 1;">测试连接</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wechat-modal-actions">
|
||||
<button class="wechat-btn wechat-btn-primary" id="wechat-mp-api-save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1508,6 +1600,161 @@ function generateGiftPageHTML() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 多人卡导入弹窗 -->
|
||||
<div id="wechat-multi-import-modal" class="wechat-modal hidden">
|
||||
<div class="wechat-modal-content" style="max-width: 420px; position: relative;">
|
||||
<button class="wechat-modal-close-x" id="wechat-multi-import-close">×</button>
|
||||
<div class="wechat-modal-header">
|
||||
<span>导入多人卡</span>
|
||||
</div>
|
||||
|
||||
<div class="wechat-modal-body">
|
||||
<!-- AI 配置区 -->
|
||||
<div class="wechat-settings-section">
|
||||
<div class="wechat-settings-title">解析 AI 配置</div>
|
||||
|
||||
<!-- 使用独立API开关 -->
|
||||
<div class="wechat-settings-row">
|
||||
<span>使用独立API</span>
|
||||
<div class="wechat-switch" id="wechat-multi-import-custom-api"></div>
|
||||
</div>
|
||||
|
||||
<!-- API配置(默认隐藏) -->
|
||||
<div id="wechat-multi-import-api-config" class="hidden" style="margin-top: 12px;">
|
||||
<div class="wechat-settings-item">
|
||||
<label>API 地址</label>
|
||||
<input type="text" class="wechat-settings-input"
|
||||
id="wechat-multi-import-api-url"
|
||||
placeholder="https://api.example.com/v1">
|
||||
</div>
|
||||
|
||||
<div class="wechat-settings-item">
|
||||
<label>API 密钥</label>
|
||||
<input type="password" class="wechat-settings-input"
|
||||
id="wechat-multi-import-api-key"
|
||||
placeholder="sk-...">
|
||||
</div>
|
||||
|
||||
<div class="wechat-settings-item">
|
||||
<label>模型</label>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<div id="wechat-multi-import-model-select-wrapper" style="flex: 1; display: flex;">
|
||||
<select class="wechat-settings-input wechat-settings-select"
|
||||
id="wechat-multi-import-model-select" style="flex: 1;">
|
||||
<option value="">--请选择模型--</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="wechat-multi-import-model-input-wrapper" style="flex: 1; display: none;">
|
||||
<input type="text" class="wechat-settings-input"
|
||||
id="wechat-multi-import-model-input"
|
||||
placeholder="手动输入模型名">
|
||||
</div>
|
||||
<button class="wechat-btn wechat-btn-small" id="wechat-multi-import-model-toggle">手动</button>
|
||||
<button class="wechat-btn wechat-btn-small wechat-btn-primary" id="wechat-multi-import-fetch-model">获取</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="wechat-btn" id="wechat-multi-import-test" style="width: 100%; margin-top: 8px;">
|
||||
测试连接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 使用全局配置提示 -->
|
||||
<div id="wechat-multi-import-global-tip" style="margin-top: 8px; font-size: 12px; color: var(--wechat-text-secondary);">
|
||||
将使用全局 AI 配置进行解析
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件选择区 -->
|
||||
<div class="wechat-settings-section" style="margin-top: 16px;">
|
||||
<div class="wechat-settings-title">选择文件</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="wechat-btn" id="wechat-multi-import-select-png" style="flex: 1;">
|
||||
选择 PNG 文件
|
||||
</button>
|
||||
<button class="wechat-btn" id="wechat-multi-import-select-json" style="flex: 1;">
|
||||
选择 JSON 文件
|
||||
</button>
|
||||
</div>
|
||||
<div id="wechat-multi-import-file-info" style="margin-top: 8px; font-size: 13px; color: var(--wechat-text-secondary);">
|
||||
未选择文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wechat-modal-footer">
|
||||
<button class="wechat-btn" id="wechat-multi-import-cancel">取消</button>
|
||||
<button class="wechat-btn wechat-btn-primary" id="wechat-multi-import-start" disabled>开始解析</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色选择弹窗(选择导入哪些角色为联系人/群聊) -->
|
||||
<div id="wechat-char-select-modal" class="wechat-modal hidden">
|
||||
<div class="wechat-modal-content" style="max-width: 450px; max-height: 80vh; display: flex; flex-direction: column;">
|
||||
<div class="wechat-modal-header">
|
||||
<span>选择要导入的角色</span>
|
||||
<span class="wechat-modal-close" id="wechat-char-select-close">×</span>
|
||||
</div>
|
||||
|
||||
<div class="wechat-modal-body" style="flex: 1; overflow-y: auto; padding: 0;">
|
||||
<!-- 角色列表区 -->
|
||||
<div style="padding: 12px; border-bottom: 1px solid var(--wechat-border);">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||
<input type="checkbox" id="wechat-char-select-all" checked style="margin-right: 8px;">
|
||||
<label for="wechat-char-select-all" style="font-weight: bold;">创建独立联系人</label>
|
||||
<span id="wechat-char-select-count" style="margin-left: auto; font-size: 12px; color: var(--wechat-text-secondary);">0/0</span>
|
||||
</div>
|
||||
<div id="wechat-char-select-list" style="max-height: 250px; overflow-y: auto;">
|
||||
<!-- 角色列表动态填充 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群聊选项区 -->
|
||||
<div style="padding: 12px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||
<input type="checkbox" id="wechat-char-select-group" checked style="margin-right: 8px;">
|
||||
<label for="wechat-char-select-group" style="font-weight: bold;">同时创建群聊</label>
|
||||
</div>
|
||||
<div id="wechat-char-select-group-options">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
|
||||
<div id="wechat-char-select-group-avatar" style="width: 48px; height: 48px; background: #fff; border: 1px solid #ddd; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; color: #000;">群</div>
|
||||
<input type="text" id="wechat-char-select-group-name" class="wechat-settings-input" placeholder="群聊名称(可选)" style="flex: 1;">
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--wechat-text-secondary);">
|
||||
将包含上方勾选的联系人(至少需要2人)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wechat-modal-footer">
|
||||
<button class="wechat-btn" id="wechat-char-select-cancel">取消</button>
|
||||
<button class="wechat-btn wechat-btn-primary" id="wechat-char-select-confirm">确认导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- "其它信息"编辑弹窗 -->
|
||||
<div id="wechat-char-other-edit-modal" class="wechat-modal hidden">
|
||||
<div class="wechat-modal-content" style="max-width: 400px;">
|
||||
<div class="wechat-modal-header">
|
||||
<span id="wechat-char-other-edit-title">编辑其它信息</span>
|
||||
<span class="wechat-modal-close" id="wechat-char-other-edit-close">×</span>
|
||||
</div>
|
||||
<div class="wechat-modal-body" style="padding: 16px;">
|
||||
<textarea id="wechat-char-other-edit-textarea"
|
||||
class="wechat-settings-input"
|
||||
style="width: 100%; height: 200px; resize: vertical; font-size: 14px; line-height: 1.5;"
|
||||
placeholder="其它信息"></textarea>
|
||||
</div>
|
||||
<div class="wechat-modal-footer">
|
||||
<button class="wechat-btn" id="wechat-char-other-edit-cancel">取消</button>
|
||||
<button class="wechat-btn wechat-btn-primary" id="wechat-char-other-edit-save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 玩具控制页面 -->
|
||||
<div id="wechat-toy-control-page" class="wechat-toy-control-page hidden">
|
||||
<div class="wechat-navbar wechat-toy-control-navbar">
|
||||
|
||||
308
proactive-message.js
Normal file
308
proactive-message.js
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* 角色主动发消息系统
|
||||
* 规则:每2-3轮随机触发,保底4轮必触发
|
||||
*/
|
||||
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getSettings, splitAIMessages } from './config.js';
|
||||
import { refreshChatList } from './ui.js';
|
||||
import { showNotificationBanner } from './toast.js';
|
||||
import { buildSystemPrompt } from './ai.js';
|
||||
|
||||
// 配置
|
||||
const CONFIG = {
|
||||
minRounds: 2, // 最少2轮后可触发
|
||||
maxRounds: 3, // 随机2-3轮
|
||||
guaranteeRounds: 4, // 保底4轮必触发
|
||||
cooldownMs: 30 * 1000, // 30秒冷却防止刷屏
|
||||
groupEmotionChance: 0.3 // 群聊情绪触发概率30%
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机阈值 (2-3)
|
||||
*/
|
||||
function randomThreshold() {
|
||||
return CONFIG.minRounds + Math.floor(Math.random() * (CONFIG.maxRounds - CONFIG.minRounds + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间字符串
|
||||
*/
|
||||
function formatTimeStr(date) {
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送主动消息
|
||||
* @param {object} contact - 联系人对象
|
||||
* @param {string} type - 消息类型:'daily' | 'angry_private' | 'want_private'
|
||||
* @param {Array} groupContext - 群聊上下文(可选,用于群聊触发的私聊)
|
||||
*/
|
||||
async function sendProactiveMessage(contact, type = 'daily', groupContext = []) {
|
||||
const prompts = {
|
||||
// 日常主动消息
|
||||
daily: `[你现在要主动给用户发一条消息。可以是:
|
||||
1. 分享你正在做的事情
|
||||
2. 想起用户了,打个招呼
|
||||
3. 看到什么有趣的东西想分享
|
||||
4. 撒娇或关心用户
|
||||
根据你的性格和当前心情,发1-2条简短消息,像真实聊天一样自然。]`,
|
||||
|
||||
// 群聊生气后私下发
|
||||
angry_private: `[你刚才在群聊里和用户有些不愉快,现在想私下和用户说点什么。
|
||||
可以是:生气、委屈、想解释、想和好,或者继续吵。
|
||||
根据你的性格决定态度,发1-2条消息。]`,
|
||||
|
||||
// 群聊中想私聊
|
||||
want_private: `[你在群聊里聊到一些话题,想私下单独和用户聊点事情。
|
||||
发一条消息说明原因,像"有件事想单独跟你说"这样自然的开场。]`,
|
||||
};
|
||||
|
||||
try {
|
||||
// 如果是群聊触发的私聊,需要特殊处理
|
||||
if ((type === 'angry_private' || type === 'want_private') && groupContext.length > 0) {
|
||||
// 使用带群聊上下文的 AI 调用
|
||||
const response = await callAIWithGroupContext(contact, prompts[type], groupContext);
|
||||
await processProactiveResponse(contact, response, type);
|
||||
} else {
|
||||
// 普通主动消息,使用标准 callAI
|
||||
const { callAI } = await import('./ai.js');
|
||||
const response = await callAI(contact, prompts[type] || prompts.daily);
|
||||
await processProactiveResponse(contact, response, type);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[可乐] 主动消息发送失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理主动消息的响应
|
||||
*/
|
||||
async function processProactiveResponse(contact, response, type) {
|
||||
const messages = splitAIMessages(response);
|
||||
const now = new Date();
|
||||
const timeStr = formatTimeStr(now);
|
||||
|
||||
if (!contact.chatHistory) contact.chatHistory = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
const content = msg.trim();
|
||||
if (!content) continue;
|
||||
|
||||
contact.chatHistory.push({
|
||||
role: 'assistant',
|
||||
content: content,
|
||||
time: timeStr,
|
||||
timestamp: Date.now(),
|
||||
isProactive: true // 标记为主动消息
|
||||
});
|
||||
|
||||
contact.unreadCount = (contact.unreadCount || 0) + 1;
|
||||
contact.lastMessage = content;
|
||||
}
|
||||
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
|
||||
// 显示通知横幅
|
||||
const previewText = messages[0]?.substring(0, 15) || '';
|
||||
showNotificationBanner('微信', `${contact.name}: ${previewText}${previewText.length >= 15 ? '...' : ''}`);
|
||||
|
||||
console.log(`[可乐] ${contact.name} 主动发消息 (${type})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带群聊上下文的 AI 调用
|
||||
* 用于群聊触发的私聊,确保AI知道群里发生了什么
|
||||
* @param {object} contact - 联系人对象
|
||||
* @param {string} userMessage - 用户消息(提示词)
|
||||
* @param {Array} groupContext - 群聊上下文
|
||||
*/
|
||||
async function callAIWithGroupContext(contact, userMessage, groupContext) {
|
||||
const { getApiConfig, fetchWithRetry, formatApiError } = await import('./ai.js');
|
||||
const settings = getSettings();
|
||||
|
||||
// 获取 API 配置
|
||||
let apiUrl, apiKey, apiModel;
|
||||
if (contact.useCustomApi) {
|
||||
apiUrl = contact.customApiUrl || '';
|
||||
apiKey = contact.customApiKey || '';
|
||||
apiModel = contact.customModel || '';
|
||||
const globalConfig = getApiConfig();
|
||||
if (!apiUrl) apiUrl = globalConfig.url;
|
||||
if (!apiKey) apiKey = globalConfig.key;
|
||||
if (!apiModel) apiModel = globalConfig.model;
|
||||
} else {
|
||||
const globalConfig = getApiConfig();
|
||||
apiUrl = globalConfig.url;
|
||||
apiKey = globalConfig.key;
|
||||
apiModel = globalConfig.model;
|
||||
}
|
||||
|
||||
if (!apiUrl) throw new Error('请先配置 API 地址');
|
||||
if (!apiModel) throw new Error('请先选择模型');
|
||||
|
||||
// 构建系统提示词(包含用户设定和世界书)
|
||||
const systemPrompt = buildSystemPrompt(contact);
|
||||
|
||||
// 构建消息数组
|
||||
const messages = [{ role: 'system', content: systemPrompt }];
|
||||
|
||||
// 添加群聊上下文(作为背景信息)
|
||||
if (groupContext.length > 0) {
|
||||
// 将群聊上下文格式化为一条系统消息
|
||||
const groupContextText = groupContext.map(msg => {
|
||||
const sender = msg.characterName || (msg.role === 'user' ? '用户' : '未知');
|
||||
return `${sender}: ${msg.content}`;
|
||||
}).join('\n');
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: `[以下是刚才群聊中的对话记录,你需要根据这些内容来决定私聊时说什么]\n\n${groupContextText}\n\n[群聊记录结束]`
|
||||
});
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: '好的,我已经了解了群聊中发生的事情。'
|
||||
});
|
||||
}
|
||||
|
||||
// 添加私聊历史记录(最近10条,让AI知道私聊的上下文)
|
||||
const chatHistory = contact.chatHistory || [];
|
||||
const recentPrivateHistory = chatHistory.slice(-10);
|
||||
recentPrivateHistory.forEach(msg => {
|
||||
if (msg.isMarker) return;
|
||||
messages.push({
|
||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
});
|
||||
});
|
||||
|
||||
// 添加当前提示词
|
||||
messages.push({ role: 'user', content: userMessage });
|
||||
|
||||
// 调用 API
|
||||
const chatUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetchWithRetry(
|
||||
chatUrl,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
model: apiModel,
|
||||
messages: messages,
|
||||
temperature: 1,
|
||||
max_tokens: 8196
|
||||
})
|
||||
},
|
||||
{ maxRetries: 3 }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await formatApiError(response, { retries: 0 }));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices?.[0]?.message?.content || '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户发消息后调用,检查其他联系人是否要主动发消息
|
||||
* @param {string} currentContactId - 当前聊天的联系人ID
|
||||
*/
|
||||
export async function checkOtherContactsProactive(currentContactId) {
|
||||
const settings = getSettings();
|
||||
|
||||
for (const contact of settings.contacts) {
|
||||
// 跳过当前聊天的联系人
|
||||
if (contact.id === currentContactId) continue;
|
||||
// 跳过被拉黑的
|
||||
if (contact.isBlocked) continue;
|
||||
// 跳过没有聊过天的(避免陌生人突然发消息)
|
||||
if (!contact.chatHistory || contact.chatHistory.length === 0) continue;
|
||||
|
||||
// 初始化计数器
|
||||
if (typeof contact.proactiveCounter !== 'number') {
|
||||
contact.proactiveCounter = 0;
|
||||
contact.proactiveThreshold = randomThreshold();
|
||||
}
|
||||
|
||||
// 递增计数
|
||||
contact.proactiveCounter++;
|
||||
|
||||
// 检查是否触发
|
||||
const shouldTrigger =
|
||||
contact.proactiveCounter >= CONFIG.guaranteeRounds || // 保底4轮
|
||||
contact.proactiveCounter >= contact.proactiveThreshold; // 随机阈值
|
||||
|
||||
if (!shouldTrigger) continue;
|
||||
|
||||
// 检查冷却时间
|
||||
if (Date.now() - (contact.lastProactiveAt || 0) < CONFIG.cooldownMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重置计数器和阈值
|
||||
contact.proactiveCounter = 0;
|
||||
contact.proactiveThreshold = randomThreshold();
|
||||
contact.lastProactiveAt = Date.now();
|
||||
|
||||
// 触发主动消息
|
||||
await sendProactiveMessage(contact, 'daily');
|
||||
}
|
||||
|
||||
requestSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* 群聊中检测到情绪后调用
|
||||
* @param {string} contactId - 联系人ID
|
||||
* @param {string} emotionType - 情绪类型:'negative' | 'want_private'
|
||||
* @param {Array} groupContext - 群聊上下文(最近40条消息)
|
||||
*/
|
||||
export async function triggerProactiveFromGroup(contactId, emotionType, groupContext = []) {
|
||||
const settings = getSettings();
|
||||
const contact = settings.contacts.find(c => c.id === contactId);
|
||||
|
||||
if (!contact || contact.isBlocked) return;
|
||||
|
||||
// 检查冷却
|
||||
if (Date.now() - (contact.lastProactiveAt || 0) < CONFIG.cooldownMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 群聊情绪触发有独立的概率
|
||||
if (Math.random() > CONFIG.groupEmotionChance) {
|
||||
console.log(`[可乐] ${contact.name} 群聊情绪触发未命中概率 (${CONFIG.groupEmotionChance * 100}%)`);
|
||||
return;
|
||||
}
|
||||
|
||||
contact.lastProactiveAt = Date.now();
|
||||
requestSave();
|
||||
|
||||
// 立即发送,传递群聊上下文
|
||||
const messageType = emotionType === 'negative' ? 'angry_private' : 'want_private';
|
||||
console.log(`[可乐] ${contact.name} 群聊情绪触发私聊 (${messageType}),群聊上下文 ${groupContext.length} 条`);
|
||||
await sendProactiveMessage(contact, messageType, groupContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置某个联系人的主动消息计数器
|
||||
* @param {string} contactId - 联系人ID
|
||||
*/
|
||||
export function resetProactiveCounter(contactId) {
|
||||
const settings = getSettings();
|
||||
const contact = settings.contacts.find(c => c.id === contactId);
|
||||
|
||||
if (contact) {
|
||||
contact.proactiveCounter = 0;
|
||||
contact.proactiveThreshold = randomThreshold();
|
||||
requestSave();
|
||||
}
|
||||
}
|
||||
|
||||
export { sendProactiveMessage };
|
||||
@@ -379,7 +379,13 @@ function cleanAIReply(text) {
|
||||
|
||||
// 移除特殊标记
|
||||
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
|
||||
// 移除 meme 表情包标签(多种可能的格式)
|
||||
reply = reply.replace(/<\s*meme\s*>[^<]*<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/<meme>[^<]*<\/meme>/gi, '').trim();
|
||||
|
||||
// 移除可能残留的单独标签
|
||||
reply = reply.replace(/<\/?meme>/gi, '').trim();
|
||||
|
||||
// 移除括号描述(中文和英文括号)
|
||||
reply = reply.replace(/([^)]+)/g, '').trim();
|
||||
@@ -828,20 +834,9 @@ export async function hangupCall() {
|
||||
}
|
||||
}
|
||||
|
||||
// 通话记录消息
|
||||
const callRecord = {
|
||||
role: callState.initiator === 'user' ? 'user' : 'assistant',
|
||||
content: callContent,
|
||||
time: timeStr,
|
||||
timestamp: Date.now(),
|
||||
isCallRecord: true,
|
||||
isRealVoice: true
|
||||
};
|
||||
|
||||
contact.chatHistory.push(callRecord);
|
||||
|
||||
// 保存通话历史
|
||||
// 保存通话历史和对话内容
|
||||
if (callState.messages && callState.messages.length > 0) {
|
||||
// 有对话内容时,直接将对话内容保存到 chatHistory(不显示通话记录标记)
|
||||
contact.realVoiceCallHistory = Array.isArray(contact.realVoiceCallHistory) ? contact.realVoiceCallHistory : [];
|
||||
contact.realVoiceCallHistory.push({
|
||||
type: 'real-voice',
|
||||
@@ -851,15 +846,42 @@ export async function hangupCall() {
|
||||
timestamp: Date.now(),
|
||||
messages: callState.messages.map(m => ({ role: m.role, content: m.content }))
|
||||
});
|
||||
|
||||
// 将通话对话内容保存到 chatHistory,让 AI 后续对话能识别
|
||||
callState.messages.forEach(msg => {
|
||||
contact.chatHistory.push({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
time: timeStr,
|
||||
timestamp: Date.now(),
|
||||
isRealVoiceContent: true // 标记为实时通话内容
|
||||
});
|
||||
});
|
||||
|
||||
// 刷新聊天界面显示对话内容
|
||||
if (currentChatIndex === callState.contactIndex) {
|
||||
openChat(currentChatIndex);
|
||||
}
|
||||
} else {
|
||||
// 没有对话内容时(取消/拒绝),显示通话记录标记
|
||||
const callRecord = {
|
||||
role: callState.initiator === 'user' ? 'user' : 'assistant',
|
||||
content: callContent,
|
||||
time: timeStr,
|
||||
timestamp: Date.now(),
|
||||
isCallRecord: true,
|
||||
isRealVoice: true
|
||||
};
|
||||
contact.chatHistory.push(callRecord);
|
||||
|
||||
// 在聊天界面显示通话记录
|
||||
if (currentChatIndex === callState.contactIndex) {
|
||||
appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', durationStr, contact);
|
||||
}
|
||||
}
|
||||
|
||||
contact.lastMessage = lastMessage;
|
||||
|
||||
// 在聊天界面显示通话记录
|
||||
if (currentChatIndex === callState.contactIndex) {
|
||||
appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', durationStr, contact);
|
||||
}
|
||||
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
}
|
||||
|
||||
526
style.css
526
style.css
@@ -276,6 +276,43 @@
|
||||
background: #d9363e;
|
||||
}
|
||||
|
||||
/* 多人群聊卡片 */
|
||||
.wechat-mp-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wechat-mp-card .wechat-card-avatar {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wechat-mp-avatar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wechat-mp-api-badge {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.wechat-mp-card-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wechat-mp-card .wechat-card-name {
|
||||
color: var(--wechat-text);
|
||||
}
|
||||
|
||||
/* ===== 搜索框 ===== */
|
||||
.wechat-search-box {
|
||||
padding: 8px 12px;
|
||||
@@ -770,6 +807,44 @@
|
||||
border-left-color: var(--wechat-bubble-self);
|
||||
}
|
||||
|
||||
/* ===== 多人群聊样式 ===== */
|
||||
.wechat-mp-message {
|
||||
flex-direction: row;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.wechat-mp-message .wechat-message-content {
|
||||
margin-left: 12px;
|
||||
margin-right: 0;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.wechat-mp-message .wechat-message-bubble {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.wechat-mp-message .wechat-message-bubble:first-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wechat-mp-sender {
|
||||
font-size: 12px;
|
||||
color: var(--wechat-text-secondary);
|
||||
margin-bottom: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
/* 暗色模式下多人群聊名字颜色 */
|
||||
.wechat-dark .wechat-mp-sender {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 浅色模式下多人群聊名字颜色 */
|
||||
.wechat-light .wechat-mp-sender,
|
||||
.wechat-mp-sender {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* 语音消息 */
|
||||
.wechat-voice {
|
||||
display: flex;
|
||||
@@ -2304,9 +2379,15 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* 用户消息的图标朝向左(水平翻转) */
|
||||
/* 用户消息的图标朝向左(水平翻转)+ 黑色 */
|
||||
.wechat-voice-bubble.self .wechat-voice-waves-icon {
|
||||
transform: scaleX(-1);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 用户消息的时长文字也是黑色 */
|
||||
.wechat-voice-bubble.self .wechat-voice-duration {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 对方消息的图标朝向右(默认方向) */
|
||||
@@ -5231,7 +5312,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 120px;
|
||||
bottom: 165px;
|
||||
max-height: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -5301,10 +5382,14 @@
|
||||
}
|
||||
|
||||
.wechat-video-call-input-area {
|
||||
position: absolute;
|
||||
bottom: 110px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
margin: 0 16px 10px 16px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.wechat-video-call-input-area.hidden {
|
||||
@@ -13039,3 +13124,438 @@
|
||||
font-size: 12px;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
/* ========== 多人卡导入 - 角色表格样式 ========== */
|
||||
|
||||
/* 表格空状态 */
|
||||
.wechat-char-tables-empty {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
color: var(--wechat-text-secondary);
|
||||
}
|
||||
|
||||
/* 表格卡片 */
|
||||
.wechat-char-table-card {
|
||||
background: var(--wechat-bg-secondary);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wechat-char-table-card.modified {
|
||||
background: linear-gradient(135deg, rgba(87, 107, 149, 0.08), rgba(87, 107, 149, 0.03));
|
||||
}
|
||||
|
||||
/* 表格标题栏 */
|
||||
.wechat-char-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wechat-char-table-header:hover {
|
||||
background: var(--wechat-bg-tertiary);
|
||||
}
|
||||
|
||||
.wechat-char-table-arrow {
|
||||
font-size: 10px;
|
||||
color: var(--wechat-text-secondary);
|
||||
margin-right: 8px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.wechat-char-table-card.expanded .wechat-char-table-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.wechat-char-table-title {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wechat-char-table-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(87, 107, 149, 0.1);
|
||||
color: #576B95;
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.wechat-char-table-modified-tip {
|
||||
font-size: 11px;
|
||||
color: #f5a623;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.wechat-char-table-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--wechat-text-secondary);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
margin-left: 8px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.wechat-char-table-header:hover .wechat-char-table-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wechat-char-table-delete:hover {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* 表格内容区 */
|
||||
.wechat-char-table-body {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.wechat-char-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wechat-char-table th {
|
||||
text-align: left;
|
||||
padding: 8px 6px;
|
||||
color: var(--wechat-text-secondary);
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.wechat-char-table td {
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.wechat-char-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 编辑输入框 */
|
||||
.wechat-char-edit-input {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--wechat-text);
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.wechat-char-edit-input:hover {
|
||||
background: var(--wechat-bg-tertiary);
|
||||
}
|
||||
|
||||
.wechat-char-edit-input:focus {
|
||||
outline: none;
|
||||
border-color: #576B95;
|
||||
background: var(--wechat-bg);
|
||||
}
|
||||
|
||||
.wechat-char-edit-input.char-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wechat-char-edit-input.char-age {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 删除行按钮 */
|
||||
.wechat-char-row-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--wechat-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.wechat-char-table tr:hover .wechat-char-row-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wechat-char-row-delete:hover {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* 添加行按钮 */
|
||||
.wechat-char-add-row {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
border: 1px dashed var(--wechat-border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--wechat-text-secondary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.wechat-char-add-row:hover {
|
||||
border-color: #576B95;
|
||||
color: #576B95;
|
||||
background: rgba(87, 107, 149, 0.05);
|
||||
}
|
||||
|
||||
/* 表格底部操作栏 */
|
||||
.wechat-char-table-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.wechat-char-table-time {
|
||||
font-size: 11px;
|
||||
color: var(--wechat-text-secondary);
|
||||
}
|
||||
|
||||
.wechat-char-table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wechat-char-table-save {
|
||||
background: rgba(87, 107, 149, 0.1) !important;
|
||||
color: #576B95 !important;
|
||||
}
|
||||
|
||||
.wechat-char-table-save:hover {
|
||||
background: rgba(87, 107, 149, 0.2) !important;
|
||||
}
|
||||
|
||||
/* 表格内容区滚动容器 */
|
||||
.wechat-char-table-scroll {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(87, 107, 149, 0.3) transparent;
|
||||
}
|
||||
|
||||
.wechat-char-table-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.wechat-char-table-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wechat-char-table-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(87, 107, 149, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.wechat-char-table-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(87, 107, 149, 0.5);
|
||||
}
|
||||
|
||||
/* 其它信息文本域 */
|
||||
.wechat-char-textarea {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
max-height: 150px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--wechat-border);
|
||||
border-radius: 4px;
|
||||
background: var(--wechat-bg-primary);
|
||||
color: var(--wechat-text-primary);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wechat-char-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #576B95;
|
||||
box-shadow: 0 0 0 2px rgba(87, 107, 149, 0.1);
|
||||
}
|
||||
|
||||
.wechat-char-textarea::placeholder {
|
||||
color: var(--wechat-text-secondary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 世界观区域 */
|
||||
.wechat-worldview-section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.wechat-worldview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wechat-worldview-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--wechat-text-primary);
|
||||
}
|
||||
|
||||
.wechat-worldview-textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
max-height: 200px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--wechat-border);
|
||||
border-radius: 6px;
|
||||
background: var(--wechat-bg-primary);
|
||||
color: var(--wechat-text-primary);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wechat-worldview-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #576B95;
|
||||
box-shadow: 0 0 0 2px rgba(87, 107, 149, 0.1);
|
||||
}
|
||||
|
||||
.wechat-worldview-textarea::placeholder {
|
||||
color: var(--wechat-text-secondary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 角色列表区域 */
|
||||
.wechat-characters-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wechat-characters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wechat-characters-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--wechat-text-primary);
|
||||
}
|
||||
|
||||
/* 解析预览弹窗 */
|
||||
.wechat-preview-section {
|
||||
background: var(--wechat-bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wechat-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wechat-preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--wechat-text-primary);
|
||||
}
|
||||
|
||||
.wechat-preview-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(87, 107, 149, 0.1);
|
||||
color: #576B95;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.wechat-preview-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--wechat-text-primary);
|
||||
white-space: pre-wrap;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background: var(--wechat-bg-primary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.wechat-preview-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wechat-preview-char-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
background: var(--wechat-bg-primary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.wechat-preview-char-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.wechat-preview-char-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wechat-preview-char-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--wechat-text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wechat-preview-char-meta {
|
||||
font-size: 12px;
|
||||
color: var(--wechat-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wechat-preview-char-other {
|
||||
font-size: 12px;
|
||||
color: var(--wechat-text-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
105
ui.js
105
ui.js
@@ -100,11 +100,12 @@ export function getUserPersonaFromST() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成聊天列表 HTML(包含单聊和群聊)
|
||||
// 生成聊天列表 HTML(包含单聊、群聊和多人群聊)
|
||||
export function generateChatList() {
|
||||
const settings = getSettings();
|
||||
const contacts = settings.contacts || [];
|
||||
const groupChats = settings.groupChats || [];
|
||||
const multiPersonChats = settings.multiPersonChats || [];
|
||||
|
||||
// 处理单聊
|
||||
const contactsWithChat = contacts.map((contact, index) => {
|
||||
@@ -136,8 +137,22 @@ export function generateChatList() {
|
||||
};
|
||||
});
|
||||
|
||||
// 处理多人群聊
|
||||
const multiPersonWithChat = multiPersonChats.map((chat, index) => {
|
||||
const chatHistory = chat.chatHistory || [];
|
||||
const lastMsg = getLastRenderableMessage(chatHistory);
|
||||
const lastMsgTime = lastMsg ? (lastMsg.timestamp || chat.lastMessageTime || 0) : (chat.lastMessageTime || 0);
|
||||
return {
|
||||
type: 'multiPerson',
|
||||
...chat,
|
||||
originalIndex: index,
|
||||
lastMsg,
|
||||
lastMsgTime: lastMsgTime || Date.now()
|
||||
};
|
||||
});
|
||||
|
||||
// 合并并排序
|
||||
const allChats = [...contactsWithChat, ...groupsWithChat].sort((a, b) => b.lastMsgTime - a.lastMsgTime);
|
||||
const allChats = [...contactsWithChat, ...groupsWithChat, ...multiPersonWithChat].sort((a, b) => b.lastMsgTime - a.lastMsgTime);
|
||||
|
||||
if (allChats.length === 0) {
|
||||
return `
|
||||
@@ -155,6 +170,8 @@ export function generateChatList() {
|
||||
return allChats.map(chat => {
|
||||
if (chat.type === 'group') {
|
||||
return generateGroupChatItem(chat, settings);
|
||||
} else if (chat.type === 'multiPerson') {
|
||||
return generateMultiPersonChatItem(chat);
|
||||
} else {
|
||||
return generateContactChatItem(chat);
|
||||
}
|
||||
@@ -163,7 +180,9 @@ export function generateChatList() {
|
||||
|
||||
// 生成单聊列表项
|
||||
function generateContactChatItem(contact) {
|
||||
if (!contact) return '';
|
||||
const lastMsg = contact.lastMsg;
|
||||
if (!lastMsg) return '';
|
||||
let preview = '';
|
||||
if (lastMsg.type === 'voice' || lastMsg.isVoice) {
|
||||
preview = '[语音]';
|
||||
@@ -319,13 +338,61 @@ function generateGroupChatItem(group, settings) {
|
||||
`;
|
||||
}
|
||||
|
||||
// 生成多人群聊列表项
|
||||
function generateMultiPersonChatItem(chat) {
|
||||
const lastMsg = chat.lastMsg;
|
||||
let preview = '';
|
||||
|
||||
if (lastMsg) {
|
||||
const sender = lastMsg.characterName ? `[${lastMsg.characterName}]: ` : '';
|
||||
if (lastMsg.isVoice) {
|
||||
preview = `${sender}[语音]`;
|
||||
} else if (lastMsg.isImage) {
|
||||
preview = `${sender}[图片]`;
|
||||
} else if (lastMsg.isSticker) {
|
||||
preview = `${sender}[表情]`;
|
||||
} else {
|
||||
let content = lastMsg.content || '';
|
||||
if (content.length > 15) content = content.substring(0, 15) + '...';
|
||||
preview = `${sender}${content}`;
|
||||
}
|
||||
} else {
|
||||
preview = '群聊已创建';
|
||||
}
|
||||
|
||||
const msgTime = chat.lastMsgTime ? formatChatTime(chat.lastMsgTime) : '';
|
||||
const memberCount = chat.members?.length || 0;
|
||||
|
||||
// 使用保存的头像,如果没有则显示白底黑字"群"
|
||||
let avatarHtml;
|
||||
if (chat.avatar) {
|
||||
avatarHtml = `<img src="${chat.avatar}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 6px;">`;
|
||||
} else {
|
||||
avatarHtml = `<div style="width: 100%; height: 100%; background: #fff; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: bold; color: #000;">群</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="wechat-chat-item wechat-chat-item-mp" data-mp-id="${chat.id}" data-mp-index="${chat.originalIndex}">
|
||||
<div class="wechat-chat-item-avatar" style="display: flex; align-items: center; justify-content: center;">${avatarHtml}</div>
|
||||
<div class="wechat-chat-item-info">
|
||||
<div class="wechat-chat-item-name">${escapeHtml(chat.name || '群聊')}(${memberCount})</div>
|
||||
<div class="wechat-chat-item-preview">${escapeHtml(preview)}</div>
|
||||
</div>
|
||||
<div class="wechat-chat-item-meta">
|
||||
<span class="wechat-chat-item-time">${msgTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 生成联系人列表 HTML
|
||||
export function generateContactsList() {
|
||||
const settings = getSettings();
|
||||
const contacts = settings.contacts || [];
|
||||
const groupChats = settings.groupChats || [];
|
||||
const multiPersonChats = settings.multiPersonChats || [];
|
||||
|
||||
if (contacts.length === 0 && groupChats.length === 0) {
|
||||
if (contacts.length === 0 && groupChats.length === 0 && multiPersonChats.length === 0) {
|
||||
return `
|
||||
<div class="wechat-empty">
|
||||
<div class="wechat-empty-icon">
|
||||
@@ -403,6 +470,38 @@ export function generateContactsList() {
|
||||
`;
|
||||
});
|
||||
|
||||
// 生成多人群聊卡片
|
||||
multiPersonChats.forEach((mpChat, index) => {
|
||||
const memberCount = mpChat.members?.length || 0;
|
||||
const chatName = mpChat.name || '群聊';
|
||||
const hasCustomApi = mpChat.useCustomApi || false;
|
||||
|
||||
// 使用保存的头像,如果没有则显示白底黑字"群"
|
||||
let avatarHtml;
|
||||
if (mpChat.avatar) {
|
||||
avatarHtml = `<img src="${mpChat.avatar}" alt="" style="width: 100%; height: 100%; object-fit: cover; border-radius: 6px;">`;
|
||||
} else {
|
||||
avatarHtml = `<div class="wechat-card-fallback" style="display:flex; background: #fff; color: #000; font-weight: bold;">群</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="wechat-contact-card wechat-mp-card" data-mp-index="${index}">
|
||||
<div class="wechat-card-swipe-wrapper">
|
||||
<div class="wechat-card-content wechat-mp-card-content" data-mp-index="${index}" title="点击开始聊天">
|
||||
<div class="wechat-card-avatar wechat-mp-avatar" data-mp-index="${index}" title="点击配置API">
|
||||
${avatarHtml}
|
||||
${hasCustomApi ? '<div class="wechat-mp-api-badge">⚙️</div>' : ''}
|
||||
</div>
|
||||
<div class="wechat-card-name">${escapeHtml(chatName)}(${memberCount})</div>
|
||||
</div>
|
||||
<div class="wechat-card-delete wechat-mp-delete" data-mp-index="${index}">
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// 生成联系人卡片
|
||||
contacts.forEach((contact, index) => {
|
||||
const firstChar = contact.name ? contact.name.charAt(0) : '?';
|
||||
|
||||
@@ -729,7 +729,9 @@ async function triggerAIGreeting() {
|
||||
|
||||
let reply = part.trim();
|
||||
// 通话中禁用表情包/图片/音乐等富媒体(兜底过滤)
|
||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/<\s*meme\s*>[^<]*<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/<meme>[^<]*<\/meme>/gi, '').trim();
|
||||
reply = reply.replace(/<\/?meme>/gi, '').trim();
|
||||
if (!reply) continue;
|
||||
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
|
||||
// 移除语音标记
|
||||
@@ -854,7 +856,9 @@ ${lastMessages}
|
||||
for (const part of parts) {
|
||||
let reply = part.trim();
|
||||
// 通话中禁用表情包/图片/音乐等富媒体(兜底过滤)
|
||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/<\s*meme\s*>[^<]*<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/<meme>[^<]*<\/meme>/gi, '').trim();
|
||||
reply = reply.replace(/<\/?meme>/gi, '').trim();
|
||||
if (!reply) continue;
|
||||
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
|
||||
// 移除可能的特殊标记
|
||||
|
||||
Reference in New Issue
Block a user