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

@@ -899,13 +899,21 @@ describe('ai orchestration fallbacks', () => {
ok: true,
text: async () =>
JSON.stringify({
imageSrc: '/generated-custom-world-scenes/world/landmark/scene.png',
assetId: 'custom-scene-1',
model: 'wan2.7-image',
size: '1280*720',
taskId: 'task-123',
prompt: '系统整理后的提示词',
actualPrompt: '扩写后的提示词',
ok: true,
data: {
ok: true,
imageSrc: '/generated-custom-world-scenes/world/landmark/scene.png',
assetId: 'custom-scene-1',
model: 'wan2.7-image',
size: '1280*720',
taskId: 'task-123',
prompt: '系统整理后的提示词',
actualPrompt: '扩写后的提示词',
},
error: null,
meta: {
apiVersion: '2026-04-08',
},
}),
} as Response);

View File

@@ -1,3 +1,9 @@
import type {
CustomWorldGenerationStep,
GenerateCustomWorldProfileInput,
GenerateCustomWorldProfileOptions,
} from '../../packages/shared/src/contracts/runtime';
import { unwrapApiResponse } from '../../packages/shared/src/http';
import { createSceneHostileNpcsFromEncounters } from '../data/hostileNpcs';
import {
buildEncounterFromSceneNpc,
@@ -26,12 +32,6 @@ import {
WorldStoryGraph,
WorldType,
} from '../types';
import type {
CustomWorldGenerationStep,
CustomWorldGenerationProgress,
GenerateCustomWorldProfileInput,
GenerateCustomWorldProfileOptions,
} from '../../packages/shared/src/contracts/runtime';
import {
buildOfflineCharacterPanelChatReply as buildOfflineCharacterPanelChatReplyFromFallback,
buildOfflineCharacterPanelChatSuggestions as buildOfflineCharacterPanelChatSuggestionsFromFallback,
@@ -136,7 +136,6 @@ export type {
GenerateCustomWorldProfileInput,
GenerateCustomWorldProfileOptions,
} from '../../packages/shared/src/contracts/runtime';
export type {
StoryGenerationContext,
StoryRequestOptions,
@@ -2018,8 +2017,8 @@ export async function generateCustomWorldSceneImage({
);
}
const data = JSON.parse(
responseText,
const data = unwrapApiResponse(
JSON.parse(responseText) as Partial<CustomWorldSceneImageResult>,
) as Partial<CustomWorldSceneImageResult>;
if (
!data.imageSrc ||

View File

@@ -30,6 +30,9 @@ import { parseApiErrorMessage } from '../../packages/shared/src/http';
import type {
AIResponse,
Character,
CustomWorldLandmark,
CustomWorldNpc,
CustomWorldPlayableNpc,
CharacterChatTurn,
CustomWorldProfile,
Encounter,
@@ -49,6 +52,7 @@ import { type CharacterChatTargetStatus } from './characterChatPrompt';
import { parseLineListContent } from './llmParsers';
const RUNTIME_API_BASE = '/api/runtime';
const CUSTOM_WORLD_API_BASE = '/api';
type LegacyAiModule = typeof import('./ai');
@@ -490,6 +494,74 @@ export async function generateCustomWorldSceneImage(
return aiClient.generateCustomWorldSceneImage(...args);
}
export async function generateCustomWorldSceneNpc(payload: {
profile: CustomWorldProfile;
landmarkId: string;
}) {
const response = await requestPostJson<{ npc: CustomWorldNpc }>(
`${CUSTOM_WORLD_API_BASE}/custom-world/scene-npc`,
payload,
'生成场景 NPC 失败',
);
return response.npc;
}
async function requestCustomWorldEntity<T>(
payload: {
profile: CustomWorldProfile;
kind: 'playable' | 'story' | 'landmark';
},
fallbackMessage: string,
) {
return requestPostJson<{
kind: 'playable' | 'story' | 'landmark';
entity: T;
}>(`${CUSTOM_WORLD_API_BASE}/custom-world/entity`, payload, fallbackMessage);
}
export async function generateCustomWorldPlayableNpc(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestCustomWorldEntity<CustomWorldPlayableNpc>(
{
...payload,
kind: 'playable',
},
'生成可扮演角色失败',
);
return response.entity;
}
export async function generateCustomWorldStoryNpc(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestCustomWorldEntity<CustomWorldNpc>(
{
...payload,
kind: 'story',
},
'生成场景角色失败',
);
return response.entity;
}
export async function generateCustomWorldLandmark(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestCustomWorldEntity<CustomWorldLandmark>(
{
...payload,
kind: 'landmark',
},
'生成场景失败',
);
return response.entity;
}
export async function createCustomWorldSession(payload: {
settingText: string;
creatorIntent?: Record<string, unknown> | null;

View File

@@ -2354,7 +2354,7 @@ export function buildCustomWorldSceneImagePrompt(
'下半部分的内容必须是明确可站立的地面本体,例如道路、石板、平台、广场、甲板、沙地或草地,要有连续、稳定、可落脚的站位逻辑,不能只是装饰性前景、坑洞、障碍堆、栏杆带或不可通行的景物。',
'下半部分地面近景要保持相对简洁、低细节、轮廓清楚、便于角色站立,不要堆满道具、植被、碎石、栏杆或复杂装饰。',
options.hasReferenceImage
? '已提供一张自定义参考图,可适度参考其构图、镜头或氛围,但仍以本次场景需求为准,不要生硬照搬。'
? '已提供一张自定义参考图,请沿用其构图、镜头或氛围线索,同时继续满足本次场景需求。'
: '',
`世界:${worldName}${worldSubtitle ? `${worldSubtitle}` : ''}`,
worldSetting ? `玩家设定:${worldSetting}` : '',

View File

@@ -116,9 +116,9 @@ test('marks all legacy progress steps complete when draft foundation finishes',
test('builds readable draft setting text from creator intent first', () => {
const settingText = buildAgentDraftFoundationSettingText(baseSession);
expect(settingText).toContain('世界核心命题');
expect(settingText).toContain('玩家身份');
expect(settingText).toContain('标志性要素');
expect(settingText).toContain('世界一句话');
expect(settingText).toContain('玩家开局');
expect(settingText).toContain('标志素');
});
test('falls back to latest user message when creator intent is unavailable', () => {

View File

@@ -7,8 +7,7 @@ import type {
CustomWorldGenerationStep,
} from '../../packages/shared/src/contracts/runtime';
import {
buildCustomWorldCreatorIntentDisplayText,
buildCustomWorldCreatorIntentGenerationText,
buildCustomWorldCreatorIntentFoundationText,
normalizeCustomWorldCreatorIntent,
} from './customWorldCreatorIntent';
@@ -177,17 +176,11 @@ export function buildAgentDraftFoundationSettingText(
);
if (creatorIntent) {
const generationText =
buildCustomWorldCreatorIntentGenerationText(creatorIntent).trim();
const displayText =
buildCustomWorldCreatorIntentDisplayText(creatorIntent).trim();
const foundationText =
buildCustomWorldCreatorIntentFoundationText(creatorIntent).trim();
if (generationText) {
return generationText;
}
if (displayText) {
return displayText;
if (foundationText) {
return foundationText;
}
if (creatorIntent.rawSettingText.trim()) {

View File

@@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest';
import {
buildCustomWorldAnchorPackFromIntent,
buildPendingClarifications,
buildCustomWorldCreatorIntentDisplayText,
buildCustomWorldCreatorIntentFoundationText,
buildPendingClarifications,
createEmptyCustomWorldCreatorIntent,
evaluateCustomWorldCreatorIntentReadiness,
mergeCustomWorldCreatorIntent,
@@ -42,6 +43,41 @@ describe('customWorldCreatorIntent', () => {
expect(summary).toContain('关键角色:沈砺 / 灰炬向导');
});
it('builds six-anchor foundation text from structured creator intent', () => {
const intent = {
...createEmptyCustomWorldCreatorIntent('card'),
worldHook: '一个会被灵潮反复改写地形的边境世界。',
themeKeywords: ['边境', '灵潮'],
toneDirectives: ['紧张', '潮湿'],
playerPremise: '玩家是带着旧名单回来的前巡夜人。',
openingSituation: '返乡第一夜,封锁线外出现了本不该存在的灯火。',
coreConflicts: ['旧案名单再次出现'],
keyCharacters: [
{
id: 'character-1',
name: '沈砺',
role: '灰炬向导',
publicMask: '看起来只是熟路的带路人',
hiddenHook: '他一直在追查撤离线失控真相',
relationToPlayer: '会先怀疑玩家身份',
notes: '',
locked: true,
},
],
iconicElements: ['会逆向蔓延的潮雾'],
};
const foundationText = buildCustomWorldCreatorIntentFoundationText(intent);
expect(foundationText).toContain(
'世界一句话:一个会被灵潮反复改写地形的边境世界。',
);
expect(foundationText).toContain('玩家开局:玩家是带着旧名单回来的前巡夜人。');
expect(foundationText).toContain('主题气质:边境、灵潮 / 紧张、潮湿');
expect(foundationText).toContain('关键关系:沈砺 · 灰炬向导');
expect(foundationText).toContain('标志元素:会逆向蔓延的潮雾');
});
it('builds anchor pack from creator intent and keeps locked ids', () => {
const intent = {
...createEmptyCustomWorldCreatorIntent('card'),

View File

@@ -710,6 +710,48 @@ function buildAnchorLine(label: string, content: string) {
return content ? `${label}${content}` : '';
}
export function buildCustomWorldCreatorIntentFoundationText(
intent: CustomWorldCreatorIntent | null | undefined,
) {
if (!hasMeaningfulCustomWorldCreatorIntent(intent)) {
return '';
}
const relationshipSeed = intent?.keyCharacters[0];
const relationshipText = relationshipSeed
? [
relationshipSeed.name,
relationshipSeed.role,
relationshipSeed.relationToPlayer
? `与玩家 ${relationshipSeed.relationToPlayer}`
: '',
relationshipSeed.hiddenHook ? `暗线 ${relationshipSeed.hiddenHook}` : '',
]
.filter(Boolean)
.join(' · ')
: '';
const playerOpeningText = [intent?.playerPremise || '', intent?.openingSituation || '']
.filter(Boolean)
.join('');
const themeToneText = [
intent?.themeKeywords.join('、') || '',
intent?.toneDirectives.join('、') || '',
]
.filter(Boolean)
.join(' / ');
return [
buildAnchorLine('世界一句话', intent?.worldHook || ''),
buildAnchorLine('玩家开局', playerOpeningText),
buildAnchorLine('主题气质', themeToneText),
buildAnchorLine('核心冲突', intent?.coreConflicts.join('') || ''),
buildAnchorLine('关键关系', relationshipText),
buildAnchorLine('标志元素', intent?.iconicElements.join('、') || ''),
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldCreatorIntentDisplayText(
intent: CustomWorldCreatorIntent | null | undefined,
) {

View File

@@ -1,23 +1,19 @@
import type { CustomWorldGalleryCard } from '../../packages/shared/src/contracts/runtime';
import type {
PlatformBrowseHistoryEntry,
PlatformBrowseHistoryWriteEntry,
} from '../../packages/shared/src/contracts/runtime';
import type { AuthUser } from './authService';
export type PlatformBrowseHistoryEntry = {
ownerUserId: string;
profileId: string;
worldName: string;
subtitle: string;
summaryText: string;
coverImageSrc: string | null;
themeMode: CustomWorldGalleryCard['themeMode'];
authorDisplayName: string;
visitedAt: string;
};
export type { PlatformBrowseHistoryEntry, PlatformBrowseHistoryWriteEntry };
const HISTORY_STORAGE_KEY_PREFIX = 'genarrative.platform.browse-history.v1';
const HISTORY_SYNC_KEY_PREFIX = 'genarrative.platform.browse-history.synced.v1';
const MAX_HISTORY_ENTRIES = 20;
function canUseLocalStorage() {
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
return (
typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
);
}
function buildHistoryStorageKey(user: AuthUser | null | undefined) {
@@ -25,6 +21,11 @@ function buildHistoryStorageKey(user: AuthUser | null | undefined) {
return `${HISTORY_STORAGE_KEY_PREFIX}:${accountId}`;
}
function buildHistorySyncKey(user: AuthUser | null | undefined) {
const accountId = user?.id?.trim() || user?.username?.trim() || 'guest';
return `${HISTORY_SYNC_KEY_PREFIX}:${accountId}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -33,7 +34,9 @@ function readString(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeHistoryEntry(value: unknown): PlatformBrowseHistoryEntry | null {
function normalizeHistoryEntry(
value: unknown,
): PlatformBrowseHistoryEntry | null {
if (!isRecord(value)) {
return null;
}
@@ -42,12 +45,11 @@ function normalizeHistoryEntry(value: unknown): PlatformBrowseHistoryEntry | nul
const profileId = readString(value.profileId);
const worldName = readString(value.worldName);
const visitedAt = readString(value.visitedAt);
if (!ownerUserId || !profileId || !worldName || !visitedAt) {
return null;
}
const themeMode = readString(value.themeMode) as PlatformBrowseHistoryEntry['themeMode'];
return {
ownerUserId,
profileId,
@@ -55,7 +57,10 @@ function normalizeHistoryEntry(value: unknown): PlatformBrowseHistoryEntry | nul
subtitle: readString(value.subtitle),
summaryText: readString(value.summaryText),
coverImageSrc: readString(value.coverImageSrc) || null,
themeMode: themeMode || 'mythic',
themeMode:
(readString(
value.themeMode,
) as PlatformBrowseHistoryEntry['themeMode']) || 'mythic',
authorDisplayName: readString(value.authorDisplayName) || '玩家',
visitedAt,
};
@@ -97,19 +102,20 @@ export function readPlatformBrowseHistory(user: AuthUser | null | undefined) {
export function writePlatformBrowseHistory(
user: AuthUser | null | undefined,
entry: Omit<PlatformBrowseHistoryEntry, 'visitedAt'> & {
visitedAt?: string;
},
entry: PlatformBrowseHistoryWriteEntry,
) {
if (!canUseLocalStorage()) {
return [] as PlatformBrowseHistoryEntry[];
}
const nextEntry: PlatformBrowseHistoryEntry = {
...entry,
ownerUserId: entry.ownerUserId.trim(),
profileId: entry.profileId.trim(),
worldName: entry.worldName.trim(),
subtitle: entry.subtitle?.trim() || '',
summaryText: entry.summaryText?.trim() || '',
coverImageSrc: entry.coverImageSrc?.trim() || null,
themeMode: entry.themeMode || 'mythic',
authorDisplayName: entry.authorDisplayName?.trim() || '玩家',
visitedAt: entry.visitedAt?.trim() || new Date().toISOString(),
};
@@ -129,5 +135,38 @@ export function writePlatformBrowseHistory(
buildHistoryStorageKey(user),
JSON.stringify(nextEntries),
);
return nextEntries;
}
export function clearPlatformBrowseHistory(user: AuthUser | null | undefined) {
if (!canUseLocalStorage()) {
return;
}
window.localStorage.removeItem(buildHistoryStorageKey(user));
window.localStorage.removeItem(buildHistorySyncKey(user));
}
export function hasPendingPlatformBrowseHistoryMigration(
user: AuthUser | null | undefined,
) {
if (!canUseLocalStorage()) {
return false;
}
return (
readPlatformBrowseHistory(user).length > 0 &&
window.localStorage.getItem(buildHistorySyncKey(user)) !== '1'
);
}
export function markPlatformBrowseHistoryMigrated(
user: AuthUser | null | undefined,
) {
if (!canUseLocalStorage()) {
return;
}
window.localStorage.setItem(buildHistorySyncKey(user), '1');
}

View File

@@ -1,6 +1,4 @@
import type {
ListCustomWorldWorksResponse,
} from '../../packages/shared/src/contracts/customWorldAgent';
import type { ListCustomWorldWorksResponse } from '../../packages/shared/src/contracts/customWorldAgent';
import type {
BasicOkResult,
CustomWorldGalleryDetailResponse,
@@ -8,15 +6,20 @@ import type {
CustomWorldLibraryEntry,
CustomWorldLibraryMutationResponse,
CustomWorldLibraryResponse,
PlatformBrowseHistoryBatchSyncRequest,
PlatformBrowseHistoryEntry,
PlatformBrowseHistoryResponse,
PlatformBrowseHistoryWriteEntry,
ProfileDashboardSummary,
ProfilePlayStatsResponse,
ProfileWalletLedgerResponse,
RuntimeSettings,
} from '../../packages/shared/src/contracts/runtime';
import type {
SavedGameSnapshotInput,
} from '../persistence/gameSaveStorage';
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';
import { type ApiRetryOptions, requestJson } from './apiClient';
const RUNTIME_API_BASE = '/api/runtime';
const RUNTIME_READ_RETRY: ApiRetryOptions = {
@@ -58,6 +61,28 @@ function requestRuntimeJson<T>(
);
}
function requestProfileJson<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>(
`/api/profile${path}`,
{
...init,
signal: options.signal,
},
fallbackMessage,
{ retry },
);
}
export async function getSaveSnapshot(options: RuntimeRequestOptions = {}) {
const snapshot = await requestRuntimeJson<HydratedSavedGameSnapshot | null>(
'/save/snapshot',
@@ -105,6 +130,35 @@ export async function getSettings(options: RuntimeRequestOptions = {}) {
);
}
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 putSettings(
settings: RuntimeSettings,
options: RuntimeRequestOptions = {},
@@ -121,8 +175,12 @@ export async function putSettings(
);
}
export async function listCustomWorldLibrary(options: RuntimeRequestOptions = {}) {
const response = await requestRuntimeJson<CustomWorldLibraryResponse<CustomWorldProfile>>(
export async function listCustomWorldLibrary(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<
CustomWorldLibraryResponse<CustomWorldProfile>
>(
'/custom-world-library',
{ method: 'GET' },
'读取自定义世界库失败',
@@ -132,7 +190,9 @@ export async function listCustomWorldLibrary(options: RuntimeRequestOptions = {}
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function listCustomWorldWorks(options: RuntimeRequestOptions = {}) {
export async function listCustomWorldWorks(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<ListCustomWorldWorksResponse>(
'/custom-world/works',
{ method: 'GET' },
@@ -147,7 +207,9 @@ export async function upsertCustomWorldProfile(
profile: CustomWorldProfile,
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<CustomWorldLibraryMutationResponse<CustomWorldProfile>>(
const response = await requestRuntimeJson<
CustomWorldLibraryMutationResponse<CustomWorldProfile>
>(
`/custom-world-library/${encodeURIComponent(profile.id)}`,
{
method: 'PUT',
@@ -170,7 +232,9 @@ export async function deleteCustomWorldProfile(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<CustomWorldLibraryResponse<CustomWorldProfile>>(
const response = await requestRuntimeJson<
CustomWorldLibraryResponse<CustomWorldProfile>
>(
`/custom-world-library/${encodeURIComponent(profileId)}`,
{ method: 'DELETE' },
'删除自定义世界失败',
@@ -184,7 +248,9 @@ export async function publishCustomWorldProfile(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<CustomWorldLibraryMutationResponse<CustomWorldProfile>>(
const response = await requestRuntimeJson<
CustomWorldLibraryMutationResponse<CustomWorldProfile>
>(
`/custom-world-library/${encodeURIComponent(profileId)}/publish`,
{ method: 'POST' },
'发布自定义世界失败',
@@ -201,7 +267,9 @@ export async function unpublishCustomWorldProfile(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<CustomWorldLibraryMutationResponse<CustomWorldProfile>>(
const response = await requestRuntimeJson<
CustomWorldLibraryMutationResponse<CustomWorldProfile>
>(
`/custom-world-library/${encodeURIComponent(profileId)}/unpublish`,
{ method: 'POST' },
'下架自定义世界失败',
@@ -214,7 +282,9 @@ export async function unpublishCustomWorldProfile(
};
}
export async function listCustomWorldGallery(options: RuntimeRequestOptions = {}) {
export async function listCustomWorldGallery(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<CustomWorldGalleryResponse>(
'/custom-world-gallery',
{ method: 'GET' },
@@ -230,7 +300,9 @@ export async function getCustomWorldGalleryDetail(
profileId: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<CustomWorldGalleryDetailResponse<CustomWorldProfile>>(
const response = await requestRuntimeJson<
CustomWorldGalleryDetailResponse<CustomWorldProfile>
>(
`/custom-world-gallery/${encodeURIComponent(ownerUserId)}/${encodeURIComponent(profileId)}`,
{ method: 'GET' },
'读取作品详情失败',
@@ -240,12 +312,79 @@ export async function getCustomWorldGalleryDetail(
return response.entry;
}
export async function listProfileBrowseHistory(
options: RuntimeRequestOptions = {},
) {
const response = await requestProfileJson<PlatformBrowseHistoryResponse>(
'/browse-history',
{ method: 'GET' },
'读取浏览历史失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function upsertProfileBrowseHistory(
entry: PlatformBrowseHistoryWriteEntry,
options: RuntimeRequestOptions = {},
) {
const response = await requestProfileJson<PlatformBrowseHistoryResponse>(
'/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 requestProfileJson<PlatformBrowseHistoryResponse>(
'/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 requestProfileJson<PlatformBrowseHistoryResponse>(
'/browse-history',
{ method: 'DELETE' },
'清空浏览历史失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export const runtimeStorageClient = {
getSaveSnapshot,
putSaveSnapshot,
deleteSaveSnapshot,
getSettings,
putSettings,
getProfileDashboard,
getProfileWalletLedger,
getProfilePlayStats,
listCustomWorldLibrary,
listCustomWorldWorks,
upsertCustomWorldProfile,
@@ -254,6 +393,11 @@ export const runtimeStorageClient = {
unpublishCustomWorldProfile,
listCustomWorldGallery,
getCustomWorldGalleryDetail,
listProfileBrowseHistory,
upsertProfileBrowseHistory,
syncProfileBrowseHistory,
clearProfileBrowseHistory,
};
export type { CustomWorldLibraryEntry };
export type { PlatformBrowseHistoryEntry };