feat(agent): 支持自定义API配置并优化UI交互

- 为agent.json添加apiUrl、apiKey、apiModel字段支持
- 更新API接口类型定义,支持传递自定义API配置
- 优化AgentRepoList组件UI样式和交互效果
- 增强JSON文件上传校验逻辑,支持API配置验证
- 改进任务结果页面布局和视觉呈现
- 添加任务过程查看抽屉功能
- 实现执行按钮动态样式和悬停效果
- 优化节点连接线渲染逻辑和性能
This commit is contained in:
zhaoweijie
2025-12-15 20:46:54 +08:00
parent 6392301833
commit 77530c49f8
25 changed files with 2040 additions and 306 deletions

View File

@@ -9,7 +9,7 @@ import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/
import variables from '@/styles/variables.module.scss'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import api from '@/api'
import ProcessCard from '../TaskProcess/ProcessCard.vue'
import ExecutePlan from './ExecutePlan.vue'
const emit = defineEmits<{
@@ -18,21 +18,68 @@ const emit = defineEmits<{
}>()
const agentsStore = useAgentsStore()
const drawerVisible = ref(false)
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 编辑逻辑
const editMode = ref(false) //全局编辑开关
const editMap = reactive<Record<string, boolean>>({}) //行级编辑状态
const editBuffer = reactive<Record<string, string | undefined>>({}) //临时输入
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 save() {
Object.keys(editMap).forEach(key => {
if (editMap[key]) {
const [stepId, processId] = key.split('-')
const value = editBuffer[key]
// 确保 value 是字符串类型
if (value !== undefined && value !== null) {
// @ts-ignore - TypeScript 无法正确推断类型,但运行时是安全的
handleSaveEdit(stepId, processId, value)
}
}
})
editMode.value = false
}
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) {
process.Description = value
}
}
editMap[key] = false
ElMessage.success('已保存(前端内存)')
}
const jsplumb = new Jsplumb('task-results-main', {
connector: {
type: BezierConnector.type,
options: { curviness: 30, stub: 10 },
},
options: { curviness: 30, stub: 10 }
}
})
// 操作折叠面板时要实时的刷新连线
let timer: number
let timer: ReturnType<typeof setInterval> | null = null
function handleCollapse() {
if (timer) {
clearInterval(timer)
@@ -40,11 +87,14 @@ function handleCollapse() {
timer = setInterval(() => {
jsplumb.repaintEverything()
emit('refreshLine')
}, 1)
}, 1) as ReturnType<typeof setInterval>
// 默认三秒后已经完全打开
const timer1 = setTimeout(() => {
clearInterval(timer)
if (timer) {
clearInterval(timer)
timer = null
}
}, 3000)
onUnmounted(() => {
@@ -61,14 +111,14 @@ function handleCollapse() {
function createInternalLine(id?: string) {
const arr: ConnectArg[] = []
jsplumb.reset()
collaborationProcess.value.forEach((item) => {
collaborationProcess.value.forEach(item => {
// 创建左侧流程与产出的连线
arr.push({
sourceId: `task-results-${item.Id}-0`,
targetId: `task-results-${item.Id}-1`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
anchor: [AnchorLocations.Left, AnchorLocations.Left]
})
collaborationProcess.value.forEach((jitem) => {
collaborationProcess.value.forEach(jitem => {
// 创建左侧产出与上一步流程的连线
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
arr.push({
@@ -76,12 +126,12 @@ function createInternalLine(id?: string) {
targetId: `task-results-${item.Id}-0`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
config: {
type: 'output',
},
type: 'output'
}
})
}
// 创建右侧任务程序与InputObject字段的连线
jitem.TaskProcess.forEach((i) => {
jitem.TaskProcess.forEach(i => {
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
const sourceId = `task-results-${item.Id}-1`
@@ -93,21 +143,21 @@ function createInternalLine(id?: string) {
config: {
stops: [
[0, color],
[1, color],
[1, color]
],
transparent: targetId !== id,
},
transparent: targetId !== id
}
})
}
})
})
// 创建右侧TaskProcess内部连线
item.TaskProcess?.forEach((i) => {
item.TaskProcess?.forEach(i => {
if (!i.ImportantInput?.length) {
return
}
item.TaskProcess?.forEach((i2) => {
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}`
@@ -119,10 +169,10 @@ function createInternalLine(id?: string) {
config: {
stops: [
[0, color],
[1, color],
[1, color]
],
transparent: targetId !== id,
},
transparent: targetId !== id
}
})
}
})
@@ -134,6 +184,14 @@ function createInternalLine(id?: string) {
}
const loading = ref(false)
// 额外产物编辑状态
const editingOutputId = ref<string | null>(null)
const editingOutputContent = ref('')
// 额外产物内容存储
const additionalOutputContents = ref<Record<string, string>>({})
async function handleRun() {
try {
loading.value = true
@@ -144,9 +202,51 @@ async function handleRun() {
}
}
// 查看任务过程
async function handleTaskProcess() {
drawerVisible.value = true
}
// 开始编辑额外产物内容
function startOutputEditing(output: string) {
editingOutputId.value = output
editingOutputContent.value = getAdditionalOutputContent(output) || ''
}
// 保存额外产物内容
function saveOutputEditing() {
if (editingOutputId.value && editingOutputContent.value.trim()) {
additionalOutputContents.value[editingOutputId.value] = editingOutputContent.value.trim()
}
editingOutputId.value = null
editingOutputContent.value = ''
}
// 取消编辑额外产物内容
function cancelOutputEditing() {
editingOutputId.value = null
editingOutputContent.value = ''
}
// 获取额外产物内容
function getAdditionalOutputContent(output: string) {
return additionalOutputContents.value[output] || ''
}
// 处理额外产物的键盘事件
function handleOutputKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault()
saveOutputEditing()
} else if (event.key === 'Escape') {
editingOutputId.value = null
editingOutputContent.value = ''
}
}
// 添加滚动状态标识
const isScrolling = ref(false)
let scrollTimer: number
let scrollTimer: ReturnType<typeof setTimeout> | null = null
// 修改滚动处理函数
function handleScroll() {
@@ -161,11 +261,11 @@ function handleScroll() {
// 设置滚动结束检测
scrollTimer = setTimeout(() => {
isScrolling.value = false
}, 300)
}, 300) as ReturnType<typeof setTimeout>
}
// 修改鼠标事件处理函数
const handleMouseEnter = throttle((id) => {
const handleMouseEnter = throttle(id => {
if (!isScrolling.value) {
createInternalLine(id)
}
@@ -181,34 +281,160 @@ function clear() {
jsplumb.reset()
}
// ========== 按钮交互状态管理 ==========
const buttonHoverState = ref<'process' | 'execute' | 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 handleButtonMouseLeave = () => {
// 添加防抖,防止快速切换时的抖动
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
buttonHoverTimer = setTimeout(() => {
buttonHoverState.value = null
}, 50) // 适当减少延迟时间
}
// 添加离开组件时的清理
onUnmounted(() => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
})
// 计算按钮类名
const processBtnClass = computed(() => {
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
})
const executeBtnClass = computed(() => {
// 鼠标悬停在过程按钮上时,执行按钮变圆形
if (buttonHoverState.value === 'process') {
return 'circle'
}
// 其他情况:如果有任务数据就显示椭圆形,否则显示圆形
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
})
// 计算按钮是否显示文字
const showProcessText = computed(() => {
return buttonHoverState.value === 'process'
})
const showExecuteText = computed(() => {
// 鼠标悬停在过程按钮上时,执行按钮不显示文字
if (buttonHoverState.value === 'process') return false
// 其他情况:如果有任务数据就显示文字,否则不显示
return agentsStore.agentRawPlan.data
})
// 计算按钮标题
const processBtnTitle = computed(() => {
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
})
const executeBtnTitle = computed(() => {
return showExecuteText.value ? '任务执行' : '点击运行'
})
defineExpose({
createInternalLine,
clear,
clear
})
</script>
<template>
<div class="h-full flex flex-col relative" id="task-results">
<div
class="h-full flex flex-col relative"
id="task-results"
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
>
<!-- 标题与执行按钮 -->
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
<span>执行结果</span>
<div class="flex items-center gap-[14px]">
<el-button circle :color="variables.tertiary" disabled title="点击刷新">
<svg-icon icon-class="refresh" />
<span class="text-[var(--color-text-title-header)]">执行结果</span>
<div
class="flex items-center gap-[14px] task-button-group"
@mouseleave="handleButtonMouseLeave"
>
<!-- 任务过程按钮 -->
<el-button
:class="processBtnClass"
:color="variables.tertiary"
:title="processBtnTitle"
@mouseenter="handleProcessMouseEnter"
@click="handleTaskProcess"
>
<svg-icon icon-class="process" />
<span v-if="showProcessText" class="btn-text">任务过程</span>
</el-button>
<el-popover :disabled="Boolean(agentsStore.agentRawPlan.data)" title="请先输入要执行的任务">
<!-- 任务执行按钮 -->
<el-popover
:disabled="Boolean(agentsStore.agentRawPlan.data)"
title="请先输入要执行的任务"
:visible="showPopover"
@hide="showPopover = false"
>
<template #reference>
<el-button
circle
:class="executeBtnClass"
:color="variables.tertiary"
title="点击运行"
:title="executeBtnTitle"
:disabled="!agentsStore.agentRawPlan.data"
@mouseenter="handleExecuteMouseEnter"
@click="handleRun"
>
<svg-icon icon-class="action" />
<span v-if="showExecuteText" 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>
<!-- 内容 -->
@@ -218,11 +444,12 @@ defineExpose({
@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' : ''"
shadow="hover"
:shadow="true"
:id="`task-results-${item.Id}-0`"
@click="emit('setCurrentTask', item)"
>
@@ -244,7 +471,14 @@ defineExpose({
<span></span>
</template>
<template #title>
<div class="flex items-center gap-[15px]">
<!-- 运行之前背景颜色是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)]': !agentsStore.executePlan.length,
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
}"
>
<!-- 右侧链接点 -->
<div
class="absolute right-0 top-1/2 transform -translate-y-1/2"
@@ -256,12 +490,19 @@ defineExpose({
>
<svg-icon
:icon-class="getAgentMapIcon(item1.AgentName).icon"
color="var(--color-text)"
color="#fff"
size="24px"
/>
</div>
<div class="text-[16px]">
<span>{{ item1.AgentName }}:&nbsp; &nbsp;</span>
<span
:class="{
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
'text-[var(--color-text-result-detail-run)]':
agentsStore.executePlan.length
}"
>{{ item1.AgentName }}:&nbsp; &nbsp;</span
>
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
{{ getActionTypeDisplay(item1.ActionType)?.name }}
</span>
@@ -279,7 +520,7 @@ defineExpose({
<el-card
class="card-item w-full relative output-object-card"
shadow="hover"
:shadow="true"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-results-${item.Id}-1`"
@click="emit('setCurrentTask', item)"
@@ -297,31 +538,114 @@ defineExpose({
<span></span>
</template>
<template #title>
<div class="text-[18px]">{{ item.OutputObject }}</div>
<div
class="text-[18px]"
:class="{
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
'text-[var(--color-text-result-detail-run)]': agentsStore.executePlan.length
}"
>
{{ item.OutputObject }}
</div>
</template>
<ExecutePlan :node-id="item.OutputObject" :execute-plans="agentsStore.executePlan" />
<ExecutePlan
:node-id="item.OutputObject"
:execute-plans="agentsStore.executePlan"
/>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
<!-- 额外产物的编辑卡片 -->
<div
v-for="(output, index) in agentsStore.additionalOutputs"
:key="`additional-output-${index}`"
class="card-item"
>
<!-- 空的流程卡片位置 -->
<div class="w-full"></div>
<!-- 额外产物的编辑卡片 -->
<el-card
class="card-item w-full relative output-object-card additional-output-card"
:shadow="false"
:id="`additional-output-results-${index}`"
>
<!-- 产物名称行 -->
<div class="text-[18px] mb-3">
{{ output }}
</div>
<!-- 编辑区域行 -->
<div class="additional-output-editor">
<div v-if="editingOutputId === output" class="w-full">
<!-- 编辑状态输入框 + 按钮 -->
<div class="flex flex-col gap-3">
<el-input
v-model="editingOutputContent"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入产物内容"
@keydown="handleOutputKeydown"
class="output-editor"
size="small"
/>
<div class="flex justify-end gap-2">
<el-button @click="saveOutputEditing" type="primary" size="small" class="px-3">
</el-button>
<el-button @click="cancelOutputEditing" size="small" class="px-3">
×
</el-button>
</div>
</div>
</div>
<div v-else class="w-full">
<!-- 非编辑状态折叠区域 + 编辑按钮 -->
<div
class="flex items-center justify-between p-3 bg-[var(--color-bg-quinary)] rounded-[8px]"
>
<div
class="text-[14px] text-[var(--color-text-secondary)] output-content-display"
>
{{ getAdditionalOutputContent(output) || '暂无内容,点击编辑' }}
</div>
<el-button
@click="startOutputEditing(output)"
size="small"
type="primary"
plain
class="flex items-center gap-1"
>
<svg-icon icon-class="action" size="12px" />
<span>编辑</span>
</el-button>
</div>
</div>
</div>
</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-secondary);
background: var(--color-bg-detail-list-run);
min-height: 41px;
line-height: 41px;
border-radius: 20px;
@@ -329,13 +653,16 @@ defineExpose({
position: relative;
.el-collapse-item__title {
background: var(--color-bg-secondary);
background: var(--color-bg-detail-list);
border-radius: 20px;
}
.el-icon {
font-size: 20px;
font-weight: bold;
font-weight: 900;
background: var(--color-bg-icon-rotate);
border-radius: 50px;
color: #d8d8d8;
}
&.is-active {
@@ -357,7 +684,7 @@ defineExpose({
background: none;
.card-item {
background: var(--color-bg-secondary);
background: var(--color-bg-detail);
padding: 25px;
padding-top: 10px;
border-radius: 7px;
@@ -367,7 +694,7 @@ defineExpose({
.el-collapse-item__wrap {
border: none;
background: var(--color-bg-secondary);
background: var(--color-bg-detail-list);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
@@ -376,6 +703,10 @@ defineExpose({
:deep(.el-card) {
.el-card__body {
padding-right: 40px;
background-color: var(--color-bg-detail);
&:hover {
background-color: var(--color-card-bg-result-hover);
}
}
}
@@ -388,13 +719,176 @@ defineExpose({
}
.active-card {
background:
linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
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;
}
}
}
}
// 额外产物编辑区域样式
.additional-output-editor {
.output-editor {
:deep(.el-textarea__inner) {
font-size: 14px;
color: var(--color-text-primary);
background: var(--color-bg-detail);
border: 1px solid #dcdfe6;
resize: none;
padding: 12px;
}
}
.output-content-display {
word-break: break-word;
white-space: pre-wrap;
transition: all 0.2s ease;
line-height: 1.5;
min-height: 20px;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
border-color: #409eff;
opacity: 1;
}
}
// 编辑按钮样式
.el-button {
font-weight: bold;
font-size: 16px;
border-radius: 4px;
&.el-button--small {
padding: 4px 12px;
}
}
}
// ========== 新增:按钮交互样式 ==========
.task-button-group {
.el-button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
overflow: hidden !important;
white-space: nowrap !important;
border: none !important;
color: var(--color-text-primary) !important;
position: relative;
background-color: var(--color-bg-tertiary);
&:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
filter: brightness(1.1) !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;
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;
.btn-text {
display: inline-block !important;
font-size: 14px;
font-weight: 500;
margin-left: 4px;
opacity: 1;
animation: fadeIn 0.3s ease forwards;
}
}
.btn-text {
font-size: 14px;
font-weight: 500;
margin-left: 4px;
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
}
}
</style>