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="重新生成"
>
+
+
+
+
+
+
+ 来源
+
+ {{ sourceCount }}
+
+
+
+
+
+
+
+
+
+
@@ -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 - 单轮对话