import { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { RefreshCw, Play, Info, AlertTriangle, CheckCircle, Loader, StopCircle, Download, FileText, SlidersHorizontal } from 'lucide-react'; import type { SavedSyncQuery } from '../../types'; interface ProcessStatus { active: boolean; total: number; downloaded: number; parsed: number; download_failed: number; parse_failed: number; current_bibcode: string; logs: string[]; action?: 'download' | 'parse' | 'translate'; } interface HarvestStatus { active: boolean; query: string; source: string; synced: number; total: number; } export function SyncPanel() { const [query, setQuery] = useState(''); const [source, setSource] = useState<'all' | 'ads' | 'arxiv'>('all'); const [limit, setLimit] = useState(200); const [estimating, setEstimating] = useState(false); const [estimatedCount, setEstimatedCount] = useState(null); const [status, setStatus] = useState({ active: false, query: '', source: '', synced: 0, total: 0, }); const [errorMsg, setErrorMsg] = useState(null); const [syncQueries, setSyncQueries] = useState([]); const pollIntervalRef = useRef(null); // 批量下载与解析相关状态 const [targetPhase, setTargetPhase] = useState<'download' | 'parse' | 'translate'>('download'); const [batchLimitCount, setBatchLimitCount] = useState(100); const [sortOrder, setSortOrder] = useState<'default' | 'pub_year_desc' | 'created_at_desc'>('default'); const [skipCompleted, setSkipCompleted] = useState(true); const [skipFailed, setSkipFailed] = useState(false); const [skipPrecedingFailed, setSkipPrecedingFailed] = useState(false); const [skipPrecedingUncompleted, setSkipPrecedingUncompleted] = useState(false); const [processStatus, setProcessStatus] = useState({ active: false, total: 0, downloaded: 0, parsed: 0, download_failed: 0, parse_failed: 0, current_bibcode: '', logs: [], }); const [processError, setProcessError] = useState(null); const processPollIntervalRef = useRef(null); const logsEndRef = useRef(null); const [showBuilder, setShowBuilder] = useState(false); const [rules, setRules] = useState>([ { field: 'all', op: 'AND', val: '' } ]); // 当高级表单规则变化时,自动更新同步输入框的检索式 const updateQueryFromRules = (currentRules: typeof rules) => { let qParts: string[] = []; currentRules.forEach((rule, idx) => { if (!rule.val.trim()) return; let valStr = rule.val.trim(); if (valStr.includes(' ') && !valStr.startsWith('"') && !valStr.startsWith('(')) { valStr = `"${valStr}"`; } let fieldPart = ''; if (rule.field !== 'all') { fieldPart = `${rule.field}:${valStr}`; } else { fieldPart = valStr; } if (idx === 0) { qParts.push(fieldPart); } else { qParts.push(`${rule.op} ${fieldPart}`); } }); setQuery(qParts.join(' ')); }; const handleAddRule = () => { setRules(prev => [...prev, { field: 'all', op: 'AND', val: '' }]); }; const handleRemoveRule = (idx: number) => { const next = rules.filter((_, i) => i !== idx); setRules(next); updateQueryFromRules(next); }; const handleRuleChange = (idx: number, key: 'field' | 'op' | 'val', value: string) => { const next = rules.map((r, i) => i === idx ? { ...r, [key]: value } : r); setRules(next); updateQueryFromRules(next); }; // 获取历史检索配置 const fetchSyncQueries = async () => { try { const res = await axios.get(`/api/sync/queries?t=${Date.now()}`); setSyncQueries(res.data); } catch (e) { console.error('获取检索配置列表失败', e); } }; const handleDeleteQuery = async (id: number) => { try { await axios.delete(`/api/sync/queries/${id}`); fetchSyncQueries(); } catch (e) { console.error('删除配置失败', e); } }; const handleReuseQuery = (sq: SavedSyncQuery) => { setQuery(sq.query); setSource(sq.source as any); setLimit(sq.limit_count); }; const handleQuickSync = async (sq: SavedSyncQuery) => { setErrorMsg(null); try { await axios.post('/api/sync/meta/run', { q: sq.query, source: sq.source, limit: sq.limit_count, }); fetchStatus(); startPolling(); setTimeout(fetchSyncQueries, 500); } catch (e: any) { console.error(e); setErrorMsg(e.response?.data || '启动快速同步失败。'); } }; // 获取当前的收割状态 const fetchStatus = async () => { try { const res = await axios.get(`/api/sync/meta/status?t=${Date.now()}`); setStatus(res.data); if (res.data.active) { startPolling(); } else { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; fetchSyncQueries(); } } } catch (e) { console.error('获取同步状态失败', e); } }; // 开始轮询 const startPolling = () => { if (pollIntervalRef.current) return; pollIntervalRef.current = setInterval(fetchStatus, 1000); }; useEffect(() => { fetchStatus(); fetchSyncQueries(); return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } }; }, []); // 批量下载与解析相关的网络操作 const fetchProcessStatus = async () => { try { const res = await axios.get(`/api/sync/asset/status?t=${Date.now()}`); setProcessStatus(res.data); if (res.data.active) { startProcessPolling(); } else if (processPollIntervalRef.current) { clearInterval(processPollIntervalRef.current); processPollIntervalRef.current = null; } } catch (e) { console.error('获取处理状态失败', e); } }; const startProcessPolling = () => { if (processPollIntervalRef.current) return; processPollIntervalRef.current = setInterval(fetchProcessStatus, 1000); }; const handleStartProcess = async () => { setProcessError(null); try { await axios.post('/api/sync/asset/run', { target_phase: targetPhase, limit_count: batchLimitCount, sort_order: sortOrder, skip_completed: skipCompleted, skip_failed: skipFailed, skip_preceding_failed: skipPrecedingFailed, skip_preceding_uncompleted: skipPrecedingUncompleted, }); fetchProcessStatus(); startProcessPolling(); } catch (e: any) { console.error(e); setProcessError(e.response?.data || '启动批量任务失败。'); } }; const handleStopProcess = async () => { try { await axios.post('/api/sync/asset/stop'); fetchProcessStatus(); } catch (e: any) { console.error(e); setProcessError(e.response?.data || '停止任务失败。'); } }; useEffect(() => { fetchProcessStatus(); return () => { if (processPollIntervalRef.current) { clearInterval(processPollIntervalRef.current); } }; }, []); // 日志终端自动滚动到底部 useEffect(() => { if (logsEndRef.current) { logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [processStatus.logs]); // 估算文献总量 const handleEstimate = async () => { if (!query.trim()) { setErrorMsg('请输入检索关键词!'); return; } setErrorMsg(null); setEstimating(true); setEstimatedCount(null); try { const res = await axios.get<{ total: number }>('/api/sync/meta/count', { params: { q: query.trim(), source } }); setEstimatedCount(res.data.total); } catch (e: any) { console.error(e); setErrorMsg(e.response?.data || '估算文献总量失败,请检查 API 密钥或网络。'); } finally { setEstimating(false); } }; // 启动收割任务 const handleStartHarvest = async () => { if (!query.trim()) { setErrorMsg('请输入检索关键词!'); return; } setErrorMsg(null); try { await axios.post('/api/sync/meta/run', { q: query.trim(), source, limit: limit, }); fetchStatus(); startPolling(); setTimeout(fetchSyncQueries, 500); } catch (e: any) { console.error(e); setErrorMsg(e.response?.data || '启动收割任务失败。'); } }; const percent = status.total > 0 ? Math.min(100, Math.round((status.synced / status.total) * 100)) : 0; return (
{/* 标题 */}

批量任务管理器

设定检索关键词,在 NASA ADS 和 arXiv 平台大批量同步学术文献索引,或针对本地文献馆藏批量执行下载、解析、翻译等学术流水线任务。

{errorMsg && (
{errorMsg}
)} {/* 控制面板卡片 */}
setQuery(e.target.value)} disabled={status.active} placeholder="例如: hot subdwarf, Gaia BH1..." className="w-full px-4 py-2 rounded-lg bg-slate-50 border border-slate-300 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-sky-500 focus:bg-white transition-all text-xs font-medium" />
高级格式: author:"Althaus" AND year:2020-2023
{[ { id: 'all', label: '全部' }, { id: 'ads', label: 'NASA ADS' }, { id: 'arxiv', label: 'arXiv 预印本' }, ].map(src => ( ))}
{/* 动态表单生成器 */} {showBuilder && (
高级条件生成器
{rules.map((rule, idx) => (
{idx > 0 ? ( ) : (
筛选条件
)} handleRuleChange(idx, 'val', e.target.value)} placeholder={ rule.field === 'year' ? '例如: 2020-2023 或 2022' : rule.field === 'author' ? '例如: Althaus' : '请输入检索词...' } className="flex-1 px-3 py-1.5 rounded-lg bg-white border border-slate-300 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-sky-500 text-xs font-medium" /> {rules.length > 1 && ( )}
))}
)}
setLimit(Math.max(1, parseInt(e.target.value) || 0))} className="w-full px-4 py-2 rounded-lg bg-slate-50 border border-slate-300 text-slate-900 focus:outline-none focus:border-sky-500 transition-all text-xs font-medium" />
{/* 预估结果 */} {estimatedCount !== null && !status.active && (
检索库中估计有约 {estimatedCount} 篇文献记录。 {estimatedCount > limit ? ` 限制最大同步前 ${limit} 篇元数据。` : ' 将全部进行同步。'}
)}
{/* 实时同步进度 */} {(status.active || status.synced > 0) && (

{status.active ? ( <> 正在同步学术元数据... ) : ( <> 元数据目录同步完成 )}

检索条件: {status.query} • 平台: {status.source === 'all' ? '全部' : status.source === 'ads' ? 'NASA ADS' : 'arXiv'}

{status.synced} / {status.total} 篇
{status.active && (status.source === 'all' || status.source === 'arxiv') ? (
💡 同步 arXiv 文献时系统会自动加设 3000 毫秒的安全限流延迟以规避服务器封锁。
) : null}
)} {/* 批量任务 */}

馆藏文献批量学术流水线任务

针对本地馆藏中的文献,批量发起正文 PDF/HTML 下载、Markdown 结构化排版解析以及中英双语对照翻译。

{processError && (
{processError}
)}
{[ { id: 'download', label: '下载' }, { id: 'parse', label: '解析' }, { id: 'translate', label: '翻译' }, ].map(phase => ( ))}
setBatchLimitCount(Math.max(1, parseInt(e.target.value) || 0))} className="w-full px-3 py-1.5 rounded-lg bg-slate-50 border border-slate-300 text-slate-900 focus:outline-none focus:border-sky-500 transition-all text-xs font-medium" />
{processStatus.active ? ( ) : ( )}
{/* 进度与终端日志展示 */} {(processStatus.active || processStatus.total > 0) && (
{processStatus.action === 'download' && } {processStatus.action === 'parse' && } {processStatus.action === 'translate' && } {processStatus.action === 'download' ? '正文离线下载进度' : processStatus.action === 'parse' ? '结构化排版解析进度' : '中英双语对照翻译进度'} {processStatus.action === 'download' ? processStatus.downloaded : processStatus.parsed} / {processStatus.total} 篇 {((processStatus.action === 'download' && processStatus.download_failed > 0) || (processStatus.action !== 'download' && processStatus.parse_failed > 0)) && ( (失败 {processStatus.action === 'download' ? processStatus.download_failed : processStatus.parse_failed} 篇) )}
0 ? Math.min( 100, Math.round( ((processStatus.action === 'download' ? processStatus.downloaded + processStatus.download_failed : processStatus.parsed + processStatus.parse_failed) / processStatus.total) * 100 ) ) : 0 }%`, }} />
{processStatus.active && processStatus.current_bibcode && (
当前正在处理: {processStatus.current_bibcode}
)} {/* 滚动日志终端 */}
{processStatus.logs.length === 0 ? (
等待数据流任务启动,暂无日志输出...
) : ( processStatus.logs.map((log, idx) => (
{log}
)) )}
)}
{/* 已存同步检索配置 */}

常用批量同步检索配置

保存的历史批量收割检索规则。可在此快速一键再次启动同步或加载检索参数。

{syncQueries.length === 0 ? (
暂无已存检索配置。执行一次批量元数据同步后将自动去重记录在此。
) : (
{syncQueries.map(sq => (
{sq.query}
数据源: {sq.source === 'all' ? '全部' : sq.source === 'ads' ? 'NASA ADS' : 'arXiv'} 数量限制: {sq.limit_count} 最近同步: {sq.last_run}
))}
)}
); }