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, ) { 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 }; }); }