feat: 监控面板 / 登录鉴权 / 多平台切换 / MySQL
在上游 MediaCrawler 之上新增一层: - 监控层 api/monitor/ —— 多博主/多笔记的定时采集、指标快照差分、报表、 企业微信通知。每轮采集写入独立目录,差分才成立。 - WebUI 登录鉴权 api/auth.py —— PBKDF2 口令 + 服务端会话,/api 全接口防护。 WebSocket 单独加依赖:BaseHTTPMiddleware 对 ws 作用域直接放行,覆盖不到。 - 全局平台切换 + 能力矩阵 —— 如实区分「爬虫模块支持」与「监控层已接线」, 未接通的平台直接拒绝建任务,而不是静默跑空。 - 监控库改用 MySQL 5.7(可回退 SQLite 供测试):逐表强制 utf8mb4 (服务端与库默认都是 latin1),启动校验所连 schema 以防写错库, 连接池 recycle + pre_ping 应对 MySQL 的 8 小时空闲断连。 修复上游缺陷: - xhs/core.py: 主页抓取失败会跳掉整个博主,导致一条作品都抓不到, 而那份资料只喂给一个空函数。改为尽力而为,失败不中断。 - xhs/login.py: cookie 登录只注入 web_session,冷启动签名会失败。 新增 INJECT_ALL_COOKIES 开关(默认关闭,原有行为不变)。 - requirements.txt: 补上 websockets。它在上游 pyproject.toml 里有声明、 这里漏了,导致 uvicorn 没有 WebSocket 能力,实时日志流从未工作。 改动过的上游文件清单及合并方式见 UPSTREAM.md。 测试:492 passed(另有 1 个既有的 Windows/gbk 上游测试失败,与本改动无关)
This commit is contained in:
+69
-31
@@ -1,62 +1,100 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Toaster } from 'sonner'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Sidebar } from '@/components/layout/Sidebar'
|
||||
import { MainContent } from '@/components/layout/MainContent'
|
||||
import { AuthorFooter } from '@/components/layout/AuthorFooter'
|
||||
import { CrawlerConfigPanel } from '@/components/config/CrawlerConfigPanel'
|
||||
import { MonitorDashboard } from '@/components/monitor/MonitorDashboard'
|
||||
import { ReportView } from '@/components/monitor/ReportView'
|
||||
import { SettingsView } from '@/components/settings/SettingsView'
|
||||
import { Login } from '@/components/auth/Login'
|
||||
import { EnvironmentCheck, isEnvChecked } from '@/components/env/EnvironmentCheck'
|
||||
import { LicenseDisclaimer, isLicenseAccepted } from '@/components/license/LicenseDisclaimer'
|
||||
import { authApi, setUnauthorizedHandler } from '@/lib/api'
|
||||
|
||||
export type AppView = 'crawler' | 'monitor' | 'report' | 'settings'
|
||||
|
||||
function App() {
|
||||
// Initialize by checking localStorage if license has been accepted
|
||||
const [licenseAccepted, setLicenseAccepted] = useState(() => isLicenseAccepted())
|
||||
// null = still probing. Rendering the app while unknown would briefly mount
|
||||
// the log WebSocket before we know whether the user is authenticated.
|
||||
const [authed, setAuthed] = useState<boolean | null>(null)
|
||||
// Initialize by checking localStorage if env check has passed
|
||||
const [envChecked, setEnvChecked] = useState(() => isEnvChecked())
|
||||
// State for showing disclaimer manually
|
||||
const [showDisclaimer, setShowDisclaimer] = useState(false)
|
||||
// Which top-level workspace is visible. Only one is mounted at a time: the
|
||||
// crawler view owns a live log WebSocket and a 2s status poll, which should
|
||||
// not keep running while the user is looking at the monitor dashboard.
|
||||
const [view, setView] = useState<AppView>('crawler')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
authApi
|
||||
.me()
|
||||
.then(() => !cancelled && setAuthed(true))
|
||||
.catch(() => !cancelled && setAuthed(false))
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
// A 401 from any request means the session expired or was revoked elsewhere
|
||||
// (e.g. the password was changed on another device), so drop back to login.
|
||||
useEffect(() => {
|
||||
setUnauthorizedHandler(() => setAuthed(false))
|
||||
return () => setUnauthorizedHandler(null)
|
||||
}, [])
|
||||
|
||||
const handleEnvCheckComplete = () => {
|
||||
setEnvChecked(true)
|
||||
}
|
||||
|
||||
const handleLicenseAccept = () => {
|
||||
setLicenseAccepted(true)
|
||||
setShowDisclaimer(false)
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authApi.logout()
|
||||
} finally {
|
||||
// Even if the call fails, the local session is over; unmounting the tree
|
||||
// closes the log WebSocket via the existing connection-count cleanup.
|
||||
setAuthed(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowDisclaimer = () => {
|
||||
setShowDisclaimer(true)
|
||||
if (authed === null) {
|
||||
return (
|
||||
<div className="flex h-screen cyber-grid items-center justify-center">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-cyber-neon-cyan" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!authed) {
|
||||
return <Login onSuccess={() => setAuthed(true)} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen cyber-grid overflow-hidden relative">
|
||||
{/* License Disclaimer Modal - Shows first or when triggered */}
|
||||
{(!licenseAccepted || showDisclaimer) && (
|
||||
<LicenseDisclaimer onAccept={handleLicenseAccept} />
|
||||
)}
|
||||
|
||||
{/* Environment Check Modal - Shows after license accepted */}
|
||||
{licenseAccepted && !showDisclaimer && !envChecked && (
|
||||
<EnvironmentCheck onCheckComplete={handleEnvCheckComplete} />
|
||||
)}
|
||||
{/* Environment Check Modal - shown until the check passes or is skipped.
|
||||
A configuration concern, so it lives inside the authenticated tree. */}
|
||||
{!envChecked && <EnvironmentCheck onCheckComplete={handleEnvCheckComplete} />}
|
||||
|
||||
{/* Header Bar */}
|
||||
<Sidebar onShowDisclaimer={handleShowDisclaimer} />
|
||||
<Sidebar currentView={view} onViewChange={setView} onLogout={handleLogout} />
|
||||
|
||||
{/* Main Area */}
|
||||
<div className="flex-1 flex flex-col gap-4 p-4 overflow-hidden min-h-0">
|
||||
{/* Config Panel - Primary Action Area (Always Expanded) */}
|
||||
<div className="flex-shrink-0">
|
||||
<CrawlerConfigPanel />
|
||||
</div>
|
||||
{view === 'crawler' && (
|
||||
<>
|
||||
{/* Config Panel - Primary Action Area (Always Expanded) */}
|
||||
<div className="flex-shrink-0">
|
||||
<CrawlerConfigPanel />
|
||||
</div>
|
||||
|
||||
{/* Console - Collapsible Terminal */}
|
||||
<MainContent />
|
||||
{/* Console - Collapsible Terminal */}
|
||||
<MainContent />
|
||||
</>
|
||||
)}
|
||||
{view === 'monitor' && <MonitorDashboard />}
|
||||
{view === 'report' && <ReportView />}
|
||||
{view === 'settings' && <SettingsView onNavigate={setView} />}
|
||||
</div>
|
||||
|
||||
{/* Author Footer */}
|
||||
<AuthorFooter />
|
||||
|
||||
{/* Toast notifications - Theme-aware style */}
|
||||
<Toaster
|
||||
position="top-right"
|
||||
|
||||
Reference in New Issue
Block a user