From 1598df378ab18d8388483d129439eec3cac38afe Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期四, 20 三月 2025 15:38:10 +0800 Subject: [PATCH] 添加 n8n 节点 --- src/api/n8n/index.ts | 23 +++ src/components/vue-flow/VueFlowHelper.ts | 60 +++++++++ src/components/vue-flow/ui/nodes/N8nNode.vue | 138 +++++++++++++++++++++++ src/components/vue-flow/ui/VueFlowConfig.ts | 12 ++ customer_list/yw/static/config/globalConfig.test.js | 2 src/components/vue-flow/MainCanvas.vue | 73 ++++++++++- src/components/vue-flow/vueFlowEnum.ts | 3 7 files changed, 300 insertions(+), 11 deletions(-) diff --git a/customer_list/yw/static/config/globalConfig.test.js b/customer_list/yw/static/config/globalConfig.test.js index e8bdb20..148f22f 100644 --- a/customer_list/yw/static/config/globalConfig.test.js +++ b/customer_list/yw/static/config/globalConfig.test.js @@ -8,6 +8,8 @@ WebApiUrl: { MainUrl: 'https://widev.cpolar.top/ai_dev/', + // MainUrl: 'http://192.168.1.58:8080/', + AuthUrl: 'http://47.100.245.85:8190/', }, SoftWareInfo: { diff --git a/src/api/n8n/index.ts b/src/api/n8n/index.ts new file mode 100644 index 0000000..e6a0d3f --- /dev/null +++ b/src/api/n8n/index.ts @@ -0,0 +1,23 @@ +import request from '/@/utils/request'; + +const convertJson = (res: any) => { + const json = JSON.parse(res.json_url); + + return { + json_ok: res.json_ok, + values: json, + }; +}; + +export const GetN8nWorkflowList = async (params, req: any = request) => { + const res = await req({ + url: '/n8n/connectionN8nList', + method: 'POST', + data: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + return convertJson(res); +}; diff --git a/src/components/vue-flow/MainCanvas.vue b/src/components/vue-flow/MainCanvas.vue index 64db7c1..82f64e9 100644 --- a/src/components/vue-flow/MainCanvas.vue +++ b/src/components/vue-flow/MainCanvas.vue @@ -2,13 +2,28 @@ <div class="relative h-full w-full" id="main-canvas" @drop="handleOnDrop" @dragover="handleOnDragOver"> <VueFlow v-model="elements" :node-types="nodeTypes" :connection-mode="ConnectionMode.Strict"> <template #node-start="startNodeProps"> - <StartNode ref="nodeRef" v-bind="startNodeProps" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(startNodeProps.id, ref)" /> + <StartNode + ref="nodeRef" + v-bind="startNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(startNodeProps.id, ref)" + /> </template> <template #node-condition="conditionNodeProps"> - <ConditionNode ref="nodeRef" v-bind="conditionNodeProps" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(conditionNodeProps.id, ref)" /> + <ConditionNode + ref="nodeRef" + v-bind="conditionNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(conditionNodeProps.id, ref)" + /> </template> <template #node-output_msg="outputNodeProps"> - <OutputNode ref="nodeRef" v-bind="outputNodeProps" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(outputNodeProps.id, ref)" /> + <OutputNode + ref="nodeRef" + v-bind="outputNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(outputNodeProps.id, ref)" + /> </template> <template #node-end="endNodeProps"> @@ -20,20 +35,50 @@ </template> <template #node-agent="agentNodeProps"> - <AgentNode ref="nodeRef" v-bind="agentNodeProps" :agentNames="agentNames" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(agentNodeProps.id, ref)" /> + <AgentNode + ref="nodeRef" + v-bind="agentNodeProps" + :agentNames="agentNames" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(agentNodeProps.id, ref)" + /> </template> <template #node-func="funcNodeProps"> <FuncNode ref="nodeRef" v-bind="funcNodeProps" :funcNames="funcNames" :isViewMode="isViewMode" /> </template> <template #node-code="codeNodeProps"> - <CodeNode ref="nodeRef" v-bind="codeNodeProps" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(codeNodeProps.id, ref)" /> + <CodeNode + ref="nodeRef" + v-bind="codeNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(codeNodeProps.id, ref)" + /> </template> <template #node-python_code="pythonCodeNodeProps"> - <PythonCodeNode ref="nodeRef" v-bind="pythonCodeNodeProps" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(pythonCodeNodeProps.id, ref)" /> + <PythonCodeNode + ref="nodeRef" + v-bind="pythonCodeNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(pythonCodeNodeProps.id, ref)" + /> + </template> + <template #node-n8n="n8nNodeProps"> + <N8nNode + :workflowList="n8nWorkflowList" + ref="nodeRef" + v-bind="n8nNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(n8nNodeProps.id, ref)" + /> </template> <template #node-text_resource="textResourceNodeProps"> - <TextResourceNode ref="nodeRef" v-bind="textResourceNodeProps" :isViewMode="isViewMode" @register="(ref) => registerNodeRef(textResourceNodeProps.id, ref)" /> + <TextResourceNode + ref="nodeRef" + v-bind="textResourceNodeProps" + :isViewMode="isViewMode" + @register="(ref) => registerNodeRef(textResourceNodeProps.id, ref)" + /> </template> <template #node-analysis="analysisNodeProps"> @@ -78,8 +123,17 @@ import TextResourceNode from './ui/nodes/TextResourceNode.vue'; import { NodeType, nodeTypeMap } from './vueFlowEnum'; import { VueFlowHelper } from './VueFlowHelper'; - +import N8nNode from './ui/nodes/N8nNode.vue'; +import { GetN8nWorkflowList } from '/@/api/n8n'; const props = defineProps(['flowJson', 'agentNames', 'funcNames', 'llmInfoList', 'isViewMode']); + +const n8nWorkflowList = ref([]); +const getN8nWorkflowList = async () => { + const res = await GetN8nWorkflowList({ + active: true, + }); + n8nWorkflowList.value = res.values?.data ?? []; +}; const nodeTypes = {}; const elements = ref<Elements>(); @@ -136,7 +190,8 @@ setInteractive(!props.isViewMode); }; -onMounted(() => { +onMounted(async () => { + getN8nWorkflowList(); setTimeout(() => { initFlowStatus(); }, 30); diff --git a/src/components/vue-flow/VueFlowHelper.ts b/src/components/vue-flow/VueFlowHelper.ts index 957dc4a..4186c9f 100644 --- a/src/components/vue-flow/VueFlowHelper.ts +++ b/src/components/vue-flow/VueFlowHelper.ts @@ -144,7 +144,8 @@ key: 'code', type: 'code', required: true, - value: "var recs = args.querySQL(\n \" SELECT * FROM guangming_data.tc_hzl_v4\"\n + \" WHERE otime >={start_time}::timestamp + '-1 months' AND otime < {end_time}::timestamp + '1 days'\", {\n start_time : args.getArg(\"start_time\"),\n end_time : args.getArg(\"end_time\")\n});\n\nvar resultText = \"鏃堕棿,姘村巶鍚嶏紝鑰楃數閲�,鍑哄彛娴侀噺\\n\";\nfor(var recIndex in recs)\n{\n var rec = recs[recIndex];\n resultText += rec[\"OTIME\"] + \",\" + rec[\"FACT_NAME\"] + \",\" + rec[\"VPOWER\"] + \",\" + rec[\"VFLOW\"] + \"\\n\";\n}\nargs.setArg(\"RECORD_LIST\", resultText);\n\nargs.sendTableToClent({\n title : \"娴侀噺\",\n columns : [\n {\"title\":\"鏃堕棿\", \"name\":\"OTIME\"},\n {\"title\":\"姘村巶\", \"name\":\"FACT_NAME\"},\n {\"title\":\"娴侀噺\", \"name\":\"VFLOW\"}\n ],\n\n recs : recs\n});\n\nargs.sendChartToClient({\n title : \"鐢甸噺\",\n col_time : \"OTIME\",\n col_name : \"FACT_NAME\",\n col_value: \"VPOWER\",\n recs : recs\n});\nargs.sendKnowledgeToClient({\n result : args.getArg(\"num1\") + \" + \" + args.getArg(\"num2\") + \"=\" + args.getArg(\"V\")\n});", + value: + 'var recs = args.querySQL(\n " SELECT * FROM guangming_data.tc_hzl_v4"\n + " WHERE otime >={start_time}::timestamp + \'-1 months\' AND otime < {end_time}::timestamp + \'1 days\'", {\n start_time : args.getArg("start_time"),\n end_time : args.getArg("end_time")\n});\n\nvar resultText = "鏃堕棿,姘村巶鍚嶏紝鑰楃數閲�,鍑哄彛娴侀噺\\n";\nfor(var recIndex in recs)\n{\n var rec = recs[recIndex];\n resultText += rec["OTIME"] + "," + rec["FACT_NAME"] + "," + rec["VPOWER"] + "," + rec["VFLOW"] + "\\n";\n}\nargs.setArg("RECORD_LIST", resultText);\n\nargs.sendTableToClent({\n title : "娴侀噺",\n columns : [\n {"title":"鏃堕棿", "name":"OTIME"},\n {"title":"姘村巶", "name":"FACT_NAME"},\n {"title":"娴侀噺", "name":"VFLOW"}\n ],\n\n recs : recs\n});\n\nargs.sendChartToClient({\n title : "鐢甸噺",\n col_time : "OTIME",\n col_name : "FACT_NAME",\n col_value: "VPOWER",\n recs : recs\n});\nargs.sendKnowledgeToClient({\n result : args.getArg("num1") + " + " + args.getArg("num2") + "=" + args.getArg("V")\n});', language: ['text', 'javascript'], defaultLanguage: 'javascript', }, @@ -212,6 +213,63 @@ ], }; break; + + case NodeType.N8n: + data = { + ...data, + description: '鎵цn8n宸ヤ綔娴併��', + [VueFlowConstant.GROUP_PARAMS_KEY]: [ + { + name: '宸ヤ綔娴両D', + [VueFlowConstant.PARAMS_KEY]: [ + { + key: 'n8n_flow_id', + type: 'n8n_flow_id', + required: true, + value: { type: 'input', label: '', value: '' }, + }, + ], + }, + { + name: '鍏ュ弬', + [VueFlowConstant.PARAMS_KEY]: [ + { + key: 'n8n_input', + type: 'n8n_input', + value: { + type: 'json', + value: {}, + }, + }, + ], + }, + { + name: '鍑哄弬', + [VueFlowConstant.PARAMS_KEY]: [ + { + key: 'n8n_output', + type: 'n8n_output', + required: true, + value: { type: 'input', label: '', value: '' }, + }, + ], + }, + // { + // name: '鎵ц浠g爜', + // [VueFlowConstant.PARAMS_KEY]: [ + // { + // key: 'code', + // type: 'code', + // required: true, + // value: `from lib.system import *\n\nargJson = readArgJson()\nrestult = argJson['num1'] + argJson['num2']\nwriteResultText(str(restult))`, + // language: ['python'], + // defaultLanguage: 'python', + // }, + // ], + // }, + ], + }; + break; case NodeType.TextResource: data = { ...data, diff --git a/src/components/vue-flow/ui/VueFlowConfig.ts b/src/components/vue-flow/ui/VueFlowConfig.ts index fc21621..cc5eebf 100644 --- a/src/components/vue-flow/ui/VueFlowConfig.ts +++ b/src/components/vue-flow/ui/VueFlowConfig.ts @@ -94,6 +94,7 @@ class: 'bg-[#0062be] !p-1', }, ], + [ NodeType.TextResource, { @@ -114,5 +115,16 @@ class: 'bg-[#98fb98] ', }, ], + + [ + NodeType.N8n, + { + type: NodeType.N8n, + title: nodeTypeMap[NodeType.N8n], + icon: 'didaima', + fontSize: '18', + class: 'bg-[#0062be] !p-1', + }, + ], ]); } diff --git a/src/components/vue-flow/ui/nodes/N8nNode.vue b/src/components/vue-flow/ui/nodes/N8nNode.vue new file mode 100644 index 0000000..4469312 --- /dev/null +++ b/src/components/vue-flow/ui/nodes/N8nNode.vue @@ -0,0 +1,138 @@ +<template> + <NodeBasicLayout + v-model:title="data.title" + :type="NodeType.N8n" + :style="{ width: '320px' }" + :showOffset="false" + :description="data.description" + :isViewMode="isViewMode" + > + <Handle :id="targetHandleId" type="target" :position="Position.Left" /> + <el-form ref="formRef" :model="data" :rules="formRules" label-position="right" label-width="60px" :show-message="false"> + <FieldLayout :title="workflow.name" required> + <!-- codeInput.params[0].value --> + <el-form-item prop="group_params.0.params.0.value.value" labelWidth="0"> + <el-select + class="!w-[220px] flex-0" + filterable + :disabled="isViewMode" + placeholder="宸ヤ綔娴両D" + v-model="workflow.params[0].value.value" + @change="workflowValueChange" + > + <el-option v-for="item in workflowList" :key="item.id" :value="item.id" :label="item.name"></el-option> + </el-select> + </el-form-item> + </FieldLayout> + <FieldLayout :title="flowInput.name" v-if="Object.keys(flowInput.params[0].value.value).length > 0"> + <!-- codeInput.params[0].value --> + + <FieldLayout level="2" :title="item" v-for="(item, index) in Object.keys(flowInput.params[0].value.value)" :key="index"> + <el-form-item :prop="`group_params.1.params.0.value.value.${item}`" labelWidth="0"> + <el-input + filterable + class="w-[120px] flex-0" + v-model="flowInput.params[0].value.value[item]" + :readonly="isViewMode" + placeholder="鍙傛暟鍚�" + ></el-input> + </el-form-item> + </FieldLayout> + </FieldLayout> + <FieldLayout :title="flowOutput.name"> + <!-- codeOutput.params[0].value --> + <el-form-item prop="group_params.2.params.0.value.value" labelWidth="0"> + <el-input + filterable + class="w-[120px] flex-0" + v-model="flowOutput.params[0].value.value" + placeholder="鍙傛暟鍚�" + :readonly="isViewMode" + ></el-input> + </el-form-item> + </FieldLayout> + </el-form> + <Handle :id="sourceHandleId" type="source" :position="Position.Right" /> + </NodeBasicLayout> +</template> + +<script lang="ts" setup> +import type { NodeProps } from '@vue-flow/core'; +import { Handle, Position, useNode } from '@vue-flow/core'; +import { onMounted, ref } from 'vue'; +import { VueFlowHelper } from '../../VueFlowHelper'; +import { NodeType, ParameterType } from '../../vueFlowEnum'; +import CodeEditor from '/@/components/input/codeEditor/index.vue'; +// import CodeEditDialog from './components/CodeEditDlg.vue'; +import FieldLayout from './components/FieldLayout.vue'; +import NodeBasicLayout from './components/NodeBasicLayout.vue'; +import type { LLMNodeData, LLMNodeEvents } from './index'; +import { validateForm } from './utils'; +import { textTypeMap } from '/@/components/input/codeEditor/types'; +const props = defineProps< + NodeProps<LLMNodeData, LLMNodeEvents> & { + isViewMode?: boolean; + workflowList: any[]; + } +>(); + +const getFlowByID = (id: string) => { + return props.workflowList.find((item) => item.id === id); +}; + +const workflowValueChange = (value: string) => { + console.log('馃殌 ~ workflowValueChange ~ workflow:', value); + const flow = getFlowByID(value); + console.log('馃殌 ~ workflowValueChange ~ flow:', flow); + const nodes = flow.nodes; + const webhookNode = nodes.find((item) => item.type === 'n8n-nodes-base.webhook'); + console.log('馃殌 ~ workflowValueChange ~ webhookNode:', webhookNode); + flowInput.value.params[0].value.value = {}; + if (!webhookNode) { + return; + } + + // Get path parameter from webhook node + const path = webhookNode?.parameters?.path; + if (path) { + // Parse path parameters from path string + const pathParams = path.match(/\/:([^\/]+)/g)?.map((p) => p.substring(2)) || []; + console.log('馃殌 ~ pathParams:', pathParams); + // Update input parameters with path params + if (pathParams.length > 0) { + const inputValue = flowInput.value.params[0].value.value || {}; + pathParams.forEach((param) => { + if (!inputValue[param]) { + inputValue[param] = ''; + } + }); + flowInput.value.params[0].value.value = inputValue; + } + console.log('馃殌 ~ flowInput.value.params[0].value.value:', flowInput.value.params[0].value.value); + } +}; +const emit = defineEmits<{ + (e: 'register', data: { validateForm: () => Promise<{ isValid: boolean; invalidFields?: any }> }): void; +}>(); +const node = useNode(); +const targetHandleId = ref(VueFlowHelper.getHandleId(node.node, 'target')); +const sourceHandleId = ref(VueFlowHelper.getHandleId(node.node, 'source')); +const data = ref(node.node.data); +const workflow = ref(VueFlowHelper.getGroupParam(data.value, 0)); +const flowInput = ref(VueFlowHelper.getGroupParam(data.value, 1)); +const flowOutput = ref(VueFlowHelper.getGroupParam(data.value, 2)); + +const formRef = ref(); +const formRules = ref({ + 'group_params.0.params.0.value.value': [{ required: true, message: '璇烽�夋嫨宸ヤ綔娴�', trigger: 'blur' }], + // 'group_params.1.params.0.value.value': [{ required: true, message: '璇疯緭鍏ュ嚭鍙�', trigger: 'blur' }], + // 'group_params.2.params.0.value': [{ required: true, message: '璇疯緭鍏ユ墽琛屼唬鐮�', trigger: 'blur' }], +}); + +// 娉ㄥ唽鑺傜偣瀹炰緥 +onMounted(() => { + emit('register', { + validateForm: validateForm(formRef) as any, + }); +}); +</script> diff --git a/src/components/vue-flow/vueFlowEnum.ts b/src/components/vue-flow/vueFlowEnum.ts index c1f636a..4e40751 100644 --- a/src/components/vue-flow/vueFlowEnum.ts +++ b/src/components/vue-flow/vueFlowEnum.ts @@ -21,6 +21,7 @@ Func = 'func', Code = 'code', PythonCode = 'python_code', + N8n = 'n8n', TextResource = 'text_resource', Analysis = 'analysis', @@ -37,7 +38,7 @@ [NodeType.Func]: '鎵ц鍔熻兘', [NodeType.Code]: '浠g爜', [NodeType.PythonCode]: 'python浠g爜', - + [NodeType.N8n]: 'n8n鑺傜偣', [NodeType.TextResource]: '鏂囨湰璧勬簮', [NodeType.Analysis]: '鍒嗘瀽', }; -- Gitblit v1.9.3