wujingjing
2025-03-20 1598df378ab18d8388483d129439eec3cac38afe
添加 n8n 节点
已修改5个文件
已添加2个文件
311 ■■■■■ 文件已修改
customer_list/yw/static/config/globalConfig.test.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/n8n/index.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/vue-flow/MainCanvas.vue 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/vue-flow/VueFlowHelper.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/vue-flow/ui/VueFlowConfig.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/vue-flow/ui/nodes/N8nNode.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/vue-flow/vueFlowEnum.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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: {
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);
};
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);
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: '工作流ID',
                            [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: '执行代码',
                        //     [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,
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',
            },
        ],
    ]);
}
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="工作流ID"
                        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>
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]: '代码',
    [NodeType.PythonCode]: 'python代码',
    [NodeType.N8n]: 'n8n节点',
    [NodeType.TextResource]: '文本资源',
    [NodeType.Analysis]: '分析',
};