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

+44 -55
View File
@@ -14,40 +14,39 @@ import (
)
const (
// msgTypeUser marks an inbound user message (bot messages are type 2).
// msgTypeUser 标记一条入站用户消息(机器人消息的 type 为 2)。
msgTypeUser = 1
// item type codes from the iLink reference (design 03 §1).
// 来自 iLink 参考的 item 类型码(design 03 §1)。
itemText = 1
itemImage = 2
itemVoice = 3
itemFile = 4
itemVideo = 5
// retryFastCount is how many consecutive failures use the fast retry.
// retryFastCount 是使用快速重试的连续失败次数上限。
retryFastCount = 3
// retryFastDelay is the delay before the first failures.
// retryFastDelay 是头几次失败前的延迟。
retryFastDelay = 2 * time.Second
// retrySlowDelay is the backoff after retryFastCount consecutive failures.
// retrySlowDelay 是连续失败超过 retryFastCount 之后的退避延迟。
retrySlowDelay = 30 * time.Second
)
// getupdatesResp is the getupdates (long-poll) response envelope.
// getupdatesResp 是 getupdates(长轮询)的响应外壳。
type getupdatesResp struct {
// Ret is the top-level business result; non-zero is a transient error.
// Ret 是顶层业务结果;非零表示瞬时错误。
Ret int `json:"ret"`
// Errcode is the platform error code; -14 means iLink rejected the session.
// Errcode 是平台错误码;-14 表示 iLink 拒绝了该会话。
Errcode int `json:"errcode"`
// Msgs holds the raw inbound messages delivered in this poll.
// Msgs 保存本次轮询投递的原始入站消息。
Msgs []json.RawMessage `json:"msgs"`
// GetUpdatesBuf is the next Cursor to echo back.
// GetUpdatesBuf 是下一次要回传的游标(Cursor)。
GetUpdatesBuf string `json:"get_updates_buf"`
// LongPollTimeoutMs is the server's suggested hold time.
// LongPollTimeoutMs 是服务端建议的保持时长。
LongPollTimeoutMs int `json:"longpolling_timeout_ms"`
}
// rawMsg is the parsed common fields of an inbound WeixinMessage. iLink media
// field names are only partially public, so item_list entries are decoded
// defensively in parseItem.
// rawMsg 是一条入站 WeixinMessage 的公共字段解析结果。iLink 的媒体字段名只有一部分
// 是公开的,因此 item_list 条目在 parseItem 中做防御式解码。
type rawMsg struct {
ClientID string `json:"client_id"`
FromUserID string `json:"from_user_id"`
@@ -60,11 +59,9 @@ type rawMsg struct {
ItemList []json.RawMessage `json:"item_list"`
}
// ReadMessages runs the long-poll read loop until ctx is canceled. Each
// converted inbound message is delivered to the handler installed via
// SetMessageHandler when one is present, otherwise to emit. The loop is
// cooperative with ctx: a canceled context returns immediately and no timers
// outlive it.
// ReadMessages 运行长轮询读循环,直到 ctx 被取消。每条转换后的入站消息,在已通过
// SetMessageHandler 安装了处理器时交给它,否则交给 emit。该循环与 ctx 协作:上下文
// 被取消即立即返回,且没有任何定时器会活过它。
func (a *Adapter) ReadMessages(ctx context.Context, emit func(adapter.UnifiedMessage)) {
a.mu.RLock()
connected := a.connected
@@ -81,7 +78,7 @@ func (a *Adapter) ReadMessages(ctx context.Context, emit func(adapter.UnifiedMes
if ctx.Err() != nil {
return
}
// A long-poll timeout is the normal "no messages yet" path.
// 长轮询超时是正常的「暂无消息」路径。
if errors.Is(err, context.DeadlineExceeded) {
continue
}
@@ -95,11 +92,10 @@ func (a *Adapter) ReadMessages(ctx context.Context, emit func(adapter.UnifiedMes
failures = 0
if resp.Errcode == errcodeSessionRejected {
// iLink rejected this session credential: retrying cannot help and the poll
// loop would go mute for good. Record the explicit rejected state (which also
// drops the connected flag and logs at ERROR) and stop the loop; the owner
// has to re-scan, and everything downstream sees ErrSessionRejected until a
// fresh token arrives.
// iLink 拒绝了这份会话凭据:重试无济于事,轮询循环会永久变哑。记录显式的
// 被拒绝状态(它同时清掉 connected 标志并以 ERROR 记录日志)并停止循环;
// 主人必须重新扫码,在新 token 到来之前,下游看到的一律是
// ErrSessionRejected。
a.markRejected(rescanReasonGetUpdates)
return
}
@@ -127,8 +123,8 @@ func (a *Adapter) ReadMessages(ctx context.Context, emit func(adapter.UnifiedMes
}
}
// deliver routes a normalized message to the handler when installed, else to
// the emit callback (design/08 §1: push-style vs pull-style delivery).
// deliver 在已安装处理器时把归一化消息交给它,否则交给 emit 回调
// (design/08 §1:推送式与拉取式投递)。
func (a *Adapter) deliver(ctx context.Context, m adapter.UnifiedMessage, emit func(adapter.UnifiedMessage)) {
if h := a.handlerLocked(); h != nil {
h(ctx, m)
@@ -139,8 +135,8 @@ func (a *Adapter) deliver(ctx context.Context, m adapter.UnifiedMessage, emit fu
}
}
// fetchUpdates performs one long poll. The request context is bounded by
// LongPollTimeout if the caller has not already set a tighter deadline.
// fetchUpdates 执行一次长轮询。若调用方尚未设置更紧的期限,请求上下文以
// LongPollTimeout 为界。
func (a *Adapter) fetchUpdates(ctx context.Context) (*getupdatesResp, error) {
reqCtx, cancel := context.WithTimeout(ctx, a.cfg.LongPollTimeout)
defer cancel()
@@ -160,8 +156,7 @@ func (a *Adapter) fetchUpdates(ctx context.Context) (*getupdatesResp, error) {
return &out, nil
}
// wait sleeps for d or returns false when ctx is canceled. A non-positive
// delay returns immediately.
// wait 休眠 d,或在 ctx 被取消时返回 false。delay 非正数时立即返回。
func (a *Adapter) wait(ctx context.Context, d time.Duration) bool {
if d <= 0 {
return true
@@ -176,8 +171,7 @@ func (a *Adapter) wait(ctx context.Context, d time.Duration) bool {
}
}
// retryDelay grows from the fast retry to the slow backoff after enough
// consecutive failures (design/08 §5.4).
// retryDelay 在连续失败足够多之后,从快速重试切换到慢速退避(design/08 §5.4)。
func (a *Adapter) retryDelay(failures int) time.Duration {
if failures <= retryFastCount {
return retryFastDelay
@@ -185,9 +179,8 @@ func (a *Adapter) retryDelay(failures int) time.Duration {
return retrySlowDelay
}
// convertMessage maps a raw iLink message to a UnifiedMessage and returns the
// raw context_token so the caller can track it per peer. It returns nil when
// the message is not an inbound user message or cannot be parsed.
// convertMessage 把一条原始 iLink 消息映射为 UnifiedMessage,并返回原始
// context_token 以便调用方按对端跟踪它。消息不是入站用户消息、或无法解析时返回 nil。
func (a *Adapter) convertMessage(raw json.RawMessage) (string, *adapter.UnifiedMessage) {
var rm rawMsg
if err := json.Unmarshal(raw, &rm); err != nil {
@@ -226,10 +219,9 @@ func (a *Adapter) convertMessage(raw json.RawMessage) (string, *adapter.UnifiedM
return rm.ContextToken, m
}
// convertItems drains the item list into text, a primary content type and a
// media slice. Text from text items is concatenated; voice items contribute
// their ASR transcript; image/file/video items are attached lazily as CDN
// references (never downloaded during the poll).
// convertItems 把 item 列表抽取成文本、一个主内容类型和媒体切片。文本 item 的文本
// 会拼接起来;语音 item 贡献其 ASR 转写;图片/文件/视频 item 以 CDN 引用的形式惰性
// 挂上(轮询期间绝不下载)。
func (a *Adapter) convertItems(rawItems []json.RawMessage) (string, adapter.ContentType, []adapter.Media) {
var text strings.Builder
var media []adapter.Media
@@ -260,9 +252,8 @@ func (a *Adapter) convertItems(rawItems []json.RawMessage) (string, adapter.Cont
return content, adapter.ContentText, nil
}
// resolveGroup returns the group identifier when the message carries one,
// otherwise "" (direct chat). WeChat group signalling is ⚠️ per design/08 §4,
// so several candidate fields are checked defensively.
// resolveGroup 在消息携带群标识时返回它,否则返回 ""(私聊)。按 design/08 §4,
// 微信群信号是 ⚠️,因此对多个候选字段做防御式检查。
func resolveGroup(rm *rawMsg) string {
if rm.GroupID != "" {
return rm.GroupID
@@ -270,16 +261,15 @@ func resolveGroup(rm *rawMsg) string {
return rm.RoomID
}
// parsedItem is a normalized view of a single native item_list entry.
// parsedItem 是单个原生 item_list 条目的归一化视图。
type parsedItem struct {
kind int
text string
media adapter.Media
}
// parseItem decodes one item_list entry defensively. iLink media field names
// are not fully public, so a generic map is scanned for the CDN reference and
// metadata with several fallback keys.
// parseItem 防御式地解码一个 item_list 条目。iLink 的媒体字段名并未完全公开,因此
// 用通用 map 配合多个兜底键来扫描 CDN 引用与元数据。
func parseItem(raw json.RawMessage) parsedItem {
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
@@ -302,8 +292,7 @@ func parseItem(raw json.RawMessage) parsedItem {
}
}
// textOf reads the text payload from the named nested sub-object, falling back
// to a flat "text" key.
// textOf 从指定的嵌套子对象读取文本载荷,兜底回退到平铺的 "text" 键。
func textOf(m map[string]any, sub string) string {
if inner, ok := m[sub].(map[string]any); ok {
if s, ok := inner["text"].(string); ok {
@@ -316,8 +305,8 @@ func textOf(m map[string]any, sub string) string {
return ""
}
// mediaOf builds an adapter.Media from a media item, resolving the CDN URL and
// metadata from the flat item or a nested sub-object.
// mediaOf 从媒体 item 构造 adapter.Media,从平铺 item 或嵌套子对象解析 CDN URL
// 与元数据。
func mediaOf(m map[string]any, kind adapter.MediaKind) adapter.Media {
res := adapter.Media{Kind: kind}
res.URL = firstString(m, "url", "cdn_url", "download_url", "file_url", "cdn_ref")
@@ -327,7 +316,7 @@ func mediaOf(m map[string]any, kind adapter.MediaKind) adapter.Media {
if res.URL != "" {
return res
}
// Bounded scan of the common nested sub-objects for the CDN reference.
// 对常见的嵌套子对象做有界扫描,寻找 CDN 引用。
for _, sub := range []string{"image_item", "file_item", "video_item", "media_item", "cdn_item"} {
inner, ok := m[sub].(map[string]any)
if !ok {
@@ -350,7 +339,7 @@ func mediaOf(m map[string]any, kind adapter.MediaKind) adapter.Media {
return res
}
// intOf coerces a JSON number to int, returning 0 for missing or non-numeric.
// intOf 把 JSON 数字强制转换为 int,缺失或非数字时返回 0。
func intOf(v any) int {
f, ok := v.(float64)
if !ok {
@@ -359,7 +348,7 @@ func intOf(v any) int {
return int(f)
}
// firstString returns the first non-empty string among keys.
// firstString 返回 keys 中第一个非空字符串。
func firstString(m map[string]any, keys ...string) string {
for _, k := range keys {
if s, ok := m[k].(string); ok && s != "" {
@@ -369,7 +358,7 @@ func firstString(m map[string]any, keys ...string) string {
return ""
}
// firstInt64 returns the first non-zero integer among keys.
// firstInt64 返回 keys 中第一个非零整数。
func firstInt64(m map[string]any, keys ...string) int64 {
for _, k := range keys {
if f, ok := m[k].(float64); ok && f != 0 {