pub mod planner; pub mod runtime; use std::ffi::OsString; use std::path::PathBuf; use crate::compat::config_adapter::resolve_skills_dir_from_sgclaw_settings; use crate::compat::runtime::CompatTaskContext; use crate::config::SgClawSettings; use crate::pipe::{AgentMessage, BrowserMessage, BrowserPipeTool, PipeError, Transport}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentRuntimeContext { config_path: Option, workspace_root: PathBuf, } impl AgentRuntimeContext { pub fn new(config_path: Option, workspace_root: PathBuf) -> Self { Self { config_path, workspace_root, } } pub fn from_process_args(args: I) -> Result where I: IntoIterator, S: Into, { let mut config_path = None; let mut args = args.into_iter().map(Into::into); let _ = args.next(); while let Some(arg) = args.next() { if arg == OsString::from("--config-path") { let Some(value) = args.next() else { return Err(PipeError::Protocol( "missing value for --config-path".to_string(), )); }; config_path = Some(PathBuf::from(value)); continue; } let arg_string = arg.to_string_lossy(); if let Some(value) = arg_string.strip_prefix("--config-path=") { config_path = Some(PathBuf::from(value)); } } let workspace_root = config_path .as_ref() .and_then(|path| path.parent().map(|parent| parent.to_path_buf())) .unwrap_or_else(default_workspace_root); Ok(Self::new(config_path, workspace_root)) } fn load_sgclaw_settings(&self) -> Result, PipeError> { SgClawSettings::load(self.config_path.as_deref()) .map_err(|err| PipeError::Protocol(err.to_string())) } fn settings_source_label(&self) -> String { match &self.config_path { Some(path) if path.exists() => path.display().to_string(), _ => "environment".to_string(), } } } impl Default for AgentRuntimeContext { fn default() -> Self { Self::new(None, default_workspace_root()) } } fn default_workspace_root() -> PathBuf { std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) } fn send_mode_log(transport: &T, mode: &str) -> Result<(), PipeError> { transport.send(&AgentMessage::LogEntry { level: "mode".to_string(), message: mode.to_string(), }) } fn missing_llm_configuration_summary() -> String { "未配置大语言模型。请先在 sgclaw_config.json 或环境变量中配置 apiKey、baseUrl 与 model。" .to_string() } fn runtime_version_log_message() -> String { format!( "sgclaw runtime version={} protocol={}", env!("CARGO_PKG_VERSION"), crate::pipe::protocol::PROTOCOL_VERSION ) } fn execute_plan( transport: &T, browser_tool: &BrowserPipeTool, plan: &planner::TaskPlan, ) -> Result { for step in &plan.steps { transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: step.log_message.clone(), })?; let result = browser_tool.invoke( step.action.clone(), step.params.clone(), &step.expected_domain, )?; if !result.success { return Err(PipeError::Protocol(format!( "browser action failed: {}", result.data ))); } } Ok(plan.summary.clone()) } pub fn execute_task( transport: &T, browser_tool: &BrowserPipeTool, instruction: &str, ) -> Result { let plan = planner::plan_instruction(instruction) .map_err(|err| PipeError::Protocol(err.to_string()))?; execute_plan(transport, browser_tool, &plan) } pub fn handle_browser_message( transport: &T, browser_tool: &BrowserPipeTool, message: BrowserMessage, ) -> Result<(), PipeError> { handle_browser_message_with_context( transport, browser_tool, &AgentRuntimeContext::default(), message, ) } pub fn handle_browser_message_with_context( transport: &T, browser_tool: &BrowserPipeTool, context: &AgentRuntimeContext, message: BrowserMessage, ) -> Result<(), PipeError> { match message { BrowserMessage::SubmitTask { instruction, conversation_id, messages, page_url, page_title, } => { let raw_instruction = instruction; let trimmed_instruction = raw_instruction.trim().to_string(); if trimmed_instruction.is_empty() { return transport.send(&AgentMessage::TaskComplete { success: false, summary: "请输入任务内容。".to_string(), }); } let task_context = CompatTaskContext { conversation_id: (!conversation_id.trim().is_empty()) .then_some(conversation_id.clone()), messages, page_url: (!page_url.trim().is_empty()).then_some(page_url), page_title: (!page_title.trim().is_empty()).then_some(page_title), }; let mut instruction = trimmed_instruction; let mut deterministic_plan = None; match crate::compat::deterministic_submit::decide_deterministic_submit( &raw_instruction, task_context.page_url.as_deref(), task_context.page_title.as_deref(), ) { crate::compat::deterministic_submit::DeterministicSubmitDecision::NotDeterministic => {} crate::compat::deterministic_submit::DeterministicSubmitDecision::Prompt { summary } => { return transport.send(&AgentMessage::TaskComplete { success: false, summary, }); } crate::compat::deterministic_submit::DeterministicSubmitDecision::Execute(plan) => { instruction = plan.instruction.clone(); deterministic_plan = Some(plan); } } let _ = transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: runtime_version_log_message(), }); if !task_context.messages.is_empty() { let _ = transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!( "continuing conversation with {} prior turns", task_context.messages.len() ), }); } let completion = match context.load_sgclaw_settings() { Ok(Some(settings)) => { let resolved_skills_dir = resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings); let _ = transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!( "DeepSeek config loaded from {} model={} base_url={}", context.settings_source_label(), settings.provider_model, settings.provider_base_url ), }); let _ = transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!( "skills dir resolved to {}", resolved_skills_dir.display() ), }); let _ = transport.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!( "runtime profile={:?} skills_prompt_mode={:?}", settings.runtime_profile, settings.skills_prompt_mode ), }); if let Some(plan) = deterministic_plan.as_ref() { let _ = send_mode_log(transport, "direct_skill_primary"); let completion = match crate::compat::deterministic_submit::execute_deterministic_submit( browser_tool.clone(), plan, &context.workspace_root, &settings, ) { Ok(outcome) => AgentMessage::TaskComplete { success: outcome.success, summary: outcome.summary, }, Err(err) => AgentMessage::TaskComplete { success: false, summary: err.to_string(), }, }; return transport.send(&completion); } if crate::compat::orchestration::should_use_primary_orchestration( &instruction, task_context.page_url.as_deref(), task_context.page_title.as_deref(), ) { let _ = send_mode_log(transport, "zeroclaw_process_message_primary"); match crate::compat::orchestration::execute_task_with_sgclaw_settings( transport, browser_tool.clone(), &instruction, &task_context, &context.workspace_root, &settings, ) { Ok(summary) => { return transport.send(&AgentMessage::TaskComplete { success: true, summary, }) } Err(err) => { return transport.send(&AgentMessage::TaskComplete { success: false, summary: err.to_string(), }) } } } if settings .direct_submit_skill .as_deref() .map(str::trim) .is_some_and(|value| !value.is_empty()) { let _ = send_mode_log(transport, "direct_skill_primary"); let completion = match crate::compat::direct_skill_runtime::execute_direct_submit_skill( browser_tool.clone(), &instruction, &task_context, &context.workspace_root, &settings, ) { Ok(outcome) => AgentMessage::TaskComplete { success: outcome.success, summary: outcome.summary, }, Err(err) => AgentMessage::TaskComplete { success: false, summary: err.to_string(), }, }; return transport.send(&completion); } let _ = send_mode_log(transport, "compat_llm_primary"); match crate::compat::runtime::execute_task_with_sgclaw_settings( transport, browser_tool.clone(), &instruction, &task_context, &context.workspace_root, &settings, ) { Ok(summary) => AgentMessage::TaskComplete { success: true, summary, }, Err(err) => AgentMessage::TaskComplete { success: false, summary: err.to_string(), }, } } Ok(None) => AgentMessage::TaskComplete { success: false, summary: missing_llm_configuration_summary(), }, Err(err) => { let _ = transport.send(&AgentMessage::LogEntry { level: "error".to_string(), message: format!("failed to load DeepSeek config: {err}"), }); AgentMessage::TaskComplete { success: false, summary: err.to_string(), } } }; transport.send(&completion) } BrowserMessage::Init { .. } => { eprintln!("ignoring duplicate init after handshake"); Ok(()) } BrowserMessage::Response { seq, .. } => { eprintln!("ignoring unsolicited response: seq={seq}"); Ok(()) } } }