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>
&ensp;
<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>
</template>
<el-scrollbar height="80vh">
@ -17,16 +37,14 @@
</div>
</transition>
</el-dialog>
<!-- <el-button @click="showDiagram = true" type="primary" style="margin-bottom: 16px;">
Show Tool Diagram
</el-button> -->
</template>
<script setup lang="ts">
import { nextTick, provide, ref } from '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';
const showDiagram = ref(true);
const caption = ref('');
@ -45,35 +63,40 @@ function setCaption(text: string) {
}
}
interface DiagramContext {
reset: () => void,
state?: DiagramState,
setCaption: (value: string) => void
}
const context: DiagramContext = {
reset: () => {},
render: () => {},
state: undefined,
setCaption
};
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;
if (state) {
const dispatches = topoSortParallel(state);
// for (const layer of dispatches) {
// await Promise.all(
// layer.map(nodeId => state.nodes[nodeId].run())
// );
// }
for (const nodeIds of dispatches) {
await Promise.all(
nodeIds.map(id => {
const node = state.dataView.get(id);
if (node) {
return makeNodeTest(node, enableXmlWrapper.value, testPrompt.value, context)
}
})
)
}
} else {
ElMessage.error('error');
}
}
</script>
<style>

View File

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

View File

@ -1,6 +1,17 @@
<template>
<div style="display: flex; align-items: center; gap: 16px;">
<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>
</template>
@ -9,7 +20,7 @@ import { ref, onMounted, nextTick, reactive, inject } from 'vue';
import * as d3 from 'd3';
import ELK from 'elkjs/lib/elk.bundled.js';
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';
const svgContainer = ref<HTMLDivElement | null>(null);
@ -21,7 +32,9 @@ const state = reactive({
edges: [] as any[],
selectedNodeId: 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 () => {
@ -79,10 +92,16 @@ const drawDiagram = async () => {
for (const tool of tools) {
nodes.push({
id: tool.name,
width: 160,
height: 48,
width: 200,
height: 64, //
labels: [{ text: tool.name || 'Tool' }]
});
state.dataView.set(tool.name, {
tool,
status: 'waiting',
result: null
});
}
state.edges = edges;
@ -273,6 +292,7 @@ function renderSvg() {
state.draggingNodeId = null;
})
.on('mouseover', function (event, d) {
state.hoverNodeId = d.id;
d3.select(this).select('rect')
.transition()
.duration(200)
@ -280,9 +300,8 @@ function renderSvg() {
.attr('stroke-width', 2);
})
.on('mouseout', function (event, d) {
if (state.selectedNodeId === d.id) {
return;
}
state.hoverNodeId = null;
if (state.selectedNodeId === d.id) return;
d3.select(this).select('rect')
.transition()
.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-width', 2);
//
nodeGroupEnter.append('text')
.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('font-size', 16)
.attr('fill', 'var(--main-color)')
.attr('font-weight', 600)
.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
nodeGroupEnter
.transition()
@ -391,16 +486,32 @@ function resetConnections() {
const context = inject('context') as any;
context.reset = resetConnections;
context.state = state;
context.render = renderSvg;
onMounted(() => {
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>
<style>
.diagram-container {
width: 100%;
min-height: 300px;
min-height: 200px;
display: flex;
justify-content: center;
align-items: flex-start;
@ -408,4 +519,28 @@ onMounted(() => {
padding: 24px 0;
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>

File diff suppressed because one or more lines are too long