fix(mcp): improve error message internationalization
- Add translateMcpBackendError utility to map backend errors to i18n keys - Update error handling in McpPanel, McpFormModal, and McpWizardModal - Internationalize stdio/http type selectors in Wizard - Implement three-tier fallback strategy: translation → raw error → default message - No backend changes required, fully frontend-based i18n implementation
This commit is contained in:
@@ -389,7 +389,11 @@ export default function SettingsModal({
|
|||||||
const filePath = await window.api.saveFileDialog(defaultName);
|
const filePath = await window.api.saveFileDialog(defaultName);
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
onNotify?.(`${t("settings.exportFailed")}: ${t("settings.selectFileFailed")}`, "error", 4000);
|
onNotify?.(
|
||||||
|
`${t("settings.exportFailed")}: ${t("settings.selectFileFailed")}`,
|
||||||
|
"error",
|
||||||
|
4000,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { McpServer } from "../../types";
|
|||||||
import { mcpPresets } from "../../config/mcpPresets";
|
import { mcpPresets } from "../../config/mcpPresets";
|
||||||
import { buttonStyles, inputStyles } from "../../lib/styles";
|
import { buttonStyles, inputStyles } from "../../lib/styles";
|
||||||
import McpWizardModal from "./McpWizardModal";
|
import McpWizardModal from "./McpWizardModal";
|
||||||
import { extractErrorMessage } from "../../utils/errorUtils";
|
import {
|
||||||
|
extractErrorMessage,
|
||||||
|
translateMcpBackendError,
|
||||||
|
} from "../../utils/errorUtils";
|
||||||
import { AppType } from "../../lib/tauri-api";
|
import { AppType } from "../../lib/tauri-api";
|
||||||
import {
|
import {
|
||||||
validateToml,
|
validateToml,
|
||||||
@@ -335,8 +338,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
await onSave(formId.trim(), server);
|
await onSave(formId.trim(), server);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const detail = extractErrorMessage(error);
|
const detail = extractErrorMessage(error);
|
||||||
const msg = detail || t("mcp.error.saveFailed");
|
const mapped = translateMcpBackendError(detail, t);
|
||||||
onNotify?.(msg, "error", detail ? 6000 : 4000);
|
const msg = mapped || detail || t("mcp.error.saveFailed");
|
||||||
|
onNotify?.(msg, "error", mapped || detail ? 6000 : 4000);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { McpServer } from "../../types";
|
|||||||
import McpListItem from "./McpListItem";
|
import McpListItem from "./McpListItem";
|
||||||
import McpFormModal from "./McpFormModal";
|
import McpFormModal from "./McpFormModal";
|
||||||
import { ConfirmDialog } from "../ConfirmDialog";
|
import { ConfirmDialog } from "../ConfirmDialog";
|
||||||
import { extractErrorMessage } from "../../utils/errorUtils";
|
import {
|
||||||
|
extractErrorMessage,
|
||||||
|
translateMcpBackendError,
|
||||||
|
} from "../../utils/errorUtils";
|
||||||
// 预设相关逻辑已迁移到“新增 MCP”面板,列表此处无需引用
|
// 预设相关逻辑已迁移到“新增 MCP”面板,列表此处无需引用
|
||||||
import { buttonStyles } from "../../lib/styles";
|
import { buttonStyles } from "../../lib/styles";
|
||||||
import { AppType } from "../../lib/tauri-api";
|
import { AppType } from "../../lib/tauri-api";
|
||||||
@@ -89,10 +92,11 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
|||||||
// 失败时回滚
|
// 失败时回滚
|
||||||
setServers(previousServers);
|
setServers(previousServers);
|
||||||
const detail = extractErrorMessage(e);
|
const detail = extractErrorMessage(e);
|
||||||
|
const mapped = translateMcpBackendError(detail, t);
|
||||||
onNotify?.(
|
onNotify?.(
|
||||||
detail || t("mcp.error.saveFailed"),
|
mapped || detail || t("mcp.error.saveFailed"),
|
||||||
"error",
|
"error",
|
||||||
detail ? 6000 : 5000,
|
mapped || detail ? 6000 : 5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -120,10 +124,11 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
|||||||
onNotify?.(t("mcp.msg.deleted"), "success", 1500);
|
onNotify?.(t("mcp.msg.deleted"), "success", 1500);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const detail = extractErrorMessage(e);
|
const detail = extractErrorMessage(e);
|
||||||
|
const mapped = translateMcpBackendError(detail, t);
|
||||||
onNotify?.(
|
onNotify?.(
|
||||||
detail || t("mcp.error.deleteFailed"),
|
mapped || detail || t("mcp.error.deleteFailed"),
|
||||||
"error",
|
"error",
|
||||||
detail ? 6000 : 5000,
|
mapped || detail ? 6000 : 5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -139,10 +144,11 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
|||||||
onNotify?.(t("mcp.msg.saved"), "success", 1500);
|
onNotify?.(t("mcp.msg.saved"), "success", 1500);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const detail = extractErrorMessage(e);
|
const detail = extractErrorMessage(e);
|
||||||
|
const mapped = translateMcpBackendError(detail, t);
|
||||||
onNotify?.(
|
onNotify?.(
|
||||||
detail || t("mcp.error.saveFailed"),
|
mapped || detail || t("mcp.error.saveFailed"),
|
||||||
"error",
|
"error",
|
||||||
detail ? 6000 : 5000,
|
mapped || detail ? 6000 : 5000,
|
||||||
);
|
);
|
||||||
// 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡)
|
// 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡)
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||||
stdio
|
{t("mcp.wizard.typeStdio")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||||
@@ -241,7 +241,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||||
http
|
{t("mcp.wizard.typeHttp")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -293,6 +293,8 @@
|
|||||||
"title": "MCP Configuration Wizard",
|
"title": "MCP Configuration Wizard",
|
||||||
"hint": "Quickly configure MCP server and auto-generate JSON configuration",
|
"hint": "Quickly configure MCP server and auto-generate JSON configuration",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
"typeStdio": "stdio",
|
||||||
|
"typeHttp": "http",
|
||||||
"command": "Command",
|
"command": "Command",
|
||||||
"commandPlaceholder": "npx or uvx",
|
"commandPlaceholder": "npx or uvx",
|
||||||
"args": "Arguments",
|
"args": "Arguments",
|
||||||
|
|||||||
@@ -293,6 +293,8 @@
|
|||||||
"title": "MCP 配置向导",
|
"title": "MCP 配置向导",
|
||||||
"hint": "快速配置 MCP 服务器,自动生成 JSON 配置",
|
"hint": "快速配置 MCP 服务器,自动生成 JSON 配置",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
|
"typeStdio": "stdio",
|
||||||
|
"typeHttp": "http",
|
||||||
"command": "命令",
|
"command": "命令",
|
||||||
"commandPlaceholder": "npx 或 uvx",
|
"commandPlaceholder": "npx 或 uvx",
|
||||||
"args": "参数",
|
"args": "参数",
|
||||||
|
|||||||
@@ -36,3 +36,62 @@ export const extractErrorMessage = (error: unknown): string => {
|
|||||||
|
|
||||||
return "";
|
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 "";
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user