Merge pull request #37 from farion1231/feature/vscode-improvements
Feature/VS Code Integration Enhancements
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"@tauri-apps/api": "^2.8.0",
|
"@tauri-apps/api": "^2.8.0",
|
||||||
"@tauri-apps/plugin-process": "^2.0.0",
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||||
|
"jsonc-parser": "^3.2.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
codemirror:
|
codemirror:
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
jsonc-parser:
|
||||||
|
specifier: ^3.2.1
|
||||||
|
version: 3.3.1
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.542.0
|
specifier: ^0.542.0
|
||||||
version: 0.542.0(react@18.3.1)
|
version: 0.542.0(react@18.3.1)
|
||||||
@@ -755,6 +758,9 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsonc-parser@3.3.1:
|
||||||
|
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -1580,6 +1586,8 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
jsonc-parser@3.3.1: {}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use tauri_plugin_opener::OpenerExt;
|
|||||||
use crate::app_config::AppType;
|
use crate::app_config::AppType;
|
||||||
use crate::codex_config;
|
use crate::codex_config;
|
||||||
use crate::config::{get_claude_settings_path, ConfigStatus};
|
use crate::config::{get_claude_settings_path, ConfigStatus};
|
||||||
|
use crate::vscode;
|
||||||
|
use crate::config;
|
||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
|
|
||||||
@@ -633,3 +635,36 @@ pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String>
|
|||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// VS Code: 获取用户 settings.json 状态
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_vscode_settings_status() -> Result<ConfigStatus, String> {
|
||||||
|
if let Some(p) = vscode::find_existing_settings() {
|
||||||
|
Ok(ConfigStatus { exists: true, path: p.to_string_lossy().to_string() })
|
||||||
|
} else {
|
||||||
|
// 默认返回 macOS 稳定版路径(或其他平台首选项的第一个候选),但标记不存在
|
||||||
|
let preferred = vscode::candidate_settings_paths().into_iter().next();
|
||||||
|
Ok(ConfigStatus { exists: false, path: preferred.unwrap_or_default().to_string_lossy().to_string() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VS Code: 读取 settings.json 文本(仅当文件存在)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_vscode_settings() -> Result<String, String> {
|
||||||
|
if let Some(p) = vscode::find_existing_settings() {
|
||||||
|
std::fs::read_to_string(&p).map_err(|e| format!("读取 VS Code 设置失败: {}", e))
|
||||||
|
} else {
|
||||||
|
Err("未找到 VS Code 用户设置文件".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VS Code: 写入 settings.json 文本(仅当文件存在;不自动创建)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn write_vscode_settings(content: String) -> Result<bool, String> {
|
||||||
|
if let Some(p) = vscode::find_existing_settings() {
|
||||||
|
config::write_text_file(&p, &content)?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Err("未找到 VS Code 用户设置文件".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -175,7 +175,19 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?;
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// Windows 上 rename 目标存在会失败,先移除再重命名(尽量接近原子性)
|
||||||
|
if path.exists() {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod app_config;
|
|||||||
mod codex_config;
|
mod codex_config;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod vscode;
|
||||||
mod migration;
|
mod migration;
|
||||||
mod provider;
|
mod provider;
|
||||||
mod store;
|
mod store;
|
||||||
@@ -357,6 +358,9 @@ pub fn run() {
|
|||||||
commands::get_settings,
|
commands::get_settings,
|
||||||
commands::save_settings,
|
commands::save_settings,
|
||||||
commands::check_for_updates,
|
commands::check_for_updates,
|
||||||
|
commands::get_vscode_settings_status,
|
||||||
|
commands::read_vscode_settings,
|
||||||
|
commands::write_vscode_settings,
|
||||||
update_tray_menu,
|
update_tray_menu,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
61
src-tauri/src/vscode.rs
Normal file
61
src-tauri/src/vscode.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use std::path::{PathBuf};
|
||||||
|
|
||||||
|
/// 枚举可能的 VS Code 发行版配置目录名称
|
||||||
|
fn vscode_product_dirs() -> Vec<&'static str> {
|
||||||
|
vec![
|
||||||
|
"Code", // VS Code Stable
|
||||||
|
"Code - Insiders", // VS Code Insiders
|
||||||
|
"VSCodium", // VSCodium
|
||||||
|
"Code - OSS", // OSS 发行版
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 VS Code 用户 settings.json 的候选路径列表(按优先级排序)
|
||||||
|
pub fn candidate_settings_paths() -> Vec<PathBuf> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
for prod in vscode_product_dirs() {
|
||||||
|
paths.push(
|
||||||
|
home.join("Library").join("Application Support").join(prod).join("User").join("settings.json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Windows: %APPDATA%\Code\User\settings.json
|
||||||
|
if let Some(roaming) = dirs::config_dir() {
|
||||||
|
for prod in vscode_product_dirs() {
|
||||||
|
paths.push(roaming.join(prod).join("User").join("settings.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
// Linux: ~/.config/Code/User/settings.json
|
||||||
|
if let Some(config) = dirs::config_dir() {
|
||||||
|
for prod in vscode_product_dirs() {
|
||||||
|
paths.push(config.join(prod).join("User").join("settings.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返回第一个存在的 settings.json 路径
|
||||||
|
pub fn find_existing_settings() -> Option<PathBuf> {
|
||||||
|
for p in candidate_settings_paths() {
|
||||||
|
if let Ok(meta) = std::fs::metadata(&p) {
|
||||||
|
if meta.is_file() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
78
src/App.tsx
78
src/App.tsx
@@ -12,9 +12,13 @@ import { Plus, Settings, Moon, Sun } from "lucide-react";
|
|||||||
import { buttonStyles } from "./lib/styles";
|
import { buttonStyles } from "./lib/styles";
|
||||||
import { useDarkMode } from "./hooks/useDarkMode";
|
import { useDarkMode } from "./hooks/useDarkMode";
|
||||||
import { extractErrorMessage } from "./utils/errorUtils";
|
import { extractErrorMessage } from "./utils/errorUtils";
|
||||||
|
import { applyProviderToVSCode } from "./utils/vscodeSettings";
|
||||||
|
import { getCodexBaseUrl } from "./utils/providerConfigUtils";
|
||||||
|
import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||||
|
const { isAutoSyncEnabled } = useVSCodeAutoSync();
|
||||||
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
||||||
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
||||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||||
@@ -76,7 +80,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听托盘切换事件
|
// 监听托盘切换事件(包括菜单切换)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: (() => void) | null = null;
|
let unlisten: (() => void) | null = null;
|
||||||
|
|
||||||
@@ -91,6 +95,11 @@ function App() {
|
|||||||
if (data.appType === activeApp) {
|
if (data.appType === activeApp) {
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若为 Codex 且开启自动同步,则静默同步到 VS Code(覆盖)
|
||||||
|
if (data.appType === "codex" && isAutoSyncEnabled) {
|
||||||
|
await syncCodexToVSCode(data.providerId, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("设置供应商切换监听器失败:", error);
|
console.error("设置供应商切换监听器失败:", error);
|
||||||
@@ -105,7 +114,7 @@ function App() {
|
|||||||
unlisten();
|
unlisten();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [activeApp]); // 依赖activeApp,切换应用时重新设置监听器
|
}, [activeApp, isAutoSyncEnabled]);
|
||||||
|
|
||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
const loadedProviders = await window.api.getProviders(activeApp);
|
const loadedProviders = await window.api.getProviders(activeApp);
|
||||||
@@ -174,6 +183,64 @@ function App() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 同步Codex供应商到VS Code设置(静默覆盖)
|
||||||
|
const syncCodexToVSCode = async (providerId: string, silent = false) => {
|
||||||
|
try {
|
||||||
|
const status = await window.api.getVSCodeSettingsStatus();
|
||||||
|
if (!status.exists) {
|
||||||
|
if (!silent) {
|
||||||
|
showNotification(
|
||||||
|
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||||
|
"error",
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await window.api.readVSCodeSettings();
|
||||||
|
const provider = providers[providerId];
|
||||||
|
const isOfficial = provider?.category === "official";
|
||||||
|
|
||||||
|
// 非官方供应商需要解析 base_url(使用公共工具函数)
|
||||||
|
let baseUrl: string | undefined = undefined;
|
||||||
|
if (!isOfficial) {
|
||||||
|
const parsed = getCodexBaseUrl(provider);
|
||||||
|
if (!parsed) {
|
||||||
|
if (!silent) {
|
||||||
|
showNotification(
|
||||||
|
"当前配置缺少 base_url,无法写入 VS Code",
|
||||||
|
"error",
|
||||||
|
4000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
baseUrl = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSettings = applyProviderToVSCode(raw, {
|
||||||
|
baseUrl,
|
||||||
|
isOfficial,
|
||||||
|
});
|
||||||
|
if (updatedSettings !== raw) {
|
||||||
|
await window.api.writeVSCodeSettings(updatedSettings);
|
||||||
|
if (!silent) {
|
||||||
|
showNotification("已同步到 VS Code", "success", 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发providers重新加载,以更新VS Code按钮状态
|
||||||
|
await loadProviders();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("同步到VS Code失败:", error);
|
||||||
|
if (!silent) {
|
||||||
|
const errorMessage = error?.message || "同步 VS Code 失败";
|
||||||
|
showNotification(errorMessage, "error", 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSwitchProvider = async (id: string) => {
|
const handleSwitchProvider = async (id: string) => {
|
||||||
const success = await window.api.switchProvider(id, activeApp);
|
const success = await window.api.switchProvider(id, activeApp);
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -187,6 +254,11 @@ function App() {
|
|||||||
);
|
);
|
||||||
// 更新托盘菜单
|
// 更新托盘菜单
|
||||||
await window.api.updateTrayMenu();
|
await window.api.updateTrayMenu();
|
||||||
|
|
||||||
|
// Codex: 切换供应商后,只在自动同步启用时同步到 VS Code
|
||||||
|
if (activeApp === "codex" && isAutoSyncEnabled) {
|
||||||
|
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showNotification("切换失败,请检查配置", "error");
|
showNotification("切换失败,请检查配置", "error");
|
||||||
}
|
}
|
||||||
@@ -281,6 +353,8 @@ function App() {
|
|||||||
onSwitch={handleSwitchProvider}
|
onSwitch={handleSwitchProvider}
|
||||||
onDelete={handleDeleteProvider}
|
onDelete={handleDeleteProvider}
|
||||||
onEdit={setEditingProviderId}
|
onEdit={setEditingProviderId}
|
||||||
|
appType={activeApp}
|
||||||
|
onNotify={showNotification}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
||||||
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||||
|
import { AppType } from "../lib/tauri-api";
|
||||||
|
import {
|
||||||
|
applyProviderToVSCode,
|
||||||
|
detectApplied,
|
||||||
|
normalizeBaseUrl,
|
||||||
|
} from "../utils/vscodeSettings";
|
||||||
|
import { getCodexBaseUrl } from "../utils/providerConfigUtils";
|
||||||
|
import { useVSCodeAutoSync } from "../hooks/useVSCodeAutoSync";
|
||||||
// 不再在列表中显示分类徽章,避免造成困惑
|
// 不再在列表中显示分类徽章,避免造成困惑
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
@@ -10,6 +18,12 @@ interface ProviderListProps {
|
|||||||
onSwitch: (id: string) => void;
|
onSwitch: (id: string) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
onEdit: (id: string) => void;
|
onEdit: (id: string) => void;
|
||||||
|
appType?: AppType;
|
||||||
|
onNotify?: (
|
||||||
|
message: string,
|
||||||
|
type: "success" | "error",
|
||||||
|
duration?: number,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProviderList: React.FC<ProviderListProps> = ({
|
const ProviderList: React.FC<ProviderListProps> = ({
|
||||||
@@ -18,6 +32,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
onSwitch,
|
onSwitch,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
appType,
|
||||||
|
onNotify,
|
||||||
}) => {
|
}) => {
|
||||||
// 提取API地址(兼容不同供应商配置:Claude env / Codex TOML)
|
// 提取API地址(兼容不同供应商配置:Claude env / Codex TOML)
|
||||||
const getApiUrl = (provider: Provider): string => {
|
const getApiUrl = (provider: Provider): string => {
|
||||||
@@ -29,8 +45,9 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
}
|
}
|
||||||
// Codex: 从 TOML 配置中解析 base_url
|
// Codex: 从 TOML 配置中解析 base_url
|
||||||
if (typeof cfg?.config === "string" && cfg.config.includes("base_url")) {
|
if (typeof cfg?.config === "string" && cfg.config.includes("base_url")) {
|
||||||
const match = cfg.config.match(/base_url\s*=\s*"([^"]+)"/);
|
// 支持单/双引号
|
||||||
if (match && match[1]) return match[1];
|
const match = cfg.config.match(/base_url\s*=\s*(['"])([^'\"]+)\1/);
|
||||||
|
if (match && match[2]) return match[2];
|
||||||
}
|
}
|
||||||
return "未配置官网地址";
|
return "未配置官网地址";
|
||||||
} catch {
|
} catch {
|
||||||
@@ -46,6 +63,128 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 解析 Codex 配置中的 base_url(已提取到公共工具)
|
||||||
|
|
||||||
|
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
||||||
|
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
||||||
|
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
||||||
|
|
||||||
|
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
|
||||||
|
useEffect(() => {
|
||||||
|
const check = async () => {
|
||||||
|
if (appType !== "codex" || !currentProviderId) {
|
||||||
|
setVscodeAppliedFor(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const status = await window.api.getVSCodeSettingsStatus();
|
||||||
|
if (!status.exists) {
|
||||||
|
setVscodeAppliedFor(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = await window.api.readVSCodeSettings();
|
||||||
|
const detected = detectApplied(content);
|
||||||
|
// 认为“已应用”的条件(非官方供应商):VS Code 中的 apiBase 与当前供应商的 base_url 完全一致
|
||||||
|
const current = providers[currentProviderId];
|
||||||
|
let applied = false;
|
||||||
|
if (current && current.category !== "official") {
|
||||||
|
const base = getCodexBaseUrl(current);
|
||||||
|
if (detected.apiBase && base) {
|
||||||
|
applied =
|
||||||
|
normalizeBaseUrl(detected.apiBase) === normalizeBaseUrl(base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setVscodeAppliedFor(applied ? currentProviderId : null);
|
||||||
|
} catch {
|
||||||
|
setVscodeAppliedFor(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
}, [appType, currentProviderId, providers]);
|
||||||
|
|
||||||
|
const handleApplyToVSCode = async (provider: Provider) => {
|
||||||
|
try {
|
||||||
|
const status = await window.api.getVSCodeSettingsStatus();
|
||||||
|
if (!status.exists) {
|
||||||
|
onNotify?.(
|
||||||
|
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||||
|
"error",
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await window.api.readVSCodeSettings();
|
||||||
|
|
||||||
|
const isOfficial = provider.category === "official";
|
||||||
|
// 非官方且缺少 base_url 时直接报错并返回,避免“空写入”假成功
|
||||||
|
if (!isOfficial) {
|
||||||
|
const parsed = getCodexBaseUrl(provider);
|
||||||
|
if (!parsed) {
|
||||||
|
onNotify?.("当前配置缺少 base_url,无法写入 VS Code", "error", 4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = isOfficial ? undefined : getCodexBaseUrl(provider);
|
||||||
|
const next = applyProviderToVSCode(raw, { baseUrl, isOfficial });
|
||||||
|
|
||||||
|
if (next === raw) {
|
||||||
|
// 幂等:没有变化也提示成功
|
||||||
|
onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000);
|
||||||
|
setVscodeAppliedFor(provider.id);
|
||||||
|
// 用户手动应用时,启用自动同步
|
||||||
|
enableAutoSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.api.writeVSCodeSettings(next);
|
||||||
|
onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000);
|
||||||
|
setVscodeAppliedFor(provider.id);
|
||||||
|
// 用户手动应用时,启用自动同步
|
||||||
|
enableAutoSync();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
const msg = e && e.message ? e.message : "应用到 VS Code 失败";
|
||||||
|
onNotify?.(msg, "error", 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromVSCode = async () => {
|
||||||
|
try {
|
||||||
|
const status = await window.api.getVSCodeSettingsStatus();
|
||||||
|
if (!status.exists) {
|
||||||
|
onNotify?.(
|
||||||
|
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||||
|
"error",
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = await window.api.readVSCodeSettings();
|
||||||
|
const next = applyProviderToVSCode(raw, {
|
||||||
|
baseUrl: undefined,
|
||||||
|
isOfficial: true,
|
||||||
|
});
|
||||||
|
if (next === raw) {
|
||||||
|
onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000);
|
||||||
|
setVscodeAppliedFor(null);
|
||||||
|
// 用户手动移除时,禁用自动同步
|
||||||
|
disableAutoSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await window.api.writeVSCodeSettings(next);
|
||||||
|
onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000);
|
||||||
|
setVscodeAppliedFor(null);
|
||||||
|
// 用户手动移除时,禁用自动同步
|
||||||
|
disableAutoSync();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
const msg = e && e.message ? e.message : "移除失败";
|
||||||
|
onNotify?.(msg, "error", 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 对供应商列表进行排序
|
// 对供应商列表进行排序
|
||||||
const sortedProviders = Object.values(providers).sort((a, b) => {
|
const sortedProviders = Object.values(providers).sort((a, b) => {
|
||||||
// 按添加时间排序
|
// 按添加时间排序
|
||||||
@@ -101,12 +240,15 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
{provider.name}
|
{provider.name}
|
||||||
</h3>
|
</h3>
|
||||||
{/* 分类徽章已移除 */}
|
{/* 分类徽章已移除 */}
|
||||||
{isCurrent && (
|
<div
|
||||||
<div className={badgeStyles.success}>
|
className={cn(
|
||||||
<CheckCircle2 size={12} />
|
badgeStyles.success,
|
||||||
当前使用
|
!isCurrent && "invisible",
|
||||||
</div>
|
)}
|
||||||
)}
|
>
|
||||||
|
<CheckCircle2 size={12} />
|
||||||
|
当前使用
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
@@ -133,6 +275,32 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
|
{appType === "codex" &&
|
||||||
|
provider.category !== "official" && (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
vscodeAppliedFor === provider.id
|
||||||
|
? handleRemoveFromVSCode()
|
||||||
|
: handleApplyToVSCode(provider)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[130px] justify-center",
|
||||||
|
!isCurrent && "invisible",
|
||||||
|
vscodeAppliedFor === provider.id
|
||||||
|
? "bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||||
|
: "bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
|
||||||
|
)}
|
||||||
|
title={
|
||||||
|
vscodeAppliedFor === provider.id
|
||||||
|
? "从 VS Code 移除我们写入的配置"
|
||||||
|
: "将当前供应商应用到 VS Code"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{vscodeAppliedFor === provider.id
|
||||||
|
? "从 VS Code 移除"
|
||||||
|
: "应用到 VS Code"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => onSwitch(provider.id)}
|
onClick={() => onSwitch(provider.id)}
|
||||||
disabled={isCurrent}
|
disabled={isCurrent}
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
</label>
|
</label>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
|
{/* VS Code 自动同步设置已移除 */}
|
||||||
|
|
||||||
{/* 配置文件位置 */}
|
{/* 配置文件位置 */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
|||||||
99
src/hooks/useVSCodeAutoSync.ts
Normal file
99
src/hooks/useVSCodeAutoSync.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
const VSCODE_AUTO_SYNC_KEY = "vscode-auto-sync-enabled";
|
||||||
|
const VSCODE_AUTO_SYNC_EVENT = "vscode-auto-sync-changed";
|
||||||
|
|
||||||
|
export function useVSCodeAutoSync() {
|
||||||
|
// 默认开启自动同步;若本地存储存在记录,则以记录为准
|
||||||
|
const [isAutoSyncEnabled, setIsAutoSyncEnabled] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// 从 localStorage 读取初始状态
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(VSCODE_AUTO_SYNC_KEY);
|
||||||
|
if (saved !== null) {
|
||||||
|
setIsAutoSyncEnabled(saved === "true");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("读取自动同步状态失败:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 订阅同窗口的自定义事件,以及跨窗口的 storage 事件,实现全局同步
|
||||||
|
useEffect(() => {
|
||||||
|
const onCustom = (e: Event) => {
|
||||||
|
try {
|
||||||
|
const detail = (e as CustomEvent).detail as
|
||||||
|
| { enabled?: boolean }
|
||||||
|
| undefined;
|
||||||
|
if (detail && typeof detail.enabled === "boolean") {
|
||||||
|
setIsAutoSyncEnabled(detail.enabled);
|
||||||
|
} else {
|
||||||
|
// 兜底:从 localStorage 读取
|
||||||
|
const saved = localStorage.getItem(VSCODE_AUTO_SYNC_KEY);
|
||||||
|
if (saved !== null) setIsAutoSyncEnabled(saved === "true");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onStorage = (e: StorageEvent) => {
|
||||||
|
if (e.key === VSCODE_AUTO_SYNC_KEY) {
|
||||||
|
setIsAutoSyncEnabled(e.newValue === "true");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener(VSCODE_AUTO_SYNC_EVENT, onCustom as EventListener);
|
||||||
|
window.addEventListener("storage", onStorage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
VSCODE_AUTO_SYNC_EVENT,
|
||||||
|
onCustom as EventListener,
|
||||||
|
);
|
||||||
|
window.removeEventListener("storage", onStorage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 启用自动同步
|
||||||
|
const enableAutoSync = useCallback(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(VSCODE_AUTO_SYNC_KEY, "true");
|
||||||
|
setIsAutoSyncEnabled(true);
|
||||||
|
// 通知同窗口其他订阅者
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(VSCODE_AUTO_SYNC_EVENT, { detail: { enabled: true } }),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存自动同步状态失败:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 禁用自动同步
|
||||||
|
const disableAutoSync = useCallback(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(VSCODE_AUTO_SYNC_KEY, "false");
|
||||||
|
setIsAutoSyncEnabled(false);
|
||||||
|
// 通知同窗口其他订阅者
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(VSCODE_AUTO_SYNC_EVENT, { detail: { enabled: false } }),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存自动同步状态失败:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 切换自动同步状态
|
||||||
|
const toggleAutoSync = useCallback(() => {
|
||||||
|
if (isAutoSyncEnabled) {
|
||||||
|
disableAutoSync();
|
||||||
|
} else {
|
||||||
|
enableAutoSync();
|
||||||
|
}
|
||||||
|
}, [isAutoSyncEnabled, enableAutoSync, disableAutoSync]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAutoSyncEnabled,
|
||||||
|
enableAutoSync,
|
||||||
|
disableAutoSync,
|
||||||
|
toggleAutoSync,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ export const cardStyles = {
|
|||||||
|
|
||||||
// 选中/激活态卡片
|
// 选中/激活态卡片
|
||||||
selected:
|
selected:
|
||||||
"bg-white rounded-lg border border-blue-500 ring-1 ring-blue-500/20 bg-blue-500/5 p-4 dark:bg-gray-900 dark:border-blue-400 dark:ring-blue-400/20 dark:bg-blue-400/10",
|
"bg-white rounded-lg border border-blue-500 shadow-sm bg-blue-50 p-4 dark:bg-gray-900 dark:border-blue-400 dark:bg-blue-400/10",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 输入控件样式
|
// 输入控件样式
|
||||||
|
|||||||
@@ -242,6 +242,38 @@ export const tauriAPI = {
|
|||||||
console.error("打开应用配置文件夹失败:", error);
|
console.error("打开应用配置文件夹失败:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// VS Code: 获取 settings.json 状态
|
||||||
|
getVSCodeSettingsStatus: async (): Promise<{
|
||||||
|
exists: boolean;
|
||||||
|
path: string;
|
||||||
|
error?: string;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_vscode_settings_status");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取 VS Code 设置状态失败:", error);
|
||||||
|
return { exists: false, path: "", error: String(error) };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// VS Code: 读取 settings.json 文本
|
||||||
|
readVSCodeSettings: async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
return await invoke("read_vscode_settings");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`读取 VS Code 设置失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// VS Code: 写回 settings.json 文本(不自动创建)
|
||||||
|
writeVSCodeSettings: async (content: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("write_vscode_settings", { content });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`写入 VS Code 设置失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建全局 API 对象,兼容现有代码
|
// 创建全局 API 对象,兼容现有代码
|
||||||
|
|||||||
@@ -287,3 +287,34 @@ export const hasTomlCommonConfigSnippet = (
|
|||||||
normalizeWhitespace(snippetString),
|
normalizeWhitespace(snippetString),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========== Codex base_url utils ==========
|
||||||
|
|
||||||
|
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
||||||
|
export const extractCodexBaseUrl = (
|
||||||
|
configText: string | undefined | null,
|
||||||
|
): string | undefined => {
|
||||||
|
try {
|
||||||
|
const text = typeof configText === "string" ? configText : "";
|
||||||
|
if (!text) return undefined;
|
||||||
|
const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/);
|
||||||
|
return m && m[2] ? m[2] : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从 Provider 对象中提取 Codex base_url(当 settingsConfig.config 为 TOML 字符串时)
|
||||||
|
export const getCodexBaseUrl = (
|
||||||
|
provider: { settingsConfig?: Record<string, any> } | undefined | null,
|
||||||
|
): string | undefined => {
|
||||||
|
try {
|
||||||
|
const text =
|
||||||
|
typeof provider?.settingsConfig?.config === "string"
|
||||||
|
? (provider as any).settingsConfig.config
|
||||||
|
: "";
|
||||||
|
return extractCodexBaseUrl(text);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
124
src/utils/vscodeSettings.ts
Normal file
124
src/utils/vscodeSettings.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
||||||
|
|
||||||
|
const fmt = { insertSpaces: true, tabSize: 2, eol: "\n" } as const;
|
||||||
|
|
||||||
|
export interface AppliedCheck {
|
||||||
|
hasApiBase: boolean;
|
||||||
|
apiBase?: string;
|
||||||
|
hasPreferredAuthMethod: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeBaseUrl(url: string): string {
|
||||||
|
return url.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDocEmpty = (s: string) => s.trim().length === 0;
|
||||||
|
|
||||||
|
// 检查 settings.json(JSONC 文本)中是否已经应用了我们的键
|
||||||
|
export function detectApplied(content: string): AppliedCheck {
|
||||||
|
try {
|
||||||
|
// 允许 JSONC 的宽松解析:jsonc-parser 的 parse 可以直接处理注释
|
||||||
|
const data = parse(content) as any;
|
||||||
|
const apiBase = data?.["chatgpt.apiBase"];
|
||||||
|
const method = data?.["chatgpt.config"]?.preferred_auth_method;
|
||||||
|
return {
|
||||||
|
hasApiBase: typeof apiBase === "string",
|
||||||
|
apiBase,
|
||||||
|
hasPreferredAuthMethod: typeof method === "string",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { hasApiBase: false, hasPreferredAuthMethod: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成“清理我们管理的键”后的文本(仅删除我们写入的两个键)
|
||||||
|
export function removeManagedKeys(content: string): string {
|
||||||
|
if (isDocEmpty(content)) return content; // 空文档无需删除
|
||||||
|
let out = content;
|
||||||
|
// 删除 chatgpt.apiBase
|
||||||
|
try {
|
||||||
|
out = applyEdits(
|
||||||
|
out,
|
||||||
|
modify(out, ["chatgpt.apiBase"], undefined, { formattingOptions: fmt }),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// 忽略删除失败
|
||||||
|
}
|
||||||
|
// 删除 chatgpt.config.preferred_auth_method(注意 chatgpt.config 是顶层带点的键)
|
||||||
|
try {
|
||||||
|
out = applyEdits(
|
||||||
|
out,
|
||||||
|
modify(out, ["chatgpt.config", "preferred_auth_method"], undefined, {
|
||||||
|
formattingOptions: fmt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// 忽略删除失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容早期错误写入:若曾写成嵌套 chatgpt.config.preferred_auth_method,也一并清理
|
||||||
|
try {
|
||||||
|
out = applyEdits(
|
||||||
|
out,
|
||||||
|
modify(out, ["chatgpt", "config", "preferred_auth_method"], undefined, {
|
||||||
|
formattingOptions: fmt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// 忽略删除失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若 chatgpt.config 变为空对象,顺便移除(不影响其他 chatgpt* 键)
|
||||||
|
try {
|
||||||
|
const data = parse(out) as any;
|
||||||
|
const cfg = data?.["chatgpt.config"];
|
||||||
|
if (
|
||||||
|
cfg &&
|
||||||
|
typeof cfg === "object" &&
|
||||||
|
!Array.isArray(cfg) &&
|
||||||
|
Object.keys(cfg).length === 0
|
||||||
|
) {
|
||||||
|
out = applyEdits(
|
||||||
|
out,
|
||||||
|
modify(out, ["chatgpt.config"], undefined, { formattingOptions: fmt }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略解析失败,保持已删除的键
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成“应用供应商到 VS Code”后的文本:
|
||||||
|
// - 先清理我们管理的键
|
||||||
|
// - 再根据是否官方决定写入(官方:不写入;非官方:写入两个键)
|
||||||
|
export function applyProviderToVSCode(
|
||||||
|
content: string,
|
||||||
|
opts: { baseUrl?: string | null; isOfficial?: boolean },
|
||||||
|
): string {
|
||||||
|
let out = removeManagedKeys(content);
|
||||||
|
if (!opts.isOfficial && opts.baseUrl) {
|
||||||
|
const apiBase = normalizeBaseUrl(opts.baseUrl);
|
||||||
|
if (isDocEmpty(out)) {
|
||||||
|
// 简化:空文档直接写入新对象
|
||||||
|
const obj: any = {
|
||||||
|
"chatgpt.apiBase": apiBase,
|
||||||
|
"chatgpt.config": { preferred_auth_method: "apikey" },
|
||||||
|
};
|
||||||
|
out = JSON.stringify(obj, null, 2) + "\n";
|
||||||
|
} else {
|
||||||
|
out = applyEdits(
|
||||||
|
out,
|
||||||
|
modify(out, ["chatgpt.apiBase"], apiBase, { formattingOptions: fmt }),
|
||||||
|
);
|
||||||
|
out = applyEdits(
|
||||||
|
out,
|
||||||
|
modify(out, ["chatgpt.config", "preferred_auth_method"], "apikey", {
|
||||||
|
formattingOptions: fmt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
4
src/vite-env.d.ts
vendored
4
src/vite-env.d.ts
vendored
@@ -40,6 +40,10 @@ declare global {
|
|||||||
checkForUpdates: () => Promise<void>;
|
checkForUpdates: () => Promise<void>;
|
||||||
getAppConfigPath: () => Promise<string>;
|
getAppConfigPath: () => Promise<string>;
|
||||||
openAppConfigFolder: () => Promise<void>;
|
openAppConfigFolder: () => Promise<void>;
|
||||||
|
// VS Code settings.json 能力
|
||||||
|
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
||||||
|
readVSCodeSettings: () => Promise<string>;
|
||||||
|
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user