Compare commits

...

2 Commits

Author SHA1 Message Date
Cola-Echo
f07e0914f0 Add files via upload 2025-12-31 15:04:18 +08:00
Cola-Echo
5068b46702 Add files via upload 2025-12-31 14:11:05 +08:00
3 changed files with 269 additions and 6 deletions

View File

@@ -275,3 +275,51 @@ export async function getStorageStats() {
};
});
}
/**
* 获取所有语音记录,按联系人分组
* @returns {Promise<Object>} { contactIndex: { count, totalDuration } }
*/
export async function getAllVoiceRecordingsGroupedByContact() {
await initAudioDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => {
const records = request.result;
const grouped = {};
records.forEach(record => {
const idx = record.contactIndex;
if (!grouped[idx]) {
grouped[idx] = { count: 0, totalDuration: 0 };
}
grouped[idx].count++;
grouped[idx].totalDuration += record.duration || 0;
});
resolve(grouped);
};
request.onerror = () => {
reject(request.error);
};
});
}
/**
* 删除指定联系人的所有语音记录
* @param {number} contactIndex - 联系人索引
* @returns {Promise<number>} 删除的记录数量
*/
export async function deleteVoiceRecordingsByContact(contactIndex) {
const records = await getVoiceRecordingsByContact(contactIndex);
for (const record of records) {
await deleteVoiceRecording(record.id);
}
console.log(`[可乐] 已删除联系人 ${contactIndex}${records.length} 条语音记录`);
return records.length;
}

223
main.js
View File

@@ -38,7 +38,7 @@ import { initGiftEvents } from './gift.js';
import { initCropper } from './cropper.js';
import { createFloatingBall, showFloatingBall, hideFloatingBall } from './floating-ball.js';
import { testSttApi, testTtsApi } from './voice-api.js';
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording } from './audio-storage.js';
import { getVoiceRecordingsByContact, deleteVoiceRecording, playVoiceRecording, getAllVoiceRecordingsGroupedByContact, deleteVoiceRecordingsByContact } from './audio-storage.js';
// ========== 历史记录功能 ==========
let currentHistoryTab = 'listen';
@@ -513,6 +513,211 @@ function updateFloatingBallMenuText(enabled) {
}
}
// 清除孤立缓存(已删除联系人/群聊的总结历史和语音记录)
async function clearOrphanedCache() {
const settings = getSettings();
const contacts = settings.contacts || [];
const groupChats = settings.groupChats || [];
const lorebooks = settings.selectedLorebooks || [];
// 获取当前有效的联系人ID和名称
const validContactIds = new Set(contacts.map(c => c.id).filter(id => id));
const validContactNames = new Set(contacts.map(c => c.name).filter(n => n));
const validGroupNames = new Set(groupChats.map(g => g.name).filter(n => n));
const validContactIndexes = new Set(contacts.map((_, idx) => idx));
// 查找孤立的总结世界书
const orphanedSummaries = [];
lorebooks.forEach((lb, idx) => {
const isSummaryBook = lb.fromSummary === true ||
(lb.name?.startsWith('【可乐】和') && lb.name?.endsWith('的聊天'));
if (isSummaryBook) {
const nameMatch = lb.name?.match(/^【可乐】和(.+)的聊天$/);
const linkedName = nameMatch ? nameMatch[1] : null;
const contactExists = linkedName && validContactNames.has(linkedName);
const groupExists = linkedName && validGroupNames.has(linkedName);
const linkedById = lb.characterId && validContactIds.has(lb.characterId);
if (!contactExists && !groupExists && !linkedById) {
const cupCount = lb.entries?.length || 0;
orphanedSummaries.push({
type: 'summary',
name: lb.name,
linkedName: linkedName || '未知',
index: idx,
cupCount
});
}
}
});
// 查找孤立的语音记录
const orphanedVoices = [];
try {
const voiceGroups = await getAllVoiceRecordingsGroupedByContact();
for (const [contactIdxStr, data] of Object.entries(voiceGroups)) {
const contactIdx = parseInt(contactIdxStr);
// 如果索引超出当前联系人范围,则为孤立数据
if (!validContactIndexes.has(contactIdx)) {
orphanedVoices.push({
type: 'voice',
contactIndex: contactIdx,
count: data.count,
totalDuration: data.totalDuration
});
}
}
} catch (err) {
console.warn('[可乐] 获取语音记录失败:', err);
}
// 如果没有孤立数据
if (orphanedSummaries.length === 0 && orphanedVoices.length === 0) {
showToast('没有发现需要清理的缓存数据');
return;
}
// 显示选择弹窗
showCacheCleanupModal(orphanedSummaries, orphanedVoices);
}
// 显示缓存清理选择弹窗
function showCacheCleanupModal(orphanedSummaries, orphanedVoices) {
document.getElementById('wechat-cache-cleanup-modal')?.remove();
const hasSummaries = orphanedSummaries.length > 0;
const hasVoices = orphanedVoices.length > 0;
const modal = document.createElement('div');
modal.className = 'wechat-modal';
modal.id = 'wechat-cache-cleanup-modal';
modal.innerHTML = `
<div class="wechat-modal-content" style="position: relative; max-width: 380px; max-height: 80vh; margin: auto;">
<button class="wechat-modal-close-x" id="wechat-cache-modal-close">×</button>
<div class="wechat-modal-title">清除缓存</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary); margin-bottom: 12px; padding: 0 4px;">
勾选要清除的项目:
</div>
<div style="max-height: 45vh; overflow-y: auto; margin-bottom: 15px;">
${hasSummaries ? `
<div style="font-size: 12px; color: var(--wechat-text-secondary); padding: 8px; background: var(--wechat-bg-secondary); border-radius: 4px; margin-bottom: 8px;">
📝 总结历史(已删除联系人/群聊)
</div>
${orphanedSummaries.map(item => `
<label class="wechat-cache-item" style="display: flex; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
<input type="checkbox" class="wechat-cache-checkbox" data-type="summary" data-index="${item.index}" checked style="margin-right: 10px; width: 18px; height: 18px;">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${item.linkedName}</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${item.cupCount} 杯总结记录</div>
</div>
</label>
`).join('')}
` : ''}
${hasVoices ? `
<div style="font-size: 12px; color: var(--wechat-text-secondary); padding: 8px; background: var(--wechat-bg-secondary); border-radius: 4px; margin-bottom: 8px; ${hasSummaries ? 'margin-top: 12px;' : ''}">
🎙️ 语音通话记录(孤立数据)
</div>
${orphanedVoices.map(item => `
<label class="wechat-cache-item" style="display: flex; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--wechat-border); cursor: pointer;">
<input type="checkbox" class="wechat-cache-checkbox" data-type="voice" data-contact-index="${item.contactIndex}" checked style="margin-right: 10px; width: 18px; height: 18px;">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 14px; font-weight: 500;">联系人 #${item.contactIndex}</div>
<div style="font-size: 12px; color: var(--wechat-text-secondary);">${item.count} 条语音,共 ${Math.round(item.totalDuration)} 秒</div>
</div>
</label>
`).join('')}
` : ''}
</div>
<div style="display: flex; gap: 10px; justify-content: space-between; align-items: center;">
<label style="display: flex; align-items: center; font-size: 13px; color: var(--wechat-text-secondary); cursor: pointer;">
<input type="checkbox" id="wechat-cache-select-all" checked style="margin-right: 6px;">
全选
</label>
<div style="display: flex; gap: 10px;">
<button class="wechat-btn wechat-btn-secondary" id="wechat-cache-cancel">取消</button>
<button class="wechat-btn wechat-btn-danger" id="wechat-cache-confirm">清除选中</button>
</div>
</div>
</div>
`;
const phoneContainer = document.querySelector('.wechat-phone') || document.body;
phoneContainer.appendChild(modal);
// 关闭按钮
modal.querySelector('#wechat-cache-modal-close').addEventListener('click', () => modal.remove());
modal.querySelector('#wechat-cache-cancel').addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
// 全选/取消全选
const selectAllCheckbox = modal.querySelector('#wechat-cache-select-all');
const itemCheckboxes = modal.querySelectorAll('.wechat-cache-checkbox');
selectAllCheckbox.addEventListener('change', () => {
itemCheckboxes.forEach(cb => cb.checked = selectAllCheckbox.checked);
});
itemCheckboxes.forEach(cb => {
cb.addEventListener('change', () => {
const allChecked = Array.from(itemCheckboxes).every(c => c.checked);
const noneChecked = Array.from(itemCheckboxes).every(c => !c.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = !allChecked && !noneChecked;
});
});
// 确认清除
modal.querySelector('#wechat-cache-confirm').addEventListener('click', async () => {
const selectedSummaryIndexes = new Set();
const selectedVoiceIndexes = [];
itemCheckboxes.forEach(cb => {
if (cb.checked) {
if (cb.dataset.type === 'summary') {
selectedSummaryIndexes.add(parseInt(cb.dataset.index));
} else if (cb.dataset.type === 'voice') {
selectedVoiceIndexes.push(parseInt(cb.dataset.contactIndex));
}
}
});
if (selectedSummaryIndexes.size === 0 && selectedVoiceIndexes.length === 0) {
showToast('请至少选择一项');
return;
}
let clearedCount = 0;
// 清除总结缓存
if (selectedSummaryIndexes.size > 0) {
const settings = getSettings();
settings.selectedLorebooks = settings.selectedLorebooks.filter((_, idx) => !selectedSummaryIndexes.has(idx));
requestSave();
clearedCount += selectedSummaryIndexes.size;
}
// 清除语音缓存
for (const contactIdx of selectedVoiceIndexes) {
try {
await deleteVoiceRecordingsByContact(contactIdx);
clearedCount++;
} catch (err) {
console.error('[可乐] 删除语音记录失败:', err);
}
}
modal.remove();
showToast(`已清除 ${clearedCount} 项缓存数据`);
console.log('[可乐] 已清除缓存,总结:', selectedSummaryIndexes.size, '语音:', selectedVoiceIndexes.length);
});
}
function setupPhoneMinimize() {
const phone = document.getElementById('wechat-phone');
const minimizeBtn = document.getElementById('wechat-minimize-btn');
@@ -609,11 +814,11 @@ function restorePhone() {
phone.classList.remove('minimized');
// 恢复原始位置或居中
if (settings.phoneOriginalPosition) {
phone.style.left = settings.phoneOriginalPosition.left;
phone.style.top = settings.phoneOriginalPosition.top;
}
// 清除缩小前保存的位置,让居中函数重新计算
delete settings.phoneOriginalPosition;
// 恢复到屏幕中央
centerPhoneInViewport({ force: true });
// 恢复时根据设置显示悬浮球
if (settings.floatingBallEnabled !== false) {
@@ -763,6 +968,12 @@ function bindEvents() {
toggleFloatingBallEnabled();
});
// 下拉菜单 - 清除缓存
document.getElementById('wechat-menu-clear-cache')?.addEventListener('click', () => {
document.getElementById('wechat-dropdown-menu')?.classList.add('hidden');
clearOrphanedCache();
});
// ===== 群聊创建弹窗事件 =====
document.getElementById('wechat-group-create-close')?.addEventListener('click', closeGroupCreateModal);
document.getElementById('wechat-group-create-confirm')?.addEventListener('click', createGroupChat);

View File

@@ -118,6 +118,10 @@ export function generatePhoneHTML() {
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M7 16.5c0-2 2.2-3.5 5-3.5s5 1.5 5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>
<span id="wechat-floating-ball-text">悬浮窗</span>
</div>
<div class="wechat-dropdown-item" id="wechat-menu-clear-cache">
<svg viewBox="0 0 24 24"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
<span>清除缓存</span>
</div>
</div>
<!-- 添加朋友页面 -->