Merge pull request #2571 from sherlockwhite/fix/openai-account-email-and-usage-refresh
fix(accounts): show email and fix usage refresh for OpenAI OAuth accounts
This commit is contained in:
commit
21ae52c01f
@ -1643,7 +1643,7 @@ func (h *OAuthHandler) SetupTokenCookieAuth(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUsage handles getting account usage information
|
// GetUsage handles getting account usage information
|
||||||
// GET /api/v1/admin/accounts/:id/usage?source=passive|active
|
// GET /api/v1/admin/accounts/:id/usage?source=passive|active&force=true
|
||||||
func (h *AccountHandler) GetUsage(c *gin.Context) {
|
func (h *AccountHandler) GetUsage(c *gin.Context) {
|
||||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1652,12 +1652,13 @@ func (h *AccountHandler) GetUsage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
source := c.DefaultQuery("source", "active")
|
source := c.DefaultQuery("source", "active")
|
||||||
|
force := c.Query("force") == "true"
|
||||||
|
|
||||||
var usage *service.UsageInfo
|
var usage *service.UsageInfo
|
||||||
if source == "passive" {
|
if source == "passive" {
|
||||||
usage, err = h.accountUsageService.GetPassiveUsage(c.Request.Context(), accountID)
|
usage, err = h.accountUsageService.GetPassiveUsage(c.Request.Context(), accountID)
|
||||||
} else {
|
} else {
|
||||||
usage, err = h.accountUsageService.GetUsage(c.Request.Context(), accountID)
|
usage, err = h.accountUsageService.GetUsage(c.Request.Context(), accountID, force)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
|
|||||||
@ -295,14 +295,16 @@ func NewAccountUsageService(
|
|||||||
// OAuth账号: 调用Anthropic API获取真实数据(需要profile scope),API响应缓存10分钟,窗口统计缓存1分钟
|
// OAuth账号: 调用Anthropic API获取真实数据(需要profile scope),API响应缓存10分钟,窗口统计缓存1分钟
|
||||||
// Setup Token账号: 根据session_window推算5h窗口,7d数据不可用(没有profile scope)
|
// Setup Token账号: 根据session_window推算5h窗口,7d数据不可用(没有profile scope)
|
||||||
// API Key账号: 不支持usage查询
|
// API Key账号: 不支持usage查询
|
||||||
func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*UsageInfo, error) {
|
func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64, force ...bool) (*UsageInfo, error) {
|
||||||
|
forceProbe := len(force) > 0 && force[0]
|
||||||
|
|
||||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get account failed: %w", err)
|
return nil, fmt.Errorf("get account failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account.Platform == PlatformOpenAI && account.Type == AccountTypeOAuth {
|
if account.Platform == PlatformOpenAI && account.Type == AccountTypeOAuth {
|
||||||
usage, err := s.getOpenAIUsage(ctx, account)
|
usage, err := s.getOpenAIUsage(ctx, account, forceProbe)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.tryClearRecoverableAccountError(ctx, account)
|
s.tryClearRecoverableAccountError(ctx, account)
|
||||||
}
|
}
|
||||||
@ -492,7 +494,7 @@ func (s *AccountUsageService) syncActiveToPassive(ctx context.Context, accountID
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Account) (*UsageInfo, error) {
|
func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Account, force bool) (*UsageInfo, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
usage := &UsageInfo{UpdatedAt: &now}
|
usage := &UsageInfo{UpdatedAt: &now}
|
||||||
|
|
||||||
@ -507,7 +509,7 @@ func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Accou
|
|||||||
usage.SevenDay = progress
|
usage.SevenDay = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldRefreshOpenAICodexSnapshot(account, usage, now) && s.shouldProbeOpenAICodexSnapshot(account.ID, now) {
|
if (force || shouldRefreshOpenAICodexSnapshot(account, usage, now)) && s.shouldProbeOpenAICodexSnapshot(account.ID, now, force) {
|
||||||
if updates, err := s.probeOpenAICodexSnapshot(ctx, account); err == nil && len(updates) > 0 {
|
if updates, err := s.probeOpenAICodexSnapshot(ctx, account); err == nil && len(updates) > 0 {
|
||||||
mergeAccountExtra(account, updates)
|
mergeAccountExtra(account, updates)
|
||||||
if usage.UpdatedAt == nil {
|
if usage.UpdatedAt == nil {
|
||||||
@ -577,13 +579,16 @@ func isOpenAICodexSnapshotStale(account *Account, now time.Time) bool {
|
|||||||
return now.Sub(ts) >= openAIProbeCacheTTL
|
return now.Sub(ts) >= openAIProbeCacheTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AccountUsageService) shouldProbeOpenAICodexSnapshot(accountID int64, now time.Time) bool {
|
func (s *AccountUsageService) shouldProbeOpenAICodexSnapshot(accountID int64, now time.Time, force ...bool) bool {
|
||||||
if s == nil || s.cache == nil || accountID <= 0 {
|
if s == nil || s.cache == nil || accountID <= 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if cached, ok := s.cache.openAIProbeCache.Load(accountID); ok {
|
forceProbe := len(force) > 0 && force[0]
|
||||||
if ts, ok := cached.(time.Time); ok && now.Sub(ts) < openAIProbeCacheTTL {
|
if !forceProbe {
|
||||||
return false
|
if cached, ok := s.cache.openAIProbeCache.Load(accountID); ok {
|
||||||
|
if ts, ok := cached.(time.Time); ok && now.Sub(ts) < openAIProbeCacheTTL {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.cache.openAIProbeCache.Store(accountID, now)
|
s.cache.openAIProbeCache.Store(accountID, now)
|
||||||
|
|||||||
@ -140,7 +140,7 @@ func TestAccountUsageService_GetOpenAIUsage_DoesNotPromoteCodexExtraToRateLimit(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
usage, err := svc.getOpenAIUsage(context.Background(), account)
|
usage, err := svc.getOpenAIUsage(context.Background(), account, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getOpenAIUsage() error = %v", err)
|
t.Fatalf("getOpenAIUsage() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -232,9 +232,12 @@ export async function clearError(id: number): Promise<Account> {
|
|||||||
* @param id - Account ID
|
* @param id - Account ID
|
||||||
* @returns Account usage info
|
* @returns Account usage info
|
||||||
*/
|
*/
|
||||||
export async function getUsage(id: number, source?: 'passive' | 'active'): Promise<AccountUsageInfo> {
|
export async function getUsage(id: number, source?: 'passive' | 'active', force?: boolean): Promise<AccountUsageInfo> {
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
if (source) params.source = source
|
||||||
|
if (force) params.force = 'true'
|
||||||
const { data } = await apiClient.get<AccountUsageInfo>(`/admin/accounts/${id}/usage`, {
|
const { data } = await apiClient.get<AccountUsageInfo>(`/admin/accounts/${id}/usage`, {
|
||||||
params: source ? { source } : undefined
|
params: Object.keys(params).length > 0 ? params : undefined
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,6 +126,30 @@
|
|||||||
:show-now-when-idle="true"
|
:show-now-when-idle="true"
|
||||||
color="emerald"
|
color="emerald"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center gap-1.5 mt-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[9px] font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/30 transition-colors"
|
||||||
|
:disabled="activeQueryLoading"
|
||||||
|
@click="loadActiveUsage"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-2.5 w-2.5"
|
||||||
|
:class="{ 'animate-spin': activeQueryLoading }"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ t('admin.accounts.usageWindow.activeQuery') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="loading" class="space-y-1.5">
|
<div v-else-if="loading" class="space-y-1.5">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@ -1070,7 +1094,7 @@ const attachVisibilityObserver = () => {
|
|||||||
const loadActiveUsage = async () => {
|
const loadActiveUsage = async () => {
|
||||||
activeQueryLoading.value = true
|
activeQueryLoading.value = true
|
||||||
try {
|
try {
|
||||||
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, 'active')
|
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, 'active', true)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to load active usage:', e)
|
console.error('Failed to load active usage:', e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -214,11 +214,11 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="row.extra?.email_address"
|
v-if="row.extra?.email_address || row.extra?.email || row.credentials?.email"
|
||||||
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]"
|
class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]"
|
||||||
:title="row.extra.email_address"
|
:title="String(row.extra?.email_address || row.extra?.email || row.credentials?.email)"
|
||||||
>
|
>
|
||||||
{{ row.extra.email_address }}
|
{{ row.extra?.email_address || row.extra?.email || row.credentials?.email }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user