建立 AGC 资源 kind Rust 枚举与 TypeScript 绑定

新增 GameCreationAppAssetKind 及 Unknown 解析边界
通过 ts-rs 生成 shell 专属 TypeScript 联合类型
补充未知值上下文日志与 Rust/绑定生成测试
This commit is contained in:
2026-09-15 16:20:52 +08:00
parent b14a533b17
commit fe85fa2a62
10 changed files with 335 additions and 37 deletions
+2
View File
@@ -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"] }
@@ -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}"
);
}
}
}
@@ -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';
@@ -3,7 +3,7 @@
| 字段 | 值 |
| --- | --- |
| Milestone | `docs/project-memory/plans/【里程碑】AGC资源kind枚举化Rust绑定与内部收紧-2026-09-15.md` |
| Status | ready |
| Status | in-progress |
| Owner | Codex |
## 修改边界
@@ -29,6 +29,7 @@
2. 为 enum 增加可选 ts-rs derive,默认 shared-contracts 构建不引入 ts-rs。
3. 让 shell 的 shared-contracts 依赖打开绑定 feature,在 shell 专属 contracts/generated 目录生成 TS。
4. 增加 Rust serde round-trip 与 Unknown 解析/序列化测试;增加 TS 生成文件存在和值集合测试。
未知值通过 `parse_with_context` 收口到 `Unknown`,并在启用 shell 的 `kind-observability` feature 时记录原始值与解析上下文。
5. 运行 shared-contracts 与 shell 定向测试;通过后暂停,等待下一个内部字段收紧切片。
## 验证命令
@@ -3,7 +3,7 @@
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | planned |
| Status | in-progress |
| Date | 2026-09-15 |
| Parent Spec | `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` |
+57 -33
View File
@@ -52,17 +52,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom 0.2.17",
"once_cell",
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -182,7 +171,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -193,7 +182,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1133,7 +1122,7 @@ dependencies = [
"openssl-sys",
"schannel",
"socket2",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1148,7 +1137,7 @@ dependencies = [
"openssl-sys",
"pkg-config",
"vcpkg",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1451,7 +1440,7 @@ version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6"
dependencies = [
"ahash 0.8.12",
"ahash",
"bytemuck",
"document-features",
"egui",
@@ -1486,7 +1475,7 @@ version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3"
dependencies = [
"ahash 0.8.12",
"ahash",
"bitflags 2.13.0",
"emath",
"epaint",
@@ -1503,7 +1492,7 @@ version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236"
dependencies = [
"ahash 0.8.12",
"ahash",
"bytemuck",
"document-features",
"egui",
@@ -1598,7 +1587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62"
dependencies = [
"ab_glyph",
"ahash 0.8.12",
"ahash",
"bytemuck",
"ecolor",
"emath",
@@ -1628,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2062,9 +2051,6 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.8",
]
[[package]]
name = "hashbrown"
@@ -2094,6 +2080,11 @@ name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
@@ -3084,7 +3075,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3739,7 +3730,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527735ac204efb9fa3884bfd9224d016c5735fabe1d394ebed145b40e7545b99"
dependencies = [
"ahash 0.8.12",
"ahash",
"async-trait",
"blake2",
"bstr",
@@ -3776,7 +3767,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a7ffe2f5acf9f94fd255cfd1438866bc9124f8f0c7d42562bd3f853df2094b7"
dependencies = [
"ahash 0.8.12",
"ahash",
"async-trait",
"brotli",
"bstr",
@@ -3914,7 +3905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6705a26ad89d241a989a5395641931ba37076f5ab5fbd19ee92402414a43af32"
dependencies = [
"arrayvec",
"hashbrown 0.12.3",
"hashbrown 0.17.1",
"parking_lot",
"rand 0.8.6",
]
@@ -4856,7 +4847,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5247,6 +5238,8 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"tracing",
"ts-rs",
]
[[package]]
@@ -5420,7 +5413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -5529,7 +5522,7 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c4e9d07e21ad665c45736871e5005d8457ab3d2cdd7806e351aa5e1c8c2ac32"
dependencies = [
"ahash 0.8.12",
"ahash",
"crossbeam-queue",
"either",
"hashbrown 0.16.1",
@@ -5815,7 +5808,16 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
@@ -6284,6 +6286,28 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
dependencies = [
"thiserror 2.0.18",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"termcolor",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -6901,7 +6925,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7134,7 +7158,7 @@ version = "0.30.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
dependencies = [
"ahash 0.8.12",
"ahash",
"android-activity",
"atomic-waker",
"bitflags 2.13.0",
@@ -8,8 +8,12 @@ license.workspace = true
# 默认给 api-server 等原生后端暴露资产上传 DTOSpacetimeDB WASM 路径通过 workspace 依赖关闭默认 feature。
default = ["oss-contracts"]
oss-contracts = []
ts-bindings = ["dep:ts-rs", "kind-observability"]
kind-observability = ["dep:tracing"]
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
ts-rs = { version = "12.0.1", optional = true }
tracing = { workspace = true, optional = true }
@@ -1,6 +1,9 @@
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
mod asset_kind;
pub use asset_kind::GameCreationAppAssetKind;
pub const GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION: &str = "game-creation-app.manifest.v1";
pub const GAME_CREATION_AGENT_RUN_SCHEMA_VERSION: &str = "game-creator-agent-run.v1";
pub const GAME_CREATION_AGENT_RUN_MAX_PASSES: u8 = 3;
@@ -0,0 +1,210 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::fmt;
use std::str::FromStr;
/// GameCreationApp manifest 资源 kind 的唯一 Rust 类型。
///
/// JSON 使用 kebab-case`Unknown` 只表示解析边界遇到尚未登记的输入,
/// 正常资源写入路径不应主动选择它。
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-bindings", ts(export_to = "GameCreationAppAssetKind.ts"))]
pub enum GameCreationAppAssetKind {
#[cfg_attr(feature = "ts-bindings", ts(rename = "image"))]
Image,
#[cfg_attr(feature = "ts-bindings", ts(rename = "scene"))]
Scene,
#[cfg_attr(feature = "ts-bindings", ts(rename = "character"))]
Character,
#[cfg_attr(feature = "ts-bindings", ts(rename = "character-animation"))]
CharacterAnimation,
#[cfg_attr(feature = "ts-bindings", ts(rename = "icon"))]
Icon,
#[cfg_attr(feature = "ts-bindings", ts(rename = "icon-spritesheet"))]
IconSpritesheet,
#[cfg_attr(feature = "ts-bindings", ts(rename = "icon-spec"))]
IconSpec,
#[cfg_attr(feature = "ts-bindings", ts(rename = "ui-design"))]
UiDesign,
#[cfg_attr(feature = "ts-bindings", ts(rename = "ui-design-doc"))]
UiDesignDoc,
#[cfg_attr(feature = "ts-bindings", ts(rename = "publication-material"))]
PublicationMaterial,
#[cfg_attr(feature = "ts-bindings", ts(rename = "spec"))]
Spec,
#[cfg_attr(feature = "ts-bindings", ts(rename = "video"))]
Video,
#[cfg_attr(feature = "ts-bindings", ts(rename = "audio"))]
Audio,
#[cfg_attr(feature = "ts-bindings", ts(rename = "sound-effect"))]
SoundEffect,
#[cfg_attr(feature = "ts-bindings", ts(rename = "background-music"))]
BackgroundMusic,
#[cfg_attr(feature = "ts-bindings", ts(rename = "font"))]
Font,
#[cfg_attr(feature = "ts-bindings", ts(rename = "document"))]
Document,
#[cfg_attr(feature = "ts-bindings", ts(rename = "code"))]
Code,
#[cfg_attr(feature = "ts-bindings", ts(rename = "unknown"))]
Unknown,
}
impl GameCreationAppAssetKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Image => "image",
Self::Scene => "scene",
Self::Character => "character",
Self::CharacterAnimation => "character-animation",
Self::Icon => "icon",
Self::IconSpritesheet => "icon-spritesheet",
Self::IconSpec => "icon-spec",
Self::UiDesign => "ui-design",
Self::UiDesignDoc => "ui-design-doc",
Self::PublicationMaterial => "publication-material",
Self::Spec => "spec",
Self::Video => "video",
Self::Audio => "audio",
Self::SoundEffect => "sound-effect",
Self::BackgroundMusic => "background-music",
Self::Font => "font",
Self::Document => "document",
Self::Code => "code",
Self::Unknown => "unknown",
}
}
pub fn from_str_lossy(value: &str) -> Self {
match value {
"image" => Self::Image,
"scene" => Self::Scene,
"character" => Self::Character,
"character-animation" => Self::CharacterAnimation,
"icon" => Self::Icon,
"icon-spritesheet" => Self::IconSpritesheet,
"icon-spec" => Self::IconSpec,
"ui-design" => Self::UiDesign,
"ui-design-doc" => Self::UiDesignDoc,
"publication-material" => Self::PublicationMaterial,
"spec" => Self::Spec,
"video" => Self::Video,
"audio" => Self::Audio,
"sound-effect" => Self::SoundEffect,
"background-music" => Self::BackgroundMusic,
"font" => Self::Font,
"document" => Self::Document,
"code" => Self::Code,
_ => Self::Unknown,
}
}
/// 解析外部 manifest/API 字符串;未知值收口到 `Unknown` 并留下可检索的原始值与上下文。
pub fn parse_with_context(value: &str, context: &'static str) -> Self {
let parsed = Self::from_str_lossy(value);
if parsed == Self::Unknown && value.trim() != Self::Unknown.as_str() {
#[cfg(feature = "kind-observability")]
tracing::warn!(
raw_kind = value,
source_context = context,
"未知的 GameCreationApp 资源 kind,已收口为 unknown"
);
}
parsed
}
}
impl FromStr for GameCreationAppAssetKind {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::from_str_lossy(value))
}
}
impl fmt::Display for GameCreationAppAssetKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for GameCreationAppAssetKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for GameCreationAppAssetKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct AssetKindVisitor;
impl<'de> de::Visitor<'de> for AssetKindVisitor {
type Value = GameCreationAppAssetKind;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a GameCreationApp asset kind string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(GameCreationAppAssetKind::parse_with_context(value, "serde"))
}
}
deserializer.deserialize_str(AssetKindVisitor)
}
}
#[cfg(test)]
mod tests {
use super::GameCreationAppAssetKind;
#[test]
fn serde_round_trip_uses_kebab_case() {
let values = [
(GameCreationAppAssetKind::Image, "image"),
(
GameCreationAppAssetKind::CharacterAnimation,
"character-animation",
),
(GameCreationAppAssetKind::UiDesignDoc, "ui-design-doc"),
(GameCreationAppAssetKind::SoundEffect, "sound-effect"),
(
GameCreationAppAssetKind::BackgroundMusic,
"background-music",
),
(GameCreationAppAssetKind::Font, "font"),
(GameCreationAppAssetKind::Unknown, "unknown"),
];
for (value, encoded) in values {
assert_eq!(
serde_json::to_string(&value).unwrap(),
format!("\"{encoded}\"")
);
assert_eq!(
serde_json::from_str::<GameCreationAppAssetKind>(&format!("\"{encoded}\""))
.unwrap(),
value
);
}
}
#[test]
fn unknown_wire_values_are_closed_over_unknown() {
for raw in ["asset", "ui", "game-background", "future-kind"] {
assert_eq!(
serde_json::from_str::<GameCreationAppAssetKind>(&format!("\"{raw}\"")).unwrap(),
GameCreationAppAssetKind::Unknown
);
}
}
}