diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 42c72087..67abb29b 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -84,11 +84,8 @@ - @@ -219,7 +216,6 @@ import HumanApprovalModal from '@/components/HumanApprovalModal.vue' import { useApproval } from '@/composables/useApproval' import { useAgentStreamHandler } from '@/composables/useAgentStreamHandler' import AgentPanel from '@/components/AgentPanel.vue' -import ConversationSourcesPanel from '@/components/ConversationSourcesPanel.vue' // ==================== PROPS & EMITS ==================== const props = defineProps({ @@ -237,7 +233,9 @@ const { defaultAgentId, selectedAgentConfigId, agentConfig, - configurableItems + configurableItems, + availableKnowledgeBases, + availableMcps } = storeToRefs(agentStore) // ==================== LOCAL CHAT & UI STATE ==================== @@ -294,10 +292,6 @@ const localUIState = reactive({ isInitialRender: true }) -// Mention resources -const availableKnowledgeBases = ref([]) -const availableMcps = ref([]) - // Agent Panel State const isAgentPanelOpen = ref(false) const isResizing = ref(false) @@ -1135,19 +1129,6 @@ const resumeActiveRunForThread = async (threadId) => { clearActiveRunSnapshot(threadId) } -const fetchMentionResources = async () => { - try { - const [dbsRes, mcpsRes] = await Promise.all([ - databaseApi.getAccessibleDatabases().catch(() => ({ databases: [] })), - mcpApi.getMcpServers().catch(() => ({ data: [] })) - ]) - availableKnowledgeBases.value = dbsRes.databases || [] - availableMcps.value = mcpsRes.data || [] - } catch (e) { - console.warn('Failed to fetch mention resources', e) - } -} - const ensureActiveThread = async (title = '新的对话') => { if (currentChatId.value) return currentChatId.value try { @@ -1648,7 +1629,7 @@ const showMsgRefs = (msg) => { // 只有真正完成的消息才显示 refs if (msg.isLast && msg.status === 'finished') { - return ['copy'] + return ['copy', 'sources'] } return false } @@ -1693,7 +1674,6 @@ const initAll = async () => { if (!agentStore.isInitialized) { await agentStore.initialize() } - await fetchMentionResources() } catch (error) { handleChatError(error, 'load') } diff --git a/web/src/components/AgentMessageComponent.vue b/web/src/components/AgentMessageComponent.vue index 1b5b4959..30bf6b1b 100644 --- a/web/src/components/AgentMessageComponent.vue +++ b/web/src/components/AgentMessageComponent.vue @@ -91,6 +91,7 @@ :message="message" :show-refs="showRefs" :is-latest-message="isLatestMessage" + :sources="messageSources" @retry="emit('retry')" @openRefs="emit('openRefs', $event)" /> @@ -115,6 +116,7 @@ import { useAgentStore } from '@/stores/agent' import { useInfoStore } from '@/stores/info' import { useThemeStore } from '@/stores/theme' import { storeToRefs } from 'pinia' +import { MessageProcessor } from '@/utils/messageProcessor' import { MdPreview } from 'md-editor-v3' import 'md-editor-v3/lib/preview.css' @@ -223,9 +225,18 @@ const getErrorMessage = computed(() => { // 引入智能体 store const agentStore = useAgentStore() +const { availableKnowledgeBases } = storeToRefs(agentStore) const infoStore = useInfoStore() const themeStore = useThemeStore() +// 提取消息来源 +const messageSources = computed(() => { + if (props.message.type === 'ai') { + return MessageProcessor.extractSourcesFromMessage(props.message, availableKnowledgeBases.value) + } + return { knowledgeChunks: [], webSources: [] } +}) + // 主题设置 - 根据系统主题动态切换 const theme = computed(() => (themeStore.isDark ? 'dark' : 'light')) diff --git a/web/src/components/ConversationSourcesPanel.vue b/web/src/components/ConversationSourcesPanel.vue deleted file mode 100644 index 9b7ae43f..00000000 --- a/web/src/components/ConversationSourcesPanel.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - diff --git a/web/src/components/RefsComponent.vue b/web/src/components/RefsComponent.vue index 2e4302c6..6abe2c65 100644 --- a/web/src/components/RefsComponent.vue +++ b/web/src/components/RefsComponent.vue @@ -41,6 +41,31 @@ title="重新生成" > + + +
+ + + + 来源 + + + + + + + +
+ +
@@ -68,8 +93,10 @@ import { ref, computed, reactive, watch } from 'vue' import { useClipboard } from '@vueuse/core' import { message } from 'ant-design-vue' -import { ThumbsUp, ThumbsDown, Bot, Copy, Check, RotateCcw } from 'lucide-vue-next' +import { ThumbsUp, ThumbsDown, Bot, Copy, Check, RotateCcw, BookOpen, ChevronDown } from 'lucide-vue-next' import { agentApi } from '@/apis' +import KnowledgeSourceSection from '@/components/KnowledgeSourceSection.vue' +import WebSearchSourceSection from '@/components/WebSearchSourceSection.vue' const emit = defineEmits(['retry', 'openRefs']) const props = defineProps({ @@ -81,11 +108,39 @@ const props = defineProps({ isLatestMessage: { type: Boolean, default: false + }, + sources: { + type: Object, + default: () => ({}) } }) const msg = ref(props.message) +// Sources state +const isSourcesExpanded = ref(false) + +const knowledgeChunks = computed(() => + Array.isArray(props.sources?.knowledgeChunks) ? props.sources.knowledgeChunks : [] +) +const webSources = computed(() => + Array.isArray(props.sources?.webSources) ? props.sources.webSources : [] +) + +const hasSources = computed( + () => + knowledgeChunks.value.length > 0 || + webSources.value.length > 0 +) + +const sourceCount = computed( + () => knowledgeChunks.value.length + webSources.value.length +) + +const toggleSources = () => { + isSourcesExpanded.value = !isSourcesExpanded.value +} + // Feedback state const feedbackState = reactive({ hasSubmitted: false, @@ -272,11 +327,12 @@ const cancelDislike = () => { diff --git a/web/src/stores/agent.js b/web/src/stores/agent.js index 868ab388..6f6d0c93 100644 --- a/web/src/stores/agent.js +++ b/web/src/stores/agent.js @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import { agentApi } from '@/apis/agent_api' +import { agentApi, databaseApi, mcpApi } from '@/apis' import { handleChatError } from '@/utils/errorHandler' import { useUserStore } from '@/stores/user' @@ -14,6 +14,10 @@ export const useAgentStore = defineStore( const selectedAgentId = ref(null) const defaultAgentId = ref(null) + // 资源相关状态 + const availableKnowledgeBases = ref([]) + const availableMcps = ref([]) + // 智能体配置相关状态 const agentConfig = ref({}) const originalAgentConfig = ref({}) @@ -91,6 +95,22 @@ export const useAgentStore = defineStore( }) // ==================== 方法 ==================== + /** + * 获取可提及的资源(知识库、MCP等) + */ + async function fetchMentionResources() { + try { + const [dbsRes, mcpsRes] = await Promise.all([ + databaseApi.getAccessibleDatabases().catch(() => ({ databases: [] })), + mcpApi.getMcpServers().catch(() => ({ data: [] })) + ]) + availableKnowledgeBases.value = dbsRes.databases || [] + availableMcps.value = mcpsRes.data || [] + } catch (e) { + console.warn('Failed to fetch mention resources:', e) + } + } + /** * 初始化 store */ @@ -104,6 +124,7 @@ export const useAgentStore = defineStore( try { await fetchAgents() await fetchDefaultAgent() + await fetchMentionResources() if (!selectedAgent.value) { if (defaultAgent.value) { @@ -449,6 +470,8 @@ export const useAgentStore = defineStore( agents.value = [] selectedAgentId.value = null defaultAgentId.value = null + availableKnowledgeBases.value = [] + availableMcps.value = [] agentConfig.value = {} originalAgentConfig.value = {} agentConfigs.value = {} @@ -468,6 +491,8 @@ export const useAgentStore = defineStore( agents, selectedAgentId, defaultAgentId, + availableKnowledgeBases, + availableMcps, agentConfig, originalAgentConfig, agentDetails, @@ -495,6 +520,7 @@ export const useAgentStore = defineStore( fetchAgents, fetchAgentDetail, fetchDefaultAgent, + fetchMentionResources, setDefaultAgent, selectAgent, selectAgentConfig, diff --git a/web/src/utils/messageProcessor.js b/web/src/utils/messageProcessor.js index 88b19fa7..c0928400 100644 --- a/web/src/utils/messageProcessor.js +++ b/web/src/utils/messageProcessor.js @@ -249,6 +249,23 @@ export class MessageProcessor { return webSources } + /** + * 提取单个消息中的来源 + * @param {Object} message - 消息对象 + * @param {Array} databases - 知识库列表 + * @returns {{knowledgeChunks: Array, webSources: Array}} + */ + static extractSourcesFromMessage(message, databases = []) { + if (!message || message.type !== 'ai') return { knowledgeChunks: [], webSources: [] } + + // 复用提取逻辑,通过构建临时对话对象 + const mockConv = { messages: [message] } + return { + knowledgeChunks: MessageProcessor.extractKnowledgeChunksFromConversation(mockConv, databases), + webSources: MessageProcessor.extractWebSourcesFromConversation(mockConv), + } + } + /** * 提取一轮对话中的全部来源(知识库+网络搜索) * @param {Object} conv - 单轮对话