Remove deprecated VS Code Codex integration

This commit is contained in:
Jason
2025-10-03 20:03:55 +08:00
parent d86994eb7e
commit e0908701b4
15 changed files with 9 additions and 629 deletions

View File

@@ -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);
}

View File

@@ -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 && (

View File

@@ -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">

View File

@@ -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,
};
}

View File

@@ -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:",

View File

@@ -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": "获取版本信息失败:",

View File

@@ -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 {

View File

@@ -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.jsonJSONC 文本)中是否已经应用了我们的键
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
View File

@@ -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>;