Keep creation as a living DAG and archive playable products separately.

Opening accept no longer locks the graph; session.json stays creation-only while play starts from an instance snapshot. Sidebar products nest run saves, and play reading fills the feed at a denser type size.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 00:52:05 +08:00
co-authored by Cursor
parent bd5d88abfa
commit f37388761b
22 changed files with 930 additions and 303 deletions
+17 -1
View File
@@ -2292,7 +2292,23 @@ export function isCloserModule(
name?: string,
): boolean {
if (module?.closer === true) return true;
return (name ?? "").trim() === "开场白与开场变量";
const t = (name ?? "").trim();
return t === "开场白与开场变量" || t.startsWith("开场白与开场变量#") || t === "开场白";
}
export function hasAcceptedCloserStep(
flow: CreationFlow | null | undefined,
acceptedStepIds: readonly string[],
catalog?: ModuleCatalog | null,
): boolean {
if (flow?.steps.length) {
return flow.steps.some(
(s) =>
isCloserModule(findModuleByName(catalog, s.name), s.name) &&
isStepAccepted(s, acceptedStepIds),
);
}
return acceptedStepIds.some((id) => isCloserModule(null, id));
}
export function findCloserStepIndex(
+17 -4
View File
@@ -1,5 +1,5 @@
/**
* 开场白终节点:选定后落库、关 DAG。
* 开场白终节点:选定后落库开场。不关 DAG;产物靠用户「保存」拆出。
* 能力 `opening-setup` 与磁盘 worker `opening-generator` 共用这条收口。
*/
import {
@@ -22,21 +22,34 @@ export const SLOT_CREATION_SEALED_BY_OPENING = "creationSealedByOpening";
export const SLOT_OPENING_SELECTED_INDEX = "openingSelectedIndex";
export const CREATION_SEALED_WAITING_MESSAGE =
"开场已选定,创作已收口并保存。可切换到「游玩」。";
"开场已选定。可保存为产物后开玩,或继续补节点。";
export const INSTANCE_OPENING_SNAPSHOT_LABEL = "创作定稿(开场)";
export const INSTANCE_OPENING_SNAPSHOT_LABEL = "定稿";
export function isOpeningLockTag(tag: string): boolean {
const t = tag.trim();
return t === OPENING_OUTPUT_TAG || t === OPENING_INITIAL_VARS_TAG;
}
/** 收口步 id / name(含「开场白与开场变量#2」) */
export function isCloserStepRef(id: string | undefined): boolean {
const t = (id ?? "").trim();
if (!t) return false;
return (
t === "开场白与开场变量" ||
t.startsWith("开场白与开场变量#") ||
t === "开场白"
);
}
export function isOpeningSealArtifact(artifact: {
workerId?: string;
stepId?: string;
outputTags?: readonly string[];
}): boolean {
if ((artifact.workerId ?? "").trim() === "opening-generator") return true;
return (artifact.outputTags ?? []).includes(OPENING_SETUP_ARTIFACT_TAG);
if ((artifact.outputTags ?? []).includes(OPENING_SETUP_ARTIFACT_TAG)) return true;
return isCloserStepRef(artifact.stepId);
}
export type OpeningVariable = {
+2
View File
@@ -5,6 +5,8 @@
*/
export const SLOT_PLAY_TURN_QUEUE = "playTurnQueue";
export const SLOT_PLAY_LAYER_ACTIVE = "playLayerActive";
/** 当前游玩线所依据的产物 snapshot id */
export const SLOT_PLAY_INSTANCE_ID = "playInstanceId";
export const PLAY_WORKING_SNAPSHOT_ID = "play-working";
export const PLAY_WORKING_SNAPSHOT_LABEL = "当前游玩";
+15 -1
View File
@@ -1,6 +1,14 @@
import type { Blackboard } from "../blackboard/blackboard.js";
import type { RuntimeSession } from "../types/runtime.js";
import { isPlayLayerActive } from "./play-turn.js";
import {
SLOT_CREATION_ACCEPTED_UNITS,
parseAcceptedUnits,
} from "./creation-units.js";
import {
isCloserStepRef,
SLOT_CREATION_SEALED_BY_OPENING,
} from "./opening-seal.js";
import {
deriveDesignStageScope,
deriveOnDemandWorkerScope,
@@ -41,8 +49,14 @@ export function hasAcceptedWorkerSet(session: RuntimeSession): boolean {
);
}
function hasAcceptedCloser(session: RuntimeSession): boolean {
if (Boolean(session.slots[SLOT_CREATION_SEALED_BY_OPENING])) return true;
const accepted = parseAcceptedUnits(session.slots[SLOT_CREATION_ACCEPTED_UNITS]);
return accepted.some((id) => isCloserStepRef(id));
}
export function canEnterPlay(session: RuntimeSession): boolean {
return hasAcceptedWorkerSet(session);
return hasAcceptedWorkerSet(session) || hasAcceptedCloser(session);
}
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {