Files

275 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package invite
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"codex-pool/internal/client"
)
// DefaultProxy 默认代理(空字符串表示不使用代理)
const DefaultProxy = ""
// 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
}