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 createKnowledgeOptionLookup(options: AgentOption[]) { const lookup = new Map(); options.forEach((option) => { const key = String(option.value); if (!lookup.has(key)) { lookup.set(key, option); } }); return lookup; } function createToolOptionLookup(options: AgentOption[]) { const lookup = new Map(); options.forEach((option) => { const raw = option.raw || {}; [option.value, raw.id, raw.mcpId].forEach((value) => { const key = String(value || ''); if (key && !lookup.has(key)) { lookup.set(key, option); } }); }); return lookup; } function buildKnowledgeTitle( binding: AgentKnowledgeBinding, optionLookup: Map, ) { const matchedOption = optionLookup.get(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 findMatchedToolOption( binding: AgentToolBinding, optionLookup: Map, ) { const targetId = String(binding.targetId || ''); if (!targetId) return undefined; return optionLookup.get(targetId); } function buildToolTitle( binding: AgentToolBinding, optionLookup: Map, matchedOption = findMatchedToolOption(binding, optionLookup), ) { return firstText( binding.resourceSummary?.title, binding.resourceSummary?.name, binding.resourceSummary?.label, binding.resourceSummary?.displayName, binding.resourceSummary?.mcpTitle, binding.resourceSnapshot?.title, binding.resourceSnapshot?.name, binding.resourceSnapshot?.label, binding.resourceSnapshot?.displayName, binding.resourceSnapshot?.mcpTitle, matchedOption?.label, matchedOption?.raw?.title, matchedOption?.raw?.name, matchedOption?.raw?.label, matchedOption?.raw?.displayName, matchedOption?.raw?.mcpTitle, binding.toolName, ); } function buildToolDetail( binding: AgentToolBinding, fallback: string, optionLookup: Map, ) { if (String(binding.toolType || '').toUpperCase() === 'MCP') { const matchedOption = findMatchedToolOption(binding, optionLookup); const resourceName = buildToolTitle(binding, optionLookup, matchedOption); const tools = binding.resourceSummary?.tools || binding.resourceSnapshot?.tools || matchedOption?.raw?.tools || []; const toolCount = Array.isArray(tools) ? tools.length : 0; if (resourceName && toolCount > 0) { return `${resourceName} · ${toolCount} 个工具`; } return resourceName || fallback; } const toolName = firstText(binding.toolName); const resourceName = buildToolTitle(binding, optionLookup); if (toolName && resourceName && toolName !== resourceName) { return `${resourceName} / ${toolName}`; } return resourceName || toolName || fallback; } function resolveToolNodePresentation(toolType: string) { if (toolType === 'WORKFLOW') { return { fallback: '待选择工作流', kind: 'workflow' as const, title: '工作流', }; } if (toolType === 'MCP') { return { fallback: '待选择 MCP', kind: 'mcp' as const, title: 'MCP', }; } return { fallback: '待选择插件工具', kind: 'plugin' as const, title: '插件', }; } 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[], toolOptions?: () => { mcp?: AgentOption[]; plugin?: AgentOption[]; workflow?: AgentOption[]; }, ) { return computed(() => { const positionOf = (nodeId: string, fallback: { x: number; y: number }) => layout?.nodePositions?.[nodeId] || fallback; const size = canvasSize?.(); const resolvedKnowledgeOptions = knowledgeOptions?.() || []; const resolvedToolOptions = toolOptions?.() || {}; const knowledgeOptionLookup = createKnowledgeOptionLookup( resolvedKnowledgeOptions, ); const toolOptionLookups = { mcp: createToolOptionLookup(resolvedToolOptions.mcp || []), plugin: createToolOptionLookup(resolvedToolOptions.plugin || []), workflow: createToolOptionLookup(resolvedToolOptions.workflow || []), }; 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, knowledgeOptionLookup); 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 toolType = String(binding.toolType || '').toUpperCase(); const presentation = resolveToolNodePresentation(toolType); const matchedOptions = toolOptionLookups[presentation.kind]; const detail = buildToolDetail( binding, presentation.fallback, matchedOptions, ); 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: presentation.title, detail, iconKey: presentation.kind, id: nodeId, kind: presentation.kind, selected: selectedNodeId() === nodeId, title: detail === presentation.fallback ? presentation.title : detail, } satisfies AgentStudioNodeData, }; }); const skillNodes: AgentStudioNodeView[] = []; if (state.skillBindings.length > 0) { const nodeId = 'skills'; const previewItems = state.skillBindings .slice(0, 3) .map((binding) => firstText(binding.resourceSummary?.displayName, '技能'), ); const position = resolveCapabilityNodePosition({ canvasSize: size, fallbackIndex: state.knowledgeBindings.length + state.toolBindings.length, layout, nodeId, occupiedPositions, }); occupiedPositions.push(position); skillNodes.push({ id: nodeId, type: 'agentStudioCapability', position, width: 240, height: 154, data: { detail: `${state.skillBindings.length} / 20`, iconKey: 'skill', id: nodeId, kind: 'skill', previewItems, remainingCount: Math.max(0, state.skillBindings.length - 3), selected: selectedNodeId() === nodeId, title: '技能', } satisfies AgentStudioNodeData, }); } const capabilityNodes = [...knowledgeNodes, ...toolNodes, ...skillNodes]; const edges: AgentStudioEdgeView[] = capabilityNodes.map((node) => ({ id: `edge:${node.id}`, source: BASE_NODE_ID, target: node.id, })); nodes.push(...capabilityNodes); return { edges, nodes }; }); }