Keep Zhihu hotlist export requests on the orchestration path so natural-language submits without page context no longer fail in direct-submit routing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
360 lines
14 KiB
Rust
360 lines
14 KiB
Rust
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<PathBuf>,
|
|
workspace_root: PathBuf,
|
|
}
|
|
|
|
impl AgentRuntimeContext {
|
|
pub fn new(config_path: Option<PathBuf>, workspace_root: PathBuf) -> Self {
|
|
Self {
|
|
config_path,
|
|
workspace_root,
|
|
}
|
|
}
|
|
|
|
pub fn from_process_args<I, S>(args: I) -> Result<Self, PipeError>
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: Into<OsString>,
|
|
{
|
|
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<Option<SgClawSettings>, 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<T: Transport>(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<T: Transport>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
plan: &planner::TaskPlan,
|
|
) -> Result<String, PipeError> {
|
|
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<T: Transport>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
instruction: &str,
|
|
) -> Result<String, PipeError> {
|
|
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<T: Transport + 'static>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
message: BrowserMessage,
|
|
) -> Result<(), PipeError> {
|
|
handle_browser_message_with_context(
|
|
transport,
|
|
browser_tool,
|
|
&AgentRuntimeContext::default(),
|
|
message,
|
|
)
|
|
}
|
|
|
|
pub fn handle_browser_message_with_context<T: Transport + 'static>(
|
|
transport: &T,
|
|
browser_tool: &BrowserPipeTool<T>,
|
|
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(())
|
|
}
|
|
}
|
|
}
|