Files
writing-agent/src/preset/versions.ts
T
chenmoranzhi f3303038fe 新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
2026-10-26 01:09:40 +08:00

151 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 */
}
}