后端核心变更: - 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 (文献信息渲染)
101 lines
3.2 KiB
Rust
101 lines
3.2 KiB
Rust
// src/agent/tools/astro/research/note.rs
|
|
// SaveNoteTool — 保存研究笔记
|
|
|
|
use async_trait::async_trait;
|
|
use serde_json::json;
|
|
use tracing::info;
|
|
|
|
use crate::agent::tools::{AgentTool, InterruptBehavior, ToolContext, ToolOutput};
|
|
|
|
pub struct SaveNoteTool;
|
|
|
|
#[async_trait]
|
|
impl AgentTool for SaveNoteTool {
|
|
fn name(&self) -> &str {
|
|
"save_note"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"将研究中间结果或最终结论保存为 Markdown 笔记文件。适用于记录阅读摘要、研究思路、文献分析等。"
|
|
}
|
|
|
|
fn parameters(&self) -> serde_json::Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "string",
|
|
"description": "笔记标题,将被用于文件名(特殊字符会被过滤)"
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "笔记正文内容,支持 Markdown 格式(含 LaTeX 数学公式)"
|
|
}
|
|
},
|
|
"required": ["title", "content"]
|
|
})
|
|
}
|
|
|
|
fn group(&self) -> &str {
|
|
"as:research"
|
|
}
|
|
|
|
fn interrupt_behavior(&self) -> InterruptBehavior {
|
|
InterruptBehavior::Block
|
|
}
|
|
|
|
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
|
|
let title = match args.get("title").and_then(|t| t.as_str()) {
|
|
Some(t) => t.to_string(),
|
|
None => return ToolOutput::error("缺少必需参数 'title'"),
|
|
};
|
|
let content = match args.get("content").and_then(|c| c.as_str()) {
|
|
Some(c) => c.to_string(),
|
|
None => return ToolOutput::error("缺少必需参数 'content'"),
|
|
};
|
|
|
|
info!("[SaveNote] 保存笔记: {}", title);
|
|
let state = &ctx.app_state;
|
|
|
|
// 文件名安全化
|
|
let safe_title: String = title
|
|
.chars()
|
|
.map(|c| {
|
|
if c.is_alphanumeric() || c == '-' || c == '_' || c == ' ' {
|
|
c
|
|
} else {
|
|
'_'
|
|
}
|
|
})
|
|
.collect::<String>()
|
|
.replace(' ', "_");
|
|
|
|
let notes_dir = state.config.library_dir.join("notes");
|
|
if let Err(e) = std::fs::create_dir_all(¬es_dir) {
|
|
return ToolOutput::error(format!("创建笔记目录失败: {}", e));
|
|
}
|
|
|
|
let filename = format!("{}.md", safe_title);
|
|
let filepath = notes_dir.join(&filename);
|
|
|
|
let full_content = format!(
|
|
"---\ntitle: {}\ndate: {}\ngenerated_by: AstroResearch Agent\n---\n\n{}",
|
|
title,
|
|
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
|
content
|
|
);
|
|
|
|
match std::fs::write(&filepath, &full_content) {
|
|
Ok(_) => ToolOutput::success(
|
|
format!("笔记已保存: {}", filename),
|
|
json!({
|
|
"filename": filename,
|
|
"path": filepath.to_string_lossy(),
|
|
"size": full_content.len()
|
|
}),
|
|
),
|
|
Err(e) => ToolOutput::error(format!("保存笔记失败: {}", e)),
|
|
}
|
|
}
|
|
}
|