perf: 优化智能体与工作流幕布渲染性能

- 分阶段加载智能体配置并按需缓存 MCP 工具

- 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线

- 隔离 Tinyflow Store 实例并补充数据同步与回归测试
This commit is contained in:
2026-07-27 18:27:01 +08:00
parent dc7e46260b
commit aedefe6b5e
39 changed files with 1349 additions and 408 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type {AgentDraftState, AgentOption} from '../../types';
import type { AgentDraftState, AgentOption } from '../../types';
import type {
AgentStudioCanvasSize,
AgentStudioConnectionView,
@@ -10,36 +10,46 @@ import type {
import {
computed,
createApp,
h,
nextTick,
onBeforeUnmount,
onMounted,
reactive,
ref,
render as renderVueVNode,
shallowRef,
watch,
} from 'vue';
import {usePreferences} from '@easyflow/preferences';
import { usePreferences } from '@easyflow/preferences';
import {Tinyflow} from '@tinyflow-ai/vue';
import { Tinyflow } from '@tinyflow-ai/vue';
import {useAgentStudioLayout} from '../../composables/agent-studio/useAgentStudioLayout';
import {useAgentStudioModel} from '../../composables/agent-studio/useAgentStudioModel';
import { useAgentStudioLayout } from '../../composables/agent-studio/useAgentStudioLayout';
import { useAgentStudioModel } from '../../composables/agent-studio/useAgentStudioModel';
import AgentStudioBaseNode from './AgentStudioBaseNode.vue';
import AgentStudioCapabilityNode from './AgentStudioCapabilityNode.vue';
import AgentStudioEdgeLayer from './AgentStudioEdgeLayer.vue';
import '@tinyflow-ai/vue/dist/index.css';
const props = defineProps<{
knowledgeOptions?: AgentOption[];
mcpOptions?: AgentOption[];
pluginOptions?: AgentOption[];
selectedNodeId: string;
state: AgentDraftState;
workflowOptions?: AgentOption[];
}>();
const props = withDefaults(
defineProps<{
active?: boolean;
knowledgeOptions?: AgentOption[];
mcpOptions?: AgentOption[];
pluginOptions?: AgentOption[];
selectedNodeId: string;
state: AgentDraftState;
workflowOptions?: AgentOption[];
}>(),
{
active: true,
knowledgeOptions: () => [],
mcpOptions: () => [],
pluginOptions: () => [],
workflowOptions: () => [],
},
);
const emit = defineEmits<{
select: [nodeId: string];
@@ -61,25 +71,33 @@ const canvasModel = useAgentStudioModel(
workflow: props.workflowOptions || [],
}),
);
const liveNodes = ref<AgentStudioNodeView[]>([]);
const liveViewport = ref<AgentStudioViewport>({ x: 250, y: 100, zoom: 1 });
const flowData = computed(() => {
const liveNodes = shallowRef<AgentStudioNodeView[]>([]);
const liveViewport = shallowRef<AgentStudioViewport>({
x: 250,
y: 100,
zoom: 1,
});
function buildFlowData() {
const liveNodeMap = new Map(liveNodes.value.map((node) => [node.id, node]));
return {
edges: canvasModel.value.edges.map((edge) => ({ ...edge })),
edges: canvasModel.value.edges,
nodes: canvasModel.value.nodes.map((node) => {
const liveNode = liveNodeMap.get(node.id);
const position = liveNode?.position || node.position;
if (position === node.position) {
return node;
}
return {
...node,
data: { ...node.data },
position: { ...(liveNode?.position || node.position) },
position,
};
}),
viewport: layout.viewport ? { ...layout.viewport } : undefined,
};
});
const lastCaptureSignature = ref('');
const lastFlowDataSignature = ref('');
}
let lastCapturedEdges: unknown;
let lastCapturedNodes: unknown;
let lastCapturedViewport: unknown;
const customNodes = computed(() => ({
agentStudioBase: {
@@ -90,6 +108,7 @@ const customNodes = computed(() => ({
onUpdate: (parent: HTMLElement, node: any) => {
renderVueNode(parent, AgentStudioBaseNode, node.data);
},
onDestroy: unmountVueNode,
},
agentStudioCapability: {
presentation: 'plain',
@@ -99,6 +118,7 @@ const customNodes = computed(() => ({
onUpdate: (parent: HTMLElement, node: any) => {
renderVueNode(parent, AgentStudioCapabilityNode, node.data);
},
onDestroy: unmountVueNode,
},
}));
@@ -116,7 +136,7 @@ const connections = computed<AgentStudioConnectionView[]>(() => {
return {
active: false,
id: edge.id,
path: buildConnectionPath(source, target, liveViewport.value),
path: buildConnectionPath(source, target),
sourceId: edge.source,
targetId: edge.target,
};
@@ -132,37 +152,28 @@ function renderVueNode(
const current = (parent as any).__agentStudioNodeView as
| undefined
| {
app: ReturnType<typeof createApp>;
component: any;
data: AgentStudioNodeData;
signature: string;
source: AgentStudioNodeData;
};
const signature = JSON.stringify(data);
if (current && current.component === component) {
if (current.signature === signature) {
if (current.source === data) {
return;
}
Object.assign(current.data, data);
current.signature = signature;
return;
current.source = data;
}
current?.app.unmount();
parent.textContent = '';
const reactiveData = reactive({ ...data }) as AgentStudioNodeData;
const app = createApp({
render: () => h(component, { data: reactiveData }),
});
app.mount(parent);
renderVueVNode(h(component, { data }), parent);
(parent as any).__agentStudioNodeView = {
app,
component,
data: reactiveData,
signature,
source: data,
};
}
function unmountVueNode(parent: HTMLElement) {
renderVueVNode(null, parent);
delete (parent as any).__agentStudioNodeView;
}
function getNodeSize(node: AgentStudioNodeView) {
return {
height: node.height || (node.data.kind === 'base' ? 104 : 78),
@@ -170,20 +181,9 @@ function getNodeSize(node: AgentStudioNodeView) {
};
}
function toScreenPoint(
point: { x: number; y: number },
viewport: AgentStudioViewport,
) {
return {
x: point.x * viewport.zoom + viewport.x,
y: point.y * viewport.zoom + viewport.y,
};
}
function buildConnectionPath(
source: AgentStudioNodeView,
target: AgentStudioNodeView,
viewport: AgentStudioViewport,
) {
const sourceSize = getNodeSize(source);
const targetSize = getNodeSize(target);
@@ -204,80 +204,119 @@ function buildConnectionPath(
x: target.position.x + (sourceOnLeft ? targetSize.width : 0),
y: targetCenter.y,
};
const screenStart = toScreenPoint(start, viewport);
const screenEnd = toScreenPoint(end, viewport);
const midX = (screenStart.x + screenEnd.x) / 2;
const midX = (start.x + end.x) / 2;
return [
`M ${screenStart.x} ${screenStart.y}`,
`L ${midX} ${screenStart.y}`,
`L ${midX} ${screenEnd.y}`,
`L ${screenEnd.x} ${screenEnd.y}`,
`M ${start.x} ${start.y}`,
`L ${midX} ${start.y}`,
`L ${midX} ${end.y}`,
`L ${end.x} ${end.y}`,
].join(' ');
}
function handleDataChange(data: any) {
const signature = buildFlowDataSignature(data);
if (signature === lastCaptureSignature.value) {
if (!props.active) {
return;
}
lastCaptureSignature.value = signature;
const edges = Array.isArray(data?.edges) ? data.edges : undefined;
const nodes = Array.isArray(data?.nodes)
? (data.nodes as AgentStudioNodeView[])
: undefined;
const viewport = data?.viewport as AgentStudioViewport | undefined;
if (
edges === lastCapturedEdges &&
nodes === lastCapturedNodes &&
viewport === lastCapturedViewport
) {
return;
}
lastCapturedEdges = edges;
lastCapturedNodes = nodes;
lastCapturedViewport = viewport;
if (Array.isArray(data?.nodes)) {
liveNodes.value = data.nodes as AgentStudioNodeView[];
if (nodes) {
liveNodes.value = nodes;
}
if (data?.viewport) {
liveViewport.value = data.viewport as AgentStudioViewport;
if (viewport) {
liveViewport.value = viewport;
}
capture({
nodes: data?.nodes as AgentStudioNodeView[] | undefined,
viewport: data?.viewport as AgentStudioViewport | undefined,
});
const selected = data?.nodes?.find((node: any) => node.selected);
const selected = nodes?.find((node: any) => node.selected);
if (selected?.id && selected.id !== props.selectedNodeId) {
emit('select', selected.id);
}
}
function buildFlowDataSignature(data: any) {
const nodes = Array.isArray(data?.nodes)
? data.nodes.map((node: any) => ({
data: node.data,
id: node.id,
position: node.position,
selected: node.selected,
type: node.type,
}))
: [];
const edges = Array.isArray(data?.edges)
? data.edges.map((edge: any) => ({
id: edge.id,
source: edge.source,
target: edge.target,
}))
: [];
return JSON.stringify({
edges,
nodes,
viewport: data?.viewport,
function handleDataCommit(data: any) {
if (!props.active) {
return;
}
capture({
nodes: Array.isArray(data?.nodes)
? (data.nodes as AgentStudioNodeView[])
: undefined,
viewport: data?.viewport as AgentStudioViewport | undefined,
});
}
const stableFlowData = shallowRef(flowData.value);
let resizeObserver: ResizeObserver | undefined;
function syncFlowData() {
const next = flowData.value;
const signature = buildFlowDataSignature(next);
if (signature === lastFlowDataSignature.value) {
return;
function shallowEqual(left: object, right: object) {
if (left === right) {
return true;
}
lastFlowDataSignature.value = signature;
stableFlowData.value = next;
const leftRecord = left as Record<string, unknown>;
const rightRecord = right as Record<string, unknown>;
const leftKeys = Object.keys(leftRecord);
if (leftKeys.length !== Object.keys(rightRecord).length) {
return false;
}
return leftKeys.every((key) => Object.is(leftRecord[key], rightRecord[key]));
}
syncFlowData();
function canvasModelsEqual(
previous: typeof canvasModel.value,
next: typeof canvasModel.value,
comparePositions: boolean,
) {
if (
previous.nodes.length !== next.nodes.length ||
previous.edges.length !== next.edges.length
) {
return false;
}
const nodesEqual = previous.nodes.every((node, index) => {
const nextNode = next.nodes[index];
return (
node.id === nextNode?.id &&
node.type === nextNode.type &&
node.width === nextNode.width &&
node.height === nextNode.height &&
shallowEqual(node.data, nextNode.data) &&
(!comparePositions ||
(node.position.x === nextNode.position.x &&
node.position.y === nextNode.position.y))
);
});
if (!nodesEqual) {
return false;
}
return previous.edges.every((edge, index) => {
const nextEdge = next.edges[index];
return nextEdge ? shallowEqual(edge, nextEdge) : false;
});
}
watch(flowData, syncFlowData);
const stableFlowData = shallowRef(buildFlowData());
let lastCanvasModel = canvasModel.value;
let resizeObserver: ResizeObserver | undefined;
function syncFlowData(nextModel: typeof canvasModel.value) {
const comparePositions = liveNodes.value.length === 0;
if (canvasModelsEqual(lastCanvasModel, nextModel, comparePositions)) {
return;
}
lastCanvasModel = nextModel;
stableFlowData.value = buildFlowData();
}
watch(canvasModel, syncFlowData);
function updateCanvasSize() {
const rect = canvasRef.value?.getBoundingClientRect();
@@ -295,16 +334,41 @@ function updateCanvasSize() {
canvasSize.value = next;
}
onMounted(() => {
function stopCanvasObservation() {
resizeObserver?.disconnect();
resizeObserver = undefined;
}
function startCanvasObservation() {
stopCanvasObservation();
if (!props.active) {
return;
}
updateCanvasSize();
if (typeof ResizeObserver === 'undefined' || !canvasRef.value) return;
resizeObserver = new ResizeObserver(updateCanvasSize);
resizeObserver.observe(canvasRef.value);
}
onMounted(() => {
startCanvasObservation();
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
stopCanvasObservation();
});
watch(
() => props.active,
async (active) => {
if (!active) {
stopCanvasObservation();
return;
}
await nextTick();
startCanvasObservation();
},
);
</script>
<template>
@@ -329,8 +393,9 @@ onBeforeUnmount(() => {
:elements-selectable="true"
:drop-enabled="false"
:on-data-change="handleDataChange"
:on-data-commit="handleDataCommit"
/>
<AgentStudioEdgeLayer :connections="connections" />
<AgentStudioEdgeLayer :connections="connections" :viewport="liveViewport" />
</div>
</template>