feat: improve workflow run visibility
This commit is contained in:
@@ -54,6 +54,64 @@ const activeLogNames = ref<string[]>([]);
|
||||
|
||||
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
||||
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() {
|
||||
if (!runId.value) return;
|
||||
@@ -240,6 +298,46 @@ defineExpose({ open });
|
||||
</ZqDescItem>
|
||||
</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-canvas border-border min-h-0 overflow-hidden rounded-lg border">
|
||||
<WorkflowReadonlyCanvas
|
||||
@@ -390,7 +488,12 @@ defineExpose({ open });
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .run-report-json {
|
||||
max-height: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.workflow-run-detail-dialog .run-report-grid,
|
||||
.workflow-run-detail-dialog .run-detail-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ const currentRunId = ref('');
|
||||
const runStatus = ref('pending');
|
||||
const runError = ref('');
|
||||
const runElapsedTime = ref(0);
|
||||
const runSubmittedInputs = ref<Record<string, any>>({});
|
||||
const runFinalOutputs = ref<Record<string, any>>({});
|
||||
const runEvents = ref<
|
||||
Array<{
|
||||
detail?: string;
|
||||
@@ -286,6 +288,32 @@ function formatRunDuration(ms?: number) {
|
||||
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(
|
||||
title: string,
|
||||
status: 'failed' | 'info' | 'running' | 'success' | 'warning' = 'info',
|
||||
@@ -305,6 +333,8 @@ function resetRunState() {
|
||||
runStatus.value = 'running';
|
||||
runError.value = '';
|
||||
runElapsedTime.value = 0;
|
||||
runSubmittedInputs.value = {};
|
||||
runFinalOutputs.value = {};
|
||||
runEvents.value = [];
|
||||
}
|
||||
|
||||
@@ -406,10 +436,13 @@ function handleWorkflowRunEvent(event: WorkflowStreamEvent) {
|
||||
case 'complete': {
|
||||
runStatus.value = 'completed';
|
||||
runElapsedTime.value = event.elapsed_time || 0;
|
||||
runFinalOutputs.value = event.outputs || {};
|
||||
const finalOutput = getWorkflowFinalOutput(event.outputs);
|
||||
pushRunEvent(
|
||||
'流程运行完成',
|
||||
'success',
|
||||
`耗时 ${formatRunDuration(event.elapsed_time)},Token ${event.total_tokens || 0}`,
|
||||
finalOutput ||
|
||||
`耗时 ${formatRunDuration(event.elapsed_time)},Token ${event.total_tokens || 0}`,
|
||||
);
|
||||
ElMessage.success('流程运行完成');
|
||||
cancelWorkflowRunStream = null;
|
||||
@@ -434,6 +467,7 @@ function handleRunWorkflow(inputs: Record<string, any>) {
|
||||
|
||||
cancelWorkflowRunStream?.();
|
||||
resetRunState();
|
||||
runSubmittedInputs.value = inputs;
|
||||
runStatusDialogVisible.value = true;
|
||||
pushRunEvent('已提交运行请求', 'info', runWorkflowTarget.value.name);
|
||||
|
||||
@@ -905,7 +939,23 @@ onUnmounted(() => {
|
||||
{{ runError }}
|
||||
</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
|
||||
v-for="event in runEvents"
|
||||
@@ -954,3 +1004,13 @@ onUnmounted(() => {
|
||||
<ImportDialog v-model="showImportDialog" @imported="handleImported" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.run-preview {
|
||||
margin: 0;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user