From f6df9d81365f71261edf25d0902dfdb5b0e46b4e Mon Sep 17 00:00:00 2001 From: Asfmq <2696428814@qq.com> Date: Thu, 18 Jun 2026 01:21:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Agent=20=E6=80=9D=E8=80=83=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=89=8D=E7=AB=AF=E5=8F=AF=E6=8E=A7=E3=80=81=E5=AD=90?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=85=A8=E9=93=BE=E8=B7=AF=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E3=80=81=E6=9D=83=E9=99=90=E7=B3=BB=E7=BB=9F=E3=80=81?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20ID=20=E8=BF=BD=E8=B8=AA=E4=BD=93=E7=B3=BB?= =?UTF-8?q?=E3=80=81=E5=89=8D=E7=AB=AF=E9=9D=A2=E6=9D=BF=E4=B8=8E=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=9E=B6=E6=9E=84=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking 开关,仅千问/DashScope 时启用 - 完善权限系统,支持细粒度的权限控制和用户权限申请 - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构 - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识 - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段 - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参 - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段 - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化 - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放 - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义 - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档 - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南 - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明 --- .env.example | 130 +- CLAUDE.md | 39 +- Cargo.toml | 4 +- README.md | 95 +- .../src/features/agent/AgentMetricsPanel.tsx | 271 ++++ .../features/agent/AskUserQuestionCard.tsx | 293 +++++ .../src/features/agent/AuditLogViewer.tsx | 227 ++++ .../features/agent/PermissionRequestCard.tsx | 129 ++ .../src/features/agent/ResearchAgentPanel.tsx | 1145 +++++++++++------ dashboard/src/types.ts | 122 ++ docs/agent-optimization-analysis.md | 738 ----------- docs/api.md | 102 ++ docs/architecture.md | 318 +---- docs/architecture/agent/context.md | 378 ++++++ docs/architecture/agent/env-vars.md | 134 ++ docs/architecture/agent/hooks.md | 274 ++++ docs/architecture/agent/memory.md | 432 +++++++ docs/architecture/agent/overview.md | 174 +++ docs/architecture/agent/permission.md | 941 ++++++++++++++ docs/architecture/agent/skills.md | 339 +++++ docs/architecture/agent/subagent.md | 364 ++++++ docs/architecture/agent/system-prompt.md | 359 ++++++ docs/architecture/agent/tasks.md | 233 ++++ docs/architecture/agent/team.md | 232 ++++ docs/architecture/agent/tools.md | 425 ++++++ docs/architecture/core-modules.md | 56 + docs/architecture/core-workflows.md | 183 +++ docs/architecture/overview.md | 79 ++ docs/contributing.md | 52 +- docs/database.md | 67 + docs/deployment.md | 9 +- docs/development.md | 107 ++ libs/obscura | 1 - profiles/readonly.toml | 29 + profiles/research.toml | 38 + src/agent/compact.rs | 42 +- src/agent/hooks.rs | 24 +- src/agent/memory/types.rs | 4 +- src/agent/runtime/denial_tracker.rs | 179 +++ src/agent/runtime/executor.rs | 329 ++++- src/agent/runtime/file_cache.rs | 3 +- src/agent/runtime/mod.rs | 258 +++- src/agent/runtime/permission.rs | 677 +++++++++- src/agent/runtime/permission_explainer.rs | 195 +++ src/agent/runtime/permission_profile.rs | 177 +++ src/agent/runtime/streaming.rs | 3 +- src/agent/subagent.rs | 282 +++- src/agent/team/teammate.rs | 7 +- src/agent/tools/filesystem/bash.rs | 467 ++++++- src/agent/tools/filesystem/security.rs | 13 +- src/agent/tools/mod.rs | 123 +- src/agent/tools/skill.rs | 10 +- src/agent/tools/subagent.rs | 50 +- src/api/agent.rs | 111 +- src/api/helpers.rs | 7 +- src/api/mod.rs | 34 +- src/api/permissions.rs | 186 +++ src/clients/llm.rs | 31 +- src/main.rs | 22 +- src/services/parser/common.rs | 4 +- 60 files changed, 9913 insertions(+), 1844 deletions(-) create mode 100644 dashboard/src/features/agent/AgentMetricsPanel.tsx create mode 100644 dashboard/src/features/agent/AskUserQuestionCard.tsx create mode 100644 dashboard/src/features/agent/AuditLogViewer.tsx create mode 100644 dashboard/src/features/agent/PermissionRequestCard.tsx delete mode 100644 docs/agent-optimization-analysis.md create mode 100644 docs/architecture/agent/context.md create mode 100644 docs/architecture/agent/env-vars.md create mode 100644 docs/architecture/agent/hooks.md create mode 100644 docs/architecture/agent/memory.md create mode 100644 docs/architecture/agent/overview.md create mode 100644 docs/architecture/agent/permission.md create mode 100644 docs/architecture/agent/skills.md create mode 100644 docs/architecture/agent/subagent.md create mode 100644 docs/architecture/agent/system-prompt.md create mode 100644 docs/architecture/agent/tasks.md create mode 100644 docs/architecture/agent/team.md create mode 100644 docs/architecture/agent/tools.md create mode 100644 docs/architecture/core-modules.md create mode 100644 docs/architecture/core-workflows.md create mode 100644 docs/architecture/overview.md create mode 100644 docs/development.md delete mode 160000 libs/obscura create mode 100644 profiles/readonly.toml create mode 100644 profiles/research.toml create mode 100644 src/agent/runtime/denial_tracker.rs create mode 100644 src/agent/runtime/permission_explainer.rs create mode 100644 src/agent/runtime/permission_profile.rs create mode 100644 src/api/permissions.rs diff --git a/.env.example b/.env.example index e8fd832..f18b3ef 100644 --- a/.env.example +++ b/.env.example @@ -1,37 +1,131 @@ -# AstroResearch Configuration Template -# Copy this to .env and fill in your details +# ============================================================================= +# AstroResearch 环境变量模板 +# 复制此文件为 .env 并填入你的配置值 +# ============================================================================= -# NASA ADS API Key (Get from ui.adsabs.harvard.edu) +# ───────────────────────────────────────────────────────────────────────────── +# 1. 核心 API 服务 +# ───────────────────────────────────────────────────────────────────────────── + +# NASA ADS API Token(从 ui.adsabs.harvard.edu 申请) ADS_API_KEY=your_ads_api_key_here -# LLM Translation Provider Settings (OpenAI-compatible endpoints) +# 大语言模型配置(兼容 OpenAI 接口协议) LLM_API_KEY=your_llm_api_key_here -LLM_API_BASE=https://api.deepseek.com/v1 -# Examples: deepseek-chat, gpt-4o-mini, gemini-1.5-flash -LLM_MODEL=deepseek-chat +LLM_API_BASE=https://api.openai.com/v1 +# 可选模型示例:deepseek-chat, qwen-flash, gpt-4o-mini +LLM_MODEL=gpt-4o-mini -# Embedding Model Settings (OpenAI-compatible endpoints, falls back to LLM keys if unspecified) -EMBEDDING_API_KEY=your_embedding_api_key_here -EMBEDDING_API_BASE=https://api.openai.com/v1 -EMBEDDING_MODEL=text-embedding-3-small +# 备用模型(连续 3 次 529 过载后自动切换,可选) +# FALLBACK_MODEL=gpt-4o-mini + +# 向量嵌入模型配置(未设置时默认回退到 LLM 的 API Key 和 Base) +# EMBEDDING_API_KEY=your_embedding_api_key_here +# EMBEDDING_API_BASE=https://api.openai.com/v1 +# EMBEDDING_MODEL=text-embedding-3-small +# 向量维度(需与所选模型输出维度一致,默认 1536) +# EMBEDDING_DIM=1536 + +# ───────────────────────────────────────────────────────────────────────────── +# 2. 七牛云对象存储(PDF 解析后的配图托管) +# ───────────────────────────────────────────────────────────────────────────── -# Qiniu Cloud Storage Config (For hosting PDF-extracted layout images) QINIU_AK=your_qiniu_access_key_here QINIU_SK=your_qiniu_secret_key_here QINIU_BUCKET=your_bucket_name QINIU_DOMAIN=http://your_cdn_domain.com -# MinerU PDF Layout Extractor Remote API (If not using HTML) -MINERU_API_URL=http://mineru.remote-api.com/api/v1/extract -MINERU_API_KEY=your_mineru_api_key +# ───────────────────────────────────────────────────────────────────────────── +# 3. 文档解析服务 +# ───────────────────────────────────────────────────────────────────────────── -# Local Data Paths +# MinerU PDF 解析远程 API(不使用 HTML 解析时可选) +# MINERU_API_URL=https://mineru.net/api/v4/extract/task +# MINERU_API_KEY=your_mineru_api_key_here + +# ───────────────────────────────────────────────────────────────────────────── +# 4. 本地路径与端口 +# ───────────────────────────────────────────────────────────────────────────── + +# SQLite 数据库路径 +DATABASE_URL=sqlite://library/astro_research.db +# 本地文献馆藏根目录 LIBRARY_DIR=./library +# Agent Skills 目录(Markdown 知识模块) +SKILLS_DIR=./skills +# 后端服务监听端口 PORT=8000 -DATABASE_URL=sqlite://astro_research.db -# Logging Configuration (Pretty console and rolling file logging) +# ───────────────────────────────────────────────────────────────────────────── +# 5. 日志配置 +# ───────────────────────────────────────────────────────────────────────────── + +# 日志级别:trace / debug / info / warn / error(可指定模块级别,如 info,astroresearch=debug) LOG_LEVEL=info,astroresearch=debug +# 日志格式:pretty(彩色)或 json(结构化) LOG_FORMAT=pretty +# 日志输出目标:stdout(控制台)、file(文件),逗号分隔可同时启用 LOG_OUTPUTS=stdout,file +# 日志文件目录(仅 LOG_OUTPUTS 含 file 时生效) LOG_DIR=./logs + +# ───────────────────────────────────────────────────────────────────────────── +# 6. Agent 运行时参数 +# ───────────────────────────────────────────────────────────────────────────── + +# 最大 ReAct 推理步数(默认 8) +# AGENT_MAX_STEPS=8 +# 单个工具执行超时秒数(默认 120) +# AGENT_TOOL_TIMEOUT_SECS=120 +# 工具输出截断字符数(默认 4000) +# AGENT_MAX_TOOL_OUTPUT_CHARS=4000 +# 上下文触发压缩的字符阈值(默认 16000) +# AGENT_CONTEXT_CHAR_LIMIT=16000 +# Token 软限制:触发渐进式提醒(默认 32000) +# AGENT_TOKEN_SOFT_LIMIT=32000 +# Token 硬限制:触发强制压缩或终止(默认 40000) +# AGENT_TOKEN_HARD_LIMIT=40000 +# 触发 snip 压缩的最大消息数(默认 50) +# AGENT_MAX_MESSAGES=50 + +# ── 权限系统 ── +# Agent 工具权限规则(逗号分隔,支持内容级匹配 "ToolName(pattern)") +# 规则优先级: Deny > Ask > Allow(先匹配先生效) +# +# 格式: "tool_name" → 工具级规则 +# "tool_name(content_pattern)" → 内容级规则 +# 示例: "run_bash(rm *)" 匹配以 rm 开头的 bash 命令 +# "read_file(*.env)" 匹配 .env 后缀的文件读取 +# "file_write(/etc/*)" 匹配 /etc/ 下的文件写入 +# +# 权限模式: +# default — 标准:逐项按规则决定(默认) +# accept_edits — 工作目录内的 file_write/file_edit 自动允许 +# bypass — 跳过所有 Ask 检查(Deny 规则仍生效) +# dont_ask — 将所有 Ask 转为 Deny +# AGENT_PERMISSION_MODE=default +# +# 拒绝规则(逗号分隔,不可覆盖) +# AGENT_PERMISSIONS_DENY=run_bash(rm *),run_bash(sudo *) +# 允许规则(逗号分隔,显式允许) +# AGENT_PERMISSIONS_ALLOW=read_file,grep_files,glob_files,search_papers +# 询问规则(逗号分隔,需用户确认) +# AGENT_PERMISSIONS_ASK=run_bash,file_write,file_edit,download_paper + +# ───────────────────────────────────────────────────────────────────────────── +# 7. 自动记忆提取(实验性功能) +# ───────────────────────────────────────────────────────────────────────────── + +# 是否启用自动记忆提取(默认 false) +# EXTRACT_MEMORY_ENABLED=false +# 记忆提取节流间隔:每 N 个 turn 触发一次(默认 3) +# EXTRACT_MEMORY_THROTTLE_TURNS=3 +# 记忆提取子代理最大步数(默认 3) +# EXTRACT_MEMORY_MAX_STEPS=3 + +# ───────────────────────────────────────────────────────────────────────────── +# 8. 其他 +# ───────────────────────────────────────────────────────────────────────────── + +# Obscura 浏览器 Profile 轮换(绕过 Cloudflare/WAF,默认 true) +# OBSCURA_ROTATE_PROFILE=true diff --git a/CLAUDE.md b/CLAUDE.md index aff6e9c..88fa44f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,14 +68,19 @@ src/ │ ├── targets.rs # Target query/associate/extract, RAG chat, figure chat │ └── helpers.rs # Shared DB helpers, format conversion, path validation ├── agent/ # ReAct-based research agent (LLM-driven tool-use loop) -│ ├── tools/ # AgentTool trait, ToolRegistry, tool implementations per domain file -│ ├── runtime/ # ReAct loop engine, streaming, session management, context building -│ ├── compact/ # Context compression (micro/auto/manual layers) +│ ├── tools/ # AgentTool trait, ToolRegistry, 25+ tool implementations per domain file +│ ├── runtime/ # ReAct loop, streaming, session/context, token budget, error recovery, +│ │ # permission checker, file cache, system prompt assembly, circuit breaker +│ ├── compact/ # Context compression (micro/auto/manual layers + collapse) +│ ├── memory/ # Persistent memory manager: extraction, dedup, decay, age, guardrails │ ├── hooks.rs # Lifecycle events (PreToolUse/PostToolUse/Stop/etc.) -│ ├── skills.rs # SkillRegistry: loads skill SKILL.md files from skills/ directory -│ ├── subagent.rs # Context-isolated sub-agent runner for delegate_research +│ ├── skills.rs # SkillRegistry: hot-loads SKILL.md files from skills/ directory +│ ├── subagent.rs # Context-isolated sub-agent runner (subagent tool) +│ ├── team/ # Multi-agent team: file-based inbox, lead/teammate coordination │ ├── background.rs# BgNotificationQueue for async slow-task (download/parse) notifications -│ └── team/ # Multi-agent team: file-based inbox, lead/teammate coordination +│ ├── task_board.rs# Persistent task board (agent_tasks table) +│ ├── trajectory.rs# Session trajectory recording for audit/debug +│ └── terminal.rs # Escape sequence filter for ANSI-heavy tool outputs ├── clients/ # External API wrappers │ ├── llm.rs # LlmClient (OpenAI-compatible chat + streaming), EmbeddingClient │ ├── ads.rs # NASA ADS API @@ -106,6 +111,7 @@ All handlers access state via `Arc`. Key fields: - `llm: LlmClient` / `embedding: EmbeddingClient` — OpenAI-compatible LLM clients - `ads: AdsClient` / `arxiv: ArxivClient` — academic search clients - `skill_registry: Arc>` — hot-reloaded agent skills +- `memory_manager: Arc` — persistent agent memory (MEMORY.md + decay) - `cancelled_runs: Arc>>` — agent cancellation tokens - `harvest_status` / `batch_status` — async batch operation status tracking @@ -114,14 +120,17 @@ All handlers access state via `Arc`. Key fields: The agent (`src/agent/`) implements a **ReAct** (Thought → Action → Observation) loop: 1. **`AgentRuntime`** (`runtime/mod.rs`) orchestrates the loop: session create/resume → context build → ReAct loop → finalize -2. **Streaming**: LLM response is streamed via SSE (`AgentStreamEvent`) — thought, tool_call, tool_result, text_delta, usage, error, done -3. **Tools**: Each tool implements `AgentTool` trait (name, description, JSON Schema parameters, execute). 19 tools in default registry including read_file, grep_files, glob_files, run_bash, file_write, file_edit, search_papers, download_paper, parse_paper, get_paper_content, rag_search, query_target, save_note, todo_write, compress_context, load_skill, delegate_research, plus optional background and team tools -4. **Parallel execution**: Same-turn tool calls execute concurrently via `executor::execute_parallel` -5. **Context compression**: Three layers — micro (placeholder replacement), auto (LLM summarization when over threshold), manual (compress_context tool). Protected by `CompactionCircuitBreaker` -6. **Skills** (`skills.rs`): Two-layer loading — system-reminder lists names (~20 tokens each), LLM calls `load_skill` to inject full SKILL.md content -7. **Sub-agents** (`subagent.rs`): `delegate_research` spawns a context-isolated sub-agent with its own ReAct loop, returning only the final summary -8. **Teams** (`team/`): File-based inbox directory per session for lead/teammate message passing -9. **Background tasks** (`background.rs`): Slow ops (download, parse) can run async; results inject via `BgNotificationQueue` before next LLM call +2. **Streaming**: LLM response is streamed via SSE (`AgentStreamEvent`) — thought, tool_call (with `id`), tool_result (with `tool_call_id`), text_delta, usage, error, done. Tool calls execute in parallel. +3. **Tools**: Each tool implements `AgentTool` trait (name, description, JSON Schema parameters, execute). Core tools: read_file, grep_files, glob_files, run_bash, file_write, file_edit, search_papers, download_paper, parse_paper, get_paper_content, rag_search, query_target, save_note, todo_write, compress_context, load_skill, subagent, ask_user, save_memory. Plus background tools (bg_task_run, bg_task_check) and team tools (spawn_teammate, send_teammate_message, team_broadcast, check_team_inbox). +4. **Thinking mode**: `enable_thinking` flag propagates from `AgentChatRequest` → `AgentConfig` → `ToolContext` → `LlmClient::chat_stream`. Only enabled for Qwen/DashScope backends; frontend-controlled via the `thinking` request field. +5. **Tool call ID tracking**: LLM may not return tool_call IDs — `LlmClient` generates UUID fallbacks. `ToolCall` and `ToolResult` SSE events carry matching IDs for precise frontend pairing. +6. **ToolContext** (`tools/mod.rs`): Injected into every tool execution — holds `app_state`, `session_id`, `sse_tx` (for intermediate events), `enable_thinking`, `read_file_state` (file cache for dedup), `silent` (sub-agents skip permission prompts). +7. **Context compression**: Four layers — micro (placeholder replacement), snip (old-message truncation), auto (LLM summarization), aggro_micro (aggressive placeholder). Protected by `CompactionCircuitBreaker`. Transcripts persisted in `agent_messages` table, not filesystem snapshots. +8. **Skills** (`skills.rs`): Two-layer loading — system-reminder lists names (~20 tokens each), LLM calls `load_skill` to inject full SKILL.md content +9. **Sub-agents** (`subagent.rs`): `subagent` tool spawns a context-isolated sub-agent with its own ReAct loop. Sub-agent messages (system/user/assistant/tool) are persisted to `agent_messages` with `agent_name` identifier. Returns final summary + activity log. SSE progress forwarded to parent via ToolContext. +10. **Memory** (`memory/`): File-based persistent memory (MEMORY.md). `MemoryManager` handles extraction from conversation, dedup, recency decay, age-based pruning, and guardrails. Tools: `save_memory`, `load_memory` (auto-injected in system prompt). +11. **Teams** (`team/`): File-based inbox directory per session for lead/teammate message passing +12. **Background tasks** (`background.rs`): Slow ops (download, parse) can run async; results inject via `BgNotificationQueue` before next LLM call Environment variables for agent tuning: `AGENT_MAX_STEPS` (default 8), `AGENT_TOOL_TIMEOUT_SECS` (default 120), `AGENT_MAX_TOOL_OUTPUT_CHARS` (default 4000), `AGENT_CONTEXT_CHAR_LIMIT` (default 16000), `AGENT_TOKEN_SOFT_LIMIT` / `AGENT_TOKEN_HARD_LIMIT`. @@ -140,7 +149,7 @@ React 19 + TypeScript + Vite + Tailwind CSS 4. Features are organized by domain: - `features/reader/` — Bilingual reader with highlight annotations (KaTeX for math) - `features/citation/` — Canvas-based force-directed citation graph - `features/sync/` — Batch sync control panel -- `features/agent/` — Agent chat interface (SSE event consumption) +- `features/agent/` — Agent chat: ResearchAgentPanel (timeline view with thought/tool_call/answer/subagent), AgentMetricsPanel (tool stats), AskUserQuestionCard (interactive Q&A), AuditLogViewer - `features/settings/` — System configuration Dependencies: `react-markdown` + `rehype-katex` + `remark-math` for Markdown/LaTeX rendering, `framer-motion` for animations, `lucide-react` for icons. diff --git a/Cargo.toml b/Cargo.toml index ff7f866..bd74e80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,8 @@ flate2 = "1.1.9" zip = "8.6.0" uuid = { version = "1.23.2", features = ["v4"] } tracing-appender = "0.2.5" -obscura-browser = { path = "/home/fmq/program/AstroResearch/libs/obscura/crates/obscura-browser", optional = true } -obscura-net = { path = "/home/fmq/program/AstroResearch/libs/obscura/crates/obscura-net", optional = true } +obscura-browser = { path = "libs/obscura/crates/obscura-browser", optional = true } +obscura-net = { path = "libs/obscura/crates/obscura-net", optional = true } libsqlite3-sys = { version = "0.27.0", features = ["bundled"] } sqlite-vec = "0.1.9" clap = { version = "4", features = ["derive"] } diff --git a/README.md b/README.md index f0196e4..e2515a1 100644 --- a/README.md +++ b/README.md @@ -85,15 +85,14 @@ cp .env.example .env ## 3. 技术文档结构 (Documentation Directory) -详细的技术和部署设计文档已集中整理至 `docs/` 目录: - -- 🏗️ **[架构设计](docs/architecture.md)**:包含系统宏观流程图与序列图。 -- 🌐 **[API 接口规范](docs/api.md)**:后端 Axum 路由及 HTTP 接口格式。 -- 🗄️ **[数据库设计](docs/database.md)**:SQLite 表结构、ER 图与索引优化。 -- 🎨 **[视觉与交互设计](docs/design.md)**:高对比度浅色中文控制台、自研 Canvas 图谱引擎说明。 -- 🛠️ **[排障指南](docs/troubleshooting.md)**:人机校验、解析失败等常见问题解法。 -- 🚀 **[编译与部署指南](docs/deployment.md)**:单执行文件打包与发布流程。 -- 🤝 **[参与贡献指南](docs/contributing.md)**:开发规范及单元测试。 +- 🏗️ **[架构设计](docs/architecture.md)** — 系统宏观架构、Agent 子系统、Mermaid 流程图 +- 🌐 **[API 接口规范](docs/api.md)** — 全部 Axum REST 端点与 SSE 事件 +- 🗄️ **[数据库设计](docs/database.md)** — SQLite 表结构、ER 图、迁移历史 +- 🛠️ **[开发指南](docs/development.md)** — 构建/测试/环境变量/项目结构 +- 🎨 **[视觉设计](docs/design.md)** — UI 设计系统与交互体验 +- 🚀 **[部署指南](docs/deployment.md)** — 生产构建与发布 +- 🔧 **[排障指南](docs/troubleshooting.md)** — 常见问题与解决方案 +- 🤝 **[参与贡献](docs/contributing.md)** — 代码规范与测试要求 --- @@ -102,50 +101,38 @@ cp .env.example .env ``` AstroResearch/ ├── src/ -│ ├── main.rs # Axum 服务入口:路由注册、中间件、静态资源托管 -│ ├── lib.rs # 库入口:Config 配置结构体与环境变量加载 -│ ├── api/ # API 层(模块化拆分) -│ │ ├── mod.rs # AppState / StandardPaper 定义 + handlers 兼容命名空间 -│ │ ├── helpers.rs # 共享工具函数:格式转换、数据库读写、路径校验 -│ │ ├── papers.rs # 文献相关:检索、下载、上传、解析、翻译、引文、导出 -│ │ ├── notes.rs # 笔记 CRUD:创建、查询、删除 -│ │ └── sync.rs # 批量同步:元数据同步、资源同步、查询管理 -│ ├── bin/ -│ │ └── health_check.rs # 独立二进制:馆藏健康度诊断与修复工具 -│ ├── clients/ -│ │ ├── ads.rs # NASA ADS API 客户端 -│ │ ├── arxiv.rs # arXiv Atom XML API 客户端 -│ │ └── qiniu.rs # 七牛云对象存储客户端 -│ └── services/ -│ ├── batch/ # 批量同步引擎(模块化拆分) -│ │ ├── mod.rs # 公共导出 -│ │ ├── meta.rs # 元数据大批量采集 (MetaSync) -│ │ └── asset.rs # 物理资源批量处理 (AssetSync) -│ ├── download.rs # 文献下载器:反爬伪装、多级回退、错误记录 -│ ├── parser.rs # HTML/PDF → Markdown 解析器 -│ ├── translation.rs # LLM 翻译器 + Trie 词典 -│ ├── query_parser.rs # 高级检索语法解析 -│ └── logging.rs # 日志系统:控制台美化 + 滚动文件 -├── dashboard/ -│ └── src/ -│ ├── App.tsx # 全局状态管理与布局 -│ ├── types.ts # TypeScript 类型定义 -│ ├── components/ -│ │ ├── CitationGalaxyCanvas.tsx # Canvas 力导向引文星系图 -│ │ └── CustomSelect.tsx # 可复用下拉选择组件 -│ └── features/ -│ ├── search/SearchPanel.tsx # 跨源检索面板 -│ ├── library/LibraryPanel.tsx # 馆藏管理面板 -│ ├── reader/ReaderPanel.tsx # 双语对照阅读器 -│ ├── citation/CitationPanel.tsx # 引文图谱面板 -│ ├── sync/SyncPanel.tsx # 批量同步控制台 -│ └── settings/SettingsPanel.tsx # 系统设置 -├── migrations/ # SQLite 数据库迁移脚本 -├── library/ # 本地文献物理存储目录 -│ ├── PDF/ # 下载的 PDF 文件 -│ ├── HTML/ # 下载的 HTML 文件 -│ ├── Markdown/ # 解析后的 Markdown 文件 -│ └── Translation/ # 翻译后的中文 Markdown 文件 -├── docs/ # 技术文档 +│ ├── main.rs # Axum 服务入口:路由、中间件、静态资源托管 +│ ├── lib.rs # Config 配置加载 +│ ├── api/ # HTTP handlers + AppState +│ │ ├── agent.rs # SSE 智能体对话、会话管理、指标、审计 +│ │ ├── papers.rs # 文献检索/下载/解析/翻译/引文/导出 +│ │ ├── notes.rs # 笔记 CRUD +│ │ ├── sync.rs # 批量同步 +│ │ ├── targets.rs # 天体目标识别 +│ │ └── helpers.rs # 共享工具函数 +│ ├── agent/ # ReAct 智能体引擎 (参考 Claude Code 分层设计) +│ │ ├── runtime/ # ReAct 循环、流式执行、Token 管理、权限、熔断 +│ │ ├── tools/ # 工具系统 (filesystem/ astro/ memory/ team/) +│ │ ├── compact/ # 三层上下文压缩 +│ │ ├── memory/ # 项目记忆管理 (提取/去重/衰减/保活/护栏) +│ │ ├── team/ # 多 Agent 团队协作 +│ │ ├── hooks.rs # 生命周期事件系统 +│ │ ├── skills.rs # 技能注册表 (热加载) +│ │ ├── subagent.rs # 上下文隔离子代理 +│ │ └── terminal.rs # 循环终止信号 +│ ├── clients/ # 外部 API 客户端 (ADS, arXiv, LLM, Qiniu) +│ ├── services/ # 业务逻辑 +│ │ ├── parser/ # HTML/PDF → Markdown 解析器 (A&A, IOP, ar5iv, MinerU) +│ │ ├── batch/ # 批量同步引擎 +│ │ ├── download.rs # 文献下载器 (反爬伪装、多级回退) +│ │ ├── translation.rs # LLM 翻译 + Trie 天文词典 +│ │ └── rag.rs # 向量检索增强生成 +│ └── bin/ # 独立二进制 (cli, health_check, reparse) +├── dashboard/ # React 19 + Vite + TypeScript 前端 +│ └── src/features/ # search/ library/ reader/ citation/ agent/ sync/ settings/ +├── skills/ # Agent Skills (Markdown 知识模块) +├── migrations/ # SQLite 迁移脚本 +├── library/ # 本地文献存储 (PDF/HTML/Markdown/Translation) +├── docs/ # 技术文档 (架构/API/数据库/开发/部署/排障) └── dictionary.txt # 天文学双语名词词典 ``` diff --git a/dashboard/src/features/agent/AgentMetricsPanel.tsx b/dashboard/src/features/agent/AgentMetricsPanel.tsx new file mode 100644 index 0000000..7bf7cce --- /dev/null +++ b/dashboard/src/features/agent/AgentMetricsPanel.tsx @@ -0,0 +1,271 @@ +// dashboard/src/features/agent/AgentMetricsPanel.tsx +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { BarChart3, Activity, AlertTriangle, Zap, Brain, RefreshCw, Loader } from 'lucide-react'; +import type { AgentMetricsResponse } from '../../types'; + +interface AgentMetricsPanelProps { + showAlert?: (message: string, title?: string) => void; +} + +// 工具名到中文显示名的映射 +const TOOL_LABELS: Record = { + read_file: '读取文件', + grep_files: '搜索文件', + glob_files: '匹配文件', + run_bash: 'Shell 命令', + file_write: '写入文件', + file_edit: '编辑文件', + search_papers: '文献检索', + get_paper_metadata: '获取元数据', + download_paper: '下载文献', + parse_paper: '解析文献', + get_paper_content: '获取内容', + rag_search: 'RAG 检索', + query_target: '天体查询', + save_note: '保存笔记', + todo_write: '任务管理', + compress_context: '上下文压缩', + load_skill: '加载技能', + subagent: '派发子代理', + delegate_research: '子代理研究(旧)', + ask_user: '用户提问', + save_memory: '保存记忆', + load_memory: '读取记忆', + bg_task_run: '后台任务', + bg_task_check: '检查后台', + spawn_teammate: '创建队友', + send_teammate_message: '队友消息', + team_broadcast: '团队广播', + check_team_inbox: '收件箱检查', +}; + +// 工具调用的分类色 +const CATEGORY_COLORS: Record = { + read_file: 'bg-blue-100 text-blue-700 border-blue-200', + grep_files: 'bg-blue-100 text-blue-700 border-blue-200', + glob_files: 'bg-blue-100 text-blue-700 border-blue-200', + run_bash: 'bg-slate-200 text-slate-700 border-slate-300', + file_write: 'bg-blue-100 text-blue-700 border-blue-200', + file_edit: 'bg-blue-100 text-blue-700 border-blue-200', + search_papers: 'bg-emerald-100 text-emerald-700 border-emerald-200', + get_paper_metadata: 'bg-emerald-100 text-emerald-700 border-emerald-200', + download_paper: 'bg-emerald-100 text-emerald-700 border-emerald-200', + parse_paper: 'bg-emerald-100 text-emerald-700 border-emerald-200', + get_paper_content: 'bg-emerald-100 text-emerald-700 border-emerald-200', + rag_search: 'bg-violet-100 text-violet-700 border-violet-200', + query_target: 'bg-amber-100 text-amber-700 border-amber-200', + save_note: 'bg-teal-100 text-teal-700 border-teal-200', + todo_write: 'bg-orange-100 text-orange-700 border-orange-200', + compress_context: 'bg-rose-100 text-rose-700 border-rose-200', + load_skill: 'bg-indigo-100 text-indigo-700 border-indigo-200', + subagent: 'bg-purple-100 text-purple-700 border-purple-200', + delegate_research: 'bg-purple-100 text-purple-700 border-purple-200', + ask_user: 'bg-amber-100 text-amber-700 border-amber-200', + save_memory: 'bg-pink-100 text-pink-700 border-pink-200', + load_memory: 'bg-pink-100 text-pink-700 border-pink-200', + bg_task_run: 'bg-cyan-100 text-cyan-700 border-cyan-200', + bg_task_check: 'bg-cyan-100 text-cyan-700 border-cyan-200', + spawn_teammate: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200', + send_teammate_message: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200', + team_broadcast: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200', + check_team_inbox: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200', +}; + +export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) { + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(false); + + const fetchMetrics = async () => { + setLoading(true); + try { + const res = await axios.get('/api/chat/metrics'); + setMetrics(res.data); + } catch (e) { + console.error('获取智能体指标失败:', e); + showAlert?.('获取智能体运行指标失败,请确认后端服务状态。', '指标加载出错'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMetrics(); + }, []); + + // 提取工具调用排行(取前10) + const toolBreakdown = metrics?.tool_call_breakdown + ? Object.entries(metrics.tool_call_breakdown) + .sort(([, a], [, b]) => b - a) + .slice(0, 15) + : []; + + const maxToolCalls = toolBreakdown.length > 0 ? toolBreakdown[0][1] : 1; + + return ( +
+ {/* 头部 */} +
+
+ +

+ 智能体运行指标 +

+
+ +
+ + {loading && !metrics ? ( +
+ + 加载运行指标中... +
+ ) : metrics ? ( + <> + {/* 概览卡片 */} +
+ } + label="总会话数" + value={metrics.total_sessions} + color="sky" + /> + } + label="总工具调用" + value={metrics.total_tool_calls} + color="emerald" + /> + } + label="平均步数/会话" + value={metrics.avg_steps_per_session.toFixed(1)} + color="violet" + /> + } + label="错误率" + value={`${(metrics.error_rate * 100).toFixed(1)}%`} + color={metrics.error_rate > 0.1 ? 'rose' : 'emerald'} + /> +
+ + {/* 工具调用分布 */} +
+ + 工具调用排行 + + {toolBreakdown.length === 0 ? ( +

+ 暂无工具调用记录 +

+ ) : ( +
+ {toolBreakdown.map(([name, count]) => { + const barWidth = Math.max((count / maxToolCalls) * 100, 2); + const colorClass = CATEGORY_COLORS[name] || 'bg-slate-100 text-slate-700 border-slate-200'; + const label = TOOL_LABELS[name] || name; + return ( +
+ + {label} + +
+
+
+ + {count} + +
+ ); + })} +
+ )} +
+ + {/* 工具分类统计 */} +
+ + 按功能域分布 + +
+ {Object.entries(getCategoryCounts(metrics.tool_call_breakdown)).map(([category, count]) => ( + + {category}: {count} + + ))} +
+
+ + ) : null} +
+ ); +} + +// 小指标卡片 +function MetricCard({ + icon, + label, + value, + color, +}: { + icon: React.ReactNode; + label: string; + value: string | number; + color: string; +}) { + const colorMap: Record = { + sky: 'border-sky-200 bg-sky-50 text-sky-700', + emerald: 'border-emerald-200 bg-emerald-50 text-emerald-700', + violet: 'border-violet-200 bg-violet-50 text-violet-700', + rose: 'border-rose-200 bg-rose-50 text-rose-700', + amber: 'border-amber-200 bg-amber-50 text-amber-700', + }; + + return ( +
+
+ {icon} + {label} +
+
+ {value} +
+
+ ); +} + +// 按功能域分组统计 +function getCategoryCounts(breakdown: Record): Record { + const categories: Record = { + '文件系统': ['read_file', 'grep_files', 'glob_files', 'run_bash', 'file_write', 'file_edit'], + '文献科研': ['search_papers', 'get_paper_metadata', 'download_paper', 'parse_paper', 'get_paper_content'], + 'RAG/天体': ['rag_search', 'query_target', 'save_note'], + 'Agent控制': ['todo_write', 'compress_context', 'load_skill', 'subagent', 'delegate_research', 'ask_user'], + '记忆系统': ['save_memory', 'load_memory'], + '后台任务': ['bg_task_run', 'bg_task_check'], + '团队协作': ['spawn_teammate', 'send_teammate_message', 'team_broadcast', 'check_team_inbox'], + }; + + const result: Record = {}; + for (const [cat, tools] of Object.entries(categories)) { + const total = tools.reduce((sum, t) => sum + (breakdown[t] || 0), 0); + if (total > 0) { + result[cat] = total; + } + } + return result; +} diff --git a/dashboard/src/features/agent/AskUserQuestionCard.tsx b/dashboard/src/features/agent/AskUserQuestionCard.tsx new file mode 100644 index 0000000..94ad37a --- /dev/null +++ b/dashboard/src/features/agent/AskUserQuestionCard.tsx @@ -0,0 +1,293 @@ +// dashboard/src/features/agent/AskUserQuestionCard.tsx +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { MessageCircle, Send, X, Loader, CheckSquare, Square } from 'lucide-react'; +import type { PendingQuestion } from '../../types'; + +interface AskUserQuestionCardProps { + onAnswered?: () => void; +} + +export function AskUserQuestionCard({ onAnswered }: AskUserQuestionCardProps) { + const [pendingQuestions, setPendingQuestions] = useState([]); + const [answers, setAnswers] = useState>({}); + const [freeText, setFreeText] = useState>({}); + const [submitting, setSubmitting] = useState>({}); + const [expanded, setExpanded] = useState>({}); + const [error, setError] = useState>({}); + + // 轮询待回答问题 + useEffect(() => { + let cancelled = false; + + const poll = async () => { + try { + const res = await axios.get('/api/chat/questions'); + if (!cancelled) { + // 安全解析:确保返回的是数组 + const data = Array.isArray(res.data) ? res.data : []; + setPendingQuestions(data); + // 自动展开新问题 + setExpanded(prev => { + const next = { ...prev }; + for (const q of data) { + if (q && q.question_id && !(q.question_id in next)) { + next[q.question_id] = true; + } + } + return next; + }); + } + } catch (e) { + console.error('获取待回答问题失败:', e); + } + }; + + poll(); + const interval = setInterval(poll, 3000); // 每3秒轮询 + return () => { + cancelled = true; + clearInterval(interval); + }; + }, []); + + // 切换选项:使用 option.label 而非整个对象 + const toggleOption = (questionId: string, optionLabel: string, multiSelect: boolean) => { + setAnswers(prev => { + const current = prev[questionId] || []; + if (multiSelect) { + return { + ...prev, + [questionId]: current.includes(optionLabel) + ? current.filter(o => o !== optionLabel) + : [...current, optionLabel], + }; + } else { + return { ...prev, [questionId]: [optionLabel] }; + } + }); + }; + + const handleSubmit = async (questionId: string) => { + setSubmitting(prev => ({ ...prev, [questionId]: true })); + setError(prev => ({ ...prev, [questionId]: null })); + try { + await axios.post('/api/chat/answer', { + question_id: questionId, + answers: answers[questionId] || [], + free_text: freeText[questionId] || null, + }); + // 移除已回答的问题 + setPendingQuestions(prev => prev.filter(q => q.question_id !== questionId)); + // 清理状态 + setAnswers(prev => { + const next = { ...prev }; + delete next[questionId]; + return next; + }); + setFreeText(prev => { + const next = { ...prev }; + delete next[questionId]; + return next; + }); + setError(prev => { + const next = { ...prev }; + delete next[questionId]; + return next; + }); + onAnswered?.(); + } catch (e: any) { + console.error('提交答案失败:', e); + const msg = e.response?.status === 410 + ? '该问题已超时或已被回答' + : e.response?.status === 404 + ? '未找到该问题' + : '提交失败,请稍后重试'; + setError(prev => ({ ...prev, [questionId]: msg })); + } finally { + setSubmitting(prev => ({ ...prev, [questionId]: false })); + } + }; + + const dismissQuestion = (questionId: string) => { + setPendingQuestions(prev => prev.filter(q => q.question_id !== questionId)); + setExpanded(prev => ({ ...prev, [questionId]: false })); + }; + + if (pendingQuestions.length === 0) return null; + + return ( +
+ {pendingQuestions.map(q => { + // 防御:确保必填字段存在 + if (!q || !q.question_id) return null; + + const isExpanded = expanded[q.question_id] !== false; + const isSubmitting = submitting[q.question_id] || false; + const qError = error[q.question_id] || null; + const options = Array.isArray(q.options) ? q.options : []; + const multiSelect = q.multi_select === true; + + return ( +
+ {/* 头部 — 显示 header 标签和问题摘要 */} + +
+ + + {/* 内容 */} + {isExpanded && ( +
+ {/* 问题文本 */} +
+ + 问题 + +

+ {q.question || ''} +

+
+ + {/* 选项列表 — 每个选项是 {label, description} 对象 */} + {options.length > 0 && ( +
+ + {multiSelect ? '可多选' : '请选择一项'} + +
+ {options.map((option, idx) => { + // 防御:确保 option 是有效对象 + const label = typeof option?.label === 'string' ? option.label : String(option); + const desc = typeof option?.description === 'string' ? option.description : ''; + const selected = (answers[q.question_id] || []).includes(label); + + return ( + + ); + })} +
+
+ )} + + {/* 自由文本 */} +
+ + 补充说明(可选) + +