260 lines
7.6 KiB
TypeScript
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 };
|
|
});
|
|
}
|