1
This commit is contained in:
403
server-node/src/services/customWorldAgentActionRegistry.ts
Normal file
403
server-node/src/services/customWorldAgentActionRegistry.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user