mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 03:35:50 +00:00
2702 lines
100 KiB
JavaScript
2702 lines
100 KiB
JavaScript
/**
|
||
* 可乐不加冰 - 主入口(模块化)
|
||
*/
|
||
|
||
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 } 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 { 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';
|
||
|
||
// ========== 历史记录功能 ==========
|
||
let currentHistoryTab = 'listen';
|
||
let currentHistoryContactIndex = -1;
|
||
|
||
function openHistoryPage(contactIndex) {
|
||
const settings = getSettings();
|
||
const contact = settings.contacts?.[contactIndex];
|
||
if (!contact) return;
|
||
|
||
currentHistoryContactIndex = contactIndex;
|
||
currentHistoryTab = 'listen';
|
||
|
||
const page = document.getElementById('wechat-history-page');
|
||
if (page) {
|
||
page.classList.remove('hidden');
|
||
// 重置标签状态
|
||
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.tab === 'listen');
|
||
});
|
||
renderHistoryContent(contact, 'listen');
|
||
}
|
||
}
|
||
|
||
function closeHistoryPage() {
|
||
const page = document.getElementById('wechat-history-page');
|
||
if (page) {
|
||
page.classList.add('hidden');
|
||
}
|
||
currentHistoryContactIndex = -1;
|
||
}
|
||
|
||
function deleteHistoryRecord(tabType, index) {
|
||
const settings = getSettings();
|
||
const contact = settings.contacts?.[currentHistoryContactIndex];
|
||
if (!contact) return;
|
||
|
||
if (tabType === 'listen') {
|
||
if (contact.listenHistory && contact.listenHistory[index]) {
|
||
contact.listenHistory.splice(index, 1);
|
||
}
|
||
} else if (tabType === 'voice' || tabType === 'video') {
|
||
// 从 callHistory 中找到并删除对应类型的记录
|
||
const callHistory = contact.callHistory || [];
|
||
const typeRecords = callHistory.filter(r => r.type === tabType);
|
||
if (typeRecords[index]) {
|
||
const originalIndex = callHistory.indexOf(typeRecords[index]);
|
||
if (originalIndex >= 0) {
|
||
contact.callHistory.splice(originalIndex, 1);
|
||
}
|
||
}
|
||
} else if (tabType === 'toy') {
|
||
if (contact.toyHistory && contact.toyHistory[index]) {
|
||
contact.toyHistory.splice(index, 1);
|
||
}
|
||
}
|
||
|
||
requestSave();
|
||
renderHistoryContent(contact, tabType);
|
||
}
|
||
|
||
function switchHistoryTab(tabType) {
|
||
currentHistoryTab = tabType;
|
||
document.querySelectorAll('.wechat-history-tab').forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.tab === tabType);
|
||
});
|
||
|
||
const settings = getSettings();
|
||
const contact = settings.contacts?.[currentHistoryContactIndex];
|
||
if (contact) {
|
||
renderHistoryContent(contact, tabType);
|
||
}
|
||
}
|
||
|
||
function renderHistoryContent(contact, tabType) {
|
||
const contentEl = document.getElementById('wechat-history-content');
|
||
if (!contentEl) return;
|
||
|
||
// 心动瞬间使用专门的渲染函数
|
||
if (tabType === 'toy') {
|
||
renderToyHistory(contact);
|
||
// 绑定心动瞬间的删除按钮事件
|
||
contentEl.querySelectorAll('.wechat-history-delete-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const index = parseInt(btn.dataset.index);
|
||
deleteHistoryRecord('toy', index);
|
||
});
|
||
});
|
||
// 绑定标签内的叉叉按钮事件
|
||
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 {
|
||
// 从 callHistory 中筛选 voice 或 video
|
||
const callHistory = contact.callHistory || [];
|
||
records = callHistory.filter(r => r.type === tabType);
|
||
}
|
||
|
||
if (records.length === 0) {
|
||
const emptyText = tabType === 'listen' ? '暂无一起听记录' :
|
||
tabType === 'voice' ? '暂无语音通话记录' : '暂无视频通话记录';
|
||
contentEl.innerHTML = `
|
||
<div class="wechat-history-empty">
|
||
<div class="wechat-history-empty-icon">📭</div>
|
||
<div>${emptyText}</div>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 按时间倒序排列
|
||
const sortedRecords = [...records].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
||
|
||
let html = '';
|
||
for (let i = 0; i < sortedRecords.length; i++) {
|
||
const record = sortedRecords[i];
|
||
const time = record.time || '未知时间';
|
||
const duration = record.duration || '';
|
||
const messages = record.messages || [];
|
||
const originalIndex = records.indexOf(record);
|
||
|
||
html += `<div class="wechat-history-card" data-tab="${tabType}" data-index="${originalIndex}">`;
|
||
html += `<div class="wechat-history-card-header">`;
|
||
html += `<span class="wechat-history-card-time">${escapeHtml(time)}</span>`;
|
||
html += `<div class="wechat-history-card-actions">`;
|
||
html += `<button class="wechat-history-delete-btn" data-tab="${tabType}" data-index="${originalIndex}" 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);
|
||
deleteHistoryRecord(tab, index);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 渲染语音回放内容
|
||
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');
|
||
|
||
// 恢复原始位置或居中
|
||
if (settings.phoneOriginalPosition) {
|
||
phone.style.left = settings.phoneOriginalPosition.left;
|
||
phone.style.top = settings.phoneOriginalPosition.top;
|
||
}
|
||
|
||
// 恢复时根据设置显示悬浮球
|
||
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);
|
||
// 清除群聊标记
|
||
const messagesContainer = document.getElementById('wechat-chat-messages');
|
||
if (messagesContainer) {
|
||
messagesContainer.dataset.isGroup = 'false';
|
||
messagesContainer.dataset.groupIndex = '-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');
|
||
});
|
||
|
||
// 查看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-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 开心跳舞abc123.gif ..." 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-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,
|
||
isInGroupChatResult: isInGroupChat()
|
||
});
|
||
|
||
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 (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();
|
||
|
||
// 展开面板
|
||
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 {
|
||
// 单聊
|
||
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;
|
||
}
|
||
|
||
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() {
|
||
loadSettings();
|
||
const settings = getSettings();
|
||
if (seedDefaultUserPersonaFromST(settings)) {
|
||
requestSave();
|
||
}
|
||
|
||
const phoneHTML = 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 });
|
||
}
|
||
|