perf: 优化智能体与工作流幕布渲染性能
- 分阶段加载智能体配置并按需缓存 MCP 工具 - 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线 - 隔离 Tinyflow Store 实例并补充数据同步与回归测试
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user