Skip to content

Commit e84903f

Browse files
authored
fix: Tool Workflow Template Center (#4997)
1 parent 91bb453 commit e84903f

File tree

10 files changed

+622
-16
lines changed

10 files changed

+622
-16
lines changed

apps/tools/serializers/tool.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@ def to_tool_workflow(work_flow, update_tool_map):
758758
return work_flow
759759

760760
@staticmethod
761-
def to_tool(tool, workspace_id, user_id):
761+
def to_tool(tool, workspace_id, user_id, folder_id):
762762
return Tool(id=tool.get('id'),
763763
user_id=user_id,
764764
name=tool.get('name'),
@@ -769,11 +769,28 @@ def to_tool(tool, workspace_id, user_id):
769769
is_active=False if len((tool.get('init_field_list') or [])) > 0 else tool.get('is_active'),
770770
tool_type=tool.get('tool_type', 'CUSTOM') or 'CUSTOM',
771771
scope=ToolScope.SHARED if workspace_id == 'None' else ToolScope.WORKSPACE,
772-
folder_id='default' if workspace_id == 'None' else workspace_id,
772+
folder_id=folder_id if folder_id else 'default' if workspace_id == 'None' else workspace_id,
773773
workspace_id=workspace_id)
774774

775-
def import_workflow_tools(self, tool, workspace_id, user_id):
776-
tool_list = tool.get('tool_list') or []
775+
def import_workflow_tools(self, tool, workspace_id, user_id, folder_id, new_child_policy):
776+
"""
777+
778+
@param tool: 工具对象
779+
@param workspace_id: 工作空间id
780+
@param user_id: 用户id
781+
@param folder_id: 文件夹id
782+
@param new_child_policy: 子工具创建策略
783+
0: 不创建
784+
1: 对比创建: 如果存在就不创建 不存在则创建
785+
2: 全部创建
786+
@return:
787+
"""
788+
if new_child_policy == 0:
789+
tool_list = []
790+
elif new_child_policy == 1:
791+
tool_list = tool.get('tool_list') or []
792+
else:
793+
tool_list = [{**tool, 'id': str(uuid.uuid7())} for tool in tool.get('tool_list') or []]
777794
update_tool_map = {}
778795
if len(tool_list) > 0:
779796
tool_id_list = reduce(lambda x, y: [*x, *y],
@@ -800,19 +817,31 @@ def import_workflow_tools(self, tool, workspace_id, user_id):
800817
tool.get('work_flow'),
801818
update_tool_map,
802819
)
803-
tool_model_list = [self.to_tool(tool, workspace_id, user_id) for tool in tool_list]
820+
QuerySet(ToolWorkflow).update_or_create(tool_id=tool.get('id'),
821+
create_defaults={'id': uuid.uuid7(),
822+
'tool_id': tool.get('id'),
823+
"workspace_id": workspace_id,
824+
'work_flow': work_flow, },
825+
defaults={
826+
'tool_id': tool.get('id'),
827+
'workspace_id': workspace_id,
828+
'work_flow': work_flow
829+
})
830+
tool_model_list = [self.to_tool(tool, workspace_id, user_id, folder_id) for tool in tool_list]
804831
workflow_tool_model_list = [{'tool_id': t.get('id'), 'workflow': self.to_tool_workflow(
805832
t.get('work_flow'),
806833
update_tool_map,
807834
)} for t in tool_list if tool.get('tool_type') == ToolType.WORKFLOW]
808-
workflow_tool_model_list.append({'tool_id': tool.get('id'), 'workflow': work_flow})
835+
809836
existing_records = QuerySet(ToolWorkflow).filter(
810837
tool_id__in=[wt.get('tool_id') for wt in workflow_tool_model_list],
811838
workspace_id=workspace_id)
839+
812840
existing_map = {
813841
record.tool_id: record
814842
for record in existing_records
815843
}
844+
816845
QuerySet(ToolWorkflow).bulk_create(
817846
[ToolWorkflow(work_flow=wt.get('workflow'), workspace_id=workspace_id,
818847
tool_id=wt.get('tool_id')) for wt in
@@ -826,6 +855,21 @@ def import_workflow_tools(self, tool, workspace_id, user_id):
826855
'auth_target_type': AuthTargetType.TOOL.value
827856
}).auth_resource_batch([t.id for t in tool_model_list])
828857

858+
def update_template_workflow(self, tool_id: str):
859+
self.is_valid(raise_exception=True)
860+
tool_instance_bytes = self.data.get('file').read()
861+
try:
862+
tool_instance = RestrictedUnpickler(io.BytesIO(tool_instance_bytes)).load()
863+
except Exception as e:
864+
raise AppApiException(1001, _("Unsupported file format"))
865+
tool = tool_instance.tool
866+
tool['id'] = tool_id
867+
folder_id = self.data.get('folder_id')
868+
self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'),
869+
user_id=self.data.get('user_id'),
870+
folder_id=folder_id, new_child_policy=2)
871+
return True
872+
829873
@transaction.atomic
830874
def import_(self, scope=ToolScope.WORKSPACE, name=None):
831875
self.is_valid()
@@ -871,7 +915,8 @@ def import_(self, scope=ToolScope.WORKSPACE, name=None):
871915
tool_model.save()
872916
if tool.get('tool_type') == ToolType.WORKFLOW:
873917
tool['id'] = tool_id
874-
self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'), user_id=user_id)
918+
self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'), user_id=user_id,
919+
folder_id=folder_id, new_child_policy=1)
875920
# 自动授权给创建者
876921
UserResourcePermissionSerializer(data={
877922
'workspace_id': self.data.get('workspace_id'),

apps/tools/serializers/tool_workflow.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
"""
99
import asyncio
1010
import json
11+
import os
1112
# coding=utf-8
1213
import pickle
14+
import tempfile
15+
import zipfile
1316
from functools import reduce
1417
from typing import Dict, List
1518

@@ -39,7 +42,7 @@
3942
from system_manage.models import AuthTargetType
4043
from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
4144
from tools.models import Tool, ToolScope, ToolWorkflow, ToolWorkflowVersion
42-
from tools.serializers.tool import ToolExportModelSerializer
45+
from tools.serializers.tool import ToolExportModelSerializer, ToolSerializer
4346
from users.models import User
4447

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

192197
try:
193198
requests.get(template_instance.get('downloadCallbackUrl'), timeout=5)
@@ -235,3 +240,55 @@ def get_mcp_servers(self, instance, with_valid=True):
235240
}
236241
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
237242
return tools
243+
244+
245+
class StoreToolWorkflow(serializers.Serializer):
246+
user_id = serializers.UUIDField(required=True, label=_("User ID"))
247+
name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)
248+
249+
def get_appstore_templates(self):
250+
self.is_valid(raise_exception=True)
251+
# 下载zip文件
252+
try:
253+
res = requests.get('https://apps-assets.fit2cloud.com/stable/maxkb.json.zip', timeout=5)
254+
res.raise_for_status()
255+
# 创建临时文件保存zip
256+
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as temp_zip:
257+
temp_zip.write(res.content)
258+
temp_zip_path = temp_zip.name
259+
260+
try:
261+
# 解压zip文件
262+
with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref:
263+
# 获取zip中的第一个文件(假设只有一个json文件)
264+
json_filename = zip_ref.namelist()[0]
265+
json_content = zip_ref.read(json_filename)
266+
267+
# 将json转换为字典
268+
tool_store = json.loads(json_content.decode('utf-8'))
269+
tag_dict = {tag['name']: tag['key'] for tag in tool_store['additionalProperties']['tags']}
270+
filter_apps = []
271+
for tool in tool_store['apps']:
272+
if self.data.get('name', '') != '':
273+
if self.data.get('name').lower() not in tool.get('name', '').lower():
274+
continue
275+
if not tool['downloadUrl'].endswith('.tool') or not [tag_dict[tag] for tag in
276+
tool.get('tags')].__contains__(
277+
'workflow_template'):
278+
continue
279+
versions = tool.get('versions', [])
280+
tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else ''
281+
tool['version'] = next(
282+
(version.get('name') for version in versions if
283+
version.get('downloadUrl') == tool['downloadUrl']),
284+
)
285+
filter_apps.append(tool)
286+
287+
tool_store['apps'] = filter_apps
288+
return tool_store
289+
finally:
290+
# 清理临时文件
291+
os.unlink(temp_zip_path)
292+
except Exception as e:
293+
maxkb_logger.error(f"fetch appstore tools error: {e}")
294+
return {'apps': [], 'additionalProperties': {'tags': []}}

apps/tools/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
urlpatterns = [
88
path('workspace/internal/tool', views.ToolView.InternalTool.as_view()),
99
path('workspace/store/tool', views.ToolView.StoreTool.as_view()),
10+
path('workspace/store/tool_workflow_template', views.StoreToolWorkflowView.as_view()),
1011
path('workspace/<str:workspace_id>/tool', views.ToolView.as_view()),
1112
path('workspace/<str:workspace_id>/tool/workflow', views.ToolWorkflowView.as_view()),
1213
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),

apps/tools/views/tool_workflow.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
from common.result import result, DefaultResultSerializer
1414
from knowledge.api.knowledge_workflow import KnowledgeWorkflowApi
1515
from knowledge.serializers.knowledge_workflow import KnowledgeWorkflowSerializer
16+
from tools.api.tool import GetInternalToolAPI
1617
from tools.api.tool_workflow import ToolWorkflowApi, ToolWorkflowExportApi, ToolWorkflowImportApi
17-
from tools.serializers.tool_workflow import ToolWorkflowSerializer, ToolWorkflowMcpSerializer
18+
from tools.serializers.tool_workflow import ToolWorkflowSerializer, ToolWorkflowMcpSerializer, StoreToolWorkflow
1819
from tools.views import get_tool_operation_object
1920

2021

@@ -187,3 +188,21 @@ def post(self, request: Request, workspace_id, tool_id: str):
187188
data={'mcp_servers': request.query_params.get('mcp_servers'), 'workspace_id': workspace_id,
188189
'user_id': request.user.id,
189190
'tool_id': tool_id}).get_mcp_servers(request.data))
191+
192+
193+
class StoreToolWorkflowView(APIView):
194+
authentication_classes = [TokenAuth]
195+
196+
@extend_schema(
197+
methods=['GET'],
198+
description=_("Get Appstore tools"),
199+
summary=_("Get Appstore tools"),
200+
operation_id=_("Get Appstore tools"), # type: ignore
201+
responses=GetInternalToolAPI.get_response(),
202+
tags=[_("Tool")] # type: ignore
203+
)
204+
def get(self, request: Request):
205+
return result.success(StoreToolWorkflow(data={
206+
'user_id': request.user.id,
207+
'name': request.query_params.get('name', ''),
208+
}).get_appstore_templates())

ui/src/api/tool/store.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ const getStoreKBList: (param?: any, loading?: Ref<boolean>) => Promise<Result<an
3838
) => {
3939
return get('/workspace/store/knowledge_template', param, loading)
4040
}
41+
const getStoreToolWorkflowList: (param?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
42+
param,
43+
loading,
44+
) => {
45+
return get('/workspace/store/tool_workflow_template', param, loading)
46+
}
4147

4248
const getStoreAppList: (param?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
4349
param,
@@ -73,6 +79,7 @@ export default {
7379
getStoreToolList,
7480
getStoreKBList,
7581
getStoreAppList,
82+
getStoreToolWorkflowList,
7683
addInternalTool,
77-
addStoreTool
84+
addStoreTool,
7885
}

ui/src/views/tool-workflow/index.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ import permissionMap from '@/permission'
174174
import { WorkflowMode } from '@/enums/application'
175175
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
176176
import { toolBaseNode, toolStartNode } from '@/workflow/common/data'
177-
import TemplateStoreDialog from '@/views/knowledge/template-store/TemplateStoreDialog.vue'
177+
import TemplateStoreDialog from '@/views/tool-workflow/template-store/TemplateStoreDialog.vue'
178178
import DebugDrawer from './debug-drawer/DebugDrawer.vue'
179179
provide('getResourceDetail', () => detail)
180180
provide('workflowMode', WorkflowMode.Tool)
@@ -472,7 +472,7 @@ function getDetail() {
472472
.then((res: any) => {
473473
detail.value = res.data
474474
saveTime.value = res.data?.update_time
475-
console.log(res.data)
475+
476476
if (!detail.value.work_flow || !('nodes' in detail.value.work_flow)) {
477477
detail.value.work_flow = { nodes: [toolBaseNode, toolStartNode] }
478478
}
@@ -481,6 +481,7 @@ function getDetail() {
481481
nextTick(() => {
482482
workflowRef.value?.render(detail.value.work_flow)
483483
cloneWorkFlow.value = getGraphData()
484+
workflowRef.value?.fitView()
484485
})
485486
})
486487
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<template>
2+
<el-drawer v-model="visibleInternalDesc" size="60%" :append-to-body="true">
3+
<template #header>
4+
<div class="flex align-center" style="margin-left: -8px">
5+
<el-button class="cursor mr-4" link @click.prevent="visibleInternalDesc = false">
6+
<el-icon :size="20">
7+
<Back />
8+
</el-icon>
9+
</el-button>
10+
<h4>{{ $t('common.detail') }}</h4>
11+
</div>
12+
</template>
13+
14+
<div>
15+
<div class="border-b">
16+
<div class="flex-between mb-24">
17+
<div class="title flex align-center">
18+
<el-avatar shape="square" :size="64" style="background: none">
19+
<img src="@/assets/knowledge/icon_basic_template.svg" alt="" />
20+
</el-avatar>
21+
<div class="ml-16">
22+
<h3 class="mb-8">{{ toolDetail.name }}</h3>
23+
<el-text type="info" v-if="toolDetail?.desc">
24+
{{ toolDetail.desc }}
25+
</el-text>
26+
<span
27+
class="color-secondary flex align-center mt-8"
28+
v-if="toolDetail?.downloads != undefined"
29+
>
30+
<AppIcon iconName="app-download" class="mr-4" />
31+
<span> {{ numberFormat(toolDetail.downloads || 0) }} </span>
32+
</span>
33+
</div>
34+
</div>
35+
<div @click.stop>
36+
<el-button type="primary" @click="addInternalTool(toolDetail)">
37+
{{ $t('common.use') }}
38+
</el-button>
39+
</div>
40+
</div>
41+
</div>
42+
<MdPreview
43+
ref="editorRef"
44+
editorId="preview-only"
45+
:modelValue="markdownContent"
46+
style="background: none"
47+
noImgZoomIn
48+
/>
49+
</div>
50+
</el-drawer>
51+
</template>
52+
53+
<script setup lang="ts">
54+
import { ref, watch } from 'vue'
55+
import { cloneDeep } from 'lodash'
56+
import { isAppIcon, numberFormat } from '@/utils/common'
57+
const emit = defineEmits(['refresh', 'addTool'])
58+
59+
const visibleInternalDesc = ref(false)
60+
const markdownContent = ref('')
61+
const toolDetail = ref<any>({})
62+
63+
watch(visibleInternalDesc, (bool) => {
64+
if (!bool) {
65+
markdownContent.value = ''
66+
}
67+
})
68+
69+
const open = (data: any, detail: any) => {
70+
toolDetail.value = detail
71+
if (data) {
72+
markdownContent.value = cloneDeep(data)
73+
}
74+
visibleInternalDesc.value = true
75+
}
76+
77+
const addInternalTool = (data: any) => {
78+
emit('addTool', data)
79+
visibleInternalDesc.value = false
80+
}
81+
82+
defineExpose({
83+
open,
84+
})
85+
</script>
86+
<style lang="scss"></style>

0 commit comments

Comments
 (0)