404 lines
13 KiB
TypeScript
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 };
|
|
}
|
|
}
|