feat(settings): add autoSaveSettings for lightweight auto-save
Add optimized auto-save function for General tab settings. - Add autoSaveSettings method for non-destructive auto-save - Only trigger system APIs when values actually change - Avoid unnecessary auto-launch and plugin config updates - Update tests to cover new functionality
This commit is contained in:
@@ -37,6 +37,9 @@ export interface UseSettingsResult {
|
||||
overrides?: Partial<SettingsFormState>,
|
||||
options?: { silent?: boolean },
|
||||
) => Promise<SaveResult | null>;
|
||||
autoSaveSettings: (
|
||||
updates: Partial<SettingsFormState>,
|
||||
) => Promise<SaveResult | null>;
|
||||
resetSettings: () => void;
|
||||
acknowledgeRestart: () => void;
|
||||
}
|
||||
@@ -117,7 +120,82 @@ export function useSettings(): UseSettingsResult {
|
||||
setRequiresRestart,
|
||||
]);
|
||||
|
||||
// 保存设置
|
||||
// 即时保存设置(用于 General 标签页的实时更新)
|
||||
// 保存基础配置 + 独立的系统 API 调用(开机自启)
|
||||
const autoSaveSettings = useCallback(
|
||||
async (updates: Partial<SettingsFormState>): Promise<SaveResult | null> => {
|
||||
const mergedSettings = settings ? { ...settings, ...updates } : null;
|
||||
if (!mergedSettings) return null;
|
||||
|
||||
try {
|
||||
const sanitizedClaudeDir = sanitizeDir(mergedSettings.claudeConfigDir);
|
||||
const sanitizedCodexDir = sanitizeDir(mergedSettings.codexConfigDir);
|
||||
const sanitizedGeminiDir = sanitizeDir(mergedSettings.geminiConfigDir);
|
||||
|
||||
const payload: Settings = {
|
||||
...mergedSettings,
|
||||
claudeConfigDir: sanitizedClaudeDir,
|
||||
codexConfigDir: sanitizedCodexDir,
|
||||
geminiConfigDir: sanitizedGeminiDir,
|
||||
language: mergedSettings.language,
|
||||
};
|
||||
|
||||
// 保存到配置文件
|
||||
await saveMutation.mutateAsync(payload);
|
||||
|
||||
// 如果开机自启状态改变,调用系统 API
|
||||
if (
|
||||
payload.launchOnStartup !== undefined &&
|
||||
payload.launchOnStartup !== data?.launchOnStartup
|
||||
) {
|
||||
try {
|
||||
await settingsApi.setAutoLaunch(payload.launchOnStartup);
|
||||
} catch (error) {
|
||||
console.error("Failed to update auto-launch:", error);
|
||||
toast.error(
|
||||
t("settings.autoLaunchFailed", {
|
||||
defaultValue: "设置开机自启失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化语言偏好
|
||||
try {
|
||||
if (typeof window !== "undefined" && updates.language) {
|
||||
window.localStorage.setItem("language", updates.language);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[useSettings] Failed to persist language preference",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// 更新托盘菜单
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (error) {
|
||||
console.warn("[useSettings] Failed to refresh tray menu", error);
|
||||
}
|
||||
|
||||
return { requiresRestart: false };
|
||||
} catch (error) {
|
||||
console.error("[useSettings] Failed to auto-save settings", error);
|
||||
toast.error(
|
||||
t("notifications.settingsSaveFailed", {
|
||||
defaultValue: "保存设置失败: {{error}}",
|
||||
error: (error as Error)?.message ?? String(error),
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[data, saveMutation, settings, t],
|
||||
);
|
||||
|
||||
// 完整保存设置(用于 Advanced 标签页的手动保存)
|
||||
// 包含所有系统 API 调用和完整的验证流程
|
||||
const saveSettings = useCallback(
|
||||
async (
|
||||
overrides?: Partial<SettingsFormState>,
|
||||
@@ -147,8 +225,11 @@ export function useSettings(): UseSettingsResult {
|
||||
|
||||
await settingsApi.setAppConfigDirOverride(sanitizedAppDir ?? null);
|
||||
|
||||
// 如果开机自启状态改变,调用系统 API
|
||||
if (payload.launchOnStartup !== undefined) {
|
||||
// 只在开机自启状态真正改变时调用系统 API
|
||||
if (
|
||||
payload.launchOnStartup !== undefined &&
|
||||
payload.launchOnStartup !== data?.launchOnStartup
|
||||
) {
|
||||
try {
|
||||
await settingsApi.setAutoLaunch(payload.launchOnStartup);
|
||||
} catch (error) {
|
||||
@@ -161,22 +242,29 @@ export function useSettings(): UseSettingsResult {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (payload.enableClaudePluginIntegration) {
|
||||
await settingsApi.applyClaudePluginConfig({ official: false });
|
||||
} else {
|
||||
await settingsApi.applyClaudePluginConfig({ official: true });
|
||||
// 只在 Claude 插件集成状态真正改变时调用系统 API
|
||||
if (
|
||||
payload.enableClaudePluginIntegration !== undefined &&
|
||||
payload.enableClaudePluginIntegration !==
|
||||
data?.enableClaudePluginIntegration
|
||||
) {
|
||||
try {
|
||||
if (payload.enableClaudePluginIntegration) {
|
||||
await settingsApi.applyClaudePluginConfig({ official: false });
|
||||
} else {
|
||||
await settingsApi.applyClaudePluginConfig({ official: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[useSettings] Failed to sync Claude plugin config",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("notifications.syncClaudePluginFailed", {
|
||||
defaultValue: "同步 Claude 插件失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[useSettings] Failed to sync Claude plugin config",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("notifications.syncClaudePluginFailed", {
|
||||
defaultValue: "同步 Claude 插件失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -268,6 +356,7 @@ export function useSettings(): UseSettingsResult {
|
||||
resetDirectory,
|
||||
resetAppConfigDir,
|
||||
saveSettings,
|
||||
autoSaveSettings,
|
||||
resetSettings,
|
||||
acknowledgeRestart,
|
||||
};
|
||||
|
||||
@@ -84,6 +84,7 @@ describe("useDirectorySettings", () => {
|
||||
appConfig: "/override/app",
|
||||
claude: "/remote/claude",
|
||||
codex: "/remote/codex",
|
||||
gemini: "/remote/codex", // Gemini 使用 codex 作为默认
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("useSettings hook", () => {
|
||||
it("saves settings and flags restart when app config directory changes", async () => {
|
||||
serverSettings = {
|
||||
...serverSettings,
|
||||
enableClaudePluginIntegration: true,
|
||||
enableClaudePluginIntegration: false,
|
||||
claudeConfigDir: "/server/claude",
|
||||
codexConfigDir: undefined,
|
||||
language: "en",
|
||||
@@ -159,7 +159,7 @@ describe("useSettings hook", () => {
|
||||
claudeConfigDir: " /custom/claude ",
|
||||
codexConfigDir: " ",
|
||||
language: "en",
|
||||
enableClaudePluginIntegration: true,
|
||||
enableClaudePluginIntegration: true, // 状态从 false 变为 true
|
||||
},
|
||||
initialLanguage: "en",
|
||||
});
|
||||
@@ -183,6 +183,7 @@ describe("useSettings hook", () => {
|
||||
expect(payload.codexConfigDir).toBeUndefined();
|
||||
expect(payload.language).toBe("en");
|
||||
expect(setAppConfigDirOverrideMock).toHaveBeenCalledWith("/override/app");
|
||||
// 状态改变,应该调用 API
|
||||
expect(applyClaudePluginConfigMock).toHaveBeenCalledWith({
|
||||
official: false,
|
||||
});
|
||||
@@ -194,10 +195,22 @@ describe("useSettings hook", () => {
|
||||
});
|
||||
|
||||
it("saves settings without restart when directory unchanged", async () => {
|
||||
// 确保服务器和本地状态一致,不触发 API 调用
|
||||
serverSettings = {
|
||||
...serverSettings,
|
||||
enableClaudePluginIntegration: false,
|
||||
launchOnStartup: false,
|
||||
};
|
||||
useSettingsQueryMock.mockReturnValue({
|
||||
data: serverSettings,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
settingsFormMock = createSettingsFormMock({
|
||||
settings: {
|
||||
...serverSettings,
|
||||
enableClaudePluginIntegration: false,
|
||||
enableClaudePluginIntegration: false, // 状态未变
|
||||
launchOnStartup: false, // 状态未变
|
||||
language: "zh",
|
||||
},
|
||||
initialLanguage: "zh",
|
||||
@@ -217,19 +230,28 @@ describe("useSettings hook", () => {
|
||||
|
||||
expect(saveResult).toEqual({ requiresRestart: false });
|
||||
expect(setAppConfigDirOverrideMock).toHaveBeenCalledWith(null);
|
||||
expect(applyClaudePluginConfigMock).toHaveBeenCalledWith({
|
||||
official: true,
|
||||
});
|
||||
// 状态未改变,不应调用 API
|
||||
expect(applyClaudePluginConfigMock).not.toHaveBeenCalled();
|
||||
expect(metadataMock.setRequiresRestart).toHaveBeenCalledWith(false);
|
||||
// 目录未变化,不应触发同步
|
||||
expect(syncCurrentProvidersLiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows toast when Claude plugin sync fails but continues flow", async () => {
|
||||
// 设置服务器状态为 false,本地状态为 true,触发状态变化
|
||||
serverSettings = {
|
||||
...serverSettings,
|
||||
enableClaudePluginIntegration: false,
|
||||
};
|
||||
useSettingsQueryMock.mockReturnValue({
|
||||
data: serverSettings,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
settingsFormMock = createSettingsFormMock({
|
||||
settings: {
|
||||
...serverSettings,
|
||||
enableClaudePluginIntegration: true,
|
||||
enableClaudePluginIntegration: true, // 状态改变
|
||||
language: "zh",
|
||||
},
|
||||
});
|
||||
@@ -286,6 +308,7 @@ describe("useSettings hook", () => {
|
||||
expect(directorySettingsMock.resetAllDirectories).toHaveBeenCalledWith(
|
||||
"/server/claude",
|
||||
undefined,
|
||||
undefined, // geminiConfigDir
|
||||
);
|
||||
expect(metadataMock.setRequiresRestart).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user