feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
194
backend/internal/auth/chromedp.go
Normal file
194
backend/internal/auth/chromedp.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
|
||||
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.Flag("no-sandbox", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
||||
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"),
|
||||
)
|
||||
|
||||
if proxy != "" {
|
||||
opts = append(opts, chromedp.ProxyServer(proxy))
|
||||
}
|
||||
|
||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer cancel()
|
||||
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
ctx, cancel = context.WithTimeout(ctx, 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var callbackURL string
|
||||
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
if req, ok := ev.(*network.EventRequestWillBeSent); ok {
|
||||
url := req.Request.URL
|
||||
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
|
||||
callbackURL = url
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
err := chromedp.Run(ctx,
|
||||
network.Enable(),
|
||||
chromedp.Navigate(authURL),
|
||||
chromedp.WaitReady("body"),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("访问失败: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if callbackURL != "" {
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
var currentURL string
|
||||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||||
|
||||
if strings.Contains(currentURL, "code=") {
|
||||
return ExtractCodeFromCallbackURL(currentURL), nil
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
emailSelectors := []string{
|
||||
`input[name="email"]`,
|
||||
`input[type="email"]`,
|
||||
`input[name="username"]`,
|
||||
}
|
||||
|
||||
var emailFilled bool
|
||||
for _, sel := range emailSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Clear(sel, chromedp.ByQuery),
|
||||
chromedp.SendKeys(sel, email, chromedp.ByQuery),
|
||||
)
|
||||
if err == nil {
|
||||
emailFilled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !emailFilled {
|
||||
return "", fmt.Errorf("未找到邮箱输入框")
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
buttonSelectors := []string{
|
||||
`button[type="submit"]`,
|
||||
`button[data-testid="login-button"]`,
|
||||
`button.continue-btn`,
|
||||
`input[type="submit"]`,
|
||||
}
|
||||
|
||||
for _, sel := range buttonSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
if callbackURL != "" {
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||||
if strings.Contains(currentURL, "code=") {
|
||||
return ExtractCodeFromCallbackURL(currentURL), nil
|
||||
}
|
||||
|
||||
passwordSelectors := []string{
|
||||
`input[name="current-password"]`,
|
||||
`input[name="password"]`,
|
||||
`input[type="password"]`,
|
||||
}
|
||||
|
||||
var passwordFilled bool
|
||||
for _, sel := range passwordSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Clear(sel, chromedp.ByQuery),
|
||||
chromedp.SendKeys(sel, password, chromedp.ByQuery),
|
||||
)
|
||||
if err == nil {
|
||||
passwordFilled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !passwordFilled {
|
||||
return "", fmt.Errorf("未找到密码输入框")
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
for _, sel := range buttonSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if callbackURL != "" {
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
var url string
|
||||
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
|
||||
if strings.Contains(url, "code=") {
|
||||
return ExtractCodeFromCallbackURL(url), nil
|
||||
}
|
||||
|
||||
if strings.Contains(url, "consent") {
|
||||
for _, sel := range buttonSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
if strings.Contains(url, "authorize") && teamID != "" {
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if callbackURL != "" {
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("授权超时")
|
||||
}
|
||||
167
backend/internal/auth/rod.go
Normal file
167
backend/internal/auth/rod.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/go-rod/stealth"
|
||||
)
|
||||
|
||||
// RodAuth 使用 Rod + Stealth 完成 OAuth 授权
|
||||
type RodAuth struct {
|
||||
browser *rod.Browser
|
||||
headless bool
|
||||
proxy string
|
||||
}
|
||||
|
||||
// NewRodAuth 创建 Rod 授权器
|
||||
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
l := launcher.New().
|
||||
Headless(headless).
|
||||
Set("disable-blink-features", "AutomationControlled").
|
||||
Set("disable-dev-shm-usage").
|
||||
Set("no-sandbox").
|
||||
Set("disable-gpu").
|
||||
Set("disable-extensions").
|
||||
Set("disable-background-networking").
|
||||
Set("disable-sync").
|
||||
Set("disable-translate").
|
||||
Set("metrics-recording-only").
|
||||
Set("no-first-run")
|
||||
|
||||
if proxy != "" {
|
||||
l = l.Proxy(proxy)
|
||||
}
|
||||
|
||||
controlURL, err := l.Launch()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("启动浏览器失败: %v", err)
|
||||
}
|
||||
|
||||
browser := rod.New().ControlURL(controlURL)
|
||||
if err := browser.Connect(); err != nil {
|
||||
return nil, fmt.Errorf("连接浏览器失败: %v", err)
|
||||
}
|
||||
|
||||
return &RodAuth{
|
||||
browser: browser,
|
||||
headless: headless,
|
||||
proxy: proxy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 关闭浏览器
|
||||
func (r *RodAuth) Close() {
|
||||
if r.browser != nil {
|
||||
r.browser.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteOAuth 完成 OAuth 授权
|
||||
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
|
||||
page, err := stealth.Page(r.browser)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建页面失败: %v", err)
|
||||
}
|
||||
defer page.Close()
|
||||
|
||||
page = page.Timeout(45 * time.Second)
|
||||
|
||||
if err := page.Navigate(authURL); err != nil {
|
||||
return "", fmt.Errorf("访问授权URL失败: %v", err)
|
||||
}
|
||||
|
||||
page.MustWaitDOMStable()
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
return code, nil
|
||||
}
|
||||
|
||||
emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("未找到邮箱输入框")
|
||||
}
|
||||
|
||||
emailInput.MustSelectAllText().MustInput(email)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
||||
btn.MustClick()
|
||||
}
|
||||
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
return code, nil
|
||||
}
|
||||
|
||||
passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("未找到密码输入框")
|
||||
}
|
||||
|
||||
passwordInput.MustSelectAllText().MustInput(password)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
||||
btn.MustClick()
|
||||
}
|
||||
|
||||
for i := 0; i < 66; i++ {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
return code, nil
|
||||
}
|
||||
|
||||
info, _ := page.Info()
|
||||
currentURL := info.URL
|
||||
|
||||
if strings.Contains(currentURL, "consent") {
|
||||
if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil {
|
||||
btn.Click(proto.InputMouseButtonLeft, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(currentURL, "authorize") && teamID != "" {
|
||||
wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID)
|
||||
if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil {
|
||||
wsBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("授权超时")
|
||||
}
|
||||
|
||||
// checkForCode 检查 URL 中是否包含 code
|
||||
func (r *RodAuth) checkForCode(page *rod.Page) string {
|
||||
info, err := page.Info()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(info.URL, "code=") {
|
||||
return ExtractCodeFromCallbackURL(info.URL)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权
|
||||
func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
auth, err := NewRodAuth(headless, proxy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer auth.Close()
|
||||
|
||||
return auth.CompleteOAuth(authURL, email, password, teamID)
|
||||
}
|
||||
|
||||
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
|
||||
func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
return CompleteWithRod(authURL, email, password, teamID, headless, proxy)
|
||||
}
|
||||
291
backend/internal/auth/s2a.go
Normal file
291
backend/internal/auth/s2a.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CodexClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
CodexRedirectURI = "http://localhost:1455/auth/callback"
|
||||
CodexScope = "openid profile email offline_access"
|
||||
)
|
||||
|
||||
// CodexTokens Codex Token 结构
|
||||
type CodexTokens struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ExpiredAt string `json:"expired_at,omitempty"`
|
||||
}
|
||||
|
||||
// S2AAuthURLRequest S2A 授权 URL 请求
|
||||
type S2AAuthURLRequest struct {
|
||||
ProxyID *int `json:"proxy_id,omitempty"`
|
||||
}
|
||||
|
||||
// S2AAuthURLResponse S2A 授权 URL 响应
|
||||
type S2AAuthURLResponse struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
AuthURL string `json:"auth_url"`
|
||||
SessionID string `json:"session_id"`
|
||||
} `json:"data"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// S2ACreateFromOAuthRequest 提交 OAuth 入库请求
|
||||
type S2ACreateFromOAuthRequest struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Concurrency int `json:"concurrency,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
GroupIDs []int `json:"group_ids,omitempty"`
|
||||
ProxyID *int `json:"proxy_id,omitempty"`
|
||||
}
|
||||
|
||||
// S2ACreateFromOAuthResponse 入库响应
|
||||
type S2ACreateFromOAuthResponse struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
Priority int `json:"priority"`
|
||||
} `json:"data"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateS2AAuthURL 从 S2A 生成 Codex 授权 URL
|
||||
func GenerateS2AAuthURL(s2aAPIBase, s2aAdminKey string, proxyID *int) (*S2AAuthURLResponse, error) {
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
apiURL := s2aAPIBase + "/api/v1/admin/openai/generate-auth-url"
|
||||
|
||||
payload := S2AAuthURLRequest{ProxyID: proxyID}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(body))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Api-Key", s2aAdminKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if len(respBody) > 0 && respBody[0] == '<' {
|
||||
return nil, fmt.Errorf("服务器返回 HTML: %s", string(respBody)[:min(100, len(respBody))])
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))])
|
||||
}
|
||||
|
||||
var result S2AAuthURLResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v, body: %s", err, string(respBody)[:min(100, len(respBody))])
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("S2A 错误: %s", result.Message)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SubmitS2AOAuth 提交 OAuth code 到 S2A 入库
|
||||
func SubmitS2AOAuth(s2aAPIBase, s2aAdminKey, sessionID, code, name string, concurrency, priority int, groupIDs []int, proxyID *int) (*S2ACreateFromOAuthResponse, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
apiURL := s2aAPIBase + "/api/v1/admin/openai/create-from-oauth"
|
||||
|
||||
payload := S2ACreateFromOAuthRequest{
|
||||
SessionID: sessionID,
|
||||
Code: code,
|
||||
Name: name,
|
||||
Concurrency: concurrency,
|
||||
Priority: priority,
|
||||
GroupIDs: groupIDs,
|
||||
ProxyID: proxyID,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(body))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Api-Key", s2aAdminKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result S2ACreateFromOAuthResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("S2A 入库失败: %s", result.Message)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// VerifyS2AAccount 验证账号入库状态
|
||||
func VerifyS2AAccount(s2aAPIBase, s2aAdminKey, email string) (bool, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/api/v1/admin/accounts?page=1&page_size=20&search=%s&timezone=Asia/Shanghai", s2aAPIBase, url.QueryEscape(email))
|
||||
|
||||
req, _ := http.NewRequest("GET", apiURL, nil)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Api-Key", s2aAdminKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Items []struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
} `json:"items"`
|
||||
Total int `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return false, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if result.Code != 0 || result.Data.Total == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, item := range result.Data.Items {
|
||||
if item.Status == "active" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ExtractCodeFromCallbackURL 从回调 URL 中提取 code
|
||||
func ExtractCodeFromCallbackURL(callbackURL string) string {
|
||||
parsedURL, err := url.Parse(callbackURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return parsedURL.Query().Get("code")
|
||||
}
|
||||
|
||||
// RefreshCodexToken 刷新 Codex token
|
||||
func RefreshCodexToken(refreshToken string, proxyURL string) (*CodexTokens, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
if proxyURL != "" {
|
||||
proxyURLParsed, _ := url.Parse(proxyURL)
|
||||
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURLParsed)}
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"client_id": {CodexClientID},
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {refreshToken},
|
||||
"scope": {"openid profile email"},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", strings.NewReader(data.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("刷新 token 失败: %d, %s", resp.StatusCode, string(body)[:min(200, len(body))])
|
||||
}
|
||||
|
||||
var tokens CodexTokens
|
||||
if err := json.Unmarshal(body, &tokens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokens.ExpiresIn > 0 {
|
||||
expiredAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second)
|
||||
tokens.ExpiredAt = expiredAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return &tokens, nil
|
||||
}
|
||||
|
||||
// ExtractWorkspaceFromCookie 从 cookie 提取 workspace_id
|
||||
func ExtractWorkspaceFromCookie(cookieValue string) string {
|
||||
parts := strings.Split(cookieValue, ".")
|
||||
if len(parts) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
payload := parts[0]
|
||||
if m := len(payload) % 4; m != 0 {
|
||||
payload += strings.Repeat("=", 4-m)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
decoded, err = base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Workspaces []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"workspaces"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(decoded, &data); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(data.Workspaces) > 0 {
|
||||
return data.Workspaces[0].ID
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user