Files
Cyrene_v2/internal/channel/adapter/adapter.go
T
Aska 36bd19e997
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(weixin): 扫码登录二维码真实可扫——前端生成QR(修坏图) + DecodeConfig容忍空config
weixin qrcode_img_content 是微信liteapp HTML页(非图片),后端fetch转base64只得<!doctype>→前端img坏图。
- 后端 login-qr 改返回 qrUrl(qrcode_img_content URL)+qrCode(原始qrcode),去掉坏 qrImage
- 前端用 qrcode 库从 qrUrl 生成真实二维码图片(微信扫https链接即开连接页), 新增 qrcode+@types/qrcode(纯UI依赖)
- DecodeConfig: nil config map 当空map解码(不报错), 否则 pending账号(空config)构建被跳过→未注册→登录端点404
- 测试更新(DecodeConfigNil/admin QR端点断言 qrUrl+qrCode)
- 另有 weixinAccountFor 裸weixin兜底(此前提交 d3b0921) 保证 accountId 命中
- 实测: login-qr 200{waiting, qrUrl=89}, 前端能看到可扫二维码
build/vet/test 全绿, 已部署.
2026-09-07 21:32:19 +08:00

101 lines
4.2 KiB
Go

package adapter
import (
"context"
"encoding/json"
"errors"
"fmt"
)
// MessageHandler receives a normalized inbound message. It is the push-style
// counterpart to ReadMessages: adapters call it when the platform delivers a
// message asynchronously, so consumers do not have to run a polling loop.
type MessageHandler func(ctx context.Context, msg UnifiedMessage)
// ChannelInfo describes a channel the adapter exposes, used by ListChannels.
type ChannelInfo struct {
ChannelID string
ChannelType ChannelType
ChannelName string
GroupID string
GroupName string
}
// PlatformAdapter is the contract every cross-platform IM adapter implements.
// Implementations translate between a native platform SDK/protocol and the
// normalized UnifiedMessage/UnifiedResponse model. The API is deliberately
// narrow so it is trivial to mock and so a new platform plugs in without
// touching consumers.
//
// All I/O operations accept a context.Context per the project constraint and
// should return promptly when the context is canceled.
type PlatformAdapter interface {
// Name returns the platform identifier (for example "telegram").
Name() string
// Config returns a snapshot of the adapter's current configuration as a
// plain map, so callers can inspect or persist it without importing the
// concrete type. Use DecodeConfig to read it into a typed struct.
Config() map[string]any
// Connect establishes the platform session. It is idempotent.
Connect(ctx context.Context) error
// Disconnect tears down the platform session. It is idempotent.
Disconnect(ctx context.Context) error
// IsConnected reports whether the session is currently live.
IsConnected() bool
// HealthCheck verifies the session is usable, returning an error when not.
HealthCheck(ctx context.Context) error
// ReadMessages runs a blocking read loop, emitting each normalized inbound
// message to emit until ctx is canceled. Adapters that receive push
// notifications should instead deliver through SetMessageHandler.
ReadMessages(ctx context.Context, emit func(UnifiedMessage))
// SendMessage delivers a reply (potentially several messages) to the channel.
SendMessage(ctx context.Context, resp UnifiedResponse) error
// SendProactive pushes a message that was not triggered by a recent inbound
// message. Whether that is permitted is governed by Capabilities.Proactive
// and platform-specific limits (for example the weixin-ilink 24h window).
SendProactive(ctx context.Context, msg UnifiedMessage) error
// SetMessageHandler installs the callbacks receiving push-style inbound
// messages. A nil fn clears the handler.
SetMessageHandler(fn MessageHandler)
// Capabilities declares what the adapter supports.
Capabilities() Capabilities
// ListChannels returns the channels currently known to the adapter.
ListChannels(ctx context.Context) ([]ChannelInfo, error)
}
// Sentinel errors returned by adapters. Use errors.Is to test for them.
var (
// ErrNotConnected is returned when an operation requires a live session.
ErrNotConnected = errors.New("adapter: not connected")
// ErrUnsupported is returned when a requested capability is not available
// (for example proactive push on a channel that does not allow it).
ErrUnsupported = errors.New("adapter: operation not supported")
// ErrClosed is returned when an operation is attempted on a closed adapter.
ErrClosed = errors.New("adapter: closed")
)
// DecodeConfig decodes a raw config map produced by Config into the struct
// pointed to by v. It is a convenience for adapters and tests that model their
// configuration as strongly typed structs. Unknown keys are ignored; missing
// keys leave the corresponding zero value.
func DecodeConfig(raw map[string]any, v any) error {
// A nil config map is treated as empty (decodes to the zero value), so a
// pending account with no keys yet (e.g. a weixin-ilink scan-login account the
// panel created before binding) builds with defaults rather than erroring.
if raw == nil {
raw = map[string]any{}
}
b, err := json.Marshal(raw)
if err != nil {
return fmt.Errorf("adapter: encode config: %w", err)
}
if err := json.Unmarshal(b, v); err != nil {
return fmt.Errorf("adapter: decode config into %T: %w", v, err)
}
return nil
}