package httpclient // 本文件是**渠道侧上游自由文本的唯一清洗入口**(M50)。它管的是同一件事在四个出口上 // 的形态:KOOK 一类 `{code,message}` 信封的 `message`、Telegram Bot API 的 // `description`、OneBot v11 信封的 `msg` / `wording`、iLink CDN 的 `x-error-message` // 头。四者都是上游给的**人话**,都可能把被拒的请求内容回显出来 —— 而请求内容里就有 // 她的原话。 // // 🔴 契约(改本文件前先读): // - 上层口径是 **默认不带**:这些字段默认不进 error、不进日志(见 client.go 的 // WithEnvelopeMessage 与各渠道调用点的注释)。本文件的函数**只在调用方已经显式 // 选择保留**时才被调用 —— 它是「保留时的上限」,不是「可以带」的许可。 // - 清洗只有一份:`trace.RedactTextLimited`(凭据形状打码 + rune 截断)。本包**不** // 另写一套正则或截断,另写一套必然漂移,而漂移的方向总是「其中一条出口开始原样 // 吐凭据」。 // - 压平空白也是必需的一步:一条诊断只许占一行,否则上游可以用换行伪造日志行 // (log injection)。 import ( "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // maxUpstreamTextRunes 是上游自由文本允许保留的 rune 上限:**40**。 // // 为什么比 `trace.RedactText` 的 80 更短:那一档服务的是**摘要素材**(出图、工具结果 // 摘要),读者只看这一条;本文件的产物进的是**错误链与日志**,同一条信息还会被调用方 // 再落一次,长度预算没有理由按摘要给。而 40 rune 已足够装下渠道侧真正有用的那类说明 // (`invalid token`、`参数错误`、`缺少 code`、`FLOOD_WAIT_30`),又短到装不下一句她 // 说的话 —— 上游回显 `invalid content: <正文>` 时,前半句就吃掉大半预算。 const maxUpstreamTextRunes = 40 // upstreamText 把一段上游自由文本收敛成可以出镜的短诊断:压平空白,再交给 // `trace.RedactTextLimited` 打码凭据形状并截到 maxUpstreamTextRunes(超出的带省略号)。 // // 空串进 ⇒ 空串出;调用方据此判断「这一段有没有东西可显示」。 // // 🔴 它**不**决定该不该带(默认不带)。把它的结果无条件拼进 error 仍然是把她的原话 // 送上日志 —— 调用点必须自己持有那个显式的开关(本包只有 WithEnvelopeMessage 一处)。 // 渠道插件当前**不带**任何自由文本,因此没有第二个调用点;将来若真要开,也要回到 // 这一条(`trace` 的唯一清洗 + 40 rune),不要在插件里另写一套截断。 func upstreamText(s string) string { if s == "" { return "" } return trace.RedactTextLimited(strings.Join(strings.Fields(s), " "), maxUpstreamTextRunes) }