diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs index 5f352dcc6..45ea19f54 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs @@ -10,6 +10,9 @@ use crate::agent::{ use std::path::Path; pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32; +// Skill 引用也是非文本 part,但没有走 reference_count:它每一条都会触发一次 +// `root//SKILL.md` 文件探测并往 turn input 里加一项,所以单独设上限。 +pub(crate) const MAX_DIRECT_CODEX_SKILL_REFERENCES: usize = 32; pub(crate) fn validate_direct_codex_user_item( root: &Path, @@ -30,6 +33,7 @@ pub(crate) fn validate_direct_codex_user_item( let manifest = read_manifest_for_project(root)?; let mut reference_count = 0usize; let mut attachment_count = 0usize; + let mut skill_count = 0usize; for part in &message.content { match part { DirectCodexUserContentPart::InputText { .. } => {} @@ -38,6 +42,12 @@ pub(crate) fn validate_direct_codex_user_item( validate_resource_id_and_manifest(&manifest, resource_id)?; } DirectCodexUserContentPart::AgcSkillReference { name } => { + skill_count = skill_count.saturating_add(1); + if skill_count > MAX_DIRECT_CODEX_SKILL_REFERENCES { + return Err(format!( + "一次最多引用 {MAX_DIRECT_CODEX_SKILL_REFERENCES} 个 Skill" + )); + } let name = name.trim(); if name.is_empty() || name.chars().count() > 120 @@ -150,7 +160,10 @@ fn validate_runtime_region_reference( #[cfg(test)] mod tests { - use super::{content_has_meaningful_input, validate_direct_codex_user_item}; + use super::{ + content_has_meaningful_input, validate_direct_codex_user_item, + MAX_DIRECT_CODEX_SKILL_REFERENCES, + }; use crate::agent::direct_codex_user_item::model::DirectCodexUserContentPart; use serde_json::json; @@ -246,6 +259,26 @@ mod tests { assert!(error.contains("缺少项目路径"), "{error}"); } + #[test] + fn inline_skill_reference_count_is_bounded_independently() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "validation-test", "校验测试") + .expect("init project"); + let content = (0..=MAX_DIRECT_CODEX_SKILL_REFERENCES) + .map(|index| json!({ "type": "agc_skill_reference", "name": format!("skill-{index}") })) + .collect::>(); + let item = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": content, + "id": "turn-1:user" + })) + .expect("deserialize user item"); + let error = validate_direct_codex_user_item(root.path(), &item) + .expect_err("too many skill references must be rejected"); + assert!(error.contains("最多引用"), "{error}"); + } + #[test] fn attachment_name_and_media_type_are_bounded_and_well_formed() { let root = tempfile::tempdir().expect("temp project");