-
-
+
-
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
{{ row?.name }}
@@ -63,7 +56,7 @@
checkedOperateChange(TeamEnum.MANAGE, row, e)"
+ v-model="row.operate[AuthorizationEnum.MANAGE]"
+ @change="(e: boolean) => checkedOperateChange(AuthorizationEnum.MANAGE, row, e)"
/>
checkedOperateChange(TeamEnum.USE, row, e)"
+ v-model="row.operate[AuthorizationEnum.USE]"
+ @change="(e: boolean) => checkedOperateChange(AuthorizationEnum.USE, row, e)"
/>
@@ -111,92 +104,96 @@
diff --git a/ui/src/workflow/common/AddFormCollect.vue b/ui/src/workflow/common/AddFormCollect.vue
new file mode 100644
index 000000000..db15703a6
--- /dev/null
+++ b/ui/src/workflow/common/AddFormCollect.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/common/CustomLine.vue b/ui/src/workflow/common/CustomLine.vue
new file mode 100644
index 000000000..48146e5a9
--- /dev/null
+++ b/ui/src/workflow/common/CustomLine.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/common/EditFormCollect.vue b/ui/src/workflow/common/EditFormCollect.vue
new file mode 100644
index 000000000..6ebbaf95e
--- /dev/null
+++ b/ui/src/workflow/common/EditFormCollect.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/common/NodeCascader.vue b/ui/src/workflow/common/NodeCascader.vue
new file mode 100644
index 000000000..66a039cfe
--- /dev/null
+++ b/ui/src/workflow/common/NodeCascader.vue
@@ -0,0 +1,87 @@
+
+
+
+
+ {{
+ data.label
+ }}
+
+
+
+
+
+
diff --git a/ui/src/workflow/common/NodeContainer.vue b/ui/src/workflow/common/NodeContainer.vue
new file mode 100644
index 000000000..b61fe25b6
--- /dev/null
+++ b/ui/src/workflow/common/NodeContainer.vue
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+
{{ nodeModel.properties.stepName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('views.applicationWorkflow.condition.title') }}
+
+ {{ $t('views.applicationWorkflow.condition.front') }}
+
+
+
+
+ {{ $t('views.applicationWorkflow.condition.text') }}
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('common.rename')
+ }}
+ {{
+ $t('common.copy')
+ }}
+ {{
+ $t('common.delete')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.param.outputParam') }}
+
+
+
+
{{ item.label }} {{ '{' + item.value + '}' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/common/NodeControl.vue b/ui/src/workflow/common/NodeControl.vue
new file mode 100644
index 000000000..8e822b097
--- /dev/null
+++ b/ui/src/workflow/common/NodeControl.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/common/app-node.ts b/ui/src/workflow/common/app-node.ts
new file mode 100644
index 000000000..3665cb262
--- /dev/null
+++ b/ui/src/workflow/common/app-node.ts
@@ -0,0 +1,407 @@
+import Components from '@/components'
+import ElementPlus from 'element-plus'
+import * as ElementPlusIcons from '@element-plus/icons-vue'
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+import { HtmlResize } from '@logicflow/extension'
+import { h as lh } from '@logicflow/core'
+import { createApp, h } from 'vue'
+import directives from '@/directives'
+import i18n from '@/locales'
+import { WorkflowType } from '@/enums/workflow'
+import { nodeDict } from '@/workflow/common/data'
+import { isActive, connect, disconnect } from './teleport'
+import { t } from '@/locales'
+import { type Dict } from '@/api/type/common'
+class AppNode extends HtmlResize.view {
+ isMounted
+ r?: any
+ component: any
+ app: any
+ root?: any
+ VueNode: any
+ up_node_field_dict?: Dict
>
+ constructor(props: any, VueNode: any) {
+ super(props)
+ this.component = VueNode
+ this.isMounted = false
+ props.model.clear_next_node_field = this.clear_next_node_field.bind(this)
+ props.model.get_up_node_field_dict = this.get_up_node_field_dict.bind(this)
+ props.model.get_node_field_list = this.get_node_field_list.bind(this)
+ props.model.get_up_node_field_list = this.get_up_node_field_list.bind(this)
+
+ if (props.model.properties.noRender) {
+ delete props.model.properties.noRender
+ } else {
+ const filterNodes = props.graphModel.nodes.filter((v: any) => v.type === props.model.type)
+ const filterNameSameNodes = filterNodes.filter(
+ (v: any) => v.properties.stepName === props.model.properties.stepName
+ )
+ if (filterNameSameNodes.length - 1 > 0) {
+ getNodesName(filterNameSameNodes.length - 1)
+ }
+ }
+ function getNodesName(num: number) {
+ const number = num
+ const name = props.model.properties.stepName + number
+ if (!props.graphModel.nodes?.some((node: any) => node.properties.stepName === name.trim())) {
+ props.model.properties.stepName = name
+ } else {
+ getNodesName(number + 1)
+ }
+ }
+ props.model.properties.config = nodeDict[props.model.type].properties.config
+ if (props.model.properties.height) {
+ props.model.height = props.model.properties.height
+ }
+ }
+ get_node_field_list() {
+ const result = []
+ if (this.props.model.type === 'start-node') {
+ result.push({
+ value: 'global',
+ label: t('views.applicationWorkflow.variable.global'),
+ type: 'global',
+ children: this.props.model.properties?.config?.globalFields || []
+ })
+ }
+ result.push({
+ value: this.props.model.id,
+ label: this.props.model.properties.stepName,
+ type: this.props.model.type,
+ children: this.props.model.properties?.config?.fields || []
+ })
+ return result
+ }
+ get_up_node_field_dict(contain_self: boolean, use_cache: boolean) {
+ if (!this.up_node_field_dict || !use_cache) {
+ const up_node_list = this.props.graphModel.getNodeIncomingNode(this.props.model.id)
+ this.up_node_field_dict = up_node_list
+ .filter((node) => node.id != 'start-node')
+ .map((node) => node.get_up_node_field_dict(true, use_cache))
+ .reduce((pre, next) => ({ ...pre, ...next }), {})
+ }
+ if (contain_self) {
+ return {
+ ...this.up_node_field_dict,
+ [this.props.model.id]: this.get_node_field_list()
+ }
+ }
+ return this.up_node_field_dict ? this.up_node_field_dict : {}
+ }
+
+ get_up_node_field_list(contain_self: boolean, use_cache: boolean) {
+ const result = Object.values(this.get_up_node_field_dict(contain_self, use_cache)).reduce(
+ (pre, next) => [...pre, ...next],
+ []
+ )
+ const start_node_field_list = this.props.graphModel
+ .getNodeModelById('start-node')
+ .get_node_field_list()
+ return [...start_node_field_list, ...result]
+ }
+
+ clear_next_node_field(contain_self: boolean) {
+ const next_node_list = this.props.graphModel.getNodeOutgoingNode(this.props.model.id)
+ next_node_list.forEach((node) => {
+ node.clear_next_node_field(true)
+ })
+ if (contain_self) {
+ this.up_node_field_dict = undefined
+ }
+ }
+ getAnchorShape(anchorData: any) {
+ const { x, y, type } = anchorData
+ let isConnect = false
+
+ if (type == 'left') {
+ isConnect = this.props.graphModel.edges.some((edge) => edge.targetAnchorId == anchorData.id)
+ } else {
+ isConnect = this.props.graphModel.edges.some((edge) => edge.sourceAnchorId == anchorData.id)
+ }
+
+ return lh(
+ 'foreignObject',
+ {
+ ...anchorData,
+ x: x - 10,
+ y: y - 12,
+ width: 30,
+ height: 30
+ },
+ [
+ lh('div', {
+ style: { zindex: 0 },
+ onClick: () => {
+ if (type == 'right') {
+ this.props.model.openNodeMenu(anchorData)
+ }
+ },
+ dangerouslySetInnerHTML: {
+ __html: isConnect
+ ? `
+ `
+ : ``
+ }
+ })
+ ]
+ )
+ }
+
+ setHtml(rootEl: HTMLElement) {
+ if (!this.isMounted) {
+ this.isMounted = true
+ const node = document.createElement('div')
+ rootEl.appendChild(node)
+ this.renderVueComponent(node)
+ } else {
+ if (this.r && this.r.component) {
+ this.r.component.props.properties = this.props.model.getProperties()
+ }
+ }
+ }
+ componentWillUnmount() {
+ super.componentWillUnmount()
+ this.unmount()
+ }
+ getComponentContainer() {
+ return this.root
+ }
+ protected targetId() {
+ return `${this.props.graphModel.flowId}:${this.props.model.id}`
+ }
+ protected renderVueComponent(root: any) {
+ this.unmountVueComponent()
+ this.root = root
+ const { model, graphModel } = this.props
+
+ if (root) {
+ if (isActive()) {
+ connect(this.targetId(), this.component, root, model, graphModel)
+ } else {
+ this.r = h(this.component, {
+ properties: this.props.model.properties,
+ nodeModel: this.props.model
+ })
+ this.app = createApp({
+ render() {
+ return this.r
+ },
+ provide() {
+ return {
+ getNode: () => model,
+ getGraph: () => graphModel
+ }
+ }
+ })
+
+ this.app.use(ElementPlus, {
+ locale: zhCn
+ })
+ this.app.use(Components)
+ this.app.use(directives)
+ this.app.use(i18n)
+ for (const [key, component] of Object.entries(ElementPlusIcons)) {
+ this.app.component(key, component)
+ }
+ this.app?.mount(root)
+ }
+ }
+ }
+
+ protected unmountVueComponent() {
+ if (this.app) {
+ this.app.unmount()
+ this.app = null
+ }
+ if (this.root) {
+ this.root.innerHTML = ''
+ }
+ return this.root
+ }
+
+ unmount() {
+ if (isActive()) {
+ disconnect(this.targetId())
+ }
+ this.unmountVueComponent()
+ }
+}
+
+class AppNodeModel extends HtmlResize.model {
+ refreshDeges() {
+ // 更新节点连接边的path
+ this.incoming.edges.forEach((edge: any) => {
+ // 调用自定义的更新方案
+ edge.updatePathByAnchor()
+ })
+ this.outgoing.edges.forEach((edge: any) => {
+ edge.updatePathByAnchor()
+ })
+ }
+ set_position(position: { x?: number; y?: number }) {
+ const { x, y } = position
+ if (x) {
+ this.x = x
+ }
+ if (y) {
+ this.y = y
+ }
+ this.refreshDeges()
+ }
+ getResizeOutlineStyle() {
+ const style = super.getResizeOutlineStyle()
+ style.stroke = 'none'
+ return style
+ }
+ getControlPointStyle() {
+ const style = super.getControlPointStyle()
+ style.stroke = 'none'
+ style.fill = 'none'
+ return style
+ }
+ getNodeStyle() {
+ return {
+ overflow: 'visible'
+ }
+ }
+ getOutlineStyle() {
+ const style = super.getOutlineStyle()
+ style.stroke = 'none'
+ if (style.hover) {
+ style.hover.stroke = 'none'
+ }
+ return style
+ }
+ // 如果不用修改锚地形状,可以重写颜色相关样式
+ getAnchorStyle(anchorInfo: any) {
+ const style = super.getAnchorStyle(anchorInfo)
+ if (anchorInfo.type === 'left') {
+ style.fill = 'red'
+ style.hover.fill = 'transparent'
+ style.hover.stroke = 'transpanrent'
+ style.className = 'lf-hide-default'
+ } else {
+ style.fill = 'green'
+ }
+ return style
+ }
+
+ setHeight(height: number) {
+ const sourceHeight = this.height
+ const targetHeight = height + 100
+ this.height = targetHeight
+ this.properties['height'] = targetHeight
+ this.move(0, (targetHeight - sourceHeight) / 2)
+ this.outgoing.edges.forEach((edge: any) => {
+ // 调用自定义的更新方案
+ edge.updatePathByAnchor()
+ })
+ this.incoming.edges.forEach((edge: any) => {
+ // 调用自定义的更新方案
+ edge.updatePathByAnchor()
+ })
+ }
+ get_width() {
+ return this.properties?.width || 340
+ }
+
+ setAttributes() {
+ const { t } = i18n.global
+ this.width = this.get_width()
+ const isLoop = (node_id: string, target_node_id: string) => {
+ const up_node_list = this.graphModel.getNodeIncomingNode(node_id)
+ for (const index in up_node_list) {
+ const item = up_node_list[index]
+ if (item.id === target_node_id) {
+ return true
+ } else {
+ const result = isLoop(item.id, target_node_id)
+ if (result) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+ const circleOnlyAsTarget = {
+ message: t('views.applicationWorkflow.tip.onlyRight'),
+ validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
+ return sourceAnchor.type === 'right'
+ }
+ }
+ this.sourceRules.push({
+ message: t('views.applicationWorkflow.tip.notRecyclable'),
+ validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
+ return !isLoop(sourceNode.id, targetNode.id)
+ }
+ })
+
+ this.sourceRules.push(circleOnlyAsTarget)
+ this.targetRules.push({
+ message: t('views.applicationWorkflow.tip.onlyLeft'),
+ validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
+ return targetAnchor.type === 'left'
+ }
+ })
+ }
+ getDefaultAnchor() {
+ const { id, x, y, width } = this
+ const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
+ const anchors: any = []
+
+ if (this.type !== WorkflowType.Base) {
+ if (this.type !== WorkflowType.Start) {
+ anchors.push({
+ x: x - width / 2 + 10,
+ y: showNode ? y : y - 15,
+ id: `${id}_left`,
+ edgeAddable: false,
+ type: 'left'
+ })
+ }
+ anchors.push({
+ x: x + width / 2 - 10,
+ y: showNode ? y : y - 15,
+ id: `${id}_right`,
+ type: 'right'
+ })
+ }
+
+ return anchors
+ }
+}
+export { AppNodeModel, AppNode }
diff --git a/ui/src/workflow/common/data.ts b/ui/src/workflow/common/data.ts
new file mode 100644
index 000000000..6ce89c243
--- /dev/null
+++ b/ui/src/workflow/common/data.ts
@@ -0,0 +1,465 @@
+import { WorkflowType } from '@/enums/workflow'
+import { t } from '@/locales'
+
+export const startNode = {
+ id: WorkflowType.Start,
+ type: WorkflowType.Start,
+ x: 480,
+ y: 3340,
+ properties: {
+ height: 364,
+ stepName: t('views.applicationWorkflow.nodes.startNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.startNode.question'),
+ value: 'question'
+ }
+ ],
+ globalFields: [
+ { label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' },
+ {
+ label: t('views.application.applicationForm.form.historyRecord.label'),
+ value: 'history_context'
+ },
+ {
+ label: t('chat.chatId'),
+ value: 'chat_id'
+ }
+ ]
+ },
+ fields: [{ label: t('views.applicationWorkflow.nodes.startNode.question'), value: 'question' }],
+ globalFields: [
+ { label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' }
+ ],
+ showNode: true
+ }
+}
+export const baseNode = {
+ id: WorkflowType.Base,
+ type: WorkflowType.Base,
+ x: 360,
+ y: 2761.3875,
+ text: '',
+ properties: {
+ height: 728.375,
+ stepName: t('views.applicationWorkflow.nodes.baseNode.label'),
+ input_field_list: [],
+ node_data: {
+ name: '',
+ desc: '',
+ // @ts-ignore
+ prologue: t('views.application.applicationForm.form.defaultPrologue'),
+ tts_type: 'BROWSER'
+ },
+ config: {},
+ showNode: true,
+ user_input_config: { title: t('chat.userInput') },
+ user_input_field_list: []
+ }
+}
+/**
+ * 说明
+ * type 与 nodes 文件对应
+ */
+export const baseNodes = [baseNode, startNode]
+/**
+ * ai对话节点配置数据
+ */
+export const aiChatNode = {
+ type: WorkflowType.AiChat,
+ text: t('views.applicationWorkflow.nodes.aiChatNode.text'),
+ label: t('views.applicationWorkflow.nodes.aiChatNode.label'),
+ height: 340,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.aiChatNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.aiChatNode.answer'),
+ value: 'answer'
+ },
+ {
+ label: t('views.applicationWorkflow.nodes.aiChatNode.think'),
+ value: 'reasoning_content'
+ }
+ ]
+ }
+ }
+}
+/**
+ * 知识库检索配置数据
+ */
+export const searchDatasetNode = {
+ type: WorkflowType.SearchDataset,
+ text: t('views.applicationWorkflow.nodes.searchDatasetNode.text'),
+ label: t('views.applicationWorkflow.nodes.searchDatasetNode.label'),
+ height: 355,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.searchDatasetNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.searchDatasetNode.paragraph_list'),
+ value: 'paragraph_list'
+ },
+ {
+ label: t('views.applicationWorkflow.nodes.searchDatasetNode.is_hit_handling_method_list'),
+ value: 'is_hit_handling_method_list'
+ },
+ {
+ label: t('views.applicationWorkflow.nodes.searchDatasetNode.result'),
+ value: 'data'
+ },
+ {
+ label: t('views.applicationWorkflow.nodes.searchDatasetNode.directly_return'),
+ value: 'directly_return'
+ }
+ ]
+ }
+ }
+}
+export const questionNode = {
+ type: WorkflowType.Question,
+ text: t('views.applicationWorkflow.nodes.questionNode.text'),
+ label: t('views.applicationWorkflow.nodes.questionNode.label'),
+ height: 345,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.questionNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.questionNode.result'),
+ value: 'answer'
+ }
+ ]
+ }
+ }
+}
+export const conditionNode = {
+ type: WorkflowType.Condition,
+ text: t('views.applicationWorkflow.nodes.conditionNode.text'),
+ label: t('views.applicationWorkflow.nodes.conditionNode.label'),
+ height: 175,
+ properties: {
+ width: 600,
+ stepName: t('views.applicationWorkflow.nodes.conditionNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.conditionNode.branch_name'),
+ value: 'branch_name'
+ }
+ ]
+ }
+ }
+}
+export const replyNode = {
+ type: WorkflowType.Reply,
+ text: t('views.applicationWorkflow.nodes.replyNode.text'),
+ label: t('views.applicationWorkflow.nodes.replyNode.label'),
+ height: 210,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.replyNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.replyNode.content'),
+ value: 'answer'
+ }
+ ]
+ }
+ }
+}
+export const rerankerNode = {
+ type: WorkflowType.RrerankerNode,
+ text: t('views.applicationWorkflow.nodes.rerankerNode.text'),
+ label: t('views.applicationWorkflow.nodes.rerankerNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.rerankerNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.rerankerNode.result_list'),
+ value: 'result_list'
+ },
+ {
+ label: t('views.applicationWorkflow.nodes.rerankerNode.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+export const formNode = {
+ type: WorkflowType.FormNode,
+ text: t('views.applicationWorkflow.nodes.formNode.text'),
+ label: t('views.applicationWorkflow.nodes.formNode.label'),
+ height: 252,
+ properties: {
+ width: 600,
+ stepName: t('views.applicationWorkflow.nodes.formNode.label'),
+ node_data: {
+ is_result: true,
+ form_field_list: [],
+ form_content_format: `${t('views.applicationWorkflow.nodes.formNode.form_content_format1')}
+{{form}}
+${t('views.applicationWorkflow.nodes.formNode.form_content_format2')}`
+ },
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.formNode.form_data'),
+ value: 'form_data'
+ }
+ ]
+ }
+ }
+}
+export const documentExtractNode = {
+ type: WorkflowType.DocumentExtractNode,
+ text: t('views.applicationWorkflow.nodes.documentExtractNode.text'),
+ label: t('views.applicationWorkflow.nodes.documentExtractNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.documentExtractNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.documentExtractNode.content'),
+ value: 'content'
+ }
+ ]
+ }
+ }
+}
+export const imageUnderstandNode = {
+ type: WorkflowType.ImageUnderstandNode,
+ text: t('views.applicationWorkflow.nodes.imageUnderstandNode.text'),
+ label: t('views.applicationWorkflow.nodes.imageUnderstandNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.imageUnderstandNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.imageUnderstandNode.answer'),
+ value: 'answer'
+ }
+ ]
+ }
+ }
+}
+
+export const variableAssignNode = {
+ type: WorkflowType.VariableAssignNode,
+ text: t('views.applicationWorkflow.nodes.variableAssignNode.text'),
+ label: t('views.applicationWorkflow.nodes.variableAssignNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.variableAssignNode.label'),
+ config: {}
+ }
+}
+
+export const mcpNode = {
+ type: WorkflowType.McpNode,
+ text: t('views.applicationWorkflow.nodes.mcpNode.text'),
+ label: t('views.applicationWorkflow.nodes.mcpNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.mcpNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('common.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+
+export const imageGenerateNode = {
+ type: WorkflowType.ImageGenerateNode,
+ text: t('views.applicationWorkflow.nodes.imageGenerateNode.text'),
+ label: t('views.applicationWorkflow.nodes.imageGenerateNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.imageGenerateNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('views.applicationWorkflow.nodes.imageGenerateNode.answer'),
+ value: 'answer'
+ },
+ {
+ label: t('common.fileUpload.image'),
+ value: 'image'
+ }
+ ]
+ }
+ }
+}
+
+export const speechToTextNode = {
+ type: WorkflowType.SpeechToTextNode,
+ text: t('views.applicationWorkflow.nodes.speechToTextNode.text'),
+ label: t('views.applicationWorkflow.nodes.speechToTextNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.speechToTextNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('common.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+export const textToSpeechNode = {
+ type: WorkflowType.TextToSpeechNode,
+ text: t('views.applicationWorkflow.nodes.textToSpeechNode.text'),
+ label: t('views.applicationWorkflow.nodes.textToSpeechNode.label'),
+ height: 252,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.textToSpeechNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('common.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+export const menuNodes = [
+ aiChatNode,
+ imageUnderstandNode,
+ imageGenerateNode,
+ searchDatasetNode,
+ rerankerNode,
+ conditionNode,
+ replyNode,
+ formNode,
+ questionNode,
+ documentExtractNode,
+ speechToTextNode,
+ textToSpeechNode,
+ variableAssignNode,
+ mcpNode
+]
+
+/**
+ * 自定义函数配置数据
+ */
+export const functionNode = {
+ type: WorkflowType.FunctionLibCustom,
+ text: t('views.applicationWorkflow.nodes.functionNode.text'),
+ label: t('views.applicationWorkflow.nodes.functionNode.label'),
+ height: 260,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.functionNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('common.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+export const functionLibNode = {
+ type: WorkflowType.FunctionLib,
+ text: t('views.applicationWorkflow.nodes.functionNode.text'),
+ label: t('views.applicationWorkflow.nodes.functionNode.label'),
+ height: 170,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.functionNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('common.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+
+export const applicationNode = {
+ type: WorkflowType.Application,
+ text: t('views.applicationWorkflow.nodes.applicationNode.label'),
+ label: t('views.applicationWorkflow.nodes.applicationNode.label'),
+ height: 260,
+ properties: {
+ stepName: t('views.applicationWorkflow.nodes.applicationNode.label'),
+ config: {
+ fields: [
+ {
+ label: t('common.result'),
+ value: 'result'
+ }
+ ]
+ }
+ }
+}
+
+export const compareList = [
+ { value: 'is_null', label: t('views.applicationWorkflow.compare.is_null') },
+ { value: 'is_not_null', label: t('views.applicationWorkflow.compare.is_not_null') },
+ { value: 'contain', label: t('views.applicationWorkflow.compare.contain') },
+ { value: 'not_contain', label: t('views.applicationWorkflow.compare.not_contain') },
+ { value: 'eq', label: t('views.applicationWorkflow.compare.eq') },
+ { value: 'ge', label: t('views.applicationWorkflow.compare.ge') },
+ { value: 'gt', label: t('views.applicationWorkflow.compare.gt') },
+ { value: 'le', label: t('views.applicationWorkflow.compare.le') },
+ { value: 'lt', label: t('views.applicationWorkflow.compare.lt') },
+ { value: 'len_eq', label: t('views.applicationWorkflow.compare.len_eq') },
+ { value: 'len_ge', label: t('views.applicationWorkflow.compare.len_ge') },
+ { value: 'len_gt', label: t('views.applicationWorkflow.compare.len_gt') },
+ { value: 'len_le', label: t('views.applicationWorkflow.compare.len_le') },
+ { value: 'len_lt', label: t('views.applicationWorkflow.compare.len_lt') },
+ { value: 'is_true', label: t('views.applicationWorkflow.compare.is_true') },
+ { value: 'is_not_true', label: t('views.applicationWorkflow.compare.is_not_true') }
+]
+
+export const nodeDict: any = {
+ [WorkflowType.AiChat]: aiChatNode,
+ [WorkflowType.SearchDataset]: searchDatasetNode,
+ [WorkflowType.Question]: questionNode,
+ [WorkflowType.Condition]: conditionNode,
+ [WorkflowType.Base]: baseNode,
+ [WorkflowType.Start]: startNode,
+ [WorkflowType.Reply]: replyNode,
+ [WorkflowType.FunctionLib]: functionLibNode,
+ [WorkflowType.FunctionLibCustom]: functionNode,
+ [WorkflowType.RrerankerNode]: rerankerNode,
+ [WorkflowType.FormNode]: formNode,
+ [WorkflowType.Application]: applicationNode,
+ [WorkflowType.DocumentExtractNode]: documentExtractNode,
+ [WorkflowType.ImageUnderstandNode]: imageUnderstandNode,
+ [WorkflowType.TextToSpeechNode]: textToSpeechNode,
+ [WorkflowType.SpeechToTextNode]: speechToTextNode,
+ [WorkflowType.ImageGenerateNode]: imageGenerateNode,
+ [WorkflowType.VariableAssignNode]: variableAssignNode,
+ [WorkflowType.McpNode]: mcpNode
+}
+export function isWorkFlow(type: string | undefined) {
+ return type === 'WORK_FLOW'
+}
+
+export function isLastNode(nodeModel: any) {
+ const incoming = nodeModel.graphModel.getNodeIncomingNode(nodeModel.id)
+ const outcomming = nodeModel.graphModel.getNodeOutgoingNode(nodeModel.id)
+ if (incoming.length > 0 && outcomming.length === 0) {
+ return true
+ } else {
+ return false
+ }
+}
diff --git a/ui/src/workflow/common/edge.ts b/ui/src/workflow/common/edge.ts
new file mode 100644
index 000000000..461b09650
--- /dev/null
+++ b/ui/src/workflow/common/edge.ts
@@ -0,0 +1,243 @@
+import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
+import { createApp, h as vh } from 'vue'
+import { isActive, connect, disconnect } from './teleport'
+import CustomLine from './CustomLine.vue'
+function isMouseInElement(element: any, e: any) {
+ const rect = element.getBoundingClientRect()
+ return (
+ e.clientX >= rect.left &&
+ e.clientX <= rect.right &&
+ e.clientY >= rect.top &&
+ e.clientY <= rect.bottom
+ )
+}
+const DEFAULT_WIDTH = 32
+const DEFAULT_HEIGHT = 32
+class CustomEdge2 extends BezierEdge {
+ isMounted
+ customLineApp?: any
+ root?: any
+ constructor() {
+ super()
+ this.isMounted = false
+ this.handleMouseUp = (e: any) => {
+ this.props.graphModel.clearSelectElements()
+ this.props.model.isSelected = true
+ const element = e.target.parentNode.parentNode.querySelector('.lf-custom-edge-wrapper')
+ if (isMouseInElement(element, e)) {
+ this.props.model.graphModel.deleteEdgeById(this.props.model.id)
+ }
+ }
+ }
+ /**
+ * 渲染vue组件
+ * @param root
+ */
+ protected renderVueComponent(root: any) {
+ this.unmountVueComponent()
+ this.root = root
+ const { graphModel } = this.props
+ if (root) {
+ if (isActive()) {
+ connect(
+ this.targetId(),
+ CustomLine,
+ root,
+ this.props.model,
+ graphModel,
+ (node: any, graph: any) => {
+ return { model: node, graph }
+ }
+ )
+ } else {
+ this.customLineApp = createApp({
+ render: () => vh(CustomLine, { model: this.props.model })
+ })
+ this.customLineApp?.mount(root)
+ }
+ }
+ }
+ protected targetId() {
+ return `${this.props.graphModel.flowId}:${this.props.model.id}`
+ }
+ /**
+ * 组件即将卸载勾子
+ */
+ componentWillUnmount() {
+ if (super.componentWillUnmount) {
+ super.componentWillUnmount()
+ }
+ if (isActive()) {
+ disconnect(this.targetId())
+ }
+ this.unmountVueComponent()
+ }
+ /**
+ * 卸载vue
+ * @returns
+ */
+ protected unmountVueComponent() {
+ if (this.customLineApp) {
+ this.customLineApp.unmount()
+ this.customLineApp = null
+ }
+ if (this.root) {
+ this.root.innerHTML = ''
+ }
+ return this.root
+ }
+
+ getEdge() {
+ const { model } = this.props
+ const id = model.id
+ const { customWidth = DEFAULT_WIDTH, customHeight = DEFAULT_HEIGHT } = model.getProperties()
+ const { startPoint, endPoint, path, isAnimation, arrowConfig } = model
+ const animationStyle = model.getEdgeAnimationStyle()
+ const {
+ strokeDasharray,
+ stroke,
+ strokeDashoffset,
+ animationName,
+ animationDuration,
+ animationIterationCount,
+ animationTimingFunction,
+ animationDirection
+ } = animationStyle
+ const positionData = {
+ x: (startPoint.x + endPoint.x - customWidth) / 2,
+ y: (startPoint.y + endPoint.y - customHeight) / 2,
+ width: customWidth,
+ height: customHeight
+ }
+ const style = model.getEdgeStyle()
+ const wrapperStyle = {
+ width: customWidth,
+ height: customHeight
+ }
+
+ setTimeout(() => {
+ const s = document.getElementById(id)
+ if (s && !this.isMounted) {
+ this.isMounted = true
+ this.renderVueComponent(s)
+ }
+ }, 0)
+
+ delete style.stroke
+
+ return h('g', {}, [
+ h(
+ 'style' as any,
+ { type: 'text/css' },
+ '.lf-edge{stroke:#afafaf}.lf-edge:hover{stroke: #3370FF;}'
+ ),
+ h('path', {
+ d: path,
+ ...style,
+ ...arrowConfig,
+ ...(isAnimation
+ ? {
+ strokeDasharray,
+ stroke,
+ style: {
+ strokeDashoffset,
+ animationName,
+ animationDuration,
+ animationIterationCount,
+ animationTimingFunction,
+ animationDirection
+ }
+ }
+ : {})
+ }),
+ h(
+ 'foreignObject',
+ {
+ ...positionData,
+ y: positionData.y + 5,
+ x: positionData.x + 5,
+ style: {}
+ },
+ [
+ h('div', {
+ id,
+ style: { ...wrapperStyle },
+ className: 'lf-custom-edge-wrapper'
+ })
+ ]
+ )
+ ])
+ }
+}
+
+class CustomEdgeModel2 extends BezierEdgeModel {
+ getArrowStyle() {
+ const arrowStyle = super.getArrowStyle()
+ arrowStyle.offset = 1
+ arrowStyle.verticalLength = 0
+ return arrowStyle
+ }
+
+ getEdgeStyle() {
+ const style = super.getEdgeStyle()
+ // svg属性
+ style.strokeWidth = 2
+ style.stroke = '#BBBFC4'
+ style.offset = 0
+ return style
+ }
+ /**
+ * 重写此方法,使保存数据是能带上锚点数据。
+ */
+ getData() {
+ const data: any = super.getData()
+ if (data) {
+ data.sourceAnchorId = this.sourceAnchorId
+ data.targetAnchorId = this.targetAnchorId
+ }
+ return data
+ }
+ /**
+ * 给边自定义方案,使其支持基于锚点的位置更新边的路径
+ */
+ updatePathByAnchor() {
+ // TODO
+ const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
+ const sourceAnchor = sourceNodeModel
+ .getDefaultAnchor()
+ .find((anchor: any) => anchor.id === this.sourceAnchorId)
+
+ const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
+ const targetAnchor = targetNodeModel
+ .getDefaultAnchor()
+ .find((anchor: any) => anchor.id === this.targetAnchorId)
+ if (sourceAnchor && targetAnchor) {
+ const startPoint = {
+ x: sourceAnchor.x,
+ y: sourceAnchor.y
+ }
+ this.updateStartPoint(startPoint)
+ const endPoint = {
+ x: targetAnchor.x,
+ y: targetAnchor.y
+ }
+
+ this.updateEndPoint(endPoint)
+ }
+
+ // 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
+ this.pointsList = []
+ this.initPoints()
+ }
+ setAttributes(): void {
+ super.setAttributes()
+ this.isHitable = true
+ this.zIndex = 0
+ }
+}
+
+export default {
+ type: 'app-edge',
+ view: CustomEdge2,
+ model: CustomEdgeModel2
+}
diff --git a/ui/src/workflow/common/shortcut.ts b/ui/src/workflow/common/shortcut.ts
new file mode 100644
index 000000000..2b3235b6b
--- /dev/null
+++ b/ui/src/workflow/common/shortcut.ts
@@ -0,0 +1,136 @@
+import type LogicFlow from '@logicflow/core'
+import { type GraphModel } from '@logicflow/core'
+import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message'
+import { WorkflowType } from '@/enums/workflow'
+import { t } from '@/locales'
+let selected: any | null = null
+
+function translationNodeData(nodeData: any, distance: any) {
+ nodeData.x += distance
+ nodeData.y += distance
+ if (nodeData.text) {
+ nodeData.text.x += distance
+ nodeData.text.y += distance
+ }
+ return nodeData
+}
+
+function translationEdgeData(edgeData: any, distance: any) {
+ if (edgeData.startPoint) {
+ edgeData.startPoint.x += distance
+ edgeData.startPoint.y += distance
+ }
+ if (edgeData.endPoint) {
+ edgeData.endPoint.x += distance
+ edgeData.endPoint.y += distance
+ }
+ if (edgeData.pointsList && edgeData.pointsList.length > 0) {
+ edgeData.pointsList.forEach((point: any) => {
+ point.x += distance
+ point.y += distance
+ })
+ }
+ if (edgeData.text) {
+ edgeData.text.x += distance
+ edgeData.text.y += distance
+ }
+ return edgeData
+}
+
+const TRANSLATION_DISTANCE = 40
+let CHILDREN_TRANSLATION_DISTANCE = 40
+
+export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
+ const { keyboard } = lf
+ const {
+ options: { keyboard: keyboardOptions }
+ } = keyboard
+ const copy_node = () => {
+ CHILDREN_TRANSLATION_DISTANCE = TRANSLATION_DISTANCE
+ if (!keyboardOptions?.enabled) return true
+ if (graph.textEditElement) return true
+ const { guards } = lf.options
+ const elements = graph.getSelectElements(false)
+ const enabledClone = guards && guards.beforeClone ? guards.beforeClone(elements) : true
+ if (!enabledClone || (elements.nodes.length === 0 && elements.edges.length === 0)) {
+ selected = null
+ return true
+ }
+ const base_nodes = elements.nodes.filter(
+ (node: any) => node.type === WorkflowType.Start || node.type === WorkflowType.Base
+ )
+ if (base_nodes.length > 0) {
+ MsgError(base_nodes[0]?.properties?.stepName + t('views.applicationWorkflow.tip.cannotCopy'))
+ return
+ }
+ selected = elements
+ selected.nodes.forEach((node: any) => translationNodeData(node, TRANSLATION_DISTANCE))
+ selected.edges.forEach((edge: any) => translationEdgeData(edge, TRANSLATION_DISTANCE))
+ MsgSuccess(t('views.applicationWorkflow.tip.copyError'))
+ return false
+ }
+ const paste_node = () => {
+ if (!keyboardOptions?.enabled) return true
+ if (graph.textEditElement) return true
+ if (selected && (selected.nodes || selected.edges)) {
+ lf.clearSelectElements()
+ const addElements = lf.addElements(selected, CHILDREN_TRANSLATION_DISTANCE)
+ if (!addElements) return true
+ addElements.nodes.forEach((node) => lf.selectElementById(node.id, true))
+ addElements.edges.forEach((edge) => lf.selectElementById(edge.id, true))
+ selected.nodes.forEach((node: any) => translationNodeData(node, TRANSLATION_DISTANCE))
+ selected.edges.forEach((edge: any) => translationEdgeData(edge, TRANSLATION_DISTANCE))
+ CHILDREN_TRANSLATION_DISTANCE = CHILDREN_TRANSLATION_DISTANCE + TRANSLATION_DISTANCE
+ }
+ return false
+ }
+ const delete_node = () => {
+ const elements = graph.getSelectElements(true)
+ lf.clearSelectElements()
+ if (elements.nodes.length == 0 && elements.edges.length == 0) {
+ return
+ }
+ if (elements.edges.length > 0 && elements.nodes.length == 0) {
+ elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
+ return
+ }
+ const nodes = elements.nodes.filter((node) => ['start-node', 'base-node'].includes(node.type))
+ if (nodes.length > 0) {
+ MsgError(`${nodes[0].properties?.stepName}${t('views.applicationWorkflow.delete.deleteMessage')}`)
+ return
+ }
+ MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
+ confirmButtonText: t('common.confirm'),
+ confirmButtonClass: 'danger'
+ }).then(() => {
+ if (!keyboardOptions?.enabled) return true
+ if (graph.textEditElement) return true
+
+ elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
+ elements.nodes.forEach((node: any) => lf.deleteNode(node.id))
+ })
+
+ return false
+ }
+ graph.eventCenter.on('copy_node', copy_node)
+ // 复制
+ keyboard.on(['cmd + c', 'ctrl + c'], copy_node)
+ // 粘贴
+ keyboard.on(['cmd + v', 'ctrl + v'], paste_node)
+ // undo
+ keyboard.on(['cmd + z', 'ctrl + z'], () => {
+ // if (!keyboardOptions?.enabled) return true
+ // if (graph.textEditElement) return true
+ // lf.undo()
+ // return false
+ })
+ // redo
+ keyboard.on(['cmd + y', 'ctrl + y'], () => {
+ if (!keyboardOptions?.enabled) return true
+ if (graph.textEditElement) return true
+ lf.redo()
+ return false
+ })
+ // delete
+ keyboard.on(['backspace'], delete_node)
+}
diff --git a/ui/src/workflow/common/teleport.ts b/ui/src/workflow/common/teleport.ts
new file mode 100644
index 000000000..6f88b414f
--- /dev/null
+++ b/ui/src/workflow/common/teleport.ts
@@ -0,0 +1,80 @@
+import { BaseEdgeModel, BaseNodeModel, GraphModel } from '@logicflow/core'
+import { defineComponent, h, reactive, isVue3, Teleport, markRaw, Fragment } from 'vue-demi'
+
+let active = false
+const items = reactive<{ [key: string]: any }>({})
+
+export function connect(
+ id: string,
+ component: any,
+ container: HTMLDivElement,
+ node: BaseNodeModel | BaseEdgeModel,
+ graph: GraphModel,
+ get_props?: any
+) {
+ if (!get_props) {
+ get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
+ return { nodeModel: node, graph }
+ }
+ }
+ if (active) {
+ items[id] = markRaw(
+ defineComponent({
+ render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
+ provide: () => ({
+ getNode: () => node,
+ getGraph: () => graph
+ })
+ })
+ )
+ }
+}
+
+export function disconnect(id: string) {
+ if (active) {
+ delete items[id]
+ }
+}
+
+export function isActive() {
+ return active
+}
+
+export function getTeleport(): any {
+ if (!isVue3) {
+ throw new Error('teleport is only available in Vue3')
+ }
+ active = true
+
+ return defineComponent({
+ props: {
+ flowId: {
+ type: String,
+ required: true
+ }
+ },
+ setup(props) {
+ return () => {
+ const children: Record[] = []
+ Object.keys(items).forEach((id) => {
+ // https://github.com/didi/LogicFlow/issues/1768
+ // 多个不同的VueNodeView都会connect注册到items中,因此items存储了可能有多个flowId流程图的数据
+ // 当使用多个LogicFlow时,会创建多个flowId + 同时使用KeepAlive
+ // 每一次items改变,会触发不同flowId持有的setup()执行,由于每次setup()执行就是遍历items,因此存在多次重复渲染元素的问题
+ // 即items[0]会在Page1的setup()执行,items[0]也会在Page2的setup()执行,从而生成两个items[0]
+
+ // 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
+ // 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
+ if (id.startsWith(props.flowId)) {
+ children.push(items[id])
+ }
+ })
+ return h(
+ Fragment,
+ {},
+ children.map((item) => h(item))
+ )
+ }
+ }
+ })
+}
diff --git a/ui/src/workflow/common/validate.ts b/ui/src/workflow/common/validate.ts
new file mode 100644
index 000000000..cc9378bc3
--- /dev/null
+++ b/ui/src/workflow/common/validate.ts
@@ -0,0 +1,150 @@
+import { WorkflowType } from '@/enums/workflow'
+import { t } from '@/locales'
+
+const end_nodes: Array = [
+ WorkflowType.AiChat,
+ WorkflowType.Reply,
+ WorkflowType.FunctionLib,
+ WorkflowType.FunctionLibCustom,
+ WorkflowType.ImageUnderstandNode,
+ WorkflowType.Application,
+ WorkflowType.SpeechToTextNode,
+ WorkflowType.TextToSpeechNode,
+ WorkflowType.ImageGenerateNode,
+]
+export class WorkFlowInstance {
+ nodes
+ edges
+ workFlowNodes: Array
+ constructor(workflow: { nodes: Array; edges: Array }) {
+ this.nodes = workflow.nodes
+ this.edges = workflow.edges
+ this.workFlowNodes = []
+ }
+ /**
+ * 校验开始节点
+ */
+ private is_valid_start_node() {
+ const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start)
+ if (start_node_list.length == 0) {
+ throw t('views.applicationWorkflow.validate.startNodeRequired')
+ } else if (start_node_list.length > 1) {
+ throw t('views.applicationWorkflow.validate.startNodeOnly')
+ }
+ }
+ /**
+ * 校验基本信息节点
+ */
+ private is_valid_base_node() {
+ const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
+ if (start_node_list.length == 0) {
+ throw t('views.applicationWorkflow.validate.baseNodeRequired')
+ } else if (start_node_list.length > 1) {
+ throw t('views.applicationWorkflow.validate.baseNodeOnly')
+ }
+ }
+ /**
+ * 校验节点
+ */
+ is_valid() {
+ this.is_valid_start_node()
+ this.is_valid_base_node()
+ this.is_valid_work_flow()
+ this.is_valid_nodes()
+ }
+
+ /**
+ * 获取开始节点
+ * @returns
+ */
+ get_start_node() {
+ const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start)
+ return start_node_list[0]
+ }
+ /**
+ * 获取基本节点
+ * @returns 基本节点
+ */
+ get_base_node() {
+ const base_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
+ return base_node_list[0]
+ }
+
+ /**
+ * 校验工作流
+ * @param up_node 上一个节点
+ */
+ private _is_valid_work_flow(up_node?: any) {
+ if (!up_node) {
+ up_node = this.get_start_node()
+ }
+ this.workFlowNodes.push(up_node)
+ this.is_valid_node(up_node)
+ const next_nodes = this.get_next_nodes(up_node)
+ for (const next_node of next_nodes) {
+ this._is_valid_work_flow(next_node)
+ }
+ }
+ private is_valid_work_flow() {
+ this.workFlowNodes = []
+ this._is_valid_work_flow()
+ const notInWorkFlowNodes = this.nodes
+ .filter((node: any) => node.id !== WorkflowType.Start && node.id !== WorkflowType.Base)
+ .filter((node) => !this.workFlowNodes.includes(node))
+ if (notInWorkFlowNodes.length > 0) {
+ throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${notInWorkFlowNodes.map((node) => node.properties.stepName).join(',')}`
+ }
+ this.workFlowNodes = []
+ }
+ /**
+ * 获取流程下一个节点列表
+ * @param node 节点
+ * @returns 节点列表
+ */
+ private get_next_nodes(node: any) {
+ const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id)
+ const node_list = edge_list
+ .map((edge) => this.nodes.filter((node) => node.id == edge.targetNodeId))
+ .reduce((x, y) => [...x, ...y], [])
+ if (node_list.length == 0 && !end_nodes.includes(node.type)) {
+ throw t('views.applicationWorkflow.validate.noNextNode')
+ }
+ return node_list
+ }
+ private is_valid_nodes() {
+ for (const node of this.nodes) {
+ if (node.type !== WorkflowType.Base && node.type !== WorkflowType.Start) {
+ if (!this.edges.some((edge) => edge.targetNodeId === node.id)) {
+ throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${node.properties.stepName}`
+ }
+ }
+ }
+ }
+ /**
+ * 校验节点
+ * @param node 节点
+ */
+ private is_valid_node(node: any) {
+ if (node.properties.status && node.properties.status === 500) {
+ throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.nodeUnavailable')}`
+ }
+ if (node.type === WorkflowType.Condition) {
+ const branch_list = node.properties.node_data.branch
+ for (const branch of branch_list) {
+ const source_anchor_id = `${node.id}_${branch.id}_right`
+ const edge_list = this.edges.filter((edge) => edge.sourceAnchorId == source_anchor_id)
+ if (edge_list.length == 0) {
+ throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.needConnect1')}${branch.type}${t('views.applicationWorkflow.validate.needConnect2')}`
+ }
+ }
+ } else {
+ const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id)
+ if (edge_list.length == 0 && !end_nodes.includes(node.type)) {
+ throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.cannotEndNode')}`
+ }
+ }
+ if (node.properties.status && node.properties.status !== 200) {
+ throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.nodeUnavailable')}`
+ }
+ }
+}
diff --git a/ui/src/workflow/icons/ai-chat-node-icon.vue b/ui/src/workflow/icons/ai-chat-node-icon.vue
new file mode 100644
index 000000000..24e6d468e
--- /dev/null
+++ b/ui/src/workflow/icons/ai-chat-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/application-node-icon.vue b/ui/src/workflow/icons/application-node-icon.vue
new file mode 100644
index 000000000..347588b51
--- /dev/null
+++ b/ui/src/workflow/icons/application-node-icon.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/base-node-icon.vue b/ui/src/workflow/icons/base-node-icon.vue
new file mode 100644
index 000000000..a0b5d2752
--- /dev/null
+++ b/ui/src/workflow/icons/base-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/condition-node-icon.vue b/ui/src/workflow/icons/condition-node-icon.vue
new file mode 100644
index 000000000..6deed3108
--- /dev/null
+++ b/ui/src/workflow/icons/condition-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/document-extract-node-icon.vue b/ui/src/workflow/icons/document-extract-node-icon.vue
new file mode 100644
index 000000000..7b81719c6
--- /dev/null
+++ b/ui/src/workflow/icons/document-extract-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/form-node-icon.vue b/ui/src/workflow/icons/form-node-icon.vue
new file mode 100644
index 000000000..40f6ea77f
--- /dev/null
+++ b/ui/src/workflow/icons/form-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/function-lib-node-icon.vue b/ui/src/workflow/icons/function-lib-node-icon.vue
new file mode 100644
index 000000000..e8e6f9910
--- /dev/null
+++ b/ui/src/workflow/icons/function-lib-node-icon.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/function-node-icon.vue b/ui/src/workflow/icons/function-node-icon.vue
new file mode 100644
index 000000000..e6e84a801
--- /dev/null
+++ b/ui/src/workflow/icons/function-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/global-icon.vue b/ui/src/workflow/icons/global-icon.vue
new file mode 100644
index 000000000..5d476dc74
--- /dev/null
+++ b/ui/src/workflow/icons/global-icon.vue
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/ui/src/workflow/icons/image-generate-node-icon.vue b/ui/src/workflow/icons/image-generate-node-icon.vue
new file mode 100644
index 000000000..64ca192b1
--- /dev/null
+++ b/ui/src/workflow/icons/image-generate-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/image-understand-node-icon.vue b/ui/src/workflow/icons/image-understand-node-icon.vue
new file mode 100644
index 000000000..6f58417d2
--- /dev/null
+++ b/ui/src/workflow/icons/image-understand-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/mcp-node-icon.vue b/ui/src/workflow/icons/mcp-node-icon.vue
new file mode 100644
index 000000000..7dc36f22c
--- /dev/null
+++ b/ui/src/workflow/icons/mcp-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/question-node-icon.vue b/ui/src/workflow/icons/question-node-icon.vue
new file mode 100644
index 000000000..74ab30d0f
--- /dev/null
+++ b/ui/src/workflow/icons/question-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/reply-node-icon.vue b/ui/src/workflow/icons/reply-node-icon.vue
new file mode 100644
index 000000000..07b2ed56f
--- /dev/null
+++ b/ui/src/workflow/icons/reply-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/reranker-node-icon.vue b/ui/src/workflow/icons/reranker-node-icon.vue
new file mode 100644
index 000000000..70c8f4842
--- /dev/null
+++ b/ui/src/workflow/icons/reranker-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/search-dataset-node-icon.vue b/ui/src/workflow/icons/search-dataset-node-icon.vue
new file mode 100644
index 000000000..d2b2302b9
--- /dev/null
+++ b/ui/src/workflow/icons/search-dataset-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/speech-to-text-node-icon.vue b/ui/src/workflow/icons/speech-to-text-node-icon.vue
new file mode 100644
index 000000000..37cc26c3b
--- /dev/null
+++ b/ui/src/workflow/icons/speech-to-text-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/start-node-icon.vue b/ui/src/workflow/icons/start-node-icon.vue
new file mode 100644
index 000000000..9a01ac96c
--- /dev/null
+++ b/ui/src/workflow/icons/start-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/text-to-speech-node-icon.vue b/ui/src/workflow/icons/text-to-speech-node-icon.vue
new file mode 100644
index 000000000..e48a06857
--- /dev/null
+++ b/ui/src/workflow/icons/text-to-speech-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/icons/utils.ts b/ui/src/workflow/icons/utils.ts
new file mode 100644
index 000000000..b667cdc6e
--- /dev/null
+++ b/ui/src/workflow/icons/utils.ts
@@ -0,0 +1,5 @@
+const icons: any = import.meta.glob('./**.vue', { eager: true })
+export function iconComponent(name: string) {
+ const url = `./${name}.vue`
+ return icons[url]?.default || null
+}
diff --git a/ui/src/workflow/icons/variable-assign-node-icon.vue b/ui/src/workflow/icons/variable-assign-node-icon.vue
new file mode 100644
index 000000000..a7b580ff3
--- /dev/null
+++ b/ui/src/workflow/icons/variable-assign-node-icon.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ui/src/workflow/index.vue b/ui/src/workflow/index.vue
new file mode 100644
index 000000000..03361bcec
--- /dev/null
+++ b/ui/src/workflow/index.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/ai-chat-node/index.ts b/ui/src/workflow/nodes/ai-chat-node/index.ts
new file mode 100644
index 000000000..b226719e1
--- /dev/null
+++ b/ui/src/workflow/nodes/ai-chat-node/index.ts
@@ -0,0 +1,12 @@
+import ChatNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class ChatNode extends AppNode {
+ constructor(props: any) {
+ super(props, ChatNodeVue)
+ }
+}
+export default {
+ type: 'ai-chat-node',
+ model: AppNodeModel,
+ view: ChatNode
+}
diff --git a/ui/src/workflow/nodes/ai-chat-node/index.vue b/ui/src/workflow/nodes/ai-chat-node/index.vue
new file mode 100644
index 000000000..8b4a6aeb9
--- /dev/null
+++ b/ui/src/workflow/nodes/ai-chat-node/index.vue
@@ -0,0 +1,338 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ $t('views.application.applicationForm.form.aiModel.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.application.applicationForm.form.prompt.label')
+ }}*
+
+
+ {{ $t('views.application.applicationForm.form.prompt.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
{{ $t('views.application.applicationForm.form.historyRecord.label') }}
+
+
+
+
+
+
+
+
+
+
+
{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.application.applicationForm.form.reasoningContent.label')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/application-node/index.ts b/ui/src/workflow/nodes/application-node/index.ts
new file mode 100644
index 000000000..6292ab5df
--- /dev/null
+++ b/ui/src/workflow/nodes/application-node/index.ts
@@ -0,0 +1,12 @@
+import ChatNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class ChatNode extends AppNode {
+ constructor(props: any) {
+ super(props, ChatNodeVue)
+ }
+}
+export default {
+ type: 'application-node',
+ model: AppNodeModel,
+ view: ChatNode
+}
diff --git a/ui/src/workflow/nodes/application-node/index.vue b/ui/src/workflow/nodes/application-node/index.vue
new file mode 100644
index 000000000..4fc9fba54
--- /dev/null
+++ b/ui/src/workflow/nodes/application-node/index.vue
@@ -0,0 +1,317 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/component/ApiFieldFormDialog.vue b/ui/src/workflow/nodes/base-node/component/ApiFieldFormDialog.vue
new file mode 100644
index 000000000..41bc6cabe
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/component/ApiFieldFormDialog.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/component/ApiInputFieldTable.vue b/ui/src/workflow/nodes/base-node/component/ApiInputFieldTable.vue
new file mode 100644
index 000000000..c81ebc94f
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/component/ApiInputFieldTable.vue
@@ -0,0 +1,157 @@
+
+
+
{{ $t('views.template.templateForm.title.apiParamPassing') }}
+
+
+
+
+ {{ $t('common.add') }}
+
+
+
+
+
+
+ {{ row.variable }}
+
+
+
+
+
+
+ {{ row.default_value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/component/FileUploadSettingDialog.vue b/ui/src/workflow/nodes/base-node/component/FileUploadSettingDialog.vue
new file mode 100644
index 000000000..0f55fa1dd
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/component/FileUploadSettingDialog.vue
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+ {{ $t('common.fileUpload.document') }}
+ {{
+ $t(
+ 'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.documentText'
+ )
+ }}
+
+
+
{{ documentExtensions.join('、') }}
+
+
+
+
+
+
+
+
+

+
+
+ {{ $t('common.fileUpload.image') }}
+ {{
+ $t(
+ 'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.imageText'
+ )
+ }}
+
+
+
{{ imageExtensions.join('、') }}
+
+
+
+
+
+
+
+
+
+

+
+
+ {{ $t('common.fileUpload.audio') }}
+ {{
+ $t(
+ 'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.audioText'
+ )
+ }}
+
+
+
{{ audioExtensions.join('、') }}
+
+
+
+
+
+
+
+
+

+
+
+ {{ $t('common.fileUpload.other') }}
+ {{
+ $t(
+ 'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.otherText'
+ )
+ }}
+
+
+
+
+ {{ tag }}
+
+
+
+ + {{ $t('common.fileUpload.addExtensions') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/component/UserFieldFormDialog.vue b/ui/src/workflow/nodes/base-node/component/UserFieldFormDialog.vue
new file mode 100644
index 000000000..21b72971d
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/component/UserFieldFormDialog.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/component/UserInputFieldTable.vue b/ui/src/workflow/nodes/base-node/component/UserInputFieldTable.vue
new file mode 100644
index 000000000..88615954c
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/component/UserInputFieldTable.vue
@@ -0,0 +1,258 @@
+
+
+
+ {{ inputFieldConfig.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.add') }}
+
+
+
+
+
+
+
+ {{ row.field }}
+
+
+
+
+
+
+
+ {{ row.label.label }}
+
+
+
+
+ {{ row.label }}
+
+
+
+
+
+ {{
+ $t('dynamicsForm.input_type_list.TextInput')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.PasswordInput')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.Slider')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.SwitchInput')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.SingleSelect')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.MultiSelect')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.RadioCard')
+ }}
+ {{
+ $t('dynamicsForm.input_type_list.DatePicker')
+ }}
+
+
+
+
+
+ {{ getDefaultValue(row) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/component/UserInputTitleDialog.vue b/ui/src/workflow/nodes/base-node/component/UserInputTitleDialog.vue
new file mode 100644
index 000000000..561e75a57
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/component/UserInputTitleDialog.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/base-node/index.ts b/ui/src/workflow/nodes/base-node/index.ts
new file mode 100644
index 000000000..69ede8e06
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/index.ts
@@ -0,0 +1,22 @@
+import BaseNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+
+class BaseNode extends AppNode {
+ constructor(props: any) {
+ super(props, BaseNodeVue)
+ }
+}
+
+class BaseModel extends AppNodeModel {
+ constructor(data: any, graphModel: any) {
+ super(data, graphModel)
+ }
+ get_width() {
+ return 600
+ }
+}
+export default {
+ type: 'base-node',
+ model: BaseModel,
+ view: BaseNode
+}
diff --git a/ui/src/workflow/nodes/base-node/index.vue b/ui/src/workflow/nodes/base-node/index.vue
new file mode 100644
index 000000000..5904745b2
--- /dev/null
+++ b/ui/src/workflow/nodes/base-node/index.vue
@@ -0,0 +1,349 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{
+ $t('views.applicationWorkflow.nodes.baseNode.fileUpload.label')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{
+ $t('views.application.applicationForm.form.voiceInput.label')
+ }}
+
+ {{
+ $t('views.application.applicationForm.form.voiceInput.autoSend')
+ }}
+
+
+
+
+
+
+
+
+
+
{{
+ $t('views.application.applicationForm.form.voicePlay.label')
+ }}
+
+ {{
+ $t('views.application.applicationForm.form.voicePlay.autoPlay')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/condition-node/index.ts b/ui/src/workflow/nodes/condition-node/index.ts
new file mode 100644
index 000000000..275c63a2d
--- /dev/null
+++ b/ui/src/workflow/nodes/condition-node/index.ts
@@ -0,0 +1,68 @@
+import ConditioNodeVue from './index.vue'
+import { cloneDeep, set } from 'lodash'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class ConditioNode extends AppNode {
+ constructor(props: any) {
+ super(props, ConditioNodeVue)
+ }
+}
+const get_up_index_height = (condition_list: Array, index: number) => {
+ return condition_list
+ .filter((item, i) => i < index)
+ .map((item) => item.height + 8)
+ .reduce((x, y) => x + y, 0)
+}
+class ConditionModel extends AppNodeModel {
+ refreshBranch() {
+ // 更新节点连接边的path
+ this.incoming.edges.forEach((edge: any) => {
+ // 调用自定义的更新方案
+ edge.updatePathByAnchor()
+ })
+ this.outgoing.edges.forEach((edge: any) => {
+ edge.updatePathByAnchor()
+ })
+ }
+ getDefaultAnchor() {
+ const {
+ id,
+ x,
+ y,
+ width,
+ height,
+ properties: { branch_condition_list }
+ } = this
+ if (this.height === undefined) {
+ this.height = 200
+ }
+ const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
+ const anchors: any = []
+ anchors.push({
+ x: x - width / 2 + 10,
+ y: showNode ? y : y - 15,
+ id: `${id}_left`,
+ edgeAddable: false,
+ type: 'left'
+ })
+
+ if (branch_condition_list) {
+ for (let index = 0; index < branch_condition_list.length; index++) {
+ const element = branch_condition_list[index]
+ const h = get_up_index_height(branch_condition_list, index)
+ anchors.push({
+ x: x + width / 2 - 10,
+ y: showNode ? y - height / 2 + 75 + h + element.height / 2 : y - 15,
+ id: `${id}_${element.id}_right`,
+ type: 'right'
+ })
+ }
+ }
+
+ return anchors
+ }
+}
+export default {
+ type: 'condition-node',
+ model: ConditionModel,
+ view: ConditioNode
+}
diff --git a/ui/src/workflow/nodes/condition-node/index.vue b/ui/src/workflow/nodes/condition-node/index.vue
new file mode 100644
index 000000000..f5044ae95
--- /dev/null
+++ b/ui/src/workflow/nodes/condition-node/index.vue
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+
+
+
+ {{ item.type }}
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.conditionNode.conditions.info')
+ }}
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.conditionNode.conditions.label')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.conditionNode.addCondition') }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.conditionNode.addBranch') }}
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/document-extract-node/index.ts b/ui/src/workflow/nodes/document-extract-node/index.ts
new file mode 100644
index 000000000..9a7072d2e
--- /dev/null
+++ b/ui/src/workflow/nodes/document-extract-node/index.ts
@@ -0,0 +1,12 @@
+import DocumentExtractNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class RerankerNode extends AppNode {
+ constructor(props: any) {
+ super(props, DocumentExtractNodeVue)
+ }
+}
+export default {
+ type: 'document-extract-node',
+ model: AppNodeModel,
+ view: RerankerNode
+}
diff --git a/ui/src/workflow/nodes/document-extract-node/index.vue b/ui/src/workflow/nodes/document-extract-node/index.vue
new file mode 100644
index 000000000..4e2047efa
--- /dev/null
+++ b/ui/src/workflow/nodes/document-extract-node/index.vue
@@ -0,0 +1,64 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/workflow/nodes/form-node/index.ts b/ui/src/workflow/nodes/form-node/index.ts
new file mode 100644
index 000000000..b7c2dbec9
--- /dev/null
+++ b/ui/src/workflow/nodes/form-node/index.ts
@@ -0,0 +1,12 @@
+import FormNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class FormNode extends AppNode {
+ constructor(props: any) {
+ super(props, FormNodeVue)
+ }
+}
+export default {
+ type: 'form-node',
+ model: AppNodeModel,
+ view: FormNode
+}
diff --git a/ui/src/workflow/nodes/form-node/index.vue b/ui/src/workflow/nodes/form-node/index.vue
new file mode 100644
index 000000000..87fe4788b
--- /dev/null
+++ b/ui/src/workflow/nodes/form-node/index.vue
@@ -0,0 +1,281 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.formNode.formContent.label')
+ }}*
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.formNode.formContent.tooltip', {
+ form: '{ form }'
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.formNode.formSetting') }}
+
+
+
+
+
+ {{ $t('common.add') }}
+
+
+
+
+
+
+ {{ row.field }}
+
+
+
+
+
+
+ {{ row.label.label }}
+
+
+
+
+ {{ row.label }}
+
+
+
+
+
+
+ {{
+ input_type_list.find((item) => item.value === row.input_type)?.label
+ }}
+
+
+
+
+
+ {{
+ getDefaultValue(row)
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/function-lib-node/index.ts b/ui/src/workflow/nodes/function-lib-node/index.ts
new file mode 100644
index 000000000..475818c1f
--- /dev/null
+++ b/ui/src/workflow/nodes/function-lib-node/index.ts
@@ -0,0 +1,12 @@
+import FunctionLibNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class FunctionLibNode extends AppNode {
+ constructor(props: any) {
+ super(props, FunctionLibNodeVue)
+ }
+}
+export default {
+ type: 'function-lib-node',
+ model: AppNodeModel,
+ view: FunctionLibNode
+}
diff --git a/ui/src/workflow/nodes/function-lib-node/index.vue b/ui/src/workflow/nodes/function-lib-node/index.vue
new file mode 100644
index 000000000..794cabd20
--- /dev/null
+++ b/ui/src/workflow/nodes/function-lib-node/index.vue
@@ -0,0 +1,161 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+ {{ $t('common.param.inputParam') }}
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+ *
+
{{ item.type }}
+
+
+
+
+
+
+
+
+
+ {{ $t('common.noData') }}
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/function-node/index.ts b/ui/src/workflow/nodes/function-node/index.ts
new file mode 100644
index 000000000..ab3f36edf
--- /dev/null
+++ b/ui/src/workflow/nodes/function-node/index.ts
@@ -0,0 +1,12 @@
+import FunctionNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class FunctionLibCustomNode extends AppNode {
+ constructor(props: any) {
+ super(props, FunctionNodeVue)
+ }
+}
+export default {
+ type: 'function-node',
+ model: AppNodeModel,
+ view: FunctionLibCustomNode
+}
diff --git a/ui/src/workflow/nodes/function-node/index.vue b/ui/src/workflow/nodes/function-node/index.vue
new file mode 100644
index 000000000..c720f61e2
--- /dev/null
+++ b/ui/src/workflow/nodes/function-node/index.vue
@@ -0,0 +1,214 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
{{ $t('common.param.inputParam') }}
+
+ {{ $t('common.add') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+ *
+
+
{{ item.type }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.noData') }}
+
+
+
+ {{ $t('views.functionLib.functionForm.form.param.code') }}
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/image-generate/index.ts b/ui/src/workflow/nodes/image-generate/index.ts
new file mode 100644
index 000000000..5afc2e571
--- /dev/null
+++ b/ui/src/workflow/nodes/image-generate/index.ts
@@ -0,0 +1,14 @@
+import ImageGenerateNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+
+class RerankerNode extends AppNode {
+ constructor(props: any) {
+ super(props, ImageGenerateNodeVue)
+ }
+}
+
+export default {
+ type: 'image-generate-node',
+ model: AppNodeModel,
+ view: RerankerNode
+}
diff --git a/ui/src/workflow/nodes/image-generate/index.vue b/ui/src/workflow/nodes/image-generate/index.vue
new file mode 100644
index 000000000..d71f3adf9
--- /dev/null
+++ b/ui/src/workflow/nodes/image-generate/index.vue
@@ -0,0 +1,266 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.imageGenerateNode.model.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.imageGenerateNode.prompt.label')
+ }}*
+
+
+ {{ $t('views.applicationWorkflow.nodes.imageGenerateNode.prompt.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.imageGenerateNode.negative_prompt.label')
+ }}
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.imageGenerateNode.negative_prompt.tooltip')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/image-understand/index.ts b/ui/src/workflow/nodes/image-understand/index.ts
new file mode 100644
index 000000000..06695d3c9
--- /dev/null
+++ b/ui/src/workflow/nodes/image-understand/index.ts
@@ -0,0 +1,14 @@
+import ImageUnderstandNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+
+class RerankerNode extends AppNode {
+ constructor(props: any) {
+ super(props, ImageUnderstandNodeVue)
+ }
+}
+
+export default {
+ type: 'image-understand-node',
+ model: AppNodeModel,
+ view: RerankerNode
+}
diff --git a/ui/src/workflow/nodes/image-understand/index.vue b/ui/src/workflow/nodes/image-understand/index.vue
new file mode 100644
index 000000000..e2ac7c13b
--- /dev/null
+++ b/ui/src/workflow/nodes/image-understand/index.vue
@@ -0,0 +1,280 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ t('views.applicationWorkflow.nodes.imageUnderstandNode.model.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.application.applicationForm.form.prompt.label')
+ }}*
+
+
+ {{ $t('views.application.applicationForm.form.prompt.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
{{ $t('views.application.applicationForm.form.historyRecord.label') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.imageUnderstandNode.image.label')
+ }}*
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/mcp-node/index.ts b/ui/src/workflow/nodes/mcp-node/index.ts
new file mode 100644
index 000000000..d626b0c62
--- /dev/null
+++ b/ui/src/workflow/nodes/mcp-node/index.ts
@@ -0,0 +1,14 @@
+import McpNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+
+class McpNode extends AppNode {
+ constructor(props: any) {
+ super(props, McpNodeVue)
+ }
+}
+
+export default {
+ type: 'mcp-node',
+ model: AppNodeModel,
+ view: McpNode
+}
diff --git a/ui/src/workflow/nodes/mcp-node/index.vue b/ui/src/workflow/nodes/mcp-node/index.vue
new file mode 100644
index 000000000..7cba4d51e
--- /dev/null
+++ b/ui/src/workflow/nodes/mcp-node/index.vue
@@ -0,0 +1,443 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+
+
+
{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.mcpNode.getTool') }}
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.mcpNode.toolParam') }}
+
+
+
+ {{ $t('common.noData') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.noData') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/question-node/index.ts b/ui/src/workflow/nodes/question-node/index.ts
new file mode 100644
index 000000000..324c24683
--- /dev/null
+++ b/ui/src/workflow/nodes/question-node/index.ts
@@ -0,0 +1,12 @@
+import QuestionNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class QuestionNode extends AppNode {
+ constructor(props: any) {
+ super(props, QuestionNodeVue)
+ }
+}
+export default {
+ type: 'question-node',
+ model: AppNodeModel,
+ view: QuestionNode
+}
diff --git a/ui/src/workflow/nodes/question-node/index.vue b/ui/src/workflow/nodes/question-node/index.vue
new file mode 100644
index 000000000..f9e9559a9
--- /dev/null
+++ b/ui/src/workflow/nodes/question-node/index.vue
@@ -0,0 +1,246 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ $t('views.application.applicationForm.form.aiModel.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.application.applicationForm.form.prompt.label')
+ }}*
+
+
+ {{
+ $t('views.application.applicationForm.form.prompt.tooltip')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/reply-node/index.ts b/ui/src/workflow/nodes/reply-node/index.ts
new file mode 100644
index 000000000..e3bd9d95d
--- /dev/null
+++ b/ui/src/workflow/nodes/reply-node/index.ts
@@ -0,0 +1,12 @@
+import ReplyNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class ReplyNode extends AppNode {
+ constructor(props: any) {
+ super(props, ReplyNodeVue)
+ }
+}
+export default {
+ type: 'reply-node',
+ model: AppNodeModel,
+ view: ReplyNode
+}
diff --git a/ui/src/workflow/nodes/reply-node/index.vue b/ui/src/workflow/nodes/reply-node/index.vue
new file mode 100644
index 000000000..e23fd889f
--- /dev/null
+++ b/ui/src/workflow/nodes/reply-node/index.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.replyNode.replyContent.label') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/reranker-node/ParamSettingDialog.vue b/ui/src/workflow/nodes/reranker-node/ParamSettingDialog.vue
new file mode 100644
index 000000000..42cc546f6
--- /dev/null
+++ b/ui/src/workflow/nodes/reranker-node/ParamSettingDialog.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
Score {{ $t('views.applicationWorkflow.nodes.rerankerNode.higher') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/reranker-node/index.ts b/ui/src/workflow/nodes/reranker-node/index.ts
new file mode 100644
index 000000000..9b3afc5c6
--- /dev/null
+++ b/ui/src/workflow/nodes/reranker-node/index.ts
@@ -0,0 +1,12 @@
+import RerankerNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class RerankerNode extends AppNode {
+ constructor(props: any) {
+ super(props, RerankerNodeVue)
+ }
+}
+export default {
+ type: 'reranker-node',
+ model: AppNodeModel,
+ view: RerankerNode
+}
diff --git a/ui/src/workflow/nodes/reranker-node/index.vue b/ui/src/workflow/nodes/reranker-node/index.vue
new file mode 100644
index 000000000..355776883
--- /dev/null
+++ b/ui/src/workflow/nodes/reranker-node/index.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
{{ $t('views.applicationWorkflow.nodes.rerankerNode.rerankerContent.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.searchDatasetNode.searchParam') }}
+
+
+
+
+
+
+
+
+ Score
+ {{ $t('views.applicationWorkflow.nodes.rerankerNode.higher') }}
+
+ {{ form_data.reranker_setting.similarity?.toFixed(3) }}
+
+ {{ $t('chat.KnowledgeSource.referenceParagraph') }} Top
+ {{ form_data.reranker_setting.top_n }}
+
+ {{
+ $t('views.applicationWorkflow.nodes.rerankerNode.max_paragraph_char_number')
+ }}
+
+ {{ form_data.reranker_setting.max_paragraph_char_number }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.label')
+ }}*
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.rerankerNode.reranker_model.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/search-dataset-node/index.ts b/ui/src/workflow/nodes/search-dataset-node/index.ts
new file mode 100644
index 000000000..316854dc4
--- /dev/null
+++ b/ui/src/workflow/nodes/search-dataset-node/index.ts
@@ -0,0 +1,12 @@
+import SearchDatasetVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class SearchDatasetNode extends AppNode {
+ constructor(props: any) {
+ super(props, SearchDatasetVue)
+ }
+}
+export default {
+ type: 'search-dataset-node',
+ model: AppNodeModel,
+ view: SearchDatasetNode
+}
diff --git a/ui/src/workflow/nodes/search-dataset-node/index.vue b/ui/src/workflow/nodes/search-dataset-node/index.vue
new file mode 100644
index 000000000..572e40daf
--- /dev/null
+++ b/ui/src/workflow/nodes/search-dataset-node/index.vue
@@ -0,0 +1,234 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
{{ $t('views.log.selectDataset') }}
+
+
+
+
+
+
+
+ {{ $t('views.application.applicationForm.form.relatedKnowledge.placeholder') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ relatedObject(datasetList, item, 'id')?.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.searchDatasetNode.searchParam') }}
+
+
+
+
+
+
+
+ {{
+ $t('views.application.applicationForm.dialog.selectSearchMode')
+ }}
+
+ {{
+ $t(SearchMode[form_data.dataset_setting.search_mode as keyof typeof SearchMode])
+ }}
+
+ {{ $t('views.application.applicationForm.dialog.similarityThreshold') }}
+
+ {{ form_data.dataset_setting.similarity?.toFixed(3) }}
+ {{
+ $t('views.application.applicationForm.dialog.topReferences')
+ }}
+ {{ form_data.dataset_setting.top_n }}
+
+ {{ $t('views.application.applicationForm.dialog.maxCharacters') }}
+
+ {{ form_data.dataset_setting.max_paragraph_char_number }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/speech-to-text-node/index.ts b/ui/src/workflow/nodes/speech-to-text-node/index.ts
new file mode 100644
index 000000000..aabaf1fc1
--- /dev/null
+++ b/ui/src/workflow/nodes/speech-to-text-node/index.ts
@@ -0,0 +1,12 @@
+import SpeechToTextVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class SpeechToTextNode extends AppNode {
+ constructor(props: any) {
+ super(props, SpeechToTextVue)
+ }
+}
+export default {
+ type: 'speech-to-text-node',
+ model: AppNodeModel,
+ view: SpeechToTextNode
+}
diff --git a/ui/src/workflow/nodes/speech-to-text-node/index.vue b/ui/src/workflow/nodes/speech-to-text-node/index.vue
new file mode 100644
index 000000000..133acddce
--- /dev/null
+++ b/ui/src/workflow/nodes/speech-to-text-node/index.vue
@@ -0,0 +1,177 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.speechToTextNode.stt_model.label')
+ }}*
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.speechToTextNode.audio.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/start-node/index.ts b/ui/src/workflow/nodes/start-node/index.ts
new file mode 100644
index 000000000..5adfb7798
--- /dev/null
+++ b/ui/src/workflow/nodes/start-node/index.ts
@@ -0,0 +1,12 @@
+import StartNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class StartNode extends AppNode {
+ constructor(props: any) {
+ super(props, StartNodeVue)
+ }
+}
+export default {
+ type: 'start-node',
+ model: AppNodeModel,
+ view: StartNode
+}
diff --git a/ui/src/workflow/nodes/start-node/index.vue b/ui/src/workflow/nodes/start-node/index.vue
new file mode 100644
index 000000000..53df2148f
--- /dev/null
+++ b/ui/src/workflow/nodes/start-node/index.vue
@@ -0,0 +1,115 @@
+
+
+ {{ $t('views.applicationWorkflow.variable.global') }}
+
+
{{ item.label }} {{ '{' + item.value + '}' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/text-to-speech-node/index.ts b/ui/src/workflow/nodes/text-to-speech-node/index.ts
new file mode 100644
index 000000000..04e2363bf
--- /dev/null
+++ b/ui/src/workflow/nodes/text-to-speech-node/index.ts
@@ -0,0 +1,12 @@
+import TextToSpeechVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+class TextToSpeechNode extends AppNode {
+ constructor(props: any) {
+ super(props, TextToSpeechVue)
+ }
+}
+export default {
+ type: 'text-to-speech-node',
+ model: AppNodeModel,
+ view: TextToSpeechNode
+}
diff --git a/ui/src/workflow/nodes/text-to-speech-node/index.vue b/ui/src/workflow/nodes/text-to-speech-node/index.vue
new file mode 100644
index 000000000..d5b9fe609
--- /dev/null
+++ b/ui/src/workflow/nodes/text-to-speech-node/index.vue
@@ -0,0 +1,203 @@
+
+
+ {{ $t('views.applicationWorkflow.nodeSetting') }}
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.textToSpeechNode.tts_model.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.textToSpeechNode.content.label')
+ }}*
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
+ }}
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/workflow/nodes/variable-assign-node/index.ts b/ui/src/workflow/nodes/variable-assign-node/index.ts
new file mode 100644
index 000000000..567bf425a
--- /dev/null
+++ b/ui/src/workflow/nodes/variable-assign-node/index.ts
@@ -0,0 +1,14 @@
+import VariableAssignNodeVue from './index.vue'
+import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
+
+class VariableAssignNode extends AppNode {
+ constructor(props: any) {
+ super(props, VariableAssignNodeVue)
+ }
+}
+
+export default {
+ type: 'variable-assign-node',
+ model: AppNodeModel,
+ view: VariableAssignNode
+}
diff --git a/ui/src/workflow/nodes/variable-assign-node/index.vue b/ui/src/workflow/nodes/variable-assign-node/index.vue
new file mode 100644
index 000000000..a786a9d77
--- /dev/null
+++ b/ui/src/workflow/nodes/variable-assign-node/index.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.variable.label') }}
+ *
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('views.applicationWorkflow.nodes.variableAssignNode.assign')
+ }}*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (form_data.variable_list[index].value = val)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.add') }}
+
+
+
+
+
+
diff --git a/ui/src/workflow/plugins/dagre.ts b/ui/src/workflow/plugins/dagre.ts
new file mode 100644
index 000000000..67b72fdad
--- /dev/null
+++ b/ui/src/workflow/plugins/dagre.ts
@@ -0,0 +1,69 @@
+import { DagreLayout, type DagreLayoutOptions } from '@antv/layout'
+
+export default class Dagre {
+ static pluginName = 'dagre'
+ lf: any
+ option: DagreLayoutOptions | any
+ render(lf: any) {
+ this.lf = lf
+ }
+
+ /**
+ * option: {
+ * rankdir: "TB", // layout 方向, 可选 TB, BT, LR, RL
+ * align: undefined, // 节点对齐方式,可选 UL, UR, DL, DR
+ * nodeSize: undefined, // 节点大小
+ * nodesepFunc: undefined, // 节点水平间距(px)
+ * ranksepFunc: undefined, // 每一层节点之间间距
+ * nodesep: 40, // 节点水平间距(px) 注意:如果有grid,需要保证nodesep为grid的偶数倍
+ * ranksep: 40, // 每一层节点之间间距 注意:如果有grid,需要保证ranksep为grid的偶数倍
+ * controlPoints: false, // 是否保留布局连线的控制点
+ * radial: false, // 是否基于 dagre 进行辐射布局
+ * focusNode: null, // radial 为 true 时生效,关注的节点
+ * };
+ */
+ layout(option = {}) {
+ const { nodes, edges, gridSize } = this.lf.graphModel
+ // 为了保证生成的节点在girdSize上,需要处理一下。
+ let nodesep = 40
+ let ranksep = 40
+ if (gridSize > 20) {
+ nodesep = gridSize * 2
+ ranksep = gridSize * 2
+ }
+ this.option = {
+ type: 'dagre',
+ rankdir: 'LR',
+ // align: 'UL',
+ // align: 'UR',
+ align: 'DR',
+ nodesep,
+ ranksep,
+ begin: [120, 120],
+ ...option
+ }
+ const layoutInstance = new DagreLayout(this.option)
+ const layoutData = layoutInstance.layout({
+ nodes: nodes.map((node: any) => ({
+ id: node.id,
+ size: {
+ width: node.width,
+ height: node.height
+ },
+ model: node
+ })),
+ edges: edges.map((edge: any) => ({
+ source: edge.sourceNodeId,
+ target: edge.targetNodeId,
+ model: edge
+ }))
+ })
+
+ layoutData.nodes?.forEach((node: any) => {
+ // @ts-ignore: pass node data
+ const { model } = node
+ model.set_position({ x: node.x, y: node.y })
+ })
+ this.lf.fitView()
+ }
+}