Skip to main content

relay_core_api/
modification.rs

1use std::collections::HashMap;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
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() {
78            None
79        } else {
80            Some(self)
81        }
82    }
83
84    pub fn from_json_value(value: &Value) -> Self {
85        Self {
86            method: value.get("method").and_then(Value::as_str).map(str::to_string),
87            url: value.get("url").and_then(Value::as_str).map(str::to_string),
88            request_headers: string_map_from_json(value.get("request_headers")),
89            request_body: value.get("request_body").and_then(Value::as_str).map(str::to_string),
90            status_code: value.get("status_code").and_then(Value::as_u64).map(|code| code as u16),
91            response_headers: string_map_from_json(value.get("response_headers")),
92            response_body: value.get("response_body").and_then(Value::as_str).map(str::to_string),
93            message_content: value.get("message_content").and_then(Value::as_str).map(str::to_string),
94        }
95    }
96}
97
98fn string_map_from_json(value: Option<&Value>) -> Option<HashMap<String, String>> {
99    value?.as_object().map(|entries| {
100        entries
101            .iter()
102            .filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
103            .collect()
104    })
105}
106
107#[cfg(test)]
108mod tests {
109    use super::FlowModification;
110    use serde_json::json;
111    use std::collections::HashMap;
112
113    #[test]
114    fn flow_modification_into_option_returns_none_when_empty() {
115        assert!(FlowModification::default().into_option().is_none());
116    }
117
118    #[test]
119    fn flow_modification_into_option_preserves_non_empty_payload() {
120        let modification = FlowModification {
121            request_body: Some("patched".to_string()),
122            ..Default::default()
123        };
124
125        assert_eq!(
126            modification.clone().into_option().unwrap().request_body,
127            modification.request_body
128        );
129    }
130
131    #[test]
132    fn flow_modification_from_json_value_reads_supported_fields() {
133        let modification = FlowModification::from_json_value(&json!({
134            "method": "PATCH",
135            "url": "http://example.com/new",
136            "request_headers": {
137                "X-Test": "1",
138                "X-Ignore": 2
139            },
140            "response_headers": {
141                "Content-Type": "application/json"
142            },
143            "request_body": "body",
144            "status_code": 202,
145            "response_body": "ok",
146            "message_content": "ws"
147        }));
148
149        assert_eq!(modification.method.as_deref(), Some("PATCH"));
150        assert_eq!(modification.url.as_deref(), Some("http://example.com/new"));
151        assert_eq!(
152            modification.request_headers,
153            Some(HashMap::from([("X-Test".to_string(), "1".to_string())]))
154        );
155        assert_eq!(
156            modification.response_headers,
157            Some(HashMap::from([(
158                "Content-Type".to_string(),
159                "application/json".to_string()
160            )]))
161        );
162        assert_eq!(modification.request_body.as_deref(), Some("body"));
163        assert_eq!(modification.status_code, Some(202));
164        assert_eq!(modification.response_body.as_deref(), Some("ok"));
165        assert_eq!(modification.message_content.as_deref(), Some("ws"));
166    }
167}