打通转述创作模式的会话与运行时,进料时跳过配方节点图。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -132,6 +132,10 @@ import {
|
||||
SLOT_CREATION_SEALED_BY_OPENING,
|
||||
SLOT_OPENING_SELECTED_INDEX,
|
||||
} from "../skills/opening-seal.js";
|
||||
import {
|
||||
CREATION_INTAKE_MODE_TAG,
|
||||
isDictateModeValue,
|
||||
} from "../dictate/types.js";
|
||||
import {
|
||||
readPlayTurnQueue,
|
||||
withPlayTurnQueue,
|
||||
@@ -480,6 +484,32 @@ export class PhaseRuntime {
|
||||
await this.awaitCreationStepPick();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转述整理式进料:跳过配方 DAG / 点选节点,直接等用户说话。
|
||||
* 总管不跑 design-flow;由 session-manager 的 dictate 对话环处理输入。
|
||||
*/
|
||||
enterDictateIntake(message?: string): void {
|
||||
this.session = {
|
||||
...this.session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: {
|
||||
kind: "input",
|
||||
message:
|
||||
message?.trim() ||
|
||||
"转述整理:直接说你想要的体验与设定;我会整理进产物。缺什么会问你。",
|
||||
},
|
||||
pendingDecision: undefined,
|
||||
currentWorkerId: undefined,
|
||||
pendingArtifactId: undefined,
|
||||
resumeContext: undefined,
|
||||
slots: {
|
||||
...this.session.slots,
|
||||
startupCompleted: true,
|
||||
},
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
||||
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
||||
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
||||
@@ -519,7 +549,18 @@ export class PhaseRuntime {
|
||||
this.session.slots.designInstanceReady
|
||||
) {
|
||||
this.ensurePlaySpecReady();
|
||||
return;
|
||||
}
|
||||
// 转述:无节点收口,落档/开玩时直接封印并补默认运行规格
|
||||
if (this.isDictateIntakeMode()) {
|
||||
this.ensurePlaySpecReady();
|
||||
}
|
||||
}
|
||||
|
||||
private isDictateIntakeMode(): boolean {
|
||||
return isDictateModeValue(
|
||||
this.blackboard.getContentByTag(CREATION_INTAKE_MODE_TAG),
|
||||
);
|
||||
}
|
||||
|
||||
getAvailableSkills(): SkillIndexEntry[] {
|
||||
|
||||
@@ -25,6 +25,7 @@ export type AgentMessageKind =
|
||||
| "orchestrator_thinking"
|
||||
| "orchestrator_prompt"
|
||||
| "orchestrator_assessment"
|
||||
| "dictate_reply"
|
||||
| "agent_tool"
|
||||
| "worker_running"
|
||||
| "worker_output"
|
||||
|
||||
@@ -177,7 +177,7 @@ export async function handleBooksApi(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 新建作品:配方一层选型(内部 = recipes catalog) */
|
||||
/** 新建作品:配方一层选型(节点流程 / 转述各一本目录,不共享) */
|
||||
if (pathname === "/api/directors" && req.method === "GET") {
|
||||
const { DEFAULT_ORCHESTRATOR_ID } = await import(
|
||||
"../config/default-orchestrator.js"
|
||||
@@ -188,17 +188,35 @@ export async function handleBooksApi(
|
||||
const { loadRecipeCatalog } = await import("../skills/creation-flow.js");
|
||||
const skill = await loadSkill(skillId);
|
||||
if (!skill.skillPackRoot) {
|
||||
json(res, 200, { directors: [], skillPackId: skillId });
|
||||
json(res, 200, {
|
||||
directors: [],
|
||||
recipeDirectors: [],
|
||||
dictateDirectors: [],
|
||||
skillPackId: skillId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const catalog = await loadRecipeCatalog(skill.skillPackRoot);
|
||||
const [recipeCat, dictateCat] = await Promise.all([
|
||||
loadRecipeCatalog(skill.skillPackRoot, undefined, "recipe"),
|
||||
loadRecipeCatalog(skill.skillPackRoot, undefined, "dictate"),
|
||||
]);
|
||||
const mapRow = (r: {
|
||||
id: string;
|
||||
name: string;
|
||||
declaration: string;
|
||||
}) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
declaration: r.declaration,
|
||||
});
|
||||
const recipeDirectors = (recipeCat?.recipes ?? []).map(mapRow);
|
||||
const dictateDirectors = (dictateCat?.recipes ?? []).map(mapRow);
|
||||
json(res, 200, {
|
||||
skillPackId: skillId,
|
||||
directors: (catalog?.recipes ?? []).map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
declaration: r.declaration,
|
||||
})),
|
||||
/** @deprecated 兼容旧前端:默认节点流程列表 */
|
||||
directors: recipeDirectors,
|
||||
recipeDirectors,
|
||||
dictateDirectors,
|
||||
});
|
||||
} catch (err) {
|
||||
json(res, 404, {
|
||||
@@ -324,6 +342,8 @@ export async function handleBooksApi(
|
||||
orchestratorId?: string;
|
||||
/** 用户手动选定的配方 id(内部 recipe) */
|
||||
recipeId?: string;
|
||||
/** recipe=配方流(默认);dictate=转述整理直填 */
|
||||
creationMode?: "recipe" | "dictate";
|
||||
};
|
||||
const skills = await listSkills();
|
||||
const requested = body.orchestratorId?.trim();
|
||||
@@ -342,10 +362,17 @@ export async function handleBooksApi(
|
||||
orchestratorId: director.name,
|
||||
orchestratorName: director.name,
|
||||
});
|
||||
const creationMode =
|
||||
body.creationMode === "dictate" ? "dictate" : "recipe";
|
||||
if (!body.recipeId?.trim()) {
|
||||
json(res, 400, { error: "请选择配方" });
|
||||
return true;
|
||||
}
|
||||
const session = await sessionManager.createForBook(
|
||||
book.id,
|
||||
director.name,
|
||||
body.recipeId?.trim(),
|
||||
body.recipeId.trim(),
|
||||
{ creationMode },
|
||||
);
|
||||
json(res, 201, { book: getBook(book.id) ?? book, session });
|
||||
return true;
|
||||
|
||||
+380
-11
@@ -139,6 +139,17 @@ import {
|
||||
buildCreationDialogueTranscript,
|
||||
foldRunProcessMessages,
|
||||
} from "./prune-creation-messages.js";
|
||||
import {
|
||||
CREATION_INTAKE_MODE_TAG,
|
||||
DICTATE_MODE_VALUE,
|
||||
DICTATE_ORDER_META_KEY,
|
||||
defaultDictateOrder,
|
||||
extractDictateDialogue,
|
||||
isDictateModeValue,
|
||||
runDictateTurn,
|
||||
sortDictateProducts,
|
||||
type DictateProduct,
|
||||
} from "../dictate/index.js";
|
||||
import {
|
||||
formatQuestionAnswersForAi,
|
||||
formatQuestionAnswersForDisplay,
|
||||
@@ -252,6 +263,10 @@ export type SessionView = {
|
||||
/** Agent / Worker 流式输出(轮询用) */
|
||||
liveStream?: LiveStreamView;
|
||||
resumed?: boolean;
|
||||
/** 创作进料:recipe=配方节点流;dictate=转述整理直填 */
|
||||
creationMode?: "recipe" | "dictate";
|
||||
/** 转述式已写入黑板的产物(按相对序) */
|
||||
dictateProducts?: Array<{ tag: string; content: string; order?: number }>;
|
||||
tokenStats?: {
|
||||
sessionTotal: number;
|
||||
sessionCached?: number;
|
||||
@@ -503,6 +518,7 @@ export class SessionManager {
|
||||
bookId?: string,
|
||||
preselectSkillId?: string,
|
||||
selectedRecipeId?: string,
|
||||
opts?: { creationMode?: "recipe" | "dictate" },
|
||||
): Promise<SessionView> {
|
||||
const id = randomUUID();
|
||||
const messages: ChatMessage[] = [];
|
||||
@@ -543,11 +559,22 @@ export class SessionManager {
|
||||
} else {
|
||||
await runtime.start();
|
||||
}
|
||||
await this.refreshRecipeOptions(managed);
|
||||
const useDictate = opts?.creationMode === "dictate";
|
||||
await this.refreshRecipeOptions(
|
||||
managed,
|
||||
useDictate ? "dictate" : "recipe",
|
||||
);
|
||||
|
||||
if (selectedRecipeId?.trim()) {
|
||||
await this.applySelectedRecipe(managed, selectedRecipeId.trim());
|
||||
await this.applySelectedRecipe(managed, selectedRecipeId.trim(), {
|
||||
seedFlow: !useDictate,
|
||||
});
|
||||
}
|
||||
if (useDictate) {
|
||||
this.enterDictateMode(managed);
|
||||
} else {
|
||||
await this.maybeKickAgentAfterDemand(runtime);
|
||||
}
|
||||
await this.maybeKickAgentAfterDemand(runtime);
|
||||
|
||||
recordPreMessageCheckpoint(
|
||||
managed.branchState,
|
||||
@@ -564,6 +591,183 @@ export class SessionManager {
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
/** 切换到转述整理进料(与配方并行;不跑 design-flow) */
|
||||
private enterDictateMode(s: ManagedSession): void {
|
||||
const board = s.runtime.getBlackboard();
|
||||
board.write({
|
||||
tag: CREATION_INTAKE_MODE_TAG,
|
||||
content: DICTATE_MODE_VALUE,
|
||||
source: "runtime",
|
||||
});
|
||||
s.runtime.enterDictateIntake();
|
||||
const recipe = resolveSelectedRecipeView(board, s.recipeOptions);
|
||||
const recipeLine = recipe?.name
|
||||
? `\n本局配方:${recipe.name}${
|
||||
recipe.declaration ? `(${recipe.declaration})` : ""
|
||||
}。`
|
||||
: "";
|
||||
const welcome =
|
||||
`转述整理模式:直接说你想要的体验与设定。我会整理进产物;缺什么会问你。对话过长时可清空对话(产物保留)。${recipeLine}`;
|
||||
s.messages.push({
|
||||
id: randomUUID(),
|
||||
role: "system",
|
||||
text: welcome,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: "orchestrator_prompt",
|
||||
actor: "orchestrator",
|
||||
title: "转述整理",
|
||||
body: welcome,
|
||||
});
|
||||
}
|
||||
|
||||
private isDictateMode(s: ManagedSession): boolean {
|
||||
return isDictateModeValue(
|
||||
s.runtime.getBlackboard().getContentByTag(CREATION_INTAKE_MODE_TAG),
|
||||
);
|
||||
}
|
||||
|
||||
private listDictateProducts(s: ManagedSession): DictateProduct[] {
|
||||
const board = s.runtime.getBlackboard();
|
||||
const products: DictateProduct[] = [];
|
||||
for (const e of board.listTagIndex()) {
|
||||
if (!e.tag.startsWith("用户.") && !e.tag.startsWith("设计.")) continue;
|
||||
const item = board.getLatestByTag(e.tag);
|
||||
const content = item?.content?.trim() ?? "";
|
||||
if (!content) continue;
|
||||
const rawOrder = item?.metadata?.[DICTATE_ORDER_META_KEY];
|
||||
const order =
|
||||
typeof rawOrder === "number" && Number.isFinite(rawOrder)
|
||||
? rawOrder
|
||||
: undefined;
|
||||
products.push({ tag: e.tag, content, order });
|
||||
}
|
||||
return sortDictateProducts(products);
|
||||
}
|
||||
|
||||
/** 清空转述对话,保留黑板产物(产物即浓缩上下文) */
|
||||
async clearDictateDialogue(sessionId: string): Promise<SessionView> {
|
||||
const s = this.require(sessionId);
|
||||
if (!this.isDictateMode(s)) {
|
||||
throw new Error("当前不是转述整理模式");
|
||||
}
|
||||
const kept = this.listDictateProducts(s);
|
||||
s.messages = [];
|
||||
const note =
|
||||
kept.length > 0
|
||||
? `对话已清空。仍保留 ${kept.length} 项产物:${kept.map((p) => p.tag).join("、")}。`
|
||||
: "对话已清空。尚无产物,请继续说明需求。";
|
||||
s.messages.push({
|
||||
id: randomUUID(),
|
||||
role: "system",
|
||||
text: note,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: "system_info",
|
||||
actor: "system",
|
||||
title: "对话已清空",
|
||||
body: note,
|
||||
});
|
||||
s.runtime.enterDictateIntake("对话已清空;产物仍在。请继续。");
|
||||
if (s.bookId) this.persist(s);
|
||||
return this.toView(sessionId);
|
||||
}
|
||||
|
||||
private async runDictateTurnForSession(
|
||||
s: ManagedSession,
|
||||
_userText: string,
|
||||
): Promise<void> {
|
||||
const llm = createDefaultMainAgentLlm(s.trackingRef);
|
||||
let dialogue = extractDictateDialogue(s.messages);
|
||||
const recipeView = resolveSelectedRecipeView(
|
||||
s.runtime.getBlackboard(),
|
||||
s.recipeOptions,
|
||||
);
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm,
|
||||
dialogue,
|
||||
recipeName: recipeView?.name,
|
||||
recipeBrief: recipeView
|
||||
? [recipeView.declaration].filter(Boolean).join(";")
|
||||
: undefined,
|
||||
handlers: {
|
||||
listProducts: () => this.listDictateProducts(s),
|
||||
writeProduct: (tag, content, order) => {
|
||||
const board = s.runtime.getBlackboard();
|
||||
const existing = board.getLatestByTag(tag);
|
||||
const prev = existing?.metadata?.[DICTATE_ORDER_META_KEY];
|
||||
const resolved =
|
||||
typeof order === "number" && Number.isFinite(order)
|
||||
? order
|
||||
: typeof prev === "number" && Number.isFinite(prev)
|
||||
? prev
|
||||
: defaultDictateOrder(tag);
|
||||
board.write({
|
||||
tag,
|
||||
content,
|
||||
source: "agent",
|
||||
metadata: {
|
||||
...(existing?.metadata ?? {}),
|
||||
[DICTATE_ORDER_META_KEY]: resolved,
|
||||
},
|
||||
});
|
||||
},
|
||||
clearDialogue: () => {
|
||||
const kept = this.listDictateProducts(s);
|
||||
const lastUser = [...s.messages]
|
||||
.reverse()
|
||||
.find((m) => m.kind === "user_input");
|
||||
const note =
|
||||
kept.length > 0
|
||||
? `(已清空先前对话;保留产物 ${kept.map((p) => p.tag).join("、")})`
|
||||
: "(已清空先前对话)";
|
||||
s.messages = [
|
||||
{
|
||||
id: randomUUID(),
|
||||
role: "system",
|
||||
text: note,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: "system_info",
|
||||
actor: "system",
|
||||
title: "对话已清空",
|
||||
body: note,
|
||||
},
|
||||
...(lastUser ? [lastUser] : []),
|
||||
];
|
||||
dialogue = extractDictateDialogue(s.messages);
|
||||
},
|
||||
onToolCall: (name, detail) => {
|
||||
s.messages.push(
|
||||
this.msg("system", `[总管 tool] ${name}: ${detail}`),
|
||||
);
|
||||
},
|
||||
onThinkingDelta: (delta) => {
|
||||
const id = s.runtime.getSession().id;
|
||||
const prev = this.agentThinkingLive.get(id) ?? "";
|
||||
this.agentThinkingLive.set(id, prev + delta);
|
||||
this.agentThinkingClosed.delete(id);
|
||||
},
|
||||
onThinkingDone: (text) => {
|
||||
const id = s.runtime.getSession().id;
|
||||
if (text.trim()) this.agentThinkingLive.set(id, text);
|
||||
this.agentThinkingClosed.add(id);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.clearAgentThinking(s.runtime.getSession().id);
|
||||
s.messages.push({
|
||||
id: randomUUID(),
|
||||
role: "system",
|
||||
text: result.reply,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: "dictate_reply",
|
||||
actor: "orchestrator",
|
||||
title: "转述整理",
|
||||
body: result.reply,
|
||||
});
|
||||
s.runtime.enterDictateIntake();
|
||||
}
|
||||
|
||||
/** 用户手动选定 / 更换初始配方 */
|
||||
async setSelectedRecipe(
|
||||
sessionId: string,
|
||||
@@ -613,10 +817,11 @@ export class SessionManager {
|
||||
|
||||
if (kind === "instance") {
|
||||
s.runtime.prepareEnterPlay();
|
||||
this.ensurePlayOpeningWritten(s);
|
||||
runtimeSession = structuredClone(s.runtime.getSession());
|
||||
blackboardItems = s.runtime.getBlackboard().exportItems();
|
||||
if (!canEnterPlay(runtimeSession)) {
|
||||
throw new Error("须先完成收口或验收 Worker 集,才能保存产物");
|
||||
throw new Error("须先完成收口、验收 Worker 集,或(转述)先写入产物,才能保存定稿");
|
||||
}
|
||||
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||
runtimeSession,
|
||||
@@ -683,7 +888,7 @@ export class SessionManager {
|
||||
this.persistCreationIfDesign(s);
|
||||
s.runtime.prepareEnterPlay();
|
||||
if (!canEnterPlay(s.runtime.getSession())) {
|
||||
throw new Error("须先验收 Worker 集或完成收口,才能开始游玩");
|
||||
throw new Error("须先验收 Worker 集、完成收口,或(转述)先写入产物并落档,才能开始游玩");
|
||||
}
|
||||
this.archivePlayWorking(s.bookId);
|
||||
this.enterPlayLayer(s, { resume: false, instanceId });
|
||||
@@ -739,6 +944,9 @@ export class SessionManager {
|
||||
|
||||
private beginNewPlayFromInstance(s: ManagedSession, instanceId?: string): void {
|
||||
const keepId = s.runtime.getSession().id;
|
||||
// 换上 instance 快照前先记下当前创作里的开场白(可能比上次定稿更新)
|
||||
const liveDesignOpening =
|
||||
s.runtime.getBlackboard().getContentByTag("设计.开场白")?.trim() ?? "";
|
||||
let runtimeSession: RuntimeSession;
|
||||
let blackboardItems: import("../types/blackboard.js").BlackboardItem[];
|
||||
const instance = s.bookId
|
||||
@@ -772,8 +980,18 @@ export class SessionManager {
|
||||
runtimeSession.currentWorkerId = undefined;
|
||||
runtimeSession.resumeContext = undefined;
|
||||
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||
const opening =
|
||||
s.runtime.getBlackboard().getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||
// 新开一局(未点名旧定稿):用当前创作的设计.开场白覆盖快照里过期的开场
|
||||
if (liveDesignOpening && !instanceId) {
|
||||
s.runtime.getBlackboard().write({
|
||||
tag: "设计.开场白",
|
||||
content: liveDesignOpening,
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
const opening = this.ensurePlayOpeningWritten(
|
||||
s,
|
||||
instance?.messages as ChatMessage[] | undefined,
|
||||
);
|
||||
if (opening) {
|
||||
s.runtime.getBlackboard().write({
|
||||
tag: DIALOGUE_HISTORY_TAG,
|
||||
@@ -787,6 +1005,104 @@ export class SessionManager {
|
||||
s.branchState = createMessageBranchState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保证黑板上有 输出.开场白,供开玩主区展示。
|
||||
* 优先级:设计.开场白(转述产物)> 已有 输出.开场白 > 其它产物拼装 > 非转述聊天兜底。
|
||||
* 切勿用 dictate_reply(常含确认/建议补充)盖过已钉的设计.开场白。
|
||||
*/
|
||||
private ensurePlayOpeningWritten(
|
||||
s: ManagedSession,
|
||||
messageSource?: ChatMessage[],
|
||||
): string {
|
||||
const board = s.runtime.getBlackboard();
|
||||
const fromDesign =
|
||||
board.getContentByTag("设计.开场白")?.trim() ||
|
||||
this.listDictateProducts(s).find((p) => p.tag === "设计.开场白")
|
||||
?.content?.trim() ||
|
||||
"";
|
||||
if (fromDesign) {
|
||||
const existing = board.getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||
if (existing !== fromDesign) {
|
||||
board.write({
|
||||
tag: OPENING_OUTPUT_TAG,
|
||||
content: fromDesign,
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
return fromDesign;
|
||||
}
|
||||
|
||||
const existing = board.getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||
if (existing) return existing;
|
||||
|
||||
const fromProducts = this.deriveOpeningFromDictateProducts(s);
|
||||
const fromMessages = fromProducts
|
||||
? ""
|
||||
: this.deriveOpeningFromMessages(messageSource ?? s.messages);
|
||||
const opening = (fromProducts || fromMessages).trim();
|
||||
if (!opening) return "";
|
||||
|
||||
board.write({
|
||||
tag: OPENING_OUTPUT_TAG,
|
||||
content: opening,
|
||||
source: "runtime",
|
||||
});
|
||||
return opening;
|
||||
}
|
||||
|
||||
private deriveOpeningFromMessages(messages: ChatMessage[] | undefined): string {
|
||||
if (!messages?.length) return "";
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const m = messages[i];
|
||||
if (!m || m.role === "user") continue;
|
||||
const kind = m.kind ?? "";
|
||||
// 转述聊天常夹确认/建议,不能当开场白
|
||||
if (
|
||||
kind === "dictate_reply" ||
|
||||
kind === "agent_tool" ||
|
||||
kind === "orchestrator_thinking" ||
|
||||
kind === "worker_questions"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
kind === "system_info" &&
|
||||
!/开场白|用户展示/.test(String(m.title ?? ""))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const body = String(m.body ?? m.text ?? "").trim();
|
||||
if (body.length < 24) continue;
|
||||
if (
|
||||
kind === "worker_output" ||
|
||||
/开场白|用户展示/.test(String(m.title ?? "")) ||
|
||||
m.actor === "narrator"
|
||||
) {
|
||||
return body;
|
||||
}
|
||||
if (body.length >= 80) return body;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private deriveOpeningFromDictateProducts(s: ManagedSession): string {
|
||||
const products = this.listDictateProducts(s);
|
||||
if (!products.length) return "";
|
||||
// 开场白是「按正文格式生成的内容」,优先设计.开场白;不要把设计.正文组成(格式)当开场白
|
||||
const opening = products.find((p) => p.tag === "设计.开场白")?.content?.trim();
|
||||
if (opening) return opening;
|
||||
const preferred = products.filter(
|
||||
(p) =>
|
||||
/开场|开局|场景|需求|设定|叙事/.test(p.tag) &&
|
||||
p.tag !== "设计.正文组成" &&
|
||||
!p.tag.includes("正文组成"),
|
||||
);
|
||||
const list = preferred.length ? preferred : products.filter((p) => p.tag !== "设计.正文组成");
|
||||
if (!list.length) return "";
|
||||
const chunks = list.slice(0, 8).map((p) => `【${p.tag}】\n${p.content}`);
|
||||
return `开场设定已就绪。\n\n${chunks.join("\n\n")}`;
|
||||
}
|
||||
|
||||
private restoreSnapshotInPlace(
|
||||
s: ManagedSession,
|
||||
snapshot: RunSnapshot,
|
||||
@@ -836,7 +1152,31 @@ export class SessionManager {
|
||||
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||
s.messages = snapshot.messages.map((m) => ({ ...m })) as ChatMessage[];
|
||||
s.branchState = createMessageBranchState();
|
||||
if (layer === "play") this.sanitizePlayHitl(s);
|
||||
if (layer === "play") {
|
||||
this.sanitizePlayHitl(s);
|
||||
// 旧产物/工作副本可能没有开场消息:补一条到主区
|
||||
if (!s.messages.some((m) => (m.body ?? m.text ?? "").trim())) {
|
||||
const opening = this.ensurePlayOpeningWritten(
|
||||
s,
|
||||
snapshot.messages as ChatMessage[],
|
||||
);
|
||||
if (opening) {
|
||||
s.messages = [this.playOpeningMessage(opening)];
|
||||
const hist =
|
||||
s.runtime.getBlackboard().getContentByTag(DIALOGUE_HISTORY_TAG) ?? "";
|
||||
if (!hist.trim()) {
|
||||
s.runtime.getBlackboard().write({
|
||||
tag: DIALOGUE_HISTORY_TAG,
|
||||
content: appendDialogueHistoryTurn("", {
|
||||
role: "助手",
|
||||
text: opening,
|
||||
}),
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 游玩层不保留创作 HITL(验收 / 确认下一步 / 修订) */
|
||||
@@ -1143,6 +1483,21 @@ export class SessionManager {
|
||||
}
|
||||
try {
|
||||
const owned = await this.runExclusive(id, s, async () => {
|
||||
// 转述提示词只服务创作进料;游玩层必须走 submitInput → GM/叙事转述管线
|
||||
{
|
||||
const sess = s.runtime.getSession();
|
||||
const inPlay =
|
||||
isPlayLayerActive(sess.slots) ||
|
||||
inferLifecycleStage(sess) === "play";
|
||||
if (
|
||||
this.isDictateMode(s) &&
|
||||
reason?.kind === "input" &&
|
||||
!inPlay
|
||||
) {
|
||||
await this.runDictateTurnForSession(s, trimmed || text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (reason?.kind === "approve_step") {
|
||||
await s.runtime.rejectStep(trimmed || text);
|
||||
} else if (reason?.kind === "review_artifact") {
|
||||
@@ -2664,6 +3019,10 @@ export class SessionManager {
|
||||
actions,
|
||||
pipeline: buildPipeline(session),
|
||||
lifecycleStage,
|
||||
creationMode: this.isDictateMode(s) ? "dictate" : "recipe",
|
||||
dictateProducts: this.isDictateMode(s)
|
||||
? this.listDictateProducts(s)
|
||||
: undefined,
|
||||
playLayerActive: isPlayLayerActive(session.slots),
|
||||
playReady: canEnterPlay(session),
|
||||
hasProduct: Boolean(
|
||||
@@ -2805,7 +3164,10 @@ export class SessionManager {
|
||||
return state;
|
||||
}
|
||||
|
||||
private async refreshRecipeOptions(s: ManagedSession): Promise<void> {
|
||||
private async refreshRecipeOptions(
|
||||
s: ManagedSession,
|
||||
family?: "recipe" | "dictate",
|
||||
): Promise<void> {
|
||||
const skillName = s.runtime.getActiveSkill()?.name;
|
||||
if (!skillName) {
|
||||
s.recipeOptions = [];
|
||||
@@ -2821,8 +3183,10 @@ export class SessionManager {
|
||||
s.openingGuide = null;
|
||||
return;
|
||||
}
|
||||
const intake =
|
||||
family ?? (this.isDictateMode(s) ? "dictate" : "recipe");
|
||||
const [catalog, modules] = await Promise.all([
|
||||
loadRecipeCatalog(skill.skillPackRoot),
|
||||
loadRecipeCatalog(skill.skillPackRoot, undefined, intake),
|
||||
loadModuleCatalog(skill.skillPackRoot),
|
||||
]);
|
||||
s.recipeOptions = catalog?.recipes ?? [];
|
||||
@@ -2837,6 +3201,7 @@ export class SessionManager {
|
||||
|
||||
/** 已选配方但尚无流程 → 写入配方近期起点(兼容旧会话) */
|
||||
private async ensureRecipeSeededFlow(s: ManagedSession): Promise<void> {
|
||||
if (this.isDictateMode(s)) return;
|
||||
const board = s.runtime.getBlackboard();
|
||||
if (board.getContentByTag(CREATION_FLOW_TAG)?.trim()) return;
|
||||
const ref = parseSelectedRecipeRef(
|
||||
@@ -2990,6 +3355,7 @@ export class SessionManager {
|
||||
private async applySelectedRecipe(
|
||||
s: ManagedSession,
|
||||
recipeId: string,
|
||||
opts?: { seedFlow?: boolean },
|
||||
): Promise<void> {
|
||||
const options = s.recipeOptions ?? [];
|
||||
const entry = findRecipeCatalogEntry(
|
||||
@@ -3005,13 +3371,16 @@ export class SessionManager {
|
||||
content: JSON.stringify({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
family: entry.family ?? (opts?.seedFlow === false ? "dictate" : "recipe"),
|
||||
}),
|
||||
source: "user",
|
||||
});
|
||||
|
||||
const seedFlow = opts?.seedFlow !== false;
|
||||
// 开局节点跟剧本 seed:写入近期起点并进入点选图(不必先跑 design-flow / 先描述需求)
|
||||
// 转述进料可只钉配方、不种 DAG
|
||||
const existingFlow = board.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
||||
if (!existingFlow) {
|
||||
if (seedFlow && !existingFlow) {
|
||||
const skillName = s.runtime.getActiveSkill()?.name;
|
||||
if (skillName) {
|
||||
try {
|
||||
|
||||
@@ -179,6 +179,18 @@ const server = createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/dictate/clear-dialogue") {
|
||||
try {
|
||||
const view = await sessionManager.clearDictateDialogue(sessionId);
|
||||
json(res, 200, view);
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
error: err instanceof Error ? err.message : "清空失败",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/answers") {
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
answers?: Array<{
|
||||
|
||||
Reference in New Issue
Block a user