use std::ffi::OsString; use std::path::PathBuf; use std::sync::Arc; use crate::browser::BrowserBackend; use crate::compat::config_adapter::resolve_skills_dir_from_sgclaw_settings; use crate::compat::runtime::CompatTaskContext; use crate::config::SgClawSettings; use crate::pipe::{ AgentMessage, BrowserPipeTool, ConversationMessage, PipeError, Transport, }; use crate::runtime::RuntimeEngine; #[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(resolve_process_path(PathBuf::from(value))); continue; } let arg_string = arg.to_string_lossy(); if let Some(value) = arg_string.strip_prefix("--config-path=") { config_path = Some(resolve_process_path(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)) } pub(crate) 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 resolve_process_path(path: PathBuf) -> PathBuf { if path.is_absolute() { path } else { default_workspace_root().join(path) } } #[cfg(test)] mod tests { use super::*; #[test] fn from_process_args_resolves_relative_config_path_against_current_dir() { let current_dir = std::env::current_dir().unwrap(); let context = AgentRuntimeContext::from_process_args([ OsString::from("sg_claw"), OsString::from("--config-path"), OsString::from("../tmp/sgclaw_config.json"), ]) .unwrap(); assert_eq!( context.config_path, Some(current_dir.join("../tmp/sgclaw_config.json")) ); assert_eq!(context.workspace_root, current_dir.join("../tmp")); assert!(context.workspace_root.is_absolute()); } } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct SubmitTaskRequest { pub instruction: String, pub conversation_id: Option, pub messages: Vec, pub page_url: Option, pub page_title: Option, } pub trait AgentEventSink: Send + Sync { fn send(&self, message: &AgentMessage) -> Result<(), PipeError>; } impl AgentEventSink for T { fn send(&self, message: &AgentMessage) -> Result<(), PipeError> { Transport::send(self, message) } } pub fn run_submit_task( transport: &T, sink: &dyn AgentEventSink, browser_tool: &BrowserPipeTool, context: &AgentRuntimeContext, request: SubmitTaskRequest, ) -> Result<(), PipeError> { let SubmitTaskRequest { instruction, conversation_id, messages, page_url, page_title, } = request; let instruction = instruction.trim().to_string(); if instruction.is_empty() { return sink.send(&AgentMessage::TaskComplete { success: false, summary: "请输入任务内容。".to_string(), }); } let task_context = CompatTaskContext { conversation_id, messages, page_url, page_title, }; let _ = sink.send(&AgentMessage::LogEntry { level: "info".to_string(), message: runtime_version_log_message(), }); if !task_context.messages.is_empty() { let _ = sink.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_dirs = resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings); let _ = sink.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 _ = sink.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!("skills dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::>().join(", ")), }); let _ = sink.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!( "runtime profile={:?} skills_prompt_mode={:?}", settings.runtime_profile, settings.skills_prompt_mode ), }); if RuntimeEngine::new(settings.runtime_profile).browser_surface_enabled() && crate::compat::orchestration::should_use_primary_orchestration( &instruction, task_context.page_url.as_deref(), task_context.page_title.as_deref(), ) { let _ = send_mode_log(sink, "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 sink.send(&AgentMessage::TaskComplete { success: true, summary, }); } Err(err) => { return sink.send(&AgentMessage::TaskComplete { success: false, summary: err.to_string(), }); } } } let _ = send_mode_log(sink, "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 _ = sink.send(&AgentMessage::LogEntry { level: "error".to_string(), message: format!("failed to load DeepSeek config: {err}"), }); AgentMessage::TaskComplete { success: false, summary: err.to_string(), } } }; sink.send(&completion) } pub fn run_submit_task_with_browser_backend( _transport: &T, sink: &dyn AgentEventSink, browser_backend: Arc, context: &AgentRuntimeContext, request: SubmitTaskRequest, ) -> Result<(), PipeError> { let SubmitTaskRequest { instruction, conversation_id, messages, page_url, page_title, } = request; let instruction = instruction.trim().to_string(); if instruction.is_empty() { return sink.send(&AgentMessage::TaskComplete { success: false, summary: "请输入任务内容。".to_string(), }); } let task_context = CompatTaskContext { conversation_id, messages, page_url, page_title, }; let _ = sink.send(&AgentMessage::LogEntry { level: "info".to_string(), message: runtime_version_log_message(), }); if !task_context.messages.is_empty() { let _ = sink.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_dirs = resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings); let _ = sink.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 _ = sink.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!("skills dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::>().join(", ")), }); let _ = sink.send(&AgentMessage::LogEntry { level: "info".to_string(), message: format!( "runtime profile={:?} skills_prompt_mode={:?}", settings.runtime_profile, settings.skills_prompt_mode ), }); if RuntimeEngine::new(settings.runtime_profile).browser_surface_enabled() && crate::compat::orchestration::should_use_primary_orchestration( &instruction, task_context.page_url.as_deref(), task_context.page_title.as_deref(), ) { let _ = send_mode_log(sink, "zeroclaw_process_message_primary"); match crate::compat::orchestration::execute_task_with_browser_backend( sink, browser_backend.clone(), &instruction, &task_context, &context.workspace_root, &settings, ) { Ok(summary) => { return sink.send(&AgentMessage::TaskComplete { success: true, summary, }); } Err(err) => { return sink.send(&AgentMessage::TaskComplete { success: false, summary: err.to_string(), }); } } } let _ = send_mode_log(sink, "compat_llm_primary"); match crate::compat::runtime::execute_task_with_browser_backend( sink, browser_backend, &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 _ = sink.send(&AgentMessage::LogEntry { level: "error".to_string(), message: format!("failed to load DeepSeek config: {err}"), }); AgentMessage::TaskComplete { success: false, summary: err.to_string(), } } }; sink.send(&completion) } fn send_mode_log(sink: &dyn AgentEventSink, mode: &str) -> Result<(), PipeError> { sink.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 ) }