feat: add Appstore tool retrieval and store tool API endpoint
parent
a5d046c26b
commit
e988cbca91
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-09 04:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tools', '0002_alter_tool_tool_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tool',
|
||||||
|
name='template_id',
|
||||||
|
field=models.CharField(db_index=True, default=None, max_length=128, null=True, verbose_name='模版id'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tool',
|
||||||
|
name='version',
|
||||||
|
field=models.CharField(default=None, max_length=64, null=True, verbose_name='版本号'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -48,11 +48,12 @@ class Tool(AppModelMixin):
|
||||||
default=ToolScope.WORKSPACE, db_index=True)
|
default=ToolScope.WORKSPACE, db_index=True)
|
||||||
tool_type = models.CharField(max_length=20, verbose_name='工具类型', choices=ToolType.choices,
|
tool_type = models.CharField(max_length=20, verbose_name='工具类型', choices=ToolType.choices,
|
||||||
default=ToolType.CUSTOM, db_index=True)
|
default=ToolType.CUSTOM, db_index=True)
|
||||||
template_id = models.UUIDField(max_length=128, verbose_name="模版id", null=True, default=None, db_index=True)
|
template_id = models.CharField(max_length=128, verbose_name="模版id", null=True, default=None, db_index=True)
|
||||||
folder = models.ForeignKey(ToolFolder, on_delete=models.DO_NOTHING, verbose_name="文件夹id", default='default')
|
folder = models.ForeignKey(ToolFolder, on_delete=models.DO_NOTHING, verbose_name="文件夹id", default='default')
|
||||||
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
|
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
|
||||||
init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True)
|
init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True)
|
||||||
label = models.CharField(max_length=128, verbose_name="标签", null=True, db_index=True)
|
label = models.CharField(max_length=128, verbose_name="标签", null=True, db_index=True)
|
||||||
|
version = models.CharField(max_length=64, verbose_name="版本号", null=True, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "tool"
|
db_table = "tool"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
import uuid_utils.compat as uuid
|
import uuid_utils.compat as uuid
|
||||||
|
|
@ -124,7 +127,7 @@ class ToolModelSerializer(serializers.ModelSerializer):
|
||||||
model = Tool
|
model = Tool
|
||||||
fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list', 'init_field_list', 'init_params',
|
fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list', 'init_field_list', 'init_params',
|
||||||
'scope', 'is_active', 'user_id', 'template_id', 'workspace_id', 'folder_id', 'tool_type', 'label',
|
'scope', 'is_active', 'user_id', 'template_id', 'workspace_id', 'folder_id', 'tool_type', 'label',
|
||||||
'create_time', 'update_time']
|
'version', 'create_time', 'update_time']
|
||||||
|
|
||||||
|
|
||||||
class ToolExportModelSerializer(serializers.ModelSerializer):
|
class ToolExportModelSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -705,6 +708,7 @@ class ToolSerializer(serializers.Serializer):
|
||||||
tool_type=ToolType.CUSTOM,
|
tool_type=ToolType.CUSTOM,
|
||||||
folder_id=instance.get('folder_id', self.data.get('workspace_id')),
|
folder_id=instance.get('folder_id', self.data.get('workspace_id')),
|
||||||
template_id=internal_tool.id,
|
template_id=internal_tool.id,
|
||||||
|
label=internal_tool.label,
|
||||||
is_active=False
|
is_active=False
|
||||||
)
|
)
|
||||||
tool.save()
|
tool.save()
|
||||||
|
|
@ -718,6 +722,140 @@ class ToolSerializer(serializers.Serializer):
|
||||||
|
|
||||||
return ToolModelSerializer(tool).data
|
return ToolModelSerializer(tool).data
|
||||||
|
|
||||||
|
class StoreTool(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_tools(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
|
||||||
|
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 requests.RequestException as e:
|
||||||
|
maxkb_logger.error(f"fetch appstore tools error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
class AddStoreTool(serializers.Serializer):
|
||||||
|
user_id = serializers.UUIDField(required=True, label=_("User ID"))
|
||||||
|
workspace_id = serializers.CharField(required=True, label=_("workspace id"))
|
||||||
|
tool_id = serializers.CharField(required=True, label=_("tool id"))
|
||||||
|
|
||||||
|
def add(self, instance: Dict, with_valid=True):
|
||||||
|
if with_valid:
|
||||||
|
self.is_valid(raise_exception=True)
|
||||||
|
AddInternalToolRequest(data=instance).is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
versions = instance.get('versions', [])
|
||||||
|
download_url = instance.get('download_url')
|
||||||
|
# 查找匹配的版本名称
|
||||||
|
version_name = next(
|
||||||
|
(version.get('name') for version in versions if version.get('downloadUrl') == download_url),
|
||||||
|
)
|
||||||
|
res = requests.get(download_url, timeout=5)
|
||||||
|
tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool
|
||||||
|
tool_id = uuid.uuid7()
|
||||||
|
tool = Tool(
|
||||||
|
id=tool_id,
|
||||||
|
name=tool_data.get('name'),
|
||||||
|
desc=tool_data.get('desc'),
|
||||||
|
code=tool_data.get('code'),
|
||||||
|
user_id=self.data.get('user_id'),
|
||||||
|
icon=instance.get('icon', ''),
|
||||||
|
workspace_id=self.data.get('workspace_id'),
|
||||||
|
input_field_list=tool_data.get('input_field_list', []),
|
||||||
|
init_field_list=tool_data.get('init_field_list', []),
|
||||||
|
scope=ToolScope.WORKSPACE,
|
||||||
|
tool_type=ToolType.CUSTOM,
|
||||||
|
folder_id=instance.get('folder_id', self.data.get('workspace_id')),
|
||||||
|
template_id=self.data.get('tool_id'),
|
||||||
|
label=instance.get('label'),
|
||||||
|
version=version_name,
|
||||||
|
is_active=False
|
||||||
|
)
|
||||||
|
tool.save()
|
||||||
|
|
||||||
|
# 自动授权给创建者
|
||||||
|
UserResourcePermissionSerializer(data={
|
||||||
|
'workspace_id': self.data.get('workspace_id'),
|
||||||
|
'user_id': self.data.get('user_id'),
|
||||||
|
'auth_target_type': AuthTargetType.TOOL.value
|
||||||
|
}).auth_resource(str(tool_id))
|
||||||
|
try:
|
||||||
|
requests.get(instance.get('download_callback_url'), timeout=5)
|
||||||
|
except Exception as e:
|
||||||
|
maxkb_logger.error(f"callback appstore tool download error: {e}")
|
||||||
|
return ToolModelSerializer(tool).data
|
||||||
|
|
||||||
|
class UpdateStoreTool(serializers.Serializer):
|
||||||
|
user_id = serializers.UUIDField(required=True, label=_("User ID"))
|
||||||
|
workspace_id = serializers.CharField(required=True, label=_("workspace id"))
|
||||||
|
tool_id = serializers.UUIDField(required=True, label=_("tool id"))
|
||||||
|
download_url = serializers.CharField(required=True, label=_("download url"))
|
||||||
|
download_callback_url = serializers.CharField(required=True, label=_("download callback url"))
|
||||||
|
icon = serializers.CharField(required=True, label=_("icon"), allow_null=True, allow_blank=True)
|
||||||
|
versions = serializers.ListField(required=True, label=_("versions"), child=serializers.DictField())
|
||||||
|
|
||||||
|
def update_tool(self, with_valid=True):
|
||||||
|
if with_valid:
|
||||||
|
self.is_valid(raise_exception=True)
|
||||||
|
tool = QuerySet(Tool).filter(id=self.data.get('tool_id')).first()
|
||||||
|
if tool is None:
|
||||||
|
raise AppApiException(500, _('Tool does not exist'))
|
||||||
|
# 查找匹配的版本名称
|
||||||
|
version_name = next(
|
||||||
|
(version.get('name') for version in self.data.get('versions') if version.get('downloadUrl') == self.data.get('download_url')),
|
||||||
|
)
|
||||||
|
res = requests.get(self.data.get('download_url'), timeout=5)
|
||||||
|
tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool
|
||||||
|
tool.name = tool_data.get('name')
|
||||||
|
tool.desc = tool_data.get('desc')
|
||||||
|
tool.code = tool_data.get('code')
|
||||||
|
tool.input_field_list = tool_data.get('input_field_list', [])
|
||||||
|
tool.init_field_list = tool_data.get('init_field_list', [])
|
||||||
|
tool.icon = self.data.get('icon', tool.icon)
|
||||||
|
tool.version = version_name
|
||||||
|
# tool.is_active = False
|
||||||
|
tool.save()
|
||||||
|
try:
|
||||||
|
requests.get(self.data.get('download_callback_url'), timeout=5)
|
||||||
|
except Exception as e:
|
||||||
|
maxkb_logger.error(f"callback appstore tool download error: {e}")
|
||||||
|
return ToolModelSerializer(tool).data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ToolTreeSerializer(serializers.Serializer):
|
class ToolTreeSerializer(serializers.Serializer):
|
||||||
class Query(serializers.Serializer):
|
class Query(serializers.Serializer):
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from (select tool."id"::text,
|
||||||
tool."update_time",
|
tool."update_time",
|
||||||
tool.init_field_list,
|
tool.init_field_list,
|
||||||
tool.input_field_list,
|
tool.input_field_list,
|
||||||
|
tool.version,
|
||||||
tool."is_active"
|
tool."is_active"
|
||||||
from tool
|
from tool
|
||||||
left join "user" on "user".id = user_id ${tool_query_set}
|
left join "user" on "user".id = user_id ${tool_query_set}
|
||||||
|
|
@ -37,6 +38,7 @@ from (select tool."id"::text,
|
||||||
tool_folder."update_time",
|
tool_folder."update_time",
|
||||||
'[]'::jsonb as init_field_list,
|
'[]'::jsonb as init_field_list,
|
||||||
'[]'::jsonb as input_field_list,
|
'[]'::jsonb as input_field_list,
|
||||||
|
'' as version,
|
||||||
'true' as "is_active"
|
'true' as "is_active"
|
||||||
from tool_folder
|
from tool_folder
|
||||||
left join "user" on "user".id = user_id ${folder_query_set}) temp
|
left join "user" on "user".id = user_id ${folder_query_set}) temp
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ FROM (SELECT tool."id"::text,
|
||||||
tool."update_time",
|
tool."update_time",
|
||||||
tool.init_field_list,
|
tool.init_field_list,
|
||||||
tool.input_field_list,
|
tool.input_field_list,
|
||||||
|
tool.version,
|
||||||
tool."is_active"
|
tool."is_active"
|
||||||
FROM (SELECT tool.*
|
FROM (SELECT tool.*
|
||||||
FROM tool tool ${tool_query_set}
|
FROM tool tool ${tool_query_set}
|
||||||
|
|
@ -43,6 +44,7 @@ FROM (SELECT tool."id"::text,
|
||||||
tool_folder."update_time",
|
tool_folder."update_time",
|
||||||
'[]'::jsonb AS init_field_list,
|
'[]'::jsonb AS init_field_list,
|
||||||
'[]'::jsonb AS input_field_list,
|
'[]'::jsonb AS input_field_list,
|
||||||
|
'' AS version,
|
||||||
'true' AS "is_active"
|
'true' AS "is_active"
|
||||||
FROM tool_folder
|
FROM tool_folder
|
||||||
LEFT JOIN "user" ON "user".id = user_id ${folder_query_set}) temp
|
LEFT JOIN "user" ON "user".id = user_id ${folder_query_set}) temp
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ FROM (SELECT tool."id"::text,
|
||||||
tool."update_time",
|
tool."update_time",
|
||||||
tool.init_field_list,
|
tool.init_field_list,
|
||||||
tool.input_field_list,
|
tool.input_field_list,
|
||||||
|
tool.version,
|
||||||
tool."is_active"
|
tool."is_active"
|
||||||
FROM (SELECT tool.*
|
FROM (SELECT tool.*
|
||||||
FROM tool tool ${tool_query_set}
|
FROM tool tool ${tool_query_set}
|
||||||
|
|
@ -53,6 +54,7 @@ FROM (SELECT tool."id"::text,
|
||||||
tool_folder."update_time",
|
tool_folder."update_time",
|
||||||
'[]'::jsonb AS init_field_list,
|
'[]'::jsonb AS init_field_list,
|
||||||
'[]'::jsonb AS input_field_list,
|
'[]'::jsonb AS input_field_list,
|
||||||
|
'' AS version,
|
||||||
'true' AS "is_active"
|
'true' AS "is_active"
|
||||||
FROM tool_folder
|
FROM tool_folder
|
||||||
LEFT JOIN "user" ON "user".id = user_id ${folder_query_set}) temp
|
LEFT JOIN "user" ON "user".id = user_id ${folder_query_set}) temp
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ app_name = "tool"
|
||||||
# @formatter:off
|
# @formatter:off
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('workspace/internal/tool', views.ToolView.InternalTool.as_view()),
|
path('workspace/internal/tool', views.ToolView.InternalTool.as_view()),
|
||||||
|
path('workspace/store/tool', views.ToolView.StoreTool.as_view()),
|
||||||
path('workspace/<str:workspace_id>/tool', views.ToolView.as_view()),
|
path('workspace/<str:workspace_id>/tool', views.ToolView.as_view()),
|
||||||
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
|
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
|
||||||
path('workspace/<str:workspace_id>/tool/pylint', views.ToolView.Pylint.as_view()),
|
path('workspace/<str:workspace_id>/tool/pylint', views.ToolView.Pylint.as_view()),
|
||||||
|
|
@ -15,5 +16,7 @@ urlpatterns = [
|
||||||
path('workspace/<str:workspace_id>/tool/<str:tool_id>/edit_icon', views.ToolView.EditIcon.as_view()),
|
path('workspace/<str:workspace_id>/tool/<str:tool_id>/edit_icon', views.ToolView.EditIcon.as_view()),
|
||||||
path('workspace/<str:workspace_id>/tool/<str:tool_id>/export', views.ToolView.Export.as_view()),
|
path('workspace/<str:workspace_id>/tool/<str:tool_id>/export', views.ToolView.Export.as_view()),
|
||||||
path('workspace/<str:workspace_id>/tool/<str:tool_id>/add_internal_tool', views.ToolView.AddInternalTool.as_view()),
|
path('workspace/<str:workspace_id>/tool/<str:tool_id>/add_internal_tool', views.ToolView.AddInternalTool.as_view()),
|
||||||
|
path('workspace/<str:workspace_id>/tool/<str:tool_id>/add_store_tool', views.ToolView.AddStoreTool.as_view()),
|
||||||
|
path('workspace/<str:workspace_id>/tool/<str:tool_id>/update_store_tool', views.ToolView.UpdateStoreTool.as_view()),
|
||||||
path('workspace/<str:workspace_id>/tool/<int:current_page>/<int:page_size>', views.ToolView.Page.as_view()),
|
path('workspace/<str:workspace_id>/tool/<int:current_page>/<int:page_size>', views.ToolView.Page.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -407,3 +407,84 @@ class ToolView(APIView):
|
||||||
'user_id': request.user.id,
|
'user_id': request.user.id,
|
||||||
'workspace_id': workspace_id
|
'workspace_id': workspace_id
|
||||||
}).add(request.data))
|
}).add(request.data))
|
||||||
|
|
||||||
|
class StoreTool(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(ToolSerializer.StoreTool(data={
|
||||||
|
'user_id': request.user.id,
|
||||||
|
'name': request.query_params.get('name', ''),
|
||||||
|
}).get_appstore_tools())
|
||||||
|
|
||||||
|
class AddStoreTool(APIView):
|
||||||
|
authentication_classes = [TokenAuth]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
methods=['POST'],
|
||||||
|
description=_("Add Appstore tool"),
|
||||||
|
summary=_("Add Appstore tool"),
|
||||||
|
operation_id=_("Add Appstore tool"), # type: ignore
|
||||||
|
parameters=AddInternalToolAPI.get_parameters(),
|
||||||
|
request=AddInternalToolAPI.get_request(),
|
||||||
|
responses=AddInternalToolAPI.get_response(),
|
||||||
|
tags=[_("Tool")] # type: ignore
|
||||||
|
)
|
||||||
|
@has_permissions(
|
||||||
|
PermissionConstants.TOOL_CREATE.get_workspace_permission(),
|
||||||
|
PermissionConstants.TOOL_CREATE.get_workspace_permission_workspace_manage_role(),
|
||||||
|
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
|
||||||
|
RoleConstants.USER.get_workspace_role(),
|
||||||
|
)
|
||||||
|
@log(
|
||||||
|
menu='Tool', operate="Add Appstore tool",
|
||||||
|
get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool_id')),
|
||||||
|
)
|
||||||
|
def post(self, request: Request, tool_id: str, workspace_id: str):
|
||||||
|
return result.success(ToolSerializer.AddStoreTool(data={
|
||||||
|
'tool_id': tool_id,
|
||||||
|
'user_id': request.user.id,
|
||||||
|
'workspace_id': workspace_id,
|
||||||
|
}).add(request.data))
|
||||||
|
|
||||||
|
class UpdateStoreTool(APIView):
|
||||||
|
authentication_classes = [TokenAuth]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
methods=['POST'],
|
||||||
|
description=_("Update Appstore tool"),
|
||||||
|
summary=_("Update Appstore tool"),
|
||||||
|
operation_id=_("Update Appstore tool"), # type: ignore
|
||||||
|
parameters=AddInternalToolAPI.get_parameters(),
|
||||||
|
request=AddInternalToolAPI.get_request(),
|
||||||
|
responses=AddInternalToolAPI.get_response(),
|
||||||
|
tags=[_("Tool")] # type: ignore
|
||||||
|
)
|
||||||
|
@has_permissions(
|
||||||
|
PermissionConstants.TOOL_CREATE.get_workspace_permission(),
|
||||||
|
PermissionConstants.TOOL_CREATE.get_workspace_permission_workspace_manage_role(),
|
||||||
|
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
|
||||||
|
RoleConstants.USER.get_workspace_role(),
|
||||||
|
)
|
||||||
|
@log(
|
||||||
|
menu='Tool', operate="Update Appstore tool",
|
||||||
|
get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool_id')),
|
||||||
|
)
|
||||||
|
def post(self, request: Request, tool_id: str, workspace_id: str):
|
||||||
|
return result.success(ToolSerializer.UpdateStoreTool(data={
|
||||||
|
'tool_id': tool_id,
|
||||||
|
'user_id': request.user.id,
|
||||||
|
'workspace_id': workspace_id,
|
||||||
|
'download_url': request.data.get('download_url'),
|
||||||
|
'download_callback_url': request.data.get('download_callback_url'),
|
||||||
|
'icon': request.data.get('icon'),
|
||||||
|
'versions': request.data.get('versions'),
|
||||||
|
}).update_tool(request.data))
|
||||||
|
|
@ -142,6 +142,24 @@ const addInternalTool: (
|
||||||
return post(`${prefix}/${tool_id}/add_internal_tool`, param, undefined, loading)
|
return post(`${prefix}/${tool_id}/add_internal_tool`, param, undefined, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具商店
|
||||||
|
*/
|
||||||
|
const addStoreTool: (
|
||||||
|
tool_id: string,
|
||||||
|
param: AddInternalToolParam,
|
||||||
|
loading?: Ref<boolean>,
|
||||||
|
) => Promise<Result<any>> = (tool_id, param, loading) => {
|
||||||
|
return post(`${prefix}/${tool_id}/add_store_tool`, param, undefined, loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStoreTool: (
|
||||||
|
tool_id: string,
|
||||||
|
param: AddInternalToolParam,
|
||||||
|
loading?: Ref<boolean>,
|
||||||
|
) => Promise<Result<any>> = (tool_id, param, loading) => {
|
||||||
|
return post(`${prefix}/${tool_id}/update_store_tool`, param, undefined, loading)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getToolList,
|
getToolList,
|
||||||
|
|
@ -156,5 +174,7 @@ export default {
|
||||||
exportTool,
|
exportTool,
|
||||||
putToolIcon,
|
putToolIcon,
|
||||||
delTool,
|
delTool,
|
||||||
addInternalTool
|
addInternalTool,
|
||||||
|
addStoreTool,
|
||||||
|
updateStoreTool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,16 @@ const getInternalToolList: (param?: any, loading?: Ref<boolean>) => Promise<Resu
|
||||||
return get('/workspace/internal/tool', param, loading)
|
return get('/workspace/internal/tool', param, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具商店列表
|
||||||
|
*/
|
||||||
|
const getStoreToolList: (param?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||||
|
param,
|
||||||
|
loading,
|
||||||
|
) => {
|
||||||
|
return get('/workspace/store/tool', param, loading)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具商店-添加系统内置
|
* 工具商店-添加系统内置
|
||||||
*/
|
*/
|
||||||
|
|
@ -33,7 +43,20 @@ const addInternalTool: (
|
||||||
return post(`${prefix.value}/${tool_id}/add_internal_tool`, param, undefined, loading)
|
return post(`${prefix.value}/${tool_id}/add_internal_tool`, param, undefined, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具商店-添加
|
||||||
|
*/
|
||||||
|
const addStoreTool: (
|
||||||
|
tool_id: string,
|
||||||
|
param: AddInternalToolParam,
|
||||||
|
loading?: Ref<boolean>,
|
||||||
|
) => Promise<Result<any>> = (tool_id, param, loading) => {
|
||||||
|
return post(`${prefix.value}/${tool_id}/add_store_tool`, param, undefined, loading)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getInternalToolList,
|
getInternalToolList,
|
||||||
|
getStoreToolList,
|
||||||
addInternalTool,
|
addInternalTool,
|
||||||
|
addStoreTool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,27 @@ const addInternalTool: (
|
||||||
return post(`${prefix.value}/${tool_id}/add_internal_tool`, param, undefined, loading)
|
return post(`${prefix.value}/${tool_id}/add_internal_tool`, param, undefined, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具商店-添加
|
||||||
|
*/
|
||||||
|
const addStoreTool: (
|
||||||
|
tool_id: string,
|
||||||
|
param: AddInternalToolParam,
|
||||||
|
loading?: Ref<boolean>,
|
||||||
|
) => Promise<Result<any>> = (tool_id, param, loading) => {
|
||||||
|
return post(`${prefix.value}/${tool_id}/add_store_tool`, param, undefined, loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStoreTool: (
|
||||||
|
tool_id: string,
|
||||||
|
param: AddInternalToolParam,
|
||||||
|
loading?: Ref<boolean>,
|
||||||
|
) => Promise<Result<any>> = (tool_id, param, loading) => {
|
||||||
|
return post(`${prefix.value}/${tool_id}/update_store_tool`, param, undefined, loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getToolList,
|
getToolList,
|
||||||
getAllToolList,
|
getAllToolList,
|
||||||
|
|
@ -162,4 +183,6 @@ export default {
|
||||||
putToolIcon,
|
putToolIcon,
|
||||||
delTool,
|
delTool,
|
||||||
addInternalTool,
|
addInternalTool,
|
||||||
|
addStoreTool,
|
||||||
|
updateStoreTool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export default {
|
||||||
developer: 'Developer',
|
developer: 'Developer',
|
||||||
communication: 'Communication',
|
communication: 'Communication',
|
||||||
searchResult: '{count} search results for',
|
searchResult: '{count} search results for',
|
||||||
|
confirmTip: 'Are you sure to update tool: ',
|
||||||
|
updateStoreToolMessage: 'Updating tools may affect resources in use, so proceed with caution.',
|
||||||
},
|
},
|
||||||
searchBar: {
|
searchBar: {
|
||||||
placeholder: 'Search by tool name',
|
placeholder: 'Search by tool name',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export default {
|
||||||
developer: '开发者',
|
developer: '开发者',
|
||||||
communication: '通信',
|
communication: '通信',
|
||||||
searchResult: '的搜索结果 {count} 个',
|
searchResult: '的搜索结果 {count} 个',
|
||||||
|
confirmTip: '是否更新工具:',
|
||||||
|
updateStoreToolMessage: '更新工具可能会影响正在使用的资源,请谨慎操作。',
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
confirmTitle: '是否刪除工具',
|
confirmTitle: '是否刪除工具',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export default {
|
||||||
developer: '開發者',
|
developer: '開發者',
|
||||||
communication: '通信',
|
communication: '通信',
|
||||||
searchResult: '的搜索結果 {count} 個',
|
searchResult: '的搜索結果 {count} 個',
|
||||||
|
confirmTip: '是否更新工具:',
|
||||||
|
updateStoreToolMessage: '更新工具可能會影響正在使用的資源,請謹慎操作。',
|
||||||
},
|
},
|
||||||
searchBar: {
|
searchBar: {
|
||||||
placeholder: '按工具名稱搜尋',
|
placeholder: '按工具名稱搜尋',
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
<el-table-column prop="tool_type" :label="$t('views.system.resource_management.type')">
|
<el-table-column prop="tool_type" :label="$t('views.system.resource_management.type')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span v-if="scope.row.tool_type === 'MCP'"> MCP </span>
|
<span v-if="scope.row.tool_type === 'MCP'"> MCP </span>
|
||||||
|
<span v-else-if="scope.row.version">{{ $t('views.tool.toolStore.title') }}</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,14 @@
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
<ToolIcon v-else :size="32" :type="item?.tool_type" />
|
<ToolIcon v-else :size="32" :type="item?.tool_type" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<div>
|
||||||
|
{{ item.name }}
|
||||||
|
<el-tag v-if="item.version" class="ml-4">
|
||||||
|
{{ item.version }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #subTitle>
|
<template #subTitle>
|
||||||
<el-text class="color-secondary lighter" size="small">
|
<el-text class="color-secondary lighter" size="small">
|
||||||
{{ $t('common.creator') }}: {{ item.nick_name }}
|
{{ $t('common.creator') }}: {{ item.nick_name }}
|
||||||
|
|
@ -185,6 +193,12 @@
|
||||||
<el-tag v-if="isShared" type="info" class="info-tag">
|
<el-tag v-if="isShared" type="info" class="info-tag">
|
||||||
{{ t('views.shared.title') }}
|
{{ t('views.shared.title') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
<el-button text @click.stop v-if="
|
||||||
|
showUpdateStoreTool(item) && !isShared && permissionPrecise.edit(item.id)
|
||||||
|
" @click="updateStoreTool(item)"
|
||||||
|
>
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
@ -353,6 +367,7 @@ import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
||||||
import permissionMap from '@/permission'
|
import permissionMap from '@/permission'
|
||||||
import useStore from '@/stores'
|
import useStore from '@/stores'
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
|
import ToolStoreApi from "@/api/tool/store.ts";
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { folder, user, tool } = useStore()
|
const { folder, user, tool } = useStore()
|
||||||
onBeforeRouteLeave((to, from) => {
|
onBeforeRouteLeave((to, from) => {
|
||||||
|
|
@ -626,6 +641,57 @@ function confirmAddInternalTool(data?: any, isEdit?: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storeTools = ref<any[]>([])
|
||||||
|
function getStoreToolList() {
|
||||||
|
ToolStoreApi.getStoreToolList({ name: '' }, loading)
|
||||||
|
.then((res: any) => {
|
||||||
|
storeTools.value = res.data.apps
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUpdateStoreTool(item: any) {
|
||||||
|
for (const tool of storeTools.value) {
|
||||||
|
if (tool.id === item.template_id && tool.version !== item.version) {
|
||||||
|
item.downloadUrl = tool.downloadUrl
|
||||||
|
item.downloadCallbackUrl = tool.downloadCallbackUrl
|
||||||
|
item.icon = tool.icon
|
||||||
|
item.versions = tool.versions
|
||||||
|
item.label = tool.label
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStoreTool(item: any) {
|
||||||
|
MsgConfirm(t('views.tool.toolStore.confirmTip') + item.name,
|
||||||
|
t('views.tool.toolStore.updateStoreToolMessage'), {
|
||||||
|
cancelButtonText: t('common.cancel'),
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const obj = {
|
||||||
|
download_url: item.downloadUrl,
|
||||||
|
download_callback_url: item.downloadCallbackUrl,
|
||||||
|
icon: item.icon,
|
||||||
|
versions: item.versions,
|
||||||
|
label: item.label
|
||||||
|
}
|
||||||
|
loadSharedApi({type: 'tool', systemType: apiType.value})
|
||||||
|
.updateStoreTool(item.id, obj, loading)
|
||||||
|
.then(async (res: any) => {
|
||||||
|
if (res?.data) {
|
||||||
|
tool.setToolList([])
|
||||||
|
return user.profile()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const elUploadRef = ref()
|
const elUploadRef = ref()
|
||||||
function importTool(file: any) {
|
function importTool(file: any) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
@ -745,6 +811,7 @@ onMounted(() => {
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
user_options.value = res.data
|
user_options.value = res.data
|
||||||
})
|
})
|
||||||
|
getStoreToolList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
<h4 :id="titleId" class="medium">
|
<h4 :id="titleId" class="medium">
|
||||||
{{ $t('views.tool.toolStore.title') }}
|
{{ $t('views.tool.toolStore.title') }}
|
||||||
</h4>
|
</h4>
|
||||||
|
<el-radio-group v-model="toolType" @change="radioChange" class="app-radio-button-group">
|
||||||
<!-- <el-tag class="store-type default-tag">{{t('views.tool.toolStore.internal')}}</el-tag> -->
|
<el-radio-button value="INTERNAL">{{ $t('views.tool.toolStore.internal') }}</el-radio-button>
|
||||||
|
<el-radio-button value="APPSTORE">{{ $t('views.tool.toolStore.title') }}</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
|
||||||
<div class="flex align-center" style="margin-right: 28px;">
|
<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"
|
<el-input v-model="searchValue" :placeholder="$t('common.search')" prefix-icon="Search" class="w-240 mr-8"
|
||||||
|
|
@ -88,6 +90,7 @@ const dialogVisible = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
const folderId = ref('')
|
const folderId = ref('')
|
||||||
|
const toolType = ref('INTERNAL')
|
||||||
|
|
||||||
const categories = ref<ToolCategory[]>([
|
const categories = ref<ToolCategory[]>([
|
||||||
// 第一版不上
|
// 第一版不上
|
||||||
|
|
@ -139,6 +142,14 @@ onBeforeMount(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getList() {
|
async function getList() {
|
||||||
|
if (toolType.value === 'INTERNAL') {
|
||||||
|
await getInternalToolList()
|
||||||
|
} else {
|
||||||
|
await getStoreToolList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInternalToolList() {
|
||||||
try {
|
try {
|
||||||
const res = await ToolStoreApi.getInternalToolList({ name: searchValue.value }, loading)
|
const res = await ToolStoreApi.getInternalToolList({ name: searchValue.value }, loading)
|
||||||
if (searchValue.value.length) {
|
if (searchValue.value.length) {
|
||||||
|
|
@ -158,16 +169,36 @@ async function getList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getStoreToolList() {
|
||||||
|
try {
|
||||||
|
const res = await ToolStoreApi.getStoreToolList({ name: searchValue.value }, loading)
|
||||||
|
const tags = res.data.additionalProperties.tags
|
||||||
|
const storeTools = res.data.apps
|
||||||
|
|
||||||
|
categories.value = tags.map((tag: any) => ({
|
||||||
|
id: tag.key,
|
||||||
|
title: tag.name, // 国际化
|
||||||
|
tools: storeTools.filter((tool: any) => tool.label === tag.key)
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalDescDrawerRef = ref<InstanceType<typeof InternalDescDrawer>>()
|
const internalDescDrawerRef = ref<InstanceType<typeof InternalDescDrawer>>()
|
||||||
async function handleDetail(tool: any) {
|
async function handleDetail(tool: any) {
|
||||||
const index = tool.icon.replace('icon.png', 'detail.md')
|
if (toolType.value === 'INTERNAL') {
|
||||||
const response = await fetch(index)
|
const index = tool.icon.replace('icon.png', 'detail.md')
|
||||||
const content = await response.text()
|
const response = await fetch(index)
|
||||||
internalDescDrawerRef.value?.open(content, tool)
|
const content = await response.text()
|
||||||
|
internalDescDrawerRef.value?.open(content, tool)
|
||||||
|
} else {
|
||||||
|
internalDescDrawerRef.value?.open(tool.readMe, tool)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addInternalToolDialogRef = ref<InstanceType<typeof AddInternalToolDialog>>()
|
const addInternalToolDialogRef = ref<InstanceType<typeof AddInternalToolDialog>>()
|
||||||
|
|
@ -177,13 +208,20 @@ function handleOpenAdd(data?: any, isEdit?: boolean) {
|
||||||
|
|
||||||
const addLoading = ref(false)
|
const addLoading = ref(false)
|
||||||
async function handleAdd(tool: any) {
|
async function handleAdd(tool: any) {
|
||||||
|
if (toolType.value === 'INTERNAL') {
|
||||||
|
await handleInternalAdd(tool)
|
||||||
|
} else {
|
||||||
|
await handleStoreAdd(tool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleInternalAdd(tool: any) {
|
||||||
try {
|
try {
|
||||||
await loadSharedApi({ type: 'tool', systemType: props.apiType })
|
await loadSharedApi({ type: 'tool', systemType: props.apiType })
|
||||||
.addInternalTool(tool.id, { name: tool.name, folder_id: folderId.value }, addLoading)
|
.addInternalTool(tool.id, { name: tool.name, folder_id: folderId.value }, addLoading)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return user.profile()
|
return user.profile()
|
||||||
})
|
})
|
||||||
// await ToolStoreApi.addInternalTool(tool.id, { name: tool.name, folder_id: folderId.value }, addLoading)
|
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
MsgSuccess(t('common.addSuccess'))
|
MsgSuccess(t('common.addSuccess'))
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
|
|
@ -192,6 +230,34 @@ async function handleAdd(tool: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleStoreAdd(tool: any) {
|
||||||
|
try {
|
||||||
|
const obj = {
|
||||||
|
name: tool.name,
|
||||||
|
folder_id: folderId.value,
|
||||||
|
download_url: tool.downloadUrl,
|
||||||
|
download_callback_url: tool.downloadCallbackUrl,
|
||||||
|
icon: tool.icon,
|
||||||
|
versions: tool.versions,
|
||||||
|
label: tool.label
|
||||||
|
}
|
||||||
|
await loadSharedApi({ type: 'tool', systemType: props.apiType })
|
||||||
|
.addStoreTool(tool.id, obj, addLoading)
|
||||||
|
.then(() => {
|
||||||
|
return user.profile()
|
||||||
|
})
|
||||||
|
emit('refresh')
|
||||||
|
MsgSuccess(t('common.addSuccess'))
|
||||||
|
dialogVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function radioChange() {
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue