From 85b6429c30a67ce50f00e9cefabc6da87d4fd634 Mon Sep 17 00:00:00 2001 From: Asfmq <2696428814@qq.com> Date: Wed, 24 Jun 2026 19:52:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Agent=20=E5=A4=9A=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E3=80=81=E8=A7=86=E8=A7=89=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=9B=86=E6=88=90=E3=80=81LLM=20=E8=83=BD=E5=8A=9B=E5=88=86?= =?UTF-8?q?=E5=B1=82=E4=B8=8E=20P3=20=E6=80=A7=E8=83=BD=E6=94=B6=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心架构变更: 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() 辅助方法 --- .env.example | 22 +- .gitignore | 1 + CLAUDE.md | 75 +++- .../src/features/agent/ResearchAgentPanel.tsx | 408 ++++++++++++++---- .../src/features/reader/AIAssistantPanel.tsx | 156 ++++--- dashboard/src/types.ts | 1 + .../20260624000000_add_session_mode.sql | 11 + src/agent/compact.rs | 47 +- src/agent/coordinator/agent.rs | 354 --------------- src/agent/coordinator/mod.rs | 93 ---- src/agent/coordinator/tools.rs | 340 --------------- src/agent/coordinator/worker.rs | 159 ------- src/agent/memory/extraction.rs | 3 +- src/agent/mod.rs | 2 +- src/agent/modes/deep_research.rs | 50 +++ src/agent/modes/default.rs | 28 ++ src/agent/modes/literature_reader.rs | 70 +++ src/agent/modes/mod.rs | 310 +++++++++++++ src/agent/runtime/executor.rs | 23 +- src/agent/runtime/mod.rs | 287 +++++++++--- src/agent/runtime/session.rs | 95 ++-- src/agent/runtime/streaming.rs | 2 + src/agent/runtime/streaming_executor.rs | 1 + src/agent/skills/curator.rs | 4 +- src/agent/subagent.rs | 19 +- src/agent/tools/astro/analyze_image.rs | 340 +++++++++++++++ src/agent/tools/astro/mod.rs | 2 + src/agent/tools/filesystem/read.rs | 10 +- src/agent/tools/mod.rs | 36 ++ src/agent/tools/persist.rs | 13 +- src/api/agent.rs | 190 +++++++- src/api/auth.rs | 22 +- src/api/helpers.rs | 14 +- src/api/mod.rs | 24 +- src/api/papers.rs | 2 +- src/api/targets.rs | 122 ------ src/clients/llm.rs | 185 ++++---- src/lib.rs | 116 ++++- src/main.rs | 73 +++- src/services/batch/asset.rs | 9 +- src/services/batch/meta.rs | 20 +- src/services/rag.rs | 37 +- src/services/target.rs | 65 ++- src/services/translation.rs | 44 +- 44 files changed, 2307 insertions(+), 1578 deletions(-) create mode 100644 migrations/20260624000000_add_session_mode.sql delete mode 100644 src/agent/coordinator/agent.rs delete mode 100644 src/agent/coordinator/mod.rs delete mode 100644 src/agent/coordinator/tools.rs delete mode 100644 src/agent/coordinator/worker.rs create mode 100644 src/agent/modes/deep_research.rs create mode 100644 src/agent/modes/default.rs create mode 100644 src/agent/modes/literature_reader.rs create mode 100644 src/agent/modes/mod.rs create mode 100644 src/agent/tools/astro/analyze_image.rs diff --git a/.env.example b/.env.example index 811790d..f227bd5 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,23 @@ LLM_MODEL=gpt-4o-mini # 备用模型链(逗号分隔,按顺序轮换尝试,可选) # LLM_FALLBACK_CHAIN=gpt-4o-mini,deepseek-chat +# 中型大语言模型配置(用于文献翻译、RAG,默认级联回退至核心 LLM 配置) +# LLM_MEDIUM_API_KEY=your_llm_medium_api_key_here +# LLM_MEDIUM_API_BASE=https://api.openai.com/v1 +# LLM_MEDIUM_MODEL=gpt-4o-mini + +# 快速大语言模型配置(用于记忆提取等后台辅助任务,默认级联回退至中型 LLM 配置) +# LLM_FAST_API_KEY=your_llm_fast_api_key_here +# LLM_FAST_API_BASE=https://api.openai.com/v1 +# LLM_FAST_MODEL=gpt-3.5-turbo + +# ── 视觉模型(可选,配置后启用图片分析能力)── +# 专用视觉模型名称。主 Agent 可为纯文本模型,图片分析通过 analyze_image 工具 +# 委托给此模型。用户上传图片时也会先用此模型预处理再交给 Agent。 +# LLM_VISION_MODEL=gpt-4o +# LLM_VISION_API_KEY=your_vision_api_key_here(空时回退到 LLM_API_KEY) +# LLM_VISION_API_BASE=https://api.openai.com/v1(空时回退到 LLM_API_BASE) + # 向量嵌入模型配置(未设置时默认回退到 LLM 的 API Key 和 Base) # EMBEDDING_API_KEY=your_embedding_api_key_here # EMBEDDING_API_BASE=https://api.openai.com/v1 @@ -92,11 +109,6 @@ LOG_DIR=./logs # 触发 snip 压缩的最大消息数(默认 50) # AGENT_MAX_MESSAGES=50 -# ── P2 Coordinator Mode ── -# Worker 最大并发数(默认 4) -# AGENT_COORDINATOR_MAX_WORKERS=4 -# Worker 超时秒数(默认 300) -# AGENT_COORDINATOR_WORKER_TIMEOUT=300 # ── 权限系统 ── # Agent 工具权限规则(逗号分隔,支持内容级匹配 "ToolName(pattern)") diff --git a/.gitignore b/.gitignore index f8fe032..15342e7 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ library/MinerU/ libs/ .claude +.checkpoints diff --git a/CLAUDE.md b/CLAUDE.md index 88fa44f..4ad0a94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,11 +69,13 @@ src/ │ └── helpers.rs # Shared DB helpers, format conversion, path validation ├── agent/ # ReAct-based research agent (LLM-driven tool-use loop) │ ├── 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 +│ ├── runtime/ # ReAct loop core: session, context, streaming, executor, token_budget, +│ │ # permission, permission_profile, checkpoint, circuit_breaker, error_recovery, +│ │ # hardline, partitioner, file_cache, system_prompt, finalize, denial_tracker │ ├── 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.) +│ ├── hooks/ # Lifecycle hooks: registry, dispatch, builtins, matcher, traits (PreToolUse/PostToolUse/Stop/etc.) +│ ├── modes/ # Agent session modes: default, deep-research, literature-reader (identity/tools/config presets) │ ├── 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 @@ -121,7 +123,7 @@ The agent (`src/agent/`) implements a **ReAct** (Thought → Action → Observat 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 (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). +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, analyze_image (vision model, only when `LLM_VISION_MODEL` set). 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). @@ -162,6 +164,71 @@ The `obscura-inprocess` feature compiles `obscura-browser` and `obscura-net` dir A 1MB+ bilingual astronomy terminology file loaded at startup into a Trie tree for longest-match glossary construction, used by the translation service to guide LLM translations with domain-accurate term mappings. +### Agent Session Modes (`src/agent/modes/`) + +Session-level modes configure the agent's identity, tool access, step limits, thinking, and permissions at creation time. Modes are pure-data constants defined at compile time — adding a new mode requires no logic changes. + +Three built-in modes (registered in `ModeRegistry::builtins()`): + +| Mode | ID | Tools | Max Steps | Thinking | Permission Profile | +|------|-----|-------|-----------|----------|---------------------| +| 通用科研助手 | `default` | All (unrestricted) | 8 (default) | user-controlled | none | +| 深度研究 | `deep-research` | All | 16 | forced on | `research` | +| 文献阅读助手 | `literature-reader` | Allowlist (read-only + literature) | 6 | forced off | `readonly` | + +Key types in `modes/mod.rs`: +- **`AgentMode`** — static definition struct: id, name, description, icon, identity/principles overrides, extra sections, `ToolSet`, `ModeConfig` +- **`ToolSet`** — `All`, `Allowlist(&[&str])`, or `Except(&[&str])` — constrains which tools are available +- **`ModeConfig`** — optional overrides for `max_steps`, `enable_thinking`, `tool_timeout_secs`, `permission_profile` +- **`ModeRegistry`** — holds `&'static AgentMode` references; `get(id)` for lookup + +The mode is stored in the `agent_sessions.mode` column (migration `20260624000000_add_session_mode.sql`, defaults to `'default'`). The frontend `ResearchAgentPanel` exposes mode selection. + +**Mode vs Skill**: Modes are session-level ("who am I"), Skills are task-level ("how do I do X"). Modes affect initialization; the ReAct loop itself is mode-agnostic. + +### Permission System (`src/agent/runtime/permission.rs`) + +Tool execution is gated by a priority-ordered rule chain: **Deny > Allow > Ask** (first match wins). Rules support content-level pattern matching (e.g., `run_bash(rm *)`). + +**Permission modes** (set via `AGENT_PERMISSION_MODE` env or mode's `permission_profile`): +- `default` — full rule chain evaluation +- `accept_edits` — auto-allow `file_write`/`file_edit` within the working directory +- `bypass` — skip all Ask checks (Deny rules still enforced) +- `dont_ask` — convert all Ask to Deny + +**Permission profiles** (`src/agent/runtime/permission_profile.rs`) are named presets loaded by `AGENT_PERMISSION_MODE` or a mode's `permission_profile` field. Profiles `research` and `readonly` are used by deep-research and literature-reader modes respectively. + +**Configuration** (in `.env`): +- `AGENT_PERMISSIONS_DENY` — comma-separated deny rules (e.g., `run_bash(rm *),run_bash(sudo *)`) +- `AGENT_PERMISSIONS_ALLOW` — comma-separated allow rules +- `AGENT_PERMISSIONS_ASK` — comma-separated ask rules +- `AGENT_PERMISSION_MODE` — default/accept_edits/bypass/dont_ask + +### Multi-Tier LLM Configuration + +The system supports three LLM tiers with cascade fallback: + +| Tier | Env Prefix | Purpose | Fallback | +|------|-----------|---------|----------| +| Primary | `LLM_` | Main agent reasoning | — | +| Medium | `LLM_MEDIUM_` | Translation, RAG | Primary LLM config | +| Fast | `LLM_FAST_` | Memory extraction, background tasks | Medium LLM config | + +Each tier has `_API_KEY`, `_API_BASE`, `_MODEL` variants. Unset tiers cascade to the next tier down. + +**Fallback chain**: `LLM_FALLBACK_CHAIN` (comma-separated model names) provides automatic model rotation on repeated 529 errors. `LLM_FALLBACK_MODEL` is a single backup model tried before the chain. + +### Vision Model (`analyze_image` tool) + +When `LLM_VISION_MODEL` env is set, the `analyze_image` tool (`src/agent/tools/astro/analyze_image.rs`) is registered. It delegates image analysis to a dedicated vision model, enabling the main agent to use a text-only model while still processing images. Supports local paths (relative to library dir) and HTTP(S) URLs. Results stream via SSE `TextDelta` events. Also supports `LLM_VISION_API_KEY` and `LLM_VISION_API_BASE` (fall back to primary LLM config). + +### Auto Memory Extraction + +Controlled by env vars: +- `EXTRACT_MEMORY_ENABLED` (default `false`) — enables automatic memory extraction at session end and during compaction +- `EXTRACT_MEMORY_THROTTLE_TURNS` (default `3`) — extract every N turns +- `EXTRACT_MEMORY_MAX_STEPS` (default `3`) — sub-agent max steps for extraction + ## Code Conventions - Use `anyhow` for application errors, `thiserror` for library-style typed errors diff --git a/dashboard/src/features/agent/ResearchAgentPanel.tsx b/dashboard/src/features/agent/ResearchAgentPanel.tsx index a7e358f..ca57526 100644 --- a/dashboard/src/features/agent/ResearchAgentPanel.tsx +++ b/dashboard/src/features/agent/ResearchAgentPanel.tsx @@ -12,7 +12,8 @@ import { Brain, Settings, Eye, CheckCircle2, AlertTriangle, Send, Loader, Plus, Trash2, Compass, Clock, Square, BarChart3, ScrollText, Network, Rewind, RotateCcw, - GitBranch, RefreshCw, Search, X, MessageSquare, BookOpen + GitBranch, RefreshCw, Search, X, MessageSquare, BookOpen, + PanelLeftClose, PanelLeftOpen, Paperclip } from 'lucide-react'; import { AskUserQuestionCard } from './AskUserQuestionCard'; import { PermissionRequestCard } from './PermissionRequestCard'; @@ -22,6 +23,7 @@ interface SessionSummary { session_id: string; title: string; model: string; + mode: string; turn_count: number; summary?: string | null; created_at: string; @@ -72,6 +74,7 @@ type ColorScheme = 'parent' | 'subagent'; interface ActiveTurn { question: string; + imagePath?: string; timeline: TimelineItem[]; finalAnswer: string; error?: string; @@ -86,6 +89,7 @@ interface ProcessedTurn { turn_index: number; question: string; questionMessageId?: number; + imagePath?: string; timeline: TimelineItem[]; usage?: { prompt_tokens: number; @@ -113,6 +117,7 @@ interface RetryResult { new_turn_index: number; deleted_count: number; session_id: string; + image_path?: string | null; } const safeSchema = { @@ -180,6 +185,23 @@ function getToolDisplayName(name: string): string { } } +interface AgentMode { + id: string; + name: string; + description: string; + icon: string; +} + +const ICON_MAP: Record> = { + Brain, + Compass, + BookOpen +}; + +function getIconComponent(iconName: string): React.ComponentType<{ className?: string }> { + return ICON_MAP[iconName] || Brain; +} + interface ResearchAgentPanelProps { showConfirm?: (message: string, onConfirm: () => void, title?: string) => void; showAlert?: (message: string, title?: string) => void; @@ -192,10 +214,41 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane const [activeTurn, setActiveTurn] = useState(null); const [streaming, setStreaming] = useState(false); const [input, setInput] = useState(''); + const [agentMode, setAgentMode] = useState('default'); + const [agentModes, setAgentModes] = useState([]); const [thinking, setThinking] = useState(false); - const [coordinatorMode, setCoordinatorMode] = useState(false); + const [pendingImage, setPendingImage] = useState<{ data?: string; path?: string; mime_type: string; name: string } | null>(null); + const fileInputRef = useRef(null); const [loadingSessions, setLoadingSessions] = useState(false); + + // 将 File 转为 base64 并设置为 pendingImage + const attachImage = (file: File) => { + if (!file.type.startsWith('image/')) return; + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result as string; + const commaIdx = result.indexOf(','); + const data = commaIdx > -1 ? result.substring(commaIdx + 1) : result; + setPendingImage({ data, mime_type: file.type, name: file.name }); + }; + reader.readAsDataURL(file); + }; + + // 粘贴事件:从剪贴板获取图片 + const handlePaste = (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; + for (let i = 0; i < items.length; i++) { + if (items[i].type.startsWith('image/')) { + e.preventDefault(); + const file = items[i].getAsFile(); + if (file) attachImage(file); + return; + } + } + }; const [loadingHistory, setLoadingHistory] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); // 全文搜索历史记录状态 const [searchQuery, setSearchQuery] = useState(''); @@ -266,6 +319,22 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane useEffect(() => { fetchSessions(true); + + const fetchModes = async () => { + try { + const res = await axios.get('/api/chat/modes'); + setAgentModes(res.data); + } catch (e) { + console.error('获取 Agent 运行模式列表失败:', e); + // Fallback in case of API failure for resilience + setAgentModes([ + { id: 'default', name: '默认', icon: 'Brain', description: '通用天体物理学研究助手,自主判断搜索、阅读或计算' }, + { id: 'deep-research', name: '深度', icon: 'Compass', description: '多来源系统性文献调研与交叉验证,适合撰写综述' }, + { id: 'literature-reader', name: '精读', icon: 'BookOpen', description: '专注论文精读、翻译与笔记提炼,强制只读安全模式' }, + ]); + } + }; + fetchModes(); }, []); // 加载选中的会话历史消息 @@ -278,6 +347,10 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane const res = await axios.get<{ session: SessionSummary; messages: MessageRecord[] }>(`/api/chat/sessions/${sessionId}`); const newMessages = res.data.messages; + if (res.data.session && res.data.session.mode) { + setAgentMode(res.data.session.mode); + } + // 如果有成功回调,在更新 messages 之前立刻执行(如清除 activeTurn),保证在同一个 React 渲染批处理周期内完成 if (onSuccess) { onSuccess(); @@ -531,7 +604,15 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane // 重新加载会话以移除已被硬删除的消息,然后自动触发发送 await loadSessionHistory(currentSessionId, true); await fetchSessions(); - + + // 如果原消息附带了图片,通过 path 复用已有文件,避免重新上传 + if (res.data.image_path) { + const ext = res.data.image_path.split('.').pop() || 'png'; + const mimeType = { jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', png: 'image/png' }[ext] || 'image/png'; + handleSend(questionText, { path: res.data.image_path, mime_type: mimeType, name: res.data.image_path.split('/').pop() || 'image' }); + return; + } + // 触发自动重发 handleSend(questionText); } catch (e: any) { @@ -555,7 +636,8 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane }; // 发送消息与流式响应处理 - const handleSend = async (questionText: string) => { + // imageOverride: 重试时直接传入图片数据,绕过 React 异步状态更新 + const handleSend = async (questionText: string, imageOverride?: { data?: string; path?: string; mime_type: string; name: string } | null) => { if (!questionText.trim() || streaming) return; if (!currentSessionId) { @@ -563,11 +645,16 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane } setInput(''); + const image = imageOverride !== undefined ? imageOverride : pendingImage; + setPendingImage(null); setStreaming(true); setShouldAutoScroll(true); const active: ActiveTurn = { question: questionText, + imagePath: image + ? (image.path ? `/api/files/${image.path}` : `data:${image.mime_type};base64,${image.data}`) + : undefined, timeline: [], finalAnswer: '', }; @@ -582,8 +669,13 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane body: JSON.stringify({ question: questionText, session_id: currentSessionId, + mode: agentMode, thinking, - coordinator_mode: coordinatorMode, + ...(image ? { + image: image.path + ? { path: image.path, mime_type: image.mime_type } + : { data: image.data, mime_type: image.mime_type }, + } : {}), }), }); @@ -756,6 +848,8 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane }); break; case 'tool_result': + // 自动展开结果区域 + setExpandedResults(prev => ({ ...prev, [event.tool_call_id]: true })); setActiveTurn(prev => { if (!prev) return prev; const tcId = event.tool_call_id; @@ -788,10 +882,11 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane } } // 父代理工具结果(正常路径) + // 始终更新 result,即使已有流式输出的部分内容——最终 tool_result 是完整答案 return { ...prev, timeline: prev.timeline.map(t => { - if (t.type === 'tool_call' && t.id === tcId && !t.result) { + if (t.type === 'tool_call' && t.id === tcId) { return { ...t, result: { output: event.output, isError: event.is_error }, @@ -803,8 +898,42 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane }); break; case 'text_delta': + // 如果带 tool_call_id,自动展开对应工具的结果区域以显示流式输出 + if (event.tool_call_id) { + setExpandedResults(prev => ({ ...prev, [event.tool_call_id]: true })); + } setActiveTurn(prev => { if (!prev) return prev; + // 如果带有 tool_call_id,流式输出到对应工具的结果区域 + if (event.tool_call_id) { + const tcId = event.tool_call_id; + // 检查 timeline 中是否有匹配的 tool_call + const hasToolCall = prev.timeline.some( + t => t.type === 'tool_call' && 'id' in t && t.id === tcId + ); + if (hasToolCall) { + return { + ...prev, + timeline: prev.timeline.map(t => { + if (t.type === 'tool_call' && t.id === tcId) { + const prevOutput = t.result?.output || ''; + return { + ...t, + result: { + output: prevOutput + event.content, + isError: false, + }, + }; + } + return t; + }), + }; + } + // 工具调用尚未出现(TTL 场景),暂存到 finalAnswer 不处理 + // 等 tool_call + tool_result 出现后,tool_result 会覆盖 + return prev; + } + // 无 tool_call_id → 主回答文本流 return { ...prev, finalAnswer: prev.finalAnswer + event.content, @@ -1065,8 +1194,13 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane {isResultExpanded ? ( -
- {item.result!.output} +
+ + {item.result!.output} +
) : (
@@ -1111,20 +1245,29 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
{/* 左侧会话列表侧栏 */} -
+
- 智能科研研讨 + {!sidebarCollapsed && 智能科研研讨} - +
+ + +
{/* 搜索框区域 */} @@ -1274,15 +1417,26 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
{/* 会话头部 */}
-
-

- {currentSessionId - ? (sessions.find(s => s.session_id === currentSessionId)?.title || '未命名会话') - : '探索性科研研讨'} -

-

- 科研智能体 (ReAct 循环:Thought → Action → Observation) -

+
+ {sidebarCollapsed && ( + + )} +
+

+ {currentSessionId + ? (sessions.find(s => s.session_id === currentSessionId)?.title || '未命名会话') + : '探索性科研研讨'} +

+

+ 科研智能体 (ReAct 循环:Thought → Action → Observation) +

+
{/* 指标面板按钮 */} @@ -1358,7 +1512,7 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
{loadingHistory ? ( @@ -1411,8 +1565,11 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane )} -
- {turn.question} +
+ {turn.imagePath && ( + 用户上传的图片 + )} +
{turn.question}
@@ -1439,8 +1596,11 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane {/* 当前提问 */}
-
- {activeTurn.question} +
+ {activeTurn.imagePath && ( + 用户上传的图片 + )} +
{activeTurn.question}
@@ -1534,65 +1694,137 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane e.preventDefault(); handleSend(input); }} - className="p-4 border-t border-slate-200 bg-white shrink-0" + className="absolute bottom-0 left-0 right-0 p-4 bg-transparent pointer-events-none shrink-0" > -
- {/* 思考模式开关 */} - - {/* 协调者模式开关 */} - - +