Files
AgentCoord/frontend/src/layout/components/Main/TaskTemplate/TaskResult/index.vue
2026-01-26 15:06:17 +08:00

1969 lines
66 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onUnmounted, ref, reactive, nextTick, watch, onMounted } from 'vue'
import { throttle } from 'lodash'
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
import AdditionalOutputCard from './AdditionalOutputCard.vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
import variables from '@/styles/variables.module.scss'
import { type IRawStepTask, useAgentsStore, type IRawPlanResponse } from '@/stores'
import api, { type StreamingEvent } from '@/api'
import ProcessCard from '../TaskProcess/ProcessCard.vue'
import ExecutePlan from './ExecutePlan.vue'
import websocket from '@/utils/websocket'
import Notification from '@/components/Notification/Notification.vue'
import { useNotification } from '@/composables/useNotification'
const emit = defineEmits<{
(e: 'refreshLine'): void
(el: 'setCurrentTask', task: IRawStepTask): void
}>()
const agentsStore = useAgentsStore()
const drawerVisible = ref(false)
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// Step execution status enum
enum StepExecutionStatus {
WAITING = 'waiting', // Waiting for data
READY = 'ready', // Ready to execute
RUNNING = 'running', // Currently running
COMPLETED = 'completed', // Execution completed
FAILED = 'failed' // Execution failed
}
// Execution status for each step
const stepExecutionStatus = ref<Record<string, StepExecutionStatus>>({})
// 用于标记暂停时的"最后动作完成"状态
const isPausing = ref(false) // 正在请求暂停(等待当前动作完成)
const isRestarting = ref(false) // 正在重新执行(停止旧执行导致的错误不应显示)
// Check if step is ready to execute (has TaskProcess data)
const isStepReady = (step: IRawStepTask) => {
return step.TaskProcess && step.TaskProcess.length > 0
}
// 判断动作是否有执行结果
const hasActionResult = (step: IRawStepTask, actionId: string) => {
const stepResult = agentsStore.executePlan.find(
r => r.NodeId === step.StepName && r.LogNodeType === 'step'
)
if (!stepResult || !stepResult.ActionHistory) {
return false
}
return stepResult.ActionHistory.some(action => action.ID === actionId)
}
// 判断 OutputObject 是否有执行结果
const hasObjectResult = (outputObject?: string) => {
if (!outputObject) return false
return agentsStore.executePlan.some(r => r.NodeId === outputObject && r.LogNodeType === 'object')
}
// Get execution status of a step
const getStepStatus = (step: IRawStepTask): StepExecutionStatus => {
const stepName = step.StepName || step.Id || ''
// If status is already recorded, return it
if (stepExecutionStatus.value[stepName]) {
return stepExecutionStatus.value[stepName]
}
// Check if has TaskProcess data
if (isStepReady(step)) {
return StepExecutionStatus.READY
} else {
return StepExecutionStatus.WAITING
}
}
// Calculate preparation status of all steps
const stepsReadyStatus = computed(() => {
const steps = collaborationProcess.value
const readySteps: string[] = []
const waitingSteps: string[] = []
steps.forEach(step => {
if (isStepReady(step)) {
readySteps.push(step.StepName || 'Unknown step')
} else {
waitingSteps.push(step.StepName || 'Unknown step')
}
})
return {
ready: readySteps,
waiting: waitingSteps,
allReady: waitingSteps.length === 0,
totalCount: steps.length,
readyCount: readySteps.length
}
})
/**
* 监听步骤数据变化,更新步骤状态并动态追加新步骤
*/
watch(
() => collaborationProcess.value,
newSteps => {
newSteps.forEach(step => {
const stepId = step.Id || step.StepName || ''
const stepName = step.StepName || step.Id || ''
const currentStatus = stepExecutionStatus.value[stepName]
if (isStepReady(step)) {
// 步骤数据已就绪,更新状态
if (!currentStatus || currentStatus === StepExecutionStatus.WAITING) {
stepExecutionStatus.value[stepName] = StepExecutionStatus.READY
}
// 动态追加新步骤到执行队列
if (loading.value && isStreaming.value && currentExecutionId.value) {
if (!sentStepIds.value.has(stepId)) {
console.log(`🔄 Watch监听到新就绪步骤: ${stepName}, 准备追加到执行队列`)
console.log(` - loading: ${loading.value}`)
console.log(` - isStreaming: ${isStreaming.value}`)
console.log(` - currentExecutionId: ${currentExecutionId.value}`)
sentStepIds.value.add(stepId)
// 异步追加步骤到后端执行队列
api
.addStepsToExecution(currentExecutionId.value, [step])
.then(addedCount => {
if (addedCount > 0) {
console.log(`✅ 成功追加步骤: ${stepName}`)
// 更新总步骤数显示
const totalStepsCount = collaborationProcess.value.length
const currentStep = executionProgress.value.currentStep || 1
executionProgress.value.totalSteps = totalStepsCount
} else {
console.warn(`⚠️ 追加步骤失败: ${stepName} - 执行ID不存在或已结束`)
// 追加失败,移除标记以便重试
sentStepIds.value.delete(stepId)
}
})
.catch(error => {
console.error(`❌ 追加步骤失败: ${stepName}`, error)
// 追加失败,移除标记以便重试
sentStepIds.value.delete(stepId)
})
}
} else if (loading.value && !isStreaming.value) {
console.log(`⚠️ 步骤 ${stepName} 已就绪,但尚未开始流式传输`)
} else if (loading.value && isStreaming.value && !currentExecutionId.value) {
console.log(`⚠️ 步骤 ${stepName} 已就绪但currentExecutionId为空`)
}
} else {
// 步骤未就绪设置为WAITING
if (!currentStatus) {
stepExecutionStatus.value[stepName] = StepExecutionStatus.WAITING
}
}
})
},
{ deep: true }
)
// Watch additional outputs changes
watch(
() => agentsStore.additionalOutputs,
() => {
nextTick(() => {
setTimeout(() => {
jsplumb.repaintEverything()
}, 0)
})
},
{ deep: true }
)
// Edit logic
const editMode = ref(false)
const editMap = reactive<Record<string, boolean>>({})
const editBuffer = reactive<Record<string, string | undefined>>({})
const showPopover = ref(false)
function getProcessDescription(stepId: string, processId: string) {
const step = collaborationProcess.value.find(s => s.Id === stepId)
if (step) {
const process = step.TaskProcess.find(p => p.ID === processId)
return process?.Description || ''
}
return ''
}
function handleOpenEdit(stepId: string, processId: string) {
if (!editMode.value) return
const key = `${stepId}-${processId}`
editMap[key] = true
editBuffer[key] = getProcessDescription(stepId, processId)
}
function handleSaveEdit(stepId: string, processId: string, value: string) {
const key = `${stepId}-${processId}`
const step = collaborationProcess.value.find(s => s.Id === stepId)
if (step) {
const process = step.TaskProcess.find(p => p.ID === processId)
if (process) {
// 保存旧值用于比较
const oldValue = process.Description
// 只有内容真正变化时才更新和记录修改
if (value !== oldValue) {
process.Description = value
// 记录修改过的步骤索引到 store
const stepIndex = collaborationProcess.value.findIndex(s => s.Id === stepId)
if (stepIndex >= 0) {
agentsStore.addModifiedStep(stepIndex)
console.log(
`📝 步骤 ${stepIndex + 1} (${step.StepName}) 的描述已被修改,将从该步骤重新执行`
)
}
} else {
console.log(` 步骤 ${step.StepName} 的描述未发生变化,不记录修改`)
}
}
}
editMap[key] = false
}
const jsplumb = new Jsplumb('task-results-main', {
connector: {
type: BezierConnector.type,
options: { curviness: 30, stub: 10 }
}
})
// Refresh connections in real-time when collapsing panels
let timer: ReturnType<typeof setInterval> | null = null
function handleCollapse() {
if (timer) {
clearInterval(timer)
}
timer = setInterval(() => {
jsplumb.repaintEverything()
emit('refreshLine')
}, 1) as ReturnType<typeof setInterval>
// Default fully open after 3 seconds
const timer1 = setTimeout(() => {
if (timer) {
clearInterval(timer)
timer = null
}
}, 3000)
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
if (timer1) {
clearInterval(timer1)
}
})
}
// Create internal connections
function createInternalLine(id?: string) {
const arr: ConnectArg[] = []
jsplumb.reset()
collaborationProcess.value.forEach(item => {
// 创建左侧流程与产出的连线
arr.push({
sourceId: `task-results-${item.Id}-0`,
targetId: `task-results-${item.Id}-1`,
anchor: [AnchorLocations.Left, AnchorLocations.Left]
})
collaborationProcess.value.forEach(jitem => {
// 创建左侧产出与上一步流程的连线
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
arr.push({
sourceId: `task-results-${jitem.Id}-1`,
targetId: `task-results-${item.Id}-0`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
config: {
type: 'output'
}
})
}
// 创建右侧任务程序与InputObject字段的连线
jitem.TaskProcess.forEach(i => {
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
const sourceId = `task-results-${item.Id}-1`
const targetId = `task-results-${jitem.Id}-0-${i.ID}`
arr.push({
sourceId,
targetId,
anchor: [AnchorLocations.Right, AnchorLocations.Right],
config: {
stops: [
[0, color],
[1, color]
],
transparent: targetId !== id
}
})
}
})
})
// 创建右侧TaskProcess内部连线
item.TaskProcess?.forEach(i => {
if (!i.ImportantInput?.length) {
return
}
item.TaskProcess?.forEach(i2 => {
if (i.ImportantInput.includes(`ActionResult:${i2.ID}`)) {
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
const sourceId = `task-results-${item.Id}-0-${i2.ID}`
const targetId = `task-results-${item.Id}-0-${i.ID}`
arr.push({
sourceId,
targetId,
anchor: [AnchorLocations.Right, AnchorLocations.Right],
config: {
stops: [
[0, color],
[1, color]
],
transparent: targetId !== id
}
})
}
})
})
})
jsplumb.connects(arr)
jsplumb.repaintEverything()
}
const loading = ref(false)
const executionProgress = ref({
currentStep: 0,
totalSteps: 0,
currentAction: 0,
totalActions: 0,
currentStepName: ''
})
// Notification system
const {
notifications,
progress: showProgress,
updateProgressDetail,
success,
info,
warning,
error
} = useNotification()
const currentProgressNotificationId = ref<string | null>(null)
// Pause functionality state
const isPaused = ref(false)
const isStreaming = ref(false)
const isButtonLoading = ref(false)
// Dynamic execution state
const currentExecutionId = ref<string | null>(null)
const sentStepIds = ref<Set<string>>(new Set())
// Flag to prevent duplicate execution calls
const isExecutingNextBatch = ref(false)
/**
* 执行下一批已就绪的步骤(使用动态追加模式)
* 支持在执行过程中动态追加新步骤
*/
async function executeNextReadyBatch() {
if (isExecutingNextBatch.value) {
console.log('executeNextReadyBatch already running, skipping duplicate call')
return
}
isExecutingNextBatch.value = true
try {
const steps = collaborationProcess.value
// 收集所有已就绪但未执行的步骤
const readySteps: IRawStepTask[] = []
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
if (!step) continue
const stepId = step.Id || step.StepName || ''
const stepName = step.StepName || step.Id || ''
// 调试日志
const isReady = isStepReady(step)
const wasSent = sentStepIds.value.has(stepId)
const status = stepExecutionStatus.value[stepId]
console.log(
`[步骤检查] ${stepName}: isReady=${isReady}, wasSent=${wasSent}, status=${status}`
)
// 只收集未发送的已就绪步骤
if (isStepReady(step) && !sentStepIds.value.has(stepId)) {
if (!status || status === StepExecutionStatus.READY) {
readySteps.push(step)
sentStepIds.value.add(stepId)
console.log(`✅ 收集步骤: ${stepName}, 当前总数: ${readySteps.length}`)
}
}
}
console.log(`📊 总共收集到 ${readySteps.length} 个已就绪步骤`)
if (readySteps.length > 0) {
// 如果还没有executionId生成一个
if (!currentExecutionId.value) {
const generalGoal = agentsStore.agentRawPlan.data?.['General Goal'] || ''
const timestamp = Date.now()
currentExecutionId.value = `${generalGoal.replace(/\s+/g, '_')}_${timestamp}`
console.log('🆔 生成执行ID:', currentExecutionId.value)
}
try {
// 标记所有要执行的步骤为运行中
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.RUNNING
})
// 构建批量执行计划
const batchPlan: IRawPlanResponse = {
'General Goal': agentsStore.agentRawPlan.data?.['General Goal'] || '',
'Initial Input Object': agentsStore.agentRawPlan.data?.['Initial Input Object'] || [],
'Collaboration Process': readySteps
}
// 执行批量步骤(启用动态追加模式)
await new Promise<void>((resolve, reject) => {
api.executePlanOptimized(
batchPlan,
// onMessage: 处理每个事件
(event: StreamingEvent) => {
// 当后端开始返回数据时设置isStreaming
if (!isStreaming.value) {
isStreaming.value = true
}
// 如果正在暂停isPausing或已暂停isPaused只允许特定事件
// 这样可以确保当前正在完成的动作的结果能够被正确保存
if (isPausing.value || isPaused.value) {
if (
event.type !== 'action_complete' &&
event.type !== 'step_complete' &&
event.type !== 'error'
) {
return
}
}
switch (event.type) {
case 'step_start':
// 使用全局步骤索引计算当前步骤
const globalStepIndex = collaborationProcess.value.findIndex(
s => s.StepName === event.step_name
)
executionProgress.value = {
currentStep:
globalStepIndex >= 0 ? globalStepIndex + 1 : (event.step_index || 0) + 1,
totalSteps: collaborationProcess.value.length,
currentAction: 0,
totalActions: 0,
currentStepName: event.step_name
}
// 创建或更新进度通知,显示详细步骤信息
if (!currentProgressNotificationId.value) {
currentProgressNotificationId.value = showProgress(
'任务执行中',
executionProgress.value.currentStep,
executionProgress.value.totalSteps
)
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${executionProgress.value.currentStep}/${executionProgress.value.totalSteps}`,
`正在执行: ${event.step_name}`
)
} else {
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${executionProgress.value.currentStep}/${executionProgress.value.totalSteps}`,
`正在执行: ${event.step_name}`,
executionProgress.value.currentStep,
executionProgress.value.totalSteps
)
}
break
case 'action_complete':
const parallelInfo = event.batch_info?.is_parallel
? ` [并行 ${event.batch_info!.batch_size} 个动作]`
: ''
// 使用全局步骤索引
const globalStepIndexForAction = collaborationProcess.value.findIndex(
s => s.StepName === event.step_name
)
const stepIndexForAction =
globalStepIndexForAction >= 0
? globalStepIndexForAction + 1
: (event.step_index || 0) + 1
const totalStepsValue = collaborationProcess.value.length
executionProgress.value = {
...executionProgress.value,
currentAction: event.completed_actions,
totalActions: event.total_actions
}
// 检测是否正在暂停(等待当前动作完成)
if (isPausing.value) {
// 当前动作完成,完成暂停
isPaused.value = true
isPausing.value = false
success('已暂停', '已暂停执行,可稍后继续')
}
// 更新详细进度信息,显示动作级别进度
if (currentProgressNotificationId.value) {
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${stepIndexForAction}/${totalStepsValue}`,
`${event.step_name} - 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}`,
stepIndexForAction,
totalStepsValue
)
}
// 实时更新store
const existingStep = collaborationProcess.value.find(
s => s.StepName === event.step_name
)
if (existingStep) {
const currentResults = agentsStore.executePlan
const stepLogNode = currentResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (!stepLogNode) {
const newStepLog = {
LogNodeType: 'step',
NodeId: event.step_name,
InputName_List: existingStep.InputObject_List || [],
OutputName: existingStep.OutputObject || '',
chatLog: [],
inputObject_Record: [],
ActionHistory: [event.action_result]
}
agentsStore.setExecutePlan([...currentResults, newStepLog])
} else {
stepLogNode.ActionHistory.push(event.action_result)
agentsStore.setExecutePlan([...currentResults])
}
}
break
case 'step_complete':
stepExecutionStatus.value[event.step_name] = StepExecutionStatus.COMPLETED
// 更新完整步骤日志
const currentResults = agentsStore.executePlan
const existingLog = currentResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (existingLog) {
existingLog.ActionHistory = event.step_log_node.ActionHistory
agentsStore.setExecutePlan([...currentResults])
} else if (event.step_log_node) {
agentsStore.setExecutePlan([...currentResults, event.step_log_node])
}
// 添加object_log_node
const updatedResults = agentsStore.executePlan
if (event.object_log_node) {
agentsStore.setExecutePlan([...updatedResults, event.object_log_node])
}
break
case 'execution_complete':
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
if (stepExecutionStatus.value[stepName] !== StepExecutionStatus.COMPLETED) {
stepExecutionStatus.value[stepName] = StepExecutionStatus.COMPLETED
}
})
// 关闭进度通知并显示完成通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
success('任务执行完成', `所有步骤已执行完成`, { duration: 3000 })
resolve()
break
case 'error':
const errorMessage = event.message || event.error || '未知错误'
console.error('执行错误:', errorMessage)
// 如果正在重新执行,不显示错误通知(因为旧执行的步骤中断是预期的)
if (isRestarting.value) {
console.log(' 正在重新执行,忽略旧执行的中断错误')
resolve() // 正常完成,不显示错误
return
}
// 关闭进度通知并显示错误通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
error('执行错误', errorMessage, { duration: 5000 })
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.FAILED
})
reject(new Error(errorMessage))
break
}
},
// onError: 处理错误
(err: Error) => {
console.error('流式执行错误:', err)
// 如果正在重新执行,不显示错误通知(因为旧执行的步骤中断是预期的)
if (isRestarting.value) {
console.log(' 正在重新执行,忽略旧执行的中断错误')
resolve() // 正常完成,不显示错误
return
}
// 关闭进度通知并显示错误通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
error('执行失败', err.message, { duration: 5000 })
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.FAILED
})
reject(err)
},
// onComplete: 完成回调
() => {
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
resolve()
},
// useWebSocket: 使用WebSocket
true,
// existingKeyObjects: 已存在的KeyObjects
{},
// enableDynamic: 启用动态追加模式
true,
// onExecutionStarted: 接收执行ID
(executionId: string) => {
console.log('动态执行已启动执行ID:', executionId)
},
// executionId: 前端生成的执行ID
currentExecutionId.value || undefined
)
})
// 不再递归调用而是通过watch监听追加新步骤
} catch (err) {
error('执行失败', '批量执行失败')
loading.value = false
isPaused.value = false
isStreaming.value = false
}
} else {
// 没有更多已就绪的步骤
loading.value = false
isPaused.value = false
isStreaming.value = false
// 检查是否还有等待填充的步骤
const hasWaitingSteps = steps.some(step => step && !isStepReady(step))
if (hasWaitingSteps) {
const waitingStepNames = steps
.filter(step => step && !isStepReady(step))
.map(step => step?.StepName || '未知')
info('等待数据填充', `等待 ${waitingStepNames.length} 个步骤数据填充中...`)
} else {
success('执行完成', '所有步骤已完成')
}
}
} finally {
isExecutingNextBatch.value = false
}
}
/**
* 移除通知
*/
function removeNotification(id: string) {
const index = notifications.value.findIndex(n => n.id === id)
if (index !== -1) {
notifications.value.splice(index, 1)
}
}
// Pause/Resume handler
async function handlePauseResume() {
if (isPaused.value) {
// Resume execution - 检查是否有修改的步骤
if (agentsStore.hasModifiedSteps()) {
// 有修改的步骤,必须从最早修改的步骤重新执行
// 因为步骤之间是串行依赖的修改了步骤2会导致步骤2-6都需要重新执行
const earliestModifiedIndex = Math.min(...agentsStore.modifiedSteps)
const modifiedStepName =
collaborationProcess.value[earliestModifiedIndex]?.StepName ||
`步骤${earliestModifiedIndex + 1}`
console.log(`🔄 从步骤 ${earliestModifiedIndex + 1} (${modifiedStepName}) 重新执行`)
success('开始重新执行', `检测到步骤修改,从步骤 ${earliestModifiedIndex + 1} 重新执行`)
// 直接调用重新执行函数
await restartFromStep(earliestModifiedIndex)
return
}
// 正常恢复执行(没有修改)
try {
if (websocket.connected) {
await websocket.send('resume_execution', {
goal: agentsStore.agentRawPlan.data?.['General Goal'] || ''
})
// 只有在收到成功响应后才更新状态
isPaused.value = false
isPausing.value = false
success('已恢复', '已恢复执行')
} else {
warning('无法恢复', 'WebSocket未连接无法恢复执行')
}
} catch (error) {
error('恢复失败', '恢复执行失败')
// 恢复失败时,保持原状态不变(仍然是暂停状态)
}
} else {
// Pause execution
try {
if (websocket.connected) {
// 先设置 isPausing允许接收当前正在执行的动作的结果
isPausing.value = true
info('暂停中', '正在等待当前动作完成...')
await websocket.send('pause_execution', {
goal: agentsStore.agentRawPlan.data?.['General Goal'] || ''
})
// 注意:不立即设置 isPaused = true
// 而是等待当前动作完成后,在 action_complete 事件中设置
// 这样可以确保在动作真正完成后才显示"已暂停"
} else {
warning('无法暂停', 'WebSocket未连接无法暂停')
isPausing.value = false
}
} catch (error) {
error('暂停失败', '暂停执行失败')
// 暂停失败时,重置状态
isPausing.value = false
}
}
}
// ==================== 重新执行功能 ====================
/**
* 从RehearsalLog中提取KeyObjects
* @param rehearsalLog RehearsalLog数组
* @returns KeyObjects字典
*/
function buildKeyObjectsFromLog(rehearsalLog: any[]): Record<string, any> {
const keyObjects: Record<string, any> = {}
for (const logNode of rehearsalLog) {
if (logNode.LogNodeType === 'object' && logNode.content) {
keyObjects[logNode.NodeId] = logNode.content
}
}
return keyObjects
}
/**
* 构建截断后的 RehearsalLog使用步骤名称匹配
* @param restartFromStepIndex 重新执行的起始步骤索引例如1 表示从步骤2重新执行
* @returns 截断后的 RehearsalLog
*/
function buildTruncatedRehearsalLog(restartFromStepIndex: number) {
const steps = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
const truncatedLog: any[] = []
for (const logNode of agentsStore.executePlan) {
if (logNode.LogNodeType === 'step') {
const stepIndex = steps.findIndex((s: any) => s.StepName === logNode.NodeId)
if (stepIndex >= 0 && stepIndex < restartFromStepIndex) {
truncatedLog.push(logNode)
}
} else if (logNode.LogNodeType === 'object') {
const stepIndex = steps.findIndex((s: any) => s.OutputObject === logNode.NodeId)
if (stepIndex >= 0 && stepIndex < restartFromStepIndex) {
truncatedLog.push(logNode)
}
}
}
return truncatedLog
}
/**
* 清除执行结果中指定步骤及之后的记录
* @param fromStepIndex 起始步骤索引
*/
function clearExecutionResults(fromStepIndex: number) {
const steps = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
// 过滤掉要重新执行的步骤及其之后的所有步骤
agentsStore.executePlan = agentsStore.executePlan.filter(logNode => {
if (logNode.LogNodeType === 'step') {
// 找到该步骤在原始步骤列表中的索引
const stepIndex = steps.findIndex((s: any) => s.StepName === logNode.NodeId)
return stepIndex < fromStepIndex
}
// 对于 object 节点,也需要判断
if (logNode.LogNodeType === 'object') {
// 找到该 object 对应的步骤索引
const stepIndex = steps.findIndex((s: any) => s.OutputObject === logNode.NodeId)
return stepIndex < fromStepIndex
}
return true
})
}
/**
* 重置步骤执行状态
* @param fromStepIndex 起始步骤索引
*/
function resetStepStatus(fromStepIndex: number) {
const steps = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
for (let i = fromStepIndex; i < steps.length; i++) {
const step = steps[i]
if (!step) continue
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.READY
}
}
/**
* 从指定步骤重新执行
* @param stepIndex 要重新执行的步骤索引例如1 表示从步骤2重新执行
*/
async function restartFromStep(stepIndex: number) {
try {
loading.value = true
isRestarting.value = true // 标记正在重新执行
// 清空修改记录(避免重复执行)
agentsStore.clearModifiedSteps()
// 0. 先停止旧的执行(避免双重执行)
if (websocket.connected && currentExecutionId.value) {
try {
console.log('🛑 停止旧的执行任务,避免双重执行')
const stopResponse = await websocket.send('stop_execution', {
goal: agentsStore.agentRawPlan.data?.['General Goal'] || ''
})
console.log('✅ 后端确认停止:', stopResponse)
// 等待一下确保后端完全停止
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (err) {
console.warn('⚠️ 停止旧执行失败(可能已经停止):', err)
}
}
// 1. 构建截断后的 RehearsalLog
const truncatedLog = buildTruncatedRehearsalLog(stepIndex)
// 2. 从截断日志中提取 KeyObjects
const existingKeyObjects = buildKeyObjectsFromLog(truncatedLog)
console.log('📦 传递给后端的数据:', {
truncatedLogLength: truncatedLog.length,
keyObjectsCount: Object.keys(existingKeyObjects).length,
keyObjectsKeys: Object.keys(existingKeyObjects)
})
// 3. 清除前端执行结果中指定步骤及之后的记录
clearExecutionResults(stepIndex)
// 3.5 关闭旧的进度通知(避免通知混乱)
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
// 4. 重置步骤状态
resetStepStatus(stepIndex)
// 4.5 强制触发 Vue 响应式更新
nextTick(() => {
// 触发 UI 更新
})
// 5. 重置执行状态为执行中
isPaused.value = false
isStreaming.value = true
loading.value = true
// 6. 调用执行 API传递截断后的 RehearsalLog 和 KeyObjects
api.executePlanOptimized(
agentsStore.agentRawPlan.data!,
// onMessage - 使用内联函数处理事件
(event: StreamingEvent) => {
// 如果正在暂停isPausing或已暂停isPaused只允许特定事件
// 这样可以确保当前正在完成的动作的结果能够被正确保存
if (isPausing.value || isPaused.value) {
if (
event.type !== 'action_complete' &&
event.type !== 'step_complete' &&
event.type !== 'error'
) {
return
}
}
switch (event.type) {
case 'step_start':
stepExecutionStatus.value[event.step_name] = StepExecutionStatus.RUNNING
// 使用全局步骤索引计算当前步骤
const globalStepIndex = collaborationProcess.value.findIndex(
s => s.StepName === event.step_name
)
const currentStepNumber =
globalStepIndex >= 0 ? globalStepIndex + 1 : (event.step_index || 0) + 1
// 创建或更新进度通知
if (!currentProgressNotificationId.value) {
currentProgressNotificationId.value = showProgress(
'重新执行中',
currentStepNumber,
collaborationProcess.value.length
)
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${currentStepNumber}/${collaborationProcess.value.length}`,
`正在执行: ${event.step_name}`
)
} else {
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${currentStepNumber}/${collaborationProcess.value.length}`,
`正在执行: ${event.step_name}`,
currentStepNumber,
collaborationProcess.value.length
)
}
break
case 'action_complete':
// 检测是否正在暂停(等待当前动作完成)
if (isPausing.value) {
// 当前动作完成,完成暂停
isPaused.value = true
isPausing.value = false
success('已暂停', '已暂停执行,可稍后继续')
}
// 实时更新store
const existingStep = collaborationProcess.value.find(
s => s.StepName === event.step_name
)
if (existingStep) {
const currentResults = agentsStore.executePlan
const stepLogNode = currentResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (!stepLogNode) {
const newStepLog = {
LogNodeType: 'step',
NodeId: event.step_name,
InputName_List: existingStep.InputObject_List || [],
OutputName: existingStep.OutputObject || '',
chatLog: [],
inputObject_Record: [],
ActionHistory: [event.action_result]
}
agentsStore.setExecutePlan([...currentResults, newStepLog])
} else {
stepLogNode.ActionHistory.push(event.action_result)
agentsStore.setExecutePlan([...currentResults])
}
}
// 使用全局步骤索引
const globalStepIndexForAction = collaborationProcess.value.findIndex(
s => s.StepName === event.step_name
)
const stepNumberForAction =
globalStepIndexForAction >= 0
? globalStepIndexForAction + 1
: (event.step_index || 0) + 1
// 更新进度通知
if (currentProgressNotificationId.value) {
const parallelInfo = event.batch_info?.is_parallel
? ` [并行 ${event.batch_info!.batch_size} 个动作]`
: ''
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${stepNumberForAction}/${collaborationProcess.value.length}`,
`${event.step_name} - 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}`,
stepNumberForAction,
collaborationProcess.value.length
)
}
break
case 'step_complete':
stepExecutionStatus.value[event.step_name] = StepExecutionStatus.COMPLETED
// 更新完整步骤日志
const currentResults = agentsStore.executePlan
const existingLog = currentResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (existingLog) {
existingLog.ActionHistory = event.step_log_node.ActionHistory
agentsStore.setExecutePlan([...currentResults])
} else if (event.step_log_node) {
agentsStore.setExecutePlan([...currentResults, event.step_log_node])
}
// 添加object_log_node
const updatedResults = agentsStore.executePlan
if (event.object_log_node) {
agentsStore.setExecutePlan([...updatedResults, event.object_log_node])
}
break
case 'execution_complete':
const steps = collaborationProcess.value
steps.forEach(step => {
const stepName = step.StepName || step.Id || ''
if (stepExecutionStatus.value[stepName] !== StepExecutionStatus.COMPLETED) {
stepExecutionStatus.value[stepName] = StepExecutionStatus.COMPLETED
}
})
// 关闭进度通知并显示完成通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
success('重新执行完成', `所有步骤已执行完成`, { duration: 3000 })
loading.value = false
isPaused.value = false
isStreaming.value = false
break
case 'error':
const errorMessage = event.message || '未知错误'
console.error('重新执行错误:', errorMessage)
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
// 静默处理所有错误,不显示任何通知
loading.value = false
isPaused.value = false
isStreaming.value = false
isRestarting.value = false // 重新执行结束
break
}
},
// onError
(err: Error) => {
console.error('重新执行错误:', err)
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
// 静默处理所有错误,不显示任何通知
loading.value = false
isPaused.value = false
isStreaming.value = false
isRestarting.value = false // 重新执行结束
},
// onComplete
() => {
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
loading.value = false
isPaused.value = false
isStreaming.value = false
isRestarting.value = false // 重新执行结束
},
true, // useWebSocket
existingKeyObjects, // ✅ 传递从截断日志中提取的KeyObjects
true, // enableDynamic
(executionId: string) => {
currentExecutionId.value = executionId
},
undefined, // executionId
stepIndex, // restartFromStepIndex
truncatedLog // ✅ 传递截断后的 RehearsalLog
)
success('重新执行', `正在从步骤 ${stepIndex + 1} 重新执行...`)
} catch (err) {
console.error('重新执行失败:', err)
error('重新执行失败', '无法启动重新执行')
loading.value = false
isRestarting.value = false // 重新执行失败,也要重置标记
}
}
// Handle execute button click
async function handleExecuteButtonClick() {
// If streaming, show pause/resume functionality
if (isStreaming.value) {
await handlePauseResume()
return
}
// Otherwise, execute normal task execution logic
await handleRun()
}
async function handleRun() {
// Check if there are ready steps
const readySteps = stepsReadyStatus.value.ready
const waitingSteps = stepsReadyStatus.value.waiting
if (readySteps.length === 0 && waitingSteps.length > 0) {
warning(
'步骤数据未就绪',
`${waitingSteps.length} 个步骤的数据还在填充中:${waitingSteps.join(
'、'
)}。建议等待数据填充完成后再执行。`,
{ duration: 5000 }
)
return
}
// Set button brief loading state (prevent double-click)
isButtonLoading.value = true
setTimeout(() => {
isButtonLoading.value = false
}, 1000)
// Reset pause and streaming state
isPaused.value = false
isStreaming.value = false
// Start execution
loading.value = true
// Clear previous execution results and status
agentsStore.setExecutePlan([])
stepExecutionStatus.value = {}
sentStepIds.value.clear()
currentExecutionId.value = null
// Start batch executing first batch of ready steps
await executeNextReadyBatch()
}
// View task process
async function handleTaskProcess() {
drawerVisible.value = true
}
// Reset execution results
function handleRefresh() {
agentsStore.setExecutePlan([])
}
// Add scroll state indicator
const isScrolling = ref(false)
let scrollTimer: ReturnType<typeof setTimeout> | null = null
// Modify scroll handler
function handleScroll() {
isScrolling.value = true
emit('refreshLine')
// 清除之前的定时器
if (scrollTimer) {
clearTimeout(scrollTimer)
}
jsplumb.repaintEverything()
// 设置滚动结束检测
scrollTimer = setTimeout(() => {
isScrolling.value = false
}, 300) as ReturnType<typeof setTimeout>
}
// Modify mouse event handler
const handleMouseEnter = throttle(id => {
if (!isScrolling.value) {
createInternalLine(id)
}
}, 0)
const handleMouseLeave = throttle(() => {
if (!isScrolling.value) {
createInternalLine()
}
}, 0)
function clear() {
jsplumb.reset()
}
// Encapsulate line redraw method
const redrawInternalLines = (highlightId?: string) => {
// Waiting DOM 更新完成
nextTick(() => {
// 清除旧连线
jsplumb.reset()
// Waiting DOM 稳定后重新绘制
setTimeout(() => {
createInternalLine(highlightId)
}, 100)
})
}
// Watch collaborationProcess changes, auto redraw connections
watch(
() => collaborationProcess,
() => {
redrawInternalLines()
},
{ deep: true }
)
// Initialize connections after component mount
onMounted(() => {
// 初始化时绘制连线
nextTick(() => {
setTimeout(() => {
createInternalLine()
}, 100)
})
})
// Button interaction state management
const buttonHoverState = ref<'process' | 'execute' | 'refresh' | null>(null)
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
const handleProcessMouseEnter = () => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
buttonHoverTimer = null
}
buttonHoverState.value = 'process'
}
const handleExecuteMouseEnter = () => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
buttonHoverTimer = null
}
if (agentsStore.agentRawPlan.data) {
buttonHoverState.value = 'execute'
}
}
const handleRefreshMouseEnter = () => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
buttonHoverTimer = null
}
if (agentsStore.executePlan.length > 0) {
buttonHoverState.value = 'refresh'
}
}
const handleButtonMouseLeave = () => {
// 添加防抖,防止快速切换时的抖动
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
buttonHoverTimer = setTimeout(() => {
buttonHoverState.value = null
}, 50) // 适当减少延迟时间
}
// Cleanup when leaving component
onUnmounted(() => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
})
// Calculate button class names
const processBtnClass = computed(() => {
if (buttonHoverState.value === 'refresh' || buttonHoverState.value === 'execute') {
return 'circle'
}
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
})
const executeBtnClass = computed(() => {
if (buttonHoverState.value === 'process' || buttonHoverState.value === 'refresh') {
return 'circle'
}
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
})
const refreshBtnClass = computed(() => {
if (buttonHoverState.value === 'process' || buttonHoverState.value === 'execute') {
return 'circle'
}
return agentsStore.executePlan.length > 0 ? 'ellipse' : 'circle'
})
// Calculate whether to show button text
const showProcessText = computed(() => {
return buttonHoverState.value === 'process'
})
const showExecuteText = computed(() => {
if (buttonHoverState.value === 'process') return false
return agentsStore.agentRawPlan.data
})
const showRefreshText = computed(() => {
return buttonHoverState.value === 'refresh'
})
// Calculate button titles
const processBtnTitle = computed(() => {
return buttonHoverState.value === 'process' ? '查看任务流程' : '点击查看任务流程'
})
const executeBtnTitle = computed(() => {
return showExecuteText.value ? '任务执行' : '点击执行任务'
})
const refreshBtnTitle = computed(() => {
return showRefreshText.value ? '重置结果' : '点击重置执行状态'
})
defineExpose({
createInternalLine,
clear
})
</script>
<template>
<div
class="h-full flex flex-col relative"
id="task-results"
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
>
<!-- Notification 通知系统 -->
<Notification :notifications="notifications" @close="id => removeNotification(id)" />
<!-- 标题与执行按钮 -->
<div class="text-[18px] font-bold mb-[7px] flex justify-between items-center px-[20px]">
<span class="text-[var(--color-text-title-header)]">执行结果</span>
<div
class="flex items-center justify-end gap-[10px] task-button-group w-[230px]"
@mouseleave="handleButtonMouseLeave"
>
<!-- 刷新按钮 -->
<el-button
:class="refreshBtnClass"
:color="variables.tertiary"
:title="refreshBtnTitle"
:disabled="agentsStore.executePlan.length === 0"
@mouseenter="handleRefreshMouseEnter"
@click="handleRefresh"
style="order: 0"
>
<svg-icon icon-class="refresh" />
<span v-if="showRefreshText" class="btn-text">重置</span>
</el-button>
<!-- Task Process按钮 -->
<el-button
:class="processBtnClass"
:color="variables.tertiary"
:title="processBtnTitle"
@mouseenter="handleProcessMouseEnter"
@click="handleTaskProcess"
style="order: 1"
>
<svg-icon icon-class="process" />
<span v-if="showProcessText" class="btn-text">任务过程</span>
</el-button>
<!-- Execute按钮 -->
<el-popover
:disabled="Boolean(agentsStore.agentRawPlan.data)"
title="请先输入任务再执行"
:visible="showPopover"
@hide="showPopover = false"
style="order: 2"
>
<template #reference>
<el-button
:class="executeBtnClass"
:color="variables.tertiary"
:title="isStreaming ? (isPaused ? '点击继续执行' : '点击暂停执行') : executeBtnTitle"
:disabled="
!agentsStore.agentRawPlan.data || (!isStreaming && loading) || isButtonLoading
"
@mouseenter="handleExecuteMouseEnter"
@click="handleExecuteButtonClick"
>
<!-- 按钮短暂加载状态防止双击 -->
<svg-icon v-if="isButtonLoading" icon-class="loading" class="animate-spin" />
<!-- 执行中加载状态已废弃保留以防万一 -->
<svg-icon
v-else-if="loading && !isStreaming"
icon-class="loading"
class="animate-spin"
/>
<!-- 流式传输中且正在暂停等待当前动作完成显示Loading图标 -->
<svg-icon
v-else-if="isStreaming && isPausing"
icon-class="loading"
size="20px"
class="btn-icon animate-spin"
/>
<!-- 流式传输中且未暂停显示Pause图标 -->
<svg-icon
v-else-if="isStreaming && !isPaused && !isPausing"
icon-class="Pause"
size="20px"
class="btn-icon"
/>
<!-- 流式传输中且已暂停显示播放/Resume图标 -->
<svg-icon
v-else-if="isStreaming && isPaused"
icon-class="video-play"
size="20px"
class="btn-icon"
/>
<!-- 默认状态显示 action 图标 -->
<svg-icon v-else icon-class="action" />
<span v-if="showExecuteText && !isStreaming" class="btn-text">任务执行</span>
<span v-else-if="isStreaming && isPaused" class="btn-text">继续执行</span>
<span v-else-if="isStreaming && isPausing" class="btn-text">暂停中...</span>
<span v-else-if="isStreaming" class="btn-text">暂停执行</span>
</el-button>
</template>
</el-popover>
<el-drawer
v-model="drawerVisible"
title="任务过程"
direction="rtl"
size="30%"
:destroy-on-close="false"
>
<!-- 头部工具栏 -->
<template #header>
<div class="drawer-header">
<span class="title">任务过程</span>
<!-- <el-button v-if="!editMode" text icon="Edit" @click="editMode = true" />
<el-button v-else text icon="Check" @click="save" /> -->
</div>
</template>
<el-scrollbar height="calc(100vh - 120px)">
<el-empty v-if="!collaborationProcess.length" description="暂无任务过程" />
<div v-else class="process-list">
<!-- 使用ProcessCard组件显示每个AgentSelection -->
<ProcessCard
v-for="step in collaborationProcess"
:key="step.Id"
:step="step"
@open-edit="handleOpenEdit"
@save-edit="handleSaveEdit"
/>
</div>
</el-scrollbar>
</el-drawer>
</div>
</div>
<!-- 内容 -->
<div
v-loading="agentsStore.agentRawPlan.loading"
class="flex-1 overflow-auto relative ml-[20px] mr-[20px]"
@scroll="handleScroll"
>
<div id="task-results-main" class="px-[40px] relative">
<!-- 额外产物卡片 -->
<div
v-if="agentsStore.additionalOutputs && agentsStore.additionalOutputs.length > 0"
class="mt-6"
:key="`additional-outputs-${agentsStore.additionalOutputs.length}`"
>
<div class="space-y-4 mb-4">
<AdditionalOutputCard
v-for="(_, index) in agentsStore.additionalOutputs"
:key="`additional-${index}-${agentsStore.additionalOutputs[index]}`"
:index="index"
/>
</div>
</div>
<!-- 原有的流程和产物 -->
<div v-for="item in collaborationProcess" :key="item.Id" class="card-item">
<el-card
class="card-item w-full relative"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-results-${item.Id}-0`"
@click="emit('setCurrentTask', item)"
>
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
<!-- 折叠面板 -->
<el-collapse @change="handleCollapse">
<el-collapse-item
v-for="item1 in item.TaskProcess"
:key="`task-results-${item.Id}-${item1.ID}`"
:name="`task-results-${item.Id}-${item1.ID}`"
:disabled="!hasActionResult(item, item1.ID)"
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
@mouseleave="handleMouseLeave"
>
<!-- 执行中且没有结果时显示 loading 图标暂停后不显示 -->
<template v-if="loading && !hasActionResult(item, item1.ID) && !isPaused" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<!-- 暂停状态且没有结果时显示空白 -->
<template v-else-if="isPaused && !hasActionResult(item, item1.ID)" #icon>
<span></span>
</template>
<!-- 没有执行计划时隐藏图标 -->
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<!-- 有结果时不提供 #icon Element Plus 显示默认箭头 -->
<template #title>
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
<div
class="flex items-center gap-[15px] rounded-[20px]"
:class="{
'bg-[var(--color-bg-detail-list)]': !hasActionResult(item, item1.ID),
'bg-[var(--color-bg-detail-list-run)]': hasActionResult(item, item1.ID)
}"
>
<!-- 右侧链接点 -->
<div
class="absolute right-0 top-1/2 transform -translate-y-1/2"
:id="`task-results-${item.Id}-0-${item1.ID}`"
></div>
<div
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(item1.AgentName).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(item1.AgentName).icon"
color="#fff"
size="24px"
/>
</div>
<div class="text-[16px]">
<span
:class="{
'text-[var(--color-text-result-detail)]': !hasActionResult(
item,
item1.ID
),
'text-[var(--color-text-result-detail-run)]': hasActionResult(
item,
item1.ID
)
}"
>{{ item1.AgentName }}:&nbsp; &nbsp;</span
>
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
{{ getActionTypeDisplay(item1.ActionType)?.name }}
</span>
</div>
</div>
</template>
<ExecutePlan
:action-id="item1.ID"
:node-id="item.StepName"
:execute-plans="agentsStore.executePlan"
/>
</el-collapse-item>
</el-collapse>
</el-card>
<el-card
class="card-item w-full relative output-object-card"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-results-${item.Id}-1`"
@click="emit('setCurrentTask', item)"
>
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
<el-collapse @change="handleCollapse" :key="agentsStore.executePlan.length">
<el-collapse-item
class="output-object"
:disabled="!hasObjectResult(item.OutputObject)"
>
<!-- 执行中且没有结果时显示 loading 图标暂停后不显示 -->
<template v-if="loading && !hasObjectResult(item.OutputObject) && !isPaused" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<!-- 暂停状态且没有结果时显示空白 -->
<template v-else-if="isPaused && !hasObjectResult(item.OutputObject)" #icon>
<span></span>
</template>
<!-- 没有执行计划时隐藏图标 -->
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<!-- 有结果时不提供 #icon Element Plus 显示默认箭头 -->
<template #title>
<div
class="text-[18px]"
:class="{
'text-[var(--color-text-result-detail)]': !hasObjectResult(item.OutputObject),
'text-[var(--color-text-result-detail-run)]': hasObjectResult(
item.OutputObject
)
}"
>
{{ item.OutputObject }}
</div>
</template>
<ExecutePlan
:node-id="item.OutputObject"
:execute-plans="agentsStore.executePlan"
/>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
#task-results.is-running {
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
}
#task-results {
:deep(.el-collapse) {
border: none;
border-radius: 20px;
.el-collapse-item + .el-collapse-item {
margin-top: 10px;
}
.el-collapse-item__header {
border: none;
background: var(--color-bg-detail-list-run);
min-height: 41px;
line-height: 41px;
border-radius: 20px;
transition: border-radius 1ms;
position: relative;
.el-collapse-item__title {
background: var(--color-bg-detail-list);
border-radius: 20px;
}
.el-icon {
font-size: 20px;
font-weight: 900;
background: var(--color-bg-icon-rotate);
border-radius: 50px;
color: #d8d8d8;
}
&.is-active {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
.output-object {
.el-collapse-item__header {
background: none;
.el-collapse-item__title {
background: none;
}
}
.el-collapse-item__wrap {
background: none;
.card-item {
background: var(--color-bg-detail);
padding: 5px;
padding-top: 10px;
border-radius: 7px;
}
}
}
.el-collapse-item__wrap {
border: none;
background: var(--color-bg-detail-list);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
}
:deep(.el-card) {
.el-card__body {
padding-right: 40px;
background-color: var(--color-bg-detail);
&:hover {
background-color: var(--color-card-bg-result-hover);
}
}
}
.output-object-card {
:deep(.el-card__body) {
padding-top: 0;
padding-bottom: 0;
padding-right: 0;
}
}
.active-card {
background: linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box;
}
.card-item + .card-item {
margin-top: 10px;
}
.additional-output-card {
border: 1px dashed #dcdfe6;
opacity: 0.9;
box-shadow: var(--color-agent-list-hover-shadow);
&:hover {
border-color: #409eff;
opacity: 1;
}
:deep(.el-card__body) {
padding: 20px;
}
// 编辑区域样式调整
.el-collapse {
border: none;
.el-collapse-item {
.el-collapse-item__header {
background: var(--color-bg-detail);
min-height: 36px;
line-height: 36px;
border-radius: 8px;
.el-collapse-item__title {
background: transparent;
font-size: 14px;
padding-left: 0;
}
.el-icon {
font-size: 16px;
}
}
.el-collapse-item__wrap {
background: var(--color-bg-detail);
border-radius: 0 0 8px 8px;
}
}
}
}
//按钮交互样式
.task-button-group {
display: flex;
flex-direction: row-reverse;
.el-button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
transition: width 0.2s ease-out, padding 0.2s ease-out, border-radius 0.2s ease-out,
transform 0.2s ease-out, box-shadow 0.2s ease-out, filter 0.2s ease-out !important;
overflow: hidden !important;
white-space: nowrap !important;
border: 1px solid transparent !important;
border-color: transparent !important;
color: var(--color-text-primary) !important;
position: relative;
background-color: var(--color-bg-tertiary) !important;
gap: 0px !important;
outline: none !important;
box-shadow: none !important;
-webkit-tap-highlight-color: transparent !important;
backface-visibility: hidden !important;
-webkit-backface-visibility: hidden !important;
transform: translateZ(0) !important;
will-change: transform, width, padding, border-radius !important;
&::before,
&::after {
display: none !important;
}
&:hover {
transform: translateY(-2px) translateZ(0) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
filter: brightness(1.1) !important;
border-color: transparent !important;
}
&.is-disabled {
opacity: 0.5;
cursor: not-allowed !important;
&:hover {
transform: none !important;
box-shadow: none !important;
filter: none !important;
}
}
}
// Circle state
.circle {
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
max-width: 40px !important;
padding: 0 !important;
border-radius: 50% !important;
.btn-text {
display: none !important;
}
}
// Ellipse state
.ellipse {
height: 40px !important;
border-radius: 20px !important;
padding: 0 16px !important;
gap: 8px !important;
// Task process button - fixed left, expand right
&:nth-child(1) {
justify-content: flex-start !important;
.btn-text {
display: inline-block !important;
font-size: 14px;
font-weight: 500;
margin-right: 8px;
margin-left: 0;
opacity: 1;
animation: fadeInLeft 0.3s ease forwards;
}
}
// Task execution button - fixed right, expand left
&:nth-child(2) {
justify-content: flex-end !important;
.btn-text {
display: inline-block !important;
font-size: 14px;
font-weight: 500;
margin-left: 8px;
margin-right: 0;
opacity: 1;
animation: fadeInRight 0.3s ease forwards;
}
}
// .btn-text {
// display: inline-block !important;
// font-size: 14px;
// font-weight: 500;
// margin-left: 4px;
// opacity: 1;
// animation: fadeIn 0.3s ease forwards;
// }
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 按钮图标间距
.btn-icon {
margin-right: 8px !important;
}
}
}
</style>