From aedefe6b5e0fefb3256adb5aa468873ab62358f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 27 Jul 2026 18:27:01 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E4=B8=8E=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=B9=95=E5=B8=83?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 分阶段加载智能体配置并按需缓存 MCP 工具 - 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线 - 隔离 Tinyflow Store 实例并补充数据同步与回归测试 --- .../app/src/views/ai/agents/AgentDesigner.vue | 248 ++++++++++----- .../app/src/views/ai/agents/api.ts | 10 + .../agents/components/AgentInspectorPanel.vue | 79 ++++- .../ai/agents/components/AgentToolForm.vue | 7 +- .../agent-studio/AgentStudioCanvas.vue | 285 +++++++++++------- .../agent-studio/AgentStudioEdgeLayer.vue | 25 +- .../agent-studio/useAgentStudioLayout.ts | 10 +- .../agent-studio/useAgentStudioModel.test.ts | 41 +++ .../agent-studio/useAgentStudioModel.ts | 75 +++-- .../src/views/ai/agents/mcpToolLoader.test.ts | 39 +++ .../app/src/views/ai/agents/mcpToolLoader.ts | 35 +++ .../src/views/ai/workflow/WorkflowDesign.vue | 28 +- .../packages/tinyflow-ui/src/Tinyflow.ts | 57 ++-- .../src/components/TinyflowComponent.svelte | 16 +- .../src/components/TinyflowCore.svelte | 267 +++++++++++++--- .../core/DefinedParameterItem.svelte | 3 +- .../src/components/core/NodeWrapper.svelte | 34 +-- .../src/components/nodes/ConditionNode.svelte | 3 +- .../src/components/nodes/CustomNode.svelte | 6 +- .../src/components/utils/nodeSizeObserver.ts | 16 + .../components/utils/useCopyNode.svelte.ts | 3 +- .../utils/useCopyPasteHandler.svelte.ts | 3 +- .../components/utils/useDeleteEdge.svelte.ts | 3 +- .../components/utils/useDeleteNode.svelte.ts | 3 +- .../useEnsureParentInNodesBefore.svelte.ts | 3 +- .../utils/useGetEdgesBySource.svelte.ts | 3 +- .../utils/useGetEdgesByTarget.svelte.ts | 3 +- .../src/components/utils/useGetNode.svelte.ts | 3 +- .../useGetNodeRelativePosition.svelte.ts | 3 +- .../utils/useGetNodesFromSource.svelte.ts | 3 +- .../utils/useUpdateEdgeData.svelte.ts | 3 +- .../tinyflow-ui/src/store/stores.svelte.ts | 20 +- .../src/tinyflow-store-isolation.test.ts | 88 ++++++ .../packages/tinyflow-ui/src/types.ts | 10 + .../src/utils/nodeInteraction.test.ts | 49 +++ .../tinyflow-ui/src/utils/nodeInteraction.ts | 28 +- .../packages/tinyflow-vue/src/Tinyflow.vue | 83 ++--- .../tinyflow-vue/src/dataSync.test.ts | 76 +++++ .../packages/tinyflow-vue/src/dataSync.ts | 86 ++++++ 39 files changed, 1349 insertions(+), 408 deletions(-) create mode 100644 easyflow-ui-admin/app/src/views/ai/agents/mcpToolLoader.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/agents/mcpToolLoader.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/nodeSizeObserver.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/tinyflow-store-isolation.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/utils/nodeInteraction.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-vue/src/dataSync.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-vue/src/dataSync.ts 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); + } +});