Skip to content

Commit bc0efd0

Browse files
committed
feat: Tool workflow node
1 parent c5ae19d commit bc0efd0

2 files changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import ToolWorkflowLibNode from './index.vue'
2+
import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node'
3+
import { WorkflowNodeType } from '@/workflow-canvas/types'
4+
5+
class ToolWorkflowLibNodeView extends WorkflowNodeView {
6+
constructor(props: ConstructorParameters<typeof WorkflowNodeView>[0]) {
7+
super(props, ToolWorkflowLibNode)
8+
}
9+
}
10+
11+
export default { type: WorkflowNodeType.ToolWorkflowLib, model: WorkflowNodeModel, view: ToolWorkflowLibNodeView }
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<script setup lang="ts">
2+
import { computed, inject, onMounted, useTemplateRef } from 'vue'
3+
import { cloneDeep, set } from 'lodash'
4+
import type { FormInstance } from 'element-plus'
5+
import ToolApi from '@/api/admin/workspace/tool/tool'
6+
import NodeContainer from '@/workflow-canvas/core/node-container/index.vue'
7+
import NodeCascader from '@/workflow-canvas/core/NodeCascader.vue'
8+
import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node'
9+
import { WorkflowMode } from '@/workflow-canvas/types'
10+
11+
defineOptions({ name: 'WorkflowToolWorkflowLibNode' })
12+
13+
interface InputField {
14+
field: string
15+
label: string
16+
name?: string
17+
type: string
18+
is_required?: boolean
19+
source: 'reference' | 'custom'
20+
value: any
21+
}
22+
23+
interface OutputField {
24+
label: string
25+
value: string
26+
}
27+
28+
interface ToolWorkflowLibNodeForm {
29+
input_field_list: InputField[]
30+
input_title?: string
31+
is_result?: boolean
32+
tool_lib_id?: string
33+
}
34+
35+
const getModel = inject('getModel') as () => WorkflowNodeModel
36+
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
37+
const model = getModel()
38+
39+
const formRef = useTemplateRef<FormInstance>('formRef')
40+
41+
const toolNodeData = (model.properties.node_data ?? {}) as Partial<ToolWorkflowLibNodeForm>
42+
if (!Array.isArray(toolNodeData.input_field_list)) toolNodeData.input_field_list = []
43+
model.properties.node_data = toolNodeData as ToolWorkflowLibNodeForm
44+
45+
const formData = computed<ToolWorkflowLibNodeForm>({
46+
get: () => model.properties.node_data as ToolWorkflowLibNodeForm,
47+
set: (value) => set(model.properties, 'node_data', value),
48+
})
49+
50+
const showReturnContent = computed(() =>
51+
[WorkflowMode.Application, WorkflowMode.ApplicationLoop, WorkflowMode.Tool, WorkflowMode.ToolLoop].includes(workflowMode),
52+
)
53+
54+
const inputTitle = computed(() => formData.value.input_title || '输入参数')
55+
56+
function onSourceChange(item: InputField) {
57+
if (item.type === 'boolean') {
58+
item.value = false
59+
} else if (['array', 'dict'].includes(item.type)) {
60+
item.value = []
61+
} else {
62+
item.value = ''
63+
}
64+
}
65+
66+
function validate() {
67+
return formRef.value?.validate().catch((error) => Promise.reject({ node: model, errMessage: error })) ?? Promise.resolve()
68+
}
69+
70+
function createInputField(field: InputField, previousFields: InputField[]): InputField {
71+
const previousField = previousFields.find((item) => item.field === field.field)
72+
if (field.source === 'reference') {
73+
return {
74+
...field,
75+
source: 'reference',
76+
value: previousField?.source === 'reference' ? cloneDeep(previousField.value) : [],
77+
}
78+
}
79+
return {
80+
...field,
81+
source: 'custom',
82+
value: previousField?.source === 'custom' ? previousField.value : '',
83+
}
84+
}
85+
86+
function updateField() {
87+
const toolId = formData.value.tool_lib_id
88+
if (!toolId) {
89+
model.properties.status = 500
90+
return
91+
}
92+
93+
ToolApi.getToolDetail(toolId)
94+
.then((tool) => {
95+
const workflowNodes = (tool as any)?.work_flow?.nodes || []
96+
const baseNode = workflowNodes.find((n: any) => n.type === 'tool-base-node')
97+
98+
if (baseNode) {
99+
const newInputList = baseNode.properties.user_input_field_list || []
100+
const newOutputList = baseNode.properties.user_output_field_list || []
101+
102+
const oldConfigFields = (model.properties.config?.fields || []) as OutputField[]
103+
const configFieldList = newOutputList.map((item: any) => {
104+
const old = oldConfigFields.find((o) => o.value === item.field)
105+
return old ? JSON.parse(JSON.stringify(old)) : { label: item.label, value: item.field }
106+
})
107+
108+
const inputTitleValue = baseNode.properties.user_input_config?.title
109+
const outputTitle = baseNode.properties.user_output_config?.title
110+
const previousFields = formData.value.input_field_list
111+
const mergedInputList = newInputList.map((item: any) => {
112+
const findField = previousFields.find((oldItem) => oldItem.field === item.field)
113+
if (findField) {
114+
return {
115+
...item,
116+
source: findField.source,
117+
value: JSON.parse(JSON.stringify(findField.value)),
118+
}
119+
}
120+
return { ...item, source: 'custom', value: '' }
121+
})
122+
123+
set(formData.value, 'input_field_list', mergedInputList)
124+
set(model.properties, 'config', {
125+
fields: configFieldList,
126+
output_title: outputTitle,
127+
})
128+
set(formData.value, 'input_title', inputTitleValue)
129+
}
130+
model.properties.status = (tool as any)?.is_active ? 200 : 500
131+
model.clearNextNodeField(true)
132+
})
133+
.catch(() => {
134+
model.properties.status = 500
135+
})
136+
}
137+
138+
onMounted(() => {
139+
if (typeof formData.value.is_result === 'undefined') {
140+
const isLast = !model.graphModel.getNodeOutgoingNode(model.id).length
141+
if (isLast) {
142+
formData.value.is_result = true
143+
}
144+
}
145+
updateField()
146+
model.validate = validate
147+
})
148+
</script>
149+
150+
<template>
151+
<NodeContainer :node-model="model">
152+
<el-form ref="formRef" :model="formData" label-position="top" hide-required-asterisk @submit.prevent>
153+
<h6 class="mk-title-decoration mb-2">{{ inputTitle }}</h6>
154+
155+
<el-card shadow="never" class="card-never mb-4" style="--el-card-padding: 12px">
156+
<template v-if="formData.input_field_list.length">
157+
<el-form-item
158+
v-for="(field, index) in formData.input_field_list"
159+
:key="`${field.field}-${index}`"
160+
:prop="`input_field_list.${index}.value`"
161+
:rules="{
162+
required: field.is_required,
163+
message: field.source === 'reference' ? '请选择参数' : '请输入参数',
164+
trigger: field.source === 'reference' ? 'change' : 'blur',
165+
}"
166+
>
167+
<template #label>
168+
<div class="flex-between">
169+
<div class="flex items-center">
170+
<div class="mr-2 max-w-32 truncate" :title="field.label">
171+
{{ field.label }}
172+
</div>
173+
<span v-if="field.is_required" class="text-danger">*</span>
174+
</div>
175+
<el-select
176+
:teleported="false"
177+
v-model="field.source"
178+
@change="onSourceChange(field)"
179+
size="small"
180+
style="width: 85px"
181+
>
182+
<el-option label="引用" value="reference" />
183+
<el-option label="自定义" value="custom" />
184+
</el-select>
185+
</div>
186+
</template>
187+
<NodeCascader
188+
v-if="field.source === 'reference'"
189+
v-model="field.value"
190+
:node-model="model"
191+
class="w-full"
192+
placeholder="请选择参数"
193+
/>
194+
<template v-else>
195+
<el-input
196+
v-if="['string'].includes(field.type)"
197+
v-model="field.value"
198+
placeholder="请输入参数"
199+
/>
200+
<el-input-number
201+
v-if="['int', 'float'].includes(field.type)"
202+
v-model="field.value"
203+
class="w-full"
204+
/>
205+
<el-switch
206+
v-if="['boolean'].includes(field.type)"
207+
v-model="field.value"
208+
:active-value="true"
209+
:inactive-value="false"
210+
/>
211+
</template>
212+
</el-form-item>
213+
</template>
214+
<MkEmpty v-else :image-size="60" />
215+
</el-card>
216+
217+
<el-form-item v-if="showReturnContent" label="返回内容" @click.prevent>
218+
<template #label>
219+
<div class="flex items-center gap-1">
220+
<span>返回内容</span>
221+
<el-tooltip content="开启后,该节点的输出会作为工作流的最终回复内容" effect="dark" placement="right">
222+
<MkIcon name="icon_help_outlined" class="cursor-help text-N600" />
223+
</el-tooltip>
224+
</div>
225+
</template>
226+
<el-switch v-model="formData.is_result" size="small" />
227+
</el-form-item>
228+
</el-form>
229+
</NodeContainer>
230+
</template>

0 commit comments

Comments
 (0)