use reqwest::Url; use serde_json::{json, Value}; use thiserror::Error; use crate::pipe::Action; const BAIDU_URL: &str = "https://www.baidu.com"; const BAIDU_DOMAIN: &str = "www.baidu.com"; const BAIDU_INPUT_SELECTOR: &str = "#kw"; const BAIDU_SEARCH_BUTTON_SELECTOR: &str = "#su"; const ZHIHU_URL: &str = "https://www.zhihu.com/search"; const ZHIHU_DOMAIN: &str = "www.zhihu.com"; #[derive(Debug, Clone, PartialEq)] pub struct PlannedStep { pub action: Action, pub params: Value, pub expected_domain: String, pub log_message: String, } #[derive(Debug, Clone, PartialEq)] pub struct TaskPlan { pub summary: String, pub steps: Vec, } #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum PlannerError { #[error("unsupported instruction: {0}")] UnsupportedInstruction(String), #[error("missing search query in instruction")] MissingQuery, } pub fn plan_instruction(instruction: &str) -> Result { let trimmed = instruction.trim(); if let Some(query) = extract_query(trimmed, &["打开百度搜索", "打开百度并搜索"])? { return Ok(plan_baidu_search(query)); } if let Some(query) = extract_query(trimmed, &["打开知乎搜索", "打开知乎并搜索"])? { return Ok(plan_zhihu_search(query)); } Err(PlannerError::UnsupportedInstruction(trimmed.to_string())) } fn extract_query<'a>( instruction: &'a str, prefixes: &[&str], ) -> Result, PlannerError> { let Some(query) = prefixes .iter() .find_map(|prefix| instruction.strip_prefix(prefix)) else { return Ok(None); }; let query = query.trim(); if query.is_empty() { return Err(PlannerError::MissingQuery); } Ok(Some(query)) } fn plan_baidu_search(query: &str) -> TaskPlan { TaskPlan { summary: format!("已在百度搜索{query}"), steps: vec![ PlannedStep { action: Action::Navigate, params: json!({ "url": BAIDU_URL }), expected_domain: BAIDU_DOMAIN.to_string(), log_message: "navigate https://www.baidu.com".to_string(), }, PlannedStep { action: Action::Type, params: json!({ "selector": BAIDU_INPUT_SELECTOR, "text": query, "clear_first": true }), expected_domain: BAIDU_DOMAIN.to_string(), log_message: format!("type {query} into {BAIDU_INPUT_SELECTOR}"), }, PlannedStep { action: Action::Click, params: json!({ "selector": BAIDU_SEARCH_BUTTON_SELECTOR }), expected_domain: BAIDU_DOMAIN.to_string(), log_message: format!("click {BAIDU_SEARCH_BUTTON_SELECTOR}"), }, ], } } fn plan_zhihu_search(query: &str) -> TaskPlan { let url = Url::parse_with_params(ZHIHU_URL, &[("type", "content"), ("q", query)]) .expect("valid Zhihu search URL"); let url: String = url.into(); TaskPlan { summary: format!("已在知乎搜索{query}"), steps: vec![PlannedStep { action: Action::Navigate, params: json!({ "url": url }), expected_domain: ZHIHU_DOMAIN.to_string(), log_message: format!("navigate {url}"), }], } }