Skip to main content

relay_core_api/
modification.rs

1pub use crate::flow_query::{
2    FlowFilter, flow_matches_filter, flow_matches_query, parse_flow_filter,
3};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// 流量搜索条件,所有字段可选,多条件为 AND 关系。
10#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
11pub struct FlowQuery {
12    /// 主机名过滤(子串匹配)
13    pub host: Option<String>,
14    /// 路径过滤(子串匹配)
15    pub path_contains: Option<String>,
16    /// HTTP 方法过滤(大写,如 "GET"、"POST")
17    pub method: Option<String>,
18    /// 状态码下限(含)
19    pub status_min: Option<u16>,
20    /// 状态码上限(含)
21    pub status_max: Option<u16>,
22    /// 仅返回含错误的流量
23    pub has_error: Option<bool>,
24    /// 仅返回 WebSocket 流量
25    pub is_websocket: Option<bool>,
26    /// 返回条数上限,默认 50
27    pub limit: Option<usize>,
28    /// 结果偏移量,默认 0
29    pub offset: Option<usize>,
30}
31
32/// Flow 的轻量摘要,用于列表展示和 AI 快速分析。
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct FlowSummary {
35    pub id: String,
36    pub method: String,
37    pub url: String,
38    pub host: String,
39    pub path: String,
40    pub status: Option<u16>,
41    pub duration_ms: Option<u64>,
42    pub tags: Vec<String>,
43    pub start_time_ms: i64,
44    pub has_error: bool,
45    pub is_websocket: bool,
46}
47
48/// 对截获中的 Flow 的修改意图。
49/// 由各适配层(Tauri、MCP 等)在 resolve_intercept 时传入,
50/// 描述用户希望如何改变请求/响应/WebSocket 消息。
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct FlowModification {
53    // 请求字段
54    pub method: Option<String>,
55    pub url: Option<String>,
56    pub request_headers: Option<HashMap<String, String>>,
57    pub request_body: Option<String>,
58
59    // 响应字段
60    pub status_code: Option<u16>,
61    pub response_headers: Option<HashMap<String, String>>,
62    pub response_body: Option<String>,
63
64    // WebSocket 字段
65    pub message_content: Option<String>,
66}
67
68impl FlowModification {
69    pub fn is_empty(&self) -> bool {
70        self.method.is_none()
71            && self.url.is_none()
72            && self.request_headers.is_none()
73            && self.request_body.is_none()
74            && self.status_code.is_none()
75            && self.response_headers.is_none()
76            && self.response_body.is_none()
77            && self.message_content.is_none()
78    }
79
80    pub fn into_option(self) -> Option<Self> {
81        if self.is_empty() { None } else { Some(self) }
82    }
83
84    pub fn from_json_value(value: &Value) -> Self {
85        Self {
86            method: value
87                .get("method")
88                .and_then(Value::as_str)
89                .map(str::to_string),
90            url: value.get("url").and_then(Value::as_str).map(str::to_string),
91            request_headers: string_map_from_json(value.get("request_headers")),
92            request_body: value
93                .get("request_body")
94                .and_then(Value::as_str)
95                .map(str::to_string),
96            status_code: value
97                .get("status_code")
98                .and_then(Value::as_u64)
99                .map(|code| code as u16),
100            response_headers: string_map_from_json(value.get("response_headers")),
101            response_body: value
102                .get("response_body")
103                .and_then(Value::as_str)
104                .map(str::to_string),
105            message_content: value
106                .get("message_content")
107                .and_then(Value::as_str)
108                .map(str::to_string),
109        }
110    }
111}
112
113fn string_map_from_json(value: Option<&Value>) -> Option<HashMap<String, String>> {
114    value?.as_object().map(|entries| {
115        entries
116            .iter()
117            .filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
118            .collect()
119    })
120}
121
122#[cfg(test)]
123mod tests {
124    use super::FlowModification;
125    use serde_json::json;
126    use std::collections::HashMap;
127
128    #[test]
129    fn flow_modification_into_option_returns_none_when_empty() {
130        assert!(FlowModification::default().into_option().is_none());
131    }
132
133    #[test]
134    fn flow_modification_into_option_preserves_non_empty_payload() {
135        let modification = FlowModification {
136            request_body: Some("patched".to_string()),
137            ..Default::default()
138        };
139
140        assert_eq!(
141            modification.clone().into_option().unwrap().request_body,
142            modification.request_body
143        );
144    }
145
146    #[test]
147    fn flow_modification_from_json_value_reads_supported_fields() {
148        let modification = FlowModification::from_json_value(&json!({
149            "method": "PATCH",
150            "url": "http://example.com/new",
151            "request_headers": {
152                "X-Test": "1",
153                "X-Ignore": 2
154            },
155            "response_headers": {
156                "Content-Type": "application/json"
157            },
158            "request_body": "body",
159            "status_code": 202,
160            "response_body": "ok",
161            "message_content": "ws"
162        }));
163
164        assert_eq!(modification.method.as_deref(), Some("PATCH"));
165        assert_eq!(modification.url.as_deref(), Some("http://example.com/new"));
166        assert_eq!(
167            modification.request_headers,
168            Some(HashMap::from([("X-Test".to_string(), "1".to_string())]))
169        );
170        assert_eq!(
171            modification.response_headers,
172            Some(HashMap::from([(
173                "Content-Type".to_string(),
174                "application/json".to_string()
175            )]))
176        );
177        assert_eq!(modification.request_body.as_deref(), Some("body"));
178        assert_eq!(modification.status_code, Some(202));
179        assert_eq!(modification.response_body.as_deref(), Some("ok"));
180        assert_eq!(modification.message_content.as_deref(), Some("ws"));
181    }
182}