mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
Merge 6d06aef563 into a6ba5d34e0
This commit is contained in:
commit
de6b91ef1b
@ -48,7 +48,8 @@ export default defineConfig([
|
||||
'@eslint-react/no-unstable-context-value': 'off',
|
||||
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off',
|
||||
'@eslint-react/hooks-extra/no-unnecessary-use-prefix': 'off',
|
||||
'@eslint-react/no-children-to-array': 'off'
|
||||
'@eslint-react/no-children-to-array': 'off',
|
||||
'@eslint-react/dom/no-unsafe-iframe-sandbox': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Available Tools",
|
||||
"enable": "Enable Tool",
|
||||
"execute": {
|
||||
"button": "Execute",
|
||||
"copied": "Copied to clipboard",
|
||||
"copy": "Copy",
|
||||
"copyFailed": "Failed to copy",
|
||||
"error": {
|
||||
"invalidJson": "Invalid JSON format: {{error}}",
|
||||
"noToolOrServer": "Tool or server not found"
|
||||
},
|
||||
"execute": "Execute",
|
||||
"label": "Execute",
|
||||
"params": "Parameters (JSON)",
|
||||
"paramsPlaceholder": "Enter JSON parameters...",
|
||||
"result": {
|
||||
"error": "Error Result",
|
||||
"success": "Result",
|
||||
"text": "Text"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"value": "Value"
|
||||
},
|
||||
"title": "Execute Tool: {{name}}",
|
||||
"tooltip": "Execute tool with custom parameters",
|
||||
"view": {
|
||||
"formatted": "Formatted",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Allowed Values"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "可用工具",
|
||||
"enable": "启用工具",
|
||||
"execute": {
|
||||
"button": "执行",
|
||||
"copied": "已复制到剪贴板",
|
||||
"copy": "复制",
|
||||
"copyFailed": "复制失败",
|
||||
"error": {
|
||||
"invalidJson": "无效的 JSON 格式: {{error}}",
|
||||
"noToolOrServer": "未找到工具或服务器"
|
||||
},
|
||||
"execute": "执行",
|
||||
"label": "执行",
|
||||
"params": "参数 (JSON)",
|
||||
"paramsPlaceholder": "输入 JSON 参数...",
|
||||
"result": {
|
||||
"error": "错误结果",
|
||||
"success": "结果",
|
||||
"text": "文本"
|
||||
},
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"value": "值"
|
||||
},
|
||||
"title": "执行工具: {{name}}",
|
||||
"tooltip": "使用自定义参数执行工具",
|
||||
"view": {
|
||||
"formatted": "美化",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "允许的值"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "可用工具",
|
||||
"enable": "啟用工具",
|
||||
"execute": {
|
||||
"button": "執行",
|
||||
"copied": "已複製到剪貼板",
|
||||
"copy": "複製",
|
||||
"copyFailed": "複製失敗",
|
||||
"error": {
|
||||
"invalidJson": "無效的 JSON 格式: {{error}}",
|
||||
"noToolOrServer": "未找到工具或伺服器"
|
||||
},
|
||||
"execute": "執行",
|
||||
"label": "執行",
|
||||
"params": "參數 (JSON)",
|
||||
"paramsPlaceholder": "輸入 JSON 參數...",
|
||||
"result": {
|
||||
"error": "錯誤結果",
|
||||
"success": "結果",
|
||||
"text": "文字"
|
||||
},
|
||||
"table": {
|
||||
"name": "名稱",
|
||||
"value": "值"
|
||||
},
|
||||
"title": "執行工具: {{name}}",
|
||||
"tooltip": "使用自訂參數執行工具",
|
||||
"view": {
|
||||
"formatted": "美化",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "允許的值"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Verfügbare Tools",
|
||||
"enable": "Tool aktivieren",
|
||||
"execute": {
|
||||
"button": "Ausführen",
|
||||
"copied": "In Zwischenablage kopiert",
|
||||
"copy": "Kopieren",
|
||||
"copyFailed": "Kopieren fehlgeschlagen",
|
||||
"error": {
|
||||
"invalidJson": "Ungültiges JSON-Format: {{error}}",
|
||||
"noToolOrServer": "Tool oder Server nicht gefunden"
|
||||
},
|
||||
"execute": "Ausführen",
|
||||
"label": "Ausführen",
|
||||
"params": "Parameter (JSON)",
|
||||
"paramsPlaceholder": "JSON-Parameter eingeben...",
|
||||
"result": {
|
||||
"error": "Fehlerergebnis",
|
||||
"success": "Ergebnis",
|
||||
"text": "Text"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"value": "Wert"
|
||||
},
|
||||
"title": "Tool ausführen: {{name}}",
|
||||
"tooltip": "Tool mit benutzerdefinierten Parametern ausführen",
|
||||
"view": {
|
||||
"formatted": "Formatiert",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Erlaubte Werte"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Διαθέσιμα Εργαλεία",
|
||||
"enable": "Ενεργοποίηση εργαλείου",
|
||||
"execute": {
|
||||
"button": "Εκτέλεση",
|
||||
"copied": "Αντιγράφηκε στο πρόχειρο",
|
||||
"copy": "Αντιγραφή",
|
||||
"copyFailed": "Αποτυχία αντιγραφής",
|
||||
"error": {
|
||||
"invalidJson": "Μη έγκυρη μορφή JSON: {{error}}",
|
||||
"noToolOrServer": "Εργαλείο ή διακομιστής δεν βρέθηκε"
|
||||
},
|
||||
"execute": "Εκτέλεση",
|
||||
"label": "Εκτέλεση",
|
||||
"params": "Παράμετροι (JSON)",
|
||||
"paramsPlaceholder": "Εισαγάγετε παραμέτρους JSON...",
|
||||
"result": {
|
||||
"error": "Αποτέλεσμα σφάλματος",
|
||||
"success": "Αποτέλεσμα",
|
||||
"text": "Κείμενο"
|
||||
},
|
||||
"table": {
|
||||
"name": "Όνομα",
|
||||
"value": "Τιμή"
|
||||
},
|
||||
"title": "Εκτέλεση εργαλείου: {{name}}",
|
||||
"tooltip": "Εκτέλεση εργαλείου με προσαρμοσμένες παραμέτρους",
|
||||
"view": {
|
||||
"formatted": "Μορφοποιημένο",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Επιτρεπόμενες τιμές"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Herramientas disponibles",
|
||||
"enable": "Habilitar herramienta",
|
||||
"execute": {
|
||||
"button": "Ejecutar",
|
||||
"copied": "Copiado al portapapeles",
|
||||
"copy": "Copiar",
|
||||
"copyFailed": "Error al copiar",
|
||||
"error": {
|
||||
"invalidJson": "Formato JSON inválido: {{error}}",
|
||||
"noToolOrServer": "Herramienta o servidor no encontrado"
|
||||
},
|
||||
"execute": "Ejecutar",
|
||||
"label": "Ejecutar",
|
||||
"params": "Parámetros (JSON)",
|
||||
"paramsPlaceholder": "Ingrese parámetros JSON...",
|
||||
"result": {
|
||||
"error": "Resultado de error",
|
||||
"success": "Resultado",
|
||||
"text": "Texto"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nombre",
|
||||
"value": "Valor"
|
||||
},
|
||||
"title": "Ejecutar herramienta: {{name}}",
|
||||
"tooltip": "Ejecutar herramienta con parámetros personalizados",
|
||||
"view": {
|
||||
"formatted": "Formateado",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Valores permitidos"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Outils disponibles",
|
||||
"enable": "Activer l'outil",
|
||||
"execute": {
|
||||
"button": "Exécuter",
|
||||
"copied": "Copié dans le presse-papiers",
|
||||
"copy": "Copier",
|
||||
"copyFailed": "Échec de la copie",
|
||||
"error": {
|
||||
"invalidJson": "Format JSON invalide: {{error}}",
|
||||
"noToolOrServer": "Outil ou serveur introuvable"
|
||||
},
|
||||
"execute": "Exécuter",
|
||||
"label": "Exécuter",
|
||||
"params": "Paramètres (JSON)",
|
||||
"paramsPlaceholder": "Entrez les paramètres JSON...",
|
||||
"result": {
|
||||
"error": "Résultat d'erreur",
|
||||
"success": "Résultat",
|
||||
"text": "Texte"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nom",
|
||||
"value": "Valeur"
|
||||
},
|
||||
"title": "Exécuter l'outil: {{name}}",
|
||||
"tooltip": "Exécuter l'outil avec des paramètres personnalisés",
|
||||
"view": {
|
||||
"formatted": "Formaté",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Valeurs autorisées"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "利用可能なツール",
|
||||
"enable": "ツールを有効にする",
|
||||
"execute": {
|
||||
"button": "実行",
|
||||
"copied": "クリップボードにコピーしました",
|
||||
"copy": "コピー",
|
||||
"copyFailed": "コピーに失敗しました",
|
||||
"error": {
|
||||
"invalidJson": "無効なJSON形式: {{error}}",
|
||||
"noToolOrServer": "ツールまたはサーバーが見つかりません"
|
||||
},
|
||||
"execute": "実行",
|
||||
"label": "実行",
|
||||
"params": "パラメータ (JSON)",
|
||||
"paramsPlaceholder": "JSONパラメータを入力...",
|
||||
"result": {
|
||||
"error": "エラー結果",
|
||||
"success": "結果",
|
||||
"text": "テキスト"
|
||||
},
|
||||
"table": {
|
||||
"name": "名前",
|
||||
"value": "値"
|
||||
},
|
||||
"title": "ツールを実行: {{name}}",
|
||||
"tooltip": "カスタムパラメータでツールを実行",
|
||||
"view": {
|
||||
"formatted": "フォーマット済み",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "許可された値"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Ferramentas Disponíveis",
|
||||
"enable": "Habilitar Ferramenta",
|
||||
"execute": {
|
||||
"button": "Executar",
|
||||
"copied": "Copiado para a área de transferência",
|
||||
"copy": "Copiar",
|
||||
"copyFailed": "Falha ao copiar",
|
||||
"error": {
|
||||
"invalidJson": "Formato JSON inválido: {{error}}",
|
||||
"noToolOrServer": "Ferramenta ou servidor não encontrado"
|
||||
},
|
||||
"execute": "Executar",
|
||||
"label": "Executar",
|
||||
"params": "Parâmetros (JSON)",
|
||||
"paramsPlaceholder": "Digite os parâmetros JSON...",
|
||||
"result": {
|
||||
"error": "Resultado de erro",
|
||||
"success": "Resultado",
|
||||
"text": "Texto"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"value": "Valor"
|
||||
},
|
||||
"title": "Executar ferramenta: {{name}}",
|
||||
"tooltip": "Executar ferramenta com parâmetros personalizados",
|
||||
"view": {
|
||||
"formatted": "Formatado",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Valores permitidos"
|
||||
|
||||
@ -4114,6 +4114,35 @@
|
||||
},
|
||||
"availableTools": "Доступные инструменты",
|
||||
"enable": "Включить инструмент",
|
||||
"execute": {
|
||||
"button": "Выполнить",
|
||||
"copied": "Скопировано в буфер обмена",
|
||||
"copy": "Копировать",
|
||||
"copyFailed": "Не удалось скопировать",
|
||||
"error": {
|
||||
"invalidJson": "Неверный формат JSON: {{error}}",
|
||||
"noToolOrServer": "Инструмент или сервер не найден"
|
||||
},
|
||||
"execute": "Выполнить",
|
||||
"label": "Выполнить",
|
||||
"params": "Параметры (JSON)",
|
||||
"paramsPlaceholder": "Введите параметры JSON...",
|
||||
"result": {
|
||||
"error": "Результат ошибки",
|
||||
"success": "Результат",
|
||||
"text": "Текст"
|
||||
},
|
||||
"table": {
|
||||
"name": "Имя",
|
||||
"value": "Значение"
|
||||
},
|
||||
"title": "Выполнить инструмент: {{name}}",
|
||||
"tooltip": "Выполнить инструмент с пользовательскими параметрами",
|
||||
"view": {
|
||||
"formatted": "Форматированный",
|
||||
"json": "JSON"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"enum": {
|
||||
"allowedValues": "Допустимые значения"
|
||||
|
||||
442
src/renderer/src/pages/settings/MCPSettings/ExecuteToolModal.tsx
Normal file
442
src/renderer/src/pages/settings/MCPSettings/ExecuteToolModal.tsx
Normal file
@ -0,0 +1,442 @@
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import type { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Button, Flex, Input, message, Modal, Space, Table, Typography } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { Code as CodeIcon, Copy, Play, Sparkles } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkCjkFriendly from 'remark-cjk-friendly'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
const logger = loggerService.withContext('ExecuteToolModal')
|
||||
|
||||
// HTML 文档结构检测模式(在模块级别定义,避免重复创建)
|
||||
const HTML_DOCUMENT_PATTERNS = [/<!DOCTYPE\s+html/i, /<html[\s>]/i, /<head[\s>]/i, /<body[\s>]/i]
|
||||
|
||||
// Markdown 检测模式(在模块级别定义,避免重复创建)
|
||||
const MARKDOWN_PATTERNS = [
|
||||
/^#{1,6}\s+.+$/m, // 标题
|
||||
/^\s*[-*+]\s+.+$/m, // 列表
|
||||
/^\s*\d+\.\s+.+$/m, // 有序列表
|
||||
/\[.+\]\(.+\)/, // 链接
|
||||
/!\[.+\]\(.+\)/, // 图片
|
||||
/```[\s\S]*?```/, // 代码块
|
||||
/`[^`]+`/, // 行内代码
|
||||
/\*\*.*?\*\*/, // 粗体
|
||||
/\*.*?\*/, // 斜体
|
||||
/^>\s+.+$/m // 引用
|
||||
]
|
||||
|
||||
/**
|
||||
* 检测文本内容类型
|
||||
* @param text 要检测的文本
|
||||
* @returns 内容类型:'json' | 'markdown' | 'html' | 'text'
|
||||
*/
|
||||
function detectContentType(text: string): 'json' | 'markdown' | 'html' | 'text' {
|
||||
if (!text) return 'text'
|
||||
|
||||
// 检测 HTML 特征(优先检测,因为 HTML 可能包含其他格式)
|
||||
// 检查是否包含完整的 HTML 文档结构或大量 HTML 标签
|
||||
const hasHtmlDocument = HTML_DOCUMENT_PATTERNS.some((pattern) => pattern.test(text))
|
||||
|
||||
// 检查 HTML 标签数量
|
||||
// 注意:每次调用时创建新的正则表达式实例,避免全局标志的状态污染
|
||||
const htmlTags = text.match(/<[a-z][a-z0-9]*[\s>]/gi)
|
||||
const htmlTagCount = htmlTags ? htmlTags.length : 0
|
||||
|
||||
// 如果包含 HTML 文档结构,或者有多个 HTML 标签,认为是 HTML
|
||||
if (hasHtmlDocument || htmlTagCount >= 3) {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
// 尝试解析为 JSON
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return 'json'
|
||||
}
|
||||
} catch {
|
||||
// 不是 JSON
|
||||
}
|
||||
|
||||
// 检测 Markdown 特征
|
||||
const hasMarkdown = MARKDOWN_PATTERNS.some((pattern) => pattern.test(text))
|
||||
if (hasMarkdown) {
|
||||
return 'markdown'
|
||||
}
|
||||
|
||||
return 'text'
|
||||
}
|
||||
|
||||
interface ExecuteToolModalProps {
|
||||
open: boolean
|
||||
tool: MCPTool | null
|
||||
server: MCPServer | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
key: string
|
||||
name: string
|
||||
value: any
|
||||
}
|
||||
|
||||
const ExecuteToolModal: React.FC<ExecuteToolModalProps> = ({ open, tool, server, onClose }) => {
|
||||
const { t } = useTranslation()
|
||||
const [paramsJson, setParamsJson] = useState('{}')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<{ content: any[]; isError?: boolean } | null>(null)
|
||||
const [viewMode, setViewMode] = useState<'json' | 'formatted'>('json')
|
||||
|
||||
// 初始化参数 JSON(基于工具的 inputSchema)
|
||||
const initialParams = useMemo(() => {
|
||||
if (!tool?.inputSchema?.properties) {
|
||||
return '{}'
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {}
|
||||
const properties = tool.inputSchema.properties
|
||||
|
||||
// 为每个属性生成默认值或示例
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const prop = properties[key] as {
|
||||
type?: string
|
||||
default?: any
|
||||
}
|
||||
if (prop.type === 'string') {
|
||||
params[key] = prop.default !== undefined ? prop.default : ''
|
||||
} else if (prop.type === 'number') {
|
||||
params[key] = prop.default !== undefined ? prop.default : 0
|
||||
} else if (prop.type === 'boolean') {
|
||||
params[key] = prop.default !== undefined ? prop.default : false
|
||||
} else if (prop.type === 'array') {
|
||||
params[key] = prop.default !== undefined ? prop.default : []
|
||||
} else if (prop.type === 'object') {
|
||||
params[key] = prop.default !== undefined ? prop.default : {}
|
||||
}
|
||||
})
|
||||
|
||||
return JSON.stringify(params, null, 2)
|
||||
}, [tool])
|
||||
|
||||
// 当工具改变时,重置参数
|
||||
// initialParams 是基于 tool 的 memoized 值,所以当 tool 改变时它会自动更新
|
||||
// 因此不需要将 initialParams 包含在依赖数组中
|
||||
useEffect(() => {
|
||||
if (open && tool) {
|
||||
setParamsJson(initialParams)
|
||||
setResult(null)
|
||||
setViewMode('json')
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, tool])
|
||||
|
||||
// 获取主要文本内容
|
||||
const mainTextContent = useMemo(() => {
|
||||
if (!result || !result.content) return ''
|
||||
|
||||
// 查找第一个 text 类型的 content
|
||||
const textContent = result.content.find((item) => item.type === 'text')
|
||||
return textContent?.text || ''
|
||||
}, [result])
|
||||
|
||||
// 获取格式化的内容类型
|
||||
const formattedContentType = useMemo(() => {
|
||||
return detectContentType(mainTextContent)
|
||||
}, [mainTextContent])
|
||||
|
||||
// 验证 JSON 格式
|
||||
const validateJson = (jsonStr: string): { valid: boolean; data?: any; error?: string } => {
|
||||
try {
|
||||
const data = JSON.parse(jsonStr)
|
||||
return { valid: true, data }
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid JSON'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行工具
|
||||
const handleExecute = async () => {
|
||||
if (!tool || !server) {
|
||||
message.error(t('settings.mcp.tools.execute.error.noToolOrServer', 'Tool or server not found'))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 JSON
|
||||
const validation = validateJson(paramsJson)
|
||||
if (!validation.valid) {
|
||||
message.error(
|
||||
t('settings.mcp.tools.execute.error.invalidJson', 'Invalid JSON format: {{error}}', {
|
||||
error: validation.error
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setResult(null)
|
||||
|
||||
try {
|
||||
logger.info(`Executing tool: ${tool.name}`, { params: validation.data })
|
||||
|
||||
const resp = await window.api.mcp.callTool({
|
||||
server,
|
||||
name: tool.name,
|
||||
args: validation.data,
|
||||
callId: `manual-${Date.now()}`
|
||||
})
|
||||
|
||||
logger.info(`Tool executed successfully: ${tool.name}`, resp)
|
||||
setResult(resp)
|
||||
} catch (error) {
|
||||
logger.error(`Error executing tool: ${tool.name}`, error as Error)
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : typeof error === 'string' ? error : JSON.stringify(error)
|
||||
|
||||
setResult({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: errorMessage
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果
|
||||
const handleCopy = () => {
|
||||
if (!result) return
|
||||
|
||||
const resultText = JSON.stringify(result, null, 2)
|
||||
navigator.clipboard.writeText(resultText).then(
|
||||
() => {
|
||||
message.success(t('settings.mcp.tools.execute.copied', 'Copied to clipboard'))
|
||||
},
|
||||
() => {
|
||||
message.error(t('settings.mcp.tools.execute.copyFailed', 'Failed to copy'))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 将结果转换为表格数据(仅当内容是 JSON 时)
|
||||
const tableData: TableData[] = useMemo(() => {
|
||||
if (!result || !result.content || formattedContentType !== 'json') return []
|
||||
|
||||
const data: TableData[] = []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(mainTextContent)
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
// 如果是对象,展开为多行
|
||||
Object.entries(parsed).forEach(([key, value]) => {
|
||||
data.push({
|
||||
key: key,
|
||||
name: key,
|
||||
value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||
})
|
||||
})
|
||||
} else if (Array.isArray(parsed)) {
|
||||
// 如果是数组,显示索引和值
|
||||
parsed.forEach((item, idx) => {
|
||||
data.push({
|
||||
key: `item-${idx}`,
|
||||
name: String(idx),
|
||||
value: typeof item === 'object' ? JSON.stringify(item, null, 2) : String(item)
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// 解析失败,返回空数组
|
||||
}
|
||||
|
||||
return data
|
||||
}, [result, formattedContentType, mainTextContent])
|
||||
|
||||
const tableColumns: ColumnsType<TableData> = [
|
||||
{
|
||||
title: t('settings.mcp.tools.execute.table.name', 'Name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.tools.execute.table.value', 'Value'),
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (value: string) => (
|
||||
<Typography.Text
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
maxWidth: '100%',
|
||||
display: 'block'
|
||||
}}>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
if (!tool || !server) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center" gap={8}>
|
||||
<Play size={16} />
|
||||
<Typography.Text strong>
|
||||
{t('settings.mcp.tools.execute.title', 'Execute Tool: {{name}}', { name: tool.name })}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={900}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>,
|
||||
<Button key="execute" type="primary" loading={loading} onClick={handleExecute} icon={<Play size={14} />}>
|
||||
{t('settings.mcp.tools.execute.execute', 'Execute')}
|
||||
</Button>
|
||||
]}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* 参数输入 */}
|
||||
<div>
|
||||
<Typography.Title level={5}>{t('settings.mcp.tools.execute.params', 'Parameters (JSON)')}</Typography.Title>
|
||||
<Input.TextArea
|
||||
value={paramsJson}
|
||||
onChange={(e) => setParamsJson(e.target.value)}
|
||||
rows={8}
|
||||
placeholder={t('settings.mcp.tools.execute.paramsPlaceholder', 'Enter JSON parameters...')}
|
||||
style={{ fontFamily: 'monospace', fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 结果显示 */}
|
||||
{result && (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
||||
<Typography.Title level={5} style={{ margin: 0, color: result.isError ? '#ff4d4f' : undefined }}>
|
||||
{result.isError
|
||||
? t('settings.mcp.tools.execute.result.error', 'Error Result')
|
||||
: t('settings.mcp.tools.execute.result.success', 'Result')}
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Button.Group>
|
||||
<Button
|
||||
type={viewMode === 'json' ? 'primary' : 'default'}
|
||||
icon={<CodeIcon size={14} />}
|
||||
onClick={() => setViewMode('json')}
|
||||
size="small">
|
||||
{t('settings.mcp.tools.execute.view.json', 'JSON')}
|
||||
</Button>
|
||||
<Button
|
||||
type={viewMode === 'formatted' ? 'primary' : 'default'}
|
||||
icon={<Sparkles size={14} />}
|
||||
onClick={() => setViewMode('formatted')}
|
||||
size="small">
|
||||
{t('settings.mcp.tools.execute.view.formatted', 'Formatted')}
|
||||
</Button>
|
||||
</Button.Group>
|
||||
<Button icon={<Copy size={14} />} onClick={handleCopy} size="small">
|
||||
{t('settings.mcp.tools.execute.copy', 'Copy')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{viewMode === 'json' ? (
|
||||
<Input.TextArea
|
||||
value={JSON.stringify(result, null, 2)}
|
||||
readOnly
|
||||
rows={12}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
backgroundColor: result.isError ? '#fff1f0' : '#f6ffed',
|
||||
borderColor: result.isError ? '#ffccc7' : '#b7eb8f'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: result.isError ? '#fff1f0' : '#f6ffed',
|
||||
border: `1px solid ${result.isError ? '#ffccc7' : '#b7eb8f'}`,
|
||||
borderRadius: '4px',
|
||||
padding: formattedContentType === 'html' ? '0' : '16px',
|
||||
maxHeight: '500px',
|
||||
overflow: formattedContentType === 'html' ? 'hidden' : 'auto',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{formattedContentType === 'html' ? (
|
||||
<iframe
|
||||
key={mainTextContent} // 强制重新创建 iframe 当内容改变时
|
||||
srcDoc={mainTextContent}
|
||||
title="HTML Preview"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '500px',
|
||||
border: 'none',
|
||||
display: 'block',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
/>
|
||||
) : formattedContentType === 'json' && tableData.length > 0 ? (
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ y: 400 }}
|
||||
style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
/>
|
||||
) : formattedContentType === 'markdown' ? (
|
||||
<div className="markdown" style={{ wordBreak: 'break-word' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkCjkFriendly, remarkMath]}
|
||||
rehypePlugins={[rehypeRaw, rehypeKatex]}>
|
||||
{mainTextContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
marginBottom: 0,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{mainTextContent || JSON.stringify(result.content, null, 2)}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExecuteToolModal
|
||||
@ -1,10 +1,13 @@
|
||||
import type { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
|
||||
import { Badge, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd'
|
||||
import { Badge, Button, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { Hammer, Info, Zap } from 'lucide-react'
|
||||
import { Hammer, Info, Play, Zap } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import ExecuteToolModal from './ExecuteToolModal'
|
||||
|
||||
interface MCPToolsSectionProps {
|
||||
tools: MCPTool[]
|
||||
server: MCPServer
|
||||
@ -14,6 +17,8 @@ interface MCPToolsSectionProps {
|
||||
|
||||
const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: MCPToolsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [executeModalOpen, setExecuteModalOpen] = useState(false)
|
||||
const [selectedTool, setSelectedTool] = useState<MCPTool | null>(null)
|
||||
|
||||
// Check if a tool is enabled (not in the disabledTools array)
|
||||
const isToolEnabled = (tool: MCPTool) => {
|
||||
@ -30,6 +35,12 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
onToggleAutoApprove(tool, checked)
|
||||
}
|
||||
|
||||
// Handle execute tool
|
||||
const handleExecuteTool = (tool: MCPTool) => {
|
||||
setSelectedTool(tool)
|
||||
setExecuteModalOpen(true)
|
||||
}
|
||||
|
||||
// Render tool properties from the input schema
|
||||
const renderToolProperties = (tool: MCPTool) => {
|
||||
if (!tool.inputSchema?.properties) return null
|
||||
@ -102,6 +113,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
}
|
||||
|
||||
const columns: ColumnsType<MCPTool> = [
|
||||
// 工具列表列定义:可用工具、启用工具、自动批准、执行工具
|
||||
{
|
||||
title: <Typography.Text strong>{t('settings.mcp.tools.availableTools')}</Typography.Text>,
|
||||
dataIndex: 'name',
|
||||
@ -141,7 +153,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
</Flex>
|
||||
),
|
||||
key: 'enable',
|
||||
width: 150, // Fixed width might be good for alignment
|
||||
width: 130, // Fixed width might be good for alignment
|
||||
align: 'center',
|
||||
render: (_, tool) => (
|
||||
<Switch checked={isToolEnabled(tool)} onChange={(checked) => handleToggle(tool, checked)} size="small" />
|
||||
@ -155,7 +167,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
</Flex>
|
||||
),
|
||||
key: 'autoApprove',
|
||||
width: 150, // Fixed width
|
||||
width: 130, // Fixed width
|
||||
align: 'center',
|
||||
render: (_, tool) => (
|
||||
<Tooltip
|
||||
@ -175,21 +187,57 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Flex align="center" justify="center" gap={4}>
|
||||
<Play size={14} color="green" />
|
||||
<Typography.Text strong>{t('settings.mcp.tools.execute.label', 'Execute')}</Typography.Text>
|
||||
</Flex>
|
||||
),
|
||||
key: 'execute',
|
||||
width: 130,
|
||||
align: 'center',
|
||||
render: (_, tool) => (
|
||||
<Tooltip title={t('settings.mcp.tools.execute.tooltip', 'Execute tool with custom parameters')}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<Play size={12} />}
|
||||
onClick={() => handleExecuteTool(tool)}
|
||||
disabled={!isToolEnabled(tool)}>
|
||||
{t('settings.mcp.tools.execute.button', 'Execute')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return tools.length > 0 ? (
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tools}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender: (tool) => renderToolProperties(tool)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
return (
|
||||
<>
|
||||
{tools.length > 0 ? (
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tools}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender: (tool) => renderToolProperties(tool)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
<ExecuteToolModal
|
||||
open={executeModalOpen}
|
||||
tool={selectedTool}
|
||||
server={server}
|
||||
onClose={() => {
|
||||
setExecuteModalOpen(false)
|
||||
setSelectedTool(null)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user