@@ -3,7 +3,7 @@ import type { Edge, Node } from '@vue-flow/core';
import type { WorkflowRun } from '#/api/ai-platform/ai-platform' ;
import { computed , defineAsyncComponent , ref } from 'vue' ;
import { computed , defineAsyncComponent , nextTick , ref } from 'vue' ;
import { useRouter } from 'vue-router' ;
import { ExternalLink } from '@vben/icons' ;
@@ -14,6 +14,7 @@ import {
ElButton ,
ElCollapse ,
ElCollapseItem ,
ElDialog ,
ElScrollbar ,
ElTag ,
} from 'element-plus' ;
@@ -44,13 +45,18 @@ const triggerOptions = getTriggerOptions();
const visible = ref ( false ) ;
const loading = ref ( false ) ;
const canvasLoading = ref ( false ) ;
const runId = ref ( '' ) ;
const run = ref < WorkflowRun | null > ( null ) ;
const nodes = ref < Node [ ] > ( [ ] ) ;
const edges = ref < Edge [ ] > ( [ ] ) ;
const activeNodeId = ref ( '' ) ;
const activeLogIndex = ref ( - 1 ) ;
const definitionFallback = ref ( false ) ;
const activeLogNames = ref < string [ ] > ( [ ] ) ;
const valueDialogVisible = ref ( false ) ;
const valueDialogTitle = ref ( '' ) ;
const valueDialogContent = ref ( '' ) ;
const dialogTitle = computed ( ( ) => run . value ? . workflow _name || $t ( 'ai-platform.workflowRuns.detail.runId' ) ) ;
const executionLogs = computed ( ( ) => run . value ? . execution _log || [ ] ) ;
@@ -83,6 +89,12 @@ const finalOutput = computed(() => {
. filter ( Boolean ) ;
return previewValue ( nodeValues . at ( - 1 ) || outputs ) ;
} ) ;
const selectedNodeLabel = computed ( ( ) => {
const nodeId = activeNodeId . value ;
if ( ! nodeId ) return '' ;
const log = executionLogs . value . find ( ( item ) => item . node _id === nodeId ) ;
return log ? . node _label || log ? . node _id || nodeId ;
} ) ;
const agentSummaries = computed ( ( ) => {
const agents = new Map <
string ,
@@ -114,6 +126,7 @@ const agentSummaries = computed(() => {
return [ ... agents . values ( ) ] ;
} ) ;
const collaborationTimeline = computed ( ( ) => {
let estimatedOffset = 0 ;
return executionLogs . value
. map ( ( log , index ) => {
const meta = getLogMeta ( log ) ;
@@ -137,6 +150,11 @@ const collaborationTimeline = computed(() => {
const model = meta . model || collaboration . model || meta . model _id || collaboration . model _id ;
const targetName =
target . agent _name || target . agent _code || target . node _label || '' ;
const timestamp =
log . timestamp ||
communication . timestamp ||
estimateLogTimestamp ( estimatedOffset ) ;
estimatedOffset += Number ( log . elapsed _time || 0 ) ;
const title =
agentName ||
subflowName ||
@@ -154,6 +172,7 @@ const collaborationTimeline = computed(() => {
return {
key : ` ${ log . node _id || 'node' } - ${ index } ` ,
index ,
nodeId : log . node _id ,
title ,
subtitle : log . node _label || log . node _id ,
@@ -161,19 +180,121 @@ const collaborationTimeline = computed(() => {
summary : summaryParts . join ( ' · ' ) ,
message : communication . message || previewValue ( log . error || log . output ) ,
elapsed : log . elapsed _time ,
timestamp ,
tokens : log . tokens _used || 0 ,
hasSignal : Boolean ( agentName || branch || subflowName || model || communication . channel ) ,
} ;
} )
. filter ( ( item ) => item . hasSignal ) ;
} ) ;
const collaborationEdges = computed ( ( ) => {
const edges : Array < {
channel : string ;
count : number ;
from : string ;
key : string ;
to : string ;
} > = [ ] ;
const seen = new Map < string , number > ( ) ;
for ( let index = 0 ; index < collaborationTimeline . value . length ; index += 1 ) {
const item = collaborationTimeline . value [ index ] ;
if ( ! item ) continue ;
const log = executionLogs . value [ item . index ] ;
if ( ! log ) continue ;
const target = getTargetName ( log ) ;
const next = collaborationTimeline . value [ index + 1 ] ;
const to = target || next ? . title || '' ;
if ( ! item . title || ! to || item . title === to ) continue ;
const channel = getCommunication ( log ) . channel || 'workflow_edge' ;
const key = ` ${ item . title } -> ${ to } : ${ channel } ` ;
const count = ( seen . get ( key ) || 0 ) + 1 ;
seen . set ( key , count ) ;
edges . push ( {
key : ` ${ key } : ${ count } ` ,
from : item . title ,
to ,
channel ,
count ,
} ) ;
}
return edges ;
} ) ;
const collaborationSupportSummary = computed ( ( ) => {
const modes = new Set < string > ( ) ;
const traceText = executionLogs . value
. map ( ( log ) =>
[
getActorName ( log ) ,
getTargetName ( log ) ,
log . node _label ,
log . node _type ,
log . branch _label ,
previewValue ( log . output ) ,
previewValue ( log . error ) ,
] . join ( ' ' ) ,
)
. join ( ' ' )
. toLowerCase ( ) ;
for ( const log of executionLogs . value ) {
const collaboration = getCollaboration ( log ) ;
const communication = getCommunication ( log ) ;
if ( collaboration . agent _code ) modes . add ( 'agent' ) ;
if ( collaboration . collaboration _mode ) modes . add ( collaboration . collaboration _mode ) ;
if ( communication . channel ) modes . add ( communication . channel ) ;
if ( log . node _type ) modes . add ( log . node _type ) ;
}
const supportsParallel =
modes . has ( 'parallel' ) ||
modes . has ( 'parallel_branch' ) ||
executionLogs . value . some ( ( log ) => log . branch || log . branch _label ) ;
const supportsHandoff = executionLogs . value . some ( ( log , index ) => {
if ( getTargetName ( log ) ) return true ;
const current = getActorName ( log ) ;
const next = executionLogs . value [ index + 1 ]
? getActorName ( executionLogs . value [ index + 1 ] )
: '' ;
return current && next && current !== next ;
} ) ;
const supportsLoop =
modes . has ( 'loop' ) ||
executionLogs . value . some ( ( log ) => String ( log . node _type ) . includes ( 'loop' ) ) ||
/返工|rework|不通过|阻塞/ . test ( traceText ) ;
const supportsSubflow = modes . has ( 'subflow' ) ;
const supportsHumanInput =
modes . has ( 'human_input' ) ||
executionLogs . value . some ( ( log ) => log . status === 'waiting' ) ;
const supportsPm = /\bpm\b|产品|分诊|拆解|汇总|收口/ . test ( traceText ) ;
const supportsDev = /dev|developer|backend|后端|开发|数据|架构/ . test ( traceText ) ;
const supportsFrontend = /frontend|前端/ . test ( traceText ) ;
const supportsQa = /\bqa\b|测试|复验|质量/ . test ( traceText ) ;
const supportsAcceptance = /验收|闭环|完成/ . test ( traceText ) ;
return [
supportsPm ? 'PM 分发' : '' ,
supportsParallel ? '并行分支' : '' ,
supportsHandoff ? 'Agent 交接' : '' ,
supportsDev ? 'Dev 实现' : '' ,
supportsFrontend ? 'Frontend 介入' : '' ,
supportsQa ? 'QA 复验' : '' ,
supportsLoop ? '返工闭环' : '' ,
supportsAcceptance ? '验收收口' : '' ,
supportsSubflow ? '子流程' : '' ,
supportsHumanInput ? '人工确认' : '' ,
] . filter ( Boolean ) ;
} ) ;
async function loadDetail ( ) {
if ( ! runId . value ) return ;
loading . value = true ;
canvasLoading . value = false ;
definitionFallback . value = false ;
activeNodeId . value = '' ;
activeLogIndex . value = - 1 ;
run . value = null ;
nodes . value = [ ] ;
edges . value = [ ] ;
@@ -181,6 +302,14 @@ async function loadDetail() {
try {
const detail = await getWorkflowRunDetailApi ( runId . value ) ;
run . value = detail ;
activeLogNames . value = executionLogs . value . slice ( 0 , 1 ) . map ( ( _ , index ) => String ( index ) ) ;
if ( executionLogs . value [ 0 ] ? . node _id ) {
activeNodeId . value = executionLogs . value [ 0 ] . node _id ;
activeLogIndex . value = 0 ;
}
loading . value = false ;
canvasLoading . value = true ;
await nextTick ( ) ;
let definition = detail . definition _snapshot ;
if ( ! definition ? . nodes ? . length ) {
@@ -200,11 +329,11 @@ async function loadDetail() {
const replayed = applyRunReplay ( base . nodes , base . edges , detail ) ;
nodes . value = replayed . nodes ;
edges . value = replayed . edges ;
activeLogNames . value = executionLogs . value . slice ( 0 , 1 ) . map ( ( _ , index ) => String ( index ) ) ;
} catch ( error ) {
console . error ( error ) ;
} finally {
loading . value = false ;
canvasLoading . value = false ;
}
}
@@ -261,6 +390,36 @@ function previewValue(value: any) {
}
}
function compactValue ( value : any , maxLength = 360 ) {
const text = previewValue ( value ) ;
if ( text . length <= maxLength ) return text ;
return ` ${ text . slice ( 0 , maxLength ) } ... ` ;
}
function openValueDialog ( title : string , value : any ) {
valueDialogTitle . value = title ;
valueDialogContent . value = previewValue ( value ) ;
valueDialogVisible . value = true ;
}
function estimateLogTimestamp ( offset : number ) {
if ( ! run . value ? . started _at ) return '' ;
const start = new Date ( run . value . started _at ) . getTime ( ) ;
if ( ! Number . isFinite ( start ) ) return '' ;
return new Date ( start + offset ) . toISOString ( ) ;
}
function formatTimelineTime ( value ? : string ) {
if ( ! value ) return '-' ;
const date = new Date ( value ) ;
if ( Number . isNaN ( date . getTime ( ) ) ) return '-' ;
return date . toLocaleTimeString ( [ ] , {
hour : '2-digit' ,
minute : '2-digit' ,
second : '2-digit' ,
} ) ;
}
function getLogMeta ( log : any ) {
return log ? . metadata || { } ;
}
@@ -294,9 +453,19 @@ function getTargetName(log: any) {
return target . agent _name || target . agent _code || target . node _label || '' ;
}
function selectLog ( nodeId ? : string ) {
function selectLog ( nodeId ? : string , index ? : number ) {
if ( ! nodeId ) return ;
activeNodeId . value = nodeId ;
const resolvedIndex =
typeof index === 'number'
? index
: executionLogs . value . findIndex ( ( item ) => item . node _id === nodeId ) ;
if ( resolvedIndex >= 0 ) {
activeLogIndex . value = resolvedIndex ;
if ( ! activeLogNames . value . includes ( String ( resolvedIndex ) ) ) {
activeLogNames . value = [ String ( resolvedIndex ) , ... activeLogNames . value ] ;
}
}
}
defineExpose ( { open } ) ;
@@ -307,7 +476,7 @@ defineExpose({ open });
v-model = "visible"
class = "workflow-run-detail-dialog"
:title = "dialogTitle"
:loading = "loading "
:loading = "false "
default -fullscreen
:draggable = "false"
:show-footer = "false"
@@ -384,19 +553,30 @@ defineExpose({ open });
< 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 class = "mb-2 flex items-center justify-between gap-2 " >
< span class = "text-sm font-medium" >
{ { $t ( 'ai-platform.workflowRuns.detail.taskInput' ) } }
< / span >
< ElButton link type = "primary" @click ="openValueDialog('任务输入', run.inputs)" >
查看全部
< / ElButton >
< / div >
< pre class = "run-json run-report-json" > { { taskSummary } } < / pre >
< pre class = "run-json run-report-json is-clickable" @click ="openValueDialog('任务输入', run.inputs) "> {{ compactValue ( taskSummary ) }} < / pre >
< / div >
< div class = "border-border bg-muted/30 rounded-lg border p-3" >
< div class = "mb-2 text-sm font-medium " >
< div class = "mb-2 flex items-center justify-between gap-2 " >
< span class = "text-sm font-medium" >
{ { $t ( 'ai-platform.workflowRuns.detail.finalOutput' ) } }
< / span >
< ElButton link type = "primary" @click ="openValueDialog('最终输出', run.error_message || run.outputs)" >
查看全部
< / ElButton >
< / div >
< pre
: class = "run.status === 'failed' ? 'text-destructive' : ''"
class = "run-json run-report-json"
> { { run . error _message || finalOutput } } < / pre >
class = "run-json run-report-json is-clickable "
@click ="openValueDialog('最终输出', run.error_message || run.outputs)"
> { { compactValue ( 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" >
@@ -435,8 +615,9 @@ defineExpose({ open });
v-for = "item in collaborationTimeline"
:key = "item.key"
class = "collaboration-item"
: class = "{ 'is-active': item.nodeId === activeNodeId }"
type = "button"
@click ="selectLog(item.nodeId)"
@click ="selectLog(item.nodeId, item.index )"
>
< span
class = "collaboration-dot"
@@ -450,8 +631,9 @@ defineExpose({ open });
{ { item . summary || item . subtitle } }
< / span >
< / span >
< span class = "text-muted-foreground shrink-0 text-xs" >
{ { formatDuration ( item . elapsed ) } }
< span class = "text-muted-foreground shrink-0 text-right text-xs" >
< span class = "block" > { { formatTimelineTime ( item . timestamp ) } } < / span >
< span class = "block" > { { formatDuration ( item . elapsed ) } } < / span >
< / span >
< / button >
< / div >
@@ -462,11 +644,65 @@ defineExpose({ open });
< / div >
< / div >
< div
v-if = "collaborationTimeline.length"
class = "border-border bg-muted/20 rounded-lg border p-3"
>
< div class = "mb-3 flex flex-wrap items-center justify-between gap-2" >
< div >
< div class = "text-sm font-medium" > 多智能体协作链路 < / div >
< div class = "text-muted-foreground mt-1 text-xs" >
支持 { { collaborationSupportSummary . join ( '、' ) || '工作流编排' } } ; 点击任一节点会定位到画布和右侧日志 。
< / div >
< / div >
< ElTag v-if = "selectedNodeLabel" type="primary" >
当前定位 : {{ selectedNodeLabel }}
< / ElTag >
< / div >
< div class = "collaboration-flow" >
< template
v-for = "(item, index) in collaborationTimeline"
:key = "`flow-${item.key}`"
>
< button
class = "collaboration-flow-node"
: class = "{ 'is-active': item.nodeId === activeNodeId }"
type = "button"
@click ="selectLog(item.nodeId, item.index)"
>
< span class = "flow-time" > { { formatTimelineTime ( item . timestamp ) } } < / span >
< span class = "flow-title" > { { item . title } } < / span >
< span class = "flow-summary" > { { item . summary || item . subtitle } } < / span >
< / button >
< span
v-if = "index < collaborationTimeline.length - 1"
class = "collaboration-flow-arrow"
>
→
< / span >
< / template >
< / div >
< div v-if = "collaborationEdges.length" class="collaboration-edge-list" >
< span
v-for = "edge in collaborationEdges"
:key = "edge.key"
class = "collaboration-edge-item"
>
{ { edge . from } } → { { edge . to } }
< small > { { edge . channel } } { { edge . count > 1 ? ` # ${ edge . count } ` : '' } } < / small >
< / span >
< / 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" >
< div
v-loading = "canvasLoading"
class = "run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border"
>
< WorkflowReadonlyCanvas
:nodes = "nodes"
:edges = "edges"
:active-node-id = "activeNodeId"
:fit-node-id = "activeNodeId"
/ >
< / div >
@@ -495,7 +731,8 @@ defineExpose({ open });
< template # title >
< div
class = "flex min-w-0 flex-1 items-center gap-2"
@click ="selectLog(log.node_id)"
: class = "{ 'text-primary': activeLogIndex === index }"
@click ="selectLog(log.node_id, index)"
>
< ElTag :type = "getLogStatusType(log.status)" size = "small" >
{ { getLogStatusText ( log . status ) } }
@@ -585,17 +822,23 @@ defineExpose({ open });
< / div >
< / div >
< / div >
< div >
< div class = "text-muted-foreground mb-1 font-medium" >
{ { $t ( 'ai-platform.workflowRuns.detail.inputsOutputs' ) } } - 输入
< div class = "run-io-block" >
< div class = "text-muted-foreground mb-1 flex items-center justify-between gap-2 font-medium" >
< span > { { $t ( 'ai-platform.workflowRuns.detail.inputsOutputs' ) } } - 输入 < / span >
< ElButton link type = "primary" @click ="openValueDialog(`${log.node_label || log.node_id} 输入`, log.inputs)" >
查看全部
< / ElButton >
< / div >
< pre class = "run-json" > { { previewValue ( log . inputs ) } } < / pre >
< pre class = "run-json is-clickable" @click ="openValueDialog(`${log.node_label || log.node_id} 输入`, log.inputs) "> {{ compactValue ( log.inputs ) }} < / pre >
< / div >
< div >
< div class = "text-muted-foreground mb-1 font-medium" >
{ { $t ( 'ai-platform.workflowRuns.detail.inputsOutputs' ) } } - 输出
< div class = "run-io-block" >
< div class = "text-muted-foreground mb-1 flex items-center justify-between gap-2 font-medium" >
< span > { { $t ( 'ai-platform.workflowRuns.detail.inputsOutputs' ) } } - 输出 < / span >
< ElButton link type = "primary" @click ="openValueDialog(`${log.node_label || log.node_id} 输出`, log.output)" >
查看全部
< / ElButton >
< / div >
< pre class = "run-json" > { { previewValue ( log . output ) } } < / pre >
< pre class = "run-json is-clickable" @click ="openValueDialog(`${log.node_label || log.node_id} 输出`, log.output) "> {{ compactValue ( log.output ) }} < / pre >
< / div >
< div class = "text-muted-foreground flex gap-3" >
< span >
@@ -612,6 +855,23 @@ defineExpose({ open });
< / div >
< / div >
< / div >
< div v-else class = "flex min-h-[420px] items-center justify-center" >
< div class = "text-muted-foreground text-sm" >
{ { loading ? '正在加载运行详情...' : '暂无运行详情' } }
< / div >
< / div >
< ElDialog
v-model = "valueDialogVisible"
append -to -body
class = "run-value-dialog"
destroy -on -close
:title = "valueDialogTitle"
width = "78vw"
>
< pre class = "run-value-full" > { { valueDialogContent } } < / pre >
< / ElDialog >
< / ZqDialog >
< / template >
@@ -659,8 +919,21 @@ defineExpose({ open });
word - break : break - word ;
}
. workflow - run - detail - dialog . run - json . is - clickable {
cursor : pointer ;
transition :
background 0.15 s ease ,
box - shadow 0.15 s ease ;
}
. workflow - run - detail - dialog . run - json . is - clickable : hover {
background : var ( -- el - fill - color - light ) ;
box - shadow : inset 0 0 0 1 px var ( -- el - color - primary - light - 5 ) ;
}
. workflow - run - detail - dialog . run - report - json {
max - height : 140 px ;
min - height : 120 px ;
max - height : 240 px ;
}
. workflow - run - detail - dialog . collaboration - timeline {
@@ -688,6 +961,12 @@ defineExpose({ open });
background : var ( -- el - fill - color - light ) ;
}
. workflow - run - detail - dialog . collaboration - item . is - active {
border - color : var ( -- el - color - primary ) ;
background : var ( -- el - color - primary - light - 9 ) ;
box - shadow : 0 0 0 2 px var ( -- el - color - primary - light - 7 ) ;
}
. workflow - run - detail - dialog . collaboration - dot {
width : 8 px ;
height : 8 px ;
@@ -711,10 +990,138 @@ defineExpose({ open });
background : var ( -- el - color - primary ) ;
}
. workflow - run - detail - dialog . collaboration - flow {
display : flex ;
align - items : stretch ;
gap : 8 px ;
overflow - x : auto ;
padding - bottom : 4 px ;
}
. workflow - run - detail - dialog . collaboration - flow - node {
flex : 0 0 190 px ;
min - width : 0 ;
border : 1 px solid var ( -- el - border - color - lighter ) ;
border - radius : 8 px ;
background : var ( -- el - fill - color - blank ) ;
padding : 10 px ;
text - align : left ;
cursor : pointer ;
transition :
border - color 0.15 s ease ,
box - shadow 0.15 s ease ,
background 0.15 s ease ;
}
. workflow - run - detail - dialog . collaboration - flow - node : hover {
border - color : var ( -- el - color - primary - light - 5 ) ;
background : var ( -- el - fill - color - light ) ;
}
. workflow - run - detail - dialog . collaboration - flow - node . is - active {
border - color : var ( -- el - color - primary ) ;
background : var ( -- el - color - primary - light - 9 ) ;
box - shadow : 0 0 0 2 px var ( -- el - color - primary - light - 7 ) ;
}
. workflow - run - detail - dialog . collaboration - flow - arrow {
display : flex ;
flex : 0 0 18 px ;
align - items : center ;
justify - content : center ;
color : var ( -- el - text - color - placeholder ) ;
font - size : 16 px ;
line - height : 1 ;
}
. workflow - run - detail - dialog . flow - time {
display : block ;
color : var ( -- el - color - primary ) ;
font - family : ui - monospace , SFMono - Regular , Menlo , Monaco , Consolas , monospace ;
font - size : 11 px ;
line - height : 16 px ;
}
. workflow - run - detail - dialog . flow - title {
display : block ;
margin - top : 4 px ;
overflow : hidden ;
color : var ( -- el - text - color - primary ) ;
font - size : 13 px ;
font - weight : 600 ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. workflow - run - detail - dialog . flow - summary {
display : - webkit - box ;
margin - top : 3 px ;
overflow : hidden ;
color : var ( -- el - text - color - secondary ) ;
font - size : 12 px ;
line - height : 18 px ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 2 ;
}
. workflow - run - detail - dialog . collaboration - edge - list {
display : flex ;
flex - wrap : wrap ;
gap : 6 px ;
margin - top : 10 px ;
}
. workflow - run - detail - dialog . collaboration - edge - item {
border : 1 px solid var ( -- el - border - color - lighter ) ;
border - radius : 999 px ;
background : var ( -- el - fill - color - light ) ;
padding : 3 px 8 px ;
color : var ( -- el - text - color - regular ) ;
font - size : 12 px ;
}
. workflow - run - detail - dialog . collaboration - edge - item small {
margin - left : 4 px ;
color : var ( -- el - text - color - secondary ) ;
}
. workflow - run - detail - dialog . run - io - block . el - button {
height : auto ;
padding : 0 ;
font - size : 12 px ;
}
. run - value - dialog . el - dialog _ _body {
padding - top : 8 px ;
}
. run - value - full {
max - height : 70 vh ;
margin : 0 ;
overflow : auto ;
border - radius : 8 px ;
background : var ( -- el - fill - color - light ) ;
padding : 14 px ;
color : var ( -- el - text - color - primary ) ;
font - size : 13 px ;
line - height : 1.6 ;
white - space : pre - wrap ;
word - break : break - word ;
}
@ media ( max - width : 1024 px ) {
. workflow - run - detail - dialog . run - report - grid ,
. workflow - run - detail - dialog . run - detail - main {
grid - template - columns : 1 fr ;
}
. workflow - run - detail - dialog . collaboration - flow {
flex - wrap : wrap ;
overflow - x : visible ;
}
. workflow - run - detail - dialog . collaboration - flow - node {
flex : 1 1 180 px ;
}
}
< / style >