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
@@ -1,17 +1,14 @@
|
||||
// Package weixin_ilink implements the WeChat ClawBot iLink channel adapter
|
||||
// for Cyrene v2 (DevDocs/design/08-渠道接入.md §4). It adapts Tencent's iLink
|
||||
// Bot API into the normalized internal/channel/adapter contract.
|
||||
// Package weixin_ilink 实现 Cyrene v2 的微信 ClawBot iLink 渠道适配器
|
||||
// (DevDocs/design/08-渠道接入.md §4)。它把腾讯 iLink Bot API 适配到归一化的
|
||||
// internal/channel/adapter 契约上。
|
||||
//
|
||||
// The adapter covers the weixin-ilink surface: scan-code login, HTTP long
|
||||
// polling with a persistent get_updates_buf cursor, per-peer context_token
|
||||
// tracking, AES-128-ECB media encrypt/decrypt with CDN download, and the 24h
|
||||
// proactive-push window. Media CDN references are captured lazily — the poll
|
||||
// loop never downloads attachments, so a large file cannot stall message
|
||||
// delivery (design 03 §8, item 6).
|
||||
// 该适配器覆盖 weixin-ilink 的能力面:扫码登录、带持久化 get_updates_buf 游标的
|
||||
// HTTP 长轮询、按对端跟踪 context_token、配合 CDN 下载的 AES-128-ECB 媒体加解密,
|
||||
// 以及 24 小时主动推送窗口。媒体 CDN 引用是惰性捕获的——轮询循环绝不下载附件,
|
||||
// 因此大文件不会拖住消息投递(design 03 §8 第 6 条)。
|
||||
//
|
||||
// All network I/O goes through an injected *http.Client and honors the passed
|
||||
// context.Context, so tests can substitute an httptest.Server and cancel the
|
||||
// read loop without leaking goroutines or timers.
|
||||
// 所有网络 I/O 都经过注入的 *http.Client 并尊重传入的 context.Context,测试因此
|
||||
// 可以换成 httptest.Server,并在取消读循环时不泄漏 goroutine 或定时器。
|
||||
package weixin_ilink
|
||||
|
||||
import (
|
||||
@@ -29,101 +26,97 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// source labels every log record emitted by this package so failures can be
|
||||
// traced back to the owning module (constraint 02 §5).
|
||||
// source 为本包发出的每条日志记录标注来源,使故障可回溯到所属模块
|
||||
// (constraint 02 §5)。
|
||||
source = "plugins/channels/weixin_ilink"
|
||||
|
||||
// platformName is the Name() value of the adapter.
|
||||
// platformName 是适配器的 Name() 取值。
|
||||
platformName = "weixin-ilink"
|
||||
|
||||
// defaultBaseURL and defaultCDNBaseURL come from the protocol summary.
|
||||
// defaultBaseURL 与 defaultCDNBaseURL 来自协议摘要。
|
||||
defaultBaseURL = "https://ilinkai.weixin.qq.com"
|
||||
defaultCDNBaseURL = "https://novac2c.cdn.weixin.qq.com/c2c"
|
||||
|
||||
// defaultLongPollTimeout exceeds the ~35s server hold so the HTTP client
|
||||
// does not abort a normal long-poll in flight.
|
||||
// defaultLongPollTimeout 超过服务端约 35s 的保持时长,使 HTTP 客户端不会
|
||||
// 中断一次正常进行中的长轮询。
|
||||
defaultLongPollTimeout = 45 * time.Second
|
||||
// defaultLoginTimeout bounds the scan-code confirmation wait.
|
||||
// defaultLoginTimeout 限定扫码确认的等待时长。
|
||||
defaultLoginTimeout = 8 * time.Minute
|
||||
|
||||
// defaultProactiveWindow is what design/08 §4 calls the iLink 24h window.
|
||||
// defaultProactiveWindow 即 design/08 §4 所称的 iLink 24 小时窗口。
|
||||
defaultProactiveWindow = 24 * time.Hour
|
||||
|
||||
// defaultMaxMediaSize caps a downloaded attachment at 50 MiB.
|
||||
// defaultMaxMediaSize 把单个下载的附件限制在 50 MiB。
|
||||
defaultMaxMediaSize int64 = 50 << 20
|
||||
|
||||
// botType is hard-coded in the official iLink source and is passed
|
||||
// verbatim to get_bot_qrcode.
|
||||
// botType 在官方 iLink 源码中硬编码,原样传给 get_bot_qrcode。
|
||||
botType = 3
|
||||
// channelVersion is the base_info.channel_version sent on getupdates.
|
||||
// channelVersion 是 getupdates 上发送的 base_info.channel_version。
|
||||
channelVersion = "1.0.2"
|
||||
// ilinkAppID mirrors the official package.json ilink_appid field and is sent
|
||||
// as the iLink-App-Id header on every request.
|
||||
// ilinkAppID 对应官方 package.json 的 ilink_appid 字段,并在每次请求上作为
|
||||
// iLink-App-Id 头发送。
|
||||
ilinkAppID = "bot"
|
||||
// ilinkVersion mirrors the upstream @tencent-weixin/openclaw-weixin version
|
||||
// whose client version we advertise on the wire as iLink-App-ClientVersion.
|
||||
// ilinkVersion 对应上游 @tencent-weixin/openclaw-weixin 的版本,我们以它的
|
||||
// 客户端版本作为 iLink-App-ClientVersion 在线路上宣称。
|
||||
ilinkVersion = "2.4.8"
|
||||
// errcodeSessionRejected is the iLink errcode meaning iLink rejected or
|
||||
// invalidated this session credential. It is NOT a time-based expiry: the bot
|
||||
// token is a long-lived credential (a token obtained by scanning a month ago
|
||||
// still works), so -14 means iLink no longer accepts this session — unbound,
|
||||
// re-logged-in elsewhere, or refused at the protocol level.
|
||||
// errcodeSessionRejected 是 iLink 的错误码,含义是 iLink 拒绝或作废了这份会话
|
||||
// 凭据。它**不是**基于时间的过期:bot token 是长期凭据(一个月前扫码拿到的 token
|
||||
// 依然可用),所以 -14 表示 iLink 不再接受这个会话——已解绑、在别处重新登录、
|
||||
// 或协议层拒绝。
|
||||
errcodeSessionRejected = -14
|
||||
)
|
||||
|
||||
// Sentinel errors returned by this adapter, usable with errors.Is.
|
||||
// 本适配器返回的哨兵错误,可用 errors.Is 判定。
|
||||
var (
|
||||
// ErrInvalidConfig is returned when a required config field is empty.
|
||||
// ErrInvalidConfig 在必填配置字段为空时返回。
|
||||
ErrInvalidConfig = errors.New("weixin_ilink: invalid config")
|
||||
// ErrLoginTimeout is returned when the QR code is not confirmed in time.
|
||||
// ErrLoginTimeout 在二维码未及时确认时返回。
|
||||
ErrLoginTimeout = errors.New("weixin_ilink: qr login timed out")
|
||||
// ErrSessionRejected is returned when iLink reports errcode=-14.
|
||||
// ErrSessionRejected 在 iLink 上报 errcode=-14 时返回。
|
||||
ErrSessionRejected = errors.New("weixin_ilink: iLink rejected the session (errcode=-14); re-scan required")
|
||||
// ErrNeedsLogin is returned by Connect when no bot_token is configured yet; the
|
||||
// scan-code bind must be driven via the panel login-qr/login-status endpoints.
|
||||
// ErrNeedsLogin 由 Connect 在尚未配置 bot_token 时返回;扫码绑定必须经面板的
|
||||
// login-qr/login-status 端点驱动。
|
||||
ErrNeedsLogin = errors.New("weixin_ilink: bot not logged in yet, scan the QR")
|
||||
// ErrNoContextToken is returned when a send lacks the peer's context_token.
|
||||
// ErrNoContextToken 在一次发送缺少该对端的 context_token 时返回。
|
||||
ErrNoContextToken = errors.New("weixin_ilink: no context_token for peer")
|
||||
// ErrProactiveWindow is returned when a proactive push is outside 24h.
|
||||
// ErrProactiveWindow 在主动推送落在 24 小时窗口之外时返回。
|
||||
ErrProactiveWindow = errors.New("weixin_ilink: proactive push outside 24h window")
|
||||
// ErrMediaTooLarge is returned when a download exceeds MaxMediaSize.
|
||||
// ErrMediaTooLarge 在下载超过 MaxMediaSize 时返回。
|
||||
ErrMediaTooLarge = errors.New("weixin_ilink: media exceeds size limit")
|
||||
)
|
||||
|
||||
// Config holds the weixin-ilink adapter settings. It is sourced from
|
||||
// environment/config by the channel layer and can be round-tripped through
|
||||
// Config() and adapter.DecodeConfig.
|
||||
// Config 保存 weixin-ilink 适配器的设置。它由渠道层从环境/配置读取,并可通过
|
||||
// Config() 与 adapter.DecodeConfig 往返转换。
|
||||
type Config struct {
|
||||
// BaseURL is the iLink API root.
|
||||
// BaseURL 是 iLink API 的根地址。
|
||||
BaseURL string `json:"base_url"`
|
||||
// CDNBaseURL is the media CDN root used for downloads and uploads.
|
||||
// CDNBaseURL 是用于下载与上传的媒体 CDN 根地址。
|
||||
CDNBaseURL string `json:"cdn_base_url"`
|
||||
// BotToken is the bearer token obtained from scan login. A non-empty token
|
||||
// means Connect skips the QR flow.
|
||||
// BotToken 是扫码登录得到的 bearer token。token 非空意味着 Connect 会跳过
|
||||
// 二维码流程。
|
||||
BotToken string `json:"bot_token"`
|
||||
// BotID is the ilink_bot_id from the login confirmation.
|
||||
// BotID 是登录确认返回的 ilink_bot_id。
|
||||
BotID string `json:"bot_id"`
|
||||
// OwnerID is the ilink_user_id of the bound owner account.
|
||||
// OwnerID 是已绑定主人账号的 ilink_user_id。
|
||||
OwnerID string `json:"owner_id"`
|
||||
// Cursor is the persisted get_updates_buf.
|
||||
// Cursor 是持久化的 get_updates_buf。
|
||||
Cursor string `json:"cursor"`
|
||||
// LongPollTimeout is the per-request timeout for getupdates.
|
||||
// LongPollTimeout 是 getupdates 的单请求超时。
|
||||
LongPollTimeout time.Duration `json:"long_poll_timeout"`
|
||||
// LoginTimeout bounds how long the scan-code flow waits for confirmation.
|
||||
// LoginTimeout 限定扫码流程等待确认的时长。
|
||||
LoginTimeout time.Duration `json:"login_timeout"`
|
||||
// LoginInterval is how often the QR status is polled during login.
|
||||
// LoginInterval 是登录期间轮询二维码状态的间隔。
|
||||
LoginInterval time.Duration `json:"login_interval"`
|
||||
// PollInterval is the gap between long polls; zero means poll immediately.
|
||||
// PollInterval 是两次长轮询之间的间隔;为零表示立即轮询。
|
||||
PollInterval time.Duration `json:"poll_interval"`
|
||||
// ProactiveWindow is how long after the user's last message a proactive
|
||||
// push is allowed.
|
||||
// ProactiveWindow 是用户最后一条消息之后仍允许主动推送的时长。
|
||||
ProactiveWindow time.Duration `json:"proactive_window"`
|
||||
// MaxMediaSize caps a downloaded attachment in bytes.
|
||||
// MaxMediaSize 以字节为单位限制单个下载的附件。
|
||||
MaxMediaSize int64 `json:"max_media_size"`
|
||||
}
|
||||
|
||||
// withDefaults fills zero-valued fields with protocol defaults. BotID/OwnerID
|
||||
// may legitimately be empty, so they are left untouched.
|
||||
// withDefaults 用协议默认值填充零值字段。BotID/OwnerID 合法地可以为空,因此原样
|
||||
// 保留。
|
||||
func (c Config) withDefaults() Config {
|
||||
if c.BaseURL == "" {
|
||||
c.BaseURL = defaultBaseURL
|
||||
@@ -152,9 +145,8 @@ func (c Config) withDefaults() Config {
|
||||
return c
|
||||
}
|
||||
|
||||
// Adapter is the PlatformAdapter implementation for WeChat ClawBot iLink. It is
|
||||
// safe for concurrent use: reads and writes of the session flag, cursor, and
|
||||
// per-peer token/timestamp maps are guarded by a single RWMutex.
|
||||
// Adapter 是微信 ClawBot iLink 的 PlatformAdapter 实现。它可安全并发使用:会话
|
||||
// 标志、游标以及按对端的 token/时间戳映射的读写都由同一把 RWMutex 保护。
|
||||
type Adapter struct {
|
||||
cfg Config
|
||||
httpClient *http.Client
|
||||
@@ -163,36 +155,34 @@ type Adapter struct {
|
||||
|
||||
mu sync.RWMutex
|
||||
connected bool
|
||||
// rejected marks a session iLink rejected with errcode=-14: the credential is
|
||||
// refused until a fresh scan-code login re-binds it (session_state.go).
|
||||
// rejectedToken records WHICH token was rejected, so a subsequently configured
|
||||
// token clears the state; rescanDetail is the panel-visible reason. All three
|
||||
// are guarded by mu.
|
||||
// rejected 标记 iLink 以 errcode=-14 拒绝过的会话:在重新扫码登录把它重新绑定
|
||||
// 之前,这份凭据一直被拒(session_state.go)。rejectedToken 记录**哪一个** token
|
||||
// 被拒,于是之后配置的新 token 会清除该状态;rescanDetail 是面板可见的原因。
|
||||
// 这三者都由 mu 保护。
|
||||
rejected bool
|
||||
rejectedToken string
|
||||
rescanDetail string
|
||||
qrcode string
|
||||
// pollBaseURL optionally overrides cfg.BaseURL for get_qrcode_status polling.
|
||||
// It is set when the server returns scaned_but_redirect with a redirect_host,
|
||||
// so subsequent status polls follow the IDC host. Guarded by mu.
|
||||
// pollBaseURL 可选地覆盖 cfg.BaseURL,用于 get_qrcode_status 轮询。服务端返回
|
||||
// scaned_but_redirect 且带 redirect_host 时会设置它,使后续状态轮询跟随该 IDC
|
||||
// 主机。由 mu 保护。
|
||||
pollBaseURL string
|
||||
contexts map[string]string
|
||||
lastMsgAt map[string]time.Time
|
||||
handler adapter.MessageHandler
|
||||
|
||||
// loginMu serializes the scan-login session lifecycle (StartLogin/Connect),
|
||||
// so only one QR bind runs at a time. loginSession / loginStatus / loginDetail
|
||||
// carry the panel-observable login state; loginStatus/loginDetail are guarded
|
||||
// by mu, loginSession by loginMu.
|
||||
// loginMu 串行化扫码登录会话的生命周期(StartLogin/Connect),使同一时刻只有
|
||||
// 一次二维码绑定在跑。loginSession / loginStatus / loginDetail 承载面板可观测的
|
||||
// 登录状态;loginStatus/loginDetail 由 mu 保护,loginSession 由 loginMu 保护。
|
||||
loginMu sync.Mutex
|
||||
loginSession *loginSession
|
||||
loginStatus string
|
||||
loginDetail string
|
||||
}
|
||||
|
||||
// New builds an Adapter. client may be nil to use http.DefaultClient; log may be
|
||||
// nil to fall back to the project logger at info level. It returns
|
||||
// ErrInvalidConfig when the base URL is blank after defaults.
|
||||
// New 构造一个 Adapter。client 可为 nil,此时使用 http.DefaultClient;log 可为 nil,
|
||||
// 此时回退到项目 logger 的 info 级别。默认值填补后基础 URL 仍为空时返回
|
||||
// ErrInvalidConfig。
|
||||
func New(cfg Config, client *http.Client, log *slog.Logger) (*Adapter, error) {
|
||||
cfg = cfg.withDefaults()
|
||||
if !strings.HasPrefix(cfg.BaseURL, "http://") && !strings.HasPrefix(cfg.BaseURL, "https://") {
|
||||
@@ -217,11 +207,10 @@ func New(cfg Config, client *http.Client, log *slog.Logger) (*Adapter, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Name returns the platform identifier "weixin-ilink".
|
||||
// Name 返回平台标识符 "weixin-ilink"。
|
||||
func (a *Adapter) Name() string { return platformName }
|
||||
|
||||
// Config returns a snapshot of the current adapter configuration as a plain
|
||||
// map, including the persisted cursor and login credentials.
|
||||
// Config 以普通 map 返回当前适配器配置的快照,其中包含持久化的游标与登录凭据。
|
||||
func (a *Adapter) Config() map[string]any {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
@@ -241,14 +230,12 @@ func (a *Adapter) Config() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes the iLink session. It is idempotent. When a bot_token is
|
||||
// already configured and usable it is treated as a live session; otherwise it
|
||||
// runs the scan-code login flow (see login.go).
|
||||
// Connect 建立 iLink 会话。它是幂等的。已配置且可用的 bot_token 会被当作存活会话;
|
||||
// 否则它会跑扫码登录流程(见 login.go)。
|
||||
//
|
||||
// A token iLink already rejected with errcode=-14 is NOT usable: Connect refuses
|
||||
// it with ErrSessionRejected (re-scan required) instead of reporting a session
|
||||
// that the very next poll would tear down, and the scan-code login flow is the
|
||||
// way back (see login_admin.go StartLogin).
|
||||
// 已被 iLink 以 errcode=-14 拒绝的 token **不可用**:Connect 会以
|
||||
// ErrSessionRejected(需要重新扫码)拒绝它,而不是上报一个「下一次轮询就会拆掉」的
|
||||
// 会话;回到正轨的途径是扫码登录流程(见 login_admin.go 的 StartLogin)。
|
||||
func (a *Adapter) Connect(ctx context.Context) error {
|
||||
if rejected, reason := a.rejectedState(); rejected {
|
||||
a.log.Error("ilink connect refused: session rejected, 需要重新扫码绑定(请用「扫码登录」)",
|
||||
@@ -263,10 +250,9 @@ func (a *Adapter) Connect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
if token == "" {
|
||||
// No token yet: the scan-code bind must be driven out-of-band (the panel's
|
||||
// login-qr / login-status endpoints, design/08 §4). Never block a "connect"
|
||||
// HTTP call on the waiting QR — surface ErrNeedsLogin so the caller tells the
|
||||
// owner to scan, rather than holding the request for up to the login timeout.
|
||||
// 尚无 token:扫码绑定必须带外驱动(面板的 login-qr / login-status 端点,
|
||||
// design/08 §4)。绝不让一次「连接」HTTP 调用阻塞在等待二维码上——上报
|
||||
// ErrNeedsLogin,让调用方告知主人去扫码,而不是把请求挂到登录超时。
|
||||
return ErrNeedsLogin
|
||||
}
|
||||
a.mu.Lock()
|
||||
@@ -276,15 +262,15 @@ func (a *Adapter) Connect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// botID returns the configured bot identifier; it is the only account identifier
|
||||
// safe to log (the bot token is a credential and never reaches a log line).
|
||||
// botID 返回已配置的机器人标识符;它是唯一可安全记录到日志的账号标识(bot token
|
||||
// 是凭据,绝不进入日志行)。
|
||||
func (a *Adapter) botID() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.cfg.BotID
|
||||
}
|
||||
|
||||
// Disconnect tears down the session. It is idempotent.
|
||||
// Disconnect 拆除会话。它是幂等的。
|
||||
func (a *Adapter) Disconnect(ctx context.Context) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
@@ -296,19 +282,17 @@ func (a *Adapter) Disconnect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected reports whether the session is currently live.
|
||||
// IsConnected 报告会话当前是否存活。
|
||||
func (a *Adapter) IsConnected() bool {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.connected
|
||||
}
|
||||
|
||||
// HealthCheck verifies the session is usable: it must be connected and carry a
|
||||
// bot_token. A session iLink rejected (errcode=-14) is reported as
|
||||
// ErrSessionRejected — "re-scan required" — so a caller can tell a refused
|
||||
// credential apart from a plain disconnected adapter. It does not probe the
|
||||
// network so callers can use it cheaply after a reconnect without incurring a
|
||||
// request.
|
||||
// HealthCheck 校验会话可用:它必须处于已连接状态并携带 bot_token。被 iLink 拒绝的
|
||||
// 会话(errcode=-14)以 ErrSessionRejected 上报——「需要重新扫码」——使调用方能
|
||||
// 把被拒凭据与单纯的适配器未连接区分开。它不探测网络,调用方因此可以在重连后廉价
|
||||
// 调用,不产生任何请求。
|
||||
func (a *Adapter) HealthCheck(ctx context.Context) error {
|
||||
if rejected, reason := a.rejectedState(); rejected {
|
||||
return fmt.Errorf("%w: %s", ErrSessionRejected, reason)
|
||||
@@ -321,15 +305,15 @@ func (a *Adapter) HealthCheck(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Capabilities declares what the adapter supports.
|
||||
// Capabilities 声明该适配器支持的能力。
|
||||
func (a *Adapter) Capabilities() adapter.Capabilities {
|
||||
caps := adapter.DefaultCapabilities()
|
||||
caps.Proactive = true
|
||||
return caps
|
||||
}
|
||||
|
||||
// ListChannels returns the bot account as a single direct channel. iLink group
|
||||
// membership is not reliably enumerable, so only the bound account is listed.
|
||||
// ListChannels 把机器人账号作为单个私聊渠道返回。iLink 的群成员关系无法可靠枚举,
|
||||
// 因此只列出已绑定的账号。
|
||||
func (a *Adapter) ListChannels(ctx context.Context) ([]adapter.ChannelInfo, error) {
|
||||
a.mu.RLock()
|
||||
botID := a.cfg.BotID
|
||||
@@ -344,9 +328,8 @@ func (a *Adapter) ListChannels(ctx context.Context) ([]adapter.ChannelInfo, erro
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// SetMessageHandler installs the callback receiving push-style inbound
|
||||
// messages. A nil fn clears it. When the handler is set, ReadMessages routes
|
||||
// converted messages to it instead of the emit callback.
|
||||
// SetMessageHandler 安装接收推送式入站消息的回调。fn 为 nil 表示清除。处理器一旦
|
||||
// 设置,ReadMessages 就把转换后的消息交给它,而不再交给 emit 回调。
|
||||
func (a *Adapter) SetMessageHandler(fn adapter.MessageHandler) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
@@ -359,7 +342,7 @@ func (a *Adapter) handlerLocked() adapter.MessageHandler {
|
||||
return a.handler
|
||||
}
|
||||
|
||||
// --- concurrency helpers ---
|
||||
// --- 并发辅助 ---
|
||||
|
||||
func (a *Adapter) cursor() string {
|
||||
a.mu.RLock()
|
||||
@@ -376,9 +359,8 @@ func (a *Adapter) setCursor(c string) {
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// recordInbound refreshes the per-peer context_token and last-message time
|
||||
// (design/08 §8: every inbound message refreshes the token so proactive sends
|
||||
// have a usable token within the window).
|
||||
// recordInbound 刷新按对端的 context_token 与最后消息时间(design/08 §8:每条入站
|
||||
// 消息都刷新 token,使主动推送在窗口内有可用 token)。
|
||||
func (a *Adapter) recordInbound(token string, m adapter.UnifiedMessage) {
|
||||
peer := m.SenderID
|
||||
if m.IsGroup() {
|
||||
@@ -393,15 +375,15 @@ func (a *Adapter) recordInbound(token string, m adapter.UnifiedMessage) {
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// contextFor returns the stored context_token for a peer.
|
||||
// contextFor 返回某个对端已存的 context_token。
|
||||
func (a *Adapter) contextFor(peer string) string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.contexts[peer]
|
||||
}
|
||||
|
||||
// recipient resolves the destination peer and group flag for an outbound
|
||||
// message: group id for group chats, else the sender id, else the channel id.
|
||||
// recipient 为一条出站消息解析目标对端与群标志:群聊取群 id,否则取发送者 id,
|
||||
// 否则取渠道 id。
|
||||
func recipient(m adapter.UnifiedMessage) (peer string, group bool) {
|
||||
if m.IsGroup() {
|
||||
return m.GroupID, true
|
||||
@@ -412,8 +394,8 @@ func recipient(m adapter.UnifiedMessage) (peer string, group bool) {
|
||||
return m.ChannelID, false
|
||||
}
|
||||
|
||||
// --- HTTP plumbing ---
|
||||
// --- HTTP 管道 ---
|
||||
//
|
||||
// The request/response HTTP helpers (authorizationHeader, doJSON, apiURL,
|
||||
// joinURL, pollBase, setPollBaseURL, buildClientVersion, cdnURL) live in
|
||||
// adapter_http.go so this file stays under the project's ≤500-line limit.
|
||||
// 请求/响应的 HTTP 辅助函数(authorizationHeader、doJSON、apiURL、joinURL、
|
||||
// pollBase、setPollBaseURL、buildClientVersion、cdnURL)位于 adapter_http.go,
|
||||
// 以便本文件保持在项目 ≤500 行的上限之内。
|
||||
Reference in new issue
Block a user