273 lines
8.0 KiB
TypeScript
273 lines
8.0 KiB
TypeScript
import { Router } from 'express';
|
|
import { z } from 'zod';
|
|
|
|
import type {
|
|
CreateCustomWorldAgentSessionRequest,
|
|
CustomWorldAgentActionRequest,
|
|
SendCustomWorldAgentMessageRequest,
|
|
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
|
import type { AppContext } from '../context.js';
|
|
import { badRequest, notFound } from '../errors.js';
|
|
import { asyncHandler, prepareApiResponse, sendApiResponse } from '../http.js';
|
|
import { requireJwtAuth } from '../middleware/auth.js';
|
|
import { routeMeta } from '../middleware/routeMeta.js';
|
|
|
|
const createSessionSchema = z.object({
|
|
seedText: z.string().trim().optional().default(''),
|
|
});
|
|
|
|
const sendMessageSchema = z.object({
|
|
clientMessageId: z.string().trim().min(1),
|
|
text: z.string().trim().min(1),
|
|
quickFillRequested: z.boolean().optional().default(false),
|
|
focusCardId: z.string().trim().nullable().optional().default(null),
|
|
selectedCardIds: z.array(z.string().trim().min(1)).optional().default([]),
|
|
});
|
|
|
|
const actionSchema = z.discriminatedUnion('action', [
|
|
z.object({
|
|
action: z.literal('draft_foundation'),
|
|
}),
|
|
z.object({
|
|
action: z.literal('update_draft_card'),
|
|
cardId: z.string().trim().min(1),
|
|
sections: z
|
|
.array(
|
|
z.object({
|
|
sectionId: z.string().trim().min(1),
|
|
value: z.string(),
|
|
}),
|
|
)
|
|
.min(1),
|
|
}),
|
|
z.object({
|
|
action: z.literal('sync_result_profile'),
|
|
profile: z.record(z.string(), z.unknown()),
|
|
}),
|
|
z.object({
|
|
action: z.literal('generate_characters'),
|
|
count: z.number().int().min(1).max(3),
|
|
promptText: z.string().trim().nullable().optional().default(null),
|
|
anchorCardIds: z.array(z.string().trim().min(1)).optional().default([]),
|
|
}),
|
|
z.object({
|
|
action: z.literal('generate_landmarks'),
|
|
count: z.number().int().min(1).max(3),
|
|
promptText: z.string().trim().nullable().optional().default(null),
|
|
anchorCardIds: z.array(z.string().trim().min(1)).optional().default([]),
|
|
}),
|
|
z.object({
|
|
action: z.literal('generate_role_assets'),
|
|
roleIds: z.array(z.string().trim().min(1)).min(1),
|
|
}),
|
|
z.object({
|
|
action: z.literal('sync_role_assets'),
|
|
roleId: z.string().trim().min(1),
|
|
portraitPath: z.string().trim().min(1),
|
|
generatedVisualAssetId: z.string().trim().min(1),
|
|
generatedAnimationSetId: z.string().trim().nullable().optional(),
|
|
animationMap: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
}),
|
|
z.object({
|
|
action: z.literal('generate_scene_assets'),
|
|
sceneIds: z.array(z.string().trim().min(1)).min(1),
|
|
}),
|
|
z.object({
|
|
action: z.literal('sync_scene_assets'),
|
|
sceneId: z.string().trim().min(1),
|
|
sceneKind: z.enum(['camp', 'landmark']),
|
|
imageSrc: z.string().trim().min(1),
|
|
generatedSceneAssetId: z.string().trim().min(1),
|
|
generatedScenePrompt: z.string().trim().nullable().optional(),
|
|
generatedSceneModel: z.string().trim().nullable().optional(),
|
|
}),
|
|
z.object({
|
|
action: z.literal('expand_long_tail'),
|
|
}),
|
|
z.object({
|
|
action: z.literal('publish_world'),
|
|
}),
|
|
z.object({
|
|
action: z.literal('revert_checkpoint'),
|
|
checkpointId: z.string().trim().min(1),
|
|
}),
|
|
]);
|
|
|
|
function readParam(param: string | string[] | undefined) {
|
|
return Array.isArray(param) ? param[0]?.trim() || '' : param?.trim() || '';
|
|
}
|
|
|
|
export function createCustomWorldAgentRoutes(context: AppContext) {
|
|
const router = Router();
|
|
const requireAuth = requireJwtAuth(context.config, context.userRepository);
|
|
|
|
router.use(requireAuth);
|
|
|
|
router.post(
|
|
'/sessions',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.createSession' }),
|
|
asyncHandler(async (request, response) => {
|
|
const payload = createSessionSchema.parse(
|
|
request.body,
|
|
) as CreateCustomWorldAgentSessionRequest;
|
|
sendApiResponse(response, {
|
|
session: await context.customWorldAgentOrchestrator.createSession(
|
|
request.userId!,
|
|
payload,
|
|
),
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
'/sessions/:sessionId',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.getSession' }),
|
|
asyncHandler(async (request, response) => {
|
|
const sessionId = readParam(request.params.sessionId);
|
|
if (!sessionId) {
|
|
throw badRequest('sessionId is required');
|
|
}
|
|
|
|
const session = await context.customWorldAgentOrchestrator.getSessionSnapshot(
|
|
request.userId!,
|
|
sessionId,
|
|
);
|
|
if (!session) {
|
|
throw notFound('custom world agent session not found');
|
|
}
|
|
|
|
sendApiResponse(response, session);
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
'/sessions/:sessionId/messages',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.sendMessage' }),
|
|
asyncHandler(async (request, response) => {
|
|
const sessionId = readParam(request.params.sessionId);
|
|
if (!sessionId) {
|
|
throw badRequest('sessionId is required');
|
|
}
|
|
|
|
const payload = sendMessageSchema.parse(
|
|
request.body,
|
|
) as SendCustomWorldAgentMessageRequest;
|
|
sendApiResponse(
|
|
response,
|
|
await context.customWorldAgentOrchestrator.submitMessage(
|
|
request.userId!,
|
|
sessionId,
|
|
payload,
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
'/sessions/:sessionId/messages/stream',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.streamMessage' }),
|
|
asyncHandler(async (request, response) => {
|
|
const sessionId = readParam(request.params.sessionId);
|
|
if (!sessionId) {
|
|
throw badRequest('sessionId is required');
|
|
}
|
|
|
|
const payload = sendMessageSchema.parse(
|
|
request.body,
|
|
) as SendCustomWorldAgentMessageRequest;
|
|
await context.customWorldAgentOrchestrator.streamMessage({
|
|
request,
|
|
response,
|
|
userId: request.userId!,
|
|
sessionId,
|
|
payload,
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
'/sessions/:sessionId/actions',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.executeAction' }),
|
|
asyncHandler(async (request, response) => {
|
|
const sessionId = readParam(request.params.sessionId);
|
|
if (!sessionId) {
|
|
throw badRequest('sessionId is required');
|
|
}
|
|
|
|
const payload = actionSchema.parse(
|
|
request.body,
|
|
) as CustomWorldAgentActionRequest;
|
|
sendApiResponse(
|
|
response,
|
|
await context.customWorldAgentOrchestrator.executeAction(
|
|
request.userId!,
|
|
sessionId,
|
|
payload,
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
'/sessions/:sessionId/operations/:operationId',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.getOperation' }),
|
|
asyncHandler(async (request, response) => {
|
|
const sessionId = readParam(request.params.sessionId);
|
|
const operationId = readParam(request.params.operationId);
|
|
if (!sessionId) {
|
|
throw badRequest('sessionId is required');
|
|
}
|
|
if (!operationId) {
|
|
throw badRequest('operationId is required');
|
|
}
|
|
|
|
const operation = await context.customWorldAgentOrchestrator.getOperation(
|
|
request.userId!,
|
|
sessionId,
|
|
operationId,
|
|
);
|
|
if (!operation) {
|
|
throw notFound('custom world agent operation not found');
|
|
}
|
|
|
|
prepareApiResponse(request, response, {
|
|
statusCode: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
},
|
|
});
|
|
response.end(JSON.stringify({ operation }));
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
'/sessions/:sessionId/cards/:cardId',
|
|
routeMeta({ operation: 'runtime.customWorldAgent.getCardDetail' }),
|
|
asyncHandler(async (request, response) => {
|
|
const sessionId = readParam(request.params.sessionId);
|
|
const cardId = readParam(request.params.cardId);
|
|
if (!sessionId) {
|
|
throw badRequest('sessionId is required');
|
|
}
|
|
if (!cardId) {
|
|
throw badRequest('cardId is required');
|
|
}
|
|
|
|
const card = await context.customWorldAgentOrchestrator.getCardDetail(
|
|
request.userId!,
|
|
sessionId,
|
|
cardId,
|
|
);
|
|
if (!card) {
|
|
throw notFound('custom world agent card not found');
|
|
}
|
|
|
|
sendApiResponse(response, {
|
|
card,
|
|
});
|
|
}),
|
|
);
|
|
|
|
return router;
|
|
}
|