mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat: add runtime debug snapshots and injection planning
This commit is contained in:
@@ -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 总结
|
||||
|
||||
20
embedding.js
20
embedding.js
@@ -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
102
index.js
@@ -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
58
llm.js
@@ -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
292
panel.js
@@ -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 `
|
||||
|
||||
@@ -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
94
runtime-debug.js
Normal 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
116
style.css
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user