support self-check
This commit is contained in:
parent
dcabd47a20
commit
25f74b8f1e
@ -4,7 +4,7 @@
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-chat"></span>
|
||||
提示词模块
|
||||
{{ t('prompt-module') }}
|
||||
</h2>
|
||||
|
||||
<PromptTemplates :tab-id="props.tabId"></PromptTemplates>
|
||||
@ -24,6 +24,9 @@ import { defineProps } from 'vue';
|
||||
import PromptTemplates from './prompt-templates.vue';
|
||||
import PromptReader from './prompt-reader.vue';
|
||||
import PromptLogger from './prompt-logger.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
|
@ -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>
|
@ -1,10 +1,14 @@
|
||||
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 { ChatStorage } from '../chat/chat-box/chat';
|
||||
import type { ChatStorage } from '../../chat/chat-box/chat';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { ToolItem } from '@/hook/type';
|
||||
|
||||
import I18n from '@/i18n';
|
||||
|
||||
const { t } = I18n.global;
|
||||
|
||||
export interface Edge {
|
||||
id: string;
|
||||
sources: string[];
|
||||
@ -50,11 +54,11 @@ export function invalidConnectionDetector(state: DiagramState, d: Node): CanConn
|
||||
const to = d.id;
|
||||
|
||||
if (!from) {
|
||||
return { canConnect: false, reason: '未选择起始节点' };
|
||||
return { canConnect: false, reason: t('not-select-begin-node') };
|
||||
}
|
||||
|
||||
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())) {
|
||||
return { canConnect: false, reason: '连接会形成环路' };
|
||||
return { canConnect: false, reason: t('can-make-loop') };
|
||||
}
|
||||
|
||||
if (hasPath(from, to, new Set())) {
|
||||
return { canConnect: false, reason: '这是一个重复的连接' };
|
||||
return { canConnect: false, reason: t('this-is-repeat-connection') };
|
||||
}
|
||||
|
||||
return {
|
||||
@ -196,13 +200,13 @@ export async function makeNodeTest(
|
||||
} catch (e) {
|
||||
// ElMessage.error('AI 生成的 JSON 解析错误');
|
||||
dataView.status = 'error';
|
||||
dataView.result = 'AI 生成的 JSON 解析错误';
|
||||
dataView.result = t('ai-gen-error-json');
|
||||
context.render();
|
||||
}
|
||||
} else {
|
||||
// ElMessage.error('AI 调用了未知的工具');
|
||||
dataView.status = 'error';
|
||||
dataView.result = 'AI 调用了未知的工具 ' + toolCall.function?.name;
|
||||
dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name;
|
||||
context.render();
|
||||
}
|
||||
loop.abort();
|
@ -2,16 +2,19 @@
|
||||
<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> -->
|
||||
<template v-for="(node, index) in state.nodes" :key="node.id + '-popup'">
|
||||
<transition name="collapse-from-top" mode="out-in">
|
||||
<div
|
||||
v-show="state.hoverNodeId === node.id && state.dataView.get(node.id)?.status !== 'waiting'"
|
||||
@mouseenter="setHoverItem(node.id)"
|
||||
@mouseleave="clearHoverItem()"
|
||||
:style="getNodePopupStyle(node)"
|
||||
class="node-popup"
|
||||
>
|
||||
<DiagramItemRecord :data-view="state.dataView.get(node.id)"/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -23,6 +26,12 @@ import { mcpClientAdapter } from '@/views/connect/core';
|
||||
import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram';
|
||||
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);
|
||||
let prevNodes: any[] = [];
|
||||
let prevEdges: any[] = [];
|
||||
@ -37,6 +46,27 @@ const state = reactive({
|
||||
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 items = [];
|
||||
for (const client of mcpClientAdapter.clients) {
|
||||
@ -287,12 +317,12 @@ function renderSvg() {
|
||||
} else {
|
||||
state.selectedNodeId = d.id;
|
||||
renderSvg();
|
||||
context.setCaption('选择另一个节点以定义测试拓扑');
|
||||
context.setCaption(t('select-node-define-test-tomo'));
|
||||
}
|
||||
state.draggingNodeId = null;
|
||||
})
|
||||
.on('mouseover', function (event, d) {
|
||||
state.hoverNodeId = d.id;
|
||||
setHoverItem(d.id);
|
||||
d3.select(this).select('rect')
|
||||
.transition()
|
||||
.duration(200)
|
||||
@ -300,7 +330,7 @@ function renderSvg() {
|
||||
.attr('stroke-width', 2);
|
||||
})
|
||||
.on('mouseout', function (event, d) {
|
||||
state.hoverNodeId = null;
|
||||
clearHoverItem();
|
||||
if (state.selectedNodeId === d.id) return;
|
||||
d3.select(this).select('rect')
|
||||
.transition()
|
||||
@ -436,7 +466,7 @@ function renderSvg() {
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 4.5);
|
||||
|
||||
context.setCaption('点击边以删除');
|
||||
context.setCaption(t('click-edge-to-delete'));
|
||||
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
@ -500,10 +530,8 @@ onMounted(() => {
|
||||
function getNodePopupStyle(node: any): any {
|
||||
// 节点的 svg 坐标转为容器内绝对定位
|
||||
// 注意:这里假设 offsetX、node.x、node.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 {
|
||||
position: 'absolute',
|
||||
left: `${left}px`,
|
@ -17,7 +17,12 @@
|
||||
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>
|
||||
<span
|
||||
:style="{
|
||||
opacity: enableXmlWrapper? 1 : 0.7,
|
||||
color: enableXmlWrapper ? 'var(--main-color)' : undefined
|
||||
}"
|
||||
>XML</span>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<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 { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { tabs } from '../../panel';
|
||||
import type { ToolStorage } from '../tools';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const showDiagram = ref(true);
|
||||
@ -52,6 +60,21 @@ const { t } = useI18n();
|
||||
const caption = ref('');
|
||||
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) {
|
||||
caption.value = text;
|
||||
if (caption.value) {
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<el-scrollbar height="100%">
|
||||
<AutoDetector />
|
||||
<AutoDetector :tab-id="props.tabId" />
|
||||
<div class="tool-module">
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-tool"></span>
|
||||
工具模块
|
||||
{{ t('tool-module') }}
|
||||
</h2>
|
||||
<ToolList :tab-id="props.tabId"></ToolList>
|
||||
|
||||
@ -25,7 +25,10 @@ import { defineProps } from 'vue';
|
||||
import ToolList from './tool-list.vue';
|
||||
import ToolExecutor from './tool-executor.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({
|
||||
tabId: {
|
||||
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "كيفية الاستخدام؟",
|
||||
"is-required": "هو حقل مطلوب",
|
||||
"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": "اختر عقدة أخرى لتحديد طوبولوجيا الاختبار"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "Wie benutzt man?",
|
||||
"is-required": "ist ein Pflichtfeld",
|
||||
"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"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "How to use?",
|
||||
"is-required": "is a required field",
|
||||
"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"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "Comment utiliser ?",
|
||||
"is-required": "est un champ obligatoire",
|
||||
"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"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "使用方法",
|
||||
"is-required": "は必須フィールドです",
|
||||
"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": "テストトポロジを定義するために別のノードを選択してください"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "사용 방법?",
|
||||
"is-required": "는 필수 필드입니다",
|
||||
"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": "테스트 토폴로지를 정의하려면 다른 노드를 선택하세요"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "Как использовать?",
|
||||
"is-required": "является обязательным полем",
|
||||
"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": "Выберите другой узел для определения тестовой топологии"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "如何使用?",
|
||||
"is-required": "是必填字段",
|
||||
"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": "选择另一个节点以定义测试拓扑"
|
||||
}
|
@ -185,5 +185,14 @@
|
||||
"how-to-use": "如何使用?",
|
||||
"is-required": "是必填欄位",
|
||||
"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": "選擇另一個節點以定義測試拓撲"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user