Initial commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { listSkills } from "../skills/loader.js";
|
||||
import type { SkillPackInfo } from "../types/book.js";
|
||||
|
||||
/** @deprecated 使用 listSkillPacks;保留兼容旧 import */
|
||||
export async function listOrchestrators(): Promise<SkillPackInfo[]> {
|
||||
return listSkillPacks();
|
||||
}
|
||||
|
||||
export async function listSkillPacks(): Promise<SkillPackInfo[]> {
|
||||
const skills = await listSkills();
|
||||
return skills.map((s) => ({
|
||||
id: s.name,
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
category: s.category,
|
||||
bookKind: s.bookKind,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getSkillPack(id: string): Promise<SkillPackInfo | null> {
|
||||
const all = await listSkillPacks();
|
||||
return all.find((o) => o.id === id) ?? null;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export async function getOrchestrator(id: string): Promise<SkillPackInfo | null> {
|
||||
return getSkillPack(id);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
|
||||
import type { RunSnapshot, RunSnapshotMeta } from "../types/run-snapshot.js";
|
||||
import { toRunSnapshotMeta } from "../types/run-snapshot.js";
|
||||
|
||||
function snapshotsDir(bookId: string): string {
|
||||
return path.join(getUserDataDir(), "books", bookId, "run-snapshots");
|
||||
}
|
||||
|
||||
function snapshotPath(bookId: string, snapshotId: string): string {
|
||||
return path.join(snapshotsDir(bookId), `${snapshotId}.json`);
|
||||
}
|
||||
|
||||
function isValidRunSnapshot(value: unknown): value is RunSnapshot {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const s = value as RunSnapshot;
|
||||
const kind = s.kind ?? "run";
|
||||
return (
|
||||
s.version === 1 &&
|
||||
typeof s.id === "string" &&
|
||||
typeof s.bookId === "string" &&
|
||||
typeof s.label === "string" &&
|
||||
(kind === "instance" || kind === "run") &&
|
||||
typeof s.orchestratorId === "string" &&
|
||||
s.runtimeSession != null &&
|
||||
Array.isArray(s.blackboardItems) &&
|
||||
Array.isArray(s.messages) &&
|
||||
typeof s.createdAt === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSnapshot(raw: RunSnapshot): RunSnapshot {
|
||||
return { ...raw, kind: raw.kind ?? "run" };
|
||||
}
|
||||
|
||||
/** 保存运行快照(同 bookId + snapshotId 则覆盖) */
|
||||
export function saveRunSnapshot(snapshot: RunSnapshot): void {
|
||||
ensureUserDataDirs();
|
||||
const dir = snapshotsDir(snapshot.bookId);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(snapshotPath(snapshot.bookId, snapshot.id), JSON.stringify(snapshot, null, 2), "utf8");
|
||||
}
|
||||
|
||||
/** 读取单个运行快照;不存在或格式无效时返回 null */
|
||||
export function loadRunSnapshot(bookId: string, snapshotId: string): RunSnapshot | null {
|
||||
try {
|
||||
const raw = readFileSync(snapshotPath(bookId, snapshotId), "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!isValidRunSnapshot(parsed) || parsed.bookId !== bookId || parsed.id !== snapshotId) {
|
||||
return null;
|
||||
}
|
||||
return normalizeSnapshot(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除单个运行快照;成功删除返回 true */
|
||||
export function deleteRunSnapshot(bookId: string, snapshotId: string): boolean {
|
||||
try {
|
||||
unlinkSync(snapshotPath(bookId, snapshotId));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 列出某 Book 下全部运行快照(按 createdAt 降序) */
|
||||
export function listRunSnapshots(bookId: string): RunSnapshotMeta[] {
|
||||
ensureUserDataDirs();
|
||||
let files: string[];
|
||||
try {
|
||||
files = readdirSync(snapshotsDir(bookId)).filter((f) => f.endsWith(".json"));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metas: RunSnapshotMeta[] = [];
|
||||
for (const file of files) {
|
||||
const id = file.replace(/\.json$/, "");
|
||||
const snapshot = loadRunSnapshot(bookId, id);
|
||||
if (snapshot) metas.push(toRunSnapshotMeta(snapshot));
|
||||
}
|
||||
|
||||
return metas.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
|
||||
/** 删除某 Book 下全部运行快照(删 Book 时调用) */
|
||||
export function deleteAllRunSnapshots(bookId: string): void {
|
||||
try {
|
||||
rmSync(snapshotsDir(bookId), { recursive: true, force: true });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
|
||||
import type { PersistedBookSession } from "../types/book-session.js";
|
||||
|
||||
const SESSION_FILENAME = "session.json";
|
||||
|
||||
function bookDir(bookId: string): string {
|
||||
return path.join(getUserDataDir(), "books", bookId);
|
||||
}
|
||||
|
||||
function sessionPath(bookId: string): string {
|
||||
return path.join(bookDir(bookId), SESSION_FILENAME);
|
||||
}
|
||||
|
||||
export function saveBookSession(snapshot: PersistedBookSession): void {
|
||||
ensureUserDataDirs();
|
||||
const dir = bookDir(snapshot.bookId);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(sessionPath(snapshot.bookId), JSON.stringify(snapshot, null, 2), "utf8");
|
||||
}
|
||||
|
||||
export function loadBookSession(bookId: string): PersistedBookSession | null {
|
||||
try {
|
||||
const raw = readFileSync(sessionPath(bookId), "utf8");
|
||||
const parsed = JSON.parse(raw) as PersistedBookSession;
|
||||
if (parsed.version !== 1 || !parsed.sessionId || !parsed.runtimeSession) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteBookSession(bookId: string): void {
|
||||
try {
|
||||
rmSync(bookDir(bookId), { recursive: true, force: true });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function hasBookSession(bookId: string): boolean {
|
||||
return loadBookSession(bookId) !== null;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { BookProject } from "../types/book.js";
|
||||
import type { PersistedBookSession } from "../types/book-session.js";
|
||||
import type { RunSnapshot } from "../types/run-snapshot.js";
|
||||
import type { RuntimeSession } from "../types/runtime.js";
|
||||
import type { ActiveSkillSnapshot } from "../types/runtime.js";
|
||||
|
||||
/** 作品或快照绑定的 skill 包 id(兼容旧 orchestratorId 字段) */
|
||||
export function bookSkillPackId(book: BookProject): string | undefined {
|
||||
return book.activeSkillId ?? book.orchestratorId;
|
||||
}
|
||||
|
||||
export function sessionSkillPackId(session: RuntimeSession): string | undefined {
|
||||
const snap = session.slots.activeSkill as ActiveSkillSnapshot | undefined;
|
||||
return snap?.name;
|
||||
}
|
||||
|
||||
export function persistedSkillPackId(snapshot: PersistedBookSession): string | undefined {
|
||||
return (
|
||||
sessionSkillPackId(snapshot.runtimeSession) ?? snapshot.orchestratorId ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function runSnapshotSkillPackId(snapshot: RunSnapshot): string | undefined {
|
||||
return (
|
||||
sessionSkillPackId(snapshot.runtimeSession) ?? snapshot.orchestratorId ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function skillPacksMatch(
|
||||
book: BookProject,
|
||||
snapshotSkill: string | undefined,
|
||||
): boolean {
|
||||
const bookSkill = bookSkillPackId(book);
|
||||
if (!snapshotSkill) return true;
|
||||
if (!bookSkill) return true;
|
||||
return bookSkill === snapshotSkill;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { BlackboardItem } from "../types/blackboard.js";
|
||||
import type { RuntimeSession } from "../types/runtime.js";
|
||||
|
||||
/**
|
||||
* 判断黑板 tag 是否属于 run 阶段(非实例化确认稿)。
|
||||
* 实例快照保存/加载时会剥离这些 tag,只保留「实例化后的对象」。
|
||||
*/
|
||||
export function isRunPhaseBlackboardTag(tag: string): boolean {
|
||||
// 实例化层:保留
|
||||
if (/^角色\.[^.]+\.设定$/.test(tag)) return false;
|
||||
if (/^用户\./.test(tag)) return false;
|
||||
if (/^book\./.test(tag)) return false;
|
||||
if (/^情境\./.test(tag)) return false;
|
||||
if (/^博弈\./.test(tag)) return false;
|
||||
|
||||
// run 层:剥离
|
||||
return (
|
||||
/^运行\./.test(tag) ||
|
||||
/^世界\.(当前|裁决)/.test(tag) ||
|
||||
/^场景\.公开/.test(tag) ||
|
||||
/^输出\./.test(tag) ||
|
||||
/^角色\.[^.]+\.(可见信息|思考|行动)$/.test(tag) ||
|
||||
/^review\./.test(tag)
|
||||
);
|
||||
}
|
||||
|
||||
export function filterBlackboardForInstance(items: BlackboardItem[]): BlackboardItem[] {
|
||||
return items.filter((item) => !isRunPhaseBlackboardTag(item.tag));
|
||||
}
|
||||
|
||||
/** 读档实例时:去掉 run 产物记录,清空 pending,便于从 instanceReady 重新开跑 */
|
||||
export function prepareRuntimeSessionForInstance(session: RuntimeSession): RuntimeSession {
|
||||
const copy = structuredClone(session);
|
||||
copy.pendingArtifactId = undefined;
|
||||
copy.pendingDecision = undefined;
|
||||
copy.currentWorkerId = undefined;
|
||||
copy.waitingReason = undefined;
|
||||
copy.artifacts = copy.artifacts.filter((a) =>
|
||||
a.outputTags.every((tag) => !isRunPhaseBlackboardTag(tag)),
|
||||
);
|
||||
if (copy.phase === "done") copy.phase = "running";
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function materializeInstanceSnapshotPayload(payload: {
|
||||
runtimeSession: RuntimeSession;
|
||||
blackboardItems: BlackboardItem[];
|
||||
}): { runtimeSession: RuntimeSession; blackboardItems: BlackboardItem[] } {
|
||||
return {
|
||||
blackboardItems: filterBlackboardForInstance(payload.blackboardItems),
|
||||
runtimeSession: prepareRuntimeSessionForInstance(payload.runtimeSession),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { BookProject, BookSummary } from "../types/book.js";
|
||||
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
|
||||
import { deleteBookSession } from "./session-store.js";
|
||||
import { deleteAllRunSnapshots } from "./run-snapshot-store.js";
|
||||
|
||||
function booksDir(): string {
|
||||
return path.join(getUserDataDir(), "books");
|
||||
}
|
||||
|
||||
function bookPath(id: string): string {
|
||||
return path.join(booksDir(), `${id}.json`);
|
||||
}
|
||||
|
||||
export function listBooks(): BookSummary[] {
|
||||
ensureUserDataDirs();
|
||||
mkdirSync(booksDir(), { recursive: true });
|
||||
let files: string[];
|
||||
try {
|
||||
files = readdirSync(booksDir()).filter((f) => f.endsWith(".json"));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const books: BookProject[] = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
const raw = readFileSync(path.join(booksDir(), file), "utf8");
|
||||
books.push(JSON.parse(raw) as BookProject);
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
|
||||
return books
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.map((b) => ({
|
||||
id: b.id,
|
||||
title: b.title,
|
||||
activeSkillId: b.activeSkillId ?? b.orchestratorId,
|
||||
activeSkillName: b.activeSkillName ?? b.orchestratorName,
|
||||
preview: b.preview,
|
||||
updatedAt: b.updatedAt,
|
||||
orchestratorId: b.orchestratorId,
|
||||
orchestratorName: b.orchestratorName,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getBook(id: string): BookProject | null {
|
||||
try {
|
||||
const raw = readFileSync(bookPath(id), "utf8");
|
||||
return JSON.parse(raw) as BookProject;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBook(input: { title?: string }): BookProject {
|
||||
ensureUserDataDirs();
|
||||
mkdirSync(booksDir(), { recursive: true });
|
||||
const now = new Date().toISOString();
|
||||
const book: BookProject = {
|
||||
id: randomUUID(),
|
||||
title: input.title?.trim() || "未命名作品",
|
||||
preview: "新建作品,选择 skill 包开始…",
|
||||
sessionIds: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
writeFileSync(bookPath(book.id), JSON.stringify(book, null, 2), "utf8");
|
||||
return book;
|
||||
}
|
||||
|
||||
export function updateBook(
|
||||
id: string,
|
||||
patch: Partial<
|
||||
Pick<
|
||||
BookProject,
|
||||
| "title"
|
||||
| "preview"
|
||||
| "sessionIds"
|
||||
| "activeSessionId"
|
||||
| "activeSkillId"
|
||||
| "activeSkillName"
|
||||
>
|
||||
>,
|
||||
): BookProject {
|
||||
const book = getBook(id);
|
||||
if (!book) throw new Error("Book 不存在");
|
||||
const updated: BookProject = {
|
||||
...book,
|
||||
...patch,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(bookPath(id), JSON.stringify(updated, null, 2), "utf8");
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function appendBookSession(id: string, sessionId: string): void {
|
||||
const book = getBook(id);
|
||||
if (!book) return;
|
||||
if (!book.sessionIds.includes(sessionId)) {
|
||||
updateBook(id, { sessionIds: [...book.sessionIds, sessionId] });
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteBook(id: string): void {
|
||||
deleteBookSession(id);
|
||||
deleteAllRunSnapshots(id);
|
||||
try {
|
||||
unlinkSync(bookPath(id));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user