// 错误分类器 -- Provider Adapter 模式实现. // // 升华改进(ELEVATED): 早期方案把 Anthropic 特定的 header 解析,消息匹配硬编码在通用错误处理中, // 导致无法支持其他供应商(OpenAI / Bedrock / 本地模型). // // 本设计:ErrorClassifier 接口定义分类契约,每个供应商实现自己的 Classifier. // 引擎只依赖 ErrorCategory 枚举,不依赖任何供应商特定的字符串. // // 替代方案:<原方案所有供应商的逻辑混在一个 500 行 if-else 中> package api import ( "net/http" "strconv" "strings" "time" "git.flytoex.net/yuanwei/flyto-agent/core/internal/apierror" ) // ============================================================ // ErrorClassifier 接口 // ============================================================ // ErrorClassifier 将原始 HTTP 响应或连接错误分类为结构化的 APIError. // // 精妙之处(CLEVER): 接口只有一个方法--简单就是最好的抽象. // 不同供应商实现不同的 Classify 逻辑,但返回统一的 *APIError. // 支持 CompositeClassifier 叠加(宪法第8条:叠加而非替换). type ErrorClassifier interface { // Classify 分类一个 API 错误. // statusCode: HTTP 状态码(连接错误时为 0) // headers: 响应头(连接错误时为 nil) // body: 响应体(连接错误时为 nil) // cause: 底层错误(网络错误时非 nil) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError } // ============================================================ // DefaultClassifier - 纯 HTTP 状态码分类 // ============================================================ // DefaultClassifier is the protocol-neutral HTTP status-code classifier. Its // implementation now lives in the apierror package so the wire path can reuse // it too; this alias keeps api.DefaultClassifier available and satisfies the // ErrorClassifier interface unchanged (the Classify return type resolves to // the same *apierror.APIError via the APIError alias above). // // DefaultClassifier 是协议无关的 HTTP 状态码分类器. 实现已移至 apierror 包 // 以便 wire 路径复用; 此别名保留 api.DefaultClassifier 且不改变对 ErrorClassifier // 接口的满足 (Classify 返回类型经上方 APIError 别名解析为同一 *apierror.APIError). type DefaultClassifier = apierror.DefaultClassifier // ============================================================ // AnthropicClassifier - Anthropic 供应商特定分类 // ============================================================ // AnthropicClassifier 在 DefaultClassifier 基础上增加 Anthropic 特定的分类逻辑: // - 解析 anthropic-ratelimit-* header // - 检测 overloaded_error(SDK 有时丢失 529 状态码) // - 解析 x-should-retry header // - SSL 证书错误诊断 // // 精妙之处(CLEVER): 先调 DefaultClassifier 做基础分类,再用 Anthropic 特定信息增强. // 这样即使 Anthropic header 格式变了,基础分类仍然正确(降级而非崩溃). type AnthropicClassifier struct { // Hinter 提供连接诊断提示(可选) Hinter DiagnosticHinter } // Classify 分类 API 错误,增强 Anthropic 特定信息. func (c *AnthropicClassifier) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError { // 先用默认分类器做基础分类 base := (&DefaultClassifier{}).Classify(statusCode, headers, body, cause) // 增强:确保 overloaded 有重试信息 // DefaultClassifier.classifyBadRequest 已经能识别 errType == "overloaded_error", // 这里补充确保 Retry 被设置. if base.ErrCategory == ErrOverloaded && base.Retry == nil { base.Retry = &RetryInfo{Retryable: true} } // 增强:429 细分 - 解析 Anthropic 统一限流 header if base.ErrCategory == ErrRateLimit && headers != nil { base.Retry = c.parseAnthropicRateLimit(headers, base.Retry) } // 增强:x-should-retry header(Anthropic 特有) if headers != nil { if shouldRetry := ParseShouldRetry(headers.Get("x-should-retry")); shouldRetry != nil { if base.Retry == nil { base.Retry = &RetryInfo{} } base.Retry.ServerSaid = shouldRetry base.Retry.Retryable = *shouldRetry } } // 增强:prompt_too_long 时提取 token 差值 if base.ErrCategory == ErrPromptTooLong { base.TokenGap = ParseTokenGap(base.Msg) } // 增强:连接错误诊断提示 if base.ErrCategory == ErrSSL || base.ErrCategory == ErrConnection { if c.Hinter != nil { base.Hint = c.Hinter.Hint(base) } } return base } // parseAnthropicRateLimit 解析 Anthropic 统一限流 header. // // Header 字段: // - anthropic-ratelimit-unified-representative-claim: "five_hour" | "seven_day" | ... // - anthropic-ratelimit-unified-overage-status: "allowed" | "rejected" // - anthropic-ratelimit-unified-reset: Unix timestamp (seconds) // - retry-after: seconds func (c *AnthropicClassifier) parseAnthropicRateLimit(headers http.Header, existing *RetryInfo) *RetryInfo { info := existing if info == nil { info = &RetryInfo{} } // retry-after(标准 HTTP header) if ra := headers.Get("Retry-After"); ra != "" { info.After = ParseRetryAfter(ra) } // anthropic-ratelimit-unified-reset(Anthropic 特有) if reset := headers.Get("anthropic-ratelimit-unified-reset"); reset != "" { // 值是 Unix 秒时间戳 // 精妙之处(CLEVER): 用 reset 时间戳计算精确等待时间, // 比 retry-after 更准确(retry-after 可能是近似值). // 但需要客户端时钟大致准确. // 如果已经有 retry-after 且更长,保留更长的那个(保守策略). resetDuration := parseResetTimestamp(reset) if resetDuration > info.After { info.After = resetDuration } } // overage-status(判断是否可以通过 overage 解决) overageStatus := headers.Get("anthropic-ratelimit-unified-overage-status") if overageStatus == "rejected" { // 被明确拒绝,不建议自动重试 info.Retryable = false } else { info.Retryable = info.After > 0 // 有 retry-after 才重试 } return info } // parseResetTimestamp 将 Unix 秒时间戳转换为从现在起的等待时间. func parseResetTimestamp(value string) time.Duration { // 使用 time 包的当前时间--不可测试性的代价换取简洁性. // 如果需要可测试性,可注入时间源. timestamp := parseInt64(value) if timestamp <= 0 { return 0 } delay := time.Until(time.Unix(timestamp, 0)) if delay < 0 { return 0 // 已经过了 reset 时间 } return delay } // parseInt64 安全地解析 int64 字符串. func parseInt64(s string) int64 { s = strings.TrimSpace(s) v, err := parseInt(s) if err != nil { return 0 } return v } // parseInt 包装 strconv.ParseInt(避免重复写错误处理). func parseInt(s string) (int64, error) { return parseIntFunc(s, 10, 64) } // parseIntFunc 是 strconv.ParseInt 的别名(同包可直接用,此处为语义清晰). var parseIntFunc = strconv.ParseInt // ============================================================ // CompositeClassifier - 叠加分类器(宪法第8条) // ============================================================ // CompositeClassifier 按优先级组合多个分类器. // 第一个返回非 ErrUnknown 分类的 Classifier 胜出. // // 升华改进(ELEVATED): 支持运行时动态添加分类器-- // 例如仓储场景可以 Add 一个识别 PLC 连接错误的分类器, // 而不需要修改核心代码. type CompositeClassifier struct { classifiers []ErrorClassifier } // NewCompositeClassifier 创建组合分类器. func NewCompositeClassifier(classifiers ...ErrorClassifier) *CompositeClassifier { return &CompositeClassifier{classifiers: classifiers} } // Add 添加分类器(追加到末尾,优先级最低). func (c *CompositeClassifier) Add(classifier ErrorClassifier) { c.classifiers = append(c.classifiers, classifier) } // Classify 按顺序调用分类器,第一个给出明确分类的胜出. func (c *CompositeClassifier) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError { var last *APIError for _, classifier := range c.classifiers { result := classifier.Classify(statusCode, headers, body, cause) if result == nil { continue } if result.ErrCategory != ErrUnknown { return result } last = result // 保留最后一个结果作为 fallback } if last != nil { return last } // 所有分类器都没结果,返回兜底 return &APIError{ ErrCategory: ErrUnknown, StatusCode: statusCode, Msg: string(body), RespHeaders: headers, Body: string(body), Cause: cause, } }