Files
MediaCrawler/webui/src/components/monitor/QrLoginPanel.tsx
T
butubb 37ca1b1cd6
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat: CDP 接管开关 + 扫码登录面板 + Docker 部署
- runner: enable_cdp_mode 从硬编码 False 改为系统设置 cdp_enabled。服务器部署下爬虫接管已开启远程调试的 Chrome(默认 9222),复用其 profile 登录态;本机桌面默认仍为关,行为不变
- qrlogin: 新增 CDP 扫码登录。Chrome 在服务器上跑于 Xvfb,show_qrcode 依赖的 PIL 桌面看图程序不存在,二维码无处可显示;改为经 CDP 从页面取出二维码交给 WebUI 渲染。刻意复用 browser.contexts[0](新建 context 是无痕 profile,扫了也白扫),且绝不调用 browser.close()(会连带关掉操作者自己的 Chrome)
- webui: 设置页新增扫码面板,替换原本跳到采集页看终端二维码的入口
- Dockerfile / .dockerignore / docker-compose.yml: 服务器部署。host 网络是必需而非图省事——容器里 127.0.0.1:9222 必须落到宿主机回环
- UPSTREAM.md: 补充 gitcode 镜像,用于 GitHub 大包传输必断时补历史
2026-10-07 10:41:11 +08:00

155 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, CheckCircle2, 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 { useCurrentPlatform } from '@/hooks/usePlatform'
/**
* 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 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.
*/
export function QrLoginPanel() {
const { capability, platform } = useCurrentPlatform()
const queryClient = useQueryClient()
const [polling, setPolling] = useState(false)
const { data: state } = useQrLoginStatus(polling)
const start = useStartQrLogin()
const cancel = useCancelQrLogin()
const label = capability?.label ?? platform
const status = state?.status ?? 'idle'
// 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(() => {
if (status === 'waiting' || status === 'idle') return
setPolling(false)
if (status === 'success') {
queryClient.invalidateQueries({ queryKey: ['monitorCookie'] })
}
}, [status, queryClient])
const begin = () => {
setPolling(true)
start.mutate()
}
const busy = start.isPending || cancel.isPending
return (
<div className="rounded-lg glass-panel float-panel p-4 space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<QrCode className="w-4 h-4 text-cyber-neon-cyan" />
<span className="font-mono text-xs tracking-wider text-cyber-text-primary">
{label}扫码登录
</span>
{status === 'waiting' && (
<Badge variant="warning" className="text-[10px]">
等待扫码
</Badge>
)}
{status === 'success' && (
<Badge variant="success" className="text-[10px]">
已登录
</Badge>
)}
</div>
{status === 'waiting' && (
<Button
variant="ghost"
size="sm"
disabled={busy}
onClick={() => {
setPolling(false)
cancel.mutate()
}}
>
<X className="w-3 h-3" />
</Button>
)}
</div>
{status === 'waiting' && state?.image && (
<div className="flex items-start gap-4">
{/* The code is a data: URL straight from the page, so nothing is
fetched from a third party and no file is written server-side. */}
<img
src={state.image}
alt="登录二维码"
className="w-44 h-44 rounded-md border border-cyber-border-DEFAULT bg-white p-1"
/>
<div className="space-y-2 pt-1">
<p className="text-[11px] font-mono text-cyber-text-secondary leading-relaxed">
用<span className="text-cyber-neon-cyan">{label} App</span>扫码。
扫码成功后登录态会写入服务器上那台 Chrome 的 profile,
之后定时监控无需再登录。
</p>
<p className="text-[11px] font-mono text-cyber-text-muted">
剩余 <span className="text-cyber-neon-cyan">{state.expires_in}</span> 秒
</p>
</div>
</div>
)}
{status === 'waiting' && !state?.image && (
<p className="flex items-center gap-2 text-[11px] font-mono text-cyber-text-muted">
<Loader2 className="w-3 h-3 animate-spin" />
正在从浏览器取二维码…
</p>
)}
{status === 'success' && (
<p className="flex items-start gap-2 text-[11px] font-mono text-cyber-neon-green leading-relaxed">
<CheckCircle2 className="w-3 h-3 mt-0.5 shrink-0" />
{state?.message || '登录成功'}
</p>
)}
{(status === 'error' || status === 'expired') && (
<div className="space-y-2">
<p className="flex items-start gap-2 text-[11px] font-mono text-cyber-neon-orange leading-relaxed">
<AlertTriangle className="w-3 h-3 mt-0.5 shrink-0" />
{state?.message || '获取二维码失败'}
</p>
<Button variant="outline" size="sm" disabled={busy} onClick={begin}>
<RefreshCw className="w-3 h-3 mr-1" />
重新获取
</Button>
</div>
)}
{status === 'idle' && (
<div className="space-y-2">
<p className="text-[11px] font-mono text-cyber-text-muted leading-relaxed">
走 CDP 接管服务器上已开启远程调试的 Chrome,把二维码取回来显示在这里。
需要先在「系统设置」里打开
<span className="text-cyber-neon-cyan">接管已有 Chrome(CDP)</span>,
并确保那台 Chrome 正以 <span className="text-cyber-neon-cyan">9222</span> 端口运行。
</p>
<Button size="sm" disabled={busy} onClick={begin}>
{start.isPending ? (
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
) : (
<QrCode className="w-3 h-3 mr-1" />
)}
获取二维码
</Button>
</div>
)}
</div>
)
}