mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 10:12:51 +00:00
feat: add template store functionality and appstore integration
This commit is contained in:
parent
9ed49599cf
commit
37ef7322cd
|
|
@ -12,9 +12,12 @@ import json
|
|||
import os
|
||||
import pickle
|
||||
import re
|
||||
import tempfile
|
||||
import zipfile
|
||||
from functools import reduce
|
||||
from typing import Dict, List
|
||||
|
||||
import requests
|
||||
import uuid_utils.compat as uuid
|
||||
from django.core import validators
|
||||
from django.db import models, transaction
|
||||
|
|
@ -37,7 +40,9 @@ from common.database_model_manage.database_model_manage import DatabaseModelMana
|
|||
from common.db.search import native_search, native_page_search
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.field.common import UploadedFileField
|
||||
from common.utils.common import get_file_content, restricted_loads, generate_uuid, _remove_empty_lines
|
||||
from common.utils.common import get_file_content, restricted_loads, generate_uuid, _remove_empty_lines, \
|
||||
bytes_to_uploaded_file
|
||||
from common.utils.logger import maxkb_logger
|
||||
from knowledge.models import Knowledge, KnowledgeScope
|
||||
from knowledge.serializers.knowledge import KnowledgeSerializer, KnowledgeModelSerializer
|
||||
from maxkb.conf import PROJECT_DIR
|
||||
|
|
@ -182,28 +187,29 @@ class ApplicationCreateSerializer(serializers.Serializer):
|
|||
node.get('properties')['node_data']['desc'] = application.get('desc')
|
||||
node.get('properties')['node_data']['name'] = application.get('name')
|
||||
node.get('properties')['node_data']['prologue'] = application.get('prologue')
|
||||
return Application(id=uuid.uuid7(),
|
||||
name=application.get('name'),
|
||||
desc=application.get('desc'),
|
||||
workspace_id=workspace_id,
|
||||
folder_id=application.get('folder_id', application.get('workspace_id')),
|
||||
prologue="",
|
||||
dialogue_number=0,
|
||||
user_id=user_id, model_id=None,
|
||||
knowledge_setting={},
|
||||
model_setting={},
|
||||
problem_optimization=False,
|
||||
type=ApplicationTypeChoices.WORK_FLOW,
|
||||
stt_model_enable=application.get('stt_model_enable', False),
|
||||
stt_model_id=application.get('stt_model', None),
|
||||
tts_model_id=application.get('tts_model', None),
|
||||
tts_model_enable=application.get('tts_model_enable', False),
|
||||
tts_model_params_setting=application.get('tts_model_params_setting', {}),
|
||||
tts_type=application.get('tts_type', 'BROWSER'),
|
||||
file_upload_enable=application.get('file_upload_enable', False),
|
||||
file_upload_setting=application.get('file_upload_setting', {}),
|
||||
work_flow=default_workflow
|
||||
)
|
||||
return Application(
|
||||
id=uuid.uuid7(),
|
||||
name=application.get('name'),
|
||||
desc=application.get('desc'),
|
||||
workspace_id=workspace_id,
|
||||
folder_id=application.get('folder_id', application.get('workspace_id')),
|
||||
prologue="",
|
||||
dialogue_number=0,
|
||||
user_id=user_id, model_id=None,
|
||||
knowledge_setting={},
|
||||
model_setting={},
|
||||
problem_optimization=False,
|
||||
type=ApplicationTypeChoices.WORK_FLOW,
|
||||
stt_model_enable=application.get('stt_model_enable', False),
|
||||
stt_model_id=application.get('stt_model', None),
|
||||
tts_model_id=application.get('tts_model', None),
|
||||
tts_model_enable=application.get('tts_model_enable', False),
|
||||
tts_model_params_setting=application.get('tts_model_params_setting', {}),
|
||||
tts_type=application.get('tts_type', 'BROWSER'),
|
||||
file_upload_enable=application.get('file_upload_enable', False),
|
||||
file_upload_setting=application.get('file_upload_setting', {}),
|
||||
work_flow=default_workflow
|
||||
)
|
||||
|
||||
class SimplateRequest(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, max_length=64, min_length=1,
|
||||
|
|
@ -459,8 +465,14 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
|
||||
user_id = serializers.UUIDField(required=True, label=_("User ID"))
|
||||
|
||||
@transaction.atomic
|
||||
def insert(self, instance: Dict):
|
||||
work_flow_template = instance.get('work_flow_template')
|
||||
application_type = instance.get('type')
|
||||
|
||||
# 处理工作流模板安装逻辑
|
||||
if work_flow_template:
|
||||
return self.insert_template_workflow(instance)
|
||||
if 'WORK_FLOW' == application_type:
|
||||
r = self.insert_workflow(instance)
|
||||
else:
|
||||
|
|
@ -472,6 +484,35 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
}).auth_resource(str(r.get('id')))
|
||||
return r
|
||||
|
||||
def insert_template_workflow(self, instance: Dict):
|
||||
self.is_valid(raise_exception=True)
|
||||
work_flow_template = instance.get('work_flow_template')
|
||||
download_url = work_flow_template.get('downloadUrl')
|
||||
# 查找匹配的版本名称
|
||||
res = requests.get(download_url, timeout=5)
|
||||
app = ApplicationSerializer(
|
||||
data={'user_id': self.data.get('user_id'), 'workspace_id': self.data.get('workspace_id')}
|
||||
).import_({
|
||||
'file': bytes_to_uploaded_file(res.content, 'file.mk'),
|
||||
'folder_id': instance.get('folder_id', instance.get('workspace_id'))
|
||||
}, True)
|
||||
work_flow = app.get('work_flow')
|
||||
for node in work_flow.get('nodes', []):
|
||||
if node.get('type') == 'base-node':
|
||||
node_data = node.get('properties').get('node_data')
|
||||
node_data['name'] = instance.get('name')
|
||||
node_data['desc'] = instance.get('desc')
|
||||
QuerySet(Application).filter(id=app.get('id')).update(
|
||||
name=instance.get('name'),
|
||||
desc=instance.get('desc'),
|
||||
work_flow=work_flow
|
||||
)
|
||||
try:
|
||||
requests.get(work_flow_template.get('downloadCallbackUrl'), timeout=5)
|
||||
except Exception as e:
|
||||
maxkb_logger.error(f"callback appstore tool download error: {e}")
|
||||
return app
|
||||
|
||||
def insert_workflow(self, instance: Dict):
|
||||
self.is_valid(raise_exception=True)
|
||||
user_id = self.data.get('user_id')
|
||||
|
|
@ -565,7 +606,8 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
'user_id': self.data.get('user_id'),
|
||||
'auth_target_type': AuthTargetType.TOOL.value
|
||||
}).auth_resource_batch([t.id for t in tool_model_list])
|
||||
return True
|
||||
|
||||
return ApplicationCreateSerializer.ApplicationResponse(application_model).data
|
||||
|
||||
@staticmethod
|
||||
def to_tool(tool, workspace_id, user_id):
|
||||
|
|
@ -620,6 +662,55 @@ class ApplicationSerializer(serializers.Serializer):
|
|||
file_upload_setting=application.get('file_upload_setting'),
|
||||
)
|
||||
|
||||
class StoreApplication(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('.mk'):
|
||||
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': []}}
|
||||
|
||||
|
||||
class TextToSpeechRequest(serializers.Serializer):
|
||||
text = serializers.CharField(required=True, label=_('Text'))
|
||||
|
|
@ -772,7 +863,7 @@ class ApplicationOperateSerializer(serializers.Serializer):
|
|||
work_flow_version.save()
|
||||
access_token = hashlib.md5(
|
||||
str(uuid.uuid7()).encode()).hexdigest()[
|
||||
8:24]
|
||||
8:24]
|
||||
application_access_token = QuerySet(ApplicationAccessToken).filter(
|
||||
application_id=application.id).first()
|
||||
if application_access_token is None:
|
||||
|
|
@ -828,6 +919,10 @@ class ApplicationOperateSerializer(serializers.Serializer):
|
|||
application_id = self.data.get("application_id")
|
||||
|
||||
application = QuerySet(Application).get(id=application_id)
|
||||
# 处理工作流模板逻辑
|
||||
if 'work_flow_template' in instance:
|
||||
return self.update_template_workflow(instance, application)
|
||||
|
||||
if instance.get('model_id') is None or len(instance.get('model_id')) == 0:
|
||||
application.model_id = None
|
||||
else:
|
||||
|
|
@ -880,6 +975,80 @@ class ApplicationOperateSerializer(serializers.Serializer):
|
|||
self.save_application_knowledge_mapping(application_knowledge_id_list, knowledge_id_list, application_id)
|
||||
return self.one(with_valid=False)
|
||||
|
||||
def update_template_workflow(self, instance: Dict, app: Application):
|
||||
self.is_valid(raise_exception=True)
|
||||
work_flow_template = instance.get('work_flow_template')
|
||||
download_url = work_flow_template.get('downloadUrl')
|
||||
# 查找匹配的版本名称
|
||||
res = requests.get(download_url, timeout=5)
|
||||
try:
|
||||
mk_instance = restricted_loads(res.content)
|
||||
except Exception as e:
|
||||
raise AppApiException(1001, _("Unsupported file format"))
|
||||
application = mk_instance.application
|
||||
tool_list = mk_instance.get_tool_list()
|
||||
update_tool_map = {}
|
||||
if len(tool_list) > 0:
|
||||
tool_id_list = reduce(lambda x, y: [*x, *y],
|
||||
[[tool.get('id'), generate_uuid((tool.get('id') + app.workspace_id or ''))]
|
||||
for tool
|
||||
in
|
||||
tool_list], [])
|
||||
# 存在的工具列表
|
||||
exits_tool_id_list = [str(tool.id) for tool in
|
||||
QuerySet(Tool).filter(id__in=tool_id_list, workspace_id=app.workspace_id)]
|
||||
# 需要更新的工具集合
|
||||
update_tool_map = {tool.get('id'): generate_uuid((tool.get('id') + app.workspace_id or '')) for tool
|
||||
in
|
||||
tool_list if
|
||||
not exits_tool_id_list.__contains__(
|
||||
tool.get('id'))}
|
||||
|
||||
tool_list = [{**tool, 'id': update_tool_map.get(tool.get('id'))} for tool in tool_list if
|
||||
not exits_tool_id_list.__contains__(
|
||||
tool.get('id')) and not exits_tool_id_list.__contains__(
|
||||
generate_uuid((tool.get('id') + app.workspace_id or '')))]
|
||||
|
||||
tool_model_list = [self.to_tool(f, app.workspace_id, self.data.get('user_id')) for f in tool_list]
|
||||
work_flow = application.get('work_flow')
|
||||
for node in work_flow.get('nodes', []):
|
||||
hand_node(node, update_tool_map)
|
||||
if node.get('type') == 'loop-node':
|
||||
for n in node.get('properties', {}).get('node_data', {}).get('loop_body', {}).get('nodes', []):
|
||||
hand_node(n, update_tool_map)
|
||||
app.work_flow = work_flow
|
||||
app.save()
|
||||
|
||||
if len(tool_model_list) > 0:
|
||||
QuerySet(Tool).bulk_create(tool_model_list)
|
||||
UserResourcePermissionSerializer(data={
|
||||
'workspace_id': app.workspace_id,
|
||||
'user_id': self.data.get('user_id'),
|
||||
'auth_target_type': AuthTargetType.TOOL.value
|
||||
}).auth_resource_batch([t.id for t in tool_model_list])
|
||||
try:
|
||||
requests.get(work_flow_template.get('downloadCallbackUrl'), timeout=5)
|
||||
except Exception as e:
|
||||
maxkb_logger.error(f"callback appstore tool download error: {e}")
|
||||
|
||||
return self.one(with_valid=False)
|
||||
|
||||
@staticmethod
|
||||
def to_tool(tool, workspace_id, user_id):
|
||||
return Tool(
|
||||
id=tool.get('id'),
|
||||
user_id=user_id,
|
||||
name=tool.get('name'),
|
||||
code=tool.get('code'),
|
||||
template_id=tool.get('template_id'),
|
||||
input_field_list=tool.get('input_field_list'),
|
||||
init_field_list=tool.get('init_field_list'),
|
||||
is_active=False if len((tool.get('init_field_list') or [])) > 0 else tool.get('is_active'),
|
||||
scope=ToolScope.WORKSPACE,
|
||||
folder_id=workspace_id,
|
||||
workspace_id=workspace_id
|
||||
)
|
||||
|
||||
def one(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from . import views
|
|||
app_name = 'application'
|
||||
# @formatter:off
|
||||
urlpatterns = [
|
||||
|
||||
path('workspace/store/application_template', views.ApplicationAPI.StoreApplication.as_view()),
|
||||
path('workspace/<str:workspace_id>/application', views.ApplicationAPI.as_view(), name='application'),
|
||||
path('workspace/<str:workspace_id>/application/folder/<str:folder_id>/import', views.ApplicationAPI.Import.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<int:current_page>/<int:page_size>', views.ApplicationAPI.Page.as_view(), name='application_page'),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from common.auth import TokenAuth
|
|||
from common.auth.authentication import has_permissions, get_is_permissions
|
||||
from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
|
||||
from common.log.log import log
|
||||
from tools.api.tool import GetInternalToolAPI
|
||||
|
||||
|
||||
def get_application_operation_object(application_id):
|
||||
|
|
@ -251,6 +252,23 @@ class ApplicationAPI(APIView):
|
|||
data={'application_id': application_id, 'user_id': request.user.id,
|
||||
'workspace_id': workspace_id, }).publish(request.data))
|
||||
|
||||
class StoreApplication(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@extend_schema(
|
||||
methods=['GET'],
|
||||
description=_("Get Appstore apps"),
|
||||
summary=_("Get Appstore apps"),
|
||||
operation_id=_("Get Appstore apps"), # type: ignore
|
||||
responses=GetInternalToolAPI.get_response(),
|
||||
tags=[_("Application")] # type: ignore
|
||||
)
|
||||
def get(self, request: Request):
|
||||
return result.success(ApplicationSerializer.StoreApplication(data={
|
||||
'user_id': request.user.id,
|
||||
'name': request.query_params.get('name', ''),
|
||||
}).get_appstore_templates())
|
||||
|
||||
|
||||
class McpServers(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ interface ApplicationFormType {
|
|||
application_enable?: boolean
|
||||
application_ids?: string[]
|
||||
mcp_output_enable?: boolean
|
||||
work_flow_template?: any
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@
|
|||
</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button
|
||||
class="ml-8"
|
||||
v-if="permissionPrecise.edit(id)"
|
||||
@click="openTemplateStoreDialog()"
|
||||
>
|
||||
<AppIcon iconName="app-template-center" class="mr-4" />
|
||||
{{ $t('workflow.setting.templateCenter') }}
|
||||
</el-button>
|
||||
<el-button @click="showPopover = !showPopover">
|
||||
<AppIcon iconName="app-add-outlined" class="mr-4" />
|
||||
{{ $t('workflow.setting.addComponent') }}
|
||||
|
|
@ -137,6 +145,12 @@
|
|||
v-click-outside="clickoutsideHistory"
|
||||
@refreshVersion="refreshVersion"
|
||||
/>
|
||||
<TemplateStoreDialog
|
||||
ref="templateStoreDialogRef"
|
||||
:api-type="apiType"
|
||||
source="work_flow"
|
||||
@refresh="getDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -159,6 +173,7 @@ import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/dat
|
|||
import permissionMap from '@/permission'
|
||||
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
||||
import { WorkflowMode } from '@/enums/application'
|
||||
import TemplateStoreDialog from "@/views/application/template-store/TemplateStoreDialog.vue";
|
||||
provide('getResourceDetail', () => detail)
|
||||
provide('workflowMode', WorkflowMode.Application)
|
||||
provide('loopWorkflowMode', WorkflowMode.ApplicationLoop)
|
||||
|
|
@ -635,6 +650,11 @@ const closeInterval = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const templateStoreDialogRef = ref()
|
||||
function openTemplateStoreDialog() {
|
||||
templateStoreDialogRef.value?.open()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
const workflowAutoSave = localStorage.getItem('workflowAutoSave')
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<el-form-item
|
||||
:label="$t('views.document.upload.template')"
|
||||
v-if="applicationForm.type === 'WORK_FLOW'"
|
||||
v-if="applicationForm.type === 'WORK_FLOW' && !work_flow_template"
|
||||
>
|
||||
<div class="w-full">
|
||||
<el-row :gutter="16">
|
||||
|
|
@ -126,6 +126,7 @@ const applicationFormRef = ref()
|
|||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const work_flow_template = ref()
|
||||
|
||||
const applicationForm = ref<ApplicationFormType>({
|
||||
name: '',
|
||||
|
|
@ -158,6 +159,7 @@ const applicationForm = ref<ApplicationFormType>({
|
|||
tts_model_enable: false,
|
||||
tts_type: 'BROWSER',
|
||||
type: 'SIMPLE',
|
||||
work_flow_template: undefined,
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules<ApplicationFormType>>({
|
||||
|
|
@ -217,10 +219,11 @@ watch(dialogVisible, (bool) => {
|
|||
}
|
||||
})
|
||||
|
||||
const open = (folder: string, type?: string) => {
|
||||
const open = (folder: string, type?: string, work_flow?: any) => {
|
||||
currentFolder.value = folder
|
||||
applicationForm.value.type = type || 'SIMPLE'
|
||||
dialogVisible.value = true
|
||||
work_flow_template.value = work_flow
|
||||
}
|
||||
|
||||
const submitHandle = async (formEl: FormInstance | undefined) => {
|
||||
|
|
@ -231,6 +234,9 @@ const submitHandle = async (formEl: FormInstance | undefined) => {
|
|||
workflowDefault.value.nodes[0].properties.node_data.desc = applicationForm.value.desc
|
||||
workflowDefault.value.nodes[0].properties.node_data.name = applicationForm.value.name
|
||||
applicationForm.value['work_flow'] = workflowDefault.value
|
||||
if (work_flow_template.value) {
|
||||
applicationForm.value['work_flow_template'] = work_flow_template.value
|
||||
}
|
||||
}
|
||||
loading.value = true
|
||||
applicationApi
|
||||
|
|
|
|||
|
|
@ -61,6 +61,14 @@
|
|||
<el-option :label="$t('common.status.unpublished')" value="unpublished" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button
|
||||
class="ml-8"
|
||||
v-if="permissionPrecise.create()"
|
||||
@click="openTemplateStoreDialog()"
|
||||
>
|
||||
<AppIcon iconName="app-template-center" class="mr-4" />
|
||||
{{ $t('workflow.setting.templateCenter') }}
|
||||
</el-button>
|
||||
<el-dropdown trigger="click" v-if="permissionPrecise.create()">
|
||||
<el-button type="primary" class="ml-8">
|
||||
{{ $t('common.create') }}
|
||||
|
|
@ -291,6 +299,7 @@
|
|||
:type="SourceTypeEnum.APPLICATION"
|
||||
ref="ResourceAuthorizationDrawerRef"
|
||||
/>
|
||||
<TemplateStoreDialog ref="templateStoreDialogRef" :api-type="apiType" @refresh="getList" />
|
||||
</LayoutContainer>
|
||||
</template>
|
||||
|
||||
|
|
@ -316,6 +325,7 @@ import WorkspaceApi from '@/api/workspace/workspace'
|
|||
import { hasPermission } from '@/utils/permission'
|
||||
import { ComplexPermission } from '@/utils/permission/type'
|
||||
import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/data'
|
||||
import TemplateStoreDialog from "@/views/application/template-store/TemplateStoreDialog.vue";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -685,6 +695,11 @@ function searchHandle() {
|
|||
getList()
|
||||
}
|
||||
|
||||
const templateStoreDialogRef = ref()
|
||||
function openTemplateStoreDialog() {
|
||||
templateStoreDialogRef.value?.open(folder.currentFolder.id)
|
||||
}
|
||||
|
||||
function getList() {
|
||||
const params: any = {
|
||||
folder_id: folder.currentFolder?.id || 'default',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<CardBox :title="props.tool.name" :description="props.tool.desc" class="cursor tool-card">
|
||||
<template #icon>
|
||||
<el-avatar shape="square" :size="32" style="background: none">
|
||||
<img src="@/assets/knowledge/icon_basic_template.svg" alt="" />
|
||||
</el-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<span :title="props.tool?.name" class="ellipsis"> {{ props.tool?.name }}</span>
|
||||
</template>
|
||||
<!-- <template #tag>
|
||||
<el-tag type="info" v-if="props.tool?.label === 'knowledge_template'" class="info-tag">
|
||||
{{ $t('知识库') }}
|
||||
</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-else>
|
||||
{{ $t('views.tool.title') }}
|
||||
</el-tag>
|
||||
</template> -->
|
||||
<!-- <template #subTitle>
|
||||
<el-text class="color-secondary lighter" size="small">
|
||||
{{ getSubTitle(props.tool) }}
|
||||
</el-text>
|
||||
</template> -->
|
||||
<template #footer>
|
||||
<span class="card-footer-left color-secondary flex align-center" v-if="props.tool?.downloads != undefined">
|
||||
<AppIcon iconName="app-download" class="mr-4" />
|
||||
<span> {{ numberFormat(props.tool.downloads || 0) }} </span>
|
||||
</span>
|
||||
|
||||
<div class="card-footer-operation mb-8" @click.stop>
|
||||
<el-button @click="emit('handleDetail')">
|
||||
{{ $t('common.detail') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="props.addLoading" @click="emit('handleAdd')">
|
||||
{{ $t('common.use') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isAppIcon, numberFormat, resetUrl } from '@/utils/common'
|
||||
|
||||
const props = defineProps<{
|
||||
tool: any
|
||||
getSubTitle: (v: any) => string
|
||||
addLoading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'handleAdd'): void
|
||||
(e: 'handleDetail'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tool-card {
|
||||
:deep(.card-footer) {
|
||||
& > div:first-of-type {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-footer-operation {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.card-footer-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-footer-operation {
|
||||
display: flex !important;
|
||||
|
||||
.el-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
width="1000"
|
||||
append-to-body
|
||||
class="tool-store-dialog"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<template #header="{ titleId }">
|
||||
<div class="dialog-header flex-between mb-8">
|
||||
<h4 :id="titleId" class="medium w-240 mr-8">
|
||||
{{ $t('workflow.setting.templateCenter') }}
|
||||
</h4>
|
||||
|
||||
<div class="flex align-center" style="margin-right: 28px">
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
:placeholder="$t('common.search')"
|
||||
prefix-icon="Search"
|
||||
class="w-240 mr-8"
|
||||
clearable
|
||||
@change="getList"
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <LayoutContainer v-loading="loading" :minLeftWidth="204">
|
||||
<template #left>
|
||||
<el-anchor
|
||||
direction="vertical"
|
||||
:offset="130"
|
||||
type="default"
|
||||
container=".category-scrollbar"
|
||||
@click="handleClick"
|
||||
>
|
||||
<el-anchor-link
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:href="`#category-${category.id}`"
|
||||
:title="category.title"
|
||||
/>
|
||||
</el-anchor>
|
||||
</template> -->
|
||||
|
||||
<el-scrollbar class="layout-bg" wrap-class="p-16-24 category-scrollbar">
|
||||
<template v-if="filterList === null">
|
||||
<div v-for="category in categories" :key="category.id">
|
||||
<!-- <h4
|
||||
class="title-decoration-1 mb-16 mt-8 color-text-primary"
|
||||
:id="`category-${category.id}`"
|
||||
>
|
||||
{{ category.title }}
|
||||
</h4> -->
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="tool in category.tools" :key="tool.id" :span="8" class="mb-16">
|
||||
<TemplateCard
|
||||
:tool="tool"
|
||||
:addLoading="addLoading"
|
||||
:get-sub-title="getSubTitle"
|
||||
@handleAdd="handleOpenAdd(tool)"
|
||||
@handleDetail="handleDetail(tool)"
|
||||
>
|
||||
</TemplateCard>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
<!-- <h4 class="color-text-primary medium mb-16">
|
||||
<span class="color-primary">{{ searchValue }}</span>
|
||||
{{ t('views.tool.toolStore.searchResult', { count: filterList.length }) }}
|
||||
</h4> -->
|
||||
<el-row :gutter="16" v-if="filterList.length">
|
||||
<el-col v-for="tool in filterList" :key="tool.id" :span="12" class="mb-16">
|
||||
<TemplateCard
|
||||
:tool="tool"
|
||||
:addLoading="addLoading"
|
||||
:get-sub-title="getSubTitle"
|
||||
@handleAdd="handleOpenAdd(tool)"
|
||||
@handleDetail="handleDetail(tool)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else :description="$t('common.noData')" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<!-- </LayoutContainer> -->
|
||||
</el-dialog>
|
||||
<InternalDescDrawer ref="internalDescDrawerRef" @addTool="handleOpenAdd" />
|
||||
<CreateApplicationDialog ref="CreateKnowledgeDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import ToolStoreApi from '@/api/tool/store'
|
||||
import { t } from '@/locales'
|
||||
import TemplateCard from './TemplateCard.vue'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
import InternalDescDrawer from './InternalDescDrawer.vue'
|
||||
import { loadSharedApi } from '@/utils/dynamics-api/shared-api.ts'
|
||||
import useStore from '@/stores'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CreateApplicationDialog from '@/views/application/component/CreateApplicationDialog.vue'
|
||||
|
||||
const { user } = useStore()
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id },
|
||||
/*
|
||||
folderId 可以区分 resource-management shared还是 workspace
|
||||
*/
|
||||
} = route as any
|
||||
|
||||
interface ToolCategory {
|
||||
id: string
|
||||
title: string
|
||||
tools: any[]
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
apiType: {
|
||||
type: String as () => 'workspace' | 'systemShare' | 'systemManage' | 'workspaceShare',
|
||||
default: 'workspace',
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: 'application',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const searchValue = ref('')
|
||||
const folderId = ref('')
|
||||
const categories = ref<ToolCategory[]>([])
|
||||
|
||||
const filterList = ref<any>(null)
|
||||
|
||||
function getSubTitle(tool: any) {
|
||||
return categories.value.find((i) => i.id === tool.label)?.title ?? ''
|
||||
}
|
||||
|
||||
function open(id: string) {
|
||||
folderId.value = id
|
||||
filterList.value = null
|
||||
dialogVisible.value = true
|
||||
|
||||
getList()
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
filterList.value = null
|
||||
const [v1] = await Promise.all([getStoreToolList()])
|
||||
|
||||
const merged = [...v1].reduce((acc, category) => {
|
||||
const existing = acc.find((item: any) => item.id === category.id)
|
||||
if (existing) {
|
||||
existing.tools = [...existing.tools, ...category.tools]
|
||||
} else {
|
||||
acc.push({ ...category })
|
||||
}
|
||||
return acc
|
||||
}, [] as ToolCategory[])
|
||||
|
||||
categories.value = merged.filter((item: any) => item.tools.length > 0)
|
||||
}
|
||||
|
||||
async function getStoreToolList() {
|
||||
try {
|
||||
const res = await ToolStoreApi.getStoreAppList({ name: searchValue.value }, loading)
|
||||
const tags = res.data.additionalProperties.tags
|
||||
const storeTools = res.data.apps
|
||||
let categories = []
|
||||
//
|
||||
storeTools.forEach((tool: any) => {
|
||||
tool.desc = tool.description
|
||||
})
|
||||
if (searchValue.value.length) {
|
||||
filterList.value = [...res.data.apps, ...(filterList.value || [])]
|
||||
} else {
|
||||
filterList.value = null
|
||||
categories = tags.map((tag: any) => ({
|
||||
id: tag.key,
|
||||
title: tag.name, // 国际化
|
||||
tools: storeTools.filter((tool: any) => tool.label === tag.key),
|
||||
}))
|
||||
}
|
||||
return categories
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const internalDescDrawerRef = ref<InstanceType<typeof InternalDescDrawer>>()
|
||||
|
||||
async function handleDetail(tool: any) {
|
||||
internalDescDrawerRef.value?.open(tool.readMe, tool)
|
||||
}
|
||||
|
||||
const CreateKnowledgeDialogRef = ref()
|
||||
|
||||
function handleOpenAdd(data?: any, isEdit?: boolean) {
|
||||
if (props.source === 'work_flow') {
|
||||
MsgConfirm(
|
||||
t('common.tip'),
|
||||
`${t('views.application.tip.confirmUse')} ${data.name} ${t('views.application.tip.overwrite')}?`,
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
handleStoreAdd(data)
|
||||
})
|
||||
.catch(() => {})
|
||||
} else {
|
||||
CreateKnowledgeDialogRef.value.open(folderId.value, 'WORK_FLOW', data)
|
||||
}
|
||||
}
|
||||
|
||||
const addLoading = ref(false)
|
||||
|
||||
function handleStoreAdd(tool: any) {
|
||||
try {
|
||||
loadSharedApi({ type: 'application', systemType: props.apiType })
|
||||
.putApplication(id, { work_flow_template: tool }, addLoading)
|
||||
.then(() => {
|
||||
emit('refresh')
|
||||
MsgSuccess(t('common.addSuccess'))
|
||||
})
|
||||
dialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.tool-store-dialog {
|
||||
padding: 0;
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 12px 20px 4px 24px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
|
||||
.dialog-header {
|
||||
position: relative;
|
||||
|
||||
.store-type {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-container__left {
|
||||
background-color: var(--app-layout-bg-color);
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
.layout-container__right {
|
||||
background-color: var(--app-layout-bg-color);
|
||||
border-radius: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.el-anchor {
|
||||
background-color: var(--app-layout-bg-color);
|
||||
|
||||
.el-anchor__marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-anchor__list {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.el-anchor__item {
|
||||
.el-anchor__link {
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
border-radius: 6px;
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: #3370ff1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-scrollbar {
|
||||
height: calc(100vh - 200px);
|
||||
// min-height: 500px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue