fix: stabilize rpg publish and launch

This commit is contained in:
kdletters
2026-05-21 20:20:06 +08:00
parent 224a26d318
commit a9d23a8a44
14 changed files with 614 additions and 82 deletions

View File

@@ -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: {

View File

@@ -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 = {