mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
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:
parent
b83837708b
commit
45405213fc
@ -7,7 +7,8 @@ Cherry Studio AI Core 是一个基于 Vercel AI SDK 的统一 AI Provider 接口
|
||||
- 🚀 统一的 AI Provider 接口
|
||||
- 🔄 动态导入支持
|
||||
- 🛠️ TypeScript 支持
|
||||
- 📦 轻量级设计
|
||||
- 📦 强大的插件系统
|
||||
- 🌍 内置webSearch(Openai,Google,Anthropic,xAI)
|
||||
|
||||
## 支持的 Providers
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
80
src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx
Normal file
80
src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx
Normal 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;
|
||||
`
|
||||
20
src/renderer/src/pages/home/Messages/Tools/MessageTools.tsx
Normal file
20
src/renderer/src/pages/home/Messages/Tools/MessageTools.tsx
Normal 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} />
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user