2025-10-09 11:04:36 +08:00
|
|
|
|
import React, { useState } from "react";
|
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
2025-10-09 11:30:28 +08:00
|
|
|
|
import { X, Save, AlertCircle } from "lucide-react";
|
2025-10-12 00:08:37 +08:00
|
|
|
|
import { McpServer, McpServerSpec } from "../../types";
|
2025-10-10 22:34:38 +08:00
|
|
|
|
import { mcpPresets } from "../../config/mcpPresets";
|
2025-10-09 11:04:36 +08:00
|
|
|
|
import { buttonStyles, inputStyles } from "../../lib/styles";
|
2025-10-09 11:30:28 +08:00
|
|
|
|
import McpWizardModal from "./McpWizardModal";
|
2025-10-11 16:20:12 +08:00
|
|
|
|
import {
|
|
|
|
|
|
extractErrorMessage,
|
|
|
|
|
|
translateMcpBackendError,
|
|
|
|
|
|
} from "../../utils/errorUtils";
|
2025-10-10 20:52:16 +08:00
|
|
|
|
import { AppType } from "../../lib/tauri-api";
|
2025-10-11 15:34:58 +08:00
|
|
|
|
import {
|
|
|
|
|
|
validateToml,
|
|
|
|
|
|
tomlToMcpServer,
|
|
|
|
|
|
extractIdFromToml,
|
|
|
|
|
|
mcpServerToToml,
|
|
|
|
|
|
} from "../../utils/tomlUtils";
|
2025-10-09 11:04:36 +08:00
|
|
|
|
|
|
|
|
|
|
interface McpFormModalProps {
|
2025-10-10 20:52:16 +08:00
|
|
|
|
appType: AppType;
|
2025-10-09 11:04:36 +08:00
|
|
|
|
editingId?: string;
|
|
|
|
|
|
initialData?: McpServer;
|
2025-10-09 16:44:28 +08:00
|
|
|
|
onSave: (id: string, server: McpServer) => Promise<void>;
|
2025-10-09 11:04:36 +08:00
|
|
|
|
onClose: () => void;
|
2025-10-10 11:17:40 +08:00
|
|
|
|
existingIds?: string[];
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify?: (
|
|
|
|
|
|
message: string,
|
|
|
|
|
|
type: "success" | "error",
|
|
|
|
|
|
duration?: number,
|
|
|
|
|
|
) => void;
|
2025-10-09 11:04:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-09 11:30:28 +08:00
|
|
|
|
* MCP 表单模态框组件(简化版)
|
2025-10-11 15:34:58 +08:00
|
|
|
|
* Claude: 使用 JSON 格式
|
|
|
|
|
|
* Codex: 使用 TOML 格式
|
2025-10-09 11:04:36 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const McpFormModal: React.FC<McpFormModalProps> = ({
|
2025-10-10 20:52:16 +08:00
|
|
|
|
appType,
|
2025-10-09 11:04:36 +08:00
|
|
|
|
editingId,
|
|
|
|
|
|
initialData,
|
|
|
|
|
|
onSave,
|
|
|
|
|
|
onClose,
|
2025-10-10 11:17:40 +08:00
|
|
|
|
existingIds = [],
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify,
|
2025-10-09 11:04:36 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
const { t } = useTranslation();
|
2025-10-11 15:34:58 +08:00
|
|
|
|
|
|
|
|
|
|
// JSON 基本校验(返回 i18n 文案)
|
|
|
|
|
|
const validateJson = (text: string): string => {
|
|
|
|
|
|
if (!text.trim()) return "";
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(text);
|
|
|
|
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
|
|
|
|
return t("mcp.error.jsonInvalid");
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return t("mcp.error.jsonInvalid");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 统一格式化 TOML 错误(本地化 + 详情)
|
|
|
|
|
|
const formatTomlError = (err: string): string => {
|
|
|
|
|
|
if (!err) return "";
|
|
|
|
|
|
if (err === "mustBeObject" || err === "parseError") {
|
|
|
|
|
|
return t("mcp.error.tomlInvalid");
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${t("mcp.error.tomlInvalid")}: ${err}`;
|
|
|
|
|
|
};
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const [formId, setFormId] = useState(
|
|
|
|
|
|
() => editingId || initialData?.id || "",
|
|
|
|
|
|
);
|
|
|
|
|
|
const [formName, setFormName] = useState(initialData?.name || "");
|
2025-10-09 23:13:33 +08:00
|
|
|
|
const [formDescription, setFormDescription] = useState(
|
2025-10-12 00:08:37 +08:00
|
|
|
|
initialData?.description || "",
|
2025-10-09 23:13:33 +08:00
|
|
|
|
);
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const [formHomepage, setFormHomepage] = useState(initialData?.homepage || "");
|
|
|
|
|
|
const [formDocs, setFormDocs] = useState(initialData?.docs || "");
|
|
|
|
|
|
const [formTags, setFormTags] = useState(initialData?.tags?.join(", ") || "");
|
2025-10-11 15:34:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据 appType 决定初始格式
|
|
|
|
|
|
const [formConfig, setFormConfig] = useState(() => {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const spec = initialData?.server;
|
|
|
|
|
|
if (!spec) return "";
|
2025-10-11 15:34:58 +08:00
|
|
|
|
if (appType === "codex") {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
return mcpServerToToml(spec);
|
2025-10-11 15:34:58 +08:00
|
|
|
|
}
|
2025-10-12 00:08:37 +08:00
|
|
|
|
return JSON.stringify(spec, null, 2);
|
2025-10-11 15:34:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [configError, setConfigError] = useState("");
|
2025-10-09 11:04:36 +08:00
|
|
|
|
const [saving, setSaving] = useState(false);
|
2025-10-09 11:30:28 +08:00
|
|
|
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
2025-10-10 11:17:40 +08:00
|
|
|
|
const [idError, setIdError] = useState("");
|
2025-10-09 11:04:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 编辑模式下禁止修改 ID
|
|
|
|
|
|
const isEditing = !!editingId;
|
|
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
// 判断是否使用 TOML 格式
|
|
|
|
|
|
const useToml = appType === "codex";
|
|
|
|
|
|
|
2025-10-10 22:34:38 +08:00
|
|
|
|
// 预设选择状态(仅新增模式显示;-1 表示自定义)
|
|
|
|
|
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
|
|
|
|
|
isEditing ? null : -1,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-10 11:17:40 +08:00
|
|
|
|
const handleIdChange = (value: string) => {
|
|
|
|
|
|
setFormId(value);
|
|
|
|
|
|
if (!isEditing) {
|
|
|
|
|
|
const exists = existingIds.includes(value.trim());
|
|
|
|
|
|
setIdError(exists ? t("mcp.error.idExists") : "");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-10 22:34:38 +08:00
|
|
|
|
const ensureUniqueId = (base: string): string => {
|
|
|
|
|
|
let candidate = base.trim();
|
|
|
|
|
|
if (!candidate) candidate = "mcp-server";
|
|
|
|
|
|
if (!existingIds.includes(candidate)) return candidate;
|
|
|
|
|
|
let i = 1;
|
|
|
|
|
|
while (existingIds.includes(`${candidate}-${i}`)) i++;
|
|
|
|
|
|
return `${candidate}-${i}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 应用预设(写入表单但不落库)
|
|
|
|
|
|
const applyPreset = (index: number) => {
|
|
|
|
|
|
if (index < 0 || index >= mcpPresets.length) return;
|
|
|
|
|
|
const p = mcpPresets[index];
|
|
|
|
|
|
const id = ensureUniqueId(p.id);
|
|
|
|
|
|
setFormId(id);
|
2025-10-12 00:08:37 +08:00
|
|
|
|
setFormName(p.name || p.id);
|
2025-10-10 22:34:38 +08:00
|
|
|
|
setFormDescription(p.description || "");
|
2025-10-12 00:08:37 +08:00
|
|
|
|
setFormHomepage(p.homepage || "");
|
|
|
|
|
|
setFormDocs(p.docs || "");
|
|
|
|
|
|
setFormTags(p.tags?.join(", ") || "");
|
2025-10-11 15:34:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据格式转换配置
|
|
|
|
|
|
if (useToml) {
|
|
|
|
|
|
const toml = mcpServerToToml(p.server);
|
|
|
|
|
|
setFormConfig(toml);
|
|
|
|
|
|
{
|
|
|
|
|
|
const err = validateToml(toml);
|
|
|
|
|
|
setConfigError(formatTomlError(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const json = JSON.stringify(p.server, null, 2);
|
|
|
|
|
|
setFormConfig(json);
|
|
|
|
|
|
setConfigError(validateJson(json));
|
|
|
|
|
|
}
|
2025-10-10 22:34:38 +08:00
|
|
|
|
setSelectedPreset(index);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切回自定义
|
|
|
|
|
|
const applyCustom = () => {
|
|
|
|
|
|
setSelectedPreset(-1);
|
|
|
|
|
|
// 恢复到空白模板
|
|
|
|
|
|
setFormId("");
|
2025-10-12 00:08:37 +08:00
|
|
|
|
setFormName("");
|
2025-10-10 22:34:38 +08:00
|
|
|
|
setFormDescription("");
|
2025-10-12 00:08:37 +08:00
|
|
|
|
setFormHomepage("");
|
|
|
|
|
|
setFormDocs("");
|
|
|
|
|
|
setFormTags("");
|
2025-10-11 15:34:58 +08:00
|
|
|
|
setFormConfig("");
|
|
|
|
|
|
setConfigError("");
|
2025-10-10 22:34:38 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
const handleConfigChange = (value: string) => {
|
|
|
|
|
|
setFormConfig(value);
|
2025-10-09 17:21:03 +08:00
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
if (useToml) {
|
|
|
|
|
|
// TOML 校验
|
|
|
|
|
|
const err = validateToml(value);
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
setConfigError(formatTomlError(err));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-09 17:21:03 +08:00
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
// 尝试解析并做必填字段提示
|
|
|
|
|
|
if (value.trim()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const server = tomlToMcpServer(value);
|
|
|
|
|
|
if (server.type === "stdio" && !server.command?.trim()) {
|
|
|
|
|
|
setConfigError(t("mcp.error.commandRequired"));
|
2025-10-09 17:21:03 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
if (server.type === "http" && !server.url?.trim()) {
|
|
|
|
|
|
setConfigError(t("mcp.wizard.urlRequired"));
|
2025-10-09 17:21:03 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 尝试提取 ID(如果用户还没有填写)
|
|
|
|
|
|
if (!formId.trim()) {
|
|
|
|
|
|
const extractedId = extractIdFromToml(value);
|
|
|
|
|
|
if (extractedId) {
|
|
|
|
|
|
setFormId(extractedId);
|
|
|
|
|
|
}
|
2025-10-09 17:21:03 +08:00
|
|
|
|
}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.message || String(e);
|
|
|
|
|
|
setConfigError(formatTomlError(msg));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// JSON 校验
|
|
|
|
|
|
const baseErr = validateJson(value);
|
|
|
|
|
|
if (baseErr) {
|
|
|
|
|
|
setConfigError(baseErr);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 进一步结构校验
|
|
|
|
|
|
if (value.trim()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const obj = JSON.parse(value);
|
|
|
|
|
|
if (obj && typeof obj === "object") {
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) {
|
|
|
|
|
|
setConfigError(t("mcp.error.singleServerObjectRequired"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const typ = (obj as any)?.type;
|
|
|
|
|
|
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
|
|
|
|
|
setConfigError(t("mcp.error.commandRequired"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typ === "http" && !(obj as any)?.url?.trim()) {
|
|
|
|
|
|
setConfigError(t("mcp.wizard.urlRequired"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 解析异常已在基础校验覆盖
|
2025-10-09 17:21:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
setConfigError("");
|
2025-10-09 11:30:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-11 11:43:32 +08:00
|
|
|
|
const handleWizardApply = (title: string, json: string) => {
|
|
|
|
|
|
setFormId(title);
|
2025-10-12 00:08:37 +08:00
|
|
|
|
if (!formName.trim()) {
|
|
|
|
|
|
setFormName(title);
|
|
|
|
|
|
}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
// Wizard 返回的是 JSON,根据格式决定是否需要转换
|
|
|
|
|
|
if (useToml) {
|
|
|
|
|
|
try {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const server = JSON.parse(json) as McpServerSpec;
|
2025-10-11 15:34:58 +08:00
|
|
|
|
const toml = mcpServerToToml(server);
|
|
|
|
|
|
setFormConfig(toml);
|
|
|
|
|
|
const err = validateToml(toml);
|
|
|
|
|
|
setConfigError(formatTomlError(err));
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setConfigError(t("mcp.error.jsonInvalid"));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setFormConfig(json);
|
|
|
|
|
|
setConfigError(validateJson(json));
|
|
|
|
|
|
}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const trimmedId = formId.trim();
|
|
|
|
|
|
if (!trimmedId) {
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify?.(t("mcp.error.idRequired"), "error", 3000);
|
2025-10-09 11:04:36 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-10 11:17:40 +08:00
|
|
|
|
// 新增模式:阻止提交重名 ID
|
2025-10-12 00:08:37 +08:00
|
|
|
|
if (!isEditing && existingIds.includes(trimmedId)) {
|
2025-10-10 11:17:40 +08:00
|
|
|
|
setIdError(t("mcp.error.idExists"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
// 验证配置格式
|
2025-10-12 00:08:37 +08:00
|
|
|
|
let serverSpec: McpServerSpec;
|
2025-10-09 11:04:36 +08:00
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
if (useToml) {
|
|
|
|
|
|
// TOML 模式
|
|
|
|
|
|
const tomlError = validateToml(formConfig);
|
|
|
|
|
|
setConfigError(formatTomlError(tomlError));
|
|
|
|
|
|
if (tomlError) {
|
|
|
|
|
|
onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!formConfig.trim()) {
|
|
|
|
|
|
// 空配置
|
2025-10-12 00:08:37 +08:00
|
|
|
|
serverSpec = {
|
2025-10-11 15:34:58 +08:00
|
|
|
|
type: "stdio",
|
|
|
|
|
|
command: "",
|
|
|
|
|
|
args: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
serverSpec = tomlToMcpServer(formConfig);
|
2025-10-11 15:34:58 +08:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.message || String(e);
|
|
|
|
|
|
setConfigError(formatTomlError(msg));
|
|
|
|
|
|
onNotify?.(t("mcp.error.tomlInvalid"), "error", 4000);
|
2025-10-09 17:21:03 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// JSON 模式
|
|
|
|
|
|
const jsonError = validateJson(formConfig);
|
|
|
|
|
|
setConfigError(jsonError);
|
|
|
|
|
|
if (jsonError) {
|
|
|
|
|
|
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!formConfig.trim()) {
|
|
|
|
|
|
// 空配置
|
2025-10-12 00:08:37 +08:00
|
|
|
|
serverSpec = {
|
2025-10-09 11:30:28 +08:00
|
|
|
|
type: "stdio",
|
|
|
|
|
|
command: "",
|
|
|
|
|
|
args: [],
|
|
|
|
|
|
};
|
2025-10-11 15:34:58 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
serverSpec = JSON.parse(formConfig) as McpServerSpec;
|
2025-10-11 15:34:58 +08:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setConfigError(t("mcp.error.jsonInvalid"));
|
|
|
|
|
|
onNotify?.(t("mcp.error.jsonInvalid"), "error", 4000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 前置必填校验
|
2025-10-12 00:08:37 +08:00
|
|
|
|
if (serverSpec?.type === "stdio" && !serverSpec?.command?.trim()) {
|
2025-10-11 15:34:58 +08:00
|
|
|
|
onNotify?.(t("mcp.error.commandRequired"), "error", 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-12 00:08:37 +08:00
|
|
|
|
if (serverSpec?.type === "http" && !serverSpec?.url?.trim()) {
|
2025-10-11 15:34:58 +08:00
|
|
|
|
onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
setSaving(true);
|
|
|
|
|
|
try {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const entry: McpServer = {
|
|
|
|
|
|
...(initialData ? { ...initialData } : {}),
|
|
|
|
|
|
id: trimmedId,
|
|
|
|
|
|
server: serverSpec,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 11:30:28 +08:00
|
|
|
|
if (initialData?.enabled !== undefined) {
|
2025-10-12 00:08:37 +08:00
|
|
|
|
entry.enabled = initialData.enabled;
|
|
|
|
|
|
} else if (!initialData) {
|
|
|
|
|
|
delete entry.enabled;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nameTrimmed = (formName || trimmedId).trim();
|
|
|
|
|
|
entry.name = nameTrimmed || trimmedId;
|
|
|
|
|
|
|
|
|
|
|
|
const descriptionTrimmed = formDescription.trim();
|
|
|
|
|
|
if (descriptionTrimmed) {
|
|
|
|
|
|
entry.description = descriptionTrimmed;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete entry.description;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const homepageTrimmed = formHomepage.trim();
|
|
|
|
|
|
if (homepageTrimmed) {
|
|
|
|
|
|
entry.homepage = homepageTrimmed;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete entry.homepage;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const docsTrimmed = formDocs.trim();
|
|
|
|
|
|
if (docsTrimmed) {
|
|
|
|
|
|
entry.docs = docsTrimmed;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete entry.docs;
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
|
2025-10-12 00:08:37 +08:00
|
|
|
|
const parsedTags = formTags
|
|
|
|
|
|
.split(",")
|
|
|
|
|
|
.map((tag) => tag.trim())
|
|
|
|
|
|
.filter((tag) => tag.length > 0);
|
|
|
|
|
|
if (parsedTags.length > 0) {
|
|
|
|
|
|
entry.tags = parsedTags;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete entry.tags;
|
2025-10-09 23:13:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
// 显式等待父组件保存流程
|
2025-10-12 00:08:37 +08:00
|
|
|
|
await onSave(trimmedId, entry);
|
2025-10-09 16:44:28 +08:00
|
|
|
|
} catch (error: any) {
|
2025-10-09 17:21:03 +08:00
|
|
|
|
const detail = extractErrorMessage(error);
|
2025-10-11 16:20:12 +08:00
|
|
|
|
const mapped = translateMcpBackendError(detail, t);
|
|
|
|
|
|
const msg = mapped || detail || t("mcp.error.saveFailed");
|
|
|
|
|
|
onNotify?.(msg, "error", mapped || detail ? 6000 : 4000);
|
2025-10-09 11:04:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-10 20:52:16 +08:00
|
|
|
|
const getFormTitle = () => {
|
|
|
|
|
|
if (appType === "claude") {
|
|
|
|
|
|
return isEditing ? t("mcp.editClaudeServer") : t("mcp.addClaudeServer");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return isEditing ? t("mcp.editCodexServer") : t("mcp.addCodexServer");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 11:04:36 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
|
|
|
|
|
{/* Backdrop */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Modal */}
|
2025-10-10 23:57:38 +08:00
|
|
|
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
2025-10-09 11:04:36 +08:00
|
|
|
|
{/* Header */}
|
2025-10-10 23:57:38 +08:00
|
|
|
|
<div className="flex-shrink-0 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
2025-10-09 11:04:36 +08:00
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
2025-10-10 20:52:16 +08:00
|
|
|
|
{getFormTitle()}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="p-1 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X size={18} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-10 23:57:38 +08:00
|
|
|
|
{/* Content - Scrollable */}
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
2025-10-10 22:34:38 +08:00
|
|
|
|
{/* 预设选择(仅新增时展示) */}
|
|
|
|
|
|
{!isEditing && (
|
2025-10-11 09:55:54 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
|
|
|
|
|
{t("mcp.presets.title")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={applyCustom}
|
|
|
|
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
|
|
|
|
selectedPreset === -1
|
|
|
|
|
|
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
|
|
|
|
|
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("presetSelector.custom")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{mcpPresets.map((p, idx) => (
|
2025-10-10 22:34:38 +08:00
|
|
|
|
<button
|
2025-10-11 09:55:54 +08:00
|
|
|
|
key={p.id}
|
2025-10-10 22:34:38 +08:00
|
|
|
|
type="button"
|
2025-10-11 09:55:54 +08:00
|
|
|
|
onClick={() => applyPreset(idx)}
|
2025-10-11 09:22:33 +08:00
|
|
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
2025-10-11 09:55:54 +08:00
|
|
|
|
selectedPreset === idx
|
2025-10-11 09:22:33 +08:00
|
|
|
|
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
|
|
|
|
|
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
|
|
|
|
}`}
|
2025-10-11 09:55:54 +08:00
|
|
|
|
title={p.description}
|
2025-10-10 22:34:38 +08:00
|
|
|
|
>
|
2025-10-12 00:08:37 +08:00
|
|
|
|
{p.id}
|
2025-10-10 22:34:38 +08:00
|
|
|
|
</button>
|
2025-10-11 09:55:54 +08:00
|
|
|
|
))}
|
2025-10-10 22:34:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
{/* ID (标题) */}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
<div>
|
2025-10-10 11:17:40 +08:00
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
|
|
|
|
{t("mcp.form.title")} <span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
{!isEditing && idError && (
|
|
|
|
|
|
<span className="text-xs text-red-500 dark:text-red-400">
|
|
|
|
|
|
{idError}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-09 11:04:36 +08:00
|
|
|
|
<input
|
|
|
|
|
|
className={inputStyles.text}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
placeholder={t("mcp.form.titlePlaceholder")}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
value={formId}
|
2025-10-10 11:17:40 +08:00
|
|
|
|
onChange={(e) => handleIdChange(e.target.value)}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
disabled={isEditing}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-12 00:08:37 +08:00
|
|
|
|
{/* Name */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
|
|
|
{t("mcp.form.name")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className={inputStyles.text}
|
|
|
|
|
|
placeholder={t("mcp.form.namePlaceholder")}
|
|
|
|
|
|
value={formName}
|
|
|
|
|
|
onChange={(e) => setFormName(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-09 11:30:28 +08:00
|
|
|
|
{/* Description (描述) */}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
2025-10-09 11:30:28 +08:00
|
|
|
|
{t("mcp.form.description")}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className={inputStyles.text}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
placeholder={t("mcp.form.descriptionPlaceholder")}
|
|
|
|
|
|
value={formDescription}
|
|
|
|
|
|
onChange={(e) => setFormDescription(e.target.value)}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-12 00:08:37 +08:00
|
|
|
|
{/* Tags */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
|
|
|
{t("mcp.form.tags")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className={inputStyles.text}
|
|
|
|
|
|
placeholder={t("mcp.form.tagsPlaceholder")}
|
|
|
|
|
|
value={formTags}
|
|
|
|
|
|
onChange={(e) => setFormTags(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Homepage */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
|
|
|
{t("mcp.form.homepage")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className={inputStyles.text}
|
|
|
|
|
|
placeholder={t("mcp.form.homepagePlaceholder")}
|
|
|
|
|
|
value={formHomepage}
|
|
|
|
|
|
onChange={(e) => setFormHomepage(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Docs */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
|
|
|
{t("mcp.form.docs")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className={inputStyles.text}
|
|
|
|
|
|
placeholder={t("mcp.form.docsPlaceholder")}
|
|
|
|
|
|
value={formDocs}
|
|
|
|
|
|
onChange={(e) => setFormDocs(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-11 15:34:58 +08:00
|
|
|
|
{/* 配置输入框(根据格式显示 JSON 或 TOML) */}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
<div>
|
2025-10-09 11:30:28 +08:00
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
2025-10-11 15:34:58 +08:00
|
|
|
|
{useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
</label>
|
2025-10-11 09:55:54 +08:00
|
|
|
|
{(isEditing || selectedPreset === -1) && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setIsWizardOpen(true)}
|
|
|
|
|
|
className="text-sm text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("mcp.form.useWizard")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
</div>
|
2025-10-09 11:04:36 +08:00
|
|
|
|
<textarea
|
2025-10-10 11:58:40 +08:00
|
|
|
|
className={`${inputStyles.text} h-48 resize-none font-mono text-xs`}
|
2025-10-11 15:34:58 +08:00
|
|
|
|
placeholder={
|
|
|
|
|
|
useToml
|
|
|
|
|
|
? t("mcp.form.tomlPlaceholder")
|
|
|
|
|
|
: t("mcp.form.jsonPlaceholder")
|
|
|
|
|
|
}
|
|
|
|
|
|
value={formConfig}
|
|
|
|
|
|
onChange={(e) => handleConfigChange(e.target.value)}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
/>
|
2025-10-11 15:34:58 +08:00
|
|
|
|
{configError && (
|
2025-10-09 11:30:28 +08:00
|
|
|
|
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm">
|
|
|
|
|
|
<AlertCircle size={16} />
|
2025-10-11 15:34:58 +08:00
|
|
|
|
<span>{configError}</span>
|
2025-10-09 11:30:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
2025-10-10 23:57:38 +08:00
|
|
|
|
<div className="flex-shrink-0 flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
2025-10-11 09:22:33 +08:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="px-4 py-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium"
|
|
|
|
|
|
>
|
2025-10-09 11:04:36 +08:00
|
|
|
|
{t("common.cancel")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleSubmit}
|
2025-10-10 11:17:40 +08:00
|
|
|
|
disabled={saving || (!isEditing && !!idError)}
|
2025-10-10 11:58:40 +08:00
|
|
|
|
className={`inline-flex items-center gap-2 ${buttonStyles.mcp}`}
|
2025-10-09 11:04:36 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Save size={16} />
|
|
|
|
|
|
{saving
|
|
|
|
|
|
? t("common.saving")
|
|
|
|
|
|
: isEditing
|
|
|
|
|
|
? t("common.save")
|
|
|
|
|
|
: t("common.add")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Wizard Modal */}
|
|
|
|
|
|
<McpWizardModal
|
|
|
|
|
|
isOpen={isWizardOpen}
|
|
|
|
|
|
onClose={() => setIsWizardOpen(false)}
|
|
|
|
|
|
onApply={handleWizardApply}
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify={onNotify}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
/>
|
2025-10-09 11:04:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default McpFormModal;
|