93 lines
4.4 KiB
Go
93 lines
4.4 KiB
Go
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()
|
||
}
|