import type { ListCustomWorldWorksResponse } from '../../packages/shared/src/contracts/customWorldAgent'; import type { BasicOkResult, CustomWorldGalleryDetailResponse, CustomWorldGalleryResponse, CustomWorldLibraryEntry, CustomWorldLibraryMutationResponse, CustomWorldLibraryResponse, PlatformBrowseHistoryBatchSyncRequest, PlatformBrowseHistoryEntry, PlatformBrowseHistoryResponse, PlatformBrowseHistoryWriteEntry, ProfileDashboardSummary, ProfileSaveArchiveListResponse, ProfileSaveArchiveResumeResponse, ProfileSaveArchiveSummary, ProfilePlayStatsResponse, ProfileWalletLedgerResponse, RuntimeSettings, } from '../../packages/shared/src/contracts/runtime'; import type { SavedGameSnapshotInput } from '../persistence/gameSaveStorage'; import { rehydrateSavedSnapshot } from '../persistence/runtimeSnapshot'; import type { HydratedSavedGameSnapshot } from '../persistence/runtimeSnapshotTypes'; import type { CustomWorldProfile } from '../types'; import { type ApiRetryOptions, requestJson } from './apiClient'; const RUNTIME_API_BASE = '/api/runtime'; const RUNTIME_READ_RETRY: ApiRetryOptions = { maxRetries: 1, baseDelayMs: 180, maxDelayMs: 480, }; const RUNTIME_WRITE_RETRY: ApiRetryOptions = { maxRetries: 1, baseDelayMs: 240, maxDelayMs: 640, retryUnsafeMethods: true, }; export type RuntimeRequestOptions = { signal?: AbortSignal; retry?: ApiRetryOptions; skipAuth?: boolean; skipRefresh?: boolean; }; function requestRuntimeJson( path: string, init: RequestInit, fallbackMessage: string, options: RuntimeRequestOptions = {}, ) { const method = (init.method ?? 'GET').toUpperCase(); const retry = options.retry ?? (method === 'GET' ? RUNTIME_READ_RETRY : RUNTIME_WRITE_RETRY); return requestJson( `${RUNTIME_API_BASE}${path}`, { ...init, signal: options.signal, }, fallbackMessage, { retry, skipAuth: options.skipAuth, skipRefresh: options.skipRefresh, }, ); } function requestPublicRuntimeJson( path: string, init: RequestInit, fallbackMessage: string, options: RuntimeRequestOptions = {}, ) { return requestRuntimeJson(path, init, fallbackMessage, { ...options, skipAuth: true, skipRefresh: true, }); } export async function getSaveSnapshot(options: RuntimeRequestOptions = {}) { const snapshot = await requestRuntimeJson( '/save/snapshot', { method: 'GET' }, '读取存档失败', options, ); return snapshot ? rehydrateSavedSnapshot(snapshot) : null; } export async function putSaveSnapshot( snapshot: SavedGameSnapshotInput, options: RuntimeRequestOptions = {}, ) { const savedSnapshot = await requestRuntimeJson( '/save/snapshot', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(snapshot), }, '保存存档失败', options, ); return rehydrateSavedSnapshot(savedSnapshot); } export async function deleteSaveSnapshot(options: RuntimeRequestOptions = {}) { return requestRuntimeJson( '/save/snapshot', { method: 'DELETE' }, '删除存档失败', options, ); } export async function getSettings(options: RuntimeRequestOptions = {}) { return requestRuntimeJson( '/settings', { method: 'GET' }, '读取设置失败', options, ); } export async function getProfileDashboard(options: RuntimeRequestOptions = {}) { return requestRuntimeJson( '/profile/dashboard', { method: 'GET' }, '读取个人看板失败', options, ); } export async function getProfileWalletLedger( options: RuntimeRequestOptions = {}, ) { return requestRuntimeJson( '/profile/wallet-ledger', { method: 'GET' }, '读取资产流水失败', options, ); } export async function getProfilePlayStats(options: RuntimeRequestOptions = {}) { return requestRuntimeJson( '/profile/play-stats', { method: 'GET' }, '读取游玩统计失败', options, ); } export async function listProfileSaveArchives( options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson( '/profile/save-archives', { method: 'GET' }, '读取存档列表失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function resumeProfileSaveArchive( worldKey: string, options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson< ProfileSaveArchiveResumeResponse >( `/profile/save-archives/${encodeURIComponent(worldKey)}`, { method: 'POST' }, '恢复存档失败', options, ); return { entry: response.entry, snapshot: rehydrateSavedSnapshot( response.snapshot as HydratedSavedGameSnapshot, ), }; } export async function putSettings( settings: RuntimeSettings, options: RuntimeRequestOptions = {}, ) { return requestRuntimeJson( '/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings), }, '保存设置失败', options, ); } export async function listCustomWorldLibrary( options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson< CustomWorldLibraryResponse >( '/custom-world-library', { method: 'GET' }, '读取自定义世界库失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function listCustomWorldWorks( options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson( '/custom-world/works', { method: 'GET' }, '读取创作作品列表失败', options, ); return Array.isArray(response?.items) ? response.items : []; } export async function upsertCustomWorldProfile( profile: CustomWorldProfile, options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson< CustomWorldLibraryMutationResponse >( `/custom-world-library/${encodeURIComponent(profile.id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile, }), }, '保存自定义世界失败', options, ); return { entry: response.entry, entries: Array.isArray(response?.entries) ? response.entries : [], }; } export async function deleteCustomWorldProfile( profileId: string, options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson< CustomWorldLibraryResponse >( `/custom-world-library/${encodeURIComponent(profileId)}`, { method: 'DELETE' }, '删除自定义世界失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function publishCustomWorldProfile( profileId: string, options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson< CustomWorldLibraryMutationResponse >( `/custom-world-library/${encodeURIComponent(profileId)}/publish`, { method: 'POST' }, '发布自定义世界失败', options, ); return { entry: response.entry, entries: Array.isArray(response?.entries) ? response.entries : [], }; } export async function unpublishCustomWorldProfile( profileId: string, options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson< CustomWorldLibraryMutationResponse >( `/custom-world-library/${encodeURIComponent(profileId)}/unpublish`, { method: 'POST' }, '下架自定义世界失败', options, ); return { entry: response.entry, entries: Array.isArray(response?.entries) ? response.entries : [], }; } export async function listCustomWorldGallery( options: RuntimeRequestOptions = {}, ) { const response = await requestPublicRuntimeJson( '/custom-world-gallery', { method: 'GET' }, '读取作品广场失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function getCustomWorldGalleryDetail( ownerUserId: string, profileId: string, options: RuntimeRequestOptions = {}, ) { const response = await requestPublicRuntimeJson< CustomWorldGalleryDetailResponse >( `/custom-world-gallery/${encodeURIComponent(ownerUserId)}/${encodeURIComponent(profileId)}`, { method: 'GET' }, '读取作品详情失败', options, ); return response.entry; } export async function listProfileBrowseHistory( options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson( '/profile/browse-history', { method: 'GET' }, '读取浏览历史失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function upsertProfileBrowseHistory( entry: PlatformBrowseHistoryWriteEntry, options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson( '/profile/browse-history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry), }, '写入浏览历史失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function syncProfileBrowseHistory( entries: PlatformBrowseHistoryWriteEntry[], options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson( '/profile/browse-history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entries, } satisfies PlatformBrowseHistoryBatchSyncRequest), }, '同步浏览历史失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export async function clearProfileBrowseHistory( options: RuntimeRequestOptions = {}, ) { const response = await requestRuntimeJson( '/profile/browse-history', { method: 'DELETE' }, '清空浏览历史失败', options, ); return Array.isArray(response?.entries) ? response.entries : []; } export const runtimeStorageClient = { getSaveSnapshot, putSaveSnapshot, deleteSaveSnapshot, getSettings, putSettings, getProfileDashboard, getProfileWalletLedger, getProfilePlayStats, listProfileSaveArchives, resumeProfileSaveArchive, listCustomWorldLibrary, listCustomWorldWorks, upsertCustomWorldProfile, deleteCustomWorldProfile, publishCustomWorldProfile, unpublishCustomWorldProfile, listCustomWorldGallery, getCustomWorldGalleryDetail, listProfileBrowseHistory, upsertProfileBrowseHistory, syncProfileBrowseHistory, clearProfileBrowseHistory, }; export type { CustomWorldLibraryEntry }; export type { PlatformBrowseHistoryEntry }; export type { ProfileSaveArchiveSummary };