1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-14 21:49:44 +08:00
parent fa435aa6a6
commit 6363267bca
13 changed files with 2743 additions and 237 deletions

View File

@@ -0,0 +1,131 @@
import { expect, test } from 'vitest';
import type {
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
} from '../../packages/shared/src/contracts/customWorldAgent';
import {
buildAgentDraftFoundationGenerationProgress,
buildAgentDraftFoundationSettingText,
isDraftFoundationOperationRunning,
} from './customWorldAgentGenerationProgress';
const baseOperation: CustomWorldAgentOperationRecord = {
operationId: 'operation-1',
type: 'draft_foundation',
status: 'running',
phaseLabel: '生成世界底稿',
phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
progress: 38,
error: null,
};
const baseSession: CustomWorldAgentSessionSnapshot = {
sessionId: 'session-1',
stage: 'foundation_review',
focusCardId: null,
creatorIntent: {
sourceMode: 'card',
worldHook: '海雾、旧灯塔和失控航路交织的边缘群岛',
themeKeywords: ['海雾', '灯塔', '旧航路'],
toneDirectives: ['压抑', '悬疑'],
playerPremise: '玩家刚回到群岛,准备调查父亲沉船的真相。',
openingSituation: '首夜就有陌生船只在禁航区点灯。',
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
keyFactions: [],
keyCharacters: [],
keyLandmarks: [],
iconicElements: ['会移动的海雾'],
forbiddenDirectives: [],
rawSettingText: '',
},
creatorIntentReadiness: {
isReady: true,
completedKeys: [],
missingKeys: [],
},
anchorPack: null,
lockState: null,
draftProfile: null,
messages: [
{
id: 'message-1',
role: 'user',
kind: 'chat',
text: '我想做一个被海雾吞没的旧航路世界。',
createdAt: '2026-04-14T10:00:00.000Z',
relatedOperationId: null,
},
],
draftCards: [],
pendingClarifications: [],
suggestedActions: [],
recommendedReplies: [],
qualityFindings: [],
assetCoverage: {
roleAssets: [],
sceneAssets: [],
allRoleAssetsReady: false,
allSceneAssetsReady: false,
},
updatedAt: '2026-04-14T10:00:00.000Z',
};
test('maps running draft_foundation operation to legacy generation progress', () => {
const progress = buildAgentDraftFoundationGenerationProgress(
baseOperation,
1_000,
5_000,
);
expect(progress).not.toBeNull();
expect(progress?.phaseId).toBe('foundation');
expect(progress?.batchLabel).toBe('生成世界底稿');
expect(progress?.overallProgress).toBe(38);
expect(progress?.elapsedMs).toBe(4_000);
expect(progress?.estimatedRemainingMs).toBeGreaterThan(0);
expect(progress?.steps.map((step) => step.status)).toEqual([
'completed',
'active',
'pending',
'pending',
]);
expect(isDraftFoundationOperationRunning(baseOperation)).toBe(true);
});
test('marks all legacy progress steps complete when draft foundation finishes', () => {
const progress = buildAgentDraftFoundationGenerationProgress(
{
...baseOperation,
status: 'completed',
phaseLabel: '世界底稿已生成',
phaseDetail: '第一版世界底稿和 6 张草稿卡已经整理完成。',
progress: 100,
},
1_000,
5_000,
);
expect(progress?.phaseId).toBe('workspace');
expect(progress?.estimatedRemainingMs).toBe(0);
expect(progress?.steps.every((step) => step.status === 'completed')).toBe(
true,
);
});
test('builds readable draft setting text from creator intent first', () => {
const settingText = buildAgentDraftFoundationSettingText(baseSession);
expect(settingText).toContain('世界核心');
expect(settingText).toContain('玩家开局');
expect(settingText).toContain('标志元素');
});
test('falls back to latest user message when creator intent is unavailable', () => {
const settingText = buildAgentDraftFoundationSettingText({
...baseSession,
creatorIntent: null,
});
expect(settingText).toBe('我想做一个被海雾吞没的旧航路世界。');
});

View 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() ?? '正在整理当前共创设定。';
}