收紧 AGC 资源 kind 类型边界并统一族判据

- 将 manifest 与本地导入回执的资源 kind 改为 ts-rs 生成绑定

- 集中音频与视觉资源族判据并复用 Rust 枚举方法

- 放宽 Unknown 未解析语义并清理上传类型墓碑注释

- 标记共享生成绑定并同步项目决策与实施计划
This commit is contained in:
2026-09-18 15:07:36 +08:00
parent 03d7bca9a5
commit e5fd454bb3
16 changed files with 175 additions and 47 deletions
@@ -468,16 +468,8 @@ pub(crate) fn external_editor_api_credentials_override_is_active() -> bool {
}
/// 上传素材落盘的 manifest `kind`:只由**内容证据**推导(`mediaType` 优先,扩展名兜底)。
///
/// 不用 `uploaded` 这类**来源词**当类型:它不在 canonical 目录里,会经别名表落到
/// `image → unclassified`,把本该归「音频」「文档」的上传素材一起说成图片。
///
/// 更不能把 `ui` 当"图片的默认类型"`ui → ui-design → ui-interaction` 会把任意上传图片
/// 钉死在「UI 交互」栏,而"是不是 UI 素材"跟"扩展名是不是 .png"毫无关系;这类错值还不可恢复
/// ——读时自愈只在落盘 `category` 是 `unclassified` 且该 `kind` 能派生出非 `unclassified`
/// 分类时才生效,`kind` 本身错时自愈只会把错值放大。
///
/// 判不出内容类型时返回中性的 `asset`(派生 `unclassified` → 「待归类」),**不猜具体类型**。
/// 判不出内容类型时返回 `Unknown`;调用者负责决定是否拒绝写入或保留待后续归类,
/// 这里不猜具体类型。
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)
@@ -538,8 +530,9 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAsse
}
}
/// 登记边界的 kind 解析:空白入参按「没有信息」处理,落中性 `image`(→「待归类」)
/// 非空但认不出的原值严格收口成 `Unknown`(同样「待归类」)并把原始串交给 reporter 留痕
/// 登记边界的 kind 解析:空白入参按「没有信息」处理,沿用登记入口的 `Image` 默认值
/// 非空但认不出的原值严格收口成 `Unknown` 并把原始串交给 reporter 留痕
/// 由调用者决定是否以错误结束或继续后续归类。
///
/// 三条外部登记边界必须同口径:平台导入(`commands::imported_platform_asset_kind`)、
/// 画板导入(`sync_canvas_project_assets_at`)、外部 `register_local_asset`。
@@ -3999,17 +3999,7 @@ pub(crate) fn normalize_local_project_raster_resource_at(
"sourceSubtype 必须是图片族 kind,无法识别:{value}"
));
}
if !matches!(
kind,
GameCreationAppAssetKind::Image
| GameCreationAppAssetKind::Scene
| GameCreationAppAssetKind::Character
| GameCreationAppAssetKind::CharacterAnimation
| GameCreationAppAssetKind::Icon
| GameCreationAppAssetKind::IconSpritesheet
| GameCreationAppAssetKind::IconSpec
| GameCreationAppAssetKind::UiDesign
) {
if !kind.is_visual() {
return Err(format!(
"sourceSubtype 必须是图片族 kind,不能使用:{}",
kind.as_str()
@@ -6,6 +6,7 @@ import { open as openNativeFileDialog } from '@tauri-apps/plugin-dialog';
import { X } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { GameCreationAppAssetKind } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
loadEditorAssetLibrary,
resolveClientAssetReadUrl,
@@ -58,7 +59,7 @@ type LocalProjectFilesResponse = { files?: LocalProjectFile[] };
type ImportAssetResponse = {
id: string;
localPath: string;
assetKind?: string | null;
assetKind?: GameCreationAppAssetKind | null;
};
type GenericImportResponse = { assets: ImportAssetResponse[] };
@@ -1,10 +1,12 @@
import type { FileManagerFile } from '@cubone/react-file-manager';
import type { ComponentType, ReactNode } from 'react';
import type { GameCreationAppAssetKind } from '../../../../../packages/shared/src/contracts/gameCreationApp';
export type ImportedAsset = {
id: string;
localPath: string;
assetKind: string | null;
assetKind: GameCreationAppAssetKind | null;
};
export type AssetSource = 'local' | 'remote';
@@ -104,7 +106,7 @@ export type ManifestAsset = {
id: string;
localPath: string;
mediaType: string;
kind?: string;
kind?: GameCreationAppAssetKind;
source?: { kind?: string };
};
@@ -1,6 +1,8 @@
import {
type GameCreationAppAssetSourceKind,
type GameCreationAppManifest,
isGameCreationAppAssetAudioKind,
isGameCreationAppAssetVisualKind,
selectGameCreationAppReadyTasks,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { taskRowsFromManifest } from '../agent-runtime';
@@ -102,12 +104,9 @@ export function isProjectAudioAsset(
asset: GameCreationAppManifest['assets'][number],
) {
const mediaType = asset.mediaType.toLowerCase();
const kind = asset.kind.toLowerCase();
return (
mediaType.startsWith('audio/') ||
kind === 'audio' ||
kind === 'sound-effect' ||
kind === 'background-music'
isGameCreationAppAssetAudioKind(asset.kind)
);
}
@@ -115,14 +114,11 @@ export function isProjectVisualAsset(
asset: GameCreationAppManifest['assets'][number],
) {
const mediaType = asset.mediaType.toLowerCase();
const kind = asset.kind.toLowerCase();
return (
mediaType.startsWith('image/') ||
mediaType.startsWith('video/') ||
mediaType === 'application/vnd.genarrative.image-sequence' ||
kind === 'image' ||
kind === 'icon' ||
kind === 'character-animation'
isGameCreationAppAssetVisualKind(asset.kind)
);
}
@@ -1,6 +1,7 @@
import { invoke } from '@tauri-apps/api/core';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { GameCreationAppAssetKind } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import type { ImportedAsset } from '../../components/AssetImporter';
import {
prepareDesignImageBatch,
@@ -78,7 +79,11 @@ import { useUiEditorNodeFocus } from './useUiEditorNodeFocus';
const SEPARATION_IMPORT_BATCH_SIZE = 100;
type LocalImageImportResponse = {
assets: Array<{ id: string; localPath: string; assetKind?: string | null }>;
assets: Array<{
id: string;
localPath: string;
assetKind?: GameCreationAppAssetKind | null;
}>;
};
function normalizeProjectRelativePath(path: string): string {
@@ -1074,7 +1079,11 @@ export function useUiEditorSession(
];
const importedByPath = new Map<
string,
{ id: string; localPath: string; assetKind: string | null }
{
id: string;
localPath: string;
assetKind: GameCreationAppAssetKind | null;
}
>();
for (
let index = 0;