kagg886 3 месяцев назад
Родитель
Сommit
fa75b854ba

+ 28 - 27
src/api/assist/index.ts

@@ -1,4 +1,4 @@
-import { ChatRequest, ChatResponse } from '/@/api/assist/type'
+import { ChatRequest, ChatResponse, ErrorResponse } from '/@/api/assist/type'
 import getOrigin from '/@/utils/origin'
 import { getToken } from "/@/utils/auth";
 
@@ -123,39 +123,40 @@ export default {
 								}
 
 								case 'toolcall': {
-									try {
-										const tools = JSON.parse(data);
-										const toolcallResponse: ChatResponse = {
-											type: 'toolcall',
-											request: {
-												name: tools["name"],
-												data: tools["arguments"]
-											}
-										};
-										onReceive(toolcallResponse);
-									} catch (error) {
-										console.error('解析toolcall数据失败:', error, '原始数据:', data);
-									}
+									const tools = JSON.parse(data);
+									const toolcallResponse: ChatResponse = {
+										type: 'toolcall',
+										request: {
+											name: tools["name"],
+											data: tools["arguments"]
+										}
+									};
+									onReceive(toolcallResponse);
 									break;
 								}
 
 								case 'toolres': {
-									try {
-										const tools = JSON.parse(data);
-										const toolresResponse: ChatResponse = {
-											type: 'toolres',
-											response: {
-												name: tools["name"],
-												data: tools["response"]
-											},
-										};
-										onReceive(toolresResponse);
-									} catch (error) {
-										console.error('解析toolres数据失败:', error, '原始数据:', data);
-									}
+									const tools = JSON.parse(data);
+									const toolresResponse: ChatResponse = {
+										type: 'toolres',
+										response: {
+											name: tools["name"],
+											data: tools["response"]
+										},
+									};
+									onReceive(toolresResponse);
 									break;
 								}
 
+								case 'error': {
+									const errorResponse: ErrorResponse = {
+										type: 'error',
+										error: data
+									}
+									onReceive(errorResponse)
+									break
+								}
+
 								default: {
 									// 如果没有明确的事件类型,默认作为消息处理
 									const defaultResponse: ChatResponse = {

+ 268 - 97
src/components/markdown/plugins/impl/ToolsLoadingCard.vue

@@ -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>

+ 14 - 4
src/views/assistant/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, nextTick, onMounted, computed, onUnmounted, reactive } from 'vue'
+import { ref, nextTick, onMounted, computed, onUnmounted, reactive, watch } from 'vue'
 import { Local } from '/@/utils/storage'
 import { User, ChatDotRound, Delete, Edit, Check, Close } from '@element-plus/icons-vue'
 import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
@@ -121,21 +121,30 @@ const sendMessage = () => {
 				case 'toolcall':
 					rtn.content += `
 \`\`\`tools-loading
+request
 ${resp.request.name}
 ${resp.request.data.replace('\n', '')}
-					`
+\`\`\`
+
+`
 					break
 				case 'toolres':
 					rtn.content += `
-${resp.response.data}
+\`\`\`tools-loading
+resp
+${resp.response.name}
+${resp.response.data.replace('\n', '')}
 \`\`\`
+
 `
 					break
 			}
 		},
 		onComplete: (e) => {
 			if (e !== undefined) {
-				rtn.content += ''
+				rtn.content += `
+
+				`
 			}
 			chatInstance.value = undefined
 		},
@@ -454,6 +463,7 @@ const getUserInfos = ref<{
 							resize="none"
 							@keydown.enter.ctrl="sendMessage"
 							@keydown.enter.meta="sendMessage"
+							:disabled="isConversationActive"
 						/>
 					</div>