docs(comments): 注释中文化 · plugins/channels/weixin_ilink(20 文件,纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:剥掉行尾注释后,每条「-」都有逐字节相同的「+」对应行(8 组,代码零改动)
- 保留协议/线上标识符原文:errcode、bot_token、context_token、getupdates、binded_redirect、PKCS#7、ECB 等
- 术语对齐既有译法:owner→主人、peer→对端、long poll→长轮询、backoff→退避、sentinel error→哨兵错误
- 已知既有欠债(未顺手修):login_reuse_test.go 在 HEAD 上就未 gofmt 干净;login_test.go 的 TestStartLogin_ScanStatusTransition 91 行 >80(HEAD 同为 91 行)
This commit is contained in:
AskaEth committed 2026-10-04 12:29:05 +08:00
1 parent 09fba77f87
commit 95f97f5df3
20 files changed
+488 -590

No files matched your search

+34 -44
View File
@@ -1,49 +1,42 @@
package weixin_ilink
// This file holds the adapter's session-rejection state machine (design/08 §4-§5).
// 本文件承载适配器的「会话被拒绝」状态机(design/08 §4-§5)。
//
// iLink answers errcode=-14 when it rejects or invalidates the session credential.
// The bot token is a LONG-LIVED credential — a token obtained by scanning a month
// ago still works — so -14 is NOT a time-based expiry: it means iLink no longer
// accepts this session (unbound, re-logged-in elsewhere, a protocol-level refusal).
// It is worth recording that no log has ever actually shown -14; the earlier
// reading of it as a time-based expiry was an inference, not an observation.
// iLink 在拒绝或作废会话凭据时返回 errcode=-14。bot token 是长期凭据——一个月前
// 扫码拿到的 token 依然可用——所以 -14 不是基于时间的过期:它表示 iLink 已不再接受
// 这个会话(已解绑、在别处重新登录、或协议层拒绝)。值得记下的是,日志里从未真正
// 出现过 -14;早先把 -14 读作「基于时间的过期」只是推断,并非观测。
//
// Either way it is not a transient failure the read loop can retry away: the same
// credential keeps being refused until the owner scans a fresh QR code. The adapter
// therefore keeps an EXPLICIT rejected flag instead of silently dropping the
// connection, so:
// 无论如何,它都不是读循环能靠重试消解的瞬时故障:同一份凭据会一直被拒,直到主人
// 重新扫码。因此适配器保留一个**显式**的 rejected 标志,而不是静默丢弃连接,使得:
//
// - the read loop cannot exit and leave the adapter quietly mute while still
// reporting "connected" (the "credential rejected → silently mute" defect);
// - Connect/HealthCheck/send surface ErrSessionRejected — "re-scan required" —
// rather than a fake OK or a generic "not connected";
// - the admin panel can read RescanRequired() and show 需要重新扫码 (see
// internal/services/gateway/admin.go).
// - 读循环不会退出后让适配器悄悄变哑,却仍上报「connected」(即「凭据被拒 →
// 静默变哑」缺陷);
// - Connect/HealthCheck/发送路径上报 ErrSessionRejected——「需要重新扫码」——
// 而不是假 OK 或笼统的「未连接」;
// - 管理面板可以读 RescanRequired() 并显示 需要重新扫码(见
// internal/services/gateway/admin.go)。
//
// Lifecycle (set once, cleared on a usable credential):
// 生命周期(一次置位,拿到可用凭据后清除):
//
// SET markRejected iLink returned errcode=-14 on getupdates,
// sendmessage or getuploadurl.
// CLEAR clearRescanLocked a scan-code login delivered a new bot_token
// (completeLogin), or
// rejectedLocked saw a bot_token different from the one
// iLink rejected — a config change / hot reload that
// supplied a fresh token.
// SET markRejected getupdates、sendmessage 或 getuploadurl
// 返回 errcode=-14。
// CLEAR clearRescanLocked 扫码登录交付了新的 bot_token(completeLogin),或
// rejectedLocked 发现 bot_token 已不同于 iLink 拒绝的
// 那一个——配置变更 / 热重载提供了新 token。
// Rescan reasons recorded on the rejected state. They are short, human-readable
// and carry no credential: the bot token itself never leaves cfg.
// 记录在「被拒绝」状态上的重扫原因。它们简短、人类可读,且不携带任何凭据:
// bot token 本身从不离开 cfg。
const (
rescanReasonGetUpdates = "iLink getupdates 返回 errcode=-14(本次会话已被 iLink 拒绝)"
rescanReasonSendMessage = "iLink sendmessage 返回 errcode=-14(本次会话已被 iLink 拒绝)"
rescanReasonUploadURL = "iLink getuploadurl 返回 errcode=-14(本次会话已被 iLink 拒绝)"
)
// markRejected records that iLink rejected the current session credential
// (errcode=-14). It drops the connected flag so IsConnected can never keep claiming
// a live session for a rejected credential, and logs the transition at ERROR level
// exactly once (a failing poll loop or a repeated send must not flood the log). Only
// the non-secret bot_id and the reason are logged.
// markRejected 记录 iLink 拒绝了当前会话凭据(errcode=-14)。它清掉 connected
// 标志,使 IsConnected 永不为一已被拒绝的凭据继续宣称会话存活;并以 ERROR 级别
// 恰好记录一次该状态转移(失败的轮询循环或反复的发送不得刷屏日志)。只记录非敏感的
// bot_id 与原因。
func (a *Adapter) markRejected(reason string) {
a.mu.Lock()
first := !a.rejected
@@ -60,17 +53,16 @@ func (a *Adapter) markRejected(reason string) {
}
}
// clearRescanLocked drops the rejected state. Callers must hold a.mu.
// clearRescanLocked 清除被拒绝状态。调用方须持有 a.mu。
func (a *Adapter) clearRescanLocked() {
a.rejected = false
a.rejectedToken = ""
a.rescanDetail = ""
}
// rejectedLocked reports whether iLink rejected the session. It first clears the
// state when the configured bot_token is no longer the one iLink rejected, so a
// fresh token (scan login, operator config change, hot reload) restores the session
// without extra bookkeeping. Callers must hold a.mu.
// rejectedLocked 报告 iLink 是否拒绝了该会话。当已配置的 bot_token 不再是 iLink
// 拒绝的那一个时,它会先清除该状态,于是新 token(扫码登录、运维改配置、热重载)
// 无需额外簿记即可恢复会话。调用方须持有 a.mu。
func (a *Adapter) rejectedLocked() bool {
if a.rejected && a.rejectedToken != a.cfg.BotToken {
a.clearRescanLocked()
@@ -78,8 +70,8 @@ func (a *Adapter) rejectedLocked() bool {
return a.rejected
}
// rejectedState reports the rejected flag together with the rescan reason in one
// lock acquisition. reason is "" when the session has not been rejected.
// rejectedState 在一次加锁内同时返回「被拒绝」标志与重扫原因。会话未被拒绝时
// reason 为 ""。
func (a *Adapter) rejectedState() (bool, string) {
a.mu.Lock()
defer a.mu.Unlock()
@@ -89,11 +81,9 @@ func (a *Adapter) rejectedState() (bool, string) {
return true, a.rescanDetail
}
// RescanRequired implements the optional capability the admin panel reads by type
// assertion (internal/services/gateway/admin.go): it reports whether this adapter
// needs a new scan-code login instead of a plain reconnect, plus the
// human-readable reason (never a secret). A healthy adapter reports false with an
// empty reason.
// RescanRequired 实现管理面板用类型断言读取的可选能力
// (internal/services/gateway/admin.go):它报告该适配器是否需要重新扫码登录而非
// 简单重连,并给出人类可读的原因(绝不含密钥)。健康的适配器返回 false 与空原因。
func (a *Adapter) RescanRequired() (bool, string) {
return a.rejectedState()
}