From 142d8218e37a1cf95152225f989477338e0491a1 Mon Sep 17 00:00:00 2001 From: shenkaibo Date: Fri, 28 Aug 2026 14:38:28 +0800 Subject: [PATCH] feat: agent, knowledge base, and tool workflows support default model configuration and switching --- apps/application/flow/common.py | 44 +++- .../ai_chat_step_node/impl/base_chat_node.py | 5 + .../impl/base_image_generate_node.py | 5 + .../impl/base_image_to_video_node.py | 5 + .../impl/base_image_understand_node.py | 5 + .../intent_node/impl/base_intent_node.py | 5 + .../i_parameter_extraction_node.py | 5 + .../question_node/impl/base_question_node.py | 5 + .../reranker_node/i_reranker_node.py | 4 + .../impl/base_speech_to_text_node.py | 5 + .../impl/base_text_to_speech_node.py | 5 + .../impl/base_text_to_video_node.py | 5 + .../impl/base_tool_workflow_lib_node.py | 3 +- .../impl/base_video_understand_node.py | 5 + apps/application/flow/tools.py | 69 +++++- apps/application/flow/workflow_manage.py | 29 +++ apps/application/long_term_memory/__init__.py | 4 +- ...lication_default_model_setting_and_more.py | 23 ++ apps/application/models/application.py | 2 + apps/application/serializers/application.py | 82 ++++++- ...workflow_default_model_setting_and_more.py | 23 ++ apps/knowledge/models/knowledge.py | 2 + apps/knowledge/serializers/common.py | 10 +- apps/knowledge/serializers/knowledge.py | 1 + .../serializers/knowledge_workflow.py | 25 +- apps/locales/en_US/LC_MESSAGES/django.po | 3 + apps/locales/zh_CN/LC_MESSAGES/django.po | 3 + apps/locales/zh_Hant/LC_MESSAGES/django.po | 3 + ...workflow_default_model_setting_and_more.py | 33 +++ apps/tools/models/tool_workflow.py | 2 + apps/tools/serializers/tool.py | 10 +- apps/tools/serializers/tool_workflow.py | 15 +- .../impl/task/tool_task/workflow_tool_task.py | 3 +- .../operation-button/ChatOperationButton.vue | 2 +- .../operation-button/LogOperationButton.vue | 4 +- ui/src/components/model-select/index.vue | 4 +- .../default-model-setting/index.vue | 224 ++++++++++++++++++ .../locales/lang/en-US/views/application.ts | 7 + ui/src/locales/lang/en-US/workflow.ts | 20 ++ .../locales/lang/zh-CN/views/application.ts | 7 + ui/src/locales/lang/zh-CN/workflow.ts | 20 ++ .../locales/lang/zh-Hant/views/application.ts | 7 + ui/src/locales/lang/zh-Hant/workflow.ts | 20 ++ ui/src/views/application-workflow/index.vue | 57 ++++- ui/src/views/knowledge-workflow/index.vue | 34 ++- ui/src/views/tool-workflow/index.vue | 34 ++- .../workflow/common/DefaultModelDisplay.vue | 81 +++++++ ui/src/workflow/common/data.ts | 2 + ui/src/workflow/common/validate.ts | 65 +++++ ui/src/workflow/nodes/ai-chat-node/index.vue | 13 +- ui/src/workflow/nodes/base-node/index.vue | 84 ++++++- .../workflow/nodes/image-generate/index.vue | 13 +- .../workflow/nodes/image-to-video/index.vue | 13 +- .../workflow/nodes/image-understand/index.vue | 13 +- .../nodes/intent-classify-node/index.vue | 13 +- .../nodes/parameter-extraction-node/index.vue | 13 +- ui/src/workflow/nodes/question-node/index.vue | 13 +- ui/src/workflow/nodes/reranker-node/index.vue | 13 +- .../nodes/speech-to-text-node/index.vue | 13 +- .../nodes/text-to-speech-node/index.vue | 13 +- ui/src/workflow/nodes/text-to-video/index.vue | 13 +- .../workflow/nodes/video-understand/index.vue | 13 +- 62 files changed, 1189 insertions(+), 87 deletions(-) create mode 100644 apps/application/migrations/0015_application_default_model_setting_and_more.py create mode 100644 apps/knowledge/migrations/0011_knowledgeworkflow_default_model_setting_and_more.py create mode 100644 apps/tools/migrations/0008_toolworkflow_default_model_setting_and_more.py create mode 100644 ui/src/components/workflow-dropdown-menu/default-model-setting/index.vue create mode 100644 ui/src/workflow/common/DefaultModelDisplay.vue diff --git a/apps/application/flow/common.py b/apps/application/flow/common.py index d7520cf690c..8ce588e0ec5 100644 --- a/apps/application/flow/common.py +++ b/apps/application/flow/common.py @@ -249,7 +249,8 @@ def is_valid_model_params(self): node_list = [node for node in self.nodes if ( node.type == 'ai-chat-node' or node.type == 'question-node' or node.type == 'parameter-extraction-node')] for node in node_list: - if (node.properties.get('node_data', {}).get('model_id_type') or 'custom') == 'reference': + model_id_type = node.properties.get('node_data', {}).get('model_id_type') or 'custom' + if model_id_type in ('reference', 'default'): continue model = QuerySet(Model).filter(id=node.properties.get('node_data', {}).get('model_id')).first() if model is None: @@ -282,3 +283,44 @@ def is_valid_base_node(self): raise AppApiException(500, _('Basic information node is required')) if len(base_node_list) > 1: raise AppApiException(500, _('There can only be one basic information node')) + + +def get_base_node_model(application, model_type): + """ + 解析基本信息节点(base-node)实际使用的模型配置。 + model_type: 'STT' | 'TTS' | 'LLM'(长期记忆)。 + WORK_FLOW 应用读 base-node node_data 的模式字段;default/DEFAULT 时取 default_model_setting; + SIMPLE 应用(无 base-node)回退到顶层模型字段。 + 返回 {'model_id', 'model_params'}。 + """ + mode_field = {'STT': 'stt_model_id_type', 'TTS': 'tts_type', 'LLM': 'long_term_model_id_type'}[model_type] + model_field = {'STT': 'stt_model_id', 'TTS': 'tts_model_id', 'LLM': 'long_term_model_id'}[model_type] + param_field = { + 'STT': 'stt_model_params_setting', + 'TTS': 'tts_model_params_setting', + 'LLM': 'long_term_model_params_setting', + }[model_type] + node_data = None + for node in ((getattr(application, 'work_flow', None) or {}).get('nodes') or []): + if node.get('id') == 'base-node': + node_data = (node.get('properties') or {}).get('node_data') or {} + break + if node_data is None: + return { + 'model_id': getattr(application, model_field, None), + 'model_params': getattr(application, param_field, None) or {}, + } + mode = node_data.get(mode_field) + if mode == 'default' or mode == 'DEFAULT': + default_setting = (application.default_model_setting or {}).get(model_type, {}) or {} + return { + 'model_id': default_setting.get('model_id'), + 'model_params': default_setting.get('model_params_setting') or {}, + } + if model_type == 'TTS' and mode == 'BROWSER': + # 浏览器播放不使用 TTS 模型,残留 model_id 不参与解析 + return {'model_id': None, 'model_params': {}} + return { + 'model_id': node_data.get(model_field), + 'model_params': node_data.get(param_field) or {}, + } diff --git a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py index 27cbe9e9cf7..15bea2b67e5 100644 --- a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py +++ b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py @@ -203,6 +203,11 @@ def execute( if reference_data and isinstance(reference_data, dict): model_id = reference_data.get("model_id", model_id) model_params_setting = reference_data.get("model_params_setting") + elif model_id_type == "default": + default_setting = self.workflow_manage.get_default_model_setting("LLM") + if default_setting.get("model_id"): + model_id = default_setting.get("model_id") + model_params_setting = default_setting.get("model_params_setting", model_params_setting) if model_id is None or model_id == "": raise Exception(_("Model is not allowed to be empty")) diff --git a/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py b/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py index bbd0fc1d12a..db2f723511d 100644 --- a/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py +++ b/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py @@ -36,6 +36,11 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('TTI') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) if model_id is None or model_id == '': raise Exception(_('Model is not allowed to be empty')) diff --git a/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py b/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py index 0f19568c6f9..08cb0f0ad55 100644 --- a/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py +++ b/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py @@ -39,6 +39,11 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('ITV') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) if model_id is None or model_id == '': raise Exception(_('Model is not allowed to be empty')) workspace_id = self.workflow_manage.get_body().get('workspace_id') diff --git a/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py b/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py index 43ad363d106..f6680c3764d 100644 --- a/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py +++ b/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py @@ -152,6 +152,11 @@ def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, hist if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('IMAGE') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) if model_id is None or model_id == '': raise Exception(_('Model is not allowed to be empty')) diff --git a/apps/application/flow/step_node/intent_node/impl/base_intent_node.py b/apps/application/flow/step_node/intent_node/impl/base_intent_node.py index e4bc05c2f84..f8c3f6c84d7 100644 --- a/apps/application/flow/step_node/intent_node/impl/base_intent_node.py +++ b/apps/application/flow/step_node/intent_node/impl/base_intent_node.py @@ -64,6 +64,11 @@ def execute(self, model_id, dialogue_number, history_chat_record, user_input, br if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('LLM') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) if not model_id: raise Exception(_('Model is not allowed to be empty')) diff --git a/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py b/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py index 54c60bb096c..c1c3667b055 100644 --- a/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py +++ b/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py @@ -47,6 +47,11 @@ def _run(self): if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('LLM') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) input_variable = self.workflow_manage.get_reference_field( self.node_params_serializer.data.get('input_variable')[0], diff --git a/apps/application/flow/step_node/question_node/impl/base_question_node.py b/apps/application/flow/step_node/question_node/impl/base_question_node.py index 6770a53c294..c984373d574 100644 --- a/apps/application/flow/step_node/question_node/impl/base_question_node.py +++ b/apps/application/flow/step_node/question_node/impl/base_question_node.py @@ -95,6 +95,11 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('LLM') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) if not model_id: raise Exception(_('Model is not allowed to be empty')) diff --git a/apps/application/flow/step_node/reranker_node/i_reranker_node.py b/apps/application/flow/step_node/reranker_node/i_reranker_node.py index af87a6f2003..216cc0afaa0 100644 --- a/apps/application/flow/step_node/reranker_node/i_reranker_node.py +++ b/apps/application/flow/step_node/reranker_node/i_reranker_node.py @@ -73,6 +73,10 @@ def _run(self): if reference_data and isinstance(reference_data, dict): reranker_model_id = reference_data.get('reranker_model_id', reference_data.get('model_id', reranker_model_id)) + elif reranker_model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('RERANKER') + if default_setting.get('model_id'): + reranker_model_id = default_setting.get('model_id') if reranker_model_id is None or reranker_model_id == '': raise Exception(_('Model is not allowed to be empty')) diff --git a/apps/application/flow/step_node/speech_to_text_step_node/impl/base_speech_to_text_node.py b/apps/application/flow/step_node/speech_to_text_step_node/impl/base_speech_to_text_node.py index 1df3f85cdeb..371bc2bcba6 100644 --- a/apps/application/flow/step_node/speech_to_text_step_node/impl/base_speech_to_text_node.py +++ b/apps/application/flow/step_node/speech_to_text_step_node/impl/base_speech_to_text_node.py @@ -32,6 +32,11 @@ def execute(self, stt_model_id, audio, model_params_setting=None, stt_model_id_t if reference_data and isinstance(reference_data, dict): stt_model_id = reference_data.get('stt_model_id', reference_data.get('model_id', stt_model_id)) model_params_setting = reference_data.get('model_params_setting') + elif stt_model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('STT') + if default_setting.get('model_id'): + stt_model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) from django.utils.translation import gettext_lazy as _ diff --git a/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py b/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py index 861f27312aa..1ba9c0bb801 100644 --- a/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py +++ b/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py @@ -56,6 +56,11 @@ def execute(self, tts_model_id, if reference_data and isinstance(reference_data, dict): tts_model_id = reference_data.get('tts_model_id', reference_data.get('model_id', tts_model_id)) model_params_setting = reference_data.get('model_params_setting') + elif tts_model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('TTS') + if default_setting.get('model_id'): + tts_model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) from django.utils.translation import gettext_lazy as _ diff --git a/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py b/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py index af787235dbe..dedcd138acf 100644 --- a/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py +++ b/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py @@ -37,6 +37,11 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('TTV') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) if model_id is None or model_id == '': raise Exception(_('Model is not allowed to be empty')) diff --git a/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py b/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py index 86f9cccde61..5e27c44b99b 100644 --- a/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py +++ b/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py @@ -228,7 +228,8 @@ def workflow_manage_new_instance(start_node_id=None, 'stream': True, 'workspace_id': workspace_id, 'user_id': runtime_user_id, - **parameters}, + **parameters, + 'default_model_setting': tool_workflow_version.default_model_setting}, ToolWorkflowPostHandler(took_execute, tool_lib_id), base_to_response=LoopToResponse(), start_node_id=start_node_id, diff --git a/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py b/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py index ea497be27d0..f819e82d286 100644 --- a/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py +++ b/apps/application/flow/step_node/video_understand_step_node/impl/base_video_understand_node.py @@ -147,6 +147,11 @@ def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, hist if reference_data and isinstance(reference_data, dict): model_id = reference_data.get('model_id', model_id) model_params_setting = reference_data.get('model_params_setting') + elif model_id_type == 'default': + default_setting = self.workflow_manage.get_default_model_setting('IMAGE') + if default_setting.get('model_id'): + model_id = default_setting.get('model_id') + model_params_setting = default_setting.get('model_params_setting', model_params_setting) from django.utils.translation import gettext_lazy as _ diff --git a/apps/application/flow/tools.py b/apps/application/flow/tools.py index 42fc2ca2241..0f0993954f3 100644 --- a/apps/application/flow/tools.py +++ b/apps/application/flow/tools.py @@ -816,6 +816,34 @@ async def anext_async(agen): return await agen.__anext__() +def _get_node_model_id(node, model_field, mode_field): + """节点为 default/reference 模式时不返回节点内 model_id(运行时才解析,避免脏映射)。""" + node_data = (node.get("properties") or {}).get("node_data") or {} + if node_data.get(mode_field) in ("default", "reference"): + return None + return node_data.get(model_field) + + +# base-node 三类模型:mode 判定与 validate_workflow_default_models/get_base_node_model 保持一致 +# (stt/长期记忆用 'default'/'reference',tts 用大写 'DEFAULT'/'BROWSER') +_base_node_model_specs = ( + ("stt_model_id_type", ("default", "reference"), "stt_model_enable", "stt_model_id"), + ("tts_type", ("DEFAULT", "BROWSER"), "tts_model_enable", "tts_model_id"), + ("long_term_model_id_type", ("default", "reference"), "long_term_enable", "long_term_model_id"), +) + + +def _get_base_node_model_ids(node): + """返回 base-node node_data 中实际自定义的 STT/TTS/长期记忆 model_id(default/BROWSER 时运行时解析,不映射)。""" + node_data = (node.get("properties") or {}).get("node_data") or {} + model_ids = [] + for mode_field, skip_modes, enable_field, model_field in _base_node_model_specs: + if node_data.get(enable_field) and node_data.get(mode_field) not in skip_modes: + if node_data.get(model_field): + model_ids.append(node_data.get(model_field)) + return model_ids + + target_source_node_mapping = { "TOOL": { "tool-lib-node": lambda n: [n.get("properties").get("node_data").get("tool_lib_id")], @@ -828,17 +856,18 @@ async def anext_async(agen): "tool-workflow-lib-node": lambda n: [n.get("properties").get("node_data").get("tool_lib_id")], }, "MODEL": { - "ai-chat-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "question-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "speech-to-text-node": lambda n: [n.get("properties").get("node_data").get("stt_model_id")], - "text-to-speech-node": lambda n: [n.get("properties").get("node_data").get("tts_model_id")], - "image-to-video-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "image-generate-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "intent-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "image-understand-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "parameter-extraction-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "video-understand-node": lambda n: [n.get("properties").get("node_data").get("model_id")], - "reranker-node": lambda n: [n.get("properties").get("node_data").get("reranker_model_id")], + "ai-chat-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "question-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "speech-to-text-node": lambda n: [v for v in [_get_node_model_id(n, 'stt_model_id', 'stt_model_id_type')] if v], + "text-to-speech-node": lambda n: [v for v in [_get_node_model_id(n, 'tts_model_id', 'tts_model_id_type')] if v], + "image-to-video-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "image-generate-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "intent-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "image-understand-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "parameter-extraction-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "video-understand-node": lambda n: [v for v in [_get_node_model_id(n, 'model_id', 'model_id_type')] if v], + "reranker-node": lambda n: [v for v in [_get_node_model_id(n, 'reranker_model_id', 'reranker_model_id_type')] if v], + "base-node": _get_base_node_model_ids, }, "KNOWLEDGE": { "search-knowledge-node": lambda n: n.get("properties").get("node_data").get("knowledge_id_list"), @@ -905,6 +934,7 @@ def get_workflow_resource(workflow, node_handle): lambda instance: [instance.long_term_model_id] if instance.long_term_model_id else [], lambda instance: [instance.tts_model_id] if instance.tts_model_id else [], lambda instance: [instance.stt_model_id] if instance.stt_model_id else [], + lambda instance: [v.get('model_id') for v in (instance.default_model_setting or {}).values() if (v or {}).get('model_id')], ], } knowledge_instance_field_call_dict = { @@ -928,6 +958,22 @@ def get_instance_resource(instance, source_type, source_id, instance_field_call_ return response +def append_default_model_mapping(instance_mapping, default_model_setting, source_type, source_id): + """把 default_model_setting 各类别 model_id 追加为 MODEL 资源映射(方案A),返回追加后的列表。""" + from system_manage.models.resource_mapping import ResourceMapping, ResourceType + + for value in (default_model_setting or {}).values(): + model_id = (value or {}).get('model_id') + if model_id: + instance_mapping.append( + ResourceMapping( + source_type=source_type, target_type=ResourceType.MODEL, + source_id=str(source_id), target_id=model_id, + ) + ) + return instance_mapping + + def save_workflow_mapping(workflow, source_type, source_id, other_resource_mapping=None): if not other_resource_mapping: other_resource_mapping = [] @@ -1058,6 +1104,7 @@ def inner(**kwargs): "workspace_id": workspace_id, "user_id": user_id, **kwargs, + "default_model_setting": qv.default_model_setting, }, ToolWorkflowPostHandler(took_execute, tool_id), is_the_task_interrupted=lambda: False, diff --git a/apps/application/flow/workflow_manage.py b/apps/application/flow/workflow_manage.py index f1323c6d4b7..075353ab072 100644 --- a/apps/application/flow/workflow_manage.py +++ b/apps/application/flow/workflow_manage.py @@ -831,3 +831,32 @@ def get_source_type(self): def get_source_id(self): return self.params.get('application_id') + + def get_application(self): + """获取当前应用的 Application 实例(仅 APPLICATION 模式有;其余模式返回 None)""" + if self.work_flow_post_handler is None: + return None + chat_info = getattr(self.work_flow_post_handler, 'chat_info', None) + if chat_info is None: + return None + return getattr(chat_info, 'application', None) + + def get_default_model_setting(self, model_type): + """获取指定模型类别的默认模型配置 {model_id, model_params_setting,...};未配置返回 {}""" + setting = self._load_default_model_setting() + return (setting or {}).get(model_type, {}) or {} + + def _load_default_model_setting(self): + """按执行来源读取 default_model_setting(params 透传),每次 run 缓存一次;Application 回退 chat_info.application。""" + if getattr(self, '_default_model_setting', None) is not None: + return self._default_model_setting + setting = self.params.get('default_model_setting') + if setting is None: + application = self.get_application() + setting = getattr(application, 'default_model_setting', {}) or {} + self._default_model_setting = setting + return setting + + def get_default_model_id(self, model_type): + """获取指定模型类别的默认模型 id;未配置返回 None""" + return self.get_default_model_setting(model_type).get('model_id') diff --git a/apps/application/long_term_memory/__init__.py b/apps/application/long_term_memory/__init__.py index ea03cf58bcc..4fa27551708 100644 --- a/apps/application/long_term_memory/__init__.py +++ b/apps/application/long_term_memory/__init__.py @@ -6,6 +6,7 @@ from django.utils import timezone from langchain_core.messages import HumanMessage +from application.flow.common import get_base_node_model from application.models import Chat, ChatRecord, Application, ApplicationLongTermMemory from common.utils.logger import maxkb_logger from models_provider.tools import get_model_instance_by_model_workspace_id @@ -138,8 +139,7 @@ def _get_long_term_config(application, chat_user_id): return { 'trigger_type': node_data.get('long_term_trigger_type'), 'trigger_setting': node_data.get('long_term_trigger_setting') or {'rounds': 10}, - 'model_id': node_data.get('long_term_model_id'), - 'model_params': node_data.get('long_term_model_params_setting') or {}, + **get_base_node_model(application, 'LLM'), } else: if not application.long_term_enable: diff --git a/apps/application/migrations/0015_application_default_model_setting_and_more.py b/apps/application/migrations/0015_application_default_model_setting_and_more.py new file mode 100644 index 00000000000..c58e3fec7a7 --- /dev/null +++ b/apps/application/migrations/0015_application_default_model_setting_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.16 on 2026-08-25 05:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('application', '0014_applicationversion_knowledge_ids'), + ] + + operations = [ + migrations.AddField( + model_name='application', + name='default_model_setting', + field=models.JSONField(default=dict, verbose_name='默认模型设置'), + ), + migrations.AddField( + model_name='applicationversion', + name='default_model_setting', + field=models.JSONField(default=dict, verbose_name='默认模型设置'), + ), + ] diff --git a/apps/application/models/application.py b/apps/application/models/application.py index 71d345164cc..f4f9c6e9c9f 100644 --- a/apps/application/models/application.py +++ b/apps/application/models/application.py @@ -111,6 +111,7 @@ class Application(AppModelMixin): long_term_model_params_setting = models.JSONField(verbose_name="长期记忆模型参数相关设置", default=dict) long_term_trigger_type = models.CharField(verbose_name='长期记忆触发类型', default='ROUND') long_term_trigger_setting = models.JSONField(verbose_name='长期记忆触发配置', default=dict) + default_model_setting = models.JSONField(verbose_name="默认模型设置", default=dict) @staticmethod def get_default_model_prompt(): @@ -193,6 +194,7 @@ class ApplicationVersion(AppModelMixin): long_term_trigger_type = models.CharField(verbose_name='长期记忆触发类型', default='ROUND') long_term_trigger_setting = models.JSONField(verbose_name='长期记忆触发配置', default=dict) knowledge_ids = models.JSONField(verbose_name="数据集id列表", default=list) + default_model_setting = models.JSONField(verbose_name="默认模型设置", default=dict) class Meta: db_table = "application_version" diff --git a/apps/application/serializers/application.py b/apps/application/serializers/application.py index c3a74a9c8a1..157aaa5abe1 100644 --- a/apps/application/serializers/application.py +++ b/apps/application/serializers/application.py @@ -21,7 +21,7 @@ import requests import uuid_utils.compat as uuid -from application.flow.common import Workflow +from application.flow.common import Workflow, get_base_node_model from application.long_term_memory import schedule_extract_long_term_memory from application.models.application import Application, ApplicationFolder, ApplicationTypeChoices, ApplicationVersion from application.models.application_access_token import ApplicationAccessToken @@ -80,6 +80,77 @@ def _walk_workflow_nodes(work_flow, collector): _walk_workflow_nodes(node_data.get("loop_body"), collector) +# 节点类型 -> 默认模型类别(与 ModelTypeConst code 对应) +NODE_DEFAULT_MODEL_TYPE = { + "ai-chat-node": "LLM", + "question-node": "LLM", + "intent-node": "LLM", + "parameter-extraction-node": "LLM", + "image-understand-node": "IMAGE", + "video-understand-node": "IMAGE", + "image-generate-node": "TTI", + "text-to-video-node": "TTV", + "image-to-video-node": "ITV", + "speech-to-text-node": "STT", + "text-to-speech-node": "TTS", + "reranker-node": "RERANKER", +} + + +def validate_workflow_default_models(work_flow, default_model_setting): + """发布前校验:节点选择「默认模型」但对应类别默认模型未配置时,禁止发布并定位。""" + if not work_flow: + return + for node in work_flow.get("nodes", []) or []: + properties = node.get("properties") or {} + node_data = properties.get("node_data") or {} + node_type = node.get("type") + node_name = properties.get("stepName") or node_data.get("name") or node_type + model_type = NODE_DEFAULT_MODEL_TYPE.get(node_type) + if node_type == 'base-node': + for mode_field, default_mode, enable_field, default_model_type in ( + ('stt_model_id_type', 'default', 'stt_model_enable', 'STT'), + ('tts_type', 'DEFAULT', 'tts_model_enable', 'TTS'), + ('long_term_model_id_type', 'default', 'long_term_enable', 'LLM'), + ): + if (node_data.get(mode_field) == default_mode and node_data.get(enable_field) + and not ((default_model_setting or {}).get(default_model_type, {}) or {}).get('model_id')): + raise AppApiException( + 500, + _( + "{node_name} selected the default model, but the default model " + "of this type is not configured." + ).format(node_name=node_name), + ) + if model_type is not None: + # 取该节点实际使用的 model_id_type 字段(custom/reference/default) + type_key = ( + "reranker_model_id_type" + if node_type == "reranker-node" + else ( + "stt_model_id_type" + if node_type == "speech-to-text-node" + else ( + "tts_model_id_type" + if node_type == "text-to-speech-node" + else "model_id_type" + ) + ) + ) + if (node_data.get(type_key) or "custom") == "default" and not ( + (default_model_setting or {}).get(model_type, {}) or {} + ).get("model_id"): + raise AppApiException( + 500, + _( + "{node_name} selected the default model, but the default model " + "of this type is not configured." + ).format(node_name=node_name), + ) + if node_type == "loop-node": + validate_workflow_default_models(node_data.get("loop_body"), default_model_setting) + + def get_bound_tool_ids(instance: Dict) -> List[str]: """ 收集应用配置(含工作流节点)中引用的所有工具id,用于绑定前的权限校验 @@ -1229,6 +1300,7 @@ def reset_application_version(application_version, application): "skill_tool_ids": "skill_tool_ids", "mcp_output_enable": "mcp_output_enable", "type": "type", + "default_model_setting": "default_model_setting", } for version_field, app_field in update_field_dict.items(): @@ -1250,6 +1322,7 @@ def publish(self, instance, with_valid=True): if work_flow is None: raise AppApiException(500, _("work_flow is a required field")) Workflow.new_instance(work_flow).is_valid() + validate_workflow_default_models(work_flow, application.default_model_setting) base_node = get_base_node_work_flow(work_flow) if base_node is not None: node_data = base_node.get("properties").get("node_data") @@ -1449,6 +1522,7 @@ def edit(self, instance: Dict, with_valid=True): "long_term_model_params_setting", "long_term_trigger_setting", "long_term_trigger_type", + "default_model_setting", "problem_optimization_prompt", "clean_time", "file_clean_time", @@ -1739,8 +1813,9 @@ def speech_to_text(self, instance, debug=True, with_valid=True): QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time").first() ) if application.stt_model_enable: + config = get_base_node_model(application, 'STT') model = get_model_instance_by_model_workspace_id( - application.stt_model_id, application.workspace_id, **application.stt_model_params_setting + config['model_id'], application.workspace_id, **config['model_params'] ) text = model.speech_to_text(instance.get("file")) return text @@ -1757,8 +1832,9 @@ def text_to_speech(self, instance, debug=True, with_valid=True): QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time").first() ) if application.tts_model_enable: + config = get_base_node_model(application, 'TTS') model = get_model_instance_by_model_workspace_id( - application.tts_model_id, application.workspace_id, **application.tts_model_params_setting + config['model_id'], application.workspace_id, **config['model_params'] ) content = _remove_empty_lines(instance.get("text", "")) diff --git a/apps/knowledge/migrations/0011_knowledgeworkflow_default_model_setting_and_more.py b/apps/knowledge/migrations/0011_knowledgeworkflow_default_model_setting_and_more.py new file mode 100644 index 00000000000..b73d3304cd2 --- /dev/null +++ b/apps/knowledge/migrations/0011_knowledgeworkflow_default_model_setting_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.16 on 2026-08-26 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('knowledge', '0010_publicfileaccess'), + ] + + operations = [ + migrations.AddField( + model_name='knowledgeworkflow', + name='default_model_setting', + field=models.JSONField(default=dict, verbose_name='默认模型设置'), + ), + migrations.AddField( + model_name='knowledgeworkflowversion', + name='default_model_setting', + field=models.JSONField(default=dict, verbose_name='默认模型设置'), + ), + ] diff --git a/apps/knowledge/models/knowledge.py b/apps/knowledge/models/knowledge.py index e261c0615a7..c1951ec75b8 100644 --- a/apps/knowledge/models/knowledge.py +++ b/apps/knowledge/models/knowledge.py @@ -156,6 +156,7 @@ class KnowledgeWorkflow(AppModelMixin): ) workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) work_flow = models.JSONField(verbose_name="工作流数据", default=dict) + default_model_setting = models.JSONField(verbose_name="默认模型设置", default=dict) is_publish = models.BooleanField(verbose_name="是否发布", default=False, db_index=True) publish_time = models.DateTimeField(verbose_name="发布时间", null=True, blank=True) @@ -173,6 +174,7 @@ class KnowledgeWorkflowVersion(AppModelMixin): workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) name = models.CharField(verbose_name="版本名称", max_length=128, default="") work_flow = models.JSONField(verbose_name="工作流数据", default=dict) + default_model_setting = models.JSONField(verbose_name="默认模型设置", default=dict) publish_user_id = models.UUIDField(verbose_name="发布者id", max_length=128, default=None, null=True) publish_user_name = models.CharField(verbose_name="发布者名称", max_length=128, default="") diff --git a/apps/knowledge/serializers/common.py b/apps/knowledge/serializers/common.py index 9dd3439aef4..83b1038e356 100644 --- a/apps/knowledge/serializers/common.py +++ b/apps/knowledge/serializers/common.py @@ -16,7 +16,12 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from application.flow.tools import save_workflow_mapping, get_instance_resource, knowledge_instance_field_call_dict +from application.flow.tools import ( + save_workflow_mapping, + get_instance_resource, + knowledge_instance_field_call_dict, + append_default_model_mapping, +) from common.config.embedding_config import ModelManage from common.db.search import native_search from common.db.sql_execute import sql_execute, update_execute @@ -296,6 +301,9 @@ def update_resource_mapping_by_knowledge(knowledge_id: str): knowledge_id=knowledge_id).order_by( '-create_time')[0:1].first() if knowledge_workflow: + instance_mapping = append_default_model_mapping( + instance_mapping, knowledge_workflow.default_model_setting, + ResourceType.KNOWLEDGE, str(knowledge_id)) save_workflow_mapping(knowledge_workflow.work_flow, ResourceType.KNOWLEDGE, str(knowledge_id), instance_mapping) return diff --git a/apps/knowledge/serializers/knowledge.py b/apps/knowledge/serializers/knowledge.py index 6908d84890d..86a9f4e332f 100644 --- a/apps/knowledge/serializers/knowledge.py +++ b/apps/knowledge/serializers/knowledge.py @@ -449,6 +449,7 @@ def one(self): k = QuerySet(KnowledgeWorkflow).filter(knowledge_id=knowledge_dict.get("id")).first() if k: workflow["work_flow"] = k.work_flow + workflow["default_model_setting"] = k.default_model_setting workflow["is_publish"] = k.is_publish workflow["publish_time"] = k.publish_time return { diff --git a/apps/knowledge/serializers/knowledge_workflow.py b/apps/knowledge/serializers/knowledge_workflow.py index d19d7d37be7..eec20b2ce34 100644 --- a/apps/knowledge/serializers/knowledge_workflow.py +++ b/apps/knowledge/serializers/knowledge_workflow.py @@ -21,7 +21,7 @@ from application.flow.knowledge_workflow_manage import KnowledgeWorkflowManage from application.flow.step_node import get_node from application.flow.tools import save_workflow_mapping -from application.serializers.application import get_mcp_tools +from application.serializers.application import get_mcp_tools, validate_workflow_default_models from common.constants.cache_version import Cache_Version from common.db.search import page_search from common.exception.app_exception import AppApiException @@ -197,6 +197,7 @@ def action(self, instance: Dict, user, with_valid=True): "workspace_id": self.data.get("workspace_id"), "user_id": str(user.id), **instance, + "default_model_setting": knowledge_workflow.default_model_setting, }, KnowledgeWorkflowPostHandler(None, knowledge_action_id), is_the_task_interrupted=lambda: ( @@ -252,6 +253,7 @@ def upload_document(self, instance: Dict, user, with_valid=True): "workspace_id": self.data.get("workspace_id"), "user_id": str(user.id), **instance, + "default_model_setting": knowledge_workflow_version.default_model_setting, }, KnowledgeWorkflowPostHandler(None, knowledge_action_id), is_the_task_interrupted=lambda: ( @@ -462,8 +464,11 @@ def import_(self, instance: dict, is_import_tool, with_valid=True): update_tool_map, ) tool_model_list = [self.to_tool(tool, workspace_id, user_id) for tool in tool_list] + defaults = {"work_flow": work_flow} + if knowledge_workflow.get("default_model_setting") is not None: + defaults["default_model_setting"] = knowledge_workflow.get("default_model_setting") KnowledgeWorkflow.objects.filter(workspace_id=workspace_id, knowledge_id=knowledge_id).update_or_create( - knowledge_id=knowledge_id, workspace_id=workspace_id, defaults={"work_flow": work_flow} + knowledge_id=knowledge_id, workspace_id=workspace_id, defaults=defaults ) if is_import_tool: @@ -586,7 +591,12 @@ def export(self, with_valid=True): @staticmethod def to_tool_dict(tool, tool_workflow_dict): if tool.tool_type == ToolType.WORKFLOW: - return {**ToolExportModelSerializer(tool).data, "work_flow": tool_workflow_dict.get(tool.id).work_flow} + tool_workflow = tool_workflow_dict.get(tool.id) + return { + **ToolExportModelSerializer(tool).data, + "work_flow": tool_workflow.work_flow, + "default_model_setting": tool_workflow.default_model_setting, + } return ToolExportModelSerializer(tool).data class Operate(serializers.Serializer): @@ -605,8 +615,10 @@ def publish(self, with_valid=True): .filter(knowledge_id=self.data.get("knowledge_id"), workspace_id=workspace_id) .first() ) + validate_workflow_default_models(knowledge_workflow.work_flow, knowledge_workflow.default_model_setting) work_flow_version = KnowledgeWorkflowVersion( work_flow=knowledge_workflow.work_flow, + default_model_setting=knowledge_workflow.default_model_setting, knowledge_id=self.data.get("knowledge_id"), name=timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S"), publish_user_id=user_id, @@ -622,15 +634,18 @@ def publish(self, with_valid=True): def edit(self, instance: Dict): self.is_valid(raise_exception=True) if instance.get("work_flow"): + defaults = {"work_flow": instance.get("work_flow")} + if instance.get("default_model_setting") is not None: + defaults["default_model_setting"] = instance.get("default_model_setting") QuerySet(KnowledgeWorkflow).update_or_create( knowledge_id=self.data.get("knowledge_id"), create_defaults={ "id": uuid.uuid7(), "knowledge_id": self.data.get("knowledge_id"), "workspace_id": self.data.get("workspace_id"), - "work_flow": instance.get("work_flow", {}), + **defaults, }, - defaults={"work_flow": instance.get("work_flow")}, + defaults=defaults, ) update_resource_mapping_by_knowledge(self.data.get("knowledge_id")) return self.one() diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 5ba4bef4c92..7bfbe7b0222 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -8688,6 +8688,9 @@ msgstr "" msgid "Image to Video" msgstr "" +msgid "{node_name} selected the default model, but the default model of this type is not configured." +msgstr "" + msgid "Authentication failed. Please verify that the parameters are correct" msgstr "" diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index 3aaa21c5618..854ba48d6d2 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -8812,6 +8812,9 @@ msgstr "文生视频" msgid "Image to Video" msgstr "图生视频" +msgid "{node_name} selected the default model, but the default model of this type is not configured." +msgstr "{node_name}选择了默认模型,但未配置该类默认模型" + msgid "Authentication failed. Please verify that the parameters are correct" msgstr "认证失败,请检查参数是否正确" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index e06cb5ff08f..0d3560db467 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -8812,6 +8812,9 @@ msgstr "文生視頻" msgid "Image to Video" msgstr "圖生視頻" +msgid "{node_name} selected the default model, but the default model of this type is not configured." +msgstr "{node_name}選擇了預設模型,但未配置該類預設模型" + msgid "Authentication failed. Please verify that the parameters are correct" msgstr "認證失敗,請檢查參數是否正確" diff --git a/apps/tools/migrations/0008_toolworkflow_default_model_setting_and_more.py b/apps/tools/migrations/0008_toolworkflow_default_model_setting_and_more.py new file mode 100644 index 00000000000..208719026a4 --- /dev/null +++ b/apps/tools/migrations/0008_toolworkflow_default_model_setting_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.16 on 2026-08-26 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tools', '0007_alter_tool_tool_type_toolworkflow_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='toolworkflow', + name='default_model_setting', + field=models.JSONField(default=dict, verbose_name='默认模型设置'), + ), + migrations.AddField( + model_name='toolworkflowversion', + name='default_model_setting', + field=models.JSONField(default=dict, verbose_name='默认模型设置'), + ), + migrations.AlterField( + model_name='tool', + name='tool_type', + field=models.CharField(choices=[('INTERNAL', '内置'), ('CUSTOM', '自定义'), ('SKILL', '技能'), ('MCP', 'MCP工具'), ('DATA_SOURCE', '数据源'), ('WORKFLOW', '工作流')], db_index=True, default='CUSTOM', max_length=20, verbose_name='工具类型'), + ), + migrations.AlterField( + model_name='toolrecord', + name='source_type', + field=models.CharField(choices=[('APPLICATION', 'Application'), ('KNOWLEDGE', 'Knowledge'), ('TOOL', 'Tool'), ('TRIGGER', 'Trigger')], default='APPLICATION', max_length=256, verbose_name='触发器任务类型'), + ), + ] diff --git a/apps/tools/models/tool_workflow.py b/apps/tools/models/tool_workflow.py index 4f070cebd6c..77aeb5fc5dc 100644 --- a/apps/tools/models/tool_workflow.py +++ b/apps/tools/models/tool_workflow.py @@ -23,6 +23,7 @@ class ToolWorkflow(AppModelMixin): db_constraint=False, related_name='workflow') workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) work_flow = models.JSONField(verbose_name="工作流数据", default=dict) + default_model_setting = models.JSONField(verbose_name="默认模型设置", default=dict) is_publish = models.BooleanField(verbose_name="是否发布", default=False, db_index=True) publish_time = models.DateTimeField(verbose_name="发布时间", null=True, blank=True) @@ -39,6 +40,7 @@ class ToolWorkflowVersion(AppModelMixin): workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) name = models.CharField(verbose_name="版本名称", max_length=128, default="") work_flow = models.JSONField(verbose_name="工作流数据", default=dict) + default_model_setting = models.JSONField(verbose_name="默认模型设置", default=dict) publish_user_id = models.UUIDField(verbose_name="发布者id", max_length=128, default=None, null=True) publish_user_name = models.CharField(verbose_name="发布者名称", max_length=128, default="") diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py index 3b30b59900a..04deecc87c4 100644 --- a/apps/tools/serializers/tool.py +++ b/apps/tools/serializers/tool.py @@ -768,17 +768,20 @@ def one(self): ) work_flow = {} is_publish = False + default_model_setting = {} if tool.tool_type == "WORKFLOW": tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=tool.id).first() if tool_workflow: work_flow = tool_workflow.work_flow is_publish = tool_workflow.is_publish + default_model_setting = tool_workflow.default_model_setting return { **ToolModelSerializer(tool).data, "init_params": tool.init_params if tool.init_params else {}, "nick_name": nick_name, "fileList": [skill_file_dict] if tool.tool_type == "SKILL" else [], "work_flow": work_flow, + "default_model_setting": default_model_setting, "is_publish": is_publish, } @@ -1028,15 +1031,18 @@ def import_workflow_tools(self, tool, workspace_id, user_id, folder_id, new_chil tool.get("work_flow"), update_tool_map, ) + defaults = {"tool_id": tool.get("id"), "workspace_id": workspace_id, "work_flow": work_flow} + if tool.get("default_model_setting") is not None: + defaults["default_model_setting"] = tool.get("default_model_setting") QuerySet(ToolWorkflow).update_or_create( tool_id=tool.get("id"), create_defaults={ "id": uuid.uuid7(), "tool_id": tool.get("id"), "workspace_id": workspace_id, - "work_flow": work_flow, + **defaults, }, - defaults={"tool_id": tool.get("id"), "workspace_id": workspace_id, "work_flow": work_flow}, + defaults=defaults, ) tool_model_list = [self.to_tool(tool, workspace_id, user_id, folder_id) for tool in tool_list] workflow_tool_model_list = [ diff --git a/apps/tools/serializers/tool_workflow.py b/apps/tools/serializers/tool_workflow.py index 2ad0f167fd0..fb0c299ae22 100644 --- a/apps/tools/serializers/tool_workflow.py +++ b/apps/tools/serializers/tool_workflow.py @@ -28,6 +28,7 @@ McpServersSerializer, get_mcp_tools, validate_bound_tool_permissions, + validate_workflow_default_models, ) from application.serializers.common import ToolExecute from common.database_model_manage.database_model_manage import DatabaseModelManage @@ -176,6 +177,7 @@ def debug(self, instance: Dict, user, with_valid=True): "workspace_id": workspace_id, "user_id": self.data.get("user_id"), **{k: v for k, v in instance.items() if k not in identity_keys}, + "default_model_setting": tool_workflow.default_model_setting, } work_flow_manage = ToolWorkflowManage( Workflow.new_instance(tool_workflow.work_flow, WorkflowMode.TOOL), @@ -209,8 +211,10 @@ def publish(self, with_valid=True): user = QuerySet(User).filter(id=user_id).first() tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=self.data.get("tool_id")).first() workspace_id = tool_workflow.workspace_id + validate_workflow_default_models(tool_workflow.work_flow, tool_workflow.default_model_setting) work_flow_version = ToolWorkflowVersion( work_flow=tool_workflow.work_flow, + default_model_setting=tool_workflow.default_model_setting, tool_id=self.data.get("tool_id"), name=timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S"), publish_user_id=user_id, @@ -297,18 +301,21 @@ def edit(self, instance: Dict): ) if not dependency: raise Exception(gettext("There is a circular dependency in the tool workflow")) + defaults = {"work_flow": instance.get("work_flow")} + if instance.get("default_model_setting") is not None: + defaults["default_model_setting"] = instance.get("default_model_setting") QuerySet(ToolWorkflow).update_or_create( tool_id=self.data.get("tool_id"), create_defaults={ "id": uuid.uuid7(), "tool_id": self.data.get("tool_id"), "workspace_id": workflow_id, - "work_flow": instance.get("work_flow", {}), + **defaults, }, defaults={ "tool_id": self.data.get("tool_id"), "workspace_id": workflow_id, - "work_flow": instance.get("work_flow"), + **defaults, }, ) # 当前用户可修改关联的知识库列表 @@ -477,13 +484,15 @@ def get_appstore_templates(self): def update_resource_mapping_by_tool(tool_id: str, other_resource_mapping=None): - from application.flow.tools import get_instance_resource, save_workflow_mapping + from application.flow.tools import get_instance_resource, save_workflow_mapping, append_default_model_mapping from system_manage.models.resource_mapping import ResourceType if other_resource_mapping is None: other_resource_mapping = [] tool = QuerySet(ToolWorkflow).filter(tool_id=tool_id).first() instance_mapping = get_instance_resource(tool, ResourceType.TOOL, str(tool_id), {}) + instance_mapping = append_default_model_mapping( + instance_mapping, tool.default_model_setting, ResourceType.TOOL, str(tool_id)) save_workflow_mapping(tool.work_flow, ResourceType.TOOL, str(tool_id), instance_mapping + other_resource_mapping) return diff --git a/apps/trigger/handler/impl/task/tool_task/workflow_tool_task.py b/apps/trigger/handler/impl/task/tool_task/workflow_tool_task.py index 9ef050fb92d..4343732b7df 100644 --- a/apps/trigger/handler/impl/task/tool_task/workflow_tool_task.py +++ b/apps/trigger/handler/impl/task/tool_task/workflow_tool_task.py @@ -106,7 +106,8 @@ def execute(self, tool, trigger_task, **kwargs): 'tool_id': tool_id, 'stream': True, 'workspace_id': tool.workspace_id, - **parameters}, + **parameters, + 'default_model_setting': tool_workflow_version.default_model_setting}, ToolWorkflowPostHandler(took_execute, tool_id), is_the_task_interrupted=lambda: False, child_node=None, diff --git a/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue b/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue index ace23946ad5..8cb5e484089 100644 --- a/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue +++ b/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue @@ -394,7 +394,7 @@ class AudioManage { this.statusList.push(AudioStatus.MOUNTED) this.tryList.push(1) index = this.textList.length - 1 - if (this.ttsType === 'TTS') { + if (this.ttsType === 'TTS' || this.ttsType === 'DEFAULT' || this.ttsType === 'CUSTOM') { const audioElement: HTMLAudioElement = document.createElement('audio') audioElement.controls = false audioElement.hidden = true diff --git a/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue b/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue index a0c2fced5d5..904e2cd201b 100644 --- a/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue +++ b/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue @@ -250,7 +250,7 @@ const playAnswerTextPart = () => { } // 调用浏览器的朗读功能 window.speechSynthesis.speak(utterance.value) - } else if (props.tts_type === 'TTS') { + } else if (props.tts_type === 'TTS' || props.tts_type === 'DEFAULT' || props.tts_type === 'CUSTOM') { // 恢复上次暂停的播放 if (audioPlayer.value && audioPlayer.value[currentAudioIndex.value]?.src) { audioPlayer.value[currentAudioIndex.value].play() @@ -301,7 +301,7 @@ const playAnswerTextPart = () => { const pausePlayAnswerText = () => { audioPlayerStatus.value = false - if (props.tts_type === 'TTS') { + if (props.tts_type === 'TTS' || props.tts_type === 'DEFAULT' || props.tts_type === 'CUSTOM') { if (audioPlayer.value) { audioPlayer.value?.forEach((item) => { item.pause() diff --git a/ui/src/components/model-select/index.vue b/ui/src/components/model-select/index.vue index d95afb08988..33039a711b4 100644 --- a/ui/src/components/model-select/index.vue +++ b/ui/src/components/model-select/index.vue @@ -112,8 +112,8 @@ defineOptions({ name: 'ModelSelect' }) const props = defineProps<{ modelValue: any options: any - showFooter?: false - modelType?: '' + showFooter?: boolean + modelType?: string }>() const permissionPrecise = computed(() => { diff --git a/ui/src/components/workflow-dropdown-menu/default-model-setting/index.vue b/ui/src/components/workflow-dropdown-menu/default-model-setting/index.vue new file mode 100644 index 00000000000..6932fc07ba5 --- /dev/null +++ b/ui/src/components/workflow-dropdown-menu/default-model-setting/index.vue @@ -0,0 +1,224 @@ + + + diff --git a/ui/src/locales/lang/en-US/views/application.ts b/ui/src/locales/lang/en-US/views/application.ts index 278aa5faf81..095626c31cd 100644 --- a/ui/src/locales/lang/en-US/views/application.ts +++ b/ui/src/locales/lang/en-US/views/application.ts @@ -52,6 +52,9 @@ export default { tips1: `After enabling, new conversations will be recorded from the activation time and memory will be generated periodically, which can be called through`, tips2: `variables in the system prompt. After disabling, the long-term memory of the conversation users will be cleared, and re-enabling will resume accumulation from the activation point.`, + defaultModel: 'Default Model', + custom: 'Custom', + modelRequiredMessage: 'Please select a long-term memory model', }, form: { appName: { @@ -129,6 +132,8 @@ After disabling, the long-term memory of the conversation users will be cleared, placeholder: 'Please select a speech recognition model', requiredMessage: 'Please select a speech input model', autoSend: 'Automatic Sending', + defaultModel: 'Default Model', + custom: 'Custom', }, voicePlay: { label: 'Voice Playback', @@ -138,6 +143,8 @@ After disabling, the long-term memory of the conversation users will be cleared, browser: 'Browser Playback (free)', tts: 'TTS Model', listeningTest: 'Preview', + defaultModel: 'Default Model', + custom: 'Custom', }, reasoningContent: { label: 'Output Thinking', diff --git a/ui/src/locales/lang/en-US/workflow.ts b/ui/src/locales/lang/en-US/workflow.ts index f322667c8a3..e1bba20f02a 100644 --- a/ui/src/locales/lang/en-US/workflow.ts +++ b/ui/src/locales/lang/en-US/workflow.ts @@ -26,6 +26,26 @@ export default { exit: 'Exit', exitSave: 'Save & Exit', templateCenter: 'Template Center', + defaultModelSetting: 'Default Model Setting', + defaultModelSettingTip: 'Nodes that select "Default Model" will use the configuration below.', + defaultModelNotConfigured: 'Default model not configured', + defaultModelUnavailable: 'Default model unavailable', + defaultModelRequired: 'The default model of this type is not configured', + applyToAll: 'Apply to All Nodes', + apply: 'Apply', + applyToAllConfirmMsg: 'All nodes will have their model source set to "Default Model", overriding their current "Custom / Referencing" settings. Proceed with caution.', + applyToAllSuccess: 'Set {count} node(s) to default model', + applyToAllNone: 'No node needs to be changed', + modelType: { + LLM: 'AI Model', + TTS: 'TTS Model', + STT: 'STT Model', + IMAGE: 'Vision Model', + TTI: 'Image Generation Model', + TTV: 'Text-to-Video Model', + ITV: 'Image-to-Video Model', + RERANKER: 'Rerank Model', + }, }, tip: { noData: 'No related results found', diff --git a/ui/src/locales/lang/zh-CN/views/application.ts b/ui/src/locales/lang/zh-CN/views/application.ts index 0dc2d2088e4..7224d7bb589 100644 --- a/ui/src/locales/lang/zh-CN/views/application.ts +++ b/ui/src/locales/lang/zh-CN/views/application.ts @@ -46,6 +46,9 @@ export default { tips1: `开启后,从开启时间记录新对话并按周期生成记忆,可通过`, tips2: `变量在系统提示词中调用。 关闭后,将清空对话用户的长期记忆,再次开启将重新从开启时点开始累积。`, + defaultModel: '默认模型', + custom: '自定义', + modelRequiredMessage: '请选择长期记忆模型', }, form: { appName: { @@ -119,6 +122,8 @@ export default { placeholder: '请选择语音识别模型', requiredMessage: '请选择语音输入模型', autoSend: '自动发送', + defaultModel: '默认模型', + custom: '自定义', }, voicePlay: { label: '语音播放', @@ -128,6 +133,8 @@ export default { browser: '浏览器播放(免费)', tts: 'TTS 模型', listeningTest: '试听', + defaultModel: '默认模型', + custom: '自定义', }, reasoningContent: { label: '输出思考', diff --git a/ui/src/locales/lang/zh-CN/workflow.ts b/ui/src/locales/lang/zh-CN/workflow.ts index 42cdf5a89f7..ac09c5b2520 100644 --- a/ui/src/locales/lang/zh-CN/workflow.ts +++ b/ui/src/locales/lang/zh-CN/workflow.ts @@ -25,6 +25,26 @@ export default { exit: '直接退出', exitSave: '保存并退出', templateCenter: '模板中心', + defaultModelSetting: '默认模型设置', + defaultModelSettingTip: '节点选择「默认模型」时,将使用以下配置。', + defaultModelNotConfigured: '未配置默认模型', + defaultModelUnavailable: '默认模型暂不可用', + defaultModelRequired: '该类型默认模型未配置', + applyToAll: '应用到所有节点', + apply: '应用', + applyToAllConfirmMsg: '将把所有节点的模型来源统一改为「默认模型」,会覆盖它们当前的「自定义/引用变量」,请谨慎操作。', + applyToAllSuccess: '已将 {count} 个节点设为默认模型', + applyToAllNone: '已没有需要修改的节点', + modelType: { + LLM: 'AI 模型', + TTS: '语音合成模型', + STT: '语音识别模型', + IMAGE: '视觉模型', + TTI: '图片生成模型', + TTV: '文生视频模型', + ITV: '图生视频模型', + RERANKER: '重排模型', + }, }, tip: { noData: '没有找到相关结果', diff --git a/ui/src/locales/lang/zh-Hant/views/application.ts b/ui/src/locales/lang/zh-Hant/views/application.ts index e1105b28fb1..8d0af6b9bb2 100644 --- a/ui/src/locales/lang/zh-Hant/views/application.ts +++ b/ui/src/locales/lang/zh-Hant/views/application.ts @@ -45,6 +45,9 @@ export default { tips1: `開啟後,從開啟時間記錄新對話並按周期生成記憶,可通過`, tips2: `變數在系統提示詞中調用。 關閉後,將清空對話用戶的長期記憶,再次開啟將重新從開啟時點開始累積。`, + defaultModel: '默認模型', + custom: '自訂', + modelRequiredMessage: '請選擇長期記憶模型', }, form: { appName: { @@ -118,6 +121,8 @@ export default { placeholder: '請選擇語音辨識模型', requiredMessage: '請選擇語音輸入模型', autoSend: '自動發送', + defaultModel: '默認模型', + custom: '自訂', }, voicePlay: { label: '語音播放', @@ -127,6 +132,8 @@ export default { browser: '瀏覽器播放(免費)', tts: 'TTS 模型', listeningTest: '試聽', + defaultModel: '默認模型', + custom: '自訂', }, reasoningContent: { label: '輸出思考', diff --git a/ui/src/locales/lang/zh-Hant/workflow.ts b/ui/src/locales/lang/zh-Hant/workflow.ts index 6678d4536a7..b50c925bf4a 100644 --- a/ui/src/locales/lang/zh-Hant/workflow.ts +++ b/ui/src/locales/lang/zh-Hant/workflow.ts @@ -25,6 +25,26 @@ export default { exit: '直接退出', exitSave: '保存並退出', templateCenter: '模板中心', + defaultModelSetting: '預設模型設定', + defaultModelSettingTip: '節點選擇「預設模型」時,將使用以下配置。', + defaultModelNotConfigured: '未設定預設模型', + defaultModelUnavailable: '預設模型暫不可用', + defaultModelRequired: '該類型預設模型未設定', + applyToAll: '套用到所有節點', + apply: '套用', + applyToAllConfirmMsg: '將把所有節點的模型來源統一改為「預設模型」,會覆蓋它們目前的「自訂/引用變數」,請謹慎操作。', + applyToAllSuccess: '已將 {count} 個節點設為預設模型', + applyToAllNone: '已沒有需要修改的節點', + modelType: { + LLM: 'AI 模型', + TTS: '語音合成模型', + STT: '語音辨識模型', + IMAGE: '視覺模型', + TTI: '圖片生成模型', + TTV: '文生影片模型', + ITV: '圖生影片模型', + RERANKER: '重排模型', + }, }, tip: { noData: '沒有找到相關結果', diff --git a/ui/src/views/application-workflow/index.vue b/ui/src/views/application-workflow/index.vue index 7705e1a53cf..36cb3bb3c81 100644 --- a/ui/src/views/application-workflow/index.vue +++ b/ui/src/views/application-workflow/index.vue @@ -38,6 +38,13 @@ {{ $t('workflow.setting.addComponent') }} + + + {{ $t('workflow.setting.defaultModelSetting') }} + {{ $t('common.debug') }} @@ -151,6 +158,14 @@ source="work_flow" @refresh="getDetail" /> + diff --git a/ui/src/workflow/common/data.ts b/ui/src/workflow/common/data.ts index 23fe80e60e2..aa74c050ed7 100644 --- a/ui/src/workflow/common/data.ts +++ b/ui/src/workflow/common/data.ts @@ -51,6 +51,8 @@ export const baseNode = { desc: '', prologue: t('views.application.form.defaultPrologue'), tts_type: 'BROWSER', + stt_model_id_type: 'default', + long_term_model_id_type: 'default', }, config: {}, showNode: true, diff --git a/ui/src/workflow/common/validate.ts b/ui/src/workflow/common/validate.ts index 08c7f8935d8..2b4e7e311e1 100644 --- a/ui/src/workflow/common/validate.ts +++ b/ui/src/workflow/common/validate.ts @@ -399,3 +399,68 @@ export class KnowledgeWorkFlowInstance extends WorkFlowInstance { } } } + +// 节点类型 -> 默认模型类别(与后端 NODE_DEFAULT_MODEL_TYPE 保持一致) +const NODE_DEFAULT_MODEL_TYPE: Record = { + 'ai-chat-node': 'LLM', + 'question-node': 'LLM', + 'intent-node': 'LLM', + 'parameter-extraction-node': 'LLM', + 'image-understand-node': 'IMAGE', + 'video-understand-node': 'IMAGE', + 'image-generate-node': 'TTI', + 'text-to-video-node': 'TTV', + 'image-to-video-node': 'ITV', + 'speech-to-text-node': 'STT', + 'text-to-speech-node': 'TTS', + 'reranker-node': 'RERANKER', +} + +/** + * 与后端 validate_workflow_default_models 对齐: + * 节点选择「默认模型」但对应类别默认模型未配置时,发布前拦截。 + * 抛出 { node, errMessage },由发布流程外层 catch 展示,与「自定义模型为空」等节点校验一致。 + */ +export function validateWorkflowDefaultModels(work_flow: any, default_model_setting: any) { + const setting = default_model_setting || {} + const hasModel = (type: string) => !!((setting[type] || {}) as any).model_id + const errMessage = t('workflow.setting.defaultModelRequired') + + const walk = (nodes: any[]) => { + for (const node of nodes || []) { + const nd = node?.properties?.node_data || {} + const nodeType = node?.type + if (nodeType === 'base-node') { + // base-node 只有 default/custom(tts 另有 BROWSER) + if (nd.stt_model_enable && nd.stt_model_id_type === 'default' && !hasModel('STT')) { + throw { node, errMessage } + } + if (nd.tts_model_enable && nd.tts_type === 'DEFAULT' && !hasModel('TTS')) { + throw { node, errMessage } + } + if (nd.long_term_enable && nd.long_term_model_id_type === 'default' && !hasModel('LLM')) { + throw { node, errMessage } + } + continue + } + const modelType = NODE_DEFAULT_MODEL_TYPE[nodeType] + if (modelType) { + const typeKey = + nodeType === 'reranker-node' + ? 'reranker_model_id_type' + : nodeType === 'speech-to-text-node' + ? 'stt_model_id_type' + : nodeType === 'text-to-speech-node' + ? 'tts_model_id_type' + : 'model_id_type' + if ((nd[typeKey] || 'custom') === 'default' && !hasModel(modelType)) { + throw { node, errMessage } + } + } + if (nodeType === 'loop-node' && nd.loop_body?.nodes) { + walk(nd.loop_body.nodes) + } + } + } + walk(work_flow?.nodes) +} diff --git a/ui/src/workflow/nodes/ai-chat-node/index.vue b/ui/src/workflow/nodes/ai-chat-node/index.vue index abd7b5b0409..d4a765feecb 100644 --- a/ui/src/workflow/nodes/ai-chat-node/index.vue +++ b/ui/src/workflow/nodes/ai-chat-node/index.vue @@ -15,7 +15,7 @@ :label="$t('views.application.form.aiModel.label')" :prop="chat_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: chat_data.model_id_type !== 'default', message: chat_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="chat_data.model_id_reference = []" > + -
+
+ import { cloneDeep, set, groupBy } from 'lodash' import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import NodeCascader from '@/workflow/common/NodeCascader.vue' import type { FormInstance } from 'element-plus' import { ref, computed, onMounted, inject, reactive } from 'vue' @@ -693,7 +700,7 @@ const collapseData = reactive({ const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], system: '', prompt: defaultPrompt, diff --git a/ui/src/workflow/nodes/base-node/index.vue b/ui/src/workflow/nodes/base-node/index.vue index 6f35dfba0b3..4d95f40acfa 100644 --- a/ui/src/workflow/nodes/base-node/index.vue +++ b/ui/src/workflow/nodes/base-node/index.vue @@ -77,7 +77,21 @@ -
+
+ + + + +
+ +
+
+ + + + +
+ - + +
+
import { groupBy, set } from 'lodash' import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import type { FormInstance } from 'element-plus' import { computed, inject, nextTick, onMounted, provide, ref } from 'vue' import { MsgSuccess } from '@/utils/message' @@ -311,20 +343,34 @@ const baseNodeFormRef = ref() const validate = () => { if ( form_data.value.tts_model_enable && - !form_data.value.tts_model_id && - form_data.value.tts_type === 'TTS' + form_data.value.tts_type === 'CUSTOM' && + !form_data.value.tts_model_id ) { return Promise.reject({ node: props.nodeModel, errMessage: t('views.application.form.voicePlay.requiredMessage'), }) } - if (form_data.value.stt_model_enable && !form_data.value.stt_model_id) { + if ( + form_data.value.stt_model_enable && + form_data.value.stt_model_id_type === 'custom' && + !form_data.value.stt_model_id + ) { return Promise.reject({ node: props.nodeModel, errMessage: t('views.application.form.voiceInput.requiredMessage'), }) } + if ( + form_data.value.long_term_enable && + form_data.value.long_term_model_id_type === 'custom' && + !form_data.value.long_term_model_id + ) { + return Promise.reject({ + node: props.nodeModel, + errMessage: t('views.application.longTermMemory.modelRequiredMessage'), + }) + } const fieldList = props.nodeModel.properties?.user_input_field_list || [] for (const field of fieldList) { @@ -408,6 +454,9 @@ function ttsModelChange() { } function ttsModelEnableChange() { + if (form_data.value.tts_model_enable && !form_data.value.tts_type) { + form_data.value.tts_type = 'BROWSER' + } if (!form_data.value.tts_model_enable) { form_data.value.tts_model_id = '' form_data.value.tts_type = 'BROWSER' @@ -415,6 +464,9 @@ function ttsModelEnableChange() { } function sttModelEnableChange() { + if (form_data.value.stt_model_enable && form_data.value.stt_model_id_type === undefined) { + form_data.value.stt_model_id_type = 'default' + } if (!form_data.value.stt_model_enable) { form_data.value.stt_model_id = '' } @@ -473,6 +525,9 @@ const long_term_model_change = (model_id?: string) => { } function switchLongTerm() { + if (form_data.value.long_term_enable && form_data.value.long_term_model_id_type === undefined) { + form_data.value.long_term_model_id_type = 'default' + } props.nodeModel.graphModel.eventCenter.emit('refreshLongTermConfig') } @@ -531,8 +586,17 @@ function getSelectModel() { onMounted(() => { set(props.nodeModel, 'validate', validate) - if (!props.nodeModel.properties.node_data.tts_type) { - set(props.nodeModel.properties.node_data, 'tts_type', 'BROWSER') + const nd = props.nodeModel.properties.node_data + if (!nd.tts_type) { + set(nd, 'tts_type', 'BROWSER') + } else if (nd.tts_type === 'TTS') { + set(nd, 'tts_type', 'CUSTOM') + } + if (nd.stt_model_id_type === undefined) { + set(nd, 'stt_model_id_type', nd.stt_model_id ? 'custom' : 'default') + } + if (nd.long_term_model_id_type === undefined) { + set(nd, 'long_term_model_id_type', nd.long_term_model_id ? 'custom' : 'default') } getTTSModel() getSTTModel() diff --git a/ui/src/workflow/nodes/image-generate/index.vue b/ui/src/workflow/nodes/image-generate/index.vue index 2634e5350f7..e8634d437dd 100644 --- a/ui/src/workflow/nodes/image-generate/index.vue +++ b/ui/src/workflow/nodes/image-generate/index.vue @@ -15,7 +15,7 @@ :label="$t('workflow.nodes.imageGenerateNode.model.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > +
-
+
+ import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import { computed, nextTick, onMounted, ref, inject } from 'vue' import { groupBy, set } from 'lodash' import type { FormInstance } from 'element-plus' @@ -232,7 +239,7 @@ const defaultPrompt = `{{${t('workflow.nodes.startNode.label')}.question}}` const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], system: '', prompt: defaultPrompt, diff --git a/ui/src/workflow/nodes/image-to-video/index.vue b/ui/src/workflow/nodes/image-to-video/index.vue index 111dd8677b6..8eaf46d7075 100644 --- a/ui/src/workflow/nodes/image-to-video/index.vue +++ b/ui/src/workflow/nodes/image-to-video/index.vue @@ -15,7 +15,7 @@ :label="$t('workflow.nodes.imageToVideoGenerate.model.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import { computed, nextTick, onMounted, ref, inject } from 'vue' import { groupBy, set } from 'lodash' import type { FormInstance } from 'element-plus' @@ -296,7 +303,7 @@ const defaultPrompt = `{{${t('workflow.nodes.startNode.label')}.question}}` const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], system: '', prompt: defaultPrompt, diff --git a/ui/src/workflow/nodes/image-understand/index.vue b/ui/src/workflow/nodes/image-understand/index.vue index 9843646fa85..5d44063d97d 100644 --- a/ui/src/workflow/nodes/image-understand/index.vue +++ b/ui/src/workflow/nodes/image-understand/index.vue @@ -15,7 +15,7 @@ :label="$t('workflow.nodes.imageUnderstandNode.model.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import { computed, onMounted, ref, inject } from 'vue' import { cloneDeep, groupBy, set } from 'lodash' import NodeCascader from '@/workflow/common/NodeCascader.vue' @@ -334,7 +341,7 @@ const defaultPrompt = `{{${t('workflow.nodes.startNode.label')}.question}}` const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], system: '', prompt: defaultPrompt, diff --git a/ui/src/workflow/nodes/intent-classify-node/index.vue b/ui/src/workflow/nodes/intent-classify-node/index.vue index d3120b08e96..9cf6433b9d5 100644 --- a/ui/src/workflow/nodes/intent-classify-node/index.vue +++ b/ui/src/workflow/nodes/intent-classify-node/index.vue @@ -15,7 +15,7 @@ :label="$t('views.application.form.aiModel.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ { const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], branch: [ { diff --git a/ui/src/workflow/nodes/parameter-extraction-node/index.vue b/ui/src/workflow/nodes/parameter-extraction-node/index.vue index d714a30bc70..8e9fe8b551b 100644 --- a/ui/src/workflow/nodes/parameter-extraction-node/index.vue +++ b/ui/src/workflow/nodes/parameter-extraction-node/index.vue @@ -15,7 +15,7 @@ :label="$t('views.application.form.aiModel.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ import { computed, onMounted, ref, inject } from 'vue' import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import NodeCascader from '@/workflow/common/NodeCascader.vue' import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue' import ParametersFieldTable from '@/workflow/nodes/parameter-extraction-node/component/ParametersFieldTable.vue' @@ -185,7 +192,7 @@ const form = { input_variable: [], model_params_setting: {}, model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], variable_list: [], } diff --git a/ui/src/workflow/nodes/question-node/index.vue b/ui/src/workflow/nodes/question-node/index.vue index e2f8fb18492..65ae738d83b 100644 --- a/ui/src/workflow/nodes/question-node/index.vue +++ b/ui/src/workflow/nodes/question-node/index.vue @@ -15,7 +15,7 @@ :label="$t('views.application.form.aiModel.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ + -
+
+ import { set, cloneDeep, groupBy } from 'lodash' import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import NodeCascader from '@/workflow/common/NodeCascader.vue' import ParamSettingDialog from './ParamSettingDialog.vue' import { ref, computed, onMounted, inject } from 'vue' @@ -222,7 +229,7 @@ const ParamSettingDialogRef = ref>() const form = { reranker_reference_list: [[]], reranker_model_id: '', - reranker_model_id_type: 'custom', + reranker_model_id_type: 'default', reranker_model_id_reference: [], question_reference_address: [], reranker_setting: { diff --git a/ui/src/workflow/nodes/speech-to-text-node/index.vue b/ui/src/workflow/nodes/speech-to-text-node/index.vue index eb0a9bbf7fd..596e26c2323 100644 --- a/ui/src/workflow/nodes/speech-to-text-node/index.vue +++ b/ui/src/workflow/nodes/speech-to-text-node/index.vue @@ -17,7 +17,7 @@ form_data.stt_model_id_type === 'reference' ? 'stt_model_id_reference' : 'stt_model_id' " :rules="{ - required: true, + required: form_data.stt_model_id_type !== 'default', message: form_data.stt_model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -40,12 +40,13 @@ style="width: 85px" @change="form_data.stt_model_id_reference = []" > +
-
+
+ import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import { computed, onMounted, ref, inject } from 'vue' import { groupBy, set } from 'lodash' import NodeCascader from '@/workflow/common/NodeCascader.vue' @@ -203,7 +210,7 @@ const wheel = (e: any) => { const form = { stt_model_id: '', - stt_model_id_type: 'custom', + stt_model_id_type: 'default', stt_model_id_reference: [], is_result: true, audio_list: [], diff --git a/ui/src/workflow/nodes/text-to-speech-node/index.vue b/ui/src/workflow/nodes/text-to-speech-node/index.vue index 3bbeeb0cc55..b64f83ecd9e 100644 --- a/ui/src/workflow/nodes/text-to-speech-node/index.vue +++ b/ui/src/workflow/nodes/text-to-speech-node/index.vue @@ -17,7 +17,7 @@ form_data.tts_model_id_type === 'reference' ? 'tts_model_id_reference' : 'tts_model_id' " :rules="{ - required: true, + required: form_data.tts_model_id_type !== 'default', message: form_data.tts_model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -40,12 +40,13 @@ style="width: 85px" @change="form_data.tts_model_id_reference = []" > + -
+
+ { const form = { tts_model_id: '', - tts_model_id_type: 'custom', + tts_model_id_type: 'default', tts_model_id_reference: [], is_result: true, content_list: [], diff --git a/ui/src/workflow/nodes/text-to-video/index.vue b/ui/src/workflow/nodes/text-to-video/index.vue index ccf8d9dbe54..0ab3983517a 100644 --- a/ui/src/workflow/nodes/text-to-video/index.vue +++ b/ui/src/workflow/nodes/text-to-video/index.vue @@ -15,7 +15,7 @@ :label="$t('workflow.nodes.textToVideoGenerate.model.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import { computed, nextTick, onMounted, ref, inject } from 'vue' import { groupBy, set } from 'lodash' import type { FormInstance } from 'element-plus' @@ -232,7 +239,7 @@ const defaultPrompt = `{{${t('workflow.nodes.startNode.label')}.question}}` const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], system: '', prompt: defaultPrompt, diff --git a/ui/src/workflow/nodes/video-understand/index.vue b/ui/src/workflow/nodes/video-understand/index.vue index 4d69f61ff3e..c49a5a88f87 100644 --- a/ui/src/workflow/nodes/video-understand/index.vue +++ b/ui/src/workflow/nodes/video-understand/index.vue @@ -15,7 +15,7 @@ :label="$t('workflow.nodes.videoUnderstandNode.model.label')" :prop="form_data.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'" :rules="{ - required: true, + required: form_data.model_id_type !== 'default', message: form_data.model_id_type === 'reference' ? $t('workflow.variable.placeholder') @@ -38,12 +38,13 @@ style="width: 85px" @change="form_data.model_id_reference = []" > + -
+
+ import NodeContainer from '@/workflow/common/NodeContainer.vue' +import DefaultModelDisplay from '@/workflow/common/DefaultModelDisplay.vue' import { computed, onMounted, ref, inject } from 'vue' import { cloneDeep, groupBy, set } from 'lodash' import NodeCascader from '@/workflow/common/NodeCascader.vue' @@ -335,7 +342,7 @@ const defaultPrompt = `{{${t('workflow.nodes.startNode.label')}.question}}` const form = { model_id: '', - model_id_type: 'custom', + model_id_type: 'default', model_id_reference: [], system: '', prompt: defaultPrompt,