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

改预设要能一边写一边看装出来的 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
+150
View File
@@ -0,0 +1,150 @@
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { getPresetsDir } from "../config/user-data-dir.js";
import type { PresetPackage } from "../types/preset.js";
/**
* 预设版本树:每个预设一个 JSON 文件,节点是完整快照。
* - 保存:在指定父节点(缺省为当前节点)下追加子节点;
* - 分叉:父节点选历史中的任意一个,形成新分支,原分支不动;
* - 追溯:沿 parentId 往上走即为修改历史。
* 不依赖 git,不做远程同步。
*/
export type PresetVersionNode = {
id: string;
parentId: string | null;
createdAt: string;
/** 被「保存至当前版本」覆盖过的时间;没改过就不写 */
updatedAt?: string;
note: string;
preset: PresetPackage;
};
export type PresetVersionTree = {
presetId: string;
currentId: string | null;
nodes: PresetVersionNode[];
};
const SAFE_ID = /^[\w\u4e00-\u9fff-]+$/;
function versionsDir(): string {
return path.join(getPresetsDir(), "versions");
}
function treeFile(presetId: string): string {
if (!SAFE_ID.test(presetId)) throw new Error(`非法预设 id:${presetId}`);
return path.join(versionsDir(), `${presetId}.json`);
}
function writeTree(tree: PresetVersionTree): void {
mkdirSync(versionsDir(), { recursive: true });
writeFileSync(treeFile(tree.presetId), JSON.stringify(tree, null, 2), "utf8");
}
export function loadVersionTree(presetId: string): PresetVersionTree {
const file = treeFile(presetId);
if (!existsSync(file)) return { presetId, currentId: null, nodes: [] };
return JSON.parse(readFileSync(file, "utf8")) as PresetVersionTree;
}
export function getVersionNode(
presetId: string,
nodeId: string,
): PresetVersionNode | null {
return loadVersionTree(presetId).nodes.find((n) => n.id === nodeId) ?? null;
}
/** 从根到指定节点的路径(含两端),即该版本的修改历史。 */
export function versionLineage(
tree: PresetVersionTree,
nodeId: string,
): PresetVersionNode[] {
const byId = new Map(tree.nodes.map((n) => [n.id, n]));
const out: PresetVersionNode[] = [];
let cursor = byId.get(nodeId) ?? null;
const guard = new Set<string>();
while (cursor && !guard.has(cursor.id)) {
guard.add(cursor.id);
out.unshift(cursor);
cursor = cursor.parentId ? byId.get(cursor.parentId) ?? null : null;
}
return out;
}
/**
* 提交一个版本。
* parentId 缺省为当前节点;显式传入历史节点即为分叉。
* 树为空时 parentId 必须为 null(建根);已有根时不允许再建第二个根。
*/
export function commitVersion(input: {
presetId: string;
preset: PresetPackage;
note?: string;
parentId?: string | null;
}): PresetVersionTree {
const tree = loadVersionTree(input.presetId);
const parentId =
input.parentId !== undefined ? input.parentId : tree.currentId;
if (parentId === null) {
if (tree.nodes.length > 0) throw new Error("已有版本,不能再新建根版本");
} else if (!tree.nodes.some((n) => n.id === parentId)) {
throw new Error(`父版本不存在:${parentId}`);
}
let id = randomUUID().slice(0, 8);
while (tree.nodes.some((n) => n.id === id)) id = randomUUID().slice(0, 8);
const node: PresetVersionNode = {
id,
parentId,
createdAt: new Date().toISOString(),
note: (input.note ?? "").trim(),
preset: structuredClone(input.preset),
};
tree.nodes.push(node);
tree.currentId = node.id;
writeTree(tree);
return tree;
}
/**
* 保存到当前版本(amend):把快照直接换掉 `currentId` 那个节点,不产生新节点。
* 还没有任何版本时退化成建根。
*/
export function amendCurrentVersion(input: {
presetId: string;
preset: PresetPackage;
note?: string;
}): PresetVersionTree {
const tree = loadVersionTree(input.presetId);
if (!tree.currentId) return commitVersion({ ...input, parentId: null });
const node = tree.nodes.find((item) => item.id === tree.currentId);
if (!node) throw new Error(`当前版本不存在:${tree.currentId}`);
node.preset = structuredClone(input.preset);
const note = (input.note ?? "").trim();
if (note) node.note = note;
node.updatedAt = new Date().toISOString();
writeTree(tree);
return tree;
}
/** 切换当前编辑位置(不产生新节点)。 */
export function checkoutVersion(presetId: string, nodeId: string): PresetVersionTree {
const tree = loadVersionTree(presetId);
if (!tree.nodes.some((n) => n.id === nodeId)) {
throw new Error(`版本不存在:${nodeId}`);
}
tree.currentId = nodeId;
writeTree(tree);
return tree;
}
export function deleteVersionTree(presetId: string): void {
try {
rmSync(treeFile(presetId), { force: true });
} catch {
/* ignore */
}
}