Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions ui/src/workflow-canvas/config/node-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,27 @@ export const toolLibNode = {
properties: { stepName: '自定义工具', config: { fields: [{ label: '结果', value: 'result' }] } },
}

/**
* 工作流工具配置数据
*/
export const toolWorkflowLibNode = {
type: WorkflowNodeType.ToolWorkflowLib,
text: '工作流工具',
label: '工作流工具',
height: 170,
properties: { stepName: '工作流工具', config: { fields: [] } },
}


/* 智能体节点 */
export const applicationNode = {
type: WorkflowNodeType.Application,
text: '智能体节点',
label: '智能体节点',
properties: { stepName: '智能体节点', config: { fields: [{ label: '结果', value: 'result' }] } },
}


export const loopStartNode = {
id: WorkflowNodeType.LoopStartNode,
type: WorkflowNodeType.LoopStartNode,
Expand Down Expand Up @@ -444,25 +465,6 @@ export const loopBreakNode = {
properties: { width: 600, stepName: 'Break', config: { fields: [] } },
}

/**
* 工作流工具配置数据
*/
export const toolWorkflowLibNode = {
type: WorkflowNodeType.ToolWorkflowLib,
text: '工作流工具',
label: '工作流工具',
height: 170,
properties: { stepName: '工作流工具', config: { fields: [] } },
}

export const applicationNode = {
type: WorkflowNodeType.Application,
text: '智能体节点',
label: '智能体节点',
height: 260,
properties: { stepName: '智能体节点', config: { fields: [{ label: '结果', value: 'result' }] } },
}

/* 文档分段 */
export const documentSplitNode = {
type: WorkflowNodeType.DocumentSplitNode,
Expand Down
12 changes: 12 additions & 0 deletions ui/src/workflow-canvas/icons/application-node-icon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
const props = defineProps<{
item: {
name: string
icon: string
}
size?: string | number
}>()
</script>
<template>
<ApplicationIcon :icon="props.item.icon" />
</template>
1 change: 0 additions & 1 deletion ui/src/workflow-canvas/nodes/ai-chat-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const model = getModel()
const store = useWorkflowStore(apiType)

const formRef = useTemplateRef<FormInstance>('formRef')
const modelCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('modelCascaderRef')
const imageCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('imageCascaderRef')
const promptGenerateDialogRef = useTemplateRef<InstanceType<typeof PromptGenerateDialog>>('promptGenerateDialogRef')
const reasoningSettingDialogRef = useTemplateRef<InstanceType<typeof ReasoningSettingDialog>>('reasoningSettingDialogRef')
Expand Down
78 changes: 44 additions & 34 deletions ui/src/workflow-canvas/nodes/application-node/index.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<script setup lang="ts">
import { computed, inject, onMounted, useTemplateRef } from 'vue'
import { QuestionFilled } from '@element-plus/icons-vue'
import { cloneDeep } from 'lodash'
import type { FormInstance } from 'element-plus'
import ApplicationApi from '@/api/admin/workspace/application/application'
import { APPLICATION_TYPE } from '@/api/enums'
import NodeCascader from '@/workflow-canvas/core/NodeCascader.vue'
import NodeContainer from '@/workflow-canvas/core/node-container/index.vue'
import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node'
import { WorkflowNodeType } from '@/workflow-canvas/types'
import { WorkflowMode, WorkflowNodeType } from '@/workflow-canvas/types'

defineOptions({ name: 'WorkflowApplicationNode' })

Expand Down Expand Up @@ -55,35 +54,45 @@ interface ReferencedApplicationProperties {

const uploadFields: UploadField[] = ['document', 'image', 'audio', 'video']
const getModel = inject('getModel') as () => WorkflowNodeModel
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

const formRef = useTemplateRef<FormInstance>('formRef')
const questionCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('questionCascaderRef')

const applicationNodeData = (model.properties.node_data ?? {}) as Partial<ApplicationNodeForm>
if (!Array.isArray(applicationNodeData.question_reference_address)) {
applicationNodeData.question_reference_address = [WorkflowNodeType.Start, 'question']
// 一次性补齐旧节点表单,保留已保存的参数和文件引用。
const defaultForm: ApplicationNodeForm = {
api_input_field_list: [],
user_input_field_list: [],
question_reference_address: [WorkflowNodeType.Start, 'question'],
is_result: false,
}
const savedForm = model.properties.node_data as Partial<ApplicationNodeForm> | undefined
model.properties.node_data = {
...defaultForm,
...savedForm,
api_input_field_list: Array.isArray(savedForm?.api_input_field_list) ? savedForm.api_input_field_list : defaultForm.api_input_field_list,
user_input_field_list: Array.isArray(savedForm?.user_input_field_list) ? savedForm.user_input_field_list : defaultForm.user_input_field_list,
question_reference_address: Array.isArray(savedForm?.question_reference_address)
? savedForm.question_reference_address
: defaultForm.question_reference_address,
is_result: savedForm?.is_result ?? defaultForm.is_result,
}
if (!Array.isArray(applicationNodeData.api_input_field_list)) applicationNodeData.api_input_field_list = []
if (!Array.isArray(applicationNodeData.user_input_field_list)) applicationNodeData.user_input_field_list = []
if (applicationNodeData.is_result === undefined) applicationNodeData.is_result = false
model.properties.node_data = applicationNodeData as ApplicationNodeForm

const formData = computed<ApplicationNodeForm>({
get: () => model.properties.node_data as ApplicationNodeForm,
set: (value) => (model.properties.node_data = value),
})

const showReturnContent = computed(() =>
[WorkflowMode.Application, WorkflowMode.ApplicationLoop, WorkflowMode.Tool, WorkflowMode.ToolLoop].includes(workflowMode),
)

function formatFieldLabel(label: FieldLabel) {
return typeof label === 'object' && label !== null ? (label.label ?? '') : label
}

function validate() {
return Promise.all([questionCascaderRef.value?.validate(), formRef.value?.validate()]).catch((error) =>
Promise.reject({ node: model, errMessage: error }),
)
}

// 根据关联智能体配置同步输入字段,按字段标识保留已选变量。
function syncUploadField(field: UploadField, enabled: boolean) {
const listKey: UploadListKey = `${field}_list`
if (enabled) {
Expand Down Expand Up @@ -152,6 +161,13 @@ function refreshApplicationFields() {
})
}

async function validate() {
return formRef.value?.validate().catch((error) =>
Promise.reject({ node: model, errMessage: error }),
)
}


onMounted(() => {
refreshApplicationFields()
model.validate = validate
Expand All @@ -160,17 +176,12 @@ onMounted(() => {

<template>
<NodeContainer :node-model="model">
<h6 class="mb-3">节点设置</h6>
<h6 class="mk-title-decoration mb-2">节点设置</h6>

<el-card shadow="never" class="card-never">
<div class="mk-gray-card">
<el-form ref="formRef" :model="formData" label-position="top" require-asterisk-position="right" @submit.prevent>
<el-form-item label="用户问题" prop="question_reference_address" :rules="{ message: '请选择用户问题', trigger: 'change', required: true }">
<NodeCascader
ref="questionCascaderRef"
v-model="formData.question_reference_address"
:node-model="model"
placeholder="请选择用户问题"
/>
<NodeCascader ref="questionCascaderRef" v-model="formData.question_reference_address" :node-model="model" placeholder="请选择用户问题" />
</el-form-item>

<el-form-item v-if="formData.document_list !== undefined" label="选择文档" prop="document_list">
Expand Down Expand Up @@ -209,18 +220,17 @@ onMounted(() => {
<NodeCascader v-model="field.value" :node-model="model" placeholder="请选择参数" />
</el-form-item>

<el-form-item label="返回内容" @click.prevent>
<template #label>
<div class="flex items-center gap-1">
<span>返回内容</span>
<el-tooltip content="开启后,该节点的输出会作为工作流的最终回复内容" effect="dark" placement="right">
<MkIcon :icon="QuestionFilled" class="cursor-help text-N600" />
</el-tooltip>
</div>
</template>
<!-- 返回内容 -->
<div v-if="showReturnContent" class="flex-between w-full">
<span class="flex items-center gap-1">
返回内容
<el-tooltip content="关闭后该节点的内容则不输出给用户。如果你想让用户看到该节点的输出内容,请打开开关。" placement="right">
<MkIcon name="icon_info_outlined" class="text-N600!" />
</el-tooltip>
</span>
<el-switch v-model="formData.is_result" size="small" />
</el-form-item>
</div>
</el-form>
</el-card>
</div>
</NodeContainer>
</template>
52 changes: 28 additions & 24 deletions ui/src/workflow-canvas/nodes/tool-lib-node/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, inject, onMounted, useTemplateRef } from 'vue'
import { QuestionFilled } from '@element-plus/icons-vue'
import { cloneDeep } from 'lodash'
import type { FormInstance } from 'element-plus'
import type { ToolInputField } from '@/api/types'
Expand Down Expand Up @@ -30,11 +29,17 @@ const model = getModel()

const formRef = useTemplateRef<FormInstance>('formRef')

// 新节点默认不返回内容,旧节点缺少开关时继续按末尾节点兼容。
const defaultForm: ToolLibNodeForm = {
input_field_list: [],
is_result: false,
}
const savedForm = model.properties.node_data as Partial<ToolLibNodeForm> | undefined
model.properties.node_data = {
...defaultForm,
...savedForm,
input_field_list: Array.isArray(savedForm?.input_field_list) ? savedForm.input_field_list : [],
is_result: savedForm ? savedForm.is_result : false,
input_field_list: Array.isArray(savedForm?.input_field_list) ? savedForm.input_field_list : defaultForm.input_field_list,
is_result: savedForm ? savedForm.is_result : defaultForm.is_result,
}

const formData = computed<ToolLibNodeForm>({
Expand All @@ -50,6 +55,7 @@ function validate() {
return formRef.value?.validate().catch((error) => Promise.reject({ node: model, errMessage: error })) ?? Promise.resolve()
}

// 工具参数按名称和来源合并,保留已有引用或自定义值。
function createInputField(field: ToolInputField, previousFields: ToolNodeInputField[]): ToolNodeInputField {
const previousField = previousFields.find((item) => item.name === field.name && item.source === field.source)
if (field.source === 'reference') {
Expand Down Expand Up @@ -94,11 +100,11 @@ onMounted(() => {

<template>
<NodeContainer :node-model="model">
<h6 class="mb-3">节点设置</h6>
<h6 class="mb-2">输入参数</h6>
<h6 class="mk-title-decoration mb-2">节点设置</h6>

<el-form ref="formRef" :model="formData" label-position="top" hide-required-asterisk @submit.prevent>
<el-card shadow="never" class="card-never mb-4" style="--el-card-padding: 12px">
<div class="mk-gray-card">
<el-form ref="formRef" :model="formData" label-position="top" hide-required-asterisk @submit.prevent>
<h6 class="mb-2">输入参数</h6>
<template v-if="formData.input_field_list.length">
<el-form-item
v-for="(field, index) in formData.input_field_list"
Expand All @@ -112,11 +118,10 @@ onMounted(() => {
>
<template #label>
<div class="flex w-full items-center gap-1">
<span class="max-w-40 truncate" :title="field.name">{{ field.name }}</span>
<span class="max-w-40 truncate" :class="{ 'mk-required': field.is_required }" :title="field.name">{{ field.name }}</span>
<el-tooltip v-if="field.desc" :content="field.desc" effect="dark" placement="right">
<MkIcon :icon="QuestionFilled" class="cursor-help text-N600" />
<MkIcon name="icon_info_outlined" class="text-N600!" />
</el-tooltip>
<span v-if="field.is_required" class="text-danger">*</span>
<el-tag size="small" type="info">{{ field.type }}</el-tag>
</div>
</template>
Expand All @@ -125,20 +130,19 @@ onMounted(() => {
<el-input v-else v-model="field.value" placeholder="请输入参数" />
</el-form-item>
</template>
<MkEmpty v-else :image-size="60" />
</el-card>

<el-form-item v-if="showReturnContent" label="返回内容" @click.prevent>
<template #label>
<div class="flex items-center gap-1">
<span>返回内容</span>
<el-tooltip content="开启后,该节点的输出会作为工作流的最终回复内容" effect="dark" placement="right">
<MkIcon :icon="QuestionFilled" class="cursor-help text-N600" />
<MkEmpty v-else :image-size="60" class="mb-4" />

<!-- 返回内容 -->
<div v-if="showReturnContent" class="flex-between w-full">
<span class="flex items-center gap-1">
返回内容
<el-tooltip content="关闭后该节点的内容则不输出给用户。如果你想让用户看到该节点的输出内容,请打开开关。" placement="right">
<MkIcon name="icon_info_outlined" class="text-N600!" />
</el-tooltip>
</div>
</template>
<el-switch v-model="formData.is_result" size="small" />
</el-form-item>
</el-form>
</span>
<el-switch v-model="formData.is_result" size="small" />
</div>
</el-form>
</div>
</NodeContainer>
</template>
Loading