mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
skip-trivial-user-input-plan.md
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -1274,6 +1274,10 @@ async function runPlanningOnce(rawUserInput, silent = false, options = {}) {
|
|||||||
function getSendTextarea() { return document.getElementById('send_textarea'); }
|
function getSendTextarea() { return document.getElementById('send_textarea'); }
|
||||||
function getSendButton() { return document.getElementById('send_but') || document.getElementById('send_button'); }
|
function getSendButton() { return document.getElementById('send_but') || document.getElementById('send_button'); }
|
||||||
|
|
||||||
|
function isTrivialPlannerInput(text) {
|
||||||
|
return _bmeRuntime?.isTrivialUserInput?.(text)?.trivial === true;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldInterceptNow() {
|
function shouldInterceptNow() {
|
||||||
const s = ensureSettings();
|
const s = ensureSettings();
|
||||||
if (!s.enabled || state.isPlanning) return false;
|
if (!s.enabled || state.isPlanning) return false;
|
||||||
@@ -1281,6 +1285,7 @@ function shouldInterceptNow() {
|
|||||||
if (!ta) return false;
|
if (!ta) return false;
|
||||||
const txt = String(ta.value ?? '').trim();
|
const txt = String(ta.value ?? '').trim();
|
||||||
if (!txt) return false;
|
if (!txt) return false;
|
||||||
|
if (isTrivialPlannerInput(txt)) return false;
|
||||||
if (state.bypassNextSend) return false;
|
if (state.bypassNextSend) return false;
|
||||||
if (s.skipIfPlotPresent && /<plot\b/i.test(txt)) return false;
|
if (s.skipIfPlotPresent && /<plot\b/i.test(txt)) return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -1293,6 +1298,7 @@ async function doInterceptAndPlanThenSend() {
|
|||||||
|
|
||||||
const raw = String(ta.value ?? '').trim();
|
const raw = String(ta.value ?? '').trim();
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
|
if (isTrivialPlannerInput(raw)) return;
|
||||||
|
|
||||||
state.isPlanning = true;
|
state.isPlanning = true;
|
||||||
setSendUIBusy(true);
|
setSendUIBusy(true);
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export function onChatChangedController(runtime) {
|
|||||||
source: "chat-changed",
|
source: "chat-changed",
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
|
runtime.clearCurrentGenerationTrivialSkip?.("chat-changed");
|
||||||
runtime.clearInjectionState();
|
runtime.clearInjectionState();
|
||||||
runtime.clearRecallInputTracking();
|
runtime.clearRecallInputTracking();
|
||||||
runtime.installSendIntentHooks();
|
runtime.installSendIntentHooks();
|
||||||
@@ -250,6 +251,10 @@ export function onMessageSentController(runtime, messageId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!message?.is_user) return;
|
if (!message?.is_user) return;
|
||||||
|
if (runtime.isTrivialUserInput?.(message.mes || "")?.trivial) {
|
||||||
|
runtime.refreshPersistedRecallMessageUi?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
runtime.recordRecallSentUserMessage(
|
runtime.recordRecallSentUserMessage(
|
||||||
resolvedMessageId,
|
resolvedMessageId,
|
||||||
message.mes || "",
|
message.mes || "",
|
||||||
@@ -310,8 +315,22 @@ export function onGenerationStartedController(
|
|||||||
: "";
|
: "";
|
||||||
const snapshotText =
|
const snapshotText =
|
||||||
runtime.normalizeRecallInputText?.(pendingIntentText || textareaText) || "";
|
runtime.normalizeRecallInputText?.(pendingIntentText || textareaText) || "";
|
||||||
|
const trivialInputResult = runtime.isTrivialUserInput?.(snapshotText);
|
||||||
if (!snapshotText) return null;
|
if (trivialInputResult?.trivial) {
|
||||||
|
const context = runtime.getContext?.() || {};
|
||||||
|
runtime.markCurrentGenerationTrivialSkip?.({
|
||||||
|
reason: trivialInputResult.reason,
|
||||||
|
chatId: context?.chatId || "",
|
||||||
|
chatLength: Array.isArray(context?.chat) ? context.chat.length : 0,
|
||||||
|
});
|
||||||
|
runtime.clearPendingRecallSendIntent?.();
|
||||||
|
runtime.clearPendingHostGenerationInputSnapshot?.();
|
||||||
|
console.info?.(
|
||||||
|
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=GENERATION_STARTED`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
runtime.clearCurrentGenerationTrivialSkip?.("new-non-trivial-generation");
|
||||||
return runtime.freezeHostGenerationInputSnapshot(
|
return runtime.freezeHostGenerationInputSnapshot(
|
||||||
snapshotText,
|
snapshotText,
|
||||||
pendingIntentText
|
pendingIntentText
|
||||||
@@ -417,6 +436,10 @@ export async function onGenerationAfterCommandsController(
|
|||||||
console.warn("[ST-BME:DIAG] EXIT: buildGenerationAfterCommandsRecallInput returned null");
|
console.warn("[ST-BME:DIAG] EXIT: buildGenerationAfterCommandsRecallInput returned null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (recallOptions?.__trivialSkip) {
|
||||||
|
console.warn("[ST-BME:DIAG] EXIT: trivial-input-skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.warn("[ST-BME:DIAG] recallOptions:", { generationType: recallOptions.generationType, overrideUserMessage: recallOptions.overrideUserMessage?.slice(0,50), overrideSource: recallOptions.overrideSource, targetIdx: recallOptions.targetUserMessageIndex });
|
console.warn("[ST-BME:DIAG] recallOptions:", { generationType: recallOptions.generationType, overrideUserMessage: recallOptions.overrideUserMessage?.slice(0,50), overrideSource: recallOptions.overrideSource, targetIdx: recallOptions.targetUserMessageIndex });
|
||||||
|
|
||||||
const recallContext = runtime.createGenerationRecallContext({
|
const recallContext = runtime.createGenerationRecallContext({
|
||||||
@@ -523,10 +546,18 @@ export async function onBeforeCombinePromptsController(
|
|||||||
{};
|
{};
|
||||||
const context = runtime.getContext();
|
const context = runtime.getContext();
|
||||||
const chat = context?.chat;
|
const chat = context?.chat;
|
||||||
|
const normalInput = runtime.buildNormalGenerationRecallInput(chat, {
|
||||||
|
frozenInputSnapshot,
|
||||||
|
});
|
||||||
|
if (normalInput?.__trivialSkip) {
|
||||||
|
console.warn("[ST-BME:DIAG] EXIT: trivial-input-skip");
|
||||||
|
return {
|
||||||
|
skipped: true,
|
||||||
|
reason: `trivial:${normalInput.trivialReason || ""}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
const recallOptions =
|
const recallOptions =
|
||||||
runtime.buildNormalGenerationRecallInput(chat, {
|
normalInput ||
|
||||||
frozenInputSnapshot,
|
|
||||||
}) ||
|
|
||||||
runtime.buildHistoryGenerationRecallInput(chat) ||
|
runtime.buildHistoryGenerationRecallInput(chat) ||
|
||||||
{};
|
{};
|
||||||
const recallContext = runtime.createGenerationRecallContext({
|
const recallContext = runtime.createGenerationRecallContext({
|
||||||
@@ -644,12 +675,27 @@ export function onMessageReceivedController(
|
|||||||
const targetMessage = runtime.isAssistantChatMessage(receivedMessage)
|
const targetMessage = runtime.isAssistantChatMessage(receivedMessage)
|
||||||
? receivedMessage
|
? receivedMessage
|
||||||
: lastMessage;
|
: lastMessage;
|
||||||
|
const targetMessageIndex = runtime.isAssistantChatMessage(receivedMessage)
|
||||||
|
? Number(messageId)
|
||||||
|
: runtime.isAssistantChatMessage(lastMessage)
|
||||||
|
? chat.length - 1
|
||||||
|
: null;
|
||||||
|
|
||||||
if (runtime.isAssistantChatMessage(targetMessage)) {
|
if (runtime.isAssistantChatMessage(targetMessage)) {
|
||||||
|
if (runtime.consumeCurrentGenerationTrivialSkip?.(targetMessageIndex)) {
|
||||||
|
runtime.console?.info?.(
|
||||||
|
"[ST-BME] trivial-input skip: extraction bypassed",
|
||||||
|
{ messageId: targetMessageIndex },
|
||||||
|
);
|
||||||
|
runtime.refreshPersistedRecallMessageUi?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
runtime.console?.debug?.(
|
runtime.console?.debug?.(
|
||||||
"[ST-BME] assistant message received, queueing auto extraction",
|
"[ST-BME] assistant message received, queueing auto extraction",
|
||||||
{
|
{
|
||||||
messageId: Number.isFinite(Number(messageId)) ? Number(messageId) : null,
|
messageId: Number.isFinite(Number(targetMessageIndex))
|
||||||
|
? Number(targetMessageIndex)
|
||||||
|
: null,
|
||||||
chatLength: Array.isArray(chat) ? chat.length : 0,
|
chatLength: Array.isArray(chat) ? chat.length : 0,
|
||||||
loadState,
|
loadState,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
|||||||
144
index.js
144
index.js
@@ -204,6 +204,7 @@ import {
|
|||||||
getStageNoticeTitle,
|
getStageNoticeTitle,
|
||||||
hashRecallInput,
|
hashRecallInput,
|
||||||
isFreshRecallInputRecord,
|
isFreshRecallInputRecord,
|
||||||
|
isTrivialUserInput,
|
||||||
normalizeRecallInputText,
|
normalizeRecallInputText,
|
||||||
normalizeStageNoticeLevel,
|
normalizeStageNoticeLevel,
|
||||||
pushBatchStageArtifact,
|
pushBatchStageArtifact,
|
||||||
@@ -521,6 +522,7 @@ let lastExtractionWarningAt = 0;
|
|||||||
const LOCAL_VECTOR_TIMEOUT_MS = 300000;
|
const LOCAL_VECTOR_TIMEOUT_MS = 300000;
|
||||||
const STATUS_TOAST_THROTTLE_MS = 1500;
|
const STATUS_TOAST_THROTTLE_MS = 1500;
|
||||||
const RECALL_INPUT_RECORD_TTL_MS = 60000;
|
const RECALL_INPUT_RECORD_TTL_MS = 60000;
|
||||||
|
const TRIVIAL_GENERATION_SKIP_TTL_MS = 60000;
|
||||||
const HISTORY_RECOVERY_SETTLE_MS = 80;
|
const HISTORY_RECOVERY_SETTLE_MS = 80;
|
||||||
const HISTORY_MUTATION_RETRY_DELAYS_MS = [80, 220, 500, 900];
|
const HISTORY_MUTATION_RETRY_DELAYS_MS = [80, 220, 500, 900];
|
||||||
const GRAPH_LOAD_RETRY_DELAYS_MS = [120, 450, 1200, 2500];
|
const GRAPH_LOAD_RETRY_DELAYS_MS = [120, 450, 1200, 2500];
|
||||||
@@ -534,6 +536,7 @@ const lastStatusToastAt = {};
|
|||||||
let pendingRecallSendIntent = createRecallInputRecord();
|
let pendingRecallSendIntent = createRecallInputRecord();
|
||||||
let lastRecallSentUserMessage = createRecallInputRecord();
|
let lastRecallSentUserMessage = createRecallInputRecord();
|
||||||
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
||||||
|
let currentGenerationTrivialSkip = null;
|
||||||
let coreEventBindingState = {
|
let coreEventBindingState = {
|
||||||
registered: false,
|
registered: false,
|
||||||
cleanups: [],
|
cleanups: [],
|
||||||
@@ -1246,9 +1249,9 @@ function restoreRecallUiStateFromPersistence(chat = getContext()?.chat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearRecallInputTracking() {
|
function clearRecallInputTracking() {
|
||||||
pendingRecallSendIntent = createRecallInputRecord();
|
clearPendingRecallSendIntent();
|
||||||
lastRecallSentUserMessage = createRecallInputRecord();
|
lastRecallSentUserMessage = createRecallInputRecord();
|
||||||
pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
clearPendingHostGenerationInputSnapshot();
|
||||||
if (typeof recordMessageTraceSnapshot === "function") {
|
if (typeof recordMessageTraceSnapshot === "function") {
|
||||||
recordMessageTraceSnapshot({
|
recordMessageTraceSnapshot({
|
||||||
lastSentUserMessage: null,
|
lastSentUserMessage: null,
|
||||||
@@ -1330,6 +1333,91 @@ function getPendingHostGenerationInputSnapshot() {
|
|||||||
return pendingHostGenerationInputSnapshot;
|
return pendingHostGenerationInputSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearPendingRecallSendIntent() {
|
||||||
|
pendingRecallSendIntent = createRecallInputRecord();
|
||||||
|
return pendingRecallSendIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingHostGenerationInputSnapshot() {
|
||||||
|
pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
||||||
|
return pendingHostGenerationInputSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentGenerationTrivialSkip(
|
||||||
|
chatId = getCurrentChatId(),
|
||||||
|
now = Date.now(),
|
||||||
|
) {
|
||||||
|
if (!currentGenerationTrivialSkip) return null;
|
||||||
|
|
||||||
|
const setAtMs = Number(currentGenerationTrivialSkip.setAtMs) || 0;
|
||||||
|
if (
|
||||||
|
!setAtMs ||
|
||||||
|
now - setAtMs > TRIVIAL_GENERATION_SKIP_TTL_MS
|
||||||
|
) {
|
||||||
|
currentGenerationTrivialSkip = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
|
const activeChatId = normalizeChatIdCandidate(
|
||||||
|
currentGenerationTrivialSkip.chatId,
|
||||||
|
);
|
||||||
|
if (normalizedChatId && activeChatId && normalizedChatId !== activeChatId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentGenerationTrivialSkip;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markCurrentGenerationTrivialSkip({
|
||||||
|
reason = "",
|
||||||
|
chatId = getCurrentChatId(),
|
||||||
|
chatLength = 0,
|
||||||
|
} = {}) {
|
||||||
|
currentGenerationTrivialSkip = {
|
||||||
|
chatId: normalizeChatIdCandidate(chatId),
|
||||||
|
setAtMs: Date.now(),
|
||||||
|
reason: String(reason || ""),
|
||||||
|
generationStartMinChatIndex: Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(Number(chatLength) || 0),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return currentGenerationTrivialSkip;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCurrentGenerationTrivialSkip(_reason = "") {
|
||||||
|
const previous = currentGenerationTrivialSkip;
|
||||||
|
currentGenerationTrivialSkip = null;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeCurrentGenerationTrivialSkip(
|
||||||
|
targetMessageIndex,
|
||||||
|
chatId = getCurrentChatId(),
|
||||||
|
now = Date.now(),
|
||||||
|
) {
|
||||||
|
const activeSkip = getCurrentGenerationTrivialSkip(chatId, now);
|
||||||
|
if (!activeSkip) return false;
|
||||||
|
|
||||||
|
const normalizedTargetIndex = Number.isFinite(Number(targetMessageIndex))
|
||||||
|
? Math.floor(Number(targetMessageIndex))
|
||||||
|
: null;
|
||||||
|
if (!Number.isFinite(normalizedTargetIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedTargetIndex <
|
||||||
|
Math.max(0, Math.floor(Number(activeSkip.generationStartMinChatIndex) || 0))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentGenerationTrivialSkip = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function recordRecallSendIntent(text, source = "dom-intent") {
|
function recordRecallSendIntent(text, source = "dom-intent") {
|
||||||
const normalized = normalizeRecallInputText(text);
|
const normalized = normalizeRecallInputText(text);
|
||||||
if (!normalized) return createRecallInputRecord();
|
if (!normalized) return createRecallInputRecord();
|
||||||
@@ -6467,9 +6555,17 @@ function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildNormalGenerationRecallInput(chat, {
|
const normalInput = buildNormalGenerationRecallInput(chat, {
|
||||||
frozenInputSnapshot: params?.frozenInputSnapshot,
|
frozenInputSnapshot: params?.frozenInputSnapshot,
|
||||||
});
|
});
|
||||||
|
return normalInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTrivialRecallSkipSentinel(reason = "") {
|
||||||
|
return {
|
||||||
|
__trivialSkip: true,
|
||||||
|
trivialReason: String(reason || ""),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNormalGenerationRecallInput(chat, options = {}) {
|
function buildNormalGenerationRecallInput(chat, options = {}) {
|
||||||
@@ -6566,9 +6662,32 @@ function buildNormalGenerationRecallInput(chat, options = {}) {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
const activeTrivialSkip = getCurrentGenerationTrivialSkip();
|
||||||
|
if (activeTrivialSkip) {
|
||||||
|
clearPendingRecallSendIntent();
|
||||||
|
clearPendingHostGenerationInputSnapshot();
|
||||||
|
return createTrivialRecallSkipSentinel(activeTrivialSkip.reason);
|
||||||
|
}
|
||||||
|
|
||||||
const selectedCandidate = sourceCandidates[0] || null;
|
const selectedCandidate = sourceCandidates[0] || null;
|
||||||
if (!selectedCandidate?.text) return null;
|
if (!selectedCandidate?.text) return null;
|
||||||
|
|
||||||
|
const trivialInputResult = isTrivialUserInput(selectedCandidate.text);
|
||||||
|
|
||||||
|
if (trivialInputResult.trivial) {
|
||||||
|
clearPendingRecallSendIntent();
|
||||||
|
clearPendingHostGenerationInputSnapshot();
|
||||||
|
markCurrentGenerationTrivialSkip({
|
||||||
|
reason: trivialInputResult.reason,
|
||||||
|
chatId: getCurrentChatId(),
|
||||||
|
chatLength: Array.isArray(chat) ? chat.length : 0,
|
||||||
|
});
|
||||||
|
console.info?.(
|
||||||
|
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=build-normal-input`,
|
||||||
|
);
|
||||||
|
return createTrivialRecallSkipSentinel(trivialInputResult.reason);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
overrideUserMessage: selectedCandidate.text,
|
overrideUserMessage: selectedCandidate.text,
|
||||||
generationType: "normal",
|
generationType: "normal",
|
||||||
@@ -7119,6 +7238,7 @@ function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更")
|
|||||||
|
|
||||||
clearGenerationRecallTransactionsForChat();
|
clearGenerationRecallTransactionsForChat();
|
||||||
clearRecallInputTracking();
|
clearRecallInputTracking();
|
||||||
|
clearCurrentGenerationTrivialSkip("history-mutation");
|
||||||
clearInjectionState({
|
clearInjectionState({
|
||||||
preserveRecallStatus: hadActiveRecall,
|
preserveRecallStatus: hadActiveRecall,
|
||||||
preserveRuntimeStatus: hadActiveRecall,
|
preserveRuntimeStatus: hadActiveRecall,
|
||||||
@@ -8843,10 +8963,14 @@ async function runPlannerRecallForEna({
|
|||||||
disableLlmRecall = false,
|
disableLlmRecall = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const userMessage = normalizeRecallInputText(rawUserInput || "");
|
const userMessage = normalizeRecallInputText(rawUserInput || "");
|
||||||
if (!userMessage) {
|
const trivialInputResult = isTrivialUserInput(userMessage);
|
||||||
|
if (trivialInputResult.trivial) {
|
||||||
|
console.info?.(
|
||||||
|
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=ena-planner`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: "empty-user-input",
|
reason: `trivial-user-input:${trivialInputResult.reason}`,
|
||||||
memoryBlock: "",
|
memoryBlock: "",
|
||||||
recentMessages: [],
|
recentMessages: [],
|
||||||
result: null,
|
result: null,
|
||||||
@@ -9018,6 +9142,7 @@ function onChatChanged() {
|
|||||||
clearPendingAutoExtraction,
|
clearPendingAutoExtraction,
|
||||||
clearPendingGraphLoadRetry,
|
clearPendingGraphLoadRetry,
|
||||||
clearPendingHistoryMutationChecks,
|
clearPendingHistoryMutationChecks,
|
||||||
|
clearCurrentGenerationTrivialSkip,
|
||||||
clearRecallInputTracking,
|
clearRecallInputTracking,
|
||||||
clearTimeout,
|
clearTimeout,
|
||||||
dismissAllStageNotices,
|
dismissAllStageNotices,
|
||||||
@@ -9090,6 +9215,7 @@ function onMessageSent(messageId) {
|
|||||||
const result = onMessageSentController(
|
const result = onMessageSentController(
|
||||||
{
|
{
|
||||||
getContext,
|
getContext,
|
||||||
|
isTrivialUserInput,
|
||||||
recordRecallSentUserMessage,
|
recordRecallSentUserMessage,
|
||||||
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
||||||
},
|
},
|
||||||
@@ -9173,11 +9299,17 @@ function onGenerationStarted(type, params = {}, dryRun = false) {
|
|||||||
return onGenerationStartedController(
|
return onGenerationStartedController(
|
||||||
{
|
{
|
||||||
clearDryRunPromptPreview,
|
clearDryRunPromptPreview,
|
||||||
|
clearCurrentGenerationTrivialSkip,
|
||||||
|
clearPendingHostGenerationInputSnapshot,
|
||||||
|
clearPendingRecallSendIntent,
|
||||||
freezeHostGenerationInputSnapshot,
|
freezeHostGenerationInputSnapshot,
|
||||||
|
getContext,
|
||||||
getPendingRecallSendIntent: () => pendingRecallSendIntent,
|
getPendingRecallSendIntent: () => pendingRecallSendIntent,
|
||||||
getSendTextareaValue,
|
getSendTextareaValue,
|
||||||
isFreshRecallInputRecord,
|
isFreshRecallInputRecord,
|
||||||
|
isTrivialUserInput,
|
||||||
markDryRunPromptPreview,
|
markDryRunPromptPreview,
|
||||||
|
markCurrentGenerationTrivialSkip,
|
||||||
normalizeRecallInputText,
|
normalizeRecallInputText,
|
||||||
},
|
},
|
||||||
type,
|
type,
|
||||||
@@ -9254,6 +9386,7 @@ async function onBeforeCombinePrompts(promptData = null) {
|
|||||||
function onMessageReceived(messageId = null, type = "") {
|
function onMessageReceived(messageId = null, type = "") {
|
||||||
const result = onMessageReceivedController({
|
const result = onMessageReceivedController({
|
||||||
console,
|
console,
|
||||||
|
consumeCurrentGenerationTrivialSkip,
|
||||||
createRecallInputRecord,
|
createRecallInputRecord,
|
||||||
getContext,
|
getContext,
|
||||||
getCurrentGraph: () => currentGraph,
|
getCurrentGraph: () => currentGraph,
|
||||||
@@ -9699,6 +9832,7 @@ async function onReembedDirect() {
|
|||||||
await initEnaPlanner({
|
await initEnaPlanner({
|
||||||
getContext,
|
getContext,
|
||||||
getExtensionPath: () => `scripts/extensions/third-party/${MODULE_NAME}`,
|
getExtensionPath: () => `scripts/extensions/third-party/${MODULE_NAME}`,
|
||||||
|
isTrivialUserInput,
|
||||||
preparePlannerRecallHandoff,
|
preparePlannerRecallHandoff,
|
||||||
runPlannerRecallForEna,
|
runPlannerRecallForEna,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
"test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs",
|
"test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs",
|
||||||
"test:indexeddb-sync": "node tests/indexeddb-sync.mjs",
|
"test:indexeddb-sync": "node tests/indexeddb-sync.mjs",
|
||||||
"test:indexeddb-migration": "node tests/indexeddb-migration.mjs",
|
"test:indexeddb-migration": "node tests/indexeddb-migration.mjs",
|
||||||
|
"test:trivial-input": "node tests/trivial-user-input.mjs",
|
||||||
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
|
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
|
||||||
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
|
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
|
||||||
"test:all": "npm run test:persistence-matrix",
|
"test:all": "npm run test:persistence-matrix && npm run test:trivial-input",
|
||||||
"check": "node --check index.js && node --check bme-db.js && node --check hide-engine.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js"
|
"check": "node --check index.js && node --check bme-db.js && node --check hide-engine.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
363
tests/helpers/generation-recall-harness.mjs
Normal file
363
tests/helpers/generation-recall-harness.mjs
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import vm from "node:vm";
|
||||||
|
import {
|
||||||
|
onBeforeCombinePromptsController,
|
||||||
|
onGenerationAfterCommandsController,
|
||||||
|
onGenerationStartedController,
|
||||||
|
onMessageReceivedController,
|
||||||
|
onMessageSentController,
|
||||||
|
} from "../../event-binding.js";
|
||||||
|
import {
|
||||||
|
GRAPH_LOAD_STATES,
|
||||||
|
GRAPH_METADATA_KEY,
|
||||||
|
GRAPH_PERSISTENCE_META_KEY,
|
||||||
|
MODULE_NAME,
|
||||||
|
} from "../../graph-persistence.js";
|
||||||
|
import {
|
||||||
|
buildPersistedRecallRecord,
|
||||||
|
bumpPersistedRecallGenerationCount,
|
||||||
|
readPersistedRecallFromUserMessage,
|
||||||
|
resolveFinalRecallInjectionSource,
|
||||||
|
writePersistedRecallToUserMessage,
|
||||||
|
} from "../../recall-persistence.js";
|
||||||
|
import {
|
||||||
|
createGraphPersistenceState,
|
||||||
|
createRecallInputRecord,
|
||||||
|
createRecallRunResult,
|
||||||
|
createUiStatus,
|
||||||
|
getGenerationRecallHookStateFromResult,
|
||||||
|
getRecallHookLabel,
|
||||||
|
getStageNoticeDuration,
|
||||||
|
getStageNoticeTitle,
|
||||||
|
hashRecallInput,
|
||||||
|
isFreshRecallInputRecord,
|
||||||
|
isTerminalGenerationRecallHookState,
|
||||||
|
isTrivialUserInput,
|
||||||
|
normalizeRecallInputText,
|
||||||
|
normalizeStageNoticeLevel,
|
||||||
|
shouldRunRecallForTransaction,
|
||||||
|
} from "../../ui-status.js";
|
||||||
|
|
||||||
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const indexPath = path.resolve(moduleDir, "../../index.js");
|
||||||
|
|
||||||
|
export function createGenerationRecallHarness(options = {}) {
|
||||||
|
const { realApplyFinal = false } = options;
|
||||||
|
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||||
|
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
|
||||||
|
const end = source.indexOf(
|
||||||
|
'function onMessageReceived(messageId = null, type = "") {',
|
||||||
|
);
|
||||||
|
const endFallback = source.indexOf("async function runExtraction()");
|
||||||
|
const resolvedEnd = end >= 0 ? end : endFallback;
|
||||||
|
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
|
||||||
|
throw new Error("无法从 index.js 提取生成召回事务定义");
|
||||||
|
}
|
||||||
|
const snippet = source
|
||||||
|
.slice(start, resolvedEnd)
|
||||||
|
.replace(/^export\s+/gm, "");
|
||||||
|
const context = {
|
||||||
|
console,
|
||||||
|
Date,
|
||||||
|
Map,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
__sendTextareaValue: "",
|
||||||
|
document: {
|
||||||
|
getElementById(id) {
|
||||||
|
if (
|
||||||
|
id === "send_textarea" &&
|
||||||
|
typeof context.__sendTextareaValue === "string" &&
|
||||||
|
context.__sendTextareaValue
|
||||||
|
) {
|
||||||
|
return { value: context.__sendTextareaValue };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: null,
|
||||||
|
currentGraph: {},
|
||||||
|
_panelModule: null,
|
||||||
|
defaultSettings: {},
|
||||||
|
settings: {},
|
||||||
|
graphPersistenceState: createGraphPersistenceState(),
|
||||||
|
extension_settings: { [MODULE_NAME]: {} },
|
||||||
|
extension_prompt_types: {
|
||||||
|
NONE: 0,
|
||||||
|
BEFORE_PROMPT: 1,
|
||||||
|
IN_PROMPT: 2,
|
||||||
|
IN_CHAT: 3,
|
||||||
|
},
|
||||||
|
extension_prompt_roles: {
|
||||||
|
SYSTEM: 0,
|
||||||
|
USER: 1,
|
||||||
|
ASSISTANT: 2,
|
||||||
|
},
|
||||||
|
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) return fallback;
|
||||||
|
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||||
|
},
|
||||||
|
getHostAdapter: () => null,
|
||||||
|
migrateLegacyTaskProfiles: (settings = {}) => ({
|
||||||
|
taskProfilesVersion: settings?.taskProfilesVersion || 0,
|
||||||
|
taskProfiles: settings?.taskProfiles || {},
|
||||||
|
}),
|
||||||
|
refreshPanelLiveStateController: () => {
|
||||||
|
context.refreshPanelCalls += 1;
|
||||||
|
},
|
||||||
|
isRecalling: false,
|
||||||
|
getCurrentChatId: () => "chat-main",
|
||||||
|
normalizeRecallInputText: (text = "") => String(text || "").trim(),
|
||||||
|
isTrivialUserInput,
|
||||||
|
getLatestUserChatMessage: (chat = []) =>
|
||||||
|
[...chat].reverse().find((message) => message?.is_user) || null,
|
||||||
|
getLastNonSystemChatMessage: (chat = []) =>
|
||||||
|
[...chat].reverse().find((message) => !message?.is_system) || null,
|
||||||
|
getSendTextareaValue: () => context.__sendTextareaValue,
|
||||||
|
getRecallUserMessageSourceLabel: (source = "") => source,
|
||||||
|
getRecallUserMessageSourceLabelController: (source = "") => source,
|
||||||
|
buildRecallRecentMessages: (
|
||||||
|
chat = [],
|
||||||
|
_limit,
|
||||||
|
syntheticUserMessage = "",
|
||||||
|
) =>
|
||||||
|
syntheticUserMessage
|
||||||
|
? [...chat, { is_user: true, mes: syntheticUserMessage }]
|
||||||
|
: [...chat],
|
||||||
|
getContext: () => ({
|
||||||
|
chatId: "chat-main",
|
||||||
|
chat: context.chat,
|
||||||
|
}),
|
||||||
|
chat: [],
|
||||||
|
runRecallCalls: [],
|
||||||
|
runExtractionCalls: [],
|
||||||
|
extractionIssues: [],
|
||||||
|
applyFinalCalls: [],
|
||||||
|
moduleInjectionCalls: [],
|
||||||
|
recordedInjectionSnapshots: [],
|
||||||
|
refreshPanelCalls: 0,
|
||||||
|
hideScheduleCalls: [],
|
||||||
|
createRecallInputRecord,
|
||||||
|
createRecallRunResult,
|
||||||
|
hashRecallInput,
|
||||||
|
isFreshRecallInputRecord,
|
||||||
|
isTerminalGenerationRecallHookState,
|
||||||
|
shouldRunRecallForTransaction,
|
||||||
|
getGenerationRecallHookStateFromResult,
|
||||||
|
createUiStatus,
|
||||||
|
createGraphPersistenceState,
|
||||||
|
getRecallHookLabel,
|
||||||
|
getStageNoticeTitle,
|
||||||
|
getStageNoticeDuration,
|
||||||
|
normalizeStageNoticeLevel,
|
||||||
|
MODULE_NAME,
|
||||||
|
GRAPH_LOAD_STATES,
|
||||||
|
GRAPH_METADATA_KEY,
|
||||||
|
GRAPH_PERSISTENCE_META_KEY,
|
||||||
|
onBeforeCombinePromptsController,
|
||||||
|
onGenerationAfterCommandsController,
|
||||||
|
onGenerationStartedController,
|
||||||
|
readPersistedRecallFromUserMessage,
|
||||||
|
writePersistedRecallToUserMessage,
|
||||||
|
buildPersistedRecallRecord,
|
||||||
|
resolveFinalRecallInjectionSource,
|
||||||
|
bumpPersistedRecallGenerationCount,
|
||||||
|
applyModuleInjectionPrompt: (text = "") => {
|
||||||
|
const normalizedText = String(text || "");
|
||||||
|
context.moduleInjectionCalls.push(normalizedText);
|
||||||
|
return {
|
||||||
|
applied: Boolean(normalizedText.trim()),
|
||||||
|
source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
||||||
|
mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getSettings: () => context.settings,
|
||||||
|
$: () => ({}),
|
||||||
|
triggerChatMetadataSave: () => {
|
||||||
|
context.metadataSaveCalls += 1;
|
||||||
|
return "debounced";
|
||||||
|
},
|
||||||
|
refreshPanelLiveState: () => {
|
||||||
|
context.refreshPanelCalls += 1;
|
||||||
|
},
|
||||||
|
recordInjectionSnapshot: (_kind, snapshot = {}) => {
|
||||||
|
context.recordedInjectionSnapshots.push({ ...snapshot });
|
||||||
|
},
|
||||||
|
schedulePersistedRecallMessageUiRefresh: () => {
|
||||||
|
context.recallUiRefreshCalls += 1;
|
||||||
|
},
|
||||||
|
getMessageHideSettings: () => ({}),
|
||||||
|
getHideRuntimeAdapters: () => ({}),
|
||||||
|
scheduleHideSettingsApply: (...args) => {
|
||||||
|
context.hideScheduleCalls.push(args);
|
||||||
|
},
|
||||||
|
estimateTokens: (text = "") =>
|
||||||
|
normalizeRecallInputText(text)
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
|
||||||
|
resolveGenerationTargetUserMessageIndex: (
|
||||||
|
chat = [],
|
||||||
|
{ generationType } = {},
|
||||||
|
) => {
|
||||||
|
const normalized = String(generationType || "normal");
|
||||||
|
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||||||
|
if (normalized === "normal")
|
||||||
|
return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
|
||||||
|
for (let index = chat.length - 1; index >= 0; index--)
|
||||||
|
if (chat[index]?.is_user) return index;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
metadataSaveCalls: 0,
|
||||||
|
recallUiRefreshCalls: 0,
|
||||||
|
};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(
|
||||||
|
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
|
||||||
|
context,
|
||||||
|
{ filename: indexPath },
|
||||||
|
);
|
||||||
|
Object.defineProperties(context, {
|
||||||
|
pendingRecallSendIntent: {
|
||||||
|
get() {
|
||||||
|
return context.result.getPendingRecallSendIntent();
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (value?.text) {
|
||||||
|
context.result.recordRecallSendIntent(
|
||||||
|
value?.text || "",
|
||||||
|
value?.source,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.result.clearPendingRecallSendIntent();
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
},
|
||||||
|
lastRecallSentUserMessage: {
|
||||||
|
get() {
|
||||||
|
return context.result.getLastRecallSentUserMessage();
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
context.result.recordRecallSentUserMessage(
|
||||||
|
value?.messageId,
|
||||||
|
value?.text || "",
|
||||||
|
value?.source,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const originalApplyFinalRecallInjectionForGeneration =
|
||||||
|
context.result.applyFinalRecallInjectionForGeneration;
|
||||||
|
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
|
||||||
|
context.applyFinalCalls.push({ ...payload });
|
||||||
|
if (realApplyFinal) {
|
||||||
|
return originalApplyFinalRecallInjectionForGeneration(payload);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
source: "fresh",
|
||||||
|
targetUserMessageIndex: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
context.runRecall = async (options = {}) => {
|
||||||
|
context.runRecallCalls.push({ ...options });
|
||||||
|
const overrideUserMessage = String(
|
||||||
|
options.overrideUserMessage || options.userMessage || "",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: "completed",
|
||||||
|
didRecall: true,
|
||||||
|
ok: true,
|
||||||
|
injectionText: `注入:${overrideUserMessage}`,
|
||||||
|
deliveryMode: String(options.deliveryMode || "immediate"),
|
||||||
|
source: options.overrideSource,
|
||||||
|
sourceLabel: options.overrideSourceLabel,
|
||||||
|
reason: options.overrideReason,
|
||||||
|
sourceCandidates: Array.isArray(options.sourceCandidates)
|
||||||
|
? options.sourceCandidates.map((candidate) => ({ ...candidate }))
|
||||||
|
: [],
|
||||||
|
selectedNodeIds: ["node-test-1"],
|
||||||
|
retrievalMeta: {
|
||||||
|
vectorHits: 1,
|
||||||
|
vectorMergedHits: 0,
|
||||||
|
diffusionHits: 0,
|
||||||
|
candidatePoolAfterDpp: 1,
|
||||||
|
},
|
||||||
|
llmMeta: {
|
||||||
|
status: "disabled",
|
||||||
|
reason: "test-disabled",
|
||||||
|
candidatePool: 0,
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
coreCount: 1,
|
||||||
|
recallCount: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
context.runExtraction = async (...args) => {
|
||||||
|
context.runExtractionCalls.push(args);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
context.invokeOnMessageSent = (messageId = null) =>
|
||||||
|
onMessageSentController(
|
||||||
|
{
|
||||||
|
getContext: context.getContext,
|
||||||
|
isTrivialUserInput,
|
||||||
|
recordRecallSentUserMessage: context.result.recordRecallSentUserMessage,
|
||||||
|
refreshPersistedRecallMessageUi: () => {
|
||||||
|
context.recallUiRefreshCalls += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageId,
|
||||||
|
);
|
||||||
|
context.invokeOnMessageReceived = (messageId = null, type = "") =>
|
||||||
|
onMessageReceivedController(
|
||||||
|
{
|
||||||
|
console,
|
||||||
|
consumeCurrentGenerationTrivialSkip:
|
||||||
|
context.result.consumeCurrentGenerationTrivialSkip,
|
||||||
|
createRecallInputRecord,
|
||||||
|
getContext: context.getContext,
|
||||||
|
getCurrentGraph: () => context.currentGraph,
|
||||||
|
getGraphPersistenceState: () => context.result.getGraphPersistenceState(),
|
||||||
|
getPendingHostGenerationInputSnapshot:
|
||||||
|
context.result.getPendingHostGenerationInputSnapshot,
|
||||||
|
getPendingRecallSendIntent: () => context.result.getPendingRecallSendIntent(),
|
||||||
|
isAssistantChatMessage: (message) =>
|
||||||
|
Boolean(message) && !message.is_user && !message.is_system,
|
||||||
|
isFreshRecallInputRecord,
|
||||||
|
isGraphMetadataWriteAllowed: () => true,
|
||||||
|
syncGraphLoadFromLiveContext: () => {},
|
||||||
|
maybeCaptureGraphShadowSnapshot: () => {},
|
||||||
|
maybeFlushQueuedGraphPersist: () => {},
|
||||||
|
notifyExtractionIssue: (message) => {
|
||||||
|
context.extractionIssues.push(String(message || ""));
|
||||||
|
},
|
||||||
|
queueMicrotask: (task) => task(),
|
||||||
|
runExtraction: context.runExtraction,
|
||||||
|
refreshPersistedRecallMessageUi: () => {
|
||||||
|
context.recallUiRefreshCalls += 1;
|
||||||
|
},
|
||||||
|
setPendingHostGenerationInputSnapshot: () => {},
|
||||||
|
setPendingRecallSendIntent: (record) => {
|
||||||
|
if (record?.text) {
|
||||||
|
context.result.recordRecallSendIntent(
|
||||||
|
record.text || "",
|
||||||
|
record.source,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.result.clearPendingRecallSendIntent();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageId,
|
||||||
|
type,
|
||||||
|
);
|
||||||
|
return context;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ import {
|
|||||||
onManualEvolveController,
|
onManualEvolveController,
|
||||||
onManualSleepController,
|
onManualSleepController,
|
||||||
} from "../ui-actions-controller.js";
|
} from "../ui-actions-controller.js";
|
||||||
|
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
|
||||||
|
|
||||||
const waitForTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
const waitForTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
const extensionsShimSource = [
|
const extensionsShimSource = [
|
||||||
@@ -281,255 +282,6 @@ function createBatchStageHarness() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGenerationRecallHarness(options = {}) {
|
|
||||||
const { realApplyFinal = false } = options;
|
|
||||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
|
||||||
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
|
|
||||||
const end = source.indexOf(
|
|
||||||
'function onMessageReceived(messageId = null, type = "") {',
|
|
||||||
);
|
|
||||||
const resolvedEnd = end >= 0 ? end : endFallback;
|
|
||||||
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
|
|
||||||
throw new Error("无法从 index.js 提取生成召回事务定义");
|
|
||||||
}
|
|
||||||
const snippet = source
|
|
||||||
.slice(start, resolvedEnd)
|
|
||||||
.replace(/^export\s+/gm, "");
|
|
||||||
const context = {
|
|
||||||
console,
|
|
||||||
Date,
|
|
||||||
Map,
|
|
||||||
setTimeout,
|
|
||||||
clearTimeout,
|
|
||||||
__sendTextareaValue: "",
|
|
||||||
document: {
|
|
||||||
getElementById(id) {
|
|
||||||
if (
|
|
||||||
id === "send_textarea" &&
|
|
||||||
typeof context.__sendTextareaValue === "string" &&
|
|
||||||
context.__sendTextareaValue
|
|
||||||
) {
|
|
||||||
return { value: context.__sendTextareaValue };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: null,
|
|
||||||
currentGraph: {},
|
|
||||||
_panelModule: null,
|
|
||||||
defaultSettings: {},
|
|
||||||
extension_settings: { [MODULE_NAME]: {} },
|
|
||||||
extension_prompt_types: {
|
|
||||||
NONE: 0,
|
|
||||||
BEFORE_PROMPT: 1,
|
|
||||||
IN_PROMPT: 2,
|
|
||||||
IN_CHAT: 3,
|
|
||||||
},
|
|
||||||
extension_prompt_roles: {
|
|
||||||
SYSTEM: 0,
|
|
||||||
USER: 1,
|
|
||||||
ASSISTANT: 2,
|
|
||||||
},
|
|
||||||
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric)) return fallback;
|
|
||||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
|
||||||
},
|
|
||||||
getHostAdapter: () => null,
|
|
||||||
migrateLegacyTaskProfiles: (settings = {}) => ({
|
|
||||||
taskProfilesVersion: settings?.taskProfilesVersion || 0,
|
|
||||||
taskProfiles: settings?.taskProfiles || {},
|
|
||||||
}),
|
|
||||||
refreshPanelLiveStateController: () => {
|
|
||||||
context.refreshPanelCalls += 1;
|
|
||||||
},
|
|
||||||
isRecalling: false,
|
|
||||||
getCurrentChatId: () => "chat-main",
|
|
||||||
normalizeRecallInputText: (text = "") => String(text || "").trim(),
|
|
||||||
getLatestUserChatMessage: (chat = []) =>
|
|
||||||
[...chat].reverse().find((message) => message?.is_user) || null,
|
|
||||||
getLastNonSystemChatMessage: (chat = []) =>
|
|
||||||
[...chat].reverse().find((message) => !message?.is_system) || null,
|
|
||||||
getSendTextareaValue: () => context.__sendTextareaValue,
|
|
||||||
getRecallUserMessageSourceLabel: (source = "") => source,
|
|
||||||
getRecallUserMessageSourceLabelController: (source = "") => source,
|
|
||||||
buildRecallRecentMessages: (
|
|
||||||
chat = [],
|
|
||||||
_limit,
|
|
||||||
syntheticUserMessage = "",
|
|
||||||
) =>
|
|
||||||
syntheticUserMessage
|
|
||||||
? [...chat, { is_user: true, mes: syntheticUserMessage }]
|
|
||||||
: [...chat],
|
|
||||||
getContext: () => ({
|
|
||||||
chatId: "chat-main",
|
|
||||||
chat: context.chat,
|
|
||||||
}),
|
|
||||||
chat: [],
|
|
||||||
runRecallCalls: [],
|
|
||||||
applyFinalCalls: [],
|
|
||||||
moduleInjectionCalls: [],
|
|
||||||
recordedInjectionSnapshots: [],
|
|
||||||
refreshPanelCalls: 0,
|
|
||||||
hideScheduleCalls: [],
|
|
||||||
createRecallInputRecord,
|
|
||||||
createRecallRunResult,
|
|
||||||
hashRecallInput,
|
|
||||||
normalizeRecallInputText,
|
|
||||||
isFreshRecallInputRecord,
|
|
||||||
isTerminalGenerationRecallHookState,
|
|
||||||
shouldRunRecallForTransaction,
|
|
||||||
getGenerationRecallHookStateFromResult,
|
|
||||||
createUiStatus,
|
|
||||||
createGraphPersistenceState,
|
|
||||||
getRecallHookLabel,
|
|
||||||
getStageNoticeTitle,
|
|
||||||
getStageNoticeDuration,
|
|
||||||
normalizeStageNoticeLevel,
|
|
||||||
MODULE_NAME,
|
|
||||||
GRAPH_LOAD_STATES,
|
|
||||||
GRAPH_METADATA_KEY,
|
|
||||||
GRAPH_PERSISTENCE_META_KEY,
|
|
||||||
onBeforeCombinePromptsController,
|
|
||||||
onGenerationAfterCommandsController,
|
|
||||||
onGenerationStartedController,
|
|
||||||
readPersistedRecallFromUserMessage,
|
|
||||||
writePersistedRecallToUserMessage,
|
|
||||||
buildPersistedRecallRecord,
|
|
||||||
resolveFinalRecallInjectionSource,
|
|
||||||
bumpPersistedRecallGenerationCount,
|
|
||||||
applyModuleInjectionPrompt: (text = "") => {
|
|
||||||
const normalizedText = String(text || "");
|
|
||||||
context.moduleInjectionCalls.push(normalizedText);
|
|
||||||
return {
|
|
||||||
applied: Boolean(normalizedText.trim()),
|
|
||||||
source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
|
||||||
mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getSettings: () => ({}),
|
|
||||||
$: () => ({}),
|
|
||||||
triggerChatMetadataSave: () => {
|
|
||||||
context.metadataSaveCalls += 1;
|
|
||||||
return "debounced";
|
|
||||||
},
|
|
||||||
refreshPanelLiveState: () => {
|
|
||||||
context.refreshPanelCalls += 1;
|
|
||||||
},
|
|
||||||
recordInjectionSnapshot: (_kind, snapshot = {}) => {
|
|
||||||
context.recordedInjectionSnapshots.push({ ...snapshot });
|
|
||||||
},
|
|
||||||
schedulePersistedRecallMessageUiRefresh: () => {
|
|
||||||
context.recallUiRefreshCalls += 1;
|
|
||||||
},
|
|
||||||
getMessageHideSettings: () => ({}),
|
|
||||||
getHideRuntimeAdapters: () => ({}),
|
|
||||||
scheduleHideSettingsApply: (...args) => {
|
|
||||||
context.hideScheduleCalls.push(args);
|
|
||||||
},
|
|
||||||
estimateTokens: (text = "") =>
|
|
||||||
normalizeRecallInputText(text)
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
|
|
||||||
resolveGenerationTargetUserMessageIndex: (
|
|
||||||
chat = [],
|
|
||||||
{ generationType } = {},
|
|
||||||
) => {
|
|
||||||
const normalized = String(generationType || "normal");
|
|
||||||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
|
||||||
if (normalized === "normal")
|
|
||||||
return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
|
|
||||||
for (let index = chat.length - 1; index >= 0; index--)
|
|
||||||
if (chat[index]?.is_user) return index;
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
metadataSaveCalls: 0,
|
|
||||||
recallUiRefreshCalls: 0,
|
|
||||||
};
|
|
||||||
vm.createContext(context);
|
|
||||||
vm.runInContext(
|
|
||||||
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, recordRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage };`,
|
|
||||||
context,
|
|
||||||
{ filename: indexPath },
|
|
||||||
);
|
|
||||||
Object.defineProperties(context, {
|
|
||||||
pendingRecallSendIntent: {
|
|
||||||
get() {
|
|
||||||
return context.result.getPendingRecallSendIntent();
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
context.result.recordRecallSendIntent(
|
|
||||||
value?.text || "",
|
|
||||||
value?.source,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
configurable: true,
|
|
||||||
},
|
|
||||||
lastRecallSentUserMessage: {
|
|
||||||
get() {
|
|
||||||
return context.result.getLastRecallSentUserMessage();
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
context.result.recordRecallSentUserMessage(
|
|
||||||
value?.messageId,
|
|
||||||
value?.text || "",
|
|
||||||
value?.source,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
configurable: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const originalApplyFinalRecallInjectionForGeneration =
|
|
||||||
context.result.applyFinalRecallInjectionForGeneration;
|
|
||||||
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
|
|
||||||
context.applyFinalCalls.push({ ...payload });
|
|
||||||
if (realApplyFinal) {
|
|
||||||
return originalApplyFinalRecallInjectionForGeneration(payload);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
source: "fresh",
|
|
||||||
targetUserMessageIndex: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
context.runRecall = async (options = {}) => {
|
|
||||||
context.runRecallCalls.push({ ...options });
|
|
||||||
const overrideUserMessage = String(
|
|
||||||
options.overrideUserMessage || options.userMessage || "",
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
status: "completed",
|
|
||||||
didRecall: true,
|
|
||||||
ok: true,
|
|
||||||
injectionText: `注入:${overrideUserMessage}`,
|
|
||||||
deliveryMode: String(options.deliveryMode || "immediate"),
|
|
||||||
source: options.overrideSource,
|
|
||||||
sourceLabel: options.overrideSourceLabel,
|
|
||||||
reason: options.overrideReason,
|
|
||||||
sourceCandidates: Array.isArray(options.sourceCandidates)
|
|
||||||
? options.sourceCandidates.map((candidate) => ({ ...candidate }))
|
|
||||||
: [],
|
|
||||||
selectedNodeIds: ["node-test-1"],
|
|
||||||
retrievalMeta: {
|
|
||||||
vectorHits: 1,
|
|
||||||
vectorMergedHits: 0,
|
|
||||||
diffusionHits: 0,
|
|
||||||
candidatePoolAfterDpp: 1,
|
|
||||||
},
|
|
||||||
llmMeta: {
|
|
||||||
status: "disabled",
|
|
||||||
reason: "test-disabled",
|
|
||||||
candidatePool: 0,
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
coreCount: 1,
|
|
||||||
recallCount: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return context;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHistoryRecoveryHarness() {
|
function createHistoryRecoveryHarness() {
|
||||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||||
const start = source.indexOf("async function recoverHistoryIfNeeded(");
|
const start = source.indexOf("async function recoverHistoryIfNeeded(");
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ const chat = [
|
|||||||
const hashes = snapshotProcessedMessageHashes(chat, 3);
|
const hashes = snapshotProcessedMessageHashes(chat, 3);
|
||||||
const cleanDetection = detectHistoryMutation(chat, {
|
const cleanDetection = detectHistoryMutation(chat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: hashes,
|
processedMessageHashes: hashes,
|
||||||
});
|
});
|
||||||
assert.equal(cleanDetection.dirty, false);
|
assert.equal(cleanDetection.dirty, false);
|
||||||
|
|
||||||
const missingHashesDetection = detectHistoryMutation(chat, {
|
const missingHashesDetection = detectHistoryMutation(chat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: {},
|
processedMessageHashes: {},
|
||||||
});
|
});
|
||||||
assert.equal(missingHashesDetection.dirty, true);
|
assert.equal(missingHashesDetection.dirty, true);
|
||||||
@@ -35,6 +37,7 @@ assert.equal(missingHashesDetection.earliestAffectedFloor, 0);
|
|||||||
|
|
||||||
const sparseHashesDetection = detectHistoryMutation(chat, {
|
const sparseHashesDetection = detectHistoryMutation(chat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: {
|
processedMessageHashes: {
|
||||||
0: hashes[0],
|
0: hashes[0],
|
||||||
2: hashes[2],
|
2: hashes[2],
|
||||||
@@ -48,6 +51,7 @@ const editedChat = structuredClone(chat);
|
|||||||
editedChat[1].mes = "我改过内容了。";
|
editedChat[1].mes = "我改过内容了。";
|
||||||
const editedDetection = detectHistoryMutation(editedChat, {
|
const editedDetection = detectHistoryMutation(editedChat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: hashes,
|
processedMessageHashes: hashes,
|
||||||
});
|
});
|
||||||
assert.equal(editedDetection.dirty, true);
|
assert.equal(editedDetection.dirty, true);
|
||||||
@@ -58,6 +62,7 @@ bmeHiddenChat[1].is_system = true;
|
|||||||
bmeHiddenChat[1].extra = { __st_bme_hide_managed: true };
|
bmeHiddenChat[1].extra = { __st_bme_hide_managed: true };
|
||||||
const bmeHiddenDetection = detectHistoryMutation(bmeHiddenChat, {
|
const bmeHiddenDetection = detectHistoryMutation(bmeHiddenChat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: hashes,
|
processedMessageHashes: hashes,
|
||||||
});
|
});
|
||||||
assert.equal(bmeHiddenDetection.dirty, false);
|
assert.equal(bmeHiddenDetection.dirty, false);
|
||||||
@@ -66,6 +71,7 @@ const realSystemFlipChat = structuredClone(chat);
|
|||||||
realSystemFlipChat[1].is_system = true;
|
realSystemFlipChat[1].is_system = true;
|
||||||
const realSystemFlipDetection = detectHistoryMutation(realSystemFlipChat, {
|
const realSystemFlipDetection = detectHistoryMutation(realSystemFlipChat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: hashes,
|
processedMessageHashes: hashes,
|
||||||
});
|
});
|
||||||
assert.equal(realSystemFlipDetection.dirty, false);
|
assert.equal(realSystemFlipDetection.dirty, false);
|
||||||
@@ -91,6 +97,7 @@ assert.equal(migratedDetection.dirty, false);
|
|||||||
const truncatedChat = chat.slice(0, 2);
|
const truncatedChat = chat.slice(0, 2);
|
||||||
const truncatedDetection = detectHistoryMutation(truncatedChat, {
|
const truncatedDetection = detectHistoryMutation(truncatedChat, {
|
||||||
lastProcessedAssistantFloor: 3,
|
lastProcessedAssistantFloor: 3,
|
||||||
|
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
|
||||||
processedMessageHashes: hashes,
|
processedMessageHashes: hashes,
|
||||||
});
|
});
|
||||||
assert.equal(truncatedDetection.dirty, true);
|
assert.equal(truncatedDetection.dirty, true);
|
||||||
|
|||||||
299
tests/trivial-user-input.mjs
Normal file
299
tests/trivial-user-input.mjs
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
// wired into npm run test:all
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { MODULE_NAME } from "../graph-persistence.js";
|
||||||
|
import { isTrivialUserInput } from "../ui-status.js";
|
||||||
|
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
|
||||||
|
|
||||||
|
function assertEmptyRecallInputRecord(record) {
|
||||||
|
assert.deepEqual(record, {
|
||||||
|
text: "",
|
||||||
|
hash: "",
|
||||||
|
messageId: null,
|
||||||
|
source: "",
|
||||||
|
at: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testIsTrivialUserInputTable() {
|
||||||
|
const cases = [
|
||||||
|
["", true, "empty"],
|
||||||
|
[" \n\t ", true, "empty"],
|
||||||
|
["/echo hello", true, "slash-command"],
|
||||||
|
["/", true, "slash-command"],
|
||||||
|
[" /echo", true, "slash-command"],
|
||||||
|
["a", true, "under-min-tokens"],
|
||||||
|
["好", true, "under-min-tokens"],
|
||||||
|
["ok", true, "under-min-tokens"],
|
||||||
|
["ok a", false, ""],
|
||||||
|
["好的", false, ""],
|
||||||
|
["好的呀", false, ""],
|
||||||
|
["hello world", false, ""],
|
||||||
|
["你好", false, ""],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [input, trivial, reason] of cases) {
|
||||||
|
const result = isTrivialUserInput(input);
|
||||||
|
assert.equal(result.trivial, trivial, `trivial mismatch for ${JSON.stringify(input)}`);
|
||||||
|
assert.equal(result.reason, reason, `reason mismatch for ${JSON.stringify(input)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSlashCommandSkipsRecallAndExtraction() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [];
|
||||||
|
harness.__sendTextareaValue = "/echo test";
|
||||||
|
|
||||||
|
const startResult = harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.equal(startResult, null);
|
||||||
|
assertEmptyRecallInputRecord(harness.result.getPendingHostGenerationInputSnapshot());
|
||||||
|
assertEmptyRecallInputRecord(harness.pendingRecallSendIntent);
|
||||||
|
assert.equal(
|
||||||
|
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||||
|
assert.equal(harness.runRecallCalls.length, 0);
|
||||||
|
|
||||||
|
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||||
|
assert.deepEqual(beforeCombine, {
|
||||||
|
skipped: true,
|
||||||
|
reason: "trivial:slash-command",
|
||||||
|
});
|
||||||
|
assert.equal(harness.runRecallCalls.length, 0);
|
||||||
|
|
||||||
|
harness.chat.push({ is_user: false, mes: "assistant reply" });
|
||||||
|
harness.invokeOnMessageReceived(0, "");
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 0);
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testUnderMinTokensSkipsRecallAndExtraction() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [];
|
||||||
|
harness.__sendTextareaValue = "a";
|
||||||
|
|
||||||
|
const startResult = harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.equal(startResult, null);
|
||||||
|
assert.equal(
|
||||||
|
harness.result.getCurrentGenerationTrivialSkip()?.reason,
|
||||||
|
"under-min-tokens",
|
||||||
|
);
|
||||||
|
|
||||||
|
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||||
|
assert.equal(harness.runRecallCalls.length, 0);
|
||||||
|
|
||||||
|
harness.chat.push({ is_user: false, mes: "assistant reply" });
|
||||||
|
harness.invokeOnMessageReceived(0, "");
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 0);
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testEmptyInputSkipsPriorHistoryFallback() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [{ is_user: true, mes: "older real user message" }];
|
||||||
|
harness.__sendTextareaValue = " ";
|
||||||
|
|
||||||
|
const startResult = harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.equal(startResult, null);
|
||||||
|
assert.equal(
|
||||||
|
harness.result.getCurrentGenerationTrivialSkip()?.reason,
|
||||||
|
"empty",
|
||||||
|
);
|
||||||
|
|
||||||
|
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||||
|
assert.equal(harness.runRecallCalls.length, 0);
|
||||||
|
|
||||||
|
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||||
|
assert.deepEqual(beforeCombine, {
|
||||||
|
skipped: true,
|
||||||
|
reason: "trivial:empty",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testNormalInputStillRecalls() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [];
|
||||||
|
harness.__sendTextareaValue = "好的呀";
|
||||||
|
|
||||||
|
const snapshot = harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.equal(snapshot?.text, "好的呀");
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
|
||||||
|
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||||
|
assert.equal(beforeCombine?.source, "fresh");
|
||||||
|
assert.equal(harness.runRecallCalls.length, 1);
|
||||||
|
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "好的呀");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSentinelBlocksHistoryFallback() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [{ is_user: true, mes: "真实旧消息" }];
|
||||||
|
harness.pendingRecallSendIntent = {
|
||||||
|
text: "/echo hidden",
|
||||||
|
source: "send-button",
|
||||||
|
at: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeCombine = await harness.result.onBeforeCombinePrompts();
|
||||||
|
assert.deepEqual(beforeCombine, {
|
||||||
|
skipped: true,
|
||||||
|
reason: "trivial:slash-command",
|
||||||
|
});
|
||||||
|
assert.equal(harness.runRecallCalls.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAfterCommandsTrivialSentinelMarksExtractionBypass() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [{ is_user: true, mes: "/echo from chat tail" }];
|
||||||
|
|
||||||
|
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||||
|
assert.equal(harness.runRecallCalls.length, 0);
|
||||||
|
assert.equal(
|
||||||
|
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.chat.push({ is_user: false, mes: "assistant after bypass flag" });
|
||||||
|
harness.invokeOnMessageReceived(1, "");
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 0);
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPlannerRecallTrivialAndNonTrivialPaths() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
|
||||||
|
let recall = await harness.result.runPlannerRecallForEna({
|
||||||
|
rawUserInput: "",
|
||||||
|
});
|
||||||
|
assert.equal(recall.reason, "trivial-user-input:empty");
|
||||||
|
|
||||||
|
recall = await harness.result.runPlannerRecallForEna({
|
||||||
|
rawUserInput: "/echo",
|
||||||
|
});
|
||||||
|
assert.equal(recall.reason, "trivial-user-input:slash-command");
|
||||||
|
|
||||||
|
harness.extension_settings[MODULE_NAME] = {
|
||||||
|
enabled: true,
|
||||||
|
recallEnabled: true,
|
||||||
|
};
|
||||||
|
harness.result.setGraphPersistenceState({
|
||||||
|
loadState: "loaded",
|
||||||
|
dbReady: true,
|
||||||
|
});
|
||||||
|
harness.currentGraph = {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
historyState: {},
|
||||||
|
};
|
||||||
|
recall = await harness.result.runPlannerRecallForEna({
|
||||||
|
rawUserInput: "好的呀",
|
||||||
|
});
|
||||||
|
assert.equal(recall.reason, "graph-empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testOnMessageSentSkipsTrivialText() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [{ is_user: true, mes: "/echo" }];
|
||||||
|
|
||||||
|
harness.invokeOnMessageSent(0);
|
||||||
|
|
||||||
|
assert.equal(harness.lastRecallSentUserMessage.text, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testNonTrivialGenerationClearsResidualTrivialSkip() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [];
|
||||||
|
harness.__sendTextareaValue = "/echo";
|
||||||
|
harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.ok(harness.result.getCurrentGenerationTrivialSkip());
|
||||||
|
|
||||||
|
harness.__sendTextareaValue = "hello world";
|
||||||
|
const snapshot = harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.equal(snapshot?.text, "hello world");
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
|
||||||
|
harness.chat.push({ is_user: false, mes: "assistant after non-trivial" });
|
||||||
|
harness.invokeOnMessageReceived(0, "");
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testNonTargetMessageIdDoesNotConsumeFlag() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [
|
||||||
|
{ is_user: true, mes: "u0" },
|
||||||
|
{ is_user: false, mes: "a1" },
|
||||||
|
{ is_user: true, mes: "u2" },
|
||||||
|
{ is_user: false, mes: "old assistant" },
|
||||||
|
{ is_user: true, mes: "u4" },
|
||||||
|
];
|
||||||
|
harness.__sendTextareaValue = "/echo";
|
||||||
|
harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
assert.equal(
|
||||||
|
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.invokeOnMessageReceived(3, "");
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 1);
|
||||||
|
assert.ok(harness.result.getCurrentGenerationTrivialSkip());
|
||||||
|
|
||||||
|
harness.chat.push({ is_user: false, mes: "target assistant" });
|
||||||
|
harness.invokeOnMessageReceived(5, "");
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 1);
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testNullMessageIdFallsBackToLastAssistantIndex() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [
|
||||||
|
{ is_user: true, mes: "u0" },
|
||||||
|
{ is_user: false, mes: "a1" },
|
||||||
|
{ is_user: true, mes: "u2" },
|
||||||
|
{ is_user: false, mes: "a3" },
|
||||||
|
{ is_user: true, mes: "u4" },
|
||||||
|
];
|
||||||
|
harness.__sendTextareaValue = "/echo";
|
||||||
|
harness.result.onGenerationStarted("normal", {}, false);
|
||||||
|
|
||||||
|
harness.chat.push({ is_user: false, mes: "latest assistant" });
|
||||||
|
harness.invokeOnMessageReceived(null, "");
|
||||||
|
assert.equal(harness.runExtractionCalls.length, 0);
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSkipFlagTtlExpires() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.result.markCurrentGenerationTrivialSkip({
|
||||||
|
reason: "slash-command",
|
||||||
|
chatId: "chat-main",
|
||||||
|
chatLength: 2,
|
||||||
|
});
|
||||||
|
const originalNow = Date.now;
|
||||||
|
Date.now = () => originalNow() + 60001;
|
||||||
|
try {
|
||||||
|
assert.equal(harness.result.consumeCurrentGenerationTrivialSkip(2), false);
|
||||||
|
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
|
||||||
|
} finally {
|
||||||
|
Date.now = originalNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
testIsTrivialUserInputTable();
|
||||||
|
await testSlashCommandSkipsRecallAndExtraction();
|
||||||
|
await testUnderMinTokensSkipsRecallAndExtraction();
|
||||||
|
await testEmptyInputSkipsPriorHistoryFallback();
|
||||||
|
await testNormalInputStillRecalls();
|
||||||
|
await testSentinelBlocksHistoryFallback();
|
||||||
|
await testAfterCommandsTrivialSentinelMarksExtractionBypass();
|
||||||
|
await testPlannerRecallTrivialAndNonTrivialPaths();
|
||||||
|
await testOnMessageSentSkipsTrivialText();
|
||||||
|
await testNonTrivialGenerationClearsResidualTrivialSkip();
|
||||||
|
await testNonTargetMessageIdDoesNotConsumeFlag();
|
||||||
|
await testNullMessageIdFallsBackToLastAssistantIndex();
|
||||||
|
await testSkipFlagTtlExpires();
|
||||||
|
|
||||||
|
console.log("trivial-user-input tests passed");
|
||||||
50
ui-status.js
50
ui-status.js
@@ -323,6 +323,56 @@ export function normalizeRecallInputText(value) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TRIVIAL_INPUT_MIN_TOKENS = 2;
|
||||||
|
const TRIVIAL_INPUT_CJK_TOKEN_REGEX =
|
||||||
|
/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Hangul}/gu;
|
||||||
|
|
||||||
|
function estimateTrivialInputTokens(text = "") {
|
||||||
|
const normalized = normalizeRecallInputText(text);
|
||||||
|
if (!normalized) return 0;
|
||||||
|
|
||||||
|
const cjkMatches = normalized.match(TRIVIAL_INPUT_CJK_TOKEN_REGEX) || [];
|
||||||
|
const nonCjkText = normalized.replace(TRIVIAL_INPUT_CJK_TOKEN_REGEX, " ");
|
||||||
|
const wordTokens = nonCjkText
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return cjkMatches.length + wordTokens.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTrivialUserInput(text) {
|
||||||
|
const normalizedText = normalizeRecallInputText(text);
|
||||||
|
if (!normalizedText) {
|
||||||
|
return {
|
||||||
|
trivial: true,
|
||||||
|
reason: "empty",
|
||||||
|
normalizedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedText.startsWith("/")) {
|
||||||
|
return {
|
||||||
|
trivial: true,
|
||||||
|
reason: "slash-command",
|
||||||
|
normalizedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estimateTrivialInputTokens(normalizedText) < TRIVIAL_INPUT_MIN_TOKENS) {
|
||||||
|
return {
|
||||||
|
trivial: true,
|
||||||
|
reason: "under-min-tokens",
|
||||||
|
normalizedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
trivial: false,
|
||||||
|
reason: "",
|
||||||
|
normalizedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function hashRecallInput(text) {
|
export function hashRecallInput(text) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
const normalized = normalizeRecallInputText(text);
|
const normalized = normalizeRecallInputText(text);
|
||||||
|
|||||||
Reference in New Issue
Block a user