518 lines
15 KiB
TypeScript
518 lines
15 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 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() ||
|
||
'正在整理当前共创设定。'
|
||
);
|
||
}
|