feat: 初始化 ChatGPT Team 管理机器人
核心功能: - 实现基于 Telegram Inline Button 交互的后台面板与用户端 - 支持通过账密登录和 RT (Refresh Token) 方式添加 ChatGPT Team 账号 - 支持管理、拉取和删除待处理邀请,支持一键清空多余邀请 - 支持按剩余容量自动生成邀请兑换码,支持分页查看与一键清空未使用兑换码 - 随机邀请功能:成功拉人后自动核销兑换码 - 定时检测 Token 状态,实现自动续订/刷新并拦截封禁账号 (处理 401/402 错误) 系统与配置: - 使用 PostgreSQL 数据库管理账号、邀请和兑换记录 - 支持在端内动态添加、移除管理员 - 完善 Docker 部署配置与 .gitignore 规则
This commit is contained in:
111
internal/chatgpt/account.go
Normal file
111
internal/chatgpt/account.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package chatgpt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go-helper/internal/model"
|
||||
)
|
||||
|
||||
// FetchAccountInfo queries the ChatGPT accounts check API and returns team accounts
|
||||
// with subscription expiry information.
|
||||
func (c *Client) FetchAccountInfo(accessToken string) ([]model.TeamAccountInfo, error) {
|
||||
token := strings.TrimPrefix(strings.TrimSpace(accessToken), "Bearer ")
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("缺少 access token")
|
||||
}
|
||||
|
||||
apiURL := "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27"
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
req.Header.Set("Oai-Client-Version", oaiClientVersion)
|
||||
req.Header.Set("Oai-Language", "zh-CN")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求 ChatGPT API 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 402 {
|
||||
return nil, fmt.Errorf("Token 已过期或被封禁")
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("ChatGPT API 错误 %d: %s", resp.StatusCode, truncate(string(body), 300))
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Accounts map[string]json.RawMessage `json:"accounts"`
|
||||
AccountOrdering []string `json:"account_ordering"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
var results []model.TeamAccountInfo
|
||||
|
||||
// Determine order.
|
||||
seen := make(map[string]bool)
|
||||
var orderedIDs []string
|
||||
for _, id := range data.AccountOrdering {
|
||||
if _, ok := data.Accounts[id]; ok && !seen[id] {
|
||||
orderedIDs = append(orderedIDs, id)
|
||||
seen[id] = true
|
||||
}
|
||||
}
|
||||
for id := range data.Accounts {
|
||||
if !seen[id] && id != "default" {
|
||||
orderedIDs = append(orderedIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range orderedIDs {
|
||||
raw := data.Accounts[id]
|
||||
var acc struct {
|
||||
Account struct {
|
||||
Name string `json:"name"`
|
||||
PlanType string `json:"plan_type"`
|
||||
} `json:"account"`
|
||||
Entitlement struct {
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
HasActiveSubscription bool `json:"has_active_subscription"`
|
||||
} `json:"entitlement"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &acc); err != nil {
|
||||
continue
|
||||
}
|
||||
if acc.Account.PlanType != "team" {
|
||||
continue
|
||||
}
|
||||
results = append(results, model.TeamAccountInfo{
|
||||
AccountID: id,
|
||||
Name: acc.Account.Name,
|
||||
PlanType: acc.Account.PlanType,
|
||||
ExpiresAt: acc.Entitlement.ExpiresAt,
|
||||
HasActiveSubscription: acc.Entitlement.HasActiveSubscription,
|
||||
})
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("未找到 Team 类型的账号")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user