From 9df7ac0ac2ccc14cdcbccc9c2210643efab32923 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Mon, 1 Sep 2025 09:59:53 +0800 Subject: [PATCH 01/29] feat: add support for downloading and retaining @napi-rs/system-ocr packages (#9741) - Implemented downloading of architecture-specific versions of the @napi-rs/system-ocr packages for macOS and Windows platforms in build-npm.js. - Updated after-pack.js to retain these packages during the packaging process, ensuring compatibility across different architectures. --- scripts/after-pack.js | 12 ++++++++++++ scripts/build-npm.js | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/scripts/after-pack.js b/scripts/after-pack.js index fd3c361788..21bef17821 100644 --- a/scripts/after-pack.js +++ b/scripts/after-pack.js @@ -25,6 +25,12 @@ exports.default = async function (context) { ? ['sharp-darwin-arm64', 'sharp-libvips-darwin-arm64'] : ['sharp-darwin-x64', 'sharp-libvips-darwin-x64'] ) + + keepPackageNodeFiles( + node_modules_path, + '@napi-rs', + arch === Arch.arm64 ? ['system-ocr-darwin-arm64'] : ['system-ocr-darwin-x64'] + ) } if (platform === 'linux') { @@ -59,6 +65,12 @@ exports.default = async function (context) { ? ['sharp-win32-arm64', 'sharp-libvips-win32-arm64'] : ['sharp-win32-x64', 'sharp-libvips-win32-x64'] ) + + keepPackageNodeFiles( + node_modules_path, + '@napi-rs', + arch === Arch.arm64 ? ['system-ocr-win32-arm64-msvc'] : ['system-ocr-win32-x64-msvc'] + ) } if (platform === 'windows') { diff --git a/scripts/build-npm.js b/scripts/build-npm.js index 159e77453e..c3a63d5221 100644 --- a/scripts/build-npm.js +++ b/scripts/build-npm.js @@ -25,6 +25,14 @@ async function downloadNpm(platform) { '@img/sharp-libvips-darwin-x64', 'https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz' ) + downloadNpmPackage( + '@napi-rs/system-ocr-darwin-arm64', + 'https://registry.npmjs.org/@napi-rs/system-ocr-darwin-arm64/-/system-ocr-darwin-arm64-1.0.2.tgz' + ) + downloadNpmPackage( + '@napi-rs/system-ocr-darwin-x64', + 'https://registry.npmjs.org/@napi-rs/system-ocr-darwin-x64/-/system-ocr-darwin-x64-1.0.2.tgz' + ) } if (!platform || platform === 'linux') { @@ -81,6 +89,15 @@ async function downloadNpm(platform) { '@img/sharp-win32-x64', 'https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz' ) + + downloadNpmPackage( + '@napi-rs/system-ocr-win32-arm64-msvc', + 'https://registry.npmjs.org/@napi-rs/system-ocr-win32-arm64-msvc/-/system-ocr-win32-arm64-msvc-1.0.2.tgz' + ) + downloadNpmPackage( + '@napi-rs/system-ocr-win32-x64-msvc', + 'https://registry.npmjs.org/@napi-rs/system-ocr-win32-x64-msvc/-/system-ocr-win32-x64-msvc-1.0.2.tgz' + ) } } From 7303c785aa81b1937a732ce1fe972308addbb3ba Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:20:02 +0800 Subject: [PATCH 02/29] feat(MCPSettings): add special error boundary & data validation for mcp server (#9633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(MCPSettings): 为McpServerCard添加错误边界处理 在McpServerCard组件外层添加ErrorBoundary,防止组件内部错误导致整个页面崩溃 * feat(types): 添加 MCP 服务器配置的类型定义和验证函数 添加 MCP 服务器配置的 Zod schema 类型定义,包括服务器类型、配置和验证函数 将 MCPServer 的 type 字段更新为复用 McpServerType * feat(MCPSettings): 添加ErrorBoundary包装路由组件以捕获错误 * feat(types): 为McpServerConfigSchema添加url、headers和tags字段 添加服务器配置的URL地址、请求头配置和标签字段,以支持更灵活的服务器配置选项 * refactor(MCPSettings): 重构JSON解析逻辑为独立函数并使用zod验证 将原有的parseAndExtractServer函数重构为getServerFromJson,使用zod进行配置验证 移除重复的解析逻辑,简化代码结构 * feat(设置): 添加MCP服务器错误处理和详情展示功能 在MCP服务器卡片中添加错误边界处理,当服务器无效时显示错误提示和详情按钮 新增GeneralPopup组件用于展示错误详情 更新i18n翻译文件添加相关文本 * fix(MCPSettings): 修复导入服务器配置时的类型检查和错误处理 修正 getServerFromJson 返回类型定义,明确区分成功和错误状态 修复错误判断逻辑,使用 null 明确检查而非隐式转换 修复服务器名称存在时的错误提示,移除不必要的非空断言 * feat(MCPSettings): 添加临时测试用的无效服务器功能 添加一个临时测试按钮用于模拟添加无效服务器配置,方便测试错误处理流程 * feat(i18n): 添加错误处理页面的多语言翻译 添加"details"和"mcp.invalid"字段的翻译,用于错误处理页面 * fix(MCPSettings): 修复导入MCP服务器配置时的JSON解析和验证逻辑 将JSON解析和验证拆分为两个步骤,分别捕获解析和验证错误并记录日志 修复服务器配置名称赋值逻辑,使用正确的键名 * refactor(MCPSettings): 替换删除图标为DeleteIcon组件 * feat(MCPSettings): 在McpServerCard中添加错误详情点击展示功能 - 提取错误信息格式化逻辑到变量errorDetails - 添加点击卡片展示完整错误详情的功能 - 统一按钮点击事件处理,阻止事件冒泡 - 优化错误展示样式,增加内边距和文字省略效果 * refactor(utils): 移除错误处理模块中的日志记录 * docs(MCPSettings): 移除AddMcpServerModal中多余的t参数注释 * test(utils): 移除对console.error的冗余断言 * fix(types): 将args字段从必需改为可选并设置默认值 修改McpServerConfigSchema中的args字段,使其从必需字段变为可选字段并设置默认空数组 * fix(types): 将服务器配置的command和args字段改为可选 command字段现在默认为空字符串,args字段默认为空数组,以提供更灵活的配置方式 * feat(types): 扩展 MCP 服务器配置类型,新增 baseUrl 等字段 添加 baseUrl、description、registryUrl 和 provider 字段以增强服务器配置能力 * fix(MCPSettings): 修复导入MCP服务器时未设置名称的问题 当导入MCP服务器配置时,仅在名称未设置时使用key作为默认名称 * refactor(types): 重构 MCP 相关类型定义并添加更多配置字段 将 MCPConfigSample 从接口改为 zod 推断类型 为 McpServerConfigSchema 添加更多可选配置字段 重新组织 MCPServer 接口字段并添加内部使用注释 * refactor(types): 将 MCP 相关类型定义提取到独立文件 将 MCP 服务器配置相关的 Zod schema 和类型定义从 index.ts 移动到新的 mcp.ts 文件 保持原有功能不变,提高代码组织性和可维护性 * docs(types): 更新MCP服务器内部字段的注释说明 添加关于JSON数据格式暴露的额外警告信息 * feat(types): 添加 strip 工具函数用于移除对象属性 添加一个通用的 strip 工具函数,用于从对象中移除指定的属性并返回新对象 * refactor(types): 调整MCPServer接口和strip函数参数格式 将MCPServer接口中的disabledTools和disabledAutoApproveTools字段移动到文档注释下方 修改strip函数参数从可变参数改为数组形式 更新McpServerConfigSchema字段的默认值和描述 * feat(mcp): 改进 MCP 配置验证并添加 Zod 错误格式化功能 添加 formatZodError 工具函数用于格式化 Zod 验证错误 修改 MCP 配置验证逻辑,使用 safeValidateMcpConfig 替代直接验证 允许 inMemory 类型服务器并添加额外校验规则 更新相关组件使用新的验证方式和错误处理 * refactor(MCPSettings): 移除临时测试代码和无效server添加按钮 * fix(MCPSettings): 修复EditMcpJsonPopup中json错误显示样式问题 --- .../src/components/Popups/GeneralPopup.tsx | 66 +++++ src/renderer/src/i18n/locales/en-us.json | 4 + src/renderer/src/i18n/locales/ja-jp.json | 4 + src/renderer/src/i18n/locales/ru-ru.json | 4 + src/renderer/src/i18n/locales/zh-cn.json | 4 + src/renderer/src/i18n/locales/zh-tw.json | 4 + src/renderer/src/i18n/translate/el-gr.json | 4 + src/renderer/src/i18n/translate/es-es.json | 4 + src/renderer/src/i18n/translate/fr-fr.json | 4 + src/renderer/src/i18n/translate/pt-pt.json | 4 + .../MCPSettings/AddMcpServerModal.tsx | 150 ++++-------- .../settings/MCPSettings/EditMcpJsonPopup.tsx | 21 +- .../settings/MCPSettings/McpServerCard.tsx | 186 ++++++++++---- .../src/pages/settings/MCPSettings/index.tsx | 43 ++-- src/renderer/src/types/index.ts | 54 +++-- src/renderer/src/types/mcp.ts | 227 ++++++++++++++++++ .../src/utils/__tests__/error.test.ts | 1 - src/renderer/src/utils/error.ts | 19 +- src/renderer/src/utils/json.ts | 1 + 19 files changed, 606 insertions(+), 198 deletions(-) create mode 100644 src/renderer/src/components/Popups/GeneralPopup.tsx create mode 100644 src/renderer/src/types/mcp.ts diff --git a/src/renderer/src/components/Popups/GeneralPopup.tsx b/src/renderer/src/components/Popups/GeneralPopup.tsx new file mode 100644 index 0000000000..3307b10162 --- /dev/null +++ b/src/renderer/src/components/Popups/GeneralPopup.tsx @@ -0,0 +1,66 @@ +import { TopView } from '@renderer/components/TopView' +import { Modal, ModalProps } from 'antd' +import { ReactNode, useState } from 'react' + +interface ShowParams extends ModalProps { + content: ReactNode +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ content, resolve, ...rest }) => { + const [open, setOpen] = useState(true) + + const onOk = () => { + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + GeneralPopup.hide = onCancel + + return ( + + {content} + + ) +} + +const TopViewKey = 'GeneralPopup' + +/** 在这个 Popup 中展示任意内容 */ +export default class GeneralPopup { + static topviewId = 0 + static hide() { + TopView.hide(TopViewKey) + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + TopView.hide(TopViewKey) + }} + />, + TopViewKey + ) + }) + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 571674567d..efcdd11158 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -820,6 +820,10 @@ "devtools": "Open debug panel", "message": "It seems that something went wrong...", "reload": "Reload" + }, + "details": "Details", + "mcp": { + "invalid": "Invalid MCP server" } }, "chat": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index cd4762ee83..1f4ef33d70 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -820,6 +820,10 @@ "devtools": "デバッグパネルを開く", "message": "何か問題が発生したようです...", "reload": "再読み込み" + }, + "details": "詳細情報", + "mcp": { + "invalid": "無効なMCPサーバー" } }, "chat": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 17036fd103..e65b979103 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -820,6 +820,10 @@ "devtools": "Открыть панель отладки", "message": "Похоже, возникла какая-то проблема...", "reload": "Перезагрузить" + }, + "details": "Подробности", + "mcp": { + "invalid": "Недействительный сервер MCP" } }, "chat": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 678cf39df3..9f330ab5fc 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -820,6 +820,10 @@ "devtools": "打开调试面板", "message": "似乎出现了一些问题...", "reload": "重新加载" + }, + "details": "详细信息", + "mcp": { + "invalid": "无效的MCP服务器" } }, "chat": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index c13cb0c370..e67daa2232 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -820,6 +820,10 @@ "devtools": "打開除錯面板", "message": "似乎出現了一些問題...", "reload": "重新載入" + }, + "details": "詳細信息", + "mcp": { + "invalid": "無效的MCP伺服器" } }, "chat": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index f50bcf3bd1..49bc277157 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -820,6 +820,10 @@ "devtools": "Άνοιγμα πίνακα αποσφαλμάτωσης", "message": "Φαίνεται ότι προέκυψε κάποιο πρόβλημα...", "reload": "Επαναφόρτωση" + }, + "details": "Λεπτομέρειες", + "mcp": { + "invalid": "Μη έγκυρος διακομιστής MCP" } }, "chat": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 902aa3fdff..cdcce9bac7 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -820,6 +820,10 @@ "devtools": "Abrir el panel de depuración", "message": "Parece que ha surgido un problema...", "reload": "Recargar" + }, + "details": "Detalles", + "mcp": { + "invalid": "Servidor MCP no válido" } }, "chat": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index e5d9f07945..0615eb50c4 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -820,6 +820,10 @@ "devtools": "Ouvrir le panneau de débogage", "message": "Il semble que quelques problèmes soient survenus...", "reload": "Recharger" + }, + "details": "Détails", + "mcp": { + "invalid": "Serveur MCP invalide" } }, "chat": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 0c94d43538..3b8c07f7f5 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -820,6 +820,10 @@ "devtools": "Abrir o painel de depuração", "message": "Parece que ocorreu um problema...", "reload": "Recarregar" + }, + "details": "Detalhes", + "mcp": { + "invalid": "Servidor MCP inválido" } }, "chat": { diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx index 2e87204cdb..a3d63ea938 100644 --- a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx @@ -5,7 +5,9 @@ import CodeEditor from '@renderer/components/CodeEditor' import { useTimer } from '@renderer/hooks/useTimer' import { useAppDispatch } from '@renderer/store' import { setMCPServerActive } from '@renderer/store/mcp' -import { MCPServer } from '@renderer/types' +import { MCPServer, objectKeys, safeValidateMcpConfig } from '@renderer/types' +import { parseJSON } from '@renderer/utils' +import { formatZodError } from '@renderer/utils/error' import { Button, Form, Modal, Upload } from 'antd' import { FC, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -80,6 +82,49 @@ const AddMcpServerModal: FC = ({ setImportMethod(initialImportMethod) }, [initialImportMethod]) + /** + * 从JSON字符串中解析MCP服务器配置 + * @param inputValue - JSON格式的服务器配置字符串 + * @returns 包含解析后的服务器配置和可能的错误信息的对象 + * - serverToAdd: 解析成功时返回服务器配置对象,失败时返回null + * - error: 解析失败时返回错误信息,成功时返回null + */ + const getServerFromJson = ( + inputValue: string + ): { serverToAdd: Partial; error: null } | { serverToAdd: null; error: string } => { + const trimmedInput = inputValue.trim() + const parsedJson = parseJSON(trimmedInput) + if (parsedJson === null) { + logger.error('Failed to parse json.', { input: trimmedInput }) + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } + } + + const { data: validConfig, error } = safeValidateMcpConfig(parsedJson) + if (error) { + logger.error('Failed to validate json.', { parsedJson, error }) + return { serverToAdd: null, error: formatZodError(error, t('settings.mcp.addServer.importFrom.invalid')) } + } + + let serverToAdd: Partial | null = null + + if (objectKeys(validConfig.mcpServers).length > 1) { + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.error.multipleServers') } + } + + if (objectKeys(validConfig.mcpServers).length > 0) { + const key = objectKeys(validConfig.mcpServers)[0] + serverToAdd = validConfig.mcpServers[key] + if (!serverToAdd.name) { + serverToAdd.name = key + } + } else { + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } + } + + // zod 太好用了你们知道吗 + return { serverToAdd, error: null } + } + const handleOk = async () => { try { setLoading(true) @@ -194,9 +239,9 @@ const AddMcpServerModal: FC = ({ const values = await form.validateFields() const inputValue = values.serverConfig.trim() - const { serverToAdd, error } = parseAndExtractServer(inputValue, t) + const { serverToAdd, error } = getServerFromJson(inputValue) - if (error) { + if (error !== null) { form.setFields([ { name: 'serverConfig', @@ -208,11 +253,11 @@ const AddMcpServerModal: FC = ({ } // 檢查重複名稱 - if (existingServers && existingServers.some((server) => server.name === serverToAdd!.name)) { + if (existingServers && existingServers.some((server) => server.name === serverToAdd.name)) { form.setFields([ { name: 'serverConfig', - errors: [t('settings.mcp.addServer.importFrom.nameExists', { name: serverToAdd!.name })] + errors: [t('settings.mcp.addServer.importFrom.nameExists', { name: serverToAdd.name })] } ]) setLoading(false) @@ -222,9 +267,9 @@ const AddMcpServerModal: FC = ({ // 如果成功解析並通過所有檢查,立即加入伺服器(非啟用狀態)並關閉對話框 const newServer: MCPServer = { id: nanoid(), - ...serverToAdd!, - name: serverToAdd!.name || t('settings.mcp.newServer'), - baseUrl: serverToAdd!.baseUrl ?? serverToAdd!.url ?? '', + ...serverToAdd, + name: serverToAdd.name || t('settings.mcp.newServer'), + baseUrl: serverToAdd.baseUrl ?? serverToAdd.url ?? '', isActive: false // 初始狀態為非啟用 } @@ -330,93 +375,4 @@ const AddMcpServerModal: FC = ({ ) } -// 解析 JSON 提取伺服器資料 -const parseAndExtractServer = ( - inputValue: string, - t: (key: string, options?: any) => string -): { serverToAdd: Partial | null; error: string | null } => { - const trimmedInput = inputValue.trim() - - let parsedJson - try { - parsedJson = JSON.parse(trimmedInput) - } catch (e) { - // JSON 解析失敗,返回錯誤 - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } - } - - let serverToAdd: Partial | null = null - - // 檢查是否包含多個伺服器配置 (適用於 JSON 格式) - if ( - parsedJson.mcpServers && - typeof parsedJson.mcpServers === 'object' && - Object.keys(parsedJson.mcpServers).length > 1 - ) { - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.error.multipleServers') } - } else if (Array.isArray(parsedJson) && parsedJson.length > 1) { - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.error.multipleServers') } - } - - if ( - parsedJson.mcpServers && - typeof parsedJson.mcpServers === 'object' && - Object.keys(parsedJson.mcpServers).length > 0 - ) { - // Case 1: {"mcpServers": {"serverName": {...}}} - const firstServerKey = Object.keys(parsedJson.mcpServers)[0] - const potentialServer = parsedJson.mcpServers[firstServerKey] - if (typeof potentialServer === 'object' && potentialServer !== null) { - serverToAdd = { ...potentialServer } - serverToAdd!.name = potentialServer.name ?? firstServerKey - } else { - logger.error('Invalid server data under mcpServers key:', potentialServer) - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } - } - } else if (Array.isArray(parsedJson) && parsedJson.length > 0) { - // Case 2: [{...}, ...] - 取第一個伺服器,確保它是物件 - if (typeof parsedJson[0] === 'object' && parsedJson[0] !== null) { - serverToAdd = { ...parsedJson[0] } - serverToAdd!.name = parsedJson[0].name ?? t('settings.mcp.newServer') - } else { - logger.error('Invalid server data in array:', parsedJson[0]) - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } - } - } else if ( - typeof parsedJson === 'object' && - !Array.isArray(parsedJson) && - !parsedJson.mcpServers // 確保是直接的伺服器物件 - ) { - // Case 3: {...} (單一伺服器物件) - // 檢查物件是否為空 - if (Object.keys(parsedJson).length > 0) { - serverToAdd = { ...parsedJson } - serverToAdd!.name = parsedJson.name ?? t('settings.mcp.newServer') - } else { - // 空物件,視為無效 - serverToAdd = null - } - } else { - // 無效結構或空的 mcpServers - serverToAdd = null - } - - // 確保 serverToAdd 存在且 name 存在 - if (!serverToAdd || !serverToAdd.name) { - logger.error('Invalid JSON structure for server config or missing name:', parsedJson) - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } - } - - // Ensure tags is string[] - if ( - serverToAdd.tags && - (!Array.isArray(serverToAdd.tags) || !serverToAdd.tags.every((tag) => typeof tag === 'string')) - ) { - logger.error('Tags must be an array of strings:', serverToAdd.tags) - return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } - } - - return { serverToAdd, error: null } -} - export default AddMcpServerModal diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx index fd02d9ae45..c1e5f1553c 100644 --- a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx @@ -3,7 +3,9 @@ import CodeEditor from '@renderer/components/CodeEditor' import { TopView } from '@renderer/components/TopView' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setMCPServers } from '@renderer/store/mcp' -import { MCPServer } from '@renderer/types' +import { MCPServer, safeValidateMcpConfig } from '@renderer/types' +import { parseJSON } from '@renderer/utils' +import { formatZodError } from '@renderer/utils/error' import { Modal, Spin, Typography } from 'antd' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -62,15 +64,19 @@ const PopupContainer: React.FC = ({ resolve }) => { return } - const parsedConfig = JSON.parse(jsonConfig) - - if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') { + const parsedJson = parseJSON(jsonConfig) + if (parseJSON === null) { throw new Error(t('settings.mcp.addServer.importFrom.invalid')) } + const { data: parsedServers, error } = safeValidateMcpConfig(parsedJson) + if (error) { + throw new Error(formatZodError(error, t('settings.mcp.addServer.importFrom.invalid'))) + } + const serversArray: MCPServer[] = [] - for (const [id, serverConfig] of Object.entries(parsedConfig.mcpServers)) { + for (const [id, serverConfig] of Object.entries(parsedServers.mcpServers)) { const server: MCPServer = { id, isActive: false, @@ -117,13 +123,12 @@ const PopupContainer: React.FC = ({ resolve }) => { afterClose={onClose} maskClosable={false} width={800} - height="80vh" loading={jsonSaving} transitionName="animation-move-down" centered>
- - {jsonError ? {jsonError} : ''} + + {jsonError ?
{jsonError}
: ''}
{isLoading ? ( diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx index f8922d233d..518849d4f0 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx @@ -1,10 +1,15 @@ +import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { DeleteIcon } from '@renderer/components/Icons' +import GeneralPopup from '@renderer/components/Popups/GeneralPopup' import Scrollbar from '@renderer/components/Scrollbar' import { getMcpTypeLabel } from '@renderer/i18n/label' import { MCPServer } from '@renderer/types' -import { Button, Switch, Tag, Typography } from 'antd' -import { Settings2, SquareArrowOutUpRight } from 'lucide-react' -import { FC } from 'react' +import { formatErrorMessage } from '@renderer/utils/error' +import { Alert, Button, Space, Switch, Tag, Tooltip, Typography } from 'antd' +import { CircleXIcon, Settings2, SquareArrowOutUpRight } from 'lucide-react' +import { FC, useCallback } from 'react' +import { FallbackProps } from 'react-error-boundary' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' interface McpServerCardProps { @@ -26,6 +31,7 @@ const McpServerCard: FC = ({ onEdit, onOpenUrl }) => { + const { t } = useTranslation() const handleOpenUrl = (e: React.MouseEvent) => { e.stopPropagation() if (server.providerUrl) { @@ -33,61 +39,135 @@ const McpServerCard: FC = ({ } } + const Fallback = useCallback( + (props: FallbackProps) => { + const { error } = props + const errorDetails = formatErrorMessage(error) + + const ErrorDetails = () => { + return ( +
+ {errorDetails} +
+ ) + } + + const onClickDetails = (e: React.MouseEvent) => { + e.stopPropagation() + GeneralPopup.show({ content: }) + } + return ( + + {errorDetails} + + } + onClick={onClickDetails} + action={ + +