package voice import ( "errors" "fmt" ) // voice 包返回的哨兵错误。每个错误都以 %w 包装, // 使调用方能按类别判断,同时保留原始原因。 var ( // ErrProviderFailed 在一条链上所有提供方都失败时返回。 ErrProviderFailed = errors.New("voice: all providers failed") // ErrNotConfigured 在提供方缺少 endpoint/配置项时返回。 ErrNotConfigured = errors.New("voice: provider not configured") // ErrNotImplemented 由适配点提供方返回。 ErrNotImplemented = errors.New("voice: provider not implemented") // ErrEmptyText 在 TTS 收到空文本时返回。 ErrEmptyText = errors.New("voice: empty text") // ErrClosed 在 Close 之后仍有服务调用到达时返回。 ErrClosed = errors.New("voice: service closed") // 下面三条是**可分类的上游失败**(切片 IM-ASR 的熔断需要)。它们各自以 // ErrProviderFailed 为底(`%w`),因此既有的 `errors.Is(err, ErrProviderFailed)` // 判定逐字不变,而需要「这条失败再试也是白试吗」的调用方可以再细分一层。 // // 分类依据是 DashScope 的 HTTP 状态码与错误码(官方口径),**绝不**匹配自由文本 // 描述:把「格式不支持」误判成「欠费」会让一条被拒的语音关掉整个转写面。 // // 归入这三类的失败打开**临时熔断**(未配置同理,见 CircuitWorthy):它们是 // 「账户/配额/权限」层面的问题,逐条重试只会白等超时并反复撞付费墙。 // // ErrAuthFailed 报告云端鉴权失败(HTTP 401/403,例如 API key 无效或被吊销)。 ErrAuthFailed = fmt.Errorf("%w: 云端鉴权失败(API key 被拒)", ErrProviderFailed) // ErrQuotaExceeded 报告云端欠费或额度耗尽(例如阿里云的 Arrearage / // AllocatedQuotaExceeded)。 ErrQuotaExceeded = fmt.Errorf("%w: 云端账户欠费或额度耗尽", ErrProviderFailed) // ErrRateLimited 报告云端限流(HTTP 429 / Throttling.*)。 ErrRateLimited = fmt.Errorf("%w: 云端限流", ErrProviderFailed) ) // CircuitWorthy 报告一次失败是否属于「**再试也是白试**」的类别:未配置 / 鉴权失败 / // 欠费 / 限流。只有这些才值得熔断退避。 // // 「这份音频格式不支持」「解码失败」「音频为空」这类**按内容发生**的失败返回 false: // 它们不该关掉整个转写面(否则一条被拒的语音会让后续语音全部降级)。判据只认哨兵链。 func CircuitWorthy(err error) bool { switch { case err == nil: return false case errors.Is(err, ErrNotConfigured), errors.Is(err, ErrAuthFailed), errors.Is(err, ErrQuotaExceeded), errors.Is(err, ErrRateLimited): return true default: return false } }