feat: 从 OpenAI JWT 提取 chatgpt_plan_type 并在前端展示

OAuth 授权和 token 刷新时从 id_token 的 OpenAI auth claim 中
提取 chatgpt_plan_type(plus/team/pro/free),存入 credentials,
账号管理页面 PlatformTypeBadge 显示订阅类型。
This commit is contained in:
QTom
2026-03-09 16:57:06 +08:00
parent c8eff34388
commit a582aa89a9
4 changed files with 33 additions and 1 deletions

View File

@@ -268,6 +268,7 @@ type IDTokenClaims struct {
type OpenAIAuthClaims struct { type OpenAIAuthClaims struct {
ChatGPTAccountID string `json:"chatgpt_account_id"` ChatGPTAccountID string `json:"chatgpt_account_id"`
ChatGPTUserID string `json:"chatgpt_user_id"` ChatGPTUserID string `json:"chatgpt_user_id"`
ChatGPTPlanType string `json:"chatgpt_plan_type"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
Organizations []OrganizationClaim `json:"organizations"` Organizations []OrganizationClaim `json:"organizations"`
} }
@@ -375,6 +376,7 @@ type UserInfo struct {
Email string Email string
ChatGPTAccountID string ChatGPTAccountID string
ChatGPTUserID string ChatGPTUserID string
PlanType string
UserID string UserID string
OrganizationID string OrganizationID string
Organizations []OrganizationClaim Organizations []OrganizationClaim
@@ -389,6 +391,7 @@ func (c *IDTokenClaims) GetUserInfo() *UserInfo {
if c.OpenAIAuth != nil { if c.OpenAIAuth != nil {
info.ChatGPTAccountID = c.OpenAIAuth.ChatGPTAccountID info.ChatGPTAccountID = c.OpenAIAuth.ChatGPTAccountID
info.ChatGPTUserID = c.OpenAIAuth.ChatGPTUserID info.ChatGPTUserID = c.OpenAIAuth.ChatGPTUserID
info.PlanType = c.OpenAIAuth.ChatGPTPlanType
info.UserID = c.OpenAIAuth.UserID info.UserID = c.OpenAIAuth.UserID
info.Organizations = c.OpenAIAuth.Organizations info.Organizations = c.OpenAIAuth.Organizations

View File

@@ -130,6 +130,7 @@ type OpenAITokenInfo struct {
ChatGPTAccountID string `json:"chatgpt_account_id,omitempty"` ChatGPTAccountID string `json:"chatgpt_account_id,omitempty"`
ChatGPTUserID string `json:"chatgpt_user_id,omitempty"` ChatGPTUserID string `json:"chatgpt_user_id,omitempty"`
OrganizationID string `json:"organization_id,omitempty"` OrganizationID string `json:"organization_id,omitempty"`
PlanType string `json:"plan_type,omitempty"`
} }
// ExchangeCode exchanges authorization code for tokens // ExchangeCode exchanges authorization code for tokens
@@ -202,6 +203,7 @@ func (s *OpenAIOAuthService) ExchangeCode(ctx context.Context, input *OpenAIExch
tokenInfo.ChatGPTAccountID = userInfo.ChatGPTAccountID tokenInfo.ChatGPTAccountID = userInfo.ChatGPTAccountID
tokenInfo.ChatGPTUserID = userInfo.ChatGPTUserID tokenInfo.ChatGPTUserID = userInfo.ChatGPTUserID
tokenInfo.OrganizationID = userInfo.OrganizationID tokenInfo.OrganizationID = userInfo.OrganizationID
tokenInfo.PlanType = userInfo.PlanType
} }
return tokenInfo, nil return tokenInfo, nil
@@ -246,6 +248,7 @@ func (s *OpenAIOAuthService) RefreshTokenWithClientID(ctx context.Context, refre
tokenInfo.ChatGPTAccountID = userInfo.ChatGPTAccountID tokenInfo.ChatGPTAccountID = userInfo.ChatGPTAccountID
tokenInfo.ChatGPTUserID = userInfo.ChatGPTUserID tokenInfo.ChatGPTUserID = userInfo.ChatGPTUserID
tokenInfo.OrganizationID = userInfo.OrganizationID tokenInfo.OrganizationID = userInfo.OrganizationID
tokenInfo.PlanType = userInfo.PlanType
} }
return tokenInfo, nil return tokenInfo, nil
@@ -510,6 +513,9 @@ func (s *OpenAIOAuthService) BuildAccountCredentials(tokenInfo *OpenAITokenInfo)
if tokenInfo.OrganizationID != "" { if tokenInfo.OrganizationID != "" {
creds["organization_id"] = tokenInfo.OrganizationID creds["organization_id"] = tokenInfo.OrganizationID
} }
if tokenInfo.PlanType != "" {
creds["plan_type"] = tokenInfo.PlanType
}
if strings.TrimSpace(tokenInfo.ClientID) != "" { if strings.TrimSpace(tokenInfo.ClientID) != "" {
creds["client_id"] = strings.TrimSpace(tokenInfo.ClientID) creds["client_id"] = strings.TrimSpace(tokenInfo.ClientID)
} }

View File

@@ -28,6 +28,10 @@
<Icon v-else name="key" size="xs" /> <Icon v-else name="key" size="xs" />
<span>{{ typeLabel }}</span> <span>{{ typeLabel }}</span>
</span> </span>
<!-- Plan type part (optional) -->
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1 border-l border-white/20', typeClass]">
<span>{{ planLabel }}</span>
</span>
</div> </div>
</template> </template>
@@ -40,6 +44,7 @@ import Icon from '@/components/icons/Icon.vue'
interface Props { interface Props {
platform: AccountPlatform platform: AccountPlatform
type: AccountType type: AccountType
planType?: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -65,6 +70,24 @@ const typeLabel = computed(() => {
} }
}) })
const planLabel = computed(() => {
if (!props.planType) return ''
const lower = props.planType.toLowerCase()
switch (lower) {
case 'plus':
return 'Plus'
case 'team':
return 'Team'
case 'chatgptpro':
case 'pro':
return 'Pro'
case 'free':
return 'Free'
default:
return props.planType
}
})
const platformClass = computed(() => { const platformClass = computed(() => {
if (props.platform === 'anthropic') { if (props.platform === 'anthropic') {
return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400' return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'

View File

@@ -171,7 +171,7 @@
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span> <span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
</template> </template>
<template #cell-platform_type="{ row }"> <template #cell-platform_type="{ row }">
<PlatformTypeBadge :platform="row.platform" :type="row.type" /> <PlatformTypeBadge :platform="row.platform" :type="row.type" :plan-type="row.credentials?.plan_type" />
</template> </template>
<template #cell-capacity="{ row }"> <template #cell-capacity="{ row }">
<AccountCapacityCell :account="row" /> <AccountCapacityCell :account="row" />