mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
267 lines
7.9 KiB
TypeScript
267 lines
7.9 KiB
TypeScript
import i18n from './i18n';
|
|
|
|
const FREE_PLAN_PARTS = new Set(['free']);
|
|
const PERSONAL_PLAN_PARTS = new Set(['plus', 'pro']);
|
|
const BUSINESS_PLAN_PARTS = new Set(['team']);
|
|
|
|
// Keep variant parsing aligned with src/cliproxy/accounts/email-account-identity.ts.
|
|
// This browser copy stays local because the server module is not bundle-safe for the UI.
|
|
|
|
export type AccountAudience = 'business' | 'free' | 'personal' | 'unknown';
|
|
|
|
export interface AccountIdentityPresentation {
|
|
email: string;
|
|
audience: AccountAudience;
|
|
audienceLabel: string | null;
|
|
detailLabel: string | null;
|
|
compactDetailLabel: string | null;
|
|
inlineLabel: string | null;
|
|
}
|
|
|
|
export interface CodexIdentityBadge {
|
|
audience: AccountAudience;
|
|
label: string | null;
|
|
}
|
|
|
|
function normalizeVariantTokenPart(value: string): string {
|
|
return value
|
|
.trim()
|
|
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, '')
|
|
.replace(/[^a-z0-9._-]+/gi, '-')
|
|
.replace(/-+/g, '-')
|
|
.toLowerCase();
|
|
}
|
|
|
|
export function formatAccountVariantPart(part: string): string {
|
|
const normalized = part.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return '';
|
|
}
|
|
|
|
switch (normalized) {
|
|
case 'team':
|
|
return i18n.t('accountIdentity.team');
|
|
case 'free':
|
|
return i18n.t('accountIdentity.free');
|
|
case 'plus':
|
|
return i18n.t('accountIdentity.plus');
|
|
case 'pro':
|
|
return i18n.t('accountIdentity.pro');
|
|
default:
|
|
return /^[a-f0-9]{8}$/i.test(normalized)
|
|
? normalized
|
|
: normalized
|
|
.split(/[._-]+/)
|
|
.filter(Boolean)
|
|
.map((segment) => segment[0]?.toUpperCase() + segment.slice(1))
|
|
.join(' ');
|
|
}
|
|
}
|
|
|
|
function extractCanonicalEmailFromAccountId(accountId: string): string | null {
|
|
const canonical = accountId.split('#')[0]?.trim();
|
|
return canonical && canonical.includes('@') ? canonical : null;
|
|
}
|
|
|
|
function extractVariantKeyFromAccountId(accountId: string, email?: string): string | null {
|
|
if (!email) {
|
|
return null;
|
|
}
|
|
|
|
const prefix = `${email}#`;
|
|
return accountId.startsWith(prefix) ? accountId.slice(prefix.length) : null;
|
|
}
|
|
|
|
function extractVariantKeyFromTokenFile(tokenFile?: string, email?: string): string | null {
|
|
if (!tokenFile || !email) {
|
|
return null;
|
|
}
|
|
|
|
const fileName = tokenFile.split(/[\\/]/).pop() ?? tokenFile;
|
|
const baseName = fileName.replace(/\.json$/i, '');
|
|
if (!baseName.toLowerCase().startsWith('codex-')) {
|
|
return null;
|
|
}
|
|
const firstDashIndex = baseName.indexOf('-');
|
|
const candidate =
|
|
firstDashIndex > 0 && !baseName.slice(0, firstDashIndex).includes('@')
|
|
? baseName.slice(firstDashIndex + 1)
|
|
: baseName;
|
|
const emailIndex = candidate.toLowerCase().indexOf(email.toLowerCase());
|
|
|
|
if (emailIndex === -1) {
|
|
return null;
|
|
}
|
|
|
|
const before = normalizeVariantTokenPart(candidate.slice(0, emailIndex));
|
|
const after = normalizeVariantTokenPart(candidate.slice(emailIndex + email.length));
|
|
const parts = [before, after].filter(Boolean);
|
|
return parts.length > 0 ? parts.join('-') : null;
|
|
}
|
|
|
|
function formatWorkspaceLabel(parts: string[]): {
|
|
detailLabel: string | null;
|
|
compactDetailLabel: string | null;
|
|
} {
|
|
const workspaceId = parts.find((part) => /^[a-f0-9]{8}$/i.test(part));
|
|
if (workspaceId) {
|
|
return {
|
|
detailLabel: i18n.t('accountIdentity.workspace', { id: workspaceId.toLowerCase() }),
|
|
compactDetailLabel: workspaceId.toLowerCase(),
|
|
};
|
|
}
|
|
|
|
const extraLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · ');
|
|
return {
|
|
detailLabel: extraLabel || null,
|
|
compactDetailLabel: extraLabel || null,
|
|
};
|
|
}
|
|
|
|
function formatAudienceDetail(parts: string[]): string | null {
|
|
const label = parts.map(formatAccountVariantPart).filter(Boolean).join(' · ');
|
|
return label || null;
|
|
}
|
|
|
|
export function extractAccountVariantKey(
|
|
accountId: string,
|
|
email?: string,
|
|
tokenFile?: string
|
|
): string | null {
|
|
const resolvedEmail = email?.trim() || extractCanonicalEmailFromAccountId(accountId) || undefined;
|
|
return (
|
|
extractVariantKeyFromTokenFile(tokenFile, resolvedEmail) ??
|
|
extractVariantKeyFromAccountId(accountId, resolvedEmail)
|
|
);
|
|
}
|
|
|
|
export function getAccountIdentityPresentation(
|
|
accountId: string,
|
|
email?: string,
|
|
tokenFile?: string
|
|
): AccountIdentityPresentation {
|
|
const resolvedEmail = email?.trim() || extractCanonicalEmailFromAccountId(accountId) || accountId;
|
|
const variantKey = extractAccountVariantKey(accountId, resolvedEmail, tokenFile);
|
|
if (!variantKey) {
|
|
return {
|
|
email: resolvedEmail,
|
|
audience: 'unknown',
|
|
audienceLabel: null,
|
|
detailLabel: null,
|
|
compactDetailLabel: null,
|
|
inlineLabel: null,
|
|
};
|
|
}
|
|
|
|
const parts = variantKey.split('-').filter(Boolean);
|
|
if (parts.length === 0) {
|
|
return {
|
|
email: resolvedEmail,
|
|
audience: 'unknown',
|
|
audienceLabel: null,
|
|
detailLabel: null,
|
|
compactDetailLabel: null,
|
|
inlineLabel: null,
|
|
};
|
|
}
|
|
|
|
const suffix = parts[parts.length - 1]?.toLowerCase();
|
|
if (suffix && BUSINESS_PLAN_PARTS.has(suffix)) {
|
|
const workspace = formatWorkspaceLabel(parts.slice(0, -1));
|
|
const inlineLabel = [i18n.t('accountIdentity.business'), workspace.detailLabel]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
return {
|
|
email: resolvedEmail,
|
|
audience: 'business',
|
|
audienceLabel: i18n.t('accountIdentity.business'),
|
|
detailLabel: workspace.detailLabel,
|
|
compactDetailLabel: workspace.compactDetailLabel,
|
|
inlineLabel,
|
|
};
|
|
}
|
|
|
|
if (suffix && FREE_PLAN_PARTS.has(suffix)) {
|
|
const detailLabel = formatAudienceDetail(parts.slice(0, -1));
|
|
const inlineLabel = [i18n.t('accountIdentity.free'), detailLabel].filter(Boolean).join(' · ');
|
|
return {
|
|
email: resolvedEmail,
|
|
audience: 'free',
|
|
audienceLabel: i18n.t('accountIdentity.free'),
|
|
detailLabel,
|
|
compactDetailLabel: detailLabel,
|
|
inlineLabel,
|
|
};
|
|
}
|
|
|
|
if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) {
|
|
const detailLabel = [formatAccountVariantPart(suffix), formatAudienceDetail(parts.slice(0, -1))]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
const inlineLabel = [i18n.t('accountIdentity.personal'), detailLabel]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
return {
|
|
email: resolvedEmail,
|
|
audience: 'personal',
|
|
audienceLabel: i18n.t('accountIdentity.personal'),
|
|
detailLabel: detailLabel || formatAccountVariantPart(suffix),
|
|
compactDetailLabel: detailLabel || formatAccountVariantPart(suffix),
|
|
inlineLabel,
|
|
};
|
|
}
|
|
|
|
const fallbackLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · ');
|
|
return {
|
|
email: resolvedEmail,
|
|
audience: 'unknown',
|
|
audienceLabel: null,
|
|
detailLabel: fallbackLabel || null,
|
|
compactDetailLabel: fallbackLabel || null,
|
|
inlineLabel: fallbackLabel || null,
|
|
};
|
|
}
|
|
|
|
export function formatAccountVariantLabel(
|
|
accountId: string,
|
|
email?: string,
|
|
tokenFile?: string
|
|
): string | null {
|
|
return getAccountIdentityPresentation(accountId, email, tokenFile).inlineLabel;
|
|
}
|
|
|
|
export function getCodexIdentityBadge(
|
|
presentation: Pick<AccountIdentityPresentation, 'audience' | 'detailLabel' | 'compactDetailLabel'>
|
|
): CodexIdentityBadge {
|
|
if (presentation.audience === 'business') {
|
|
return { audience: 'business', label: i18n.t('accountIdentity.business') };
|
|
}
|
|
|
|
if (presentation.audience === 'free') {
|
|
return { audience: 'free', label: i18n.t('accountIdentity.free') };
|
|
}
|
|
|
|
if (presentation.audience === 'personal') {
|
|
return {
|
|
audience: 'personal',
|
|
label:
|
|
presentation.compactDetailLabel ??
|
|
presentation.detailLabel ??
|
|
i18n.t('accountIdentity.personal'),
|
|
};
|
|
}
|
|
|
|
return { audience: 'unknown', label: null };
|
|
}
|
|
|
|
export function formatAccountDisplayName(
|
|
accountId: string,
|
|
email?: string,
|
|
tokenFile?: string
|
|
): string {
|
|
const presentation = getAccountIdentityPresentation(accountId, email, tokenFile);
|
|
return presentation.inlineLabel
|
|
? `${presentation.email} (${presentation.inlineLabel})`
|
|
: presentation.email;
|
|
}
|