From 06f6ded20f321c7af922fa3719d70fe43a8ffa26 Mon Sep 17 00:00:00 2001 From: zhsama Date: Mon, 19 Jan 2026 14:59:08 +0800 Subject: [PATCH] fix: Fix assemble variables insertion in prompt editor --- .../plugins/component-picker-block/index.tsx | 8 +- .../components/base/prompt-editor/types.ts | 2 +- .../_base/components/form-input-item.tsx | 10 +- .../variable/var-reference-vars.tsx | 2 +- .../mixed-variable-text-input/index.tsx | 217 ++++++++++-------- .../components/workflow/nodes/tool/node.tsx | 2 +- 6 files changed, 141 insertions(+), 100 deletions(-) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx index c7ed721dde..8ff0ed5a02 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -14,6 +14,7 @@ import type { } from '../../types' import type { PickerBlockMenuOption } from './menu' import type { AgentNode } from '@/app/components/base/prompt-editor/types' +import type { ValueSelector } from '@/app/components/workflow/types' import { flip, offset, @@ -163,7 +164,7 @@ const ComponentPicker = ({ editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent) }, [editor]) - const handleSelectAssembleVariables = useCallback(() => { + const handleSelectAssembleVariables = useCallback((): ValueSelector | null => { editor.update(() => { const match = checkForTriggerMatch(triggerString, editor) if (!match) @@ -172,8 +173,11 @@ const ComponentPicker = ({ if (needRemove) needRemove.remove() }) - workflowVariableBlock?.onAssembleVariables?.() + const assembleVariables = workflowVariableBlock?.onAssembleVariables?.() + if (assembleVariables && assembleVariables.length) + editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, assembleVariables) handleClose() + return assembleVariables ?? null }, [editor, checkForTriggerMatch, triggerString, workflowVariableBlock, handleClose]) const handleSelectAgent = useCallback((agent: { id: string, title: string }) => { diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts index 81c0baaced..3b4565e5c6 100644 --- a/web/app/components/base/prompt-editor/types.ts +++ b/web/app/components/base/prompt-editor/types.ts @@ -72,7 +72,7 @@ export type WorkflowVariableBlockType = { showManageInputField?: boolean onManageInputField?: () => void showAssembleVariables?: boolean - onAssembleVariables?: () => void + onAssembleVariables?: () => ValueSelector | null } export type AgentNode = { diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx index 661c69c4dc..b75a16a491 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -235,7 +235,15 @@ const FormInputItem: FC = ({ const handleValueChange = (newValue: any, newType?: VarKindType, mentionConfig?: MentionConfig | null) => { const normalizedValue = isNumber ? Number.parseFloat(newValue) : newValue - const resolvedType = newType ?? (varInput?.type === VarKindType.mention ? VarKindType.mention : getVarKindType()) + const assemblePlaceholder = nodeId && variable + ? `{{#${nodeId}_ext_${variable}.result#}}` + : '' + const isAssembleValue = typeof normalizedValue === 'string' + && assemblePlaceholder + && normalizedValue.includes(assemblePlaceholder) + const resolvedType = isAssembleValue + ? VarKindType.mixed + : newType ?? (varInput?.type === VarKindType.mention ? VarKindType.mention : getVarKindType()) const resolvedMentionConfig = resolvedType === VarKindType.mention ? (mentionConfig ?? varInput?.mention_config ?? { extractor_node_id: '', diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 16901ae37a..708ef3a77b 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -256,7 +256,7 @@ type Props = { showManageInputField?: boolean onManageInputField?: () => void showAssembleVariables?: boolean - onAssembleVariables?: () => void + onAssembleVariables?: () => ValueSelector | null autoFocus?: boolean preferSchemaType?: boolean } diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx index 5283583bf9..33eb6ae3d8 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -38,7 +38,8 @@ import Placeholder from './placeholder' * Matches agent context variable syntax: {{@nodeId.context@}} * Example: {{@agent-123.context@}} -> captures "agent-123" */ -const AGENT_CONTEXT_VAR_PATTERN = /\{\{[@#]([^.@#]+)\.context[@#]\}\}/g +const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g + const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string) => { if (!toolNodeId || !paramKey) return '' @@ -179,7 +180,7 @@ const MixedVariableTextInput = ({ const isAssembleValue = useMemo(() => { if (!assemblePlaceholder) return false - return value.trim() === assemblePlaceholder + return value.includes(assemblePlaceholder) }, [assemblePlaceholder, value]) const contextNodeIds = useMemo(() => { @@ -204,6 +205,99 @@ const MixedVariableTextInput = ({ return `${toolNodeId}_ext_${paramKey}` }, [paramKey, toolNodeId]) + const ensureExtractorNode = useCallback((payload: { + extractorNodeId: string + nodeType: BlockEnum + data: Partial + }) => { + if (!toolNodeId) + return null + const defaultValue = nodesMetaDataMap?.[payload.nodeType]?.defaultValue as Partial | undefined + if (!defaultValue) + return null + + const { getNodes, setNodes } = reactFlowStore.getState() + const currentNodes = getNodes() + const existingNode = currentNodes.find(node => node.id === payload.extractorNodeId) + const shouldReplace = existingNode && existingNode.data.type !== payload.nodeType + if (!existingNode || shouldReplace) { + const nextNodes = shouldReplace + ? currentNodes.filter(node => node.id !== payload.extractorNodeId) + : currentNodes + const { newNode } = generateNewNode({ + id: payload.extractorNodeId, + type: getNodeCustomTypeByNodeDataType(payload.nodeType), + data: { + ...defaultValue, + ...payload.data, + type: payload.nodeType, + title: defaultValue?.title ?? '', + desc: defaultValue.desc || '', + parent_node_id: toolNodeId, + }, + position: { + x: 0, + y: 0, + }, + hidden: true, + }) + setNodes([...nextNodes, newNode]) + handleSyncWorkflowDraft() + return newNode + } + + return existingNode + }, [handleSyncWorkflowDraft, nodesMetaDataMap, reactFlowStore, toolNodeId]) + + const ensureAssembleExtractorNode = useCallback(() => { + if (!assembleExtractorNodeId) + return '' + const extractorNode = ensureExtractorNode({ + extractorNodeId: assembleExtractorNodeId, + nodeType: BlockEnum.Code, + data: { + outputs: { + result: { + type: VarType.string, + children: null, + }, + }, + }, + }) + if (!extractorNode) + return '' + if (extractorNode.data.type !== BlockEnum.Code) + return assembleExtractorNodeId + + const outputs = (extractorNode.data as CodeNodeType).outputs || {} + const resultOutput = outputs.result + if (!resultOutput || resultOutput.type !== VarType.string) { + const { getNodes, setNodes } = reactFlowStore.getState() + const currentNodes = getNodes() + const nextOutputs = { + ...outputs, + result: { + type: VarType.string, + children: null, + }, + } + setNodes(currentNodes.map((node) => { + if (node.id !== assembleExtractorNodeId) + return node + return { + ...node, + data: { + ...node.data, + outputs: nextOutputs, + }, + } + })) + handleSyncWorkflowDraft() + } + + return assembleExtractorNodeId + }, [assembleExtractorNodeId, ensureExtractorNode, handleSyncWorkflowDraft, reactFlowStore]) + type DetectedAgent = { nodeId: string name: string @@ -315,7 +409,7 @@ const MixedVariableTextInput = ({ return const escapedAgentId = detectedAgent.nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const leadingPattern = new RegExp(`^\\{\\{[@#]${escapedAgentId}\\.context[@#]\\}\\}`) + const leadingPattern = new RegExp(`^\\{\\{@${escapedAgentId}\\.context@\\}\\}`) const promptText = text.replace(leadingPattern, '') const extractorNodeId = `${toolNodeId}_ext_${paramKey}` @@ -385,45 +479,25 @@ const MixedVariableTextInput = ({ const newValue = `{{@${agent.id}.context@}}${valueWithoutTrigger}` if (toolNodeId && paramKey) { - const extractorNodeId = `${toolNodeId}_ext_${paramKey}` - const defaultValue = nodesMetaDataMap?.[BlockEnum.LLM]?.defaultValue as Partial | undefined - const { getNodes, setNodes } = reactFlowStore.getState() - const nodes = getNodes() - const hasExtractorNode = nodes.some(node => node.id === extractorNodeId) - - if (!hasExtractorNode && defaultValue) { - const { newNode } = generateNewNode({ - id: extractorNodeId, - type: getNodeCustomTypeByNodeDataType(BlockEnum.LLM), - data: { - ...defaultValue, - type: BlockEnum.LLM, - title: defaultValue?.title ?? '', - desc: defaultValue.desc || '', - parent_node_id: toolNodeId, - structured_output_enabled: true, - structured_output: { - schema: { - type: Type.object, - properties: { - [paramKey]: { - type: Type.string, - }, + ensureExtractorNode({ + extractorNodeId: `${toolNodeId}_ext_${paramKey}`, + nodeType: BlockEnum.LLM, + data: { + structured_output_enabled: true, + structured_output: { + schema: { + type: Type.object, + properties: { + [paramKey]: { + type: Type.string, }, - required: [paramKey], - additionalProperties: false, }, + required: [paramKey], + additionalProperties: false, }, }, - position: { - x: 0, - y: 0, - }, - hidden: true, - }) - setNodes([...nodes, newNode]) - handleSyncWorkflowDraft() - } + }, + }) } const mentionConfigWithOutputSelector: MentionConfig = { @@ -434,71 +508,26 @@ const MixedVariableTextInput = ({ onChange(newValue, VarKindTypeEnum.mention, mentionConfigWithOutputSelector) syncExtractorPromptFromText(newValue) setControlPromptEditorRerenderKey(Date.now()) - }, [handleSyncWorkflowDraft, nodesMetaDataMap, onChange, paramKey, reactFlowStore, setControlPromptEditorRerenderKey, syncExtractorPromptFromText, toolNodeId, value]) + }, [ensureExtractorNode, onChange, paramKey, setControlPromptEditorRerenderKey, syncExtractorPromptFromText, toolNodeId, value]) - const handleAssembleSelect = useCallback(() => { - if (!onChange || !toolNodeId || !paramKey || !assemblePlaceholder) - return - - const defaultValue = nodesMetaDataMap?.[BlockEnum.Code]?.defaultValue as Partial | undefined - if (!defaultValue) - return - - const extractorNodeId = `${toolNodeId}_ext_${paramKey}` - const { getNodes, setNodes } = reactFlowStore.getState() - const currentNodes = getNodes() - const existingNode = currentNodes.find(node => node.id === extractorNodeId) - const shouldReplace = existingNode && existingNode.data.type !== BlockEnum.Code - const shouldCreate = !existingNode || shouldReplace - - if (shouldCreate) { - const nextNodes = shouldReplace - ? currentNodes.filter(node => node.id !== extractorNodeId) - : currentNodes - const { newNode } = generateNewNode({ - id: extractorNodeId, - type: getNodeCustomTypeByNodeDataType(BlockEnum.Code), - data: { - ...defaultValue, - type: BlockEnum.Code, - title: defaultValue?.title ?? '', - desc: defaultValue?.desc || '', - parent_node_id: toolNodeId, - outputs: { - result: { - type: VarType.string, - children: null, - }, - }, - }, - position: { - x: 0, - y: 0, - }, - hidden: true, - }) - setNodes([...nextNodes, newNode]) - handleSyncWorkflowDraft() - } - - const mentionConfigWithOutputSelector: MentionConfig = { - ...DEFAULT_MENTION_CONFIG, - extractor_node_id: extractorNodeId, - output_selector: ['result'], - } - onChange(assemblePlaceholder, VarKindTypeEnum.mention, mentionConfigWithOutputSelector) + const handleAssembleSelect = useCallback((): ValueSelector | null => { + if (!toolNodeId || !paramKey || !assemblePlaceholder) + return null + const extractorNodeId = assembleExtractorNodeId || `${toolNodeId}_ext_${paramKey}` + ensureAssembleExtractorNode() + onChange?.(assemblePlaceholder, VarKindTypeEnum.mixed, null) setControlPromptEditorRerenderKey(Date.now()) - }, [assemblePlaceholder, handleSyncWorkflowDraft, nodesMetaDataMap, onChange, paramKey, reactFlowStore, setControlPromptEditorRerenderKey, toolNodeId]) + return [extractorNodeId, 'result'] + }, [assembleExtractorNodeId, assemblePlaceholder, ensureAssembleExtractorNode, onChange, paramKey, setControlPromptEditorRerenderKey, toolNodeId]) const handleAssembleRemove = useCallback(() => { if (!onChange || !assemblePlaceholder) return - const nextValue = value.replace(assemblePlaceholder, '') removeExtractorNode() - onChange(nextValue, VarKindTypeEnum.mixed, null) + onChange('', VarKindTypeEnum.mixed, null) setControlPromptEditorRerenderKey(Date.now()) - }, [assemblePlaceholder, onChange, removeExtractorNode, setControlPromptEditorRerenderKey, value]) + }, [assemblePlaceholder, onChange, removeExtractorNode, setControlPromptEditorRerenderKey]) const handleOpenSubGraphModal = useCallback(() => { setIsSubGraphModalOpen(true) diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index 5c888b66a3..555bc1218f 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -20,7 +20,7 @@ import { useStrategyProviders } from '@/service/use-strategy' import { cn } from '@/utils/classnames' import { VarType } from './types' -const AGENT_CONTEXT_VAR_PATTERN = /\{\{[@#]([^.@#]+)\.context[@#]\}\}/g +const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g type AgentCheckValidContext = { provider?: StrategyPluginDetail strategy?: StrategyDetail