新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { Readable } from "node:stream";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { savePreset } from "../src/preset/store.js";
|
||||
import { handlePresetLabApi } from "../src/server/preset-lab-handlers.js";
|
||||
import type { PresetPackage } from "../src/types/preset.js";
|
||||
|
||||
function preset(id = "lab"): PresetPackage {
|
||||
return {
|
||||
id,
|
||||
name: "工作台预设",
|
||||
source: "native",
|
||||
prompts: [
|
||||
{ id: "sys", name: "规则", enabled: true, role: "system", content: "\u4f60\u662f{{char}}", marker: false, sourceIdentifier: "sys" },
|
||||
{ id: "chatHistory", name: "历史", enabled: true, role: "system", content: "", marker: true, sourceIdentifier: "chatHistory" },
|
||||
],
|
||||
promptOrder: [
|
||||
{ promptId: "sys", enabled: true, orderIndex: 0 },
|
||||
{ promptId: "chatHistory", enabled: true, orderIndex: 1 },
|
||||
],
|
||||
generation: {},
|
||||
unsupported: [],
|
||||
importedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
type Captured = { status: number; body: any };
|
||||
|
||||
async function call(
|
||||
pathname: string,
|
||||
init: { method?: string; body?: unknown; query?: string } = {},
|
||||
): Promise<Captured> {
|
||||
const raw = init.body === undefined ? "" : JSON.stringify(init.body);
|
||||
const req = Readable.from(raw ? [Buffer.from(raw, "utf8")] : []) as unknown as IncomingMessage;
|
||||
(req as any).method = init.method ?? "GET";
|
||||
let status = 0;
|
||||
let payload = "";
|
||||
const res = {
|
||||
writeHead(code: number) {
|
||||
status = code;
|
||||
},
|
||||
end(chunk: string) {
|
||||
payload = chunk ?? "";
|
||||
},
|
||||
} as unknown as ServerResponse;
|
||||
const handled = await handlePresetLabApi(req, res, pathname, new URLSearchParams(init.query ?? ""));
|
||||
expect(handled).toBe(true);
|
||||
return { status, body: payload ? JSON.parse(payload) : null };
|
||||
}
|
||||
|
||||
let dir = "";
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(path.join(os.tmpdir(), "preset-lab-api-"));
|
||||
process.env.WRITING_AGENT_DATA_DIR = dir;
|
||||
savePreset(preset());
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.WRITING_AGENT_DATA_DIR;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("预设工作台 API", () => {
|
||||
it("非本模块路径不接管", async () => {
|
||||
const req = Readable.from([]) as unknown as IncomingMessage;
|
||||
(req as any).method = "GET";
|
||||
const res = { writeHead() {}, end() {} } as unknown as ServerResponse;
|
||||
expect(await handlePresetLabApi(req, res, "/api/settings/x", new URLSearchParams())).toBe(false);
|
||||
});
|
||||
|
||||
it("列出预设", async () => {
|
||||
const { status, body } = await call("/api/preset-lab/presets");
|
||||
expect(status).toBe(200);
|
||||
expect(body.presets.some((p: any) => p.id === "lab")).toBe(true);
|
||||
});
|
||||
|
||||
it("读取单个预设与结构检查", async () => {
|
||||
const one = await call("/api/preset-lab/preset", { query: "presetId=lab" });
|
||||
expect(one.body.preset.id).toBe("lab");
|
||||
const checked = await call("/api/preset-lab/check", { method: "POST", body: { preset: one.body.preset } });
|
||||
expect(checked.body.issues.filter((i: any) => i.level === "error")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("提交版本后能读回版本树", async () => {
|
||||
const committed = await call("/api/preset-lab/versions/commit", {
|
||||
method: "POST",
|
||||
body: { preset: preset(), note: "第一次" },
|
||||
});
|
||||
expect(committed.body.nodes).toHaveLength(1);
|
||||
const tree = await call("/api/preset-lab/versions", { query: "presetId=lab" });
|
||||
expect(tree.body.currentId).toBe(committed.body.nodes[0].id);
|
||||
});
|
||||
|
||||
it("保存至当前版本只覆盖节点,不新增", async () => {
|
||||
const first = await call("/api/preset-lab/versions/commit", {
|
||||
method: "POST",
|
||||
body: { preset: preset(), note: "建根" },
|
||||
});
|
||||
const nodeId = first.body.currentId;
|
||||
const edited = { ...preset(), name: "改过的名字" };
|
||||
const amended = await call("/api/preset-lab/versions/amend", {
|
||||
method: "POST",
|
||||
body: { preset: edited, note: "就地改" },
|
||||
});
|
||||
expect(amended.body.nodes).toHaveLength(1);
|
||||
expect(amended.body.currentId).toBe(nodeId);
|
||||
const node = amended.body.nodes[0];
|
||||
expect(node.id).toBe(nodeId);
|
||||
expect(node.parentId).toBe(null);
|
||||
expect(node.preset.name).toBe("改过的名字");
|
||||
expect(node.note).toBe("就地改");
|
||||
expect(typeof node.updatedAt).toBe("string");
|
||||
});
|
||||
|
||||
it("预览按用例组装消息,{{char}} 被替换", async () => {
|
||||
const { status, body } = await call("/api/preset-lab/preview", {
|
||||
method: "POST",
|
||||
body: {
|
||||
preset: preset(),
|
||||
testCase: { id: "c", name: "c", charName: "林", charDescription: "", worldBook: "", userInput: "开门", history: [] },
|
||||
},
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
const text = body.messages.map((m: any) => m.content).join("\n");
|
||||
expect(text).toContain("你是林");
|
||||
expect(text).toContain("开门");
|
||||
});
|
||||
|
||||
it("测试组保存与列出", async () => {
|
||||
const saved = await call("/api/preset-lab/groups/save", {
|
||||
method: "POST",
|
||||
body: { presetId: "lab", name: "组一", cases: [{ name: "a", userInput: "hi" }] },
|
||||
});
|
||||
expect(saved.body.cases[0].id).toBeTruthy();
|
||||
const groups = await call("/api/preset-lab/groups", { query: "presetId=lab" });
|
||||
expect(groups.body.groups).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("缺少 preset 时返回 400 而不是崩溃", async () => {
|
||||
const { status, body } = await call("/api/preset-lab/check", { method: "POST", body: {} });
|
||||
expect(status).toBe(400);
|
||||
expect(body.error).toContain("preset");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user