mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
refactor(tools): streamline tool selection and enhance unknown tool handling
- Introduced a new utility function to determine if a tool is an agent tool, simplifying the tool selection logic in MessageTool. - Refactored MessageAgentTools to improve rendering logic and added an UnknownToolRenderer for better handling of unrecognized tools. - Updated BashOutputTool to remove unnecessary Card components, enhancing layout consistency. - Improved overall code clarity and maintainability by reducing redundancy and adhering to existing patterns.
This commit is contained in:
parent
965d7d3008
commit
e95219f2ec
@ -1,4 +1,4 @@
|
||||
import { AccordionItem, Card, CardBody, Chip, Code } from '@heroui/react'
|
||||
import { AccordionItem, Chip, Code } from '@heroui/react'
|
||||
import { CheckCircle, Terminal, XCircle } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@ -132,57 +132,43 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<Card
|
||||
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
|
||||
shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<Card
|
||||
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
|
||||
shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{output}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</AccordionItem>
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { Wrench } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
|
||||
interface UnknownToolProps {
|
||||
toolName: string
|
||||
input?: unknown
|
||||
output?: unknown
|
||||
}
|
||||
|
||||
export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToolProps) {
|
||||
const { highlightCode } = useCodeStyle()
|
||||
const [inputHtml, setInputHtml] = useState<string>('')
|
||||
const [outputHtml, setOutputHtml] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
if (input !== undefined) {
|
||||
const inputStr = JSON.stringify(input, null, 2)
|
||||
highlightCode(inputStr, 'json').then(setInputHtml)
|
||||
}
|
||||
}, [input, highlightCode])
|
||||
|
||||
useEffect(() => {
|
||||
if (output !== undefined) {
|
||||
const outputStr = JSON.stringify(output, null, 2)
|
||||
highlightCode(outputStr, 'json').then(setOutputHtml)
|
||||
}
|
||||
}, [output, highlightCode])
|
||||
|
||||
const getToolDisplayName = (name: string) => {
|
||||
if (name.startsWith('mcp__')) {
|
||||
const parts = name.substring(5).split('__')
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0]}:${parts.slice(1).join(':')}`
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
const getToolDescription = () => {
|
||||
if (toolName.startsWith('mcp__')) {
|
||||
return 'MCP Server Tool'
|
||||
}
|
||||
return 'Tool'
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="unknown-tool"
|
||||
aria-label={toolName}
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Wrench className="h-4 w-4" />}
|
||||
label={getToolDisplayName(toolName)}
|
||||
params={getToolDescription()}
|
||||
/>
|
||||
}>
|
||||
<div className="space-y-3">
|
||||
{input !== undefined && (
|
||||
<div>
|
||||
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Input:</div>
|
||||
<div
|
||||
className="overflow-x-auto rounded bg-gray-50 dark:bg-gray-900"
|
||||
dangerouslySetInnerHTML={{ __html: inputHtml }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output !== undefined && (
|
||||
<div>
|
||||
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Output:</div>
|
||||
<div
|
||||
className="rounded bg-gray-50 dark:bg-gray-900 [&>*]:whitespace-pre-line"
|
||||
dangerouslySetInnerHTML={{ __html: outputHtml }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{input === undefined && output === undefined && (
|
||||
<div className="text-foreground-500 text-xs">No data available for this tool</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -19,10 +19,10 @@ import { SearchTool } from './SearchTool'
|
||||
import { TaskTool } from './TaskTool'
|
||||
import { TodoWriteTool } from './TodoWriteTool'
|
||||
import { AgentToolsType, ToolInput, ToolOutput } from './types'
|
||||
import { UnknownToolRenderer } from './UnknownToolRenderer'
|
||||
import { WebFetchTool } from './WebFetchTool'
|
||||
import { WebSearchTool } from './WebSearchTool'
|
||||
import { WriteTool } from './WriteTool'
|
||||
|
||||
const logger = loggerService.withContext('MessageAgentTools')
|
||||
|
||||
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
||||
@ -54,20 +54,24 @@ function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?:
|
||||
const Renderer = toolRenderers[toolName]
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
className="w-max max-w-full"
|
||||
itemClasses={{
|
||||
trigger:
|
||||
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
|
||||
indicator: 'flex-shrink-0',
|
||||
subtitle: 'text-xs',
|
||||
content:
|
||||
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
|
||||
}}
|
||||
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||
{/* <Renderer input={input as any} output={output as any} /> */}
|
||||
{Renderer({ input: input as any, output: output as any })}
|
||||
</Accordion>
|
||||
<div className="w-max max-w-full rounded-md bg-foreground-100 py-1 transition-all duration-300 ease-in-out dark:bg-foreground-100">
|
||||
<Accordion
|
||||
className="w-max max-w-full"
|
||||
itemClasses={{
|
||||
trigger:
|
||||
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
|
||||
indicator: 'flex-shrink-0',
|
||||
subtitle: 'text-xs',
|
||||
content:
|
||||
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll',
|
||||
base: 'space-y-1'
|
||||
}}
|
||||
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||
{Renderer
|
||||
? Renderer({ input: input as any, output: output as any })
|
||||
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })}
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -80,17 +84,5 @@ export function MessageAgentTools({ toolResponse }: { toolResponse: NormalToolRe
|
||||
response
|
||||
})
|
||||
|
||||
// 使用类型守卫确保类型安全
|
||||
if (!isValidAgentToolsType(tool?.name)) {
|
||||
logger.warn('Invalid tool name received', { toolName: tool?.name })
|
||||
return (
|
||||
<div className="rounded-lg bg-red-50 p-3 dark:bg-red-900/20">
|
||||
<div className="text-red-600 text-sm dark:text-red-400">Invalid tool name: {tool?.name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const toolName = tool.name
|
||||
|
||||
return renderToolContent(toolName, args as ToolInput, response as ToolOutput)
|
||||
return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput)
|
||||
}
|
||||
|
||||
@ -10,39 +10,48 @@ interface Props {
|
||||
block: ToolMessageBlock
|
||||
}
|
||||
const prefix = 'builtin_'
|
||||
const agentPrefix = 'mcp__'
|
||||
const agentTools = [
|
||||
'Read',
|
||||
'Task',
|
||||
'Bash',
|
||||
'Search',
|
||||
'Glob',
|
||||
'TodoWrite',
|
||||
'WebSearch',
|
||||
'Grep',
|
||||
'Write',
|
||||
'WebFetch',
|
||||
'Edit',
|
||||
'MultiEdit',
|
||||
'BashOutput',
|
||||
'NotebookEdit',
|
||||
'ExitPlanMode'
|
||||
]
|
||||
const isAgentTool = (toolName: string) => {
|
||||
if (agentTools.includes(toolName) || toolName.startsWith(agentPrefix)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
|
||||
let toolName = toolResponse.tool.name
|
||||
if (toolName.startsWith(prefix)) {
|
||||
toolName = toolName.slice(prefix.length)
|
||||
}
|
||||
|
||||
switch (toolName) {
|
||||
case 'web_search':
|
||||
case 'web_search_preview':
|
||||
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
|
||||
case 'knowledge_search':
|
||||
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
|
||||
case 'memory_search':
|
||||
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
|
||||
case 'Read':
|
||||
case 'Task':
|
||||
case 'Bash':
|
||||
case 'Search':
|
||||
case 'Glob':
|
||||
case 'TodoWrite':
|
||||
case 'WebSearch':
|
||||
case 'Grep':
|
||||
case 'Write':
|
||||
case 'WebFetch':
|
||||
case 'Edit':
|
||||
case 'MultiEdit':
|
||||
case 'BashOutput':
|
||||
case 'NotebookEdit':
|
||||
case 'ExitPlanMode':
|
||||
return <MessageAgentTools toolResponse={toolResponse} />
|
||||
default:
|
||||
return null
|
||||
switch (toolName) {
|
||||
case 'web_search':
|
||||
case 'web_search_preview':
|
||||
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
|
||||
case 'knowledge_search':
|
||||
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
|
||||
case 'memory_search':
|
||||
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else if (isAgentTool(toolName)) {
|
||||
return <MessageAgentTools toolResponse={toolResponse} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user