AstroResearch/src/api/targets.rs
Asfmq 85b6429c30 feat: Agent 多模式系统、视觉模型集成、LLM 能力分层与 P3 性能收尾
核心架构变更:

  1. Agent 多模式系统替代 Coordinator
     - 移除 src/agent/coordinator/(Coordinator Agent/Worker/Tools,946 行)
     - 新建 src/agent/modes/:声明式模式抽象(AgentMode/ModeConfig/ToolSet)
     - 三种内置模式:
       - default:通用科研助手,零覆盖保持现有行为
       - deep-research:16 步、启用思考、research 权限、系统性调研
       - literature-reader:白名单工具、只读沙箱、结构化阅读
     - ModeRegistry + ModeConfig 预设 + ToolSet 过滤 + 身份/原则覆盖
     - AgentRuntime::with_mode() 统一入口,模式持久化到 session.mode 字段
     - GET /api/chat/modes 提供模式列表给前端选择器

  2. 视觉模型与图片分析
     - 新增 analyze_image 工具(340 行):本地/URL 图片 → 视觉模型流式分析
     - LlmClient::analyze_image_stream():SSE 增量实时推送
     - 配置:LLM_VISION_MODEL / LLM_VISION_API_KEY / LLM_VISION_API_BASE
     - 前端:粘贴/选择图片附件,重试时复用文件路径
     - Service 层移除 /chat/rag 和 /chat/figure 端点,统一走 Agent SSE
     - Body limit 提升至 100MB 适配大图上传

  3. LLM 三级能力分层
     - Tier 1 (Core) → Tier 2 (Medium) → Tier 3 (Fast),级联回退
     - medium_llm / fast_llm / vision_llm 注入 AppState
     - 资产批量翻译 → Medium LLM + Semaphore(3) 并发控制
     - 记忆提取/上下文压缩子代理 → Fast LLM
     - SubAgentRunner::with_llm_client() 支持注入专用 LLM

  4. 数据库与性能优化
     - SQLite 启用 WAL + busy_timeout(10s) 处理并发写入
     - RAG ingest:DELETE 合并为原子语句 + 批量事务写入
     - Meta sync:save_paper_to_db_tx() 事务化批量插入
     - 翻译词典:first_words HashSet 预过滤 + next_valid_index 跳跃优化
     - read_file 不截断输出 + skip_persist 防止级联磁盘持久化

  5. 工具系统增强
     - ToolContext 增加 tool_call_id + max_output_chars
     - ToolOutput 增加 skip_persist 标记
     - TextDelta SSE 携带可选 tool_call_id 支持工具的流式输出
     - ChatMessage::text() 辅助方法
2026-06-24 19:52:27 +08:00

187 lines
5.5 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/api/targets.rs
use axum::{
extract::{Query, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use super::AppState;
use crate::services::target::{query_target_cached, TargetInfo};
#[derive(Deserialize)]
pub struct TargetQueryParams {
pub object_name: String,
}
#[derive(Deserialize)]
pub struct AssociateTargetRequest {
pub bibcode: String,
pub object_name: String,
}
#[derive(Deserialize)]
pub struct TargetListParams {
pub bibcode: String,
}
#[derive(Serialize)]
pub struct AssociateResponse {
pub status: String,
pub target: TargetInfo,
}
// ── GET /api/target/query ──
pub async fn query_target(
State(state): State<Arc<AppState>>,
Query(params): Query<TargetQueryParams>,
) -> Result<Json<TargetInfo>, (StatusCode, String)> {
let client = reqwest::Client::new();
let info = query_target_cached(&state.db, &params.object_name, None, &client)
.await
.map_err(|e| {
tracing::error!("查询天体信息失败 ({}): {}", params.object_name, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("查询天体信息失败: {}", e),
)
})?;
Ok(Json(info))
}
// ── POST /api/target/associate ──
pub async fn associate_target(
State(state): State<Arc<AppState>>,
Json(req): Json<AssociateTargetRequest>,
) -> Result<Json<AssociateResponse>, (StatusCode, String)> {
let client = reqwest::Client::new();
// 查询并将结果缓存/关联到对应文献
let info = query_target_cached(&state.db, &req.object_name, Some(&req.bibcode), &client)
.await
.map_err(|e| {
tracing::error!(
"手动关联天体失败 ({} -> {}): {}",
req.object_name,
req.bibcode,
e
);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("手动关联天体失败: {}", e),
)
})?;
Ok(Json(AssociateResponse {
status: "success".to_string(),
target: info,
}))
}
// ── GET /api/target/list ──
pub async fn list_targets(
State(state): State<Arc<AppState>>,
Query(params): Query<TargetListParams>,
) -> Result<Json<Vec<TargetInfo>>, (StatusCode, String)> {
let rows = sqlx::query_as::<_, (String, Option<String>, Option<String>, Option<f64>, Option<String>, Option<f64>, Option<String>)>(
"SELECT target_name, ra, dec, parallax, spectral_type, v_magnitude, aliases FROM paper_targets WHERE bibcode = ? ORDER BY target_name"
)
.bind(&params.bibcode)
.fetch_all(&state.db)
.await
.map_err(|e| {
tracing::error!("获取文献天体关联列表失败 ({}): {}", params.bibcode, e);
(StatusCode::INTERNAL_SERVER_ERROR, format!("获取文献天体关联列表失败: {}", e))
})?;
let targets: Vec<TargetInfo> = rows
.into_iter()
.map(
|(name, ra, dec, parallax, spectral_type, v_magnitude, aliases_json)| {
let aliases: Vec<String> = aliases_json
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
TargetInfo {
target_name: name,
ra,
dec,
parallax,
spectral_type,
v_magnitude,
aliases,
}
},
)
.collect();
Ok(Json(targets))
}
// ── POST /api/target/extract ──
#[derive(Deserialize)]
pub struct ExtractTargetsRequest {
pub bibcode: String,
}
#[derive(Serialize)]
pub struct ExtractTargetsResponse {
pub targets: Vec<TargetInfo>,
}
pub async fn extract_paper_targets(
State(state): State<Arc<AppState>>,
Json(req): Json<ExtractTargetsRequest>,
) -> Result<Json<ExtractTargetsResponse>, (StatusCode, String)> {
tracing::info!("接收到文献天体提取与识别指令: {}", req.bibcode);
let (_, _, md_opt, _) = crate::api::helpers::check_paper_paths_in_db(
&state.db,
&state.config.library_dir,
&req.bibcode,
)
.await
.map_err(|e| (StatusCode::NOT_FOUND, format!("获取文献路径失败: {}", e)))?
.ok_or((StatusCode::NOT_FOUND, "该文献未注册在数据库中".to_string()))?;
let md_rel = md_opt.ok_or((
StatusCode::BAD_REQUEST,
"文献尚未解析为 Markdown请先执行解析".to_string(),
))?;
let md_abs = state.config.library_dir.join(&md_rel);
if !md_abs.exists() {
return Err((
StatusCode::NOT_FOUND,
"文献 Markdown 文件未找到,请重新解析".to_string(),
));
}
let markdown_content = std::fs::read_to_string(&md_abs).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("读取 Markdown 文件失败: {}", e),
)
})?;
// 重新识别前先清除该文献已有的天体关联记录,确保陈旧和错误绑定的天体得到重置与刷新
if let Err(e) = sqlx::query("DELETE FROM paper_targets WHERE bibcode = ?")
.bind(&req.bibcode)
.execute(&state.db)
.await
{
tracing::error!("清除文献 {} 的旧天体关联失败: {}", req.bibcode, e);
}
let client = reqwest::Client::new();
let targets = crate::services::target::extract_and_cache_targets(
&state.db,
&markdown_content,
&req.bibcode,
&client,
)
.await;
Ok(Json(ExtractTargetsResponse { targets }))
}