package invite import ( "bytes" "encoding/json" "fmt" "io" "net/http" "codex-pool/internal/client" ) // DefaultProxy 默认代理 const DefaultProxy = "http://127.0.0.1:7890" // TeamInviter Team 邀请器 type TeamInviter struct { client *client.TLSClient accessToken string accountID string } // InviteRequest 邀请请求 type InviteRequest struct { EmailAddresses []string `json:"email_addresses"` Role string `json:"role"` ResendEmails bool `json:"resend_emails"` } // AccountCheckResponse 账号检查响应 type AccountCheckResponse struct { Accounts map[string]struct { Account struct { PlanType string `json:"plan_type"` } `json:"account"` } `json:"accounts"` } // AccountStatus 账户状态检查结果 type AccountStatus struct { Status string // active, banned, token_expired, error PlanType string // team, plus, free 等 AccountID string // 账户 ID Error string // 错误信息 } // New 创建邀请器 (使用默认代理) func New(accessToken string) *TeamInviter { c, _ := client.New(DefaultProxy) return &TeamInviter{ client: c, accessToken: accessToken, } } // NewWithProxy 创建邀请器 (指定代理) func NewWithProxy(accessToken, proxy string) *TeamInviter { c, _ := client.New(proxy) return &TeamInviter{ client: c, accessToken: accessToken, } } // SetAccountID 手动设置 account_id(当已有存储值时使用) func (t *TeamInviter) SetAccountID(accountID string) { t.accountID = accountID } // CheckAccountStatus 检查账户状态(封禁检测) // 基于 check_ban.py 的逻辑: // - HTTP 200 + 有账户数据 → active // - HTTP 200 + 无账户数据 → banned // - HTTP 401 → token_expired // - HTTP 403 → banned func (t *TeamInviter) CheckAccountStatus() AccountStatus { result := AccountStatus{Status: "error"} req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil) req.Header.Set("Authorization", "Bearer "+t.accessToken) req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") resp, err := t.client.Do(req) if err != nil { result.Error = fmt.Sprintf("请求失败: %v", err) return result } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) switch resp.StatusCode { case 200: var checkResp AccountCheckResponse if err := json.Unmarshal(body, &checkResp); err != nil { result.Error = fmt.Sprintf("解析失败: %v", err) return result } // 检查是否有账户数据 if len(checkResp.Accounts) == 0 { result.Status = "banned" result.Error = "无账户数据" return result } // 查找非 default 的账户 for accountID, info := range checkResp.Accounts { if accountID == "default" { continue } result.Status = "active" result.AccountID = accountID result.PlanType = info.Account.PlanType t.accountID = accountID return result } // 只有 default 账户,也视为正常 result.Status = "active" result.PlanType = "default" return result case 401: result.Status = "token_expired" result.Error = "Token 已过期" return result case 403: result.Status = "banned" result.Error = "账户被封禁 (403)" return result default: result.Error = fmt.Sprintf("HTTP %d", resp.StatusCode) return result } } // GetAccountID 获取 Team 的 account_id (workspace_id) func (t *TeamInviter) GetAccountID() (string, error) { req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil) req.Header.Set("Authorization", "Bearer "+t.accessToken) req.Header.Set("Accept", "application/json") resp, err := t.client.Do(req) if err != nil { return "", fmt.Errorf("请求失败: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(200, len(body))]) } var result AccountCheckResponse if err := json.Unmarshal(body, &result); err != nil { return "", fmt.Errorf("解析失败: %v", err) } // 查找 team plan 的 account_id for accountID, info := range result.Accounts { if accountID != "default" && info.Account.PlanType == "team" { t.accountID = accountID return accountID, nil } } // 如果没找到 team,返回第一个非 default 的 for accountID := range result.Accounts { if accountID != "default" { t.accountID = accountID return accountID, nil } } return "", fmt.Errorf("未找到 account_id") } // SendInvites 发送邀请 func (t *TeamInviter) SendInvites(emails []string) error { if t.accountID == "" { return fmt.Errorf("未设置 account_id,请先调用 GetAccountID()") } url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites", t.accountID) payload := InviteRequest{ EmailAddresses: emails, Role: "standard-user", ResendEmails: true, } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+t.accessToken) req.Header.Set("Content-Type", "application/json") req.Header.Set("Chatgpt-Account-Id", t.accountID) resp, err := t.client.Do(req) if err != nil { return fmt.Errorf("请求失败: %v", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 && resp.StatusCode != 201 { return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))]) } return nil } // GetPendingInvites 获取待处理的邀请列表 func (t *TeamInviter) GetPendingInvites() ([]string, error) { if t.accountID == "" { return nil, fmt.Errorf("未设置 account_id") } url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites?offset=0&limit=100&query=", t.accountID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+t.accessToken) resp, err := t.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result struct { Invites []struct { Email string `json:"email"` } `json:"invites"` } if err := json.Unmarshal(body, &result); err != nil { return nil, err } var emails []string for _, inv := range result.Invites { emails = append(emails, inv.Email) } return emails, nil } // AcceptInvite 接受邀请 (使用被邀请账号的 token) func AcceptInvite(inviteLink string, accessToken string) error { c, _ := client.New(DefaultProxy) req, _ := http.NewRequest("GET", inviteLink, nil) req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := c.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 && resp.StatusCode != 302 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(100, len(body))]) } return nil }