feat: LLM/Embedding 客户端模块化、侧边栏折叠交互、arXiv→ADS 下载回退与前端体验重构

**后端架构**
  - 抽取翻译服务中内嵌的 LLM HTTP 调用为独立的 LlmClient /
    EmbeddingClient(src/clients/llm.rs),翻译模块改为委托调用,消除
    对 reqwest/serde 的直接耦合
  - Config 新增 EMBEDDING_API_KEY/EMBEDDING_API_BASE/EMBEDDING_MODEL
    三项配置,默认 fallback 至 LLM 对应值,补齐向量嵌入基础设施

  **下载策略优化**
  - arXiv 直连下载失败后自动回退至 ADS 网关 PUB_PDF→EPRINT_PDF→CrossRef
    多级通道,替换此前单路径策略;批量同步同步应用此逻辑
  - PDF/HTML 任一方成功时,失败方的 path 字段不再存储 "error:" 报错字符串,
    改为置 NULL,防止日志污染数据

  **前端交互增强**
  - 侧边栏支持折叠/展开:收起为仅图标模式(w-16),展开恢复完整模式(w-64);
    收起后点击 Logo 展开,含流畅 cubic-bezier 过渡动画
  - 阅读面板新增 PDF 内嵌预览:已下载 PDF 时可通过 iframe 切换查看
    /api/files 下的本地文献
  - reader/citation 面板未选文献时展示带图标的空状态引导页,替代空白页
  - 文献详情面板改为固定高度弹性布局(h-[460px]),各区块按比例分配避免
    内容挤压;期刊名过长截断+悬停tooltip;关键词无数据显式占位
  - 全局移除 emoji Unicode,统一替换为 lucide-react 图标组件,
    消除跨平台字体渲染差异

  **反爬检测精细化**
  - 按响应长度分层:>150KB 跳过检测(完整文献),<5KB 才扫描通用 HTTP
    错误关键字,杜绝长文献误触 Cloudflare/503 模式匹配
  - 新增 Radware Bot Manager、ShieldSquare WAF 特征识别

  **健壮性**
  - Obscura 下载校验失败后自动清理硬盘残留坏文件
  - 健康检查工具:文献已有有效 HTML 但 PDF 字段为旧报错时自动判定可修复
  - 上传接口 body limit 提升至 100MB,新增 /api/files 静态文件服务路由
  - StandardPaper 新增 has_pdf/has_html 字段区分格式级下载状态
This commit is contained in:
fmq
2026-06-13 11:11:33 +08:00
parent 2a5b1c0c91
commit 3f1935678b
27 changed files with 1275 additions and 473 deletions
+161 -43
View File
@@ -1,5 +1,6 @@
// dashboard/src/components/layout/Sidebar.tsx
import { Search, BookOpen, GitFork, Library, RefreshCw } from 'lucide-react';
import { useState } from 'react';
import { Search, BookOpen, GitFork, Library, RefreshCw, ChevronLeft } from 'lucide-react';
import type { StandardPaper } from '../../types';
interface SidebarProps {
@@ -10,31 +11,81 @@ interface SidebarProps {
}
export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations }: SidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
const renderLogo = () => (
<svg width="100%" height="100%" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
<circle cx="24" cy="24" r="18" stroke="#bae6fd" strokeWidth="1.5" />
<circle cx="24" cy="24" r="21" stroke="#0284c7" strokeWidth="1.5" strokeDasharray="2 3" />
<path d="M24 9C24 18 24 18 33 24C24 24 24 24 24 33C24 24 24 24 15 24C24 18 24 18 24 9Z" fill="url(#sidebarStarGrad)" />
<ellipse cx="24" cy="24" rx="20" ry="7" transform="rotate(-28 24 24)" stroke="#0284c7" strokeWidth="2" />
<circle cx="38" cy="16" r="4.5" fill="#0284c7" stroke="#ffffff" strokeWidth="1.5" />
<circle cx="10" cy="32" r="2.5" fill="#38bdf8" />
<defs>
<linearGradient id="sidebarStarGrad" x1="15" y1="9" x2="33" y2="33" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#0284c7" />
<stop offset="100%" stopColor="#0369a1" />
</linearGradient>
</defs>
</svg>
);
return (
<aside className="w-64 bg-slate-50 border-r border-slate-200 flex flex-col justify-between py-6 px-4 z-10 select-none">
<aside
className={`bg-slate-50 border-r border-slate-200 flex flex-col justify-between py-6 z-20 select-none transition-all duration-355 cubic-bezier(0.4, 0, 0.2, 1) ${
isCollapsed ? 'w-16 px-2' : 'w-64 px-4'
}`}
>
<div>
{/* 系统LOGO区 */}
<div className="px-3 mb-8 flex items-center gap-3">
<div className="w-9 h-9 flex items-center justify-center">
<svg className="w-9 h-9" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="18" stroke="#bae6fd" strokeWidth="1.5" />
<circle cx="24" cy="24" r="21" stroke="#0284c7" strokeWidth="1.5" strokeDasharray="2 3" />
<path d="M24 9C24 18 24 18 33 24C24 24 24 24 24 33C24 24 24 24 15 24C24 18 24 18 24 9Z" fill="url(#sidebarStarGrad)" />
<ellipse cx="24" cy="24" rx="20" ry="7" transform="rotate(-28 24 24)" stroke="#0284c7" strokeWidth="2" />
<circle cx="38" cy="16" r="4.5" fill="#0284c7" stroke="#ffffff" strokeWidth="1.5" />
<circle cx="10" cy="32" r="2.5" fill="#38bdf8" />
<defs>
<linearGradient id="sidebarStarGrad" x1="15" y1="9" x2="33" y2="33" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#0284c7" />
<stop offset="100%" stopColor="#0369a1" />
</linearGradient>
</defs>
</svg>
</div>
<div>
<h1 className="text-sm font-bold text-slate-800 tracking-wider">AstroResearch</h1>
<span className="text-[11px] text-slate-500 block font-medium"></span>
{/* 系统LOGO与折叠控制区 */}
<div className={`mb-8 flex items-center transition-all duration-300 ${
isCollapsed ? 'justify-center px-0' : 'justify-between px-3'
}`}>
{/* Logo & 标题文字 */}
<div className="flex items-center gap-3 min-w-0">
{/* Logo按钮 (只在折叠状态下可点击展开) */}
<button
type="button"
disabled={!isCollapsed}
onClick={() => setIsCollapsed(false)}
className={`flex items-center justify-center shrink-0 rounded-xl transition-all duration-300 ${
isCollapsed
? 'w-11 h-11 bg-white hover:bg-sky-50/50 border border-slate-200 cursor-pointer shadow-xs hover:shadow-sm hover:scale-105 active:scale-95'
: 'w-9 h-9 bg-transparent border border-transparent cursor-default'
}`}
title={isCollapsed ? "展开导航" : undefined}
>
<div className={`transition-all duration-300 flex items-center justify-center ${isCollapsed ? 'w-8 h-8' : 'w-9 h-9'}`}>
{renderLogo()}
</div>
</button>
{/* 系统说明文字 */}
<div
className={`flex flex-col transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 scale-95 -translate-x-2 pointer-events-none select-none overflow-hidden h-0'
: 'opacity-100 max-w-[150px] scale-100 translate-x-0'
}`}
>
<h1 className="text-sm font-bold text-slate-800 tracking-wider whitespace-nowrap">AstroResearch</h1>
<span className="text-[11px] text-slate-500 block font-medium font-sans whitespace-nowrap"></span>
</div>
</div>
{/* 折叠控制按钮 (圆润悬浮效果) */}
<button
type="button"
onClick={() => setIsCollapsed(true)}
className={`p-1.5 rounded-full bg-slate-100 hover:bg-slate-200 border border-slate-200 text-slate-500 hover:text-slate-800 transition-all duration-300 cursor-pointer shadow-2xs shrink-0 flex items-center justify-center hover:scale-105 active:scale-95 ${
isCollapsed
? 'opacity-0 scale-75 pointer-events-none w-0 h-0 p-0 border-0 overflow-hidden'
: 'opacity-100 scale-100'
}`}
title="收起导航"
>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
</div>
{/* 导航菜单列表 */}
@@ -43,22 +94,25 @@ export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations
{ id: 'search', label: '统一检索', icon: Search },
{ id: 'library', label: '馆藏管理', icon: Library },
{ id: 'sync', label: '批量任务', icon: RefreshCw },
{ id: 'reader', label: '双语阅读', icon: BookOpen, disabled: !selectedPaper },
{ id: 'citation', label: '引用星系', icon: GitFork, disabled: !selectedPaper },
].map(tab => {
{ id: 'reader', label: '双语阅读', icon: BookOpen },
{ id: 'citation', label: '引用星系', icon: GitFork },
].map((tab: { id: string; label: string; icon: any; disabled?: boolean }) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
disabled={tab.disabled}
title={isCollapsed ? tab.label : undefined}
onClick={() => {
setActiveTab(tab.id as any);
if (tab.id === 'citation' && selectedPaper) {
loadCitations(selectedPaper.bibcode);
}
}}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-semibold tracking-wider transition-all border ${
className={`w-full flex items-center rounded-lg text-xs font-semibold tracking-wider transition-all duration-300 border ${
isCollapsed ? 'px-2 py-2.5 justify-center' : 'px-3 py-2.5'
} ${
isActive
? 'bg-sky-50 border-sky-200 text-sky-700 shadow-sm'
: tab.disabled
@@ -66,29 +120,93 @@ export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations
: 'border-transparent text-slate-650 hover:bg-slate-100 hover:text-slate-800'
}`}
>
<Icon className={`w-4 h-4 ${isActive ? 'text-sky-600' : 'text-slate-500'}`} />
<span>{tab.label}</span>
<Icon className={`w-4 h-4 shrink-0 transition-colors duration-300 ${isActive ? 'text-sky-600' : 'text-slate-500'}`} />
<span
className={`truncate transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 pointer-events-none select-none overflow-hidden scale-90 -translate-x-2'
: 'opacity-100 max-w-[150px] scale-100 translate-x-0 ml-3'
}`}
>
{tab.label}
</span>
</button>
);
})}
</nav>
</div>
{/* 底部当前选定文献提示 */}
{selectedPaper ? (
<div className="p-3.5 rounded-lg border border-sky-100 bg-sky-50/50">
<span className="text-[9px] font-bold text-sky-600 tracking-widest block mb-1"></span>
<h4 className="text-xs text-slate-800 font-bold line-clamp-2 mb-2 leading-relaxed">{selectedPaper.title}</h4>
<div className="flex items-center justify-between text-[10px] font-medium text-slate-500">
<span>: {selectedPaper.year}</span>
<span className="truncate max-w-[90px] font-mono">{selectedPaper.bibcode}</span>
{/* 底部当前选定文献提示 (平滑动画版本) */}
<div className="space-y-4">
{selectedPaper ? (
<div
className={`relative overflow-hidden transition-all duration-300 border rounded-lg ${
isCollapsed
? 'p-0 border-transparent bg-transparent flex justify-center'
: 'p-3.5 border-sky-100 bg-sky-50/50'
}`}
title={isCollapsed ? `当前选定文献: ${selectedPaper.title}` : undefined}
>
{/* 折叠下的微型图书图标 */}
<div
className={`transition-all duration-300 flex items-center justify-center rounded-lg border border-sky-100 bg-sky-50 text-sky-600 shrink-0 shadow-sm ${
isCollapsed
? 'w-9 h-9 opacity-100 scale-100'
: 'w-0 h-0 opacity-0 scale-75 overflow-hidden'
}`}
>
<BookOpen className="w-4 h-4" />
</div>
{/* 展开下的完整详情 */}
<div
className={`transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 max-h-0 pointer-events-none select-none overflow-hidden'
: 'opacity-100 max-w-[200px] max-h-40'
}`}
>
<span className="text-[9px] font-bold text-sky-600 tracking-widest block mb-1"></span>
<h4 className="text-xs text-slate-800 font-bold line-clamp-2 mb-2 leading-relaxed">{selectedPaper.title}</h4>
<div className="flex items-center justify-between text-[10px] font-medium text-slate-500 gap-2">
<span className="shrink-0">: {selectedPaper.year}</span>
<span className="truncate max-w-[90px] font-mono">{selectedPaper.bibcode}</span>
</div>
</div>
</div>
</div>
) : (
<div className="p-3 rounded-lg border border-slate-200 bg-slate-100/30 text-center">
<span className="text-[10px] text-slate-400 font-medium tracking-wide"></span>
</div>
)}
) : (
<div
className={`relative overflow-hidden transition-all duration-300 border rounded-lg ${
isCollapsed
? 'p-0 border-transparent bg-transparent flex justify-center'
: 'p-3 border-slate-200 bg-slate-100/30'
}`}
title={isCollapsed ? "未选定研究目标" : undefined}
>
{/* 折叠下的微型馆藏图标 */}
<div
className={`transition-all duration-300 flex items-center justify-center rounded-lg border border-slate-200 bg-slate-100/30 text-slate-400 shrink-0 ${
isCollapsed
? 'w-9 h-9 opacity-100 scale-100'
: 'w-0 h-0 opacity-0 scale-75 overflow-hidden'
}`}
>
<Library className="w-4 h-4" />
</div>
{/* 展开下的提示文字 */}
<div
className={`text-center transition-all duration-300 origin-left ${
isCollapsed
? 'opacity-0 max-w-0 max-h-0 pointer-events-none select-none overflow-hidden'
: 'opacity-100 max-w-[200px] max-h-12'
}`}
>
<span className="text-[10px] text-slate-400 font-medium tracking-wide"></span>
</div>
</div>
)}
</div>
</aside>
);
}