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
59 changes: 52 additions & 7 deletions apps/tools/serializers/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ def to_tool_workflow(work_flow, update_tool_map):
return work_flow

@staticmethod
def to_tool(tool, workspace_id, user_id):
def to_tool(tool, workspace_id, user_id, folder_id):
return Tool(id=tool.get('id'),
user_id=user_id,
name=tool.get('name'),
Expand All @@ -769,11 +769,28 @@ def to_tool(tool, workspace_id, user_id):
is_active=False if len((tool.get('init_field_list') or [])) > 0 else tool.get('is_active'),
tool_type=tool.get('tool_type', 'CUSTOM') or 'CUSTOM',
scope=ToolScope.SHARED if workspace_id == 'None' else ToolScope.WORKSPACE,
folder_id='default' if workspace_id == 'None' else workspace_id,
folder_id=folder_id if folder_id else 'default' if workspace_id == 'None' else workspace_id,
workspace_id=workspace_id)

def import_workflow_tools(self, tool, workspace_id, user_id):
tool_list = tool.get('tool_list') or []
def import_workflow_tools(self, tool, workspace_id, user_id, folder_id, new_child_policy):
"""

@param tool: 工具对象
@param workspace_id: 工作空间id
@param user_id: 用户id
@param folder_id: 文件夹id
@param new_child_policy: 子工具创建策略
0: 不创建
1: 对比创建: 如果存在就不创建 不存在则创建
2: 全部创建
@return:
"""
if new_child_policy == 0:
tool_list = []
elif new_child_policy == 1:
tool_list = tool.get('tool_list') or []
else:
tool_list = [{**tool, 'id': str(uuid.uuid7())} for tool in tool.get('tool_list') or []]
update_tool_map = {}
if len(tool_list) > 0:
tool_id_list = reduce(lambda x, y: [*x, *y],
Expand All @@ -800,19 +817,31 @@ def import_workflow_tools(self, tool, workspace_id, user_id):
tool.get('work_flow'),
update_tool_map,
)
tool_model_list = [self.to_tool(tool, workspace_id, user_id) for tool in tool_list]
QuerySet(ToolWorkflow).update_or_create(tool_id=tool.get('id'),
create_defaults={'id': uuid.uuid7(),
'tool_id': tool.get('id'),
"workspace_id": workspace_id,
'work_flow': work_flow, },
defaults={
'tool_id': tool.get('id'),
'workspace_id': workspace_id,
'work_flow': work_flow
})
tool_model_list = [self.to_tool(tool, workspace_id, user_id, folder_id) for tool in tool_list]
workflow_tool_model_list = [{'tool_id': t.get('id'), 'workflow': self.to_tool_workflow(
t.get('work_flow'),
update_tool_map,
)} for t in tool_list if tool.get('tool_type') == ToolType.WORKFLOW]
workflow_tool_model_list.append({'tool_id': tool.get('id'), 'workflow': work_flow})

existing_records = QuerySet(ToolWorkflow).filter(
tool_id__in=[wt.get('tool_id') for wt in workflow_tool_model_list],
workspace_id=workspace_id)

existing_map = {
record.tool_id: record
for record in existing_records
}

QuerySet(ToolWorkflow).bulk_create(
[ToolWorkflow(work_flow=wt.get('workflow'), workspace_id=workspace_id,
tool_id=wt.get('tool_id')) for wt in
Expand All @@ -826,6 +855,21 @@ def import_workflow_tools(self, tool, workspace_id, user_id):
'auth_target_type': AuthTargetType.TOOL.value
}).auth_resource_batch([t.id for t in tool_model_list])

def update_template_workflow(self, tool_id: str):
self.is_valid(raise_exception=True)
tool_instance_bytes = self.data.get('file').read()
try:
tool_instance = RestrictedUnpickler(io.BytesIO(tool_instance_bytes)).load()
except Exception as e:
raise AppApiException(1001, _("Unsupported file format"))
tool = tool_instance.tool
tool['id'] = tool_id
folder_id = self.data.get('folder_id')
self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'),
user_id=self.data.get('user_id'),
folder_id=folder_id, new_child_policy=2)
return True

@transaction.atomic
def import_(self, scope=ToolScope.WORKSPACE, name=None):
self.is_valid()
Expand Down Expand Up @@ -871,7 +915,8 @@ def import_(self, scope=ToolScope.WORKSPACE, name=None):
tool_model.save()
if tool.get('tool_type') == ToolType.WORKFLOW:
tool['id'] = tool_id
self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'), user_id=user_id)
self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'), user_id=user_id,
folder_id=folder_id, new_child_policy=1)
# 自动授权给创建者
UserResourcePermissionSerializer(data={
'workspace_id': self.data.get('workspace_id'),
Expand Down
65 changes: 61 additions & 4 deletions apps/tools/serializers/tool_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
"""
import asyncio
import json
import os
# coding=utf-8
import pickle
import tempfile
import zipfile
from functools import reduce
from typing import Dict, List

Expand Down Expand Up @@ -39,7 +42,7 @@
from system_manage.models import AuthTargetType
from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
from tools.models import Tool, ToolScope, ToolWorkflow, ToolWorkflowVersion
from tools.serializers.tool import ToolExportModelSerializer
from tools.serializers.tool import ToolExportModelSerializer, ToolSerializer
from users.models import User

tool_executor = ToolExecutor()
Expand Down Expand Up @@ -183,11 +186,13 @@ def edit(self, instance: Dict):
download_url = template_instance.get('downloadUrl')
# 查找匹配的版本名称
res = requests.get(download_url, timeout=5)
ToolWorkflowSerializer.Import(data={
tool = QuerySet(Tool).filter(id=self.data.get("tool_id")).first()
ToolSerializer.Import(data={
'user_id': self.data.get('user_id'),
'workspace_id': self.data.get('workspace_id'),
'tool_id': str(self.data.get('tool_id')),
}).import_({'file': bytes_to_uploaded_file(res.content, 'file.tool')}, is_import_tool=False)
'folder_id': tool.folder_id,
'file': bytes_to_uploaded_file(res.content, 'file.tool')
}).update_template_workflow(str(self.data.get('tool_id')))

try:
requests.get(template_instance.get('downloadCallbackUrl'), timeout=5)
Expand Down Expand Up @@ -235,3 +240,55 @@ def get_mcp_servers(self, instance, with_valid=True):
}
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
return tools


class StoreToolWorkflow(serializers.Serializer):
user_id = serializers.UUIDField(required=True, label=_("User ID"))
name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)

def get_appstore_templates(self):
self.is_valid(raise_exception=True)
# 下载zip文件
try:
res = requests.get('https://apps-assets.fit2cloud.com/stable/maxkb.json.zip', timeout=5)
res.raise_for_status()
# 创建临时文件保存zip
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as temp_zip:
temp_zip.write(res.content)
temp_zip_path = temp_zip.name

try:
# 解压zip文件
with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref:
# 获取zip中的第一个文件(假设只有一个json文件)
json_filename = zip_ref.namelist()[0]
json_content = zip_ref.read(json_filename)

# 将json转换为字典
tool_store = json.loads(json_content.decode('utf-8'))
tag_dict = {tag['name']: tag['key'] for tag in tool_store['additionalProperties']['tags']}
filter_apps = []
for tool in tool_store['apps']:
if self.data.get('name', '') != '':
if self.data.get('name').lower() not in tool.get('name', '').lower():
continue
if not tool['downloadUrl'].endswith('.tool') or not [tag_dict[tag] for tag in
tool.get('tags')].__contains__(
'workflow_template'):
continue
versions = tool.get('versions', [])
tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else ''
tool['version'] = next(
(version.get('name') for version in versions if
version.get('downloadUrl') == tool['downloadUrl']),
)
filter_apps.append(tool)

tool_store['apps'] = filter_apps
return tool_store
finally:
# 清理临时文件
os.unlink(temp_zip_path)
except Exception as e:
maxkb_logger.error(f"fetch appstore tools error: {e}")
return {'apps': [], 'additionalProperties': {'tags': []}}
1 change: 1 addition & 0 deletions apps/tools/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
urlpatterns = [
path('workspace/internal/tool', views.ToolView.InternalTool.as_view()),
path('workspace/store/tool', views.ToolView.StoreTool.as_view()),
path('workspace/store/tool_workflow_template', views.StoreToolWorkflowView.as_view()),
path('workspace/<str:workspace_id>/tool', views.ToolView.as_view()),
path('workspace/<str:workspace_id>/tool/workflow', views.ToolWorkflowView.as_view()),
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
Expand Down
21 changes: 20 additions & 1 deletion apps/tools/views/tool_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from common.result import result, DefaultResultSerializer
from knowledge.api.knowledge_workflow import KnowledgeWorkflowApi
from knowledge.serializers.knowledge_workflow import KnowledgeWorkflowSerializer
from tools.api.tool import GetInternalToolAPI
from tools.api.tool_workflow import ToolWorkflowApi, ToolWorkflowExportApi, ToolWorkflowImportApi
from tools.serializers.tool_workflow import ToolWorkflowSerializer, ToolWorkflowMcpSerializer
from tools.serializers.tool_workflow import ToolWorkflowSerializer, ToolWorkflowMcpSerializer, StoreToolWorkflow
from tools.views import get_tool_operation_object


Expand Down Expand Up @@ -187,3 +188,21 @@ def post(self, request: Request, workspace_id, tool_id: str):
data={'mcp_servers': request.query_params.get('mcp_servers'), 'workspace_id': workspace_id,
'user_id': request.user.id,
'tool_id': tool_id}).get_mcp_servers(request.data))


class StoreToolWorkflowView(APIView):
authentication_classes = [TokenAuth]

@extend_schema(
methods=['GET'],
description=_("Get Appstore tools"),
summary=_("Get Appstore tools"),
operation_id=_("Get Appstore tools"), # type: ignore
responses=GetInternalToolAPI.get_response(),
tags=[_("Tool")] # type: ignore
)
def get(self, request: Request):
return result.success(StoreToolWorkflow(data={
'user_id': request.user.id,
'name': request.query_params.get('name', ''),
}).get_appstore_templates())
9 changes: 8 additions & 1 deletion ui/src/api/tool/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ const getStoreKBList: (param?: any, loading?: Ref<boolean>) => Promise<Result<an
) => {
return get('/workspace/store/knowledge_template', param, loading)
}
const getStoreToolWorkflowList: (param?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
param,
loading,
) => {
return get('/workspace/store/tool_workflow_template', param, loading)
}

const getStoreAppList: (param?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
param,
Expand Down Expand Up @@ -73,6 +79,7 @@ export default {
getStoreToolList,
getStoreKBList,
getStoreAppList,
getStoreToolWorkflowList,
addInternalTool,
addStoreTool
addStoreTool,
}
5 changes: 3 additions & 2 deletions ui/src/views/tool-workflow/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ import permissionMap from '@/permission'
import { WorkflowMode } from '@/enums/application'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { toolBaseNode, toolStartNode } from '@/workflow/common/data'
import TemplateStoreDialog from '@/views/knowledge/template-store/TemplateStoreDialog.vue'
import TemplateStoreDialog from '@/views/tool-workflow/template-store/TemplateStoreDialog.vue'
import DebugDrawer from './debug-drawer/DebugDrawer.vue'
provide('getResourceDetail', () => detail)
provide('workflowMode', WorkflowMode.Tool)
Expand Down Expand Up @@ -472,7 +472,7 @@ function getDetail() {
.then((res: any) => {
detail.value = res.data
saveTime.value = res.data?.update_time
console.log(res.data)

if (!detail.value.work_flow || !('nodes' in detail.value.work_flow)) {
detail.value.work_flow = { nodes: [toolBaseNode, toolStartNode] }
}
Expand All @@ -481,6 +481,7 @@ function getDetail() {
nextTick(() => {
workflowRef.value?.render(detail.value.work_flow)
cloneWorkFlow.value = getGraphData()
workflowRef.value?.fitView()
})
})
}
Expand Down
86 changes: 86 additions & 0 deletions ui/src/views/tool-workflow/template-store/InternalDescDrawer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<el-drawer v-model="visibleInternalDesc" size="60%" :append-to-body="true">
<template #header>
<div class="flex align-center" style="margin-left: -8px">
<el-button class="cursor mr-4" link @click.prevent="visibleInternalDesc = false">
<el-icon :size="20">
<Back />
</el-icon>
</el-button>
<h4>{{ $t('common.detail') }}</h4>
</div>
</template>

<div>
<div class="border-b">
<div class="flex-between mb-24">
<div class="title flex align-center">
<el-avatar shape="square" :size="64" style="background: none">
<img src="@/assets/knowledge/icon_basic_template.svg" alt="" />
</el-avatar>
<div class="ml-16">
<h3 class="mb-8">{{ toolDetail.name }}</h3>
<el-text type="info" v-if="toolDetail?.desc">
{{ toolDetail.desc }}
</el-text>
<span
class="color-secondary flex align-center mt-8"
v-if="toolDetail?.downloads != undefined"
>
<AppIcon iconName="app-download" class="mr-4" />
<span> {{ numberFormat(toolDetail.downloads || 0) }} </span>
</span>
</div>
</div>
<div @click.stop>
<el-button type="primary" @click="addInternalTool(toolDetail)">
{{ $t('common.use') }}
</el-button>
</div>
</div>
</div>
<MdPreview
ref="editorRef"
editorId="preview-only"
:modelValue="markdownContent"
style="background: none"
noImgZoomIn
/>
</div>
</el-drawer>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { cloneDeep } from 'lodash'
import { isAppIcon, numberFormat } from '@/utils/common'
const emit = defineEmits(['refresh', 'addTool'])

const visibleInternalDesc = ref(false)
const markdownContent = ref('')
const toolDetail = ref<any>({})

watch(visibleInternalDesc, (bool) => {
if (!bool) {
markdownContent.value = ''
}
})

const open = (data: any, detail: any) => {
toolDetail.value = detail
if (data) {
markdownContent.value = cloneDeep(data)
}
visibleInternalDesc.value = true
}

const addInternalTool = (data: any) => {
emit('addTool', data)
visibleInternalDesc.value = false
}

defineExpose({
open,
})
</script>
<style lang="scss"></style>
Loading
Loading