Files
claude-code-usage-dashboard/server/api-client.ts
YouXam c6bdca6379 init
2025-08-27 20:33:03 +08:00

154 lines
3.8 KiB
TypeScript

interface LoginResponse {
success: boolean;
token: string;
expiresIn: number;
}
interface ApiKeysResponse {
success: boolean;
data: Array<{
id: string;
name: string;
usage: {
total: {
cost: number;
tokens: number;
inputTokens: number;
outputTokens: number;
cacheCreateTokens: number;
cacheReadTokens: number;
requests: number;
formattedCost: string;
};
};
[key: string]: any;
}>;
}
interface KeyIdResponse {
success: boolean;
data: {
id: string;
};
}
export class ApiClient {
private baseUrl: string;
private username: string;
private password: string;
private token: string | null = null;
private expiresAt: number = 0;
private readonly skewMs = 10000; // 10 seconds
constructor() {
this.baseUrl = process.env.BASE_URL || '';
this.username = process.env.ADMIN_USERNAME || '';
this.password = process.env.ADMIN_PASSWORD || '';
if (!this.baseUrl || !this.username || !this.password) {
throw new Error('Missing required environment variables: BASE_URL, ADMIN_USERNAME, ADMIN_PASSWORD');
}
}
private async ensureValidToken(): Promise<void> {
if (this.token && Date.now() + this.skewMs < this.expiresAt) {
return;
}
await this.login();
}
private async login(): Promise<void> {
let retries = 0;
const maxRetries = 3;
while (retries < maxRetries) {
try {
const response = await fetch(`${this.baseUrl}/web/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: this.username,
password: this.password,
}),
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status} ${response.statusText}`);
}
const data = await response.json() as LoginResponse;
if (!data.success || !data.token) {
throw new Error('Login response invalid');
}
this.token = data.token;
this.expiresAt = Date.now() + data.expiresIn;
return;
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error(`Login failed after ${maxRetries} retries: ${error}`);
}
// Exponential backoff
const delay = Math.pow(2, retries - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
async getCurrentCosts(): Promise<ApiKeysResponse['data']> {
await this.ensureValidToken();
const response = await fetch(`${this.baseUrl}/admin/api-keys?timeRange=all`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch current costs: ${response.status} ${response.statusText}`);
}
const data = await response.json() as ApiKeysResponse;
if (!data.success || !Array.isArray(data.data)) {
throw new Error('Invalid response format from admin/api-keys');
}
return data.data;
}
async getKeyId(apiKey: string): Promise<string> {
const response = await fetch(`${this.baseUrl}/apiStats/api/get-key-id`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
apiKey,
}),
});
if (!response.ok) {
throw new Error(`Failed to get key ID: ${response.status} ${response.statusText}`);
}
const data = await response.json() as KeyIdResponse;
if (!data.success || !data.data?.id) {
throw new Error('Invalid API key or response format');
}
return data.data.id;
}
}
export const apiClient = new ApiClient();