Add AI confirmation center

This commit is contained in:
2026-06-10 08:13:30 +08:00
parent 3ed272a875
commit 29820dc106
4 changed files with 284 additions and 0 deletions
+45
View File
@@ -6758,6 +6758,51 @@
"sys_dept_id": null
}
},
{
"model": "core.menu.model.Menu",
"pk": "ai-agent-admin-ai-confirmation-center",
"fields": {
"application_id": null,
"is_system": true,
"parent_id": null,
"name": "AIConfirmationCenter",
"title": "AI ????",
"authCode": null,
"path": "/ai-confirmation-center",
"type": "menu",
"component": "/_core/ai-confirmation-center/index",
"redirect": null,
"activePath": null,
"query": null,
"noBasicLayout": false,
"icon": "lucide:clipboard-check",
"activeIcon": null,
"order": 32,
"hideInMenu": false,
"hideChildrenInMenu": false,
"hideInBreadcrumb": false,
"hideInTab": false,
"affixTab": false,
"affixTabOrder": null,
"keepAlive": false,
"maxNumOfOpenTab": null,
"fullPathKey": true,
"link": null,
"iframeSrc": null,
"openInNewWindow": false,
"badge": null,
"badgeType": null,
"badgeVariants": null,
"id": "ai-agent-admin-ai-confirmation-center",
"sort": 32,
"is_deleted": false,
"sys_create_datetime": "2026-06-10T00:00:00.000000",
"sys_update_datetime": "2026-06-10T00:00:00.000000",
"sys_creator_id": null,
"sys_modifier_id": null,
"sys_dept_id": null
}
},
{
"model": "core.menu.model.Menu",
"pk": "ai-agent-admin-application-management",
@@ -983,6 +983,7 @@ export async function getWorkflowRunsApi(
/** 获取全局工作流运行记录 */
export async function getAllWorkflowRunsApi(params?: {
applicationId?: string;
page?: number;
pageSize?: number;
workflowId?: string;
+1
View File
@@ -16,6 +16,7 @@ const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob([
'../views/_core/agent-chat/**/*.vue',
'../views/_core/ai-confirmation-center/index.vue',
'../views/_core/announcement/index.vue',
'../views/_core/announcement/list.vue',
'../views/_core/application/index.vue',
@@ -0,0 +1,237 @@
<script setup lang="ts">
import type { WorkflowRun, WorkflowRunListItem } from '#/api/ai-platform/ai-platform';
import { computed, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Check, Eye, RefreshCw, Send, X } from '@vben/icons';
import {
ElButton,
ElDialog,
ElEmpty,
ElInput,
ElMessage,
ElRadio,
ElRadioGroup,
ElTable,
ElTableColumn,
ElTag,
} from 'element-plus';
import {
getAllWorkflowRunsApi,
getWorkflowRunDetailApi,
resumeWorkflowStreamApi,
} from '#/api/ai-platform/ai-platform';
import { useAppContextStore } from '#/store/app-context';
defineOptions({ name: 'AIConfirmationCenter' });
const appContextStore = useAppContextStore();
const loading = ref(false);
const submitting = ref(false);
const runs = ref<WorkflowRunListItem[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const detailVisible = ref(false);
const currentRun = ref<WorkflowRun | null>(null);
const answer = ref('');
const selectedValue = ref('');
let cancelResumeStream: (() => void) | null = null;
const waitingConfig = computed(() => currentRun.value?.waiting_config || {});
const interactionType = computed(() => waitingConfig.value.type || 'question');
const options = computed<Array<{ label: string; value: string }>>(
() => waitingConfig.value.options || [],
);
function formatTime(value?: string) {
return value ? new Date(value).toLocaleString() : '-';
}
function promptText() {
return (
waitingConfig.value.question ||
waitingConfig.value.content ||
waitingConfig.value.title ||
'等待人工确认'
);
}
async function loadRuns() {
loading.value = true;
try {
const res = await getAllWorkflowRunsApi({
page: page.value,
pageSize: pageSize.value,
status: 'waiting',
applicationId: appContextStore.currentApp?.id,
});
runs.value = res.items || [];
total.value = res.total || 0;
} catch (error: any) {
ElMessage.error(error?.message || '加载确认任务失败');
} finally {
loading.value = false;
}
}
async function openDetail(row: WorkflowRunListItem) {
try {
currentRun.value = await getWorkflowRunDetailApi(row.id);
answer.value = waitingConfig.value.default_value || '';
selectedValue.value = options.value[0]?.value || '';
detailVisible.value = true;
} catch (error: any) {
ElMessage.error(error?.message || '加载详情失败');
}
}
function buildSubmitValue(value?: any) {
if (interactionType.value === 'confirm') {
return value;
}
if (interactionType.value === 'choice') {
return selectedValue.value;
}
return value ?? answer.value;
}
function submitInteraction(value?: any) {
if (!currentRun.value || submitting.value) return;
submitting.value = true;
cancelResumeStream?.();
cancelResumeStream = resumeWorkflowStreamApi(
currentRun.value.id,
buildSubmitValue(value),
(event) => {
if (event.type === 'error') {
ElMessage.error(event.error || '恢复运行失败');
}
if (event.type === 'complete') {
ElMessage.success('已提交确认');
}
},
(error) => {
ElMessage.error(error.message || '恢复运行失败');
submitting.value = false;
},
async () => {
submitting.value = false;
detailVisible.value = false;
await loadRuns();
},
);
}
onMounted(loadRuns);
</script>
<template>
<Page auto-content-height>
<template #title>
<div class="flex items-center justify-between">
<div class="text-base font-medium">AI 确认中心</div>
<ElButton :icon="RefreshCw" :loading="loading" @click="loadRuns">
刷新
</ElButton>
</div>
</template>
<ElTable v-loading="loading" :data="runs" height="100%" border>
<ElTableColumn prop="workflow_name" label="流程" min-width="180" />
<ElTableColumn prop="trigger_type" label="来源" width="130">
<template #default="{ row }">
<ElTag size="small">{{ row.trigger_type }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="total_steps" label="步骤" width="80" />
<ElTableColumn label="开始时间" min-width="180">
<template #default="{ row }">{{ formatTime(row.started_at) }}</template>
</ElTableColumn>
<ElTableColumn label="状态" width="100">
<template #default>
<ElTag type="warning" size="small">等待确认</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="120" fixed="right">
<template #default="{ row }">
<ElButton link type="primary" :icon="Eye" @click="openDetail(row)">
处理
</ElButton>
</template>
</ElTableColumn>
<template #empty>
<ElEmpty description="暂无等待确认的 AI 流程" />
</template>
</ElTable>
<div class="mt-3 text-right text-sm text-[var(--el-text-color-secondary)]">
{{ total }}
</div>
<ElDialog
v-model="detailVisible"
title="处理 AI 确认"
width="680px"
:close-on-click-modal="false"
>
<div v-if="currentRun" class="space-y-4">
<div class="rounded border border-[var(--el-border-color)] p-3">
<div class="font-medium">{{ currentRun.workflow_name }}</div>
<div class="mt-1 text-sm text-[var(--el-text-color-secondary)]">
当前节点{{ currentRun.current_node_id || '-' }}
</div>
</div>
<div class="rounded bg-[var(--el-fill-color-light)] p-3">
{{ promptText() }}
</div>
<ElRadioGroup
v-if="interactionType === 'choice'"
v-model="selectedValue"
class="flex flex-col items-start gap-2"
>
<ElRadio
v-for="option in options"
:key="option.value"
:label="option.value"
>
{{ option.label }}
</ElRadio>
</ElRadioGroup>
<ElInput
v-else-if="interactionType !== 'confirm'"
v-model="answer"
type="textarea"
:rows="4"
:placeholder="waitingConfig.placeholder || '请输入处理意见或回复内容'"
/>
</div>
<template #footer>
<ElButton @click="detailVisible = false">关闭</ElButton>
<ElButton
v-if="interactionType === 'confirm'"
:icon="X"
:loading="submitting"
@click="submitInteraction(false)"
>
{{ waitingConfig.cancel_text || '拒绝' }}
</ElButton>
<ElButton
type="primary"
:icon="interactionType === 'confirm' ? Check : Send"
:loading="submitting"
@click="submitInteraction(true)"
>
{{ interactionType === 'confirm' ? (waitingConfig.confirm_text || '确认') : '提交' }}
</ElButton>
</template>
</ElDialog>
</Page>
</template>