package docker import ( "context" "encoding/json" "fmt" "log" "time" "github.com/go-resty/resty/v2" "gitee.ltd/lxh/wechat-robot/internal/config" "gitee.ltd/lxh/wechat-robot/internal/model" ) const ( // WechatBotLabelKey 微信机器人容器标签键 WechatBotLabelKey = "app.type" // WechatBotLabelValue 微信机器人容器标签值 WechatBotLabelValue = "wechat-bot" ) // APIResponse API响应结构 type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` Data interface{} `json:"data"` } // QRCodeResponse 获取二维码响应 type QRCodeResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { UUID string `json:"Uuid"` ExpiredTime string `json:"ExpiredTime"` QRCodeBase64 string `json:"QRCodeBase64"` QRCodeURL string `json:"QRCodeURL"` } `json:"data"` } // CheckUuidResponse 检查二维码状态响应 type CheckUuidResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Status int `json:"Status"` } `json:"data"` } // AwakenLoginResponse 唤醒登录响应 type AwakenLoginResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { QrCodeResponse json.RawMessage `json:"QrCodeResponse"` } `json:"data"` } // AutoHeartbeatResponse 心跳响应 type AutoHeartbeatResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct{} } // AutoHeartbeatStatusResponse 心跳状态响应 type AutoHeartbeatStatusResponse struct { Success bool `json:"success"` Message string `json:"message"` Running bool `json:"running"` } // 定义一个HTTP客户端生成函数,用于向容器内的API发出请求 func newHTTPClient() *resty.Client { return resty.New(). SetTimeout(30*time.Second). SetHeader("Content-Type", "application/json") } // CreateRobotContainer 创建微信机器人容器 func CreateRobotContainer(ctx context.Context, cfg *config.DockerConfig, robotName string, port int) (string, string, error) { // 创建容器标签 labels := map[string]string{ WechatBotLabelKey: WechatBotLabelValue, "robot.name": robotName, } // 创建环境变量 env := []string{ "BOT_NAME=" + robotName, } // 创建容器 containerID, err := CreateContainer(ctx, cfg, "wechat-bot-"+robotName, env, labels, port) if err != nil { return "", "", err } // 启动容器 err = StartContainer(ctx, containerID) if err != nil { // 如果启动失败,尝试删除容器 RemoveContainer(ctx, containerID, true) return "", "", err } // 获取容器访问地址 containerHost, err := GetContainerHost(ctx, containerID, cfg) if err != nil { log.Printf("警告: 无法获取容器访问地址: %v", err) containerHost = "localhost:9000" // 使用默认值 } return containerID, containerHost, nil } // GetLoginQRCode 获取登录二维码(旧方法) func GetLoginQRCode(ctx context.Context, containerID string) (string, error) { return ExecuteCommandWithOutput(ctx, containerID, []string{"cat", "/data/qrcode.png"}) } // GetQRCode 获取登录二维码 func GetQRCode(ctx context.Context, containerID string, containerHost string) (*QRCodeResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/GetQRCode", containerHost) var response QRCodeResponse resp, err := client.R(). SetContext(ctx). SetBody("{}"). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("获取二维码请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("获取二维码API返回错误状态码: %d", resp.StatusCode()) } if !response.Success { return nil, fmt.Errorf("获取二维码API错误: %s", response.Message) } return &response, nil } // CheckUuid 检查二维码状态 func CheckUuid(ctx context.Context, containerID string, uuid string, containerHost string) (*CheckUuidResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/CheckUuid", containerHost) reqBody := map[string]string{"Uuid": uuid} var response CheckUuidResponse resp, err := client.R(). SetContext(ctx). SetBody(reqBody). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("检查二维码状态请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("检查二维码状态API返回错误状态码: %d", resp.StatusCode()) } return &response, nil } // AwakenLogin 唤醒登录 func AwakenLogin(ctx context.Context, containerID string, wxid string, containerHost string) (*AwakenLoginResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/AwakenLogin", containerHost) reqBody := map[string]string{"Wxid": wxid} var response AwakenLoginResponse resp, err := client.R(). SetContext(ctx). SetBody(reqBody). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("唤醒登录请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("唤醒登录API返回错误状态码: %d", resp.StatusCode()) } return &response, nil } // AutoHeartbeatStart 开启自动心跳 func AutoHeartbeatStart(ctx context.Context, containerID string, wxid string, containerHost string) (*AutoHeartbeatResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/AutoHeartbeatStart", containerHost) reqBody := map[string]string{"Wxid": wxid} var response AutoHeartbeatResponse resp, err := client.R(). SetContext(ctx). SetBody(reqBody). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("启动自动心跳请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("启动自动心跳API返回错误状态码: %d", resp.StatusCode()) } if !response.Success { return nil, fmt.Errorf("启动自动心跳API错误: %s", response.Message) } return &response, nil } // AutoHeartbeatStatus 获取自动心跳状态 func AutoHeartbeatStatus(ctx context.Context, containerID string, wxid string, containerHost string) (*AutoHeartbeatStatusResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/AutoHeartbeatStatus", containerHost) reqBody := map[string]string{"Wxid": wxid} var response AutoHeartbeatStatusResponse resp, err := client.R(). SetContext(ctx). SetBody(reqBody). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("获取自动心跳状态请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("获取自动心跳状态API返回错误状态码: %d", resp.StatusCode()) } return &response, nil } // AutoHeartbeatStop 停止自动心跳 func AutoHeartbeatStop(ctx context.Context, containerID string, wxid string, containerHost string) (*AutoHeartbeatResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/AutoHeartbeatStop", containerHost) reqBody := map[string]string{"Wxid": wxid} var response AutoHeartbeatResponse resp, err := client.R(). SetContext(ctx). SetBody(reqBody). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("停止自动心跳请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("停止自动心跳API返回错误状态码: %d", resp.StatusCode()) } if !response.Success { return nil, fmt.Errorf("停止自动心跳API错误: %s", response.Message) } return &response, nil } // LogOut 登出微信 func LogOut(ctx context.Context, containerID string, wxid string, containerHost string) (*AutoHeartbeatResponse, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/LogOut", containerHost) reqBody := map[string]string{"Wxid": wxid} var response AutoHeartbeatResponse resp, err := client.R(). SetContext(ctx). SetBody(reqBody). SetResult(&response). Post(url) if err != nil { return nil, fmt.Errorf("退出登录请求失败: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("退出登录API返回错误状态码: %d", resp.StatusCode()) } if !response.Success { return nil, fmt.Errorf("退出登录API错误: %s", response.Message) } return &response, nil } // LogoutWechatBot 登出微信机器人(旧方法,使用HTTP请求替代) func LogoutWechatBot(ctx context.Context, containerID string) error { // 获取容器IP(简化处理,默认使用localhost) hostIP := "localhost" client := newHTTPClient() url := fmt.Sprintf("http://%s:3000/api/logout", hostIP) var response APIResponse resp, err := client.R(). SetContext(ctx). SetResult(&response). Post(url) if err != nil { return fmt.Errorf("failed to execute logout command: %w", err) } if resp.StatusCode() != 200 { return fmt.Errorf("logout API returned non-200 status code: %d", resp.StatusCode()) } if !response.Success { return fmt.Errorf("logout failed: %s", response.Message) } return nil } // GetWechatBotStatus 获取微信机器人状态(使用HTTP请求替代) func GetWechatBotStatus(ctx context.Context, containerID string) (model.RobotStatus, string, error) { // 检查容器状态 status, err := GetContainerStatus(ctx, containerID) if err != nil { return model.RobotStatusError, "", fmt.Errorf("failed to get container status: %w", err) } if status != "running" { return model.RobotStatusOffline, "", nil } // 获取容器IP(简化处理,默认使用localhost) hostIP := "localhost" client := newHTTPClient() url := fmt.Sprintf("http://%s:3000/api/status", hostIP) var response APIResponse resp, err := client.R(). SetContext(ctx). SetResult(&response). Get(url) if err != nil { return model.RobotStatusError, "", fmt.Errorf("failed to get status result: %w", err) } if resp.StatusCode() != 200 { return model.RobotStatusError, "", fmt.Errorf("status API returned non-200 status code: %d", resp.StatusCode()) } if !response.Success { return model.RobotStatusError, response.Message, nil } // 解析状态数据 data, ok := response.Data.(map[string]interface{}) if !ok { return model.RobotStatusError, "invalid status data format", nil } isLoggedIn, ok := data["isLoggedIn"].(bool) if !ok { return model.RobotStatusError, "invalid login status format", nil } if isLoggedIn { return model.RobotStatusOnline, "", nil } return model.RobotStatusOffline, "", nil } // ListWechatBots 列出所有微信机器人容器 func ListWechatBots(ctx context.Context) ([]ContainerInfo, error) { // 过滤条件:所有微信机器人容器 filter := map[string][]string{ "label": {fmt.Sprintf("%s=%s", WechatBotLabelKey, WechatBotLabelValue)}, } return ListContainers(ctx, filter) } // GetWechatContacts 获取微信联系人列表 func GetWechatContacts(ctx context.Context, containerID string, containerHost string) (string, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/api/contacts", containerHost) resp, err := client.R(). SetContext(ctx). Get(url) if err != nil { return "", fmt.Errorf("获取联系人列表请求失败: %w", err) } return resp.String(), nil } // GetWechatGroupMembers 获取微信群成员 func GetWechatGroupMembers(ctx context.Context, containerID string, groupID string, containerHost string) (string, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/api/group/%s/members", containerHost, groupID) resp, err := client.R(). SetContext(ctx). Get(url) if err != nil { return "", fmt.Errorf("获取群成员请求失败: %w", err) } return resp.String(), nil } // GetWechatMessages 获取微信消息历史 func GetWechatMessages(ctx context.Context, containerID string, contactID string, limit int, containerHost string) (string, error) { client := newHTTPClient() url := fmt.Sprintf("http://%s/api/messages/%s?limit=%d", containerHost, contactID, limit) resp, err := client.R(). SetContext(ctx). Get(url) if err != nil { return "", fmt.Errorf("获取消息历史请求失败: %w", err) } return resp.String(), nil }