feat: 增强工具调用结果组件,新增 grep 工具支持,优化工具图标映射和渲染逻辑
This commit is contained in:
parent
abee1fcc9b
commit
61d4bbf9ea
@ -48,6 +48,7 @@
|
|||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
||||||
|
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
|
||||||
|
|
||||||
<!-- 添加到这里 -->
|
<!-- 添加到这里 -->
|
||||||
|
|
||||||
|
|||||||
@ -31,11 +31,50 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
padding: 12px 12px 0;
|
padding: 8px 12px 0;
|
||||||
|
|
||||||
|
:deep(.ant-input-affix-wrapper) {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
// border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--gray-0);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&.ant-input-affix-wrapper-focused {
|
||||||
|
// border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input-prefix) {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input) {
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input::placeholder) {
|
||||||
|
color: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input-clear-icon) {
|
||||||
|
color: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-input-clear-icon:hover) {
|
||||||
|
color: var(--gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :deep(.ant-input-outlined) {
|
||||||
|
// border: none;
|
||||||
|
// }
|
||||||
|
|
||||||
.search-input :deep(.ant-input) {
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-container {
|
.list-container {
|
||||||
|
|||||||
@ -155,7 +155,6 @@
|
|||||||
:is-loading="isProcessing"
|
:is-loading="isProcessing"
|
||||||
:disabled="!currentAgent"
|
:disabled="!currentAgent"
|
||||||
:send-button-disabled="isSendButtonDisabled"
|
:send-button-disabled="isSendButtonDisabled"
|
||||||
placeholder="输入问题..."
|
|
||||||
:mention="mentionConfig"
|
:mention="mentionConfig"
|
||||||
:supports-file-upload="supportsFileUpload"
|
:supports-file-upload="supportsFileUpload"
|
||||||
:is-panel-open="isAgentPanelOpen"
|
:is-panel-open="isAgentPanelOpen"
|
||||||
|
|||||||
@ -63,7 +63,6 @@ const props = defineProps({
|
|||||||
isLoading: { type: Boolean, default: false },
|
isLoading: { type: Boolean, default: false },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
sendButtonDisabled: { type: Boolean, default: false },
|
sendButtonDisabled: { type: Boolean, default: false },
|
||||||
placeholder: { type: String, default: '输入问题...' },
|
|
||||||
mention: { type: Object, default: () => null },
|
mention: { type: Object, default: () => null },
|
||||||
supportsFileUpload: { type: Boolean, default: false },
|
supportsFileUpload: { type: Boolean, default: false },
|
||||||
isPanelOpen: { type: Boolean, default: false },
|
isPanelOpen: { type: Boolean, default: false },
|
||||||
@ -80,6 +79,7 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
const inputRef = ref(null)
|
const inputRef = ref(null)
|
||||||
const currentImage = ref(null)
|
const currentImage = ref(null)
|
||||||
|
const placeholder = '问点什么?使用 @ 可以提及哦~'
|
||||||
|
|
||||||
const updateValue = (val) => {
|
const updateValue = (val) => {
|
||||||
emit('update:modelValue', val)
|
emit('update:modelValue', val)
|
||||||
|
|||||||
@ -141,7 +141,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-content flat-md-preview inline-file-content">
|
<div class="file-content inline-file-content">
|
||||||
<template v-if="currentFile?.previewType === 'image' && currentFile?.previewUrl">
|
<template v-if="currentFile?.previewType === 'image' && currentFile?.previewUrl">
|
||||||
<div class="image-preview-wrapper">
|
<div class="image-preview-wrapper">
|
||||||
<img
|
<img
|
||||||
@ -168,6 +168,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="isMarkdown">
|
<template v-else-if="isMarkdown">
|
||||||
<MdPreview
|
<MdPreview
|
||||||
|
class="flat-md-preview "
|
||||||
:modelValue="formatContent(currentFile?.content)"
|
:modelValue="formatContent(currentFile?.content)"
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
previewTheme="github"
|
previewTheme="github"
|
||||||
@ -258,7 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="file-content flat-md-preview">
|
<div class="file-content">
|
||||||
<template v-if="currentFile?.previewType === 'image' && currentFile?.previewUrl">
|
<template v-if="currentFile?.previewType === 'image' && currentFile?.previewUrl">
|
||||||
<div class="image-preview-wrapper">
|
<div class="image-preview-wrapper">
|
||||||
<img :src="currentFile.previewUrl" :alt="currentFilePath" class="image-preview" />
|
<img :src="currentFile.previewUrl" :alt="currentFilePath" class="image-preview" />
|
||||||
@ -277,6 +278,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="isMarkdown">
|
<template v-else-if="isMarkdown">
|
||||||
<MdPreview
|
<MdPreview
|
||||||
|
class="flat-md-preview"
|
||||||
:modelValue="formatContent(currentFile?.content)"
|
:modelValue="formatContent(currentFile?.content)"
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
previewTheme="github"
|
previewTheme="github"
|
||||||
@ -1297,6 +1299,10 @@ watch(useInlinePreview, (isInline) => {
|
|||||||
color: var(--gray-1000);
|
color: var(--gray-1000);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flat-md-preview {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-content-pre.code-highlight {
|
.file-content-pre.code-highlight {
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Fixed Status Icon -->
|
<!-- Fixed Status Icon -->
|
||||||
<span v-if="toolCall.status === 'success' || toolCall.tool_call_result">
|
<span v-if="(toolCall.status === 'success' || toolCall.tool_call_result) && toolIcon">
|
||||||
<component :is="toolIcon" size="16" class="tool-loader tool-success" />
|
<component :is="toolIcon" size="16" class="tool-loader tool-success" />
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="toolCall.status === 'error'">
|
<span v-else-if="toolCall.status === 'error'">
|
||||||
@ -93,30 +93,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import {
|
import { Loader, ChevronsUpDown, ChevronsDownUp, XCircle } from 'lucide-vue-next'
|
||||||
Loader,
|
|
||||||
CircleCheckBig,
|
|
||||||
ChevronsUpDown,
|
|
||||||
ChevronsDownUp,
|
|
||||||
FileText,
|
|
||||||
FileEdit,
|
|
||||||
FilePen,
|
|
||||||
FolderSearch,
|
|
||||||
Folder,
|
|
||||||
Database,
|
|
||||||
BookOpen,
|
|
||||||
Globe,
|
|
||||||
BarChart3,
|
|
||||||
Image,
|
|
||||||
Calculator,
|
|
||||||
CheckSquare,
|
|
||||||
Wrench,
|
|
||||||
XCircle,
|
|
||||||
HelpCircle,
|
|
||||||
Terminal
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
import { useAgentStore } from '@/stores/agent'
|
import { useAgentStore } from '@/stores/agent'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { getToolCallId, getToolIcon } from './toolRegistry'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
toolCall: {
|
toolCall: {
|
||||||
@ -143,47 +123,16 @@ const toggleExpand = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tool Name Logic
|
// Tool Name Logic
|
||||||
|
const toolId = computed(() => getToolCallId(props.toolCall))
|
||||||
|
|
||||||
const toolName = computed(() => {
|
const toolName = computed(() => {
|
||||||
const toolId = props.toolCall.name || props.toolCall.function?.name
|
|
||||||
const toolsList = availableTools.value ? Object.values(availableTools.value) : []
|
const toolsList = availableTools.value ? Object.values(availableTools.value) : []
|
||||||
const tool = toolsList.find((t) => t.id === toolId)
|
const tool = toolsList.find((t) => t.id === toolId.value)
|
||||||
return tool ? tool.name : toolId
|
return tool ? tool.name : toolId.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Tool Icon Mapping
|
// Tool Icon Mapping
|
||||||
const toolIcon = computed(() => {
|
const toolIcon = computed(() => getToolIcon(toolId.value))
|
||||||
const name = toolName.value.toLowerCase()
|
|
||||||
// 文件操作
|
|
||||||
if (name.includes('read_file') || name.includes('file')) return FileText
|
|
||||||
if (name.includes('write_file')) return FileEdit
|
|
||||||
if (name.includes('edit_file') || name.includes('replace')) return FilePen
|
|
||||||
if (name.includes('glob') || name.includes('search_file')) return FolderSearch
|
|
||||||
if (name.includes('list_directory') || name.includes('ls')) return Folder
|
|
||||||
// 数据库
|
|
||||||
if (name.includes('mysql')) return Database
|
|
||||||
// 知识库/搜索
|
|
||||||
if (name.includes('kb') || name.includes('knowledge')) return BookOpen
|
|
||||||
if (name.includes('web_search') || name.includes('tavily')) return Globe
|
|
||||||
// 图表/图像
|
|
||||||
if (name.includes('chart')) return BarChart3
|
|
||||||
if (name.includes('image') || name.includes('img')) return Image
|
|
||||||
// 计算
|
|
||||||
if (name.includes('calc') || name.includes('math')) return Calculator
|
|
||||||
// 任务
|
|
||||||
if (name.includes('task') || name.includes('todo')) return CheckSquare
|
|
||||||
// 命令
|
|
||||||
if (
|
|
||||||
name.includes('execute') ||
|
|
||||||
name.includes('cmd') ||
|
|
||||||
name.includes('shell') ||
|
|
||||||
name.includes('bash')
|
|
||||||
)
|
|
||||||
return Terminal
|
|
||||||
// 向用户提问
|
|
||||||
if (name.includes('ask_user_question') || name.includes('question')) return HelpCircle
|
|
||||||
// 默认
|
|
||||||
return Wrench
|
|
||||||
})
|
|
||||||
|
|
||||||
// Args Logic
|
// Args Logic
|
||||||
const formattedArgs = computed(() => {
|
const formattedArgs = computed(() => {
|
||||||
@ -197,7 +146,7 @@ const formattedArgs = computed(() => {
|
|||||||
} else if (typeof args === 'object' && args !== null) {
|
} else if (typeof args === 'object' && args !== null) {
|
||||||
return JSON.stringify(args, null, 2)
|
return JSON.stringify(args, null, 2)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
@ -223,7 +172,7 @@ const parsedResultData = computed(() => {
|
|||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(content)
|
return JSON.parse(content)
|
||||||
} catch (error) {
|
} catch {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,82 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 知识图谱查询结果 -->
|
<component
|
||||||
<KnowledgeGraphTool v-if="isKnowledgeGraphResult" :tool-call="toolCall" ref="graphToolCallRef" />
|
:is="currentRenderer"
|
||||||
|
v-if="currentRenderer"
|
||||||
<!-- 网页搜索 -->
|
|
||||||
<WebSearchTool v-else-if="isWebSearchResult" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- Chart -->
|
|
||||||
<ChartTool v-else-if="isChartResult" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 知识库列表 -->
|
|
||||||
<ListKbsTool v-else-if="toolName === 'list_kbs'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 思维导图 -->
|
|
||||||
<GetMindmapTool v-else-if="toolName === 'get_mindmap'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 知识库检索 -->
|
|
||||||
<QueryKbTool v-else-if="toolName === 'query_kb'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 待办事项 -->
|
|
||||||
<TodoListTool v-else-if="toolName === 'write_todos'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 计算器 -->
|
|
||||||
<CalculatorTool v-else-if="isCalculatorResult" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 图片 -->
|
|
||||||
<ImageTool v-else-if="isImageResult" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 任务分配 -->
|
|
||||||
<TaskTool v-else-if="isTaskResult" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 写文件 -->
|
|
||||||
<WriteFileTool v-else-if="toolName === 'write_file'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 读文件 -->
|
|
||||||
<ReadFileTool v-else-if="toolName === 'read_file'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 列目录 -->
|
|
||||||
<ListDirectoryTool
|
|
||||||
v-else-if="toolName === 'list_directory' || toolName === 'ls'"
|
|
||||||
:tool-call="toolCall"
|
:tool-call="toolCall"
|
||||||
|
ref="toolRendererRef"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 搜索文件内容 -->
|
|
||||||
<SearchFileContentTool v-else-if="toolName === 'search_file_content'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- Glob 搜索 -->
|
|
||||||
<GlobTool v-else-if="toolName === 'glob'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 编辑文件 -->
|
|
||||||
<EditFileTool
|
|
||||||
v-else-if="toolName === 'edit_file' || toolName === 'replace'"
|
|
||||||
:tool-call="toolCall"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- MySQL 查询 -->
|
|
||||||
<MysqlQueryTool v-else-if="toolName === 'mysql_query'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- MySQL 描述表 -->
|
|
||||||
<MysqlDescribeTableTool v-else-if="toolName === 'mysql_describe_table'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- MySQL 列出表 -->
|
|
||||||
<MysqlListTablesTool v-else-if="toolName === 'mysql_list_tables'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 向用户提问 -->
|
|
||||||
<AskUserQuestionTool v-else-if="toolName === 'ask_user_question'" :tool-call="toolCall" />
|
|
||||||
|
|
||||||
<!-- 执行命令 -->
|
|
||||||
<ExecuteTool
|
|
||||||
v-else-if="
|
|
||||||
toolName === 'execute' ||
|
|
||||||
toolName === 'run_shell_command' ||
|
|
||||||
toolName === 'bash' ||
|
|
||||||
toolName === 'cmd'
|
|
||||||
"
|
|
||||||
:tool-call="toolCall"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 默认展示 -->
|
|
||||||
<BaseToolCall v-else :tool-call="toolCall" />
|
<BaseToolCall v-else :tool-call="toolCall" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -89,15 +17,14 @@ import ListKbsTool from './tools/ListKbsTool.vue'
|
|||||||
import GetMindmapTool from './tools/GetMindmapTool.vue'
|
import GetMindmapTool from './tools/GetMindmapTool.vue'
|
||||||
import QueryKbTool from './tools/QueryKbTool.vue'
|
import QueryKbTool from './tools/QueryKbTool.vue'
|
||||||
import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue'
|
import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue'
|
||||||
import ChartTool from './tools/ChartTool.vue'
|
|
||||||
import CalculatorTool from './tools/CalculatorTool.vue'
|
import CalculatorTool from './tools/CalculatorTool.vue'
|
||||||
import TodoListTool from './tools/TodoListTool.vue'
|
import TodoListTool from './tools/TodoListTool.vue'
|
||||||
import ImageTool from './tools/ImageTool.vue'
|
import ImageTool from './tools/ImageTool.vue'
|
||||||
import TaskTool from './tools/TaskTool.vue'
|
|
||||||
import WriteFileTool from './tools/WriteFileTool.vue'
|
import WriteFileTool from './tools/WriteFileTool.vue'
|
||||||
import ReadFileTool from './tools/ReadFileTool.vue'
|
import ReadFileTool from './tools/ReadFileTool.vue'
|
||||||
import ListDirectoryTool from './tools/ListDirectoryTool.vue'
|
import ListDirectoryTool from './tools/ListDirectoryTool.vue'
|
||||||
import SearchFileContentTool from './tools/SearchFileContentTool.vue'
|
import SearchFileContentTool from './tools/SearchFileContentTool.vue'
|
||||||
|
import GrepTool from './tools/GrepTool.vue'
|
||||||
import GlobTool from './tools/GlobTool.vue'
|
import GlobTool from './tools/GlobTool.vue'
|
||||||
import EditFileTool from './tools/EditFileTool.vue'
|
import EditFileTool from './tools/EditFileTool.vue'
|
||||||
import MysqlQueryTool from './tools/MysqlQueryTool.vue'
|
import MysqlQueryTool from './tools/MysqlQueryTool.vue'
|
||||||
@ -105,6 +32,7 @@ import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue'
|
|||||||
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
|
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
|
||||||
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
|
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
|
||||||
import ExecuteTool from './tools/ExecuteTool.vue'
|
import ExecuteTool from './tools/ExecuteTool.vue'
|
||||||
|
import { getToolCallId } from './toolRegistry'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
toolCall: {
|
toolCall: {
|
||||||
@ -113,74 +41,42 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '')
|
const toolId = computed(() => getToolCallId(props.toolCall))
|
||||||
|
|
||||||
const parseData = (content) => {
|
const TOOL_RENDERERS = {
|
||||||
if (typeof content === 'string') {
|
ask_user_question: AskUserQuestionTool,
|
||||||
try {
|
bash: ExecuteTool,
|
||||||
return JSON.parse(content)
|
calculator: CalculatorTool,
|
||||||
} catch (error) {
|
cmd: ExecuteTool,
|
||||||
return content
|
edit_file: EditFileTool,
|
||||||
}
|
execute: ExecuteTool,
|
||||||
}
|
get_mindmap: GetMindmapTool,
|
||||||
return content
|
glob: GlobTool,
|
||||||
|
grep: GrepTool,
|
||||||
|
list_directory: ListDirectoryTool,
|
||||||
|
list_kbs: ListKbsTool,
|
||||||
|
ls: ListDirectoryTool,
|
||||||
|
mysql_describe_table: MysqlDescribeTableTool,
|
||||||
|
mysql_list_tables: MysqlListTablesTool,
|
||||||
|
mysql_query: MysqlQueryTool,
|
||||||
|
query_kb: QueryKbTool,
|
||||||
|
query_knowledge_graph: KnowledgeGraphTool,
|
||||||
|
read_file: ReadFileTool,
|
||||||
|
replace: EditFileTool,
|
||||||
|
run_shell_command: ExecuteTool,
|
||||||
|
search_file_content: SearchFileContentTool,
|
||||||
|
tavily_search: WebSearchTool,
|
||||||
|
text_to_img_qwen_image: ImageTool,
|
||||||
|
write_file: WriteFileTool,
|
||||||
|
write_todos: TodoListTool
|
||||||
}
|
}
|
||||||
|
|
||||||
// 识别逻辑
|
const currentRenderer = computed(() => TOOL_RENDERERS[toolId.value] || null)
|
||||||
const isWebSearchResult = computed(() => {
|
|
||||||
const name = toolName.value.toLowerCase()
|
|
||||||
return name.includes('tavily_search')
|
|
||||||
})
|
|
||||||
|
|
||||||
const isTaskResult = computed(() => {
|
const toolRendererRef = ref(null)
|
||||||
let args = props.toolCall.args || props.toolCall.function?.arguments
|
|
||||||
if (typeof args === 'string') {
|
|
||||||
try {
|
|
||||||
args = JSON.parse(args)
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return args && typeof args === 'object' && 'subagent_type' in args
|
|
||||||
})
|
|
||||||
|
|
||||||
const isKnowledgeGraphResult = computed(() => {
|
|
||||||
const name = toolName.value.toLowerCase()
|
|
||||||
const hasGraphKeyword = name.includes('graph') || name.includes('图谱') || name.includes('kg')
|
|
||||||
const data = parseData(props.toolCall.tool_call_result?.content)
|
|
||||||
const hasBasicStructure = data && typeof data === 'object'
|
|
||||||
const hasTriples = hasBasicStructure && 'triples' in data
|
|
||||||
return (
|
|
||||||
(hasTriples && Array.isArray(data.triples) && data.triples.length > 0) ||
|
|
||||||
(hasTriples && hasGraphKeyword)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isCalculatorResult = computed(() => {
|
|
||||||
const name = toolName.value.toLowerCase()
|
|
||||||
return name.includes('calculator') || name.includes('calc') || name.includes('math')
|
|
||||||
})
|
|
||||||
|
|
||||||
const isChartResult = computed(() => {
|
|
||||||
const name = toolName.value.toLowerCase()
|
|
||||||
if (!name.includes('chart')) return false
|
|
||||||
const data = parseData(props.toolCall.tool_call_result?.content)
|
|
||||||
// chart 返回数组格式 [{ type: "text", text: "url", id: "..." }]
|
|
||||||
return Array.isArray(data) && data.length > 0 && data[0].type === 'text'
|
|
||||||
})
|
|
||||||
|
|
||||||
const isImageResult = computed(() => {
|
|
||||||
const name = toolName.value.toLowerCase()
|
|
||||||
if (!name.includes('text_to_img')) return false
|
|
||||||
const data = parseData(props.toolCall.tool_call_result?.content)
|
|
||||||
return data && typeof data === 'string' && data.startsWith('http')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理知识图谱刷新
|
|
||||||
const graphToolCallRef = ref(null)
|
|
||||||
const refreshGraph = () => {
|
const refreshGraph = () => {
|
||||||
if (graphToolCallRef.value && typeof graphToolCallRef.value.refreshGraph === 'function') {
|
if (toolRendererRef.value && typeof toolRendererRef.value.refreshGraph === 'function') {
|
||||||
graphToolCallRef.value.refreshGraph()
|
toolRendererRef.value.refreshGraph()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
web/src/components/ToolCallingResult/toolRegistry.js
Normal file
48
web/src/components/ToolCallingResult/toolRegistry.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
Calculator,
|
||||||
|
CheckSquare,
|
||||||
|
Database,
|
||||||
|
FileEdit,
|
||||||
|
FilePen,
|
||||||
|
FileText,
|
||||||
|
Folder,
|
||||||
|
FolderSearch,
|
||||||
|
Globe,
|
||||||
|
HelpCircle,
|
||||||
|
Image,
|
||||||
|
Network,
|
||||||
|
Terminal
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
|
export const TOOL_ICON_MAP = {
|
||||||
|
ask_user_question: HelpCircle,
|
||||||
|
bash: Terminal,
|
||||||
|
calculator: Calculator,
|
||||||
|
cmd: Terminal,
|
||||||
|
edit_file: FilePen,
|
||||||
|
execute: Terminal,
|
||||||
|
get_mindmap: Network,
|
||||||
|
glob: FolderSearch,
|
||||||
|
grep: FolderSearch,
|
||||||
|
list_directory: Folder,
|
||||||
|
list_kbs: BookOpen,
|
||||||
|
ls: Folder,
|
||||||
|
mysql_describe_table: Database,
|
||||||
|
mysql_list_tables: Database,
|
||||||
|
mysql_query: Database,
|
||||||
|
query_kb: BookOpen,
|
||||||
|
query_knowledge_graph: Network,
|
||||||
|
read_file: FileText,
|
||||||
|
replace: FilePen,
|
||||||
|
run_shell_command: Terminal,
|
||||||
|
search_file_content: FolderSearch,
|
||||||
|
tavily_search: Globe,
|
||||||
|
text_to_img_qwen_image: Image,
|
||||||
|
write_file: FileEdit,
|
||||||
|
write_todos: CheckSquare
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getToolCallId = (toolCall) => toolCall?.name || toolCall?.function?.name || ''
|
||||||
|
|
||||||
|
export const getToolIcon = (toolId) => TOOL_ICON_MAP[toolId] || null
|
||||||
195
web/src/components/ToolCallingResult/tools/GrepTool.vue
Normal file
195
web/src/components/ToolCallingResult/tools/GrepTool.vue
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<BaseToolCall :tool-call="toolCall" :hide-params="true">
|
||||||
|
<template #header>
|
||||||
|
<div class="sep-header">
|
||||||
|
<span class="note">grep</span>
|
||||||
|
<span class="separator" v-if="pattern">|</span>
|
||||||
|
<span class="keywords">{{ pattern }}</span>
|
||||||
|
<span class="separator" v-if="targetPath">|</span>
|
||||||
|
<span class="description code">{{ targetPath }}</span>
|
||||||
|
<span class="tag" v-if="outputMode">{{ outputMode }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #result>
|
||||||
|
<div class="tool-result-renderer grep-result">
|
||||||
|
<div class="result-summary" v-if="matchCountLabel">
|
||||||
|
<span>{{ matchCountLabel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-list" v-if="isFileListResult">
|
||||||
|
<div v-for="file in fileMatches" :key="file" class="result-item">
|
||||||
|
<span class="path code">{{ file }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-list" v-else-if="lineMatches.length">
|
||||||
|
<div
|
||||||
|
v-for="match in lineMatches"
|
||||||
|
:key="`${match.path}-${match.line}-${match.text}`"
|
||||||
|
class="result-item"
|
||||||
|
>
|
||||||
|
<div class="result-meta">
|
||||||
|
<span class="path code">{{ match.path }}</span>
|
||||||
|
<span class="line-number" v-if="match.line">Line {{ match.line }}</span>
|
||||||
|
</div>
|
||||||
|
<pre class="matched-text">{{ match.text }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="empty-state" v-else-if="isEmptyResult">未找到匹配结果</div>
|
||||||
|
|
||||||
|
<div class="raw-result" v-else>
|
||||||
|
<pre>{{ rawResultText }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseToolCall>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import BaseToolCall from '../BaseToolCall.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
toolCall: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedArgs = computed(() => {
|
||||||
|
const args = props.toolCall.args || props.toolCall.function?.arguments
|
||||||
|
if (!args) return {}
|
||||||
|
if (typeof args === 'object') return args
|
||||||
|
try {
|
||||||
|
return JSON.parse(args)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedResult = computed(() => {
|
||||||
|
const content = props.toolCall.tool_call_result?.content
|
||||||
|
if (!content) return null
|
||||||
|
if (typeof content === 'object') return content
|
||||||
|
try {
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pattern = computed(() => parsedArgs.value.pattern || '')
|
||||||
|
const targetPath = computed(() => parsedArgs.value.path || parsedArgs.value.dir_path || '/')
|
||||||
|
const outputMode = computed(() => parsedArgs.value.output_mode || '')
|
||||||
|
|
||||||
|
const isFileListResult = computed(() => {
|
||||||
|
return Array.isArray(parsedResult.value) && parsedResult.value.every((item) => typeof item === 'string')
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileMatches = computed(() => {
|
||||||
|
return isFileListResult.value ? parsedResult.value : []
|
||||||
|
})
|
||||||
|
|
||||||
|
const lineMatches = computed(() => {
|
||||||
|
if (!Array.isArray(parsedResult.value)) return []
|
||||||
|
return parsedResult.value.filter(
|
||||||
|
(item) =>
|
||||||
|
item &&
|
||||||
|
typeof item === 'object' &&
|
||||||
|
typeof item.path === 'string' &&
|
||||||
|
('text' in item || 'line' in item)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEmptyResult = computed(() => {
|
||||||
|
return Array.isArray(parsedResult.value) && parsedResult.value.length === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const matchCountLabel = computed(() => {
|
||||||
|
if (isFileListResult.value) {
|
||||||
|
const count = fileMatches.value.length
|
||||||
|
return count ? `共匹配 ${count} 个文件` : ''
|
||||||
|
}
|
||||||
|
if (lineMatches.value.length) {
|
||||||
|
return `共匹配 ${lineMatches.value.length} 行`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawResultText = computed(() => {
|
||||||
|
if (typeof parsedResult.value === 'string') return parsedResult.value
|
||||||
|
return JSON.stringify(parsedResult.value, null, 2)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.grep-result {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--gray-0);
|
||||||
|
|
||||||
|
.result-summary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-600);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--gray-150);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--gray-25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
color: var(--gray-800);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--main-700);
|
||||||
|
background: var(--main-50);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matched-text,
|
||||||
|
.raw-result pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--gray-700);
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matched-text {
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--gray-0);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -42,7 +42,12 @@
|
|||||||
@click="selectTool(tool)"
|
@click="selectTool(tool)"
|
||||||
>
|
>
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<Wrench :size="16" class="item-icon" />
|
<component
|
||||||
|
:is="getToolIcon(tool.id)"
|
||||||
|
v-if="getToolIcon(tool.id)"
|
||||||
|
:size="16"
|
||||||
|
class="item-icon"
|
||||||
|
/>
|
||||||
<span class="item-name">{{ tool.name }}</span>
|
<span class="item-name">{{ tool.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-details item-details-inline">
|
<div class="item-details item-details-inline">
|
||||||
@ -140,6 +145,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { Search, Wrench, Tag, Tags, FileText, List } from 'lucide-vue-next'
|
import { Search, Wrench, Tag, Tags, FileText, List } from 'lucide-vue-next'
|
||||||
import { toolApi } from '@/apis/tool_api'
|
import { toolApi } from '@/apis/tool_api'
|
||||||
|
import { getToolIcon } from '@/components/ToolCallingResult/toolRegistry'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
@ -192,7 +198,7 @@ const fetchTools = async () => {
|
|||||||
if (!currentTool.value && tools.value.length > 0) {
|
if (!currentTool.value && tools.value.length > 0) {
|
||||||
currentTool.value = tools.value[0]
|
currentTool.value = tools.value[0]
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('加载工具失败')
|
message.error('加载工具失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user