AstroResearch/src/agent/tools/astro/research/paper.rs
Asfmq 5ad0cf1d28 - Config 按职责拆分为 LlmConfig/EmbeddingConfig/VisionConfig/CdsConfig/StorageConfig 五个子结构
- AppState 拆分为 LlmState/DataSourceState/SessionState 三个子结构,消除 50+ 平铺字段
- 新增 ServiceError 结构化错误类型替代 handler 中的 msg.contains() 字符串匹配
- 移除 api::handlers 兼容命名空间,路由直接引用 agent/auth/papers 等模块

- login_rate_limiter/upload_rate_limiter 从 Mutex\<HashMap\> 迁移为 DashMap(无锁)
- 新增 session_last_active: DashMap\<String, AtomicU64\>,auth 中间件快速路径免写锁
- 会话过期清理改为按间隔触发(300s),避免每次请求全表扫描
- MAX_SESSIONS 1000→10000,SSE 广播通道 256→1024

- AgentTool trait 新增 is_internal()/display_name(),SSE 事件携带工具元数据
- 新增 GET /chat/tools 端点暴露注册工具列表
- pending_questions/pending_permissions 增加 created_at 时间戳,自动清理过期条目(10min TTL)
- Agent 超时现在正确 abort 后台任务并设置取消令牌

- FITS 解析: APOGEE/DESI 改用 read_image+切片替代 read_rows(修复 fitsio panic)
- ZTF: CIRCLE 参数分隔符 +→空格(修复 IRSA 400),半径自动裁剪至硬上限
- MAST TESS: parse_tic_json 兼容数组/对象两种 API 响应格式
- 统一检索: per_target_limit 默认 50→1,sources 支持 per-source release/version
- Gaia 测光从 VizieR 镜像切换至官方 TAP 服务

- 向量化降级从串行改为并发 5 条/批(buffer_unordered)
- 混合检索 RRF 合并从借用改为 owned RetrievalResult

- PDF 中间件: URL 解码 %2F/%2E 后判扩展名;文件名过滤非 ASCII + 禁 \ 防头注入
- chat_agent 日志截断问题内容至 50 字符;list_sessions 强制 limit clamp

- 设计令牌三层架构: primitive→semantic→component,浅色暖纸张学术/暗色 Night Indigo
- useTheme hook + ThemeToggle 侧边栏组件 + main.tsx 防 FOUC 初始化
- 全组件从硬编码 slate 色迁移至语义令牌(bg-surface/text-content/border-subtle 等)
- 新增 ToastContainer 非阻塞通知系统

- deploy.sh 引入 SSH ControlMaster 单次密码复用
2026-07-11 14:12:11 +08:00

206 lines
6.9 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/astro/research/paper_reader_tools.rs
//
// 文献内容读取与大纲提取的智能体工具定义。
use async_trait::async_trait;
use serde_json::json;
use tracing::info;
use crate::agent::tools::{AgentTool, ToolContext, ToolOutput};
// ==========================================
// 1. GetPaperOutlineTool (获取文献大纲目录)
// ==========================================
#[derive(Default)]
pub struct GetPaperOutlineTool;
#[async_trait]
impl AgentTool for GetPaperOutlineTool {
fn name(&self) -> &'static str {
"get_paper_outline"
}
fn display_name(&self) -> &str {
"文献提纲提取"
}
fn description(&self) -> &'static str {
"获取文献的章节大纲目录,列出所有章节的序号和标题。\n\
在读取文献内容前,强烈建议先使用此工具了解章节结构,以便按需读取,节约 token。"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"bibcode": {
"type": "string",
"description": "文献的唯一标识符bibcode如 2006BaltA..15...69W"
}
},
"required": ["bibcode"]
})
}
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 mode = crate::services::paper::ReadMode::Outline { max_level: Some(3) };
match crate::services::paper::read_paper_content(
&state.db,
&state.config.storage.library_dir,
&bibcode,
mode,
)
.await
{
Ok(res) => {
let sections = res.outline.unwrap_or_default();
if sections.is_empty() {
return ToolOutput::success(
"该文献未检测到章节标题(## 或 ### 格式)。请直接读取全文。",
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.clone(),
})
})
.collect();
let header = format!("文献 {} 章节大纲(共 {} 节):\n", bibcode, sections.len());
let footer = "\n\n使用 get_paper_content 并传入 section_index 或 section_name 读取指定章节内容。";
ToolOutput::success(
header + &res.content + footer,
json!({
"bibcode": bibcode,
"section_count": sections.len(),
"sections": display_sections
}),
)
}
Err(e) => ToolOutput::error(e.to_string()),
}
}
}
// ==========================================
// 2. GetPaperContentTool (获取文献具体内容)
// ==========================================
#[derive(Default)]
pub struct GetPaperContentTool;
#[async_trait]
impl AgentTool for GetPaperContentTool {
fn name(&self) -> &'static str {
"get_paper_content"
}
fn display_name(&self) -> &str {
"文献内容读取"
}
fn description(&self) -> &'static str {
"获取文献的具体内容(全文或指定章节内容)。\n\
可传入 section_index 或 section_name 指定仅读取某一章节。\n\
先使用 get_paper_outline 获取章节结构后按需读取,可大幅减少 token 消耗。"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"bibcode": {
"type": "string",
"description": "文献的唯一标识符bibcode如 2006BaltA..15...69W"
},
"section_index": {
"type": "integer",
"description": "章节序号0-based。从 get_paper_outline 返回的 sections[index] 获取。与 section_name 二选一"
},
"section_name": {
"type": "string",
"description": "章节标题文本(支持模糊匹配,如 'introduction')。与 section_index 二选一"
}
},
"required": ["bibcode"]
})
}
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 section_index = args
.get("section_index")
.and_then(|s| s.as_u64())
.map(|n| n as usize);
let section_name = args
.get("section_name")
.and_then(|s| s.as_str())
.map(String::from);
info!(
"[GetPaperContent] bibcode={}, section_index={:?}, section_name={:?}",
bibcode, section_index, section_name
);
let state = &ctx.app_state;
let mode = if let Some(idx) = section_index {
crate::services::paper::ReadMode::SectionIndex(idx)
} else if let Some(name) = section_name {
crate::services::paper::ReadMode::SectionName(name)
} else {
crate::services::paper::ReadMode::Full {
include_translation: false,
}
};
let label = match &mode {
crate::services::paper::ReadMode::SectionIndex(idx) => Some(format!("#{}", idx)),
crate::services::paper::ReadMode::SectionName(name) => Some(name.clone()),
_ => None,
};
let is_full = matches!(mode, crate::services::paper::ReadMode::Full { .. });
match crate::services::paper::read_paper_content(
&state.db,
&state.config.storage.library_dir,
&bibcode,
mode,
)
.await
{
Ok(res) => ToolOutput::success(
res.content.clone(),
json!({
"bibcode": bibcode,
"chars": res.content.len(),
"section": label,
"full_text": is_full
}),
),
Err(e) => ToolOutput::error(e.to_string()),
}
}
}