11
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-16 21:47:20 +08:00
parent 2456c10c63
commit 09d4c0c31b
79 changed files with 11873 additions and 2341 deletions

View File

@@ -9,6 +9,12 @@ import type {
CustomWorldGalleryResponse,
CustomWorldLibraryMutationResponse,
CustomWorldLibraryResponse,
PlatformBrowseHistoryBatchSyncRequest,
PlatformBrowseHistoryResponse,
PlatformBrowseHistoryWriteEntry,
ProfileDashboardSummary,
ProfilePlayStatsResponse,
ProfileWalletLedgerResponse,
RuntimeSettings,
SavedGameSnapshotInput,
} from '../../../packages/shared/src/contracts/runtime.js';
@@ -52,10 +58,10 @@ import {
npcChatDialogueRequestSchema,
npcRecruitDialogueRequestSchema,
} from '../services/chatService.js';
import { generateCustomWorldEntity } from '../services/customWorldEntityGenerationService.js';
import { generateCustomWorldProfile } from '../services/customWorldGenerationService.js';
import {
listCustomWorldWorkSummaries,
} from '../services/customWorldWorkSummaryService.js';
import { generateSceneNpcForLandmark } from '../services/customWorldSceneNpcGenerationService.js';
import { listCustomWorldWorkSummaries } from '../services/customWorldWorkSummaryService.js';
import { generateQuestForNpcEncounter } from '../services/questService.js';
import { generateRuntimeItemIntents } from '../services/runtimeItemService.js';
import {
@@ -82,10 +88,36 @@ const settingsSchema = z.object({
musicVolume: z.number().min(0).max(1),
});
const platformBrowseHistoryEntrySchema = z.object({
ownerUserId: z.string().trim().min(1),
profileId: z.string().trim().min(1),
worldName: z.string().trim().min(1),
subtitle: z.string().trim().optional().default(''),
summaryText: z.string().trim().optional().default(''),
coverImageSrc: z.string().trim().nullable().optional().default(null),
themeMode: z.string().trim().optional().default('mythic'),
authorDisplayName: z.string().trim().optional().default('玩家'),
visitedAt: z.string().trim().optional().default(''),
});
const platformBrowseHistoryBatchSchema = z.object({
entries: z.array(platformBrowseHistoryEntrySchema).max(100),
});
const customWorldProfileSchema = z.object({
profile: jsonObjectSchema,
});
const customWorldSceneNpcSchema = z.object({
profile: jsonObjectSchema,
landmarkId: z.string().trim().min(1),
});
const customWorldEntitySchema = z.object({
profile: jsonObjectSchema,
kind: z.enum(['playable', 'story', 'landmark']),
});
const customWorldSessionSchema = z.object({
settingText: z.string().trim().min(1),
creatorIntent: jsonObjectSchema.nullable().optional().default(null),
@@ -125,6 +157,29 @@ async function resolveAuthDisplayName(context: AppContext, userId: string) {
export function createRuntimeRoutes(context: AppContext) {
const router = Router();
const requireAuth = requireJwtAuth(context.config, context.userRepository);
const routeCompatPaths = (path: string) => [
path,
`/runtime${path}`,
] as const;
const handleCustomWorldEntityGeneration = asyncHandler(async (request, response) => {
const payload = customWorldEntitySchema.parse(request.body) as {
profile: Record<string, unknown>;
kind: 'playable' | 'story' | 'landmark';
};
sendApiResponse(
response,
await generateCustomWorldEntity(context.llmClient, payload),
);
});
const handleCustomWorldSceneNpcGeneration = asyncHandler(async (request, response) => {
const payload = customWorldSceneNpcSchema.parse(request.body) as {
profile: Record<string, unknown>;
landmarkId: string;
};
sendApiResponse(response, {
npc: await generateSceneNpcForLandmark(context.llmClient, payload),
});
});
router.use(requireAuth);
router.use(
@@ -132,6 +187,129 @@ export function createRuntimeRoutes(context: AppContext) {
createCustomWorldAgentRoutes(context),
);
routeCompatPaths('/profile/dashboard').forEach((path, index) => {
router.get(
path,
routeMeta({
operation:
index === 0
? 'profile.dashboard.get'
: 'profile.dashboard.get.compat',
}),
asyncHandler(async (request, response) => {
sendApiResponse<ProfileDashboardSummary>(
response,
await context.runtimeRepository.getProfileDashboard(request.userId!),
);
}),
);
});
routeCompatPaths('/profile/wallet-ledger').forEach((path, index) => {
router.get(
path,
routeMeta({
operation:
index === 0
? 'profile.walletLedger.list'
: 'profile.walletLedger.list.compat',
}),
asyncHandler(async (request, response) => {
sendApiResponse<ProfileWalletLedgerResponse>(response, {
entries: await context.runtimeRepository.listProfileWalletLedger(
request.userId!,
),
});
}),
);
});
routeCompatPaths('/profile/play-stats').forEach((path, index) => {
router.get(
path,
routeMeta({
operation:
index === 0
? 'profile.playStats.get'
: 'profile.playStats.get.compat',
}),
asyncHandler(async (request, response) => {
sendApiResponse<ProfilePlayStatsResponse>(
response,
await context.runtimeRepository.getProfilePlayStats(request.userId!),
);
}),
);
});
routeCompatPaths('/profile/browse-history').forEach((path, index) => {
router.get(
path,
routeMeta({
operation:
index === 0
? 'profile.browseHistory.list'
: 'profile.browseHistory.list.compat',
}),
asyncHandler(async (request, response) => {
sendApiResponse<PlatformBrowseHistoryResponse>(response, {
entries: await context.runtimeRepository.listPlatformBrowseHistory(
request.userId!,
),
});
}),
);
router.post(
path,
routeMeta({
operation:
index === 0
? 'profile.browseHistory.upsert'
: 'profile.browseHistory.upsert.compat',
}),
asyncHandler(async (request, response) => {
const rawBody =
request.body && typeof request.body === 'object' ? request.body : {};
const payload = (
'entries' in rawBody
? platformBrowseHistoryBatchSchema.parse(rawBody)
: platformBrowseHistoryEntrySchema.parse(rawBody)
) as
| PlatformBrowseHistoryBatchSyncRequest
| PlatformBrowseHistoryWriteEntry;
const entries = 'entries' in payload ? payload.entries : [payload];
sendApiResponse<PlatformBrowseHistoryResponse>(response, {
entries:
await context.runtimeRepository.upsertPlatformBrowseHistoryEntries(
request.userId!,
entries,
),
});
}),
);
router.delete(
path,
routeMeta({
operation:
index === 0
? 'profile.browseHistory.clear'
: 'profile.browseHistory.clear.compat',
}),
asyncHandler(async (request, response) => {
await context.runtimeRepository.clearPlatformBrowseHistory(
request.userId!,
);
sendApiResponse<PlatformBrowseHistoryResponse>(response, {
entries: [],
});
}),
);
});
router.post(
'/llm/chat/completions',
routeMeta({ operation: 'runtime.llm.chatCompletionsProxy' }),
@@ -150,6 +328,30 @@ export function createRuntimeRoutes(context: AppContext) {
}),
);
router.post(
'/custom-world/entity',
routeMeta({ operation: 'runtime.customWorld.entity' }),
handleCustomWorldEntityGeneration,
);
router.post(
'/runtime/custom-world/entity',
routeMeta({ operation: 'runtime.customWorld.entity.compat' }),
handleCustomWorldEntityGeneration,
);
router.post(
'/custom-world/scene-npc',
routeMeta({ operation: 'runtime.customWorld.sceneNpc' }),
handleCustomWorldSceneNpcGeneration,
);
router.post(
'/runtime/custom-world/scene-npc',
routeMeta({ operation: 'runtime.customWorld.sceneNpc.compat' }),
handleCustomWorldSceneNpcGeneration,
);
router.get(
'/runtime/save/snapshot',
routeMeta({ operation: 'runtime.snapshot.get' }),
@@ -237,14 +439,11 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world-library',
routeMeta({ operation: 'runtime.customWorldLibrary.list' }),
asyncHandler(async (request, response) => {
sendApiResponse(
response,
{
entries: await context.runtimeRepository.listCustomWorldProfiles(
request.userId!,
),
} satisfies CustomWorldLibraryResponse,
);
sendApiResponse(response, {
entries: await context.runtimeRepository.listCustomWorldProfiles(
request.userId!,
),
} satisfies CustomWorldLibraryResponse);
}),
);
@@ -252,12 +451,10 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world-gallery',
routeMeta({ operation: 'runtime.customWorldGallery.list' }),
asyncHandler(async (_request, response) => {
sendApiResponse(
response,
{
entries: await context.runtimeRepository.listPublishedCustomWorldGallery(),
} satisfies CustomWorldGalleryResponse,
);
sendApiResponse(response, {
entries:
await context.runtimeRepository.listPublishedCustomWorldGallery(),
} satisfies CustomWorldGalleryResponse);
}),
);
@@ -280,12 +477,9 @@ export function createRuntimeRoutes(context: AppContext) {
throw notFound('public custom world not found');
}
sendApiResponse(
response,
{
entry,
} satisfies CustomWorldGalleryDetailResponse,
);
sendApiResponse(response, {
entry,
} satisfies CustomWorldGalleryDetailResponse);
}),
);
@@ -322,15 +516,12 @@ export function createRuntimeRoutes(context: AppContext) {
if (!profileId) {
throw badRequest('profileId is required');
}
sendApiResponse(
response,
{
entries: await context.runtimeRepository.deleteCustomWorldProfile(
request.userId!,
profileId,
),
} satisfies CustomWorldLibraryResponse,
);
sendApiResponse(response, {
entries: await context.runtimeRepository.deleteCustomWorldProfile(
request.userId!,
profileId,
),
} satisfies CustomWorldLibraryResponse);
}),
);