优化: 边界加固(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:
lxy
2026-08-05 22:10:32 +08:00
parent ec9f0bf1ea
commit 5667da6cf4
16 changed files with 1696 additions and 215 deletions
+118 -2
View File
@@ -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_retryableA2-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:业务层 bailprovider 串已含状态码)----
/// provider 在 non-2xx bail 的典型串:抠出 401(鉴权失败/Key 错)