import { Alert, Button, Card, Drawer, Empty, Input, Space, Table, Typography } from 'antd' import { useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import type { ApiError } from '../lib/api' import { isOrchestratorDbUnavailable, listCleanedNotes, type CleanedNoteRecord } from '../lib/content' export default function CleanedNotesPage() { const [searchParams, setSearchParams] = useSearchParams() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [notes, setNotes] = useState([]) const [total, setTotal] = useState(0) const [drawerOpen, setDrawerOpen] = useState(false) const [active, setActive] = useState(null) const page = useMemo(() => { const raw = Number(searchParams.get('page') || '1') return Number.isFinite(raw) && raw > 0 ? raw : 1 }, [searchParams]) const pageSize = useMemo(() => { const raw = Number(searchParams.get('page_size') || '20') return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 200) : 20 }, [searchParams]) const query = useMemo(() => String(searchParams.get('query') || '').trim(), [searchParams]) const [queryInput, setQueryInput] = useState(query) useEffect(() => { setQueryInput(query) }, [query]) const setParam = useCallback( (patch: Record) => { const next = new URLSearchParams(searchParams) Object.entries(patch).forEach(([k, v]) => { const val = String(v || '').trim() if (val) next.set(k, val) else next.delete(k) }) setSearchParams(next) }, [searchParams, setSearchParams], ) const loadList = useCallback(async () => { setLoading(true) setError(null) try { const res = await listCleanedNotes({ limit: pageSize, offset: (page - 1) * pageSize, query: query || undefined, }) setNotes(res.notes || []) setTotal(res.total || 0) } catch (e) { setError(e as ApiError) } finally { setLoading(false) } }, [page, pageSize, query]) useEffect(() => { void loadList() }, [loadList]) const columns = useMemo( () => [ { title: 'ID', dataIndex: 'id', width: 90 }, { title: '原始作者', dataIndex: 'raw_author', width: 160, render: (v: unknown) => (v ? String(v) : '—'), }, { title: '原始 URL', dataIndex: 'raw_url', width: 340, ellipsis: true, render: (v: unknown) => { const url = String(v || '').trim() if (!url) return '—' return ( {url} ) }, }, { title: '清洗内容', dataIndex: 'cleaned_content', ellipsis: true, render: (_: unknown, record: CleanedNoteRecord) => { const text = String(record.cleaned_content || '').trim() return ( {text || '—'} ) }, }, { title: '创建时间', dataIndex: 'created_at', width: 200, render: (v: unknown) => (v ? String(v) : '—'), }, ], [], ) const dbMissing = isOrchestratorDbUnavailable(error) const cleanedText = String(active?.cleaned_content || '').trim() return (
清洗笔记库 setQueryInput(e.target.value)} onSearch={(value) => setParam({ query: value.trim() || undefined, page: '1' })} style={{ width: 360 }} /> } loading={loading} > {dbMissing ? ( 内容库数据库不可用 服务端返回 503,通常表示 ORCHESTRATOR_DB_PATH 未配置、路径不存在或 DB 尚未初始化。
处理方式:
1)配置 ORCHESTRATOR_DB_PATH 指向可读的 sqlite 文件(默认 orchestrator/data/mvp.db)
2)初始化 DB:python Spider_XHS/orchestrator/db_init.py
} /> ) : error ? ( ) : null} setParam({ page: String(nextPage), page_size: String(nextSize) }), }} /> setDrawerOpen(false)} width={720} > {active ? (
raw_note_id: {active.raw_note_id ?? '—'}
raw_author: {active.raw_author || '—'}
raw_url: {active.raw_url ? ( {active.raw_url} ) : ( )}
created_at: {active.created_at || '—'}
cleaned_content:
                {cleanedText || '—'}
              
) : null}
) }