init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
export {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetail,
listRpgEntryWorldGallery,
listRpgEntryWorldLibrary,
publishRpgEntryWorldProfile,
rpgEntryLibraryClient,
type RuntimeRequestOptions,
unpublishRpgEntryWorldProfile,
upsertRpgEntryWorldProfile,
} from './rpgEntryLibraryClient';
export {
clearRpgProfileBrowseHistory,
createRpgProfileRechargeOrder,
getRpgProfileDashboard,
getRpgProfilePlayStats,
getRpgProfileRechargeCenter,
getRpgProfileSettings,
getRpgProfileWalletLedger,
listRpgProfileBrowseHistory,
listRpgProfileSaveArchives,
putRpgProfileSettings,
resumeRpgProfileSaveArchive,
rpgProfileClient,
syncRpgProfileBrowseHistory,
upsertRpgProfileBrowseHistory,
} from './rpgProfileClient';

View File

@@ -0,0 +1,206 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { requestJsonMock } = vi.hoisted(() => ({
requestJsonMock: vi.fn(),
}));
import {
clearRpgProfileBrowseHistory,
listRpgProfileBrowseHistory,
listRpgProfileSaveArchives,
resumeRpgProfileSaveArchive,
syncRpgProfileBrowseHistory,
upsertRpgProfileBrowseHistory,
} from './rpgProfileClient';
import {
getRpgEntryWorldGalleryDetail,
listRpgEntryWorldGallery,
} from './rpgEntryLibraryClient';
vi.mock('../apiClient', () => ({
requestJson: requestJsonMock,
}));
describe('rpgEntry profile browse history routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads browse history from the runtime profile route', async () => {
await listRpgProfileBrowseHistory();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({ method: 'GET' }),
'读取浏览历史失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('writes browse history through the runtime profile route', async () => {
await upsertRpgProfileBrowseHistory({
ownerUserId: 'user-1',
profileId: 'profile-1',
worldName: '测试世界',
subtitle: '测试副标题',
summaryText: '测试摘要',
coverImageSrc: null,
themeMode: 'mythic',
authorDisplayName: '测试作者',
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
'写入浏览历史失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('syncs browse history through the runtime profile route', async () => {
await syncRpgProfileBrowseHistory([
{
ownerUserId: 'user-1',
profileId: 'profile-1',
worldName: '测试世界',
subtitle: '测试副标题',
summaryText: '测试摘要',
coverImageSrc: null,
themeMode: 'mythic',
authorDisplayName: '测试作者',
},
]);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
'同步浏览历史失败',
expect.any(Object),
);
});
it('clears browse history through the runtime profile route', async () => {
await clearRpgProfileBrowseHistory();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({ method: 'DELETE' }),
'清空浏览历史失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});
describe('rpgEntry public custom world gallery routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads the public gallery without attaching auth or refresh coupling', async () => {
await listRpgEntryWorldGallery();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-gallery',
expect.objectContaining({ method: 'GET' }),
'读取作品广场失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
skipAuth: true,
skipRefresh: true,
}),
);
});
it('reads public gallery detail without attaching auth or refresh coupling', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
ownerUserId: 'user-1',
profileId: 'profile-1',
},
});
await getRpgEntryWorldGalleryDetail('user-1', 'profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-gallery/user-1/profile-1',
expect.objectContaining({ method: 'GET' }),
'读取作品详情失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
skipAuth: true,
skipRefresh: true,
}),
);
});
});
describe('rpgEntry save archive routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads save archives from the runtime profile route', async () => {
await listRpgProfileSaveArchives();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/save-archives',
expect.objectContaining({ method: 'GET' }),
'读取存档列表失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('resumes a save archive through the runtime profile route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
worldKey: 'custom:world-1',
},
snapshot: {
version: 2,
savedAt: '2026-04-19T10:15:00.000Z',
bottomTab: 'adventure',
currentStory: null,
gameState: {
worldType: 'CUSTOM',
},
},
});
await resumeRpgProfileSaveArchive('custom:world-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/save-archives/custom%3Aworld-1',
expect.objectContaining({ method: 'POST' }),
'恢复存档失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});

View File

@@ -0,0 +1,165 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { requestJsonMock } = vi.hoisted(() => ({
requestJsonMock: vi.fn(),
}));
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetail,
listRpgEntryWorldGallery,
listRpgEntryWorldLibrary,
publishRpgEntryWorldProfile,
unpublishRpgEntryWorldProfile,
upsertRpgEntryWorldProfile,
} from './rpgEntryLibraryClient';
vi.mock('../apiClient', () => ({
requestJson: requestJsonMock,
}));
describe('rpgEntryLibraryClient world library routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads world library from the runtime entry route', async () => {
await listRpgEntryWorldLibrary();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library',
expect.objectContaining({ method: 'GET' }),
'读取自定义世界库失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('reads world gallery from the public runtime entry route', async () => {
await listRpgEntryWorldGallery();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-gallery',
expect.objectContaining({ method: 'GET' }),
'读取作品广场失败',
expect.objectContaining({
skipAuth: true,
skipRefresh: true,
}),
);
});
it('reads gallery detail from the public runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
ownerUserId: 'owner-1',
profileId: 'profile-1',
},
});
await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-gallery/owner-1/profile-1',
expect.objectContaining({ method: 'GET' }),
'读取作品详情失败',
expect.objectContaining({
skipAuth: true,
skipRefresh: true,
}),
);
});
it('writes world profile through the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
profileId: 'profile-1',
},
entries: [],
});
await upsertRpgEntryWorldProfile({
id: 'profile-1',
name: '测试世界',
} as never);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1',
expect.objectContaining({
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
}),
'保存自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('deletes world profile through the runtime entry route', async () => {
await deleteRpgEntryWorldProfile('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1',
expect.objectContaining({ method: 'DELETE' }),
'删除自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('publishes world profile through the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
profileId: 'profile-1',
},
entries: [],
});
await publishRpgEntryWorldProfile('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1/publish',
expect.objectContaining({ method: 'POST' }),
'发布自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('unpublishes world profile through the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
profileId: 'profile-1',
},
entries: [],
});
await unpublishRpgEntryWorldProfile('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1/unpublish',
expect.objectContaining({ method: 'POST' }),
'下架自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});

View File

@@ -0,0 +1,169 @@
import {
type RuntimeRequestOptions,
requestPublicRpgRuntimeJson,
requestRpgRuntimeJson,
} from '../rpg-runtime/rpgRuntimeRequest';
import type {
CustomWorldGalleryDetailResponse,
CustomWorldGalleryResponse,
CustomWorldLibraryMutationResponse,
CustomWorldLibraryResponse,
} from '../../../packages/shared/src/contracts/runtime';
import type { CustomWorldProfile } from '../../types';
export type { RuntimeRequestOptions };
/**
* RPG 入口世界库 client 的真实实现。
* 第三批收口后,平台首页/详情页开始游戏链直接走 rpg-entry 域请求,不再反向穿旧 storageService 兼容层。
*/
export async function listRpgEntryWorldLibrary(
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<
CustomWorldLibraryResponse<CustomWorldProfile>
>(
'/custom-world-library',
{ method: 'GET' },
'读取自定义世界库失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function listRpgEntryWorldGallery(
options: RuntimeRequestOptions = {},
) {
const response = await requestPublicRpgRuntimeJson<CustomWorldGalleryResponse>(
'/custom-world-gallery',
{ method: 'GET' },
'读取作品广场失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function getRpgEntryWorldGalleryDetail(
ownerUserId: string,
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestPublicRpgRuntimeJson<
CustomWorldGalleryDetailResponse<CustomWorldProfile>
>(
`/custom-world-gallery/${encodeURIComponent(ownerUserId)}/${encodeURIComponent(profileId)}`,
{ method: 'GET' },
'读取作品详情失败',
options,
);
return response.entry;
}
export async function getRpgEntryWorldGalleryDetailByCode(
code: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestPublicRpgRuntimeJson<
CustomWorldGalleryDetailResponse<CustomWorldProfile>
>(
`/custom-world-gallery/by-code/${encodeURIComponent(code)}`,
{ method: 'GET' },
'读取作品详情失败',
options,
);
return response.entry;
}
export async function upsertRpgEntryWorldProfile(
profile: CustomWorldProfile,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<
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 deleteRpgEntryWorldProfile(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<
CustomWorldLibraryResponse<CustomWorldProfile>
>(
`/custom-world-library/${encodeURIComponent(profileId)}`,
{ method: 'DELETE' },
'删除自定义世界失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function publishRpgEntryWorldProfile(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<
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 unpublishRpgEntryWorldProfile(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<
CustomWorldLibraryMutationResponse<CustomWorldProfile>
>(
`/custom-world-library/${encodeURIComponent(profileId)}/unpublish`,
{ method: 'POST' },
'下架自定义世界失败',
options,
);
return {
entry: response.entry,
entries: Array.isArray(response?.entries) ? response.entries : [],
};
}
export const rpgEntryLibraryClient = {
listWorldLibrary: listRpgEntryWorldLibrary,
listWorldGallery: listRpgEntryWorldGallery,
getWorldGalleryDetail: getRpgEntryWorldGalleryDetail,
getWorldGalleryDetailByCode: getRpgEntryWorldGalleryDetailByCode,
upsertWorldProfile: upsertRpgEntryWorldProfile,
deleteWorldProfile: deleteRpgEntryWorldProfile,
publishWorldProfile: publishRpgEntryWorldProfile,
unpublishWorldProfile: unpublishRpgEntryWorldProfile,
};

View File

@@ -0,0 +1,158 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { requestJsonMock } = vi.hoisted(() => ({
requestJsonMock: vi.fn(),
}));
import {
clearRpgProfileBrowseHistory,
listRpgProfileBrowseHistory,
listRpgProfileSaveArchives,
resumeRpgProfileSaveArchive,
syncRpgProfileBrowseHistory,
upsertRpgProfileBrowseHistory,
} from './rpgProfileClient';
vi.mock('../apiClient', () => ({
requestJson: requestJsonMock,
}));
describe('rpgProfileClient browse history routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads browse history from the runtime profile route', async () => {
await listRpgProfileBrowseHistory();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({ method: 'GET' }),
'读取浏览历史失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('writes browse history through the runtime profile route', async () => {
await upsertRpgProfileBrowseHistory({
ownerUserId: 'user-1',
profileId: 'profile-1',
worldName: '测试世界',
subtitle: '测试副标题',
summaryText: '测试摘要',
coverImageSrc: null,
themeMode: 'mythic',
authorDisplayName: '测试作者',
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
'写入浏览历史失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('syncs browse history through the runtime profile route', async () => {
await syncRpgProfileBrowseHistory([
{
ownerUserId: 'user-1',
profileId: 'profile-1',
worldName: '测试世界',
subtitle: '测试副标题',
summaryText: '测试摘要',
coverImageSrc: null,
themeMode: 'mythic',
authorDisplayName: '测试作者',
},
]);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
'同步浏览历史失败',
expect.any(Object),
);
});
it('clears browse history through the runtime profile route', async () => {
await clearRpgProfileBrowseHistory();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/browse-history',
expect.objectContaining({ method: 'DELETE' }),
'清空浏览历史失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});
describe('rpgProfileClient save archive routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads save archives from the runtime profile route', async () => {
await listRpgProfileSaveArchives();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/save-archives',
expect.objectContaining({ method: 'GET' }),
'读取存档列表失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('resumes a save archive through the runtime profile route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
worldKey: 'custom:world-1',
},
snapshot: {
version: 2,
savedAt: '2026-04-19T10:15:00.000Z',
bottomTab: 'adventure',
currentStory: null,
gameState: {
worldType: 'CUSTOM',
},
},
});
await resumeRpgProfileSaveArchive('custom:world-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/save-archives/custom%3Aworld-1',
expect.objectContaining({ method: 'POST' }),
'恢复存档失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});

View File

@@ -0,0 +1,249 @@
import type {
CreateProfileRechargeOrderResponse,
PlatformBrowseHistoryBatchSyncRequest,
PlatformBrowseHistoryResponse,
PlatformBrowseHistoryWriteEntry,
ProfileDashboardSummary,
ProfilePlayStatsResponse,
ProfileReferralInviteCenterResponse,
ProfileRechargeCenterResponse,
ProfileSaveArchiveListResponse,
ProfileSaveArchiveResumeResponse,
ProfileWalletLedgerResponse,
RedeemProfileReferralInviteCodeResponse,
RuntimeSettings,
} from '../../../packages/shared/src/contracts/runtime';
import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
requestRpgRuntimeJson,
type RuntimeRequestOptions,
} from '../rpg-runtime/rpgRuntimeRequest';
export type { RuntimeRequestOptions };
/**
* RPG profile 域 client。
* 工作包 C 需要把继续游戏归档与资料读取收进新域目录,避免继续堆在 `storageService`。
*/
export function getRpgProfileSettings(options: RuntimeRequestOptions = {}) {
return requestRpgRuntimeJson<RuntimeSettings>(
'/settings',
{ method: 'GET' },
'读取设置失败',
options,
);
}
export function putRpgProfileSettings(
settings: RuntimeSettings,
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<RuntimeSettings>(
'/settings',
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
},
'保存设置失败',
options,
);
}
export function getRpgProfileDashboard(options: RuntimeRequestOptions = {}) {
return requestRpgRuntimeJson<ProfileDashboardSummary>(
'/profile/dashboard',
{ method: 'GET' },
'读取个人看板失败',
options,
);
}
export function getRpgProfileWalletLedger(
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<ProfileWalletLedgerResponse>(
'/profile/wallet-ledger',
{ method: 'GET' },
'读取资产流水失败',
options,
);
}
export function getRpgProfileRechargeCenter(
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<ProfileRechargeCenterResponse>(
'/profile/recharge-center',
{ method: 'GET' },
'读取账户充值失败',
options,
);
}
export function createRpgProfileRechargeOrder(
productId: string,
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<CreateProfileRechargeOrderResponse>(
'/profile/recharge/orders',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, paymentChannel: 'mock' }),
},
'充值失败',
options,
);
}
export function getRpgProfileReferralInviteCenter(
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<ProfileReferralInviteCenterResponse>(
'/profile/referrals/invite-center',
{ method: 'GET' },
'读取邀请码失败',
options,
);
}
export function redeemRpgProfileReferralInviteCode(
inviteCode: string,
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<RedeemProfileReferralInviteCodeResponse>(
'/profile/referrals/redeem-code',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inviteCode }),
},
'填写邀请码失败',
options,
);
}
export function getRpgProfilePlayStats(options: RuntimeRequestOptions = {}) {
return requestRpgRuntimeJson<ProfilePlayStatsResponse>(
'/profile/play-stats',
{ method: 'GET' },
'读取游玩统计失败',
options,
);
}
export async function listRpgProfileSaveArchives(
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<ProfileSaveArchiveListResponse>(
'/profile/save-archives',
{ method: 'GET' },
'读取存档列表失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function resumeRpgProfileSaveArchive(
worldKey: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<ProfileSaveArchiveResumeResponse>(
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复存档失败',
options,
);
return {
entry: response.entry,
snapshot: rehydrateSavedSnapshot(
response.snapshot as HydratedSavedGameSnapshot,
),
};
}
export async function listRpgProfileBrowseHistory(
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<PlatformBrowseHistoryResponse>(
'/profile/browse-history',
{ method: 'GET' },
'读取浏览历史失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function upsertRpgProfileBrowseHistory(
entry: PlatformBrowseHistoryWriteEntry,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<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 syncRpgProfileBrowseHistory(
entries: PlatformBrowseHistoryWriteEntry[],
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<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 clearRpgProfileBrowseHistory(
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<PlatformBrowseHistoryResponse>(
'/profile/browse-history',
{ method: 'DELETE' },
'清空浏览历史失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export const rpgProfileClient = {
getDashboard: getRpgProfileDashboard,
getPlayStats: getRpgProfilePlayStats,
getWalletLedger: getRpgProfileWalletLedger,
getRechargeCenter: getRpgProfileRechargeCenter,
createRechargeOrder: createRpgProfileRechargeOrder,
getReferralInviteCenter: getRpgProfileReferralInviteCenter,
redeemReferralInviteCode: redeemRpgProfileReferralInviteCode,
getSettings: getRpgProfileSettings,
putSettings: putRpgProfileSettings,
listSaveArchives: listRpgProfileSaveArchives,
resumeSaveArchive: resumeRpgProfileSaveArchive,
listBrowseHistory: listRpgProfileBrowseHistory,
upsertBrowseHistory: upsertRpgProfileBrowseHistory,
syncBrowseHistory: syncRpgProfileBrowseHistory,
clearBrowseHistory: clearRpgProfileBrowseHistory,
};