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