Files
skill-lib/src/agent/runtime.rs
木炎 6aad2ce48e feat: restore zhihu browser skills
Reconnect the recovered Zhihu skill flows to the live browser runtime and resolve their resources relative to the executable so they work outside the repo root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:29:38 +08:00

152 lines
4.6 KiB
Rust

use serde_json::{json, Map, Value};
use crate::llm::{ChatMessage, LlmError, LlmProvider, ToolDefinition, ToolFunctionCall};
use crate::pipe::{Action, AgentMessage, BrowserPipeTool, PipeError, Transport};
const BROWSER_ACTION_TOOL_NAME: &str = "browser_action";
#[derive(Debug, Clone, PartialEq)]
struct BrowserActionCall {
action: Action,
expected_domain: String,
params: Value,
}
pub fn execute_task_with_provider<P: LlmProvider, T: Transport>(
transport: &T,
browser_tool: &BrowserPipeTool<T>,
provider: &P,
instruction: &str,
) -> Result<String, PipeError> {
let messages = vec![
ChatMessage {
role: "system".to_string(),
content: "You are sgClaw. Use browser_action to complete the browser task.".to_string(),
},
ChatMessage {
role: "user".to_string(),
content: instruction.to_string(),
},
];
let tools = vec![browser_action_tool_definition()];
let calls = provider
.chat(&messages, &tools)
.map_err(map_llm_error_to_pipe_error)?;
for call in calls {
let browser_call =
parse_browser_action_call(call).map_err(|err| PipeError::Protocol(err.to_string()))?;
transport.send(&AgentMessage::LogEntry {
level: "info".to_string(),
message: format!(
"{} {}",
browser_call.action.as_str(),
browser_call.expected_domain
),
})?;
let result = browser_tool.invoke(
browser_call.action,
browser_call.params,
&browser_call.expected_domain,
)?;
if !result.success {
return Err(PipeError::Protocol(format!(
"browser action failed: {}",
result.data
)));
}
}
Ok(format!("已通过 Agent 执行任务: {instruction}"))
}
pub fn browser_action_tool_definition() -> ToolDefinition {
ToolDefinition {
name: BROWSER_ACTION_TOOL_NAME.to_string(),
description: "Execute browser actions in SuperRPA".to_string(),
parameters: json!({
"type": "object",
"required": ["action", "expected_domain"],
"properties": {
"action": { "type": "string", "enum": ["click", "type", "navigate", "getText"] },
"expected_domain": { "type": "string" },
"selector": { "type": "string" },
"text": { "type": "string" },
"url": { "type": "string" },
"clear_first": { "type": "boolean" }
}
}),
}
}
fn parse_browser_action_call(call: ToolFunctionCall) -> Result<BrowserActionCall, RuntimeError> {
if call.name != BROWSER_ACTION_TOOL_NAME {
return Err(RuntimeError::UnsupportedTool(call.name));
}
let mut args = match call.arguments {
Value::Object(args) => args,
other => {
return Err(RuntimeError::InvalidArguments(format!(
"expected object arguments, got {other}"
)))
}
};
let action_name = take_required_string(&mut args, "action")?;
let expected_domain = take_required_string(&mut args, "expected_domain")?;
let action = parse_action(&action_name)?;
let params = Value::Object(action_params_from_args(args));
Ok(BrowserActionCall {
action,
expected_domain,
params,
})
}
fn map_llm_error_to_pipe_error(err: LlmError) -> PipeError {
PipeError::Protocol(err.to_string())
}
fn parse_action(action_name: &str) -> Result<Action, RuntimeError> {
match action_name {
"click" => Ok(Action::Click),
"type" => Ok(Action::Type),
"navigate" => Ok(Action::Navigate),
"getText" => Ok(Action::GetText),
other => Err(RuntimeError::UnsupportedAction(other.to_string())),
}
}
fn take_required_string(
args: &mut Map<String, Value>,
key: &'static str,
) -> Result<String, RuntimeError> {
match args.remove(key) {
Some(Value::String(value)) if !value.trim().is_empty() => Ok(value),
Some(other) => Err(RuntimeError::InvalidArguments(format!(
"{key} must be a non-empty string, got {other}"
))),
None => Err(RuntimeError::MissingField(key)),
}
}
fn action_params_from_args(args: Map<String, Value>) -> Map<String, Value> {
args
}
#[derive(Debug, thiserror::Error)]
enum RuntimeError {
#[error("unsupported tool: {0}")]
UnsupportedTool(String),
#[error("unsupported action: {0}")]
UnsupportedAction(String),
#[error("missing required field: {0}")]
MissingField(&'static str),
#[error("invalid tool arguments: {0}")]
InvalidArguments(String),
}