feat: Implement the ChatGPT Owner Demotion Console with new frontend and backend components.
This commit is contained in:
292
backend/internal/demote/demote.go
Normal file
292
backend/internal/demote/demote.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package demote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codex-pool/internal/client"
|
||||
)
|
||||
|
||||
// DemoteRequest 降级请求
|
||||
type DemoteRequest struct {
|
||||
AccessToken string `json:"access_token"` // JWT Token 或完整的 session JSON
|
||||
AccountID string `json:"account_id"` // 可选,Team ID
|
||||
Role string `json:"role"` // 目标角色: standard-user 或 account-admin
|
||||
Proxy string `json:"proxy"` // 可选,代理
|
||||
}
|
||||
|
||||
// DemoteResult 降级结果
|
||||
type DemoteResult struct {
|
||||
Success bool `json:"success"`
|
||||
Email string `json:"email,omitempty"`
|
||||
OriginalRole string `json:"original_role,omitempty"`
|
||||
NewRole string `json:"new_role,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SessionData ChatGPT session 数据结构
|
||||
type SessionData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
} `json:"user"`
|
||||
Account struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"account"`
|
||||
}
|
||||
|
||||
// JWTPayload JWT 解析结构
|
||||
type JWTPayload struct {
|
||||
Auth struct {
|
||||
AccountUserID string `json:"chatgpt_account_user_id"`
|
||||
} `json:"https://api.openai.com/auth"`
|
||||
Profile struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"https://api.openai.com/profile"`
|
||||
}
|
||||
|
||||
// decodeJWTPayload 解码 JWT 的 payload 部分
|
||||
func decodeJWTPayload(token string) (*JWTPayload, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid JWT format")
|
||||
}
|
||||
|
||||
payload := parts[1]
|
||||
// 补齐 base64 padding
|
||||
if m := len(payload) % 4; m != 0 {
|
||||
payload += strings.Repeat("=", 4-m)
|
||||
}
|
||||
|
||||
decoded, err := base64.URLEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
// 尝试标准 base64
|
||||
decoded, err = base64.StdEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWT payload: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var result JWTPayload
|
||||
if err := json.Unmarshal(decoded, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT payload: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// extractUserInfo 从 Token 和 Session 中提取用户信息
|
||||
func extractUserInfo(token string, session *SessionData) (userID, accountID, email string) {
|
||||
// 优先从 session 获取(更准确)
|
||||
if session != nil {
|
||||
if session.User.ID != "" {
|
||||
userID = session.User.ID
|
||||
}
|
||||
if session.Account.ID != "" {
|
||||
accountID = session.Account.ID
|
||||
}
|
||||
if session.User.Email != "" {
|
||||
email = session.User.Email
|
||||
}
|
||||
|
||||
// 如果已经获取到必要信息,直接返回
|
||||
if userID != "" && accountID != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 备选:从 JWT 解析
|
||||
payload, err := decodeJWTPayload(token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 chatgpt_account_user_id,格式可能是 user-xxx__account-id
|
||||
accountUserID := payload.Auth.AccountUserID
|
||||
if accountUserID != "" {
|
||||
if strings.Contains(accountUserID, "__") {
|
||||
parts := strings.Split(accountUserID, "__")
|
||||
if userID == "" {
|
||||
userID = parts[0]
|
||||
}
|
||||
if accountID == "" && len(parts) > 1 {
|
||||
accountID = parts[1]
|
||||
}
|
||||
} else if userID == "" {
|
||||
userID = accountUserID
|
||||
}
|
||||
}
|
||||
|
||||
if email == "" {
|
||||
email = payload.Profile.Email
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DemoteOwner 执行 Owner 降级
|
||||
func DemoteOwner(req DemoteRequest) *DemoteResult {
|
||||
// 验证角色
|
||||
validRoles := map[string]bool{
|
||||
"standard-user": true,
|
||||
"account-admin": true,
|
||||
}
|
||||
if !validRoles[req.Role] {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Error: "无效的角色,必须是 standard-user 或 account-admin",
|
||||
}
|
||||
}
|
||||
|
||||
accessToken := strings.TrimSpace(req.AccessToken)
|
||||
var session *SessionData
|
||||
|
||||
// 检测是否是完整的 session JSON
|
||||
if strings.HasPrefix(accessToken, "{") {
|
||||
if err := json.Unmarshal([]byte(accessToken), &session); err != nil {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Error: "无法解析 session JSON: " + err.Error(),
|
||||
}
|
||||
}
|
||||
accessToken = session.AccessToken
|
||||
if accessToken == "" {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Error: "session JSON 中没有 accessToken",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提取用户信息
|
||||
userID, accountID, email := extractUserInfo(accessToken, session)
|
||||
|
||||
// 如果请求中指定了 account_id,优先使用
|
||||
if req.AccountID != "" {
|
||||
accountID = req.AccountID
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Error: "无法获取 user_id,请提供完整的 session JSON",
|
||||
}
|
||||
}
|
||||
if accountID == "" {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Error: "无法获取 account_id,请提供完整的 session JSON 或指定 account_id",
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 TLS 客户端
|
||||
var tlsClient *client.TLSClient
|
||||
var lastErr error
|
||||
|
||||
// 403 重试机制 - 最多 3 次
|
||||
for retry := 0; retry < 3; retry++ {
|
||||
var err error
|
||||
tlsClient, err = client.New(req.Proxy)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
// 初始化会话 - 先访问 chatgpt.com 通过 CF 验证
|
||||
resp, err := tlsClient.Get("https://chatgpt.com")
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
tlsClient.Close()
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 403 {
|
||||
lastErr = fmt.Errorf("Cloudflare 403")
|
||||
tlsClient.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
lastErr = nil
|
||||
break
|
||||
}
|
||||
|
||||
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
tlsClient.Close()
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Email: email,
|
||||
Error: fmt.Sprintf("初始化会话失败(已重试3次): %v", lastErr),
|
||||
}
|
||||
}
|
||||
defer tlsClient.Close()
|
||||
|
||||
// 构建降级 API 请求
|
||||
apiURL := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/users/%s", accountID, userID)
|
||||
|
||||
payload := map[string]string{"role": req.Role}
|
||||
jsonBody, _ := json.Marshal(payload)
|
||||
|
||||
patchReq, err := http.NewRequest("PATCH", apiURL, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Email: email,
|
||||
Error: "创建请求失败: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
patchReq.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
patchReq.Header.Set("Content-Type", "application/json")
|
||||
patchReq.Header.Set("Accept", "*/*")
|
||||
patchReq.Header.Set("Origin", "https://chatgpt.com")
|
||||
patchReq.Header.Set("Referer", "https://chatgpt.com/")
|
||||
|
||||
resp, err := tlsClient.Do(patchReq)
|
||||
if err != nil {
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Email: email,
|
||||
Error: "请求失败: " + err.Error(),
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := client.ReadBodyString(resp)
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
roleDisplay := "普通成员"
|
||||
if req.Role == "account-admin" {
|
||||
roleDisplay = "管理员"
|
||||
}
|
||||
return &DemoteResult{
|
||||
Success: true,
|
||||
Email: email,
|
||||
NewRole: req.Role,
|
||||
Message: fmt.Sprintf("成功降级为%s", roleDisplay),
|
||||
}
|
||||
}
|
||||
|
||||
return &DemoteResult{
|
||||
Success: false,
|
||||
Email: email,
|
||||
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(body, 200)),
|
||||
}
|
||||
}
|
||||
|
||||
func truncateStr(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user