增加与作品对话并列的直聊,多模型按列对照并可选定基准。

服务启动时顺带拉取开放路由价目,用户目录同时预留生图工作流。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 08:30:04 +08:00
co-authored by Cursor
parent e7a4b6c954
commit 749eb2f5b0
15 changed files with 3854 additions and 1 deletions
+232
View File
@@ -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" },
]);
});
});