Add AI confirmation center
This commit is contained in:
@@ -6758,6 +6758,51 @@
|
|||||||
"sys_dept_id": null
|
"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",
|
"model": "core.menu.model.Menu",
|
||||||
"pk": "ai-agent-admin-application-management",
|
"pk": "ai-agent-admin-application-management",
|
||||||
|
|||||||
@@ -983,6 +983,7 @@ export async function getWorkflowRunsApi(
|
|||||||
|
|
||||||
/** 获取全局工作流运行记录 */
|
/** 获取全局工作流运行记录 */
|
||||||
export async function getAllWorkflowRunsApi(params?: {
|
export async function getAllWorkflowRunsApi(params?: {
|
||||||
|
applicationId?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
workflowId?: string;
|
workflowId?: string;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
|||||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
const pageMap: ComponentRecordType = import.meta.glob([
|
const pageMap: ComponentRecordType = import.meta.glob([
|
||||||
'../views/_core/agent-chat/**/*.vue',
|
'../views/_core/agent-chat/**/*.vue',
|
||||||
|
'../views/_core/ai-confirmation-center/index.vue',
|
||||||
'../views/_core/announcement/index.vue',
|
'../views/_core/announcement/index.vue',
|
||||||
'../views/_core/announcement/list.vue',
|
'../views/_core/announcement/list.vue',
|
||||||
'../views/_core/application/index.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