|
@@ -1,101 +1,87 @@
|
|
|
<template>
|
|
|
- <el-card
|
|
|
- class="tools-loading-card"
|
|
|
- shadow="hover"
|
|
|
- @click="showDialog = true"
|
|
|
- :style="{ cursor: 'pointer' }"
|
|
|
- >
|
|
|
- <template #header>
|
|
|
- <div class="card-header">
|
|
|
- <span class="card-title">{{ toolData.toolName || 'MCP工具调用' }}</span>
|
|
|
- <el-icon :class="{ 'is-loading': isLoading }">
|
|
|
- <Loading v-if="isLoading" />
|
|
|
- <Tools v-else />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <div class="card-content">
|
|
|
- <p class="tool-description">
|
|
|
- {{ getToolDescription() }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- 详情对话框 -->
|
|
|
- <el-dialog
|
|
|
- v-model="showDialog"
|
|
|
- :title="toolData.toolName || 'MCP工具调用详情'"
|
|
|
- width="800px"
|
|
|
- destroy-on-close
|
|
|
- >
|
|
|
- <el-tabs v-model="activeTab" type="border-card">
|
|
|
- <el-tab-pane label="工具信息" name="info">
|
|
|
- <div class="tab-content">
|
|
|
- <el-descriptions :column="1" border>
|
|
|
+ <el-collapse v-model="activeCollapse" class="tools-collapse">
|
|
|
+ <el-collapse-item :name="collapseKey" class="tools-collapse-item">
|
|
|
+ <template #title>
|
|
|
+ <div class="collapse-header" :class="getRequestTypeClass()">
|
|
|
+ <div class="header-left">
|
|
|
+ <el-icon class="request-icon">
|
|
|
+ <component :is="getRequestIcon()" />
|
|
|
+ </el-icon>
|
|
|
+ <span class="request-type">{{ toolData.requestType }}</span>
|
|
|
+ <el-divider direction="vertical" />
|
|
|
+ <span class="tool-name">{{ toolData.toolName || '未知工具' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="header-right">
|
|
|
+ <el-icon :class="{ 'is-loading': isLoading }">
|
|
|
+ <Loading v-if="isLoading" />
|
|
|
+ <Check v-else />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="collapse-content">
|
|
|
+ <div class="content-section">
|
|
|
+ <h4 class="section-title">调用详情</h4>
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item label="请求类型">
|
|
|
+ <el-tag :type="getRequestTagType()" size="small">
|
|
|
+ {{ toolData.requestType }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
<el-descriptions-item label="工具名称">
|
|
|
{{ toolData.toolName || '未知工具' }}
|
|
|
</el-descriptions-item>
|
|
|
- <el-descriptions-item label="调用状态">
|
|
|
- <el-tag :type="getStatusType()">
|
|
|
+ <el-descriptions-item label="执行状态">
|
|
|
+ <el-tag :type="getStatusType()" size="small">
|
|
|
{{ getStatusText() }}
|
|
|
</el-tag>
|
|
|
</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</div>
|
|
|
- </el-tab-pane>
|
|
|
-
|
|
|
- <el-tab-pane label="传入参数" name="params">
|
|
|
- <div class="tab-content">
|
|
|
- <div v-if="toolData.params">
|
|
|
- <pre class="json-content">{{ toolData.params }}</pre>
|
|
|
- </div>
|
|
|
- <el-empty v-else description="无传入参数" />
|
|
|
- </div>
|
|
|
- </el-tab-pane>
|
|
|
|
|
|
- <el-tab-pane label="返回结果" name="result">
|
|
|
- <div class="tab-content">
|
|
|
- <div v-if="toolData.result">
|
|
|
- <pre class="json-content">{{ toolData.result }}</pre>
|
|
|
+ <div class="content-section">
|
|
|
+ <h4 class="section-title">{{ getDataSectionTitle() }}</h4>
|
|
|
+ <div v-if="toolData.data">
|
|
|
+ <pre class="json-content">{{ formatData(toolData.data) }}</pre>
|
|
|
</div>
|
|
|
<div v-else-if="isLoading">
|
|
|
- <el-skeleton :rows="5" animated />
|
|
|
+ <el-skeleton :rows="3" animated />
|
|
|
</div>
|
|
|
- <el-empty v-else description="暂无返回结果" />
|
|
|
+ <el-empty v-else :description="getEmptyDescription()" />
|
|
|
</div>
|
|
|
- </el-tab-pane>
|
|
|
- </el-tabs>
|
|
|
- </el-dialog>
|
|
|
+ </div>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
-import { Loading, Tools } from '@element-plus/icons-vue'
|
|
|
+import { Loading, Check, ArrowRight, ArrowLeft, Tools } from '@element-plus/icons-vue'
|
|
|
|
|
|
type Props = {
|
|
|
data: string
|
|
|
}
|
|
|
|
|
|
interface ToolData {
|
|
|
- toolName: string
|
|
|
- params: string
|
|
|
- result?: string
|
|
|
+ requestType: string // request/response
|
|
|
+ toolName: string // 工具名
|
|
|
+ data: string // 参数或返回结果
|
|
|
}
|
|
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
|
|
-const showDialog = ref(false)
|
|
|
-const activeTab = ref('info')
|
|
|
+const activeCollapse = ref<string[]>([])
|
|
|
+const collapseKey = ref('tool-collapse')
|
|
|
const toolData = ref<ToolData>({
|
|
|
+ requestType: '',
|
|
|
toolName: '',
|
|
|
- params: '',
|
|
|
- result: undefined
|
|
|
+ data: ''
|
|
|
})
|
|
|
|
|
|
-// 计算是否为加载状态(2行数据表示正在加载)
|
|
|
+// 计算是否为加载状态(request类型且无数据表示正在加载)
|
|
|
const isLoading = computed(() => {
|
|
|
- return !toolData.value.result
|
|
|
+ return toolData.value.requestType.toLowerCase() === 'request' && !toolData.value.data
|
|
|
})
|
|
|
|
|
|
// 解析传入的数据
|
|
@@ -106,72 +92,231 @@ onMounted(() => {
|
|
|
|
|
|
if (lines.length >= 2) {
|
|
|
toolData.value = {
|
|
|
- toolName: lines[0] || '未知工具',
|
|
|
- params: lines[1] || '无参数',
|
|
|
- result: lines[2] || undefined // 第三行为结果,可能不存在
|
|
|
+ requestType: lines[0] || 'request',
|
|
|
+ toolName: lines[1] || '未知工具',
|
|
|
+ data: lines[2] || '' // 第三行为参数/结果,可能不存在
|
|
|
}
|
|
|
} else {
|
|
|
toolData.value = {
|
|
|
+ requestType: 'error',
|
|
|
toolName: '数据格式错误',
|
|
|
- params: '应为2-3行格式',
|
|
|
- result: undefined
|
|
|
+ data: '应为3行格式:request/response、工具名、参数/结果'
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('解析工具数据失败:', e)
|
|
|
toolData.value = {
|
|
|
+ requestType: 'error',
|
|
|
toolName: '数据解析失败',
|
|
|
- params: '解析错误',
|
|
|
- result: undefined
|
|
|
+ data: '解析错误'
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-// 获取工具描述
|
|
|
-const getToolDescription = () => {
|
|
|
- if (isLoading.value) {
|
|
|
- return '工具正在执行中...'
|
|
|
+// 获取请求类型对应的图标
|
|
|
+const getRequestIcon = () => {
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ switch (type) {
|
|
|
+ case 'request':
|
|
|
+ return ArrowRight
|
|
|
+ case 'response':
|
|
|
+ return ArrowLeft
|
|
|
+ case 'error':
|
|
|
+ return Tools
|
|
|
+ default:
|
|
|
+ return Tools
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取请求类型对应的CSS类
|
|
|
+const getRequestTypeClass = () => {
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ return `request-type-${type}`
|
|
|
+}
|
|
|
+
|
|
|
+// 获取请求类型标签类型
|
|
|
+const getRequestTagType = () => {
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ switch (type) {
|
|
|
+ case 'request':
|
|
|
+ return 'primary'
|
|
|
+ case 'response':
|
|
|
+ return 'success'
|
|
|
+ case 'error':
|
|
|
+ return 'danger'
|
|
|
+ default:
|
|
|
+ return 'info'
|
|
|
}
|
|
|
- return '工具执行完成,点击查看详情'
|
|
|
}
|
|
|
|
|
|
// 获取状态类型
|
|
|
const getStatusType = () => {
|
|
|
if (isLoading.value) return 'warning'
|
|
|
- return 'success'
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ return type === 'error' ? 'danger' : 'success'
|
|
|
}
|
|
|
|
|
|
// 获取状态文本
|
|
|
const getStatusText = () => {
|
|
|
if (isLoading.value) return '执行中'
|
|
|
- return '执行完成'
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ switch (type) {
|
|
|
+ case 'request':
|
|
|
+ return '请求发送'
|
|
|
+ case 'response':
|
|
|
+ return '响应接收'
|
|
|
+ case 'error':
|
|
|
+ return '执行错误'
|
|
|
+ default:
|
|
|
+ return '执行完成'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取数据区域标题
|
|
|
+const getDataSectionTitle = () => {
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ switch (type) {
|
|
|
+ case 'request':
|
|
|
+ return '请求参数'
|
|
|
+ case 'response':
|
|
|
+ return '响应结果'
|
|
|
+ case 'error':
|
|
|
+ return '错误信息'
|
|
|
+ default:
|
|
|
+ return '数据内容'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取空数据描述
|
|
|
+const getEmptyDescription = () => {
|
|
|
+ const type = toolData.value.requestType.toLowerCase()
|
|
|
+ switch (type) {
|
|
|
+ case 'request':
|
|
|
+ return '无请求参数'
|
|
|
+ case 'response':
|
|
|
+ return '暂无响应结果'
|
|
|
+ case 'error':
|
|
|
+ return '无错误详情'
|
|
|
+ default:
|
|
|
+ return '无数据'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化数据显示
|
|
|
+const formatData = (data: string) => {
|
|
|
+ try {
|
|
|
+ // 尝试解析为JSON并格式化
|
|
|
+ const parsed = JSON.parse(data)
|
|
|
+ return JSON.stringify(parsed, null, 2)
|
|
|
+ } catch {
|
|
|
+ // 如果不是JSON,直接返回原始数据
|
|
|
+ return data
|
|
|
+ }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.tools-loading-card {
|
|
|
- transition: all 0.3s ease;
|
|
|
+.tools-collapse {
|
|
|
border: 1px solid var(--el-border-color-light);
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-.tools-loading-card:hover {
|
|
|
- border-color: var(--el-color-primary);
|
|
|
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+.tools-collapse-item {
|
|
|
+ border: none;
|
|
|
}
|
|
|
|
|
|
-.card-header {
|
|
|
+.tools-collapse-item :deep(.el-collapse-item__header) {
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.tools-collapse-item :deep(.el-collapse-item__content) {
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.collapse-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ width: 100%;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.collapse-header:hover {
|
|
|
+ background-color: var(--el-fill-color-light);
|
|
|
+}
|
|
|
+
|
|
|
+.header-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.header-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
-.card-title {
|
|
|
+.request-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.request-type {
|
|
|
font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+}
|
|
|
+
|
|
|
+.tool-name {
|
|
|
+ font-weight: 500;
|
|
|
color: var(--el-text-color-primary);
|
|
|
}
|
|
|
|
|
|
-.el-icon {
|
|
|
+/* 不同请求类型的样式 */
|
|
|
+.request-type-request {
|
|
|
+ border-left: 4px solid var(--el-color-primary);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-request .request-icon {
|
|
|
+ color: var(--el-color-primary);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-request .request-type {
|
|
|
color: var(--el-color-primary);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-response {
|
|
|
+ border-left: 4px solid var(--el-color-success);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-response .request-icon {
|
|
|
+ color: var(--el-color-success);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-response .request-type {
|
|
|
+ color: var(--el-color-success);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-error {
|
|
|
+ border-left: 4px solid var(--el-color-danger);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-error .request-icon {
|
|
|
+ color: var(--el-color-danger);
|
|
|
+}
|
|
|
+
|
|
|
+.request-type-error .request-type {
|
|
|
+ color: var(--el-color-danger);
|
|
|
+}
|
|
|
+
|
|
|
+.el-icon {
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
@@ -189,20 +334,25 @@ const getStatusText = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.card-content {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
+.collapse-content {
|
|
|
+ padding: 16px;
|
|
|
+ background-color: var(--el-fill-color-blank);
|
|
|
+ border-top: 1px solid var(--el-border-color-lighter);
|
|
|
}
|
|
|
|
|
|
-.tool-description {
|
|
|
- margin: 0;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- font-size: 14px;
|
|
|
+.content-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
-.tab-content {
|
|
|
- padding: 16px 0;
|
|
|
+.content-section:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ margin: 0 0 12px 0;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
}
|
|
|
|
|
|
.json-content {
|
|
@@ -218,5 +368,26 @@ const getStatusText = () => {
|
|
|
white-space: pre-wrap;
|
|
|
word-break: break-all;
|
|
|
overflow-x: auto;
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .header-left {
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .request-type {
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-name {
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .collapse-content {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|