diff --git a/.github/workflows/beta.yaml b/.github/workflows/beta.yaml index b78aed1b..5ed54ad2 100644 --- a/.github/workflows/beta.yaml +++ b/.github/workflows/beta.yaml @@ -27,7 +27,7 @@ jobs: id: setup run: | echo "build_start=$(date '+%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT - echo "beta_version=beta-$(date -u '+%Y%m%d%H%M%S')-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "beta_version=$(grep -m1 '^version' service/pyproject.toml | sed 's/version = "\(.*\)"/\1/')" >> $GITHUB_OUTPUT echo "commit_author=$(git log -1 --pretty=format:'%an')" >> $GITHUB_OUTPUT echo "commit_email=$(git log -1 --pretty=format:'%ae')" >> $GITHUB_OUTPUT echo "commit_message=$(git log -1 --pretty=format:'%s')" >> $GITHUB_OUTPUT diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx index 940f4919..3fc94db4 100644 --- a/web/src/app/App.tsx +++ b/web/src/app/App.tsx @@ -8,10 +8,9 @@ import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect, useState } from "react"; import { SecretCodePage } from "@/components/admin/SecretCodePage"; -import { CenteredInput, UpdateOverlay } from "@/components/features"; +import { CenteredInput } from "@/components/features"; import { DEFAULT_BACKEND_URL } from "@/configs"; import { MOBILE_BREAKPOINT } from "@/configs/common"; -import { useAutoUpdate } from "@/hooks/useAutoUpdate"; import useTheme from "@/hooks/useTheme"; import { LAYOUT_STYLE, type InputPosition } from "@/store/slices/uiSlice/types"; import { AppFullscreen } from "./AppFullscreen"; @@ -260,7 +259,7 @@ export function Xyzen({ ) ) : ( - {mainLayout} + <>{mainLayout} ); // Check if we're on the secret code page @@ -279,20 +278,6 @@ export function Xyzen({ ); } -/** - * Wrapper component that handles auto-update logic. - * Must be rendered inside QueryClientProvider since it uses useBackendVersion. - */ -function AutoUpdateWrapper({ children }: { children: React.ReactNode }) { - const { isUpdating, targetVersion } = useAutoUpdate(); - - if (isUpdating && targetVersion) { - return ; - } - - return <>{children}; -} - const LOADING_MESSAGES = [ "我要加广告,老板说不行", "「懒」是第一生产力", diff --git a/web/src/components/agents/AgentList.tsx b/web/src/components/agents/AgentList.tsx index b33962a3..4d088987 100644 --- a/web/src/components/agents/AgentList.tsx +++ b/web/src/components/agents/AgentList.tsx @@ -328,12 +328,11 @@ export const AgentList: React.FC = (props) => { > {content} - // NOTE: Render DragOverlay into document.body to avoid positioning - bugs when // this list is inside a transformed/animated container - (e.g. framer-motion). // CSS transforms create a new containing block, - which can cause @dnd-kit’s // fixed-position overlay to calculate - coordinates relative to that container // (often showing the dragged - item jumping to the bottom). + {/* NOTE: Render DragOverlay into document.body to avoid positioning bugs when + this list is inside a transformed/animated container (e.g. framer-motion). + CSS transforms create a new containing block, which can cause @dnd-kit's + fixed-position overlay to calculate coordinates relative to that container + (often showing the dragged item jumping to the bottom). */} {createPortal( {renderOverlayItem()}, document.body, @@ -404,12 +403,11 @@ export const AgentList: React.FC = (props) => { > {content} - // NOTE: Render DragOverlay into document.body to avoid positioning bugs - when // this list is inside a transformed/animated container (e.g. - framer-motion). // CSS transforms create a new containing block, which - can cause @dnd-kit’s // fixed-position overlay to calculate coordinates - relative to that container // (often showing the dragged item jumping to - the bottom). + {/* NOTE: Render DragOverlay into document.body to avoid positioning bugs when + this list is inside a transformed/animated container (e.g. framer-motion). + CSS transforms create a new containing block, which can cause @dnd-kit's + fixed-position overlay to calculate coordinates relative to that container + (often showing the dragged item jumping to the bottom). */} {createPortal( {renderOverlayItem()}, document.body, diff --git a/web/src/components/features/UpdateOverlay.tsx b/web/src/components/features/UpdateOverlay.tsx deleted file mode 100644 index 6bd544d4..00000000 --- a/web/src/components/features/UpdateOverlay.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { LoadingSpinner } from "@/components/base/LoadingSpinner"; -import { useTranslation } from "react-i18next"; - -interface UpdateOverlayProps { - /** The version being updated to */ - targetVersion: string; -} - -/** - * Fullscreen overlay shown during auto-update process. - * Displays a spinner and "Updating to vX.X.X..." message. - */ -export function UpdateOverlay({ targetVersion }: UpdateOverlayProps) { - const { t } = useTranslation(); - - return ( -
- -

- {t("app.update.updating", { version: targetVersion })} -

-
- ); -} diff --git a/web/src/components/features/index.ts b/web/src/components/features/index.ts index 93665a67..109fae57 100644 --- a/web/src/components/features/index.ts +++ b/web/src/components/features/index.ts @@ -6,4 +6,3 @@ export { CenteredInput } from "./CenteredInput"; export { FileUploadButton } from "./FileUploadButton"; export { FileUploadThumbnail } from "./FileUploadThumbnail"; export { FileUploadPreview } from "./FileUploadPreview"; -export { UpdateOverlay } from "./UpdateOverlay"; diff --git a/web/src/hooks/useAutoUpdate.ts b/web/src/hooks/useAutoUpdate.ts deleted file mode 100644 index c34914a8..00000000 --- a/web/src/hooks/useAutoUpdate.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { useBackendVersion } from "@/hooks/queries"; -import { getFrontendVersion } from "@/types/version"; -import { useCallback, useEffect, useState } from "react"; - -const STORAGE_KEY = "xyzen-update-state"; -const MAX_RETRIES = 3; - -interface UpdateState { - targetVersion: string; - retryCount: number; -} - -interface UseAutoUpdateResult { - /** True during update process (cache clearing, reloading) */ - isUpdating: boolean; - /** The backend version we're updating to, if updating */ - targetVersion: string | null; -} - -/** - * Clears all caches and unregisters service workers - */ -async function clearCachesAndServiceWorkers(): Promise { - // Unregister all service workers - if ("serviceWorker" in navigator) { - const registrations = await navigator.serviceWorker.getRegistrations(); - await Promise.all(registrations.map((r) => r.unregister())); - } - - // Clear all caches - if ("caches" in window) { - const cacheNames = await caches.keys(); - await Promise.all(cacheNames.map((name) => caches.delete(name))); - } -} - -/** - * Gets the current update state from localStorage - */ -function getUpdateState(): UpdateState | null { - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (!stored) return null; - return JSON.parse(stored) as UpdateState; - } catch { - return null; - } -} - -/** - * Saves update state to localStorage - */ -function saveUpdateState(state: UpdateState): void { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); - } catch { - // Storage unavailable or quota exceeded - proceed without persistence - } -} - -/** - * Clears update state from localStorage - */ -function clearUpdateState(): void { - try { - localStorage.removeItem(STORAGE_KEY); - } catch { - // Storage unavailable - proceed without clearing - } -} - -/** - * Hook that auto-updates the frontend when version mismatches backend. - * - * Flow: - * 1. Fetches backend version after auth succeeds - * 2. Compares with frontend version - * 3. If mismatch: clears caches and reloads (up to MAX_RETRIES times) - * 4. Prevents infinite loops via localStorage retry tracking - * - * @param enabled - Whether to enable auto-update checking (default: true) - * @returns Object with isUpdating state and target version - */ -export function useAutoUpdate(enabled = true): UseAutoUpdateResult { - const [isUpdating, setIsUpdating] = useState(false); - const [targetVersion, setTargetVersion] = useState(null); - - const { - data: backendData, - isLoading, - isError, - } = useBackendVersion({ enabled }); - - const performUpdate = useCallback(async (version: string) => { - setIsUpdating(true); - setTargetVersion(version); - - try { - await clearCachesAndServiceWorkers(); - // Small delay to ensure UI shows the updating state - await new Promise((resolve) => setTimeout(resolve, 500)); - window.location.reload(); - } catch (error) { - console.error("[AutoUpdate] Failed to clear caches:", error); - // Still try to reload even if cache clearing fails - window.location.reload(); - } - }, []); - - useEffect(() => { - // Skip if disabled, still loading, or errored - if (!enabled || isLoading || isError || !backendData) { - return; - } - - const frontendVersion = getFrontendVersion().version; - const backendVersion = backendData.version; - - // Versions match - clear any update state and continue normally - if (frontendVersion === backendVersion) { - clearUpdateState(); - return; - } - - // Version mismatch - check retry count - const existingState = getUpdateState(); - - // If we're targeting the same version, increment retry count - // Otherwise, this is a new version - start fresh - const retryCount = - existingState?.targetVersion === backendVersion - ? existingState.retryCount + 1 - : 1; - - // Exceeded max retries - give up to prevent infinite loop - if (retryCount > MAX_RETRIES) { - console.warn( - `[AutoUpdate] Failed to update to ${backendVersion} after ${MAX_RETRIES} attempts. ` + - `Frontend: ${frontendVersion}, Backend: ${backendVersion}`, - ); - clearUpdateState(); - return; - } - - // Save state before reload - saveUpdateState({ - targetVersion: backendVersion, - retryCount, - }); - - console.info( - `[AutoUpdate] Version mismatch detected. ` + - `Frontend: ${frontendVersion}, Backend: ${backendVersion}. ` + - `Attempt ${retryCount}/${MAX_RETRIES}.`, - ); - - void performUpdate(backendVersion); - }, [enabled, backendData, isLoading, isError, performUpdate]); - - return { - isUpdating, - targetVersion, - }; -}