refactor: 前端 Agent 组件重构
- AgentRuntimeConfigForm 从 AgentConfigSidebar 重命名 - AgentChatComponent、AgentInputArea、AgentMessageComponent 重写交互逻辑 - AgentPanel、AgentArtifactsCard、AgentView 适配新数据模型 - stores/agent.js 简化状态管理 - BasicSettingsSection、ShareConfigForm、DebugComponent 收敛配置 - SubagentCardList、ToolsCardList 适配新 API - useApproval 逻辑优化
This commit is contained in:
parent
42636477c2
commit
749ed8d757
@ -3,7 +3,6 @@ import {
|
||||
apiPost,
|
||||
apiDelete,
|
||||
apiPut,
|
||||
apiAdminPost,
|
||||
apiRequest
|
||||
} from './base'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
@ -31,7 +30,7 @@ export const agentApi = {
|
||||
...useUserStore().getAuthHeaders()
|
||||
}
|
||||
|
||||
return fetch('/api/chat/agent', {
|
||||
return fetch('/api/agent/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
signal,
|
||||
@ -64,24 +63,20 @@ export const agentApi = {
|
||||
return response.response
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取默认智能体
|
||||
* @returns {Promise} - 默认智能体信息
|
||||
*/
|
||||
getDefaultAgent: () => apiGet('/api/chat/default_agent'),
|
||||
|
||||
/**
|
||||
* 获取智能体列表
|
||||
* @returns {Promise} - 智能体列表
|
||||
*/
|
||||
getAgents: () => apiGet('/api/chat/agent'),
|
||||
getAgents: () => apiGet('/api/agent'),
|
||||
|
||||
getAgentBackends: () => apiGet('/api/agent/backends'),
|
||||
|
||||
/**
|
||||
* 获取单个智能体详情
|
||||
* @param {string} agentId - 智能体ID
|
||||
* @returns {Promise} - 智能体详情
|
||||
*/
|
||||
getAgentDetail: (agentId) => apiGet(`/api/chat/agent/${agentId}`),
|
||||
getAgentDetail: (agentId) => apiGet(`/api/agent/${agentId}`),
|
||||
|
||||
/**
|
||||
* 获取智能体历史消息
|
||||
@ -117,36 +112,16 @@ export const agentApi = {
|
||||
getMessageFeedback: (messageId) => apiGet(`/api/chat/message/${messageId}/feedback`),
|
||||
|
||||
|
||||
getAgentConfigs: (agentId) => apiGet(`/api/chat/agent/${agentId}/configs`),
|
||||
createAgent: (payload) => apiPost('/api/agent', payload),
|
||||
|
||||
getAgentConfigProfile: (agentId, configId) =>
|
||||
apiGet(`/api/chat/agent/${agentId}/configs/${configId}`),
|
||||
updateAgent: (agentId, payload) => apiPut(`/api/agent/${agentId}`, payload),
|
||||
|
||||
createAgentConfigProfile: (agentId, payload) =>
|
||||
apiPost(`/api/chat/agent/${agentId}/configs`, payload),
|
||||
|
||||
updateAgentConfigProfile: (agentId, configId, payload) =>
|
||||
apiPut(`/api/chat/agent/${agentId}/configs/${configId}`, payload),
|
||||
|
||||
setAgentConfigDefault: (agentId, configId) =>
|
||||
apiPost(`/api/chat/agent/${agentId}/configs/${configId}/set_default`, {}),
|
||||
|
||||
deleteAgentConfigProfile: (agentId, configId) =>
|
||||
apiDelete(`/api/chat/agent/${agentId}/configs/${configId}`),
|
||||
|
||||
/**
|
||||
* 设置默认智能体
|
||||
* @param {string} agentId - 智能体ID
|
||||
* @returns {Promise} - 设置结果
|
||||
*/
|
||||
setDefaultAgent: async (agentId) => {
|
||||
return apiAdminPost('/api/chat/set_default_agent', { agent_id: agentId })
|
||||
},
|
||||
deleteAgent: (agentId) => apiDelete(`/api/agent/${agentId}`),
|
||||
|
||||
/**
|
||||
* 恢复被人工审批中断的对话(流式响应)
|
||||
* @param {string} agentId - 智能体ID
|
||||
* @param {Object} data - 恢复数据 { thread_id, answer: { question_id: answer }, approved }
|
||||
* @param {string} threadId - 会话 ID
|
||||
* @param {Object} data - 恢复数据 { answer: { question_id: answer }, approved }
|
||||
* @param {Object} options - 可选参数(signal, headers等)
|
||||
* @returns {Promise} - 恢复响应流
|
||||
*/
|
||||
@ -175,9 +150,9 @@ export const agentApi = {
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
createAgentRun: (data) =>
|
||||
apiPost('/api/chat/runs', {
|
||||
apiPost('/api/agent/runs', {
|
||||
query: data.query,
|
||||
agent_config_id: data.agent_config_id,
|
||||
agent_id: data.agent_id,
|
||||
thread_id: data.thread_id,
|
||||
meta: data.meta || {},
|
||||
image_content: data.image_content || null
|
||||
@ -188,21 +163,21 @@ export const agentApi = {
|
||||
* @param {string} runId - run ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getAgentRun: (runId) => apiGet(`/api/chat/runs/${runId}`),
|
||||
getAgentRun: (runId) => apiGet(`/api/agent/runs/${runId}`),
|
||||
|
||||
/**
|
||||
* 取消 Run
|
||||
* @param {string} runId - run ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
cancelAgentRun: (runId) => apiPost(`/api/chat/runs/${runId}/cancel`, {}),
|
||||
cancelAgentRun: (runId) => apiPost(`/api/agent/runs/${runId}/cancel`, {}),
|
||||
|
||||
/**
|
||||
* 获取线程活跃 Run
|
||||
* @param {string} threadId - 线程ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getThreadActiveRun: (threadId) => apiGet(`/api/chat/thread/${threadId}/active_run`),
|
||||
getThreadActiveRun: (threadId) => apiGet(`/api/agent/thread/${threadId}/active_run`),
|
||||
|
||||
/**
|
||||
* 打开 Run 事件 SSE 连接(调用方负责关闭)
|
||||
@ -214,7 +189,7 @@ export const agentApi = {
|
||||
streamAgentRunEvents: (runId, afterSeq = '0-0', options = {}) => {
|
||||
const { signal } = options
|
||||
return fetch(
|
||||
`/api/chat/runs/${runId}/events?after_seq=${encodeURIComponent(String(afterSeq))}`,
|
||||
`/api/agent/runs/${runId}/events?after_seq=${encodeURIComponent(String(afterSeq))}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@ -47,14 +47,6 @@ const props = defineProps({
|
||||
threadId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
agentId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
agentConfigId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['saved', 'open-preview'])
|
||||
@ -104,12 +96,7 @@ const downloadFile = async (file) => {
|
||||
if (!props.threadId || !file?.path) return
|
||||
|
||||
try {
|
||||
const response = await downloadViewerFile(
|
||||
props.threadId,
|
||||
file.path,
|
||||
props.agentId,
|
||||
props.agentConfigId
|
||||
)
|
||||
const response = await downloadViewerFile(props.threadId, file.path)
|
||||
const blob = await response.blob()
|
||||
const contentDisposition =
|
||||
response.headers.get('Content-Disposition') || response.headers.get('content-disposition')
|
||||
|
||||
@ -52,8 +52,6 @@
|
||||
v-if="shouldShowArtifacts(row.conv)"
|
||||
:artifacts="currentArtifacts"
|
||||
:thread-id="currentChatId"
|
||||
:agent-id="currentThread?.agent_id || currentAgentId"
|
||||
:agent-config-id="selectedAgentConfigId"
|
||||
@saved="handleArtifactSaved"
|
||||
@open-preview="openPanelPreview"
|
||||
/>
|
||||
@ -82,7 +80,6 @@
|
||||
<span class="generating-text">正在生成回复...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="bottom" :class="{ 'start-screen': !conversations.length }">
|
||||
<!-- 人工审批弹窗 - 放在输入框上方 -->
|
||||
@ -101,54 +98,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 打招呼区域 - 在输入框上方 -->
|
||||
<div v-if="!conversations.length" class="chat-examples-input">
|
||||
<div v-if="!conversations.length" class="chat-greeting-input">
|
||||
<h1>{{ randomGreeting }}</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="showStartAgentSegment" class="agent-segment-wrapper">
|
||||
<a-segmented
|
||||
:value="currentAgentId"
|
||||
:options="agentSegmentOptions"
|
||||
@change="handleStartAgentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="showStartAgentDropdown" class="agent-switcher-wrapper">
|
||||
<a-dropdown :trigger="['click']" placement="bottomCenter">
|
||||
<button type="button" class="agent-switcher-btn">
|
||||
<component :is="currentAgentIcon" size="16" class="agent-switcher-icon" />
|
||||
<span class="agent-switcher-text">{{ currentAgentName }}</span>
|
||||
<ChevronDown size="16" class="agent-switcher-chevron" />
|
||||
</button>
|
||||
<template #overlay>
|
||||
<a-menu class="agent-switcher-menu">
|
||||
<a-menu-item
|
||||
v-for="agent in startAgents"
|
||||
:key="agent.id"
|
||||
@click="handleStartAgentChange(agent.id)"
|
||||
>
|
||||
<div class="agent-switcher-menu-item">
|
||||
<component
|
||||
:is="getAgentIconComponent(agent.id)"
|
||||
size="16"
|
||||
class="agent-switcher-menu-icon"
|
||||
/>
|
||||
<span class="agent-switcher-menu-text">{{
|
||||
agent.name || 'Unknown'
|
||||
}}</span>
|
||||
<span
|
||||
v-if="agent.id === currentAgentId"
|
||||
class="agent-switcher-menu-badge"
|
||||
>
|
||||
当前
|
||||
</span>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
|
||||
<AgentInputArea
|
||||
v-model="userInput"
|
||||
:is-loading="isProcessing"
|
||||
@ -159,31 +112,16 @@
|
||||
:supports-file-upload="supportsFileUpload"
|
||||
:has-active-thread="!!currentChatId"
|
||||
:todos="currentTodos"
|
||||
:attachments="currentPendingThreadAttachments"
|
||||
@send="handleSendOrStop"
|
||||
@upload-attachment="handleAttachmentUpload"
|
||||
@remove-attachment="handleAttachmentRemove"
|
||||
>
|
||||
<template #actions-left-extra>
|
||||
<slot name="input-actions-left"></slot>
|
||||
<slot name="input-actions-left" :has-active-thread="!!currentChatId"></slot>
|
||||
</template>
|
||||
</AgentInputArea>
|
||||
|
||||
<!-- 示例问题 -->
|
||||
<div
|
||||
class="example-questions"
|
||||
v-if="!conversations.length && exampleQuestions.length > 0"
|
||||
>
|
||||
<div class="example-chips">
|
||||
<div
|
||||
v-for="question in exampleQuestions"
|
||||
:key="question.id"
|
||||
class="example-chip"
|
||||
@click="handleExampleClick(question.text)"
|
||||
>
|
||||
{{ question.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-actions" v-if="conversations.length > 0">
|
||||
<p class="note">当前智能体:{{ currentThreadAgentName }};请注意辨别内容的可靠性</p>
|
||||
</div>
|
||||
@ -208,8 +146,6 @@
|
||||
v-if="isAgentPanelOpen"
|
||||
:agent-state="currentAgentState"
|
||||
:thread-id="currentChatId"
|
||||
:agent-id="currentThread?.agent_id || currentAgentId"
|
||||
:agent-config-id="selectedAgentConfigId"
|
||||
:panel-ratio="panelRatio"
|
||||
:preview-tabs="agentPanelPreviewTabs"
|
||||
:active-preview-path="agentPanelActivePreviewPath"
|
||||
@ -239,15 +175,13 @@ import {
|
||||
computed,
|
||||
onUnmounted,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
h
|
||||
onDeactivated
|
||||
} from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import AgentInputArea from '@/components/AgentInputArea.vue'
|
||||
import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
|
||||
import RefsComponent from '@/components/RefsComponent.vue'
|
||||
import ToolCallsGroupComponent from '@/components/ToolCallsGroupComponent.vue'
|
||||
import { Bot, Telescope, ChevronDown } from 'lucide-vue-next'
|
||||
import { handleChatError, handleValidationError } from '@/utils/errorHandler'
|
||||
import { ScrollController } from '@/utils/scrollController'
|
||||
import { AgentValidator } from '@/utils/agentValidator'
|
||||
@ -280,15 +214,8 @@ const agentStore = useAgentStore()
|
||||
const chatThreadsStore = useChatThreadsStore()
|
||||
const chatUIStore = useChatUIStore()
|
||||
const configStore = useConfigStore()
|
||||
const {
|
||||
agents,
|
||||
selectedAgentId,
|
||||
defaultAgentId,
|
||||
selectedAgentConfigId,
|
||||
agentConfig,
|
||||
configurableItems,
|
||||
availableKnowledgeBases
|
||||
} = storeToRefs(agentStore)
|
||||
const { agents, selectedAgentId, agentConfig, configurableItems, availableKnowledgeBases } =
|
||||
storeToRefs(agentStore)
|
||||
const { threads, currentThreadId, currentThread } = storeToRefs(chatThreadsStore)
|
||||
|
||||
// ==================== LOCAL CHAT & UI STATE ====================
|
||||
@ -309,20 +236,6 @@ const greetingMessages = [
|
||||
// 随机选择一个打招呼文本
|
||||
const randomGreeting = greetingMessages[Math.floor(Math.random() * greetingMessages.length)]
|
||||
|
||||
// 从智能体元数据获取示例问题
|
||||
const exampleQuestions = computed(() => {
|
||||
const agentId = currentAgentId.value
|
||||
let examples = []
|
||||
if (agentId && agents.value && agents.value.length > 0) {
|
||||
const agent = agents.value.find((a) => a.id === agentId)
|
||||
examples = agent ? agent.metadata?.examples || [] : []
|
||||
}
|
||||
return examples.map((text, index) => ({
|
||||
id: index + 1,
|
||||
text: text
|
||||
}))
|
||||
})
|
||||
|
||||
// 业务状态(保留在组件本地)
|
||||
const chatState = reactive({
|
||||
currentThreadId: null,
|
||||
@ -383,7 +296,10 @@ const getPanelContainerWidth = () => {
|
||||
|
||||
const getMaxPanelRatio = (containerWidth = getPanelContainerWidth()) => {
|
||||
if (!containerWidth) return maxPanelRatio
|
||||
return Math.max(minPanelRatio, Math.min(maxPanelRatio, (containerWidth - minChatMainWidth) / containerWidth))
|
||||
return Math.max(
|
||||
minPanelRatio,
|
||||
Math.min(maxPanelRatio, (containerWidth - minChatMainWidth) / containerWidth)
|
||||
)
|
||||
}
|
||||
|
||||
const clampPanelRatio = (ratio, containerWidth = getPanelContainerWidth()) => {
|
||||
@ -416,7 +332,8 @@ const resetAgentPanelState = () => {
|
||||
}
|
||||
|
||||
const setAgentPanelViewMode = (mode) => {
|
||||
agentPanelViewMode.value = mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
|
||||
agentPanelViewMode.value =
|
||||
mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
|
||||
|
||||
if (agentPanelActivePreviewPath.value) {
|
||||
panelRatio.value = clampPanelRatio(previewPanelRatio)
|
||||
@ -487,10 +404,9 @@ const closePanelPreviewPath = (targetPath) => {
|
||||
// ==================== COMPUTED PROPERTIES ====================
|
||||
const currentAgentId = computed(() => {
|
||||
if (props.singleMode) {
|
||||
return props.agentId || defaultAgentId.value
|
||||
} else {
|
||||
return selectedAgentId.value
|
||||
return props.agentId || selectedAgentId.value || agents.value[0]?.id || ''
|
||||
}
|
||||
return selectedAgentId.value
|
||||
})
|
||||
|
||||
const currentAgentName = computed(() => {
|
||||
@ -502,7 +418,6 @@ const currentAgent = computed(() => {
|
||||
if (!currentAgentId.value || !agents.value || !agents.value.length) return null
|
||||
return agents.value.find((a) => a.id === currentAgentId.value) || null
|
||||
})
|
||||
const startAgents = computed(() => agents.value || [])
|
||||
const currentChatId = computed(() => currentThreadId.value)
|
||||
|
||||
const currentThreadAgentName = computed(() => {
|
||||
@ -515,8 +430,6 @@ const currentThreadAgentName = computed(() => {
|
||||
}
|
||||
return currentAgentName.value
|
||||
})
|
||||
const currentAgentIcon = computed(() => getAgentIconComponent(currentAgentId.value))
|
||||
|
||||
// 检查当前智能体是否支持文件上传
|
||||
const supportsFileUpload = computed(() => {
|
||||
if (!currentAgent.value) return false
|
||||
@ -542,6 +455,9 @@ const currentThreadAttachments = computed(() => {
|
||||
if (!currentChatId.value) return []
|
||||
return threadAttachmentsMap.value[currentChatId.value] || []
|
||||
})
|
||||
const currentPendingThreadAttachments = computed(() =>
|
||||
currentThreadAttachments.value.filter((attachment) => !attachment?.request_id)
|
||||
)
|
||||
const currentArtifacts = computed(() => {
|
||||
const artifacts = currentAgentState.value?.artifacts
|
||||
return Array.isArray(artifacts) ? artifacts : []
|
||||
@ -653,55 +569,6 @@ const conversationRows = computed(() => {
|
||||
return rows
|
||||
})
|
||||
|
||||
// 智能体图标映射
|
||||
const agentIconMap = {
|
||||
ChatbotAgent: Bot,
|
||||
DeepAgent: Telescope
|
||||
}
|
||||
|
||||
const getAgentIconComponent = (agentId) => {
|
||||
return agentIconMap[agentId] || Bot
|
||||
}
|
||||
|
||||
const agentSegmentOptions = computed(() => {
|
||||
return startAgents.value.map((agent) => {
|
||||
const IconComponent = getAgentIconComponent(agent.id)
|
||||
return {
|
||||
label: () =>
|
||||
h('div', { class: 'agent-option-label' }, [
|
||||
h(IconComponent, { size: 16, class: 'agent-option-icon' }),
|
||||
h('span', null, agent.name || 'Unknown')
|
||||
]),
|
||||
value: agent.id
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const showStartAgentSelector = computed(() => {
|
||||
return !props.singleMode && !conversations.value.length && startAgents.value.length > 1
|
||||
})
|
||||
|
||||
const showStartAgentDropdown = computed(() => {
|
||||
return (
|
||||
showStartAgentSelector.value &&
|
||||
(startAgents.value.length >= 4 || localUIState.chatMainWidth < 380)
|
||||
)
|
||||
})
|
||||
|
||||
const showStartAgentSegment = computed(() => {
|
||||
return showStartAgentSelector.value && !showStartAgentDropdown.value
|
||||
})
|
||||
|
||||
const handleStartAgentChange = async (agentId) => {
|
||||
if (!agentId || agentId === currentAgentId.value) return
|
||||
if (conversations.value.length > 0) return
|
||||
try {
|
||||
await agentStore.selectAgent(agentId)
|
||||
} catch (error) {
|
||||
handleChatError(error, 'load')
|
||||
}
|
||||
}
|
||||
|
||||
const isLoadingMessages = computed(() => chatUIStore.isLoadingMessages)
|
||||
const isStreaming = computed(() => {
|
||||
const threadState = currentThreadState.value
|
||||
@ -736,7 +603,12 @@ const createClientRequestId = () => {
|
||||
return `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
const buildOptimisticHumanMessage = ({ requestId, text, imageContent = null }) => {
|
||||
const buildOptimisticHumanMessage = ({
|
||||
requestId,
|
||||
text,
|
||||
imageContent = null,
|
||||
attachments = []
|
||||
}) => {
|
||||
const message = {
|
||||
id: requestId,
|
||||
role: 'user',
|
||||
@ -744,7 +616,8 @@ const buildOptimisticHumanMessage = ({ requestId, text, imageContent = null }) =
|
||||
content: text,
|
||||
message_type: imageContent ? 'multimodal_image' : 'text',
|
||||
extra_metadata: {
|
||||
request_id: requestId
|
||||
request_id: requestId,
|
||||
attachments
|
||||
}
|
||||
}
|
||||
|
||||
@ -799,15 +672,33 @@ const stripDuplicatedOngoingHumanMessage = (historyConvs, ongoingMessages) => {
|
||||
}
|
||||
|
||||
// 发送 runs 前先在前端插入一条用户消息,避免等待 worker 轮询后消息才出现。
|
||||
const insertOptimisticHumanMessage = (threadState, { requestId, text, imageContent = null }) => {
|
||||
const insertOptimisticHumanMessage = (
|
||||
threadState,
|
||||
{ requestId, text, imageContent = null, attachments = [] }
|
||||
) => {
|
||||
if (!threadState || !requestId) return
|
||||
threadState.pendingRequestId = requestId
|
||||
threadState.replyLoadingVisible = false
|
||||
threadState.onGoingConv.msgChunks[requestId] = [
|
||||
buildOptimisticHumanMessage({ requestId, text, imageContent })
|
||||
buildOptimisticHumanMessage({ requestId, text, imageContent, attachments })
|
||||
]
|
||||
}
|
||||
|
||||
const markAttachmentsRequestId = (threadId, attachments, requestId) => {
|
||||
if (!threadId || !attachments.length) return null
|
||||
const previousAttachments = threadAttachmentsMap.value[threadId] || []
|
||||
const fileIds = new Set(attachments.map((attachment) => attachment.file_id).filter(Boolean))
|
||||
threadAttachmentsMap.value[threadId] = previousAttachments.map((attachment) =>
|
||||
fileIds.has(attachment.file_id) ? { ...attachment, request_id: requestId } : attachment
|
||||
)
|
||||
return previousAttachments
|
||||
}
|
||||
|
||||
const rollbackAttachments = (threadId, previousAttachments) => {
|
||||
if (!threadId || !Array.isArray(previousAttachments)) return
|
||||
threadAttachmentsMap.value[threadId] = previousAttachments
|
||||
}
|
||||
|
||||
const CONFIG_CHANGE_NOTICE_MESSAGE =
|
||||
'在运行过程中切换或修改配置可能会影响最终效果,建议新建一个对话。'
|
||||
|
||||
@ -823,7 +714,6 @@ const withConfigNoticeSync = async (task) => {
|
||||
const buildThreadConfigSnapshot = () => {
|
||||
return {
|
||||
agentId: currentAgentId.value || '',
|
||||
agentConfigId: selectedAgentConfigId.value ?? null,
|
||||
configJson: JSON.stringify(agentConfig.value || {})
|
||||
}
|
||||
}
|
||||
@ -912,7 +802,6 @@ const maybeInsertThreadConfigNotice = () => {
|
||||
|
||||
if (
|
||||
previousSnapshot.agentId === currentSnapshot.agentId &&
|
||||
previousSnapshot.agentConfigId === currentSnapshot.agentConfigId &&
|
||||
previousSnapshot.configJson === currentSnapshot.configJson
|
||||
) {
|
||||
return
|
||||
@ -1021,34 +910,6 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
// ==================== 线程管理方法 ====================
|
||||
const setThreadAgentConfigId = (threadId, agentConfigId) => {
|
||||
if (!threadId) return
|
||||
const thread = threads.value.find((item) => item.id === threadId)
|
||||
if (thread) {
|
||||
thread.metadata = {
|
||||
...(thread.metadata || {}),
|
||||
agent_config_id: agentConfigId ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncSelectedConfigForThread = async (thread) => {
|
||||
const threadAgentConfigId = thread?.metadata?.agent_config_id
|
||||
if (!threadAgentConfigId) return
|
||||
|
||||
const targetAgentId = thread.agent_id || currentAgentId.value
|
||||
if (!targetAgentId) return
|
||||
|
||||
const configList = agentStore.agentConfigs[targetAgentId] || []
|
||||
if (!configList.length) {
|
||||
await agentStore.fetchAgentConfigs(targetAgentId)
|
||||
}
|
||||
|
||||
if (selectedAgentConfigId.value !== threadAgentConfigId) {
|
||||
await agentStore.selectAgentConfig(threadAgentConfigId)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前智能体的线程列表
|
||||
const fetchThreads = async (agentId = null) => {
|
||||
const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId
|
||||
@ -1205,6 +1066,28 @@ const handleAttachmentUpload = async (files) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAttachmentRemove = async (attachment) => {
|
||||
const threadId = currentChatId.value
|
||||
const fileId = attachment?.file_id
|
||||
if (!threadId || !fileId) return
|
||||
|
||||
const previousAttachments = threadAttachmentsMap.value[threadId] || []
|
||||
threadAttachmentsMap.value[threadId] = previousAttachments.filter(
|
||||
(item) => item.file_id !== fileId
|
||||
)
|
||||
|
||||
try {
|
||||
await threadApi.deleteThreadAttachment(threadId, fileId)
|
||||
await Promise.all([
|
||||
fetchAgentState(currentAgentId.value, threadId),
|
||||
refreshThreadFilesAndAttachments(threadId)
|
||||
])
|
||||
} catch (error) {
|
||||
threadAttachmentsMap.value[threadId] = previousAttachments
|
||||
handleChatError(error, 'delete')
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 审批功能管理 ====================
|
||||
const { approvalState, handleApproval, processApprovalInStream } = useApproval({
|
||||
getThreadState,
|
||||
@ -1238,7 +1121,9 @@ const sendMessage = async ({
|
||||
threadId,
|
||||
text,
|
||||
signal = undefined,
|
||||
imageData = undefined
|
||||
imageData = undefined,
|
||||
requestId = '',
|
||||
attachmentFileIds = []
|
||||
}) => {
|
||||
if (!agentId || !threadId || !text) {
|
||||
const error = new Error('Missing agent, thread, or message text')
|
||||
@ -1246,18 +1131,14 @@ const sendMessage = async ({
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (!selectedAgentConfigId.value) {
|
||||
const error = new Error('Missing agent_config_id')
|
||||
handleChatError(error, 'send')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
setThreadAgentConfigId(threadId, selectedAgentConfigId.value)
|
||||
|
||||
const requestData = {
|
||||
query: text,
|
||||
agent_id: agentId,
|
||||
thread_id: threadId,
|
||||
agent_config_id: selectedAgentConfigId.value
|
||||
meta: {
|
||||
request_id: requestId,
|
||||
attachment_file_ids: attachmentFileIds
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有图片,添加到请求中
|
||||
@ -1317,7 +1198,6 @@ const selectChat = async (chatId) => {
|
||||
await agentStore.selectAgent(targetChat.agent_id)
|
||||
}
|
||||
|
||||
await syncSelectedConfigForThread(targetChat)
|
||||
syncThreadConfigSnapshot(chatId)
|
||||
})
|
||||
} catch (error) {
|
||||
@ -1382,11 +1262,6 @@ const handleSendMessage = async ({ image } = {}) => {
|
||||
if ((!text && !image) || !currentAgent.value || isProcessing.value || sendCooldownActive.value)
|
||||
return
|
||||
|
||||
if (!selectedAgentConfigId.value) {
|
||||
message.error('请先选择智能体配置后再发送消息')
|
||||
return
|
||||
}
|
||||
|
||||
// 发送后进入短暂冷却,防止连续触发停止
|
||||
startSendCooldown()
|
||||
|
||||
@ -1407,6 +1282,11 @@ const handleSendMessage = async ({ image } = {}) => {
|
||||
const threadState = getThreadState(threadId)
|
||||
if (!threadState) return
|
||||
|
||||
const pendingAttachments = [...currentPendingThreadAttachments.value]
|
||||
const pendingAttachmentFileIds = pendingAttachments
|
||||
.map((attachment) => attachment.file_id)
|
||||
.filter(Boolean)
|
||||
|
||||
if (useRunsApi) {
|
||||
if ((threadMessages.value[threadId] || []).length === 0) {
|
||||
const autoTitle = text.replace(/\s+/g, ' ').trim().slice(0, 2000)
|
||||
@ -1434,19 +1314,25 @@ const handleSendMessage = async ({ image } = {}) => {
|
||||
|
||||
resetOnGoingConv(threadId)
|
||||
const requestId = createClientRequestId()
|
||||
const previousAttachments = markAttachmentsRequestId(threadId, pendingAttachments, requestId)
|
||||
insertOptimisticHumanMessage(threadState, {
|
||||
requestId,
|
||||
text,
|
||||
imageContent
|
||||
imageContent,
|
||||
attachments: pendingAttachments.map((attachment) => ({
|
||||
...attachment,
|
||||
request_id: requestId
|
||||
}))
|
||||
})
|
||||
threadState.isStreaming = true
|
||||
try {
|
||||
const runResp = await agentApi.createAgentRun({
|
||||
query: text,
|
||||
agent_config_id: selectedAgentConfigId.value,
|
||||
agent_id: currentAgentId.value,
|
||||
thread_id: threadId,
|
||||
meta: {
|
||||
request_id: requestId
|
||||
request_id: requestId,
|
||||
attachment_file_ids: pendingAttachmentFileIds
|
||||
},
|
||||
image_content: imageContent
|
||||
})
|
||||
@ -1459,6 +1345,7 @@ const handleSendMessage = async ({ image } = {}) => {
|
||||
threadState.isStreaming = false
|
||||
threadState.replyLoadingVisible = false
|
||||
threadState.pendingRequestId = null
|
||||
rollbackAttachments(threadId, previousAttachments)
|
||||
resetOnGoingConv(threadId)
|
||||
handleChatError(error, 'send')
|
||||
}
|
||||
@ -1493,10 +1380,12 @@ const handleSendMessage = async ({ image } = {}) => {
|
||||
threadState.isStreaming = true
|
||||
resetOnGoingConv(threadId)
|
||||
const requestId = createClientRequestId()
|
||||
const previousAttachments = markAttachmentsRequestId(threadId, pendingAttachments, requestId)
|
||||
insertOptimisticHumanMessage(threadState, {
|
||||
requestId,
|
||||
text,
|
||||
imageContent
|
||||
imageContent,
|
||||
attachments: pendingAttachments.map((attachment) => ({ ...attachment, request_id: requestId }))
|
||||
})
|
||||
threadState.streamAbortController = new AbortController()
|
||||
|
||||
@ -1506,13 +1395,16 @@ const handleSendMessage = async ({ image } = {}) => {
|
||||
threadId: threadId,
|
||||
text: text,
|
||||
signal: threadState.streamAbortController?.signal,
|
||||
imageData: image
|
||||
imageData: image,
|
||||
requestId,
|
||||
attachmentFileIds: pendingAttachmentFileIds
|
||||
})
|
||||
|
||||
await handleAgentResponse(response, threadId)
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Stream error:', error)
|
||||
rollbackAttachments(threadId, previousAttachments)
|
||||
handleChatError(error, 'send')
|
||||
} else {
|
||||
console.warn('[Interrupted] Catch')
|
||||
@ -1586,7 +1478,7 @@ const handleApprovalWithStream = async (answer) => {
|
||||
|
||||
try {
|
||||
// 使用审批 composable 处理审批
|
||||
const response = await handleApproval(answer, currentAgentId.value, selectedAgentConfigId.value)
|
||||
const response = await handleApproval(answer)
|
||||
|
||||
if (!response) return // 如果 handleApproval 抛出错误,这里不会执行
|
||||
|
||||
@ -1619,14 +1511,6 @@ const handleQuestionCancel = () => {
|
||||
handleApprovalWithStream('reject')
|
||||
}
|
||||
|
||||
// 处理示例问题点击
|
||||
const handleExampleClick = (questionText) => {
|
||||
userInput.value = questionText
|
||||
nextTick(() => {
|
||||
handleSendMessage()
|
||||
})
|
||||
}
|
||||
|
||||
const buildExportPayload = () => {
|
||||
const agentId = currentAgentId.value
|
||||
let agentDescription = ''
|
||||
@ -1966,11 +1850,6 @@ watch(currentAgentId, (newAgentId, oldAgentId) => {
|
||||
maybeInsertThreadConfigNotice()
|
||||
})
|
||||
|
||||
watch(selectedAgentConfigId, (newConfigId, oldConfigId) => {
|
||||
if (oldConfigId === undefined || newConfigId === oldConfigId) return
|
||||
maybeInsertThreadConfigNotice()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => JSON.stringify(agentConfig.value || {}),
|
||||
(newConfigJson, oldConfigJson) => {
|
||||
@ -2122,7 +2001,7 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.chat-examples-input {
|
||||
.chat-greeting-input {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
|
||||
@ -2244,45 +2123,6 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.example-questions {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
|
||||
.example-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.example-chip {
|
||||
padding: 6px 12px;
|
||||
background: var(--gray-25);
|
||||
// border: 1px solid var(--gray-100);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray-700);
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
// background: var(--main-25);
|
||||
border-color: var(--main-200);
|
||||
color: var(--main-700);
|
||||
box-shadow: 0 0px 4px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-loading {
|
||||
padding: 0 50px;
|
||||
text-align: center;
|
||||
|
||||
@ -13,12 +13,57 @@
|
||||
@keydown="handleKeyDown"
|
||||
>
|
||||
<template #top>
|
||||
<div v-if="currentImage" class="input-top-stack">
|
||||
<div v-if="currentImage || previewAttachments.length" class="input-top-stack">
|
||||
<ImagePreviewComponent
|
||||
v-if="currentImage"
|
||||
:image-data="currentImage"
|
||||
@remove="handleImageRemoved"
|
||||
class="image-preview-wrapper"
|
||||
/>
|
||||
|
||||
<div v-if="previewAttachments.length" class="attachment-preview-list">
|
||||
<div
|
||||
v-for="attachment in previewImageAttachments"
|
||||
:key="attachment.fileId"
|
||||
class="attachment-preview-image"
|
||||
>
|
||||
<img
|
||||
:src="attachment.previewUrl"
|
||||
:alt="attachment.name"
|
||||
class="attachment-image-thumb"
|
||||
/>
|
||||
<button
|
||||
class="attachment-remove-btn"
|
||||
type="button"
|
||||
:aria-label="`移除附件 ${attachment.name}`"
|
||||
@click.stop="handleAttachmentRemoved(attachment)"
|
||||
>
|
||||
<X :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="attachment in previewFileAttachments"
|
||||
:key="attachment.fileId"
|
||||
class="attachment-file-card"
|
||||
>
|
||||
<div class="attachment-file-icon" :style="{ color: attachment.iconColor }">
|
||||
<component :is="attachment.icon" />
|
||||
</div>
|
||||
<div class="attachment-file-body">
|
||||
<div class="attachment-file-name" :title="attachment.name">{{ attachment.name }}</div>
|
||||
<div class="attachment-file-meta">{{ attachment.meta }}</div>
|
||||
</div>
|
||||
<button
|
||||
class="attachment-remove-btn"
|
||||
type="button"
|
||||
:aria-label="`移除附件 ${attachment.name}`"
|
||||
@click.stop="handleAttachmentRemoved(attachment)"
|
||||
>
|
||||
<X :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #options-left>
|
||||
@ -99,7 +144,8 @@ import { computed, ref, watch } from 'vue'
|
||||
import MessageInputComponent from '@/components/MessageInputComponent.vue'
|
||||
import ImagePreviewComponent from '@/components/ImagePreviewComponent.vue'
|
||||
import AttachmentOptionsComponent from '@/components/AttachmentOptionsComponent.vue'
|
||||
import { SquareCheck } from 'lucide-vue-next'
|
||||
import { SquareCheck, X } from 'lucide-vue-next'
|
||||
import { normalizeAttachmentPreviews } from '@/utils/file_utils'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
@ -120,10 +166,20 @@ const props = defineProps({
|
||||
todos: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'send', 'keydown', 'upload-attachment'])
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'send',
|
||||
'keydown',
|
||||
'upload-attachment',
|
||||
'remove-attachment'
|
||||
])
|
||||
|
||||
const inputRef = ref(null)
|
||||
const currentImage = ref(null)
|
||||
@ -139,6 +195,13 @@ const todoProgress = computed(() => {
|
||||
if (!totalTodoCount.value) return 0
|
||||
return Math.round((completedTodoCount.value / totalTodoCount.value) * 100)
|
||||
})
|
||||
const previewAttachments = computed(() => normalizeAttachmentPreviews(props.attachments))
|
||||
const previewImageAttachments = computed(() =>
|
||||
previewAttachments.value.filter((attachment) => attachment.isImage && attachment.previewUrl)
|
||||
)
|
||||
const previewFileAttachments = computed(() =>
|
||||
previewAttachments.value.filter((attachment) => !attachment.isImage || !attachment.previewUrl)
|
||||
)
|
||||
|
||||
watch(showTodoEntry, (visible) => {
|
||||
if (!visible) {
|
||||
@ -171,6 +234,10 @@ const handleImageRemoved = () => {
|
||||
currentImage.value = null
|
||||
}
|
||||
|
||||
const handleAttachmentRemoved = (attachment) => {
|
||||
emit('remove-attachment', attachment.raw)
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
emit('send', { image: currentImage.value })
|
||||
currentImage.value = null
|
||||
@ -229,6 +296,105 @@ const getTodoStatusLabel = (status) => {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.attachment-preview-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attachment-preview-image {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--gray-150);
|
||||
background: var(--gray-25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.attachment-image-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.attachment-file-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 220px;
|
||||
min-width: 0;
|
||||
padding: 10px 34px 10px 12px;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 12px;
|
||||
background: var(--gray-0);
|
||||
box-shadow: 0 1px 4px var(--shadow-0);
|
||||
}
|
||||
|
||||
.attachment-file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--main-700);
|
||||
background: var(--main-30);
|
||||
}
|
||||
|
||||
.attachment-file-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attachment-file-name {
|
||||
overflow: hidden;
|
||||
color: var(--gray-900);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachment-file-meta {
|
||||
margin-top: 2px;
|
||||
color: var(--gray-500);
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.attachment-remove-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
color: var(--gray-0);
|
||||
background: var(--gray-900);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-700);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框操作按钮通用样式(穿透到 slot 内容)
|
||||
:deep(.input-action-btn) {
|
||||
display: flex;
|
||||
@ -411,6 +577,10 @@ const getTodoStatusLabel = (status) => {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.attachment-file-card {
|
||||
width: min(220px, 100%);
|
||||
}
|
||||
|
||||
.todo-popover-card {
|
||||
width: min(320px, calc(100vw - 24px));
|
||||
padding: 12px;
|
||||
|
||||
@ -5,7 +5,14 @@
|
||||
>
|
||||
<img :src="`data:image/jpeg;base64,${message.image_content}`" alt="用户上传的图片" />
|
||||
</div>
|
||||
<div class="message-box" :class="[message.type, customClasses]">
|
||||
<div
|
||||
class="message-box"
|
||||
:class="[
|
||||
message.type,
|
||||
customClasses,
|
||||
{ 'has-attachments': message.type === 'human' && messageAttachments.length }
|
||||
]"
|
||||
>
|
||||
<!-- 用户消息 -->
|
||||
<div
|
||||
v-if="message.type === 'human'"
|
||||
@ -94,6 +101,35 @@
|
||||
<!-- 自定义内容 -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.type === 'human' && messageAttachments.length"
|
||||
class="human-message-attachments"
|
||||
>
|
||||
<div
|
||||
v-for="attachment in imageAttachments"
|
||||
:key="attachment.fileId"
|
||||
class="message-attachment-image"
|
||||
>
|
||||
<img :src="attachment.previewUrl" :alt="attachment.name" class="message-attachment-thumb" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="attachment in fileAttachments"
|
||||
:key="attachment.fileId"
|
||||
class="message-attachment-file"
|
||||
>
|
||||
<div class="message-attachment-icon" :style="{ color: attachment.iconColor }">
|
||||
<component :is="attachment.icon" />
|
||||
</div>
|
||||
<div class="message-attachment-body">
|
||||
<div class="message-attachment-name" :title="attachment.name">
|
||||
{{ attachment.name }}
|
||||
</div>
|
||||
<div class="message-attachment-meta">{{ attachment.meta }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -107,6 +143,7 @@ import { useAgentStore } from '@/stores/agent'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { MessageProcessor } from '@/utils/messageProcessor'
|
||||
import { normalizeAttachmentPreviews } from '@/utils/file_utils'
|
||||
|
||||
const props = defineProps({
|
||||
// 消息角色:'user'|'assistant'|'sent'|'received'
|
||||
@ -216,7 +253,17 @@ const getErrorMessage = computed(() => {
|
||||
const agentStore = useAgentStore()
|
||||
const { availableKnowledgeBases } = storeToRefs(agentStore)
|
||||
const infoStore = useInfoStore()
|
||||
// 提取消息来源
|
||||
const messageAttachments = computed(() =>
|
||||
normalizeAttachmentPreviews(props.message.extra_metadata?.attachments)
|
||||
)
|
||||
|
||||
const imageAttachments = computed(() =>
|
||||
messageAttachments.value.filter((attachment) => attachment.isImage && attachment.previewUrl)
|
||||
)
|
||||
const fileAttachments = computed(() =>
|
||||
messageAttachments.value.filter((attachment) => !attachment.isImage || !attachment.previewUrl)
|
||||
)
|
||||
|
||||
const messageSources = computed(() => {
|
||||
if (props.message.type === 'ai') {
|
||||
return MessageProcessor.extractSourcesFromMessage(props.message, availableKnowledgeBases.value)
|
||||
@ -317,6 +364,11 @@ const parsedData = computed(() => {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
&.human.has-attachments,
|
||||
&.sent.has-attachments {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.message-copy-btn {
|
||||
cursor: pointer;
|
||||
color: var(--gray-400);
|
||||
@ -460,6 +512,78 @@ const parsedData = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.human-message-attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
align-self: flex-end;
|
||||
max-width: 95%;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.message-attachment-image {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--gray-0);
|
||||
}
|
||||
|
||||
.message-attachment-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-attachment-file {
|
||||
width: 220px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 0.625rem;
|
||||
background: var(--gray-0);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.message-attachment-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--main-color);
|
||||
background: var(--main-50);
|
||||
}
|
||||
|
||||
.message-attachment-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-attachment-name {
|
||||
overflow: hidden;
|
||||
color: var(--gray-900);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-attachment-meta {
|
||||
margin-top: 0.125rem;
|
||||
color: var(--gray-500);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.retry-hint {
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
|
||||
@ -151,14 +151,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
agentId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
agentConfigId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
panelRatio: {
|
||||
type: Number,
|
||||
default: 0.35
|
||||
@ -341,12 +333,7 @@ const getFileName = (fileItem) => {
|
||||
}
|
||||
|
||||
const loadDirectoryChildren = async (directoryPath) => {
|
||||
const res = await getViewerFileSystemTree(
|
||||
props.threadId,
|
||||
directoryPath,
|
||||
props.agentId,
|
||||
props.agentConfigId
|
||||
)
|
||||
const res = await getViewerFileSystemTree(props.threadId, directoryPath)
|
||||
return sortEntries(res?.entries || []).map((entry) => createTreeNode(entry))
|
||||
}
|
||||
|
||||
@ -361,12 +348,7 @@ const refreshFileSystem = async () => {
|
||||
filesystemError.value = ''
|
||||
|
||||
try {
|
||||
const res = await getViewerFileSystemTree(
|
||||
props.threadId,
|
||||
'/',
|
||||
props.agentId,
|
||||
props.agentConfigId
|
||||
)
|
||||
const res = await getViewerFileSystemTree(props.threadId, '/')
|
||||
if (res?.entries) {
|
||||
const displayRootEntry = res.entries.find(
|
||||
(entry) => entry?.is_dir && entry.name === DISPLAY_ROOT_DIRECTORY_NAME
|
||||
@ -439,22 +421,14 @@ const loadActivePreview = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getViewerFileContent(
|
||||
props.threadId,
|
||||
filePath,
|
||||
props.agentId,
|
||||
props.agentConfigId
|
||||
)
|
||||
const res = await getViewerFileContent(props.threadId, filePath)
|
||||
if (requestSeq !== previewRequestSeq) return
|
||||
|
||||
const previewType = res?.preview_type || 'text'
|
||||
let previewUrl = ''
|
||||
|
||||
if ((previewType === 'image' || previewType === 'pdf') && res?.supported) {
|
||||
const response = await downloadViewerFile(
|
||||
props.threadId,
|
||||
filePath,
|
||||
props.agentId,
|
||||
props.agentConfigId
|
||||
)
|
||||
const response = await downloadViewerFile(props.threadId, filePath)
|
||||
const blob = await response.blob()
|
||||
previewUrl = window.URL.createObjectURL(blob)
|
||||
}
|
||||
@ -525,7 +499,7 @@ const confirmDeleteNode = (node) => {
|
||||
deletingPaths.value = nextDeletingPaths
|
||||
|
||||
try {
|
||||
await deleteViewerFile(props.threadId, node.key, props.agentId, props.agentConfigId)
|
||||
await deleteViewerFile(props.threadId, node.key)
|
||||
dynamicTreeData.value = removeTreeNode(dynamicTreeData.value, node.key)
|
||||
pruneTreeStateAfterDelete(node.key)
|
||||
message.success(isDirectory ? '文件夹删除成功' : '文件删除成功')
|
||||
@ -545,12 +519,7 @@ const downloadFile = async (fileItem) => {
|
||||
if (!props.threadId || !fileItem?.path) return
|
||||
|
||||
try {
|
||||
const response = await downloadViewerFile(
|
||||
props.threadId,
|
||||
fileItem.path,
|
||||
props.agentId,
|
||||
props.agentConfigId
|
||||
)
|
||||
const response = await downloadViewerFile(props.threadId, fileItem.path)
|
||||
const blob = await response.blob()
|
||||
const contentDisposition =
|
||||
response.headers.get('Content-Disposition') || response.headers.get('content-disposition')
|
||||
@ -650,7 +619,7 @@ onUnmounted(() => {
|
||||
revokeCurrentPreviewUrl()
|
||||
})
|
||||
|
||||
watch([() => props.threadId, () => props.agentId, () => props.agentConfigId], ([threadId]) => {
|
||||
watch(() => props.threadId, (threadId) => {
|
||||
if (threadId) {
|
||||
refreshFileSystem()
|
||||
} else {
|
||||
@ -661,11 +630,7 @@ watch([() => props.threadId, () => props.agentId, () => props.agentConfigId], ([
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
[() => props.threadId, () => props.agentId, () => props.agentConfigId, () => props.activePreviewPath],
|
||||
loadActivePreview,
|
||||
{ immediate: true }
|
||||
)
|
||||
watch([() => props.threadId, () => props.activePreviewPath], loadActivePreview, { immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.activePreviewPath,
|
||||
|
||||
@ -1,43 +1,8 @@
|
||||
<template>
|
||||
<div class="agent-config-sidebar" :class="{ open: isOpen }">
|
||||
<!-- 侧边栏头部 -->
|
||||
<div class="sidebar-header">
|
||||
<div class="header-top-row">
|
||||
<div v-if="selectedAgentId" class="config-title-row">
|
||||
<span class="config-title-text">{{ currentConfigName }}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-tooltip
|
||||
v-if="!isEmptyConfig"
|
||||
:title="isCurrentDefault ? '当前已是默认配置' : '设为默认配置'"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="header-icon-action"
|
||||
:class="{ 'is-default': isCurrentDefault }"
|
||||
@click="setAsDefault"
|
||||
>
|
||||
<Star :size="18" :fill="isCurrentDefault ? 'currentColor' : 'none'" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<button type="button" @click="closeSidebar" class="header-icon-action">
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏内容 -->
|
||||
<div class="sidebar-content">
|
||||
<div class="agent-runtime-config-form">
|
||||
<div class="runtime-config-content">
|
||||
<div class="agent-info" v-if="selectedAgent">
|
||||
<!-- <div class="agent-basic-info">
|
||||
<p class="agent-description">{{ selectedAgent.description }}</p>
|
||||
</div> -->
|
||||
|
||||
<!-- <a-divider /> -->
|
||||
|
||||
<div class="config-segment" v-if="!isEmptyConfig">
|
||||
<div class="config-segment" v-if="props.showSegmented && !isEmptyConfig">
|
||||
<a-segmented v-model:value="currentSegment" :options="segmentOptions" block />
|
||||
</div>
|
||||
|
||||
@ -56,6 +21,11 @@
|
||||
class="config-alert"
|
||||
/>
|
||||
<!-- 统一显示所有配置项 -->
|
||||
<a-empty
|
||||
v-if="isCurrentSegmentEmpty"
|
||||
description="暂无配置项"
|
||||
class="config-empty"
|
||||
/>
|
||||
<template v-for="(value, key) in filteredConfigurableItems" :key="key">
|
||||
<a-form-item :label="getConfigLabel(key, value)" :name="key" class="config-item">
|
||||
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
||||
@ -277,32 +247,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 固定在底部的操作按钮 -->
|
||||
<div class="sidebar-footer" v-if="selectedAgentId">
|
||||
<div class="form-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
class="footer-main-btn save-btn"
|
||||
:class="{ changed: agentStore.hasConfigChanges }"
|
||||
:disabled="isSavingConfig || isEmptyConfig"
|
||||
>
|
||||
保存
|
||||
</a-button>
|
||||
|
||||
<a-tooltip v-if="!isEmptyConfig" title="删除配置">
|
||||
<button
|
||||
type="button"
|
||||
class="footer-icon-btn"
|
||||
@click="confirmDeleteConfig"
|
||||
:disabled="isDeletingConfig"
|
||||
>
|
||||
<Trash2 :size="16" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:open="selectionModalOpen"
|
||||
:title="`选择${configurableItems[currentConfigKey]?.name || '项目'}`"
|
||||
@ -418,10 +362,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { X, Trash2, Check, Plus, Search, Star, RotateCw, Settings } from 'lucide-vue-next'
|
||||
import { Check, Plus, Search, RotateCw, Settings } from 'lucide-vue-next'
|
||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import {
|
||||
@ -433,52 +377,21 @@ import {
|
||||
} from '@/utils/agentConfigUtils'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
segment: {
|
||||
type: String,
|
||||
default: 'model'
|
||||
},
|
||||
disabled: {
|
||||
showSegmented: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// Store 管理
|
||||
const agentStore = useAgentStore()
|
||||
const router = useRouter()
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
if (selectedAgentId.value) {
|
||||
agentStore.fetchAgentConfigs(selectedAgentId.value).catch((error) => {
|
||||
console.error('刷新智能体配置列表失败:', error)
|
||||
})
|
||||
try {
|
||||
await agentStore.fetchAgentDetail(selectedAgentId.value, true)
|
||||
} catch (error) {
|
||||
console.error('刷新智能体配置项失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
selectedAgent,
|
||||
selectedAgentId,
|
||||
selectedAgentConfigId,
|
||||
selectedConfigSummary,
|
||||
agentConfigs,
|
||||
agentConfig,
|
||||
configurableItems
|
||||
} = storeToRefs(agentStore)
|
||||
const { selectedAgent, selectedAgentId, agentConfig, configurableItems } = storeToRefs(agentStore)
|
||||
|
||||
// console.log(availableTools.value)
|
||||
|
||||
@ -496,24 +409,15 @@ const segmentOptions = [
|
||||
{ label: '工具', value: 'tools' },
|
||||
{ label: '其他', value: 'other' }
|
||||
]
|
||||
const activeSegment = computed(() => (props.showSegmented ? currentSegment.value : props.segment))
|
||||
const isToolResourceKind = (kind) => isDefaultAllAgentResourceKind(kind)
|
||||
|
||||
const isEmptyConfig = computed(() => {
|
||||
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0
|
||||
})
|
||||
|
||||
const isCurrentDefault = computed(() => {
|
||||
return !!selectedConfigSummary.value?.is_default
|
||||
})
|
||||
|
||||
const currentConfigName = computed(() => {
|
||||
return selectedConfigSummary.value?.name || '当前配置'
|
||||
})
|
||||
|
||||
const isReadOnlyConfig = computed(() => false)
|
||||
|
||||
const isSavingConfig = ref(false)
|
||||
const isDeletingConfig = ref(false)
|
||||
const canManageCurrentAgent = computed(() => !!selectedAgent.value?.can_manage)
|
||||
const isReadOnlyConfig = computed(() => !canManageCurrentAgent.value)
|
||||
|
||||
const segmentConfigKeys = computed(() => {
|
||||
const keys = Object.keys(configurableItems.value)
|
||||
@ -535,7 +439,7 @@ const segmentConfigKeys = computed(() => {
|
||||
|
||||
const filteredConfigurableItems = computed(() => {
|
||||
if (isEmptyConfig.value) return {}
|
||||
const keys = segmentConfigKeys.value[currentSegment.value] || []
|
||||
const keys = segmentConfigKeys.value[activeSegment.value] || []
|
||||
const filtered = {}
|
||||
keys.forEach((key) => {
|
||||
filtered[key] = configurableItems.value[key]
|
||||
@ -543,6 +447,10 @@ const filteredConfigurableItems = computed(() => {
|
||||
return filtered
|
||||
})
|
||||
|
||||
const isCurrentSegmentEmpty = computed(
|
||||
() => !isEmptyConfig.value && Object.keys(filteredConfigurableItems.value).length === 0
|
||||
)
|
||||
|
||||
// 判断是否为需要跳转的配置类型
|
||||
const isToolsKind = (kind) => {
|
||||
return isToolResourceKind(kind)
|
||||
@ -634,10 +542,6 @@ const updateConfigValue = (key, value) => {
|
||||
})
|
||||
}
|
||||
|
||||
const closeSidebar = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const getConfigLabel = (key, value) => {
|
||||
// console.log(configurableItems)
|
||||
if (value.description && value.name !== key) {
|
||||
@ -799,177 +703,14 @@ const validateAndFilterConfig = () => {
|
||||
return validatedConfig
|
||||
}
|
||||
|
||||
// 配置保存和重置
|
||||
const saveConfig = async () => {
|
||||
if (!selectedAgentId.value) {
|
||||
message.error('没有选择智能体')
|
||||
return
|
||||
}
|
||||
|
||||
if (!agentStore.hasConfigChanges) return
|
||||
|
||||
try {
|
||||
isSavingConfig.value = true
|
||||
// 验证和过滤配置
|
||||
const validatedConfig = validateAndFilterConfig()
|
||||
|
||||
// 如果配置有变化,先更新到store
|
||||
if (JSON.stringify(validatedConfig) !== JSON.stringify(agentConfig.value)) {
|
||||
agentStore.updateAgentConfig(validatedConfig)
|
||||
message.info('检测到无效配置项,已自动过滤')
|
||||
}
|
||||
|
||||
await agentStore.saveAgentConfig()
|
||||
message.success('配置已保存到服务器')
|
||||
} catch (error) {
|
||||
console.error('保存配置到服务器出错:', error)
|
||||
message.error('保存配置到服务器失败')
|
||||
} finally {
|
||||
isSavingConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const setAsDefault = async () => {
|
||||
if (!selectedAgentId.value || !selectedAgentConfigId.value) return
|
||||
try {
|
||||
await agentStore.setSelectedAgentConfigDefault()
|
||||
message.success('已设为默认配置')
|
||||
} catch (error) {
|
||||
console.error('设置默认配置出错:', error)
|
||||
message.error('设置默认配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteConfig = async () => {
|
||||
if (!selectedAgentId.value || !selectedAgentConfigId.value) return
|
||||
|
||||
const currentName = selectedConfigSummary.value?.name || '当前配置'
|
||||
const list = agentConfigs.value[selectedAgentId.value] || []
|
||||
const content =
|
||||
list.length <= 1
|
||||
? `将删除「${currentName}」。删除后系统会自动创建一个新的默认配置。`
|
||||
: `将删除「${currentName}」。`
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除配置?',
|
||||
content,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
isDeletingConfig.value = true
|
||||
try {
|
||||
await agentStore.deleteSelectedAgentConfigProfile()
|
||||
message.success('配置已删除')
|
||||
} catch (error) {
|
||||
console.error('删除配置出错:', error)
|
||||
message.error('删除配置失败')
|
||||
} finally {
|
||||
isDeletingConfig.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({ validateAndFilterConfig })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.agent-config-sidebar {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 100vh;
|
||||
.agent-runtime-config-form {
|
||||
background: var(--gray-0);
|
||||
border-left: 1px solid var(--gray-200);
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.open {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: var(--header-height);
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
background: var(--gray-0);
|
||||
flex-shrink: 0;
|
||||
min-width: 360px;
|
||||
z-index: 10;
|
||||
|
||||
.header-top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-title-text {
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.header-icon-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--gray-600);
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--main-600);
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
&.is-default,
|
||||
&.is-default:hover:not(:disabled) {
|
||||
color: var(--color-warning-500);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background: transparent;
|
||||
color: var(--gray-400);
|
||||
|
||||
&.is-default {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
.runtime-config-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px 8px;
|
||||
@ -1040,7 +781,8 @@ const confirmDeleteConfig = async () => {
|
||||
}
|
||||
|
||||
.config-form {
|
||||
.config-alert {
|
||||
.config-alert,
|
||||
.config-empty {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@ -1137,87 +879,6 @@ const confirmDeleteConfig = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--gray-100);
|
||||
background: var(--gray-0);
|
||||
// min-width: 400px;
|
||||
z-index: 10;
|
||||
flex-shrink: 0; // Ensure footer doesn't shrink
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.footer-main-btn {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: var(--gray-100);
|
||||
border: 1px solid var(--gray-200);
|
||||
color: var(--gray-600);
|
||||
|
||||
&.changed {
|
||||
background-color: var(--main-color);
|
||||
color: var(--gray-0);
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--gray-100);
|
||||
border-color: var(--gray-200);
|
||||
color: var(--gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-icon-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 8px;
|
||||
background: var(--gray-0);
|
||||
color: var(--gray-500);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--color-error-700);
|
||||
border-color: var(--color-error-100);
|
||||
background: var(--color-error-50);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--gray-400);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择器样式
|
||||
@ -1298,11 +959,12 @@ const confirmDeleteConfig = async () => {
|
||||
|
||||
.options-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
@ -1343,11 +1005,14 @@ const confirmDeleteConfig = async () => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.option-indicator {
|
||||
@ -1576,16 +1241,4 @@ const confirmDeleteConfig = async () => {
|
||||
.selection-search .inline-action-btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 768px) {
|
||||
.agent-config-sidebar.open {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-header,
|
||||
.sidebar-content {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -49,22 +49,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="setting-row">
|
||||
<div class="col-item">
|
||||
<div class="setting-label">默认智能体</div>
|
||||
<div class="setting-content">
|
||||
<a-select
|
||||
class="agent-select"
|
||||
:value="agentStore.defaultAgentId"
|
||||
:options="agentOptions"
|
||||
:loading="isSettingDefaultAgent"
|
||||
:disabled="isSettingDefaultAgent || !agentOptions.length"
|
||||
placeholder="请选择默认智能体"
|
||||
@change="handleDefaultAgentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="userStore.isSuperAdmin">
|
||||
@ -174,10 +158,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { Globe } from 'lucide-vue-next'
|
||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||
@ -185,16 +167,8 @@ import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue'
|
||||
import RerankModelSelector from '@/components/RerankModelSelector.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const agentStore = useAgentStore()
|
||||
const userStore = useUserStore()
|
||||
const items = computed(() => configStore.config?._config_items || {})
|
||||
const isSettingDefaultAgent = ref(false)
|
||||
const agentOptions = computed(() =>
|
||||
(agentStore.agents || []).map((agent) => ({
|
||||
label: agent.name || 'Unknown',
|
||||
value: agent.id
|
||||
}))
|
||||
)
|
||||
|
||||
const handleChange = (key, e) => {
|
||||
configStore.setConfigValue(key, e)
|
||||
@ -218,30 +192,9 @@ const handleContentGuardModelSelect = (spec) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDefaultAgentChange = async (agentId) => {
|
||||
if (!agentId || agentId === agentStore.defaultAgentId) return
|
||||
|
||||
isSettingDefaultAgent.value = true
|
||||
try {
|
||||
await agentStore.setDefaultAgent(agentId)
|
||||
message.success('默认智能体已更新')
|
||||
} finally {
|
||||
isSettingDefaultAgent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openLink = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!agentStore.agents.length) {
|
||||
await agentStore.fetchAgents()
|
||||
}
|
||||
if (!agentStore.defaultAgentId) {
|
||||
await agentStore.fetchDefaultAgent()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@ -161,7 +161,7 @@ watch(showModal, (isOpen) => {
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useDatabaseStore } from '@/stores/database'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { isBuiltinAgent, useAgentStore } from '@/stores/agent'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import {
|
||||
@ -484,7 +484,6 @@ const printAgentConfig = async () => {
|
||||
console.log('Store 状态:', {
|
||||
isInitialized: agentStore.isInitialized,
|
||||
selectedAgentId: agentStore.selectedAgentId,
|
||||
defaultAgentId: agentStore.defaultAgentId,
|
||||
agentCount: agentStore.agentsList.length,
|
||||
loadingStates: {
|
||||
isLoadingAgents: agentStore.isLoadingAgents,
|
||||
@ -505,7 +504,7 @@ const printAgentConfig = async () => {
|
||||
if (agentStore.selectedAgent) {
|
||||
console.log('当前选中智能体:', {
|
||||
agent: toRaw(agentStore.selectedAgent),
|
||||
isDefault: agentStore.isDefaultAgent,
|
||||
isBuiltin: isBuiltinAgent(agentStore.selectedAgent),
|
||||
configurableItemsCount: Object.keys(agentStore.configurableItems).length
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div class="share-config-form">
|
||||
<div class="share-mode-cards" :class="`active-${config.access_level}`" role="radiogroup" aria-label="共享设置">
|
||||
<div class="share-config-form" :class="{ disabled }">
|
||||
<div
|
||||
class="share-mode-cards"
|
||||
:class="`active-${config.access_level}`"
|
||||
role="radiogroup"
|
||||
aria-label="共享设置"
|
||||
>
|
||||
<div
|
||||
v-for="option in shareModeOptions"
|
||||
:key="option.value"
|
||||
@ -8,7 +13,7 @@
|
||||
class="share-mode-card"
|
||||
:class="{ active: config.access_level === option.value }"
|
||||
:aria-checked="config.access_level === option.value"
|
||||
:tabindex="config.access_level === option.value ? 0 : -1"
|
||||
:tabindex="!disabled && config.access_level === option.value ? 0 : -1"
|
||||
@click="setAccessLevel(option.value)"
|
||||
@keydown.enter.prevent="setAccessLevel(option.value)"
|
||||
@keydown.space.prevent="setAccessLevel(option.value)"
|
||||
@ -29,6 +34,7 @@
|
||||
size="small"
|
||||
class="select-action lucide-icon-btn"
|
||||
:aria-label="option.value === 'department' ? '选择部门' : '选择用户'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<UserPlus class="select-action-icon" :size="14" />
|
||||
<span class="access-count">{{ getAccessCount(option.value) }}</span>
|
||||
@ -88,6 +94,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-alert v-if="disabled && disabledReason" type="info" show-icon class="share-disabled-alert" :message="disabledReason" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -103,7 +110,7 @@ const departments = ref([])
|
||||
const users = ref([])
|
||||
const syncingFromProps = ref(false)
|
||||
|
||||
const shareModeOptions = [
|
||||
const baseShareModeOptions = [
|
||||
{
|
||||
value: 'global',
|
||||
title: '全局共享',
|
||||
@ -137,6 +144,18 @@ const props = defineProps({
|
||||
autoSelectUserDept: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabledReason: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
allowedAccessLevels: {
|
||||
type: Array,
|
||||
default: () => ['global', 'department', 'user']
|
||||
}
|
||||
})
|
||||
|
||||
@ -159,6 +178,13 @@ const currentDepartmentId = computed(() => {
|
||||
})
|
||||
|
||||
const currentUserUid = computed(() => userStore.uid || '')
|
||||
const normalizedAllowedAccessLevels = computed(() => {
|
||||
const allowed = props.allowedAccessLevels.filter((level) => ['global', 'department', 'user'].includes(level))
|
||||
return allowed.length ? allowed : ['global']
|
||||
})
|
||||
const shareModeOptions = computed(() =>
|
||||
baseShareModeOptions.filter((option) => normalizedAllowedAccessLevels.value.includes(option.value))
|
||||
)
|
||||
|
||||
const departmentOptions = computed(() =>
|
||||
departments.value.map((dept) => {
|
||||
@ -220,10 +246,12 @@ const normalizeActiveConfig = () => {
|
||||
|
||||
const initConfig = () => {
|
||||
syncingFromProps.value = true
|
||||
const accessLevel = ['global', 'department', 'user'].includes(props.modelValue?.access_level)
|
||||
const requestedAccessLevel = ['global', 'department', 'user'].includes(props.modelValue?.access_level)
|
||||
? props.modelValue.access_level
|
||||
: 'global'
|
||||
config.access_level = accessLevel
|
||||
config.access_level = normalizedAllowedAccessLevels.value.includes(requestedAccessLevel)
|
||||
? requestedAccessLevel
|
||||
: normalizedAllowedAccessLevels.value[0]
|
||||
config.department_ids = normalizeDepartmentIds(props.modelValue?.department_ids)
|
||||
config.user_uids = normalizeUserUids(props.modelValue?.user_uids)
|
||||
normalizeActiveConfig()
|
||||
@ -241,6 +269,7 @@ const emitConfig = () => {
|
||||
}
|
||||
|
||||
const setAccessLevel = (accessLevel) => {
|
||||
if (props.disabled || !normalizedAllowedAccessLevels.value.includes(accessLevel)) return
|
||||
if (config.access_level === accessLevel) return
|
||||
config.access_level = accessLevel
|
||||
normalizeActiveConfig()
|
||||
@ -273,6 +302,7 @@ const isSelected = (accessLevel, value) => {
|
||||
}
|
||||
|
||||
const toggleSelection = (accessLevel, value, checked) => {
|
||||
if (props.disabled) return
|
||||
if (accessLevel === 'department') {
|
||||
const departmentId = Number(value)
|
||||
const selected = checked
|
||||
@ -316,6 +346,8 @@ watch(
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(normalizedAllowedAccessLevels, () => initConfig())
|
||||
|
||||
watch(
|
||||
config,
|
||||
() => {
|
||||
@ -383,6 +415,15 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.share-disabled-alert {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&.disabled .share-mode-card {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.share-mode-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@ -26,8 +26,9 @@
|
||||
<ExtensionCardGrid>
|
||||
<InfoCard
|
||||
v-for="agent in filteredEnabledSubAgents"
|
||||
:key="agent.name"
|
||||
:key="agent.slug"
|
||||
:title="agent.name"
|
||||
:subtitle="agent.slug"
|
||||
:description="agent.description || '暂无描述'"
|
||||
:default-icon="BotIcon"
|
||||
:tags="agent.is_builtin ? [{ name: '内置' }] : [{ name: '添加' }]"
|
||||
@ -41,8 +42,9 @@
|
||||
<ExtensionCardGrid v-if="filteredDisabledSubAgents.length">
|
||||
<InfoCard
|
||||
v-for="agent in filteredDisabledSubAgents"
|
||||
:key="agent.name"
|
||||
:key="agent.slug"
|
||||
:title="agent.name"
|
||||
:subtitle="agent.slug"
|
||||
:description="agent.description || '暂无描述'"
|
||||
:default-icon="BotIcon"
|
||||
:tags="agent.is_builtin ? [{ name: '内置' }] : [{ name: '添加' }]"
|
||||
|
||||
@ -30,11 +30,11 @@
|
||||
<ExtensionCardGrid v-else>
|
||||
<InfoCard
|
||||
v-for="tool in filteredTools"
|
||||
:key="tool.id"
|
||||
:key="getToolSlug(tool)"
|
||||
:title="tool.name"
|
||||
:subtitle="tool.id"
|
||||
:subtitle="getToolSlug(tool)"
|
||||
:description="tool.description || '无描述'"
|
||||
:default-icon="getToolIcon(tool.id) || WrenchIcon"
|
||||
:default-icon="getToolIcon(getToolSlug(tool)) || WrenchIcon"
|
||||
:tags="toolTags(tool)"
|
||||
@click="selectTool(tool)"
|
||||
>
|
||||
@ -133,6 +133,8 @@ const categories = ['buildin', 'knowledge', 'mysql', 'debug']
|
||||
const categoryLabels = { buildin: '内置工具', knowledge: '知识库', mysql: 'MySQL', debug: '调试' }
|
||||
const categoryColors = { buildin: 'blue', knowledge: 'purple', mysql: 'green', debug: 'orange' }
|
||||
|
||||
const getToolSlug = (tool) => tool?.slug || tool?.id || ''
|
||||
|
||||
const toolTags = (tool) => {
|
||||
const tags = []
|
||||
if (tool.category) {
|
||||
@ -161,7 +163,7 @@ const filteredTools = computed(() => {
|
||||
result = result.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(q) ||
|
||||
t.id.toLowerCase().includes(q) ||
|
||||
getToolSlug(t).toLowerCase().includes(q) ||
|
||||
t.description?.toLowerCase().includes(q) ||
|
||||
t.config_guide?.toLowerCase().includes(q)
|
||||
)
|
||||
|
||||
@ -50,7 +50,7 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
||||
threadId: null
|
||||
})
|
||||
|
||||
const handleApproval = async (answer, currentAgentId, agentConfigId) => {
|
||||
const handleApproval = async (answer) => {
|
||||
const threadId = approvalState.threadId
|
||||
if (!threadId) {
|
||||
message.error('无效的提问请求')
|
||||
@ -65,12 +65,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
||||
return
|
||||
}
|
||||
|
||||
if (!agentConfigId) {
|
||||
message.error('缺少智能体配置,请重新选择配置后重试')
|
||||
approvalState.showModal = false
|
||||
return
|
||||
}
|
||||
|
||||
approvalState.showModal = false
|
||||
|
||||
if (threadState.streamAbortController) {
|
||||
@ -83,8 +77,7 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
||||
threadState.streamAbortController = new AbortController()
|
||||
|
||||
const requestBody = {
|
||||
thread_id: threadId,
|
||||
config: { agent_config_id: agentConfigId }
|
||||
thread_id: threadId
|
||||
}
|
||||
|
||||
if (approvalState.status === 'human_approval_required') {
|
||||
|
||||
@ -4,85 +4,67 @@ import { agentApi, databaseApi, mcpApi, skillApi } from '@/apis'
|
||||
import { isDefaultAllAgentResourceKind } from '@/utils/agentConfigUtils'
|
||||
import { handleChatError } from '@/utils/errorHandler'
|
||||
|
||||
const CHATBOT_AGENT_ID = 'ChatbotAgent'
|
||||
function normalizeAgent(agent) {
|
||||
const agentId = agent?.agent_id || agent?.slug || agent?.id
|
||||
return agentId ? { ...agent, id: agentId, agent_id: agentId, slug: agent?.slug || agentId } : agent
|
||||
}
|
||||
|
||||
export const BUILTIN_AGENT_ID = 'default-chatbot'
|
||||
|
||||
export function isBuiltinAgent(agent) {
|
||||
return agent?.is_builtin || agent?.id === BUILTIN_AGENT_ID || agent?.slug === BUILTIN_AGENT_ID
|
||||
}
|
||||
|
||||
function sortAgents(agents) {
|
||||
return [...agents].sort((a, b) => {
|
||||
const isAChatbotAgent = a.id === CHATBOT_AGENT_ID
|
||||
const isBChatbotAgent = b.id === CHATBOT_AGENT_ID
|
||||
|
||||
if (isAChatbotAgent && !isBChatbotAgent) return -1
|
||||
if (!isAChatbotAgent && isBChatbotAgent) return 1
|
||||
return a.id.localeCompare(b.id)
|
||||
if (isBuiltinAgent(a) !== isBuiltinAgent(b)) return isBuiltinAgent(a) ? -1 : 1
|
||||
return String(a.name || a.id).localeCompare(String(b.name || b.id), 'zh-CN')
|
||||
})
|
||||
}
|
||||
|
||||
function getPreferredAgentId(agents, persistedId) {
|
||||
if (persistedId && agents.some((agent) => agent.id === persistedId)) return persistedId
|
||||
return agents.find(isBuiltinAgent)?.id || agents[0]?.id || null
|
||||
}
|
||||
|
||||
function extractContext(agent) {
|
||||
const configJson = agent?.config_json || {}
|
||||
return { ...(configJson.context || configJson || {}) }
|
||||
}
|
||||
|
||||
export const useAgentStore = defineStore(
|
||||
'agent',
|
||||
() => {
|
||||
// ==================== 状态定义 ====================
|
||||
// 智能体相关状态
|
||||
const agents = ref([])
|
||||
const selectedAgentId = ref(null)
|
||||
const defaultAgentId = ref(null)
|
||||
|
||||
// 资源相关状态
|
||||
const availableKnowledgeBases = ref([])
|
||||
const availableMcps = ref([])
|
||||
const availableSkills = ref([])
|
||||
|
||||
// 智能体配置相关状态
|
||||
const agentConfig = ref({})
|
||||
const originalAgentConfig = ref({})
|
||||
const agentConfigs = ref({})
|
||||
const selectedAgentConfigId = ref(null)
|
||||
const agentDetails = ref({})
|
||||
|
||||
// 智能体详情相关状态
|
||||
const agentDetails = ref({}) // 存储每个智能体的详细信息(含 configurable_items)
|
||||
|
||||
// 加载状态
|
||||
const isLoadingAgents = ref(false)
|
||||
const isLoadingConfig = ref(false)
|
||||
const isLoadingAgentConfigs = ref(false)
|
||||
const isLoadingAgentDetail = ref(false)
|
||||
|
||||
// 错误状态
|
||||
const error = ref(null)
|
||||
|
||||
// 初始化状态
|
||||
const isInitialized = ref(false)
|
||||
const isInitializing = ref(false)
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
const selectedAgent = computed(() =>
|
||||
selectedAgentId.value ? agents.value.find((a) => a.id === selectedAgentId.value) : null
|
||||
)
|
||||
|
||||
const defaultAgent = computed(() =>
|
||||
defaultAgentId.value
|
||||
? agents.value.find((a) => a.id === defaultAgentId.value)
|
||||
: agents.value[0]
|
||||
)
|
||||
const selectedAgent = computed(() => {
|
||||
const agentId = selectedAgentId.value
|
||||
return agentId ? agentDetails.value[agentId] || agents.value.find((a) => a.id === agentId) || null : null
|
||||
})
|
||||
|
||||
const agentsList = computed(() => agents.value)
|
||||
|
||||
const isDefaultAgent = computed(() => selectedAgentId.value === defaultAgentId.value)
|
||||
|
||||
const configurableItems = computed(() => {
|
||||
const agentId = selectedAgentId.value
|
||||
if (
|
||||
!agentId ||
|
||||
!agentDetails.value[agentId] ||
|
||||
!agentDetails.value[agentId].configurable_items
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const agentConfigurableItems = agentDetails.value[agentId].configurable_items
|
||||
const items = { ...agentConfigurableItems }
|
||||
const items = { ...(selectedAgent.value?.configurable_items || {}) }
|
||||
Object.keys(items).forEach((key) => {
|
||||
const item = items[key]
|
||||
if (item && item.x_oap_ui_config) {
|
||||
if (item?.x_oap_ui_config) {
|
||||
items[key] = { ...item, ...item.x_oap_ui_config }
|
||||
delete items[key].x_oap_ui_config
|
||||
}
|
||||
@ -90,27 +72,11 @@ export const useAgentStore = defineStore(
|
||||
return items
|
||||
})
|
||||
|
||||
// 工具相关状态
|
||||
const availableTools = computed(() => {
|
||||
return configurableItems.value.tools?.options || []
|
||||
})
|
||||
|
||||
const availableTools = computed(() => configurableItems.value.tools?.options || [])
|
||||
const hasConfigChanges = computed(
|
||||
() => JSON.stringify(agentConfig.value) !== JSON.stringify(originalAgentConfig.value)
|
||||
)
|
||||
|
||||
const selectedConfigSummary = computed(() => {
|
||||
const agentId = selectedAgentId.value
|
||||
const configId = selectedAgentConfigId.value
|
||||
if (!agentId || !configId) return null
|
||||
const list = agentConfigs.value[agentId] || []
|
||||
return list.find((c) => c.id === configId) || null
|
||||
})
|
||||
|
||||
// ==================== 方法 ====================
|
||||
/**
|
||||
* 获取可提及的资源(知识库、MCP、Skills)
|
||||
*/
|
||||
async function fetchMentionResources() {
|
||||
try {
|
||||
const [dbsRes, mcpsRes, skillsRes] = await Promise.all([
|
||||
@ -126,57 +92,16 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 store
|
||||
*/
|
||||
async function initialize() {
|
||||
if (isInitialized.value) return
|
||||
|
||||
// 防止并发初始化
|
||||
if (isInitializing.value) return
|
||||
if (isInitialized.value || isInitializing.value) return
|
||||
isInitializing.value = true
|
||||
|
||||
try {
|
||||
await fetchAgents()
|
||||
await fetchDefaultAgent()
|
||||
await fetchMentionResources()
|
||||
await Promise.all([fetchAgents(), fetchMentionResources()])
|
||||
|
||||
if (!selectedAgent.value) {
|
||||
if (defaultAgent.value) {
|
||||
await selectAgent(defaultAgentId.value)
|
||||
} else if (agents.value.length > 0) {
|
||||
const firstAgentId = agents.value[0].id
|
||||
await selectAgent(firstAgentId)
|
||||
}
|
||||
} else {
|
||||
console.log('Condition FALSE: Persisted selected agent is valid. Keeping it.')
|
||||
// 确保已缓存的智能体详细信息存在
|
||||
if (selectedAgentId.value && !agentDetails.value[selectedAgentId.value]) {
|
||||
try {
|
||||
await fetchAgentDetail(selectedAgentId.value)
|
||||
} catch (err) {
|
||||
console.warn(`Failed to fetch agent detail for ${selectedAgentId.value}:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedAgentId.value) {
|
||||
try {
|
||||
await fetchAgentConfigs(selectedAgentId.value)
|
||||
const list = agentConfigs.value[selectedAgentId.value] || []
|
||||
const persistedId = selectedAgentConfigId.value
|
||||
const persistedItem = persistedId ? list.find((c) => c.id === persistedId) : null
|
||||
const defaultItem = list.find((c) => c.is_default) || list[0]
|
||||
const pickId = (persistedItem || defaultItem)?.id || null
|
||||
selectedAgentConfigId.value = pickId
|
||||
if (pickId) {
|
||||
await loadAgentConfig(selectedAgentId.value, pickId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to init agent configs for ${selectedAgentId.value}:`, err)
|
||||
}
|
||||
}
|
||||
const targetAgentId = getPreferredAgentId(agents.value, selectedAgentId.value)
|
||||
if (targetAgentId) {
|
||||
await selectAgent(targetAgentId)
|
||||
}
|
||||
|
||||
isInitialized.value = true
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize agent store:', err)
|
||||
@ -187,16 +112,12 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体列表
|
||||
*/
|
||||
async function fetchAgents() {
|
||||
isLoadingAgents.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await agentApi.getAgents()
|
||||
agents.value = sortAgents(response.agents || [])
|
||||
agents.value = sortAgents((response.agents || []).map(normalizeAgent))
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch agents:', err)
|
||||
handleChatError(err, 'fetch')
|
||||
@ -207,26 +128,40 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个智能体的详细信息(包含配置选项)
|
||||
* @param {string} agentId - 智能体ID
|
||||
*/
|
||||
async function fetchAgentDetail(agentId, forceRefresh = false) {
|
||||
if (!agentId) return
|
||||
function applyConfigDefaults(loadedConfig, configItems = configurableItems.value) {
|
||||
const items = { ...configItems }
|
||||
Object.keys(items).forEach((key) => {
|
||||
const item = items[key]?.x_oap_ui_config ? { ...items[key], ...items[key].x_oap_ui_config } : items[key]
|
||||
const isDefaultAllList = isDefaultAllAgentResourceKind(item?.kind)
|
||||
if (loadedConfig[key] === undefined || (loadedConfig[key] === null && !isDefaultAllList)) {
|
||||
if (item.default !== undefined) loadedConfig[key] = item.default
|
||||
}
|
||||
if (
|
||||
loadedConfig[key] !== undefined &&
|
||||
loadedConfig[key] !== null &&
|
||||
loadedConfig[key] !== '' &&
|
||||
(item?.type === 'number' || item?.type === 'int' || item?.type === 'float')
|
||||
) {
|
||||
const numericValue = Number(loadedConfig[key])
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
loadedConfig[key] = item.type === 'int' ? Math.trunc(numericValue) : numericValue
|
||||
}
|
||||
}
|
||||
})
|
||||
return loadedConfig
|
||||
}
|
||||
|
||||
// 如果已经缓存了详细信息且不强制刷新,直接返回
|
||||
if (!forceRefresh && agentDetails.value[agentId]) {
|
||||
return agentDetails.value[agentId]
|
||||
}
|
||||
async function fetchAgentDetail(agentId, forceRefresh = false) {
|
||||
if (!agentId) return null
|
||||
if (!forceRefresh && agentDetails.value[agentId]) return agentDetails.value[agentId]
|
||||
|
||||
isLoadingAgentDetail.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await agentApi.getAgentDetail(agentId)
|
||||
agentDetails.value[agentId] = response
|
||||
// availableTools.value[agentId] = response.available_tools || []
|
||||
return response
|
||||
const agent = normalizeAgent(response.agent || response)
|
||||
agentDetails.value[agent.id] = agent
|
||||
return agent
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch agent detail for ${agentId}:`, err)
|
||||
handleChatError(err, 'fetch')
|
||||
@ -237,172 +172,31 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认智能体
|
||||
*/
|
||||
async function fetchDefaultAgent() {
|
||||
try {
|
||||
const response = await agentApi.getDefaultAgent()
|
||||
defaultAgentId.value = response.default_agent_id
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch default agent:', err)
|
||||
handleChatError(err, 'fetch')
|
||||
error.value = err.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认智能体
|
||||
*/
|
||||
async function setDefaultAgent(agentId) {
|
||||
try {
|
||||
await agentApi.setDefaultAgent(agentId)
|
||||
defaultAgentId.value = agentId
|
||||
} catch (err) {
|
||||
console.error('Failed to set default agent:', err)
|
||||
handleChatError(err, 'save')
|
||||
error.value = err.message
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择智能体
|
||||
*/
|
||||
async function selectAgent(agentId) {
|
||||
if (agents.value.find((a) => a.id === agentId)) {
|
||||
selectedAgentId.value = agentId
|
||||
// 清空之前的配置
|
||||
agentConfig.value = {}
|
||||
originalAgentConfig.value = {}
|
||||
selectedAgentConfigId.value = null
|
||||
|
||||
// 并行获取智能体详情和配置列表
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
await fetchAgentDetail(agentId)
|
||||
} catch (err) {
|
||||
console.warn(`Failed to fetch agent detail for ${agentId}:`, err)
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
await fetchAgentConfigs(agentId)
|
||||
const list = agentConfigs.value[agentId] || []
|
||||
const defaultItem = list.find((c) => c.is_default) || list[0]
|
||||
selectedAgentConfigId.value = defaultItem ? defaultItem.id : null
|
||||
if (selectedAgentConfigId.value) {
|
||||
await loadAgentConfig(agentId, selectedAgentConfigId.value)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to fetch agent configs for ${agentId}:`, err)
|
||||
}
|
||||
})()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载智能体配置
|
||||
*/
|
||||
async function fetchAgentConfigs(agentId = null) {
|
||||
const targetAgentId = agentId || selectedAgentId.value
|
||||
if (!targetAgentId) return
|
||||
|
||||
isLoadingAgentConfigs.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await agentApi.getAgentConfigs(targetAgentId)
|
||||
agentConfigs.value[targetAgentId] = response.configs || []
|
||||
} catch (err) {
|
||||
console.error('Failed to load agent configs:', err)
|
||||
handleChatError(err, 'load')
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
isLoadingAgentConfigs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAgentConfig(agentId = null, configId = null) {
|
||||
const targetAgentId = agentId || selectedAgentId.value
|
||||
const targetConfigId = configId || selectedAgentConfigId.value
|
||||
if (!targetAgentId || !targetConfigId) return
|
||||
|
||||
if (!agentId || !agents.value.find((a) => a.id === agentId)) return
|
||||
isLoadingConfig.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await agentApi.getAgentConfigProfile(targetAgentId, targetConfigId)
|
||||
const configJson = response.config?.config_json || {}
|
||||
const contextConfig = configJson.context || configJson
|
||||
const loadedConfig = { ...contextConfig }
|
||||
|
||||
// 确保 configurableItems 已加载
|
||||
if (!agentDetails.value[targetAgentId]) {
|
||||
await fetchAgentDetail(targetAgentId)
|
||||
}
|
||||
|
||||
// 使用 configurableItems 中的默认值补全缺失的配置项
|
||||
const items = configurableItems.value
|
||||
Object.keys(items).forEach((key) => {
|
||||
const item = items[key]
|
||||
const isDefaultAllList = isDefaultAllAgentResourceKind(item?.kind)
|
||||
if (loadedConfig[key] === undefined || (loadedConfig[key] === null && !isDefaultAllList)) {
|
||||
// 只有当默认值存在时才设置
|
||||
if (item.default !== undefined) {
|
||||
loadedConfig[key] = item.default
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
loadedConfig[key] !== undefined &&
|
||||
loadedConfig[key] !== null &&
|
||||
loadedConfig[key] !== '' &&
|
||||
(item?.type === 'number' || item?.type === 'int' || item?.type === 'float')
|
||||
) {
|
||||
const numericValue = Number(loadedConfig[key])
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
loadedConfig[key] = item.type === 'int' ? Math.trunc(numericValue) : numericValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const detail = await fetchAgentDetail(agentId)
|
||||
const loadedConfig = applyConfigDefaults(extractContext(detail), detail?.configurable_items || {})
|
||||
selectedAgentId.value = agentId
|
||||
agentConfig.value = loadedConfig
|
||||
originalAgentConfig.value = { ...loadedConfig }
|
||||
} catch (err) {
|
||||
console.error('Failed to load agent config profile:', err)
|
||||
handleChatError(err, 'load')
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
isLoadingConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectAgentConfig(configId) {
|
||||
async function saveAgentConfig() {
|
||||
const targetAgentId = selectedAgentId.value
|
||||
if (!targetAgentId || !configId) return
|
||||
selectedAgentConfigId.value = configId
|
||||
await loadAgentConfig(targetAgentId, configId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存智能体配置
|
||||
* @param {Object} options - 额外参数 (e.g., { reload_graph: true })
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function saveAgentConfig(options = {}) {
|
||||
const targetAgentId = selectedAgentId.value
|
||||
const targetConfigId = selectedAgentConfigId.value
|
||||
if (!targetAgentId || !targetConfigId) return
|
||||
|
||||
if (!targetAgentId) return
|
||||
try {
|
||||
await agentApi.updateAgentConfigProfile(targetAgentId, targetConfigId, {
|
||||
const response = await agentApi.updateAgent(targetAgentId, {
|
||||
config_json: { context: agentConfig.value }
|
||||
})
|
||||
const updated = normalizeAgent(response.agent)
|
||||
agentDetails.value[targetAgentId] = updated
|
||||
const index = agents.value.findIndex((item) => item.id === targetAgentId)
|
||||
if (index >= 0) agents.value.splice(index, 1, updated)
|
||||
originalAgentConfig.value = { ...agentConfig.value }
|
||||
} catch (err) {
|
||||
console.error('Failed to save agent config:', err)
|
||||
@ -412,100 +206,66 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
async function createAgentConfigProfile({ name, setDefault = false, fromCurrent = true } = {}) {
|
||||
const targetAgentId = selectedAgentId.value
|
||||
if (!targetAgentId) return null
|
||||
if (!name) throw new Error('配置名称不能为空')
|
||||
|
||||
const baseContext = fromCurrent ? { ...agentConfig.value } : {}
|
||||
|
||||
const response = await agentApi.createAgentConfigProfile(targetAgentId, {
|
||||
name,
|
||||
config_json: { context: baseContext },
|
||||
set_default: setDefault
|
||||
})
|
||||
|
||||
await fetchAgentConfigs(targetAgentId)
|
||||
const created = response?.config
|
||||
async function createAgent(payload) {
|
||||
const response = await agentApi.createAgent(payload)
|
||||
const created = normalizeAgent(response.agent)
|
||||
if (created?.id) {
|
||||
await selectAgentConfig(created.id)
|
||||
agentDetails.value[created.id] = created
|
||||
agents.value = sortAgents([created, ...agents.value.filter((item) => item.id !== created.id)])
|
||||
await selectAgent(created.id)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
async function deleteSelectedAgentConfigProfile() {
|
||||
const targetAgentId = selectedAgentId.value
|
||||
const targetConfigId = selectedAgentConfigId.value
|
||||
if (!targetAgentId || !targetConfigId) return
|
||||
async function updateAgentProfile(agentId, payload) {
|
||||
const response = await agentApi.updateAgent(agentId, payload)
|
||||
const updated = normalizeAgent(response.agent)
|
||||
agentDetails.value[updated.id] = updated
|
||||
const index = agents.value.findIndex((item) => item.id === updated.id)
|
||||
if (index >= 0) agents.value.splice(index, 1, updated)
|
||||
return updated
|
||||
}
|
||||
|
||||
await agentApi.deleteAgentConfigProfile(targetAgentId, targetConfigId)
|
||||
await fetchAgentConfigs(targetAgentId)
|
||||
const list = agentConfigs.value[targetAgentId] || []
|
||||
const defaultItem = list.find((c) => c.is_default) || list[0]
|
||||
selectedAgentConfigId.value = defaultItem ? defaultItem.id : null
|
||||
if (selectedAgentConfigId.value) {
|
||||
await loadAgentConfig(targetAgentId, selectedAgentConfigId.value)
|
||||
} else {
|
||||
async function deleteAgent(agentId) {
|
||||
await agentApi.deleteAgent(agentId)
|
||||
agents.value = agents.value.filter((item) => item.id !== agentId)
|
||||
delete agentDetails.value[agentId]
|
||||
if (selectedAgentId.value === agentId) {
|
||||
selectedAgentId.value = null
|
||||
agentConfig.value = {}
|
||||
originalAgentConfig.value = {}
|
||||
const nextAgentId = getPreferredAgentId(agents.value)
|
||||
if (nextAgentId) await selectAgent(nextAgentId)
|
||||
}
|
||||
}
|
||||
|
||||
async function setSelectedAgentConfigDefault() {
|
||||
const targetAgentId = selectedAgentId.value
|
||||
const targetConfigId = selectedAgentConfigId.value
|
||||
if (!targetAgentId || !targetConfigId) return
|
||||
|
||||
await agentApi.setAgentConfigDefault(targetAgentId, targetConfigId)
|
||||
await fetchAgentConfigs(targetAgentId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置智能体配置
|
||||
*/
|
||||
function resetAgentConfig() {
|
||||
agentConfig.value = { ...originalAgentConfig.value }
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置项
|
||||
*/
|
||||
function updateConfigItem(key, value) {
|
||||
agentConfig.value[key] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新智能体配置(支持批量更新)
|
||||
*/
|
||||
function updateAgentConfig(updates) {
|
||||
Object.assign(agentConfig.value, updates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误状态
|
||||
*/
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 store 状态
|
||||
*/
|
||||
function reset() {
|
||||
agents.value = []
|
||||
selectedAgentId.value = null
|
||||
defaultAgentId.value = null
|
||||
availableKnowledgeBases.value = []
|
||||
availableMcps.value = []
|
||||
availableSkills.value = []
|
||||
agentConfig.value = {}
|
||||
originalAgentConfig.value = {}
|
||||
agentConfigs.value = {}
|
||||
selectedAgentConfigId.value = null
|
||||
agentDetails.value = {}
|
||||
isLoadingAgents.value = false
|
||||
isLoadingConfig.value = false
|
||||
isLoadingAgentConfigs.value = false
|
||||
isLoadingAgentDetail.value = false
|
||||
error.value = null
|
||||
isInitialized.value = false
|
||||
@ -513,10 +273,8 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
agents,
|
||||
selectedAgentId,
|
||||
defaultAgentId,
|
||||
availableKnowledgeBases,
|
||||
availableMcps,
|
||||
availableSkills,
|
||||
@ -526,37 +284,22 @@ export const useAgentStore = defineStore(
|
||||
isLoadingAgents,
|
||||
isLoadingConfig,
|
||||
isLoadingAgentDetail,
|
||||
isLoadingAgentConfigs,
|
||||
error,
|
||||
isInitialized,
|
||||
|
||||
// 计算属性
|
||||
selectedAgent,
|
||||
defaultAgent,
|
||||
agentsList,
|
||||
isDefaultAgent,
|
||||
configurableItems,
|
||||
availableTools,
|
||||
hasConfigChanges,
|
||||
agentConfigs,
|
||||
selectedAgentConfigId,
|
||||
selectedConfigSummary,
|
||||
|
||||
// 方法
|
||||
initialize,
|
||||
fetchAgents,
|
||||
fetchAgentDetail,
|
||||
fetchDefaultAgent,
|
||||
fetchMentionResources,
|
||||
setDefaultAgent,
|
||||
selectAgent,
|
||||
selectAgentConfig,
|
||||
fetchAgentConfigs,
|
||||
loadAgentConfig,
|
||||
saveAgentConfig,
|
||||
createAgentConfigProfile,
|
||||
deleteSelectedAgentConfigProfile,
|
||||
setSelectedAgentConfigDefault,
|
||||
createAgent,
|
||||
updateAgentProfile,
|
||||
deleteAgent,
|
||||
resetAgentConfig,
|
||||
updateConfigItem,
|
||||
updateAgentConfig,
|
||||
@ -565,11 +308,10 @@ export const useAgentStore = defineStore(
|
||||
}
|
||||
},
|
||||
{
|
||||
// 持久化配置
|
||||
persist: {
|
||||
key: 'agent-store',
|
||||
storage: localStorage,
|
||||
pick: ['selectedAgentId', 'selectedAgentConfigId']
|
||||
pick: ['selectedAgentId']
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -8,10 +8,10 @@
|
||||
:single-mode="false"
|
||||
@thread-change="handleThreadChange"
|
||||
>
|
||||
<template #input-actions-left>
|
||||
<template #input-actions-left="{ hasActiveThread }">
|
||||
<a-dropdown
|
||||
v-if="selectedAgentId"
|
||||
v-model:open="configDropdownOpen"
|
||||
v-model:open="agentDropdownOpen"
|
||||
:trigger="['click']"
|
||||
placement="topLeft"
|
||||
overlay-class-name="config-dropdown-overlay"
|
||||
@ -23,49 +23,58 @@
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
>
|
||||
<Settings2 size="18" class="nav-btn-icon" />
|
||||
<span class="hide-text config-dropdown-text">{{ currentConfigLabel }}</span>
|
||||
<img
|
||||
class="config-dropdown-agent-icon nav-btn-icon"
|
||||
:src="currentAgentIcon"
|
||||
:alt="`${currentAgentLabel}图标`"
|
||||
/>
|
||||
<span class="hide-text config-dropdown-text">{{ currentAgentLabel }}</span>
|
||||
<ChevronDown size="15" class="config-dropdown-chevron" />
|
||||
</button>
|
||||
|
||||
<template #overlay>
|
||||
<div class="config-dropdown-panel" @click.stop>
|
||||
<button
|
||||
v-for="config in configQuickSwitchOptions"
|
||||
:key="config.value"
|
||||
v-for="agent in agentQuickSwitchOptions"
|
||||
:key="agent.value"
|
||||
type="button"
|
||||
class="config-dropdown-item"
|
||||
:class="{ selected: config.value === selectedAgentConfigId }"
|
||||
@click="handleConfigSwitch(config.value)"
|
||||
:class="{
|
||||
selected: agent.value === selectedAgentId,
|
||||
disabled: hasActiveThread && agent.value !== selectedAgentId
|
||||
}"
|
||||
@click="handleAgentSwitch(agent.value, hasActiveThread)"
|
||||
>
|
||||
<span class="config-dropdown-item-label">{{ config.label }}</span>
|
||||
<span v-if="config.isDefault" class="config-dropdown-item-badge">默认</span>
|
||||
<img
|
||||
class="config-dropdown-item-icon-image"
|
||||
:src="agent.icon"
|
||||
:alt="`${agent.label}图标`"
|
||||
/>
|
||||
<span class="config-dropdown-item-label">{{ agent.label }}</span>
|
||||
<span v-if="agent.isBuiltin" class="config-dropdown-item-badge">内置</span>
|
||||
<Check
|
||||
v-if="config.value === selectedAgentConfigId"
|
||||
v-if="agent.value === selectedAgentId"
|
||||
:size="14"
|
||||
class="config-dropdown-item-check"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div class="config-dropdown-divider"></div>
|
||||
<div v-if="hasActiveThread" class="config-dropdown-hint">
|
||||
当前对话已绑定智能体,新对话可切换。
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="config-dropdown-item action-item"
|
||||
@click="toggleConfigSidebar"
|
||||
>
|
||||
<Settings2 :size="15" class="config-dropdown-item-icon" />
|
||||
<span class="config-dropdown-item-label">{{ configSidebarActionLabel }}</span>
|
||||
</button>
|
||||
<template v-if="userStore.isAdmin">
|
||||
<div class="config-dropdown-divider"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="config-dropdown-item action-item"
|
||||
@click="openCreateConfigModal"
|
||||
>
|
||||
<Plus :size="15" class="config-dropdown-item-icon" />
|
||||
<span class="config-dropdown-item-label">新建配置</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="config-dropdown-item action-item"
|
||||
@click="openAgentManagement"
|
||||
>
|
||||
<Settings2 :size="15" class="config-dropdown-item-icon" />
|
||||
<span class="config-dropdown-item-label">管理智能体</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
@ -96,12 +105,6 @@
|
||||
</AgentChatComponent>
|
||||
</div>
|
||||
|
||||
<!-- 配置侧边栏 -->
|
||||
<AgentConfigSidebar
|
||||
:isOpen="chatUIStore.isConfigSidebarOpen"
|
||||
@close="() => (chatUIStore.isConfigSidebarOpen = false)"
|
||||
/>
|
||||
|
||||
<!-- 反馈模态框 -->
|
||||
<FeedbackModalComponent
|
||||
v-if="userStore.isAdmin"
|
||||
@ -109,17 +112,6 @@
|
||||
:agent-id="selectedAgentId"
|
||||
/>
|
||||
|
||||
<a-modal
|
||||
v-model:open="createConfigModalOpen"
|
||||
title="新建配置"
|
||||
:width="360"
|
||||
:confirm-loading="createConfigLoading"
|
||||
@ok="handleCreateConfig"
|
||||
@cancel="closeCreateConfigModal"
|
||||
>
|
||||
<a-input v-model:value="createConfigName" placeholder="请输入配置名称" allow-clear />
|
||||
</a-modal>
|
||||
|
||||
<!-- 自定义更多菜单 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="menu-fade">
|
||||
@ -151,17 +143,17 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { MessageOutlined, ShareAltOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Settings2, Ellipsis, ChevronDown, Check, Plus, FolderKanban } from 'lucide-vue-next'
|
||||
import { Settings2, Ellipsis, ChevronDown, Check, FolderKanban } from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AgentChatComponent from '@/components/AgentChatComponent.vue'
|
||||
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue'
|
||||
import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { isBuiltinAgent, useAgentStore } from '@/stores/agent'
|
||||
import { useChatUIStore } from '@/stores/chatUI'
|
||||
import { ChatExporter } from '@/utils/chatExporter'
|
||||
import { handleChatError } from '@/utils/errorHandler'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import defaultAgentIcon from '@/assets/defaults/agent.png'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
@ -177,8 +169,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 从 agentStore 中获取响应式状态
|
||||
const { selectedAgentId, defaultAgentId, selectedAgentConfigId, agentConfigs, isLoadingConfig } =
|
||||
storeToRefs(agentStore)
|
||||
const { agents, selectedAgentId, isLoadingConfig } = storeToRefs(agentStore)
|
||||
|
||||
const syncingRouteThread = ref(false)
|
||||
|
||||
@ -194,14 +185,8 @@ const syncSelectedThreadFromRoute = async () => {
|
||||
const threadId = getRouteThreadId()
|
||||
syncingRouteThread.value = true
|
||||
try {
|
||||
if (!threadId) {
|
||||
if (!agentStore.isInitialized) {
|
||||
await agentStore.initialize()
|
||||
}
|
||||
const targetAgentId = defaultAgentId.value
|
||||
if (targetAgentId && selectedAgentId.value !== targetAgentId) {
|
||||
await agentStore.selectAgent(targetAgentId)
|
||||
}
|
||||
if (!threadId && !agentStore.isInitialized) {
|
||||
await agentStore.initialize()
|
||||
}
|
||||
|
||||
const ok = await chatComponent.selectThreadFromRoute(threadId)
|
||||
@ -241,88 +226,48 @@ const handleThreadChange = (threadId) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 输入区只负责快速切换已有配置,详细查看/编辑仍通过侧边栏完成。
|
||||
const configQuickSwitchOptions = computed(() => {
|
||||
if (!selectedAgentId.value) return []
|
||||
const list = agentConfigs.value[selectedAgentId.value] || []
|
||||
return list.map((config) => ({
|
||||
label: config.name,
|
||||
value: config.id,
|
||||
isDefault: !!config.is_default
|
||||
const agentQuickSwitchOptions = computed(() =>
|
||||
(agents.value || []).map((agent) => ({
|
||||
label: agent.name || agent.id,
|
||||
value: agent.id,
|
||||
icon: agent.icon || defaultAgentIcon,
|
||||
isBuiltin: isBuiltinAgent(agent)
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
const currentConfigLabel = computed(() => {
|
||||
const currentAgentOption = computed(() =>
|
||||
agentQuickSwitchOptions.value.find((agent) => agent.value === selectedAgentId.value)
|
||||
)
|
||||
|
||||
const currentAgentLabel = computed(() => {
|
||||
if (isLoadingConfig.value) return '加载中...'
|
||||
const current = configQuickSwitchOptions.value.find(
|
||||
(config) => config.value === selectedAgentConfigId.value
|
||||
)
|
||||
return current?.label || '配置'
|
||||
return currentAgentOption.value?.label || '智能体'
|
||||
})
|
||||
|
||||
const configSidebarActionLabel = computed(() => {
|
||||
return chatUIStore.isConfigSidebarOpen ? '收起配置侧边栏' : '查看/编辑配置'
|
||||
})
|
||||
const currentAgentIcon = computed(() => currentAgentOption.value?.icon || defaultAgentIcon)
|
||||
|
||||
const configDropdownOpen = ref(false)
|
||||
const createConfigModalOpen = ref(false)
|
||||
const createConfigLoading = ref(false)
|
||||
const createConfigName = ref('')
|
||||
const agentDropdownOpen = ref(false)
|
||||
|
||||
const handleConfigSwitch = async (configId) => {
|
||||
if (!configId || configId === selectedAgentConfigId.value) return
|
||||
try {
|
||||
await agentStore.selectAgentConfig(configId)
|
||||
configDropdownOpen.value = false
|
||||
} catch (error) {
|
||||
console.error('切换配置出错:', error)
|
||||
message.error('切换配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleConfigSidebar = () => {
|
||||
chatUIStore.isConfigSidebarOpen = !chatUIStore.isConfigSidebarOpen
|
||||
configDropdownOpen.value = false
|
||||
}
|
||||
|
||||
const openCreateConfigModal = () => {
|
||||
configDropdownOpen.value = false
|
||||
createConfigName.value = ''
|
||||
createConfigModalOpen.value = true
|
||||
}
|
||||
|
||||
const closeCreateConfigModal = () => {
|
||||
createConfigModalOpen.value = false
|
||||
createConfigName.value = ''
|
||||
}
|
||||
|
||||
const handleCreateConfig = async () => {
|
||||
if (!selectedAgentId.value) return
|
||||
|
||||
const name = createConfigName.value.trim()
|
||||
if (!name) {
|
||||
message.error('请输入配置名称')
|
||||
const handleAgentSwitch = async (agentId, hasActiveThread) => {
|
||||
if (!agentId || agentId === selectedAgentId.value) return
|
||||
if (hasActiveThread) {
|
||||
message.info('当前对话已绑定智能体,请新建对话后切换')
|
||||
return
|
||||
}
|
||||
|
||||
createConfigLoading.value = true
|
||||
try {
|
||||
await agentStore.createAgentConfigProfile({
|
||||
name,
|
||||
setDefault: false,
|
||||
fromCurrent: false
|
||||
})
|
||||
closeCreateConfigModal()
|
||||
chatUIStore.isConfigSidebarOpen = true
|
||||
message.success('配置已创建')
|
||||
await agentStore.selectAgent(agentId)
|
||||
agentDropdownOpen.value = false
|
||||
} catch (error) {
|
||||
console.error('创建配置出错:', error)
|
||||
message.error(error.message || '创建配置失败')
|
||||
} finally {
|
||||
createConfigLoading.value = false
|
||||
console.error('切换智能体出错:', error)
|
||||
message.error('切换智能体失败')
|
||||
}
|
||||
}
|
||||
|
||||
const openAgentManagement = () => {
|
||||
agentDropdownOpen.value = false
|
||||
router.push({ name: 'ModelManageComp', query: { tab: 'agents' } })
|
||||
}
|
||||
|
||||
// 更多菜单相关
|
||||
const moreMenuRef = ref(null)
|
||||
const moreButtonRef = ref(null)
|
||||
@ -443,6 +388,13 @@ const handleFeedback = () => {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.config-dropdown-agent-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.config-dropdown-trigger :deep(svg) {
|
||||
color: currentColor;
|
||||
}
|
||||
@ -596,6 +548,11 @@ const handleFeedback = () => {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item.selected {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
@ -615,11 +572,21 @@ const handleFeedback = () => {
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item-icon {
|
||||
.config-dropdown-overlay .config-dropdown-item-icon,
|
||||
.config-dropdown-overlay .config-dropdown-item-icon-image {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item-icon {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item-icon-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 6px;
|
||||
@ -635,6 +602,13 @@ const handleFeedback = () => {
|
||||
color: var(--main-600);
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-hint {
|
||||
padding: 6px 8px;
|
||||
color: var(--gray-500);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-divider {
|
||||
height: 1px;
|
||||
margin: 4px 4px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user