diff --git a/ai.js b/ai.js
index 4bfe239..0b04feb 100644
--- a/ai.js
+++ b/ai.js
@@ -258,6 +258,17 @@ export async function testApiConnection() {
}
}
+// 测试指定 API 连接(接受参数)
+export async function testConnection(apiUrl, apiKey, model) {
+ if (!apiUrl) {
+ throw new Error('请先配置 API 地址');
+ }
+
+ // 尝试获取模型列表来验证连接
+ await fetchModelListFromApi(apiUrl, apiKey);
+ return true;
+}
+
// 获取模型列表
export async function fetchModelList() {
const config = getApiConfig();
diff --git a/chat.js b/chat.js
index 93babe7..d777cd7 100644
--- a/chat.js
+++ b/chat.js
@@ -753,6 +753,11 @@ export function openChat(contactIndex) {
// 加载联系人的聊天背景
loadContactBackground(contactIndex);
+
+ // 隐藏群聊专属菜单项,显示单聊专属菜单项
+ document.getElementById('wechat-menu-invite-member')?.classList.add('hidden');
+ document.getElementById('wechat-menu-block')?.classList.remove('hidden');
+ document.getElementById('wechat-menu-moments')?.classList.remove('hidden');
}
// 通过联系人ID打开聊天
@@ -2488,6 +2493,13 @@ export async function sendMessage(messageText, isMultipleMessages = false, isVoi
// 尝试触发语音/视频通话(随机触发+保底机制)
tryTriggerCallAfterChat(contactIndex);
+ // 检查其他联系人是否要主动发消息
+ import('./proactive-message.js').then(m => {
+ m.checkOtherContactsProactive(contact.id);
+ }).catch(err => {
+ console.error('[可乐] 主动消息检查失败:', err);
+ });
+
} catch (err) {
hideTypingIndicator();
console.error('[可乐] AI 调用失败:', err);
diff --git a/contacts.js b/contacts.js
index 0aabc5c..4a27b09 100644
--- a/contacts.js
+++ b/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 = `
`;
+ } 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 = '';
+ if (mpChat.customModel) {
+ modelSelect.innerHTML += ``;
+ }
+ }
+
+ // 显示弹窗
+ 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 = `
`;
+ }
+ };
+ reader.readAsDataURL(file);
+}
+
+// 关闭多人群聊API配置弹窗
+export function closeMpApiSettings() {
+ document.getElementById('wechat-mp-api-modal')?.classList.add('hidden');
+ currentEditingMpIndex = -1;
+}
+
// 更换角色头像(在设置弹窗中使用)
export function changeContactAvatar(contactIndex) {
pendingAvatarContactIndex = contactIndex;
@@ -351,6 +493,37 @@ export function bindContactsEvents() {
});
});
+ // 多人群聊卡片点击进入聊天
+ import('./multi-person-chat.js').then(mpModule => {
+ document.querySelectorAll('.wechat-mp-card .wechat-mp-card-content').forEach(card => {
+ card.addEventListener('click', function(e) {
+ // 如果点击的是头像,不进入聊天(由头像自己的事件处理)
+ if (e.target.closest('.wechat-mp-avatar')) return;
+ const cardEl = this.closest('.wechat-mp-card');
+ const mpIndex = parseInt(cardEl.dataset.mpIndex);
+ mpModule.openMultiPersonChat(mpIndex);
+ });
+ });
+ });
+
+ // 多人群聊头像点击配置API
+ document.querySelectorAll('.wechat-mp-avatar').forEach(avatar => {
+ avatar.addEventListener('click', function(e) {
+ e.stopPropagation();
+ const mpIndex = parseInt(this.dataset.mpIndex);
+ openMpApiSettings(mpIndex);
+ });
+ });
+
+ // 多人群聊删除按钮
+ document.querySelectorAll('.wechat-mp-delete').forEach(btn => {
+ btn.addEventListener('click', function(e) {
+ e.stopPropagation();
+ const mpIndex = parseInt(this.dataset.mpIndex);
+ deleteMultiPersonChat(mpIndex);
+ });
+ });
+
// 头像事件绑定(长按删除 + 单击打开设置)
document.querySelectorAll('.wechat-card-avatar').forEach(avatar => {
let pressTimer = null;
diff --git a/group-chat.js b/group-chat.js
index dfe553f..efbf752 100644
--- a/group-chat.js
+++ b/group-chat.js
@@ -635,6 +635,11 @@ export function openGroupChat(groupIndex) {
// 加载群聊背景
loadGroupBackground(groupIndex);
+
+ // 显示群聊专属菜单项,隐藏单聊专属菜单项
+ document.getElementById('wechat-menu-invite-member')?.classList.remove('hidden');
+ document.getElementById('wechat-menu-block')?.classList.add('hidden');
+ document.getElementById('wechat-menu-moments')?.classList.add('hidden');
}
// 渲染群聊历史
@@ -2404,6 +2409,11 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
refreshChatList();
checkGroupSummaryReminder(groupChat);
+ // 检测群聊中的负面情绪,可能触发私聊
+ // 传递群聊上下文(最近40条消息)
+ const groupContext = getGroupChatHistoryForApi(groupChat.chatHistory, 40);
+ detectGroupEmotionAndTriggerPrivate(responses, members, groupContext);
+
} catch (err) {
hideGroupTypingIndicator();
console.error('[可乐] 群聊 AI 调用失败:', err);
@@ -2932,3 +2942,194 @@ export async function sendGroupBatchMessages(messages) {
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null);
}
}
+
+/**
+ * 显示邀请成员弹窗
+ */
+export function showInviteMemberModal() {
+ const settings = getSettings();
+ const groupChat = settings.groupChats?.[currentGroupChatIndex];
+ if (!groupChat) {
+ showToast('请先打开一个群聊', '⚠️');
+ return;
+ }
+
+ const currentMemberIds = groupChat.memberIds || [];
+
+ // 检查是否已满
+ if (currentMemberIds.length >= GROUP_CHAT_MAX_AI_MEMBERS) {
+ showToast(`群聊已满(最多${GROUP_CHAT_MAX_AI_MEMBERS}人)`, '⚠️');
+ return;
+ }
+
+ // 获取可邀请的联系人(不在群里的、配置了独立API的)
+ const availableContacts = settings.contacts.filter(c =>
+ !currentMemberIds.includes(c.id) &&
+ c.useCustomApi &&
+ c.customApiUrl &&
+ c.customModel
+ );
+
+ if (availableContacts.length === 0) {
+ showToast('没有可邀请的联系人\n(需配置独立API)', '⚠️');
+ return;
+ }
+
+ // 获取手机容器
+ const phoneContainer = document.querySelector('.wechat-phone');
+ if (!phoneContainer) return;
+
+ // 构建弹窗HTML
+ const modal = document.createElement('div');
+ modal.className = 'wechat-modal';
+ modal.id = 'wechat-invite-member-modal';
+ modal.innerHTML = `
+
+
邀请成员
+
+
+ 当前 ${currentMemberIds.length}/${GROUP_CHAT_MAX_AI_MEMBERS} 人
+
+
+
+
+
+
+
+ `;
+
+ 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);
+ });
+ }
+ }
+}
diff --git a/main.js b/main.js
index d735917..36d0f4d 100644
--- a/main.js
+++ b/main.js
@@ -12,7 +12,7 @@ import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, upda
import { showToast } from './toast.js';
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
-import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
+import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex, closeMpApiSettings, saveMpApiSettings, handleMpAvatarChange } from './contacts.js';
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat, updateBlockMenuText, startBlockedAIMessages, stopBlockedAIMessages, showBlockedMessages } from './chat.js';
import { refreshFavoritesList, showLorebookModal, syncCharacterBookToTavern, showAddLorebookPanel, showAddPersonaPanel } from './favorites.js';
import { executeSummary, rollbackSummary, refreshSummaryChatList, selectAllSummaryChats, recoverFromTavernWorldbook } from './summary.js';
@@ -23,6 +23,7 @@ import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST
import { setupPhoneAutoCentering, setupPhoneDrag, centerPhoneInViewport } from './phone.js';
import { showGroupCreateModal, closeGroupCreateModal, createGroupChat, sendGroupMessage, isInGroupChat, setCurrentGroupChatIndex, getCurrentGroupIndex, openGroupChat } from './group-chat.js';
+import { isInMultiPersonChat, sendMultiPersonMessage, setCurrentMultiPersonChatIndex } from './multi-person-chat.js';
import { toggleDarkMode, refreshContextTags } from './settings-ui.js';
import { initFuncPanel, toggleFuncPanel, hideFuncPanel, showExpandVoice, closeExpandPanel, sendExpandContent } from './chat-func-panel.js';
import { initEmojiPanel, toggleEmojiPanel, hideEmojiPanel } from './emoji-panel.js';
@@ -39,6 +40,7 @@ import { initCropper } from './cropper.js';
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
import { testSttApi, testTtsApi } from './voice-api.js';
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording, getAllVoiceRecordingsGroupedByContact, deleteVoiceRecordingsByContact } from './audio-storage.js';
+import { initMultiCharImport, openMultiImportModal, getMultiCharImportModalHtml, getCharSelectModalHtml, getCharOtherEditModalHtml } from './multi-char-import.js';
// ========== 历史记录功能 ==========
let currentHistoryTab = 'listen';
@@ -71,7 +73,7 @@ function closeHistoryPage() {
currentHistoryContactIndex = -1;
}
-function deleteHistoryRecord(tabType, index) {
+function deleteHistoryRecord(tabType, index, isRealVoice = false) {
const settings = getSettings();
const contact = settings.contacts?.[currentHistoryContactIndex];
if (!contact) return;
@@ -80,8 +82,32 @@ function deleteHistoryRecord(tabType, index) {
if (contact.listenHistory && contact.listenHistory[index]) {
contact.listenHistory.splice(index, 1);
}
- } else if (tabType === 'voice' || tabType === 'video') {
- // 从 callHistory 中找到并删除对应类型的记录
+ } else if (tabType === 'voice') {
+ if (isRealVoice) {
+ // 删除实时语音记录
+ if (contact.realVoiceCallHistory && contact.realVoiceCallHistory.length > 0) {
+ // 找到实时语音记录在合并数组中的索引对应的原始索引
+ const realVoiceRecords = contact.realVoiceCallHistory;
+ const callHistory = contact.callHistory || [];
+ const voiceRecords = callHistory.filter(r => r.type === 'voice');
+ // index 是在合并数组中的位置,需要计算在 realVoiceCallHistory 中的实际位置
+ const realVoiceIndex = index - voiceRecords.length;
+ if (realVoiceIndex >= 0 && realVoiceIndex < realVoiceRecords.length) {
+ contact.realVoiceCallHistory.splice(realVoiceIndex, 1);
+ }
+ }
+ } else {
+ // 删除普通语音通话记录
+ const callHistory = contact.callHistory || [];
+ const typeRecords = callHistory.filter(r => r.type === 'voice');
+ if (typeRecords[index]) {
+ const originalIndex = callHistory.indexOf(typeRecords[index]);
+ if (originalIndex >= 0) {
+ contact.callHistory.splice(originalIndex, 1);
+ }
+ }
+ }
+ } else if (tabType === 'video') {
const callHistory = contact.callHistory || [];
const typeRecords = callHistory.filter(r => r.type === tabType);
if (typeRecords[index]) {
@@ -151,8 +177,17 @@ function renderHistoryContent(contact, tabType) {
let records = [];
if (tabType === 'listen') {
records = contact.listenHistory || [];
+ } else if (tabType === 'voice') {
+ // 语音通话:合并普通语音通话和实时语音通话
+ const callHistory = contact.callHistory || [];
+ const voiceRecords = callHistory.filter(r => r.type === 'voice');
+ const realVoiceRecords = (contact.realVoiceCallHistory || []).map(r => ({
+ ...r,
+ isRealVoice: true // 标记为实时语音
+ }));
+ records = [...voiceRecords, ...realVoiceRecords];
} else {
- // 从 callHistory 中筛选 voice 或 video
+ // 从 callHistory 中筛选 video
const callHistory = contact.callHistory || [];
records = callHistory.filter(r => r.type === tabType);
}
@@ -179,12 +214,13 @@ function renderHistoryContent(contact, tabType) {
const duration = record.duration || '';
const messages = record.messages || [];
const originalIndex = records.indexOf(record);
+ const isRealVoice = record.isRealVoice ? 'true' : 'false';
- html += `
diff --git a/proactive-message.js b/proactive-message.js
new file mode 100644
index 0000000..6fddda2
--- /dev/null
+++ b/proactive-message.js
@@ -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 };
diff --git a/real-voice-call.js b/real-voice-call.js
index e204f1d..e9aa543 100644
--- a/real-voice-call.js
+++ b/real-voice-call.js
@@ -379,7 +379,13 @@ function cleanAIReply(text) {
// 移除特殊标记
reply = reply.replace(/\[.*?\]/g, '').trim();
- reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
+
+ // 移除 meme 表情包标签(多种可能的格式)
+ reply = reply.replace(/<\s*meme\s*>[^<]*<\s*\/\s*meme\s*>/gi, '').trim();
+ reply = reply.replace(/
[^<]*<\/meme>/gi, '').trim();
+
+ // 移除可能残留的单独标签
+ reply = reply.replace(/<\/?meme>/gi, '').trim();
// 移除括号描述(中文和英文括号)
reply = reply.replace(/([^)]+)/g, '').trim();
@@ -828,20 +834,9 @@ export async function hangupCall() {
}
}
- // 通话记录消息
- const callRecord = {
- role: callState.initiator === 'user' ? 'user' : 'assistant',
- content: callContent,
- time: timeStr,
- timestamp: Date.now(),
- isCallRecord: true,
- isRealVoice: true
- };
-
- contact.chatHistory.push(callRecord);
-
- // 保存通话历史
+ // 保存通话历史和对话内容
if (callState.messages && callState.messages.length > 0) {
+ // 有对话内容时,直接将对话内容保存到 chatHistory(不显示通话记录标记)
contact.realVoiceCallHistory = Array.isArray(contact.realVoiceCallHistory) ? contact.realVoiceCallHistory : [];
contact.realVoiceCallHistory.push({
type: 'real-voice',
@@ -851,15 +846,42 @@ export async function hangupCall() {
timestamp: Date.now(),
messages: callState.messages.map(m => ({ role: m.role, content: m.content }))
});
+
+ // 将通话对话内容保存到 chatHistory,让 AI 后续对话能识别
+ callState.messages.forEach(msg => {
+ contact.chatHistory.push({
+ role: msg.role,
+ content: msg.content,
+ time: timeStr,
+ timestamp: Date.now(),
+ isRealVoiceContent: true // 标记为实时通话内容
+ });
+ });
+
+ // 刷新聊天界面显示对话内容
+ if (currentChatIndex === callState.contactIndex) {
+ openChat(currentChatIndex);
+ }
+ } else {
+ // 没有对话内容时(取消/拒绝),显示通话记录标记
+ const callRecord = {
+ role: callState.initiator === 'user' ? 'user' : 'assistant',
+ content: callContent,
+ time: timeStr,
+ timestamp: Date.now(),
+ isCallRecord: true,
+ isRealVoice: true
+ };
+ contact.chatHistory.push(callRecord);
+
+ // 在聊天界面显示通话记录
+ if (currentChatIndex === callState.contactIndex) {
+ appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', durationStr, contact);
+ }
}
contact.lastMessage = lastMessage;
- // 在聊天界面显示通话记录
- if (currentChatIndex === callState.contactIndex) {
- appendCallRecordMessage(callState.initiator === 'user' ? 'user' : 'assistant', durationStr, contact);
- }
-
requestSave();
refreshChatList();
}
diff --git a/style.css b/style.css
index 76f8c1e..56a7b8e 100644
--- a/style.css
+++ b/style.css
@@ -276,6 +276,43 @@
background: #d9363e;
}
+/* 多人群聊卡片 */
+.wechat-mp-card {
+ position: relative;
+}
+
+.wechat-mp-card .wechat-card-avatar {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ position: relative;
+}
+
+.wechat-mp-avatar {
+ position: relative;
+}
+
+.wechat-mp-api-badge {
+ position: absolute;
+ bottom: -2px;
+ right: -2px;
+ width: 18px;
+ height: 18px;
+ background: #fff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+
+.wechat-mp-card-content {
+ position: relative;
+}
+
+.wechat-mp-card .wechat-card-name {
+ color: var(--wechat-text);
+}
+
/* ===== 搜索框 ===== */
.wechat-search-box {
padding: 8px 12px;
@@ -770,6 +807,44 @@
border-left-color: var(--wechat-bubble-self);
}
+/* ===== 多人群聊样式 ===== */
+.wechat-mp-message {
+ flex-direction: row;
+ margin-left: 0;
+}
+
+.wechat-mp-message .wechat-message-content {
+ margin-left: 12px;
+ margin-right: 0;
+ max-width: 85%;
+}
+
+.wechat-mp-message .wechat-message-bubble {
+ margin-left: 0;
+}
+
+.wechat-mp-message .wechat-message-bubble:first-child::before {
+ display: none;
+}
+
+.wechat-mp-sender {
+ font-size: 12px;
+ color: var(--wechat-text-secondary);
+ margin-bottom: 2px;
+ padding-left: 2px;
+}
+
+/* 暗色模式下多人群聊名字颜色 */
+.wechat-dark .wechat-mp-sender {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+/* 浅色模式下多人群聊名字颜色 */
+.wechat-light .wechat-mp-sender,
+.wechat-mp-sender {
+ color: rgba(0, 0, 0, 0.6);
+}
+
/* 语音消息 */
.wechat-voice {
display: flex;
@@ -5301,10 +5376,14 @@
}
.wechat-video-call-input-area {
+ position: absolute;
+ bottom: 110px;
+ left: 16px;
+ right: 16px;
display: flex;
gap: 8px;
padding: 8px 0;
- margin: 0 16px 10px 16px;
+ z-index: 20;
}
.wechat-video-call-input-area.hidden {
@@ -13039,3 +13118,438 @@
font-size: 12px;
color: #07c160;
}
+
+/* ========== 多人卡导入 - 角色表格样式 ========== */
+
+/* 表格空状态 */
+.wechat-char-tables-empty {
+ text-align: center;
+ padding: 30px 20px;
+ color: var(--wechat-text-secondary);
+}
+
+/* 表格卡片 */
+.wechat-char-table-card {
+ background: var(--wechat-bg-secondary);
+ border-radius: 8px;
+ margin-bottom: 10px;
+ overflow: hidden;
+}
+
+.wechat-char-table-card.modified {
+ background: linear-gradient(135deg, rgba(87, 107, 149, 0.08), rgba(87, 107, 149, 0.03));
+}
+
+/* 表格标题栏 */
+.wechat-char-table-header {
+ display: flex;
+ align-items: center;
+ padding: 12px 15px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.wechat-char-table-header:hover {
+ background: var(--wechat-bg-tertiary);
+}
+
+.wechat-char-table-arrow {
+ font-size: 10px;
+ color: var(--wechat-text-secondary);
+ margin-right: 8px;
+ transition: transform 0.2s;
+}
+
+.wechat-char-table-card.expanded .wechat-char-table-arrow {
+ transform: rotate(90deg);
+}
+
+.wechat-char-table-title {
+ flex: 1;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.wechat-char-table-badge {
+ font-size: 11px;
+ padding: 2px 8px;
+ background: rgba(87, 107, 149, 0.1);
+ color: #576B95;
+ border-radius: 10px;
+ margin-left: 8px;
+}
+
+.wechat-char-table-modified-tip {
+ font-size: 11px;
+ color: #f5a623;
+ margin-left: 8px;
+}
+
+.wechat-char-table-delete {
+ background: none;
+ border: none;
+ color: var(--wechat-text-secondary);
+ font-size: 18px;
+ cursor: pointer;
+ padding: 4px 8px;
+ margin-left: 8px;
+ opacity: 0.6;
+ transition: opacity 0.2s, color 0.2s;
+}
+
+.wechat-char-table-header:hover .wechat-char-table-delete {
+ opacity: 1;
+}
+
+.wechat-char-table-delete:hover {
+ color: #ff4d4f;
+}
+
+/* 表格内容区 */
+.wechat-char-table-body {
+ padding: 0 15px 15px;
+}
+
+/* 表格样式 */
+.wechat-char-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+}
+
+.wechat-char-table th {
+ text-align: left;
+ padding: 8px 6px;
+ color: var(--wechat-text-secondary);
+ font-weight: normal;
+ font-size: 12px;
+ border-bottom: 1px solid var(--wechat-border);
+}
+
+.wechat-char-table td {
+ padding: 6px;
+ border-bottom: 1px solid var(--wechat-border);
+}
+
+.wechat-char-table tr:last-child td {
+ border-bottom: none;
+}
+
+/* 编辑输入框 */
+.wechat-char-edit-input {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--wechat-text);
+ font-size: 13px;
+ box-sizing: border-box;
+ transition: border-color 0.2s, background 0.2s;
+}
+
+.wechat-char-edit-input:hover {
+ background: var(--wechat-bg-tertiary);
+}
+
+.wechat-char-edit-input:focus {
+ outline: none;
+ border-color: #576B95;
+ background: var(--wechat-bg);
+}
+
+.wechat-char-edit-input.char-name {
+ font-weight: 500;
+}
+
+.wechat-char-edit-input.char-age {
+ text-align: center;
+}
+
+/* 删除行按钮 */
+.wechat-char-row-delete {
+ background: none;
+ border: none;
+ color: var(--wechat-text-secondary);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 4px;
+ opacity: 0;
+ transition: opacity 0.2s, color 0.2s;
+}
+
+.wechat-char-table tr:hover .wechat-char-row-delete {
+ opacity: 1;
+}
+
+.wechat-char-row-delete:hover {
+ color: #ff4d4f;
+}
+
+/* 添加行按钮 */
+.wechat-char-add-row {
+ display: block;
+ width: 100%;
+ padding: 10px;
+ margin-top: 10px;
+ border: 1px dashed var(--wechat-border);
+ border-radius: 6px;
+ background: transparent;
+ color: var(--wechat-text-secondary);
+ font-size: 13px;
+ cursor: pointer;
+ transition: border-color 0.2s, color 0.2s, background 0.2s;
+}
+
+.wechat-char-add-row:hover {
+ border-color: #576B95;
+ color: #576B95;
+ background: rgba(87, 107, 149, 0.05);
+}
+
+/* 表格底部操作栏 */
+.wechat-char-table-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid var(--wechat-border);
+}
+
+.wechat-char-table-time {
+ font-size: 11px;
+ color: var(--wechat-text-secondary);
+}
+
+.wechat-char-table-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.wechat-char-table-save {
+ background: rgba(87, 107, 149, 0.1) !important;
+ color: #576B95 !important;
+}
+
+.wechat-char-table-save:hover {
+ background: rgba(87, 107, 149, 0.2) !important;
+}
+
+/* 表格内容区滚动容器 */
+.wechat-char-table-scroll {
+ max-height: 300px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(87, 107, 149, 0.3) transparent;
+}
+
+.wechat-char-table-scroll::-webkit-scrollbar {
+ width: 6px;
+}
+
+.wechat-char-table-scroll::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.wechat-char-table-scroll::-webkit-scrollbar-thumb {
+ background: rgba(87, 107, 149, 0.3);
+ border-radius: 3px;
+}
+
+.wechat-char-table-scroll::-webkit-scrollbar-thumb:hover {
+ background: rgba(87, 107, 149, 0.5);
+}
+
+/* 其它信息文本域 */
+.wechat-char-textarea {
+ width: 100%;
+ min-height: 60px;
+ max-height: 150px;
+ resize: vertical;
+ font-family: inherit;
+ font-size: 13px;
+ line-height: 1.4;
+ padding: 6px 8px;
+ border: 1px solid var(--wechat-border);
+ border-radius: 4px;
+ background: var(--wechat-bg-primary);
+ color: var(--wechat-text-primary);
+ box-sizing: border-box;
+}
+
+.wechat-char-textarea:focus {
+ outline: none;
+ border-color: #576B95;
+ box-shadow: 0 0 0 2px rgba(87, 107, 149, 0.1);
+}
+
+.wechat-char-textarea::placeholder {
+ color: var(--wechat-text-secondary);
+ opacity: 0.6;
+}
+
+/* 世界观区域 */
+.wechat-worldview-section {
+ margin-bottom: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--wechat-border);
+}
+
+.wechat-worldview-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.wechat-worldview-title {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--wechat-text-primary);
+}
+
+.wechat-worldview-textarea {
+ width: 100%;
+ min-height: 100px;
+ max-height: 200px;
+ resize: vertical;
+ font-family: inherit;
+ font-size: 13px;
+ line-height: 1.5;
+ padding: 10px 12px;
+ border: 1px solid var(--wechat-border);
+ border-radius: 6px;
+ background: var(--wechat-bg-primary);
+ color: var(--wechat-text-primary);
+ box-sizing: border-box;
+}
+
+.wechat-worldview-textarea:focus {
+ outline: none;
+ border-color: #576B95;
+ box-shadow: 0 0 0 2px rgba(87, 107, 149, 0.1);
+}
+
+.wechat-worldview-textarea::placeholder {
+ color: var(--wechat-text-secondary);
+ opacity: 0.6;
+}
+
+/* 角色列表区域 */
+.wechat-characters-section {
+ margin-bottom: 12px;
+}
+
+.wechat-characters-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.wechat-characters-title {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--wechat-text-primary);
+}
+
+/* 解析预览弹窗 */
+.wechat-preview-section {
+ background: var(--wechat-bg-secondary);
+ border-radius: 8px;
+ padding: 12px;
+}
+
+.wechat-preview-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.wechat-preview-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--wechat-text-primary);
+}
+
+.wechat-preview-badge {
+ font-size: 11px;
+ padding: 2px 8px;
+ background: rgba(87, 107, 149, 0.1);
+ color: #576B95;
+ border-radius: 10px;
+}
+
+.wechat-preview-content {
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--wechat-text-primary);
+ white-space: pre-wrap;
+ max-height: 150px;
+ overflow-y: auto;
+ padding: 10px;
+ background: var(--wechat-bg-primary);
+ border-radius: 6px;
+ border: 1px solid var(--wechat-border);
+}
+
+.wechat-preview-list {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.wechat-preview-char-item {
+ display: flex;
+ align-items: flex-start;
+ padding: 10px;
+ margin-bottom: 8px;
+ background: var(--wechat-bg-primary);
+ border-radius: 6px;
+ border: 1px solid var(--wechat-border);
+}
+
+.wechat-preview-char-avatar {
+ width: 40px;
+ height: 40px;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ font-weight: bold;
+ color: #000;
+ flex-shrink: 0;
+ margin-right: 12px;
+}
+
+.wechat-preview-char-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.wechat-preview-char-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--wechat-text-primary);
+ margin-bottom: 4px;
+}
+
+.wechat-preview-char-meta {
+ font-size: 12px;
+ color: var(--wechat-text-secondary);
+ margin-bottom: 4px;
+}
+
+.wechat-preview-char-other {
+ font-size: 12px;
+ color: var(--wechat-text-secondary);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
diff --git a/ui.js b/ui.js
index be88c0f..98de308 100644
--- a/ui.js
+++ b/ui.js
@@ -100,11 +100,12 @@ export function getUserPersonaFromST() {
return null;
}
-// 生成聊天列表 HTML(包含单聊和群聊)
+// 生成聊天列表 HTML(包含单聊、群聊和多人群聊)
export function generateChatList() {
const settings = getSettings();
const contacts = settings.contacts || [];
const groupChats = settings.groupChats || [];
+ const multiPersonChats = settings.multiPersonChats || [];
// 处理单聊
const contactsWithChat = contacts.map((contact, index) => {
@@ -136,8 +137,22 @@ export function generateChatList() {
};
});
+ // 处理多人群聊
+ const multiPersonWithChat = multiPersonChats.map((chat, index) => {
+ const chatHistory = chat.chatHistory || [];
+ const lastMsg = getLastRenderableMessage(chatHistory);
+ const lastMsgTime = lastMsg ? (lastMsg.timestamp || chat.lastMessageTime || 0) : (chat.lastMessageTime || 0);
+ return {
+ type: 'multiPerson',
+ ...chat,
+ originalIndex: index,
+ lastMsg,
+ lastMsgTime: lastMsgTime || Date.now()
+ };
+ });
+
// 合并并排序
- const allChats = [...contactsWithChat, ...groupsWithChat].sort((a, b) => b.lastMsgTime - a.lastMsgTime);
+ const allChats = [...contactsWithChat, ...groupsWithChat, ...multiPersonWithChat].sort((a, b) => b.lastMsgTime - a.lastMsgTime);
if (allChats.length === 0) {
return `
@@ -155,6 +170,8 @@ export function generateChatList() {
return allChats.map(chat => {
if (chat.type === 'group') {
return generateGroupChatItem(chat, settings);
+ } else if (chat.type === 'multiPerson') {
+ return generateMultiPersonChatItem(chat);
} else {
return generateContactChatItem(chat);
}
@@ -319,13 +336,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 = `
`;
+ } else {
+ avatarHtml = `群
`;
+ }
+
+ return `
+
+
${avatarHtml}
+
+
${escapeHtml(chat.name || '群聊')}(${memberCount})
+
${escapeHtml(preview)}
+
+
+ ${msgTime}
+
+
+ `;
+}
+
// 生成联系人列表 HTML
export function generateContactsList() {
const settings = getSettings();
const contacts = settings.contacts || [];
const groupChats = settings.groupChats || [];
+ const multiPersonChats = settings.multiPersonChats || [];
- if (contacts.length === 0 && groupChats.length === 0) {
+ if (contacts.length === 0 && groupChats.length === 0 && multiPersonChats.length === 0) {
return `
@@ -403,6 +468,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 = `

`;
+ } else {
+ avatarHtml = `
群
`;
+ }
+
+ html += `
+
+ `;
+ });
+
// 生成联系人卡片
contacts.forEach((contact, index) => {
const firstChar = contact.name ? contact.name.charAt(0) : '?';
diff --git a/voice-call.js b/voice-call.js
index 8dc8bb4..2088335 100644
--- a/voice-call.js
+++ b/voice-call.js
@@ -729,7 +729,9 @@ async function triggerAIGreeting() {
let reply = part.trim();
// 通话中禁用表情包/图片/音乐等富媒体(兜底过滤)
- reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
+ reply = reply.replace(/<\s*meme\s*>[^<]*<\s*\/\s*meme\s*>/gi, '').trim();
+ reply = reply.replace(/
[^<]*<\/meme>/gi, '').trim();
+ reply = reply.replace(/<\/?meme>/gi, '').trim();
if (!reply) continue;
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
// 移除语音标记
@@ -854,7 +856,9 @@ ${lastMessages}
for (const part of parts) {
let reply = part.trim();
// 通话中禁用表情包/图片/音乐等富媒体(兜底过滤)
- reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
+ reply = reply.replace(/<\s*meme\s*>[^<]*<\s*\/\s*meme\s*>/gi, '').trim();
+ reply = reply.replace(/[^<]*<\/meme>/gi, '').trim();
+ reply = reply.replace(/<\/?meme>/gi, '').trim();
if (!reply) continue;
if (/^\[(?:表情|照片|分享音乐|音乐)[::]/.test(reply)) continue;
// 移除可能的特殊标记