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.
This commit is contained in:
lizhixuan 2025-07-18 00:37:28 +08:00
parent b83837708b
commit 45405213fc
9 changed files with 140 additions and 35 deletions

View File

@ -7,7 +7,8 @@ Cherry Studio AI Core 是一个基于 Vercel AI SDK 的统一 AI Provider 接口
- 🚀 统一的 AI Provider 接口
- 🔄 动态导入支持
- 🛠️ TypeScript 支持
- 📦 轻量级设计
- 📦 强大的插件系统
- 🌍 内置webSearch(Openai,Google,Anthropic,xAI)
## 支持的 Providers

View File

@ -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)

View File

@ -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<ToolSet>
): 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
}

View File

@ -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

View File

@ -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

View File

@ -19,7 +19,7 @@ interface Props {
const COUNTDOWN_TIME = 30
const MessageTools: FC<Props> = ({ block }) => {
const MessageMcpTool: FC<Props> = ({ block }) => {
const [activeKeys, setActiveKeys] = useState<string[]>([])
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
const [countdown, setCountdown] = useState<number>(COUNTDOWN_TIME)
@ -682,4 +682,4 @@ const ExpandedResponseContainer = styled.div`
}
`
export default memo(MessageTools)
export default memo(MessageMcpTool)

View File

@ -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<string, any>) => {
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 (
<Spinner
text={
<PrepareToolWrapper>
{toolNameText}
<span>{JSON.stringify(toolResponse.arguments)}</span>
</PrepareToolWrapper>
}
/>
)
}
const DoneTool = ({ toolResponse }: { toolResponse: MCPToolResponse }) => {
const toolDoneNameText = useMemo(
() => toolDoneNameMapText({ count: toolResponse.response?.data?.length ?? 0 }),
[toolResponse.response]
)
return <p>{toolDoneNameText}</p>
}
export default function MessageTool({ block }: Props) {
const toolResponse = block.metadata?.rawMcpToolResponse
if (!toolResponse) return null
console.log('toolResponse', toolResponse)
return (
<Collapse
items={[
{
key: '1',
label:
toolResponse.status !== 'done' ? (
<PrepareTool toolResponse={toolResponse} />
) : (
<DoneTool toolResponse={toolResponse} />
),
children: (
<p>{JSON.stringify(toolResponse.status !== 'done' ? toolResponse.arguments : toolResponse.response)}</p>
),
showArrow: false
}
]}
ghost
/>
)
}
const PrepareToolWrapper = styled.span`
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
padding: 10px;
padding-left: 0;
`

View File

@ -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 <MessageMcpTool block={block} />
}
return <MessageTool block={block} />
}

View File

@ -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<string, unknown> | undefined
status: 'invoking' | 'completed' | 'error'
result?: any // AI SDK的工具执行结果
error?: string
providerExecuted?: boolean // 标识是Provider端执行还是客户端执行
}
export interface BuiltinTool extends BaseTool {