Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59644b29e6 | ||
|
|
5427ae04e4 | ||
|
|
a2aa5f8434 | ||
|
|
06010ff78e | ||
|
|
e77eab2116 | ||
|
|
ed9dd7bbc3 | ||
|
|
3d20245a80 |
26
README.md
26
README.md
@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
|
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
|
||||||
|
|
||||||
|
> **📢 重要通知**:CC Switch 即将进行大规模重构,请暂缓提交新的 PR,感谢理解与配合!
|
||||||
|
|
||||||
|
> v3.5.0 :新增 **MCP 管理**、**配置导入/导出**、**端点速度测试**功能,完善国际化覆盖,新增 Longcat、kat-coder 预设,标准化发布文件命名规范。
|
||||||
|
|
||||||
> v3.4.0 :新增 i18next 国际化(还有部分未完成)、对新模型(qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp)的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
|
> v3.4.0 :新增 i18next 国际化(还有部分未完成)、对新模型(qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp)的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
|
||||||
|
|
||||||
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。(该 VS Code 写入功能已在 v3.4.x 停用)
|
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。(该 VS Code 写入功能已在 v3.4.x 停用)
|
||||||
@@ -16,15 +20,25 @@
|
|||||||
|
|
||||||
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
||||||
|
|
||||||
## 功能特性(v3.4.0)
|
## 功能特性(v3.5.0)
|
||||||
|
|
||||||
- **国际化与语言切换**:内置 i18next,默认显示中文,可在设置中快速切换到英文,界面文文案自动实时刷新。
|
- **MCP (Model Context Protocol) 管理**:完整的 MCP 服务器配置管理系统
|
||||||
|
- 支持 stdio 和 http 服务器类型,并提供命令校验
|
||||||
|
- 内置常用 MCP 服务器模板(如 mcp-fetch 等)
|
||||||
|
- 实时启用/禁用 MCP 服务器,原子文件写入防止配置损坏
|
||||||
|
- **配置导入/导出**:备份和恢复你的供应商配置
|
||||||
|
- 一键导出所有配置到 JSON 文件
|
||||||
|
- 导入配置时自动验证并备份,自动轮换备份(保留最近 10 个)
|
||||||
|
- 带有详细状态反馈的进度模态框
|
||||||
|
- **端点速度测试**:测试 API 端点响应时间
|
||||||
|
- 测量不同供应商端点的延迟,可视化连接质量指示器
|
||||||
|
- 帮助用户选择最快的供应商
|
||||||
|
- **国际化与语言切换**:完整的 i18next 国际化覆盖,默认显示中文,可在设置中快速切换到英文,界面文案自动实时刷新。
|
||||||
- **Claude 插件同步**:内置按钮可一键应用或恢复 Claude 插件配置,切换供应商后立即生效。
|
- **Claude 插件同步**:内置按钮可一键应用或恢复 Claude 插件配置,切换供应商后立即生效。
|
||||||
- **VS Code Codex 设置停用**:由于新版 Codex 插件无需修改 `settings.json`,应用不再写入 VS Code 设置,避免潜在冲突。
|
- **供应商预设扩展**:新增 Longcat、kat-coder 等预设,更新 GLM 供应商配置至最新模型。
|
||||||
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
|
|
||||||
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
||||||
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
||||||
- **UI 与安装体验优化**:设置面板改为可滚动布局并加入保存图标,按钮宽度与状态一致性加强,Windows MSI 安装默认写入 per-user LocalAppData 并改进组件跟踪,Windows 便携版现在指向最新 release 页面,不再自动更为为安装版。
|
- **标准化发布命名**:所有平台发布文件使用一致的版本标签命名(macOS: `.tar.gz` / `.zip`,Windows: `.msi` / `-Portable.zip`,Linux: `.AppImage` / `.deb`)。
|
||||||
|
|
||||||
## 界面预览
|
## 界面预览
|
||||||
|
|
||||||
@@ -211,7 +225,7 @@ cargo test
|
|||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request!
|
欢迎提交 Issue 反馈问题和建议!
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cc-switch",
|
"name": "cc-switch",
|
||||||
"version": "3.5.0",
|
"version": "3.5.1",
|
||||||
"description": "Claude Code & Codex 供应商切换工具",
|
"description": "Claude Code & Codex 供应商切换工具",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm tauri dev",
|
"dev": "pnpm tauri dev",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 200 KiB |
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -563,7 +563,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.5.0"
|
version = "3.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.5.0"
|
version = "3.5.1"
|
||||||
description = "Claude Code & Codex 供应商配置管理工具"
|
description = "Claude Code & Codex 供应商配置管理工具"
|
||||||
authors = ["Jason Young"]
|
authors = ["Jason Young"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -841,15 +841,59 @@ pub async fn upsert_mcp_server_in_config(
|
|||||||
app: Option<String>,
|
app: Option<String>,
|
||||||
id: String,
|
id: String,
|
||||||
spec: serde_json::Value,
|
spec: serde_json::Value,
|
||||||
|
sync_other_side: Option<bool>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let mut cfg = state
|
let mut cfg = state
|
||||||
.config
|
.config
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
let app_ty = crate::app_config::AppType::from(app.as_deref().unwrap_or("claude"));
|
let app_ty = crate::app_config::AppType::from(app.as_deref().unwrap_or("claude"));
|
||||||
let changed = crate::mcp::upsert_in_config_for(&mut cfg, &app_ty, &id, spec)?;
|
let mut sync_targets: Vec<crate::app_config::AppType> = Vec::new();
|
||||||
|
|
||||||
|
let changed = crate::mcp::upsert_in_config_for(&mut cfg, &app_ty, &id, spec.clone())?;
|
||||||
|
|
||||||
|
let should_sync_current = cfg
|
||||||
|
.mcp_for(&app_ty)
|
||||||
|
.servers
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|entry| entry.get("enabled"))
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if should_sync_current {
|
||||||
|
sync_targets.push(app_ty.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if sync_other_side.unwrap_or(false) {
|
||||||
|
let other_app = match app_ty.clone() {
|
||||||
|
crate::app_config::AppType::Claude => crate::app_config::AppType::Codex,
|
||||||
|
crate::app_config::AppType::Codex => crate::app_config::AppType::Claude,
|
||||||
|
};
|
||||||
|
crate::mcp::upsert_in_config_for(&mut cfg, &other_app, &id, spec)?;
|
||||||
|
|
||||||
|
let should_sync_other = cfg
|
||||||
|
.mcp_for(&other_app)
|
||||||
|
.servers
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|entry| entry.get("enabled"))
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if should_sync_other {
|
||||||
|
sync_targets.push(other_app.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
state.save()?;
|
state.save()?;
|
||||||
|
|
||||||
|
let cfg2 = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
for app_ty_to_sync in sync_targets {
|
||||||
|
match app_ty_to_sync {
|
||||||
|
crate::app_config::AppType::Claude => crate::mcp::sync_enabled_to_claude(&cfg2)?,
|
||||||
|
crate::app_config::AppType::Codex => crate::mcp::sync_enabled_to_codex(&cfg2)?,
|
||||||
|
};
|
||||||
|
}
|
||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CC Switch",
|
"productName": "CC Switch",
|
||||||
"version": "3.5.0",
|
"version": "3.5.1",
|
||||||
"identifier": "com.ccswitch.desktop",
|
"identifier": "com.ccswitch.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useMemo, useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { X, Save, AlertCircle, ChevronDown, ChevronUp } from "lucide-react";
|
import { X, Save, AlertCircle, ChevronDown, ChevronUp, AlertTriangle } from "lucide-react";
|
||||||
import { McpServer, McpServerSpec } from "../../types";
|
import { McpServer, McpServerSpec } from "../../types";
|
||||||
import {
|
import {
|
||||||
mcpPresets,
|
mcpPresets,
|
||||||
@@ -24,7 +24,11 @@ interface McpFormModalProps {
|
|||||||
appType: AppType;
|
appType: AppType;
|
||||||
editingId?: string;
|
editingId?: string;
|
||||||
initialData?: McpServer;
|
initialData?: McpServer;
|
||||||
onSave: (id: string, server: McpServer) => Promise<void>;
|
onSave: (
|
||||||
|
id: string,
|
||||||
|
server: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
|
) => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
existingIds?: string[];
|
existingIds?: string[];
|
||||||
onNotify?: (
|
onNotify?: (
|
||||||
@@ -113,9 +117,65 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
const [idError, setIdError] = useState("");
|
const [idError, setIdError] = useState("");
|
||||||
|
const [syncOtherSide, setSyncOtherSide] = useState(false);
|
||||||
|
const [otherSideHasConflict, setOtherSideHasConflict] = useState(false);
|
||||||
|
|
||||||
// 判断是否使用 TOML 格式
|
// 判断是否使用 TOML 格式
|
||||||
const useToml = appType === "codex";
|
const useToml = appType === "codex";
|
||||||
|
const syncTargetLabel =
|
||||||
|
appType === "claude" ? t("apps.codex") : t("apps.claude");
|
||||||
|
const otherAppType: AppType = appType === "claude" ? "codex" : "claude";
|
||||||
|
const syncCheckboxId = useMemo(
|
||||||
|
() => `sync-other-side-${appType}`,
|
||||||
|
[appType],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检测另一侧是否有同名 MCP
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOtherSide = async () => {
|
||||||
|
const currentId = formId.trim();
|
||||||
|
if (!currentId) {
|
||||||
|
setOtherSideHasConflict(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const otherConfig = await window.api.getMcpConfig(otherAppType);
|
||||||
|
const hasConflict = Object.keys(otherConfig.servers || {}).includes(currentId);
|
||||||
|
setOtherSideHasConflict(hasConflict);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查另一侧 MCP 配置失败:", error);
|
||||||
|
setOtherSideHasConflict(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkOtherSide();
|
||||||
|
}, [formId, otherAppType]);
|
||||||
|
|
||||||
|
const wizardInitialSpec = useMemo(() => {
|
||||||
|
const fallback = initialData?.server;
|
||||||
|
if (!formConfig.trim()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useToml) {
|
||||||
|
try {
|
||||||
|
return tomlToMcpServer(formConfig);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(formConfig);
|
||||||
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
|
return parsed as McpServerSpec;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}, [formConfig, initialData, useToml]);
|
||||||
|
|
||||||
// 预设选择状态(仅新增模式显示;-1 表示自定义)
|
// 预设选择状态(仅新增模式显示;-1 表示自定义)
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||||
@@ -407,7 +467,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显式等待父组件保存流程
|
// 显式等待父组件保存流程
|
||||||
await onSave(trimmedId, entry);
|
await onSave(trimmedId, entry, { syncOtherSide });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const detail = extractErrorMessage(error);
|
const detail = extractErrorMessage(error);
|
||||||
const mapped = translateMcpBackendError(detail, t);
|
const mapped = translateMcpBackendError(detail, t);
|
||||||
@@ -633,25 +693,56 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<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">
|
<div className="flex-shrink-0 flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
||||||
<button
|
{/* 双端同步选项 */}
|
||||||
onClick={onClose}
|
<div className="flex items-center gap-3">
|
||||||
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"
|
<div className="flex items-center gap-2">
|
||||||
>
|
<input
|
||||||
{t("common.cancel")}
|
id={syncCheckboxId}
|
||||||
</button>
|
type="checkbox"
|
||||||
<button
|
className="h-4 w-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800"
|
||||||
onClick={handleSubmit}
|
checked={syncOtherSide}
|
||||||
disabled={saving || (!isEditing && !!idError)}
|
onChange={(event) => setSyncOtherSide(event.target.checked)}
|
||||||
className={`inline-flex items-center gap-2 ${buttonStyles.mcp}`}
|
/>
|
||||||
>
|
<label
|
||||||
<Save size={16} />
|
htmlFor={syncCheckboxId}
|
||||||
{saving
|
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
|
||||||
? t("common.saving")
|
title={t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
|
||||||
: isEditing
|
>
|
||||||
? t("common.save")
|
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
|
||||||
: t("common.add")}
|
</label>
|
||||||
</button>
|
</div>
|
||||||
|
{syncOtherSide && otherSideHasConflict && (
|
||||||
|
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
|
||||||
|
<AlertTriangle size={14} />
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{t("mcp.form.willOverwriteWarning", { target: syncTargetLabel })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={saving || (!isEditing && !!idError)}
|
||||||
|
className={`inline-flex items-center gap-2 ${buttonStyles.mcp}`}
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
|
{saving
|
||||||
|
? t("common.saving")
|
||||||
|
: isEditing
|
||||||
|
? t("common.save")
|
||||||
|
: t("common.add")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -661,6 +752,8 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
onClose={() => setIsWizardOpen(false)}
|
onClose={() => setIsWizardOpen(false)}
|
||||||
onApply={handleWizardApply}
|
onApply={handleWizardApply}
|
||||||
onNotify={onNotify}
|
onNotify={onNotify}
|
||||||
|
initialTitle={formId}
|
||||||
|
initialServer={wizardInitialSpec}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -135,10 +135,16 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (id: string, server: McpServer) => {
|
const handleSave = async (
|
||||||
|
id: string,
|
||||||
|
server: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const payload: McpServer = { ...server, id };
|
const payload: McpServer = { ...server, id };
|
||||||
await window.api.upsertMcpServerInConfig(appType, id, payload);
|
await window.api.upsertMcpServerInConfig(appType, id, payload, {
|
||||||
|
syncOtherSide: options?.syncOtherSide,
|
||||||
|
});
|
||||||
await reload();
|
await reload();
|
||||||
setIsFormOpen(false);
|
setIsFormOpen(false);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { X, Save } from "lucide-react";
|
import { X, Save } from "lucide-react";
|
||||||
import { McpServerSpec } from "../../types";
|
import { McpServerSpec } from "../../types";
|
||||||
@@ -13,6 +13,8 @@ interface McpWizardModalProps {
|
|||||||
type: "success" | "error",
|
type: "success" | "error",
|
||||||
duration?: number,
|
duration?: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
initialTitle?: string;
|
||||||
|
initialServer?: McpServerSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +74,8 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onApply,
|
onApply,
|
||||||
onNotify,
|
onNotify,
|
||||||
|
initialTitle,
|
||||||
|
initialServer,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio");
|
const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio");
|
||||||
@@ -162,6 +166,55 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const title = initialTitle ?? "";
|
||||||
|
setWizardTitle(title);
|
||||||
|
|
||||||
|
const resolvedType =
|
||||||
|
initialServer?.type ??
|
||||||
|
(initialServer?.url ? "http" : "stdio");
|
||||||
|
|
||||||
|
setWizardType(resolvedType);
|
||||||
|
|
||||||
|
if (resolvedType === "http") {
|
||||||
|
setWizardUrl(initialServer?.url ?? "");
|
||||||
|
const headersCandidate = initialServer?.headers;
|
||||||
|
const headers =
|
||||||
|
headersCandidate && typeof headersCandidate === "object"
|
||||||
|
? headersCandidate
|
||||||
|
: undefined;
|
||||||
|
setWizardHeaders(
|
||||||
|
headers
|
||||||
|
? Object.entries(headers)
|
||||||
|
.map(([k, v]) => `${k}: ${v ?? ""}`)
|
||||||
|
.join("\n")
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
setWizardCommand("");
|
||||||
|
setWizardArgs("");
|
||||||
|
setWizardEnv("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWizardCommand(initialServer?.command ?? "");
|
||||||
|
const argsValue = initialServer?.args;
|
||||||
|
setWizardArgs(Array.isArray(argsValue) ? argsValue.join("\n") : "");
|
||||||
|
const envCandidate = initialServer?.env;
|
||||||
|
const env =
|
||||||
|
envCandidate && typeof envCandidate === "object" ? envCandidate : undefined;
|
||||||
|
setWizardEnv(
|
||||||
|
env
|
||||||
|
? Object.entries(env)
|
||||||
|
.map(([k, v]) => `${k}=${v ?? ""}`)
|
||||||
|
.join("\n")
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
setWizardUrl("");
|
||||||
|
setWizardHeaders("");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const preview = generatePreview();
|
const preview = generatePreview();
|
||||||
|
|||||||
@@ -298,7 +298,10 @@
|
|||||||
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
"tomlConfig": "TOML Configuration",
|
"tomlConfig": "TOML Configuration",
|
||||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
"useWizard": "Config Wizard"
|
"useWizard": "Config Wizard",
|
||||||
|
"syncOtherSide": "Mirror to {{target}}",
|
||||||
|
"syncOtherSideHint": "Apply the same settings to {{target}}; existing entries with the same id will be overwritten.",
|
||||||
|
"willOverwriteWarning": "Will overwrite existing config in {{target}}"
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "MCP Configuration Wizard",
|
"title": "MCP Configuration Wizard",
|
||||||
|
|||||||
@@ -298,7 +298,10 @@
|
|||||||
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
"tomlConfig": "TOML 配置",
|
"tomlConfig": "TOML 配置",
|
||||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
"useWizard": "配置向导"
|
"useWizard": "配置向导",
|
||||||
|
"syncOtherSide": "同步到 {{target}}",
|
||||||
|
"syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖",
|
||||||
|
"willOverwriteWarning": "将覆盖 {{target}} 中的同名配置"
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "MCP 配置向导",
|
"title": "MCP 配置向导",
|
||||||
|
|||||||
@@ -354,13 +354,18 @@ export const tauriAPI = {
|
|||||||
app: AppType = "claude",
|
app: AppType = "claude",
|
||||||
id: string,
|
id: string,
|
||||||
spec: McpServer,
|
spec: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke<boolean>("upsert_mcp_server_in_config", {
|
const payload = {
|
||||||
app,
|
app,
|
||||||
id,
|
id,
|
||||||
spec,
|
spec,
|
||||||
});
|
...(options?.syncOtherSide !== undefined
|
||||||
|
? { syncOtherSide: options.syncOtherSide }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
return await invoke<boolean>("upsert_mcp_server_in_config", payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("写入 MCP(config.json)失败:", error);
|
console.error("写入 MCP(config.json)失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -84,6 +84,7 @@ declare global {
|
|||||||
app: AppType | undefined,
|
app: AppType | undefined,
|
||||||
id: string,
|
id: string,
|
||||||
spec: McpServer,
|
spec: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
deleteMcpServerInConfig: (
|
deleteMcpServerInConfig: (
|
||||||
app: AppType | undefined,
|
app: AppType | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user