重构: AI流式断线保文StreamResult三分支+重试对齐决策a1,推进链落df-nodes

This commit is contained in:
2026-06-16 19:11:19 +08:00
parent 7d5cd4c89a
commit ba7f35552b
30 changed files with 1325 additions and 283 deletions

View File

@@ -6,7 +6,10 @@ pub mod context;
pub mod coordinator;
pub mod openai_compat;
pub mod provider;
mod retry;
// CR-30-1: 流前重试退避对外复用。complete() 的 retry_with_backoff 仍 crate 内用,
// stream_recv/agentic 流前重试需复用 backoff_delay(jitter)+is_status_retryable(Fatal 分类)
// 避免重写退避/分类逻辑(对齐决策 F-260616-07 a1)。改 pub mod 后对外仅暴露纯函数 + 常量。
pub mod retry;
use provider::LlmProvider;

View File

@@ -37,8 +37,10 @@ const MAX_TOTAL_BUDGET: Duration = Duration::from_secs(30);
const JITTER_RATIO: f64 = 0.2;
/// 一次尝试的分类结果 —— 在 anyhow 不透明化前决定是否值得重试。
///
/// CR-30-1: 暴露给 agentic.rs 流前重试复用(Init 失败分类 Fatal/Retryable 决定是否重试)。
#[derive(Debug)]
pub(super) enum AttemptOutcome<T> {
pub enum AttemptOutcome<T> {
/// 成功,携带结果。
Ok(T),
/// 可重试错误(timeout / connect / 5xx / 429)。携带人类可读错误描述。
@@ -50,12 +52,14 @@ pub(super) enum AttemptOutcome<T> {
/// 判定 reqwest::Error 是否可重试(仅 timeout / connect 类)。
///
/// body 解析错(reqwest::Error::is_decode)与请求构造错(is_builder)属 Fatal不重试。
pub(super) fn is_reqwest_error_retryable(e: &reqwest::Error) -> bool {
pub fn is_reqwest_error_retryable(e: &reqwest::Error) -> bool {
e.is_timeout() || e.is_connect() || (e.is_request() && !e.is_builder())
}
/// 判定 HTTP 状态码是否可重试: 5xx 与 429 重试,其余(含 4xx)不重试。
pub(super) fn is_status_retryable(status: u16) -> bool {
///
/// CR-30-1: 暴露给 agentic.rs 流前重试复用(4xx Fatal 立即放弃,5xx/429 重试)。
pub fn is_status_retryable(status: u16) -> bool {
status == 429 || (500..600).contains(&status)
}
@@ -64,7 +68,10 @@ pub(super) fn is_status_retryable(status: u16) -> bool {
///
/// jitter 用 SystemTime 纳秒取模生成(无依赖),避免多客户端同步重试风暴。
/// 以毫秒粒度计算后向下取整(避免秒级截断把 0.9s 砍成 0)。
fn backoff_delay(attempt: u32) -> Duration {
///
/// CR-30-1: 暴露 pub 供 src-tauri/agentic.rs 流前重试复用(对齐决策 F-260616-07 a1
/// "复用 retry.rs backoff_delay 退避 1s→2s→4s+jitter"),避免重写退避逻辑。
pub fn backoff_delay(attempt: u32) -> Duration {
let base_ms = BASE_BACKOFF_SECS.saturating_mul(1u64 << (attempt - 1)) * 1000;
// 纳秒 → [0, 2000) 区间,再映射到 [-1.0, +1.0) 比例
let nanos = SystemTime::now()
@@ -85,7 +92,11 @@ fn backoff_delay(attempt: u32) -> Duration {
///
/// 调用方约定: `attempt_fn` 内部应保留完整请求重发能力(每次重建 RequestBuilder)
/// 因为 reqwest::RequestBuilder 一次 `.send()` 后不可复用。
pub(super) async fn retry_with_backoff<T, F, Fut>(
///
/// CR-30-1: 仍 pub(crate)(complete() 内部用),流前重试 stream_recv/agentic 不走此函数
/// (流式 request 不可整体 retry_with_backoff 包裹,需在 agentic loop 内手写循环复用
/// backoff_delay + is_status_retryable)。
pub(crate) async fn retry_with_backoff<T, F, Fut>(
label: &str,
attempt_fn: F,
) -> anyhow::Result<T>