feat(provider): use live config for edit and backfill SSOT after switch
- Edit modal (Claude+Codex): when editing the current provider, initialize form from live files (Claude: ~/.claude/settings.json; Codex: ~/.codex/auth.json + ~/.codex/config.toml) instead of SSOT. - Switch (Claude): after writing live settings.json for the target provider, read it back and update the provider’s SSOT to match live. - Switch (Codex): keep MCP sync to config.toml, then read back TOML and update the target provider’s SSOT (preserves mcp.servers/mcp_servers schema). - Add Tauri command read_live_provider_settings for both apps, register handler, and expose window.api.getLiveProviderSettings. - Types updated accordingly; cargo check and pnpm typecheck pass.
This commit is contained in:
@@ -420,6 +420,18 @@ pub async fn switch_provider(
|
|||||||
|
|
||||||
// 不做归档,直接写入
|
// 不做归档,直接写入
|
||||||
write_json_file(&settings_path, &provider.settings_config)?;
|
write_json_file(&settings_path, &provider.settings_config)?;
|
||||||
|
|
||||||
|
// 写入后回读 live,并回填到目标供应商的 SSOT,保证一致
|
||||||
|
if settings_path.exists() {
|
||||||
|
if let Ok(live_after) = read_json_file::<serde_json::Value>(&settings_path) {
|
||||||
|
let m = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
if let Some(target) = m.providers.get_mut(&id) {
|
||||||
|
target.settings_config = live_after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,9 +443,32 @@ pub async fn switch_provider(
|
|||||||
manager.current = id;
|
manager.current = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对 Codex:切换完成且释放可变借用后,再依据 SSOT 同步 MCP 到 config.toml
|
// 对 Codex:切换完成后,同步 MCP 到 config.toml,并将最新的 config.toml 回填到当前供应商 settings_config.config
|
||||||
if let AppType::Codex = app_type {
|
if let AppType::Codex = app_type {
|
||||||
|
// 1) 依据 SSOT 将启用的 MCP 投影到 ~/.codex/config.toml
|
||||||
crate::mcp::sync_enabled_to_codex(&config)?;
|
crate::mcp::sync_enabled_to_codex(&config)?;
|
||||||
|
|
||||||
|
// 2) 读取投影后的 live config.toml 文本
|
||||||
|
let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||||
|
|
||||||
|
// 3) 回填到当前(目标)供应商的 settings_config.config,确保编辑面板读取到最新 MCP
|
||||||
|
let cur_id = {
|
||||||
|
let m = config
|
||||||
|
.get_manager(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
m.current.clone()
|
||||||
|
};
|
||||||
|
let m = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
if let Some(p) = m.providers.get_mut(&cur_id) {
|
||||||
|
if let Some(obj) = p.settings_config.as_object_mut() {
|
||||||
|
obj.insert(
|
||||||
|
"config".to_string(),
|
||||||
|
serde_json::Value::String(cfg_text_after),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("成功切换到供应商: {}", provider.name);
|
log::info!("成功切换到供应商: {}", provider.name);
|
||||||
@@ -874,6 +909,41 @@ pub async fn import_mcp_from_codex(state: State<'_, AppState>) -> Result<usize,
|
|||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 读取当前生效(live)的配置内容,返回可直接作为 provider.settings_config 的对象
|
||||||
|
/// - Codex: 返回 { auth: JSON, config: string }
|
||||||
|
/// - Claude: 返回 settings.json 的 JSON 内容
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_live_provider_settings(
|
||||||
|
app_type: Option<AppType>,
|
||||||
|
app: Option<String>,
|
||||||
|
appType: Option<String>,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
let app_type = app_type
|
||||||
|
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||||
|
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||||
|
.unwrap_or(AppType::Claude);
|
||||||
|
|
||||||
|
match app_type {
|
||||||
|
AppType::Codex => {
|
||||||
|
let auth_path = crate::codex_config::get_codex_auth_path();
|
||||||
|
if !auth_path.exists() {
|
||||||
|
return Err("Codex 配置文件不存在:缺少 auth.json".to_string());
|
||||||
|
}
|
||||||
|
let auth: serde_json::Value = crate::config::read_json_file(&auth_path)?;
|
||||||
|
let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||||
|
Ok(serde_json::json!({ "auth": auth, "config": cfg_text }))
|
||||||
|
}
|
||||||
|
AppType::Claude => {
|
||||||
|
let path = crate::config::get_claude_settings_path();
|
||||||
|
if !path.exists() {
|
||||||
|
return Err("Claude Code 配置文件不存在".to_string());
|
||||||
|
}
|
||||||
|
let v: serde_json::Value = crate::config::read_json_file(&path)?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取设置
|
/// 获取设置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_settings() -> Result<crate::settings::AppSettings, String> {
|
pub async fn get_settings() -> Result<crate::settings::AppSettings, String> {
|
||||||
|
|||||||
@@ -415,6 +415,7 @@ pub fn run() {
|
|||||||
commands::open_external,
|
commands::open_external,
|
||||||
commands::get_app_config_path,
|
commands::get_app_config_path,
|
||||||
commands::open_app_config_folder,
|
commands::open_app_config_folder,
|
||||||
|
commands::read_live_provider_settings,
|
||||||
commands::get_settings,
|
commands::get_settings,
|
||||||
commands::save_settings,
|
commands::save_settings,
|
||||||
commands::check_for_updates,
|
commands::check_for_updates,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
@@ -18,6 +18,31 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [effectiveProvider, setEffectiveProvider] = useState<Provider>(provider);
|
||||||
|
|
||||||
|
// 若为当前应用且正在编辑“当前供应商”,则优先读取 live 配置作为初始值(Claude/Codex 均适用)
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
const maybeLoadLive = async () => {
|
||||||
|
try {
|
||||||
|
const currentId = await window.api.getCurrentProvider(appType);
|
||||||
|
if (currentId && currentId === provider.id) {
|
||||||
|
const live = await window.api.getLiveProviderSettings(appType);
|
||||||
|
if (!mounted) return;
|
||||||
|
setEffectiveProvider({ ...provider, settingsConfig: live });
|
||||||
|
} else {
|
||||||
|
setEffectiveProvider(provider);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 读取失败则回退到原 provider
|
||||||
|
setEffectiveProvider(provider);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
maybeLoadLive();
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [appType, provider]);
|
||||||
|
|
||||||
const handleSubmit = (data: Omit<Provider, "id">) => {
|
const handleSubmit = (data: Omit<Provider, "id">) => {
|
||||||
onSave({
|
onSave({
|
||||||
@@ -31,7 +56,7 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
|||||||
appType={appType}
|
appType={appType}
|
||||||
title={t("common.edit")}
|
title={t("common.edit")}
|
||||||
submitText={t("common.save")}
|
submitText={t("common.save")}
|
||||||
initialData={provider}
|
initialData={effectiveProvider}
|
||||||
showPresets={false}
|
showPresets={false}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
|||||||
@@ -429,6 +429,22 @@ export const tauriAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 读取当前生效(live)的 provider settings(根据 appType)
|
||||||
|
// Codex: { auth: object, config: string }
|
||||||
|
// Claude: settings.json 内容
|
||||||
|
getLiveProviderSettings: async (app?: AppType): Promise<any> => {
|
||||||
|
try {
|
||||||
|
return await invoke<any>("read_live_provider_settings", {
|
||||||
|
app_type: app,
|
||||||
|
app,
|
||||||
|
appType: app,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("读取 live 配置失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ours: 第三方/自定义供应商——测速与端点管理
|
// ours: 第三方/自定义供应商——测速与端点管理
|
||||||
// 第三方/自定义供应商:批量测试端点延迟
|
// 第三方/自定义供应商:批量测试端点延迟
|
||||||
testApiEndpoints: async (
|
testApiEndpoints: async (
|
||||||
|
|||||||
4
src/vite-env.d.ts
vendored
4
src/vite-env.d.ts
vendored
@@ -96,6 +96,10 @@ declare global {
|
|||||||
syncEnabledMcpToCodex: () => Promise<boolean>;
|
syncEnabledMcpToCodex: () => Promise<boolean>;
|
||||||
importMcpFromClaude: () => Promise<number>;
|
importMcpFromClaude: () => Promise<number>;
|
||||||
importMcpFromCodex: () => Promise<number>;
|
importMcpFromCodex: () => Promise<number>;
|
||||||
|
// 读取当前生效(live)的 provider settings(根据 appType)
|
||||||
|
// Codex: { auth: object, config: string }
|
||||||
|
// Claude: settings.json 内容
|
||||||
|
getLiveProviderSettings: (app?: AppType) => Promise<any>;
|
||||||
testApiEndpoints: (
|
testApiEndpoints: (
|
||||||
urls: string[],
|
urls: string[],
|
||||||
options?: { timeoutSecs?: number },
|
options?: { timeoutSecs?: number },
|
||||||
|
|||||||
Reference in New Issue
Block a user