support self-check

This commit is contained in:
锦恢 2025-07-03 17:18:54 +08:00
parent dcabd47a20
commit 25f74b8f1e
15 changed files with 289 additions and 40 deletions

View File

@ -4,7 +4,7 @@
<div class="left"> <div class="left">
<h2> <h2>
<span class="iconfont icon-chat"></span> <span class="iconfont icon-chat"></span>
提示词模块 {{ t('prompt-module') }}
</h2> </h2>
<PromptTemplates :tab-id="props.tabId"></PromptTemplates> <PromptTemplates :tab-id="props.tabId"></PromptTemplates>
@ -24,6 +24,9 @@ import { defineProps } from 'vue';
import PromptTemplates from './prompt-templates.vue'; import PromptTemplates from './prompt-templates.vue';
import PromptReader from './prompt-reader.vue'; import PromptReader from './prompt-reader.vue';
import PromptLogger from './prompt-logger.vue'; import PromptLogger from './prompt-logger.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {

View File

@ -0,0 +1,107 @@
<template>
<div class="diagram-item-record" v-if="props.dataView && props.dataView.tool">
<div class="item-header">
<span class="item-title">{{ props.dataView.tool.name }}</span>
<span class="item-status" :class="props.dataView.status">{{ props.dataView.status }}</span>
</div>
<div class="item-desc">{{ props.dataView.tool.description }}</div>
<div class="item-schema">
<span class="item-label">Input Schema:</span>
<pre class="item-json">{{ formatJson(props.dataView.tool.inputSchema) }}</pre>
</div>
<div v-if="props.dataView.result !== undefined" class="item-result">
<span class="item-label">Result:</span>
<pre class="item-json">{{ formatJson(props.dataView.result) }}</pre>
</div>
</div>
<div v-else class="diagram-item-record">
<div class="item-header">
<span class="item-title">No Tool Selected</span>
</div>
<div class="item-desc">Please select a tool to view its details.</div>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue';
import type { NodeDataView } from './diagram';
const props = defineProps({
dataView: {
type: Object as PropType<NodeDataView | undefined | null>,
required: true
}
})
function formatJson(obj: any) {
try {
return JSON.stringify(obj, null, 2)
} catch {
return String(obj)
}
}
</script>
<style scoped>
.diagram-item-record {
padding: 14px 18px;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
font-size: 15px;
color: #222;
max-width: 420px;
word-break: break-all;
}
.item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.item-title {
font-weight: bold;
font-size: 17px;
color: var(--main-color, #409EFF);
}
.item-status {
font-size: 13px;
padding: 2px 10px;
border-radius: 12px;
margin-left: 8px;
text-transform: capitalize;
}
.item-status.running { color: #2196f3; }
.item-status.success { color: #43a047; }
.item-status.error { color: #e53935; }
.item-status.waiting { color: #aaa; }
.item-status.default { color: #888; }
.item-desc {
margin-bottom: 8px;
opacity: 0.8;
font-size: 14px;
}
.item-label {
font-weight: 500;
margin-right: 4px;
color: var(--main-color, #409EFF);
}
.item-json {
border-radius: 4px;
padding: 6px 10px;
font-size: 13px;
font-family: var(--code-font-family, monospace);
margin: 2px 0 8px 0;
white-space: pre-wrap;
word-break: break-all;
}
.item-result {
margin-top: 6px;
}
</style>

View File

@ -1,10 +1,14 @@
import type { ElkNode } from 'elkjs/lib/elk-api'; import type { ElkNode } from 'elkjs/lib/elk-api';
import { TaskLoop } from '../chat/core/task-loop'; 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'; import type { ToolItem } from '@/hook/type';
import I18n from '@/i18n';
const { t } = I18n.global;
export interface Edge { export interface Edge {
id: string; id: string;
sources: string[]; sources: string[];
@ -50,11 +54,11 @@ export function invalidConnectionDetector(state: DiagramState, d: Node): CanConn
const to = d.id; const to = d.id;
if (!from) { if (!from) {
return { canConnect: false, reason: '未选择起始节点' }; return { canConnect: false, reason: t('not-select-begin-node') };
} }
if (from === to) { if (from === to) {
return { canConnect: false, reason: '不能连接到自身' }; return { canConnect: false, reason: '' };
} }
// 建立邻接表 // 建立邻接表
@ -84,11 +88,11 @@ export function invalidConnectionDetector(state: DiagramState, d: Node): CanConn
} }
if (hasPath(to, from, new Set())) { if (hasPath(to, from, new Set())) {
return { canConnect: false, reason: '连接会形成环路' }; return { canConnect: false, reason: t('can-make-loop') };
} }
if (hasPath(from, to, new Set())) { if (hasPath(from, to, new Set())) {
return { canConnect: false, reason: '这是一个重复的连接' }; return { canConnect: false, reason: t('this-is-repeat-connection') };
} }
return { return {
@ -196,13 +200,13 @@ export async function makeNodeTest(
} catch (e) { } catch (e) {
// ElMessage.error('AI 生成的 JSON 解析错误'); // ElMessage.error('AI 生成的 JSON 解析错误');
dataView.status = 'error'; dataView.status = 'error';
dataView.result = 'AI 生成的 JSON 解析错误'; dataView.result = t('ai-gen-error-json');
context.render(); context.render();
} }
} else { } else {
// ElMessage.error('AI 调用了未知的工具'); // ElMessage.error('AI 调用了未知的工具');
dataView.status = 'error'; dataView.status = 'error';
dataView.result = 'AI 调用了未知的工具 ' + toolCall.function?.name; dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name;
context.render(); context.render();
} }
loop.abort(); loop.abort();

View File

@ -2,16 +2,19 @@
<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'"> <template v-for="(node, index) in state.nodes" :key="node.id + '-popup'">
<transition name="collapse-from-top" mode="out-in">
<div <div
v-if="state.hoverNodeId === node.id" v-show="state.hoverNodeId === node.id && state.dataView.get(node.id)?.status !== 'waiting'"
@mouseenter="setHoverItem(node.id)"
@mouseleave="clearHoverItem()"
:style="getNodePopupStyle(node)" :style="getNodePopupStyle(node)"
class="node-popup" class="node-popup"
> >
<div>节点{{ node.labels?.[0]?.text || node.id }}</div> <DiagramItemRecord :data-view="state.dataView.get(node.id)"/>
<div>: {{ node.width }}, : {{ node.height }}</div>
</div> </div>
</template> --> </transition>
</template>
</div> </div>
</template> </template>
@ -23,6 +26,12 @@ import { mcpClientAdapter } from '@/views/connect/core';
import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram'; import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import DiagramItemRecord from './diagram-item-record.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const svgContainer = ref<HTMLDivElement | null>(null); const svgContainer = ref<HTMLDivElement | null>(null);
let prevNodes: any[] = []; let prevNodes: any[] = [];
let prevEdges: any[] = []; let prevEdges: any[] = [];
@ -37,6 +46,27 @@ const state = reactive({
dataView: new Map<string, NodeDataView> dataView: new Map<string, NodeDataView>
}); });
let cancelHoverHandler: NodeJS.Timeout | undefined = undefined;
const setHoverItem = (id: string) => {
if (cancelHoverHandler) {
clearTimeout(cancelHoverHandler);
}
state.hoverNodeId = id;
}
const clearHoverItem = () => {
cancelHoverHandler = setTimeout(() => {
if (cancelHoverHandler) {
clearTimeout(cancelHoverHandler);
}
if (state.hoverNodeId) {
state.hoverNodeId = null;
}
}, 300);
};
const getAllTools = async () => { const getAllTools = async () => {
const items = []; const items = [];
for (const client of mcpClientAdapter.clients) { for (const client of mcpClientAdapter.clients) {
@ -287,12 +317,12 @@ function renderSvg() {
} else { } else {
state.selectedNodeId = d.id; state.selectedNodeId = d.id;
renderSvg(); renderSvg();
context.setCaption('选择另一个节点以定义测试拓扑'); context.setCaption(t('select-node-define-test-tomo'));
} }
state.draggingNodeId = null; state.draggingNodeId = null;
}) })
.on('mouseover', function (event, d) { .on('mouseover', function (event, d) {
state.hoverNodeId = d.id; setHoverItem(d.id);
d3.select(this).select('rect') d3.select(this).select('rect')
.transition() .transition()
.duration(200) .duration(200)
@ -300,7 +330,7 @@ function renderSvg() {
.attr('stroke-width', 2); .attr('stroke-width', 2);
}) })
.on('mouseout', function (event, d) { .on('mouseout', function (event, d) {
state.hoverNodeId = null; clearHoverItem();
if (state.selectedNodeId === d.id) return; if (state.selectedNodeId === d.id) return;
d3.select(this).select('rect') d3.select(this).select('rect')
.transition() .transition()
@ -436,7 +466,7 @@ function renderSvg() {
.attr('stroke', 'var(--main-color)') .attr('stroke', 'var(--main-color)')
.attr('stroke-width', 4.5); .attr('stroke-width', 4.5);
context.setCaption('点击边以删除'); context.setCaption(t('click-edge-to-delete'));
}) })
.on('mouseout', function () { .on('mouseout', function () {
@ -500,10 +530,8 @@ onMounted(() => {
function getNodePopupStyle(node: any): any { function getNodePopupStyle(node: any): any {
// svg // svg
// offsetXnode.xnode.y // offsetXnode.xnode.y
console.log(node); const left = (node.x || 0) + (node.width || 160) + 120; //
const top = (node.y || 0) + 30;
const left = (node.x || 0) + (node.width || 160) - 120; //
const top = (node.y || 0) + 30; //
return { return {
position: 'absolute', position: 'absolute',
left: `${left}px`, left: `${left}px`,

View File

@ -17,7 +17,12 @@
placeholder="请输入 prompt" /> placeholder="请输入 prompt" />
<div style="display: flex; align-items: center; margin-bottom: 8px;"> <div style="display: flex; align-items: center; margin-bottom: 8px;">
<el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" /> <el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" />
<span style="opacity: 0.7;">enableXmlWrapper</span> <span
:style="{
opacity: enableXmlWrapper? 1 : 0.7,
color: enableXmlWrapper ? 'var(--main-color)' : undefined
}"
>XML</span>
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">
<el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button> <el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button>
@ -44,6 +49,9 @@ import { nextTick, provide, ref } from 'vue';
import Diagram from './diagram.vue'; import Diagram from './diagram.vue';
import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram'; import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { tabs } from '../../panel';
import type { ToolStorage } from '../tools';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const showDiagram = ref(true); const showDiagram = ref(true);
@ -52,6 +60,21 @@ const { t } = useI18n();
const caption = ref(''); const caption = ref('');
const showCaption = ref(false); const showCaption = ref(false);
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage;
if (!tabStorage.formData) {
tabStorage.formData = {};
}
function setCaption(text: string) { function setCaption(text: string) {
caption.value = text; caption.value = text;
if (caption.value) { if (caption.value) {

View File

@ -1,11 +1,11 @@
<template> <template>
<el-scrollbar height="100%"> <el-scrollbar height="100%">
<AutoDetector /> <AutoDetector :tab-id="props.tabId" />
<div class="tool-module"> <div class="tool-module">
<div class="left"> <div class="left">
<h2> <h2>
<span class="iconfont icon-tool"></span> <span class="iconfont icon-tool"></span>
工具模块 {{ t('tool-module') }}
</h2> </h2>
<ToolList :tab-id="props.tabId"></ToolList> <ToolList :tab-id="props.tabId"></ToolList>
@ -25,7 +25,10 @@ import { defineProps } from 'vue';
import ToolList from './tool-list.vue'; import ToolList from './tool-list.vue';
import ToolExecutor from './tool-executor.vue'; import ToolExecutor from './tool-executor.vue';
import ToolLogger from './tool-logger.vue'; import ToolLogger from './tool-logger.vue';
import AutoDetector from './auto-detector.vue'; import AutoDetector from './auto-detector/index.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {

View File

@ -185,5 +185,14 @@
"how-to-use": "كيفية الاستخدام؟", "how-to-use": "كيفية الاستخدام؟",
"is-required": "هو حقل مطلوب", "is-required": "هو حقل مطلوب",
"edit-ai-mook-prompt": "تحرير إشارات AI Mook", "edit-ai-mook-prompt": "تحرير إشارات AI Mook",
"start-auto-detect": "بدء عملية الفحص الذاتي" "start-auto-detect": "بدء عملية الفحص الذاتي",
"tool-module": "وحدة الأدوات",
"prompt-module": "وحدة المطالبات",
"not-select-begin-node": "لم يتم تحديد عقدة البداية",
"can-make-loop": "سيؤدي الاتصال إلى تكوين حلقة",
"this-is-repeat-connection": "هذا رابط مكرر",
"ai-gen-error-json": "خطأ في تحليل JSON الذي تم إنشاؤه بواسطة الذكاء الاصطناعي",
"ai-invoke-unknown-tool": "استدعت الذكاء الاصطناعي أداة غير معروفة",
"click-edge-to-delete": "انقر على الحافة للحذف",
"select-node-define-test-tomo": "اختر عقدة أخرى لتحديد طوبولوجيا الاختبار"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "Wie benutzt man?", "how-to-use": "Wie benutzt man?",
"is-required": "ist ein Pflichtfeld", "is-required": "ist ein Pflichtfeld",
"edit-ai-mook-prompt": "AI Mook-Prompts bearbeiten", "edit-ai-mook-prompt": "AI Mook-Prompts bearbeiten",
"start-auto-detect": "Selbsttest starten" "start-auto-detect": "Selbsttest starten",
"tool-module": "Werkzeugmodul",
"prompt-module": "Aufforderungsmodul",
"not-select-begin-node": "Kein Startknoten ausgewählt",
"can-make-loop": "Die Verbindung wird eine Schleife bilden",
"this-is-repeat-connection": "Dies ist ein doppelter Link",
"ai-gen-error-json": "Fehler beim Parsen von KI-generiertem JSON",
"ai-invoke-unknown-tool": "KI hat ein unbekanntes Tool aufgerufen",
"click-edge-to-delete": "Klicken Sie auf die Kante, um sie zu löschen",
"select-node-define-test-tomo": "Wählen Sie einen anderen Knoten aus, um die Testtopologie zu definieren"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "How to use?", "how-to-use": "How to use?",
"is-required": "is a required field", "is-required": "is a required field",
"edit-ai-mook-prompt": "Edit AI Mook prompts", "edit-ai-mook-prompt": "Edit AI Mook prompts",
"start-auto-detect": "Start self-check" "start-auto-detect": "Start self-check",
"tool-module": "Tool module",
"prompt-module": "Prompt Module",
"not-select-begin-node": "No starting node selected",
"can-make-loop": "The connection will form a loop",
"this-is-repeat-connection": "This is a duplicate link",
"ai-gen-error-json": "AI-generated JSON parsing error",
"ai-invoke-unknown-tool": "AI called an unknown tool",
"click-edge-to-delete": "Click the edge to delete",
"select-node-define-test-tomo": "Select another node to define the test topology"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "Comment utiliser ?", "how-to-use": "Comment utiliser ?",
"is-required": "est un champ obligatoire", "is-required": "est un champ obligatoire",
"edit-ai-mook-prompt": "Modifier les invites AI Mook", "edit-ai-mook-prompt": "Modifier les invites AI Mook",
"start-auto-detect": "Démarrer l'autovérification" "start-auto-detect": "Démarrer l'autovérification",
"tool-module": "Module d'outils",
"prompt-module": "Module d'invite",
"not-select-begin-node": "Aucun nœud de départ sélectionné",
"can-make-loop": "La connexion formera une boucle",
"this-is-repeat-connection": "Ceci est un lien en double",
"ai-gen-error-json": "Erreur d'analyse JSON générée par IA",
"ai-invoke-unknown-tool": "L'IA a appelé un outil inconnu",
"click-edge-to-delete": "Cliquez sur le bord pour supprimer",
"select-node-define-test-tomo": "Sélectionnez un autre nœud pour définir la topologie de test"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "使用方法", "how-to-use": "使用方法",
"is-required": "は必須フィールドです", "is-required": "は必須フィールドです",
"edit-ai-mook-prompt": "AI Mookプロンプトを編集", "edit-ai-mook-prompt": "AI Mookプロンプトを編集",
"start-auto-detect": "自己診断を開始" "start-auto-detect": "自己診断を開始",
"tool-module": "ツールモジュール",
"prompt-module": "プロンプトモジュール",
"not-select-begin-node": "開始ノードが選択されていません",
"can-make-loop": "接続によりループが形成されます",
"this-is-repeat-connection": "これは重複したリンクです",
"ai-gen-error-json": "AI生成JSONの解析エラー",
"ai-invoke-unknown-tool": "AIが未知のツールを呼び出しました",
"click-edge-to-delete": "クリックして削除",
"select-node-define-test-tomo": "テストトポロジを定義するために別のノードを選択してください"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "사용 방법?", "how-to-use": "사용 방법?",
"is-required": "는 필수 필드입니다", "is-required": "는 필수 필드입니다",
"edit-ai-mook-prompt": "AI Mook 프롬프트 편집", "edit-ai-mook-prompt": "AI Mook 프롬프트 편집",
"start-auto-detect": "자체 점검 시작" "start-auto-detect": "자체 점검 시작",
"tool-module": "도구 모듈",
"prompt-module": "프롬프트 모듈",
"not-select-begin-node": "시작 노드가 선택되지 않았습니다",
"can-make-loop": "연결이 루프를 형성합니다",
"this-is-repeat-connection": "이것은 중복된 링크입니다",
"ai-gen-error-json": "AI 생성 JSON 구문 분석 오류",
"ai-invoke-unknown-tool": "AI가 알 수 없는 도구를 호출했습니다",
"click-edge-to-delete": "가장자리를 클릭하여 삭제",
"select-node-define-test-tomo": "테스트 토폴로지를 정의하려면 다른 노드를 선택하세요"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "Как использовать?", "how-to-use": "Как использовать?",
"is-required": "является обязательным полем", "is-required": "является обязательным полем",
"edit-ai-mook-prompt": "Редактировать подсказки AI Mook", "edit-ai-mook-prompt": "Редактировать подсказки AI Mook",
"start-auto-detect": "Запустить самопроверку" "start-auto-detect": "Запустить самопроверку",
"tool-module": "Модуль инструментов",
"prompt-module": "Модуль подсказок",
"not-select-begin-node": "Начальный узел не выбран",
"can-make-loop": "Соединение образует петлю",
"this-is-repeat-connection": "Это повторяющаяся ссылка",
"ai-gen-error-json": "Ошибка разбора JSON, созданного ИИ",
"ai-invoke-unknown-tool": "ИИ вызвал неизвестный инструмент",
"click-edge-to-delete": "Нажмите на край, чтобы удалить",
"select-node-define-test-tomo": "Выберите другой узел для определения тестовой топологии"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "如何使用?", "how-to-use": "如何使用?",
"is-required": "是必填字段", "is-required": "是必填字段",
"edit-ai-mook-prompt": "编辑 AI Mook 提示词", "edit-ai-mook-prompt": "编辑 AI Mook 提示词",
"start-auto-detect": "开启自检程序" "start-auto-detect": "开启自检程序",
"tool-module": "工具模块",
"prompt-module": "提示词模块",
"not-select-begin-node": "未选择起始节点",
"can-make-loop": "连接会形成环路",
"this-is-repeat-connection": "这是一个重复的连接",
"ai-gen-error-json": "AI 生成的 JSON 解析错误",
"ai-invoke-unknown-tool": "AI 调用了未知的工具",
"click-edge-to-delete": "点击边以删除",
"select-node-define-test-tomo": "选择另一个节点以定义测试拓扑"
} }

View File

@ -185,5 +185,14 @@
"how-to-use": "如何使用?", "how-to-use": "如何使用?",
"is-required": "是必填欄位", "is-required": "是必填欄位",
"edit-ai-mook-prompt": "編輯AI Mook提示詞", "edit-ai-mook-prompt": "編輯AI Mook提示詞",
"start-auto-detect": "開啟自檢程序" "start-auto-detect": "開啟自檢程序",
"tool-module": "工具模組",
"prompt-module": "提示詞模組",
"not-select-begin-node": "未選擇起始節點",
"can-make-loop": "連接會形成環路",
"this-is-repeat-connection": "這是一個重複的連結",
"ai-gen-error-json": "AI 生成的 JSON 解析錯誤",
"ai-invoke-unknown-tool": "AI調用了未知的工具",
"click-edge-to-delete": "點擊邊緣以刪除",
"select-node-define-test-tomo": "選擇另一個節點以定義測試拓撲"
} }