import crypto from 'node:crypto'; import type { Request, Response } from 'express'; import type { CreateCustomWorldAgentSessionRequest, CustomWorldAgentActionRequest, CustomWorldAgentActionResponse, CustomWorldAgentMessage, CustomWorldAgentOperationRecord, CustomWorldAgentSessionSnapshot, CustomWorldDraftCardSummary, CustomWorldPendingClarification, CustomWorldSuggestedAction, SendCustomWorldAgentMessageRequest, SendCustomWorldAgentMessageResponse, } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import { badRequest, notFound } from '../errors.js'; import { prepareEventStreamResponse } from '../http.js'; import { normalizeCustomWorldProfile } from '../modules/custom-world/runtimeProfile.js'; import type { CustomWorldProfile } from '../modules/custom-world/runtimeTypes.js'; import { CustomWorldAgentAssetBridgeService } from './customWorldAgentAssetBridgeService.js'; import { CustomWorldAgentAutoAssetService } from './customWorldAgentAutoAssetService.js'; import { CustomWorldAgentChangeSummaryService } from './customWorldAgentChangeSummaryService.js'; import { buildPendingClarifications, evaluateCreatorIntentReadiness, resolveCreatorIntentStage, } from './customWorldAgentClarificationService.js'; import { CustomWorldAgentDraftCompiler, getWorldFoundationCardId, normalizeFoundationDraftProfile, } from './customWorldAgentDraftCompiler.js'; import { updateDraftCardSections } from './customWorldAgentDraftEditService.js'; import { CustomWorldAgentEntityGenerationService } from './customWorldAgentEntityGenerationService.js'; import { CustomWorldAgentFoundationDraftService } from './customWorldAgentFoundationDraftService.js'; import { buildAnchorPackFromIntent, buildDraftSummaryFromIntent, buildDraftTitleFromIntent, createEmptyCreatorIntentRecord, type CustomWorldCreatorIntentRecord, extractCreatorIntentPatch, hasMeaningfulCreatorIntentRecord, mergeCreatorIntentRecord, normalizeCreatorIntentRecord, } from './customWorldAgentIntentExtractionService.js'; import { rebuildRoleAssetCoverage, resolveRoleAssetStatusLabel, } from './customWorldAgentRoleAssetStateService.js'; import { type CustomWorldAgentSessionRecord, CustomWorldAgentSessionStore, } from './customWorldAgentSessionStore.js'; import { buildAnchorPackFromEightAnchorContent, buildCreatorIntentFromEightAnchorContent, buildEightAnchorContentFromCreatorIntent, estimateProgressPercentFromAnchorContent, } from './eightAnchorCompatibilityService.js'; import { EightAnchorSingleTurnService } from './eightAnchorSingleTurnService.js'; import type { UpstreamLlmClient } from './llmClient.js'; const PHASE2_FORCE_FAIL_TOKEN = '__phase1_force_fail__'; function truncateText(value: string, maxLength: number) { if (value.length <= maxLength) { return value; } return `${value.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } function sleep(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function buildSuggestedActions( params: { stage?: CustomWorldAgentSessionRecord['stage']; isReady?: boolean; draftProfile?: unknown; draftCards?: CustomWorldDraftCardSummary[]; } = {}, ): CustomWorldSuggestedAction[] { const profile = normalizeFoundationDraftProfile(params.draftProfile); const actions: CustomWorldSuggestedAction[] = [ { id: 'request_summary', type: 'request_summary', label: params.stage === 'object_refining' || params.stage === 'visual_refining' ? '总结当前世界底稿' : '总结当前设定', }, ]; if (params.stage === 'foundation_review' && params.isReady) { actions.push({ id: 'draft_foundation', type: 'draft_foundation', label: '整理一版世界底稿', }); return actions; } if ( (params.stage === 'object_refining' || params.stage === 'visual_refining') && profile ) { const worldCardId = params.draftCards?.find((entry) => entry.kind === 'world')?.id ?? getWorldFoundationCardId(); const firstCharacter = [...profile.playableNpcs, ...profile.storyNpcs][0]; const firstLandmark = profile.landmarks[0]; actions.push({ id: 'refine_world', type: 'refine_focus_target', label: '先看世界总卡', targetId: worldCardId, }); if (firstCharacter) { actions.push({ id: `refine-character-${firstCharacter.id}`, type: 'refine_focus_target', label: `精修角色:${firstCharacter.name}`, targetId: firstCharacter.id, }); } if (firstLandmark) { actions.push({ id: `refine-landmark-${firstLandmark.id}`, type: 'refine_focus_target', label: `继续补地点:${firstLandmark.name}`, targetId: firstLandmark.id, }); } } return actions; } function buildOperation(type: CustomWorldAgentOperationRecord['type']) { const phaseDetail = type === 'draft_foundation' ? '正在把已确认设定编成第一版世界底稿。' : type === 'update_draft_card' ? '正在把这次设定改动写回草稿。' : type === 'sync_result_profile' ? '正在把结果页里的世界快照同步回当前草稿。' : type === 'generate_characters' ? '正在围绕当前底稿补出新角色。' : type === 'generate_landmarks' ? '正在围绕当前底稿补出新地点。' : type === 'generate_role_assets' ? '正在准备角色资产工坊入口。' : type === 'sync_role_assets' ? '正在把角色资产结果写回世界草稿。' : '正在整理这一轮新增的世界设定。'; return { operationId: `operation-${crypto.randomBytes(10).toString('hex')}`, type, status: 'queued', phaseLabel: '已接收请求', phaseDetail, progress: 10, error: null, } satisfies CustomWorldAgentOperationRecord; } function buildUserMessage( text: string, clientMessageId: string, ): CustomWorldAgentMessage { return { id: clientMessageId.trim() || `message-${crypto.randomBytes(8).toString('hex')}`, role: 'user', kind: 'chat', text, createdAt: new Date().toISOString(), relatedOperationId: null, }; } function buildRoleAssetSyncResultText(params: { roleName: string; assetStatusLabel: string; }) { return `已把「${params.roleName}」的角色资产写回草稿,当前状态:${params.assetStatusLabel}。`; } function syncResultProfileIntoDraftProfile(params: { currentDraftProfile: Record | null | undefined; resultProfile: CustomWorldProfile; }) { const currentDraftProfile = params.currentDraftProfile ?? {}; const resultProfile = params.resultProfile; return { // 阶段一只回写基础摘要和完整 legacy 快照,避免把结果页的运行时结构反向拆回 foundation draft。 ...currentDraftProfile, name: resultProfile.name, subtitle: resultProfile.subtitle, summary: resultProfile.summary, tone: resultProfile.tone, playerGoal: resultProfile.playerGoal, majorFactions: resultProfile.majorFactions, coreConflicts: resultProfile.coreConflicts, legacyResultProfile: resultProfile as unknown as Record, } satisfies Record; } function buildQuestionLines( pendingClarifications: CustomWorldPendingClarification[], ) { return pendingClarifications.map((entry) => entry.question.trim()); } function composeAssistantReply(params: { openingText: string; intent: CustomWorldCreatorIntentRecord; pendingClarifications: CustomWorldPendingClarification[]; isReady: boolean; }) { const questionLines = buildQuestionLines(params.pendingClarifications); return [ params.openingText, params.isReady ? '当前设定已经齐备。' : questionLines.slice(0, 1).join('\n'), ].join('\n'); } function buildDerivedState( intent: CustomWorldCreatorIntentRecord, hasUserInput: boolean, ) { const readiness = evaluateCreatorIntentReadiness(intent); const pendingClarifications = buildPendingClarifications(intent, readiness); const stage = resolveCreatorIntentStage({ hasUserInput, readiness, }); return { readiness, pendingClarifications, stage, anchorPack: buildAnchorPackFromIntent(intent, { completedKeys: readiness.completedKeys, missingKeys: readiness.missingKeys, }), draftProfile: { title: buildDraftTitleFromIntent(intent), summary: buildDraftSummaryFromIntent(intent), }, suggestedActions: buildSuggestedActions({ stage, isReady: readiness.isReady, }), }; } function buildWelcomeMessage(params: { seedText: string; intent: CustomWorldCreatorIntentRecord; pendingClarifications: CustomWorldPendingClarification[]; isReady: boolean; }) { let openingText: string; if (params.seedText) { openingText = `收到:${truncateText(params.seedText, 88)}`; } else { // When user enters without saying anything, provide a welcoming introduction const hasAnyAnchors = hasMeaningfulCreatorIntentRecord(params.intent); openingText = hasAnyAnchors ? '继续聊聊你的世界设定吧。' : '你好!我是你的世界设定助手,可以帮你一起构建游戏世界的核心设定。'; } return composeAssistantReply({ openingText, intent: params.intent, pendingClarifications: params.pendingClarifications, isReady: params.isReady, }); } function buildFoundationDraftAssistantMessage(params: { relatedOperationId: string; draftProfile: unknown; warnings?: string[]; }) { const profile = normalizeFoundationDraftProfile(params.draftProfile); const leadCharacter = profile?.playableNpcs[0]; const leadLandmark = profile?.landmarks[0]; const warnings = (params.warnings ?? []).filter(Boolean); return { id: `message-${crypto.randomBytes(8).toString('hex')}`, role: 'assistant', kind: 'summary', text: [ `我先把第一版世界底稿整理出来了:${profile?.summary || '底稿已经生成完成。'}`, '', `当前已经落下来的第一批对象数量是:关键角色 ${profile?.playableNpcs.length ?? 0} 个,关键地点 ${profile?.landmarks.length ?? 0} 个,势力 ${profile?.factions.length ?? 0} 个。`, `建议你先从“${profile?.name || '世界总卡'}”这张世界总卡看起${leadCharacter ? `,再顺着角色「${leadCharacter.name}」往下细修` : ''}${leadLandmark ? `,地点可以先看「${leadLandmark.name}」` : ''}。`, ...(warnings.length > 0 ? [ '', `这一轮有 ${warnings.length} 项资产补齐未完成,但不影响世界底稿继续精修。`, ] : []), ].join('\n'), createdAt: new Date().toISOString(), relatedOperationId: params.relatedOperationId, } satisfies CustomWorldAgentMessage; } function buildActionResultMessage(params: { relatedOperationId: string; text: string; }) { return { id: `message-${crypto.randomBytes(8).toString('hex')}`, role: 'assistant', kind: 'action_result', text: params.text, createdAt: new Date().toISOString(), relatedOperationId: params.relatedOperationId, } satisfies CustomWorldAgentMessage; } function writeSseEvent( response: Response, event: string, data: unknown, ) { if (response.writableEnded) { return; } response.write(`event: ${event}\n`); response.write(`data: ${JSON.stringify(data)}\n\n`); } export class CustomWorldAgentOrchestrator { private readonly foundationDraftService: CustomWorldAgentFoundationDraftService; private readonly draftCompiler: CustomWorldAgentDraftCompiler; private readonly entityGenerationService: CustomWorldAgentEntityGenerationService; private readonly changeSummaryService: CustomWorldAgentChangeSummaryService; private readonly assetBridgeService: CustomWorldAgentAssetBridgeService; private readonly autoAssetService: CustomWorldAgentAutoAssetService | null; private readonly eightAnchorSingleTurnService: EightAnchorSingleTurnService; constructor( private readonly sessionStore: CustomWorldAgentSessionStore, llmClient: UpstreamLlmClient | null = null, options: { singleTurnLlmClient?: UpstreamLlmClient | null; autoAssetService?: CustomWorldAgentAutoAssetService | null; } = {}, ) { this.foundationDraftService = new CustomWorldAgentFoundationDraftService( llmClient, ); this.draftCompiler = new CustomWorldAgentDraftCompiler(); this.entityGenerationService = new CustomWorldAgentEntityGenerationService( llmClient, ); this.changeSummaryService = new CustomWorldAgentChangeSummaryService(); this.assetBridgeService = new CustomWorldAgentAssetBridgeService(); this.autoAssetService = options.autoAssetService ?? null; this.eightAnchorSingleTurnService = new EightAnchorSingleTurnService( (options.singleTurnLlmClient ?? llmClient) ?? undefined, ); } async createSession( userId: string, payload: CreateCustomWorldAgentSessionRequest, ): Promise { const seedText = payload.seedText?.trim() ?? ''; const baseIntent = createEmptyCreatorIntentRecord('freeform'); const seedPatch = seedText ? extractCreatorIntentPatch({ currentIntent: baseIntent, latestUserMessage: seedText, }) : {}; const creatorIntent = mergeCreatorIntentRecord(baseIntent, seedPatch); const derivedState = buildDerivedState(creatorIntent, Boolean(seedText)); const anchorContent = buildEightAnchorContentFromCreatorIntent(creatorIntent); const progressPercent = seedText ? estimateProgressPercentFromAnchorContent(anchorContent) : 0; const fallbackWelcomeMessage = buildWelcomeMessage({ seedText, intent: creatorIntent, pendingClarifications: derivedState.pendingClarifications, isReady: derivedState.readiness.isReady, }); const record = await this.sessionStore.create(userId, { seedText, welcomeMessage: fallbackWelcomeMessage, currentTurn: 0, anchorContent, progressPercent, lastAssistantReply: fallbackWelcomeMessage, creatorIntent, creatorIntentReadiness: derivedState.readiness, anchorPack: buildAnchorPackFromEightAnchorContent( anchorContent, progressPercent, ), draftProfile: derivedState.draftProfile, pendingClarifications: derivedState.pendingClarifications, stage: progressPercent >= 100 ? 'foundation_review' : 'collecting_intent', suggestedActions: derivedState.suggestedActions, recommendedReplies: [], }); return (await this.sessionStore.getSnapshot( userId, record.sessionId, )) as CustomWorldAgentSessionSnapshot; } async getSessionSnapshot(userId: string, sessionId: string) { return this.sessionStore.getSnapshot(userId, sessionId); } async submitMessage( userId: string, sessionId: string, payload: SendCustomWorldAgentMessageRequest, ): Promise { const session = await this.sessionStore.get(userId, sessionId); if (!session) { throw notFound('custom world agent session not found'); } const trimmedText = payload.text.trim(); const operation = buildOperation('process_message'); await this.sessionStore.createOperation(userId, sessionId, operation); await this.sessionStore.appendMessage( userId, sessionId, buildUserMessage(trimmedText, payload.clientMessageId), ); void this.processMessageOperation({ userId, sessionId, operationId: operation.operationId, latestUserText: trimmedText, quickFillRequested: Boolean(payload.quickFillRequested), }); return { operation, }; } async streamMessage(params: { request: Request; response: Response; userId: string; sessionId: string; payload: SendCustomWorldAgentMessageRequest; }) { const session = await this.sessionStore.get(params.userId, params.sessionId); if (!session) { throw notFound('custom world agent session not found'); } prepareEventStreamResponse(params.request, params.response); const trimmedText = params.payload.text.trim(); const userMessage = buildUserMessage( trimmedText, params.payload.clientMessageId, ); await this.sessionStore.appendMessage( params.userId, params.sessionId, userMessage, ); let latestReplyText = ''; try { const nextSession = await this.applyMessageTurn({ userId: params.userId, sessionId: params.sessionId, latestUserText: trimmedText, quickFillRequested: Boolean(params.payload.quickFillRequested), relatedOperationId: null, onReplyUpdate: (text) => { if (!text.trim() || text === latestReplyText) { return; } latestReplyText = text; writeSseEvent(params.response, 'reply_delta', { text, }); }, }); writeSseEvent(params.response, 'session', { session: nextSession, }); writeSseEvent(params.response, 'done', { ok: true, }); } catch (error) { writeSseEvent(params.response, 'error', { message: error instanceof Error ? error.message : 'stream custom world message failed', }); } finally { params.response.end(); } } async executeAction( userId: string, sessionId: string, payload: CustomWorldAgentActionRequest, ): Promise { const session = await this.sessionStore.get(userId, sessionId); if (!session) { throw notFound('custom world agent session not found'); } if (payload.action === 'draft_foundation') { if (session.progressPercent < 100) { throw badRequest('draft_foundation requires progressPercent >= 100'); } const operation = buildOperation('draft_foundation'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processDraftFoundationOperation({ userId, sessionId, operationId: operation.operationId, }); return { operation, }; } if ( payload.action === 'update_draft_card' || payload.action === 'sync_result_profile' || payload.action === 'generate_characters' || payload.action === 'generate_landmarks' || payload.action === 'generate_role_assets' || payload.action === 'sync_role_assets' ) { if ( session.stage !== 'object_refining' && session.stage !== 'visual_refining' ) { throw badRequest( `${payload.action} is only available during object_refining or visual_refining`, ); } const hasDraftFoundation = Boolean( normalizeFoundationDraftProfile(session.draftProfile) && session.draftCards.length > 0, ); if (!hasDraftFoundation) { throw badRequest( `${payload.action} requires an existing draft foundation`, ); } } if (payload.action === 'update_draft_card') { if (!payload.cardId.trim()) { throw badRequest('update_draft_card requires cardId'); } if (!Array.isArray(payload.sections) || payload.sections.length === 0) { throw badRequest('update_draft_card requires sections'); } const operation = buildOperation('update_draft_card'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processUpdateDraftCardOperation({ userId, sessionId, operationId: operation.operationId, payload, }); return { operation, }; } if (payload.action === 'sync_result_profile') { const normalizedProfile = normalizeCustomWorldProfile( payload.profile, '', ); if (!normalizedProfile) { throw badRequest('sync_result_profile requires a valid profile'); } const operation = buildOperation('sync_result_profile'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processSyncResultProfileOperation({ userId, sessionId, operationId: operation.operationId, payload: { ...payload, profile: normalizedProfile as unknown as Record, }, }); return { operation, }; } if (payload.action === 'generate_characters') { if (payload.count < 1 || payload.count > 3) { throw badRequest('generate_characters count must be between 1 and 3'); } const operation = buildOperation('generate_characters'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processGenerateCharactersOperation({ userId, sessionId, operationId: operation.operationId, payload, }); return { operation, }; } if (payload.action === 'generate_landmarks') { if (payload.count < 1 || payload.count > 3) { throw badRequest('generate_landmarks count must be between 1 and 3'); } const operation = buildOperation('generate_landmarks'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processGenerateLandmarksOperation({ userId, sessionId, operationId: operation.operationId, payload, }); return { operation, }; } if (payload.action === 'generate_role_assets') { if (!Array.isArray(payload.roleIds) || payload.roleIds.length !== 1) { throw badRequest( 'generate_role_assets currently requires exactly one roleId', ); } const operation = buildOperation('generate_role_assets'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processGenerateRoleAssetsOperation({ userId, sessionId, operationId: operation.operationId, payload, }); return { operation, }; } if (payload.action === 'sync_role_assets') { if (!payload.roleId.trim()) { throw badRequest('sync_role_assets requires roleId'); } if ( !payload.portraitPath.trim() || !payload.generatedVisualAssetId.trim() ) { throw badRequest( 'sync_role_assets requires portraitPath and generatedVisualAssetId', ); } const operation = buildOperation('sync_role_assets'); await this.sessionStore.createOperation(userId, sessionId, operation); void this.processSyncRoleAssetsOperation({ userId, sessionId, operationId: operation.operationId, payload, }); return { operation, }; } if (payload.action === 'publish_world') { throw badRequest('publish_world is not available in phase5'); } throw badRequest(`${payload.action} is not available in phase5`); } async getOperation(userId: string, sessionId: string, operationId: string) { return this.sessionStore.getOperation(userId, sessionId, operationId); } async getCardDetail(userId: string, sessionId: string, cardId: string) { const session = await this.sessionStore.get(userId, sessionId); if (!session) { return null; } return this.draftCompiler.getDraftCardDetail(session.draftProfile, cardId); } private async applyMessageTurn(params: { userId: string; sessionId: string; latestUserText: string; quickFillRequested: boolean; relatedOperationId?: string | null; onReplyUpdate?: (text: string) => void; }) { const latestSession = (await this.sessionStore.get( params.userId, params.sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const shouldPreserveDraftStage = (latestSession.stage === 'object_refining' || latestSession.stage === 'visual_refining') && latestSession.draftCards.length > 0; const assistantTurn = await this.eightAnchorSingleTurnService.streamTurn( { currentTurn: latestSession.currentTurn + 1, progressPercent: latestSession.progressPercent, quickFillRequested: params.quickFillRequested, currentAnchorContent: latestSession.anchorContent, chatHistory: latestSession.messages .filter( (message): message is CustomWorldAgentMessage => (message.role === 'user' || message.role === 'assistant') && Boolean(message.text.trim()), ) .map((message) => ({ role: message.role, content: message.text, })), }, { onReplyUpdate: params.onReplyUpdate, }, ); const nextCreatorIntent = buildCreatorIntentFromEightAnchorContent( assistantTurn.nextAnchorContent, ); const progressPercent = Math.max( 0, Math.min(100, Math.round(assistantTurn.progressPercent)), ); const creatorIntentReadiness = progressPercent >= 100 ? { isReady: true, completedKeys: [ 'world_hook', 'player_premise', 'theme_and_tone', 'core_conflict', 'relationship_seed', 'iconic_element', ], missingKeys: [], } : evaluateCreatorIntentReadiness(nextCreatorIntent); const derivedState = buildDerivedState(nextCreatorIntent, true); const preservedStage = latestSession.stage === 'visual_refining' ? ('visual_refining' as const) : ('object_refining' as const); const shouldStayInDraftStage = shouldPreserveDraftStage && progressPercent >= 100; const nextStage = shouldStayInDraftStage ? preservedStage : derivedState.stage; const assistantMessage = { id: `message-${crypto.randomBytes(8).toString('hex')}`, role: 'assistant', kind: 'chat', text: assistantTurn.replyText, createdAt: new Date().toISOString(), relatedOperationId: params.relatedOperationId ?? null, } satisfies CustomWorldAgentMessage; await this.sessionStore.replaceDerivedState(params.userId, params.sessionId, { currentTurn: latestSession.currentTurn + 1, anchorContent: assistantTurn.nextAnchorContent, progressPercent, lastAssistantReply: assistantTurn.replyText, stage: nextStage, focusCardId: shouldStayInDraftStage ? latestSession.focusCardId : null, creatorIntent: nextCreatorIntent, creatorIntentReadiness, anchorPack: buildAnchorPackFromEightAnchorContent( assistantTurn.nextAnchorContent, progressPercent, ), draftProfile: shouldStayInDraftStage ? latestSession.draftProfile : progressPercent >= 100 ? { title: buildDraftTitleFromIntent(nextCreatorIntent), summary: buildDraftSummaryFromIntent(nextCreatorIntent), } : derivedState.draftProfile, draftCards: shouldStayInDraftStage ? latestSession.draftCards : [], assetCoverage: shouldStayInDraftStage ? latestSession.assetCoverage : rebuildRoleAssetCoverage( progressPercent >= 100 ? { title: buildDraftTitleFromIntent(nextCreatorIntent), summary: buildDraftSummaryFromIntent(nextCreatorIntent), } : derivedState.draftProfile, ), pendingClarifications: progressPercent >= 100 ? [] : derivedState.pendingClarifications, suggestedActions: shouldStayInDraftStage ? buildSuggestedActions({ stage: preservedStage, isReady: true, draftProfile: latestSession.draftProfile, draftCards: latestSession.draftCards, }) : progressPercent >= 100 ? [ { id: 'draft_foundation', type: 'draft_foundation', label: '生成游戏设定草稿', }, ] : [], recommendedReplies: [], }); await this.sessionStore.appendMessage( params.userId, params.sessionId, assistantMessage, ); return (await this.sessionStore.getSnapshot( params.userId, params.sessionId, )) as CustomWorldAgentSessionSnapshot; } private async processDraftFoundationOperation(params: { userId: string; sessionId: string; operationId: string; }) { const { userId, sessionId, operationId } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '整理世界骨架', phaseDetail: '正在校验已确认锚点,并准备第一版世界框架生成链路。', progress: 12, }); await sleep(30); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } if (latestSession.progressPercent < 100) { throw new Error('session progressPercent is below 100'); } const creatorIntent = buildCreatorIntentFromEightAnchorContent( latestSession.anchorContent, ); const anchorPack = buildAnchorPackFromEightAnchorContent( latestSession.anchorContent, latestSession.progressPercent, ); const draftProfile = await this.foundationDraftService.generate({ creatorIntent, anchorPack, anchorContent: latestSession.anchorContent, onProgress: async (progress) => { await this.sessionStore.updateOperation( userId, sessionId, operationId, { status: 'running', phaseLabel: progress.phaseLabel, phaseDetail: progress.phaseDetail, progress: progress.progress, }, ); }, }); const draftWithAssets = this.autoAssetService ? await this.autoAssetService.populateDraftAssets({ draftProfile, onProgress: async (progress) => { await this.sessionStore.updateOperation( userId, sessionId, operationId, { status: 'running', phaseLabel: progress.phaseLabel, phaseDetail: progress.phaseDetail, progress: progress.progress, }, ); }, }) : { draftProfile, assetCoverage: rebuildRoleAssetCoverage(draftProfile), warnings: [], }; await this.sessionStore.updateOperation(userId, sessionId, operationId, { phaseLabel: '编译草稿卡', phaseDetail: '正在把世界底稿整理成可浏览的卡片摘要和详情结构。', progress: 98, }); const draftCards = this.draftCompiler.compileDraftCards( draftWithAssets.draftProfile, ); const assetCoverage = draftWithAssets.assetCoverage; const nextStage = 'object_refining' as const; const nextSuggestedActions = buildSuggestedActions({ stage: nextStage, isReady: true, draftProfile: draftWithAssets.draftProfile, draftCards, }); await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: nextStage, creatorIntent, anchorPack, draftProfile: draftWithAssets.draftProfile as unknown as Record, draftCards, assetCoverage, pendingClarifications: [], suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendCheckpoint(userId, sessionId, { label: '世界底稿 V1', }); await this.sessionStore.appendMessage( userId, sessionId, buildFoundationDraftAssistantMessage({ relatedOperationId: operationId, draftProfile: draftWithAssets.draftProfile, warnings: draftWithAssets.warnings, }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '世界底稿已生成', phaseDetail: draftWithAssets.warnings.length > 0 ? `第一版世界底稿和 ${draftCards.length} 张草稿卡已经整理完成,另有 ${draftWithAssets.warnings.length} 项资产补齐待后续处理。` : `第一版世界底稿和 ${draftCards.length} 张草稿卡已经整理完成。`, progress: 100, error: null, }); } catch (error) { const currentOperation = await this.sessionStore.getOperation( userId, sessionId, operationId, ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: currentOperation?.phaseLabel?.trim() || '底稿生成失败', phaseDetail: currentOperation?.phaseDetail?.trim() || '这一轮没有成功把设定编成世界底稿。', progress: 100, error: error instanceof Error ? error.message : 'draft foundation failed', }); } } private async processUpdateDraftCardOperation(params: { userId: string; sessionId: string; operationId: string; payload: Extract< CustomWorldAgentActionRequest, { action: 'update_draft_card' } >; }) { const { userId, sessionId, operationId, payload } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '写回草稿设定', phaseDetail: '正在把这次编辑内容写回当前世界底稿。', progress: 34, }); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const nextDraftProfile = updateDraftCardSections({ draftProfile: (latestSession.draftProfile ?? {}) as Record< string, unknown >, cardId: payload.cardId, sections: payload.sections, }); await this.sessionStore.updateOperation(userId, sessionId, operationId, { phaseLabel: '重编译草稿卡', phaseDetail: '正在同步更新草稿摘要和详情内容。', progress: 72, }); const nextDraftCards = this.draftCompiler.compileDraftCards(nextDraftProfile); const assetCoverage = rebuildRoleAssetCoverage(nextDraftProfile); const nextStage = latestSession.stage === 'visual_refining' ? ('visual_refining' as const) : ('object_refining' as const); const nextSuggestedActions = buildSuggestedActions({ stage: nextStage, isReady: true, draftProfile: nextDraftProfile, draftCards: nextDraftCards, }); const updatedDetail = this.draftCompiler.getDraftCardDetail( nextDraftProfile, payload.cardId, ); const changedSectionIds = new Set( payload.sections .map((section) => section.sectionId.trim()) .filter(Boolean), ); await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: nextStage, draftProfile: nextDraftProfile, draftCards: nextDraftCards, assetCoverage, focusCardId: payload.cardId, suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendCheckpoint(userId, sessionId, { label: `编辑 ${updatedDetail?.title || '草稿卡'}`, }); await this.sessionStore.appendMessage( userId, sessionId, buildActionResultMessage({ relatedOperationId: operationId, text: this.changeSummaryService.buildSummary({ action: 'update_draft_card', cardId: payload.cardId, changedLabels: updatedDetail?.sections .filter((section) => changedSectionIds.has(section.id)) .map((section) => section.label) ?? [], draftProfile: nextDraftProfile, }), }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '草稿设定已保存', phaseDetail: `「${updatedDetail?.title || '当前卡片'}」的设定已经同步更新。`, progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '保存失败', phaseDetail: '这次草稿编辑没有成功写回到底稿。', progress: 100, error: error instanceof Error ? error.message : 'update draft card failed', }); } } private async processSyncResultProfileOperation(params: { userId: string; sessionId: string; operationId: string; payload: Extract< CustomWorldAgentActionRequest, { action: 'sync_result_profile' } >; }) { const { userId, sessionId, operationId, payload } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '同步结果页快照', phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。', progress: 36, }); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const resultProfile = payload.profile as unknown as CustomWorldProfile; const nextDraftProfile = syncResultProfileIntoDraftProfile({ currentDraftProfile: latestSession.draftProfile, resultProfile, }); await this.sessionStore.updateOperation(userId, sessionId, operationId, { phaseLabel: '重编译草稿摘要', phaseDetail: '正在刷新草稿卡摘要、资产覆盖和结果页恢复快照。', progress: 72, }); const nextDraftCards = this.draftCompiler.compileDraftCards(nextDraftProfile); const assetCoverage = rebuildRoleAssetCoverage(nextDraftProfile); const nextStage = latestSession.stage === 'visual_refining' ? ('visual_refining' as const) : ('object_refining' as const); const nextSuggestedActions = buildSuggestedActions({ stage: nextStage, isReady: true, draftProfile: nextDraftProfile, draftCards: nextDraftCards, }); await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: nextStage, draftProfile: nextDraftProfile, draftCards: nextDraftCards, assetCoverage, suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendCheckpoint(userId, sessionId, { label: '同步结果页编辑', }); await this.sessionStore.appendMessage( userId, sessionId, buildActionResultMessage({ relatedOperationId: operationId, text: '结果页里的最新世界结构已经同步回当前草稿。', }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '结果页快照已同步', phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。', progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '结果页同步失败', phaseDetail: '这一轮结果页编辑没有成功写回当前草稿。', progress: 100, error: error instanceof Error ? error.message : 'sync result profile failed', }); } } private async processGenerateCharactersOperation(params: { userId: string; sessionId: string; operationId: string; payload: Extract< CustomWorldAgentActionRequest, { action: 'generate_characters' } >; }) { const { userId, sessionId, operationId, payload } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '生成新角色', phaseDetail: '正在围绕当前世界底稿补出新角色。', progress: 32, }); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const generationResult = await this.entityGenerationService.generateAdditionalCharacters({ creatorIntent: latestSession.creatorIntent, anchorPack: latestSession.anchorPack, draftProfile: (latestSession.draftProfile ?? {}) as Record< string, unknown >, count: payload.count, promptText: payload.promptText, anchorCardIds: payload.anchorCardIds && payload.anchorCardIds.length > 0 ? payload.anchorCardIds : latestSession.focusCardId ? [latestSession.focusCardId] : [getWorldFoundationCardId()], }); await this.sessionStore.updateOperation(userId, sessionId, operationId, { phaseLabel: '插入新角色卡', phaseDetail: '正在把新角色插回草稿并刷新卡片列表。', progress: 74, }); const nextDraftCards = this.draftCompiler.compileDraftCards( generationResult.draftProfile, ); const assetCoverage = rebuildRoleAssetCoverage( generationResult.draftProfile, ); const nextStage = latestSession.stage === 'visual_refining' ? ('visual_refining' as const) : ('object_refining' as const); const nextSuggestedActions = buildSuggestedActions({ stage: nextStage, isReady: true, draftProfile: generationResult.draftProfile, draftCards: nextDraftCards, }); const focusCardId = generationResult.generatedCharacters[0]?.id ?? null; await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: nextStage, draftProfile: generationResult.draftProfile, draftCards: nextDraftCards, assetCoverage, focusCardId, suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendCheckpoint(userId, sessionId, { label: `新增角色 ${generationResult.generatedCharacters.length} 个`, }); await this.sessionStore.appendMessage( userId, sessionId, buildActionResultMessage({ relatedOperationId: operationId, text: this.changeSummaryService.buildSummary({ action: 'generate_characters', names: generationResult.generatedCharacters.map( (entry) => entry.name, ), draftProfile: generationResult.draftProfile, }), }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '新角色已加入草稿', phaseDetail: `已补出 ${generationResult.generatedCharacters.length} 个新角色。`, progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '角色生成失败', phaseDetail: '这一轮没有成功补出新角色。', progress: 100, error: error instanceof Error ? error.message : 'generate characters failed', }); } } private async processGenerateLandmarksOperation(params: { userId: string; sessionId: string; operationId: string; payload: Extract< CustomWorldAgentActionRequest, { action: 'generate_landmarks' } >; }) { const { userId, sessionId, operationId, payload } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '生成新地点', phaseDetail: '正在围绕当前世界底稿补出新地点。', progress: 32, }); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const generationResult = await this.entityGenerationService.generateAdditionalLandmarks({ creatorIntent: latestSession.creatorIntent, anchorPack: latestSession.anchorPack, draftProfile: (latestSession.draftProfile ?? {}) as Record< string, unknown >, count: payload.count, promptText: payload.promptText, anchorCardIds: payload.anchorCardIds && payload.anchorCardIds.length > 0 ? payload.anchorCardIds : latestSession.focusCardId ? [latestSession.focusCardId] : [getWorldFoundationCardId()], }); await this.sessionStore.updateOperation(userId, sessionId, operationId, { phaseLabel: '插入新地点卡', phaseDetail: '正在把新地点插回草稿并刷新卡片列表。', progress: 74, }); const nextDraftCards = this.draftCompiler.compileDraftCards( generationResult.draftProfile, ); const assetCoverage = rebuildRoleAssetCoverage( generationResult.draftProfile, ); const nextStage = latestSession.stage === 'visual_refining' ? ('visual_refining' as const) : ('object_refining' as const); const nextSuggestedActions = buildSuggestedActions({ stage: nextStage, isReady: true, draftProfile: generationResult.draftProfile, draftCards: nextDraftCards, }); const focusCardId = generationResult.generatedLandmarks[0]?.id ?? null; await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: nextStage, draftProfile: generationResult.draftProfile, draftCards: nextDraftCards, assetCoverage, focusCardId, suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendCheckpoint(userId, sessionId, { label: `新增地点 ${generationResult.generatedLandmarks.length} 个`, }); await this.sessionStore.appendMessage( userId, sessionId, buildActionResultMessage({ relatedOperationId: operationId, text: this.changeSummaryService.buildSummary({ action: 'generate_landmarks', names: generationResult.generatedLandmarks.map( (entry) => entry.name, ), draftProfile: generationResult.draftProfile, }), }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '新地点已加入草稿', phaseDetail: `已补出 ${generationResult.generatedLandmarks.length} 个新地点。`, progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '地点生成失败', phaseDetail: '这一轮没有成功补出新地点。', progress: 100, error: error instanceof Error ? error.message : 'generate landmarks failed', }); } } private async processGenerateRoleAssetsOperation(params: { userId: string; sessionId: string; operationId: string; payload: Extract< CustomWorldAgentActionRequest, { action: 'generate_role_assets' } >; }) { const { userId, sessionId, operationId, payload } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '准备角色资产工坊', phaseDetail: '正在校验角色并整理工坊上下文。', progress: 40, }); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const roleId = payload.roleIds[0]!; const studioContext = this.assetBridgeService.buildRoleAssetStudioContext( latestSession.draftProfile, roleId, ); const nextStage = 'visual_refining' as const; const nextSuggestedActions = buildSuggestedActions({ stage: nextStage, isReady: true, draftProfile: latestSession.draftProfile, draftCards: latestSession.draftCards, }); await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: nextStage, focusCardId: roleId, suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendMessage( userId, sessionId, buildActionResultMessage({ relatedOperationId: operationId, text: `已为「${studioContext.roleName}」准备好角色资产工坊,先生成主图候选,再补核心动作。`, }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '角色资产工坊已就绪', phaseDetail: `「${studioContext.roleName}」现在可以开始生成主图和动作。`, progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '角色资产工坊准备失败', phaseDetail: '这一轮没有成功进入角色资产工坊。', progress: 100, error: error instanceof Error ? error.message : 'generate role assets failed', }); } } private async processSyncRoleAssetsOperation(params: { userId: string; sessionId: string; operationId: string; payload: Extract< CustomWorldAgentActionRequest, { action: 'sync_role_assets' } >; }) { const { userId, sessionId, operationId, payload } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: '同步角色资产', phaseDetail: '正在把主图与动作结果写回当前世界草稿。', progress: 36, }); const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const syncResult = this.assetBridgeService.applyRoleAssetPublishResult( latestSession.draftProfile, payload, ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { phaseLabel: '刷新角色卡摘要', phaseDetail: '正在同步更新角色卡状态与资产覆盖。', progress: 72, }); const nextDraftCards = this.draftCompiler.compileDraftCards( syncResult.draftProfile, ); const assetCoverage = rebuildRoleAssetCoverage(syncResult.draftProfile); const nextSuggestedActions = buildSuggestedActions({ stage: 'visual_refining', isReady: true, draftProfile: syncResult.draftProfile, draftCards: nextDraftCards, }); await this.sessionStore.replaceDerivedState(userId, sessionId, { stage: 'visual_refining', draftProfile: syncResult.draftProfile, draftCards: nextDraftCards, assetCoverage, focusCardId: payload.roleId, suggestedActions: nextSuggestedActions, recommendedReplies: [], }); await this.sessionStore.appendCheckpoint(userId, sessionId, { label: `同步角色资产 ${syncResult.updatedAssetSummary.roleName}`, }); await this.sessionStore.appendMessage( userId, sessionId, buildActionResultMessage({ relatedOperationId: operationId, text: buildRoleAssetSyncResultText({ roleName: syncResult.updatedAssetSummary.roleName, assetStatusLabel: resolveRoleAssetStatusLabel( syncResult.updatedAssetSummary.status, ), }), }), ); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '角色资产已同步', phaseDetail: `「${syncResult.updatedAssetSummary.roleName}」的资产状态已更新为${resolveRoleAssetStatusLabel(syncResult.updatedAssetSummary.status)}。`, progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '角色资产同步失败', phaseDetail: '这一轮没有成功把角色资产写回草稿。', progress: 100, error: error instanceof Error ? error.message : 'sync role assets failed', }); } } private async processMessageOperation(params: { userId: string; sessionId: string; operationId: string; latestUserText: string; quickFillRequested: boolean; }) { const { userId, sessionId, operationId, latestUserText, quickFillRequested, } = params; try { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'running', phaseLabel: quickFillRequested ? '补全剩余设定' : '整理当前设定', phaseDetail: quickFillRequested ? '正在基于当前方向补齐剩余设定。' : '正在把这轮输入沉淀成新的完整设定。', progress: 45, }); await sleep(30); if (latestUserText.includes(PHASE2_FORCE_FAIL_TOKEN)) { throw new Error('phase2 forced failure'); } const latestSession = (await this.sessionStore.get( userId, sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const shouldPreserveDraftStage = (latestSession.stage === 'object_refining' || latestSession.stage === 'visual_refining') && latestSession.draftCards.length > 0; await this.applyMessageTurn({ userId, sessionId, latestUserText, quickFillRequested, relatedOperationId: operationId, }); await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'completed', phaseLabel: '设定已更新', phaseDetail: shouldPreserveDraftStage ? '这轮补充已挂回当前底稿语境,现有草稿卡保持可继续浏览。' : quickFillRequested ? '剩余设定已补全,现在可以进入游戏设定草稿生成。' : '这一轮的设定更新已经完成。', progress: 100, error: null, }); } catch (error) { await this.sessionStore.updateOperation(userId, sessionId, operationId, { status: 'failed', phaseLabel: '处理失败', phaseDetail: '这一轮消息没有成功沉淀为当前设定。', progress: 100, error: error instanceof Error ? error.message : 'process message failed', }); } } }