From d716d34dabe1d816a9cf6b05182e0be06e007a71 Mon Sep 17 00:00:00 2001 From: X Date: Mon, 16 Feb 2026 09:08:00 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E5=82=B3=E6=AA=94=E6=A1=88=E5=88=B0?= =?UTF-8?q?=E3=80=8Cmodules/story-summary=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/story-summary-a.css | 4 + modules/story-summary/story-summary-ui.js | 54 +++++++++- modules/story-summary/story-summary.css | 4 + modules/story-summary/story-summary.html | 22 ++++ modules/story-summary/story-summary.js | 123 ++++++++++++---------- 5 files changed, 149 insertions(+), 58 deletions(-) diff --git a/modules/story-summary/story-summary-a.css b/modules/story-summary/story-summary-a.css index 83157dc..8db28eb 100644 --- a/modules/story-summary/story-summary-a.css +++ b/modules/story-summary/story-summary-a.css @@ -21,6 +21,10 @@ padding-right: 4px; } +.confirm-modal-box { + max-width: 440px; +} + .fact-group { margin-bottom: 12px; } diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index 7cff296..6a4d5cb 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -358,8 +358,8 @@ postMsg('ANCHOR_GENERATE'); }; - $('btn-anchor-clear').onclick = () => { - if (confirm('清空所有记忆锚点?(L0 向量也会一并清除)')) { + $('btn-anchor-clear').onclick = async () => { + if (await showConfirm('清空锚点', '清空所有记忆锚点?(L0 向量也会一并清除)')) { postMsg('ANCHOR_CLEAR'); } }; @@ -375,6 +375,7 @@ }; $('btn-test-vector-api').onclick = () => { + saveConfig(); // 先保存新 Key 到 localStorage postMsg('VECTOR_TEST_ONLINE', { provider: 'siliconflow', config: { @@ -391,8 +392,10 @@ postMsg('VECTOR_GENERATE', { config: getVectorConfig() }); }; - $('btn-clear-vectors').onclick = () => { - if (confirm('确定清空所有向量数据?')) postMsg('VECTOR_CLEAR'); + $('btn-clear-vectors').onclick = async () => { + if (await showConfirm('清空向量', '确定清空所有向量数据?')) { + postMsg('VECTOR_CLEAR'); + } }; $('btn-cancel-vectors').onclick = () => postMsg('VECTOR_CANCEL_GENERATE'); @@ -955,6 +958,43 @@ postMsg('FULLSCREEN_CLOSED'); } + /** + * 显示通用确认弹窗 + * @returns {Promise} + */ + function showConfirm(title, message, okText = '执行', cancelText = '取消') { + return new Promise(resolve => { + const modal = $('confirm-modal'); + const titleEl = $('confirm-title'); + const msgEl = $('confirm-message'); + const okBtn = $('confirm-ok'); + const cancelBtn = $('confirm-cancel'); + const closeBtn = $('confirm-close'); + const backdrop = $('confirm-backdrop'); + + titleEl.textContent = title; + msgEl.textContent = message; + okBtn.textContent = okText; + cancelBtn.textContent = cancelText; + + const close = (result) => { + modal.classList.remove('active'); + okBtn.onclick = null; + cancelBtn.onclick = null; + closeBtn.onclick = null; + backdrop.onclick = null; + resolve(result); + }; + + okBtn.onclick = () => close(true); + cancelBtn.onclick = () => close(false); + closeBtn.onclick = () => close(false); + backdrop.onclick = () => close(false); + + modal.classList.add('active'); + }); + } + function renderArcsEditor(arcs) { const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }]; const es = $('editor-struct'); @@ -1526,7 +1566,11 @@ }; // Main actions - $('btn-clear').onclick = () => postMsg('REQUEST_CLEAR'); + $('btn-clear').onclick = async () => { + if (await showConfirm('清空数据', '确定要清空本聊天的所有总结、关键词及人物关系数据吗?此操作不可撤销。')) { + postMsg('REQUEST_CLEAR'); + } + }; $('btn-generate').onclick = () => { const btn = $('btn-generate'); if (!localGenerating) { diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css index 3e26a78..a7d8b2b 100644 --- a/modules/story-summary/story-summary.css +++ b/modules/story-summary/story-summary.css @@ -20,6 +20,10 @@ padding-right: 4px; } +.confirm-modal-box { + max-width: 440px; +} + .fact-group { margin-bottom: 12px; } diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index bf666ef..4e92395 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -833,6 +833,28 @@ + + diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index eca1617..30f29f9 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -449,6 +449,34 @@ async function handleGenerateVectors(vectorCfg) { await clearStateVectors(chatId); await updateMeta(chatId, { lastChunkFloor: -1, fingerprint }); + // Helper to embed with retry + const embedWithRetry = async (texts, phase, currentBatchIdx, totalItems) => { + while (true) { + if (vectorCancelled) return null; + try { + return await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); + } catch (e) { + if (e?.name === "AbortError" || vectorCancelled) return null; + xbLog.error(MODULE_ID, `${phase} 向量化单次失败`, e); + + // 等待 60 秒重试 + const waitSec = 60; + for (let s = waitSec; s > 0; s--) { + if (vectorCancelled) return null; + postToFrame({ + type: "VECTOR_GEN_PROGRESS", + phase, + current: currentBatchIdx, + total: totalItems, + message: `触发限流,${s}s 后重试...` + }); + await new Promise(r => setTimeout(r, 1000)); + } + postToFrame({ type: "VECTOR_GEN_PROGRESS", phase, current: currentBatchIdx, total: totalItems, message: "正在重试..." }); + } + } + }; + const atoms = getStateAtoms(); if (!atoms.length) { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: 0, total: 0, message: "L0 为空,跳过" }); @@ -462,29 +490,26 @@ async function handleGenerateVectors(vectorCfg) { const batch = atoms.slice(i, i + batchSize); const semTexts = batch.map(a => a.semantic); const rTexts = batch.map(a => buildRAggregateText(a)); - try { - const vectors = await embed(semTexts.concat(rTexts), vectorCfg, { signal: vectorAbortController.signal }); - const split = semTexts.length; - if (!Array.isArray(vectors) || vectors.length < split * 2) { - throw new Error(`embed length mismatch: expect>=${split * 2}, got=${vectors?.length || 0}`); - } - const semVectors = vectors.slice(0, split); - const rVectors = vectors.slice(split, split + split); - const items = batch.map((a, j) => ({ - atomId: a.atomId, - floor: a.floor, - vector: semVectors[j], - rVector: rVectors[j] || semVectors[j], - })); - await saveStateVectors(chatId, items, fingerprint); - l0Completed += batch.length; - postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: l0Completed, total: atoms.length }); - } catch (e) { - if (e?.name === "AbortError") break; - xbLog.error(MODULE_ID, "L0 向量化失败", e); - vectorCancelled = true; - break; + + const vectors = await embedWithRetry(semTexts.concat(rTexts), "L0", l0Completed, atoms.length); + if (!vectors) break; // cancelled + + const split = semTexts.length; + if (!Array.isArray(vectors) || vectors.length < split * 2) { + xbLog.error(MODULE_ID, `embed长度不匹配: expect>=${split * 2}, got=${vectors?.length || 0}`); + continue; } + const semVectors = vectors.slice(0, split); + const rVectors = vectors.slice(split, split + split); + const items = batch.map((a, j) => ({ + atomId: a.atomId, + floor: a.floor, + vector: semVectors[j], + rVector: rVectors[j] || semVectors[j], + })); + await saveStateVectors(chatId, items, fingerprint); + l0Completed += batch.length; + postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: l0Completed, total: atoms.length }); } } @@ -516,22 +541,18 @@ async function handleGenerateVectors(vectorCfg) { const batch = allChunks.slice(i, i + batchSize); const texts = batch.map(c => c.text); - try { - const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); - const items = batch.map((c, j) => ({ - chunkId: c.chunkId, - vector: vectors[j], - })); - await saveChunkVectors(chatId, items, fingerprint); - l1Vectors = l1Vectors.concat(items); - l1Completed += batch.length; - postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: l1Completed, total: allChunks.length }); - } catch (e) { - if (e?.name === "AbortError") break; - xbLog.error(MODULE_ID, "L1 向量化失败", e); - vectorCancelled = true; - break; - } + + const vectors = await embedWithRetry(texts, "L1", l1Completed, allChunks.length); + if (!vectors) break; // cancelled + + const items = batch.map((c, j) => ({ + chunkId: c.chunkId, + vector: vectors[j], + })); + await saveChunkVectors(chatId, items, fingerprint); + l1Vectors = l1Vectors.concat(items); + l1Completed += batch.length; + postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: l1Completed, total: allChunks.length }); } } @@ -555,21 +576,17 @@ async function handleGenerateVectors(vectorCfg) { const batch = l2Pairs.slice(i, i + batchSize); const texts = batch.map(p => p.text); - try { - const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); - const items = batch.map((p, idx) => ({ - eventId: p.id, - vector: vectors[idx], - })); - await saveEventVectorsToDb(chatId, items, fingerprint); - l2Completed += batch.length; - postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Pairs.length }); - } catch (e) { - if (e?.name === "AbortError") break; - xbLog.error(MODULE_ID, "L2 向量化失败", e); - vectorCancelled = true; - break; - } + + const vectors = await embedWithRetry(texts, "L2", l2Completed, l2Pairs.length); + if (!vectors) break; // cancelled + + const items = batch.map((p, idx) => ({ + eventId: p.id, + vector: vectors[idx], + })); + await saveEventVectorsToDb(chatId, items, fingerprint); + l2Completed += batch.length; + postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Pairs.length }); } }