Files
Genarrative/server-node/src/services/customWorldAgentActionRegistry.ts
2026-04-21 18:27:46 +08:00

404 lines
13 KiB
TypeScript

import type {
CustomWorldAgentActionRequest,
CustomWorldAgentOperationRecord,
CustomWorldSupportedAction,
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
import { badRequest } from '../errors.js';
import { normalizeCustomWorldProfile } from '../modules/custom-world/runtimeProfile.js';
import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js';
import type {
CustomWorldAgentActionExecutorMap,
CustomWorldAgentActionPayload,
} from './customWorldAgentActionExecutors/index.js';
import type { CustomWorldAgentSessionRecord } from './customWorldAgentSessionStore.js';
type EnabledAction = keyof CustomWorldAgentActionExecutorMap;
type EnabledDescriptor<K extends EnabledAction> = {
operationType: CustomWorldAgentOperationRecord['type'];
normalizePayload?: (
payload: CustomWorldAgentActionPayload<K>,
) => CustomWorldAgentActionPayload<K>;
validate?: (
session: CustomWorldAgentSessionRecord,
payload: CustomWorldAgentActionPayload<K>,
) => void;
execute: CustomWorldAgentActionExecutorMap[K];
};
type DisabledAction = Exclude<CustomWorldAgentActionRequest['action'], EnabledAction>;
type DisabledDescriptor = {
disabledReason: string;
};
type ActionCapabilityState = {
enabled: boolean;
reason?: string;
};
function assertDraftRefiningActionAvailable(
session: CustomWorldAgentSessionRecord,
action: string,
) {
if (
session.stage !== 'object_refining' &&
session.stage !== 'visual_refining'
) {
throw badRequest(
`${action} is only available during object_refining or visual_refining`,
);
}
const hasDraftFoundation = Boolean(
normalizeFoundationDraftProfile(session.draftProfile) &&
session.draftCards.length > 0,
);
if (!hasDraftFoundation) {
throw badRequest(`${action} requires an existing draft foundation`);
}
}
function assertLongTailActionAvailable(
session: CustomWorldAgentSessionRecord,
action: string,
) {
if (
session.stage !== 'object_refining' &&
session.stage !== 'visual_refining' &&
session.stage !== 'long_tail_review' &&
session.stage !== 'ready_to_publish'
) {
throw badRequest(
`${action} is only available during object_refining, visual_refining, long_tail_review or ready_to_publish`,
);
}
}
function assertPublishActionAvailable(
session: CustomWorldAgentSessionRecord,
action: string,
) {
assertLongTailActionAvailable(session, action);
if (!normalizeFoundationDraftProfile(session.draftProfile)) {
throw badRequest(`${action} requires an existing draft foundation`);
}
}
export type PreparedCustomWorldAgentActionExecution = {
operationType: CustomWorldAgentOperationRecord['type'];
execute: (params: {
userId: string;
sessionId: string;
operationId: string;
}) => Promise<void>;
};
export class CustomWorldAgentActionRegistry {
private readonly descriptors: Record<
CustomWorldAgentActionRequest['action'],
EnabledDescriptor<EnabledAction> | DisabledDescriptor
>;
constructor(executors: CustomWorldAgentActionExecutorMap) {
this.descriptors = {
draft_foundation: {
operationType: 'draft_foundation',
validate: (session) => {
if (session.progressPercent < 100) {
throw badRequest('draft_foundation requires progressPercent >= 100');
}
},
execute: executors.draft_foundation,
},
update_draft_card: {
operationType: 'update_draft_card',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
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');
}
},
execute: executors.update_draft_card,
},
sync_result_profile: {
operationType: 'sync_result_profile',
normalizePayload: (payload) => {
const normalizedProfile = normalizeCustomWorldProfile(payload.profile, '');
if (!normalizedProfile) {
throw badRequest('sync_result_profile requires a valid profile');
}
return {
...payload,
profile: normalizedProfile as unknown as Record<string, unknown>,
};
},
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
},
execute: executors.sync_result_profile,
},
generate_characters: {
operationType: 'generate_characters',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
if (payload.count < 1 || payload.count > 3) {
throw badRequest(
'generate_characters count must be between 1 and 3',
);
}
},
execute: executors.generate_characters,
},
generate_landmarks: {
operationType: 'generate_landmarks',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
if (payload.count < 1 || payload.count > 3) {
throw badRequest(
'generate_landmarks count must be between 1 and 3',
);
}
},
execute: executors.generate_landmarks,
},
generate_role_assets: {
operationType: 'generate_role_assets',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
if (!Array.isArray(payload.roleIds) || payload.roleIds.length !== 1) {
throw badRequest(
'generate_role_assets currently requires exactly one roleId',
);
}
},
execute: executors.generate_role_assets,
},
sync_role_assets: {
operationType: 'sync_role_assets',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
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',
);
}
},
execute: executors.sync_role_assets,
},
generate_scene_assets: {
operationType: 'generate_scene_assets',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
if (!Array.isArray(payload.sceneIds) || payload.sceneIds.length !== 1) {
throw badRequest(
'generate_scene_assets currently requires exactly one sceneId',
);
}
},
execute: executors.generate_scene_assets,
},
sync_scene_assets: {
operationType: 'sync_scene_assets',
validate: (session, payload) => {
assertDraftRefiningActionAvailable(session, payload.action);
if (!payload.sceneId.trim()) {
throw badRequest('sync_scene_assets requires sceneId');
}
if (!payload.imageSrc.trim() || !payload.generatedSceneAssetId.trim()) {
throw badRequest(
'sync_scene_assets requires imageSrc and generatedSceneAssetId',
);
}
},
execute: executors.sync_scene_assets,
},
expand_long_tail: {
operationType: 'expand_long_tail',
validate: (session, payload) => {
assertLongTailActionAvailable(session, payload.action);
if (!normalizeFoundationDraftProfile(session.draftProfile)) {
throw badRequest('expand_long_tail requires an existing draft foundation');
}
},
execute: executors.expand_long_tail,
},
publish_world: {
operationType: 'publish_world',
validate: (session, payload) => {
assertPublishActionAvailable(session, payload.action);
},
execute: executors.publish_world,
},
revert_checkpoint: {
operationType: 'revert_checkpoint',
validate: (session, payload) => {
assertLongTailActionAvailable(session, payload.action);
if (!payload.checkpointId.trim()) {
throw badRequest('revert_checkpoint requires checkpointId');
}
const checkpoint = session.checkpoints.find(
(entry) => entry.checkpointId === payload.checkpointId,
);
if (!checkpoint) {
throw badRequest('revert_checkpoint target checkpoint does not exist');
}
if (!checkpoint.snapshot) {
throw badRequest(
'revert_checkpoint target checkpoint does not contain a restorable snapshot',
);
}
},
execute: executors.revert_checkpoint,
},
};
}
// orchestrator 只关心“拿到一个已校验的动作执行计划”,不再自己维护所有 action 分支。
prepareExecution(
session: CustomWorldAgentSessionRecord,
payload: CustomWorldAgentActionRequest,
): PreparedCustomWorldAgentActionExecution {
const descriptor = this.descriptors[payload.action];
if ('disabledReason' in descriptor) {
throw badRequest(descriptor.disabledReason);
}
const normalizedPayload = descriptor.normalizePayload
? descriptor.normalizePayload(payload as never)
: payload;
descriptor.validate?.(session, normalizedPayload as never);
return {
operationType: descriptor.operationType,
execute: ({ userId, sessionId, operationId }) =>
descriptor.execute({
userId,
sessionId,
operationId,
payload: normalizedPayload as never,
}),
};
}
buildSupportedActions(
session: CustomWorldAgentSessionRecord,
): CustomWorldSupportedAction[] {
return (
Object.entries(this.descriptors) as Array<
[
CustomWorldAgentActionRequest['action'],
EnabledDescriptor<EnabledAction> | DisabledDescriptor,
]
>
).map(([action, descriptor]) => {
const capability = this.resolveCapabilityState(session, action, descriptor);
return {
action,
enabled: capability.enabled,
reason: capability.reason ?? null,
} satisfies CustomWorldSupportedAction;
});
}
private resolveCapabilityState(
session: CustomWorldAgentSessionRecord,
action: CustomWorldAgentActionRequest['action'],
descriptor: EnabledDescriptor<EnabledAction> | DisabledDescriptor,
): ActionCapabilityState {
if ('disabledReason' in descriptor) {
return {
enabled: false,
reason: descriptor.disabledReason,
};
}
if (action === 'draft_foundation') {
return session.progressPercent >= 100
? { enabled: true }
: {
enabled: false,
reason: 'draft_foundation requires progressPercent >= 100',
};
}
if (
action === 'update_draft_card' ||
action === 'sync_result_profile' ||
action === 'generate_characters' ||
action === 'generate_landmarks' ||
action === 'generate_role_assets' ||
action === 'sync_role_assets' ||
action === 'generate_scene_assets' ||
action === 'sync_scene_assets'
) {
try {
assertDraftRefiningActionAvailable(session, action);
return { enabled: true };
} catch (error) {
return {
enabled: false,
reason: error instanceof Error ? error.message : 'action unavailable',
};
}
}
if (action === 'expand_long_tail') {
try {
assertLongTailActionAvailable(session, action);
return { enabled: true };
} catch (error) {
return {
enabled: false,
reason: error instanceof Error ? error.message : 'action unavailable',
};
}
}
if (action === 'publish_world') {
try {
assertPublishActionAvailable(session, action);
return { enabled: true };
} catch (error) {
return {
enabled: false,
reason: error instanceof Error ? error.message : 'action unavailable',
};
}
}
if (action === 'revert_checkpoint') {
const restorableCheckpoint = session.checkpoints.find(
(entry) => Boolean(entry.snapshot),
);
if (!restorableCheckpoint) {
return {
enabled: false,
reason: 'revert_checkpoint requires at least one restorable checkpoint snapshot',
};
}
try {
assertLongTailActionAvailable(session, action);
return { enabled: true };
} catch (error) {
return {
enabled: false,
reason: error instanceof Error ? error.message : 'action unavailable',
};
}
}
return { enabled: true };
}
}