From a2380c4fd3ed59a5019047362e151336372b489f Mon Sep 17 00:00:00 2001 From: zhsama Date: Thu, 5 Feb 2026 06:06:38 +0800 Subject: [PATCH] fix: ensure sub-graph modal syncs immediately when applying generated code. --- api/core/llm_generator/llm_generator.py | 31 +++++++++++--- .../workflow/nodes/code/use-config.ts | 27 +++++++++++-- .../context-generate-modal/index.tsx | 2 +- .../tool/components/sub-graph-modal/index.tsx | 40 ++++++++++++++++--- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index 773ee8eaeb..370b814cd2 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -14,6 +14,7 @@ from core.llm_generator.context_models import ( ) from core.llm_generator.entities import RuleCodeGeneratePayload, RuleGeneratePayload, RuleStructuredOutputPayload from core.llm_generator.output_models import ( + CodeNodeOutputItem, CodeNodeStructuredOutput, InstructionModifyOutput, SuggestedQuestionsOutput, @@ -479,8 +480,10 @@ class LLMGenerator: model_parameters=model_parameters, ) + response_payload = response.model_dump() + response_payload["outputs"] = cls._format_code_outputs(response.outputs) return { - **response.model_dump(), + **response_payload, "code_language": language, "error": "", } @@ -503,6 +506,20 @@ class LLMGenerator: "error": error, } + @classmethod + def _format_code_outputs(cls, outputs: Sequence[CodeNodeOutputItem]) -> dict[str, dict[str, str]]: + """Normalize code outputs to a stable mapping for frontend consumers. + + The LLM structured output uses an array to satisfy strict-mode schemas, but the + frontend expects a name-to-type mapping for Code node outputs. + """ + mapped: dict[str, dict[str, str]] = {} + for output_item in outputs: + if not output_item.name: + continue + mapped[output_item.name] = {"type": str(output_item.type)} + return mapped + @classmethod def generate_suggested_questions( cls, @@ -557,10 +574,14 @@ class LLMGenerator: completion_params = model_config.get("completion_params", {}) if model_config else {} try: - response = invoke_llm_with_pydantic_model(provider=model_instance.provider, model_schema=model_schema, - model_instance=model_instance, prompt_messages=prompt_messages, - output_model=SuggestedQuestionsOutput, - model_parameters=completion_params) + response = invoke_llm_with_pydantic_model( + provider=model_instance.provider, + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + output_model=SuggestedQuestionsOutput, + model_parameters=completion_params, + ) return {"questions": response.questions, "error": ""} diff --git a/web/app/components/workflow/nodes/code/use-config.ts b/web/app/components/workflow/nodes/code/use-config.ts index 2655dd1d43..17b661326f 100644 --- a/web/app/components/workflow/nodes/code/use-config.ts +++ b/web/app/components/workflow/nodes/code/use-config.ts @@ -1,7 +1,7 @@ import type { Var, Variable } from '../../types' import type { CodeNodeType, OutputVar } from './types' import { produce } from 'immer' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useNodesReadOnly, } from '@/app/components/workflow/hooks' @@ -56,9 +56,28 @@ const useConfig = (id: string, payload: CodeNodeType) => { setInputs, }) - const [outputKeyOrders, setOutputKeyOrders] = useState(() => Object.keys(payload.outputs || {})) + const outputKeyOrdersRef = useRef(Object.keys(payload.outputs || {})) + const outputKeyOrders = (() => { + const outputKeys = inputs.outputs ? Object.keys(inputs.outputs) : [] + if (outputKeys.length === 0) { + if (outputKeyOrdersRef.current.length > 0) + outputKeyOrdersRef.current = [] + return [] as string[] + } + + const nextOutputKeyOrders = outputKeyOrdersRef.current.filter(key => outputKeys.includes(key)) + outputKeys.forEach((key) => { + if (!nextOutputKeyOrders.includes(key)) + nextOutputKeyOrders.push(key) + }) + outputKeyOrdersRef.current = nextOutputKeyOrders + return nextOutputKeyOrders + })() const syncOutputKeyOrders = useCallback((outputs: OutputVar) => { - setOutputKeyOrders(Object.keys(outputs)) + outputKeyOrdersRef.current = Object.keys(outputs) + }, []) + const handleOutputKeyOrdersChange = useCallback((newOutputKeyOrders: string[]) => { + outputKeyOrdersRef.current = newOutputKeyOrders }, []) useEffect(() => { const outputKeys = inputs.outputs ? Object.keys(inputs.outputs) : [] @@ -174,7 +193,7 @@ const useConfig = (id: string, payload: CodeNodeType) => { inputs, setInputs, outputKeyOrders, - onOutputKeyOrdersChange: setOutputKeyOrders, + onOutputKeyOrdersChange: handleOutputKeyOrdersChange, }) const filterVar = useCallback((varPayload: Var) => { diff --git a/web/app/components/workflow/nodes/tool/components/context-generate-modal/index.tsx b/web/app/components/workflow/nodes/tool/components/context-generate-modal/index.tsx index 1e9669f632..2b7f7ce070 100644 --- a/web/app/components/workflow/nodes/tool/components/context-generate-modal/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/context-generate-modal/index.tsx @@ -179,7 +179,7 @@ const ContextGenerateModal = forwardRef(({ outputs: nextOutputs, variables: nextVariables, }, - }) + }, { sync: true }) if (closeOnApply) handleCloseModal() diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx index 497e9b73c2..31a2361e85 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx @@ -48,6 +48,7 @@ const SubGraphModal: FC = (props) => { const workflowNodes = useWorkflowStore(state => state.nodes) const workflowEdges = useReactFlowStore(state => state.edges) const setControlPromptEditorRerenderKey = useWorkflowStore(state => state.setControlPromptEditorRerenderKey) + const setWorkflowNodes = useWorkflowStore(state => state.setNodes) const { handleSyncWorkflowDraft, doSyncWorkflowDraft } = useNodesSyncDraft() const configsMap = useHooksStore(state => state.configsMap) const { getBeforeNodesInSameBranch } = useWorkflow() @@ -134,8 +135,9 @@ const SubGraphModal: FC = (props) => { } }) setNodes(nextNodes) - handleSyncWorkflowDraft() - }, [handleSyncWorkflowDraft, paramKey, reactflowStore, toolNodeId]) + setWorkflowNodes(nextNodes) + handleSyncWorkflowDraft(true) + }, [handleSyncWorkflowDraft, paramKey, reactflowStore, setWorkflowNodes, toolNodeId]) useEffect(() => { if (!toolParam || (toolParam.type && toolParam.type !== VarKindType.nested_node)) @@ -179,7 +181,7 @@ const SubGraphModal: FC = (props) => { const ensureAssembleOutputs = (payload: CodeNodeType) => { const outputs = payload.outputs || {} - if (outputs.result) + if (Object.keys(outputs).length > 0) return payload return { ...payload, @@ -193,6 +195,20 @@ const SubGraphModal: FC = (props) => { } } + const resolveAssembleOutputSelector = (rawSelector: unknown, outputKeys: string[]) => { + if (outputKeys.length === 0) + return null + const normalizedSelector = Array.isArray(rawSelector) + ? (rawSelector[0] === extractorNodeId ? rawSelector.slice(1) : rawSelector) + : [] + const currentKey = normalizedSelector[0] + const fallbackKey = outputKeys.includes('result') ? 'result' : outputKeys[0] + const nextKey = outputKeys.includes(currentKey) ? currentKey : fallbackKey + if (!nextKey || nextKey === currentKey) + return null + return [nextKey, ...normalizedSelector.slice(1)] + } + const userPromptText = isAgentVariant ? getUserPromptText((extractorNodeData.data as LLMNodeType).prompt_template) : '' @@ -223,6 +239,19 @@ const SubGraphModal: FC = (props) => { return node const currentParam = toolData.tool_parameters[paramKey] + const baseNestedConfig = currentParam.nested_node_config ?? nestedNodeConfig + let nextNestedConfig = baseNestedConfig + if (!isAgentVariant) { + const outputKeys = Object.keys((extractorNodeData.data as CodeNodeType).outputs || {}) + const nextSelector = resolveAssembleOutputSelector(baseNestedConfig?.output_selector, outputKeys) + if (nextSelector) { + nextNestedConfig = { + ...baseNestedConfig, + extractor_node_id: baseNestedConfig?.extractor_node_id || extractorNodeId, + output_selector: nextSelector, + } + } + } return { ...node, data: { @@ -233,7 +262,7 @@ const SubGraphModal: FC = (props) => { ...currentParam, type: VarKindType.nested_node, value: nextValue, - nested_node_config: currentParam.nested_node_config ?? nestedNodeConfig, + nested_node_config: nextNestedConfig, }, }, }, @@ -242,8 +271,9 @@ const SubGraphModal: FC = (props) => { return node }) setNodes(nextNodes) + setWorkflowNodes(nextNodes) setControlPromptEditorRerenderKey(Date.now()) - }, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, nestedNodeConfig, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, toolNodeId]) + }, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, nestedNodeConfig, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, setWorkflowNodes, toolNodeId]) return (