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 joinText(items: Array) { return items.filter(Boolean).join(';'); } function buildEightAnchorFoundationText(anchorContent: EightAnchorContent) { return [ anchorContent.worldPromise ? `世界承诺:${joinText([ anchorContent.worldPromise.hook, anchorContent.worldPromise.differentiator, anchorContent.worldPromise.desiredExperience, ])}` : '', anchorContent.playerFantasy ? `玩家幻想:${joinText([ anchorContent.playerFantasy.playerRole, anchorContent.playerFantasy.corePursuit, anchorContent.playerFantasy.fearOfLoss, ])}` : '', anchorContent.themeBoundary ? `主题边界:${joinText([ anchorContent.themeBoundary.toneKeywords.join('、'), anchorContent.themeBoundary.aestheticDirectives.join('、'), anchorContent.themeBoundary.forbiddenDirectives.join('、'), ])}` : '', anchorContent.playerEntryPoint ? `玩家切入口:${joinText([ anchorContent.playerEntryPoint.openingIdentity, anchorContent.playerEntryPoint.openingProblem, anchorContent.playerEntryPoint.entryMotivation, ])}` : '', anchorContent.coreConflict ? `核心冲突:${joinText([ anchorContent.coreConflict.surfaceConflicts.join('、'), anchorContent.coreConflict.hiddenCrisis, anchorContent.coreConflict.firstTouchedConflict, ])}` : '', anchorContent.keyRelationships.length > 0 ? `关键关系:${anchorContent.keyRelationships .map((entry) => joinText([entry.pairs, entry.relationshipType, entry.secretOrCost]), ) .join(';')}` : '', anchorContent.hiddenLines ? `暗线与揭示:${joinText([ anchorContent.hiddenLines.hiddenTruths.join('、'), anchorContent.hiddenLines.misdirectionHints.join('、'), anchorContent.hiddenLines.revealPacing, ])}` : '', anchorContent.iconicElements ? `标志元素:${joinText([ anchorContent.iconicElements.iconicMotifs.join('、'), anchorContent.iconicElements.institutionsOrArtifacts.join('、'), anchorContent.iconicElements.hardRules.join('、'), ])}` : '', ] .filter(Boolean) .join('\n'); } export function buildAgentDraftFoundationAnchorEntries( session: CustomWorldAgentSessionSnapshot | null | undefined, ): CustomWorldStructuredAnchorEntry[] { if (!session) { return []; } const anchorContent = session.anchorContent; return [ { id: 'world-promise', label: '世界承诺', value: anchorContent.worldPromise ? joinText([ anchorContent.worldPromise.hook, anchorContent.worldPromise.differentiator, anchorContent.worldPromise.desiredExperience, ]) : '', }, { id: 'player-fantasy', label: '玩家幻想', value: anchorContent.playerFantasy ? joinText([ anchorContent.playerFantasy.playerRole, anchorContent.playerFantasy.corePursuit, anchorContent.playerFantasy.fearOfLoss, ]) : '', }, { id: 'theme-boundary', label: '主题边界', value: anchorContent.themeBoundary ? joinText([ anchorContent.themeBoundary.toneKeywords.join('、'), anchorContent.themeBoundary.aestheticDirectives.join('、'), anchorContent.themeBoundary.forbiddenDirectives.length > 0 ? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}` : '', ]) : '', }, { id: 'player-entry-point', label: '玩家切入口', value: anchorContent.playerEntryPoint ? joinText([ anchorContent.playerEntryPoint.openingIdentity, anchorContent.playerEntryPoint.openingProblem, anchorContent.playerEntryPoint.entryMotivation, ]) : '', }, { id: 'core-conflict', label: '核心冲突', value: anchorContent.coreConflict ? joinText([ anchorContent.coreConflict.surfaceConflicts.join('、'), anchorContent.coreConflict.hiddenCrisis, anchorContent.coreConflict.firstTouchedConflict, ]) : '', }, { id: 'key-relationships', label: '关键关系', value: anchorContent.keyRelationships.length > 0 ? anchorContent.keyRelationships .map((entry) => joinText([ entry.pairs, entry.relationshipType, entry.secretOrCost ? `代价/秘密:${entry.secretOrCost}` : '', ]), ) .join('\n') : '', }, { id: 'hidden-lines', label: '暗线与揭示', value: anchorContent.hiddenLines ? joinText([ anchorContent.hiddenLines.hiddenTruths.join('、'), anchorContent.hiddenLines.misdirectionHints.join('、'), anchorContent.hiddenLines.revealPacing, ]) : '', }, { id: 'iconic-elements', label: '标志元素', value: anchorContent.iconicElements ? joinText([ anchorContent.iconicElements.iconicMotifs.join('、'), anchorContent.iconicElements.institutionsOrArtifacts.join('、'), anchorContent.iconicElements.hardRules.join('、'), ]) : '', }, ].filter((entry) => entry.value.trim()); } type AgentDraftFoundationStepDefinition = { id: string; label: string; detail: string; matchers: string[]; minProgress: number; }; type AgentDraftFoundationFailedStep = { id: string; label: string; detail: string; }; // 这里按真实服务端 phaseLabel 归并步骤,避免把草稿生成硬折成 4 个失真的阶段。 const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [ { id: 'queue', label: '接收生成请求', detail: '正在校验当前锚点并准备底稿编译链路。', matchers: ['已接收请求'], minProgress: 0, }, { id: 'framework', label: '整理世界骨架', detail: '正在生成第一版世界框架、主题与核心冲突。', matchers: ['整理世界骨架', '生成世界底稿'], minProgress: 12, }, { id: 'playable-outline', label: '生成可扮演角色', detail: '正在补出玩家视角角色的首轮名单与定位。', matchers: ['生成可扮演角色'], minProgress: 16, }, { id: 'story-outline', label: '生成场景角色', detail: '正在整理关键 NPC、势力接口人与关系入口。', matchers: ['生成场景角色'], minProgress: 30, }, { id: 'landmark-seed', label: '生成关键场景', detail: '正在补出第一批关键场景与地点骨架。', matchers: ['生成关键场景'], minProgress: 44, }, { id: 'landmark-network', label: '建立场景连接', detail: '正在串联地点关系、线程挂钩与角色分布。', matchers: ['建立场景连接'], minProgress: 56, }, { id: 'playable-detail', label: '补全可扮演角色细节', detail: '正在补全可扮演角色的叙事基础与档案细节。', matchers: ['补全可扮演角色'], minProgress: 66, }, { id: 'story-detail', label: '补全场景角色细节', detail: '正在补全场景角色的叙事基础与档案细节。', matchers: ['补全场景角色'], minProgress: 84, }, { id: 'finalize', label: '编译世界底稿', detail: '正在把分批生成结果汇总成第一版可浏览的世界底稿。', matchers: ['编译世界底稿'], minProgress: 97, }, { id: 'role-visuals', label: '生成角色主形象', detail: '正在为关键角色补主形象预览资源。', matchers: ['生成角色主形象'], minProgress: 97, }, { id: 'act-backgrounds', label: '生成幕背景图', detail: '正在为场景章节的每一幕补背景图预览资源。', matchers: ['生成幕背景图'], minProgress: 98, }, { id: 'cards', label: '编译草稿卡', detail: '正在整理世界卡、角色卡、地点卡与详情结构。', matchers: ['编译草稿卡'], minProgress: 99, }, { id: 'workspace', label: '准备结果页', detail: '正在写回草稿数据,并打开可继续完善的结果页。', matchers: ['世界底稿已生成'], minProgress: 100, }, ] 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 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'], ) { if (status === 'completed') { return 0; } if (!startedAtMs || progress <= 0 || progress >= 100) { return null; } const elapsedMs = Math.max(0, nowMs - startedAtMs); const progressFraction = progress / 100; return Math.max(0, Math.round(elapsedMs / progressFraction - elapsedMs)); } 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, startedAtMs: number | null, nowMs = Date.now(), ): CustomWorldGenerationProgress | null { if (!isDraftFoundationOperation(operation)) { return null; } const overallProgress = clampProgress(operation.progress); const activeStepIndex = resolveAgentDraftFoundationStepIndex(operation); const elapsedMs = startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0; const estimatedRemainingMs = resolveEstimatedRemainingMs( overallProgress, startedAtMs, nowMs, operation.status, ); 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() || '正在整理当前共创设定。' ); }