核心架构变更:
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() 辅助方法
187 lines
5.5 KiB
Rust
187 lines
5.5 KiB
Rust
// 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, ¶ms.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(¶ms.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 }))
|
||
}
|