503 lines
14 KiB
TypeScript
503 lines
14 KiB
TypeScript
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: 'playable-detail',
|
||
label: '补全可扮演角色细节',
|
||
detail: '正在补全可扮演角色的叙事基础与档案细节。',
|
||
matchers: ['补全可扮演角色'],
|
||
minProgress: 66,
|
||
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<AgentDraftFoundationStepDefinition>;
|
||
|
||
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() ||
|
||
'正在整理当前共创设定。'
|
||
);
|
||
}
|