mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(oauth): 新增 Codex account_user_id 和 organizations 字段采集、展示与判重
- 从 Codex id_token claims 和 token_response 中提取 account_user_id 和 organizations - OAuth 判重逻辑改为优先按 account_user_id 匹配,支持同用户不同 Team 不误判 - 号池和 Provider 详情页展示组织标签、account ID 和 account_user_id - 前端重复的 OAuth identity 工具函数提取到 utils/oauthIdentity.ts - 后端重复的 normalize_oauth_organizations 提取到 core/provider_oauth_utils.py
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import client from '../client'
|
||||
import { dedupedRequest } from '@/utils/cache'
|
||||
import type { AllowedModels, ProxyConfig } from './types/provider'
|
||||
import type { AllowedModels, OAuthOrganizationInfo, ProxyConfig } from './types/provider'
|
||||
|
||||
const POOL_BATCH_ACTION_TIMEOUT_MS = 5 * 60 * 1000
|
||||
|
||||
@@ -102,6 +102,9 @@ export interface PoolKeyDetail {
|
||||
oauth_invalid_at?: number | null
|
||||
oauth_invalid_reason?: string | null
|
||||
oauth_plan_type?: string | null
|
||||
oauth_account_id?: string | null
|
||||
oauth_account_user_id?: string | null
|
||||
oauth_organizations?: OAuthOrganizationInfo[] | null
|
||||
quota_updated_at?: number | null
|
||||
health_score?: number
|
||||
circuit_breaker_open?: boolean
|
||||
|
||||
@@ -12,6 +12,13 @@ export interface ProxyConfig {
|
||||
enabled?: boolean // 是否启用代理(false 时保留配置但不使用)
|
||||
}
|
||||
|
||||
export interface OAuthOrganizationInfo {
|
||||
id?: string | null
|
||||
title?: string | null
|
||||
is_default?: boolean | null
|
||||
role?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求头规则类型
|
||||
* - set: 设置/覆盖请求头
|
||||
@@ -264,6 +271,8 @@ export interface EndpointAPIKey {
|
||||
oauth_email?: string | null // OAuth 授权的邮箱
|
||||
oauth_plan_type?: string | null // Codex 订阅类型: plus/free/team/enterprise
|
||||
oauth_account_id?: string | null // Codex ChatGPT 账号 ID
|
||||
oauth_account_user_id?: string | null // Codex ChatGPT account-user 联合 ID
|
||||
oauth_organizations?: OAuthOrganizationInfo[] | null // OAuth 关联组织/工作区摘要
|
||||
oauth_invalid_at?: number | null // OAuth Token 失效时间(Unix 时间戳)
|
||||
oauth_invalid_reason?: string | null // OAuth Token 失效原因
|
||||
// 上游元数据(由上游响应采集,如 Codex 额度信息 / Antigravity 配额信息)
|
||||
|
||||
@@ -121,6 +121,18 @@
|
||||
variant="outline"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
>{{ key.oauth_plan_type }}</Badge>
|
||||
<Badge
|
||||
v-if="getPrimaryOAuthOrganizationTitle(key)"
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0 max-w-[92px] truncate"
|
||||
:title="getOAuthOrganizationsTooltip(key)"
|
||||
>{{ getPrimaryOAuthOrganizationTitle(key) }}</Badge>
|
||||
<Badge
|
||||
v-if="key.oauth_account_id"
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
:title="key.oauth_account_id"
|
||||
>acct {{ formatOAuthIdentityShort(key.oauth_account_id) }}</Badge>
|
||||
<Badge
|
||||
v-if="isBannedKey(key)"
|
||||
variant="destructive"
|
||||
@@ -129,6 +141,10 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 mt-0.5 text-[11px] text-muted-foreground flex-wrap">
|
||||
<span :class="key.is_active ? '' : 'text-destructive'">{{ key.is_active ? '启用' : '禁用' }}</span>
|
||||
<span
|
||||
v-if="key.oauth_account_user_id"
|
||||
:title="key.oauth_account_user_id"
|
||||
>AUID {{ formatOAuthIdentityShort(key.oauth_account_user_id, 10, 8) }}</span>
|
||||
<span v-if="key.account_quota">{{ shortenQuota(key.account_quota) }}</span>
|
||||
<span v-if="key.proxy?.node_id">独立代理</span>
|
||||
<span
|
||||
@@ -277,6 +293,7 @@ import {
|
||||
import { exportKey, refreshProviderQuota } from '@/api/endpoints/keys'
|
||||
import { refreshProviderOAuth } from '@/api/endpoints/provider_oauth'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import { formatOAuthIdentityShort, getPrimaryOAuthOrganizationTitle, getOAuthOrganizationsTooltip } from '@/utils/oauthIdentity'
|
||||
|
||||
type QuickSelectorValue =
|
||||
| 'banned'
|
||||
|
||||
@@ -289,6 +289,22 @@
|
||||
>
|
||||
{{ formatOAuthPlanType(key.oauth_plan_type) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getPrimaryOAuthOrganizationTitle(key)"
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0 max-w-[120px] truncate"
|
||||
:title="getOAuthOrganizationsTooltip(key)"
|
||||
>
|
||||
{{ getPrimaryOAuthOrganizationTitle(key) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="key.oauth_account_id"
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:title="key.oauth_account_id"
|
||||
>
|
||||
acct {{ formatOAuthIdentityShort(key.oauth_account_id) }}
|
||||
</Badge>
|
||||
<!-- Kiro 订阅类型标签 -->
|
||||
<Badge
|
||||
v-if="provider.provider_type === 'kiro' && key.upstream_metadata?.kiro?.subscription_title"
|
||||
@@ -303,6 +319,13 @@
|
||||
<span class="text-[11px] font-mono text-muted-foreground">
|
||||
{{ key.auth_type === 'oauth' ? '[Refresh Token]' : (key.auth_type === 'service_account' ? '[Service Account]' : key.api_key_masked) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="key.oauth_account_user_id"
|
||||
class="text-[10px] text-muted-foreground"
|
||||
:title="key.oauth_account_user_id"
|
||||
>
|
||||
AUID {{ formatOAuthIdentityShort(key.oauth_account_user_id, 10, 8) }}
|
||||
</span>
|
||||
<Button
|
||||
v-if="key.auth_type === 'oauth'"
|
||||
variant="ghost"
|
||||
@@ -1103,6 +1126,7 @@ import type { UpstreamMetadata, AntigravityModelQuota } from '@/api/endpoints/ty
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { isOAuthAccountProviderType, isKeyManagedProviderType } from '../utils/providerTypeUtils'
|
||||
import { isAccountLevelBlockReason, cleanAccountBlockReason } from '@/utils/accountBlock'
|
||||
import { formatOAuthIdentityShort, getPrimaryOAuthOrganizationTitle, getOAuthOrganizationsTooltip } from '@/utils/oauthIdentity'
|
||||
|
||||
// 扩展端点类型,包含密钥列表
|
||||
interface ProviderEndpointWithKeys extends ProviderEndpoint {
|
||||
|
||||
43
frontend/src/utils/oauthIdentity.ts
Normal file
43
frontend/src/utils/oauthIdentity.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { OAuthOrganizationInfo } from '@/api/endpoints/types/provider'
|
||||
|
||||
export function formatOAuthIdentityShort(
|
||||
value: string | null | undefined,
|
||||
head = 8,
|
||||
tail = 6,
|
||||
): string {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) return ''
|
||||
if (normalized.length <= head + tail + 3) return normalized
|
||||
return `${normalized.slice(0, head)}...${normalized.slice(-tail)}`
|
||||
}
|
||||
|
||||
export function getPrimaryOAuthOrganizationTitle(
|
||||
value: { oauth_organizations?: OAuthOrganizationInfo[] | null } | null | undefined,
|
||||
): string | null {
|
||||
const organizations = Array.isArray(value?.oauth_organizations) ? value.oauth_organizations : []
|
||||
const defaultOrg = organizations.find(
|
||||
(org) => org?.is_default && typeof org?.title === 'string' && org.title.trim(),
|
||||
)
|
||||
if (defaultOrg?.title) return defaultOrg.title.trim()
|
||||
const firstWithTitle = organizations.find(
|
||||
(org) => typeof org?.title === 'string' && org.title.trim(),
|
||||
)
|
||||
return firstWithTitle?.title?.trim() || null
|
||||
}
|
||||
|
||||
export function getOAuthOrganizationsTooltip(
|
||||
value: { oauth_organizations?: OAuthOrganizationInfo[] | null } | null | undefined,
|
||||
): string {
|
||||
const organizations = Array.isArray(value?.oauth_organizations) ? value.oauth_organizations : []
|
||||
if (organizations.length === 0) return ''
|
||||
return organizations
|
||||
.map((org) => {
|
||||
const title =
|
||||
typeof org?.title === 'string' && org.title.trim() ? org.title.trim() : '未命名组织'
|
||||
const role =
|
||||
typeof org?.role === 'string' && org.role.trim() ? ` (${org.role.trim()})` : ''
|
||||
const suffix = org?.is_default ? ' [default]' : ''
|
||||
return `${title}${role}${suffix}`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
@@ -492,6 +492,29 @@
|
||||
>
|
||||
{{ formatOAuthPlanType(key.oauth_plan_type) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getPrimaryOAuthOrganizationTitle(key)"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0 max-w-[92px] truncate"
|
||||
:title="getOAuthOrganizationsTooltip(key)"
|
||||
>
|
||||
{{ getPrimaryOAuthOrganizationTitle(key) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="key.oauth_account_id"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="key.oauth_account_id"
|
||||
>
|
||||
acct {{ formatOAuthIdentityShort(key.oauth_account_id) }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="key.oauth_account_user_id"
|
||||
class="text-[10px] text-muted-foreground shrink-0"
|
||||
:title="key.oauth_account_user_id"
|
||||
>
|
||||
AUID {{ formatOAuthIdentityShort(key.oauth_account_user_id, 10, 8) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -794,6 +817,29 @@
|
||||
>
|
||||
{{ formatOAuthPlanType(key.oauth_plan_type) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getPrimaryOAuthOrganizationTitle(key)"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0 max-w-[92px] truncate"
|
||||
:title="getOAuthOrganizationsTooltip(key)"
|
||||
>
|
||||
{{ getPrimaryOAuthOrganizationTitle(key) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="key.oauth_account_id"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="key.oauth_account_id"
|
||||
>
|
||||
acct {{ formatOAuthIdentityShort(key.oauth_account_id) }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="key.oauth_account_user_id"
|
||||
class="text-[10px] text-muted-foreground shrink-0"
|
||||
:title="key.oauth_account_user_id"
|
||||
>
|
||||
AUID {{ formatOAuthIdentityShort(key.oauth_account_user_id, 10, 8) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 shrink-0 flex-wrap justify-end max-w-[210px]">
|
||||
@@ -1161,7 +1207,12 @@ import type {
|
||||
PoolKeysPageResponse,
|
||||
PoolPresetMeta,
|
||||
} from '@/api/endpoints/pool'
|
||||
import type { ClaudeCodeAdvancedConfig, EndpointAPIKey, PoolAdvancedConfig, ProviderWithEndpointsSummary } from '@/api/endpoints/types/provider'
|
||||
import type {
|
||||
ClaudeCodeAdvancedConfig,
|
||||
EndpointAPIKey,
|
||||
PoolAdvancedConfig,
|
||||
ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints/types/provider'
|
||||
import { getProvider, updateProvider } from '@/api/endpoints'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import PoolSchedulingDialog from '@/features/pool/components/PoolSchedulingDialog.vue'
|
||||
@@ -1174,6 +1225,7 @@ import OAuthKeyEditDialog from '@/features/providers/components/OAuthKeyEditDial
|
||||
import OAuthAccountDialog from '@/features/providers/components/OAuthAccountDialog.vue'
|
||||
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
|
||||
import { isAccountLevelBlockReason, classifyAccountBlockLabel, cleanAccountBlockReason } from '@/utils/accountBlock'
|
||||
import { formatOAuthIdentityShort, getPrimaryOAuthOrganizationTitle, getOAuthOrganizationsTooltip } from '@/utils/oauthIdentity'
|
||||
|
||||
const { success, error: showError, warning: showWarning } = useToast()
|
||||
const { confirm } = useConfirm()
|
||||
@@ -1717,7 +1769,11 @@ function toEndpointApiKey(key: PoolKeyDetail): EndpointAPIKey {
|
||||
model_include_patterns: key.model_include_patterns || [],
|
||||
model_exclude_patterns: key.model_exclude_patterns || [],
|
||||
oauth_expires_at: key.oauth_expires_at ?? null,
|
||||
oauth_email: null,
|
||||
oauth_plan_type: key.oauth_plan_type ?? null,
|
||||
oauth_account_id: key.oauth_account_id ?? null,
|
||||
oauth_account_user_id: key.oauth_account_user_id ?? null,
|
||||
oauth_organizations: key.oauth_organizations ?? [],
|
||||
oauth_invalid_at: key.oauth_invalid_at ?? null,
|
||||
oauth_invalid_reason: key.oauth_invalid_reason ?? null,
|
||||
proxy: key.proxy ?? null,
|
||||
|
||||
Reference in New Issue
Block a user