From ac476d37646a745d4e23bec4cee2b08460908f46 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Tue, 6 Oct 2026 13:42:15 +0800 Subject: [PATCH] fix(servers): number existing duplicate names --- GLOSSARY.md | 2 +- .../CatalogWorkflow+Duplicate.swift | 11 +++++++++- .../CatalogWorkflowDuplicateTests.swift | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/GLOSSARY.md b/GLOSSARY.md index 53e11d3a..af87d410 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -29,7 +29,7 @@ _避免用词_:导出(导出指配置组或诊断报告等文件快照)。 把剪贴板文本或文件中的服务器配置引入配置目录、形成独立手动子树的操作。来源资料中的标识只用于装配其内部结构,导入产生的每个节点都获得全新的本地持久身份;同一资料重复导入产生相互独立的服务器配置,不与既有节点合并或去重。 **节点复制(Node duplication)**: -在原节点之后的同级位置创建独立手动副本。组复制包含整棵子树,保留层级、顺序、连接字段和空组;所有节点身份及凭据引用重新生成。支持手动服务器、手动组和根层订阅组,不支持订阅服务器或嵌套订阅子组。订阅副本不携带订阅来源资料;顶层副本名称加本地化后缀并在同级重名时追加序号。目录和凭据作为一次逻辑变更提交,失败不发布部分子树,凭据回滚不完整时必须报告。复制不切换活动目标;在活动组内新增的副本按普通目录变更进入运行时收敛。 +在原节点之后的同级位置创建独立手动副本。组复制包含整棵子树,保留层级、顺序、连接字段和空组;所有节点身份及凭据引用重新生成。支持手动服务器、手动组和根层订阅组,不支持订阅服务器或嵌套订阅子组。订阅副本不携带订阅来源资料;顶层副本名称加本地化后缀并在同级重名时追加序号;名称末尾已有“副本”或“Copy”(含编号)时沿用该后缀,以同级未占用的编号命名,避免重复追加后缀。目录和凭据作为一次逻辑变更提交,失败不发布部分子树,凭据回滚不完整时必须报告。复制不切换活动目标;在活动组内新增的副本按普通目录变更进入运行时收敛。 **导入来源(Import source)**: 一次服务器导入中可独立处理的一份资料,例如剪贴板文本或单个文件。每个来源独立解码、独立提交,是成功、部分成功与失败报告的粒度单位;一个来源失败不影响其他来源。 diff --git a/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Duplicate.swift b/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Duplicate.swift index a175843d..6070a039 100644 --- a/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Duplicate.swift +++ b/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Duplicate.swift @@ -17,9 +17,18 @@ extension CatalogWorkflow { let siblings = try source.children(of: parent) guard let position = siblings.firstIndex(of: id) else { throw CatalogError.nodeNotFound(id) } let names = Set(siblings.compactMap { source.entry(for: $0)?.displayName }) - let base = "\(entry.displayName) \(nameSuffix)" + var base = "\(entry.displayName) \(nameSuffix)" var name = base var number = 2 + // 两种语言的已保存后缀都识别,切换语言后仍沿用原名称的复制序列。 + if let suffixRange = entry.displayName.range( + of: #" (?:副本|Copy)(?: [0-9]+)?$"#, options: .regularExpression) + { + let suffix = entry.displayName[suffixRange].split(separator: " ")[0] + base = "\(entry.displayName[..