feat: add runtime debug snapshots and injection planning

This commit is contained in:
Youzini-afk
2026-03-26 23:15:35 +08:00
parent 777edf9f9a
commit 28616fc177
10 changed files with 1011 additions and 92 deletions

View File

@@ -51,12 +51,15 @@ export async function compressType({
if (!compression || compression.mode !== "hierarchical") {
return { created: 0, archived: 0 };
}
const maxDepth = Number.isFinite(Number(compression.maxDepth))
? Math.max(1, Number(compression.maxDepth))
: 1;
let totalCreated = 0;
let totalArchived = 0;
// 从最低层级开始逐层压缩
for (let level = 0; level < compression.maxDepth; level++) {
for (let level = 0; level < maxDepth; level++) {
throwIfAborted(signal);
const result = await compressLevel({
graph,
@@ -93,6 +96,9 @@ async function compressLevel({
settings = {},
}) {
const compression = typeDef.compression;
const fanIn = Number.isFinite(Number(compression.fanIn))
? Math.max(2, Number(compression.fanIn))
: 2;
throwIfAborted(signal);
// 获取该层级的活跃叶子节点
@@ -101,18 +107,24 @@ async function compressLevel({
.sort((a, b) => a.seq - b.seq);
const threshold = force
? Math.max(2, compression.fanIn)
: compression.threshold;
const keepRecent = force ? 0 : compression.keepRecentLeaves;
? fanIn
: Number.isFinite(Number(compression.threshold))
? Math.max(2, Number(compression.threshold))
: fanIn;
const keepRecent = force
? 0
: Number.isFinite(Number(compression.keepRecentLeaves))
? Math.max(0, Number(compression.keepRecentLeaves))
: 0;
// 不够阈值,无需压缩
if (levelNodes.length <= threshold) {
// 不够阈值,无需压缩;强制压缩时只要求满足 fanIn
if (force ? levelNodes.length < fanIn : levelNodes.length <= threshold) {
return { created: 0, archived: 0 };
}
// 排除最近的节点
const compressible = levelNodes.slice(0, levelNodes.length - keepRecent);
if (compressible.length < compression.fanIn) {
if (compressible.length < fanIn) {
return { created: 0, archived: 0 };
}
@@ -120,8 +132,8 @@ async function compressLevel({
let archived = 0;
// 按 fanIn 分组压缩
for (let i = 0; i < compressible.length; i += compression.fanIn) {
const batch = compressible.slice(i, i + compression.fanIn);
for (let i = 0; i < compressible.length; i += fanIn) {
const batch = compressible.slice(i, i + fanIn);
if (batch.length < 2) break; // 至少 2 个才压缩
// 调用 LLM 总结

View File

@@ -11,6 +11,11 @@ import { extension_settings } from "../../../extensions.js";
const MODULE_NAME = "st_bme";
const EMBEDDING_REQUEST_TIMEOUT_MS = 300000;
function getEmbeddingTestOverride(name) {
const override = globalThis.__stBmeTestOverrides?.embedding?.[name];
return typeof override === "function" ? override : null;
}
function getConfiguredTimeoutMs(
settings = extension_settings[MODULE_NAME] || {},
) {
@@ -98,6 +103,11 @@ async function fetchWithTimeout(
* @returns {Promise<Float64Array|null>} 向量或 null
*/
export async function embedText(text, config, { signal } = {}) {
const override = getEmbeddingTestOverride("embedText");
if (override) {
return await override(text, config, { signal });
}
const apiUrl = normalizeOpenAICompatibleBaseUrl(config?.apiUrl);
if (!text || !apiUrl || !config?.model) {
console.warn("[ST-BME] Embedding 配置不完整,跳过");
@@ -159,6 +169,11 @@ export async function embedText(text, config, { signal } = {}) {
* @returns {Promise<(Float64Array|null)[]>}
*/
export async function embedBatch(texts, config, { signal } = {}) {
const override = getEmbeddingTestOverride("embedBatch");
if (override) {
return await override(texts, config, { signal });
}
const apiUrl = normalizeOpenAICompatibleBaseUrl(config?.apiUrl);
if (!texts.length || !apiUrl || !config?.model) {
return texts.map(() => null);
@@ -256,6 +271,11 @@ export function cosineSimilarity(vecA, vecB) {
* @returns {Array<{nodeId: string, score: number}>} 按相似度降序
*/
export function searchSimilar(queryVec, candidates, topK = 20) {
const override = getEmbeddingTestOverride("searchSimilar");
if (override) {
return override(queryVec, candidates, topK);
}
if (!queryVec || candidates.length === 0) return [];
const scored = candidates

102
index.js
View File

@@ -60,6 +60,11 @@ import {
rollbackBatch,
snapshotProcessedMessageHashes,
} from "./runtime-state.js";
import {
getRuntimeDebugSnapshot as readRuntimeDebugSnapshot,
recordHostCapabilitySnapshot,
recordInjectionSnapshot,
} from "./runtime-debug.js";
import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js";
import {
deleteBackendVectorHashesForRecovery,
@@ -671,7 +676,7 @@ function initializeHostCapabilityBridge(options = {}) {
}
function buildHostCapabilityErrorStatus(error) {
return {
const snapshot = {
available: false,
mode: "error",
fallbackReason:
@@ -685,6 +690,8 @@ function buildHostCapabilityErrorStatus(error) {
snapshotRevision: -1,
snapshotCreatedAt: "",
};
recordHostCapabilitySnapshot(snapshot);
return snapshot;
}
export function getHostCapabilityStatus(options = {}) {
@@ -695,9 +702,11 @@ export function getHostCapabilityStatus(options = {}) {
delete normalizedOptions.refresh;
try {
return shouldRefresh
const snapshot = shouldRefresh
? refreshHostCapabilitySnapshot(normalizedOptions)
: getHostCapabilitySnapshot();
recordHostCapabilitySnapshot(snapshot);
return snapshot;
} catch (error) {
console.warn("[ST-BME] 读取宿主桥接状态失败:", error);
return buildHostCapabilityErrorStatus(error);
@@ -723,6 +732,18 @@ export function getHostCapability(name, options = {}) {
}
}
export function getPanelRuntimeDebugSnapshot(options = {}) {
const shouldRefreshHost = options?.refreshHost === true;
const hostCapabilities = shouldRefreshHost
? refreshHostCapabilityStatus()
: getHostCapabilityStatus();
return {
hostCapabilities,
runtimeDebug: readRuntimeDebugSnapshot(),
};
}
function getSchema() {
const settings = getSettings();
const schema = settings.nodeTypeSchema || DEFAULT_NODE_SCHEMA;
@@ -857,6 +878,17 @@ function clearInjectionState() {
lastRecalledItems = [];
lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
recordInjectionSnapshot("recall", {
injectionText: "",
selectedNodeIds: [],
retrievalMeta: {},
llmMeta: {},
transport: {
applied: false,
source: "cleared",
mode: "cleared",
},
});
if (!isRecalling) {
dismissStageNotice("recall");
}
@@ -1773,25 +1805,31 @@ function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
return null;
}
if (generationType === "normal") {
const lastNonSystemMessage = getLastNonSystemChatMessage(chat);
const tailUserText = lastNonSystemMessage?.is_user
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
: "";
const textareaText = normalizeRecallInputText(
pendingRecallSendIntent.text || getSendTextareaValue(),
);
const userMessage = tailUserText || textareaText;
if (!userMessage) return null;
return generationType === "normal"
? buildNormalGenerationRecallInput(chat)
: buildHistoryGenerationRecallInput(chat);
}
return {
overrideUserMessage: userMessage,
overrideSource: tailUserText ? "chat-tail-user" : "send-intent",
overrideSourceLabel: tailUserText ? "当前用户楼层" : "发送意图",
includeSyntheticUserMessage: !tailUserText,
};
}
function buildNormalGenerationRecallInput(chat) {
const lastNonSystemMessage = getLastNonSystemChatMessage(chat);
const tailUserText = lastNonSystemMessage?.is_user
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
: "";
const textareaText = normalizeRecallInputText(
pendingRecallSendIntent.text || getSendTextareaValue(),
);
const userMessage = tailUserText || textareaText;
if (!userMessage) return null;
return {
overrideUserMessage: userMessage,
overrideSource: tailUserText ? "chat-tail-user" : "send-intent",
overrideSourceLabel: tailUserText ? "当前用户楼层" : "发送意图",
includeSyntheticUserMessage: !tailUserText,
};
}
function buildHistoryGenerationRecallInput(chat) {
const latestUserText = normalizeRecallInputText(
getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text,
);
@@ -3179,7 +3217,20 @@ function applyRecallInjection(settings, recallInput, recentMessages, result) {
);
}
applyModuleInjectionPrompt(injectionText, settings);
const injectionTransport = applyModuleInjectionPrompt(injectionText, settings);
recordInjectionSnapshot("recall", {
taskType: "recall",
source: recallInput.source,
sourceLabel: recallInput.sourceLabel,
hookName: recallInput.hookName,
recentMessages,
selectedNodeIds: result.selectedNodeIds || [],
retrievalMeta,
llmMeta,
stats: result.stats || {},
injectionText,
transport: injectionTransport,
});
currentGraph.lastRecallResult = result.selectedNodeIds;
updateLastRecalledItems(result.selectedNodeIds || []);
@@ -3455,10 +3506,16 @@ async function onGenerationAfterCommands(type, params = {}, dryRun = false) {
}
async function onBeforeCombinePrompts() {
const context = getContext();
const chat = context?.chat;
const recallOptions =
buildNormalGenerationRecallInput(chat) ||
buildHistoryGenerationRecallInput(chat) ||
{};
const recallContext = createGenerationRecallContext({
hookName: "GENERATE_BEFORE_COMBINE_PROMPTS",
generationType: "normal",
recallOptions: {},
recallOptions,
});
if (!recallContext.shouldRun) {
return;
@@ -3470,6 +3527,7 @@ async function onBeforeCombinePrompts() {
"running",
);
const didRecall = await runRecall({
...recallOptions,
recallKey: recallContext.recallKey,
hookName: recallContext.hookName,
});
@@ -4107,6 +4165,8 @@ async function onReembedDirect() {
getLastBatchStatus: () =>
currentGraph?.historyState?.lastBatchStatus || null,
getLastInjection: () => lastInjectionContent,
getRuntimeDebugSnapshot: (options = {}) =>
getPanelRuntimeDebugSnapshot(options),
updateSettings: (patch) => {
const settings = updateModuleSettings(patch);
if (Object.prototype.hasOwnProperty.call(patch, "panelTheme")) {

58
llm.js
View File

@@ -5,6 +5,7 @@ import { getRequestHeaders } from "../../../../script.js";
import { extension_settings } from "../../../extensions.js";
import { chat_completion_sources, sendOpenAIRequest } from "../../../openai.js";
import { resolveTaskGenerationOptions } from "./generation-options.js";
import { recordTaskLlmRequest } from "./runtime-debug.js";
const MODULE_NAME = "st_bme";
const LLM_REQUEST_TIMEOUT_MS = 300000;
@@ -12,6 +13,11 @@ const DEFAULT_TEXT_COMPLETION_TOKENS = 64000;
const DEFAULT_JSON_COMPLETION_TOKENS = 64000;
const RETRY_JSON_COMPLETION_TOKENS = 3200;
function getLlmTestOverride(name) {
const override = globalThis.__stBmeTestOverrides?.llm?.[name];
return typeof override === "function" ? override : null;
}
function getMemoryLLMConfig() {
const settings = extension_settings[MODULE_NAME] || {};
return {
@@ -364,6 +370,23 @@ async function callDedicatedOpenAICompatible(
filtered: {},
removed: [],
};
recordTaskLlmRequest(taskType || privateRequestSource, {
requestSource: privateRequestSource,
taskType: String(taskType || "").trim(),
jsonMode,
dedicatedConfig: hasDedicatedConfig,
route: hasDedicatedConfig
? "dedicated-openai-compatible"
: "sillytavern-current-model",
model: hasDedicatedConfig ? config.model : "sillytavern-current-model",
apiUrl: hasDedicatedConfig ? config.apiUrl : "",
messages,
generation: generationResolved.generation || {},
filteredGeneration: generationResolved.filtered || {},
removedGeneration: generationResolved.removed || [],
capabilityMode: generationResolved.capabilityMode || "",
maxCompletionTokens,
});
if (!hasDedicatedConfig) {
const payload = await sendOpenAIRequest(
"quiet",
@@ -446,6 +469,23 @@ async function callDedicatedOpenAICompatible(
});
}
recordTaskLlmRequest(taskType || privateRequestSource, {
requestSource: privateRequestSource,
taskType: String(taskType || "").trim(),
jsonMode,
dedicatedConfig: true,
route: "dedicated-openai-compatible",
model: config.model,
apiUrl: config.apiUrl,
messages,
generation: generationResolved.generation || {},
filteredGeneration,
removedGeneration: generationResolved.removed || [],
capabilityMode: generationResolved.capabilityMode || "",
resolvedCompletionTokens,
requestBody: body,
});
const response = await fetchWithTimeout(
"/api/backends/chat-completions/generate",
{
@@ -528,6 +568,19 @@ export async function callLLMForJSON({
requestSource = "",
additionalMessages = [],
} = {}) {
const override = getLlmTestOverride("callLLMForJSON");
if (override) {
return await override({
systemPrompt,
userPrompt,
maxRetries,
signal,
taskType,
requestSource,
additionalMessages,
});
}
const privateRequestSource = resolvePrivateRequestSource(
taskType,
requestSource,
@@ -597,6 +650,11 @@ export async function callLLMForJSON({
* @returns {Promise<string|null>}
*/
export async function callLLM(systemPrompt, userPrompt, options = {}) {
const override = getLlmTestOverride("callLLM");
if (override) {
return await override(systemPrompt, userPrompt, options);
}
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },

292
panel.js
View File

@@ -37,6 +37,7 @@ const TASK_PROFILE_TABS = [
{ id: "generation", label: "生成参数" },
{ id: "prompt", label: "Prompt 编排" },
{ id: "regex", label: "正则" },
{ id: "debug", label: "调试预览" },
];
const TASK_PROFILE_ROLE_OPTIONS = [
@@ -136,6 +137,7 @@ let _getLastExtractionStatus = null;
let _getLastVectorStatus = null;
let _getLastRecallStatus = null;
let _getLastInjection = null;
let _getRuntimeDebugSnapshot = null;
let _updateSettings = null;
let _actionHandlers = {};
@@ -162,6 +164,7 @@ export async function initPanel({
getLastVectorStatus,
getLastRecallStatus,
getLastInjection,
getRuntimeDebugSnapshot,
updateSettings,
actions,
}) {
@@ -174,6 +177,7 @@ export async function initPanel({
_getLastVectorStatus = getLastVectorStatus;
_getLastRecallStatus = getLastRecallStatus;
_getLastInjection = getLastInjection;
_getRuntimeDebugSnapshot = getRuntimeDebugSnapshot;
_updateSettings = updateSettings;
_actionHandlers = actions || {};
@@ -275,6 +279,14 @@ export function refreshLiveState() {
break;
}
if (
currentTabId === "config" &&
currentConfigSectionId === "prompts" &&
currentTaskProfileTabId === "debug"
) {
_refreshTaskProfileWorkspace();
}
_refreshGraph();
}
@@ -1561,6 +1573,10 @@ function _handleTaskProfileWorkspaceChange(event) {
function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) {
const taskProfiles = ensureTaskProfiles(settings);
const taskTypeOptions = getTaskTypeOptions();
const runtimeDebug = _getRuntimeDebugSnapshot?.() || {
hostCapabilities: null,
runtimeDebug: null,
};
if (!taskTypeOptions.some((item) => item.id === currentTaskProfileTaskType)) {
currentTaskProfileTaskType = taskTypeOptions[0]?.id || "extract";
@@ -1605,6 +1621,7 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) {
selectedRule:
regexRules.find((rule) => rule.id === currentTaskProfileRuleId) || null,
builtinBlockDefinitions: getBuiltinBlockDefinitions(),
runtimeDebug,
};
}
@@ -1651,6 +1668,12 @@ async function _handleTaskProfileWorkspaceClick(event) {
actionEl.dataset.taskTab || currentTaskProfileTabId;
_refreshTaskProfileWorkspace();
return;
case "refresh-task-debug":
if (typeof _getRuntimeDebugSnapshot === "function") {
_getRuntimeDebugSnapshot({ refreshHost: true });
}
_refreshTaskProfileWorkspace();
return;
case "select-block":
currentTaskProfileBlockId = actionEl.dataset.blockId || "";
_refreshTaskProfileWorkspace();
@@ -1911,7 +1934,9 @@ function _renderTaskProfileWorkspace(state) {
? _renderTaskGenerationTab(state)
: state.taskTabId === "regex"
? _renderTaskRegexTab(state)
: _renderTaskPromptTab(state)
: state.taskTabId === "debug"
? _renderTaskDebugTab(state)
: _renderTaskPromptTab(state)
}
</div>
</div>
@@ -2126,6 +2151,271 @@ function _renderTaskRegexTab(state) {
`;
}
function _renderTaskDebugTab(state) {
const hostCapabilities = state.runtimeDebug?.hostCapabilities || null;
const runtimeDebug = state.runtimeDebug?.runtimeDebug || {};
const promptBuild = runtimeDebug?.taskPromptBuilds?.[state.taskType] || null;
const llmRequest = runtimeDebug?.taskLlmRequests?.[state.taskType] || null;
const recallInjection = runtimeDebug?.injections?.recall || null;
return `
<div class="bme-task-tab-body">
<div class="bme-task-toolbar-row">
<div class="bme-task-note">
这里展示的是最近一次真实运行留下的调试快照,不是静态配置推演。没有数据时,先跑一次对应任务即可。
</div>
<button class="bme-config-secondary-btn" data-task-action="refresh-task-debug" type="button">
刷新状态
</button>
</div>
<div class="bme-task-debug-grid">
<div class="bme-config-card">
${_renderTaskDebugHostCard(hostCapabilities)}
</div>
<div class="bme-config-card">
${_renderTaskDebugPromptCard(state.taskType, promptBuild)}
</div>
<div class="bme-config-card">
${_renderTaskDebugLlmCard(state.taskType, llmRequest)}
</div>
<div class="bme-config-card">
${_renderTaskDebugInjectionCard(recallInjection)}
</div>
</div>
</div>
`;
}
function _renderTaskDebugHostCard(hostCapabilities) {
if (!hostCapabilities) {
return `
<div class="bme-config-card-title">宿主桥接状态</div>
<div class="bme-config-help">当前还没有宿主桥接快照。</div>
`;
}
const capabilityNames = ["context", "worldbook", "regex", "injection"];
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">宿主桥接状态</div>
<div class="bme-config-card-subtitle">
当前插件和 SillyTavern 的接轨情况。
</div>
</div>
<span class="bme-task-pill ${hostCapabilities.available ? "is-builtin" : ""}">
${hostCapabilities.mode || (hostCapabilities.available ? "available" : "unavailable")}
</span>
</div>
<div class="bme-debug-kv-list">
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">总状态</span>
<span class="bme-debug-kv-value">${_escHtml(hostCapabilities.available ? "可用" : "不可用")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">说明</span>
<span class="bme-debug-kv-value">${_escHtml(hostCapabilities.fallbackReason || "无")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">快照版本</span>
<span class="bme-debug-kv-value">${_escHtml(String(hostCapabilities.snapshotRevision ?? "—"))}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">快照时间</span>
<span class="bme-debug-kv-value">${_escHtml(_formatTaskProfileTime(hostCapabilities.snapshotCreatedAt))}</span>
</div>
</div>
<div class="bme-task-section-label">分项能力</div>
<div class="bme-debug-capability-list">
${capabilityNames
.map((name) => {
const capability = hostCapabilities[name] || {};
return `
<div class="bme-debug-capability-item">
<div class="bme-debug-capability-head">
<span class="bme-debug-capability-title">${_escHtml(name)}</span>
<span class="bme-task-pill ${capability.available ? "is-builtin" : ""}">
${_escHtml(capability.mode || (capability.available ? "available" : "unavailable"))}
</span>
</div>
<div class="bme-debug-capability-desc">
${_escHtml(capability.fallbackReason || "无")}
</div>
</div>
`;
})
.join("")}
</div>
`;
}
function _renderTaskDebugPromptCard(taskType, promptBuild) {
if (!promptBuild) {
return `
<div class="bme-config-card-title">最近 Prompt 组装</div>
<div class="bme-config-help">当前任务还没有最近一次 prompt 组装快照。</div>
`;
}
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">最近 Prompt 组装</div>
<div class="bme-config-card-subtitle">
任务 ${_escHtml(taskType)} 最近一次真实编排结果。
</div>
</div>
<span class="bme-task-pill">${_escHtml(_formatTaskProfileTime(promptBuild.updatedAt))}</span>
</div>
<div class="bme-debug-kv-list">
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">预设</span>
<span class="bme-debug-kv-value">${_escHtml(promptBuild.profileName || promptBuild.profileId || "—")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">块数量</span>
<span class="bme-debug-kv-value">${_escHtml(String(promptBuild.debug?.renderedBlockCount ?? promptBuild.renderedBlocks?.length ?? 0))}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">宿主注入</span>
<span class="bme-debug-kv-value">${_escHtml(String(promptBuild.debug?.hostInjectionPlanCount ?? promptBuild.debug?.hostInjectionCount ?? 0))}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">私有消息</span>
<span class="bme-debug-kv-value">${_escHtml(String(promptBuild.debug?.privateTaskMessageCount ?? promptBuild.privateTaskMessages?.length ?? 0))}</span>
</div>
</div>
${_renderDebugDetails("渲染后的块", promptBuild.renderedBlocks)}
${_renderDebugDetails("宿主注入计划", promptBuild.hostInjectionPlan || null)}
${_renderDebugDetails("宿主注入描述", promptBuild.hostInjections)}
${_renderDebugDetails("私有任务消息", promptBuild.privateTaskMessages)}
${_renderDebugDetails("系统提示词", promptBuild.systemPrompt || "")}
`;
}
function _renderTaskDebugLlmCard(taskType, llmRequest) {
if (!llmRequest) {
return `
<div class="bme-config-card-title">最近实际下发参数</div>
<div class="bme-config-help">当前任务还没有最近一次 LLM 请求快照。</div>
`;
}
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">最近实际下发参数</div>
<div class="bme-config-card-subtitle">
任务 ${_escHtml(taskType)} 最近一次走私有请求层时的实际发送信息。
</div>
</div>
<span class="bme-task-pill">${_escHtml(_formatTaskProfileTime(llmRequest.updatedAt))}</span>
</div>
<div class="bme-debug-kv-list">
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">请求来源</span>
<span class="bme-debug-kv-value">${_escHtml(llmRequest.requestSource || "—")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">请求路径</span>
<span class="bme-debug-kv-value">${_escHtml(llmRequest.route || "—")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">模型</span>
<span class="bme-debug-kv-value">${_escHtml(llmRequest.model || "—")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">能力过滤模式</span>
<span class="bme-debug-kv-value">${_escHtml(llmRequest.capabilityMode || "—")}</span>
</div>
</div>
${_renderDebugDetails("实际保留参数", llmRequest.filteredGeneration || {})}
${_renderDebugDetails("被过滤掉的参数", llmRequest.removedGeneration || [])}
${_renderDebugDetails("最终消息列表", llmRequest.messages || [])}
${_renderDebugDetails("最终请求体", llmRequest.requestBody || null)}
`;
}
function _renderTaskDebugInjectionCard(injectionSnapshot) {
if (!injectionSnapshot) {
return `
<div class="bme-config-card-title">最近注入结果</div>
<div class="bme-config-help">还没有最近一次召回注入快照。</div>
`;
}
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">最近注入结果</div>
<div class="bme-config-card-subtitle">
展示最近一次召回后的注入文本和宿主投递方式。
</div>
</div>
<span class="bme-task-pill">${_escHtml(_formatTaskProfileTime(injectionSnapshot.updatedAt))}</span>
</div>
<div class="bme-debug-kv-list">
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">来源</span>
<span class="bme-debug-kv-value">${_escHtml(injectionSnapshot.sourceLabel || injectionSnapshot.source || "—")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">触发钩子</span>
<span class="bme-debug-kv-value">${_escHtml(injectionSnapshot.hookName || "—")}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">选中节点数</span>
<span class="bme-debug-kv-value">${_escHtml(String(injectionSnapshot.selectedNodeIds?.length ?? 0))}</span>
</div>
<div class="bme-debug-kv-item">
<span class="bme-debug-kv-key">宿主投递</span>
<span class="bme-debug-kv-value">${_escHtml(injectionSnapshot.transport?.source || "—")} / ${_escHtml(injectionSnapshot.transport?.mode || "—")}</span>
</div>
</div>
${_renderDebugDetails("召回统计", {
retrievalMeta: injectionSnapshot.retrievalMeta || {},
llmMeta: injectionSnapshot.llmMeta || {},
stats: injectionSnapshot.stats || {},
transport: injectionSnapshot.transport || {},
})}
${_renderDebugDetails("最终注入文本", injectionSnapshot.injectionText || "")}
`;
}
function _renderDebugDetails(title, value) {
const isEmptyArray = Array.isArray(value) && value.length === 0;
const isEmptyObject =
value &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.keys(value).length === 0;
const isEmpty = value == null || value === "" || isEmptyArray || isEmptyObject;
return `
<details class="bme-debug-details" ${isEmpty ? "" : "open"}>
<summary>${_escHtml(title)}</summary>
${
isEmpty
? '<div class="bme-debug-empty">暂无内容</div>'
: `<pre class="bme-debug-pre">${_escHtml(_stringifyDebugValue(value))}</pre>`
}
</details>
`;
}
function _stringifyDebugValue(value) {
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function _renderTaskBlockListItem(block, index, state) {
const isSelected = block.id === state.selectedBlock?.id;
return `

View File

@@ -2,6 +2,7 @@
// 统一负责任务预设块排序、变量渲染,以及世界书/EJS 上下文接入。
import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js";
import { recordTaskPromptBuild } from "./runtime-debug.js";
import { resolveTaskWorldInfo } from "./task-worldinfo.js";
const WORLD_INFO_VARIABLE_KEYS = [
@@ -122,6 +123,98 @@ function buildWorldInfoResolution(worldInfoContext = {}) {
};
}
function sortInjectionEntries(entries = []) {
return [...entries].sort((left, right) => {
const orderLeft = Number.isFinite(Number(left?.order))
? Number(left.order)
: 0;
const orderRight = Number.isFinite(Number(right?.order))
? Number(right.order)
: 0;
return orderLeft - orderRight;
});
}
function createHostInjectionPlanEntry(block = {}, position, extra = {}) {
return {
source: "block",
origin: "profile-block",
position,
role: normalizeRole(block.role),
content: String(block.content || "").trim(),
blockId: String(block.id || ""),
blockName: String(block.name || ""),
sourceKey: String(block.sourceKey || ""),
injectionMode: normalizeInjectionMode(block.injectionMode),
order: Number.isFinite(Number(block.order)) ? Number(block.order) : 0,
...extra,
};
}
function buildHostInjectionPlan(renderedBlocks = [], worldInfoResolution = {}) {
const beforeEntryNames = (
Array.isArray(worldInfoResolution.beforeEntries)
? worldInfoResolution.beforeEntries
: []
)
.map((entry) => String(entry?.name || entry?.sourceName || "").trim())
.filter(Boolean);
const afterEntryNames = (
Array.isArray(worldInfoResolution.afterEntries)
? worldInfoResolution.afterEntries
: []
)
.map((entry) => String(entry?.name || entry?.sourceName || "").trim())
.filter(Boolean);
const atDepthEntries = Array.isArray(worldInfoResolution.injections?.atDepth)
? worldInfoResolution.injections.atDepth
: [];
const plan = {
before: [],
after: [],
atDepth: [],
};
for (const block of renderedBlocks) {
if (!block?.content) continue;
if (block.delivery === "host.before") {
plan.before.push(
createHostInjectionPlanEntry(block, "before", {
entryNames: beforeEntryNames,
entryCount: beforeEntryNames.length,
}),
);
continue;
}
if (block.delivery === "host.after") {
plan.after.push(
createHostInjectionPlanEntry(block, "after", {
entryNames: afterEntryNames,
entryCount: afterEntryNames.length,
}),
);
}
}
for (const entry of atDepthEntries) {
if (!entry?.content) continue;
plan.atDepth.push({
...entry,
origin: "worldInfo-entry",
entryName: String(entry.name || entry.sourceName || "").trim(),
});
}
return {
before: sortInjectionEntries(plan.before),
after: sortInjectionEntries(plan.after),
atDepth: sortInjectionEntries(plan.atDepth),
};
}
function resolveBlockDelivery(block = {}) {
if (
block.type === "builtin" &&
@@ -281,10 +374,15 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
...customMessages,
...worldInfoResolution.additionalMessages,
];
const hostInjectionPlan = buildHostInjectionPlan(
renderedBlocks,
worldInfoResolution,
);
return {
const result = {
profile,
hostInjections: worldInfoResolution.injections,
hostInjectionPlan,
privateTaskPrompt: {
systemPrompt,
messages: privateTaskMessages,
@@ -318,11 +416,30 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
worldInfoResolution.injections.before.length +
worldInfoResolution.injections.after.length +
worldInfoResolution.injections.atDepth.length,
hostInjectionPlanCount:
hostInjectionPlan.before.length +
hostInjectionPlan.after.length +
hostInjectionPlan.atDepth.length,
customMessageCount: customMessages.length,
additionalMessageCount: worldInfoResolution.additionalMessages.length,
privateTaskMessageCount: privateTaskMessages.length,
},
};
recordTaskPromptBuild(taskType, {
taskType,
profileId: profile?.id || "",
profileName: profile?.name || "",
systemPrompt,
privateTaskMessages,
renderedBlocks,
hostInjections: worldInfoResolution.injections,
hostInjectionPlan,
worldInfoResolution,
debug: result.debug,
});
return result;
}
export function interpolateVariables(template, context = {}) {

94
runtime-debug.js Normal file
View File

@@ -0,0 +1,94 @@
function safeClone(value, fallback = null) {
if (value == null) {
return fallback;
}
try {
if (typeof structuredClone === "function") {
return structuredClone(value);
}
} catch {
// ignore and fall through
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallback ?? value;
}
}
function nowIso() {
return new Date().toISOString();
}
const runtimeDebugState = {
hostCapabilities: null,
taskPromptBuilds: {},
taskLlmRequests: {},
injections: {},
updatedAt: "",
};
function touchRuntimeDebugState() {
runtimeDebugState.updatedAt = nowIso();
}
export function resetRuntimeDebugSnapshot() {
runtimeDebugState.hostCapabilities = null;
runtimeDebugState.taskPromptBuilds = {};
runtimeDebugState.taskLlmRequests = {};
runtimeDebugState.injections = {};
runtimeDebugState.updatedAt = nowIso();
}
export function recordHostCapabilitySnapshot(snapshot = null) {
runtimeDebugState.hostCapabilities = safeClone(snapshot, null);
touchRuntimeDebugState();
}
export function recordTaskPromptBuild(taskType, snapshot = {}) {
const normalizedTaskType = String(taskType || "").trim() || "unknown";
runtimeDebugState.taskPromptBuilds[normalizedTaskType] = {
updatedAt: nowIso(),
...safeClone(snapshot, {}),
};
touchRuntimeDebugState();
}
export function recordTaskLlmRequest(taskType, snapshot = {}) {
const normalizedTaskType = String(taskType || "").trim() || "unknown";
runtimeDebugState.taskLlmRequests[normalizedTaskType] = {
updatedAt: nowIso(),
...safeClone(snapshot, {}),
};
touchRuntimeDebugState();
}
export function recordInjectionSnapshot(kind, snapshot = {}) {
const normalizedKind = String(kind || "").trim() || "default";
runtimeDebugState.injections[normalizedKind] = {
updatedAt: nowIso(),
...safeClone(snapshot, {}),
};
touchRuntimeDebugState();
}
export function getRuntimeDebugSnapshot() {
return safeClone(
{
hostCapabilities: runtimeDebugState.hostCapabilities,
taskPromptBuilds: runtimeDebugState.taskPromptBuilds,
taskLlmRequests: runtimeDebugState.taskLlmRequests,
injections: runtimeDebugState.injections,
updatedAt: runtimeDebugState.updatedAt,
},
{
hostCapabilities: null,
taskPromptBuilds: {},
taskLlmRequests: {},
injections: {},
updatedAt: "",
},
);
}

116
style.css
View File

@@ -1669,6 +1669,114 @@
min-height: 160px;
}
.bme-task-debug-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.bme-debug-kv-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.bme-debug-kv-item {
display: grid;
grid-template-columns: 110px minmax(0, 1fr);
gap: 10px;
align-items: start;
}
.bme-debug-kv-key {
font-size: 11px;
color: var(--bme-on-surface-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.bme-debug-kv-value {
font-size: 12px;
line-height: 1.5;
color: var(--bme-on-surface);
word-break: break-word;
}
.bme-debug-capability-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.bme-debug-capability-item {
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.bme-debug-capability-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 6px;
}
.bme-debug-capability-title {
font-size: 13px;
font-weight: 700;
color: var(--bme-on-surface);
}
.bme-debug-capability-desc {
font-size: 12px;
line-height: 1.5;
color: var(--bme-on-surface-dim);
}
.bme-debug-details {
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
background: rgba(255, 255, 255, 0.02);
overflow: hidden;
}
.bme-debug-details summary {
cursor: pointer;
padding: 12px 14px;
font-size: 12px;
font-weight: 700;
color: var(--bme-on-surface);
list-style: none;
}
.bme-debug-details summary::-webkit-details-marker {
display: none;
}
.bme-debug-pre {
margin: 0;
padding: 0 14px 14px;
white-space: pre-wrap;
word-break: break-word;
font-size: 12px;
line-height: 1.55;
color: var(--bme-on-surface);
font-family:
"JetBrains Mono",
"Cascadia Code",
"Fira Code",
Consolas,
monospace;
}
.bme-debug-empty {
padding: 0 14px 14px;
font-size: 12px;
color: var(--bme-on-surface-dim);
}
.bme-theme-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1932,10 +2040,16 @@
.bme-theme-card-grid,
.bme-task-field-grid,
.bme-task-editor-grid,
.bme-task-regex-top {
.bme-task-regex-top,
.bme-task-debug-grid {
grid-template-columns: 1fr;
}
.bme-debug-kv-item {
grid-template-columns: 1fr;
gap: 4px;
}
.bme-config-card-head,
.bme-prompt-card-head {
flex-direction: column;

View File

@@ -1,11 +1,73 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import { createRequire, registerHooks } from "node:module";
import path from "node:path";
import vm from "node:vm";
const extensionsShimSource = [
"export const extension_settings = globalThis.__p0ExtensionSettings || {};",
"export function getContext(...args) {",
" return globalThis.SillyTavern?.getContext?.(...args) || null;",
"}",
].join("\n");
const scriptShimSource = [
"export function getRequestHeaders() {",
" return { 'Content-Type': 'application/json' };",
"}",
].join("\n");
const openAiShimSource = [
"export const chat_completion_sources = { CUSTOM: 'custom', OPENAI: 'openai' };",
"export async function sendOpenAIRequest(...args) {",
" if (typeof globalThis.__p0SendOpenAIRequest === 'function') {",
" return await globalThis.__p0SendOpenAIRequest(...args);",
" }",
" return { choices: [{ message: { content: '{}' } }] };",
"}",
].join("\n");
const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
extensionsShimSource,
)}`;
const scriptShimUrl = `data:text/javascript,${encodeURIComponent(
scriptShimSource,
)}`;
const openAiShimUrl = `data:text/javascript,${encodeURIComponent(
openAiShimSource,
)}`;
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "../../../extensions.js") {
return {
shortCircuit: true,
url: extensionsShimUrl,
};
}
if (specifier === "../../../../script.js") {
return {
shortCircuit: true,
url: scriptShimUrl,
};
}
if (specifier === "../../../openai.js") {
return {
shortCircuit: true,
url: openAiShimUrl,
};
}
return nextResolve(specifier, context);
},
});
const require = createRequire(import.meta.url);
const originalRequire = globalThis.require;
const originalP0ExtensionSettings = globalThis.__p0ExtensionSettings;
const originalP0SendOpenAIRequest = globalThis.__p0SendOpenAIRequest;
const originalStBmeTestOverrides = globalThis.__stBmeTestOverrides;
globalThis.__p0ExtensionSettings = {
st_bme: {},
};
globalThis.__stBmeTestOverrides = {};
globalThis.require = require;
const { createEmptyGraph, createNode, addNode, createEdge, addEdge } =
@@ -29,6 +91,24 @@ if (originalRequire === undefined) {
globalThis.require = originalRequire;
}
if (originalP0ExtensionSettings === undefined) {
delete globalThis.__p0ExtensionSettings;
} else {
globalThis.__p0ExtensionSettings = originalP0ExtensionSettings;
}
if (originalP0SendOpenAIRequest === undefined) {
delete globalThis.__p0SendOpenAIRequest;
} else {
globalThis.__p0SendOpenAIRequest = originalP0SendOpenAIRequest;
}
if (originalStBmeTestOverrides === undefined) {
delete globalThis.__stBmeTestOverrides;
} else {
globalThis.__stBmeTestOverrides = originalStBmeTestOverrides;
}
const schema = [
{
id: "event",
@@ -56,12 +136,12 @@ function createBatchStageHarness() {
const indexPath = path.resolve("./index.js");
return fs.readFile(indexPath, "utf8").then((source) => {
const marker = "function isAssistantChatMessage(message) {";
const start = source.indexOf("const BATCH_STAGE_ORDER =");
const start = source.indexOf("function shouldAdvanceProcessedHistory(");
const end = source.indexOf(marker);
if (start < 0 || end < 0 || end <= start) {
throw new Error("无法从 index.js 提取批次状态机定义");
}
const snippet = source.slice(start, end);
const snippet = source.slice(start, end).replace(/^export\s+/gm, "");
const context = {
console,
result: null,
@@ -103,13 +183,18 @@ function createGenerationRecallHarness() {
if (start < 0 || end < 0 || end <= start) {
throw new Error("无法从 index.js 提取生成召回事务定义");
}
const snippet = source.slice(start, end);
const snippet = source.slice(start, end).replace(/^export\s+/gm, "");
const context = {
console,
Date,
Map,
setTimeout,
clearTimeout,
document: {
getElementById() {
return null;
},
},
result: null,
currentGraph: {},
isRecalling: false,
@@ -132,14 +217,11 @@ function createGenerationRecallHarness() {
? [...chat, { is_user: true, mes: syntheticUserMessage }]
: [...chat],
getContext: () => ({
chatId: "chat-main",
chat: context.chat,
}),
chat: [],
runRecallCalls: [],
runRecall: async (options = {}) => {
context.runRecallCalls.push({ ...options });
return true;
},
};
vm.createContext(context);
vm.runInContext(
@@ -147,10 +229,34 @@ function createGenerationRecallHarness() {
context,
{ filename: indexPath },
);
context.runRecall = async (options = {}) => {
context.runRecallCalls.push({ ...options });
return true;
};
return context;
});
}
function pushTestOverrides(patch = {}) {
const previous = globalThis.__stBmeTestOverrides || {};
globalThis.__stBmeTestOverrides = {
...previous,
...patch,
llm: {
...(previous.llm || {}),
...(patch.llm || {}),
},
embedding: {
...(previous.embedding || {}),
...(patch.embedding || {}),
},
};
return () => {
globalThis.__stBmeTestOverrides = previous;
};
}
function makeEvent(seq, title) {
return createNode({
type: "event",
@@ -186,13 +292,18 @@ async function testCompressorMigratesEdgesToCompressedNode() {
}),
);
const originalSummarize = llm.callLLMForJSON;
llm.callLLMForJSON = async () => ({
fields: {
title: "压缩事件",
summary: "合并摘要",
participants: "Alice",
status: "done",
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
fields: {
title: "压缩事件",
summary: "合并摘要",
participants: "Alice",
status: "done",
},
};
},
},
});
@@ -220,7 +331,7 @@ async function testCompressorMigratesEdgesToCompressedNode() {
);
assert.ok(migrated);
} finally {
llm.callLLMForJSON = originalSummarize;
restoreOverrides();
}
}
@@ -233,8 +344,13 @@ async function testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure() {
graph.vectorIndexState.dirty = true;
graph.vectorIndexState.lastWarning = "旧 warning";
const originalEmbedBatch = embedding.embedBatch;
embedding.embedBatch = async () => [[0.1, 0.2], null];
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.1, 0.2], null];
},
},
});
try {
const result = await syncGraphVectorIndex(
@@ -262,7 +378,7 @@ async function testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure() {
);
assert.equal(second.embedding, null);
} finally {
embedding.embedBatch = originalEmbedBatch;
restoreOverrides();
}
}
@@ -292,20 +408,29 @@ async function testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing() {
addNode(graph, target);
addNode(graph, incoming);
const originalFindSimilar = embedding.searchSimilar;
const originalEmbedBatch = embedding.embedBatch;
const originalCall = llm.callLLMForJSON;
embedding.embedBatch = async () => [[0.2, 0.3]];
embedding.searchSimilar = async () => [{ nodeId: target.id, score: 0.99 }];
llm.callLLMForJSON = async () => ({
results: [
{
node_id: incoming.id,
action: "merge",
merge_target_id: "missing-node-id",
reason: "故意触发无效 merge target 回退",
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.2, 0.3]];
},
],
searchSimilar() {
return [{ nodeId: target.id, score: 0.99 }];
},
},
llm: {
async callLLMForJSON() {
return {
results: [
{
node_id: incoming.id,
action: "merge",
merge_target_id: "missing-node-id",
reason: "故意触发无效 merge target 回退",
},
],
};
},
},
});
try {
@@ -326,17 +451,20 @@ async function testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing() {
assert.equal(incoming.archived, false);
assert.deepEqual(target.embedding, [0.9, 0.1]);
} finally {
embedding.searchSimilar = originalFindSimilar;
embedding.embedBatch = originalEmbedBatch;
llm.callLLMForJSON = originalCall;
restoreOverrides();
}
}
async function testExtractorFailsOnUnknownOperation() {
const graph = createEmptyGraph();
const originalCall = llm.callLLMForJSON;
llm.callLLMForJSON = async () => ({
operations: [{ action: "nonsense", foo: 1 }],
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
operations: [{ action: "nonsense", foo: 1 }],
};
},
},
});
try {
@@ -354,7 +482,7 @@ async function testExtractorFailsOnUnknownOperation() {
assert.match(result.error, /未知操作类型/);
assert.equal(graph.lastProcessedSeq, -1);
} finally {
llm.callLLMForJSON = originalCall;
restoreOverrides();
}
}
@@ -371,6 +499,7 @@ async function testConsolidatorMergeUpdatesSeqRange() {
status: "active",
},
});
target.embedding = [0.8, 0.2];
const incoming = createNode({
type: "event",
seq: 8,
@@ -385,25 +514,41 @@ async function testConsolidatorMergeUpdatesSeqRange() {
addNode(graph, target);
addNode(graph, incoming);
const originalFindSimilar = embedding.searchSimilar;
const originalCall = llm.callLLMForJSON;
embedding.searchSimilar = async () => [{ nodeId: target.id, score: 0.99 }];
llm.callLLMForJSON = async () => ({
results: [
{
node_id: incoming.id,
action: "merge",
merge_target_id: target.id,
merged_fields: { summary: "合并后摘要" },
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.4, 0.5]];
},
],
searchSimilar() {
return [{ nodeId: target.id, score: 0.99 }];
},
},
llm: {
async callLLMForJSON() {
return {
results: [
{
node_id: incoming.id,
action: "merge",
merge_target_id: target.id,
merged_fields: { summary: "合并后摘要" },
},
],
};
},
},
});
try {
const stats = await consolidateMemories({
graph,
newNodeIds: [incoming.id],
embeddingConfig: null,
embeddingConfig: {
mode: "direct",
source: "direct",
apiUrl: "https://example.com/v1",
model: "text-embedding-3-small",
},
settings: {},
});
@@ -414,8 +559,7 @@ async function testConsolidatorMergeUpdatesSeqRange() {
assert.equal(target.embedding, null);
assert.equal(incoming.archived, true);
} finally {
embedding.searchSimilar = originalFindSimilar;
llm.callLLMForJSON = originalCall;
restoreOverrides();
}
}
@@ -423,17 +567,17 @@ async function testBatchJournalVectorDeltaCapturesRecoveryFields() {
const before = normalizeGraphRuntimeState(createEmptyGraph(), "chat-a");
const after = normalizeGraphRuntimeState(createEmptyGraph(), "chat-a");
const beforeNode = createNode({
id: "node-before",
type: "event",
seq: 1,
fields: { title: "旧", summary: "旧", participants: "A", status: "old" },
});
beforeNode.id = "node-before";
const afterNode = createNode({
id: "node-before",
type: "event",
seq: 1,
fields: { title: "新", summary: "新", participants: "A", status: "new" },
});
afterNode.id = "node-before";
addNode(before, beforeNode);
addNode(after, afterNode);
before.vectorIndexState.hashToNodeId = { hash_old: "node-before" };
@@ -550,7 +694,6 @@ async function testReverseJournalRollbackStateFormsReplayClosure() {
const before = normalizeGraphRuntimeState(createEmptyGraph(), "chat-replay");
const after = normalizeGraphRuntimeState(createEmptyGraph(), "chat-replay");
const stableNode = createNode({
id: "node-stable",
type: "event",
seq: 1,
fields: {
@@ -560,8 +703,8 @@ async function testReverseJournalRollbackStateFormsReplayClosure() {
status: "stable",
},
});
stableNode.id = "node-stable";
const touchedBefore = createNode({
id: "node-touched",
type: "event",
seq: 2,
fields: {
@@ -571,8 +714,8 @@ async function testReverseJournalRollbackStateFormsReplayClosure() {
status: "old",
},
});
touchedBefore.id = "node-touched";
const touchedAfter = createNode({
id: "node-touched",
type: "event",
seq: 5,
fields: {
@@ -582,8 +725,8 @@ async function testReverseJournalRollbackStateFormsReplayClosure() {
status: "updated",
},
});
touchedAfter.id = "node-touched";
const appendedNode = createNode({
id: "node-appended",
type: "event",
seq: 6,
fields: {
@@ -593,6 +736,7 @@ async function testReverseJournalRollbackStateFormsReplayClosure() {
status: "new",
},
});
appendedNode.id = "node-appended";
addNode(before, stableNode);
addNode(before, touchedBefore);
addNode(after, stableNode);

View File

@@ -237,9 +237,19 @@ try {
promptBuild.hostInjections.before.map((entry) => entry.name),
["常驻设定", "EW/Controller/Main", "线索条目"],
);
assert.equal(promptBuild.hostInjectionPlan.before.length, 1);
assert.equal(promptBuild.hostInjectionPlan.before[0].blockId, "b1");
assert.equal(promptBuild.hostInjectionPlan.before[0].sourceKey, "worldInfoBefore");
assert.deepEqual(promptBuild.hostInjectionPlan.before[0].entryNames, [
"常驻设定",
"EW/Controller/Main",
"线索条目",
]);
assert.equal(promptBuild.hostInjections.after.length, 0);
assert.equal(promptBuild.hostInjections.atDepth.length, 1);
assert.equal(promptBuild.hostInjections.atDepth[0].depth, 2);
assert.equal(promptBuild.hostInjectionPlan.atDepth.length, 1);
assert.equal(promptBuild.hostInjectionPlan.atDepth[0].entryName, "深度注入");
assert.deepEqual(
promptBuild.renderedBlocks.map((block) => block.delivery),
["host.before", "private.message"],