Files
AgentCoord/frontend/src/layout/components/Main/TaskTemplate/TaskResult/index.vue

1816 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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 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'
// 定义组件 props
const props = defineProps<{
TaskID?: string // 任务唯一标识,用于写入数据库
}>()
// 定义组件事件
const emit = defineEmits<{
(e: 'refreshLine'): void
(e: 'setCurrentTask', task: IRawStepTask): void
}>()
const agentsStore = useAgentsStore()
const drawerVisible = ref(false)
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 步骤执行状态枚举
enum StepExecutionStatus {
WAITING = 'waiting', // 等待数据
READY = 'ready', // 准备执行
RUNNING = 'running', // 正在执行
COMPLETED = 'completed', // 执行完成
FAILED = 'failed' // 执行失败
}
// 每个步骤的执行状态
const stepExecutionStatus = ref<Record<string, StepExecutionStatus>>({})
const isPausing = ref(false) // 正在请求暂停
const isRestarting = ref(false) // 正在重新执行
// 检查步骤是否准备好执行
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')
}
// 获取折叠面板图标状态
type IconStatus = 'loading' | 'empty' | 'hidden' | 'default'
const getCollapseIconStatus = (hasResult: boolean): IconStatus => {
if (loading.value && !hasResult && !isPaused.value) {
return 'loading'
}
if (isPaused && !hasResult) {
return 'empty'
}
if (!agentsStore.executePlan.length) {
return 'hidden'
}
return 'default'
}
// 计算所有步骤的准备状态
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)) {
sentStepIds.value.add(stepId)
// 异步追加步骤到后端执行队列
api
.addStepsToExecution(currentExecutionId.value, [step])
.then(addedCount => {
if (addedCount > 0) {
const totalStepsCount = collaborationProcess.value.length
executionProgress.value.totalSteps = totalStepsCount
} else {
sentStepIds.value.delete(stepId)
}
})
.catch(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 }
)
// 弹窗提示(执行按钮用)
const showPopover = ref(false)
// 处理保存编辑(比较新旧值,记录修改步骤)
function handleSaveEdit(stepId: string, processId: string, value: string) {
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
const stepIndex = collaborationProcess.value.findIndex(s => s.Id === stepId)
if (stepIndex >= 0) {
agentsStore.addModifiedStep(stepIndex)
}
}
}
}
}
const jsplumb = new Jsplumb('task-results-main', {
connector: {
type: BezierConnector.type,
options: { curviness: 30, stub: 10 }
}
})
// 折叠面板时实时刷新连线
let timer: ReturnType<typeof setInterval> | null = null
function handleCollapse() {
if (timer) {
clearInterval(timer)
}
timer = setInterval(() => {
jsplumb.repaintEverything()
emit('refreshLine')
}, 1) as ReturnType<typeof setInterval>
// 默认3秒后完全展开
const timer1 = setTimeout(() => {
if (timer) {
clearInterval(timer)
timer = null
}
}, 3000)
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
if (timer1) {
clearInterval(timer1)
}
})
}
// 创建内部连线
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: ''
})
// 通知系统
const {
notifications,
progress: showProgress,
updateProgressDetail,
updateNotificationTitle,
success,
removeNotification,
info,
warning,
error
} = useNotification()
const currentProgressNotificationId = ref<string | null>(null)
const currentProgressTitle = ref('任务执行中') //追踪当前进度通知的标题
// 更新进度通知的标题
function updateProgressNotificationTitle(title: string) {
currentProgressTitle.value = title
if (currentProgressNotificationId.value) {
updateNotificationTitle(currentProgressNotificationId.value, title)
}
}
// 暂停功能状态
const isPaused = ref(false)
const isStreaming = ref(false)
const isButtonLoading = ref(false)
// 动态执行状态
const currentExecutionId = ref<string | null>(null)
const sentStepIds = ref<Set<string>>(new Set())
// 防止重复执行调用的标记
const isExecutingNextBatch = ref(false)
// 收集所有已就绪但未执行的步骤
function collectReadySteps(): IRawStepTask[] {
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 status = stepExecutionStatus.value[stepId]
if (isStepReady(step) && !sentStepIds.value.has(stepId)) {
if (!status || status === StepExecutionStatus.READY) {
readySteps.push(step)
sentStepIds.value.add(stepId)
}
}
}
return readySteps
}
//确保有 executionId不存在则生成
function ensureExecutionId() {
if (!currentExecutionId.value) {
const generalGoal = agentsStore.agentRawPlan.data?.['General Goal'] || ''
const timestamp = Date.now()
currentExecutionId.value = `${generalGoal.replace(/\s+/g, '_')}_${timestamp}`
}
}
//标记步骤为运行中状态
function markStepsRunning(readySteps: IRawStepTask[]) {
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.RUNNING
})
}
//构建批量执行计划
function buildBatchPlan(readySteps: IRawStepTask[]): IRawPlanResponse {
return {
'General Goal': agentsStore.agentRawPlan.data?.['General Goal'] || '',
'Initial Input Object': agentsStore.agentRawPlan.data?.['Initial Input Object'] || [],
'Collaboration Process': readySteps
}
}
//执行批量步骤
async function executeBatchSteps(readySteps: IRawStepTask[]) {
const batchPlan = buildBatchPlan(readySteps)
await new Promise<void>((resolve, reject) => {
const callbacks = createExecutionCallbacks({
title: '任务执行中',
useProgress: true,
onCompleteCallback: () => resolve()
})
api.executePlanOptimized(
batchPlan,
callbacks.onMessage,
(err: Error) => {
callbacks.onError(err)
const errorMessage = err.message || '未知错误'
if (isRestarting.value) {
resolve()
return
}
error('执行错误', errorMessage, { duration: 5000 })
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.FAILED
})
reject(err)
},
() => callbacks.onComplete(),
true,
{},
true,
(executionId: string) => {
console.log('动态执行已启动执行ID:', executionId)
isStreaming.value = true
currentExecutionId.value = executionId
},
currentExecutionId.value || undefined,
undefined,
undefined,
props.TaskID || undefined
)
})
}
//处理没有已就绪步骤的情况
function handleNoReadySteps() {
loading.value = false
isPaused.value = false
isStreaming.value = false
const steps = collaborationProcess.value
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('执行完成', '所有步骤已完成')
}
}
//执行下一批已就绪的步骤(使用动态追加模式)支持在执行过程中动态追加新步骤
async function executeNextReadyBatch() {
if (isExecutingNextBatch.value) {
return
}
isExecutingNextBatch.value = true
try {
const readySteps = collectReadySteps()
if (readySteps.length > 0) {
try {
ensureExecutionId()
markStepsRunning(readySteps)
await executeBatchSteps(readySteps)
} catch (err) {
error('执行失败', '批量执行失败')
loading.value = false
isPaused.value = false
isStreaming.value = false
}
} else {
handleNoReadySteps()
}
} finally {
isExecutingNextBatch.value = false
}
}
// 暂停/继续处理函数
async function handlePauseResume() {
if (isPaused.value) {
// 恢复执行 - 检查是否有修改的步骤
if (agentsStore.hasModifiedSteps()) {
// 有修改的步骤,必须从最早修改的步骤重新执行
const earliestModifiedIndex = Math.min(...agentsStore.modifiedSteps)
success('开始重新执行', `检测到步骤修改,从步骤 ${earliestModifiedIndex + 1} 重新执行`)
await restartFromStep(earliestModifiedIndex)
return
}
// 正常恢复执行
try {
if (websocket.connected) {
// 检查 execution_id 是否存在
if (!currentExecutionId.value) {
warning('无法恢复', '执行ID不存在请等待执行开始')
return
}
await websocket.send('resume_execution', {
execution_id: currentExecutionId.value
})
isPaused.value = false
isPausing.value = false
updateProgressNotificationTitle('任务执行中')
success('已恢复', '已恢复执行')
} else {
warning('无法恢复', 'WebSocket未连接无法恢复执行')
}
} catch (err) {
error('恢复失败', '恢复执行失败')
}
} else {
// 暂停执行
try {
if (websocket.connected) {
// 检查 execution_id 是否存在
if (!currentExecutionId.value) {
warning('无法暂停', '执行ID不存在请等待执行开始')
isPausing.value = false
return
}
// 先设置 isPausing允许接收当前正在执行的动作的结果
isPausing.value = true
info('暂停中', '正在等待当前动作完成')
await websocket.send('pause_execution', {
execution_id: currentExecutionId.value
})
/*不立即设置 isPaused = true
*而是等待当前动作完成后,在 action_complete 事件中设置
*这样可以确保在动作真正完成后才显示"已暂停"
**/
} else {
warning('无法暂停', 'WebSocket未连接无法暂停')
isPausing.value = false
}
} catch (err) {
error('暂停失败', '暂停执行失败')
isPausing.value = false
}
}
}
/**
* 创建执行回调函数
* @param options 配置选项
* @returns 包含 onMessage, onError, onComplete 的对象
*/
function createExecutionCallbacks(options: {
title?: string
isRestart?: boolean
onCompleteCallback?: () => void
useProgress?: boolean // 是否使用进度追踪模式
}) {
const {
title = '任务执行中',
isRestart = false,
onCompleteCallback,
useProgress = false
} = options
const handleStepStart = (event: any) => {
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
// 创建或更新进度通知(根据状态显示不同标题)
const progressTitle = isPausing.value ? '暂停中' : title
updateProgressNotificationTitle(progressTitle)
if (!currentProgressNotificationId.value) {
currentProgressNotificationId.value = showProgress(
progressTitle,
currentStepNumber,
collaborationProcess.value.length
)
}
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${currentStepNumber}/${collaborationProcess.value.length}`,
`正在执行: ${event.step_name}`,
currentStepNumber,
collaborationProcess.value.length
)
}
// 用于 executeNextReadyBatch 的特殊处理函数
const handleStepStartWithProgress = (event: any) => {
// 当后端开始返回数据时,设置 isStreaming
if (!isStreaming.value) {
isStreaming.value = true
}
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
}
handleStepStart(event)
}
const handleActionCompleteWithProgress = (event: any) => {
executionProgress.value = {
...executionProgress.value,
currentAction: event.completed_actions,
totalActions: event.total_actions
}
handleActionComplete(event)
}
const handleActionComplete = (event: any) => {
// 检测是否正在暂停(等待当前动作完成)
if (isPausing.value) {
isPaused.value = true
isPausing.value = false
updateProgressNotificationTitle('已暂停')
}
// 实时更新 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
const totalStepsValue = collaborationProcess.value.length
// 更新进度通知
if (currentProgressNotificationId.value) {
const parallelInfo = event.batch_info?.is_parallel
? ` [并行 ${event.batch_info!.batch_size} 个动作]`
: ''
updateProgressDetail(
currentProgressNotificationId.value,
`步骤 ${stepNumberForAction}/${totalStepsValue}`,
`${event.step_name} - 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}`,
stepNumberForAction,
totalStepsValue
)
}
}
const handleStepComplete = (event: any) => {
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])
}
}
const handleExecutionComplete = () => {
// 标记所有步骤为完成
collaborationProcess.value.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
}
// 重置标题
currentProgressTitle.value = '任务执行中'
success(isRestart ? '重新执行完成' : '任务执行完成', '所有步骤已执行完成', { duration: 3000 })
loading.value = false
isPaused.value = false
isStreaming.value = false
if (isRestart) {
isRestarting.value = false
}
onCompleteCallback?.()
}
const handleError = (err: Error) => {
console.error(isRestart ? '重新执行错误:' : '流式执行错误:', err)
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
// 静默处理所有错误,不显示任何通知
loading.value = false
isPaused.value = false
isStreaming.value = false
if (isRestart) {
isRestarting.value = false
}
}
const 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':
if (useProgress) {
handleStepStartWithProgress(event as any)
} else {
handleStepStart(event as any)
}
break
case 'action_complete':
if (useProgress) {
handleActionCompleteWithProgress(event as any)
} else {
handleActionComplete(event as any)
}
break
case 'step_complete':
handleStepComplete(event as any)
break
case 'execution_complete':
handleExecutionComplete()
break
case 'error':
// 错误处理由 onError 回调处理
break
}
}
const onError = (err: Error) => {
handleError(err)
}
const onComplete = () => {
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
loading.value = false
isPaused.value = false
isStreaming.value = false
if (isRestart) {
isRestarting.value = false
}
onCompleteCallback?.()
}
return { onMessage, onError, onComplete }
}
/**
* 从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()
// 保存旧的 execution_id 用于停止
const oldExecutionId = currentExecutionId.value
// 停止旧的执行
if (websocket.connected && oldExecutionId) {
try {
await websocket.send('stop_execution', {
execution_id: oldExecutionId
})
// 等待一下确保后端完全停止
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (err) {
console.warn('⚠️ 停止旧执行失败(可能已经停止):', err)
}
}
// 前端生成新的 execution_id确保前端和后端使用同一个 ID
const generalGoal = agentsStore.agentRawPlan.data?.['General Goal'] || ''
const newExecutionId = `${generalGoal.replace(/\s+/g, '_')}_${Date.now()}`
currentExecutionId.value = newExecutionId
console.log('🔄 [DEBUG] restartFromStep: 生成新的 execution_id =', newExecutionId)
// 构建截断后的 RehearsalLog
const truncatedLog = buildTruncatedRehearsalLog(stepIndex)
//从截断日志中提取 KeyObjects
const existingKeyObjects = buildKeyObjectsFromLog(truncatedLog)
clearExecutionResults(stepIndex)
// 关闭旧的进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
//重置步骤状态
resetStepStatus(stepIndex)
// 强制触发 Vue 响应式更新
nextTick(() => {})
//重置执行状态为执行中
isPaused.value = false
isStreaming.value = true
loading.value = true
// 调用执行 API传递截断后的 RehearsalLog 和 KeyObjects
const callbacks = createExecutionCallbacks({
title: '重新执行中',
isRestart: true
})
api.executePlanOptimized(
agentsStore.agentRawPlan.data!,
callbacks.onMessage,
callbacks.onError,
callbacks.onComplete,
true,
existingKeyObjects,
true,
(executionId: string) => {
isStreaming.value = true
currentExecutionId.value = executionId
},
newExecutionId, // 传入前端生成的 execution_id
stepIndex,
truncatedLog,
props.TaskID || undefined // 传入 TaskID 用于更新数据库
)
success('重新执行', `正在从步骤 ${stepIndex + 1} 重新执行...`)
} catch (err) {
error('重新执行失败', '无法启动重新执行')
loading.value = false
isRestarting.value = false
}
}
// 处理执行按钮点击
async function handleExecuteButtonClick() {
if (isStreaming.value) {
await handlePauseResume()
return
}
await handleRun()
}
async function handleRun() {
// 检查是否有已就绪的步骤
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
}
// 设置按钮短暂加载状态
isButtonLoading.value = true
setTimeout(() => {
isButtonLoading.value = false
}, 1000)
// 重置暂停和流式传输状态
isPaused.value = false
isStreaming.value = false
isPausing.value = false
//重置进度通知标题
currentProgressTitle.value = '任务执行中'
// 开始执行
loading.value = true
// 清除之前的执行结果和状态
agentsStore.setExecutePlan([])
stepExecutionStatus.value = {}
sentStepIds.value.clear()
currentExecutionId.value = null
// 开始批量执行第一批已就绪的步骤
await executeNextReadyBatch()
}
// 查看任务流程
async function handleTaskProcess() {
drawerVisible.value = true
}
// 重置执行结果
async function handleRefresh() {
// 如果有正在执行的任务,先通知后端停止
if (websocket.connected && currentExecutionId.value) {
try {
await websocket.send('stop_execution', {
execution_id: currentExecutionId.value
})
// 等待一下确保后端完全停止
await new Promise(resolve => setTimeout(resolve, 500))
} catch (err) {
console.warn('⚠️ 停止执行失败(可能已经停止):', err)
}
}
// 重置所有状态
agentsStore.setExecutePlan([])
stepExecutionStatus.value = {}
sentStepIds.value.clear()
currentExecutionId.value = null
isPaused.value = false
isStreaming.value = false
isPausing.value = false
loading.value = false
isRestarting.value = false
// 重置进度通知标题
currentProgressTitle.value = '任务执行中'
// 关闭进度通知
if (currentProgressNotificationId.value) {
removeNotification(currentProgressNotificationId.value)
currentProgressNotificationId.value = null
}
success('已重置', '执行状态已重置')
}
// 添加滚动状态指示器
const isScrolling = ref(false)
let scrollTimer: ReturnType<typeof setTimeout> | null = null
// 修改滚动处理函数
function handleScroll() {
isScrolling.value = true
emit('refreshLine')
// 清除之前的定时器
if (scrollTimer) {
clearTimeout(scrollTimer)
}
jsplumb.repaintEverything()
// 设置滚动结束检测
scrollTimer = setTimeout(() => {
isScrolling.value = false
}, 300) as ReturnType<typeof setTimeout>
}
// 修改鼠标事件处理函数
const handleMouseEnter = throttle(id => {
if (!isScrolling.value) {
createInternalLine(id)
}
}, 0)
const handleMouseLeave = throttle(() => {
if (!isScrolling.value) {
createInternalLine()
}
}, 0)
function clear() {
jsplumb.reset()
}
// 封装连线重绘方法
const redrawInternalLines = (highlightId?: string) => {
// 等待DOM更新完成
nextTick(() => {
// 清除旧连线
jsplumb.reset()
// 等待DOM稳定后重新绘制
setTimeout(() => {
createInternalLine(highlightId)
}, 100)
})
}
// 监听 collaborationProcess 变化,自动重绘连线
watch(
() => collaborationProcess,
() => {
redrawInternalLines()
},
{ deep: true }
)
// 组件挂载后初始化连线
onMounted(() => {
// 初始化时绘制连线
nextTick(() => {
setTimeout(() => {
createInternalLine()
}, 100)
})
})
// 按钮交互状态管理
type ButtonState = 'process' | 'execute' | 'refresh' | null
const buttonHoverState = ref<ButtonState>(null)
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
// 清除按钮 hover 定时器
const clearButtonHoverTimer = () => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
buttonHoverTimer = null
}
}
// 设置按钮 hover 状态
const setButtonHoverState = (state: ButtonState, condition?: boolean) => {
clearButtonHoverTimer()
if (condition ?? true) {
buttonHoverState.value = state
}
}
const handleProcessMouseEnter = () => setButtonHoverState('process')
const handleExecuteMouseEnter = () =>
setButtonHoverState('execute', !!agentsStore.agentRawPlan.data)
const handleRefreshMouseEnter = () =>
setButtonHoverState('refresh', agentsStore.executePlan.length > 0)
const handleButtonMouseLeave = () => {
clearButtonHoverTimer()
buttonHoverTimer = setTimeout(() => {
buttonHoverState.value = null
}, 50)
}
// 离开组件时清理
onUnmounted(() => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
})
// 计算按钮类名
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'
})
// 计算是否显示按钮文字
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'
})
// 计算按钮标题
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"
@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-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="getCollapseIconStatus(hasActionResult(item, item1.ID)) === 'loading'"
#icon
>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<!-- 图标状态empty hidden -->
<template
v-else-if="
['empty', 'hidden'].includes(
getCollapseIconStatus(hasActionResult(item, item1.ID))
)
"
#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="getCollapseIconStatus(hasObjectResult(item.OutputObject)) === 'loading'"
#icon
>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<!-- 图标状态empty 或 hidden -->
<template
v-else-if="
['empty', 'hidden'].includes(
getCollapseIconStatus(hasObjectResult(item.OutputObject))
)
"
#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 {
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 {
height: 40px !important;
border-radius: 20px !important;
padding: 0 16px !important;
gap: 8px !important;
// 任务流程按钮 - 固定在左侧向右展开
&: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;
}
}
// 任务执行按钮 - 固定在右侧向左展开
&: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>