重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
co-authored by Cursor
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions
+86 -2
View File
@@ -1,11 +1,23 @@
import { randomUUID } from "node:crypto";
import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
import type { BookProject, BookSummary } from "../types/book.js";
import type { PersistedBookSession } from "../types/book-session.js";
import type { RunSnapshot } from "../types/run-snapshot.js";
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
import { deleteBookSession } from "./session-store.js";
import { deleteAllRunSnapshots } from "./run-snapshot-store.js";
const SESSION_FILENAME = "session.json";
function booksDir(): string {
return path.join(getUserDataDir(), "books");
}
@@ -14,6 +26,10 @@ function bookPath(id: string): string {
return path.join(booksDir(), `${id}.json`);
}
function bookDataDir(id: string): string {
return path.join(getUserDataDir(), "books", id);
}
export function listBooks(): BookSummary[] {
ensureUserDataDirs();
mkdirSync(booksDir(), { recursive: true });
@@ -64,7 +80,7 @@ export function createBook(input: { title?: string }): BookProject {
const book: BookProject = {
id: randomUUID(),
title: input.title?.trim() || "未命名作品",
preview: "新建作品,选择 skill 包开始…",
preview: "新建作品,描述你想创作什么…",
sessionIds: [],
createdAt: now,
updatedAt: now,
@@ -84,6 +100,8 @@ export function updateBook(
| "activeSessionId"
| "activeSkillId"
| "activeSkillName"
| "orchestratorId"
| "orchestratorName"
>
>,
): BookProject {
@@ -115,3 +133,69 @@ export function deleteBook(id: string): void {
/* ignore */
}
}
/** 复制作品及其会话、游玩存档为新作品 */
export function duplicateBook(sourceId: string, title?: string): BookProject {
const source = getBook(sourceId);
if (!source) throw new Error("Book 不存在");
const newId = randomUUID();
const newSessionId = randomUUID();
const now = new Date().toISOString();
const dupTitle = title?.trim() || `${source.title} 副本`;
ensureUserDataDirs();
const sourceDir = bookDataDir(sourceId);
const destDir = bookDataDir(newId);
if (existsSync(sourceDir)) {
cpSync(sourceDir, destDir, { recursive: true });
} else {
mkdirSync(destDir, { recursive: true });
}
const sessionFile = path.join(destDir, SESSION_FILENAME);
let sessionIds: string[] = [];
let activeSessionId: string | undefined;
if (existsSync(sessionFile)) {
try {
const snap = JSON.parse(readFileSync(sessionFile, "utf8")) as PersistedBookSession;
snap.bookId = newId;
snap.sessionId = newSessionId;
snap.runtimeSession.id = newSessionId;
snap.savedAt = now;
writeFileSync(sessionFile, JSON.stringify(snap, null, 2), "utf8");
sessionIds = [newSessionId];
activeSessionId = newSessionId;
} catch {
/* ignore broken session */
}
}
const snapDir = path.join(destDir, "run-snapshots");
if (existsSync(snapDir)) {
for (const file of readdirSync(snapDir).filter((f) => f.endsWith(".json"))) {
try {
const full = path.join(snapDir, file);
const raw = JSON.parse(readFileSync(full, "utf8")) as RunSnapshot;
raw.bookId = newId;
writeFileSync(full, JSON.stringify(raw, null, 2), "utf8");
} catch {
/* skip */
}
}
}
const newBook: BookProject = {
...structuredClone(source),
id: newId,
title: dupTitle,
sessionIds,
activeSessionId,
createdAt: now,
updatedAt: now,
};
writeFileSync(bookPath(newId), JSON.stringify(newBook, null, 2), "utf8");
return newBook;
}