From 45405213fc5fca0f4211b021bd80f7b51144546b Mon Sep 17 00:00:00 2001 From: lizhixuan Date: Fri, 18 Jul 2025 00:37:28 +0800 Subject: [PATCH] feat: enhance AI core functionality and introduce new tool components - Updated README to reflect the addition of a powerful plugin system and built-in web search capabilities. - Refactored tool call handling in `ToolCallChunkHandler` to improve state management and response formatting. - Introduced new components `MessageMcpTool`, `MessageTool`, and `MessageTools` for better handling of tool responses and user interactions. - Updated type definitions to support new tool response structures and improved overall code organization. - Enhanced spinner component to accept React nodes for more flexible content rendering. --- packages/aiCore/README.md | 3 +- .../src/aiCore/chunk/AiSdkToChunkAdapter.ts | 10 +-- .../src/aiCore/chunk/handleTooCallChunk.ts | 44 +++++----- src/renderer/src/components/Spinner.tsx | 2 +- .../pages/home/Messages/Blocks/ToolBlock.tsx | 2 +- .../MessageMcpTool.tsx} | 4 +- .../pages/home/Messages/Tools/MessageTool.tsx | 80 +++++++++++++++++++ .../home/Messages/Tools/MessageTools.tsx | 20 +++++ src/renderer/src/types/tool.ts | 10 ++- 9 files changed, 140 insertions(+), 35 deletions(-) rename src/renderer/src/pages/home/Messages/{MessageTools.tsx => Tools/MessageMcpTool.tsx} (99%) create mode 100644 src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx create mode 100644 src/renderer/src/pages/home/Messages/Tools/MessageTools.tsx diff --git a/packages/aiCore/README.md b/packages/aiCore/README.md index ab9e04541f..42d4d48167 100644 --- a/packages/aiCore/README.md +++ b/packages/aiCore/README.md @@ -7,7 +7,8 @@ Cherry Studio AI Core 是一个基于 Vercel AI SDK 的统一 AI Provider 接口 - 🚀 统一的 AI Provider 接口 - 🔄 动态导入支持 - 🛠️ TypeScript 支持 -- 📦 轻量级设计 +- 📦 强大的插件系统 +- 🌍 内置webSearch(Openai,Google,Anthropic,xAI) ## 支持的 Providers diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 0b9518a40c..35168ccf3f 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -133,11 +133,11 @@ export class AiSdkToChunkAdapter { // === 工具调用相关事件(原始 AI SDK 事件,如果没有被中间件处理) === - // case 'tool-input-start': - // case 'tool-input-delta': - // case 'tool-input-end': - // this.toolCallHandler.handleToolCallCreated(chunk) - // break + case 'tool-input-start': + case 'tool-input-delta': + case 'tool-input-end': + this.toolCallHandler.handleToolCallCreated(chunk) + break // case 'tool-input-delta': // this.toolCallHandler.handleToolCallCreated(chunk) diff --git a/src/renderer/src/aiCore/chunk/handleTooCallChunk.ts b/src/renderer/src/aiCore/chunk/handleTooCallChunk.ts index 7587ec211e..152fce03aa 100644 --- a/src/renderer/src/aiCore/chunk/handleTooCallChunk.ts +++ b/src/renderer/src/aiCore/chunk/handleTooCallChunk.ts @@ -6,7 +6,7 @@ import { ToolCallUnion, ToolResultUnion, ToolSet } from '@cherrystudio/ai-core' import Logger from '@renderer/config/logger' -import { BaseTool, MCPToolResponse } from '@renderer/types' +import { BaseTool, MCPToolResponse, ToolCallResponse } from '@renderer/types' import { Chunk, ChunkType } from '@renderer/types/chunk' // import type { // AnthropicSearchOutput, @@ -25,7 +25,7 @@ export class ToolCallChunkHandler { toolName: string args: any // mcpTool 现在可以是 MCPTool 或我们为 Provider 工具创建的通用类型 - mcpTool: BaseTool + tool: BaseTool } >() constructor( @@ -43,16 +43,20 @@ export class ToolCallChunkHandler { handleToolCallCreated(chunk: { type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-end' }): void { switch (chunk.type) { case 'tool-input-start': { + // 能拿到说明是mcpTool + if (this.activeToolCalls.get(chunk.id)) return + + const tool: BaseTool = { + id: chunk.id, + name: chunk.toolName, + description: chunk.toolName, + type: chunk.toolName.startsWith('builtin_') ? 'builtin' : 'provider' + } this.activeToolCalls.set(chunk.id, { toolCallId: chunk.id, toolName: chunk.toolName, args: '', - mcpTool: { - id: chunk.id, - name: chunk.toolName, - description: chunk.toolName, - type: chunk.toolName.startsWith('builtin_') ? 'builtin' : 'provider' - } + tool }) break } @@ -72,14 +76,14 @@ export class ToolCallChunkHandler { Logger.warn(`🔧 [ToolCallChunkHandler] Tool call not found: ${chunk.id}`) return } - const toolResponse: MCPToolResponse = { + const toolResponse: ToolCallResponse = { id: toolCall.toolCallId, - tool: toolCall.mcpTool, + tool: toolCall.tool, arguments: toolCall.args, status: 'pending', toolCallId: toolCall.toolCallId } - + console.log('toolResponse', toolResponse) this.onChunk({ type: ChunkType.MCP_TOOL_PENDING, responses: [toolResponse] @@ -155,7 +159,7 @@ export class ToolCallChunkHandler { toolCallId, toolName, args, - mcpTool: tool + tool }) // 创建 MCPToolResponse 格式 @@ -184,8 +188,7 @@ export class ToolCallChunkHandler { type: 'tool-result' } & ToolResultUnion ): void { - const toolCallId = chunk.toolCallId - const result = chunk.output + const { toolCallId, output, input } = chunk if (!toolCallId) { Logger.warn(`🔧 [ToolCallChunkHandler] Invalid tool result chunk: missing toolCallId`) @@ -202,17 +205,12 @@ export class ToolCallChunkHandler { // 创建工具调用结果的 MCPToolResponse 格式 const toolResponse: MCPToolResponse = { id: toolCallId, - tool: toolCallInfo.mcpTool, - arguments: toolCallInfo.args, + tool: toolCallInfo.tool, + arguments: input, status: 'done', response: { - content: [ - { - type: 'text', - text: typeof result === 'string' ? result : JSON.stringify(result) - } - ], - isError: false + data: output, + success: true }, toolCallId: toolCallId } diff --git a/src/renderer/src/components/Spinner.tsx b/src/renderer/src/components/Spinner.tsx index 5495115056..4431a9b69e 100644 --- a/src/renderer/src/components/Spinner.tsx +++ b/src/renderer/src/components/Spinner.tsx @@ -3,7 +3,7 @@ import { motion } from 'motion/react' import styled from 'styled-components' interface Props { - text: string + text: React.ReactNode } // Define variants for the spinner animation diff --git a/src/renderer/src/pages/home/Messages/Blocks/ToolBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ToolBlock.tsx index 865f9c2948..67ef3ed607 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ToolBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ToolBlock.tsx @@ -1,7 +1,7 @@ import type { ToolMessageBlock } from '@renderer/types/newMessage' import React from 'react' -import MessageTools from '../MessageTools' +import MessageTools from '../Tools/MessageTools' interface Props { block: ToolMessageBlock diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx similarity index 99% rename from src/renderer/src/pages/home/Messages/MessageTools.tsx rename to src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx index feb713c362..182446f843 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx @@ -19,7 +19,7 @@ interface Props { const COUNTDOWN_TIME = 30 -const MessageTools: FC = ({ block }) => { +const MessageMcpTool: FC = ({ block }) => { const [activeKeys, setActiveKeys] = useState([]) const [copiedMap, setCopiedMap] = useState>({}) const [countdown, setCountdown] = useState(COUNTDOWN_TIME) @@ -682,4 +682,4 @@ const ExpandedResponseContainer = styled.div` } ` -export default memo(MessageTools) +export default memo(MessageMcpTool) diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx new file mode 100644 index 0000000000..14333dfd24 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx @@ -0,0 +1,80 @@ +import Spinner from '@renderer/components/Spinner' +import i18n from '@renderer/i18n' +import type { MCPToolResponse } from '@renderer/types' +import type { ToolMessageBlock } from '@renderer/types/newMessage' +import { Collapse } from 'antd' +import { useMemo } from 'react' +import styled from 'styled-components' + +interface Props { + block: ToolMessageBlock +} + +const toolNameMapText = { + web_search: i18n.t('message.searching') +} +const toolDoneNameMapText = (args: Record) => { + const count = args.count ?? 0 + return i18n.t('message.websearch.fetch_complete', { count }) +} + +const PrepareTool = ({ toolResponse }: { toolResponse: MCPToolResponse }) => { + const toolNameText = useMemo( + () => toolNameMapText[toolResponse.tool.name] || toolResponse.tool.name, + [toolResponse.tool] + ) + + return ( + + {toolNameText} + {JSON.stringify(toolResponse.arguments)} + + } + /> + ) +} + +const DoneTool = ({ toolResponse }: { toolResponse: MCPToolResponse }) => { + const toolDoneNameText = useMemo( + () => toolDoneNameMapText({ count: toolResponse.response?.data?.length ?? 0 }), + [toolResponse.response] + ) + return

{toolDoneNameText}

+} + +export default function MessageTool({ block }: Props) { + const toolResponse = block.metadata?.rawMcpToolResponse + if (!toolResponse) return null + console.log('toolResponse', toolResponse) + + return ( + + ) : ( + + ), + children: ( +

{JSON.stringify(toolResponse.status !== 'done' ? toolResponse.arguments : toolResponse.response)}

+ ), + showArrow: false + } + ]} + ghost + /> + ) +} +const PrepareToolWrapper = styled.span` + display: flex; + align-items: center; + gap: 4px; + font-size: 14px; + padding: 10px; + padding-left: 0; +` diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageTools.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageTools.tsx new file mode 100644 index 0000000000..15db1525ee --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageTools.tsx @@ -0,0 +1,20 @@ +import type { ToolMessageBlock } from '@renderer/types/newMessage' + +import MessageMcpTool from './MessageMcpTool' +import MessageTool from './MessageTool' + +interface Props { + block: ToolMessageBlock +} + +export default function MessageTools({ block }: Props) { + const toolResponse = block.metadata?.rawMcpToolResponse + if (!toolResponse) return null + + const tool = toolResponse.tool + if (tool.type === 'mcp') { + return + } + + return +} diff --git a/src/renderer/src/types/tool.ts b/src/renderer/src/types/tool.ts index 5f41e4a85c..8dfc41491e 100644 --- a/src/renderer/src/types/tool.ts +++ b/src/renderer/src/types/tool.ts @@ -9,8 +9,14 @@ export interface BaseTool { type: ToolType } -export interface GenericProviderTool extends BaseTool { - type: 'provider' +export interface ToolCallResponse { + id: string + toolName: string + arguments: Record | undefined + status: 'invoking' | 'completed' | 'error' + result?: any // AI SDK的工具执行结果 + error?: string + providerExecuted?: boolean // 标识是Provider端执行还是客户端执行 } export interface BuiltinTool extends BaseTool {