diff --git a/apps/application/migrations/0019_applicationversion_publish_desc.py b/apps/application/migrations/0019_applicationversion_publish_desc.py new file mode 100644 index 00000000000..f9c071ff985 --- /dev/null +++ b/apps/application/migrations/0019_applicationversion_publish_desc.py @@ -0,0 +1,17 @@ +# Generated by Django 6.1 on 2026-09-16 08:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("application", "0018_application_default_model_setting_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="applicationversion", + name="publish_desc", + field=models.CharField(default="", max_length=1024, verbose_name="更新说明"), + ), + ] diff --git a/apps/application/models/application.py b/apps/application/models/application.py index bfbbb470861..f81e48cad33 100644 --- a/apps/application/models/application.py +++ b/apps/application/models/application.py @@ -165,6 +165,7 @@ class ApplicationVersion(AppModelMixin): name = models.CharField(verbose_name="版本名称", max_length=128, default="") 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="") + publish_desc = models.CharField(verbose_name="更新说明", max_length=1024, default="") workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) application_name = models.CharField(max_length=128, verbose_name="应用名称") desc = models.CharField(max_length=512, verbose_name="引用描述", default="") diff --git a/apps/application/serializers/application.py b/apps/application/serializers/application.py index a115a98bcc5..6c9ef959e85 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.workflow.common import Workflow +from application.workflow.common import new_instance 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 @@ -1107,6 +1107,12 @@ class ApplicationOperateSerializer(serializers.Serializer): application_id = serializers.UUIDField(required=True, label=_("Application ID")) user_id = serializers.UUIDField(required=True, label=_("User ID")) workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) + publish_name = serializers.CharField( + required=False, max_length=128, allow_null=True, allow_blank=True, label=_("Publish Name") + ) + publish_desc = serializers.CharField( + required=False, max_length=1024, allow_null=True, allow_blank=True, label=_("Publish Desc") + ) def is_valid(self, *, raise_exception=False): super().is_valid(raise_exception=True) @@ -1260,6 +1266,10 @@ def publish(self, instance, with_valid=True): self.is_valid() user_id = self.data.get("user_id") workspace_id = self.data.get("workspace_id") + name = (instance or {}).get("publish_name") + if not name or not str(name).strip(): + raise AppApiException(500, _("publish_name is required")) + publish_desc = (instance or {}).get("publish_desc") or "" user = QuerySet(User).filter(id=user_id).first() application = ( QuerySet(Application).filter(id=self.data.get("application_id"), workspace_id=workspace_id).first() @@ -1268,7 +1278,7 @@ def publish(self, instance, with_valid=True): work_flow = application.work_flow if work_flow is None: raise AppApiException(500, _("work_flow is a required field")) - Workflow.new_instance(work_flow).is_valid() + new_instance(work_flow).is_valid() base_node = get_base_node_work_flow(work_flow) if base_node is not None: node_data = base_node.get("properties").get("node_data") @@ -1283,9 +1293,10 @@ def publish(self, instance, with_valid=True): work_flow_version = ApplicationVersion( work_flow=application.work_flow, application=application, - name=timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S"), + name=name, publish_user_id=user_id, publish_user_name=user.username, + publish_desc=publish_desc, workspace_id=workspace_id, ) self.reset_application_version(work_flow_version, application) diff --git a/apps/application/serializers/application_version.py b/apps/application/serializers/application_version.py index 5856220336f..ac0fcf49de8 100644 --- a/apps/application/serializers/application_version.py +++ b/apps/application/serializers/application_version.py @@ -1,11 +1,12 @@ # coding=utf-8 """ - @project: MaxKB - @Author:虎虎 - @file: application_version.py - @date:2025/6/3 16:25 - @desc: +@project: MaxKB +@Author:虎虎 +@file: application_version.py +@date:2025/6/3 16:25 +@desc: """ + from typing import Dict from django.db.models import QuerySet @@ -19,21 +20,33 @@ class ApplicationVersionQuerySerializer(serializers.Serializer): application_id = serializers.UUIDField(required=True, label=_("Application ID")) - name = serializers.CharField(required=False, allow_null=True, allow_blank=True, - label=_("summary")) + name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("summary")) class ApplicationVersionModelSerializer(serializers.ModelSerializer): class Meta: model = ApplicationVersion - fields = ['id', 'name', 'workspace_id', 'application_id', 'work_flow', 'publish_user_id', 'publish_user_name', - 'create_time', - 'update_time'] + fields = [ + "id", + "name", + "publish_desc", + "workspace_id", + "application_id", + "work_flow", + "publish_user_id", + "publish_user_name", + "create_time", + "update_time", + ] class ApplicationVersionEditSerializer(serializers.Serializer): - name = serializers.CharField(required=False, max_length=128, allow_null=True, allow_blank=True, - label=_("Version Name")) + name = serializers.CharField( + required=False, max_length=128, allow_null=True, allow_blank=True, label=_("Version Name") + ) + publish_desc = serializers.CharField( + required=False, max_length=1024, allow_null=True, allow_blank=True, label=_("Publish Description") + ) class ApplicationVersionSerializer(serializers.Serializer): @@ -43,11 +56,11 @@ class Query(serializers.Serializer): workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) def get_query_set(self, query): - query_set = QuerySet(ApplicationVersion).filter(application_id=query.get('application_id')) - if 'name' in query and query.get('name') is not None: - query_set = query_set.filter(name__contains=query.get('name')) - if 'workspace_id' in self.data and self.data.get('workspace_id') is not None: - query_set = query_set.filter(workspace_id=self.data.get('workspace_id')) + query_set = QuerySet(ApplicationVersion).filter(application_id=query.get("application_id")) + if "name" in query and query.get("name") is not None: + query_set = query_set.filter(name__contains=query.get("name")) + if "workspace_id" in self.data and self.data.get("workspace_id") is not None: + query_set = query_set.filter(workspace_id=self.data.get("workspace_id")) return query_set.order_by("-create_time") def list(self, query, with_valid=True): @@ -60,48 +73,57 @@ def list(self, query, with_valid=True): def page(self, query, current_page, page_size, with_valid=True): if with_valid: self.is_valid(raise_exception=True) - return page_search(current_page, page_size, - self.get_query_set(query), - post_records_handler=lambda v: ApplicationVersionModelSerializer(v).data) + return page_search( + current_page, + page_size, + self.get_query_set(query), + post_records_handler=lambda v: ApplicationVersionModelSerializer(v).data, + ) class Operate(serializers.Serializer): workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) application_id = serializers.UUIDField(required=True, label=_("Application ID")) - application_version_id = serializers.UUIDField(required=True, - label=_("Application version ID")) + application_version_id = serializers.UUIDField(required=True, label=_("Application version ID")) def is_valid(self, *, raise_exception=False): super().is_valid(raise_exception=True) - workspace_id = self.data.get('workspace_id') - query_set = QuerySet(Application).filter(id=self.data.get('application_id')) + workspace_id = self.data.get("workspace_id") + query_set = QuerySet(Application).filter(id=self.data.get("application_id")) if workspace_id: query_set = query_set.filter(workspace_id=workspace_id) if not query_set.exists(): - raise AppApiException(500, _('Application id does not exist')) + raise AppApiException(500, _("Application id does not exist")) def one(self, with_valid=True): if with_valid: self.is_valid(raise_exception=True) - application_version = QuerySet(ApplicationVersion).filter(application_id=self.data.get('application_id'), - id=self.data.get( - 'application_version_id')).first() + application_version = ( + QuerySet(ApplicationVersion) + .filter(application_id=self.data.get("application_id"), id=self.data.get("application_version_id")) + .first() + ) if application_version is not None: return ApplicationVersionModelSerializer(application_version).data else: - raise AppApiException(500, _('Workflow version does not exist')) + raise AppApiException(500, _("Workflow version does not exist")) def edit(self, instance: Dict, with_valid=True): if with_valid: self.is_valid(raise_exception=True) ApplicationVersionEditSerializer(data=instance).is_valid(raise_exception=True) - application_version = QuerySet(ApplicationVersion).filter(application_id=self.data.get('application_id'), - id=self.data.get( - 'application_version_id')).first() + application_version = ( + QuerySet(ApplicationVersion) + .filter(application_id=self.data.get("application_id"), id=self.data.get("application_version_id")) + .first() + ) if application_version is not None: - name = instance.get('name', None) + name = instance.get("name", None) + publish_desc = instance.get("publish_desc", None) if name is not None and len(name) > 0: application_version.name = name + if publish_desc is not None and len(publish_desc) > 0: + application_version.publish_desc = publish_desc application_version.save() return ApplicationVersionModelSerializer(application_version).data else: - raise AppApiException(500, _('Workflow version does not exist')) + raise AppApiException(500, _("Workflow version does not exist")) diff --git a/apps/application/workflow/common.py b/apps/application/workflow/common.py index b5bde9265f8..f386939b835 100644 --- a/apps/application/workflow/common.py +++ b/apps/application/workflow/common.py @@ -194,6 +194,9 @@ def reset_prompt(self, prompt): prompt = node_field.reset_variable(prompt) return prompt + def is_valid(self): + pass + class WorkflowType(Enum): # 应用 diff --git a/ui/src/api/admin/workspace/application/application.ts b/ui/src/api/admin/workspace/application/application.ts index 58b48d12a96..6b9ad58522f 100644 --- a/ui/src/api/admin/workspace/application/application.ts +++ b/ui/src/api/admin/workspace/application/application.ts @@ -69,8 +69,11 @@ const putBatchMoveApplications = (applicationIds: string[], folderId: string) => } /** 发布工作空间智能体。 */ -const putApplicationPublish = (applicationId: string) => { - return put, ApplicationDetail>(`${getPrefix()}/${applicationId}/publish`, {}) +const putApplicationPublish = (applicationId: string, publishName: string, publishDesc?: string) => { + return put, ApplicationDetail>(`${getPrefix()}/${applicationId}/publish`, { + publish_name: publishName, + publish_desc: publishDesc, + }) } /** 使用指定模型流式生成或优化系统提示词。 */ diff --git a/ui/src/api/types/workflow-version.ts b/ui/src/api/types/workflow-version.ts index 97d02405ccc..ac162e41c00 100644 --- a/ui/src/api/types/workflow-version.ts +++ b/ui/src/api/types/workflow-version.ts @@ -5,7 +5,7 @@ export interface WorkflowVersion { id: string name: string /** 更新说明;旧版本接口可能不返回。 */ - description?: string | null + publish_desc?: string | null work_flow: LogicFlow.GraphConfigData publish_user_name: string create_time: string @@ -15,5 +15,5 @@ export interface WorkflowVersion { /** 发布历史的标题和更新说明编辑内容。 */ export interface WorkflowVersionPayload { name: string - description: string + publish_desc: string } diff --git a/ui/src/views/workflow/application/ApplicationWorkflowView.vue b/ui/src/views/workflow/application/ApplicationWorkflowView.vue index 0fc31a8c925..fd2c8623c85 100644 --- a/ui/src/views/workflow/application/ApplicationWorkflowView.vue +++ b/ui/src/views/workflow/application/ApplicationWorkflowView.vue @@ -16,6 +16,7 @@ import WorkflowViewLayout from '../components/WorkflowViewLayout.vue' import ButtonPublishHistory from './ButtonPublishHistory.vue' import TemplateStoreDialog from '@/views/application/template-store/TemplateStoreDialog.vue' import DebugPanel from './debug/DebugPanel.vue' +import ApplicationPublishDialog from './components/ApplicationPublishDialog.vue' import { getResourceScope } from '@/utils/resource-context.ts' import { goBack } from './navigation' @@ -211,23 +212,21 @@ function handleSaveDefaultModelSetting(settings: DefaultModelSettingPayload) { return handleSave() } -/* 发布工作流 */ +/* 发布工作流:先校验画布,再由弹窗收集发布标题与更新说明 */ +const publishDialogRef = useTemplateRef>('publishDialogRef') + function handlePublish() { workflowRef.value?.validate().then(() => { - publishing.value = true - return saveApplication(undefined, false) - .then(() => ApplicationApi.putApplicationPublish(applicationId)) - .then((application) => { - applicationDetail.value = application - saveTime.value = application.update_time || saveTime.value - MsgSuccess('发布成功') - }) - .finally(() => { - publishing.value = false - }) + publishDialogRef.value?.open() }) } +/* 发布成功:更新详情与保存时间,保持与保存流程一致 */ +function handlePublished(application: ApplicationDetail) { + applicationDetail.value = application + saveTime.value = application.update_time || saveTime.value +} + /* 退出工作流 */ function handleBack() { if (loading.value || saving.value || confirmingExit.value) return @@ -382,5 +381,7 @@ onBeforeUnmount(() => stopAutoSave()) + + diff --git a/ui/src/views/workflow/application/components/ApplicationPublishDialog.vue b/ui/src/views/workflow/application/components/ApplicationPublishDialog.vue new file mode 100644 index 00000000000..b3e9a45a71b --- /dev/null +++ b/ui/src/views/workflow/application/components/ApplicationPublishDialog.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/ui/src/views/workflow/components/publish-history/EditPublishVersionDialog.vue b/ui/src/views/workflow/components/publish-history/EditPublishVersionDialog.vue index a8a995c488c..b65a9a6f871 100644 --- a/ui/src/views/workflow/components/publish-history/EditPublishVersionDialog.vue +++ b/ui/src/views/workflow/components/publish-history/EditPublishVersionDialog.vue @@ -9,16 +9,17 @@ defineOptions({ name: 'EditPublishVersionDialog' }) const props = defineProps<{ saving?: boolean }>() const emit = defineEmits<{ submit: [payload: WorkflowVersionPayload] }>() const visible = ref(false) -const versionForm = reactive({ name: '', description: '' }) +/** 表单内部使用 description 命名,提交时映射为后端字段 publish_desc。 */ +const versionForm = reactive<{ name: string; description: string }>({ name: '', description: '' }) const formRef = useTemplateRef('formRef') -const rules: FormRules = { +const rules: FormRules<{ name: string; description: string }> = { name: [{ required: true, message: '请输入标题', trigger: 'blur' }], } /* 编辑副本并校验,接口提交和成功关闭由业务组件处理。 */ function open(version: WorkflowVersion) { versionForm.name = version.name || datetimeFormat(version.create_time) - versionForm.description = version.description ?? '' + versionForm.description = version.publish_desc ?? '' visible.value = true } @@ -26,7 +27,7 @@ function handleSubmit() { if (props.saving) return formRef.value?.validate((valid) => { if (!valid || props.saving) return - emit('submit', { name: versionForm.name.trim(), description: versionForm.description }) + emit('submit', { name: versionForm.name.trim(), publish_desc: versionForm.description }) }) } diff --git a/ui/src/views/workflow/components/publish-history/PublishHistoryDrawer.vue b/ui/src/views/workflow/components/publish-history/PublishHistoryDrawer.vue index a36b63b9e40..380bce78ff9 100644 --- a/ui/src/views/workflow/components/publish-history/PublishHistoryDrawer.vue +++ b/ui/src/views/workflow/components/publish-history/PublishHistoryDrawer.vue @@ -36,7 +36,7 @@ function handleSubmit(payload: WorkflowVersionPayload) { } function handleShowDescription(version: WorkflowVersion) { - descriptionDialogRef.value?.open(version.description ?? '') + descriptionDialogRef.value?.open(version.publish_desc ?? '') } function closeEdit() {