核心功能: - 实现基于 Telegram Inline Button 交互的后台面板与用户端 - 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号 - 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请 - 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码 - 随机邀请功能:成功拉人后自动核销兑换码 - 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误) 系统与配置: - 使用 PostgreSQL 数据库管理账号、邀请和兑换记录 - 支持在端内动态添加、移除管理员 - 完善 Docker 部署配置与 .gitignore 规则
94 lines
2.5 KiB
Go
94 lines
2.5 KiB
Go
package chatgpt
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"go-helper/internal/model"
|
|
)
|
|
|
|
const (
|
|
maxInviteAttempts = 3
|
|
retryBaseDelay = 800 * time.Millisecond
|
|
retryMaxDelay = 5 * time.Second
|
|
)
|
|
|
|
// InviteUser sends an invitation to the given email on the specified team account.
|
|
func (c *Client) InviteUser(email string, account *model.GptAccount) error {
|
|
email = strings.TrimSpace(strings.ToLower(email))
|
|
if email == "" {
|
|
return fmt.Errorf("缺少邀请邮箱")
|
|
}
|
|
if account.Token == "" || account.ChatgptAccountID == "" {
|
|
return fmt.Errorf("账号配置不完整")
|
|
}
|
|
|
|
apiURL := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites", account.ChatgptAccountID)
|
|
payload := map[string]interface{}{
|
|
"email_addresses": []string{email},
|
|
"role": "standard-user",
|
|
"resend_emails": true,
|
|
}
|
|
bodyBytes, _ := json.Marshal(payload)
|
|
|
|
var lastErr error
|
|
for attempt := 1; attempt <= maxInviteAttempts; attempt++ {
|
|
req, err := http.NewRequest("POST", apiURL, bytes.NewReader(bodyBytes))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
headers := buildHeaders(account.Token, account.ChatgptAccountID)
|
|
req.Header = headers
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if account.OaiDeviceID != "" {
|
|
req.Header.Set("Oai-Device-Id", account.OaiDeviceID)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
lastErr = fmt.Errorf("网络错误: %w", err)
|
|
log.Printf("[Invite] 尝试 %d/%d 网络错误: %v", attempt, maxInviteAttempts, err)
|
|
if attempt < maxInviteAttempts {
|
|
sleepRetry(attempt)
|
|
}
|
|
continue
|
|
}
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
log.Printf("[Invite] 成功邀请 %s 到账号 %s", email, account.ChatgptAccountID)
|
|
return nil
|
|
}
|
|
|
|
lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(respBody), 300))
|
|
log.Printf("[Invite] 尝试 %d/%d 失败: %v", attempt, maxInviteAttempts, lastErr)
|
|
|
|
if !isRetryableStatus(resp.StatusCode) || attempt >= maxInviteAttempts {
|
|
break
|
|
}
|
|
sleepRetry(attempt)
|
|
}
|
|
return fmt.Errorf("邀请失败(已尝试 %d 次): %v", maxInviteAttempts, lastErr)
|
|
}
|
|
|
|
func sleepRetry(attempt int) {
|
|
delay := float64(retryBaseDelay) * math.Pow(2, float64(attempt-1))
|
|
if delay > float64(retryMaxDelay) {
|
|
delay = float64(retryMaxDelay)
|
|
}
|
|
time.Sleep(time.Duration(delay))
|
|
}
|
|
|
|
func isRetryableStatus(status int) bool {
|
|
return status == 408 || status == 429 || (status >= 500 && status <= 599)
|
|
}
|