feat: 收敛AI资源发布审批生命周期

- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路

- 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览

- 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
2026-04-09 17:13:54 +08:00
parent 81125ce55c
commit 4e565aef99
68 changed files with 3859 additions and 817 deletions

View File

@@ -26,8 +26,9 @@ import { $t } from '#/locales';
import { router } from '#/router';
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
import {
canAiResourceRepublish,
isAiResourceApprovalPending,
normalizeAiPublishStatus,
resolveAiResourceDisplayStatus,
} from '#/views/ai/shared/publish-status';
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
@@ -251,18 +252,26 @@ const updatePluginNode = ref<any>(null);
const pageLoading = ref(false);
const chainInfo = ref<any>(null);
const publishActionText = computed(() => {
switch (normalizeAiPublishStatus(workflowInfo.value?.publishStatus)) {
switch (
resolveAiResourceDisplayStatus(
workflowInfo.value?.displayPublishStatus,
workflowInfo.value?.publishStatus,
)
) {
case 'DELETE_PENDING': {
return $t('aiWorkflow.publishStatusDeletePending');
}
case 'OFFLINE_PENDING': {
return $t('aiWorkflow.publishStatusOfflinePending');
}
case 'PUBLISH_PENDING': {
return $t('aiWorkflow.publishStatusPublishPending');
}
case 'PUBLISHED': {
return `${$t('aiWorkflow.publishStatusPublished')} · ${$t('button.republish')}`;
return $t('button.republish');
}
default: {
return `${$t('aiWorkflow.publishStatusDraft')} · ${$t('button.submitPublishApproval')}`;
return $t('button.publish');
}
}
});
@@ -272,7 +281,10 @@ const publishActionDisabled = computed(
saveLoading.value ||
checkLoading.value ||
publishLoading.value ||
isAiResourceApprovalPending(workflowInfo.value?.publishStatus),
isAiResourceApprovalPending(
workflowInfo.value?.displayPublishStatus,
workflowInfo.value?.publishStatus,
),
);
function syncNavTitle(title: string) {
@@ -498,17 +510,27 @@ function closeCheckIssues() {
async function handleCheck() {
await runCheck('PRE_EXECUTE');
}
async function handlePublish() {
async function handlePublishAction() {
if (publishLoading.value) {
return;
}
if (isAiResourceApprovalPending(workflowInfo.value?.publishStatus)) {
if (
isAiResourceApprovalPending(
workflowInfo.value?.displayPublishStatus,
workflowInfo.value?.publishStatus,
)
) {
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('aiWorkflow.submitPublishApprovalConfirm'),
canAiResourceRepublish(
workflowInfo.value?.displayPublishStatus,
workflowInfo.value?.publishStatus,
)
? $t('aiWorkflow.submitRepublishApprovalConfirm')
: $t('aiWorkflow.submitPublishApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
@@ -680,8 +702,8 @@ function onAsyncExecute(info: any) {
:loading="publishLoading"
:disabled="publishActionDisabled"
class="workflow-publish-button"
:class="`workflow-publish-button--${normalizeAiPublishStatus(workflowInfo?.publishStatus)}`"
@click="handlePublish"
:class="`workflow-publish-button--${resolveAiResourceDisplayStatus(workflowInfo?.displayPublishStatus, workflowInfo?.publishStatus)}`"
@click="handlePublishAction"
>
{{ publishActionText }}
</ElButton>
@@ -809,12 +831,24 @@ function onAsyncExecute(info: any) {
border-color: hsl(var(--warning) / 24%);
}
:deep(.workflow-publish-button--OFFLINE_PENDING.el-button) {
color: hsl(var(--warning));
background: hsl(var(--warning) / 18%);
border-color: hsl(var(--warning) / 24%);
}
:deep(.workflow-publish-button--PUBLISHED.el-button) {
color: hsl(var(--success));
background: hsl(var(--success) / 18%);
border-color: hsl(var(--success) / 24%);
}
:deep(.workflow-publish-button--OFFLINE.el-button) {
color: hsl(var(--foreground) / 78%);
background: hsl(var(--muted) / 62%);
border-color: hsl(var(--foreground) / 14%);
}
:deep(.workflow-publish-button--DELETE_PENDING.el-button) {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 16%);

View File

@@ -50,9 +50,16 @@ import { router } from '#/router';
import { useDictStore } from '#/store';
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
import {
buildOfflineImpactMessage,
type OfflineImpactCheck,
} from '#/views/ai/shared/offline-impact';
import {
canAiResourceDelete,
canAiResourceOffline,
canAiResourcePublish,
canAiResourceRepublish,
isAiResourceApprovalPending,
isAiResourcePublished,
normalizeAiPublishStatus,
resolveAiResourceDisplayStatus,
} from '#/views/ai/shared/publish-status';
import WorkflowModal from './WorkflowModal.vue';
@@ -175,21 +182,35 @@ const actions: ActionButton[] = [
{
icon: Promotion,
text: (row: any) =>
isAiResourcePublished(row.publishStatus)
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
? $t('button.republish')
: $t('button.submitPublishApproval'),
: $t('button.publish'),
permission: '/api/v1/workflow/save',
placement: 'inline',
visible: (row: any) =>
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitPublishApproval(row);
submitPublishAction(row);
},
},
{
icon: Promotion,
text: $t('button.offline'),
permission: '/api/v1/workflow/save',
placement: 'menu',
visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitOfflineAction(row);
},
},
{
icon: Delete,
text: $t('button.submitDeleteApproval'),
text: $t('button.delete'),
tone: 'danger',
permission: '/api/v1/workflow/remove',
placement: 'menu',
visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitDeleteApproval(row);
},
@@ -282,14 +303,22 @@ function showDialog(row: any, importMode = false) {
function resolveNavTitle(row: any) {
return row?.title || row?.name || '';
}
async function submitPublishApproval(row: any) {
if (isAiResourceApprovalPending(row.publishStatus)) {
function isRepublishAction(row: any) {
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
}
async function submitPublishAction(row: any) {
if (
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
) {
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('aiWorkflow.submitPublishApprovalConfirm'),
isRepublishAction(row)
? $t('aiWorkflow.submitRepublishApprovalConfirm')
: $t('aiWorkflow.submitPublishApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
@@ -300,7 +329,56 @@ async function submitPublishApproval(row: any) {
} catch {
return;
}
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
const res = await api.post(
'/api/v1/workflow/submitPublishApproval',
{
id: row.id,
},
);
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
pageDataRef.value?.reload?.();
}
}
async function submitOfflineAction(row: any) {
if (
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
) {
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
return;
}
const impactRes = await api.get<{
data: OfflineImpactCheck;
errorCode: number;
}>(
'/api/v1/workflow/offlineImpactCheck',
{
params: { id: row.id },
},
);
if (impactRes.errorCode !== 0) {
return;
}
try {
await ElMessageBox.confirm(
impactRes.data?.hasBotBindings
? buildOfflineImpactMessage(
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
impactRes.data.botBindings,
$t('aiWorkflow.offlineImpactBoundBotsFooter'),
)
: $t('aiWorkflow.submitOfflineApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
const res = await api.post('/api/v1/workflow/submitOfflineApproval', {
id: row.id,
});
if (res.errorCode === 0) {
@@ -309,7 +387,9 @@ async function submitPublishApproval(row: any) {
}
}
async function submitDeleteApproval(row: any) {
if (isAiResourceApprovalPending(row.publishStatus)) {
if (
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
) {
ElMessage.warning($t('aiWorkflow.deletePendingHint'));
return;
}
@@ -334,14 +414,29 @@ async function submitDeleteApproval(row: any) {
pageDataRef.value?.reload?.();
}
}
function resolvePublishStatusMeta(status?: string) {
switch (normalizeAiPublishStatus(status)) {
function resolvePublishStatusMetaByInstance(
displayPublishStatus?: string,
publishStatus?: string,
) {
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
case 'DELETE_PENDING': {
return {
label: $t('aiWorkflow.publishStatusDeletePending'),
tone: 'danger',
};
}
case 'OFFLINE_PENDING': {
return {
label: $t('aiWorkflow.publishStatusOfflinePending'),
tone: 'pending',
};
}
case 'OFFLINE': {
return {
label: $t('aiWorkflow.publishStatusOffline'),
tone: 'draft',
};
}
case 'PUBLISH_PENDING': {
return {
label: $t('aiWorkflow.publishStatusPublishPending'),
@@ -572,11 +667,11 @@ function handleHeaderButtonClick(data: any) {
<template #publish>
<div
class="workflow-publish-chip"
:class="`workflow-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
:class="`workflow-publish-chip--${resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).tone}`"
>
<span class="workflow-publish-chip__dot"></span>
<span>{{
resolvePublishStatusMeta(item.publishStatus).label
resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
}}</span>
</div>
</template>