AstroResearch/src/agent/tools/search_history.rs
Asfmq cec4b8cf7b feat: Docker 容器化、Cookie 鉴权、Coordinator 编排、FTS5 搜索与 P1-P3 全面收尾
Docker 容器化部署
  - 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
    两种镜像,Docker Compose 一键启动
  - build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
  - 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制

  安全:Cookie-Based 鉴权系统
  - HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
  - 登录/登出/验证接口 + 中间件注入
  - 前端登录页面 + 退出按钮
  - 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
  - 书签脚本 fetch 添加 credentials:'include'

  Coordinator 模式 (P2)
  - 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
  - WorkerPool + Semaphore 并发控制 + 超时保护
  - 前端协调者模式开关

  Hook 系统:UserPromptSubmit 事件 (P2)
  - 第 13 个生命周期事件,fire-and-forget 审计

  FTS5 全文搜索 (P3)
  - agent_sessions_fts + agent_messages_fts 虚拟表
  - search_history Agent 工具 + /api/search/history HTTP 接口
  - 前端防抖搜索框 + 仅当前会话筛选

  工具加载优化 (P3)
  - defer_loading 延迟加载 (7 个重型工具)
  - is_readonly 只读标记 (9 个查询工具)
  - classifier_summary 工具目录供 LLM 按需判断

  模型回退策略 (P3)
  - LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
  - LlmClient model 改为 Arc<RwLock> 支持运行时切换
  - 连续 3 次过载后自动切换

  压缩记忆桥接 (P3)
  - 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)

  git2 依赖修复
  - 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
2026-06-23 20:22:06 +08:00

137 lines
4.7 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/search_history.rs
//
// search_history 工具搜索跨会话历史记录P3 FTS5 集成)。
// Agent 可用此工具查找之前研究过的主题、已发现的结论。
use async_trait::async_trait;
use serde_json::json;
use super::{AgentTool, ToolContext, ToolOutput};
pub struct SearchHistoryTool;
#[async_trait]
impl AgentTool for SearchHistoryTool {
fn name(&self) -> &str {
"search_history"
}
fn description(&self) -> &str {
"搜索之前会话的历史记录,查找已研究过的主题、已发现的结论、已下载的文献。\
使用此工具可以避免重复研究已经完成的工作。"
}
fn parameters(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词(支持 FTS5 查询语法,多个词用空格分隔)"
},
"scope": {
"type": "string",
"enum": ["sessions", "messages", "all"],
"description": "搜索范围sessions=会话标题/摘要, messages=消息内容, all=全部",
"default": "all"
}
},
"required": ["query"]
})
}
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
true
}
fn is_readonly(&self) -> bool {
true
}
fn defer_loading(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput {
let query = match args.get("query").and_then(|v| v.as_str()) {
Some(q) if !q.is_empty() => q,
_ => return ToolOutput::error("缺少 query 参数"),
};
let scope = args.get("scope").and_then(|v| v.as_str()).unwrap_or("all");
let db = &ctx.app_state.db;
let mut results = Vec::new();
if scope == "all" || scope == "sessions" {
match sqlx::query_as::<_, (String, String, String)>(
"SELECT s.session_id, s.title, \
snippet(agent_sessions_fts, 1, '<mark>', '</mark>', '...', 40) \
FROM agent_sessions_fts fts \
JOIN agent_sessions s ON s.session_id = fts.session_id \
WHERE agent_sessions_fts MATCH $1 ORDER BY rank LIMIT 10",
)
.bind(query)
.fetch_all(db)
.await
{
Ok(rows) => {
for (sid, title, snippet) in rows {
results.push(json!({
"type": "session",
"session_id": sid,
"title": title,
"snippet": snippet,
}));
}
}
Err(e) => {
tracing::warn!("[SearchHistory] 搜索会话失败: {}", e);
}
}
}
if scope == "all" || scope == "messages" {
match sqlx::query_as::<_, (String, String, String, String)>(
"SELECT fts.session_id, s.title, \
snippet(agent_messages_fts, 2, '<mark>', '</mark>', '...', 80), fts.role \
FROM agent_messages_fts fts \
JOIN agent_sessions s ON s.session_id = fts.session_id \
JOIN agent_messages m ON m.rowid = fts.rowid \
WHERE agent_messages_fts MATCH $1 AND m.active = 1 \
ORDER BY rank LIMIT 10",
)
.bind(query)
.fetch_all(db)
.await
{
Ok(rows) => {
for (sid, title, snippet, role) in rows {
results.push(json!({
"type": format!("message/{}", role),
"session_id": sid,
"title": title,
"snippet": snippet,
}));
}
}
Err(e) => {
tracing::warn!("[SearchHistory] 搜索消息失败: {}", e);
}
}
}
if results.is_empty() {
ToolOutput::success(
format!("未找到与 \"{}\" 相关的历史记录。", query),
json!({"query": query, "results": [], "count": 0}),
)
} else {
let count = results.len();
ToolOutput::success(
format!("找到 {} 条与 \"{}\" 相关的历史记录", count, query),
json!({"query": query, "results": results, "count": count}),
)
}
}
}