210
src/services/customWorldAgentGenerationProgress.ts
Normal file
210
src/services/customWorldAgentGenerationProgress.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type {
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
} from '../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
CustomWorldGenerationProgress,
|
||||
CustomWorldGenerationStep,
|
||||
} from '../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
buildCustomWorldCreatorIntentDisplayText,
|
||||
buildCustomWorldCreatorIntentGenerationText,
|
||||
normalizeCustomWorldCreatorIntent,
|
||||
} from './customWorldCreatorIntent';
|
||||
|
||||
const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
{
|
||||
id: 'queue',
|
||||
label: '接收生成请求',
|
||||
detail: '正在锁定当前已确认的世界锚点与草稿范围。',
|
||||
},
|
||||
{
|
||||
id: 'foundation',
|
||||
label: '生成世界底稿',
|
||||
detail: '正在根据世界核心、关系种子与冲突线编排第一版世界结构。',
|
||||
},
|
||||
{
|
||||
id: 'cards',
|
||||
label: '编译草稿卡',
|
||||
detail: '正在整理世界卡、角色卡与地点卡的摘要和详情。',
|
||||
},
|
||||
{
|
||||
id: 'workspace',
|
||||
label: '准备精修工作区',
|
||||
detail: '正在写回草稿数据,并切回可继续精修的工作区。',
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
}>;
|
||||
|
||||
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 resolveAgentDraftFoundationStepIndex(
|
||||
operation: CustomWorldAgentOperationRecord,
|
||||
) {
|
||||
const progress = clampProgress(operation.progress);
|
||||
const phaseLabel = operation.phaseLabel.trim();
|
||||
|
||||
if (
|
||||
operation.status === 'completed' ||
|
||||
phaseLabel.includes('世界底稿已生成') ||
|
||||
progress >= 90
|
||||
) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (phaseLabel.includes('编译草稿卡') || progress >= 60) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (phaseLabel.includes('生成世界底稿') || progress >= 25) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function buildAgentDraftFoundationSteps(
|
||||
operation: CustomWorldAgentOperationRecord,
|
||||
activeStepIndex: number,
|
||||
) {
|
||||
return AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.map((step, index) => {
|
||||
const isCompleted =
|
||||
operation.status === 'completed' || index < activeStepIndex;
|
||||
const isActive = !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 activeStep =
|
||||
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 displayText =
|
||||
buildCustomWorldCreatorIntentDisplayText(creatorIntent).trim();
|
||||
const generationText =
|
||||
buildCustomWorldCreatorIntentGenerationText(creatorIntent).trim();
|
||||
|
||||
if (displayText) {
|
||||
return displayText;
|
||||
}
|
||||
|
||||
if (generationText) {
|
||||
return generationText;
|
||||
}
|
||||
|
||||
if (creatorIntent.rawSettingText.trim()) {
|
||||
return creatorIntent.rawSettingText.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const latestUserMessage = [...session.messages]
|
||||
.reverse()
|
||||
.find((message) => message.role === 'user' && message.text.trim());
|
||||
|
||||
return latestUserMessage?.text.trim() ?? '正在整理当前共创设定。';
|
||||
}
|
||||
Reference in New Issue
Block a user