diff --git a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx index 54eaa2db1..23524532f 100644 --- a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx +++ b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx @@ -96,7 +96,7 @@ function buildPublishProjectContext(manifest: GameCreationAppManifest) { .join('、'); const assetSummary = (manifest.assets ?? []) .slice(0, 24) - .map((asset) => `${asset.kind}:${asset.localPath}`) + .map((asset) => `${asset.kind}:${contextSafeAssetLabel(asset.localPath)}`) .join('、'); const versionSummary = (manifest.versions ?? []) .slice(-3) @@ -116,6 +116,19 @@ function buildPublishProjectContext(manifest: GameCreationAppManifest) { .slice(0, 6_000); } +/** + * 送进模型的项目上下文必须脱敏:素材只带文件名,不带目录片段。 + * + * 素材清单里的 `localPath` 正常是项目内相对路径,但登记来源不受本地约束时也可能是 + * 绝对路径(含盘符或用户目录)——`C:\Users\<用户名>\...\assets\x.png` 这种一旦进提示词 + * 就等于把本地目录结构发给了平台模型。这里统一取最后一段并限长。 + */ +function contextSafeAssetLabel(value: string) { + const normalized = value.trim().replaceAll('\\', '/'); + const base = normalized.split('/').filter(Boolean).pop() ?? ''; + return base.slice(0, 80); +} + function buildCoverGenerationPrompt( manifest: GameCreationAppManifest, summary: string, diff --git a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx b/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx index c4e34ede5..ddc3458b8 100644 --- a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx @@ -176,6 +176,42 @@ describe('GameDistributionPublishPanel', () => { ).not.toBeNull(); }); + test('发布资料建议只带脱敏且有界的项目上下文', async () => { + const assets = Array.from({ length: 40 }, (_, index) => ({ + kind: 'image', + localPath: `C:\\Users\\author\\secret-project\\assets\\shot-${index}.png`, + name: `shot-${index}.png`, + })); + installTauriInvoke(async () => undefined); + renderPanel({ + manifest: { + ...MANIFEST, + assets, + tasks: Array.from({ length: 24 }, (_, index) => ({ + title: `任务 ${index}`, + status: 'completed', + })), + // 超长版本说明用于验证上下文真的被截到上限,而不是只在短样例里碰巧合规。 + versions: Array.from({ length: 6 }, () => ({ + editPrompt: '长'.repeat(3_000), + })), + } as unknown as GameCreationAppManifest, + }); + + await waitFor(() => { + expect(suggestGameDistributionPublishMetadata).toHaveBeenCalled(); + }); + const [request] = vi.mocked(suggestGameDistributionPublishMetadata).mock + .calls[0]; + const context = String(request.context ?? ''); + expect(context.length).toBeGreaterThanOrEqual(5_000); + expect(context.length).toBeLessThanOrEqual(6_000); + expect(context).toContain('shot-0.png'); + // 脱敏:绝对路径里的用户目录与项目目录不得进入提示词。 + expect(context).not.toContain('C:\\Users'); + expect(context).not.toContain('secret-project'); + }); + test('确认后基于项目上下文生成封面并作为发布素材', async () => { vi.mocked(readGameCoverGenerationPrice).mockResolvedValueOnce(5); installTauriInvoke(async () => undefined); diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index b4785808f..4ed1582c1 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -96,7 +96,7 @@ ### 行为与验收 - [ ] AGC 从已构建 dist 生成根入口为 `index.html` 的真实包,一次提交动作完成检查、资料确认、上传和送审;状态及失败原因与服务端回读一致。 -- [ ] AGC 发布面板隐藏发行包技术摘要;打开时基于有界、脱敏的项目上下文免费生成一句话简介与白名单分类,失败保留本地兜底且不阻断发布;作者始终可以直接编辑生成结果。 +- [ ] AGC 发布面板隐藏发行包技术摘要;打开时基于有界、脱敏的项目上下文免费生成一句话简介与白名单分类,失败保留本地兜底且不阻断发布;作者始终可以直接编辑生成结果。(2026-09-29 补客户端侧口径:「脱敏」此前只在服务端有界——`buildPublishProjectContext()` 会把素材的 `localPath` 原样写进提示词,登记来源不受本地约束时绝对路径(含用户目录)就会外发。现在素材只带**文件名**(`contextSafeAssetLabel` 取最后一段并限长 80),上下文整体仍截到 6,000 字符;`gameDistributionPublishPanel.test.tsx` 新增「发布资料建议只带脱敏且有界的项目上下文」并用 40 个绝对路径素材 + 超长版本说明验证。变异验证:把该处改回 `asset.localPath` 原样拼接后,用例立即以 `expected ... to not include 'C:\Users'` 变红。GUI 打开面板、真实 Provider 生成与失败兜底仍待客户端验收。) - [ ] AGC 发布封面支持基于项目上下文生成,复用现役图片生成与泥点扣费链路;生成结果登记为当前账号平台素材后自动作为 `coverAssetId`,不二次上传。 - [x] 网页可选 ZIP、提交封面和必需资料,进入相同上传/校验/审核流程;任一客户端可以查看同账号游戏状态,更新沿用相同 `gameId`。 - [x] 上传中断、双击、登录失效、窗口关闭后恢复原操作;换账号不能恢复前账号私有状态;待审不能显示为已发布。