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/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
||||
codemirror:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
jsonc-parser:
|
||||
specifier: ^3.2.1
|
||||
version: 3.3.1
|
||||
lucide-react:
|
||||
specifier: ^0.542.0
|
||||
version: 0.542.0(react@18.3.1)
|
||||
@@ -755,6 +758,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsonc-parser@3.3.1:
|
||||
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -1580,6 +1586,8 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsonc-parser@3.3.1: {}
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ use tauri_plugin_opener::OpenerExt;
|
||||
use crate::app_config::AppType;
|
||||
use crate::codex_config;
|
||||
use crate::config::{get_claude_settings_path, ConfigStatus};
|
||||
use crate::vscode;
|
||||
use crate::config;
|
||||
use crate::provider::Provider;
|
||||
use crate::store::AppState;
|
||||
|
||||
@@ -633,3 +635,36 @@ pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String>
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ mod app_config;
|
||||
mod codex_config;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod vscode;
|
||||
mod migration;
|
||||
mod provider;
|
||||
mod store;
|
||||
@@ -357,6 +358,9 @@ pub fn run() {
|
||||
commands::get_settings,
|
||||
commands::save_settings,
|
||||
commands::check_for_updates,
|
||||
commands::get_vscode_settings_status,
|
||||
commands::read_vscode_settings,
|
||||
commands::write_vscode_settings,
|
||||
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 { useDarkMode } from "./hooks/useDarkMode";
|
||||
import { extractErrorMessage } from "./utils/errorUtils";
|
||||
import { applyProviderToVSCode } from "./utils/vscodeSettings";
|
||||
import { getCodexBaseUrl } from "./utils/providerConfigUtils";
|
||||
import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync";
|
||||
|
||||
function App() {
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const { isAutoSyncEnabled } = useVSCodeAutoSync();
|
||||
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
||||
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||
@@ -76,7 +80,7 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听托盘切换事件
|
||||
// 监听托盘切换事件(包括菜单切换)
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | null = null;
|
||||
|
||||
@@ -91,6 +95,11 @@ function App() {
|
||||
if (data.appType === activeApp) {
|
||||
await loadProviders();
|
||||
}
|
||||
|
||||
// 若为 Codex 且开启自动同步,则静默同步到 VS Code(覆盖)
|
||||
if (data.appType === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(data.providerId, true);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("设置供应商切换监听器失败:", error);
|
||||
@@ -105,7 +114,7 @@ function App() {
|
||||
unlisten();
|
||||
}
|
||||
};
|
||||
}, [activeApp]); // 依赖activeApp,切换应用时重新设置监听器
|
||||
}, [activeApp, isAutoSyncEnabled]);
|
||||
|
||||
const loadProviders = async () => {
|
||||
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 success = await window.api.switchProvider(id, activeApp);
|
||||
if (success) {
|
||||
@@ -187,6 +254,11 @@ function App() {
|
||||
);
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
|
||||
// Codex: 切换供应商后,只在自动同步启用时同步到 VS Code
|
||||
if (activeApp === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
||||
}
|
||||
} else {
|
||||
showNotification("切换失败,请检查配置", "error");
|
||||
}
|
||||
@@ -281,6 +353,8 @@ function App() {
|
||||
onSwitch={handleSwitchProvider}
|
||||
onDelete={handleDeleteProvider}
|
||||
onEdit={setEditingProviderId}
|
||||
appType={activeApp}
|
||||
onNotify={showNotification}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Provider } from "../types";
|
||||
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
||||
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 {
|
||||
@@ -10,6 +18,12 @@ interface ProviderListProps {
|
||||
onSwitch: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
appType?: AppType;
|
||||
onNotify?: (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration?: number,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const ProviderList: React.FC<ProviderListProps> = ({
|
||||
@@ -18,6 +32,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
onSwitch,
|
||||
onDelete,
|
||||
onEdit,
|
||||
appType,
|
||||
onNotify,
|
||||
}) => {
|
||||
// 提取API地址(兼容不同供应商配置:Claude env / Codex TOML)
|
||||
const getApiUrl = (provider: Provider): string => {
|
||||
@@ -29,8 +45,9 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
}
|
||||
// Codex: 从 TOML 配置中解析 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 "未配置官网地址";
|
||||
} 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) => {
|
||||
// 按添加时间排序
|
||||
@@ -101,12 +240,15 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
{provider.name}
|
||||
</h3>
|
||||
{/* 分类徽章已移除 */}
|
||||
{isCurrent && (
|
||||
<div className={badgeStyles.success}>
|
||||
<CheckCircle2 size={12} />
|
||||
当前使用
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
badgeStyles.success,
|
||||
!isCurrent && "invisible",
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 size={12} />
|
||||
当前使用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
@@ -133,6 +275,32 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
</div>
|
||||
|
||||
<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
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
|
||||
@@ -201,6 +201,8 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</label>
|
||||
</div> */}
|
||||
|
||||
{/* VS Code 自动同步设置已移除 */}
|
||||
|
||||
{/* 配置文件位置 */}
|
||||
<div>
|
||||
<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:
|
||||
"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;
|
||||
|
||||
// 输入控件样式
|
||||
|
||||
@@ -242,6 +242,38 @@ export const tauriAPI = {
|
||||
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 对象,兼容现有代码
|
||||
|
||||
@@ -287,3 +287,34 @@ export const hasTomlCommonConfigSnippet = (
|
||||
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>;
|
||||
getAppConfigPath: () => Promise<string>;
|
||||
openAppConfigFolder: () => Promise<void>;
|
||||
// VS Code settings.json 能力
|
||||
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
||||
readVSCodeSettings: () => Promise<string>;
|
||||
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
||||
};
|
||||
platform: {
|
||||
isMac: boolean;
|
||||
|
||||
Reference in New Issue
Block a user