Files
AskaEth 2b6750b821
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(channels): 再修 3 处注释里的 design/08 引用(其中 -14 口径无对应节 ⇒ 改指对照清单)
2026-10-05 11:31:14 +08:00

93 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package weixin_ilink
// 本文件承载适配器的「会话被拒绝」状态机(design/08 §4 的 weixin-ilink 条目只写到
// 「扫码登录」,未记 `-14` 口径;其语义与对照依据见
// DevDocs/reference/weixin_ClawBot_Bridge/对照清单.md §2.2「`errcode=-14` = 会话/token
// 被拒」与 §3 的「-14 处理」对照行)。
//
// iLink 在拒绝或作废会话凭据时返回 errcode=-14。bot token 是长期凭据——一个月前
// 扫码拿到的 token 依然可用——所以 -14 不是基于时间的过期:它表示 iLink 已不再接受
// 这个会话(已解绑、在别处重新登录、或协议层拒绝)。值得记下的是,日志里从未真正
// 出现过 -14;早先把 -14 读作「基于时间的过期」只是推断,并非观测。
//
// 无论如何,它都不是读循环能靠重试消解的瞬时故障:同一份凭据会一直被拒,直到主人
// 重新扫码。因此适配器保留一个**显式**的 rejected 标志,而不是静默丢弃连接,使得:
//
// - 读循环不会退出后让适配器悄悄变哑,却仍上报「connected」(即「凭据被拒 →
// 静默变哑」缺陷);
// - Connect/HealthCheck/发送路径上报 ErrSessionRejected——「需要重新扫码」——
// 而不是假 OK 或笼统的「未连接」;
// - 管理面板可以读 RescanRequired() 并显示 需要重新扫码(见
// internal/services/gateway/admin.go)。
//
// 生命周期(一次置位,拿到可用凭据后清除):
//
// SET markRejected getupdates、sendmessage 或 getuploadurl
// 返回 errcode=-14。
// CLEAR clearRescanLocked 扫码登录交付了新的 bot_token(completeLogin),或
// rejectedLocked 发现 bot_token 已不同于 iLink 拒绝的
// 那一个——配置变更 / 热重载提供了新 token。
// 记录在「被拒绝」状态上的重扫原因。它们简短、人类可读,且不携带任何凭据:
// bot token 本身从不离开 cfg。
const (
rescanReasonGetUpdates = "iLink getupdates 返回 errcode=-14(本次会话已被 iLink 拒绝)"
rescanReasonSendMessage = "iLink sendmessage 返回 errcode=-14(本次会话已被 iLink 拒绝)"
rescanReasonUploadURL = "iLink getuploadurl 返回 errcode=-14(本次会话已被 iLink 拒绝)"
)
// markRejected 记录 iLink 拒绝了当前会话凭据(errcode=-14)。它清掉 connected
// 标志,使 IsConnected 永不为一已被拒绝的凭据继续宣称会话存活;并以 ERROR 级别
// 恰好记录一次该状态转移(失败的轮询循环或反复的发送不得刷屏日志)。只记录非敏感的
// bot_id 与原因。
func (a *Adapter) markRejected(reason string) {
a.mu.Lock()
first := !a.rejected
a.rejected = true
a.rejectedToken = a.cfg.BotToken
a.rescanDetail = reason
a.connected = false
botID := a.cfg.BotID
a.mu.Unlock()
if first {
a.log.Error("ilink session rejected(errcode=-14):本次会话已被 iLink 拒绝,需要重新扫码绑定(请用「扫码登录」)",
"bot_id", botID, "errcode", errcodeSessionRejected, "reason", reason)
}
}
// clearRescanLocked 清除被拒绝状态。调用方须持有 a.mu。
func (a *Adapter) clearRescanLocked() {
a.rejected = false
a.rejectedToken = ""
a.rescanDetail = ""
}
// rejectedLocked 报告 iLink 是否拒绝了该会话。当已配置的 bot_token 不再是 iLink
// 拒绝的那一个时,它会先清除该状态,于是新 token(扫码登录、运维改配置、热重载)
// 无需额外簿记即可恢复会话。调用方须持有 a.mu。
func (a *Adapter) rejectedLocked() bool {
if a.rejected && a.rejectedToken != a.cfg.BotToken {
a.clearRescanLocked()
}
return a.rejected
}
// rejectedState 在一次加锁内同时返回「被拒绝」标志与重扫原因。会话未被拒绝时
// reason 为 ""。
func (a *Adapter) rejectedState() (bool, string) {
a.mu.Lock()
defer a.mu.Unlock()
if !a.rejectedLocked() {
return false, ""
}
return true, a.rescanDetail
}
// RescanRequired 实现管理面板用类型断言读取的可选能力
// (internal/services/gateway/admin.go):它报告该适配器是否需要重新扫码登录而非
// 简单重连,并给出人类可读的原因(绝不含密钥)。健康的适配器返回 false 与空原因。
func (a *Adapter) RescanRequired() (bool, string) {
return a.rejectedState()
}