feat: knowledge workflow

This commit is contained in:
shaohuzhang1 2025-11-04 18:00:31 +08:00 committed by CaptainB
parent c632a09f1f
commit 45b32bf405
13 changed files with 577 additions and 19 deletions

View File

@ -6,6 +6,7 @@ export enum SearchMode {
export enum WorkflowType {
Base = 'base-node',
KnowledgeBase = 'knowledge-base-node',
Start = 'start-node',
AiChat = 'ai-chat-node',
SearchKnowledge = 'search-knowledge-node',

View File

@ -140,7 +140,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed, nextTick, provide } from 'vue'
import { ref, onBeforeMount, onBeforeUnmount, computed, nextTick, provide } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { Action } from 'element-plus'
import Workflow from '@/workflow/index.vue'
@ -159,6 +159,7 @@ import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/dat
import permissionMap from '@/permission'
import { WorkflowMode } from '@/enums/application'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { knowledgeBaseNode } from '@/workflow/common/data'
provide('getResourceDetail', () => detail)
provide('workflowMode', WorkflowMode.Knowledge)
provide('loopWorkflowMode', WorkflowMode.KnowledgeLoop)
@ -423,18 +424,16 @@ function getDetail() {
loadSharedApi({ type: 'knowledge', systemType: apiType.value })
.getKnowledgeDetail(id)
.then((res: any) => {
let workspace = res.data?.work_flow
if (!workspace) {
workspace = {}
}
detail.value = res.data
detail.value.stt_model_id = res.data.stt_model
detail.value.tts_model_id = res.data.tts_model
detail.value.tts_type = res.data.tts_type
saveTime.value = res.data?.update_time
if (!detail.value.work_flow || !('nodes' in detail.value.work_flow)) {
detail.value.work_flow = { nodes: [knowledgeBaseNode] }
}
detail.value.work_flow?.nodes
?.filter((v: any) => v.id === 'base-node')
?.filter((v: any) => v.id === 'knowledge-base-node')
.map((v: any) => {
apiInputParams.value = v.properties.api_input_field_list
? v.properties.api_input_field_list.map((v: any) => {
@ -526,7 +525,7 @@ const closeInterval = () => {
}
}
onMounted(() => {
onBeforeMount(() => {
getDetail()
const workflowAutoSave = localStorage.getItem('workflowAutoSave')
isSave.value = workflowAutoSave === 'true' ? true : false

View File

@ -345,9 +345,12 @@ const nodeFields = computed(() => {
})
function showOperate(type: string) {
return ![WorkflowType.Start, WorkflowType.Base, WorkflowType.LoopStartNode.toString()].includes(
type,
)
return ![
WorkflowType.Start,
WorkflowType.Base,
WorkflowType.KnowledgeBase,
WorkflowType.LoopStartNode.toString(),
].includes(type)
}
const openNodeMenu = (anchorValue: any) => {
showAnchor.value = true

View File

@ -107,10 +107,11 @@ class AppNode extends HtmlResize.view {
(pre, next) => [...pre, ...next],
[],
)
const start_node_field_list = (
this.props.graphModel.getNodeModelById('start-node') ||
this.props.graphModel.getNodeModelById('loop-start-node')
).get_node_field_list()
const start_node_field_list =
(
this.props.graphModel.getNodeModelById('start-node') ||
this.props.graphModel.getNodeModelById('loop-start-node')
)?.get_node_field_list() || []
return [...start_node_field_list, ...result]
}
@ -414,7 +415,7 @@ class AppNodeModel extends HtmlResize.model {
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
const anchors: any = []
if (this.type !== WorkflowType.Base) {
if (![WorkflowType.Base as string, WorkflowType.KnowledgeBase as string].includes(this.type)) {
if (![WorkflowType.Start, WorkflowType.LoopStartNode.toString()].includes(this.type)) {
anchors.push({
x: x - width / 2 + 10,

View File

@ -57,6 +57,28 @@ export const baseNode = {
user_input_field_list: [],
},
}
export const knowledgeBaseNode = {
id: WorkflowType.KnowledgeBase,
type: WorkflowType.KnowledgeBase,
x: 360,
y: 2761.3875,
text: '',
properties: {
height: 728.375,
stepName: t('views.applicationWorkflow.nodes.baseNode.label'),
input_field_list: [],
node_data: {
name: '',
desc: '',
prologue: t('views.application.form.defaultPrologue'),
tts_type: 'BROWSER',
},
config: {},
showNode: true,
user_input_config: { title: t('chat.userInput') },
user_input_field_list: [],
},
}
/**
*
* type nodes
@ -880,6 +902,7 @@ export const nodeDict: any = {
[WorkflowType.VideoUnderstandNode]: videoUnderstandNode,
[WorkflowType.ParameterExtractionNode]: parameterExtractionNode,
[WorkflowType.VariableAggregationNode]: variableAggregationNode,
[WorkflowType.KnowledgeBase]: knowledgeBaseNode,
}
export function isWorkFlow(type: string | undefined) {

View File

@ -0,0 +1,6 @@
<template>
<el-avatar shape="square" style="background: #ff8800">
<img src="@/assets/workflow/icon_hi.svg" style="width: 75%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -78,7 +78,6 @@ const renderGraphData = (data?: any) => {
strokeWidth: 1,
},
})
lf.value.graphModel.get = 'sdasdaad'
lf.value.on('graph:rendered', () => {
flowId.value = lf.value.graphModel.flowId
})

View File

@ -15,8 +15,9 @@ class BaseModel extends AppNodeModel {
return 600
}
}
export default {
type: 'base-node',
type: 'knowledge-base-node',
model: BaseModel,
view: BaseNode
view: BaseNode,
}

View File

@ -0,0 +1,168 @@
<template>
<el-dialog
:title="
isEdit
? $t('common.param.editParam')
: $t('common.param.addParam')
"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<DynamicsFormConstructor
v-model="currentRow"
label-position="top"
require-asterisk-position="right"
:input_type_list="inputTypeList"
ref="DynamicsFormConstructorRef"
></DynamicsFormConstructor>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { cloneDeep } from 'lodash'
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
import type { FormField } from '@/components/dynamics-form/type'
import _ from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const DynamicsFormConstructorRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const currentItem = ref<FormField | any>()
const check_field = (field_list: Array<string>, obj: any) => {
return field_list.every((field) => _.get(obj, field, undefined) !== undefined)
}
const currentRow = computed(() => {
if (currentItem.value) {
const row = currentItem.value
switch (row.type) {
case 'input':
if (check_field(['field', 'input_type', 'label', 'required', 'attrs'], currentItem.value)) {
return currentItem.value
}
return {
attrs: row.attrs || { maxlength: 200, minlength: 0 },
field: row.field || row.variable,
input_type: 'TextInput',
label: row.label || row.name,
default_value: row.default_value,
required: row.required != undefined ? row.required : row.is_required
}
case 'select':
if (
check_field(
['field', 'input_type', 'label', 'required', 'option_list'],
currentItem.value
)
) {
return currentItem.value
}
return {
attrs: row.attrs || {},
field: row.field || row.variable,
input_type: 'SingleSelect',
label: row.label || row.name,
default_value: row.default_value,
required: row.required != undefined ? row.required : row.is_required,
option_list: row.option_list
? row.option_list
: row.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
if (
check_field(
[
'field',
'input_type',
'label',
'required',
'attrs.format',
'attrs.value-format',
'attrs.type'
],
currentItem.value
)
) {
return currentItem.value
}
return {
field: row.field || row.variable,
input_type: 'DatePicker',
label: row.label || row.name,
default_value: row.default_value,
required: row.required != undefined ? row.required : row.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
return currentItem.value
}
} else {
return { input_type: 'TextInput', required: false, attrs: { maxlength: 200, minlength: 0 }, show_default_value: true }
}
})
const currentIndex = ref(null)
const inputTypeList = ref([
{ label: t('dynamicsForm.input_type_list.TextInput'), value: 'TextInputConstructor' },
{ label: t('dynamicsForm.input_type_list.PasswordInput'), value: 'PasswordInputConstructor' },
{ label: t('dynamicsForm.input_type_list.SingleSelect'), value: 'SingleSelectConstructor' },
{ label: t('dynamicsForm.input_type_list.MultiSelect'), value: 'MultiSelectConstructor' },
{ label: t('dynamicsForm.input_type_list.RadioCard'), value: 'RadioCardConstructor' },
{ label: t('dynamicsForm.input_type_list.DatePicker'), value: 'DatePickerConstructor' },
{ label: t('dynamicsForm.input_type_list.SwitchInput'), value: 'SwitchInputConstructor' },
])
const dialogVisible = ref<boolean>(false)
const open = (row: any, index: any) => {
dialogVisible.value = true
if (row) {
isEdit.value = true
currentItem.value = cloneDeep(row)
currentIndex.value = index
} else {
currentItem.value = null
}
}
const close = () => {
dialogVisible.value = false
isEdit.value = false
currentIndex.value = null
currentItem.value = null as any
}
const submit = async () => {
const formEl = DynamicsFormConstructorRef.value
if (!formEl) return
await formEl.validate().then(() => {
emit('refresh', formEl?.getData(), currentIndex.value)
isEdit.value = false
currentItem.value = null as any
currentIndex.value = null
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,222 @@
<template>
<div class="flex-between mb-16">
<h5 class="break-all ellipsis lighter" style="max-width: 80%" :title="inputFieldConfig.title">
{{ inputFieldConfig.title }}
</h5>
<div>
<el-button type="primary" link @click="openChangeTitleDialog">
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
<span class="ml-4">
<el-button link type="primary" @click="openAddDialog()">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ $t('common.add') }}
</el-button>
</span>
</div>
</div>
<el-table
v-if="props.nodeModel.properties.user_input_field_list?.length > 0"
:data="props.nodeModel.properties.user_input_field_list"
class="mb-16"
ref="tableRef"
row-key="field"
>
<el-table-column prop="field" :label="$t('dynamicsForm.paramForm.field.label')" width="95">
<template #default="{ row }">
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
</template>
</el-table-column>
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
<template #default="{ row }">
<span v-if="row.label && row.label.input_type === 'TooltipLabel'">
<span :title="row.label.label" class="ellipsis-1">
{{ row.label.label }}
</span>
</span>
<span v-else>
<span :title="row.label" class="ellipsis-1">
{{ row.label }}
</span></span
>
</template>
</el-table-column>
<el-table-column :label="$t('dynamicsForm.paramForm.input_type.label')" width="95">
<template #default="{ row }">
<el-tag type="info" class="info-tag" v-if="row.input_type === 'TextInput'">{{
$t('dynamicsForm.input_type_list.TextInput')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'PasswordInput'">{{
$t('dynamicsForm.input_type_list.PasswordInput')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'Slider'">{{
$t('dynamicsForm.input_type_list.Slider')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SwitchInput'">{{
$t('dynamicsForm.input_type_list.SwitchInput')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SingleSelect'">{{
$t('dynamicsForm.input_type_list.SingleSelect')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'MultiSelect'">{{
$t('dynamicsForm.input_type_list.MultiSelect')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'RadioCard'">{{
$t('dynamicsForm.input_type_list.RadioCard')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'DatePicker'">{{
$t('dynamicsForm.input_type_list.DatePicker')
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
<template #default="{ row }">
<span :title="row.default_value" class="ellipsis-1">{{ getDefaultValue(row) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.required')">
<template #default="{ row }">
<div @click.stop>
<el-switch disabled size="small" v-model="row.required" />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
<AppIcon iconName="app-edit"></AppIcon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<UserFieldFormDialog ref="UserFieldFormDialogRef" @refresh="refreshFieldList" />
<UserInputTitleDialog ref="UserInputTitleDialogRef" @refresh="refreshFieldTitle" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { set, cloneDeep } from 'lodash'
import Sortable from 'sortablejs'
import UserFieldFormDialog from './UserFieldFormDialog.vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
import UserInputTitleDialog from '@/workflow/nodes/base-node/component/UserInputTitleDialog.vue'
const props = defineProps<{ nodeModel: any }>()
const tableRef = ref()
const UserFieldFormDialogRef = ref()
const UserInputTitleDialogRef = ref()
const inputFieldList = ref<any[]>([])
const inputFieldConfig = ref({ title: t('chat.userInput') })
function openAddDialog(data?: any, index?: any) {
UserFieldFormDialogRef.value.open(data, index)
}
function openChangeTitleDialog() {
UserInputTitleDialogRef.value.open(inputFieldConfig.value)
}
function deleteField(index: any) {
inputFieldList.value.splice(index, 1)
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
const fields = inputFieldList.value.map((item) => ({
label: item.label.label,
value: item.field,
}))
set(props.nodeModel.properties.config, 'fields', fields)
onDragHandle()
}
function refreshFieldList(data: any, index: any) {
for (let i = 0; i < inputFieldList.value.length; i++) {
if (inputFieldList.value[i].field === data.field && index !== i) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
return
}
}
if (index !== null) {
inputFieldList.value.splice(index, 1, data)
} else {
inputFieldList.value.push(data)
}
UserFieldFormDialogRef.value.close()
const fields = inputFieldList.value.map((item) => ({
label: item.label.label,
value: item.field,
}))
set(props.nodeModel.properties.config, 'fields', fields)
onDragHandle()
}
function refreshFieldTitle(data: any) {
inputFieldConfig.value = data
UserInputTitleDialogRef.value.close()
}
const getDefaultValue = (row: any) => {
if (row.input_type === 'PasswordInput') {
return '******'
}
if (row.default_value) {
const default_value = row.option_list
?.filter((v: any) => row.default_value.indexOf(v.value) > -1)
.map((v: any) => v.label)
.join(',')
if (default_value) {
return default_value
}
return row.default_value
}
if (row.default_value !== undefined) {
return row.default_value
}
}
function onDragHandle() {
if (!tableRef.value) return
// tbody DOM
const wrapper = tableRef.value.$el as HTMLElement
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
if (!tbody) return
// Sortable
Sortable.create(tbody as HTMLElement, {
animation: 150,
ghostClass: 'ghost-row',
onEnd: (evt) => {
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
//
const items = cloneDeep([...inputFieldList.value])
const [movedItem] = items.splice(evt.oldIndex, 1)
items.splice(evt.newIndex, 0, movedItem)
inputFieldList.value = items
},
})
}
onMounted(() => {
set(props.nodeModel.properties, 'user_input_field_list', inputFieldList)
if (props.nodeModel.properties.config) {
inputFieldConfig.value = props.nodeModel.properties.user_input_config
}
set(props.nodeModel.properties, 'user_input_config', inputFieldConfig)
onDragHandle()
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,83 @@
<template>
<el-dialog
:title="$t('common.setting')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
@submit.prevent
>
<el-form-item :label="$t('common.title')" prop="title">
<el-input
v-model="form.title"
maxlength="64"
show-word-limit
@blur="form.title = form.title.trim()"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const form = ref<any>({
title: t('chat.userInput'),
})
const rules = reactive({
title: [
{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' },
],
})
const dialogVisible = ref<boolean>(false)
const open = (row: any) => {
if (row) {
form.value = cloneDeep(row)
}
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value)
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,22 @@
import BaseNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class BaseNode extends AppNode {
constructor(props: any) {
super(props, BaseNodeVue)
}
}
class BaseModel extends AppNodeModel {
constructor(data: any, graphModel: any) {
super(data, graphModel)
}
get_width() {
return 600
}
}
export default {
type: 'knowledge-base-node',
model: BaseModel,
view: BaseNode,
}

View File

@ -0,0 +1,30 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<UserInputFieldTable ref="UserInputFieldTableFef" :node-model="nodeModel" />
</NodeContainer>
</template>
<script setup lang="ts">
import { set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted, inject } from 'vue'
import { t } from '@/locales'
import UserInputFieldTable from './component/UserInputFieldTable.vue'
const getResourceDetail = inject('getResourceDetail') as any
const props = defineProps<{ nodeModel: any }>()
const UserInputFieldTableFef = ref()
const baseNodeFormRef = ref<FormInstance>()
const resource = getResourceDetail()
onMounted(() => {})
</script>
<style lang="scss" scoped>
:deep(.el-form-item__label) {
display: block;
}
</style>