Remove deprecated VS Code Codex integration
This commit is contained in:
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### ✨ Features
|
||||
- Enable internationalization via i18next with a Chinese default and English fallback, plus an in-app language switcher
|
||||
- Add Claude plugin sync alongside the existing VS Code integration controls
|
||||
- Add Claude plugin sync while retiring the legacy VS Code integration controls (Codex no longer requires settings.json edits)
|
||||
- Extend provider presets with optional API key URLs and updated models, including DeepSeek-V3.1-Terminus and Qwen3-Max
|
||||
- Support portable mode launches and enforce a single running instance to avoid conflicts
|
||||
|
||||
@@ -22,13 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### 🐛 Fixes
|
||||
- Remove the unnecessary OpenAI auth requirement from third-party provider configurations
|
||||
- Fix layout shifts while switching app types with Claude plugin sync enabled
|
||||
- Align Enable/In Use button states to avoid visual jank across VS Code and Codex views
|
||||
- Align Enable/In Use button states to avoid visual jank across app views
|
||||
|
||||
## [3.3.0] - 2025-09-22
|
||||
|
||||
### ✨ Features
|
||||
- Add “Apply to VS Code / Remove from VS Code” actions on provider cards, writing settings for Code/Insiders/VSCodium variants
|
||||
- Enable VS Code auto-sync by default with window broadcast and tray hooks so Codex switches sync silently
|
||||
- Add “Apply to VS Code / Remove from VS Code” actions on provider cards, writing settings for Code/Insiders/VSCodium variants *(Removed in 3.4.x)*
|
||||
- Enable VS Code auto-sync by default with window broadcast and tray hooks so Codex switches sync silently *(Removed in 3.4.x)*
|
||||
- Extend the Codex provider wizard with display name, dedicated API key URL, and clearer guidance
|
||||
- Introduce shared common config snippets with JSON/TOML reuse, validation, and consistent error surfaces
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> v3.4.0 :新增 i18next 国际化(还有部分未完成)、对新模型(qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp)的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
|
||||
|
||||
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。
|
||||
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。(该 VS Code 写入功能已在 v3.4.x 停用)
|
||||
|
||||
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
## 功能特性(v3.4.0)
|
||||
|
||||
- **国际化与语言切换**:内置 i18next,默认显示中文,可在设置中快速切换到英文,界面文文案自动实时刷新。
|
||||
- **Claude 插件同步**:在 VS Code 同步按钮旁新增 Claude 插件同步选项,与 Codex 同步互不冲突,切换供应商后立即应用。
|
||||
- **Claude 插件同步**:内置按钮可一键应用或恢复 Claude 插件配置,切换供应商后立即生效。
|
||||
- **VS Code Codex 设置停用**:由于新版 Codex 插件无需修改 `settings.json`,应用不再写入 VS Code 设置,避免潜在冲突。
|
||||
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
|
||||
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
||||
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
- i18n
|
||||
- gemini cli
|
||||
- homebrew 支持
|
||||
- 自定义 vscode 路径
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::codex_config;
|
||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||
use crate::provider::Provider;
|
||||
use crate::store::AppState;
|
||||
use crate::vscode;
|
||||
|
||||
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
|
||||
match app_type {
|
||||
@@ -693,45 +692,6 @@ pub async fn is_portable_mode() -> Result<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:获取 ~/.claude/config.json 状态
|
||||
#[tauri::command]
|
||||
pub async fn get_claude_plugin_status() -> Result<ConfigStatus, String> {
|
||||
|
||||
@@ -7,7 +7,6 @@ mod migration;
|
||||
mod provider;
|
||||
mod settings;
|
||||
mod store;
|
||||
mod vscode;
|
||||
|
||||
use store::AppState;
|
||||
use tauri::{
|
||||
@@ -416,9 +415,6 @@ pub fn run() {
|
||||
commands::save_settings,
|
||||
commands::check_for_updates,
|
||||
commands::is_portable_mode,
|
||||
commands::get_vscode_settings_status,
|
||||
commands::read_vscode_settings,
|
||||
commands::write_vscode_settings,
|
||||
commands::get_claude_plugin_status,
|
||||
commands::read_claude_plugin_config,
|
||||
commands::apply_claude_plugin_config,
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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
|
||||
}
|
||||
72
src/App.tsx
72
src/App.tsx
@@ -13,14 +13,10 @@ 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 { t } = useTranslation();
|
||||
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>("");
|
||||
@@ -98,11 +94,7 @@ function App() {
|
||||
await loadProviders();
|
||||
}
|
||||
|
||||
// 若为 Codex 且开启自动同步,则静默同步到 VS Code(覆盖)
|
||||
if (data.appType === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(data.providerId, true);
|
||||
}
|
||||
|
||||
// 若为 Claude,则同步插件配置
|
||||
if (data.appType === "claude") {
|
||||
await syncClaudePlugin(data.providerId, true);
|
||||
}
|
||||
@@ -120,7 +112,7 @@ function App() {
|
||||
unlisten();
|
||||
}
|
||||
};
|
||||
}, [activeApp, isAutoSyncEnabled]);
|
||||
}, [activeApp]);
|
||||
|
||||
const loadProviders = async () => {
|
||||
const loadedProviders = await window.api.getProviders(activeApp);
|
||||
@@ -189,61 +181,6 @@ 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(
|
||||
t("notifications.vscodeSettingsNotFound"),
|
||||
"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(t("notifications.missingBaseUrl"), "error", 4000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
baseUrl = parsed;
|
||||
}
|
||||
|
||||
const updatedSettings = applyProviderToVSCode(raw, {
|
||||
baseUrl,
|
||||
isOfficial,
|
||||
});
|
||||
if (updatedSettings !== raw) {
|
||||
await window.api.writeVSCodeSettings(updatedSettings);
|
||||
if (!silent) {
|
||||
showNotification(t("notifications.syncedToVSCode"), "success", 1500);
|
||||
}
|
||||
}
|
||||
|
||||
// 触发providers重新加载,以更新VS Code按钮状态
|
||||
await loadProviders();
|
||||
} catch (error: any) {
|
||||
console.error(t("console.syncToVSCodeFailed"), error);
|
||||
if (!silent) {
|
||||
const errorMessage =
|
||||
error?.message || t("notifications.syncVSCodeFailed");
|
||||
showNotification(errorMessage, "error", 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 同步 Claude 插件配置(写入/移除固定 JSON)
|
||||
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
||||
try {
|
||||
@@ -284,11 +221,6 @@ function App() {
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
|
||||
// Codex: 切换供应商后,只在自动同步启用时同步到 VS Code
|
||||
if (activeApp === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
||||
}
|
||||
|
||||
if (activeApp === "claude") {
|
||||
await syncClaudePlugin(id, true);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ import { Provider } from "../types";
|
||||
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } 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 {
|
||||
@@ -65,46 +58,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 解析 Codex 配置中的 base_url(已提取到公共工具)
|
||||
|
||||
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
||||
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
||||
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
||||
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
|
||||
|
||||
// 当当前供应商或 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]);
|
||||
|
||||
// 检查 Claude 插件配置是否已应用
|
||||
useEffect(() => {
|
||||
const checkClaude = async () => {
|
||||
@@ -123,83 +78,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
checkClaude();
|
||||
}, [appType, currentProviderId, providers]);
|
||||
|
||||
const handleApplyToVSCode = async (provider: Provider) => {
|
||||
try {
|
||||
const status = await window.api.getVSCodeSettingsStatus();
|
||||
if (!status.exists) {
|
||||
onNotify?.(t("notifications.vscodeSettingsNotFound"), "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?.(t("notifications.missingBaseUrl"), "error", 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = isOfficial ? undefined : getCodexBaseUrl(provider);
|
||||
const next = applyProviderToVSCode(raw, { baseUrl, isOfficial });
|
||||
|
||||
if (next === raw) {
|
||||
// 幂等:没有变化也提示成功
|
||||
onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(provider.id);
|
||||
// 用户手动应用时,启用自动同步
|
||||
enableAutoSync();
|
||||
return;
|
||||
}
|
||||
|
||||
await window.api.writeVSCodeSettings(next);
|
||||
onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(provider.id);
|
||||
// 用户手动应用时,启用自动同步
|
||||
enableAutoSync();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
const msg =
|
||||
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
|
||||
onNotify?.(msg, "error", 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFromVSCode = async () => {
|
||||
try {
|
||||
const status = await window.api.getVSCodeSettingsStatus();
|
||||
if (!status.exists) {
|
||||
onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
const raw = await window.api.readVSCodeSettings();
|
||||
const next = applyProviderToVSCode(raw, {
|
||||
baseUrl: undefined,
|
||||
isOfficial: true,
|
||||
});
|
||||
if (next === raw) {
|
||||
onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(null);
|
||||
// 用户手动移除时,禁用自动同步
|
||||
disableAutoSync();
|
||||
return;
|
||||
}
|
||||
await window.api.writeVSCodeSettings(next);
|
||||
onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(null);
|
||||
// 用户手动移除时,禁用自动同步
|
||||
disableAutoSync();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
const msg =
|
||||
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
|
||||
onNotify?.(msg, "error", 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyToClaudePlugin = async () => {
|
||||
try {
|
||||
await window.api.applyClaudePluginConfig({ official: false });
|
||||
@@ -320,36 +198,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{/* 同步按钮占位容器 - 只在对应模式下渲染,避免布局跳动 */}
|
||||
{appType === "codex" ? (
|
||||
<div className="w-[130px]">
|
||||
{provider.category !== "official" && isCurrent && (
|
||||
<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-full whitespace-nowrap justify-center",
|
||||
vscodeAppliedFor === provider.id
|
||||
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
|
||||
: "border border-gray-300 text-gray-700 hover:border-blue-300 hover:text-blue-600 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-700 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
|
||||
)}
|
||||
title={
|
||||
vscodeAppliedFor === provider.id
|
||||
? t("provider.removeFromVSCode")
|
||||
: t("provider.applyToVSCode")
|
||||
}
|
||||
>
|
||||
{vscodeAppliedFor === provider.id
|
||||
? t("provider.removeFromVSCode")
|
||||
: t("provider.applyToVSCode")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{appType === "claude" ? (
|
||||
<div className="w-[130px]">
|
||||
{provider.category !== "official" && isCurrent && (
|
||||
|
||||
@@ -435,8 +435,6 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VS Code 自动同步设置已移除 */}
|
||||
|
||||
{/* 配置文件位置 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
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,8 +38,6 @@
|
||||
"addNewProvider": "Add New Provider",
|
||||
"configError": "Configuration Error",
|
||||
"notConfigured": "Not configured for official website",
|
||||
"applyToVSCode": "Apply to VS Code",
|
||||
"removeFromVSCode": "Remove from VS Code",
|
||||
"applyToClaudePlugin": "Apply to Claude plugin",
|
||||
"removeFromClaudePlugin": "Remove from Claude plugin"
|
||||
},
|
||||
@@ -49,14 +47,8 @@
|
||||
"switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect",
|
||||
"switchFailed": "Switch failed, please check configuration",
|
||||
"autoImported": "Default provider created from existing configuration",
|
||||
"appliedToVSCode": "Applied to VS Code, restart Codex plugin to take effect",
|
||||
"removedFromVSCode": "Removed from VS Code, restart Codex plugin to take effect",
|
||||
"syncedToVSCode": "Synced to VS Code",
|
||||
"vscodeSettingsNotFound": "VS Code user settings file (settings.json) not found",
|
||||
"missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code",
|
||||
"saveFailed": "Save failed: {{error}}",
|
||||
"saveFailedGeneric": "Save failed, please try again",
|
||||
"syncVSCodeFailed": "Sync to VS Code failed",
|
||||
"appliedToClaudePlugin": "Applied to Claude plugin",
|
||||
"removedFromClaudePlugin": "Removed from Claude plugin",
|
||||
"syncClaudePluginFailed": "Sync Claude plugin failed"
|
||||
@@ -101,7 +93,6 @@
|
||||
"providerSwitchReceived": "Received provider switch event:",
|
||||
"setupListenerFailed": "Failed to setup provider switch listener:",
|
||||
"updateProviderFailed": "Update provider failed:",
|
||||
"syncToVSCodeFailed": "Sync to VS Code failed:",
|
||||
"autoImportFailed": "Auto import default configuration failed:",
|
||||
"openLinkFailed": "Failed to open link:",
|
||||
"getVersionFailed": "Failed to get version info:",
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
"addNewProvider": "添加新供应商",
|
||||
"configError": "配置错误",
|
||||
"notConfigured": "未配置官网地址",
|
||||
"applyToVSCode": "应用到 VS Code",
|
||||
"removeFromVSCode": "从 VS Code 移除",
|
||||
"applyToClaudePlugin": "应用到 Claude 插件",
|
||||
"removeFromClaudePlugin": "从 Claude 插件移除"
|
||||
},
|
||||
@@ -49,14 +47,8 @@
|
||||
"switchSuccess": "切换成功!请重启 {{appName}} 终端以生效",
|
||||
"switchFailed": "切换失败,请检查配置",
|
||||
"autoImported": "已从现有配置创建默认供应商",
|
||||
"appliedToVSCode": "已应用到 VS Code,重启 Codex 插件以生效",
|
||||
"removedFromVSCode": "已从 VS Code 移除,重启 Codex 插件以生效",
|
||||
"syncedToVSCode": "已同步到 VS Code",
|
||||
"vscodeSettingsNotFound": "未找到 VS Code 用户设置文件 (settings.json)",
|
||||
"missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code",
|
||||
"saveFailed": "保存失败:{{error}}",
|
||||
"saveFailedGeneric": "保存失败,请重试",
|
||||
"syncVSCodeFailed": "同步 VS Code 失败",
|
||||
"appliedToClaudePlugin": "已应用到 Claude 插件",
|
||||
"removedFromClaudePlugin": "已从 Claude 插件移除",
|
||||
"syncClaudePluginFailed": "同步 Claude 插件失败"
|
||||
@@ -101,7 +93,6 @@
|
||||
"providerSwitchReceived": "收到供应商切换事件:",
|
||||
"setupListenerFailed": "设置供应商切换监听器失败:",
|
||||
"updateProviderFailed": "更新供应商失败:",
|
||||
"syncToVSCodeFailed": "同步到VS Code失败:",
|
||||
"autoImportFailed": "自动导入默认配置失败:",
|
||||
"openLinkFailed": "打开链接失败:",
|
||||
"getVersionFailed": "获取版本信息失败:",
|
||||
|
||||
@@ -197,8 +197,6 @@ export const tauriAPI = {
|
||||
});
|
||||
},
|
||||
|
||||
// (保留空位,取消迁移提示)
|
||||
|
||||
// 选择配置目录
|
||||
selectConfigDirectory: async (
|
||||
defaultPath?: string,
|
||||
@@ -275,38 +273,6 @@ export const tauriAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 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)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:获取 ~/.claude/config.json 状态
|
||||
getClaudePluginStatus: async (): Promise<ConfigStatus> => {
|
||||
try {
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
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 的异常情况:
|
||||
// 1. 早期遗留的标量值(字符串/数字/null等)
|
||||
// 2. 空对象
|
||||
// 3. 数组类型
|
||||
try {
|
||||
const data = parse(out) as any;
|
||||
const cfg = data?.["chatgpt.config"];
|
||||
|
||||
// 需要清理的情况:
|
||||
// - 标量值(null、字符串、数字等)
|
||||
// - 数组
|
||||
// - 空对象
|
||||
const shouldRemove = cfg !== undefined && (
|
||||
cfg === null ||
|
||||
typeof cfg !== "object" ||
|
||||
Array.isArray(cfg) ||
|
||||
(typeof cfg === "object" && Object.keys(cfg).length === 0)
|
||||
);
|
||||
|
||||
if (shouldRemove) {
|
||||
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
@@ -42,10 +42,6 @@ declare global {
|
||||
isPortable: () => Promise<boolean>;
|
||||
getAppConfigPath: () => Promise<string>;
|
||||
openAppConfigFolder: () => Promise<void>;
|
||||
// VS Code settings.json 能力
|
||||
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
||||
readVSCodeSettings: () => Promise<string>;
|
||||
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
||||
// Claude 插件配置能力
|
||||
getClaudePluginStatus: () => Promise<ConfigStatus>;
|
||||
readClaudePluginConfig: () => Promise<string | null>;
|
||||
|
||||
Reference in New Issue
Block a user