增加与作品对话并列的直聊,多模型按列对照并可选定基准。
服务启动时顺带拉取开放路由价目,用户目录同时预留生图工作流。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import { mkdtempSync } 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 {
|
||||
appendTurnMessages,
|
||||
assembleDirectChatMessages,
|
||||
buildChatHistory,
|
||||
buildDirectChatTaskMessages,
|
||||
buildPriorForWindow,
|
||||
createConversation,
|
||||
createShell,
|
||||
DEFAULT_DIRECT_CHAT_SYSTEM,
|
||||
EMPTY_SHELL_SLOTS,
|
||||
getConversation,
|
||||
getShell,
|
||||
listConversations,
|
||||
listShells,
|
||||
promoteTemporary,
|
||||
resolveDirectChatPreset,
|
||||
runDirectChatTurn,
|
||||
} from "../src/direct-chat/index.js";
|
||||
|
||||
function samplePreset() {
|
||||
return importSillyTavernPreset({
|
||||
prompts: [
|
||||
{ identifier: "main", name: "Main", role: "system", content: "MAIN" },
|
||||
{
|
||||
identifier: "charDescription",
|
||||
name: "Desc",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{
|
||||
identifier: "chatHistory",
|
||||
name: "History",
|
||||
role: "system",
|
||||
marker: true,
|
||||
},
|
||||
{ identifier: "jailbreak", name: "PHI", role: "system", content: "PHI" },
|
||||
],
|
||||
prompt_order: [
|
||||
{
|
||||
character_id: 1,
|
||||
order: [
|
||||
{ identifier: "main", enabled: true },
|
||||
{ identifier: "charDescription", enabled: true },
|
||||
{ identifier: "chatHistory", enabled: true },
|
||||
{ identifier: "jailbreak", enabled: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}).preset;
|
||||
}
|
||||
|
||||
describe("direct-chat history assembly", () => {
|
||||
it("joins seed history with prior turns", () => {
|
||||
expect(
|
||||
buildChatHistory("种子", [
|
||||
{ role: "user", content: "你好" },
|
||||
{ role: "assistant", content: "嗨" },
|
||||
]),
|
||||
).toBe("种子\n\n【用户】\n你好\n\n【助手】\n嗨");
|
||||
});
|
||||
|
||||
it("fills sandwich with seed + transcript + turn", () => {
|
||||
const messages = assembleDirectChatMessages(samplePreset(), {
|
||||
message: "继续",
|
||||
slots: {
|
||||
...EMPTY_SHELL_SLOTS,
|
||||
systemPrompt: "契约",
|
||||
loreBefore: "设定A",
|
||||
seedHistory: "旧史",
|
||||
},
|
||||
priorMessages: [{ role: "user", content: "上一句" }],
|
||||
});
|
||||
const contents = messages.map((m) => m.content);
|
||||
expect(contents.some((c) => String(c).includes("设定A"))).toBe(true);
|
||||
expect(contents.some((c) => String(c).includes("旧史"))).toBe(true);
|
||||
expect(contents.some((c) => String(c).includes("上一句"))).toBe(true);
|
||||
expect(contents).toContain("继续");
|
||||
expect(contents.some((c) => String(c).includes("契约"))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses default system prompt when slot empty", () => {
|
||||
const task = buildDirectChatTaskMessages({
|
||||
message: "hi",
|
||||
slots: EMPTY_SHELL_SLOTS,
|
||||
priorMessages: [],
|
||||
});
|
||||
expect(task[0]?.content).toBe(DEFAULT_DIRECT_CHAT_SYSTEM);
|
||||
});
|
||||
});
|
||||
|
||||
describe("direct-chat store", () => {
|
||||
beforeEach(() => {
|
||||
process.env.WRITING_AGENT_DATA_DIR = mkdtempSync(
|
||||
path.join(tmpdir(), "wa-direct-chat-"),
|
||||
);
|
||||
});
|
||||
|
||||
it("roundtrips shell and conversation", () => {
|
||||
const shell = createShell({
|
||||
name: "测试壳",
|
||||
slots: { ...EMPTY_SHELL_SLOTS, loreBefore: "世界" },
|
||||
});
|
||||
expect(getShell(shell.id)?.loreBefore).toBe("世界");
|
||||
const conv = createConversation({
|
||||
shellId: shell.id,
|
||||
name: "会话1",
|
||||
messages: [
|
||||
{
|
||||
id: "m1",
|
||||
role: "user",
|
||||
content: "你好",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(listShells()[0]?.conversationCount).toBe(1);
|
||||
expect(listConversations(shell.id)).toHaveLength(1);
|
||||
expect(getConversation(conv.id)?.messages[0]?.content).toBe("你好");
|
||||
});
|
||||
|
||||
it("promotes temporary messages into persistent shell+conversation", () => {
|
||||
const { shell, conversation } = promoteTemporary({
|
||||
shell: { name: "从临时", slots: { loreBefore: "L" } },
|
||||
conversationName: "升格会话",
|
||||
messages: [
|
||||
{
|
||||
id: "u1",
|
||||
role: "user",
|
||||
content: "问",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
role: "assistant",
|
||||
content: "答",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(shell.name).toBe("从临时");
|
||||
expect(shell.loreBefore).toBe("L");
|
||||
expect(conversation.name).toBe("升格会话");
|
||||
expect(conversation.messages).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("direct-chat turn gating", () => {
|
||||
it("rejects when no preset is available", async () => {
|
||||
process.env.WRITING_AGENT_DATA_DIR = mkdtempSync(
|
||||
path.join(tmpdir(), "wa-direct-chat-preset-"),
|
||||
);
|
||||
await expect(
|
||||
runDirectChatTurn({
|
||||
message: "你好",
|
||||
slots: EMPTY_SHELL_SLOTS,
|
||||
priorMessages: [],
|
||||
presetId: null,
|
||||
complete: false,
|
||||
}),
|
||||
).rejects.toThrow(/预设/);
|
||||
});
|
||||
|
||||
it("appends user/assistant and keeps contextTrace on latest", () => {
|
||||
const messages = appendTurnMessages({
|
||||
prior: [],
|
||||
userText: "问",
|
||||
reply: "答",
|
||||
contextTrace: {
|
||||
caller: "direct-chat",
|
||||
createdAt: new Date().toISOString(),
|
||||
messages: [{ role: "user", content: "x" }],
|
||||
charCount: 1,
|
||||
},
|
||||
keepTraceLatest: 3,
|
||||
});
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[1]?.contextTrace?.caller).toBe("direct-chat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDirectChatPreset", () => {
|
||||
it("throws for missing explicit preset id", () => {
|
||||
expect(() => resolveDirectChatPreset("missing-preset-xyz")).toThrow(
|
||||
/预设不存在/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPriorForWindow", () => {
|
||||
const turns = [
|
||||
{
|
||||
id: "t1",
|
||||
user: { content: "问1", createdAt: "1" },
|
||||
replies: [
|
||||
{ windowId: "a", content: "A1" },
|
||||
{ windowId: "b", content: "B1" },
|
||||
],
|
||||
createdAt: "1",
|
||||
},
|
||||
{
|
||||
id: "t2",
|
||||
user: { content: "问2", createdAt: "2" },
|
||||
replies: [
|
||||
{ windowId: "a", content: "A2" },
|
||||
{ windowId: "b", content: "B2" },
|
||||
],
|
||||
selectedWindowId: "a",
|
||||
createdAt: "2",
|
||||
},
|
||||
];
|
||||
|
||||
it("uses each window's own reply when nothing selected", () => {
|
||||
expect(buildPriorForWindow([turns[0]!], "b")).toEqual([
|
||||
{ role: "user", content: "问1" },
|
||||
{ role: "assistant", content: "B1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("forces selected reply into every window's prior", () => {
|
||||
expect(buildPriorForWindow(turns, "b")).toEqual([
|
||||
{ role: "user", content: "问1" },
|
||||
{ role: "assistant", content: "B1" },
|
||||
{ role: "user", content: "问2" },
|
||||
{ role: "assistant", content: "A2" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user