mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-16 08:15:05 +00:00
* feat(auth): support named chatgpt oauth providers - add provider-scoped ChatGPT OAuth routes and CLI support - persist refresh tokens per provider and reject provider-type collisions - wire provider OAuth setup flows in the dashboard and setup UI Refs #448 * feat(agent): add chatgpt oauth account routing - add agent other_config routing for manual and round-robin selection - reuse routed provider resolution across resolver and pending loaders - add router, parser, and agent advanced dialog coverage for multi-account use Refs #448 * docs(api): describe chatgpt oauth routing - document named-provider ChatGPT OAuth auth routes - describe agent-side account routing and round-robin behavior - update OpenAPI agent config schema and provider type enum Refs #448 * fix(store): add missing agent key context helpers * feat(ui): clarify chatgpt oauth account setup and routing * docs(providers): align chatgpt oauth alias examples * feat(agent): add codex pool activity dashboard * fix(providers): harden codex oauth alias setup * feat(codex-pool): improve routing dashboard UX - redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence - add clearer selection, attention, and recent-proof states for pool members - make the lower panels fill the remaining desktop viewport while staying responsive * fix(store): resolve context helper merge duplication * feat(oauth): add codex pool quota and observation APIs - add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers - teach codex routing to surface pool activity, observation metadata, and quota-aware readiness - extend tests and HTTP docs/OpenAPI for the new pool monitoring flows * feat(web): add codex pool quota monitor and controls - add provider quota fetching, readiness badges, and live routing evidence on the account pool page - redesign pool setup and activity panels for multi-account management with localized copy updates - keep the live monitor internally scrollable and compact the account cards for better viewport fit * fix(web): clarify pool routing labels - rename the recent request badge from Direct to Selected - restore compact quota bars in the live pool cards * feat(codex-pool): add runtime health dashboard - derive per-provider success and failure health from routed Codex traces - surface routing, quota, and recent request evidence in the pool UI - align provider alias guidance and owner access with the dashboard role model * docs(auth): document tenant scoping and key roles * fix(auth): harden tenant and codex pool access control * fix(providers): align codex pool runtime defaults * feat(ui): tighten codex pool responsive layout * feat(chatgpt-oauth): refine codex pool management UX * feat(chatgpt-oauth): surface quota bars on provider pages - add compact quota bars to Codex provider rows and provider detail - fetch quota only for ready visible provider rows and ready detail aliases - fix managed-member detail visibility and tighten provider locale copy
127 lines
2.8 KiB
Go
127 lines
2.8 KiB
Go
package oauth
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type openAITokenMetadata struct {
|
|
AccountID string
|
|
PlanType string
|
|
}
|
|
|
|
type openAIJWTClaims struct {
|
|
Auth *openAIAuthClaims `json:"https://api.openai.com/auth"`
|
|
AccountID string `json:"https://api.openai.com/auth.chatgpt_account_id"`
|
|
PlanType string `json:"https://api.openai.com/auth.chatgpt_plan_type"`
|
|
}
|
|
|
|
type openAIAuthClaims struct {
|
|
AccountID string `json:"chatgpt_account_id"`
|
|
PlanType string `json:"chatgpt_plan_type"`
|
|
}
|
|
|
|
func parseOAuthSettings(raw json.RawMessage) OAuthSettings {
|
|
if len(raw) == 0 {
|
|
return OAuthSettings{}
|
|
}
|
|
var settings OAuthSettings
|
|
if err := json.Unmarshal(raw, &settings); err != nil {
|
|
return OAuthSettings{}
|
|
}
|
|
return settings
|
|
}
|
|
|
|
func mergeOAuthSettings(existing OAuthSettings, tokenResp *OpenAITokenResponse, expiresAt time.Time) OAuthSettings {
|
|
settings := existing
|
|
settings.ExpiresAt = expiresAt.Unix()
|
|
|
|
if scope := strings.TrimSpace(tokenResp.Scope); scope != "" {
|
|
settings.Scopes = scope
|
|
}
|
|
|
|
metadata := openAIMetadataFromTokenResponse(tokenResp)
|
|
if metadata.AccountID != "" {
|
|
settings.AccountID = metadata.AccountID
|
|
}
|
|
if metadata.PlanType != "" {
|
|
settings.PlanType = metadata.PlanType
|
|
}
|
|
|
|
return settings
|
|
}
|
|
|
|
func openAIMetadataFromTokenResponse(tokenResp *OpenAITokenResponse) openAITokenMetadata {
|
|
for _, token := range []string{tokenResp.IDToken, tokenResp.AccessToken} {
|
|
metadata, ok := parseOpenAIJWTMetadata(token)
|
|
if ok {
|
|
return metadata
|
|
}
|
|
}
|
|
return openAITokenMetadata{}
|
|
}
|
|
|
|
func parseOpenAIJWTMetadata(token string) (openAITokenMetadata, bool) {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
return openAITokenMetadata{}, false
|
|
}
|
|
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) < 2 {
|
|
return openAITokenMetadata{}, false
|
|
}
|
|
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return openAITokenMetadata{}, false
|
|
}
|
|
|
|
var claims openAIJWTClaims
|
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
|
return openAITokenMetadata{}, false
|
|
}
|
|
|
|
metadata := openAITokenMetadata{
|
|
AccountID: firstNonEmpty(
|
|
strings.TrimSpace(claims.AccountID),
|
|
strings.TrimSpace(claims.NestedAccountID()),
|
|
),
|
|
PlanType: firstNonEmpty(
|
|
strings.TrimSpace(claims.PlanType),
|
|
strings.TrimSpace(claims.NestedPlanType()),
|
|
),
|
|
}
|
|
|
|
if metadata.AccountID == "" && metadata.PlanType == "" {
|
|
return openAITokenMetadata{}, false
|
|
}
|
|
|
|
return metadata, true
|
|
}
|
|
|
|
func (c openAIJWTClaims) NestedAccountID() string {
|
|
if c.Auth == nil {
|
|
return ""
|
|
}
|
|
return c.Auth.AccountID
|
|
}
|
|
|
|
func (c openAIJWTClaims) NestedPlanType() string {
|
|
if c.Auth == nil {
|
|
return ""
|
|
}
|
|
return c.Auth.PlanType
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|