diff --git a/manifest.json b/manifest.json index 51a5f15..90078ae 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.2.3", + "version": "4.2.2", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } diff --git a/prompting/prompt-builder.js b/prompting/prompt-builder.js index d217115..1b9c3cd 100644 --- a/prompting/prompt-builder.js +++ b/prompting/prompt-builder.js @@ -269,6 +269,14 @@ function messageUsesWorldInfoContent(message = {}) { return String(message?.source || "") === "worldInfo-atDepth"; } +function getOptionalFiniteNumber(value) { + if (value === null || value === undefined || value === "") { + return null; + } + const numericValue = Number(value); + return Number.isFinite(numericValue) ? numericValue : null; +} + function getPromptMessageLikeDescriptor(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; @@ -279,7 +287,7 @@ function getPromptMessageLikeDescriptor(value) { return { content: String(value.content || ""), role: role === "user" ? "user" : "assistant", - seq: Number.isFinite(Number(value.seq)) ? Number(value.seq) : null, + seq: getOptionalFiniteNumber(value.seq), }; } @@ -287,7 +295,7 @@ function getPromptMessageLikeDescriptor(value) { return { content: String(value.mes || ""), role: value.is_user === true ? "user" : "assistant", - seq: Number.isFinite(Number(value.seq)) ? Number(value.seq) : null, + seq: getOptionalFiniteNumber(value.seq), }; } @@ -1072,6 +1080,41 @@ function sortInjectionEntries(entries = []) { }); } +function sortAtDepthInjectionEntries(entries = []) { + return [...entries].sort((left, right) => { + const depthLeft = Number.isFinite(Number(left?.depth)) + ? Number(left.depth) + : 0; + const depthRight = Number.isFinite(Number(right?.depth)) + ? Number(right.depth) + : 0; + const orderLeft = Number.isFinite(Number(left?.order)) + ? Number(left.order) + : 0; + const orderRight = Number.isFinite(Number(right?.order)) + ? Number(right.order) + : 0; + const uidLeft = Number.isFinite(Number(left?.uid)) + ? Number(left.uid) + : Number.NEGATIVE_INFINITY; + const uidRight = Number.isFinite(Number(right?.uid)) + ? Number(right.uid) + : Number.NEGATIVE_INFINITY; + const indexLeft = Number.isFinite(Number(left?.index)) + ? Number(left.index) + : 0; + const indexRight = Number.isFinite(Number(right?.index)) + ? Number(right.index) + : 0; + return ( + depthRight - depthLeft || + orderLeft - orderRight || + uidRight - uidLeft || + indexLeft - indexRight + ); + }); +} + function createHostInjectionPlanEntry(block = {}, position, extra = {}) { return { source: "block", @@ -1154,10 +1197,98 @@ function buildHostInjectionPlan(renderedBlocks = [], worldInfoResolution = {}) { return { before: sortInjectionEntries(plan.before), after: sortInjectionEntries(plan.after), - atDepth: sortInjectionEntries(plan.atDepth), + atDepth: sortAtDepthInjectionEntries(plan.atDepth), }; } +function createInjectedAtDepthChatMessage(message = {}) { + const descriptor = getPromptMessageLikeDescriptor(message); + if (!descriptor) { + return null; + } + return { + ...(message && typeof message === "object" ? message : {}), + role: descriptor.role, + content: descriptor.content, + seq: descriptor.seq, + uid: Number.isFinite(Number(message?.uid)) + ? Number(message.uid) + : null, + index: Number.isFinite(Number(message?.index)) + ? Number(message.index) + : null, + name: String(message?.name || ""), + sourceName: String(message?.sourceName || ""), + worldbook: String(message?.worldbook || ""), + source: String(message?.source || "worldInfo-atDepth"), + sourceKey: String(message?.sourceKey || "taskAdditionalMessages"), + derivedFromWorldInfo: true, + contentOrigin: + String(message?.contentOrigin || "") || + PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED, + sanitizationEligible: message?.sanitizationEligible === true, + regexSourceType: String(message?.regexSourceType || "world_info"), + depth: Number.isFinite(Number(message?.depth)) + ? Number(message.depth) + : 0, + order: Number.isFinite(Number(message?.order)) + ? Number(message.order) + : 0, + }; +} + +function injectAtDepthMessagesIntoChatMessages( + chatMessages = [], + atDepthMessages = [], +) { + const normalizedChatMessages = (Array.isArray(chatMessages) ? chatMessages : []) + .map((message) => { + const descriptor = getPromptMessageLikeDescriptor(message); + if (!descriptor) return null; + return { + ...(message && typeof message === "object" ? message : {}), + role: descriptor.role, + content: descriptor.content, + seq: descriptor.seq, + }; + }) + .filter(Boolean); + if (normalizedChatMessages.length === 0) { + return null; + } + + const groupedByDepth = new Map(); + for (const message of sortAtDepthInjectionEntries(atDepthMessages)) { + const injectedMessage = createInjectedAtDepthChatMessage(message); + if (!injectedMessage) continue; + const depth = Math.max(0, Number(injectedMessage.depth || 0)); + if (!groupedByDepth.has(depth)) { + groupedByDepth.set(depth, []); + } + groupedByDepth.get(depth).push(injectedMessage); + } + if (groupedByDepth.size === 0) { + return normalizedChatMessages; + } + + const reversedMessages = [...normalizedChatMessages].reverse(); + const sortedDepths = [...groupedByDepth.keys()].sort((left, right) => left - right); + let totalInsertedMessages = 0; + + for (const depth of sortedDepths) { + const depthMessages = groupedByDepth.get(depth) || []; + if (depthMessages.length === 0) continue; + const injectIndex = Math.min( + Math.max(0, depth + totalInsertedMessages), + reversedMessages.length, + ); + reversedMessages.splice(injectIndex, 0, ...depthMessages); + totalInsertedMessages += depthMessages.length; + } + + return reversedMessages.reverse(); +} + function getPromptFieldContentOrigin(sourceKey = "") { const normalizedSourceKey = String(sourceKey || "").trim(); if (!normalizedSourceKey) { @@ -1303,6 +1434,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) { const emptyWorldInfo = buildEmptyWorldInfoContext(); let resolvedWorldInfo = emptyWorldInfo; let worldInfoRuntimeBlockedContents = []; + let deliveredAtDepthViaChatMessages = false; if (worldInfoRequested) { const worldInfo = await resolveTaskWorldInfo({ @@ -1333,6 +1465,28 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) { taskAdditionalMessages: sanitizedWorldInfo.additionalMessages || [], worldInfoDebug: sanitizedWorldInfo.debug || null, }; + + if ( + Array.isArray(sanitizedInputContext.chatMessages) && + isPromptMessageArray(sanitizedInputContext.chatMessages) + ) { + const injectedChatMessages = injectAtDepthMessagesIntoChatMessages( + sanitizedInputContext.chatMessages, + sanitizedWorldInfo.additionalMessages, + ); + if (Array.isArray(injectedChatMessages) && injectedChatMessages.length > 0) { + sanitizedInputContext.chatMessages = injectedChatMessages; + if (typeof context.recentMessages === "string") { + sanitizedInputContext.recentMessages = + stringifyInterpolatedValue(injectedChatMessages); + } + if (typeof context.dialogueText === "string") { + sanitizedInputContext.dialogueText = + stringifyInterpolatedValue(injectedChatMessages); + } + deliveredAtDepthViaChatMessages = true; + } + } } const resolvedContext = { @@ -1479,36 +1633,44 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) { } } + const atDepthExecutionMessages = (worldInfoResolution.additionalMessages || []) + .map((message) => + createExecutionMessage( + message.role, + message.content, + { + source: "worldInfo-atDepth", + sourceKey: "taskAdditionalMessages", + contentOrigin: + String(message.contentOrigin || "") || + PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED, + sanitizationEligible: message.sanitizationEligible === true, + regexSourceType: String(message.regexSourceType || "world_info"), + depth: Number.isFinite(Number(message?.depth)) + ? Number(message.depth) + : null, + order: Number.isFinite(Number(message?.order)) + ? Number(message.order) + : 0, + }, + ), + ) + .filter(Boolean); + const finalExecutionMessages = deliveredAtDepthViaChatMessages + ? executionMessages + : [...atDepthExecutionMessages, ...executionMessages]; + + const privateTaskMessages = deliveredAtDepthViaChatMessages + ? [...customMessages] + : [...worldInfoResolution.additionalMessages, ...customMessages]; debugLog( `[ST-BME][prompt-diag] buildTaskPrompt done: ` + - `executionMessages=${executionMessages.length}, ` + + `executionMessages=${finalExecutionMessages.length}, ` + `userBlocks=${userRoleBlockCount}, systemBlocks=${systemRoleBlockCount}, ` + - `customMessages=${customMessages.length}`, + `customMessages=${customMessages.length}, ` + + `atDepthMessages=${atDepthExecutionMessages.length}, ` + + `atDepthViaChatMessages=${deliveredAtDepthViaChatMessages}`, ); - - for (const message of worldInfoResolution.additionalMessages || []) { - const executionMessage = createExecutionMessage( - message.role, - message.content, - { - source: "worldInfo-atDepth", - sourceKey: "taskAdditionalMessages", - contentOrigin: - String(message.contentOrigin || "") || - PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED, - sanitizationEligible: message.sanitizationEligible === true, - regexSourceType: String(message.regexSourceType || "world_info"), - }, - ); - if (executionMessage) { - executionMessages.push(executionMessage); - } - } - - const privateTaskMessages = [ - ...customMessages, - ...worldInfoResolution.additionalMessages, - ]; const hostInjectionPlan = buildHostInjectionPlan( renderedBlocks, worldInfoResolution, @@ -1522,7 +1684,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) { systemPrompt, messages: privateTaskMessages, }, - executionMessages, + executionMessages: finalExecutionMessages, privateTaskMessages, renderedBlocks, regexInput: mergeRegexCollectors(promptRegexInput), @@ -1562,14 +1724,16 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) { customMessageCount: customMessages.length, additionalMessageCount: worldInfoResolution.additionalMessages.length, privateTaskMessageCount: privateTaskMessages.length, - executionMessageCount: executionMessages.length, + executionMessageCount: finalExecutionMessages.length, userRoleBlockCount, assistantRoleBlockCount, systemRoleBlockCount, effectiveDelivery: { profileBlocks: "ordered-private-messages", worldInfoBeforeAfter: "inline-in-ordered-messages", - worldInfoAtDepth: "appended-private-messages", + worldInfoAtDepth: deliveredAtDepthViaChatMessages + ? "inserted-into-chat-messages-by-depth" + : "appended-private-messages-fallback", }, worldInfoCacheHit: Boolean(worldInfoResolution.debug?.cache?.hit), ejsRuntimeStatus: worldInfoResolution.debug?.ejsRuntimeStatus || "", @@ -1639,7 +1803,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) { profileName: profile?.name || "", systemPrompt, privateTaskMessages, - executionMessages, + executionMessages: finalExecutionMessages, renderedBlocks, hostInjections: worldInfoResolution.injections, hostInjectionPlan, diff --git a/prompting/task-worldinfo.js b/prompting/task-worldinfo.js index 71d9dfc..fca250f 100644 --- a/prompting/task-worldinfo.js +++ b/prompting/task-worldinfo.js @@ -1158,6 +1158,7 @@ function normalizeResolvedEntry(entry = {}, fallbackIndex = 0) { ? entry.role : "system"; return { + uid: Number(entry.uid ?? 0), name: normalizeKey(entry.name), sourceName: normalizeKey( entry.sourceName || entry.source_name || entry.name, @@ -1185,6 +1186,7 @@ function sortAtDepthEntries(entries = []) { return ( depthB - depthA || (a.order ?? 100) - (b.order ?? 100) || + (b.uid ?? 0) - (a.uid ?? 0) || a.index - b.index ); }); @@ -1195,6 +1197,13 @@ function buildAdditionalMessages(entries = []) { .map((entry) => ({ role: entry.role, content: String(entry.content || "").trim(), + depth: Number(entry.depth ?? 0), + order: Number(entry.order ?? 100), + uid: Number(entry.uid ?? 0), + index: Number(entry.index ?? 0), + name: String(entry.name || ""), + sourceName: String(entry.sourceName || entry.name || ""), + worldbook: String(entry.worldbook || ""), source: "worldInfo-atDepth", sourceKey: "taskAdditionalMessages", })) diff --git a/tests/task-worldinfo.mjs b/tests/task-worldinfo.mjs index e1a4f42..5847372 100644 --- a/tests/task-worldinfo.mjs +++ b/tests/task-worldinfo.mjs @@ -265,7 +265,7 @@ const bonusEntry = createWorldbookEntry({ order: 10, }); -const bonusMvuEntry = createWorldbookEntry({ + const bonusMvuEntry = createWorldbookEntry({ uid: 102, name: "Bonus MVU", comment: "Bonus MVU", @@ -273,8 +273,8 @@ const bonusMvuEntry = createWorldbookEntry({ order: 20, }); -const worldbooksByName = { - "main-book": [ + const worldbooksByName = { + "main-book": [ constantEntry, dynEntry, inlineSummaryEntry, @@ -293,9 +293,9 @@ const worldbooksByName = { atDepthEntry, mvuTaggedEntry, mvuHeuristicEntry, - ], - "bonus-book": [bonusEntry, bonusMvuEntry], -}; + ], + "bonus-book": [bonusEntry, bonusMvuEntry], + }; try { globalThis.SillyTavern = { @@ -663,7 +663,11 @@ try { ); assert.deepEqual( promptBuild.privateTaskMessages.map((message) => message.role), - ["user", "system"], + ["system", "user"], + ); + assert.equal( + promptBuild.privateTaskMessages[0].content, + "这是一条 atDepth 消息。", ); assert.deepEqual( promptBuild.hostInjections.before.map((entry) => entry.name), @@ -703,7 +707,11 @@ try { assert.equal(promptBuild.executionMessages.length, 4); assert.deepEqual( promptBuild.executionMessages.map((message) => message.role), - ["system", "system", "user", "system"], + ["system", "system", "system", "user"], + ); + assert.equal( + promptBuild.executionMessages[0].content, + "这是一条 atDepth 消息。", ); assert.deepEqual( promptBuild.renderedBlocks.map((block) => block.delivery), @@ -845,8 +853,106 @@ try { ); assert.deepEqual( atDepthOnlyPromptBuild.executionMessages.map((message) => message.role), - ["user", "system"], + ["system", "user"], ); + assert.equal( + atDepthOnlyPromptBuild.executionMessages[0].content, + "这是一条 atDepth 消息。", + ); + + const depthD4Entry = createWorldbookEntry({ + uid: 201, + name: "深度注入 D4", + comment: "深度注入 D4", + content: "这是 d4 atDepth 消息。", + positionType: "at_depth_as_system", + depth: 4, + order: 8, + }); + const depthD1Entry = createWorldbookEntry({ + uid: 202, + name: "深度注入 D1", + comment: "深度注入 D1", + content: "这是 d1 atDepth 消息。", + positionType: "at_depth_as_system", + depth: 1, + order: 3, + }); + worldbooksByName["main-book"].push(depthD4Entry, depthD1Entry); + const previousGetContext = globalThis.SillyTavern.getContext; + globalThis.SillyTavern.getContext = () => ({ + ...previousGetContext(), + chatId: "depth-aware-chat", + }); + + const depthAwareSettings = { + taskProfiles: { + recall: { + activeProfileId: "depth-aware", + profiles: [ + { + id: "depth-aware", + name: "深度顺序预设", + taskType: "recall", + builtin: false, + blocks: [ + { + id: "depth-recent", + type: "builtin", + sourceKey: "recentMessages", + role: "system", + enabled: true, + order: 0, + injectionMode: "append", + }, + { + id: "depth-user", + type: "custom", + content: "用户问题:{{userMessage}}", + role: "user", + enabled: true, + order: 1, + injectionMode: "append", + }, + ], + }, + ], + }, + }, + }; + + const depthAwarePromptBuild = await buildTaskPrompt(depthAwareSettings, "recall", { + taskName: "recall", + userMessage: "继续调查 depth 排序", + recentMessages: "这里会被 chatMessages 替换", + chatMessages: [ + { seq: 11, role: "user", content: "第一句" }, + { seq: 12, role: "assistant", content: "第二句" }, + ], + charName: "Alice", + }); + + assert.deepEqual( + depthAwarePromptBuild.executionMessages.map((message) => message.content), + [ + "#1 [assistant]: 这是 d4 atDepth 消息。\n\n#2 [assistant]: 这是一条 atDepth 消息。\n\n#11 [user]: 第一句\n\n#4 [assistant]: 这是 d1 atDepth 消息。\n\n#12 [assistant]: 第二句", + "用户问题:继续调查 depth 排序", + ], + ); + assert.deepEqual( + depthAwarePromptBuild.hostInjections.atDepth.map((entry) => entry.name), + ["深度注入 D4", "深度注入", "深度注入 D1"], + ); + assert.deepEqual( + depthAwarePromptBuild.hostInjectionPlan.atDepth.map((entry) => entry.entryName), + ["深度注入 D4", "深度注入", "深度注入 D1"], + ); + assert.equal( + depthAwarePromptBuild.executionMessages.at(-1)?.content.includes("atDepth"), + false, + ); + worldbooksByName["main-book"].splice(-2, 2); + globalThis.SillyTavern.getContext = previousGetContext; const { initializeHostAdapter } = await import("../host/adapter/index.js"); const partialBridgeCalls = [];