新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。

改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
This commit is contained in:
2026-10-26 01:09:40 +08:00
parent 98e51f9c6c
commit f3303038fe
20 changed files with 6263 additions and 6 deletions
+105
View File
@@ -0,0 +1,105 @@
import { mkdtempSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { PresetPackage } from "../src/types/preset.js";
import { checkAuthoringPreset } from "../src/preset/authoring-check.js";
import {
checkoutVersion,
commitVersion,
loadVersionTree,
versionLineage,
} from "../src/preset/versions.js";
function preset(id: string, content = "正文"): PresetPackage {
return {
id,
name: id,
source: "native",
prompts: [
{ id: "sys", name: "规则", enabled: true, role: "system", content, 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(),
};
}
let dir = "";
beforeEach(() => {
dir = mkdtempSync(path.join(os.tmpdir(), "preset-versions-"));
process.env.WRITING_AGENT_DATA_DIR = dir;
});
afterEach(() => {
delete process.env.WRITING_AGENT_DATA_DIR;
rmSync(dir, { recursive: true, force: true });
});
describe("版本树", () => {
it("线性保存并可追溯", () => {
commitVersion({ presetId: "p1", preset: preset("p1", "v1") });
const tree = commitVersion({ presetId: "p1", preset: preset("p1", "v2") });
const lineage = versionLineage(tree, tree.currentId!);
expect(lineage.map((n) => n.preset.prompts[0]!.content)).toEqual(["v1", "v2"]);
});
it("从历史节点分叉,原分支保持不变", () => {
const first = commitVersion({ presetId: "p2", preset: preset("p2", "a") });
const rootId = first.currentId!;
commitVersion({ presetId: "p2", preset: preset("p2", "b") });
const forked = commitVersion({ presetId: "p2", preset: preset("p2", "c"), parentId: rootId });
expect(forked.nodes).toHaveLength(3);
const branch = forked.nodes.filter((n) => n.parentId === rootId);
expect(branch).toHaveLength(2);
expect(versionLineage(forked, forked.currentId!)[0]!.id).toBe(rootId);
});
it("不允许再建第二个根,也不允许挂到不存在的父节点", () => {
commitVersion({ presetId: "p3", preset: preset("p3") });
expect(() => commitVersion({ presetId: "p3", preset: preset("p3"), parentId: null })).toThrow();
expect(() => commitVersion({ presetId: "p3", preset: preset("p3"), parentId: "missing" })).toThrow();
});
it("checkout 只移动当前位置,不新增节点", () => {
const tree = commitVersion({ presetId: "p4", preset: preset("p4") });
const rootId = tree.currentId!;
commitVersion({ presetId: "p4", preset: preset("p4", "x") });
const moved = checkoutVersion("p4", rootId);
expect(moved.currentId).toBe(rootId);
expect(loadVersionTree("p4").nodes).toHaveLength(2);
});
it("拒绝非法预设 id(防路径穿越)", () => {
expect(() => loadVersionTree("../evil")).toThrow();
});
});
describe("结构检查", () => {
it("正常预设没有 error", () => {
const issues = checkAuthoringPreset(preset("ok"));
expect(issues.filter((i) => i.level === "error")).toHaveLength(0);
});
it("全角 {{ 给 warn 而不是 error", () => {
const issues = checkAuthoringPreset(preset("fw", "你好 {{user{{"));
expect(issues.some((i) => i.level === "warn" && i.message.includes("全角"))).toBe(true);
expect(issues.some((i) => i.level === "error")).toBe(false);
});
it("未闭合的 {{ 为 error", () => {
const issues = checkAuthoringPreset(preset("open", "{{setvar::a::1"));
expect(issues.some((i) => i.level === "error" && i.message.includes("未闭合"))).toBe(true);
});
it("未启用对话历史位置给 warn", () => {
const p = preset("nohist");
p.promptOrder = p.promptOrder.filter((o) => o.promptId !== "chatHistory");
const issues = checkAuthoringPreset(p);
expect(issues.some((i) => i.message.includes("对话历史"))).toBe(true);
});
});