fix(channel): 渠道适配器与出站工具的上游响应体/带凭据 URL 不再进日志与错误(M41)

接 M38 的 18 条真泄漏里「渠道适配器 + 出站工具」这一族(#10 #11 #12 #13 #15 #16)。
只动日志与 error 文字:发送/接收、解析口径、重试次数一个都没改。

- httpclient.do:非 2xx 不再把 512B 响应体原文拼进 error,改 trace.UpstreamError
  (状态/体积/sha256 前缀)+ 信封 code/message(RedactText + 200 rune);error 里的
  target 改 trace.HostOf,并剥掉 *url.Error 外壳(它带整条地址)
- telegram doJSON:4xx 只解 error_code/description(清洗 + 截断),体其余字段不拼;
  传输错误只留来源 —— Bot API 把 token 写在地址路径里
- weixin_ilink doJSON:4096B 体 ⇒ trace.UpstreamError,rawURL ⇒ mediafetch.HostOf;
  uploadOnce 剥掉预签名上传地址(encrypted_query_param),重试日志补来源主机
- qq_obv11 connectClient 成功 INFO:改走同包已有的 maskAccessTokenQuery
  (OneBot 惯例把 ?access_token= 写进 remote_url)
- plugins/tools/io http 拦截日志:改记 trace.HostOf + 原因码,不记原 URL、也不记错误
  原文(解析失败时它包着 *url.Error)

每条都有能变异打红的用例:QQ/iLink 预签名地址的哨兵、三处 4xx 体哨兵、
user:pass@ 与 query 凭据;八处变异逐条验证变红。
This commit is contained in:
AskaEth committed 2026-10-26 13:47:56 +08:00
1 parent 46a6da13e0
commit 0f9251b983
11 files changed
+726 -29

No files matched your search

+59 -7
View File
@@ -4,11 +4,15 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// botResponse is the shared envelope Telegram returns for every method: ok plus
@@ -20,6 +24,15 @@ type botResponse struct {
ErrorCode int `json:"error_code"`
}
const (
// maxErrorBodyBytes caps how much of a non-200 reply is read before classifying it.
maxErrorBodyBytes = 512
// maxDescriptionRunes caps the description once it enters an error. Telegram's
// description is upstream free text (it can echo the parameter we sent), so it
// is cleaned and bounded instead of being passed through verbatim.
maxDescriptionRunes = 200
)
// doJSON posts a JSON payload to a Bot API method and decodes the envelope. The
// caller is responsible for decoding Result; a non-OK reply is returned as
// ErrBadResponse wrapped with the API description.
@@ -39,17 +52,15 @@ func (a *Adapter) doJSON(ctx context.Context, method string, payload any) (*botR
resp, err := a.client.Do(req)
if err != nil {
return nil, fmt.Errorf("telegram: %s call: %w", method, err)
// 只留来源:Bot API 把 token 写在地址路径里(/bot<token>/<method>),而
// *url.Error 会把整条地址拼进错误串 —— 原样 `%w` 等于把 bot token 写进日志。
return nil, fmt.Errorf("telegram: %s call %s: %w", method, trace.HostOf(a.endpoint(method)), callCause(err))
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
desc := strings.TrimSpace(string(body))
if desc != "" {
return nil, fmt.Errorf("telegram: %s: status %d: %s: %w", method, resp.StatusCode, desc, ErrBadResponse)
}
return nil, fmt.Errorf("telegram: %s: status %d: %w", method, resp.StatusCode, ErrBadResponse)
body, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyBytes))
return nil, badResponseError(method, resp.StatusCode, body)
}
var out botResponse
@@ -203,3 +214,44 @@ func parseReplyToID(s string) int64 {
}
return n
}
// badResponseError 把一次非 200 的回复收敛成错误:**只**取 error_code 与
// description。响应体其余字段(以及 Telegram 回显的请求参数)一个字都不拼 ——
// 旧实现把 512B 体原文当 desc 拼进来,等于把上游报文写进调用方的日志。
//
// description 是上游自由文本(例如 setWebhook 失败时会回显我们给的地址),因此过
// trace.RedactText 清洗凭据形状并按 rune 截断后才出镜。
func badResponseError(method string, status int, body []byte) error {
var env botResponse
_ = json.Unmarshal(body, &env)
parts := []string{fmt.Sprintf("telegram: %s: status %d", method, status)}
if env.ErrorCode != 0 {
parts = append(parts, fmt.Sprintf("error_code %d", env.ErrorCode))
}
if desc := boundedDescription(env.Description); desc != "" {
parts = append(parts, desc)
}
return fmt.Errorf("%s: %w", strings.Join(parts, ": "), ErrBadResponse)
}
// boundedDescription 是 Bot API description 进入错误前的唯一处理:压平空白、清洗
// 凭据形状,再按 rune 截断(宁可丢细节,不可把一段回显整段带出来)。
func boundedDescription(s string) string {
flat := strings.Join(strings.Fields(s), " ")
flat = trace.RedactText(flat)
if r := []rune(flat); len(r) > maxDescriptionRunes {
return string(r[:maxDescriptionRunes]) + "…"
}
return flat
}
// callCause 剥掉 net/http 的 *url.Error 外壳:它会把完整地址拼进错误串,而 Bot API
// 的地址路径里就写着 bot token。剥壳只丢地址,内层哨兵链(context 取消等)原样保留。
// 与 internal/channel/mediafetch 的 sanitizeErr 同一口径(那份未导出,且在别包里)。
func callCause(err error) error {
var uerr *url.Error
if errors.As(err, &uerr) && uerr.Err != nil {
return uerr.Err
}
return err
}