Files
Genarrative/server-node/src/services/customWorldAgentOrchestrator.ts
2026-04-21 00:48:17 +08:00

1734 lines
56 KiB
TypeScript

import crypto from 'node:crypto';
import type { Request, Response } from 'express';
import type {
CreateCustomWorldAgentSessionRequest,
CustomWorldAgentActionRequest,
CustomWorldAgentActionResponse,
CustomWorldAgentMessage,
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
CustomWorldDraftCardSummary,
CustomWorldPendingClarification,
CustomWorldSuggestedAction,
SendCustomWorldAgentMessageRequest,
SendCustomWorldAgentMessageResponse,
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
import { badRequest, notFound } from '../errors.js';
import { prepareEventStreamResponse } from '../http.js';
import { normalizeCustomWorldProfile } from '../modules/custom-world/runtimeProfile.js';
import type { CustomWorldProfile } from '../modules/custom-world/runtimeTypes.js';
import { CustomWorldAgentAssetBridgeService } from './customWorldAgentAssetBridgeService.js';
import { CustomWorldAgentAutoAssetService } from './customWorldAgentAutoAssetService.js';
import { CustomWorldAgentChangeSummaryService } from './customWorldAgentChangeSummaryService.js';
import {
buildPendingClarifications,
evaluateCreatorIntentReadiness,
resolveCreatorIntentStage,
} from './customWorldAgentClarificationService.js';
import {
CustomWorldAgentDraftCompiler,
getWorldFoundationCardId,
normalizeFoundationDraftProfile,
} from './customWorldAgentDraftCompiler.js';
import { updateDraftCardSections } from './customWorldAgentDraftEditService.js';
import { CustomWorldAgentEntityGenerationService } from './customWorldAgentEntityGenerationService.js';
import { CustomWorldAgentFoundationDraftService } from './customWorldAgentFoundationDraftService.js';
import {
buildAnchorPackFromIntent,
buildDraftSummaryFromIntent,
buildDraftTitleFromIntent,
createEmptyCreatorIntentRecord,
type CustomWorldCreatorIntentRecord,
extractCreatorIntentPatch,
hasMeaningfulCreatorIntentRecord,
mergeCreatorIntentRecord,
normalizeCreatorIntentRecord,
} from './customWorldAgentIntentExtractionService.js';
import {
rebuildRoleAssetCoverage,
resolveRoleAssetStatusLabel,
} from './customWorldAgentRoleAssetStateService.js';
import {
type CustomWorldAgentSessionRecord,
CustomWorldAgentSessionStore,
} from './customWorldAgentSessionStore.js';
import {
buildAnchorPackFromEightAnchorContent,
buildCreatorIntentFromEightAnchorContent,
buildEightAnchorContentFromCreatorIntent,
estimateProgressPercentFromAnchorContent,
} from './eightAnchorCompatibilityService.js';
import { EightAnchorSingleTurnService } from './eightAnchorSingleTurnService.js';
import type { UpstreamLlmClient } from './llmClient.js';
const PHASE2_FORCE_FAIL_TOKEN = '__phase1_force_fail__';
function truncateText(value: string, maxLength: number) {
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, Math.max(0, maxLength - 1)).trim()}`;
}
function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function buildSuggestedActions(
params: {
stage?: CustomWorldAgentSessionRecord['stage'];
isReady?: boolean;
draftProfile?: unknown;
draftCards?: CustomWorldDraftCardSummary[];
} = {},
): CustomWorldSuggestedAction[] {
const profile = normalizeFoundationDraftProfile(params.draftProfile);
const actions: CustomWorldSuggestedAction[] = [
{
id: 'request_summary',
type: 'request_summary',
label:
params.stage === 'object_refining' || params.stage === 'visual_refining'
? '总结当前世界底稿'
: '总结当前设定',
},
];
if (params.stage === 'foundation_review' && params.isReady) {
actions.push({
id: 'draft_foundation',
type: 'draft_foundation',
label: '整理一版世界底稿',
});
return actions;
}
if (
(params.stage === 'object_refining' ||
params.stage === 'visual_refining') &&
profile
) {
const worldCardId =
params.draftCards?.find((entry) => entry.kind === 'world')?.id ??
getWorldFoundationCardId();
const firstCharacter = [...profile.playableNpcs, ...profile.storyNpcs][0];
const firstLandmark = profile.landmarks[0];
actions.push({
id: 'refine_world',
type: 'refine_focus_target',
label: '先看世界总卡',
targetId: worldCardId,
});
if (firstCharacter) {
actions.push({
id: `refine-character-${firstCharacter.id}`,
type: 'refine_focus_target',
label: `精修角色:${firstCharacter.name}`,
targetId: firstCharacter.id,
});
}
if (firstLandmark) {
actions.push({
id: `refine-landmark-${firstLandmark.id}`,
type: 'refine_focus_target',
label: `继续补地点:${firstLandmark.name}`,
targetId: firstLandmark.id,
});
}
}
return actions;
}
function buildOperation(type: CustomWorldAgentOperationRecord['type']) {
const phaseDetail =
type === 'draft_foundation'
? '正在把已确认设定编成第一版世界底稿。'
: type === 'update_draft_card'
? '正在把这次设定改动写回草稿。'
: type === 'sync_result_profile'
? '正在把结果页里的世界快照同步回当前草稿。'
: type === 'generate_characters'
? '正在围绕当前底稿补出新角色。'
: type === 'generate_landmarks'
? '正在围绕当前底稿补出新地点。'
: type === 'generate_role_assets'
? '正在准备角色资产工坊入口。'
: type === 'sync_role_assets'
? '正在把角色资产结果写回世界草稿。'
: '正在整理这一轮新增的世界设定。';
return {
operationId: `operation-${crypto.randomBytes(10).toString('hex')}`,
type,
status: 'queued',
phaseLabel: '已接收请求',
phaseDetail,
progress: 10,
error: null,
} satisfies CustomWorldAgentOperationRecord;
}
function buildUserMessage(
text: string,
clientMessageId: string,
): CustomWorldAgentMessage {
return {
id:
clientMessageId.trim() ||
`message-${crypto.randomBytes(8).toString('hex')}`,
role: 'user',
kind: 'chat',
text,
createdAt: new Date().toISOString(),
relatedOperationId: null,
};
}
function buildRoleAssetSyncResultText(params: {
roleName: string;
assetStatusLabel: string;
}) {
return `已把「${params.roleName}」的角色资产写回草稿,当前状态:${params.assetStatusLabel}`;
}
function syncResultProfileIntoDraftProfile(params: {
currentDraftProfile: Record<string, unknown> | null | undefined;
resultProfile: CustomWorldProfile;
}) {
const currentDraftProfile = params.currentDraftProfile ?? {};
const resultProfile = params.resultProfile;
return {
// 阶段一只回写基础摘要和完整 legacy 快照,避免把结果页的运行时结构反向拆回 foundation draft。
...currentDraftProfile,
name: resultProfile.name,
subtitle: resultProfile.subtitle,
summary: resultProfile.summary,
tone: resultProfile.tone,
playerGoal: resultProfile.playerGoal,
majorFactions: resultProfile.majorFactions,
coreConflicts: resultProfile.coreConflicts,
legacyResultProfile: resultProfile as unknown as Record<string, unknown>,
} satisfies Record<string, unknown>;
}
function buildQuestionLines(
pendingClarifications: CustomWorldPendingClarification[],
) {
return pendingClarifications.map((entry) => entry.question.trim());
}
function composeAssistantReply(params: {
openingText: string;
intent: CustomWorldCreatorIntentRecord;
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
}) {
const questionLines = buildQuestionLines(params.pendingClarifications);
return [
params.openingText,
params.isReady
? '当前设定已经齐备。'
: questionLines.slice(0, 1).join('\n'),
].join('\n');
}
function buildDerivedState(
intent: CustomWorldCreatorIntentRecord,
hasUserInput: boolean,
) {
const readiness = evaluateCreatorIntentReadiness(intent);
const pendingClarifications = buildPendingClarifications(intent, readiness);
const stage = resolveCreatorIntentStage({
hasUserInput,
readiness,
});
return {
readiness,
pendingClarifications,
stage,
anchorPack: buildAnchorPackFromIntent(intent, {
completedKeys: readiness.completedKeys,
missingKeys: readiness.missingKeys,
}),
draftProfile: {
title: buildDraftTitleFromIntent(intent),
summary: buildDraftSummaryFromIntent(intent),
},
suggestedActions: buildSuggestedActions({
stage,
isReady: readiness.isReady,
}),
};
}
function buildWelcomeMessage(params: {
seedText: string;
intent: CustomWorldCreatorIntentRecord;
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
}) {
let openingText: string;
if (params.seedText) {
openingText = `收到:${truncateText(params.seedText, 88)}`;
} else {
// When user enters without saying anything, provide a welcoming introduction
const hasAnyAnchors = hasMeaningfulCreatorIntentRecord(params.intent);
openingText = hasAnyAnchors
? '继续聊聊你的世界设定吧。'
: '你好!我是你的世界设定助手,可以帮你一起构建游戏世界的核心设定。';
}
return composeAssistantReply({
openingText,
intent: params.intent,
pendingClarifications: params.pendingClarifications,
isReady: params.isReady,
});
}
function buildFoundationDraftAssistantMessage(params: {
relatedOperationId: string;
draftProfile: unknown;
warnings?: string[];
}) {
const profile = normalizeFoundationDraftProfile(params.draftProfile);
const leadCharacter = profile?.playableNpcs[0];
const leadLandmark = profile?.landmarks[0];
const warnings = (params.warnings ?? []).filter(Boolean);
return {
id: `message-${crypto.randomBytes(8).toString('hex')}`,
role: 'assistant',
kind: 'summary',
text: [
`我先把第一版世界底稿整理出来了:${profile?.summary || '底稿已经生成完成。'}`,
'',
`当前已经落下来的第一批对象数量是:关键角色 ${profile?.playableNpcs.length ?? 0} 个,关键地点 ${profile?.landmarks.length ?? 0} 个,势力 ${profile?.factions.length ?? 0} 个。`,
`建议你先从“${profile?.name || '世界总卡'}”这张世界总卡看起${leadCharacter ? `,再顺着角色「${leadCharacter.name}」往下细修` : ''}${leadLandmark ? `,地点可以先看「${leadLandmark.name}` : ''}`,
...(warnings.length > 0
? [
'',
`这一轮有 ${warnings.length} 项资产补齐未完成,但不影响世界底稿继续精修。`,
]
: []),
].join('\n'),
createdAt: new Date().toISOString(),
relatedOperationId: params.relatedOperationId,
} satisfies CustomWorldAgentMessage;
}
function buildActionResultMessage(params: {
relatedOperationId: string;
text: string;
}) {
return {
id: `message-${crypto.randomBytes(8).toString('hex')}`,
role: 'assistant',
kind: 'action_result',
text: params.text,
createdAt: new Date().toISOString(),
relatedOperationId: params.relatedOperationId,
} satisfies CustomWorldAgentMessage;
}
function writeSseEvent(
response: Response,
event: string,
data: unknown,
) {
if (response.writableEnded) {
return;
}
response.write(`event: ${event}\n`);
response.write(`data: ${JSON.stringify(data)}\n\n`);
}
export class CustomWorldAgentOrchestrator {
private readonly foundationDraftService: CustomWorldAgentFoundationDraftService;
private readonly draftCompiler: CustomWorldAgentDraftCompiler;
private readonly entityGenerationService: CustomWorldAgentEntityGenerationService;
private readonly changeSummaryService: CustomWorldAgentChangeSummaryService;
private readonly assetBridgeService: CustomWorldAgentAssetBridgeService;
private readonly autoAssetService: CustomWorldAgentAutoAssetService | null;
private readonly eightAnchorSingleTurnService: EightAnchorSingleTurnService;
constructor(
private readonly sessionStore: CustomWorldAgentSessionStore,
llmClient: UpstreamLlmClient | null = null,
options: {
singleTurnLlmClient?: UpstreamLlmClient | null;
autoAssetService?: CustomWorldAgentAutoAssetService | null;
} = {},
) {
this.foundationDraftService = new CustomWorldAgentFoundationDraftService(
llmClient,
);
this.draftCompiler = new CustomWorldAgentDraftCompiler();
this.entityGenerationService = new CustomWorldAgentEntityGenerationService(
llmClient,
);
this.changeSummaryService = new CustomWorldAgentChangeSummaryService();
this.assetBridgeService = new CustomWorldAgentAssetBridgeService();
this.autoAssetService =
options.autoAssetService ?? null;
this.eightAnchorSingleTurnService = new EightAnchorSingleTurnService(
(options.singleTurnLlmClient ?? llmClient) ?? undefined,
);
}
async createSession(
userId: string,
payload: CreateCustomWorldAgentSessionRequest,
): Promise<CustomWorldAgentSessionSnapshot> {
const seedText = payload.seedText?.trim() ?? '';
const baseIntent = createEmptyCreatorIntentRecord('freeform');
const seedPatch = seedText
? extractCreatorIntentPatch({
currentIntent: baseIntent,
latestUserMessage: seedText,
})
: {};
const creatorIntent = mergeCreatorIntentRecord(baseIntent, seedPatch);
const derivedState = buildDerivedState(creatorIntent, Boolean(seedText));
const anchorContent = buildEightAnchorContentFromCreatorIntent(creatorIntent);
const progressPercent = seedText
? estimateProgressPercentFromAnchorContent(anchorContent)
: 0;
const fallbackWelcomeMessage = buildWelcomeMessage({
seedText,
intent: creatorIntent,
pendingClarifications: derivedState.pendingClarifications,
isReady: derivedState.readiness.isReady,
});
const record = await this.sessionStore.create(userId, {
seedText,
welcomeMessage: fallbackWelcomeMessage,
currentTurn: 0,
anchorContent,
progressPercent,
lastAssistantReply: fallbackWelcomeMessage,
creatorIntent,
creatorIntentReadiness: derivedState.readiness,
anchorPack: buildAnchorPackFromEightAnchorContent(
anchorContent,
progressPercent,
),
draftProfile: derivedState.draftProfile,
pendingClarifications: derivedState.pendingClarifications,
stage: progressPercent >= 100 ? 'foundation_review' : 'collecting_intent',
suggestedActions: derivedState.suggestedActions,
recommendedReplies: [],
});
return (await this.sessionStore.getSnapshot(
userId,
record.sessionId,
)) as CustomWorldAgentSessionSnapshot;
}
async getSessionSnapshot(userId: string, sessionId: string) {
return this.sessionStore.getSnapshot(userId, sessionId);
}
async submitMessage(
userId: string,
sessionId: string,
payload: SendCustomWorldAgentMessageRequest,
): Promise<SendCustomWorldAgentMessageResponse> {
const session = await this.sessionStore.get(userId, sessionId);
if (!session) {
throw notFound('custom world agent session not found');
}
const trimmedText = payload.text.trim();
const operation = buildOperation('process_message');
await this.sessionStore.createOperation(userId, sessionId, operation);
await this.sessionStore.appendMessage(
userId,
sessionId,
buildUserMessage(trimmedText, payload.clientMessageId),
);
void this.processMessageOperation({
userId,
sessionId,
operationId: operation.operationId,
latestUserText: trimmedText,
quickFillRequested: Boolean(payload.quickFillRequested),
});
return {
operation,
};
}
async streamMessage(params: {
request: Request;
response: Response;
userId: string;
sessionId: string;
payload: SendCustomWorldAgentMessageRequest;
}) {
const session = await this.sessionStore.get(params.userId, params.sessionId);
if (!session) {
throw notFound('custom world agent session not found');
}
prepareEventStreamResponse(params.request, params.response);
const trimmedText = params.payload.text.trim();
const userMessage = buildUserMessage(
trimmedText,
params.payload.clientMessageId,
);
await this.sessionStore.appendMessage(
params.userId,
params.sessionId,
userMessage,
);
let latestReplyText = '';
try {
const nextSession = await this.applyMessageTurn({
userId: params.userId,
sessionId: params.sessionId,
latestUserText: trimmedText,
quickFillRequested: Boolean(params.payload.quickFillRequested),
relatedOperationId: null,
onReplyUpdate: (text) => {
if (!text.trim() || text === latestReplyText) {
return;
}
latestReplyText = text;
writeSseEvent(params.response, 'reply_delta', {
text,
});
},
});
writeSseEvent(params.response, 'session', {
session: nextSession,
});
writeSseEvent(params.response, 'done', {
ok: true,
});
} catch (error) {
writeSseEvent(params.response, 'error', {
message:
error instanceof Error ? error.message : 'stream custom world message failed',
});
} finally {
params.response.end();
}
}
async executeAction(
userId: string,
sessionId: string,
payload: CustomWorldAgentActionRequest,
): Promise<CustomWorldAgentActionResponse> {
const session = await this.sessionStore.get(userId, sessionId);
if (!session) {
throw notFound('custom world agent session not found');
}
if (payload.action === 'draft_foundation') {
if (session.progressPercent < 100) {
throw badRequest('draft_foundation requires progressPercent >= 100');
}
const operation = buildOperation('draft_foundation');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processDraftFoundationOperation({
userId,
sessionId,
operationId: operation.operationId,
});
return {
operation,
};
}
if (
payload.action === 'update_draft_card' ||
payload.action === 'sync_result_profile' ||
payload.action === 'generate_characters' ||
payload.action === 'generate_landmarks' ||
payload.action === 'generate_role_assets' ||
payload.action === 'sync_role_assets'
) {
if (
session.stage !== 'object_refining' &&
session.stage !== 'visual_refining'
) {
throw badRequest(
`${payload.action} is only available during object_refining or visual_refining`,
);
}
const hasDraftFoundation = Boolean(
normalizeFoundationDraftProfile(session.draftProfile) &&
session.draftCards.length > 0,
);
if (!hasDraftFoundation) {
throw badRequest(
`${payload.action} requires an existing draft foundation`,
);
}
}
if (payload.action === 'update_draft_card') {
if (!payload.cardId.trim()) {
throw badRequest('update_draft_card requires cardId');
}
if (!Array.isArray(payload.sections) || payload.sections.length === 0) {
throw badRequest('update_draft_card requires sections');
}
const operation = buildOperation('update_draft_card');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processUpdateDraftCardOperation({
userId,
sessionId,
operationId: operation.operationId,
payload,
});
return {
operation,
};
}
if (payload.action === 'sync_result_profile') {
const normalizedProfile = normalizeCustomWorldProfile(
payload.profile,
'',
);
if (!normalizedProfile) {
throw badRequest('sync_result_profile requires a valid profile');
}
const operation = buildOperation('sync_result_profile');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processSyncResultProfileOperation({
userId,
sessionId,
operationId: operation.operationId,
payload: {
...payload,
profile: normalizedProfile as unknown as Record<string, unknown>,
},
});
return {
operation,
};
}
if (payload.action === 'generate_characters') {
if (payload.count < 1 || payload.count > 3) {
throw badRequest('generate_characters count must be between 1 and 3');
}
const operation = buildOperation('generate_characters');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processGenerateCharactersOperation({
userId,
sessionId,
operationId: operation.operationId,
payload,
});
return {
operation,
};
}
if (payload.action === 'generate_landmarks') {
if (payload.count < 1 || payload.count > 3) {
throw badRequest('generate_landmarks count must be between 1 and 3');
}
const operation = buildOperation('generate_landmarks');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processGenerateLandmarksOperation({
userId,
sessionId,
operationId: operation.operationId,
payload,
});
return {
operation,
};
}
if (payload.action === 'generate_role_assets') {
if (!Array.isArray(payload.roleIds) || payload.roleIds.length !== 1) {
throw badRequest(
'generate_role_assets currently requires exactly one roleId',
);
}
const operation = buildOperation('generate_role_assets');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processGenerateRoleAssetsOperation({
userId,
sessionId,
operationId: operation.operationId,
payload,
});
return {
operation,
};
}
if (payload.action === 'sync_role_assets') {
if (!payload.roleId.trim()) {
throw badRequest('sync_role_assets requires roleId');
}
if (
!payload.portraitPath.trim() ||
!payload.generatedVisualAssetId.trim()
) {
throw badRequest(
'sync_role_assets requires portraitPath and generatedVisualAssetId',
);
}
const operation = buildOperation('sync_role_assets');
await this.sessionStore.createOperation(userId, sessionId, operation);
void this.processSyncRoleAssetsOperation({
userId,
sessionId,
operationId: operation.operationId,
payload,
});
return {
operation,
};
}
if (payload.action === 'publish_world') {
throw badRequest('publish_world is not available in phase5');
}
throw badRequest(`${payload.action} is not available in phase5`);
}
async getOperation(userId: string, sessionId: string, operationId: string) {
return this.sessionStore.getOperation(userId, sessionId, operationId);
}
async getCardDetail(userId: string, sessionId: string, cardId: string) {
const session = await this.sessionStore.get(userId, sessionId);
if (!session) {
return null;
}
return this.draftCompiler.getDraftCardDetail(session.draftProfile, cardId);
}
private async applyMessageTurn(params: {
userId: string;
sessionId: string;
latestUserText: string;
quickFillRequested: boolean;
relatedOperationId?: string | null;
onReplyUpdate?: (text: string) => void;
}) {
const latestSession = (await this.sessionStore.get(
params.userId,
params.sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const shouldPreserveDraftStage =
(latestSession.stage === 'object_refining' ||
latestSession.stage === 'visual_refining') &&
latestSession.draftCards.length > 0;
const assistantTurn = await this.eightAnchorSingleTurnService.streamTurn(
{
currentTurn: latestSession.currentTurn + 1,
progressPercent: latestSession.progressPercent,
quickFillRequested: params.quickFillRequested,
currentAnchorContent: latestSession.anchorContent,
chatHistory: latestSession.messages
.filter(
(message): message is CustomWorldAgentMessage =>
(message.role === 'user' || message.role === 'assistant') &&
Boolean(message.text.trim()),
)
.map((message) => ({
role: message.role,
content: message.text,
})),
},
{
onReplyUpdate: params.onReplyUpdate,
},
);
const nextCreatorIntent = buildCreatorIntentFromEightAnchorContent(
assistantTurn.nextAnchorContent,
);
const progressPercent = Math.max(
0,
Math.min(100, Math.round(assistantTurn.progressPercent)),
);
const creatorIntentReadiness =
progressPercent >= 100
? {
isReady: true,
completedKeys: [
'world_hook',
'player_premise',
'theme_and_tone',
'core_conflict',
'relationship_seed',
'iconic_element',
],
missingKeys: [],
}
: evaluateCreatorIntentReadiness(nextCreatorIntent);
const derivedState = buildDerivedState(nextCreatorIntent, true);
const preservedStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const shouldStayInDraftStage =
shouldPreserveDraftStage && progressPercent >= 100;
const nextStage = shouldStayInDraftStage
? preservedStage
: derivedState.stage;
const assistantMessage = {
id: `message-${crypto.randomBytes(8).toString('hex')}`,
role: 'assistant',
kind: 'chat',
text: assistantTurn.replyText,
createdAt: new Date().toISOString(),
relatedOperationId: params.relatedOperationId ?? null,
} satisfies CustomWorldAgentMessage;
await this.sessionStore.replaceDerivedState(params.userId, params.sessionId, {
currentTurn: latestSession.currentTurn + 1,
anchorContent: assistantTurn.nextAnchorContent,
progressPercent,
lastAssistantReply: assistantTurn.replyText,
stage: nextStage,
focusCardId: shouldStayInDraftStage ? latestSession.focusCardId : null,
creatorIntent: nextCreatorIntent,
creatorIntentReadiness,
anchorPack: buildAnchorPackFromEightAnchorContent(
assistantTurn.nextAnchorContent,
progressPercent,
),
draftProfile: shouldStayInDraftStage
? latestSession.draftProfile
: progressPercent >= 100
? {
title: buildDraftTitleFromIntent(nextCreatorIntent),
summary: buildDraftSummaryFromIntent(nextCreatorIntent),
}
: derivedState.draftProfile,
draftCards: shouldStayInDraftStage ? latestSession.draftCards : [],
assetCoverage: shouldStayInDraftStage
? latestSession.assetCoverage
: rebuildRoleAssetCoverage(
progressPercent >= 100
? {
title: buildDraftTitleFromIntent(nextCreatorIntent),
summary: buildDraftSummaryFromIntent(nextCreatorIntent),
}
: derivedState.draftProfile,
),
pendingClarifications:
progressPercent >= 100 ? [] : derivedState.pendingClarifications,
suggestedActions: shouldStayInDraftStage
? buildSuggestedActions({
stage: preservedStage,
isReady: true,
draftProfile: latestSession.draftProfile,
draftCards: latestSession.draftCards,
})
: progressPercent >= 100
? [
{
id: 'draft_foundation',
type: 'draft_foundation',
label: '生成游戏设定草稿',
},
]
: [],
recommendedReplies: [],
});
await this.sessionStore.appendMessage(
params.userId,
params.sessionId,
assistantMessage,
);
return (await this.sessionStore.getSnapshot(
params.userId,
params.sessionId,
)) as CustomWorldAgentSessionSnapshot;
}
private async processDraftFoundationOperation(params: {
userId: string;
sessionId: string;
operationId: string;
}) {
const { userId, sessionId, operationId } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '整理世界骨架',
phaseDetail: '正在校验已确认锚点,并准备第一版世界框架生成链路。',
progress: 12,
});
await sleep(30);
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
if (latestSession.progressPercent < 100) {
throw new Error('session progressPercent is below 100');
}
const creatorIntent = buildCreatorIntentFromEightAnchorContent(
latestSession.anchorContent,
);
const anchorPack = buildAnchorPackFromEightAnchorContent(
latestSession.anchorContent,
latestSession.progressPercent,
);
const draftProfile = await this.foundationDraftService.generate({
creatorIntent,
anchorPack,
anchorContent: latestSession.anchorContent,
onProgress: async (progress) => {
await this.sessionStore.updateOperation(
userId,
sessionId,
operationId,
{
status: 'running',
phaseLabel: progress.phaseLabel,
phaseDetail: progress.phaseDetail,
progress: progress.progress,
},
);
},
});
const draftWithAssets = this.autoAssetService
? await this.autoAssetService.populateDraftAssets({
draftProfile,
onProgress: async (progress) => {
await this.sessionStore.updateOperation(
userId,
sessionId,
operationId,
{
status: 'running',
phaseLabel: progress.phaseLabel,
phaseDetail: progress.phaseDetail,
progress: progress.progress,
},
);
},
})
: {
draftProfile,
assetCoverage: rebuildRoleAssetCoverage(draftProfile),
warnings: [],
};
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '编译草稿卡',
phaseDetail: '正在把世界底稿整理成可浏览的卡片摘要和详情结构。',
progress: 98,
});
const draftCards = this.draftCompiler.compileDraftCards(
draftWithAssets.draftProfile,
);
const assetCoverage = draftWithAssets.assetCoverage;
const nextStage = 'object_refining' as const;
const nextSuggestedActions = buildSuggestedActions({
stage: nextStage,
isReady: true,
draftProfile: draftWithAssets.draftProfile,
draftCards,
});
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
creatorIntent,
anchorPack,
draftProfile:
draftWithAssets.draftProfile as unknown as Record<string, unknown>,
draftCards,
assetCoverage,
pendingClarifications: [],
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendCheckpoint(userId, sessionId, {
label: '世界底稿 V1',
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildFoundationDraftAssistantMessage({
relatedOperationId: operationId,
draftProfile: draftWithAssets.draftProfile,
warnings: draftWithAssets.warnings,
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '世界底稿已生成',
phaseDetail:
draftWithAssets.warnings.length > 0
? `第一版世界底稿和 ${draftCards.length} 张草稿卡已经整理完成,另有 ${draftWithAssets.warnings.length} 项资产补齐待后续处理。`
: `第一版世界底稿和 ${draftCards.length} 张草稿卡已经整理完成。`,
progress: 100,
error: null,
});
} catch (error) {
const currentOperation = await this.sessionStore.getOperation(
userId,
sessionId,
operationId,
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel:
currentOperation?.phaseLabel?.trim() || '底稿生成失败',
phaseDetail:
currentOperation?.phaseDetail?.trim() ||
'这一轮没有成功把设定编成世界底稿。',
progress: 100,
error:
error instanceof Error ? error.message : 'draft foundation failed',
});
}
}
private async processUpdateDraftCardOperation(params: {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'update_draft_card' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '写回草稿设定',
phaseDetail: '正在把这次编辑内容写回当前世界底稿。',
progress: 34,
});
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const nextDraftProfile = updateDraftCardSections({
draftProfile: (latestSession.draftProfile ?? {}) as Record<
string,
unknown
>,
cardId: payload.cardId,
sections: payload.sections,
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '重编译草稿卡',
phaseDetail: '正在同步更新草稿摘要和详情内容。',
progress: 72,
});
const nextDraftCards =
this.draftCompiler.compileDraftCards(nextDraftProfile);
const assetCoverage = rebuildRoleAssetCoverage(nextDraftProfile);
const nextStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const nextSuggestedActions = buildSuggestedActions({
stage: nextStage,
isReady: true,
draftProfile: nextDraftProfile,
draftCards: nextDraftCards,
});
const updatedDetail = this.draftCompiler.getDraftCardDetail(
nextDraftProfile,
payload.cardId,
);
const changedSectionIds = new Set(
payload.sections
.map((section) => section.sectionId.trim())
.filter(Boolean),
);
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
draftProfile: nextDraftProfile,
draftCards: nextDraftCards,
assetCoverage,
focusCardId: payload.cardId,
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendCheckpoint(userId, sessionId, {
label: `编辑 ${updatedDetail?.title || '草稿卡'}`,
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildActionResultMessage({
relatedOperationId: operationId,
text: this.changeSummaryService.buildSummary({
action: 'update_draft_card',
cardId: payload.cardId,
changedLabels:
updatedDetail?.sections
.filter((section) => changedSectionIds.has(section.id))
.map((section) => section.label) ?? [],
draftProfile: nextDraftProfile,
}),
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '草稿设定已保存',
phaseDetail: `${updatedDetail?.title || '当前卡片'}」的设定已经同步更新。`,
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '保存失败',
phaseDetail: '这次草稿编辑没有成功写回到底稿。',
progress: 100,
error:
error instanceof Error ? error.message : 'update draft card failed',
});
}
}
private async processSyncResultProfileOperation(params: {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'sync_result_profile' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '同步结果页快照',
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
progress: 36,
});
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const resultProfile = payload.profile as unknown as CustomWorldProfile;
const nextDraftProfile = syncResultProfileIntoDraftProfile({
currentDraftProfile: latestSession.draftProfile,
resultProfile,
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '重编译草稿摘要',
phaseDetail: '正在刷新草稿卡摘要、资产覆盖和结果页恢复快照。',
progress: 72,
});
const nextDraftCards = this.draftCompiler.compileDraftCards(nextDraftProfile);
const assetCoverage = rebuildRoleAssetCoverage(nextDraftProfile);
const nextStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const nextSuggestedActions = buildSuggestedActions({
stage: nextStage,
isReady: true,
draftProfile: nextDraftProfile,
draftCards: nextDraftCards,
});
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
draftProfile: nextDraftProfile,
draftCards: nextDraftCards,
assetCoverage,
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendCheckpoint(userId, sessionId, {
label: '同步结果页编辑',
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildActionResultMessage({
relatedOperationId: operationId,
text: '结果页里的最新世界结构已经同步回当前草稿。',
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '结果页快照已同步',
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '结果页同步失败',
phaseDetail: '这一轮结果页编辑没有成功写回当前草稿。',
progress: 100,
error:
error instanceof Error ? error.message : 'sync result profile failed',
});
}
}
private async processGenerateCharactersOperation(params: {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'generate_characters' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '生成新角色',
phaseDetail: '正在围绕当前世界底稿补出新角色。',
progress: 32,
});
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const generationResult =
await this.entityGenerationService.generateAdditionalCharacters({
creatorIntent: latestSession.creatorIntent,
anchorPack: latestSession.anchorPack,
draftProfile: (latestSession.draftProfile ?? {}) as Record<
string,
unknown
>,
count: payload.count,
promptText: payload.promptText,
anchorCardIds:
payload.anchorCardIds && payload.anchorCardIds.length > 0
? payload.anchorCardIds
: latestSession.focusCardId
? [latestSession.focusCardId]
: [getWorldFoundationCardId()],
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '插入新角色卡',
phaseDetail: '正在把新角色插回草稿并刷新卡片列表。',
progress: 74,
});
const nextDraftCards = this.draftCompiler.compileDraftCards(
generationResult.draftProfile,
);
const assetCoverage = rebuildRoleAssetCoverage(
generationResult.draftProfile,
);
const nextStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const nextSuggestedActions = buildSuggestedActions({
stage: nextStage,
isReady: true,
draftProfile: generationResult.draftProfile,
draftCards: nextDraftCards,
});
const focusCardId = generationResult.generatedCharacters[0]?.id ?? null;
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
draftProfile: generationResult.draftProfile,
draftCards: nextDraftCards,
assetCoverage,
focusCardId,
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendCheckpoint(userId, sessionId, {
label: `新增角色 ${generationResult.generatedCharacters.length}`,
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildActionResultMessage({
relatedOperationId: operationId,
text: this.changeSummaryService.buildSummary({
action: 'generate_characters',
names: generationResult.generatedCharacters.map(
(entry) => entry.name,
),
draftProfile: generationResult.draftProfile,
}),
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '新角色已加入草稿',
phaseDetail: `已补出 ${generationResult.generatedCharacters.length} 个新角色。`,
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '角色生成失败',
phaseDetail: '这一轮没有成功补出新角色。',
progress: 100,
error:
error instanceof Error ? error.message : 'generate characters failed',
});
}
}
private async processGenerateLandmarksOperation(params: {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'generate_landmarks' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '生成新地点',
phaseDetail: '正在围绕当前世界底稿补出新地点。',
progress: 32,
});
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const generationResult =
await this.entityGenerationService.generateAdditionalLandmarks({
creatorIntent: latestSession.creatorIntent,
anchorPack: latestSession.anchorPack,
draftProfile: (latestSession.draftProfile ?? {}) as Record<
string,
unknown
>,
count: payload.count,
promptText: payload.promptText,
anchorCardIds:
payload.anchorCardIds && payload.anchorCardIds.length > 0
? payload.anchorCardIds
: latestSession.focusCardId
? [latestSession.focusCardId]
: [getWorldFoundationCardId()],
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '插入新地点卡',
phaseDetail: '正在把新地点插回草稿并刷新卡片列表。',
progress: 74,
});
const nextDraftCards = this.draftCompiler.compileDraftCards(
generationResult.draftProfile,
);
const assetCoverage = rebuildRoleAssetCoverage(
generationResult.draftProfile,
);
const nextStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const nextSuggestedActions = buildSuggestedActions({
stage: nextStage,
isReady: true,
draftProfile: generationResult.draftProfile,
draftCards: nextDraftCards,
});
const focusCardId = generationResult.generatedLandmarks[0]?.id ?? null;
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
draftProfile: generationResult.draftProfile,
draftCards: nextDraftCards,
assetCoverage,
focusCardId,
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendCheckpoint(userId, sessionId, {
label: `新增地点 ${generationResult.generatedLandmarks.length}`,
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildActionResultMessage({
relatedOperationId: operationId,
text: this.changeSummaryService.buildSummary({
action: 'generate_landmarks',
names: generationResult.generatedLandmarks.map(
(entry) => entry.name,
),
draftProfile: generationResult.draftProfile,
}),
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '新地点已加入草稿',
phaseDetail: `已补出 ${generationResult.generatedLandmarks.length} 个新地点。`,
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '地点生成失败',
phaseDetail: '这一轮没有成功补出新地点。',
progress: 100,
error:
error instanceof Error ? error.message : 'generate landmarks failed',
});
}
}
private async processGenerateRoleAssetsOperation(params: {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'generate_role_assets' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '准备角色资产工坊',
phaseDetail: '正在校验角色并整理工坊上下文。',
progress: 40,
});
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const roleId = payload.roleIds[0]!;
const studioContext = this.assetBridgeService.buildRoleAssetStudioContext(
latestSession.draftProfile,
roleId,
);
const nextStage = 'visual_refining' as const;
const nextSuggestedActions = buildSuggestedActions({
stage: nextStage,
isReady: true,
draftProfile: latestSession.draftProfile,
draftCards: latestSession.draftCards,
});
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
focusCardId: roleId,
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildActionResultMessage({
relatedOperationId: operationId,
text: `已为「${studioContext.roleName}」准备好角色资产工坊,先生成主图候选,再补核心动作。`,
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '角色资产工坊已就绪',
phaseDetail: `${studioContext.roleName}」现在可以开始生成主图和动作。`,
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '角色资产工坊准备失败',
phaseDetail: '这一轮没有成功进入角色资产工坊。',
progress: 100,
error:
error instanceof Error
? error.message
: 'generate role assets failed',
});
}
}
private async processSyncRoleAssetsOperation(params: {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'sync_role_assets' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '同步角色资产',
phaseDetail: '正在把主图与动作结果写回当前世界草稿。',
progress: 36,
});
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const syncResult = this.assetBridgeService.applyRoleAssetPublishResult(
latestSession.draftProfile,
payload,
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '刷新角色卡摘要',
phaseDetail: '正在同步更新角色卡状态与资产覆盖。',
progress: 72,
});
const nextDraftCards = this.draftCompiler.compileDraftCards(
syncResult.draftProfile,
);
const assetCoverage = rebuildRoleAssetCoverage(syncResult.draftProfile);
const nextSuggestedActions = buildSuggestedActions({
stage: 'visual_refining',
isReady: true,
draftProfile: syncResult.draftProfile,
draftCards: nextDraftCards,
});
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: 'visual_refining',
draftProfile: syncResult.draftProfile,
draftCards: nextDraftCards,
assetCoverage,
focusCardId: payload.roleId,
suggestedActions: nextSuggestedActions,
recommendedReplies: [],
});
await this.sessionStore.appendCheckpoint(userId, sessionId, {
label: `同步角色资产 ${syncResult.updatedAssetSummary.roleName}`,
});
await this.sessionStore.appendMessage(
userId,
sessionId,
buildActionResultMessage({
relatedOperationId: operationId,
text: buildRoleAssetSyncResultText({
roleName: syncResult.updatedAssetSummary.roleName,
assetStatusLabel: resolveRoleAssetStatusLabel(
syncResult.updatedAssetSummary.status,
),
}),
}),
);
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '角色资产已同步',
phaseDetail: `${syncResult.updatedAssetSummary.roleName}」的资产状态已更新为${resolveRoleAssetStatusLabel(syncResult.updatedAssetSummary.status)}`,
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '角色资产同步失败',
phaseDetail: '这一轮没有成功把角色资产写回草稿。',
progress: 100,
error:
error instanceof Error ? error.message : 'sync role assets failed',
});
}
}
private async processMessageOperation(params: {
userId: string;
sessionId: string;
operationId: string;
latestUserText: string;
quickFillRequested: boolean;
}) {
const {
userId,
sessionId,
operationId,
latestUserText,
quickFillRequested,
} = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: quickFillRequested ? '补全剩余设定' : '整理当前设定',
phaseDetail: quickFillRequested
? '正在基于当前方向补齐剩余设定。'
: '正在把这轮输入沉淀成新的完整设定。',
progress: 45,
});
await sleep(30);
if (latestUserText.includes(PHASE2_FORCE_FAIL_TOKEN)) {
throw new Error('phase2 forced failure');
}
const latestSession = (await this.sessionStore.get(
userId,
sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const shouldPreserveDraftStage =
(latestSession.stage === 'object_refining' ||
latestSession.stage === 'visual_refining') &&
latestSession.draftCards.length > 0;
await this.applyMessageTurn({
userId,
sessionId,
latestUserText,
quickFillRequested,
relatedOperationId: operationId,
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '设定已更新',
phaseDetail: shouldPreserveDraftStage
? '这轮补充已挂回当前底稿语境,现有草稿卡保持可继续浏览。'
: quickFillRequested
? '剩余设定已补全,现在可以进入游戏设定草稿生成。'
: '这一轮的设定更新已经完成。',
progress: 100,
error: null,
});
} catch (error) {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '处理失败',
phaseDetail: '这一轮消息没有成功沉淀为当前设定。',
progress: 100,
error:
error instanceof Error ? error.message : 'process message failed',
});
}
}
}