Merge branch 'release/v0.2.3' into develop

# Conflicts:
#	api/app/core/agent/langchain_agent.py
#	api/app/core/memory/agent/langgraph_graph/write_graph.py
#	api/app/repositories/neo4j/graph_saver.py
#	api/app/services/draft_run_service.py
This commit is contained in:
Mark
2026-02-06 14:48:50 +08:00
45 changed files with 973 additions and 850 deletions

View File

@@ -8,6 +8,7 @@ import { request } from '@/utils/request'
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData, OntologyExportModalData } from '@/views/Ontology/types'
// Scene list
export const getOntologyScenesSimpleUrl = '/memory/ontology/scenes/simple'
export const getOntologyScenesUrl = '/memory/ontology/scenes'
export const getOntologyScenesList = (data: Query) => {
return request.get(getOntologyScenesUrl, data)

View File

@@ -0,0 +1,150 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-04 17:20:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 17:20:52
*/
import { useEffect, useRef, useMemo } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { python } from '@codemirror/lang-python';
import { javascript } from '@codemirror/lang-javascript';
import { java } from '@codemirror/lang-java';
import { cpp } from '@codemirror/lang-cpp';
import { rust } from '@codemirror/lang-rust';
import { oneDark } from '@codemirror/theme-one-dark';
/**
* Props for the CodeMirrorEditor component
* @property {string} value - The initial code content to display in the editor
* @property {string} language - Programming language for syntax highlighting (python, python3, javascript, typescript, java, cpp, c, rust)
* @property {function} onChange - Callback function triggered when editor content changes, receives the new code value
* @property {string} theme - Editor theme, either 'light' or 'dark'
* @property {boolean} readOnly - Whether the editor is read-only
* @property {string} height - Custom height for the editor
* @property {string} size - Predefined size preset: 'default' (120px min-height, 14px font) or 'small' (60px min-height, 12px font)
*/
interface CodeMirrorEditorProps {
value?: string;
language?: 'python' | 'python3' | 'javascript' | 'typescript' | 'java' | 'cpp' | 'c' | 'rust';
onChange?: (value: string) => void;
theme?: 'light' | 'dark';
readOnly?: boolean;
height?: string;
size?: 'default' | 'small';
}
/**
* Map of language identifiers to their corresponding CodeMirror language extensions
* Supports multiple programming languages with syntax highlighting
*/
const languageExtensions: Record<string, any> = {
python: python(),
python3: python(),
javascript: javascript(),
typescript: javascript({ typescript: true }),
java: java(),
cpp: cpp(),
c: cpp(),
rust: rust(),
};
/**
* CodeMirrorEditor - A React wrapper component for CodeMirror 6 editor
* Provides a code editor with syntax highlighting, theme support, and customizable sizing
* Used in workflow code execution nodes for editing Python and JavaScript code
*/
const CodeMirrorEditor = ({
value = '',
language = 'javascript',
onChange,
theme = 'light',
readOnly = false,
size,
}: CodeMirrorEditorProps) => {
// Reference to the DOM element that will contain the editor
const editorRef = useRef<HTMLDivElement>(null);
// Reference to the CodeMirror EditorView instance
const viewRef = useRef<EditorView | null>(null);
/**
* Initialize CodeMirror editor when component mounts or when language/theme/readOnly changes
* Sets up extensions for syntax highlighting, change listeners, and theme
*/
useEffect(() => {
if (!editorRef.current) return;
// Get the appropriate language extension, fallback to JavaScript if not found
const langExtension = languageExtensions[language] || languageExtensions.javascript;
// Configure editor extensions
const extensions = [
basicSetup, // Basic editor features (line numbers, bracket matching, etc.)
langExtension, // Language-specific syntax highlighting
// Listen for document changes and trigger onChange callback
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) {
onChange(update.state.doc.toString());
}
}),
EditorState.readOnly.of(readOnly), // Set read-only mode
];
// Apply dark theme if specified
if (theme === 'dark') {
extensions.push(oneDark);
}
// Create editor state with initial value and extensions
const state = EditorState.create({
doc: value,
extensions,
});
// Create and mount the editor view
viewRef.current = new EditorView({
state,
parent: editorRef.current,
});
// Cleanup: destroy editor instance when component unmounts or dependencies change
return () => {
viewRef.current?.destroy();
};
}, [language, theme, readOnly]);
/**
* Update editor content when the value prop changes externally
* Only updates if the new value differs from current editor content
*/
useEffect(() => {
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
viewRef.current.dispatch({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: value,
},
});
}
}, [value]);
// Calculate minimum height based on size prop: small (60px) or default (120px)
const minHeight = useMemo(() => {
return `${size === 'small' ? 60 : 120}px`
}, [size])
// Calculate font size based on size prop: small (12px) or default (14px)
const fontSize = useMemo(() => {
return `${size === 'small' ? 12 : 14}px`
}, [size])
// Calculate line height based on size prop: small (16px) or default (20px)
const lineHeight = useMemo(() => {
return `${size === 'small' ? 16 : 20}px`
}, [size])
return <div ref={editorRef} style={{ minHeight, fontSize, lineHeight }} />;
};
export default CodeMirrorEditor;

View File

@@ -81,7 +81,7 @@ const components = {
audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>,
button: ({ children }: any) => <RbButton node={{ children }}>{[children]}</RbButton>,
table: ({ children, ...props }: any) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table>,
table: ({ children, ...props }: any) => <div className="rb:overflow-x-auto rb:max-w-full"><table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table></div>,
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>,
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,

View File

@@ -180,4 +180,9 @@ body {
.x6-node foreignObject > body {
min-height: 100%;
max-height: 100%;
}
.ͼ2 .cm-gutters {
background-color: #FFFFFF;
border: none;
}

View File

@@ -140,7 +140,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
title={t('application.knowledgeBaseAssociation')}
extra={
<Space>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('workflow.config.knowledge-retrieval.recallConfig')}</Button>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('application.globalConfig')}</Button>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button>
</Space>
}

View File

@@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next';
import type { MemoryFormData, Memory, MemoryFormRef } from '../types';
import RbModal from '@/components/RbModal'
import { createMemoryConfig, updateMemoryConfig } from '@/api/memory'
import { getOntologyScenesUrl } from '@/api/ontology'
import { getOntologyScenesSimpleUrl } from '@/api/ontology'
import CustomSelect from '@/components/CustomSelect';
const FormItem = Form.Item;
@@ -129,8 +129,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
>
<CustomSelect
placeholder={t('common.pleaseSelect')}
url={getOntologyScenesUrl}
params={{ pagesize: 100, page: 1 }}
url={getOntologyScenesSimpleUrl}
hasAll={false}
valueKey='scene_id'
labelKey="scene_name"

View File

@@ -112,7 +112,7 @@ const MemoryManagement: React.FC = () => {
title={item.config_name}
>
<Tooltip title={item.config_desc}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.config_desc}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1 rb:h-[17px]">{item.config_desc}</div>
</Tooltip>
<RbAlert className="rb:mt-3 ">
<div className={clsx("rb:flex rb:gap-5 rb:font-regular rb:text-[14px]")}>

View File

@@ -103,9 +103,9 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
{model.api_keys && model.api_keys.length > 0 && (
<div className="rb:mb-4">
{model.api_keys.map((key) => (
<div key={key.id} className="rb:flex rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
<div>
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium">{key.api_key}</div>
<div key={key.id} className="rb:flex rb:gap-3 rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
<div className="rb:flex-1">
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium rb:break-all">{key.api_key}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{key.api_base}</div>
</div>
<Button type="primary" danger ghost onClick={() => handleDelete(key.id)}>{t('common.remove')}</Button>

View File

@@ -15,8 +15,6 @@ import CharacterCountPlugin from './plugin/CharacterCountPlugin'
import InitialValuePlugin from './plugin/InitialValuePlugin';
import CommandPlugin from './plugin/CommandPlugin';
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
import Python3HighlightPlugin from './plugin/Python3HighlightPlugin';
import JavaScriptHighlightPlugin from './plugin/JavaScriptHighlightPlugin';
import LineNumberPlugin from './plugin/LineNumberPlugin';
import BlurPlugin from './plugin/BlurPlugin';
import { VariableNode } from './nodes/VariableNode'
@@ -32,7 +30,7 @@ export interface LexicalEditorProps {
lineHeight?: number;
size?: 'default' | 'small';
type?: 'input' | 'textarea',
language?: 'string' | 'jinja2' | 'python3' | 'javascript'
language?: 'string' | 'jinja2'
}
const theme = {
@@ -67,7 +65,7 @@ const Editor: FC<LexicalEditorProps> =({
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
useEffect(() => {
const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript';
const needsLineNumbers = language === 'jinja2';
setEnableJinja2(language === 'jinja2');
setEnableLineNumbers(needsLineNumbers);
@@ -237,13 +235,11 @@ const Editor: FC<LexicalEditorProps> =({
<HistoryPlugin />
<CommandPlugin />
{language === 'jinja2' && <Jinja2HighlightPlugin />}
{language === 'python3' && <Python3HighlightPlugin />}
{language === 'javascript' && <JavaScriptHighlightPlugin />}
{enableLineNumbers && <LineNumberPlugin />}
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
<InitialValuePlugin key={language} value={value} options={options} enableLineNumbers={enableLineNumbers} />
{enableLineNumbers && <BlurPlugin />}
<InitialValuePlugin value={value} options={options} enableLineNumbers={enableLineNumbers} />
{enableJinja2 && <BlurPlugin />}
</div>
</LexicalComposer>
);

View File

@@ -1,182 +0,0 @@
import { useEffect, useRef } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical';
const JS_KEYWORDS = new Set([
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import',
'in', 'instanceof', 'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try',
'typeof', 'var', 'void', 'while', 'with', 'yield', 'true', 'false', 'null', 'undefined'
]);
const JavaScriptHighlightPlugin = () => {
const [editor] = useLexicalComposerContext();
const isPastingRef = useRef(false);
useEffect(() => {
return editor.registerCommand(
PASTE_COMMAND,
() => {
isPastingRef.current = true;
setTimeout(() => {
isPastingRef.current = false;
}, 100);
return false;
},
COMMAND_PRIORITY_LOW
);
}, [editor]);
useEffect(() => {
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
if (isPastingRef.current) return;
const text = textNode.getTextContent();
if (textNode.hasFormat('code')) return;
if (!needsHighlight(text)) return;
if (textNode.getStyle()) return;
const parent = textNode.getParent();
if (!parent) return;
const selection = $getSelection();
let selectionOffset = null;
if ($isRangeSelection(selection)) {
const anchor = selection.anchor;
if (anchor.getNode() === textNode) {
selectionOffset = anchor.offset;
}
}
const tokens = tokenizeJavaScript(text);
if (tokens.length <= 1) return;
const newNodes = tokens.map(token => {
const newNode = $createTextNode(token.text);
newNode.toggleFormat('code');
switch (token.type) {
case 'keyword':
newNode.setStyle('color: #d73a49; font-weight: 600;');
break;
case 'string':
newNode.setStyle('color: #032f62;');
break;
case 'comment':
newNode.setStyle('color: #6a737d; font-style: italic;');
break;
case 'number':
newNode.setStyle('color: #005cc5; font-weight: 500;');
break;
case 'function':
newNode.setStyle('color: #6f42c1; font-weight: 500;');
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]);
}
if (selectionOffset !== null && $isRangeSelection(selection)) {
let currentOffset = 0;
for (const node of newNodes) {
const nodeLength = node.getTextContent().length;
if (currentOffset + nodeLength >= selectionOffset) {
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
break;
}
currentOffset += nodeLength;
}
}
}
});
}, [editor]);
return null;
};
function needsHighlight(text: string): boolean {
return /[a-zA-Z0-9_/"'`]/.test(text);
}
function tokenizeJavaScript(text: string): Array<{text: string, type: string}> {
const tokens: Array<{text: string, type: string}> = [];
let i = 0;
while (i < text.length) {
// Single-line comments
if (text.slice(i, i + 2) === '//') {
let start = i;
while (i < text.length && text[i] !== '\n') i++;
tokens.push({ text: text.slice(start, i), type: 'comment' });
continue;
}
// Multi-line comments
if (text.slice(i, i + 2) === '/*') {
let start = i;
i += 2;
while (i < text.length && text.slice(i, i + 2) !== '*/') i++;
if (i < text.length) i += 2;
tokens.push({ text: text.slice(start, i), type: 'comment' });
continue;
}
// Strings
if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
const quote = text[i];
let start = i++;
while (i < text.length) {
if (text[i] === quote && text[i - 1] !== '\\') {
i++;
break;
}
i++;
}
tokens.push({ text: text.slice(start, i), type: 'string' });
continue;
}
// 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;
}
// Keywords and identifiers
if (/[a-zA-Z_$]/.test(text[i])) {
let start = i;
while (i < text.length && /[a-zA-Z0-9_$]/.test(text[i])) i++;
const word = text.slice(start, i);
if (JS_KEYWORDS.has(word)) {
tokens.push({ text: word, type: 'keyword' });
} else if (i < text.length && text[i] === '(') {
tokens.push({ text: word, type: 'function' });
} else {
tokens.push({ text: word, type: 'text' });
}
continue;
}
// Other characters
let start = i;
while (i < text.length && !/[a-zA-Z0-9_$/"'`]/.test(text[i])) i++;
if (start < i) {
tokens.push({ text: text.slice(start, i), type: 'text' });
}
}
return tokens;
}
export default JavaScriptHighlightPlugin;

View File

@@ -1,177 +0,0 @@
import { useEffect, useRef } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical';
const PYTHON_KEYWORDS = new Set([
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import',
'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while',
'with', 'yield'
]);
const Python3HighlightPlugin = () => {
const [editor] = useLexicalComposerContext();
const isPastingRef = useRef(false);
useEffect(() => {
return editor.registerCommand(
PASTE_COMMAND,
() => {
isPastingRef.current = true;
setTimeout(() => {
isPastingRef.current = false;
}, 100);
return false;
},
COMMAND_PRIORITY_LOW
);
}, [editor]);
useEffect(() => {
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
if (isPastingRef.current) return;
const text = textNode.getTextContent();
if (textNode.hasFormat('code')) return;
if (textNode.getStyle()) return;
if (!needsHighlight(text)) return;
const parent = textNode.getParent();
if (!parent) return;
const selection = $getSelection();
let selectionOffset = null;
if ($isRangeSelection(selection)) {
const anchor = selection.anchor;
if (anchor.getNode() === textNode) {
selectionOffset = anchor.offset;
}
}
const tokens = tokenizePython(text);
if (tokens.length <= 1) return;
const newNodes = tokens.map(token => {
const newNode = $createTextNode(token.text);
newNode.toggleFormat('code');
switch (token.type) {
case 'keyword':
newNode.setStyle('color: #d73a49; font-weight: 600;');
break;
case 'string':
newNode.setStyle('color: #032f62;');
break;
case 'comment':
newNode.setStyle('color: #6a737d; font-style: italic;');
break;
case 'number':
newNode.setStyle('color: #005cc5; font-weight: 500;');
break;
case 'function':
newNode.setStyle('color: #6f42c1; font-weight: 500;');
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]);
}
if (selectionOffset !== null && $isRangeSelection(selection)) {
let currentOffset = 0;
for (const node of newNodes) {
const nodeLength = node.getTextContent().length;
if (currentOffset + nodeLength >= selectionOffset) {
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
break;
}
currentOffset += nodeLength;
}
}
}
});
}, [editor]);
return null;
};
function needsHighlight(text: string): boolean {
return /[a-zA-Z0-9_#"']/.test(text);
}
function tokenizePython(text: string): Array<{text: string, type: string}> {
const tokens: Array<{text: string, type: string}> = [];
let i = 0;
while (i < text.length) {
// Comments
if (text[i] === '#') {
let start = i;
while (i < text.length && text[i] !== '\n') i++;
tokens.push({ text: text.slice(start, i), type: 'comment' });
continue;
}
// Strings
if (text[i] === '"' || text[i] === "'") {
const quote = text[i];
let start = i++;
const isTriple = text.slice(start, start + 3) === quote.repeat(3);
if (isTriple) i += 2;
while (i < text.length) {
if (isTriple && text.slice(i, i + 3) === quote.repeat(3)) {
i += 3;
break;
} else if (!isTriple && text[i] === quote && text[i - 1] !== '\\') {
i++;
break;
}
i++;
}
tokens.push({ text: text.slice(start, i), type: 'string' });
continue;
}
// 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;
}
// Keywords and identifiers
if (/[a-zA-Z_]/.test(text[i])) {
let start = i;
while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) i++;
const word = text.slice(start, i);
if (PYTHON_KEYWORDS.has(word)) {
tokens.push({ text: word, type: 'keyword' });
} else if (i < text.length && text[i] === '(') {
tokens.push({ text: word, type: 'function' });
} else {
tokens.push({ text: word, type: 'text' });
}
continue;
}
// Other characters
let start = i;
while (i < text.length && !/[a-zA-Z0-9_#"']/.test(text[i])) i++;
if (start < i) {
tokens.push({ text: text.slice(start, i), type: 'text' });
}
}
return tokens;
}
export default Python3HighlightPlugin;

View File

@@ -5,8 +5,8 @@ import { Node } from '@antv/x6'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import MappingList from '../MappingList'
import Editor from '../../Editor'
import OutputList from './OutputList'
import CodeMirrorEditor from '@/components/CodeMirrorEditor';
interface MappingItem {
name?: string
@@ -110,7 +110,10 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.language !== curr.language}>
{() => (
<Form.Item name="code" noStyle>
<Editor size="small" language={form.getFieldValue('language')} />
<CodeMirrorEditor
language={form.getFieldValue('language')}
size="small"
/>
</Form.Item>
)}
</Form.Item>

View File

@@ -126,7 +126,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
<div
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/recall_hover.svg')]"
></div>
{t('workflow.config.knowledge-retrieval.recallConfig')}
{t('application.globalConfig')}
</Button>
</div>