From 6155a2ec2b0c2bfad0d9e7b6124bdeab8986c7e0 Mon Sep 17 00:00:00 2001 From: xuhd Date: Thu, 14 May 2026 18:20:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20curator=20+=20notifications=20react-quer?= =?UTF-8?q?y=20cache=20=E6=8C=89=20user=5Fid=20=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 切换账号在同一 tab 里时,旧 cache key (["curator","config"] 等) 会 让新账号"看到"上一个账号的值。如果用户没意识到、直接点保存,就会 把上一个账号的配置原样写到新账号的 per-user JSON 里去。 修复:在 useCuratorKeys / useNotificationKeys hook 里读 getStoredAuth().user_id 织进 queryKey。每个用户拥有独立 cache 桶,切换账号绝不命中对方的数据。 Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend-web/src/core/curator/hooks.ts | 41 +++++++++++++++----- frontend-web/src/core/notifications/hooks.ts | 29 +++++++++++--- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/frontend-web/src/core/curator/hooks.ts b/frontend-web/src/core/curator/hooks.ts index 191641c3..35df0d9f 100644 --- a/frontend-web/src/core/curator/hooks.ts +++ b/frontend-web/src/core/curator/hooks.ts @@ -1,4 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMemo } from "react"; + +import { getStoredAuth } from "@/core/auth"; import { getCuratorConfig, @@ -9,21 +12,36 @@ import { } from "./api"; import type { CuratorConfig } from "./types"; -const STATUS_KEY = ["curator", "status"] as const; -const CONFIG_KEY = ["curator", "config"] as const; -const LOGS_KEY = ["curator", "logs"] as const; +// All curator-related queries are per-user data (status / config / logs all +// live under .deer-flow/users/{user_id}/), so the cache key MUST be scoped by +// user_id. Otherwise switching accounts in the same tab would let user B +// briefly read user A's cached values — and clicking "save" on that stale +// frame would silently overwrite user B's real config with user A's values. +function useCuratorKeys() { + const userId = useMemo(() => getStoredAuth()?.user_id ?? "anonymous", []); + return useMemo( + () => ({ + status: ["curator", userId, "status"] as const, + config: ["curator", userId, "config"] as const, + logs: ["curator", userId, "logs"] as const, + }), + [userId], + ); +} export function useCuratorStatus() { + const keys = useCuratorKeys(); const query = useQuery({ - queryKey: STATUS_KEY, + queryKey: keys.status, queryFn: getCuratorStatus, }); return { status: query.data, isLoading: query.isLoading, error: query.error }; } export function useCuratorConfig() { + const keys = useCuratorKeys(); const query = useQuery({ - queryKey: CONFIG_KEY, + queryKey: keys.config, queryFn: getCuratorConfig, }); return { config: query.data, isLoading: query.isLoading, error: query.error }; @@ -31,32 +49,35 @@ export function useCuratorConfig() { export function useUpdateCuratorConfig() { const queryClient = useQueryClient(); + const keys = useCuratorKeys(); return useMutation({ mutationFn: (payload: CuratorConfig) => updateCuratorConfig(payload), onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: CONFIG_KEY }); - void queryClient.invalidateQueries({ queryKey: STATUS_KEY }); + void queryClient.invalidateQueries({ queryKey: keys.config }); + void queryClient.invalidateQueries({ queryKey: keys.status }); }, }); } export function useRunCurator() { const queryClient = useQueryClient(); + const keys = useCuratorKeys(); return useMutation({ mutationFn: () => runCurator(), onSuccess: () => { // Logs start from scratch on a new run — refresh so the UI clears // immediately and then picks up the new run_started record. - void queryClient.invalidateQueries({ queryKey: LOGS_KEY }); - void queryClient.invalidateQueries({ queryKey: STATUS_KEY }); + void queryClient.invalidateQueries({ queryKey: keys.logs }); + void queryClient.invalidateQueries({ queryKey: keys.status }); }, }); } export function useCuratorLogs(options?: { autoRefresh?: boolean }) { const autoRefresh = options?.autoRefresh ?? true; + const keys = useCuratorKeys(); const query = useQuery({ - queryKey: LOGS_KEY, + queryKey: keys.logs, queryFn: getCuratorLogs, refetchInterval: autoRefresh ? 2000 : false, }); diff --git a/frontend-web/src/core/notifications/hooks.ts b/frontend-web/src/core/notifications/hooks.ts index ab3fbb16..4286350b 100644 --- a/frontend-web/src/core/notifications/hooks.ts +++ b/frontend-web/src/core/notifications/hooks.ts @@ -1,4 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMemo } from "react"; + +import { getStoredAuth } from "@/core/auth"; import { deleteNotification, @@ -8,20 +11,35 @@ import { markNotificationRead, } from "./api"; -const ROOT_KEY = ["notifications"] as const; -const COUNT_KEY = [...ROOT_KEY, "unread-count"] as const; +// Notifications are per-user data, so the cache key MUST be scoped by user_id. +// Otherwise switching accounts in the same tab would show the previous user's +// inbox until the next fetch lands. +function useNotificationKeys() { + const userId = useMemo(() => getStoredAuth()?.user_id ?? "anonymous", []); + return useMemo(() => { + const root = ["notifications", userId] as const; + return { + root, + list: (limit: number, unreadOnly: boolean) => + [...root, "list", { limit, unreadOnly }] as const, + count: [...root, "unread-count"] as const, + }; + }, [userId]); +} export function useNotifications(options: { limit?: number; unreadOnly?: boolean } = {}) { const { limit = 20, unreadOnly = false } = options; + const keys = useNotificationKeys(); return useQuery({ - queryKey: [...ROOT_KEY, "list", { limit, unreadOnly }], + queryKey: keys.list(limit, unreadOnly), queryFn: () => listNotifications({ limit, unreadOnly }), }); } export function useUnreadCount(refetchInterval = 30000) { + const keys = useNotificationKeys(); return useQuery({ - queryKey: COUNT_KEY, + queryKey: keys.count, queryFn: () => getUnreadCount(), refetchInterval, // Keep the badge fresh when the user comes back to the tab without @@ -32,8 +50,9 @@ export function useUnreadCount(refetchInterval = 30000) { function useInvalidateNotifications() { const queryClient = useQueryClient(); + const keys = useNotificationKeys(); return () => { - void queryClient.invalidateQueries({ queryKey: ROOT_KEY }); + void queryClient.invalidateQueries({ queryKey: keys.root }); }; }