diff --git a/crates/zclaw-hands/src/hands/researcher.rs b/crates/zclaw-hands/src/hands/researcher.rs index 742c910..35dbcf3 100644 --- a/crates/zclaw-hands/src/hands/researcher.rs +++ b/crates/zclaw-hands/src/hands/researcher.rs @@ -301,6 +301,61 @@ impl ResearcherHand { } } + /// Infer action from input fields when LLM omits the `action` field. + /// Many LLMs (especially non-OpenAI models like glm) call tools without + /// including the enum tag, e.g. sending `{"query": "search terms"}` instead + /// of `{"action": "search", "query": "search terms"}`. + fn infer_action(input: &Value) -> Result { + // Has "url" (singular) → fetch + if let Some(url) = input.get("url").and_then(|v| v.as_str()) { + if !url.is_empty() && url.starts_with("http") { + return Ok(ResearcherAction::Fetch { url: url.to_string() }); + } + } + // Has "urls" (plural) → summarize + if let Some(urls) = input.get("urls").and_then(|v| v.as_array()) { + let url_list: Vec = urls.iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(); + if !url_list.is_empty() { + return Ok(ResearcherAction::Summarize { urls: url_list }); + } + } + // Has "query" (string or object) → search + if let Some(query_val) = input.get("query") { + let query: ResearchQuery = if query_val.is_string() { + // LLM sent plain string: {"query": "search terms"} + ResearchQuery { + query: query_val.as_str().unwrap_or("").to_string(), + engine: SearchEngine::Auto, + depth: ResearchDepth::Standard, + max_results: 10, + include_related: false, + time_limit_secs: 60, + } + } else { + // LLM sent object: {"query": {"query": "...", "engine": "..."}} + serde_json::from_value(query_val.clone()).unwrap_or_else(|_| ResearchQuery { + query: query_val.get("query") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + engine: SearchEngine::Auto, + depth: ResearchDepth::Standard, + max_results: 10, + include_related: false, + time_limit_secs: 60, + }) + }; + if !query.query.trim().is_empty() { + return Ok(ResearcherAction::Search { query }); + } + } + Err(zclaw_types::ZclawError::HandError( + "无法识别搜索意图:请提供 query(搜索)或 url(获取网页)参数".to_string() + )) + } + /// Execute a web search — route to the configured backend async fn execute_search(&self, query: &ResearchQuery) -> Result> { query.validate().map_err(|e| zclaw_types::ZclawError::HandError(e))?; @@ -1030,8 +1085,11 @@ impl Hand for ResearcherHand { } async fn execute(&self, _context: &HandContext, input: Value) -> Result { - let action: ResearcherAction = serde_json::from_value(input.clone()) - .map_err(|e| zclaw_types::ZclawError::HandError(format!("Invalid action: {}", e)))?; + // Try strict deserialization first, then fall back to inference + let action: ResearcherAction = match serde_json::from_value(input.clone()) { + Ok(a) => a, + Err(_) => Self::infer_action(&input)?, + }; let start = std::time::Instant::now();