Merge commit 'fb41b215' into sandboxed-agent-rebase

Made-with: Cursor

# Conflicts:
#	.devcontainer/post_create_command.sh
#	api/commands.py
#	api/core/agent/cot_agent_runner.py
#	api/core/agent/fc_agent_runner.py
#	api/core/app/apps/workflow_app_runner.py
#	api/core/app/entities/queue_entities.py
#	api/core/app/entities/task_entities.py
#	api/core/workflow/workflow_entry.py
#	api/dify_graph/enums.py
#	api/dify_graph/graph/graph.py
#	api/dify_graph/graph_events/node.py
#	api/dify_graph/model_runtime/entities/message_entities.py
#	api/dify_graph/node_events/node.py
#	api/dify_graph/nodes/agent/agent_node.py
#	api/dify_graph/nodes/base/__init__.py
#	api/dify_graph/nodes/base/entities.py
#	api/dify_graph/nodes/base/node.py
#	api/dify_graph/nodes/llm/entities.py
#	api/dify_graph/nodes/llm/node.py
#	api/dify_graph/nodes/tool/tool_node.py
#	api/pyproject.toml
#	api/uv.lock
#	web/app/components/base/avatar/__tests__/index.spec.tsx
#	web/app/components/base/avatar/index.tsx
#	web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx
#	web/app/components/base/file-uploader/file-from-link-or-local/index.tsx
#	web/app/components/base/prompt-editor/index.tsx
#	web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx
#	web/app/components/header/account-dropdown/index.spec.tsx
#	web/app/components/share/text-generation/index.tsx
#	web/app/components/workflow/block-selector/tool/action-item.tsx
#	web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx
#	web/app/components/workflow/hooks/use-edges-interactions.ts
#	web/app/components/workflow/hooks/use-nodes-interactions.ts
#	web/app/components/workflow/index.tsx
#	web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
#	web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx
#	web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx
#	web/app/components/workflow/nodes/loop/use-interactions.ts
#	web/contract/router.ts
#	web/env.ts
#	web/eslint-suppressions.json
#	web/package.json
#	web/pnpm-lock.yaml
This commit is contained in:
Novice
2026-03-23 10:52:06 +08:00
1395 changed files with 167201 additions and 73658 deletions

View File

@ -1,24 +1,18 @@
'use client'
import type { FC } from 'react'
import Editor, { loader } from '@monaco-editor/react'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
getFilesInLogs,
} from '@/app/components/base/file-uploader/utils'
import { ModernMonacoEditor } from '@/app/components/base/modern-monaco/modern-monaco-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import { cn } from '@/utils/classnames'
import { basePath } from '@/utils/var'
import Base from '../base'
import './style.css'
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
if (typeof window !== 'undefined')
loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } })
const CODE_EDITOR_LINE_HEIGHT = 18
export type Props = {
@ -76,15 +70,10 @@ const CodeEditor: FC<Props> = ({
tip,
footer,
}) => {
const { t } = useTranslation()
const [isFocus, setIsFocus] = React.useState(false)
const [isMounted, setIsMounted] = React.useState(false)
const minHeight = height || 200
const [editorContentHeight, setEditorContentHeight] = useState(56)
const { theme: appTheme } = useTheme()
const valueRef = useRef(value)
useEffect(() => {
valueRef.current = value
}, [value])
const fileList = useMemo(() => {
if (typeof value === 'object')
@ -111,19 +100,16 @@ const CodeEditor: FC<Props> = ({
editorRef.current = editor
resizeEditorToContent()
editor.onDidFocusEditorText(() => {
setIsFocus(true)
onFocus?.()
})
editor.onDidBlurEditorText(() => {
setIsFocus(false)
onBlur?.()
})
monaco.editor.setTheme(appTheme === Theme.light ? 'light' : 'vs-dark') // Fix: sometimes not load the default theme
onMount?.(editor, monaco)
setIsMounted(true)
}
const handleEditorFocus = () => {
setIsFocus(true)
}
const handleEditorBlur = () => {
setIsFocus(false)
}
const outPutValue = (() => {
@ -137,31 +123,23 @@ const CodeEditor: FC<Props> = ({
}
})()
const theme = useMemo(() => {
if (appTheme === Theme.light)
return 'light'
return 'vs-dark'
}, [appTheme])
const main = (
<>
{/* https://www.npmjs.com/package/@monaco-editor/react */}
<Editor
<ModernMonacoEditor
// className='min-h-[100%]' // h-full
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
language={languageMap[language] || 'javascript'}
theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
value={outPutValue}
loading={<span className="text-text-primary">Loading...</span>}
readOnly={readOnly}
onChange={handleEditorChange}
onFocus={handleEditorFocus}
onBlur={handleEditorBlur}
onReady={handleEditorDidMount}
loading={<span className="text-text-primary">{t('loading', { ns: 'common' })}</span>}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
options={{
readOnly,
domReadOnly: true,
quickSuggestions: false,
minimap: { enabled: false },
lineNumbersMinChars: 1, // would change line num width
wordWrap: 'on', // auto line wrap
// lineNumbers: (num) => {
// return <div>{num}</div>
// }
@ -171,7 +149,6 @@ const CodeEditor: FC<Props> = ({
},
stickyScroll: { enabled: false },
}}
onMount={handleEditorDidMount}
/>
{!outPutValue && !isFocus && <div className="pointer-events-none absolute left-[36px] top-0 text-[13px] font-normal leading-[18px] text-components-input-text-placeholder">{placeholder}</div>}
</>

View File

@ -61,12 +61,12 @@ const Editor: FC<Props> = ({
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
return (
<div className={cn(className, 'relative')}>
<div className={cn(className, 'relative min-h-8')}>
<>
<PromptEditor
key={`${instanceId ?? 'input-support-select-var'}-${readOnly ? 'ro' : 'rw'}`}
instanceId={instanceId}
className={cn(promptMinHeightClassName, '!leading-[18px]')}
className={cn(promptMinHeightClassName, 'leading-[18px]')}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
value={value}

View File

@ -36,6 +36,7 @@ const VariableLabel = ({
)}
onClick={onClick}
ref={ref}
{...(isExceptionVariable ? { 'data-testid': 'exception-variable' } : {})}
>
{isShowNodeLabel && (
<VariableNodeLabel

View File

@ -59,9 +59,9 @@ const KeyValueList: FC<Props> = ({
return (
<div className="overflow-hidden rounded-lg border border-divider-regular">
<div className={cn('flex h-7 items-center leading-7 text-text-tertiary system-xs-medium-uppercase')}>
<div className={cn('h-full border-r border-divider-regular pl-3', isSupportFile ? 'w-[140px]' : 'w-1/2')}>{t(`${i18nPrefix}.key`, { ns: 'workflow' })}</div>
{isSupportFile && <div className="h-full w-[70px] shrink-0 border-r border-divider-regular pl-3">{t(`${i18nPrefix}.type`, { ns: 'workflow' })}</div>}
<div className={cn('h-full items-center justify-between pl-3 pr-1', isSupportFile ? 'grow' : 'w-1/2')}>{t(`${i18nPrefix}.value`, { ns: 'workflow' })}</div>
<div className={cn('flex h-full items-center border-r border-divider-regular pl-3', isSupportFile ? 'w-[140px]' : 'w-1/2')}>{t(`${i18nPrefix}.key`, { ns: 'workflow' })}</div>
{isSupportFile && <div className="flex h-full w-[70px] shrink-0 items-center border-r border-divider-regular pl-3">{t(`${i18nPrefix}.type`, { ns: 'workflow' })}</div>}
<div className={cn('flex h-full items-center justify-between pl-3 pr-1', isSupportFile ? 'grow' : 'w-1/2')}>{t(`${i18nPrefix}.value`, { ns: 'workflow' })}</div>
</div>
{
list.map((item, index) => (

View File

@ -59,12 +59,12 @@ const InputItem: FC<Props> = ({
}, [onRemove])
return (
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex !h-[30px] items-center')}>
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex')}>
{(!readOnly)
? (
<Input
instanceId={instanceId}
className={cn(isFocus ? 'bg-components-input-bg-active' : 'bg-width', 'h-full w-0 grow px-3 py-1')}
className={cn(isFocus ? 'bg-components-input-bg-active' : '', 'clamp group w-0 grow px-3 py-1')}
value={value}
onChange={onChange}
readOnly={readOnly}
@ -73,7 +73,6 @@ const InputItem: FC<Props> = ({
onFocusChange={setIsFocus}
placeholder={t('nodes.http.insertVarPlaceholder', { ns: 'workflow' })!}
placeholderClassName="!leading-[21px]"
promptMinHeightClassName="h-full"
insertVarTipToLeft={insertVarTipToLeft}
/>
)
@ -85,7 +84,7 @@ const InputItem: FC<Props> = ({
{hasValue && (
<Input
instanceId={instanceId}
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'h-full w-0 grow rounded-lg border px-3 py-[6px]')}
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'clamp group h-full w-0 grow rounded-lg border px-3 py-[6px]')}
value={value}
onChange={onChange}
readOnly={readOnly}

View File

@ -3,7 +3,7 @@ import type { Member } from '@/models/common'
import { RiCloseCircleFill, RiErrorWarningFill } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import { Avatar } from '@/app/components/base/avatar'
import { cn } from '@/utils/classnames'
type Props = {
@ -34,8 +34,8 @@ const EmailItem = ({
{isError && (
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
)}
{!isError && <Avatar avatar={data.avatar_url} size={16} name={data.name || data.email} />}
<div title={data.email} className="max-w-[500px] truncate text-text-primary system-xs-regular">
{!isError && <Avatar avatar={data.avatar_url} size="xxs" name={data.name || data.email} />}
<div title={data.email} className="system-xs-regular max-w-[500px] truncate text-text-primary">
{email === data.email ? data.name : data.email}
{email === data.email && <span className="text-text-tertiary system-xs-regular">{t('members.you', { ns: 'common' })}</span>}
</div>

View File

@ -4,7 +4,7 @@ import type { Recipient } from '@/app/components/workflow/nodes/human-input/type
import type { Member } from '@/models/common'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import { Avatar } from '@/app/components/base/avatar'
import Input from '@/app/components/base/input'
import { cn } from '@/utils/classnames'
@ -65,7 +65,7 @@ const MemberList: FC<Props> = ({ searchValue, list, value, onSearchChange, onSel
onSelect(account.id)
}}
>
<Avatar className={cn(value.some(item => item.user_id === account.id) && 'opacity-50')} avatar={account.avatar_url} size={24} name={account.name} />
<Avatar className={cn(value.some(item => item.user_id === account.id) && 'opacity-50')} avatar={account.avatar_url} size="sm" name={account.name} />
<div className={cn('grow', value.some(item => item.user_id === account.id) && 'opacity-50')}>
<div className="text-text-secondary system-sm-medium">
{account.name}

View File

@ -2,7 +2,6 @@
import type { FC } from 'react'
import type { FormInputItem, UserAction } from '../types'
import type { ButtonProps } from '@/app/components/base/button'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
@ -14,6 +13,7 @@ import { useStore } from '@/app/components/workflow/store'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { Note, rehypeNotes, rehypeVariable, Variable } from './variable-in-markdown'
const NODE_ID_RE = /#([^#.]+)([.#])/g
const i18nPrefix = 'nodes.humanInput'
type FormContentPreviewProps = {
@ -47,25 +47,25 @@ const FormContentPreview: FC<FormContentPreviewProps> = ({
>
<div className="flex h-[26px] items-center justify-between px-4">
<Badge uppercase className="border-text-accent-secondary text-text-accent-secondary">{t(`${i18nPrefix}.formContent.preview`, { ns: 'workflow' })}</Badge>
<ActionButton onClick={onClose}><RiCloseLine className="w-5 text-text-tertiary" /></ActionButton>
<ActionButton onClick={onClose}><span className="i-ri-close-line size-5 text-text-tertiary" /></ActionButton>
</div>
<div className="max-h-[calc(100vh-167px)] overflow-y-auto px-4">
<Markdown
content={content}
rehypePlugins={[rehypeVariable, rehypeNotes]}
customComponents={{
variable: ({ node }: { node: { properties?: { [key: string]: string } } }) => {
const path = node.properties?.['data-path'] as string
variable: ({ node }) => {
const path = String(node?.properties?.dataPath ?? '')
let newPath = path
if (path) {
newPath = path.replace(/#([^#.]+)([.#])/g, (match, nodeId, sep) => {
newPath = path.replace(NODE_ID_RE, (match, nodeId, sep) => {
return `#${nodeName(nodeId)}${sep}`
})
}
return <Variable path={newPath} />
},
section: ({ node }: { node: { properties?: { [key: string]: string } } }) => (() => {
const name = node.properties?.['data-name'] as string
section: ({ node }) => (() => {
const name = String(node?.properties?.dataName ?? '')
const input = formInputs.find(i => i.output_variable_name === name)
if (!input) {
return (

View File

@ -24,7 +24,7 @@ export function rehypeVariable() {
parts.push({
type: 'element',
tagName: 'variable',
properties: { 'data-path': m[0].trim() },
properties: { dataPath: m[0].trim() },
children: [],
})
@ -77,7 +77,7 @@ export function rehypeNotes() {
parts.push({
type: 'element',
tagName: 'section',
properties: { 'data-name': name },
properties: { dataName: name },
children: [],
})

View File

@ -76,14 +76,14 @@ const ConditionValue = ({
}, [isSelect, t, value])
return (
<div className="flex h-6 items-center rounded-md bg-workflow-block-parma-bg px-1">
<div className="flex flex-wrap items-center rounded-md bg-workflow-block-parma-bg">
<VariableLabelInText
className="w-0 grow"
className="flex min-w-0 shrink-0 items-center border-none bg-transparent shadow-none"
variables={variableSelector}
nodeTitle={node?.data.title}
nodeType={node?.data.type}
isExceptionVariable={isException}
notShowFullPath
notShowFullPath={false}
/>
<div
className="mx-1 shrink-0 text-xs font-medium text-text-primary"
@ -93,7 +93,7 @@ const ConditionValue = ({
</div>
{
!notHasValue && (
<div className="shrink-[3] truncate text-xs text-text-secondary" title={formatValue}>{isSelect ? selectName : formatValue}</div>
<div className="grow truncate px-1.5 text-xs leading-6 text-text-secondary" title={formatValue}>{isSelect ? selectName : formatValue}</div>
)
}
</div>

View File

@ -1,13 +1,11 @@
import type { FC } from 'react'
import { Editor } from '@monaco-editor/react'
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { ModernMonacoEditor } from '@/app/components/base/modern-monaco/modern-monaco-editor'
import Tooltip from '@/app/components/base/tooltip'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import { cn } from '@/utils/classnames'
type CodeEditorProps = {
@ -35,54 +33,11 @@ const CodeEditor: FC<CodeEditorProps> = ({
onBlur,
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
const monacoRef = useRef<any>(null)
const editorRef = useRef<any>(null)
const [isMounted, setIsMounted] = React.useState(false)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (monacoRef.current) {
if (theme === Theme.light)
monacoRef.current.editor.setTheme('light-theme')
else
monacoRef.current.editor.setTheme('dark-theme')
}
}, [theme])
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
const handleEditorReady = useCallback((editor: any) => {
editorRef.current = editor
monacoRef.current = monaco
editor.onDidFocusEditorText(() => {
onFocus?.()
})
editor.onDidBlurEditorText(() => {
onBlur?.()
})
monaco.editor.defineTheme('light-theme', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': '#00000000',
'editor.lineHighlightBackground': '#00000000',
'focusBorder': '#00000000',
},
})
monaco.editor.defineTheme('dark-theme', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#00000000',
'editor.lineHighlightBackground': '#00000000',
'focusBorder': '#00000000',
},
})
monaco.editor.setTheme('light-theme')
setIsMounted(true)
editor.getModel()?.updateOptions({ tabSize: 2 })
}, [])
const formatJsonContent = useCallback(() => {
@ -95,24 +50,6 @@ const CodeEditor: FC<CodeEditorProps> = ({
onUpdate?.(value)
}, [onUpdate])
const editorTheme = useMemo(() => {
if (theme === Theme.light)
return 'light-theme'
return 'dark-theme'
}, [theme])
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
editorRef.current?.layout()
})
if (containerRef.current)
resizeObserver.observe(containerRef.current)
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<div className={cn('flex h-full flex-col overflow-hidden bg-components-input-bg-normal', hideTopMenu && 'pt-2', className)}>
{!hideTopMenu && (
@ -146,19 +83,17 @@ const CodeEditor: FC<CodeEditorProps> = ({
)}
{topContent}
<div className={cn('relative overflow-hidden', editorWrapperClassName)}>
<Editor
defaultLanguage="json"
theme={isMounted ? editorTheme : 'default-theme'} // sometimes not load the default theme
<ModernMonacoEditor
language="json"
value={value}
readOnly={readOnly}
onChange={handleEditorChange}
onMount={handleEditorDidMount}
onReady={handleEditorReady}
onFocus={onFocus}
onBlur={onBlur}
loading={<span className="text-text-primary">{t('loading', { ns: 'common' })}</span>}
options={{
readOnly,
domReadOnly: true,
minimap: { enabled: false },
tabSize: 2,
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'same',
overviewRulerBorder: false,
hideCursorInOverviewRuler: true,

View File

@ -103,11 +103,12 @@ export const useNodeLoopInteractions = () => {
handleNodeLoopRerender(parentId)
}, [collaborativeWorkflow, handleNodeLoopRerender])
const handleNodeLoopChildrenCopy = useCallback((nodeId: string, newNodeId: string) => {
const handleNodeLoopChildrenCopy = useCallback((nodeId: string, newNodeId: string, idMapping: Record<string, string>) => {
const { nodes } = collaborativeWorkflow.getState()
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_LOOP_START_NODE)
const newIdMapping = { ...idMapping }
return childrenNodes.map((child, index) => {
const copyChildren = childrenNodes.map((child, index) => {
const childNodeType = child.data.type as BlockEnum
const defaultValue = nodesMetaDataMap?.[childNodeType]?.defaultValue ?? {}
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
@ -133,8 +134,14 @@ export const useNodeLoopInteractions = () => {
zIndex: LOOP_CHILDREN_Z_INDEX,
})
newNode.id = `${newNodeId}${newNode.id + index}`
newIdMapping[child.id] = newNode.id
return newNode
})
return {
copyChildren,
newIdMapping,
}
}, [collaborativeWorkflow, nodesMetaDataMap])
return {

View File

@ -174,7 +174,7 @@ const AddExtractParameter: FC<Props> = ({
<Field title={t(`${i18nPrefix}.addExtractParameterContent.required`, { ns: 'workflow' })}>
<>
<div className="mb-1.5 text-xs font-normal leading-[18px] text-text-tertiary">{t(`${i18nPrefix}.addExtractParameterContent.requiredContent`, { ns: 'workflow' })}</div>
<Switch size="l" value={param.required ?? false} onChange={handleParamChange('required')} />
<Switch size="lg" value={param.required ?? false} onChange={handleParamChange('required')} />
</>
</Field>
</div>

View File

@ -96,7 +96,10 @@ const GenericTable: FC<GenericTableProps> = ({
})
// If the last configured row has content, append a trailing empty row
const lastHasContent = !isEmptyRow(data[data.length - 1])
const lastRow = data.at(-1)
if (!lastRow)
return rows
const lastHasContent = !isEmptyRow(lastRow)
if (lastHasContent)
rows.push({ row: { ...emptyRowData }, dataIndex: null, isVirtual: true })
@ -217,7 +220,7 @@ const GenericTable: FC<GenericTableProps> = ({
<div
key={column.key}
className={cn(
'h-full pl-3',
'flex h-full items-center pl-3',
column.width && column.width.startsWith('w-') ? 'shrink-0' : 'flex-1',
column.width,
// Add right border except for last column