Files
Genarrative/src/services/storageService.ts
高物 50759f3c1e
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-20 09:54:17 +08:00

441 lines
11 KiB
TypeScript

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<T>(
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<T>(
`${RUNTIME_API_BASE}${path}`,
{
...init,
signal: options.signal,
},
fallbackMessage,
{
retry,
skipAuth: options.skipAuth,
skipRefresh: options.skipRefresh,
},
);
}
function requestPublicRuntimeJson<T>(
path: string,
init: RequestInit,
fallbackMessage: string,
options: RuntimeRequestOptions = {},
) {
return requestRuntimeJson<T>(path, init, fallbackMessage, {
...options,
skipAuth: true,
skipRefresh: true,
});
}
export async function getSaveSnapshot(options: RuntimeRequestOptions = {}) {
const snapshot = await requestRuntimeJson<HydratedSavedGameSnapshot | null>(
'/save/snapshot',
{ method: 'GET' },
'读取存档失败',
options,
);
return snapshot ? rehydrateSavedSnapshot(snapshot) : null;
}
export async function putSaveSnapshot(
snapshot: SavedGameSnapshotInput,
options: RuntimeRequestOptions = {},
) {
const savedSnapshot = await requestRuntimeJson<HydratedSavedGameSnapshot>(
'/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<BasicOkResult>(
'/save/snapshot',
{ method: 'DELETE' },
'删除存档失败',
options,
);
}
export async function getSettings(options: RuntimeRequestOptions = {}) {
return requestRuntimeJson<RuntimeSettings>(
'/settings',
{ method: 'GET' },
'读取设置失败',
options,
);
}
export async function getProfileDashboard(options: RuntimeRequestOptions = {}) {
return requestRuntimeJson<ProfileDashboardSummary>(
'/profile/dashboard',
{ method: 'GET' },
'读取个人看板失败',
options,
);
}
export async function getProfileWalletLedger(
options: RuntimeRequestOptions = {},
) {
return requestRuntimeJson<ProfileWalletLedgerResponse>(
'/profile/wallet-ledger',
{ method: 'GET' },
'读取资产流水失败',
options,
);
}
export async function getProfilePlayStats(options: RuntimeRequestOptions = {}) {
return requestRuntimeJson<ProfilePlayStatsResponse>(
'/profile/play-stats',
{ method: 'GET' },
'读取游玩统计失败',
options,
);
}
export async function listProfileSaveArchives(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<ProfileSaveArchiveListResponse>(
'/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<RuntimeSettings>(
'/settings',
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
},
'保存设置失败',
options,
);
}
export async function listCustomWorldLibrary(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<
CustomWorldLibraryResponse<CustomWorldProfile>
>(
'/custom-world-library',
{ method: 'GET' },
'读取自定义世界库失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function listCustomWorldWorks(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<ListCustomWorldWorksResponse>(
'/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<CustomWorldProfile>
>(
`/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<CustomWorldProfile>
>(
`/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<CustomWorldProfile>
>(
`/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<CustomWorldProfile>
>(
`/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<CustomWorldGalleryResponse>(
'/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<CustomWorldProfile>
>(
`/custom-world-gallery/${encodeURIComponent(ownerUserId)}/${encodeURIComponent(profileId)}`,
{ method: 'GET' },
'读取作品详情失败',
options,
);
return response.entry;
}
export async function listProfileBrowseHistory(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<PlatformBrowseHistoryResponse>(
'/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<PlatformBrowseHistoryResponse>(
'/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<PlatformBrowseHistoryResponse>(
'/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<PlatformBrowseHistoryResponse>(
'/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 };