aiclient-2-api / src /services /usage-service.js
Jaasomn
Initial deployment
ceb3821
/**
* 用量查询服务
* 用于处理各个提供商的授权文件用量查询
*/
import { getProviderPoolManager } from './service-manager.js';
import { serviceInstances } from '../providers/adapter.js';
import { MODEL_PROVIDER } from '../utils/common.js';
/**
* 用量查询服务类
* 提供统一的接口来查询各提供商的用量信息
*/
export class UsageService {
constructor() {
this.providerHandlers = {
[MODEL_PROVIDER.KIRO_API]: this.getKiroUsage.bind(this),
[MODEL_PROVIDER.GEMINI_CLI]: this.getGeminiUsage.bind(this),
[MODEL_PROVIDER.ANTIGRAVITY]: this.getAntigravityUsage.bind(this),
};
}
/**
* 获取指定提供商的用量信息
* @param {string} providerType - 提供商类型
* @param {string} [uuid] - 可选的提供商实例 UUID
* @returns {Promise<Object>} 用量信息
*/
async getUsage(providerType, uuid = null) {
const handler = this.providerHandlers[providerType];
if (!handler) {
throw new Error(`不支持的提供商类型: ${providerType}`);
}
return handler(uuid);
}
/**
* 获取所有提供商的用量信息
* @returns {Promise<Object>} 所有提供商的用量信息
*/
async getAllUsage() {
const results = {};
const poolManager = getProviderPoolManager();
for (const [providerType, handler] of Object.entries(this.providerHandlers)) {
try {
// 检查是否有号池配置
if (poolManager) {
const pools = poolManager.getProviderPools(providerType);
if (pools && pools.length > 0) {
results[providerType] = [];
for (const pool of pools) {
try {
const usage = await handler(pool.uuid);
results[providerType].push({
uuid: pool.uuid,
usage
});
} catch (error) {
results[providerType].push({
uuid: pool.uuid,
error: error.message
});
}
}
}
}
// 如果没有号池配置,尝试获取单个实例的用量
if (!results[providerType] || results[providerType].length === 0) {
const usage = await handler(null);
results[providerType] = [{ uuid: 'default', usage }];
}
} catch (error) {
results[providerType] = [{ uuid: 'default', error: error.message }];
}
}
return results;
}
/**
* 获取 Kiro 提供商的用量信息
* @param {string} [uuid] - 可选的提供商实例 UUID
* @returns {Promise<Object>} Kiro 用量信息
*/
async getKiroUsage(uuid = null) {
const providerKey = uuid ? MODEL_PROVIDER.KIRO_API + uuid : MODEL_PROVIDER.KIRO_API;
const adapter = serviceInstances[providerKey];
if (!adapter) {
throw new Error(`Kiro 服务实例未找到: ${providerKey}`);
}
// 使用适配器的 getUsageLimits 方法
if (typeof adapter.getUsageLimits === 'function') {
return adapter.getUsageLimits();
}
// 兼容直接访问 kiroApiService 的情况
if (adapter.kiroApiService && typeof adapter.kiroApiService.getUsageLimits === 'function') {
return adapter.kiroApiService.getUsageLimits();
}
throw new Error(`Kiro 服务实例不支持用量查询: ${providerKey}`);
}
/**
* 获取 Gemini CLI 提供商的用量信息
* @param {string} [uuid] - 可选的提供商实例 UUID
* @returns {Promise<Object>} Gemini 用量信息
*/
async getGeminiUsage(uuid = null) {
const providerKey = uuid ? MODEL_PROVIDER.GEMINI_CLI + uuid : MODEL_PROVIDER.GEMINI_CLI;
const adapter = serviceInstances[providerKey];
if (!adapter) {
throw new Error(`Gemini CLI 服务实例未找到: ${providerKey}`);
}
// 使用适配器的 getUsageLimits 方法
if (typeof adapter.getUsageLimits === 'function') {
return adapter.getUsageLimits();
}
// 兼容直接访问 geminiApiService 的情况
if (adapter.geminiApiService && typeof adapter.geminiApiService.getUsageLimits === 'function') {
return adapter.geminiApiService.getUsageLimits();
}
throw new Error(`Gemini CLI 服务实例不支持用量查询: ${providerKey}`);
}
/**
* 获取 Antigravity 提供商的用量信息
* @param {string} [uuid] - 可选的提供商实例 UUID
* @returns {Promise<Object>} Antigravity 用量信息
*/
async getAntigravityUsage(uuid = null) {
const providerKey = uuid ? MODEL_PROVIDER.ANTIGRAVITY + uuid : MODEL_PROVIDER.ANTIGRAVITY;
const adapter = serviceInstances[providerKey];
if (!adapter) {
throw new Error(`Antigravity 服务实例未找到: ${providerKey}`);
}
// 使用适配器的 getUsageLimits 方法
if (typeof adapter.getUsageLimits === 'function') {
return adapter.getUsageLimits();
}
// 兼容直接访问 antigravityApiService 的情况
if (adapter.antigravityApiService && typeof adapter.antigravityApiService.getUsageLimits === 'function') {
return adapter.antigravityApiService.getUsageLimits();
}
throw new Error(`Antigravity 服务实例不支持用量查询: ${providerKey}`);
}
/**
* 获取支持用量查询的提供商列表
* @returns {Array<string>} 支持的提供商类型列表
*/
getSupportedProviders() {
return Object.keys(this.providerHandlers);
}
}
// 导出单例实例
export const usageService = new UsageService();
/**
* 格式化 Kiro 用量信息为易读格式
* @param {Object} usageData - 原始用量数据
* @returns {Object} 格式化后的用量信息
*/
export function formatKiroUsage(usageData) {
if (!usageData) {
return null;
}
const result = {
// 基本信息
daysUntilReset: usageData.daysUntilReset,
nextDateReset: usageData.nextDateReset ? new Date(usageData.nextDateReset * 1000).toISOString() : null,
// 订阅信息
subscription: null,
// 用户信息
user: null,
// 用量明细
usageBreakdown: []
};
// 解析订阅信息
if (usageData.subscriptionInfo) {
result.subscription = {
title: usageData.subscriptionInfo.subscriptionTitle,
type: usageData.subscriptionInfo.type,
upgradeCapability: usageData.subscriptionInfo.upgradeCapability,
overageCapability: usageData.subscriptionInfo.overageCapability
};
}
// 解析用户信息
if (usageData.userInfo) {
result.user = {
email: usageData.userInfo.email,
userId: usageData.userInfo.userId
};
}
// 解析用量明细
if (usageData.usageBreakdownList && Array.isArray(usageData.usageBreakdownList)) {
for (const breakdown of usageData.usageBreakdownList) {
const item = {
resourceType: breakdown.resourceType,
displayName: breakdown.displayName,
displayNamePlural: breakdown.displayNamePlural,
unit: breakdown.unit,
currency: breakdown.currency,
// 当前用量
currentUsage: breakdown.currentUsageWithPrecision ?? breakdown.currentUsage,
usageLimit: breakdown.usageLimitWithPrecision ?? breakdown.usageLimit,
// 超额信息
currentOverages: breakdown.currentOveragesWithPrecision ?? breakdown.currentOverages,
overageCap: breakdown.overageCapWithPrecision ?? breakdown.overageCap,
overageRate: breakdown.overageRate,
overageCharges: breakdown.overageCharges,
// 下次重置时间
nextDateReset: breakdown.nextDateReset ? new Date(breakdown.nextDateReset * 1000).toISOString() : null,
// 免费试用信息
freeTrial: null,
// 奖励信息
bonuses: []
};
// 解析免费试用信息
if (breakdown.freeTrialInfo) {
item.freeTrial = {
status: breakdown.freeTrialInfo.freeTrialStatus,
currentUsage: breakdown.freeTrialInfo.currentUsageWithPrecision ?? breakdown.freeTrialInfo.currentUsage,
usageLimit: breakdown.freeTrialInfo.usageLimitWithPrecision ?? breakdown.freeTrialInfo.usageLimit,
expiresAt: breakdown.freeTrialInfo.freeTrialExpiry
? new Date(breakdown.freeTrialInfo.freeTrialExpiry * 1000).toISOString()
: null
};
}
// 解析奖励信息
if (breakdown.bonuses && Array.isArray(breakdown.bonuses)) {
for (const bonus of breakdown.bonuses) {
item.bonuses.push({
code: bonus.bonusCode,
displayName: bonus.displayName,
description: bonus.description,
status: bonus.status,
currentUsage: bonus.currentUsage,
usageLimit: bonus.usageLimit,
redeemedAt: bonus.redeemedAt ? new Date(bonus.redeemedAt * 1000).toISOString() : null,
expiresAt: bonus.expiresAt ? new Date(bonus.expiresAt * 1000).toISOString() : null
});
}
}
result.usageBreakdown.push(item);
}
}
return result;
}
/**
* 格式化 Gemini 用量信息为易读格式(映射到 Kiro 数据结构)
* @param {Object} usageData - 原始用量数据
* @returns {Object} 格式化后的用量信息
*/
export function formatGeminiUsage(usageData) {
if (!usageData) {
return null;
}
const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset
/**
* 将 UTC 时间转换为北京时间
* @param {string} utcString - UTC 时间字符串
* @returns {string} 北京时间字符串
*/
function utcToBeijing(utcString) {
try {
if (!utcString) return '--';
const utcDate = new Date(utcString);
const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET);
return beijingTime
.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
.replace(/\//g, '-');
} catch (e) {
return '--';
}
}
const result = {
// 基本信息 - 映射到 Kiro 结构
daysUntilReset: null,
nextDateReset: null,
// 订阅信息
subscription: {
title: 'Gemini CLI OAuth',
type: 'gemini-cli-oauth',
upgradeCapability: null,
overageCapability: null
},
// 用户信息
user: {
email: null,
userId: null
},
// 用量明细
usageBreakdown: []
};
// 解析配额信息
if (usageData.quotaInfo) {
result.subscription.title = usageData.quotaInfo.currentTier || 'Gemini CLI OAuth';
if (usageData.quotaInfo.quotaResetTime) {
result.nextDateReset = usageData.quotaInfo.quotaResetTime;
// 计算距离重置的天数
const resetDate = new Date(usageData.quotaInfo.quotaResetTime);
const now = new Date();
const diffTime = resetDate.getTime() - now.getTime();
result.daysUntilReset = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
}
// 解析模型配额信息
if (usageData.models && typeof usageData.models === 'object') {
for (const [modelName, modelInfo] of Object.entries(usageData.models)) {
// Gemini 返回的数据结构:{ remaining, resetTime, resetTimeRaw }
// remaining 是 0-1 之间的比例值,表示剩余配额百分比
const remainingPercent = typeof modelInfo.remaining === 'number' ? modelInfo.remaining : 1;
const usedPercent = 1 - remainingPercent;
const item = {
resourceType: 'MODEL_USAGE',
displayName: modelInfo.displayName || modelName,
displayNamePlural: modelInfo.displayName || modelName,
unit: 'quota',
currency: null,
// 当前用量 - Gemini 返回的是剩余比例,转换为已用比例(百分比形式)
currentUsage: Math.round(usedPercent * 100),
usageLimit: 100, // 以百分比表示,总量为 100%
// 超额信息
currentOverages: 0,
overageCap: 0,
overageRate: null,
overageCharges: 0,
// 下次重置时间
nextDateReset: modelInfo.resetTimeRaw ? new Date(modelInfo.resetTimeRaw).toISOString() :
(modelInfo.resetTime ? new Date(modelInfo.resetTime).toISOString() : null),
// 免费试用信息
freeTrial: null,
// 奖励信息
bonuses: [],
// 额外的 Gemini 特有信息
modelName: modelName,
inputTokenLimit: modelInfo.inputTokenLimit || 0,
outputTokenLimit: modelInfo.outputTokenLimit || 0,
remaining: remainingPercent,
remainingPercent: Math.round(remainingPercent * 100), // 剩余百分比
resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ?
utcToBeijing(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--',
resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null
};
result.usageBreakdown.push(item);
}
}
return result;
}
/**
* 格式化 Antigravity 用量信息为易读格式(映射到 Kiro 数据结构)
* @param {Object} usageData - 原始用量数据
* @returns {Object} 格式化后的用量信息
*/
export function formatAntigravityUsage(usageData) {
if (!usageData) {
return null;
}
const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset
/**
* 将 UTC 时间转换为北京时间
* @param {string} utcString - UTC 时间字符串
* @returns {string} 北京时间字符串
*/
function utcToBeijing(utcString) {
try {
if (!utcString) return '--';
const utcDate = new Date(utcString);
const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET);
return beijingTime
.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
.replace(/\//g, '-');
} catch (e) {
return '--';
}
}
const result = {
// 基本信息 - 映射到 Kiro 结构
daysUntilReset: null,
nextDateReset: null,
// 订阅信息
subscription: {
title: 'Gemini Antigravity',
type: 'gemini-antigravity',
upgradeCapability: null,
overageCapability: null
},
// 用户信息
user: {
email: null,
userId: null
},
// 用量明细
usageBreakdown: []
};
// 解析配额信息
if (usageData.quotaInfo) {
result.subscription.title = usageData.quotaInfo.currentTier || 'Gemini Antigravity';
if (usageData.quotaInfo.quotaResetTime) {
result.nextDateReset = usageData.quotaInfo.quotaResetTime;
// 计算距离重置的天数
const resetDate = new Date(usageData.quotaInfo.quotaResetTime);
const now = new Date();
const diffTime = resetDate.getTime() - now.getTime();
result.daysUntilReset = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
}
// 解析模型配额信息
if (usageData.models && typeof usageData.models === 'object') {
for (const [modelName, modelInfo] of Object.entries(usageData.models)) {
// Antigravity 返回的数据结构:{ remaining, resetTime, resetTimeRaw }
// remaining 是 0-1 之间的比例值,表示剩余配额百分比
const remainingPercent = typeof modelInfo.remaining === 'number' ? modelInfo.remaining : 1;
const usedPercent = 1 - remainingPercent;
const item = {
resourceType: 'MODEL_USAGE',
displayName: modelInfo.displayName || modelName,
displayNamePlural: modelInfo.displayName || modelName,
unit: 'quota',
currency: null,
// 当前用量 - Antigravity 返回的是剩余比例,转换为已用比例(百分比形式)
currentUsage: usedPercent * 100,
usageLimit: 100, // 以百分比表示,总量为 100%
// 超额信息
currentOverages: 0,
overageCap: 0,
overageRate: null,
overageCharges: 0,
// 下次重置时间
nextDateReset: modelInfo.resetTimeRaw ? new Date(modelInfo.resetTimeRaw).toISOString() :
(modelInfo.resetTime ? new Date(modelInfo.resetTime).toISOString() : null),
// 免费试用信息
freeTrial: null,
// 奖励信息
bonuses: [],
// 额外的 Antigravity 特有信息
modelName: modelName,
inputTokenLimit: modelInfo.inputTokenLimit || 0,
outputTokenLimit: modelInfo.outputTokenLimit || 0,
remaining: remainingPercent,
remainingPercent: remainingPercent * 100, // 剩余百分比
resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ?
utcToBeijing(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--',
resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null
};
result.usageBreakdown.push(item);
}
}
return result;
}