侧边栏在「监控」右边加了「运营」:账号列表 → 点进二级详情看该账号的数据。 【为什么是独立模块而不是监控的子视图】两者形状不同:监控是公开数据(点赞/收藏/评论/分享)的每轮快照+差分;运营是创作者后台按日期给出的曝光/观看/完播率/涨粉。凭据不同、采集方式也不同 —— 那边要浏览器登录态,这边是纯请求。硬塞进同一个模型会同时污染两边。 【扫码登录的关键差异】监控的扫码把登录态写进浏览器默认 profile(爬虫要复用)。运营要的是 cookie 字符串(纯请求够用),所以每次登录开一个**临时上下文**,扫完取出 cookie 就丢弃 —— 登第二个账号不会把第一个顶掉,也不影响监控那个登录态,十个账号互不干扰。 【决策依据】tools/probe_creator_api.py 的 Phase 0 实测:签名可自造(XYW_:MD5 → base64 → AES-128-CBC,与 xhshow 内置实现常量逐字节一致);主站 cookie 即可认证创作者后台;接口与参数已与真实页面对齐。 后端: - api/creator/models.py: creator_account / creator_note_stat。**复用 MonitorBase**,这样 create_all 与上一轮改成元数据驱动的 _ensure_columns 会自动覆盖新表 - api/creator/signing.py: XYW_ 签名,带三条实测结论(url= 前缀、appId=ugc、401 与 406 的区别) - api/creator/client.py: 纯 httpx 客户端。字段名尚未亲眼验证过,所以写成多别名匹配;解析不出来存 None 而非 0 - api/creator/service.py: 账号 CRUD 与同步。cookie 绝不进入对外结构,只给 has_cookie - api/creator/login.py: 临时上下文的扫码登录 - api/routers/creator.py: 8 条路由,全部带鉴权 前端: - 侧边栏「运营」+ OperationView(账号列表 → 二级详情)+ AddAccountDialog - 权限状态显眼呈现:pending 时照抄后台原话「已为您申请数据权限,次日可查看」,并说明此时同步返回 0 条是正常的,不是采集失败 测试:tests/test_creator_client.py 新增 48 例,含「cookie 不得出现在对外结构里」这条不变量,以及权限未生效时空壳响应的处理。
115 lines
4.0 KiB
TypeScript
115 lines
4.0 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 { OperationView } from '@/components/creator/OperationView'
|
|
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' | 'operation' | '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 === 'operation' && <OperationView />}
|
|
{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
|