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:
+53
-13
@@ -8,8 +8,13 @@ use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
// 检测防爬、验证码、登录墙特征
|
||||
fn detect_anti_bot(content: &str) -> Option<&'static str> {
|
||||
if content.len() > 150_000 {
|
||||
return None;
|
||||
}
|
||||
let lower = content.to_lowercase();
|
||||
let cf_patterns = [
|
||||
|
||||
// 1. 强特征防爬与 WAF 挑战(任何小于 150KB 的内容都做检测)
|
||||
let waf_patterns = [
|
||||
("checking your browser", "Cloudflare WAF 浏览器检查"),
|
||||
("please wait while we verify", "Cloudflare WAF 验证"),
|
||||
("cf-browser-verification", "Cloudflare WAF 验证特征"),
|
||||
@@ -35,11 +40,32 @@ fn detect_anti_bot(content: &str) -> Option<&'static str> {
|
||||
("shieldsquare_styles", "ShieldSquare WAF 拦截"),
|
||||
];
|
||||
|
||||
for &(p, desc) in &cf_patterns {
|
||||
for &(p, desc) in &waf_patterns {
|
||||
if lower.contains(p) {
|
||||
return Some(desc);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 通用 HTTP 错误与 CDN 关键字检测(仅当内容长度小于 5000 字节时检测,避免在正常文献中误判 CDN 脚本等)
|
||||
if content.len() < 5000 {
|
||||
let err_patterns = [
|
||||
("cloudflare", "Cloudflare 错误/防护页面"),
|
||||
("service temporarily unavailable", "503 服务暂时不可用"),
|
||||
("503 service", "503 服务异常"),
|
||||
("502 bad gateway", "502 网关错误"),
|
||||
("504 gateway timeout", "504 网关超时"),
|
||||
("403 forbidden", "403 访问被拒绝"),
|
||||
("404 not found", "404 资源未找到"),
|
||||
("500 internal server error", "500 服务器错误"),
|
||||
("site error", "网站错误"),
|
||||
];
|
||||
for &(p, desc) in &err_patterns {
|
||||
if lower.contains(p) {
|
||||
return Some(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -342,10 +368,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
db_skip_type_cleaned += 1;
|
||||
}
|
||||
} else {
|
||||
let has_valid_pdf = pdf_path_opt.as_ref()
|
||||
.map(|p| !p.starts_with("error:") && library_dir.join(p).exists())
|
||||
.unwrap_or(false);
|
||||
let has_valid_html = html_path_opt.as_ref()
|
||||
.map(|p| !p.starts_with("error:") && library_dir.join(p).exists())
|
||||
.unwrap_or(false);
|
||||
|
||||
if let Some(ref pdf_p) = pdf_path_opt {
|
||||
if pdf_p.starts_with("error:") {
|
||||
db_pdf_err_text += 1;
|
||||
pdf_db_msg = format!("数据库存储了报错字符串: {}", pdf_p);
|
||||
if has_valid_html {
|
||||
db_pdf_err_text += 1;
|
||||
pdf_db_msg = format!("文献已成功下载 HTML 格式,但 PDF 仍留有报错日志(将清理为 NULL): {}", pdf_p);
|
||||
need_db_fix = true;
|
||||
pdf_needs_fix = true;
|
||||
} else {
|
||||
db_pdf_err_text += 1;
|
||||
pdf_db_msg = format!("数据库存储了报错字符串: {}", pdf_p);
|
||||
}
|
||||
} else if !library_dir.join(pdf_p).exists() {
|
||||
db_pdf_missing += 1;
|
||||
pdf_db_msg = format!("物理 PDF 文件丢失 (路径: {})", pdf_p);
|
||||
@@ -356,8 +396,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
if let Some(ref html_p) = html_path_opt {
|
||||
if html_p.starts_with("error:") {
|
||||
db_html_err_text += 1;
|
||||
html_db_msg = format!("数据库存储了报错字符串: {}", html_p);
|
||||
if has_valid_pdf {
|
||||
db_html_err_text += 1;
|
||||
html_db_msg = format!("文献已成功下载 PDF 格式,但 HTML 仍留有报错日志(将清理为 NULL): {}", html_p);
|
||||
need_db_fix = true;
|
||||
html_needs_fix = true;
|
||||
} else {
|
||||
db_html_err_text += 1;
|
||||
html_db_msg = format!("数据库存储了报错字符串: {}", html_p);
|
||||
}
|
||||
} else if !library_dir.join(html_p).exists() {
|
||||
db_html_missing += 1;
|
||||
html_db_msg = format!("物理 HTML 文件丢失 (路径: {})", html_p);
|
||||
@@ -374,13 +421,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
markdown_needs_fix = true;
|
||||
} else {
|
||||
// 如果 Markdown 物理文件存在,但它既没有有效 PDF 也没有有效 HTML
|
||||
let has_valid_pdf = pdf_path_opt.as_ref()
|
||||
.map(|p| !p.starts_with("error:") && library_dir.join(p).exists())
|
||||
.unwrap_or(false);
|
||||
let has_valid_html = html_path_opt.as_ref()
|
||||
.map(|p| !p.starts_with("error:") && library_dir.join(p).exists())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_valid_pdf && !has_valid_html {
|
||||
db_markdown_orphaned += 1;
|
||||
markdown_db_msg = format!("Markdown 存在且完好,但失去有效 PDF/HTML 数据源,判定为孤立的 Markdown (路径: {})", md_p);
|
||||
|
||||
Reference in New Issue
Block a user