From 1a2296581385bddbcd74539508335508196ef53b Mon Sep 17 00:00:00 2001 From: shenkaibo Date: Tue, 1 Sep 2026 16:27:17 +0800 Subject: [PATCH] fix: overhaul default model settings across workflow apps and tools One combined change set covering default-model configuration for workflow applications and tools, from Persistence/execution correctness through the panel UI and per-category parameter dialog. Bugs and issues fixed: - [Copy app] Copying a workflow application dropped its default model configuration: the WorkflowRequest serializer neither declared nor mapped default_model_setting, so DRF silently discarded it and the copy had empty dropdowns in the default-model settings panel. - [Copy tool] Copying a workflow tool lost its default model settings: tool creation (ToolSerializer.Create.insert) built the ToolWorkflow row with only work_flow. default_model_setting is now passed through so the copy keeps it. - [Execution] A node in "default" model mode kept running the last configured model after that default was cleared: base step-node impls left stale node_data.model_id. They now always resolve model_id from default_model_setting in "default" mode (None when unconfigured) so debug fails fast with the existing empty-model error, matching publish-time validation across all 13 affected step nodes. - [Panel staging] Default-model-setting panel: closing without save let nodes pick up non-persisted config and debug still succeeded. Edits are now staged locally and committed only on save, with an unsaved-changes confirm (save/discard/cancel) on close; panel mounts via v-if so its click-outside listener is not live while collapsed. - [Publish highlight] Missing default model blocks publish validation but the offending problem node was not highlighted on the canvas; it is now located and highlighted when publish validation fails. - [Long-term memory] Long-term memory AI model config field position did not match the prototype; repositioned to match. - [Save button] Default-model-settings save button stayed enabled with nothing changed; now disabled when there are no unstaged modifications. - [Readonly] Read-only users could not see/open the default-model settings at all (button gated on edit-level permission). Re-gated on read-level (debug/read) and the panel is view-only for them (model select, param button, apply-to-all, save disabled; hasChanges always false). - [Reranker] Parameter-settings button in the default-model panel was enabled for RERANKER, which has no parameter form; disabled for that category. - [Param dialog flash] Opening a model's parameter config flashed the previous category's content (~1s) before the correct one: AIModeParamSettingDialog reused the prior model_form_field on a destroy-on-close remount. Fields are now cleared before the dialog shows, and the body is wrapped in a v-loading mask covering the async getModelParamsForm window. --- .../ai_chat_step_node/impl/base_chat_node.py | 3 +- .../impl/base_image_generate_node.py | 3 +- .../impl/base_image_to_video_node.py | 3 +- .../impl/base_image_understand_node.py | 3 +- .../intent_node/impl/base_intent_node.py | 3 +- .../i_parameter_extraction_node.py | 3 +- .../question_node/impl/base_question_node.py | 3 +- .../reranker_node/i_reranker_node.py | 3 +- .../impl/base_speech_to_text_node.py | 3 +- .../impl/base_text_to_speech_node.py | 3 +- .../impl/base_text_to_video_node.py | 3 +- .../impl/base_video_understand_node.py | 3 +- apps/application/serializers/application.py | 24 ++-- 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 + apps/tools/serializers/tool.py | 7 +- .../default-model-setting/index.vue | 87 ++++++++++-- .../locales/lang/en-US/views/application.ts | 1 + ui/src/locales/lang/en-US/workflow.ts | 4 + .../locales/lang/zh-CN/views/application.ts | 1 + ui/src/locales/lang/zh-CN/workflow.ts | 4 + .../locales/lang/zh-Hant/views/application.ts | 1 + ui/src/locales/lang/zh-Hant/workflow.ts | 4 + ui/src/views/application-workflow/index.vue | 5 +- .../views/application/ApplicationSetting.vue | 8 +- .../component/AIModeParamSettingDialog.vue | 31 +++-- .../component/LongTermSettingDialog.vue | 131 +++++++++++++++++- ui/src/views/knowledge-workflow/index.vue | 5 +- ui/src/views/tool-workflow/index.vue | 5 +- ui/src/workflow/common/validate.ts | 19 ++- ui/src/workflow/index.vue | 26 ++++ ui/src/workflow/nodes/base-node/index.vue | 108 +++------------ 33 files changed, 354 insertions(+), 162 deletions(-) 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 cd27c0bb918..53d39174d37 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 @@ -205,8 +205,7 @@ def execute( 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_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 db2f723511d..d415a2acfae 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 @@ -38,8 +38,7 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t 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_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 == '': 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 08cb0f0ad55..b58f463e6e5 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 @@ -41,8 +41,7 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t 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_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_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 f6680c3764d..a577fb435df 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 @@ -154,8 +154,7 @@ def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, hist 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_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 == '': 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 f8c3f6c84d7..b516ec0ab9d 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 @@ -66,8 +66,7 @@ def execute(self, model_id, dialogue_number, history_chat_record, user_input, br 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_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 c1c3667b055..2b68fd53c67 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 @@ -49,8 +49,7 @@ def _run(self): 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_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( 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 c984373d574..f62266f7f63 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 @@ -97,8 +97,7 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record 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_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 216cc0afaa0..d91bd515785 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 @@ -75,8 +75,7 @@ def _run(self): 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') + 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 371bc2bcba6..a91f176e5a1 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 @@ -34,8 +34,7 @@ def execute(self, stt_model_id, audio, model_params_setting=None, stt_model_id_t 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') + 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 1ba9c0bb801..6282e9f4dad 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 @@ -58,8 +58,7 @@ def execute(self, 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') + 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 dedcd138acf..4f690779381 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 @@ -39,8 +39,7 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t 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_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 == '': 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 f819e82d286..12ea746a365 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 @@ -149,8 +149,7 @@ def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, hist 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_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/serializers/application.py b/apps/application/serializers/application.py index 157aaa5abe1..321300c45a8 100644 --- a/apps/application/serializers/application.py +++ b/apps/application/serializers/application.py @@ -54,6 +54,7 @@ from langchain_mcp_adapters.client import MultiServerMCPClient from maxkb.conf import PROJECT_DIR from maxkb.const import CONFIG +from models_provider.base_model_provider import ModelTypeConst from models_provider.models import Model from models_provider.tools import get_model_instance_by_model_workspace_id from rest_framework import serializers, status @@ -97,6 +98,17 @@ def _walk_workflow_nodes(work_flow, collector): } +def _default_model_not_configured_message(node_name: str, model_type: str) -> str: + """节点选了默认模型但该类别默认模型未配置时的报错,与前端「{节点名称}节点,{模型类别}的默认模型未配置」文案一致。""" + try: + model_type_label = str(ModelTypeConst[model_type].value['message']) + except Exception: + model_type_label = model_type + return _( + "{node_name} Node, the default model for {model_type_label} is not configured" + ).format(node_name=node_name, model_type_label=model_type_label) + + def validate_workflow_default_models(work_flow, default_model_setting): """发布前校验:节点选择「默认模型」但对应类别默认模型未配置时,禁止发布并定位。""" if not work_flow: @@ -117,10 +129,7 @@ def validate_workflow_default_models(work_flow, default_model_setting): 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), + _default_model_not_configured_message(node_name, default_model_type), ) if model_type is not None: # 取该节点实际使用的 model_id_type 字段(custom/reference/default) @@ -142,10 +151,7 @@ def validate_workflow_default_models(work_flow, default_model_setting): ).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), + _default_model_not_configured_message(node_name, model_type), ) if node_type == "loop-node": validate_workflow_default_models(node_data.get("loop_body"), default_model_setting) @@ -465,6 +471,7 @@ class WorkflowRequest(serializers.Serializer): required=False, allow_null=True, allow_blank=True, max_length=102400, label=_("Opening remarks") ) folder_id = serializers.CharField(required=True, label=_("folder id")) + default_model_setting = serializers.DictField(required=False, label=_("Default model settings")) @staticmethod def to_application_model(user_id: str, workspace_id: str, application: Dict): @@ -497,6 +504,7 @@ def to_application_model(user_id: str, workspace_id: str, application: Dict): file_upload_enable=application.get("file_upload_enable", False), file_upload_setting=application.get("file_upload_setting", {}), work_flow=default_workflow, + default_model_setting=application.get("default_model_setting") or {}, ) class SimplateRequest(serializers.Serializer): diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 9eebebaa08b..78c598020ba 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -8691,6 +8691,9 @@ msgstr "" msgid "{node_name} selected the default model, but the default model of this type is not configured." msgstr "" +msgid "{node_name} Node, the default model for {model_type_label} 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 ff4bfeb6a8f..8131866a881 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -8815,6 +8815,9 @@ msgstr "图生视频" msgid "{node_name} selected the default model, but the default model of this type is not configured." msgstr "{node_name}选择了默认模型,但未配置该类默认模型" +msgid "{node_name} Node, the default model for {model_type_label} is not configured" +msgstr "{node_name} 节点,{model_type_label}的默认模型未配置" + 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 3d69eeaa926..54ca6ac533a 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -8815,6 +8815,9 @@ msgstr "圖生視頻" msgid "{node_name} selected the default model, but the default model of this type is not configured." msgstr "{node_name}選擇了預設模型,但未配置該類預設模型" +msgid "{node_name} Node, the default model for {model_type_label} is not configured" +msgstr "{node_name} 節點,{model_type_label}的預設模型未配置" + msgid "Authentication failed. Please verify that the parameters are correct" msgstr "認證失敗,請檢查參數是否正確" diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py index 04deecc87c4..5f7773af812 100644 --- a/apps/tools/serializers/tool.py +++ b/apps/tools/serializers/tool.py @@ -524,7 +524,12 @@ def insert(self, instance, with_valid=True): } ).auth_resource(str(tool_id)) if instance.get("tool_type") == ToolType.WORKFLOW: - ToolWorkflow(id=uuid.uuid7(), tool_id=tool_id, work_flow=instance.get("work_flow", {})).save() + ToolWorkflow( + id=uuid.uuid7(), + tool_id=tool_id, + work_flow=instance.get("work_flow", {}), + default_model_setting=instance.get("default_model_setting") or {}, + ).save() # 如果是SKILL类型的工具,修改file表中对应的记录 if instance.get("tool_type") == ToolType.SKILL: file_id = instance.get("code") 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 index 6932fc07ba5..a02099e2dc3 100644 --- a/ui/src/components/workflow-dropdown-menu/default-model-setting/index.vue +++ b/ui/src/components/workflow-dropdown-menu/default-model-setting/index.vue @@ -1,6 +1,6 @@