From cc373b286478fc620768c17ec873476a917d1cfb Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 11:45:41 +0800 Subject: [PATCH 01/21] fix(web): loop/iteration edge --- .../Workflow/components/PortClickHandler.tsx | 3 + .../views/Workflow/hooks/useWorkflowGraph.ts | 207 +++++++++--------- 2 files changed, 108 insertions(+), 102 deletions(-) diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index 31693722..7e2350ee 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -34,9 +34,12 @@ const PortClickHandler: React.FC = ({ graph }) => { }; window.addEventListener('port:click', handlePortClick as EventListener); + const handleBlankClick = () => handlePopoverClose(); + window.addEventListener('blank:click', handleBlankClick); return () => { window.removeEventListener('port:click', handlePortClick as EventListener); + window.removeEventListener('blank:click', handleBlankClick); }; }, []); diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 45400362..4c2ab44e 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -94,10 +94,10 @@ export const useWorkflowGraph = ({ const { message } = App.useApp(); const { t } = useTranslation() const { user } = useUser(); - + // Refs const graphRef = useRef(); - + // State const [selectedNode, setSelectedNode] = useState(null); const [zoomLevel, setZoomLevel] = useState(1); @@ -134,7 +134,7 @@ export const useWorkflowGraph = ({ useEffect(() => { initWorkflow() }, [config, graphRef.current]) - + /** * Initialize workflow graph with nodes and edges from configuration */ @@ -211,7 +211,7 @@ export const useWorkflowGraph = ({ id, type, name, - data: { ...node, ...nodeLibraryConfig}, + data: { ...node, ...nodeLibraryConfig }, ...position, } @@ -221,11 +221,11 @@ export const useWorkflowGraph = ({ if (w) nodeConfig.width = w as number; if (h) nodeConfig.height = h as number; } - + // Generate ports dynamically for if-else node based on cases if (type === 'if-else' && config.cases && Array.isArray(config.cases)) { const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE - + const portItems: PortMetadata[] = [ defaultPortItems[0], ]; @@ -240,24 +240,24 @@ export const useWorkflowGraph = ({ }, }); } - + nodeConfig.ports = { groups: defaultAbsolutePortGroups, items: portItems }; - + nodeConfig.height = calcConditionNodeTotalHeight(config.cases); } - + // Generate ports dynamically for question-classifier node based on categories if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) { const categoryCount = config.categories.length; const newHeight = conditionNodeHeight + (categoryCount - 2) * conditionNodeItemHeight; - + const portItems: PortMetadata[] = [ defaultPortItems[0] ]; - + // Add category ports config.categories.forEach((_category: any, index: number) => { portItems.push({ @@ -269,15 +269,15 @@ export const useWorkflowGraph = ({ }, }); }); - + nodeConfig.ports = { groups: defaultAbsolutePortGroups, items: portItems }; - + nodeConfig.height = newHeight; } - + // Check error_handle.method config for http-request node if (type === 'http-request' && (nodeConfig as any).error_handle?.method === 'branch') { nodeConfig.ports = { @@ -294,21 +294,22 @@ export const useWorkflowGraph = ({ x: nodeWidth, y: portItemArgsY + portItemArgsY, }, - id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}} + id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs } } + } ] }; } - + return nodeConfig }) - + // Separate parent nodes and child nodes const parentNodes = nodeList.filter(node => !node.data.cycle) const childNodes = nodeList.filter(node => node.data.cycle) - + // Add parent nodes first graphRef.current?.addNodes(parentNodes) - + // Then process child nodes, use addChild to add to corresponding parent node childNodes.forEach(childNode => { const cycleId = childNode.data.cycle @@ -322,14 +323,14 @@ export const useWorkflowGraph = ({ } } }) - + // Adjust parent node size to fit child nodes setTimeout(() => { const parentNodesWithChildren = parentNodes.filter(parentNode => { const parentId = parentNode.data.id return childNodes.some(child => child.data.cycle === parentId) }) - + parentNodesWithChildren.forEach(parentNodeConfig => { const parentNode = graphRef.current?.getCellById(parentNodeConfig.data.id) if (parentNode) { @@ -340,18 +341,18 @@ export const useWorkflowGraph = ({ const minY = Math.min(...childBounds.map(b => b.y)) const maxX = Math.max(...childBounds.map(b => b.x + b.width)) const maxY = Math.max(...childBounds.map(b => b.y + b.height)) - + const padding = 24 const headerHeight = 50 const parentBBox = parentNode.getBBox() - + const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2) const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight) console.log('newWidth', newHeight, newWidth) - + parentNode.prop('size', { width: newWidth, height: newHeight }) - + // Update x position of right group ports const ports = (parentNode as Node).getPorts() ports.forEach(port => { @@ -371,7 +372,7 @@ export const useWorkflowGraph = ({ const sourceCell = graphRef.current?.getCellById(e.source); const sourceType = sourceCell?.getData()?.type; const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else'; - + if (isMultiPortNode) { // Multi-port nodes need to compare source, target and label return e.source === edge.source && e.target === edge.target && e.label === edge.label; @@ -381,18 +382,18 @@ export const useWorkflowGraph = ({ } }) === index; }); - + const edgeList = uniqueEdges.map(edge => { const { source, target, label } = edge const sourceCell = graphRef.current?.getCellById(source) const targetCell = graphRef.current?.getCellById(target) - + if (sourceCell && targetCell) { const sourcePorts = (sourceCell as Node).getPorts() const targetPorts = (targetCell as Node).getPorts() - + let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right'; - + // If if-else node has label, match corresponding port by label if (sourceCell.getData()?.type === 'if-else' && label) { // Find matching port ID @@ -401,7 +402,7 @@ export const useWorkflowGraph = ({ sourcePort = label; } } - + // If question-classifier node has label, match corresponding port by label if (sourceCell.getData()?.type === 'question-classifier' && label) { const matchingPort = sourcePorts.find((port: any) => port.id === label); @@ -409,7 +410,7 @@ export const useWorkflowGraph = ({ sourcePort = label; } } - + // If http-request node has label, match corresponding port by label if (sourceCell.getData()?.type === 'http-request' && label) { const matchingPort = sourcePorts.find((port: any) => port.id === label); @@ -417,7 +418,7 @@ export const useWorkflowGraph = ({ sourcePort = label; } } - + const edgeConfig = { source: { cell: sourceCell.id, @@ -438,15 +439,23 @@ export const useWorkflowGraph = ({ }) graphRef.current.addEdges(edgeList.filter(vo => vo !== null)) } - + // Initialize after completion, display nodes in visible area if (nodes.length > 0 || edges.length > 0) { setTimeout(() => { if (graphRef.current) { graphRef.current.centerContent() - // graphRef.current.getNodes().forEach(node => node.toFront()); + graphRef.current.getNodes().forEach(node => { + if (!node.getData()?.cycle) node.toFront(); + }); // Bring edges to front first, then child nodes above edges; parent nodes stay behind - graphRef.current.getEdges().forEach(edge => edge.toFront()); + graphRef.current.getEdges().forEach(edge => { + const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId()); + const targetCell = graphRef.current?.getCellById(edge.getTargetCellId()); + if (sourceCell?.getData()?.cycle || targetCell?.getData()?.cycle) { + edge.toFront(); + } + }); graphRef.current.getNodes().forEach(node => { if (node.getData()?.cycle) node.toFront(); }); @@ -575,6 +584,7 @@ export const useWorkflowGraph = ({ clearEdgeSelect(); graphRef.current?.cleanSelection(); setSelectedNode(null); + window.dispatchEvent(new CustomEvent('blank:click')); }; /** * Handle canvas scale/zoom event @@ -595,26 +605,26 @@ export const useWorkflowGraph = ({ // Get parent node and child node bounding boxes const parentBBox = parentNode.getBBox(); const childBBox = node.getBBox(); - + // Calculate parent node padding const padding = 24; const headerHeight = 50; - + // Calculate minimum and maximum positions allowed for child node const minX = parentBBox.x + padding; const minY = parentBBox.y + padding + headerHeight; const maxX = parentBBox.x + parentBBox.width - padding - childBBox.width; const maxY = parentBBox.y + parentBBox.height - padding - childBBox.height; - + // Restrict child node movement within parent node let newX = childBBox.x; let newY = childBBox.y; - + if (newX < minX) newX = minX; if (newY < minY) newY = minY; if (newX > maxX) newX = maxX; if (newY > maxY) newY = maxY; - + // If child node position is restricted, update its position if (newX !== childBBox.x || newY !== childBBox.y) { node.setPosition(newX, newY); @@ -706,7 +716,7 @@ export const useWorkflowGraph = ({ } // Add child node to deletion list cells.push(nodeToDelete); - } + } // Check if it's LoopNode, IterationNode or SubGraphNode else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') { // Find all child nodes with cycle equal to current node id @@ -718,7 +728,7 @@ export const useWorkflowGraph = ({ }); // Add parent node to deletion list cells.push(nodeToDelete); - } + } // Normal node else { cells.push(nodeToDelete); @@ -726,7 +736,7 @@ export const useWorkflowGraph = ({ }); blankClick(); } - + // Delete all collected nodes and edges if (cells.length > 0) { graphRef.current?.removeCells(cells); @@ -873,19 +883,19 @@ export const useWorkflowGraph = ({ const targetGroup = targetMagnet ? getPortGroup(targetMagnet) : null; if (sourceGroup === 'left' || targetGroup === 'right') return false; - + // Node cannot connect to itself if (sourceCell?.id === targetCell?.id) return false; const targetType = targetCell?.getData()?.type; - + // Start node cannot be connection target if (targetType === 'start') return false; - + // Get source node and target node parent IDs const sourceParentId = sourceCell?.getData()?.cycle; const targetParentId = targetCell?.getData()?.cycle; - + // Validate parent-child relationship: // 1. If both nodes have parent IDs, they must be same to connect // 2. If both have no parent ID, can connect normally @@ -975,7 +985,6 @@ export const useWorkflowGraph = ({ if (!port) return; const portData = node.getPort(port); if (portData?.group !== 'right') return; - node.toFront(); node.setPortProp(port, 'attrs/body/opacity', 0); node.setPortProp(port, 'attrs/hoverBody/opacity', 1); node.setPortProp(port, 'attrs/label/opacity', 1); @@ -1028,33 +1037,28 @@ export const useWorkflowGraph = ({ graphRef.current.on('scale', scaleEvent); // Listen to node move event graphRef.current.on('node:moved', nodeMoved); - // When parent (isGroup) node position changes, move children with it - graphRef.current.on('node:change:position', ({ node, current, previous }: { node: Node; current: { x: number; y: number }; previous: { x: number; y: number } }) => { - - if (!(node.getData()?.type === 'iteration' && node.getData()?.type === 'loop') || !current || !previous) return; - const dx = current.x - previous.x; - const dy = current.y - previous.y; - const parentId = node.getData()?.id || node.id; - graphRef.current?.getNodes().forEach(child => { - if (child.getData()?.cycle === parentId) { - const cp = child.getPosition(); - child.setPosition(cp.x + dx, cp.y + dy, { silent: true }); - } - }); - }); graphRef.current.on('node:removed', blankClick) // When edge connected, bring connected nodes' ports to front - graphRef.current.on('edge:connected', ({ isNew, edge }) => { + graphRef.current.on('edge:connected', ({ isNew }) => { // Bring edge to front first, then bring child nodes above edges // Parent (loop/iteration) nodes stay behind to avoid covering edges - edge.toFront(); - graphRef.current?.getNodes().forEach(node => { - if (node.getData()?.cycle) node.toFront(); - }); // Reset any port hover state left from dragging if (isNew) { graphRef.current?.getNodes().forEach(node => { + if (!node.getData()?.cycle) node.toFront(); + }); + graphRef.current?.getEdges().forEach(edge => { + const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId()); + const targetCell = graphRef.current?.getCellById(edge.getTargetCellId()); + if (sourceCell?.getData()?.cycle || targetCell?.getData()?.cycle) { + edge.toFront(); + } + }); + graphRef.current?.getNodes().forEach(node => { + graphRef.current?.getNodes().forEach(node => { + if (node.getData()?.cycle) node.toFront(); + }); node.getPorts().filter(p => p.group === 'right').forEach(p => { node.setPortProp(p.id!, 'attrs/body/opacity', 1); node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0); @@ -1100,7 +1104,6 @@ export const useWorkflowGraph = ({ // Enter new if (found) { const { node, portId } = found; - node.toFront(); node.setPortProp(portId, 'attrs/body/opacity', 0); node.setPortProp(portId, 'attrs/hoverBody/opacity', 1); node.setPortProp(portId, 'attrs/label/opacity', 1); @@ -1173,7 +1176,7 @@ export const useWorkflowGraph = ({ if (!graph) return; const point = graphRef.current.clientToLocal(event.clientX, event.clientY); - + // Get original config from node library to avoid config data chaining let nodeLibraryConfig = [...nodeLibrary] .flatMap(category => category.nodes) @@ -1326,11 +1329,11 @@ export const useWorkflowGraph = ({ const sourcePortId = edge.getSourcePortId(); // Filter invalid edges: source or target node doesn't exist, or is add-node type - if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id || - sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') { + if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id || + sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') { return null; } - + // If if-else node right port connection, add label if (sourceCell?.getData()?.type === 'if-else' && sourcePortId?.startsWith('CASE')) { return { @@ -1339,7 +1342,7 @@ export const useWorkflowGraph = ({ label: sourcePortId, }; } - + // If question-classifier node right port connection, add label if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) { return { @@ -1348,7 +1351,7 @@ export const useWorkflowGraph = ({ label: sourcePortId, }; } - + // If http-request node right port connection, add label if (sourceCell?.getData()?.type === 'http-request') { if (sourcePortId === 'ERROR') { @@ -1365,40 +1368,40 @@ export const useWorkflowGraph = ({ }; } } - + return { source: sourceCell?.getData().id, target: targetCell?.getData().id, }; }) - .filter(edge => edge !== null) - .filter((edge, index, arr) => { - // Deduplication: For if-else and question-classifier nodes, different ports can connect to same node - return arr.findIndex(e => { - if (!e || !edge) return false; - const sourceCell = graphRef.current?.getCellById(e.source); - const sourceType = sourceCell?.getData()?.type; - const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else'; - - if (isMultiPortNode) { - // Multi-port nodes need to compare source, target and label - return e.source === edge.source && e.target === edge.target && e.label === edge.label; - } else { - // Other nodes only compare source and target - return e.source === edge.source && e.target === edge.target; - } - }) === index; - }), + .filter(edge => edge !== null) + .filter((edge, index, arr) => { + // Deduplication: For if-else and question-classifier nodes, different ports can connect to same node + return arr.findIndex(e => { + if (!e || !edge) return false; + const sourceCell = graphRef.current?.getCellById(e.source); + const sourceType = sourceCell?.getData()?.type; + const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else'; + + if (isMultiPortNode) { + // Multi-port nodes need to compare source, target and label + return e.source === edge.source && e.target === edge.target && e.label === edge.label; + } else { + // Other nodes only compare source and target + return e.source === edge.source && e.target === edge.target; + } + }) === index; + }), } saveWorkflowConfig(config.app_id, params as WorkflowConfig) - .then((res) => { - if (flag) { - message.success({ content: t('common.saveSuccess'), duration: 1 }) - } - resolve(res) - }).catch(error => { - reject(error) - }) + .then((res) => { + if (flag) { + message.success({ content: t('common.saveSuccess'), duration: 1 }) + } + resolve(res) + }).catch(error => { + reject(error) + }) }) } From 62fb6c79a0dd5e5e31137dd85bbcd4d3509eb89e Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 11:47:23 +0800 Subject: [PATCH 02/21] fix(web): change pdf enhancement method init value --- web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx index 1ec66004..7d32796d 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx @@ -94,7 +94,7 @@ const CreateDataset = () => { const [processingMethod, setProcessingMethod] = useState('directBlock'); const [parameterSettings, setParameterSettings] = useState('defaultSettings'); const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState(true); - const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState('deepdoc'); + const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState('mineru'); const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt','png','jpg','mp3','mp4','mov','wav'] const steps = useMemo( () => [ From 807ddce5cd7ee07f4a184b468176d1da937fe812 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 11:50:03 +0800 Subject: [PATCH 03/21] fix(web): remove editor variable space --- .../views/Workflow/components/Editor/plugin/CommandPlugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/Workflow/components/Editor/plugin/CommandPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/CommandPlugin.tsx index d929f18c..6d31fc24 100644 --- a/web/src/views/Workflow/components/Editor/plugin/CommandPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/CommandPlugin.tsx @@ -50,7 +50,7 @@ const CommandPlugin = () => { // Create and insert the variable node const tagNode = $createVariableNode(payload.data); - const spaceNode = $createTextNode(' '); + const spaceNode = $createTextNode(''); anchorNode.insertAfter(tagNode); tagNode.insertAfter(spaceNode); From 1503b242eabdc4ff6485419e10189c50cb663a58 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 12:32:24 +0800 Subject: [PATCH 04/21] fix(web): editor init --- .../Workflow/components/Editor/index.tsx | 4 +-- .../Editor/plugin/CharacterCountPlugin.tsx | 27 +++---------------- .../Editor/plugin/InitialValuePlugin.tsx | 20 ++++++++++---- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index a631c9a3..52a4f23e 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -149,8 +149,8 @@ const Editor: FC =({ - - + + diff --git a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx index 2a826fd0..ee9f4b89 100644 --- a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx @@ -1,41 +1,22 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { $getRoot, $isParagraphNode } from 'lexical'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $isVariableNode } from '../nodes/VariableNode'; - -const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => { +const CharacterCountPlugin = ({ setCount }: { setCount: (count: number) => void }) => { const [editor] = useLexicalComposerContext(); - const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; useEffect(() => { return editor.registerUpdateListener(({ editorState, tags }) => { if (tags.has('programmatic')) return; editorState.read(() => { const root = $getRoot(); - let serializedContent = ''; - - // Traverse all nodes and serialize properly const paragraphs: string[] = []; root.getChildren().forEach(child => { if ($isParagraphNode(child)) { - let paragraphContent = ''; - child.getChildren().forEach(node => { - if ($isVariableNode(node)) { - paragraphContent += node.getTextContent(); - } else { - paragraphContent += node.getTextContent(); - } - }); - paragraphs.push(paragraphContent); + paragraphs.push(child.getChildren().map(n => n.getTextContent()).join('')); } }); - - serializedContent = paragraphs.join('\n'); - - setCount(serializedContent.length); - onChangeRef.current?.(serializedContent); + setCount(paragraphs.join('\n').length); }); }); }, [editor, setCount]); diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index 8b1685ae..cbdff244 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useRef } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; +import { $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode } from 'lexical'; import { $createVariableNode } from '../nodes/VariableNode'; import { type Suggestion } from '../plugin/AutocompletePlugin' @@ -14,24 +14,34 @@ import { type Suggestion } from '../plugin/AutocompletePlugin' interface InitialValuePluginProps { value: string; options?: Suggestion[]; + onChange?: (value: string) => void; } -const InitialValuePlugin: React.FC = ({ value, options = [] }) => { +const InitialValuePlugin: React.FC = ({ value, options = [], onChange }) => { const [editor] = useLexicalComposerContext(); const prevValueRef = useRef(''); const isUserInputRef = useRef(false); const optionsRef = useRef(options); optionsRef.current = options; + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; useEffect(() => { return editor.registerUpdateListener(({ editorState, tags }) => { if (tags.has('programmatic')) return; editorState.read(() => { const root = $getRoot(); - const textContent = root.getTextContent(); - if (textContent !== prevValueRef.current) { + const paragraphs: string[] = []; + root.getChildren().forEach(child => { + if ($isParagraphNode(child)) { + paragraphs.push(child.getChildren().map(n => n.getTextContent()).join('')); + } + }); + const text = paragraphs.join('\n'); + if (text !== prevValueRef.current) { isUserInputRef.current = true; - prevValueRef.current = textContent; + prevValueRef.current = text; + onChangeRef.current?.(text); } }); }); From b7c1ce261b9886ad8726411a3241afb60c00cd17 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 13:43:47 +0800 Subject: [PATCH 05/21] fix(web): remove tooltip --- web/src/views/UserMemoryDetail/components/WordCloud.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/web/src/views/UserMemoryDetail/components/WordCloud.tsx b/web/src/views/UserMemoryDetail/components/WordCloud.tsx index ff914bb8..d2d9d586 100644 --- a/web/src/views/UserMemoryDetail/components/WordCloud.tsx +++ b/web/src/views/UserMemoryDetail/components/WordCloud.tsx @@ -89,14 +89,6 @@ const WordCloud: FC = () => { return { color: ['#155EEF'], - tooltip: { - trigger: 'item', - formatter: (params: any) => { - const dataIndex = params.dataIndex - const item = radarData[dataIndex] - return `${item.name}
${item.percentage.toFixed(1)}%` - } - }, radar: { indicator: radarData.map(item => ({ name: t(`statementDetail.${item.name}`), From 70aab94fc3ead743193e7ee7070649b50f40bacb Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 15:00:49 +0800 Subject: [PATCH 06/21] feat(knowledge): support graph retrieval type with dynamic API key selection --- api/app/controllers/chunk_controller.py | 17 +++-- api/app/core/rag/nlp/search.py | 29 +++++++-- api/app/core/workflow/nodes/knowledge/node.py | 64 ++++++++++++++----- 3 files changed, 80 insertions(+), 30 deletions(-) diff --git a/api/app/controllers/chunk_controller.py b/api/app/controllers/chunk_controller.py index 988aa706..b5c0a5ae 100644 --- a/api/app/controllers/chunk_controller.py +++ b/api/app/controllers/chunk_controller.py @@ -23,6 +23,7 @@ from app.models.user_model import User from app.schemas import chunk_schema from app.schemas.response_schema import ApiResponse from app.services import knowledge_service, document_service, file_service, knowledgeshare_service +from app.services.model_service import ModelApiKeyService # Obtain a dedicated API logger api_logger = get_api_logger() @@ -460,18 +461,20 @@ async def retrieve_chunks( if retrieve_data.retrieve_type == chunk_schema.RetrieveType.Graph: kb_ids = [str(kb_id) for kb_id in private_kb_ids] workspace_ids = [str(workspace_id) for workspace_id in private_workspace_ids] + llm_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.llm_id) + emb_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.embedding_id) # Prepare to configure chat_mdl、embedding_model、vision_model information chat_model = Base( - key=db_knowledge.llm.api_keys[0].api_key, - model_name=db_knowledge.llm.api_keys[0].model_name, - base_url=db_knowledge.llm.api_keys[0].api_base + key=llm_key.api_key, + model_name=llm_key.model_name, + base_url=llm_key.api_base ) embedding_model = OpenAIEmbed( - key=db_knowledge.embedding.api_keys[0].api_key, - model_name=db_knowledge.embedding.api_keys[0].model_name, - base_url=db_knowledge.embedding.api_keys[0].api_base + key=emb_key.api_key, + model_name=emb_key.model_name, + base_url=emb_key.api_base ) - doc = kg_retriever.retrieval(question=retrieve_data.query, workspace_ids=workspace_ids, kb_ids= kb_ids, emb_mdl=embedding_model, llm=chat_model) + doc = kg_retriever.retrieval(question=retrieve_data.query, workspace_ids=workspace_ids, kb_ids=kb_ids, emb_mdl=embedding_model, llm=chat_model) if doc: rs.insert(0, doc) return success(data=jsonable_encoder(rs), msg="retrieval successful") \ No newline at end of file diff --git a/api/app/core/rag/nlp/search.py b/api/app/core/rag/nlp/search.py index db93bc48..1a84b8a7 100644 --- a/api/app/core/rag/nlp/search.py +++ b/api/app/core/rag/nlp/search.py @@ -28,6 +28,7 @@ from app.core.rag.common.float_utils import get_float from app.core.rag.common.constants import PAGERANK_FLD, TAG_FLD from app.core.rag.llm.chat_model import Base from app.core.rag.llm.embedding_model import OpenAIEmbed +from app.services.model_service import ModelApiKeyService import logging logger = logging.getLogger(__name__) @@ -198,16 +199,18 @@ def _retrieve_for_knowledge( workspace_ids.append(str(db_knowledge.workspace_id)) if not chat_model: + llm_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.llm_id) chat_model = Base( - key=db_knowledge.llm.api_keys[0].api_key, - model_name=db_knowledge.llm.api_keys[0].model_name, - base_url=db_knowledge.llm.api_keys[0].api_base, + key=llm_key.api_key, + model_name=llm_key.model_name, + base_url=llm_key.api_base, ) if not embedding_model: + emb_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.embedding_id) embedding_model = OpenAIEmbed( - key=db_knowledge.embedding.api_keys[0].api_key, - model_name=db_knowledge.embedding.api_keys[0].model_name, - base_url=db_knowledge.embedding.api_keys[0].api_base, + key=emb_key.api_key, + model_name=emb_key.model_name, + base_url=emb_key.api_base, ) vector_service = ElasticSearchVectorFactory().init_vector(knowledge=db_knowledge) @@ -248,6 +251,20 @@ def _retrieve_for_knowledge( seen_ids.add(doc.metadata["doc_id"]) unique_rs.append(doc) rs = unique_rs + if kb_config["retrieve_type"] == "graph": + try: + from app.core.rag.common.settings import kg_retriever + graph_doc = kg_retriever.retrieval( + question=kb_config["query"], + workspace_ids=[str(db_knowledge.workspace_id)], + kb_ids=[str(db_knowledge.id)], + emb_mdl=embedding_model, + llm=chat_model, + ) + if graph_doc: + rs.insert(0, graph_doc) + except Exception as graph_error: + logger.warning(f"Graph retrieval failed for kb {db_knowledge.id}: {graph_error}") results.extend(rs) return results, chat_model, embedding_model diff --git a/api/app/core/workflow/nodes/knowledge/node.py b/api/app/core/workflow/nodes/knowledge/node.py index 97fa86cb..29e46902 100644 --- a/api/app/core/workflow/nodes/knowledge/node.py +++ b/api/app/core/workflow/nodes/knowledge/node.py @@ -8,6 +8,8 @@ from langchain_core.documents import Document from app.core.error_codes import BizCode from app.core.exceptions import BusinessException from app.core.models import RedBearRerank, RedBearModelConfig +from app.core.rag.llm.chat_model import Base +from app.core.rag.llm.embedding_model import OpenAIEmbed from app.core.rag.models.chunk import DocumentChunk from app.core.rag.vdb.elasticsearch.elasticsearch_vector import ElasticSearchVectorFactory from app.core.workflow.engine.state_manager import WorkflowState @@ -39,8 +41,9 @@ class KnowledgeRetrievalNode(BaseNode): if isinstance(business_result, dict) and "chunks" in business_result: return business_result["chunks"] return business_result - - def _extract_citations(self, business_result: Any) -> list: + + @staticmethod + def _extract_citations(business_result: Any) -> list: if isinstance(business_result, dict): return business_result.get("citations", []) return [] @@ -230,23 +233,23 @@ class KnowledgeRetrievalNode(BaseNode): } ) ) - case RetrieveType.HYBRID: + case RetrieveType.HYBRID | RetrieveType.Graph: rs1_task = asyncio.to_thread( - vector_service.search_by_vector, **{ - "query": query, - "top_k": kb_config.top_k, - "indices": indices, - "score_threshold": kb_config.vector_similarity_weight - } - ) + vector_service.search_by_vector, **{ + "query": query, + "top_k": kb_config.top_k, + "indices": indices, + "score_threshold": kb_config.vector_similarity_weight + } + ) rs2_task = asyncio.to_thread( - vector_service.search_by_full_text, **{ - "query": query, - "top_k": kb_config.top_k, - "indices": indices, - "score_threshold": kb_config.similarity_threshold - } - ) + vector_service.search_by_full_text, **{ + "query": query, + "top_k": kb_config.top_k, + "indices": indices, + "score_threshold": kb_config.similarity_threshold + } + ) rs1, rs2 = await asyncio.gather(rs1_task, rs2_task) # Deduplicate hybrid retrieval results @@ -266,6 +269,33 @@ class KnowledgeRetrievalNode(BaseNode): key=lambda d: d.metadata.get("score", 0), reverse=True )[:kb_config.top_k]) + if kb_config.retrieve_type == RetrieveType.Graph: + from app.core.rag.common.settings import kg_retriever + llm_key = self.model_balance(db_knowledge.llm) + emb_key = self.model_balance(db_knowledge.embedding) + chat_model = Base( + key=llm_key.api_key, + model_name=llm_key.model_name, + base_url=llm_key.api_base + ) + embedding_model = OpenAIEmbed( + key=emb_key.api_key, + model_name=emb_key.model_name, + base_url=emb_key.api_base + ) + doc = await asyncio.to_thread( + kg_retriever.retrieval, + question=query, + workspace_ids=[str(db_knowledge.workspace_id)], + kb_ids=[str(kb_config.kb_id)], + emb_mdl=embedding_model, + llm=chat_model + ) + if doc: + rs.insert(0, DocumentChunk( + page_content=doc.get("page_content", ""), + metadata=doc.get("metadata", {}) + )) case _: raise RuntimeError("Unknown retrieval type") return rs From 5975d70bf9b4e1398730a3386f37d1df258ac369 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 15:14:15 +0800 Subject: [PATCH 07/21] feat(tool): add datetime_to_timestamp operation with timezone support --- api/app/core/tools/builtin/datetime_tool.py | 4 ++-- api/app/core/tools/builtin/operation_tool.py | 23 ++++++++++++++++++++ api/app/services/tool_service.py | 23 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/api/app/core/tools/builtin/datetime_tool.py b/api/app/core/tools/builtin/datetime_tool.py index 8db830ce..aaba90f0 100644 --- a/api/app/core/tools/builtin/datetime_tool.py +++ b/api/app/core/tools/builtin/datetime_tool.py @@ -253,9 +253,9 @@ class DateTimeTool(BuiltinTool): return { "datetime": input_value, "timezone": timezone_str, - "timestamp": int(dt.timestamp()), + "timestamp": int(dt.timestamp()) * 1000, "iso_format": dt.isoformat(), - "result_data": int(dt.timestamp()) + "result_data": int(dt.timestamp()) * 1000 } def _calculate_datetime(self, kwargs) -> dict: diff --git a/api/app/core/tools/builtin/operation_tool.py b/api/app/core/tools/builtin/operation_tool.py index 126541a8..95e6fdf5 100644 --- a/api/app/core/tools/builtin/operation_tool.py +++ b/api/app/core/tools/builtin/operation_tool.py @@ -138,6 +138,29 @@ class OperationTool(BaseTool): default="Asia/Shanghai" ) ] + elif self.operation == "datetime_to_timestamp": + return [ + ToolParameter( + name="input_value", + type=ParameterType.STRING, + description="输入值(时间字符串,如:2026-04-07 10:30:25)", + required=True + ), + ToolParameter( + name="input_format", + type=ParameterType.STRING, + description="输入时间格式(如:%Y-%m-%d %H:%M:%S)", + required=False, + default="%Y-%m-%d %H:%M:%S" + ), + ToolParameter( + name="from_timezone", + type=ParameterType.STRING, + description="源时区(如:UTC, Asia/Shanghai)", + required=False, + default="Asia/Shanghai" + ) + ] else: return [] diff --git a/api/app/services/tool_service.py b/api/app/services/tool_service.py index 089f0ec5..165b060f 100644 --- a/api/app/services/tool_service.py +++ b/api/app/services/tool_service.py @@ -574,6 +574,29 @@ class ToolService: "default": "Asia/Shanghai" } ] + elif operation == "datetime_to_timestamp": + return [ + { + "name": "input_value", + "type": "string", + "description": "输入值(时间字符串,如:2026-04-07 10:30:25)", + "required": True + }, + { + "name": "input_format", + "type": "string", + "description": "输入时间格式(如:%Y-%m-%d %H:%M:%S)", + "required": False, + "default": "%Y-%m-%d %H:%M:%S" + }, + { + "name": "from_timezone", + "type": "string", + "description": "源时区(如:UTC, Asia/Shanghai)", + "required": False, + "default": "Asia/Shanghai" + } + ] else: # 默认返回所有参数(除了operation) return [ From ea2f5e61c9d8bbba75e2768979d1f45a413e66a2 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 15:18:39 +0800 Subject: [PATCH 08/21] fix(tool): strip input_value in datetime_to_timestamp to prevent whitespace-related parsing errors --- api/app/core/tools/builtin/datetime_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/core/tools/builtin/datetime_tool.py b/api/app/core/tools/builtin/datetime_tool.py index aaba90f0..2fda6b8b 100644 --- a/api/app/core/tools/builtin/datetime_tool.py +++ b/api/app/core/tools/builtin/datetime_tool.py @@ -230,7 +230,7 @@ class DateTimeTool(BuiltinTool): @staticmethod def _datetime_to_timestamp(kwargs) -> dict: """日期时间转时间戳""" - input_value = kwargs.get("input_value") + input_value = kwargs.get("input_value").strip() input_format = kwargs.get("input_format", "%Y-%m-%d %H:%M:%S") timezone_str = kwargs.get("from_timezone", "Asia/Shanghai") From 2782d0661fee2822eb3eaae8d8b4db9388f368d9 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 15:28:45 +0800 Subject: [PATCH 09/21] fix(web): retrieve types add graph --- .../components/Knowledge/KnowledgeConfigModal.tsx | 2 +- .../components/Properties/Knowledge/KnowledgeConfigModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeConfigModal.tsx b/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeConfigModal.tsx index 5ee56504..ef8abe38 100644 --- a/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeConfigModal.tsx +++ b/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeConfigModal.tsx @@ -33,7 +33,7 @@ interface KnowledgeConfigModalProps { * Available retrieval types */ const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid', - // 'graph' + 'graph' ] /** diff --git a/web/src/views/Workflow/components/Properties/Knowledge/KnowledgeConfigModal.tsx b/web/src/views/Workflow/components/Properties/Knowledge/KnowledgeConfigModal.tsx index 46ecd62d..06625e7a 100644 --- a/web/src/views/Workflow/components/Properties/Knowledge/KnowledgeConfigModal.tsx +++ b/web/src/views/Workflow/components/Properties/Knowledge/KnowledgeConfigModal.tsx @@ -13,7 +13,7 @@ interface KnowledgeConfigModalProps { refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void; } const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid', - // 'graph' + 'graph' ] const KnowledgeConfigModal = forwardRef(({ From 32ae60fc65f6e15c41d567ed76026f5da0ba0e8a Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 16:14:24 +0800 Subject: [PATCH 10/21] fix(web): port add node front --- .../Workflow/components/PortClickHandler.tsx | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index 7e2350ee..b556ffab 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -191,38 +191,39 @@ const PortClickHandler: React.FC = ({ graph }) => { setTimeout(() => { const newPorts = newNode.getPorts(); + const addedEdges: any[] = []; if (edgeInsertion) { // Edge insertion: create source→new and new→target edges const { targetCell, targetPort: origTargetPort } = edgeInsertion; const newLeftPort = newPorts.find((p: any) => p.group === 'left')?.id || 'left'; const newRightPort = newPorts.find((p: any) => p.group === 'right')?.id || 'right'; - graph.addEdge({ + addedEdges.push(graph.addEdge({ source: { cell: sourceNode.id, port: sourcePort }, target: { cell: newNode.id, port: newLeftPort }, ...edgeAttrs - }); - graph.addEdge({ + })); + addedEdges.push(graph.addEdge({ source: { cell: newNode.id, port: newRightPort }, target: { cell: targetCell.id, port: origTargetPort }, ...edgeAttrs - }); + })); setEdgeInsertion(null); } else if (sourcePortGroup === 'left') { // Connect from left port to new node's right side const targetPort = newPorts.find((port: any) => port.group === 'right')?.id || 'right'; - graph.addEdge({ + addedEdges.push(graph.addEdge({ source: { cell: newNode.id, port: targetPort }, target: { cell: sourceNode.id, port: sourcePort }, ...edgeAttrs - }); + })); } else { // Connect from right port to new node's left side const targetPort = newPorts.find((port: any) => port.group === 'left')?.id || 'left'; - graph.addEdge({ + addedEdges.push(graph.addEdge({ source: { cell: sourceNode.id, port: sourcePort }, target: { cell: newNode.id, port: targetPort }, ...edgeAttrs - }); + })); } // Adjust loop node size when child node is added via port within loop node @@ -269,6 +270,44 @@ const PortClickHandler: React.FC = ({ graph }) => { }); } } + + const isCycleContainer = (type: string) => type === 'loop' || type === 'iteration'; + const newNodeType = selectedNodeType.type; + + // Helper: bring all child nodes and their edges of a cycle container to front + const bringCycleChildrenToFront = (cycleContainerId: string) => { + + graph.getEdges().forEach((e: any) => { + const src = graph.getCellById(e.getSourceCellId()); + const tgt = graph.getCellById(e.getTargetCellId()); + if (src?.getData()?.cycle === cycleContainerId || tgt?.getData()?.cycle === cycleContainerId) e.toFront(); + }); + graph.getNodes().forEach((n: any) => { + if (n.getData()?.cycle === cycleContainerId) n.toFront(); + }); + }; + + if (isCycleContainer(sourceNodeType)) { + console.log('isCycleContainer(sourceNodeType)') + // Case 4: source is a loop/iteration node — bring new node to front, then its children + newNode.toFront(); + sourceNode.toFront(); + bringCycleChildrenToFront(sourceNodeData.id); + } else if (isCycleContainer(newNodeType)) { + console.log('isCycleContainer(newNodeType)') + // Case 3: adding a loop/iteration node from a normal node — bring new node to front, then its children + newNode.toFront(); + sourceNode.toFront() + bringCycleChildrenToFront(id); + } else { + // Case 2: normal node → normal node + addedEdges.forEach(e => { + const src = graph.getCellById(e.getSourceCellId()); + const tgt = graph.getCellById(e.getTargetCellId()); + if (src?.isNode()) src.toFront(); + if (tgt?.isNode()) tgt.toFront(); + }); + } }, 50); // Clean up temporary element From b598171a3d3dca3512a818791ece932d353917bb Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 16:34:04 +0800 Subject: [PATCH 11/21] fix(web): if-else/question-classifier add node front --- .../Workflow/components/Nodes/AddNode.tsx | 24 +++++++++++++------ .../Workflow/components/Nodes/LoopNode.tsx | 9 +++++++ .../components/Properties/CaseList/index.tsx | 4 ++++ .../Properties/CategoryList/index.tsx | 4 ++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx index 5f1e7e65..3bdb96c0 100644 --- a/web/src/views/Workflow/components/Nodes/AddNode.tsx +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -49,23 +49,24 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { const incomingEdges = graph.getIncomingEdges(node); const outgoingEdges = graph.getOutgoingEdges(node); - - incomingEdges?.forEach(edge => { - graph.addEdge({ + const addedEdges: any[] = []; + + incomingEdges?.forEach((edge: any) => { + addedEdges.push(graph.addEdge({ source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() }, target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' }, ...edgeAttrs - }); + })); }); - outgoingEdges?.forEach(edge => { + outgoingEdges?.forEach((edge: any) => { const targetCell = graph.getCellById(edge.getTargetCellId()) as any; const targetPortId = targetCell?.getPorts?.()?.find((port: any) => port.group === 'left')?.id || edge.getTargetPortId(); - graph.addEdge({ + addedEdges.push(graph.addEdge({ source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' }, target: { cell: edge.getTargetCellId(), port: targetPortId }, ...edgeAttrs - }); + })); }); // Remove all add-node type nodes @@ -75,6 +76,15 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { } }); + setTimeout(() => { + addedEdges.forEach(e => { + const src = graph.getCellById(e.getSourceCellId()); + const tgt = graph.getCellById(e.getTargetCellId()); + if (src?.isNode()) src.toFront(); + if (tgt?.isNode()) tgt.toFront(); + }); + }, 50); + // Automatically adjust loop node size const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); if (loopNode) { diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index 4a803246..ca0eaeff 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -59,6 +59,9 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { target: { cell: addNode.id, port: targetPort }, ...edgeAttrs, }); + + cycleStartNode.toFront() + addNode.toFront() } } @@ -117,6 +120,12 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { ...edgeAttrs } graph.addEdge(edgeConfig) + + setTimeout(() => { + + cycleStartNode.toFront() + addNode.toFront() + }, 0) } return ( diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx index 5d3d2607..d1849872 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -139,6 +139,8 @@ const CaseList: FC = ({ ...edgeAttrs, }); } + sourceCell.toFront() + selectedNode.toFront() graphRef.current?.removeCell(edge); return; } @@ -183,6 +185,8 @@ const CaseList: FC = ({ target: { cell: targetCellId, port: targetPortId }, ...edgeAttrs }); + selectedNode.toFront() + targetCell.toFront() } } diff --git a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx index 9137b223..d37699d9 100644 --- a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx @@ -88,6 +88,8 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe target: { cell: selectedNode.id, port: targetPortId }, ...edgeAttrs }); + sourceCell.toFront() + selectedNode.toFront() } return; } @@ -119,6 +121,8 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe target: { cell: targetCellId, port: targetPortId }, ...edgeAttrs }); + selectedNode.toFront() + targetCell.toFront() } } }); From a7b8ba0c660d3b7456f09010375f3cd809f4616e Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 17:48:16 +0800 Subject: [PATCH 12/21] fix(rag): fix pdfplumber concurrency issue and add debug logging The pdfplumber parser now uses a global lock to prevent concurrent access issues during PDF image rendering. Additionally, added a warning log to trace knowledge retrieval results for debugging purposes. The syntax fix in knowledge node's match case ensures correct pattern matching behavior. BREAKING CHANGE: The pdfplumber parser now requires LOCK_KEY_pdfplumber to be defined in sys.modules for thread safety. Closes #841 --- api/app/core/rag/deepdoc/parser/mineru_parser.py | 7 ++++--- api/app/core/workflow/nodes/knowledge/node.py | 2 +- api/app/services/draft_run_service.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/app/core/rag/deepdoc/parser/mineru_parser.py b/api/app/core/rag/deepdoc/parser/mineru_parser.py index fe6178ec..c2f7af16 100644 --- a/api/app/core/rag/deepdoc/parser/mineru_parser.py +++ b/api/app/core/rag/deepdoc/parser/mineru_parser.py @@ -292,9 +292,10 @@ class MinerUParser(RAGPdfParser): self.page_from = page_from self.page_to = page_to try: - with pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm)) as pdf: - self.pdf = pdf - self.page_images = [p.to_image(resolution=72 * zoomin, antialias=True).original for _, p in enumerate(self.pdf.pages[page_from:page_to])] + with sys.modules[LOCK_KEY_pdfplumber]: # ← 加这一行,获取全局锁 + with pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm)) as pdf: + self.pdf = pdf + self.page_images = [p.to_image(resolution=72 * zoomin, antialias=True).original for _, p in enumerate(self.pdf.pages[page_from:page_to])] except Exception as e: self.page_images = None self.total_page = 0 diff --git a/api/app/core/workflow/nodes/knowledge/node.py b/api/app/core/workflow/nodes/knowledge/node.py index 29e46902..0601883d 100644 --- a/api/app/core/workflow/nodes/knowledge/node.py +++ b/api/app/core/workflow/nodes/knowledge/node.py @@ -233,7 +233,7 @@ class KnowledgeRetrievalNode(BaseNode): } ) ) - case RetrieveType.HYBRID | RetrieveType.Graph: + case (RetrieveType.HYBRID, RetrieveType.Graph): rs1_task = asyncio.to_thread( vector_service.search_by_vector, **{ "query": query, diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 978dfdab..461ee0c4 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -224,6 +224,7 @@ def create_knowledge_retrieval_tool(kb_config, kb_ids, user_id, citations_collec retrieve_chunks_result = knowledge_retrieval(query, kb_config) if retrieve_chunks_result: retrieval_knowledge = [i.page_content for i in retrieve_chunks_result] + logger.warning(f"检索知识结果:{retrieval_knowledge}") context = '\n\n'.join(retrieval_knowledge) logger.info( "知识库检索成功", From 62e0b2730bfd86ac5c027a465508c38212cf7340 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 18:29:08 +0800 Subject: [PATCH 13/21] refactor(workflow/knowledge): update pattern matching to support multiple retrieve types --- api/app/core/workflow/nodes/knowledge/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/core/workflow/nodes/knowledge/node.py b/api/app/core/workflow/nodes/knowledge/node.py index 0601883d..2a8c5249 100644 --- a/api/app/core/workflow/nodes/knowledge/node.py +++ b/api/app/core/workflow/nodes/knowledge/node.py @@ -233,7 +233,7 @@ class KnowledgeRetrievalNode(BaseNode): } ) ) - case (RetrieveType.HYBRID, RetrieveType.Graph): + case retrieve_type if retrieve_type in (RetrieveType.HYBRID, RetrieveType.Graph): rs1_task = asyncio.to_thread( vector_service.search_by_vector, **{ "query": query, From 130684cac0b008a2726ea41a65884b68ca6168c8 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 19:07:53 +0800 Subject: [PATCH 14/21] refactor(rag/nlp): standardize knowledge graph retrieval to use DocumentChunk and add debug logging The knowledge graph retrieval logic in `search.py` was updated to consistently return `DocumentChunk` instances instead of raw dictionaries, improving type safety and alignment with the RAG pipeline's expected data structure. Additionally, debug logging was enhanced in `draft_run_service.py` to log the full `retrieve_chunks_result` before extracting page content, aiding troubleshooting. --- api/app/core/rag/nlp/search.py | 10 ++++++++-- api/app/services/draft_run_service.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/api/app/core/rag/nlp/search.py b/api/app/core/rag/nlp/search.py index 1a84b8a7..371facb4 100644 --- a/api/app/core/rag/nlp/search.py +++ b/api/app/core/rag/nlp/search.py @@ -133,7 +133,10 @@ def knowledge_retrieval( from app.core.rag.common.settings import kg_retriever doc = kg_retriever.retrieval(question=query, workspace_ids=workspace_ids, kb_ids=kb_ids, emb_mdl=embedding_model, llm=chat_model) if doc: - all_results.insert(0, doc) + all_results.insert(0, DocumentChunk( + page_content=doc.get("page_content", ""), + metadata=doc.get("metadata", {}) + )) except Exception as graph_error: print(f"Failed to retrieve from knowledge graph: {str(graph_error)}") @@ -262,7 +265,10 @@ def _retrieve_for_knowledge( llm=chat_model, ) if graph_doc: - rs.insert(0, graph_doc) + rs.insert(0, DocumentChunk( + page_content=graph_doc.get("page_content", ""), + metadata=graph_doc.get("metadata", {}) + )) except Exception as graph_error: logger.warning(f"Graph retrieval failed for kb {db_knowledge.id}: {graph_error}") diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 461ee0c4..8a381c8f 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -223,8 +223,8 @@ def create_knowledge_retrieval_tool(kb_config, kb_ids, user_id, citations_collec retrieve_chunks_result = knowledge_retrieval(query, kb_config) if retrieve_chunks_result: + logger.warning(f"检索知识结果:{retrieve_chunks_result}") retrieval_knowledge = [i.page_content for i in retrieve_chunks_result] - logger.warning(f"检索知识结果:{retrieval_knowledge}") context = '\n\n'.join(retrieval_knowledge) logger.info( "知识库检索成功", From ca4f7aa65db4a1ec1ae9f12c406a62a16afdb663 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 9 Apr 2026 19:35:43 +0800 Subject: [PATCH 15/21] refactor(rag/nlp): refactor reranking logic to apply post-deduplication and remove debug log --- api/app/core/rag/nlp/search.py | 9 +++++++-- api/app/services/draft_run_service.py | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/app/core/rag/nlp/search.py b/api/app/core/rag/nlp/search.py index 371facb4..61540ee4 100644 --- a/api/app/core/rag/nlp/search.py +++ b/api/app/core/rag/nlp/search.py @@ -115,9 +115,8 @@ def knowledge_retrieval( # Use the specified reranker for re-ranking if reranker_id: try: - return rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k) + all_results = rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k) except Exception as rerank_error: - # If reranker fails, log warning and continue with original results logger.warning( "Reranker failed, falling back to original results", extra={ @@ -254,6 +253,12 @@ def _retrieve_for_knowledge( seen_ids.add(doc.metadata["doc_id"]) unique_rs.append(doc) rs = unique_rs + if unique_rs: + rs = vector_service.rerank( + query=kb_config["query"], + docs=unique_rs, + top_k=kb_config["top_k"] + ) if kb_config["retrieve_type"] == "graph": try: from app.core.rag.common.settings import kg_retriever diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 8a381c8f..978dfdab 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -223,7 +223,6 @@ def create_knowledge_retrieval_tool(kb_config, kb_ids, user_id, citations_collec retrieve_chunks_result = knowledge_retrieval(query, kb_config) if retrieve_chunks_result: - logger.warning(f"检索知识结果:{retrieve_chunks_result}") retrieval_knowledge = [i.page_content for i in retrieve_chunks_result] context = '\n\n'.join(retrieval_knowledge) logger.info( From 384a67482cc0518a9254939f016f844d5e7cd730 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 20:29:17 +0800 Subject: [PATCH 16/21] fix(web): edge connected event --- web/src/views/Workflow/hooks/useWorkflowGraph.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 4c2ab44e..8779f6bc 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -1056,9 +1056,7 @@ export const useWorkflowGraph = ({ } }); graphRef.current?.getNodes().forEach(node => { - graphRef.current?.getNodes().forEach(node => { - if (node.getData()?.cycle) node.toFront(); - }); + if (node.getData()?.cycle) node.toFront(); node.getPorts().filter(p => p.group === 'right').forEach(p => { node.setPortProp(p.id!, 'attrs/body/opacity', 1); node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0); From 428e7ebaa588efe8af6de4fc74e31b81bf45bf82 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 21:12:59 +0800 Subject: [PATCH 17/21] fix(web): agent knowledge bases config --- web/src/views/ApplicationConfig/Agent.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 94845571..aa71ead7 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -229,6 +229,9 @@ const Agent = forwardRef ({ kb_id: item.kb_id || item.id, + top_k: item.top_k, + similarity_threshold: item.similarity_threshold, + vector_similarity_weight: item.vector_similarity_weight, ...(item.config || {}) })) } as KnowledgeConfig : null, From 27e9f9968d3ff15010d50d937b9ed702bca407e5 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 21:31:36 +0800 Subject: [PATCH 18/21] fix(web): remove port hover style --- web/src/views/Workflow/constant.ts | 4 +- .../views/Workflow/hooks/useWorkflowGraph.ts | 61 +------------------ 2 files changed, 4 insertions(+), 61 deletions(-) diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 67553555..77b936f6 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -730,7 +730,7 @@ const defaultPortGroup = { stroke: port_color, strokeWidth: edge_width, fill: port_color, - opacity: 0, + opacity: 1, }, label: { text: '+', @@ -741,7 +741,7 @@ const defaultPortGroup = { textVerticalAnchor: 'middle', pointerEvents: 'none', y: '0.15em', - opacity: 0, + opacity: 1, }, }, } diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 8779f6bc..c3bd468d 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -979,24 +979,6 @@ export const useWorkflowGraph = ({ graphRef.current.on('edge:click', edgeClick); // Listen to port click event graphRef.current.on('node:port:click', nodePortClickEvent); - // Port hover: show circle style on right ports - graphRef.current.on('node:port:mouseenter', ({ node, port }) => { - console.log('node:port:mouseenter', port) - if (!port) return; - const portData = node.getPort(port); - if (portData?.group !== 'right') return; - node.setPortProp(port, 'attrs/body/opacity', 0); - node.setPortProp(port, 'attrs/hoverBody/opacity', 1); - node.setPortProp(port, 'attrs/label/opacity', 1); - }); - graphRef.current.on('node:port:mouseleave', ({ node, port }) => { - if (!port) return; - const portData = node.getPort(port); - if (portData?.group !== 'right') return; - node.setPortProp(port, 'attrs/body/opacity', 1); - node.setPortProp(port, 'attrs/hoverBody/opacity', 0); - node.setPortProp(port, 'attrs/label/opacity', 0); - }); // Listen to canvas click event, cancel selection graphRef.current.on('blank:click', blankClick); // Node hover: highlight connected edges @@ -1014,11 +996,6 @@ export const useWorkflowGraph = ({ edge.setData({ ...edge.getData(), isNodeHover: true }); } }); - node.getPorts().filter(p => p.group === 'right').forEach(p => { - node.setPortProp(p.id!, 'attrs/body/opacity', 0); - node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 1); - node.setPortProp(p.id!, 'attrs/label/opacity', 1); - }); }); graphRef.current.on('node:mouseleave', ({ node }) => { graphRef.current?.getConnectedEdges(node).forEach(edge => { @@ -1027,11 +1004,6 @@ export const useWorkflowGraph = ({ edge.setData({ ...edge.getData(), isNodeHover: false }); } }); - node.getPorts().filter(p => p.group === 'right').forEach(p => { - node.setPortProp(p.id!, 'attrs/body/opacity', 1); - node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0); - node.setPortProp(p.id!, 'attrs/label/opacity', 0); - }); }); // Listen to zoom event graphRef.current.on('scale', scaleEvent); @@ -1057,11 +1029,6 @@ export const useWorkflowGraph = ({ }); graphRef.current?.getNodes().forEach(node => { if (node.getData()?.cycle) node.toFront(); - node.getPorts().filter(p => p.group === 'right').forEach(p => { - node.setPortProp(p.id!, 'attrs/body/opacity', 1); - node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0); - node.setPortProp(p.id!, 'attrs/label/opacity', 0); - }); }); } }); @@ -1091,33 +1058,9 @@ export const useWorkflowGraph = ({ if (found) break; } - if (found?.node.id !== lastHoveredPort?.node.id || found?.portId !== lastHoveredPort?.portId) { - // Leave previous - if (lastHoveredPort) { - const { node, portId } = lastHoveredPort; - node.setPortProp(portId, 'attrs/body/opacity', 1); - node.setPortProp(portId, 'attrs/hoverBody/opacity', 0); - node.setPortProp(portId, 'attrs/label/opacity', 0); - } - // Enter new - if (found) { - const { node, portId } = found; - node.setPortProp(portId, 'attrs/body/opacity', 0); - node.setPortProp(portId, 'attrs/hoverBody/opacity', 1); - node.setPortProp(portId, 'attrs/label/opacity', 1); - } - lastHoveredPort = found; - } - }); - graphRef.current.on('edge:mouseup', () => { - if (lastHoveredPort) { - const { node, portId } = lastHoveredPort; - node.setPortProp(portId, 'attrs/body/opacity', 1); - node.setPortProp(portId, 'attrs/hoverBody/opacity', 0); - node.setPortProp(portId, 'attrs/label/opacity', 0); - lastHoveredPort = null; - } + lastHoveredPort = found; }); + graphRef.current.on('edge:mouseup', () => { lastHoveredPort = null; }); // Listen to copy keyboard event graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent); // Listen to paste keyboard event From 8b21dab25556ab73efc64a0ddd22a7e7caca7e24 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 22:09:32 +0800 Subject: [PATCH 19/21] fix(web): agent knowledge --- web/src/views/ApplicationConfig/Agent.tsx | 3 ++- .../ApplicationConfig/components/Knowledge/Knowledge.tsx | 1 + .../components/Knowledge/KnowledgeListModal.tsx | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index aa71ead7..b694d1eb 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -229,10 +229,11 @@ const Agent = forwardRef ({ kb_id: item.kb_id || item.id, + retrieve_type: item.retrieve_type, top_k: item.top_k, similarity_threshold: item.similarity_threshold, vector_similarity_weight: item.vector_similarity_weight, - ...(item.config || {}) + // ...(item.config || {}) })) } as KnowledgeConfig : null, tools: tools.map(vo => { diff --git a/web/src/views/ApplicationConfig/components/Knowledge/Knowledge.tsx b/web/src/views/ApplicationConfig/components/Knowledge/Knowledge.tsx index 7ad3073d..d213e739 100644 --- a/web/src/views/ApplicationConfig/components/Knowledge/Knowledge.tsx +++ b/web/src/views/ApplicationConfig/components/Knowledge/Knowledge.tsx @@ -117,6 +117,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi const list = [...knowledgeList] list[index] = { ...list[index], + ...values, config: {...values as KnowledgeConfigForm} } setKnowledgeList([...list]) diff --git a/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeListModal.tsx b/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeListModal.tsx index f2c2ce3c..5b6391c0 100644 --- a/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeListModal.tsx +++ b/web/src/views/ApplicationConfig/components/Knowledge/KnowledgeListModal.tsx @@ -88,6 +88,10 @@ const KnowledgeListModal = forwardRef(({ const handleSave = () => { refresh(selectedRows.map(item => ({ ...item, + similarity_threshold: 0.7, + retrieve_type: "hybrid", + top_k: 3, + weight: 1, config: { similarity_threshold: 0.7, retrieve_type: "hybrid", From 84b1a953134053efa3bc372de1801f9db9cf7dd1 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 22:13:52 +0800 Subject: [PATCH 20/21] fix(web): iteration/loop toFront --- .../components/Properties/CaseList/index.tsx | 18 ++++++++++++++++++ .../Properties/CategoryList/index.tsx | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx index d1849872..e1583ca0 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -61,6 +61,20 @@ const CaseList: FC = ({ const { t } = useTranslation(); const form = Form.useFormInstance(); + const bringLoopChildrenToFront = (cell: any) => { + const type = cell?.getData()?.type; + if ((type !== 'loop' && type !== 'iteration') || !graphRef?.current) return; + const cycleId = cell.getData().id; + graphRef.current.getEdges().forEach((edge: any) => { + const src = graphRef.current?.getCellById(edge.getSourceCellId()); + const tgt = graphRef.current?.getCellById(edge.getTargetCellId()); + if (src?.getData()?.cycle === cycleId || tgt?.getData()?.cycle === cycleId) edge.toFront(); + }); + graphRef.current.getNodes().forEach((n: any) => { + if (n.getData()?.cycle === cycleId) n.toFront(); + }); + }; + // Recalculate node height and port Y positions without rebuilding ports const updateNodeLayout = (cases: any[]) => { if (!selectedNode || !graphRef?.current) return; @@ -141,6 +155,8 @@ const CaseList: FC = ({ } sourceCell.toFront() selectedNode.toFront() + bringLoopChildrenToFront(sourceCell) + bringLoopChildrenToFront(selectedNode) graphRef.current?.removeCell(edge); return; } @@ -186,7 +202,9 @@ const CaseList: FC = ({ ...edgeAttrs }); selectedNode.toFront() + bringLoopChildrenToFront(selectedNode) targetCell.toFront() + bringLoopChildrenToFront(targetCell) } } diff --git a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx index d37699d9..8a406a7a 100644 --- a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx @@ -25,6 +25,20 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe const form = Form.useFormInstance(); const formValues = Form.useWatch([parentName], form); + const bringLoopChildrenToFront = (cell: any) => { + const type = cell?.getData()?.type; + if ((type !== 'loop' && type !== 'iteration') || !graphRef?.current) return; + const cycleId = cell.getData().id; + graphRef.current.getEdges().forEach((edge: any) => { + const src = graphRef.current?.getCellById(edge.getSourceCellId()); + const tgt = graphRef.current?.getCellById(edge.getTargetCellId()); + if (src?.getData()?.cycle === cycleId || tgt?.getData()?.cycle === cycleId) edge.toFront(); + }); + graphRef.current.getNodes().forEach((n: any) => { + if (n.getData()?.cycle === cycleId) n.toFront(); + }); + }; + // Update node ports based on category count changes (add/remove categories) const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => { if (!selectedNode || !graphRef?.current) return; @@ -89,7 +103,9 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe ...edgeAttrs }); sourceCell.toFront() + bringLoopChildrenToFront(sourceCell) selectedNode.toFront() + bringLoopChildrenToFront(selectedNode) } return; } @@ -122,7 +138,9 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe ...edgeAttrs }); selectedNode.toFront() + bringLoopChildrenToFront(selectedNode) targetCell.toFront() + bringLoopChildrenToFront(targetCell) } } }); From 660cd2fadb0df83872e9277082acd7dd4f6ebe09 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 9 Apr 2026 22:29:31 +0800 Subject: [PATCH 21/21] fix(web): deep_thinking reset --- .../ApplicationConfig/components/ModelConfigModal.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx b/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx index e04594a8..8e3e3257 100644 --- a/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx +++ b/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx @@ -155,12 +155,10 @@ const ModelConfigModal = forwardRef( {['model', 'chat'].includes(source) && <>