1212import os
1313import pickle
1414import re
15+ import tempfile
16+ import zipfile
1517from functools import reduce
1618from typing import Dict , List
1719
20+ import requests
1821import uuid_utils .compat as uuid
1922from django .core import validators
2023from django .db import models , transaction
3740from common .db .search import native_search , native_page_search
3841from common .exception .app_exception import AppApiException
3942from 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
4146from knowledge .models import Knowledge , KnowledgeScope
4247from knowledge .serializers .knowledge import KnowledgeSerializer , KnowledgeModelSerializer
4348from 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
624715class 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 ()
0 commit comments