From 5fcf08ed09b62d6b63fca9bbc4495f0effcfa37f Mon Sep 17 00:00:00 2001 From: Huaaudio Date: Sun, 22 Mar 2026 05:06:23 +0100 Subject: [PATCH] fix lint --- cmd/picoclaw/internal/onboard/command.go | 1 - cmd/picoclaw/internal/onboard/weixin.go | 4 +- pkg/channels/weixin/api.go | 45 +++++++------ pkg/channels/weixin/auth.go | 20 +++--- pkg/channels/weixin/types.go | 86 ++++++++++++------------ pkg/channels/weixin/weixin.go | 13 ++-- pkg/config/config.go | 12 ++-- 7 files changed, 93 insertions(+), 88 deletions(-) diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go index 103d03e47..1f94c6718 100644 --- a/cmd/picoclaw/internal/onboard/command.go +++ b/cmd/picoclaw/internal/onboard/command.go @@ -35,4 +35,3 @@ func NewOnboardCommand() *cobra.Command { return cmd } - diff --git a/cmd/picoclaw/internal/onboard/weixin.go b/cmd/picoclaw/internal/onboard/weixin.go index 8c9badbb5..c454de87f 100644 --- a/cmd/picoclaw/internal/onboard/weixin.go +++ b/cmd/picoclaw/internal/onboard/weixin.go @@ -116,7 +116,7 @@ func saveWeixinConfig(token, baseURL, proxy string) error { } func writeMinimalWeixinConfig(cfgPath, token, baseURL, proxy string) error { - if err := os.MkdirAll(internal.GetPicoclawHome(), 0755); err != nil { + if err := os.MkdirAll(internal.GetPicoclawHome(), 0o755); err != nil { return err } @@ -137,7 +137,7 @@ func writeMinimalWeixinConfig(cfgPath, token, baseURL, proxy string) error { if err != nil { return err } - if err := os.WriteFile(cfgPath, data, 0600); err != nil { + if err := os.WriteFile(cfgPath, data, 0o600); err != nil { return err } fmt.Printf("✓ Created config at %s\n", cfgPath) diff --git a/pkg/channels/weixin/api.go b/pkg/channels/weixin/api.go index 1733c7ecc..4d5d25e68 100644 --- a/pkg/channels/weixin/api.go +++ b/pkg/channels/weixin/api.go @@ -24,7 +24,7 @@ func NewApiClient(baseURL, token string, proxy string) (*ApiClient, error) { if baseURL == "" { baseURL = "https://ilinkai.weixin.qq.com/" } - + client := &http.Client{ // Default timeout; will be overridden per context } @@ -53,7 +53,7 @@ func randomWechatUIN() string { return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", uint32Val))) } -func (c *ApiClient) post(ctx context.Context, endpoint string, body interface{}, responseObj interface{}) error { +func (c *ApiClient) post(ctx context.Context, endpoint string, body any, responseObj any) error { u, err := url.Parse(c.BaseURL) if err != nil { return err @@ -72,16 +72,17 @@ func (c *ApiClient) post(ctx context.Context, endpoint string, body interface{}, req.Header.Set("Content-Type", "application/json") if endpoint == "ilink/bot/get_bot_qrcode" || endpoint == "ilink/bot/get_qrcode_status" { - // QR routes have different headers sometimes, but let's stick to base ones - if endpoint == "ilink/bot/get_qrcode_status" { - req.Header.Set("iLink-App-ClientVersion", "1") - } + // QR routes have different headers sometimes, but let's stick to base ones + if endpoint == "ilink/bot/get_qrcode_status" { + // Use direct map assignment to send exact header name the Tencent API expects + req.Header["iLink-App-ClientVersion"] = []string{"1"} + } } else { - req.Header.Set("AuthorizationType", "ilink_bot_token") - req.Header.Set("X-WECHAT-UIN", randomWechatUIN()) - if c.Token != "" { - req.Header.Set("Authorization", "Bearer "+c.Token) - } + req.Header["AuthorizationType"] = []string{"ilink_bot_token"} + req.Header["X-WECHAT-UIN"] = []string{randomWechatUIN()} + if c.Token != "" { + req.Header.Set("Authorization", "Bearer "+c.Token) + } } resp, err := c.HttpClient.Do(req) @@ -140,7 +141,7 @@ func (c *ApiClient) SendTyping(ctx context.Context, req SendTypingReq) error { } func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeResponse, error) { - // get_bot_qrcode is GET, not POST + // get_bot_qrcode is GET, not POST u, err := url.Parse(c.BaseURL) if err != nil { return nil, err @@ -149,18 +150,18 @@ func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeRespo q := u.Query() q.Set("bot_type", botType) u.RawQuery = q.Encode() - + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, err } - + resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() - + respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err @@ -168,7 +169,7 @@ func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeRespo if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("get_bot_qrcode failed: %d %s", resp.StatusCode, string(respBody)) } - + var qrcodeResp QRCodeResponse if err := json.Unmarshal(respBody, &qrcodeResp); err != nil { return nil, err @@ -177,7 +178,7 @@ func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeRespo } func (c *ApiClient) GetQRCodeStatus(ctx context.Context, qrcode string) (*StatusResponse, error) { - // get_qrcode_status is GET + // get_qrcode_status is GET u, err := url.Parse(c.BaseURL) if err != nil { return nil, err @@ -186,19 +187,19 @@ func (c *ApiClient) GetQRCodeStatus(ctx context.Context, qrcode string) (*Status q := u.Query() q.Set("qrcode", qrcode) u.RawQuery = q.Encode() - + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, err } - req.Header.Set("iLink-App-ClientVersion", "1") - + req.Header["iLink-App-ClientVersion"] = []string{"1"} + resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() - + respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err @@ -206,7 +207,7 @@ func (c *ApiClient) GetQRCodeStatus(ctx context.Context, qrcode string) (*Status if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("get_qrcode_status failed: %d %s", resp.StatusCode, string(respBody)) } - + var statusResp StatusResponse if err := json.Unmarshal(respBody, &statusResp); err != nil { return nil, err diff --git a/pkg/channels/weixin/auth.go b/pkg/channels/weixin/auth.go index e439d0c4c..adec2925c 100644 --- a/pkg/channels/weixin/auth.go +++ b/pkg/channels/weixin/auth.go @@ -7,10 +7,11 @@ import ( "time" "github.com/mdp/qrterminal/v3" + "github.com/sipeed/picoclaw/pkg/logger" ) -// AuthFlow opts +// AuthFlowOpts configures the interactive QR login flow. type AuthFlowOpts struct { BaseURL string BotType string @@ -20,7 +21,10 @@ type AuthFlowOpts struct { // PerformLoginInteractive starts the Weixin QR login flow and blocks until login is successful or times out. // It prints a QR code to the terminal for the user to scan. // Returns the BotToken, UserID, AccountID, and BaseUrl on success. -func PerformLoginInteractive(ctx context.Context, opts AuthFlowOpts) (botToken, userID, accountID, baseUrl string, err error) { +func PerformLoginInteractive( + ctx context.Context, + opts AuthFlowOpts, +) (botToken, userID, accountID, baseUrl string, err error) { if opts.BaseURL == "" { opts.BaseURL = "https://ilinkai.weixin.qq.com/" } @@ -46,11 +50,11 @@ func PerformLoginInteractive(ctx context.Context, opts AuthFlowOpts) (botToken, fmt.Println("Please scan the following QR code with WeChat to login:") fmt.Println("=======================================================") fmt.Println() - + // Create Small QR qrconfig := qrterminal.Config{ - Level: qrterminal.L, - Writer: os.Stdout, + Level: qrterminal.L, + Writer: os.Stdout, HalfBlocks: true, } qrterminal.GenerateWithConfig(qrResp.QrcodeImgContent, qrconfig) @@ -89,15 +93,15 @@ func PerformLoginInteractive(ctx context.Context, opts AuthFlowOpts) (botToken, if statusResp.BotToken == "" || statusResp.IlinkBotID == "" { return "", "", "", "", fmt.Errorf("login confirmed but missing bot_token or ilink_bot_id") } - logger.InfoCF("weixin", "Login successful", map[string]interface{}{ + logger.InfoCF("weixin", "Login successful", map[string]any{ "account_id": statusResp.IlinkBotID, }) - + return statusResp.BotToken, statusResp.IlinkUserID, statusResp.IlinkBotID, statusResp.Baseurl, nil case "expired": return "", "", "", "", fmt.Errorf("qrcode expired, please try again") default: - logger.WarnCF("weixin", "Unknown QR code status", map[string]interface{}{ + logger.WarnCF("weixin", "Unknown QR code status", map[string]any{ "status": statusResp.Status, }) } diff --git a/pkg/channels/weixin/types.go b/pkg/channels/weixin/types.go index f62de54df..06f80ab89 100644 --- a/pkg/channels/weixin/types.go +++ b/pkg/channels/weixin/types.go @@ -14,18 +14,18 @@ const ( ) type GetUploadUrlReq 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"` // base64 - BaseInfo BaseInfo `json:"base_info,omitempty"` + 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"` // base64 + BaseInfo BaseInfo `json:"base_info,omitempty"` } type GetUploadUrlResp struct { @@ -77,12 +77,12 @@ type ImageItem struct { } 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 int `json:"playtime,omitempty"` - Text string `json:"text,omitempty"` + 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 int `json:"playtime,omitempty"` + Text string `json:"text,omitempty"` } type FileItem struct { @@ -109,34 +109,34 @@ type RefMessage struct { } 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"` - RefMsg *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 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"` + RefMsg *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 WeixinMessage struct { - Seq int `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"` + Seq int `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"` } type GetUpdatesReq struct { diff --git a/pkg/channels/weixin/weixin.go b/pkg/channels/weixin/weixin.go index e17abec6d..aba66872c 100644 --- a/pkg/channels/weixin/weixin.go +++ b/pkg/channels/weixin/weixin.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/config" @@ -18,11 +19,11 @@ import ( // WeixinChannel is the Weixin channel implementation over Tencent iLink REST API. type WeixinChannel struct { *channels.BaseChannel - api *ApiClient - config config.WeixinConfig - ctx context.Context - cancel context.CancelFunc - bus *bus.MessageBus + api *ApiClient + config config.WeixinConfig + ctx context.Context + cancel context.CancelFunc + bus *bus.MessageBus // contextTokens stores the last context_token per user (from_user_id → context_token). // This is required by the iLink API to associate replies with the right chat session. contextTokens sync.Map @@ -76,7 +77,7 @@ func (c *WeixinChannel) Stop(ctx context.Context) error { return nil } -// pollLoop is the long-poll receive loop. It runs until ctx is cancelled. +// pollLoop is the long-poll receive loop. It runs until ctx is canceled. func (c *WeixinChannel) pollLoop(ctx context.Context) { const ( defaultPollTimeoutMs = 35_000 diff --git a/pkg/config/config.go b/pkg/config/config.go index fc1f1b7f4..ab981998b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -501,12 +501,12 @@ type WeComAIBotConfig struct { } type WeixinConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_WEIXIN_TOKEN"` - BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"` - Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"` + Token string `json:"token" env:"PICOCLAW_CHANNELS_WEIXIN_TOKEN"` + BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"` + Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"` + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"` } type PicoConfig struct {