收紧 AGC manifest 资源 kind 字段
将 manifest 条目 kind 改为 Rust 枚举并把未知值收口为 Unknown 修正字体上传与图集切片的正式资源 kind 保持 source.kind 与 generationKind 等其他字符串字段不变
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -4036,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;
|
||||
}
|
||||
@@ -4234,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,
|
||||
|
||||
@@ -880,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) {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
- shell 内生成的 GameCreationApp kind TypeScript 文件;
|
||||
- 对应 Rust/TS 定向测试与文档 todo 证据。
|
||||
|
||||
本切片已落地:
|
||||
|
||||
- `GameCreationAppAssetManifestEntry.kind` 改为 `GameCreationAppAssetKind`;manifest 反序列化未知值收口为 `Unknown`。
|
||||
- 上传内容推断的字体写入 `Font`,未知上传类型写入 `Unknown`;图集切片 manifest 写入 `Icon`。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- manifest 结构体的 `kind: String` 字段及所有调用方(留给下一个小提交);
|
||||
|
||||
@@ -496,7 +496,7 @@ pub struct GameCreationAppImageSequenceFrame {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameCreationAppAssetManifestEntry {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub kind: GameCreationAppAssetKind,
|
||||
pub media_type: String,
|
||||
pub local_path: String,
|
||||
pub source: GameCreationAppAssetSource,
|
||||
@@ -558,7 +558,7 @@ impl<'de> Deserialize<'de> for GameCreationAppAssetManifestEntry {
|
||||
};
|
||||
Ok(Self {
|
||||
id: wire.id,
|
||||
kind: wire.kind,
|
||||
kind: GameCreationAppAssetKind::parse_with_context(&wire.kind, "manifest.asset.kind"),
|
||||
media_type: wire.media_type,
|
||||
local_path: wire.local_path,
|
||||
source: wire.source,
|
||||
@@ -2084,7 +2084,7 @@ mod tests {
|
||||
});
|
||||
manifest.assets.push(GameCreationAppAssetManifestEntry {
|
||||
id: "asset-player".to_string(),
|
||||
kind: "character".to_string(),
|
||||
kind: GameCreationAppAssetKind::Character,
|
||||
media_type: "image".to_string(),
|
||||
local_path: "assets/images/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// GameCreationApp manifest 资源 kind 的唯一 Rust 类型。
|
||||
@@ -101,6 +102,8 @@ impl GameCreationAppAssetKind {
|
||||
|
||||
/// 解析外部 manifest/API 字符串;未知值收口到 `Unknown` 并留下可检索的原始值与上下文。
|
||||
pub fn parse_with_context(value: &str, context: &'static str) -> Self {
|
||||
#[cfg(not(feature = "kind-observability"))]
|
||||
let _ = context;
|
||||
let parsed = Self::from_str_lossy(value);
|
||||
if parsed == Self::Unknown && value.trim() != Self::Unknown.as_str() {
|
||||
#[cfg(feature = "kind-observability")]
|
||||
@@ -128,6 +131,38 @@ impl fmt::Display for GameCreationAppAssetKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for GameCreationAppAssetKind {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for GameCreationAppAssetKind {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for GameCreationAppAssetKind {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.as_str() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for GameCreationAppAssetKind {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.as_str() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<String> for GameCreationAppAssetKind {
|
||||
fn eq(&self, other: &String) -> bool {
|
||||
self.as_str() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for GameCreationAppAssetKind {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
Reference in New Issue
Block a user