优化: 边界加固(AI loop竞态根治+数据/审批/反馈/并发/错误分类)
AI loop 竞态(P0):per-conv epoch/owner token + 存活心跳治 force_send 双loop + stop 3s兜底误判;旧loop stale 全跳过(guard/emit/save)
agentic 收尾(A2-B8):Fatal 退出落库user消息(镜像Exhausted)+ 入口早退补save + usage is_estimated 打标 + emit_ai_completed_once 单点收敛清审批残留
聊天清理(A2-B9):clearChat 先停loop→DB单事务→内存清(clear_conversation_atomic)+ 前端错误气泡
循环并发(A2-B11):三态 ProviderAcquire(NotConfigured/Acquired/Exhausted)+ 候选循环非阻塞+防抖3次饱和降级+单测
错误分类(A2-B12):stream error帧接入 classify_status_or_class + 关键词保守降级 + 7单测
数据(G1.2/G1.4):purge_with_descendants 级联补全(11表单事务+存在性守卫)+ move_task_queue 单事务收口(两调用方共用)
git只读(G3.1):run_git_status/diff/log success判定(exit_code差异语义,失败结构化{success:false,error})
安全(G5.2/G5.6):create_project 目录Err+name校验 + module.rs 路径遍历DRY(分段匹配修a..b.rs误伤)
幂等(V2/V32):裸ALTER全守卫化 + v1..v40全链重跑幂等测试(16过)
附:remote_bridge await 临时引用修(E0716)+ agentic emit 收敛 E0716 app_state 绑定修
This commit is contained in:
@@ -374,20 +374,33 @@ pub(crate) async fn stream_llm(
|
||||
}
|
||||
// provider 流式错误事件
|
||||
if let Some(err_msg) = &chunk.error {
|
||||
// A2-B12 / G4.2:error 帧按 message 文本分类 retryable。
|
||||
// 原硬编码 retryable:true,确定性请求错(鉴权/context_length/invalid_request)
|
||||
// 被当瞬态空耗重试。现抠 HTTP 状态码/传输类走既有 classify_status_or_class
|
||||
// 单一分类源;无码则按错误类型关键词保守降级(仅明确非重试降 Fatal)。
|
||||
let (status_or_class, _raw) = extract_error_diag_from_str(err_msg);
|
||||
let retryable = classify_error_frame_retryable(err_msg, &status_or_class);
|
||||
let diag_class = if status_or_class == "unknown" {
|
||||
"stream-error"
|
||||
} else {
|
||||
status_or_class.as_str()
|
||||
};
|
||||
warn!(
|
||||
provider = %provider.name(),
|
||||
conv_id = %conv_id,
|
||||
error = %err_msg,
|
||||
status_or_class = %status_or_class,
|
||||
retryable = %retryable,
|
||||
text_len = full_text.len(),
|
||||
"[ai] provider 流式错误事件",
|
||||
);
|
||||
if full_text.is_empty() && tool_calls_acc.is_empty() {
|
||||
return StreamResult::InitFailed {
|
||||
retryable: true,
|
||||
retryable,
|
||||
error: fmt_diag(
|
||||
provider.name(),
|
||||
DiagKind::MidStream,
|
||||
"stream-error",
|
||||
diag_class,
|
||||
err_msg,
|
||||
),
|
||||
};
|
||||
@@ -544,6 +557,52 @@ fn classify_status_or_class(status_or_class: &str) -> bool {
|
||||
|| lower.contains("broken pipe")
|
||||
}
|
||||
|
||||
/// 据 provider 流式 error 帧的 message 文本分类是否可重试(A2-B12 / G4.2)。
|
||||
///
|
||||
/// openai/anthropic helper 仅将 error 帧的 `message` 字段透传进 chunk.error
|
||||
/// (结构化 `type` 未上抛),故此处按 message 文本保守分类,对齐 Err 路径单一分类源:
|
||||
/// - 内含显式 HTTP 状态码或传输类(timeout/connect) → 复用 classify_status_or_class
|
||||
/// - 无状态码时按错误类型关键词:明确非重试签名(authentication / context_length /
|
||||
/// invalid_request / 鉴权 / 参数错等) → Fatal
|
||||
/// - 其余 → retryable=true(保守,防误判瞬态为 Fatal)
|
||||
fn classify_error_frame_retryable(msg: &str, status_or_class: &str) -> bool {
|
||||
// 已有状态码/传输类 → 走既有单一分类源(显式 4xx Fatal,5xx/429/timeout/connect 可重试)
|
||||
if status_or_class != "unknown" {
|
||||
return classify_status_or_class(status_or_class);
|
||||
}
|
||||
// 无状态码:按错误类型关键词明确非重试签名降级 Fatal
|
||||
let lower = msg.to_lowercase();
|
||||
const FATAL_KEYWORDS: &[&str] = &[
|
||||
// OpenAI / Anthropic error.type
|
||||
"authentication_error",
|
||||
"invalid_request_error",
|
||||
"context_length_exceeded",
|
||||
"permission_error",
|
||||
"not_found_error",
|
||||
"request_too_large",
|
||||
"insufficient_quota",
|
||||
// OpenAI 消息文本签名
|
||||
"authentication",
|
||||
"invalid_request",
|
||||
"invalid request",
|
||||
"context length",
|
||||
"api key",
|
||||
"api-key",
|
||||
"api_key",
|
||||
"unauthorized",
|
||||
// 中文签名(BUG-260617-01:Anthropic 中文参数错误)
|
||||
"鉴权",
|
||||
"认证失败",
|
||||
"未授权",
|
||||
"密钥",
|
||||
"参数非法",
|
||||
"参数错误",
|
||||
"上下文长度",
|
||||
"输入超长",
|
||||
];
|
||||
!FATAL_KEYWORDS.iter().any(|k| lower.contains(k))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 单测:诊断提取/格式化(纯函数,不发 HTTP、不依赖 app_handle)
|
||||
// ============================================================
|
||||
@@ -589,6 +648,63 @@ mod tests {
|
||||
assert!(!classify_status_or_class("unknown"));
|
||||
}
|
||||
|
||||
// ---- classify_error_frame_retryable:A2-B12 / G4.2 error 帧分类 ----
|
||||
|
||||
/// error 帧 message 含显式 4xx → Fatal(接入 classify_status_or_class 单一分类源)
|
||||
#[test]
|
||||
fn error_frame_http_4xx_fatal() {
|
||||
let (s, _) = extract_error_diag_from_str("LLM 流式 API 错误 401: Unauthorized");
|
||||
assert!(!classify_error_frame_retryable("LLM 流式 API 错误 401: Unauthorized", &s));
|
||||
}
|
||||
|
||||
/// error 帧 message 含显式 5xx / 429 → retryable
|
||||
#[test]
|
||||
fn error_frame_http_5xx_retryable() {
|
||||
let (s, _) = extract_error_diag_from_str("LLM 流式 API 错误 500: Internal Server Error");
|
||||
assert!(classify_error_frame_retryable("LLM 流式 API 错误 500: Internal Server Error", &s));
|
||||
let (s2, _) = extract_error_diag_from_str("rate limit 429");
|
||||
assert!(classify_error_frame_retryable("rate limit 429", &s2));
|
||||
}
|
||||
|
||||
/// error 帧 message 含 timeout → retryable(瞬态)
|
||||
#[test]
|
||||
fn error_frame_timeout_retryable() {
|
||||
let (s, _) = extract_error_diag_from_str("operation timed out");
|
||||
assert!(classify_error_frame_retryable("operation timed out", &s));
|
||||
}
|
||||
|
||||
/// authentication / api key 签名 → Fatal
|
||||
#[test]
|
||||
fn error_frame_authentication_fatal() {
|
||||
let (s, _) = extract_error_diag_from_str("Incorrect API key provided: sk-xxx");
|
||||
assert!(!classify_error_frame_retryable("Incorrect API key provided: sk-xxx", &s));
|
||||
let (s2, _) = extract_error_diag_from_str("invalid x-api-key");
|
||||
assert!(!classify_error_frame_retryable("invalid x-api-key", &s2));
|
||||
}
|
||||
|
||||
/// context_length_exceeded / context length 签名 → Fatal
|
||||
#[test]
|
||||
fn error_frame_context_length_fatal() {
|
||||
let (s, _) = extract_error_diag_from_str("This model's maximum context length is 128000 tokens");
|
||||
assert!(!classify_error_frame_retryable("This model's maximum context length is 128000 tokens", &s));
|
||||
}
|
||||
|
||||
/// invalid_request 签名 → Fatal
|
||||
#[test]
|
||||
fn error_frame_invalid_request_fatal() {
|
||||
let (s, _) = extract_error_diag_from_str("invalid_request_error: the model is not supported");
|
||||
assert!(!classify_error_frame_retryable("invalid_request_error: the model is not supported", &s));
|
||||
}
|
||||
|
||||
/// 无明确签名(overloaded / stream error 等瞬态语义)→ 保守 retryable=true(防误判 Fatal 错杀)
|
||||
#[test]
|
||||
fn error_frame_unknown_conservative_retryable() {
|
||||
let (s, _) = extract_error_diag_from_str("overloaded_error: Overloaded");
|
||||
assert!(classify_error_frame_retryable("overloaded_error: Overloaded", &s));
|
||||
let (s2, _) = extract_error_diag_from_str("stream error");
|
||||
assert!(classify_error_frame_retryable("stream error", &s2));
|
||||
}
|
||||
|
||||
// ---- extract_error_diag:业务层 bail(provider 串已含状态码)----
|
||||
|
||||
/// provider 在 non-2xx bail 的典型串:抠出 401(鉴权失败/Key 错)
|
||||
|
||||
Reference in New Issue
Block a user