新增 external v1 游戏场景生成路由并迁移 AGC 美术包背景阶段
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
- api-server 新增 POST /api/external/v1/editor/scenes/generations,复用 editor:image-generate scope、幂等键和站内场景生图队列 - editor_project.rs 抽取站内与外部共用的场景生图 payload 构造函数,站内 handler 改为调用共享函数 - 同步更新 external v1 OpenAPI 契约与 MCP 派生排除标记,登记 docs/README.md 索引 - AGC 客户端美术包背景阶段改走新场景路由,stylePreset 固定 custom + customStyle 保留现有风格文案 - external_generation_state 快照与 recovery_scan 账本白名单支持新场景路由 - direct_runtime 背景资源识别同时兼容新场景路由与旧通用路由,新增旧路由回放测试 - 更新 genarrative-external-editor-api skill 参考文档 - 新增主规范、里程碑规范与实施计划三份 SDD 文档
This commit is contained in:
@@ -2626,6 +2626,30 @@ fn direct_taonier_reference_matches_local_source(
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn direct_taonier_background_asset_identity(
|
||||
root: &Path,
|
||||
expected_reference_source: Option<&DirectTaonierArtAssetIdentity>,
|
||||
) -> Option<DirectTaonierArtAssetIdentity> {
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
"scene",
|
||||
expected_reference_source,
|
||||
)
|
||||
.or_else(|| {
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
expected_reference_source,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
|
||||
let Some(art_spec) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
@@ -2637,14 +2661,7 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let Some(background) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(&art_spec),
|
||||
) else {
|
||||
let Some(background) = direct_taonier_background_asset_identity(root, Some(&art_spec)) else {
|
||||
return false;
|
||||
};
|
||||
let _ = (background, art_spec);
|
||||
@@ -3625,15 +3642,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
})
|
||||
.flatten();
|
||||
let existing_art_spec_and_background = existing_art_spec.as_ref().is_some_and(|art_spec| {
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(art_spec),
|
||||
)
|
||||
.is_some()
|
||||
direct_taonier_background_asset_identity(root, Some(art_spec)).is_some()
|
||||
});
|
||||
let art_spec = match existing_art_spec {
|
||||
Some(identity) => identity,
|
||||
@@ -3709,15 +3718,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
}
|
||||
};
|
||||
if mode.regenerates_existing()
|
||||
|| direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(&art_spec),
|
||||
)
|
||||
.is_none()
|
||||
|| direct_taonier_background_asset_identity(root, Some(&art_spec)).is_none()
|
||||
{
|
||||
emit_direct_game_creator_progress(root, "art.background", "正在生成 16:9 游戏场景背景图");
|
||||
let outcome = match generate_direct_taonier_art_asset_at(
|
||||
@@ -8421,8 +8422,8 @@ mod tests {
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
),
|
||||
GameCreationAppAssetKind::Scene => (
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
"scene",
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
),
|
||||
_ => (
|
||||
@@ -8806,6 +8807,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_route_background_registration_still_counts_as_valid_base() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "legacy-background-route", "旧路由背景登记")
|
||||
.expect("init project");
|
||||
std::fs::create_dir_all(root.path().join("assets")).expect("assets dir");
|
||||
register_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
);
|
||||
std::fs::write(
|
||||
root.path().join(DIRECT_CODEX_BACKGROUND_ASSET_PATH),
|
||||
tiny_visible_png(),
|
||||
)
|
||||
.expect("background bytes");
|
||||
register_local_asset_at(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"image/png",
|
||||
"platform-art",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("taonier-project".to_string()),
|
||||
resource_id: Some("taonier-resource-game-background".to_string()),
|
||||
asset_object_id: Some("taonier-object-game-background".to_string()),
|
||||
task_id: Some("taonier-task-game-background".to_string()),
|
||||
prompt: None,
|
||||
model: Some("gpt-image-2".to_string()),
|
||||
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
|
||||
generation_kind: Some("spec".to_string()),
|
||||
reference_resource_ids: vec!["taonier-resource-icon-spec".to_string()],
|
||||
},
|
||||
)
|
||||
.expect("register legacy-route background");
|
||||
|
||||
assert!(direct_taonier_art_base_is_valid(root.path()));
|
||||
}
|
||||
|
||||
fn tiny_visible_png() -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let image = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||
|
||||
@@ -2835,14 +2835,15 @@ pub(in crate::agent) fn retained_platform_art_generation_runtime_state_matches_d
|
||||
&& art_spec_asset_type == Some("icon-spec")
|
||||
}
|
||||
GameCreationAppAssetKind::Scene => {
|
||||
snapshot.endpoint == "/api/external/v1/editor/images/generations"
|
||||
&& snapshot.generation_kind == "spec"
|
||||
snapshot.endpoint == "/api/external/v1/editor/scenes/generations"
|
||||
&& snapshot.generation_kind == "scene"
|
||||
&& platform_art_runtime_references_match_request_contract(
|
||||
&snapshot.reference_resource_ids,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
)
|
||||
&& request_asset_kind.as_deref() == Some("game-background")
|
||||
&& art_spec_asset_type == Some("background")
|
||||
&& json_string_field(&request_body, "sceneContent")
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
&& json_string_field(&request_body, "stylePreset").as_deref() == Some("custom")
|
||||
}
|
||||
GameCreationAppAssetKind::IconSpritesheet => {
|
||||
snapshot.endpoint == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
@@ -3248,6 +3249,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
GameCreationAppAssetKind::Image => "image",
|
||||
GameCreationAppAssetKind::Character => "character",
|
||||
GameCreationAppAssetKind::PublicationMaterial => "publication-material",
|
||||
GameCreationAppAssetKind::Scene => "scene",
|
||||
_ => "spec",
|
||||
};
|
||||
let is_canonical_art_spritesheet =
|
||||
@@ -3291,6 +3293,25 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else if options.asset_kind == GameCreationAppAssetKind::Scene {
|
||||
(
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
serde_json::json!({
|
||||
"sceneContent": generation_prompt,
|
||||
"stylePreset": "custom",
|
||||
"customStyle": prompt_text!("media.scene_style"),
|
||||
"aspectRatio": options.aspect_ratio,
|
||||
"imageSize": options.image_size,
|
||||
"assetLabel": options.asset_label,
|
||||
"projectId": canvas_context.project_id,
|
||||
"assetFolderId": canvas_context.asset_folder_id,
|
||||
"referenceImageSrcs": references.ordered.clone(),
|
||||
"canvasCompletion": {
|
||||
"title": options.asset_label,
|
||||
"placeholder": external_canvas_placeholder(&options.aspect_ratio),
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"/api/external/v1/editor/images/generations",
|
||||
@@ -13272,14 +13293,13 @@ mod canvas_generation_tests {
|
||||
let background = write_retained_stage(
|
||||
root,
|
||||
"run-background-canonical-and-user-references",
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"/api/external/v1/editor/scenes/generations",
|
||||
serde_json::json!({
|
||||
"prompt": "保留账本参考合同",
|
||||
"kind": "spec",
|
||||
"assetKind": "game-background",
|
||||
"sceneContent": "保留账本参考合同",
|
||||
"stylePreset": "custom",
|
||||
"customStyle": "原创横屏 Web 游戏场景背景",
|
||||
"projectId": "test-canvas-project",
|
||||
"assetFolderId": "test-asset-folder",
|
||||
"generationInputs": { "artSpec": { "assetType": "background" } },
|
||||
"referenceImageSrcs": ["resource-icon-spec", "user-reference-1"],
|
||||
}),
|
||||
);
|
||||
|
||||
+24
@@ -735,6 +735,30 @@ pub(super) fn platform_art_generation_runtime_request_snapshot(
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
(generation_kind, reference_resource_ids)
|
||||
}
|
||||
"/api/external/v1/editor/scenes/generations" => {
|
||||
if json_string_field(&request_body, "sceneContent")
|
||||
.is_none_or(|value| value.trim().is_empty())
|
||||
{
|
||||
return Err("External Editor 场景生成账本请求缺少 sceneContent".to_string());
|
||||
}
|
||||
let reference_resource_ids = request_body
|
||||
.get("referenceImageSrcs")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.ok_or_else(|| {
|
||||
"External Editor 场景生成账本请求缺少 referenceImageSrcs".to_string()
|
||||
})?
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| "External Editor 场景生成账本引用资源 ID 无效".to_string())
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
("scene".to_string(), reference_resource_ids)
|
||||
}
|
||||
"/api/external/v1/editor/icon-spritesheets/generations" => {
|
||||
let reference_resource_id = json_string_field(&request_body, "referenceId")
|
||||
.or_else(|| json_string_field(&request_body, "referenceImageSrc"))
|
||||
|
||||
@@ -555,6 +555,7 @@ fn platform_art_generation_request_snapshot_is_valid(payload: &serde_json::Value
|
||||
endpoint,
|
||||
Some(
|
||||
"/api/external/v1/editor/images/generations"
|
||||
| "/api/external/v1/editor/scenes/generations"
|
||||
| "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user