diff --git a/easyflow-ui-admin/app/src/views/ai/agents/AgentDesigner.vue b/easyflow-ui-admin/app/src/views/ai/agents/AgentDesigner.vue index 141d141d..dffb17c9 100644 --- a/easyflow-ui-admin/app/src/views/ai/agents/AgentDesigner.vue +++ b/easyflow-ui-admin/app/src/views/ai/agents/AgentDesigner.vue @@ -6,7 +6,7 @@ import type { AgentValidationIssue, } from './types'; -import { computed, onMounted, ref } from 'vue'; +import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { ElMessage, ElMessageBox } from 'element-plus'; @@ -23,6 +23,8 @@ import { import { getAgentDetail, getAgentModels, + getMcpPage, + getMcpTools, getPublishedKnowledgeList, saveAgent, submitAgentOfflineApproval, @@ -35,6 +37,7 @@ import AgentStudioCanvas from './components/agent-studio/AgentStudioCanvas.vue'; import AgentCommandBar from './components/AgentCommandBar.vue'; import AgentInspectorPanel from './components/AgentInspectorPanel.vue'; import { useAgentDesignerState } from './composables/useAgentDesignerState'; +import { createMcpToolLoader } from './mcpToolLoader'; const route = useRoute(); const router = useRouter(); @@ -56,10 +59,13 @@ const { validate, } = useAgentDesignerState(); -const pageLoading = ref(false); +const pageLoading = ref(true); +const contentReady = ref(false); +const canvasActive = ref(true); const saveLoading = ref(false); const offlineLoading = ref(false); const publishLoading = ref(false); +const mcpToolsLoading = ref>({}); const issues = ref([]); const categories = ref([]); const models = ref([]); @@ -67,6 +73,10 @@ const knowledges = ref([]); const workflows = ref([]); const pluginTools = ref([]); const mcps = ref([]); +const fetchMcpToolResource = createMcpToolLoader(async (id) => { + const res = await getMcpTools(id); + return res.errorCode === 0 ? res.data : undefined; +}); const isNew = computed(() => String(route.params.id || '') === 'new'); const publishText = computed(() => { @@ -116,14 +126,23 @@ const offlineDisabled = computed(() => { }); onMounted(async () => { - pageLoading.value = true; + void loadDeferredOptions(); try { - await Promise.all([loadOptions(), loadAgent()]); + await Promise.all([loadCriticalOptions(), loadAgent()]); } finally { + contentReady.value = true; pageLoading.value = false; } }); +onActivated(() => { + canvasActive.value = true; +}); + +onDeactivated(() => { + canvasActive.value = false; +}); + async function loadAgent() { if (isNew.value) { reset(); @@ -191,13 +210,33 @@ function syncNavTitle(title: string, options: { force?: boolean } = {}) { }); } -async function loadOptions() { - const [categoryRes, modelRes, knowledgeRes, workflowRes, pluginRes, mcpRes] = - await Promise.all([ - api.get('/api/v1/agentCategory/visibleList', { - params: { sortKey: 'sortNo', sortType: 'asc' }, - }), - getAgentModels(), +async function loadCriticalOptions() { + const [categoryResult, modelResult] = await Promise.allSettled([ + api.get('/api/v1/agentCategory/visibleList', { + params: { sortKey: 'sortNo', sortType: 'asc' }, + }), + getAgentModels(), + ]); + + if (categoryResult.status === 'fulfilled') { + categories.value = (categoryResult.value.data || []).map((item: any) => ({ + label: item.categoryName || item.name, + value: String(item.id), + raw: item, + })); + } + if (modelResult.status === 'fulfilled') { + models.value = (modelResult.value.data || []).map((item: any) => ({ + label: item.title || item.name, + value: String(item.id), + raw: item, + })); + } +} + +async function loadDeferredOptions() { + const [knowledgeResult, workflowResult, pluginResult, mcpResult] = + await Promise.allSettled([ getPublishedKnowledgeList(), api.get('/api/v1/workflow/page', { params: { pageNumber: 1, pageSize: 200 }, @@ -205,37 +244,37 @@ async function loadOptions() { api.get('/api/v1/plugin/pageByCategory', { params: { pageNumber: 1, pageSize: 200, category: 0 }, }), - api.get('/api/v1/mcp/pageTools', { - params: { pageNumber: 1, pageSize: 200, status: 1 }, - }), + getMcpPage(), ]); - categories.value = (categoryRes.data || []).map((item: any) => ({ - label: item.categoryName || item.name, - value: String(item.id), - raw: item, - })); - models.value = (modelRes.data || []).map((item: any) => ({ - label: item.title || item.name, - value: String(item.id), - raw: item, - })); - knowledges.value = (knowledgeRes.data || []).map((item: any) => ({ - label: item.title || item.name, - value: String(item.id), - raw: item, - })); - workflows.value = ( - (workflowRes.data?.records || workflowRes.data || []) as any[] - ).map((item) => ({ - label: item.title || item.name, - value: String(item.id), - raw: item, - })); - pluginTools.value = flattenPluginTools( - pluginRes.data?.records || pluginRes.data || [], - ); - mcps.value = mapMcpOptions(mcpRes.data?.records || mcpRes.data || []); + if (knowledgeResult.status === 'fulfilled') { + knowledges.value = (knowledgeResult.value.data || []).map((item: any) => ({ + label: item.title || item.name, + value: String(item.id), + raw: item, + })); + } + if (workflowResult.status === 'fulfilled') { + workflows.value = ( + (workflowResult.value.data?.records || + workflowResult.value.data || + []) as any[] + ).map((item) => ({ + label: item.title || item.name, + value: String(item.id), + raw: item, + })); + } + if (pluginResult.status === 'fulfilled') { + pluginTools.value = flattenPluginTools( + pluginResult.value.data?.records || pluginResult.value.data || [], + ); + } + if (mcpResult.status === 'fulfilled') { + mcps.value = mapMcpOptions( + mcpResult.value.data?.records || mcpResult.value.data || [], + ); + } } function flattenPluginTools(list: any[]): AgentOption[] { @@ -269,6 +308,56 @@ function mapMcpOptions(list: any[]): AgentOption[] { })); } +async function loadMcpToolsForOption(id: number | string) { + const key = String(id || '').trim(); + if (!key || mcpToolsLoading.value[key]) return; + + mcpToolsLoading.value = { + ...mcpToolsLoading.value, + [key]: true, + }; + try { + const resource = await fetchMcpToolResource(key); + if (!resource) return; + + const currentOption = mcps.value.find((item) => String(item.value) === key); + const mergedResource = { + ...currentOption?.raw, + ...resource, + tools: Array.isArray(resource.tools) ? resource.tools : [], + }; + if (currentOption) { + mcps.value = mcps.value.map((item) => + String(item.value) === key + ? { + ...item, + label: + resource.title || resource.name || currentOption.label || 'MCP', + raw: mergedResource, + } + : item, + ); + } + state.toolBindings.forEach((binding) => { + if ( + String(binding.toolType || '').toUpperCase() === 'MCP' && + String(binding.targetId || '') === key + ) { + binding.resourceSummary = { + ...binding.resourceSummary, + ...mergedResource, + }; + } + }); + } catch (error) { + console.error('加载 MCP 工具失败', error); + } finally { + const nextLoading = { ...mcpToolsLoading.value }; + delete nextLoading[key]; + mcpToolsLoading.value = nextLoading; + } +} + async function handleAdd(kind: AgentCapabilityKind) { if (kind === 'knowledge') { addKnowledgeNode(); @@ -417,43 +506,48 @@ function handleCloseTryout() { diff --git a/easyflow-ui-admin/app/src/views/ai/agents/api.ts b/easyflow-ui-admin/app/src/views/ai/agents/api.ts index 0532368e..80ac3956 100644 --- a/easyflow-ui-admin/app/src/views/ai/agents/api.ts +++ b/easyflow-ui-admin/app/src/views/ai/agents/api.ts @@ -109,3 +109,13 @@ export function getPublishedKnowledgeList() { params: { publishedOnly: true }, }); } + +export function getMcpPage() { + return api.get>('/api/v1/mcp/page', { + params: { pageNumber: 1, pageSize: 200, status: 1 }, + }); +} + +export function getMcpTools(id: number | string) { + return api.post>('/api/v1/mcp/getMcpTools', { id }); +} diff --git a/easyflow-ui-admin/app/src/views/ai/agents/components/AgentInspectorPanel.vue b/easyflow-ui-admin/app/src/views/ai/agents/components/AgentInspectorPanel.vue index 08bbc952..fc16da73 100644 --- a/easyflow-ui-admin/app/src/views/ai/agents/components/AgentInspectorPanel.vue +++ b/easyflow-ui-admin/app/src/views/ai/agents/components/AgentInspectorPanel.vue @@ -5,22 +5,27 @@ import type { AgentValidationIssue, } from '../types'; -import { computed, nextTick, ref, watch } from 'vue'; +import { + computed, + defineAsyncComponent, + nextTick, + onBeforeUnmount, + onMounted, + ref, + watch, +} from 'vue'; import { Close } from '@element-plus/icons-vue'; import { ElButton, ElTabPane, ElTabs } from 'element-plus'; import AgentBaseForm from './AgentBaseForm.vue'; -import AgentInteractionForm from './AgentInteractionForm.vue'; -import AgentKnowledgeForm from './AgentKnowledgeForm.vue'; -import AgentToolForm from './AgentToolForm.vue'; -import AgentTryoutPanel from './AgentTryoutPanel.vue'; const props = defineProps<{ categories: AgentOption[]; issues: AgentValidationIssue[]; knowledges: AgentOption[]; mcps: AgentOption[]; + mcpToolsLoading: Record; models: AgentOption[]; pluginTools: AgentOption[]; state: AgentDraftState; @@ -30,10 +35,24 @@ const props = defineProps<{ const emit = defineEmits<{ change: []; closeTryout: []; + loadMcpTools: [id: string]; removeCapability: []; selectIssue: [nodeId: string]; }>(); +const loadAgentInteractionForm = () => import('./AgentInteractionForm.vue'); +const loadAgentKnowledgeForm = () => import('./AgentKnowledgeForm.vue'); +const loadAgentToolForm = () => import('./AgentToolForm.vue'); +const loadAgentTryoutPanel = () => import('./AgentTryoutPanel.vue'); +const AgentInteractionForm = defineAsyncComponent(loadAgentInteractionForm); +const AgentKnowledgeForm = defineAsyncComponent(loadAgentKnowledgeForm); +const AgentToolForm = defineAsyncComponent(loadAgentToolForm); +const AgentTryoutPanel = defineAsyncComponent(loadAgentTryoutPanel); + +interface AgentInteractionFormExpose { + focusField: (field: string) => Promise | void; +} + const selectedKnowledge = computed(() => { if (!props.state.selectedNodeId.startsWith('knowledge:')) return; const localId = props.state.selectedNodeId.slice('knowledge:'.length); @@ -41,7 +60,7 @@ const selectedKnowledge = computed(() => { }); const activeBaseTab = ref<'basic' | 'interaction'>('basic'); -const interactionForm = ref>(); +const interactionForm = ref(); const selectedModel = computed(() => props.models.find((item) => item.value === String(props.state.agent.modelId)), ); @@ -56,7 +75,7 @@ function isInteractionIssue(issue?: AgentValidationIssue) { async function focusIssue(issue: AgentValidationIssue) { if (issue.nodeId !== 'agent-base') return; activeBaseTab.value = isInteractionIssue(issue) ? 'interaction' : 'basic'; - if (isInteractionIssue(issue)) { + if (isInteractionIssue(issue) && issue.field) { await nextTick(); await interactionForm.value?.focusField(issue.field); } @@ -94,6 +113,49 @@ const selectedToolOptions = computed(() => { if (selectedToolKind.value === 'mcp') return props.mcps; return props.pluginTools; }); + +watch( + () => + selectedToolKind.value === 'mcp' + ? String(selectedTool.value?.targetId || '') + : '', + (mcpId) => { + if (mcpId) { + emit('loadMcpTools', mcpId); + } + }, + { immediate: true }, +); + +let prefetchHandle: number | undefined; +let prefetchTimer: number | undefined; + +onMounted(() => { + const prefetchHiddenPanels = () => { + void Promise.allSettled([ + loadAgentInteractionForm(), + loadAgentKnowledgeForm(), + loadAgentToolForm(), + loadAgentTryoutPanel(), + ]); + }; + if (typeof window.requestIdleCallback === 'function') { + prefetchHandle = window.requestIdleCallback(prefetchHiddenPanels, { + timeout: 3000, + }); + return; + } + prefetchTimer = window.setTimeout(prefetchHiddenPanels, 1000); +}); + +onBeforeUnmount(() => { + if (prefetchHandle !== undefined) { + window.cancelIdleCallback(prefetchHandle); + } + if (prefetchTimer !== undefined) { + window.clearTimeout(prefetchTimer); + } +});