feat: fetch Claude account profiles (plan type) on startup
- Call /api/oauth/profile for each token, cache in memory - Display plan type (max/pro/free) next to Claude account names - Profiles fetched once on startup, no periodic refresh needed
This commit is contained in:
@@ -8,6 +8,8 @@
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { getCurrentToken, getAllTokens } from './token-rotation.js';
|
import { getCurrentToken, getAllTokens } from './token-rotation.js';
|
||||||
|
|
||||||
|
const PROFILE_ENDPOINT = 'https://api.anthropic.com/api/oauth/profile';
|
||||||
|
|
||||||
export interface ClaudeUsageData {
|
export interface ClaudeUsageData {
|
||||||
five_hour?: { utilization: number; resets_at: string };
|
five_hour?: { utilization: number; resets_at: string };
|
||||||
seven_day?: { utilization: number; resets_at: string };
|
seven_day?: { utilization: number; resets_at: string };
|
||||||
@@ -99,6 +101,66 @@ export async function fetchClaudeUsage(): Promise<ClaudeUsageData | null> {
|
|||||||
return fetchUsageForToken(token);
|
return fetchUsageForToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClaudeAccountProfile {
|
||||||
|
email: string;
|
||||||
|
planType: string; // "max", "pro", "free"
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileCache = new Map<number, ClaudeAccountProfile>();
|
||||||
|
|
||||||
|
async function fetchProfileForToken(token: string): Promise<ClaudeAccountProfile | null> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const res = await fetch(PROFILE_ENDPOINT, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'anthropic-beta': 'oauth-2025-04-20',
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json() as {
|
||||||
|
account?: { email?: string; has_claude_max?: boolean; has_claude_pro?: boolean };
|
||||||
|
organization?: { organization_type?: string };
|
||||||
|
};
|
||||||
|
const orgType = data.organization?.organization_type || '';
|
||||||
|
const planType = data.account?.has_claude_max ? 'max'
|
||||||
|
: data.account?.has_claude_pro ? 'pro'
|
||||||
|
: orgType.replace('claude_', '') || 'free';
|
||||||
|
return {
|
||||||
|
email: data.account?.email || '?',
|
||||||
|
planType,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch profiles for all Claude tokens (cached, called once on startup).
|
||||||
|
*/
|
||||||
|
export async function fetchAllClaudeProfiles(): Promise<void> {
|
||||||
|
const allTokens = getAllTokens();
|
||||||
|
for (const t of allTokens) {
|
||||||
|
const profile = await fetchProfileForToken(t.token);
|
||||||
|
if (profile) {
|
||||||
|
profileCache.set(t.index, profile);
|
||||||
|
logger.info(
|
||||||
|
{ account: t.index + 1, plan: profile.planType, email: profile.email },
|
||||||
|
`Claude account #${t.index + 1}: ${profile.planType}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClaudeProfile(index: number): ClaudeAccountProfile | undefined {
|
||||||
|
return profileCache.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClaudeAccountUsage {
|
export interface ClaudeAccountUsage {
|
||||||
index: number;
|
index: number;
|
||||||
masked: string;
|
masked: string;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { STATUS_SHOW_ROOMS, USAGE_DASHBOARD_ENABLED } from './config.js';
|
|||||||
import {
|
import {
|
||||||
fetchClaudeUsage as fetchClaudeUsageApi,
|
fetchClaudeUsage as fetchClaudeUsageApi,
|
||||||
fetchAllClaudeUsage,
|
fetchAllClaudeUsage,
|
||||||
|
fetchAllClaudeProfiles,
|
||||||
|
getClaudeProfile,
|
||||||
type ClaudeUsageData,
|
type ClaudeUsageData,
|
||||||
type ClaudeAccountUsage,
|
type ClaudeAccountUsage,
|
||||||
} from './claude-usage.js';
|
} from './claude-usage.js';
|
||||||
@@ -600,12 +602,14 @@ async function buildUsageContent(): Promise<string> {
|
|||||||
if (!u) continue;
|
if (!u) continue;
|
||||||
const h5 = u.five_hour;
|
const h5 = u.five_hour;
|
||||||
const d7 = u.seven_day;
|
const d7 = u.seven_day;
|
||||||
|
const profile = getClaudeProfile(account.index);
|
||||||
|
const planSuffix = profile ? ` ${profile.planType}` : '';
|
||||||
const label =
|
const label =
|
||||||
claudeAccounts.length > 1
|
claudeAccounts.length > 1
|
||||||
? `Claude${account.index + 1}${account.isActive ? '*' : ''}${account.isRateLimited ? '!' : ''}`
|
? `Claude${account.index + 1}${account.isActive ? '*' : ''}${account.isRateLimited ? '!' : ''}${planSuffix}`
|
||||||
: claudeUsageIsCached
|
: claudeUsageIsCached
|
||||||
? 'Claude*'
|
? `Claude*${planSuffix}`
|
||||||
: 'Claude';
|
: `Claude${planSuffix}`;
|
||||||
rows.push({
|
rows.push({
|
||||||
name: label,
|
name: label,
|
||||||
h5pct: h5
|
h5pct: h5
|
||||||
@@ -863,6 +867,7 @@ export async function startUnifiedDashboard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRenderer) {
|
if (isRenderer) {
|
||||||
|
await fetchAllClaudeProfiles();
|
||||||
await refreshUsageCache();
|
await refreshUsageCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user