Skip to content

Commit 37ef732

Browse files
committed
feat: add template store functionality and appstore integration
1 parent 9ed4959 commit 37ef732

File tree

10 files changed

+741
-28
lines changed

10 files changed

+741
-28
lines changed

apps/application/serializers/application.py

Lines changed: 194 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
import os
1313
import pickle
1414
import re
15+
import tempfile
16+
import zipfile
1517
from functools import reduce
1618
from typing import Dict, List
1719

20+
import requests
1821
import uuid_utils.compat as uuid
1922
from django.core import validators
2023
from django.db import models, transaction
@@ -37,7 +40,9 @@
3740
from common.db.search import native_search, native_page_search
3841
from common.exception.app_exception import AppApiException
3942
from common.field.common import UploadedFileField
40-
from common.utils.common import get_file_content, restricted_loads, generate_uuid, _remove_empty_lines
43+
from common.utils.common import get_file_content, restricted_loads, generate_uuid, _remove_empty_lines, \
44+
bytes_to_uploaded_file
45+
from common.utils.logger import maxkb_logger
4146
from knowledge.models import Knowledge, KnowledgeScope
4247
from knowledge.serializers.knowledge import KnowledgeSerializer, KnowledgeModelSerializer
4348
from maxkb.conf import PROJECT_DIR
@@ -182,28 +187,29 @@ def to_application_model(user_id: str, workspace_id: str, application: Dict):
182187
node.get('properties')['node_data']['desc'] = application.get('desc')
183188
node.get('properties')['node_data']['name'] = application.get('name')
184189
node.get('properties')['node_data']['prologue'] = application.get('prologue')
185-
return Application(id=uuid.uuid7(),
186-
name=application.get('name'),
187-
desc=application.get('desc'),
188-
workspace_id=workspace_id,
189-
folder_id=application.get('folder_id', application.get('workspace_id')),
190-
prologue="",
191-
dialogue_number=0,
192-
user_id=user_id, model_id=None,
193-
knowledge_setting={},
194-
model_setting={},
195-
problem_optimization=False,
196-
type=ApplicationTypeChoices.WORK_FLOW,
197-
stt_model_enable=application.get('stt_model_enable', False),
198-
stt_model_id=application.get('stt_model', None),
199-
tts_model_id=application.get('tts_model', None),
200-
tts_model_enable=application.get('tts_model_enable', False),
201-
tts_model_params_setting=application.get('tts_model_params_setting', {}),
202-
tts_type=application.get('tts_type', 'BROWSER'),
203-
file_upload_enable=application.get('file_upload_enable', False),
204-
file_upload_setting=application.get('file_upload_setting', {}),
205-
work_flow=default_workflow
206-
)
190+
return Application(
191+
id=uuid.uuid7(),
192+
name=application.get('name'),
193+
desc=application.get('desc'),
194+
workspace_id=workspace_id,
195+
folder_id=application.get('folder_id', application.get('workspace_id')),
196+
prologue="",
197+
dialogue_number=0,
198+
user_id=user_id, model_id=None,
199+
knowledge_setting={},
200+
model_setting={},
201+
problem_optimization=False,
202+
type=ApplicationTypeChoices.WORK_FLOW,
203+
stt_model_enable=application.get('stt_model_enable', False),
204+
stt_model_id=application.get('stt_model', None),
205+
tts_model_id=application.get('tts_model', None),
206+
tts_model_enable=application.get('tts_model_enable', False),
207+
tts_model_params_setting=application.get('tts_model_params_setting', {}),
208+
tts_type=application.get('tts_type', 'BROWSER'),
209+
file_upload_enable=application.get('file_upload_enable', False),
210+
file_upload_setting=application.get('file_upload_setting', {}),
211+
work_flow=default_workflow
212+
)
207213

208214
class SimplateRequest(serializers.Serializer):
209215
name = serializers.CharField(required=True, max_length=64, min_length=1,
@@ -459,8 +465,14 @@ class ApplicationSerializer(serializers.Serializer):
459465
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
460466
user_id = serializers.UUIDField(required=True, label=_("User ID"))
461467

468+
@transaction.atomic
462469
def insert(self, instance: Dict):
470+
work_flow_template = instance.get('work_flow_template')
463471
application_type = instance.get('type')
472+
473+
# 处理工作流模板安装逻辑
474+
if work_flow_template:
475+
return self.insert_template_workflow(instance)
464476
if 'WORK_FLOW' == application_type:
465477
r = self.insert_workflow(instance)
466478
else:
@@ -472,6 +484,35 @@ def insert(self, instance: Dict):
472484
}).auth_resource(str(r.get('id')))
473485
return r
474486

487+
def insert_template_workflow(self, instance: Dict):
488+
self.is_valid(raise_exception=True)
489+
work_flow_template = instance.get('work_flow_template')
490+
download_url = work_flow_template.get('downloadUrl')
491+
# 查找匹配的版本名称
492+
res = requests.get(download_url, timeout=5)
493+
app = ApplicationSerializer(
494+
data={'user_id': self.data.get('user_id'), 'workspace_id': self.data.get('workspace_id')}
495+
).import_({
496+
'file': bytes_to_uploaded_file(res.content, 'file.mk'),
497+
'folder_id': instance.get('folder_id', instance.get('workspace_id'))
498+
}, True)
499+
work_flow = app.get('work_flow')
500+
for node in work_flow.get('nodes', []):
501+
if node.get('type') == 'base-node':
502+
node_data = node.get('properties').get('node_data')
503+
node_data['name'] = instance.get('name')
504+
node_data['desc'] = instance.get('desc')
505+
QuerySet(Application).filter(id=app.get('id')).update(
506+
name=instance.get('name'),
507+
desc=instance.get('desc'),
508+
work_flow=work_flow
509+
)
510+
try:
511+
requests.get(work_flow_template.get('downloadCallbackUrl'), timeout=5)
512+
except Exception as e:
513+
maxkb_logger.error(f"callback appstore tool download error: {e}")
514+
return app
515+
475516
def insert_workflow(self, instance: Dict):
476517
self.is_valid(raise_exception=True)
477518
user_id = self.data.get('user_id')
@@ -565,7 +606,8 @@ def import_(self, instance: dict, is_import_tool, with_valid=True):
565606
'user_id': self.data.get('user_id'),
566607
'auth_target_type': AuthTargetType.TOOL.value
567608
}).auth_resource_batch([t.id for t in tool_model_list])
568-
return True
609+
610+
return ApplicationCreateSerializer.ApplicationResponse(application_model).data
569611

570612
@staticmethod
571613
def to_tool(tool, workspace_id, user_id):
@@ -620,6 +662,55 @@ def to_application(application, workspace_id, user_id, update_tool_map, folder_i
620662
file_upload_setting=application.get('file_upload_setting'),
621663
)
622664

665+
class StoreApplication(serializers.Serializer):
666+
user_id = serializers.UUIDField(required=True, label=_("User ID"))
667+
name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)
668+
669+
def get_appstore_templates(self):
670+
self.is_valid(raise_exception=True)
671+
# 下载zip文件
672+
try:
673+
res = requests.get('https://apps-assets.fit2cloud.com/stable/maxkb.json.zip', timeout=5)
674+
res.raise_for_status()
675+
# 创建临时文件保存zip
676+
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as temp_zip:
677+
temp_zip.write(res.content)
678+
temp_zip_path = temp_zip.name
679+
680+
try:
681+
# 解压zip文件
682+
with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref:
683+
# 获取zip中的第一个文件(假设只有一个json文件)
684+
json_filename = zip_ref.namelist()[0]
685+
json_content = zip_ref.read(json_filename)
686+
687+
# 将json转换为字典
688+
tool_store = json.loads(json_content.decode('utf-8'))
689+
tag_dict = {tag['name']: tag['key'] for tag in tool_store['additionalProperties']['tags']}
690+
filter_apps = []
691+
for tool in tool_store['apps']:
692+
if self.data.get('name', '') != '':
693+
if self.data.get('name').lower() not in tool.get('name', '').lower():
694+
continue
695+
if not tool['downloadUrl'].endswith('.mk'):
696+
continue
697+
versions = tool.get('versions', [])
698+
tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else ''
699+
tool['version'] = next(
700+
(version.get('name') for version in versions if
701+
version.get('downloadUrl') == tool['downloadUrl']),
702+
)
703+
filter_apps.append(tool)
704+
705+
tool_store['apps'] = filter_apps
706+
return tool_store
707+
finally:
708+
# 清理临时文件
709+
os.unlink(temp_zip_path)
710+
except Exception as e:
711+
maxkb_logger.error(f"fetch appstore tools error: {e}")
712+
return {'apps': [], 'additionalProperties': {'tags': []}}
713+
623714

624715
class TextToSpeechRequest(serializers.Serializer):
625716
text = serializers.CharField(required=True, label=_('Text'))
@@ -772,7 +863,7 @@ def publish(self, instance, with_valid=True):
772863
work_flow_version.save()
773864
access_token = hashlib.md5(
774865
str(uuid.uuid7()).encode()).hexdigest()[
775-
8:24]
866+
8:24]
776867
application_access_token = QuerySet(ApplicationAccessToken).filter(
777868
application_id=application.id).first()
778869
if application_access_token is None:
@@ -828,6 +919,10 @@ def edit(self, instance: Dict, with_valid=True):
828919
application_id = self.data.get("application_id")
829920

830921
application = QuerySet(Application).get(id=application_id)
922+
# 处理工作流模板逻辑
923+
if 'work_flow_template' in instance:
924+
return self.update_template_workflow(instance, application)
925+
831926
if instance.get('model_id') is None or len(instance.get('model_id')) == 0:
832927
application.model_id = None
833928
else:
@@ -880,6 +975,80 @@ def edit(self, instance: Dict, with_valid=True):
880975
self.save_application_knowledge_mapping(application_knowledge_id_list, knowledge_id_list, application_id)
881976
return self.one(with_valid=False)
882977

978+
def update_template_workflow(self, instance: Dict, app: Application):
979+
self.is_valid(raise_exception=True)
980+
work_flow_template = instance.get('work_flow_template')
981+
download_url = work_flow_template.get('downloadUrl')
982+
# 查找匹配的版本名称
983+
res = requests.get(download_url, timeout=5)
984+
try:
985+
mk_instance = restricted_loads(res.content)
986+
except Exception as e:
987+
raise AppApiException(1001, _("Unsupported file format"))
988+
application = mk_instance.application
989+
tool_list = mk_instance.get_tool_list()
990+
update_tool_map = {}
991+
if len(tool_list) > 0:
992+
tool_id_list = reduce(lambda x, y: [*x, *y],
993+
[[tool.get('id'), generate_uuid((tool.get('id') + app.workspace_id or ''))]
994+
for tool
995+
in
996+
tool_list], [])
997+
# 存在的工具列表
998+
exits_tool_id_list = [str(tool.id) for tool in
999+
QuerySet(Tool).filter(id__in=tool_id_list, workspace_id=app.workspace_id)]
1000+
# 需要更新的工具集合
1001+
update_tool_map = {tool.get('id'): generate_uuid((tool.get('id') + app.workspace_id or '')) for tool
1002+
in
1003+
tool_list if
1004+
not exits_tool_id_list.__contains__(
1005+
tool.get('id'))}
1006+
1007+
tool_list = [{**tool, 'id': update_tool_map.get(tool.get('id'))} for tool in tool_list if
1008+
not exits_tool_id_list.__contains__(
1009+
tool.get('id')) and not exits_tool_id_list.__contains__(
1010+
generate_uuid((tool.get('id') + app.workspace_id or '')))]
1011+
1012+
tool_model_list = [self.to_tool(f, app.workspace_id, self.data.get('user_id')) for f in tool_list]
1013+
work_flow = application.get('work_flow')
1014+
for node in work_flow.get('nodes', []):
1015+
hand_node(node, update_tool_map)
1016+
if node.get('type') == 'loop-node':
1017+
for n in node.get('properties', {}).get('node_data', {}).get('loop_body', {}).get('nodes', []):
1018+
hand_node(n, update_tool_map)
1019+
app.work_flow = work_flow
1020+
app.save()
1021+
1022+
if len(tool_model_list) > 0:
1023+
QuerySet(Tool).bulk_create(tool_model_list)
1024+
UserResourcePermissionSerializer(data={
1025+
'workspace_id': app.workspace_id,
1026+
'user_id': self.data.get('user_id'),
1027+
'auth_target_type': AuthTargetType.TOOL.value
1028+
}).auth_resource_batch([t.id for t in tool_model_list])
1029+
try:
1030+
requests.get(work_flow_template.get('downloadCallbackUrl'), timeout=5)
1031+
except Exception as e:
1032+
maxkb_logger.error(f"callback appstore tool download error: {e}")
1033+
1034+
return self.one(with_valid=False)
1035+
1036+
@staticmethod
1037+
def to_tool(tool, workspace_id, user_id):
1038+
return Tool(
1039+
id=tool.get('id'),
1040+
user_id=user_id,
1041+
name=tool.get('name'),
1042+
code=tool.get('code'),
1043+
template_id=tool.get('template_id'),
1044+
input_field_list=tool.get('input_field_list'),
1045+
init_field_list=tool.get('init_field_list'),
1046+
is_active=False if len((tool.get('init_field_list') or [])) > 0 else tool.get('is_active'),
1047+
scope=ToolScope.WORKSPACE,
1048+
folder_id=workspace_id,
1049+
workspace_id=workspace_id
1050+
)
1051+
8831052
def one(self, with_valid=True):
8841053
if with_valid:
8851054
self.is_valid()

apps/application/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
app_name = 'application'
66
# @formatter:off
77
urlpatterns = [
8-
8+
path('workspace/store/application_template', views.ApplicationAPI.StoreApplication.as_view()),
99
path('workspace/<str:workspace_id>/application', views.ApplicationAPI.as_view(), name='application'),
1010
path('workspace/<str:workspace_id>/application/folder/<str:folder_id>/import', views.ApplicationAPI.Import.as_view()),
1111
path('workspace/<str:workspace_id>/application/<int:current_page>/<int:page_size>', views.ApplicationAPI.Page.as_view(), name='application_page'),

apps/application/views/application.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from common.auth.authentication import has_permissions, get_is_permissions
2424
from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
2525
from common.log.log import log
26+
from tools.api.tool import GetInternalToolAPI
2627

2728

2829
def get_application_operation_object(application_id):
@@ -251,6 +252,23 @@ def put(self, request: Request, workspace_id: str, application_id: str):
251252
data={'application_id': application_id, 'user_id': request.user.id,
252253
'workspace_id': workspace_id, }).publish(request.data))
253254

255+
class StoreApplication(APIView):
256+
authentication_classes = [TokenAuth]
257+
258+
@extend_schema(
259+
methods=['GET'],
260+
description=_("Get Appstore apps"),
261+
summary=_("Get Appstore apps"),
262+
operation_id=_("Get Appstore apps"), # type: ignore
263+
responses=GetInternalToolAPI.get_response(),
264+
tags=[_("Application")] # type: ignore
265+
)
266+
def get(self, request: Request):
267+
return result.success(ApplicationSerializer.StoreApplication(data={
268+
'user_id': request.user.id,
269+
'name': request.query_params.get('name', ''),
270+
}).get_appstore_templates())
271+
254272

255273
class McpServers(APIView):
256274
authentication_classes = [TokenAuth]

ui/src/api/type/application.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface ApplicationFormType {
3737
application_enable?: boolean
3838
application_ids?: string[]
3939
mcp_output_enable?: boolean
40+
work_flow_template?: any
4041
}
4142

4243
interface Chunk {

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
</el-button>
2727
</div>
2828
<div v-else>
29+
<el-button
30+
class="ml-8"
31+
v-if="permissionPrecise.edit(id)"
32+
@click="openTemplateStoreDialog()"
33+
>
34+
<AppIcon iconName="app-template-center" class="mr-4" />
35+
{{ $t('workflow.setting.templateCenter') }}
36+
</el-button>
2937
<el-button @click="showPopover = !showPopover">
3038
<AppIcon iconName="app-add-outlined" class="mr-4" />
3139
{{ $t('workflow.setting.addComponent') }}
@@ -137,6 +145,12 @@
137145
v-click-outside="clickoutsideHistory"
138146
@refreshVersion="refreshVersion"
139147
/>
148+
<TemplateStoreDialog
149+
ref="templateStoreDialogRef"
150+
:api-type="apiType"
151+
source="work_flow"
152+
@refresh="getDetail"
153+
/>
140154
</div>
141155
</template>
142156
<script setup lang="ts">
@@ -159,6 +173,7 @@ import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/dat
159173
import permissionMap from '@/permission'
160174
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
161175
import { WorkflowMode } from '@/enums/application'
176+
import TemplateStoreDialog from "@/views/application/template-store/TemplateStoreDialog.vue";
162177
provide('getResourceDetail', () => detail)
163178
provide('workflowMode', WorkflowMode.Application)
164179
provide('loopWorkflowMode', WorkflowMode.ApplicationLoop)
@@ -635,6 +650,11 @@ const closeInterval = () => {
635650
}
636651
}
637652
653+
const templateStoreDialogRef = ref()
654+
function openTemplateStoreDialog() {
655+
templateStoreDialogRef.value?.open()
656+
}
657+
638658
onMounted(() => {
639659
getDetail()
640660
const workflowAutoSave = localStorage.getItem('workflowAutoSave')

0 commit comments

Comments
 (0)