From 4561458fb54f51d58656e5b5a155c2d597edb62f Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 1 Sep 2026 07:44:58 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=20Skill=20=E5=90=8D=E7=A7=B0=E5=A4=A7=E5=B0=8F=E5=86=99?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skill 导入和重命名按 ASCII 大小写不敏感规则分配名称 保留 MCP 名称原有大小写敏感语义 补充 Foo、foo、FOO 回归测试 同步更新 DirectProject 扩展命名方案 --- .../src-tauri/src/client_extensions.rs | 63 ++++++++++++++++--- ...roject客户端Skill与MCP扩展导入方案-2026-08-31.md | 2 + 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs b/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs index 4bb9d7453..c2b072935 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs @@ -363,14 +363,37 @@ fn native_name(value: &str) -> String { } fn allocate_name(original_name: &str, existing: &mut BTreeSet) -> (String, bool) { + allocate_name_with_case_mode(original_name, existing, false) +} + +fn allocate_skill_name(original_name: &str, existing: &mut BTreeSet) -> (String, bool) { + allocate_name_with_case_mode(original_name, existing, true) +} + +fn allocate_name_with_case_mode( + original_name: &str, + existing: &mut BTreeSet, + case_insensitive: bool, +) -> (String, bool) { let base = native_name(original_name); - if existing.insert(base.clone()) { + let conflicts = |candidate: &str| { + if case_insensitive { + existing + .iter() + .any(|name| name.eq_ignore_ascii_case(candidate)) + } else { + existing.contains(candidate) + } + }; + if !conflicts(&base) { + existing.insert(base.clone()); return (base, false); } let mut suffix = 2_u32; loop { let candidate = format!("{base}-{suffix}"); - if existing.insert(candidate.clone()) { + if !conflicts(&candidate) { + existing.insert(candidate.clone()); return (candidate, true); } suffix += 1; @@ -1248,12 +1271,14 @@ pub(crate) fn import_client_extension( .map(|item| item.name.clone()); duplicate |= duplicate_name.is_some(); let names = used_names.entry(extension_type.clone()).or_default(); - let (name, was_renamed) = allocate_name( - duplicate_name - .as_deref() - .unwrap_or(&candidate.original_name), - names, - ); + let requested_name = duplicate_name + .as_deref() + .unwrap_or(&candidate.original_name); + let (name, was_renamed) = if extension_type == "skill" { + allocate_skill_name(requested_name, names) + } else { + allocate_name(requested_name, names) + }; renamed |= was_renamed; let stored = StoredExtensionItem { id: new_id("extension"), @@ -1329,7 +1354,13 @@ pub(crate) fn rename_client_extension( let extension_type = index.items[item_index].extension_type.clone(); let conflict = (extension_type == "mcp" && normalized == RESERVED_MCP_SERVER_NAME) || index.items.iter().enumerate().any(|(index, item)| { - index != item_index && item.extension_type == extension_type && item.name == normalized + index != item_index + && item.extension_type == extension_type + && if extension_type == "skill" { + item.name.eq_ignore_ascii_case(&normalized) + } else { + item.name == normalized + } }); let final_name = if conflict { let mut names = index @@ -1341,7 +1372,11 @@ pub(crate) fn rename_client_extension( if extension_type == "mcp" { names.insert(RESERVED_MCP_SERVER_NAME.to_string()); } - allocate_name(&normalized, &mut names).0 + if extension_type == "skill" { + allocate_skill_name(&normalized, &mut names).0 + } else { + allocate_name(&normalized, &mut names).0 + } } else { normalized }; @@ -1379,6 +1414,14 @@ mod tests { ); } + #[test] + fn skill_names_avoid_case_insensitive_runtime_collisions() { + let mut names = BTreeSet::new(); + assert_eq!(allocate_skill_name("Foo", &mut names).0, "Foo"); + assert_eq!(allocate_skill_name("foo", &mut names).0, "foo-2"); + assert_eq!(allocate_skill_name("FOO", &mut names).0, "FOO-3"); + } + #[test] fn parse_mcp_toml_splits_each_server() { let directory = tempfile::tempdir().expect("temp directory"); diff --git a/docs/technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md b/docs/technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md index fd6e70d3c..9fad4995c 100644 --- a/docs/technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md +++ b/docs/technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md @@ -180,6 +180,8 @@ art-skill-3 - MCP 与 MCP 之间避免重复; - Skill 和 MCP 可以同名,因为属于不同的运行时命名空间。 +Skill 名称冲突按 ASCII 大小写不敏感判断(例如 `Foo` 与 `foo` 视为冲突),以匹配 Windows 和默认大小写不敏感 macOS 文件系统上的运行时 Skill 目录;MCP Server 名称继续按原生大小写敏感规则处理。 + 如果名称相同但内容不同,视为名称冲突;如果内容指纹相同,视为重复导入。两种情况都保留新项并使用同一套后缀分配逻辑。 提示合并为一次普通通知,例如: