修复客户端 Skill 名称大小写冲突
Skill 导入和重命名按 ASCII 大小写不敏感规则分配名称 保留 MCP 名称原有大小写敏感语义 补充 Foo、foo、FOO 回归测试 同步更新 DirectProject 扩展命名方案
This commit is contained in:
@@ -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 名称继续按原生大小写敏感规则处理。
|
||||
|
||||
如果名称相同但内容不同,视为名称冲突;如果内容指纹相同,视为重复导入。两种情况都保留新项并使用同一套后缀分配逻辑。
|
||||
|
||||
提示合并为一次普通通知,例如:
|
||||
|
||||
Reference in New Issue
Block a user