diff --git a/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs b/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs index 92a37e851..f8e86833f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs @@ -446,6 +446,9 @@ fn metadata_digest( hasher.update(&bytes); hasher.update(price_mud_points.to_le_bytes()); for image in media.cover.iter().chain(media.screenshots.iter()) { + // 逐张带长度前缀:图片之间没有分隔符,只串裸字节会让「封面 [1,2] + 截图 [3]」与 + // 「封面 [1] + 截图 [2,3]」撞成同一摘要,本地幂等账本就会把改过的图当成重放。 + hasher.update((image.bytes.len() as u64).to_le_bytes()); hasher.update(&image.bytes); } Ok(format!("{:x}", hasher.finalize())) @@ -1672,6 +1675,31 @@ mod tests { metadata_digest(&metadata, 0, &media).expect("changed"), first ); + // 分片边界也要进摘要:两组图裸字节串起来相同,但封面/截图的切分不同。 + let split_a = PublishMedia { + cover: Some(DraftImage { + bytes: vec![1, 2], + content_type: "image/png", + }), + screenshots: vec![DraftImage { + bytes: vec![3], + content_type: "image/png", + }], + }; + let split_b = PublishMedia { + cover: Some(DraftImage { + bytes: vec![1], + content_type: "image/png", + }), + screenshots: vec![DraftImage { + bytes: vec![2, 3], + content_type: "image/png", + }], + }; + assert_ne!( + metadata_digest(&metadata, 0, &split_a).expect("split a"), + metadata_digest(&metadata, 0, &split_b).expect("split b") + ); } #[test]