服务启动时顺带拉取开放路由价目,用户目录同时预留生图工作流。 Co-authored-by: Cursor <cursoragent@cursor.com>
233 lines
6.5 KiB
TypeScript
233 lines
6.5 KiB
TypeScript
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" },
|
|
]);
|
|
});
|
|
});
|