feat: 全新智能体功能

- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
This commit is contained in:
2026-05-25 11:42:48 +08:00
parent 6c3d98eaac
commit 72df00f25b
168 changed files with 22045 additions and 400 deletions

View File

@@ -0,0 +1,380 @@
<script setup lang="ts">
import type {AgentDraftState, AgentOption} from '../../types';
import type {
AgentStudioCanvasSize,
AgentStudioConnectionView,
AgentStudioNodeData,
AgentStudioNodeView,
AgentStudioViewport,
} from './types';
import {
computed,
createApp,
h,
onBeforeUnmount,
onMounted,
reactive,
ref,
shallowRef,
watch,
} from 'vue';
import {usePreferences} from '@easyflow/preferences';
import {Tinyflow} from '@tinyflow-ai/vue';
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[];
selectedNodeId: string;
state: AgentDraftState;
}>();
const emit = defineEmits<{
select: [nodeId: string];
}>();
const { isDark } = usePreferences();
const { capture, layout } = useAgentStudioLayout(props.state);
const canvasRef = ref<HTMLElement>();
const canvasSize = ref<AgentStudioCanvasSize>();
const canvasModel = useAgentStudioModel(
props.state,
() => props.selectedNodeId,
layout,
() => canvasSize.value,
() => props.knowledgeOptions || [],
);
const liveNodes = ref<AgentStudioNodeView[]>([]);
const liveViewport = ref<AgentStudioViewport>({ x: 250, y: 100, zoom: 1 });
const flowData = computed(() => {
const liveNodeMap = new Map(liveNodes.value.map((node) => [node.id, node]));
return {
edges: canvasModel.value.edges.map((edge) => ({ ...edge })),
nodes: canvasModel.value.nodes.map((node) => {
const liveNode = liveNodeMap.get(node.id);
return {
...node,
data: { ...node.data },
position: { ...(liveNode?.position || node.position) },
};
}),
viewport: layout.viewport ? { ...layout.viewport } : undefined,
};
});
const lastCaptureSignature = ref('');
const lastFlowDataSignature = ref('');
const customNodes = computed(() => ({
agentStudioBase: {
presentation: 'plain',
render: (parent: HTMLElement, node: any) => {
renderVueNode(parent, AgentStudioBaseNode, node.data);
},
onUpdate: (parent: HTMLElement, node: any) => {
renderVueNode(parent, AgentStudioBaseNode, node.data);
},
},
agentStudioCapability: {
presentation: 'plain',
render: (parent: HTMLElement, node: any) => {
renderVueNode(parent, AgentStudioCapabilityNode, node.data);
},
onUpdate: (parent: HTMLElement, node: any) => {
renderVueNode(parent, AgentStudioCapabilityNode, node.data);
},
},
}));
const connections = computed<AgentStudioConnectionView[]>(() => {
const nodes =
liveNodes.value.length > 0
? liveNodes.value
: (canvasModel.value.nodes as AgentStudioNodeView[]);
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
return canvasModel.value.edges
.map((edge) => {
const source = nodeMap.get(edge.source);
const target = nodeMap.get(edge.target);
if (!source || !target) return undefined;
return {
active: false,
id: edge.id,
path: buildConnectionPath(source, target, liveViewport.value),
sourceId: edge.source,
targetId: edge.target,
};
})
.filter(Boolean) as AgentStudioConnectionView[];
});
function renderVueNode(
parent: HTMLElement,
component: any,
data: AgentStudioNodeData,
) {
const current = (parent as any).__agentStudioNodeView as
| undefined
| {
app: ReturnType<typeof createApp>;
component: any;
data: AgentStudioNodeData;
signature: string;
};
const signature = JSON.stringify(data);
if (current && current.component === component) {
if (current.signature === signature) {
return;
}
Object.assign(current.data, data);
current.signature = signature;
return;
}
current?.app.unmount();
parent.textContent = '';
const reactiveData = reactive({ ...data }) as AgentStudioNodeData;
const app = createApp({
render: () => h(component, { data: reactiveData }),
});
app.mount(parent);
(parent as any).__agentStudioNodeView = {
app,
component,
data: reactiveData,
signature,
};
}
function getNodeSize(node: AgentStudioNodeView) {
return {
height: node.height || (node.data.kind === 'base' ? 104 : 78),
width: node.width || (node.data.kind === 'base' ? 268 : 212),
};
}
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);
const sourceCenter = {
x: source.position.x + sourceSize.width / 2,
y: source.position.y + sourceSize.height / 2,
};
const targetCenter = {
x: target.position.x + targetSize.width / 2,
y: target.position.y + targetSize.height / 2,
};
const sourceOnLeft = targetCenter.x < sourceCenter.x;
const start = {
x: source.position.x + (sourceOnLeft ? 0 : sourceSize.width),
y: sourceCenter.y,
};
const end = {
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;
return [
`M ${screenStart.x} ${screenStart.y}`,
`L ${midX} ${screenStart.y}`,
`L ${midX} ${screenEnd.y}`,
`L ${screenEnd.x} ${screenEnd.y}`,
].join(' ');
}
function handleDataChange(data: any) {
const signature = buildFlowDataSignature(data);
if (signature === lastCaptureSignature.value) {
return;
}
lastCaptureSignature.value = signature;
if (Array.isArray(data?.nodes)) {
liveNodes.value = data.nodes as AgentStudioNodeView[];
}
if (data?.viewport) {
liveViewport.value = data.viewport as AgentStudioViewport;
}
capture({
nodes: data?.nodes as AgentStudioNodeView[] | undefined,
viewport: data?.viewport as AgentStudioViewport | undefined,
});
const selected = data?.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,
});
}
const stableFlowData = shallowRef(flowData.value);
let resizeObserver: ResizeObserver | undefined;
function syncFlowData() {
const next = flowData.value;
const signature = buildFlowDataSignature(next);
if (signature === lastFlowDataSignature.value) {
return;
}
lastFlowDataSignature.value = signature;
stableFlowData.value = next;
}
syncFlowData();
watch(flowData, syncFlowData);
function updateCanvasSize() {
const rect = canvasRef.value?.getBoundingClientRect();
if (!rect) return;
const next = {
height: rect.height,
width: rect.width,
};
if (
canvasSize.value?.height === next.height &&
canvasSize.value.width === next.width
) {
return;
}
canvasSize.value = next;
}
onMounted(() => {
updateCanvasSize();
if (typeof ResizeObserver === 'undefined' || !canvasRef.value) return;
resizeObserver = new ResizeObserver(updateCanvasSize);
resizeObserver.observe(canvasRef.value);
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
});
</script>
<template>
<div ref="canvasRef" class="agent-studio-canvas">
<Tinyflow
class="agent-studio-canvas__flow"
:data="stableFlowData"
:theme="isDark ? 'dark' : 'light'"
:custom-nodes="customNodes"
:hide-bottom-dock="true"
:hide-edge-panel="true"
:hide-mini-map="true"
:hide-node-handles="true"
:hide-node-picker="true"
:hide-node-setting="true"
:hide-node-toolbar="true"
:hide-edge-markers="true"
:edge-animated="false"
:edge-interaction-width="0"
:nodes-draggable="true"
:nodes-connectable="false"
:elements-selectable="true"
:drop-enabled="false"
:on-data-change="handleDataChange"
/>
<AgentStudioEdgeLayer :connections="connections" />
</div>
</template>
<style scoped>
.agent-studio-canvas {
position: absolute;
inset: 0;
overflow: hidden;
}
.agent-studio-canvas__flow {
width: 100%;
height: 100%;
}
.agent-studio-canvas :deep(.tinyflow),
.agent-studio-canvas :deep(.agentsflow) {
width: 100%;
height: 100% !important;
}
.agent-studio-canvas :deep(.svelte-flow__node) {
z-index: 2 !important;
border: 0;
border-radius: 18px;
}
.agent-studio-canvas :deep(.svelte-flow__node::after) {
display: none;
}
.agent-studio-canvas :deep(.svelte-flow__node:hover),
.agent-studio-canvas :deep(.svelte-flow__node.selectable.selected) {
border: 0;
box-shadow: none;
}
.agent-studio-canvas :deep(.svelte-flow__handle),
.agent-studio-canvas :deep(.svelte-flow__minimap),
.agent-studio-canvas :deep(.tf-left-dock),
.agent-studio-canvas :deep(.tf-top-dock),
.agent-studio-canvas :deep(.tf-bottom-dock),
.agent-studio-canvas :deep(.tf-node-toolbar) {
display: none !important;
}
.agent-studio-canvas :deep(.tf-flow-line-path) {
stroke: color-mix(
in srgb,
var(--el-color-primary) 30%,
var(--el-border-color)
);
stroke-width: 1.5;
}
</style>