feat: add session deletion with per-provider cleanup and path safety

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.
This commit is contained in:
Jason
2026-03-06 23:09:38 +08:00
parent e18db31752
commit 8c3f18a9bd
17 changed files with 1043 additions and 15 deletions
+15
View File
@@ -1,6 +1,12 @@
import { invoke } from "@tauri-apps/api/core";
import type { SessionMessage, SessionMeta } from "@/types";
export interface DeleteSessionOptions {
providerId: string;
sessionId: string;
sourcePath: string;
}
export const sessionsApi = {
async list(): Promise<SessionMeta[]> {
return await invoke("list_sessions");
@@ -13,6 +19,15 @@ export const sessionsApi = {
return await invoke("get_session_messages", { providerId, sourcePath });
},
async delete(options: DeleteSessionOptions): Promise<boolean> {
const { providerId, sessionId, sourcePath } = options;
return await invoke("delete_session", {
providerId,
sessionId,
sourcePath,
});
},
async launchTerminal(options: {
command: string;
cwd?: string | null;
+47 -2
View File
@@ -1,9 +1,10 @@
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 { 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, Settings } from "@/types";
import type { Provider, SessionMeta, Settings } from "@/types";
import { extractErrorMessage } from "@/utils/errorUtils";
import { generateUUID } from "@/utils/uuid";
import { openclawKeys } from "@/hooks/useOpenClaw";
@@ -267,6 +268,50 @@ export const useSwitchProviderMutation = (appId: AppId) => {
});
};
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();