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={
-
-
-
}
- loading={importMutation.isPending}
- disabled={!precheck || isLoading || isError}
- onClick={handleCopy}
- >
- {t("agentRepository.card.copy")}
-
+
+ {hasSkillConflicts ? (
+
+
+
+
+ {t("agentRepository.copy.skillConflict", {
+ count: skillConflictItems.length,
+ })}
+
+ {skillConflictNames.join(", ")}
+
+
+
+
+ }
+ loading={importMutation.isPending}
+ onClick={() => handleCopy(true)}
+ >
+ {t("agentRepository.copy.skipAndCopy")}
+
+
+
+ ) : null}
+
+
+ }
+ loading={importMutation.isPending}
+ disabled={
+ !precheck ||
+ isLoading ||
+ isError ||
+ hasSkillConflicts
+ }
+ onClick={() => handleCopy(false)}
+ >
+ {t("agentRepository.card.copy")}
+
+
}
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 ? (