mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
fix(ui): resolve cliproxy account stats by provider
This commit is contained in:
1 parent
75ccbb3ad1
commit
1edc10362a
6 files changed
+104
-14
No files matched your search
@@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { CheckCircle2, AlertCircle, XCircle, MinusCircle, RefreshCw, Clock } from 'lucide-react';
|
import { CheckCircle2, AlertCircle, XCircle, MinusCircle, RefreshCw, Clock } from 'lucide-react';
|
||||||
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||||
import { useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
import { useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
||||||
|
import { getAccountStats } from '@/lib/cliproxy-account-stats';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -155,8 +156,7 @@ export function CredentialHealthList() {
|
|||||||
const credentials =
|
const credentials =
|
||||||
authData?.authStatus.flatMap((status) =>
|
authData?.authStatus.flatMap((status) =>
|
||||||
(status.accounts ?? []).map((account) => {
|
(status.accounts ?? []).map((account) => {
|
||||||
const accountKey = account.email || account.id;
|
const runtimeLastUsed = getAccountStats(stats, account)?.lastUsedAt;
|
||||||
const runtimeLastUsed = stats?.accountStats?.[accountKey]?.lastUsedAt;
|
|
||||||
return {
|
return {
|
||||||
name: account.id,
|
name: account.id,
|
||||||
provider: status.provider,
|
provider: status.provider,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
isClaudeQuotaResult,
|
isClaudeQuotaResult,
|
||||||
isCodexQuotaResult,
|
isCodexQuotaResult,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
|
import { getAccountStats } from '@/lib/cliproxy-account-stats';
|
||||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
||||||
import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
|
import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
|
||||||
@@ -124,7 +125,7 @@ export function AccountItem({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get last used time from runtime stats (more accurate than file)
|
// Get last used time from runtime stats (more accurate than file)
|
||||||
const runtimeLastUsed = stats?.accountStats?.[account.email || account.id]?.lastUsedAt;
|
const runtimeLastUsed = getAccountStats(stats, account)?.lastUsedAt;
|
||||||
const wasRecentlyUsed = isRecentlyUsed(runtimeLastUsed);
|
const wasRecentlyUsed = isRecentlyUsed(runtimeLastUsed);
|
||||||
|
|
||||||
// Use shared utility functions for provider-specific quota handling
|
// Use shared utility functions for provider-specific quota handling
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
|
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||||
import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
|
import { useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
||||||
import { getProviderDisplayName } from '@/lib/provider-config';
|
import { getProviderDisplayName } from '@/lib/provider-config';
|
||||||
|
import { getAccountStats } from '@/lib/cliproxy-account-stats';
|
||||||
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
||||||
import type { AccountRow, ProviderStats } from './types';
|
import type { AccountRow, ProviderStats } from './types';
|
||||||
import { ACCOUNT_COLORS } from './utils';
|
import { ACCOUNT_COLORS } from './utils';
|
||||||
@@ -44,12 +45,6 @@ export function useAuthMonitorData(): AuthMonitorData {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [dataUpdatedAt]);
|
}, [dataUpdatedAt]);
|
||||||
|
|
||||||
// Build a map of account email -> usage stats from CLIProxy
|
|
||||||
const accountStatsMap = useMemo(() => {
|
|
||||||
if (!statsData?.accountStats) return new Map<string, AccountUsageStats>();
|
|
||||||
return new Map(Object.entries(statsData.accountStats));
|
|
||||||
}, [statsData?.accountStats]);
|
|
||||||
|
|
||||||
// Transform auth status data into account rows
|
// Transform auth status data into account rows
|
||||||
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
|
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
|
||||||
if (!data?.authStatus) {
|
if (!data?.authStatus) {
|
||||||
@@ -80,8 +75,7 @@ export function useAuthMonitorData(): AuthMonitorData {
|
|||||||
if (!providerData) return;
|
if (!providerData) return;
|
||||||
|
|
||||||
status.accounts?.forEach((account: OAuthAccount) => {
|
status.accounts?.forEach((account: OAuthAccount) => {
|
||||||
const accountEmail = account.email || account.id;
|
const realStats = getAccountStats(statsData, account);
|
||||||
const realStats = accountStatsMap.get(accountEmail);
|
|
||||||
const success = realStats?.successCount ?? 0;
|
const success = realStats?.successCount ?? 0;
|
||||||
const failure = realStats?.failureCount ?? 0;
|
const failure = realStats?.failureCount ?? 0;
|
||||||
tSuccess += success;
|
tSuccess += success;
|
||||||
@@ -132,7 +126,7 @@ export function useAuthMonitorData(): AuthMonitorData {
|
|||||||
totalRequests: tSuccess + tFailure,
|
totalRequests: tSuccess + tFailure,
|
||||||
providerStats: providerStatsArr,
|
providerStats: providerStatsArr,
|
||||||
};
|
};
|
||||||
}, [data?.authStatus, accountStatsMap]);
|
}, [data?.authStatus, statsData]);
|
||||||
|
|
||||||
const overallSuccessRate =
|
const overallSuccessRate =
|
||||||
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
|
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import type { UnifiedQuotaResult } from '@/lib/utils';
|
|||||||
|
|
||||||
/** Per-account usage statistics */
|
/** Per-account usage statistics */
|
||||||
export interface AccountUsageStats {
|
export interface AccountUsageStats {
|
||||||
/** Account email or identifier */
|
/** Provider-qualified lookup key (for example: "codex:user@example.com") */
|
||||||
|
accountKey?: string;
|
||||||
|
/** Canonical provider name reported by CLIProxyAPI */
|
||||||
|
provider?: string;
|
||||||
|
/** Raw account email or identifier */
|
||||||
source: string;
|
source: string;
|
||||||
/** Number of successful requests */
|
/** Number of successful requests */
|
||||||
successCount: number;
|
successCount: number;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { OAuthAccount } from '@/lib/api-client';
|
||||||
|
import type { AccountUsageStats, CliproxyStats } from '@/hooks/use-cliproxy-stats';
|
||||||
|
|
||||||
|
export function buildQualifiedAccountStatsKey(provider: string, source: string): string {
|
||||||
|
return `${provider.trim().toLowerCase()}:${source.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccountStats(
|
||||||
|
stats: Pick<CliproxyStats, 'accountStats'> | null | undefined,
|
||||||
|
account: Pick<OAuthAccount, 'provider' | 'email' | 'id'>
|
||||||
|
): AccountUsageStats | undefined {
|
||||||
|
const source = account.email || account.id;
|
||||||
|
const qualifiedKey = buildQualifiedAccountStatsKey(account.provider, source);
|
||||||
|
|
||||||
|
return stats?.accountStats?.[qualifiedKey] ?? stats?.accountStats?.[source];
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { getAccountStats } from '../../../../src/lib/cliproxy-account-stats';
|
||||||
|
import type { CliproxyStats } from '../../../../src/hooks/use-cliproxy-stats';
|
||||||
|
import type { OAuthAccount } from '../../../../src/lib/api-client';
|
||||||
|
|
||||||
|
describe('getAccountStats', () => {
|
||||||
|
const baseAccount = {
|
||||||
|
id: 'shared@example.com',
|
||||||
|
email: 'shared@example.com',
|
||||||
|
isDefault: true,
|
||||||
|
tokenFile: 'shared.json',
|
||||||
|
createdAt: '2026-03-26T00:00:00.000Z',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
it('prefers provider-qualified stats when the same email exists across providers', () => {
|
||||||
|
const stats = {
|
||||||
|
accountStats: {
|
||||||
|
'codex:shared@example.com': {
|
||||||
|
accountKey: 'codex:shared@example.com',
|
||||||
|
provider: 'codex',
|
||||||
|
source: 'shared@example.com',
|
||||||
|
successCount: 11,
|
||||||
|
failureCount: 1,
|
||||||
|
totalTokens: 0,
|
||||||
|
lastUsedAt: '2026-03-26T10:00:00.000Z',
|
||||||
|
},
|
||||||
|
'gemini:shared@example.com': {
|
||||||
|
accountKey: 'gemini:shared@example.com',
|
||||||
|
provider: 'gemini',
|
||||||
|
source: 'shared@example.com',
|
||||||
|
successCount: 3,
|
||||||
|
failureCount: 2,
|
||||||
|
totalTokens: 0,
|
||||||
|
lastUsedAt: '2026-03-26T11:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Pick<CliproxyStats, 'accountStats'>;
|
||||||
|
|
||||||
|
const codexAccount: OAuthAccount = { ...baseAccount, provider: 'codex' };
|
||||||
|
const geminiAccount: OAuthAccount = { ...baseAccount, provider: 'gemini' };
|
||||||
|
|
||||||
|
expect(getAccountStats(stats, codexAccount)).toMatchObject({
|
||||||
|
successCount: 11,
|
||||||
|
failureCount: 1,
|
||||||
|
provider: 'codex',
|
||||||
|
});
|
||||||
|
expect(getAccountStats(stats, geminiAccount)).toMatchObject({
|
||||||
|
successCount: 3,
|
||||||
|
failureCount: 2,
|
||||||
|
provider: 'gemini',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to legacy raw-source keys for older stats payloads', () => {
|
||||||
|
const stats = {
|
||||||
|
accountStats: {
|
||||||
|
'shared@example.com': {
|
||||||
|
source: 'shared@example.com',
|
||||||
|
successCount: 7,
|
||||||
|
failureCount: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
lastUsedAt: '2026-03-26T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Pick<CliproxyStats, 'accountStats'>;
|
||||||
|
|
||||||
|
const account: OAuthAccount = { ...baseAccount, provider: 'codex' };
|
||||||
|
|
||||||
|
expect(getAccountStats(stats, account)).toMatchObject({
|
||||||
|
source: 'shared@example.com',
|
||||||
|
successCount: 7,
|
||||||
|
failureCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user