直聊的种子历史改成逐条真多轮,不再靠块头文本猜轮次。

一条一个角色下拉加一整句正文,换行原样保留;旧盘里的纯文本在读盘时迁移,切得出轮次的转成条目,切不出的挪进史前设定免得悄悄丢掉。历史编辑器做成共用控件,预设工作台的用例与假历史也用同一套。
This commit is contained in:
2026-10-26 01:09:34 +08:00
parent 860eff59b2
commit 98e51f9c6c
11 changed files with 686 additions and 17 deletions
+127
View File
@@ -0,0 +1,127 @@
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import { importSillyTavernPreset } from "../src/preset/importer.js";
import {
EMPTY_SHELL_SLOTS,
assembleDirectChatMessages,
createShell,
getDirectChatsDir,
getShell,
worldInfoPackFromDirectChat,
} from "../src/direct-chat/index.js";
function samplePreset() {
return importSillyTavernPreset({
prompts: [
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
{ identifier: "chatHistory", name: "History", role: "system", marker: true },
],
prompt_order: [
{
character_id: 1,
order: [
{ identifier: "main", enabled: true },
{ identifier: "chatHistory", enabled: true },
],
},
],
}).preset;
}
const slots = (extra: Record<string, unknown> = {}) => ({
...EMPTY_SHELL_SLOTS,
...extra,
});
describe("直聊种子历史:结构化真多轮", () => {
beforeEach(() => {
process.env.WRITING_AGENT_DATA_DIR = mkdtempSync(
path.join(tmpdir(), "wa-seed-history-"),
);
});
it("seedMessages 每条一个 message,排在已有轮次之前", () => {
const messages = assembleDirectChatMessages(samplePreset(), {
message: "继续",
slots: slots({
seedMessages: [
{ role: "user", content: "前情-问" },
{ role: "assistant", content: "前情-答" },
],
}),
priorMessages: [{ role: "user", content: "真-问" }],
});
const contents = messages.map((m) => String(m.content));
const at = (text: string) => contents.findIndex((c) => c.includes(text));
expect(at("前情-问")).toBeGreaterThanOrEqual(0);
expect(at("前情-问")).toBeLessThan(at("前情-答"));
expect(at("前情-答")).toBeLessThan(at("真-问"));
expect(at("真-问")).toBeLessThan(at("继续"));
// 真轮次,不是塞进 system 的 few-shot
expect(messages.some((m) => m.name)).toBe(false);
});
it("seedMessages 优先于旧版 seedHistory 文本", () => {
const pack = worldInfoPackFromDirectChat({
message: "继续",
slots: slots({
seedHistory: "## 用户\n\n旧文本",
seedMessages: [{ role: "assistant", content: "新结构" }],
}),
priorMessages: [],
});
expect(pack.history).toEqual([{ role: "assistant", content: "新结构" }]);
});
it("脏条目丢掉,全空时回落旧文本", () => {
const pack = worldInfoPackFromDirectChat({
message: "继续",
slots: slots({
seedHistory: "## 用户\n\n旧文本",
seedMessages: [
{ role: "system", content: "不是历史角色" },
{ role: "user", content: " " },
],
}),
priorMessages: [],
});
expect(pack.history).toEqual([{ role: "user", content: "旧文本" }]);
});
it("读旧盘时把种子历史文本迁成结构化条目并落盘", () => {
const shell = createShell({ name: "旧壳", slots: slots() });
const file = path.join(getDirectChatsDir(), "shells", `${shell.id}.json`);
writeFileSync(
file,
JSON.stringify({
...shell,
seedHistory: "【用户】\n我推开门\n\n【助手】\n屋里没有人。\n\n【系统】\n旁白一句",
}),
"utf8",
);
const loaded = getShell(shell.id);
expect(loaded?.seedHistory).toBe("");
expect(loaded?.seedMessages).toEqual([
{ role: "user", content: "我推开门" },
{ role: "assistant", content: "屋里没有人。" },
{ role: "assistant", content: "旁白一句" },
]);
// 迁移要写回盘,下次读到的就是新形状
const onDisk = JSON.parse(readFileSync(file, "utf8")) as { seedMessages?: unknown };
expect(Array.isArray(onDisk.seedMessages)).toBe(true);
});
it("切不出轮次的旧文本挪进前情,避免变成看不见的发送内容", () => {
const shell = createShell({ name: "读料壳", slots: slots({ loreBefore: "设定" }) });
const file = path.join(getDirectChatsDir(), "shells", `${shell.id}.json`);
writeFileSync(file, JSON.stringify({ ...shell, seedHistory: "一段没有块头的读料" }), "utf8");
const loaded = getShell(shell.id);
expect(loaded?.seedHistory).toBe("");
expect(loaded?.seedMessages).toEqual([]);
expect(loaded?.loreBefore).toBe("设定\n\n一段没有块头的读料");
});
});