Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d0dd2d721 | |||
| 2befaaef9d | |||
| 20b0bf4dd7 | |||
| b4fd8d9b8b | |||
| fe85fa2a62 | |||
| b14a533b17 | |||
| 42969b2218 | |||
| c4ee75a1af | |||
| 6f2137cbb0 |
+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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
@@ -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),
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -52,7 +53,10 @@ export function findLinkedUiDesignResource(
|
||||
manifest.assets
|
||||
.filter(
|
||||
(asset) =>
|
||||
asset.kind === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
|
||||
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),
|
||||
@@ -84,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/')) {
|
||||
|
||||
@@ -96,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,
|
||||
@@ -7226,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:
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
function manifestWithPrototype(): GameCreationAppManifest {
|
||||
const prototype: GameCreationAppAssetManifestEntry = {
|
||||
id: 'prototype-asset',
|
||||
kind: 'ui-prototype',
|
||||
kind: 'ui-design',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/ui-prototype.png',
|
||||
source: { kind: 'canvas', referenceResourceIds: [] },
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
## AI 游戏创作与 Agent Runtime
|
||||
|
||||
- [AGC 资源 kind 枚举化契约](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-09-15-gamecreationapp-资源-kind-枚举化当前权威口径):GameCreationApp 资源 kind 的 Rust enum、ts-rs 绑定、Unknown 可观测性和 shell 内重构边界。
|
||||
|
||||
- [策划 Agent 生产迁移与工作区浏览](./technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md):实施中;以自由协作原型为行为基线,复用生产基建,采用阶段审批与用户工作区文件浏览。无旧 V2 会话的策划入口切换到新设计 Agent。
|
||||
|
||||
- [LLM 累计额度结算](./technical/【技术方案】LLM累计额度结算-2026-09-05.md):Router 累计额度、首次基线与原子钱包结算。
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 【实施计划】AGC 资源 kind Rust 枚举与 ts-rs 绑定
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】AGC资源kind枚举化Rust绑定与内部收紧-2026-09-15.md` |
|
||||
| Status | in-progress |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
允许修改:
|
||||
|
||||
- `server-rs/crates/shared-contracts` 内 GameCreationApp 资源 kind 的独立 Rust 模块;
|
||||
- shared-contracts 的可选 ts-rs 绑定 feature;
|
||||
- ai-game-creator-shell 的 shared-contracts feature 引用和生成测试入口;
|
||||
- shell 内生成的 GameCreationApp kind TypeScript 文件;
|
||||
- 对应 Rust/TS 定向测试与文档 todo 证据。
|
||||
|
||||
本切片已落地:
|
||||
|
||||
- `GameCreationAppAssetManifestEntry.kind` 改为 `GameCreationAppAssetKind`;manifest 反序列化未知值收口为 `Unknown`。
|
||||
- 上传内容推断的字体写入 `Font`,未知上传类型写入 `Unknown`;图集切片 manifest 写入 `Icon`。
|
||||
- shell TS 资源引用边界开始消费生成绑定,并为未知值保留原始值与上下文日志。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- manifest 结构体的 `kind: String` 字段及所有调用方(留给下一个小提交);
|
||||
- `category`、`mediaType`、`source.kind`、`source.generationKind`;
|
||||
- api-server、SpacetimeDB schema、OpenAPI;
|
||||
- alias/fallback 删除和 Unknown writer 处理(留给后续切片)。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 新建独立 kind 模块,定义当前正式成员:17 个现有 canonical kind + `font` + `Unknown`,serde 使用 kebab-case。
|
||||
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 定向测试;通过后暂停,等待下一个内部字段收紧切片。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. `cargo fmt --all -- --check`
|
||||
2. `cargo test --locked -p shared-contracts game_creation_app --manifest-path server-rs/Cargo.toml`
|
||||
3. `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml exports_game_creation_app_asset_kind`
|
||||
4. `npm run check:encoding`
|
||||
5. `git diff --check`
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 风险:ts-rs 默认生成 enum/union 形态与 serde rename 不一致;必须以生成文件和 round-trip 测试为准。
|
||||
- 风险:shared-contracts 被 SpacetimeDB/WASM 使用;ts-rs 必须是 optional feature,默认构建不可依赖。
|
||||
- 回滚点:本切片仅新增 enum/绑定,不改变现有 manifest JSON 解析;若生成链路不稳定,可回滚本提交而不影响现有业务。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 【实施计划】AGC 资源 kind 词汇审计与绑定架构
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】AGC资源kind枚举化词汇审计与绑定架构-2026-09-15.md` |
|
||||
| Status | ready |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
允许修改:
|
||||
|
||||
- 主规范中的资源 kind 当前口径;
|
||||
- `docs/project-memory/plans/` 下本任务的里程碑/实施计划;
|
||||
- `docs/project-memory/todos/` 下本任务的扫描清单;
|
||||
- 为审计提供只读扫描脚本(如确有必要,脚本也只覆盖 shell)。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- `apps/ai-game-creator-shell` 业务代码;
|
||||
- server-side 代码、OpenAPI、SpacetimeDB schema;
|
||||
- MIME、`source.kind`、`generationKind` 和其他字符串领域。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 按“manifest asset kind / 其他字段 / 仅文本同名”三类扫描 shell 的 `kind`、`assetKind`、`asset_kind` 和 manifest 构造器。
|
||||
2. 对每个候选值回到调用上下文,标记生产写入、读取投影、协议输入、测试 fixture 或纯文本/路径。
|
||||
3. 对照 api-server 当前有效资源 kind,排除 legacy、billing/storage synthetic taxonomy 和开放字段。
|
||||
4. 记录正式候选 enum、Unknown 输入行为、保留字符串字段和后续 PR 边界。
|
||||
5. 形成下一里程碑的文件级修改顺序和验证矩阵;本里程碑结束后暂停,等待验收。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. `rg -n 'kind|assetKind|asset_kind' apps/ai-game-creator-shell/src apps/ai-game-creator-shell/src-tauri/src`
|
||||
2. `npm run check:doc-index`
|
||||
3. `npm run check:encoding`
|
||||
4. `git diff --check`
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 风险:同一字符串可能是 manifest kind、workflow kind 或路径名;任何未回到上下文确认的替换都禁止进入代码。
|
||||
- 风险:shared-contracts 被多个 server crate 使用;ts-rs 生成必须通过 feature/独立入口隔离默认构建。
|
||||
- 回滚点:本里程碑只有文档和审计清单变更;若词汇边界未能闭合,删除本计划和 todo,不进入代码里程碑。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 【里程碑】AGC 资源 kind 枚举化 Rust 绑定与内部收紧
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | in-progress |
|
||||
| Date | 2026-09-15 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` |
|
||||
|
||||
## 目标
|
||||
|
||||
将 GameCreationApp 资源 kind 从 shell 内裸字符串收紧为 Rust `GameCreationAppAssetKind`,并生成 shell 专属 TypeScript 绑定;所有同语义内部字段使用该类型,未知输入解析为 `Unknown` 并记录日志。
|
||||
|
||||
## 范围
|
||||
|
||||
- Rust enum、serde kebab-case、`Unknown` 与 category 派生边界。
|
||||
- ts-rs feature-gated 生成入口、生成文件和 verify 门禁。
|
||||
- `packages/shared/src/contracts/gameCreationApp.ts` 中重复的资源 kind 手写契约迁入 shell 生成绑定后的删除/收口。
|
||||
- shell 内资源 kind 参数、投影、筛选、Agent 资源引用和 manifest 读写边界的类型收紧。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- server-side 其他 `asset_kind`、External API DTO、SpacetimeDB schema。
|
||||
- `source.kind`、`generationKind`、MIME 和文件格式。
|
||||
- 其他领域字段为配合本里程碑进行“顺手枚举化”。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 词汇审计里程碑通过并关闭所有阻塞项。
|
||||
- 生成目录、TS 导入路径、Unknown 日志字段和 writer 禁止主动写 Unknown 的规则已确认。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] Rust enum 与生成的 TS 绑定值逐项一致,JSON 使用 kebab-case;正式资源成员为当前 17 个 canonical kind 加 `font`,另有 `Unknown` 解析成员。
|
||||
- [ ] 未知输入得到 `Unknown`,日志包含原始值与调用上下文。
|
||||
- [ ] 生产 writer 不再接受裸 `string` 作为 GameCreationApp kind。
|
||||
- [ ] `font`、`audio`、`sound-effect`、`background-music` 均有真实生产路径或明确的测试证据。
|
||||
- [ ] 旧 alias 不再由共享契约转换为 canonical。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 【里程碑】AGC 资源 kind 枚举化写入方清理与验证
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | planned |
|
||||
| Date | 2026-09-15 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` |
|
||||
|
||||
## 目标
|
||||
|
||||
逐处修正 shell 内仍产生非正式 GameCreationApp kind 的生产代码;保留仅属于 workflow、generation、路径或其他领域的字符串,并为这些边界补充清晰类型/命名说明而不扩大本 PR。
|
||||
|
||||
## 范围
|
||||
|
||||
- manifest 登记、恢复、回填、资源替换、画板/图集导出、字体导入、上传推断和 Agent 资源登记。
|
||||
- shell 前端资源投影、筛选和资源引用消费方。
|
||||
- 删除 alias/fallback 生产逻辑及其只服务于旧值的测试。
|
||||
- 为每个仍保留的非 manifest kind 字符串补充 todo,转入后续独立 PR。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 不改 `source.kind`、`generationKind` 等其他字段,即使它们恰好使用同名值。
|
||||
- 不修改 server-side 代码或公开 API。
|
||||
- 不实现字体格式/MIME enum;`font` 只作为本次 manifest asset kind 成员。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- Rust/TS 绑定里程碑通过。
|
||||
- 写入方扫描清单中每个 alias 都已有“manifest kind / 其他领域 / 删除”结论。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 新建、导入、生成、恢复、替换路径不会写入未纳入 enum 的 manifest kind。
|
||||
- [ ] 未知外部值只进入 `Unknown` 并有 trace,不会被静默改成 `image` 或 `asset`。
|
||||
- [ ] `source.kind` 与 `generationKind` 的既有字符串行为不变。
|
||||
- [ ] 定向 Rust、TS、绑定生成、编码和 diff 检查通过。
|
||||
@@ -0,0 +1,46 @@
|
||||
# 【里程碑】AGC 资源 kind 枚举化词汇审计与绑定架构
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | approved |
|
||||
| Date | 2026-09-15 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` |
|
||||
|
||||
## 目标
|
||||
|
||||
在 `apps/ai-game-creator-shell` 内建立唯一的 GameCreationApp 资源 kind 领域边界:Rust enum 是权威,TypeScript 由 `ts-rs` 生成;先完成全量词汇审计和生成架构合同,再进入业务代码替换。
|
||||
|
||||
## 范围
|
||||
|
||||
- 审计 shell 内所有可能表示 GameCreationApp 资源 kind 的字段、构造器、写入边界、筛选器、资源投影、Agent 资源引用和测试 fixture。
|
||||
- 以 api-server 当前仍有效的同语义 kind 作为命名与 serde 值参考,并评估 shell 当前实际生产 kind;排除 api-server legacy、billing/storage taxonomy 和其他开放领域。
|
||||
- 确认 Rust enum、`Unknown`、kebab-case JSON 和 ts-rs 生成文件的归属、生成命令与校验门禁。
|
||||
- 形成逐项 todo:当前值、真实语义、生产/测试状态、目标 enum、是否改写、验证方式。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 不扫描或修改 server-side 代码;api-server 只提供词汇参考。
|
||||
- 不改 `mediaType`、MIME、文件扩展名、`source.kind`、`source.generationKind`、任务/工作流 kind、路径名或 tracking scope。
|
||||
- 不在本里程碑修改 manifest writer、前端消费方或公开 API。
|
||||
- 不实现旧 manifest migration、alias 表或 fallback 清理。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 主规范已确认资源 kind 候选集合、`Unknown` 语义和 shell 边界。
|
||||
- `shared-contracts` 当前被 server-rs 多个 crate 使用,ts-rs 生成能力必须 feature-gated 或由独立生成入口启用,不能污染默认 SpacetimeDB/WASM 构建。
|
||||
- 当前已有 Tauri ts-rs 生成测试可作为生成命令形态参考,但不复用 UI Editor 类型目录作为资源契约目录。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] shell 内 manifest 资源 kind 的生产写入点、解析点、投影点和测试 fixture 有完整清单。
|
||||
- [ ] 每个旧值均已标注为:正式 GameCreationApp kind、其他领域字符串、legacy/错误 fallback;没有只按 grep 结果猜测的条目。
|
||||
- [ ] api-server 有效值、shell 扩展值和排除值各有明确依据。
|
||||
- [ ] 生成架构不会要求 server-side 默认构建安装 ts-rs。
|
||||
- [ ] todo 清单的每项都有关闭条件和对应验证证据。
|
||||
|
||||
## 证据要求
|
||||
|
||||
- 自动化:raw kind 扫描脚本/报告、Rust/TS 生成试验、serde kebab-case round-trip 试验。
|
||||
- 文档:主规范、todo 清单、下一里程碑实施计划互相链接且无冲突。
|
||||
- 边界:至少覆盖 `font`、`audio`、`sound-effect`、`background-music`、`Unknown`,以及 `source.kind`/`generationKind` 不误收编。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user