diff --git a/apps/ai-game-creator-shell/src/services/assetDirectUpload.ts b/apps/ai-game-creator-shell/src/services/assetDirectUpload.ts index 9ab0e5597..d0b00b106 100644 --- a/apps/ai-game-creator-shell/src/services/assetDirectUpload.ts +++ b/apps/ai-game-creator-shell/src/services/assetDirectUpload.ts @@ -121,10 +121,22 @@ export async function uploadPlatformMediaAsset(args: { const uploadHost = resolvePlatformAssetUploadUrl(ticket.upload.host); const uploadFetch = args.fetchImpl ?? tauriHttpFetch; - const uploadResponse = await uploadFetch(uploadHost, { - method: 'POST', - body: buildDirectUploadFormData(ticket.upload, args.file), - }); + let uploadResponse: Response; + try { + uploadResponse = await uploadFetch(uploadHost, { + method: 'POST', + body: buildDirectUploadFormData(ticket.upload, args.file), + }); + } catch (error) { + // Tauri http 插件在目标不在能力作用域、网络不可达或请求被取消时直接抛错; + // 统一转成可操作文案,避免把英文插件错误原样暴露给作者。 + const detail = error instanceof Error ? error.message.trim() : ''; + throw new Error( + `上传素材失败:无法访问素材存储,请检查网络后重试${ + detail ? `(${detail.slice(0, 120)})` : '' + }`, + ); + } if (!uploadResponse.ok) { throw new Error( `上传素材到对象存储失败(HTTP ${uploadResponse.status}),请重试`, diff --git a/apps/ai-game-creator-shell/tests/assetDirectUpload.test.ts b/apps/ai-game-creator-shell/tests/assetDirectUpload.test.ts index d2c2a4946..5d9d59f3e 100644 --- a/apps/ai-game-creator-shell/tests/assetDirectUpload.test.ts +++ b/apps/ai-game-creator-shell/tests/assetDirectUpload.test.ts @@ -160,6 +160,27 @@ test('对象存储拒绝上传时给出可重试文案', async () => { expect(requestClientApiMock).toHaveBeenCalledTimes(1); }); +test('直传网络/作用域错误转成可操作文案', async () => { + const uploadFetch = vi.fn(async () => { + throw new Error('url not allowed on the configured scope'); + }); + + await expect( + uploadPlatformMediaAsset({ + file: buildCoverFile(), + assetKind: 'game_distribution_cover', + pathSegments: ['game-distribution', 'cover', '42'], + entityId: 'game-distribution-cover', + fetchImpl: uploadFetch, + }), + ).rejects.toThrow( + '上传素材失败:无法访问素材存储,请检查网络后重试(url not allowed on the configured scope)', + ); + + // 直传抛错时不得继续登记素材。 + expect(requestClientApiMock).toHaveBeenCalledTimes(1); +}); + test('本地回环与阿里云 OSS 之外的主机一律拒绝', () => { expect(resolvePlatformAssetUploadUrl('http://127.0.0.1:9000/bucket')).toBe( 'http://127.0.0.1:9000/bucket', diff --git a/docs/project-memory/plans/【实施计划】游戏分发阶段A领域合同-2026-09-19.md b/docs/project-memory/plans/【实施计划】游戏分发阶段A领域合同-2026-09-19.md index a174b0ff2..a6a370b05 100644 --- a/docs/project-memory/plans/【实施计划】游戏分发阶段A领域合同-2026-09-19.md +++ b/docs/project-memory/plans/【实施计划】游戏分发阶段A领域合同-2026-09-19.md @@ -100,6 +100,7 @@ - 真实浏览器展示验证(同一栈,headless Chromium `390x844` 与 `1280x900`):`/games` 卡片渲染真实封面签名地址(无占位回退),`/games/detail?id=…` hero 显示封面、缩略图条渲染「封面 + 2 张截图」,点击第 3 张后 `aria-pressed` 与 hero 图片同步切换;移动端布局正常。截图存于 `~/data/tmp/gd3/`(不入库)。 - 网页发布页真实上传验证(同一栈 + headless Chromium):在 `/games/publish` 里用作者会话填写资料、选择真实 PNG 封面与 ZIP 并提交,页面返回「已提交审核」;随后 `my-games` 显示该游戏 `pending_review`,版本回读的 `frozenMetadata.coverAssetId` / `coverObjectKey` 正是本次浏览器上传的素材,证明网页端封面直传(凭证 → OSS → confirm)与冻结链路真实可用。 - 发布资料入口交互打磨:原生 file 控件在上传后清空 value 时会显示「未选择任何文件」,与「已选择」提示互相矛盾;网页发布页与 AGC 面板都改成透明 input 覆盖自定义胶囊按钮(点击命中原生控件,文案显示「选择/更换封面图片」「添加截图」),并在验证中用 `elementFromPoint` 确认点击命中的是 `input[type=file]`。 +- AGC 直传能力作用域静态验证:用 `tauri-plugin-http` 同一套 `urlpattern` 解析逻辑验证 `capabilities/main.json` 新增的 `https://*.aliyuncs.com/*` 命中 dev 桶(`xushi-dev.oss-cn-beijing.aliyuncs.com/…`)与生产桶(`genarrative-assets.oss-cn-shanghai.aliyuncs.com/…`)、拒绝 `evil.example.com`;直传失败(含作用域拒绝/网络不可达)统一转成中文可操作文案,并有单测覆盖。桌面端真实执行仍需在装有 AGC 的机器上跑一次。 - 创建游戏同步校验素材归属:此前 `POST /api/game-distribution/games` 只校验「封面必填 / 截图 ≤6 / ID 非空」,素材是否存在与是否属于当前作者要等到创建版本才失败,游戏行会先落一个无效素材 ID;现在创建游戏与创建版本都走 `resolve_owned_game_media`,不存在返回 400、他人素材返回 403。 ## 尚未完成