feat: hide assistant card names in extraction transcript, split sectioned recentMessages into 2 system messages

This commit is contained in:
Youzini-afk
2026-04-12 13:33:39 +08:00
parent 433e62e084
commit f40b03c306
8 changed files with 232 additions and 46 deletions

View File

@@ -76,9 +76,23 @@ const chat = [
{
const mixed = [
{ seq: 1, role: "user", content: "context user", speaker: "A", isContextOnly: true },
{ seq: 2, role: "assistant", content: "context ai", speaker: "B", isContextOnly: true },
{
seq: 2,
role: "assistant",
content: "context ai",
speaker: "B",
hideSpeakerLabel: true,
isContextOnly: true,
},
{ seq: 3, role: "user", content: "target user", speaker: "A", isContextOnly: false },
{ seq: 4, role: "assistant", content: "target ai", speaker: "B", isContextOnly: false },
{
seq: 4,
role: "assistant",
content: "target ai",
speaker: "B",
hideSpeakerLabel: true,
isContextOnly: false,
},
];
const transcript = formatExtractionTranscript(mixed);
assert.match(transcript, /已提取过/, "transcript should contain context review header");
@@ -89,6 +103,8 @@ const chat = [
);
assert.match(transcript, /#1.*context user/, "context message should appear");
assert.match(transcript, /#3.*target user/, "target message should appear");
assert.match(transcript, /#2 \[assistant\]: context ai/, "assistant card name should be hidden");
assert.doesNotMatch(transcript, /#2 \[assistant\|B\]:/, "assistant card name should not be rendered");
console.log(" ✓ formatExtractionTranscript: section dividers for mixed context/target");
}
@@ -132,8 +148,20 @@ const chat = [
const targetFiltered = result.filteredMessages.filter((m) => !m.isContextOnly);
assert.equal(contextFiltered.length, 2, "context messages propagated through filtering");
assert.equal(targetFiltered.length, 2, "target messages propagated through filtering");
assert.equal(
result.filteredMessages.find((m) => m.seq === 2)?.hideSpeakerLabel,
true,
"active character assistant label should be hidden",
);
assert.equal(
result.filteredMessages.find((m) => m.seq === 1)?.hideSpeakerLabel,
false,
"user label should remain visible",
);
assert.match(result.filteredTranscript, /已提取过/, "transcript includes context header");
assert.match(result.filteredTranscript, /本次需要提取/, "transcript includes target header");
assert.match(result.filteredTranscript, /#2 \[assistant\]: old answer/, "assistant transcript should hide character name");
assert.doesNotMatch(result.filteredTranscript, /#2 \[assistant\|B\]:/, "assistant transcript should not show character name");
console.log(" ✓ buildExtractionInputContext: isContextOnly propagated to filteredMessages and transcript");
}

View File

@@ -141,8 +141,9 @@ try {
(message) => message.sourceKey === "recentMessages",
);
assert.ok(recentBlock);
assert.match(String(recentBlock?.content || ""), /#10 \[assistant\|艾琳\]: 继续说明/);
assert.match(String(recentBlock?.content || ""), /#10 \[assistant\]: 继续说明/);
assert.match(String(recentBlock?.content || ""), /#11 \[user\|玩家\]: 用户输入/);
assert.doesNotMatch(String(recentBlock?.content || ""), /#10 \[assistant\|艾琳\]:/);
assert.doesNotMatch(String(recentBlock?.content || ""), /隐式思维|<think>/);
} finally {
restore();

View File

@@ -224,16 +224,25 @@ function collectAllPromptContent(captured) {
assert.equal(result.success, true);
assert.ok(captured);
const recentBlock = (Array.isArray(captured.promptMessages) ? captured.promptMessages : []).find(
const recentMessages = (Array.isArray(captured.promptMessages)
? captured.promptMessages
: []
).filter(
(m) => m.sourceKey === "recentMessages",
);
assert.ok(recentBlock, "recentMessages block should exist");
const recentContent = String(recentBlock?.content || "");
assert.match(recentContent, /以下是上下文回顾(已提取过),仅供理解剧情/);
assert.match(recentContent, /以下是本次需要提取记忆的新对话内容/);
assert.equal(recentMessages.length, 2, "recentMessages should split into 2 section system messages");
assert.equal(recentMessages[0]?.role, "system");
assert.equal(recentMessages[0]?.transcriptSection, "context");
assert.match(String(recentMessages[0]?.content || ""), /^--- 以下是上下文回顾(已提取过),仅供理解剧情 ---/);
assert.match(String(recentMessages[0]?.content || ""), /#10 \[user\|玩家\]: 第一轮消息/);
assert.equal(recentMessages[1]?.role, "system");
assert.equal(recentMessages[1]?.transcriptSection, "target");
assert.match(String(recentMessages[1]?.content || ""), /^--- 以下是本次需要提取记忆的新对话内容 ---/);
assert.match(String(recentMessages[1]?.content || ""), /#12 \[user\|玩家\]: 第二轮消息/);
assert.ok(
recentContent.indexOf("已提取过") < recentContent.indexOf("本次需要提取"),
"context review should appear before extraction target section",
recentMessages[0].content.includes("提取") &&
recentMessages[1].content.includes("本次需要提取"),
"context and target sections should each be emitted as a single system message",
);
} finally {
restore();

View File

@@ -286,7 +286,7 @@ try {
).find((message) => message.sourceKey === "recentMessages");
assert.ok(recentBlock, "recentMessages block should exist");
const recentContent = String(recentBlock?.content || "");
assert.match(recentContent, /#30 \[assistant\|艾琳\]: 艾琳说:去调查蓝钥匙。/);
assert.match(recentContent, /#30 \[assistant\]: 艾琳说:去调查蓝钥匙。/);
assert.match(
recentContent,
/#31 \[assistant\|旁白\]: 旁白补充:<status mood='tense'>雨夜<\/status>巷子很安静。/,
@@ -351,7 +351,7 @@ try {
: []
).find((message) => message.sourceKey === "recentMessages");
assert.ok(recentBlock, "recentMessages block should still exist when worldbook is disabled");
assert.match(String(recentBlock?.content || ""), /#30 \[assistant\|艾琳\]: 艾琳说:去调查蓝钥匙。/);
assert.match(String(recentBlock?.content || ""), /#30 \[assistant\]: 艾琳说:去调查蓝钥匙。/);
} finally {
restore();
}

View File

@@ -117,6 +117,7 @@ const promptBuild = await buildTaskPrompt(settings, "extract", {
content: "继续说明",
name: "艾琳",
speaker: "艾琳",
hideSpeakerLabel: true,
isContextOnly: true,
},
{
@@ -133,25 +134,41 @@ const promptBuild = await buildTaskPrompt(settings, "extract", {
currentRange: "41 ~ 42",
});
const payload = buildTaskLlmPayload(promptBuild, "fallback-user");
const recentBlock = payload.promptMessages.find(
const recentMessages = payload.promptMessages.filter(
(message) => message.sourceKey === "recentMessages",
);
assert.match(
String(recentBlock?.content || ""),
/以下是上下文回顾(已提取过),仅供理解剧情/,
assert.deepEqual(
recentMessages.map((message) => ({
role: message.role,
sourceKey: message.sourceKey,
transcriptSection: message.transcriptSection,
transcriptSectionPart: message.transcriptSectionPart,
})),
[
{
role: "system",
sourceKey: "recentMessages",
transcriptSection: "context",
transcriptSectionPart: "section",
},
{
role: "system",
sourceKey: "recentMessages",
transcriptSection: "target",
transcriptSectionPart: "section",
},
],
);
assert.match(
String(recentBlock?.content || ""),
/以下是本次需要提取记忆的新对话内容/,
);
assert.match(String(recentBlock?.content || ""), /#41 \[assistant\|艾琳\]: 助手已净化/);
assert.match(String(recentBlock?.content || ""), /#42 \[user\|玩家\]: 用户已净化/);
assert.match(String(recentMessages[0]?.content || ""), /^--- 以下是上下文回顾(已提取过),仅供理解剧情 ---/);
assert.match(String(recentMessages[0]?.content || ""), /#41 \[assistant\]: 助手已净化/);
assert.match(String(recentMessages[1]?.content || ""), /^--- 以下是本次需要提取记忆的新对话内容 ---/);
assert.match(String(recentMessages[1]?.content || ""), /#42 \[user\|玩家\]: 用户已净化/);
assert.doesNotMatch(
String(recentBlock?.content || ""),
/#41 \[assistant\|艾琳\]: 用户已净化/,
String(recentMessages[0]?.content || ""),
/#41 \[assistant\|艾琳\]:/,
);
assert.doesNotMatch(
String(recentBlock?.content || ""),
String(recentMessages[1]?.content || ""),
/#42 \[user\|玩家\]: 助手已净化/,
);