import type { CustomWorldAgentOperationRecord, CustomWorldAgentSessionSnapshot, EightAnchorContent, } from '../../packages/shared/src/contracts/customWorldAgent'; import type { CustomWorldGenerationProgress, CustomWorldGenerationStep, } from '../../packages/shared/src/contracts/runtime'; import { buildCustomWorldCreatorIntentFoundationText, normalizeCustomWorldCreatorIntent, } from './customWorldCreatorIntent'; export type CustomWorldStructuredAnchorEntry = { id: string; label: string; value: string; }; function normalizeAnchorText(value: string | null | undefined) { return typeof value === 'string' ? value.trim() : ''; } function buildEightAnchorFoundationText(anchorContent: EightAnchorContent) { return [ ['世界承诺', anchorContent.worldPromise], ['玩家幻想', anchorContent.playerFantasy], ['主题边界', anchorContent.themeBoundary], ['玩家切入口', anchorContent.playerEntryPoint], ['核心冲突', anchorContent.coreConflict], ['关键关系', anchorContent.keyRelationships], ['暗线与揭示', anchorContent.hiddenLines], ['标志元素', anchorContent.iconicElements], ] .map(([label, value]) => { const text = normalizeAnchorText(value); return text ? `${label}:${text}` : ''; }) .filter((line) => line) .join('\n'); } export function buildAgentDraftFoundationAnchorEntries( session: CustomWorldAgentSessionSnapshot | null | undefined, ): CustomWorldStructuredAnchorEntry[] { if (!session) { return []; } const anchorContent = session.anchorContent; return [ { id: 'world-promise', label: '世界承诺', value: normalizeAnchorText(anchorContent.worldPromise), }, { id: 'player-fantasy', label: '玩家幻想', value: normalizeAnchorText(anchorContent.playerFantasy), }, { id: 'theme-boundary', label: '主题边界', value: normalizeAnchorText(anchorContent.themeBoundary), }, { id: 'player-entry-point', label: '玩家切入口', value: normalizeAnchorText(anchorContent.playerEntryPoint), }, { id: 'core-conflict', label: '核心冲突', value: normalizeAnchorText(anchorContent.coreConflict), }, { id: 'key-relationships', label: '关键关系', value: normalizeAnchorText(anchorContent.keyRelationships), }, { id: 'hidden-lines', label: '暗线与揭示', value: normalizeAnchorText(anchorContent.hiddenLines), }, { id: 'iconic-elements', label: '标志元素', value: normalizeAnchorText(anchorContent.iconicElements), }, ].filter((entry) => entry.value.trim()); } type AgentDraftFoundationStepDefinition = { id: string; label: string; detail: string; matchers: string[]; minProgress: number; expectedDurationMs: number; }; type AgentDraftFoundationFailedStep = { id: string; label: string; detail: string; }; // 这里按真实服务端 phaseLabel 归并步骤,避免把草稿生成硬折成 4 个失真的阶段。 const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [ { id: 'queue', label: '接收生成请求', detail: '正在校验当前锚点并准备底稿编译链路。', matchers: ['已接收请求'], minProgress: 0, expectedDurationMs: 3_000, }, { id: 'framework', label: '整理世界骨架', detail: '正在生成第一版世界框架、主题与核心冲突。', matchers: ['整理世界骨架', '生成世界底稿'], minProgress: 12, expectedDurationMs: 25_000, }, { id: 'playable-outline', label: '生成可扮演角色', detail: '正在补出玩家视角角色的首轮名单与定位。', matchers: ['生成可扮演角色'], minProgress: 16, expectedDurationMs: 18_000, }, { id: 'story-outline', label: '生成场景角色', detail: '正在整理关键 NPC、势力接口人与关系入口。', matchers: ['生成场景角色'], minProgress: 30, expectedDurationMs: 45_000, }, { id: 'landmark-seed', label: '生成关键场景', detail: '正在补出关键场景、幕 NPC 与地点连接。', matchers: ['生成关键场景'], minProgress: 44, expectedDurationMs: 36_000, }, { id: 'scene-link', label: '建立场景连接', detail: '正在整理关键场景之间的入口、连接和章节线索。', matchers: ['建立场景连接'], minProgress: 66, expectedDurationMs: 8_000, }, { id: 'playable-detail', label: '补全可扮演角色细节', detail: '正在补全可扮演角色的叙事基础与档案细节。', matchers: ['补全可扮演角色'], minProgress: 76, expectedDurationMs: 32_000, }, { id: 'story-detail', label: '补全场景角色细节', detail: '正在补全场景角色的叙事基础与档案细节。', matchers: ['补全场景角色'], minProgress: 84, expectedDurationMs: 65_000, }, { id: 'finalize', label: '编译世界底稿', detail: '正在把分批生成结果汇总成第一版可浏览的世界底稿。', matchers: ['编译世界底稿'], minProgress: 97, expectedDurationMs: 6_000, }, { id: 'role-visuals', label: '生成角色主形象', detail: '正在为关键角色补主形象预览资源。', matchers: ['生成角色主形象'], minProgress: 97, expectedDurationMs: 85_000, }, { id: 'act-backgrounds', label: '生成幕背景图', detail: '正在为场景章节的每一幕补背景图预览资源。', matchers: ['生成幕背景图'], minProgress: 98, expectedDurationMs: 85_000, }, { id: 'cards', label: '编译草稿卡', detail: '正在整理世界卡、角色卡、地点卡与详情结构。', matchers: ['编译草稿卡'], minProgress: 99, expectedDurationMs: 15_000, }, { id: 'workspace', label: '准备结果页', detail: '正在写回草稿数据,并打开可继续完善的结果页。', matchers: ['世界底稿已生成'], minProgress: 100, expectedDurationMs: 4_000, }, ] as const satisfies ReadonlyArray; const AGENT_DRAFT_FOUNDATION_FAILED_STEP = { id: 'failed', label: '生成失败', detail: '这一轮世界草稿没有编译完成,可以返回工作区补充设定后重试。', } as const satisfies AgentDraftFoundationFailedStep; function clampProgress(progress: number | null | undefined) { if (typeof progress !== 'number' || Number.isNaN(progress)) { return 0; } return Math.max(0, Math.min(100, Math.round(progress))); } function resolveAgentDraftFoundationStepIndexByProgress(progress: number) { let matchedIndex = 0; for ( let index = 0; index < AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.length - 1; index += 1 ) { const step = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[index]; if (step && progress >= step.minProgress) { matchedIndex = index; } } return matchedIndex; } function resolveFailedProgress( operation: CustomWorldAgentOperationRecord, activeStepIndex: number, ) { const progress = clampProgress(operation.progress); if (operation.status !== 'failed') { return progress; } if (progress < 100) { return progress; } const activeStep = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ?? AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0]; return Math.max(0, Math.min(99, activeStep.minProgress)); } function parseOperationUpdatedAtMs( operation: CustomWorldAgentOperationRecord, ) { const rawUpdatedAt = operation.updatedAt?.trim(); if (!rawUpdatedAt) { return null; } const parsedMs = Date.parse(rawUpdatedAt); return Number.isFinite(parsedMs) ? parsedMs : null; } function parseOperationStartedAtMs( operation: CustomWorldAgentOperationRecord, ) { const rawStartedAt = operation.startedAt?.trim(); if (!rawStartedAt) { return null; } const parsedMs = Date.parse(rawStartedAt); return Number.isFinite(parsedMs) ? parsedMs : null; } function resolveAgentDraftFoundationStepIndex( operation: CustomWorldAgentOperationRecord, ) { const progress = clampProgress(operation.progress); const phaseLabel = operation.phaseLabel.trim(); if (operation.status === 'completed' || phaseLabel.includes('世界底稿已生成')) { return AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.length - 1; } for ( let index = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.length - 2; index >= 0; index -= 1 ) { const step = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[index]; if (step?.matchers.some((matcher) => phaseLabel.includes(matcher))) { return index; } } return resolveAgentDraftFoundationStepIndexByProgress(progress); } function resolveAgentDraftFoundationFailedStep( operation: CustomWorldAgentOperationRecord, ) { if (operation.status !== 'failed') { return null; } const phaseLabel = operation.phaseLabel.trim(); const phaseDetail = operation.phaseDetail.trim(); const error = operation.error?.trim() ?? ''; return { id: AGENT_DRAFT_FOUNDATION_FAILED_STEP.id, label: phaseLabel || error || AGENT_DRAFT_FOUNDATION_FAILED_STEP.label, detail: phaseDetail || error || AGENT_DRAFT_FOUNDATION_FAILED_STEP.detail, } satisfies AgentDraftFoundationFailedStep; } function buildAgentDraftFoundationSteps( operation: CustomWorldAgentOperationRecord, activeStepIndex: number, ) { return AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.map((step, index) => { const isCompleted = operation.status === 'completed' || (operation.status === 'failed' ? index < activeStepIndex : index < activeStepIndex); const isActive = operation.status !== 'failed' && !isCompleted && index === activeStepIndex; return { id: step.id, label: step.label, detail: step.detail, completed: isCompleted ? 1 : 0, total: 1, status: isCompleted ? 'completed' : isActive ? 'active' : 'pending', } satisfies CustomWorldGenerationStep; }); } function resolveEstimatedRemainingMs( progress: number, startedAtMs: number | null, nowMs: number, status: CustomWorldAgentOperationRecord['status'], activeStepIndex: number, operationUpdatedAtMs: number | null, ) { if (status === 'completed') { return 0; } if (status === 'failed' || progress >= 100) { return null; } const activeStep = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ?? AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0]; const nextStep = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex + 1] ?? activeStep; const phaseProgressRange = Math.max( 1, nextStep.minProgress - activeStep.minProgress, ); const phaseProgressRatio = Math.max( 0, Math.min(0.95, (progress - activeStep.minProgress) / phaseProgressRange), ); const phaseStartedAtMs = operationUpdatedAtMs ?? startedAtMs; const currentPhaseElapsedMs = phaseStartedAtMs ? Math.max(0, nowMs - phaseStartedAtMs) : 0; const currentPhaseRemainingMs = Math.max( 0, Math.round( activeStep.expectedDurationMs * (1 - phaseProgressRatio) - currentPhaseElapsedMs, ), ); const followingStepsRemainingMs = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.slice( activeStepIndex + 1, ).reduce((sum, step) => sum + step.expectedDurationMs, 0); return currentPhaseRemainingMs + followingStepsRemainingMs; } export function isDraftFoundationOperation( operation: CustomWorldAgentOperationRecord | null | undefined, ): operation is CustomWorldAgentOperationRecord { return Boolean(operation && operation.type === 'draft_foundation'); } export function isDraftFoundationOperationRunning( operation: CustomWorldAgentOperationRecord | null | undefined, ) { return ( isDraftFoundationOperation(operation) && (operation.status === 'queued' || operation.status === 'running') ); } export function buildAgentDraftFoundationGenerationProgress( operation: CustomWorldAgentOperationRecord | null | undefined, fallbackStartedAtMs: number | null, nowMs = Date.now(), ): CustomWorldGenerationProgress | null { if (!isDraftFoundationOperation(operation)) { return null; } const activeStepIndex = resolveAgentDraftFoundationStepIndex(operation); const overallProgress = resolveFailedProgress(operation, activeStepIndex); // 中文注释:总耗时必须绑定服务端 operation 创建时间,避免刷新或前端重挂载后重新计时。 const startedAtMs = parseOperationStartedAtMs(operation) ?? fallbackStartedAtMs; const elapsedMs = startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0; const estimatedRemainingMs = resolveEstimatedRemainingMs( overallProgress, startedAtMs, nowMs, operation.status, activeStepIndex, parseOperationUpdatedAtMs(operation), ); const failedStep = resolveAgentDraftFoundationFailedStep(operation); const activeStep = failedStep ?? AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ?? AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0]; return { phaseId: activeStep.id, phaseLabel: operation.phaseLabel || activeStep.label, phaseDetail: operation.phaseDetail || activeStep.detail, batchLabel: activeStep.label, overallProgress, completedWeight: overallProgress, totalWeight: 100, elapsedMs, estimatedRemainingMs, activeStepIndex, steps: buildAgentDraftFoundationSteps(operation, activeStepIndex), }; } export function buildAgentDraftFoundationSettingText( session: CustomWorldAgentSessionSnapshot | null | undefined, ) { if (!session) { return ''; } const creatorIntent = normalizeCustomWorldCreatorIntent( session.creatorIntent, 'freeform', ); if (creatorIntent) { const foundationText = buildCustomWorldCreatorIntentFoundationText(creatorIntent).trim(); if (foundationText) { return foundationText; } if (creatorIntent.rawSettingText.trim()) { return creatorIntent.rawSettingText.trim(); } } const latestUserMessage = [...session.messages] .reverse() .find((message) => message.role === 'user' && message.text.trim()); const anchorSettingText = buildEightAnchorFoundationText(session.anchorContent); return ( anchorSettingText || latestUserMessage?.text.trim() || '正在整理当前共创设定。' ); }