fix(weixin): 扫码上游字段过令牌守卫,errortoken 契约改写

复审 p2 两条:

① 第五个出口:get_qrcode_status 的 status 与 scaned_but_redirect 的
redirect_host 此前原样进 INFO;同一 status 经 setPhase 还从复用二维码那条
INFO(phase)二次出镜。三处一律过 trace.ErrorToken;**控制流仍用原值**
(状态机读 status.Status、路由读 status.RedirectHost、confirmingPhase 读
phase),用例同时钉住「日志变空串」与「路由/判定不变」两个方向。

② 顺带补扫出一处 URL 类:get_bot_qrcode 的 qrcode_img_content(上游给的
图片地址,可能是带签名的 URL 或 data: 内联图)原样进 INFO ⇒ 改走
trace.MaskQuery(凭据类 query 打码、保留路径、解析不出 host 回空串)。

③ errortoken.go 的契约改写:原「本包之外不应出现 ErrorToken 调用」已被
M50 的正当调用点证伪。新口径按**字段类别**判定:协议枚举字段(status/
code/type/request_id)可在渲染点调用;自由文本字段(message/description/
msg/wording)不得靠它出镜 —— 那类默认不带,要带必须 opt-in + RedactText。
dashscope 的 request_id 例外一并写入。

每条用例都做了变异验证(去掉守卫 / 把守卫挪进控制流 / 改回原串,均打红)。
This commit is contained in:
AskaEth committed 2026-10-26 19:41:28 +08:00
1 parent 996201c4ed
commit 1b2576ecaf
4 files changed
+269 -11

No files matched your search

+22 -3
View File
@@ -10,6 +10,8 @@ import (
"net/url"
"strconv"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// loginPollInterval 是 Connect 轮询二维码确认状态的默认间隔。它是文档记录的约 1s
@@ -86,10 +88,17 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
}
// 跟踪线上的原始阶段,便于 StartLogin 保护「已扫描/确认中」的绑定不被重新取码。
// 仅在状态变化时打日志以避免噪声,且绝不记录 qrcode 标识符或 token。
//
// 🔴 status 是**上游字段**:进日志前过令牌守卫(trace.ErrorToken)。真实取值
// (wait / scaned / confirmed / scaned_but_redirect / …)全是短令牌 ⇒ 原样通过;
// 上游若在这一栏塞散文,这一行就是空串 —— 与 qq_obv11 的 status、
// trace.UpstreamError 的 code/type 同一道守卫(M50 复审的第五个出口)。
// **控制流仍用原值**:setPhase 与下面的 switch 都读 status.Status,渲染口径
// 不参与判定。
sess.setPhase(status.Status)
if status.Status != prevStatus {
prevStatus = status.Status
a.log.Info("weixin_ilink: get_qrcode_status", "status", status.Status)
a.log.Info("weixin_ilink: get_qrcode_status", "status", trace.ErrorToken(status.Status))
}
switch status.Status {
@@ -115,8 +124,13 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
a.setLoginState(LoginStatusWaiting, loginScannedDetail)
case "scaned_but_redirect":
if status.RedirectHost != "" {
// 🔴 路由用**原值**(下一行),日志只留守卫后的形态:redirect_host 是上游
// 字段,与上面的 status 同一道令牌守卫(trace.ErrorToken)。真实主机名通过;
// 带端口(`h:8443`)或散文的整体丢弃 —— 宁可日志少一个字段,也不让这一栏
// 成为第二个正文通道。
a.setPollBaseURL(status.RedirectHost)
a.log.Info("weixin_ilink: idc redirect, switching polling host", "host", status.RedirectHost)
a.log.Info("weixin_ilink: idc redirect, switching polling host",
"host", trace.ErrorToken(status.RedirectHost))
} else {
a.log.Warn("weixin_ilink: scaned_but_redirect without redirect_host, continuing on current host")
}
@@ -240,7 +254,12 @@ func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
}
// 只记录可渲染的二维码 URL,绝不记录原始 qrcode 标识符(它是回传给
// get_qrcode_status 的密钥)。时间戳由 logger 附加。
a.log.Info("weixin_ilink: get_bot_qrcode ok", "url", out.QrcodeImgContent)
//
// 🔴 URL 一律走 trace.MaskQuery(M50 复审补扫):它是**上游给的**串,可能是带签名
// 的图片地址,也可能整段是 `data:` 内联图 —— 原样落日志等于把二维码(扫码即绑定)
// 抄进一个会被 tail、采集、粘进工单的面。MaskQuery 打码凭据类 query 值并保留路径,
// 解析不出 host(data: URI、畸形串)时回空串而**不**回原串。
a.log.Info("weixin_ilink: get_bot_qrcode ok", "url", trace.MaskQuery(out.QrcodeImgContent))
return out, nil
}