diff --git a/modules/fourth-wall/fourth-wall.html b/modules/fourth-wall/fourth-wall.html index 4e9cafe..5f107a8 100644 --- a/modules/fourth-wall/fourth-wall.html +++ b/modules/fourth-wall/fourth-wall.html @@ -858,7 +858,7 @@ function renderContent(text) { if (!text) return ''; let html = String(text).replace(/&/g, '&').replace(//g, '>'); - html = html.replace(/\[(?:image|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => { + html = html.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => { const tags = parseImageToken(inner); if (!tags) return _; return `
`; @@ -900,6 +900,7 @@ function renderContent(text) { return html; } + function renderMessages() { const container = document.getElementById('messages'); const { history, isStreaming, editingIndex } = state; diff --git a/modules/fourth-wall/fw-image.js b/modules/fourth-wall/fw-image.js index 83302bb..c93f16e 100644 --- a/modules/fourth-wall/fw-image.js +++ b/modules/fourth-wall/fw-image.js @@ -272,9 +272,9 @@ export async function handleGenerate(data, postToFrame) { export const IMG_GUIDELINE = `## 模拟图片 如果需要发图、照片给对方时,可以在聊天文本中穿插以下格式行,进行图片模拟: -[image: Subject, Appearance, Background, Atmosphere, Extra descriptors] +[img: Subject, Appearance, Background, Atmosphere, Extra descriptors] - tag必须为英文,用逗号分隔,使用Danbooru风格的tag,5-15个tag - 第一个tag须固定为人物数量标签,如: 1girl, 1boy, 2girls, solo, etc. -- 可以多张照片: 每行一张 [image: ...] +- 可以多张照片: 每行一张 [img: ...] - 当需要发送的内容尺度较大时加上nsfw相关tag - image部分也需要在内`; diff --git a/modules/fourth-wall/fw-message-enhancer.js b/modules/fourth-wall/fw-message-enhancer.js index 06a3c38..27ff03a 100644 --- a/modules/fourth-wall/fw-message-enhancer.js +++ b/modules/fourth-wall/fw-message-enhancer.js @@ -25,6 +25,7 @@ const CSS_INJECTED_KEY = 'xb-me-css-injected'; let currentAudio = null; let imageObserver = null; +let domObserver = null; // ▼ 新增 // ════════════════════════════════════════════════════════════════════════════ // 初始化与清理 @@ -39,6 +40,7 @@ export async function initMessageEnhancer() { injectStyles(); await loadVoices(); initImageObserver(); + initDomObserver(); // ▼ 新增 events.on(event_types.CHAT_CHANGED, () => { clearQueue(); @@ -65,12 +67,99 @@ export function cleanupMessageEnhancer() { imageObserver = null; } + // ▼ 新增 + if (domObserver) { + domObserver.disconnect(); + domObserver = null; + } + if (currentAudio) { currentAudio.pause(); currentAudio = null; } } +// ════════════════════════════════════════════════════════════════════════════ +// DOM 变化观察器(新增) +// ════════════════════════════════════════════════════════════════════════════ + +function initDomObserver() { + if (domObserver) return; + + const chatContainer = document.getElementById('chat'); + if (!chatContainer) { + // 如果 chat 容器还没加载,延迟重试 + setTimeout(initDomObserver, 500); + return; + } + + // 用于防抖处理 + let pendingTexts = new Set(); + let debounceTimer = null; + + domObserver = new MutationObserver((mutations) => { + const settings = extension_settings[EXT_ID]; + if (!settings?.fourthWall?.enabled) return; + + for (const mutation of mutations) { + let mesText = null; + + if (mutation.type === 'childList') { + for (const node of mutation.addedNodes) { + if (node.nodeType !== Node.ELEMENT_NODE) continue; + + if (node.classList?.contains('mes_text')) { + mesText = node; + } else if (node.classList?.contains('mes')) { + mesText = node.querySelector('.mes_text'); + } else { + mesText = node.querySelector?.('.mes_text'); + } + + if (mesText && hasUnrenderedPlaceholders(mesText)) { + pendingTexts.add(mesText); + } + } + } + + if (mutation.target?.classList?.contains('mes_text')) { + if (hasUnrenderedPlaceholders(mutation.target)) { + pendingTexts.add(mutation.target); + } + } else if (mutation.target?.closest?.('.mes_text')) { + const target = mutation.target.closest('.mes_text'); + if (hasUnrenderedPlaceholders(target)) { + pendingTexts.add(target); + } + } + } + + if (pendingTexts.size > 0 && !debounceTimer) { + debounceTimer = setTimeout(() => { + pendingTexts.forEach(mesText => { + if (document.contains(mesText)) { + enhanceMessageContent(mesText); + } + }); + pendingTexts.clear(); + debounceTimer = null; + }, 50); + } + }); + + domObserver.observe(chatContainer, { + childList: true, + subtree: true, + }); +} + +function hasUnrenderedPlaceholders(mesText) { + if (!mesText) return false; + const html = mesText.innerHTML; + return /\[(?:img|图片)\s*:\s*[^\]]+\]/i.test(html) || + /\[(?:voice|语音)\s*:[^\]]+\]/i.test(html); +} + // ════════════════════════════════════════════════════════════════════════════ // 事件处理 // ════════════════════════════════════════════════════════════════════════════ @@ -271,7 +360,7 @@ function enhanceMessageContent(container) { let enhanced = html; let hasChanges = false; - enhanced = enhanced.replace(/\[(?:image|图片)\s*:\s*([^\]]+)\]/gi, (match, inner) => { + enhanced = enhanced.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (match, inner) => { const tags = parseImageToken(inner); if (!tags) return match; hasChanges = true; diff --git a/modules/novel-draw/gallery-cache.js b/modules/novel-draw/gallery-cache.js index fc6ecc6..0a053c0 100644 --- a/modules/novel-draw/gallery-cache.js +++ b/modules/novel-draw/gallery-cache.js @@ -12,6 +12,7 @@ const DB_NAME = 'xb_novel_draw_previews'; const DB_STORE = 'previews'; const DB_SELECTIONS_STORE = 'selections'; const DB_VERSION = 2; +const CACHE_TTL = 5000; // ═══════════════════════════════════════════════════════════════════════════ // 状态 @@ -21,24 +22,31 @@ let db = null; let dbOpening = null; let galleryOverlayCreated = false; let currentGalleryData = null; -let dbInitialized = false; + +const previewCache = new Map(); // ═══════════════════════════════════════════════════════════════════════════ -// 日志 +// 内存缓存 // ═══════════════════════════════════════════════════════════════════════════ -function log(...args) { - console.log('[GalleryCache]', ...args); +function getCachedPreviews(slotId) { + const cached = previewCache.get(slotId); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data; + } + return null; } -function logDbState(label) { - log(label, { - dbExists: !!db, - dbOpening: !!dbOpening, - dbName: db?.name, - dbVersion: db?.version, - stores: db ? [...db.objectStoreNames] : null - }); +function setCachedPreviews(slotId, data) { + previewCache.set(slotId, { data, timestamp: Date.now() }); +} + +function invalidateCache(slotId) { + if (slotId) { + previewCache.delete(slotId); + } else { + previewCache.clear(); + } } // ═══════════════════════════════════════════════════════════════════════════ @@ -59,9 +67,9 @@ function showToast(message, type = 'success', duration = 2500) { const colors = { success: 'rgba(62,207,142,0.95)', error: 'rgba(248,113,113,0.95)', info: 'rgba(212,165,116,0.95)' }; const toast = document.createElement('div'); toast.textContent = message; - toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:' + (colors[type] || colors.info) + ';color:#fff;padding:10px 20px;border-radius:8px;font-size:13px;z-index:99999;animation:fadeInOut ' + (duration/1000) + 's ease-in-out;max-width:80vw;text-align:center;word-break:break-all'; + toast.style.cssText = `position:fixed;top:20px;left:50%;transform:translateX(-50%);background:${colors[type] || colors.info};color:#fff;padding:10px 20px;border-radius:8px;font-size:13px;z-index:99999;animation:fadeInOut ${duration/1000}s ease-in-out;max-width:80vw;text-align:center;word-break:break-all`; document.body.appendChild(toast); - setTimeout(function() { toast.remove(); }, duration); + setTimeout(() => toast.remove(), duration); } // ═══════════════════════════════════════════════════════════════════════════ @@ -69,74 +77,48 @@ function showToast(message, type = 'success', duration = 2500) { // ═══════════════════════════════════════════════════════════════════════════ function isDbValid() { - if (!db) { - log('isDbValid: db is null'); - return false; - } + if (!db) return false; try { - const valid = db.objectStoreNames.length > 0; - log('isDbValid:', valid); - return valid; - } catch (e) { - log('isDbValid: error', e.message); + return db.objectStoreNames.length > 0; + } catch { return false; } } export async function openDB() { - if (!dbInitialized) { - dbInitialized = true; - log('openDB: first call'); + if (dbOpening) return dbOpening; + + if (isDbValid() && db.objectStoreNames.contains(DB_SELECTIONS_STORE)) { + return db; } - if (dbOpening) { - return dbOpening; - } - - if (isDbValid()) { - if (db.objectStoreNames.contains(DB_SELECTIONS_STORE)) { - return db; - } - try { - db.close(); - } catch (e) {} + if (db) { + try { db.close(); } catch {} db = null; } - dbOpening = new Promise(function(resolve, reject) { - var request = indexedDB.open(DB_NAME, DB_VERSION); + dbOpening = new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onerror = function() { + request.onerror = () => { dbOpening = null; reject(request.error); }; - request.onsuccess = function() { + request.onsuccess = () => { db = request.result; - - db.onclose = function() { - db = null; - }; - - db.onversionchange = function() { - db.close(); - db = null; - }; - + db.onclose = () => { db = null; }; + db.onversionchange = () => { db.close(); db = null; }; dbOpening = null; resolve(db); }; - request.onupgradeneeded = function(e) { - var database = e.target.result; - + request.onupgradeneeded = (e) => { + const database = e.target.result; if (!database.objectStoreNames.contains(DB_STORE)) { - var store = database.createObjectStore(DB_STORE, { keyPath: 'imgId' }); - ['messageId', 'chatId', 'timestamp', 'slotId'].forEach(function(idx) { - store.createIndex(idx, idx); - }); + const store = database.createObjectStore(DB_STORE, { keyPath: 'imgId' }); + ['messageId', 'chatId', 'timestamp', 'slotId'].forEach(idx => store.createIndex(idx, idx)); } - if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) { database.createObjectStore(DB_SELECTIONS_STORE, { keyPath: 'slotId' }); } @@ -151,55 +133,45 @@ export async function openDB() { // ═══════════════════════════════════════════════════════════════════════════ export async function setSlotSelection(slotId, imgId) { - log('setSlotSelection:', slotId, imgId); - var database = await openDB(); - logDbState('setSlotSelection got db'); - if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) { - log('setSlotSelection: no store'); - return; - } - return new Promise(function(resolve, reject) { + const database = await openDB(); + if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) return; + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite'); - tx.objectStore(DB_SELECTIONS_STORE).put({ slotId: slotId, selectedImgId: imgId, timestamp: Date.now() }); - tx.oncomplete = function() { log('setSlotSelection: done'); resolve(); }; - tx.onerror = function() { log('setSlotSelection: error', tx.error); reject(tx.error); }; + const tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite'); + tx.objectStore(DB_SELECTIONS_STORE).put({ slotId, selectedImgId: imgId, timestamp: Date.now() }); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); } catch (e) { - log('setSlotSelection: tx error', e.message); reject(e); } }); } export async function getSlotSelection(slotId) { - log('getSlotSelection:', slotId); - var database = await openDB(); + const database = await openDB(); if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) return null; - return new Promise(function(resolve, reject) { + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_SELECTIONS_STORE, 'readonly'); - var request = tx.objectStore(DB_SELECTIONS_STORE).get(slotId); - request.onsuccess = function() { resolve(request.result?.selectedImgId || null); }; - request.onerror = function() { reject(request.error); }; + const tx = database.transaction(DB_SELECTIONS_STORE, 'readonly'); + const request = tx.objectStore(DB_SELECTIONS_STORE).get(slotId); + request.onsuccess = () => resolve(request.result?.selectedImgId || null); + request.onerror = () => reject(request.error); } catch (e) { - log('getSlotSelection: tx error', e.message); reject(e); } }); } export async function clearSlotSelection(slotId) { - log('clearSlotSelection:', slotId); - var database = await openDB(); + const database = await openDB(); if (!database.objectStoreNames.contains(DB_SELECTIONS_STORE)) return; - return new Promise(function(resolve, reject) { + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite'); + const tx = database.transaction(DB_SELECTIONS_STORE, 'readwrite'); tx.objectStore(DB_SELECTIONS_STORE).delete(slotId); - tx.oncomplete = resolve; - tx.onerror = function() { reject(tx.error); }; + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); } catch (e) { - log('clearSlotSelection: tx error', e.message); reject(e); } }); @@ -210,56 +182,41 @@ export async function clearSlotSelection(slotId) { // ═══════════════════════════════════════════════════════════════════════════ export async function storePreview(opts) { - var imgId = opts.imgId; - var slotId = opts.slotId; - var messageId = opts.messageId; - var base64 = opts.base64 || null; - var tags = opts.tags; - var positive = opts.positive; - var savedUrl = opts.savedUrl || null; - var status = opts.status || 'success'; - var errorType = opts.errorType || null; - var errorMessage = opts.errorMessage || null; - var characterPrompts = opts.characterPrompts || null; - var negativePrompt = opts.negativePrompt || null; + const { imgId, slotId, messageId, base64 = null, tags, positive, savedUrl = null, status = 'success', errorType = null, errorMessage = null, characterPrompts = null, negativePrompt = null } = opts; + const database = await openDB(); + const ctx = getContext(); - log('storePreview:', imgId); - var database = await openDB(); - logDbState('storePreview got db'); - var ctx = getContext(); - return new Promise(function(resolve, reject) { + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_STORE, 'readwrite'); + const tx = database.transaction(DB_STORE, 'readwrite'); tx.objectStore(DB_STORE).put({ - imgId: imgId, + imgId, slotId: slotId || imgId, - messageId: messageId, + messageId, chatId: ctx.chatId || (ctx.characterId || 'unknown'), characterName: getChatCharacterName(), - base64: base64, - tags: tags, - positive: positive, - savedUrl: savedUrl, - status: status, - errorType: errorType, - errorMessage: errorMessage, - characterPrompts: characterPrompts, - negativePrompt: negativePrompt, + base64, + tags, + positive, + savedUrl, + status, + errorType, + errorMessage, + characterPrompts, + negativePrompt, timestamp: Date.now() }); - tx.oncomplete = function() { log('storePreview: done'); resolve(); }; - tx.onerror = function() { log('storePreview: error', tx.error); reject(tx.error); }; + tx.oncomplete = () => { invalidateCache(slotId); resolve(); }; + tx.onerror = () => reject(tx.error); } catch (e) { - log('storePreview: tx error', e.message); reject(e); } }); } export async function storeFailedPlaceholder(opts) { - var imgId = 'failed-' + opts.slotId + '-' + Date.now(); return storePreview({ - imgId: imgId, + imgId: `failed-${opts.slotId}-${Date.now()}`, slotId: opts.slotId, messageId: opts.messageId, base64: null, @@ -274,84 +231,75 @@ export async function storeFailedPlaceholder(opts) { } export async function getPreview(imgId) { - log('getPreview:', imgId); - var database = await openDB(); - logDbState('getPreview got db'); - return new Promise(function(resolve, reject) { + const database = await openDB(); + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_STORE, 'readonly'); - var request = tx.objectStore(DB_STORE).get(imgId); - request.onsuccess = function() { - log('getPreview: found:', !!request.result); - resolve(request.result); - }; - request.onerror = function() { - log('getPreview: error', request.error); - reject(request.error); - }; + const tx = database.transaction(DB_STORE, 'readonly'); + const request = tx.objectStore(DB_STORE).get(imgId); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); } catch (e) { - log('getPreview: tx error', e.message); reject(e); } }); } export async function getPreviewsBySlot(slotId) { - log('getPreviewsBySlot:', slotId); - var database = await openDB(); - return new Promise(function(resolve, reject) { + const cached = getCachedPreviews(slotId); + if (cached) return cached; + + const database = await openDB(); + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_STORE, 'readonly'); - var store = tx.objectStore(DB_STORE); + const tx = database.transaction(DB_STORE, 'readonly'); + const store = tx.objectStore(DB_STORE); + + const processResults = (results) => { + results.sort((a, b) => b.timestamp - a.timestamp); + setCachedPreviews(slotId, results); + resolve(results); + }; if (store.indexNames.contains('slotId')) { - var index = store.index('slotId'); - var request = index.getAll(slotId); - request.onsuccess = function() { - var results = request.result || []; - if (results.length === 0) { - var allRequest = store.getAll(); - allRequest.onsuccess = function() { - var allRecords = allRequest.result || []; - results = allRecords.filter(function(r) { - return r.slotId === slotId || r.imgId === slotId || (!r.slotId && r.imgId === slotId); - }); - results.sort(function(a, b) { return b.timestamp - a.timestamp; }); - resolve(results); - }; - allRequest.onerror = function() { reject(allRequest.error); }; + const request = store.index('slotId').getAll(slotId); + request.onsuccess = () => { + if (request.result?.length) { + processResults(request.result); } else { - results.sort(function(a, b) { return b.timestamp - a.timestamp; }); - resolve(results); + const allRequest = store.getAll(); + allRequest.onsuccess = () => { + const results = (allRequest.result || []).filter(r => + r.slotId === slotId || r.imgId === slotId || (!r.slotId && r.imgId === slotId) + ); + processResults(results); + }; + allRequest.onerror = () => reject(allRequest.error); } }; - request.onerror = function() { reject(request.error); }; + request.onerror = () => reject(request.error); } else { - var request2 = store.getAll(); - request2.onsuccess = function() { - var allRecords = request2.result || []; - var results = allRecords.filter(function(r) { return r.slotId === slotId || r.imgId === slotId; }); - results.sort(function(a, b) { return b.timestamp - a.timestamp; }); - resolve(results); + const request = store.getAll(); + request.onsuccess = () => { + const results = (request.result || []).filter(r => r.slotId === slotId || r.imgId === slotId); + processResults(results); }; - request2.onerror = function() { reject(request2.error); }; + request.onerror = () => reject(request.error); } } catch (e) { - log('getPreviewsBySlot: tx error', e.message); reject(e); } }); } export async function getDisplayPreviewForSlot(slotId) { - var previews = await getPreviewsBySlot(slotId); + const previews = await getPreviewsBySlot(slotId); if (!previews.length) return { preview: null, historyCount: 0, hasData: false, isFailed: false }; - var successPreviews = previews.filter(function(p) { return p.status !== 'failed' && p.base64; }); - var failedPreviews = previews.filter(function(p) { return p.status === 'failed' || !p.base64; }); + const successPreviews = previews.filter(p => p.status !== 'failed' && p.base64); + const failedPreviews = previews.filter(p => p.status === 'failed' || !p.base64); if (successPreviews.length === 0) { - var latestFailed = failedPreviews[0]; + const latestFailed = failedPreviews[0]; return { preview: latestFailed, historyCount: 0, @@ -366,9 +314,9 @@ export async function getDisplayPreviewForSlot(slotId) { }; } - var selectedImgId = await getSlotSelection(slotId); + const selectedImgId = await getSlotSelection(slotId); if (selectedImgId) { - var selected = successPreviews.find(function(p) { return p.imgId === selectedImgId; }); + const selected = successPreviews.find(p => p.imgId === selectedImgId); if (selected) { return { preview: selected, historyCount: successPreviews.length, hasData: true, isFailed: false }; } @@ -378,78 +326,65 @@ export async function getDisplayPreviewForSlot(slotId) { } export async function getLatestPreviewForSlot(slotId) { - var result = await getDisplayPreviewForSlot(slotId); + const result = await getDisplayPreviewForSlot(slotId); return result.preview; } export async function deletePreview(imgId) { - log('deletePreview:', imgId); - var database = await openDB(); - return new Promise(function(resolve, reject) { + const database = await openDB(); + const preview = await getPreview(imgId); + const slotId = preview?.slotId; + + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_STORE, 'readwrite'); + const tx = database.transaction(DB_STORE, 'readwrite'); tx.objectStore(DB_STORE).delete(imgId); - tx.oncomplete = function() { log('deletePreview: done'); resolve(); }; - tx.onerror = function() { reject(tx.error); }; + tx.oncomplete = () => { if (slotId) invalidateCache(slotId); resolve(); }; + tx.onerror = () => reject(tx.error); } catch (e) { - log('deletePreview: tx error', e.message); reject(e); } }); } export async function deleteFailedRecordsForSlot(slotId) { - var previews = await getPreviewsBySlot(slotId); - var failedRecords = previews.filter(function(p) { return p.status === 'failed' || !p.base64; }); - for (var i = 0; i < failedRecords.length; i++) { - await deletePreview(failedRecords[i].imgId); + const previews = await getPreviewsBySlot(slotId); + const failedRecords = previews.filter(p => p.status === 'failed' || !p.base64); + for (const record of failedRecords) { + await deletePreview(record.imgId); } } export async function updatePreviewSavedUrl(imgId, savedUrl) { - log('updatePreviewSavedUrl:', imgId, savedUrl); - var database = await openDB(); - logDbState('updatePreviewSavedUrl got db'); - - var preview = await getPreview(imgId); - if (!preview) { - log('updatePreviewSavedUrl: not found'); - return; - } + const database = await openDB(); + const preview = await getPreview(imgId); + if (!preview) return; preview.savedUrl = savedUrl; - log('updatePreviewSavedUrl: re-getting db for write...'); - database = await openDB(); - logDbState('updatePreviewSavedUrl got db again'); - - return new Promise(function(resolve, reject) { + return new Promise((resolve, reject) => { try { - var tx = database.transaction(DB_STORE, 'readwrite'); + const tx = database.transaction(DB_STORE, 'readwrite'); tx.objectStore(DB_STORE).put(preview); - tx.oncomplete = function() { log('updatePreviewSavedUrl: done'); resolve(); }; - tx.onerror = function() { log('updatePreviewSavedUrl: error', tx.error); reject(tx.error); }; + tx.oncomplete = () => { invalidateCache(preview.slotId); resolve(); }; + tx.onerror = () => reject(tx.error); } catch (e) { - log('updatePreviewSavedUrl: tx error', e.message); reject(e); } }); } export async function getCacheStats() { - log('getCacheStats'); - var database = await openDB(); - return new Promise(function(resolve) { + const database = await openDB(); + return new Promise((resolve) => { try { - var tx = database.transaction(DB_STORE, 'readonly'); - var store = tx.objectStore(DB_STORE); - var countReq = store.count(); - var totalSize = 0; - var successCount = 0; - var failedCount = 0; + const tx = database.transaction(DB_STORE, 'readonly'); + const store = tx.objectStore(DB_STORE); + const countReq = store.count(); + let totalSize = 0, successCount = 0, failedCount = 0; - store.openCursor().onsuccess = function(e) { - var cursor = e.target.result; + store.openCursor().onsuccess = (e) => { + const cursor = e.target.result; if (cursor) { totalSize += (cursor.value.base64?.length || 0) * 0.75; if (cursor.value.status === 'failed' || !cursor.value.base64) { @@ -460,38 +395,34 @@ export async function getCacheStats() { cursor.continue(); } }; - tx.oncomplete = function() { - resolve({ - count: countReq.result || 0, - successCount: successCount, - failedCount: failedCount, - sizeBytes: Math.round(totalSize), - sizeMB: (totalSize / 1024 / 1024).toFixed(2) - }); - }; - } catch (e) { - log('getCacheStats: error', e.message); + tx.oncomplete = () => resolve({ + count: countReq.result || 0, + successCount, + failedCount, + sizeBytes: Math.round(totalSize), + sizeMB: (totalSize / 1024 / 1024).toFixed(2) + }); + } catch { resolve({ count: 0, successCount: 0, failedCount: 0, sizeBytes: 0, sizeMB: '0' }); } }); } -export async function clearExpiredCache(cacheDays) { - cacheDays = cacheDays || 3; - log('clearExpiredCache:', cacheDays, 'days'); - var cutoff = Date.now() - cacheDays * 24 * 60 * 60 * 1000; - var database = await openDB(); - var deleted = 0; - return new Promise(function(resolve) { +export async function clearExpiredCache(cacheDays = 3) { + const cutoff = Date.now() - cacheDays * 24 * 60 * 60 * 1000; + const database = await openDB(); + let deleted = 0; + + return new Promise((resolve) => { try { - var tx = database.transaction(DB_STORE, 'readwrite'); - var store = tx.objectStore(DB_STORE); - store.openCursor().onsuccess = function(e) { - var cursor = e.target.result; + const tx = database.transaction(DB_STORE, 'readwrite'); + const store = tx.objectStore(DB_STORE); + store.openCursor().onsuccess = (e) => { + const cursor = e.target.result; if (cursor) { - var record = cursor.value; - var isExpiredUnsaved = record.timestamp < cutoff && !record.savedUrl; - var isFailed = record.status === 'failed' || !record.base64; + const record = cursor.value; + const isExpiredUnsaved = record.timestamp < cutoff && !record.savedUrl; + const isFailed = record.status === 'failed' || !record.base64; if (isExpiredUnsaved || (isFailed && record.timestamp < cutoff)) { cursor.delete(); deleted++; @@ -499,70 +430,59 @@ export async function clearExpiredCache(cacheDays) { cursor.continue(); } }; - tx.oncomplete = function() { - log('clearExpiredCache: deleted', deleted); - resolve(deleted); - }; - } catch (e) { - log('clearExpiredCache: error', e.message); + tx.oncomplete = () => { invalidateCache(); resolve(deleted); }; + } catch { resolve(0); } }); } export async function clearAllCache() { - log('clearAllCache'); - var database = await openDB(); - return new Promise(function(resolve, reject) { + const database = await openDB(); + return new Promise((resolve, reject) => { try { - var stores = [DB_STORE]; + const stores = [DB_STORE]; if (database.objectStoreNames.contains(DB_SELECTIONS_STORE)) { stores.push(DB_SELECTIONS_STORE); } - var tx = database.transaction(stores, 'readwrite'); + const tx = database.transaction(stores, 'readwrite'); tx.objectStore(DB_STORE).clear(); if (stores.length > 1) { tx.objectStore(DB_SELECTIONS_STORE).clear(); } - tx.oncomplete = function() { log('clearAllCache: done'); resolve(); }; - tx.onerror = function() { reject(tx.error); }; + tx.oncomplete = () => { invalidateCache(); resolve(); }; + tx.onerror = () => reject(tx.error); } catch (e) { - log('clearAllCache: error', e.message); reject(e); } }); } export async function getGallerySummary() { - log('getGallerySummary'); - var database = await openDB(); - return new Promise(function(resolve) { + const database = await openDB(); + return new Promise((resolve) => { try { - var tx = database.transaction(DB_STORE, 'readonly'); - var store = tx.objectStore(DB_STORE); - var request = store.getAll(); + const tx = database.transaction(DB_STORE, 'readonly'); + const request = tx.objectStore(DB_STORE).getAll(); - request.onsuccess = function() { - var results = request.result || []; - var summary = {}; + request.onsuccess = () => { + const results = request.result || []; + const summary = {}; - for (var i = 0; i < results.length; i++) { - var item = results[i]; + for (const item of results) { if (item.status === 'failed' || !item.base64) continue; - var charName = item.characterName || 'Unknown'; + const charName = item.characterName || 'Unknown'; if (!summary[charName]) { summary[charName] = { count: 0, totalSize: 0, slots: {}, latestTimestamp: 0 }; } - var slotId = item.slotId || item.imgId; + const slotId = item.slotId || item.imgId; if (!summary[charName].slots[slotId]) { - summary[charName].slots[slotId] = { - count: 0, hasSaved: false, latestTimestamp: 0, latestImgId: null, - }; + summary[charName].slots[slotId] = { count: 0, hasSaved: false, latestTimestamp: 0, latestImgId: null }; } - var slot = summary[charName].slots[slotId]; + const slot = summary[charName].slots[slotId]; slot.count++; if (item.savedUrl) slot.hasSaved = true; if (item.timestamp > slot.latestTimestamp) { @@ -579,46 +499,41 @@ export async function getGallerySummary() { resolve(summary); }; - request.onerror = function() { resolve({}); }; - } catch (e) { - log('getGallerySummary: error', e.message); + request.onerror = () => resolve({}); + } catch { resolve({}); } }); } export async function getCharacterPreviews(charName) { - log('getCharacterPreviews:', charName); - var database = await openDB(); - return new Promise(function(resolve) { + const database = await openDB(); + return new Promise((resolve) => { try { - var tx = database.transaction(DB_STORE, 'readonly'); - var store = tx.objectStore(DB_STORE); - var request = store.getAll(); + const tx = database.transaction(DB_STORE, 'readonly'); + const request = tx.objectStore(DB_STORE).getAll(); - request.onsuccess = function() { - var results = request.result || []; - var slots = {}; + request.onsuccess = () => { + const results = request.result || []; + const slots = {}; - for (var i = 0; i < results.length; i++) { - var item = results[i]; + for (const item of results) { if ((item.characterName || 'Unknown') !== charName) continue; if (item.status === 'failed' || !item.base64) continue; - var slotId = item.slotId || item.imgId; + const slotId = item.slotId || item.imgId; if (!slots[slotId]) slots[slotId] = []; slots[slotId].push(item); } - for (var sid in slots) { - slots[sid].sort(function(a, b) { return b.timestamp - a.timestamp; }); + for (const sid in slots) { + slots[sid].sort((a, b) => b.timestamp - a.timestamp); } resolve(slots); }; - request.onerror = function() { resolve({}); }; - } catch (e) { - log('getCharacterPreviews: error', e.message); + request.onerror = () => resolve({}); + } catch { resolve({}); } }); @@ -630,9 +545,9 @@ export async function getCharacterPreviews(charName) { function ensureGalleryStyles() { if (document.getElementById('nd-gallery-styles')) return; - var style = document.createElement('style'); + const style = document.createElement('style'); style.id = 'nd-gallery-styles'; - style.textContent = '#nd-gallery-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:100000;display:none;background:rgba(0,0,0,0.85);backdrop-filter:blur(8px)}#nd-gallery-overlay.visible{display:flex;flex-direction:column;align-items:center;justify-content:center}.nd-gallery-close{position:absolute;top:16px;right:16px;width:40px;height:40px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:20px;cursor:pointer;z-index:10}.nd-gallery-close:hover{background:rgba(255,255,255,0.2)}.nd-gallery-main{display:flex;align-items:center;gap:16px;max-width:90vw;max-height:70vh}.nd-gallery-nav{width:48px;height:48px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:24px;cursor:pointer;flex-shrink:0}.nd-gallery-nav:hover{background:rgba(255,255,255,0.2)}.nd-gallery-nav:disabled{opacity:0.3;cursor:not-allowed}.nd-gallery-img-wrap{position:relative;max-width:calc(90vw - 140px);max-height:70vh}.nd-gallery-img{max-width:100%;max-height:70vh;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.5)}.nd-gallery-saved-badge{position:absolute;top:12px;left:12px;background:rgba(62,207,142,0.9);padding:4px 10px;border-radius:6px;font-size:11px;color:#fff;font-weight:600}.nd-gallery-thumbs{display:flex;gap:8px;margin-top:20px;padding:12px;background:rgba(0,0,0,0.3);border-radius:12px;max-width:90vw;overflow-x:auto}.nd-gallery-thumb{width:64px;height:64px;border-radius:8px;object-fit:cover;cursor:pointer;border:2px solid transparent;opacity:0.6;transition:all 0.15s;flex-shrink:0}.nd-gallery-thumb:hover{opacity:0.9}.nd-gallery-thumb.active{border-color:#d4a574;opacity:1}.nd-gallery-thumb.saved{border-color:rgba(62,207,142,0.8)}.nd-gallery-actions{display:flex;gap:12px;margin-top:16px}.nd-gallery-btn{padding:10px 20px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;cursor:pointer;transition:all 0.15s}.nd-gallery-btn:hover{background:rgba(255,255,255,0.2)}.nd-gallery-btn.primary{background:rgba(212,165,116,0.3);border-color:rgba(212,165,116,0.5)}.nd-gallery-btn.danger{color:#f87171;border-color:rgba(248,113,113,0.3)}.nd-gallery-btn.danger:hover{background:rgba(248,113,113,0.15)}.nd-gallery-info{text-align:center;margin-top:12px;font-size:12px;color:rgba(255,255,255,0.6)}'; + style.textContent = `#nd-gallery-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:100000;display:none;background:rgba(0,0,0,0.85);backdrop-filter:blur(8px)}#nd-gallery-overlay.visible{display:flex;flex-direction:column;align-items:center;justify-content:center}.nd-gallery-close{position:absolute;top:16px;right:16px;width:40px;height:40px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:20px;cursor:pointer;z-index:10}.nd-gallery-close:hover{background:rgba(255,255,255,0.2)}.nd-gallery-main{display:flex;align-items:center;gap:16px;max-width:90vw;max-height:70vh}.nd-gallery-nav{width:48px;height:48px;border:none;background:rgba(255,255,255,0.1);border-radius:50%;color:#fff;font-size:24px;cursor:pointer;flex-shrink:0}.nd-gallery-nav:hover{background:rgba(255,255,255,0.2)}.nd-gallery-nav:disabled{opacity:0.3;cursor:not-allowed}.nd-gallery-img-wrap{position:relative;max-width:calc(90vw - 140px);max-height:70vh}.nd-gallery-img{max-width:100%;max-height:70vh;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.5)}.nd-gallery-saved-badge{position:absolute;top:12px;left:12px;background:rgba(62,207,142,0.9);padding:4px 10px;border-radius:6px;font-size:11px;color:#fff;font-weight:600}.nd-gallery-thumbs{display:flex;gap:8px;margin-top:20px;padding:12px;background:rgba(0,0,0,0.3);border-radius:12px;max-width:90vw;overflow-x:auto}.nd-gallery-thumb{width:64px;height:64px;border-radius:8px;object-fit:cover;cursor:pointer;border:2px solid transparent;opacity:0.6;transition:all 0.15s;flex-shrink:0}.nd-gallery-thumb:hover{opacity:0.9}.nd-gallery-thumb.active{border-color:#d4a574;opacity:1}.nd-gallery-thumb.saved{border-color:rgba(62,207,142,0.8)}.nd-gallery-actions{display:flex;gap:12px;margin-top:16px}.nd-gallery-btn{padding:10px 20px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;cursor:pointer;transition:all 0.15s}.nd-gallery-btn:hover{background:rgba(255,255,255,0.2)}.nd-gallery-btn.primary{background:rgba(212,165,116,0.3);border-color:rgba(212,165,116,0.5)}.nd-gallery-btn.danger{color:#f87171;border-color:rgba(248,113,113,0.3)}.nd-gallery-btn.danger:hover{background:rgba(248,113,113,0.15)}.nd-gallery-info{text-align:center;margin-top:12px;font-size:12px;color:rgba(255,255,255,0.6)}`; document.head.appendChild(style); } @@ -641,51 +556,45 @@ function createGalleryOverlay() { galleryOverlayCreated = true; ensureGalleryStyles(); - var overlay = document.createElement('div'); + const overlay = document.createElement('div'); overlay.id = 'nd-gallery-overlay'; - overlay.innerHTML = ''; + overlay.innerHTML = ``; document.body.appendChild(overlay); document.getElementById('nd-gallery-close').addEventListener('click', closeGallery); - document.getElementById('nd-gallery-prev').addEventListener('click', function() { navigateGallery(-1); }); - document.getElementById('nd-gallery-next').addEventListener('click', function() { navigateGallery(1); }); + document.getElementById('nd-gallery-prev').addEventListener('click', () => navigateGallery(-1)); + document.getElementById('nd-gallery-next').addEventListener('click', () => navigateGallery(1)); document.getElementById('nd-gallery-use').addEventListener('click', useCurrentGalleryImage); document.getElementById('nd-gallery-save').addEventListener('click', saveCurrentGalleryImage); document.getElementById('nd-gallery-delete').addEventListener('click', deleteCurrentGalleryImage); - - overlay.addEventListener('click', function(e) { - if (e.target === overlay) closeGallery(); - }); + overlay.addEventListener('click', (e) => { if (e.target === overlay) closeGallery(); }); } -export async function openGallery(slotId, messageId, callbacks) { - callbacks = callbacks || {}; - log('openGallery:', slotId, messageId); +export async function openGallery(slotId, messageId, callbacks = {}) { createGalleryOverlay(); - var previews = await getPreviewsBySlot(slotId); - var validPreviews = previews.filter(function(p) { return p.status !== 'failed' && p.base64; }); + const previews = await getPreviewsBySlot(slotId); + const validPreviews = previews.filter(p => p.status !== 'failed' && p.base64); if (!validPreviews.length) { - showToast('\u6CA1\u6709\u627E\u5230\u56FE\u7247\u5386\u53F2', 'error'); + showToast('没有找到图片历史', 'error'); return; } - var selectedImgId = await getSlotSelection(slotId); - var startIndex = 0; + const selectedImgId = await getSlotSelection(slotId); + let startIndex = 0; if (selectedImgId) { - var idx = validPreviews.findIndex(function(p) { return p.imgId === selectedImgId; }); + const idx = validPreviews.findIndex(p => p.imgId === selectedImgId); if (idx >= 0) startIndex = idx; } - currentGalleryData = { slotId: slotId, messageId: messageId, previews: validPreviews, currentIndex: startIndex, callbacks: callbacks }; + currentGalleryData = { slotId, messageId, previews: validPreviews, currentIndex: startIndex, callbacks }; renderGallery(); document.getElementById('nd-gallery-overlay').classList.add('visible'); } export function closeGallery() { - log('closeGallery'); - var el = document.getElementById('nd-gallery-overlay'); + const el = document.getElementById('nd-gallery-overlay'); if (el) el.classList.remove('visible'); currentGalleryData = null; } @@ -693,30 +602,27 @@ export function closeGallery() { function renderGallery() { if (!currentGalleryData) return; - var previews = currentGalleryData.previews; - var currentIndex = currentGalleryData.currentIndex; - var current = previews[currentIndex]; + const { previews, currentIndex } = currentGalleryData; + const current = previews[currentIndex]; if (!current) return; - var img = document.getElementById('nd-gallery-img'); - img.src = current.savedUrl || ('data:image/png;base64,' + current.base64); - + document.getElementById('nd-gallery-img').src = current.savedUrl || `data:image/png;base64,${current.base64}`; document.getElementById('nd-gallery-saved-badge').style.display = current.savedUrl ? 'block' : 'none'; - var reversedPreviews = previews.slice().reverse(); - var thumbsContainer = document.getElementById('nd-gallery-thumbs'); + const reversedPreviews = previews.slice().reverse(); + const thumbsContainer = document.getElementById('nd-gallery-thumbs'); - thumbsContainer.innerHTML = reversedPreviews.map(function(p, i) { - var src = p.savedUrl || ('data:image/png;base64,' + p.base64); - var originalIndex = previews.length - 1 - i; - var classes = ['nd-gallery-thumb']; + thumbsContainer.innerHTML = reversedPreviews.map((p, i) => { + const src = p.savedUrl || `data:image/png;base64,${p.base64}`; + const originalIndex = previews.length - 1 - i; + const classes = ['nd-gallery-thumb']; if (originalIndex === currentIndex) classes.push('active'); if (p.savedUrl) classes.push('saved'); - return ''; + return ``; }).join(''); - thumbsContainer.querySelectorAll('.nd-gallery-thumb').forEach(function(thumb) { - thumb.addEventListener('click', function() { + thumbsContainer.querySelectorAll('.nd-gallery-thumb').forEach(thumb => { + thumb.addEventListener('click', () => { currentGalleryData.currentIndex = parseInt(thumb.dataset.index); renderGallery(); }); @@ -725,23 +631,23 @@ function renderGallery() { document.getElementById('nd-gallery-prev').disabled = currentIndex >= previews.length - 1; document.getElementById('nd-gallery-next').disabled = currentIndex <= 0; - var saveBtn = document.getElementById('nd-gallery-save'); + const saveBtn = document.getElementById('nd-gallery-save'); if (current.savedUrl) { - saveBtn.textContent = '\u2713 \u5DF2\u4FDD\u5B58'; + saveBtn.textContent = '✓ 已保存'; saveBtn.disabled = true; } else { - saveBtn.textContent = '\uD83D\uDCBE \u4FDD\u5B58\u5230\u670D\u52A1\u5668'; + saveBtn.textContent = '💾 保存到服务器'; saveBtn.disabled = false; } - var displayVersion = previews.length - currentIndex; - var date = new Date(current.timestamp).toLocaleString(); - document.getElementById('nd-gallery-info').textContent = '\u7248\u672C ' + displayVersion + ' / ' + previews.length + ' \u00B7 ' + date; + const displayVersion = previews.length - currentIndex; + const date = new Date(current.timestamp).toLocaleString(); + document.getElementById('nd-gallery-info').textContent = `版本 ${displayVersion} / ${previews.length} · ${date}`; } function navigateGallery(delta) { if (!currentGalleryData) return; - var newIndex = currentGalleryData.currentIndex - delta; + const newIndex = currentGalleryData.currentIndex - delta; if (newIndex >= 0 && newIndex < currentGalleryData.previews.length) { currentGalleryData.currentIndex = newIndex; renderGallery(); @@ -750,70 +656,53 @@ function navigateGallery(delta) { async function useCurrentGalleryImage() { if (!currentGalleryData) return; - log('useCurrentGalleryImage'); - var slotId = currentGalleryData.slotId; - var messageId = currentGalleryData.messageId; - var previews = currentGalleryData.previews; - var currentIndex = currentGalleryData.currentIndex; - var callbacks = currentGalleryData.callbacks; - var selected = previews[currentIndex]; + const { slotId, messageId, previews, currentIndex, callbacks } = currentGalleryData; + const selected = previews[currentIndex]; if (!selected) return; await setSlotSelection(slotId, selected.imgId); - if (callbacks.onUse) callbacks.onUse(slotId, messageId, selected, previews.length); closeGallery(); - showToast('\u5DF2\u5207\u6362\u663E\u793A\u56FE\u7247'); + showToast('已切换显示图片'); } async function saveCurrentGalleryImage() { if (!currentGalleryData) return; - log('saveCurrentGalleryImage'); - var slotId = currentGalleryData.slotId; - var previews = currentGalleryData.previews; - var currentIndex = currentGalleryData.currentIndex; - var callbacks = currentGalleryData.callbacks; - var current = previews[currentIndex]; + const { slotId, previews, currentIndex, callbacks } = currentGalleryData; + const current = previews[currentIndex]; if (!current || current.savedUrl) return; try { - var charName = current.characterName || getChatCharacterName(); - var url = await saveBase64AsFile(current.base64, charName, 'novel_' + current.imgId, 'png'); + const charName = current.characterName || getChatCharacterName(); + const url = await saveBase64AsFile(current.base64, charName, `novel_${current.imgId}`, 'png'); await updatePreviewSavedUrl(current.imgId, url); current.savedUrl = url; - await setSlotSelection(slotId, current.imgId); - - showToast('\u5DF2\u4FDD\u5B58: ' + url, 'success', 4000); + showToast(`已保存: ${url}`, 'success', 4000); renderGallery(); if (callbacks.onSave) callbacks.onSave(current.imgId, url); } catch (e) { console.error('[GalleryCache] save failed:', e); - showToast('\u4FDD\u5B58\u5931\u8D25: ' + e.message, 'error'); + showToast(`保存失败: ${e.message}`, 'error'); } } async function deleteCurrentGalleryImage() { if (!currentGalleryData) return; - log('deleteCurrentGalleryImage'); - var slotId = currentGalleryData.slotId; - var messageId = currentGalleryData.messageId; - var previews = currentGalleryData.previews; - var currentIndex = currentGalleryData.currentIndex; - var callbacks = currentGalleryData.callbacks; - var current = previews[currentIndex]; + const { slotId, messageId, previews, currentIndex, callbacks } = currentGalleryData; + const current = previews[currentIndex]; if (!current) return; - var msg = current.savedUrl ? '\u786E\u5B9A\u5220\u9664\u8FD9\u6761\u8BB0\u5F55\u5417\uFF1F\u670D\u52A1\u5668\u4E0A\u7684\u56FE\u7247\u6587\u4EF6\u4E0D\u4F1A\u88AB\u5220\u9664\u3002' : '\u786E\u5B9A\u5220\u9664\u8FD9\u5F20\u56FE\u7247\u5417\uFF1F'; + const msg = current.savedUrl ? '确定删除这条记录吗?服务器上的图片文件不会被删除。' : '确定删除这张图片吗?'; if (!confirm(msg)) return; try { await deletePreview(current.imgId); - var selectedId = await getSlotSelection(slotId); + const selectedId = await getSlotSelection(slotId); if (selectedId === current.imgId) { await clearSlotSelection(slotId); } @@ -822,25 +711,21 @@ async function deleteCurrentGalleryImage() { if (previews.length === 0) { closeGallery(); - if (callbacks.onBecameEmpty) { - callbacks.onBecameEmpty(slotId, messageId, { - tags: current.tags || '', - positive: current.positive || '' - }); + callbacks.onBecameEmpty(slotId, messageId, { tags: current.tags || '', positive: current.positive || '' }); } - showToast('\u56FE\u7247\u5DF2\u5220\u9664\uFF0C\u53EF\u70B9\u51FB\u91CD\u8BD5\u91CD\u65B0\u751F\u6210'); + showToast('图片已删除,可点击重试重新生成'); } else { if (currentGalleryData.currentIndex >= previews.length) { currentGalleryData.currentIndex = previews.length - 1; } renderGallery(); if (callbacks.onDelete) callbacks.onDelete(slotId, current.imgId, previews); - showToast('\u56FE\u7247\u5DF2\u5220\u9664'); + showToast('图片已删除'); } } catch (e) { console.error('[GalleryCache] delete failed:', e); - showToast('\u5220\u9664\u5931\u8D25: ' + e.message, 'error'); + showToast(`删除失败: ${e.message}`, 'error'); } } @@ -849,21 +734,15 @@ async function deleteCurrentGalleryImage() { // ═══════════════════════════════════════════════════════════════════════════ export function destroyGalleryCache() { - log('destroyGalleryCache called'); - console.trace('destroyGalleryCache trace'); closeGallery(); - var el1 = document.getElementById('nd-gallery-overlay'); - if (el1) el1.remove(); - var el2 = document.getElementById('nd-gallery-styles'); - if (el2) el2.remove(); + invalidateCache(); + + document.getElementById('nd-gallery-overlay')?.remove(); + document.getElementById('nd-gallery-styles')?.remove(); galleryOverlayCreated = false; + if (db) { - try { - db.close(); - log('destroyGalleryCache: closed db'); - } catch (e) { - log('destroyGalleryCache: close error', e.message); - } + try { db.close(); } catch {} db = null; } dbOpening = null; diff --git a/modules/novel-draw/novel-draw.js b/modules/novel-draw/novel-draw.js index ee15532..f3a5cea 100644 --- a/modules/novel-draw/novel-draw.js +++ b/modules/novel-draw/novel-draw.js @@ -1545,7 +1545,74 @@ async function removePlaceholder(container) { container.remove(); showToast('占位符已移除'); } - +// ═══════════════════════════════════════════════════════════════════════════ +// 图片懒加载 +// ═══════════════════════════════════════════════════════════════════════════ +let slotObserver = null; +function initSlotObserver() { + if (slotObserver) return; + + slotObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (!entry.isIntersecting) return; + const slot = entry.target; + if (slot.dataset.loaded === '1' || slot.dataset.loading === '1') return; + slot.dataset.loading = '1'; + loadSlotImage(slot); + }); + }, { rootMargin: '200px 0px', threshold: 0.01 }); +} +async function loadSlotImage(slot) { + const slotId = slot.dataset.slotId; + const messageId = parseInt(slot.dataset.mesid); + + try { + const displayData = await getDisplayPreviewForSlot(slotId); + + if (displayData.isFailed) { + slot.outerHTML = buildFailedPlaceholderHtml({ + slotId, messageId, + tags: displayData.failedInfo?.tags || '', + positive: displayData.failedInfo?.positive || '', + errorType: displayData.failedInfo?.errorType || ErrorType.CACHE_LOST.label, + errorMessage: displayData.failedInfo?.errorMessage || ErrorType.CACHE_LOST.desc + }); + } else if (displayData.hasData && displayData.preview) { + const url = displayData.preview.savedUrl || `data:image/png;base64,${displayData.preview.base64}`; + slot.outerHTML = buildImageHtml({ + slotId, + imgId: displayData.preview.imgId, + url, + tags: displayData.preview.tags, + positive: displayData.preview.positive, + messageId, + state: displayData.preview.savedUrl ? ImageState.SAVED : ImageState.PREVIEW, + historyCount: displayData.historyCount, + currentIndex: 0 + }); + } else { + slot.outerHTML = buildFailedPlaceholderHtml({ + slotId, messageId, tags: '', positive: '', + errorType: ErrorType.CACHE_LOST.label, + errorMessage: ErrorType.CACHE_LOST.desc + }); + } + } catch (e) { + slot.dataset.loading = ''; + } +} +function buildLoadingPlaceholderHtml(slotId, messageId) { + return `
+
📷 滚动加载
+
`; +} +function hydrateSlots(container) { + initSlotObserver(); + container.querySelectorAll('.xb-nd-loading-slot:not([data-observed])').forEach(slot => { + slot.dataset.observed = '1'; + slotObserver.observe(slot); + }); +} // ═══════════════════════════════════════════════════════════════════════════ // 预览渲染 // ═══════════════════════════════════════════════════════════════════════════ @@ -1560,59 +1627,25 @@ async function renderPreviewsForMessage(messageId) { const $mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`); if (!$mesText.length) return; + let html = $mesText.html(); let replaced = false; for (const slotId of slotIds) { if (html.includes(`data-slot-id="${slotId}"`)) continue; - const displayData = await getDisplayPreviewForSlot(slotId); const placeholder = createPlaceholder(slotId); const escapedPlaceholder = placeholder.replace(/[[\]]/g, '\\$&'); if (!new RegExp(escapedPlaceholder).test(html)) continue; - let imgHtml; - if (displayData.isFailed) { - imgHtml = buildFailedPlaceholderHtml({ - slotId, - messageId, - tags: displayData.failedInfo?.tags || '', - positive: displayData.failedInfo?.positive || '', - errorType: displayData.failedInfo?.errorType || ErrorType.CACHE_LOST.label, - errorMessage: displayData.failedInfo?.errorMessage || ErrorType.CACHE_LOST.desc - }); - } else if (displayData.hasData && displayData.preview) { - const url = displayData.preview.savedUrl || `data:image/png;base64,${displayData.preview.base64}`; - const allPreviews = await getPreviewsBySlot(slotId); - const successPreviews = allPreviews.filter(p => p.status !== 'failed' && p.base64); - const currentIndex = successPreviews.findIndex(p => p.imgId === displayData.preview.imgId); - imgHtml = buildImageHtml({ - slotId, - imgId: displayData.preview.imgId, - url, - tags: displayData.preview.tags, - positive: displayData.preview.positive, - messageId, - state: displayData.preview.savedUrl ? ImageState.SAVED : ImageState.PREVIEW, - historyCount: displayData.historyCount, - currentIndex: currentIndex >= 0 ? currentIndex : 0 - }); - } else { - imgHtml = buildFailedPlaceholderHtml({ - slotId, - messageId, - tags: '', - positive: '', - errorType: ErrorType.CACHE_LOST.label, - errorMessage: ErrorType.CACHE_LOST.desc - }); - } - html = html.replace(new RegExp(escapedPlaceholder, 'g'), imgHtml); + const loadingHtml = buildLoadingPlaceholderHtml(slotId, messageId); + html = html.replace(new RegExp(escapedPlaceholder, 'g'), loadingHtml); replaced = true; } if (replaced && !isMessageBeingEdited(messageId)) { $mesText.html(html); + hydrateSlots($mesText[0]); } } @@ -1620,7 +1653,9 @@ async function renderAllPreviews() { const ctx = getContext(); const chat = ctx.chat || []; for (let i = 0; i < chat.length; i++) { - if (extractSlotIds(chat[i]?.mes).size > 0) await renderPreviewsForMessage(i); + if (extractSlotIds(chat[i]?.mes).size > 0) { + await renderPreviewsForMessage(i); + } } } @@ -1630,7 +1665,7 @@ async function handleMessageRendered(data) { } async function handleChatChanged() { - await new Promise(r => setTimeout(r, 200)); + await new Promise(r => setTimeout(r, 50)); await renderAllPreviews(); } @@ -1642,6 +1677,20 @@ async function handleMessageModified(data) { await renderPreviewsForMessage(messageId); } +function handleVisibilityChange() { + if (document.visibilityState === 'visible' && moduleInitialized) { + document.querySelectorAll('.xb-nd-loading-slot[data-observed="1"]').forEach(slot => { + if (slot.dataset.loaded !== '1' && slot.dataset.loading !== '1') { + const rect = slot.getBoundingClientRect(); + if (rect.bottom >= 0 && rect.top <= window.innerHeight + 200) { + slot.dataset.loading = '1'; + loadSlotImage(slot); + } + } + }); + } +} + // ═══════════════════════════════════════════════════════════════════════════ // 多图生成 // ═══════════════════════════════════════════════════════════════════════════ @@ -2375,6 +2424,8 @@ export async function initNovelDraw() { events.on(event_types.MESSAGE_SWIPED, handleMessageModified); events.on(event_types.GENERATION_ENDED, async () => { try { await autoGenerateForLastAI(); } catch (e) { console.error('[NovelDraw]', e); } }); + document.addEventListener('visibilitychange', handleVisibilityChange); + window.xiaobaixNovelDraw = { getSettings, saveSettings, @@ -2416,7 +2467,14 @@ export async function cleanupNovelDraw() { destroyGalleryCache(); overlayCreated = false; frameReady = false; + + if (slotObserver) { + slotObserver.disconnect(); + slotObserver = null; + } + window.removeEventListener('message', handleFrameMessage); + document.removeEventListener('visibilitychange', handleVisibilityChange); document.getElementById('xiaobaix-novel-draw-overlay')?.remove(); const { destroyFloatingPanel } = await import('./floating-panel.js');