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

+33 -36
View File
@@ -16,19 +16,18 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// item kind constants reuse the iLink item type codes from poll.go, so
// outbound media maps to the same numbers as inbound parsing.
// item 类型常量复用 poll.go 里的 iLink item 类型码,使出站媒体与入站解析映射到
// 同一组数字。
// sendResult is the sendmessage response envelope.
// sendResult 是 sendmessage 的响应外壳。
type sendResult struct {
Ret int `json:"ret"`
Errcode int `json:"errcode"`
}
// SendMessage delivers a reply to the channel. Each message is sent to the
// peer resolved from the message (or resp.ReplyTo) using that peer's stored
// context_token, so the platform can associate the reply with the right
// conversation (design/08 §8).
// SendMessage 向渠道投递一条回复。每条消息都发给从该消息(或 resp.ReplyTo)
// 解析出的对端,并使用该对端已存的 context_token,以便平台把回复关联到正确的
// 会话(design/08 §8)。
func (a *Adapter) SendMessage(ctx context.Context, resp adapter.UnifiedResponse) error {
if err := a.sendGuard(); err != nil {
return err
@@ -45,11 +44,10 @@ func (a *Adapter) SendMessage(ctx context.Context, resp adapter.UnifiedResponse)
return nil
}
// SendProactive pushes a message that was not triggered by a recent inbound.
// WeChat iLink only allows this inside the 24h window since the user's last
// message (design/08 §3-4): when the user has been silent longer, or has never
// messaged, the push is rejected with ErrProactiveWindow and Capabilities.
// Proactive is only true so callers know the capability exists.
// SendProactive 推送一条并非由近期入站消息触发的消息。微信 iLink 只允许在用户
// 最后一条消息之后的 24 小时窗口内这么做(design/08 §3-4):用户沉默更久、或从未
// 发过消息时,推送以 ErrProactiveWindow 被拒,而 Capabilities.Proactive 仅为 true,
// 好让调用方知道该能力存在。
func (a *Adapter) SendProactive(ctx context.Context, msg adapter.UnifiedMessage) error {
if err := a.sendGuard(); err != nil {
return err
@@ -73,9 +71,9 @@ func (a *Adapter) SendProactive(ctx context.Context, msg adapter.UnifiedMessage)
return a.sendToOne(ctx, peer, msg)
}
// sendGuard reports the error a send must surface before touching the network: a
// session iLink already rejected (errcode=-14) reads as ErrSessionRejected — the
// owner must re-scan — while any other down session stays ErrNotConnected.
// sendGuard 报告发送在触网之前必须暴露的错误:iLink 已拒绝的会话(errcode=-14)
// 表现为 ErrSessionRejected——主人必须重新扫码——而其余任何断开状态仍是
// ErrNotConnected。
func (a *Adapter) sendGuard() error {
if rejected, reason := a.rejectedState(); rejected {
return fmt.Errorf("weixin_ilink: send: %w: %s", ErrSessionRejected, reason)
@@ -86,8 +84,8 @@ func (a *Adapter) sendGuard() error {
return nil
}
// sendToOne sends one normalized message to a peer as a native iLink message,
// composing its item_list from any text plus uploaded media.
// sendToOne 把一条归一化消息作为原生 iLink 消息发给某个对端,用文本加上已上传的
// 媒体组装其 item_list。
func (a *Adapter) sendToOne(ctx context.Context, peer string, m adapter.UnifiedMessage) error {
if peer == "" {
return ErrNoContextToken
@@ -114,8 +112,8 @@ func (a *Adapter) sendToOne(ctx context.Context, peer string, m adapter.UnifiedM
return a.sendNative(ctx, peer, ctxToken, items)
}
// buildMediaItem encrypts media bytes, uploads them to the CDN and returns the
// native item referencing the uploaded object and its AES key.
// buildMediaItem 加密媒体字节、上传到 CDN,并返回引用该已上传对象及其 AES 密钥的
// 原生 item。
func (a *Adapter) buildMediaItem(ctx context.Context, md adapter.Media) (any, error) {
plaintext := md.Data
if len(plaintext) == 0 && md.URL != "" {
@@ -144,14 +142,14 @@ func (a *Adapter) buildMediaItem(ctx context.Context, md adapter.Media) (any, er
return nativeMediaItem(nativeMediaTypeFor(md.Kind), uploadURL, key, md), nil
}
// sendNative posts a composed sendmessage payload.
// sendNative 提交一份组装好的 sendmessage 载荷。
func (a *Adapter) sendNative(ctx context.Context, peer, ctxToken string, items []any) error {
body, err := json.Marshal(map[string]any{
"msg": map[string]any{
"to_user_id": peer,
"client_id": newClientID(),
"message_type": 2, // bot message
"message_state": 2, // FINISH
"message_type": 2, // 机器人消息
"message_state": 2, // FINISH(结束态)
"context_token": ctxToken,
"item_list": items,
},
@@ -165,9 +163,8 @@ func (a *Adapter) sendNative(ctx context.Context, peer, ctxToken string, items [
return fmt.Errorf("weixin_ilink: sendmessage: %w", err)
}
if out.Errcode == errcodeSessionRejected {
// The send path sees the same rejected credential as the poll path: record
// the rejected state so the adapter stops claiming a live session and the
// panel can ask for a re-scan.
// 发送路径与轮询路径看到的是同一份被拒凭据:记录该拒绝状态,让适配器不再
// 宣称会话存活,面板也就可以要求重新扫码。
a.markRejected(rescanReasonSendMessage)
return ErrSessionRejected
}
@@ -177,8 +174,8 @@ func (a *Adapter) sendNative(ctx context.Context, peer, ctxToken string, items [
return nil
}
// getUploadURL requests a pre-signed CDN upload URL. The response field names
// vary across server versions, so several candidates are tried.
// getUploadURL 申请一条预签名的 CDN 上传 URL。响应字段名在不同服务端版本间有
// 差异,所以会尝试多个候选键。
func (a *Adapter) getUploadURL(ctx context.Context) (string, error) {
body, err := json.Marshal(map[string]any{})
if err != nil {
@@ -190,8 +187,8 @@ func (a *Adapter) getUploadURL(ctx context.Context) (string, error) {
return "", fmt.Errorf("weixin_ilink: getuploadurl: %w", err)
}
if intOf(raw["errcode"]) == errcodeSessionRejected {
// Media upload is part of the send path, so the same rejected credential is
// recorded here too rather than degrading into "no url returned".
// 媒体上传属于发送路径,所以这里同样记录这份被拒凭据,而不是退化成
//「未返回 url」。
a.markRejected(rescanReasonUploadURL)
return "", ErrSessionRejected
}
@@ -202,7 +199,7 @@ func (a *Adapter) getUploadURL(ctx context.Context) (string, error) {
return u, nil
}
// putBytes uploads encrypted media bytes to a pre-signed CDN URL.
// putBytes 把加密后的媒体字节上传到预签名 CDN URL。
func (a *Adapter) putBytes(ctx context.Context, uploadURL string, data []byte) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(data))
if err != nil {
@@ -220,7 +217,7 @@ func (a *Adapter) putBytes(ctx context.Context, uploadURL string, data []byte) e
return nil
}
// nativeTextItem builds a text item_list entry.
// nativeTextItem 构造一个文本 item_list 条目。
func nativeTextItem(text string) map[string]any {
return map[string]any{
"type": itemText,
@@ -228,8 +225,8 @@ func nativeTextItem(text string) map[string]any {
}
}
// nativeMediaItem builds a media item_list entry that references the uploaded
// object. Both cdn_ref and url are set because the exact field is not public.
// nativeMediaItem 构造一个引用已上传对象的媒体 item_list 条目。同时设置 cdn_ref
// 与 url,因为确切的字段名并未公开。
func nativeMediaItem(kind int, ref string, key []byte, md adapter.Media) map[string]any {
item := map[string]any{
"type": kind,
@@ -245,7 +242,7 @@ func nativeMediaItem(kind int, ref string, key []byte, md adapter.Media) map[str
return item
}
// nativeMediaTypeFor maps a normalized MediaKind to the iLink item type code.
// nativeMediaTypeFor 把归一化的 MediaKind 映射到 iLink item 类型码。
func nativeMediaTypeFor(kind adapter.MediaKind) int {
switch kind {
case adapter.MediaImage:
@@ -259,8 +256,8 @@ func nativeMediaTypeFor(kind adapter.MediaKind) int {
}
}
// newClientID returns a random hex client id used to de-duplicate sends. On an
// (unlikely) crypto/rand failure it falls back to a time-based value.
// newClientID 返回随机的十六进制 client id,用于发送去重。若(极小概率的)
// crypto/rand 失败,则退回到基于时间的值。
func newClientID() string {
var b [12]byte
if _, err := rand.Read(b[:]); err == nil {