Merge branch 'main' into main

This commit is contained in:
Hao19911125
2026-04-10 18:09:40 +08:00
committed by GitHub
29 changed files with 2090 additions and 407 deletions

View File

@@ -5,6 +5,7 @@ import {
resetHideState,
} from "../ui/hide-engine.js";
import {
buildPluginVisibleChatMessages,
buildExtractionMessages,
getAssistantTurns,
isAssistantChatMessage,
@@ -36,6 +37,25 @@ const realSystemMessage = {
};
assert.equal(isSystemMessageForExtraction(realSystemMessage), true);
assert.equal(isAssistantChatMessage(realSystemMessage), false);
const pluginVisibleChat = buildPluginVisibleChatMessages([
realSystemMessage,
managedHiddenAssistant,
]);
assert.equal(
pluginVisibleChat[0].is_system,
true,
"real system message should remain system in plugin-visible chat",
);
assert.equal(
pluginVisibleChat[1].is_system,
false,
"BME-managed hidden message should be restored for plugin-internal chat views",
);
assert.equal(
managedHiddenAssistant.is_system,
true,
"plugin-visible chat clone must not mutate original managed hidden message",
);
function createRuntime(chat, chatId = "chat-a") {
return {

View File

@@ -12,12 +12,15 @@ function createRuntime(persistResult) {
nodes: [],
edges: [],
historyState: {},
batchJournal: [],
};
let processedHistoryUpdates = 0;
let persistedGraphSnapshot = null;
return {
graph,
processedHistoryUpdates,
persistedGraphSnapshot,
ensureCurrentGraphRuntimeState() {},
throwIfAborted() {},
getCurrentGraph() {
@@ -64,6 +67,7 @@ function createRuntime(persistResult) {
};
},
async persistExtractionBatchResult() {
persistedGraphSnapshot = arguments[0]?.graphSnapshot || null;
return persistResult;
},
finalizeBatchStatus,
@@ -73,13 +77,20 @@ function createRuntime(persistResult) {
updateProcessedHistorySnapshot() {
processedHistoryUpdates += 1;
},
appendBatchJournal() {},
appendBatchJournal(targetGraph, entry) {
if (!targetGraph.batchJournal) targetGraph.batchJournal = [];
targetGraph.batchJournal.push(entry);
},
createBatchJournalEntry() {
return { id: "journal-1" };
return { id: "journal-1", processedRange: [5, 5] };
},
computePostProcessArtifacts() {
return [];
},
applyProcessedHistorySnapshotToGraph(targetGraph, _chat, floor) {
targetGraph.historyState.lastProcessedAssistantFloor = floor;
targetGraph.lastProcessedSeq = floor;
},
getGraphPersistenceState() {
return { chatId: "chat-test" };
},
@@ -87,6 +98,9 @@ function createRuntime(persistResult) {
get processedHistoryUpdates() {
return processedHistoryUpdates;
},
get persistedGraphSnapshot() {
return persistedGraphSnapshot;
},
};
}
@@ -119,6 +133,14 @@ function createRuntime(persistResult) {
runtime.graph.historyState.lastBatchStatus.historyAdvanceAllowed,
false,
);
assert.equal(
runtime.persistedGraphSnapshot?.historyState?.lastProcessedAssistantFloor,
5,
);
assert.equal(
runtime.persistedGraphSnapshot?.batchJournal?.length,
1,
);
}
{
@@ -150,6 +172,14 @@ function createRuntime(persistResult) {
runtime.graph.historyState.lastBatchStatus.historyAdvanceAllowed,
true,
);
assert.equal(
runtime.persistedGraphSnapshot?.historyState?.lastProcessedAssistantFloor,
5,
);
assert.equal(
runtime.persistedGraphSnapshot?.batchJournal?.length,
1,
);
}
console.log("extraction-persistence-gating tests passed");

View File

@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = {};",
@@ -34,39 +37,30 @@ const openAiShimSource = [
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
if (
specifier === "../../../../openai.js" ||
specifier === "../../../../../openai.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: toDataModuleUrl(scriptShimSource),
},
{
specifiers: [
"../../../../openai.js",
"../../../../../openai.js",
],
url: toDataModuleUrl(openAiShimSource),
},
]);
const { createEmptyGraph, createNode, addNode } = await import("../graph/graph.js");
const { DEFAULT_NODE_SCHEMA } = await import("../graph/schema.js");

View File

@@ -12,10 +12,13 @@ import {
import { onMessageReceivedController } from "../host/event-binding.js";
import {
buildGraphCommitMarker,
buildGraphChatStateSnapshot,
canUseGraphChatState,
detectIndexedDbSnapshotCommitMarkerMismatch,
cloneGraphForPersistence,
cloneRuntimeDebugValue,
findGraphShadowSnapshotByIntegrity,
GRAPH_CHAT_STATE_NAMESPACE,
getAcceptedCommitMarkerRevision,
getGraphPersistedRevision,
getGraphIdentityAliasCandidates,
@@ -33,6 +36,7 @@ import {
MODULE_NAME,
normalizeGraphCommitMarker,
readGraphCommitMarker,
readGraphChatStateSnapshot,
readGraphShadowSnapshot,
rememberGraphIdentityAlias,
removeGraphShadowSnapshot,
@@ -40,6 +44,7 @@ import {
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphChatStateSnapshot,
writeGraphShadowSnapshot,
} from "../graph/graph-persistence.js";
import {
@@ -391,9 +396,12 @@ async function createGraphPersistenceHarness({
readPersistedRecallFromUserMessage,
cloneGraphForPersistence,
buildGraphCommitMarker,
buildGraphChatStateSnapshot,
canUseGraphChatState,
cloneRuntimeDebugValue,
detectIndexedDbSnapshotCommitMarkerMismatch,
onMessageReceivedController,
GRAPH_CHAT_STATE_NAMESPACE,
getAcceptedCommitMarkerRevision,
getGraphPersistenceMeta,
getGraphPersistedRevision,
@@ -412,6 +420,7 @@ async function createGraphPersistenceHarness({
findGraphShadowSnapshotByIntegrity,
normalizeGraphCommitMarker,
readGraphCommitMarker,
readGraphChatStateSnapshot,
readGraphShadowSnapshot,
rememberGraphIdentityAlias,
removeGraphShadowSnapshot,
@@ -419,6 +428,7 @@ async function createGraphPersistenceHarness({
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphChatStateSnapshot,
writeGraphShadowSnapshot,
// Shadow snapshot functions need VM-local sessionStorage overrides
// because imported versions use the outer globalThis (no sessionStorage)
@@ -711,6 +721,7 @@ async function createGraphPersistenceHarness({
characterId,
groupId,
chat,
__chatStateStore: new Map(),
updateChatMetadata(patch) {
const base =
this.chatMetadata &&
@@ -729,6 +740,36 @@ async function createGraphPersistenceHarness({
async saveMetadata() {
runtimeContext.__contextImmediateSaveCalls += 1;
},
async getChatState(namespace) {
const key = String(namespace || "").trim().toLowerCase();
const value = this.__chatStateStore.get(key);
return value == null ? null : structuredClone(value);
},
async updateChatState(namespace, updater) {
const key = String(namespace || "").trim().toLowerCase();
if (!key || typeof updater !== "function") {
return { ok: false, state: null, updated: false };
}
const current = this.__chatStateStore.has(key)
? structuredClone(this.__chatStateStore.get(key))
: {};
const next = await updater(structuredClone(current), {
attempt: 0,
target: null,
namespace: key,
});
if (next == null) {
return { ok: true, state: current, updated: false };
}
const currentJson = JSON.stringify(current);
const nextJson = JSON.stringify(next);
this.__chatStateStore.set(key, structuredClone(next));
return {
ok: true,
state: structuredClone(next),
updated: currentJson !== nextJson,
};
},
},
__contextSaveCalls: 0,
__contextImmediateSaveCalls: 0,
@@ -2332,6 +2373,16 @@ result = {
historyAdvanceAllowed: false,
historyAdvanced: false,
};
const committedGraph = structuredClone(graph);
committedGraph.historyState.lastProcessedAssistantFloor = 1;
committedGraph.lastProcessedSeq = 1;
committedGraph.batchJournal = [
{
id: "journal-queued-1",
processedRange: [1, 1],
createdAt: Date.now(),
},
];
harness.api.setCurrentGraph(graph);
harness.api.setGraphPersistenceState({
loadState: "loaded",
@@ -2344,6 +2395,14 @@ result = {
pendingPersist: true,
writesBlocked: false,
});
harness.api.writeGraphShadowSnapshot(
"chat-pending-persist-retry",
committedGraph,
{
revision: 7,
reason: "queued-persist-authoritative",
},
);
harness.runtimeContext.__markSyncDirtyShouldThrow = true;
const result = await harness.api.retryPendingGraphPersist({
@@ -2369,6 +2428,15 @@ result = {
harness.api.getCurrentGraph().historyState.lastBatchStatus.persistence.outcome,
"saved",
);
assert.equal(
harness.api.getCurrentGraph().batchJournal?.length,
1,
"pending persist retry 应把 authoritative batch journal 回填到 runtime graph",
);
assert.equal(
harness.api.getCurrentGraph().batchJournal?.[0]?.id,
"journal-queued-1",
);
}
{
@@ -2648,4 +2716,111 @@ result = {
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-state-save",
globalChatId: "chat-state-save",
chatMetadata: {
integrity: "meta-chat-state-save",
},
indexedDbSnapshot: {
meta: {
chatId: "chat-state-save",
revision: 0,
},
nodes: [],
edges: [],
tombstones: [],
state: {
lastProcessedFloor: -1,
extractionCount: 0,
},
},
});
const graph = stampPersistedGraph(
createMeaningfulGraph("chat-state-save", "sidecar"),
{
revision: 7,
integrity: "meta-chat-state-save",
chatId: "chat-state-save",
reason: "chat-state-seed",
},
);
const result = await harness.runtimeContext.persistGraphToHostChatState(
harness.runtimeContext.__chatContext,
{
graph,
revision: 7,
reason: "chat-state-direct-save",
storageTier: "chat-state",
accepted: true,
lastProcessedAssistantFloor: 6,
extractionCount: 3,
mode: "primary",
},
);
assert.equal(result.saved, true);
const stored = await harness.runtimeContext.__chatContext.getChatState(
GRAPH_CHAT_STATE_NAMESPACE,
);
assert.equal(stored?.revision, 7);
assert.equal(stored?.commitMarker?.storageTier, "chat-state");
assert.equal(
harness.api.getGraphPersistenceState().dualWriteLastResult?.target,
"chat-state",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-state-read",
globalChatId: "chat-state-read",
chatMetadata: {
integrity: "meta-chat-state-read",
},
});
const sidecarGraph = stampPersistedGraph(
createMeaningfulGraph("chat-state-read", "sidecar-read"),
{
revision: 9,
integrity: "meta-chat-state-read",
chatId: "chat-state-read",
reason: "chat-state-read-seed",
},
);
harness.runtimeContext.__chatContext.__chatStateStore.set(
GRAPH_CHAT_STATE_NAMESPACE,
buildGraphChatStateSnapshot(sidecarGraph, {
revision: 9,
storageTier: "chat-state",
accepted: true,
reason: "chat-state-read-seed",
chatId: "chat-state-read",
integrity: "meta-chat-state-read",
lastProcessedAssistantFloor: 6,
extractionCount: 3,
}),
);
const result = await harness.runtimeContext.readGraphChatStateSnapshot(
harness.runtimeContext.__chatContext,
{
namespace: GRAPH_CHAT_STATE_NAMESPACE,
},
);
assert.equal(
harness.runtimeContext.canUseGraphChatState(
harness.runtimeContext.__chatContext,
),
true,
);
assert.equal(result?.revision, 9);
assert.equal(result?.commitMarker?.storageTier, "chat-state");
}
console.log("graph-persistence tests passed");

View File

@@ -9,6 +9,7 @@ import {
onMessageReceivedController,
onMessageSentController,
} from "../../host/event-binding.js";
import { isSystemMessageForExtraction } from "../../maintenance/chat-history.js";
import { resolveAutoExtractionPlanController } from "../../maintenance/extraction-controller.js";
import {
GRAPH_LOAD_STATES,
@@ -125,12 +126,22 @@ export function createGenerationRecallHarness(options = {}) {
isTrivialUserInput,
getAssistantTurns: (chat = []) =>
chat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
!message?.is_user &&
!isSystemMessageForExtraction(message, { index, chat })
? [index]
: [],
),
isSystemMessageForExtraction,
getLatestUserChatMessage: (chat = []) =>
[...chat].reverse().find((message) => message?.is_user) || null,
getLastNonSystemChatMessage: (chat = []) =>
[...chat].reverse().find((message) => !message?.is_system) || null,
[...chat]
.map((message, index) => ({ message, index }))
.reverse()
.find(
({ message, index }) =>
!isSystemMessageForExtraction(message, { index, chat }),
)?.message || null,
getSmartTriggerDecision,
getSendTextareaValue: () => context.__sendTextareaValue,
getRecallUserMessageSourceLabel: (source = "") => source,

View File

@@ -0,0 +1,54 @@
import { register, registerHooks } from "node:module";
export function toDataModuleUrl(source = "") {
return `data:text/javascript,${encodeURIComponent(String(source || ""))}`;
}
export function installResolveHooks(entries = []) {
const normalizedEntries = (Array.isArray(entries) ? entries : [])
.map((entry) => ({
specifiers: Array.isArray(entry?.specifiers)
? entry.specifiers.map((value) => String(value || "")).filter(Boolean)
: [],
url: String(entry?.url || ""),
}))
.filter((entry) => entry.specifiers.length > 0 && entry.url);
if (typeof registerHooks === "function") {
registerHooks({
resolve(specifier, context, nextResolve) {
for (const entry of normalizedEntries) {
if (entry.specifiers.includes(specifier)) {
return {
shortCircuit: true,
url: entry.url,
};
}
}
return nextResolve(specifier, context);
},
});
return;
}
if (typeof register === "function") {
const loaderSource = `
const entries = ${JSON.stringify(normalizedEntries)};
export async function resolve(specifier, context, nextResolve) {
for (const entry of entries) {
if (Array.isArray(entry.specifiers) && entry.specifiers.includes(specifier)) {
return {
shortCircuit: true,
url: entry.url,
};
}
}
return nextResolve(specifier, context);
}
`;
register(toDataModuleUrl(loaderSource), import.meta.url);
return;
}
throw new Error("No compatible module hook API available");
}

View File

@@ -1,5 +1,9 @@
import assert from "node:assert/strict";
import { createRequire, registerHooks } from "node:module";
import { createRequire } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = globalThis.__llmModelFetchExtensionSettings || {};",
@@ -22,39 +26,30 @@ const openAiShimSource = [
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
if (
specifier === "../../../openai.js" ||
specifier === "../../../../openai.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: toDataModuleUrl(scriptShimSource),
},
{
specifiers: [
"../../../openai.js",
"../../../../openai.js",
],
url: toDataModuleUrl(openAiShimSource),
},
]);
const require = createRequire(import.meta.url);
const originalRequire = globalThis.require;

View File

@@ -1,5 +1,9 @@
import assert from "node:assert/strict";
import { createRequire, registerHooks } from "node:module";
import { createRequire } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = globalThis.__llmStreamingExtensionSettings || {};",
@@ -22,39 +26,30 @@ const openAiShimSource = [
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
if (
specifier === "../../../openai.js" ||
specifier === "../../../../openai.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: toDataModuleUrl(scriptShimSource),
},
{
specifiers: [
"../../../openai.js",
"../../../../openai.js",
],
url: toDataModuleUrl(openAiShimSource),
},
]);
const require = createRequire(import.meta.url);
const originalRequire = globalThis.require;

View File

@@ -1,9 +1,13 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import { createRequire, registerHooks } from "node:module";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
import { pruneProcessedMessageHashesFromFloor } from "../maintenance/chat-history.js";
import {
onBeforeCombinePromptsController,
@@ -106,39 +110,30 @@ const openAiShimUrl = `data:text/javascript,${encodeURIComponent(
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../index.js");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: extensionsShimUrl,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: scriptShimUrl,
};
}
if (
specifier === "../../../openai.js" ||
specifier === "../../../../openai.js"
) {
return {
shortCircuit: true,
url: openAiShimUrl,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: extensionsShimUrl || toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: scriptShimUrl || toDataModuleUrl(scriptShimSource),
},
{
specifiers: [
"../../../openai.js",
"../../../../openai.js",
],
url: openAiShimUrl || toDataModuleUrl(openAiShimSource),
},
]);
const require = createRequire(import.meta.url);
const originalRequire = globalThis.require;
@@ -3173,6 +3168,7 @@ async function testProcessedHistoryAdvanceTracksCoreExtractionSuccess() {
);
setBatchStageOutcome(structuralPartial, "finalize", "success");
finalizeBatchStatus(structuralPartial);
delete structuralPartial.historyAdvanceAllowed;
assert.equal(structuralPartial.completed, true);
assert.equal(structuralPartial.outcome, "partial");
assert.equal(structuralPartial.consistency, "weak");
@@ -3186,6 +3182,7 @@ async function testProcessedHistoryAdvanceTracksCoreExtractionSuccess() {
setBatchStageOutcome(semanticFailed, "semantic", "failed", "semantic down");
setBatchStageOutcome(semanticFailed, "finalize", "success");
finalizeBatchStatus(semanticFailed);
delete semanticFailed.historyAdvanceAllowed;
assert.equal(semanticFailed.completed, true);
assert.equal(semanticFailed.outcome, "failed");
assert.equal(semanticFailed.consistency, "strong");
@@ -3203,9 +3200,10 @@ async function testProcessedHistoryAdvanceTracksCoreExtractionSuccess() {
"vector finalize down",
);
finalizeBatchStatus(finalizeFailed);
delete finalizeFailed.historyAdvanceAllowed;
assert.equal(finalizeFailed.completed, false);
assert.equal(finalizeFailed.outcome, "failed");
assert.equal(shouldAdvanceProcessedHistory(finalizeFailed), true);
assert.equal(shouldAdvanceProcessedHistory(finalizeFailed), false);
const fullSuccess = createBatchStatusSkeleton({
processedRange: [8, 9],
@@ -3216,6 +3214,7 @@ async function testProcessedHistoryAdvanceTracksCoreExtractionSuccess() {
setBatchStageOutcome(fullSuccess, "semantic", "success");
setBatchStageOutcome(fullSuccess, "finalize", "success");
finalizeBatchStatus(fullSuccess);
delete fullSuccess.historyAdvanceAllowed;
assert.equal(fullSuccess.completed, true);
assert.equal(fullSuccess.outcome, "success");
assert.equal(fullSuccess.consistency, "strong");

View File

@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = {};",
@@ -24,30 +27,23 @@ const scriptShimSource = [
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: toDataModuleUrl(scriptShimSource),
},
]);
const { buildTaskLlmPayload, buildTaskPrompt } = await import("../prompting/prompt-builder.js");
const { createDefaultTaskProfiles } = await import("../prompting/prompt-profiles.js");
@@ -145,7 +141,9 @@ const recallRulesBlock = recallPayload.promptMessages.find(
);
assert.match(String(recallFormatBlock?.content || ""), /active_owner_keys/);
assert.match(String(recallFormatBlock?.content || ""), /active_owner_scores/);
assert.match(String(recallFormatBlock?.content || ""), /selected_keys/);
assert.match(String(recallRulesBlock?.content || ""), /剧情时间/);
assert.match(String(recallRulesBlock?.content || ""), /评分召回/);
const formatterCalls = [];
initializeHostAdapter({

View File

@@ -1,5 +1,9 @@
import assert from "node:assert/strict";
import { createRequire, registerHooks } from "node:module";
import { createRequire } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = globalThis.__promptBuilderMvuExtensionSettings || {};",
@@ -35,39 +39,30 @@ const openAiShimSource = [
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
if (
specifier === "../../../openai.js" ||
specifier === "../../../../openai.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: toDataModuleUrl(scriptShimSource),
},
{
specifiers: [
"../../../openai.js",
"../../../../openai.js",
],
url: toDataModuleUrl(openAiShimSource),
},
]);
const require = createRequire(import.meta.url);
const originalRequire = globalThis.require;

View File

@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { buildRecallRecentMessagesController } from "../retrieval/recall-controller.js";
const chat = [
{ is_user: false, is_system: true, mes: "greeting/system" },
{
is_user: false,
is_system: true,
mes: "managed hidden assistant",
extra: { __st_bme_hide_managed: true },
},
{ is_user: true, is_system: false, mes: "user message" },
{ is_user: false, is_system: true, mes: "real system" },
{ is_user: false, is_system: false, mes: "visible assistant" },
];
const recentMessages = buildRecallRecentMessagesController(chat, 6, "", {
formatRecallContextLine(message) {
return `[${message.is_user ? "user" : "assistant"}]: ${message.mes}`;
},
normalizeRecallInputText(value = "") {
return String(value || "").trim();
},
});
assert.deepEqual(recentMessages, [
"[assistant]: managed hidden assistant",
"[user]: user message",
"[assistant]: visible assistant",
]);
console.log("recall-hide-bypass tests passed");

View File

@@ -85,7 +85,7 @@ const state = {
diffusionCalls: [],
llmCalls: [],
llmCandidateCount: 0,
llmResponse: { selected_ids: ["rule-2", "rule-1"] },
llmResponse: { selected_keys: ["R1", "R2"] },
llmOptions: [],
};
@@ -447,7 +447,7 @@ state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmCandidateCount = 0;
state.llmResponse = { selected_ids: ["rule-2", "rule-1"] };
state.llmResponse = { selected_keys: ["R1", "R2"] };
const llmPoolResult = await retrieve({
graph,
userMessage: "请根据规则给出结论",
@@ -471,6 +471,23 @@ assert.equal(state.diffusionCalls.length, 0);
assert.equal(state.llmCandidateCount, 2);
assert.deepEqual(Array.from(llmPoolResult.selectedNodeIds), ["rule-2", "rule-1"]);
assert.equal(llmPoolResult.meta.retrieval.llm.status, "llm");
assert.equal(
llmPoolResult.meta.retrieval.llm.selectionProtocol,
"candidate-keys-v1",
);
assert.deepEqual(
Array.from(llmPoolResult.meta.retrieval.llm.rawSelectedKeys),
["R1", "R2"],
);
assert.deepEqual(
Array.from(llmPoolResult.meta.retrieval.llm.resolvedSelectedNodeIds),
["rule-2", "rule-1"],
);
assert.equal(
llmPoolResult.meta.retrieval.llm.candidateKeyMapPreview?.R1?.nodeId,
"rule-2",
);
assert.equal(llmPoolResult.meta.retrieval.llm.legacySelectionUsed, false);
assert.equal(llmPoolResult.meta.retrieval.llm.candidatePool, 2);
assert.equal(llmPoolResult.meta.retrieval.vectorMergedHits, 3);
assert.equal(llmPoolResult.meta.retrieval.diversityApplied, true);
@@ -479,6 +496,135 @@ assert.equal(llmPoolResult.meta.retrieval.candidatePoolAfterDpp, 2);
assert.equal(state.llmOptions[0].returnFailureDetails, true);
assert.equal(state.llmOptions[0].maxRetries, 2);
assert.equal(state.llmOptions[0].maxCompletionTokens, 512);
assert.match(String(state.llmCalls[0] || ""), /\[R1\]/);
assert.doesNotMatch(String(state.llmCalls[0] || ""), /\[rule-1\]|\[rule-2\]/);
state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = {
selected_keys: ["R2"],
selected_ids: ["rule-2"],
};
const selectedKeysPriorityResult = await retrieve({
graph,
userMessage: "优先吃新协议",
recentMessages: ["用户:测试 selected_keys 优先级"],
embeddingConfig: {},
schema,
options: {
topK: 4,
maxRecallNodes: 2,
enableVectorPrefilter: true,
enableGraphDiffusion: false,
enableLLMRecall: true,
llmCandidatePool: 2,
},
});
assert.deepEqual(Array.from(selectedKeysPriorityResult.selectedNodeIds), ["rule-1"]);
assert.equal(
selectedKeysPriorityResult.meta.retrieval.llm.selectionProtocol,
"candidate-keys-v1",
);
assert.equal(
selectedKeysPriorityResult.meta.retrieval.llm.legacySelectionUsed,
false,
);
state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = { selected_ids: ["rule-1"] };
const legacySelectionResult = await retrieve({
graph,
userMessage: "兼容旧 selected_ids",
recentMessages: ["用户:测试 legacy 路径"],
embeddingConfig: {},
schema,
options: {
topK: 4,
maxRecallNodes: 2,
enableVectorPrefilter: true,
enableGraphDiffusion: false,
enableLLMRecall: true,
llmCandidatePool: 2,
},
});
assert.deepEqual(Array.from(legacySelectionResult.selectedNodeIds), ["rule-1"]);
assert.equal(
legacySelectionResult.meta.retrieval.llm.selectionProtocol,
"legacy-selected-ids",
);
assert.equal(
legacySelectionResult.meta.retrieval.llm.legacySelectionUsed,
true,
);
state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = { selected_keys: [] };
const emptySelectionFallbackResult = await retrieve({
graph,
userMessage: "这次故意空选",
recentMessages: ["用户:测试空选回退"],
embeddingConfig: {},
schema,
options: {
topK: 4,
maxRecallNodes: 2,
enableVectorPrefilter: true,
enableGraphDiffusion: false,
enableLLMRecall: true,
llmCandidatePool: 2,
},
});
assert.equal(emptySelectionFallbackResult.meta.retrieval.llm.status, "fallback");
assert.equal(
emptySelectionFallbackResult.meta.retrieval.llm.fallbackType,
"empty-selection",
);
assert.equal(
emptySelectionFallbackResult.meta.retrieval.llm.emptySelectionAccepted,
false,
);
assert.deepEqual(
Array.from(emptySelectionFallbackResult.selectedNodeIds),
["rule-2", "rule-1"],
);
state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = { selected_keys: ["R99"] };
const invalidKeyFallbackResult = await retrieve({
graph,
userMessage: "这次给无效 key",
recentMessages: ["用户:测试无效候选回退"],
embeddingConfig: {},
schema,
options: {
topK: 4,
maxRecallNodes: 2,
enableVectorPrefilter: true,
enableGraphDiffusion: false,
enableLLMRecall: true,
llmCandidatePool: 2,
},
});
assert.equal(invalidKeyFallbackResult.meta.retrieval.llm.status, "fallback");
assert.equal(
invalidKeyFallbackResult.meta.retrieval.llm.fallbackType,
"invalid-candidate",
);
assert.deepEqual(
Array.from(invalidKeyFallbackResult.selectedNodeIds),
["rule-2", "rule-1"],
);
state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
@@ -792,6 +938,14 @@ const multiOwnerResult = await retrieve({
llmCandidatePool: 4,
},
});
assert.equal(
multiOwnerResult.meta.retrieval.llm.selectionProtocol,
"legacy-selected-ids",
);
assert.equal(
multiOwnerResult.meta.retrieval.llm.legacySelectionUsed,
true,
);
assert.deepEqual(
Array.from(multiOwnerResult.meta.retrieval.activeRecallOwnerKeys),
["character:艾琳", "character:露西亚"],

View File

@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export function getContext(...args) {",
@@ -10,20 +13,15 @@ const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
extensionsShimSource,
)}`;
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js"
) {
return {
shortCircuit: true,
url: extensionsShimUrl,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
],
url: extensionsShimUrl || toDataModuleUrl(extensionsShimSource),
},
});
]);
const originalSillyTavern = globalThis.SillyTavern;
const originalGetCurrentChatId = globalThis.getCurrentChatId;
@@ -69,6 +67,14 @@ try {
},
chat: [
{ is_user: true, mes: "第一句" },
{
is_user: false,
is_system: true,
mes: "被 BME 隐藏的助手楼层",
extra: {
__st_bme_hide_managed: true,
},
},
{
is_user: false,
mes: "回应",
@@ -115,6 +121,14 @@ try {
assert.equal(hostSnapshot.snapshot.variables.local.location, "library");
assert.equal(hostSnapshot.snapshot.chat.lastUserMessage, "最后一句");
assert.equal(hostSnapshot.snapshot.chat.id, "chat-from-global");
assert.equal(
hostSnapshot.snapshot.chat.messages[1]?.is_system,
false,
);
assert.equal(
hostSnapshot.snapshot.chat.messages[1]?.mes,
"被 BME 隐藏的助手楼层",
);
assert.equal(hostSnapshot.prompt.charName, "Alice");
assert.equal(hostSnapshot.prompt.userPersona, "桥接 persona");

View File

@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import {
installResolveHooks,
toDataModuleUrl,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = {};",
@@ -34,39 +37,30 @@ const openAiShimSource = [
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
if (
specifier === "../../../openai.js" ||
specifier === "../../../../openai.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: toDataModuleUrl(extensionsShimSource),
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: toDataModuleUrl(scriptShimSource),
},
{
specifiers: [
"../../../openai.js",
"../../../../openai.js",
],
url: toDataModuleUrl(openAiShimSource),
},
]);
const { createEmptyGraph } = await import("../graph/graph.js");
const { appendSummaryEntry } = await import("../graph/summary-state.js");

View File

@@ -1,5 +1,7 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import {
installResolveHooks,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = globalThis.__taskRegexTestExtensionSettings || {};",
@@ -11,21 +13,16 @@ const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
extensionsShimSource,
)}`;
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: extensionsShimUrl,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: extensionsShimUrl,
},
});
]);
const originalSillyTavern = globalThis.SillyTavern;
const originalGetTavernRegexes = globalThis.getTavernRegexes;

View File

@@ -1,5 +1,7 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import {
installResolveHooks,
} from "./helpers/register-hooks-compat.mjs";
const extensionsShimSource = [
"export const extension_settings = {};",
@@ -19,30 +21,23 @@ const scriptShimUrl = `data:text/javascript,${encodeURIComponent(
scriptShimSource,
)}`;
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js" ||
specifier === "../../../../../extensions.js"
) {
return {
shortCircuit: true,
url: extensionsShimUrl,
};
}
if (
specifier === "../../../../script.js" ||
specifier === "../../../../../script.js"
) {
return {
shortCircuit: true,
url: scriptShimUrl,
};
}
return nextResolve(specifier, context);
installResolveHooks([
{
specifiers: [
"../../../extensions.js",
"../../../../extensions.js",
"../../../../../extensions.js",
],
url: extensionsShimUrl,
},
});
{
specifiers: [
"../../../../script.js",
"../../../../../script.js",
],
url: scriptShimUrl,
},
]);
const originalSillyTavern = globalThis.SillyTavern;
const originalEjsTemplate = globalThis.EjsTemplate;