import { randomUUID } from "node:crypto"; import { appendBookSession, getBook, updateBook } from "../book/store.js"; import { loadBookSession, saveBookSession } from "../book/session-store.js"; import { deleteRunSnapshot as deleteRunSnapshotFile, listRunSnapshots, loadRunSnapshot as loadRunSnapshotFile, saveRunSnapshot as saveRunSnapshotFile, } from "../book/run-snapshot-store.js"; import type { RunSnapshot, RunSnapshotMeta, SnapshotKind } from "../types/run-snapshot.js"; import { toRunSnapshotMeta } from "../types/run-snapshot.js"; import { materializeInstanceSnapshotPayload } from "../book/snapshot-filters.js"; import { INSTANCE_OPENING_SNAPSHOT_LABEL, OPENING_OUTPUT_TAG, } from "../skills/opening-seal.js"; import { PLAY_WORKING_SNAPSHOT_ID, PLAY_WORKING_SNAPSHOT_LABEL, SLOT_PLAY_INSTANCE_ID, SLOT_PLAY_LAYER_ACTIVE, isPlayLayerActive, } from "../skills/play-turn.js"; import { DIALOGUE_HISTORY_TAG, appendDialogueHistoryTurn, } from "../skills/dialogue-history.js"; import { PhaseRuntime, createDecision } from "../runtime/phase-runtime.js"; import { createSession } from "../runtime/phase-machine.js"; import { createDefaultMainAgentLlm, hasRealLlmConfig, reloadDefaultMainAgentLlm, type LlmTrackingRef, } from "../runtime/llm-factory.js"; import { isAbortError, runWithAbortSignal } from "../llm/run-abort.js"; import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js"; import type { PersistedBookSession } from "../types/book-session.js"; import type { RuntimeSession, WaitingReason } from "../types/runtime.js"; import type { IntakeProgress } from "../types/intake.js"; import { buildFocus, buildPipeline, buildToolTrace, buildBurstState, buildSkillCatalog, inferLifecycleStage, canEnterPlay, classifyAgentMessage, type AgentMessageKind, type SessionFocus, type StageStep, type LifecycleStage, type SkillCatalogEntry, type ToolTraceEntry, type BurstState, } from "./agent-view.js"; import { buildIntakeProgress, readIntakeValues, } from "../intake/intake.js"; import { bookSkillPackId, persistedSkillPackId, runSnapshotSkillPackId, sessionSkillPackId, skillPacksMatch, } from "../book/skill-id.js"; import { loadAppSettings, normalizeContextTraceKeepLatest, } from "../config/settings.js"; import { pruneContextTraces, type LlmContextTrace, } from "../types/context-trace.js"; import type { ActiveSkillSnapshot } from "../types/runtime.js"; import type { BlackboardItem } from "../types/blackboard.js"; import { Blackboard } from "../blackboard/blackboard.js"; import { parseWorkerSetYaml } from "../skills/worker-set-parse.js"; import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js"; import { CREATION_ACCEPTED_CONTENT_TAG, CREATION_ACCEPTED_UNITS_TAG, isDesignDiskWorker, parseAcceptedContentStore, parseAcceptedUnits, SLOT_CREATION_ACCEPTED_UNITS, } from "../skills/creation-units.js"; import { formatWorkerSetForUser, type WorkerSetUserView, } from "../skills/worker-set-view.js"; import { CONTEXT_ORDER_TAG, applyContextOrderEdit, mergeContextOrderIntoWorkerSetJson, parseContextOrder, serializeContextOrder, synthesizeContextOrderFromWorkers, type ContextOrderEdit, } from "../skills/context-order.js"; import { CREATION_CURRENT_STEP_TAG, CREATION_FLOW_TAG, CREATION_PROPOSED_STEP_TAG, CREATION_SELECTED_RECIPE_TAG, DESIGN_FLOW_WORKER_ID, creationFlowFromRecipeSeed, findRecipeCatalogEntry, formatCreationFlowForUser, formatDictateRecipeBrief, findModuleByName, findStepByRef, type FlowStepTitleHint, isProgressPointerTag, loadRecipeCatalog, loadRecipeDetail, loadModuleCatalog, parseCreationFlow, parseSelectedRecipeRef, resolveDesignStepBinding, stepUnitId, stringifyCreationFlow, stripReviseSteps, type CreationFlow, type CreationFlowUserView, type ModuleCatalog, type RecipeCatalogEntry, } from "../skills/creation-flow.js"; import { loadSkill } from "../skills/loader.js"; import { effectiveStartupMode, DEFAULT_UI_PROMPT } from "../config/default-orchestrator.js"; import { buildBoardPanel } from "../runtime/compress-after-worker.js"; import { displayWorkerLabel, formatWorkerDisplayTitle, reviewComposerCopy, } from "./display-labels.js"; 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, isModuleOpeningQuestions, normalizeQuestions, } from "../skills/question-protocol.js"; import type { QuestionAnswer } from "../types/questions.js"; import { appendBranchVariant, createMessageBranchState, createUserVariantMessage, ensureBranchForEdit, ensureBranchForRefresh, findPrecedingUserIndex, isRefreshableMessage, recordPreMessageCheckpoint, switchBranchVariant, updateActiveBranchVariant, type MessageBranchState, type SessionCheckpoint, } from "./message-branch.js"; /** 这些消息种类挂载全量 LLM 上下文痕迹(右键可查看) */ const CONTEXT_TRACE_MESSAGE_KINDS = new Set([ "worker_output", "orchestrator_decision", "orchestrator_assessment", "orchestrator_thinking", ]); export type ChatMessage = { id: string; role: "system" | "user"; text: string; createdAt: string; kind?: AgentMessageKind; actor?: string; title?: string; body?: string; /** 思维链 / reasoning_content */ thinking?: string; /** 该次 LLM 请求实际携带的全量上下文(观察用;按设置只保留最新 N 条) */ contextTrace?: LlmContextTrace; /** run 验收后过程讨论折叠(主 feed 隐藏);创作 messages 不删不藏 */ compressed?: boolean; tokenUsage?: MessageTokenUsage; /** 消息分支:同位多版本(编辑 / 重 roll) */ branchGroupId?: string; branchIndex?: number; branchTotal?: number; }; export type SessionView = { id: string; bookId?: string; bookTitle?: string; activeSkill?: string; phase: RuntimeSession["phase"]; waitingReason?: WaitingReason; startupCompleted: boolean; skills: Array<{ name: string; description: string; category: string }>; messages: ChatMessage[]; hints: string[]; actions: SessionAction[]; /** @deprecated 用 skillCatalog */ pipeline: StageStep[]; lifecycleStage: LifecycleStage; /** 已从定稿开出聊天层(不是只把顶栏标成游玩) */ playLayerActive: boolean; playReady: boolean; /** 已落档至少一份产物(instance 快照) */ hasProduct: boolean; /** 当前游玩所依据的产物 id */ playInstanceId?: string; skillCatalog: SkillCatalogEntry[]; toolTrace: ToolTraceEntry[]; burst: BurstState; focus: SessionFocus; /** 启动填空进度(waitingReason.kind === intake 时有值) */ intake?: IntakeProgress; intakePrompt?: string; /** agent-first 首屏固定引导(纯 UI,非消息流) */ uiPrompt?: string; /** 待验收产物全文(review_artifact 时在右侧 Tab 展示) */ reviewArtifact?: ReviewArtifactView; /** Worker 集用户视图(验收 / 设计进度共用) */ workerSetView?: WorkerSetUserView; /** 创作流程用户视图(步骤顺序 + 依赖) */ creationFlowView?: CreationFlowUserView; /** 确认开干:下一步提案(含可编辑编排参数) */ proposedNextStep?: ProposedNextStepView; /** 能力包内可选初始配方(用户手动选) */ recipes?: Array<{ id: string; name: string; declaration: string }>; /** 用户已选初始配方 */ selectedRecipe?: { id: string; name: string; declaration: string } | null; /** * 开局引导:配方起点或当前能力默认问题的 opening(说话面主柱;对齐美学纲领)。 */ openingGuide?: { stepId: string; stepName: string; text: string } | null; /** 黑板与定稿上下文(强可读) */ boardPanel?: BoardPanelView; /** 游玩呈现壳微调(来自设计.正文组成) */ presentationTweaks?: { shell_id?: string; tone_chrome?: string; show_suggested_actions?: boolean; block_labels?: Record; empty_states?: Record; }; /** 总管正在输出的思维链(流式,轮询用) */ agentThinking?: string; /** Agent / Worker 流式输出(轮询用) */ liveStream?: LiveStreamView; resumed?: boolean; /** 创作进料:recipe=配方节点流;dictate=转述整理直填 */ creationMode?: "recipe" | "dictate"; /** 转述式已写入黑板的产物(按相对序) */ dictateProducts?: Array<{ tag: string; content: string; order?: number }>; tokenStats?: { sessionTotal: number; sessionCached?: number; sessionCacheMiss?: number; lastCaller?: string; lastTotal?: number; byCaller?: Record< string, { totalTokens: number; cachedTokens: number; cacheMissTokens: number; calls: number } >; }; }; export type SessionAction = | { type: "send_message"; label: string; placeholder: string } | { type: "answer_questions"; label: string } | { type: "skip_questions"; label: string } | { type: "confirm_intake"; label: string } | { type: "approve"; label: string } | { type: "accept"; label: string } | { type: "reject"; label: string } | { type: "replan"; label: string } | { type: "leave_step"; label: string } | { type: "run_outline"; label: string } | { type: "finish"; label: string }; export type LiveStreamView = { actor: "orchestrator" | "worker"; actorId?: string; label: string; thinking?: string; output?: string; }; export type BoardPanelView = { finals: Array<{ tag: string; source: string; preview: string; updatedAt: string }>; active: Array<{ tag: string; source: string; preview: string; updatedAt: string }>; archivedCount: number; brief?: string; }; export type ProposedNextStepView = { stepId: string; name: string; declaration?: string; params: Record; paramsMissing: string[]; paramSpecs: Array<{ key: string; label: string; required: boolean; hint?: string; }>; /** 节点特性:prior-artifact = 先验产物(不在确认开干时钉「写什么」) */ kind?: string; /** 用户在「下一步想写什么」里写的意向(与步骤 mode 不同) */ intent?: string; /** 步骤怎么跑:revise = 回头修改既有产物 */ mode?: "fresh" | "revise"; revises?: string; /** 收口节点(开场白):确认开干前仍可改编排追加前序节点 */ closer?: boolean; }; export type ReviewArtifactView = { id: string; workerId: string; summary?: string; body: string; outputTags?: string[]; /** 产出该产物的 worker_output 消息(验收态下 feed 会隐藏该消息) */ sourceMessageId?: string; /** 与 sourceMessage 同源的全量 LLM 上下文,供待验收卡片右键查看 */ contextTrace?: LlmContextTrace; /** 创作 design-* 验收时解析后的 Worker 集(供 UI 结构化展示) */ workerSet?: import("../skills/worker-set-parse.js").ParsedWorkerSet; /** 中文卡片化用户视图 */ workerSetView?: WorkerSetUserView; /** 创作流程(步骤 + 依赖) */ creationFlowView?: CreationFlowUserView; }; type ManagedSession = { runtime: PhaseRuntime; messages: ChatMessage[]; bookId?: string; trackingRef: LlmTrackingRef; branchState: MessageBranchState; /** 当前能力包可选初始配方(用户手动选) */ recipeOptions?: RecipeCatalogEntry[]; /** 模块目录(分层图 / 可反复增殖) */ moduleCatalog?: ModuleCatalog | null; /** 开局模块 opening,缓存给意图页 */ openingGuide?: { stepId: string; stepName: string; text: string } | null; /** 当前 LLM 请求的取消器(停止并重试) */ runAbort?: AbortController; /** 递增世代:后一次请求作废前一次 */ runGen?: number; /** 本轮开始时的消息条数,取消时裁掉执行中产生的调度句 */ runMessageCutoff?: number; }; export class SessionManager { private readonly sessions = new Map(); /** bookId → 当前内存中的 sessionId */ private readonly activeBookSessions = new Map(); /** sessionId → 流式 thinking 缓冲(思考结束后仍保留,直到本轮结束) */ private readonly agentThinkingLive = new Map(); /** 本轮思维链已收束;下一次 delta 起新一轮 */ private readonly agentThinkingClosed = new Set(); /** sessionId → worker 流式缓冲 */ private readonly workerLive = new Map< string, { workerId: string; thinking: string; output: string } >(); async create(): Promise { return this.createForBook(); } setLifecycleStage(sessionId: string, stage: LifecycleStage): SessionView { const s = this.require(sessionId); if (stage === "play") { s.runtime.prepareEnterPlay(); if (!canEnterPlay(s.runtime.getSession())) { throw new Error("实例尚未就绪,无法进入游玩"); } this.enterPlayLayer(s, { resume: true }); } else { this.enterDesignLayer(s); } if (s.bookId) this.persist(s); return this.toView(sessionId); } /** 用户手改黑板 tag(创作期改设定/表;尊重后续 worker 的表 rev 合并) */ writeUserBoardTag( sessionId: string, tag: string, content: string, ): SessionView { const s = this.require(sessionId); const trimmedTag = tag.trim(); if (!trimmedTag) throw new Error("tag 不能为空"); s.runtime.getBlackboard().write({ tag: trimmedTag, content: content ?? "", source: "user", }); if (s.bookId) this.persist(s); return this.toView(sessionId); } /** * 编排上下文投影排序:写入 设计.worker集.context_order(若有规格), * 并同步 设计.上下文投影排序。下次声明驱动拼装即按新序。 */ patchContextOrder(sessionId: string, edit: ContextOrderEdit): SessionView { const s = this.require(sessionId); const bb = s.runtime.getBlackboard(); const workerSetRaw = bb.getContentByTag("设计.worker集")?.trim() || bb.getContentByTag("设计.worker集.草稿")?.trim() || ""; const orderTagRaw = bb.getContentByTag(CONTEXT_ORDER_TAG)?.trim() || ""; let current = parseContextOrder( workerSetRaw ? parseWorkerSetYaml(workerSetRaw).context_order : undefined, ); if (!current) current = parseContextOrder(orderTagRaw); if (!current && workerSetRaw) { const parsed = parseWorkerSetYaml(workerSetRaw); // 与检查器合成逻辑一致:缺 context 时用包内默认契约(视图侧也会合成) current = synthesizeContextOrderFromWorkers( parsed.workers.map((w) => ({ ref: w.ref, name: w.name, context: w.context, })), parsed.play_slots, ); } if (!current && edit.action !== "replace") { throw new Error("尚无上下文投影排序可编辑;请先完成游玩拓扑/细化终稿,或提交完整 replace"); } if (!current && edit.action === "replace") { current = parseContextOrder(edit.context_order); if (!current) throw new Error("context_order 无法解析"); } const next = applyContextOrderEdit(current!, edit); const serialized = serializeContextOrder(next); bb.write({ tag: CONTEXT_ORDER_TAG, content: serialized, source: "user", }); if (workerSetRaw) { const targetTag = bb.getContentByTag("设计.worker集")?.trim() ? "设计.worker集" : bb.getContentByTag("设计.worker集.草稿")?.trim() ? "设计.worker集.草稿" : null; if (targetTag) { bb.write({ tag: targetTag, content: mergeContextOrderIntoWorkerSetJson(workerSetRaw, next), source: "user", }); } } else { // 尚无规格:至少落下排序表,供细化终稿合并 bb.write({ tag: CONTEXT_ORDER_TAG, content: serialized, source: "user", }); } if (s.bookId) this.persist(s); return this.toView(sessionId); } /** * 打开 Book:优先恢复磁盘快照;无快照则新建 Session。 * 若该 Book 已在内存中,直接返回当前视图。 */ async openBook(bookId: string): Promise { const book = getBook(bookId); if (!book) throw new Error("Book 不存在"); const inMemory = this.getActiveSessionForBook(bookId); if (inMemory) return inMemory; const snapshot = loadBookSession(bookId); if (snapshot && skillPacksMatch(book, persistedSkillPackId(snapshot))) { return this.restoreFromSnapshot(snapshot); } const legacyPreselect = bookSkillPackId(book); return this.createForBook(bookId, legacyPreselect); } async createForBook( bookId?: string, preselectSkillId?: string, selectedRecipeId?: string, opts?: { creationMode?: "recipe" | "dictate" }, ): Promise { const id = randomUUID(); const messages: ChatMessage[] = []; const book = bookId ? getBook(bookId) : null; const initialSession = createSession("default"); initialSession.id = id; const trackingRef: LlmTrackingRef = { current: { sessionId: id, bookId, bookTitle: book?.title, orchestratorId: preselectSkillId ?? (book ? bookSkillPackId(book) : undefined), }, }; const managed: ManagedSession = { runtime: null as unknown as PhaseRuntime, messages, bookId, trackingRef, branchState: createMessageBranchState(), }; const onMessage = this.buildOnMessageHandler(() => managed); const thinkingHandlers = this.buildStreamHandlers(id, () => managed); const llm = createDefaultMainAgentLlm(trackingRef); const runtime = new PhaseRuntime({ autoStubWorker: !hasRealLlmConfig(), llm, onMessage, ...thinkingHandlers, initialSession, }); managed.runtime = runtime; if (preselectSkillId) { await runtime.startWithOrchestrator(preselectSkillId); } else { await runtime.start(); } const useDictate = opts?.creationMode === "dictate"; await this.refreshRecipeOptions( managed, useDictate ? "dictate" : "recipe", ); if (selectedRecipeId?.trim()) { await this.applySelectedRecipe(managed, selectedRecipeId.trim(), { seedFlow: !useDictate, }); } if (useDictate) { this.enterDictateMode(managed); } else { await this.maybeKickAgentAfterDemand(runtime); } recordPreMessageCheckpoint( managed.branchState, 0, this.captureCheckpoint(managed), ); this.sessions.set(id, managed); if (bookId) { this.activeBookSessions.set(bookId, id); appendBookSession(bookId, id); this.persist(managed); } 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 = `Boss 直聘模式:直接说你想要的体验与设定。我会整理进产物;变量与映射用工具钉死。对话过长时可清空对话(产物保留)。${recipeLine}`; s.messages.push({ id: randomUUID(), role: "system", text: welcome, createdAt: new Date().toISOString(), kind: "orchestrator_prompt", actor: "orchestrator", title: "Boss直聘", 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 { 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 { const llm = createDefaultMainAgentLlm(s.trackingRef); let dialogue = extractDictateDialogue(s.messages); const recipeView = resolveSelectedRecipeView( s.runtime.getBlackboard(), s.recipeOptions, ); const recipeBrief = await resolveDictateRecipeBriefForSession({ skillName: s.runtime.getActiveSkill()?.name, recipeView, recipeOptions: s.recipeOptions, }); const result = await runDictateTurn({ llm, dialogue, recipeName: recipeView?.name, recipeBrief, 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, }, }); }, readTag: (tag) => s.runtime.getBlackboard().getContentByTag(tag), writeTag: (tag, content) => { const board = s.runtime.getBlackboard(); board.write({ tag, content, source: "agent", metadata: { ...(board.getLatestByTag(tag)?.metadata ?? {}), [DICTATE_ORDER_META_KEY]: defaultDictateOrder(tag), }, }); }, 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) => { recordPreMessageCheckpoint( s.branchState, s.messages.length, this.captureCheckpoint(s), ); 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); recordPreMessageCheckpoint( s.branchState, s.messages.length, this.captureCheckpoint(s), ); 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, recipeId: string, ): Promise { const s = this.require(sessionId); await this.refreshRecipeOptions(s); await this.applySelectedRecipe(s, recipeId); if (s.bookId) this.persist(s); return this.toView(sessionId); } get(id: string): SessionView | null { const s = this.sessions.get(id); if (!s) return null; if (s.runtime.recoverOrphanedRun()) this.persist(s); return this.toView(id); } getActiveSessionForBook(bookId: string): SessionView | null { const sessionId = this.activeBookSessions.get(bookId); if (!sessionId) return null; return this.get(sessionId); } /** * 保存快照。 * - instance:实例化后的对象(情境/规则/角色设定等),不含 run 轮次状态 * - run:运行存档,完整进度 */ saveGameSnapshot( sessionId: string, label: string, kind: SnapshotKind = "run", note?: string, ): RunSnapshotMeta { const s = this.require(sessionId); if (!s.bookId) throw new Error("仅绑定作品时可存档"); const book = getBook(s.bookId); if (!book) throw new Error("Book 不存在"); const trimmed = label.trim(); if (!trimmed) throw new Error("请输入存档名称"); let runtimeSession = structuredClone(s.runtime.getSession()); let blackboardItems = s.runtime.getBlackboard().exportItems(); 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 集,或(转述)先写入产物,才能保存定稿"); } ({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({ runtimeSession, blackboardItems, })); } const skillPackId = sessionSkillPackId(runtimeSession) ?? bookSkillPackId(book) ?? ""; const snapshot: RunSnapshot = { version: 1, id: randomUUID(), bookId: s.bookId, label: trimmed, kind, instanceId: kind === "run" ? this.currentPlayInstanceId(s) : undefined, orchestratorId: skillPackId, runtimeSession, blackboardItems, messages: s.messages.map((m) => ({ ...m })), createdAt: new Date().toISOString(), note: note?.trim() || undefined, }; saveRunSnapshotFile(snapshot); return toRunSnapshotMeta(snapshot); } /** 列出某 Book 的全部存档(不含内部「当前游玩」工作副本) */ listGameSnapshots(bookId: string): RunSnapshotMeta[] { if (!getBook(bookId)) throw new Error("Book 不存在"); return listRunSnapshots(bookId).filter((s) => s.id !== PLAY_WORKING_SNAPSHOT_ID); } /** 仅游玩 run 存档(UI 抽屉用) */ listPlaySnapshots(bookId: string): RunSnapshotMeta[] { return this.listGameSnapshots(bookId).filter((s) => s.kind === "run"); } /** 保存游玩进度(创作靠 session.json 自动续作,不手动存 instance) */ savePlaySnapshot( sessionId: string, label: string, note?: string, ): RunSnapshotMeta { const s = this.require(sessionId); const session = s.runtime.getSession(); if (!canEnterPlay(session)) { throw new Error("须先验收 Worker 集,才能保存游玩存档"); } if (inferLifecycleStage(session) !== "play" || !isPlayLayerActive(session.slots)) { throw new Error("请先切换到「游玩」再保存游玩存档"); } return this.saveGameSnapshot(sessionId, label, "run", note); } /** 从已落档产物开一条新的游玩线(清空 run 层状态) */ async startNewPlayRun( sessionId: string, instanceId?: string, ): Promise { const s = this.require(sessionId); if (!s.bookId) throw new Error("仅绑定作品时可新建游玩"); this.persistCreationIfDesign(s); s.runtime.prepareEnterPlay(); if (!canEnterPlay(s.runtime.getSession())) { throw new Error("须先验收 Worker 集、完成收口,或(转述)先写入产物并落档,才能开始游玩"); } this.archivePlayWorking(s.bookId); this.enterPlayLayer(s, { resume: false, instanceId }); return this.toView(sessionId); } /** 进入游玩层:resume 时恢复「当前游玩」,否则从产物新开一条聊天 */ private enterPlayLayer( s: ManagedSession, opts: { resume: boolean; instanceId?: string }, ): void { this.persistCreationIfDesign(s); s.runtime.prepareEnterPlay(); const session = s.runtime.getSession(); if (!canEnterPlay(session)) { throw new Error("实例尚未就绪,无法进入游玩"); } if ( opts.resume && isPlayLayerActive(session.slots) && inferLifecycleStage(session) === "play" ) { this.sanitizePlayHitl(s); return; } if (s.bookId) this.ensureInstanceSnapshot(s); if (opts.resume && s.bookId) { const working = loadRunSnapshotFile(s.bookId, PLAY_WORKING_SNAPSHOT_ID); if (working?.kind === "run") { this.restoreSnapshotInPlace(s, working, "play"); return; } } this.beginNewPlayFromInstance(s, opts.instanceId); if (s.bookId) this.savePlayWorking(s); } /** 回到创作层:先把当前聊天写入「当前游玩」,再恢复创作过程 */ private enterDesignLayer(s: ManagedSession): void { const session = s.runtime.getSession(); if ( inferLifecycleStage(session) === "design" && !isPlayLayerActive(session.slots) ) { s.runtime.setLifecycleStage("design"); return; } if (s.bookId && isPlayLayerActive(session.slots)) { this.savePlayWorking(s); } this.restoreCreationFromDisk(s); } 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 ? instanceId ? this.loadInstanceSnapshot(s.bookId, instanceId) : this.latestInstanceSnapshot(s.bookId) : null; if (instance) { runtimeSession = structuredClone(instance.runtimeSession); blackboardItems = structuredClone(instance.blackboardItems); } else { runtimeSession = structuredClone(s.runtime.getSession()); blackboardItems = s.runtime.getBlackboard().exportItems(); } ({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({ runtimeSession, blackboardItems, })); runtimeSession.id = keepId; runtimeSession.slots = { ...runtimeSession.slots, uiLifecycleStage: "play", [SLOT_PLAY_LAYER_ACTIVE]: true, [SLOT_PLAY_INSTANCE_ID]: instance?.id, startupCompleted: true, }; runtimeSession.phase = "waiting_user"; runtimeSession.waitingReason = { kind: "input" }; runtimeSession.pendingArtifactId = undefined; runtimeSession.pendingDecision = undefined; runtimeSession.currentWorkerId = undefined; runtimeSession.resumeContext = undefined; s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems); // 新开一局(未点名旧定稿):用当前创作的设计.开场白覆盖快照里过期的开场 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, content: appendDialogueHistoryTurn("", { role: "助手", text: opening }), source: "runtime", }); s.messages = [this.playOpeningMessage(opening)]; } else { s.messages = []; } 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, layer: LifecycleStage, ): void { const keepId = s.runtime.getSession().id; let runtimeSession = structuredClone(snapshot.runtimeSession); let blackboardItems = structuredClone(snapshot.blackboardItems); if (snapshot.kind === "instance" || layer === "design") { ({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({ runtimeSession, blackboardItems, })); } runtimeSession.id = keepId; runtimeSession.slots = { ...runtimeSession.slots, uiLifecycleStage: layer, [SLOT_PLAY_LAYER_ACTIVE]: layer === "play" ? true : undefined, [SLOT_PLAY_INSTANCE_ID]: layer === "play" ? snapshot.instanceId || (typeof runtimeSession.slots[SLOT_PLAY_INSTANCE_ID] === "string" ? runtimeSession.slots[SLOT_PLAY_INSTANCE_ID] : undefined) : undefined, startupCompleted: true, }; if (layer === "play") { runtimeSession.phase = runtimeSession.phase === "done" ? "waiting_user" : runtimeSession.phase; if (!runtimeSession.waitingReason) { runtimeSession.waitingReason = { kind: "input" }; } } else { if (runtimeSession.phase === "done") { runtimeSession.phase = "waiting_user"; } if (!runtimeSession.waitingReason) { runtimeSession.waitingReason = { kind: "input" }; } runtimeSession.pendingArtifactId = undefined; runtimeSession.pendingDecision = undefined; runtimeSession.currentWorkerId = undefined; } s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems); s.messages = snapshot.messages.map((m) => ({ ...m })) as ChatMessage[]; s.branchState = createMessageBranchState(); 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(验收 / 确认下一步 / 修订) */ private sanitizePlayHitl(s: ManagedSession): void { const session = s.runtime.getSession(); const play = isPlayLayerActive(session.slots) || inferLifecycleStage(session) === "play"; if (!play) return; const kind = session.waitingReason?.kind; if ( kind !== "review_artifact" && kind !== "approve_step" && kind !== "revision" && kind !== "next_intent" && kind !== "pick_creation_step" && kind !== "worker_questions" ) { return; } session.phase = "waiting_user"; session.waitingReason = { kind: "input" }; session.pendingArtifactId = undefined; session.pendingDecision = undefined; session.currentWorkerId = undefined; session.resumeContext = undefined; session.updatedAt = new Date().toISOString(); } private savePlayWorking(s: ManagedSession): void { if (!s.bookId) return; const book = getBook(s.bookId); const runtimeSession = structuredClone(s.runtime.getSession()); const snapshot: RunSnapshot = { version: 1, id: PLAY_WORKING_SNAPSHOT_ID, bookId: s.bookId, label: PLAY_WORKING_SNAPSHOT_LABEL, kind: "run", instanceId: this.currentPlayInstanceId(s), orchestratorId: sessionSkillPackId(runtimeSession) ?? (book ? bookSkillPackId(book) : "") ?? "", runtimeSession, blackboardItems: s.runtime.getBlackboard().exportItems(), messages: s.messages.map((m) => ({ ...m })), createdAt: new Date().toISOString(), note: "切换创作/游玩时自动保存的当前聊天", }; saveRunSnapshotFile(snapshot); } private archivePlayWorking(bookId: string): void { const working = loadRunSnapshotFile(bookId, PLAY_WORKING_SNAPSHOT_ID); if (!working) return; if (!working.messages.some((m) => m.role === "user")) return; const stamped: RunSnapshot = { ...working, id: randomUUID(), label: `游玩 ${new Date().toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", })}`, createdAt: new Date().toISOString(), note: "新建游玩前自动保存", }; saveRunSnapshotFile(stamped); } private ensureInstanceSnapshot(s: ManagedSession): void { if (!s.bookId) return; if (this.latestInstanceSnapshot(s.bookId)) return; if (!canEnterPlay(s.runtime.getSession())) return; try { this.saveGameSnapshot( s.runtime.getSession().id, INSTANCE_OPENING_SNAPSHOT_LABEL, "instance", "开玩前自动保存产物", ); } catch (err) { console.warn("[会话] 开玩前保存产物失败", err); } } private currentPlayInstanceId(s: ManagedSession): string | undefined { const raw = s.runtime.getSession().slots[SLOT_PLAY_INSTANCE_ID]; if (typeof raw === "string" && raw.trim()) return raw.trim(); if (!s.bookId) return undefined; return this.latestInstanceSnapshot(s.bookId)?.id; } playWorkingInstanceId(bookId: string): string | undefined { const working = loadRunSnapshotFile(bookId, PLAY_WORKING_SNAPSHOT_ID); if (!working || working.kind !== "run") return undefined; return working.instanceId; } renameGameSnapshot( bookId: string, snapshotId: string, label: string, ): RunSnapshotMeta { if (!getBook(bookId)) throw new Error("Book 不存在"); const trimmed = label.trim(); if (!trimmed) throw new Error("名称不能为空"); const snapshot = loadRunSnapshotFile(bookId, snapshotId); if (!snapshot) throw new Error("存档不存在"); const next: RunSnapshot = { ...snapshot, label: trimmed }; saveRunSnapshotFile(next); return toRunSnapshotMeta(next); } private latestInstanceSnapshot(bookId: string): RunSnapshot | null { const meta = this.listGameSnapshots(bookId).find((s) => s.kind === "instance"); if (!meta) return null; return loadRunSnapshotFile(bookId, meta.id); } private loadInstanceSnapshot(bookId: string, snapshotId: string): RunSnapshot { const snapshot = loadRunSnapshotFile(bookId, snapshotId); if (!snapshot) throw new Error("产物不存在"); if (snapshot.kind !== "instance") throw new Error("该存档不是产物定稿"); return snapshot; } private restoreCreationFromDisk(s: ManagedSession): void { const keepId = s.runtime.getSession().id; if (!s.bookId) { this.clearPlayLayerOnSession(s, keepId); return; } const snap = loadBookSession(s.bookId); if (!snap?.runtimeSession) { this.clearPlayLayerOnSession(s, keepId); return; } const runtimeSession = structuredClone(snap.runtimeSession); runtimeSession.id = keepId; runtimeSession.slots = { ...runtimeSession.slots, uiLifecycleStage: "design", [SLOT_PLAY_LAYER_ACTIVE]: undefined, }; s.runtime.restoreFromCheckpoint(runtimeSession, snap.blackboardItems); s.messages = snap.messages.map((m) => ({ ...m })) as ChatMessage[]; s.branchState = this.deserializeBranchState(snap.messageBranchState); } private clearPlayLayerOnSession(s: ManagedSession, keepId: string): void { const runtimeSession = structuredClone(s.runtime.getSession()); runtimeSession.id = keepId; runtimeSession.slots = { ...runtimeSession.slots, uiLifecycleStage: "design", [SLOT_PLAY_LAYER_ACTIVE]: undefined, }; s.runtime.restoreFromCheckpoint( runtimeSession, s.runtime.getBlackboard().exportItems(), ); } private playOpeningMessage(text: string): ChatMessage { return { id: randomUUID(), role: "system", text, createdAt: new Date().toISOString(), kind: "system_info", actor: "narrator", title: "开场白", body: text, }; } /** 读档:产物开玩,游玩存档续玩。不覆盖创作过程。 */ async loadGameSnapshot(bookId: string, snapshotId: string): Promise { const snapshot = loadRunSnapshotFile(bookId, snapshotId); if (!snapshot) throw new Error("存档不存在"); const book = getBook(bookId); if (!book) throw new Error("Book 不存在"); if (!skillPacksMatch(book, runSnapshotSkillPackId(snapshot))) { throw new Error("存档与当前作品 skill 包不匹配,无法读档"); } let sessionId = this.activeBookSessions.get(bookId); if (!sessionId) { await this.openBook(bookId); sessionId = this.activeBookSessions.get(bookId); } if (!sessionId) throw new Error("无法打开作品"); const s = this.require(sessionId); if (snapshot.kind === "instance") { this.persistCreationIfDesign(s); if (isPlayLayerActive(s.runtime.getSession().slots)) { this.archivePlayWorking(bookId); } this.beginNewPlayFromInstance(s, snapshot.id); this.savePlayWorking(s); return this.toView( sessionId, true, `已用产物「${snapshot.label}」开玩。创作流程仍在「创作」里。`, ); } this.persistCreationIfDesign(s); this.restoreSnapshotInPlace(s, snapshot, "play"); this.savePlayWorking(s); return this.toView( sessionId, true, `已从存档「${snapshot.label}」继续游玩。`, ); } /** 删除单个存档 */ deleteGameSnapshot(bookId: string, snapshotId: string): void { if (!getBook(bookId)) throw new Error("Book 不存在"); if (!deleteRunSnapshotFile(bookId, snapshotId)) { throw new Error("存档不存在"); } } /** 删除 Book 时清理内存中的会话 */ dropBookSessions(bookId: string): void { for (const [sessionId, managed] of this.sessions) { if (managed.bookId === bookId) { this.sessions.delete(sessionId); } } this.activeBookSessions.delete(bookId); } /** 将当前 API / 预设设置应用到所有活跃会话 */ reloadAllLlms(): number { let count = 0; for (const s of this.sessions.values()) { const llm = reloadDefaultMainAgentLlm(s.trackingRef); s.runtime.reloadLlm(llm, !hasRealLlmConfig()); count += 1; } return count; } async sendMessage( id: string, text: string, opts?: { answers?: QuestionAnswer[] }, ): Promise { const s = this.require(id); this.sanitizePlayHitl(s); const waitingBefore = s.runtime.getSession().waitingReason; if ( inferLifecycleStage(s.runtime.getSession()) === "play" && !isPlayLayerActive(s.runtime.getSession().slots) && waitingBefore?.kind === "input" ) { this.enterPlayLayer(s, { resume: true }); if (s.bookId) this.persist(s); } this.clearAgentThinking(id); recordPreMessageCheckpoint( s.branchState, s.messages.length, this.captureCheckpoint(s), ); const reason = s.runtime.getSession().waitingReason; const trimmed = text.trim(); const sidecarAnswers = reason?.kind === "review_artifact" && reason.questions?.length && opts?.answers?.length ? opts.answers.filter((a) => a.text?.trim() && a.text !== "(未答)") : []; // next_intent 允许留空:不塞空白气泡 if (trimmed || reason?.kind !== "next_intent" || sidecarAnswers.length) { let recordText = trimmed || text; let displayBody: string | undefined; if (sidecarAnswers.length && reason?.kind === "review_artifact") { const qs = normalizeQuestions(reason.questions ?? []); recordText = formatQuestionAnswersForAi( qs, sidecarAnswers, trimmed || undefined, ); displayBody = formatQuestionAnswersForDisplay( qs, sidecarAnswers, trimmed || undefined, ); } const userMsg = this.msg("user", recordText); if (displayBody) userMsg.body = displayBody; s.messages.push(userMsg); this.syncCreationDialogue(s); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } 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") { // 先把卡上追问(问+选项+补充)写入「用户.worker答复」,再按意见改产物 if (sidecarAnswers.length && reason.questions?.length) { const qaText = formatQuestionAnswersForAi( normalizeQuestions(reason.questions), sidecarAnswers, trimmed || undefined, ); await s.runtime.resolveSidecarQuestions(qaText); } if (trimmed || sidecarAnswers.length) { await s.runtime.rejectArtifact( trimmed || "按追问作答更新产物", ); } } else { await s.runtime.submitInput(trimmed || text); } }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 发送消息失败", detail); await this.recoverFailedRun(s, detail); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** * 结构化追问作答:UI 可省略结构,发给 AI 必须含问+答。 * displayText 进气泡;aiText 进 runtime / 黑板。 * review_artifact 挂载题:作答后仍停留在验收态(可再 Accept)。 */ async answerQuestions( id: string, answers: QuestionAnswer[], note?: string, ): Promise { const s = this.require(id); const reason = s.runtime.getSession().waitingReason; const isSidecar = reason?.kind === "review_artifact" && Boolean(reason.questions?.length); const questions = reason?.kind === "worker_questions" ? reason.questions : reason?.kind === "input" ? (reason.questions ?? []) : reason?.kind === "review_artifact" ? (reason.questions ?? []) : []; if (!questions.length) { throw new Error("当前没有待回答的结构化追问"); } const aiText = formatQuestionAnswersForAi(questions, answers, note); const displayText = formatQuestionAnswersForDisplay(questions, answers, note); this.clearAgentThinking(id); recordPreMessageCheckpoint( s.branchState, s.messages.length, this.captureCheckpoint(s), ); const userMsg = this.msg("user", aiText); userMsg.body = displayText; s.messages.push(userMsg); this.syncCreationDialogue(s); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); try { const owned = await this.runExclusive(id, s, async () => { if (isSidecar) { await s.runtime.resolveSidecarQuestions(aiText); // 作答必须写回产物,否则验收后进度空转、追问在美学/交互间打转 await s.runtime.rejectArtifact("按追问作答更新产物"); } else { await s.runtime.submitInput(aiText); } }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 回答追问失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** * 跳过追问: * - review_artifact 挂载题:只收起题,留在验收 * - input / worker_questions:提交跳过句并继续 */ async skipQuestions(id: string): Promise { const s = this.require(id); const reason = s.runtime.getSession().waitingReason; const skipText = "(用户跳过追问)请基于已有信息继续。"; if (reason?.kind === "review_artifact" && reason.questions?.length) { this.clearAgentThinking(id); s.messages.push(this.msg("user", "跳过可选追问")); try { const owned = await this.runExclusive(id, s, () => s.runtime.resolveSidecarQuestions(), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); s.messages.push(this.msg("system", formatRuntimeError(detail))); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } if ( (reason?.kind === "input" && reason.questions?.length) || reason?.kind === "worker_questions" ) { return this.sendMessage(id, skipText); } throw new Error("当前没有可跳过的追问"); } /** 编辑用户消息 → 新分支 + 从该点重跑 */ async editMessage(id: string, messageId: string, newText: string): Promise { const s = this.require(id); const trimmed = newText.trim(); if (!trimmed) throw new Error("内容不能为空"); const idx = s.messages.findIndex((m) => m.id === messageId); if (idx < 0) throw new Error("消息不存在"); const target = s.messages[idx]; if (target.role !== "user") throw new Error("只能编辑用户消息"); const checkpoint = s.branchState.preMessageCheckpoints[idx] ?? this.captureCheckpoint(s); const branch = ensureBranchForEdit(s.branchState, s.messages, idx, checkpoint); s.runtime.restoreFromCheckpoint(checkpoint.runtimeSession, checkpoint.blackboardItems); s.messages = s.messages.slice(0, idx); const variantIndex = branch.variants.length; const userMsg = createUserVariantMessage(trimmed, branch.groupId, variantIndex) as ChatMessage; appendBranchVariant(branch, userMsg, checkpoint); s.messages.push(userMsg); this.syncCreationDialogue(s); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); try { const owned = await this.runExclusive(id, s, async () => { const sess = s.runtime.getSession(); const reason = sess.waitingReason; const inPlay = isPlayLayerActive(sess.slots) || inferLifecycleStage(sess) === "play"; if ( this.isDictateMode(s) && reason?.kind === "input" && !inPlay ) { await this.runDictateTurnForSession(s, trimmed); return; } await s.runtime.submitInput(trimmed); }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); s.messages.push(this.msg("system", formatRuntimeError(detail))); } updateActiveBranchVariant( branch, s.messages.slice(branch.anchorIndex), this.captureCheckpoint(s), ); for (const m of s.messages.slice(branch.anchorIndex)) { m.branchGroupId = branch.groupId; m.branchIndex = branch.activeIndex; m.branchTotal = branch.variants.length; } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** 刷新 Worker / Skill 回复 → 重 roll */ async refreshMessage(id: string, messageId: string): Promise { const s = this.require(id); const idx = s.messages.findIndex((m) => m.id === messageId); if (idx < 0) throw new Error("消息不存在"); const target = s.messages[idx]; if (!isRefreshableMessage(target)) { throw new Error("此消息不支持刷新"); } const workerSnap = s.runtime.getLastWorkerRunSnapshot(); const workerId = target.actor ?? workerSnap?.workerId; if (!workerId) throw new Error("无法识别要刷新的 Worker"); const checkpoint = workerSnap ?? s.branchState.preMessageCheckpoints[idx]; if (!checkpoint) { throw new Error("缺少重 roll 快照,请尝试编辑上一条用户消息"); } const branch = ensureBranchForRefresh( s.branchState, s.messages, idx, { runtimeSession: checkpoint.runtimeSession, blackboardItems: checkpoint.blackboardItems, }, ); s.runtime.restoreFromCheckpoint(checkpoint.runtimeSession, checkpoint.blackboardItems); s.messages = s.messages.slice(0, idx); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); try { const owned = await this.runExclusive(id, s, () => s.runtime.rerunWorker(workerId), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); s.messages.push(this.msg("system", formatRuntimeError(detail))); } const newIndex = branch.variants.length; branch.variants.push({ messages: structuredClone(s.messages.slice(idx)), checkpoint: this.captureCheckpoint(s), }); branch.activeIndex = newIndex; for (const m of s.messages.slice(branch.anchorIndex)) { m.branchGroupId = branch.groupId; m.branchIndex = branch.activeIndex; m.branchTotal = branch.variants.length; } this.persist(s); return this.toView(id); } /** 中止当前生成并重跑卡住的 worker / 总管 */ async abortAndRetry(id: string): Promise { const s = this.require(id); const session = s.runtime.getSession(); if (session.phase !== "running" || session.waitingReason) { throw new Error("当前没有正在执行的任务"); } const cutoff = s.runMessageCutoff ?? s.messages.length; s.messages = s.messages.slice(0, cutoff); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); try { const owned = await this.runExclusive(id, s, async () => { await s.runtime.retryStuckRun(); }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 停止并重试失败", detail); await this.recoverFailedRun(s, detail); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** 中止当前生成,不重跑;回到生成前的等待面或分层图。 */ async abortRun(id: string): Promise { const s = this.require(id); const session = s.runtime.getSession(); if (session.phase !== "running" || session.waitingReason) { return this.toView(id); } const cutoff = s.runMessageCutoff ?? s.messages.length; s.messages = s.messages.slice(0, cutoff); try { const owned = await this.runExclusive(id, s, async () => { s.runtime.abortCurrentRun(); }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 停止生成失败", detail); await this.recoverFailedRun(s, detail); } s.messages.push(this.msg("system", "已停止生成。")); this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** 删除消息及其后的对话(仅截断,不重生成) */ async deleteMessage(id: string, messageId: string): Promise { const s = this.require(id); const idx = s.messages.findIndex((m) => m.id === messageId); if (idx < 0) throw new Error("消息不存在"); const checkpoint = s.branchState.preMessageCheckpoints[idx]; if (!checkpoint && idx > 0) throw new Error("无法删除此消息"); if (checkpoint) { s.runtime.restoreFromCheckpoint( checkpoint.runtimeSession, checkpoint.blackboardItems, ); } s.messages = s.messages.slice(0, idx); for (const key of Object.keys(s.branchState.preMessageCheckpoints)) { const n = Number(key); if (n >= idx) delete s.branchState.preMessageCheckpoints[n]; } this.syncCreationDialogue(s); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); this.persist(s); return this.toView(id); } /** * 从这里重开:丢掉该条及之后内容,并按本轮用户输入重新生成(重 roll)。 * 用户消息 → 同文编辑重跑;可刷新的 Worker 回复 → refresh;其余 → 回溯到上一轮用户输入再 edit。 */ async restartFromMessage(id: string, messageId: string): Promise { const s = this.require(id); const idx = s.messages.findIndex((m) => m.id === messageId); if (idx < 0) throw new Error("消息不存在"); const target = s.messages[idx]; if (target.role === "user") { return this.editMessage(id, messageId, target.text); } if (isRefreshableMessage(target)) { return this.refreshMessage(id, messageId); } const userIdx = findPrecedingUserIndex(s.messages, idx); if (userIdx < 0) { return this.deleteMessage(id, messageId); } const userMsg = s.messages[userIdx]; return this.editMessage(id, userMsg.id, userMsg.text); } /** 左右切换同位消息版本(swipe / branch) */ async switchMessageVariant( id: string, messageId: string, direction: "prev" | "next", ): Promise { const s = this.require(id); const idx = s.messages.findIndex((m) => m.id === messageId); if (idx < 0) throw new Error("消息不存在"); const target = s.messages[idx]; const groupId = target.branchGroupId ?? target.id; const delta = direction === "prev" ? -1 : 1; const switched = switchBranchVariant(s.branchState, s.messages, groupId, delta); if (!switched) throw new Error("没有更多版本"); s.messages = switched.messages as ChatMessage[]; s.runtime.restoreFromCheckpoint( switched.checkpoint.runtimeSession, switched.checkpoint.blackboardItems, ); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); this.persist(s); return this.toView(id); } async approve( id: string, stepParams?: Record, ): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, () => s.runtime.approve(stepParams), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 确认开干失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); } return this.toView(id); } async confirmIntake(id: string): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, () => s.runtime.confirmIntake(), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 确认需求失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } return this.toView(id); } async accept( id: string, opts?: { openingIndex?: number }, ): Promise { const s = this.require(id); const pendingId = s.runtime.getSession().pendingArtifactId; const waiting = s.runtime.getSession().waitingReason; const hadSidecarQuestions = waiting?.kind === "review_artifact" && Boolean(waiting.questions?.length); const artifact = pendingId ? s.runtime.getSession().artifacts.find((a) => a.id === pendingId) : undefined; const stageBefore = inferLifecycleStage(s.runtime.getSession()); try { const owned = await this.runExclusive(id, s, () => s.runtime.acceptArtifact(undefined, opts), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 接受产物失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); this.persist(s); return this.toView(id); } // 接受产物 = 不再用追问完善;询问模块随 waitingReason 清除而收起 if (hadSidecarQuestions) { s.messages.push( this.msg("system", "[验收] 已接受产物,可选追问已收起(无需再完善)。"), ); } if (artifact) { if (stageBefore === "design") { // 创作验收:messages 全量保留供用户浏览;仅同步拼给 AI 的「创作.对话」 this.syncCreationDialogue(s); } else { foldRunProcessMessages(s.messages, artifact.workerId); } } this.persist(s); return this.toView(id); } /** 拒绝当前待确认步骤或待验收产物(无说明时触发重新来 / 回到总管) */ async reject(id: string, reason?: string): Promise { const s = this.require(id); const waiting = s.runtime.getSession().waitingReason; try { const owned = await this.runExclusive(id, s, async () => { if (waiting?.kind === "approve_step") { await s.runtime.leaveCreationStep(); } else if (waiting?.kind === "review_artifact") { await s.runtime.rejectArtifact(reason ?? "用户要求重新来"); } else { throw new Error("当前没有可拒绝的确认或验收"); } }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 打回失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** 离开当前技能步,回到分层图点选 */ async leaveStep(id: string): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, () => s.runtime.leaveCreationStep(), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 返回节点失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } /** 开场白等步骤上发现漏了前序节点:再进流程编排,不整局重开 */ async replan(id: string, reason?: string): Promise { const s = this.require(id); const note = reason?.trim() || "回到流程编排,追加或改排节点。"; this.clearAgentThinking(id); recordPreMessageCheckpoint( s.branchState, s.messages.length, this.captureCheckpoint(s), ); s.messages.push(this.msg("user", note)); this.syncCreationDialogue(s); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); try { const owned = await this.runExclusive(id, s, () => s.runtime.replanCreationFlow(note), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 再编排失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } this.syncCreationDialogue(s); this.persist(s); return this.toView(id); } async pickStep( id: string, stepId: string, stepParams?: Record, reenter?: boolean, ): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, async () => { await this.confirmPendingFlowPlan(s); const waiting = s.runtime.getSession().waitingReason?.kind; if (waiting !== "pick_creation_step") { throw new Error("当前不是点选节点的时机"); } await s.runtime.pickCreationStep(stepId, stepParams, { reenter: reenter === true, }); }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 点选节点失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); } this.persist(s); return this.toView(id); } async getStepArtifact( id: string, stepId: string, ): Promise<{ stepId: string; name: string; tag: string | null; content: string; }> { const s = this.require(id); const catalog = s.moduleCatalog; const flow = parseCreationFlow( s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG), ); if (!flow) throw new Error("还没有工作流计划"); const cleaned = stripReviseSteps(flow); const step = findStepByRef(cleaned, stepId.trim()); if (!step) throw new Error(`找不到节点:${stepId}`); const mod = findModuleByName(catalog, step.name); const tag = mod?.artifact?.trim() || null; const content = tag ? s.runtime.getBlackboard().getContentByTag(tag)?.trim() || "" : ""; return { stepId: step.id, name: step.name, tag, content }; } async spawnStep(id: string, moduleName: string): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, async () => { await this.confirmPendingFlowPlan(s); const waiting = s.runtime.getSession().waitingReason?.kind; if (waiting !== "pick_creation_step") { throw new Error("当前不是追加节点的时机"); } await s.runtime.spawnCreationStep(moduleName); }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 追加节点失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); } this.persist(s); return this.toView(id); } async deleteStep(id: string, stepId: string): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, async () => { await this.confirmPendingFlowPlan(s); const waiting = s.runtime.getSession().waitingReason?.kind; if (waiting !== "pick_creation_step") { throw new Error("当前不是删除产物的时机"); } await s.runtime.deleteCreationStep(stepId); }); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); console.error("[会话] 删除产物失败", detail); s.messages.push(this.msg("system", formatRuntimeError(detail))); } this.persist(s); return this.toView(id); } /** 已点进技能步、尚未验收:可退回分层图(误点可增殖节点时用) */ private canOfferLeaveCreationStep( s: ManagedSession, reason: WaitingReason | undefined, ): boolean { const session = s.runtime.getSession(); if ( isPlayLayerActive(session.slots) || inferLifecycleStage(session) === "play" ) { return false; } const kind = reason?.kind; if (session.phase === "running" && !kind) { return Boolean( s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG)?.trim(), ); } if (kind === "review_artifact") { return Boolean( s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG)?.trim(), ); } if ( kind !== "worker_questions" && kind !== "input" && kind !== "revision" && kind !== "approve_step" ) { return false; } const stepId = s.runtime.getBlackboard().getContentByTag(CREATION_CURRENT_STEP_TAG)?.trim() || session.currentStepId?.trim() || ""; return Boolean(stepId); } /** 流程编排还停在核对态时,点节点先收下计划再进入 */ private async confirmPendingFlowPlan(s: ManagedSession): Promise { const session = s.runtime.getSession(); if (session.waitingReason?.kind !== "review_artifact") return; const art = session.pendingArtifactId ? session.artifacts.find((a) => a.id === session.pendingArtifactId) : undefined; if (art?.workerId !== DESIGN_FLOW_WORKER_ID) { throw new Error("当前不是点选节点的时机"); } await s.runtime.acceptArtifact(); } async runOutline(id: string): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, () => s.runtime.submitDecision( createDecision({ action: "run_worker", reason: "根据创作简报生成大纲", workerId: "outline", requiresApproval: true, }), ), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); s.messages.push(this.msg("system", formatRuntimeError(detail))); } return this.toView(id); } async finish(id: string): Promise { const s = this.require(id); try { const owned = await this.runExclusive(id, s, () => s.runtime.submitDecision( createDecision({ action: "finish", reason: "创作流程结束", requiresApproval: false, }), ), ); if (!owned) return this.toView(id); } catch (err) { const detail = err instanceof Error ? err.message : String(err); s.messages.push(this.msg("system", formatRuntimeError(detail))); } return this.toView(id); } private async restoreFromSnapshot( snapshot: PersistedBookSession, ): Promise { return this.mountRestoredSession({ sessionId: snapshot.sessionId, bookId: snapshot.bookId, skillPackId: persistedSkillPackId(snapshot) ?? "", runtimeSession: snapshot.runtimeSession, blackboardItems: snapshot.blackboardItems, messages: snapshot.messages.map((m) => ({ ...m })) as ChatMessage[], messageBranchState: snapshot.messageBranchState, resumeHint: "已从上次进度恢复,可继续创作。", }); } private async mountRestoredSession(params: { sessionId: string; bookId: string; skillPackId: string; runtimeSession: RuntimeSession; blackboardItems: import("../types/blackboard.js").BlackboardItem[]; messages: ChatMessage[]; messageBranchState?: import("../types/book-session.js").PersistedMessageBranchState; resumeHint: string; }): Promise { const book = getBook(params.bookId); const trackingRef: LlmTrackingRef = { current: { sessionId: params.sessionId, bookId: params.bookId, bookTitle: book?.title, orchestratorId: params.skillPackId || undefined, }, }; let managedRef: ManagedSession | null = null; const onMessage = this.buildOnMessageHandler(() => managedRef); const thinkingHandlers = this.buildStreamHandlers( params.sessionId, () => managedRef, ); const runtime = new PhaseRuntime({ autoStubWorker: !hasRealLlmConfig(), llm: createDefaultMainAgentLlm(trackingRef), onMessage, ...thinkingHandlers, initialSession: params.runtimeSession, initialBlackboardItems: params.blackboardItems, }); await runtime.ensureAvailableSkills(); const keep = normalizeContextTraceKeepLatest( loadAppSettings().contextTraceKeepLatest, ); managedRef = { runtime, messages: pruneContextTraces(params.messages, keep), bookId: params.bookId, trackingRef, branchState: this.deserializeBranchState(params.messageBranchState), }; this.sessions.set(params.sessionId, managedRef); this.activeBookSessions.set(params.bookId, params.sessionId); appendBookSession(params.bookId, params.sessionId); await this.refreshRecipeOptions(managedRef); runtime.recoverOrphanedRun(); this.persist(managedRef); await this.maybeKickAgentAfterDemand(runtime); return this.toView(params.sessionId, true, params.resumeHint); } /** 旧会话:用户已写需求但 Agent 未跑过 → 补 invoke 总管 */ private async maybeKickAgentAfterDemand(runtime: PhaseRuntime): Promise { const session = runtime.getSession(); const active = session.slots.activeSkill as ActiveSkillSnapshot | undefined; if (!active || effectiveStartupMode(active) !== "agent-first") return; if (session.slots.startupCompleted) return; const demandKey = active.startupTargetKey || "用户.需求"; const text = String( session.slots[demandKey] ?? session.slots.lastUserInput ?? "", ).trim(); if (!text) return; const reason = session.waitingReason?.kind; if (reason !== "input" && reason !== "intake") return; try { await runtime.dispatch({ type: "user_submitted_input", payload: { text }, }); } catch (err) { console.error("[会话] 需求后唤醒总管失败", err); } } private buildOnMessageHandler(getManaged: () => ManagedSession | null) { return (text: string) => { const managed = getManaged(); if (!managed) return; recordPreMessageCheckpoint( managed.branchState, managed.messages.length, this.captureCheckpoint(managed), ); const usage = managed.trackingRef.current.pendingUsage; const sessionId = managed.trackingRef.current.sessionId; const liveThinking = sessionId ? this.workerLive.get(sessionId)?.thinking?.trim() : ""; managed.trackingRef.current.pendingUsage = undefined; const pendingReasoning = managed.trackingRef.current.pendingReasoning; managed.trackingRef.current.pendingReasoning = undefined; const classified = text.trim().length > 0 ? classifyAgentMessage(text) : null; const thinking = pendingReasoning || (classified?.kind === "worker_output" || classified?.kind === "worker_questions" ? liveThinking : "") || undefined; const pendingTrace = managed.trackingRef.current.pendingContextTrace; let contextTrace: LlmContextTrace | undefined; if ( pendingTrace && classified && CONTEXT_TRACE_MESSAGE_KINDS.has(classified.kind) ) { const keep = normalizeContextTraceKeepLatest( loadAppSettings().contextTraceKeepLatest, ); if (keep > 0) { contextTrace = pendingTrace; } managed.trackingRef.current.pendingContextTrace = undefined; } managed.messages.push( this.msg("system", text, usage, thinking, contextTrace), ); managed.messages = pruneContextTraces( managed.messages, normalizeContextTraceKeepLatest( loadAppSettings().contextTraceKeepLatest, ), ); this.syncCreationDialogue(managed); if (managed.bookId) { this.syncBookPreview(managed.bookId, managed.messages); this.persist(managed); } }; } /** 创作模式:从全量 messages 拼「创作.对话」(AI 只留最后一次),不改用户可见消息 */ private syncCreationDialogue(s: ManagedSession): void { try { if (inferLifecycleStage(s.runtime.getSession()) !== "design") return; const transcript = buildCreationDialogueTranscript(s.messages); s.runtime.writeCreationDialogue(transcript); } catch (err) { console.warn("[会话] 同步创作对话失败", err); } } /** 游玩中只更新工作副本,不覆盖创作过程 */ private persist(s: ManagedSession): void { if (!s.bookId) return; const book = getBook(s.bookId); if (!book) return; const session = s.runtime.getSession(); if ( isPlayLayerActive(session.slots) || inferLifecycleStage(session) === "play" ) { this.savePlayWorking(s); this.syncBookSkill(s.bookId, session); updateBook(s.bookId, { activeSessionId: session.id }); return; } this.writeBookSession(s); } /** 切到游玩前把当前创作过程写入 session.json */ private persistCreationIfDesign(s: ManagedSession): void { if (!s.bookId) return; if (isPlayLayerActive(s.runtime.getSession().slots)) return; this.writeBookSession(s); } private writeBookSession(s: ManagedSession): void { if (!s.bookId) return; const book = getBook(s.bookId); if (!book) return; const session = s.runtime.getSession(); const skillPackId = sessionSkillPackId(session) ?? (book ? bookSkillPackId(book) : undefined) ?? ""; const snapshot: PersistedBookSession = { version: 1, sessionId: session.id, bookId: s.bookId, orchestratorId: skillPackId || undefined, runtimeSession: session, blackboardItems: s.runtime.getBlackboard().exportItems(), messages: s.messages, messageBranchState: this.serializeBranchState(s.branchState), savedAt: new Date().toISOString(), }; saveBookSession(snapshot); this.syncBookSkill(s.bookId, session); updateBook(s.bookId, { activeSessionId: session.id }); } private syncBookSkill(bookId: string, session: RuntimeSession): void { const snap = session.slots.activeSkill as ActiveSkillSnapshot | undefined; if (!snap?.name) return; try { updateBook(bookId, { activeSkillId: snap.name, activeSkillName: snap.name, }); } catch { /* book may have been deleted */ } } private syncBookPreview(bookId: string, messages: ChatMessage[]): void { const preview = this.previewFromMessages(messages); try { updateBook(bookId, { preview }); } catch { /* book may have been deleted */ } } private previewFromMessages(messages: ChatMessage[]): string { for (let i = messages.length - 1; i >= 0; i--) { const t = messages[i].text?.trim(); if (t) return t.slice(0, 80); } return "等待开始…"; } private clearAgentThinking(sessionId: string): void { this.agentThinkingLive.delete(sessionId); this.agentThinkingClosed.delete(sessionId); this.workerLive.delete(sessionId); } private buildStreamHandlers( sessionId: string, _getManaged: () => ManagedSession | null, ): Pick< import("../runtime/phase-runtime.js").PhaseRuntimeOptions, | "onAgentThinkingDelta" | "onAgentThinkingDone" | "onWorkerStreamStart" | "onWorkerThinkingDelta" | "onWorkerOutputDelta" > { return { onAgentThinkingDelta: (delta) => { const prev = this.agentThinkingClosed.has(sessionId) ? "" : (this.agentThinkingLive.get(sessionId) ?? ""); this.agentThinkingClosed.delete(sessionId); this.agentThinkingLive.set(sessionId, prev + delta); }, onAgentThinkingDone: (text) => { // 正文仍等完整后再渲染;思考收束后继续挂在 liveStream,避免中间只剩「…」 this.agentThinkingClosed.add(sessionId); const done = text.trim(); if (done) this.agentThinkingLive.set(sessionId, done); }, onWorkerStreamStart: (workerId) => { this.workerLive.set(sessionId, { workerId, thinking: "", output: "" }); }, onWorkerThinkingDelta: (workerId, delta) => { const prev = this.workerLive.get(sessionId) ?? { workerId, thinking: "", output: "", }; prev.workerId = workerId; prev.thinking += delta; this.workerLive.set(sessionId, prev); }, onWorkerOutputDelta: (workerId, delta) => { const prev = this.workerLive.get(sessionId) ?? { workerId, thinking: "", output: "", }; prev.workerId = workerId; prev.output += delta; this.workerLive.set(sessionId, prev); }, }; } private buildLiveStreamView(sessionId: string): LiveStreamView | undefined { const worker = this.workerLive.get(sessionId); if (worker) { return { actor: "worker", actorId: worker.workerId, label: formatWorkerDisplayTitle(worker.workerId, "running"), thinking: worker.thinking || undefined, output: worker.output || undefined, }; } const agentThinking = this.agentThinkingLive.get(sessionId); if (agentThinking) { return { actor: "orchestrator", label: "总管 · 思考", thinking: agentThinking, }; } return undefined; } private require(id: string): ManagedSession { const s = this.sessions.get(id); if (!s) throw new Error("会话不存在"); return s; } private beginRun(s: ManagedSession, sessionId: string): number { s.runAbort?.abort(); s.runAbort = new AbortController(); s.runGen = (s.runGen ?? 0) + 1; s.runMessageCutoff = s.messages.length; this.clearAgentThinking(sessionId); return s.runGen; } private isCurrentRun(s: ManagedSession, gen: number): boolean { return s.runGen === gen; } /** true = 本轮仍有效;false = 已被停止并重试取代 */ private async runExclusive( id: string, s: ManagedSession, fn: () => Promise, ): Promise { const gen = this.beginRun(s, id); try { await runWithAbortSignal(s.runAbort!.signal, fn); return this.isCurrentRun(s, gen); } catch (err) { if (isAbortError(err) && !this.isCurrentRun(s, gen)) { return false; } throw err; } finally { if (this.isCurrentRun(s, gen)) { this.clearAgentThinking(id); s.runAbort = undefined; } } } /** 执行失败后离开 running,避免界面一直转圈。 */ private async recoverFailedRun( s: ManagedSession, detail: string, ): Promise { const formatted = formatRuntimeError(detail); if (s.runtime.getSession().phase === "running") { await s.runtime.failRun(formatted); } else { s.messages.push(this.msg("system", formatted)); } if (s.bookId) this.syncBookPreview(s.bookId, s.messages); } private enrichDisplayMessages( session: RuntimeSession, messages: ChatMessage[], ): ChatMessage[] { const reason = session.waitingReason; const activeSkill = session.slots.activeSkill as ActiveSkillSnapshot | undefined; if ( !session.slots.startupCompleted && activeSkill && effectiveStartupMode(activeSkill) === "agent-first" && (reason?.kind === "input" || reason?.kind === "intake") ) { return messages; } if (reason?.kind === "intake" && !session.slots.startupCompleted) { const prompt = reason.prompt?.trim(); if (!prompt) return messages; const needle = prompt.slice(0, 48); const hasStartup = messages.some( (m) => m.kind === "orchestrator_prompt" || (typeof m.text === "string" && m.text.includes(needle)), ); if (hasStartup) return messages; return [...messages, this.msg("system", prompt)]; } if (reason?.kind !== "input" || session.slots.startupCompleted) { return messages; } const prompt = reason.message?.trim(); if (!prompt) return messages; const needle = prompt.slice(0, 48); const hasStartup = messages.some( (m) => m.kind === "orchestrator_prompt" || (typeof m.text === "string" && m.text.includes(needle)), ); if (hasStartup) return messages; return [...messages, this.msg("system", prompt)]; } private msg( role: ChatMessage["role"], text: string, tokenUsage?: MessageTokenUsage, thinking?: string, contextTrace?: LlmContextTrace, ): ChatMessage { const base = { id: randomUUID(), role, text, createdAt: new Date().toISOString(), ...(tokenUsage ? { tokenUsage } : {}), ...(thinking ? { thinking } : {}), ...(contextTrace ? { contextTrace } : {}), }; if (role === "user") { return { ...base, kind: "user_input", title: "你的输入", body: text, }; } const classified = classifyAgentMessage(text); return { ...base, ...classified }; } /** 按设置修剪本会话上下文痕迹;返回仍保留痕迹的条数 */ pruneSessionContextTraces(sessionId: string): { kept: number; cleared: number } { const s = this.require(sessionId); const before = s.messages.filter((m) => m.contextTrace).length; const keep = normalizeContextTraceKeepLatest( loadAppSettings().contextTraceKeepLatest, ); s.messages = pruneContextTraces(s.messages, keep); const after = s.messages.filter((m) => m.contextTrace).length; if (s.bookId) this.persist(s); return { kept: after, cleared: Math.max(0, before - after) }; } /** 清空本会话全部上下文痕迹 */ clearSessionContextTraces(sessionId: string): { cleared: number } { const s = this.require(sessionId); const before = s.messages.filter((m) => m.contextTrace).length; s.messages = pruneContextTraces(s.messages, 0); if (s.bookId) this.persist(s); return { cleared: before }; } /** 按当前设置修剪所有内存中的会话 */ pruneAllOpenContextTraces(): { sessions: number; kept: number; cleared: number; } { let sessions = 0; let kept = 0; let cleared = 0; for (const id of this.sessions.keys()) { const r = this.pruneSessionContextTraces(id); sessions += 1; kept += r.kept; cleared += r.cleared; } return { sessions, kept, cleared }; } /** 清空所有内存会话的上下文痕迹 */ clearAllOpenContextTraces(): { sessions: number; cleared: number } { let sessions = 0; let cleared = 0; for (const id of this.sessions.keys()) { const r = this.clearSessionContextTraces(id); sessions += 1; cleared += r.cleared; } return { sessions, cleared }; } /** 兼容旧会话 string[] questions → QuestionItem[] */ private normalizeWaitingReasonForView( reason: WaitingReason | undefined, ): WaitingReason | undefined { if (!reason) return reason; if (reason.kind === "worker_questions") { return { ...reason, questions: normalizeQuestions(reason.questions) }; } if (reason.kind === "input" && reason.questions?.length) { return { ...reason, questions: normalizeQuestions(reason.questions) }; } if (reason.kind === "review_artifact" && reason.questions?.length) { return { ...reason, questions: normalizeQuestions(reason.questions).map((q) => ({ ...q, required: false, })), }; } return reason; } private toView(id: string, resumed = false, resumeHint?: string): SessionView { const s = this.require(id); this.sanitizePlayHitl(s); const session = s.runtime.getSession(); const reason = session.waitingReason; const book = s.bookId ? getBook(s.bookId) : null; const hints: string[] = []; const actions: SessionAction[] = []; let intake: IntakeProgress | undefined; let intakePrompt: string | undefined; const activeSnap = session.slots.activeSkill as ActiveSkillSnapshot | undefined; if (resumeHint) { hints.push(resumeHint); } else if (resumed) { hints.push("已从上次进度恢复,可继续创作。"); } if (reason?.kind === "intake" && activeSnap?.intakeFields?.length) { intake = buildIntakeProgress( activeSnap.intakeFields, readIntakeValues(session.slots), ); intakePrompt = reason.prompt; if (intake.ready) { hints.push("必要项已齐,可确认进入实例化;也可继续补充可选项。"); actions.push({ type: "confirm_intake", label: "确认,进入实例化", }); actions.push({ type: "send_message", label: "继续补充", placeholder: "补充可选项或修正已填内容…", }); } else { hints.push( `填写必要项(${intake.requiredFilled}/${intake.requiredTotal})后可确认进入实例化`, ); actions.push({ type: "send_message", label: "发送", placeholder: "描述你想创作什么…", }); } } else if (reason?.kind === "skill_selection") { hints.push("旧版会话处于选包阶段;建议新建作品直接描述需求"); actions.push({ type: "send_message", label: "发送", placeholder: "skill id(如 world-simulator)或列表编号", }); } else if (reason?.kind === "input" && !session.slots.startupCompleted) { actions.push({ type: "send_message", label: "发送", placeholder: "描述你想做的作品与体验…", }); } else if (reason?.kind === "intake") { const isAgentFirst = activeSnap && effectiveStartupMode(activeSnap) === "agent-first"; hints.push( isAgentFirst ? "描述你想做的体验;发送后总管将开始:创作 · 核心" : (reason.prompt?.trim() ?? "请按填空项补充创作信息"), ); actions.push({ type: "send_message", label: "发送", placeholder: isAgentFirst ? "例如:网恋对象对话、西幻升级交互、规则怪谈……" : "描述你想写什么…", }); } else if (reason?.kind === "input") { const playNow = inferLifecycleStage(session) === "play" || isPlayLayerActive(session.slots); if (playNow) { actions.push({ type: "send_message", label: "发送", placeholder: "说你要做什么…", }); } else if (reason.questions?.length) { const qs = normalizeQuestions(reason.questions); const hasAssessment = Boolean(reason.message?.trim()); hints.push( hasAssessment ? "请阅读内容评价;下方追问可选,也可跳过并直接回复继续" : `总管追问(可跳过):${qs.map((q) => q.prompt).join(" ")}`, ); actions.push({ type: "answer_questions", label: "提交作答" }); actions.push({ type: "skip_questions", label: "跳过追问" }); actions.push({ type: "send_message", label: "自由补充", placeholder: "也可在此自由补充…", }); } else { if (reason.message?.trim()) { hints.push(reason.message.trim()); } else { hints.push("请回答启动问题,或补充创作目标"); } actions.push({ type: "send_message", label: "发送", placeholder: "描述你想写什么…", }); } } else if (reason?.kind === "worker_questions") { const qs = normalizeQuestions(reason.questions); if (isModuleOpeningQuestions(qs)) { // 能力默认问题:对齐美学纲领 → 自由书写,不是可跳过追问卡 hints.push("按主栏引导先说几句(想到什么写什么,不必整齐)"); actions.push({ type: "send_message", label: "发送", placeholder: "想到什么写什么…", }); } else if (qs.length) { hints.push( `${displayWorkerLabel(reason.workerId)} · 提问(请在询问卡作答):${qs.map((q) => q.prompt).join(" ")}`, ); actions.push({ type: "answer_questions", label: "提交作答" }); actions.push({ type: "skip_questions", label: "跳过追问" }); actions.push({ type: "send_message", label: "自由补充", placeholder: "也可在此自由补充…", }); } else { hints.push( `${displayWorkerLabel(reason.workerId)} 需要更多信息,请补充说明`, ); actions.push({ type: "send_message", label: "发送", placeholder: "补充说明…", }); } } else if (reason?.kind === "approve_step") { const proposed = readProposedNextStep( s.runtime.getSession().slots, s.runtime.getBlackboard(), ); if (proposed) { hints.push( proposed.mode === "revise" ? `回头修改「${proposed.name}」${ proposed.paramsMissing.length ? `(请先补齐:${proposed.paramsMissing.join("、")})` : "(继承既有产物继续改,不是从零再生成)" }` : `确认开始「${proposed.name}」${ proposed.paramsMissing.length ? `(请先补齐:${proposed.paramsMissing.join("、")})` : "" }`, ); actions.push({ type: "approve", label: proposed.paramsMissing.length ? "补参后开始" : "同意并开始", }); } else { actions.push({ type: "approve", label: "确认执行" }); } actions.push({ type: "reject", label: "返回节点" }); actions.push({ type: "send_message", label: "说明意见", placeholder: "改排、补节点(如 NPC)、回头改某步,或说明不要这一步…", }); } else if (reason?.kind === "pick_creation_step") { hints.push( "点图上可进入的节点即开始。要看技能池点上方「技能」。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。", ); actions.push({ type: "replan", label: "发送" }); actions.push({ type: "send_message", label: "发送", placeholder: "例如:补舞台骨架、把某步改到开场白之前…", }); } else if (reason?.kind === "next_intent") { hints.push("下一步想写什么?可留空;也可说回头改某步。发送后会展示下一节点供你确认。"); actions.push({ type: "send_message", label: "继续", placeholder: "下一步想写什么?(可留空)…", }); } else if (reason?.kind === "review_artifact") { const art = session.artifacts.find((a) => a.id === session.pendingArtifactId); const copy = reviewComposerCopy(art?.workerId, { hasQuestions: Boolean(reason.questions?.length), }); if (art?.workerId === DESIGN_FLOW_WORKER_ID) { hints.push( "点图上可进入的节点即开始。要看技能池点上方「技能」。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。", ); actions.push({ type: "send_message", label: "发送", placeholder: "例如:补舞台骨架、把某步改到开场白之前…", }); } else { actions.push({ type: "accept", label: copy.acceptLabel }); actions.push({ type: "reject", label: "不接受" }); actions.push({ type: "send_message", label: copy.submitLabel, placeholder: `${copy.placeholder};在现有产物上改,不整份重做…`, }); } if (reason.questions?.length) { hints.push( art?.workerId === DESIGN_FLOW_WORKER_ID ? "下方追问可选;点节点进入即收起" : `下方追问可选;${copy.acceptLabel}即收起(表示无需再完善)`, ); actions.push({ type: "answer_questions", label: "提交作答" }); actions.push({ type: "skip_questions", label: "跳过追问" }); } } else if (reason?.kind === "revision") { hints.push( reason.instruction?.trim() || "请说明要改哪里;发送后在现有产物上修改", ); actions.push({ type: "send_message", label: "按意见修改", placeholder: "说明要改哪里…", }); } else if (session.phase === "error") { hints.push("上一轮执行出错。可直接发消息让总管重试,或刷新作品后继续。"); actions.push({ type: "send_message", label: "重试 / 继续", placeholder: "例如:重试上一步,或补充说明…", }); } else if ( session.phase === "running" && s.runtime.needsMainAgentDecision() && !s.runtime.hasMainAgent() ) { hints.push("简报已就绪,可手动生成大纲(未配置总管模型)"); actions.push({ type: "run_outline", label: "生成大纲" }); actions.push({ type: "send_message", label: "发送", placeholder: "补充说明…", }); } else if (session.phase === "running" && s.runtime.needsMainAgentDecision()) { hints.push("总管正在调度…"); } else if (session.phase === "running" && session.pendingArtifactId) { /* worker 运行中 */ } else if ( session.phase === "running" && session.slots.startupCompleted && !session.pendingDecision ) { actions.push({ type: "finish", label: "结束流程" }); } else if (session.phase === "done") { hints.push("流程已完成"); } else { actions.push({ type: "send_message", label: "发送", placeholder: "输入消息…", }); } if (this.canOfferLeaveCreationStep(s, reason)) { actions.push({ type: "leave_step", label: "返回节点" }); } if (s.bookId) { this.persist(s); } const tokens = getSessionTokenTotals(id); const messages = this.enrichDisplayMessages(session, [...s.messages]); const lifecycleStage = inferLifecycleStage(session); const skillPackId = sessionSkillPackId(session) ?? (book ? bookSkillPackId(book) : undefined); const workerSetYaml = readWorkerSetYaml(s.runtime.getBlackboard()); const skillCatalog = buildSkillCatalog( session, skillPackId, lifecycleStage, { workerSetYaml }, ); const shellAdapt = parseShellAdaptationFromReplyFormat( s.runtime.getBlackboard().getContentByTag("设计.正文组成") ?? s.runtime.getBlackboard().getContentByTag("设计.回复格式"), ); return { id, bookId: s.bookId, bookTitle: book?.title, activeSkill: s.runtime.getActiveSkill()?.name ?? skillPackId, phase: session.phase, waitingReason: this.normalizeWaitingReasonForView(reason), startupCompleted: Boolean(session.slots.startupCompleted), skills: s.runtime.getAvailableSkills(), messages, hints, 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( s.bookId && this.listGameSnapshots(s.bookId).some((item) => item.kind === "instance"), ), playInstanceId: isPlayLayerActive(session.slots) ? this.currentPlayInstanceId(s) : undefined, skillCatalog, toolTrace: buildToolTrace(messages), burst: buildBurstState(messages, session), focus: buildFocus(session, reason, intake, lifecycleStage), intake, intakePrompt, uiPrompt: !session.slots.startupCompleted && activeSnap && effectiveStartupMode(activeSnap) === "agent-first" ? activeSnap.uiPrompt?.trim() || DEFAULT_UI_PROMPT : undefined, reviewArtifact: buildReviewArtifactView( session, messages, s.runtime.getBlackboard(), s.moduleCatalog, ), workerSetView: buildWorkerSetUserView( s.runtime.getBlackboard(), s.runtime.getSession(), ), creationFlowView: buildCreationFlowUserView( s.runtime.getBlackboard(), session.slots[SLOT_CREATION_ACCEPTED_UNITS], s.moduleCatalog, ), proposedNextStep: reason?.kind === "approve_step" ? readProposedNextStep( session.slots, s.runtime.getBlackboard(), ) : undefined, recipes: s.recipeOptions?.map((r) => ({ id: r.id, name: r.name, declaration: r.declaration, })), selectedRecipe: resolveSelectedRecipeView( s.runtime.getBlackboard(), s.recipeOptions, ), openingGuide: this.resolveOpeningGuideForView(s, reason), boardPanel: buildBoardPanelFromRuntime(s.runtime.getBlackboard()), presentationTweaks: shellAdapt ? { shell_id: shellAdapt.shell_id, ...shellAdapt.tweaks, } : undefined, liveStream: this.buildLiveStreamView(id), agentThinking: this.agentThinkingLive.get(id), resumed: resumed || undefined, tokenStats: { sessionTotal: tokens.totalTokens, sessionCached: tokens.totalCached || undefined, sessionCacheMiss: tokens.totalCacheMiss || undefined, lastCaller: tokens.last?.caller, lastTotal: tokens.last?.totalTokens, byCaller: tokens.byCaller, }, }; } private captureCheckpoint(s: ManagedSession): SessionCheckpoint { return { runtimeSession: structuredClone(s.runtime.getSession()), blackboardItems: s.runtime.getBlackboard().exportItems(), }; } private serializeBranchState( state: MessageBranchState, ): import("../types/book-session.js").PersistedMessageBranchState { const preMessageCheckpoints: Record< string, { runtimeSession: RuntimeSession; blackboardItems: BlackboardItem[] } > = {}; for (const [key, cp] of Object.entries(state.preMessageCheckpoints)) { preMessageCheckpoints[key] = { runtimeSession: structuredClone(cp.runtimeSession), blackboardItems: structuredClone(cp.blackboardItems), }; } const branches: import("../types/book-session.js").PersistedMessageBranchState["branches"] = {}; for (const [groupId, branch] of Object.entries(state.branches)) { branches[groupId] = { anchorIndex: branch.anchorIndex, groupId: branch.groupId, activeIndex: branch.activeIndex, variants: branch.variants.map((v) => ({ messages: v.messages.map((m) => ({ ...m })), checkpoint: { runtimeSession: structuredClone(v.checkpoint.runtimeSession), blackboardItems: structuredClone(v.checkpoint.blackboardItems), }, })), }; } return { branches, preMessageCheckpoints }; } private deserializeBranchState( raw?: import("../types/book-session.js").PersistedMessageBranchState, ): MessageBranchState { const state = createMessageBranchState(); if (!raw) return state; for (const [key, cp] of Object.entries(raw.preMessageCheckpoints ?? {})) { state.preMessageCheckpoints[Number(key)] = { runtimeSession: structuredClone(cp.runtimeSession), blackboardItems: structuredClone(cp.blackboardItems), }; } for (const [groupId, branch] of Object.entries(raw.branches ?? {})) { state.branches[groupId] = { anchorIndex: branch.anchorIndex, groupId: branch.groupId, activeIndex: branch.activeIndex, variants: branch.variants.map((v) => ({ messages: v.messages.map((m) => ({ ...m })), checkpoint: { runtimeSession: structuredClone(v.checkpoint.runtimeSession), blackboardItems: structuredClone(v.checkpoint.blackboardItems), }, })), }; } return state; } private async refreshRecipeOptions( s: ManagedSession, family?: "recipe" | "dictate", ): Promise { const skillName = s.runtime.getActiveSkill()?.name; if (!skillName) { s.recipeOptions = []; s.moduleCatalog = null; s.openingGuide = null; return; } try { const skill = await loadSkill(skillName); if (!skill.skillPackRoot) { s.recipeOptions = []; s.moduleCatalog = null; s.openingGuide = null; return; } const intake = family ?? (this.isDictateMode(s) ? "dictate" : "recipe"); const [catalog, modules] = await Promise.all([ loadRecipeCatalog(skill.skillPackRoot, undefined, intake), loadModuleCatalog(skill.skillPackRoot), ]); s.recipeOptions = catalog?.recipes ?? []; s.moduleCatalog = modules; } catch { s.recipeOptions = []; s.moduleCatalog = null; } await this.ensureRecipeSeededFlow(s); await this.refreshOpeningGuide(s); } /** 已选配方但尚无流程 → 写入配方近期起点(兼容旧会话) */ private async ensureRecipeSeededFlow(s: ManagedSession): Promise { if (this.isDictateMode(s)) return; const board = s.runtime.getBlackboard(); if (board.getContentByTag(CREATION_FLOW_TAG)?.trim()) return; const ref = parseSelectedRecipeRef( board.getContentByTag(CREATION_SELECTED_RECIPE_TAG), ); if (!ref) return; const entry = findRecipeCatalogEntry( s.recipeOptions?.length ? { recipes: s.recipeOptions } : null, ref, ); if (!entry) return; const skillName = s.runtime.getActiveSkill()?.name; if (!skillName) return; try { const skill = await loadSkill(skillName); const packRoot = skill.skillPackRoot?.trim(); if (!packRoot) return; const detail = await loadRecipeDetail(packRoot, entry); const seeded = creationFlowFromRecipeSeed(detail); if (!seeded) return; await this.commitSeededCreationFlow(s, seeded); } catch (err) { console.error("[会话] 配方预置失败", err); } } /** * 下一待做开局步(无 depends_on)的 opening → 意图页引导。 * 有起点模块就展示;无则 null(再走 design-flow 生成)。 * 中段步骤的默认问题改由 resolveOpeningGuideForView 在 waiting 时覆盖。 */ private async refreshOpeningGuide(s: ManagedSession): Promise { const skillName = s.runtime.getActiveSkill()?.name; if (!skillName) { s.openingGuide = null; return; } try { const skill = await loadSkill(skillName); const packRoot = skill.skillPackRoot?.trim(); if (!packRoot) { s.openingGuide = null; return; } const board = s.runtime.getBlackboard(); const session = s.runtime.getSession(); const accepted = parseAcceptedUnits( session.slots[SLOT_CREATION_ACCEPTED_UNITS] ?? board.getContentByTag(CREATION_ACCEPTED_UNITS_TAG), ); const binding = await resolveDesignStepBinding({ skillPackRoot: packRoot, flowRaw: board.getContentByTag(CREATION_FLOW_TAG), acceptedStepNames: accepted, }); if ( !binding?.opening?.trim() || (binding.step.depends_on?.length ?? 0) > 0 ) { s.openingGuide = null; return; } s.openingGuide = { stepId: binding.step.id, stepName: binding.module.name || binding.step.name, text: binding.opening.trim(), }; } catch { s.openingGuide = null; } } /** * 视图用开场引导:等待能力默认问题时用题干(对齐美学纲领说话面); * 中段步骤(有 depends_on)也会覆盖,避免只剩空答题壳。 */ private resolveOpeningGuideForView( s: ManagedSession, reason: WaitingReason | undefined, ): { stepId: string; stepName: string; text: string } | null { if (reason?.kind === "pick_creation_step") return null; if (reason?.kind === "worker_questions") { const qs = normalizeQuestions(reason.questions); if (isModuleOpeningQuestions(qs)) { const text = qs[0]?.prompt?.trim() ?? ""; if (text) { const board = s.runtime.getBlackboard(); const stepKey = board.getContentByTag(CREATION_CURRENT_STEP_TAG)?.trim() || ""; const flow = parseCreationFlow(board.getContentByTag(CREATION_FLOW_TAG)); const step = flow?.steps?.find( (st) => stepUnitId(st) === stepKey || st.id === stepKey || st.name === stepKey, ); const stepName = step?.name?.trim() || s.openingGuide?.stepName?.trim() || stepKey || "按引导先说几句"; return { stepId: qs[0]!.id, stepName, text, }; } } } return s.openingGuide ?? null; } /** 写入剧本 seed,并立刻进入点选图(开局节点跟配方走)。 */ private async commitSeededCreationFlow( s: ManagedSession, seeded: CreationFlow, ): Promise { this.writeSeededCreationFlow(s, seeded); await s.runtime.enterSeededCreationPick(); } /** 配方预置 DAG:写入黑板并留下可回退的编排检查点(不必先跑 LLM) */ private writeSeededCreationFlow(s: ManagedSession, seeded: CreationFlow): void { s.runtime.getBlackboard().write({ tag: CREATION_FLOW_TAG, content: stringifyCreationFlow(seeded), source: "runtime", }); const alreadyRecorded = s.messages.some( (m) => m.kind === "worker_output" && m.actor === "design-flow" && (m.text ?? "").includes("配方预置近期起点"), ); if (alreadyRecorded) return; const names = seeded.steps.map((step) => step.name).join(" → "); recordPreMessageCheckpoint( s.branchState, s.messages.length, this.captureCheckpoint(s), ); s.messages.push( this.msg( "system", `[Worker] design-flow 已完成\n\n配方预置近期起点${names ? `:${names}` : ""}。\n这是可回退的编排检查点;之后可再编排追加节点(如生成规则 / 具体实例)。`, ), ); this.syncCreationDialogue(s); } private async applySelectedRecipe( s: ManagedSession, recipeId: string, opts?: { seedFlow?: boolean }, ): Promise { const options = s.recipeOptions ?? []; const entry = findRecipeCatalogEntry( options.length ? { recipes: options } : null, recipeId, ); if (!entry) { throw new Error(`未知初始配方:${recipeId}`); } const board = s.runtime.getBlackboard(); board.write({ tag: CREATION_SELECTED_RECIPE_TAG, 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 (seedFlow && !existingFlow) { const skillName = s.runtime.getActiveSkill()?.name; if (skillName) { try { const skill = await loadSkill(skillName); const packRoot = skill.skillPackRoot?.trim(); if (packRoot) { const detail = await loadRecipeDetail(packRoot, entry); const seeded = creationFlowFromRecipeSeed(detail); if (seeded) { await this.commitSeededCreationFlow(s, seeded); } } } catch (err) { console.error("[会话] 从配方写入创作流程失败", err); } } } await this.refreshOpeningGuide(s); } } export const sessionManager = new SessionManager(); function readWorkerSetYaml(blackboard: Blackboard): string | undefined { for (const tag of ["设计.worker集", "设计.worker集.草稿"]) { const content = blackboard.getContentByTag(tag)?.trim(); if (content) return content; } return undefined; } function collectFilledTags(blackboard: Blackboard): string[] { return blackboard.listTagIndex().map((item) => item.tag); } function buildCreationFlowUserView( blackboard: Blackboard, acceptedRaw?: unknown, catalog?: ModuleCatalog | null, ): CreationFlowUserView | undefined { const raw = blackboard.getContentByTag(CREATION_FLOW_TAG)?.trim(); if (!raw) return undefined; const flow = parseCreationFlow(raw); if (!flow) { return { steps: [], parseError: "创作流程无法解析为 JSON(需要 steps 数组)" }; } const accepted = parseAcceptedUnits( acceptedRaw ?? blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG), ); const titleHints: Record = {}; const store = parseAcceptedContentStore( blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG), ); for (const [id, entry] of Object.entries(store.units)) { if (!entry.summary && entry.content == null) continue; titleHints[id] = { ...(entry.summary ? { summary: entry.summary } : {}), ...(entry.content != null ? { content: entry.content } : {}), }; } return formatCreationFlowForUser( flow, catalog, accepted, collectFilledTags(blackboard), titleHints, ); } function readProposedNextStep( slots: Record, blackboard: Blackboard, ): ProposedNextStepView | undefined { const raw = (typeof slots[CREATION_PROPOSED_STEP_TAG] === "string" ? String(slots[CREATION_PROPOSED_STEP_TAG]) : null) || blackboard.getContentByTag(CREATION_PROPOSED_STEP_TAG); if (!raw?.trim()) return undefined; try { const doc = JSON.parse(raw) as Partial; if (typeof doc.stepId !== "string" || typeof doc.name !== "string") { return undefined; } return { stepId: doc.stepId, name: doc.name, declaration: typeof doc.declaration === "string" ? doc.declaration : undefined, params: doc.params && typeof doc.params === "object" && !Array.isArray(doc.params) ? (doc.params as Record) : {}, paramsMissing: Array.isArray(doc.paramsMissing) ? doc.paramsMissing.map(String) : [], paramSpecs: Array.isArray(doc.paramSpecs) ? doc.paramSpecs .map((p) => ({ key: String((p as { key?: string }).key ?? ""), label: String((p as { label?: string }).label ?? ""), required: Boolean((p as { required?: boolean }).required), hint: typeof (p as { hint?: string }).hint === "string" ? (p as { hint: string }).hint : undefined, })) .filter((p) => p.key) : [], kind: typeof doc.kind === "string" ? doc.kind : undefined, intent: typeof doc.intent === "string" ? doc.intent : undefined, mode: doc.mode === "revise" ? "revise" : undefined, revises: typeof doc.revises === "string" ? doc.revises : undefined, closer: doc.closer === true ? true : undefined, }; } catch { return undefined; } } function resolveSelectedRecipeView( blackboard: Blackboard, options?: RecipeCatalogEntry[], ): { id: string; name: string; declaration: string } | null { const ref = parseSelectedRecipeRef( blackboard.getContentByTag(CREATION_SELECTED_RECIPE_TAG), ); if (!ref) return null; const entry = findRecipeCatalogEntry( options?.length ? { recipes: options } : null, ref, ); if (entry) { return { id: entry.id, name: entry.name, declaration: entry.declaration, }; } return { id: ref, name: ref, declaration: "" }; } /** Boss 直聘:优先注入 recipe.yaml 方法论,缺则退回 catalog declaration */ async function resolveDictateRecipeBriefForSession(params: { skillName?: string | null; recipeView: { id: string; name: string; declaration: string } | null; recipeOptions?: RecipeCatalogEntry[]; }): Promise { const view = params.recipeView; if (!view) return undefined; const entry = findRecipeCatalogEntry( params.recipeOptions?.length ? { recipes: params.recipeOptions } : null, view.id, ); const skillName = params.skillName?.trim(); if (entry && skillName) { try { const skill = await loadSkill(skillName); const packRoot = skill.skillPackRoot?.trim(); if (packRoot) { const detail = await loadRecipeDetail(packRoot, entry); const brief = formatDictateRecipeBrief(detail); if (brief) return brief; } } catch (err) { console.error("[会话] 加载转述配方要点失败", err); } } return view.declaration?.trim() || undefined; } function buildWorkerSetUserView( blackboard: Blackboard, session?: RuntimeSession, ): WorkerSetUserView | undefined { const yaml = readWorkerSetYaml(blackboard); if (!yaml) return undefined; const parsed = parseWorkerSetYaml(yaml); if (!parsed) return undefined; const acceptedUnitIds = parseAcceptedUnits( session?.slots?.creationAcceptedUnits ?? blackboard.getContentByTag("创作.已验收单位"), ); const currentUnitId = (typeof session?.slots?.creationCurrentUnitId === "string" ? session.slots.creationCurrentUnitId : null) || blackboard.getContentByTag("创作.当前单位")?.trim() || null; return ( formatWorkerSetForUser(parsed, { filledTags: collectFilledTags(blackboard), acceptedUnitIds, currentUnitId, }) ?? undefined ); } function buildReviewArtifactView( session: RuntimeSession, messages: ChatMessage[], blackboard: Blackboard, catalog?: ModuleCatalog | null, ): ReviewArtifactView | undefined { if (session.waitingReason?.kind !== "review_artifact") return undefined; const art = session.artifacts.find((a) => a.id === session.pendingArtifactId); if (!art) return undefined; const workerSetTags = ["设计.worker集", "设计.worker集.草稿"]; const tagBodies: string[] = []; for (const tag of workerSetTags) { const content = blackboard.getContentByTag(tag); if (content?.trim()) { tagBodies.push(`## ${tag}\n\n${content.trim()}`); } } for (const tag of art.outputTags) { if (workerSetTags.includes(tag)) continue; if (isProgressPointerTag(tag)) continue; const content = blackboard.getContentByTag(tag); if (content?.trim()) { tagBodies.push(`## ${tag}\n\n${content.trim()}`); } } // 回退消息里也可能夹带「创作.当前步骤」段落,验收正文里去掉 const stripPointerSections = (text: string): string => { const trimmed = text.trim(); if (!trimmed) return ""; const re = /^##\s+(.+?)\s*$/gm; const hits: Array<{ title: string; index: number; headerEnd: number }> = []; let m: RegExpExecArray | null; while ((m = re.exec(trimmed))) { hits.push({ title: m[1]!.trim(), index: m.index, headerEnd: m.index + m[0].length, }); } if (!hits.length) return trimmed; return hits .filter((h) => !isProgressPointerTag(h.title) && !/当前步骤|当前单位/.test(h.title)) .map((h, i) => { const end = i + 1 < hits.length ? hits[i + 1]!.index : trimmed.length; const chunk = trimmed.slice(h.headerEnd, end).trim(); return chunk ? `## ${h.title}\n\n${chunk}` : ""; }) .filter(Boolean) .join("\n\n"); }; const outputMsg = [...messages] .reverse() .find((m) => m.kind === "worker_output" && m.actor === art.workerId); const body = tagBodies.join("\n\n") || stripPointerSections((outputMsg?.body ?? outputMsg?.text ?? "").trim()) || art.summary?.trim() || "(Worker 未写入可读正文,请检查黑板 tag 或重试创作 skill)"; const isFinalSet = art.outputTags.includes("设计.worker集"); const isFlowArt = art.workerId === "design-flow" || art.outputTags.includes(CREATION_FLOW_TAG); const currentUnit = blackboard.getContentByTag("创作.当前单位")?.trim() || (typeof session.slots.creationCurrentUnitId === "string" ? session.slots.creationCurrentUnitId : ""); const isDesignArt = isDesignDiskWorker(art.workerId); const summary = art.summary?.trim() || (isFlowArt ? "创作流程(步骤与依赖)" : isDesignArt ? isFinalSet ? "完整 Worker 集(终稿)" : currentUnit ? `创作单位 · ${currentUnit}` : "创作单位草稿" : `${art.workerId} 产物`); const workerSetYaml = readWorkerSetYaml(blackboard); const workerSet = isDesignArt && !isFlowArt && workerSetYaml ? parseWorkerSetYaml(workerSetYaml) ?? undefined : undefined; const acceptedUnitIds = parseAcceptedUnits( session.slots.creationAcceptedUnits ?? blackboard.getContentByTag("创作.已验收单位"), ); const workerSetView = isDesignArt && workerSet ? formatWorkerSetForUser(workerSet, { filledTags: collectFilledTags(blackboard), acceptedUnitIds, currentUnitId: currentUnit || null, }) ?? undefined : undefined; const creationFlowView = isFlowArt ? buildCreationFlowUserView( blackboard, session.slots[SLOT_CREATION_ACCEPTED_UNITS], catalog, ) : undefined; return { id: art.id, workerId: art.workerId, summary, body, outputTags: art.outputTags, sourceMessageId: outputMsg?.id, contextTrace: outputMsg?.contextTrace, workerSet, workerSetView, creationFlowView, }; } function buildBoardPanelFromRuntime( blackboard: import("../blackboard/blackboard.js").Blackboard, ): BoardPanelView { return buildBoardPanel(blackboard); } function formatRuntimeError(detail: string): string { if (detail.includes("401") || detail.toLowerCase().includes("authentication")) { return ( "[请求失败] API Key 无效或未授权。请到「设置 → API 配置」检查 Key 与 Base URL," + "或点击「探测」验证。" ); } if (detail.startsWith("LLM request failed")) { return `[请求失败] ${detail.replace(/^LLM request failed \(\d+\): /, "").slice(0, 300)}`; } if (detail.startsWith("Main Agent returned invalid JSON")) { return "[请求失败] 总管返回了无效 JSON,请检查模型是否支持 json 输出,或暂时关闭预设后重试。"; } return `[请求失败] ${detail.slice(0, 300)}`; }