修复游戏封面预览并统一发布灰度说明
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m53s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m41s
Project CI / Frontend tests (pull_request) Failing after 1m5s
Project CI / Repository checks (pull_request) Failing after 23s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m48s
Project CI / Native shell tests (pull_request) Successful in 5m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m6s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m53s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m41s
Project CI / Frontend tests (pull_request) Failing after 1m5s
Project CI / Repository checks (pull_request) Failing after 23s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m48s
Project CI / Native shell tests (pull_request) Successful in 5m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m6s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m9s
封面生成结果经鉴权 read-url 转为安全 data URL 预览,失败时不阻断已登记素材发布。 统一后台、前端与后端对 game-distribution:publish 未配置和关闭状态的说明。
This commit is contained in:
@@ -64,7 +64,7 @@ const FIXED_GATE_TARGETS: GateTargetOption[] = [
|
||||
key: 'game-distribution:publish',
|
||||
label: '游戏发布',
|
||||
description:
|
||||
'游戏发布入口灰度:未配置或关闭时对已登录作者默认开放,开启后只放行白名单 / 灰度命中',
|
||||
'未配置或关闭时不开放,启用后按白名单 / 用户标签 / 灰度比例放行',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ use crate::http_client::agc_main_site_client_builder;
|
||||
use crate::platform_session::{
|
||||
current_platform_session, validate_platform_session_identity, PlatformSessionSnapshot,
|
||||
};
|
||||
use base64::Engine;
|
||||
|
||||
use reqwest::header::{HeaderName, HeaderValue, CONTENT_TYPE};
|
||||
use reqwest::{Method, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,6 +35,8 @@ const HTTP_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const COVER_QUEUE_POLL_INTERVAL: Duration = Duration::from_millis(1_600);
|
||||
const COVER_QUEUE_TIMEOUT: Duration = Duration::from_secs(20 * 60);
|
||||
const COVER_QUEUE_MAX_POLLS: usize = 800;
|
||||
const COVER_PREVIEW_MAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
|
||||
const PACKAGE_UPLOAD_PROGRESS_EVENT: &str = "game-package-upload-progress";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
@@ -62,6 +66,8 @@ struct CoverGenerationPayload {
|
||||
#[serde(default)]
|
||||
image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
asset_object_id: Option<String>,
|
||||
#[serde(default)]
|
||||
asset: Option<CoverAssetPayload>,
|
||||
@@ -287,6 +293,48 @@ fn read_cover_asset_id(payload: &CoverGenerationPayload) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn cover_preview_data_url(media_type: &str, bytes: &[u8]) -> Result<String, String> {
|
||||
let media_type = media_type
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap_or(media_type)
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if !media_type.starts_with("image/") {
|
||||
return Err("游戏封面预览不是受支持的图片类型".to_string());
|
||||
}
|
||||
if bytes.is_empty() || bytes.len() > COVER_PREVIEW_MAX_BYTES {
|
||||
return Err("游戏封面预览超过安全大小限制".to_string());
|
||||
}
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
Ok(format!("data:{media_type};base64,{encoded}"))
|
||||
}
|
||||
|
||||
async fn resolve_cover_preview_data_url(
|
||||
snapshot: &PlatformSessionSnapshot,
|
||||
payload: &CoverGenerationPayload,
|
||||
) -> Result<String, String> {
|
||||
current_scoped_session(snapshot)?;
|
||||
let client = build_client()?;
|
||||
let source = json!({
|
||||
"objectKey": payload.object_key.as_deref(),
|
||||
"imageSrc": payload.image_src.as_deref(),
|
||||
});
|
||||
let download = crate::assets::resolve_canvas_resource_download_with_limit_route_and_fence(
|
||||
&client,
|
||||
snapshot.api_base_url.as_str(),
|
||||
snapshot.access_token.as_str(),
|
||||
&source,
|
||||
COVER_PREVIEW_MAX_BYTES,
|
||||
"/api/assets/read-url",
|
||||
|| Ok(()),
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| "游戏封面预览内容为空".to_string())?;
|
||||
current_scoped_session(snapshot)?;
|
||||
cover_preview_data_url(&download.media_type, &download.bytes)
|
||||
}
|
||||
|
||||
async fn resolve_cover_queue(
|
||||
client: &reqwest::Client,
|
||||
snapshot: &PlatformSessionSnapshot,
|
||||
@@ -349,6 +397,7 @@ async fn resolve_cover_queue(
|
||||
.map_err(|error| format!("封面生成结果格式无效:{error}"))?;
|
||||
return Ok(CoverGenerationPayload {
|
||||
image_src: completed.image_src.or(initial.image_src),
|
||||
object_key: completed.object_key.or(initial.object_key),
|
||||
asset_object_id: completed.asset_object_id.or(initial.asset_object_id),
|
||||
asset: completed.asset.or(initial.asset),
|
||||
task_id: completed.task_id.or(initial.task_id),
|
||||
@@ -462,9 +511,16 @@ pub(crate) async fn generate_game_distribution_cover(
|
||||
let payload = resolve_cover_queue(&client, &snapshot, initial).await?;
|
||||
let asset_object_id =
|
||||
read_cover_asset_id(&payload).ok_or_else(|| "生成游戏封面未返回平台素材 ID".to_string())?;
|
||||
let preview_url = match resolve_cover_preview_data_url(&snapshot, &payload).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
app_log!("游戏封面生成成功但预览下载失败:{error}");
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
Ok(GameDistributionCoverGenerationResult {
|
||||
asset_object_id,
|
||||
preview_url: payload.image_src.unwrap_or_default().trim().to_string(),
|
||||
preview_url,
|
||||
task_id: payload.task_id.unwrap_or_default().trim().to_string(),
|
||||
model: payload
|
||||
.model
|
||||
@@ -811,4 +867,14 @@ mod tests {
|
||||
Some("asset-from-top-level")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn cover_preview_data_url_accepts_images_and_rejects_invalid_payloads() {
|
||||
assert_eq!(
|
||||
cover_preview_data_url("image/png; charset=binary", &[0, 1, 2])
|
||||
.expect("image data URL"),
|
||||
"data:image/png;base64,AAEC"
|
||||
);
|
||||
assert!(cover_preview_data_url("text/plain", &[0, 1, 2]).is_err());
|
||||
assert!(cover_preview_data_url("image/png", &[]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ AGC 发布面板完成三项收敛:
|
||||
## 6. 本次缺陷修复记录
|
||||
|
||||
- 2026-09-23:AGC 生成游戏封面请求补充 `generationInputs.source = "ai-game-creator-client"`。队列 worker 依据该来源选择 `GameCreatorResourceEditor` 结果契约;未标记来源时会按 `Standard` 紧凑化并省略 `result`,导致生成完成后无法回传 `assetObjectId`。对应前端定向测试已锁定请求字段与平台素材 ID 回填链路。
|
||||
- 2026-10-02:生成结果中的 `imageSrc/objectKey` 是私有素材引用,不再直接交给 WebView `<img>`;AGC Rust 发布 facade 经鉴权 `read-url` 有界下载为 `data:` 图片预览,预览失败不阻断已登记素材发布。
|
||||
|
||||
## 7. 本轮核对(2026-09-28)
|
||||
|
||||
六项交付结果都在合并后的工作树里,并有本轮实测:
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::{
|
||||
pub struct FrontendRuntimeConfigResponse {
|
||||
pub image_editor_agent_sidebar_enabled: bool,
|
||||
pub agc_template_library_enabled: bool,
|
||||
/// 游戏发布灰度:运营未配置 `game-distribution:publish` 时对已登录作者默认开放,
|
||||
/// 显式收紧后只有白名单/灰度命中的作者拿到 `true`;匿名一律 `false`(发布必须先登录)。
|
||||
/// 游戏发布灰度:未配置 `game-distribution:publish` 或 `enabled=false` 时不开放;
|
||||
/// 显式配置后按 enabled、白名单、标签和灰度比例判定,匿名一律 `false`(发布必须先登录)。
|
||||
pub game_distribution_publish_enabled: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn creation_entry_feature_gate_key(creation_type_id: &str) -> String {
|
||||
|
||||
pub const IMAGE_EDITOR_AGENT_SIDEBAR_GATE_KEY: &str = "image-editor:agent-sidebar";
|
||||
pub const AGC_TEMPLATE_LIBRARY_GATE_KEY: &str = "agc:template-library";
|
||||
/// 游戏分发发布开关:`gate 行缺失`或 `enabled=false` 都表示**未开放**(灰度默认关闭);
|
||||
/// 游戏分发发布开关:gate 行缺失或 `enabled=false` 都表示**未开放**;
|
||||
/// 只有 `enabled=true` 且白名单 / 灰度比例 / 用户标签命中时才允许发布。
|
||||
pub const GAME_DISTRIBUTION_PUBLISH_GATE_KEY: &str = "game-distribution:publish";
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ export type FrontendRuntimeConfig = {
|
||||
imageEditorAgentSidebarEnabled: boolean;
|
||||
agcTemplateLibraryEnabled: boolean;
|
||||
/**
|
||||
* 游戏发布灰度开关:后端未配置 `game-distribution:publish` 时对已登录作者默认开放,
|
||||
* 显式收紧后只有白名单/灰度命中的作者为 `true`,匿名恒为 `false`。
|
||||
* 游戏发布灰度:未配置 `game-distribution:publish` 或 `enabled=false` 时不开放;
|
||||
* 显式配置后只有白名单/灰度命中的作者为 `true`,匿名恒为 `false`。
|
||||
*/
|
||||
gameDistributionPublishEnabled: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user