mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-05 23:25:51 +00:00
Add files via upload
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
213
main.js
213
main.js
@@ -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');
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 添加朋友页面 -->
|
||||
|
||||
Reference in New Issue
Block a user