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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user