mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 10:21:16 +08:00
fix(claude-plugin): sync current provider config to settings.json (#1905)
* fix(claude-plugin): sync current provider config to settings.json on toggle enable
- Extract syncClaudePluginIfChanged to share logic between autoSaveSettings and saveSettings
- Fix P1: enableClaudePluginIntegration toggle in General tab now actually syncs ~/.claude/settings.json
- Fix P2: check syncCurrentProvidersLiveSafe() return value and show toast on failure
- Fix P3: sync providers on both enable and disable, not just enable
- Fix P4: avoid double syncCurrentProvidersLiveSafe when plugin toggle + dir change happen together
- Remove duplicate comment
- Add missing providersApi.getCurrent/getAll mocks in tests
* style: reformat after rebase onto main
Prettier flagged a line-break introduced by the openclaw directory
change (from main) after rebase.
* fix(claude-plugin): read prev enabled state from live cache to avoid stale closure
syncClaudePluginIfChanged compared enabled against data?.enableClaudePluginIntegration
captured in a useCallback closure. After invalidateQueries + refetch, the React
Query cache is up to date, but the consuming hook's closure does not see the new
value until React re-renders. Quick on->off toggles could therefore skip
applyClaudePluginConfig, leaving ~/.claude/config.json in the previously enabled
state even though settings.json was persisted as disabled.
Read the previous value synchronously from queryClient.getQueryData(["settings"])
before saveMutation.mutateAsync(), then pass it to the helper as prevEnabled.
getQueryData bypasses the closure and reflects the live cache at call time.
Test covers the race: closure data stays at false while the cache reports true;
the helper must still call applyClaudePluginConfig({ official: true }).
---------
Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
+86
-32
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { providersApi, settingsApi, type AppId } from "@/lib/api";
|
||||
import { syncCurrentProvidersLiveSafe } from "@/utils/postChangeSync";
|
||||
import { useSettingsQuery, useSaveSettingsMutation } from "@/lib/query";
|
||||
@@ -63,6 +64,7 @@ export function useSettings(): UseSettingsResult {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSettingsQuery();
|
||||
const saveMutation = useSaveSettingsMutation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// 1️⃣ 表单状态管理
|
||||
const {
|
||||
@@ -122,6 +124,58 @@ export function useSettings(): UseSettingsResult {
|
||||
setRequiresRestart,
|
||||
]);
|
||||
|
||||
// 同步 Claude 插件集成配置到 ~/.claude/settings.json
|
||||
// 返回 true 表示已执行过 syncCurrentProvidersLiveSafe,调用方可跳过重复同步
|
||||
// prevEnabled 必须由调用方在 saveMutation 之前从实时缓存(queryClient.getQueryData)捕获,
|
||||
// 避免 useCallback closure 中 data 因未 re-render 而滞后导致的快速连切 race。
|
||||
const syncClaudePluginIfChanged = useCallback(
|
||||
async (
|
||||
enabled: boolean | undefined,
|
||||
prevEnabled: boolean | undefined,
|
||||
): Promise<boolean> => {
|
||||
if (enabled === undefined || enabled === prevEnabled) return false;
|
||||
try {
|
||||
if (enabled) {
|
||||
const currentId = await providersApi.getCurrent("claude");
|
||||
let isOfficial = false;
|
||||
if (currentId) {
|
||||
const allProviders = await providersApi.getAll("claude");
|
||||
isOfficial = allProviders[currentId]?.category === "official";
|
||||
}
|
||||
await settingsApi.applyClaudePluginConfig({ official: isOfficial });
|
||||
} else {
|
||||
await settingsApi.applyClaudePluginConfig({ official: true });
|
||||
}
|
||||
|
||||
const syncResult = await syncCurrentProvidersLiveSafe();
|
||||
if (!syncResult.ok) {
|
||||
console.warn(
|
||||
"[useSettings] Failed to sync providers after toggling Claude plugin",
|
||||
syncResult.error,
|
||||
);
|
||||
toast.error(
|
||||
t("notifications.syncClaudePluginFailed", {
|
||||
defaultValue: "同步 Claude 插件失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[useSettings] Failed to sync Claude plugin config",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("notifications.syncClaudePluginFailed", {
|
||||
defaultValue: "同步 Claude 插件失败",
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// 即时保存设置(用于 General 标签页的实时更新)
|
||||
// 保存基础配置 + 独立的系统 API 调用(开机自启)
|
||||
const autoSaveSettings = useCallback(
|
||||
@@ -152,6 +206,12 @@ export function useSettings(): UseSettingsResult {
|
||||
language: mergedSettings.language,
|
||||
};
|
||||
|
||||
// 在 mutate 之前从实时缓存捕获上一次持久化的插件集成状态,
|
||||
// 避免 closure 里的 data 因 React 尚未 re-render 而滞后
|
||||
const prevPluginEnabled = queryClient.getQueryData<Settings>([
|
||||
"settings",
|
||||
])?.enableClaudePluginIntegration;
|
||||
|
||||
// 保存到配置文件
|
||||
await saveMutation.mutateAsync(payload);
|
||||
|
||||
@@ -202,6 +262,11 @@ export function useSettings(): UseSettingsResult {
|
||||
}
|
||||
}
|
||||
|
||||
await syncClaudePluginIfChanged(
|
||||
payload.enableClaudePluginIntegration,
|
||||
prevPluginEnabled,
|
||||
);
|
||||
|
||||
// 持久化语言偏好
|
||||
try {
|
||||
if (typeof window !== "undefined" && updates.language) {
|
||||
@@ -233,7 +298,7 @@ export function useSettings(): UseSettingsResult {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[data, saveMutation, settings, t],
|
||||
[data, queryClient, saveMutation, settings, syncClaudePluginIfChanged, t],
|
||||
);
|
||||
|
||||
// 完整保存设置(用于 Advanced 标签页的手动保存)
|
||||
@@ -275,6 +340,12 @@ export function useSettings(): UseSettingsResult {
|
||||
language: mergedSettings.language,
|
||||
};
|
||||
|
||||
// 在 mutate 之前从实时缓存捕获上一次持久化的插件集成状态,
|
||||
// 避免 closure 里的 data 因 React 尚未 re-render 而滞后
|
||||
const prevPluginEnabled = queryClient.getQueryData<Settings>([
|
||||
"settings",
|
||||
])?.enableClaudePluginIntegration;
|
||||
|
||||
await saveMutation.mutateAsync(payload);
|
||||
|
||||
await settingsApi.setAppConfigDirOverride(sanitizedAppDir ?? null);
|
||||
@@ -323,30 +394,10 @@ export function useSettings(): UseSettingsResult {
|
||||
}
|
||||
}
|
||||
|
||||
// 只在 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 插件失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
const pluginSynced = await syncClaudePluginIfChanged(
|
||||
payload.enableClaudePluginIntegration,
|
||||
prevPluginEnabled,
|
||||
);
|
||||
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -369,18 +420,19 @@ export function useSettings(): UseSettingsResult {
|
||||
}
|
||||
|
||||
// 如果 Claude/Codex/Gemini/OpenCode/OpenClaw 的目录覆盖发生变化,则立即将"当前使用的供应商"写回对应应用的 live 配置
|
||||
// 如果插件同步已经执行过 syncCurrentProvidersLiveSafe,则跳过避免重复
|
||||
const claudeDirChanged = sanitizedClaudeDir !== previousClaudeDir;
|
||||
const codexDirChanged = sanitizedCodexDir !== previousCodexDir;
|
||||
const geminiDirChanged = sanitizedGeminiDir !== previousGeminiDir;
|
||||
const opencodeDirChanged = sanitizedOpencodeDir !== previousOpencodeDir;
|
||||
const openclawDirChanged =
|
||||
sanitizedOpenclawDir !== previousOpenclawDir;
|
||||
const openclawDirChanged = sanitizedOpenclawDir !== previousOpenclawDir;
|
||||
if (
|
||||
claudeDirChanged ||
|
||||
codexDirChanged ||
|
||||
geminiDirChanged ||
|
||||
opencodeDirChanged ||
|
||||
openclawDirChanged
|
||||
!pluginSynced &&
|
||||
(claudeDirChanged ||
|
||||
codexDirChanged ||
|
||||
geminiDirChanged ||
|
||||
opencodeDirChanged ||
|
||||
openclawDirChanged)
|
||||
) {
|
||||
const syncResult = await syncCurrentProvidersLiveSafe();
|
||||
if (!syncResult.ok) {
|
||||
@@ -419,9 +471,11 @@ export function useSettings(): UseSettingsResult {
|
||||
appConfigDir,
|
||||
data,
|
||||
initialAppConfigDir,
|
||||
queryClient,
|
||||
saveMutation,
|
||||
settings,
|
||||
setRequiresRestart,
|
||||
syncClaudePluginIfChanged,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user