feat: improve workflow run visibility

This commit is contained in:
2026-06-13 14:02:36 +08:00
parent 1ae8b1007a
commit b3ce64881d
2 changed files with 165 additions and 2 deletions
@@ -54,6 +54,64 @@ 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 || []); const executionLogs = computed(() => run.value?.execution_log || []);
const taskSummary = computed(() => {
const inputs = run.value?.inputs || {};
const issueId = inputs.issue_id || inputs.issueId;
const request =
inputs.request ||
inputs.user_input ||
inputs.query ||
inputs.task ||
inputs.prompt ||
inputs.message;
if (issueId && request) return `${issueId}: ${request}`;
return previewValue(request || inputs);
});
const finalOutput = computed(() => {
const outputs = run.value?.outputs || {};
const direct =
outputs.output ||
outputs.final_result ||
outputs.result ||
outputs.answer ||
outputs.summary;
if (direct) return previewValue(direct);
const nodeValues = Object.entries(outputs)
.filter(([key]) => key.startsWith('_node_'))
.map(([, value]) => (value as any)?.output || value)
.filter(Boolean);
return previewValue(nodeValues.at(-1) || outputs);
});
const agentSummaries = computed(() => {
const agents = new Map<
string,
{ code: string; model: string; name: string; steps: number; tokens: number }
>();
for (const log of executionLogs.value) {
const meta = getLogMeta(log);
const code = meta.agent_code;
if (!code) continue;
const current = agents.get(code) || {
code,
model: meta.model || meta.model_id || '-',
name: meta.agent_name || code,
steps: 0,
tokens: 0,
};
current.steps += 1;
current.tokens +=
log.tokens_used ||
meta.total_tokens ||
meta.prompt_tokens + meta.completion_tokens ||
0;
agents.set(code, current);
}
return [...agents.values()];
});
async function loadDetail() { async function loadDetail() {
if (!runId.value) return; if (!runId.value) return;
@@ -240,6 +298,46 @@ defineExpose({ open });
</ZqDescItem> </ZqDescItem>
</ZqDesc> </ZqDesc>
<div class="run-report-grid grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_360px]">
<div class="border-border bg-muted/30 rounded-lg border p-3">
<div class="mb-2 text-sm font-medium">任务输入</div>
<pre class="run-json run-report-json">{{ taskSummary }}</pre>
</div>
<div class="border-border bg-muted/30 rounded-lg border p-3">
<div class="mb-2 text-sm font-medium">最终输出</div>
<pre
:class="run.status === 'failed' ? 'text-destructive' : ''"
class="run-json run-report-json"
>{{ run.error_message || finalOutput }}</pre>
</div>
<div class="border-border bg-muted/30 rounded-lg border p-3">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">协作智能体</span>
<ElTag size="small">{{ agentSummaries.length }}</ElTag>
</div>
<div v-if="agentSummaries.length" class="space-y-2">
<div
v-for="agent in agentSummaries"
:key="agent.code"
class="border-border rounded-md border bg-background px-3 py-2"
>
<div class="flex items-center justify-between gap-2">
<span class="truncate text-sm font-medium">{{ agent.name }}</span>
<span class="text-muted-foreground shrink-0 text-xs">
{{ agent.steps }} / {{ agent.tokens }} Token
</span>
</div>
<div class="text-muted-foreground mt-1 break-words font-mono text-xs">
{{ agent.code }} · {{ agent.model }}
</div>
</div>
</div>
<div v-else class="text-muted-foreground text-sm">
暂无智能体协作记录
</div>
</div>
</div>
<div class="run-detail-main grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_420px] gap-4"> <div class="run-detail-main grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_420px] gap-4">
<div class="run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border"> <div class="run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border">
<WorkflowReadonlyCanvas <WorkflowReadonlyCanvas
@@ -390,7 +488,12 @@ defineExpose({ open });
word-break: break-word; word-break: break-word;
} }
.workflow-run-detail-dialog .run-report-json {
max-height: 140px;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.workflow-run-detail-dialog .run-report-grid,
.workflow-run-detail-dialog .run-detail-main { .workflow-run-detail-dialog .run-detail-main {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -110,6 +110,8 @@ const currentRunId = ref('');
const runStatus = ref('pending'); const runStatus = ref('pending');
const runError = ref(''); const runError = ref('');
const runElapsedTime = ref(0); const runElapsedTime = ref(0);
const runSubmittedInputs = ref<Record<string, any>>({});
const runFinalOutputs = ref<Record<string, any>>({});
const runEvents = ref< const runEvents = ref<
Array<{ Array<{
detail?: string; detail?: string;
@@ -286,6 +288,32 @@ function formatRunDuration(ms?: number) {
return `${(ms / 1000).toFixed(2)}s`; return `${(ms / 1000).toFixed(2)}s`;
} }
function previewRunValue(value: any, maxLength = 500) {
if (value === undefined || value === null || value === '') return '-';
const text =
typeof value === 'string'
? value
: JSON.stringify(value, null, 2);
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
}
function getWorkflowFinalOutput(outputs?: Record<string, any>) {
if (!outputs) return '';
const direct =
outputs.output ||
outputs.final_result ||
outputs.result ||
outputs.answer ||
outputs.summary;
if (direct) return previewRunValue(direct, 800);
const nodeValues = Object.entries(outputs)
.filter(([key]) => key.startsWith('_node_'))
.map(([, value]) => (value as any)?.output || value)
.filter(Boolean);
return previewRunValue(nodeValues.at(-1) || outputs, 800);
}
function pushRunEvent( function pushRunEvent(
title: string, title: string,
status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info', status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info',
@@ -305,6 +333,8 @@ function resetRunState() {
runStatus.value = 'running'; runStatus.value = 'running';
runError.value = ''; runError.value = '';
runElapsedTime.value = 0; runElapsedTime.value = 0;
runSubmittedInputs.value = {};
runFinalOutputs.value = {};
runEvents.value = []; runEvents.value = [];
} }
@@ -406,9 +436,12 @@ function handleWorkflowRunEvent(event: WorkflowStreamEvent) {
case 'complete': { case 'complete': {
runStatus.value = 'completed'; runStatus.value = 'completed';
runElapsedTime.value = event.elapsed_time || 0; runElapsedTime.value = event.elapsed_time || 0;
runFinalOutputs.value = event.outputs || {};
const finalOutput = getWorkflowFinalOutput(event.outputs);
pushRunEvent( pushRunEvent(
'流程运行完成', '流程运行完成',
'success', 'success',
finalOutput ||
`耗时 ${formatRunDuration(event.elapsed_time)}Token ${event.total_tokens || 0}`, `耗时 ${formatRunDuration(event.elapsed_time)}Token ${event.total_tokens || 0}`,
); );
ElMessage.success('流程运行完成'); ElMessage.success('流程运行完成');
@@ -434,6 +467,7 @@ function handleRunWorkflow(inputs: Record<string, any>) {
cancelWorkflowRunStream?.(); cancelWorkflowRunStream?.();
resetRunState(); resetRunState();
runSubmittedInputs.value = inputs;
runStatusDialogVisible.value = true; runStatusDialogVisible.value = true;
pushRunEvent('已提交运行请求', 'info', runWorkflowTarget.value.name); pushRunEvent('已提交运行请求', 'info', runWorkflowTarget.value.name);
@@ -905,7 +939,23 @@ onUnmounted(() => {
{{ runError }} {{ runError }}
</div> </div>
<ElScrollbar height="320px"> <div class="grid gap-3 md:grid-cols-2">
<div class="border-border bg-muted/30 rounded-md border p-3">
<div class="mb-2 text-sm font-medium">本次任务</div>
<pre class="run-preview">{{ previewRunValue(runSubmittedInputs) }}</pre>
</div>
<div class="border-border bg-muted/30 rounded-md border p-3">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">最终输出</span>
<span class="text-muted-foreground text-xs">
{{ formatRunDuration(runElapsedTime) }}
</span>
</div>
<pre class="run-preview">{{ getWorkflowFinalOutput(runFinalOutputs) || '-' }}</pre>
</div>
</div>
<ElScrollbar height="300px">
<div class="space-y-2 pr-2"> <div class="space-y-2 pr-2">
<div <div
v-for="event in runEvents" v-for="event in runEvents"
@@ -954,3 +1004,13 @@ onUnmounted(() => {
<ImportDialog v-model="showImportDialog" @imported="handleImported" /> <ImportDialog v-model="showImportDialog" @imported="handleImported" />
</div> </div>
</template> </template>
<style scoped>
.run-preview {
margin: 0;
max-height: 120px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
</style>