Files
Cyrene_v2/internal/runtime/multimodal/voice/errors.go
T
AskaEth a918819878
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(channels,voice): IM 各渠道语音消息可被读到(全渠道云端 ASR + 临时熔断)
采集面:mediafetch 新增 inline_voice.go(音频白名单,与 documents.ExtAllowed 互不污染;
复用既有来源白名单 / 逐跳校验 / 边读边限 / 超时);四个适配器零改动。
消费面:channelbridge 在流水线最前端把「语音字节 → 文本」归一并写回 msg.Content;
inbound.go 的丢空判据放宽为「无文本且无可用媒体」。
微信优先平台自带转写(Content 非空 ⇒ 不调 ASR,不重复付费)。
降级(主人定的两层):新增 voice/outcome.go 的 STTOutcome 识破静默档位;
errors.go 分类鉴权失败/欠费/限流(CircuitWorthy);voice_breaker.go 临时熔断 + 退避(TTL 恢复);
无论是否熔断,消息面都落一行中文诚实提示(绝不静默、绝不留空消息)。
独立短超时:VOICE_STT_TIMEOUT_SEC(默认 20s,同步 .env.example)。
kook 不再把 CDN 地址写进 Content(地址仍在 Media[].URL)。
设计先行:design/06 §1.1/§1.2、design/08 §4.1、design/24 §1.3/§4.2、开源对照清单。
变异 5 条(静默档位不带 Cause / Transcribed 误判 / 熔断恒放行 / ChatText 退化 / 平台文字判据取反)。
诚实边界:实施时官方音频规格尚未抓到 ⇒ MaxVoiceBytes 自定 5 MiB(保守)、采样率仍 16 kHz;
官方规格随后已抓到(10 MB / 5 min;格式含 amr、无 silk),对齐留待下次改动。
2026-10-05 08:32:37 +08:00

59 lines
2.7 KiB
Go

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
}
}