asset.list 的 kind 过滤改为拒绝非 canonical 值

- direct_tool_bridge.rs:`kind` 收口后既不是 canonical、也不是字面 `unknown` 时直接返回工具错误并带上原值,不再静默过滤出空列表。
- direct_tool_bridge.rs:新增用例,钉住"项目访问前就报错"与"字面 `unknown` 仍是合法过滤值"。
- direct_tools_mcp.rs:schema 描述改为必须是 canonical kind、非 canonical 值会被拒绝。
This commit is contained in:
2026-09-17 13:42:22 +08:00
parent 29ba9a358e
commit 5f13698129
2 changed files with 43 additions and 2 deletions
@@ -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<GameCreationAppAssetKind, String> {
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!(
@@ -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-musicicon-spritesheet"
"description": "可选的 manifest 资源 kind 精确过滤,必须是 canonical kind例如 video、character-animation、sound-effect、background-musicicon-spritesheet);传非 canonical 值会被拒绝并报错,不会静默返回空列表"
},
"assetId": {
"type": "string",