From 438fe730551cc12aa4568f47c337209ef7407916 Mon Sep 17 00:00:00 2001 From: qianshengjia <840646206@qq.com> Date: Mon, 24 Aug 2026 10:48:16 +0800 Subject: [PATCH 1/3] copy agent skip duplicate skills --- backend/apps/agent_repository_app.py | 5 ++ backend/services/agent_repository_service.py | 17 +++- .../components/AgentRepositoryCopyDialog.tsx | 87 +++++++++++++++---- .../useAgentRepositoryListings.ts | 7 +- frontend/public/locales/en/common.json | 3 + frontend/public/locales/zh/common.json | 3 + frontend/services/agentRepositoryService.ts | 17 ++-- 7 files changed, 110 insertions(+), 29 deletions(-) diff --git a/backend/apps/agent_repository_app.py b/backend/apps/agent_repository_app.py index 37a023b8d1..3bbf5c83d1 100644 --- a/backend/apps/agent_repository_app.py +++ b/backend/apps/agent_repository_app.py @@ -246,6 +246,10 @@ async def check_repository_import_precheck_api( @agent_repository_router.post("/{agent_repository_id}/import") async def import_agent_from_repository_api( agent_repository_id: int, + skip_duplicates: bool = Query( + False, + description="If True, skip duplicate skills and proceed with the remaining skills", + ), authorization: Optional[str] = Header(None), ): """Import an agent tree from a marketplace repository listing into the current tenant.""" @@ -255,6 +259,7 @@ async def import_agent_from_repository_api( agent_repository_id=agent_repository_id, tenant_id=tenant_id, authorization=authorization, + skip_duplicates=skip_duplicates, ) return JSONResponse(status_code=HTTPStatus.OK, content={}) except UnauthorizedError as e: diff --git a/backend/services/agent_repository_service.py b/backend/services/agent_repository_service.py index 9369075205..03f86c5af4 100644 --- a/backend/services/agent_repository_service.py +++ b/backend/services/agent_repository_service.py @@ -12,7 +12,7 @@ VALID_OWNERSHIP_FILTERS, VALID_REPOSITORY_STATUSES, ) -from consts.exceptions import UnauthorizedError +from consts.exceptions import SkillDuplicateError, UnauthorizedError from consts.model import AgentRepositorySnapshot from consts.notification import EVENT_TYPE_REPOSITORY_REVIEW_PENDING, RESOURCE_TYPE_AGENT_REPOSITORY from database.agent_db import search_agent_info_by_agent_id @@ -1045,8 +1045,13 @@ async def import_agent_from_repository_impl( agent_repository_id: int, tenant_id: str, authorization: str, + skip_duplicates: bool = False, ) -> Dict[int, int]: - """Import an agent tree from a marketplace repository listing into the current tenant.""" + """Import an agent tree from a marketplace repository listing into the current tenant. + + When skip_duplicates=True, duplicate skills are silently dropped. If all skills are + duplicates, the agent is imported without any skills (same as AgentImportWizard behavior). + """ record = get_agent_repository_by_id( agent_repository_id, tenant_id, @@ -1059,13 +1064,19 @@ async def import_agent_from_repository_impl( raise ValueError("Repository listing has no agent snapshot") snapshot = AgentRepositorySnapshot.model_validate(agent_info_json) - if snapshot.skills: + + if snapshot.skills and not skip_duplicates: result = await import_agent_with_skills_impl( snapshot, snapshot.skills, authorization, ) else: + if snapshot.skills and skip_duplicates: + logger.info( + "Skipping %d skills on repository import (id=%s) due to conflicts", + len(snapshot.skills), agent_repository_id, + ) result = await import_agent_impl(snapshot, authorization) affected = increment_agent_repository_downloads(agent_repository_id) diff --git a/frontend/app/[locale]/agent-space/components/AgentRepositoryCopyDialog.tsx b/frontend/app/[locale]/agent-space/components/AgentRepositoryCopyDialog.tsx index 47814c14d7..8363beacaf 100644 --- a/frontend/app/[locale]/agent-space/components/AgentRepositoryCopyDialog.tsx +++ b/frontend/app/[locale]/agent-space/components/AgentRepositoryCopyDialog.tsx @@ -13,6 +13,7 @@ import { ExternalLink, Plug, RefreshCw, + SkipForward, Sparkles, Wrench, X, @@ -105,6 +106,14 @@ export function AgentRepositoryCopyDialog({ [precheck] ); + const skillConflictItems = useMemo( + () => + abnormalItems.filter((item) => item.reason_code === "skill_duplicate"), + [abnormalItems] + ); + const hasSkillConflicts = skillConflictItems.length > 0; + const skillConflictNames = skillConflictItems.map((item) => item.name); + const percent = precheck?.percent ?? 0; const hasAbnormal = precheck?.has_abnormal ?? false; @@ -116,14 +125,21 @@ export function AgentRepositoryCopyDialog({ router.push(`/${locale}${path}`); }; - const handleCopy = async () => { + const handleCopy = async (skipDuplicates = false) => { if (!agentRepositoryId) { return; } try { - await importMutation.mutateAsync(agentRepositoryId); + await importMutation.mutateAsync({ + agentRepositoryId, + skipDuplicates, + }); message.success( - t("agentRepository.copy.success", { name: listingTitle }) + skipDuplicates + ? t("agentRepository.copy.successWithoutSkills", { + name: listingTitle, + }) + : t("agentRepository.copy.success", { name: listingTitle }) ); onOpenChange(false); onSuccess?.(); @@ -141,7 +157,7 @@ export function AgentRepositoryCopyDialog({ detail?.type === "skill_duplicate" && Array.isArray(detail.duplicate_skills) ) { - message.error( + message.warning( t("agentRepository.copy.skillDuplicate", { names: detail.duplicate_skills.join(", "), }) @@ -169,17 +185,49 @@ export function AgentRepositoryCopyDialog({ width={520} destroyOnHidden footer={ -
- - +
+ {hasSkillConflicts ? ( +
+
+ + + {t("agentRepository.copy.skillConflict", { + count: skillConflictItems.length, + })} + + {skillConflictNames.join(", ")} + + +
+
+ +
+
+ ) : null} +
+ + +
} styles={{ @@ -354,6 +402,8 @@ function RequirementTypeGroup({ const abnormal = status === "abnormal"; const typeLabel = getRepositoryRequirementTypeLabel(type, t); const activatePath = getRepositoryRequirementActivatePath(type); + const isSkillDuplicate = + items.length > 0 && items[0]?.reason_code === "skill_duplicate"; return (
@@ -363,7 +413,12 @@ function RequirementTypeGroup({ {typeLabel}
{abnormal ? ( - activatePath ? ( + isSkillDuplicate ? ( + + + {getRepositoryRequirementReasonLabel("skill_duplicate", t)} + + ) : activatePath ? (