finish auto detection

This commit is contained in:
锦恢 2025-07-03 03:32:40 +08:00
parent 92c8cf90ed
commit 9294275874
4 changed files with 2485 additions and 82 deletions

View File

@ -5,7 +5,27 @@
<span>Tool Diagram</span> <span>Tool Diagram</span>
&ensp; &ensp;
<el-button size="small" type="primary" @click="() => context.reset()">重置</el-button> <el-button size="small" type="primary" @click="() => context.reset()">重置</el-button>
<el-button size="small" type="primary" @click="() => startTest()">开启自检程序</el-button> <!-- 自检程序弹出表单 -->
<el-popover placement="top" width="350" trigger="click" v-model:visible="testFormVisible">
<template #reference>
<el-button size="small" type="primary">
开启自检程序
</el-button>
</template>
<el-input type="textarea" v-model="testPrompt" :rows="2" style="margin-bottom: 8px;"
placeholder="请输入 prompt" />
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" />
<span style="opacity: 0.7;">enableXmlWrapper</span>
</div>
<div style="text-align: right;">
<el-button size="small" @click="testFormVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="onTestConfirm">
确认
</el-button>
</div>
</el-popover>
</div> </div>
</template> </template>
<el-scrollbar height="80vh"> <el-scrollbar height="80vh">
@ -17,16 +37,14 @@
</div> </div>
</transition> </transition>
</el-dialog> </el-dialog>
<!-- <el-button @click="showDiagram = true" type="primary" style="margin-bottom: 16px;">
Show Tool Diagram
</el-button> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, provide, ref } from 'vue'; import { nextTick, provide, ref } from 'vue';
import Diagram from './diagram.vue'; import Diagram from './diagram.vue';
import { topoSortParallel, type DiagramState } from './diagram'; import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
const showDiagram = ref(true); const showDiagram = ref(true);
const caption = ref(''); const caption = ref('');
@ -45,35 +63,40 @@ function setCaption(text: string) {
} }
} }
interface DiagramContext {
reset: () => void,
state?: DiagramState,
setCaption: (value: string) => void
}
const context: DiagramContext = { const context: DiagramContext = {
reset: () => {}, reset: () => {},
render: () => {},
state: undefined, state: undefined,
setCaption setCaption
}; };
provide('context', context); provide('context', context);
async function startTest() { //
const testFormVisible = ref(false);
const enableXmlWrapper = ref(false);
const testPrompt = ref('please call the tool {tool} to make some test');
async function onTestConfirm() {
testFormVisible.value = false;
// enableXmlWrapper.value testPrompt.value
const state = context.state; const state = context.state;
if (state) { if (state) {
const dispatches = topoSortParallel(state); const dispatches = topoSortParallel(state);
// for (const layer of dispatches) { for (const nodeIds of dispatches) {
// await Promise.all( await Promise.all(
// layer.map(nodeId => state.nodes[nodeId].run()) nodeIds.map(id => {
// ); const node = state.dataView.get(id);
// } if (node) {
return makeNodeTest(node, enableXmlWrapper.value, testPrompt.value, context)
}
})
)
}
} else { } else {
ElMessage.error('error'); ElMessage.error('error');
} }
} }
</script> </script>
<style> <style>
@ -90,7 +113,7 @@ async function startTest() {
min-height: 32px; min-height: 32px;
background: rgba(245, 247, 250, 0.05); background: rgba(245, 247, 250, 0.05);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.06); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
color: var(--main-color); color: var(--main-color);
font-size: 15px; font-size: 15px;
display: flex; display: flex;

View File

@ -3,6 +3,7 @@ import { TaskLoop } from '../chat/core/task-loop';
import type { Reactive } from 'vue'; import type { Reactive } from 'vue';
import type { ChatStorage } from '../chat/chat-box/chat'; import type { ChatStorage } from '../chat/chat-box/chat';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import type { ToolItem } from '@/hook/type';
export interface Edge { export interface Edge {
id: string; id: string;
@ -19,6 +20,7 @@ export interface DiagramState {
nodes: Node[]; nodes: Node[];
edges: Edge[]; edges: Edge[];
selectedNodeId: string | null; selectedNodeId: string | null;
dataView: Map<string, NodeDataView>;
[key: string]: any; [key: string]: any;
} }
@ -27,6 +29,19 @@ export interface CanConnectResult {
reason?: string; reason?: string;
} }
export interface NodeDataView {
tool: ToolItem;
status: 'default' | 'running' | 'waiting' | 'success' | 'error';
result: any;
}
export interface DiagramContext {
reset: () => void,
render: () => void,
state?: DiagramState,
setCaption: (value: string) => void
}
/** /**
* @description * @description
*/ */
@ -133,25 +148,31 @@ export function topoSortParallel(state: DiagramState): string[][] {
return result; return result;
} }
export async function makeNodeTest(dataView: Reactive<any>, enableXmlWrapper: boolean, prompt: string | null = null) { export async function makeNodeTest(
dataView: Reactive<NodeDataView>,
enableXmlWrapper: boolean,
prompt: string | null = null,
context: DiagramContext
) {
if (!dataView.tool.inputSchema) { if (!dataView.tool.inputSchema) {
return; return;
} }
dataView.loading = true; dataView.status = 'running';
context.render();
try { try {
const loop = new TaskLoop({ maxEpochs: 1 }); const loop = new TaskLoop({ maxEpochs: 1 });
const usePrompt = prompt || `please call the tool ${dataView.too.name} to make some test`; const usePrompt = (prompt || 'please call the tool {tool} to make some test').replace('{tool}', dataView.tool.name);
const chatStorage = { const chatStorage = {
messages: [], messages: [],
settings: { settings: {
temperature: 0.6, temperature: 0.6,
systemPrompt: '', systemPrompt: '',
enableTools: [{ enableTools: [{
name: dataView.too.name, name: dataView.tool.name,
description: dataView.too.description, description: dataView.tool.description,
inputSchema: dataView.too.inputSchema, inputSchema: dataView.tool.inputSchema,
enabled: true enabled: true
}], }],
enableWebSearch: false, enableWebSearch: false,
@ -168,15 +189,21 @@ export async function makeNodeTest(dataView: Reactive<any>, enableXmlWrapper: bo
loop.registerOnToolCall(toolCall => { loop.registerOnToolCall(toolCall => {
console.log(toolCall); console.log(toolCall);
if (toolCall.function?.name === dataView.too?.name) { if (toolCall.function?.name === dataView.tool?.name) {
try { try {
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}'); const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
aiMockJson = toolArgs; aiMockJson = toolArgs;
} catch (e) { } catch (e) {
// ElMessage.error('AI 生成的 JSON 解析错误'); // ElMessage.error('AI 生成的 JSON 解析错误');
dataView.status = 'error';
dataView.result = 'AI 生成的 JSON 解析错误';
context.render();
} }
} else { } else {
// ElMessage.error('AI 调用了未知的工具'); // ElMessage.error('AI 调用了未知的工具');
dataView.status = 'error';
dataView.result = 'AI 调用了未知的工具 ' + toolCall.function?.name;
context.render();
} }
loop.abort(); loop.abort();
return toolCall; return toolCall;
@ -189,6 +216,9 @@ export async function makeNodeTest(dataView: Reactive<any>, enableXmlWrapper: bo
await loop.start(chatStorage, usePrompt); await loop.start(chatStorage, usePrompt);
} finally { } finally {
dataView.loading = false; if (dataView.status === 'running') {
dataView.status = 'success';
context.render();
}
} }
}; };

View File

@ -1,6 +1,17 @@
<template> <template>
<div style="display: flex; align-items: center; gap: 16px;"> <div style="display: flex; align-items: center; gap: 16px;">
<div ref="svgContainer" class="diagram-container"></div> <div ref="svgContainer" class="diagram-container"></div>
<!-- <template v-for="(node, index) in state.nodes" :key="node.id + '-popup'">
<div
v-if="state.hoverNodeId === node.id"
:style="getNodePopupStyle(node)"
class="node-popup"
>
<div>节点{{ node.labels?.[0]?.text || node.id }}</div>
<div>: {{ node.width }}, : {{ node.height }}</div>
</div>
</template> -->
</div> </div>
</template> </template>
@ -9,7 +20,7 @@ import { ref, onMounted, nextTick, reactive, inject } from 'vue';
import * as d3 from 'd3'; import * as d3 from 'd3';
import ELK from 'elkjs/lib/elk.bundled.js'; import ELK from 'elkjs/lib/elk.bundled.js';
import { mcpClientAdapter } from '@/views/connect/core'; import { mcpClientAdapter } from '@/views/connect/core';
import { invalidConnectionDetector, type Edge, type Node } from './diagram'; import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
const svgContainer = ref<HTMLDivElement | null>(null); const svgContainer = ref<HTMLDivElement | null>(null);
@ -21,7 +32,9 @@ const state = reactive({
edges: [] as any[], edges: [] as any[],
selectedNodeId: null as string | null, selectedNodeId: null as string | null,
draggingNodeId: null as string | null, draggingNodeId: null as string | null,
offset: { x: 0, y: 0 } hoverNodeId: null as string | null,
offset: { x: 0, y: 0 },
dataView: new Map<string, NodeDataView>
}); });
const getAllTools = async () => { const getAllTools = async () => {
@ -66,7 +79,7 @@ const drawDiagram = async () => {
const nodes = [] as Node[]; const nodes = [] as Node[];
const edges = [] as Edge[]; const edges = [] as Edge[];
for (let i = 0; i < tools.length - 1; ++ i) { for (let i = 0; i < tools.length - 1; ++i) {
const prev = tools[i]; const prev = tools[i];
const next = tools[i + 1]; const next = tools[i + 1];
edges.push({ edges.push({
@ -79,10 +92,16 @@ const drawDiagram = async () => {
for (const tool of tools) { for (const tool of tools) {
nodes.push({ nodes.push({
id: tool.name, id: tool.name,
width: 160, width: 200,
height: 48, height: 64, //
labels: [{ text: tool.name || 'Tool' }] labels: [{ text: tool.name || 'Tool' }]
}); });
state.dataView.set(tool.name, {
tool,
status: 'waiting',
result: null
});
} }
state.edges = edges; state.edges = edges;
@ -273,6 +292,7 @@ function renderSvg() {
state.draggingNodeId = null; state.draggingNodeId = null;
}) })
.on('mouseover', function (event, d) { .on('mouseover', function (event, d) {
state.hoverNodeId = d.id;
d3.select(this).select('rect') d3.select(this).select('rect')
.transition() .transition()
.duration(200) .duration(200)
@ -280,9 +300,8 @@ function renderSvg() {
.attr('stroke-width', 2); .attr('stroke-width', 2);
}) })
.on('mouseout', function (event, d) { .on('mouseout', function (event, d) {
if (state.selectedNodeId === d.id) { state.hoverNodeId = null;
return; if (state.selectedNodeId === d.id) return;
}
d3.select(this).select('rect') d3.select(this).select('rect')
.transition() .transition()
.duration(200) .duration(200)
@ -298,15 +317,91 @@ function renderSvg() {
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)') .attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)')
.attr('stroke-width', 2); .attr('stroke-width', 2);
//
nodeGroupEnter.append('text') nodeGroupEnter.append('text')
.attr('x', d => d.width / 2) .attr('x', d => d.width / 2)
.attr('y', d => d.height / 2 + 6) .attr('y', d => d.height / 2 - 6) //
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('font-size', 16) .attr('font-size', 16)
.attr('fill', 'var(--main-color)') .attr('fill', 'var(--main-color)')
.attr('font-weight', 600) .attr('font-weight', 600)
.text(d => d.labels?.[0]?.text || 'Tool'); .text(d => d.labels?.[0]?.text || 'Tool');
//
nodeGroupEnter.append('g')
.attr('class', 'node-status')
.each(function (d) {
const status = state.dataView.get(d.id)?.status || 'waiting';
const g = d3.select(this);
if (status === 'running') {
// +
g.append('circle')
.attr('cx', d.width / 2 - 32)
.attr('cy', d.height - 16)
.attr('r', 6) //
.attr('fill', 'none')
.attr('stroke', 'var(--main-color)') // 使
.attr('stroke-width', 3)
.attr('stroke-dasharray', 20)
.attr('stroke-dashoffset', 0)
.append('animateTransform')
.attr('attributeName', 'transform')
.attr('attributeType', 'XML')
.attr('type', 'rotate')
.attr('from', `0 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
.attr('to', `360 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
.attr('dur', '1s')
.attr('repeatCount', 'indefinite');
g.append('text')
.attr('x', d.width / 2 - 16)
.attr('y', d.height - 12)
.attr('font-size', 13)
.attr('fill', 'var(--main-color)')
.text('running');
} else if (status === 'waiting') {
g.append('circle')
.attr('cx', d.width / 2 - 32)
.attr('cy', d.height - 16)
.attr('r', 6)
.attr('fill', 'none')
.attr('stroke', '#bdbdbd')
.attr('stroke-width', 3);
g.append('text')
.attr('x', d.width / 2 - 16)
.attr('y', d.height - 12)
.attr('font-size', 13)
.attr('fill', '#bdbdbd')
.text('waiting');
} else if (status === 'success') {
g.append('circle')
.attr('cx', d.width / 2 - 32)
.attr('cy', d.height - 16)
.attr('r', 6) // waiting
.attr('fill', 'none')
.attr('stroke', '#4caf50')
.attr('stroke-width', 3);
g.append('text')
.attr('x', d.width / 2 - 16)
.attr('y', d.height - 12)
.attr('font-size', 13)
.attr('fill', '#4caf50')
.text('success');
} else if (status === 'error') {
g.append('circle')
.attr('cx', d.width / 2 - 32)
.attr('cy', d.height - 16)
.attr('r', 6) // waiting
.attr('fill', 'none')
.attr('stroke', '#f44336')
.attr('stroke-width', 3);
g.append('text')
.attr('x', d.width / 2 - 16)
.attr('y', d.height - 12)
.attr('font-size', 13)
.attr('fill', '#f44336')
.text('error');
}
});
// enter // enter
nodeGroupEnter nodeGroupEnter
.transition() .transition()
@ -375,7 +470,7 @@ function renderSvg() {
function resetConnections() { function resetConnections() {
if (!state.nodes.length) return; if (!state.nodes.length) return;
const edges = []; const edges = [];
for (let i = 0; i < state.nodes.length - 1; ++ i) { for (let i = 0; i < state.nodes.length - 1; ++i) {
const prev = state.nodes[i]; const prev = state.nodes[i];
const next = state.nodes[i + 1]; const next = state.nodes[i + 1];
edges.push({ edges.push({
@ -391,16 +486,32 @@ function resetConnections() {
const context = inject('context') as any; const context = inject('context') as any;
context.reset = resetConnections; context.reset = resetConnections;
context.state = state; context.state = state;
context.render = renderSvg;
onMounted(() => { onMounted(() => {
nextTick(drawDiagram); nextTick(drawDiagram);
}); });
// 4.
function getNodePopupStyle(node: any): any {
// svg
// offsetXnode.xnode.y
console.log(node);
const left = (node.x || 0) + (node.width || 160) - 120; //
const top = (node.y || 0) + 30; //
return {
position: 'absolute',
left: `${left}px`,
top: `${top}px`,
};
}
</script> </script>
<style> <style>
.diagram-container { .diagram-container {
width: 100%; width: 100%;
min-height: 300px; min-height: 200px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
@ -408,4 +519,28 @@ onMounted(() => {
padding: 24px 0; padding: 24px 0;
overflow-x: auto; overflow-x: auto;
} }
.node-popup {
position: absolute;
background: var(--background);
border: 1px solid var(--main-color);
width: 240px;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
white-space: nowrap;
z-index: 10;
}
/* 旋转动画 */
.status-running-circle {
animation: spin 1s linear infinite;
transform-origin: center;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
</style> </style>

File diff suppressed because one or more lines are too long