mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Add delete_session Tauri command dispatching to provider-specific deletion logic for all 5 providers (Claude, Codex, Gemini, OpenCode, OpenClaw). Includes path traversal protection via canonicalize + starts_with validation, session ID verification against file contents, frontend confirmation dialog with optimistic cache updates, i18n keys (zh/en/ja), and component tests.
327 lines
9.3 KiB
TypeScript
327 lines
9.3 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { providersApi, sessionsApi, settingsApi, type AppId } from "@/lib/api";
|
|
import type { DeleteSessionOptions } from "@/lib/api/sessions";
|
|
import type { SwitchResult } from "@/lib/api/providers";
|
|
import type { Provider, SessionMeta, 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" ||
|
|
providerInput.category === "omo-slim"
|
|
) {
|
|
const prefix = providerInput.category === "omo" ? "omo" : "omo-slim";
|
|
id = `${prefix}-${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"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo-slim", "current-provider-id"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo-slim", "provider-count"],
|
|
});
|
|
}
|
|
|
|
if (appId === "openclaw") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
}
|
|
|
|
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] });
|
|
if (appId === "openclaw") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
}
|
|
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"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo-slim", "current-provider-id"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo-slim", "provider-count"],
|
|
});
|
|
}
|
|
|
|
if (appId === "openclaw") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
}
|
|
|
|
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"],
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["omo-slim", "current-provider-id"],
|
|
});
|
|
}
|
|
if (appId === "openclaw") {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.liveProviderIds,
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.defaultModel,
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
}
|
|
|
|
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 useDeleteSessionMutation = () => {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation({
|
|
mutationFn: async (input: DeleteSessionOptions) => {
|
|
await sessionsApi.delete(input);
|
|
return input;
|
|
},
|
|
onSuccess: async (input) => {
|
|
queryClient.setQueryData<SessionMeta[]>(["sessions"], (current) =>
|
|
(current ?? []).filter(
|
|
(session) =>
|
|
!(
|
|
session.providerId === input.providerId &&
|
|
session.sessionId === input.sessionId &&
|
|
session.sourcePath === input.sourcePath
|
|
),
|
|
),
|
|
);
|
|
queryClient.removeQueries({
|
|
queryKey: ["sessionMessages", input.providerId, input.sourcePath],
|
|
});
|
|
|
|
await queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
|
|
|
toast.success(
|
|
t("sessionManager.sessionDeleted", {
|
|
defaultValue: "会话已删除",
|
|
}),
|
|
);
|
|
},
|
|
onError: (error: Error) => {
|
|
const detail = extractErrorMessage(error) || t("common.unknown");
|
|
toast.error(
|
|
t("sessionManager.deleteFailed", {
|
|
defaultValue: "删除会话失败: {{error}}",
|
|
error: detail,
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useSaveSettingsMutation = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (settings: Settings) => {
|
|
await settingsApi.save(settings);
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
|
},
|
|
});
|
|
};
|