Compare commits

..

No commits in common. "aed0168795fe66ebcb73369f2d8a153c2bbcc8fc" and "082cfaaada4e26a757240e79a56b5e89bcda002e" have entirely different histories.

32 changed files with 146 additions and 536 deletions

View File

@ -38,8 +38,6 @@ jobs:
if: ${{ contains(github.event.inputs.registry, 'fit2cloud') }} if: ${{ contains(github.event.inputs.registry, 'fit2cloud') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clear Work Dir
run: rm -rf -- ./* ./.??*
- name: Check Disk Space - name: Check Disk Space
run: df -h run: df -h
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
@ -92,12 +90,6 @@ jobs:
registry: ${{ secrets.FIT2CLOUD_REGISTRY_HOST }} registry: ${{ secrets.FIT2CLOUD_REGISTRY_HOST }}
username: ${{ secrets.FIT2CLOUD_REGISTRY_USERNAME }} username: ${{ secrets.FIT2CLOUD_REGISTRY_USERNAME }}
password: ${{ secrets.FIT2CLOUD_REGISTRY_PASSWORD }} password: ${{ secrets.FIT2CLOUD_REGISTRY_PASSWORD }}
- name: Build Web
run: |
docker buildx build --no-cache --target web-build --output type=local,dest=./web-build-output . -f installer/Dockerfile
rm -rf ./ui
cp -r ./web-build-output/ui ./
rm -rf ./web-build-output
- name: Docker Buildx (build-and-push) - name: Docker Buildx (build-and-push)
run: | run: |
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
@ -107,8 +99,6 @@ jobs:
if: ${{ contains(github.event.inputs.registry, 'dockerhub') }} if: ${{ contains(github.event.inputs.registry, 'dockerhub') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clear Work Dir
run: rm -rf -- ./* ./.??*
- name: Check Disk Space - name: Check Disk Space
run: df -h run: df -h
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
@ -160,12 +150,6 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build Web
run: |
docker buildx build --no-cache --target web-build --output type=local,dest=./web-build-output . -f installer/Dockerfile
rm -rf ./ui
cp -r ./web-build-output/ui ./
rm -rf ./web-build-output
- name: Docker Buildx (build-and-push) - name: Docker Buildx (build-and-push)
run: | run: |
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m

View File

@ -8663,7 +8663,4 @@ msgid "resource authorization"
msgstr "" msgstr ""
msgid "The Qwen Audio based end-to-end speech recognition model supports audio recognition within 3 minutes. At present, it mainly supports Chinese and English recognition." msgid "The Qwen Audio based end-to-end speech recognition model supports audio recognition within 3 minutes. At present, it mainly supports Chinese and English recognition."
msgstr ""
msgid "If not passed, the default value is 'zh'"
msgstr "" msgstr ""

View File

@ -8789,7 +8789,4 @@ msgid "resource authorization"
msgstr "资源授权" msgstr "资源授权"
msgid "The Qwen Audio based end-to-end speech recognition model supports audio recognition within 3 minutes. At present, it mainly supports Chinese and English recognition." msgid "The Qwen Audio based end-to-end speech recognition model supports audio recognition within 3 minutes. At present, it mainly supports Chinese and English recognition."
msgstr "基于Qwen-Audio的端到端语音识别大模型支持3分钟以内的音频识别目前主要支持中英文识别。" msgstr "基于Qwen-Audio的端到端语音识别大模型支持3分钟以内的音频识别目前主要支持中英文识别。"
msgid "If not passed, the default value is 'zh'"
msgstr "如果未传递,则默认值为'zh'"

View File

@ -8789,7 +8789,4 @@ msgid "resource authorization"
msgstr "資源授權" msgstr "資源授權"
msgid "The Qwen Audio based end-to-end speech recognition model supports audio recognition within 3 minutes. At present, it mainly supports Chinese and English recognition." msgid "The Qwen Audio based end-to-end speech recognition model supports audio recognition within 3 minutes. At present, it mainly supports Chinese and English recognition."
msgstr "基於Qwen-Audio的端到端語音辨識大模型支持3分鐘以內的音訊識別現時主要支持中英文識別。" msgstr "基於Qwen-Audio的端到端語音辨識大模型支持3分鐘以內的音訊識別現時主要支持中英文識別。"
msgid "If not passed, the default value is 'zh'"
msgstr "如果未傳遞,則預設值為'zh'"

View File

@ -1,62 +0,0 @@
# coding=utf-8
import traceback
from typing import Dict
from django.utils.translation import gettext_lazy as _, gettext
from langchain_core.messages import HumanMessage
from common import forms
from common.exception.app_exception import AppApiException
from common.forms import BaseForm, TooltipLabel
from models_provider.base_model_provider import BaseModelCredential, ValidCode
class VLLMWhisperModelParams(BaseForm):
Language = forms.TextInputField(
TooltipLabel(_('Language'),
_("If not passed, the default value is 'zh'")),
required=True,
default_value='zh',
)
class VLLMWhisperModelCredential(BaseForm, BaseModelCredential):
api_url = forms.TextInputField('API URL', required=True)
api_key = forms.PasswordInputField('API Key', required=True)
def is_valid(self,
model_type: str,
model_name,
model_credential: Dict[str, object],
model_params,
provider,
raise_exception=False):
model_type_list = provider.get_model_type_list()
if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))):
raise AppApiException(ValidCode.valid_error.value,
gettext('{model_type} Model type is not supported').format(model_type=model_type))
try:
model_list = provider.get_base_model_list(model_credential.get('api_url'), model_credential.get('api_key'))
except Exception as e:
raise AppApiException(ValidCode.valid_error.value, gettext('API domain name is invalid'))
exist = provider.get_model_info_by_name(model_list, model_name)
if len(exist) == 0:
raise AppApiException(ValidCode.valid_error.value,
gettext('The model does not exist, please download the model first'))
model = provider.get_model(model_type, model_name, model_credential, **model_params)
return True
def encryption_dict(self, model_info: Dict[str, object]):
return {**model_info, 'api_key': super().encryption(model_info.get('api_key', ''))}
def build_model(self, model_info: Dict[str, object]):
for key in ['api_key', 'model']:
if key not in model_info:
raise AppApiException(500, gettext('{key} is required').format(key=key))
self.api_key = model_info.get('api_key')
return self
def get_model_params_setting_form(self, model_name):
return VLLMWhisperModelParams()

View File

@ -1,64 +0,0 @@
import base64
import os
import traceback
from typing import Dict
from openai import OpenAI
from common.utils.logger import maxkb_logger
from models_provider.base_model_provider import MaxKBBaseModel
from models_provider.impl.base_stt import BaseSpeechToText
class VllmWhisperSpeechToText(MaxKBBaseModel, BaseSpeechToText):
api_key: str
api_url: str
model: str
params: dict
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.api_key = kwargs.get('api_key')
self.model = kwargs.get('model')
self.params = kwargs.get('params')
self.api_url = kwargs.get('api_url')
@staticmethod
def is_cache_model():
return False
@staticmethod
def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs):
return VllmWhisperSpeechToText(
model=model_name,
api_key=model_credential.get('api_key'),
api_url=model_credential.get('api_url'),
params=model_kwargs,
**model_kwargs
)
def check_auth(self):
cwd = os.path.dirname(os.path.abspath(__file__))
with open(f'{cwd}/iat_mp3_16k.mp3', 'rb') as audio_file:
self.speech_to_text(audio_file)
def speech_to_text(self, audio_file):
base_url = f"{self.api_url}/v1"
try:
client = OpenAI(
api_key=self.api_key,
base_url=base_url
)
result = client.audio.transcriptions.create(
file=audio_file,
model=self.model,
language=self.params.get('Language'),
response_format="json"
)
return result.text
except Exception as err:
maxkb_logger.error(f":Error: {str(err)}: {traceback.format_exc()}")

View File

@ -10,27 +10,20 @@ from models_provider.base_model_provider import IModelProvider, ModelProvideInfo
from models_provider.impl.vllm_model_provider.credential.embedding import VllmEmbeddingCredential from models_provider.impl.vllm_model_provider.credential.embedding import VllmEmbeddingCredential
from models_provider.impl.vllm_model_provider.credential.image import VllmImageModelCredential from models_provider.impl.vllm_model_provider.credential.image import VllmImageModelCredential
from models_provider.impl.vllm_model_provider.credential.llm import VLLMModelCredential from models_provider.impl.vllm_model_provider.credential.llm import VLLMModelCredential
from models_provider.impl.vllm_model_provider.credential.whisper_stt import VLLMWhisperModelCredential
from models_provider.impl.vllm_model_provider.model.embedding import VllmEmbeddingModel from models_provider.impl.vllm_model_provider.model.embedding import VllmEmbeddingModel
from models_provider.impl.vllm_model_provider.model.image import VllmImage from models_provider.impl.vllm_model_provider.model.image import VllmImage
from models_provider.impl.vllm_model_provider.model.llm import VllmChatModel from models_provider.impl.vllm_model_provider.model.llm import VllmChatModel
from maxkb.conf import PROJECT_DIR from maxkb.conf import PROJECT_DIR
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from models_provider.impl.vllm_model_provider.model.whisper_sst import VllmWhisperSpeechToText
v_llm_model_credential = VLLMModelCredential() v_llm_model_credential = VLLMModelCredential()
image_model_credential = VllmImageModelCredential() image_model_credential = VllmImageModelCredential()
embedding_model_credential = VllmEmbeddingCredential() embedding_model_credential = VllmEmbeddingCredential()
whisper_model_credential = VLLMWhisperModelCredential()
model_info_list = [ model_info_list = [
ModelInfo('facebook/opt-125m', _('Facebooks 125M parameter model'), ModelTypeConst.LLM, v_llm_model_credential, ModelInfo('facebook/opt-125m', _('Facebooks 125M parameter model'), ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel),
VllmChatModel), ModelInfo('BAAI/Aquila-7B', _('BAAIs 7B parameter model'), ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel),
ModelInfo('BAAI/Aquila-7B', _('BAAIs 7B parameter model'), ModelTypeConst.LLM, v_llm_model_credential, ModelInfo('BAAI/AquilaChat-7B', _('BAAIs 13B parameter mode'), ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel),
VllmChatModel),
ModelInfo('BAAI/AquilaChat-7B', _('BAAIs 13B parameter mode'), ModelTypeConst.LLM, v_llm_model_credential,
VllmChatModel),
] ]
@ -39,15 +32,7 @@ image_model_info_list = [
] ]
embedding_model_info_list = [ embedding_model_info_list = [
ModelInfo('HIT-TMG/KaLM-embedding-multilingual-mini-instruct-v1.5', '', ModelTypeConst.EMBEDDING, ModelInfo('HIT-TMG/KaLM-embedding-multilingual-mini-instruct-v1.5', '', ModelTypeConst.EMBEDDING, embedding_model_credential, VllmEmbeddingModel),
embedding_model_credential, VllmEmbeddingModel),
]
whisper_model_info_list = [
ModelInfo('whisper-tiny', '', ModelTypeConst.STT, whisper_model_credential, VllmWhisperSpeechToText),
ModelInfo('whisper-large-v3-turbo', '', ModelTypeConst.STT, whisper_model_credential, VllmWhisperSpeechToText),
ModelInfo('whisper-small', '', ModelTypeConst.STT, whisper_model_credential, VllmWhisperSpeechToText),
ModelInfo('whisper-large-v3', '', ModelTypeConst.STT, whisper_model_credential, VllmWhisperSpeechToText),
] ]
model_info_manage = ( model_info_manage = (
@ -60,8 +45,6 @@ model_info_manage = (
.append_default_model_info(image_model_info_list[0]) .append_default_model_info(image_model_info_list[0])
.append_model_info_list(embedding_model_info_list) .append_model_info_list(embedding_model_info_list)
.append_default_model_info(embedding_model_info_list[0]) .append_default_model_info(embedding_model_info_list[0])
.append_model_info_list(whisper_model_info_list)
.append_default_model_info(whisper_model_info_list[0])
.build() .build()
) )

View File

@ -106,11 +106,11 @@ class WenxinLLMModelCredential(BaseForm, BaseModelCredential):
method='', ) method='', )
# v2版本字段 # v2版本字段
api_base = forms.TextInputField("API URL", required=True, relation_show_field_dict={"api_version": ["v2"]}) api_base = forms.TextInputField("API Base", required=False, relation_show_field_dict={"api_version": ["v2"]})
# v1版本字段 # v1版本字段
api_key = forms.PasswordInputField('API Key', required=True) api_key = forms.PasswordInputField('API Key', required=False)
secret_key = forms.PasswordInputField("Secret Key", required=True, secret_key = forms.PasswordInputField("Secret Key", required=False,
relation_show_field_dict={"api_version": ["v1"]}) relation_show_field_dict={"api_version": ["v1"]})
def get_model_params_setting_form(self, model_name): def get_model_params_setting_form(self, model_name):

View File

@ -381,7 +381,7 @@ class ModelSerializer(serializers.Serializer):
return [ return [
self._build_model_data( self._build_model_data(
model model
) for model in query_params.get('model_query_set') ) for model in query_params.get('model_query_set').order_by("-create_time")
] ]
def model_list(self, workspace_id, with_valid=True): def model_list(self, workspace_id, with_valid=True):
@ -398,7 +398,7 @@ class ModelSerializer(serializers.Serializer):
shared_queryset = get_authorized_model(shared_queryset, workspace_id) shared_queryset = get_authorized_model(shared_queryset, workspace_id)
# 构建共享模型和普通模型列表 # 构建共享模型和普通模型列表
shared_model = [self._build_model_data(model) for model in shared_queryset] shared_model = [self._build_model_data(model) for model in shared_queryset.order_by("-create_time")]
is_x_pack_ee = self.is_x_pack_ee() is_x_pack_ee = self.is_x_pack_ee()
normal_model = native_search( normal_model = native_search(
@ -429,7 +429,6 @@ class ModelSerializer(serializers.Serializer):
queryset = queryset.filter(user_id=value) queryset = queryset.filter(user_id=value)
else: else:
queryset = queryset.filter(**{field: value}) queryset = queryset.filter(**{field: value})
queryset = queryset.order_by("-create_time")
return { return {
'model_query_set': queryset, 'model_query_set': queryset,
'workspace_user_resource_permission_query_set': QuerySet(WorkspaceUserResourcePermission).filter( 'workspace_user_resource_permission_query_set': QuerySet(WorkspaceUserResourcePermission).filter(

View File

@ -1,6 +1,6 @@
FROM node:24-alpine AS web-build FROM node:24-alpine AS web-build
COPY ui ui COPY ui ui
RUN cd ui && ls -la && if [ -d "dist" ]; then exit 0; fi && \ RUN cd ui && \
npm install --prefer-offline --no-audit && \ npm install --prefer-offline --no-audit && \
npm install -D concurrently && \ npm install -D concurrently && \
NODE_OPTIONS="--max-old-space-size=4096" npx concurrently "npm run build" "npm run build-chat" && \ NODE_OPTIONS="--max-old-space-size=4096" npx concurrently "npm run build" "npm run build-chat" && \

View File

@ -421,29 +421,16 @@ const uploadFile = async (file: any, fileList: any) => {
uploadAudioList.value.length + uploadAudioList.value.length +
uploadVideoList.value.length + uploadVideoList.value.length +
uploadOtherList.value.length uploadOtherList.value.length
if (file_limit_once >= maxFiles) { if (file_limit_once >= maxFiles) {
MsgWarning(t('chat.uploadFile.limitMessage1') + maxFiles + t('chat.uploadFile.limitMessage2')) MsgWarning(t('chat.uploadFile.limitMessage1') + maxFiles + t('chat.uploadFile.limitMessage2'))
fileList.splice(0, fileList.length, ...fileList.slice(0, maxFiles)) fileList.splice(0, fileList.length, ...fileList.slice(0, maxFiles))
return return
} }
console.log(fileList)
if (fileList.filter((f: any) => f.size == 0).length > 0) {
// MB
MsgWarning(t('chat.uploadFile.sizeLimit2') + fileLimit + 'MB')
//
fileList.splice(0, fileList.length, ...fileList.filter((f: any) => f.size > 0))
return
}
if (fileList.filter((f: any) => f.size > fileLimit * 1024 * 1024).length > 0) { if (fileList.filter((f: any) => f.size > fileLimit * 1024 * 1024).length > 0) {
// MB // MB
MsgWarning(t('chat.uploadFile.sizeLimit') + fileLimit + 'MB') MsgWarning(t('chat.uploadFile.sizeLimit') + fileLimit + 'MB')
// //
fileList.splice( fileList.splice(0, fileList.length, ...fileList.filter((f: any) => f.size <= fileLimit * 1024 * 1024))
0,
fileList.length,
...fileList.filter((f: any) => f.size <= fileLimit * 1024 * 1024),
)
return return
} }
const inner = reactive(file) const inner = reactive(file)

View File

@ -1,6 +1,6 @@
<template> <template>
<el-upload <el-upload
style="width: 100%" style="width: 80%"
v-loading="loading" v-loading="loading"
action="#" action="#"
v-bind="$attrs" v-bind="$attrs"
@ -10,26 +10,26 @@
multiple multiple
> >
<el-button type="primary">{{ $t('chat.uploadFile.label') }}</el-button> <el-button type="primary">{{ $t('chat.uploadFile.label') }}</el-button>
<template #file="{ file }"> <template #file="{ file, index }"
<el-card style="--el-card-padding: 0" shadow="never"> ><el-card style="--el-card-padding: 0" shadow="never">
<div <div
class="flex-between"
:class="[inputDisabled ? 'is-disabled' : '']" :class="[inputDisabled ? 'is-disabled' : '']"
style="padding: 0 8px 0 8px" style="
padding: 0 8px 0 8px;
display: flex;
justify-content: space-between;
align-items: center;
align-content: center;
"
> >
<div class="flex align-center" style="width: 70%"> <el-tooltip class="box-item" effect="dark" :content="file.name" placement="top-start">
<img :src="getImgUrl(file && file?.name)" alt="" width="24" class="mr-4" /> <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 40%">
<span class="ellipsis-1" :title="file.name">
{{ file.name }} {{ file.name }}
</span> </div></el-tooltip
</div> >
<div class="flex align-center">
<div>{{ formatSize(file.size) }}</div>
<el-button link class="ml-8" @click="deleteFile(file)"> <div>{{ formatSize(file.size) }}</div>
<AppIcon iconName="app-delete"></AppIcon> <el-icon @click="deleteFile(file)" style="cursor: pointer"><DeleteFilled /></el-icon>
</el-button>
</div>
</div> </div>
</el-card> </el-card>
</template> </template>
@ -39,7 +39,6 @@
import { computed, inject, ref, useAttrs } from 'vue' import { computed, inject, ref, useAttrs } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormField } from '@/components/dynamics-form/type' import type { FormField } from '@/components/dynamics-form/type'
import { getImgUrl } from '@/utils/common'
import { t } from '@/locales' import { t } from '@/locales'
import { useFormDisabled } from 'element-plus' import { useFormDisabled } from 'element-plus'
const inputDisabled = useFormDisabled() const inputDisabled = useFormDisabled()
@ -72,7 +71,7 @@ const deleteFile = (file: any) => {
const model_value = computed({ const model_value = computed({
get: () => { get: () => {
if (!model_value.value) { if (!model_value) {
emit('update:modelValue', []) emit('update:modelValue', [])
} }
return props.modelValue return props.modelValue

View File

@ -66,7 +66,6 @@ export default {
limitMessage1: 'You can upload up to', limitMessage1: 'You can upload up to',
limitMessage2: 'files', limitMessage2: 'files',
sizeLimit: 'Each file must not exceed', sizeLimit: 'Each file must not exceed',
sizeLimit2: 'Empty files are not supported for upload',
imageMessage: 'Please process the image content', imageMessage: 'Please process the image content',
fileMessage: 'Please process the file content', fileMessage: 'Please process the file content',
errorMessage: 'Upload Failed', errorMessage: 'Upload Failed',

View File

@ -3,10 +3,8 @@ export default {
syncUsers: 'Sync Users', syncUsers: 'Sync Users',
syncUsersTip: 'Only sync newly added users', syncUsersTip: 'Only sync newly added users',
setUserGroups: 'Configure User Groups', setUserGroups: 'Configure User Groups',
knowledgeTitleTip: knowledgeTitleTip: 'This configuration will only take effect after enabling chat user login authentication in the associated application',
'This configuration will only take effect after enabling chat user login authentication in the associated application', applicationTitleTip: 'This configuration requires login authentication to be enabled in the application',
applicationTitleTip:
'This configuration requires login authentication to be enabled in the application',
autoAuthorization: 'Auto Authorization', autoAuthorization: 'Auto Authorization',
authorization: 'Authorization', authorization: 'Authorization',
batchDeleteUser: 'Delete selected {count} users?', batchDeleteUser: 'Delete selected {count} users?',
@ -16,12 +14,10 @@ export default {
group: { group: {
title: 'User Groups', title: 'User Groups',
name: 'User Group Name', name: 'User Group Name',
requiredMessage: 'Please select user group',
usernameOrName: 'Username/Name', usernameOrName: 'Username/Name',
delete: { delete: {
confirmTitle: 'Confirm to delete user group:', confirmTitle: 'Confirm to delete user group:',
confirmMessage: confirmMessage: 'All members in this group will be removed after deletion. Proceed with caution!',
'All members in this group will be removed after deletion. Proceed with caution!',
}, },
batchDeleteMember: 'Remove selected {count} members?', batchDeleteMember: 'Remove selected {count} members?',
}, },
@ -29,5 +25,5 @@ export default {
title: 'Successfully synced {count} users', title: 'Successfully synced {count} users',
usernameExist: 'The following usernames already exist:', usernameExist: 'The following usernames already exist:',
nicknameExist: 'The following nicknames already exist:', nicknameExist: 'The following nicknames already exist:',
}, }
} }

View File

@ -3,11 +3,8 @@ export default {
all: 'All', all: 'All',
createTool: 'Create Tool', createTool: 'Create Tool',
editTool: 'Edit Tool', editTool: 'Edit Tool',
createMcpTool: 'Create MCP',
editMcpTool: 'Edit MCP',
copyTool: 'Copy Tool', copyTool: 'Copy Tool',
importTool: 'Import Tool', importTool: 'Import Tool',
settingTool: 'Set Tool',
toolStore: { toolStore: {
title: 'Tool Store', title: 'Tool Store',
createFromToolStore: 'Create from Tool Store', createFromToolStore: 'Create from Tool Store',

View File

@ -68,7 +68,6 @@ export default {
limitMessage1: '最多上传', limitMessage1: '最多上传',
limitMessage2: '个文件', limitMessage2: '个文件',
sizeLimit: '单个文件大小不能超过', sizeLimit: '单个文件大小不能超过',
sizeLimit2: '空文件不支持上传',
imageMessage: '请解析图片内容', imageMessage: '请解析图片内容',
fileMessage: '请解析文件内容', fileMessage: '请解析文件内容',
errorMessage: '上传失败', errorMessage: '上传失败',

View File

@ -13,7 +13,6 @@ export default {
replace: '替换', replace: '替换',
group: { group: {
title: '用户组', title: '用户组',
requiredMessage: '请选择用户组',
name: '用户组名称', name: '用户组名称',
usernameOrName: '用户名/姓名', usernameOrName: '用户名/姓名',
delete: { delete: {
@ -26,5 +25,5 @@ export default {
title: '成功同步 {count} 个用户', title: '成功同步 {count} 个用户',
usernameExist: '以下用户名已存在:', usernameExist: '以下用户名已存在:',
nicknameExist: '以下姓名已存在:', nicknameExist: '以下姓名已存在:',
}, }
} }

View File

@ -7,7 +7,6 @@ export default {
editMcpTool: '编辑MCP', editMcpTool: '编辑MCP',
copyTool: '复制工具', copyTool: '复制工具',
importTool: '导入工具', importTool: '导入工具',
settingTool: '设置工具',
toolStore: { toolStore: {
title: '工具商店', title: '工具商店',
createFromToolStore: '从工具商店创建', createFromToolStore: '从工具商店创建',

View File

@ -64,7 +64,6 @@ export default {
limitMessage1: '最多上傳', limitMessage1: '最多上傳',
limitMessage2: '個文件', limitMessage2: '個文件',
sizeLimit: '單個文件大小不能超過', sizeLimit: '單個文件大小不能超過',
sizeLimit2: '空文件不支持上傳',
imageMessage: '請解析圖片內容', imageMessage: '請解析圖片內容',
fileMessage: '請解析文件內容', fileMessage: '請解析文件內容',
errorMessage: '上傳失敗', errorMessage: '上傳失敗',

View File

@ -13,7 +13,6 @@ export default {
replace: '替換', replace: '替換',
group: { group: {
title: '用戶組', title: '用戶組',
requiredMessage: '請選擇用戶組',
name: '用戶組名稱', name: '用戶組名稱',
usernameOrName: '用戶名/姓名', usernameOrName: '用戶名/姓名',
delete: { delete: {
@ -26,5 +25,5 @@ export default {
title: '成功同步 {count} 個用戶', title: '成功同步 {count} 個用戶',
usernameExist: '以下用戶名已存在:', usernameExist: '以下用戶名已存在:',
nicknameExist: '以下姓名已存在:', nicknameExist: '以下姓名已存在:',
}, }
} }

View File

@ -3,11 +3,8 @@ export default {
all: '全部', all: '全部',
createTool: '建立工具', createTool: '建立工具',
editTool: '編輯工具', editTool: '編輯工具',
createMcpTool: '建立MCP',
editMcpTool: '編輯MCP',
copyTool: '複製工具', copyTool: '複製工具',
importTool: '匯入工具', importTool: '匯入工具',
settingTool: '設定工具',
toolStore: { toolStore: {
title: '工具商店', title: '工具商店',
createFromToolStore: '從工具商店創建', createFromToolStore: '從工具商店創建',

View File

@ -56,7 +56,7 @@ div:focus {
} }
ul { ul {
list-style: none; list-style: circle;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }

View File

@ -287,13 +287,3 @@
.el-input { .el-input {
--el-input-text-color: var(--el-text-color-primary); --el-input-text-color: var(--el-text-color-primary);
} }
.el-input-group__prepend div.el-select .el-select__wrapper {
background: #ffffff;
&:hover {
background: #ffffff;
}
.el-select__placeholder {
color: var(--el-text-color-regular);
}
}

View File

@ -21,9 +21,6 @@
border: 0 !important; border: 0 !important;
max-width: 360px !important; max-width: 360px !important;
} }
ul {
list-style: circle;
}
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {

View File

@ -17,7 +17,7 @@
@submit.prevent @submit.prevent
> >
<el-form-item> <el-form-item>
<el-radio-group v-model="form.mcp_source" @change="mcpSourceChange"> <el-radio-group v-model="form.mcp_source">
<el-radio value="referencing"> <el-radio value="referencing">
{{ $t('views.applicationWorkflow.nodes.mcpNode.reference') }} {{ $t('views.applicationWorkflow.nodes.mcpNode.reference') }}
</el-radio> </el-radio>
@ -100,8 +100,17 @@ import { computed, inject, onMounted, ref, watch } from 'vue'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api.ts' import { loadSharedApi } from '@/utils/dynamics-api/shared-api.ts'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const getApplicationDetail = inject('getApplicationDetail') as any
const applicationDetail = getApplicationDetail()
const emit = defineEmits(['refresh']) const emit = defineEmits(['refresh'])
const route = useRoute()
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
const paramFormRef = ref() const paramFormRef = ref()
const mcpServerJson = `{ const mcpServerJson = `{
@ -134,20 +143,32 @@ watch(dialogVisible, (bool) => {
} }
}) })
function mcpSourceChange() { function getMcpToolSelectOptions() {
if (form.value.mcp_source === 'referencing') { const obj =
form.value.mcp_servers = '' apiType.value === 'systemManage'
} else { ? {
form.value.mcp_tool_id = '' scope: 'WORKSPACE',
} tool_type: 'MCP',
workspace_id: applicationDetail.value?.workspace_id,
}
: {
scope: 'WORKSPACE',
tool_type: 'MCP',
}
loadSharedApi({ type: 'tool', systemType: apiType.value })
.getAllToolList(obj, loading)
.then((res: any) => {
mcpToolSelectOptions.value = [...res.data.shared_tools, ...res.data.tools].filter(
(item: any) => item.is_active,
)
})
} }
const open = (data: any) => {
const open = (data: any, selectOptions: any) => {
form.value = { ...form.value, ...data } form.value = { ...form.value, ...data }
form.value.mcp_source = data.mcp_source || 'referencing' form.value.mcp_source = data.mcp_source || 'referencing'
dialogVisible.value = true dialogVisible.value = true
mcpToolSelectOptions.value = selectOptions || []
} }
const submit = () => { const submit = () => {
@ -159,6 +180,10 @@ const submit = () => {
}) })
} }
onMounted(() => {
getMcpToolSelectOptions()
})
defineExpose({ open }) defineExpose({ open })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -1,227 +1,89 @@
<template> <template>
<el-dialog <el-dialog
v-model="dialogVisible"
width="1000"
append-to-body
class="addTool-dialog"
align-center align-center
:title="$t('common.setting')"
v-model="dialogVisible"
style="width: 550px"
append-to-body
:close-on-click-modal="false" :close-on-click-modal="false"
:close-on-press-escape="false" :close-on-press-escape="false"
> >
<template #header="{ titleId, titleClass }"> <el-form
<div class="flex-between mb-8"> label-position="top"
<div class="flex"> ref="paramFormRef"
<h4 :id="titleId" :class="titleClass" class="mr-8"> :model="form"
{{ $t('views.tool.settingTool') }} require-asterisk-position="right"
</h4> >
</div> <el-form-item>
<el-select v-model="form.tool_ids" filterable multiple>
<el-button link class="mr-24" @click="refresh"> <el-option
<el-icon :size="18"><Refresh /></el-icon> v-for="mcpTool in toolSelectOptions"
</el-button> :key="mcpTool.id"
</div> :label="mcpTool.name"
</template> :value="mcpTool.id"
<LayoutContainer class="application-manage"> >
<template #left> <span>{{ mcpTool.name }}</span>
<div class="p-8"> <el-tag v-if="mcpTool.scope === 'SHARED'" type="info" class="info-tag ml-8 mt-4">
<folder-tree {{ $t('views.shared.title') }}
:data="folderList" </el-tag>
:currentNodeKey="currentFolder?.id" </el-option>
@handleNodeClick="folderClickHandle" </el-select>
v-loading="folderLoading" </el-form-item>
:canOperation="false" </el-form>
showShared
:shareTitle="$t('views.shared.shared_tool')"
:treeStyle="{ height: 'calc(100vh - 240px)' }"
/>
</div>
</template>
<div class="layout-bg">
<div class="flex-between p-16 ml-8">
<h4>{{ currentFolder?.name }}</h4>
<el-input
v-model="searchValue"
:placeholder="$t('common.search')"
prefix-icon="Search"
class="w-240 mr-8"
clearable
/>
</div>
<el-scrollbar>
<div class="p-16-24 pt-0" style="height: calc(100vh - 200px)">
<el-row :gutter="12" v-loading="apiLoading" v-if="searchData.length">
<el-col :span="12" v-for="(item, index) in searchData" :key="index" class="mb-16">
<CardCheckbox
value-field="id"
:data="item"
v-model="checkList"
@change="changeHandle"
>
<template #icon>
<el-avatar
v-if="item?.icon"
shape="square"
:size="32"
style="background: none"
class="mr-8"
>
<img :src="resetUrl(item?.icon)" alt="" />
</el-avatar>
<ToolIcon v-else :size="32" :type="item?.tool_type" />
</template>
<span class="ellipsis cursor ml-12" :title="item.name"> {{ item.name }}</span>
</CardCheckbox>
</el-col>
</el-row>
<el-empty :description="$t('common.noData')" v-else />
</div>
</el-scrollbar>
</div>
</LayoutContainer>
<template #footer> <template #footer>
<div class="flex-between"> <span class="dialog-footer">
<div class="flex"> <el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-text type="info" class="color-secondary mr-8" v-if="checkList.length > 0"> <el-button type="primary" @click="submit()" :loading="loading">
{{ $t('common.selected') }} {{ checkList.length }} {{ $t('common.save') }}
</el-text> </el-button>
<el-button link type="primary" v-if="checkList.length > 0" @click="clearCheck"> </span>
{{ $t('common.clear') }}
</el-button>
</div>
<span>
<el-button @click.prevent="dialogVisible = false">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submitHandle">
{{ $t('common.add') }}
</el-button>
</span>
</div>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import {ref, watch} from 'vue'
import { useRoute } from 'vue-router'
import useStore from '@/stores'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { uniqueArray } from '@/utils/array'
import { resetUrl } from '@/utils/common'
const route = useRoute()
const emit = defineEmits(['refresh']) const emit = defineEmits(['refresh'])
const { folder, user } = useStore()
const apiType = computed(() => { const paramFormRef = ref()
if (route.path.includes('shared')) {
return 'systemShare' const form = ref<any>({
} else if (route.path.includes('resource-management')) { tool_ids: [],
return 'systemManage'
} else {
return 'workspace'
}
}) })
const toolSelectOptions = ref<any[]>([])
const dialogVisible = ref<boolean>(false) const dialogVisible = ref<boolean>(false)
const checkList = ref<Array<string>>([])
const searchValue = ref('') const loading = ref(false)
const searchData = ref<Array<any>>([])
const toolList = ref<Array<any>>([])
const apiLoading = ref(false)
watch(dialogVisible, (bool) => { watch(dialogVisible, (bool) => {
if (!bool) { if (!bool) {
checkList.value = [] form.value = {
searchValue.value = '' tool_ids: [],
searchData.value = [] }
toolList.value = []
} }
}) })
watch(searchValue, (val) => {
if (val) {
searchData.value = toolList.value.filter((v) => v.name.includes(val))
} else {
searchData.value = toolList.value
}
})
function changeHandle() {} const open = (data: any, selectOptions: any) => {
function clearCheck() { form.value = {...form.value, ...data}
checkList.value = []
}
const open = (checked: any) => {
checkList.value = checked || []
getFolder()
dialogVisible.value = true dialogVisible.value = true
toolSelectOptions.value = selectOptions
} }
const submitHandle = () => { const submit = () => {
emit('refresh', { paramFormRef.value.validate().then((valid: any) => {
tool_ids: checkList.value, if (valid) {
}) emit('refresh', form.value)
dialogVisible.value = false dialogVisible.value = false
} }
const refresh = () => {
searchValue.value = ''
toolList.value = []
getList()
}
const folderList = ref<any[]>([])
const currentFolder = ref<any>({})
const folderLoading = ref(false)
//
function folderClickHandle(row: any) {
if (row.id === currentFolder.value?.id) {
return
}
currentFolder.value = row
getList()
}
function getFolder() {
const params = {}
folder.asyncGetFolder('TOOL', params, folderLoading).then((res: any) => {
folderList.value = res.data
currentFolder.value = res.data?.[0] || {}
getList()
}) })
} }
function getList() {
const folder_id = currentFolder.value?.id || user.getWorkspaceId()
loadSharedApi({
type: 'tool',
systemType: apiType.value,
})
.getToolList({ folder_id }, apiLoading)
.then((res: any) => {
toolList.value = uniqueArray([...toolList.value, ...res.data.tools], 'id')
searchData.value = res.data.tools
})
}
defineExpose({ open })
defineExpose({open})
</script> </script>
<style lang="scss"> <style lang="scss" scoped></style>
.addTool-dialog {
padding: 0;
.el-dialog__header {
padding: 12px 20px 4px 24px;
border-bottom: 1px solid var(--el-border-color-light);
}
.el-dialog__footer {
padding: 12px 24px 12px 24px;
border-top: 1px solid var(--el-border-color-light);
}
.el-dialog__headerbtn {
top: 2px;
right: 6px;
}
}
</style>

View File

@ -22,7 +22,7 @@
</div> </div>
</div> </div>
<el-scrollbar> <el-scrollbar>
<div :style="{ height: user.isExpire() ? 'calc(100vh - 340px)' : 'calc(100vh - 300px)' }"> <div class="hit-test-height">
<el-empty <el-empty
v-if="first" v-if="first"
:image="emptyImg" :image="emptyImg"
@ -231,7 +231,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, ref, onMounted, computed } from 'vue' import { nextTick, ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import useStore from '@/stores'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import ParagraphDialog from '@/views/paragraph/component/ParagraphDialog.vue' import ParagraphDialog from '@/views/paragraph/component/ParagraphDialog.vue'
import { arraySort } from '@/utils/array' import { arraySort } from '@/utils/array'
@ -242,7 +241,6 @@ const route = useRoute()
const { const {
params: { id }, params: { id },
} = route as any } = route as any
const { user } = useStore()
const apiType = computed(() => { const apiType = computed(() => {
if (route.path.includes('shared')) { if (route.path.includes('shared')) {
return 'systemShare' return 'systemShare'
@ -410,6 +408,10 @@ onMounted(() => {})
position: absolute; position: absolute;
right: calc(var(--app-base-px) * 3); right: calc(var(--app-base-px) * 3);
} }
.hit-test-height {
height: calc(100vh - 300px);
}
.document-card { .document-card {
height: 210px; height: 210px;
border: 1px solid var(--app-layout-bg-color); border: 1px solid var(--app-layout-bg-color);

View File

@ -160,7 +160,7 @@
</div> </div>
</LayoutContainer> </LayoutContainer>
<div class="mul-operation border-t w-full flex align-center" v-if="isBatch === true"> <div class="mul-operation border-t w-full" v-if="isBatch === true">
<el-button :disabled="multipleSelection.length === 0" @click="openGenerateDialog()"> <el-button :disabled="multipleSelection.length === 0" @click="openGenerateDialog()">
{{ $t('views.document.generateQuestion.title') }} {{ $t('views.document.generateQuestion.title') }}
</el-button> </el-button>
@ -171,7 +171,7 @@
<el-button :disabled="multipleSelection.length === 0" @click="deleteMulParagraph"> <el-button :disabled="multipleSelection.length === 0" @click="deleteMulParagraph">
{{ $t('common.delete') }} {{ $t('common.delete') }}
</el-button> </el-button>
<span class="ml-24"> <span class="ml-8">
{{ $t('common.selected') }} {{ multipleSelection.length }} {{ $t('common.selected') }} {{ multipleSelection.length }}
{{ $t('views.document.items') }} {{ $t('views.document.items') }}
</span> </span>

View File

@ -51,6 +51,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch } from 'vue' import { ref, reactive, watch } from 'vue'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import chatUserApi from '@/api/system/chat-user'
import userManageApi from '@/api/system/user-manage' import userManageApi from '@/api/system/user-manage'
import { MsgSuccess } from '@/utils/message' import { MsgSuccess } from '@/utils/message'
import { t } from '@/locales' import { t } from '@/locales'
@ -110,14 +111,6 @@ const rules = reactive({
trigger: 'blur', trigger: 'blur',
}, },
], ],
user_group_ids: [
{
type: 'array',
required: true,
message: t('views.chatUser.group.requiredMessage'),
trigger: 'change',
},
],
}) })
const visible = ref<boolean>(false) const visible = ref<boolean>(false)
const loading = ref(false) const loading = ref(false)

View File

@ -235,7 +235,7 @@
{{ $t('common.edit') }} {{ $t('common.edit') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="!item.template_id && permissionPrecise.copy(item.id) && item.tool_type!== 'MCP'" v-if="!item.template_id && permissionPrecise.copy(item.id)"
@click.stop="copyTool(item)" @click.stop="copyTool(item)"
> >
<AppIcon iconName="app-copy" class="color-secondary"></AppIcon> <AppIcon iconName="app-copy" class="color-secondary"></AppIcon>
@ -275,7 +275,7 @@
{{ $t('views.shared.authorized_workspace') }}</el-dropdown-item {{ $t('views.shared.authorized_workspace') }}</el-dropdown-item
> >
<el-dropdown-item <el-dropdown-item
v-if="!item.template_id && permissionPrecise.export(item.id) && item.tool_type!== 'MCP'" v-if="!item.template_id && permissionPrecise.export(item.id)"
@click.stop="exportTool(item)" @click.stop="exportTool(item)"
> >
<AppIcon iconName="app-export" class="color-secondary"></AppIcon> <AppIcon iconName="app-export" class="color-secondary"></AppIcon>

View File

@ -114,7 +114,6 @@
/> />
</el-form-item> </el-form-item>
<!-- MCP-->
<div class="flex-between mb-16"> <div class="flex-between mb-16">
<div class="lighter">MCP</div> <div class="lighter">MCP</div>
<div> <div>
@ -130,24 +129,6 @@
<el-switch size="small" v-model="chat_data.mcp_enable" /> <el-switch size="small" v-model="chat_data.mcp_enable" />
</div> </div>
</div> </div>
<div class="w-full" v-if="
(chat_data.mcp_tool_id) ||
(chat_data.mcp_servers && chat_data.mcp_servers.length > 0)"
>
<div class="flex-between border border-r-6 white-bg mb-4" style="padding: 5px 8px">
<div class="flex align-center" style="line-height: 20px">
<ToolIcon type="MCP" class="mr-8" :size="20" />
<div class="ellipsis" :title="relatedObject(toolSelectOptions, chat_data.mcp_tool_id, 'id')?.name">
{{ relatedObject(mcpToolSelectOptions, chat_data.mcp_tool_id, 'id')?.name || $t('common.custom') + ' MCP' }}
</div>
</div>
<el-button text @click="chat_data.mcp_tool_id = ''">
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
<!-- 工具 -->
<div class="flex-between mb-16"> <div class="flex-between mb-16">
<div class="lighter">{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</div> <div class="lighter">{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</div>
<div> <div>
@ -163,22 +144,6 @@
<el-switch size="small" v-model="chat_data.tool_enable" /> <el-switch size="small" v-model="chat_data.tool_enable" />
</div> </div>
</div> </div>
<div class="w-full" v-if="chat_data.tool_ids?.length > 0">
<template v-for="(item, index) in chat_data.tool_ids" :key="index">
<div class="flex-between border border-r-6 white-bg mb-4" style="padding: 5px 8px">
<div class="flex align-center" style="line-height: 20px">
<ToolIcon type="CUSTOM" class="mr-8" :size="20" />
<div class="ellipsis" :title="relatedObject(toolSelectOptions, item, 'id')?.name">
{{ relatedObject(toolSelectOptions, item, 'id')?.name }}
</div>
</div>
<el-button text @click="removeTool(item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
</template>
</div>
<el-form-item @click.prevent> <el-form-item @click.prevent>
<template #label> <template #label>
@ -241,7 +206,6 @@ import McpServersDialog from '@/views/application/component/McpServersDialog.vue
import { loadSharedApi } from '@/utils/dynamics-api/shared-api' import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import ToolDialog from '@/views/application/component/ToolDialog.vue' import ToolDialog from '@/views/application/component/ToolDialog.vue'
import {relatedObject} from "@/utils/array.ts";
const getApplicationDetail = inject('getApplicationDetail') as any const getApplicationDetail = inject('getApplicationDetail') as any
const route = useRoute() const route = useRoute()
@ -283,6 +247,7 @@ const model_change = (model_id?: string) => {
} }
} }
// @ts-ignore
const defaultPrompt = `${t('views.applicationWorkflow.nodes.aiChatNode.defaultPrompt')} const defaultPrompt = `${t('views.applicationWorkflow.nodes.aiChatNode.defaultPrompt')}
{{${t('views.applicationWorkflow.nodes.searchKnowledgeNode.label')}.data}} {{${t('views.applicationWorkflow.nodes.searchKnowledgeNode.label')}.data}}
${t('views.problem.title')} ${t('views.problem.title')}
@ -388,7 +353,7 @@ function openMcpServersDialog() {
mcp_tool_id: chat_data.value.mcp_tool_id, mcp_tool_id: chat_data.value.mcp_tool_id,
mcp_source: chat_data.value.mcp_source, mcp_source: chat_data.value.mcp_source,
} }
mcpServersDialogRef.value.open(config, mcpToolSelectOptions.value) mcpServersDialogRef.value.open(config)
} }
function submitMcpServersDialog(config: any) { function submitMcpServersDialog(config: any) {
@ -399,15 +364,14 @@ function submitMcpServersDialog(config: any) {
const toolDialogRef = ref() const toolDialogRef = ref()
function openToolDialog() { function openToolDialog() {
toolDialogRef.value.open(chat_data.value.tool_ids) const config = {
tool_ids: chat_data.value.tool_ids,
}
toolDialogRef.value.open(config, toolSelectOptions.value)
} }
function submitToolDialog(config: any) { function submitToolDialog(config: any) {
set(props.nodeModel.properties.node_data, 'tool_ids', config.tool_ids) set(props.nodeModel.properties.node_data, 'tool_ids', config.tool_ids)
} }
function removeTool(id: any) {
const list = props.nodeModel.properties.node_data.tool_ids.filter((v: any) => v !== id)
set(props.nodeModel.properties.node_data, 'tool_ids', list)
}
const toolSelectOptions = ref<any[]>([]) const toolSelectOptions = ref<any[]>([])
function getToolSelectOptions() { function getToolSelectOptions() {
@ -432,29 +396,6 @@ function getToolSelectOptions() {
}) })
} }
const mcpToolSelectOptions = ref<any[]>([])
function getMcpToolSelectOptions() {
const obj =
apiType.value === 'systemManage'
? {
scope: 'WORKSPACE',
tool_type: 'MCP',
workspace_id: application.value?.workspace_id,
}
: {
scope: 'WORKSPACE',
tool_type: 'MCP',
}
loadSharedApi({ type: 'tool', systemType: apiType.value })
.getAllToolList(obj)
.then((res: any) => {
mcpToolSelectOptions.value = [...res.data.shared_tools, ...res.data.tools].filter(
(item: any) => item.is_active,
)
})
}
onMounted(() => { onMounted(() => {
getSelectModel() getSelectModel()
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') { if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
@ -468,7 +409,6 @@ onMounted(() => {
} }
getToolSelectOptions() getToolSelectOptions()
getMcpToolSelectOptions()
}) })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>