{games.map((game) => {
const active = fork.activeGameId === game.id;
+ const coverUrl = coverUrls[game.id];
return (
- {(game.title || game.id).slice(0, 1)}
+ {/*
+ 封面:优先用有界 data URL(AGC 不让渲染层直连签名地址)。没有封面、换签失败
+ 或超限时保持首字占位——**降级必须保留**,否则列表会出现破图。
+ */}
+ {coverUrl ? (
+
+ ) : (
+
+ {(game.title || game.id).slice(0, 1)}
+
+ )}
diff --git a/apps/ai-game-creator-shell/tests/coCreation.test.tsx b/apps/ai-game-creator-shell/tests/coCreation.test.tsx
index 2aedab6cf..e3453d6c1 100644
--- a/apps/ai-game-creator-shell/tests/coCreation.test.tsx
+++ b/apps/ai-game-creator-shell/tests/coCreation.test.tsx
@@ -272,6 +272,60 @@ describe('共创页', () => {
expect(await screen.findByText('开放共创的作品')).not.toBeNull();
});
+ it('有封面时用有界 data URL 渲染,换签失败或没有封面则保留首字占位', async () => {
+ const calls = installInvoke((command, args) => {
+ if (command === CATALOG_COMMAND) {
+ return {
+ games: [
+ {
+ ...catalogGame('game_with_cover', '有封面的作品', 'full'),
+ coverObjectKey: 'game-distribution/cover/open.png',
+ },
+ {
+ ...catalogGame('game_broken_cover', '换签失败的作品', 'full'),
+ coverObjectKey: 'game-distribution/cover/broken.png',
+ },
+ catalogGame('game_no_cover', '没有封面的作品', 'full'),
+ ],
+ nextCursor: null,
+ };
+ }
+ if (command === 'read_public_game_cover_preview') {
+ if ((args as { objectKey?: string })?.objectKey?.includes('broken')) {
+ throw new Error('换签失败');
+ }
+ return 'data:image/png;base64,AAAA';
+ }
+ throw new Error(`unexpected invoke ${command}`);
+ });
+ const { container } = render();
+
+ // 有封面的那张走
,src 是命令返回的 data URL。
+ const image = await screen.findByRole('img', {
+ name: '有封面的作品 封面',
+ });
+ expect(image.getAttribute('src')).toBe('data:image/png;base64,AAAA');
+ // 失败与缺封面都保留首字占位:不出现破图(即没有第二张
)。
+ expect(container.querySelectorAll('img')).toHaveLength(1);
+ expect(
+ screen.queryByRole('img', { name: '换签失败的作品 封面' }),
+ ).toBeNull();
+ expect(
+ screen.queryByRole('img', { name: '没有封面的作品 封面' }),
+ ).toBeNull();
+ // 没有对象键的那张根本不该发换签请求。
+ expect(
+ calls.filter(
+ (call) =>
+ call.command === 'read_public_game_cover_preview' &&
+ (call.args as { objectKey?: string })?.objectKey === undefined,
+ ),
+ ).toHaveLength(0);
+ expect(
+ calls.filter((call) => call.command === 'read_public_game_cover_preview'),
+ ).toHaveLength(2);
+ });
+
it('深链只预填输入框,不自动开始下载', async () => {
const calls = installInvoke((command) => {
if (command === CATALOG_COMMAND) {