Skip to main content

j_agent/
chat_error.rs

1//! Chat 模块类型化错误
2//!
3//! 所有 chat 核心链路中的错误统一使用 `ChatError`,按语义分类,
4//! 便于 UI 层给出差异化提示(认证失败 vs 网络超时 vs 服务端错误等)。
5
6use crate::constants::{TEAMMATE_LOG_RESULT_MAX_CHARS, TEAMMATE_PROMPT_PREVIEW_MAX_CHARS};
7use crate::llm::LlmError;
8
9/// Chat 模块类型化错误
10#[derive(Debug)]
11pub enum ChatError {
12    // ── API 错误(按 HTTP 语义分类)──
13    /// 401/403 认证/授权失败
14    ApiAuth(String),
15    /// 429 请求过于频繁
16    ApiRateLimit {
17        message: String,
18        retry_after_secs: Option<u64>,
19    },
20    /// 400 请求参数错误
21    ApiBadRequest(String),
22    /// 5xx 服务端错误(504/502/500 等)
23    ApiServerError { status: u16, message: String },
24
25    // ── 网络错误 ──
26    /// 连接超时
27    NetworkTimeout(String),
28    /// DNS/TLS/连接拒绝等
29    NetworkError(String),
30
31    // ── 流式错误 ──
32    /// 流式响应中断(SSE 连接断开)
33    StreamInterrupted(String),
34    /// 流式反序列化失败(触发 fallback 重试)
35    StreamDeserialize(String),
36
37    // ── 请求构建 ──
38    /// 构建请求失败(参数无效等)
39    RequestBuild(String),
40
41    // ── Hook ──
42    /// 被 hook 中止
43    HookAborted,
44
45    // ── 运行时 ──
46    /// Agent 运行时错误
47    RuntimeFailed(String),
48    /// Agent 线程 panic
49    AgentPanic(String),
50
51    // ── 其他 ──
52    /// 异常 finish_reason(如 network_error)
53    AbnormalFinish(String),
54    /// 兜底
55    Other(String),
56}
57
58impl ChatError {
59    /// 清理后的用户可读消息(剥离 HTML,截断长度,提取状态码)
60    pub fn display_message(&self) -> String {
61        match self {
62            Self::ApiAuth(_) => "API 认证失败,请检查 API Key".to_string(),
63            Self::ApiRateLimit {
64                retry_after_secs, ..
65            } => {
66                if let Some(secs) = retry_after_secs {
67                    format!("请求过于频繁,请在 {} 秒后重试", secs)
68                } else {
69                    "请求过于频繁,请稍后重试".to_string()
70                }
71            }
72            Self::ApiBadRequest(msg) => {
73                format!("请求参数错误: {}", truncate(&sanitize_html(msg), 150))
74            }
75            Self::ApiServerError { status, .. } => {
76                format!("{} (HTTP {})", http_status_label(*status), status)
77            }
78            Self::NetworkTimeout(_) => "网络连接超时,请检查网络后重试".to_string(),
79            Self::NetworkError(_) => "网络连接失败,请检查网络设置".to_string(),
80            Self::StreamInterrupted(msg) => {
81                format!(
82                    "流式响应中断: {}",
83                    truncate(&sanitize_html(msg), TEAMMATE_PROMPT_PREVIEW_MAX_CHARS)
84                )
85            }
86            Self::StreamDeserialize(msg) => {
87                format!(
88                    "响应解析失败: {}",
89                    truncate(&sanitize_html(msg), TEAMMATE_PROMPT_PREVIEW_MAX_CHARS)
90                )
91            }
92            Self::RequestBuild(msg) => {
93                format!("构建请求失败: {}", truncate(msg, 150))
94            }
95            Self::HookAborted => "请求被 hook 中止".to_string(),
96            Self::RuntimeFailed(msg) => {
97                format!(
98                    "运行时错误: {}",
99                    truncate(msg, TEAMMATE_PROMPT_PREVIEW_MAX_CHARS)
100                )
101            }
102            Self::AgentPanic(msg) => {
103                format!(
104                    "Agent 异常: {}",
105                    truncate(msg, TEAMMATE_PROMPT_PREVIEW_MAX_CHARS)
106                )
107            }
108            Self::AbnormalFinish(reason) => {
109                format!("API 返回异常: finish_reason={}", truncate(reason, 80))
110            }
111            Self::Other(msg) => truncate(&sanitize_html(msg), TEAMMATE_LOG_RESULT_MAX_CHARS),
112        }
113    }
114}
115
116impl std::fmt::Display for ChatError {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match self {
119            Self::ApiAuth(msg) => write!(f, "API 认证失败: {}", msg),
120            Self::ApiRateLimit { message, .. } => write!(f, "API 请求过于频繁: {}", message),
121            Self::ApiBadRequest(msg) => write!(f, "API 请求参数错误: {}", msg),
122            Self::ApiServerError { status, message } => {
123                write!(f, "API 服务端错误 (HTTP {}): {}", status, message)
124            }
125            Self::NetworkTimeout(msg) => write!(f, "网络连接超时: {}", msg),
126            Self::NetworkError(msg) => write!(f, "网络错误: {}", msg),
127            Self::StreamInterrupted(msg) => write!(f, "流式响应中断: {}", msg),
128            Self::StreamDeserialize(msg) => write!(f, "流式反序列化失败: {}", msg),
129            Self::RequestBuild(msg) => write!(f, "构建请求失败: {}", msg),
130            Self::HookAborted => write!(f, "LLM 请求被 hook 中止"),
131            Self::RuntimeFailed(msg) => write!(f, "运行时错误: {}", msg),
132            Self::AgentPanic(msg) => write!(f, "Agent 异常: {}", msg),
133            Self::AbnormalFinish(reason) => write!(f, "API 返回异常: finish_reason={}", reason),
134            Self::Other(msg) => write!(f, "{}", msg),
135        }
136    }
137}
138
139impl std::error::Error for ChatError {}
140
141// ── 从 LlmError 转换 ──
142
143impl From<LlmError> for ChatError {
144    fn from(e: LlmError) -> Self {
145        match e {
146            LlmError::Http(re) => ChatError::from(re),
147            LlmError::Api { status, body } => {
148                // 尝试从 body 中解析 { "error": { "code": ..., "message": ... } }
149                if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&body) {
150                    let error_obj = parsed.get("error").unwrap_or(&parsed);
151                    let code = error_obj.get("code").and_then(|v| {
152                        v.as_str()
153                            .map(|s| s.to_string())
154                            .or_else(|| v.as_i64().map(|n| n.to_string()))
155                    });
156                    let message = error_obj
157                        .get("message")
158                        .and_then(|v| v.as_str())
159                        .unwrap_or(&body);
160                    ChatError::from_api_error(code.as_deref(), message)
161                } else {
162                    ChatError::from_http_status(status, sanitize_html(&body))
163                }
164            }
165            LlmError::Deserialize(msg) => ChatError::StreamDeserialize(truncate(&msg, 500)),
166            LlmError::StreamInterrupted(msg) => ChatError::StreamInterrupted(msg),
167            LlmError::RequestBuild(msg) => ChatError::RequestBuild(msg),
168        }
169    }
170}
171
172// ── 从 reqwest::Error 转换 ──
173
174impl From<reqwest::Error> for ChatError {
175    fn from(e: reqwest::Error) -> Self {
176        if e.is_timeout() {
177            ChatError::NetworkTimeout(e.to_string())
178        } else if let Some(status) = e.status() {
179            ChatError::from_http_status(status.as_u16(), e.to_string())
180        } else {
181            ChatError::NetworkError(e.to_string())
182        }
183    }
184}
185
186impl ChatError {
187    /// 根据 HTTP 状态码构造对应的 ChatError 变体
188    pub(crate) fn from_http_status(status: u16, message: String) -> Self {
189        match status {
190            401 | 403 => ChatError::ApiAuth(message),
191            429 => ChatError::ApiRateLimit {
192                message,
193                retry_after_secs: None,
194            },
195            400 => ChatError::ApiBadRequest(message),
196            500..=599 => ChatError::ApiServerError {
197                status,
198                message: sanitize_html(&message),
199            },
200            _ => ChatError::Other(format!("HTTP {}: {}", status, message)),
201        }
202    }
203
204    /// 从结构化 API 错误(code + message)转换
205    fn from_api_error(code: Option<&str>, message: &str) -> Self {
206        match code {
207            Some("rate_limit_exceeded") => ChatError::ApiRateLimit {
208                message: message.to_string(),
209                retry_after_secs: None,
210            },
211            Some("invalid_api_key") | Some("authentication_required") => {
212                ChatError::ApiAuth(message.to_string())
213            }
214            Some("invalid_request_error") => ChatError::ApiBadRequest(message.to_string()),
215            Some("1305") => ChatError::ApiRateLimit {
216                message: message.to_string(),
217                retry_after_secs: None,
218            },
219            Some(code) if code.contains("rpm_exceeded") || code.contains("tpm_exceeded") => {
220                ChatError::ApiRateLimit {
221                    message: message.to_string(),
222                    retry_after_secs: None,
223                }
224            }
225            _ => {
226                let msg_lower = message.to_lowercase();
227                if msg_lower.contains("api key")
228                    || msg_lower.contains("unauthorized")
229                    || msg_lower.contains("authentication")
230                {
231                    ChatError::ApiAuth(message.to_string())
232                } else if msg_lower.contains("rate limit")
233                    || msg_lower.contains("too many requests")
234                    || msg_lower.contains("访问量过大")
235                    || msg_lower.contains("请稍后再试")
236                    || msg_lower.contains("过载")
237                    || msg_lower.contains("overloaded")
238                    || msg_lower.contains("too busy")
239                    || msg_lower.contains("速率限制")
240                    || msg_lower.contains("网络错误")
241                    || msg_lower.contains("quota exceeded")
242                    || msg_lower.contains("concurrency limit")
243                    || msg_lower.contains("请求频率")
244                    || msg_lower.contains("busy")
245                    || msg_lower.contains("rpm_exceeded")
246                    || msg_lower.contains("rpm exceeded")
247                    || msg_lower.contains("tpm_exceeded")
248                    || msg_lower.contains("tpm exceeded")
249                    || msg_lower.contains("channel:rpm")
250                    || msg_lower.contains("channel:tpm")
251                {
252                    ChatError::ApiRateLimit {
253                        message: message.to_string(),
254                        retry_after_secs: None,
255                    }
256                } else if msg_lower.contains("invalid") || msg_lower.contains("bad request") {
257                    ChatError::ApiBadRequest(message.to_string())
258                } else {
259                    ChatError::Other(sanitize_html(message))
260                }
261            }
262        }
263    }
264}
265
266// ── 工具函数 ──
267
268/// 剥离 HTML 标签,保留纯文本内容
269fn sanitize_html(input: &str) -> String {
270    let mut result = String::with_capacity(input.len());
271    let mut in_tag = false;
272    for ch in input.chars() {
273        match ch {
274            '<' => in_tag = true,
275            '>' => {
276                in_tag = false;
277                // 标签闭合时插入空格,以便相邻标签内容之间有分隔
278                if !result.is_empty() && !result.ends_with(char::is_whitespace) {
279                    result.push(' ');
280                }
281            }
282            _ if !in_tag => result.push(ch),
283            _ => {}
284        }
285    }
286    // 清理多余空白
287    let collapsed: String = result.split_whitespace().collect::<Vec<_>>().join(" ");
288    collapsed
289}
290
291/// 截断超长字符串
292fn truncate(s: &str, max_len: usize) -> String {
293    if s.len() <= max_len {
294        return s.to_string();
295    }
296    // 尝试在字符边界截断
297    let mut end = max_len;
298    while !s.is_char_boundary(end) && end > 0 {
299        end -= 1;
300    }
301    format!("{}...", &s[..end])
302}
303
304/// HTTP 状态码 → 中文标签
305fn http_status_label(status: u16) -> &'static str {
306    match status {
307        500 => "服务端内部错误",
308        502 => "网关错误",
309        503 => "服务暂不可用",
310        504 => "网关超时",
311        529 => "服务过载",
312        _ => "服务端错误",
313    }
314}