mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
369 lines
13 KiB
JavaScript
369 lines
13 KiB
JavaScript
export function createRerollRecallInput(deps = {}) {
|
||
const plannerRecallHandoffs = new Map();
|
||
|
||
const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args);
|
||
const normalizeChatIdCandidate = (value = "") =>
|
||
deps.normalizeChatIdCandidate?.(value) ?? String(value ?? "").trim();
|
||
const normalizeRecallInputText = (value = "") =>
|
||
deps.normalizeRecallInputText?.(value) ?? String(value || "").trim();
|
||
const hashRecallInput = (value = "") => deps.hashRecallInput?.(value) ?? "";
|
||
const getLastRecallSentUserMessage = () =>
|
||
deps.getLastRecallSentUserMessage?.() || {};
|
||
const getPendingRecallSendIntent = () =>
|
||
deps.getPendingRecallSendIntent?.() || {};
|
||
const getPlannerRecallHandoffTtlMs = () =>
|
||
Number.isFinite(Number(deps.PLANNER_RECALL_HANDOFF_TTL_MS))
|
||
? Number(deps.PLANNER_RECALL_HANDOFF_TTL_MS)
|
||
: 60000;
|
||
|
||
function clearPendingRerollRecallReuse(reason = "") {
|
||
return null;
|
||
}
|
||
|
||
function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
|
||
if (params?.automatic_trigger || params?.quiet_prompt) {
|
||
return null;
|
||
}
|
||
|
||
const generationType = String(type || "").trim() || "normal";
|
||
if (!["normal", "continue", "regenerate", "swipe"].includes(generationType)) {
|
||
return null;
|
||
}
|
||
|
||
const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, {
|
||
generationType,
|
||
generationContext: params?.generationContext,
|
||
});
|
||
|
||
// 对于 history 类型(continue/regenerate/swipe),必须依赖 chat 中的用户消息
|
||
if (generationType !== "normal") {
|
||
if (!Number.isFinite(targetUserMessageIndex)) {
|
||
return {
|
||
generationType,
|
||
targetUserMessageIndex: null,
|
||
};
|
||
}
|
||
const historyInput = buildHistoryGenerationRecallInput(chat, {
|
||
generationType,
|
||
generationContext: params?.generationContext,
|
||
});
|
||
if (!historyInput) {
|
||
return {
|
||
generationType,
|
||
targetUserMessageIndex,
|
||
};
|
||
}
|
||
return {
|
||
...historyInput,
|
||
generationType,
|
||
targetUserMessageIndex,
|
||
};
|
||
}
|
||
|
||
// 对于 normal 类型:GENERATION_AFTER_COMMANDS 触发时用户消息可能不在 chat 末尾
|
||
// (ST 可能已追加空 assistant 消息)。如果 chat 中存在任何用户消息,
|
||
// 继续走 buildNormalGenerationRecallInput,它会通过 latestUserText 兜底找到。
|
||
// 如果 chat 中完全没有用户消息,则延迟到 BEFORE_COMBINE_PROMPTS 处理。
|
||
if (!Number.isFinite(targetUserMessageIndex) && !deps.getLatestUserChatMessage(chat)) {
|
||
return {
|
||
generationType,
|
||
targetUserMessageIndex: null,
|
||
};
|
||
}
|
||
|
||
const normalInput = buildNormalGenerationRecallInput(chat, {
|
||
frozenInputSnapshot: params?.frozenInputSnapshot,
|
||
});
|
||
return normalInput;
|
||
}
|
||
|
||
function buildNormalGenerationRecallInput(chat, options = {}) {
|
||
const lastNonSystemMessage = deps.getLastNonSystemChatMessage(chat);
|
||
const tailUserText = lastNonSystemMessage?.is_user
|
||
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
|
||
: "";
|
||
// 当 GENERATION_AFTER_COMMANDS 触发时,ST 可能已追加了空 assistant 消息。
|
||
// 导致 lastNonSystemMessage 不是 user。用 getLatestUserChatMessage 反向扫描
|
||
// 定位真正的用户消息(与 shujuku 参考实现一致)。
|
||
const latestUserMessage = !tailUserText ? deps.getLatestUserChatMessage(chat) : null;
|
||
const latestUserText = latestUserMessage
|
||
? normalizeRecallInputText(latestUserMessage?.mes || "")
|
||
: "";
|
||
const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, {
|
||
generationType: "normal",
|
||
});
|
||
const frozenInputSnapshot = deps.isFreshRecallInputRecord(
|
||
options?.frozenInputSnapshot,
|
||
)
|
||
? options.frozenInputSnapshot
|
||
: null;
|
||
const pendingRecallSendIntent = getPendingRecallSendIntent();
|
||
const pendingSendIntent = deps.isFreshRecallInputRecord(pendingRecallSendIntent)
|
||
? pendingRecallSendIntent
|
||
: null;
|
||
const sendIntentText = normalizeRecallInputText(
|
||
pendingSendIntent?.text || "",
|
||
);
|
||
const hostSnapshotText = normalizeRecallInputText(
|
||
frozenInputSnapshot?.text || "",
|
||
);
|
||
const textareaText = normalizeRecallInputText(deps.getSendTextareaValue());
|
||
const sourceCandidates = [
|
||
sendIntentText
|
||
? {
|
||
text: sendIntentText,
|
||
source: "send-intent",
|
||
sourceLabel: "发送意图",
|
||
reason: tailUserText
|
||
? "send-intent-overrides-chat-tail"
|
||
: "send-intent-captured",
|
||
includeSyntheticUserMessage: !tailUserText,
|
||
}
|
||
: null,
|
||
hostSnapshotText
|
||
? {
|
||
text: hostSnapshotText,
|
||
source: String(
|
||
frozenInputSnapshot?.source || "host-generation-lifecycle",
|
||
),
|
||
sourceLabel: "宿主发送快照",
|
||
reason: sendIntentText
|
||
? "host-snapshot-suppressed-by-send-intent"
|
||
: tailUserText
|
||
? "host-snapshot-suppressed-by-chat-tail"
|
||
: "host-snapshot-captured",
|
||
includeSyntheticUserMessage: !tailUserText,
|
||
}
|
||
: null,
|
||
tailUserText
|
||
? {
|
||
text: tailUserText,
|
||
source: "chat-tail-user",
|
||
sourceLabel: "当前用户楼层",
|
||
reason:
|
||
sendIntentText || hostSnapshotText
|
||
? "chat-tail-deprioritized"
|
||
: "chat-tail-fallback",
|
||
includeSyntheticUserMessage: false,
|
||
}
|
||
: null,
|
||
latestUserText
|
||
? {
|
||
text: latestUserText,
|
||
source: "chat-latest-user",
|
||
sourceLabel: "最近用户消息",
|
||
reason:
|
||
sendIntentText || hostSnapshotText || tailUserText
|
||
? "latest-user-deprioritized"
|
||
: "latest-user-fallback",
|
||
includeSyntheticUserMessage: false,
|
||
}
|
||
: null,
|
||
textareaText
|
||
? {
|
||
text: textareaText,
|
||
source: "textarea-live",
|
||
sourceLabel: "输入框当前文本",
|
||
reason:
|
||
sendIntentText || hostSnapshotText || tailUserText
|
||
? "textarea-live-deprioritized"
|
||
: "textarea-live-fallback",
|
||
includeSyntheticUserMessage: !tailUserText,
|
||
}
|
||
: null,
|
||
].filter(Boolean);
|
||
const activeTrivialSkip = deps.getCurrentGenerationTrivialSkip();
|
||
if (activeTrivialSkip) {
|
||
deps.clearPendingRecallSendIntent();
|
||
deps.clearPendingHostGenerationInputSnapshot();
|
||
return deps.createTrivialRecallSkipSentinel(activeTrivialSkip.reason);
|
||
}
|
||
|
||
const selectedCandidate = sourceCandidates[0] || null;
|
||
if (!selectedCandidate?.text) return null;
|
||
|
||
const trivialInputResult = deps.isTrivialUserInput(selectedCandidate.text);
|
||
|
||
if (trivialInputResult.trivial) {
|
||
deps.clearPendingRecallSendIntent();
|
||
deps.clearPendingHostGenerationInputSnapshot();
|
||
deps.markCurrentGenerationTrivialSkip({
|
||
reason: trivialInputResult.reason,
|
||
chatId: getCurrentChatId(),
|
||
chatLength: Array.isArray(chat) ? chat.length : 0,
|
||
});
|
||
deps.console?.info?.(
|
||
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=build-normal-input`,
|
||
);
|
||
return deps.createTrivialRecallSkipSentinel(trivialInputResult.reason);
|
||
}
|
||
|
||
return {
|
||
overrideUserMessage: selectedCandidate.text,
|
||
generationType: "normal",
|
||
targetUserMessageIndex,
|
||
overrideSource: selectedCandidate.source,
|
||
overrideSourceLabel: selectedCandidate.sourceLabel,
|
||
overrideReason: selectedCandidate.reason,
|
||
sourceCandidates,
|
||
includeSyntheticUserMessage: selectedCandidate.includeSyntheticUserMessage,
|
||
};
|
||
}
|
||
|
||
function buildHistoryGenerationRecallInput(chat, options = {}) {
|
||
const generationType = String(options?.generationType || "history").trim() || "history";
|
||
const lastRecallSentUserMessage = getLastRecallSentUserMessage();
|
||
const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, {
|
||
generationType,
|
||
generationContext: options?.generationContext,
|
||
});
|
||
const targetUserText = Number.isFinite(targetUserMessageIndex)
|
||
? normalizeRecallInputText(chat?.[targetUserMessageIndex]?.mes || "")
|
||
: "";
|
||
const latestUserText = normalizeRecallInputText(
|
||
targetUserText || deps.getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text,
|
||
);
|
||
if (!latestUserText) return null;
|
||
|
||
return {
|
||
overrideUserMessage: latestUserText,
|
||
generationType,
|
||
targetUserMessageIndex,
|
||
overrideSource: Number.isFinite(targetUserMessageIndex)
|
||
? "chat-last-user"
|
||
: "chat-last-user-missing",
|
||
overrideSourceLabel: Number.isFinite(targetUserMessageIndex)
|
||
? "历史最后用户楼层"
|
||
: "历史用户楼层缺失",
|
||
includeSyntheticUserMessage: false,
|
||
};
|
||
}
|
||
|
||
function cleanupPlannerRecallHandoffs(now = Date.now()) {
|
||
for (const [chatId, handoff] of plannerRecallHandoffs.entries()) {
|
||
if (
|
||
!handoff ||
|
||
String(handoff.chatId || "") !== String(chatId || "") ||
|
||
now - Number(handoff.updatedAt || handoff.createdAt || 0) >
|
||
getPlannerRecallHandoffTtlMs()
|
||
) {
|
||
plannerRecallHandoffs.delete(chatId);
|
||
}
|
||
}
|
||
}
|
||
|
||
function peekPlannerRecallHandoff(
|
||
chatId = getCurrentChatId(),
|
||
now = Date.now(),
|
||
) {
|
||
cleanupPlannerRecallHandoffs(now);
|
||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||
if (!normalizedChatId) return null;
|
||
|
||
const handoff = plannerRecallHandoffs.get(normalizedChatId) || null;
|
||
if (!handoff) return null;
|
||
if (
|
||
now - Number(handoff.updatedAt || handoff.createdAt || 0) >
|
||
getPlannerRecallHandoffTtlMs()
|
||
) {
|
||
plannerRecallHandoffs.delete(normalizedChatId);
|
||
return null;
|
||
}
|
||
return handoff;
|
||
}
|
||
|
||
function clearPlannerRecallHandoffsForChat(
|
||
chatId = getCurrentChatId(),
|
||
{ clearAll = false } = {},
|
||
) {
|
||
cleanupPlannerRecallHandoffs();
|
||
if (clearAll) {
|
||
const removed = plannerRecallHandoffs.size;
|
||
plannerRecallHandoffs.clear();
|
||
return removed;
|
||
}
|
||
|
||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||
if (!normalizedChatId) return 0;
|
||
return plannerRecallHandoffs.delete(normalizedChatId) ? 1 : 0;
|
||
}
|
||
|
||
function consumePlannerRecallHandoff(
|
||
chatId = getCurrentChatId(),
|
||
{ handoffId = "" } = {},
|
||
) {
|
||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||
if (!normalizedChatId) return null;
|
||
|
||
const handoff = peekPlannerRecallHandoff(normalizedChatId);
|
||
if (!handoff) return null;
|
||
if (handoffId && String(handoff.id || "") !== String(handoffId || "")) {
|
||
return null;
|
||
}
|
||
|
||
plannerRecallHandoffs.delete(normalizedChatId);
|
||
return handoff;
|
||
}
|
||
|
||
function preparePlannerRecallHandoff({
|
||
rawUserInput = "",
|
||
plannerAugmentedMessage = "",
|
||
plannerRecall = null,
|
||
plannerPlotRecord = null,
|
||
chatId = getCurrentChatId(),
|
||
} = {}) {
|
||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||
const normalizedRawUserInput = normalizeRecallInputText(rawUserInput);
|
||
const normalizedPlannerAugmentedMessage = normalizeRecallInputText(
|
||
plannerAugmentedMessage,
|
||
);
|
||
const result = plannerRecall?.result || null;
|
||
if (!normalizedChatId || !normalizedRawUserInput || !result) {
|
||
return null;
|
||
}
|
||
|
||
cleanupPlannerRecallHandoffs();
|
||
const createdAt = Date.now();
|
||
const injectionText = normalizeRecallInputText(
|
||
plannerRecall?.memoryBlock || deps.formatInjection(result, deps.getSchema()),
|
||
);
|
||
const handoff = {
|
||
id: [
|
||
normalizedChatId,
|
||
hashRecallInput(normalizedRawUserInput),
|
||
createdAt,
|
||
].join(":"),
|
||
chatId: normalizedChatId,
|
||
rawUserInput: normalizedRawUserInput,
|
||
plannerAugmentedMessage: normalizedPlannerAugmentedMessage,
|
||
result,
|
||
recentMessages: Array.isArray(plannerRecall?.recentMessages)
|
||
? plannerRecall.recentMessages.map((item) => String(item || ""))
|
||
: [],
|
||
injectionText,
|
||
plannerPlotRecord:
|
||
plannerPlotRecord && typeof plannerPlotRecord === "object"
|
||
? { ...plannerPlotRecord }
|
||
: null,
|
||
source: "planner-handoff",
|
||
sourceLabel: "Planner handoff",
|
||
createdAt,
|
||
updatedAt: createdAt,
|
||
};
|
||
plannerRecallHandoffs.set(normalizedChatId, handoff);
|
||
return handoff;
|
||
}
|
||
|
||
return {
|
||
clearPendingRerollRecallReuse,
|
||
buildNormalGenerationRecallInput,
|
||
buildHistoryGenerationRecallInput,
|
||
buildGenerationAfterCommandsRecallInput,
|
||
preparePlannerRecallHandoff,
|
||
cleanupPlannerRecallHandoffs,
|
||
peekPlannerRecallHandoff,
|
||
clearPlannerRecallHandoffsForChat,
|
||
consumePlannerRecallHandoff,
|
||
};
|
||
}
|