Files
ST-Bionic-Memory-Ecology/prompt-builder.js
Youzini-afk d31c0325d3 feat: Phase 3 世界书引擎移植 + EJS 支持
- 新增 task-worldinfo.js: 从 EW 移植世界书激活/分桶引擎
- 新增 task-ejs.js: 从 EW 移植 EJS 模板渲染引擎
- 新增 vendor/ejs.js: EJS runtime vendor
- prompt-builder.js: 改为异步, 接入 worldInfoBefore/After/atDepth
- prompt-profiles.js: 新增内置块 charDescription/userPersona/worldInfoBefore/After
- 更新 extractor/retriever/compressor/consolidator 接入新 builder
- st-context.js: 扩展 ST 上下文字段兜底
- 新增 tests/task-worldinfo.mjs: 世界书引擎测试
2026-03-26 13:57:07 +08:00

219 lines
6.4 KiB
JavaScript

// ST-BME: Prompt Builder
// 统一负责任务预设块排序、变量渲染,以及世界书/EJS 上下文接入。
import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js";
import { resolveTaskWorldInfo } from "./task-worldinfo.js";
const WORLD_INFO_VARIABLE_KEYS = [
"worldInfoBefore",
"worldInfoAfter",
"worldInfoBeforeEntries",
"worldInfoAfterEntries",
"worldInfoAtDepthEntries",
"activatedWorldInfoNames",
"taskAdditionalMessages",
];
function getByPath(target, path) {
return String(path || "")
.split(".")
.filter(Boolean)
.reduce((acc, key) => (acc == null ? undefined : acc[key]), target);
}
function normalizeRole(role) {
const value = String(role || "system").toLowerCase();
if (["system", "user", "assistant"].includes(value)) {
return value;
}
return "system";
}
function normalizeInjectionMode(mode) {
const value = String(mode || "append").toLowerCase();
if (["prepend", "append", "relative"].includes(value)) {
return value;
}
return "append";
}
function stringifyInterpolatedValue(value) {
if (value == null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function buildEmptyWorldInfoContext() {
return {
worldInfoBefore: "",
worldInfoAfter: "",
worldInfoBeforeEntries: [],
worldInfoAfterEntries: [],
worldInfoAtDepthEntries: [],
activatedWorldInfoNames: [],
taskAdditionalMessages: [],
};
}
function profileRequiresWorldInfo(profile) {
const blocks = Array.isArray(profile?.blocks) ? profile.blocks : [];
for (const block of blocks) {
if (!block || block.enabled === false) continue;
if (
block.type === "builtin" &&
["worldInfoBefore", "worldInfoAfter"].includes(String(block.sourceKey || ""))
) {
return true;
}
const rawContent = String(block.content || "");
if (!rawContent.includes("{{")) continue;
if (
WORLD_INFO_VARIABLE_KEYS.some((key) =>
rawContent.includes(`{{${key}}}`) ||
rawContent.includes(`{{ ${key} }}`),
)
) {
return true;
}
}
return false;
}
function extractWorldInfoChatMessages(context = {}) {
if (Array.isArray(context.chatMessages)) {
return context.chatMessages;
}
return [];
}
export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
const profile = getActiveTaskProfile(settings, taskType);
const legacyPrompt = getLegacyPromptForTask(settings, taskType);
const rawBlocks = Array.isArray(profile?.blocks) ? profile.blocks : [];
const blocks = rawBlocks
.map((block, index) => ({ ...block, _orderIndex: index }))
.sort((a, b) => {
const orderA = Number.isFinite(Number(a.order))
? Number(a.order)
: a._orderIndex;
const orderB = Number.isFinite(Number(b.order))
? Number(b.order)
: b._orderIndex;
return orderA - orderB;
});
const worldInfoRequested = profileRequiresWorldInfo(profile);
const emptyWorldInfo = buildEmptyWorldInfoContext();
let resolvedWorldInfo = emptyWorldInfo;
if (worldInfoRequested) {
const worldInfo = await resolveTaskWorldInfo({
settings,
chatMessages: extractWorldInfoChatMessages(context),
userMessage: String(context.userMessage || ""),
templateContext: context,
});
resolvedWorldInfo = {
worldInfoBefore: worldInfo.beforeText || "",
worldInfoAfter: worldInfo.afterText || "",
worldInfoBeforeEntries: worldInfo.beforeEntries || [],
worldInfoAfterEntries: worldInfo.afterEntries || [],
worldInfoAtDepthEntries: worldInfo.atDepthEntries || [],
activatedWorldInfoNames: worldInfo.activatedEntryNames || [],
taskAdditionalMessages: worldInfo.additionalMessages || [],
};
}
const resolvedContext = {
...context,
...emptyWorldInfo,
...resolvedWorldInfo,
};
let systemPrompt = "";
const customMessages = [];
for (const block of blocks) {
if (!block || block.enabled === false) continue;
const role = normalizeRole(block.role);
let content = "";
if (block.type === "legacyPrompt") {
content = legacyPrompt || block.content || "";
} else if (block.type === "builtin") {
if (block.content) {
content = interpolateVariables(block.content, resolvedContext);
} else if (block.sourceKey) {
content = stringifyInterpolatedValue(
getByPath(resolvedContext, block.sourceKey),
);
}
} else if (block.type === "custom") {
content = interpolateVariables(block.content || "", resolvedContext);
}
if (!String(content || "").trim()) continue;
const mode = normalizeInjectionMode(block.injectionMode);
if (role === "system") {
if (!systemPrompt) {
systemPrompt = content;
} else if (mode === "prepend") {
systemPrompt = `${content}\n\n${systemPrompt}`;
} else {
systemPrompt = `${systemPrompt}\n\n${content}`;
}
continue;
}
if (mode === "prepend") {
customMessages.unshift({ role, content });
} else {
customMessages.push({ role, content });
}
}
return {
profile,
systemPrompt,
customMessages,
additionalMessages: resolvedContext.taskAdditionalMessages || [],
worldInfo: {
beforeText: resolvedContext.worldInfoBefore,
afterText: resolvedContext.worldInfoAfter,
beforeEntries: resolvedContext.worldInfoBeforeEntries,
afterEntries: resolvedContext.worldInfoAfterEntries,
atDepthEntries: resolvedContext.worldInfoAtDepthEntries,
activatedEntryNames: resolvedContext.activatedWorldInfoNames,
},
debug: {
taskType,
profileId: profile?.id || "",
profileName: profile?.name || "",
usedLegacyPrompt: Boolean(legacyPrompt),
blockCount: blocks.length,
worldInfoRequested,
worldInfoBeforeCount: resolvedContext.worldInfoBeforeEntries.length,
worldInfoAfterCount: resolvedContext.worldInfoAfterEntries.length,
worldInfoAtDepthCount: resolvedContext.worldInfoAtDepthEntries.length,
additionalMessageCount: resolvedContext.taskAdditionalMessages.length,
},
};
}
export function interpolateVariables(template, context = {}) {
return String(template || "").replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
return stringifyInterpolatedValue(getByPath(context, key));
});
}