mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 00:49:14 +08:00
refactor(Tools): replace MCPToolResponse with NormalToolResponse in m… (#10303)
* refactor(Tools): replace MCPToolResponse with NormalToolResponse in message tools and add new agent tools - Updated MessageKnowledgeSearch, MessageMemorySearch, and MessageWebSearch components to use NormalToolResponse. - Refactored MessageTool to handle NormalToolResponse and simplified tool rendering logic. - Introduced new agent tools: BashTool, GlobTool, GrepTool, ReadTool, SearchTool, TaskTool, and TodoWriteTool with corresponding types and renderers. - Enhanced type safety by updating tool response types in the codebase. * fix(i18n): Auto update translations for PR #10303 * chunk type * refactor(migration): renumber migration steps after removing step 155 Remove unused migration step 155 and renumber subsequent steps to maintain sequence * fix(store): prevent mutation of assistant presets by using spread operator * fix(store): ignore ts-2589 false positives and refactor preset updates Refactor assistant preset updates to use forEach instead of map for consistency Add ts-ignore comments for TypeScript false positives in store operations * Fix tool result handling and session creation flow - Populate toolName in tool-result chunks from contentBlockState - Add onSessionCreated callback to SessionModal for post-creation actions - Return created session from useSessions hook and update SWR cache optimistically * Fix toolName reference in ClaudeCode message transformation - Correctly reference toolName from contentBlockState using blockKey instead of block.tool_use_id - Ensure proper tool result chunk generation when handling assistant messages - Maintain consistent data structure for tool call processing * Fix toolName reference and add stream event logging - Correct toolName lookup to use tool_use_id instead of blockKey in tool-result chunks - Add debug logging for stream event handling - Update contentBlockState key to use event.content_block.id for tool_use events * Add debug logging for message content blocks - Log each content block when processing user or assistant messages - Maintain existing switch case logic for text block handling - Improve debugging visibility for multi-block message processing * get toolName * chore: bump version to 1.7.0-alpha.1 * fix(getSdkClient): add authToken to anthropic client initialization for claude code * Update transform.ts * Refactor logging levels in transform.ts and adjust JSON body parser configuration in app.ts * refactor(sessions): simplify session creation by removing modal and using direct button The SessionModal component was removed and replaced with a direct button click handler that creates a session using the agent data. Also added error handling to display an alert when session fetching fails. * feat(agents): add api server check and warning for agent features - Add api server enabled check in multiple components - Show warning alert when api server is disabled - Add dismissable warning in AgentSection - Disable send button when api server is disabled - Add iknow state to store dismissed warnings * feat(i18n): add warning message for enabling API server Add warning message in multiple languages to inform users they need to enable API server to use agent features * feat(sessions): make session creation async and set active session Dispatch active session id after successful creation to ensure UI reflects current state * feat(sessions): add session waiting state and improve deletion handling - Add sessionWaiting state to track updating/deleting sessions - Extract updateSession logic into separate hook - Improve session deletion with waiting state and fallback session selection - Disable session items during deletion to prevent duplicate actions * feat(i18n): add error message for last session deletion Add error message to prevent deletion of the last session in all supported languages * fix(i18n): Auto update translations for PR #10096 * fix(i18n): Auto update translations for PR #10096 * feat(tools): add WriteTool and update tool rendering logic - Introduced WriteTool for handling file writing operations. - Updated MessageAgentTools to include the new WriteTool in the tool renderers. - Refactored existing tools to streamline rendering and improve code clarity. - Enhanced BashTool and TaskTool to better display input and output information. --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: suyao <sy20010504@gmail.com> Co-authored-by: icarus <eurfelux@gmail.com> Co-authored-by: Vaayne <liu.vaayne@gmail.com>
This commit is contained in:
parent
c3adcf663f
commit
939782ac4e
@ -0,0 +1,30 @@
|
||||
import { AccordionItem, Code } from '@heroui/react'
|
||||
import { Terminal } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types'
|
||||
|
||||
export function BashTool({ input, output }: { input: BashToolInputType; output?: BashToolOutputType }) {
|
||||
// 如果有输出,计算输出行数
|
||||
const outputLines = output ? output.split('\n').length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Bash Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash"
|
||||
params={
|
||||
<Code size="sm" className="text-sm">
|
||||
{input.command}
|
||||
</Code>
|
||||
}
|
||||
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
// 通用工具组件 - 减少重复代码
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
// 生成 AccordionItem 的标题
|
||||
export function ToolTitle({
|
||||
icon,
|
||||
label,
|
||||
params,
|
||||
stats,
|
||||
className = 'text-sm'
|
||||
}: {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
params?: string | ReactNode
|
||||
stats?: string | ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
{icon}
|
||||
{label && <span className="font-medium">{label}</span>}
|
||||
{params && <span className="flex-shrink-0 text-muted-foreground text-sm">{params}</span>}
|
||||
{stats && <span className="flex-shrink-0 text-muted-foreground">{stats}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 纯字符串输入工具 (Task, Bash, Search)
|
||||
export function StringInputTool({
|
||||
input,
|
||||
label,
|
||||
className = ''
|
||||
}: {
|
||||
input: string
|
||||
label: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>{label}:</div>
|
||||
<div>{input}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 单字段输入工具 (pattern, query, file_path 等)
|
||||
export function SimpleFieldInputTool({
|
||||
input,
|
||||
label,
|
||||
fieldName,
|
||||
className = ''
|
||||
}: {
|
||||
input: Record<string, any>
|
||||
label: string
|
||||
fieldName: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>{label}:</div>
|
||||
<div>
|
||||
<div>{input[fieldName]}</div>
|
||||
{/* 显示其他字段(如 Grep 的 output_mode) */}
|
||||
{Object.entries(input)
|
||||
.filter(([key]) => key !== fieldName)
|
||||
.map(([key, value]) => (
|
||||
<span key={key}>
|
||||
{key}: {String(value)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 字符串输出工具 (Read, Bash, Search, Glob, WebSearch, Grep 等)
|
||||
export function StringOutputTool({
|
||||
output,
|
||||
label,
|
||||
className = '',
|
||||
textColor = ''
|
||||
}: {
|
||||
output: string
|
||||
label: string
|
||||
className?: string
|
||||
textColor?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={textColor}>{label}:</div>
|
||||
<div>{output}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FolderSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutputType } from './types'
|
||||
|
||||
export function GlobTool({ input, output }: { input: GlobToolInputType; output?: GlobToolOutputType }) {
|
||||
// 如果有输出,计算文件数量
|
||||
const fileCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Glob Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${fileCount} found` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GrepToolInput, GrepToolOutput } from './types'
|
||||
|
||||
export function GrepTool({ input, output }: { input: GrepToolInput; output?: GrepToolOutput }) {
|
||||
// 如果有输出,计算结果行数
|
||||
const resultLines = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Grep Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { SimpleFieldInputTool, StringOutputTool, ToolTitle } from './GenericTools'
|
||||
import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType } from './types'
|
||||
|
||||
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
|
||||
// 如果有输出,计算统计信息
|
||||
const stats = output
|
||||
? {
|
||||
lineCount: output.split('\n').length,
|
||||
fileSize: new Blob([output]).size,
|
||||
formatSize: (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
}
|
||||
: null
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Read Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
stats={output && stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>
|
||||
<SimpleFieldInputTool input={input} label="File Path" fieldName="file_path" />
|
||||
{output && (
|
||||
<div>
|
||||
<StringOutputTool output={output} label="File Content" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { Search } from 'lucide-react'
|
||||
|
||||
import { StringInputTool, StringOutputTool, ToolTitle } from './GenericTools'
|
||||
import type { SearchToolInput as SearchToolInputType, SearchToolOutput as SearchToolOutputType } from './types'
|
||||
|
||||
export function SearchTool({ input, output }: { input: SearchToolInputType; output?: SearchToolOutputType }) {
|
||||
// 如果有输出,计算结果数量
|
||||
const resultCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Search Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search"
|
||||
params={`"${input}"`}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>
|
||||
<StringInputTool input={input} label="Search Query" />
|
||||
{output && (
|
||||
<div>
|
||||
<StringOutputTool output={output} label="Search Results" textColor="text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { Bot } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { TaskToolInput as TaskToolInputType, TaskToolOutput as TaskToolOutputType } from './types'
|
||||
|
||||
export function TaskTool({ input, output }: { input: TaskToolInputType; output?: TaskToolOutputType }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Task Tool"
|
||||
title={<ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />}>
|
||||
{output?.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>Type: {item.type}</div>
|
||||
<div>{item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { ListTodo } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type {
|
||||
TodoWriteToolInput as TodoWriteToolInputType,
|
||||
TodoWriteToolOutput as TodoWriteToolOutputType
|
||||
} from './types'
|
||||
|
||||
export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Todo Write Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Update"
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
}>
|
||||
<div>
|
||||
{input.todos.map((todo, index) => (
|
||||
<div key={index}>
|
||||
<div>
|
||||
<span>{todo.status}</span>
|
||||
{todo.activeForm && <span>{todo.activeForm}</span>}
|
||||
</div>
|
||||
<div>{todo.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
import { SimpleFieldInputTool, ToolTitle } from './GenericTools'
|
||||
import type { WebSearchToolInput, WebSearchToolOutput } from './types'
|
||||
|
||||
export function WebSearchTool({ input, output }: { input: WebSearchToolInput; output?: WebSearchToolOutput }) {
|
||||
// 如果有输出,计算结果数量
|
||||
const resultCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Web Search Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Web Search"
|
||||
params={input.query}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>
|
||||
<SimpleFieldInputTool input={input} label="Web Search Query" fieldName="query" />
|
||||
{output}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WriteToolInput, WriteToolOutput } from './types'
|
||||
|
||||
export function WriteTool({ input, output }: { input: WriteToolInput; output?: WriteToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Write Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />}>
|
||||
<div>{input.content}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
import { Accordion } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { NormalToolResponse } from '@renderer/types'
|
||||
|
||||
// 导出所有类型
|
||||
export * from './types'
|
||||
|
||||
// 导入所有渲染器
|
||||
import { BashTool } from './BashTool'
|
||||
import { GlobTool } from './GlobTool'
|
||||
import { GrepTool } from './GrepTool'
|
||||
import { ReadTool } from './ReadTool'
|
||||
import { SearchTool } from './SearchTool'
|
||||
import { TaskTool } from './TaskTool'
|
||||
import { TodoWriteTool } from './TodoWriteTool'
|
||||
import { AgentToolsType, ToolInput, ToolOutput } from './types'
|
||||
import { WebSearchTool } from './WebSearchTool'
|
||||
import { WriteTool } from './WriteTool'
|
||||
|
||||
const logger = loggerService.withContext('MessageAgentTools')
|
||||
|
||||
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
||||
export const toolRenderers = {
|
||||
[AgentToolsType.Read]: ReadTool,
|
||||
[AgentToolsType.Task]: TaskTool,
|
||||
[AgentToolsType.Bash]: BashTool,
|
||||
[AgentToolsType.Search]: SearchTool,
|
||||
[AgentToolsType.Glob]: GlobTool,
|
||||
[AgentToolsType.TodoWrite]: TodoWriteTool,
|
||||
[AgentToolsType.WebSearch]: WebSearchTool,
|
||||
[AgentToolsType.Grep]: GrepTool,
|
||||
[AgentToolsType.Write]: WriteTool
|
||||
} as const
|
||||
|
||||
// 类型守卫函数
|
||||
export function isValidAgentToolsType(toolName: unknown): toolName is AgentToolsType {
|
||||
return typeof toolName === 'string' && Object.values(AgentToolsType).includes(toolName as AgentToolsType)
|
||||
}
|
||||
|
||||
// 统一的渲染函数
|
||||
function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) {
|
||||
const Renderer = toolRenderers[toolName]
|
||||
if (!Renderer) {
|
||||
logger.error('Unknown tool type', { toolName })
|
||||
return <div>Unknown tool type: {toolName}</div>
|
||||
}
|
||||
|
||||
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'
|
||||
}}>
|
||||
{/* <Renderer input={input as any} output={output as any} /> */}
|
||||
{Renderer({ input: input as any, output: output as any })}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
// 统一的组件渲染入口
|
||||
export function MessageAgentTools({ toolResponse }: { toolResponse: NormalToolResponse }) {
|
||||
const { arguments: args, response, tool } = toolResponse
|
||||
logger.info('Rendering agent tool response', {
|
||||
tool: tool,
|
||||
arguments: args,
|
||||
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)
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
export enum AgentToolsType {
|
||||
Read = 'Read',
|
||||
Task = 'Task',
|
||||
Bash = 'Bash',
|
||||
Search = 'Search',
|
||||
Glob = 'Glob',
|
||||
TodoWrite = 'TodoWrite',
|
||||
WebSearch = 'WebSearch',
|
||||
Grep = 'Grep',
|
||||
Write = 'Write'
|
||||
}
|
||||
|
||||
// Read 工具的类型定义
|
||||
export interface ReadToolInput {
|
||||
file_path: string
|
||||
}
|
||||
|
||||
export type ReadToolOutput = string
|
||||
|
||||
// Task 工具的类型定义
|
||||
export type TaskToolInput = {
|
||||
description: string
|
||||
prompt: string
|
||||
subagent_type: string
|
||||
}
|
||||
|
||||
export type TaskToolOutput = {
|
||||
type: 'text'
|
||||
text: string
|
||||
}[]
|
||||
|
||||
// Bash 工具的类型定义
|
||||
export type BashToolInput = {
|
||||
command: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type BashToolOutput = string
|
||||
|
||||
// Search 工具的类型定义
|
||||
export type SearchToolInput = string
|
||||
|
||||
export type SearchToolOutput = string
|
||||
|
||||
// Glob 工具的类型定义
|
||||
export interface GlobToolInput {
|
||||
pattern: string
|
||||
}
|
||||
|
||||
export type GlobToolOutput = string
|
||||
|
||||
// TodoWrite 工具的类型定义
|
||||
export interface TodoItem {
|
||||
content: string
|
||||
status: string
|
||||
activeForm?: string
|
||||
}
|
||||
|
||||
export type TodoWriteToolInput = {
|
||||
todos: TodoItem[]
|
||||
}
|
||||
|
||||
export type TodoWriteToolOutput = string
|
||||
|
||||
// WebSearch 工具的类型定义
|
||||
export interface WebSearchToolInput {
|
||||
query: string
|
||||
}
|
||||
|
||||
export type WebSearchToolOutput = string
|
||||
|
||||
// Grep 工具的类型定义
|
||||
export interface GrepToolInput {
|
||||
pattern: string
|
||||
output_mode: string
|
||||
}
|
||||
|
||||
export type GrepToolOutput = string
|
||||
|
||||
export type WriteToolInput = {
|
||||
content: string
|
||||
file_path: string
|
||||
}
|
||||
|
||||
export type WriteToolOutput = string
|
||||
|
||||
// 联合类型
|
||||
export type ToolInput =
|
||||
| ReadToolInput
|
||||
| TaskToolInput
|
||||
| BashToolInput
|
||||
| SearchToolInput
|
||||
| GlobToolInput
|
||||
| TodoWriteToolInput
|
||||
| WebSearchToolInput
|
||||
| GrepToolInput
|
||||
| WriteToolInput
|
||||
export type ToolOutput =
|
||||
| ReadToolOutput
|
||||
| TaskToolOutput
|
||||
| BashToolOutput
|
||||
| SearchToolOutput
|
||||
| GlobToolOutput
|
||||
| TodoWriteToolOutput
|
||||
| WebSearchToolOutput
|
||||
| GrepToolOutput
|
||||
| WriteToolOutput
|
||||
// 工具渲染器接口
|
||||
export interface ToolRenderer {
|
||||
render: (props: { input: ToolInput; output?: ToolOutput }) => React.ReactElement
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
import { KnowledgeSearchToolInput, KnowledgeSearchToolOutput } from '@renderer/aiCore/tools/KnowledgeSearchTool'
|
||||
import Spinner from '@renderer/components/Spinner'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { MCPToolResponse } from '@renderer/types'
|
||||
import { NormalToolResponse } from '@renderer/types'
|
||||
import { Typography } from 'antd'
|
||||
import { FileSearch } from 'lucide-react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
export function MessageKnowledgeSearchToolTitle({ toolResponse }: { toolResponse: MCPToolResponse }) {
|
||||
export function MessageKnowledgeSearchToolTitle({ toolResponse }: { toolResponse: NormalToolResponse }) {
|
||||
const toolInput = toolResponse.arguments as KnowledgeSearchToolInput
|
||||
const toolOutput = toolResponse.response as KnowledgeSearchToolOutput
|
||||
|
||||
@ -28,7 +28,7 @@ export function MessageKnowledgeSearchToolTitle({ toolResponse }: { toolResponse
|
||||
)
|
||||
}
|
||||
|
||||
export function MessageKnowledgeSearchToolBody({ toolResponse }: { toolResponse: MCPToolResponse }) {
|
||||
export function MessageKnowledgeSearchToolBody({ toolResponse }: { toolResponse: NormalToolResponse }) {
|
||||
const toolOutput = toolResponse.response as KnowledgeSearchToolOutput
|
||||
|
||||
return toolResponse.status === 'done' ? (
|
||||
|
||||
@ -4,6 +4,7 @@ import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { MCPToolResponse } from '@renderer/types'
|
||||
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
||||
import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
|
||||
import { cancelToolAction, confirmToolAction } from '@renderer/utils/userConfirmation'
|
||||
@ -59,7 +60,7 @@ const MessageMcpTool: FC<Props> = ({ block }) => {
|
||||
|
||||
const toolResponse = block.metadata?.rawMcpToolResponse
|
||||
|
||||
const { id, tool, status, response } = toolResponse!
|
||||
const { id, tool, status, response } = toolResponse as MCPToolResponse
|
||||
const isPending = status === 'pending'
|
||||
const isDone = status === 'done'
|
||||
const isError = status === 'error'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { MemorySearchToolInput, MemorySearchToolOutput } from '@renderer/aiCore/tools/MemorySearchTool'
|
||||
import Spinner from '@renderer/components/Spinner'
|
||||
import { MCPToolResponse } from '@renderer/types'
|
||||
import { NormalToolResponse } from '@renderer/types'
|
||||
import { Typography } from 'antd'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -8,7 +8,7 @@ import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export const MessageMemorySearchToolTitle = ({ toolResponse }: { toolResponse: MCPToolResponse }) => {
|
||||
export const MessageMemorySearchToolTitle = ({ toolResponse }: { toolResponse: NormalToolResponse }) => {
|
||||
const { t } = useTranslation()
|
||||
const toolInput = toolResponse.arguments as MemorySearchToolInput
|
||||
const toolOutput = toolResponse.response as MemorySearchToolOutput
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { MCPToolResponse } from '@renderer/types'
|
||||
import { NormalToolResponse } from '@renderer/types'
|
||||
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
||||
import { Collapse } from 'antd'
|
||||
|
||||
import { MessageAgentTools } from './MessageAgentTools'
|
||||
import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch'
|
||||
import { MessageMemorySearchToolTitle } from './MessageMemorySearch'
|
||||
import { MessageWebSearchToolTitle } from './MessageWebSearch'
|
||||
@ -11,7 +11,7 @@ interface Props {
|
||||
}
|
||||
const prefix = 'builtin_'
|
||||
|
||||
const ChooseTool = (toolResponse: MCPToolResponse): { label: React.ReactNode; body: React.ReactNode } | null => {
|
||||
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
|
||||
let toolName = toolResponse.tool.name
|
||||
if (toolName.startsWith(prefix)) {
|
||||
toolName = toolName.slice(prefix.length)
|
||||
@ -20,20 +20,21 @@ const ChooseTool = (toolResponse: MCPToolResponse): { label: React.ReactNode; bo
|
||||
switch (toolName) {
|
||||
case 'web_search':
|
||||
case 'web_search_preview':
|
||||
return {
|
||||
label: <MessageWebSearchToolTitle toolResponse={toolResponse} />,
|
||||
body: null
|
||||
}
|
||||
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
|
||||
case 'knowledge_search':
|
||||
return {
|
||||
label: <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />,
|
||||
body: null
|
||||
}
|
||||
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
|
||||
case 'memory_search':
|
||||
return {
|
||||
label: <MessageMemorySearchToolTitle toolResponse={toolResponse} />,
|
||||
body: null
|
||||
}
|
||||
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
|
||||
case 'Read':
|
||||
case 'Task':
|
||||
case 'Bash':
|
||||
case 'Search':
|
||||
case 'Glob':
|
||||
case 'TodoWrite':
|
||||
case 'WebSearch':
|
||||
case 'Grep':
|
||||
case 'Write':
|
||||
return <MessageAgentTools toolResponse={toolResponse} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@ -45,32 +46,13 @@ export default function MessageTool({ block }: Props) {
|
||||
|
||||
if (!toolResponse) return null
|
||||
|
||||
const toolRenderer = ChooseTool(toolResponse)
|
||||
const toolRenderer = ChooseTool(toolResponse as NormalToolResponse)
|
||||
|
||||
if (!toolRenderer) return null
|
||||
|
||||
return toolRenderer.body ? (
|
||||
<Collapse
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: toolRenderer.label,
|
||||
children: toolRenderer.body,
|
||||
showArrow: false,
|
||||
styles: {
|
||||
header: {
|
||||
paddingLeft: '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
size="small"
|
||||
ghost
|
||||
/>
|
||||
) : (
|
||||
toolRenderer.label
|
||||
)
|
||||
return toolRenderer
|
||||
}
|
||||
|
||||
// const PrepareToolWrapper = styled.span`
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { WebSearchToolInput, WebSearchToolOutput } from '@renderer/aiCore/tools/WebSearchTool'
|
||||
import Spinner from '@renderer/components/Spinner'
|
||||
import { MCPToolResponse } from '@renderer/types'
|
||||
import { NormalToolResponse } from '@renderer/types'
|
||||
import { Typography } from 'antd'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -8,7 +8,7 @@ import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export const MessageWebSearchToolTitle = ({ toolResponse }: { toolResponse: MCPToolResponse }) => {
|
||||
export const MessageWebSearchToolTitle = ({ toolResponse }: { toolResponse: NormalToolResponse }) => {
|
||||
const { t } = useTranslation()
|
||||
const toolInput = toolResponse.arguments as WebSearchToolInput
|
||||
const toolOutput = toolResponse.response as WebSearchToolOutput
|
||||
|
||||
@ -730,7 +730,7 @@ export type MCPToolResponseStatus = 'pending' | 'cancelled' | 'invoking' | 'done
|
||||
interface BaseToolResponse {
|
||||
id: string // unique id
|
||||
tool: BaseTool | MCPTool
|
||||
arguments: Record<string, unknown> | undefined
|
||||
arguments: Record<string, unknown> | Record<string, unknown>[] | string | undefined
|
||||
status: MCPToolResponseStatus
|
||||
response?: any
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import type {
|
||||
MemoryItem,
|
||||
Metrics,
|
||||
Model,
|
||||
NormalToolResponse,
|
||||
Topic,
|
||||
Usage,
|
||||
WebSearchResponse,
|
||||
@ -114,7 +115,7 @@ export interface ToolMessageBlock extends BaseMessageBlock {
|
||||
arguments?: Record<string, any>
|
||||
content?: string | object
|
||||
metadata?: BaseMessageBlock['metadata'] & {
|
||||
rawMcpToolResponse?: MCPToolResponse
|
||||
rawMcpToolResponse?: MCPToolResponse | NormalToolResponse
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user