Merge main HEAD (segment 5) into sandboxed-agent-rebase

Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files.
Preserve sandbox/agent/collaboration features while adopting main's
UI refactorings (Dialog/AlertDialog/Popover), model provider updates,
and enterprise features.

Made-with: Cursor
This commit is contained in:
Novice
2026-03-23 14:20:06 +08:00
1671 changed files with 124822 additions and 22302 deletions

View File

@ -0,0 +1,75 @@
import type { WorkflowNodesMap } from '@/app/components/base/prompt-editor/types'
import { renderHook } from '@testing-library/react'
import { BlockEnum } from '@/app/components/workflow/types'
import { useLlmModelPluginInstalled } from '../use-llm-model-plugin-installed'
let mockModelProviders: Array<{ provider: string }> = []
vi.mock('@/context/provider-context', () => ({
useProviderContextSelector: <T>(selector: (state: { modelProviders: Array<{ provider: string }> }) => T): T =>
selector({ modelProviders: mockModelProviders }),
}))
const createWorkflowNodesMap = (node: Record<string, unknown>): WorkflowNodesMap =>
({
target: {
title: 'Target',
type: BlockEnum.Start,
...node,
},
} as unknown as WorkflowNodesMap)
describe('useLlmModelPluginInstalled', () => {
beforeEach(() => {
vi.clearAllMocks()
mockModelProviders = []
})
it('should return true when the node is missing', () => {
const { result } = renderHook(() => useLlmModelPluginInstalled('target', undefined))
expect(result.current).toBe(true)
})
it('should return true when the node is not an LLM node', () => {
const workflowNodesMap = createWorkflowNodesMap({
id: 'target',
type: BlockEnum.Start,
})
const { result } = renderHook(() => useLlmModelPluginInstalled('target', workflowNodesMap))
expect(result.current).toBe(true)
})
it('should return true when the matching model plugin is installed', () => {
mockModelProviders = [
{ provider: 'langgenius/openai/openai' },
{ provider: 'langgenius/anthropic/claude' },
]
const workflowNodesMap = createWorkflowNodesMap({
id: 'target',
type: BlockEnum.LLM,
modelProvider: 'langgenius/openai/gpt-4.1',
})
const { result } = renderHook(() => useLlmModelPluginInstalled('target', workflowNodesMap))
expect(result.current).toBe(true)
})
it('should return false when the matching model plugin is not installed', () => {
mockModelProviders = [
{ provider: 'langgenius/anthropic/claude' },
]
const workflowNodesMap = createWorkflowNodesMap({
id: 'target',
type: BlockEnum.LLM,
modelProvider: 'langgenius/openai/gpt-4.1',
})
const { result } = renderHook(() => useLlmModelPluginInstalled('target', workflowNodesMap))
expect(result.current).toBe(false)
})
})

View File

@ -15,7 +15,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow, useStoreApi } from 'reactflow'
import Tooltip from '@/app/components/base/tooltip'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, isValueSelectorInNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
import {
@ -30,6 +30,7 @@ import {
UPDATE_WORKFLOW_NODES_MAP,
} from './index'
import { WorkflowVariableBlockNode } from './node'
import { useLlmModelPluginInstalled } from './use-llm-model-plugin-installed'
type WorkflowVariableBlockComponentProps = {
nodeKey: string
@ -75,6 +76,8 @@ const WorkflowVariableBlockComponent = ({
&& variables[variablesLength - 1] === 'context'
const isException = isExceptionVariable(varName, node?.type)
const sourceNodeId = variables[isRagVar ? 1 : 0]
const isLlmModelInstalled = useLlmModelPluginInstalled(sourceNodeId, localWorkflowNodesMap)
const variableValid = useMemo(() => {
if (localNodeOutputVars.length)
return isValueSelectorInNodeOutputVars(variables, localNodeOutputVars)
@ -158,7 +161,13 @@ const WorkflowVariableBlockComponent = ({
handleVariableJump()
}}
isExceptionVariable={isException}
errorMsg={!variableValid ? t('errorMsg.invalidVariable', { ns: 'workflow' }) : undefined}
errorMsg={
!variableValid
? t('errorMsg.invalidVariable', { ns: 'workflow' })
: !isLlmModelInstalled
? t('errorMsg.modelPluginNotInstalled', { ns: 'workflow' })
: undefined
}
isSelected={isSelected}
ref={ref}
notShowFullPath={isShowAPart}
@ -169,9 +178,9 @@ const WorkflowVariableBlockComponent = ({
return Item
return (
<Tooltip
noDecoration
popupContent={(
<Tooltip>
<TooltipTrigger disabled={!isShowAPart} render={<div>{Item}</div>} />
<TooltipContent variant="plain">
<VarFullPathPanel
nodeName={node.title}
path={variables.slice(1)}
@ -183,10 +192,7 @@ const WorkflowVariableBlockComponent = ({
: Type.string}
nodeType={node?.type}
/>
)}
disabled={!isShowAPart}
>
<div>{Item}</div>
</TooltipContent>
</Tooltip>
)
}

View File

@ -0,0 +1,23 @@
import type { WorkflowNodesMap } from '@/app/components/base/prompt-editor/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { extractPluginId } from '@/app/components/workflow/utils/plugin'
import { useProviderContextSelector } from '@/context/provider-context'
export function useLlmModelPluginInstalled(
nodeId: string,
workflowNodesMap: WorkflowNodesMap | undefined,
): boolean {
const node = workflowNodesMap?.[nodeId]
const modelProvider = node?.type === BlockEnum.LLM
? node.modelProvider
: undefined
const modelPluginId = modelProvider ? extractPluginId(modelProvider) : undefined
return useProviderContextSelector((state) => {
if (!modelPluginId)
return true
return state.modelProviders.some(p =>
extractPluginId(p.provider) === modelPluginId,
)
})
}