mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 23:06:40 +08:00
feat: add external agent workflow selection and settings
This commit is contained in:
1 parent
a8da8bac60
commit
f2417f3ec0
58 files changed
+2509
-100
No files matched your search
Generated
-13
@@ -3658,11 +3658,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/agent-selector.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/all-start-blocks.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
@@ -4133,9 +4128,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/node-control.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
@@ -4386,11 +4378,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/agent/components/model-bar.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
|
||||
@@ -12,6 +12,8 @@ import { createEdge, createNode } from '@/app/components/workflow/__tests__/fixt
|
||||
import { renderWorkflowCanvasHook } from '@/app/components/workflow/__tests__/workflow-test-env'
|
||||
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
|
||||
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
|
||||
import { markAppDeletionFailed, markAppDeletionStarted } from '@/service/app-deletion'
|
||||
import { createHttpError } from '@/service/http-error'
|
||||
import { createConsoleQueryWrapper, seedAppDetail } from '@/test/console/query-data'
|
||||
@@ -439,6 +441,29 @@ describe('useNodesSyncDraft', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('excludes external agent previews and their connected edges from backend draft payloads', async () => {
|
||||
const previewData = createExternalAgentWorkflowPreviewData(createExternalRosterPreview()[1]!)
|
||||
const { result } = renderUseNodesSyncDraft({
|
||||
nodes: [
|
||||
startNode,
|
||||
createNode({ id: 'external-preview', data: { type: BlockEnum.Agent, ...previewData } }),
|
||||
],
|
||||
edges: [createEdge({ source: startNode.id, target: 'external-preview' })],
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.doSyncWorkflowDraft()
|
||||
})
|
||||
|
||||
expect(mockSyncWorkflowDraft).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
graph: expect.objectContaining({ nodes: [savedStartNode], edges: [] }),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps pending inline Agent v2 nodes in draft without incomplete bindings', async () => {
|
||||
const agentData = {
|
||||
type: BlockEnum.Agent,
|
||||
|
||||
@@ -389,7 +389,7 @@ describe('Blocks', () => {
|
||||
expect(await screen.findByText('Nadia')).toBeInTheDocument()
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Nadia Researcher' }))
|
||||
await user.click(screen.getByRole('button', { name: /Nadia.*Researcher/ }))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
|
||||
agent_binding: {
|
||||
@@ -511,11 +511,14 @@ describe('Blocks', () => {
|
||||
await user.click(screen.getByRole('button', { name: /Agent/ }))
|
||||
expect(await screen.findByText('Nadia')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Nadia Researcher' }))
|
||||
await user.click(screen.getByRole('button', { name: /Nadia.*Researcher/ }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(queryMocks.toastError).toHaveBeenCalledWith('workflow.nodes.agent.modelNotSelected'),
|
||||
expect(screen.getByRole('button', { name: /Nadia.*Researcher/ })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByText('workflow.nodes.agent.modelNotSelected')).toBeInTheDocument()
|
||||
expect(queryMocks.toastError).not.toHaveBeenCalled()
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -546,14 +549,14 @@ describe('Blocks', () => {
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Agent/ }))
|
||||
const consoleLink = await screen.findByRole('menuitem', {
|
||||
const consoleLink = await screen.findByRole('link', {
|
||||
name: 'agentRoster.roster.nodeSelector.manageInAgentConsole',
|
||||
})
|
||||
expect(consoleLink).toHaveAttribute('href', '/agents')
|
||||
expect(consoleLink).toHaveAttribute('target', '_blank')
|
||||
expect(consoleLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', {
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentRoster.roster.nodeSelector.startFromScratch',
|
||||
}),
|
||||
)
|
||||
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { EditorResource } from '@/app/components/workflow/editor-resource/resource'
|
||||
import type { NodeDefault } from '@/app/components/workflow/types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createInstance } from 'i18next'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import agentRoster from '@/i18n/locales/en-US/agent-roster.json'
|
||||
import common from '@/i18n/locales/en-US/common.json'
|
||||
import navigation from '@/i18n/locales/en-US/navigation.json'
|
||||
import workflow from '@/i18n/locales/en-US/workflow.json'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import { AgentBlockItem } from '../agent-selector'
|
||||
import { BlockClassification } from '../types'
|
||||
|
||||
const difyAgent: AgentInviteOptionResponse = {
|
||||
id: 'dify-agent-1',
|
||||
name: 'Research Agent',
|
||||
description: 'Researches public information.',
|
||||
role: 'Researcher',
|
||||
icon_type: 'emoji',
|
||||
icon: '🤖',
|
||||
icon_background: '#FFFFFF',
|
||||
active_config_snapshot_id: 'snapshot-1',
|
||||
agent_kind: 'dify_agent',
|
||||
scope: 'roster',
|
||||
source: 'workflow',
|
||||
status: 'active',
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({ fetchAgents: vi.fn() }))
|
||||
vi.mock('react-i18next', async (importOriginal) => importOriginal())
|
||||
// boundary: Dify invite options are server data; source filtering and selection remain real.
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'agent.inviteOptions.get': (
|
||||
input: Parameters<typeof actual.consoleClient.agent.inviteOptions.get>[0],
|
||||
) => {
|
||||
mocks.fetchAgents(input)
|
||||
const keyword = input?.query?.keyword?.toLowerCase() ?? ''
|
||||
const data = [difyAgent].filter((agent) => agent.name.toLowerCase().includes(keyword))
|
||||
return { data, has_more: false, limit: 8, page: 1, total: data.length }
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const block: NodeDefault = {
|
||||
metaData: {
|
||||
classification: BlockClassification.Default,
|
||||
sort: 3,
|
||||
type: BlockEnum.AgentV2,
|
||||
title: 'Agent',
|
||||
author: 'Dify',
|
||||
description: 'Call an agent.',
|
||||
},
|
||||
defaultValue: {},
|
||||
checkValid: () => ({ isValid: true }),
|
||||
}
|
||||
|
||||
type AgentBlockProps = ComponentProps<typeof AgentBlockItem>
|
||||
|
||||
async function setup(
|
||||
resource: EditorResource = {
|
||||
kind: 'app',
|
||||
collaborationEnabled: false,
|
||||
appId: 'workflow-app',
|
||||
mode: 'workflow',
|
||||
},
|
||||
) {
|
||||
await page.viewport(1100, 750)
|
||||
const i18n = createInstance()
|
||||
await i18n.init({
|
||||
lng: 'en-US',
|
||||
resources: { 'en-US': { agentRoster, common, navigation, workflow } },
|
||||
keySeparator: false,
|
||||
interpolation: { escapeValue: false },
|
||||
})
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
onTestFinished(() => queryClient.clear())
|
||||
const onSelect = vi.fn<AgentBlockProps['onSelect']>()
|
||||
const onSelectExternal = vi.fn<NonNullable<AgentBlockProps['onSelectExternal']>>()
|
||||
const onStartFromScratch = vi.fn()
|
||||
await render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<EditorResourceScope resource={resource}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div className="isolate w-60 p-4">
|
||||
<AgentBlockItem
|
||||
block={block}
|
||||
onSelect={onSelect}
|
||||
onSelectExternal={onSelectExternal}
|
||||
onStartFromScratch={onStartFromScratch}
|
||||
/>
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
</EditorResourceScope>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
const trigger = page.getByRole('button', { name: /^Agent/ })
|
||||
await trigger.click()
|
||||
const picker = page.getByRole('dialog', { name: 'Select an agent', exact: true })
|
||||
await expect.element(picker).toBeVisible()
|
||||
return { picker, trigger, onSelect, onSelectExternal, onStartFromScratch }
|
||||
}
|
||||
|
||||
beforeEach(({ onTestFinished }) => {
|
||||
mocks.fetchAgents.mockClear()
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
})
|
||||
|
||||
it('filters the active source while sharing the search and avoiding Dify requests on External', async () => {
|
||||
const { picker } = await setup()
|
||||
const search = picker.getByRole('searchbox', { name: 'Search agents', exact: true })
|
||||
await expect
|
||||
.element(picker.getByRole('tab', { name: 'All', exact: true }))
|
||||
.toHaveAttribute('aria-selected', 'true')
|
||||
await expect.element(picker.getByRole('button', { name: /Research Agent/ })).toBeVisible()
|
||||
await expect.element(picker.getByRole('button', { name: /Contract Review Agent/ })).toBeVisible()
|
||||
await picker.getByRole('tab', { name: 'External', exact: true }).click()
|
||||
await expect
|
||||
.element(picker.getByRole('button', { name: /Research Agent/ }))
|
||||
.not.toBeInTheDocument()
|
||||
await expect
|
||||
.element(picker.getByRole('button', { name: 'Start from Scratch', exact: true }))
|
||||
.not.toBeInTheDocument()
|
||||
const requests = mocks.fetchAgents.mock.calls.length
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
onTestFinished(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
await search.fill('document')
|
||||
await expect.element(search).toHaveValue('document')
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
await expect.element(picker.getByRole('button', { name: /Document Review Agent/ })).toBeVisible()
|
||||
await expect
|
||||
.element(picker.getByRole('button', { name: /Contract Review Agent/ }))
|
||||
.not.toBeInTheDocument()
|
||||
expect(mocks.fetchAgents).toHaveBeenCalledTimes(requests)
|
||||
vi.useRealTimers()
|
||||
|
||||
await picker.getByRole('tab', { name: 'Dify', exact: true }).click()
|
||||
await expect
|
||||
.poll(() => mocks.fetchAgents.mock.calls.at(-1))
|
||||
.toEqual([{ query: { app_id: 'workflow-app', limit: 8, page: 1, keyword: 'document' } }])
|
||||
await expect.element(search).toHaveValue('document')
|
||||
await expect.element(picker.getByText('No matching agents', { exact: true })).toBeVisible()
|
||||
await picker.getByRole('tab', { name: 'All', exact: true }).click()
|
||||
await expect.element(picker.getByRole('button', { name: /Document Review Agent/ })).toBeVisible()
|
||||
})
|
||||
|
||||
it('skips disabled external agents during keyboard selection and closes with the external callback', async () => {
|
||||
const { picker, onSelect, onSelectExternal } = await setup()
|
||||
await picker.getByRole('tab', { name: 'External', exact: true }).click()
|
||||
const contract = picker.getByRole('button', { name: /Contract Review Agent/ })
|
||||
const document = picker.getByRole('button', { name: /Document Review Agent/ })
|
||||
await expect.element(picker.getByRole('button', { name: /Invoice Audit Agent/ })).toBeDisabled()
|
||||
await userEvent.tab()
|
||||
await expect
|
||||
.element(picker.getByRole('tabpanel', { name: 'External', exact: true }))
|
||||
.toHaveFocus()
|
||||
await userEvent.tab()
|
||||
await expect.element(contract).toHaveFocus()
|
||||
await userEvent.tab()
|
||||
await expect.element(document).toHaveFocus()
|
||||
await userEvent.tab()
|
||||
const manage = picker.getByRole('link', { name: 'Manage in Agents', exact: true })
|
||||
await expect.element(manage).toHaveFocus()
|
||||
await expect.element(manage).toHaveAttribute('href', '/agents')
|
||||
await userEvent.tab({ shift: true })
|
||||
await expect.element(document).toHaveFocus()
|
||||
await userEvent.keyboard('{Enter}')
|
||||
await expect
|
||||
.poll(() => onSelectExternal.mock.calls)
|
||||
.toEqual([[expect.objectContaining({ key: 'document-review', name: 'Document Review Agent' })]])
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
await expect.element(picker).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps Dify selection and starting from scratch as distinct actions', async () => {
|
||||
const { picker, trigger, onSelect, onSelectExternal, onStartFromScratch } = await setup()
|
||||
await picker.getByRole('button', { name: /Research Agent/ }).click()
|
||||
await expect
|
||||
.poll(() => onSelect.mock.calls)
|
||||
.toEqual([
|
||||
[
|
||||
{
|
||||
description: difyAgent.description,
|
||||
icon: difyAgent.icon,
|
||||
icon_background: difyAgent.icon_background,
|
||||
icon_type: difyAgent.icon_type,
|
||||
id: difyAgent.id,
|
||||
name: difyAgent.name,
|
||||
role: difyAgent.role,
|
||||
},
|
||||
],
|
||||
])
|
||||
expect(onSelectExternal).not.toHaveBeenCalled()
|
||||
await expect.element(picker).not.toBeInTheDocument()
|
||||
await trigger.click()
|
||||
await picker.getByRole('button', { name: 'Start from Scratch', exact: true }).click()
|
||||
expect(onStartFromScratch).toHaveBeenCalledOnce()
|
||||
expect(onSelect).toHaveBeenCalledOnce()
|
||||
await expect.element(picker).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resets the source and search when closing and reopening the picker', async () => {
|
||||
const { picker, trigger } = await setup()
|
||||
await picker.getByRole('tab', { name: 'External', exact: true }).click()
|
||||
await picker.getByRole('searchbox', { name: 'Search agents', exact: true }).fill('contract')
|
||||
await expect
|
||||
.element(picker.getByRole('button', { name: /Document Review Agent/ }))
|
||||
.not.toBeInTheDocument()
|
||||
await userEvent.keyboard('{Escape}')
|
||||
await expect
|
||||
.element(picker.getByRole('searchbox', { name: 'Search agents', exact: true }))
|
||||
.toHaveValue('')
|
||||
await userEvent.keyboard('{Escape}')
|
||||
await expect.element(picker).not.toBeInTheDocument()
|
||||
await trigger.click()
|
||||
await expect
|
||||
.element(picker.getByRole('tab', { name: 'All', exact: true }))
|
||||
.toHaveAttribute('aria-selected', 'true')
|
||||
await expect
|
||||
.element(picker.getByRole('searchbox', { name: 'Search agents', exact: true }))
|
||||
.toHaveValue('')
|
||||
await expect.element(picker.getByRole('button', { name: /Research Agent/ })).toBeVisible()
|
||||
await expect.element(picker.getByRole('button', { name: /Document Review Agent/ })).toBeVisible()
|
||||
})
|
||||
|
||||
it('keeps external previews out of snippet selection and omits the workflow app filter', async () => {
|
||||
const { picker } = await setup({ kind: 'snippet', snippetId: 'snippet-1' })
|
||||
await expect
|
||||
.element(picker.getByRole('tab', { name: 'External', exact: true }))
|
||||
.not.toBeInTheDocument()
|
||||
await expect
|
||||
.element(picker.getByRole('button', { name: /Contract Review Agent/ }))
|
||||
.not.toBeInTheDocument()
|
||||
await expect
|
||||
.poll(() => mocks.fetchAgents.mock.calls.at(-1))
|
||||
.toEqual([{ query: { limit: 8, page: 1 } }])
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import type { MenuContentProps } from '@langgenius/dify-ui/menu'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { NodeDefault } from '../types'
|
||||
import type { AgentRosterNodeData } from './types'
|
||||
import type { ExternalAgentWorkflowPreviewAgent } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
MenuSeparator,
|
||||
MenuTrigger,
|
||||
} from '@langgenius/dify-ui/menu'
|
||||
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
@@ -24,11 +26,15 @@ import { createContext, use, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { editorAppIdAtom } from '@/app/components/workflow/editor-resource/state'
|
||||
import {
|
||||
editorAppIdAtom,
|
||||
editorResourceKindAtom,
|
||||
} from '@/app/components/workflow/editor-resource/state'
|
||||
import { toast } from '@/app/notifications'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { WorkflowAgentSelectorContent } from './workflow-agent-selector'
|
||||
|
||||
const AGENT_SELECTOR_PAGE_SIZE = 8
|
||||
|
||||
@@ -163,7 +169,7 @@ function AgentSelectorList({
|
||||
<MenuItem
|
||||
key={agent.id}
|
||||
closeOnClick={Boolean(agent.active_config_snapshot_id)}
|
||||
className="h-auto gap-2 py-1.5 pr-3 pl-2"
|
||||
className="h-auto gap-2 py-1.5 ps-2 pe-3"
|
||||
onClick={() => handleSelect(agent)}
|
||||
>
|
||||
<span aria-hidden className="shrink-0">
|
||||
@@ -215,7 +221,7 @@ function AgentSelectorLoadingSkeleton() {
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
'flex items-center gap-2 py-1.5 pr-3 pl-2 opacity-20',
|
||||
'flex items-center gap-2 py-1.5 ps-2 pe-3 opacity-20',
|
||||
index === 3 && 'opacity-10',
|
||||
)}
|
||||
>
|
||||
@@ -247,27 +253,32 @@ function toAgentRosterNodeData(agent: AgentInviteOptionResponse): AgentRosterNod
|
||||
export function AgentBlockItem({
|
||||
block,
|
||||
onSelect,
|
||||
onSelectExternal,
|
||||
onStartFromScratch,
|
||||
}: {
|
||||
block: NodeDefault
|
||||
onSelect: (agent: AgentRosterNodeData) => void
|
||||
onSelectExternal?: (agent: ExternalAgentWorkflowPreviewAgent) => void
|
||||
onStartFromScratch: () => void
|
||||
}) {
|
||||
const { t } = useTranslation(['navigation'])
|
||||
const { t: tAgentRoster } = useTranslation(['agentRoster'])
|
||||
const resourceKind = useAtomValue(editorResourceKindAtom)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<AgentSelectorMenu>
|
||||
<AgentSelectorMenuTrigger
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
className="w-full justify-start gap-0 px-3 text-left data-popup-open:bg-state-base-hover"
|
||||
className="w-full justify-start gap-0 px-3 text-start data-popup-open:bg-state-base-hover"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BlockIcon className="mr-2 shrink-0" type={block.metaData.type} />
|
||||
<BlockIcon className="me-2 shrink-0" type={block.metaData.type} />
|
||||
<span className="min-w-0 grow truncate system-sm-medium text-text-secondary">
|
||||
{block.metaData.title}
|
||||
</span>
|
||||
@@ -275,19 +286,28 @@ export function AgentBlockItem({
|
||||
size="xs"
|
||||
variant="dimm"
|
||||
text={t(($) => $['menus.status'], { ns: 'navigation' })}
|
||||
className="ml-2 shrink-0"
|
||||
className="ms-2 shrink-0"
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-custom-vender-solid-general-arrow-down-round-fill size-4 shrink-0 -rotate-90 text-text-tertiary"
|
||||
/>
|
||||
</AgentSelectorMenuTrigger>
|
||||
<AgentSelectorMenuContent
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
onSelect={onSelect}
|
||||
onStartFromScratch={onStartFromScratch}
|
||||
/>
|
||||
</AgentSelectorMenu>
|
||||
className="w-100 max-w-[calc(100vw-1rem)] overflow-hidden"
|
||||
>
|
||||
<PopoverTitle className="sr-only">
|
||||
{tAgentRoster(($) => $['roster.nodeSelector.dialogLabel'])}
|
||||
</PopoverTitle>
|
||||
<WorkflowAgentSelectorContent
|
||||
onSelect={onSelect}
|
||||
onSelectExternal={resourceKind === 'app' ? onSelectExternal : undefined}
|
||||
onStartFromScratch={onStartFromScratch}
|
||||
onRequestClose={() => setOpen(false)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { Fragment, memo, useCallback, useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { useHumanInputMigration } from '../nodes/human-input-v2/migration/context'
|
||||
import { getHumanInputCreationPolicy } from '../nodes/human-input-v2/migration/policy'
|
||||
@@ -142,6 +143,9 @@ const Blocks = ({
|
||||
version: '2',
|
||||
})
|
||||
}
|
||||
onSelectExternal={(agent) =>
|
||||
onSelect(BlockEnum.AgentV2, createExternalAgentWorkflowPreviewData(agent))
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from '../../plugins/types'
|
||||
import type { Collection, CollectionProviderType, Event } from '../../tools/types'
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ExternalAgentWorkflowPreviewDefaultValue } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
|
||||
export const TabType = {
|
||||
Start: 'start',
|
||||
@@ -123,7 +124,10 @@ type AgentDefaultValue = {
|
||||
|
||||
export type PluginDefaultValue = ToolDefaultValue | DataSourceDefaultValue | TriggerDefaultValue
|
||||
|
||||
export type BlockDefaultValue = PluginDefaultValue | AgentDefaultValue
|
||||
export type BlockDefaultValue =
|
||||
| PluginDefaultValue
|
||||
| AgentDefaultValue
|
||||
| ExternalAgentWorkflowPreviewDefaultValue
|
||||
|
||||
export type ToolValue = {
|
||||
provider_name: string
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ExternalAgentWorkflowPreviewAgent } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
|
||||
// Local examples for the external Agent picker until its API is available.
|
||||
export const externalWorkflowAgentExamples: readonly ExternalAgentWorkflowPreviewAgent[] = [
|
||||
{
|
||||
key: 'contract-review',
|
||||
name: 'Contract Review Agent',
|
||||
role: 'Legal & compliance',
|
||||
description: 'Reviews contract clauses against your company’s policies.',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
key: 'document-review',
|
||||
name: 'Document Review Agent',
|
||||
role: 'Document review',
|
||||
description: 'Reviews documents and highlights information that needs attention.',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
key: 'invoice-audit',
|
||||
name: 'Invoice Audit Agent',
|
||||
role: 'Finance',
|
||||
description: 'Checks invoices for discrepancies and missing information.',
|
||||
enabled: false,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,250 @@
|
||||
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AgentRosterNodeData } from './types'
|
||||
import type { ExternalAgentWorkflowPreviewAgent } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { SearchInput } from '@/app/components/base/search-input'
|
||||
import { editorAppIdAtom } from '@/app/components/workflow/editor-resource/state'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { externalWorkflowAgentExamples } from './workflow-agent-selector-preview'
|
||||
|
||||
type AgentSource = 'all' | 'dify' | 'external'
|
||||
|
||||
type WorkflowAgentSelectorContentProps = {
|
||||
onSelect: (agent: AgentRosterNodeData) => void
|
||||
onSelectExternal?: (agent: ExternalAgentWorkflowPreviewAgent) => void
|
||||
onStartFromScratch: () => void
|
||||
onRequestClose: () => void
|
||||
}
|
||||
|
||||
export function WorkflowAgentSelectorContent({
|
||||
onSelect,
|
||||
onSelectExternal,
|
||||
onStartFromScratch,
|
||||
onRequestClose,
|
||||
}: WorkflowAgentSelectorContentProps) {
|
||||
const { t } = useTranslation(['agentRoster', 'workflow', 'common'])
|
||||
const appId = useAtomValue(editorAppIdAtom)
|
||||
const [source, setSource] = useState<AgentSource>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
const keyword = useDebounce(search.trim(), { wait: 300 })
|
||||
const agentsQuery = useQuery({
|
||||
...consoleQuery.agent.inviteOptions.get.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
limit: 8,
|
||||
page: 1,
|
||||
...(appId ? { app_id: appId } : {}),
|
||||
...(keyword ? { keyword } : {}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
enabled: source !== 'external',
|
||||
staleTime: 0,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
const difyAgents = source === 'external' ? [] : (agentsQuery.data?.data ?? [])
|
||||
const externalAgents =
|
||||
source === 'dify' || !onSelectExternal
|
||||
? []
|
||||
: externalWorkflowAgentExamples.filter((agent) =>
|
||||
`${agent.name} ${agent.role} ${agent.description}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(search.trim().toLocaleLowerCase()),
|
||||
)
|
||||
const isLoading = source !== 'external' && agentsQuery.isPending
|
||||
const hasDifyError = source !== 'external' && agentsQuery.isError
|
||||
const isEmpty =
|
||||
!isLoading && !hasDifyError && difyAgents.length === 0 && externalAgents.length === 0
|
||||
const sources: AgentSource[] = onSelectExternal ? ['all', 'dify', 'external'] : ['all', 'dify']
|
||||
|
||||
const selectDifyAgent = (agent: AgentInviteOptionResponse) => {
|
||||
if (!agent.active_config_snapshot_id) return
|
||||
onRequestClose()
|
||||
onSelect({
|
||||
description: agent.description,
|
||||
icon: agent.icon,
|
||||
icon_background: agent.icon_background,
|
||||
icon_type: agent.icon_type,
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2">
|
||||
<SearchInput
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
aria-label={t(($) => $['roster.searchLabel'])}
|
||||
placeholder={t(($) => $['roster.nodeSelector.searchAgentsPlaceholder'])}
|
||||
/>
|
||||
</div>
|
||||
<Tabs
|
||||
value={source}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'all' || value === 'dify' || value === 'external') setSource(value)
|
||||
}}
|
||||
>
|
||||
<TabsList
|
||||
aria-label={t(($) => $['roster.nodeSelector.dialogLabel'])}
|
||||
className="h-8 gap-0.5 border-b border-divider-subtle px-3"
|
||||
>
|
||||
{sources.map((tab) => (
|
||||
<TabsTab
|
||||
key={tab}
|
||||
value={tab}
|
||||
className="h-6 rounded-md border-0 px-2 py-1 system-xs-medium text-text-tertiary data-active:bg-state-base-hover-alt data-active:text-text-primary"
|
||||
>
|
||||
{t(($) => $[`roster.nodeSelector.tabs.${tab}`])}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{sources.map((tab) => (
|
||||
<TabsPanel
|
||||
key={tab}
|
||||
value={tab}
|
||||
className="flex max-h-64 min-h-26 flex-col overflow-y-auto overscroll-contain p-1"
|
||||
>
|
||||
{source === tab && (
|
||||
<>
|
||||
{source !== 'external' && !search.trim() && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-full shrink-0 justify-start gap-2 rounded-lg ps-3 pe-2 system-sm-medium"
|
||||
onClick={() => {
|
||||
onRequestClose()
|
||||
onStartFromScratch()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-add-line size-4 shrink-0 text-text-tertiary"
|
||||
/>
|
||||
{t(($) => $['roster.nodeSelector.startFromScratch'])}
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && <DifyAgentSelectorSkeleton />}
|
||||
{hasDifyError && (
|
||||
<p role="status" className="px-3 py-2 system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['roster.loadingError'])}
|
||||
</p>
|
||||
)}
|
||||
{!hasDifyError &&
|
||||
difyAgents.map((agent) => (
|
||||
<Button
|
||||
key={agent.id}
|
||||
variant="ghost"
|
||||
disabled={!agent.active_config_snapshot_id}
|
||||
focusableWhenDisabled
|
||||
className="h-auto min-h-8 w-full shrink-0 justify-start gap-2 rounded-lg py-1 ps-3 pe-2"
|
||||
onClick={() => selectDifyAgent(agent)}
|
||||
>
|
||||
<AppIcon size="tiny" icon={{ ...agent, icon_url: agent.icon }} />
|
||||
<span className="flex min-w-0 flex-1 flex-col text-start">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate system-sm-medium text-text-secondary">
|
||||
{agent.name}
|
||||
</span>
|
||||
{source === 'all' && (
|
||||
<span className="shrink-0 system-xs-regular text-text-quaternary">
|
||||
{t(($) => $['roster.nodeSelector.source.dify'])}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{(agent.role || agent.description) && (
|
||||
<span className="truncate system-xs-regular text-text-tertiary">
|
||||
{agent.role || agent.description}
|
||||
</span>
|
||||
)}
|
||||
{!agent.active_config_snapshot_id && (
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['nodes.agent.modelNotSelected'], { ns: 'workflow' })}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
{externalAgents.map((agent) => (
|
||||
<Button
|
||||
key={agent.key}
|
||||
variant="ghost"
|
||||
disabled={!agent.enabled}
|
||||
className="h-8 w-full shrink-0 justify-start gap-2 rounded-lg ps-3 pe-2 disabled:opacity-50"
|
||||
onClick={() => {
|
||||
onRequestClose()
|
||||
onSelectExternal?.(agent)
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
size="tiny"
|
||||
icon={agent.icon}
|
||||
className="rounded-[7px] border-0"
|
||||
fallback={{
|
||||
content: (
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-robot-2-line size-3.5 text-text-accent"
|
||||
/>
|
||||
),
|
||||
background: 'var(--color-components-icon-bg-indigo-soft)',
|
||||
}}
|
||||
/>
|
||||
<span className="truncate system-sm-medium text-text-secondary">
|
||||
{agent.name}
|
||||
</span>
|
||||
<span className="shrink-0 system-xs-regular text-text-quaternary">
|
||||
{agent.enabled
|
||||
? t(($) => $['roster.nodeSelector.source.external'])
|
||||
: t(($) => $['external.filters.disabled'])}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
{isEmpty && (
|
||||
<p role="status" className="px-3 py-2 system-sm-regular text-text-tertiary">
|
||||
{search.trim()
|
||||
? t(($) => $['roster.emptySearch'])
|
||||
: t(($) => $['roster.empty'])}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className="flex h-8 items-center border-t border-divider-subtle px-4">
|
||||
<Link
|
||||
href="/agents"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-0.5 rounded-sm system-xs-medium text-text-tertiary hover:text-text-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-state-accent-solid"
|
||||
>
|
||||
{t(($) => $['roster.nodeSelector.manageInAgentConsole'])}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DifyAgentSelectorSkeleton() {
|
||||
return (
|
||||
<div aria-hidden className="space-y-1 p-2">
|
||||
{[0, 1, 2].map((item) => (
|
||||
<div key={item} className="flex h-8 items-center gap-2 opacity-20">
|
||||
<span className="size-6 rounded-md bg-text-quaternary" />
|
||||
<span className="h-2 w-32 rounded-xs bg-text-quaternary" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import ReactFlow, { ReactFlowProvider, useEdges, useNodes } from 'reactflow'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { AppToaster } from '@/app/notifications/app-toaster'
|
||||
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
|
||||
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
@@ -687,6 +689,49 @@ describe('useChecklistBeforePublish', () => {
|
||||
})
|
||||
|
||||
describe('useWorkflowRunValidation', () => {
|
||||
it('blocks running and publishing a graph containing a local external agent preview', async () => {
|
||||
const graph = buildConnectedGraph()
|
||||
const preview = createNode({
|
||||
id: 'external-preview',
|
||||
data: {
|
||||
type: BlockEnum.Agent,
|
||||
title: 'External Agent',
|
||||
...createExternalAgentWorkflowPreviewData(createExternalRosterPreview()[1]!),
|
||||
},
|
||||
})
|
||||
const { wrapper } = createChecklistWrapper({
|
||||
nodes: [...graph.nodes, preview],
|
||||
edges: [
|
||||
createEdge({ source: 'start', target: preview.id }),
|
||||
createEdge({ source: preview.id, target: 'code' }),
|
||||
createEdge({ source: 'code', target: 'end' }),
|
||||
],
|
||||
})
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
nodes: useNodes<CommonNodeType>(),
|
||||
run: useWorkflowRunValidation(),
|
||||
publish: useChecklistBeforePublish(),
|
||||
}),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
expect(result.current.run.warningNodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: preview.id,
|
||||
errorMessages: ['agentRoster.external.workflow.runUnavailable'],
|
||||
canNavigate: true,
|
||||
}),
|
||||
)
|
||||
act(() => {
|
||||
expect(result.current.run.validateBeforeRun()).toBe(false)
|
||||
})
|
||||
await act(async () => {
|
||||
expect(await result.current.publish.handleCheckBeforePublish()).toBe(false)
|
||||
})
|
||||
expect(result.current.nodes.find((node) => node.id === preview.id)?.data.selected).toBe(true)
|
||||
})
|
||||
|
||||
it('blocks a run with disconnected nodes and explains why', async () => {
|
||||
const { nodes } = buildConnectedGraph()
|
||||
const { wrapper } = createChecklistWrapper({ nodes, edges: [] })
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { act, waitFor } from '@testing-library/react'
|
||||
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
|
||||
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
|
||||
import { createEdge, createNode } from '../../__tests__/fixtures'
|
||||
import { renderWorkflowFlowHook } from '../../__tests__/workflow-test-env'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { useCollaborativeWorkflow } from '../use-collaborative-workflow'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -76,3 +79,47 @@ it('updates the local graph while stripping selection state from broadcasts', as
|
||||
expect(previousEdges[0].data).not.toHaveProperty('_connectedNodeIsSelected')
|
||||
expect(nextEdges[0].data).not.toHaveProperty('_connectedNodeIsSelected')
|
||||
})
|
||||
|
||||
it('keeps external agent previews and their edges local through edits and deletion', async () => {
|
||||
mocks.canApplyLocalGraphMutation.mockReturnValue(true)
|
||||
const { result } = setup()
|
||||
await waitFor(() => expect(result.current.getState().nodes).toHaveLength(2))
|
||||
const originalNodes = result.current.getState().nodes
|
||||
const preview = createNode({
|
||||
id: 'external-preview',
|
||||
data: {
|
||||
type: BlockEnum.Agent,
|
||||
...createExternalAgentWorkflowPreviewData(createExternalRosterPreview()[1]!),
|
||||
},
|
||||
})
|
||||
const previewEdge = createEdge({ id: 'preview-edge', source: oldNode.id, target: preview.id })
|
||||
act(() => {
|
||||
result.current.setNodes([...originalNodes, preview], false)
|
||||
result.current.setEdges([oldEdge, previewEdge], false)
|
||||
})
|
||||
expect(mocks.setNodes).not.toHaveBeenCalled()
|
||||
expect(mocks.setEdges).not.toHaveBeenCalled()
|
||||
|
||||
act(() => {
|
||||
result.current.setNodes([newNode, originalNodes[1]!, preview])
|
||||
result.current.setEdges([newEdge, previewEdge])
|
||||
})
|
||||
expect(result.current.getState().nodes).toHaveLength(3)
|
||||
expect(result.current.getState().edges).toHaveLength(2)
|
||||
const [oldBroadcastNodes, newBroadcastNodes] = mocks.setNodes.mock.lastCall!
|
||||
expect(oldBroadcastNodes.map((node: { id: string }) => node.id)).toEqual(['old-node', 'target'])
|
||||
expect(newBroadcastNodes.map((node: { id: string }) => node.id)).toEqual(['new-node', 'target'])
|
||||
expect(mocks.setEdges).toHaveBeenLastCalledWith(
|
||||
[expect.objectContaining({ id: oldEdge.id })],
|
||||
[expect.objectContaining({ id: newEdge.id })],
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setNodes([newNode, originalNodes[1]!])
|
||||
result.current.setEdges([newEdge])
|
||||
})
|
||||
expect(mocks.setEdges).toHaveBeenLastCalledWith(
|
||||
[expect.objectContaining({ id: newEdge.id })],
|
||||
[expect.objectContaining({ id: newEdge.id })],
|
||||
)
|
||||
})
|
||||
@@ -6,9 +6,13 @@ import { zWorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps
|
||||
import { act, waitFor } from '@testing-library/react'
|
||||
import { use, useLayoutEffect } from 'react'
|
||||
import { useEdges, useNodes } from 'reactflow'
|
||||
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryWrapper, seedAppDslVersion } from '@/test/console/query-data'
|
||||
import { createDeferred } from '@/test/deferred'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import { createEdge, createNode } from '../../__tests__/fixtures'
|
||||
import { renderWorkflowFlowHook } from '../../__tests__/workflow-test-env'
|
||||
import { WORKFLOW_COMMON_NODES } from '../../constants/node'
|
||||
@@ -543,6 +547,56 @@ it.each(['add', 'replace'] as const)(
|
||||
},
|
||||
)
|
||||
|
||||
it('adds an external agent preview beside an existing connection without changing or syncing the workflow graph', async ({
|
||||
onTestFinished,
|
||||
}) => {
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
const originalNodes = [codeNode('a'), codeNode('b')]
|
||||
const originalEdge = createEdge({ id: 'original-connection', source: 'a', target: 'b' })
|
||||
const { result, sync } = setup({ nodes: originalNodes, edges: [originalEdge] })
|
||||
const originalConfiguration = structuredClone(
|
||||
result.current.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
position: node.position,
|
||||
data: { ...node.data, selected: false },
|
||||
})),
|
||||
)
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] })
|
||||
onTestFinished(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
act(() =>
|
||||
result.current.handleNodeAdd(
|
||||
{
|
||||
nodeType: BlockEnum.AgentV2,
|
||||
pluginDefaultValue: createExternalAgentWorkflowPreviewData(
|
||||
createExternalRosterPreview()[1]!,
|
||||
),
|
||||
},
|
||||
{ prevNodeId: 'a', nextNodeId: 'b' },
|
||||
),
|
||||
)
|
||||
await act(async () => vi.advanceTimersByTimeAsync(2000))
|
||||
|
||||
const previews = result.current.nodes.filter((node) =>
|
||||
isExternalAgentWorkflowPreviewData(node.data),
|
||||
)
|
||||
expect(previews).toHaveLength(1)
|
||||
expect(result.current.edges).toEqual([originalEdge])
|
||||
expect(
|
||||
result.current.nodes
|
||||
.filter((node) => !isExternalAgentWorkflowPreviewData(node.data))
|
||||
.map((node) => ({
|
||||
id: node.id,
|
||||
position: node.position,
|
||||
data: { ...node.data, selected: false },
|
||||
})),
|
||||
).toEqual(originalConfiguration)
|
||||
expect(sync).not.toHaveBeenCalled()
|
||||
expect(createAgent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates an inline Agent through the service boundary and resolves the pending graph node', async () => {
|
||||
const request = createDeferred<Awaited<ReturnType<typeof createAgent>>>()
|
||||
createAgent.mockReturnValue(request.promise)
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
getAgentToolPublishIssues,
|
||||
useAgentToolPresentation,
|
||||
} from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
@@ -181,7 +182,7 @@ const getDuplicateEndOutputMessages = (
|
||||
}
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation(['agentV2', 'common', 'workflow', 'modelProvider'])
|
||||
const { t } = useTranslation(['agentV2', 'common', 'workflow', 'modelProvider', 'agentRoster'])
|
||||
const language = useGetLanguage()
|
||||
const workspaceId = useAtomValue(currentWorkspaceIdAtom)
|
||||
const contactIds = useMemo(() => getHumanInputContactIds(nodes), [nodes])
|
||||
@@ -410,6 +411,16 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
for (let i = 0; i < filteredNodes.length; i++) {
|
||||
const node = filteredNodes[i]
|
||||
if (node && isExternalAgentWorkflowPreviewData(node.data)) {
|
||||
list.push({
|
||||
id: node.id,
|
||||
type: BlockEnum.AgentV2,
|
||||
title: node.data.title,
|
||||
errorMessages: [t(($) => $['external.workflow.runUnavailable'], { ns: 'agentRoster' })],
|
||||
canNavigate: true,
|
||||
})
|
||||
continue
|
||||
}
|
||||
let moreDataForCheckValid: CheckValidExtraData
|
||||
let usedVars: ValueSelector[] = []
|
||||
|
||||
@@ -756,6 +767,19 @@ export const useChecklistBeforePublish = () => {
|
||||
const { dataSourceList, environmentVariables = [] } = workflowStore.getState()
|
||||
const nodes = getNodes()
|
||||
const filteredNodes = nodes.filter((node) => node.type === CUSTOM_NODE)
|
||||
const externalAgentPreview = filteredNodes.find((node) =>
|
||||
isExternalAgentWorkflowPreviewData(node.data),
|
||||
)
|
||||
if (externalAgentPreview) {
|
||||
store.getState().setNodes(
|
||||
nodes.map((node) => ({
|
||||
...node,
|
||||
selected: node.id === externalAgentPreview.id,
|
||||
data: { ...node.data, selected: node.id === externalAgentPreview.id },
|
||||
})),
|
||||
)
|
||||
return false
|
||||
}
|
||||
const duplicateEndOutputMessages = getDuplicateEndOutputMessages(filteredNodes, t)
|
||||
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Edge, Node } from '../types'
|
||||
import { useRefWithInit } from '@langgenius/dify-ui/use-ref-with-init'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
|
||||
const sanitizeNodeForBroadcast = (node: Node): Node => {
|
||||
@@ -33,6 +35,7 @@ const sanitizeEdgeForBroadcast = (edge: Edge): Edge => {
|
||||
|
||||
export const useCollaborativeWorkflow = () => {
|
||||
const store = useStoreApi()
|
||||
const previewNodeIds = useRefWithInit(() => new Set<string>()).current
|
||||
const { setNodes: collabSetNodes, setEdges: collabSetEdges } = collaborationManager
|
||||
|
||||
const setNodes = useCallback(
|
||||
@@ -42,33 +45,48 @@ export const useCollaborativeWorkflow = () => {
|
||||
source = 'use-collaborative-workflow:setNodes',
|
||||
) => {
|
||||
const { getNodes, setNodes: reactFlowSetNodes } = store.getState()
|
||||
const oldNodes = getNodes()
|
||||
for (const node of [...oldNodes, ...newNodes]) {
|
||||
if (isExternalAgentWorkflowPreviewData(node.data)) previewNodeIds.add(node.id)
|
||||
}
|
||||
if (shouldBroadcast) {
|
||||
if (!collaborationManager.canApplyLocalGraphMutation()) return
|
||||
|
||||
const oldNodes = getNodes()
|
||||
collabSetNodes(
|
||||
oldNodes.map(sanitizeNodeForBroadcast),
|
||||
newNodes.map(sanitizeNodeForBroadcast),
|
||||
oldNodes
|
||||
.filter((node) => !isExternalAgentWorkflowPreviewData(node.data))
|
||||
.map(sanitizeNodeForBroadcast),
|
||||
newNodes
|
||||
.filter((node) => !isExternalAgentWorkflowPreviewData(node.data))
|
||||
.map(sanitizeNodeForBroadcast),
|
||||
source,
|
||||
)
|
||||
}
|
||||
reactFlowSetNodes(newNodes)
|
||||
},
|
||||
[store, collabSetNodes],
|
||||
[store, collabSetNodes, previewNodeIds],
|
||||
)
|
||||
|
||||
const setEdges = useCallback(
|
||||
(newEdges: Edge[], shouldBroadcast: boolean = true) => {
|
||||
const { edges, setEdges: reactFlowSetEdges } = store.getState()
|
||||
const { edges, getNodes, setEdges: reactFlowSetEdges } = store.getState()
|
||||
for (const node of getNodes()) {
|
||||
if (isExternalAgentWorkflowPreviewData(node.data)) previewNodeIds.add(node.id)
|
||||
}
|
||||
if (shouldBroadcast) {
|
||||
if (!collaborationManager.canApplyLocalGraphMutation()) return
|
||||
|
||||
collabSetEdges(edges.map(sanitizeEdgeForBroadcast), newEdges.map(sanitizeEdgeForBroadcast))
|
||||
const canBroadcastEdge = (edge: Edge) =>
|
||||
!previewNodeIds.has(edge.source) && !previewNodeIds.has(edge.target)
|
||||
collabSetEdges(
|
||||
edges.filter(canBroadcastEdge).map(sanitizeEdgeForBroadcast),
|
||||
newEdges.filter(canBroadcastEdge).map(sanitizeEdgeForBroadcast),
|
||||
)
|
||||
}
|
||||
|
||||
reactFlowSetEdges(newEdges)
|
||||
},
|
||||
[store, collabSetEdges],
|
||||
[store, collabSetEdges, previewNodeIds],
|
||||
)
|
||||
|
||||
const collaborativeStore = useCallback(() => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { getConnectedEdges, getOutgoers, useReactFlow } from 'reactflow'
|
||||
import { isChatflowAtom } from '@/app/components/workflow/editor-resource/state'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import {
|
||||
@@ -884,6 +885,27 @@ export const useNodesInteractions = () => {
|
||||
y: 0,
|
||||
},
|
||||
})
|
||||
if (isExternalAgentWorkflowPreviewData(newNode.data)) {
|
||||
const anchor = nodes.find((node) => node.id === (nextNodeId ?? prevNodeId))
|
||||
const anchorPosition = anchor?.positionAbsolute ?? anchor?.position
|
||||
const mousePosition = workflowStore.getState().mousePosition
|
||||
newNode.position = anchorPosition
|
||||
? { x: anchorPosition.x + NODE_WIDTH_X_OFFSET, y: anchorPosition.y + Y_OFFSET }
|
||||
: reactflow.screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
setNodes(
|
||||
[
|
||||
...nodes.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
data: { ...node.data, selected: false },
|
||||
})),
|
||||
newNode,
|
||||
],
|
||||
false,
|
||||
)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
|
||||
return
|
||||
}
|
||||
if (prevNodeId && !nextNodeId) {
|
||||
const prevNodeIndex = nodes.findIndex((node) => node.id === prevNodeId)
|
||||
const prevNode = nodes[prevNodeIndex]
|
||||
@@ -1356,6 +1378,7 @@ export const useNodesInteractions = () => {
|
||||
createInlineAgentBindingForNode,
|
||||
saveStateToHistory,
|
||||
workflowStore,
|
||||
reactflow,
|
||||
getAfterNodesInSameBranch,
|
||||
nodesMetaDataMap,
|
||||
t,
|
||||
@@ -1375,6 +1398,15 @@ export const useNodesInteractions = () => {
|
||||
const nodeMetaData = nodesMetaDataMap?.[nodeType]
|
||||
if (!nodeMetaData) return
|
||||
const { defaultValue } = nodeMetaData
|
||||
if (
|
||||
isExternalAgentWorkflowPreviewData({
|
||||
...defaultValue,
|
||||
...pluginDefaultValue,
|
||||
} as Node['data'])
|
||||
) {
|
||||
handleNodeAdd({ nodeType, sourceHandle, pluginDefaultValue }, { prevNodeId: currentNodeId })
|
||||
return
|
||||
}
|
||||
const insertionBlocker = getHumanInputInsertionBlocker(nodes, nodeType, {
|
||||
...(defaultValue as Node['data']),
|
||||
...pluginDefaultValue,
|
||||
@@ -1576,6 +1608,7 @@ export const useNodesInteractions = () => {
|
||||
saveStateToHistory,
|
||||
nodesMetaDataMap,
|
||||
autoGenerateWebhookUrl,
|
||||
handleNodeAdd,
|
||||
t,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { canRunBySingle } from '@/app/components/workflow/utils'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { matchesProviderReference } from '@/utils/provider-reference'
|
||||
import { useNodesInteractions } from '../hooks/use-nodes-interactions'
|
||||
@@ -41,7 +42,11 @@ export function useNodeActionsMenuModel({
|
||||
)
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
const canRun = canRunWorkflow && !nodesReadOnly && canRunBySingle(data.type, isChildNode)
|
||||
const canRun =
|
||||
canRunWorkflow &&
|
||||
!nodesReadOnly &&
|
||||
!isExternalAgentWorkflowPreviewData(data) &&
|
||||
canRunBySingle(data.type, isChildNode)
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
const canChangeBlock = !nodeMetaData.isTypeFixed && !nodeMetaData.isUndeletable && !nodesReadOnly
|
||||
const sourceHandle = useMemo(() => {
|
||||
@@ -62,6 +67,7 @@ export function useNodeActionsMenuModel({
|
||||
}, [data.provider_id, data.provider_type, data.type, workflowTools])
|
||||
|
||||
const handleRun = useCallback(() => {
|
||||
if (!canRun) return
|
||||
const store = workflowStore.getState()
|
||||
store.setInitShowLastRunTab(true)
|
||||
store.setPendingSingleRun({
|
||||
@@ -70,7 +76,7 @@ export function useNodeActionsMenuModel({
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
onClose()
|
||||
}, [handleNodeSelect, id, isSingleRunning, onClose, workflowStore])
|
||||
}, [canRun, handleNodeSelect, id, isSingleRunning, onClose, workflowStore])
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
onClose()
|
||||
|
||||
+20
-10
@@ -2,9 +2,26 @@ import { RiMindMap } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
const FailBranchCard = () => {
|
||||
const FailBranchCard = ({
|
||||
learnMorePlacement = 'inline',
|
||||
}: {
|
||||
learnMorePlacement?: 'inline' | 'below'
|
||||
}) => {
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const docLink = useDocLink()
|
||||
const learnMore = (
|
||||
<a
|
||||
href={docLink('/use-dify/debug/error-type')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 rounded-sm text-text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
>
|
||||
{t(($) => $['common.learnMore'], { ns: 'workflow' })}
|
||||
{learnMorePlacement === 'below' && (
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3" />
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="px-4 pt-2">
|
||||
@@ -17,17 +34,10 @@ const FailBranchCard = () => {
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['nodes.common.errorHandle.failBranch.customizeTip'], { ns: 'workflow' })}
|
||||
|
||||
<a
|
||||
href={docLink('/use-dify/debug/error-type')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t(($) => $['common.learnMore'], { ns: 'workflow' })}
|
||||
</a>
|
||||
{learnMorePlacement === 'inline' && <> {learnMore}</>}
|
||||
</div>
|
||||
</div>
|
||||
{learnMorePlacement === 'below' && <div className="mt-2 system-xs-medium">{learnMore}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { NodeActionsMenu } from '@/app/components/workflow/node-actions-menu'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { useNodesInteractions } from '../../../hooks/use-nodes-interactions'
|
||||
import { useNodesReadOnly } from '../../../hooks/use-workflow'
|
||||
import { NodeRunningStatus } from '../../../types'
|
||||
@@ -27,7 +28,7 @@ const NodeControl: FC<NodeControlProps> = ({ id, data, pluginInstallLocked }) =>
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute -top-8 right-0 flex h-8 origin-bottom-right pb-1 opacity-0',
|
||||
'pointer-events-none absolute inset-e-0 -top-8 flex h-8 origin-bottom-right pb-1 opacity-0',
|
||||
'focus-within:pointer-events-auto focus-within:opacity-100',
|
||||
!pluginInstallLocked && 'group-hover:pointer-events-auto group-hover:opacity-100',
|
||||
data.selected && 'pointer-events-auto opacity-100',
|
||||
@@ -40,39 +41,42 @@ const NodeControl: FC<NodeControlProps> = ({ id, data, pluginInstallLocked }) =>
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{canRun && !nodesReadOnly && canRunBySingle(data.type, isChildNode) && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
isSingleRunning
|
||||
? t(($) => $['debug.variableInspect.trigger.stop'], { ns: 'workflowDebug' })
|
||||
: t(($) => $['panel.runThisStep'], { ns: 'workflow' })
|
||||
}
|
||||
className={`flex size-6 items-center justify-center rounded-md ${isSingleRunning && 'cursor-pointer hover:bg-state-base-hover'}`}
|
||||
onClick={() => {
|
||||
const action = isSingleRunning ? 'stop' : 'run'
|
||||
{canRun &&
|
||||
!nodesReadOnly &&
|
||||
!isExternalAgentWorkflowPreviewData(data) &&
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
isSingleRunning
|
||||
? t(($) => $['debug.variableInspect.trigger.stop'], { ns: 'workflowDebug' })
|
||||
: t(($) => $['panel.runThisStep'], { ns: 'workflow' })
|
||||
}
|
||||
className={`flex size-6 items-center justify-center rounded-md ${isSingleRunning && 'cursor-pointer hover:bg-state-base-hover'}`}
|
||||
onClick={() => {
|
||||
const action = isSingleRunning ? 'stop' : 'run'
|
||||
|
||||
const store = workflowStore.getState()
|
||||
store.setInitShowLastRunTab(true)
|
||||
store.setPendingSingleRun({
|
||||
nodeId: id,
|
||||
action,
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
}}
|
||||
>
|
||||
{isSingleRunning ? (
|
||||
<span aria-hidden className="i-custom-vender-line-mediaanddevices-stop size-3" />
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-play-large-line size-3" />} />
|
||||
<TooltipContent>
|
||||
{t(($) => $['panel.runThisStep'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
const store = workflowStore.getState()
|
||||
store.setInitShowLastRunTab(true)
|
||||
store.setPendingSingleRun({
|
||||
nodeId: id,
|
||||
action,
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
}}
|
||||
>
|
||||
{isSingleRunning ? (
|
||||
<span aria-hidden className="i-custom-vender-line-mediaanddevices-stop size-3" />
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-play-large-line size-3" />} />
|
||||
<TooltipContent>
|
||||
{t(($) => $['panel.runThisStep'], { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<NodeActionsMenu id={id} data={data} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-12
@@ -25,7 +25,6 @@ import {
|
||||
getOutputDisplayType,
|
||||
getOutputTypeOptionValue,
|
||||
insertOutputChildAtPath,
|
||||
isDefaultOutput,
|
||||
toDeclaredOutputChild,
|
||||
updateOutputChildAtPath,
|
||||
} from './utils'
|
||||
@@ -44,10 +43,10 @@ function OutputRow({
|
||||
onEdit: () => void
|
||||
}) {
|
||||
const { t } = useTranslation(['workflow', 'common', 'workflowAgent'])
|
||||
const description = getOutputDescription(output, t)
|
||||
const description = editable ? output.description || '' : getOutputDescription(output, t)
|
||||
return (
|
||||
<div className="group flex min-h-12 flex-col rounded-lg py-0.5 focus-within:bg-state-base-hover hover:bg-state-base-hover">
|
||||
<div className="flex h-6 items-center gap-x-1 pr-0.5 pl-1">
|
||||
<div className="flex h-6 items-center gap-x-1 ps-1 pe-0.5">
|
||||
<div className="flex min-w-0 grow items-center gap-x-1">
|
||||
<span className="flex h-5 min-w-0 items-center px-1">
|
||||
<span className="truncate code-sm-semibold leading-4 text-text-primary">
|
||||
@@ -144,6 +143,8 @@ export function AgentOutputVariables({
|
||||
onChange,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
compact = false,
|
||||
reservedOutputNames = AGENT_V2_RESERVED_OUTPUT_NAMES,
|
||||
}: AgentOutputVariablesProps) {
|
||||
const { t } = useTranslation(['workflowAgent'])
|
||||
const [editingState, setEditingState] = useState<EditingState | null>(null)
|
||||
@@ -277,11 +278,28 @@ export function AgentOutputVariables({
|
||||
)
|
||||
}
|
||||
return (
|
||||
<OutputVars collapsed={isCollapsed} onCollapse={handleCollapseChange}>
|
||||
<OutputVars
|
||||
collapsed={isCollapsed}
|
||||
onCollapse={handleCollapseChange}
|
||||
operations={
|
||||
compact ? (
|
||||
<IconButton
|
||||
aria-label={t(($) => $['nodes.agent.outputVars.newOutput'], { ns: 'workflowAgent' })}
|
||||
className="me-4"
|
||||
onClick={() => {
|
||||
handleCollapseChange(false)
|
||||
handleNewOutput()
|
||||
}}
|
||||
>
|
||||
<span aria-hidden className="i-ri-add-line size-4" />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className="pb-2">
|
||||
<div className="flex flex-col">
|
||||
{outputs.map((output, index) => {
|
||||
const editable = !isDefaultOutput(output)
|
||||
const editable = !reservedOutputNames.has(output.name)
|
||||
const children = getOutputChildren(output)
|
||||
const isEditingOutput =
|
||||
editingState?.outputIndex === index &&
|
||||
@@ -297,7 +315,7 @@ export function AgentOutputVariables({
|
||||
key={`${output.name}-editing`}
|
||||
editingIndex={index}
|
||||
existingOutputs={outputs}
|
||||
reservedNames={AGENT_V2_RESERVED_OUTPUT_NAMES}
|
||||
reservedNames={reservedOutputNames}
|
||||
state={editingState}
|
||||
onCancel={() => setEditingState(null)}
|
||||
onConfirm={handleConfirm}
|
||||
@@ -332,18 +350,20 @@ export function AgentOutputVariables({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="py-1">
|
||||
<Separator decorative orientation="horizontal" className="my-2 bg-divider-subtle" />
|
||||
</div>
|
||||
{!compact && (
|
||||
<div className="py-1">
|
||||
<Separator decorative orientation="horizontal" className="my-2 bg-divider-subtle" />
|
||||
</div>
|
||||
)}
|
||||
{editingState && editingState.outputIndex == null ? (
|
||||
<OutputEditCard
|
||||
existingOutputs={outputs}
|
||||
reservedNames={AGENT_V2_RESERVED_OUTPUT_NAMES}
|
||||
reservedNames={reservedOutputNames}
|
||||
state={editingState}
|
||||
onCancel={() => setEditingState(null)}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
) : (
|
||||
) : !compact ? (
|
||||
<div className="pt-1">
|
||||
<Button
|
||||
size="small"
|
||||
@@ -355,7 +375,7 @@ export function AgentOutputVariables({
|
||||
{t(($) => $['nodes.agent.outputVars.newOutput'], { ns: 'workflowAgent' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</OutputVars>
|
||||
|
||||
@@ -45,6 +45,8 @@ export type AgentOutputVariablesProps = {
|
||||
onChange: (outputs: DeclaredOutputConfig[]) => void
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
compact?: boolean
|
||||
reservedOutputNames?: ReadonlySet<string>
|
||||
}
|
||||
|
||||
export const OUTPUT_NAME_PATTERN = /^[a-z_]\w*$/i
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ComponentProps, ComponentType } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { ExternalAgentWorkflowPreviewNode } from '@/features/agent-v2/external-agent/workflow-preview/node'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { AgentV2Node } from './agent-v2/node'
|
||||
import { isAgentV2NodeData } from './agent-v2/types'
|
||||
@@ -36,6 +38,9 @@ type WorkflowAgentNodeProps = ComponentProps<typeof AgentNode> | ComponentProps<
|
||||
type WorkflowComponentMap = Record<string, ComponentType<Record<string, never>>>
|
||||
|
||||
function WorkflowAgentNode(props: WorkflowAgentNodeProps) {
|
||||
if (isExternalAgentWorkflowPreviewData(props.data))
|
||||
return createElement(ExternalAgentWorkflowPreviewNode, { data: props.data })
|
||||
|
||||
if (isAgentV2NodeData(props.data))
|
||||
return createElement(AgentV2Node, props as ComponentProps<typeof AgentV2Node>)
|
||||
|
||||
@@ -65,7 +70,7 @@ export const NodeComponentMap = {
|
||||
[BlockEnum.DocExtractor]: DocExtractorNode,
|
||||
[BlockEnum.ListFilter]: ListFilterNode,
|
||||
[BlockEnum.Agent]: WorkflowAgentNode,
|
||||
[BlockEnum.AgentV2]: AgentV2Node,
|
||||
[BlockEnum.AgentV2]: WorkflowAgentNode,
|
||||
[BlockEnum.DataSource]: DataSourceNode,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
|
||||
[BlockEnum.HumanInput]: WorkflowHumanInputNode,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Node } from '../types'
|
||||
import { memo } from 'react'
|
||||
import { ExternalAgentWorkflowPreviewPanel } from '@/features/agent-v2/external-agent/workflow-preview/panel'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { CUSTOM_NODE } from '../constants'
|
||||
import { getNodeCatalogType } from '../utils/node'
|
||||
import BasePanel from './_base/components/workflow-panel'
|
||||
@@ -15,6 +17,9 @@ const Panel = memo((props: PanelProps) => {
|
||||
const nodeData = props.data
|
||||
if (nodeClass !== CUSTOM_NODE) return null
|
||||
|
||||
if (isExternalAgentWorkflowPreviewData(nodeData))
|
||||
return <ExternalAgentWorkflowPreviewPanel id={props.id} data={nodeData} />
|
||||
|
||||
const PanelComponent = PanelComponentMap[nodeData.type]!
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import type { CommonNodeType, Node } from '../types'
|
||||
import { Position } from 'reactflow'
|
||||
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
|
||||
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
|
||||
import { CUSTOM_NODE, NESTED_ELEMENT_Z_INDEX } from '../constants'
|
||||
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
|
||||
import { isHumanInputV2NodeData } from '../nodes/human-input-v2/types'
|
||||
@@ -11,6 +12,7 @@ import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
||||
import { BlockEnum } from '../types'
|
||||
|
||||
export function getNodeCatalogType(data: CommonNodeType): BlockEnum {
|
||||
if (isExternalAgentWorkflowPreviewData(data)) return BlockEnum.AgentV2
|
||||
if (isAgentV2NodeData(data)) return BlockEnum.AgentV2
|
||||
if (isHumanInputV2NodeData(data)) return BlockEnum.HumanInputV2
|
||||
return data.type
|
||||
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import type { ExternalAgentWorkflowPreviewNodeData } from '../types'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { createInstance } from 'i18next'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import ReactFlow, { Handle, Position, ReactFlowProvider, useNodes } from 'reactflow'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { createEdge, createNode } from '@/app/components/workflow/__tests__/fixtures'
|
||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
|
||||
import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provider'
|
||||
import { createHooksStore } from '@/app/components/workflow/hooks-store/store'
|
||||
import { NodeComponentMap } from '@/app/components/workflow/nodes/components'
|
||||
import Panel from '@/app/components/workflow/nodes/panel'
|
||||
import { createWorkflowStore } from '@/app/components/workflow/store/workflow'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
|
||||
import agentRoster from '@/i18n/locales/en-US/agent-roster.json'
|
||||
import common from '@/i18n/locales/en-US/common.json'
|
||||
import workflowAgent from '@/i18n/locales/en-US/workflow-agent.json'
|
||||
import workflowDebug from '@/i18n/locales/en-US/workflow-debug.json'
|
||||
import workflow from '@/i18n/locales/en-US/workflow.json'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { seedToolProviders } from '@/test/console/tool-providers'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import { createExternalAgentWorkflowPreviewData } from '../preview'
|
||||
import { isExternalAgentWorkflowPreviewData } from '../types'
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
vi.mock('react-i18next', async (importOriginal) => importOriginal())
|
||||
|
||||
// The Agent catalog entry routes external preview data before mounting Dify API owners.
|
||||
const AgentNode = NodeComponentMap[BlockEnum.Agent] as unknown as ComponentType<{
|
||||
id: string
|
||||
data: ExternalAgentWorkflowPreviewNodeData
|
||||
}>
|
||||
|
||||
function CanvasNode({ id, data }: NodeProps<CommonNodeType>) {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
{isExternalAgentWorkflowPreviewData(data) ? (
|
||||
<AgentNode id={id} data={data} />
|
||||
) : (
|
||||
<div>{data.title}</div>
|
||||
)}
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes = { custom: CanvasNode }
|
||||
|
||||
function SelectedPanel() {
|
||||
const node = useNodes<CommonNodeType>().find((node) => node.selected || node.data.selected)
|
||||
return node ? <Panel type={node.type} id={node.id} data={node.data} /> : null
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
await page.viewport(1280, 1000)
|
||||
const i18n = createInstance()
|
||||
await i18n.init({
|
||||
lng: 'en-US',
|
||||
resources: { 'en-US': { agentRoster, common, workflowAgent, workflowDebug, workflow } },
|
||||
keySeparator: false,
|
||||
interpolation: { escapeValue: false },
|
||||
})
|
||||
const { queryClient, wrapper: QueryProvider } = createConsoleQueryWrapper({
|
||||
currentWorkspace: { role: 'owner' },
|
||||
})
|
||||
onTestFinished(() => queryClient.clear())
|
||||
seedModelProviderQueries(queryClient)
|
||||
seedToolProviders(queryClient, {
|
||||
builtInTools: [],
|
||||
customTools: [],
|
||||
workflowTools: [],
|
||||
mcpTools: [],
|
||||
})
|
||||
queryClient.setQueryData(consoleQuery.spec.schemaDefinitions.get.queryKey(), [])
|
||||
const syncDraft = vi.fn(async () => {})
|
||||
const hooks = createHooksStore({ doSyncWorkflowDraft: syncDraft })
|
||||
const store = createWorkflowStore({})
|
||||
const agent = createExternalRosterPreview()[1]!
|
||||
const data: ExternalAgentWorkflowPreviewNodeData = {
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Agent',
|
||||
desc: '',
|
||||
selected: true,
|
||||
...createExternalAgentWorkflowPreviewData(agent),
|
||||
}
|
||||
const nodes = [
|
||||
createNode({
|
||||
id: 'input',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: 'Input',
|
||||
variables: [
|
||||
{
|
||||
variable: 'contract_text',
|
||||
type: InputVarType.paragraph,
|
||||
label: 'Contract text',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
createNode({
|
||||
id: 'policy',
|
||||
position: { x: 0, y: 100 },
|
||||
data: {
|
||||
type: BlockEnum.Code,
|
||||
title: 'Policy',
|
||||
outputs: { result: { type: VarType.string } },
|
||||
},
|
||||
}),
|
||||
createNode({ id: 'external-agent', position: { x: 160, y: 50 }, selected: true, data }),
|
||||
]
|
||||
const edges = [
|
||||
createEdge({ source: 'input', target: 'policy' }),
|
||||
createEdge({ source: 'policy', target: 'external-agent' }),
|
||||
]
|
||||
await render(
|
||||
<QueryProvider>
|
||||
<EditorResourceScope
|
||||
resource={{
|
||||
kind: 'app',
|
||||
appId: 'external-preview-app',
|
||||
mode: 'workflow',
|
||||
collaborationEnabled: false,
|
||||
}}
|
||||
>
|
||||
<WorkflowContext value={store}>
|
||||
<HooksStoreContext value={hooks}>
|
||||
<EventEmitterContextProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ReactFlowProvider>
|
||||
<div className="isolate flex gap-4" style={{ height: 960 }}>
|
||||
<section aria-label="Agent canvas" style={{ width: 480, height: 960 }}>
|
||||
<ReactFlow
|
||||
defaultNodes={nodes}
|
||||
defaultEdges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
/>
|
||||
</section>
|
||||
<SelectedPanel />
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
</I18nextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
</HooksStoreContext>
|
||||
</WorkflowContext>
|
||||
</EditorResourceScope>
|
||||
</QueryProvider>,
|
||||
)
|
||||
onTestFinished(() => {
|
||||
expect(syncDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
const panel = page.getByRole('region', { name: 'Contract Review Agent', exact: true })
|
||||
await expect.element(panel).toBeVisible()
|
||||
return { panel, canvas: page.getByRole('region', { name: 'Agent canvas', exact: true }) }
|
||||
}
|
||||
|
||||
async function closeAndReopen(
|
||||
panel: ReturnType<typeof page.getByRole>,
|
||||
name = 'Contract Review Agent',
|
||||
) {
|
||||
await panel.getByRole('button', { name: 'Close', exact: true }).click()
|
||||
await expect.element(panel).not.toBeInTheDocument()
|
||||
await page
|
||||
.getByRole('region', { name: 'Agent canvas', exact: true })
|
||||
.getByText(name, { exact: true })
|
||||
.click()
|
||||
const reopened = page.getByRole('region', { name, exact: true })
|
||||
await expect.element(reopened).toBeVisible()
|
||||
return reopened
|
||||
}
|
||||
|
||||
async function replaceTask(task: ReturnType<typeof page.getByRole>, value: string) {
|
||||
await task.click()
|
||||
await userEvent.keyboard(
|
||||
/Mac/.test(navigator.userAgent) ? '{Meta>}a{/Meta}' : '{Control>}a{/Control}',
|
||||
)
|
||||
await userEvent.keyboard('{Backspace}')
|
||||
await expect.element(task).toHaveTextContent('')
|
||||
await task.fill(value)
|
||||
}
|
||||
|
||||
beforeEach(({ onTestFinished }) => {
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
})
|
||||
|
||||
it('shows the external badge and updates the canvas agent through the change picker without Dify APIs', async () => {
|
||||
const { panel, canvas } = await setup()
|
||||
await expect.element(panel.getByText('External', { exact: true })).toBeVisible()
|
||||
await expect.element(canvas.getByText('External', { exact: true })).toBeVisible()
|
||||
await expect
|
||||
.element(panel.getByRole('tab', { name: 'Settings', exact: true }))
|
||||
.toHaveAttribute('aria-selected', 'true')
|
||||
await expect
|
||||
.element(panel.getByRole('button', { name: /Run this step/i }))
|
||||
.toHaveAttribute('aria-disabled', 'true')
|
||||
await panel.getByRole('button', { name: 'Change', exact: true }).click()
|
||||
await page.getByRole('button', { name: /Document Review Agent/ }).click()
|
||||
const changedPanel = page.getByRole('region', { name: 'Document Review Agent', exact: true })
|
||||
await expect.element(changedPanel).toBeVisible()
|
||||
await expect.element(canvas.getByText('Document Review Agent', { exact: true })).toBeVisible()
|
||||
await expect
|
||||
.element(canvas.getByText('Contract Review Agent', { exact: true }))
|
||||
.not.toBeInTheDocument()
|
||||
await closeAndReopen(changedPanel, 'Document Review Agent')
|
||||
})
|
||||
|
||||
it('keeps edits made in the real task editor when closing and reopening the node', async () => {
|
||||
const { panel } = await setup()
|
||||
const task = panel.getByRole('textbox', { name: 'Agent task', exact: true })
|
||||
await expect.element(task).toMatchTextContent(/Policy/)
|
||||
await expect.element(task).toMatchTextContent(/Contract/)
|
||||
await replaceTask(task, 'Review the renewal terms and highlight unusual liability clauses.')
|
||||
await userEvent.tab()
|
||||
const reopened = await closeAndReopen(panel)
|
||||
await expect
|
||||
.element(reopened.getByRole('textbox', { name: 'Agent task', exact: true }))
|
||||
.toHaveTextContent('Review the renewal terms and highlight unusual liability clauses.')
|
||||
})
|
||||
|
||||
it('retains the wait limit and error strategy through panel reopening', async () => {
|
||||
const { panel } = await setup()
|
||||
const enabled = panel.getByRole('switch', { name: 'External agent wait limit', exact: true })
|
||||
const minutes = panel.getByRole('textbox', { name: 'Wait limit', exact: true })
|
||||
await expect.element(enabled).toBeChecked()
|
||||
await expect.element(minutes).toHaveValue('10')
|
||||
await minutes.fill('25')
|
||||
await userEvent.tab()
|
||||
await expect
|
||||
.element(panel.getByRole('slider', { name: 'Wait limit', exact: true }))
|
||||
.toHaveAttribute('aria-valuenow', '25')
|
||||
await enabled.click()
|
||||
await expect.element(enabled).not.toBeChecked()
|
||||
await enabled.click()
|
||||
await expect.element(minutes).toHaveValue('25')
|
||||
const errorHandling = panel.getByRole('button', { name: 'Fail Branch', exact: true })
|
||||
await expect.element(errorHandling).toBeVisible()
|
||||
await errorHandling.click()
|
||||
await page.getByRole('menuitemradio', { name: /^None/ }).click()
|
||||
const reopened = await closeAndReopen(panel)
|
||||
await expect
|
||||
.element(reopened.getByRole('textbox', { name: 'Wait limit', exact: true }))
|
||||
.toHaveValue('25')
|
||||
await expect.element(reopened.getByRole('button', { name: 'None', exact: true })).toBeVisible()
|
||||
await expect
|
||||
.element(reopened.getByRole('combobox', { name: 'On timeout', exact: true }))
|
||||
.toMatchTextContent('End waiting')
|
||||
})
|
||||
|
||||
it('creates an output token from the task editor and retains its output definition', async () => {
|
||||
const { panel } = await setup()
|
||||
await replaceTask(panel.getByRole('textbox', { name: 'Agent task', exact: true }), 'Return ')
|
||||
await userEvent.keyboard('/')
|
||||
await page
|
||||
.getByRole('listbox', { name: 'Typeahead menu', exact: true })
|
||||
.getByRole('button', { name: 'New output', exact: true })
|
||||
.click()
|
||||
const name = page.getByRole('textbox', { name: 'Field name', exact: true })
|
||||
await expect.element(name).toHaveFocus()
|
||||
await name.fill('review_notes')
|
||||
await userEvent.keyboard('{Enter}')
|
||||
await expect.element(name).not.toBeInTheDocument()
|
||||
await expect
|
||||
.element(panel.getByRole('textbox', { name: 'Agent task', exact: true }))
|
||||
.toMatchTextContent(/review_notes/)
|
||||
const reopened = await closeAndReopen(panel)
|
||||
await expect
|
||||
.element(reopened.getByRole('textbox', { name: 'Agent task', exact: true }))
|
||||
.toMatchTextContent(/review_notes/)
|
||||
await expect
|
||||
.element(reopened.getByRole('button', { name: 'Delete review_notes', exact: true }))
|
||||
.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('allows the default text output to be edited and deleted while retaining a newly added output', async () => {
|
||||
const { panel } = await setup()
|
||||
await expect.element(panel.getByText('Text returned by the agent', { exact: true })).toBeVisible()
|
||||
await panel.getByText('text', { exact: true }).last().hover()
|
||||
await panel.getByRole('button', { name: 'Edit text', exact: true }).click()
|
||||
await panel
|
||||
.getByRole('form', { name: 'Output variable editor', exact: true })
|
||||
.getByRole('textbox', { name: 'Description', exact: true })
|
||||
.fill('Contract review result')
|
||||
await panel.getByRole('button', { name: 'Confirm', exact: true }).click()
|
||||
await expect.element(panel.getByText('Contract review result', { exact: true })).toBeVisible()
|
||||
await panel.getByText('text', { exact: true }).last().hover()
|
||||
await panel.getByRole('button', { name: 'Edit text', exact: true }).click()
|
||||
const name = panel.getByRole('textbox', { name: 'Field name', exact: true })
|
||||
await name.fill('summary')
|
||||
await panel.getByRole('button', { name: 'Confirm', exact: true }).click()
|
||||
await expect.element(panel.getByText('summary', { exact: true })).toBeVisible()
|
||||
await panel.getByRole('button', { name: 'New output', exact: true }).click()
|
||||
await name.fill('review_notes')
|
||||
await panel.getByRole('button', { name: 'Confirm', exact: true }).click()
|
||||
await panel.getByText('summary', { exact: true }).hover()
|
||||
await panel.getByRole('button', { name: 'Delete summary', exact: true }).click()
|
||||
await expect.element(panel.getByText('summary', { exact: true })).not.toBeInTheDocument()
|
||||
const reopened = await closeAndReopen(panel)
|
||||
await expect.element(reopened.getByText('review_notes', { exact: true })).toBeVisible()
|
||||
await expect.element(reopened.getByText('text', { exact: true })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the empty Last Run tab without requesting an execution record', async () => {
|
||||
const { panel } = await setup()
|
||||
await panel.getByRole('tab', { name: 'Last Run', exact: true }).click()
|
||||
await expect
|
||||
.element(panel.getByText('The results of the last run will be displayed here', { exact: true }))
|
||||
.toBeVisible()
|
||||
await panel.getByRole('tab', { name: 'Settings', exact: true }).click()
|
||||
await expect
|
||||
.element(panel.getByRole('textbox', { name: 'Agent task', exact: true }))
|
||||
.toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { ExternalAgentWorkflowPreviewAgent } from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useId, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { SearchInput } from '@/app/components/base/search-input'
|
||||
import { externalWorkflowAgentExamples } from '@/app/components/workflow/block-selector/workflow-agent-selector-preview'
|
||||
import { ExternalAgentBadge } from './external-badge'
|
||||
|
||||
export function ExternalAgentField({
|
||||
agent,
|
||||
onChange,
|
||||
}: {
|
||||
agent: ExternalAgentWorkflowPreviewAgent
|
||||
onChange: (agent: ExternalAgentWorkflowPreviewAgent) => void
|
||||
}) {
|
||||
const { t } = useTranslation(['workflow', 'workflowAgent', 'agentRoster'])
|
||||
const labelId = useId()
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<div role="group" aria-labelledby={labelId} className="px-4 pt-4 pb-2">
|
||||
<div className="mb-1 flex h-6 items-center justify-between gap-2">
|
||||
<span id={labelId} className="system-sm-semibold-uppercase text-text-secondary">
|
||||
{t(($) => $['nodes.agent.roster.label'], { ns: 'workflow' })}
|
||||
</span>
|
||||
<PopoverTrigger
|
||||
render={<Button variant="ghost" size="small" className="text-text-tertiary" />}
|
||||
>
|
||||
{t(($) => $['nodes.agent.roster.change'], { ns: 'workflowAgent' })}
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-full justify-start gap-1 rounded-lg bg-components-input-bg-normal p-1 text-start hover:bg-state-base-hover-alt"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className="flex size-6 shrink-0 items-center justify-center">
|
||||
<AppIcon
|
||||
size="tiny"
|
||||
className="size-5 rounded-md"
|
||||
icon={agent.icon}
|
||||
fallback={{
|
||||
content: (
|
||||
<span aria-hidden className="i-ri-robot-2-line size-3.5 text-text-accent" />
|
||||
),
|
||||
background: 'var(--color-background-default)',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="truncate system-sm-regular text-components-input-text-filled">
|
||||
{agent.name}
|
||||
</span>
|
||||
<ExternalAgentBadge />
|
||||
<span
|
||||
aria-hidden
|
||||
className="ms-auto me-1 i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
<PopoverContent align="start" className="w-100 max-w-[calc(100vw-2rem)] p-1">
|
||||
<PopoverTitle className="sr-only">
|
||||
{t(($) => $['roster.nodeSelector.dialogLabel'], { ns: 'agentRoster' })}
|
||||
</PopoverTitle>
|
||||
<ExternalAgentOptions
|
||||
onSelect={(next) => {
|
||||
onChange(next)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function ExternalAgentOptions({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (agent: ExternalAgentWorkflowPreviewAgent) => void
|
||||
}) {
|
||||
const { t } = useTranslation(['agentRoster'])
|
||||
const [search, setSearch] = useState('')
|
||||
const agents = externalWorkflowAgentExamples.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(search.trim().toLowerCase()),
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<div className="p-1">
|
||||
<SearchInput
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
aria-label={t(($) => $['roster.searchLabel'])}
|
||||
placeholder={t(($) => $['roster.nodeSelector.searchAgentsPlaceholder'])}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-h-60 flex-col overflow-y-auto p-1">
|
||||
{agents.map((agent) => (
|
||||
<Button
|
||||
key={agent.key}
|
||||
variant="ghost"
|
||||
disabled={!agent.enabled}
|
||||
className="h-8 w-full shrink-0 justify-start gap-2"
|
||||
onClick={() => onSelect(agent)}
|
||||
>
|
||||
<AppIcon size="tiny" icon={agent.icon} />
|
||||
<span className="truncate system-sm-medium">{agent.name}</span>
|
||||
<span className="system-xs-regular text-text-quaternary">
|
||||
{agent.enabled
|
||||
? t(($) => $['roster.nodeSelector.source.external'])
|
||||
: t(($) => $['external.filters.disabled'])}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
{agents.length === 0 && (
|
||||
<p role="status" className="p-3 system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['roster.emptySearch'])}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function ExternalAgentBadge() {
|
||||
const { t } = useTranslation(['agentRoster'])
|
||||
return (
|
||||
<span className="shrink-0 rounded-[5px] border border-divider-deep px-1.25 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t(($) => $['roster.nodeSelector.source.external'])}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldGroup,
|
||||
NumberFieldInput,
|
||||
NumberFieldUnit,
|
||||
} from '@langgenius/dify-ui/number-field'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import {
|
||||
Slider,
|
||||
SliderControl,
|
||||
SliderIndicator,
|
||||
SliderLabel,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
} from '@langgenius/dify-ui/slider'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useId, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Collapse,
|
||||
CollapseActions,
|
||||
CollapseContent,
|
||||
CollapseHeader,
|
||||
CollapseIndicator,
|
||||
CollapseTitle,
|
||||
CollapseTrigger,
|
||||
} from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import ErrorHandleTypeSelector from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector'
|
||||
import FailBranchCard from '@/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
type ExternalAgentWorkflowSettingsProps = {
|
||||
waitLimitEnabled: boolean
|
||||
waitLimitMinutes: number
|
||||
onWaitLimitEnabledChange: (enabled: boolean) => void
|
||||
onWaitLimitMinutesChange: (minutes: number) => void
|
||||
errorStrategy: ErrorHandleTypeEnum
|
||||
onErrorStrategyChange: (strategy: ErrorHandleTypeEnum) => void
|
||||
}
|
||||
|
||||
export function ExternalAgentWorkflowSettings({
|
||||
waitLimitEnabled,
|
||||
waitLimitMinutes,
|
||||
onWaitLimitEnabledChange,
|
||||
onWaitLimitMinutesChange,
|
||||
errorStrategy,
|
||||
onErrorStrategyChange,
|
||||
}: ExternalAgentWorkflowSettingsProps) {
|
||||
const { t } = useTranslation(['agentRoster', 'workflow'])
|
||||
const waitLimitTitleId = useId()
|
||||
const waitLimitLabelId = useId()
|
||||
const timeoutHelpId = useId()
|
||||
const errorHandlingTitleId = useId()
|
||||
const [errorHandlingCollapsed, setErrorHandlingCollapsed] = useState(false)
|
||||
const waitLimitLabel = t(($) => $['external.workflow.waitLimit.label'])
|
||||
const waitLimitHelp = t(($) => $['external.workflow.waitLimit.help'])
|
||||
const hasErrorStrategy = errorStrategy !== ErrorHandleTypeEnum.none
|
||||
|
||||
const changeWaitLimit = (minutes: number | null) => {
|
||||
if (minutes !== null && Number.isFinite(minutes) && minutes > 0)
|
||||
onWaitLimitMinutesChange(minutes)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="px-4 py-4">
|
||||
<Field name="external_agent_wait_limit_enabled" className="gap-0">
|
||||
<div className="flex min-h-6 items-center gap-1">
|
||||
<FieldLabel
|
||||
id={waitLimitTitleId}
|
||||
className="min-w-0 py-1 system-sm-semibold-uppercase text-text-secondary"
|
||||
>
|
||||
{t(($) => $['external.workflow.waitLimit.title'])}
|
||||
</FieldLabel>
|
||||
<Infotip>
|
||||
<InfotipTrigger aria-labelledby={waitLimitTitleId} />
|
||||
<InfotipContent aria-labelledby={waitLimitTitleId}>{waitLimitHelp}</InfotipContent>
|
||||
</Infotip>
|
||||
<Switch
|
||||
aria-labelledby={waitLimitTitleId}
|
||||
checked={waitLimitEnabled}
|
||||
onCheckedChange={onWaitLimitEnabledChange}
|
||||
className="ms-auto"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
{waitLimitEnabled && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex min-h-8 items-center justify-between gap-3">
|
||||
<span
|
||||
id={waitLimitLabelId}
|
||||
className="system-xs-regular text-text-tertiary uppercase"
|
||||
>
|
||||
{waitLimitLabel}
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<Slider<number>
|
||||
value={waitLimitMinutes}
|
||||
min={Math.min(1, waitLimitMinutes)}
|
||||
max={Math.max(30, waitLimitMinutes)}
|
||||
step={1}
|
||||
onValueChange={changeWaitLimit}
|
||||
className="w-27"
|
||||
>
|
||||
<SliderLabel className="sr-only">{waitLimitLabel}</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb />
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
</Slider>
|
||||
<Field name="external_agent_wait_limit_minutes" className="w-20 gap-0">
|
||||
<FieldLabel className="sr-only">{waitLimitLabel}</FieldLabel>
|
||||
<NumberField
|
||||
value={waitLimitMinutes}
|
||||
min={0}
|
||||
step={1}
|
||||
format={{ useGrouping: false }}
|
||||
onValueChange={changeWaitLimit}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput aria-labelledby={waitLimitLabelId} className="ps-2 pe-1" />
|
||||
<NumberFieldUnit>
|
||||
{t(($) => $['external.workflow.waitLimit.unit'])}
|
||||
</NumberFieldUnit>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<Select<'end-waiting'> value="end-waiting">
|
||||
<div className="flex min-h-8 items-center justify-between gap-3">
|
||||
<SelectLabel className="py-0 system-xs-regular text-text-tertiary uppercase">
|
||||
{t(($) => $['external.workflow.waitLimit.onTimeout'])}
|
||||
</SelectLabel>
|
||||
<SelectTrigger aria-describedby={timeoutHelpId} className="w-50 shrink-0">
|
||||
{t(($) => $['external.workflow.waitLimit.endWaiting'])}
|
||||
</SelectTrigger>
|
||||
</div>
|
||||
<SelectContent>
|
||||
<SelectItem value="end-waiting">
|
||||
<SelectItemText>
|
||||
{t(($) => $['external.workflow.waitLimit.endWaiting'])}
|
||||
</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p id={timeoutHelpId} className="system-xs-regular text-text-tertiary">
|
||||
{waitLimitHelp}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<Separator decorative className="my-0 bg-divider-subtle" />
|
||||
<section className="py-4">
|
||||
<Collapse
|
||||
collapsed={errorHandlingCollapsed}
|
||||
onCollapse={setErrorHandlingCollapsed}
|
||||
disabled={!hasErrorStrategy}
|
||||
>
|
||||
<CollapseHeader>
|
||||
<CollapseTitle className="ms-4">
|
||||
<span id={errorHandlingTitleId}>
|
||||
{t(($) => $['nodes.common.errorHandle.title'], { ns: 'workflow' })}
|
||||
</span>
|
||||
</CollapseTitle>
|
||||
<Infotip>
|
||||
<InfotipTrigger aria-labelledby={errorHandlingTitleId} />
|
||||
<InfotipContent aria-labelledby={errorHandlingTitleId}>
|
||||
{t(($) => $['nodes.common.errorHandle.tip'], { ns: 'workflow' })}
|
||||
</InfotipContent>
|
||||
</Infotip>
|
||||
{hasErrorStrategy && (
|
||||
<CollapseTrigger
|
||||
aria-labelledby={errorHandlingTitleId}
|
||||
className="ms-0 size-6 justify-center"
|
||||
>
|
||||
<CollapseIndicator />
|
||||
</CollapseTrigger>
|
||||
)}
|
||||
<CollapseActions>
|
||||
<div className="pe-4">
|
||||
<ErrorHandleTypeSelector
|
||||
value={errorStrategy}
|
||||
onSelected={(strategy) => {
|
||||
setErrorHandlingCollapsed(false)
|
||||
onErrorStrategyChange(strategy)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CollapseActions>
|
||||
</CollapseHeader>
|
||||
<CollapseContent>
|
||||
{errorStrategy === ErrorHandleTypeEnum.failBranch && (
|
||||
<FailBranchCard learnMorePlacement="below" />
|
||||
)}
|
||||
{errorStrategy === ErrorHandleTypeEnum.defaultValue && (
|
||||
<p className="px-4 pt-2 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['nodes.common.errorHandle.defaultValue.desc'], { ns: 'workflow' })}
|
||||
</p>
|
||||
)}
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ExternalAgentWorkflowPreviewNodeData } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { ExternalAgentBadge } from './external-badge'
|
||||
|
||||
export function ExternalAgentWorkflowPreviewNode({
|
||||
data,
|
||||
}: {
|
||||
data: ExternalAgentWorkflowPreviewNodeData
|
||||
}) {
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const agent = data._externalAgentPreview
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 px-2.5 py-1">
|
||||
<div className="py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t(($) => $['nodes.agent.roster.label'])}
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 rounded-lg bg-workflow-block-parma-bg p-1">
|
||||
<AppIcon
|
||||
size="small"
|
||||
icon={agent.icon}
|
||||
fallback={{
|
||||
content: <span aria-hidden className="i-ri-robot-2-line size-4 text-text-tertiary" />,
|
||||
background: 'var(--color-components-icon-bg-midnight-soft)',
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="truncate system-xs-regular text-text-secondary">{agent.name}</span>
|
||||
<ExternalAgentBadge />
|
||||
</div>
|
||||
{agent.role && (
|
||||
<div className="truncate system-2xs-regular text-text-tertiary">{agent.role}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import type { ExternalAgentWorkflowPreviewNodeData } from './types'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { Menu, MenuContent, MenuItem, MenuTrigger } from '@langgenius/dify-ui/menu'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useCallback, useId, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import { TitleInput } from '@/app/components/workflow/nodes/_base/components/title-description-input'
|
||||
import NoLastRunData from '@/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data'
|
||||
import { AgentOutputVariables } from '@/app/components/workflow/nodes/agent-v2/components/agent-output-variables'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { ExternalAgentField } from './agent-field'
|
||||
import { ExternalAgentWorkflowSettings } from './external-settings'
|
||||
import { externalAgentDefaultOutputs, externalAgentExampleTask } from './preview'
|
||||
import { ExternalAgentTaskField } from './task-field'
|
||||
import { isExternalAgentWorkflowPreviewData } from './types'
|
||||
|
||||
const externalReservedOutputNames: ReadonlySet<string> = new Set()
|
||||
|
||||
type PreviewUpdate =
|
||||
| Partial<ExternalAgentWorkflowPreviewNodeData>
|
||||
| ((
|
||||
current: ExternalAgentWorkflowPreviewNodeData,
|
||||
) => Partial<ExternalAgentWorkflowPreviewNodeData>)
|
||||
|
||||
export function ExternalAgentWorkflowPreviewPanel({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: string
|
||||
data: ExternalAgentWorkflowPreviewNodeData
|
||||
}) {
|
||||
const { t } = useTranslation(['agentRoster', 'common', 'workflow', 'workflowDebug'])
|
||||
const { setNodes, setEdges } = useReactFlow<CommonNodeType>()
|
||||
const [tab, setTab] = useState<'settings' | 'lastRun'>('settings')
|
||||
const [outputsCollapsed, setOutputsCollapsed] = useState(false)
|
||||
const titleContainerRef = useRef<HTMLDivElement>(null)
|
||||
const renameRequestedRef = useRef(false)
|
||||
const descriptionId = useId()
|
||||
const agent = data._externalAgentPreview
|
||||
const task = data.agent_task ?? externalAgentExampleTask
|
||||
const outputs = data.agent_declared_outputs ?? externalAgentDefaultOutputs
|
||||
const waitLimit = data._externalAgentWaitLimit ?? { enabled: true, minutes: 10 }
|
||||
const update = useCallback(
|
||||
(patch: PreviewUpdate) => {
|
||||
// Preview configuration belongs to this local canvas node. It does not enter draft sync
|
||||
// or the collaboration writer; those boundaries also exclude temporary external nodes.
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== id || !isExternalAgentWorkflowPreviewData(node.data)) return node
|
||||
const changes = typeof patch === 'function' ? patch(node.data) : patch
|
||||
return {
|
||||
...node,
|
||||
...(typeof changes.selected === 'boolean' ? { selected: changes.selected } : {}),
|
||||
data: { ...node.data, ...changes },
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
[id, setNodes],
|
||||
)
|
||||
const close = () => update({ selected: false })
|
||||
const remove = () => {
|
||||
setNodes((nodes) => nodes.filter((node) => node.id !== id))
|
||||
setEdges((edges) => edges.filter((edge) => edge.source !== id && edge.target !== id))
|
||||
}
|
||||
const runLabel = t(($) => $['panel.runThisStep'], { ns: 'workflow' })
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={agent.name}
|
||||
className="me-1 flex h-full w-100 max-w-full shrink-0 flex-col overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg"
|
||||
>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'settings' || value === 'lastRun') setTab(value)
|
||||
}}
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
<header className="shrink-0 border-b border-divider-subtle px-4 pt-4">
|
||||
<div className="flex h-7 items-center gap-1">
|
||||
<BlockIcon type={BlockEnum.AgentV2} size="md" />
|
||||
<div ref={titleContainerRef} className="min-w-0 flex-1">
|
||||
<TitleInput value={data.title || agent.name} onBlur={(title) => update({ title })} />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<IconButton aria-label={runLabel} disabled focusableWhenDisabled>
|
||||
<span aria-hidden className="i-ri-play-line size-4" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{t(($) => $['external.workflow.runUnavailable'])}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
onOpenChange={(open) => {
|
||||
if (open) renameRequestedRef.current = false
|
||||
}}
|
||||
>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<IconButton aria-label={t(($) => $['operation.more'], { ns: 'common' })}>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<MenuContent
|
||||
finalFocus={() =>
|
||||
renameRequestedRef.current
|
||||
? (titleContainerRef.current?.querySelector('input') ?? true)
|
||||
: true
|
||||
}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
renameRequestedRef.current = true
|
||||
}}
|
||||
>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4" />
|
||||
{t(($) => $['operation.rename'], { ns: 'common' })}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={remove} className="text-text-destructive">
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4" />
|
||||
{t(($) => $['operation.delete'], { ns: 'common' })}
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
<span aria-hidden className="mx-2 h-3.5 w-px bg-divider-subtle" />
|
||||
<IconButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
onClick={close}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<label htmlFor={descriptionId} className="sr-only">
|
||||
{t(($) => $['common.nodeDescription'], { ns: 'workflow' })}
|
||||
</label>
|
||||
<textarea
|
||||
id={descriptionId}
|
||||
rows={1}
|
||||
value={data.desc || ''}
|
||||
onChange={(event) => update({ desc: event.target.value })}
|
||||
placeholder={t(($) => $['common.addDescription'], { ns: 'workflow' })}
|
||||
className="mt-1 block h-5 w-full resize-none rounded-sm bg-transparent py-0.5 system-xs-regular text-text-secondary outline-hidden placeholder:text-text-quaternary focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
/>
|
||||
<TabsList aria-label={t(($) => $['external.overview.type'])} className="mt-0.5 h-8 gap-4">
|
||||
<TabsTab
|
||||
value="settings"
|
||||
className="border-b-2 py-1 system-sm-medium uppercase data-active:text-text-primary"
|
||||
>
|
||||
{t(($) => $['debug.settingsTab'], { ns: 'workflowDebug' })}
|
||||
</TabsTab>
|
||||
<TabsTab value="lastRun" className="border-b-2 py-1 system-sm-medium uppercase">
|
||||
{t(($) => $['debug.lastRunTab'], { ns: 'workflowDebug' })}
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
</header>
|
||||
<TabsPanel value="settings" className="min-h-0 flex-1 overflow-y-auto pb-4">
|
||||
<ExternalAgentField
|
||||
agent={agent}
|
||||
onChange={(next) =>
|
||||
update((current) => ({
|
||||
_externalAgentPreview: next,
|
||||
title:
|
||||
current.title === current._externalAgentPreview.name ? next.name : current.title,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ExternalAgentTaskField
|
||||
id={id}
|
||||
value={task}
|
||||
outputs={outputs}
|
||||
onChange={(agent_task) => update({ agent_task })}
|
||||
onOutputsChange={(agent_declared_outputs, agent_task) =>
|
||||
update({
|
||||
agent_declared_outputs,
|
||||
...(agent_task === undefined ? {} : { agent_task }),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="border-b border-divider-subtle">
|
||||
<AgentOutputVariables
|
||||
compact
|
||||
reservedOutputNames={externalReservedOutputNames}
|
||||
outputs={outputs}
|
||||
collapsed={outputsCollapsed}
|
||||
onCollapse={setOutputsCollapsed}
|
||||
onChange={(agent_declared_outputs) => update({ agent_declared_outputs })}
|
||||
/>
|
||||
</div>
|
||||
<ExternalAgentWorkflowSettings
|
||||
waitLimitEnabled={waitLimit.enabled}
|
||||
waitLimitMinutes={waitLimit.minutes}
|
||||
onWaitLimitEnabledChange={(enabled) =>
|
||||
update((current) => ({
|
||||
_externalAgentWaitLimit: {
|
||||
...(current._externalAgentWaitLimit ?? waitLimit),
|
||||
enabled,
|
||||
},
|
||||
}))
|
||||
}
|
||||
onWaitLimitMinutesChange={(minutes) =>
|
||||
update((current) => ({
|
||||
_externalAgentWaitLimit: {
|
||||
...(current._externalAgentWaitLimit ?? waitLimit),
|
||||
minutes,
|
||||
},
|
||||
}))
|
||||
}
|
||||
errorStrategy={data.error_strategy ?? ErrorHandleTypeEnum.failBranch}
|
||||
onErrorStrategyChange={(error_strategy) => update({ error_strategy })}
|
||||
/>
|
||||
</TabsPanel>
|
||||
<TabsPanel value="lastRun" className="flex min-h-0 flex-1 flex-col">
|
||||
<NoLastRunData canSingleRun={false} onSingleRun={() => {}} />
|
||||
</TabsPanel>
|
||||
</Tabs>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
ExternalAgentWorkflowPreviewAgent,
|
||||
ExternalAgentWorkflowPreviewDefaultValue,
|
||||
} from './types'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
export const externalAgentExampleTask =
|
||||
'Review this contract against the company’s payment policy. Identify risks and suggest safer wording.\nPolicy: {{#policy.result#}}\nContract: {{#input.contract_text#}}'
|
||||
|
||||
export const externalAgentDefaultOutputs = [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'string' as const,
|
||||
required: false,
|
||||
description: 'Text returned by the agent',
|
||||
},
|
||||
]
|
||||
|
||||
// The external-agent workflow contract is not available yet. Keep this selection on the
|
||||
// current canvas only; draft serialization excludes temporary nodes and their edges.
|
||||
export function createExternalAgentWorkflowPreviewData(
|
||||
agent: ExternalAgentWorkflowPreviewAgent,
|
||||
): ExternalAgentWorkflowPreviewDefaultValue {
|
||||
return {
|
||||
title: agent.name,
|
||||
agent_node_kind: 'external_agent',
|
||||
version: '2',
|
||||
agent_binding: undefined,
|
||||
_isTempNode: true,
|
||||
_externalAgentPreview: { ...agent },
|
||||
agent_task: externalAgentExampleTask,
|
||||
agent_declared_outputs: externalAgentDefaultOutputs.map((output) => ({ ...output })),
|
||||
_externalAgentWaitLimit: { enabled: true, minutes: 10 },
|
||||
error_strategy: ErrorHandleTypeEnum.failBranch,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { WorkflowNodesMap } from '@/app/components/base/prompt-editor/types'
|
||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { useId, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { $createCustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import { useWorkflowVariableType } from '@/app/components/workflow/hooks/use-workflow-variables'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
const exampleVariables: NodeOutPutVar[] = [
|
||||
{
|
||||
nodeId: 'policy',
|
||||
title: 'Knowledge Retrieval',
|
||||
vars: [{ variable: 'result', type: VarType.string }],
|
||||
},
|
||||
{
|
||||
nodeId: 'input',
|
||||
title: 'User Input',
|
||||
vars: [{ variable: 'contract_text', type: VarType.string }],
|
||||
},
|
||||
]
|
||||
const exampleNodes: WorkflowNodesMap = {
|
||||
policy: { title: 'Knowledge Retrieval', type: BlockEnum.KnowledgeRetrieval },
|
||||
input: { title: 'User Input', type: BlockEnum.Start },
|
||||
}
|
||||
|
||||
function TaskToolbar({ value, focused }: { value: string; focused: boolean }) {
|
||||
const { t } = useTranslation(['workflowAgent', 'agentRoster'])
|
||||
const [editor] = useLexicalComposerContext()
|
||||
return (
|
||||
<div className="flex h-8 shrink-0 items-center justify-between px-3">
|
||||
{focused ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded-sm system-xs-medium text-text-tertiary focus-visible:outline-2 focus-visible:outline-state-accent-solid"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
editor.focus()
|
||||
editor.update(() => $insertNodes([$createCustomTextNode('/')]))
|
||||
}}
|
||||
>
|
||||
<span aria-hidden className="i-ri-slash-commands-2 size-3.5" />
|
||||
{t(($) => $['nodes.agent.task.insert'], { ns: 'workflowAgent' })}
|
||||
</button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<span className="rounded-[5px] border border-divider-deep px-1 system-2xs-medium text-text-tertiary">
|
||||
{t(($) => $['external.workflow.task.characterCount'], {
|
||||
ns: 'agentRoster',
|
||||
count: value.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ExternalAgentTaskField({
|
||||
id,
|
||||
value,
|
||||
outputs,
|
||||
onChange,
|
||||
onOutputsChange,
|
||||
}: {
|
||||
id: string
|
||||
value: string
|
||||
outputs: DeclaredOutputConfig[]
|
||||
onChange: (value: string) => void
|
||||
onOutputsChange: (outputs: DeclaredOutputConfig[], task?: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation(['workflowAgent'])
|
||||
const labelId = useId()
|
||||
const [focused, setFocused] = useState(false)
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(id)
|
||||
const getWorkflowVarType = useWorkflowVariableType()
|
||||
const missingExamples = exampleVariables.filter(
|
||||
(example) => !availableVars.some((item) => item.nodeId === example.nodeId),
|
||||
)
|
||||
const variables = [...availableVars, ...missingExamples]
|
||||
const workflowNodesMap = { ...exampleNodes }
|
||||
for (const node of availableNodesWithParent)
|
||||
workflowNodesMap[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width ?? undefined,
|
||||
height: node.height ?? undefined,
|
||||
position: node.position,
|
||||
}
|
||||
return (
|
||||
<Field name="agent_task" className="gap-1 px-4 py-2">
|
||||
<div className="flex h-6 items-center gap-1">
|
||||
<FieldLabel id={labelId} className="system-sm-semibold-uppercase! text-text-secondary">
|
||||
{t(($) => $['nodes.agent.task.label'])}
|
||||
</FieldLabel>
|
||||
<Infotip>
|
||||
<InfotipTrigger aria-labelledby={labelId} />
|
||||
<InfotipContent aria-labelledby={labelId}>
|
||||
{t(($) => $['nodes.agent.task.tooltip'])}
|
||||
</InfotipContent>
|
||||
</Infotip>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'h-30 rounded-[10px] p-0.5',
|
||||
focused
|
||||
? 'bg-linear-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2'
|
||||
: 'bg-transparent',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col rounded-lg',
|
||||
focused ? 'bg-background-default' : 'bg-components-input-bg-normal',
|
||||
)}
|
||||
>
|
||||
<PromptEditor
|
||||
aria-label={t(($) => $['nodes.agent.task.label'])}
|
||||
wrapperClassName="flex h-full flex-col"
|
||||
className="min-h-0 flex-1 overflow-y-auto px-3 py-2"
|
||||
placeholderClassName="px-3 py-2"
|
||||
placeholder={t(($) => $['nodes.agent.task.placeholder'])}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
compact
|
||||
editable
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables,
|
||||
workflowNodesMap,
|
||||
getVarType: (payload) =>
|
||||
missingExamples.some((example) => example.nodeId === payload.nodeId)
|
||||
? Type.string
|
||||
: getWorkflowVarType(payload),
|
||||
}}
|
||||
isSupportFileVar
|
||||
agentOutputBlock={{ show: true, outputs, onChange: onOutputsChange }}
|
||||
>
|
||||
<TaskToolbar value={value} focused={focused} />
|
||||
</PromptEditor>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import type { ExternalAgentListItem } from '@/features/agent-v2/roster/components/external-agent-list'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export type ExternalAgentWorkflowPreviewAgent = Pick<
|
||||
ExternalAgentListItem,
|
||||
'key' | 'name' | 'role' | 'description' | 'icon' | 'enabled'
|
||||
>
|
||||
|
||||
export type ExternalAgentWorkflowPreviewDefaultValue = {
|
||||
title?: string
|
||||
agent_node_kind: 'external_agent'
|
||||
version: '2'
|
||||
agent_binding: undefined
|
||||
_isTempNode: true
|
||||
_externalAgentPreview: ExternalAgentWorkflowPreviewAgent
|
||||
agent_task?: string
|
||||
agent_declared_outputs?: DeclaredOutputConfig[]
|
||||
_externalAgentWaitLimit?: { enabled: boolean; minutes: number }
|
||||
error_strategy?: CommonNodeType['error_strategy']
|
||||
}
|
||||
|
||||
export type ExternalAgentWorkflowPreviewNodeData = CommonNodeType &
|
||||
ExternalAgentWorkflowPreviewDefaultValue
|
||||
|
||||
export function isExternalAgentWorkflowPreviewData(
|
||||
data: CommonNodeType,
|
||||
): data is ExternalAgentWorkflowPreviewNodeData {
|
||||
const preview = data as Partial<ExternalAgentWorkflowPreviewNodeData>
|
||||
return (
|
||||
(data.type === BlockEnum.Agent || data.type === BlockEnum.AgentV2) &&
|
||||
preview.agent_node_kind === 'external_agent' &&
|
||||
preview.version === '2' &&
|
||||
preview._isTempNode === true &&
|
||||
!!preview._externalAgentPreview
|
||||
)
|
||||
}
|
||||
@@ -152,6 +152,15 @@
|
||||
"external.references": "التطبيقات التي تستخدم {{name}}",
|
||||
"external.searchPlaceholder": "البحث عن وكلاء…",
|
||||
"external.updated": "تم التحديث {{time}}",
|
||||
"external.workflow.runUnavailable": "تشغيلات الوكيل الخارجي غير متاحة بعد.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} حرف",
|
||||
"external.workflow.task.characterCount_other": "{{count}} حرفًا",
|
||||
"external.workflow.waitLimit.endWaiting": "إنهاء الانتظار",
|
||||
"external.workflow.waitLimit.help": "طبّق معالجة الأخطاء المكوّنة. قد تستمر مهمة الوكيل في العمل.",
|
||||
"external.workflow.waitLimit.label": "حد الانتظار",
|
||||
"external.workflow.waitLimit.onTimeout": "عند انتهاء المهلة",
|
||||
"external.workflow.waitLimit.title": "حد انتظار الوكيل الخارجي",
|
||||
"external.workflow.waitLimit.unit": "دقيقة",
|
||||
"roster.createAgent": "إنشاء وكيل",
|
||||
"roster.createDialog.description": "أنشئ Agent قابلاً لإعادة الاستخدام في مساحة العمل هذه.",
|
||||
"roster.createDialog.title": "إنشاء وكيل",
|
||||
@@ -190,8 +199,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "فشل إنشاء وكيل inline. قم بالتحديث وحاول مرة أخرى.",
|
||||
"roster.nodeSelector.dialogLabel": "اختر وكيلاً",
|
||||
"roster.nodeSelector.manageInAgentConsole": "إدارة في Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "البحث عن الوكلاء…",
|
||||
"roster.nodeSelector.searchPlaceholder": "بحث…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "خارجي",
|
||||
"roster.nodeSelector.startFromScratch": "البدء من الصفر",
|
||||
"roster.nodeSelector.tabs.all": "الكل",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "خارجي",
|
||||
"roster.references.label": "سير العمل الذي يستخدم {{name}}",
|
||||
"roster.references.trigger": "سير العمل الذي يستخدم {{name}}",
|
||||
"roster.saveToRoster": "حفظ إلى Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "{{name}} istifadə edən tətbiqlər",
|
||||
"external.searchPlaceholder": "Agentləri axtar…",
|
||||
"external.updated": "Yenilənib: {{time}}",
|
||||
"external.workflow.runUnavailable": "Xarici agent işə salınmaları hələ əlçatan deyil.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} simvol",
|
||||
"external.workflow.task.characterCount_other": "{{count}} simvol",
|
||||
"external.workflow.waitLimit.endWaiting": "Gözləməni bitir",
|
||||
"external.workflow.waitLimit.help": "Konfiqurasiya edilmiş xəta emalını tətbiq edin. Agentin tapşırığı icra olunmağa davam edə bilər.",
|
||||
"external.workflow.waitLimit.label": "Gözləmə limiti",
|
||||
"external.workflow.waitLimit.onTimeout": "Vaxt bitdikdə",
|
||||
"external.workflow.waitLimit.title": "Xarici agent gözləmə limiti",
|
||||
"external.workflow.waitLimit.unit": "dəq",
|
||||
"roster.createAgent": "Agent yarat",
|
||||
"roster.createDialog.description": "Bu iş sahəsində təkrar istifadə edilə bilən agent yaradın.",
|
||||
"roster.createDialog.title": "Agentin yaradılması",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Daxili agent yaratmaq mümkün olmadı. Səhifəni yeniləyin və yenidən cəhd edin.",
|
||||
"roster.nodeSelector.dialogLabel": "Agent seçimi",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Agentlər bölməsində idarə et",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Agentləri axtarın…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Axtar…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Xarici",
|
||||
"roster.nodeSelector.startFromScratch": "Sıfırdan başla",
|
||||
"roster.nodeSelector.tabs.all": "Hamısı",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Xarici",
|
||||
"roster.references.label": "{{name}} agentindən istifadə edən iş axınları",
|
||||
"roster.references.trigger": "{{name}} agentindən istifadə edən iş axınları",
|
||||
"roster.saveToRoster": "Agentlər bölməsində yadda saxla",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "Apps, die {{name}} verwenden",
|
||||
"external.searchPlaceholder": "Agenten suchen…",
|
||||
"external.updated": "Aktualisiert {{time}}",
|
||||
"external.workflow.runUnavailable": "Ausführungen externer Agenten sind noch nicht verfügbar.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} Zeichen",
|
||||
"external.workflow.task.characterCount_other": "{{count}} Zeichen",
|
||||
"external.workflow.waitLimit.endWaiting": "Warten beenden",
|
||||
"external.workflow.waitLimit.help": "Wenden Sie die konfigurierte Fehlerbehandlung an. Die Aufgabe des Agenten kann weiter ausgeführt werden.",
|
||||
"external.workflow.waitLimit.label": "Wartezeitlimit",
|
||||
"external.workflow.waitLimit.onTimeout": "Bei Zeitüberschreitung",
|
||||
"external.workflow.waitLimit.title": "Wartezeitlimit für externen Agenten",
|
||||
"external.workflow.waitLimit.unit": "Min.",
|
||||
"roster.createAgent": "Agent erstellen",
|
||||
"roster.createDialog.description": "Erstellen Sie einen wiederverwendbaren Agent in diesem Workspace.",
|
||||
"roster.createDialog.title": "Agent erstellen",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Inline-Agent konnte nicht erstellt werden. Bitte aktualisieren und erneut versuchen.",
|
||||
"roster.nodeSelector.dialogLabel": "Agent auswählen",
|
||||
"roster.nodeSelector.manageInAgentConsole": "In Agents verwalten",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Agenten suchen…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Suchen…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Extern",
|
||||
"roster.nodeSelector.startFromScratch": "Von Grund auf starten",
|
||||
"roster.nodeSelector.tabs.all": "Alle",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Extern",
|
||||
"roster.references.label": "Workflows, die {{name}} verwenden",
|
||||
"roster.references.trigger": "Workflows, die {{name}} verwenden",
|
||||
"roster.saveToRoster": "In Agents speichern",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "Apps using {{name}}",
|
||||
"external.searchPlaceholder": "Search agents…",
|
||||
"external.updated": "Updated {{time}}",
|
||||
"external.workflow.runUnavailable": "External agent runs are not available yet.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} character",
|
||||
"external.workflow.task.characterCount_other": "{{count}} characters",
|
||||
"external.workflow.waitLimit.endWaiting": "End waiting",
|
||||
"external.workflow.waitLimit.help": "Apply the configured error handling. The agent’s task may continue running.",
|
||||
"external.workflow.waitLimit.label": "Wait limit",
|
||||
"external.workflow.waitLimit.onTimeout": "On timeout",
|
||||
"external.workflow.waitLimit.title": "External agent wait limit",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Create agent",
|
||||
"roster.createDialog.description": "Create a reusable Agent in this workspace.",
|
||||
"roster.createDialog.title": "Create agent",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Failed to create inline agent. Refresh and try again.",
|
||||
"roster.nodeSelector.dialogLabel": "Select an agent",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Manage in Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Search agents…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Search…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "External",
|
||||
"roster.nodeSelector.startFromScratch": "Start from Scratch",
|
||||
"roster.nodeSelector.tabs.all": "All",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "External",
|
||||
"roster.references.label": "Workflows using {{name}}",
|
||||
"roster.references.trigger": "Workflows using {{name}}",
|
||||
"roster.saveToRoster": "Save to Agents",
|
||||
|
||||
@@ -146,6 +146,15 @@
|
||||
"external.references": "Aplicaciones que usan {{name}}",
|
||||
"external.searchPlaceholder": "Buscar agentes…",
|
||||
"external.updated": "Actualizado {{time}}",
|
||||
"external.workflow.runUnavailable": "Las ejecuciones de agentes externos aún no están disponibles.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} carácter",
|
||||
"external.workflow.task.characterCount_other": "{{count}} caracteres",
|
||||
"external.workflow.waitLimit.endWaiting": "Finalizar espera",
|
||||
"external.workflow.waitLimit.help": "Aplica el manejo de errores configurado. La tarea del agente puede seguir ejecutándose.",
|
||||
"external.workflow.waitLimit.label": "Límite de espera",
|
||||
"external.workflow.waitLimit.onTimeout": "Al superar el tiempo de espera",
|
||||
"external.workflow.waitLimit.title": "Límite de espera del agente externo",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Crear agente",
|
||||
"roster.createDialog.description": "Crea un Agent reutilizable en este espacio de trabajo.",
|
||||
"roster.createDialog.title": "Crear agente",
|
||||
@@ -184,8 +193,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Error al crear el agente inline. Actualiza y vuelve a intentarlo.",
|
||||
"roster.nodeSelector.dialogLabel": "Selecciona un agente",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Gestionar en Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Buscar agentes…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Buscar…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Externo",
|
||||
"roster.nodeSelector.startFromScratch": "Empezar desde cero",
|
||||
"roster.nodeSelector.tabs.all": "Todos",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Externo",
|
||||
"roster.references.label": "Flujos de trabajo que usan {{name}}",
|
||||
"roster.references.trigger": "Flujos de trabajo que usan {{name}}",
|
||||
"roster.saveToRoster": "Guardar en Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "برنامههای استفادهکننده از {{name}}",
|
||||
"external.searchPlaceholder": "جستجوی عاملها…",
|
||||
"external.updated": "بهروزرسانی {{time}}",
|
||||
"external.workflow.runUnavailable": "اجرای عاملهای خارجی هنوز در دسترس نیست.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} نویسه",
|
||||
"external.workflow.task.characterCount_other": "{{count}} نویسه",
|
||||
"external.workflow.waitLimit.endWaiting": "پایان انتظار",
|
||||
"external.workflow.waitLimit.help": "رسیدگی به خطای پیکربندیشده را اعمال کنید. ممکن است وظیفهٔ عامل همچنان در حال اجرا باشد.",
|
||||
"external.workflow.waitLimit.label": "محدودیت انتظار",
|
||||
"external.workflow.waitLimit.onTimeout": "هنگام پایان مهلت",
|
||||
"external.workflow.waitLimit.title": "محدودیت زمان انتظار عامل خارجی",
|
||||
"external.workflow.waitLimit.unit": "دقیقه",
|
||||
"roster.createAgent": "ایجاد عامل",
|
||||
"roster.createDialog.description": "یک Agent قابل استفاده مجدد در این فضای کاری ایجاد کنید.",
|
||||
"roster.createDialog.title": "ایجاد عامل",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "ایجاد عامل inline ناموفق بود. تازهسازی کنید و دوباره امتحان کنید.",
|
||||
"roster.nodeSelector.dialogLabel": "یک عامل انتخاب کنید",
|
||||
"roster.nodeSelector.manageInAgentConsole": "مدیریت در Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "جستجوی عاملها…",
|
||||
"roster.nodeSelector.searchPlaceholder": "جستجو…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "خارجی",
|
||||
"roster.nodeSelector.startFromScratch": "از صفر شروع کنید",
|
||||
"roster.nodeSelector.tabs.all": "همه",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "خارجی",
|
||||
"roster.references.label": "گردشهای کار با استفاده از {{name}}",
|
||||
"roster.references.trigger": "گردشهای کار با استفاده از {{name}}",
|
||||
"roster.saveToRoster": "ذخیره در Agents",
|
||||
|
||||
@@ -146,6 +146,15 @@
|
||||
"external.references": "Applications utilisant {{name}}",
|
||||
"external.searchPlaceholder": "Rechercher des agents…",
|
||||
"external.updated": "Mis à jour {{time}}",
|
||||
"external.workflow.runUnavailable": "Les exécutions d’agents externes ne sont pas encore disponibles.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} caractère",
|
||||
"external.workflow.task.characterCount_other": "{{count}} caractères",
|
||||
"external.workflow.waitLimit.endWaiting": "Arrêter l’attente",
|
||||
"external.workflow.waitLimit.help": "Appliquez la gestion des erreurs configurée. La tâche de l’agent peut continuer à s’exécuter.",
|
||||
"external.workflow.waitLimit.label": "Limite d’attente",
|
||||
"external.workflow.waitLimit.onTimeout": "En cas de délai dépassé",
|
||||
"external.workflow.waitLimit.title": "Limite d’attente de l’agent externe",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Créer un agent",
|
||||
"roster.createDialog.description": "Créez un Agent réutilisable dans cet espace de travail.",
|
||||
"roster.createDialog.title": "Créer un agent",
|
||||
@@ -184,8 +193,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Échec de la création d’un agent inline. Actualisez et réessayez.",
|
||||
"roster.nodeSelector.dialogLabel": "Sélectionner un agent",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Gérer dans Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Rechercher des agents…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Rechercher…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Externe",
|
||||
"roster.nodeSelector.startFromScratch": "Partir de zéro",
|
||||
"roster.nodeSelector.tabs.all": "Tous",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Externe",
|
||||
"roster.references.label": "Workflows utilisant {{name}}",
|
||||
"roster.references.trigger": "Workflows utilisant {{name}}",
|
||||
"roster.saveToRoster": "Enregistrer dans Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "{{name}} का उपयोग करने वाले ऐप",
|
||||
"external.searchPlaceholder": "एजेंट खोजें…",
|
||||
"external.updated": "{{time}} अपडेट किया गया",
|
||||
"external.workflow.runUnavailable": "बाहरी एजेंट रन अभी उपलब्ध नहीं हैं।",
|
||||
"external.workflow.task.characterCount_one": "{{count}} वर्ण",
|
||||
"external.workflow.task.characterCount_other": "{{count}} वर्ण",
|
||||
"external.workflow.waitLimit.endWaiting": "प्रतीक्षा समाप्त करें",
|
||||
"external.workflow.waitLimit.help": "कॉन्फ़िगर की गई त्रुटि हैंडलिंग लागू करें। एजेंट का कार्य चलता रह सकता है।",
|
||||
"external.workflow.waitLimit.label": "प्रतीक्षा सीमा",
|
||||
"external.workflow.waitLimit.onTimeout": "समय समाप्त होने पर",
|
||||
"external.workflow.waitLimit.title": "बाहरी एजेंट प्रतीक्षा सीमा",
|
||||
"external.workflow.waitLimit.unit": "मिनट",
|
||||
"roster.createAgent": "एजेंट बनाएँ",
|
||||
"roster.createDialog.description": "इस वर्कस्पेस में एक पुनः उपयोग योग्य Agent बनाएँ।",
|
||||
"roster.createDialog.title": "एजेंट बनाएँ",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "इनलाइन एजेंट बनाने में विफल। रीफ़्रेश करें और पुनः प्रयास करें।",
|
||||
"roster.nodeSelector.dialogLabel": "एक एजेंट चुनें",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Agents में प्रबंधित करें",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "एजेंट खोजें…",
|
||||
"roster.nodeSelector.searchPlaceholder": "खोजें…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "बाहरी",
|
||||
"roster.nodeSelector.startFromScratch": "शुरुआत से आरंभ करें",
|
||||
"roster.nodeSelector.tabs.all": "सभी",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "बाहरी",
|
||||
"roster.references.label": "{{name}} का उपयोग करने वाले वर्कफ़्लो",
|
||||
"roster.references.trigger": "{{name}} का उपयोग करने वाले वर्कफ़्लो",
|
||||
"roster.saveToRoster": "Agents में सहेजें",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "Aplikasi yang menggunakan {{name}}",
|
||||
"external.searchPlaceholder": "Cari agen…",
|
||||
"external.updated": "Diperbarui {{time}}",
|
||||
"external.workflow.runUnavailable": "Eksekusi agen eksternal belum tersedia.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} karakter",
|
||||
"external.workflow.task.characterCount_other": "{{count}} karakter",
|
||||
"external.workflow.waitLimit.endWaiting": "Akhiri penantian",
|
||||
"external.workflow.waitLimit.help": "Terapkan penanganan error yang dikonfigurasi. Tugas agen mungkin tetap berjalan.",
|
||||
"external.workflow.waitLimit.label": "Batas tunggu",
|
||||
"external.workflow.waitLimit.onTimeout": "Saat waktu habis",
|
||||
"external.workflow.waitLimit.title": "Batas tunggu agen eksternal",
|
||||
"external.workflow.waitLimit.unit": "mnt",
|
||||
"roster.createAgent": "Buat agen",
|
||||
"roster.createDialog.description": "Buat Agent yang dapat digunakan kembali di ruang kerja ini.",
|
||||
"roster.createDialog.title": "Buat agen",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Gagal membuat agen inline. Segarkan dan coba lagi.",
|
||||
"roster.nodeSelector.dialogLabel": "Pilih agen",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Kelola di Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Cari agen…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Cari…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Eksternal",
|
||||
"roster.nodeSelector.startFromScratch": "Mulai dari Awal",
|
||||
"roster.nodeSelector.tabs.all": "Semua",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Eksternal",
|
||||
"roster.references.label": "Alur kerja yang menggunakan {{name}}",
|
||||
"roster.references.trigger": "Alur kerja yang menggunakan {{name}}",
|
||||
"roster.saveToRoster": "Simpan ke Agents",
|
||||
|
||||
@@ -146,6 +146,15 @@
|
||||
"external.references": "App che utilizzano {{name}}",
|
||||
"external.searchPlaceholder": "Cerca agenti…",
|
||||
"external.updated": "Aggiornato {{time}}",
|
||||
"external.workflow.runUnavailable": "Le esecuzioni degli agenti esterni non sono ancora disponibili.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} carattere",
|
||||
"external.workflow.task.characterCount_other": "{{count}} caratteri",
|
||||
"external.workflow.waitLimit.endWaiting": "Termina l’attesa",
|
||||
"external.workflow.waitLimit.help": "Applica la gestione degli errori configurata. L’attività dell’agente potrebbe continuare a essere eseguita.",
|
||||
"external.workflow.waitLimit.label": "Limite di attesa",
|
||||
"external.workflow.waitLimit.onTimeout": "Alla scadenza",
|
||||
"external.workflow.waitLimit.title": "Limite di attesa dell’agente esterno",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Crea agente",
|
||||
"roster.createDialog.description": "Crea un Agent riutilizzabile in questo workspace.",
|
||||
"roster.createDialog.title": "Crea agente",
|
||||
@@ -184,8 +193,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Impossibile creare l’agente inline. Aggiorna e riprova.",
|
||||
"roster.nodeSelector.dialogLabel": "Seleziona un agente",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Gestisci in Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Cerca agenti…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Cerca…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Esterno",
|
||||
"roster.nodeSelector.startFromScratch": "Inizia da zero",
|
||||
"roster.nodeSelector.tabs.all": "Tutte",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Esterno",
|
||||
"roster.references.label": "Workflow che usano {{name}}",
|
||||
"roster.references.trigger": "Workflow che usano {{name}}",
|
||||
"roster.saveToRoster": "Salva in Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "{{name}} を使用するアプリ",
|
||||
"external.searchPlaceholder": "エージェントを検索…",
|
||||
"external.updated": "更新:{{time}}",
|
||||
"external.workflow.runUnavailable": "外部エージェントの実行はまだ利用できません。",
|
||||
"external.workflow.task.characterCount_one": "{{count}} 文字",
|
||||
"external.workflow.task.characterCount_other": "{{count}} 文字",
|
||||
"external.workflow.waitLimit.endWaiting": "待機を終了",
|
||||
"external.workflow.waitLimit.help": "設定されたエラー処理を適用します。エージェントのタスクは引き続き実行される場合があります。",
|
||||
"external.workflow.waitLimit.label": "待機上限",
|
||||
"external.workflow.waitLimit.onTimeout": "タイムアウト時",
|
||||
"external.workflow.waitLimit.title": "外部エージェントの待機上限",
|
||||
"external.workflow.waitLimit.unit": "分",
|
||||
"roster.createAgent": "エージェントを作成",
|
||||
"roster.createDialog.description": "このワークスペースに再利用可能な Agent を作成します。",
|
||||
"roster.createDialog.title": "エージェントを作成",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "インラインエージェントの作成に失敗しました。更新してもう一度お試しください。",
|
||||
"roster.nodeSelector.dialogLabel": "エージェントを選択",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Agents で管理",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "エージェントを検索…",
|
||||
"roster.nodeSelector.searchPlaceholder": "検索…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "外部",
|
||||
"roster.nodeSelector.startFromScratch": "ゼロから始める",
|
||||
"roster.nodeSelector.tabs.all": "すべて",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "外部",
|
||||
"roster.references.label": "{{name}} を使用するワークフロー",
|
||||
"roster.references.trigger": "{{name}} を使用するワークフロー",
|
||||
"roster.saveToRoster": "Agents に保存",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "{{name}}을(를) 사용하는 앱",
|
||||
"external.searchPlaceholder": "에이전트 검색…",
|
||||
"external.updated": "{{time}} 업데이트됨",
|
||||
"external.workflow.runUnavailable": "외부 에이전트 실행은 아직 사용할 수 없습니다.",
|
||||
"external.workflow.task.characterCount_one": "{{count}}자",
|
||||
"external.workflow.task.characterCount_other": "{{count}}자",
|
||||
"external.workflow.waitLimit.endWaiting": "대기 종료",
|
||||
"external.workflow.waitLimit.help": "구성된 오류 처리를 적용합니다. 에이전트 작업은 계속 실행될 수 있습니다.",
|
||||
"external.workflow.waitLimit.label": "대기 제한",
|
||||
"external.workflow.waitLimit.onTimeout": "시간 초과 시",
|
||||
"external.workflow.waitLimit.title": "외부 에이전트 대기 제한",
|
||||
"external.workflow.waitLimit.unit": "분",
|
||||
"roster.createAgent": "에이전트 만들기",
|
||||
"roster.createDialog.description": "이 워크스페이스에 재사용 가능한 Agent 를 만듭니다.",
|
||||
"roster.createDialog.title": "에이전트 만들기",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "인라인 에이전트 생성에 실패했습니다. 새로 고침 후 다시 시도하세요.",
|
||||
"roster.nodeSelector.dialogLabel": "에이전트 선택",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Agents에서 관리",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "에이전트 검색…",
|
||||
"roster.nodeSelector.searchPlaceholder": "검색…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "외부",
|
||||
"roster.nodeSelector.startFromScratch": "처음부터 시작",
|
||||
"roster.nodeSelector.tabs.all": "전체",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "외부",
|
||||
"roster.references.label": "{{name}}을(를) 사용하는 워크플로",
|
||||
"roster.references.trigger": "{{name}}을(를) 사용하는 워크플로",
|
||||
"roster.saveToRoster": "Agents에 저장",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "ແອັບທີ່ໃຊ້ {{name}}",
|
||||
"external.searchPlaceholder": "ຄົ້ນຫາຕົວແທນ…",
|
||||
"external.updated": "ອັບເດດ {{time}}",
|
||||
"external.workflow.runUnavailable": "ການເຮັດວຽກຂອງຕົວແທນພາຍນອກຍັງບໍ່ພ້ອມໃຊ້.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} ຕົວອັກສອນ",
|
||||
"external.workflow.task.characterCount_other": "{{count}} ຕົວອັກສອນ",
|
||||
"external.workflow.waitLimit.endWaiting": "ຢຸດລໍຖ້າ",
|
||||
"external.workflow.waitLimit.help": "ນຳໃຊ້ການຈັດການຂໍ້ຜິດພາດທີ່ຕັ້ງຄ່າໄວ້. ວຽກຂອງຕົວແທນອາດຍັງດຳເນີນຕໍ່.",
|
||||
"external.workflow.waitLimit.label": "ຂີດຈຳກັດການລໍຖ້າ",
|
||||
"external.workflow.waitLimit.onTimeout": "ເມື່ອໝົດເວລາ",
|
||||
"external.workflow.waitLimit.title": "ຂີດຈຳກັດການລໍຖ້າຂອງຕົວແທນພາຍນອກ",
|
||||
"external.workflow.waitLimit.unit": "ນາທີ",
|
||||
"roster.createAgent": "ສ້າງຕົວແທນ",
|
||||
"roster.createDialog.description": "ສ້າງຕົວແທນທີ່ສາມາດນຳມາໃຊ້ໃໝ່ໄດ້ໃນພື້ນທີ່ເຮັດວຽກນີ້.",
|
||||
"roster.createDialog.title": "ສ້າງຕົວແທນ",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "ສ້າງຕົວແທນ inline ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໜ້າຄືນໃໝ່ ແລະ ລອງອີກຄັ້ງ.",
|
||||
"roster.nodeSelector.dialogLabel": "ເລືອກຕົວແທນ",
|
||||
"roster.nodeSelector.manageInAgentConsole": "ຈັດການໃນ Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "ຄົ້ນຫາຕົວແທນ…",
|
||||
"roster.nodeSelector.searchPlaceholder": "ຄົ້ນຫາ...",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "ພາຍນອກ",
|
||||
"roster.nodeSelector.startFromScratch": "ເລີ່ມຈາກສູນ",
|
||||
"roster.nodeSelector.tabs.all": "ທັງໝົດ",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "ພາຍນອກ",
|
||||
"roster.references.label": "ເວີກໂຟລທີ່ໃຊ້ {{name}}",
|
||||
"roster.references.trigger": "ເວີກໂຟລທີ່ໃຊ້ {{name}}",
|
||||
"roster.saveToRoster": "ບັນທຶກລົງໃນ Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "Apps die {{name}} gebruiken",
|
||||
"external.searchPlaceholder": "Agenten zoeken…",
|
||||
"external.updated": "Bijgewerkt {{time}}",
|
||||
"external.workflow.runUnavailable": "Uitvoeringen van externe agents zijn nog niet beschikbaar.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} teken",
|
||||
"external.workflow.task.characterCount_other": "{{count}} tekens",
|
||||
"external.workflow.waitLimit.endWaiting": "Wachten beëindigen",
|
||||
"external.workflow.waitLimit.help": "Pas de geconfigureerde foutafhandeling toe. De taak van de agent kan blijven uitvoeren.",
|
||||
"external.workflow.waitLimit.label": "Wachtlimiet",
|
||||
"external.workflow.waitLimit.onTimeout": "Bij time-out",
|
||||
"external.workflow.waitLimit.title": "Wachtlimiet voor externe agent",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Agent aanmaken",
|
||||
"roster.createDialog.description": "Maak een herbruikbare Agent in deze workspace.",
|
||||
"roster.createDialog.title": "Agent aanmaken",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Inline-agent aanmaken mislukt. Vernieuw en probeer het opnieuw.",
|
||||
"roster.nodeSelector.dialogLabel": "Selecteer een agent",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Beheren in Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Agents zoeken…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Zoeken…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Extern",
|
||||
"roster.nodeSelector.startFromScratch": "Vanaf nul beginnen",
|
||||
"roster.nodeSelector.tabs.all": "Alle",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Extern",
|
||||
"roster.references.label": "Workflows die {{name}} gebruiken",
|
||||
"roster.references.trigger": "Workflows die {{name}} gebruiken",
|
||||
"roster.saveToRoster": "Opslaan in Agents",
|
||||
|
||||
@@ -148,6 +148,15 @@
|
||||
"external.references": "Aplikacje korzystające z {{name}}",
|
||||
"external.searchPlaceholder": "Szukaj agentów…",
|
||||
"external.updated": "Zaktualizowano {{time}}",
|
||||
"external.workflow.runUnavailable": "Uruchomienia agentów zewnętrznych nie są jeszcze dostępne.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} znak",
|
||||
"external.workflow.task.characterCount_other": "{{count}} znaków",
|
||||
"external.workflow.waitLimit.endWaiting": "Zakończ oczekiwanie",
|
||||
"external.workflow.waitLimit.help": "Zastosuj skonfigurowaną obsługę błędów. Zadanie agenta może nadal działać.",
|
||||
"external.workflow.waitLimit.label": "Limit oczekiwania",
|
||||
"external.workflow.waitLimit.onTimeout": "Po przekroczeniu limitu czasu",
|
||||
"external.workflow.waitLimit.title": "Limit oczekiwania agenta zewnętrznego",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Utwórz agenta",
|
||||
"roster.createDialog.description": "Utwórz wielokrotnego użytku Agent w tym workspace.",
|
||||
"roster.createDialog.title": "Utwórz agenta",
|
||||
@@ -186,8 +195,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Nie udało się utworzyć inline agenta. Odśwież i spróbuj ponownie.",
|
||||
"roster.nodeSelector.dialogLabel": "Wybierz agenta",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Zarządzaj w Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Wyszukaj agentów…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Szukaj…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Zewnętrzne",
|
||||
"roster.nodeSelector.startFromScratch": "Zacznij od zera",
|
||||
"roster.nodeSelector.tabs.all": "Wszystkie",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Zewnętrzne",
|
||||
"roster.references.label": "Workflow używające {{name}}",
|
||||
"roster.references.trigger": "Workflow używające {{name}}",
|
||||
"roster.saveToRoster": "Zapisz w Agents",
|
||||
|
||||
@@ -146,6 +146,15 @@
|
||||
"external.references": "Aplicativos que usam {{name}}",
|
||||
"external.searchPlaceholder": "Pesquisar agentes…",
|
||||
"external.updated": "Atualizado {{time}}",
|
||||
"external.workflow.runUnavailable": "Execuções de agentes externos ainda não estão disponíveis.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} caractere",
|
||||
"external.workflow.task.characterCount_other": "{{count}} caracteres",
|
||||
"external.workflow.waitLimit.endWaiting": "Encerrar espera",
|
||||
"external.workflow.waitLimit.help": "Aplique o tratamento de erros configurado. A tarefa do agente pode continuar em execução.",
|
||||
"external.workflow.waitLimit.label": "Limite de espera",
|
||||
"external.workflow.waitLimit.onTimeout": "Ao atingir o tempo limite",
|
||||
"external.workflow.waitLimit.title": "Limite de espera do agente externo",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Criar agente",
|
||||
"roster.createDialog.description": "Crie um Agent reutilizável neste workspace.",
|
||||
"roster.createDialog.title": "Criar agente",
|
||||
@@ -184,8 +193,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Falha ao criar agente inline. Atualize e tente novamente.",
|
||||
"roster.nodeSelector.dialogLabel": "Selecione um agente",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Gerenciar no Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Pesquisar agentes…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Pesquisar…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Externo",
|
||||
"roster.nodeSelector.startFromScratch": "Começar do zero",
|
||||
"roster.nodeSelector.tabs.all": "Todos",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Externo",
|
||||
"roster.references.label": "Workflows que usam {{name}}",
|
||||
"roster.references.trigger": "Workflows que usam {{name}}",
|
||||
"roster.saveToRoster": "Salvar no Agents",
|
||||
|
||||
@@ -146,6 +146,15 @@
|
||||
"external.references": "Aplicații care folosesc {{name}}",
|
||||
"external.searchPlaceholder": "Caută agenți…",
|
||||
"external.updated": "Actualizat {{time}}",
|
||||
"external.workflow.runUnavailable": "Rulările agenților externi nu sunt încă disponibile.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} caracter",
|
||||
"external.workflow.task.characterCount_other": "{{count}} caractere",
|
||||
"external.workflow.waitLimit.endWaiting": "Oprește așteptarea",
|
||||
"external.workflow.waitLimit.help": "Aplicați gestionarea configurată a erorilor. Sarcina agentului poate continua să ruleze.",
|
||||
"external.workflow.waitLimit.label": "Limită de așteptare",
|
||||
"external.workflow.waitLimit.onTimeout": "La expirarea timpului",
|
||||
"external.workflow.waitLimit.title": "Limită de așteptare pentru agentul extern",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Creează agent",
|
||||
"roster.createDialog.description": "Creați un Agent reutilizabil în acest workspace.",
|
||||
"roster.createDialog.title": "Creează agent",
|
||||
@@ -184,8 +193,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Crearea agentului inline a eșuat. Reîmprospătați și încercați din nou.",
|
||||
"roster.nodeSelector.dialogLabel": "Selectați un agent",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Gestionați în Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Căutați agenți…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Căutați…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Extern",
|
||||
"roster.nodeSelector.startFromScratch": "Începeți de la zero",
|
||||
"roster.nodeSelector.tabs.all": "Toate",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Extern",
|
||||
"roster.references.label": "Workflow-uri care folosesc {{name}}",
|
||||
"roster.references.trigger": "Workflow-uri care folosesc {{name}}",
|
||||
"roster.saveToRoster": "Salvează în Agents",
|
||||
|
||||
@@ -148,6 +148,15 @@
|
||||
"external.references": "Приложения, использующие {{name}}",
|
||||
"external.searchPlaceholder": "Поиск агентов…",
|
||||
"external.updated": "Обновлено {{time}}",
|
||||
"external.workflow.runUnavailable": "Запуски внешних агентов пока недоступны.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} символ",
|
||||
"external.workflow.task.characterCount_other": "{{count}} символов",
|
||||
"external.workflow.waitLimit.endWaiting": "Завершить ожидание",
|
||||
"external.workflow.waitLimit.help": "Примените настроенную обработку ошибок. Задача агента может продолжать выполняться.",
|
||||
"external.workflow.waitLimit.label": "Лимит ожидания",
|
||||
"external.workflow.waitLimit.onTimeout": "При превышении времени ожидания",
|
||||
"external.workflow.waitLimit.title": "Лимит ожидания внешнего агента",
|
||||
"external.workflow.waitLimit.unit": "мин.",
|
||||
"roster.createAgent": "Создать агента",
|
||||
"roster.createDialog.description": "Создайте переиспользуемый Agent в этом workspace.",
|
||||
"roster.createDialog.title": "Создать агента",
|
||||
@@ -186,8 +195,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Не удалось создать встроенного агента. Обновите и попробуйте снова.",
|
||||
"roster.nodeSelector.dialogLabel": "Выберите агента",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Управлять в Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Поиск агентов…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Поиск…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Внешние",
|
||||
"roster.nodeSelector.startFromScratch": "Начать с нуля",
|
||||
"roster.nodeSelector.tabs.all": "Все",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Внешние",
|
||||
"roster.references.label": "Рабочие процессы, использующие {{name}}",
|
||||
"roster.references.trigger": "Рабочие процессы, использующие {{name}}",
|
||||
"roster.saveToRoster": "Сохранить в Agents",
|
||||
|
||||
@@ -148,6 +148,15 @@
|
||||
"external.references": "Aplikacije, ki uporabljajo {{name}}",
|
||||
"external.searchPlaceholder": "Iskanje agentov…",
|
||||
"external.updated": "Posodobljeno {{time}}",
|
||||
"external.workflow.runUnavailable": "Izvajanja zunanjih agentov še niso na voljo.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} znak",
|
||||
"external.workflow.task.characterCount_other": "{{count}} znakov",
|
||||
"external.workflow.waitLimit.endWaiting": "Končaj čakanje",
|
||||
"external.workflow.waitLimit.help": "Uporabite konfigurirano obravnavo napak. Naloga agenta se lahko še naprej izvaja.",
|
||||
"external.workflow.waitLimit.label": "Omejitev čakanja",
|
||||
"external.workflow.waitLimit.onTimeout": "Ob prekoračitvi časa",
|
||||
"external.workflow.waitLimit.title": "Omejitev čakanja zunanjega agenta",
|
||||
"external.workflow.waitLimit.unit": "min",
|
||||
"roster.createAgent": "Ustvari agenta",
|
||||
"roster.createDialog.description": "Ustvarite ponovno uporabnega Agent v tem workspace.",
|
||||
"roster.createDialog.title": "Ustvari agenta",
|
||||
@@ -186,8 +195,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Vgrajenega agenta ni bilo mogoče ustvariti. Osvežite in poskusite znova.",
|
||||
"roster.nodeSelector.dialogLabel": "Izberite agenta",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Upravljaj v Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Iskanje agentov…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Išči…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Zunanji",
|
||||
"roster.nodeSelector.startFromScratch": "Začni iz nič",
|
||||
"roster.nodeSelector.tabs.all": "Vsi",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Zunanji",
|
||||
"roster.references.label": "Poteki dela, ki uporabljajo {{name}}",
|
||||
"roster.references.trigger": "Poteki dela, ki uporabljajo {{name}}",
|
||||
"roster.saveToRoster": "Shrani v Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "แอปที่ใช้ {{name}}",
|
||||
"external.searchPlaceholder": "ค้นหาตัวแทน…",
|
||||
"external.updated": "อัปเดต{{time}}",
|
||||
"external.workflow.runUnavailable": "การเรียกใช้เอเจนต์ภายนอกยังไม่พร้อมใช้งาน",
|
||||
"external.workflow.task.characterCount_one": "{{count}} อักขระ",
|
||||
"external.workflow.task.characterCount_other": "{{count}} อักขระ",
|
||||
"external.workflow.waitLimit.endWaiting": "สิ้นสุดการรอ",
|
||||
"external.workflow.waitLimit.help": "ใช้การจัดการข้อผิดพลาดที่กำหนดค่าไว้ งานของเอเจนต์อาจยังทำงานต่อไป",
|
||||
"external.workflow.waitLimit.label": "ขีดจำกัดเวลารอ",
|
||||
"external.workflow.waitLimit.onTimeout": "เมื่อหมดเวลา",
|
||||
"external.workflow.waitLimit.title": "ขีดจำกัดเวลารอของเอเจนต์ภายนอก",
|
||||
"external.workflow.waitLimit.unit": "นาที",
|
||||
"roster.createAgent": "สร้างตัวแทน",
|
||||
"roster.createDialog.description": "สร้าง Agent ที่นำกลับมาใช้ใหม่ได้ในเวิร์กสเปซนี้",
|
||||
"roster.createDialog.title": "สร้างตัวแทน",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "สร้างตัวแทนแบบอินไลน์ไม่สำเร็จ รีเฟรชและลองอีกครั้ง",
|
||||
"roster.nodeSelector.dialogLabel": "เลือกตัวแทน",
|
||||
"roster.nodeSelector.manageInAgentConsole": "จัดการใน Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "ค้นหาตัวแทน…",
|
||||
"roster.nodeSelector.searchPlaceholder": "ค้นหา…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "ภายนอก",
|
||||
"roster.nodeSelector.startFromScratch": "เริ่มต้นจากศูนย์",
|
||||
"roster.nodeSelector.tabs.all": "ทั้งหมด",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "ภายนอก",
|
||||
"roster.references.label": "เวิร์กโฟลว์ที่ใช้ {{name}}",
|
||||
"roster.references.trigger": "เวิร์กโฟลว์ที่ใช้ {{name}}",
|
||||
"roster.saveToRoster": "บันทึกไปยัง Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "{{name}} kullanan uygulamalar",
|
||||
"external.searchPlaceholder": "Ajan ara…",
|
||||
"external.updated": "Güncellendi: {{time}}",
|
||||
"external.workflow.runUnavailable": "Harici ajan çalıştırmaları henüz kullanılamıyor.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} karakter",
|
||||
"external.workflow.task.characterCount_other": "{{count}} karakter",
|
||||
"external.workflow.waitLimit.endWaiting": "Beklemeyi sonlandır",
|
||||
"external.workflow.waitLimit.help": "Yapılandırılmış hata işlemesini uygulayın. Ajanın görevi çalışmaya devam edebilir.",
|
||||
"external.workflow.waitLimit.label": "Bekleme sınırı",
|
||||
"external.workflow.waitLimit.onTimeout": "Zaman aşımında",
|
||||
"external.workflow.waitLimit.title": "Harici ajan bekleme sınırı",
|
||||
"external.workflow.waitLimit.unit": "dk",
|
||||
"roster.createAgent": "Ajan oluştur",
|
||||
"roster.createDialog.description": "Bu workspace içinde yeniden kullanılabilir bir Agent oluşturun.",
|
||||
"roster.createDialog.title": "Ajan oluştur",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Inline ajan oluşturulamadı. Yenileyip tekrar deneyin.",
|
||||
"roster.nodeSelector.dialogLabel": "Bir ajan seç",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Agents'da yönet",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Ajan ara…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Ara…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Harici",
|
||||
"roster.nodeSelector.startFromScratch": "Sıfırdan Başla",
|
||||
"roster.nodeSelector.tabs.all": "Tümü",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Harici",
|
||||
"roster.references.label": "{{name}} kullanan iş akışları",
|
||||
"roster.references.trigger": "{{name}} kullanan iş akışları",
|
||||
"roster.saveToRoster": "Agents'a kaydet",
|
||||
|
||||
@@ -148,6 +148,15 @@
|
||||
"external.references": "Застосунки, що використовують {{name}}",
|
||||
"external.searchPlaceholder": "Пошук агентів…",
|
||||
"external.updated": "Оновлено {{time}}",
|
||||
"external.workflow.runUnavailable": "Запуски зовнішніх агентів ще недоступні.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} символ",
|
||||
"external.workflow.task.characterCount_other": "{{count}} символів",
|
||||
"external.workflow.waitLimit.endWaiting": "Завершити очікування",
|
||||
"external.workflow.waitLimit.help": "Застосуйте налаштовану обробку помилок. Завдання агента може продовжувати виконуватися.",
|
||||
"external.workflow.waitLimit.label": "Ліміт очікування",
|
||||
"external.workflow.waitLimit.onTimeout": "У разі перевищення часу очікування",
|
||||
"external.workflow.waitLimit.title": "Ліміт очікування зовнішнього агента",
|
||||
"external.workflow.waitLimit.unit": "хв",
|
||||
"roster.createAgent": "Створити агента",
|
||||
"roster.createDialog.description": "Створіть перевикористовний Agent у цьому workspace.",
|
||||
"roster.createDialog.title": "Створити агента",
|
||||
@@ -186,8 +195,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Не вдалося створити вбудованого агента. Оновіть і спробуйте ще раз.",
|
||||
"roster.nodeSelector.dialogLabel": "Виберіть агента",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Керувати в Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Пошук агентів…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Пошук…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Зовнішні",
|
||||
"roster.nodeSelector.startFromScratch": "Почати з нуля",
|
||||
"roster.nodeSelector.tabs.all": "Усі",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Зовнішні",
|
||||
"roster.references.label": "Робочі процеси, що використовують {{name}}",
|
||||
"roster.references.trigger": "Робочі процеси, що використовують {{name}}",
|
||||
"roster.saveToRoster": "Зберегти в Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "Ứng dụng sử dụng {{name}}",
|
||||
"external.searchPlaceholder": "Tìm tác nhân…",
|
||||
"external.updated": "Đã cập nhật {{time}}",
|
||||
"external.workflow.runUnavailable": "Các lượt chạy tác nhân bên ngoài hiện chưa khả dụng.",
|
||||
"external.workflow.task.characterCount_one": "{{count}} ký tự",
|
||||
"external.workflow.task.characterCount_other": "{{count}} ký tự",
|
||||
"external.workflow.waitLimit.endWaiting": "Kết thúc chờ",
|
||||
"external.workflow.waitLimit.help": "Áp dụng cách xử lý lỗi đã cấu hình. Tác vụ của tác nhân có thể vẫn tiếp tục chạy.",
|
||||
"external.workflow.waitLimit.label": "Giới hạn chờ",
|
||||
"external.workflow.waitLimit.onTimeout": "Khi hết thời gian chờ",
|
||||
"external.workflow.waitLimit.title": "Giới hạn chờ của tác nhân bên ngoài",
|
||||
"external.workflow.waitLimit.unit": "phút",
|
||||
"roster.createAgent": "Tạo tác nhân",
|
||||
"roster.createDialog.description": "Tạo một Agent có thể tái sử dụng trong workspace này.",
|
||||
"roster.createDialog.title": "Tạo tác nhân",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Tạo tác nhân inline thất bại. Làm mới và thử lại.",
|
||||
"roster.nodeSelector.dialogLabel": "Chọn một tác nhân",
|
||||
"roster.nodeSelector.manageInAgentConsole": "Quản lý trong Agents",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "Tìm kiếm tác nhân…",
|
||||
"roster.nodeSelector.searchPlaceholder": "Tìm kiếm…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "Bên ngoài",
|
||||
"roster.nodeSelector.startFromScratch": "Bắt đầu từ đầu",
|
||||
"roster.nodeSelector.tabs.all": "Tất cả",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "Bên ngoài",
|
||||
"roster.references.label": "Quy trình làm việc đang dùng {{name}}",
|
||||
"roster.references.trigger": "Quy trình làm việc đang dùng {{name}}",
|
||||
"roster.saveToRoster": "Lưu vào Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "使用 {{name}} 的应用",
|
||||
"external.searchPlaceholder": "搜索 Agent…",
|
||||
"external.updated": "更新于 {{time}}",
|
||||
"external.workflow.runUnavailable": "外部 Agent 运行暂不可用。",
|
||||
"external.workflow.task.characterCount_one": "{{count}} 个字符",
|
||||
"external.workflow.task.characterCount_other": "{{count}} 个字符",
|
||||
"external.workflow.waitLimit.endWaiting": "结束等待",
|
||||
"external.workflow.waitLimit.help": "应用已配置的错误处理。Agent 的任务可能仍会继续运行。",
|
||||
"external.workflow.waitLimit.label": "等待时限",
|
||||
"external.workflow.waitLimit.onTimeout": "超时时",
|
||||
"external.workflow.waitLimit.title": "外部 Agent 等待时限",
|
||||
"external.workflow.waitLimit.unit": "分钟",
|
||||
"roster.createAgent": "创建 Agent",
|
||||
"roster.createDialog.description": "在当前工作区创建一个可复用 Agent。",
|
||||
"roster.createDialog.title": "创建 Agent",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Inline Agent 创建失败。请刷新后重试。",
|
||||
"roster.nodeSelector.dialogLabel": "选择 Agent",
|
||||
"roster.nodeSelector.manageInAgentConsole": "在 Agents 中管理",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "搜索 Agent…",
|
||||
"roster.nodeSelector.searchPlaceholder": "搜索…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "外部",
|
||||
"roster.nodeSelector.startFromScratch": "从空白开始",
|
||||
"roster.nodeSelector.tabs.all": "全部",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "外部",
|
||||
"roster.references.label": "使用 {{name}} 的工作流",
|
||||
"roster.references.trigger": "使用 {{name}} 的工作流",
|
||||
"roster.saveToRoster": "保存到 Agents",
|
||||
|
||||
@@ -144,6 +144,15 @@
|
||||
"external.references": "使用 {{name}} 的應用程式",
|
||||
"external.searchPlaceholder": "搜尋 Agent…",
|
||||
"external.updated": "更新於 {{time}}",
|
||||
"external.workflow.runUnavailable": "外部 Agent 執行暫不可用。",
|
||||
"external.workflow.task.characterCount_one": "{{count}} 個字元",
|
||||
"external.workflow.task.characterCount_other": "{{count}} 個字元",
|
||||
"external.workflow.waitLimit.endWaiting": "結束等待",
|
||||
"external.workflow.waitLimit.help": "套用已設定的錯誤處理。Agent 的工作可能仍會繼續執行。",
|
||||
"external.workflow.waitLimit.label": "等待時限",
|
||||
"external.workflow.waitLimit.onTimeout": "逾時時",
|
||||
"external.workflow.waitLimit.title": "外部 Agent 等待時限",
|
||||
"external.workflow.waitLimit.unit": "分鐘",
|
||||
"roster.createAgent": "建立 Agent",
|
||||
"roster.createDialog.description": "在目前工作區建立一個可複用 Agent。",
|
||||
"roster.createDialog.title": "建立 Agent",
|
||||
@@ -182,8 +191,14 @@
|
||||
"roster.nodeSelector.createInlineFailed": "Inline Agent 建立失敗。請重新整理後再試。",
|
||||
"roster.nodeSelector.dialogLabel": "選擇 Agent",
|
||||
"roster.nodeSelector.manageInAgentConsole": "在 Agents 中管理",
|
||||
"roster.nodeSelector.searchAgentsPlaceholder": "搜尋 Agent…",
|
||||
"roster.nodeSelector.searchPlaceholder": "搜尋…",
|
||||
"roster.nodeSelector.source.dify": "Dify",
|
||||
"roster.nodeSelector.source.external": "外部",
|
||||
"roster.nodeSelector.startFromScratch": "從空白開始",
|
||||
"roster.nodeSelector.tabs.all": "全部",
|
||||
"roster.nodeSelector.tabs.dify": "Dify",
|
||||
"roster.nodeSelector.tabs.external": "外部",
|
||||
"roster.references.label": "使用 {{name}} 的工作流程",
|
||||
"roster.references.trigger": "使用 {{name}} 的工作流程",
|
||||
"roster.saveToRoster": "儲存到 Agents",
|
||||
|
||||
Vendored
+14
@@ -321,6 +321,14 @@ export default interface Resources {
|
||||
'external.references': 'Apps using {{name}}'
|
||||
'external.searchPlaceholder': 'Search agents…'
|
||||
'external.updated': 'Updated {{time}}'
|
||||
'external.workflow.runUnavailable': 'External agent runs are not available yet.'
|
||||
'external.workflow.task.characterCount': '{{count}} character' | '{{count}} characters'
|
||||
'external.workflow.waitLimit.endWaiting': 'End waiting'
|
||||
'external.workflow.waitLimit.help': 'Apply the configured error handling. The agent’s task may continue running.'
|
||||
'external.workflow.waitLimit.label': 'Wait limit'
|
||||
'external.workflow.waitLimit.onTimeout': 'On timeout'
|
||||
'external.workflow.waitLimit.title': 'External agent wait limit'
|
||||
'external.workflow.waitLimit.unit': 'min'
|
||||
'roster.createAgent': 'Create agent'
|
||||
'roster.createDialog.description': 'Create a reusable Agent in this workspace.'
|
||||
'roster.createDialog.title': 'Create agent'
|
||||
@@ -359,8 +367,14 @@ export default interface Resources {
|
||||
'roster.nodeSelector.createInlineFailed': 'Failed to create inline agent. Refresh and try again.'
|
||||
'roster.nodeSelector.dialogLabel': 'Select an agent'
|
||||
'roster.nodeSelector.manageInAgentConsole': 'Manage in Agents'
|
||||
'roster.nodeSelector.searchAgentsPlaceholder': 'Search agents…'
|
||||
'roster.nodeSelector.searchPlaceholder': 'Search…'
|
||||
'roster.nodeSelector.source.dify': 'Dify'
|
||||
'roster.nodeSelector.source.external': 'External'
|
||||
'roster.nodeSelector.startFromScratch': 'Start from Scratch'
|
||||
'roster.nodeSelector.tabs.all': 'All'
|
||||
'roster.nodeSelector.tabs.dify': 'Dify'
|
||||
'roster.nodeSelector.tabs.external': 'External'
|
||||
'roster.references.label': 'Workflows using {{name}}'
|
||||
'roster.references.trigger': 'Workflows using {{name}}'
|
||||
'roster.saveToRoster': 'Save to Agents'
|
||||
|
||||
Reference in new issue
Block a user