)
diff --git a/webui/src/components/monitor/QrLoginPanel.tsx b/webui/src/components/monitor/QrLoginPanel.tsx
index 9bc7df6..92f465a 100644
--- a/webui/src/components/monitor/QrLoginPanel.tsx
+++ b/webui/src/components/monitor/QrLoginPanel.tsx
@@ -1,22 +1,38 @@
import { useEffect, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
-import { AlertTriangle, CheckCircle2, Loader2, QrCode, RefreshCw, X } from 'lucide-react'
+import {
+ AlertTriangle,
+ CheckCircle2,
+ KeyRound,
+ Loader2,
+ QrCode,
+ RefreshCw,
+ X,
+} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
-import { useCancelQrLogin, useQrLoginStatus, useStartQrLogin } from '@/hooks/useMonitor'
+import {
+ useCancelQrLogin,
+ useLoginState,
+ useQrLoginStatus,
+ useRecheckLogin,
+ useStartQrLogin,
+} from '@/hooks/useMonitor'
import { useCurrentPlatform } from '@/hooks/usePlatform'
/**
- * Scan-to-login for a host with no display.
+ * Scan-to-login, for a host with no display.
*
* The crawler's own QR flow prints the code into the terminal via PIL's
* `Image.show()`, which needs a desktop image viewer. On a server Chrome runs
* under Xvfb and there is no such viewer, so the backend reads the code out of
- * that same browser over CDP and hands it here instead.
+ * that same browser over CDP and hands it here.
*
- * That browser is the one monitor runs attach to, which is the point: scanning
- * once leaves the login in the profile that every later unattended run reuses.
+ * **The login state is shown first and independently of the scan.** It comes from
+ * the browser's own profile, so it stays true across a server restart — the QR
+ * session does not, and reporting "did it work?" from a value that a redeploy
+ * silently erases is how a successful scan ends up looking like nothing happened.
*/
export function QrLoginPanel() {
const { capability, platform } = useCurrentPlatform()
@@ -24,12 +40,19 @@ export function QrLoginPanel() {
const [polling, setPolling] = useState(false)
const { data: state } = useQrLoginStatus(polling)
+ const { data: login } = useLoginState(polling)
const start = useStartQrLogin()
const cancel = useCancelQrLogin()
+ const recheck = useRecheckLogin()
const label = capability?.label ?? platform
const status = state?.status ?? 'idle'
+ // The browser's own answer wins over the session's: the session is memory, the
+ // profile is not.
+ const loggedIn = login?.logged_in ?? state?.logged_in ?? false
+ const nickname = login?.nickname ?? state?.nickname ?? null
+
// Stop polling the moment the outcome is known, and refresh the cookie panel:
// a completed scan is what makes it start reporting a healthy login.
useEffect(() => {
@@ -37,6 +60,7 @@ export function QrLoginPanel() {
setPolling(false)
if (status === 'success') {
queryClient.invalidateQueries({ queryKey: ['monitorCookie'] })
+ queryClient.invalidateQueries({ queryKey: ['monitorLoginState'] })
}
}, [status, queryClient])
@@ -45,43 +69,53 @@ export function QrLoginPanel() {
start.mutate()
}
- const busy = start.isPending || cancel.isPending
+ const busy = start.isPending || cancel.isPending || recheck.isPending
return (
diff --git a/webui/src/hooks/useMonitor.ts b/webui/src/hooks/useMonitor.ts
index 73e6b25..abbd54d 100644
--- a/webui/src/hooks/useMonitor.ts
+++ b/webui/src/hooks/useMonitor.ts
@@ -244,6 +244,34 @@ export function useCancelQrLogin() {
})
}
+/**
+ * Whether the browser is actually signed in.
+ *
+ * Tracked separately from the QR session above: that session lives in the
+ * server's memory and a restart erases it, while the profile it wrote to does
+ * not. Polled slowly when idle and briskly while a code is on screen.
+ */
+export function useLoginState(polling: boolean) {
+ return useQuery({
+ queryKey: ['monitorLoginState'],
+ queryFn: async () => (await monitorApi.getLoginState()).data,
+ refetchInterval: polling ? 4000 : 30000,
+ refetchOnWindowFocus: false,
+ })
+}
+
+/** Re-ask after reloading the page, for when the state looks stale. */
+export function useRecheckLogin() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: () => monitorApi.getLoginState(true),
+ onSuccess: (response) => {
+ queryClient.setQueryData(['monitorLoginState'], response.data)
+ },
+ onError: (error: Error) => toast.error(`检测失败:${error.message}`),
+ })
+}
+
// --- Settings -------------------------------------------------------------
export function useSettings() {
diff --git a/webui/src/lib/api.ts b/webui/src/lib/api.ts
index 4bc2283..f74f8e7 100644
--- a/webui/src/lib/api.ts
+++ b/webui/src/lib/api.ts
@@ -10,6 +10,7 @@ import type {
MonitorOverview,
MonitorRun,
MonitorTask,
+ LoginState,
PlatformCapability,
QrLoginState,
ReportResult,
@@ -263,6 +264,9 @@ export const monitorApi = {
api.post('/monitor/login/qr', null, { params: { platform } }),
getQrLogin: () => api.get('/monitor/login/qr'),
cancelQrLogin: () => api.delete('/monitor/login/qr'),
+ /** `force` reloads the page first, for a stale-looking state. */
+ getLoginState: (force = false) =>
+ api.get('/monitor/login/state', { params: { force } }),
getWebhook: () => api.get('/monitor/webhook'),
setWebhook: (url: string) => api.post('/monitor/webhook', { url }),
diff --git a/webui/src/types/monitor.ts b/webui/src/types/monitor.ts
index 78e0074..d5647d9 100644
--- a/webui/src/types/monitor.ts
+++ b/webui/src/types/monitor.ts
@@ -196,6 +196,26 @@ export interface QrLoginState {
elapsed: number
/** Seconds left before the code is written off. */
expires_in: number
+ /** Whether the browser reports being signed in right now. */
+ logged_in: boolean
+ nickname: string | null
+}
+
+/**
+ * The browser's own answer to "am I signed in?".
+ *
+ * Asked of the page, not derived from the QR session -- that session lives in the
+ * server's memory and dies on a restart, so tying the answer to it makes a
+ * successful scan look like nothing happened.
+ *
+ * `known` is false when the page could not report (not loaded, browser
+ * unreachable); `logged_in` is then meaningless rather than false.
+ */
+export interface LoginState {
+ known: boolean
+ logged_in: boolean
+ nickname: string | null
+ error?: string
}
export interface MonitorOverview {