feat: add application enable flag and application IDs to application model

This commit is contained in:
CaptainB 2025-12-16 16:03:46 +08:00
parent fe004c235c
commit 54e16acee6
13 changed files with 581 additions and 35 deletions

View File

@ -88,6 +88,8 @@ class IChatStep(IBaseChatPipelineStep):
mcp_source = serializers.CharField(label="MCP Source", required=False, default="referencing")
tool_enable = serializers.BooleanField(label="工具是否启用", required=False, default=False)
tool_ids = serializers.JSONField(label="工具ID列表", required=False, default=list)
application_enable = serializers.BooleanField(label="应用是否启用", required=False, default=False)
application_ids = serializers.JSONField(label="应用ID列表", required=False, default=list)
mcp_output_enable = serializers.BooleanField(label="MCP输出是否启用", required=False, default=True)
def is_valid(self, *, raise_exception=False):
@ -115,6 +117,6 @@ class IChatStep(IBaseChatPipelineStep):
padding_problem_text: str = None, stream: bool = True, chat_user_id=None, chat_user_type=None,
no_references_setting=None, model_params_setting=None, model_setting=None,
mcp_enable=False, mcp_tool_ids=None, mcp_servers='', mcp_source="referencing",
tool_enable=False, tool_ids=None, mcp_output_enable=True,
tool_enable=False, tool_ids=None, application_enable=None, application_ids=None, mcp_output_enable=True,
**kwargs):
pass

View File

@ -7,12 +7,11 @@
@desc: 对话step Base实现
"""
import json
import os
import time
import traceback
import uuid_utils.compat as uuid
from typing import List
import uuid_utils.compat as uuid
from django.db.models import QuerySet
from django.http import StreamingHttpResponse
from django.utils.translation import gettext as _
@ -26,11 +25,10 @@ from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineMode
from application.chat_pipeline.pipeline_manage import PipelineManage
from application.chat_pipeline.step.chat_step.i_chat_step import IChatStep, PostResponseHandler
from application.flow.tools import Reasoning, mcp_response_generator
from application.models import ApplicationChatUserStats, ChatUserType
from application.models import ApplicationChatUserStats, ChatUserType, Application, ApplicationApiKey
from common.utils.logger import maxkb_logger
from common.utils.rsa_util import rsa_long_decrypt
from common.utils.tool_code import ToolExecutor
from maxkb.const import CONFIG
from models_provider.tools import get_model_instance_by_model_workspace_id
from tools.models import Tool
@ -59,7 +57,6 @@ def write_context(step, manage, request_token, response_token, all_text):
manage.context['answer_tokens'] = manage.context['answer_tokens'] + response_token
def event_content(response,
chat_id,
chat_record_id,
@ -182,6 +179,8 @@ class BaseChatStep(IChatStep):
mcp_source="referencing",
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True,
**kwargs):
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id,
@ -193,6 +192,7 @@ class BaseChatStep(IChatStep):
no_references_setting,
model_setting,
mcp_enable, mcp_tool_ids, mcp_servers, mcp_source, tool_enable, tool_ids,
application_enable, application_ids,
mcp_output_enable)
else:
return self.execute_block(message_list, chat_id, problem_text, post_response_handler, chat_model,
@ -200,6 +200,7 @@ class BaseChatStep(IChatStep):
manage, padding_problem_text, chat_user_id, chat_user_type, no_references_setting,
model_setting,
mcp_enable, mcp_tool_ids, mcp_servers, mcp_source, tool_enable, tool_ids,
application_enable, application_ids,
mcp_output_enable)
def get_details(self, manage, **kwargs):
@ -225,8 +226,9 @@ class BaseChatStep(IChatStep):
return result
def _handle_mcp_request(self, mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_ids, tool_ids,
application_enable, application_ids,
mcp_output_enable, chat_model, message_list):
if not mcp_enable and not tool_enable:
if not mcp_enable and not tool_enable and not application_enable:
return None
mcp_servers_config = {}
@ -258,10 +260,25 @@ class BaseChatStep(IChatStep):
params = json.loads(rsa_long_decrypt(tool.init_params))
else:
params = {}
tool_config = executor.get_tool_mcp_config(tool.code, params)
tool_config = executor.get_tool_mcp_config(tool.code, params, tool.name, tool.desc)
mcp_servers_config[str(tool.id)] = tool_config
if application_enable:
if application_ids and len(application_ids) > 0:
self.context['application_ids'] = application_ids
for application_id in application_ids:
app = QuerySet(Application).filter(id=application_id).first()
app_key = QuerySet(ApplicationApiKey).filter(application_id=application_id, is_active=True).first()
# TODO 处理api
if app_key is not None:
api_key = app_key.secret_key
else:
continue
executor = ToolExecutor()
app_config = executor.get_app_mcp_config(api_key, app.name, app.desc)
mcp_servers_config[str(app.id)] = app_config
if len(mcp_servers_config) > 0:
return mcp_response_generator(chat_model, message_list, json.dumps(mcp_servers_config), mcp_output_enable)
@ -278,6 +295,8 @@ class BaseChatStep(IChatStep):
mcp_source="referencing",
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True):
if paragraph_list is None:
paragraph_list = []
@ -296,7 +315,8 @@ class BaseChatStep(IChatStep):
else:
# 处理 MCP 请求
mcp_result = self._handle_mcp_request(
mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_ids, tool_ids, mcp_output_enable, chat_model,
mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_ids, tool_ids, application_enable,
application_ids, mcp_output_enable, chat_model,
message_list,
)
if mcp_result:
@ -320,10 +340,13 @@ class BaseChatStep(IChatStep):
mcp_source="referencing",
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True):
chat_result, is_ai_chat = self.get_stream_result(message_list, chat_model, paragraph_list,
no_references_setting, problem_text, mcp_enable, mcp_tool_ids,
mcp_servers, mcp_source, tool_enable, tool_ids,
application_enable, application_ids,
mcp_output_enable)
chat_record_id = uuid.uuid7()
r = StreamingHttpResponse(
@ -347,6 +370,8 @@ class BaseChatStep(IChatStep):
mcp_source="referencing",
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True
):
if paragraph_list is None:
@ -365,7 +390,8 @@ class BaseChatStep(IChatStep):
else:
# 处理 MCP 请求
mcp_result = self._handle_mcp_request(
mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_ids, tool_ids, mcp_output_enable,
mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_ids, tool_ids, application_enable,
application_ids, mcp_output_enable,
chat_model, message_list,
)
if mcp_result:
@ -388,6 +414,8 @@ class BaseChatStep(IChatStep):
mcp_source="referencing",
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True):
reasoning_content_enable = model_setting.get('reasoning_content_enable', False)
reasoning_content_start = model_setting.get('reasoning_content_start', '<think>')
@ -400,7 +428,8 @@ class BaseChatStep(IChatStep):
chat_result, is_ai_chat = self.get_block_result(message_list, chat_model, paragraph_list,
no_references_setting, problem_text, mcp_enable,
mcp_tool_ids, mcp_servers, mcp_source, tool_enable,
tool_ids, mcp_output_enable)
tool_ids, application_enable, application_ids,
mcp_output_enable)
if is_ai_chat:
request_token = chat_model.get_num_tokens_from_messages(message_list)
response_token = chat_model.get_num_tokens(chat_result.content)

View File

@ -42,6 +42,9 @@ class ChatNodeSerializer(serializers.Serializer):
tool_enable = serializers.BooleanField(required=False, default=False, label=_("Whether to enable tools"))
tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
label=_("Tool IDs"), )
application_enable = serializers.BooleanField(required=False, default=False, label=_("Whether to enable apps"))
application_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
label=_("App IDs"), )
mcp_output_enable = serializers.BooleanField(required=False, default=True, label=_("Whether to enable MCP output"))
@ -73,6 +76,8 @@ class IChatNode(INode):
mcp_source=None,
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True,
**kwargs) -> NodeResult:
pass

View File

@ -7,7 +7,6 @@
@desc:
"""
import json
import os
import re
import time
from functools import reduce
@ -20,9 +19,9 @@ from langchain_core.messages import BaseMessage, AIMessage
from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
from application.flow.tools import Reasoning, mcp_response_generator
from application.models import Application
from common.utils.rsa_util import rsa_long_decrypt
from common.utils.tool_code import ToolExecutor
from maxkb.const import CONFIG
from models_provider.models import Model
from models_provider.tools import get_model_credential, get_model_instance_by_model_workspace_id
from tools.models import Tool
@ -160,6 +159,8 @@ class BaseChatNode(IChatNode):
mcp_source=None,
tool_enable=False,
tool_ids=None,
application_enable=False,
application_ids=None,
mcp_output_enable=True,
**kwargs) -> NodeResult:
if dialogue_type is None:
@ -186,7 +187,8 @@ class BaseChatNode(IChatNode):
# 处理 MCP 请求
mcp_result = self._handle_mcp_request(
mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids, mcp_output_enable,
mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids, application_enable,
application_ids, mcp_output_enable,
chat_model, message_list, history_message, question
)
if mcp_result:
@ -208,8 +210,9 @@ class BaseChatNode(IChatNode):
_write_context=write_context)
def _handle_mcp_request(self, mcp_enable, tool_enable, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
application_enable, application_ids,
mcp_output_enable, chat_model, message_list, history_message, question):
if not mcp_enable and not tool_enable:
if not mcp_enable and not tool_enable and not application_enable:
return None
mcp_servers_config = {}
@ -245,10 +248,19 @@ class BaseChatNode(IChatNode):
params = json.loads(rsa_long_decrypt(tool.init_params))
else:
params = {}
tool_config = executor.get_tool_mcp_config(tool.code, params)
tool_config = executor.get_tool_mcp_config(tool.code, params, tool.name, tool.desc)
mcp_servers_config[str(tool.id)] = tool_config
if application_enable:
if application_ids and len(application_ids) > 0:
self.context['application_ids'] = application_ids
for application_id in application_ids:
app = QuerySet(Application).filter(id=application_id).first()
executor = ToolExecutor()
app_config = executor.get_app_mcp_config(app.id, app.name, app.desc)
mcp_servers_config[str(app.id)] = app_config
if len(mcp_servers_config) > 0:
r = mcp_response_generator(chat_model, message_list, json.dumps(mcp_servers_config), mcp_output_enable)
return NodeResult(

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.9 on 2025-12-16 08:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0003_application_stt_model_params_setting_and_more'),
]
operations = [
migrations.AddField(
model_name='application',
name='application_enable',
field=models.BooleanField(default=False, verbose_name='应用是否启用'),
),
migrations.AddField(
model_name='application',
name='application_ids',
field=models.JSONField(default=list, verbose_name='应用ID列表'),
),
]

View File

@ -100,6 +100,8 @@ class Application(AppModelMixin):
mcp_source = models.CharField(verbose_name="MCP Source", max_length=20, default="referencing")
tool_enable = models.BooleanField(verbose_name="工具是否启用", default=False)
tool_ids = models.JSONField(verbose_name="工具ID列表", default=list)
application_enable = models.BooleanField(verbose_name="应用是否启用", default=False)
application_ids = models.JSONField(verbose_name="应用ID列表", default=list)
mcp_output_enable = models.BooleanField(verbose_name="MCP输出是否启用", default=True)
@staticmethod

View File

@ -857,7 +857,7 @@ class ApplicationOperateSerializer(serializers.Serializer):
'api_key_is_active', 'icon', 'work_flow', 'model_params_setting', 'tts_model_params_setting',
'stt_model_params_setting',
'mcp_enable', 'mcp_tool_ids', 'mcp_servers', 'mcp_source', 'tool_enable', 'tool_ids',
'mcp_output_enable',
'mcp_output_enable', 'application_enable', 'application_ids',
'problem_optimization_prompt', 'clean_time', 'folder_id']
for update_key in update_keys:
if update_key in instance and instance.get(update_key) is not None:

View File

@ -162,6 +162,8 @@ class ChatInfo:
'mcp_source': self.application.mcp_source,
'tool_enable': self.application.tool_enable,
'tool_ids': self.application.tool_ids,
'application_enable': self.application.application_enable,
'application_ids': self.application.application_ids,
'mcp_output_enable': self.application.mcp_output_enable,
}

View File

@ -1,33 +1,40 @@
# coding=utf-8
import ast
import base64
import getpass
import gzip
import json
import os
import pwd
import random
import resource
import socket
import subprocess
import sys
import tempfile
import pwd
import resource
import getpass
import random
import time
import uuid_utils.compat as uuid
from contextlib import contextmanager
from common.utils.logger import maxkb_logger
from textwrap import dedent
import uuid_utils.compat as uuid
from django.utils.translation import gettext_lazy as _
from common.utils.logger import maxkb_logger
from maxkb.const import BASE_DIR, CONFIG
from maxkb.const import PROJECT_DIR
from textwrap import dedent
_enable_sandbox = bool(CONFIG.get('SANDBOX', 0))
_run_user = 'sandbox' if _enable_sandbox else getpass.getuser()
_sandbox_path = CONFIG.get("SANDBOX_HOME", '/opt/maxkb-app/sandbox') if _enable_sandbox else os.path.join(PROJECT_DIR, 'data', 'sandbox')
_sandbox_path = CONFIG.get("SANDBOX_HOME", '/opt/maxkb-app/sandbox') if _enable_sandbox else os.path.join(PROJECT_DIR,
'data',
'sandbox')
_process_limit_timeout_seconds = int(CONFIG.get("SANDBOX_PYTHON_PROCESS_LIMIT_TIMEOUT_SECONDS", '3600'))
_process_limit_cpu_cores = min(max(int(CONFIG.get("SANDBOX_PYTHON_PROCESS_LIMIT_CPU_CORES", '1')), 1), len(os.sched_getaffinity(0))) if sys.platform.startswith("linux") else os.cpu_count() # 只支持linuxwindow和mac不支持
_process_limit_cpu_cores = min(max(int(CONFIG.get("SANDBOX_PYTHON_PROCESS_LIMIT_CPU_CORES", '1')), 1),
len(os.sched_getaffinity(0))) if sys.platform.startswith(
"linux") else os.cpu_count() # 只支持linuxwindow和mac不支持
_process_limit_mem_mb = int(CONFIG.get("SANDBOX_PYTHON_PROCESS_LIMIT_MEM_MB", '256'))
class ToolExecutor:
def __init__(self):
@ -124,7 +131,7 @@ sys.stdout.flush()
return result.get('data')
raise Exception(result.get('msg') + (f'\n{subprocess_result.stderr}' if subprocess_result.stderr else ''))
def _generate_mcp_server_code(self, _code, params):
def _generate_mcp_server_code(self, _code, params, name=None, description=None):
# 解析代码,提取导入语句和函数定义
try:
tree = ast.parse(_code)
@ -139,6 +146,9 @@ sys.stdout.flush()
if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
imports.append(ast.unparse(node))
elif isinstance(node, ast.FunctionDef):
if node.name.startswith('_'):
other_code.append(ast.unparse(node))
continue
# 修改函数参数以包含 params 中的默认值
arg_names = [arg.arg for arg in node.args.args]
@ -173,7 +183,7 @@ sys.stdout.flush()
node.args.defaults = defaults
func_code = ast.unparse(node)
functions.append(f"@mcp.tool()\n{func_code}\n")
functions.append(f"@mcp.tool(name='{name}', description='{description}')\n{func_code}\n")
else:
other_code.append(ast.unparse(node))
@ -187,9 +197,9 @@ sys.stdout.flush()
return "\n".join(code_parts)
def generate_mcp_server_code(self, code_str, params):
def generate_mcp_server_code(self, code_str, params, name, description):
python_paths = CONFIG.get_sandbox_python_package_paths().split(',')
code = self._generate_mcp_server_code(code_str, params)
code = self._generate_mcp_server_code(code_str, params, name, description)
set_run_user = f'os.setgid({pwd.getpwnam(_run_user).pw_gid});os.setuid({pwd.getpwnam(_run_user).pw_uid});' if _enable_sandbox else ''
return f"""
import os, sys, logging
@ -204,8 +214,8 @@ os.environ.clear()
exec({dedent(code)!a})
"""
def get_tool_mcp_config(self, code, params):
_code = self.generate_mcp_server_code(code, params)
def get_tool_mcp_config(self, code, params, name, description):
_code = self.generate_mcp_server_code(code, params, name, description)
maxkb_logger.debug(f"Python code of mcp tool: {_code}")
compressed_and_base64_encoded_code_str = base64.b64encode(gzip.compress(_code.encode())).decode()
tool_config = {
@ -222,6 +232,66 @@ exec({dedent(code)!a})
}
return tool_config
def get_app_mcp_config(self, api_key, name, description):
chat_path = CONFIG.get_chat_path()
_code = f'''
import requests
from typing import Optional
def _get_chat_id() -> Optional[str]:
url = f"http://127.0.0.1:8080/{chat_path}/api/open"
headers = {{
'accept': '*/*',
'Authorization': f'Bearer {api_key}'
}}
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
return resp.json().get("data")
except Exception as e:
raise e
def _chat_with_ai(chat_id: str, message: str) -> Optional[str]:
url = f"http://127.0.0.1:8080/{chat_path}/api/chat_message/{{chat_id}}"
headers = {{"Content-Type": "application/json", "Authorization": f'Bearer {api_key}'}}
payload = {{
"message": message,
"re_chat": False,
"stream": False
}}
try:
resp = requests.post(url, json=payload, headers=headers, timeout=600)
resp.raise_for_status()
data = resp.json()
return str(data.get("data", {{}}).get("content") or data.get("response"))
except Exception as e:
raise e
def ai_chat(message: str) -> str:
chat_id = _get_chat_id()
reply = _chat_with_ai(chat_id, message)
return reply or "AI 未能生成回复"
'''
_code = self.generate_mcp_server_code(_code, {}, name, description)
# print(_code)
maxkb_logger.debug(f"Python code of mcp app: {_code}")
compressed_and_base64_encoded_code_str = base64.b64encode(gzip.compress(_code.encode())).decode()
app_config = {
'command': sys.executable,
'args': [
'-c',
f'import base64,gzip; exec(gzip.decompress(base64.b64decode(\'{compressed_and_base64_encoded_code_str}\')).decode())',
],
'cwd': _sandbox_path,
'env': {
'LD_PRELOAD': f'{_sandbox_path}/lib/sandbox.so',
},
'transport': 'stdio',
}
return app_config
def _exec(self, execute_file):
kwargs = {'cwd': BASE_DIR, 'env': {
'LD_PRELOAD': f'{_sandbox_path}/lib/sandbox.so',
@ -255,4 +325,4 @@ def execution_timer(id=""):
try:
yield
finally:
maxkb_logger.debug(f"Tool execution({id}) takes {time.perf_counter() - start:.6f} seconds.")
maxkb_logger.debug(f"Tool execution({id}) takes {time.perf_counter() - start:.6f} seconds.")

View File

@ -34,6 +34,8 @@ interface ApplicationFormType {
mcp_source?: string
tool_enable?: boolean
tool_ids?: string[]
application_enable?: boolean
application_ids?: string[]
mcp_output_enable?: boolean
}

View File

@ -433,9 +433,73 @@
</div>
</template>
</div>
<!-- 应用 -->
<el-form-item @click.prevent v-if="toolPermissionPrecise.read()">
<template #label>
<div class="flex-between">
<span class="mr-4">
{{ $t('views.application.title') }}
</span>
<div class="flex">
<el-button
type="primary"
link
@click="openApplicationDialog"
@refreshForm="refreshParam"
v-if="applicationForm.application_enable"
>
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
<el-switch
class="ml-8"
size="small"
v-model="applicationForm.application_enable"
/>
</div>
</div>
</template>
</el-form-item>
<div
class="w-full mb-16"
v-if="applicationForm.application_ids && applicationForm.application_ids.length > 0 && toolPermissionPrecise.read()"
>
<template v-for="(item, index) in applicationForm.application_ids" :key="index">
<div
v-if="relatedObject(applicationSelectOptions, item, 'id')"
class="flex-between border border-r-6 white-bg mb-4"
style="padding: 5px 8px"
>
<div class="flex align-center" style="line-height: 20px">
<el-avatar
v-if="relatedObject(applicationSelectOptions, item, 'id')?.icon"
shape="square"
:size="20"
style="background: none"
class="mr-8"
>
<img
:src="resetUrl(relatedObject(applicationSelectOptions, item, 'id')?.icon)"
alt=""
/>
</el-avatar>
<AppIcon v-else class="mr-8" :size="20" />
<div
class="ellipsis"
:title="relatedObject(applicationSelectOptions, item, 'id')?.name"
>
{{ relatedObject(applicationSelectOptions, item, 'id')?.name }}
</div>
</div>
<el-button text @click="removeApplication(item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
</template>
</div>
<el-form-item
@click.prevent
v-if="(applicationForm.mcp_enable || applicationForm.tool_enable) && toolPermissionPrecise.read()"
v-if="(applicationForm.mcp_enable || applicationForm.tool_enable || applicationForm.application_enable) && toolPermissionPrecise.read()"
>
<template #label>
<div class="flex-between">
@ -633,6 +697,7 @@
/>
<McpServersDialog ref="mcpServersDialogRef" @refresh="submitMcpServersDialog" />
<ToolDialog ref="toolDialogRef" @refresh="submitToolDialog" />
<ApplicationDialog ref="applicationDialogRef" @refresh="submitApplicationDialog" />
</div>
</template>
<script setup lang="ts">
@ -658,11 +723,14 @@ import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { resetUrl } from '@/utils/common.ts'
import McpServersDialog from '@/views/application/component/McpServersDialog.vue'
import ToolDialog from '@/views/application/component/ToolDialog.vue'
import ApplicationDialog from "@/views/application/component/ApplicationDialog.vue";
import useStore from "@/stores";
const route = useRoute()
const router = useRouter()
const {
params: { id },
} = route as any
const { user, folder } = useStore()
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
@ -736,6 +804,8 @@ const applicationForm = ref<ApplicationFormType>({
tts_model_enable: false,
tts_type: 'BROWSER',
type: 'SIMPLE',
application_enable: false,
application_ids: [],
mcp_enable: false,
mcp_tool_ids: [],
mcp_servers: '',
@ -877,6 +947,14 @@ function removeMcpTool(id: any) {
}
}
function removeApplication(id: any) {
if (applicationForm.value.application_ids) {
applicationForm.value.application_ids = applicationForm.value.application_ids.filter(
(v: any) => v !== id,
)
}
}
const mcpServersDialogRef = ref()
function openMcpServersDialog() {
const config = {
@ -902,6 +980,26 @@ function submitToolDialog(config: any) {
applicationForm.value.tool_ids = config.tool_ids
}
const applicationDialogRef = ref()
function openApplicationDialog() {
applicationDialogRef.value.open(applicationForm.value.application_ids)
}
function submitApplicationDialog(config: any) {
applicationForm.value.application_ids = config.application_ids
}
const applicationSelectOptions = ref<any[]>([])
function getApplicationSelectOptions() {
loadSharedApi({ type: 'application', systemType: apiType.value })
.getAllApplication({folder_id: folder.currentFolder?.id || user.getWorkspaceId()})
.then((res: any) => {
applicationSelectOptions.value = res.data.filter(
(item: any) => item.is_publish,
)
})
}
const toolSelectOptions = ref<any[]>([])
function getToolSelectOptions() {
const obj =
@ -1118,6 +1216,7 @@ onMounted(() => {
if (toolPermissionPrecise.value.read()) {
getToolSelectOptions();
getMcpToolSelectOptions()
getApplicationSelectOptions()
}
})
</script>

View File

@ -0,0 +1,228 @@
<template>
<el-dialog
v-model="dialogVisible"
width="1000"
append-to-body
class="addTool-dialog"
align-center
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header="{ titleId, titleClass }">
<div class="flex-between mb-8">
<div class="flex">
<h4 :id="titleId" :class="titleClass" class="mr-8">
{{ $t('设置应用') }}
</h4>
</div>
<el-button link class="mr-24" @click="refresh">
<el-icon :size="18"><Refresh /></el-icon>
</el-button>
</div>
</template>
<LayoutContainer class="application-manage">
<template #left>
<folder-tree
:data="folderList"
:currentNodeKey="currentFolder?.id"
@handleNodeClick="folderClickHandle"
v-loading="folderLoading"
:canOperation="false"
:treeStyle="{ height: 'calc(100vh - 240px)' }"
/>
</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>
<div class="flex-between">
<div class="flex">
<el-text type="info" class="color-secondary mr-8" v-if="checkList.length > 0">
{{ $t('common.selected') }} {{ checkList.length }}
</el-text>
<el-button link type="primary" v-if="checkList.length > 0" @click="clearCheck">
{{ $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>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, 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 { folder, user } = useStore()
const apiType = computed(() => {
if (route.path.includes('shared')) {
return 'systemShare'
} else if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
const dialogVisible = ref<boolean>(false)
const checkList = ref<Array<string>>([])
const searchValue = ref('')
const searchData = ref<Array<any>>([])
const applicationList = ref<Array<any>>([])
const apiLoading = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
checkList.value = []
searchValue.value = ''
searchData.value = []
applicationList.value = []
}
})
watch(searchValue, (val) => {
if (val) {
searchData.value = applicationList.value.filter((v) => v.name.includes(val))
} else {
searchData.value = applicationList.value
}
})
function changeHandle() {}
function clearCheck() {
checkList.value = []
}
const open = (checked: any) => {
checkList.value = checked || []
getFolder()
dialogVisible.value = true
}
const submitHandle = () => {
emit('refresh', {
application_ids: checkList.value,
})
dialogVisible.value = false
}
const refresh = () => {
searchValue.value = ''
applicationList.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('APPLICATION', params, apiType.value, 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: 'application',
isShared: folder_id === 'share',
systemType: 'workspace',
})
.getAllApplication({
folder_id: folder_id,
})
.then((res: any) => {
applicationList.value = res.data
applicationList.value = applicationList.value?.filter((item: any) => item.is_publish && item.id !== route.params.id)
searchData.value = res.data
searchData.value = searchData.value?.filter((item: any) => item.is_publish && item.id !== route.params.id)
})
}
defineExpose({ open })
</script>
<style lang="scss">
.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

@ -252,7 +252,49 @@
</div>
</template>
</div>
<el-form-item @click.prevent v-if="chat_data.mcp_enable || chat_data.tool_enable">
<!-- 应用 -->
<div class="flex-between mb-16">
<div class="lighter">{{ $t('views.application.title') }}</div>
<div>
<el-button
type="primary"
class="mr-4"
link
@click="openApplicationDialog"
@refreshForm="refreshParam"
v-if="chat_data.application_enable"
>
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
<el-switch size="small" v-model="chat_data.application_enable" />
</div>
</div>
<div class="w-full mb-16" v-if="chat_data.application_ids?.length > 0">
<template v-for="(item, index) in chat_data.application_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">
<el-avatar
v-if="relatedObject(applicationSelectOptions, item, 'id')?.icon"
shape="square"
:size="20"
style="background: none"
class="mr-8"
>
<img :src="resetUrl(relatedObject(applicationSelectOptions, item, 'id')?.icon)" alt="" />
</el-avatar>
<AppIcon v-else class="mr-8" :size="20" />
<div class="ellipsis" :title="relatedObject(applicationSelectOptions, item, 'id')?.name">
{{ relatedObject(applicationSelectOptions, item, 'id')?.name }}
</div>
</div>
<el-button text @click="removeApplication(item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
</template>
</div>
<el-form-item @click.prevent v-if="chat_data.mcp_enable || chat_data.tool_enable || chat_data.application_enable">
<template #label>
<div class="flex-between">
<span class="mr-4">
@ -319,6 +361,7 @@
/>
<McpServersDialog ref="mcpServersDialogRef" @refresh="submitMcpServersDialog" />
<ToolDialog ref="toolDialogRef" @refresh="submitToolDialog" />
<ApplicationDialog ref="applicationDialogRef" @refresh="submitApplicationDialog" />
</NodeContainer>
</template>
<script setup lang="ts">
@ -339,6 +382,7 @@ import { useRoute } from 'vue-router'
import { resetUrl } from '@/utils/common'
import { relatedObject } from '@/utils/array.ts'
import { WorkflowMode } from '@/enums/application'
import ApplicationDialog from "@/views/application/component/ApplicationDialog.vue";
const workflowMode = (inject('workflowMode') as WorkflowMode) || WorkflowMode.Application
const getResourceDetail = inject('getResourceDetail') as any
const route = useRoute()
@ -566,6 +610,33 @@ function getMcpToolSelectOptions() {
})
}
const applicationSelectOptions = ref<any[]>([])
function getApplicationSelectOptions() {
loadSharedApi({ type: 'application', systemType: apiType.value })
.getAllApplication({folder_id: resource.value?.workspace_id})
.then((res: any) => {
applicationSelectOptions.value = res.data.filter(
(item: any) => item.is_publish,
)
})
}
const applicationDialogRef = ref()
function openApplicationDialog() {
applicationDialogRef.value.open(props.nodeModel.properties.node_data.application_ids)
}
function submitApplicationDialog(config: any) {
set(props.nodeModel.properties.node_data, 'application_ids', config.application_ids)
}
function removeApplication(id: any) {
if (chat_data.value.application_ids) {
chat_data.value.application_ids = chat_data.value.application_ids.filter(
(v: any) => v !== id,
)
}
}
onMounted(() => {
getSelectModel()
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
@ -590,6 +661,7 @@ onMounted(() => {
getToolSelectOptions()
getMcpToolSelectOptions()
getApplicationSelectOptions()
})
</script>
<style lang="scss" scoped></style>