support tomology detection

This commit is contained in:
锦恢 2025-07-02 21:37:33 +08:00
parent a735bbf023
commit 4a210eaa82
4 changed files with 214 additions and 37 deletions

View File

@ -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);

View 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;
// }
// };

View File

@ -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;
// xx
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

View File

@ -233,6 +233,7 @@ const generateAIMockData = async (prompt?: string) => {
aiMockLoading.value = false;
}
};
const generateMockData = async () => {
if (!currentTool.value?.inputSchema) return;
mockLoading.value = true;