feat:单个agent配置各自的apiurl、apimodel、apikey

This commit is contained in:
liailing1026
2025-12-18 09:43:15 +08:00
parent 5ef86c6fa9
commit b42ab5aedd
7 changed files with 145 additions and 911 deletions

View File

@@ -1,5 +1,7 @@
import asyncio
from openai import OpenAI, AsyncOpenAI
import httpx
from openai import OpenAI, AsyncOpenAI, max_retries
import yaml
from termcolor import colored
import os
@@ -21,6 +23,9 @@ OPENAI_API_BASE = os.getenv("OPENAI_API_BASE") or yaml_data.get(
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or yaml_data.get(
"OPENAI_API_KEY", ""
)
OPENAI_API_MODEL = os.getenv("OPENAI_API_MODEL") or yaml_data.get(
"OPENAI_API_MODEL", ""
)
# Initialize OpenAI clients
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE)
@@ -41,8 +46,11 @@ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY") or yaml_data.get(
# for LLM completion
def LLM_Completion(
messages: list[dict], stream: bool = True, useGroq: bool = True
messages: list[dict], stream: bool = True, useGroq: bool = True,model_config: dict = None
) -> str:
if model_config:
print_colored(f"Using model config: {model_config}", "blue")
return _call_with_custom_config(messages,stream,model_config)
if not useGroq or not FAST_DESIGN_MODE:
force_gpt4 = True
useGroq = False
@@ -75,6 +83,82 @@ def LLM_Completion(
return _chat_completion(messages=messages)
def _call_with_custom_config(messages: list[dict], stream: bool, model_config: dict) ->str:
"使用自定义配置调用API"
api_url = model_config.get("apiUrl", OPENAI_API_BASE)
api_key = model_config.get("apiKey", OPENAI_API_KEY)
api_model = model_config.get("apiModel", OPENAI_API_MODEL)
temp_client = OpenAI(api_key=api_key, base_url=api_url)
temp_async_client = AsyncOpenAI(api_key=api_key, base_url=api_url)
try:
if stream:
try:
loop = asyncio.get_event_loop()
except RuntimeError as ex:
if "There is no current event loop in thread" in str(ex):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(
_achat_completion_stream_custom(messages=messages, temp_async_client=temp_async_client, api_model=api_model)
)
else:
response = temp_client.chat.completions.create(
messages=messages,
model=api_model,
temperature=0.3,
max_tokens=4096,
timeout=180
)
full_reply_content = response.choices[0].message.content
print(colored(full_reply_content, "blue", "on_white"), end="")
return full_reply_content
except Exception as e:
print_colored(f"Custom API error for model {api_model} :{str(e)}","red")
raise
async def _achat_completion_stream_custom(messages:list[dict], temp_async_client, api_model: str ) -> str:
max_retries=3
for attempt in range(max_retries):
try:
response = await temp_async_client.chat.completions.create(
messages=messages,
model=api_model,
temperature=0.3,
max_tokens=4096,
stream=True,
timeout=180
)
collected_chunks = []
collected_messages = []
async for chunk in response:
collected_chunks.append(chunk)
choices = chunk.choices
if len(choices) > 0:
chunk_message = chunk.choices[0].delta
collected_messages.append(chunk_message)
if chunk_message.content:
print(colored(chunk_message.content, "blue", "on_white"), end="")
print()
full_reply_content = "".join(
[m.content or "" for m in collected_messages if m is not None]
)
return full_reply_content
except httpx.RemoteProtocolError as e:
if attempt < max_retries - 1:
wait_time = (attempt + 1) *2
print_colored(f"⚠️ Stream connection interrupted (attempt {attempt+1}/{max_retries}). Retrying in {wait_time}s...", text_color="yellow")
await asyncio.sleep(wait_time)
continue
except Exception as e:
print_colored(f"Custom API stream error for model {api_model} :{str(e)}","red")
raise
async def _achat_completion_stream_groq(messages: list[dict]) -> str:
from groq import AsyncGroq
groq_client = AsyncGroq(api_key=GROQ_API_KEY)
@@ -144,7 +228,7 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
messages=messages,
max_tokens=4096,
temperature=0.3,
timeout=30,
timeout=600,
model="gpt-3.5-turbo-16k",
stream=True,
)
@@ -172,16 +256,15 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
return full_reply_content
async def _achat_completion_json(messages: list[dict]) -> str:
def _achat_completion_json(messages: list[dict] ) -> str:
max_attempts = 5
for attempt in range(max_attempts):
try:
response = await async_client.chat.completions.create(
response = async_client.chat.completions.create(
messages=messages,
max_tokens=4096,
temperature=0.3,
timeout=30,
timeout=600,
model=MODEL,
response_format={"type": "json_object"},
)
@@ -245,7 +328,7 @@ def _cons_kwargs(messages: list[dict]) -> dict:
"messages": messages,
"max_tokens": 2000,
"temperature": 0.3,
"timeout": 60,
"timeout": 600,
}
kwargs_mode = {"model": MODEL}
kwargs.update(kwargs_mode)

View File

@@ -81,16 +81,33 @@ class BaseAction():
action_Record += PROMPT_TEMPLATE_ACTION_RECORD.format(AgentName = actionInfo["AgentName"], Action_Description = actionInfo["AgentName"], Action_Result = actionInfo["Action_Result"], Important_Mark = Important_Mark)
# Handle missing agent profiles gracefully
model_config = None
if agentName not in AgentProfile_Dict:
print_colored(text=f"Warning: Agent '{agentName}' not found in AgentProfile_Dict. Using default profile.", text_color="yellow")
agentProfile = f"AI Agent named {agentName}"
else:
agentProfile = AgentProfile_Dict[agentName]
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(agentName = agentName, agentProfile = agentProfile, General_Goal = General_Goal, Current_Task_Description = TaskDescription, Input_Objects = inputObject_Record, History_Action = action_Record, Action_Description = self.info["Description"], Action_Custom_Note = self.Action_Custom_Note)
# agentProfile = AgentProfile_Dict[agentName]
agent_config = AgentProfile_Dict[agentName]
agentProfile = agent_config.get("profile",f"AI Agent named {agentName}")
if agent_config.get("useCustomAPI",False):
model_config = {
"apiModel":agent_config.get("apiModel"),
"apiUrl":agent_config.get("apiUrl"),
"apiKey":agent_config.get("apiKey"),
}
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(
agentName = agentName,
agentProfile = agentProfile,
General_Goal = General_Goal,
Current_Task_Description = TaskDescription,
Input_Objects = inputObject_Record,
History_Action = action_Record,
Action_Description = self.info["Description"],
Action_Custom_Note = self.Action_Custom_Note
)
print_colored(text = prompt, text_color="red")
messages = [{"role":"system", "content": prompt}]
ActionResult = LLM_Completion(messages,True,False)
ActionResult = LLM_Completion(messages,True,False,model_config=model_config)
ActionInfo_with_Result = copy.deepcopy(self.info)
ActionInfo_with_Result["Action_Result"] = ActionResult

View File

@@ -1,10 +1,10 @@
## config for default LLM
OPENAI_API_BASE: ""
OPENAI_API_KEY: ""
OPENAI_API_MODEL: "gpt-4-turbo-preview"
OPENAI_API_BASE: "https://ai.gitee.com/v1"
OPENAI_API_KEY: "HYCNGM39GGFNSB1F8MBBMI9QYJR3P1CRSYS2PV1A"
OPENAI_API_MODEL: "DeepSeek-V3"
## config for fast mode
FAST_DESIGN_MODE: True
FAST_DESIGN_MODE: False
GROQ_API_KEY: ""
MISTRAL_API_KEY: ""

View File

@@ -271,13 +271,29 @@ def Handle_saveRequestCashe():
@app.route("/setAgents", methods=["POST"])
def set_agents():
global AgentBoard, AgentProfile_Dict
global AgentBoard, AgentProfile_Dict,yaml_data
AgentBoard = request.json
AgentProfile_Dict = {}
for item in AgentBoard:
name = item["Name"]
profile = item["Profile"]
AgentProfile_Dict[name] = profile
if all(item.get(field) for field in ["apiUrl","apiKey","apiModel"]):
agent_config = {
"profile": item["Profile"],
"apiUrl": item["apiUrl"],
"apiKey": item["apiKey"],
"apiModel": item["apiModel"],
"useCustomAPI":True
}
else:
agent_config = {
"profile": item["Profile"],
"apiUrl": yaml_data.get("OPENAI_API_BASE"),
"apiKey": yaml_data.get("OPENAI_API_KEY"),
"apiModel": yaml_data.get("OPENAI_API_MODEL"),
"useCustomAPI":False
}
AgentProfile_Dict[name] = agent_config
return jsonify({"code": 200, "content": "set agentboard successfully"})

View File

@@ -19,7 +19,9 @@ onMounted(async () => {
const res = await readConfig<Agent[]>('agent.json')
agentsStore.setAgents(res)
}
await api.setAgents(agentsStore.agents.map(item => pick(item, ['Name', 'Profile'])))
await api.setAgents(
agentsStore.agents.map(item => pick(item, ['Name', 'Profile', 'apiUrl', 'apiKey', 'apiModel']))
)
})
// 上传agent文件
@@ -118,8 +120,6 @@ const readFileContent = (file: File) => {
return true
})
console.log('🔄 开始处理智能体数据...')
// 修改发送到后端的数据
const processedAgents = validAgents.map(agent => ({
Name: agent.Name,
@@ -131,35 +131,9 @@ const readFileContent = (file: File) => {
apiModel: agent.apiModel
}))
console.log(
'📤 发送到后端的智能体数据:',
processedAgents.map(a => ({
Name: a.Name,
apiUrl: a.apiUrl,
apiKey: a.apiKey ? '***' + a.apiKey.slice(-4) : '未设置',
apiModel: a.apiModel
}))
)
// 调用API
api
.setAgents(processedAgents)
.then(response => {
console.log('✅ 后端API调用成功响应:', response)
ElMessage.success('智能体上传成功')
})
.catch(error => {
console.error('❌ 后端API调用失败:', error)
ElMessage.error('智能体上传失败')
})
// 更新store
console.log('💾 更新智能体store...')
agentsStore.setAgents(processedAgents)
console.log('✅ 智能体store更新完成')
// 调用API
console.log('🌐 开始调用后端API...')
api
.setAgents(processedAgents)
.then(() => {

View File

@@ -1,861 +0,0 @@
<script setup lang="ts">
import { computed, onUnmounted, ref } 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 } from '@/stores'
import api from '@/api'
import ProcessCard from '../TaskProcess/ProcessCard.vue'
import ExecutePlan from './ExecutePlan.vue'
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'] ?? []
})
// 编辑逻辑
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 }
}
})
// 操作折叠面板时要实时的刷新连线
let timer: ReturnType<typeof setInterval> | null = null
function handleCollapse() {
if (timer) {
clearInterval(timer)
}
timer = setInterval(() => {
jsplumb.repaintEverything()
emit('refreshLine')
}, 1) as ReturnType<typeof setInterval>
// 默认三秒后已经完全打开
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 editingOutputId = ref<string | null>(null)
const editingOutputContent = ref('')
// 额外产物内容存储
const additionalOutputContents = ref<Record<string, string>>({})
async function handleRun() {
try {
loading.value = true
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
agentsStore.setExecutePlan(d)
} finally {
loading.value = false
}
}
// 查看任务过程
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: 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)
}
}, 100)
const handleMouseLeave = throttle(() => {
if (!isScrolling.value) {
createInternalLine()
}
}, 100)
function clear() {
jsplumb.reset()
}
// ========== 按钮交互状态管理 ==========
const buttonHoverState = ref(null) // null | 'process' | 'execute'
const handleProcessMouseEnter = () => {
buttonHoverState.value = 'process'
}
const handleExecuteMouseEnter = () => {
if (agentsStore.agentRawPlan.data) {
buttonHoverState.value = 'execute'
}
}
const handleButtonMouseLeave = () => {
setTimeout(() => {
buttonHoverState.value = null
}, 150)
}
// 计算按钮类名
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
})
</script>
<template>
<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 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="请先输入要执行的任务"
:visible="showPopover"
@hide="showPopover = false"
>
<template #reference>
<el-button
:class="executeBtnClass"
:color="variables.tertiary"
: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>
<!-- 内容 -->
<div
v-loading="agentsStore.agentRawPlan.loading"
class="flex-1 overflow-auto relative"
@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="true"
: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="Boolean(!agentsStore.executePlan.length || loading)"
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
@mouseleave="handleMouseLeave"
>
<template v-if="loading" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<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)]': !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"
: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>{{ 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"
:shadow="true"
: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">
<el-collapse-item
class="output-object"
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
>
<template v-if="loading" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<template #title>
<div class="text-[18px]">{{ item.OutputObject }}</div>
</template>
<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-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: 25px;
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;
}
}
}
}
// 额外产物编辑区域样式
.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>

View File

@@ -1,4 +1,10 @@
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import type {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
import axios from 'axios'
import qs from 'qs'
import type { Ref } from 'vue'
@@ -16,7 +22,6 @@ export interface AxiosResponseData {
export function initService() {
service = axios.create({
baseURL: '/api',
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
paramsSerializer: (params) => {
return qs.stringify(params)