修复客户端 Skill 名称大小写冲突

Skill 导入和重命名按 ASCII 大小写不敏感规则分配名称

保留 MCP 名称原有大小写敏感语义

补充 Foo、foo、FOO 回归测试

同步更新 DirectProject 扩展命名方案
This commit is contained in:
2026-09-01 07:44:58 +00:00
parent bb0854ec5b
commit 4561458fb5
2 changed files with 55 additions and 10 deletions
@@ -363,14 +363,37 @@ fn native_name(value: &str) -> String {
}
fn allocate_name(original_name: &str, existing: &mut BTreeSet<String>) -> (String, bool) {
allocate_name_with_case_mode(original_name, existing, false)
}
fn allocate_skill_name(original_name: &str, existing: &mut BTreeSet<String>) -> (String, bool) {
allocate_name_with_case_mode(original_name, existing, true)
}
fn allocate_name_with_case_mode(
original_name: &str,
existing: &mut BTreeSet<String>,
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");
@@ -180,6 +180,8 @@ art-skill-3
- MCP 与 MCP 之间避免重复;
- Skill 和 MCP 可以同名,因为属于不同的运行时命名空间。
Skill 名称冲突按 ASCII 大小写不敏感判断(例如 `Foo` 与 `foo` 视为冲突),以匹配 Windows 和默认大小写不敏感 macOS 文件系统上的运行时 Skill 目录;MCP Server 名称继续按原生大小写敏感规则处理。
如果名称相同但内容不同,视为名称冲突;如果内容指纹相同,视为重复导入。两种情况都保留新项并使用同一套后缀分配逻辑。
提示合并为一次普通通知,例如: