weixin

package module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 24, 2026 License: MIT Imports: 21 Imported by: 0

README

GoDoc Codecov

weixin-clawbot

中文文档 / Chinese

Go package for the Weixin QR login flow and bot API used by OpenClaw.

About

weixin-clawbot is a Go library for integrating with WeChat (Weixin) as a bot via the iLink Bot platform. It serves as the underlying client for the OpenClaw WeChat plugin.

It addresses the core problem of how to log into WeChat as a bot, receive messages, and reply automatically from Go code. The main capabilities are:

  1. QR-code login: Renders a QR code in the terminal; after the user scans it with their phone, the library persists the login credentials so subsequent runs don't require re-scanning.
  2. Message monitoring: Receives messages from WeChat contacts and groups in real time using long polling.
  3. Message sending: Sends text, image, video, and file messages to individual users or groups.
  4. Media handling: Uploads media files to the CDN (with AES-ECB encryption) and downloads inbound media to local storage.

If you are building a WeChat chatbot, automated messaging system, or customer-service bot, this library provides the low-level building blocks you need.

Install

go get github.com/daemon365/weixin-clawbot
import weixin "github.com/daemon365/weixin-clawbot"

Features

  • Interactive Weixin QR login with local account persistence
  • Long-poll message monitoring with sync buffer persistence
  • Text, image, video, and file sending helpers
  • CDN upload/download helpers with AES-ECB handling
  • Utilities for inbound media download and message conversion

Quick Start

1. Login
package main

import (
	"context"
	"log"
	"os"

	weixin "github.com/daemon365/weixin-clawbot"
)

func main() {
	client := weixin.NewClient(weixin.Options{})

	account, err := client.LoginInteractive(context.Background(), weixin.InteractiveLoginOptions{
		Output:  os.Stdout,
		SaveDir: ".weixin-accounts",
	})
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("account=%s token=%s", account.AccountID, account.BotToken)
}
2. Send a text message
ctx := context.Background()

sender := weixin.NewSender(weixin.SenderOptions{
	BaseURL:      "https://ilinkai.weixin.qq.com",
	Token:        "YOUR_BOT_TOKEN",
	Timeout:      15 * time.Second,
})
conversation := sender.Conversation(weixin.Target{
	ToUserID:     "user@im.wechat",
	ContextToken: "YOUR_CONTEXT_TOKEN",
})

clientID, err := conversation.SendText(ctx, "hello from bot")
if err != nil {
	log.Fatal(err)
}

log.Println("sent:", clientID)
3. Monitor updates
api := weixin.NewAPIClient(weixin.APIOptions{
	BaseURL: "https://ilinkai.weixin.qq.com",
	Token:   "YOUR_BOT_TOKEN",
})

err := weixin.Monitor(context.Background(), weixin.MonitorOptions{
	API:         api,
	AccountID:   "bot@im.bot",
	SyncBufPath: weixin.SyncBufFilePath(weixin.ResolveStateDir(), "bot@im.bot"),
	OnMessages: func(ctx context.Context, messages []weixin.WeixinMessage) error {
		for _, msg := range messages {
			log.Printf("from=%s body=%s", msg.FromUserID, weixin.BodyFromItemList(msg.ItemList))
		}
		return nil
	},
})
if err != nil {
	log.Fatal(err)
}

Main Types

  • Client: QR login flow
  • APIClient: ilink bot API wrapper
  • Sender: reusable outbound sender
  • Conversation: bound sender for one ToUserID + ContextToken
  • Target: outbound conversation target
  • MonitorOptions: long-poll monitor configuration
  • UploadedFileInfo: CDN upload result

Notes

  • The package name is weixin, while the module import path is github.com/daemon365/weixin-clawbot.
  • Account files are stored as base64url-encoded filenames to avoid unsafe path characters.
  • Target.ContextToken is required for outbound messaging.

Documentation

Overview

package weixin provides a small Go client for the ilink Weixin QR-login flow used by the OpenClaw Weixin plugin.

Minimal usage:

client := weixin.NewClient(weixin.Options{})
account, err := client.LoginInteractive(ctx, weixin.InteractiveLoginOptions{
    Output:  os.Stdout,
    SaveDir: ".weixin-accounts",
})
if err != nil {
    log.Fatal(err)
}

The returned account contains the ilink bot token and account identifiers that can be reused by your own application.

Index

Constants

View Source
const (
	DefaultBaseURL           = "https://ilinkai.weixin.qq.com"
	DefaultBotType           = "3"
	DefaultQRSessionTTL      = 5 * time.Minute
	DefaultQRLongPollTimeout = 35 * time.Second
	DefaultLoginTimeout      = 8 * time.Minute
	DefaultPollInterval      = time.Second
	DefaultMaxQRRefresh      = 3
)
View Source
const (
	UploadMediaTypeImage = 1
	UploadMediaTypeVideo = 2
	UploadMediaTypeFile  = 3
	UploadMediaTypeVoice = 4
)
View Source
const (
	MessageTypeNone = 0
	MessageTypeUser = 1
	MessageTypeBot  = 2
)
View Source
const (
	MessageItemTypeNone  = 0
	MessageItemTypeText  = 1
	MessageItemTypeImage = 2
	MessageItemTypeVoice = 3
	MessageItemTypeFile  = 4
	MessageItemTypeVideo = 5
)
View Source
const (
	MessageStateNew        = 0
	MessageStateGenerating = 1
	MessageStateFinish     = 2
)
View Source
const (
	TypingStatusTyping = 1
	TypingStatusCancel = 2
)
View Source
const (
	SessionExpiredErrCode = -14
)

Variables

This section is empty.

Functions

func AESECBPaddedSize

func AESECBPaddedSize(plaintextSize int64) int64

func AssertSessionActive

func AssertSessionActive(accountID string) error

func BodyFromItemList

func BodyFromItemList(items []MessageItem) string

func BuildCDNDownloadURL

func BuildCDNDownloadURL(encryptedQueryParam, cdnBaseURL string) string

func BuildCDNUploadURL

func BuildCDNUploadURL(cdnBaseURL, uploadParam, fileKey string) string

func DecryptAESECB

func DecryptAESECB(ciphertext, key []byte) ([]byte, error)

func DownloadAndDecryptBuffer

func DownloadAndDecryptBuffer(ctx context.Context, httpClient *http.Client, encryptedQueryParam, aesKeyBase64, cdnBaseURL string) ([]byte, error)

func DownloadPlainCDNBuffer

func DownloadPlainCDNBuffer(ctx context.Context, httpClient *http.Client, encryptedQueryParam, cdnBaseURL string) ([]byte, error)

func DownloadRemoteMediaToTemp

func DownloadRemoteMediaToTemp(ctx context.Context, httpClient *http.Client, rawURL, destDir string) (string, error)

func EncryptAESECB

func EncryptAESECB(plaintext, key []byte) ([]byte, error)

func ExtensionFromContentTypeOrURL

func ExtensionFromContentTypeOrURL(contentType, rawURL string) string

func ExtensionFromMIME

func ExtensionFromMIME(mimeType string) string

func GenerateID

func GenerateID(prefix string) string

func GetContextToken

func GetContextToken(accountID, userID string) string

func IsMediaItem

func IsMediaItem(item MessageItem) bool

func IsSessionPaused

func IsSessionPaused(accountID string) bool

func LoadSyncBuffer

func LoadSyncBuffer(filePath string) (string, error)

func MIMEFromFilename

func MIMEFromFilename(filename string) string

func MarkdownToPlainText

func MarkdownToPlainText(text string) string

func Monitor

func Monitor(ctx context.Context, opts MonitorOptions) error

func PauseSession

func PauseSession(accountID string)

func PrintQRCode

func PrintQRCode(w io.Writer, content string) error

func RemainingPause

func RemainingPause(accountID string) time.Duration

func ResolveStateDir

func ResolveStateDir() string

func SaveAccount

func SaveAccount(dir string, account *Account) (string, error)

func SaveSyncBuffer

func SaveSyncBuffer(filePath, getUpdatesBuf string) error

func SetContextToken

func SetContextToken(accountID, userID, token string)

func SyncBufFilePath

func SyncBufFilePath(stateDir, accountID string) string

func TempFileName

func TempFileName(prefix, ext string) string

func UploadBufferToCDN

func UploadBufferToCDN(ctx context.Context, httpClient *http.Client, buf []byte, uploadParam, fileKey, cdnBaseURL string, aesKey []byte) (string, error)

Types

type APIClient

type APIClient struct {
	// contains filtered or unexported fields
}

func NewAPIClient

func NewAPIClient(opts APIOptions) *APIClient

func (*APIClient) BuildBaseInfo

func (c *APIClient) BuildBaseInfo() BaseInfo

func (*APIClient) GetConfig

func (c *APIClient) GetConfig(ctx context.Context, ilinkUserID, contextToken string, timeout time.Duration) (*GetConfigResponse, error)

func (*APIClient) GetUpdates

func (c *APIClient) GetUpdates(ctx context.Context, req GetUpdatesRequest, timeout time.Duration) (*GetUpdatesResponse, error)

func (*APIClient) GetUploadURL

func (c *APIClient) GetUploadURL(ctx context.Context, req GetUploadURLRequest, timeout time.Duration) (*GetUploadURLResponse, error)

func (*APIClient) SendMessage

func (c *APIClient) SendMessage(ctx context.Context, req SendMessageRequest, timeout time.Duration) error

func (*APIClient) SendTyping

func (c *APIClient) SendTyping(ctx context.Context, req SendTypingRequest, timeout time.Duration) error

type APIOptions

type APIOptions struct {
	BaseURL        string
	Token          string
	RouteTag       string
	ChannelVersion string
	HTTPClient     *http.Client
	AccountID      string
}

type Account

type Account struct {
	AccountID string `json:"account_id"`
	BotToken  string `json:"bot_token"`
	BaseURL   string `json:"base_url,omitempty"`
	UserID    string `json:"user_id,omitempty"`
	SavedAt   string `json:"saved_at,omitempty"`
}

func ListAccounts

func ListAccounts(dir string) ([]Account, error)

func LoadAccount

func LoadAccount(dir, accountID string) (*Account, error)

type BaseInfo

type BaseInfo struct {
	ChannelVersion string `json:"channel_version,omitempty"`
}

type CDNMedia

type CDNMedia struct {
	EncryptQueryParam string `json:"encrypt_query_param,omitempty"`
	AESKey            string `json:"aes_key,omitempty"`
	EncryptType       int    `json:"encrypt_type,omitempty"`
}

type CachedConfig

type CachedConfig struct {
	TypingTicket string
}

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(opts Options) *Client

func (*Client) LoginInteractive

func (c *Client) LoginInteractive(ctx context.Context, opts InteractiveLoginOptions) (*Account, error)

func (*Client) StartLogin

func (c *Client) StartLogin(ctx context.Context, accountHint string) (*LoginSession, error)

func (*Client) WaitLogin

func (c *Client) WaitLogin(ctx context.Context, session *LoginSession, opts WaitOptions) (*Account, error)

type ConfigManager

type ConfigManager struct {
	// contains filtered or unexported fields
}

func NewConfigManager

func NewConfigManager(api *APIClient) *ConfigManager

func (*ConfigManager) GetForUser

func (m *ConfigManager) GetForUser(ctx context.Context, userID, contextToken string) (CachedConfig, error)

type Conversation added in v0.0.2

type Conversation struct {
	// contains filtered or unexported fields
}

func (*Conversation) SendFile added in v0.0.2

func (c *Conversation) SendFile(ctx context.Context, text, fileName string, uploaded UploadedFileInfo) (string, error)

func (*Conversation) SendImage added in v0.0.2

func (c *Conversation) SendImage(ctx context.Context, text string, uploaded UploadedFileInfo) (string, error)

func (*Conversation) SendMediaFile added in v0.0.2

func (c *Conversation) SendMediaFile(ctx context.Context, filePath, text string) (string, error)

func (*Conversation) SendText added in v0.0.2

func (c *Conversation) SendText(ctx context.Context, text string) (string, error)

func (*Conversation) SendVideo added in v0.0.2

func (c *Conversation) SendVideo(ctx context.Context, text string, uploaded UploadedFileInfo) (string, error)

type FileItem

type FileItem struct {
	Media    *CDNMedia `json:"media,omitempty"`
	FileName string    `json:"file_name,omitempty"`
	MD5      string    `json:"md5,omitempty"`
	Length   string    `json:"len,omitempty"`
}

type GetConfigResponse

type GetConfigResponse struct {
	Ret          int    `json:"ret,omitempty"`
	ErrMsg       string `json:"errmsg,omitempty"`
	TypingTicket string `json:"typing_ticket,omitempty"`
}

type GetUpdatesRequest

type GetUpdatesRequest struct {
	SyncBuf       string `json:"sync_buf,omitempty"`
	GetUpdatesBuf string `json:"get_updates_buf,omitempty"`
}

type GetUpdatesResponse

type GetUpdatesResponse struct {
	Ret                  int             `json:"ret,omitempty"`
	ErrCode              int             `json:"errcode,omitempty"`
	ErrMsg               string          `json:"errmsg,omitempty"`
	Messages             []WeixinMessage `json:"msgs,omitempty"`
	SyncBuf              string          `json:"sync_buf,omitempty"`
	GetUpdatesBuf        string          `json:"get_updates_buf,omitempty"`
	LongPollingTimeoutMS int             `json:"longpolling_timeout_ms,omitempty"`
}

type GetUploadURLRequest

type GetUploadURLRequest struct {
	FileKey         string `json:"filekey,omitempty"`
	MediaType       int    `json:"media_type,omitempty"`
	ToUserID        string `json:"to_user_id,omitempty"`
	RawSize         int64  `json:"rawsize,omitempty"`
	RawFileMD5      string `json:"rawfilemd5,omitempty"`
	FileSize        int64  `json:"filesize,omitempty"`
	ThumbRawSize    int64  `json:"thumb_rawsize,omitempty"`
	ThumbRawFileMD5 string `json:"thumb_rawfilemd5,omitempty"`
	ThumbFileSize   int64  `json:"thumb_filesize,omitempty"`
	NoNeedThumb     bool   `json:"no_need_thumb,omitempty"`
	AESKey          string `json:"aeskey,omitempty"`
}

type GetUploadURLResponse

type GetUploadURLResponse struct {
	UploadParam      string `json:"upload_param,omitempty"`
	ThumbUploadParam string `json:"thumb_upload_param,omitempty"`
}

type ImageItem

type ImageItem struct {
	Media       *CDNMedia `json:"media,omitempty"`
	ThumbMedia  *CDNMedia `json:"thumb_media,omitempty"`
	AESKeyHex   string    `json:"aeskey,omitempty"`
	URL         string    `json:"url,omitempty"`
	MidSize     int64     `json:"mid_size,omitempty"`
	ThumbSize   int64     `json:"thumb_size,omitempty"`
	ThumbHeight int64     `json:"thumb_height,omitempty"`
	ThumbWidth  int64     `json:"thumb_width,omitempty"`
	HDSize      int64     `json:"hd_size,omitempty"`
}

type InboundMediaOptions

type InboundMediaOptions struct {
	DecryptedPicPath   string
	DecryptedVoicePath string
	VoiceMediaType     string
	DecryptedFilePath  string
	FileMediaType      string
	DecryptedVideoPath string
}

func DownloadMediaFromItem

func DownloadMediaFromItem(ctx context.Context, item MessageItem, cdnBaseURL string, httpClient *http.Client, saveMedia SaveMediaFunc, silkToWAV SilkToWAVFunc) (*InboundMediaOptions, error)

type InteractiveLoginOptions

type InteractiveLoginOptions struct {
	AccountHint string
	Timeout     time.Duration
	Output      io.Writer
	SaveDir     string
}

type LoginSession

type LoginSession struct {
	SessionKey  string
	AccountHint string
	QRCode      string
	QRContent   string
	StartedAt   time.Time
}

type MessageAPI added in v0.0.2

type MessageAPI interface {
	SendMessage(ctx context.Context, req SendMessageRequest, timeout time.Duration) error
	GetUploadURL(ctx context.Context, req GetUploadURLRequest, timeout time.Duration) (*GetUploadURLResponse, error)
}

type MessageContext

type MessageContext struct {
	Body              string
	From              string
	To                string
	AccountID         string
	OriginatingTo     string
	MessageSID        string
	Timestamp         int64
	Provider          string
	ChatType          string
	SessionKey        string
	ContextToken      string
	MediaURL          string
	MediaPath         string
	MediaType         string
	CommandBody       string
	CommandAuthorized bool
}

func WeixinMessageToContext

func WeixinMessageToContext(msg WeixinMessage, accountID string, opts *InboundMediaOptions) MessageContext

type MessageItem

type MessageItem struct {
	Type         int         `json:"type,omitempty"`
	CreateTimeMS int64       `json:"create_time_ms,omitempty"`
	UpdateTimeMS int64       `json:"update_time_ms,omitempty"`
	IsCompleted  bool        `json:"is_completed,omitempty"`
	MsgID        string      `json:"msg_id,omitempty"`
	RefMessage   *RefMessage `json:"ref_msg,omitempty"`
	TextItem     *TextItem   `json:"text_item,omitempty"`
	ImageItem    *ImageItem  `json:"image_item,omitempty"`
	VoiceItem    *VoiceItem  `json:"voice_item,omitempty"`
	FileItem     *FileItem   `json:"file_item,omitempty"`
	VideoItem    *VideoItem  `json:"video_item,omitempty"`
}

type MessageSender added in v0.0.2

type MessageSender interface {
	SendText(ctx context.Context, text string) (string, error)
	SendImage(ctx context.Context, text string, uploaded UploadedFileInfo) (string, error)
	SendVideo(ctx context.Context, text string, uploaded UploadedFileInfo) (string, error)
	SendFile(ctx context.Context, text, fileName string, uploaded UploadedFileInfo) (string, error)
	SendMediaFile(ctx context.Context, filePath, text string) (string, error)
}

type MonitorOptions

type MonitorOptions struct {
	API             *APIClient
	AccountID       string
	SyncBufPath     string
	LongPollTimeout time.Duration
	AllowFrom       []string
	OnMessages      func(context.Context, []WeixinMessage) error
	OnError         func(error)
	OnStatus        func(lastEventAt time.Time)
}

type Options

type Options struct {
	BaseURL           string
	BotType           string
	RouteTag          string
	HTTPClient        *http.Client
	Output            io.Writer
	QRSessionTTL      time.Duration
	QRLongPollTimeout time.Duration
	PollInterval      time.Duration
	MaxQRRefresh      int
}

type RefMessage

type RefMessage struct {
	MessageItem *MessageItem `json:"message_item,omitempty"`
	Title       string       `json:"title,omitempty"`
}

type SaveMediaFunc

type SaveMediaFunc func(buffer []byte, contentType, subdir string, maxBytes int64, originalFilename string) (string, error)

func SaveMediaToDir

func SaveMediaToDir(rootDir string) SaveMediaFunc

type SendMessageRequest

type SendMessageRequest struct {
	Message *WeixinMessage `json:"msg,omitempty"`
}

type SendTypingRequest

type SendTypingRequest struct {
	ILinkUserID  string `json:"ilink_user_id,omitempty"`
	TypingTicket string `json:"typing_ticket,omitempty"`
	Status       int    `json:"status,omitempty"`
}

type SendTypingResponse

type SendTypingResponse struct {
	Ret    int    `json:"ret,omitempty"`
	ErrMsg string `json:"errmsg,omitempty"`
}

type Sender added in v0.0.2

type Sender struct {
	// contains filtered or unexported fields
}

func NewSender added in v0.0.2

func NewSender(opts SenderOptions) *Sender

func (*Sender) Conversation added in v0.0.2

func (s *Sender) Conversation(target Target) *Conversation

type SenderOptions added in v0.0.2

type SenderOptions struct {
	API            MessageAPI
	BaseURL        string
	Token          string
	RouteTag       string
	ChannelVersion string
	HTTPClient     *http.Client
	Timeout        time.Duration
	AccountID      string
	CDNBaseURL     string
}

type SilkToWAVFunc

type SilkToWAVFunc func(silk []byte) ([]byte, error)

type Target added in v0.0.2

type Target struct {
	ToUserID     string
	ContextToken string
}

type TextItem

type TextItem struct {
	Text string `json:"text,omitempty"`
}

type UploadedFileInfo

type UploadedFileInfo struct {
	FileKey                     string
	DownloadEncryptedQueryParam string
	AESKeyHex                   string
	FileSize                    int64
	FileSizeCiphertext          int64
}

func UploadFileAttachmentToWeixin

func UploadFileAttachmentToWeixin(ctx context.Context, filePath, toUserID, cdnBaseURL string, apiOpts APIOptions) (*UploadedFileInfo, error)

func UploadFileToWeixin

func UploadFileToWeixin(ctx context.Context, filePath, toUserID, cdnBaseURL string, apiOpts APIOptions) (*UploadedFileInfo, error)

func UploadVideoToWeixin

func UploadVideoToWeixin(ctx context.Context, filePath, toUserID, cdnBaseURL string, apiOpts APIOptions) (*UploadedFileInfo, error)

type VideoItem

type VideoItem struct {
	Media       *CDNMedia `json:"media,omitempty"`
	VideoSize   int64     `json:"video_size,omitempty"`
	PlayLength  int64     `json:"play_length,omitempty"`
	VideoMD5    string    `json:"video_md5,omitempty"`
	ThumbMedia  *CDNMedia `json:"thumb_media,omitempty"`
	ThumbSize   int64     `json:"thumb_size,omitempty"`
	ThumbHeight int64     `json:"thumb_height,omitempty"`
	ThumbWidth  int64     `json:"thumb_width,omitempty"`
}

type VoiceItem

type VoiceItem struct {
	Media         *CDNMedia `json:"media,omitempty"`
	EncodeType    int       `json:"encode_type,omitempty"`
	BitsPerSample int       `json:"bits_per_sample,omitempty"`
	SampleRate    int       `json:"sample_rate,omitempty"`
	Playtime      int64     `json:"playtime,omitempty"`
	Text          string    `json:"text,omitempty"`
}

type WaitOptions

type WaitOptions struct {
	Timeout time.Duration
	Output  io.Writer
	SaveDir string
}

type WeixinMessage

type WeixinMessage struct {
	Seq          int64         `json:"seq,omitempty"`
	MessageID    int64         `json:"message_id,omitempty"`
	FromUserID   string        `json:"from_user_id,omitempty"`
	ToUserID     string        `json:"to_user_id,omitempty"`
	ClientID     string        `json:"client_id,omitempty"`
	CreateTimeMS int64         `json:"create_time_ms,omitempty"`
	UpdateTimeMS int64         `json:"update_time_ms,omitempty"`
	DeleteTimeMS int64         `json:"delete_time_ms,omitempty"`
	SessionID    string        `json:"session_id,omitempty"`
	GroupID      string        `json:"group_id,omitempty"`
	MessageType  int           `json:"message_type,omitempty"`
	MessageState int           `json:"message_state,omitempty"`
	ItemList     []MessageItem `json:"item_list,omitempty"`
	ContextToken string        `json:"context_token,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL