package adapter import "errors" // 本文件给**出站投递失败**一个稳定的原因码。此前一次失败只有自由文本(真机日志: // `weixin_ilink: proactive push outside 24h window`),调用方只能把整串原文塞进日志与 // 记录 —— 于是「她的话没送到微信」在面板上只表现为一句人读的英文,既没法稳定地告诉 // 主人「哪个渠道、因为哪条平台规则」,也没法按原因聚合。 // // 口径与 internal/runtime/voice 的 `Category(err)` 同族(那处给语音转码失败分类): // **产生错误的那一层**声明码值,读取侧只调一个访问函数;错误**没有自报**更具体的 // 原因码时一律落到兜底码 CategoryFailed —— 绝不替它编一个更精确的码。 // // 为什么放在 adapter 契约层:能见到真实错误的层(plugin 适配器)与要记录它的层 // (channelbridge / backthink)之间只共享本包。契约层是唯一不需要任何一方反向依赖 // 具体插件的落点。范式与 SystemNoticeNormalizer 逐字同族:**可选接口 + 调用方探测**。 // DeliveryCategory 是出站投递失败的**稳定原因码**(snake_case,可直接进日志字段、 // 记录字段与审计文案)。机器读它,人读文案 —— 所以文案会变,码不变。 type DeliveryCategory string // 契约层自带的四个码。平台特有的码(如 weixin-ilink 的 proactive_window_closed) // 由各插件在自己的包内声明并自报,本包不枚举它们。 const ( // CategoryNotConnected 表示适配器当前没有可用会话(ErrNotConnected)。 CategoryNotConnected DeliveryCategory = "not_connected" // CategoryUnsupported 表示该能力/操作不被平台支持(ErrUnsupported)。 CategoryUnsupported DeliveryCategory = "unsupported" // CategoryClosed 表示适配器已关闭(ErrClosed)。 CategoryClosed DeliveryCategory = "closed" // CategoryFailed 是**兜底**码:错误没有自报更具体的原因码时使用。它如实表示 // 「只知道失败了,不知道平台规则层面的原因」,既不是成功,也不是猜测。 CategoryFailed DeliveryCategory = "failed" ) // DeliveryFailure 是投递错误**可选**的自我描述(适配器按需实现,调用方用 // errors.As 探测)。实现必须无副作用:不做 I/O、不改状态。 type DeliveryFailure interface { // DeliveryCategory 返回该错误的稳定原因码。返回空串表示「不自报」,读取侧 // 退到共享哨兵判定与兜底码。 DeliveryCategory() DeliveryCategory // DeliveryChannel 返回出错渠道的平台名(例如 "weixin-ilink")。空串表示 // **不知道**——绝不填一个猜出来的平台名。 DeliveryChannel() string } // DeliveryCategoryOf 返回 err 的稳定原因码:err 自报则用自报值,否则按契约层 // 哨兵判定,最后落到 CategoryFailed。err 为 nil 时返回空串 —— 「没有失败」不是 // 一种失败码。 // // errors.As 沿 %w 链走,因此中间层的包装(channelbridge 的 // `send bubble #1 of 8: …`、`send proactive via "weixin-ilink": …`)都不会把 // 自报的码藏起来。 func DeliveryCategoryOf(err error) DeliveryCategory { if err == nil { return "" } if f := deliveryFailureOf(err); f != nil { if c := f.DeliveryCategory(); c != "" { return c } } switch { case errors.Is(err, ErrNotConnected): return CategoryNotConnected case errors.Is(err, ErrUnsupported): return CategoryUnsupported case errors.Is(err, ErrClosed): return CategoryClosed default: return CategoryFailed } } // DeliveryChannelOf 返回 err 自报的出错渠道;未自报时返回空串(= 未知)。 func DeliveryChannelOf(err error) string { if f := deliveryFailureOf(err); f != nil { return f.DeliveryChannel() } return "" } // deliveryFailureOf 在错误链上找出第一个自描述失败;没有则返回 nil。 func deliveryFailureOf(err error) DeliveryFailure { var f DeliveryFailure if errors.As(err, &f) { return f } return nil }