+
API 配置
+
+
+
+
-
手机/低配适用
-
-
- 检查中...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 未测试
-
-
-
-
-
- 💡
硅基流动 免费、速度快、质量好,推荐 BAAI/bge-m3
-
-
-
-
-
+
-
-
- 遇到「起始」后跳过,直到「结束」。起始或结束可单独留空。用于过滤思考标签等干扰内容。
-
-
-
+
+
+
默认端点:OpenAI:/v1,Gemini:/v1beta,Claude:/v1
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- 事件向量: 0/0
- ·
- Chunks: 0 个(0/0 层)
- ·
- 消息: 0
-
-
- ⚠ 引擎/模型已变更,需重新生成向量
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
首次生成向量可能耗时较久,页面短暂卡顿属正常。若本地模型重进酒馆后需重下。
-
-
-
+
-
-
-
导出/导入均为 zip 格式,勿解压
-
-
+
+
+
+
+
总结设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
智能记忆(向量检索)
+
+
+
+
+
+
+
+
+
+
+
+
手机/低配适用
+
+
+
+ 检查中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💡
硅基流动 免费、速度快、质量好,推荐
+ BAAI/bge-m3
+
+
+
+
+
+
+
过滤干扰内容(如思考标签):遇到「起始」跳过直到「结束」
+
+
+
+
+
+
+
+
+
+
+ 事件向量:
+ 0/0
+
+
·
+
+ Chunks:
+ 0
+ 个(0/0 层)
+
+
·
+
+ 消息:
+ 0
+
+
+
+ ⚠ 引擎/模型已变更,需重新生成向量
+
+
+
+
+
+
+
+
+
+
+
+
+
首次生成向量可能耗时较久,页面短暂卡顿属正常。若本地模型重进酒馆后需重下。
+
+
+
+
+
+
+
+
导出/导入均为 zip 格式,勿解压
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -500,7 +586,8 @@
🤗 Hugging Face Space 部署指南
@@ -508,25 +595,10 @@
-
-
-
-
-
-
✨ 涌现 · 记忆召回日志
-
-
-
-
-
+
+
diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js
index 71e0c90..9b01842 100644
--- a/modules/story-summary/story-summary.js
+++ b/modules/story-summary/story-summary.js
@@ -60,6 +60,7 @@ import {
updateMeta,
saveEventVectors as saveEventVectorsToDb,
clearEventVectors,
+ deleteEventVectorsByIds,
clearAllChunks,
saveChunks,
saveChunkVectors,
@@ -506,6 +507,91 @@ async function handleGenerateVectors(vectorCfg) {
xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`);
}
+// ═══════════════════════════════════════════════════════════════════════════
+// L2 自动增量向量化(总结完成后调用)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function autoVectorizeNewEvents(newEventIds) {
+ if (!newEventIds?.length) return;
+
+ const vectorCfg = getVectorConfig();
+ if (!vectorCfg?.enabled) return;
+
+ const { chatId } = getContext();
+ if (!chatId) return;
+
+ // 本地模型未加载时跳过(不阻塞总结流程)
+ if (vectorCfg.engine === "local") {
+ const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
+ if (!isLocalModelLoaded(modelId)) {
+ xbLog.warn(MODULE_ID, "L2 自动向量化跳过:本地模型未加载");
+ return;
+ }
+ }
+
+ const store = getSummaryStore();
+ const events = store?.json?.events || [];
+ const newEventIdSet = new Set(newEventIds);
+
+ // 只取本次新增的 events
+ const newEvents = events.filter((e) => newEventIdSet.has(e.id));
+ if (!newEvents.length) return;
+
+ const pairs = newEvents
+ .map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() }))
+ .filter((p) => p.text);
+
+ if (!pairs.length) return;
+
+ try {
+ const fingerprint = getEngineFingerprint(vectorCfg);
+ const batchSize = vectorCfg.engine === "local" ? 5 : 25;
+
+ for (let i = 0; i < pairs.length; i += batchSize) {
+ const batch = pairs.slice(i, i + batchSize);
+ const texts = batch.map((p) => p.text);
+
+ const vectors = await embed(texts, vectorCfg);
+ const items = batch.map((p, idx) => ({
+ eventId: p.id,
+ vector: vectors[idx],
+ }));
+
+ await saveEventVectorsToDb(chatId, items, fingerprint);
+ }
+
+ xbLog.info(MODULE_ID, `L2 自动增量完成: ${pairs.length} 个事件`);
+ await sendVectorStatsToFrame();
+ } catch (e) {
+ xbLog.error(MODULE_ID, "L2 自动向量化失败", e);
+ // 不抛出,不阻塞总结流程
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// L2 跟随编辑同步(用户编辑 events 时调用)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function syncEventVectorsOnEdit(oldEvents, newEvents) {
+ const vectorCfg = getVectorConfig();
+ if (!vectorCfg?.enabled) return;
+
+ const { chatId } = getContext();
+ if (!chatId) return;
+
+ const oldIds = new Set((oldEvents || []).map((e) => e.id).filter(Boolean));
+ const newIds = new Set((newEvents || []).map((e) => e.id).filter(Boolean));
+
+ // 找出被删除的 eventIds
+ const deletedIds = [...oldIds].filter((id) => !newIds.has(id));
+
+ if (deletedIds.length > 0) {
+ await deleteEventVectorsByIds(chatId, deletedIds);
+ xbLog.info(MODULE_ID, `L2 同步删除: ${deletedIds.length} 个事件向量`);
+ await sendVectorStatsToFrame();
+ }
+}
+
// ═══════════════════════════════════════════════════════════════════════════
// 向量完整性检测(仅提醒,不自动操作)
// ═══════════════════════════════════════════════════════════════════════════
@@ -565,6 +651,7 @@ async function handleClearVectors() {
await clearAllChunks(chatId);
await updateMeta(chatId, { lastChunkFloor: -1 });
await sendVectorStatsToFrame();
+ await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。');
xbLog.info(MODULE_ID, "向量数据已清除");
}
@@ -769,6 +856,11 @@ function openPanelForMessage(mesId) {
// ═══════════════════════════════════════════════════════════════════════════
async function getHideBoundaryFloor(store) {
+ // 没有总结时,不隐藏
+ if (store?.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
+ return -1;
+ }
+
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) {
return store?.lastSummarizedMesId ?? -1;
@@ -845,7 +937,7 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
const result = await runSummaryGeneration(targetMesId, configForRun, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
- onComplete: ({ merged, endMesId }) => {
+ onComplete: async ({ merged, endMesId, newEventIds }) => {
postToFrame({
type: "SUMMARY_FULL_DATA",
payload: {
@@ -860,6 +952,9 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
applyHideStateDebounced();
updateFrameStatsAfterSummary(endMesId, merged);
+
+ // L2 自动增量向量化
+ await autoVectorizeNewEvents(newEventIds);
},
});
@@ -1060,11 +1155,20 @@ function handleFrameMessage(event) {
const store = getSummaryStore();
if (!store) break;
store.json ||= {};
+
+ // 如果是 events,先记录旧数据用于同步向量
+ const oldEvents = data.section === "events" ? [...(store.json.events || [])] : null;
+
if (VALID_SECTIONS.includes(data.section)) {
store.json[data.section] = data.data;
}
store.updatedAt = Date.now();
saveSummaryStore();
+
+ // 同步 L2 向量(删除被移除的事件)
+ if (data.section === "events" && oldEvents) {
+ syncEventVectorsOnEdit(oldEvents, data.data);
+ }
break;
}
@@ -1133,7 +1237,7 @@ async function handleManualGenerate(mesId, config) {
await runSummaryGeneration(mesId, config, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
- onComplete: ({ merged, endMesId }) => {
+ onComplete: async ({ merged, endMesId, newEventIds }) => {
postToFrame({
type: "SUMMARY_FULL_DATA",
payload: {
@@ -1148,6 +1252,9 @@ async function handleManualGenerate(mesId, config) {
applyHideStateDebounced();
updateFrameStatsAfterSummary(endMesId, merged);
+
+ // L2 自动增量向量化
+ await autoVectorizeNewEvents(newEventIds);
},
});
@@ -1206,6 +1313,9 @@ async function handleMessageReceived() {
initButtonsForAll();
+ // 向量全量生成中时跳过 L1 sync(避免竞争写入)
+ if (vectorGenerating) return;
+
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
await maybeAutoBuildChunks();
@@ -1289,7 +1399,8 @@ async function handleGenerationStarted(type, _params, isDryRun) {
if (boundary < 0) return;
// 2) depth:倒序插入,从末尾往前数
- const depth = chatLen - boundary - 1;
+ // 最小为 1,避免插入到最底部导致 AI 看到的最后是总结
+ const depth = Math.max(1, chatLen - boundary - 1);
if (depth < 0) return;
// 3) 构建注入文本(保持原逻辑)
diff --git a/modules/story-summary/vector/chunk-builder.js b/modules/story-summary/vector/chunk-builder.js
index 7c3fcca..f8bd988 100644
--- a/modules/story-summary/vector/chunk-builder.js
+++ b/modules/story-summary/vector/chunk-builder.js
@@ -335,6 +335,13 @@ export async function syncOnMessageReceived(chatId, lastFloor, message, vectorCo
if (!chatId || lastFloor < 0 || !message) return;
if (!vectorConfig?.enabled) return;
+ // 本地模型未加载时跳过(避免意外触发下载或报错)
+ if (vectorConfig.engine === "local") {
+ const { isLocalModelLoaded, DEFAULT_LOCAL_MODEL } = await import("./embedder.js");
+ const modelId = vectorConfig.local?.modelId || DEFAULT_LOCAL_MODEL;
+ if (!isLocalModelLoaded(modelId)) return;
+ }
+
// 删除该楼层旧的
await deleteChunksAtFloor(chatId, lastFloor);