收紧 AGC manifest 资源 kind 字段

将 manifest 条目 kind 改为 Rust 枚举并把未知值收口为 Unknown
修正字体上传与图集切片的正式资源 kind
保持 source.kind 与 generationKind 等其他字符串字段不变
This commit is contained in:
2026-09-15 17:37:28 +08:00
parent fe85fa2a62
commit b4fd8d9b8b
12 changed files with 90 additions and 39 deletions
@@ -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