docs(comments): 注释中文化 · plugins/channels/weixin_ilink(20 文件,纯替换)
- 只翻注释/文档字符串:剥掉行尾注释后,每条「-」都有逐字节相同的「+」对应行(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:
1 parent
09fba77f87
commit
95f97f5df3
20 files changed
+488
-590
No files matched your search
@@ -12,34 +12,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// loginPollInterval is the default how often Connect polls the QR confirmation
|
||||
// status. It is the documented ~1s cadence; the Config.LoginInterval field
|
||||
// overrides it (used by tests to keep the flow short).
|
||||
// loginPollInterval 是 Connect 轮询二维码确认状态的默认间隔。它是文档记录的约 1s
|
||||
// 节奏;Config.LoginInterval 字段可覆盖它(测试用它把流程压缩得很短)。
|
||||
const loginPollInterval = 1 * time.Second
|
||||
|
||||
// qrcodeResp is the get_bot_qrcode response.
|
||||
// qrcodeResp 是 get_bot_qrcode 的响应。
|
||||
type qrcodeResp struct {
|
||||
Qrcode string `json:"qrcode"`
|
||||
QrcodeImgContent string `json:"qrcode_img_content"`
|
||||
}
|
||||
|
||||
// qrcodeStatusResp is the get_qrcode_status response. The token and ids are
|
||||
// only populated once status is "confirmed".
|
||||
// qrcodeStatusResp 是 get_qrcode_status 的响应。只有 status 为 "confirmed" 时才会
|
||||
// 填充 token 与各类 id。
|
||||
type qrcodeStatusResp struct {
|
||||
Status string `json:"status"`
|
||||
BotToken string `json:"bot_token"`
|
||||
BotID string `json:"ilink_bot_id"`
|
||||
UserID string `json:"ilink_user_id"`
|
||||
BaseURL string `json:"baseurl"`
|
||||
// RedirectHost carries the IDC host to poll next when status is
|
||||
// "scaned_but_redirect" (official currentApiBaseUrl redirect).
|
||||
// RedirectHost 携带 status 为 "scaned_but_redirect" 时下一个要轮询的 IDC 主机
|
||||
// (官方的 currentApiBaseUrl 重定向)。
|
||||
RedirectHost string `json:"redirect_host"`
|
||||
}
|
||||
|
||||
// login is the Connect-side scan-code binding (design/08 §4). It joins (or
|
||||
// starts) a scan-login via StartLogin and blocks until the user confirms or the
|
||||
// window elapses. Because it shares the session machinery with the panel's
|
||||
// StartLogin, a panel-initiated login and Connect never run two QR binds at once.
|
||||
// login 是 Connect 一侧的扫码绑定(design/08 §4)。它通过 StartLogin 加入(或发起)
|
||||
// 一次扫码登录,并阻塞直到用户确认或窗口结束。由于它与面板的 StartLogin 共用同一套
|
||||
// 会话机制,面板发起的登录与 Connect 绝不会同时跑两个二维码绑定。
|
||||
func (a *Adapter) login(ctx context.Context) error {
|
||||
_, err := a.StartLogin(ctx)
|
||||
if errors.Is(err, ErrAlreadyLoggedIn) {
|
||||
@@ -65,9 +63,8 @@ func (a *Adapter) login(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// runLogin polls get_qrcode_status until the QR is confirmed or the login window
|
||||
// elapses, then records the outcome under the session. It runs in a goroutine so
|
||||
// StartLogin returns immediately while the panel polls LoginStatus.
|
||||
// runLogin 轮询 get_qrcode_status,直到二维码被确认或登录窗口结束,然后把结果记录在
|
||||
// 会话上。它运行在 goroutine 中,所以 StartLogin 能立即返回,而面板轮询 LoginStatus。
|
||||
func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
timeoutCtx, cancel := context.WithTimeout(sessCtx, a.cfg.LoginTimeout)
|
||||
defer cancel()
|
||||
@@ -78,8 +75,8 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
for {
|
||||
status, err := a.pollQRStatus(timeoutCtx, sess.qrcode)
|
||||
if err != nil {
|
||||
// A deadline/cancel during the poll means the whole window elapsed;
|
||||
// surface ErrLoginTimeout rather than the raw transport error.
|
||||
// 轮询期间出现期限到点/取消,意味着整个窗口已过:上报
|
||||
// ErrLoginTimeout,而不是原始的传输层错误。
|
||||
if timeoutCtx.Err() != nil {
|
||||
a.failLogin(sess, fmt.Errorf("%w: %w", ErrLoginTimeout, timeoutCtx.Err()))
|
||||
} else {
|
||||
@@ -87,9 +84,8 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
// Track the raw wire phase so StartLogin can protect a scanned/confirming bind
|
||||
// from being re-minted. Log only on a status change to avoid noise, and never
|
||||
// the qrcode identifier or tokens.
|
||||
// 跟踪线上的原始阶段,便于 StartLogin 保护「已扫描/确认中」的绑定不被重新取码。
|
||||
// 仅在状态变化时打日志以避免噪声,且绝不记录 qrcode 标识符或 token。
|
||||
sess.setPhase(status.Status)
|
||||
if status.Status != prevStatus {
|
||||
prevStatus = status.Status
|
||||
@@ -105,18 +101,17 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
a.completeLogin(sess, status)
|
||||
return
|
||||
case "binded_redirect":
|
||||
// The scanned bot is already bound to this instance; no new credentials
|
||||
// are issued. Treat it as an already-bound (successful) outcome.
|
||||
// 被扫描的机器人已绑定到本实例,不会签发新凭据。把它当作「已绑定」的
|
||||
// 成功结果处理。
|
||||
a.completeBindedRedirect(sess)
|
||||
return
|
||||
case "verify_code_blocked":
|
||||
a.failLogin(sess, fmt.Errorf("weixin_ilink: verify code blocked, too many failed attempts"))
|
||||
return
|
||||
case "scaned":
|
||||
// The code has been scanned and awaits confirmation on the phone. Keep the
|
||||
// session and keep polling — re-minting here would discard the pending
|
||||
// confirmation. Expose the state through the backward-compatible waiting
|
||||
// status plus a detail so the panel can prompt the user to confirm.
|
||||
// 码已被扫描,正在手机端等待确认。保留该会话并继续轮询——此处重新取码会
|
||||
// 丢弃待处理的确认。用向后兼容的 waiting 状态加一条详情暴露该状态,
|
||||
// 便于面板提示用户去确认。
|
||||
a.setLoginState(LoginStatusWaiting, loginScannedDetail)
|
||||
case "scaned_but_redirect":
|
||||
if status.RedirectHost != "" {
|
||||
@@ -126,13 +121,12 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
a.log.Warn("weixin_ilink: scaned_but_redirect without redirect_host, continuing on current host")
|
||||
}
|
||||
case "need_verifycode":
|
||||
// The server asks for a pairing code shown in the phone WeChat. Expose
|
||||
// the state and a hint so the panel can prompt the user; the code itself
|
||||
// is not yet plumbed back to the poll (see TODO).
|
||||
// 服务端要求输入手机微信上显示的配对码。暴露该状态与提示,便于面板提示
|
||||
// 用户;该码本身尚未回传到轮询(见 TODO)。
|
||||
a.setLoginState(LoginStatusNeedVerifyCode, loginNeedVerifyCodeHint)
|
||||
}
|
||||
// wait / scaned / expired / need_verifycode / scaned_but_redirect keep
|
||||
// polling until confirmed or the window elapses.
|
||||
// wait / scaned / expired / need_verifycode / scaned_but_redirect 会持续轮询,
|
||||
// 直到确认或窗口结束。
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
a.failLogin(sess, fmt.Errorf("%w: %w", ErrLoginTimeout, timeoutCtx.Err()))
|
||||
@@ -142,11 +136,10 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// completeLogin persists the confirmed credentials and marks the session live.
|
||||
// It writes cfg under a.mu, then clears the session under loginMu (never nesting
|
||||
// the two locks), and finally signals the waiting Connect path. A confirmed scan
|
||||
// delivers a fresh bot_token, which is the canonical way OUT of the rejected state
|
||||
// (session_state.go): the rescan flag is cleared here.
|
||||
// completeLogin 持久化已确认的凭据并把会话标记为存活。它在 a.mu 下写 cfg,再在
|
||||
// loginMu 下清空会话(绝不嵌套两把锁),最后唤醒等待中的 Connect 路径。确认的扫码
|
||||
// 会交付新的 bot_token,这是走出「被拒绝」状态的正规途径(session_state.go):
|
||||
// 重扫标志就在这里被清除。
|
||||
func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
a.mu.Lock()
|
||||
a.cfg.BotToken = status.BotToken
|
||||
@@ -172,13 +165,11 @@ func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
sess.done <- nil
|
||||
}
|
||||
|
||||
// completeBindedRedirect treats binded_redirect as an already-bound outcome: the
|
||||
// scanned bot is already linked to this instance, so the server issues no new
|
||||
// credentials and any existing token remains valid. Because NO new token arrives,
|
||||
// a rejected session stays rejected here — binded_redirect carries no proof that a
|
||||
// token iLink already rejected with errcode=-14 works again, so the adapter does
|
||||
// not claim a live session on the strength of it. A confirmed scan (completeLogin)
|
||||
// is what clears the rejected state.
|
||||
// completeBindedRedirect 把 binded_redirect 当作「已绑定」的结果:被扫描的机器人
|
||||
// 已关联到本实例,因此服务端不签发新凭据,已有 token 继续有效。由于**没有**新 token
|
||||
// 到达,被拒绝的会话在这里保持被拒绝——binded_redirect 并不证明一份已被 iLink 以
|
||||
// errcode=-14 拒绝的 token 重新可用,所以适配器不凭它宣称会话存活。清除被拒绝状态的
|
||||
// 是确认的扫码(completeLogin)。
|
||||
func (a *Adapter) completeBindedRedirect(sess *loginSession) {
|
||||
a.log.Info("weixin_ilink: login already bound (binded_redirect)")
|
||||
a.mu.Lock()
|
||||
@@ -196,10 +187,9 @@ func (a *Adapter) completeBindedRedirect(sess *loginSession) {
|
||||
sess.done <- nil
|
||||
}
|
||||
|
||||
// failLogin records a failed scan-login and signals the waiting Connect path. A
|
||||
// superseded session (one cancelled so StartLogin could mint a fresh QR for an
|
||||
// unscanned code) never clobbers the newer session's state, but its Connect wait
|
||||
// must still be released.
|
||||
// failLogin 记录一次失败的扫码登录并唤醒等待中的 Connect 路径。被取代的会话
|
||||
// (因 StartLogin 要为未扫描的码生成新二维码而被取消的那个)绝不会覆盖新会话的状态,
|
||||
// 但它等待中的 Connect 仍必须被释放。
|
||||
func (a *Adapter) failLogin(sess *loginSession, err error) {
|
||||
a.loginMu.Lock()
|
||||
isCurrent := a.loginSession == sess
|
||||
@@ -215,13 +205,12 @@ func (a *Adapter) failLogin(sess *loginSession, err error) {
|
||||
sess.done <- err
|
||||
}
|
||||
|
||||
// localTokenList returns the locally-held bot tokens to advertise on
|
||||
// get_bot_qrcode, so an already-bound bot can be detected (binded_redirect). On a
|
||||
// fresh bind it returns an empty (non-nil) slice so the wire carries `[]` rather
|
||||
// than `null`, matching the official client. A token iLink REJECTED is deliberately
|
||||
// not advertised: it is not a usable bind, and offering it lets the server answer
|
||||
// binded_redirect (no new credentials) — which would leave the owner stuck with an
|
||||
// unusable credential and no way back. Reporting `[]` asks for a real rebind.
|
||||
// localTokenList 返回本机持有的 bot token,用于在 get_bot_qrcode 上上报,以便服务端
|
||||
// 识别出已绑定的机器人(binded_redirect)。全新绑定时返回空的(非 nil)切片,使线上
|
||||
// 载荷携带 `[]` 而不是 `null`,与官方客户端一致。已被 iLink **拒绝**的 token 刻意
|
||||
// 不上报:它不是可用绑定,把它交出去会让服务端回以 binded_redirect(不签发新凭据)
|
||||
// ——那会让主人卡在一份不可用的凭据上、没有回头路。上报 `[]` 才是请求一次真正的
|
||||
// 重新绑定。
|
||||
func (a *Adapter) localTokenList() []string {
|
||||
if rejected, _ := a.rejectedState(); rejected {
|
||||
return []string{}
|
||||
@@ -234,9 +223,8 @@ func (a *Adapter) localTokenList() []string {
|
||||
return []string{a.cfg.BotToken}
|
||||
}
|
||||
|
||||
// fetchQRCode requests a new bot login QR code. The official client issues this as
|
||||
// a POST carrying local_token_list so the server can detect an existing bind and
|
||||
// answer binded_redirect instead of a fresh code.
|
||||
// fetchQRCode 申请一个新的机器人登录二维码。官方客户端以 POST 携带 local_token_list
|
||||
// 发出该请求,使服务端能探测到已存在的绑定并回以 binded_redirect,而不是新码。
|
||||
func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
|
||||
path := a.apiURL("/ilink/bot/get_bot_qrcode?bot_type=" + strconv.Itoa(botType))
|
||||
body, err := json.Marshal(map[string]any{"local_token_list": a.localTokenList()})
|
||||
@@ -250,14 +238,14 @@ func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
|
||||
if out.Qrcode == "" {
|
||||
return qrcodeResp{}, fmt.Errorf("weixin_ilink: get_bot_qrcode returned empty qrcode")
|
||||
}
|
||||
// Log only the renderable QR URL, never the raw qrcode identifier (it is a secret
|
||||
// echoed to get_qrcode_status). The timestamp is attached by the logger.
|
||||
// 只记录可渲染的二维码 URL,绝不记录原始 qrcode 标识符(它是回传给
|
||||
// get_qrcode_status 的密钥)。时间戳由 logger 附加。
|
||||
a.log.Info("weixin_ilink: get_bot_qrcode ok", "url", out.QrcodeImgContent)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// pollQRStatus checks whether a QR code has been confirmed. It polls the IDC
|
||||
// redirect host if one was recorded (scaned_but_redirect), else the base URL.
|
||||
// pollQRStatus 检查某个二维码是否已确认。若已记录 IDC 重定向主机
|
||||
// (scaned_but_redirect)则轮询它,否则轮询基础 URL。
|
||||
func (a *Adapter) pollQRStatus(ctx context.Context, qrcode string) (qrcodeStatusResp, error) {
|
||||
path := "/ilink/bot/get_qrcode_status?qrcode=" + url.QueryEscape(qrcode)
|
||||
var out qrcodeStatusResp
|
||||
|
||||
Reference in new issue
Block a user