diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 8369f85..c33570a 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -9,6 +9,7 @@ use crate::codex_config; use crate::config::{get_claude_settings_path, ConfigStatus}; use crate::provider::Provider; use crate::store::AppState; +use crate::vscode_config; fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> { match app_type { @@ -41,6 +42,44 @@ fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result Ok(()) } +fn extract_base_url_from_toml(cfg_text: &str) -> Result, String> { + if cfg_text.trim().is_empty() { + return Ok(None); + } + + let value: toml::Value = + toml::from_str(cfg_text).map_err(|e| format!("解析 config.toml 失败: {}", e))?; + + fn walk(value: &toml::Value) -> Option { + match value { + toml::Value::Table(table) => { + if let Some(toml::Value::String(v)) = table.get("base_url") { + if !v.trim().is_empty() { + return Some(v.clone()); + } + } + for item in table.values() { + if let Some(found) = walk(item) { + return Some(found); + } + } + None + } + toml::Value::Array(arr) => { + for item in arr { + if let Some(found) = walk(item) { + return Some(found); + } + } + None + } + _ => None, + } + } + + Ok(walk(&value)) +} + /// 获取所有供应商 #[tauri::command] pub async fn get_providers( @@ -360,6 +399,26 @@ pub async fn switch_provider( .get("config") .and_then(|v| v.as_str()); crate::codex_config::write_codex_live_atomic(auth, cfg_text)?; + + let is_official = provider + .category + .as_ref() + .map(|c| c == "official") + .unwrap_or(false); + + if is_official { + vscode_config::write_vscode_settings(None)?; + } else { + let cfg_text = cfg_text.unwrap_or_default(); + match extract_base_url_from_toml(cfg_text)? { + Some(base_url) => { + vscode_config::write_vscode_settings(Some(&base_url))?; + } + None => { + return Err("目标 Codex 配置缺少 base_url 字段".to_string()); + } + } + } } AppType::Claude => { use crate::config::{read_json_file, write_json_file}; @@ -569,6 +628,17 @@ pub async fn open_external(app: tauri::AppHandle, url: String) -> Result, + baseUrl: Option, +) -> Result { + let payload = base_url.or(baseUrl); + vscode_config::write_vscode_settings(payload.as_deref())?; + Ok(true) +} + /// 获取应用配置文件路径 #[tauri::command] pub async fn get_app_config_path() -> Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2dd5dd6..1b2269a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,7 @@ mod config; mod migration; mod provider; mod store; +mod vscode_config; use store::AppState; #[cfg(target_os = "macos")] @@ -352,6 +353,7 @@ pub fn run() { commands::get_claude_code_config_path, commands::open_config_folder, commands::open_external, + commands::write_vscode_settings_command, commands::get_app_config_path, commands::open_app_config_folder, commands::get_settings, diff --git a/src-tauri/src/vscode_config.rs b/src-tauri/src/vscode_config.rs new file mode 100644 index 0000000..5d5b484 --- /dev/null +++ b/src-tauri/src/vscode_config.rs @@ -0,0 +1,115 @@ +use serde_json::{Map, Value}; +use std::path::{Path, PathBuf}; + +use crate::config::write_json_file; + +/// VS Code 默认用户配置子目录 +const MAC_CODE_USER_DIR: &str = "Library/Application Support/Code/User"; + +/// 解析 VS Code 用户 settings.json 路径 +pub fn get_vscode_settings_path() -> PathBuf { + #[cfg(target_os = "macos")] + { + return dirs::home_dir() + .expect("无法获取用户主目录") + .join(MAC_CODE_USER_DIR) + .join("settings.json"); + } + + #[cfg(target_os = "linux")] + { + return dirs::home_dir() + .expect("无法获取用户主目录") + .join(".config/Code/User") + .join("settings.json"); + } + + #[cfg(target_os = "windows")] + { + if let Some(data_dir) = dirs::data_dir() { + return data_dir.join("Code/User").join("settings.json"); + } + return dirs::home_dir() + .expect("无法获取用户主目录") + .join("AppData/Roaming") + .join("Code/User") + .join("settings.json"); + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + { + dirs::home_dir() + .expect("无法获取用户主目录") + .join(".config/Code/User") + .join("settings.json") + } +} + +fn load_settings(path: &Path) -> Result, String> { + if !path.exists() { + return Ok(Map::new()); + } + + let content = + std::fs::read_to_string(path).map_err(|e| format!("读取 VS Code 设置失败: {}", e))?; + + if content.trim().is_empty() { + return Ok(Map::new()); + } + + match serde_json::from_str::(&content) { + Ok(Value::Object(obj)) => Ok(obj), + Ok(_) => Err("VS Code settings.json 必须为 JSON 对象".to_string()), + Err(err) => Err(format!("解析 VS Code settings.json 失败: {}", err)), + } +} + +fn persist_settings(path: &Path, map: Map) -> Result<(), String> { + let value = Value::Object(map); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("创建 VS Code 配置目录失败: {}", e))?; + } + write_json_file(path, &value) +} + +/// 写入或移除 chatgpt 相关 VS Code 配置 +/// +/// - `base_url` 为 Some 时更新/覆盖 `"chatgpt.apiBase"` 与 `"chatgpt.config"` +/// - `base_url` 为 None 时删除上述字段 +pub fn write_vscode_settings(base_url: Option<&str>) -> Result<(), String> { + let path = get_vscode_settings_path(); + let mut map = load_settings(&path)?; + + match base_url { + Some(url) => { + if url.trim().is_empty() { + return Err("base_url 不能为空".into()); + } + + map.insert( + "chatgpt.apiBase".to_string(), + Value::String(url.to_string()), + ); + + let entry = map + .entry("chatgpt.config".to_string()) + .or_insert_with(|| Value::Object(Map::new())); + + let obj = match entry { + Value::Object(o) => o, + _ => return Err("VS Code settings 中 chatgpt.config 必须是 JSON 对象".into()), + }; + + obj.insert( + "preferred_auth_method".to_string(), + Value::String("apikey".to_string()), + ); + } + None => { + map.remove("chatgpt.apiBase"); + map.remove("chatgpt.config"); + } + } + + persist_settings(&path, map) +} diff --git a/src/components/ProviderForm/CodexConfigEditor.tsx b/src/components/ProviderForm/CodexConfigEditor.tsx index ebd6eee..4af5d33 100644 --- a/src/components/ProviderForm/CodexConfigEditor.tsx +++ b/src/components/ProviderForm/CodexConfigEditor.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import { X, Save } from "lucide-react"; +import { extractBaseUrlFromToml } from "../../utils/providerConfigUtils"; interface CodexConfigEditorProps { authValue: string; @@ -29,6 +30,9 @@ const CodexConfigEditor: React.FC = ({ authError, }) => { const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); + const [isWritingVscode, setIsWritingVscode] = useState(false); + const [vscodeError, setVscodeError] = useState(""); + const [vscodeSuccess, setVscodeSuccess] = useState(""); useEffect(() => { if (commonConfigError && !isCommonConfigModalOpen) { @@ -36,6 +40,14 @@ const CodexConfigEditor: React.FC = ({ } }, [commonConfigError, isCommonConfigModalOpen]); + useEffect(() => { + if (!vscodeSuccess) return; + const timer = window.setTimeout(() => { + setVscodeSuccess(""); + }, 3000); + return () => window.clearTimeout(timer); + }, [vscodeSuccess]); + // 支持按下 ESC 关闭弹窗 useEffect(() => { if (!isCommonConfigModalOpen) return; @@ -66,6 +78,42 @@ const CodexConfigEditor: React.FC = ({ onCommonConfigSnippetChange(value); }; + const handleWriteVscodeConfig = async () => { + setVscodeError(""); + setVscodeSuccess(""); + + if (typeof window === "undefined" || !window.api?.writeVscodeSettings) { + setVscodeError("当前环境暂不支持写入 VS Code 配置"); + return; + } + + const trimmed = configValue.trim(); + if (!trimmed) { + setVscodeError("请先填写 config.toml,再写入 VS Code 配置"); + return; + } + + const baseUrl = extractBaseUrlFromToml(trimmed); + if (!baseUrl) { + setVscodeError("未在 config.toml 中找到 base_url 字段"); + return; + } + + setIsWritingVscode(true); + try { + const success = await window.api.writeVscodeSettings(baseUrl); + if (success) { + setVscodeSuccess("已写入 VS Code 配置"); + } else { + setVscodeError("写入 VS Code 配置失败,请稍后重试"); + } + } catch (error) { + setVscodeError(`写入 VS Code 配置失败: ${String(error)}`); + } finally { + setIsWritingVscode(false); + } + }; + return (
@@ -124,7 +172,15 @@ const CodexConfigEditor: React.FC = ({ 写入通用配置
-
+
+