fix: wire ai platform run and codex chat

This commit is contained in:
2026-06-13 11:45:03 +08:00
parent af879e4607
commit 9632c1b4a6
7 changed files with 526 additions and 24 deletions
@@ -44,6 +44,75 @@ from ai_platform.services.agent_import_export import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CODEX_AGENT_PROMPT = """
你是 ai-agent-admin 内置的 Codex 协作助手。你的目标是帮助用户拆解工程任务、解释代码、规划实现、生成可执行步骤,并在需要时提示用户进入智能体、流程编排、模型配置和执行历史等模块完成操作。
回答要求:
- 直接、具体、可执行。
- 优先给出最小可落地方案,再说明关键风险。
- 涉及代码或配置时,明确文件、接口、命令或字段。
- 对不确定事实要说明需要验证,不要编造。
""".strip()
async def _resolve_default_chat_model_id(db: AsyncSession) -> Optional[str]:
result = await db.execute(
select(LLMModel)
.where(
LLMModel.is_deleted == False,
LLMModel.is_active == True,
LLMModel.model_type == "chat",
)
.order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc())
)
model = result.scalars().first()
return str(model.id) if model else None
async def _ensure_codex_agent(db: AsyncSession) -> Agent:
model_id = await _resolve_default_chat_model_id(db)
if not model_id:
raise HTTPException(
status_code=400,
detail="Codex 智能体需要先配置一个启用的 chat 模型",
)
agent = Agent(
name="Codex",
code="codex",
description="内置通用 AI 编程协作助手",
mode="autonomous",
status="published",
is_global=True,
is_public=True,
model_id=model_id,
temperature=0.3,
top_p=1.0,
max_tokens=4096,
max_iterations=6,
enable_memory=True,
memory_window=10,
enable_streaming=True,
persona={
"role": "AI 编程协作助手",
"skills": ["代码理解", "任务拆解", "方案设计", "问题排查"],
"constraints": ["不编造事实", "优先输出可执行步骤"],
},
system_prompt=CODEX_AGENT_PROMPT,
welcome_message="我是 Codex 协作助手,可以帮你拆解任务、解释代码、规划实现和排查问题。",
suggested_questions=[
"帮我分析当前 AI Agent Admin 的下一步实现重点",
"解释一下智能体和流程编排如何协作",
"帮我设计一个 OC-69 多智能体协作流程",
],
sort=100,
)
db.add(agent)
await db.commit()
await db.refresh(agent)
return agent
router = APIRouter(prefix="/agent", tags=["AI-智能体"]) router = APIRouter(prefix="/agent", tags=["AI-智能体"])
@@ -171,6 +240,8 @@ async def get_agent_by_code(code: str, db: AsyncSession = Depends(get_db)):
select(Agent).where(Agent.code == code, Agent.is_deleted == False) select(Agent).where(Agent.code == code, Agent.is_deleted == False)
) )
agent = result.scalar_one_or_none() agent = result.scalar_one_or_none()
if not agent and code == "codex":
agent = await _ensure_codex_agent(db)
if not agent: if not agent:
raise HTTPException(status_code=404, detail="智能体不存在") raise HTTPException(status_code=404, detail="智能体不存在")
@@ -110,6 +110,8 @@ async def update_provider(
raise HTTPException(status_code=404, detail="提供商不存在") raise HTTPException(status_code=404, detail="提供商不存在")
update_data = data.model_dump(exclude_unset=True) update_data = data.model_dump(exclude_unset=True)
if update_data.get("api_key") == "":
update_data.pop("api_key")
for key, value in update_data.items(): for key, value in update_data.items():
setattr(provider, key, value) setattr(provider, key, value)
+11
View File
@@ -62,6 +62,17 @@ function createLightAiMenuRoute(
} }
const LIGHT_AI_PLATFORM_MENU_ROUTES = [ const LIGHT_AI_PLATFORM_MENU_ROUTES = [
createLightAiMenuRoute({
component: '/_core/agent-chat/index',
meta: {
fullPathKey: true,
icon: 'lucide:code',
order: 25,
title: 'menu-title.codex',
},
name: 'CodexAgentChat',
path: '/agent-chat/codex',
}),
createLightAiMenuRoute({ createLightAiMenuRoute({
component: '/ai-platform/model/index', component: '/ai-platform/model/index',
meta: { meta: {
@@ -7,6 +7,7 @@ export const LIGHT_MENU_NAMES = new Set([
'AIWorkflowRuns', 'AIWorkflowRuns',
'AnnouncementList', 'AnnouncementList',
'AnnouncementManage', 'AnnouncementManage',
'CodexAgentChat',
'KnowledgeBase', 'KnowledgeBase',
'PageRenderMainHome', 'PageRenderMainHome',
'SystemManagement', 'SystemManagement',
@@ -23,6 +24,7 @@ const LIGHT_ROUTE_PATH_PREFIXES = [
'/ai-platform/model', '/ai-platform/model',
'/ai-platform/workflow', '/ai-platform/workflow',
'/ai-platform/workflow-runs', '/ai-platform/workflow-runs',
'/agent-chat',
'/message/announcement', '/message/announcement',
'/page-render/main_home', '/page-render/main_home',
'/system/menu', '/system/menu',
@@ -2,6 +2,7 @@
import type { import type {
DefaultModel, DefaultModel,
ModelListItem, ModelListItem,
ProviderCreateInput,
ProviderListItem, ProviderListItem,
ProviderType, ProviderType,
} from '#/api/ai-platform/ai-platform'; } from '#/api/ai-platform/ai-platform';
@@ -57,6 +58,7 @@ import {
deleteProviderApi, deleteProviderApi,
fetchProviderModelsApi, fetchProviderModelsApi,
getModelListApi, getModelListApi,
getProviderDetailApi,
getProviderDefaultModelsApi, getProviderDefaultModelsApi,
getProviderListApi, getProviderListApi,
getProviderTypesApi, getProviderTypesApi,
@@ -95,6 +97,7 @@ const providerForm = ref({
name: '', name: '',
provider_type: '', provider_type: '',
api_key: '', api_key: '',
api_key_masked: '',
api_base: '', api_base: '',
ollama_host: 'http://localhost:11434', ollama_host: 'http://localhost:11434',
description: '', description: '',
@@ -247,6 +250,7 @@ function handleAddProvider() {
name: '', name: '',
provider_type: '', provider_type: '',
api_key: '', api_key: '',
api_key_masked: '',
api_base: '', api_base: '',
ollama_host: 'http://localhost:11434', ollama_host: 'http://localhost:11434',
description: '', description: '',
@@ -255,28 +259,51 @@ function handleAddProvider() {
showProviderDialog.value = true; showProviderDialog.value = true;
} }
function handleEditProvider(row: ProviderListItem) { async function handleEditProvider(row: ProviderListItem) {
providerDialogMode.value = 'edit'; providerDialogMode.value = 'edit';
providerForm.value = { try {
id: row.id, const detail = await getProviderDetailApi(row.id);
name: row.name, providerForm.value = {
provider_type: row.provider_type, id: detail.id,
api_key: '', name: detail.name,
api_base: '', provider_type: detail.provider_type,
ollama_host: 'http://localhost:11434', api_key: '',
description: row.description, api_key_masked: detail.api_key_masked || '',
is_active: row.is_active, api_base: detail.api_base || '',
ollama_host: detail.ollama_host || 'http://localhost:11434',
description: detail.description || '',
is_active: detail.is_active,
};
showProviderDialog.value = true;
} catch (error: any) {
ElMessage.error(error.message || $t('ui.actionMessage.operationFailed'));
}
}
function buildProviderPayload() {
const payload: ProviderCreateInput = {
name: providerForm.value.name,
provider_type: providerForm.value.provider_type,
api_base: providerForm.value.api_base,
ollama_host: providerForm.value.ollama_host,
description: providerForm.value.description,
is_active: providerForm.value.is_active,
}; };
showProviderDialog.value = true; const apiKey = providerForm.value.api_key.trim();
if (apiKey) {
payload.api_key = apiKey;
}
return payload;
} }
async function handleSaveProvider() { async function handleSaveProvider() {
try { try {
const payload = buildProviderPayload();
if (providerDialogMode.value === 'add') { if (providerDialogMode.value === 'add') {
await createProviderApi(providerForm.value); await createProviderApi(payload);
ElMessage.success($t('ui.actionMessage.operationSuccess')); ElMessage.success($t('ui.actionMessage.operationSuccess'));
} else { } else {
await updateProviderApi(providerForm.value.id, providerForm.value); await updateProviderApi(providerForm.value.id, payload);
ElMessage.success($t('ui.actionMessage.operationSuccess')); ElMessage.success($t('ui.actionMessage.operationSuccess'));
} }
showProviderDialog.value = false; showProviderDialog.value = false;
@@ -889,9 +916,19 @@ defineExpose({ reset });
<ElInput <ElInput
v-model="providerForm.api_key" v-model="providerForm.api_key"
type="password" type="password"
:placeholder="$t('ui.placeholder.input')" :placeholder="
providerDialogMode === 'edit' && providerForm.api_key_masked
? '已配置,留空不修改'
: $t('ui.placeholder.input')
"
show-password show-password
/> />
<div
v-if="providerDialogMode === 'edit' && providerForm.api_key_masked"
class="text-muted-foreground mt-1 text-xs"
>
当前 Key{{ providerForm.api_key_masked }}留空不会修改
</div>
</ElFormItem> </ElFormItem>
<ElFormItem <ElFormItem
v-if="providerForm.provider_type !== 'ollama'" v-if="providerForm.provider_type !== 'ollama'"
@@ -9,7 +9,14 @@ import { useRouter } from 'vue-router';
import { ExternalLink } from '@vben/icons'; import { ExternalLink } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { ElAlert, ElButton, ElTag } from 'element-plus'; import {
ElAlert,
ElButton,
ElCollapse,
ElCollapseItem,
ElScrollbar,
ElTag,
} from 'element-plus';
import { getWorkflowDetailApi, getWorkflowRunDetailApi } from '#/api/ai-platform/ai-platform'; import { getWorkflowDetailApi, getWorkflowRunDetailApi } from '#/api/ai-platform/ai-platform';
import { ZqDesc, ZqDescItem } from '#/components/zq-desc'; import { ZqDesc, ZqDescItem } from '#/components/zq-desc';
@@ -43,8 +50,10 @@ const nodes = ref<Node[]>([]);
const edges = ref<Edge[]>([]); const edges = ref<Edge[]>([]);
const activeNodeId = ref(''); const activeNodeId = ref('');
const definitionFallback = ref(false); const definitionFallback = ref(false);
const activeLogNames = ref<string[]>([]);
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId')); const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
const executionLogs = computed(() => run.value?.execution_log || []);
async function loadDetail() { async function loadDetail() {
if (!runId.value) return; if (!runId.value) return;
@@ -78,6 +87,7 @@ async function loadDetail() {
const replayed = applyRunReplay(base.nodes, base.edges, detail); const replayed = applyRunReplay(base.nodes, base.edges, detail);
nodes.value = replayed.nodes; nodes.value = replayed.nodes;
edges.value = replayed.edges; edges.value = replayed.edges;
activeLogNames.value = executionLogs.value.slice(0, 1).map((_, index) => String(index));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
@@ -109,6 +119,40 @@ function triggerLabel(value?: string) {
return getTagLabel(value, triggerOptions); return getTagLabel(value, triggerOptions);
} }
function getLogStatusType(status?: string) {
if (status === 'completed' || status === 'success') return 'success';
if (status === 'failed') return 'danger';
if (status === 'waiting') return 'warning';
if (status === 'running') return 'primary';
return 'info';
}
function getLogStatusText(status?: string) {
const map: Record<string, string> = {
completed: $t('ai-platform.workflowRuns.status.completed'),
failed: $t('ai-platform.workflowRuns.status.failed'),
running: $t('ai-platform.workflowRuns.status.running'),
success: $t('ai-platform.workflowRuns.status.completed'),
waiting: $t('ai-platform.workflowRuns.status.waiting'),
};
return status ? map[status] || status : '-';
}
function previewValue(value: any) {
if (value === undefined || value === null || value === '') return '-';
if (typeof value === 'string') return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function selectLog(nodeId?: string) {
if (!nodeId) return;
activeNodeId.value = nodeId;
}
defineExpose({ open }); defineExpose({ open });
</script> </script>
@@ -192,12 +236,79 @@ defineExpose({ open });
</ZqDescItem> </ZqDescItem>
</ZqDesc> </ZqDesc>
<div class="run-detail-canvas border-border overflow-hidden rounded-lg border"> <div class="run-detail-main grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_420px] gap-4">
<WorkflowReadonlyCanvas <div class="run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border">
:nodes="nodes" <WorkflowReadonlyCanvas
:edges="edges" :nodes="nodes"
:fit-node-id="activeNodeId" :edges="edges"
/> :fit-node-id="activeNodeId"
/>
</div>
<div class="border-border flex min-h-0 flex-col rounded-lg border">
<div class="border-border flex items-center justify-between border-b px-3 py-2">
<div class="text-sm font-medium">
{{ $t('ai-platform.workflowRuns.detail.timeline') }}
</div>
<ElTag size="small">{{ executionLogs.length }}</ElTag>
</div>
<ElScrollbar class="flex-1">
<div class="p-3">
<ElAlert
v-if="executionLogs.length === 0"
type="info"
:closable="false"
:title="$t('ai-platform.workflowRuns.detail.noLogs')"
/>
<ElCollapse v-else v-model="activeLogNames">
<ElCollapseItem
v-for="(log, index) in executionLogs"
:key="`${log.node_id}-${index}`"
:name="String(index)"
>
<template #title>
<div
class="flex min-w-0 flex-1 items-center gap-2"
@click="selectLog(log.node_id)"
>
<ElTag :type="getLogStatusType(log.status)" size="small">
{{ getLogStatusText(log.status) }}
</ElTag>
<span class="truncate text-sm">
{{ log.node_label || log.node_id }}
</span>
<span class="text-muted-foreground text-xs">
{{ formatDuration(log.elapsed_time) }}
</span>
</div>
</template>
<div class="space-y-3 text-xs">
<div v-if="log.error" class="text-destructive break-words">
{{ log.error }}
</div>
<div>
<div class="text-muted-foreground mb-1 font-medium">
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入
</div>
<pre class="run-json">{{ previewValue(log.inputs) }}</pre>
</div>
<div>
<div class="text-muted-foreground mb-1 font-medium">
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出
</div>
<pre class="run-json">{{ previewValue(log.output) }}</pre>
</div>
<div class="text-muted-foreground flex gap-3">
<span>Token: {{ log.tokens_used || 0 }}</span>
<span>{{ log.node_type }}</span>
</div>
</div>
</ElCollapseItem>
</ElCollapse>
</div>
</ElScrollbar>
</div>
</div> </div>
</div> </div>
</ZqDialog> </ZqDialog>
@@ -236,4 +347,20 @@ defineExpose({ open });
min-height: 320px; min-height: 320px;
height: 0; height: 0;
} }
.workflow-run-detail-dialog .run-json {
background: hsl(var(--muted));
border-radius: 6px;
max-height: 180px;
overflow: auto;
padding: 8px;
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 1024px) {
.workflow-run-detail-dialog .run-detail-main {
grid-template-columns: 1fr;
}
}
</style> </style>
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { import type {
WorkflowListItem, WorkflowListItem,
WorkflowStreamEvent,
WorkflowType, WorkflowType,
} from '#/api/ai-platform/ai-platform'; } from '#/api/ai-platform/ai-platform';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, defineAsyncComponent, onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui/es/page'; import { Page } from '@vben/common-ui/es/page';
@@ -24,6 +25,7 @@ import { $t } from '@vben/locales';
import { import {
ElButton, ElButton,
ElCard, ElCard,
ElDialog,
ElDropdown, ElDropdown,
ElDropdownItem, ElDropdownItem,
ElDropdownMenu, ElDropdownMenu,
@@ -35,6 +37,7 @@ import {
ElMessageBox, ElMessageBox,
ElOption, ElOption,
ElPagination, ElPagination,
ElScrollbar,
ElSelect, ElSelect,
ElSwitch, ElSwitch,
ElTag, ElTag,
@@ -49,6 +52,7 @@ import {
getWorkflowDetailApi, getWorkflowDetailApi,
getWorkflowListApi, getWorkflowListApi,
publishWorkflowApi, publishWorkflowApi,
runWorkflowStreamApi,
updateWorkflowApi, updateWorkflowApi,
} from '#/api/ai-platform/ai-platform'; } from '#/api/ai-platform/ai-platform';
import { ZqDialog } from '#/components/zq-dialog'; import { ZqDialog } from '#/components/zq-dialog';
@@ -57,6 +61,10 @@ import { useAppContextStore } from '#/store/app-context';
import RunDialog from '../workflow/editor/components/RunDialog.vue'; import RunDialog from '../workflow/editor/components/RunDialog.vue';
import ImportDialog from './modules/import-dialog.vue'; import ImportDialog from './modules/import-dialog.vue';
const DetailDialog = defineAsyncComponent(
() => import('../workflow-runs/modules/detail-dialog.vue'),
);
const router = useRouter(); const router = useRouter();
const appContextStore = useAppContextStore(); const appContextStore = useAppContextStore();
const isMainApp = computed(() => appContextStore.isMainApp); const isMainApp = computed(() => appContextStore.isMainApp);
@@ -96,6 +104,22 @@ const runDialogVisible = ref(false);
const runWorkflowTarget = ref<any>(null); const runWorkflowTarget = ref<any>(null);
const runWorkflowNodes = ref<any[]>([]); const runWorkflowNodes = ref<any[]>([]);
const showImportDialog = ref(false); const showImportDialog = ref(false);
const runDetailRef = ref<any>();
const runStatusDialogVisible = ref(false);
const currentRunId = ref('');
const runStatus = ref('pending');
const runError = ref('');
const runElapsedTime = ref(0);
const runEvents = ref<
Array<{
detail?: string;
id: string;
status: 'failed' | 'info' | 'running' | 'success' | 'warning';
time: string;
title: string;
}>
>([]);
let cancelWorkflowRunStream: (() => void) | null = null;
async function handleConfirm() { async function handleConfirm() {
if (!formRef.value) return; if (!formRef.value) return;
@@ -232,6 +256,157 @@ const handleQuickRun = async (row: WorkflowListItem) => {
} }
}; };
const runStatusType = computed(() => {
const map: Record<string, string> = {
completed: 'success',
failed: 'danger',
pending: 'info',
running: 'primary',
stopped: 'info',
waiting: 'warning',
};
return map[runStatus.value] || 'info';
});
function getRunStatusText(status: string) {
const map: Record<string, string> = {
completed: '已完成',
failed: '失败',
pending: '等待中',
running: '运行中',
stopped: '已停止',
waiting: '等待输入',
};
return map[status] || status;
}
function formatRunDuration(ms?: number) {
if (!ms && ms !== 0) return '-';
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
function pushRunEvent(
title: string,
status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info',
detail?: string,
) {
runEvents.value.push({
detail,
id: `${Date.now()}-${runEvents.value.length}`,
status,
time: new Date().toLocaleTimeString(),
title,
});
}
function resetRunState() {
currentRunId.value = '';
runStatus.value = 'running';
runError.value = '';
runElapsedTime.value = 0;
runEvents.value = [];
}
function handleWorkflowRunEvent(event: WorkflowStreamEvent) {
switch (event.type) {
case 'start': {
currentRunId.value = event.run_id || '';
runStatus.value = 'running';
pushRunEvent(
'已创建运行记录',
'success',
currentRunId.value ? `Run ID: ${currentRunId.value}` : undefined,
);
break;
}
case 'node_start': {
pushRunEvent(
`开始执行:${event.node_label || event.node_id || '节点'}`,
'running',
event.node_type,
);
break;
}
case 'node_complete': {
const ok = event.status === 'success' || event.status === 'completed';
pushRunEvent(
`${ok ? '执行完成' : '执行失败'}${event.node_label || event.node_id || '节点'}`,
ok ? 'success' : 'failed',
event.error_message ||
`耗时 ${formatRunDuration(event.elapsed_time)}Token ${event.tokens_used || 0}`,
);
break;
}
case 'waiting_input': {
runStatus.value = 'waiting';
pushRunEvent(
`等待输入:${event.node_label || event.node_id || '节点'}`,
'warning',
);
break;
}
case 'complete': {
runStatus.value = 'completed';
runElapsedTime.value = event.elapsed_time || 0;
pushRunEvent(
'流程运行完成',
'success',
`耗时 ${formatRunDuration(event.elapsed_time)}Token ${event.total_tokens || 0}`,
);
ElMessage.success('流程运行完成');
cancelWorkflowRunStream = null;
fetchList();
break;
}
case 'error': {
runStatus.value = 'failed';
runError.value =
event.message || event.error_message || event.error || '流程运行失败';
pushRunEvent('流程运行失败', 'failed', runError.value);
ElMessage.error(runError.value);
cancelWorkflowRunStream = null;
fetchList();
break;
}
}
}
function handleRunWorkflow(inputs: Record<string, any>) {
if (!runWorkflowTarget.value?.id) return;
cancelWorkflowRunStream?.();
resetRunState();
runStatusDialogVisible.value = true;
pushRunEvent('已提交运行请求', 'info', runWorkflowTarget.value.name);
cancelWorkflowRunStream = runWorkflowStreamApi(
runWorkflowTarget.value.id,
inputs,
handleWorkflowRunEvent,
(error) => {
runStatus.value = 'failed';
runError.value = error.message || '流程运行失败';
pushRunEvent('流程运行失败', 'failed', runError.value);
ElMessage.error(runError.value);
cancelWorkflowRunStream = null;
fetchList();
},
() => {
cancelWorkflowRunStream = null;
},
);
}
function openRunHistory() {
router.push(appContextStore.getContextPath('/ai-platform/workflow-runs'));
}
function openCurrentRunDetail() {
if (!currentRunId.value) return;
runDetailRef.value?.open(currentRunId.value);
}
const handleEdit = (row: WorkflowListItem) => { const handleEdit = (row: WorkflowListItem) => {
router.push( router.push(
appContextStore.getContextPath(`/ai-platform/workflow/editor/${row.id}`), appContextStore.getContextPath(`/ai-platform/workflow/editor/${row.id}`),
@@ -371,10 +546,16 @@ const getStatusText = (status: string) => {
onMounted(() => { onMounted(() => {
fetchList(); fetchList();
}); });
onUnmounted(() => {
cancelWorkflowRunStream?.();
});
</script> </script>
<template> <template>
<div class="ai-workflow-list-page"> <div class="ai-workflow-list-page">
<DetailDialog ref="runDetailRef" />
<Page auto-content-height v-loading="loading"> <Page auto-content-height v-loading="loading">
<template #title> <template #title>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -640,12 +821,83 @@ onMounted(() => {
<!-- 快速运行弹窗(数据处理/自动化工作流) --> <!-- 快速运行弹窗(数据处理/自动化工作流) -->
<RunDialog <RunDialog
v-model:visible="runDialogVisible" v-model:visible="runDialogVisible"
:workflow-id="runWorkflowTarget?.id || ''"
:nodes="runWorkflowNodes" :nodes="runWorkflowNodes"
:workflow-name="runWorkflowTarget?.name" :workflow-name="runWorkflowTarget?.name"
:auto-run="true" @submit="handleRunWorkflow"
/> />
<ElDialog
v-model="runStatusDialogVisible"
title="流程运行"
width="680px"
:close-on-click-modal="false"
>
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<ElTag :type="runStatusType as any">
{{ getRunStatusText(runStatus) }}
</ElTag>
<span class="text-sm font-medium">
{{ runWorkflowTarget?.name || '-' }}
</span>
<span class="text-muted-foreground text-xs">
{{ currentRunId || '正在创建运行记录...' }}
</span>
</div>
<div
v-if="runError"
class="border-destructive/30 bg-destructive/10 text-destructive rounded-md border px-3 py-2 text-sm"
>
{{ runError }}
</div>
<ElScrollbar height="320px">
<div class="space-y-2 pr-2">
<div
v-for="event in runEvents"
:key="event.id"
class="border-border flex gap-3 rounded-md border px-3 py-2"
>
<ElTag :type="event.status === 'failed' ? 'danger' : event.status as any" size="small">
{{ event.time }}
</ElTag>
<div class="min-w-0 flex-1">
<div class="text-sm font-medium">{{ event.title }}</div>
<div
v-if="event.detail"
class="text-muted-foreground mt-1 break-words text-xs"
>
{{ event.detail }}
</div>
</div>
</div>
</div>
</ElScrollbar>
</div>
<template #footer>
<div class="flex justify-between">
<ElButton @click="openRunHistory">
打开执行历史
</ElButton>
<div class="flex gap-2">
<ElButton
:disabled="!currentRunId"
type="primary"
plain
@click="openCurrentRunDetail"
>
查看本次详情
</ElButton>
<ElButton @click="runStatusDialogVisible = false">
关闭
</ElButton>
</div>
</div>
</template>
</ElDialog>
<ImportDialog v-model="showImportDialog" @imported="handleImported" /> <ImportDialog v-model="showImportDialog" @imported="handleImported" />
</div> </div>
</template> </template>