Fix regex stage timing and implicit world info resolution

This commit is contained in:
Youzini-afk
2026-04-05 18:12:09 +08:00
parent 782d7fb3cd
commit 12f2a7a6eb
5 changed files with 136 additions and 22 deletions

View File

@@ -3926,7 +3926,7 @@ function _renderTaskDebugPromptCard(taskType, promptBuild) {
${_renderDebugDetails("实际投递路径", promptBuild.debug?.effectivePath || null)} ${_renderDebugDetails("实际投递路径", promptBuild.debug?.effectivePath || null)}
${_renderDebugDetails("渲染后的块(按配置顺序)", promptBuild.renderedBlocks)} ${_renderDebugDetails("渲染后的块(按配置顺序)", promptBuild.renderedBlocks)}
${_renderDebugDetails("实际执行消息序列", promptBuild.executionMessages || promptBuild.privateTaskMessages || null)} ${_renderDebugDetails("实际执行消息序列", promptBuild.executionMessages || promptBuild.privateTaskMessages || null)}
${_renderDebugDetails("系统提示词(兼容视图)", promptBuild.systemPrompt || "")} ${_renderDebugDetails("系统提示词(兼容视图,不含 atDepth 消息", promptBuild.systemPrompt || "")}
${_renderDebugDetails("世界书桶内容(诊断)", promptBuild.hostInjections)} ${_renderDebugDetails("世界书桶内容(诊断)", promptBuild.hostInjections)}
${_renderDebugDetails("世界书块命中计划(诊断)", promptBuild.hostInjectionPlan || null)} ${_renderDebugDetails("世界书块命中计划(诊断)", promptBuild.hostInjectionPlan || null)}
${_renderDebugDetails("世界书调试", promptBuild.worldInfo?.debug || promptBuild.worldInfoResolution?.debug || null)} ${_renderDebugDetails("世界书调试", promptBuild.worldInfo?.debug || promptBuild.worldInfoResolution?.debug || null)}

View File

@@ -969,6 +969,13 @@ function getBlockDiagnosticInjectionPosition(block = {}) {
} }
function profileRequiresWorldInfo(profile) { function profileRequiresWorldInfo(profile) {
if (
profile?.worldInfo === false ||
profile?.metadata?.disableWorldInfo === true
) {
return false;
}
const blocks = Array.isArray(profile?.blocks) ? profile.blocks : []; const blocks = Array.isArray(profile?.blocks) ? profile.blocks : [];
for (const block of blocks) { for (const block of blocks) {
if (!block || block.enabled === false) continue; if (!block || block.enabled === false) continue;
@@ -990,7 +997,10 @@ function profileRequiresWorldInfo(profile) {
return true; return true;
} }
} }
return false;
// atDepth world info is implicit in the final message chain, so profiles
// without explicit before/after placeholders should still resolve lore.
return blocks.some((block) => block && block.enabled !== false);
} }
function extractWorldInfoChatMessages(context = {}) { function extractWorldInfoChatMessages(context = {}) {

View File

@@ -596,32 +596,27 @@ function normalizeRegexStageKey(stageKey = "") {
export function normalizeTaskRegexStages(stages = {}) { export function normalizeTaskRegexStages(stages = {}) {
const source = const source =
stages && typeof stages === "object" && !Array.isArray(stages) ? stages : {}; stages && typeof stages === "object" && !Array.isArray(stages) ? stages : {};
const normalized = { ...source }; const normalized = {};
for (const [key, value] of Object.entries(source)) {
if (Object.prototype.hasOwnProperty.call(TASK_REGEX_STAGE_ALIAS_MAP, key)) {
continue;
}
normalized[key] = Boolean(value);
}
for (const [legacyKey, canonicalKey] of Object.entries( for (const [legacyKey, canonicalKey] of Object.entries(
TASK_REGEX_STAGE_ALIAS_MAP, TASK_REGEX_STAGE_ALIAS_MAP,
)) { )) {
if ( if (Object.prototype.hasOwnProperty.call(source, legacyKey)) {
!Object.prototype.hasOwnProperty.call(normalized, canonicalKey) && // Older exports may carry both legacy and canonical keys at the same
Object.prototype.hasOwnProperty.call(normalized, legacyKey) // time. When that happens, keep the legacy intent instead of letting a
) { // newer placeholder default silently flip stage timing.
normalized[canonicalKey] = Boolean(normalized[legacyKey]); normalized[canonicalKey] = Boolean(source[legacyKey]);
}
delete normalized[legacyKey];
}
for (const [groupKey, stageKeys] of Object.entries(TASK_REGEX_STAGE_GROUPS)) {
if (normalized[groupKey] === false) {
continue; continue;
} }
const allSpecificStagesFalse = if (Object.prototype.hasOwnProperty.call(source, canonicalKey)) {
stageKeys.length > 0 && normalized[canonicalKey] = Boolean(source[canonicalKey]);
stageKeys.every((stageKey) => normalized[stageKey] === false);
if (!allSpecificStagesFalse) {
continue;
}
for (const stageKey of stageKeys) {
delete normalized[stageKey];
} }
} }

View File

@@ -153,6 +153,65 @@ try {
const { applyTaskRegex, inspectTaskRegexReuse } = await import( const { applyTaskRegex, inspectTaskRegexReuse } = await import(
"../task-regex.js" "../task-regex.js"
); );
const {
createDefaultTaskProfiles,
isTaskRegexStageEnabled,
normalizeTaskRegexStages,
} = await import("../prompt-profiles.js");
const normalizedLegacyStages = normalizeTaskRegexStages({
finalPrompt: true,
"input.userMessage": false,
"input.recentMessages": false,
"input.candidateText": false,
"input.finalPrompt": false,
rawResponse: false,
beforeParse: false,
"output.rawResponse": false,
"output.beforeParse": false,
});
assert.equal(normalizedLegacyStages["input.finalPrompt"], true);
assert.equal(normalizedLegacyStages["input.userMessage"], false);
assert.equal(normalizedLegacyStages["input.recentMessages"], false);
assert.equal(normalizedLegacyStages["input.candidateText"], false);
assert.equal(normalizedLegacyStages["output.rawResponse"], false);
assert.equal(normalizedLegacyStages["output.beforeParse"], false);
assert.equal(
isTaskRegexStageEnabled(normalizedLegacyStages, "input.finalPrompt"),
true,
);
assert.equal(
isTaskRegexStageEnabled(normalizedLegacyStages, "input.userMessage"),
false,
);
assert.equal(
isTaskRegexStageEnabled(normalizedLegacyStages, "input.recentMessages"),
false,
);
assert.equal(
isTaskRegexStageEnabled(normalizedLegacyStages, "input.candidateText"),
false,
);
const defaultProfiles = createDefaultTaskProfiles();
const defaultExtractStages =
defaultProfiles.extract?.profiles?.[0]?.regex?.stages || {};
assert.equal(
isTaskRegexStageEnabled(defaultExtractStages, "input.finalPrompt"),
true,
);
assert.equal(
isTaskRegexStageEnabled(defaultExtractStages, "input.userMessage"),
false,
);
assert.equal(
isTaskRegexStageEnabled(defaultExtractStages, "input.recentMessages"),
false,
);
assert.equal(
isTaskRegexStageEnabled(defaultExtractStages, "input.candidateText"),
false,
);
globalThis.getTavernRegexes = () => { globalThis.getTavernRegexes = () => {
throw new Error("legacy global getter should not be used in regex tests"); throw new Error("legacy global getter should not be used in regex tests");

View File

@@ -456,6 +456,56 @@ try {
assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。"); assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。");
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 0, true); assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 0, true);
const noWorldInfoBlockSettings = {
taskProfiles: {
recall: {
activeProfileId: "custom",
profiles: [
{
id: "custom",
name: "无世界书显式块",
taskType: "recall",
builtin: false,
blocks: [
{
id: "u1",
type: "custom",
content: "角色: {{charName}}",
role: "user",
enabled: true,
order: 0,
injectionMode: "append",
},
],
},
],
},
},
};
const atDepthOnlyPromptBuild = await buildTaskPrompt(
noWorldInfoBlockSettings,
"recall",
{
taskName: "recall",
userMessage: "继续调查",
recentMessages: "我们继续调查那条线索",
charName: "Alice",
},
);
assert.equal(atDepthOnlyPromptBuild.debug.worldInfoRequested, true);
assert.equal(atDepthOnlyPromptBuild.debug.worldInfoAtDepthCount, 1);
assert.equal(atDepthOnlyPromptBuild.additionalMessages.length, 1);
assert.equal(
atDepthOnlyPromptBuild.additionalMessages[0].content,
"这是一条 atDepth 消息。",
);
assert.deepEqual(
atDepthOnlyPromptBuild.executionMessages.map((message) => message.role),
["user", "system"],
);
const { initializeHostAdapter } = await import("../host-adapter/index.js"); const { initializeHostAdapter } = await import("../host-adapter/index.js");
const partialBridgeCalls = []; const partialBridgeCalls = [];
const partialBridgeEntriesByWorldbook = { const partialBridgeEntriesByWorldbook = {