Files
Cyrene_v2/plugins/channels/weixin_ilink/adapter_http.go
T
AskaEth 95f97f5df3
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 行)
2026-10-04 12:29:05 +08:00

136 lines
4.5 KiB
Go
Raw 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
// 本文件承载 iLink 适配器的 HTTP 管道(请求构造与响应解码)。它从 adapter.go
// 拆出,以便那个文件保持在项目 ≤500 行的单文件上限之内。这里不放任何「包内其余
// 代码或其测试依赖其位于某个特定文件」的导出符号。
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// authorizationHeader 为 req 装上 iLink 通用请求头:JSON 内容类型、bearer
// token、认证类型、每次新生成的随机 X-WECHAT-UIN 防重放值,以及 App 标识头。
func (a *Adapter) authorizationHeader(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("AuthorizationType", "ilink_bot_token")
req.Header.Set("X-WECHAT-UIN", randomUIN())
req.Header.Set("iLink-App-Id", ilinkAppID)
req.Header.Set("iLink-App-ClientVersion", buildClientVersion(ilinkVersion))
a.mu.RLock()
token := a.cfg.BotToken
a.mu.RUnlock()
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
}
// randomUIN 返回 base64 编码的十进制 uint32,每次请求都新生成,作为 iLink 的
// 防重放值。
func randomUIN() string {
var b [4]byte
_, _ = rand.Read(b[:])
n := binary.BigEndian.Uint32(b[:])
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprint(n)))
}
// doJSON 发起一次 HTTP 请求并把 JSON 响应解码进 out。它关闭响应体,并用 %w
// 包裹网络/解码错误。out 为 nil 时跳过解码(用于响应体是不透明字节的 PUT)。
func (a *Adapter) doJSON(ctx context.Context, method, rawURL string, body io.Reader, out any) error {
req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
if err != nil {
return fmt.Errorf("weixin_ilink: build %s %s: %w", method, rawURL, err)
}
a.authorizationHeader(req)
resp, err := a.httpClient.Do(req)
if err != nil {
return fmt.Errorf("weixin_ilink: %s %s: %w", method, rawURL, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("weixin_ilink: %s %s: status %d: %s",
method, rawURL, resp.StatusCode, strings.TrimSpace(string(msg)))
}
if out == nil {
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
return fmt.Errorf("weixin_ilink: drain %s %s: %w", method, rawURL, err)
}
return nil
}
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("weixin_ilink: decode %s %s: %w", method, rawURL, err)
}
return nil
}
// apiURL 把路径与已配置的基础 URL 拼接起来。
func (a *Adapter) apiURL(path string) string {
return joinURL(a.cfg.BaseURL, path)
}
// joinURL 把基础 URL 与以斜杠开头的路径拼接起来,并容忍基础 URL 末尾带斜杠。
func joinURL(base, path string) string {
return strings.TrimRight(base, "/") + path
}
// pollBase 返回 get_qrcode_status 使用的基础 URL:已记录 IDC 重定向主机
// (scaned_but_redirect)时用它,否则用已配置的基础 URL。
func (a *Adapter) pollBase() string {
a.mu.RLock()
defer a.mu.RUnlock()
if a.pollBaseURL != "" {
return a.pollBaseURL
}
return a.cfg.BaseURL
}
// setPollBaseURL 按官方 currentApiBaseUrl 的逻辑,为后续的 get_qrcode_status
// 轮询记录一个 IDC 重定向主机(https://<redirect_host>)。host 为空则清除该覆盖。
func (a *Adapter) setPollBaseURL(host string) {
a.mu.Lock()
defer a.mu.Unlock()
if host == "" {
a.pollBaseURL = ""
return
}
a.pollBaseURL = "https://" + host
}
// buildClientVersion 按官方 api.ts 的做法,把语义化版本编码成 iLink
// App-ClientVersion 的线上值 0x00MMNNPP = (major&0xff)<<16 | (minor&0xff)<<8 |
// (patch&0xff)。例如 "2.4.8" -> "132104"(十进制字符串,而不是 "1")。
func buildClientVersion(version string) string {
parts := strings.Split(version, ".")
at := func(i int) int {
if i >= len(parts) {
return 0
}
n, err := strconv.Atoi(parts[i])
if err != nil {
return 0
}
return n
}
v := ((at(0) & 0xff) << 16) | ((at(1) & 0xff) << 8) | (at(2) & 0xff)
return strconv.Itoa(v)
}
// cdnURL 把路径与已配置的 CDN 基础 URL 拼接起来,并转义部分 CDN 引用携带的原始
// 查询串。
func (a *Adapter) cdnURL(raw string) string {
if u, err := url.Parse(raw); err == nil && u.IsAbs() {
return raw
}
return strings.TrimRight(a.cfg.CDNBaseURL, "/") + "/" + strings.TrimLeft(raw, "/")
}