后端核心变更: - API 层: 新增 AppError 枚举统一错误类型,替代散落的 (StatusCode, String) - Agent 工具域: 重组为 astro/system/ 和 astro/research/ 两级域,新增 ProcessPaperTool 流水线工具 - 安全: 新增 SSRF 双层防护 (同步字符串级 + 异步 DNS 解析级),覆盖 IPv4/IPv6 私网段 - 弱密码检测: 扩展弱密码列表并增加最小长度检查 - LLM 客户端: 新增 ChatCompleter/Embedder trait,支持依赖注入与批量向量化 embed_batch - 批量处理: AssetBatch 从串行改为 Semaphore 并发池 (BATCH_CONCURRENCY=3) - 分块器: 重写为三阶段结构化管线 (章节解析→短节合并→带标题路径子块) - RAG: embedding 计算移出事务,RetrievalResult 新增 headings/section_index 字段 - 检索: ADS/arXiv 并行检索 (tokio::join!),去重改用 HashSet,本地库回填批量 IN 查询 - 天体查询: Sesame API 升级到 v4,新增视差误差/自行/视向速度/多波段测光字段 - 迁移: 14 个增量文件合并为单一 init.sql,支持 sqlx::migrate! 内存库集成测试 - 测试: circuit_breaker/hooks/task_board/session/memory/streaming_executor 新增修正 15+ 测试 前端架构重构: - 目录重组: features/ → pages/ + components/ + hooks/ 三层分离 - App.tsx 从 1181 行压缩至 ~174 行 (逻辑抽入 9 个自定义 Hook) - Agent 面板拆分为 AgentSessionSidebar/AgentMessageList/AgentInputArea 子组件 - 新增 GlobalDialog/PaperDetailModal/UncachedPaperModal 通用对话框组件 - 工具函数抽取: celestial.ts (天体坐标格式), paper.tsx (文献信息渲染)
136 lines
4.4 KiB
Rust
136 lines
4.4 KiB
Rust
// src/agent/tools/astro/research/paper_outline.rs
|
|
// GetPaperOutlineTool — 提取文献章节大纲(目录)
|
|
|
|
use async_trait::async_trait;
|
|
use serde_json::json;
|
|
use tracing::info;
|
|
|
|
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
|
|
|
|
pub struct GetPaperOutlineTool;
|
|
|
|
#[async_trait]
|
|
impl AgentTool for GetPaperOutlineTool {
|
|
fn name(&self) -> &str {
|
|
"get_paper_outline"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"提取已解析文献的章节大纲(目录)。返回所有章节标题、层级和序号,供后续按章节读取使用。\
|
|
适用于:快速浏览文献结构、定位感兴趣的章节。配合 get_paper_content 的 section_index/section_name 参数使用。"
|
|
}
|
|
|
|
fn parameters(&self) -> serde_json::Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"bibcode": {
|
|
"type": "string",
|
|
"description": "文献的唯一标识符,支持 ADS Bibcode、DOI 或 arXiv ID"
|
|
}
|
|
},
|
|
"required": ["bibcode"]
|
|
})
|
|
}
|
|
|
|
fn group(&self) -> &str {
|
|
"as:research"
|
|
}
|
|
|
|
fn is_readonly(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
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!("[GetPaperOutline] 提取大纲: {}", bibcode);
|
|
let state = &ctx.app_state;
|
|
|
|
let paths =
|
|
match crate::api::helpers::check_paper_paths_in_db(
|
|
&state.db,
|
|
&state.config.library_dir,
|
|
&bibcode,
|
|
)
|
|
.await
|
|
{
|
|
Ok(Some((_, _, md_opt, _))) => md_opt,
|
|
Ok(None) => return ToolOutput::error(
|
|
"获取大纲失败:该文献未在本地数据库中注册。请先使用 search_papers 搜索该文献。",
|
|
),
|
|
Err(e) => return ToolOutput::error(format!("获取大纲失败: {}", e)),
|
|
};
|
|
|
|
let md_rel = match paths {
|
|
Some(rel) => rel,
|
|
None => {
|
|
return ToolOutput::error(
|
|
"获取大纲失败:该文献尚未完成结构化解析。请先使用 process_paper 执行 download 和 parse 任务。",
|
|
)
|
|
}
|
|
};
|
|
|
|
let md_abs = state.config.library_dir.join(&md_rel);
|
|
let content = match std::fs::read_to_string(&md_abs) {
|
|
Ok(c) => c,
|
|
Err(e) => return ToolOutput::error(format!("获取大纲失败,读取文件错误: {}", e)),
|
|
};
|
|
|
|
let sections = crate::services::section_parser::extract_outline(&content, 3);
|
|
|
|
if sections.is_empty() {
|
|
return ToolOutput::success(
|
|
"该文献未检测到章节标题(## 或 ### 格式)。请使用 get_paper_content 直接读取全文。",
|
|
json!({ "bibcode": bibcode, "sections": [] }),
|
|
);
|
|
}
|
|
|
|
let display_sections: Vec<serde_json::Value> = sections
|
|
.iter()
|
|
.map(|s| {
|
|
json!({
|
|
"index": s.index,
|
|
"level": s.level,
|
|
"heading": s.heading,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let content_display = sections
|
|
.iter()
|
|
.map(|s| {
|
|
let prefix = "#".repeat(s.level);
|
|
format!(
|
|
"[{}] {} {} ({} 字符)",
|
|
s.index,
|
|
prefix,
|
|
s.heading,
|
|
s.char_end - s.char_start
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let header = format!("文献 {} 章节大纲(共 {} 节):\n", bibcode, sections.len());
|
|
let footer =
|
|
"\n\n使用 get_paper_content 并传入 section_index 或 section_name 读取指定章节内容。";
|
|
|
|
ToolOutput::success(
|
|
header + &content_display + footer,
|
|
json!({
|
|
"bibcode": bibcode,
|
|
"section_count": sections.len(),
|
|
"sections": display_sections
|
|
}),
|
|
)
|
|
}
|
|
}
|