feat: support export agent dsl in sidebar (#39299)

This commit is contained in:
Joel 2026-07-20 18:41:26 +08:00 committed by GitHub
parent cbd7872520
commit 0862641533
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 4 deletions

View File

@ -1,14 +1,24 @@
import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AgentDetailSection, AgentDetailTop } from '../navigation'
const mocks = vi.hoisted(() => ({
downloadBlob: vi.fn(),
exportAppConfig: vi.fn(),
pathname: '/agents/agent-1/configure',
queryData: undefined as AgentAppDetailWithSite | undefined,
}))
vi.mock('@/service/apps', () => ({
exportAppConfig: mocks.exportAppConfig,
}))
vi.mock('@/utils/download', () => ({
downloadBlob: mocks.downloadBlob,
}))
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
@ -70,6 +80,7 @@ vi.mock('@/service/client', () => ({
}))
const createAgent = (overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppDetailWithSite => ({
app_id: 'app-1',
description: 'Find and summarize market materials.',
enable_api: true,
enable_site: true,
@ -96,6 +107,8 @@ function renderAgentDetailSection(expand = true) {
describe('AgentDetailSection', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.exportAppConfig.mockResolvedValue({ data: 'kind: app\napp:\n mode: agent\n' })
mocks.pathname = '/agents/agent-1/configure'
mocks.queryData = createAgent()
})
@ -131,9 +144,31 @@ describe('AgentDetailSection', () => {
await user.click(trigger)
expect(screen.getByRole('menuitem', { name: 'agentV2.roster.editInfo' })).toBeInTheDocument()
expect(screen.getByRole('menuitem', { name: 'common.operation.duplicate' })).toBeInTheDocument()
expect(screen.getByRole('menuitem', { name: 'common.operation.delete' })).toBeInTheDocument()
expect(screen.getAllByRole('menuitem').map((item) => item.textContent)).toEqual([
'agentV2.roster.editInfo',
'common.operation.duplicate',
'app.export',
'common.operation.delete',
])
})
it('exports the Agent App DSL from the detail action menu', async () => {
const user = userEvent.setup()
renderAgentDetailSection()
await user.click(screen.getByRole('button', { name: /agentV2\.roster\.moreActions/ }))
await user.click(screen.getByRole('menuitem', { name: 'app.export' }))
await waitFor(() => {
expect(mocks.exportAppConfig).toHaveBeenCalledWith({
appID: 'app-1',
include: false,
})
})
expect(mocks.downloadBlob).toHaveBeenCalledWith({
data: expect.any(Blob),
fileName: 'Research Agent.yml',
})
})
it('does not render more actions in collapsed sidebar mode', () => {

View File

@ -8,14 +8,18 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { toast } from '@langgenius/dify-ui/toast'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DeleteAgentDialog } from '@/features/agent-v2/roster/components/delete-agent-dialog'
import { DuplicateAgentDialog } from '@/features/agent-v2/roster/components/duplicate-agent-dialog'
import { EditAgentDialog } from '@/features/agent-v2/roster/components/edit-agent-dialog'
import { exportAppConfig } from '@/service/apps'
import { downloadBlob } from '@/utils/download'
type AgentDetailSidebarActionAgent = Pick<
AgentAppPartial,
| 'app_id'
| 'description'
| 'icon'
| 'icon_background'
@ -30,6 +34,7 @@ type AgentDetailSidebarActionAgent = Pick<
export function AgentDetailSidebarActions({ agent }: { agent: AgentDetailSidebarActionAgent }) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const { t: tApp } = useTranslation('app')
const [isEditOpen, setIsEditOpen] = useState(false)
const [editSessionKey, setEditSessionKey] = useState(0)
const [isDuplicateOpen, setIsDuplicateOpen] = useState(false)
@ -57,6 +62,26 @@ export function AgentDetailSidebarActions({ agent }: { agent: AgentDetailSidebar
setIsDuplicateOpen(true)
}
const handleExport = async () => {
if (!agent.app_id) {
toast.error(tApp(($) => $.exportFailed))
return
}
try {
const { data } = await exportAppConfig({
appID: agent.app_id,
include: false,
})
downloadBlob({
data: new Blob([data], { type: 'application/yaml' }),
fileName: `${agent.name}.yml`,
})
} catch {
toast.error(tApp(($) => $.exportFailed))
}
}
return (
<>
<DropdownMenu>
@ -75,6 +100,13 @@ export function AgentDetailSidebarActions({ agent }: { agent: AgentDetailSidebar
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
<span>{tCommon(($) => $['operation.duplicate'])}</span>
</DropdownMenuItem>
<DropdownMenuItem className="gap-2" onClick={handleExport}>
<span
aria-hidden
className="i-ri-file-download-line size-4 shrink-0 text-text-tertiary"
/>
<span>{tApp(($) => $.export)}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"