feat: 初始化 ChatGPT Team 管理机器人
核心功能: - 实现基于 Telegram Inline Button 交互的后台面板与用户端 - 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号 - 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请 - 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码 - 随机邀请功能:成功拉人后自动核销兑换码 - 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误) 系统与配置: - 使用 PostgreSQL 数据库管理账号、邀请和兑换记录 - 支持在端内动态添加、移除管理员 - 完善 Docker 部署配置与 .gitignore 规则
This commit is contained in:
195
internal/chatgpt/member.go
Normal file
195
internal/chatgpt/member.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package chatgpt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go-helper/internal/model"
|
||||
)
|
||||
|
||||
// GetUsers fetches the list of members for a team account.
|
||||
func (c *Client) GetUsers(account *model.GptAccount) (int, []model.ChatGPTUser, error) {
|
||||
apiURL := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/users?offset=0&limit=100&query=",
|
||||
account.ChatgptAccountID)
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
req.Header = buildHeaders(account.Token, account.ChatgptAccountID)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("获取成员列表网络错误: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := checkAPIError(resp.StatusCode, body, "获取成员"); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Total int `json:"total"`
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
AccountUserID string `json:"account_user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return 0, nil, fmt.Errorf("解析成员数据失败: %w", err)
|
||||
}
|
||||
|
||||
var users []model.ChatGPTUser
|
||||
for _, item := range data.Items {
|
||||
users = append(users, model.ChatGPTUser{
|
||||
ID: item.ID,
|
||||
AccountUserID: item.AccountUserID,
|
||||
Email: item.Email,
|
||||
Role: item.Role,
|
||||
Name: item.Name,
|
||||
})
|
||||
}
|
||||
return data.Total, users, nil
|
||||
}
|
||||
|
||||
// DeleteUser removes a user from the team account.
|
||||
func (c *Client) DeleteUser(account *model.GptAccount, userID string) error {
|
||||
normalizedID := userID
|
||||
if !strings.HasPrefix(normalizedID, "user-") {
|
||||
normalizedID = "user-" + normalizedID
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/users/%s",
|
||||
account.ChatgptAccountID, normalizedID)
|
||||
|
||||
req, err := http.NewRequest("DELETE", apiURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header = buildHeaders(account.Token, account.ChatgptAccountID)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除用户网络错误: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := checkAPIError(resp.StatusCode, body, "删除用户"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[Member] 成功删除用户 %s (账号 %s)", normalizedID, account.ChatgptAccountID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInvites fetches pending invitations for a team account.
|
||||
func (c *Client) GetInvites(account *model.GptAccount) (int, []model.ChatGPTInvite, error) {
|
||||
apiURL := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites?offset=0&limit=100&query=",
|
||||
account.ChatgptAccountID)
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
req.Header = buildHeaders(account.Token, account.ChatgptAccountID)
|
||||
req.Header.Set("Referer", "https://chatgpt.com/admin/members?tab=invites")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("获取邀请列表网络错误: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := checkAPIError(resp.StatusCode, body, "获取邀请列表"); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Total int `json:"total"`
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
EmailAddress string `json:"email_address"`
|
||||
Role string `json:"role"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return 0, nil, fmt.Errorf("解析邀请数据失败: %w", err)
|
||||
}
|
||||
|
||||
var invites []model.ChatGPTInvite
|
||||
for _, item := range data.Items {
|
||||
invites = append(invites, model.ChatGPTInvite{
|
||||
ID: item.ID,
|
||||
EmailAddress: item.EmailAddress,
|
||||
Role: item.Role,
|
||||
})
|
||||
}
|
||||
return data.Total, invites, nil
|
||||
}
|
||||
|
||||
// DeleteInvite cancels a pending invitation by email on the team account.
|
||||
func (c *Client) DeleteInvite(account *model.GptAccount, email string) error {
|
||||
apiURL := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites",
|
||||
account.ChatgptAccountID)
|
||||
|
||||
payload := map[string]string{"email_address": strings.TrimSpace(strings.ToLower(email))}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest("DELETE", apiURL, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header = buildHeaders(account.Token, account.ChatgptAccountID)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Referer", "https://chatgpt.com/admin/members?tab=invites")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除邀请网络错误: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := checkAPIError(resp.StatusCode, body, "删除邀请"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[Member] 成功删除邀请 %s (账号 %s)", email, account.ChatgptAccountID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAPIError inspects the HTTP status code and returns a descriptive error.
|
||||
func checkAPIError(statusCode int, body []byte, label string) error {
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bodyStr := truncate(string(body), 500)
|
||||
|
||||
// Check for account_deactivated.
|
||||
if strings.Contains(bodyStr, "account_deactivated") {
|
||||
return fmt.Errorf("账号已停用 (account_deactivated)")
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case 401, 402:
|
||||
return fmt.Errorf("Token 已过期或被封禁")
|
||||
case 403:
|
||||
return fmt.Errorf("资源不存在或无权访问")
|
||||
case 429:
|
||||
return fmt.Errorf("API 请求过于频繁,请稍后重试")
|
||||
default:
|
||||
return fmt.Errorf("%s失败 (HTTP %d): %s", label, statusCode, bodyStr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user