AstroResearch/src/services/batch/asset/helpers.rs
Asfmq 5f2d2d83f6 refactor: 大文件模块化拆分、自进化Skill管线、前端设计系统统一
后端:
  - runtime: 拆分 AgentConfig/events/duplicate_detector 为独立模块
  - error_recovery: 1499行单体拆为 classification(21种FailoverReason)/overflow/mod
  - executor: 提取 helpers.rs (PreparedCall/ToolExecutionResult/execute_single_tool)
  - skills: 新增 SkillCreator + SelfImprovePipeline(模式检测→自动生成SKILL.md→质量审查)
  - clients/llm: 拆分为 chat/embedding/types 三个子模块
  - services/download: 1548行拆为 mod/headers(反爬+SSRF)/strategies(多级回退)
  - services/batch/asset: 1264行拆为 mod/helpers/process

  前端:
  - 设计系统统一: sky/indigo → blueprint 色系, rounded-xl→lg, shadow-lg→sm
  - 删除 Vite 模板残留 App.css
  - GlobalDialog/PaperDetailModal/UncachedPaperModal 提取公共 BaseModal 组件
2026-06-27 09:56:36 +08:00

239 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/services/batch/asset/helpers.rs
//
// 批量处理各步骤的独立函数,从 process_single_bibcode 提取,
// 降低单文件行数并提高可测试性。
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use sqlx::SqlitePool;
use tokio::sync::Mutex;
use crate::services::download::Downloader;
use crate::Config;
use super::{AssetBatchStatus, BatchAction};
/// 执行单篇文献的下载步骤。
///
/// 若成功下载 PDF 或 HTML 任一格式,更新 `pdf_path` / `html_path` 并写回数据库。
/// 返回 true 表示下载成功至少一个文件false 表示全部失败。
#[allow(clippy::too_many_arguments)]
pub(super) async fn run_download_step(
bibcode: &str,
action: BatchAction,
db: &SqlitePool,
config: &Config,
downloader: &Arc<Downloader>,
status: &Arc<Mutex<AssetBatchStatus>>,
dl_count: &AtomicI32,
dl_failed_count: &AtomicI32,
arxiv_id: &str,
doi: &str,
pdf_path: &mut Option<String>,
html_path: &mut Option<String>,
) -> bool {
// 仅当 action 包含下载时执行
if action != BatchAction::Download && action != BatchAction::All {
return true; // 无需下载,视为"成功"
}
let is_pdf_exist = pdf_path
.as_ref()
.map(|p| config.library_dir.join(p).exists())
.unwrap_or(false);
let is_html_exist = html_path
.as_ref()
.map(|p| config.library_dir.join(p).exists())
.unwrap_or(false);
if is_pdf_exist || is_html_exist {
{
let mut s = status.lock().await;
s.add_log(format!(
"文献 {} 本地已存在 PDF 或 HTML跳过下载。",
bibcode
));
}
let _c = dl_count.fetch_add(1, Ordering::SeqCst) + 1;
{
let mut s = status.lock().await;
s.downloaded = _c;
}
return true;
}
// 需要执行下载
{
let mut s = status.lock().await;
s.add_log(format!("文献 {} 本地无 PDF/HTML开始下载...", bibcode));
}
let (pdf_res, html_res) = if !arxiv_id.is_empty() {
let res = downloader
.download_arxiv_direct(arxiv_id, &config.library_dir)
.await;
if res.0.is_ok() || res.1.is_ok() {
res
} else {
{
let mut s = status.lock().await;
s.add_log(format!(
"文献 {} arXiv 通道下载失败,回退尝试 ADS/出版商下载...",
bibcode
));
}
let doi_opt = if !doi.is_empty() { Some(doi) } else { None };
downloader
.download_paper(bibcode, doi_opt, &config.library_dir)
.await
}
} else {
let doi_opt = if !doi.is_empty() { Some(doi) } else { None };
downloader
.download_paper(bibcode, doi_opt, &config.library_dir)
.await
};
if pdf_res.is_ok() || html_res.is_ok() {
let pdf_rel = match pdf_res {
Ok(p) => Some(
p.strip_prefix(&config.library_dir)
.unwrap_or(&p)
.to_string_lossy()
.to_string(),
),
Err(_) => None,
};
let html_rel = match html_res {
Ok(p) => Some(
p.strip_prefix(&config.library_dir)
.unwrap_or(&p)
.to_string_lossy()
.to_string(),
),
Err(_) => None,
};
*pdf_path = pdf_rel.clone();
*html_path = html_rel.clone();
let _ = sqlx::query("UPDATE papers SET pdf_path = ?, html_path = ? WHERE bibcode = ?")
.bind(pdf_rel)
.bind(html_rel)
.bind(bibcode)
.execute(db)
.await;
let _c = dl_count.fetch_add(1, Ordering::SeqCst) + 1;
{
let mut s = status.lock().await;
s.downloaded = _c;
s.add_log(format!("文献 {} 下载成功!", bibcode));
}
true
} else {
let _f = dl_failed_count.fetch_add(1, Ordering::SeqCst) + 1;
let mut s = status.lock().await;
s.download_failed = _f;
let pdf_err = match pdf_res {
Err(e) => format!("error: {}", e),
_ => "error: 未知错误".to_string(),
};
let html_err = match html_res {
Err(e) => format!("error: {}", e),
_ => "error: 未知错误".to_string(),
};
s.add_log(format!(
"文献 {} 下载失败。PDF: {}, HTML: {}",
bibcode, pdf_err, html_err
));
let _ = sqlx::query("UPDATE papers SET pdf_path = ?, html_path = ? WHERE bibcode = ?")
.bind(&pdf_err)
.bind(&html_err)
.bind(bibcode)
.execute(db)
.await;
false
}
}
/// 生成文献的源链接 URL优先 ADS 标准 bibcode其次 arXiv
pub(super) fn build_source_url(bibcode: &str, arxiv_id: &str) -> String {
if bibcode.len() == 19 {
format!("https://ui.adsabs.harvard.edu/abs/{}/abstract", bibcode)
} else if !arxiv_id.is_empty() {
format!(
"https://ui.adsabs.harvard.edu/abs/arXiv:{}/abstract",
arxiv_id
)
} else {
format!("https://ui.adsabs.harvard.edu/abs/{}/abstract", bibcode)
}
}
/// 为已解析的 Markdown 内容构建 YAML frontmatter 头部。
pub(super) async fn build_markdown_with_frontmatter(
db: &SqlitePool,
bibcode: &str,
md_body: &str,
source_url: &str,
) -> Option<String> {
use sqlx::Row;
let row =
sqlx::query("SELECT title, authors, pub, year, keywords FROM papers WHERE bibcode = ?")
.bind(bibcode)
.fetch_optional(db)
.await
.ok()??;
let title: String = row.get(0);
let authors_json: String = row.get(1);
let pub_journal: String = row.get(2);
let year: String = row.get(3);
let keywords_json: String = row.get(4);
let authors: Vec<String> = serde_json::from_str(&authors_json).unwrap_or_default();
let keywords: Vec<String> = serde_json::from_str(&keywords_json).unwrap_or_default();
let front_matter = format!(
"---\ntitle: {}\nauthor: [{}]\npublisher: {}\nsource: \"{}\"\ndate: \"{}\"\ntags: \"{}\"\n---\n\n",
serde_json::to_string(&title).unwrap_or_else(|_| format!("\"{}\"", title)),
authors
.iter()
.map(|a| format!("\"{}\"", a))
.collect::<Vec<_>>()
.join(", "),
serde_json::to_string(&pub_journal)
.unwrap_or_else(|_| format!("\"{}\"", pub_journal)),
source_url,
year,
keywords.join(",")
);
Some(format!("{}{}", front_matter, md_body))
}
/// 将 Markdown 内容写入 library_dir 并更新数据库 markdown_path。
pub(super) fn write_markdown_and_update_db(
config: &Config,
bibcode: &str,
content: &str,
) -> Option<String> {
let md_filename = format!("{}.md", bibcode);
let md_dest = config.library_dir.join("Markdown").join(&md_filename);
if let Some(parent) = md_dest.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::write(&md_dest, content).is_ok() {
Some(format!("Markdown/{}", md_filename))
} else {
None
}
}