fix(web): worflow bugfix

This commit is contained in:
zhaoying
2026-01-12 18:44:52 +08:00
parent ea944d0ee2
commit 7eb0d84947
10 changed files with 473 additions and 230 deletions

View File

@@ -14,6 +14,8 @@ import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
import InitialValuePlugin from './plugin/InitialValuePlugin';
import CommandPlugin from './plugin/CommandPlugin';
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
import LineNumberPlugin from './plugin/LineNumberPlugin';
import { VariableNode } from './nodes/VariableNode'
interface LexicalEditorProps {
@@ -88,6 +90,35 @@ const Editor: FC<LexicalEditorProps> =({
.editor-paragraph:has-text('[') .editor-text {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
}
.editor-with-line-numbers {
display: flex;
}
.line-numbers {
background-color: #f8f9fa;
border-right: 1px solid #e1e4e8;
color: #656d76;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px;
line-height: 20px;
padding: 4px 8px;
text-align: right;
user-select: none;
display: flex;
flex-direction: column;
}
.line-numbers > div {
min-height: 20px;
display: flex;
align-items: flex-start;
}
.editor-content-with-numbers {
flex: 1;
white-space: pre-wrap;
}
.editor-content-with-numbers p {
margin: 0;
min-height: 20px;
}
`;
document.head.appendChild(style);
}
@@ -117,25 +148,49 @@ const Editor: FC<LexicalEditorProps> =({
<div style={{ position: 'relative' }}>
<RichTextPlugin
contentEditable={
<ContentEditable
style={{
minHeight: `${height}px`,
padding: variant === 'borderless' ? '0' : '4px 11px',
enableJinja2 ? (
<div className="editor-with-line-numbers" style={{
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
borderRadius: '6px',
outline: 'none',
resize: 'none',
fontSize: '14px',
lineHeight: '20px',
}}
/>
minHeight: `${height}px`,
}}>
<div className="line-numbers">
<div>1</div>
</div>
<ContentEditable
className="editor-content-with-numbers"
style={{
minHeight: `${height}px`,
padding: '4px 11px',
outline: 'none',
resize: 'none',
fontSize: '14px',
lineHeight: '20px',
border: 'none',
}}
/>
</div>
) : (
<ContentEditable
style={{
minHeight: `${height}px`,
padding: variant === 'borderless' ? '0' : '4px 11px',
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
borderRadius: '6px',
outline: 'none',
resize: 'none',
fontSize: '14px',
lineHeight: '20px',
}}
/>
)
}
placeholder={
<div
style={{
position: 'absolute',
top: variant === 'borderless' ? '0' : '6px',
left: variant === 'borderless' ? '0' : '11px',
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
color: '#5B6167',
fontSize: '14px',
lineHeight: '20px',
@@ -149,6 +204,8 @@ const Editor: FC<LexicalEditorProps> =({
/>
<HistoryPlugin />
<CommandPlugin />
{enableJinja2 && <Jinja2HighlightPlugin />}
{enableJinja2 && <LineNumberPlugin />}
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />

View File

@@ -36,64 +36,68 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
editor.update(() => {
const root = $getRoot();
root.clear();
const paragraph = $createParagraphNode();
const parts = value.split(/(\{\{[^}]+\}\})/);
parts.forEach(part => {
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
const contextMatch = part.match(/^\{\{context\}\}$/);
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
if (enableJinja2) {
// Handle newlines properly in Jinja2 mode
const lines = value.split('\n');
lines.forEach((line) => {
const paragraph = $createParagraphNode();
paragraph.append($createTextNode(line));
root.append(paragraph);
});
} else {
const paragraph = $createParagraphNode();
parts.forEach(part => {
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
const contextMatch = part.match(/^\{\{context\}\}$/);
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
if (enableJinja2) {
paragraph.append($createTextNode(part));
return;
}
if (contextMatch) {
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
if (contextSuggestion) {
paragraph.append($createVariableNode(contextSuggestion));
} else {
paragraph.append($createTextNode(part));
}
return
}
if (conversationMatch) {
const [_, variableName] = conversationMatch;
const conversationSuggestion = options.find(s =>
s.group === 'CONVERSATION' && s.label === variableName
);
if (conversationSuggestion) {
paragraph.append($createVariableNode(conversationSuggestion));
} else {
paragraph.append($createTextNode(part));
}
return
}
if (match) {
const [_, nodeId, label] = match;
const suggestion = options.find(s => {
if (nodeId === 'sys') {
return s.nodeData.type === 'start' && s.label === `sys.${label}`
if (contextMatch) {
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
if (contextSuggestion) {
paragraph.append($createVariableNode(contextSuggestion));
} else {
paragraph.append($createTextNode(part));
}
return s.nodeData.id === nodeId && s.label === label
});
return
}
if (conversationMatch) {
const [_, variableName] = conversationMatch;
const conversationSuggestion = options.find(s =>
s.group === 'CONVERSATION' && s.label === variableName
);
if (conversationSuggestion) {
paragraph.append($createVariableNode(conversationSuggestion));
} else {
paragraph.append($createTextNode(part));
}
return
}
if (match) {
const [_, nodeId, label] = match;
if (suggestion) {
paragraph.append($createVariableNode(suggestion));
} else {
const suggestion = options.find(s => {
if (nodeId === 'sys') {
return s.nodeData.type === 'start' && s.label === `sys.${label}`
}
return s.nodeData.id === nodeId && s.label === label
});
if (suggestion) {
paragraph.append($createVariableNode(suggestion));
} else {
paragraph.append($createTextNode(part));
}
} else if (part) {
paragraph.append($createTextNode(part));
}
} else if (part) {
paragraph.append($createTextNode(part));
}
});
root.append(paragraph);
});
root.append(paragraph);
}
}, { discrete: true });
}

View File

@@ -0,0 +1,151 @@
import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { TextNode, $createTextNode } from 'lexical';
const Jinja2HighlightPlugin = () => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
const text = textNode.getTextContent();
if (containsJinja2Patterns(text)) {
const parent = textNode.getParent();
if (!parent) return;
const tokens = tokenizeJinja2(text);
const newNodes = tokens.map(token => {
const newNode = $createTextNode(token.text);
switch (token.type) {
case 'number':
newNode.setStyle('color: #005cc5; font-weight: 500;');
break;
case 'header-0':
case 'header-1':
case 'header-2':
case 'header-3':
case 'header-4':
case 'header-5':
newNode.setStyle('color: #008000');
break;
case 'brace-0':
newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;');
break;
case 'brace-1':
newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;');
break;
case 'brace-2':
newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;');
break;
case 'brace-3':
newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;');
break;
case 'expression-0':
case 'expression-1':
case 'expression-2':
case 'expression-3':
case 'statement-0':
case 'statement-1':
case 'statement-2':
case 'statement-3':
// Jinja2 delimiters use same color as braces
break;
case 'comment-0':
case 'comment-1':
case 'comment-2':
case 'comment-3':
newNode.setStyle('color: #721c24; font-family: monospace;');
break;
case 'variable':
newNode.setStyle('color: #0969da; font-weight: 500;');
break;
case 'filter':
newNode.setStyle('color: #8250df; font-weight: 500;');
break;
case 'keyword':
newNode.setStyle('color: #cf222e; font-weight: 600;');
break;
}
return newNode;
});
if (newNodes.length > 1) {
textNode.replace(newNodes[0]);
for (let i = 1; i < newNodes.length; i++) {
newNodes[i - 1].insertAfter(newNodes[i]);
}
}
}
});
}, [editor]);
return null;
};
function containsJinja2Patterns(text: string): boolean {
return /[{}#\d]/.test(text);
}
function tokenizeJinja2(text: string): Array<{text: string, type: string}> {
const tokens: Array<{text: string, type: string}> = [];
let i = 0;
let braceLevel = 0;
while (i < text.length) {
// Check for markdown headers (at start or after whitespace)
if (text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) {
let headerLevel = 0;
let start = i;
while (i < text.length && text[i] === '#') {
headerLevel++;
i++;
}
// Skip space after #
if (i < text.length && text[i] === ' ') {
i++;
}
// Get the rest of the header text
while (i < text.length && text[i] !== '\n' && !/[{}]/.test(text[i])) {
i++;
}
tokens.push({ text: text.slice(start, i), type: `header-${Math.min(headerLevel - 1, 5)}` });
continue;
}
// Check for numbers
if (/\d/.test(text[i])) {
let start = i;
while (i < text.length && /[\d.]/.test(text[i])) {
i++;
}
tokens.push({ text: text.slice(start, i), type: 'number' });
continue;
}
if (text[i] === '{') {
tokens.push({ text: '{', type: `brace-${braceLevel % 4}` });
braceLevel++;
i++;
} else if (text[i] === '}') {
braceLevel = Math.max(0, braceLevel - 1);
tokens.push({ text: '}', type: `brace-${braceLevel % 4}` });
i++;
} else {
let start = i;
while (i < text.length && text[i] !== '{' && text[i] !== '}' &&
!(text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) &&
!/\d/.test(text[i])) {
i++;
}
if (start < i) {
tokens.push({ text: text.slice(start, i), type: 'text' });
}
}
}
return tokens;
}
export default Jinja2HighlightPlugin;

View File

@@ -1,109 +0,0 @@
import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical';
const JsonHighlightPlugin = () => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
const text = textNode.getTextContent();
// Check if text contains JSON-like patterns
if (containsJsonPatterns(text)) {
const parent = textNode.getParent();
if (!parent) return;
// Split text into tokens and create new nodes with appropriate classes
const tokens = tokenizeJson(text);
const newNodes = tokens.map(token => {
const newNode = $createTextNode(token.text);
// Set format based on token type
switch (token.type) {
case 'string':
newNode.setFormat('code');
newNode.setStyle('color: #032f62');
break;
case 'number':
newNode.setFormat('code');
newNode.setStyle('color: #005cc5');
break;
case 'boolean':
newNode.setFormat('code');
newNode.setStyle('color: #d73a49');
break;
case 'null':
newNode.setFormat('code');
newNode.setStyle('color: #6f42c1');
break;
case 'key':
newNode.setFormat('code');
newNode.setStyle('color: #22863a; font-weight: bold');
break;
case 'punctuation':
newNode.setFormat('code');
newNode.setStyle('color: #24292e');
break;
}
return newNode;
});
// Replace the original text node with the new highlighted nodes
if (newNodes.length > 1) {
textNode.replace(newNodes[0]);
for (let i = 1; i < newNodes.length; i++) {
newNodes[i - 1].insertAfter(newNodes[i]);
}
}
}
});
}, [editor]);
return null;
};
function containsJsonPatterns(text: string): boolean {
// Check for JSON-like patterns
return /[{}\[\]:,]/.test(text) ||
/"[^"]*"/.test(text) ||
/\b\d+(\.\d+)?\b/.test(text) ||
/\b(true|false|null)\b/.test(text);
}
function tokenizeJson(text: string): Array<{text: string, type: string}> {
const tokens: Array<{text: string, type: string}> = [];
const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g;
let match;
while ((match = regex.exec(text)) !== null) {
const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match;
if (string) {
// Check if it's a key (followed by colon)
const afterMatch = text.slice(match.index + fullMatch.length).trim();
if (afterMatch.startsWith(':')) {
tokens.push({ text: fullMatch, type: 'key' });
} else {
tokens.push({ text: fullMatch, type: 'string' });
}
} else if (punctuation) {
tokens.push({ text: fullMatch, type: 'punctuation' });
} else if (number) {
tokens.push({ text: fullMatch, type: 'number' });
} else if (boolean) {
if (fullMatch === 'null') {
tokens.push({ text: fullMatch, type: 'null' });
} else {
tokens.push({ text: fullMatch, type: 'boolean' });
}
} else if (whitespace || other) {
tokens.push({ text: fullMatch, type: 'text' });
}
}
return tokens;
}
export default JsonHighlightPlugin;

View File

@@ -0,0 +1,63 @@
import { useEffect, useState } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot } from 'lexical';
const LineNumberPlugin = () => {
const [editor] = useLexicalComposerContext();
const [lineCount, setLineCount] = useState(1);
useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
const root = $getRoot();
const paragraphCount = root.getChildren().length;
const lines = Math.max(1, paragraphCount);
setLineCount(lines);
});
});
}, [editor]);
useEffect(() => {
const updateLineNumbers = () => {
const lineNumbersElement = document.querySelector('.line-numbers');
const editorElement = document.querySelector('.editor-content-with-numbers');
if (lineNumbersElement && editorElement) {
const paragraphs = editorElement.querySelectorAll('p');
// Clear existing line numbers
lineNumbersElement.innerHTML = '';
// Create line numbers positioned at each paragraph
paragraphs.forEach((paragraph, index) => {
const lineNumber = document.createElement('div');
lineNumber.textContent = (index + 1).toString();
lineNumber.style.position = 'absolute';
lineNumber.style.top = paragraph.offsetTop + 'px';
lineNumber.style.right = '8px';
lineNumber.style.height = '20px';
lineNumber.style.lineHeight = '20px';
lineNumbersElement.appendChild(lineNumber);
});
// Set line numbers container to relative positioning
(lineNumbersElement as HTMLElement).style.position = 'relative';
}
};
// Update line numbers after content changes
const timer = setTimeout(updateLineNumbers, 100);
// Also update on window resize
window.addEventListener('resize', updateLineNumbers);
return () => {
clearTimeout(timer);
window.removeEventListener('resize', updateLineNumbers);
};
}, [lineCount]);
return null;
};
export default LineNumberPlugin;

View File

@@ -56,7 +56,8 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
graph.addEdge({
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
target: { cell: edge.getTargetCellId(), port: targetPortId },
attrs: edge.getAttrs()
attrs: edge.getAttrs(),
zIndex: 3
});
});

View File

@@ -61,6 +61,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
},
},
},
zIndex: 3
});
}
}
@@ -127,6 +128,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
},
},
},
zIndex: 3
}
graph.addEdge(edgeConfig)

View File

@@ -101,8 +101,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<EditableTable
parentName="headers"
title="HEADERS"
options={options}
filterBooleanType={true}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
/>
</Form.Item>
@@ -110,8 +109,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<EditableTable
parentName="params"
title="PARAMS"
options={options}
filterBooleanType={true}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
/>
</Form.Item>
@@ -134,8 +132,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item name={['body', 'data']} noStyle>
<EditableTable
parentName={['body', 'data']}
options={options}
filterBooleanType={true}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
typeOptions={[
{ label: 'text', value: 'text' },
{ label: 'file', value: 'file' }
@@ -168,7 +165,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<MessageEditor
key="raw"
parentName={['body', 'data']}
options={options}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
isArray={false}
title="RAW TEXT"
/>
@@ -177,7 +174,8 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{values?.body?.content_type === 'binary' &&
<Form.Item name={['body', 'data']}>
<VariableSelect
options={options}
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType.includes('file'))}
filterBooleanType={true}
/>
</Form.Item>

View File

@@ -64,7 +64,7 @@ const Properties: FC<PropertiesProps> = ({
useEffect(() => {
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.map((item: any) => item.name).filter(Boolean) : []
const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.filter(item => item && item.name).map((item: any) => item.name) : []
const prevNames = prevMappingNamesRef.current
if (prevNames.length === 0) {
@@ -121,8 +121,8 @@ const Properties: FC<PropertiesProps> = ({
return
}
const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping] : []
const existingNames = updatedMapping.map(item => item.name)
const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping.filter(item => item)] : []
const existingNames = updatedMapping.filter(item => item && item.name).map(item => item.name)
let updatedTemplate = String(values.template)
if (prevTemplateVarsRef.current.length > 0) {
@@ -157,7 +157,7 @@ const Properties: FC<PropertiesProps> = ({
isSyncingRef.current = true
lastSyncSourceRef.current = 'template'
prevMappingNamesRef.current = finalMapping.map((item: any) => item.name).filter(Boolean)
prevMappingNamesRef.current = finalMapping.filter(item => item && item.name).map((item: any) => item.name)
prevTemplateVarsRef.current = templateVars
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
@@ -391,6 +391,54 @@ const Properties: FC<PropertiesProps> = ({
}
}
// Check if parent loop/iteration is connected to http-request via ERROR connection
if (parentData.type === 'loop' || parentData.type === 'iteration') {
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
parentPreviousNodeIds.forEach(prevNodeId => {
const prevNode = nodes.find(n => n.id === prevNodeId);
if (!prevNode) return;
const prevNodeData = prevNode.getData();
if (prevNodeData.type === 'http-request') {
// Check if connected via ERROR connection point
const errorEdges = edges.filter(edge => {
return edge.getTargetCellId() === parentLoopNode.id &&
edge.getSourceCellId() === prevNodeId &&
edge.getSourcePortId() === 'ERROR'
});
if (errorEdges.length > 0) {
const errorMessageKey = `${prevNodeData.id}_error_message`;
const errorTypeKey = `${prevNodeData.id}_error_type`;
if (!addedKeys.has(errorMessageKey)) {
addedKeys.add(errorMessageKey);
variableList.push({
key: errorMessageKey,
label: 'error_message',
type: 'variable',
dataType: 'string',
value: `${prevNodeData.id}.error_message`,
nodeData: prevNodeData,
});
}
if (!addedKeys.has(errorTypeKey)) {
addedKeys.add(errorTypeKey);
variableList.push({
key: errorTypeKey,
label: 'error_type',
type: 'variable',
dataType: 'string',
value: `${prevNodeData.id}.error_type`,
nodeData: prevNodeData,
});
}
}
}
});
}
// Add variables from nodes preceding the parent loop/iteration node
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
allRelevantNodeIds.push(...parentPreviousNodeIds);
@@ -455,15 +503,15 @@ const Properties: FC<PropertiesProps> = ({
}
break
case 'knowledge-retrieval':
const knowledgeKey = `${dataNodeId}_message`;
const knowledgeKey = `${dataNodeId}_output`;
if (!addedKeys.has(knowledgeKey)) {
addedKeys.add(knowledgeKey);
variableList.push({
key: knowledgeKey,
label: 'message',
label: 'output',
type: 'variable',
dataType: 'array[object]',
value: `${dataNodeId}.message`,
value: `${dataNodeId}.output`,
nodeData: nodeData,
});
}
@@ -571,6 +619,42 @@ const Properties: FC<PropertiesProps> = ({
nodeData: nodeData,
});
}
// Check if connected via ERROR connection point
const errorEdges = edges.filter(edge =>
edge.getTargetCellId() === selectedNode.id &&
edge.getSourceCellId() === nodeId &&
edge.getSourcePortId() === 'ERROR'
);
if (errorEdges.length > 0) {
const errorMessageKey = `${dataNodeId}_error_message`;
const errorTypeKey = `${dataNodeId}_error_type`;
if (!addedKeys.has(errorMessageKey)) {
addedKeys.add(errorMessageKey);
variableList.push({
key: errorMessageKey,
label: 'error_message',
type: 'variable',
dataType: 'string',
value: `${dataNodeId}.error_message`,
nodeData: nodeData,
});
}
if (!addedKeys.has(errorTypeKey)) {
addedKeys.add(errorTypeKey);
variableList.push({
key: errorTypeKey,
label: 'error_type',
type: 'variable',
dataType: 'string',
value: `${dataNodeId}.error_type`,
nodeData: nodeData,
});
}
}
break
case 'jinja-render':
const jinjaOutputKey = `${dataNodeId}_output`;
@@ -613,7 +697,6 @@ const Properties: FC<PropertiesProps> = ({
}
break
case 'iteration':
console.log('iteration addedKeys', addedKeys)
const iterationOutputKey = `${dataNodeId}_output`;
const iterationItemKey = `${dataNodeId}_item`;
if (!addedKeys.has(iterationOutputKey)) {
@@ -651,18 +734,21 @@ const Properties: FC<PropertiesProps> = ({
break
case 'loop':
const cycleVars = nodeData.config.cycle_vars.defaultValue || [];
console.log('cycleVars', cycleVars)
cycleVars.forEach((cycleVar: any) => {
const cycleVarKey = `${dataNodeId}_cycle_${cycleVar.name}`;
if (!addedKeys.has(cycleVarKey)) {
addedKeys.add(cycleVarKey);
variableList.push({
key: cycleVarKey,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'string',
value: `${dataNodeId}.${cycleVar.name}`,
nodeData: nodeData,
});
if (cycleVar.name && cycleVar.name.trim() !== '') {
variableList.push({
key: cycleVarKey,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'string',
value: `${dataNodeId}.${cycleVar.name}`,
nodeData: nodeData,
});
}
}
});
break
@@ -818,7 +904,11 @@ const Properties: FC<PropertiesProps> = ({
return (
<Form.Item key={key} name={key}>
<MessageEditor key={key} options={contextVariableList} parentName={key} />
<MessageEditor
key={key}
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
parentName={key}
/>
</Form.Item>
)
}
@@ -1010,39 +1100,25 @@ const Properties: FC<PropertiesProps> = ({
? <ConditionList
parentName={key}
options={(() => {
// For loop nodes, add cycle_vars to condition options
if (selectedNode?.data?.type === 'loop') {
const cycleVars = values?.cycle_vars || [];
const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${selectedNode.getData().id}.${cycleVar.name}`,
nodeData: selectedNode.getData(),
}));
return [...getFilteredVariableList(selectedNode?.data?.type).filter(variable => {
// Keep conversation variables
if (variable.group === 'CONVERSATION') return true;
// Keep sys variables from start nodes
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
// Keep variables from non-start nodes
if (variable.nodeData?.type !== 'start') return true;
// Filter out custom variables from start nodes
return false;
}), ...cycleVarSuggestions];
}
// Filter options for condition list: only sys variables from start nodes and conversation variables
return getFilteredVariableList(selectedNode?.data?.type).filter(variable => {
const cycleVars = values?.cycle_vars || [];
const cycleVarSuggestions: Suggestion[] = cycleVars.filter(vo => vo.name && vo.name.trim() !== '').map((cycleVar: any) => ({
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${selectedNode.getData().id}.${cycleVar.name}`,
nodeData: selectedNode.getData(),
}));
return [...variableList.filter(variable => {
// Keep conversation variables
if (variable.group === 'CONVERSATION') return true;
// Keep sys variables from start nodes
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
// Keep variables from non-start nodes
if (variable.nodeData?.type !== 'start') return true;
if (variable.nodeData?.type !== 'start' && variable.nodeData?.type !== 'http-request' && variable.dataType !== 'boolean') return true;
// Filter out custom variables from start nodes
return false;
});
}), ...cycleVarSuggestions];
})()
}
selectedNode={selectedNode}

View File

@@ -342,7 +342,7 @@ export const useWorkflowGraph = ({
},
},
},
zIndex: 0
zIndex: targetCell.getData()?.cycle ? 3 : 0
}
return edgeConfig
@@ -434,13 +434,13 @@ export const useWorkflowGraph = ({
);
};
// 显示/隐藏连接桩
const showPorts = (show: boolean) => {
const container = containerRef.current!;
const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGElement>;
for (let i = 0, len = ports.length; i < len; i += 1) {
ports[i].style.visibility = show ? 'visible' : 'hidden';
}
};
// const showPorts = (show: boolean) => {
// const container = containerRef.current!;
// const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGElement>;
// for (let i = 0, len = ports.length; i < len; i += 1) {
// ports[i].style.visibility = show ? 'visible' : 'hidden';
// }
// };
// 节点选择事件
const nodeClick = ({ node }: { node: Node }) => {
// 忽略 add-node 类型的节点点击