Initial commit

This commit is contained in:
2026-07-10 08:31:27 +08:00
commit 2b74c30d36
134 changed files with 21801 additions and 0 deletions
+28
View File
@@ -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);
}
+96
View File
@@ -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 */
}
}
+46
View File
@@ -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;
}
+37
View File
@@ -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;
}
+53
View File
@@ -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),
};
}
+117
View File
@@ -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 */
}
}