mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-04 10:16:34 +00:00
feat(cliproxy): support duplicate-email codex accounts
- keep Codex team and personal auth files as separate identities - resolve quota and live monitor stats by token file-backed account id - surface duplicate-aware account labels across the dashboard and variant UI
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
isClaudeQuotaResult,
|
||||
isCodexQuotaResult,
|
||||
} from '@/lib/utils';
|
||||
import { formatAccountVariantLabel } from '@/lib/account-identity';
|
||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
import {
|
||||
GripVertical,
|
||||
@@ -188,6 +189,7 @@ export function AccountCard({
|
||||
account.tier &&
|
||||
account.tier !== 'unknown' &&
|
||||
account.tier !== 'free';
|
||||
const variantLabel = formatAccountVariantLabel(account.id, account.email);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -226,6 +228,11 @@ export function AccountCard({
|
||||
>
|
||||
{cleanEmail(account.email)}
|
||||
</span>
|
||||
{variantLabel && (
|
||||
<span className="text-[7px] font-bold uppercase tracking-wide px-1 py-px rounded shrink-0 bg-muted text-muted-foreground">
|
||||
{variantLabel}
|
||||
</span>
|
||||
)}
|
||||
{showTierBadge && (
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@@ -18,6 +18,7 @@ import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||
import { usePrivacy } from '@/contexts/privacy-context';
|
||||
import { formatAccountDisplayName } from '@/lib/account-identity';
|
||||
import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
|
||||
import { isDeniedAgyModelId } from '@/lib/utils';
|
||||
|
||||
@@ -220,7 +221,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
|
||||
<option value="">{t('cliproxyDialog.useDefaultAccount')}</option>
|
||||
{providerAccounts.map((acc) => (
|
||||
<option key={acc.id} value={acc.id}>
|
||||
{privacyMode ? '••••••' : acc.email || acc.id}
|
||||
{privacyMode ? '••••••' : formatAccountDisplayName(acc.id, acc.email)}
|
||||
{acc.isDefault ? ` ${t('cliproxyDialog.defaultSuffix')}` : ''}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
isClaudeQuotaResult,
|
||||
isCodexQuotaResult,
|
||||
} from '@/lib/utils';
|
||||
import { formatAccountVariantLabel } from '@/lib/account-identity';
|
||||
import { getAccountStats } from '@/lib/cliproxy-account-stats';
|
||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats';
|
||||
@@ -187,6 +188,7 @@ export function AccountItem({
|
||||
: failureInfo?.tone === 'destructive'
|
||||
? 'border-destructive/50 text-destructive'
|
||||
: 'border-muted-foreground/50 text-muted-foreground';
|
||||
const variantLabel = formatAccountVariantLabel(account.id, account.email);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -274,6 +276,11 @@ export function AccountItem({
|
||||
>
|
||||
{account.email || account.id}
|
||||
</span>
|
||||
{variantLabel && (
|
||||
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
||||
{variantLabel}
|
||||
</Badge>
|
||||
)}
|
||||
{account.isDefault && (
|
||||
<Badge variant="secondary" className="text-[10px] h-4 px-1.5 gap-0.5">
|
||||
<Star className="w-2.5 h-2.5 fill-current" />
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { ChevronRight, AlertTriangle } from 'lucide-react';
|
||||
import { formatAccountDisplayName } from '@/lib/account-identity';
|
||||
import { cn, STATUS_COLORS } from '@/lib/utils';
|
||||
import { PROVIDER_COLORS } from '@/lib/provider-config';
|
||||
import { ProviderIcon } from '@/components/shared/provider-icon';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { ProviderStats } from '../types';
|
||||
import { getSuccessRate, cleanEmail } from '../utils';
|
||||
import { getSuccessRate } from '../utils';
|
||||
import { InlineStatsBadge } from './inline-stats-badge';
|
||||
|
||||
interface ProviderCardProps {
|
||||
@@ -112,7 +113,7 @@ export function ProviderCard({
|
||||
<div
|
||||
className={cn('w-2 h-2 rounded-full', acc.paused && 'opacity-50')}
|
||||
style={{ backgroundColor: acc.color }}
|
||||
title={privacyMode ? '••••••' : cleanEmail(acc.email)}
|
||||
title={privacyMode ? '••••••' : formatAccountDisplayName(acc.id, acc.email)}
|
||||
/>
|
||||
{isMissingProjectId && (
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronRight, ArrowLeft, User, ExternalLink } from 'lucide-react';
|
||||
import { formatAccountDisplayName } from '@/lib/account-identity';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
import type { AccountStepProps } from '../types';
|
||||
@@ -37,7 +38,7 @@ export function AccountStep({
|
||||
</div>
|
||||
<div>
|
||||
<div className={cn('font-medium', privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||
{acc.email || acc.id}
|
||||
{formatAccountDisplayName(acc.id, acc.email)}
|
||||
</div>
|
||||
{acc.isDefault && (
|
||||
<div className="text-xs text-muted-foreground">Default account</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { formatAccountDisplayName } from '@/lib/account-identity';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -63,7 +64,7 @@ export function VariantStep({
|
||||
<span>
|
||||
{t('setupVariant.using')}{' '}
|
||||
<span className={cn(privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||
{selectedAccount.email || selectedAccount.id}
|
||||
{formatAccountDisplayName(selectedAccount.id, selectedAccount.email)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
function formatVariantPart(part: string): string {
|
||||
const normalized = part.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (normalized) {
|
||||
case 'team':
|
||||
return 'Team';
|
||||
case 'free':
|
||||
return 'Free';
|
||||
case 'plus':
|
||||
return 'Plus';
|
||||
case 'pro':
|
||||
return 'Pro';
|
||||
default:
|
||||
return /^[a-f0-9]{8}$/i.test(normalized)
|
||||
? normalized
|
||||
: normalized
|
||||
.split(/[._-]+/)
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment[0]?.toUpperCase() + segment.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
export function extractAccountVariantKey(accountId: string, email?: string): string | null {
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefix = `${email}#`;
|
||||
return accountId.startsWith(prefix) ? accountId.slice(prefix.length) : null;
|
||||
}
|
||||
|
||||
export function formatAccountVariantLabel(accountId: string, email?: string): string | null {
|
||||
const variantKey = extractAccountVariantKey(accountId, email);
|
||||
if (!variantKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = variantKey.split('-').filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const genericSuffix = parts[parts.length - 1];
|
||||
if (['team', 'free', 'plus', 'pro'].includes(genericSuffix)) {
|
||||
return [formatVariantPart(genericSuffix), ...parts.slice(0, -1).map(formatVariantPart)]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
return parts.map(formatVariantPart).filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
export function formatAccountDisplayName(accountId: string, email?: string): string {
|
||||
const base = email || accountId;
|
||||
const variantLabel = formatAccountVariantLabel(accountId, email);
|
||||
return variantLabel ? `${base} (${variantLabel})` : base;
|
||||
}
|
||||
@@ -9,8 +9,17 @@ 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);
|
||||
const sources = Array.from(
|
||||
new Set([account.id, account.email].filter((value): value is string => Boolean(value?.trim())))
|
||||
);
|
||||
|
||||
return stats?.accountStats?.[qualifiedKey] ?? stats?.accountStats?.[source];
|
||||
for (const source of sources) {
|
||||
const qualifiedKey = buildQualifiedAccountStatsKey(account.provider, source);
|
||||
const match = stats?.accountStats?.[qualifiedKey] ?? stats?.accountStats?.[source];
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user