Files
Genarrative/server-node/src/routes/customWorldAgent.ts
2026-04-21 19:18:26 +08:00

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;
}