fix: improve VS Code config synchronization and code formatting

- Add automatic VS Code config sync when base_url changes in TOML
- Improve error handling for VS Code configuration writes
- Enhance state management with ref tracking to prevent duplicate API calls
- Fix code formatting issues and improve readability across components
- Optimize common configuration handling for both Claude and Codex providers
This commit is contained in:
Jason
2025-09-18 15:25:10 +08:00
parent 32e66e054b
commit 463e430a3d
7 changed files with 195 additions and 106 deletions

View File

@@ -62,7 +62,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
return diagnostics;
}),
[showValidation]
[showValidation],
);
useEffect(() => {

View File

@@ -131,22 +131,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// Codex 通用配置状态
const [useCodexCommonConfig, setUseCodexCommonConfig] = useState(false);
const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] = useState<string>(() => {
if (typeof window === "undefined") {
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
}
try {
const stored = window.localStorage.getItem(
CODEX_COMMON_CONFIG_STORAGE_KEY,
);
if (stored && stored.trim()) {
return stored;
const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] =
useState<string>(() => {
if (typeof window === "undefined") {
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
}
} catch {
// ignore localStorage 读取失败
}
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
});
try {
const stored = window.localStorage.getItem(
CODEX_COMMON_CONFIG_STORAGE_KEY,
);
if (stored && stored.trim()) {
return stored;
}
} catch {
// ignore localStorage 读取失败
}
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
});
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
const isUpdatingFromCodexCommonConfig = useRef(false);
// -1 表示自定义null 表示未选择,>= 0 表示预设索引
@@ -217,7 +218,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useEffect(() => {
if (initialData) {
if (!isCodex) {
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
const configString = JSON.stringify(
initialData.settingsConfig,
null,
2,
);
const hasCommon = hasCommonConfigSnippet(
configString,
commonConfigSnippet,
@@ -235,7 +240,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
};
if (config.env) {
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
setClaudeSmallFastModel(
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
);
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
// 初始化 Kimi 模型选择
@@ -254,7 +261,13 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
setUseCodexCommonConfig(hasCommon);
}
}
}, [initialData, commonConfigSnippet, codexCommonConfigSnippet, isCodex, codexConfig]);
}, [
initialData,
commonConfigSnippet,
codexCommonConfigSnippet,
isCodex,
codexConfig,
]);
// 当选择预设变化时,同步类别
useEffect(() => {
@@ -529,10 +542,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
setBaseUrl(""); // 清空基础 URL
// 同步通用配置状态
const hasCommon = hasCommonConfigSnippet(
configString,
commonConfigSnippet,
);
const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet);
setUseCommonConfig(hasCommon);
setCommonConfigError("");
@@ -643,10 +653,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
updateSettingsConfigValue(configString);
// 同步通用配置开关
const hasCommon = hasCommonConfigSnippet(
configString,
commonConfigSnippet,
);
const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet);
setUseCommonConfig(hasCommon);
};
@@ -681,11 +688,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// Codex: 处理通用配置开关
const handleCodexCommonConfigToggle = (checked: boolean) => {
const { updatedConfig, error: snippetError } = updateTomlCommonConfigSnippet(
codexConfig,
codexCommonConfigSnippet,
checked,
);
const { updatedConfig, error: snippetError } =
updateTomlCommonConfigSnippet(
codexConfig,
codexCommonConfigSnippet,
checked,
);
if (snippetError) {
setCodexCommonConfigError(snippetError);
@@ -753,10 +761,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 保存 Codex 通用配置到 localStorage
if (typeof window !== "undefined") {
try {
window.localStorage.setItem(
CODEX_COMMON_CONFIG_STORAGE_KEY,
value,
);
window.localStorage.setItem(CODEX_COMMON_CONFIG_STORAGE_KEY, value);
} catch {
// ignore localStorage 写入失败
}
@@ -1177,7 +1182,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useCommonConfig={useCodexCommonConfig}
onCommonConfigToggle={handleCodexCommonConfigToggle}
commonConfigSnippet={codexCommonConfigSnippet}
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
onCommonConfigSnippetChange={
handleCodexCommonConfigSnippetChange
}
commonConfigError={codexCommonConfigError}
authError={codexAuthError}
/>

View File

@@ -120,9 +120,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
rows={12}
/>
{configError && (
<p className="text-xs text-red-500 dark:text-red-400">
{configError}
</p>
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
)}
<p className="text-xs text-gray-500 dark:text-gray-400">
Claude Code settings.json

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { X, Save } from "lucide-react";
import { extractBaseUrlFromToml } from "../../utils/providerConfigUtils";
@@ -34,6 +34,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
const [vscodeError, setVscodeError] = useState("");
const [vscodeSuccess, setVscodeSuccess] = useState("");
const [isWritingVscode, setIsWritingVscode] = useState(false);
const lastAppliedBaseUrlRef = useRef<string | null>(null);
useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) {
@@ -49,6 +50,61 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
return () => window.clearTimeout(timer);
}, [vscodeSuccess]);
const ensureVscodeApiAvailable = () => {
if (typeof window === "undefined" || !window.api?.writeVscodeSettings) {
setVscodeError("当前环境暂不支持写入 VS Code 配置");
return false;
}
return true;
};
const applyVscodeConfig = async (
baseUrl: string,
successMessage = "已写入 VS Code 配置",
) => {
if (!ensureVscodeApiAvailable()) {
return false;
}
setIsWritingVscode(true);
try {
const success = await window.api.writeVscodeSettings(baseUrl);
if (success) {
setVscodeSuccess(successMessage);
lastAppliedBaseUrlRef.current = baseUrl;
return true;
}
setVscodeError("写入 VS Code 配置失败,请稍后重试");
} catch (error) {
setVscodeError(`写入 VS Code 配置失败: ${String(error)}`);
} finally {
setIsWritingVscode(false);
}
return false;
};
const removeVscodeConfig = async () => {
if (!ensureVscodeApiAvailable()) {
return false;
}
setIsWritingVscode(true);
try {
const success = await window.api.writeVscodeSettings();
if (success) {
setVscodeSuccess("已移除 VS Code 配置");
lastAppliedBaseUrlRef.current = null;
return true;
}
setVscodeError("移除 VS Code 配置失败,请稍后重试");
} catch (error) {
setVscodeError(`移除 VS Code 配置失败: ${String(error)}`);
} finally {
setIsWritingVscode(false);
}
return false;
};
const handleVscodeConfigToggle = async (checked: boolean) => {
if (isWritingVscode) return;
@@ -56,12 +112,6 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
setVscodeError("");
setVscodeSuccess("");
if (typeof window === "undefined" || !window.api?.writeVscodeSettings) {
setVscodeError("当前环境暂不支持写入 VS Code 配置");
setWriteVscodeConfig(!checked);
return;
}
if (checked) {
const trimmed = configValue.trim();
if (!trimmed) {
@@ -77,41 +127,65 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
return;
}
const success = await applyVscodeConfig(baseUrl);
if (!success) {
setWriteVscodeConfig(false);
}
return;
}
const success = await removeVscodeConfig();
if (!success) {
setWriteVscodeConfig(true);
}
};
useEffect(() => {
if (!writeVscodeConfig || isWritingVscode) {
return;
}
const trimmed = configValue.trim();
if (!trimmed) {
return;
}
const baseUrl = extractBaseUrlFromToml(trimmed);
if (!baseUrl) {
setVscodeError("未在 config.toml 中找到 base_url 字段");
setWriteVscodeConfig(false);
return;
}
if (lastAppliedBaseUrlRef.current === baseUrl) {
return;
}
const sync = async () => {
// 直接调用 API 而不依赖 applyVscodeConfig 函数,避免闭包问题
if (typeof window === "undefined" || !window.api?.writeVscodeSettings) {
setVscodeError("当前环境暂不支持写入 VS Code 配置");
return;
}
setIsWritingVscode(true);
try {
const success = await window.api.writeVscodeSettings(baseUrl);
if (success) {
setVscodeSuccess("已写入 VS Code 配置");
setVscodeSuccess("已更新 VS Code 配置");
lastAppliedBaseUrlRef.current = baseUrl;
} else {
setVscodeError("写入 VS Code 配置失败,请稍后重试");
setWriteVscodeConfig(false);
}
} catch (error) {
setVscodeError(`写入 VS Code 配置失败: ${String(error)}`);
setWriteVscodeConfig(false);
} finally {
setIsWritingVscode(false);
}
};
return;
}
setIsWritingVscode(true);
try {
const success = await window.api.writeVscodeSettings();
if (success) {
setVscodeSuccess("已移除 VS Code 配置");
} else {
setVscodeError("移除 VS Code 配置失败,请稍后重试");
setWriteVscodeConfig(true);
}
} catch (error) {
setVscodeError(`移除 VS Code 配置失败: ${String(error)}`);
setWriteVscodeConfig(true);
} finally {
setIsWritingVscode(false);
}
};
sync();
}, [configValue, writeVscodeConfig, isWritingVscode]);
// 支持按下 ESC 关闭弹窗
useEffect(() => {
@@ -162,9 +236,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
data-enable-grammarly="false"
/>
{authError && (
<p className="text-xs text-red-500 dark:text-red-400">
{authError}
</p>
<p className="text-xs text-red-500 dark:text-red-400">{authError}</p>
)}
<p className="text-xs text-gray-500 dark:text-gray-400">
Codex auth.json
@@ -201,7 +273,10 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
</p>
)}
{vscodeError && (
<p className="text-xs text-red-500 dark:text-red-400 text-right truncate" title={vscodeError}>
<p
className="text-xs text-red-500 dark:text-red-400 text-right truncate"
title={vscodeError}
>
{vscodeError}
</p>
)}
@@ -227,7 +302,10 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
</button>
{commonConfigError && !isCommonConfigModalOpen && (
<p className="text-xs text-red-500 dark:text-red-400 mt-1 max-w-[120px] truncate" title={commonConfigError}>
<p
className="text-xs text-red-500 dark:text-red-400 mt-1 max-w-[120px] truncate"
title={commonConfigError}
>
{commonConfigError}
</p>
)}

View File

@@ -22,7 +22,10 @@ const deepMerge = (
return target;
};
const deepRemove = (target: Record<string, any>, source: Record<string, any>) => {
const deepRemove = (
target: Record<string, any>,
source: Record<string, any>,
) => {
Object.entries(source).forEach(([key, value]) => {
if (!(key in target)) return;
@@ -59,7 +62,7 @@ const isSubset = (target: any, source: any): boolean => {
const deepClone = <T>(obj: T): T => {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj.getTime()) as T;
if (obj instanceof Array) return obj.map(item => deepClone(item)) as T;
if (obj instanceof Array) return obj.map((item) => deepClone(item)) as T;
if (obj instanceof Object) {
const clonedObj = {} as T;
for (const key in obj) {
@@ -78,7 +81,10 @@ export interface UpdateCommonConfigResult {
}
// 验证JSON配置格式
export const validateJsonConfig = (value: string, fieldName: string = "配置"): string => {
export const validateJsonConfig = (
value: string,
fieldName: string = "配置",
): string => {
if (!value.trim()) {
return "";
}
@@ -254,13 +260,13 @@ const removeTomlCommonConfig = (tomlString: string): string => {
// 找到标记前的换行符(如果有)
let realStartIdx = startIdx;
if (startIdx > 0 && tomlString[startIdx - 1] === '\n') {
if (startIdx > 0 && tomlString[startIdx - 1] === "\n") {
realStartIdx = startIdx - 1;
}
// 找到标记后的换行符(如果有)
let realEndIdx = endIdx + COMMON_CONFIG_MARKER_END.length;
if (realEndIdx < tomlString.length && tomlString[realEndIdx] === '\n') {
if (realEndIdx < tomlString.length && tomlString[realEndIdx] === "\n") {
realEndIdx = realEndIdx + 1;
}