use std::sync::Arc;
use std::time::Duration;
use futures::future::BoxFuture;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use super::{HumanLoopKind, HumanLoopProvider, HumanLoopRequest, HumanLoopResponse};
use echo_core::error::{ReactError, Result};
pub struct WebhookHumanLoopProvider {
client: Arc<Client>,
url: String,
timeout: Duration,
}
impl WebhookHumanLoopProvider {
pub fn new(url: impl Into<String>) -> Self {
Self {
client: Arc::new(
Client::builder()
.timeout(std::time::Duration::from_secs(120))
.build()
.unwrap_or_default(),
),
url: url.into(),
timeout: Duration::from_secs(300),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
}
#[derive(Serialize)]
struct WebhookPayload<'a> {
kind: &'a str,
prompt: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
tool_name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
args: Option<&'a serde_json::Value>,
}
#[derive(Deserialize)]
struct WebhookResponse {
decision: Option<String>,
text: Option<String>,
reason: Option<String>,
}
impl HumanLoopProvider for WebhookHumanLoopProvider {
fn request(&self, req: HumanLoopRequest) -> BoxFuture<'_, Result<HumanLoopResponse>> {
Box::pin(async move {
let kind_str = match req.kind {
HumanLoopKind::Approval => "approval",
HumanLoopKind::Input => "input",
};
let payload = WebhookPayload {
kind: kind_str,
prompt: &req.prompt,
tool_name: req.tool_name.as_deref(),
args: req.args.as_ref(),
};
let resp = self
.client
.post(&self.url)
.timeout(self.timeout)
.json(&payload)
.send()
.await
.map_err(|e| ReactError::Other(format!("Webhook 请求失败: {e}")))?;
if !resp.status().is_success() {
return Err(ReactError::Other(format!(
"Webhook 返回非成功状态码: {}",
resp.status()
)));
}
let response: WebhookResponse = resp
.json()
.await
.map_err(|e| ReactError::Other(format!("Webhook 响应解析失败: {e}")))?;
match req.kind {
HumanLoopKind::Approval => match response.decision.as_deref() {
Some("approved") => Ok(HumanLoopResponse::Approved),
Some("rejected") => Ok(HumanLoopResponse::Rejected {
reason: response.reason,
}),
Some("timeout") | None => Ok(HumanLoopResponse::Timeout),
Some(other) => Err(ReactError::Other(format!("未知的审批决策值: {other}"))),
},
HumanLoopKind::Input => match response.text {
Some(text) => Ok(HumanLoopResponse::Text(text)),
None => Ok(HumanLoopResponse::Timeout),
},
}
})
}
}