瀏覽代碼

工作流动态匹配

CzRger 1 周之前
父節點
當前提交
3110052b65

src/assets/images/answer.png → src/assets/images/workflow/answer.png


src/assets/images/if-else.png → src/assets/images/workflow/if-else.png


File diff suppressed because it is too large
+ 1 - 0
src/assets/images/workflow/llm.svg


src/assets/images/test.png → src/assets/images/workflow/test.png


+ 1 - 1
src/views/manage/app/workflow/index.vue

@@ -25,7 +25,7 @@
         <div class="text-[#576275]" v-if="state.autoSaveTimestamp">
           自动保存{{ state.autoSaveTimestamp }}
         </div>
-        <CzrButton type="primary" title="发布" />
+        <!--        <CzrButton type="primary" title="发布" />-->
       </div>
       <div class="mt-4 flex-1 rounded-sm bg-[var(--czr-main-color)]/5 shadow">
         <workflowGraph

+ 2 - 5
src/views/workflow/README.md

@@ -1,7 +1,4 @@
 # 添加节点的方法
 1、instance内新建对应类型的node、panel文件和default.ts
-2、[types.ts](types.ts)文件中完善NodeDataStruct、NodeType、NodeTypeObj
-3、[config.ts](config.ts)文件中完善
-4、[node-add.vue](chart/node-add.vue)文件中完善
-5、[node-index.vue](chart/node-index.vue)文件中完善
-6、[panel-index.vue](chart/panel-index.vue)文件中完善
+2、[types.ts](types.ts)文件中完善NodeType、NodeTypeObj、NodeDataStruct
+3、[config.ts](config.ts)文件中完善nodeSources

+ 13 - 41
src/views/workflow/chart/node-add.vue

@@ -1,32 +1,22 @@
 <template>
-  <div class="node-add">
-    <div
-      class="node-add-item __hover-bg"
-      @click="$emit('onAddNode', { type: NodeType.Test, e: $event })"
-    >
-      <img src="@/assets/images/test.png" />
-      {{ NodeTypeObj[NodeType.Test].title }}
-    </div>
-    <div
-      class="node-add-item __hover-bg"
-      @click="$emit('onAddNode', { type: NodeType.Answer, e: $event })"
-    >
-      <img src="@/assets/images/answer.png" />
-      {{ NodeTypeObj[NodeType.Answer].title }}
-    </div>
-    <div
-      class="node-add-item __hover-bg"
-      @click="$emit('onAddNode', { type: NodeType.IfElse, e: $event })"
-    >
-      <img src="@/assets/images/if-else.png" />
-      {{ NodeTypeObj[NodeType.IfElse].title }}
-    </div>
+  <div class="flex flex-col gap-1">
+    <template v-for="(value, key) in nodeSources">
+      <div
+        v-if="value.isAdd"
+        class="__hover-bg flex items-center rounded-sm px-1 py-0.5 text-xs"
+        @click="$emit('onAddNode', { type: key, e: $event })"
+      >
+        <img :src="value.icon" class="m-1 size-4" />
+        {{ NodeTypeObj[key].title }}
+      </div>
+    </template>
   </div>
 </template>
 
 <script setup lang="ts">
 import { getCurrentInstance, reactive, ref } from 'vue'
 import { NodeType, NodeTypeObj } from '@/views/workflow/types'
+import { nodeSources } from '@/views/workflow/config'
 
 const emit = defineEmits(['onAddNode'])
 const props = defineProps({})
@@ -34,22 +24,4 @@ const { proxy }: any = getCurrentInstance()
 const state: any = reactive({})
 </script>
 
-<style lang="scss" scoped>
-.node-add {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  .node-add-item {
-    padding: 2px 4px;
-    font-size: 13px;
-    border-radius: 6px;
-    display: flex;
-    align-items: center;
-    > img {
-      width: 16px;
-      height: 16px;
-      margin: 4px;
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 5 - 17
src/views/workflow/chart/node-index.vue

@@ -19,17 +19,10 @@
       </div>
     </div>
     <div class="px-3" v-if="state.init">
-      <template v-if="state.nodeData?.type === NodeType.Start">
-        <startNode :node="state.node" />
-      </template>
-      <template v-else-if="state.nodeData?.type === NodeType.Test">
-        <testNode :node="state.node" />
-      </template>
-      <template v-else-if="state.nodeData?.type === NodeType.IfElse">
-        <ifElseNode :node="state.node" />
-      </template>
-      <template v-else-if="state.nodeData?.type === NodeType.Answer">
-        <answerNode :node="state.node" />
+      <template v-for="(value, key) in nodeSources">
+        <template v-if="state.nodeData?.type === key">
+          <component :is="value.nodeCom" :node="state.node" />
+        </template>
       </template>
     </div>
   </div>
@@ -46,13 +39,8 @@ import {
   ref,
   watch,
 } from 'vue'
-import startNode from '../instance/start/node/index.vue'
-import ifElseNode from '../instance/if-else/node/index.vue'
-import testNode from '../instance/test/node/index.vue'
-import answerNode from '../instance/answer/node/index.vue'
 import { useWorkflowStore } from '@/stores'
-import { NodeType } from '@/views/workflow/types'
-import { lineActiveStyle, lineStyle } from '@/views/workflow/config'
+import { nodeSources } from '@/views/workflow/config'
 
 const WorkflowStore = useWorkflowStore()
 const getNode: any = inject('getNode')

+ 6 - 11
src/views/workflow/chart/panel-index.vue

@@ -17,17 +17,10 @@
         />
       </div>
       <div class="panel-content">
-        <template v-if="nodeDataCpt.type === NodeType.Start">
-          <startPanel :node="WorkflowStore.panel.node" />
-        </template>
-        <template v-else-if="nodeDataCpt.type === NodeType.Test">
-          <testPanel :node="WorkflowStore.panel.node" />
-        </template>
-        <template v-else-if="nodeDataCpt.type === NodeType.Answer">
-          <answerPanel :node="WorkflowStore.panel.node" />
-        </template>
-        <template v-else-if="nodeDataCpt.type === NodeType.IfElse">
-          <ifElsePanel :node="WorkflowStore.panel.node" />
+        <template v-for="(value, key) in nodeSources">
+          <template v-if="nodeDataCpt.type === key">
+            <component :is="value.panelCom" :node="WorkflowStore.panel.node" />
+          </template>
         </template>
       </div>
     </div>
@@ -45,10 +38,12 @@ import {
 } from 'vue'
 import startPanel from '../instance/start/panel/index.vue'
 import answerPanel from '../instance/answer/panel/index.vue'
+import llmPanel from '../instance/llm/panel/index.vue'
 import ifElsePanel from '../instance/if-else/panel/index.vue'
 import testPanel from '../instance/test/panel/index.vue'
 import { useWorkflowStore } from '@/stores'
 import { NodeType } from '@/views/workflow/types'
+import { nodeSources } from '@/views/workflow/config'
 
 const WorkflowStore = useWorkflowStore()
 const emit = defineEmits([])

+ 123 - 53
src/views/workflow/config.ts

@@ -1,15 +1,25 @@
 import { v4 } from 'uuid'
 import {
-  ConditionMode,
   NodeDataStruct,
   NodePortStruct,
   NodeStruct,
   NodeType,
+  NodeTypeObj,
 } from '@/views/workflow/types'
 import startNodeDefault from '@/views/workflow/instance/start/default'
 import answerNodeDefault from '@/views/workflow/instance/answer/default'
+// @ts-ignore
+import answerImg from '@/assets/images/workflow/answer.png'
 import ifElseNodeDefault from '@/views/workflow/instance/if-else/default'
+// @ts-ignore
+import ifElseImg from '@/assets/images/workflow/if-else.png'
+import llmNodeDefault from '@/views/workflow/instance/llm/default'
+// @ts-ignore
+import llmImg from '@/assets/images/workflow/llm.svg'
 import testNodeDefault from '@/views/workflow/instance/test/default'
+// @ts-ignore
+import testImg from '@/assets/images/workflow/test.png'
+import { defineAsyncComponent } from 'vue'
 
 export const lineStyle = {
   stroke: '#bcbcbc',
@@ -22,64 +32,124 @@ export const lineActiveStyle = {
   targetMarker: null,
 }
 
-export const nodeDefault = {
-  [NodeType.Test]: () =>
-    <NodeStruct>{
-      x: 0,
-      y: 0,
-      data: <NodeDataStruct>{
-        id: v4(),
-        title: '测试节点',
-        desc: '',
-        ...testNodeDefault.defaultValue(),
+export const nodeSources = {
+  [NodeType.Start]: {
+    defaultValue: () =>
+      <NodeStruct>{
+        x: 0,
+        y: 0,
+        data: <NodeDataStruct>{
+          id: v4(),
+          title: NodeTypeObj[NodeType.Start].title,
+          desc: '',
+          ...startNodeDefault.defaultValue(),
+        },
       },
-    },
-  [NodeType.Start]: () =>
-    <NodeStruct>{
-      x: 0,
-      y: 0,
-      data: <NodeDataStruct>{
-        id: v4(),
-        title: '开始',
-        desc: '',
-        ...startNodeDefault.defaultValue(),
+    isAdd: false,
+    nodeCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/start/node/index.vue'),
+    ),
+    panelCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/start/panel/index.vue'),
+    ),
+  },
+  [NodeType.Answer]: {
+    defaultValue: () =>
+      <NodeStruct>{
+        x: 0,
+        y: 0,
+        data: <NodeDataStruct>{
+          id: v4(),
+          title: NodeTypeObj[NodeType.Answer].title,
+          desc: '',
+          ...answerNodeDefault.defaultValue(),
+        },
       },
-    },
-  [NodeType.Answer]: () =>
-    <NodeStruct>{
-      x: 0,
-      y: 0,
-      data: <NodeDataStruct>{
-        id: v4(),
-        title: '直接回复',
-        desc: '',
-        ...answerNodeDefault.defaultValue(),
+    isAdd: true,
+    icon: answerImg,
+    nodeCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/answer/node/index.vue'),
+    ),
+    panelCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/answer/panel/index.vue'),
+    ),
+  },
+  [NodeType.LLM]: {
+    defaultValue: () =>
+      <NodeStruct>{
+        x: 0,
+        y: 0,
+        data: <NodeDataStruct>{
+          id: v4(),
+          title: NodeTypeObj[NodeType.LLM].title,
+          desc: '',
+          ...llmNodeDefault.defaultValue(),
+        },
       },
-    },
-  [NodeType.IfElse]: () =>
-    <NodeStruct>{
-      x: 0,
-      y: 0,
-      data: <NodeDataStruct>{
-        id: v4(),
-        title: '条件分支',
-        desc: '',
-        ...ifElseNodeDefault.defaultValue(),
-        ports: <NodePortStruct[]>[
-          {
-            id: v4(),
-            ...ifElseNodeDefault.caseValue(),
-          },
-          {
-            id: v4(),
-            isElse: true,
-          },
-        ],
+    isAdd: true,
+    icon: llmImg,
+    nodeCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/llm/node/index.vue'),
+    ),
+    panelCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/llm/panel/index.vue'),
+    ),
+  },
+  [NodeType.IfElse]: {
+    defaultValue: () =>
+      <NodeStruct>{
+        x: 0,
+        y: 0,
+        data: <NodeDataStruct>{
+          id: v4(),
+          title: NodeTypeObj[NodeType.IfElse].title,
+          desc: '',
+          ...ifElseNodeDefault.defaultValue(),
+          ports: <NodePortStruct[]>[
+            {
+              id: v4(),
+              ...ifElseNodeDefault.caseValue(),
+            },
+            {
+              id: v4(),
+              isElse: true,
+            },
+          ],
+        },
       },
-    },
+    isAdd: true,
+    icon: ifElseImg,
+    nodeCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/if-else/node/index.vue'),
+    ),
+    panelCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/if-else/panel/index.vue'),
+    ),
+  },
+  [NodeType.Test]: {
+    defaultValue: () =>
+      <NodeStruct>{
+        x: 0,
+        y: 0,
+        data: <NodeDataStruct>{
+          id: v4(),
+          title: NodeTypeObj[NodeType.Test].title,
+          desc: '',
+          ...testNodeDefault.defaultValue(),
+        },
+      },
+    isAdd: true,
+    icon: testImg,
+    nodeCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/test/node/index.vue'),
+    ),
+    panelCom: defineAsyncComponent(
+      () => import('@/views/workflow/instance/test/panel/index.vue'),
+    ),
+  },
 }
 export const getNodeDefault = (type: NodeType) => {
-  const node = nodeDefault[type]()
+  const node = nodeSources[type].defaultValue()
   node.id = node.data.id
   node.data.type = type
   return node

+ 1 - 0
src/views/workflow/instance/answer/node/index.vue

@@ -5,6 +5,7 @@
       :node="node"
       :readonly="true"
       class="mb-2"
+      title="回复"
     />
   </div>
 </template>

+ 1 - 0
src/views/workflow/instance/answer/panel/index.vue

@@ -4,6 +4,7 @@
       v-model="state.nodeData.answer"
       :node="node"
       class="mt-4 max-h-100"
+      title="回复"
     />
   </div>
 </template>

+ 7 - 1
src/views/workflow/instance/component/params-textarea/index.vue

@@ -1,5 +1,10 @@
 <template>
-  <version2 v-model="state.value" :node="node" :readonly="readonly" />
+  <version2
+    v-model="state.value"
+    :node="node"
+    :readonly="readonly"
+    :title="title"
+  />
 </template>
 
 <script setup lang="ts">
@@ -25,6 +30,7 @@ const props = defineProps({
     default: '',
   },
   node: {},
+  title: { default: '' },
   readonly: {
     default: false,
   },

+ 3 - 2
src/views/workflow/instance/component/params-textarea/version-2.vue

@@ -6,7 +6,7 @@
       <div
         class="flex items-center gap-2 py-1 text-xs font-semibold text-gray-700"
       >
-        <div>回复</div>
+        <div>{{ title }}</div>
         <div class="ml-auto">{{ state.textCount }}</div>
         <varsPopover :node="node" @setVars="setVars">
           <el-tooltip content="变量" placement="top">
@@ -75,7 +75,7 @@
       <div
         class="flex items-center gap-2 py-1 text-xs font-semibold text-gray-700"
       >
-        <div>回复</div>
+        <div>{{ title }}</div>
       </div>
       <div class="text-text-secondary flex-1 py-0 text-xs">
         <div
@@ -119,6 +119,7 @@ const props = defineProps({
     default: '',
   },
   node: {},
+  title: {},
   readonly: {
     default: false,
   },

+ 9 - 0
src/views/workflow/instance/llm/default.ts

@@ -0,0 +1,9 @@
+const nodeDefault = {
+  defaultValue: () => ({
+    modelId: '',
+    tip: '',
+    outVars: [{ label: '生成内容', key: 'text', type: 'String' }],
+  }),
+}
+
+export default nodeDefault

+ 40 - 0
src/views/workflow/instance/llm/node/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <div class="">
+    <div class="mb-2 text-xs opacity-85" v-if="state.nodeData.modelName">
+      {{ state.nodeData.modelName }}
+    </div>
+    <paramsTextarea
+      v-model="state.nodeData.tip"
+      :node="node"
+      :readonly="true"
+      class="mb-2"
+      title="提示词"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import {
+  computed,
+  getCurrentInstance,
+  inject,
+  onMounted,
+  reactive,
+  ref,
+} from 'vue'
+import paramsTextarea from '@/views/workflow/instance/component/params-textarea/index.vue'
+
+const emit = defineEmits([])
+const props = defineProps({
+  node: <any>{},
+})
+const { proxy }: any = getCurrentInstance()
+const state: any = reactive({
+  nodeData: {},
+})
+onMounted(() => {
+  state.nodeData = props.node.data.workflowData
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 74 - 0
src/views/workflow/instance/llm/panel/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="panel-block" v-if="state.nodeData">
+    <div class="_p-title">
+      <div class="text-sm">模型</div>
+    </div>
+    <CzrFormColumn
+      :span="24"
+      label-width="0px"
+      v-model:param="state.nodeData.modelId"
+      link="select"
+      :options="state.llmModelOptions"
+      labelKey="name"
+      valueKey="id"
+      @getObject="(val) => (state.nodeData.modelName = val.name)"
+    />
+    <paramsTextarea
+      v-model="state.nodeData.tip"
+      :node="node"
+      class="mt-4 max-h-100"
+      title="提示词"
+    />
+    <div class="_p-title">
+      <div class="text-sm">输出变量</div>
+    </div>
+    <varsOut :outVars="state.nodeData.outVars" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getCurrentInstance, reactive, ref, watch } from 'vue'
+import { useWorkflowStore } from '@/stores'
+import paramsTextarea from '@/views/workflow/instance/component/params-textarea/index.vue'
+import varsOut from '@/views/workflow/instance/component/vars/vars-out.vue'
+import { pluginGetInstanceList } from '@/api/modules/model'
+
+const WorkflowStore = useWorkflowStore()
+const emit = defineEmits([])
+const props = defineProps({
+  node: <any>{},
+})
+const { proxy }: any = getCurrentInstance()
+const state: any = reactive({
+  nodeData: null,
+  vars: {
+    show: false,
+    transfer: {},
+  },
+  llmModelOptions: [],
+})
+const initDictionary = () => {
+  pluginGetInstanceList({
+    page: 1,
+    size: 100000,
+    modeType: 'LLM',
+    status: 1,
+  }).then(({ data }: any) => {
+    state.llmModelOptions = data.records
+  })
+}
+watch(
+  () => props.node,
+  (n) => {
+    if (n) {
+      state.nodeData = n.data.workflowData
+      initDictionary()
+    }
+  },
+  { immediate: true },
+)
+</script>
+
+<style lang="scss" scoped>
+@use '@/views/workflow/instance/component/style';
+</style>

+ 31 - 21
src/views/workflow/types.ts

@@ -5,48 +5,58 @@ export type NodeStruct = {
   data: NodeDataStruct
 }
 
-export type NodeDataStruct = {
-  id: string
-  title: string
-  desc?: string
-  type?: NodeType.Test | NodeType.Start | NodeType.Answer | NodeType.IfElse
-  ports?: NodePortStruct[]
-  edgeSource?: string
-}
-
-export type NodePortStruct = {
-  id: string
-  mode?: string
-  cases?: Array<any>
-  isElse?: boolean
-}
-
 export enum NodeType {
+  LLM = 'llm',
+  Answer = 'answer',
   Test = 'test',
   Start = 'start',
-  Answer = 'answer',
   IfElse = 'if-else',
 }
 
 export const NodeTypeObj = {
-  [NodeType.Test]: {
-    type: NodeType.Test,
-    title: '测试节点',
-  },
   [NodeType.Start]: {
     type: NodeType.Start,
     title: '开始',
   },
+  [NodeType.LLM]: {
+    type: NodeType.LLM,
+    title: 'LLM',
+  },
   [NodeType.Answer]: {
     type: NodeType.Answer,
     title: '直接回复',
   },
+  [NodeType.Test]: {
+    type: NodeType.Test,
+    title: '测试节点',
+  },
   [NodeType.IfElse]: {
     type: NodeType.IfElse,
     title: '条件分支',
   },
 }
 
+export type NodeDataStruct = {
+  id: string
+  title: string
+  desc?: string
+  type?:
+    | NodeType.Start
+    | NodeType.Answer
+    | NodeType.LLM
+    | NodeType.Test
+    | NodeType.IfElse
+  ports?: NodePortStruct[]
+  edgeSource?: string
+}
+
+export type NodePortStruct = {
+  id: string
+  mode?: string
+  cases?: Array<any>
+  isElse?: boolean
+}
+
 export enum ConditionString {
   Includes = 'includes',
   NotIncludes = 'not includes',