接通画布生成参考素材的原生链路

新增 referenceAssetIds 原生入参:start_local_project_asset_generation 与 generate_local_project_asset 经 prepare_local_project_asset_generation 统一收口当前项目 manifest 图片素材 id
PlatformArtAssetGenerationOptions 增加 reference_asset_ids,GUI、agent 桥、media 观察与 Direct 运行时构造点显式传空
参考解析固定规范图前置在前、用户参考按给出顺序随后,按远端资源 ID 去重,普通生成总量 5 张、有规范前置 4 张用户参考,图集拒绝额外参考而非静默丢弃
参考素材复用 manifest 到本地文件、图片解码、内容 hash、当前账号 binding 上传的既有通道,历史账号遗留远端 ID 不再复用
retained 阶段与清单校验改为与提交同一套请求合同:icon-spec、ui-prototype、game-background 允许正确引用,art-spritesheet 仍只接受唯一规范引用
修复 Part::file_name 的生命周期错误与 4 处测试构造点少传参考参数
This commit is contained in:
2026-09-17 17:47:16 +08:00
parent 0a1f0e0b26
commit aec9568c39
9 changed files with 432 additions and 83 deletions
@@ -2566,11 +2566,18 @@ fn direct_taonier_art_asset_identity(
.to_string(),
reference_resource_ids: asset.source.reference_resource_ids.clone(),
};
// 参考集合先按该 kind 的请求合同收口:根素材(规范图)允许用户参考(icon-spec 没有
// 规范前置,参考只是风格输入),派生素材仍必须按合同携带规范前置。
let references_match_contract =
crate::agent::platform_art_runtime_references_match_request_contract(
&identity.reference_resource_ids,
expected_kind,
);
let lineage_matches = match expected_reference_source {
Some(source) => direct_taonier_reference_matches_local_source(root, source, &identity),
None => identity.reference_resource_ids.is_empty(),
None => true,
};
lineage_matches.then_some(identity)
(references_match_contract && lineage_matches).then_some(identity)
})
}
@@ -2579,7 +2586,9 @@ fn direct_taonier_reference_matches_local_source(
source: &DirectTaonierArtAssetIdentity,
derived: &DirectTaonierArtAssetIdentity,
) -> bool {
let [remote_reference_id] = derived.reference_resource_ids.as_slice() else {
// 派生素材的规范身份只由参考序列首项承担:用户参考按顺序追加在规范图之后,
// 不能让它们顶替或淹没规范引用,也不能因为多出用户参考就判定派生关系不成立。
let Some(remote_reference_id) = derived.reference_resource_ids.first() else {
return false;
};
if derived.canvas_project_id == source.canvas_project_id
@@ -3386,6 +3395,7 @@ async fn generate_direct_taonier_art_asset_at(
slice_mode: None,
grid_x: None,
grid_y: None,
reference_asset_ids: Vec::new(),
};
let runtime_context =
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
@@ -2262,6 +2262,7 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
slice_mode,
grid_x,
grid_y,
reference_asset_ids: Vec::new(),
};
let _generation_guard = state.image_generation_gate.lock().await;
let generated = with_direct_editor_api_credentials(
@@ -67,10 +67,11 @@ pub(crate) use canvas_generation::{
generate_platform_art_asset_with_options_at,
generate_platform_art_asset_with_required_slices_at, maybe_generate_platform_art_asset_step,
needs_platform_art_asset_generation, normalize_platform_art_asset_generation_kind,
platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
prepare_platform_art_asset_output_path, project_canvas_asset_media_types,
role_has_canvas_assets, suggested_canvas_tool_call, PlatformArtAssetGenerationOptions,
PLATFORM_ART_ASSET_GENERATION_KINDS,
normalize_platform_art_reference_asset_ids, platform_art_asset_art_spec,
platform_art_asset_output_extension_matches,
platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path,
project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call,
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
};
#[allow(unused_imports)]
pub(crate) use draft_validation::{
File diff suppressed because it is too large Load Diff
@@ -577,6 +577,7 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
slice_mode: (!slice_mode.trim().is_empty()).then_some(slice_mode.clone()),
grid_x,
grid_y,
reference_asset_ids: Vec::new(),
};
if let Some(pending) = pending_action {
match recover_persisted_visual_generation_options(
@@ -628,6 +629,7 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
.or_else(|| (!slice_mode.trim().is_empty()).then_some(slice_mode)),
grid_x,
grid_y,
reference_asset_ids: requested_options.reference_asset_ids,
}
};
options.replace_existing = replace_existing;
@@ -390,6 +390,9 @@ pub(crate) async fn start_local_project_asset_generation(
image_size: Option<String>,
asset_name: Option<String>,
output_path: Option<String>,
// 前端 IPC 字段 `referenceAssetIds`:当前项目 manifest 里的图片素材 id,只做参考输入,
// 不进任务账本(重试由调用方继续用同一份引用提交,账本本身不新增字段)。
reference_asset_ids: Option<Vec<String>>,
) -> Result<AssetGenerationTaskRecord, String> {
let task_id = asset_generation_task_id(&task_id)?;
let request = prepare_local_project_asset_generation(
@@ -400,6 +403,7 @@ pub(crate) async fn start_local_project_asset_generation(
image_size.as_deref(),
asset_name.as_deref(),
output_path.as_deref(),
reference_asset_ids.as_deref().unwrap_or_default(),
)?;
enforce_project_permission_policy(&request.root, "canvas.asset_generate")?;
enforce_project_permission_policy(&request.root, "asset.register")?;
@@ -4610,6 +4610,7 @@ pub(crate) fn prepare_local_project_asset_generation(
image_size: Option<&str>,
asset_name: Option<&str>,
output_path: Option<&str>,
reference_asset_ids: &[String],
) -> Result<LocalProjectAssetGenerationRequest, String> {
let project_path = project_path.trim();
if project_path.is_empty() {
@@ -4617,6 +4618,10 @@ pub(crate) fn prepare_local_project_asset_generation(
}
let asset_kind = normalize_platform_art_asset_generation_kind(kind)
.ok_or_else(|| format!("素材类型不受支持:{}", kind.trim()))?;
// 参考入参只接受当前项目 manifest 素材 id:路径、远端 resourceId 与超限在这里就被拒绝,
// 不把校验推迟到远端(远端只该收到当前账号绑定下的 resource ID)。
let reference_asset_ids =
normalize_platform_art_reference_asset_ids(asset_kind, reference_asset_ids)?;
Ok(LocalProjectAssetGenerationRequest {
root: PathBuf::from(project_path),
prompt: local_project_asset_prompt(prompt)?,
@@ -4650,6 +4655,7 @@ pub(crate) fn prepare_local_project_asset_generation(
slice_mode: None,
grid_x: None,
grid_y: None,
reference_asset_ids,
},
})
}
@@ -4671,6 +4677,7 @@ pub(crate) async fn generate_local_project_asset(
image_size: Option<String>,
asset_name: Option<String>,
output_path: Option<String>,
reference_asset_ids: Option<Vec<String>>,
) -> Result<UploadLocalAssetResult, String> {
let request = prepare_local_project_asset_generation(
&project_path,
@@ -4680,6 +4687,7 @@ pub(crate) async fn generate_local_project_asset(
image_size.as_deref(),
asset_name.as_deref(),
output_path.as_deref(),
reference_asset_ids.as_deref().unwrap_or_default(),
)?;
enforce_project_permission_policy(&request.root, "canvas.asset_generate")?;
enforce_project_permission_policy(&request.root, "asset.register")?;
@@ -4698,7 +4706,16 @@ mod local_project_asset_generation_tests {
use super::*;
fn prepare(kind: &str, prompt: &str) -> Result<LocalProjectAssetGenerationRequest, String> {
prepare_local_project_asset_generation("/tmp/project", kind, prompt, None, None, None, None)
prepare_local_project_asset_generation(
"/tmp/project",
kind,
prompt,
None,
None,
None,
None,
&[],
)
}
#[test]
@@ -4738,6 +4755,7 @@ mod local_project_asset_generation_tests {
Some("2K"),
Some(" 主角图集 "),
Some(" assets/hero.png "),
&[],
)
.expect("explicit options");
assert_eq!(explicit.root, PathBuf::from("/tmp/project"));
@@ -4765,8 +4783,17 @@ mod local_project_asset_generation_tests {
#[test]
fn invalid_toolbar_arguments_are_rejected_before_any_generation() {
assert_eq!(
prepare_local_project_asset_generation("", "image", "要求", None, None, None, None)
.expect_err("empty project path"),
prepare_local_project_asset_generation(
"",
"image",
"要求",
None,
None,
None,
None,
&[]
)
.expect_err("empty project path"),
"项目路径不能为空"
);
assert_eq!(
@@ -4793,7 +4820,8 @@ mod local_project_asset_generation_tests {
Some("4:3"),
None,
None,
None
None,
&[]
)
.expect_err("unsupported ratio"),
"图片比例不受支持:4:3"
@@ -4806,7 +4834,8 @@ mod local_project_asset_generation_tests {
None,
Some("4K"),
None,
None
None,
&[]
)
.expect_err("unsupported size"),
"图片尺寸不受支持:4K"
@@ -4819,7 +4848,8 @@ mod local_project_asset_generation_tests {
None,
None,
Some("\u{7}名字"),
None
None,
&[]
)
.expect_err("control character in asset name"),
"素材名称超出安全边界"
@@ -4832,7 +4862,8 @@ mod local_project_asset_generation_tests {
None,
None,
None,
Some(&"a".repeat(LOCAL_PROJECT_ASSET_MAX_OUTPUT_PATH_CHARS + 1))
Some(&"a".repeat(LOCAL_PROJECT_ASSET_MAX_OUTPUT_PATH_CHARS + 1)),
&[]
)
.expect_err("oversized output path"),
"输出路径超出安全边界"
@@ -1131,8 +1131,17 @@ pub(crate) fn validate_manifest_required_visual_asset(
}
if task_id == "art-director" {
if !asset.source.reference_resource_ids.is_empty() {
return Err("统一视觉规范图不得声明派生资源引用".to_string());
// 规范图是视觉来源链的根:它自身不派生任何视觉资产,但 icon-spec 生成允许用户参考
// (没有规范前置,最多总上限),这些参考只是风格输入,不构成派生关系。这里改为验证
// 参考集合仍符合 icon-spec 请求合同;route / generation kind / canvasProjectId /
// resourceId / PNG 解码等身份判据全部保持不变。
if !crate::agent::platform_art_runtime_references_match_request_contract(
&asset.source.reference_resource_ids,
expected_kind,
) {
return Err(format!(
"统一视觉规范图的参考集合不符合请求合同:{expected_path}"
));
}
return Ok(());
}
@@ -1157,11 +1166,17 @@ pub(crate) fn validate_manifest_required_visual_asset(
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "统一视觉规范图缺少 resourceId".to_string())?;
let [reference_resource_id] = asset.source.reference_resource_ids.as_slice() else {
// 派生素材的参考合同是「规范图前置在最前,用户参考按顺序追加在后」,图集不接受用户参考:
// 规范身份仍只由首项承担,用户参考不能顶替也不能冒充规范引用。
if !crate::agent::platform_art_runtime_references_match_request_contract(
&asset.source.reference_resource_ids,
expected_kind,
) {
return Err(format!(
"派生视觉资产未精确引用当前统一视觉规范图:{expected_path}"
));
};
}
let reference_resource_id = asset.source.reference_resource_ids[0].as_str();
let original_provenance_matches =
canvas_project_id == art_spec_project_id && reference_resource_id == art_spec_resource_id;
let rebound_local_source_matches = if original_provenance_matches {
@@ -1079,6 +1079,7 @@ async fn canonical_art_spec_and_ui_requests_use_the_shared_reference_chain() {
slice_mode: None,
grid_x: None,
grid_y: None,
reference_asset_ids: Vec::new(),
},
)
.await;
@@ -2656,6 +2657,7 @@ async fn generate_local_project_asset_command_registers_the_requested_toolbar_ki
Some("2K".to_string()),
Some("工具栏图片".to_string()),
None,
None,
)
.await
.expect("toolbar asset generation");
@@ -2722,6 +2724,7 @@ async fn generate_local_project_asset_command_maps_spec_onto_the_verified_icon_s
None,
Some("视觉规范图".to_string()),
None,
None,
)
.await
.expect("toolbar spec generation");
@@ -2788,6 +2791,7 @@ async fn generate_local_project_asset_command_generates_art_spritesheet_from_the
None,
None,
None,
None,
)
.await
.expect_err("art-spritesheet requires a registered icon-spec");
@@ -2811,6 +2815,7 @@ async fn generate_local_project_asset_command_generates_art_spritesheet_from_the
Some("1K".to_string()),
Some("游戏首版图集".to_string()),
None,
None,
)
.await
.expect("toolbar spritesheet generation");
@@ -5499,6 +5504,7 @@ fn ui_prototype_generation_uses_dedicated_prompt_and_art_spec() {
slice_mode: None,
grid_x: None,
grid_y: None,
reference_asset_ids: Vec::new(),
};
let prompt = build_platform_art_asset_prompt(
"原创网格贪吃蛇:分数与状态 HUD、四类不同分值食物、开始、方向键/WASD、触控方向键、失败与重开",