From 5b5930e4425a6602e994d432fb99e40201c7e106 Mon Sep 17 00:00:00 2001 From: youzini Date: Sun, 31 May 2026 20:14:32 +0000 Subject: [PATCH] feat(recall): resolve reroll parent user floor --- retrieval/recall-persistence.js | 22 ++---------- runtime/generation-context.js | 60 +++++++++++++++++++++++++++++++++ tests/generation-context.mjs | 46 +++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/retrieval/recall-persistence.js b/retrieval/recall-persistence.js index 69b5cfe..f1fa370 100644 --- a/retrieval/recall-persistence.js +++ b/retrieval/recall-persistence.js @@ -1,5 +1,7 @@ // ST-BME: 持久化召回记录纯函数 +import { resolveGenerationParentUserFloor } from "../runtime/generation-context.js"; + export const BME_RECALL_EXTRA_KEY = "bme_recall"; export const BME_RECALL_VERSION = 1; @@ -135,25 +137,7 @@ export function resolveGenerationTargetUserMessageIndex( { generationType = "normal" } = {}, ) { if (!Array.isArray(chat) || chat.length === 0) return null; - - const normalizedType = String(generationType || "normal").trim() || "normal"; - - // normal:取「最后一条非系统用户楼层」。若直接 return 末条非 user(常见为刚追加的助手回合), - // 会得到 null,导致持久化无法回绑到本轮 user,`hasRecordForLatest` 长期为 false。 - if (normalizedType === "normal") { - for (let index = chat.length - 1; index >= 0; index--) { - const message = chat[index]; - if (message?.is_system) continue; - if (message?.is_user) return index; - } - return null; - } - - for (let index = chat.length - 1; index >= 0; index--) { - if (chat[index]?.is_user) return index; - } - - return null; + return resolveGenerationParentUserFloor(chat, { type: generationType }); } export function resolveFinalRecallInjectionSource({ diff --git a/runtime/generation-context.js b/runtime/generation-context.js index d55eaa3..350f9df 100644 --- a/runtime/generation-context.js +++ b/runtime/generation-context.js @@ -23,6 +23,66 @@ export function classifyGenerationKind(type = "normal", params = {}) { return "fresh"; } +export function isVisibleUserGenerationMessage(message, { index = null, chat = null, isSystemMessage = null } = {}) { + if (!message?.is_user) return false; + if (message?.extra?.isSmallSys) return false; + if (typeof isSystemMessage === "function" && isSystemMessage(message, { index, chat })) { + return false; + } + if (message?.is_system) return false; + return true; +} + +export function resolveGenerationParentUserFloor( + chat, + context = {}, + { phase = "", isSystemMessage = null } = {}, +) { + if (!Array.isArray(chat) || chat.length === 0) return null; + const generationType = normalizeGenerationType(context?.type || context?.generationType || "normal"); + const findVisibleUserBefore = (startIndex) => { + for (let index = Math.min(chat.length - 1, Math.floor(Number(startIndex))); index >= 0; index--) { + if (isVisibleUserGenerationMessage(chat[index], { index, chat, isSystemMessage })) return index; + } + return null; + }; + const findLastVisibleNonSystemIndex = () => { + for (let index = chat.length - 1; index >= 0; index--) { + const message = chat[index]; + if (!message) continue; + if (message?.extra?.isSmallSys) continue; + if (typeof isSystemMessage === "function" && isSystemMessage(message, { index, chat })) continue; + if (message?.is_system) continue; + return index; + } + return null; + }; + + if (generationType === "swipe") { + const swipedFloor = Number(context?.swipedAssistantFloor); + if (Number.isFinite(swipedFloor)) return findVisibleUserBefore(swipedFloor - 1); + const lastVisible = findLastVisibleNonSystemIndex(); + return Number.isFinite(lastVisible) ? findVisibleUserBefore(lastVisible - 1) : null; + } + + if (generationType === "regenerate") { + const lastVisible = findLastVisibleNonSystemIndex(); + if (!Number.isFinite(lastVisible)) return null; + if (isVisibleUserGenerationMessage(chat[lastVisible], { index: lastVisible, chat, isSystemMessage })) { + return lastVisible; + } + return findVisibleUserBefore(lastVisible - 1); + } + + if (generationType === "continue") { + const lastVisible = findLastVisibleNonSystemIndex(); + if (!Number.isFinite(lastVisible)) return null; + return findVisibleUserBefore(lastVisible - (chat[lastVisible]?.is_user ? 0 : 1)); + } + + return findVisibleUserBefore(chat.length - 1); +} + function clonePlain(value, fallback = null) { if (!value || typeof value !== "object") return fallback; try { diff --git a/tests/generation-context.mjs b/tests/generation-context.mjs index 7f1d3ad..7bedfe9 100644 --- a/tests/generation-context.mjs +++ b/tests/generation-context.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { classifyGenerationKind, createGenerationContextTracker, + resolveGenerationParentUserFloor, } from "../runtime/generation-context.js"; assert.equal(classifyGenerationKind("normal"), "fresh"); @@ -100,3 +101,48 @@ assert.equal(classifyGenerationKind("normal", { quiet_prompt: true }), "skip"); chatId = "chat-original"; assert.equal(tracker.get(), null); } + +{ + const chat = [ + { is_system: true, mes: "greeting" }, + { is_user: true, mes: "first" }, + { is_user: false, mes: "assistant first" }, + { is_user: true, mes: "parent" }, + { is_user: false, mes: "assistant active" }, + ]; + + assert.equal( + resolveGenerationParentUserFloor(chat, { + type: "swipe", + swipedAssistantFloor: 4, + }), + 3, + ); + assert.equal(resolveGenerationParentUserFloor(chat, { type: "regenerate" }), 3); +} + +{ + const chatAfterRegenerateDelete = [ + { is_system: true, mes: "greeting" }, + { is_user: true, mes: "parent" }, + ]; + assert.equal( + resolveGenerationParentUserFloor(chatAfterRegenerateDelete, { + type: "regenerate", + }), + 1, + ); +} + +{ + const chat = [ + { is_system: true, mes: "greeting" }, + { is_user: true, mes: "hidden", is_system: true }, + { is_user: true, mes: "visible" }, + { is_user: false, mes: "assistant" }, + ]; + assert.equal( + resolveGenerationParentUserFloor(chat, { type: "swipe", swipedAssistantFloor: 3 }), + 2, + ); +}