1734 lines
56 KiB
TypeScript
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',
|
|
});
|
|
}
|
|
}
|
|
}
|