support tomology detection
This commit is contained in:
parent
a735bbf023
commit
4a210eaa82
@ -12,8 +12,8 @@
|
||||
<Diagram />
|
||||
</el-scrollbar>
|
||||
<transition name="main-fade" mode="out-in">
|
||||
<div class="caption" v-show="context.caption.value">
|
||||
{{ context.caption }}
|
||||
<div class="caption" v-show="showCaption">
|
||||
{{ caption }}
|
||||
</div>
|
||||
</transition>
|
||||
</el-dialog>
|
||||
@ -23,13 +23,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue';
|
||||
import { nextTick, provide, ref } from 'vue';
|
||||
import Diagram from './diagram.vue';
|
||||
const showDiagram = ref(true);
|
||||
|
||||
const caption = ref('');
|
||||
const showCaption = ref(false);
|
||||
|
||||
const context = {
|
||||
reset: () => {},
|
||||
caption: ref('')
|
||||
setCaption: (text: string) => {
|
||||
caption.value = text;
|
||||
if (caption.value) {
|
||||
nextTick(() => {
|
||||
showCaption.value = true;
|
||||
});
|
||||
} else {
|
||||
nextTick(() => {
|
||||
showCaption.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
provide('context', context);
|
||||
|
141
renderer/src/components/main-panel/tool/diagram.ts
Normal file
141
renderer/src/components/main-panel/tool/diagram.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import type { ElkNode } from 'elkjs/lib/elk-api';
|
||||
|
||||
export interface Edge {
|
||||
id: string;
|
||||
sources: string[];
|
||||
targets: string[];
|
||||
section?: any; // { startPoint: { x, y }, endPoint: { x,
|
||||
}
|
||||
|
||||
export type Node = ElkNode & {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export interface DiagramState {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
selectedNodeId: string | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface CanConnectResult {
|
||||
canConnect: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断两个节点是否可以连接
|
||||
*/
|
||||
export function invalidConnectionDetector(state: DiagramState, d: Node): CanConnectResult {
|
||||
const from = state.selectedNodeId;
|
||||
const to = d.id;
|
||||
|
||||
if (!from) {
|
||||
return { canConnect: false, reason: '未选择起始节点' };
|
||||
}
|
||||
|
||||
if (from === to) {
|
||||
return { canConnect: false, reason: '不能连接到自身' };
|
||||
}
|
||||
|
||||
// 建立邻接表
|
||||
const adjacencyList: Record<string, Set<string>> = {};
|
||||
state.edges.forEach(edge => {
|
||||
const src = edge.sources[0];
|
||||
const tgt = edge.targets[0];
|
||||
if (!adjacencyList[src]) {
|
||||
adjacencyList[src] = new Set();
|
||||
}
|
||||
adjacencyList[src].add(tgt);
|
||||
});
|
||||
|
||||
// DFS 检测是否存在
|
||||
function hasPath(current: string, target: string, visited: Set<string>): boolean {
|
||||
if (current === target) return true;
|
||||
visited.add(current);
|
||||
const neighbors = adjacencyList[current] || new Set();
|
||||
for (const neighbor of neighbors) {
|
||||
if (!visited.has(neighbor)) {
|
||||
if (hasPath(neighbor, target, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasPath(to, from, new Set())) {
|
||||
return { canConnect: false, reason: '连接会形成环路' };
|
||||
}
|
||||
|
||||
if (hasPath(from, to, new Set())) {
|
||||
return { canConnect: false, reason: '这是一个重复的连接' };
|
||||
}
|
||||
|
||||
return {
|
||||
canConnect: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export async function generateAIMockData(params: any) {
|
||||
// if (!currentTool.value?.inputSchema) return;
|
||||
// aiMockLoading.value = true;
|
||||
// try {
|
||||
// const loop = new TaskLoop({ maxEpochs: 1 });
|
||||
// const usePrompt = prompt || `please call the tool ${currentTool.value.name} to make some test`;
|
||||
// const chatStorage = {
|
||||
// messages: [],
|
||||
// settings: {
|
||||
// temperature: 0.6,
|
||||
// systemPrompt: '',
|
||||
// enableTools: [{
|
||||
// name: currentTool.value.name,
|
||||
// description: currentTool.value.description,
|
||||
// inputSchema: currentTool.value.inputSchema,
|
||||
// enabled: true
|
||||
// }],
|
||||
// enableWebSearch: false,
|
||||
// contextLength: 5,
|
||||
// enableXmlWrapper: enableXmlWrapper.value,
|
||||
// parallelToolCalls: false
|
||||
// }
|
||||
// } as ChatStorage;
|
||||
|
||||
// loop.setMaxEpochs(1);
|
||||
|
||||
// let aiMockJson: any = undefined;
|
||||
|
||||
// loop.registerOnToolCall(toolCall => {
|
||||
// console.log(toolCall);
|
||||
|
||||
// if (toolCall.function?.name === currentTool.value?.name) {
|
||||
// try {
|
||||
// const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
|
||||
// aiMockJson = toolArgs;
|
||||
// } catch (e) {
|
||||
// ElMessage.error('AI 生成的 JSON 解析错误');
|
||||
// }
|
||||
// } else {
|
||||
// ElMessage.error('AI 调用了未知的工具');
|
||||
// }
|
||||
// loop.abort();
|
||||
// return toolCall;
|
||||
// });
|
||||
|
||||
// loop.registerOnError(error => {
|
||||
// ElMessage.error(error + '');
|
||||
// });
|
||||
|
||||
// await loop.start(chatStorage, usePrompt);
|
||||
|
||||
// if (aiMockJson && typeof aiMockJson === 'object') {
|
||||
// Object.keys(aiMockJson).forEach(key => {
|
||||
// tabStorage.formData[key] = aiMockJson[key];
|
||||
// });
|
||||
// formRef.value?.clearValidate?.();
|
||||
// }
|
||||
// } finally {
|
||||
// aiMockLoading.value = false;
|
||||
// }
|
||||
// };
|
@ -7,17 +7,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, reactive, inject } from 'vue';
|
||||
import * as d3 from 'd3';
|
||||
import ELK, { type ElkNode } from 'elkjs/lib/elk.bundled.js';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
import { invalidConnectionDetector, type Edge, type Node } from './diagram';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const svgContainer = ref<HTMLDivElement | null>(null);
|
||||
let prevNodes: any[] = [];
|
||||
let prevEdges: any[] = [];
|
||||
|
||||
type Node = ElkNode & {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
nodes: [] as any[],
|
||||
edges: [] as any[],
|
||||
@ -63,16 +61,11 @@ const recomputeLayout = async () => {
|
||||
|
||||
const drawDiagram = async () => {
|
||||
const tools = await getAllTools();
|
||||
|
||||
state.nodes = tools.map((tool, i) => ({
|
||||
id: tool.name,
|
||||
width: 160,
|
||||
height: 48,
|
||||
labels: [{ text: tool.name || 'Tool' }]
|
||||
}));
|
||||
|
||||
// 默认按照链表进行串联
|
||||
const edges = [];
|
||||
const nodes = [] as Node[];
|
||||
const edges = [] as Edge[];
|
||||
|
||||
for (let i = 0; i < tools.length - 1; ++ i) {
|
||||
const prev = tools[i];
|
||||
const next = tools[i + 1];
|
||||
@ -83,7 +76,17 @@ const drawDiagram = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
for (const tool of tools) {
|
||||
nodes.push({
|
||||
id: tool.name,
|
||||
width: 160,
|
||||
height: 48,
|
||||
labels: [{ text: tool.name || 'Tool' }]
|
||||
});
|
||||
}
|
||||
|
||||
state.edges = edges;
|
||||
state.nodes = nodes;
|
||||
|
||||
// 重新计算布局
|
||||
await recomputeLayout();
|
||||
@ -96,7 +99,14 @@ function renderSvg() {
|
||||
const prevNodeMap = new Map(prevNodes.map(n => [n.id, n]));
|
||||
const prevEdgeMap = new Map(prevEdges.map(e => [e.id, e]));
|
||||
|
||||
const width = Math.max(...state.nodes.map(n => (n.x || 0) + (n.width || 160)), 400) + 60;
|
||||
// 计算所有节点的最小x和最大x
|
||||
const xs = state.nodes.map(n => (n.x || 0));
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs.map((x, i) => x + (state.nodes[i].width || 160)));
|
||||
const contentWidth = maxX - minX;
|
||||
const svgWidth = Math.max(contentWidth + 120, 400); // 120为两侧留白
|
||||
const offsetX = (svgWidth - contentWidth) / 2 - minX;
|
||||
|
||||
const height = Math.max(...state.nodes.map(n => (n.y || 0) + (n.height || 48)), 300) + 60;
|
||||
|
||||
// 不再全量清空,只清空 svg 元素
|
||||
@ -105,11 +115,11 @@ function renderSvg() {
|
||||
svg = d3
|
||||
.select(svgContainer.value)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('width', svgWidth)
|
||||
.attr('height', height)
|
||||
.style('user-select', 'none') as any;
|
||||
} else {
|
||||
svg.attr('width', width).attr('height', height);
|
||||
svg.attr('width', svgWidth).attr('height', height);
|
||||
svg.selectAll('defs').remove();
|
||||
}
|
||||
|
||||
@ -128,6 +138,16 @@ function renderSvg() {
|
||||
.attr('d', 'M 0 0 L 8 4 L 0 8 z')
|
||||
.attr('fill', 'var(--main-color)');
|
||||
|
||||
// 1. 创建/获取 main group
|
||||
let mainGroup = svg.select('g.main-group');
|
||||
if (mainGroup.empty()) {
|
||||
mainGroup = svg.append('g').attr('class', 'main-group') as any;
|
||||
}
|
||||
mainGroup
|
||||
.transition()
|
||||
.duration(600)
|
||||
.attr('transform', `translate(${offsetX}, 0)`);
|
||||
|
||||
// Draw edges with enter animation
|
||||
const allSections: { id: string, section: any }[] = [];
|
||||
(state.edges || []).forEach(edge => {
|
||||
@ -140,7 +160,7 @@ function renderSvg() {
|
||||
});
|
||||
});
|
||||
|
||||
const edgeSelection = svg.selectAll<SVGLineElement, any>('.edge')
|
||||
const edgeSelection = mainGroup.selectAll<SVGLineElement, any>('.edge')
|
||||
.data(allSections, d => d.id);
|
||||
|
||||
edgeSelection.exit().remove();
|
||||
@ -198,7 +218,7 @@ function renderSvg() {
|
||||
.attr('opacity', 1);
|
||||
|
||||
// --- 节点动画部分 ---
|
||||
const nodeGroup = svg.selectAll<SVGGElement, any>('.node')
|
||||
const nodeGroup = mainGroup.selectAll<SVGGElement, any>('.node')
|
||||
.data(state.nodes, d => d.id);
|
||||
|
||||
nodeGroup.exit().remove();
|
||||
@ -219,16 +239,18 @@ function renderSvg() {
|
||||
.on('mousedown', null)
|
||||
.on('mouseup', function (event, d) {
|
||||
event.stopPropagation();
|
||||
if (state.selectedNodeId && state.selectedNodeId !== d.id) {
|
||||
// 检查是否已存在这条连线
|
||||
const exists = state.edges.some(
|
||||
e =>
|
||||
Array.isArray(e.sources) &&
|
||||
Array.isArray(e.targets) &&
|
||||
e.sources[0] === state.selectedNodeId &&
|
||||
e.targets[0] === d.id
|
||||
);
|
||||
if (!exists) {
|
||||
if (state.selectedNodeId) {
|
||||
|
||||
const { canConnect, reason } = invalidConnectionDetector(state, d);
|
||||
|
||||
console.log(reason);
|
||||
|
||||
|
||||
if (reason) {
|
||||
ElMessage.warning(reason);
|
||||
}
|
||||
|
||||
if (canConnect) {
|
||||
state.edges.push({
|
||||
id: `e${state.selectedNodeId}_${d.id}_${Date.now()}`,
|
||||
sources: [state.selectedNodeId],
|
||||
@ -241,12 +263,12 @@ function renderSvg() {
|
||||
state.selectedNodeId = null;
|
||||
renderSvg();
|
||||
}
|
||||
context.caption.value = '';
|
||||
context.setCaption('');
|
||||
|
||||
} else {
|
||||
state.selectedNodeId = d.id;
|
||||
renderSvg();
|
||||
context.caption.value = '选择另一个节点以构建顺序';
|
||||
context.setCaption('选择另一个节点以定义测试拓扑');
|
||||
}
|
||||
state.draggingNodeId = null;
|
||||
})
|
||||
@ -315,7 +337,7 @@ function renderSvg() {
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 4.5);
|
||||
|
||||
context.caption.value = '点击边以删除';
|
||||
context.setCaption('点击边以删除');
|
||||
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
@ -325,8 +347,7 @@ function renderSvg() {
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 2.5);
|
||||
|
||||
context.caption.value = '';
|
||||
|
||||
context.setCaption('');
|
||||
})
|
||||
.on('click', function (event, d) {
|
||||
// 只删除当前 edge
|
||||
|
@ -233,6 +233,7 @@ const generateAIMockData = async (prompt?: string) => {
|
||||
aiMockLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const generateMockData = async () => {
|
||||
if (!currentTool.value?.inputSchema) return;
|
||||
mockLoading.value = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user