mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 14:59:27 +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 { CheckCircle, Terminal, XCircle } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
@ -132,57 +132,43 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
|
|||||||
|
|
||||||
{/* Standard Output */}
|
{/* Standard Output */}
|
||||||
{parsedOutput.stdout && (
|
{parsedOutput.stdout && (
|
||||||
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
|
<div>
|
||||||
<CardBody className="p-3">
|
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</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">
|
||||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
{parsedOutput.stdout}
|
||||||
{parsedOutput.stdout}
|
</pre>
|
||||||
</pre>
|
</div>
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Standard Error */}
|
{/* Standard Error */}
|
||||||
{parsedOutput.stderr && (
|
{parsedOutput.stderr && (
|
||||||
<Card
|
<div className="border border-danger-200">
|
||||||
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
|
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||||
shadow="none">
|
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||||
<CardBody className="p-3">
|
{parsedOutput.stderr}
|
||||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
</pre>
|
||||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
</div>
|
||||||
{parsedOutput.stderr}
|
|
||||||
</pre>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tool Use Error */}
|
{/* Tool Use Error */}
|
||||||
{parsedOutput.tool_use_error && (
|
{parsedOutput.tool_use_error && (
|
||||||
<Card
|
<div className="border border-danger-200">
|
||||||
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
|
<div className="mb-2 flex items-center gap-2">
|
||||||
shadow="none">
|
<XCircle className="h-4 w-4 text-danger" />
|
||||||
<CardBody className="p-3">
|
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
</div>
|
||||||
<XCircle className="h-4 w-4 text-danger" />
|
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
{parsedOutput.tool_use_error}
|
||||||
</div>
|
</pre>
|
||||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
</div>
|
||||||
{parsedOutput.tool_use_error}
|
|
||||||
</pre>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// 原始输出(如果解析失败或非 XML 格式)
|
// 原始输出(如果解析失败或非 XML 格式)
|
||||||
output && (
|
output && (
|
||||||
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
|
<div>
|
||||||
<CardBody className="p-3">
|
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
</div>
|
||||||
{output}
|
|
||||||
</pre>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</AccordionItem>
|
</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 { TaskTool } from './TaskTool'
|
||||||
import { TodoWriteTool } from './TodoWriteTool'
|
import { TodoWriteTool } from './TodoWriteTool'
|
||||||
import { AgentToolsType, ToolInput, ToolOutput } from './types'
|
import { AgentToolsType, ToolInput, ToolOutput } from './types'
|
||||||
|
import { UnknownToolRenderer } from './UnknownToolRenderer'
|
||||||
import { WebFetchTool } from './WebFetchTool'
|
import { WebFetchTool } from './WebFetchTool'
|
||||||
import { WebSearchTool } from './WebSearchTool'
|
import { WebSearchTool } from './WebSearchTool'
|
||||||
import { WriteTool } from './WriteTool'
|
import { WriteTool } from './WriteTool'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MessageAgentTools')
|
const logger = loggerService.withContext('MessageAgentTools')
|
||||||
|
|
||||||
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
||||||
@ -54,20 +54,24 @@ function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?:
|
|||||||
const Renderer = toolRenderers[toolName]
|
const Renderer = toolRenderers[toolName]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
className="w-max max-w-full"
|
<Accordion
|
||||||
itemClasses={{
|
className="w-max max-w-full"
|
||||||
trigger:
|
itemClasses={{
|
||||||
'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',
|
trigger:
|
||||||
indicator: 'flex-shrink-0',
|
'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',
|
||||||
subtitle: 'text-xs',
|
indicator: 'flex-shrink-0',
|
||||||
content:
|
subtitle: 'text-xs',
|
||||||
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
|
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] : []}>
|
base: 'space-y-1'
|
||||||
{/* <Renderer input={input as any} output={output as any} /> */}
|
}}
|
||||||
{Renderer({ input: input as any, output: output as any })}
|
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||||
</Accordion>
|
{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
|
response
|
||||||
})
|
})
|
||||||
|
|
||||||
// 使用类型守卫确保类型安全
|
return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,39 +10,48 @@ interface Props {
|
|||||||
block: ToolMessageBlock
|
block: ToolMessageBlock
|
||||||
}
|
}
|
||||||
const prefix = 'builtin_'
|
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 => {
|
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
|
||||||
let toolName = toolResponse.tool.name
|
let toolName = toolResponse.tool.name
|
||||||
if (toolName.startsWith(prefix)) {
|
if (toolName.startsWith(prefix)) {
|
||||||
toolName = toolName.slice(prefix.length)
|
toolName = toolName.slice(prefix.length)
|
||||||
}
|
switch (toolName) {
|
||||||
|
case 'web_search':
|
||||||
switch (toolName) {
|
case 'web_search_preview':
|
||||||
case 'web_search':
|
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
|
||||||
case 'web_search_preview':
|
case 'knowledge_search':
|
||||||
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
|
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
|
||||||
case 'knowledge_search':
|
case 'memory_search':
|
||||||
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
|
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
|
||||||
case 'memory_search':
|
default:
|
||||||
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
|
return null
|
||||||
case 'Read':
|
}
|
||||||
case 'Task':
|
} else if (isAgentTool(toolName)) {
|
||||||
case 'Bash':
|
return <MessageAgentTools toolResponse={toolResponse} />
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user