核心架构重构: - Config 按职责拆分为 LlmConfig/EmbeddingConfig/VisionConfig/CdsConfig/StorageConfig 五个子结构 - AppState 拆分为 LlmState/DataSourceState/SessionState 三个子结构,消除 50+ 平铺字段 - 新增 ServiceError 结构化错误类型替代 handler 中的 msg.contains() 字符串匹配 - 移除 api::handlers 兼容命名空间,路由直接引用 agent/auth/papers 等模块 认证性能优化: - login_rate_limiter/upload_rate_limiter 从 Mutex<HashMap> 迁移为 DashMap(无锁) - 新增 session_last_active: DashMap<String, AtomicU64>,auth 中间件快速路径免写锁 - 会话过期清理改为按间隔触发(300s),避免每次请求全表扫描 - MAX_SESSIONS 1000→10000,SSE 广播通道 256→1024 Agent 工具增强: - AgentTool trait 新增 is_internal()/display_name(),SSE 事件携带工具元数据 - 新增 GET /chat/tools 端点暴露注册工具列表 - pending_questions/pending_permissions 增加 created_at 时间戳,自动清理过期条目(10min TTL) - Agent 超时现在正确 abort 后台任务并设置取消令牌 观测数据源修复: - FITS 解析: APOGEE/DESI 改用 read_image+切片替代 read_rows(修复 fitsio panic) - ZTF: CIRCLE 参数分隔符 +→空格(修复 IRSA 400),半径自动裁剪至硬上限 - MAST TESS: parse_tic_json 兼容数组/对象两种 API 响应格式 - 统一检索: per_target_limit 默认 50→1,sources 支持 per-source release/version - Gaia 测光从 VizieR 镜像切换至官方 TAP 服务 RAG 并发优化: - 向量化降级从串行改为并发 5 条/批(buffer_unordered) - 混合检索 RRF 合并从借用改为 owned RetrievalResult 安全加固: - PDF 中间件: URL 解码 %2F/%2E 后判扩展名;文件名过滤非 ASCII + 禁 \ 防头注入 - chat_agent 日志截断问题内容至 50 字符;list_sessions 强制 limit clamp 前端双主题: - 设计令牌三层架构: primitive→semantic→component,浅色暖纸张学术/暗色 Night Indigo - useTheme hook + ThemeToggle 侧边栏组件 + main.tsx 防 FOUC 初始化 - 全组件从硬编码 slate 色迁移至语义令牌(bg-surface/text-content/border-subtle 等) - 新增 ToastContainer 非阻塞通知系统 部署优化: - deploy.sh 引入 SSH ControlMaster 单次密码复用
346 lines
13 KiB
TypeScript
346 lines
13 KiB
TypeScript
// dashboard/src/components/sync/BatchPipelineCard.tsx
|
||
import React from 'react';
|
||
import {
|
||
Download,
|
||
AlertTriangle,
|
||
Play,
|
||
StopCircle,
|
||
Loader,
|
||
FileText,
|
||
RefreshCw,
|
||
SlidersHorizontal,
|
||
} from 'lucide-react';
|
||
import { CustomSelect } from '../CustomSelect';
|
||
import type { BatchStatus } from '../../hooks/useSyncState';
|
||
|
||
interface BatchPipelineCardProps {
|
||
targetPhase: 'download' | 'parse' | 'translate' | 'embed' | 'target';
|
||
setTargetPhase: (
|
||
p: 'download' | 'parse' | 'translate' | 'embed' | 'target'
|
||
) => void;
|
||
batchLimitCount: number;
|
||
setBatchLimitCount: (c: number) => void;
|
||
sortOrder: 'default' | 'pub_year_desc' | 'created_at_desc';
|
||
setSortOrder: (o: 'default' | 'pub_year_desc' | 'created_at_desc') => void;
|
||
skipCompleted: boolean;
|
||
setSkipCompleted: (s: boolean) => void;
|
||
skipFailed: boolean;
|
||
setSkipFailed: (s: boolean) => void;
|
||
skipPrecedingFailed: boolean;
|
||
setSkipPrecedingFailed: (s: boolean) => void;
|
||
skipPrecedingUncompleted: boolean;
|
||
setSkipPrecedingUncompleted: (s: boolean) => void;
|
||
batchStatus: BatchStatus;
|
||
batchError: string | null;
|
||
logsContainerRef: React.RefObject<HTMLDivElement | null>;
|
||
handleStartBatch: () => Promise<void>;
|
||
handleStopBatch: () => Promise<void>;
|
||
}
|
||
|
||
export function BatchPipelineCard({
|
||
targetPhase,
|
||
setTargetPhase,
|
||
batchLimitCount,
|
||
setBatchLimitCount,
|
||
sortOrder,
|
||
setSortOrder,
|
||
skipCompleted,
|
||
setSkipCompleted,
|
||
skipFailed,
|
||
setSkipFailed,
|
||
skipPrecedingFailed,
|
||
setSkipPrecedingFailed,
|
||
skipPrecedingUncompleted,
|
||
setSkipPrecedingUncompleted,
|
||
batchStatus,
|
||
batchError,
|
||
logsContainerRef,
|
||
handleStartBatch,
|
||
handleStopBatch,
|
||
}: BatchPipelineCardProps) {
|
||
return (
|
||
<div className="console-panel p-6 rounded-lg bg-surface space-y-6 relative overflow-hidden shadow-sm">
|
||
<div className="flex flex-col gap-1 select-none">
|
||
<h3 className="text-xs font-bold text-content flex items-center gap-2">
|
||
<Download className="w-4 h-4 text-secondary" />
|
||
<span>馆藏文献批量学术流水线任务</span>
|
||
</h3>
|
||
<p className="text-secondary text-xs mt-0.5">
|
||
针对本地馆藏中的文献,批量发起正文 PDF/HTML 下载、Markdown
|
||
结构化排版解析以及中英双语对照翻译。
|
||
</p>
|
||
</div>
|
||
|
||
{batchError && (
|
||
<div className="p-4 rounded-md bg-rose-50 border border-rose-200/60 dark:bg-rose-500/10 dark:border-rose-500/30 flex gap-3 text-xs text-rose-800 dark:text-rose-300 items-start">
|
||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
|
||
<div>{batchError}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 条件设置 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-bold text-secondary block">
|
||
目标阶段
|
||
</label>
|
||
<CustomSelect
|
||
value={targetPhase}
|
||
disabled={batchStatus.active}
|
||
onChange={(val) =>
|
||
setTargetPhase(
|
||
val as 'download' | 'parse' | 'translate' | 'embed' | 'target'
|
||
)
|
||
}
|
||
className="w-full"
|
||
options={[
|
||
{ value: 'download', label: '下载' },
|
||
{ value: 'parse', label: '解析' },
|
||
{ value: 'translate', label: '翻译' },
|
||
{ value: 'embed', label: '向量化' },
|
||
{ value: 'target', label: '天体识别' },
|
||
]}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-bold text-secondary block">
|
||
批量处理上限
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={batchLimitCount}
|
||
disabled={batchStatus.active}
|
||
onChange={(e) =>
|
||
setBatchLimitCount(Math.max(1, parseInt(e.target.value) || 0))
|
||
}
|
||
className="w-full px-3 py-2 rounded-md bg-sunken text-content focus:outline-none focus:border-blueprint transition-all text-xs font-medium"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-bold text-secondary block">
|
||
处理顺序
|
||
</label>
|
||
<CustomSelect
|
||
value={sortOrder}
|
||
disabled={batchStatus.active}
|
||
onChange={(val) =>
|
||
setSortOrder(
|
||
val as 'default' | 'pub_year_desc' | 'created_at_desc'
|
||
)
|
||
}
|
||
className="w-full"
|
||
options={[
|
||
{ value: 'default', label: '默认(不指定)' },
|
||
{ value: 'pub_year_desc', label: '按出版年份降序' },
|
||
{ value: 'created_at_desc', label: '按入库时间降序' },
|
||
]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 执行策略 */}
|
||
<div className="space-y-2 border-t border-subtle pt-4">
|
||
<label className="text-xs font-bold text-secondary block">
|
||
执行策略
|
||
</label>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 select-none">
|
||
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={skipCompleted}
|
||
disabled={batchStatus.active}
|
||
onChange={(e) => setSkipCompleted(e.target.checked)}
|
||
className="rounded text-blueprint focus:ring-blueprint/25"
|
||
/>
|
||
<span>跳过已完成</span>
|
||
</label>
|
||
|
||
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={skipFailed}
|
||
disabled={batchStatus.active}
|
||
onChange={(e) => setSkipFailed(e.target.checked)}
|
||
className="rounded text-blueprint focus:ring-blueprint/25"
|
||
/>
|
||
<span>跳过当前失败</span>
|
||
</label>
|
||
|
||
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={skipPrecedingFailed}
|
||
disabled={batchStatus.active}
|
||
onChange={(e) => setSkipPrecedingFailed(e.target.checked)}
|
||
className="rounded text-blueprint focus:ring-blueprint/25"
|
||
/>
|
||
<span>跳过前置失败</span>
|
||
</label>
|
||
|
||
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={skipPrecedingUncompleted}
|
||
disabled={batchStatus.active}
|
||
onChange={(e) => setSkipPrecedingUncompleted(e.target.checked)}
|
||
className="rounded text-blueprint focus:ring-blueprint/25"
|
||
/>
|
||
<span>跳过前置未完成</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 控制按钮 */}
|
||
<div className="flex justify-end pt-2 border-t border-subtle">
|
||
<div className="w-full md:w-1/3 flex">
|
||
{batchStatus.active ? (
|
||
<button
|
||
type="button"
|
||
onClick={handleStopBatch}
|
||
className="w-full py-2 rounded-md bg-danger hover:opacity-90 text-white text-xs font-bold flex items-center justify-center gap-2 transition-all shadow-sm cursor-pointer"
|
||
>
|
||
<StopCircle className="w-3.5 h-3.5" />
|
||
停止批量任务
|
||
</button>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={handleStartBatch}
|
||
className="btn-console btn-console-primary w-full py-2 rounded-md text-xs font-bold flex items-center justify-center gap-2 cursor-pointer"
|
||
>
|
||
<Play className="w-3.5 h-3.5" />
|
||
启动批量任务
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 进度与终端日志展示 */}
|
||
{(batchStatus.active || batchStatus.total > 0) && (
|
||
<div className="space-y-4 pt-4 border-t border-subtle">
|
||
<div className="space-y-1.5">
|
||
<div className="flex justify-between text-xs font-bold text-secondary select-none">
|
||
<span className="flex items-center gap-1.5">
|
||
{batchStatus.action === 'download' && (
|
||
<Download className="w-3.5 h-3.5 text-blueprint" />
|
||
)}
|
||
{batchStatus.action === 'parse' && (
|
||
<FileText className="w-3.5 h-3.5 text-emerald-700" />
|
||
)}
|
||
{batchStatus.action === 'translate' && (
|
||
<RefreshCw className="w-3.5 h-3.5 text-slate-700 animate-spin" />
|
||
)}
|
||
{batchStatus.action === 'embed' && (
|
||
<SlidersHorizontal className="w-3.5 h-3.5 text-amber-600" />
|
||
)}
|
||
{batchStatus.action === 'target' && (
|
||
<RefreshCw className="w-3.5 h-3.5 text-blueprint" />
|
||
)}
|
||
{batchStatus.action === 'download'
|
||
? '正文离线下载进度'
|
||
: batchStatus.action === 'parse'
|
||
? '结构化排版解析进度'
|
||
: batchStatus.action === 'translate'
|
||
? '中英双语对照翻译进度'
|
||
: batchStatus.action === 'embed'
|
||
? '段落向量分块入库进度'
|
||
: '天体识别与缓存进度'}
|
||
</span>
|
||
<span>
|
||
{batchStatus.action === 'download'
|
||
? batchStatus.downloaded
|
||
: batchStatus.parsed}{' '}
|
||
/ {batchStatus.total} 篇
|
||
{((batchStatus.action === 'download' &&
|
||
batchStatus.download_failed > 0) ||
|
||
(batchStatus.action !== 'download' &&
|
||
batchStatus.parse_failed > 0)) && (
|
||
<span className="text-danger ml-2 font-bold">
|
||
(失败{' '}
|
||
{batchStatus.action === 'download'
|
||
? batchStatus.download_failed
|
||
: batchStatus.parse_failed}{' '}
|
||
篇)
|
||
</span>
|
||
)}
|
||
</span>
|
||
</div>
|
||
<div className="w-full h-2 rounded-md bg-sunken overflow-hidden select-none">
|
||
<div
|
||
className={`h-full transition-all duration-300 ${
|
||
batchStatus.action === 'download'
|
||
? 'bg-blueprint'
|
||
: batchStatus.action === 'parse'
|
||
? 'bg-emerald-600'
|
||
: batchStatus.action === 'translate'
|
||
? 'bg-slate-700'
|
||
: batchStatus.action === 'embed'
|
||
? 'bg-amber-600'
|
||
: 'bg-blueprint'
|
||
}`}
|
||
style={{
|
||
width: `${
|
||
batchStatus.total > 0
|
||
? Math.min(
|
||
100,
|
||
Math.round(
|
||
((batchStatus.action === 'download'
|
||
? batchStatus.downloaded +
|
||
batchStatus.download_failed
|
||
: batchStatus.parsed + batchStatus.parse_failed) /
|
||
batchStatus.total) *
|
||
100
|
||
)
|
||
)
|
||
: 0
|
||
}%`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{batchStatus.active && batchStatus.current_bibcode && (
|
||
<div className="text-xs font-bold text-secondary flex items-center gap-2">
|
||
<Loader className="w-3.5 h-3.5 text-blueprint animate-spin" />
|
||
<span>
|
||
当前正在处理:{' '}
|
||
<code className="bg-sunken px-2 py-0.5 rounded-md font-mono font-bold text-content select-all">
|
||
{batchStatus.current_bibcode}
|
||
</code>
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 滚动日志终端 */}
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-bold text-secondary block select-none">
|
||
实时处理日志流终端
|
||
</label>
|
||
<div
|
||
ref={logsContainerRef}
|
||
className="bg-sunken text-content font-mono text-[11px] p-4 rounded-md h-48 overflow-y-auto space-y-1 scrollbar-thin relative select-all"
|
||
>
|
||
{batchStatus.logs.length === 0 ? (
|
||
<div className="text-tertiary italic select-none">
|
||
等待数据流任务启动,暂无日志输出...
|
||
</div>
|
||
) : (
|
||
batchStatus.logs.map((log: string, idx: number) => (
|
||
<div
|
||
key={idx}
|
||
className="whitespace-pre-wrap leading-relaxed"
|
||
>
|
||
{log}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|