Files
MediaCrawler/webui/src/App.tsx
T
butubb 4e60524f37
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
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 上游测试失败,与本改动无关)
2026-10-07 09:58:40 +08:00

113 lines
3.9 KiB
TypeScript

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 { 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 { authApi, setUnauthorizedHandler } from '@/lib/api'
export type AppView = 'crawler' | 'monitor' | 'report' | 'settings'
function App() {
// 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())
// 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 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)
}
}
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">
{/* 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 currentView={view} onViewChange={setView} onLogout={handleLogout} />
{/* Main Area */}
<div className="flex-1 flex flex-col gap-4 p-4 overflow-hidden min-h-0">
{view === 'crawler' && (
<>
{/* Config Panel - Primary Action Area (Always Expanded) */}
<div className="flex-shrink-0">
<CrawlerConfigPanel />
</div>
{/* Console - Collapsible Terminal */}
<MainContent />
</>
)}
{view === 'monitor' && <MonitorDashboard />}
{view === 'report' && <ReportView />}
{view === 'settings' && <SettingsView onNavigate={setView} />}
</div>
{/* Toast notifications - Theme-aware style */}
<Toaster
position="top-right"
toastOptions={{
className: 'glass-panel font-mono text-cyber-text-primary',
style: {
fontFamily: 'JetBrains Mono, monospace',
},
}}
/>
</div>
)
}
export default App