// ST-BME: 提取编排控制器(纯函数) // 通过 runtime 依赖注入,避免直接访问 index.js 模块级状态。 export async function executeExtractionBatchController( runtime, { chat, startIdx, endIdx, settings, smartTriggerDecision = null, signal = undefined, } = {}, ) { runtime.ensureCurrentGraphRuntimeState(); runtime.throwIfAborted(signal, "提取已终止"); const currentGraph = runtime.getCurrentGraph(); const lastProcessed = runtime.getLastProcessedAssistantFloor(); const extractionCountBefore = runtime.getExtractionCount(); const beforeSnapshot = runtime.cloneGraphSnapshot(currentGraph); const messages = runtime.buildExtractionMessages(chat, startIdx, endIdx, settings); const batchStatus = runtime.createBatchStatusSkeleton({ processedRange: [startIdx, endIdx], extractionCountBefore, }); runtime.console.log( `[ST-BME] 开始提取: 楼层 ${startIdx}-${endIdx}` + (smartTriggerDecision?.triggered ? ` [智能触发 score=${smartTriggerDecision.score}; ${smartTriggerDecision.reasons.join(" / ")}]` : ""), ); const result = await runtime.extractMemories({ graph: currentGraph, messages, startSeq: startIdx, endSeq: endIdx, lastProcessedSeq: lastProcessed, schema: runtime.getSchema(), embeddingConfig: runtime.getEmbeddingConfig(), extractPrompt: undefined, settings, signal, onStreamProgress: ({ previewText, receivedChars }) => { const preview = previewText?.length > 60 ? "…" + previewText.slice(-60) : previewText || ""; runtime.setLastExtractionStatus( "AI 生成中", `${preview} [${receivedChars}字]`, "running", { noticeMarquee: true }, ); }, }); if (!result.success) { runtime.setBatchStageOutcome( batchStatus, "core", "failed", result?.error || "提取阶段未返回有效操作", ); runtime.finalizeBatchStatus(batchStatus, runtime.getExtractionCount()); runtime.getCurrentGraph().historyState.lastBatchStatus = batchStatus; return { success: false, result, effects: null, batchStatus, error: result?.error || "提取阶段未返回有效操作", }; } runtime.setBatchStageOutcome(batchStatus, "core", "success"); const effects = await runtime.handleExtractionSuccess( result, endIdx, settings, signal, batchStatus, ); const finalizedBatchStatus = effects?.batchStatus || runtime.finalizeBatchStatus(batchStatus, runtime.getExtractionCount()); runtime.getCurrentGraph().historyState.lastBatchStatus = { ...finalizedBatchStatus, historyAdvanced: runtime.shouldAdvanceProcessedHistory(finalizedBatchStatus), }; if (runtime.getCurrentGraph().historyState.lastBatchStatus.historyAdvanced) { runtime.updateProcessedHistorySnapshot(chat, endIdx); } const afterSnapshot = runtime.cloneGraphSnapshot(runtime.getCurrentGraph()); const postProcessArtifacts = runtime.computePostProcessArtifacts( beforeSnapshot, afterSnapshot, effects?.postProcessArtifacts || [], ); runtime.appendBatchJournal( runtime.getCurrentGraph(), runtime.createBatchJournalEntry(beforeSnapshot, afterSnapshot, { processedRange: [startIdx, endIdx], postProcessArtifacts, vectorHashesInserted: effects?.vectorHashesInserted || [], extractionCountBefore, }), ); runtime.saveGraphToChat({ reason: "extraction-batch-complete" }); return { success: finalizedBatchStatus.completed, result, effects, batchStatus: finalizedBatchStatus, error: finalizedBatchStatus.completed ? "" : effects?.vectorError || finalizedBatchStatus.errors?.[0] || "批次未完成 finalize 闭环", }; } export async function runExtractionController(runtime) { if (runtime.getIsExtracting() || !runtime.getCurrentGraph()) return; const settings = runtime.getSettings(); if (!settings.enabled) return; if (!runtime.ensureGraphMutationReady("自动提取", { notify: false })) { runtime.setLastExtractionStatus( "等待图谱加载", runtime.getGraphMutationBlockReason("自动提取"), "warning", { syncRuntime: true }, ); return; } if (!(await runtime.recoverHistoryIfNeeded("auto-extract"))) return; const context = runtime.getContext(); const chat = context.chat; if (!chat || chat.length === 0) return; const assistantTurns = runtime.getAssistantTurns(chat); const lastProcessed = runtime.getLastProcessedAssistantFloor(); const unprocessedAssistantTurns = assistantTurns.filter((i) => i > lastProcessed); if (unprocessedAssistantTurns.length === 0) return; const extractEvery = runtime.clampInt(settings.extractEvery, 1, 1, 50); const smartTriggerDecision = settings.enableSmartTrigger ? runtime.getSmartTriggerDecision(chat, lastProcessed, settings) : { triggered: false, score: 0, reasons: [] }; if ( unprocessedAssistantTurns.length < extractEvery && !smartTriggerDecision.triggered ) { return; } const batchAssistantTurns = smartTriggerDecision.triggered ? unprocessedAssistantTurns : unprocessedAssistantTurns.slice(0, extractEvery); const startIdx = batchAssistantTurns[0]; const endIdx = batchAssistantTurns[batchAssistantTurns.length - 1]; runtime.setIsExtracting(true); const extractionController = runtime.beginStageAbortController("extraction"); const extractionSignal = extractionController.signal; runtime.setLastExtractionStatus( "提取中", `楼层 ${startIdx}-${endIdx}${smartTriggerDecision.triggered ? " · 智能触发" : ""}`, "running", { syncRuntime: true }, ); try { const batchResult = await runtime.executeExtractionBatch({ chat, startIdx, endIdx, settings, smartTriggerDecision, signal: extractionSignal, }); if (!batchResult.success) { const message = batchResult.error || batchResult?.result?.error || "提取批次未返回有效结果"; runtime.console.warn("[ST-BME] 提取批次未返回有效结果:", message); runtime.notifyExtractionIssue(message); return; } runtime.setLastExtractionStatus( "提取完成", `楼层 ${startIdx}-${endIdx} · 新建 ${batchResult.result?.newNodes || 0} · 更新 ${batchResult.result?.updatedNodes || 0} · 新边 ${batchResult.result?.newEdges || 0}`, "success", { syncRuntime: true }, ); } catch (e) { if (runtime.isAbortError(e)) { runtime.setLastExtractionStatus( "提取已终止", e?.message || "已手动终止当前提取", "warning", { syncRuntime: true, }, ); return; } runtime.console.error("[ST-BME] 提取失败:", e); runtime.notifyExtractionIssue(e?.message || String(e) || "自动提取失败"); } finally { runtime.finishStageAbortController("extraction", extractionController); runtime.setIsExtracting(false); } }