AstroResearch/src/agent/tools/astro/paper.rs
Asfmq cec4b8cf7b feat: Docker 容器化、Cookie 鉴权、Coordinator 编排、FTS5 搜索与 P1-P3 全面收尾
Docker 容器化部署
  - 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
    两种镜像,Docker Compose 一键启动
  - build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
  - 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制

  安全:Cookie-Based 鉴权系统
  - HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
  - 登录/登出/验证接口 + 中间件注入
  - 前端登录页面 + 退出按钮
  - 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
  - 书签脚本 fetch 添加 credentials:'include'

  Coordinator 模式 (P2)
  - 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
  - WorkerPool + Semaphore 并发控制 + 超时保护
  - 前端协调者模式开关

  Hook 系统:UserPromptSubmit 事件 (P2)
  - 第 13 个生命周期事件,fire-and-forget 审计

  FTS5 全文搜索 (P3)
  - agent_sessions_fts + agent_messages_fts 虚拟表
  - search_history Agent 工具 + /api/search/history HTTP 接口
  - 前端防抖搜索框 + 仅当前会话筛选

  工具加载优化 (P3)
  - defer_loading 延迟加载 (7 个重型工具)
  - is_readonly 只读标记 (9 个查询工具)
  - classifier_summary 工具目录供 LLM 按需判断

  模型回退策略 (P3)
  - LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
  - LlmClient model 改为 Arc<RwLock> 支持运行时切换
  - 连续 3 次过载后自动切换

  压缩记忆桥接 (P3)
  - 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)

  git2 依赖修复
  - 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
2026-06-23 20:22:06 +08:00

260 lines
8.7 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/agent/tools/paper.rs — 文献下载、解析、内容读取工具
use async_trait::async_trait;
use serde_json::json;
use tracing::info;
use crate::agent::tools::{AgentTool, InterruptBehavior, ToolContext, ToolOutput};
// ── GetPaperContentTool ──
/// 获取文献内容工具:仅从本地读取已解析的 Markdown 全文
pub struct GetPaperContentTool;
#[async_trait]
impl AgentTool for GetPaperContentTool {
fn name(&self) -> &str {
"get_paper_content"
}
fn description(&self) -> &str {
"读取并在本地库中获取已解析的文献 Markdown 完整文本内容。\
注意:本工具仅能读取已在数据库注册且已解析的文献,不会自动触发下载或解析。\
若文献未下载或未解析,本工具会返回详细指引。"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"bibcode": {
"type": "string",
"description": "文献的唯一标识符,支持 ADS Bibcode、DOI 或 arXiv ID"
}
},
"required": ["bibcode"]
})
}
/// 纯读取操作,并发安全
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
true
}
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
let bibcode = match args.get("bibcode").and_then(|b| b.as_str()) {
Some(b) => b.to_string(),
None => return ToolOutput::error("缺少必需参数 'bibcode'"),
};
info!("[GetPaperContent] 获取文献内容: {}", bibcode);
let state = &ctx.app_state;
let paths = crate::api::helpers::check_paper_paths_in_db(
&state.db,
&state.config.library_dir,
&bibcode,
)
.await;
let md_opt = match paths {
Ok(Some((_, _, md_opt, _))) => md_opt,
Ok(None) => return ToolOutput::error(
"获取文献内容失败:该文献未在本地数据库中注册,请先使用 search_papers 搜索该文献。",
),
Err(e) => return ToolOutput::error(format!("获取文献内容失败: {}", e)),
};
let md_rel = match md_opt {
Some(rel) => rel,
None => {
return ToolOutput::error(
"获取文献内容失败:该文献尚未完成结构化解析。如果未下载,请先调用 download_paper如果已下载请先调用 parse_paper 进行解析。",
)
}
};
let md_abs = state.config.library_dir.join(&md_rel);
if !md_abs.exists() {
return ToolOutput::error(
"获取文献内容失败:文献本地 Markdown 文件已丢失,请重新调用 parse_paper 进行解析。",
);
}
match std::fs::read_to_string(&md_abs) {
Ok(content) => ToolOutput::success(
content.clone(),
json!({ "bibcode": bibcode, "chars": content.len() }),
),
Err(e) => ToolOutput::error(format!("获取文献内容失败,读取本地文件错误: {}", e)),
}
}
fn is_readonly(&self) -> bool {
true
}
}
// ── DownloadPaperTool ──
/// 下载文献全文资源工具
pub struct DownloadPaperTool;
#[async_trait]
impl AgentTool for DownloadPaperTool {
fn name(&self) -> &str {
"download_paper"
}
fn description(&self) -> &str {
"下载指定文献的全文资源PDF 或 HTML至本地图书馆为后续的结构化解析做好准备。\
适用于:需要阅读或分析新搜寻到的、尚未下载的文献。"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"bibcode": {
"type": "string",
"description": "文献的唯一标识符,支持 ADS Bibcode、DOI 或 arXiv ID"
},
"force": {
"type": "boolean",
"description": "是否强制重新下载(即使本地已下载该文献)"
}
},
"required": ["bibcode"]
})
}
/// 下载有副作用,中断时应阻塞以完成
fn interrupt_behavior(&self) -> InterruptBehavior {
InterruptBehavior::Block
}
/// 下载失败时应中止兄弟并行执行(避免继续处理同一文献)
fn causes_sibling_abort(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
let bibcode = match args.get("bibcode").and_then(|b| b.as_str()) {
Some(b) => b.to_string(),
None => return ToolOutput::error("缺少必需参数 'bibcode'"),
};
let force = args.get("force").and_then(|f| f.as_bool()).unwrap_or(false);
info!("[DownloadPaper] 下载文献: {}, 强制重下: {}", bibcode, force);
let state = &ctx.app_state;
match state
.downloader
.download_paper_service(&state.db, &state.config.library_dir, &bibcode, force)
.await
{
Ok(paper) => ToolOutput::success(
format!(
"文献 {} 全文资源下载成功。格式 - PDF: {}, HTML: {}",
bibcode, paper.has_pdf, paper.has_html
),
json!({ "bibcode": bibcode, "has_pdf": paper.has_pdf, "has_html": paper.has_html }),
),
Err(e) => ToolOutput::error(format!("文献 {} 下载失败: {}", bibcode, e)),
}
}
fn defer_loading(&self) -> bool {
true
}
}
// ── ParsePaperTool ──
/// 结构化解析文献内容工具
pub struct ParsePaperTool;
#[async_trait]
impl AgentTool for ParsePaperTool {
fn name(&self) -> &str {
"parse_paper"
}
fn description(&self) -> &str {
"将指定文献本地已下载的 HTML 或 PDF 资源解析为结构化的 Markdown 文本,并保存至本地 Markdown 文件夹。\
注意:调用此工具前必须确保文献已被成功下载(已执行 download_paper"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"bibcode": {
"type": "string",
"description": "文献的唯一标识符,支持 ADS Bibcode、DOI 或 arXiv ID"
},
"force": {
"type": "boolean",
"description": "是否强制重新解析(即使本地已解析过该文献)"
}
},
"required": ["bibcode"]
})
}
/// 解析有副作用(写文件),中断时应阻塞以完成
fn interrupt_behavior(&self) -> InterruptBehavior {
InterruptBehavior::Block
}
/// 解析失败时应中止兄弟并行执行
fn causes_sibling_abort(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
let bibcode = match args.get("bibcode").and_then(|b| b.as_str()) {
Some(b) => b.to_string(),
None => return ToolOutput::error("缺少必需参数 'bibcode'"),
};
let force = args.get("force").and_then(|f| f.as_bool()).unwrap_or(false);
info!("[ParsePaper] 解析文献: {}, 强制重析: {}", bibcode, force);
let state = &ctx.app_state;
match crate::services::parser::parse_paper_service(
&state.db,
&state.config.library_dir,
&state.qiniu,
&state.config,
&bibcode,
force,
)
.await
{
Ok(markdown) => ToolOutput::success(
format!(
"文献 {} 结构化解析成功。解析后 Markdown 字符总数: {}",
bibcode,
markdown.len()
),
json!({ "bibcode": bibcode, "chars": markdown.len() }),
),
Err(e) => {
let msg = e.to_string();
if msg.contains("请先下载") {
ToolOutput::error(format!(
"文献 {} 解析失败:未检测到已下载的本地资源文件,请先调用 download_paper 工具进行下载。",
bibcode
))
} else {
ToolOutput::error(format!("文献 {} 解析失败: {}", bibcode, msg))
}
}
}
}
fn defer_loading(&self) -> bool {
true
}
}