mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
3afec8a10f
Four improvements to the database backup mechanism: 1. Auto backup before schema migration - creates a snapshot when upgrading from an older database version, providing a safety net beyond the existing SAVEPOINT rollback mechanism. 2. Periodic startup backup - checks on app launch whether the latest backup is older than 24 hours and creates a new one if needed, ensuring all users have recent backups regardless of usage patterns. 3. Backfill failure notification - switch now returns SwitchResult with warnings instead of silently ignoring backfill errors, so users are informed when their manual config changes may not have been saved. 4. Backup management UI - new BackupListSection in Settings > Data Management showing all backup snapshots with restore capability, including a confirmation dialog and automatic safety backup before restore.
243 lines
6.8 KiB
TypeScript
243 lines
6.8 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { providersApi, settingsApi, type AppId } from "@/lib/api";
|
|
import type { SwitchResult } from "@/lib/api/providers";
|
|
import type { Provider, Settings } from "@/types";
|
|
import { extractErrorMessage } from "@/utils/errorUtils";
|
|
import { generateUUID } from "@/utils/uuid";
|
|
import { openclawKeys } from "@/hooks/useOpenClaw";
|
|
|
|
export const useAddProviderMutation = (appId: AppId) => {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation({
|
|
mutationFn: async (
|
|
providerInput: Omit<Provider, "id"> & { providerKey?: string },
|
|
) => {
|
|
let id: string;
|
|
|
|
if (appId === "opencode" || appId === "openclaw") {
|
|
if (providerInput.category === "omo") {
|
|
id = `omo-${generateUUID()}`;
|
|
} else {
|
|
if (!providerInput.providerKey) {
|
|
throw new Error(`Provider key is required for ${appId}`);
|
|
}
|
|
id = providerInput.providerKey;
|
|
}
|
|
} else {
|
|
id = generateUUID();
|
|
}
|
|
|
|
const { providerKey: _providerKey, ...rest } = providerInput;
|
|
|
|
const newProvider: Provider = {
|
|
...rest,
|
|
id,
|
|
createdAt: Date.now(),
|
|
};
|
|
delete (newProvider as any).providerKey;
|
|
|
|
await providersApi.add(newProvider, appId);
|
|
return newProvider;
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
|
|
|
if (appId === "opencode") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo", "current-provider-id"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo", "provider-count"],
|
|
});
|
|
}
|
|
|
|
try {
|
|
await providersApi.updateTrayMenu();
|
|
} catch (trayError) {
|
|
console.error(
|
|
"Failed to update tray menu after adding provider",
|
|
trayError,
|
|
);
|
|
}
|
|
|
|
toast.success(
|
|
t("notifications.providerAdded", {
|
|
defaultValue: "供应商已添加",
|
|
}),
|
|
{
|
|
closeButton: true,
|
|
},
|
|
);
|
|
},
|
|
onError: (error: Error) => {
|
|
const detail = extractErrorMessage(error) || t("common.unknown");
|
|
toast.error(
|
|
t("notifications.addFailed", {
|
|
defaultValue: "添加供应商失败: {{error}}",
|
|
error: detail,
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useUpdateProviderMutation = (appId: AppId) => {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation({
|
|
mutationFn: async (provider: Provider) => {
|
|
await providersApi.update(provider, appId);
|
|
return provider;
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
|
toast.success(
|
|
t("notifications.updateSuccess", {
|
|
defaultValue: "供应商更新成功",
|
|
}),
|
|
{
|
|
closeButton: true,
|
|
},
|
|
);
|
|
},
|
|
onError: (error: Error) => {
|
|
const detail = extractErrorMessage(error) || t("common.unknown");
|
|
toast.error(
|
|
t("notifications.updateFailed", {
|
|
defaultValue: "更新供应商失败: {{error}}",
|
|
error: detail,
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useDeleteProviderMutation = (appId: AppId) => {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation({
|
|
mutationFn: async (providerId: string) => {
|
|
await providersApi.delete(providerId, appId);
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
|
|
|
if (appId === "opencode") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo", "current-provider-id"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo", "provider-count"],
|
|
});
|
|
}
|
|
|
|
try {
|
|
await providersApi.updateTrayMenu();
|
|
} catch (trayError) {
|
|
console.error(
|
|
"Failed to update tray menu after deleting provider",
|
|
trayError,
|
|
);
|
|
}
|
|
|
|
toast.success(
|
|
t("notifications.deleteSuccess", {
|
|
defaultValue: "供应商已删除",
|
|
}),
|
|
{
|
|
closeButton: true,
|
|
},
|
|
);
|
|
},
|
|
onError: (error: Error) => {
|
|
const detail = extractErrorMessage(error) || t("common.unknown");
|
|
toast.error(
|
|
t("notifications.deleteFailed", {
|
|
defaultValue: "删除供应商失败: {{error}}",
|
|
error: detail,
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useSwitchProviderMutation = (appId: AppId) => {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation({
|
|
mutationFn: async (providerId: string): Promise<SwitchResult> => {
|
|
return await providersApi.switch(providerId, appId);
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
|
|
|
// OpenCode/OpenClaw: also invalidate live provider IDs cache to update button state
|
|
if (appId === "opencode") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["opencodeLiveProviderIds"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo", "current-provider-id"],
|
|
});
|
|
}
|
|
if (appId === "openclaw") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.liveProviderIds,
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.defaultModel,
|
|
});
|
|
}
|
|
|
|
try {
|
|
await providersApi.updateTrayMenu();
|
|
} catch (trayError) {
|
|
console.error(
|
|
"Failed to update tray menu after switching provider",
|
|
trayError,
|
|
);
|
|
}
|
|
},
|
|
onError: (error: Error) => {
|
|
const detail = extractErrorMessage(error) || t("common.unknown");
|
|
|
|
toast.error(
|
|
t("notifications.switchFailedTitle", { defaultValue: "切换失败" }),
|
|
{
|
|
description: t("notifications.switchFailed", {
|
|
defaultValue: "切换失败:{{error}}",
|
|
error: detail,
|
|
}),
|
|
duration: 6000,
|
|
action: {
|
|
label: t("common.copy", { defaultValue: "复制" }),
|
|
onClick: () => {
|
|
navigator.clipboard?.writeText(detail).catch(() => undefined);
|
|
},
|
|
},
|
|
},
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useSaveSettingsMutation = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (settings: Settings) => {
|
|
await settingsApi.save(settings);
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
|
},
|
|
});
|
|
};
|