上傳檔案到「modules/story-summary」

This commit is contained in:
X
2026-02-16 09:08:00 +00:00
parent dad51478f4
commit d716d34dab
5 changed files with 149 additions and 58 deletions

View File

@@ -21,6 +21,10 @@
padding-right: 4px; padding-right: 4px;
} }
.confirm-modal-box {
max-width: 440px;
}
.fact-group { .fact-group {
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@@ -358,8 +358,8 @@
postMsg('ANCHOR_GENERATE'); postMsg('ANCHOR_GENERATE');
}; };
$('btn-anchor-clear').onclick = () => { $('btn-anchor-clear').onclick = async () => {
if (confirm('清空所有记忆锚点L0 向量也会一并清除)')) { if (await showConfirm('清空锚点', '清空所有记忆锚点L0 向量也会一并清除)')) {
postMsg('ANCHOR_CLEAR'); postMsg('ANCHOR_CLEAR');
} }
}; };
@@ -375,6 +375,7 @@
}; };
$('btn-test-vector-api').onclick = () => { $('btn-test-vector-api').onclick = () => {
saveConfig(); // 先保存新 Key 到 localStorage
postMsg('VECTOR_TEST_ONLINE', { postMsg('VECTOR_TEST_ONLINE', {
provider: 'siliconflow', provider: 'siliconflow',
config: { config: {
@@ -391,8 +392,10 @@
postMsg('VECTOR_GENERATE', { config: getVectorConfig() }); postMsg('VECTOR_GENERATE', { config: getVectorConfig() });
}; };
$('btn-clear-vectors').onclick = () => { $('btn-clear-vectors').onclick = async () => {
if (confirm('确定清空所有向量数据?')) postMsg('VECTOR_CLEAR'); if (await showConfirm('清空向量', '确定清空所有向量数据?')) {
postMsg('VECTOR_CLEAR');
}
}; };
$('btn-cancel-vectors').onclick = () => postMsg('VECTOR_CANCEL_GENERATE'); $('btn-cancel-vectors').onclick = () => postMsg('VECTOR_CANCEL_GENERATE');
@@ -955,6 +958,43 @@
postMsg('FULLSCREEN_CLOSED'); postMsg('FULLSCREEN_CLOSED');
} }
/**
* 显示通用确认弹窗
* @returns {Promise<boolean>}
*/
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) { function renderArcsEditor(arcs) {
const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }]; const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }];
const es = $('editor-struct'); const es = $('editor-struct');
@@ -1526,7 +1566,11 @@
}; };
// Main actions // Main actions
$('btn-clear').onclick = () => postMsg('REQUEST_CLEAR'); $('btn-clear').onclick = async () => {
if (await showConfirm('清空数据', '确定要清空本聊天的所有总结、关键词及人物关系数据吗?此操作不可撤销。')) {
postMsg('REQUEST_CLEAR');
}
};
$('btn-generate').onclick = () => { $('btn-generate').onclick = () => {
const btn = $('btn-generate'); const btn = $('btn-generate');
if (!localGenerating) { if (!localGenerating) {

View File

@@ -20,6 +20,10 @@
padding-right: 4px; padding-right: 4px;
} }
.confirm-modal-box {
max-width: 440px;
}
.fact-group { .fact-group {
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@@ -833,6 +833,28 @@
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="story-summary-ui.js"></script> <script src="story-summary-ui.js"></script>
<!-- Confirm Modal -->
<div class="modal" id="confirm-modal">
<div class="modal-bg" id="confirm-backdrop"></div>
<div class="modal-box confirm-modal-box">
<div class="modal-head">
<h2 id="confirm-title">确认操作</h2>
<button class="modal-close" id="confirm-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<div class="modal-body">
<div id="confirm-message" style="margin: 10px 0; line-height: 1.6; color: var(--fg);">内容</div>
</div>
<div class="modal-foot">
<button class="btn" id="confirm-cancel">取消</button>
<button class="btn btn-del" id="confirm-ok">执行</button>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -449,6 +449,34 @@ async function handleGenerateVectors(vectorCfg) {
await clearStateVectors(chatId); await clearStateVectors(chatId);
await updateMeta(chatId, { lastChunkFloor: -1, fingerprint }); 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(); const atoms = getStateAtoms();
if (!atoms.length) { if (!atoms.length) {
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: 0, total: 0, message: "L0 为空,跳过" }); postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: 0, total: 0, message: "L0 为空,跳过" });
@@ -462,11 +490,14 @@ async function handleGenerateVectors(vectorCfg) {
const batch = atoms.slice(i, i + batchSize); const batch = atoms.slice(i, i + batchSize);
const semTexts = batch.map(a => a.semantic); const semTexts = batch.map(a => a.semantic);
const rTexts = batch.map(a => buildRAggregateText(a)); const rTexts = batch.map(a => buildRAggregateText(a));
try {
const vectors = await embed(semTexts.concat(rTexts), vectorCfg, { signal: vectorAbortController.signal }); const vectors = await embedWithRetry(semTexts.concat(rTexts), "L0", l0Completed, atoms.length);
if (!vectors) break; // cancelled
const split = semTexts.length; const split = semTexts.length;
if (!Array.isArray(vectors) || vectors.length < split * 2) { if (!Array.isArray(vectors) || vectors.length < split * 2) {
throw new Error(`embed length mismatch: expect>=${split * 2}, got=${vectors?.length || 0}`); xbLog.error(MODULE_ID, `embed长度不匹配: expect>=${split * 2}, got=${vectors?.length || 0}`);
continue;
} }
const semVectors = vectors.slice(0, split); const semVectors = vectors.slice(0, split);
const rVectors = vectors.slice(split, split + split); const rVectors = vectors.slice(split, split + split);
@@ -479,12 +510,6 @@ async function handleGenerateVectors(vectorCfg) {
await saveStateVectors(chatId, items, fingerprint); await saveStateVectors(chatId, items, fingerprint);
l0Completed += batch.length; l0Completed += batch.length;
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: l0Completed, total: atoms.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;
}
} }
} }
@@ -516,8 +541,10 @@ async function handleGenerateVectors(vectorCfg) {
const batch = allChunks.slice(i, i + batchSize); const batch = allChunks.slice(i, i + batchSize);
const texts = batch.map(c => c.text); const texts = batch.map(c => c.text);
try {
const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); const vectors = await embedWithRetry(texts, "L1", l1Completed, allChunks.length);
if (!vectors) break; // cancelled
const items = batch.map((c, j) => ({ const items = batch.map((c, j) => ({
chunkId: c.chunkId, chunkId: c.chunkId,
vector: vectors[j], vector: vectors[j],
@@ -526,12 +553,6 @@ async function handleGenerateVectors(vectorCfg) {
l1Vectors = l1Vectors.concat(items); l1Vectors = l1Vectors.concat(items);
l1Completed += batch.length; l1Completed += batch.length;
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: l1Completed, total: allChunks.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;
}
} }
} }
@@ -555,8 +576,10 @@ async function handleGenerateVectors(vectorCfg) {
const batch = l2Pairs.slice(i, i + batchSize); const batch = l2Pairs.slice(i, i + batchSize);
const texts = batch.map(p => p.text); const texts = batch.map(p => p.text);
try {
const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); const vectors = await embedWithRetry(texts, "L2", l2Completed, l2Pairs.length);
if (!vectors) break; // cancelled
const items = batch.map((p, idx) => ({ const items = batch.map((p, idx) => ({
eventId: p.id, eventId: p.id,
vector: vectors[idx], vector: vectors[idx],
@@ -564,12 +587,6 @@ async function handleGenerateVectors(vectorCfg) {
await saveEventVectorsToDb(chatId, items, fingerprint); await saveEventVectorsToDb(chatId, items, fingerprint);
l2Completed += batch.length; l2Completed += batch.length;
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Pairs.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;
}
} }
} }