This commit is contained in:
2026-04-14 20:43:46 +08:00
39 changed files with 2971 additions and 940 deletions

View File

@@ -5,6 +5,10 @@ import type { ListCustomWorldWorksResponse } from '../../../packages/shared/src/
import type {
AnswerCustomWorldSessionQuestionRequest,
CreateCustomWorldSessionRequest,
CustomWorldGalleryDetailResponse,
CustomWorldGalleryResponse,
CustomWorldLibraryMutationResponse,
CustomWorldLibraryResponse,
RuntimeSettings,
SavedGameSnapshotInput,
} from '../../../packages/shared/src/contracts/runtime.js';
@@ -109,6 +113,15 @@ function readParam(param: string | string[] | undefined) {
return Array.isArray(param) ? param[0]?.trim() || '' : param?.trim() || '';
}
async function resolveAuthDisplayName(context: AppContext, userId: string) {
const user = await context.userRepository.findById(userId);
if (!user) {
throw notFound('user not found');
}
return user.displayName?.trim() || '玩家';
}
export function createRuntimeRoutes(context: AppContext) {
const router = Router();
const requireAuth = requireJwtAuth(context.config, context.userRepository);
@@ -224,11 +237,55 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world-library',
routeMeta({ operation: 'runtime.customWorldLibrary.list' }),
asyncHandler(async (request, response) => {
sendApiResponse(response, {
profiles: await context.runtimeRepository.listCustomWorldProfiles(
request.userId!,
),
});
sendApiResponse(
response,
{
entries: await context.runtimeRepository.listCustomWorldProfiles(
request.userId!,
),
} satisfies CustomWorldLibraryResponse,
);
}),
);
router.get(
'/runtime/custom-world-gallery',
routeMeta({ operation: 'runtime.customWorldGallery.list' }),
asyncHandler(async (_request, response) => {
sendApiResponse(
response,
{
entries: await context.runtimeRepository.listPublishedCustomWorldGallery(),
} satisfies CustomWorldGalleryResponse,
);
}),
);
router.get(
'/runtime/custom-world-gallery/:ownerUserId/:profileId',
routeMeta({ operation: 'runtime.customWorldGallery.detail' }),
asyncHandler(async (request, response) => {
const ownerUserId = readParam(request.params.ownerUserId);
const profileId = readParam(request.params.profileId);
if (!ownerUserId || !profileId) {
throw badRequest('ownerUserId and profileId are required');
}
const entry =
await context.runtimeRepository.getPublishedCustomWorldGalleryDetail(
ownerUserId,
profileId,
);
if (!entry) {
throw notFound('public custom world not found');
}
sendApiResponse(
response,
{
entry,
} satisfies CustomWorldGalleryDetailResponse,
);
}),
);
@@ -241,13 +298,19 @@ export function createRuntimeRoutes(context: AppContext) {
throw badRequest('profileId is required');
}
const payload = customWorldProfileSchema.parse(request.body);
sendApiResponse(response, {
profiles: await context.runtimeRepository.upsertCustomWorldProfile(
const authorDisplayName = await resolveAuthDisplayName(
context,
request.userId!,
);
sendApiResponse(
response,
await context.runtimeRepository.upsertCustomWorldProfile(
request.userId!,
profileId,
jsonClone(payload.profile),
authorDisplayName,
),
});
);
}),
);
@@ -259,12 +322,75 @@ export function createRuntimeRoutes(context: AppContext) {
if (!profileId) {
throw badRequest('profileId is required');
}
sendApiResponse(response, {
profiles: await context.runtimeRepository.deleteCustomWorldProfile(
sendApiResponse(
response,
{
entries: await context.runtimeRepository.deleteCustomWorldProfile(
request.userId!,
profileId,
),
} satisfies CustomWorldLibraryResponse,
);
}),
);
router.post(
'/runtime/custom-world-library/:profileId/publish',
routeMeta({ operation: 'runtime.customWorldLibrary.publish' }),
asyncHandler(async (request, response) => {
const profileId = readParam(request.params.profileId);
if (!profileId) {
throw badRequest('profileId is required');
}
const authorDisplayName = await resolveAuthDisplayName(
context,
request.userId!,
);
const mutation =
await context.runtimeRepository.publishCustomWorldProfile(
request.userId!,
profileId,
),
});
authorDisplayName,
);
if (!mutation) {
throw notFound('custom world not found');
}
sendApiResponse(
response,
mutation satisfies CustomWorldLibraryMutationResponse,
);
}),
);
router.post(
'/runtime/custom-world-library/:profileId/unpublish',
routeMeta({ operation: 'runtime.customWorldLibrary.unpublish' }),
asyncHandler(async (request, response) => {
const profileId = readParam(request.params.profileId);
if (!profileId) {
throw badRequest('profileId is required');
}
const authorDisplayName = await resolveAuthDisplayName(
context,
request.userId!,
);
const mutation =
await context.runtimeRepository.unpublishCustomWorldProfile(
request.userId!,
profileId,
authorDisplayName,
);
if (!mutation) {
throw notFound('custom world not found');
}
sendApiResponse(
response,
mutation satisfies CustomWorldLibraryMutationResponse,
);
}),
);