refactor(agent): 重构智能体相关逻辑至Pinia store

将智能体管理、配置、线程和消息等逻辑从组件中抽离,统一管理至Pinia store
优化代码结构,提升可维护性和复用性
修复多处直接调用API的问题,统一通过store管理状态
This commit is contained in:
Wenjie Zhang 2025-08-25 01:46:31 +08:00
parent b7c54d80c0
commit 3bca00a864
9 changed files with 712 additions and 384 deletions

View File

@ -1,5 +1,13 @@
<script setup>
import { themeConfig } from '@/assets/theme'
import { useAgentStore } from '@/stores/agent'
import { onMounted } from 'vue'
const agentStore = useAgentStore();
onMounted(async () => {
await agentStore.initialize();
})
</script>
<template>
<a-config-provider :theme="themeConfig">

View File

@ -7,7 +7,7 @@
:is-initial-render="state.isInitialRender"
:single-mode="props.singleMode"
:agents="agents"
:selected-agent-id="props.agentId"
:selected-agent-id="agentStore.selectedAgentId"
@create-chat="createNewChat"
@select-chat="selectChat"
@delete-chat="deleteChat"
@ -34,8 +34,8 @@
</div>
</div>
<div class="header__center" @mouseenter="showRenameButton = true" @mouseleave="showRenameButton = false">
<div @click="console.log(currentChat)" class="center-title">
{{ currentChat?.title }}
<div @click="console.log(agentStore.currentThread)" class="center-title">
{{ agentStore.currentThread?.title }}
</div>
<div class="rename-button" v-if="currentChatId" :class="{ 'visible': showRenameButton }" @click="handleRenameChat">
<EditOutlined style="font-size: 14px; color: var(--gray-600);"/>
@ -54,7 +54,7 @@
</div>
</div>
<div v-if="isLoading" class="chat-loading">
<div v-if="isLoadingThreads || isLoadingMessages" class="chat-loading">
<LoadingOutlined />
<span>正在加载历史记录...</span>
</div>
@ -147,19 +147,12 @@ import MessageInputComponent from '@/components/MessageInputComponent.vue'
import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue'
import RefsComponent from '@/components/RefsComponent.vue'
import { agentApi, threadApi } from '@/apis/agent'
import { PanelLeftOpen, MessageSquarePlus } from 'lucide-vue-next';
import { useAgentStore } from '@/stores/agent';
import { storeToRefs } from 'pinia';
// propsagentId
const props = defineProps({
agentId: {
type: String,
default: null
},
config: {
type: Object,
default: () => ({})
},
state: {
type: Object,
default: () => ({})
@ -172,6 +165,19 @@ const props = defineProps({
const emit = defineEmits(['open-config', 'open-agent-modal']);
// ==================== Store ====================
const agentStore = useAgentStore();
const {
agents,
currentAgentThreads,
currentThread,
currentThreadMessages,
isLoadingThreads,
isLoadingMessages,
selectedAgent,
configSchema
} = storeToRefs(agentStore);
// ==================== ====================
// UI
@ -272,15 +278,17 @@ const getLastMessage = (conv) => {
const messagesContainer = ref(null);
//
const agents = ref({}); //
const userInput = ref(''); //
const currentChatId = ref(null); // ID
const chatsList = ref([]); //
const isLoading = ref(false); //
const convs = ref([]);
const currentAgent = computed(() => agents.value[props.agentId]);
const currentChat = computed(() => chatsList.value.find(chat => chat.id === currentChatId.value));
const currentAgent = computed(() => {
if (!agentStore.selectedAgentId || !agents.value) return null;
return agents.value[agentStore.selectedAgentId];
});
// 使 agentStore 线
const currentChatId = computed(() => agentStore.currentThreadId);
const chatsList = computed(() => currentAgentThreads.value || []);
const onGoingConv = reactive({
msgChunks: {},
@ -308,7 +316,7 @@ const expandedToolCalls = ref(new Set()); // 展开的工具调用集合
//
const createNewChat = async () => {
// AgentID
if (!props.agentId) {
if (!agentStore.selectedAgentId) {
console.warn("未指定AgentID无法创建对话");
return;
}
@ -319,24 +327,20 @@ const createNewChat = async () => {
return;
}
if (currentChatId.value &&convs.value.length === 0) {
if (currentChatId.value && convs.value.length === 0) {
return;
}
try {
// API
// 使 agentStore
state.creatingNewChat = true;
const response = await threadApi.createThread(props.agentId, '新的对话');
if (!response || !response.id) {
const thread = await agentStore.createThread(agentStore.selectedAgentId, '新的对话');
if (!thread || !thread.id) {
throw new Error('创建对话失败');
}
//
currentChatId.value = response.id;
// 线
resetThread();
//
loadChatsList();
} catch (error) {
console.error('创建对话失败:', error);
message.error('创建对话失败');
@ -348,37 +352,33 @@ const createNewChat = async () => {
//
const selectChat = async (chatId) => {
// AgentID
if (!props.agentId) {
if (!agentStore.selectedAgentId) {
console.warn("未指定AgentID无法选择对话");
return;
}
console.log("选择对话:", chatId);
//
currentChatId.value = chatId;
// 使 agentStore 线
agentStore.selectThread(chatId);
await getAgentHistory();
};
//
const deleteChat = async (chatId) => {
if (!props.agentId) {
if (!agentStore.selectedAgentId) {
console.warn("未指定AgentID无法删除对话");
return;
}
try {
// API
await threadApi.deleteThread(chatId);
// 使 agentStore
await agentStore.deleteThread(chatId);
//
if (chatId === currentChatId.value) {
resetThread();
currentChatId.value = null;
}
//
loadChatsList();
} catch (error) {
console.error('删除对话失败:', error);
message.error('删除对话失败');
@ -395,7 +395,7 @@ const renameChat = async (data) => {
}
// AgentID
if (!props.agentId) {
if (!agentStore.selectedAgentId) {
console.warn("未指定AgentID无法重命名对话");
return;
}
@ -406,11 +406,8 @@ const renameChat = async (data) => {
}
try {
// API
await threadApi.updateThread(chatId, title);
//
loadChatsList();
// 使 agentStore
await agentStore.updateThread(chatId, title);
} catch (error) {
console.error('重命名对话失败:', error);
message.error('重命名对话失败');
@ -419,12 +416,12 @@ const renameChat = async (data) => {
//
const handleRenameChat = () => {
if (!currentChatId.value || !currentChat.value) {
if (!currentChatId.value || !currentThread.value) {
message.warning('请先选择对话');
return;
}
let newTitle = currentChat.value.title;
let newTitle = currentThread.value.title;
Modal.confirm({
title: '重命名对话',
@ -473,7 +470,7 @@ const shareChat = () => {
const link = document.createElement('a');
//
const chatTitle = currentChat.value?.title || '新对话';
const chatTitle = currentThread.value?.title || '新对话';
const timestamp = new Date().toLocaleString('zh-CN').replace(/[:/\s]/g, '-');
const filename = `${chatTitle}-${timestamp}.html`;
@ -497,7 +494,7 @@ const shareChat = () => {
// HTML
const generateChatHTML = () => {
const chatTitle = currentChat.value?.title || '新对话';
const chatTitle = agentStore.currentThread.value?.title || '新对话';
const agentName = currentAgent.value?.name || '智能助手';
const agentDescription = currentAgent.value?.description || '';
const exportTime = new Date().toLocaleString('zh-CN');
@ -939,7 +936,7 @@ const sendMessageToServer = async (text) => {
const requestData = {
query: text.trim(),
config: {
...props.config,
...agentStore.agentConfig,
thread_id: currentChatId.value
},
meta: {
@ -950,7 +947,7 @@ const sendMessageToServer = async (text) => {
try {
state.waitingServerResponse = true;
const response = await agentApi.sendAgentMessage(currentAgent.value.id, requestData);
const response = await agentStore.sendMessage(currentAgent.value.id, requestData);
if (!response.ok) {
throw new Error('请求失败');
}
@ -1049,52 +1046,37 @@ const processResponseChunk = async (data) => {
//
const initAll = async () => {
try {
isLoading.value = true;
//
// 使 agentStore
// agent store
if (!agentStore.agents || Object.keys(agentStore.agents).length === 0) {
await agentStore.initialize();
}
//
setTimeout(async () => {
await fetchAgents(); //
await loadChatsList(); //
isLoading.value = false;
await loadChatsList();
}, 100);
} catch (error) {
console.error("组件挂载出错:", error);
message.error(`加载数据失败: ${error}`);
isLoading.value = false;
}
}
//
const fetchAgents = async () => {
try {
const data = await agentApi.getAgents();
//
agents.value = data.agents.reduce((acc, agent) => {
acc[agent.id] = agent;
return acc;
}, {});
console.log("agents", agents.value);
} catch (error) {
console.error('获取智能体错误:', error);
}
};
//
const loadChatsList = async () => {
try {
if (!props.agentId) {
if (!agentStore.selectedAgentId) {
console.warn("未指定AgentID无法加载状态");
return;
}
//
const threads = await threadApi.getThreads(props.agentId);
// 使 agentStore
await agentStore.fetchThreads(agentStore.selectedAgentId);
if (threads && Array.isArray(threads) && threads.length > 0) {
//
chatsList.value = threads;
currentChatId.value = threads[0].id; //
await getAgentHistory()
if (currentAgentThreads.value && currentAgentThreads.value.length > 0) {
//
await agentStore.selectThread(currentAgentThreads.value[0].id);
await getAgentHistory();
} else {
//
await createNewChat();
@ -1106,21 +1088,22 @@ const loadChatsList = async () => {
//
const getAgentHistory = async () => {
if (!props.agentId || !currentChatId.value) {
if (!agentStore.selectedAgentId || !currentChatId.value) {
console.warn('未选择智能体或对话ID');
return;
}
try {
console.debug(`正在获取智能体[${props.agentId}]的历史记录对话ID: ${currentChatId.value}`);
const response = await agentApi.getAgentHistory(props.agentId, currentChatId.value);
console.debug('智能体历史记录:', response);
console.debug(`正在获取智能体[${agentStore.selectedAgentId}]的历史记录对话ID: ${currentChatId.value}`);
//
if (response && Array.isArray(response.history)) {
// 使 agentStore 线
await agentStore.fetchThreadMessages(currentChatId.value);
// store
if (currentThreadMessages.value && Array.isArray(currentThreadMessages.value)) {
//
onGoingConv.msgChunks = {};
convs.value = convertServerHistoryToMessages(response.history);
convs.value = convertServerHistoryToMessages(currentThreadMessages.value);
//
shouldAutoScroll.value = true;
@ -1202,7 +1185,7 @@ onMounted(async () => {
// agentId
onMounted(() => {
watch(() => props.agentId, async (newAgentId, oldAgentId) => {
watch(() => agentStore.selectedAgentId, async (newAgentId, oldAgentId) => {
try {
console.debug("智能体ID变化", oldAgentId, "->", newAgentId);

View File

@ -18,7 +18,7 @@
<!-- 侧边栏内容 -->
<div class="sidebar-content">
<div class="agent-info">
<div class="agent-info" v-if="selectedAgent">
<div class="agent-basic-info">
<h4>{{ selectedAgent.name || '未选择智能体' }}</h4>
<p class="agent-description">{{ selectedAgent.description }}</p>
@ -70,7 +70,8 @@
<!-- 系统提示词 -->
<a-textarea
v-else-if="key === 'system_prompt'"
v-model:value="agentConfig[key]"
:value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })"
:rows="3"
:placeholder="getPlaceholder(key, value)"
class="system-prompt-input"
@ -116,13 +117,15 @@
<!-- 布尔类型 -->
<a-switch
v-else-if="typeof agentConfig[key] === 'boolean'"
v-model:checked="agentConfig[key]"
:checked="agentConfig[key]"
@update:checked="(val) => agentStore.updateAgentConfig({ [key]: val })"
/>
<!-- 单选 -->
<a-select
v-else-if="value?.options && (value?.type === 'str' || value?.type === 'select')"
v-model:value="agentConfig[key]"
:value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })"
class="config-select"
>
<a-select-option v-for="option in value.options" :key="option" :value="option">
@ -168,7 +171,8 @@
<!-- 数字 -->
<a-input-number
v-else-if="value?.type === 'number'"
v-model:value="agentConfig[key]"
:value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })"
:placeholder="getPlaceholder(key, value)"
class="config-input-number"
/>
@ -176,7 +180,8 @@
<!-- 滑块 -->
<a-slider
v-else-if="value?.type === 'slider'"
v-model:value="agentConfig[key]"
:value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })"
:min="value.min"
:max="value.max"
:step="value.step"
@ -186,7 +191,8 @@
<!-- 其他类型 -->
<a-input
v-else
v-model:value="agentConfig[key]"
:value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })"
:placeholder="getPlaceholder(key, value)"
class="config-input"
/>
@ -279,45 +285,41 @@ import {
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import { agentApi } from '@/apis/agent';
import { useAgentStore } from '@/stores/agent';
import { storeToRefs } from 'pinia';
// Props
const props = defineProps({
isOpen: {
type: Boolean,
default: false
},
selectedAgent: {
type: Object,
default: () => ({})
},
selectedAgentId: {
type: String,
default: null
},
agentConfig: {
type: Object,
default: () => ({})
}
});
// Emits
const emit = defineEmits([
'close',
'update:agentConfig',
'config-saved',
'config-reset'
'close'
]);
// Store
const agentStore = useAgentStore();
const {
availableTools,
selectedAgent,
selectedAgentId,
agentConfig
} = storeToRefs(agentStore);
// console.log(availableTools.value)
//
const debugMode = ref(false);
const toolsModalOpen = ref(false);
const availableTools = ref([]);
const selectedTools = ref([]);
const toolsSearchText = ref('');
//
const configSchema = computed(() => props.selectedAgent.config_schema || {});
const configSchema = computed(() => selectedAgent.value?.config_schema || {});
const configurableItems = computed(() => {
const items = configSchema.value.configurable_items || {};
@ -333,15 +335,16 @@ const configurableItems = computed(() => {
});
const isEmptyConfig = computed(() => {
return !props.selectedAgentId || Object.keys(configurableItems.value).length === 0;
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0;
});
const filteredTools = computed(() => {
const toolsList = availableTools.value ? Object.values(availableTools.value) : [];
if (!toolsSearchText.value) {
return availableTools.value;
return toolsList;
}
const searchLower = toolsSearchText.value.toLowerCase();
return availableTools.value.filter(tool =>
return toolsList.filter(tool =>
tool.name.toLowerCase().includes(searchLower) ||
tool.description.toLowerCase().includes(searchLower)
);
@ -357,6 +360,7 @@ const toggleDebugMode = () => {
};
const getConfigLabel = (key, value) => {
// console.log(configurableItems)
if (value.description && value.name !== key) {
return `${value.name}${key}`;
}
@ -368,33 +372,32 @@ const getPlaceholder = (key, value) => {
};
const handleModelChange = (data) => {
const newConfig = { ...props.agentConfig };
newConfig.model = `${data.provider}/${data.name}`;
emit('update:agentConfig', newConfig);
agentStore.updateAgentConfig({
model: `${data.provider}/${data.name}`
});
};
//
const ensureArray = (key) => {
const config = { ...props.agentConfig };
const config = agentConfig.value || {};
if (!config[key] || !Array.isArray(config[key])) {
config[key] = [];
return [];
}
return config;
return config[key];
};
const isOptionSelected = (key, option) => {
const config = ensureArray(key);
return config[key].includes(option);
const currentOptions = ensureArray(key);
return currentOptions.includes(option);
};
const getSelectedCount = (key) => {
const config = ensureArray(key);
return config[key].length;
const currentOptions = ensureArray(key);
return currentOptions.length;
};
const toggleOption = (key, option) => {
const config = ensureArray(key);
const currentOptions = [...config[key]];
const currentOptions = [...ensureArray(key)];
const index = currentOptions.indexOf(option);
if (index > -1) {
@ -403,26 +406,27 @@ const toggleOption = (key, option) => {
currentOptions.push(option);
}
config[key] = currentOptions;
emit('update:agentConfig', config);
agentStore.updateAgentConfig({
[key]: currentOptions
});
};
const clearSelection = (key) => {
const config = { ...props.agentConfig };
config[key] = [];
emit('update:agentConfig', config);
agentStore.updateAgentConfig({
[key]: []
});
};
//
const getToolNameById = (toolId) => {
const tool = availableTools.value.find(t => t.id === toolId);
const toolsList = availableTools.value ? Object.values(availableTools.value) : [];
const tool = toolsList.find(t => t.id === toolId);
return tool ? tool.name : toolId;
};
const loadAvailableTools = async () => {
try {
const response = await agentApi.getTools();
availableTools.value = Object.values(response.tools || {});
await agentStore.fetchTools();
} catch (error) {
console.error('加载工具列表失败:', error);
}
@ -430,10 +434,10 @@ const loadAvailableTools = async () => {
const openToolsModal = async () => {
try {
if (availableTools.value.length === 0) {
if (!availableTools.value || Object.keys(availableTools.value).length === 0) {
await loadAvailableTools();
}
selectedTools.value = [...(props.agentConfig.tools || [])];
selectedTools.value = [...(agentConfig.value?.tools || [])];
toolsModalOpen.value = true;
} catch (error) {
console.error('打开工具选择弹窗失败:', error);
@ -451,18 +455,20 @@ const toggleToolSelection = (toolId) => {
};
const removeSelectedTool = (toolId) => {
const config = { ...props.agentConfig };
const index = config.tools.indexOf(toolId);
const currentTools = [...(agentConfig.value?.tools || [])];
const index = currentTools.indexOf(toolId);
if (index > -1) {
config.tools.splice(index, 1);
emit('update:agentConfig', config);
currentTools.splice(index, 1);
agentStore.updateAgentConfig({
tools: currentTools
});
}
};
const confirmToolsSelection = () => {
const config = { ...props.agentConfig };
config.tools = [...selectedTools.value];
emit('update:agentConfig', config);
agentStore.updateAgentConfig({
tools: [...selectedTools.value]
});
toolsModalOpen.value = false;
toolsSearchText.value = '';
};
@ -475,15 +481,14 @@ const cancelToolsSelection = () => {
//
const saveConfig = async () => {
if (!props.selectedAgentId) {
if (!selectedAgentId.value) {
message.error('没有选择智能体');
return;
}
try {
await agentApi.saveAgentConfig(props.selectedAgentId, props.agentConfig);
await agentStore.saveAgentConfig();
message.success('配置已保存到服务器');
emit('config-saved');
} catch (error) {
console.error('保存配置到服务器出错:', error);
message.error('保存配置到服务器失败');
@ -491,14 +496,13 @@ const saveConfig = async () => {
};
const resetConfig = async () => {
if (!props.selectedAgentId) {
if (!selectedAgentId.value) {
message.error('没有选择智能体');
return;
}
try {
await agentApi.saveAgentConfig(props.selectedAgentId, {});
emit('config-reset');
agentStore.resetAgentConfig();
message.info('配置已重置');
} catch (error) {
console.error('重置配置出错:', error);
@ -508,7 +512,7 @@ const resetConfig = async () => {
//
watch(() => props.isOpen, (newVal) => {
if (newVal && availableTools.value.length === 0) {
if (newVal && (!availableTools.value || Object.keys(availableTools.value).length === 0)) {
loadAvailableTools();
}
});

View File

@ -35,10 +35,10 @@
<span v-if="!toolCall.tool_call_result">
<span><Loader size="16" class="tool-loader rotate" /></span> &nbsp;
<span>正在调用工具: </span>
<span class="tool-name">{{ toolCall.name || toolCall.function.name }}</span>
<span class="tool-name">{{ getToolNameByToolCall(toolCall) }}</span>
</span>
<span v-else>
<span><CircleCheckBig size="16" class="tool-loader" /></span> &nbsp; 工具 <span class="tool-name">{{ toolCall.name || toolCall.function.name }}</span> 执行完成
<span><CircleCheckBig size="16" class="tool-loader" /></span> &nbsp; 工具 <span class="tool-name">{{ getToolNameByToolCall(toolCall) }}</span> 执行完成
</span>
</div>
<div class="tool-content" v-show="expandedToolCalls.has(toolCall.id)">
@ -85,6 +85,8 @@ import { CaretRightOutlined, ThunderboltOutlined, LoadingOutlined } from '@ant-d
import RefsComponent from '@/components/RefsComponent.vue'
import { Loader, CircleCheckBig } from 'lucide-vue-next';
import { ToolResultRenderer } from '@/components/ToolCallingResult'
import { useAgentStore } from '@/stores/agent'
import { storeToRefs } from 'pinia'
import { MdPreview } from 'md-editor-v3'
@ -138,6 +140,18 @@ const isEmptyAndLoading = computed(() => {
return isEmpty && isLoading;
});
// store
const agentStore = useAgentStore();
const { availableTools } = storeToRefs(agentStore);
//
const getToolNameByToolCall = (toolCall) => {
const toolId = toolCall.name || toolCall.function.name;
const toolsList = availableTools.value ? Object.values(availableTools.value) : [];
const tool = toolsList.find(t => t.id === toolId);
return tool ? tool.name : toolId;
};
const parsedMessage = computed(() => {
const message = props.message;
if (message.content) {

View File

@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import AppLayout from '@/layouts/AppLayout.vue';
import BlankLayout from '@/layouts/BlankLayout.vue';
import { useUserStore } from '@/stores/user';
import { agentApi } from '@/apis/agent';
import { useAgentStore } from '@/stores/agent';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -121,21 +121,24 @@ router.beforeEach(async (to, from, next) => {
if (requiresAdmin && !isAdmin) {
// 如果是普通用户,跳转到默认智能体页面
try {
// 先尝试获取默认智能体
const data = await agentApi.getDefaultAgent();
if (data && data.default_agent_id) {
// 如果存在默认智能体,直接跳转
next(`/agent/${data.default_agent_id}`);
return;
const agentStore = useAgentStore();
// 等待 store 初始化完成
if (!agentStore.isInitialized) {
await agentStore.initialize();
}
// 如果没有默认智能体,则获取第一个可用智能体
const agentData = await agentApi.getAgents();
if (agentData && agentData.agents && agentData.agents.length > 0) {
const firstAgentId = agentData.agents[0].name;
next(`/agent/${firstAgentId}`);
const defaultAgent = agentStore.defaultAgent;
if (defaultAgent && defaultAgent.id) {
next(`/agent/${defaultAgent.id}`);
} else {
next('/');
// 如果没有默认智能体,可以考虑跳转到第一个可用的智能体,或者一个特定的页面
const agentIds = Object.keys(agentStore.agents);
if (agentIds.length > 0) {
next(`/agent/${agentIds[0]}`);
} else {
// 没有可用的智能体,跳转到首页
next('/');
}
}
} catch (error) {
console.error('获取智能体信息失败:', error);

445
web/src/stores/agent.js Normal file
View File

@ -0,0 +1,445 @@
import { defineStore } from 'pinia';
import { agentApi, threadApi } from '@/apis/agent';
export const useAgentStore = defineStore('agent', {
state: () => ({
// 智能体相关状态
agents: {}, // 以ID为键的智能体对象
selectedAgentId: null, // 当前选中的智能体ID
defaultAgentId: null, // 默认智能体ID
// 智能体配置相关状态
agentConfig: {}, // 当前智能体的配置
originalAgentConfig: {}, // 原始配置,用于重置
// 工具相关状态
availableTools: [], // 所有可用工具列表
// 线程相关状态
threads: {}, // 以智能体ID为键的线程列表
currentThreadId: null, // 当前选中的线程ID
threadMessages: {}, // 以线程ID为键的消息列表
// 加载状态
isLoadingAgents: false,
isLoadingConfig: false,
isLoadingTools: false,
isLoadingThreads: false,
isLoadingMessages: false,
// 错误状态
error: null,
// 初始化状态
isInitialized: false
}),
getters: {
// 获取当前选中的智能体
selectedAgent() {
return this.selectedAgentId ? this.agents[this.selectedAgentId] : null;
},
// 获取默认智能体,默认取第一个
defaultAgent() {
return this.defaultAgentId ? this.agents[this.defaultAgentId] : this.agents[Object.keys(this.agents)[0]];
},
// 获取智能体列表(数组形式)
agentsList() {
return Object.values(this.agents);
},
// 判断当前智能体是否为默认智能体
isDefaultAgent() {
return this.selectedAgentId === this.defaultAgentId;
},
// 获取当前智能体的配置schema
configSchema() {
const agent = this.selectedAgentId ? this.agents[this.selectedAgentId] : null;
return agent?.config_schema || {};
},
// 获取可配置项处理x_oap_ui_config
configurableItems() {
const schema = this.configSchema || {};
if (!schema || !schema.configurable_items) return {};
const items = { ...schema.configurable_items };
// 处理x_oap_ui_config将其提升到上一层
Object.keys(items).forEach(key => {
const item = items[key];
if (item && item.x_oap_ui_config) {
items[key] = {
...item,
...item.x_oap_ui_config
};
delete items[key].x_oap_ui_config;
}
});
return items;
},
// 检查配置是否有变更
hasConfigChanges() {
return JSON.stringify(this.agentConfig) !== JSON.stringify(this.originalAgentConfig);
},
// 获取当前智能体的线程列表
currentAgentThreads() {
return this.selectedAgentId ? (this.threads[this.selectedAgentId] || []) : [];
},
// 获取当前线程
currentThread() {
if (!this.currentThreadId || !this.selectedAgentId) return null;
const agentThreads = this.threads[this.selectedAgentId] || [];
return agentThreads.find(thread => thread.id === this.currentThreadId);
},
// 获取当前线程的消息
currentThreadMessages() {
return this.currentThreadId ? (this.threadMessages[this.currentThreadId] || []) : [];
}
},
actions: {
// 初始化store
async initialize() {
if (this.isInitialized) return;
try {
await Promise.all([
this.fetchAgents(),
this.fetchDefaultAgent(),
this.fetchTools()
]);
this.isInitialized = true;
} catch (error) {
console.error('Failed to initialize agent store:', error);
this.error = error.message;
}
},
// 获取智能体列表
async fetchAgents() {
this.isLoadingAgents = true;
this.error = null;
try {
const response = await agentApi.getAgents();
// 将数组转换为以ID为键的对象
this.agents = response.agents.reduce((acc, agent) => {
acc[agent.id] = agent;
return acc;
}, {});
} catch (error) {
console.error('Failed to fetch agents:', error);
this.error = error.message;
throw error;
} finally {
this.isLoadingAgents = false;
}
},
// 获取默认智能体
async fetchDefaultAgent() {
try {
const response = await agentApi.getDefaultAgent();
this.defaultAgentId = response.default_agent_id;
// 如果没有选中的智能体,则选择默认智能体
if (!this.selectedAgentId && this.defaultAgentId) {
this.selectedAgentId = this.defaultAgentId;
}
} catch (error) {
console.error('Failed to fetch default agent:', error);
this.error = error.message;
}
},
// 设置默认智能体
async setDefaultAgent(agentId) {
try {
await agentConfigApi.setDefaultAgent(agentId);
this.defaultAgentId = agentId;
} catch (error) {
console.error('Failed to set default agent:', error);
this.error = error.message;
throw error;
}
},
// 选择智能体
selectAgent(agentId) {
if (this.agents[agentId]) {
this.selectedAgentId = agentId;
// 清空之前的配置
this.agentConfig = {};
this.originalAgentConfig = {};
}
},
// 加载智能体配置
async loadAgentConfig(agentId = null) {
const targetAgentId = agentId || this.selectedAgentId;
if (!targetAgentId) return;
this.isLoadingConfig = true;
this.error = null;
try {
const response = await agentApi.getAgentConfig(targetAgentId);
this.agentConfig = { ...response.config };
this.originalAgentConfig = { ...response.config };
} catch (error) {
console.error('Failed to load agent config:', error);
this.error = error.message;
throw error;
} finally {
this.isLoadingConfig = false;
}
},
// 保存智能体配置
async saveAgentConfig(agentId = null) {
const targetAgentId = agentId || this.selectedAgentId;
if (!targetAgentId) return;
try {
await agentApi.saveAgentConfig(targetAgentId, this.agentConfig);
this.originalAgentConfig = { ...this.agentConfig };
} catch (error) {
console.error('Failed to save agent config:', error);
this.error = error.message;
throw error;
}
},
// 重置智能体配置
resetAgentConfig() {
this.agentConfig = { ...this.originalAgentConfig };
},
// 更新配置项
updateConfigItem(key, value) {
this.agentConfig[key] = value;
},
// 更新智能体配置(支持批量更新)
updateAgentConfig(updates) {
Object.assign(this.agentConfig, updates);
},
// 获取工具列表
async fetchTools() {
this.isLoadingTools = true;
this.error = null;
try {
const response = await agentApi.getTools();
this.availableTools = response.tools;
} catch (error) {
console.error('Failed to fetch tools:', error);
this.error = error.message;
throw error;
} finally {
this.isLoadingTools = false;
}
},
// 清除错误状态
clearError() {
this.error = null;
},
// ==================== 线程管理方法 ====================
// 获取智能体的线程列表
async fetchThreads(agentId = null) {
const targetAgentId = agentId || this.selectedAgentId;
if (!targetAgentId) return;
this.isLoadingThreads = true;
this.error = null;
try {
const threads = await threadApi.getThreads(targetAgentId);
this.threads[targetAgentId] = threads || [];
} catch (error) {
console.error('Failed to fetch threads:', error);
this.error = error.message;
throw error;
} finally {
this.isLoadingThreads = false;
}
},
// 创建新线程
async createThread(agentId, title = '新的对话') {
if (!agentId) return null;
try {
const thread = await threadApi.createThread(agentId, title);
if (thread) {
// 更新线程列表
if (!this.threads[agentId]) {
this.threads[agentId] = [];
}
this.threads[agentId].unshift(thread);
// 设置为当前线程
this.currentThreadId = thread.id;
// 初始化消息列表
this.threadMessages[thread.id] = [];
}
return thread;
} catch (error) {
console.error('Failed to create thread:', error);
this.error = error.message;
throw error;
}
},
// 删除线程
async deleteThread(threadId) {
if (!threadId) return;
try {
await threadApi.deleteThread(threadId);
// 从所有智能体的线程列表中移除
Object.keys(this.threads).forEach(agentId => {
this.threads[agentId] = this.threads[agentId].filter(thread => thread.id !== threadId);
});
// 清理消息
delete this.threadMessages[threadId];
// 如果删除的是当前线程重置当前线程ID
if (this.currentThreadId === threadId) {
this.currentThreadId = null;
}
} catch (error) {
console.error('Failed to delete thread:', error);
this.error = error.message;
throw error;
}
},
// 更新线程标题
async updateThread(threadId, title) {
if (!threadId || !title) return;
try {
await threadApi.updateThread(threadId, title);
// 更新本地线程列表中的标题
Object.keys(this.threads).forEach(agentId => {
const thread = this.threads[agentId].find(t => t.id === threadId);
if (thread) {
thread.title = title;
}
});
} catch (error) {
console.error('Failed to update thread:', error);
this.error = error.message;
throw error;
}
},
// 选择线程
selectThread(threadId) {
this.currentThreadId = threadId;
// 如果没有该线程的消息,初始化空数组
if (threadId && !this.threadMessages[threadId]) {
this.threadMessages[threadId] = [];
}
},
// 获取线程消息
async fetchThreadMessages(threadId) {
if (!threadId) return;
this.isLoadingMessages = true;
this.error = null;
try {
const response = await agentApi.getAgentHistory(this.selectedAgentId, threadId);
this.threadMessages[threadId] = response.history || [];
} catch (error) {
console.error('Failed to fetch thread messages:', error);
this.error = error.message;
throw error;
} finally {
this.isLoadingMessages = false;
}
},
// 发送消息
async sendMessage(agentId, requestData) {
if (!agentId) return null;
try {
const response = await agentApi.sendAgentMessage(agentId, requestData);
return response;
} catch (error) {
console.error('Failed to send message:', error);
this.error = error.message;
throw error;
}
},
// 添加消息到线程
addMessageToThread(threadId, message) {
if (!threadId || !message) return;
if (!this.threadMessages[threadId]) {
this.threadMessages[threadId] = [];
}
this.threadMessages[threadId].push(message);
},
// 更新线程中的消息
updateMessageInThread(threadId, messageIndex, updatedMessage) {
if (!threadId || messageIndex < 0 || !this.threadMessages[threadId]) return;
if (messageIndex < this.threadMessages[threadId].length) {
this.threadMessages[threadId][messageIndex] = updatedMessage;
}
},
// 重置store状态
reset() {
this.agents = {};
this.selectedAgentId = null;
this.defaultAgentId = null;
this.agentConfig = {};
this.originalAgentConfig = {};
this.availableTools = [];
this.threads = {};
this.currentThreadId = null;
this.threadMessages = {};
this.isLoadingAgents = false;
this.isLoadingConfig = false;
this.isLoadingTools = false;
this.isLoadingThreads = false;
this.isLoadingMessages = false;
this.error = null;
this.isInitialized = false;
}
},
// 持久化配置
persist: {
key: 'agent-store',
storage: localStorage,
paths: ['selectedAgentId', 'defaultAgentId'] // 只持久化关键状态
}
});

View File

@ -5,7 +5,7 @@
<div class="header-item">
<a-button class="header-button" @click="openAgentModal">
<Bot size="18" stroke-width="1.75" />
选择{{ selectedAgent.name || '选择智能体' }}
选择{{ selectedAgent?.name || '选择智能体' }}
</a-button>
</div>
</div>
@ -62,8 +62,6 @@
<!-- 中间内容区域 -->
<div class="content">
<AgentChatComponent
:agent-id="selectedAgentId"
:config="agentConfig"
:state="state"
:single-mode="false"
@open-config="toggleConf"
@ -76,13 +74,7 @@
<!-- 配置侧边栏 -->
<AgentConfigSidebar
:isOpen="state.isConfigSidebarOpen"
:selectedAgent="selectedAgent"
:selectedAgentId="selectedAgentId"
:agentConfig="agentConfig"
@close="() => state.isConfigSidebarOpen = false"
@update:agentConfig="(config) => agentConfig = config"
@config-saved="loadAgentConfig"
@config-reset="loadAgentConfig"
/>
</div>
</div>
@ -92,72 +84,53 @@
import { ref, onMounted, reactive, watch, computed, h } from 'vue';
import { useRouter } from 'vue-router';
import {
CloseOutlined,
SettingOutlined,
LinkOutlined,
StarOutlined,
StarFilled,
CheckCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { Bot } from 'lucide-vue-next';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue';
import { useUserStore } from '@/stores/user';
import { agentApi } from '@/apis/agent';
import { useAgentStore } from '@/stores/agent';
import { storeToRefs } from 'pinia';
//
// stores
const router = useRouter();
const userStore = useUserStore();
const agentStore = useAgentStore();
//
const agents = ref({});
const selectedAgentId = ref(null);
const defaultAgentId = ref(null); // ID
// store
const {
agents,
selectedAgentId,
defaultAgentId,
agentConfig,
selectedAgent,
configurableItems,
} = storeToRefs(agentStore);
const state = reactive({
debug_mode: false,
agentModalOpen: false,
isConfigSidebarOpen: false,
isEmptyConfig: computed(() =>
!selectedAgentId.value ||
Object.keys(configurableItems.value).length === 0
Object.keys(configurableItems.value || {}).length === 0
)
});
const selectedAgent = computed(() => agents.value[selectedAgentId.value] || {});
const configSchema = computed(() => selectedAgent.value.config_schema || {});
const configurableItems = computed(() => {
const items = configSchema.value.configurable_items || {};
// x_oap_ui_config
Object.keys(items).forEach(key => {
const item = items[key];
if (item.x_oap_ui_config) {
items[key] = { ...item, ...item.x_oap_ui_config };
delete items[key].x_oap_ui_config;
}
});
return items;
});
//
const agentConfig = ref({});
//
const isDefaultAgent = computed(() => {
return selectedAgentId.value === defaultAgentId.value;
});
// UI
//
const setAsDefaultAgent = async () => {
if (!selectedAgentId.value || !userStore.isAdmin) return;
try {
await agentApi.setDefaultAgent(selectedAgentId.value);
defaultAgentId.value = selectedAgentId.value;
await agentStore.setDefaultAgent(selectedAgentId.value);
message.success('已将当前智能体设为默认');
} catch (error) {
console.error('设置默认智能体错误:', error);
@ -169,97 +142,15 @@ const setAsDefaultAgent = async () => {
// ID
const fetchDefaultAgent = async () => {
try {
const data = await agentApi.getDefaultAgent();
defaultAgentId.value = data.default_agent_id;
console.debug("Default agent ID:", defaultAgentId.value);
} catch (error) {
console.error('获取默认智能体错误:', error);
}
};
// agentStore
//
const fetchAgents = async () => {
try {
const data = await agentApi.getAgents();
//
agents.value = data.agents.reduce((acc, agent) => {
acc[agent.id] = agent;
return acc;
}, {});
// console.log("agents", agents.value);
//
if (selectedAgentId.value) {
loadAgentConfig();
}
} catch (error) {
console.error('获取智能体错误:', error);
}
};
//
// 使store
const loadAgentConfig = async () => {
// BUG:
if (!selectedAgentId.value || !agents.value[selectedAgentId.value]) return;
const agent = agents.value[selectedAgentId.value];
const schema = agent.config_schema || {};
const items = schema.configurable_items || {};
//
agentConfig.value = {};
//
if (schema.system_prompt) {
agentConfig.value.system_prompt = schema.system_prompt;
}
if (schema.model) {
agentConfig.value.model = schema.model;
}
if (schema.tools && schema.tools.length > 0 && schema.tools[0] != 'undefined') {
agentConfig.value.tools = schema.tools;
}
//
Object.keys(items).forEach(key => {
const item = items[key];
//
if (typeof item.default === 'boolean') {
agentConfig.value[key] = item.default;
} else if (item.type === 'list') {
// list
agentConfig.value[key] = Array.isArray(item.default) ? item.default : [];
} else {
agentConfig.value[key] = item.default || '';
}
});
try {
//
const response = await agentApi.getAgentConfig(selectedAgentId.value);
if (response.success && response.config) {
//
Object.keys(response.config).forEach(key => {
if (key in agentConfig.value) {
const item = items[key];
// list
if (item && item.type === 'list') {
agentConfig.value[key] = Array.isArray(response.config[key]) ? response.config[key] : [];
} else {
agentConfig.value[key] = response.config[key];
}
}
});
// console.log(` ${selectedAgentId.value} , ${JSON.stringify(agentConfig.value)}`);
}
await agentStore.loadAgentConfig();
} catch (error) {
console.error('从服务器加载配置出错:', error);
console.error('加载配置出错:', error);
message.error('加载配置失败');
}
};
@ -267,43 +158,6 @@ const handleModelChange = (data) => {
agentConfig.value.model = `${data.provider}/${data.name}`;
}
//
const saveConfig = async () => {
if (!selectedAgentId.value) {
message.error('没有选择智能体');
return;
}
try {
//
await agentApi.saveAgentConfig(selectedAgentId.value, agentConfig.value);
//
message.success('配置已保存到服务器');
console.log("保存配置:", agentConfig.value);
} catch (error) {
console.error('保存配置到服务器出错:', error);
message.error('保存配置到服务器失败');
}
};
//
const resetConfig = async () => {
if (!selectedAgentId.value) {
message.error('没有选择智能体');
return;
}
try {
//
await agentApi.saveAgentConfig(selectedAgentId.value, {});
//
await loadAgentConfig();
message.info('配置已重置');
} catch (error) {
console.error('重置配置出错:', error);
message.error('重置配置失败');
}
};
//
watch(
@ -313,11 +167,9 @@ watch(
}
);
//
// 使store
const selectAgent = (agentId) => {
selectedAgentId.value = agentId;
//
localStorage.setItem('last-selected-agent', agentId);
agentStore.selectAgent(agentId);
//
loadAgentConfig();
};
@ -335,29 +187,27 @@ const selectAgentFromModal = (agentId) => {
//
// 使store
onMounted(async () => {
//
await fetchDefaultAgent();
//
await fetchAgents();
//
try {
//
const lastSelectedAgent = localStorage.getItem('last-selected-agent');
if (lastSelectedAgent && agents.value[lastSelectedAgent]) {
agentStore.selectAgent(lastSelectedAgent);
} else if (defaultAgentId.value && agents.value[defaultAgentId.value]) {
//
agentStore.selectAgent(defaultAgentId.value);
} else if (Object.keys(agents.value).length > 0) {
//
agentStore.selectAgent(Object.keys(agents.value)[0]);
}
//
const lastSelectedAgent = localStorage.getItem('last-selected-agent');
if (lastSelectedAgent && agents.value[lastSelectedAgent]) {
selectedAgentId.value = lastSelectedAgent;
} else if (defaultAgentId.value && agents.value[defaultAgentId.value]) {
//
selectedAgentId.value = defaultAgentId.value;
} else if (Object.keys(agents.value).length > 0) {
//
selectedAgentId.value = Object.keys(agents.value)[0];
//
await loadAgentConfig();
} catch (error) {
console.error('初始化失败:', error);
message.error('初始化失败');
}
//
loadAgentConfig();
});
//
@ -445,6 +295,35 @@ const toggleConf = () => {
// padding: var(--gap-radius);
// gap: var(--gap-radius);
.content {
flex: 1;
display: flex;
flex-direction: column;
}
.no-agent-selected {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-content);
}
.no-agent-content {
text-align: center;
color: var(--text-secondary);
svg {
margin-bottom: 16px;
opacity: 0.6;
}
h3 {
margin-bottom: 16px;
color: var(--text-primary);
}
}
// .content {
// border-radius: var(--gap-radius);
// border: 1px solid var(--gray-300);

View File

@ -64,12 +64,13 @@ import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useInfoStore } from '@/stores/info'
import { agentApi } from '@/apis/agent'
import { useAgentStore } from '@/stores/agent'
import UserInfoComponent from '@/components/UserInfoComponent.vue'
const router = useRouter()
const userStore = useUserStore()
const infoStore = useInfoStore()
const agentStore = useAgentStore()
const goToChat = async () => {
//
@ -90,23 +91,11 @@ const goToChat = async () => {
//
try {
//
const data = await agentApi.getDefaultAgent();
if (data.default_agent_id) {
// 使
router.push(`/agent/${data.default_agent_id}`);
} else {
//
const agentData = await agentApi.getAgents();
if (agentData.agents && agentData.agents.length > 0) {
router.push(`/agent/${agentData.agents[0].id}`);
} else {
// 退chat
router.push("/chat");
}
}
const defaultAgent = agentStore.defaultAgent;
router.push(`/agent/${defaultAgent.id}`);
} catch (error) {
console.error('跳转到智能体页面失败:', error);
router.push("/chat");
router.push("/");
}
};

View File

@ -170,13 +170,14 @@ import { ref, reactive, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { useInfoStore } from '@/stores/info';
import { useAgentStore } from '@/stores/agent';
import { message } from 'ant-design-vue';
import { agentApi } from '@/apis/agent';
import { healthApi } from '@/apis/system_api';
import { UserOutlined, LockOutlined, WechatOutlined, QrcodeOutlined, ThunderboltOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
const router = useRouter();
const userStore = useUserStore();
const infoStore = useInfoStore();
const agentStore = useAgentStore();
// infoStore
const loginBgImage = computed(() => {
@ -248,18 +249,20 @@ const handleLogin = async () => {
//
try {
// agentStore
await agentStore.initialize();
//
const data = await agentApi.getDefaultAgent();
if (data.default_agent_id) {
if (agentStore.defaultAgentId) {
//
router.push(`/agent/${data.default_agent_id}`);
router.push(`/agent/${agentStore.defaultAgentId}`);
return;
}
//
const agentData = await agentApi.getAgents();
if (agentData.agents && agentData.agents.length > 0) {
router.push(`/agent/${agentData.agents[0].id}`);
const agentIds = Object.keys(agentStore.agents);
if (agentIds.length > 0) {
router.push(`/agent/${agentIds[0]}`);
return;
}