fix: stabilize rpg publish and launch
This commit is contained in:
@@ -79,6 +79,72 @@ describe('rpgEntryLibraryClient world library routes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes detail profiles before runtime launch consumes them', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
entry: {
|
||||
ownerUserId: 'owner-1',
|
||||
profileId: 'profile-1',
|
||||
publicWorkCode: 'CW-1',
|
||||
authorPublicUserCode: 'U-1',
|
||||
profile: {
|
||||
id: 'profile-1',
|
||||
name: '旧数据世界',
|
||||
summary: '只有摘要字段的旧 profile。',
|
||||
},
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-05-21T00:00:00.000Z',
|
||||
updatedAt: '2026-05-21T00:00:00.000Z',
|
||||
authorDisplayName: '作者',
|
||||
worldName: '旧数据世界',
|
||||
subtitle: '旧数据',
|
||||
summaryText: '只有摘要字段的旧 profile。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'martial',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
|
||||
|
||||
expect(Array.isArray(entry.profile.playableNpcs)).toBe(true);
|
||||
expect(Array.isArray(entry.profile.storyNpcs)).toBe(true);
|
||||
expect(Array.isArray(entry.profile.landmarks)).toBe(true);
|
||||
expect(entry.profile.attributeSchema.schemaVersion).toBe(1);
|
||||
});
|
||||
|
||||
it('falls back to entry summary when old detail profile cannot be normalized', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
entry: {
|
||||
ownerUserId: 'owner-1',
|
||||
profileId: 'profile-1',
|
||||
publicWorkCode: 'CW-1',
|
||||
authorPublicUserCode: 'U-1',
|
||||
profile: {
|
||||
id: 'profile-1',
|
||||
summary: '缺少 name 的旧 profile。',
|
||||
},
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-05-21T00:00:00.000Z',
|
||||
updatedAt: '2026-05-21T00:00:00.000Z',
|
||||
authorDisplayName: '作者',
|
||||
worldName: '摘要兜底世界',
|
||||
subtitle: '旧数据',
|
||||
summaryText: '缺少 name 的旧 profile。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'martial',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
|
||||
|
||||
expect(entry.profile.id).toBe('profile-1');
|
||||
expect(entry.profile.name).toBe('摘要兜底世界');
|
||||
expect(Array.isArray(entry.profile.playableNpcs)).toBe(true);
|
||||
});
|
||||
|
||||
it('reads owned library detail from the runtime entry route', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
entry: {
|
||||
|
||||
@@ -7,13 +7,62 @@ import {
|
||||
import type {
|
||||
CustomWorldGalleryDetailResponse,
|
||||
CustomWorldGalleryResponse,
|
||||
CustomWorldLibraryEntry,
|
||||
CustomWorldLibraryMutationResponse,
|
||||
CustomWorldLibraryResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
export type { RuntimeRequestOptions };
|
||||
|
||||
type RpgEntryWorldEntry = CustomWorldLibraryEntry<CustomWorldProfile>;
|
||||
type RpgEntryWorldMutationResponse =
|
||||
CustomWorldLibraryMutationResponse<CustomWorldProfile>;
|
||||
|
||||
function normalizeRpgEntryWorldProfile(entry: RpgEntryWorldEntry) {
|
||||
const rawProfile =
|
||||
entry.profile && typeof entry.profile === 'object' ? entry.profile : {};
|
||||
const fallbackProfile = {
|
||||
id: entry.profileId,
|
||||
name: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summary: entry.summaryText,
|
||||
settingText: entry.summaryText || entry.worldName,
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [],
|
||||
};
|
||||
const normalizedProfile =
|
||||
normalizeCustomWorldProfileRecord({
|
||||
...fallbackProfile,
|
||||
...rawProfile,
|
||||
}) ?? normalizeCustomWorldProfileRecord(fallbackProfile);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
profile: normalizedProfile ?? entry.profile,
|
||||
} as RpgEntryWorldEntry;
|
||||
}
|
||||
|
||||
function normalizeRpgEntryWorldEntries(
|
||||
entries: RpgEntryWorldEntry[] | null | undefined,
|
||||
) {
|
||||
return Array.isArray(entries)
|
||||
? entries.map((entry) => normalizeRpgEntryWorldProfile(entry))
|
||||
: [];
|
||||
}
|
||||
|
||||
function normalizeRpgEntryWorldMutationResponse(
|
||||
response: RpgEntryWorldMutationResponse,
|
||||
) {
|
||||
return {
|
||||
entry: normalizeRpgEntryWorldProfile(response.entry),
|
||||
entries: normalizeRpgEntryWorldEntries(response.entries),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* RPG 入口世界库 client 的真实实现。
|
||||
* 第三批收口后,平台首页/详情页开始游戏链直接走 rpg-entry 域请求,不再反向穿旧 storageService 兼容层。
|
||||
@@ -33,7 +82,7 @@ export async function listRpgEntryWorldLibrary(
|
||||
},
|
||||
);
|
||||
|
||||
return Array.isArray(response?.entries) ? response.entries : [];
|
||||
return normalizeRpgEntryWorldEntries(response?.entries);
|
||||
}
|
||||
|
||||
export async function listRpgEntryWorldGallery(
|
||||
@@ -63,7 +112,7 @@ export async function getRpgEntryWorldGalleryDetail(
|
||||
options,
|
||||
);
|
||||
|
||||
return response.entry;
|
||||
return normalizeRpgEntryWorldProfile(response.entry);
|
||||
}
|
||||
|
||||
export async function getRpgEntryWorldGalleryDetailByCode(
|
||||
@@ -79,7 +128,7 @@ export async function getRpgEntryWorldGalleryDetailByCode(
|
||||
options,
|
||||
);
|
||||
|
||||
return response.entry;
|
||||
return normalizeRpgEntryWorldProfile(response.entry);
|
||||
}
|
||||
|
||||
export async function remixRpgEntryWorldGallery(
|
||||
@@ -96,10 +145,7 @@ export async function remixRpgEntryWorldGallery(
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
entry: response.entry,
|
||||
entries: Array.isArray(response?.entries) ? response.entries : [],
|
||||
};
|
||||
return normalizeRpgEntryWorldMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function recordRpgEntryWorldGalleryPlay(
|
||||
@@ -116,7 +162,7 @@ export async function recordRpgEntryWorldGalleryPlay(
|
||||
options,
|
||||
);
|
||||
|
||||
return response.entry;
|
||||
return normalizeRpgEntryWorldProfile(response.entry);
|
||||
}
|
||||
|
||||
export async function likeRpgEntryWorldGallery(
|
||||
@@ -133,7 +179,7 @@ export async function likeRpgEntryWorldGallery(
|
||||
options,
|
||||
);
|
||||
|
||||
return response.entry;
|
||||
return normalizeRpgEntryWorldProfile(response.entry);
|
||||
}
|
||||
|
||||
export async function getRpgEntryWorldLibraryDetail(
|
||||
@@ -149,7 +195,7 @@ export async function getRpgEntryWorldLibraryDetail(
|
||||
options,
|
||||
);
|
||||
|
||||
return response.entry;
|
||||
return normalizeRpgEntryWorldProfile(response.entry);
|
||||
}
|
||||
|
||||
export async function upsertRpgEntryWorldProfile(
|
||||
@@ -171,10 +217,7 @@ export async function upsertRpgEntryWorldProfile(
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
entry: response.entry,
|
||||
entries: Array.isArray(response?.entries) ? response.entries : [],
|
||||
};
|
||||
return normalizeRpgEntryWorldMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function deleteRpgEntryWorldProfile(
|
||||
@@ -190,7 +233,7 @@ export async function deleteRpgEntryWorldProfile(
|
||||
options,
|
||||
);
|
||||
|
||||
return Array.isArray(response?.entries) ? response.entries : [];
|
||||
return normalizeRpgEntryWorldEntries(response?.entries);
|
||||
}
|
||||
|
||||
export async function publishRpgEntryWorldProfile(
|
||||
@@ -206,10 +249,7 @@ export async function publishRpgEntryWorldProfile(
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
entry: response.entry,
|
||||
entries: Array.isArray(response?.entries) ? response.entries : [],
|
||||
};
|
||||
return normalizeRpgEntryWorldMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function unpublishRpgEntryWorldProfile(
|
||||
@@ -225,10 +265,7 @@ export async function unpublishRpgEntryWorldProfile(
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
entry: response.entry,
|
||||
entries: Array.isArray(response?.entries) ? response.entries : [],
|
||||
};
|
||||
return normalizeRpgEntryWorldMutationResponse(response);
|
||||
}
|
||||
|
||||
export const rpgEntryLibraryClient = {
|
||||
|
||||
Reference in New Issue
Block a user