在上游 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 上游测试失败,与本改动无关)
113 lines
3.9 KiB
TypeScript
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
|