Skip to main content

relay_core_api/
modification.rs

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