feat: tool datasource

This commit is contained in:
shaohuzhang1 2025-11-21 17:05:21 +08:00
parent 5922597775
commit ada109d7ca
11 changed files with 331 additions and 30 deletions

View File

@ -41,7 +41,8 @@ class FunctionLibNodeParamsSerializer(serializers.Serializer):
class IToolLibNode(INode):
type = 'tool-lib-node'
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP]
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE,
WorkflowMode.KNOWLEDGE_LOOP]
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return FunctionLibNodeParamsSerializer

View File

@ -138,7 +138,7 @@ class BaseToolLibNodeNode(IToolLibNode):
tool_lib = QuerySet(Tool).filter(id=tool_lib_id).first()
valid_function(tool_lib, workspace_id)
params = {
field.get('name'): convert_value(
field.get('name'): convert_value(
field.get('name'), field.get('value'), field.get('type'),
field.get('is_required'),
field.get('source'), self
@ -157,8 +157,12 @@ class BaseToolLibNodeNode(IToolLibNode):
all_params = init_params_default_value | json.loads(rsa_long_decrypt(tool_lib.init_params)) | params
else:
all_params = init_params_default_value | params
if self.node.properties.get('kind') == 'data-source':
all_params = {**all_params, **self.workflow_params.get('data_source')}
result = function_executor.exec_code(tool_lib.code, all_params)
return NodeResult({'result': result}, {}, _write_context=write_context)
return NodeResult({'result': result},
(self.workflow_manage.params.get('knowledge_base') or {}) if self.node.properties.get(
'kind') == 'data-source' else {}, _write_context=write_context)
def get_details(self, index: int, **kwargs):
return {

View File

@ -47,11 +47,12 @@ class ToolExecutor:
finally:
os.umask(old_mask)
def exec_code(self, code_str, keywords):
def exec_code(self, code_str, keywords, function_name=None):
self.validate_banned_keywords(code_str)
_id = str(uuid.uuid7())
success = '{"code":200,"msg":"成功","data":exec_result}'
err = '{"code":500,"msg":str(e),"data":None}'
action_function = f'({function_name !a}, locals_v.get({function_name !a}))' if function_name else 'locals_v.popitem()'
result_path = f'{self.sandbox_path}/result/{_id}.result'
python_paths = CONFIG.get_sandbox_python_package_paths().split(',')
_exec_code = f"""
@ -66,7 +67,7 @@ try:
keywords={keywords}
globals_v={'{}'}
exec({dedent(code_str)!a}, globals_v, locals_v)
f_name, f = locals_v.popitem()
f_name, f = {action_function}
for local in locals_v:
globals_v[local] = locals_v[local]
exec_result=f(**keywords)

View File

@ -1,5 +1,5 @@
# coding=utf-8
import json
from typing import Dict
import uuid_utils.compat as uuid
@ -13,13 +13,18 @@ from application.flow.i_step_node import KnowledgeWorkflowPostHandler
from application.flow.knowledge_workflow_manage import KnowledgeWorkflowManage
from application.flow.step_node import get_node
from common.exception.app_exception import AppApiException
from common.utils.rsa_util import rsa_long_decrypt
from common.utils.tool_code import ToolExecutor
from knowledge.models import KnowledgeScope, Knowledge, KnowledgeType, KnowledgeWorkflow
from knowledge.models.knowledge_action import KnowledgeAction, State
from knowledge.serializers.knowledge import KnowledgeModelSerializer
from maxkb.const import CONFIG
from system_manage.models import AuthTargetType
from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
from tools.models import Tool
tool_executor = ToolExecutor(CONFIG.get('SANDBOX'))
class KnowledgeWorkflowModelSerializer(serializers.ModelSerializer):
class Meta:
@ -62,20 +67,22 @@ class KnowledgeWorkflowActionSerializer(serializers.Serializer):
class KnowledgeWorkflowSerializer(serializers.Serializer):
class Form(serializers.Serializer):
class Datasource(serializers.Serializer):
type = serializers.CharField(required=True, label=_('type'))
id = serializers.CharField(required=True, label=_('type'))
node = serializers.DictField(required=True, label="")
params = serializers.DictField(required=True, label="")
function_name = serializers.CharField(required=True, label=_('function_name'))
def get_form_list(self):
def action(self):
self.is_valid(raise_exception=True)
if self.data.get('type') == 'local':
node = get_node(self.data.get('id'), WorkflowMode.KNOWLEDGE)
return node.get_form_list(self.data.get("node"))
return node.__getattribute__(node, self.data.get("function_name"))(**self.data.get("params"))
elif self.data.get('type') == 'tool':
tool = QuerySet(Tool).filter(id=self.data.get("id")).first()
# todo 调用工具数据源的函数获取表单列表
return None
init_params = json.loads(rsa_long_decrypt(tool.init_params))
return tool_executor.exec_code(tool.code, {**init_params, **self.data.get('params')},
self.data.get('function_name'))
class Create(serializers.Serializer):
user_id = serializers.UUIDField(required=True, label=_('user id'))

View File

@ -69,7 +69,8 @@ urlpatterns = [
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/problem/<int:current_page>/<int:page_size>', views.ProblemView.Page.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<int:current_page>/<int:page_size>', views.DocumentView.Page.as_view()),
path('workspace/<str:workspace_id>/knowledge/<int:current_page>/<int:page_size>', views.KnowledgeView.Page.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/form_list/<str:type>/<str:id>', views.KnowledgeWorkflowFormView.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/datasource/<str:type>/<str:id>/form_list', views.KnowledgeDatasourceFormListView.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/datasource/<str:type>/<str:id>/<str:function_name>', views.KnowledgeDatasourceView.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/action', views.KnowledgeWorkflowActionView.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/action/<str:knowledge_action_id>', views.KnowledgeWorkflowActionView.Operate.as_view())

View File

@ -15,12 +15,18 @@ from knowledge.serializers.common import get_knowledge_operation_object
from knowledge.serializers.knowledge_workflow import KnowledgeWorkflowSerializer, KnowledgeWorkflowActionSerializer
class KnowledgeWorkflowFormView(APIView):
class KnowledgeDatasourceFormListView(APIView):
authentication_classes = [TokenAuth]
def post(self, request: Request, workspace_id: str, knowledge_id: str, type: str, id: str):
return result.success(KnowledgeWorkflowSerializer.Form(
data={'type': type, 'id': id, 'node': request.data.get('node')}).get_form_list())
return result.success(KnowledgeWorkflowSerializer.Datasource(
data={'type': type, 'id': id, 'params': request.data, 'function_name': 'get_form_list'}).action())
class KnowledgeDatasourceView(APIView):
def post(self, request: Request, workspace_id: str, knowledge_id: str, type: str, id: str, function_name: str):
return result.success(KnowledgeWorkflowSerializer.Datasource(
data={'type': type, 'id': id, 'params': request.data, 'function_name': function_name}).action())
class KnowledgeWorkflowActionView(APIView):

View File

@ -328,7 +328,34 @@ const getKnowledgeWorkflowFormList: (
node,
loading,
) => {
return post(`${prefix.value}/${knowledge_id}/form_list/${type}/${id}`, { node }, {}, loading)
return post(
`${prefix.value}/${knowledge_id}/datasource/${type}/${id}/form_list`,
{ node },
{},
loading,
)
}
const getKnowledgeWorkflowDatasourceDetails: (
knowledge_id: string,
type: 'loacl' | 'tool',
id: string,
params: any,
function_name: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (
knowledge_id: string,
type: 'loacl' | 'tool',
id: string,
params,
function_name,
loading,
) => {
return post(
`${prefix.value}/${knowledge_id}/datasource/${type}/${id}/${function_name}`,
params,
{},
loading,
)
}
const workflowAction: (
knowledge_id: string,
@ -371,4 +398,5 @@ export default {
getKnowledgeWorkflowFormList,
workflowAction,
getWorkflowAction,
getKnowledgeWorkflowDatasourceDetails,
}

View File

@ -22,6 +22,15 @@ import { ref } from 'vue'
import type { Dict } from '@/api/type/common'
const damo_data: Array<FormField> = [
{
field: 'aaa',
input_type: 'Tree',
attrs: {
lazy: true,
url: '/workspace/${current_workspace_id}/knowledge/${current_knowledge_id}/datasource/tool/019aa0bb-552d-73a3-b0c6-1809eaedb139/get_file_list',
},
label: '',
},
{
field: 'aa',
input_type: 'LocalFileUpload',

View File

@ -0,0 +1,222 @@
<template>
<div>
<div class="update-info flex p-8-12 border-r-6 mb-16">
<div class="mt-4">
<AppIcon iconName="app-warning-colorful" style="font-size: 16px"></AppIcon>
</div>
<div class="ml-16 lighter">
<p>{{ $t('views.document.feishu.tip1') }}</p>
<p>{{ $t('views.document.feishu.tip2') }}</p>
</div>
</div>
<div class="card-never border-r-6 mb-16">
<el-checkbox
v-model="allCheck"
:label="$t('views.document.feishu.allCheck')"
size="large"
class="ml-24"
@change="handleAllCheckChange"
/>
</div>
<div style="height: calc(100vh - 450px)">
<el-scrollbar>
<el-tree
:data="option_list"
@check-change="change"
v-loading="loading"
style="width: 100%"
:props="propsData"
:load="loadNode"
:lazy="attrs.lazy"
show-checkbox
:node-key="valueField"
ref="treeRef"
>
<template #default="{ node, data }">
<div class="flex align-center lighter">
<img :src="data.icon" alt="" height="20" v-if="data.icon" />
<img
src="@/assets/fileType/file-icon.svg"
alt=""
height="20"
v-else-if="data.type === 'folder'"
/>
<img
src="@/assets/fileType/docx-icon.svg"
alt=""
height="22"
v-else-if="data.type === 'docx' || data.name.endsWith('.docx')"
/>
<img
src="@/assets/fileType/xlsx-icon.svg"
alt=""
height="22"
v-else-if="data.type === 'sheet' || data.name.endsWith('.xlsx')"
/>
<img
src="@/assets/fileType/xls-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('xls')"
/>
<img
src="@/assets/fileType/csv-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('csv')"
/>
<img
src="@/assets/fileType/pdf-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('.pdf')"
/>
<img
src="@/assets/fileType/html-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('.html')"
/>
<img
src="@/assets/fileType/txt-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('.txt')"
/>
<img
src="@/assets/fileType/zip-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('.zip')"
/>
<img
src="@/assets/fileType/md-icon.svg"
alt=""
height="22"
v-else-if="data.name.endsWith('.md')"
/>
<span class="ml-4">{{ node.label }}</span>
</div>
</template>
</el-tree></el-scrollbar
>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, useAttrs, nextTick, inject } from 'vue'
import type { FormField } from '@/components/dynamics-form/type'
import type Node from 'element-plus/es/components/tree/src/model/node'
import { get, post, put, del } from '@/request/index'
import { cloneDeep } from 'lodash'
import { formItemContextKey } from 'element-plus'
const elFormItem = inject(formItemContextKey, void 0)
const request = {
get,
post,
put,
del,
}
const allCheck = ref<boolean>(false)
const handleAllCheckChange = (checked: boolean) => {
if (checked) {
const nodes = Object.values(treeRef.value?.store.nodesMap || {}) as any[]
nodes.forEach((node) => {
if (!node.disabled) {
treeRef.value?.setChecked(node.data, true, false)
}
})
} else {
treeRef.value?.setCheckedKeys([])
}
}
interface Tree {
label: string
leaf?: boolean
type: string
value: string
disabled: boolean
icon?: string
}
const textField = computed(() => {
return props.formField.text_field ? props.formField.text_field : 'label'
})
const valueField = computed(() => {
return props.formField.value_field ? props.formField.value_field : 'value'
})
const childrenField = computed(() => {
return props.formField.childrenField ? props.formField.childrenField : 'children'
})
const option_list = computed(() => {
return props.formField.option_list ? props.formField.option_list : []
})
const propsData = computed(() => {
return {
label: textField,
children: childrenField,
isLeaf: (data: any) => data.leaf,
disabled: (data: any) => data.disabled,
}
})
const attrs = useAttrs() as any
const treeRef = ref<any>(null)
const request_call = new Function(
'request',
'extra',
'return request.post(extra.url,extra.body,{},extra.loading).then(extra.then);',
)
function renderTemplate(template: string, data: any) {
return template.replace(/\$\{(\w+)\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match
})
}
const loadNode = (node: Node, resolve: (nodeData: Tree[]) => void) => {
request_call(request, {
url: renderTemplate(attrs.url, props.otherParams),
body: { current_node: node.level == 0 ? undefined : node.data },
then: (res) => {
resolve(res.data)
res.data.forEach((childNode) => {
if (childNode.is_exist) {
treeRef.value?.setChecked(childNode.token, true, false)
}
})
},
loading: loading,
})
}
const props = withDefaults(defineProps<{ modelValue?: any; formField: FormField; otherParams }>(), {
modelValue: () => [],
})
const emit = defineEmits(['update:modelValue', 'change'])
const model_value = computed({
get: () => {
if (!props.modelValue) {
emit('update:modelValue', [])
}
return props.modelValue
},
set: (v: Array<any>) => {
emit('update:modelValue', v)
},
})
const change = () => {
model_value.value = cloneDeep(treeRef.value?.getCheckedNodes() || [])
nextTick(() => {
if (elFormItem?.validate) {
elFormItem.validate('change')
}
})
}
const loading = ref<boolean>(false)
</script>
<style lang="scss" scoped></style>

View File

@ -72,7 +72,7 @@
</el-scrollbar>
</el-tab-pane>
<!-- 工具 -->
<el-tab-pane :label="$t('views.tool.title')" name="tool">
<el-tab-pane :label="$t('views.tool.datasource.title', '数据源')" name="DATA_SOURCE_TOOL">
<LayoutContainer>
<template #left>
<div class="p-8">
@ -96,25 +96,27 @@
</el-scrollbar>
</LayoutContainer>
</el-tab-pane>
<!-- 应用 -->
<el-tab-pane :label="$t('views.application.title')" name="application">
<!-- 工具 -->
<el-tab-pane :label="$t('views.tool.title')" name="CUSTOM_TOOL">
<LayoutContainer>
<template #left>
<div class="p-8">
<folder-tree
:source="SourceTypeEnum.APPLICATION"
:data="applicationTreeData"
:source="SourceTypeEnum.TOOL"
:data="toolTreeData"
:currentNodeKey="folder.currentFolder?.id"
@handleNodeClick="folderClickHandle"
:shareTitle="$t('views.shared.shared_tool')"
:showShared="permissionPrecise['is_share']()"
:canOperation="false"
/>
</div>
</template>
<el-scrollbar height="450">
<NodeContent
:list="applicationList"
@clickNodes="(val: any) => clickNodes(applicationNode, val, 'application')"
@onmousedown="(val: any) => onmousedown(applicationNode, val, 'application')"
:list="toolList"
@clickNodes="(val: any) => clickNodes(toolLibNode, val, 'tool')"
@onmousedown="(val: any) => onmousedown(toolLibNode, val, 'tool')"
/>
</el-scrollbar>
</LayoutContainer>
@ -133,7 +135,7 @@ import NodeContent from './NodeContent.vue'
import { SourceTypeEnum } from '@/enums/common'
import permissionMap from '@/permission'
import { useRoute } from 'vue-router'
import { WorkflowMode } from '@/enums/application'
import { WorkflowKind, WorkflowMode } from '@/enums/application'
const workflowModel = inject('workflowMode') as WorkflowMode
const route = useRoute()
const { user, folder } = useStore()
@ -188,6 +190,9 @@ function clickNodes(item: any, data?: any, type?: string) {
if (data) {
item['properties']['stepName'] = data.name
if (type == 'tool') {
if (data.tool_type == 'DATA_SOURCE') {
item['properties'].kind = WorkflowKind.DataSource
}
item['properties']['node_data'] = {
...data,
tool_lib_id: data.id,
@ -214,6 +219,9 @@ function onmousedown(item: any, data?: any, type?: string) {
if (data) {
item['properties']['stepName'] = data.name
if (type == 'tool') {
if (data.tool_type == 'DATA_SOURCE') {
item['properties'].kind = WorkflowKind.DataSource
}
item['properties']['node_data'] = {
...data,
tool_lib_id: data.id,
@ -261,7 +269,7 @@ async function getToolList() {
systemType: 'workspace',
}).getToolList({
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
tool_type: 'CUSTOM',
tool_type: activeName.value == 'DATA_SOURCE_TOOL' ? 'DATA_SOURCE' : 'CUSTOM',
})
toolList.value = res.data?.tools || res.data || []
toolList.value = toolList.value?.filter((item: any) => item.is_active)
@ -300,7 +308,7 @@ function folderClickHandle(row: any) {
async function handleClick(val: string) {
console.log(val)
if (val === 'tool') {
if (['DATA_SOURCE_TOOL', 'CUSTOM_TOOL'].includes(val)) {
await getToolFolder()
getToolList()
} else if (val === 'application') {

View File

@ -7,6 +7,7 @@
ref="dynamicsFormRef"
label-position="top"
require-asterisk-position="right"
:other-params="{ current_workspace_id: workspace_id, current_knowledge_id: knowledge_id }"
>
<template #default>
<el-form-item prop="node_id" :rules="base_form_data_rule.node_id">
@ -37,6 +38,8 @@ import type { Dict } from '@/api/type/common'
import type { FormRules } from 'element-plus'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { useRoute } from 'vue-router'
import useStore from '@/stores'
const { user } = useStore()
const route = useRoute()
const apiType = computed(() => {
@ -49,7 +52,11 @@ const apiType = computed(() => {
const model_form_field = ref<Array<FormField>>([])
const props = defineProps<{
workflow: any
knowledge_id: string
}>()
const workspace_id = computed(() => {
return user.getWorkspaceId()
})
const loading = ref<boolean>(false)
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
const base_form_data = ref<{ node_id: string }>({ node_id: '' })
@ -73,10 +80,17 @@ const sourceChange = (node_id: string) => {
node_id = n
? [WorkflowType.DataSourceLocalNode, WorkflowType.DataSourceWebNode].includes(n.type)
? n.type
: node_id
: n.properties.node_data.tool_lib_id
: node_id
loadSharedApi({ type: 'knowledge', systemType: apiType.value })
.getKnowledgeWorkflowFormList(id, 'local', node_id, n)
.getKnowledgeWorkflowFormList(
id,
[WorkflowType.DataSourceLocalNode, WorkflowType.DataSourceWebNode].includes(n.type)
? 'local'
: 'tool',
node_id,
n,
)
.then((ok: any) => {
dynamicsFormRef.value?.render(ok.data)
})