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 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,10 +436,13 @@ 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',
|
||||||
`耗时 ${formatRunDuration(event.elapsed_time)},Token ${event.total_tokens || 0}`,
|
finalOutput ||
|
||||||
|
`耗时 ${formatRunDuration(event.elapsed_time)},Token ${event.total_tokens || 0}`,
|
||||||
);
|
);
|
||||||
ElMessage.success('流程运行完成');
|
ElMessage.success('流程运行完成');
|
||||||
cancelWorkflowRunStream = null;
|
cancelWorkflowRunStream = null;
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user