From 1019cf7f3abe878edd6e3af5c0ca847ecf8e4fae Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Wed, 10 Jun 2026 16:00:53 +0800 Subject: [PATCH 01/13] Move non-shadcn ui component to other folder --- .../agents/components/agentConfig/SkillBuildModal.tsx | 2 +- .../agents/components/agentConfig/SkillDetailModal.tsx | 2 +- frontend/app/[locale]/chat/components/chatAttachment.tsx | 2 +- frontend/app/[locale]/chat/components/chatInput.tsx | 4 ++-- .../app/[locale]/chat/streaming/chatStreamFinalMessage.tsx | 2 +- frontend/app/[locale]/chat/streaming/taskWindow.tsx | 2 +- .../[locale]/knowledges/components/document/DocumentList.tsx | 4 ++-- .../tenant-resources/components/resources/KnowledgeList.tsx | 2 +- frontend/components/{ui => common}/Diagram.tsx | 0 frontend/components/{ui => common}/PdfViewer.tsx | 0 frontend/components/{ui => common}/copyButton.tsx | 0 frontend/components/{ui => common}/filePreviewDrawer.tsx | 4 ++-- frontend/components/{ui => common}/markdownRenderer.tsx | 4 ++-- frontend/components/{ui => common}/tokenUsageIndicator.tsx | 0 14 files changed, 14 insertions(+), 14 deletions(-) rename frontend/components/{ui => common}/Diagram.tsx (100%) rename frontend/components/{ui => common}/PdfViewer.tsx (100%) rename frontend/components/{ui => common}/copyButton.tsx (100%) rename frontend/components/{ui => common}/filePreviewDrawer.tsx (99%) rename frontend/components/{ui => common}/markdownRenderer.tsx (99%) rename frontend/components/{ui => common}/tokenUsageIndicator.tsx (100%) diff --git a/frontend/app/[locale]/agents/components/agentConfig/SkillBuildModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/SkillBuildModal.tsx index 7f969edb9..8f040d4b3 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/SkillBuildModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/SkillBuildModal.tsx @@ -56,7 +56,7 @@ import { SkillFilesAccessDeniedError, type SkillFileNode, } from "@/services/agentConfigService"; -import { MarkdownRenderer } from "@/components/ui/markdownRenderer"; +import { MarkdownRenderer } from "@/components/common/markdownRenderer"; import log from "@/lib/logger"; const { TextArea } = Input; diff --git a/frontend/app/[locale]/agents/components/agentConfig/SkillDetailModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/SkillDetailModal.tsx index 4161a3b1a..05f11d6a3 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/SkillDetailModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/SkillDetailModal.tsx @@ -10,7 +10,7 @@ import { fetchSkillFileContent, SkillFilesAccessDeniedError, } from "@/services/agentConfigService"; -import { MarkdownRenderer } from "@/components/ui/markdownRenderer"; +import { MarkdownRenderer } from "@/components/common/markdownRenderer"; import { buildTreeData, collectDirKeys, diff --git a/frontend/app/[locale]/chat/components/chatAttachment.tsx b/frontend/app/[locale]/chat/components/chatAttachment.tsx index d12e939cd..69dfbc71a 100644 --- a/frontend/app/[locale]/chat/components/chatAttachment.tsx +++ b/frontend/app/[locale]/chat/components/chatAttachment.tsx @@ -19,7 +19,7 @@ import { } from "@/services/storageService"; import { cn } from "@/lib/utils"; import { AttachmentItem, ChatAttachmentProps } from "@/types/chat"; -import { FilePreviewDrawer } from "@/components/ui/filePreviewDrawer"; +import { FilePreviewDrawer } from "@/components/common/filePreviewDrawer"; import { App } from "antd"; // Selected file state for preview drawer diff --git a/frontend/app/[locale]/chat/components/chatInput.tsx b/frontend/app/[locale]/chat/components/chatInput.tsx index b609bb999..e53b60a52 100644 --- a/frontend/app/[locale]/chat/components/chatInput.tsx +++ b/frontend/app/[locale]/chat/components/chatInput.tsx @@ -18,7 +18,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "antd"; import { Tooltip } from "@/components/ui/tooltip"; import { Textarea } from "@/components/ui/textarea"; -import { FilePreviewDrawer } from "@/components/ui/filePreviewDrawer"; +import { FilePreviewDrawer } from "@/components/common/filePreviewDrawer"; import { conversationService } from "@/services/conversationService"; import { useConfig } from "@/hooks/useConfig"; import { extractColorsFromUri } from "@/lib/avatar"; @@ -27,7 +27,7 @@ import { chatConfig } from "@/const/chatConfig"; import { FilePreview } from "@/types/chat"; import { ChatAgentSelector } from "./chatAgentSelector"; -import { TokenUsageIndicator } from "@/components/ui/tokenUsageIndicator"; +import { TokenUsageIndicator } from "@/components/common/tokenUsageIndicator"; import { TokenMetrics } from "@/types/chat"; // Get file extension diff --git a/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx b/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx index 8ed121b00..40ceaa2eb 100644 --- a/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx +++ b/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx @@ -10,7 +10,7 @@ import { ThumbsUp, } from "lucide-react"; -import { MarkdownRenderer } from "@/components/ui/markdownRenderer"; +import { MarkdownRenderer } from "@/components/common/markdownRenderer"; /** * Convert custom code tags to standard markdown code fences diff --git a/frontend/app/[locale]/chat/streaming/taskWindow.tsx b/frontend/app/[locale]/chat/streaming/taskWindow.tsx index 665ed8467..5a5e7a89a 100644 --- a/frontend/app/[locale]/chat/streaming/taskWindow.tsx +++ b/frontend/app/[locale]/chat/streaming/taskWindow.tsx @@ -13,7 +13,7 @@ import { import { ScrollArea } from "@/components/ui/scrollArea"; import { Button, message as antdMessage } from "antd"; -import { MarkdownRenderer, CodeBlock } from "@/components/ui/markdownRenderer"; +import { MarkdownRenderer, CodeBlock } from "@/components/common/markdownRenderer"; import { chatConfig } from "@/const/chatConfig"; import { ChatMessageType, diff --git a/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx b/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx index 0bfbe6467..4f75fd66e 100644 --- a/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx +++ b/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx @@ -18,8 +18,8 @@ import { Glasses, CircleOff, } from "lucide-react"; -import { MarkdownRenderer } from "@/components/ui/markdownRenderer"; -import { FilePreviewDrawer } from "@/components/ui/filePreviewDrawer"; +import { MarkdownRenderer } from "@/components/common/markdownRenderer"; +import { FilePreviewDrawer } from "@/components/common/filePreviewDrawer"; import { UI_CONFIG, diff --git a/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx index 18d70ad51..b8b00598e 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx @@ -6,7 +6,7 @@ import { Table, Popconfirm, message, Button, Modal, Tag } from "antd"; import { ColumnsType } from "antd/es/table"; import { Edit, Trash2, BookOpen } from "lucide-react"; import { Tooltip } from "@/components/ui/tooltip"; -import { MarkdownRenderer } from "@/components/ui/markdownRenderer"; +import { MarkdownRenderer } from "@/components/common/markdownRenderer"; import { useKnowledgeList } from "@/hooks/knowledge/useKnowledgeList"; import { useGroupList } from "@/hooks/group/useGroupList"; import knowledgeBaseService from "@/services/knowledgeBaseService"; diff --git a/frontend/components/ui/Diagram.tsx b/frontend/components/common/Diagram.tsx similarity index 100% rename from frontend/components/ui/Diagram.tsx rename to frontend/components/common/Diagram.tsx diff --git a/frontend/components/ui/PdfViewer.tsx b/frontend/components/common/PdfViewer.tsx similarity index 100% rename from frontend/components/ui/PdfViewer.tsx rename to frontend/components/common/PdfViewer.tsx diff --git a/frontend/components/ui/copyButton.tsx b/frontend/components/common/copyButton.tsx similarity index 100% rename from frontend/components/ui/copyButton.tsx rename to frontend/components/common/copyButton.tsx diff --git a/frontend/components/ui/filePreviewDrawer.tsx b/frontend/components/common/filePreviewDrawer.tsx similarity index 99% rename from frontend/components/ui/filePreviewDrawer.tsx rename to frontend/components/common/filePreviewDrawer.tsx index f36a4db3e..409352e15 100644 --- a/frontend/components/ui/filePreviewDrawer.tsx +++ b/frontend/components/common/filePreviewDrawer.tsx @@ -52,13 +52,13 @@ import { MarkdownRenderer, extractMarkdownHeadings, type MarkdownHeading, -} from "@/components/ui/markdownRenderer"; +} from "@/components/common/markdownRenderer"; import { formatFileSize } from "@/lib/utils"; import log from "@/lib/logger"; const PdfViewer = dynamic( () => - import("@/components/ui/PdfViewer").then((mod) => ({ + import("@/components/common/PdfViewer").then((mod) => ({ default: mod.PdfViewer, })), { diff --git a/frontend/components/ui/markdownRenderer.tsx b/frontend/components/common/markdownRenderer.tsx similarity index 99% rename from frontend/components/ui/markdownRenderer.tsx rename to frontend/components/common/markdownRenderer.tsx index 53dfb6962..93dbcce11 100644 --- a/frontend/components/ui/markdownRenderer.tsx +++ b/frontend/components/common/markdownRenderer.tsx @@ -16,8 +16,8 @@ import { visit } from "unist-util-visit"; import { SearchResult } from "@/types/chat"; import { resolveS3UrlToDataUrl } from "@/services/storageService"; import { Tooltip, TooltipProvider } from "@/components/ui/tooltip"; -import { CopyButton } from "@/components/ui/copyButton"; -import { Diagram } from "@/components/ui/Diagram"; +import { CopyButton } from "@/components/common/copyButton"; +import { Diagram } from "@/components/common/Diagram"; interface MarkdownRendererProps { content: string; diff --git a/frontend/components/ui/tokenUsageIndicator.tsx b/frontend/components/common/tokenUsageIndicator.tsx similarity index 100% rename from frontend/components/ui/tokenUsageIndicator.tsx rename to frontend/components/common/tokenUsageIndicator.tsx From acaaba4e6ac25efb5b9df701a3387f54ae871dad Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Thu, 11 Jun 2026 10:57:39 +0800 Subject: [PATCH 02/13] Bugfix: Fix incomplete display of tenant resources page after window resize --- .../agentConfig/skill/SkillConfigModal.tsx | 2 +- .../[locale]/chat/components/chatInput.tsx | 3 +- .../chat/streaming/chatStreamFinalMessage.tsx | 7 +- .../knowledge/KnowledgeBaseList.tsx | 3 +- .../components/UserManageComp.tsx | 312 +++++++++--------- .../components/resources/GroupList.tsx | 4 +- .../components/resources/InvitationList.tsx | 4 +- .../components/resources/KnowledgeList.tsx | 5 +- .../components/resources/ModelList.tsx | 5 +- .../components/resources/SkillList.tsx | 4 +- .../components/resources/UserList.tsx | 8 +- .../components/common/tokenUsageIndicator.tsx | 2 +- .../skill/InstallOfficialSkillsModal.tsx | 3 +- 13 files changed, 181 insertions(+), 181 deletions(-) diff --git a/frontend/app/[locale]/agents/components/agentConfig/skill/SkillConfigModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/skill/SkillConfigModal.tsx index 6f372e2b4..9729007e2 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/skill/SkillConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/skill/SkillConfigModal.tsx @@ -12,13 +12,13 @@ import { message, Tag, Skeleton, + Tooltip } from "antd"; import { Settings } from "lucide-react"; import { CloseOutlined } from "@ant-design/icons"; import { Skill, SkillParam } from "@/types/agentConfig"; import { KnowledgeBase } from "@/types/knowledgeBase"; -import { Tooltip } from "@/components/ui/tooltip"; import { saveSkillInstance } from "@/services/agentConfigService"; import KnowledgeBaseSelectorModal from "@/components/tool-config/KnowledgeBaseSelectorModal"; import { diff --git a/frontend/app/[locale]/chat/components/chatInput.tsx b/frontend/app/[locale]/chat/components/chatInput.tsx index e53b60a52..512f940e9 100644 --- a/frontend/app/[locale]/chat/components/chatInput.tsx +++ b/frontend/app/[locale]/chat/components/chatInput.tsx @@ -15,8 +15,7 @@ import { } from "@ant-design/icons"; import { Input } from "@/components/ui/input"; -import { Button } from "antd"; -import { Tooltip } from "@/components/ui/tooltip"; +import { Button, Tooltip } from "antd"; import { Textarea } from "@/components/ui/textarea"; import { FilePreviewDrawer } from "@/components/common/filePreviewDrawer"; import { conversationService } from "@/services/conversationService"; diff --git a/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx b/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx index 40ceaa2eb..285225f23 100644 --- a/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx +++ b/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx @@ -30,8 +30,7 @@ const convertToMarkdownCodeFences = (content: string): string => { }); return content; }; -import { Button } from "antd"; -import { Tooltip, TooltipProvider } from "@/components/ui/tooltip"; +import { Button, Tooltip } from "antd"; import { ChatMessageType, MaxStepsInfo } from "@/types/chat"; import { chatConfig, Opinion } from "@/const/chatConfig"; import { conversationService } from "@/services/conversationService"; @@ -403,7 +402,7 @@ function ChatStreamFinalMessageInner({ {/* Tool button */}
- +
{/* Copy button */} - +
)} diff --git a/frontend/app/[locale]/knowledges/components/knowledge/KnowledgeBaseList.tsx b/frontend/app/[locale]/knowledges/components/knowledge/KnowledgeBaseList.tsx index 7f4878523..53758147b 100644 --- a/frontend/app/[locale]/knowledges/components/knowledge/KnowledgeBaseList.tsx +++ b/frontend/app/[locale]/knowledges/components/knowledge/KnowledgeBaseList.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import log from "@/lib/logger"; -import { Button, Input, Select } from "antd"; +import { Button, Input, Select, Tooltip } from "antd"; import { SyncOutlined, PlusOutlined, @@ -19,7 +19,6 @@ import { SquarePen, CircleOff, } from "lucide-react"; -import { Tooltip } from "@/components/ui/tooltip"; import { Can } from "@/components/permission/Can"; import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; import { useGroupList } from "@/hooks/group/useGroupList"; diff --git a/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx b/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx index f4d20ae0b..9c215a0ac 100644 --- a/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx +++ b/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx @@ -18,6 +18,8 @@ import { Pagination, Alert, Space, + Divider, + Tooltip } from "antd"; import { Users, @@ -62,7 +64,6 @@ import { useDeployment } from "@/components/providers/deploymentProvider"; import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; import { USER_ROLES } from "@/const/auth"; import { Can } from "@/components/permission/Can"; -import { Tooltip } from "@/components/ui/tooltip"; import { getPasswordChecks, getStrengthLevel, @@ -1179,167 +1180,170 @@ export default function UserManageComp() { }; return ( -
+
{/* Page header: grouped header without dividing line */} -
- -
-
- -
-
-

- {t("tenantResources.title") || "Tenant Resource Management"} -

-

- {t("tenantResources.subtitle") || - "Manage tenants, users, groups and resources"} -

-
+
+
+
+
- +
+

+ {t("tenantResources.title") || "Tenant Resource Management"} +

+

+ {t("tenantResources.subtitle") || + "Manage tenants, users, groups and resources"} +

+
+
- - - -
-
-
- setTenantId(id)} - tenants={tenantData?.data || []} - total={tenantData?.total} - page={tenantData?.page} - pageSize={tenantData?.page_size} - totalPages={tenantData?.total_pages} - onPageChange={handlePageChange} - onTenantsRefetch={async () => { - setCurrentPage(1); - return refetchTenants(); - }} - loading={tenantsLoading} - t={t} - onUserListRefresh={() => - setUserListRefreshKey((prev) => prev + 1) - } - onInvitationListRefresh={() => - setInvitationListRefreshKey((prev) => prev + 1) - } - locale={locale} - /> +
+
+ + +
+
+
+ setTenantId(id)} + tenants={tenantData?.data || []} + total={tenantData?.total} + page={tenantData?.page} + pageSize={tenantData?.page_size} + totalPages={tenantData?.total_pages} + onPageChange={handlePageChange} + onTenantsRefetch={async () => { + setCurrentPage(1); + return refetchTenants(); + }} + loading={tenantsLoading} + t={t} + onUserListRefresh={() => + setUserListRefreshKey((prev) => prev + 1) + } + onInvitationListRefresh={() => + setInvitationListRefreshKey((prev) => prev + 1) + } + locale={locale} + /> +
-
- - - -
- {/* Tenant name header */} -
- {isEditingTenantName ? ( - setEditingTenantName(e.target.value)} - onBlur={saveTenantName} - onKeyDown={handleTenantNameKeyDown} - className="text-lg font-semibold text-gray-900 dark:text-gray-100" - placeholder={t("tenantResources.tenants.name")} - /> - ) : ( -
-

- {currentTenantName} -

- + + + +
+ {/* Tenant name header */} +
+ {isEditingTenantName ? ( + setEditingTenantName(e.target.value)} + onBlur={saveTenantName} + onKeyDown={handleTenantNameKeyDown} + className="text-lg font-semibold text-gray-900 dark:text-gray-100" + placeholder={t("tenantResources.tenants.name")} + /> + ) : ( +
+

+ {currentTenantName} +

+ +
+ )} + +
+ +
+ +
+ {tenantId ? ( + + ), + }, + { + key: "groups", + label: t("tenantResources.tabs.groups") || "Groups", + children: , + }, + { + key: "models", + label: t("tenantResources.tabs.models") || "Models", + children: , + }, + { + key: "knowledge", + label: + t("tenantResources.tabs.knowledge") || "Knowledge Base", + children: , + }, + { + key: "agents", + label: t("tenantResources.tabs.agents") || "Agents", + children: , + }, + { + key: "mcp", + label: t("tenantResources.tabs.mcp") || "MCP", + children: , + }, + { + key: "skills", + label: "Skills", + children: , + }, + { + key: "invitations", + label: t("tenantResources.invitation.tab") || "Invitations", + children: ( + + ), + }, + ]} + /> + ) : ( +
+
+ +
+

+ {t("tenantResources.selectTenantFirst") || + "Please select a tenant"} +

+

+ {t("tenantResources.selectTenantDescription") || + "Choose a tenant from the list to manage its users, groups, models, and knowledge base."} +

+
+ )}
- )} -
- {tenantId ? ( - - ), - }, - { - key: "groups", - label: t("tenantResources.tabs.groups") || "Groups", - children: , - }, - { - key: "models", - label: t("tenantResources.tabs.models") || "Models", - children: , - }, - { - key: "knowledge", - label: - t("tenantResources.tabs.knowledge") || "Knowledge Base", - children: , - }, - { - key: "agents", - label: t("tenantResources.tabs.agents") || "Agents", - children: , - }, - { - key: "mcp", - label: t("tenantResources.tabs.mcp") || "MCP", - children: , - }, - { - key: "skills", - label: "Skills", - children: , - }, - { - key: "invitations", - label: t("tenantResources.invitation.tab") || "Invitations", - children: ( - - ), - }, - ]} - /> - ) : ( -
-
- -
-

- {t("tenantResources.selectTenantFirst") || - "Please select a tenant"} -

-

- {t("tenantResources.selectTenantDescription") || - "Choose a tenant from the list to manage its users, groups, models, and knowledge base."} -

- )} -
- - +
+ +
+
); } diff --git a/frontend/app/[locale]/tenant-resources/components/resources/GroupList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/GroupList.tsx index ec3397219..32af131db 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/GroupList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/GroupList.tsx @@ -12,9 +12,9 @@ import { Popconfirm, message, Select, + Tooltip } from "antd"; import { Edit, Trash2 } from "lucide-react"; -import { Tooltip } from "@/components/ui/tooltip"; import { ColumnsType } from "antd/es/table"; import { useGroupList } from "@/hooks/group/useGroupList"; import { useUserList } from "@/hooks/user/useUserList"; @@ -278,7 +278,7 @@ export default function GroupList({ tenantId }: { tenantId: string | null }) { }; return ( -
+
diff --git a/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx index 688fda8b1..648936660 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx @@ -17,6 +17,7 @@ import { Collapse, DatePicker, Progress, + Tooltip } from "antd"; import { ColumnsType } from "antd/es/table"; import { useInvitationList } from "@/hooks/invitation/useInvitationList"; @@ -41,7 +42,6 @@ import { Copy, CircleSlash, } from "lucide-react"; -import { Tooltip } from "@/components/ui/tooltip"; import { formatDate } from "@/lib/date"; import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; import { @@ -443,7 +443,7 @@ export default function InvitationList({ }, [invitations, tenantId]); return ( -
+
diff --git a/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx index b8b00598e..0ca15065c 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx @@ -2,10 +2,9 @@ import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Table, Popconfirm, message, Button, Modal, Tag } from "antd"; +import { Table, Popconfirm, message, Button, Modal, Tag, Tooltip } from "antd"; import { ColumnsType } from "antd/es/table"; import { Edit, Trash2, BookOpen } from "lucide-react"; -import { Tooltip } from "@/components/ui/tooltip"; import { MarkdownRenderer } from "@/components/common/markdownRenderer"; import { useKnowledgeList } from "@/hooks/knowledge/useKnowledgeList"; import { useGroupList } from "@/hooks/group/useGroupList"; @@ -255,7 +254,7 @@ export default function KnowledgeList({ ]; return ( -
+
+
+
), + width: "20%" }, ], [] @@ -207,7 +210,7 @@ export default function UserList({ tenantId, refreshKey }: { tenantId: string | }; return ( -
+
- Date: Thu, 11 Jun 2026 15:14:34 +0800 Subject: [PATCH 03/13] Bugfix: Fix incomplete display of tenant resources page after window resize --- .../components/AssetOwnerResourcesComp.tsx | 16 +++- .../components/UserManageComp.tsx | 16 +++- .../components/resources/AgentList.tsx | 27 ++++--- .../components/resources/InvitationList.tsx | 6 +- .../components/resources/KnowledgeList.tsx | 6 +- .../components/resources/McpList.tsx | 78 +++++++++---------- .../components/resources/ModelList.tsx | 6 +- .../components/resources/SkillList.tsx | 8 +- .../components/resources/UserList.tsx | 6 +- 9 files changed, 96 insertions(+), 73 deletions(-) diff --git a/frontend/app/[locale]/tenant-resources/components/AssetOwnerResourcesComp.tsx b/frontend/app/[locale]/tenant-resources/components/AssetOwnerResourcesComp.tsx index 38fb3ceb1..6cf23c0a9 100644 --- a/frontend/app/[locale]/tenant-resources/components/AssetOwnerResourcesComp.tsx +++ b/frontend/app/[locale]/tenant-resources/components/AssetOwnerResourcesComp.tsx @@ -54,7 +54,7 @@ export default function AssetOwnerResourcesComp() { ); } + + diff --git a/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx b/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx index 9c215a0ac..cfff26fe9 100644 --- a/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx +++ b/frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx @@ -1234,7 +1234,7 @@ export default function UserManageComp() { -
+
{/* Tenant name header */}
{isEditingTenantName ? ( @@ -1267,7 +1267,7 @@ export default function UserManageComp() { {tenantId ? ( ); } + + diff --git a/frontend/app/[locale]/tenant-resources/components/resources/AgentList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/AgentList.tsx index 41fbf4c93..2e2383ad5 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/AgentList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/AgentList.tsx @@ -411,20 +411,19 @@ export default function AgentList({ tenantId }: { tenantId: string | null }) { ]; return ( -
-
-
-
- +
+
+
{/* View Modal */} diff --git a/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx index 648936660..6365ab68a 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/InvitationList.tsx @@ -443,7 +443,7 @@ export default function InvitationList({ }, [invitations, tenantId]); return ( -
+
@@ -465,8 +465,8 @@ export default function InvitationList({ loading={isLoading} rowKey="invitation_id" pagination={{ pageSize: 10 }} - scroll={{ x: 1000 }} - className="flex-1" + scroll={{ y: "calc(100vh - 560px)" }} + className="flex-1 [&_.ant-table]:h-full" /> ) : ( // Multi-tenant view with collapse diff --git a/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx index 0ca15065c..7b1a703b1 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/KnowledgeList.tsx @@ -254,15 +254,15 @@ export default function KnowledgeList({ ]; return ( -
+
{/* Edit Knowledge Base Modal */} diff --git a/frontend/app/[locale]/tenant-resources/components/resources/McpList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/McpList.tsx index 19e8e23ae..412ff402f 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/McpList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/McpList.tsx @@ -760,7 +760,7 @@ export default function McpList({ tenantId }: { tenantId: string | null }) { ]; return ( -
+
-
-
- {t("mcpConfig.serverList.title")} -
`${record.service_name}-${record.mcp_url}`} - loading={loading} - size="small" - pagination={{ pageSize: 7 }} - locale={{ emptyText: t("mcpConfig.serverList.empty") }} - /> - +
+ {t("mcpConfig.serverList.title")} +
`${record.service_name}-${record.mcp_url}`} + loading={loading} + size="small" + pagination={{ pageSize: 7 }} + locale={{ emptyText: t("mcpConfig.serverList.empty") }} + scroll={{ y: "calc(100vh - 560px)" }} + className="flex-1 [&_.ant-table]:h-full" + /> -
- {t("mcpConfig.containerList.title")} -
- + {t("mcpConfig.containerList.title")} +
-
- {t("mcpConfig.openapiService.list.title")} -
- + {t("mcpConfig.openapiService.list.title")} +
{/* Add Modal */} diff --git a/frontend/app/[locale]/tenant-resources/components/resources/ModelList.tsx b/frontend/app/[locale]/tenant-resources/components/resources/ModelList.tsx index b19fd6fc7..6715852f7 100644 --- a/frontend/app/[locale]/tenant-resources/components/resources/ModelList.tsx +++ b/frontend/app/[locale]/tenant-resources/components/resources/ModelList.tsx @@ -360,7 +360,7 @@ export default function ModelList({ tenantId }: { tenantId: string | null }) { ]; return ( -
+
(null); const [savingParams, setSavingParams] = useState(false); @@ -730,8 +729,8 @@ export default function SkillList({ tenantId }: { tenantId: string | null }) { : "closed"; return ( -
-
+
+
Date: Tue, 16 Jun 2026 10:42:47 +0800 Subject: [PATCH 04/13] Bugfix: Fix inability to select agent from agent space to edit --- frontend/app/[locale]/agents/page.tsx | 24 +++++++++++++++++++ .../[locale]/space/components/AgentCard.tsx | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/app/[locale]/agents/page.tsx b/frontend/app/[locale]/agents/page.tsx index 52d66fe8a..2ef7692d4 100644 --- a/frontend/app/[locale]/agents/page.tsx +++ b/frontend/app/[locale]/agents/page.tsx @@ -12,6 +12,8 @@ import AgentInfoComp from "./components/AgentInfoComp"; import { useAgentConfigStore } from "@/stores/agentConfigStore"; import AgentVersionManage from "./AgentVersionManage"; import AgentSelectorHeader from "./components/AgentSelectorHeader"; +import { searchAgentInfo } from "@/services/agentConfigService"; +import log from "@/lib/logger"; const { Header, Content } = Layout; @@ -21,6 +23,8 @@ export default function AgentSetupOrchestrator() { const enterCreateMode = useAgentConfigStore((state) => state.enterCreateMode); const reset = useAgentConfigStore((state) => state.reset); const setDefaultLlmConfig = useAgentConfigStore((state) => state.setDefaultLlmConfig); + const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); + const setCurrentAgent = useAgentConfigStore((state) => state.setCurrentAgent); const { config } = useConfig(); // Sync default LLM config from load_config @@ -47,6 +51,26 @@ export default function AgentSetupOrchestrator() { } }, [searchParams, enterCreateMode]); + // Handle auto-select agent from URL params (agent_id) + useEffect(() => { + const agentId = searchParams.get('agent_id'); + if (agentId && (!currentAgentId || String(currentAgentId) !== agentId)) { + const loadAgent = async () => { + try { + const result = await searchAgentInfo(parseInt(agentId)); + if (result.success && result.data) { + setCurrentAgent(result.data); + } else { + log.warn("Failed to load agent from URL agent_id:", result.message); + } + } catch (error) { + log.error("Failed to load agent from URL agent_id:", error); + } + }; + loadAgent(); + } + }, [searchParams, currentAgentId, setCurrentAgent]); + // Reset agent selection state when leaving the page useEffect(() => { return () => { diff --git a/frontend/app/[locale]/space/components/AgentCard.tsx b/frontend/app/[locale]/space/components/AgentCard.tsx index 0e005d9be..099694e66 100644 --- a/frontend/app/[locale]/space/components/AgentCard.tsx +++ b/frontend/app/[locale]/space/components/AgentCard.tsx @@ -139,9 +139,9 @@ export default function AgentCard({ agent, onRefresh }: AgentCardProps) { } }; - // Handle edit - navigate to agents view + // Handle edit - navigate to agents view with agent id const handleEdit = () => { - router.push("/agents"); + router.push(`/agents?agent_id=${agent.id}`); }; const queryClient = useQueryClient(); From a2e1487ae2360190ab1e66af0ae111358e7ed2b4 Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Tue, 16 Jun 2026 14:50:35 +0800 Subject: [PATCH 05/13] Bugfix: Display correct version info when viewing agent details --- backend/services/agent_service.py | 8 +++++++- backend/services/agent_version_service.py | 6 +++--- frontend/app/[locale]/space/components/AgentCard.tsx | 5 ++++- frontend/services/agentConfigService.ts | 2 +- test/backend/services/test_agent_service.py | 11 +++++++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/backend/services/agent_service.py b/backend/services/agent_service.py index 4354a32d4..b6cbf42b7 100644 --- a/backend/services/agent_service.py +++ b/backend/services/agent_service.py @@ -73,7 +73,7 @@ from database.attachment_db import upload_fileobj from services.skill_service import SkillService from services.file_management_service import is_allowed_skill_upload_path -from database.agent_version_db import query_version_list +from database.agent_version_db import query_version_list, query_current_version_no from database.group_db import query_group_ids_by_user from database.user_tenant_db import get_user_tenant_by_user_id from database.a2a_agent_db import get_server_agent_ids, query_external_sub_agents @@ -1068,6 +1068,12 @@ async def get_agent_info_impl(agent_id: int, tenant_id: str, version_no: int = 0 agent_info["is_available"] = is_available agent_info["unavailable_reasons"] = unavailable_reasons + # Set current_version_no from draft record (version_no=0) + # This ensures the returned data always has the current published version info + if version_no > 0: + draft_version_no = query_current_version_no(agent_id, tenant_id) + agent_info["current_version_no"] = draft_version_no + return agent_info diff --git a/backend/services/agent_version_service.py b/backend/services/agent_version_service.py index e711da0d6..8ed6e14d4 100644 --- a/backend/services/agent_version_service.py +++ b/backend/services/agent_version_service.py @@ -879,8 +879,8 @@ async def list_published_agents_impl( agent_info['sub_agent_id_list'] = [r['selected_agent_id'] for r in relations_snapshot] agent_info['sub_agent_relations'] = _build_sub_agent_relations(relations_snapshot) - # Add published version info - agent_info['published_version_no'] = current_version_no + # Add current version info + agent_info['current_version_no'] = current_version_no # Check agent availability using the shared function _, unavailable_reasons = check_agent_availability( @@ -934,7 +934,7 @@ async def list_published_agents_impl( "is_new": agent.get("is_new", False), "group_ids": agent.get("group_ids", []), "permission": permission, - "published_version_no": agent.get("published_version_no"), + "current_version_no": agent.get("current_version_no"), "greeting_message": agent.get("greeting_message"), "example_questions": agent.get("example_questions"), }) diff --git a/frontend/app/[locale]/space/components/AgentCard.tsx b/frontend/app/[locale]/space/components/AgentCard.tsx index 099694e66..cd4ecb57a 100644 --- a/frontend/app/[locale]/space/components/AgentCard.tsx +++ b/frontend/app/[locale]/space/components/AgentCard.tsx @@ -166,7 +166,10 @@ export default function AgentCard({ agent, onRefresh }: AgentCardProps) { setShowDetail(true); setIsLoadingDetails(true); try { - const result = await searchAgentInfo(parseInt(agent.id)); + // Use current_version_no if available (the currently published version) + // Falls back to 0 only if not set (for unpublished/draft agents) + const versionNo = agent.current_version_no ?? 0; + const result = await searchAgentInfo(parseInt(agent.id), undefined, versionNo); if (result.success) { setAgentDetails(result.data); } else { diff --git a/frontend/services/agentConfigService.ts b/frontend/services/agentConfigService.ts index 5e53865ad..7e7182baf 100644 --- a/frontend/services/agentConfigService.ts +++ b/frontend/services/agentConfigService.ts @@ -200,7 +200,7 @@ export const fetchPublishedAgentList = async () => { group_ids: agent.group_ids || [], is_new: agent.is_new || false, permission: agent.permission, - published_version_no: agent.published_version_no, + current_version_no: agent.current_version_no, greeting_message: agent.greeting_message, example_questions: agent.example_questions || [], })); diff --git a/test/backend/services/test_agent_service.py b/test/backend/services/test_agent_service.py index ce0ab1406..6cd7b5da4 100644 --- a/test/backend/services/test_agent_service.py +++ b/test/backend/services/test_agent_service.py @@ -505,6 +505,7 @@ async def test_get_agent_info_impl_success(mock_search_agent_info, mock_search_t mock_check_availability.assert_called_once() +@patch('backend.services.agent_service.query_current_version_no') @patch('backend.services.agent_service.SkillService') @patch('backend.services.agent_service.query_external_sub_agents') @patch('backend.services.agent_service.check_agent_availability') @@ -513,7 +514,7 @@ async def test_get_agent_info_impl_success(mock_search_agent_info, mock_search_t @patch('backend.services.agent_service.search_tools_for_sub_agent') @patch('backend.services.agent_service.search_agent_info_by_agent_id') @pytest.mark.asyncio -async def test_get_agent_info_impl_with_version_no(mock_search_agent_info, mock_search_tools, mock_query_sub_agents_id, mock_get_model_by_model_id, mock_check_availability, mock_query_external_sub_agents, mock_skill_service): +async def test_get_agent_info_impl_with_version_no(mock_search_agent_info, mock_search_tools, mock_query_sub_agents_id, mock_get_model_by_model_id, mock_check_availability, mock_query_external_sub_agents, mock_skill_service, mock_query_current_version_no): """ Test get_agent_info_impl with explicit version_no parameter. @@ -549,6 +550,9 @@ async def test_get_agent_info_impl_with_version_no(mock_search_agent_info, mock_ # Mock check_agent_availability - agent is available mock_check_availability.return_value = (True, []) + # Mock query_current_version_no - return 5 as the current version + mock_query_current_version_no.return_value = 5 + # Execute with explicit version_no result = await get_agent_info_impl(agent_id=123, tenant_id="test_tenant", version_no=5) @@ -566,7 +570,8 @@ async def test_get_agent_info_impl_with_version_no(mock_search_agent_info, mock_ "prompt_template_id": 0, "prompt_template_name": "system_default", "is_available": True, - "unavailable_reasons": [] + "unavailable_reasons": [], + "current_version_no": 5 } assert result == expected_result # Verify version_no is passed correctly @@ -576,6 +581,8 @@ async def test_get_agent_info_impl_with_version_no(mock_search_agent_info, mock_ mock_query_sub_agents_id.assert_called_once_with( main_agent_id=123, tenant_id="test_tenant") mock_check_availability.assert_called_once() + # Verify query_current_version_no is called for version_no > 0 + mock_query_current_version_no.assert_called_once_with(123, "test_tenant") @patch('backend.services.agent_service.get_model_by_model_id') From 921fe424028a2e43d83ea3c52121901c420c2cf2 Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Tue, 16 Jun 2026 20:27:19 +0800 Subject: [PATCH 06/13] Bugfix: Adjust agent detail UI layout to accommodate newly added "self-verification" field --- .../agents/components/agentInfo/AgentGenerateDetail.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx b/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx index 24ec60616..69046c8a9 100644 --- a/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx +++ b/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx @@ -809,7 +809,7 @@ export default function AgentGenerateDetail({}) { - + - + - + - + Date: Thu, 18 Jun 2026 10:21:04 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E8=A1=A5=E5=85=85sql=20(#3248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 补充sql * 扩大limit限制 --- k8s/helm/deploy.sh | 2 +- .../charts/nexent-common/files/init.sql | 207 ++++++++++++++++++ .../charts/nexent-data-process/values.yaml | 2 +- scripts/deployment/common.sh | 5 - 4 files changed, 209 insertions(+), 7 deletions(-) diff --git a/k8s/helm/deploy.sh b/k8s/helm/deploy.sh index 7a583307d..07522d22c 100755 --- a/k8s/helm/deploy.sh +++ b/k8s/helm/deploy.sh @@ -611,7 +611,7 @@ apply() { sleep 5 for svc in $backend_services; do echo " Waiting for nexent-$svc..." - if kubectl wait --for=condition=ready pod -l app=nexent-$svc -n $NAMESPACE --timeout=300s 2>/dev/null; then + if kubectl rollout status "deployment/nexent-$svc" -n "$NAMESPACE" --timeout=300s >/dev/null 2>&1; then echo " nexent-$svc is ready." else echo " Error: nexent-$svc did not become ready within timeout." diff --git a/k8s/helm/nexent/charts/nexent-common/files/init.sql b/k8s/helm/nexent/charts/nexent-common/files/init.sql index a2f202b90..399c50917 100644 --- a/k8s/helm/nexent/charts/nexent-common/files/init.sql +++ b/k8s/helm/nexent/charts/nexent-common/files/init.sql @@ -1896,3 +1896,210 @@ COMMENT ON TABLE nexent.user_cas_session_t IS 'Server-side session records for C COMMENT ON COLUMN nexent.user_cas_session_t.session_id IS 'JWT sid claim for revocation checks'; COMMENT ON COLUMN nexent.user_cas_session_t.cas_user_id IS 'User identifier returned by CAS'; COMMENT ON COLUMN nexent.user_cas_session_t.cas_session_index IS 'CAS SessionIndex or service ticket'; + +-- Rename params -> config_values, add config_schemas to ag_skill_info_t +-- Add tenant_id column for multi-tenancy support +ALTER TABLE nexent.ag_skill_info_t ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(100); + +-- Add config_values and config_schemas to ag_skill_info_t +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'nexent' + AND table_name = 'ag_skill_info_t' + AND column_name = 'params' + ) THEN + ALTER TABLE nexent.ag_skill_info_t RENAME COLUMN params TO config_values; + END IF; +END $$; +ALTER TABLE nexent.ag_skill_info_t ADD COLUMN IF NOT EXISTS config_schemas JSON; + +-- Comments for ag_skill_info_t columns +COMMENT ON COLUMN nexent.ag_skill_info_t.tenant_id IS 'Tenant ID for multi-tenancy. NULL for pre-existing skills.'; +COMMENT ON COLUMN nexent.ag_skill_info_t.config_values IS 'Runtime parameter values from config/config.yaml'; +COMMENT ON COLUMN nexent.ag_skill_info_t.config_schemas IS 'Parameter metadata list from config/schema.yaml'; + +-- Add config_values and config_schemas to ag_skill_instance_t +ALTER TABLE nexent.ag_skill_instance_t ADD COLUMN IF NOT EXISTS config_values JSON; +ALTER TABLE nexent.ag_skill_instance_t ADD COLUMN IF NOT EXISTS config_schemas JSON; + +-- Comments for ag_skill_instance_t columns +COMMENT ON COLUMN nexent.ag_skill_instance_t.config_values IS 'Per-agent runtime parameter values from config/config.yaml'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.config_schemas IS 'Per-agent parameter schema overrides from config/schema.yaml'; + +-- Migration: ASSET_OWNER role permissions and invitation type comment +-- Date: 2026-05-29 +-- Description: Add ASSET_OWNER role permissions, SU asset-owner invite permissions, +-- update invitation code_type comment, and ensure ag_skill_info_t.tenant_id exists +-- Source: commit 15cece97692db2372a978cbdf21b5d5316e79f30 (init.sql) + +SET search_path TO nexent; + +BEGIN; + +COMMENT ON COLUMN nexent.tenant_invitation_code_t.code_type IS + 'Invitation code type: ADMIN_INVITE, DEV_INVITE, USER_INVITE, ASSET_OWNER_INVITE'; + +INSERT INTO nexent.role_permission_t + (role_permission_id, user_role, permission_category, permission_type, permission_subtype) +VALUES + (188, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'CREATE'), + (189, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'READ'), + (190, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'UPDATE'), + (191, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'DELETE'), + (192, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), + (193, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/agents'), + (194, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/knowledges'), + (195, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'), + (196, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/space'), + (197, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/market'), + (198, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/models'), + (199, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'CREATE'), + (200, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'READ'), + (201, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'UPDATE'), + (202, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'DELETE'), + (203, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'CREATE'), + (204, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'READ'), + (205, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'UPDATE'), + (206, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'DELETE'), + (207, 'ASSET_OWNER', 'RESOURCE', 'KB', 'CREATE'), + (208, 'ASSET_OWNER', 'RESOURCE', 'KB', 'READ'), + (209, 'ASSET_OWNER', 'RESOURCE', 'KB', 'UPDATE'), + (210, 'ASSET_OWNER', 'RESOURCE', 'KB', 'DELETE'), + (211, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'CREATE'), + (212, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'READ'), + (213, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'UPDATE'), + (214, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'DELETE'), + (215, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'CREATE'), + (216, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'READ'), + (217, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'UPDATE'), + (218, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'DELETE'), + (219, 'ASSET_OWNER', 'RESOURCE', 'USER.ROLE', 'READ'), + (220, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'), + (221, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/asset-owner-resources') +ON CONFLICT (role_permission_id) DO NOTHING; + +COMMIT; + +-- Migration: Add preserve_source_file to knowledge_record_t table +-- Date: 2026-06-01 +-- Description: Whether to preserve uploaded source documents after vectorization (default: true) + +ALTER TABLE nexent.knowledge_record_t +ADD COLUMN IF NOT EXISTS preserve_source_file BOOLEAN NOT NULL DEFAULT true; + +COMMENT ON COLUMN nexent.knowledge_record_t.preserve_source_file IS 'Whether to preserve uploaded source documents after vectorization'; + +-- Migration: Add ag_agent_repository_t table +-- Date: 2026-06-05 +-- Description: Agent marketplace repository for frozen shareable agent snapshots. + +SET search_path TO nexent; + +BEGIN; + +CREATE SEQUENCE IF NOT EXISTS nexent.ag_agent_repository_t_agent_repository_id_seq; + +CREATE TABLE IF NOT EXISTS nexent.ag_agent_repository_t ( + agent_repository_id BIGINT NOT NULL DEFAULT nextval('nexent.ag_agent_repository_t_agent_repository_id_seq'), + publisher_tenant_id VARCHAR(100) NOT NULL, + publisher_user_id VARCHAR(100) NOT NULL, + agent_id INTEGER NOT NULL, + source_version_no INTEGER NOT NULL, + name VARCHAR(100) NOT NULL, + display_name VARCHAR(100), + description TEXT, + author VARCHAR(100), + category_id INTEGER, + tags TEXT[], + tool_count INTEGER, + version_label VARCHAR(100), + agent_info_json JSONB NOT NULL, + status VARCHAR(30) DEFAULT 'NOT_SHARED', + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N', + CONSTRAINT ag_agent_repository_t_pkey PRIMARY KEY (agent_repository_id) +); + +ALTER SEQUENCE nexent.ag_agent_repository_t_agent_repository_id_seq + OWNED BY nexent.ag_agent_repository_t.agent_repository_id; + +ALTER TABLE nexent.ag_agent_repository_t OWNER TO root; + +COMMENT ON TABLE nexent.ag_agent_repository_t IS 'Agent marketplace repository for frozen shareable agent snapshots'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.agent_repository_id IS 'Agent repository listing ID, unique primary key'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.publisher_tenant_id IS 'Publisher tenant ID'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.publisher_user_id IS 'Publisher user ID'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.agent_id IS 'Root agent ID from ag_tenant_agent_t; upsert key with publisher_tenant_id'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.source_version_no IS 'Published version number frozen at share time'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.name IS 'Root agent programmatic name for display and search'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.display_name IS 'Root agent display name'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.description IS 'Root agent description'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.author IS 'Agent author'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.category_id IS 'Optional marketplace category ID'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.tags IS 'Marketplace tags'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.tool_count IS 'Total tool count across all agents in the bundle (display only)'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.version_label IS 'Repository entry version label for display (e.g. v1.0)'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.agent_info_json IS 'Frozen ExportAndImportDataFormat snapshot with optional skills'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.status IS 'Listing status: NOT_SHARED (未共享) / PENDING_REVIEW (待审核) / REJECTED (审核驳回) / SHARED (已共享)'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.created_by IS 'Creator ID'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.updated_by IS 'Updater ID'; +COMMENT ON COLUMN nexent.ag_agent_repository_t.delete_flag IS 'Soft delete flag: Y/N'; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_agent_repository_tenant_agent_active + ON nexent.ag_agent_repository_t (publisher_tenant_id, agent_id) + WHERE delete_flag = 'N'; + +CREATE INDEX IF NOT EXISTS idx_agent_repository_publisher_delete + ON nexent.ag_agent_repository_t (publisher_tenant_id, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_agent_repository_status_delete + ON nexent.ag_agent_repository_t (status, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_agent_repository_name_delete + ON nexent.ag_agent_repository_t (name, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_agent_repository_tags_gin + ON nexent.ag_agent_repository_t USING GIN (tags); + +CREATE OR REPLACE FUNCTION update_ag_agent_repository_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION update_ag_agent_repository_update_time() IS 'Auto-update update_time for ag_agent_repository_t'; + +DROP TRIGGER IF EXISTS update_ag_agent_repository_update_time_trigger ON nexent.ag_agent_repository_t; +CREATE TRIGGER update_ag_agent_repository_update_time_trigger +BEFORE UPDATE ON nexent.ag_agent_repository_t +FOR EACH ROW +EXECUTE FUNCTION update_ag_agent_repository_update_time(); + +COMMENT ON TRIGGER update_ag_agent_repository_update_time_trigger ON nexent.ag_agent_repository_t IS 'Trigger to maintain update_time'; + +COMMIT; + +-- Migration: Add selected_agent_version_no to ag_agent_relation_t +-- Date: 2026-06-09 +-- Description: Pin child agent version on parent-child relations at publish time. + +SET search_path TO nexent; + +BEGIN; + +ALTER TABLE nexent.ag_agent_relation_t + ADD COLUMN IF NOT EXISTS selected_agent_version_no INTEGER; + +COMMENT ON COLUMN nexent.ag_agent_relation_t.selected_agent_version_no IS + 'Pinned version of selected_agent_id. NULL = use child current published version at runtime (legacy/draft).'; + +COMMIT; diff --git a/k8s/helm/nexent/charts/nexent-data-process/values.yaml b/k8s/helm/nexent/charts/nexent-data-process/values.yaml index 189292667..d6bb70a7f 100644 --- a/k8s/helm/nexent/charts/nexent-data-process/values.yaml +++ b/k8s/helm/nexent/charts/nexent-data-process/values.yaml @@ -12,7 +12,7 @@ resources: memory: 512Mi cpu: 0.5 limits: - memory: 4Gi + memory: 64Gi cpu: 8 config: diff --git a/scripts/deployment/common.sh b/scripts/deployment/common.sh index 5855af1a0..006561553 100755 --- a/scripts/deployment/common.sh +++ b/scripts/deployment/common.sh @@ -319,11 +319,6 @@ deployment_validate() { deployment_error "Local config schemaVersion $DEPLOYMENT_LOADED_SCHEMA_VERSION is incompatible with $DEPLOYMENT_SCHEMA_VERSION. Re-run with --reconfigure." return 1 fi - if [ -n "$DEPLOYMENT_LOADED_APP_VERSION" ] && [ -n "${APP_VERSION:-}" ] && [ -z "${DEPLOYMENT_APP_VERSION_EXPLICIT:-}" ] && [ "$DEPLOYMENT_LOADED_APP_VERSION" != "$APP_VERSION" ]; then - deployment_error "Local config appVersion $DEPLOYMENT_LOADED_APP_VERSION does not match current appVersion $APP_VERSION. Re-run with --reconfigure or pass --app-version." - return 1 - fi - local old_ifs="$IFS" local component IFS=',' From ac538ceb43114c5478f17c099ff1ffb79bba24ed Mon Sep 17 00:00:00 2001 From: panyehong <91180085+YehongPan@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:41:26 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=90=9B=20Bugfix:=20Fixed=20an=20iss?= =?UTF-8?q?ue=20where=20the=20MCP=20service=20failed=20to=20start=20in=20a?= =?UTF-8?q?=20Kubernetes=20container.=20(#3254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Specification Details] 1. Modify the pod naming logic to convert all non-compliant characters to -. 2. Modify test cases. --- sdk/nexent/container/k8s_client.py | 50 +++++++- test/sdk/container/test_k8s_client.py | 168 +++++++++++++++++++++++++- 2 files changed, 210 insertions(+), 8 deletions(-) diff --git a/sdk/nexent/container/k8s_client.py b/sdk/nexent/container/k8s_client.py index c1fa4db53..c2fb72741 100644 --- a/sdk/nexent/container/k8s_client.py +++ b/sdk/nexent/container/k8s_client.py @@ -8,6 +8,7 @@ import asyncio import logging import socket +import re import uuid import kubernetes @@ -23,6 +24,47 @@ logger = logging.getLogger("nexent.container.kubernetes") +# Kubernetes naming constraints: lowercase alphanumeric or dash, cannot start/end with dash, +# cannot have consecutive dashes, max 253 characters +K8S_NAME_PATTERN = re.compile(r"[^a-z0-9-]+") +K8S_CONSECUTIVE_DASHES = re.compile(r"-+") + + +def _sanitize_k8s_name(name: str) -> str: + """Convert arbitrary string to valid Kubernetes resource name. + + Rules: + - Convert to lowercase + - Replace invalid characters with dash + - Collapse consecutive dashes + - Remove leading/trailing dashes + - Must start with alphanumeric + + Args: + name: Input string to sanitize + + Returns: + Valid Kubernetes name (lowercase alphanumeric and dashes only) + """ + if not name: + return "unknown" + + # Lowercase and replace invalid chars with dash + sanitized = K8S_NAME_PATTERN.sub("-", name.lower()) + + # Collapse consecutive dashes + sanitized = K8S_CONSECUTIVE_DASHES.sub("-", sanitized) + + # Remove leading/trailing dashes + sanitized = sanitized.strip("-") + + # Ensure it starts with alphanumeric + if sanitized and not sanitized[0].isalnum(): + sanitized = "x" + sanitized + + # Fallback if empty + return sanitized if sanitized else "unknown" + class ContainerError(Exception): """Raised when container operation fails""" @@ -77,9 +119,9 @@ def __init__(self, config: KubernetesContainerConfig): def _generate_pod_name(self, service_name: str, tenant_id: str, user_id: str) -> str: """Generate unique pod name with service, tenant, and user segments.""" - safe_name = "".join(c if c.isalnum() or c == "-" else "-" for c in service_name) - tenant_part = (tenant_id or "")[:8] - user_part = (user_id or "")[:8] + safe_name = _sanitize_k8s_name(service_name) + tenant_part = _sanitize_k8s_name(tenant_id)[:8] + user_part = _sanitize_k8s_name(user_id)[:8] uuid_part = uuid.uuid4().hex[:8] return f"mcp-{safe_name}-{tenant_part}-{user_part}-{uuid_part}" @@ -486,7 +528,7 @@ def list_containers( # Filter by service_name if provided if service_name: - safe_name = "".join(c if c.isalnum() or c == "-" else "-" for c in service_name) + safe_name = _sanitize_k8s_name(service_name) pod_component = labels.get(self.LABEL_COMPONENT, "") if safe_name not in pod_component: continue diff --git a/test/sdk/container/test_k8s_client.py b/test/sdk/container/test_k8s_client.py index 42db8c58c..84e0bc557 100644 --- a/test/sdk/container/test_k8s_client.py +++ b/test/sdk/container/test_k8s_client.py @@ -11,6 +11,7 @@ KubernetesContainerClient, ContainerError, ContainerConnectionError, + _sanitize_k8s_name, ) from nexent.container.k8s_config import KubernetesContainerConfig @@ -90,6 +91,79 @@ def mock_pod(): return pod +# --------------------------------------------------------------------------- +# Test _sanitize_k8s_name +# --------------------------------------------------------------------------- + + +class TestSanitizeK8sName: + """Test _sanitize_k8s_name helper function""" + + def test_sanitize_basic_alphanumeric(self): + """Test basic alphanumeric string passes through""" + assert _sanitize_k8s_name("test-service") == "test-service" + assert _sanitize_k8s_name("abc123") == "abc123" + + def test_sanitize_lowercase_conversion(self): + """Test uppercase letters are converted to lowercase""" + assert _sanitize_k8s_name("TestService") == "testservice" + assert _sanitize_k8s_name("UPPERCASE") == "uppercase" + + def test_sanitize_special_characters_replaced(self): + """Test special characters are replaced with dash""" + assert _sanitize_k8s_name("test@service") == "test-service" + assert _sanitize_k8s_name("foo#bar") == "foo-bar" + assert _sanitize_k8s_name("test$123") == "test-123" + + def test_sanitize_consecutive_special_chars(self): + """Test consecutive special characters are collapsed to single dash""" + assert _sanitize_k8s_name("foo@@bar") == "foo-bar" + assert _sanitize_k8s_name("test@#$service") == "test-service" + assert _sanitize_k8s_name("a!!b") == "a-b" + + def test_sanitize_leading_special_chars(self): + """Test leading special characters are removed""" + assert _sanitize_k8s_name("@test") == "test" + assert _sanitize_k8s_name("#foo") == "foo" + assert _sanitize_k8s_name("!test@service") == "test-service" + + def test_sanitize_trailing_special_chars(self): + """Test trailing special characters are removed""" + assert _sanitize_k8s_name("test@") == "test" + assert _sanitize_k8s_name("test-service!") == "test-service" + + def test_sanitize_mixed_case_with_specials(self): + """Test mixed case with special characters""" + assert _sanitize_k8s_name("Foo@Bar!Test") == "foo-bar-test" + + def test_sanitize_empty_string(self): + """Test empty string returns 'unknown'""" + assert _sanitize_k8s_name("") == "unknown" + + def test_sanitize_only_special_chars(self): + """Test string with only special characters returns 'unknown'""" + assert _sanitize_k8s_name("@@@") == "unknown" + assert _sanitize_k8s_name("!@#") == "unknown" + + def test_sanitize_none(self): + """Test None returns 'unknown'""" + assert _sanitize_k8s_name(None) == "unknown" + + def test_sanitize_with_dots(self): + """Test dots are converted to dashes""" + assert _sanitize_k8s_name("foo.bar") == "foo-bar" + assert _sanitize_k8s_name("foo...bar") == "foo-bar" + + def test_sanitize_underscore_replaced(self): + """Test underscores are replaced with dash""" + assert _sanitize_k8s_name("foo_bar") == "foo-bar" + + def test_sanitize_spaces_replaced(self): + """Test spaces are replaced with dash""" + assert _sanitize_k8s_name("foo bar") == "foo-bar" + assert _sanitize_k8s_name("foo bar") == "foo-bar" + + # --------------------------------------------------------------------------- # Test KubernetesContainerClient.__init__ # --------------------------------------------------------------------------- @@ -192,6 +266,72 @@ def test_generate_pod_name_with_special_chars(self, k8s_container_client): assert "@" not in name assert "#" not in name + def test_generate_pod_name_consecutive_special_chars(self, k8s_container_client): + """Test pod name generation with consecutive special characters""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "foo@@bar", "tenant123", "user12345") + assert name == "mcp-foo-bar-tenant12-user1234-a1b2c3d4" + assert "--" not in name + + def test_generate_pod_name_leading_special_chars(self, k8s_container_client): + """Test pod name generation with leading special characters""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "@test-service", "tenant123", "user12345") + # "@test-service" -> "test-service" (leading @ stripped) + assert name.startswith("mcp-test") + assert not name.startswith("mcp-@") + + def test_generate_pod_name_trailing_special_chars(self, k8s_container_client): + """Test pod name generation with trailing special characters""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "test-service@", "tenant123", "user12345") + assert name == "mcp-test-service-tenant12-user1234-a1b2c3d4" + assert name.endswith("-a1b2c3d4") + + def test_generate_pod_name_uppercase(self, k8s_container_client): + """Test pod name generation with uppercase letters""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "TestService", "tenant123", "user12345") + assert name == "mcp-testservice-tenant12-user1234-a1b2c3d4" + + def test_generate_pod_name_underscores(self, k8s_container_client): + """Test pod name generation with underscores""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "test_service", "tenant_123", "user_12345") + # tenant_123 -> tenant-123 (9 chars), truncated to 8 -> tenant-1 + # user_12345 -> user-12345 (10 chars), truncated to 8 -> user-123 + assert name == "mcp-test-service-tenant-1-user-123-a1b2c3d4" + + def test_generate_pod_name_dots(self, k8s_container_client): + """Test pod name generation with dots""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "test.service", "tenant.123", "user.12345") + # tenant.123 -> tenant.123 (9 chars), truncated to 8 -> tenant.1 + # user.12345 -> user.12345 (10 chars), truncated to 8 -> user.123 + assert name == "mcp-test-service-tenant-1-user-123-a1b2c3d4" + + def test_generate_pod_name_spaces(self, k8s_container_client): + """Test pod name generation with spaces""" + with patch("nexent.container.k8s_client.uuid.uuid4") as mock_uuid: + mock_uuid.return_value.hex = "a1b2c3d4" + name = k8s_container_client._generate_pod_name( + "test service", "tenant 123", "user 12345") + # tenant 123 -> tenant 123 (9 chars), truncated to 8 -> tenant 1 + # user 12345 -> user 12345 (10 chars), truncated to 8 -> user 123 + assert name == "mcp-test-service-tenant-1-user-123-a1b2c3d4" + def test_generate_pod_name_long_user_id(self, k8s_container_client): """Test pod name generation with long user ID""" long_user_id = "a" * 20 @@ -216,7 +356,7 @@ def test_generate_pod_name_empty_tenant(self, k8s_container_client): mock_uuid.return_value.hex = "a1b2c3d4" name = k8s_container_client._generate_pod_name( "test-service", "", "user12345") - assert name == "mcp-test-service--user1234-a1b2c3d4" + assert name == "mcp-test-service-unknown-user1234-a1b2c3d4" def test_generate_pod_name_empty_user(self, k8s_container_client): """Test pod name generation with empty user_id""" @@ -224,7 +364,7 @@ def test_generate_pod_name_empty_user(self, k8s_container_client): mock_uuid.return_value.hex = "a1b2c3d4" name = k8s_container_client._generate_pod_name( "test-service", "tenant123", "") - assert name == "mcp-test-service-tenant12--a1b2c3d4" + assert name == "mcp-test-service-tenant12-unknown-a1b2c3d4" def test_generate_pod_name_none_tenant(self, k8s_container_client): """Test pod name generation with None tenant_id""" @@ -232,7 +372,7 @@ def test_generate_pod_name_none_tenant(self, k8s_container_client): mock_uuid.return_value.hex = "a1b2c3d4" name = k8s_container_client._generate_pod_name( "test-service", None, "user12345") - assert name == "mcp-test-service--user1234-a1b2c3d4" + assert name == "mcp-test-service-unknown-user1234-a1b2c3d4" def test_generate_pod_name_none_user(self, k8s_container_client): """Test pod name generation with None user_id""" @@ -240,7 +380,7 @@ def test_generate_pod_name_none_user(self, k8s_container_client): mock_uuid.return_value.hex = "a1b2c3d4" name = k8s_container_client._generate_pod_name( "test-service", "tenant123", None) - assert name == "mcp-test-service-tenant12--a1b2c3d4" + assert name == "mcp-test-service-tenant12-unknown-a1b2c3d4" # --------------------------------------------------------------------------- @@ -1265,6 +1405,26 @@ def test_list_containers_service_filter_special_chars(self, k8s_container_client assert len(result) == 0 + def test_list_containers_service_filter_consecutive_special_chars(self, k8s_container_client, mock_pod): + """Test listing containers with service filter containing consecutive special characters""" + k8s_container_client.core_v1.list_namespaced_pod.return_value = MagicMock(items=[mock_pod]) + + # The sanitized version of "test@@service" is "test-service" + # Since mock_pod's component is "test-service", it should match + result = k8s_container_client.list_containers(service_name="test@@service") + + assert len(result) == 1 + + def test_list_containers_service_filter_leading_special_chars(self, k8s_container_client, mock_pod): + """Test listing containers with service filter containing leading special characters""" + k8s_container_client.core_v1.list_namespaced_pod.return_value = MagicMock(items=[mock_pod]) + + # The sanitized version of "@test-service" is "test-service" (leading @ stripped) + # Since mock_pod's component is "test-service", it should match + result = k8s_container_client.list_containers(service_name="@test-service") + + assert len(result) == 1 + def test_list_containers_pod_no_ports(self, k8s_container_client): """Test listing containers when pod has no ports configured""" mock_pod_no_ports = MagicMock() From b2afd9be138d40183a67f5369f127f872fc8f95b Mon Sep 17 00:00:00 2001 From: Xia Yichen Date: Thu, 18 Jun 2026 14:04:31 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=90=9B=20Bugfix:=20knowledge=5Fbase?= =?UTF-8?q?=5Fsearch=5Ftool=20called=20with=20TypeError:=20argument=20of?= =?UTF-8?q?=20type=20'FieldInfo'=20is=20not=20iterable=20(#3259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/nexent/core/agents/nexent_agent.py | 20 ++++- .../core/tools/knowledge_base_search_tool.py | 24 ++++- test/sdk/core/agents/test_nexent_agent.py | 82 +++++++++++++++++ .../tools/test_knowledge_base_search_tool.py | 88 +++++++++++++++++++ 4 files changed, 207 insertions(+), 7 deletions(-) diff --git a/sdk/nexent/core/agents/nexent_agent.py b/sdk/nexent/core/agents/nexent_agent.py index a9a31a94b..ed43b6691 100644 --- a/sdk/nexent/core/agents/nexent_agent.py +++ b/sdk/nexent/core/agents/nexent_agent.py @@ -198,11 +198,16 @@ def create_local_tool(self, tool_config: ToolConfig): raise ValueError(f"{class_name} not found in local") else: if class_name == "KnowledgeBaseSearchTool": - # Filter out conflicting parameters from params to avoid conflicts - # These parameters have exclude=True and cannot be passed to __init__ - # due to smolagents.tools.Tool wrapper restrictions + # Filter out conflicting parameters from params to avoid conflicts. + # Parameters declared with exclude=True cannot be passed to __init__ + # due to smolagents.tools.Tool wrapper restrictions; they are set as + # attributes on the instance after construction, sourced from metadata. + # `document_paths` is intentionally hidden from the LLM and only + # populated via tool_params from the northbound interface. filtered_params = {k: v for k, v in params.items() - if k not in ["vdb_core", "embedding_model", "observer", "rerank_model", "display_name_to_index_map"]} + if k not in ["vdb_core", "embedding_model", "observer", + "rerank_model", "display_name_to_index_map", + "document_paths"]} # Create instance with only non-excluded parameters tools_obj = tool_class(**filtered_params) # Set excluded parameters directly as attributes after instantiation @@ -216,6 +221,13 @@ def create_local_tool(self, tool_config: ToolConfig): "rerank_model", None) if tool_config.metadata else None tools_obj.display_name_to_index_map = tool_config.metadata.get( "display_name_to_index_map", {}) if tool_config.metadata else {} + # Internal access control: restrict results to documents whose + # path_or_url is in the allow list. Only the northbound interface + # may populate this; never the LLM. + tools_obj.set_document_paths( + tool_config.metadata.get( + "document_paths") if tool_config.metadata else None + ) elif class_name in ["DifySearchTool", "DataMateSearchTool"]: # These parameters have exclude=True and cannot be passed to __init__ filtered_params = {k: v for k, v in params.items() diff --git a/sdk/nexent/core/tools/knowledge_base_search_tool.py b/sdk/nexent/core/tools/knowledge_base_search_tool.py index 9149ed05d..c0115a0ab 100644 --- a/sdk/nexent/core/tools/knowledge_base_search_tool.py +++ b/sdk/nexent/core/tools/knowledge_base_search_tool.py @@ -21,6 +21,21 @@ logger = logging.getLogger("knowledge_base_search_tool") +def _unwrap_field_info(value): + """Resolve a value that may be wrapped in a Pydantic FieldInfo. + + Parameters declared with `Field(...)` and `exclude=True` are not expanded by + smolagents' Tool wrapper, so they arrive at `__init__` as raw FieldInfo + instances instead of their declared defaults. This helper extracts the + concrete value so callers can safely treat the result as plain data. + """ + if isinstance(value, FieldInfo): + if value.default_factory is not None: + return value.default_factory() + return value.default + return value + + class KnowledgeBaseSearchTool(Tool): """Knowledge base search tool""" @@ -129,7 +144,10 @@ def __init__( self.rerank_model = rerank_model self.data_process_service = os.getenv("DATA_PROCESS_SERVICE") self.display_name_to_index_map = display_name_to_index_map - self._internal_document_paths = document_paths + # `document_paths` is declared with `exclude=True` so smolagents passes the + # raw FieldInfo default when no value is supplied. Unwrap it here so the + # internal filter is always a concrete list (or None), never a FieldInfo. + self._internal_document_paths = _unwrap_field_info(document_paths) self.record_ops = 1 self.running_prompt_zh = "知识库检索中..." @@ -144,7 +162,7 @@ def set_document_paths(self, document_paths: Optional[List[str]]) -> None: Args: document_paths: List of allowed document path_or_urls. If None, no filtering is applied. """ - self._internal_document_paths = document_paths + self._internal_document_paths = _unwrap_field_info(document_paths) def _convert_to_index_names(self, names: List[str]) -> List[str]: """Convert display names (knowledge_name) to index names if necessary. @@ -188,7 +206,7 @@ def _filter_by_document_paths(self, results: List[dict]) -> List[dict]: Returns: Filtered list containing only results with allowed document paths """ - allowed_paths = self._internal_document_paths + allowed_paths = _unwrap_field_info(self._internal_document_paths) if not allowed_paths: return results diff --git a/test/sdk/core/agents/test_nexent_agent.py b/test/sdk/core/agents/test_nexent_agent.py index ff8da11f8..882e28514 100644 --- a/test/sdk/core/agents/test_nexent_agent.py +++ b/test/sdk/core/agents/test_nexent_agent.py @@ -939,6 +939,88 @@ def test_create_local_tool_knowledge_base_with_display_name_map(nexent_agent_ins assert result.rerank_model == "mock_rerank_model" +def test_create_local_tool_knowledge_base_with_document_paths_from_metadata(nexent_agent_instance): + """KnowledgeBaseSearchTool should receive document_paths from metadata via set_document_paths. + + The `document_paths` parameter is declared with `exclude=True` so it must not + be passed to __init__. Instead it must be forwarded to `set_document_paths` + on the instance, sourced from `tool_config.metadata`. This guards against + the FieldInfo-iteration regression reported when document_paths is unset. + """ + mock_kb_tool_class = MagicMock() + mock_kb_tool_instance = MagicMock() + mock_kb_tool_class.return_value = mock_kb_tool_instance + + document_paths = ["s3://bucket/doc1.txt", "s3://bucket/doc2.txt"] + + tool_config = ToolConfig( + class_name="KnowledgeBaseSearchTool", + name="knowledge_base_search", + description="desc", + inputs="{}", + output_type="string", + params={"top_k": 5, "index_names": ["kb1"]}, + source="local", + metadata={ + "vdb_core": "mock_vdb_core", + "embedding_model": "mock_embedding_model", + "document_paths": document_paths, + }, + ) + + original_value = nexent_agent.__dict__.get("KnowledgeBaseSearchTool") + nexent_agent.__dict__["KnowledgeBaseSearchTool"] = mock_kb_tool_class + + try: + nexent_agent_instance.create_local_tool(tool_config) + finally: + if original_value is not None: + nexent_agent.__dict__["KnowledgeBaseSearchTool"] = original_value + elif "KnowledgeBaseSearchTool" in nexent_agent.__dict__: + del nexent_agent.__dict__["KnowledgeBaseSearchTool"] + + # document_paths is excluded and must not be forwarded to __init__. + init_kwargs = mock_kb_tool_class.call_args.kwargs + assert "document_paths" not in init_kwargs + # It must instead be applied via set_document_paths on the instance. + mock_kb_tool_instance.set_document_paths.assert_called_once_with(document_paths) + + +def test_create_local_tool_knowledge_base_without_metadata_calls_set_document_paths_none(nexent_agent_instance): + """When metadata lacks document_paths, set_document_paths(None) must still be invoked. + + Ensures the tool's internal filter is explicitly reset to None rather than + left as a stale FieldInfo default from the smolagents wrapper. + """ + mock_kb_tool_class = MagicMock() + mock_kb_tool_instance = MagicMock() + mock_kb_tool_class.return_value = mock_kb_tool_instance + + tool_config = ToolConfig( + class_name="KnowledgeBaseSearchTool", + name="knowledge_base_search", + description="desc", + inputs="{}", + output_type="string", + params={"top_k": 5, "index_names": ["kb1"]}, + source="local", + metadata=None, + ) + + original_value = nexent_agent.__dict__.get("KnowledgeBaseSearchTool") + nexent_agent.__dict__["KnowledgeBaseSearchTool"] = mock_kb_tool_class + + try: + nexent_agent_instance.create_local_tool(tool_config) + finally: + if original_value is not None: + nexent_agent.__dict__["KnowledgeBaseSearchTool"] = original_value + elif "KnowledgeBaseSearchTool" in nexent_agent.__dict__: + del nexent_agent.__dict__["KnowledgeBaseSearchTool"] + + mock_kb_tool_instance.set_document_paths.assert_called_once_with(None) + + def test_create_local_tool_knowledge_base_with_empty_display_name_map(nexent_agent_instance): """Test KnowledgeBaseSearchTool creation handles empty display_name_to_index_map.""" mock_kb_tool_class = MagicMock() diff --git a/test/sdk/core/tools/test_knowledge_base_search_tool.py b/test/sdk/core/tools/test_knowledge_base_search_tool.py index acb94f43f..7a4b23ebe 100644 --- a/test/sdk/core/tools/test_knowledge_base_search_tool.py +++ b/test/sdk/core/tools/test_knowledge_base_search_tool.py @@ -1776,3 +1776,91 @@ def test_forward_with_document_paths_filter_no_results_after_filter(self, mock_v assert "No results found" in str(excinfo.value) + def test_filter_by_document_paths_unwraps_fieldinfo_default(self, mock_vdb_core, mock_embedding_model): + """Filter should tolerate a FieldInfo default instead of a concrete list. + + Regression: smolagents' Tool wrapper does not expand FieldInfo defaults for + parameters declared with `exclude=True`, so `self._internal_document_paths` + may arrive as a FieldInfo. The filter must unwrap it instead of failing with + `TypeError: argument of type 'FieldInfo' is not iterable`. + """ + try: + from pydantic import FieldInfo + except ImportError: + from pydantic.fields import FieldInfo + + field_info_default = FieldInfo(default=["s3://bucket/doc1.txt"]) + + tool = KnowledgeBaseSearchTool( + index_names=["kb1"], + search_mode="hybrid", + vdb_core=mock_vdb_core, + embedding_model=mock_embedding_model, + document_paths=None, + ) + # Simulate a FieldInfo being assigned directly (e.g. from smolagents wrapper). + tool._internal_document_paths = field_info_default + + results = self._create_mock_formatted_results_with_paths( + ["s3://bucket/doc1.txt", "s3://bucket/doc2.txt"] + ) + filtered = tool._filter_by_document_paths(results) + + assert len(filtered) == 1 + assert filtered[0]["path_or_url"] == "s3://bucket/doc1.txt" + + def test_filter_by_document_paths_unwraps_fieldinfo_default_factory(self, mock_vdb_core, mock_embedding_model): + """Filter should tolerate a FieldInfo with default_factory.""" + try: + from pydantic import FieldInfo + except ImportError: + from pydantic.fields import FieldInfo + + field_info_factory = FieldInfo( + default_factory=lambda: ["s3://bucket/doc2.txt"] + ) + + tool = KnowledgeBaseSearchTool( + index_names=["kb1"], + search_mode="hybrid", + vdb_core=mock_vdb_core, + embedding_model=mock_embedding_model, + document_paths=None, + ) + tool._internal_document_paths = field_info_factory + + results = self._create_mock_formatted_results_with_paths( + ["s3://bucket/doc1.txt", "s3://bucket/doc2.txt"] + ) + filtered = tool._filter_by_document_paths(results) + + assert len(filtered) == 1 + assert filtered[0]["path_or_url"] == "s3://bucket/doc2.txt" + + def test_set_document_paths_unwraps_fieldinfo(self, mock_vdb_core, mock_embedding_model): + """set_document_paths should also accept FieldInfo input defensively.""" + try: + from pydantic import FieldInfo + except ImportError: + from pydantic.fields import FieldInfo + + tool = KnowledgeBaseSearchTool( + index_names=["kb1"], + search_mode="hybrid", + vdb_core=mock_vdb_core, + embedding_model=mock_embedding_model, + document_paths=None, + ) + + field_info = FieldInfo(default=["s3://bucket/doc1.txt"]) + tool.set_document_paths(field_info) + + results = self._create_mock_formatted_results_with_paths( + ["s3://bucket/doc1.txt", "s3://bucket/doc2.txt"] + ) + filtered = tool._filter_by_document_paths(results) + + assert len(filtered) == 1 + assert filtered[0]["path_or_url"] == "s3://bucket/doc1.txt" + + From 2cfde4a5cea05200911a75234164ab6908f6d786 Mon Sep 17 00:00:00 2001 From: panyehong <91180085+YehongPan@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:06:22 +0800 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=90=9B=20Bugfix:=20Fixed=20an=20iss?= =?UTF-8?q?ue=20where=20the=20one-click=20rename=20function=20failed=20aft?= =?UTF-8?q?er=20importing=20an=20agent.=20(#3258)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Specification Details] 1. The frontend does not pass `agent_id` when calling the `regenerate_name` API. --- frontend/components/agent/AgentImportWizard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/components/agent/AgentImportWizard.tsx b/frontend/components/agent/AgentImportWizard.tsx index 5ccf79033..504237c1c 100644 --- a/frontend/components/agent/AgentImportWizard.tsx +++ b/frontend/components/agent/AgentImportWizard.tsx @@ -393,7 +393,6 @@ export default function AgentImportWizard({ items: agentsWithConflicts.map(([agentKey, conflict]) => { const agentInfo = initialData.agent_info[agentKey] as any; return { - agent_id: agentInfo?.agent_id, name: conflict.renamedName || agentInfo?.name || "", display_name: conflict.renamedDisplayName || agentInfo?.display_name || "", task_description: agentInfo?.business_description || agentInfo?.description || "", From 3d40e4ce3b4f3dd66b437a0e2f7a5693ffd87028 Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Thu, 18 Jun 2026 14:27:56 +0800 Subject: [PATCH 11/13] Bugfix: Exclude attachments from assistant when saving conversation history --- backend/services/conversation_management_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/services/conversation_management_service.py b/backend/services/conversation_management_service.py index 34db53525..0b7345461 100644 --- a/backend/services/conversation_management_service.py +++ b/backend/services/conversation_management_service.py @@ -227,7 +227,7 @@ def save_conversation_assistant(request: AgentRequest, messages: List[str], user message_list.append(message) conversation_req = MessageRequest(conversation_id=request.conversation_id, message_idx=user_role_count * 2 + 1, - role=MESSAGE_ROLE["ASSISTANT"], message=message_list, minio_files=request.minio_files) + role=MESSAGE_ROLE["ASSISTANT"], message=message_list, minio_files=None) save_message(conversation_req, user_id=user_id, tenant_id=tenant_id) From 5dacf70e11742f564c0070cd233d8f4c0e199e08 Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Thu, 18 Jun 2026 17:21:34 +0800 Subject: [PATCH 12/13] Bugfix: Adapt frontend for k8s A2A port configuration --- doc/docs/zh/user-guide/agent-development.md | 3 ++- .../agents/components/a2a/A2AServerSettingsPanel.tsx | 6 +++--- frontend/public/locales/en/common.json | 6 +++--- frontend/public/locales/zh/common.json | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/docs/zh/user-guide/agent-development.md b/doc/docs/zh/user-guide/agent-development.md index 40805aeea..aa86b95b2 100644 --- a/doc/docs/zh/user-guide/agent-development.md +++ b/doc/docs/zh/user-guide/agent-development.md @@ -418,8 +418,9 @@ Content-Type: application/json ``` > 💡 **提示**: -> - 本地开发时,请将路径前面的 `/nb/a2a` 部分替换为 `http://localhost:5013/nb/a2a` +> - 本地开发时,如果使用docker启动:请将路径前面的 `/nb/a2a` 部分替换为 `http://localhost:5013/nb/a2a`;如果是如果通过k8s启动,请使用端口:30013 > - 生产环境请将路径替换为您的服务器域名或公网 IP 地址 +> - > ⚠️ **注意事项**: > - 调用 A2A Agent 需要在请求头中携带有效的认证信息 diff --git a/frontend/app/[locale]/agents/components/a2a/A2AServerSettingsPanel.tsx b/frontend/app/[locale]/agents/components/a2a/A2AServerSettingsPanel.tsx index e15ff34ed..00d3e2b24 100644 --- a/frontend/app/[locale]/agents/components/a2a/A2AServerSettingsPanel.tsx +++ b/frontend/app/[locale]/agents/components/a2a/A2AServerSettingsPanel.tsx @@ -109,7 +109,7 @@ export default function A2AServerSettingsPanel({
{previewData.agentCardUrl} - {t("a2a.server.urlHint", { defaultValue: "Append base URL to access. For local dev: localhost:5013" })} + {t("a2a.server.urlHint", { defaultValue: "Append base URL to access. For local dev: localhost:5013(or 30013)" })}
@@ -162,10 +162,10 @@ export default function A2AServerSettingsPanel({ {t("a2a.server.usageTitle", { defaultValue: "How to use these endpoints" })}

- {t("a2a.server.localDevHint", { defaultValue: "For local development: prepend localhost:5013 to the paths above." })} + {t("a2a.server.localDevHint", { defaultValue: "For local development: prepend localhost:5013(or 30013) to the paths above." })}

- {t("a2a.server.productionHint", { defaultValue: "For production: replace localhost:5013 with your server domain or public IP and port 5013." })} + {t("a2a.server.productionHint", { defaultValue: "For production: replace localhost with your server domain or public IP." })}

diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index c3ccbd6c0..a656e4071 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -2749,9 +2749,9 @@ "a2a.server.protocolVersion": "Protocol Version", "a2a.server.restEndpoints": "REST Endpoints", "a2a.server.usageTitle": "How to use these endpoints", - "a2a.server.localDevHint": "For local development: prepend localhost:5013/api to the paths above.", - "a2a.server.productionHint": "For production: replace localhost:5013 with your server domain or public IP and port 5013.", - "a2a.server.urlHint": "Append base URL to access. For local dev: localhost:5013/api", + "a2a.server.localDevHint": "For local development: prepend localhost:5013/api (docker), localhost:30013/api(k8s)", + "a2a.server.productionHint": "For production: replace localhost with your server domain or public IP.", + "a2a.server.urlHint": "Append base URL to access. For local dev: localhost:5013(or 30013)/api", "a2a.service.discoverFailed": "Failed to discover agent", "a2a.service.listFailed": "Failed to get list", diff --git a/frontend/public/locales/zh/common.json b/frontend/public/locales/zh/common.json index 09b8bcd4a..4180a1a2b 100644 --- a/frontend/public/locales/zh/common.json +++ b/frontend/public/locales/zh/common.json @@ -2791,9 +2791,9 @@ "a2a.server.protocolVersion": "协议版本", "a2a.server.restEndpoints": "HTTP+JSON (REST)", "a2a.server.usageTitle": "如何使用这些端点", - "a2a.server.localDevHint": "本地开发:在上述路径前加上 localhost:5013/api", - "a2a.server.productionHint": "生产环境:将 localhost:5013 替换为您的服务器域名或公网 IP 及端口 5013", - "a2a.server.urlHint": "需要拼接基础 URL 才能访问。本地开发请使用 localhost:5013/api", + "a2a.server.localDevHint": "本地开发:在上述路径前加上 localhost:5013/api(docker启动) localhost:30013/api(k8s启动)", + "a2a.server.productionHint": "生产环境:将 localhost 替换为您的服务器域名或公网 IP", + "a2a.server.urlHint": "需要拼接基础 URL 才能访问。本地开发请使用 localhost:5013(或30013)/api", "a2a.service.discoverFailed": "发现 Agent 失败", "a2a.service.listFailed": "获取列表失败", From a49a9e657810b47ec3d8b8477674b6075feb987f Mon Sep 17 00:00:00 2001 From: panyehong <91180085+YehongPan@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:40:26 +0800 Subject: [PATCH 13/13] Bump APP_VERSION from v2.2.0 to v2.2.1 (#3268) The default setting for client-side self-validation is "False". --- backend/consts/const.py | 2 +- ...2.1_0601_add_preserve_source_file_to_knowledge_record_t.sql} | 0 ...=> v2.2.1_0603_add_greeting_fields_to_ag_tenant_agent_t.sql} | 0 ...pository_t.sql => v2.2.1_0605_add_ag_agent_repository_t.sql} | 0 ..._0609_add_selected_agent_version_no_to_agent_relation_t.sql} | 0 .../agents/components/agentInfo/AgentGenerateDetail.tsx | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename docker/sql/{v2.2.0_0601_add_preserve_source_file_to_knowledge_record_t.sql => v2.2.1_0601_add_preserve_source_file_to_knowledge_record_t.sql} (100%) rename docker/sql/{v2.2.0_0603_add_greeting_fields_to_ag_tenant_agent_t.sql => v2.2.1_0603_add_greeting_fields_to_ag_tenant_agent_t.sql} (100%) rename docker/sql/{v2.2.0_0605_add_ag_agent_repository_t.sql => v2.2.1_0605_add_ag_agent_repository_t.sql} (100%) rename docker/sql/{v2.2.0_0609_add_selected_agent_version_no_to_agent_relation_t.sql => v2.2.1_0609_add_selected_agent_version_no_to_agent_relation_t.sql} (100%) diff --git a/backend/consts/const.py b/backend/consts/const.py index a3a897043..574d550c0 100644 --- a/backend/consts/const.py +++ b/backend/consts/const.py @@ -486,7 +486,7 @@ def _parse_otlp_headers(headers_str: str) -> dict: # APP Version -APP_VERSION = "v2.2.0" +APP_VERSION = "v2.2.1" # Skill Creation Streaming Configuration diff --git a/docker/sql/v2.2.0_0601_add_preserve_source_file_to_knowledge_record_t.sql b/docker/sql/v2.2.1_0601_add_preserve_source_file_to_knowledge_record_t.sql similarity index 100% rename from docker/sql/v2.2.0_0601_add_preserve_source_file_to_knowledge_record_t.sql rename to docker/sql/v2.2.1_0601_add_preserve_source_file_to_knowledge_record_t.sql diff --git a/docker/sql/v2.2.0_0603_add_greeting_fields_to_ag_tenant_agent_t.sql b/docker/sql/v2.2.1_0603_add_greeting_fields_to_ag_tenant_agent_t.sql similarity index 100% rename from docker/sql/v2.2.0_0603_add_greeting_fields_to_ag_tenant_agent_t.sql rename to docker/sql/v2.2.1_0603_add_greeting_fields_to_ag_tenant_agent_t.sql diff --git a/docker/sql/v2.2.0_0605_add_ag_agent_repository_t.sql b/docker/sql/v2.2.1_0605_add_ag_agent_repository_t.sql similarity index 100% rename from docker/sql/v2.2.0_0605_add_ag_agent_repository_t.sql rename to docker/sql/v2.2.1_0605_add_ag_agent_repository_t.sql diff --git a/docker/sql/v2.2.0_0609_add_selected_agent_version_no_to_agent_relation_t.sql b/docker/sql/v2.2.1_0609_add_selected_agent_version_no_to_agent_relation_t.sql similarity index 100% rename from docker/sql/v2.2.0_0609_add_selected_agent_version_no_to_agent_relation_t.sql rename to docker/sql/v2.2.1_0609_add_selected_agent_version_no_to_agent_relation_t.sql diff --git a/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx b/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx index 69046c8a9..cd46d2aa3 100644 --- a/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx +++ b/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx @@ -171,7 +171,7 @@ export default function AgentGenerateDetail({}) { constraintPrompt: editedAgent.constraint_prompt || "", fewShotsPrompt: editedAgent.few_shots_prompt || "", provideRunSummary: editedAgent.provide_run_summary || false, - verificationEnabled: editedAgent.verification_config?.enabled ?? true, + verificationEnabled: editedAgent.verification_config?.enabled ?? false, businessDescription: editedAgent.business_description || "", businessLogicModelName:editedAgent.business_logic_model_name, businessLogicModelId: editedAgent.business_logic_model_id,