mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 16:05:50 +00:00
Compare commits
3 Commits
f07e0914f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262611c736 | ||
|
|
4a097a613b | ||
|
|
8595a7c48d |
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() {
|
export async function fetchModelList() {
|
||||||
const config = getApiConfig();
|
const config = getApiConfig();
|
||||||
|
|||||||
27
chat.js
27
chat.js
@@ -753,6 +753,11 @@ export function openChat(contactIndex) {
|
|||||||
|
|
||||||
// 加载联系人的聊天背景
|
// 加载联系人的聊天背景
|
||||||
loadContactBackground(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打开聊天
|
// 通过联系人ID打开聊天
|
||||||
@@ -1471,7 +1476,8 @@ function getRealMsgIndexForVoice(container, msgElement) {
|
|||||||
let visualMsgCount = 1;
|
let visualMsgCount = 1;
|
||||||
const content = msg.content || '';
|
const content = msg.content || '';
|
||||||
const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic;
|
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);
|
const parts = content.split('|||').map(p => p.trim()).filter(p => p);
|
||||||
visualMsgCount = parts.length || 1;
|
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) {
|
if (voiceMatch) {
|
||||||
aiMsg = voiceMatch[1];
|
aiMsg = voiceMatch[1];
|
||||||
aiIsVoice = true;
|
aiIsVoice = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析AI照片格式 [照片:描述]
|
// 解析AI照片格式 [照片:描述]
|
||||||
const photoMatch = aiMsg.match(/^\[照片[::]\s*(.+?)\]$/);
|
const photoMatch = aiMsg.match(/^\s*\[照片[::]\s*(.+?)\]\s*$/);
|
||||||
if (photoMatch) {
|
if (photoMatch) {
|
||||||
aiMsg = photoMatch[1];
|
aiMsg = photoMatch[1];
|
||||||
aiIsPhoto = true;
|
aiIsPhoto = true;
|
||||||
@@ -2488,6 +2494,13 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
|
|||||||
// 尝试触发语音/视频通话(随机触发+保底机制)
|
// 尝试触发语音/视频通话(随机触发+保底机制)
|
||||||
tryTriggerCallAfterChat(contactIndex);
|
tryTriggerCallAfterChat(contactIndex);
|
||||||
|
|
||||||
|
// 检查其他联系人是否要主动发消息
|
||||||
|
import('./proactive-message.js').then(m => {
|
||||||
|
m.checkOtherContactsProactive(contact.id);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[可乐] 主动消息检查失败:', err);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideTypingIndicator();
|
hideTypingIndicator();
|
||||||
console.error('[可乐] AI 调用失败:', err);
|
console.error('[可乐] AI 调用失败:', err);
|
||||||
@@ -2590,14 +2603,14 @@ export async function sendStickerMessage(stickerUrl, description = '') {
|
|||||||
if (!aiMsg.trim()) continue;
|
if (!aiMsg.trim()) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = aiMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
aiMsg = voiceMatch[1];
|
aiMsg = voiceMatch[1];
|
||||||
aiIsVoice = true;
|
aiIsVoice = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析AI照片格式 [照片:描述]
|
// 解析AI照片格式 [照片:描述]
|
||||||
const photoMatch = aiMsg.match(/^\[照片[::]\s*(.+?)\]$/);
|
const photoMatch = aiMsg.match(/^\s*\[照片[::]\s*(.+?)\]\s*$/);
|
||||||
if (photoMatch) {
|
if (photoMatch) {
|
||||||
aiMsg = photoMatch[1];
|
aiMsg = photoMatch[1];
|
||||||
aiIsPhoto = true;
|
aiIsPhoto = true;
|
||||||
@@ -3037,14 +3050,14 @@ export async function sendPhotoMessage(description) {
|
|||||||
if (!aiMsg.trim()) continue;
|
if (!aiMsg.trim()) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceMatch = aiMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = aiMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
aiMsg = voiceMatch[1];
|
aiMsg = voiceMatch[1];
|
||||||
aiIsVoice = true;
|
aiIsVoice = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析AI照片格式 [照片:描述]
|
// 解析AI照片格式 [照片:描述]
|
||||||
const photoMatch = aiMsg.match(/^\[照片[::]\s*(.+?)\]$/);
|
const photoMatch = aiMsg.match(/^\s*\[照片[::]\s*(.+?)\]\s*$/);
|
||||||
if (photoMatch) {
|
if (photoMatch) {
|
||||||
aiMsg = photoMatch[1];
|
aiMsg = photoMatch[1];
|
||||||
aiIsPhoto = true;
|
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) {
|
export function changeContactAvatar(contactIndex) {
|
||||||
pendingAvatarContactIndex = 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 => {
|
document.querySelectorAll('.wechat-card-avatar').forEach(avatar => {
|
||||||
let pressTimer = null;
|
let pressTimer = null;
|
||||||
|
|||||||
201
group-chat.js
201
group-chat.js
@@ -635,6 +635,11 @@ export function openGroupChat(groupIndex) {
|
|||||||
|
|
||||||
// 加载群聊背景
|
// 加载群聊背景
|
||||||
loadGroupBackground(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();
|
refreshChatList();
|
||||||
checkGroupSummaryReminder(groupChat);
|
checkGroupSummaryReminder(groupChat);
|
||||||
|
|
||||||
|
// 检测群聊中的负面情绪,可能触发私聊
|
||||||
|
// 传递群聊上下文(最近40条消息)
|
||||||
|
const groupContext = getGroupChatHistoryForApi(groupChat.chatHistory, 40);
|
||||||
|
detectGroupEmotionAndTriggerPrivate(responses, members, groupContext);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideGroupTypingIndicator();
|
hideGroupTypingIndicator();
|
||||||
console.error('[可乐] 群聊 AI 调用失败:', err);
|
console.error('[可乐] 群聊 AI 调用失败:', err);
|
||||||
@@ -2932,3 +2942,194 @@ export async function sendGroupBatchMessages(messages) {
|
|||||||
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
285
main.js
285
main.js
@@ -12,7 +12,7 @@ import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, upda
|
|||||||
import { showToast } from './toast.js';
|
import { showToast } from './toast.js';
|
||||||
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
|
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
|
||||||
|
|
||||||
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
|
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex, closeMpApiSettings, saveMpApiSettings, handleMpAvatarChange } from './contacts.js';
|
||||||
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
|
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
|
||||||
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
|
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
|
||||||
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats, recoverFromTavernWorldbook } from './summary.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 { setupPhoneAutoCentering, setupPhoneDrag, centerPhoneInViewport } from './phone.js';
|
||||||
|
|
||||||
import { showGroupCreateModal, closeGroupCreateModal, createGroupChat, sendGroupMessage, isInGroupChat, setCurrentGroupChatIndex, getCurrentGroupIndex, openGroupChat } from './group-chat.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 { toggleDarkMode, refreshContextTags } from './settings-ui.js';
|
||||||
import { initFuncPanel, toggleFuncPanel, hideFuncPanel, showExpandVoice, closeExpandPanel, sendExpandContent } from './chat-func-panel.js';
|
import { initFuncPanel, toggleFuncPanel, hideFuncPanel, showExpandVoice, closeExpandPanel, sendExpandContent } from './chat-func-panel.js';
|
||||||
import { initEmojiPanel, toggleEmojiPanel, hideEmojiPanel } from './emoji-panel.js';
|
import { initEmojiPanel, toggleEmojiPanel, hideEmojiPanel } from './emoji-panel.js';
|
||||||
@@ -39,6 +40,7 @@ import { initCropper } from './cropper.js';
|
|||||||
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
|
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
|
||||||
import { testSttApi, testTtsApi } from './voice-api.js';
|
import { testSttApi, testTtsApi } from './voice-api.js';
|
||||||
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording, getAllVoiceRecordingsGroupedByContact, deleteVoiceRecordingsByContact } 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';
|
let currentHistoryTab = 'listen';
|
||||||
@@ -71,7 +73,7 @@ function closeHistoryPage() {
|
|||||||
currentHistoryContactIndex = -1;
|
currentHistoryContactIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteHistoryRecord(tabType, index) {
|
function deleteHistoryRecord(tabType, index, isRealVoice = false) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const contact = settings.contacts?.[currentHistoryContactIndex];
|
const contact = settings.contacts?.[currentHistoryContactIndex];
|
||||||
if (!contact) return;
|
if (!contact) return;
|
||||||
@@ -80,8 +82,32 @@ function deleteHistoryRecord(tabType, index) {
|
|||||||
if (contact.listenHistory && contact.listenHistory[index]) {
|
if (contact.listenHistory && contact.listenHistory[index]) {
|
||||||
contact.listenHistory.splice(index, 1);
|
contact.listenHistory.splice(index, 1);
|
||||||
}
|
}
|
||||||
} else if (tabType === 'voice' || tabType === 'video') {
|
} else if (tabType === 'voice') {
|
||||||
// 从 callHistory 中找到并删除对应类型的记录
|
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 callHistory = contact.callHistory || [];
|
||||||
const typeRecords = callHistory.filter(r => r.type === tabType);
|
const typeRecords = callHistory.filter(r => r.type === tabType);
|
||||||
if (typeRecords[index]) {
|
if (typeRecords[index]) {
|
||||||
@@ -151,8 +177,17 @@ function renderHistoryContent(contact, tabType) {
|
|||||||
let records = [];
|
let records = [];
|
||||||
if (tabType === 'listen') {
|
if (tabType === 'listen') {
|
||||||
records = contact.listenHistory || [];
|
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 {
|
} else {
|
||||||
// 从 callHistory 中筛选 voice 或 video
|
// 从 callHistory 中筛选 video
|
||||||
const callHistory = contact.callHistory || [];
|
const callHistory = contact.callHistory || [];
|
||||||
records = callHistory.filter(r => r.type === tabType);
|
records = callHistory.filter(r => r.type === tabType);
|
||||||
}
|
}
|
||||||
@@ -179,12 +214,13 @@ function renderHistoryContent(contact, tabType) {
|
|||||||
const duration = record.duration || '';
|
const duration = record.duration || '';
|
||||||
const messages = record.messages || [];
|
const messages = record.messages || [];
|
||||||
const originalIndex = records.indexOf(record);
|
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 += `<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 += `<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) {
|
if (duration) {
|
||||||
html += `<span class="wechat-history-card-duration">${escapeHtml(duration)}</span>`;
|
html += `<span class="wechat-history-card-duration">${escapeHtml(duration)}</span>`;
|
||||||
}
|
}
|
||||||
@@ -224,7 +260,8 @@ function renderHistoryContent(contact, tabType) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const tab = btn.dataset.tab;
|
const tab = btn.dataset.tab;
|
||||||
const index = parseInt(btn.dataset.index);
|
const index = parseInt(btn.dataset.index);
|
||||||
deleteHistoryRecord(tab, index);
|
const isRealVoice = btn.dataset.realVoice === 'true';
|
||||||
|
deleteHistoryRecord(tab, index, isRealVoice);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -986,11 +1023,14 @@ function bindEvents() {
|
|||||||
document.getElementById('wechat-chat-back-btn')?.addEventListener('click', () => {
|
document.getElementById('wechat-chat-back-btn')?.addEventListener('click', () => {
|
||||||
setCurrentChatIndex(-1);
|
setCurrentChatIndex(-1);
|
||||||
setCurrentGroupChatIndex(-1);
|
setCurrentGroupChatIndex(-1);
|
||||||
// 清除群聊标记
|
setCurrentMultiPersonChatIndex(-1);
|
||||||
|
// 清除群聊和多人群聊标记
|
||||||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
messagesContainer.dataset.isGroup = 'false';
|
messagesContainer.dataset.isGroup = 'false';
|
||||||
messagesContainer.dataset.groupIndex = '-1';
|
messagesContainer.dataset.groupIndex = '-1';
|
||||||
|
messagesContainer.dataset.isMultiPerson = 'false';
|
||||||
|
messagesContainer.dataset.multiPersonIndex = '-1';
|
||||||
// 清除背景
|
// 清除背景
|
||||||
messagesContainer.style.backgroundImage = '';
|
messagesContainer.style.backgroundImage = '';
|
||||||
}
|
}
|
||||||
@@ -1026,6 +1066,12 @@ function bindEvents() {
|
|||||||
document.getElementById('wechat-recalled-panel')?.classList.add('hidden');
|
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的朋友圈
|
// 查看TA的朋友圈
|
||||||
document.getElementById('wechat-menu-moments')?.addEventListener('click', () => {
|
document.getElementById('wechat-menu-moments')?.addEventListener('click', () => {
|
||||||
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
|
||||||
@@ -1218,6 +1264,11 @@ function bindEvents() {
|
|||||||
this.value = '';
|
this.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 导入多人卡
|
||||||
|
document.getElementById('wechat-import-multi-card')?.addEventListener('click', () => {
|
||||||
|
openMultiImportModal();
|
||||||
|
});
|
||||||
|
|
||||||
// 深色模式切换
|
// 深色模式切换
|
||||||
document.getElementById('wechat-dark-toggle')?.addEventListener('click', toggleDarkMode);
|
document.getElementById('wechat-dark-toggle')?.addEventListener('click', toggleDarkMode);
|
||||||
|
|
||||||
@@ -1522,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', () => {
|
document.getElementById('wechat-group-inject-toggle')?.addEventListener('click', () => {
|
||||||
@@ -1584,10 +1815,15 @@ function bindEvents() {
|
|||||||
text: text.substring(0, 20),
|
text: text.substring(0, 20),
|
||||||
isGroup: messagesContainer?.dataset?.isGroup,
|
isGroup: messagesContainer?.dataset?.isGroup,
|
||||||
groupIndex: messagesContainer?.dataset?.groupIndex,
|
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');
|
console.log('[可乐] 调用 sendGroupMessage');
|
||||||
sendGroupMessage(text);
|
sendGroupMessage(text);
|
||||||
} else {
|
} else {
|
||||||
@@ -1605,7 +1841,9 @@ function bindEvents() {
|
|||||||
const text = chatInput?.value?.trim();
|
const text = chatInput?.value?.trim();
|
||||||
if (text) {
|
if (text) {
|
||||||
// 有文字时发送消息
|
// 有文字时发送消息
|
||||||
if (isInGroupChat()) {
|
if (isInMultiPersonChat()) {
|
||||||
|
sendMultiPersonMessage(text);
|
||||||
|
} else if (isInGroupChat()) {
|
||||||
sendGroupMessage(text);
|
sendGroupMessage(text);
|
||||||
} else {
|
} else {
|
||||||
sendMessage(text);
|
sendMessage(text);
|
||||||
@@ -1639,6 +1877,7 @@ function bindEvents() {
|
|||||||
initGiftEvents();
|
initGiftEvents();
|
||||||
initCropper();
|
initCropper();
|
||||||
initHistoryEvents();
|
initHistoryEvents();
|
||||||
|
initMultiCharImport();
|
||||||
|
|
||||||
// 展开面板
|
// 展开面板
|
||||||
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
|
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
|
||||||
@@ -1682,7 +1921,7 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 聊天列表项点击(支持单聊和群聊)
|
// 聊天列表项点击(支持单聊、群聊和多人群聊)
|
||||||
document.getElementById('wechat-chat-list')?.addEventListener('click', (e) => {
|
document.getElementById('wechat-chat-list')?.addEventListener('click', (e) => {
|
||||||
const chatItem = e.target.closest('.wechat-chat-item');
|
const chatItem = e.target.closest('.wechat-chat-item');
|
||||||
if (!chatItem) return;
|
if (!chatItem) return;
|
||||||
@@ -1693,6 +1932,12 @@ function bindEvents() {
|
|||||||
if (!isNaN(groupIndex)) {
|
if (!isNaN(groupIndex)) {
|
||||||
import('./group-chat.js').then(m => m.openGroupChat(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 {
|
} else {
|
||||||
// 单聊
|
// 单聊
|
||||||
const contactId = chatItem.dataset.contactId;
|
const contactId = chatItem.dataset.contactId;
|
||||||
@@ -2029,6 +2274,13 @@ function bindEvents() {
|
|||||||
return;
|
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 || '该';
|
const label = item.querySelector('span')?.textContent || '该';
|
||||||
showToast(`"${label}" 功能开发中...`, 'info');
|
showToast(`"${label}" 功能开发中...`, 'info');
|
||||||
});
|
});
|
||||||
@@ -2631,13 +2883,18 @@ function bindEvents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
console.log('[可乐] init() 开始');
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
console.log('[可乐] loadSettings 调用完成,开始 getSettings');
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
console.log('[可乐] getSettings 完成,开始 seedDefaultUserPersonaFromST');
|
||||||
if (seedDefaultUserPersonaFromST(settings)) {
|
if (seedDefaultUserPersonaFromST(settings)) {
|
||||||
requestSave();
|
requestSave();
|
||||||
}
|
}
|
||||||
|
console.log('[可乐] seedDefaultUserPersonaFromST 完成,开始 generatePhoneHTML');
|
||||||
|
|
||||||
const phoneHTML = generatePhoneHTML();
|
const phoneHTML = generatePhoneHTML();
|
||||||
|
console.log('[可乐] generatePhoneHTML 完成');
|
||||||
document.body.insertAdjacentHTML('beforeend', phoneHTML);
|
document.body.insertAdjacentHTML('beforeend', phoneHTML);
|
||||||
|
|
||||||
setupPhoneAutoCentering();
|
setupPhoneAutoCentering();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { requestSave } from './save-manager.js';
|
|||||||
import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js';
|
import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js';
|
||||||
import { showToast } from './toast.js';
|
import { showToast } from './toast.js';
|
||||||
import { getContext } from '../../../extensions.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';
|
import { isInGroupChat, getCurrentGroupIndex, openGroupChat } from './group-chat.js';
|
||||||
|
|
||||||
// 当前显示菜单的消息索引
|
// 当前显示菜单的消息索引
|
||||||
@@ -118,12 +118,24 @@ export function showMessageMenu(msgElement, msgIndex, event) {
|
|||||||
msg = contact?.chatHistory?.[msgIndex];
|
msg = contact?.chatHistory?.[msgIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先从历史记录判断,其次从元素属性判断(处理分割显示的消息)
|
// 从元素或其父元素获取 role 属性
|
||||||
let isUserMessage = msg?.role === 'user';
|
let roleAttr = msgElement?.dataset?.msgRole;
|
||||||
if (msg === undefined) {
|
if (!roleAttr) {
|
||||||
// 如果找不到消息记录,尝试从元素属性获取
|
// 尝试从父元素获取(气泡元素在 .wechat-message 内部)
|
||||||
const roleAttr = msgElement?.dataset?.msgRole || msgElement?.closest?.('[data-msg-role]')?.dataset?.msgRole;
|
const parentMsg = msgElement?.closest?.('.wechat-message') || msgElement?.parentElement?.closest?.('.wechat-message');
|
||||||
isUserMessage = roleAttr === 'user';
|
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重新回复
|
// 触发AI重新回复
|
||||||
try {
|
try {
|
||||||
|
// 等待 DOM 更新后再显示 typing 指示器
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
showTypingIndicator(contact);
|
showTypingIndicator(contact);
|
||||||
|
|
||||||
const { callAI } = await import('./ai.js');
|
const { callAI } = await import('./ai.js');
|
||||||
@@ -490,12 +504,17 @@ async function regenerateMessage(msgIndex, contact) {
|
|||||||
if (!finalMsg) continue;
|
if (!finalMsg) continue;
|
||||||
|
|
||||||
let isVoice = false;
|
let isVoice = false;
|
||||||
const voiceMatch = finalMsg.match(/^\[语音[::]\s*(.+?)\]$/);
|
const voiceMatch = finalMsg.match(/^\s*\[语音[::]\s*(.+?)\]\s*$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
finalMsg = voiceMatch[1];
|
finalMsg = voiceMatch[1];
|
||||||
isVoice = true;
|
isVoice = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 每条消息都要有typing效果和2-2.5秒延迟(与普通回复一致)
|
||||||
|
showTypingIndicator(contact);
|
||||||
|
await sleep(2000 + Math.random() * 500);
|
||||||
|
hideTypingIndicator();
|
||||||
|
|
||||||
contact.chatHistory.push({
|
contact.chatHistory.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: finalMsg,
|
content: finalMsg,
|
||||||
@@ -721,7 +740,8 @@ function getRealMsgIndex(container, msgElement) {
|
|||||||
const content = msg.content || '';
|
const content = msg.content || '';
|
||||||
const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic;
|
const isSpecial = msg.isVoice || msg.isSticker || msg.isPhoto || msg.isMusic;
|
||||||
// 检查是否包含 ||| 或 <meme> 标签(这些会导致消息被分割显示)
|
// 检查是否包含 ||| 或 <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 计算实际分割数量
|
// 使用 splitAIMessages 计算实际分割数量
|
||||||
const parts = splitAIMessages(content).filter(p => p && p.trim());
|
const parts = splitAIMessages(content).filter(p => p && p.trim());
|
||||||
visualMsgCount = parts.length || 1;
|
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;
|
||||||
|
}
|
||||||
245
phone-html.js
245
phone-html.js
@@ -154,6 +154,13 @@ export function generatePhoneHTML() {
|
|||||||
<div class="wechat-add-option-text">导入角色卡 (JSON)</div>
|
<div class="wechat-add-option-text">导入角色卡 (JSON)</div>
|
||||||
<span class="wechat-add-option-arrow">›</span>
|
<span class="wechat-add-option-arrow">›</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,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>
|
<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>
|
<span>撤回消息</span>
|
||||||
</div>
|
</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">
|
<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>
|
<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>
|
<span>聊天背景</span>
|
||||||
@@ -774,6 +789,14 @@ function generateServicePageHTML(settings) {
|
|||||||
<div class="wechat-service-grid">
|
<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="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="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>
|
</div>
|
||||||
<div class="wechat-service-section">
|
<div class="wechat-service-section">
|
||||||
@@ -1014,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 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;">
|
<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>
|
||||||
|
|
||||||
<div class="wechat-modal-actions" style="margin-top: 16px;">
|
<div class="wechat-modal-actions" style="margin-top: 16px;">
|
||||||
@@ -1039,6 +1062,71 @@ function generateModalsHTML(settings) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1512,6 +1600,161 @@ function generateGiftPageHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</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 id="wechat-toy-control-page" class="wechat-toy-control-page hidden">
|
||||||
<div class="wechat-navbar wechat-toy-control-navbar">
|
<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(/\[.*?\]/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();
|
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) {
|
if (callState.messages && callState.messages.length > 0) {
|
||||||
|
// 有对话内容时,直接将对话内容保存到 chatHistory(不显示通话记录标记)
|
||||||
contact.realVoiceCallHistory = Array.isArray(contact.realVoiceCallHistory) ? contact.realVoiceCallHistory : [];
|
contact.realVoiceCallHistory = Array.isArray(contact.realVoiceCallHistory) ? contact.realVoiceCallHistory : [];
|
||||||
contact.realVoiceCallHistory.push({
|
contact.realVoiceCallHistory.push({
|
||||||
type: 'real-voice',
|
type: 'real-voice',
|
||||||
@@ -851,15 +846,42 @@ export async function hangupCall() {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
messages: callState.messages.map(m => ({ role: m.role, content: m.content }))
|
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;
|
contact.lastMessage = lastMessage;
|
||||||
|
|
||||||
// 在聊天界面显示通话记录
|
|
||||||
if (currentChatIndex === callState.contactIndex) {
|
|
||||||
appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', durationStr, contact);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestSave();
|
requestSave();
|
||||||
refreshChatList();
|
refreshChatList();
|
||||||
}
|
}
|
||||||
|
|||||||
526
style.css
526
style.css
@@ -276,6 +276,43 @@
|
|||||||
background: #d9363e;
|
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 {
|
.wechat-search-box {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -770,6 +807,44 @@
|
|||||||
border-left-color: var(--wechat-bubble-self);
|
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 {
|
.wechat-voice {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2304,9 +2379,15 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 用户消息的图标朝向左(水平翻转) */
|
/* 用户消息的图标朝向左(水平翻转)+ 黑色 */
|
||||||
.wechat-voice-bubble.self .wechat-voice-waves-icon {
|
.wechat-voice-bubble.self .wechat-voice-waves-icon {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户消息的时长文字也是黑色 */
|
||||||
|
.wechat-voice-bubble.self .wechat-voice-duration {
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对方消息的图标朝向右(默认方向) */
|
/* 对方消息的图标朝向右(默认方向) */
|
||||||
@@ -5231,7 +5312,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 120px;
|
bottom: 165px;
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -5301,10 +5382,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wechat-video-call-input-area {
|
.wechat-video-call-input-area {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 110px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
margin: 0 16px 10px 16px;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wechat-video-call-input-area.hidden {
|
.wechat-video-call-input-area.hidden {
|
||||||
@@ -13039,3 +13124,438 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #07c160;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成聊天列表 HTML(包含单聊和群聊)
|
// 生成聊天列表 HTML(包含单聊、群聊和多人群聊)
|
||||||
export function generateChatList() {
|
export function generateChatList() {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const contacts = settings.contacts || [];
|
const contacts = settings.contacts || [];
|
||||||
const groupChats = settings.groupChats || [];
|
const groupChats = settings.groupChats || [];
|
||||||
|
const multiPersonChats = settings.multiPersonChats || [];
|
||||||
|
|
||||||
// 处理单聊
|
// 处理单聊
|
||||||
const contactsWithChat = contacts.map((contact, index) => {
|
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) {
|
if (allChats.length === 0) {
|
||||||
return `
|
return `
|
||||||
@@ -155,6 +170,8 @@ export function generateChatList() {
|
|||||||
return allChats.map(chat => {
|
return allChats.map(chat => {
|
||||||
if (chat.type === 'group') {
|
if (chat.type === 'group') {
|
||||||
return generateGroupChatItem(chat, settings);
|
return generateGroupChatItem(chat, settings);
|
||||||
|
} else if (chat.type === 'multiPerson') {
|
||||||
|
return generateMultiPersonChatItem(chat);
|
||||||
} else {
|
} else {
|
||||||
return generateContactChatItem(chat);
|
return generateContactChatItem(chat);
|
||||||
}
|
}
|
||||||
@@ -163,7 +180,9 @@ export function generateChatList() {
|
|||||||
|
|
||||||
// 生成单聊列表项
|
// 生成单聊列表项
|
||||||
function generateContactChatItem(contact) {
|
function generateContactChatItem(contact) {
|
||||||
|
if (!contact) return '';
|
||||||
const lastMsg = contact.lastMsg;
|
const lastMsg = contact.lastMsg;
|
||||||
|
if (!lastMsg) return '';
|
||||||
let preview = '';
|
let preview = '';
|
||||||
if (lastMsg.type === 'voice' || lastMsg.isVoice) {
|
if (lastMsg.type === 'voice' || lastMsg.isVoice) {
|
||||||
preview = '[语音]';
|
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
|
// 生成联系人列表 HTML
|
||||||
export function generateContactsList() {
|
export function generateContactsList() {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const contacts = settings.contacts || [];
|
const contacts = settings.contacts || [];
|
||||||
const groupChats = settings.groupChats || [];
|
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 `
|
return `
|
||||||
<div class="wechat-empty">
|
<div class="wechat-empty">
|
||||||
<div class="wechat-empty-icon">
|
<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) => {
|
contacts.forEach((contact, index) => {
|
||||||
const firstChar = contact.name ? contact.name.charAt(0) : '?';
|
const firstChar = contact.name ? contact.name.charAt(0) : '?';
|
||||||
|
|||||||
@@ -729,7 +729,9 @@ async function triggerAIGreeting() {
|
|||||||
|
|
||||||
let reply = part.trim();
|
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 (!reply) continue;
|
||||||
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
|
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
|
||||||
// 移除语音标记
|
// 移除语音标记
|
||||||
@@ -854,7 +856,9 @@ ${lastMessages}
|
|||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
let reply = part.trim();
|
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 (!reply) continue;
|
||||||
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
|
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
|
||||||
// 移除可能的特殊标记
|
// 移除可能的特殊标记
|
||||||
|
|||||||
Reference in New Issue
Block a user