feat: 增强工具调用结果组件,新增 grep 工具支持,优化工具图标映射和渲染逻辑
This commit is contained in:
parent
abee1fcc9b
commit
61d4bbf9ea
@ -48,6 +48,7 @@
|
||||
### 修复
|
||||
|
||||
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
||||
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
|
||||
|
||||
<!-- 添加到这里 -->
|
||||
|
||||
|
||||
@ -31,11 +31,50 @@
|
||||
flex-shrink: 0;
|
||||
|
||||
.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 {
|
||||
|
||||
@ -155,7 +155,6 @@
|
||||
:is-loading="isProcessing"
|
||||
:disabled="!currentAgent"
|
||||
:send-button-disabled="isSendButtonDisabled"
|
||||
placeholder="输入问题..."
|
||||
:mention="mentionConfig"
|
||||
:supports-file-upload="supportsFileUpload"
|
||||
:is-panel-open="isAgentPanelOpen"
|
||||
|
||||
@ -63,7 +63,6 @@ const props = defineProps({
|
||||
isLoading: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
sendButtonDisabled: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '输入问题...' },
|
||||
mention: { type: Object, default: () => null },
|
||||
supportsFileUpload: { type: Boolean, default: false },
|
||||
isPanelOpen: { type: Boolean, default: false },
|
||||
@ -80,6 +79,7 @@ const emit = defineEmits([
|
||||
|
||||
const inputRef = ref(null)
|
||||
const currentImage = ref(null)
|
||||
const placeholder = '问点什么?使用 @ 可以提及哦~'
|
||||
|
||||
const updateValue = (val) => {
|
||||
emit('update:modelValue', val)
|
||||
|
||||
@ -141,7 +141,7 @@
|
||||
</button>
|
||||
</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">
|
||||
<div class="image-preview-wrapper">
|
||||
<img
|
||||
@ -168,6 +168,7 @@
|
||||
</template>
|
||||
<template v-else-if="isMarkdown">
|
||||
<MdPreview
|
||||
class="flat-md-preview "
|
||||
:modelValue="formatContent(currentFile?.content)"
|
||||
:theme="theme"
|
||||
previewTheme="github"
|
||||
@ -258,7 +259,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file-content flat-md-preview">
|
||||
<div class="file-content">
|
||||
<template v-if="currentFile?.previewType === 'image' && currentFile?.previewUrl">
|
||||
<div class="image-preview-wrapper">
|
||||
<img :src="currentFile.previewUrl" :alt="currentFilePath" class="image-preview" />
|
||||
@ -277,6 +278,7 @@
|
||||
</template>
|
||||
<template v-else-if="isMarkdown">
|
||||
<MdPreview
|
||||
class="flat-md-preview"
|
||||
:modelValue="formatContent(currentFile?.content)"
|
||||
:theme="theme"
|
||||
previewTheme="github"
|
||||
@ -1297,6 +1299,10 @@ watch(useInlinePreview, (isInline) => {
|
||||
color: var(--gray-1000);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.flat-md-preview {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-content-pre.code-highlight {
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
-->
|
||||
|
||||
<!-- 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" />
|
||||
</span>
|
||||
<span v-else-if="toolCall.status === 'error'">
|
||||
@ -93,30 +93,10 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
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 { Loader, ChevronsUpDown, ChevronsDownUp, XCircle } from 'lucide-vue-next'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getToolCallId, getToolIcon } from './toolRegistry'
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
@ -143,47 +123,16 @@ const toggleExpand = () => {
|
||||
}
|
||||
|
||||
// Tool Name Logic
|
||||
const toolId = computed(() => getToolCallId(props.toolCall))
|
||||
|
||||
const toolName = computed(() => {
|
||||
const toolId = props.toolCall.name || props.toolCall.function?.name
|
||||
const toolsList = availableTools.value ? Object.values(availableTools.value) : []
|
||||
const tool = toolsList.find((t) => t.id === toolId)
|
||||
return tool ? tool.name : toolId
|
||||
const tool = toolsList.find((t) => t.id === toolId.value)
|
||||
return tool ? tool.name : toolId.value
|
||||
})
|
||||
|
||||
// Tool Icon Mapping
|
||||
const toolIcon = computed(() => {
|
||||
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
|
||||
})
|
||||
const toolIcon = computed(() => getToolIcon(toolId.value))
|
||||
|
||||
// Args Logic
|
||||
const formattedArgs = computed(() => {
|
||||
@ -197,7 +146,7 @@ const formattedArgs = computed(() => {
|
||||
} else if (typeof args === 'object' && args !== null) {
|
||||
return JSON.stringify(args, null, 2)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return args
|
||||
@ -223,7 +172,7 @@ const parsedResultData = computed(() => {
|
||||
if (typeof content === 'string') {
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,82 +1,10 @@
|
||||
<template>
|
||||
<!-- 知识图谱查询结果 -->
|
||||
<KnowledgeGraphTool v-if="isKnowledgeGraphResult" :tool-call="toolCall" ref="graphToolCallRef" />
|
||||
|
||||
<!-- 网页搜索 -->
|
||||
<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'"
|
||||
<component
|
||||
:is="currentRenderer"
|
||||
v-if="currentRenderer"
|
||||
: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" />
|
||||
</template>
|
||||
|
||||
@ -89,15 +17,14 @@ import ListKbsTool from './tools/ListKbsTool.vue'
|
||||
import GetMindmapTool from './tools/GetMindmapTool.vue'
|
||||
import QueryKbTool from './tools/QueryKbTool.vue'
|
||||
import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue'
|
||||
import ChartTool from './tools/ChartTool.vue'
|
||||
import CalculatorTool from './tools/CalculatorTool.vue'
|
||||
import TodoListTool from './tools/TodoListTool.vue'
|
||||
import ImageTool from './tools/ImageTool.vue'
|
||||
import TaskTool from './tools/TaskTool.vue'
|
||||
import WriteFileTool from './tools/WriteFileTool.vue'
|
||||
import ReadFileTool from './tools/ReadFileTool.vue'
|
||||
import ListDirectoryTool from './tools/ListDirectoryTool.vue'
|
||||
import SearchFileContentTool from './tools/SearchFileContentTool.vue'
|
||||
import GrepTool from './tools/GrepTool.vue'
|
||||
import GlobTool from './tools/GlobTool.vue'
|
||||
import EditFileTool from './tools/EditFileTool.vue'
|
||||
import MysqlQueryTool from './tools/MysqlQueryTool.vue'
|
||||
@ -105,6 +32,7 @@ import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue'
|
||||
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
|
||||
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
|
||||
import ExecuteTool from './tools/ExecuteTool.vue'
|
||||
import { getToolCallId } from './toolRegistry'
|
||||
|
||||
const props = defineProps({
|
||||
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) => {
|
||||
if (typeof content === 'string') {
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
return content
|
||||
}
|
||||
}
|
||||
return content
|
||||
const TOOL_RENDERERS = {
|
||||
ask_user_question: AskUserQuestionTool,
|
||||
bash: ExecuteTool,
|
||||
calculator: CalculatorTool,
|
||||
cmd: ExecuteTool,
|
||||
edit_file: EditFileTool,
|
||||
execute: ExecuteTool,
|
||||
get_mindmap: GetMindmapTool,
|
||||
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 isWebSearchResult = computed(() => {
|
||||
const name = toolName.value.toLowerCase()
|
||||
return name.includes('tavily_search')
|
||||
})
|
||||
const currentRenderer = computed(() => TOOL_RENDERERS[toolId.value] || null)
|
||||
|
||||
const isTaskResult = computed(() => {
|
||||
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 toolRendererRef = ref(null)
|
||||
const refreshGraph = () => {
|
||||
if (graphToolCallRef.value && typeof graphToolCallRef.value.refreshGraph === 'function') {
|
||||
graphToolCallRef.value.refreshGraph()
|
||||
if (toolRendererRef.value && typeof toolRendererRef.value.refreshGraph === 'function') {
|
||||
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)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div class="item-details item-details-inline">
|
||||
@ -140,6 +145,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Search, Wrench, Tag, Tags, FileText, List } from 'lucide-vue-next'
|
||||
import { toolApi } from '@/apis/tool_api'
|
||||
import { getToolIcon } from '@/components/ToolCallingResult/toolRegistry'
|
||||
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
@ -192,7 +198,7 @@ const fetchTools = async () => {
|
||||
if (!currentTool.value && tools.value.length > 0) {
|
||||
currentTool.value = tools.value[0]
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
message.error('加载工具失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
Loading…
Reference in New Issue
Block a user