526 lines
No EOL
19 KiB
JavaScript
526 lines
No EOL
19 KiB
JavaScript
/**
|
|
* 用量查询服务
|
|
* 用于处理各个提供商的授权文件用量查询
|
|
*/
|
|
|
|
import { getProviderPoolManager } from './service-manager.js';
|
|
import { serviceInstances } from './adapter.js';
|
|
import { MODEL_PROVIDER } from './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;
|
|
} |