diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index d12104d..1c83380 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -389,7 +389,11 @@ export default function SettingsModal({ const filePath = await window.api.saveFileDialog(defaultName); if (!filePath) { - onNotify?.(`${t("settings.exportFailed")}: ${t("settings.selectFileFailed")}`, "error", 4000); + onNotify?.( + `${t("settings.exportFailed")}: ${t("settings.selectFileFailed")}`, + "error", + 4000, + ); return; } diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 445ea63..25d540f 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -5,7 +5,10 @@ import { McpServer } from "../../types"; import { mcpPresets } from "../../config/mcpPresets"; import { buttonStyles, inputStyles } from "../../lib/styles"; import McpWizardModal from "./McpWizardModal"; -import { extractErrorMessage } from "../../utils/errorUtils"; +import { + extractErrorMessage, + translateMcpBackendError, +} from "../../utils/errorUtils"; import { AppType } from "../../lib/tauri-api"; import { validateToml, @@ -335,8 +338,9 @@ const McpFormModal: React.FC = ({ await onSave(formId.trim(), server); } catch (error: any) { const detail = extractErrorMessage(error); - const msg = detail || t("mcp.error.saveFailed"); - onNotify?.(msg, "error", detail ? 6000 : 4000); + const mapped = translateMcpBackendError(detail, t); + const msg = mapped || detail || t("mcp.error.saveFailed"); + onNotify?.(msg, "error", mapped || detail ? 6000 : 4000); } finally { setSaving(false); } diff --git a/src/components/mcp/McpPanel.tsx b/src/components/mcp/McpPanel.tsx index ec0ab44..48343ae 100644 --- a/src/components/mcp/McpPanel.tsx +++ b/src/components/mcp/McpPanel.tsx @@ -5,7 +5,10 @@ import { McpServer } from "../../types"; import McpListItem from "./McpListItem"; import McpFormModal from "./McpFormModal"; import { ConfirmDialog } from "../ConfirmDialog"; -import { extractErrorMessage } from "../../utils/errorUtils"; +import { + extractErrorMessage, + translateMcpBackendError, +} from "../../utils/errorUtils"; // 预设相关逻辑已迁移到“新增 MCP”面板,列表此处无需引用 import { buttonStyles } from "../../lib/styles"; import { AppType } from "../../lib/tauri-api"; @@ -89,10 +92,11 @@ const McpPanel: React.FC = ({ onClose, onNotify, appType }) => { // 失败时回滚 setServers(previousServers); const detail = extractErrorMessage(e); + const mapped = translateMcpBackendError(detail, t); onNotify?.( - detail || t("mcp.error.saveFailed"), + mapped || detail || t("mcp.error.saveFailed"), "error", - detail ? 6000 : 5000, + mapped || detail ? 6000 : 5000, ); } }; @@ -120,10 +124,11 @@ const McpPanel: React.FC = ({ onClose, onNotify, appType }) => { onNotify?.(t("mcp.msg.deleted"), "success", 1500); } catch (e: any) { const detail = extractErrorMessage(e); + const mapped = translateMcpBackendError(detail, t); onNotify?.( - detail || t("mcp.error.deleteFailed"), + mapped || detail || t("mcp.error.deleteFailed"), "error", - detail ? 6000 : 5000, + mapped || detail ? 6000 : 5000, ); } }, @@ -139,10 +144,11 @@ const McpPanel: React.FC = ({ onClose, onNotify, appType }) => { onNotify?.(t("mcp.msg.saved"), "success", 1500); } catch (e: any) { const detail = extractErrorMessage(e); + const mapped = translateMcpBackendError(detail, t); onNotify?.( - detail || t("mcp.error.saveFailed"), + mapped || detail || t("mcp.error.saveFailed"), "error", - detail ? 6000 : 5000, + mapped || detail ? 6000 : 5000, ); // 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡) throw e; diff --git a/src/components/mcp/McpWizardModal.tsx b/src/components/mcp/McpWizardModal.tsx index 6e37a53..beeb520 100644 --- a/src/components/mcp/McpWizardModal.tsx +++ b/src/components/mcp/McpWizardModal.tsx @@ -227,7 +227,7 @@ const McpWizardModal: React.FC = ({ className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2" /> - stdio + {t("mcp.wizard.typeStdio")} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c463e45..223cd9b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -293,6 +293,8 @@ "title": "MCP Configuration Wizard", "hint": "Quickly configure MCP server and auto-generate JSON configuration", "type": "Type", + "typeStdio": "stdio", + "typeHttp": "http", "command": "Command", "commandPlaceholder": "npx or uvx", "args": "Arguments", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index faa9891..8aecb96 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -293,6 +293,8 @@ "title": "MCP 配置向导", "hint": "快速配置 MCP 服务器,自动生成 JSON 配置", "type": "类型", + "typeStdio": "stdio", + "typeHttp": "http", "command": "命令", "commandPlaceholder": "npx 或 uvx", "args": "参数", diff --git a/src/utils/errorUtils.ts b/src/utils/errorUtils.ts index 43cb23c..fbcba07 100644 --- a/src/utils/errorUtils.ts +++ b/src/utils/errorUtils.ts @@ -36,3 +36,62 @@ export const extractErrorMessage = (error: unknown): string => { return ""; }; + +/** + * 将已知的 MCP 相关后端错误(通常为中文硬编码)映射为 i18n 文案 + * 采用包含式匹配,尽量稳健地覆盖不同上下文的相似消息。 + * 若无法识别,返回空字符串以便调用方回退到原始 detail 或默认 i18n。 + */ +export const translateMcpBackendError = ( + message: string, + t: (key: string, opts?: any) => string, +): string => { + if (!message) return ""; + const msg = String(message).trim(); + + // 基础字段与结构校验相关 + if (msg.includes("MCP 服务器 ID 不能为空")) { + return t("mcp.error.idRequired"); + } + if ( + msg.includes("MCP 服务器定义必须为 JSON 对象") || + msg.includes("MCP 服务器 '" /* 不是对象 */) || + msg.includes("不是对象") || + msg.includes("服务器配置必须是对象") + ) { + return t("mcp.error.jsonInvalid"); + } + if (msg.includes("MCP 服务器 type 必须是")) { + return t("mcp.error.jsonInvalid"); + } + + // 必填字段 + if ( + msg.includes("stdio 类型的 MCP 服务器缺少 command 字段") || + msg.includes("必须包含 command 字段") + ) { + return t("mcp.error.commandRequired"); + } + if ( + msg.includes("http 类型的 MCP 服务器缺少 url 字段") || + msg.includes("必须包含 url 字段") || + msg === "URL 不能为空" + ) { + return t("mcp.wizard.urlRequired"); + } + + // 文件解析/序列化 + if ( + msg.includes("解析 ~/.claude.json 失败") || + msg.includes("解析 config.toml 失败") || + msg.includes("无法识别的 TOML 格式") || + msg.includes("TOML 内容不能为空") + ) { + return t("mcp.error.tomlInvalid"); + } + if (msg.includes("序列化 config.toml 失败")) { + return t("mcp.error.tomlInvalid"); + } + + return ""; +};