diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index e7bd84f06..641560770 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -252,6 +252,50 @@ impl SkillService { .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| skill.directory.clone()); + // 检查数据库中是否已有同名 directory 的 skill(来自其他仓库) + let existing_skills = db.get_all_installed_skills()?; + for existing in existing_skills.values() { + if existing.directory.eq_ignore_ascii_case(&install_name) { + // 检查是否来自同一仓库 + let same_repo = existing.repo_owner.as_deref() == Some(&skill.repo_owner) + && existing.repo_name.as_deref() == Some(&skill.repo_name); + if same_repo { + // 同一仓库的同名 skill,返回现有记录(可能需要更新启用状态) + let mut updated = existing.clone(); + updated.apps.set_enabled_for(current_app, true); + db.save_skill(&updated)?; + Self::sync_to_app_dir(&updated.directory, current_app)?; + log::info!( + "Skill {} 已存在,更新 {:?} 启用状态", + updated.name, + current_app + ); + return Ok(updated); + } else { + // 不同仓库的同名 skill,报错 + return Err(anyhow!(format_skill_error( + "SKILL_DIRECTORY_CONFLICT", + &[ + ("directory", &install_name), + ( + "existing_repo", + &format!( + "{}/{}", + existing.repo_owner.as_deref().unwrap_or("unknown"), + existing.repo_name.as_deref().unwrap_or("unknown") + ) + ), + ( + "new_repo", + &format!("{}/{}", skill.repo_owner, skill.repo_name) + ), + ], + Some("uninstallFirst"), + ))); + } + } + } + let dest = ssot_dir.join(&install_name); // 如果已存在则跳过下载 @@ -933,10 +977,12 @@ impl SkillService { Ok(meta) } - /// 去重技能列表 + /// 去重技能列表(基于完整 key,不同仓库的同名 skill 分开显示) fn deduplicate_discoverable_skills(skills: &mut Vec) { let mut seen = HashMap::new(); skills.retain(|skill| { + // 使用完整 key(owner/repo:directory)作为唯一标识 + // 这样不同仓库的同名 skill 会分开显示 let unique_key = skill.key.to_lowercase(); if let std::collections::hash_map::Entry::Vacant(e) = seen.entry(unique_key) { e.insert(true); diff --git a/src/components/skills/SkillsPage.tsx b/src/components/skills/SkillsPage.tsx index 7ab030e0b..73865513f 100644 --- a/src/components/skills/SkillsPage.tsx +++ b/src/components/skills/SkillsPage.tsx @@ -65,10 +65,17 @@ export const SkillsPage = forwardRef( const addRepoMutation = useAddSkillRepo(); const removeRepoMutation = useRemoveSkillRepo(); - // 已安装的 directory 集合 - const installedDirs = useMemo(() => { + // 已安装的 skill key 集合(使用 directory + repoOwner + repoName 组合判断) + const installedKeys = useMemo(() => { if (!installedSkills) return new Set(); - return new Set(installedSkills.map((s) => s.directory.toLowerCase())); + return new Set( + installedSkills.map((s) => { + // 构建唯一 key:directory + repoOwner + repoName + const owner = s.repoOwner?.toLowerCase() || ""; + const name = s.repoName?.toLowerCase() || ""; + return `${s.directory.toLowerCase()}:${owner}:${name}`; + }), + ); }, [installedSkills]); type DiscoverableSkillItem = DiscoverableSkill & { installed: boolean }; @@ -80,12 +87,14 @@ export const SkillsPage = forwardRef( const installName = d.directory.split("/").pop()?.toLowerCase() || d.directory.toLowerCase(); + // 使用 directory + repoOwner + repoName 组合判断是否已安装 + const key = `${installName}:${d.repoOwner.toLowerCase()}:${d.repoName.toLowerCase()}`; return { ...d, - installed: installedDirs.has(installName), + installed: installedKeys.has(key), }; }); - }, [discoverableSkills, installedDirs]); + }, [discoverableSkills, installedKeys]); const loading = loadingDiscoverable || fetchingDiscoverable; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9f08e7eda..00c9af3e7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -972,6 +972,7 @@ "downloadTimeoutHint": "Please check network connection or retry later", "skillPathNotFound": "Skill path '{{path}}' not found in repository {{owner}}/{{name}}", "skillDirNotFound": "Skill directory not found: {{path}}", + "directoryConflict": "Skill directory '{{directory}}' is already occupied by {{existing_repo}}, cannot install from {{new_repo}}", "emptyArchive": "Downloaded archive is empty", "downloadFailed": "Download failed: HTTP {{status}}", "allBranchesFailed": "All branches failed, tried: {{branches}}", @@ -990,7 +991,8 @@ "retryLater": "Please retry later", "checkRepoUrl": "Please check repository URL and branch name", "checkDiskSpace": "Please check disk space", - "checkPermission": "Please check directory permissions" + "checkPermission": "Please check directory permissions", + "uninstallFirst": "Please uninstall the existing skill with the same name first" } }, "repo": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 029fbc6bc..a9254f47b 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -972,6 +972,7 @@ "downloadTimeoutHint": "ネットワークを確認するか、時間をおいて再試行してください", "skillPathNotFound": "リポジトリ {{owner}}/{{name}} にスキルパス '{{path}}' がありません", "skillDirNotFound": "スキルディレクトリが見つかりません: {{path}}", + "directoryConflict": "スキルディレクトリ '{{directory}}' は既に {{existing_repo}} で使用されています。{{new_repo}} からインストールできません", "emptyArchive": "ダウンロードしたアーカイブが空です", "downloadFailed": "ダウンロードに失敗しました: HTTP {{status}}", "allBranchesFailed": "すべてのブランチで失敗しました。試行: {{branches}}", @@ -990,7 +991,8 @@ "retryLater": "時間をおいて再試行してください", "checkRepoUrl": "リポジトリ URL とブランチ名を確認してください", "checkDiskSpace": "ディスク容量を確認してください", - "checkPermission": "ディレクトリの権限を確認してください" + "checkPermission": "ディレクトリの権限を確認してください", + "uninstallFirst": "同名のスキルを先にアンインストールしてください" } }, "repo": { diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index b83d8fc0b..e86c7aae6 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -972,6 +972,7 @@ "downloadTimeoutHint": "请检查网络连接或稍后重试", "skillPathNotFound": "仓库 {{owner}}/{{name}} 中未找到技能路径 '{{path}}'", "skillDirNotFound": "技能目录不存在:{{path}}", + "directoryConflict": "技能目录 '{{directory}}' 已被 {{existing_repo}} 占用,无法从 {{new_repo}} 安装", "emptyArchive": "下载的压缩包为空", "downloadFailed": "下载失败:HTTP {{status}}", "allBranchesFailed": "所有分支下载失败,尝试了:{{branches}}", @@ -990,7 +991,8 @@ "retryLater": "请稍后重试", "checkRepoUrl": "请检查仓库地址和分支名称", "checkDiskSpace": "请检查磁盘空间", - "checkPermission": "请检查目录权限" + "checkPermission": "请检查目录权限", + "uninstallFirst": "请先卸载已安装的同名技能" } }, "repo": { diff --git a/src/lib/errors/skillErrorParser.ts b/src/lib/errors/skillErrorParser.ts index 6b0f93ed3..db6f6d905 100644 --- a/src/lib/errors/skillErrorParser.ts +++ b/src/lib/errors/skillErrorParser.ts @@ -35,6 +35,7 @@ function getErrorI18nKey(code: string): string { DOWNLOAD_TIMEOUT: "skills.error.downloadTimeout", DOWNLOAD_FAILED: "skills.error.downloadFailed", SKILL_DIR_NOT_FOUND: "skills.error.skillDirNotFound", + SKILL_DIRECTORY_CONFLICT: "skills.error.directoryConflict", EMPTY_ARCHIVE: "skills.error.emptyArchive", GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed", }; @@ -52,6 +53,7 @@ function getSuggestionI18nKey(suggestion: string): string { retryLater: "skills.error.suggestion.retryLater", checkRepoUrl: "skills.error.suggestion.checkRepoUrl", checkPermission: "skills.error.suggestion.checkPermission", + uninstallFirst: "skills.error.suggestion.uninstallFirst", http403: "skills.error.http403", http404: "skills.error.http404", http429: "skills.error.http429",