Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d0dd2d721 | |||
| 2befaaef9d | |||
| 20b0bf4dd7 | |||
| b4fd8d9b8b | |||
| fe85fa2a62 | |||
| b14a533b17 | |||
| 42969b2218 | |||
| c4ee75a1af | |||
| 6f2137cbb0 | |||
| 70a2246e8b | |||
| b7e3dac661 | |||
| 2e4a1996c8 | |||
| fe4e952853 | |||
| 18654b6806 | |||
| ee7d00c0b1 | |||
| 9cd1a94369 | |||
| 470e85c0ff |
+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,4 +1,8 @@
|
||||
use super::*;
|
||||
use crate::ui_editor::persistence::{
|
||||
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
|
||||
pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32;
|
||||
const MAX_DIRECT_CODEX_REFERENCE_ID_CHARS: usize = 200;
|
||||
@@ -133,7 +137,35 @@ fn validate_resource_reference_id(value: &str) -> Result<String, String> {
|
||||
Ok(resource_id.to_string())
|
||||
}
|
||||
|
||||
/// Render the prompt context for a UI design asset.
|
||||
///
|
||||
/// Keep this separate from the generic resource renderer so UI-specific
|
||||
/// instructions/metadata can evolve without changing other asset kinds.
|
||||
fn render_ui_design_reference_line(
|
||||
root: &Path,
|
||||
manifest: &GameCreationAppManifest,
|
||||
asset: &GameCreationAppAssetManifestEntry,
|
||||
resource_id: &str,
|
||||
label: &str,
|
||||
local_path: &str,
|
||||
source: &str,
|
||||
) -> String {
|
||||
let context = 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}"),
|
||||
};
|
||||
format!(
|
||||
"- 素材 ID:{resource_id};名称:{label};类型:{};媒体类型:{};项目路径:{local_path};来源:{source}",
|
||||
asset.kind, asset.media_type
|
||||
) + "\n" + &context
|
||||
}
|
||||
|
||||
fn render_resource_reference_line(
|
||||
root: &Path,
|
||||
manifest: &GameCreationAppManifest,
|
||||
reference: &DirectCodexResourceReference,
|
||||
) -> Result<String, String> {
|
||||
@@ -149,6 +181,19 @@ fn render_resource_reference_line(
|
||||
.unwrap_or_else(|| asset_display_label(asset));
|
||||
let source = sanitize_reference_source(reference.source.as_deref())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let is_ui_design =
|
||||
asset.kind == UI_DESIGN_DOC_ASSET_KIND && asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE;
|
||||
if is_ui_design {
|
||||
return Ok(render_ui_design_reference_line(
|
||||
root,
|
||||
manifest,
|
||||
asset,
|
||||
&resource_id,
|
||||
&label,
|
||||
&local_path,
|
||||
&source,
|
||||
));
|
||||
}
|
||||
Ok(format!(
|
||||
"- 素材 ID:{resource_id};名称:{label};类型:{};媒体类型:{};项目路径:{local_path};来源:{source}",
|
||||
asset.kind, asset.media_type
|
||||
@@ -234,7 +279,7 @@ pub(crate) fn render_direct_codex_references_section(
|
||||
for reference in references {
|
||||
lines.push(match reference {
|
||||
DirectCodexTurnReference::Resource(reference) => {
|
||||
render_resource_reference_line(&manifest, reference)?
|
||||
render_resource_reference_line(root, &manifest, reference)?
|
||||
}
|
||||
DirectCodexTurnReference::RuntimeRegion(reference) => {
|
||||
render_runtime_region_reference_line(&manifest, reference)?
|
||||
|
||||
@@ -2020,7 +2020,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
|
||||
@@ -2546,7 +2546,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) =
|
||||
|
||||
@@ -1169,7 +1169,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,
|
||||
@@ -1828,7 +1828,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()),
|
||||
|
||||
@@ -1649,7 +1649,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,
|
||||
@@ -6801,7 +6801,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;
|
||||
@@ -6815,7 +6815,7 @@ 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,
|
||||
|
||||
@@ -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(" / ");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ pub(crate) fn upload_local_asset_at(
|
||||
register_local_asset_entry(
|
||||
root,
|
||||
&relative_path,
|
||||
uploaded_asset_kind(file_name, media_type),
|
||||
uploaded_asset_kind(file_name, media_type).as_str(),
|
||||
media_type,
|
||||
"upload",
|
||||
GameCreationAppAssetSource {
|
||||
@@ -1879,7 +1879,10 @@ pub(crate) fn register_local_asset_entry(
|
||||
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 kind = GameCreationAppAssetKind::parse_with_context(
|
||||
if kind.is_empty() { "unknown" } else { kind },
|
||||
"register_local_asset_entry",
|
||||
);
|
||||
let media_type = if media_type.is_empty() {
|
||||
"application/octet-stream"
|
||||
} else {
|
||||
@@ -1899,8 +1902,8 @@ pub(crate) fn register_local_asset_entry(
|
||||
// 时才触发),于是这个资产永远停在错误栏目。
|
||||
// kind 没变时刻意不动 category——落盘分类是权威值,同 kind 重登记不得抹掉它。
|
||||
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;
|
||||
@@ -1913,12 +1916,12 @@ pub(crate) fn register_local_asset_entry(
|
||||
);
|
||||
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,
|
||||
});
|
||||
@@ -1931,7 +1934,7 @@ pub(crate) fn register_local_asset_entry(
|
||||
"recordType": record_type,
|
||||
"assetId": id.clone(),
|
||||
"localPath": normalized_path.clone(),
|
||||
"kind": kind,
|
||||
"kind": kind.as_str(),
|
||||
"mediaType": media_type,
|
||||
"source": source_for_record,
|
||||
}),
|
||||
|
||||
@@ -2144,7 +2144,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}");
|
||||
@@ -2178,8 +2181,8 @@ pub(crate) fn create_ui_design_resource(
|
||||
let asset = match register_local_asset_at(
|
||||
root,
|
||||
&relative_path,
|
||||
"UI",
|
||||
"application/json",
|
||||
crate::ui_editor::persistence::UI_DESIGN_DOC_ASSET_KIND,
|
||||
crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"generated",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
@@ -4033,7 +4036,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;
|
||||
}
|
||||
@@ -4231,7 +4234,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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1303,8 +1303,13 @@ fn resolve_resource_edit_source(
|
||||
let asset_kind = source_asset
|
||||
.as_ref()
|
||||
.map(|asset| asset.kind.clone())
|
||||
.or_else(|| input.source_subtype.clone())
|
||||
.unwrap_or_else(|| "asset".to_string());
|
||||
.or_else(|| {
|
||||
input
|
||||
.source_subtype
|
||||
.as_deref()
|
||||
.map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit"))
|
||||
})
|
||||
.unwrap_or(GameCreationAppAssetKind::Unknown);
|
||||
let canonical_resource_id = source_asset
|
||||
.as_ref()
|
||||
.map(source_asset_canonical_resource_id)
|
||||
@@ -1332,7 +1337,7 @@ fn resolve_resource_edit_source(
|
||||
canonical_resource_id,
|
||||
source_path: Some(path),
|
||||
media_type,
|
||||
asset_kind,
|
||||
asset_kind: asset_kind.to_string(),
|
||||
source_sha256: sha256_hex(text.as_bytes()),
|
||||
bytes: None,
|
||||
generation_mode: input.generation_mode,
|
||||
@@ -1363,7 +1368,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());
|
||||
}
|
||||
@@ -1385,7 +1390,7 @@ fn resolve_resource_edit_source(
|
||||
canonical_resource_id,
|
||||
source_path: Some(path),
|
||||
media_type,
|
||||
asset_kind,
|
||||
asset_kind: asset_kind.to_string(),
|
||||
source_sha256: sha256_hex(&bytes),
|
||||
bytes: Some(bytes),
|
||||
generation_mode: input.generation_mode,
|
||||
@@ -3805,7 +3810,10 @@ pub(crate) fn normalize_local_project_raster_resource_at(
|
||||
.unwrap_or("art-image");
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: asset_id.clone(),
|
||||
kind: source_subtype.to_string(),
|
||||
kind: GameCreationAppAssetKind::parse_with_context(
|
||||
source_subtype,
|
||||
"resource-edit.derivative",
|
||||
),
|
||||
media_type: verified_media_type,
|
||||
local_path: source_path,
|
||||
image_sequence_frames: None,
|
||||
@@ -4058,7 +4066,7 @@ fn commit_resource_edit_asset_internal(
|
||||
let category = game_creation_app_asset_category_for_kind(&asset_kind);
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: asset_id.clone(),
|
||||
kind: asset_kind,
|
||||
kind: GameCreationAppAssetKind::parse_with_context(&asset_kind, "resource-edit.derivative"),
|
||||
media_type: staged_media_type.to_string(),
|
||||
local_path: relative_path.clone(),
|
||||
image_sequence_frames,
|
||||
@@ -5165,7 +5173,7 @@ 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(|| source_asset.as_ref().map(|asset| asset.kind.to_string()))
|
||||
.or_else(|| Some(infer_resource_edit_source_asset_kind(&ledger.edit_kind)));
|
||||
derive_local_project_resource_at(DeriveLocalProjectResourceInput {
|
||||
project_path: input.project_path,
|
||||
|
||||
@@ -2031,8 +2031,8 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
|
||||
let registered = register_local_asset_at(
|
||||
&root,
|
||||
"ui/UI 设计 1.json",
|
||||
"UI",
|
||||
"application/json",
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"ui-workflow",
|
||||
source(),
|
||||
)
|
||||
@@ -2053,8 +2053,8 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
|
||||
register_local_asset_at(
|
||||
&root,
|
||||
"ui/UI 设计 1.json",
|
||||
"UI",
|
||||
"application/json",
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"ui-workflow",
|
||||
source(),
|
||||
)
|
||||
@@ -2063,7 +2063,10 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
|
||||
.expect("manifest json");
|
||||
assert_eq!(manifest["assets"][0]["kind"], "UI");
|
||||
assert_eq!(
|
||||
manifest["assets"][0]["kind"],
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND
|
||||
);
|
||||
assert_eq!(manifest["assets"][0]["category"], "audio");
|
||||
assert_eq!(manifest["assets"][0]["tags"], serde_json::json!(["界面"]));
|
||||
|
||||
@@ -2074,7 +2077,7 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
|
||||
///
|
||||
/// 这里的字面量与写入侧逐字一致:UI 设计资产是
|
||||
/// `ui_editor/resource_bridge.rs` / `workflow.rs` / `persistence.rs` 的
|
||||
/// `register_local_asset_at(root, path, "UI", "application/json", ...)`,
|
||||
/// `register_local_asset_at` 使用 UI 文档 kind/media 常量,
|
||||
/// 字体是 `commands.rs` 字体上传的 `register_local_asset_entry(root, path, "font", ...)`。
|
||||
/// 只要别名表漏掉它们,真机资产就会永远停在「待归类」且读时自愈也救不回来。
|
||||
#[test]
|
||||
@@ -2099,8 +2102,8 @@ fn register_local_asset_derives_category_from_real_write_side_kinds() {
|
||||
register_local_asset_at(
|
||||
&root,
|
||||
"ui/UI 设计 1.json",
|
||||
"UI",
|
||||
"application/json",
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"ui-workflow",
|
||||
source(),
|
||||
)
|
||||
@@ -2133,7 +2136,11 @@ fn register_local_asset_derives_category_from_real_write_side_kinds() {
|
||||
assert_eq!(
|
||||
categories,
|
||||
vec![
|
||||
("UI".to_string(), "ui-interaction".to_string()),
|
||||
(
|
||||
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND
|
||||
.to_string(),
|
||||
"ui-interaction".to_string(),
|
||||
),
|
||||
("font".to_string(), "document".to_string()),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ use platform_llm::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_contracts::game_creation_app::GameCreationAppAssetKind;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use ts_rs::TS;
|
||||
@@ -645,5 +646,30 @@ mod tests {
|
||||
Node::export_all(&config).expect("Node TypeScript export succeeds");
|
||||
TextComponent::export_all(&config).expect("TextComponent TypeScript export succeeds");
|
||||
FontSource::export_all(&config).expect("FontSource TypeScript export succeeds");
|
||||
|
||||
let game_creation_config = Config::new().with_out_dir(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../src/contracts/generated/"
|
||||
));
|
||||
GameCreationAppAssetKind::export_all(&game_creation_config)
|
||||
.expect("GameCreationAppAssetKind TypeScript export succeeds");
|
||||
let generated = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../src/contracts/generated/GameCreationAppAssetKind.ts"
|
||||
))
|
||||
.expect("generated GameCreationAppAssetKind binding exists");
|
||||
for value in [
|
||||
"\"image\"",
|
||||
"\"audio\"",
|
||||
"\"sound-effect\"",
|
||||
"\"background-music\"",
|
||||
"\"font\"",
|
||||
"\"unknown\"",
|
||||
] {
|
||||
assert!(
|
||||
generated.contains(value),
|
||||
"generated binding misses {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use typed_floats::tf32::StrictlyPositiveFinite;
|
||||
|
||||
const UI_DESIGN_STATE_SCHEMA_VERSION: &str = "game-creator-ui-design-state.v1";
|
||||
pub(crate) use shared_contracts::game_creation_app::{
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND as UI_DESIGN_DOC_ASSET_KIND,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE as UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
const UI_DESIGN_STATE_MAX_BYTES: usize = 2 * 1024 * 1024;
|
||||
const UI_DESIGN_CODE_MAX_BYTES: usize = UI_DESIGN_STATE_MAX_BYTES * 8;
|
||||
const UI_DESIGN_STATE_MAX_IMAGES: usize = 4;
|
||||
@@ -321,7 +325,7 @@ fn ui_design_asset(
|
||||
.into_iter()
|
||||
.find(|asset| asset.id == asset_id)
|
||||
.ok_or_else(|| "UI 设计资源不存在".to_string())?;
|
||||
if asset.kind != "UI" || asset.media_type != "application/json" {
|
||||
if asset.kind != UI_DESIGN_DOC_ASSET_KIND || asset.media_type != UI_DESIGN_DOC_MEDIA_TYPE {
|
||||
return Err("目标资源不是 UI 设计 JSON 资产".to_string());
|
||||
}
|
||||
normalize_relative_path(&asset.local_path)?;
|
||||
@@ -815,8 +819,8 @@ mod tests {
|
||||
let asset = register_local_asset_at(
|
||||
directory.path(),
|
||||
relative_path,
|
||||
"UI",
|
||||
"application/json",
|
||||
UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"test",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::ui_editor::persistence::initialize_ui_design_state_with_source_image_at;
|
||||
use crate::ui_editor::persistence::{
|
||||
initialize_ui_design_state_with_source_image_at, UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
use crate::{
|
||||
acquire_project_write_lock, advance_agent_runtime_project_revision_locked,
|
||||
enforce_project_permission_policy, read_existing_manifest_for_project,
|
||||
@@ -89,8 +92,8 @@ pub(crate) fn ensure_ui_design_resource_for_prototype(
|
||||
}
|
||||
|
||||
if let Some(asset) = manifest.assets.iter().find(|asset| {
|
||||
asset.kind == "UI"
|
||||
&& asset.media_type == "application/json"
|
||||
asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
&& asset.source.reference_resource_ids.iter().any(|reference| {
|
||||
source_reference_ids
|
||||
.iter()
|
||||
@@ -118,8 +121,8 @@ pub(crate) fn ensure_ui_design_resource_for_prototype(
|
||||
let asset = match register_local_asset_at(
|
||||
root,
|
||||
&relative_path,
|
||||
"UI",
|
||||
"application/json",
|
||||
UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"generated",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
@@ -193,7 +196,9 @@ fn next_ui_design_path(
|
||||
let mut index = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.kind == "UI")
|
||||
.filter(|asset| {
|
||||
asset.kind == UI_DESIGN_DOC_ASSET_KIND && asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
})
|
||||
.count()
|
||||
+ 1;
|
||||
loop {
|
||||
@@ -281,11 +286,9 @@ mod tests {
|
||||
};
|
||||
let first = ensure_ui_design_resource_for_prototype(input.clone()).expect("bridge");
|
||||
assert!(first.created);
|
||||
assert_eq!(first.asset.kind, "UI");
|
||||
assert_eq!(first.asset.kind, UI_DESIGN_DOC_ASSET_KIND);
|
||||
// 写侧 → 分类的端到端断言:这条路径走的是与
|
||||
// `workflow.rs` / `persistence.rs` 完全相同的 `register_local_asset_at(..., "UI", ...)`。
|
||||
// 别名表漏掉大写 `UI` 时,这里会落 unclassified(真机 8 条 UI 资产的表现),
|
||||
// 且派生值本身就是 unclassified,读时自愈也救不回来。
|
||||
// 写侧与 persistence/workflow 共用 UI 文档 kind/media 常量。
|
||||
assert_eq!(
|
||||
first.asset.category,
|
||||
GameCreationAppAssetCategory::UiInteraction
|
||||
@@ -322,7 +325,10 @@ mod tests {
|
||||
.manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.kind == "UI")
|
||||
.filter(|asset| {
|
||||
asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
})
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::ui_editor::layout::node::{Node, StageStatus};
|
||||
use crate::ui_editor::persistence::{
|
||||
initialize_ui_design_state_at, load_ui_design_state_at, save_ui_design_state_at,
|
||||
LoadUiDesignStateInput, SaveUiDesignStateInput, SaveUiDesignStateResult,
|
||||
UI_DESIGN_DOC_ASSET_KIND, UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
use crate::ui_editor::resource::font::FontAsset;
|
||||
use crate::ui_editor::resource::sprite::{SpriteAsset, SpriteAssetMetadata, SpriteBorder};
|
||||
@@ -683,8 +684,8 @@ fn find_page_ui_resource(
|
||||
let Some(asset) = matches.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if asset.kind != "UI"
|
||||
|| asset.media_type != "application/json"
|
||||
if asset.kind != UI_DESIGN_DOC_ASSET_KIND
|
||||
|| asset.media_type != UI_DESIGN_DOC_MEDIA_TYPE
|
||||
|| asset.local_path != workflow_relative_path(source, &page.page_id)
|
||||
{
|
||||
return Err(format!("页面 {} 的 UI workflow 资源身份冲突", page.page_id));
|
||||
@@ -748,8 +749,8 @@ fn ensure_page_ui_resource(
|
||||
let registered = register_local_asset_at(
|
||||
root,
|
||||
&relative_path,
|
||||
"UI",
|
||||
"application/json",
|
||||
UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
"ui-workflow",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
@@ -879,7 +880,7 @@ fn install_page_component_assets(
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("UI 素材")
|
||||
.to_string(),
|
||||
asset_type: asset.kind.clone(),
|
||||
asset_type: asset.kind.to_string(),
|
||||
};
|
||||
sprite.path = asset.local_path.clone();
|
||||
match state.sprite_assets.get(&asset_id) {
|
||||
@@ -1179,7 +1180,7 @@ fn update_page_manifest_stage(root: &Path, asset_id: &str, stage: &str) -> Resul
|
||||
.iter_mut()
|
||||
.find(|asset| asset.id == asset_id)
|
||||
.ok_or_else(|| format!("UI workflow 资源 {} 未登记", asset_id))?;
|
||||
if asset.kind != "UI" || asset.media_type != "application/json" {
|
||||
if asset.kind != UI_DESIGN_DOC_ASSET_KIND || asset.media_type != UI_DESIGN_DOC_MEDIA_TYPE {
|
||||
return Err(format!("UI workflow 资源 {} 类型不匹配", asset_id));
|
||||
}
|
||||
let next_kind = format!("ui-workflow.{stage}");
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { GameCreationAppAssetKind } from './generated/GameCreationAppAssetKind';
|
||||
|
||||
export type { GameCreationAppAssetKind } from './generated/GameCreationAppAssetKind';
|
||||
|
||||
export const GAME_CREATION_APP_ASSET_KINDS = [
|
||||
'image',
|
||||
'scene',
|
||||
'character',
|
||||
'character-animation',
|
||||
'icon',
|
||||
'icon-spritesheet',
|
||||
'icon-spec',
|
||||
'ui-design',
|
||||
'ui-design-doc',
|
||||
'publication-material',
|
||||
'spec',
|
||||
'video',
|
||||
'audio',
|
||||
'sound-effect',
|
||||
'background-music',
|
||||
'font',
|
||||
'document',
|
||||
'code',
|
||||
'unknown',
|
||||
] as const satisfies readonly GameCreationAppAssetKind[];
|
||||
|
||||
const KNOWN_ASSET_KINDS = new Set<string>(GAME_CREATION_APP_ASSET_KINDS);
|
||||
|
||||
export function parseGameCreationAppAssetKind(
|
||||
raw: unknown,
|
||||
context: string,
|
||||
): GameCreationAppAssetKind {
|
||||
if (typeof raw === 'string' && KNOWN_ASSET_KINDS.has(raw)) {
|
||||
return raw as GameCreationAppAssetKind;
|
||||
}
|
||||
|
||||
const rawValue = typeof raw === 'string' ? raw : String(raw);
|
||||
if (rawValue !== 'unknown') {
|
||||
console.warn('未知的 GameCreationApp 资源 kind,已收口为 unknown', {
|
||||
rawKind: rawValue,
|
||||
sourceContext: context,
|
||||
});
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* GameCreationApp manifest 资源 kind 的唯一 Rust 类型。
|
||||
*
|
||||
* JSON 使用 kebab-case;`Unknown` 只表示解析边界遇到尚未登记的输入,
|
||||
* 正常资源写入路径不应主动选择它。
|
||||
*/
|
||||
export type GameCreationAppAssetKind =
|
||||
| 'image'
|
||||
| 'scene'
|
||||
| 'character'
|
||||
| 'character-animation'
|
||||
| 'icon'
|
||||
| 'icon-spritesheet'
|
||||
| 'icon-spec'
|
||||
| 'ui-design'
|
||||
| 'ui-design-doc'
|
||||
| 'publication-material'
|
||||
| 'spec'
|
||||
| 'video'
|
||||
| 'audio'
|
||||
| 'sound-effect'
|
||||
| 'background-music'
|
||||
| 'font'
|
||||
| 'document'
|
||||
| 'code'
|
||||
| 'unknown';
|
||||
+5
-1
@@ -53,6 +53,7 @@ import { createPortal, flushSync } from 'react-dom';
|
||||
import { PlatformResourceFilterBar } from '../../../../../packages/shared/src/components/PlatformResourceFilterBar';
|
||||
import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs';
|
||||
import {
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
type GameCreationAppAssetManifestEntry,
|
||||
gameCreationAppAssetTags,
|
||||
type GameIterationVersion,
|
||||
@@ -1253,7 +1254,10 @@ function ResourcePickerThumbnail({
|
||||
if (mediaType.startsWith('image/')) {
|
||||
return <Loader2 size={18} className="animate-spin" aria-hidden="true" />;
|
||||
}
|
||||
if (kind === 'ui' || mediaType === 'application/json') {
|
||||
if (
|
||||
kind === 'ui' ||
|
||||
mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
|
||||
) {
|
||||
return <FileText size={18} aria-hidden="true" />;
|
||||
}
|
||||
return <ImageIcon size={18} aria-hidden="true" />;
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
buildGameCreationAppAssetTagLibrary,
|
||||
type GameCreationAppAssetTagLibraryEntry,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationAppAssetTagLibrary';
|
||||
import {
|
||||
type GameCreationAppAssetKind,
|
||||
parseGameCreationAppAssetKind,
|
||||
} from '../../contracts/assetKind';
|
||||
|
||||
export type ResourceReferenceSource =
|
||||
| 'asset-picker'
|
||||
@@ -20,7 +24,7 @@ export type ResourceReferenceSource =
|
||||
export type ResourceReference = {
|
||||
type: 'resource';
|
||||
resourceId: string;
|
||||
kind: string;
|
||||
kind: GameCreationAppAssetKind;
|
||||
mediaType: string;
|
||||
label: string;
|
||||
category: GameCreationAppAssetCategory;
|
||||
@@ -98,7 +102,7 @@ export function resourceReferenceFromAsset(
|
||||
return {
|
||||
type: 'resource',
|
||||
resourceId: asset.id,
|
||||
kind: asset.kind,
|
||||
kind: parseGameCreationAppAssetKind(asset.kind, 'resource-reference'),
|
||||
mediaType: asset.mediaType,
|
||||
label: resourceDisplayName(asset),
|
||||
category: gameCreationAppAssetCategory(asset),
|
||||
|
||||
@@ -2,6 +2,11 @@ import type {
|
||||
GameCreationAppAssetManifestEntry,
|
||||
GameCreationAppManifest,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { parseGameCreationAppAssetKind } from '../../contracts/assetKind';
|
||||
|
||||
export type UiDesignResourceBridgeResult = {
|
||||
asset: GameCreationAppAssetManifestEntry;
|
||||
@@ -48,8 +53,11 @@ export function findLinkedUiDesignResource(
|
||||
manifest.assets
|
||||
.filter(
|
||||
(asset) =>
|
||||
asset.kind === 'UI' &&
|
||||
asset.mediaType === 'application/json' &&
|
||||
parseGameCreationAppAssetKind(
|
||||
asset.kind,
|
||||
'ui-design-resource-bridge.linked-document',
|
||||
) === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
|
||||
asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE &&
|
||||
asset.source.referenceResourceIds?.some((reference) =>
|
||||
referenceIds.has(reference),
|
||||
),
|
||||
@@ -80,7 +88,13 @@ export async function ensureUiDesignResourceForPrototype({
|
||||
const prototype = manifest.assets.find(
|
||||
(asset) => asset.id === normalizedPrototypeAssetId,
|
||||
);
|
||||
if (!prototype || prototype.kind !== 'ui-prototype') {
|
||||
if (
|
||||
!prototype ||
|
||||
parseGameCreationAppAssetKind(
|
||||
prototype.kind,
|
||||
'ui-design-resource-bridge.prototype',
|
||||
) !== 'ui-design'
|
||||
) {
|
||||
throw new Error('目标资源不是 UI 原型图片');
|
||||
}
|
||||
if (!prototype.mediaType.toLowerCase().startsWith('image/')) {
|
||||
|
||||
@@ -73,6 +73,10 @@ import type {
|
||||
GameIterationVersion,
|
||||
ProjectResourceCanvasLayoutMode,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { ImageCanvasCharacterAnimationPanelView } from '../../../../../src/components/image-editor/ImageCanvasCharacterAnimationPanelView';
|
||||
import type {
|
||||
CanvasLayer,
|
||||
@@ -92,6 +96,7 @@ import {
|
||||
isFloatingOverlayWheelEvent,
|
||||
useImageCanvasFloatingOptionDismiss,
|
||||
} from '../../../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
|
||||
import { parseGameCreationAppAssetKind } from '../../contracts/assetKind';
|
||||
import { DesignWorkspacePanel } from '../../features/project-workspace/DesignWorkspacePanel';
|
||||
import {
|
||||
LocalGamePreviewFrame,
|
||||
@@ -5040,8 +5045,8 @@ export default function ProjectDevelopmentView({
|
||||
if (canvasOpenEpochRef.current !== openEpoch) return;
|
||||
if (
|
||||
result.manifest.projectId !== manifest.projectId ||
|
||||
result.asset.kind !== 'UI' ||
|
||||
result.asset.mediaType !== 'application/json'
|
||||
result.asset.kind !== GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND ||
|
||||
result.asset.mediaType !== GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
|
||||
) {
|
||||
throw new Error('UI 编辑资源结果与当前项目不一致');
|
||||
}
|
||||
@@ -5112,8 +5117,8 @@ export default function ProjectDevelopmentView({
|
||||
if (uiEditorRoute) return;
|
||||
const completed = manifest.assets.find(
|
||||
(asset) =>
|
||||
asset.kind === 'UI' &&
|
||||
asset.mediaType === 'application/json' &&
|
||||
asset.kind === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
|
||||
asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE &&
|
||||
asset.source.generationKind === 'ui-workflow.completed',
|
||||
);
|
||||
if (
|
||||
@@ -6812,7 +6817,7 @@ export default function ProjectDevelopmentView({
|
||||
[manifest, selectedResource],
|
||||
);
|
||||
const selectedResourceOpensUiEditor =
|
||||
selectedResource?.subtype === 'UI' ||
|
||||
selectedResource?.subtype === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND ||
|
||||
selectedResource?.subtype === 'ui-prototype';
|
||||
|
||||
const selectedToolbarStyle = selectedResourceLayer
|
||||
@@ -7222,9 +7227,11 @@ export default function ProjectDevelopmentView({
|
||||
type: 'resource',
|
||||
resourceId:
|
||||
selectedResource.manifestAssetId!,
|
||||
kind:
|
||||
kind: parseGameCreationAppAssetKind(
|
||||
selectedResource.subtype ||
|
||||
selectedResource.category,
|
||||
selectedResource.category,
|
||||
'project-development.resource-reference',
|
||||
),
|
||||
mediaType: selectedResource.mediaType,
|
||||
label: selectedResource.label,
|
||||
category:
|
||||
|
||||
@@ -4,6 +4,10 @@ import type {
|
||||
ProjectResourceCanvasLayout,
|
||||
ProjectResourceCanvasPosition,
|
||||
} from '../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
} from '../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { RESOURCE_REFERENCE_INSERT_EVENT } from '../../src/features/project-workspace/resourceReferences';
|
||||
import { RESOURCE_BOOK_OVERVIEW_STACK_LIMIT } from '../../src/view/project-development/resourceBookLayout';
|
||||
import {
|
||||
@@ -5908,8 +5912,8 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
manifest.assets = [
|
||||
{
|
||||
id: 'ui-design-resource',
|
||||
kind: 'UI',
|
||||
mediaType: 'application/json',
|
||||
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
localPath: 'assets/ui-design.json',
|
||||
source: { kind: 'generated' },
|
||||
},
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
GAME_CREATION_APP_ASSET_KINDS,
|
||||
parseGameCreationAppAssetKind,
|
||||
} from '../src/contracts/assetKind';
|
||||
|
||||
describe('shell generated asset kind boundary', () => {
|
||||
it('keeps the generated kind values aligned with the manifest vocabulary', () => {
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('font');
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('audio');
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('sound-effect');
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('background-music');
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('unknown');
|
||||
});
|
||||
|
||||
it('does not alias old values and records unknown input', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
expect(parseGameCreationAppAssetKind('ui', 'manifest.asset.kind')).toBe(
|
||||
'unknown',
|
||||
);
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'未知的 GameCreationApp 资源 kind,已收口为 unknown',
|
||||
{ rawKind: 'ui', sourceContext: 'manifest.asset.kind' },
|
||||
);
|
||||
warn.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
canonicalGameCreationAppAssetKind,
|
||||
GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND,
|
||||
GAME_CREATION_APP_CANONICAL_ASSET_KINDS,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
type GameCreationAppAssetCategory,
|
||||
gameCreationAppAssetCategory,
|
||||
gameCreationAppAssetCategoryForKind,
|
||||
@@ -219,8 +220,13 @@ describe('真机出现的 kind 归类口径', () => {
|
||||
canonical: 'icon',
|
||||
category: 'ui-interaction',
|
||||
},
|
||||
// 现役写入侧的大写字面量与字体 kind,两者都必须落进明确栏目。
|
||||
// UI 文档与字体 kind,两者都必须落进明确栏目。
|
||||
{ kind: 'UI', canonical: 'ui-design', category: 'ui-interaction' },
|
||||
{
|
||||
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
canonical: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
category: 'ui-interaction',
|
||||
},
|
||||
{ kind: 'font', canonical: 'document', category: 'document' },
|
||||
// `Object.prototype` 上的键不是别名:TS 侧必须用 `Object.hasOwn` 挡住对象字面量的
|
||||
// 原型命中,Rust 侧 match 字面量本来就落 `image`;这类极端输入两侧也必须一致。
|
||||
@@ -277,13 +283,15 @@ describe('写侧 kind 字面量 → 分类的端到端口径', () => {
|
||||
return kinds;
|
||||
}
|
||||
|
||||
test('UI 设计写点仍直接写 `"UI"`,且它落 UI 交互而不是待归类', () => {
|
||||
// 8 条真机 UI 资产是 `kind:"UI"` + `mediaType:"application/json"`:
|
||||
// 该 kind 不在别名表里时派生结果也是 unclassified,读时自愈同样救不回来。
|
||||
test('UI 设计写点统一使用文档资产常量,且它落 UI 交互而不是待归类', () => {
|
||||
for (const file of UI_EDITOR_WRITE_SITES) {
|
||||
expect(registeredKindLiterals(file)).toContain('UI');
|
||||
expect(readFileSync(file, 'utf8')).toContain('UI_DESIGN_DOC_ASSET_KIND');
|
||||
}
|
||||
expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction');
|
||||
expect(
|
||||
gameCreationAppAssetCategoryForKind(
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
),
|
||||
).toBe('ui-interaction');
|
||||
});
|
||||
|
||||
test('UI 编辑器写点写出的每个 kind 都落明确栏目', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user