Add AI confirmation center
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user