Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4296a4bdd | |||
| 7eee89ded3 | |||
| f9bc2d917c | |||
| b96cd4b7c2 | |||
| f185d21f55 | |||
| a61b0cce81 | |||
| 2a75a2d2c8 | |||
| 512f669f57 | |||
| 21dbbb4c3c | |||
| 55d0c99e2b | |||
| 3907a8bf74 | |||
| 46dfa2cffb | |||
| 97e2b26900 | |||
| 39e33f7a5b | |||
| 040f646480 | |||
| 8b007c637d | |||
| f0a62c6306 | |||
| 30edfee00e | |||
| c2da8e184e | |||
| a19319d815 | |||
| d07ea186fa | |||
| 80caec373a | |||
| 3caefd6716 | |||
| 4d0dd2d721 | |||
| 2befaaef9d | |||
| 20b0bf4dd7 | |||
| b4fd8d9b8b | |||
| fe85fa2a62 | |||
| b14a533b17 | |||
| 42969b2218 | |||
| c4ee75a1af | |||
| 6f2137cbb0 | |||
| 70a2246e8b | |||
| b7e3dac661 | |||
| 2e4a1996c8 | |||
| fe4e952853 | |||
| 18654b6806 | |||
| ee7d00c0b1 | |||
| 9cd1a94369 | |||
| 470e85c0ff |
@@ -164,6 +164,7 @@ module.exports = {
|
||||
'server-rs/target-*',
|
||||
'apps/desktop-shell/src-tauri/target',
|
||||
'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
|
||||
'apps/ai-game-creator-shell/src/contracts/generated/**',
|
||||
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
|
||||
'target',
|
||||
'src/main.tsx',
|
||||
|
||||
@@ -4,6 +4,8 @@ node_modules
|
||||
.codex-logs
|
||||
public/Icons
|
||||
apps/ai-game-creator-shell/src/features/ui-editor/types/
|
||||
# ts-rs 直接落盘的生成产物:保持生成器原始格式,`cargo test export_bindings` 才可幂等复现。
|
||||
apps/ai-game-creator-shell/src/contracts/generated/
|
||||
apps/ai-game-creator-shell/src-tauri/resources/agc-skills/
|
||||
media
|
||||
*.log
|
||||
|
||||
+2
@@ -4900,6 +4900,8 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -14,7 +14,7 @@ cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-i
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
|
||||
tauri-build = { version = "2.6.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
@@ -50,7 +50,7 @@ portable-pty = "0.9"
|
||||
percent-encoding = "2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
|
||||
regex = "1"
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
|
||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem};
|
||||
use super::validation::validate_direct_codex_user_item;
|
||||
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
|
||||
use crate::ui_editor::persistence::{
|
||||
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -111,29 +115,165 @@ pub(crate) fn direct_codex_user_item_to_prompt(
|
||||
item: &DirectCodexUserItem,
|
||||
) -> Result<String, String> {
|
||||
let wire = direct_codex_user_item_to_wire_input(root, item)?;
|
||||
wire.as_array()
|
||||
let DirectCodexUserItem::Message(message) = item;
|
||||
let mut prompt = wire
|
||||
.as_array()
|
||||
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())
|
||||
.map(|parts| {
|
||||
.and_then(|parts| {
|
||||
parts
|
||||
.iter()
|
||||
.filter_map(|part| part.get("text").and_then(Value::as_str))
|
||||
.collect::<String>()
|
||||
})
|
||||
.and_then(|prompt| {
|
||||
if prompt.trim().is_empty() {
|
||||
Err("DirectProject user item 不能转换为空 prompt".to_string())
|
||||
} else {
|
||||
Ok(prompt)
|
||||
.map(|part| {
|
||||
part.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
|
||||
})
|
||||
.collect::<Result<String, String>>()
|
||||
})?;
|
||||
if let Some(code_context) = render_ui_design_code_context(root, message)? {
|
||||
prompt.push('\n');
|
||||
prompt.push_str(&code_context);
|
||||
}
|
||||
if prompt.trim().is_empty() {
|
||||
return Err("DirectProject user item 不能转换为空 prompt".to_string());
|
||||
}
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
|
||||
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
|
||||
///
|
||||
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
|
||||
/// 的纯投影,不得在这里产生项目写副作用。
|
||||
fn render_ui_design_code_context(
|
||||
root: &Path,
|
||||
message: &DirectCodexUserMessageItem,
|
||||
) -> Result<Option<String>, String> {
|
||||
let referenced_ids = message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
Some(resource_id.trim())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if referenced_ids.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let mut lines = Vec::new();
|
||||
for resource_id in referenced_ids {
|
||||
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
|
||||
asset.id == resource_id
|
||||
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
});
|
||||
if !is_ui_design_doc {
|
||||
continue;
|
||||
}
|
||||
lines.push(
|
||||
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
asset_id: resource_id.to_string(),
|
||||
}) {
|
||||
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
|
||||
Err(error) => format!("生成代码遇到错误{error}"),
|
||||
},
|
||||
);
|
||||
}
|
||||
if lines.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(lines.join("\n")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::direct_codex_user_item_to_response_item;
|
||||
use super::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item};
|
||||
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
|
||||
use serde_json::json;
|
||||
use shared_contracts::game_creation_app::{
|
||||
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
const PROMPT_CONTEXT_PROJECT_ID: &str = "direct-prompt-context-project";
|
||||
|
||||
fn prompt_context_project() -> tempfile::TempDir {
|
||||
let directory = tempfile::tempdir().expect("temp project");
|
||||
crate::init_local_game_project_at(
|
||||
directory.path(),
|
||||
PROMPT_CONTEXT_PROJECT_ID,
|
||||
"Direct prompt 上下文测试",
|
||||
)
|
||||
.expect("init project");
|
||||
directory
|
||||
}
|
||||
|
||||
fn register_fixture_asset(
|
||||
root: &Path,
|
||||
relative_path: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
media_type: &str,
|
||||
) -> String {
|
||||
let absolute_path = crate::resolve_local_project_path(root, relative_path)
|
||||
.expect("resolve fixture asset path");
|
||||
std::fs::create_dir_all(absolute_path.parent().expect("fixture asset parent"))
|
||||
.expect("create fixture asset parent");
|
||||
std::fs::write(&absolute_path, b"{}").expect("write fixture asset file");
|
||||
crate::assets::register_local_asset_at(
|
||||
root,
|
||||
relative_path,
|
||||
kind,
|
||||
media_type,
|
||||
"prompt-context-test",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
resource_id: Some("prompt-context".to_string()),
|
||||
asset_object_id: None,
|
||||
task_id: None,
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: None,
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("register fixture asset")
|
||||
.id
|
||||
}
|
||||
|
||||
fn ui_design_doc_fixture(initialize_state: bool) -> (tempfile::TempDir, String) {
|
||||
let directory = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
directory.path(),
|
||||
"ui/design.json",
|
||||
GameCreationAppAssetKind::UiDesignDoc,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
);
|
||||
if initialize_state {
|
||||
crate::ui_editor::persistence::initialize_ui_design_state_at(
|
||||
directory.path(),
|
||||
PROMPT_CONTEXT_PROJECT_ID,
|
||||
&asset_id,
|
||||
)
|
||||
.expect("initialize UI design state");
|
||||
}
|
||||
(directory, asset_id)
|
||||
}
|
||||
|
||||
fn user_item_with_resource_reference(asset_id: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"id": "turn-1:user",
|
||||
"content": [{"type": "agc_resource_reference", "resourceId": asset_id}],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_response_item_passes_through_without_agc_private_parts() {
|
||||
let item = json!({
|
||||
@@ -174,4 +314,81 @@ mod tests {
|
||||
.expect_err("history item without type must fail");
|
||||
assert!(error.contains("缺少 type"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_doc_reference_appends_generated_code_context() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(true);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(
|
||||
prompt.contains(&format!(
|
||||
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
|
||||
)),
|
||||
"{prompt}"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
|
||||
"{prompt}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_projection_never_writes_generated_ui_design_code() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(true);
|
||||
let item = user_item_with_resource_reference(&asset_id);
|
||||
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
|
||||
let generated_root =
|
||||
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
|
||||
let generated_files = std::fs::read_dir(generated_root)
|
||||
.expect("read ui directory")
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with("generated-")
|
||||
})
|
||||
.count();
|
||||
assert_eq!(
|
||||
generated_files, 0,
|
||||
"历史回读只做纯投影,不得生成 UI 设计代码"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(false);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
|
||||
let project = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
project.path(),
|
||||
"assets/hero.png",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(
|
||||
!prompt.contains("请先阅读生成的带有文档的代码片段"),
|
||||
"{prompt}"
|
||||
);
|
||||
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,32 +69,31 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [
|
||||
const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art";
|
||||
const DIRECT_CODEX_HOME_CREATE_PROJECT_MARKER: &str = "[[AGC_CREATE_PROJECT]]";
|
||||
|
||||
fn direct_codex_game_outputs(root: &Path) -> Vec<(String, &'static str, &'static str)> {
|
||||
/// 直连 Codex 生成的游戏工程文件 → manifest 登记项。
|
||||
///
|
||||
/// 这些不是用户素材,而是游戏工程自身的源码/配置(`index.html`、`style.css`、`game.js`、
|
||||
/// `package.json`、`package-lock.json`、`vite.config.js`)。它们的 manifest kind 统一是正式成员
|
||||
/// `code`:`code` 与旧的 `game-*` / `image` 兜底一样派生 `unclassified`(「待归类」),
|
||||
/// 不会把工程文件塞进内容栏目,同时写入侧不再产出非 canonical 的 kind。
|
||||
fn direct_codex_game_outputs(root: &Path) -> Vec<(String, GameCreationAppAssetKind, &'static str)> {
|
||||
let entry = agent_runtime_game_entry_relative_path(root);
|
||||
let prefix = if entry == AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH {
|
||||
""
|
||||
} else {
|
||||
"game/"
|
||||
};
|
||||
let code = GameCreationAppAssetKind::Code;
|
||||
vec![
|
||||
(entry.to_string(), "game-entry", "text/html"),
|
||||
(format!("{prefix}style.css"), "game-style", "text/css"),
|
||||
(format!("{prefix}game.js"), "game-script", "text/javascript"),
|
||||
(
|
||||
format!("{prefix}package.json"),
|
||||
"game-package",
|
||||
"application/json",
|
||||
),
|
||||
(entry.to_string(), code, "text/html"),
|
||||
(format!("{prefix}style.css"), code, "text/css"),
|
||||
(format!("{prefix}game.js"), code, "text/javascript"),
|
||||
(format!("{prefix}package.json"), code, "application/json"),
|
||||
(
|
||||
format!("{prefix}package-lock.json"),
|
||||
"game-lockfile",
|
||||
code,
|
||||
"application/json",
|
||||
),
|
||||
(
|
||||
format!("{prefix}vite.config.js"),
|
||||
"game-build-config",
|
||||
"text/javascript",
|
||||
),
|
||||
(format!("{prefix}vite.config.js"), code, "text/javascript"),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -938,7 +937,7 @@ fn direct_taonier_spritesheet_matches_retained_stage_result_at(
|
||||
let Some(art_spec) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -948,7 +947,7 @@ fn direct_taonier_spritesheet_matches_retained_stage_result_at(
|
||||
let Some(spritesheet) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
Some(&art_spec),
|
||||
@@ -2501,7 +2500,7 @@ fn direct_taonier_art_generation_runtime_context(
|
||||
fn direct_taonier_art_asset_identity(
|
||||
root: &Path,
|
||||
expected_path: &str,
|
||||
expected_kind: &str,
|
||||
expected_kind: GameCreationAppAssetKind,
|
||||
expected_generation_route: &str,
|
||||
expected_generation_kind: &str,
|
||||
expected_reference_source: Option<&DirectTaonierArtAssetIdentity>,
|
||||
@@ -2535,7 +2534,7 @@ fn direct_taonier_art_asset_identity(
|
||||
local_asset_id: asset.id.clone(),
|
||||
source_sha256: validated.content_sha256,
|
||||
media_type: asset.media_type.clone(),
|
||||
canonical_asset_kind: asset.kind.clone(),
|
||||
canonical_asset_kind: asset.kind.to_string(),
|
||||
resource_id: asset
|
||||
.source
|
||||
.resource_id
|
||||
@@ -2609,7 +2608,7 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
|
||||
let Some(art_spec) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -2619,7 +2618,7 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
|
||||
let Some(background) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(&art_spec),
|
||||
@@ -2634,7 +2633,7 @@ fn direct_taonier_art_package_is_valid(root: &Path) -> bool {
|
||||
let Some(art_spec) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -2647,7 +2646,7 @@ fn direct_taonier_art_package_is_valid(root: &Path) -> bool {
|
||||
let Some(spritesheet) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
Some(&art_spec),
|
||||
@@ -2698,7 +2697,7 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
|
||||
let Some(art_spec) = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -2740,7 +2739,10 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
|
||||
return Vec::new();
|
||||
};
|
||||
if assets.next().is_some()
|
||||
|| asset.kind != "art-spritesheet-slice"
|
||||
|| asset.kind != GameCreationAppAssetKind::Icon
|
||||
|| !asset
|
||||
.local_path
|
||||
.starts_with("assets/art-spritesheet-slices/")
|
||||
|| asset.media_type != "image/png"
|
||||
|| asset.source.kind != GameCreationAppAssetSourceKind::Canvas
|
||||
|| asset.source.generation_route.as_deref()
|
||||
@@ -3050,7 +3052,7 @@ fn snapshot_direct_spritesheet_recovery_manifest_for_install(
|
||||
let current_art_spec = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -3069,7 +3071,7 @@ fn register_direct_recovered_spritesheet_at(
|
||||
register_local_asset_at(
|
||||
root,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"image/png",
|
||||
"platform-art-recovery",
|
||||
source,
|
||||
@@ -3106,7 +3108,7 @@ fn install_direct_recovered_spritesheet_binding_at(
|
||||
®istered_asset.id,
|
||||
content_sha256,
|
||||
"image/png",
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet.as_str(),
|
||||
)?;
|
||||
let binding = new_external_editor_resource_binding(
|
||||
project_id,
|
||||
@@ -3153,7 +3155,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
|
||||
&source_asset.id,
|
||||
&format!("{:x}", Sha256::digest(&source_bytes)),
|
||||
&source_asset.media_type,
|
||||
&source_asset.kind,
|
||||
source_asset.kind.as_str(),
|
||||
)?;
|
||||
let principal = external_editor_binding_principal(&access)?;
|
||||
let Some(project_binding) =
|
||||
@@ -3419,7 +3421,7 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
let art_spec = direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -3579,7 +3581,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -3590,7 +3592,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(art_spec),
|
||||
@@ -3656,7 +3658,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -3674,7 +3676,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
|| direct_taonier_art_asset_identity(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Some(&art_spec),
|
||||
@@ -4414,7 +4416,7 @@ fn sync_direct_codex_project_file_projection_at(
|
||||
register_local_asset_at(
|
||||
root,
|
||||
&local_path,
|
||||
"game-source",
|
||||
GameCreationAppAssetKind::Code,
|
||||
media_type,
|
||||
"direct-codex",
|
||||
direct_codex_generated_source(),
|
||||
@@ -7118,7 +7120,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"replacement-before-crash",
|
||||
Vec::new(),
|
||||
&replacement_spec,
|
||||
@@ -7216,7 +7218,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"strict-crash-spec",
|
||||
Vec::new(),
|
||||
&new_spec,
|
||||
@@ -7230,7 +7232,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"strict-crash-background",
|
||||
vec!["strict-crash-spec".to_string()],
|
||||
&new_background,
|
||||
@@ -7446,7 +7448,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"replacement-before-crash",
|
||||
Vec::new(),
|
||||
&tiny_replacement_png(64, 96, 224),
|
||||
@@ -7529,7 +7531,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"unknown-stage-replacement",
|
||||
Vec::new(),
|
||||
&tiny_replacement_png(224, 64, 32),
|
||||
@@ -8128,7 +8130,7 @@ mod tests {
|
||||
let art_spec = direct_taonier_art_asset_identity(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
None,
|
||||
@@ -8143,7 +8145,7 @@ mod tests {
|
||||
let concurrent = register_local_asset_at(
|
||||
root.path(),
|
||||
concurrent_path,
|
||||
"illustration",
|
||||
GameCreationAppAssetKind::Image,
|
||||
"image/png",
|
||||
"uploaded",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -8176,7 +8178,7 @@ mod tests {
|
||||
let recovery = register_local_asset_at(
|
||||
root.path(),
|
||||
recovery_path,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"image/png",
|
||||
"platform-art-recovery",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -8224,7 +8226,7 @@ mod tests {
|
||||
let registered = register_local_asset_at(
|
||||
root.path(),
|
||||
local_path,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"image/png",
|
||||
"platform-art-recovery",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -8341,14 +8343,18 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
fn register_direct_taonier_art_asset_fixture(root: &Path, local_path: &str, kind: &str) {
|
||||
fn register_direct_taonier_art_asset_fixture(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
) {
|
||||
let (generation_route, generation_kind, reference_resource_ids) = match kind {
|
||||
"art-spritesheet" => (
|
||||
GameCreationAppAssetKind::IconSpritesheet => (
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
),
|
||||
"game-background" => (
|
||||
GameCreationAppAssetKind::Scene => (
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
@@ -8359,6 +8365,14 @@ mod tests {
|
||||
Vec::new(),
|
||||
),
|
||||
};
|
||||
// 平台侧资源/对象/任务 id 按生成阶段命名(art-spec / game-background /
|
||||
// art-spritesheet),与 manifest kind 是两套词汇;kind 收紧成枚举后不能让这些
|
||||
// fixture 标识跟着改名。
|
||||
let identity_slug = match kind {
|
||||
GameCreationAppAssetKind::IconSpritesheet => "art-spritesheet",
|
||||
GameCreationAppAssetKind::Scene => "game-background",
|
||||
_ => kind.as_str(),
|
||||
};
|
||||
let mut png_bytes = Vec::new();
|
||||
let image = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||
1,
|
||||
@@ -8381,9 +8395,9 @@ mod tests {
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("taonier-project".to_string()),
|
||||
resource_id: Some(format!("taonier-resource-{kind}")),
|
||||
asset_object_id: Some(format!("taonier-object-{kind}")),
|
||||
task_id: Some(format!("taonier-task-{kind}")),
|
||||
resource_id: Some(format!("taonier-resource-{identity_slug}")),
|
||||
asset_object_id: Some(format!("taonier-object-{identity_slug}")),
|
||||
task_id: Some(format!("taonier-task-{identity_slug}")),
|
||||
prompt: None,
|
||||
model: Some("gpt-image-2".to_string()),
|
||||
generation_route: Some(generation_route.to_string()),
|
||||
@@ -8399,7 +8413,7 @@ mod tests {
|
||||
register_direct_taonier_art_asset_fixture(
|
||||
root,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
);
|
||||
let main_bytes = std::fs::read(root.join(DIRECT_CODEX_SPRITESHEET_ASSET_PATH))
|
||||
.expect("spritesheet bytes");
|
||||
@@ -8476,7 +8490,7 @@ mod tests {
|
||||
register_local_asset_at(
|
||||
root,
|
||||
path,
|
||||
"art-spritesheet-slice",
|
||||
GameCreationAppAssetKind::Icon,
|
||||
"image/png",
|
||||
"platform-art",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -8588,7 +8602,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
&spritesheet_resource_id,
|
||||
vec![art_spec_resource_id.to_string()],
|
||||
&spritesheet_bytes,
|
||||
@@ -8675,19 +8689,20 @@ mod tests {
|
||||
fn replace_direct_taonier_art_asset_fixture(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
resource_id: &str,
|
||||
reference_resource_ids: Vec<String>,
|
||||
bytes: &[u8],
|
||||
) {
|
||||
let (generation_route, generation_kind) = if kind == "art-spritesheet" {
|
||||
(
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
)
|
||||
} else {
|
||||
("/api/external/v1/editor/images/generations", "spec")
|
||||
};
|
||||
let (generation_route, generation_kind) =
|
||||
if kind == GameCreationAppAssetKind::IconSpritesheet {
|
||||
(
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
)
|
||||
} else {
|
||||
("/api/external/v1/editor/images/generations", "spec")
|
||||
};
|
||||
std::fs::write(root.join(local_path), bytes).expect("replace fixture bytes");
|
||||
register_local_asset_at(
|
||||
root,
|
||||
@@ -8716,12 +8731,12 @@ mod tests {
|
||||
register_direct_taonier_art_asset_fixture(
|
||||
root,
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
);
|
||||
register_direct_taonier_art_asset_fixture(
|
||||
root,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9016,7 +9031,11 @@ mod tests {
|
||||
init_local_game_project_at(root.path(), "direct-independent-image", "独立平台图片")
|
||||
.expect("init project");
|
||||
std::fs::create_dir_all(root.path().join("assets")).expect("assets dir");
|
||||
register_direct_taonier_art_asset_fixture(root.path(), "assets/neon-mine.png", "image");
|
||||
register_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
"assets/neon-mine.png",
|
||||
GameCreationAppAssetKind::Image,
|
||||
);
|
||||
std::fs::write(root.path().join("game/index.html"), "<!doctype html>").expect("index");
|
||||
std::fs::write(root.path().join("game/style.css"), "body {} ").expect("style");
|
||||
std::fs::write(
|
||||
@@ -9224,7 +9243,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"replacement-spec",
|
||||
Vec::new(),
|
||||
&tiny_replacement_png(32, 64, 192),
|
||||
@@ -9235,7 +9254,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"replacement-background",
|
||||
vec!["replacement-spec".to_string()],
|
||||
&tiny_replacement_png(16, 128, 224),
|
||||
@@ -9323,7 +9342,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"durable-replacement-spec",
|
||||
Vec::new(),
|
||||
&tiny_replacement_png(32, 64, 192),
|
||||
@@ -9336,7 +9355,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"durable-replacement-background",
|
||||
vec!["durable-replacement-spec".to_string()],
|
||||
&tiny_replacement_png(16, 128, 224),
|
||||
@@ -9421,7 +9440,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"uncertain-committed-spec",
|
||||
Vec::new(),
|
||||
&replacement,
|
||||
@@ -9481,7 +9500,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"uncertain-committed-spritesheet",
|
||||
vec!["taonier-resource-icon-spec".to_string()],
|
||||
&replacement,
|
||||
@@ -9537,7 +9556,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"late-committed-spec",
|
||||
Vec::new(),
|
||||
&tiny_replacement_png(32, 64, 192),
|
||||
@@ -9591,7 +9610,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"replacement-spec-before-background",
|
||||
Vec::new(),
|
||||
&tiny_replacement_png(32, 64, 192),
|
||||
@@ -9610,7 +9629,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"late-committed-background",
|
||||
vec!["replacement-spec-before-background".to_string()],
|
||||
&tiny_replacement_png(16, 128, 224),
|
||||
@@ -9655,7 +9674,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"replacement-spec",
|
||||
Vec::new(),
|
||||
&replacement_spec,
|
||||
@@ -9666,7 +9685,7 @@ mod tests {
|
||||
replace_direct_taonier_art_asset_fixture(
|
||||
root.path(),
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
"replacement-background",
|
||||
vec!["replacement-spec".to_string()],
|
||||
&replacement_background,
|
||||
|
||||
@@ -1203,7 +1203,7 @@ fn bridge_registered_resource(
|
||||
// (落盘值 + 按 kind 派生 + 读时自愈),这里走 Rust 的同构实现。
|
||||
// 直接透传落盘 `asset.category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、
|
||||
// 在 Agent 侧读到 `unclassified`(真机 55 条分歧)。
|
||||
"category": game_creation_app_asset_effective_category(&asset.kind, asset.category),
|
||||
"category": game_creation_app_asset_effective_category(asset.kind.as_str(), asset.category),
|
||||
"tags": asset.tags,
|
||||
"canvasProjectId": asset.source.canvas_project_id,
|
||||
"resourceId": asset.source.resource_id,
|
||||
@@ -1244,7 +1244,8 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
|
||||
arguments,
|
||||
"kind",
|
||||
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_KIND_CHARS,
|
||||
)?;
|
||||
)?
|
||||
.map(|kind| GameCreationAppAssetKind::parse_with_context(&kind, "asset.list.kind"));
|
||||
let asset_id = bridge_optional_bounded_string(
|
||||
arguments,
|
||||
"assetId",
|
||||
@@ -1264,7 +1265,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
|
||||
let mut assets = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| kind.as_ref().is_none_or(|kind| asset.kind == *kind))
|
||||
.filter(|asset| kind.is_none_or(|kind| asset.kind == kind))
|
||||
.filter(|asset| {
|
||||
asset_id
|
||||
.as_ref()
|
||||
@@ -1862,7 +1863,7 @@ async fn bridge_create_or_derive_resource(
|
||||
source_asset_id: source_asset.as_ref().map(|asset| asset.id.clone()),
|
||||
source_path: source_asset.as_ref().map(|asset| asset.local_path.clone()),
|
||||
source_media_type: source_asset.as_ref().map(|asset| asset.media_type.clone()),
|
||||
source_subtype: source_asset.as_ref().map(|asset| asset.kind.clone()),
|
||||
source_subtype: source_asset.as_ref().map(|asset| asset.kind.to_string()),
|
||||
producer_task_id: source_asset
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.source.task_id.clone()),
|
||||
@@ -3323,7 +3324,7 @@ mod tests {
|
||||
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "local-art-1".to_string(),
|
||||
kind: "art-spritesheet-slice".to_string(),
|
||||
kind: GameCreationAppAssetKind::Icon,
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/art-spritesheet-slices/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
@@ -3380,7 +3381,7 @@ mod tests {
|
||||
fn bridge_registered_resource_exposes_formal_sequence_without_private_generation_fields() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "animation-1".to_string(),
|
||||
kind: "character-animation".to_string(),
|
||||
kind: GameCreationAppAssetKind::CharacterAnimation,
|
||||
media_type: "video/mp4".to_string(),
|
||||
local_path: "assets/edits/animation.mp4".to_string(),
|
||||
image_sequence_frames: Some(vec![
|
||||
@@ -3431,7 +3432,7 @@ mod tests {
|
||||
fn bridge_registered_resource_keeps_explicit_manifest_classification() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "spec-1".to_string(),
|
||||
kind: "spec".to_string(),
|
||||
kind: GameCreationAppAssetKind::Spec,
|
||||
media_type: "application/json".to_string(),
|
||||
local_path: "assets/specs/hero.json".to_string(),
|
||||
image_sequence_frames: None,
|
||||
@@ -3467,13 +3468,13 @@ mod tests {
|
||||
///
|
||||
/// UI 栏目走 TS 的 `gameCreationAppAssetCategory`(落盘值 + 按 kind 派生 + 读时自愈),
|
||||
/// Agent 走 Rust 的同构实现 `game_creation_app_asset_effective_category`。直接透传落盘
|
||||
/// `category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
|
||||
/// `unclassified`——真机 122 条资产里有 55 条这样分叉。
|
||||
/// `category` 会让 `ui-design` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
|
||||
/// `unclassified`,两侧分类口径必须一致。
|
||||
#[test]
|
||||
fn bridge_registered_resource_projects_effective_category_not_raw_persisted_value() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: "ui-1".to_string(),
|
||||
kind: "ui".to_string(),
|
||||
id: "ui-design-1".to_string(),
|
||||
kind: GameCreationAppAssetKind::UiDesign,
|
||||
media_type: "application/json".to_string(),
|
||||
local_path: "assets/UI 设计 1.json".to_string(),
|
||||
image_sequence_frames: None,
|
||||
@@ -3496,7 +3497,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let projection = bridge_registered_resource(&asset, false);
|
||||
assert_eq!(projection["kind"], "ui");
|
||||
assert_eq!(projection["kind"], "ui-design");
|
||||
assert_eq!(projection["category"], "ui-interaction");
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,27 @@ pub(crate) fn normalize_platform_art_asset_generation_kind(kind: &str) -> Option
|
||||
})
|
||||
}
|
||||
|
||||
/// 平台美术生成请求的 `assetKind` → manifest 资源 kind。
|
||||
///
|
||||
/// `PlatformArtAssetGenerationOptions::asset_kind` 是**平台请求词汇**(`ui-prototype` /
|
||||
/// `art-spritesheet` / `game-background` / `game-art` …),跟 manifest 的正式
|
||||
/// `GameCreationAppAssetKind` 不是同一套词表。生成结果登记进 manifest 之前必须在这里显式
|
||||
/// 转换,否则 manifest 会落进非 canonical 值,读侧只能靠 shared-contracts 的别名表兜底。
|
||||
///
|
||||
/// 词表外的值不猜:返回 `Unknown`,由调用方按既有未知输入口径处理。
|
||||
pub(crate) fn platform_art_asset_manifest_kind(asset_kind: &str) -> GameCreationAppAssetKind {
|
||||
match asset_kind.trim() {
|
||||
"image" | "game-art" => GameCreationAppAssetKind::Image,
|
||||
"character" | "character-art" => GameCreationAppAssetKind::Character,
|
||||
"spec" | "icon-spec" => GameCreationAppAssetKind::IconSpec,
|
||||
"ui-prototype" | "ui-design" => GameCreationAppAssetKind::UiDesign,
|
||||
"art-spritesheet" | "icon-spritesheet" => GameCreationAppAssetKind::IconSpritesheet,
|
||||
"publication-material" => GameCreationAppAssetKind::PublicationMaterial,
|
||||
"game-background" | "scene" => GameCreationAppAssetKind::Scene,
|
||||
_ => GameCreationAppAssetKind::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn recover_persisted_visual_generation_options(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
@@ -1614,7 +1635,7 @@ async fn canonical_art_spec_reference_at(
|
||||
.iter()
|
||||
.find(|asset| {
|
||||
asset.local_path == AGENT_RUNTIME_ART_SPEC_PATH
|
||||
&& asset.kind == "icon-spec"
|
||||
&& asset.kind == GameCreationAppAssetKind::IconSpec
|
||||
&& asset.media_type.starts_with("image/")
|
||||
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
|
||||
})
|
||||
@@ -1638,7 +1659,7 @@ async fn canonical_art_spec_reference_at(
|
||||
&source.id,
|
||||
&format!("{:x}", Sha256::digest(&bytes)),
|
||||
&source.media_type,
|
||||
&source.kind,
|
||||
source.kind.as_str(),
|
||||
)?;
|
||||
if let Some(binding) = read_external_editor_resource_binding_at(
|
||||
root,
|
||||
@@ -6847,7 +6868,7 @@ fn register_platform_art_slice_manifest_entries_at(
|
||||
.iter_mut()
|
||||
.find(|asset| asset.local_path == registration.local_path)
|
||||
{
|
||||
existing.kind = "art-spritesheet-slice".to_string();
|
||||
existing.kind = GameCreationAppAssetKind::Icon;
|
||||
existing.media_type = registration.media_type.clone();
|
||||
existing.image_sequence_frames = None;
|
||||
existing.image_sequence_duration_ms = None;
|
||||
@@ -6861,12 +6882,14 @@ fn register_platform_art_slice_manifest_entries_at(
|
||||
);
|
||||
manifest.assets.push(GameCreationAppAssetManifestEntry {
|
||||
id: id.clone(),
|
||||
kind: "art-spritesheet-slice".to_string(),
|
||||
kind: GameCreationAppAssetKind::Icon,
|
||||
media_type: registration.media_type.clone(),
|
||||
local_path: registration.local_path.clone(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
category: game_creation_app_asset_category_for_kind("art-spritesheet-slice"),
|
||||
category: game_creation_app_asset_category_for_kind(
|
||||
GameCreationAppAssetKind::Icon.as_str(),
|
||||
),
|
||||
tags: Vec::new(),
|
||||
source,
|
||||
});
|
||||
@@ -6898,7 +6921,7 @@ fn existing_platform_art_slice_registrations_are_complete(
|
||||
return Ok(false);
|
||||
};
|
||||
if candidates.next().is_some()
|
||||
|| asset.kind != "art-spritesheet-slice"
|
||||
|| asset.kind != GameCreationAppAssetKind::Icon
|
||||
|| asset.media_type != "image/png"
|
||||
|| asset.source.kind != GameCreationAppAssetSourceKind::Canvas
|
||||
|| asset.source.generation_route.as_deref()
|
||||
@@ -6987,10 +7010,12 @@ pub(in crate::agent) fn register_existing_platform_art_slices_at(
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| "旧项目图集源资源缺少 sourceResourceId".to_string())?;
|
||||
let has_partial_or_invalid_registration = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.any(|asset| asset.kind == "art-spritesheet-slice");
|
||||
let has_partial_or_invalid_registration = manifest.assets.iter().any(|asset| {
|
||||
asset.kind == GameCreationAppAssetKind::Icon
|
||||
&& asset
|
||||
.local_path
|
||||
.starts_with("assets/art-spritesheet-slices/")
|
||||
});
|
||||
|
||||
let receipt_path =
|
||||
resolve_local_project_path(root, ".agent/runtime/art-spritesheet-contract.json")?;
|
||||
@@ -7600,7 +7625,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
let registered = match register_local_asset_entry(
|
||||
root,
|
||||
&local_path,
|
||||
&options.asset_kind,
|
||||
platform_art_asset_manifest_kind(&options.asset_kind),
|
||||
&download.media_type,
|
||||
"platform-art",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -11209,7 +11234,7 @@ mod canvas_generation_tests {
|
||||
register_local_asset_at(
|
||||
root,
|
||||
"assets/art-spec.png",
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"image/png",
|
||||
"changed-reference-test",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -12779,7 +12804,7 @@ mod canvas_generation_tests {
|
||||
let first_slice_ids = registered
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.kind == "art-spritesheet-slice")
|
||||
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
|
||||
.map(|asset| {
|
||||
assert_eq!(asset.source.kind, GameCreationAppAssetSourceKind::Canvas);
|
||||
assert_eq!(
|
||||
@@ -12813,7 +12838,7 @@ mod canvas_generation_tests {
|
||||
.clone();
|
||||
mutate_manifest_at(root, |manifest| {
|
||||
manifest.assets.retain(|asset| {
|
||||
asset.kind != "art-spritesheet-slice" || asset.local_path == retained_path
|
||||
asset.kind != GameCreationAppAssetKind::Icon || asset.local_path == retained_path
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
@@ -12827,7 +12852,7 @@ mod canvas_generation_tests {
|
||||
let backfilled_slice_ids = backfilled
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.kind == "art-spritesheet-slice")
|
||||
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
|
||||
.map(|asset| (asset.local_path.clone(), asset.id.clone()))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
assert_eq!(backfilled_slice_ids.len(), 4);
|
||||
@@ -12876,7 +12901,7 @@ mod canvas_generation_tests {
|
||||
let replaced_slice_ids = replaced
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.kind == "art-spritesheet-slice")
|
||||
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
|
||||
.map(|asset| {
|
||||
assert_eq!(
|
||||
asset.source.reference_resource_ids,
|
||||
@@ -12909,7 +12934,7 @@ mod canvas_generation_tests {
|
||||
register_local_asset_entry(
|
||||
root,
|
||||
AGENT_RUNTIME_ART_SPRITESHEET_PATH,
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"image/png",
|
||||
"platform-art",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -12970,12 +12995,12 @@ mod canvas_generation_tests {
|
||||
assert!(after
|
||||
.assets
|
||||
.iter()
|
||||
.all(|asset| asset.kind != "art-spritesheet-slice"));
|
||||
.all(|asset| asset.kind != GameCreationAppAssetKind::Icon));
|
||||
|
||||
register_local_asset_entry(
|
||||
root,
|
||||
"assets/art-spritesheet-slices/player.png",
|
||||
"art-spritesheet-slice",
|
||||
GameCreationAppAssetKind::Icon,
|
||||
"image/png",
|
||||
"platform-art",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -14531,7 +14556,10 @@ mod canvas_generation_tests {
|
||||
let manifest = read_manifest_for_project(root).expect("read replacement manifest");
|
||||
assert_eq!(manifest.assets.len(), 1);
|
||||
assert_eq!(manifest.assets[0].local_path, "assets/art-spritesheet.png");
|
||||
assert_eq!(manifest.assets[0].kind, "art-spritesheet");
|
||||
assert_eq!(
|
||||
manifest.assets[0].kind,
|
||||
GameCreationAppAssetKind::IconSpritesheet
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_dir(root.join("assets"))
|
||||
.expect("read assets directory")
|
||||
|
||||
@@ -98,7 +98,7 @@ pub(crate) fn render_local_asset_prompt_context(root: &Path) -> Result<String, S
|
||||
output.push_str("- ");
|
||||
output.push_str(&asset.id);
|
||||
output.push_str(": ");
|
||||
output.push_str(&asset.kind);
|
||||
output.push_str(asset.kind.as_str());
|
||||
output.push_str(" / ");
|
||||
output.push_str(&asset.media_type);
|
||||
output.push_str(" / ");
|
||||
|
||||
+12
-6
@@ -2061,17 +2061,23 @@ mod tests {
|
||||
.expect("resume first manifest task fixture");
|
||||
|
||||
for (local_path, kind) in [
|
||||
("assets/art-spec.png", "icon-spec"),
|
||||
("assets/ui-prototype.png", "ui-prototype"),
|
||||
("assets/art-spritesheet.png", "art-spritesheet"),
|
||||
("assets/art-spec.png", GameCreationAppAssetKind::IconSpec),
|
||||
(
|
||||
"assets/ui-prototype.png",
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
),
|
||||
(
|
||||
"assets/art-spritesheet.png",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
),
|
||||
] {
|
||||
let (generation_route, generation_kind, reference_resource_ids) = match kind {
|
||||
"icon-spec" => (
|
||||
GameCreationAppAssetKind::IconSpec => (
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Vec::new(),
|
||||
),
|
||||
"ui-prototype" => (
|
||||
GameCreationAppAssetKind::UiDesign => (
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"ui-design",
|
||||
vec!["resource-icon-spec".to_string()],
|
||||
@@ -2082,7 +2088,7 @@ mod tests {
|
||||
vec!["resource-icon-spec".to_string()],
|
||||
),
|
||||
};
|
||||
let bytes = if kind == "art-spritesheet" {
|
||||
let bytes = if kind == GameCreationAppAssetKind::IconSpritesheet {
|
||||
use image::ImageEncoder;
|
||||
let mut bytes = Vec::new();
|
||||
image::codecs::png::PngEncoder::new(&mut bytes)
|
||||
|
||||
@@ -1216,11 +1216,19 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
|
||||
return None;
|
||||
}
|
||||
let (expected_path, expected_kind, label) = match agent_id {
|
||||
"art-director" => (AGENT_RUNTIME_ART_SPEC_PATH, "icon-spec", "统一视觉规范图"),
|
||||
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"),
|
||||
"art-director" => (
|
||||
AGENT_RUNTIME_ART_SPEC_PATH,
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"统一视觉规范图",
|
||||
),
|
||||
"design-foundation" => (
|
||||
"assets/ui-prototype.png",
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
"策划界面原型图",
|
||||
),
|
||||
"art-asset-plan" => (
|
||||
"assets/art-spritesheet.png",
|
||||
"art-spritesheet",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"首版美术素材图",
|
||||
),
|
||||
_ => return None,
|
||||
|
||||
@@ -1561,10 +1561,14 @@ mod orphaned_external_generation_recovery_tests {
|
||||
fs::create_dir_all(workflow_path.parent().expect("workflow parent"))
|
||||
.expect("workflow directory");
|
||||
let rollback_assets = [
|
||||
("assets/art-spec.png", "icon-spec", b"old-spec".as_slice()),
|
||||
(
|
||||
"assets/art-spec.png",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
b"old-spec".as_slice(),
|
||||
),
|
||||
(
|
||||
"assets/direct-game-background.png",
|
||||
"game-background",
|
||||
GameCreationAppAssetKind::Scene,
|
||||
b"old-background".as_slice(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -451,7 +451,7 @@ mod asset_generation_task_tests {
|
||||
init_local_game_project_at, read_manifest_for_project, write_project_permission_policy_at,
|
||||
};
|
||||
use shared_contracts::game_creation_app::{
|
||||
GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
|
||||
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
|
||||
};
|
||||
|
||||
fn temp_project_root(label: &str) -> PathBuf {
|
||||
@@ -484,7 +484,7 @@ mod asset_generation_task_tests {
|
||||
register_local_asset_at(
|
||||
root,
|
||||
local_path,
|
||||
"icon-spec",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"image/png",
|
||||
"generated",
|
||||
GameCreationAppAssetSource {
|
||||
|
||||
@@ -478,7 +478,7 @@ pub(crate) fn external_editor_api_credentials_override_is_active() -> bool {
|
||||
/// 分类时才生效,`kind` 本身错时自愈只会把错值放大。
|
||||
///
|
||||
/// 判不出内容类型时返回中性的 `asset`(派生 `unclassified` → 「待归类」),**不猜具体类型**。
|
||||
fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str {
|
||||
fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAssetKind {
|
||||
let media_type = media_type.trim().to_ascii_lowercase();
|
||||
let extension = Path::new(file_name)
|
||||
.extension()
|
||||
@@ -492,20 +492,20 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str {
|
||||
"mp3" | "wav" | "ogg" | "m4a" | "aac" | "flac" | "opus"
|
||||
)
|
||||
{
|
||||
"audio"
|
||||
GameCreationAppAssetKind::Audio
|
||||
} else if media_type.starts_with("video/") || matches!(extension, "mp4" | "webm" | "mov") {
|
||||
"video"
|
||||
GameCreationAppAssetKind::Video
|
||||
} else if media_type.starts_with("font/")
|
||||
|| matches!(extension, "ttf" | "otf" | "woff" | "woff2")
|
||||
{
|
||||
"document"
|
||||
GameCreationAppAssetKind::Font
|
||||
} else if media_type.starts_with("image/")
|
||||
|| matches!(
|
||||
extension,
|
||||
"png" | "jpg" | "jpeg" | "webp" | "gif" | "svg" | "avif" | "bmp"
|
||||
)
|
||||
{
|
||||
"image"
|
||||
GameCreationAppAssetKind::Image
|
||||
} else if matches!(media_type.as_str(), "text/html" | "text/css")
|
||||
|| media_type.contains("javascript")
|
||||
|| media_type.contains("typescript")
|
||||
@@ -514,7 +514,7 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str {
|
||||
"html" | "htm" | "css" | "js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx"
|
||||
)
|
||||
{
|
||||
"code"
|
||||
GameCreationAppAssetKind::Code
|
||||
} else if media_type.starts_with("text/")
|
||||
|| matches!(media_type.as_str(), "application/json" | "application/xml")
|
||||
|| matches!(
|
||||
@@ -532,9 +532,9 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str {
|
||||
| "xml"
|
||||
)
|
||||
{
|
||||
"document"
|
||||
GameCreationAppAssetKind::Document
|
||||
} else {
|
||||
"asset"
|
||||
GameCreationAppAssetKind::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,7 +584,7 @@ pub(crate) fn upload_local_asset_at(
|
||||
pub(crate) fn register_local_asset_at(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
media_type: &str,
|
||||
source_kind: &str,
|
||||
source: GameCreationAppAssetSource,
|
||||
@@ -647,7 +647,7 @@ pub(crate) fn register_design_artifacts_at(root: &Path) -> Result<bool, String>
|
||||
let (_, asset_changed) = register_local_asset_entry_with_change(
|
||||
root,
|
||||
&relative,
|
||||
"document",
|
||||
GameCreationAppAssetKind::Document,
|
||||
media_type,
|
||||
"document",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -686,6 +686,7 @@ pub(crate) fn import_canvas_asset_at(
|
||||
if resource_id.is_none() && asset_object_id.is_none() {
|
||||
return Err("画板资源 ID 和资产对象 ID 至少需要一个".to_string());
|
||||
}
|
||||
let kind = GameCreationAppAssetKind::parse_with_context(kind.trim(), "canvas.asset_import");
|
||||
|
||||
register_local_asset_at(
|
||||
root,
|
||||
@@ -918,7 +919,12 @@ pub(crate) async fn sync_canvas_project_assets_at(
|
||||
&local_path,
|
||||
json_string_field(resource, "assetKind")
|
||||
.as_deref()
|
||||
.unwrap_or("canvas-resource"),
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| {
|
||||
GameCreationAppAssetKind::parse_with_context(value, "canvas.asset_sync")
|
||||
})
|
||||
.unwrap_or(GameCreationAppAssetKind::Image),
|
||||
&download.media_type,
|
||||
"canvas",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -1772,33 +1778,28 @@ pub(crate) fn normalized_zip_entry_name(name: &str) -> Result<String, String> {
|
||||
|
||||
/// 画板导出图层推断出的资源 kind。
|
||||
///
|
||||
/// 返回值必须是 **canonical kind**:这个值会被 `register_local_asset_entry` 原样写入
|
||||
/// manifest,并据以派生落盘 `category`。历史上这里写过非 canonical 的
|
||||
/// `ui` / `animation` / `asset`,只能靠别名表兜底,等于同时维护两套词汇;别名表只用于
|
||||
/// 兼容存量数据,不作为新写入值的来源。
|
||||
/// 直接返回正式枚举成员:这个值会被 `register_local_asset_entry` 原样写入 manifest,
|
||||
/// 并据以派生落盘 `category`。分支字面量写成非 canonical 值在这里已经无法表达,
|
||||
/// 不需要别名表兜底。
|
||||
pub(crate) fn infer_canvas_export_asset_kind(
|
||||
layer: &CanvasExportLayerMetadata,
|
||||
file: &str,
|
||||
) -> &'static str {
|
||||
) -> GameCreationAppAssetKind {
|
||||
let layer_type = layer.visible.layer_type.as_str();
|
||||
let inferred = if file.starts_with("sequences/")
|
||||
|| contains_any(layer_type, &["序列", "动画", "动作"])
|
||||
{
|
||||
"character-animation"
|
||||
if file.starts_with("sequences/") || contains_any(layer_type, &["序列", "动画", "动作"]) {
|
||||
GameCreationAppAssetKind::CharacterAnimation
|
||||
} else if file.starts_with("media/") || contains_any(layer_type, &["音频", "音乐", "音效"])
|
||||
{
|
||||
"audio"
|
||||
GameCreationAppAssetKind::Audio
|
||||
} else if contains_any(layer_type, &["角色"]) {
|
||||
"character"
|
||||
GameCreationAppAssetKind::Character
|
||||
} else if contains_any(layer_type, &["场景", "背景"]) {
|
||||
"scene"
|
||||
GameCreationAppAssetKind::Scene
|
||||
} else if contains_any(layer_type, &["UI", "界面", "图标"]) {
|
||||
"ui-design"
|
||||
GameCreationAppAssetKind::UiDesign
|
||||
} else {
|
||||
"image"
|
||||
};
|
||||
// 防御性归一:分支字面量写错时由覆盖测试暴露,这里再兜一层。
|
||||
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(inferred)
|
||||
GameCreationAppAssetKind::Image
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn infer_canvas_export_media_type(file: &str) -> &'static str {
|
||||
@@ -1871,7 +1872,7 @@ pub(crate) fn build_canvas_project_url(
|
||||
pub(crate) fn register_local_asset_entry(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
media_type: &str,
|
||||
id_prefix: &str,
|
||||
source: GameCreationAppAssetSource,
|
||||
@@ -1883,7 +1884,7 @@ pub(crate) fn register_local_asset_entry(
|
||||
fn register_local_asset_entry_with_change(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
media_type: &str,
|
||||
id_prefix: &str,
|
||||
source: GameCreationAppAssetSource,
|
||||
@@ -1891,7 +1892,6 @@ fn register_local_asset_entry_with_change(
|
||||
let normalized_path = normalize_relative_path(local_path)?;
|
||||
let absolute_path = resolve_local_project_path(root, &normalized_path)?;
|
||||
let manifest_path = root.join(".agent/manifest.json");
|
||||
let kind = if kind.is_empty() { "asset" } else { kind };
|
||||
let media_type = if media_type.is_empty() {
|
||||
"application/octet-stream"
|
||||
} else {
|
||||
@@ -1914,8 +1914,8 @@ fn register_local_asset_entry_with_change(
|
||||
|| existing.media_type != media_type
|
||||
|| existing.source != source;
|
||||
if existing.kind != kind {
|
||||
existing.kind = kind.to_string();
|
||||
existing.category = game_creation_app_asset_category_for_kind(kind);
|
||||
existing.kind = kind;
|
||||
existing.category = game_creation_app_asset_category_for_kind(kind.as_str());
|
||||
}
|
||||
existing.media_type = media_type.to_string();
|
||||
existing.source = source;
|
||||
@@ -1928,12 +1928,12 @@ fn register_local_asset_entry_with_change(
|
||||
);
|
||||
manifest.assets.push(GameCreationAppAssetManifestEntry {
|
||||
id: id.clone(),
|
||||
kind: kind.to_string(),
|
||||
kind,
|
||||
media_type: media_type.to_string(),
|
||||
local_path: normalized_path.clone(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
category: game_creation_app_asset_category_for_kind(kind),
|
||||
category: game_creation_app_asset_category_for_kind(kind.as_str()),
|
||||
tags: Vec::new(),
|
||||
source,
|
||||
});
|
||||
@@ -1946,7 +1946,7 @@ fn register_local_asset_entry_with_change(
|
||||
"recordType": record_type,
|
||||
"assetId": id.clone(),
|
||||
"localPath": normalized_path.clone(),
|
||||
"kind": kind,
|
||||
"kind": kind.as_str(),
|
||||
"mediaType": media_type,
|
||||
"source": source_for_record,
|
||||
}),
|
||||
@@ -2199,13 +2199,21 @@ mod tests {
|
||||
}
|
||||
|
||||
// 每个分支的代表输入 → 期望的 canonical kind。
|
||||
let cases: [(&str, &str, &str); 6] = [
|
||||
("序列", "sequences/01.png", "character-animation"),
|
||||
("动画", "layer.png", "character-animation"),
|
||||
("音频", "layer.png", "audio"),
|
||||
("角色", "layer.png", "character"),
|
||||
("场景", "layer.png", "scene"),
|
||||
("UI", "layer.png", "ui-design"),
|
||||
let cases: [(&str, &str, GameCreationAppAssetKind); 6] = [
|
||||
(
|
||||
"序列",
|
||||
"sequences/01.png",
|
||||
GameCreationAppAssetKind::CharacterAnimation,
|
||||
),
|
||||
(
|
||||
"动画",
|
||||
"layer.png",
|
||||
GameCreationAppAssetKind::CharacterAnimation,
|
||||
),
|
||||
("音频", "layer.png", GameCreationAppAssetKind::Audio),
|
||||
("角色", "layer.png", GameCreationAppAssetKind::Character),
|
||||
("场景", "layer.png", GameCreationAppAssetKind::Scene),
|
||||
("UI", "layer.png", GameCreationAppAssetKind::UiDesign),
|
||||
// 无匹配时落到 image。
|
||||
];
|
||||
for (layer_type, file, expected) in cases {
|
||||
@@ -2215,20 +2223,20 @@ mod tests {
|
||||
// media/ 前缀同样走音频分支。
|
||||
assert_eq!(
|
||||
infer_canvas_export_asset_kind(&layer("图层"), "media/bgm.mp3"),
|
||||
"audio"
|
||||
GameCreationAppAssetKind::Audio
|
||||
);
|
||||
// 兜底分支是 image 而不是旧的 "asset"。
|
||||
assert_eq!(
|
||||
infer_canvas_export_asset_kind(&layer("图层"), "layer.png"),
|
||||
"image"
|
||||
GameCreationAppAssetKind::Image
|
||||
);
|
||||
|
||||
// 所有分支的返回值都必须在 canonical 目录里,且不能是旧的非 canonical 写法。
|
||||
// 所有分支的返回值都必须是正式枚举成员,且不能是旧的非 canonical 写法。
|
||||
for layer_type in ["序列", "音频", "角色", "场景", "UI", "图层", "其他"] {
|
||||
let kind = infer_canvas_export_asset_kind(&layer(layer_type), "layer.png");
|
||||
assert!(
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_CANONICAL_ASSET_KINDS
|
||||
.contains(&kind),
|
||||
.contains(&kind.as_str()),
|
||||
"非 canonical kind: {kind}(layer_type={layer_type})"
|
||||
);
|
||||
for legacy in ["ui", "animation", "asset"] {
|
||||
@@ -2250,14 +2258,14 @@ mod tests {
|
||||
assert_eq!(uploaded_asset_kind("theme.mp3", "audio/mpeg"), "audio");
|
||||
assert_eq!(uploaded_asset_kind("intro.mp4", ""), "video");
|
||||
assert_eq!(uploaded_asset_kind("rules.md", "text/markdown"), "document");
|
||||
assert_eq!(uploaded_asset_kind("ui-font.ttf", "font/ttf"), "document");
|
||||
assert_eq!(uploaded_asset_kind("ui-font.ttf", "font/ttf"), "font");
|
||||
assert_eq!(uploaded_asset_kind("game.js", "text/javascript"), "code");
|
||||
// 判不出内容类型 → 中性 `asset`(派生 `unclassified` → 「待归类」),不猜具体类型。
|
||||
// 判不出内容类型 → `unknown`(派生 `unclassified` → 「待归类」),不猜具体类型。
|
||||
assert_eq!(
|
||||
uploaded_asset_kind("unknown.bin", "application/octet-stream"),
|
||||
"asset"
|
||||
"unknown"
|
||||
);
|
||||
assert_eq!(uploaded_asset_kind("no-extension", ""), "asset");
|
||||
assert_eq!(uploaded_asset_kind("no-extension", ""), "unknown");
|
||||
// 反查:正文里的"来源词"和"UI 默认值"都不得出现在返回值里。
|
||||
for (file_name, media_type) in [
|
||||
("hero.png", "image/png"),
|
||||
@@ -2269,8 +2277,11 @@ mod tests {
|
||||
assert_ne!(kind, "ui", "{file_name} 写回了非 canonical 的 `ui`");
|
||||
assert!(
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_CANONICAL_ASSET_KINDS
|
||||
.contains(&kind)
|
||||
|| kind == "asset",
|
||||
.contains(&kind.as_str())
|
||||
|| matches!(
|
||||
kind,
|
||||
GameCreationAppAssetKind::Font | GameCreationAppAssetKind::Unknown
|
||||
),
|
||||
"非 canonical kind: {kind}({file_name})"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2167,7 +2167,7 @@ pub(crate) fn register_local_asset(
|
||||
register_local_asset_at(
|
||||
root,
|
||||
local_path.trim(),
|
||||
kind.trim(),
|
||||
GameCreationAppAssetKind::parse_with_context(kind.trim(), "asset.register"),
|
||||
media_type.trim(),
|
||||
source_kind.trim(),
|
||||
GameCreationAppAssetSource {
|
||||
@@ -2200,7 +2200,10 @@ pub(crate) fn create_ui_design_resource(
|
||||
let next_index = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.kind == "UI")
|
||||
.filter(|asset| {
|
||||
asset.kind == crate::ui_editor::persistence::UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE
|
||||
})
|
||||
.count()
|
||||
+ 1;
|
||||
let resource_name = format!("UI 设计 {next_index}");
|
||||
@@ -2234,8 +2237,8 @@ pub(crate) fn create_ui_design_resource(
|
||||
let asset = match register_local_asset_at(
|
||||
root,
|
||||
&relative_path,
|
||||
"UI",
|
||||
"application/json",
|
||||
GameCreationAppAssetKind::UiDesignDoc,
|
||||
crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"generated",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
@@ -2725,7 +2728,7 @@ pub(crate) fn import_ui_editor_local_fonts(
|
||||
let registered = register_local_asset_entry(
|
||||
root,
|
||||
&relative_path,
|
||||
"font",
|
||||
GameCreationAppAssetKind::Font,
|
||||
validated.metadata.format.media_type(),
|
||||
"font",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -2837,7 +2840,7 @@ mod ui_editor_font_tests {
|
||||
let registered = register_local_asset_entry(
|
||||
root,
|
||||
relative_path,
|
||||
"font",
|
||||
GameCreationAppAssetKind::Font,
|
||||
"font/ttf",
|
||||
"font",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -2940,7 +2943,7 @@ mod ui_editor_font_tests {
|
||||
symlink(fixture_font_path(), &target).expect("create font symlink");
|
||||
let entry = GameCreationAppAssetManifestEntry {
|
||||
id: "font-linked".to_string(),
|
||||
kind: "font".to_string(),
|
||||
kind: GameCreationAppAssetKind::Font,
|
||||
media_type: "font/ttf".to_string(),
|
||||
local_path: relative_path.to_string(),
|
||||
image_sequence_frames: None,
|
||||
@@ -3255,24 +3258,34 @@ mod agent_asset_import_tests {
|
||||
/// 本用例必须变红(`character` / `scene` / 空值 / 未知值四组断言都会失败)。
|
||||
#[test]
|
||||
fn imported_platform_asset_kind_prefers_payload_type_over_ui_default() {
|
||||
assert_eq!(imported_platform_asset_kind(Some("character")), "character");
|
||||
assert_eq!(imported_platform_asset_kind(Some("scene")), "scene");
|
||||
use GameCreationAppAssetKind as Kind;
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some("character")),
|
||||
Kind::Character
|
||||
);
|
||||
assert_eq!(imported_platform_asset_kind(Some("scene")), Kind::Scene);
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some("character-animation")),
|
||||
"character-animation"
|
||||
Kind::CharacterAnimation
|
||||
);
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some("icon-spec")),
|
||||
Kind::IconSpec
|
||||
);
|
||||
assert_eq!(imported_platform_asset_kind(Some("icon-spec")), "icon-spec");
|
||||
// 平台确实说它是 UI 设计稿时,才允许落 `ui-design`(→ 「UI 交互」);
|
||||
// 大写 `UI` 与旧值 `ui` 都要归一到同一口径,但**不允许由我们替它默认**。
|
||||
assert_eq!(imported_platform_asset_kind(Some("UI")), "ui-design");
|
||||
assert_eq!(imported_platform_asset_kind(Some("ui")), "ui-design");
|
||||
assert_eq!(imported_platform_asset_kind(Some("UI")), Kind::UiDesign);
|
||||
assert_eq!(imported_platform_asset_kind(Some("ui")), Kind::UiDesign);
|
||||
// 缺失 / 空白 / 未知值一律落 canonical `image` 兜底(→ 「待归类」)。
|
||||
assert_eq!(imported_platform_asset_kind(None), "image");
|
||||
assert_eq!(imported_platform_asset_kind(Some(" ")), "image");
|
||||
assert_eq!(imported_platform_asset_kind(Some("mystery")), "image");
|
||||
assert_eq!(imported_platform_asset_kind(None), Kind::Image);
|
||||
assert_eq!(imported_platform_asset_kind(Some(" ")), Kind::Image);
|
||||
assert_eq!(imported_platform_asset_kind(Some("mystery")), Kind::Image);
|
||||
// `kind` 是外部输入,原型链键必须走 `image` 兜底,不能取到原型上的值。
|
||||
assert_eq!(imported_platform_asset_kind(Some("__proto__")), "image");
|
||||
assert_eq!(imported_platform_asset_kind(Some("constructor")), "image");
|
||||
assert_eq!(imported_platform_asset_kind(Some("__proto__")), Kind::Image);
|
||||
assert_eq!(
|
||||
imported_platform_asset_kind(Some("constructor")),
|
||||
Kind::Image
|
||||
);
|
||||
}
|
||||
|
||||
/// 反查门禁:两处平台素材导入必须继续用 `imported_platform_asset_kind` 解析 `kind`,
|
||||
@@ -3801,7 +3814,7 @@ pub(crate) async fn list_editor_assets_for_agent_at(
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct AgentLocalProjectFileType {
|
||||
category: &'static str,
|
||||
asset_kind: &'static str,
|
||||
asset_kind: GameCreationAppAssetKind,
|
||||
media_type: &'static str,
|
||||
max_file_size: u64,
|
||||
}
|
||||
@@ -3856,14 +3869,14 @@ fn agent_local_project_file_type(
|
||||
// 错值放大成显式的 `ui-interaction`。
|
||||
media_type.map(|media_type| AgentLocalProjectFileType {
|
||||
category: "image",
|
||||
asset_kind: "image",
|
||||
asset_kind: GameCreationAppAssetKind::Image,
|
||||
media_type,
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
})
|
||||
}
|
||||
"ttf" | "otf" | "woff" | "woff2" => Some(AgentLocalProjectFileType {
|
||||
category: "font",
|
||||
asset_kind: "font",
|
||||
asset_kind: GameCreationAppAssetKind::Font,
|
||||
media_type: match extension.as_str() {
|
||||
"ttf" => "font/ttf",
|
||||
"otf" => "font/otf",
|
||||
@@ -3874,68 +3887,68 @@ fn agent_local_project_file_type(
|
||||
}),
|
||||
"mp3" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/mpeg",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"wav" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/wav",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"ogg" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/ogg",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"flac" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/flac",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"m4a" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/mp4",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"aac" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/aac",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"opus" => Some(AgentLocalProjectFileType {
|
||||
category: "audio",
|
||||
asset_kind: "audio",
|
||||
asset_kind: GameCreationAppAssetKind::Audio,
|
||||
media_type: "audio/opus",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"mp4" => Some(AgentLocalProjectFileType {
|
||||
category: "video",
|
||||
asset_kind: "video",
|
||||
asset_kind: GameCreationAppAssetKind::Video,
|
||||
media_type: "video/mp4",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"webm" => Some(AgentLocalProjectFileType {
|
||||
category: "video",
|
||||
asset_kind: "video",
|
||||
asset_kind: GameCreationAppAssetKind::Video,
|
||||
media_type: "video/webm",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"mov" => Some(AgentLocalProjectFileType {
|
||||
category: "video",
|
||||
asset_kind: "video",
|
||||
asset_kind: GameCreationAppAssetKind::Video,
|
||||
media_type: "video/quicktime",
|
||||
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
|
||||
}),
|
||||
"md" | "markdown" | "mdx" | "txt" | "json" | "yaml" | "yml" | "toml" | "csv" | "ini"
|
||||
| "conf" | "xml" => Some(AgentLocalProjectFileType {
|
||||
category: "document",
|
||||
asset_kind: "document",
|
||||
asset_kind: GameCreationAppAssetKind::Document,
|
||||
media_type: if extension == "json" {
|
||||
"application/json"
|
||||
} else if matches!(extension.as_str(), "yaml" | "yml") {
|
||||
@@ -3952,7 +3965,7 @@ fn agent_local_project_file_type(
|
||||
| "swift" | "php" | "rb" | "lua" | "sh" | "bash" | "zsh" | "sql" | "graphql" | "gql"
|
||||
| "vue" | "svelte" => Some(AgentLocalProjectFileType {
|
||||
category: "code",
|
||||
asset_kind: "code",
|
||||
asset_kind: GameCreationAppAssetKind::Code,
|
||||
media_type: if matches!(extension.as_str(), "html" | "htm") {
|
||||
"text/html"
|
||||
} else if matches!(extension.as_str(), "css" | "scss" | "less") {
|
||||
@@ -4070,7 +4083,7 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
}
|
||||
inputs.push((
|
||||
local_path,
|
||||
file_type.asset_kind.to_string(),
|
||||
file_type.asset_kind,
|
||||
file_type.media_type.to_string(),
|
||||
bytes,
|
||||
));
|
||||
@@ -4089,7 +4102,7 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
imported.push(ImportedAsset {
|
||||
id: existing.id.clone(),
|
||||
local_path: existing.local_path.clone(),
|
||||
asset_kind: Some(existing.kind.clone()),
|
||||
asset_kind: Some(existing.kind.to_string()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -4128,7 +4141,7 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
let registered = register_local_asset_entry(
|
||||
root,
|
||||
&local_path,
|
||||
&asset_kind,
|
||||
asset_kind,
|
||||
&media_type,
|
||||
"local",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -4147,7 +4160,7 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
imported.push(ImportedAsset {
|
||||
id: registered.id,
|
||||
local_path: registered.local_path,
|
||||
asset_kind: Some(asset_kind),
|
||||
asset_kind: Some(asset_kind.to_string()),
|
||||
});
|
||||
advance_agent_runtime_project_revision_locked(root).map_err(|error| {
|
||||
format!("reconciliation-required: 本地资源已登记,但项目 revision 未能推进:{error}")
|
||||
@@ -4164,15 +4177,14 @@ pub(crate) fn import_local_project_assets_for_agent(
|
||||
/// 唯一一条"这东西是什么"的事实,并把角色立绘、怪物、道具、场景图一并钉进「UI 交互」栏
|
||||
/// (`ui → ui-design → ui-interaction`)。落盘 `category` 一旦是非 `unclassified` 值,
|
||||
/// 读时自愈永远救不回来,所以宁可写 `image`(待归类)也不能写假 `ui`。
|
||||
fn imported_platform_asset_kind(platform_kind: Option<&str>) -> String {
|
||||
fn imported_platform_asset_kind(platform_kind: Option<&str>) -> GameCreationAppAssetKind {
|
||||
match platform_kind.map(str::trim).filter(|kind| !kind.is_empty()) {
|
||||
// 平台侧词汇可能与 canonical 目录不完全一致(例如大写 `UI`),统一过 canonical 归一,
|
||||
// 保证写入侧只产出 canonical `kind`。
|
||||
Some(kind) => {
|
||||
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(kind)
|
||||
.to_string()
|
||||
}
|
||||
None => "image".to_string(),
|
||||
// 平台是外部系统,它的 `assetKind` 词汇(含大写 `UI` 这类变体)由共享契约统一收口到
|
||||
// canonical 值;这里只把收口结果转成正式枚举,写入侧不再产出裸字符串。
|
||||
Some(kind) => GameCreationAppAssetKind::from_str_lossy(
|
||||
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(kind),
|
||||
),
|
||||
None => GameCreationAppAssetKind::Image,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4280,7 +4292,7 @@ pub(crate) async fn import_account_editor_assets_for_agent(
|
||||
imported.push(ImportedAsset {
|
||||
id: existing.id.clone(),
|
||||
local_path: existing.local_path.clone(),
|
||||
asset_kind: Some(existing.kind.clone()),
|
||||
asset_kind: Some(existing.kind.to_string()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -4330,7 +4342,7 @@ pub(crate) async fn import_account_editor_assets_for_agent(
|
||||
let registered = register_local_asset_entry(
|
||||
root,
|
||||
&local_path,
|
||||
&asset_kind,
|
||||
asset_kind,
|
||||
&media_type,
|
||||
"canvas",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -4448,7 +4460,7 @@ pub(crate) async fn import_ui_editor_remote_assets(
|
||||
let registered = register_local_asset_entry(
|
||||
root,
|
||||
&local_path,
|
||||
&asset_kind,
|
||||
asset_kind,
|
||||
&media_type,
|
||||
"canvas",
|
||||
GameCreationAppAssetSource {
|
||||
|
||||
@@ -33,8 +33,8 @@ use shared_contracts::game_creation_app::{
|
||||
GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace,
|
||||
GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
|
||||
GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace,
|
||||
GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource,
|
||||
GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
|
||||
GameCreationAppAgentGroup, GameCreationAppAssetKind, GameCreationAppAssetManifestEntry,
|
||||
GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
|
||||
GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
|
||||
GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState,
|
||||
GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus,
|
||||
|
||||
@@ -1069,9 +1069,15 @@ pub(crate) fn validate_manifest_required_visual_asset(
|
||||
task_id: &str,
|
||||
) -> Result<(), String> {
|
||||
let (expected_path, expected_kind) = match task_id {
|
||||
"art-director" => ("assets/art-spec.png", "icon-spec"),
|
||||
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype"),
|
||||
"art-asset-plan" => ("assets/art-spritesheet.png", "art-spritesheet"),
|
||||
"art-director" => ("assets/art-spec.png", GameCreationAppAssetKind::IconSpec),
|
||||
"design-foundation" => (
|
||||
"assets/ui-prototype.png",
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
),
|
||||
"art-asset-plan" => (
|
||||
"assets/art-spritesheet.png",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let asset = manifest
|
||||
@@ -1141,7 +1147,10 @@ pub(crate) fn validate_manifest_required_visual_asset(
|
||||
let art_spec = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.local_path == "assets/art-spec.png" && asset.kind == "icon-spec")
|
||||
.find(|asset| {
|
||||
asset.local_path == "assets/art-spec.png"
|
||||
&& asset.kind == GameCreationAppAssetKind::IconSpec
|
||||
})
|
||||
.ok_or_else(|| "缺少当前统一视觉规范图".to_string())?;
|
||||
let art_spec_project_id = art_spec
|
||||
.source
|
||||
|
||||
@@ -19,7 +19,7 @@ fn project_with_two_assets(test_name: &str) -> (PathBuf, String) {
|
||||
let first = register_local_asset_entry(
|
||||
&root,
|
||||
"assets/hero.png",
|
||||
"character",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
"asset",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -39,7 +39,7 @@ fn project_with_two_assets(test_name: &str) -> (PathBuf, String) {
|
||||
register_local_asset_entry(
|
||||
&root,
|
||||
"assets/theme.mp3",
|
||||
"background-music",
|
||||
GameCreationAppAssetKind::BackgroundMusic,
|
||||
"audio/mpeg",
|
||||
"asset",
|
||||
GameCreationAppAssetSource {
|
||||
|
||||
@@ -96,7 +96,7 @@ fn manifest_task_and_asset_mutations_are_serialized_across_the_full_transaction(
|
||||
crate::assets::register_local_asset_entry(
|
||||
&second_root,
|
||||
"assets/concurrent.png",
|
||||
"ui-prototype",
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
"image/png",
|
||||
"test",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -180,7 +180,7 @@ fn successful_first_playable_registration_creates_one_initial_version_with_asset
|
||||
let mut manifest = new_game_creation_app_manifest("project-first-playable", "首板项目");
|
||||
manifest.assets.push(GameCreationAppAssetManifestEntry {
|
||||
id: "asset-player".to_string(),
|
||||
kind: "character".to_string(),
|
||||
kind: GameCreationAppAssetKind::Character,
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
|
||||
@@ -652,7 +652,7 @@ mod tests {
|
||||
) -> GameCreationAppAssetManifestEntry {
|
||||
GameCreationAppAssetManifestEntry {
|
||||
id: id.to_string(),
|
||||
kind: "test".to_string(),
|
||||
kind: GameCreationAppAssetKind::Unknown,
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: format!("assets/{id}.png"),
|
||||
image_sequence_frames: None,
|
||||
|
||||
@@ -180,6 +180,8 @@ pub(crate) struct DeriveLocalProjectResourceInput {
|
||||
pub(crate) source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) source_media_type: Option<String>,
|
||||
/// 源资源 subtype:只接受 manifest 的正式 `GameCreationAppAssetKind` 成员。
|
||||
/// 任务产物、附件、项目版本与 Agent 回执等非 manifest 来源不通过该字段传递。
|
||||
#[serde(default)]
|
||||
pub(crate) source_subtype: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -296,6 +298,7 @@ pub(crate) struct NormalizeLocalProjectRasterResourceInput {
|
||||
pub(crate) source_resource_id: String,
|
||||
pub(crate) source_path: String,
|
||||
pub(crate) source_media_type: String,
|
||||
/// 源资源 subtype:只接受 manifest 的正式 `GameCreationAppAssetKind` 成员。
|
||||
#[serde(default)]
|
||||
pub(crate) source_subtype: Option<String>,
|
||||
pub(crate) producer_task_id: String,
|
||||
@@ -309,12 +312,43 @@ pub(crate) struct NormalizeLocalProjectRasterResourceResult {
|
||||
pub(crate) manifest: GameCreationAppManifest,
|
||||
}
|
||||
|
||||
/// 资源编辑源身份的词汇表。
|
||||
///
|
||||
/// `Manifest` 只承载 manifest 的正式 `GameCreationAppAssetKind` 成员;项目版本快照与
|
||||
/// Agent 回执文本没有 manifest kind,单独成变体,避免它们混进 kind 字段后被静默收口成
|
||||
/// `unknown`。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ResourceEditSourceKind {
|
||||
Manifest(GameCreationAppAssetKind),
|
||||
ProjectVersion,
|
||||
AgentResultText,
|
||||
}
|
||||
|
||||
impl ResourceEditSourceKind {
|
||||
/// 私有账本与外部接口上下文使用的稳定标签。
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Manifest(kind) => kind.as_str(),
|
||||
Self::ProjectVersion => "project-version",
|
||||
Self::AgentResultText => "agent-result-derivative",
|
||||
}
|
||||
}
|
||||
|
||||
/// 派生资源写回 manifest 时使用的正式 kind;非 manifest 谱系没有 kind。
|
||||
fn manifest_kind(self) -> GameCreationAppAssetKind {
|
||||
match self {
|
||||
Self::Manifest(kind) => kind,
|
||||
Self::ProjectVersion | Self::AgentResultText => GameCreationAppAssetKind::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ResourceEditSourceSnapshot {
|
||||
canonical_resource_id: String,
|
||||
source_path: Option<String>,
|
||||
media_type: String,
|
||||
asset_kind: String,
|
||||
asset_kind: ResourceEditSourceKind,
|
||||
source_sha256: String,
|
||||
bytes: Option<Vec<u8>>,
|
||||
generation_mode: LocalProjectResourceGenerationMode,
|
||||
@@ -1125,19 +1159,28 @@ fn infer_resource_edit_source_media_type(
|
||||
Some(media_type.to_string())
|
||||
}
|
||||
|
||||
fn infer_resource_edit_source_asset_kind(edit_kind: &LocalProjectResourceEditKind) -> String {
|
||||
match edit_kind {
|
||||
LocalProjectResourceEditKind::ImageReference => "art-image",
|
||||
LocalProjectResourceEditKind::Svg => "svg",
|
||||
LocalProjectResourceEditKind::CharacterAnimation => "character-animation",
|
||||
LocalProjectResourceEditKind::Video => "video",
|
||||
LocalProjectResourceEditKind::SoundEffect => "sound-effect",
|
||||
LocalProjectResourceEditKind::BackgroundMusic => "background-music",
|
||||
LocalProjectResourceEditKind::Text => "text",
|
||||
LocalProjectResourceEditKind::AgentResult => "agent-result-derivative",
|
||||
LocalProjectResourceEditKind::Version => "project-version",
|
||||
}
|
||||
.to_string()
|
||||
fn infer_resource_edit_source_asset_kind(
|
||||
edit_kind: &LocalProjectResourceEditKind,
|
||||
) -> ResourceEditSourceKind {
|
||||
let kind = match edit_kind {
|
||||
LocalProjectResourceEditKind::ImageReference | LocalProjectResourceEditKind::Svg => {
|
||||
GameCreationAppAssetKind::Image
|
||||
}
|
||||
LocalProjectResourceEditKind::CharacterAnimation => {
|
||||
GameCreationAppAssetKind::CharacterAnimation
|
||||
}
|
||||
LocalProjectResourceEditKind::Video => GameCreationAppAssetKind::Video,
|
||||
LocalProjectResourceEditKind::SoundEffect => GameCreationAppAssetKind::SoundEffect,
|
||||
LocalProjectResourceEditKind::BackgroundMusic => GameCreationAppAssetKind::BackgroundMusic,
|
||||
LocalProjectResourceEditKind::Text => GameCreationAppAssetKind::Document,
|
||||
LocalProjectResourceEditKind::AgentResult => {
|
||||
return ResourceEditSourceKind::AgentResultText;
|
||||
}
|
||||
LocalProjectResourceEditKind::Version => {
|
||||
return ResourceEditSourceKind::ProjectVersion;
|
||||
}
|
||||
};
|
||||
ResourceEditSourceKind::Manifest(kind)
|
||||
}
|
||||
|
||||
fn resolve_agent_result_source(
|
||||
@@ -1183,7 +1226,7 @@ fn resolve_resource_edit_source(
|
||||
canonical_resource_id: format!("version:{version_id}"),
|
||||
source_path: None,
|
||||
media_type: "application/vnd.genarrative.project-version+json".to_string(),
|
||||
asset_kind: "project-version".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::ProjectVersion,
|
||||
source_sha256: sha256_hex(
|
||||
&serde_json::to_vec(&version)
|
||||
.map_err(|error| format!("序列化源项目版本失败:{error}"))?,
|
||||
@@ -1208,7 +1251,7 @@ fn resolve_resource_edit_source(
|
||||
canonical_resource_id,
|
||||
source_path: None,
|
||||
media_type: "text/markdown".to_string(),
|
||||
asset_kind: "agent-result-derivative".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::AgentResultText,
|
||||
source_sha256: sha256_hex(content.as_bytes()),
|
||||
bytes: None,
|
||||
generation_mode: input.generation_mode,
|
||||
@@ -1302,9 +1345,18 @@ fn resolve_resource_edit_source(
|
||||
.unwrap_or_default();
|
||||
let asset_kind = source_asset
|
||||
.as_ref()
|
||||
.map(|asset| asset.kind.clone())
|
||||
.or_else(|| input.source_subtype.clone())
|
||||
.unwrap_or_else(|| "asset".to_string());
|
||||
.map(|asset| ResourceEditSourceKind::Manifest(asset.kind))
|
||||
.or_else(|| {
|
||||
input.source_subtype.as_deref().map(|value| {
|
||||
ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::parse_with_context(
|
||||
value,
|
||||
"resource-edit.source-subtype",
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or(ResourceEditSourceKind::Manifest(
|
||||
GameCreationAppAssetKind::Unknown,
|
||||
));
|
||||
let canonical_resource_id = source_asset
|
||||
.as_ref()
|
||||
.map(source_asset_canonical_resource_id)
|
||||
@@ -1363,7 +1415,7 @@ fn resolve_resource_edit_source(
|
||||
if matches!(
|
||||
input.edit_kind,
|
||||
LocalProjectResourceEditKind::SoundEffect | LocalProjectResourceEditKind::BackgroundMusic
|
||||
) && resource_edit_audio_kind(&asset_kind, &path) != input.edit_kind
|
||||
) && resource_edit_audio_kind(asset_kind.as_str(), &path) != input.edit_kind
|
||||
{
|
||||
return Err("音频资源的音效/BGM 编辑类型与源资源用途不一致".to_string());
|
||||
}
|
||||
@@ -1818,7 +1870,7 @@ async fn confirm_resource_edit_source(
|
||||
"contentType": source.media_type,
|
||||
"contentLength": bytes.len(),
|
||||
"contentHash": source.source_sha256,
|
||||
"assetKind": source.asset_kind,
|
||||
"assetKind": source.asset_kind.as_str(),
|
||||
"accessPolicy": "private",
|
||||
})),
|
||||
)
|
||||
@@ -1926,7 +1978,7 @@ async fn register_resource_edit_source_image(
|
||||
"width": decoded.width(),
|
||||
"height": decoded.height(),
|
||||
"sourceType": "uploaded",
|
||||
"assetKind": source.asset_kind,
|
||||
"assetKind": source.asset_kind.as_str(),
|
||||
"generationInputs": {
|
||||
"source": RESOURCE_EDIT_QUEUE_SOURCE,
|
||||
"localAssetId": local_asset_id,
|
||||
@@ -2039,7 +2091,7 @@ async fn ensure_resource_edit_source_reference(
|
||||
local_asset_id,
|
||||
&source.source_sha256,
|
||||
&source.media_type,
|
||||
&source.asset_kind,
|
||||
source.asset_kind.as_str(),
|
||||
)?;
|
||||
let principal_key =
|
||||
external_editor_project_binding_key_sha256(&input.expected_project_id, principal)?;
|
||||
@@ -2261,7 +2313,7 @@ fn build_resource_edit_audio_prompt(
|
||||
.and_then(|asset| asset.source.prompt.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let mut source_context = format!("名称 {source_name},用途 {}", source.asset_kind);
|
||||
let mut source_context = format!("名称 {source_name},用途 {}", source.asset_kind.as_str());
|
||||
if let Some(original_prompt) = original_prompt {
|
||||
source_context.push_str(",原始描述 ");
|
||||
source_context.push_str(original_prompt);
|
||||
@@ -3797,20 +3849,24 @@ pub(crate) fn normalize_local_project_raster_resource_at(
|
||||
.map_err(|error| format!("序列化源图片身份失败:{error}"))?;
|
||||
let identity_hash = sha256_hex(&identity_material);
|
||||
let asset_id = format!("normalized-{}", &identity_hash[..24]);
|
||||
let source_subtype = input
|
||||
// 该接口只把栅格源登记成 manifest 图片资产:源 subtype 只接受正式枚举成员,
|
||||
// 未声明或非图片族成员时按图片收口。
|
||||
let source_kind = input
|
||||
.source_subtype
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty() && *value != "task-artifact")
|
||||
.unwrap_or("art-image");
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit.normalize"))
|
||||
.filter(|kind| *kind != GameCreationAppAssetKind::Unknown)
|
||||
.unwrap_or(GameCreationAppAssetKind::Image);
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: asset_id.clone(),
|
||||
kind: source_subtype.to_string(),
|
||||
kind: source_kind,
|
||||
media_type: verified_media_type,
|
||||
local_path: source_path,
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
category: game_creation_app_asset_category_for_kind(source_subtype),
|
||||
category: game_creation_app_asset_category_for_kind(source_kind.as_str()),
|
||||
tags: Vec::new(),
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
@@ -4051,11 +4107,11 @@ fn commit_resource_edit_asset_internal(
|
||||
.transpose()?;
|
||||
let image_sequence_duration_ms = ledger.remote_sequence_duration_ms;
|
||||
let asset_kind = if input.edit_kind == LocalProjectResourceEditKind::CharacterAnimation {
|
||||
"character-animation".to_string()
|
||||
GameCreationAppAssetKind::CharacterAnimation
|
||||
} else {
|
||||
source.asset_kind.clone()
|
||||
source.asset_kind.manifest_kind()
|
||||
};
|
||||
let category = game_creation_app_asset_category_for_kind(&asset_kind);
|
||||
let category = game_creation_app_asset_category_for_kind(asset_kind.as_str());
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: asset_id.clone(),
|
||||
kind: asset_kind,
|
||||
@@ -5147,8 +5203,14 @@ pub(crate) async fn resume_local_project_resource_edit_at(
|
||||
let source_subtype = ledger
|
||||
.source_asset_kind
|
||||
.clone()
|
||||
.or_else(|| source_asset.as_ref().map(|asset| asset.kind.clone()))
|
||||
.or_else(|| Some(infer_resource_edit_source_asset_kind(&ledger.edit_kind)));
|
||||
.or_else(|| source_asset.as_ref().map(|asset| asset.kind.to_string()))
|
||||
.or_else(|| {
|
||||
Some(
|
||||
infer_resource_edit_source_asset_kind(&ledger.edit_kind)
|
||||
.as_str()
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
derive_local_project_resource_at(DeriveLocalProjectResourceInput {
|
||||
project_path: input.project_path,
|
||||
expected_project_id: input.expected_project_id,
|
||||
@@ -5247,7 +5309,7 @@ pub(crate) async fn derive_local_project_resource_at(
|
||||
source_asset_id: source.source_asset.as_ref().map(|asset| asset.id.clone()),
|
||||
source_path: source.source_path.clone(),
|
||||
source_media_type: Some(source.media_type.clone()),
|
||||
source_asset_kind: Some(source.asset_kind.clone()),
|
||||
source_asset_kind: Some(source.asset_kind.as_str().to_string()),
|
||||
producer_task_id: input.producer_task_id.clone().or_else(|| {
|
||||
source
|
||||
.source_asset
|
||||
@@ -5966,7 +6028,7 @@ mod tests {
|
||||
source_asset_id: source.source_asset.as_ref().map(|asset| asset.id.clone()),
|
||||
source_path: source.source_path.clone(),
|
||||
source_media_type: Some(source.media_type.clone()),
|
||||
source_asset_kind: Some(source.asset_kind.clone()),
|
||||
source_asset_kind: Some(source.asset_kind.as_str().to_string()),
|
||||
producer_task_id: input.producer_task_id.clone().or_else(|| {
|
||||
source
|
||||
.source_asset
|
||||
@@ -6231,7 +6293,7 @@ mod tests {
|
||||
canonical_resource_id: "local-asset:video-1".to_string(),
|
||||
source_path: Some("assets/source-video.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(vec![1, 2, 3]),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -6452,7 +6514,7 @@ mod tests {
|
||||
&uploaded.id,
|
||||
&source.source_sha256,
|
||||
&source.media_type,
|
||||
&source.asset_kind,
|
||||
source.asset_kind.as_str(),
|
||||
)
|
||||
.expect("owner A source identity");
|
||||
let (a_resource_id, a_width, a_height) =
|
||||
@@ -6599,7 +6661,7 @@ mod tests {
|
||||
first_request.source_asset_id = Some(source_asset.id.clone());
|
||||
first_request.source_path = Some(source_asset.local_path.clone());
|
||||
first_request.source_media_type = Some(source_asset.media_type.clone());
|
||||
first_request.source_subtype = Some(source_asset.kind.clone());
|
||||
first_request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let mut second_request = first_request.clone();
|
||||
second_request.operation_id = Uuid::new_v4().to_string();
|
||||
second_request.idempotency_key = Uuid::new_v4().to_string();
|
||||
@@ -6692,7 +6754,7 @@ mod tests {
|
||||
&source_asset.id,
|
||||
&source.source_sha256,
|
||||
&source.media_type,
|
||||
&source.asset_kind,
|
||||
source.asset_kind.as_str(),
|
||||
)
|
||||
.expect("concurrent source identity");
|
||||
let binding_key = external_editor_resource_binding_key_sha256(
|
||||
@@ -6746,7 +6808,7 @@ mod tests {
|
||||
request.source_asset_id = Some(source_asset.id.clone());
|
||||
request.source_path = Some(source_asset.local_path.clone());
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.source_subtype = Some(source_asset.kind.clone());
|
||||
request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let source = resolve_resource_edit_source(root, &manifest, &request)
|
||||
.expect("resolve character source");
|
||||
let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::Prepared);
|
||||
@@ -6780,7 +6842,7 @@ mod tests {
|
||||
&source_asset.id,
|
||||
&source.source_sha256,
|
||||
&source.media_type,
|
||||
&source.asset_kind,
|
||||
source.asset_kind.as_str(),
|
||||
)
|
||||
.expect("character source identity");
|
||||
write_external_editor_resource_binding_at(
|
||||
@@ -6870,7 +6932,7 @@ mod tests {
|
||||
request.source_asset_id = Some(source_asset.id.clone());
|
||||
request.source_path = Some(source_asset.local_path.clone());
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.source_subtype = Some(source_asset.kind.clone());
|
||||
request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let source =
|
||||
resolve_resource_edit_source(root, &manifest, &request).expect("resolve switch source");
|
||||
let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::Prepared);
|
||||
@@ -7041,7 +7103,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/accepted-switch-video.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: request.generation_mode,
|
||||
@@ -7099,7 +7161,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/video-one.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -7239,7 +7301,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/legacy-video.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -7412,7 +7474,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/identity-switch.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: None,
|
||||
generation_mode: request.generation_mode,
|
||||
@@ -7480,7 +7542,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/resource-identity-owner.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: None,
|
||||
generation_mode: request.generation_mode,
|
||||
@@ -7587,7 +7649,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/resource-owner-lease.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: None,
|
||||
generation_mode: request.generation_mode,
|
||||
@@ -7723,7 +7785,7 @@ mod tests {
|
||||
request.source_asset_id = Some(source_asset.id.clone());
|
||||
request.source_path = Some(source_asset.local_path.clone());
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.source_subtype = Some(source_asset.kind.clone());
|
||||
request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let source = resolve_resource_edit_source(root, &manifest, &request)
|
||||
.expect("resolve unowned source");
|
||||
let prompt = normalize_resource_edit_prompt(&request.edit_kind, &request.prompt)
|
||||
@@ -7785,7 +7847,7 @@ mod tests {
|
||||
canonical_resource_id: "local-asset:video-submission-status".to_string(),
|
||||
source_path: Some("assets/video-submission-status.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -7912,7 +7974,7 @@ mod tests {
|
||||
canonical_resource_id: "local-asset:video-accepted-response".to_string(),
|
||||
source_path: Some("assets/video-accepted-response.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -8078,7 +8140,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/video-terminal-failure.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -8230,7 +8292,7 @@ mod tests {
|
||||
canonical_resource_id: request.source_resource_id.clone(),
|
||||
source_path: Some("assets/archive-owner-video.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: request.generation_mode,
|
||||
@@ -8290,7 +8352,7 @@ mod tests {
|
||||
canonical_resource_id,
|
||||
source_path: Some("assets/pending-owner.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: None,
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -8346,7 +8408,7 @@ mod tests {
|
||||
canonical_resource_id: local_request.source_resource_id.clone(),
|
||||
source_path: Some("assets/pending-local.md".to_string()),
|
||||
media_type: "text/markdown".to_string(),
|
||||
asset_kind: "text".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Document),
|
||||
source_sha256: "b".repeat(64),
|
||||
bytes: Some(b"local".to_vec()),
|
||||
generation_mode: local_request.generation_mode,
|
||||
@@ -8437,7 +8499,7 @@ mod tests {
|
||||
canonical_resource_id: "local-asset:audio-1".to_string(),
|
||||
source_path: Some("audio/theme.ogg".to_string()),
|
||||
media_type: "audio/ogg".to_string(),
|
||||
asset_kind: "background-music".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::BackgroundMusic),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(vec![1]),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -8535,7 +8597,7 @@ mod tests {
|
||||
canonical_resource_id: "local-asset:media-1".to_string(),
|
||||
source_path: Some("assets/source.mp4".to_string()),
|
||||
media_type: "video/mp4".to_string(),
|
||||
asset_kind: "video".to_string(),
|
||||
asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video),
|
||||
source_sha256: "a".repeat(64),
|
||||
bytes: Some(b"\0\0\0\x18ftypisom".to_vec()),
|
||||
generation_mode: LocalProjectResourceGenerationMode::Derive,
|
||||
@@ -9258,7 +9320,7 @@ mod tests {
|
||||
request.source_asset_id = Some(source_asset.id.clone());
|
||||
request.source_path = Some(source_asset.local_path.clone());
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.source_subtype = Some(source_asset.kind.clone());
|
||||
request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let source = resolve_resource_edit_source(
|
||||
root,
|
||||
&read_existing_manifest_for_project(root).expect("reread manifest"),
|
||||
@@ -9319,7 +9381,7 @@ mod tests {
|
||||
request.source_asset_id = Some(source_asset.id.clone());
|
||||
request.source_path = Some(source_asset.local_path.clone());
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.source_subtype = Some(source_asset.kind.clone());
|
||||
request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let source =
|
||||
resolve_resource_edit_source(root, &manifest, &request).expect("resolve commit source");
|
||||
let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::MediaDownloaded);
|
||||
@@ -9432,7 +9494,7 @@ mod tests {
|
||||
request.source_asset_id = Some(source_asset.id.clone());
|
||||
request.source_path = Some(source_asset.local_path.clone());
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.source_subtype = Some(source_asset.kind.clone());
|
||||
request.source_subtype = Some(source_asset.kind.to_string());
|
||||
let current_source = resolve_resource_edit_source(root, &manifest, &request)
|
||||
.expect("resolve current local source");
|
||||
assert_eq!(
|
||||
|
||||
@@ -35,7 +35,7 @@ fn register_asset_delete_fixture_asset(root: &Path, relative_path: &str, task_id
|
||||
register_local_asset_at(
|
||||
root,
|
||||
relative_path,
|
||||
"character",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
"generated",
|
||||
generated_asset_source(task_id),
|
||||
|
||||
@@ -9,7 +9,7 @@ fn asset_rename_project_fixture() -> (PathBuf, String) {
|
||||
let asset = register_local_asset_at(
|
||||
&root,
|
||||
"assets/hero.png",
|
||||
"character",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
"canvas",
|
||||
asset_rename_source(),
|
||||
@@ -192,7 +192,7 @@ fn rename_local_project_asset_rejects_extension_changes() {
|
||||
let extensionless = register_local_asset_at(
|
||||
&root,
|
||||
"assets/notes",
|
||||
"asset",
|
||||
GameCreationAppAssetKind::Document,
|
||||
"text/plain",
|
||||
"canvas",
|
||||
asset_rename_source(),
|
||||
|
||||
@@ -1401,7 +1401,11 @@ async fn canvas_replacement_rejects_parent_run_that_terminates_during_external_r
|
||||
);
|
||||
init_local_game_project_at(&root, "project-canvas-repair-atomic", "月光厨房")
|
||||
.expect("project init");
|
||||
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
|
||||
register_canvas_visual_asset_fixture(
|
||||
&root,
|
||||
"assets/art-spec.png",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
);
|
||||
bind_canvas_visual_asset_fixture_to_current_editor(
|
||||
&root,
|
||||
"assets/art-spec.png",
|
||||
|
||||
@@ -3602,28 +3602,32 @@ fn spawn_mock_external_canvas_generation_failure_server() -> String {
|
||||
base_url
|
||||
}
|
||||
|
||||
fn register_canvas_visual_asset_fixture(root: &Path, local_path: &str, kind: &str) {
|
||||
fn register_canvas_visual_asset_fixture(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
) {
|
||||
let absolute_path = root.join(local_path);
|
||||
fs::create_dir_all(absolute_path.parent().expect("visual asset parent"))
|
||||
.expect("create visual asset fixture directory");
|
||||
let bytes = if kind == "art-spritesheet" {
|
||||
let bytes = if kind == GameCreationAppAssetKind::IconSpritesheet {
|
||||
transparent_test_png_bytes()
|
||||
} else {
|
||||
valid_test_png_bytes()
|
||||
};
|
||||
fs::write(&absolute_path, bytes).expect("write visual asset fixture");
|
||||
let (generation_route, generation_kind, reference_resource_ids) = match kind {
|
||||
"icon-spec" => (
|
||||
GameCreationAppAssetKind::IconSpec => (
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"spec",
|
||||
Vec::new(),
|
||||
),
|
||||
"ui-prototype" => (
|
||||
GameCreationAppAssetKind::UiDesign => (
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"ui-design",
|
||||
vec!["resource-icon-spec".to_string()],
|
||||
),
|
||||
"art-spritesheet" => (
|
||||
GameCreationAppAssetKind::IconSpritesheet => (
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
vec!["resource-icon-spec".to_string()],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user