Files
Cola/main.js
2026-01-02 02:48:58 +08:00

2959 lines
110 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 可乐不加冰 - 主入口(模块化)
*/
console.log('[可乐] main.js 开始加载...');
import { requestSave, setupUnloadSave } from './save-manager.js';
import { loadSettings, getSettings } from './config.js';
import { generatePhoneHTML } from './phone-html.js';
import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js';
import { showToast } from './toast.js';
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex, 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';
import { fetchModelListFromApi } from './ai.js';
import { extractCharacterFromPNG, extractCharacterFromJSON, importCharacterToST } from './character-import.js';
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';
import { injectAuthorNote, setupMessageObserver, addExtensionButton } from './st-integration.js';
import { getCurrentTime } from './utils.js';
import { refreshHistoryList, refreshLogsList, clearErrorLogs, initErrorCapture, addErrorLog, renderToyHistory } from './history-logs.js';
import { initChatBackground } from './chat-background.js';
import { initMoments, openMomentsPage, clearContactMoments } from './moments.js';
import { initRedPacketEvents } from './red-packet.js';
import { initTransferEvents } from './transfer.js';
import { initGroupRedPacket } from './group-red-packet.js';
import { initGiftEvents } from './gift.js';
import { initCropper } from './cropper.js';
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
import { testSttApi, testTtsApi } from './voice-api.js';
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording, getAllVoiceRecordingsGroupedByContact, deleteVoiceRecordingsByContact } from './audio-storage.js';
import { initMultiCharImport, openMultiImportModal, getMultiCharImportModalHtml, getCharSelectModalHtml, getCharOtherEditModalHtml } from './multi-char-import.js';
// ========== 历史记录功能 ==========
let currentHistoryTab = 'listen';
let currentHistoryContactIndex = -1;
function openHistoryPage(contactIndex) {
const settings = getSettings();
const contact = settings.contacts?.[contactIndex];
if (!contact) return;
currentHistoryContactIndex = contactIndex;
currentHistoryTab = 'listen';
const page = document.getElementById('wechat-history-page');
if (page) {
page.classList.remove('hidden');
// 重置标签状态
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === 'listen');
});
renderHistoryContent(contact, 'listen');
}
}
function closeHistoryPage() {
const page = document.getElementById('wechat-history-page');
if (page) {
page.classList.add('hidden');
}
currentHistoryContactIndex = -1;
}
function deleteHistoryRecord(tabType, index, isRealVoice = false) {
const settings = getSettings();
const contact = settings.contacts?.[currentHistoryContactIndex];
if (!contact) return;
if (tabType === 'listen') {
if (contact.listenHistory && contact.listenHistory[index]) {
contact.listenHistory.splice(index, 1);
}
} else if (tabType === 'voice') {
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]) {
const originalIndex = callHistory.indexOf(typeRecords[index]);
if (originalIndex >= 0) {
contact.callHistory.splice(originalIndex, 1);
}
}
} else if (tabType === 'toy') {
if (contact.toyHistory && contact.toyHistory[index]) {
contact.toyHistory.splice(index, 1);
}
}
requestSave();
renderHistoryContent(contact, tabType);
}
function switchHistoryTab(tabType) {
currentHistoryTab = tabType;
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabType);
});
const settings = getSettings();
const contact = settings.contacts?.[currentHistoryContactIndex];
if (contact) {
renderHistoryContent(contact, tabType);
}
}
function renderHistoryContent(contact, tabType) {
const contentEl = document.getElementById('wechat-history-content');
if (!contentEl) return;
// 心动瞬间使用专门的渲染函数
if (tabType === 'toy') {
renderToyHistory(contact);
// 绑定心动瞬间的删除按钮事件
contentEl.querySelectorAll('.wechat-history-delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
deleteHistoryRecord('toy', index);
});
});
// 绑定标签内的叉叉按钮事件
contentEl.querySelectorAll('.wechat-toy-target-close-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
deleteHistoryRecord('toy', index);
});
});
return;
}
// 语音回放使用专门的渲染函数
if (tabType === 'playback') {
renderVoicePlaybackContent(contact);
return;
}
const context = window.SillyTavern?.getContext?.() || {};
const userName = context.name1 || '用户';
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 中筛选 video
const callHistory = contact.callHistory || [];
records = callHistory.filter(r => r.type === tabType);
}
if (records.length === 0) {
const emptyText = tabType === 'listen' ? '暂无一起听记录' :
tabType === 'voice' ? '暂无语音通话记录' : '暂无视频通话记录';
contentEl.innerHTML = `
<div class="wechat-history-empty">
<div class="wechat-history-empty-icon">📭</div>
<div>${emptyText}</div>
</div>
`;
return;
}
// 按时间倒序排列
const sortedRecords = [...records].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
let html = '';
for (let i = 0; i < sortedRecords.length; i++) {
const record = sortedRecords[i];
const time = record.time || '未知时间';
const duration = record.duration || '';
const messages = record.messages || [];
const originalIndex = records.indexOf(record);
const isRealVoice = record.isRealVoice ? 'true' : 'false';
html += `<div class="wechat-history-card" data-tab="${tabType}" data-index="${originalIndex}" data-real-voice="${isRealVoice}">`;
html += `<div class="wechat-history-card-header">`;
html += `<span class="wechat-history-card-time">${escapeHtml(time)}${record.isRealVoice ? ' <span style="color: #07c160; font-size: 12px;">[实时语音]</span>' : ''}</span>`;
html += `<div class="wechat-history-card-actions">`;
html += `<button class="wechat-history-delete-btn" data-tab="${tabType}" data-index="${originalIndex}" data-real-voice="${isRealVoice}" title="删除">×</button>`;
if (duration) {
html += `<span class="wechat-history-card-duration">${escapeHtml(duration)}</span>`;
}
html += `</div>`;
html += `</div>`;
// 一起听显示歌曲信息
if (tabType === 'listen' && record.song) {
const songName = record.song.name || '未知歌曲';
const songArtist = record.song.artist || '未知歌手';
html += `<div class="wechat-history-card-song">[${escapeHtml(songName)} - ${escapeHtml(songArtist)}]</div>`;
}
// 消息列表
if (messages.length > 0) {
html += `<div class="wechat-history-card-messages">`;
for (const msg of messages) {
const isUser = msg.role === 'user';
const senderName = isUser ? userName : contact.name;
const senderClass = isUser ? 'user' : '';
html += `<div class="wechat-history-msg">`;
html += `<span class="wechat-history-msg-sender ${senderClass}">${escapeHtml(senderName)}:</span> `;
html += `<span class="wechat-history-msg-content">${escapeHtml(msg.content || '')}</span>`;
html += `</div>`;
}
html += `</div>`;
}
html += `</div>`;
}
contentEl.innerHTML = html;
// 绑定删除按钮事件
contentEl.querySelectorAll('.wechat-history-delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const tab = btn.dataset.tab;
const index = parseInt(btn.dataset.index);
const isRealVoice = btn.dataset.realVoice === 'true';
deleteHistoryRecord(tab, index, isRealVoice);
});
});
}
// 渲染语音回放内容
async function renderVoicePlaybackContent(contact) {
const contentEl = document.getElementById('wechat-history-content');
if (!contentEl) return;
const contactIndex = currentHistoryContactIndex;
if (contactIndex < 0) {
contentEl.innerHTML = '<div class="wechat-history-empty"><div class="wechat-history-empty-icon">📭</div><div>请先选择联系人</div></div>';
return;
}
// 显示加载状态
contentEl.innerHTML = '<div class="wechat-history-empty"><div>加载中...</div></div>';
try {
const recordings = await getVoiceRecordingsByContact(contactIndex);
if (!recordings || recordings.length === 0) {
contentEl.innerHTML = `
<div class="wechat-history-empty">
<div class="wechat-history-empty-icon" style="color: #07c160;">
<svg viewBox="0 0 24 24" width="48" height="48">
<path d="M12 1a4 4 0 0 0-4 4v7a4 4 0 0 0 8 0V5a4 4 0 0 0-4-4z" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>
</svg>
</div>
<div>暂无语音回放记录</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary); margin-top: 8px;">实时语音通话结束后可选择保存语音</div>
</div>
`;
return;
}
// 按保存时间倒序排列
const sortedRecordings = [...recordings].sort((a, b) => (b.savedAt || 0) - (a.savedAt || 0));
let html = '<div class="wechat-voice-playback-list">';
for (const recording of sortedRecordings) {
const savedTime = recording.savedAt ? new Date(recording.savedAt).toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}) : '未知时间';
const durationSec = Math.round(recording.duration || 0);
const durationStr = durationSec > 0 ? `${durationSec}"` : '?秒';
html += `
<div class="wechat-voice-playback-card" data-id="${recording.id}">
<div class="wechat-voice-playback-card-header">
<span class="wechat-voice-playback-time">${escapeHtml(savedTime)}</span>
<div class="wechat-voice-playback-actions">
<span class="wechat-voice-playback-duration">${durationStr}</span>
<button class="wechat-voice-playback-delete" data-id="${recording.id}" title="删除">×</button>
</div>
</div>
<div class="wechat-voice-playback-content">
<div class="wechat-voice-playback-text">${escapeHtml(recording.text || '')}</div>
<button class="wechat-voice-playback-btn" data-id="${recording.id}" title="播放">
<svg viewBox="0 0 24 24" width="20" height="20"><polygon points="5,3 19,12 5,21" fill="currentColor"/></svg>
</button>
</div>
</div>
`;
}
html += '</div>';
contentEl.innerHTML = html;
// 绑定播放按钮事件
contentEl.querySelectorAll('.wechat-voice-playback-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const id = parseInt(btn.dataset.id);
try {
btn.disabled = true;
btn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20"><rect x="6" y="4" width="4" height="16" fill="currentColor"/><rect x="14" y="4" width="4" height="16" fill="currentColor"/></svg>';
await playVoiceRecording(id);
} catch (err) {
console.error('[可乐] 播放语音失败:', err);
showToast('播放失败', '⚠️');
} finally {
btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20"><polygon points="5,3 19,12 5,21" fill="currentColor"/></svg>';
}
});
});
// 绑定删除按钮事件
contentEl.querySelectorAll('.wechat-voice-playback-delete').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const id = parseInt(btn.dataset.id);
if (confirm('确定要删除这条语音吗?')) {
try {
await deleteVoiceRecording(id);
showToast('已删除', '✓');
// 重新渲染
renderVoicePlaybackContent(contact);
} catch (err) {
console.error('[可乐] 删除语音失败:', err);
showToast('删除失败', '⚠️');
}
}
});
});
} catch (err) {
console.error('[可乐] 加载语音记录失败:', err);
contentEl.innerHTML = `
<div class="wechat-history-empty">
<div class="wechat-history-empty-icon">⚠️</div>
<div>加载失败</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${escapeHtml(err.message || '')}</div>
</div>
`;
}
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function initHistoryEvents() {
// 返回按钮
document.getElementById('wechat-history-back-btn')?.addEventListener('click', closeHistoryPage);
// 标签切换
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabType = tab.dataset.tab;
if (tabType) {
switchHistoryTab(tabType);
}
});
});
}
// ========== 历史记录功能结束 ==========
function normalizeModelListForSelect(models) {
return (models || []).map(m => {
if (typeof m === 'string') return { id: m, name: m };
return { id: m?.id || '', name: m?.name || m?.id || '' };
}).filter(m => m.id);
}
function restoreModelSelect() {
// select 元素在 HTML 生成时已经包含了选项,无需额外恢复
}
function restoreGroupModelSelect() {
// select 元素在 HTML 生成时已经包含了选项,无需额外恢复
}
function seedDefaultUserPersonaFromST(settings) {
if (Array.isArray(settings.userPersonas) && settings.userPersonas.length > 0) return false;
const stPersona = getUserPersonaFromST();
const content = stPersona?.description?.trim();
if (!content) return false;
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')}`;
settings.userPersonas = [{
name: (stPersona?.name || '').trim() || '用户设定',
content,
enabled: true,
addedTime: timeStr,
}];
return true;
}
async function refreshModelSelect() {
const select = document.getElementById('wechat-model-select');
const refreshBtn = document.getElementById('wechat-refresh-models');
if (!select) return;
const settings = getSettings();
const apiUrl = document.getElementById('wechat-api-url')?.value?.trim() || settings.apiUrl || '';
const apiKey = document.getElementById('wechat-api-key')?.value?.trim() || settings.apiKey || '';
if (!apiUrl) {
showToast('请先填写 API 地址', 'info');
return;
}
const originalText = refreshBtn?.textContent;
if (refreshBtn) {
refreshBtn.textContent = '加载中...';
refreshBtn.disabled = true;
}
try {
const modelIds = await fetchModelListFromApi(apiUrl, apiKey);
// 更新 select 选项
select.innerHTML = '<option value="">-- 选择模型 --</option>' +
modelIds.map(id => `<option value="${id}">${id}</option>`).join('');
settings.modelList = modelIds;
requestSave();
showToast(`获取到 ${modelIds.length} 个模型`);
} catch (err) {
console.error('[可乐] 获取模型列表失败:', err);
showToast(`获取失败,请手动输入模型名`, '⚠️');
} finally {
if (refreshBtn) {
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
}
}
}
function syncContextEnabledUI(enabled) {
const display = document.getElementById('wechat-context-level-display');
if (display) display.textContent = enabled ? '已开启' : '已关闭';
const settingsSection = document.getElementById('wechat-context-settings');
if (settingsSection) {
settingsSection.style.opacity = enabled ? '1' : '0.5';
settingsSection.style.pointerEvents = enabled ? 'auto' : 'none';
}
}
function updateWalletAmountDisplay() {
const settings = getSettings();
const amountEl = document.getElementById('wechat-wallet-amount');
if (!amountEl) return;
const amount = settings.walletAmount || '5773.89';
amountEl.textContent = amount.startsWith('¥') ? amount : `${amount}`;
}
// ===== 缩小/恢复手机功能 =====
let minimizeState = {
isDragging: false,
startX: 0,
startY: 0,
initialLeft: 0,
initialTop: 0,
hasMoved: false
};
// 悬浮窗开关
function toggleFloatingBallEnabled() {
const settings = getSettings();
const isEnabled = settings.floatingBallEnabled !== false;
if (isEnabled) {
// 关闭悬浮窗
settings.floatingBallEnabled = false;
hideFloatingBall();
updateFloatingBallMenuText(false);
} else {
// 开启悬浮窗
settings.floatingBallEnabled = true;
// 只有非缩小状态才显示
const phone = document.getElementById('wechat-phone');
if (!phone?.classList.contains('minimized')) {
showFloatingBall();
}
updateFloatingBallMenuText(true);
}
requestSave();
}
function updateFloatingBallMenuText(enabled) {
const textEl = document.getElementById('wechat-floating-ball-text');
if (textEl) {
textEl.textContent = enabled ? '关闭悬浮窗' : '开启悬浮窗';
}
}
// 清除孤立缓存(已删除联系人/群聊的总结历史和语音记录)
async function clearOrphanedCache() {
const settings = getSettings();
const contacts = settings.contacts || [];
const groupChats = settings.groupChats || [];
const lorebooks = settings.selectedLorebooks || [];
// 获取当前有效的联系人ID和名称
const validContactIds = new Set(contacts.map(c => c.id).filter(id => id));
const validContactNames = new Set(contacts.map(c => c.name).filter(n => n));
const validGroupNames = new Set(groupChats.map(g => g.name).filter(n => n));
const validContactIndexes = new Set(contacts.map((_, idx) => idx));
// 查找孤立的总结世界书
const orphanedSummaries = [];
lorebooks.forEach((lb, idx) => {
const isSummaryBook = lb.fromSummary === true ||
(lb.name?.startsWith('【可乐】和') && lb.name?.endsWith('的聊天'));
if (isSummaryBook) {
const nameMatch = lb.name?.match(/^【可乐】和(.+)的聊天$/);
const linkedName = nameMatch ? nameMatch[1] : null;
const contactExists = linkedName && validContactNames.has(linkedName);
const groupExists = linkedName && validGroupNames.has(linkedName);
const linkedById = lb.characterId && validContactIds.has(lb.characterId);
if (!contactExists && !groupExists && !linkedById) {
const cupCount = lb.entries?.length || 0;
orphanedSummaries.push({
type: 'summary',
name: lb.name,
linkedName: linkedName || '未知',
index: idx,
cupCount
});
}
}
});
// 查找孤立的语音记录
const orphanedVoices = [];
try {
const voiceGroups = await getAllVoiceRecordingsGroupedByContact();
for (const [contactIdxStr, data] of Object.entries(voiceGroups)) {
const contactIdx = parseInt(contactIdxStr);
// 如果索引超出当前联系人范围,则为孤立数据
if (!validContactIndexes.has(contactIdx)) {
orphanedVoices.push({
type: 'voice',
contactIndex: contactIdx,
count: data.count,
totalDuration: data.totalDuration
});
}
}
} catch (err) {
console.warn('[可乐] 获取语音记录失败:', err);
}
// 如果没有孤立数据
if (orphanedSummaries.length === 0 && orphanedVoices.length === 0) {
showToast('没有发现需要清理的缓存数据');
return;
}
// 显示选择弹窗
showCacheCleanupModal(orphanedSummaries, orphanedVoices);
}
// 显示缓存清理选择弹窗
function showCacheCleanupModal(orphanedSummaries, orphanedVoices) {
document.getElementById('wechat-cache-cleanup-modal')?.remove();
const hasSummaries = orphanedSummaries.length > 0;
const hasVoices = orphanedVoices.length > 0;
const modal = document.createElement('div');
modal.className = 'wechat-modal';
modal.id = 'wechat-cache-cleanup-modal';
modal.innerHTML = `
<div class="wechat-modal-content" style="position: relative; max-width: 380px; max-height: 80vh; margin: auto;">
<button class="wechat-modal-close-x" id="wechat-cache-modal-close">×</button>
<div class="wechat-modal-title">清除缓存</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary); margin-bottom: 12px; padding: 0 4px;">
勾选要清除的项目:
</div>
<div style="max-height: 45vh; overflow-y: auto; margin-bottom: 15px;">
${hasSummaries ? `
<div style="font-size: 12px; color: var(--wechat-text-secondary); padding: 8px; background: var(--wechat-bg-secondary); border-radius: 4px; margin-bottom: 8px;">
📝 总结历史(已删除联系人/群聊)
</div>
${orphanedSummaries.map(item => `
<label class="wechat-cache-item" style="display: flex; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
<input type="checkbox" class="wechat-cache-checkbox" data-type="summary" data-index="${item.index}" checked style="margin-right: 10px; width: 18px; height: 18px;">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${item.linkedName}</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${item.cupCount} 杯总结记录</div>
</div>
</label>
`).join('')}
` : ''}
${hasVoices ? `
<div style="font-size: 12px; color: var(--wechat-text-secondary); padding: 8px; background: var(--wechat-bg-secondary); border-radius: 4px; margin-bottom: 8px; ${hasSummaries ? 'margin-top: 12px;' : ''}">
🎙️ 语音通话记录(孤立数据)
</div>
${orphanedVoices.map(item => `
<label class="wechat-cache-item" style="display: flex; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
<input type="checkbox" class="wechat-cache-checkbox" data-type="voice" data-contact-index="${item.contactIndex}" checked style="margin-right: 10px; width: 18px; height: 18px;">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 14px; font-weight: 500;">联系人 #${item.contactIndex}</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${item.count} 条语音,共 ${Math.round(item.totalDuration)} 秒</div>
</div>
</label>
`).join('')}
` : ''}
</div>
<div style="display: flex; gap: 10px; justify-content: space-between; align-items: center;">
<label style="display: flex; align-items: center; font-size: 13px; color: var(--wechat-text-secondary); cursor: pointer;">
<input type="checkbox" id="wechat-cache-select-all" checked style="margin-right: 6px;">
全选
</label>
<div style="display: flex; gap: 10px;">
<button class="wechat-btn wechat-btn-secondary" id="wechat-cache-cancel">取消</button>
<button class="wechat-btn wechat-btn-danger" id="wechat-cache-confirm">清除选中</button>
</div>
</div>
</div>
`;
const phoneContainer = document.querySelector('.wechat-phone') || document.body;
phoneContainer.appendChild(modal);
// 关闭按钮
modal.querySelector('#wechat-cache-modal-close').addEventListener('click', () => modal.remove());
modal.querySelector('#wechat-cache-cancel').addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
// 全选/取消全选
const selectAllCheckbox = modal.querySelector('#wechat-cache-select-all');
const itemCheckboxes = modal.querySelectorAll('.wechat-cache-checkbox');
selectAllCheckbox.addEventListener('change', () => {
itemCheckboxes.forEach(cb => cb.checked = selectAllCheckbox.checked);
});
itemCheckboxes.forEach(cb => {
cb.addEventListener('change', () => {
const allChecked = Array.from(itemCheckboxes).every(c => c.checked);
const noneChecked = Array.from(itemCheckboxes).every(c => !c.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = !allChecked && !noneChecked;
});
});
// 确认清除
modal.querySelector('#wechat-cache-confirm').addEventListener('click', async () => {
const selectedSummaryIndexes = new Set();
const selectedVoiceIndexes = [];
itemCheckboxes.forEach(cb => {
if (cb.checked) {
if (cb.dataset.type === 'summary') {
selectedSummaryIndexes.add(parseInt(cb.dataset.index));
} else if (cb.dataset.type === 'voice') {
selectedVoiceIndexes.push(parseInt(cb.dataset.contactIndex));
}
}
});
if (selectedSummaryIndexes.size === 0 && selectedVoiceIndexes.length === 0) {
showToast('请至少选择一项');
return;
}
let clearedCount = 0;
// 清除总结缓存
if (selectedSummaryIndexes.size > 0) {
const settings = getSettings();
settings.selectedLorebooks = settings.selectedLorebooks.filter((_, idx) => !selectedSummaryIndexes.has(idx));
requestSave();
clearedCount += selectedSummaryIndexes.size;
}
// 清除语音缓存
for (const contactIdx of selectedVoiceIndexes) {
try {
await deleteVoiceRecordingsByContact(contactIdx);
clearedCount++;
} catch (err) {
console.error('[可乐] 删除语音记录失败:', err);
}
}
modal.remove();
showToast(`已清除 ${clearedCount} 项缓存数据`);
console.log('[可乐] 已清除缓存,总结:', selectedSummaryIndexes.size, '语音:', selectedVoiceIndexes.length);
});
}
function setupPhoneMinimize() {
const phone = document.getElementById('wechat-phone');
const minimizeBtn = document.getElementById('wechat-minimize-btn');
if (!phone || !minimizeBtn) return;
// 点击右上角图标 - 缩小 (PC)
minimizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
minimizePhone();
});
// 移动端触摸支持
let minimizeBtnTouchMoved = false;
minimizeBtn.addEventListener('touchstart', (e) => {
minimizeBtnTouchMoved = false;
}, { passive: true });
minimizeBtn.addEventListener('touchmove', (e) => {
minimizeBtnTouchMoved = true;
}, { passive: true });
minimizeBtn.addEventListener('touchend', (e) => {
if (!minimizeBtnTouchMoved) {
e.stopPropagation();
e.preventDefault();
minimizePhone();
}
});
// 缩小后点击恢复 + 拖动支持
phone.addEventListener('mousedown', onMinimizedDragStart);
document.addEventListener('mousemove', onMinimizedDragMove);
document.addEventListener('mouseup', onMinimizedDragEnd);
// 触摸支持
phone.addEventListener('touchstart', onMinimizedDragStart, { passive: false });
document.addEventListener('touchmove', onMinimizedDragMove, { passive: false });
document.addEventListener('touchend', onMinimizedDragEnd);
}
function minimizePhone() {
const phone = document.getElementById('wechat-phone');
if (!phone) return;
// 获取当前位置
const rect = phone.getBoundingClientRect();
const settings = getSettings();
// 保存原始位置
if (!settings.phoneOriginalPosition) {
settings.phoneOriginalPosition = {
left: phone.style.left || rect.left + 'px',
top: phone.style.top || rect.top + 'px'
};
}
// 缩小后移到右下角
const scale = 0.25;
const phoneWidth = rect.width * scale;
const phoneHeight = rect.height * scale;
// 使用保存的缩小位置或默认右下角
const savedMinPos = settings.phoneMinimizedPosition;
let targetLeft, targetTop;
if (savedMinPos) {
targetLeft = savedMinPos.left;
targetTop = savedMinPos.top;
} else {
targetLeft = window.innerWidth - phoneWidth - 20;
targetTop = window.innerHeight - phoneHeight - 20;
}
phone.style.left = targetLeft + 'px';
phone.style.top = targetTop + 'px';
phone.style.right = 'auto';
phone.style.bottom = 'auto';
phone.classList.add('minimized');
// 缩小时隐藏悬浮球
hideFloatingBall();
requestSave();
}
function restorePhone() {
const phone = document.getElementById('wechat-phone');
if (!phone) return;
const settings = getSettings();
phone.classList.remove('minimized');
// 清除缩小前保存的位置,让居中函数重新计算
delete settings.phoneOriginalPosition;
// 恢复到屏幕中央
centerPhoneInViewport({ force: true });
// 恢复时根据设置显示悬浮球
if (settings.floatingBallEnabled !== false) {
showFloatingBall();
}
requestSave();
}
function onMinimizedDragStart(e) {
const phone = document.getElementById('wechat-phone');
if (!phone || !phone.classList.contains('minimized')) return;
minimizeState.isDragging = true;
minimizeState.hasMoved = false;
const rect = phone.getBoundingClientRect();
// 缩小状态下需要考虑缩放后的实际位置
minimizeState.initialLeft = parseFloat(phone.style.left) || rect.left;
minimizeState.initialTop = parseFloat(phone.style.top) || rect.top;
if (e.type === 'touchstart') {
minimizeState.startX = e.touches[0].clientX;
minimizeState.startY = e.touches[0].clientY;
e.preventDefault();
} else {
minimizeState.startX = e.clientX;
minimizeState.startY = e.clientY;
}
phone.style.transition = 'none';
}
function onMinimizedDragMove(e) {
if (!minimizeState.isDragging) return;
const phone = document.getElementById('wechat-phone');
if (!phone || !phone.classList.contains('minimized')) return;
let clientX, clientY;
if (e.type === 'touchmove') {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
e.preventDefault();
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const deltaX = clientX - minimizeState.startX;
const deltaY = clientY - minimizeState.startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
minimizeState.hasMoved = true;
}
// 直接使用当前触摸/鼠标位置减去元素尺寸的一半,让元素中心跟随手指
const scale = 0.25;
const rect = phone.getBoundingClientRect();
const scaledWidth = rect.width;
const scaledHeight = rect.height;
// 计算新位置:让缩小后的手机中心跟随手指
let newLeft = clientX - scaledWidth / 2;
let newTop = clientY - scaledHeight / 2;
// 限制在视口内
const maxX = window.innerWidth - scaledWidth;
const maxY = window.innerHeight - scaledHeight;
newLeft = Math.min(Math.max(0, newLeft), maxX);
newTop = Math.min(Math.max(0, newTop), maxY);
phone.style.left = newLeft + 'px';
phone.style.top = newTop + 'px';
}
function onMinimizedDragEnd(e) {
if (!minimizeState.isDragging) return;
const phone = document.getElementById('wechat-phone');
minimizeState.isDragging = false;
if (phone) {
phone.style.transition = '';
}
if (!minimizeState.hasMoved) {
// 没有移动,视为点击 - 恢复
restorePhone();
} else {
// 移动了,保存位置
if (phone && phone.classList.contains('minimized')) {
const settings = getSettings();
settings.phoneMinimizedPosition = {
left: parseFloat(phone.style.left),
top: parseFloat(phone.style.top)
};
requestSave();
}
}
}
function bindEvents() {
// ===== 缩小/恢复手机功能 =====
setupPhoneMinimize();
// 添加按钮 - 显示下拉菜单
document.getElementById('wechat-add-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('wechat-dropdown-menu')?.classList.toggle('hidden');
});
// 点击其他地方关闭下拉菜单
document.getElementById('wechat-phone')?.addEventListener('click', (e) => {
if (!e.target.closest('#wechat-add-btn') && !e.target.closest('#wechat-dropdown-menu')) {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
}
});
// 通讯录页面的添加按钮 - 直接进入添加朋友页面
document.getElementById('wechat-contacts-add-btn')?.addEventListener('click', () => {
showPage('wechat-add-page');
});
// 下拉菜单 - 添加朋友
document.getElementById('wechat-menu-add-friend')?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
showPage('wechat-add-page');
});
// 下拉菜单 - 发起群聊
document.getElementById('wechat-menu-group')?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
showGroupCreateModal();
});
// 下拉菜单 - 其他选项(暂时只关闭菜单)
['wechat-menu-scan', 'wechat-menu-pay'].forEach(id => {
document.getElementById(id)?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
});
});
// 下拉菜单 - 悬浮窗开关
document.getElementById('wechat-menu-floating-ball')?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
toggleFloatingBallEnabled();
});
// 下拉菜单 - 清除缓存
document.getElementById('wechat-menu-clear-cache')?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
clearOrphanedCache();
});
// ===== 群聊创建弹窗事件 =====
document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal);
document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat);
// 返回按钮
document.getElementById('wechat-back-btn')?.addEventListener('click', () => {
showPage('wechat-main-content');
});
document.getElementById('wechat-chat-back-btn')?.addEventListener('click', () => {
setCurrentChatIndex(-1);
setCurrentGroupChatIndex(-1);
setCurrentMultiPersonChatIndex(-1);
// 清除群聊和多人群聊标记
const messagesContainer = document.getElementById('wechat-chat-messages');
if (messagesContainer) {
messagesContainer.dataset.isGroup = 'false';
messagesContainer.dataset.groupIndex = '-1';
messagesContainer.dataset.isMultiPerson = 'false';
messagesContainer.dataset.multiPersonIndex = '-1';
// 清除背景
messagesContainer.style.backgroundImage = '';
}
// 关闭所有聊天页面板
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
document.getElementById('wechat-recalled-panel')?.classList.add('hidden');
document.getElementById('wechat-chat-bg-panel')?.classList.add('hidden');
showPage('wechat-main-content');
refreshContactsList();
refreshChatList();
});
// ===== 聊天页菜单事件 =====
// 三个点按钮 - 显示聊天菜单
document.getElementById('wechat-chat-more-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
const menu = document.getElementById('wechat-chat-menu');
const recalledPanel = document.getElementById('wechat-recalled-panel');
const bgPanel = document.getElementById('wechat-chat-bg-panel');
recalledPanel?.classList.add('hidden');
bgPanel?.classList.add('hidden');
menu?.classList.toggle('hidden');
});
// 撤回消息菜单项 - 显示撤回消息区
document.getElementById('wechat-menu-recalled')?.addEventListener('click', () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
showRecalledMessages();
});
// 关闭撤回消息区面板
document.getElementById('wechat-recalled-close')?.addEventListener('click', () => {
document.getElementById('wechat-recalled-panel')?.classList.add('hidden');
});
// 邀请成员(群聊)
document.getElementById('wechat-menu-invite-member')?.addEventListener('click', () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
import('./group-chat.js').then(m => m.showInviteMemberModal());
});
// 查看TA的朋友圈
document.getElementById('wechat-menu-moments')?.addEventListener('click', () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
if (currentChatIndex >= 0) {
openMomentsPage(currentChatIndex);
}
});
// 查看历史记录
document.getElementById('wechat-menu-history')?.addEventListener('click', () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
if (currentChatIndex >= 0) {
openHistoryPage(currentChatIndex);
}
});
// 清空TA的朋友圈
document.getElementById('wechat-menu-clear-moments')?.addEventListener('click', () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
if (currentChatIndex >= 0) {
clearContactMoments(currentChatIndex);
}
});
// 清空当前聊天(支持单聊和群聊)
document.getElementById('wechat-menu-clear-chat')?.addEventListener('click', () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
const groupIndex = getCurrentGroupIndex();
const settings = getSettings();
// 群聊清空
if (groupIndex >= 0) {
if (!confirm('确定要清空当前群聊记录吗?此操作不可恢复。')) return;
const groupChat = settings.groupChats?.[groupIndex];
if (groupChat) {
groupChat.chatHistory = [];
groupChat.lastMessage = '';
requestSave();
openGroupChat(groupIndex); // 刷新群聊界面
showToast('群聊记录已清空');
}
return;
}
// 单聊清空
if (currentChatIndex < 0) return;
if (!confirm('确定要清空当前聊天记录吗?此操作不可恢复。')) return;
const contact = settings.contacts[currentChatIndex];
if (contact) {
contact.chatHistory = [];
contact.lastMessage = '';
requestSave();
openChat(currentChatIndex); // 刷新聊天界面
showToast('聊天记录已清空');
}
});
// 拉黑/取消拉黑功能
document.getElementById('wechat-menu-block')?.addEventListener('click', async () => {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
// 群聊不支持拉黑
const groupIndex = getCurrentGroupIndex();
if (groupIndex >= 0) {
showToast('群聊暂不支持此功能', 'info');
return;
}
if (currentChatIndex < 0) return;
const settings = getSettings();
const contact = settings.contacts[currentChatIndex];
if (!contact) return;
const isBlocked = contact.isBlocked === true;
if (isBlocked) {
// 取消拉黑
if (!confirm(`确定要取消拉黑"${contact.name}"吗?`)) return;
contact.isBlocked = false;
stopBlockedAIMessages(contact);
requestSave();
refreshChatList();
updateBlockMenuText(false);
showToast('已取消拉黑', '✓');
// 显示被拉黑期间AI发送的消息
await showBlockedMessages(contact);
} else {
// 拉黑
if (!confirm(`确定要拉黑"${contact.name}"吗?拉黑后对方将无法给你发消息。`)) return;
contact.isBlocked = true;
requestSave();
refreshChatList();
updateBlockMenuText(true);
showToast('已拉黑', '🚫');
// 开始AI被拉黑期间发消息
startBlockedAIMessages(contact);
}
});
// 点击聊天页其他地方关闭菜单和面板
document.getElementById('wechat-chat-page')?.addEventListener('click', (e) => {
if (!e.target.closest('#wechat-chat-more-btn') && !e.target.closest('#wechat-chat-menu')) {
document.getElementById('wechat-chat-menu')?.classList.add('hidden');
}
if (!e.target.closest('#wechat-chat-bg-panel') && !e.target.closest('#wechat-chat-menu')) {
document.getElementById('wechat-chat-bg-panel')?.classList.add('hidden');
}
});
document.getElementById('wechat-settings-back-btn')?.addEventListener('click', () => {
showPage('wechat-me-page');
});
document.getElementById('wechat-favorites-back-btn')?.addEventListener('click', () => {
showPage('wechat-me-page');
});
// 导入 PNG/JSON
document.getElementById('wechat-import-png')?.addEventListener('click', () => {
document.getElementById('wechat-file-png')?.click();
});
document.getElementById('wechat-import-json')?.addEventListener('click', () => {
document.getElementById('wechat-file-json')?.click();
});
// PNG 文件选择
document.getElementById('wechat-file-png')?.addEventListener('change', async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const charData = await extractCharacterFromPNG(file);
charData.file = file;
if (addContact(charData)) {
showToast('导入成功');
try {
await importCharacterToST(charData);
} catch (err) {
console.log('导入到酒馆失败(可忽略):', err.message);
}
// 同步角色卡内置世界书
const lorebookName = await syncCharacterBookToTavern(charData);
if (lorebookName) {
showToast(`角色书「${lorebookName}」已同步`);
}
showPage('wechat-main-content');
}
} catch (err) {
showToast(err.message, '⚠️');
}
this.value = '';
});
// JSON 文件选择
document.getElementById('wechat-file-json')?.addEventListener('change', async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const charData = await extractCharacterFromJSON(file);
charData.file = file;
if (addContact(charData)) {
showToast('导入成功');
try {
await importCharacterToST(charData);
} catch (err) {
console.log('导入到酒馆失败(可忽略):', err.message);
}
// 同步角色卡内置世界书
const lorebookName = await syncCharacterBookToTavern(charData);
if (lorebookName) {
showToast(`角色书「${lorebookName}」已同步`);
}
showPage('wechat-main-content');
}
} catch (err) {
showToast(err.message, '⚠️');
}
this.value = '';
});
// 导入多人卡
document.getElementById('wechat-import-multi-card')?.addEventListener('click', () => {
openMultiImportModal();
});
// 深色模式切换
document.getElementById('wechat-dark-toggle')?.addEventListener('click', toggleDarkMode);
// 自动注入提示
document.getElementById('wechat-auto-inject-toggle')?.addEventListener('click', () => {
const settings = getSettings();
settings.autoInjectPrompt = !settings.autoInjectPrompt;
document.getElementById('wechat-auto-inject-toggle')?.classList.toggle('on', settings.autoInjectPrompt);
// 展开/收起编辑区域
const contentDiv = document.getElementById('wechat-auto-inject-content');
if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.autoInjectPrompt);
}
requestSave();
if (settings.autoInjectPrompt) injectAuthorNote();
});
// 保存作者注释模板
document.getElementById('wechat-save-author-note')?.addEventListener('click', () => {
const settings = getSettings();
settings.authorNoteCustom = document.getElementById('wechat-author-note-content')?.value || '';
requestSave();
showToast('作者注释模板已保存');
});
// 哈基米破限开关
document.getElementById('wechat-hakimi-toggle')?.addEventListener('click', () => {
const settings = getSettings();
settings.hakimiBreakLimit = !settings.hakimiBreakLimit;
document.getElementById('wechat-hakimi-toggle')?.classList.toggle('on', settings.hakimiBreakLimit);
// 展开/收起编辑区域
const contentDiv = document.getElementById('wechat-hakimi-content');
if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.hakimiBreakLimit);
}
requestSave();
showToast(settings.hakimiBreakLimit ? '哈基米破限已开启' : '哈基米破限已关闭');
});
// 保存哈基米破限词
document.getElementById('wechat-save-hakimi')?.addEventListener('click', () => {
const settings = getSettings();
settings.hakimiCustomPrompt = document.getElementById('wechat-hakimi-prompt')?.value || '';
requestSave();
showToast('破限提示词已保存');
});
// ===== Meme表情包事件 =====
// 关闭面板
document.getElementById('wechat-meme-stickers-close')?.addEventListener('click', () => {
document.getElementById('wechat-meme-stickers-panel')?.classList.add('hidden');
});
// Meme开关
document.getElementById('wechat-meme-stickers-toggle')?.addEventListener('click', () => {
const settings = getSettings();
settings.memeStickersEnabled = !settings.memeStickersEnabled;
const toggle = document.getElementById('wechat-meme-stickers-toggle');
toggle?.classList.toggle('on', settings.memeStickersEnabled);
requestSave();
showToast(settings.memeStickersEnabled ? 'Meme表情包已启用' : 'Meme表情包已禁用');
});
// 添加表情包 - 弹出文本输入框
document.getElementById('wechat-add-meme-sticker')?.addEventListener('click', () => {
// 创建弹窗
const modal = document.createElement('div');
modal.className = 'wechat-modal';
modal.id = 'wechat-add-meme-modal';
modal.innerHTML = `
<div class="wechat-modal-content" style="max-width: 320px; background: #fff !important; color: #000 !important;">
<div class="wechat-modal-title" style="color: #000 !important;">添加表情包</div>
<div style="font-size: 12px; color: #666 !important; margin-bottom: 10px;">
输入猫箱格式的文件名,每行一个<br>
格式:名称+6位ID.扩展名<br>
例如是的主人yvrgdc.jpg
</div>
<textarea id="wechat-meme-input" placeholder="被揍了哭哭81x5zq.jpg&#10;开心跳舞abc123.gif&#10;..." style="width: 100%; height: 120px; box-sizing: border-box; font-size: 12px; color: #000 !important; background: #fff !important; padding: 10px; border-radius: 6px; border: 1px solid #ddd; font-family: monospace; resize: vertical;"></textarea>
<div style="display: flex; gap: 10px; margin-top: 12px; justify-content: flex-end;">
<button class="wechat-btn wechat-btn-secondary" id="wechat-meme-cancel" style="background: #f0f0f0 !important; color: #333 !important;">取消</button>
<button class="wechat-btn wechat-btn-primary" id="wechat-meme-confirm">添加</button>
</div>
</div>
`;
const phoneContainer = document.getElementById('wechat-phone');
if (phoneContainer) {
phoneContainer.appendChild(modal);
} else {
document.body.appendChild(modal);
}
// 聚焦输入框
document.getElementById('wechat-meme-input')?.focus();
// 取消按钮
document.getElementById('wechat-meme-cancel')?.addEventListener('click', () => modal.remove());
// 点击背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
// 确认添加
document.getElementById('wechat-meme-confirm')?.addEventListener('click', () => {
const input = document.getElementById('wechat-meme-input');
const text = input?.value?.trim();
if (!text) {
modal.remove();
return;
}
// 解析输入的每一行
const lines = text.split('\n').map(s => s.trim()).filter(s => s);
if (lines.length === 0) {
modal.remove();
return;
}
// 添加到表情包列表
const textarea = document.getElementById('wechat-meme-stickers-list');
if (textarea) {
const currentList = textarea.value.trim();
const updatedList = currentList ? currentList + '\n' + lines.join('\n') : lines.join('\n');
textarea.value = updatedList;
showToast(`已添加 ${lines.length} 个表情包`);
}
modal.remove();
});
});
// ===== 角色设置弹窗事件 =====
// 关闭按钮
document.getElementById('wechat-contact-settings-close')?.addEventListener('click', closeContactSettings);
// 保存按钮
document.getElementById('wechat-contact-settings-save')?.addEventListener('click', saveContactSettings);
// 更换头像按钮
document.getElementById('wechat-change-avatar-btn')?.addEventListener('click', () => {
const index = getCurrentEditingContactIndex();
if (index >= 0) changeContactAvatar(index);
});
// 独立API开关
document.getElementById('wechat-contact-custom-api-toggle')?.addEventListener('click', () => {
const toggle = document.getElementById('wechat-contact-custom-api-toggle');
const apiSettingsDiv = document.getElementById('wechat-contact-api-settings');
toggle?.classList.toggle('on');
const isOn = toggle?.classList.contains('on');
if (apiSettingsDiv) {
if (isOn) {
apiSettingsDiv.classList.remove('hidden');
apiSettingsDiv.style.display = 'flex';
} else {
apiSettingsDiv.classList.add('hidden');
apiSettingsDiv.style.display = 'none';
}
}
});
// 角色独立哈基米开关
document.getElementById('wechat-contact-hakimi-toggle')?.addEventListener('click', () => {
document.getElementById('wechat-contact-hakimi-toggle')?.classList.toggle('on');
});
// 角色独立API获取模型按钮
document.getElementById('wechat-contact-fetch-model')?.addEventListener('click', async () => {
const apiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim();
const apiKey = document.getElementById('wechat-contact-api-key')?.value?.trim();
const modelSelect = document.getElementById('wechat-contact-model-select');
const fetchBtn = document.getElementById('wechat-contact-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-contact-model-manual')?.addEventListener('click', () => {
const selectWrapper = document.getElementById('wechat-contact-model-select-wrapper');
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
const modelSelect = document.getElementById('wechat-contact-model-select');
const modelInput = document.getElementById('wechat-contact-model-input');
// 将当前选中的值复制到输入框
if (modelSelect?.value) {
modelInput.value = modelSelect.value;
}
selectWrapper.style.display = 'none';
inputWrapper.style.display = 'flex';
modelInput?.focus();
});
// 角色独立API返回按钮
document.getElementById('wechat-contact-model-back')?.addEventListener('click', () => {
const selectWrapper = document.getElementById('wechat-contact-model-select-wrapper');
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
const modelSelect = document.getElementById('wechat-contact-model-select');
const modelInput = document.getElementById('wechat-contact-model-input');
// 如果输入框有值,尝试在下拉列表中选中,或添加为新选项
const inputValue = modelInput?.value?.trim();
if (inputValue && modelSelect) {
const existingOption = Array.from(modelSelect.options).find(opt => opt.value === inputValue);
if (existingOption) {
modelSelect.value = inputValue;
} else {
// 添加为新选项并选中
const newOption = document.createElement('option');
newOption.value = inputValue;
newOption.textContent = inputValue;
modelSelect.appendChild(newOption);
modelSelect.value = inputValue;
}
}
selectWrapper.style.display = 'flex';
inputWrapper.style.display = 'none';
});
// 角色独立API测试连接按钮
document.getElementById('wechat-contact-test-api')?.addEventListener('click', async () => {
const apiUrl = document.getElementById('wechat-contact-api-url')?.value?.trim();
const apiKey = document.getElementById('wechat-contact-api-key')?.value?.trim();
// 优先从输入框获取,其次从下拉列表获取
const inputWrapper = document.getElementById('wechat-contact-model-input-wrapper');
const isManualMode = inputWrapper?.style.display === 'flex';
const model = isManualMode
? document.getElementById('wechat-contact-model-input')?.value?.trim()
: document.getElementById('wechat-contact-model-select')?.value?.trim();
const testBtn = document.getElementById('wechat-contact-test-api');
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: 8196
})
});
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-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', () => {
const settings = getSettings();
settings.groupAutoInjectPrompt = !settings.groupAutoInjectPrompt;
document.getElementById('wechat-group-inject-toggle')?.classList.toggle('on', settings.groupAutoInjectPrompt);
// 展开/收起编辑区域
const contentDiv = document.getElementById('wechat-group-inject-content');
if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.groupAutoInjectPrompt);
}
requestSave();
showToast(settings.groupAutoInjectPrompt ? '群聊提示词注入已开启' : '群聊提示词注入已关闭');
});
// 保存群聊作者注释
document.getElementById('wechat-save-group-note')?.addEventListener('click', () => {
const settings = getSettings();
settings.userGroupAuthorNote = document.getElementById('wechat-group-author-note')?.value || '';
requestSave();
showToast('群聊作者注释已保存');
});
// 聊天输入框发送消息(支持单聊和群聊)
const chatInput = document.getElementById('wechat-input');
// 更新发送按钮状态(全局可用)
window.updateSendButtonState = () => {
const moreBtn = document.querySelector('.wechat-chat-input-more');
const sendText = moreBtn?.querySelector('.wechat-input-send-text');
const moreIcon = moreBtn?.querySelector('.wechat-input-more-icon');
if (!sendText || !moreIcon) return;
const input = document.getElementById('wechat-input');
const hasText = input?.value?.trim();
if (hasText) {
sendText.style.display = 'inline-block';
moreIcon.style.display = 'none';
} else {
sendText.style.display = 'none';
moreIcon.style.display = 'inline-block';
}
};
// 监听输入变化
chatInput?.addEventListener('input', window.updateSendButtonState);
// 监听聚焦时也更新状态
chatInput?.addEventListener('focus', window.updateSendButtonState);
chatInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const text = chatInput.value?.trim();
if (!text) return;
// 调试日志
const messagesContainer = document.getElementById('wechat-chat-messages');
console.log('[可乐] Enter 键发送消息:', {
text: text.substring(0, 20),
isGroup: messagesContainer?.dataset?.isGroup,
groupIndex: messagesContainer?.dataset?.groupIndex,
isMultiPerson: messagesContainer?.dataset?.isMultiPerson,
isInGroupChatResult: isInGroupChat(),
isInMultiPersonChatResult: isInMultiPersonChat()
});
if (isInMultiPersonChat()) {
console.log('[可乐] 调用 sendMultiPersonMessage');
sendMultiPersonMessage(text);
} else if (isInGroupChat()) {
console.log('[可乐] 调用 sendGroupMessage');
sendGroupMessage(text);
} else {
console.log('[可乐] 调用 sendMessage (单聊)');
sendMessage(text);
}
// 发送后更新按钮状态
setTimeout(window.updateSendButtonState, 50);
}
});
// 聊天输入区按钮
document.querySelector('.wechat-chat-input-more')?.addEventListener('click', () => {
const text = chatInput?.value?.trim();
if (text) {
// 有文字时发送消息
if (isInMultiPersonChat()) {
sendMultiPersonMessage(text);
} else if (isInGroupChat()) {
sendGroupMessage(text);
} else {
sendMessage(text);
}
// 发送后更新按钮状态
setTimeout(window.updateSendButtonState, 50);
} else {
// 无文字时切换功能面板
toggleFuncPanel();
}
});
document.querySelector('.wechat-chat-input-voice')?.addEventListener('click', () => {
hideFuncPanel();
hideEmojiPanel();
showExpandVoice();
});
// 表情按钮
document.querySelector('.wechat-chat-input-emoji')?.addEventListener('click', () => {
hideFuncPanel();
toggleEmojiPanel();
});
initFuncPanel();
initEmojiPanel();
initChatBackground();
initMoments();
initRedPacketEvents();
initTransferEvents();
initGroupRedPacket();
initGiftEvents();
initCropper();
initHistoryEvents();
initMultiCharImport();
// 展开面板
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
document.getElementById('wechat-expand-send')?.addEventListener('click', sendExpandContent);
// 标签栏切换
document.querySelectorAll('.wechat-tab').forEach(tab => {
tab.addEventListener('click', function () {
document.querySelectorAll('.wechat-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === this.dataset.tab);
});
const tabName = this.dataset.tab;
if (tabName === 'me') {
showPage('wechat-me-page');
return;
}
if (tabName === 'discover') {
showPage('wechat-discover-page');
return;
}
if (tabName === 'chat') {
showPage('wechat-main-content');
document.getElementById('wechat-chat-tab-content')?.classList.remove('hidden');
document.getElementById('wechat-contacts-tab-content')?.classList.add('hidden');
refreshChatList();
return;
}
if (tabName === 'contacts') {
showPage('wechat-main-content');
document.getElementById('wechat-chat-tab-content')?.classList.add('hidden');
document.getElementById('wechat-contacts-tab-content')?.classList.remove('hidden');
refreshContactsList();
return;
}
showPage('wechat-main-content');
});
});
// 聊天列表项点击(支持单聊、群聊和多人群聊)
document.getElementById('wechat-chat-list')?.addEventListener('click', (e) => {
const chatItem = e.target.closest('.wechat-chat-item');
if (!chatItem) return;
// 检查是否是群聊
if (chatItem.classList.contains('wechat-chat-item-group')) {
const groupIndex = parseInt(chatItem.dataset.groupIndex);
if (!isNaN(groupIndex)) {
import('./group-chat.js').then(m => m.openGroupChat(groupIndex));
}
} else if (chatItem.classList.contains('wechat-chat-item-mp')) {
// 多人群聊
const mpIndex = parseInt(chatItem.dataset.mpIndex);
if (!isNaN(mpIndex)) {
import('./multi-person-chat.js').then(m => m.openMultiPersonChat(mpIndex));
}
} else {
// 单聊
const contactId = chatItem.dataset.contactId;
const index = parseInt(chatItem.dataset.index);
if (contactId) openChatByContactId(contactId, index);
}
});
// “我”页面菜单
document.getElementById('wechat-menu-favorites')?.addEventListener('click', () => {
showPage('wechat-favorites-page');
});
document.getElementById('wechat-menu-settings')?.addEventListener('click', () => {
showPage('wechat-settings-page');
});
document.getElementById('wechat-menu-service')?.addEventListener('click', () => {
showPage('wechat-service-page');
updateWalletAmountDisplay();
});
// 服务页返回
document.getElementById('wechat-service-back-btn')?.addEventListener('click', () => {
showPage('wechat-me-page');
});
// 服务页面 - 钱包/上下文开关面板
document.getElementById('wechat-service-wallet')?.addEventListener('click', () => {
document.getElementById('wechat-context-panel')?.classList.add('hidden');
document.getElementById('wechat-wallet-panel')?.classList.toggle('hidden');
});
document.getElementById('wechat-service-context')?.addEventListener('click', () => {
document.getElementById('wechat-wallet-panel')?.classList.add('hidden');
document.getElementById('wechat-context-panel')?.classList.toggle('hidden');
});
// 上下文开关变化
document.getElementById('wechat-context-enabled')?.addEventListener('change', (e) => {
const settings = getSettings();
settings.contextEnabled = e.target.checked;
requestSave();
syncContextEnabledUI(settings.contextEnabled);
});
// 上下文滑块变化
document.getElementById('wechat-context-slider')?.addEventListener('input', (e) => {
const settings = getSettings();
settings.contextLevel = parseInt(e.target.value);
requestSave();
document.getElementById('wechat-context-value').textContent = e.target.value;
});
// 标签容器事件委托(添加/删除)
document.getElementById('wechat-context-tags')?.addEventListener('click', (e) => {
const settings = getSettings();
if (e.target.classList.contains('wechat-tag-del-btn')) {
const index = parseInt(e.target.dataset.index);
if (Array.isArray(settings.contextTags) && index >= 0 && index < settings.contextTags.length) {
settings.contextTags.splice(index, 1);
requestSave();
refreshContextTags();
}
return;
}
if (e.target.classList.contains('wechat-tag-add-btn')) {
const tagName = prompt('输入标签名(如 content、scene:');
if (tagName && tagName.trim()) {
settings.contextTags = Array.isArray(settings.contextTags) ? settings.contextTags : [];
if (!settings.contextTags.includes(tagName.trim())) {
settings.contextTags.push(tagName.trim());
requestSave();
refreshContextTags();
}
}
}
});
// 钱包金额保存(滑出面板)
document.getElementById('wechat-wallet-save-slide')?.addEventListener('click', () => {
const input = document.getElementById('wechat-wallet-input-slide');
const amount = input?.value || '0.00';
const settings = getSettings();
settings.walletAmount = amount;
requestSave();
updateWalletAmountDisplay();
document.getElementById('wechat-wallet-panel')?.classList.add('hidden');
});
// 支付密码保存
document.getElementById('wechat-save-password-btn')?.addEventListener('click', () => {
const input = document.getElementById('wechat-new-password-input');
const password = input?.value || '';
// 验证是否为6位数字
if (!/^\d{6}$/.test(password)) {
showToast('请输入6位数字密码', 'info');
return;
}
const settings = getSettings();
settings.paymentPassword = password;
requestSave();
showToast('密码已保存', '✓');
document.getElementById('wechat-change-password-panel')?.classList.add('hidden');
input.value = '';
});
// 密码输入框只允许数字
document.getElementById('wechat-new-password-input')?.addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6);
});
// 总结模板保存
document.getElementById('wechat-summary-template-save')?.addEventListener('click', () => {
const input = document.getElementById('wechat-summary-template-input');
const template = input?.value || '';
const settings = getSettings();
settings.customSummaryTemplate = template;
requestSave();
showToast('模板已保存', '✓');
document.getElementById('wechat-summary-template-panel')?.classList.add('hidden');
});
// 总结模板恢复默认
document.getElementById('wechat-summary-template-reset')?.addEventListener('click', () => {
const input = document.getElementById('wechat-summary-template-input');
if (input) input.value = '';
const settings = getSettings();
settings.customSummaryTemplate = '';
requestSave();
showToast('已恢复默认模板', '✓');
});
// 总结 API 配置
document.getElementById('wechat-summary-key-toggle')?.addEventListener('click', () => {
const input = document.getElementById('wechat-summary-key');
if (input) input.type = input.type === 'password' ? 'text' : 'password';
});
document.getElementById('wechat-summary-fetch-models')?.addEventListener('click', async () => {
const statusEl = document.getElementById('wechat-summary-status');
const url = document.getElementById('wechat-summary-url')?.value?.trim();
const key = document.getElementById('wechat-summary-key')?.value?.trim();
const modelSelect = document.getElementById('wechat-summary-model');
if (!url || !key) {
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先填写 URL 和 Key`;
return;
}
if (statusEl) statusEl.textContent = '⏳ 正在获取模型列表...';
try {
const models = await fetchModelListFromApi(url, key);
if (models.length === 0) {
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 未找到可用模型`;
return;
}
if (modelSelect) {
modelSelect.innerHTML = '<option value="">-- 选择模型 --</option>' +
models.map(m => `<option value="${m}">${m}</option>`).join('');
}
const settings = getSettings();
settings.summaryModelList = models;
requestSave();
if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 获取到 ${models.length} 个模型`;
} catch (err) {
console.error('[可乐] 获取模型列表失败:', err);
if (statusEl) statusEl.textContent = `⚠️ 获取失败: ${err.message}`;
}
});
document.getElementById('wechat-summary-test')?.addEventListener('click', async () => {
const statusEl = document.getElementById('wechat-summary-status');
const url = document.getElementById('wechat-summary-url')?.value?.trim();
const key = document.getElementById('wechat-summary-key')?.value?.trim();
const model = document.getElementById('wechat-summary-model')?.value;
if (!url || !key) {
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先填写 URL 和 Key`;
return;
}
if (!model) {
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先选择模型`;
return;
}
if (statusEl) statusEl.textContent = '⏳ 正在测试连接...';
try {
const chatUrl = url.replace(/\/+$/, '') + '/chat/completions';
const response = await fetch(chatUrl, {
method: 'POST',
headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model, messages: [{ role: 'user', content: 'Hi' }], max_tokens: 5 })
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.error?.message || `HTTP ${response.status}`);
}
if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 连接成功!`;
} catch (err) {
console.error('[可乐] 测试连接失败:', err);
if (statusEl) statusEl.textContent = `⚠️ 连接失败: ${err.message}`;
}
});
document.getElementById('wechat-summary-save')?.addEventListener('click', () => {
const statusEl = document.getElementById('wechat-summary-status');
const urlInput = document.getElementById('wechat-summary-url');
const keyInput = document.getElementById('wechat-summary-key');
const modelSelect = document.getElementById('wechat-summary-model');
const settings = getSettings();
settings.summaryApiUrl = urlInput?.value?.trim() || '';
settings.summaryApiKey = keyInput?.value?.trim() || '';
settings.summarySelectedModel = modelSelect?.value || '';
requestSave();
if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 配置已保存`;
setTimeout(() => document.getElementById('wechat-summary-panel')?.classList.add('hidden'), 1500);
});
document.getElementById('wechat-summary-model')?.addEventListener('change', (e) => {
const settings = getSettings();
settings.summarySelectedModel = e.target.value;
requestSave();
});
document.getElementById('wechat-summary-execute')?.addEventListener('click', () => {
executeSummary();
});
document.getElementById('wechat-summary-rollback')?.addEventListener('click', () => {
rollbackSummary();
});
// 暴露恢复函数到全局,可在控制台调用: window.keleRecoverSummary()
window.keleRecoverSummary = recoverFromTavernWorldbook;
document.getElementById('wechat-summary-close')?.addEventListener('click', () => {
document.getElementById('wechat-summary-panel')?.classList.add('hidden');
});
// 总结面板 - 全选/取消全选
// 刷新按钮
document.getElementById('wechat-summary-refresh')?.addEventListener('click', () => {
refreshSummaryChatList();
});
document.getElementById('wechat-summary-select-all')?.addEventListener('click', () => {
selectAllSummaryChats(true);
});
document.getElementById('wechat-summary-deselect-all')?.addEventListener('click', () => {
selectAllSummaryChats(false);
});
// 发现页面 - 朋友圈点击
document.getElementById('wechat-discover-moments')?.addEventListener('click', () => {
openMomentsPage();
});
// 服务页面 - 服务项点击
document.querySelectorAll('.wechat-service-item').forEach(item => {
item.addEventListener('click', () => {
const service = item.dataset.service;
// 关闭其他面板
const allPanels = ['wechat-context-panel', 'wechat-wallet-panel', 'wechat-summary-panel', 'wechat-history-panel', 'wechat-logs-panel', 'wechat-meme-stickers-panel', 'wechat-change-password-panel', 'wechat-summary-template-panel'];
if (service === 'summary') {
allPanels.filter(p => p !== 'wechat-summary-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-summary-panel');
const isHidden = panel?.classList.contains('hidden');
panel?.classList.toggle('hidden');
if (isHidden) {
refreshSummaryChatList();
}
return;
}
if (service === 'history') {
allPanels.filter(p => p !== 'wechat-history-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-history-panel');
const isHidden = panel?.classList.contains('hidden');
panel?.classList.toggle('hidden');
if (isHidden) {
refreshHistoryList('all');
}
return;
}
if (service === 'logs') {
allPanels.filter(p => p !== 'wechat-logs-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-logs-panel');
const isHidden = panel?.classList.contains('hidden');
panel?.classList.toggle('hidden');
if (isHidden) {
refreshLogsList();
}
return;
}
if (service === 'meme-stickers') {
allPanels.filter(p => p !== 'wechat-meme-stickers-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-meme-stickers-panel');
panel?.classList.toggle('hidden');
return;
}
if (service === 'change-password') {
allPanels.filter(p => p !== 'wechat-change-password-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-change-password-panel');
panel?.classList.toggle('hidden');
return;
}
if (service === 'summary-template') {
allPanels.filter(p => p !== 'wechat-summary-template-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-summary-template-panel');
panel?.classList.toggle('hidden');
return;
}
if (service === 'voice-api') {
allPanels.filter(p => p !== 'wechat-voice-api-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-voice-api-panel');
panel?.classList.toggle('hidden');
return;
}
if (service === 'multi-char-table') {
// 切换角色表格区域的显示/隐藏
const section = document.getElementById('wechat-char-tables-section');
section?.classList.toggle('hidden');
return;
}
const label = item.querySelector('span')?.textContent || '该';
showToast(`"${label}" 功能开发中...`, 'info');
});
});
// 收藏页面 - 添加按钮根据当前标签显示不同功能
document.getElementById('wechat-favorites-add-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
// 获取当前选中的标签
const activeTab = document.querySelector('.wechat-favorites-tab.active');
const currentFilter = activeTab?.dataset.tab || 'all';
// 根据标签执行不同操作
if (currentFilter === 'user') {
// 用户标签:直接弹出添加用户设定
showAddPersonaPanel();
return;
}
if (currentFilter === 'character') {
// 角色卡标签:显示导入选项
let menu = document.getElementById('wechat-favorites-add-menu');
if (!menu) {
menu = document.createElement('div');
menu.id = 'wechat-favorites-add-menu';
menu.className = 'wechat-dropdown-menu';
menu.style.cssText = 'position: absolute; top: 45px; right: 10px; z-index: 100;';
document.getElementById('wechat-favorites-page')?.appendChild(menu);
}
menu.innerHTML = `
<div class="wechat-dropdown-item" id="wechat-add-menu-import-png">
<span><svg viewBox="0 0 24 24" width="18" height="18"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8.5" cy="8.5" r="1.5" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M21 15l-5-5L5 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span>
<span>导入 PNG</span>
</div>
<div class="wechat-dropdown-item" id="wechat-add-menu-import-json">
<span><svg viewBox="0 0 24 24" width="18" height="18"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span>
<span>导入 JSON</span>
</div>
`;
menu.classList.remove('hidden');
menu.querySelector('#wechat-add-menu-import-png')?.addEventListener('click', () => {
menu.classList.add('hidden');
document.getElementById('wechat-file-png')?.click();
});
menu.querySelector('#wechat-add-menu-import-json')?.addEventListener('click', () => {
menu.classList.add('hidden');
document.getElementById('wechat-file-json')?.click();
});
// 点击其他地方关闭菜单
const closeMenu = (ev) => {
if (!ev.target.closest('#wechat-favorites-add-menu') && !ev.target.closest('#wechat-favorites-add-btn')) {
menu.classList.add('hidden');
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => document.addEventListener('click', closeMenu), 0);
return;
}
if (currentFilter === 'lorebook') {
// 世界书标签:直接弹出添加世界书
showAddLorebookPanel();
return;
}
// 全部标签:显示完整菜单
let menu = document.getElementById('wechat-favorites-add-menu');
if (!menu) {
menu = document.createElement('div');
menu.id = 'wechat-favorites-add-menu';
menu.className = 'wechat-dropdown-menu';
menu.style.cssText = 'position: absolute; top: 45px; right: 10px; z-index: 100;';
document.getElementById('wechat-favorites-page')?.appendChild(menu);
}
menu.innerHTML = `
<div class="wechat-dropdown-item" id="wechat-add-menu-persona">
<span><svg viewBox="0 0 24 24" width="18" height="18"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span>
<span>添加用户设定</span>
</div>
<div class="wechat-dropdown-item" id="wechat-add-menu-import-png">
<span><svg viewBox="0 0 24 24" width="18" height="18"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8.5" cy="8.5" r="1.5" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M21 15l-5-5L5 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span>
<span>导入 PNG</span>
</div>
<div class="wechat-dropdown-item" id="wechat-add-menu-import-json">
<span><svg viewBox="0 0 24 24" width="18" height="18"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span>
<span>导入 JSON</span>
</div>
<div class="wechat-dropdown-item" id="wechat-add-menu-lorebook">
<span><svg viewBox="0 0 24 24" width="18" height="18"><path d="M4 19.5A2.5 2.5 0 016.5 17H20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span>
<span>添加世界书</span>
</div>
`;
menu.classList.remove('hidden');
menu.querySelector('#wechat-add-menu-lorebook')?.addEventListener('click', () => {
menu.classList.add('hidden');
showAddLorebookPanel();
});
menu.querySelector('#wechat-add-menu-persona')?.addEventListener('click', () => {
menu.classList.add('hidden');
showAddPersonaPanel();
});
menu.querySelector('#wechat-add-menu-import-png')?.addEventListener('click', () => {
menu.classList.add('hidden');
document.getElementById('wechat-file-png')?.click();
});
menu.querySelector('#wechat-add-menu-import-json')?.addEventListener('click', () => {
menu.classList.add('hidden');
document.getElementById('wechat-file-json')?.click();
});
// 点击其他地方关闭菜单
const closeMenu = (ev) => {
if (!ev.target.closest('#wechat-favorites-add-menu') && !ev.target.closest('#wechat-favorites-add-btn')) {
menu.classList.add('hidden');
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => document.addEventListener('click', closeMenu), 0);
});
document.getElementById('wechat-lorebook-cancel')?.addEventListener('click', () => {
document.getElementById('wechat-lorebook-modal')?.classList.add('hidden');
});
document.querySelectorAll('.wechat-favorites-tab').forEach(tab => {
tab.addEventListener('click', function () {
document.querySelectorAll('.wechat-favorites-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
refreshFavoritesList(this.dataset.tab);
});
});
// 清空联系人
document.getElementById('wechat-clear-contacts')?.addEventListener('click', () => {
if (!confirm('确定要清空所有联系人吗?')) return;
const settings = getSettings();
settings.contacts = [];
requestSave();
refreshContactsList();
showToast('已清空所有联系人');
});
// 用户头像更换
document.getElementById('wechat-me-avatar')?.addEventListener('click', () => {
document.getElementById('wechat-user-avatar-input')?.click();
});
document.getElementById('wechat-user-avatar-input')?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const reader = new FileReader();
reader.onload = function (event) {
const settings = getSettings();
settings.userAvatar = event.target.result;
requestSave();
updateMePageInfo();
showToast('头像已更换');
};
reader.readAsDataURL(file);
} catch (err) {
console.error('[可乐] 更换头像失败:', err);
showToast('更换头像失败: ' + err.message, '⚠️');
}
e.target.value = '';
});
// API 配置:密钥可见性
document.getElementById('wechat-toggle-key-visibility')?.addEventListener('click', () => {
const keyInput = document.getElementById('wechat-api-key');
const eyeBtn = document.getElementById('wechat-toggle-key-visibility');
if (!keyInput || !eyeBtn) return;
if (keyInput.type === 'password') {
keyInput.type = 'text';
eyeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24M1 1l22 22" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>';
} else {
keyInput.type = 'password';
eyeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>';
}
});
// 保存 API 配置
document.getElementById('wechat-save-api')?.addEventListener('click', () => {
const apiUrl = document.getElementById('wechat-api-url')?.value.trim() || '';
const apiKey = document.getElementById('wechat-api-key')?.value.trim() || '';
const selectedModel = document.getElementById('wechat-model-select')?.value || '';
const settings = getSettings();
settings.apiUrl = apiUrl;
settings.apiKey = apiKey;
settings.selectedModel = selectedModel;
requestSave();
showToast('API 配置已保存');
});
// 刷新模型列表
document.getElementById('wechat-refresh-models')?.addEventListener('click', () => {
refreshModelSelect();
});
// 模型选择变化(支持手动输入和从列表选择)
const modelInput = document.getElementById('wechat-model-select');
if (modelInput) {
modelInput.addEventListener('change', (e) => {
const settings = getSettings();
settings.selectedModel = e.target.value.trim();
requestSave();
});
modelInput.addEventListener('input', (e) => {
const settings = getSettings();
settings.selectedModel = e.target.value.trim();
requestSave();
});
}
// 测试 API 连接
document.getElementById('wechat-test-api')?.addEventListener('click', async () => {
const apiUrl = document.getElementById('wechat-api-url')?.value.trim() || '';
const apiKey = document.getElementById('wechat-api-key')?.value.trim() || '';
if (!apiUrl) {
showToast('请先填写 API 地址', 'info');
return;
}
const testBtn = document.getElementById('wechat-test-api');
const originalText = testBtn?.textContent;
if (testBtn) {
testBtn.textContent = '测试中...';
testBtn.disabled = true;
}
try {
await fetchModelListFromApi(apiUrl, apiKey);
showToast('连接成功');
} catch (err) {
showToast('连接失败:' + err.message, '⚠️');
} finally {
if (testBtn) {
testBtn.textContent = originalText;
testBtn.disabled = false;
}
}
});
// 自己填模型按钮 - 单聊
document.getElementById('wechat-manual-model')?.addEventListener('click', () => {
const modelName = prompt('请输入模型名称:');
if (modelName && modelName.trim()) {
const select = document.getElementById('wechat-model-select');
if (select) {
// 添加一个新选项并选中
const option = document.createElement('option');
option.value = modelName.trim();
option.textContent = modelName.trim();
option.selected = true;
select.appendChild(option);
const settings = getSettings();
settings.selectedModel = modelName.trim();
requestSave();
showToast('模型已设置');
}
}
});
// ===== 群聊 API 配置事件 =====
// 群聊密钥可见性
document.getElementById('wechat-toggle-group-key-visibility')?.addEventListener('click', () => {
const keyInput = document.getElementById('wechat-group-api-key');
const eyeBtn = document.getElementById('wechat-toggle-group-key-visibility');
if (!keyInput || !eyeBtn) return;
if (keyInput.type === 'password') {
keyInput.type = 'text';
eyeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24M1 1l22 22" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>';
} else {
keyInput.type = 'password';
eyeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>';
}
});
// 群聊获取模型列表
document.getElementById('wechat-group-refresh-models')?.addEventListener('click', async () => {
const settings = getSettings();
const apiUrl = document.getElementById('wechat-group-api-url')?.value?.trim() || settings.groupApiUrl || '';
const apiKey = document.getElementById('wechat-group-api-key')?.value?.trim() || settings.groupApiKey || '';
const refreshBtn = document.getElementById('wechat-group-refresh-models');
const select = document.getElementById('wechat-group-model-select');
if (!apiUrl) {
showToast('请先填写群聊 API 地址', 'info');
return;
}
const originalText = refreshBtn?.textContent;
if (refreshBtn) {
refreshBtn.textContent = '加载中...';
refreshBtn.disabled = true;
}
try {
const modelIds = await fetchModelListFromApi(apiUrl, apiKey);
// 更新 select 选项
if (select) {
select.innerHTML = '<option value="">-- 选择模型 --</option>' +
modelIds.map(id => `<option value="${id}">${id}</option>`).join('');
}
settings.groupModelList = modelIds;
requestSave();
showToast(`获取到 ${modelIds.length} 个模型`);
} catch (err) {
console.error('[可乐] 获取群聊模型列表失败:', err);
showToast('获取失败,请手动输入模型名', '⚠️');
} finally {
if (refreshBtn) {
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
}
}
});
// 群聊自己填模型
document.getElementById('wechat-group-manual-model')?.addEventListener('click', () => {
const modelName = prompt('请输入群聊模型名称:');
if (modelName && modelName.trim()) {
const select = document.getElementById('wechat-group-model-select');
if (select) {
// 添加一个新选项并选中
const option = document.createElement('option');
option.value = modelName.trim();
option.textContent = modelName.trim();
option.selected = true;
select.appendChild(option);
const settings = getSettings();
settings.groupSelectedModel = modelName.trim();
requestSave();
showToast('群聊模型已设置');
}
}
});
// 群聊测试连接
document.getElementById('wechat-group-test-api')?.addEventListener('click', async () => {
const apiUrl = document.getElementById('wechat-group-api-url')?.value.trim() || '';
const apiKey = document.getElementById('wechat-group-api-key')?.value.trim() || '';
if (!apiUrl) {
showToast('请先填写群聊 API 地址', 'info');
return;
}
const testBtn = document.getElementById('wechat-group-test-api');
const originalText = testBtn?.textContent;
if (testBtn) {
testBtn.textContent = '测试中...';
testBtn.disabled = true;
}
try {
await fetchModelListFromApi(apiUrl, apiKey);
showToast('群聊 API 连接成功');
} catch (err) {
showToast('连接失败:' + err.message, '⚠️');
} finally {
if (testBtn) {
testBtn.textContent = originalText;
testBtn.disabled = false;
}
}
});
// 保存群聊 API 配置
document.getElementById('wechat-group-save-api')?.addEventListener('click', () => {
const apiUrl = document.getElementById('wechat-group-api-url')?.value.trim() || '';
const apiKey = document.getElementById('wechat-group-api-key')?.value.trim() || '';
const selectedModel = document.getElementById('wechat-group-model-select')?.value || '';
const settings = getSettings();
settings.groupApiUrl = apiUrl;
settings.groupApiKey = apiKey;
settings.groupSelectedModel = selectedModel;
requestSave();
showToast('群聊 API 配置已保存');
});
// 群聊模型选择变化
const groupModelInput = document.getElementById('wechat-group-model-select');
if (groupModelInput) {
groupModelInput.addEventListener('change', (e) => {
const settings = getSettings();
settings.groupSelectedModel = e.target.value.trim();
requestSave();
});
groupModelInput.addEventListener('input', (e) => {
const settings = getSettings();
settings.groupSelectedModel = e.target.value.trim();
requestSave();
});
}
// 总结 API - 自己填模型
document.getElementById('wechat-summary-manual-model')?.addEventListener('click', () => {
const modelName = prompt('请输入总结模型名称:');
if (modelName && modelName.trim()) {
const select = document.getElementById('wechat-summary-model');
if (select) {
// 添加一个新选项并选中
const option = document.createElement('option');
option.value = modelName.trim();
option.textContent = modelName.trim();
option.selected = true;
select.appendChild(option);
const settings = getSettings();
settings.summarySelectedModel = modelName.trim();
requestSave();
showToast('总结模型已设置');
}
}
});
// ===== 历史回顾面板事件 =====
document.getElementById('wechat-history-close')?.addEventListener('click', () => {
document.getElementById('wechat-history-panel')?.classList.add('hidden');
});
// 历史回顾标签切换
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.wechat-history-tab').forEach(t => {
t.classList.remove('active', 'wechat-btn-primary');
});
tab.classList.add('active', 'wechat-btn-primary');
refreshHistoryList(tab.dataset.tab);
});
});
// ===== 日志面板事件 =====
document.getElementById('wechat-logs-close')?.addEventListener('click', () => {
document.getElementById('wechat-logs-panel')?.classList.add('hidden');
});
document.getElementById('wechat-logs-clear')?.addEventListener('click', () => {
if (confirm('确定清空所有日志?')) {
clearErrorLogs();
refreshLogsList();
showToast('日志已清空');
}
});
// ===== 语音 API 面板事件 =====
// 关闭按钮
document.getElementById('wechat-voice-api-close')?.addEventListener('click', () => {
document.getElementById('wechat-voice-api-panel')?.classList.add('hidden');
});
// STT 密钥可见性切换
document.getElementById('wechat-stt-key-toggle')?.addEventListener('click', () => {
const keyInput = document.getElementById('wechat-stt-api-key');
if (keyInput) {
keyInput.type = keyInput.type === 'password' ? 'text' : 'password';
}
});
// TTS 密钥可见性切换
document.getElementById('wechat-tts-key-toggle')?.addEventListener('click', () => {
const keyInput = document.getElementById('wechat-tts-api-key');
if (keyInput) {
keyInput.type = keyInput.type === 'password' ? 'text' : 'password';
}
});
// 测试 STT API
document.getElementById('wechat-voice-api-test-stt')?.addEventListener('click', async () => {
const btn = document.getElementById('wechat-voice-api-test-stt');
const originalText = btn?.textContent;
if (btn) {
btn.textContent = '测试中...';
btn.disabled = true;
}
try {
// 先保存当前配置
const settings = getSettings();
settings.sttApiUrl = document.getElementById('wechat-stt-api-url')?.value?.trim() || '';
settings.sttApiKey = document.getElementById('wechat-stt-api-key')?.value?.trim() || '';
settings.sttModel = document.getElementById('wechat-stt-model')?.value?.trim() || '';
await testSttApi();
showToast('STT 连接成功!', '✓');
} catch (err) {
console.error('[可乐] STT 测试失败:', err);
showToast('STT 测试失败: ' + err.message, '⚠️');
} finally {
if (btn) {
btn.textContent = originalText;
btn.disabled = false;
}
}
});
// 测试 TTS API
document.getElementById('wechat-voice-api-test-tts')?.addEventListener('click', async () => {
const btn = document.getElementById('wechat-voice-api-test-tts');
const originalText = btn?.textContent;
if (btn) {
btn.textContent = '测试中...';
btn.disabled = true;
}
try {
// 先保存当前配置
const settings = getSettings();
settings.ttsApiUrl = document.getElementById('wechat-tts-api-url')?.value?.trim() || '';
settings.ttsApiKey = document.getElementById('wechat-tts-api-key')?.value?.trim() || '';
settings.ttsModel = document.getElementById('wechat-tts-model')?.value?.trim() || '';
settings.ttsVoice = document.getElementById('wechat-tts-voice')?.value?.trim() || '';
settings.ttsSpeed = parseFloat(document.getElementById('wechat-tts-speed')?.value) || 1;
settings.ttsEmotion = document.getElementById('wechat-tts-emotion')?.value?.trim() || '默认';
settings.ttsProxyUrl = document.getElementById('wechat-tts-proxy-url')?.value?.trim() || '';
const audioBlob = await testTtsApi();
console.log('[可乐] TTS 测试返回音频:', {
size: audioBlob?.size,
type: audioBlob?.type
});
if (!audioBlob || audioBlob.size < 100) {
throw new Error('返回的音频数据无效');
}
// 播放测试音频
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.volume = 1.0;
await new Promise((resolve, reject) => {
audio.onended = () => {
URL.revokeObjectURL(audioUrl);
resolve();
};
audio.onerror = (e) => {
URL.revokeObjectURL(audioUrl);
reject(new Error('音频播放失败'));
};
audio.play().then(() => {
console.log('[可乐] 测试音频开始播放');
}).catch(reject);
});
showToast('TTS 测试成功!正在播放', '✓');
} catch (err) {
console.error('[可乐] TTS 测试失败:', err);
showToast('TTS 测试失败: ' + err.message, '⚠️');
} finally {
if (btn) {
btn.textContent = originalText;
btn.disabled = false;
}
}
});
// 保存语音 API 配置
document.getElementById('wechat-voice-api-save')?.addEventListener('click', () => {
const settings = getSettings();
// STT 配置
settings.sttApiUrl = document.getElementById('wechat-stt-api-url')?.value?.trim() || '';
settings.sttApiKey = document.getElementById('wechat-stt-api-key')?.value?.trim() || '';
settings.sttModel = document.getElementById('wechat-stt-model')?.value?.trim() || '';
// TTS 配置
settings.ttsApiUrl = document.getElementById('wechat-tts-api-url')?.value?.trim() || '';
settings.ttsApiKey = document.getElementById('wechat-tts-api-key')?.value?.trim() || '';
settings.ttsModel = document.getElementById('wechat-tts-model')?.value?.trim() || '';
settings.ttsVoice = document.getElementById('wechat-tts-voice')?.value?.trim() || '';
settings.ttsSpeed = parseFloat(document.getElementById('wechat-tts-speed')?.value) || 1;
settings.ttsEmotion = document.getElementById('wechat-tts-emotion')?.value?.trim() || '默认';
settings.ttsProxyUrl = document.getElementById('wechat-tts-proxy-url')?.value?.trim() || '';
requestSave();
showToast('语音 API 配置已保存', '✓');
});
// 绑定联系人点击
refreshContactsList();
}
function init() {
console.log('[可乐] init() 开始');
loadSettings();
console.log('[可乐] loadSettings 调用完成,开始 getSettings');
const settings = getSettings();
console.log('[可乐] getSettings 完成,开始 seedDefaultUserPersonaFromST');
if (seedDefaultUserPersonaFromST(settings)) {
requestSave();
}
console.log('[可乐] seedDefaultUserPersonaFromST 完成,开始 generatePhoneHTML');
const phoneHTML = generatePhoneHTML();
console.log('[可乐] generatePhoneHTML 完成');
document.body.insertAdjacentHTML('beforeend', phoneHTML);
setupPhoneAutoCentering();
setupPhoneDrag();
bindEvents();
// 初始化发送按钮状态
window.updateSendButtonState?.();
// 初始化底部导航栏红点
updateTabBadge();
restoreModelSelect();
restoreGroupModelSelect();
// 同步上下文面板初始 UI
syncContextEnabledUI(settings.contextEnabled);
refreshContextTags();
updateWalletAmountDisplay();
if (settings.autoInjectPrompt) {
injectAuthorNote();
}
setupMessageObserver();
addExtensionButton();
// 初始化错误捕获
initErrorCapture();
// 初始化页面卸载保存
setupUnloadSave();
setInterval(() => {
const phone = document.getElementById('wechat-phone');
if (!phone || phone.classList.contains('hidden')) return;
const timeEl = document.querySelector('.wechat-statusbar-time');
if (timeEl) timeEl.textContent = getCurrentTime();
}, 60000);
// 首次可见时居中
centerPhoneInViewport({ force: true });
// 初始化悬浮球
createFloatingBall();
// 根据设置决定是否显示
if (settings.floatingBallEnabled === false) {
hideFloatingBall();
}
updateFloatingBallMenuText(settings.floatingBallEnabled !== false);
console.log('✅ 可乐不加冰 已加载');
}
if (typeof jQuery === 'function') {
jQuery(() => setTimeout(init, 500));
} else {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 500), { once: true });
}