Files
Genarrative/src/services/customWorldAgentGenerationProgress.ts
高物 75944b1f1f
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-20 21:06:48 +08:00

518 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string | null | undefined>) {
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<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
) {
if (progress >= AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[index].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() ||
'正在整理当前共创设定。'
);
}