diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index c006fed28..df52d77df 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -1245,7 +1245,8 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value { "kind", DIRECT_TOOL_BRIDGE_MAX_RESOURCE_KIND_CHARS, )? - .map(|kind| GameCreationAppAssetKind::parse_with_context(&kind, "asset.list.kind")); + .map(|kind| bridge_asset_list_kind_filter(&kind)) + .transpose()?; let asset_id = bridge_optional_bounded_string( arguments, "assetId", @@ -1325,6 +1326,24 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value { } } +/// `asset.list` 的 `kind` 过滤:只接受 canonical 值,认不出的值直接报错。 +/// +/// 以前认不出的值会收口成 `unknown` 再参与等值过滤,结果是**静默返回空列表**:调用方(模型) +/// 会以为项目里没有这类资产,而不是"你传的 kind 不合法",于是继续按错误前提往下走。 +/// `unknown` 本身仍是合法输入(确有 kind 未知的资产),只拒绝"既不是 canonical、也不是字面 +/// `unknown`"的原值。 +fn bridge_asset_list_kind_filter(raw: &str) -> Result { + let kind = GameCreationAppAssetKind::parse_with_context(raw, "asset.list.kind"); + if kind == GameCreationAppAssetKind::Unknown + && raw.trim() != GameCreationAppAssetKind::Unknown.as_str() + { + return Err(format!( + "kind 不是已知的 manifest 资源 kind:{raw};请改用 canonical kind(如 image、scene、character、icon、icon-spritesheet、character-animation、audio、video、document)" + )); + } + Ok(kind) +} + fn bridge_project_file_class(path: &str) -> (&'static str, Option<&'static str>) { let extension = Path::new(path) .extension() @@ -2745,6 +2764,28 @@ mod tests { .contains("x-genarrative-client:")); } + /// `asset.list` 的 `kind` 过滤必须报错,而不是静默返回空列表:旧值会收口成 `unknown` 再参与 + /// 等值过滤,匹配不到任何资产,让模型误以为项目里没有这类资产。校验必须发生在项目访问之前。 + #[test] + fn bridge_asset_list_rejects_non_canonical_kind_filter() { + let root = tempfile::tempdir().expect("bridge root"); + let rejected = + bridge_list_registered_assets(root.path(), &json!({ "kind": "art-spritesheet-slice" })); + assert_eq!(rejected["isError"], true); + assert!( + rejected.to_string().contains("art-spritesheet-slice"), + "错误里必须带上原值,调用方才能自纠:{rejected}" + ); + // 字面 `unknown` 仍是合法过滤值:它应当越过 kind 校验,失败点换成项目侧(此目录未初始化)。 + let allowed = bridge_list_registered_assets(root.path(), &json!({ "kind": "unknown" })); + assert!( + !allowed + .to_string() + .contains("不是已知的 manifest 资源 kind"), + "未知 kind 的字面量本身是合法输入:{allowed}" + ); + } + #[test] fn bridge_argument_bounds_are_deterministic() { assert_eq!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index f242153cd..af2421e91 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -307,7 +307,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab "type": "string", "minLength": 1, "maxLength": 80, - "description": "可选的 manifest 资源 kind 精确过滤,例如 video、character-animation、sound-effect、background-music 或 icon-spritesheet" + "description": "可选的 manifest 资源 kind 精确过滤,必须是 canonical kind(例如 video、character-animation、sound-effect、background-music、icon-spritesheet);传非 canonical 值会被拒绝并报错,不会静默返回空列表" }, "assetId": { "type": "string",