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 = {}) => ({ ...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一段没有块头的读料"); }); });