Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/composables/agent-studio/useAgentStudioModel.ts
陈子默 72df00f25b feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
2026-05-25 11:42:48 +08:00

260 lines
7.6 KiB
TypeScript

import type {
AgentStudioCanvasSize,
AgentStudioEdgeView,
AgentStudioLayoutSnapshot,
AgentStudioNodeData,
AgentStudioNodeView,
} from '../../components/agent-studio/types';
import type {
AgentDraftState,
AgentKnowledgeBinding,
AgentOption,
AgentToolBinding,
} from '../../types';
import {computed} from 'vue';
const BASE_NODE_ID = 'agent-base';
const BASE_POSITION = { x: 430, y: 260 };
const CAPABILITY_GAP = 104;
const CAPABILITY_OFFSET_X = 360;
const CAPABILITY_START_Y = 148;
const CAPABILITY_NODE_HEIGHT = 78;
const CAPABILITY_NODE_WIDTH = 212;
const LEFT_SCREEN_PADDING = 48;
const TOP_SCREEN_PADDING = 96;
const INSPECTOR_RESERVED_WIDTH = 468;
const MIN_VISIBLE_WORKSPACE_WIDTH = 520;
function firstText(...values: unknown[]) {
const matched = values.find((value) => String(value || '').trim());
return matched ? String(matched).trim() : '';
}
function buildKnowledgeTitle(
binding: AgentKnowledgeBinding,
options: AgentOption[],
) {
const matchedOption = options.find(
(item) => String(item.value) === String(binding.knowledgeId),
);
return firstText(
binding.resourceSummary?.title,
binding.resourceSummary?.name,
binding.resourceSummary?.label,
binding.resourceSummary?.displayName,
binding.resourceSnapshot?.title,
binding.resourceSnapshot?.name,
binding.resourceSnapshot?.label,
binding.resourceSnapshot?.displayName,
binding.title,
binding.name,
binding.knowledgeName,
binding.collectionName,
matchedOption?.label,
matchedOption?.raw?.title,
matchedOption?.raw?.name,
);
}
function buildToolTitle(binding: AgentToolBinding) {
return firstText(
binding.resourceSummary?.title,
binding.resourceSummary?.name,
binding.resourceSnapshot?.title,
binding.resourceSnapshot?.name,
binding.toolName,
);
}
function buildToolDetail(binding: AgentToolBinding, fallback: string) {
const toolName = firstText(binding.toolName);
const resourceName = buildToolTitle(binding);
if (toolName && resourceName && toolName !== resourceName) {
return `${resourceName} / ${toolName}`;
}
return resourceName || toolName || fallback;
}
function toFlowPoint(
point: { x: number; y: number },
viewport: NonNullable<AgentStudioLayoutSnapshot['viewport']>,
) {
return {
x: (point.x - viewport.x) / viewport.zoom,
y: (point.y - viewport.y) / viewport.zoom,
};
}
function resolveVisibleLeftX(
layout?: AgentStudioLayoutSnapshot,
canvasSize?: AgentStudioCanvasSize,
) {
const viewport = layout?.viewport;
if (!viewport || !canvasSize?.width) {
return BASE_POSITION.x - CAPABILITY_OFFSET_X;
}
const availableWidth = Math.max(
MIN_VISIBLE_WORKSPACE_WIDTH,
canvasSize.width - INSPECTOR_RESERVED_WIDTH,
);
const screenX = Math.min(
LEFT_SCREEN_PADDING,
Math.max(24, availableWidth - CAPABILITY_NODE_WIDTH - LEFT_SCREEN_PADDING),
);
return toFlowPoint({ x: screenX, y: 0 }, viewport).x;
}
function resolveVisibleTopY(
layout?: AgentStudioLayoutSnapshot,
canvasSize?: AgentStudioCanvasSize,
) {
const viewport = layout?.viewport;
if (!viewport || !canvasSize?.height) {
return CAPABILITY_START_Y;
}
return toFlowPoint({ x: 0, y: TOP_SCREEN_PADDING }, viewport).y;
}
function hasVerticalOverlap(
position: { x: number; y: number },
occupied: Array<{ x: number; y: number }>,
) {
return occupied.some(
(item) =>
Math.abs(item.x - position.x) < CAPABILITY_NODE_WIDTH &&
Math.abs(item.y - position.y) < CAPABILITY_NODE_HEIGHT,
);
}
export function resolveCapabilityNodePosition(params: {
canvasSize?: AgentStudioCanvasSize;
fallbackIndex: number;
layout?: AgentStudioLayoutSnapshot;
nodeId: string;
occupiedPositions?: Array<{ x: number; y: number }>;
}) {
const persisted = params.layout?.nodePositions?.[params.nodeId];
if (persisted) return persisted;
const occupied = params.occupiedPositions || [];
const x = resolveVisibleLeftX(params.layout, params.canvasSize);
let y =
resolveVisibleTopY(params.layout, params.canvasSize) +
params.fallbackIndex * CAPABILITY_GAP;
while (hasVerticalOverlap({ x, y }, occupied)) {
y += CAPABILITY_GAP;
}
return { x, y };
}
export function useAgentStudioModel(
state: AgentDraftState,
selectedNodeId: () => string,
layout?: AgentStudioLayoutSnapshot,
canvasSize?: () => AgentStudioCanvasSize | undefined,
knowledgeOptions?: () => AgentOption[],
) {
return computed(() => {
const positionOf = (nodeId: string, fallback: { x: number; y: number }) =>
layout?.nodePositions?.[nodeId] || fallback;
const size = canvasSize?.();
const occupiedPositions: Array<{ x: number; y: number }> = Object.values(
layout?.nodePositions || {},
);
const nodes: AgentStudioNodeView[] = [
{
id: BASE_NODE_ID,
type: 'agentStudioBase',
position: positionOf(BASE_NODE_ID, BASE_POSITION),
width: 268,
height: 104,
data: {
badge: '基座',
detail: firstText(state.agent.description, '等待配置核心提示词'),
iconKey: 'base',
id: BASE_NODE_ID,
kind: 'base',
selected: selectedNodeId() === BASE_NODE_ID,
title: firstText(state.agent.name, '未命名智能体'),
} satisfies AgentStudioNodeData,
},
];
const knowledgeNodes = state.knowledgeBindings.map((binding, index) => {
const nodeId = `knowledge:${binding.localId}`;
const title = buildKnowledgeTitle(binding, knowledgeOptions?.() || []);
const position = resolveCapabilityNodePosition({
canvasSize: size,
fallbackIndex: index,
layout,
nodeId,
occupiedPositions,
});
occupiedPositions.push(position);
return {
id: nodeId,
type: 'agentStudioCapability',
position,
width: CAPABILITY_NODE_WIDTH,
height: CAPABILITY_NODE_HEIGHT,
data: {
badge: '知识库',
detail: title || '待选择知识库',
iconKey: 'knowledge',
id: nodeId,
kind: 'knowledge',
selected: selectedNodeId() === nodeId,
title: title || '知识库',
} satisfies AgentStudioNodeData,
};
});
const toolNodes = state.toolBindings.map((binding, index) => {
const nodeId = `tool:${binding.localId}`;
const isWorkflow =
String(binding.toolType || '').toUpperCase() === 'WORKFLOW';
const fallback = isWorkflow ? '待选择工作流' : '待选择插件工具';
const detail = buildToolDetail(binding, fallback);
const position = resolveCapabilityNodePosition({
canvasSize: size,
fallbackIndex: state.knowledgeBindings.length + index,
layout,
nodeId,
occupiedPositions,
});
occupiedPositions.push(position);
return {
id: nodeId,
type: 'agentStudioCapability',
position,
width: CAPABILITY_NODE_WIDTH,
height: CAPABILITY_NODE_HEIGHT,
data: {
badge: isWorkflow ? '工作流' : '插件',
detail,
iconKey: isWorkflow ? 'workflow' : 'plugin',
id: nodeId,
kind: isWorkflow ? 'workflow' : 'plugin',
selected: selectedNodeId() === nodeId,
title:
detail === fallback ? (isWorkflow ? '工作流' : '插件') : detail,
} satisfies AgentStudioNodeData,
};
});
const capabilityNodes = [...knowledgeNodes, ...toolNodes];
const edges: AgentStudioEdgeView[] = capabilityNodes.map((node) => ({
id: `edge:${node.id}`,
source: BASE_NODE_ID,
target: node.id,
}));
nodes.push(...capabilityNodes);
return { edges, nodes };
});
}