新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
This commit is contained in:
@@ -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 */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user