feat(flow): 新增流式执行模式与SSE支持

新增流式执行模式,通过SSE实时推送节点执行事件与日志
重构HTTP执行器与中间件,提取通用HTTP客户端组件
优化前端测试面板,支持流式模式切换与实时日志展示
更新依赖版本并修复密码哈希的随机数生成器问题
修复前端节点类型映射问题,确保Code节点表单可用
This commit is contained in:
2025-09-21 01:48:24 +08:00
parent 296f0ae9f6
commit dd7857940f
24 changed files with 1695 additions and 885 deletions

View File

@ -2,8 +2,7 @@ use async_trait::async_trait;
use serde_json::{Value, json, Map};
use tracing::info;
use std::collections::HashMap;
use std::time::Duration;
use reqwest::Certificate;
use crate::middlewares::http_client::{execute_http, HttpClientOptions, HttpRequest};
use crate::flow::task::Executor;
use crate::flow::domain::{NodeDef, NodeId};
@ -34,64 +33,29 @@ impl Executor for HttpTask {
return Ok(());
};
// 3) 解析配置
// 3) 解析配置 -> 转换为中间件请求参数
let (method, url, headers, query, body, opts) = parse_http_config(cfg)?;
info!(target = "udmin.flow", "http task: {} {}", method, url);
// 4) 发送请求(支持 HTTPS 相关选项)
let client = {
let mut builder = reqwest::Client::builder();
if let Some(ms) = opts.timeout_ms { builder = builder.timeout(Duration::from_millis(ms)); }
if opts.insecure { builder = builder.danger_accept_invalid_certs(true); }
if opts.http1_only { builder = builder.http1_only(); }
if let Some(pem) = opts.ca_pem {
if let Ok(cert) = Certificate::from_pem(pem.as_bytes()) {
builder = builder.add_root_certificate(cert);
}
}
builder.build()?
let req = HttpRequest {
method,
url,
headers,
query,
body,
};
let client_opts = HttpClientOptions {
timeout_ms: opts.timeout_ms,
insecure: opts.insecure,
ca_pem: opts.ca_pem,
http1_only: opts.http1_only,
};
let mut req = client.request(method.parse()?, url);
if let Some(hs) = headers {
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
let mut map = HeaderMap::new();
for (k, v) in hs {
if let (Ok(name), Ok(value)) = (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v)) {
map.insert(name, value);
}
}
req = req.headers(map);
}
if let Some(qs) = query {
// 将查询参数转成 (String, String) 列表,便于 reqwest 序列化
let mut pairs: Vec<(String, String)> = Vec::new();
for (k, v) in qs {
let s = match v {
Value::String(s) => s,
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
other => other.to_string(),
};
pairs.push((k, s));
}
req = req.query(&pairs);
}
if let Some(b) = body { req = req.json(&b); }
let resp = req.send().await?;
let status = resp.status().as_u16();
let headers_out: Map<String, Value> = resp
.headers()
.iter()
.map(|(k, v)| (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string())))
.collect();
// 尝试以 JSON 解析,否则退回文本
let text = resp.text().await?;
let parsed_body: Value = serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text));
// 4) 调用中间件发送请求
let out = execute_http(req, client_opts).await?;
let status = out.status;
let headers_out = out.headers;
let parsed_body = out.body;
// 5) 将结果写回 ctx
let result = json!({
@ -138,8 +102,15 @@ fn parse_http_config(cfg: Value) -> anyhow::Result<(
let query = m.remove("query").and_then(|v| v.as_object().cloned());
let body = m.remove("body");
// 可选 HTTPS/超时/HTTP 版本配置
let timeout_ms = m.remove("timeout_ms").and_then(|v| v.as_u64());
// 统一解析超时配置(内联)
let timeout_ms = if let Some(ms) = m.remove("timeout_ms").and_then(|v| v.as_u64()) {
Some(ms)
} else if let Some(Value::Object(mut to)) = m.remove("timeout") {
to.remove("timeout").and_then(|v| v.as_u64())
} else {
None
};
let insecure = m.remove("insecure").and_then(|v| v.as_bool()).unwrap_or(false);
let http1_only = m.remove("http1_only").and_then(|v| v.as_bool()).unwrap_or(false);
let ca_pem = m.remove("ca_pem").and_then(|v| v.as_str().map(|s| s.to_string()));

View File

@ -1,6 +1,5 @@
pub mod http;
pub mod db;
// removed: pub mod expr;
pub mod variable;
pub mod script_rhai;
pub mod script_js;